feat(ivy): introduce missing-injectable migration for google3 (#30956)
Introduces a new migration schematic for adding the "@Injectable()" decorator to provider classes which are currently not migrated. Previously in ViewEngine, classes which are declared as providers sometimes don't require the "@Injectable()" decorator (e.g. https://stackblitz.com/edit/angular-hpo7gw) With Ivy, provider classes need to be explicitly decorated with the "@Injectable()" decorator if they are declared as providers of a given module. This commit introduces a migration schematic which automatically adds the explicit decorator to places where the decorator is currently missing. The migration logic is designed in a CLI devkit and TSlint agnostic way so that we can also have this migration run as part of a public CLI migration w/ `ng update`. This will be handled as part of a follow-up to reiterate on console output etc. Resolves FW-1371 PR Close #30956
This commit is contained in:

committed by
Kara Erickson

parent
9eefe25e2f
commit
9f2ae5d6ff
@ -11,6 +11,8 @@ ts_library(
|
||||
deps = [
|
||||
"//packages/core/schematics/migrations/injectable-pipe",
|
||||
"//packages/core/schematics/migrations/injectable-pipe/google3",
|
||||
"//packages/core/schematics/migrations/missing-injectable",
|
||||
"//packages/core/schematics/migrations/missing-injectable/google3",
|
||||
"//packages/core/schematics/migrations/move-document",
|
||||
"//packages/core/schematics/migrations/renderer-to-renderer2",
|
||||
"//packages/core/schematics/migrations/renderer-to-renderer2/google3",
|
||||
|
@ -0,0 +1,227 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {readFileSync, writeFileSync} from 'fs';
|
||||
import {dirname, join} from 'path';
|
||||
import * as shx from 'shelljs';
|
||||
import {Configuration, Linter} from 'tslint';
|
||||
|
||||
describe('Google3 missing injectable tslint rule', () => {
|
||||
const rulesDirectory = dirname(
|
||||
require.resolve('../../migrations/missing-injectable/google3/noMissingInjectableRule'));
|
||||
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = join(process.env['TEST_TMPDIR'] !, 'google3-test');
|
||||
shx.mkdir('-p', tmpDir);
|
||||
|
||||
writeFile('tsconfig.json', JSON.stringify({compilerOptions: {module: 'es2015'}}));
|
||||
});
|
||||
|
||||
afterEach(() => shx.rm('-r', tmpDir));
|
||||
|
||||
function runTSLint(fix = true) {
|
||||
const program = Linter.createProgram(join(tmpDir, 'tsconfig.json'));
|
||||
const linter = new Linter({fix, rulesDirectory: [rulesDirectory]}, program);
|
||||
const config = Configuration.parseConfigFile(
|
||||
{rules: {'no-missing-injectable': true}, linterOptions: {typeCheck: true}});
|
||||
|
||||
program.getRootFileNames().forEach(fileName => {
|
||||
linter.lint(fileName, program.getSourceFile(fileName) !.getFullText(), config);
|
||||
});
|
||||
|
||||
return linter;
|
||||
}
|
||||
|
||||
function writeFile(fileName: string, content: string) {
|
||||
writeFileSync(join(tmpDir, fileName), content);
|
||||
}
|
||||
|
||||
function getFile(fileName: string) { return readFileSync(join(tmpDir, fileName), 'utf8'); }
|
||||
|
||||
it('should create proper failures for missing injectable providers', () => {
|
||||
writeFile('index.ts', `
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
export class A {}
|
||||
|
||||
@NgModule({providers: [A]})
|
||||
export class AppModule {}
|
||||
`);
|
||||
|
||||
const linter = runTSLint(false);
|
||||
const failures = linter.getResult().failures;
|
||||
|
||||
expect(failures.length).toBe(2);
|
||||
expect(failures[0].getFailure())
|
||||
.toMatch(/Class needs to be decorated with "@Injectable\(\)".*provided by "AppModule"/);
|
||||
expect(failures[0].getStartPosition().getLineAndCharacter()).toEqual({line: 3, character: 6});
|
||||
expect(failures[1].getFailure()).toMatch(/Import needs to be updated to import.*Injectable/);
|
||||
expect(failures[1].getStartPosition().getLineAndCharacter()).toEqual({line: 1, character: 13});
|
||||
});
|
||||
|
||||
it('should update provider classes which need to be migrated in Ivy', () => {
|
||||
writeFile('/index.ts', `
|
||||
import {Pipe, Directive, Component, NgModule} from '@angular/core';
|
||||
|
||||
@Pipe()
|
||||
export class WithPipe {}
|
||||
|
||||
@Directive()
|
||||
export class WithDirective {}
|
||||
|
||||
@Component()
|
||||
export class WithComponent {}
|
||||
|
||||
export class MyServiceA {}
|
||||
export class MyServiceB {}
|
||||
export class MyServiceC {}
|
||||
export class MyServiceD {}
|
||||
export class MyServiceE {}
|
||||
export class MyServiceF {}
|
||||
export class MyServiceG {}
|
||||
|
||||
@NgModule({providers: [
|
||||
WithPipe,
|
||||
[
|
||||
WithDirective,
|
||||
WithComponent,
|
||||
MyServiceA,
|
||||
]
|
||||
MyServiceB,
|
||||
{provide: MyServiceC},
|
||||
{provide: null, useClass: MyServiceD},
|
||||
{provide: null, useExisting: MyServiceE},
|
||||
{provide: MyServiceF, useFactory: () => null},
|
||||
{provide: MyServiceG, useValue: null},
|
||||
]})
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
|
||||
runTSLint();
|
||||
|
||||
expect(getFile('/index.ts')).toMatch(/'@angular\/core';\s+@Pipe\(\)\s+export class WithPipe/);
|
||||
expect(getFile('/index.ts'))
|
||||
.toMatch(/WithPipe {}\s+@Directive\(\)\s+export class WithDirective/);
|
||||
expect(getFile('/index.ts'))
|
||||
.toMatch(/WithDirective {}\s+@Component\(\)\s+export class WithComponent/);
|
||||
expect(getFile('/index.ts')).toMatch(/@Injectable\(\)\s+export class MyServiceA/);
|
||||
expect(getFile('/index.ts')).toMatch(/@Injectable\(\)\s+export class MyServiceB/);
|
||||
expect(getFile('/index.ts')).toMatch(/@Injectable\(\)\s+export class MyServiceC/);
|
||||
expect(getFile('/index.ts')).toMatch(/@Injectable\(\)\s+export class MyServiceD/);
|
||||
expect(getFile('/index.ts')).toMatch(/@Injectable\(\)\s+export class MyServiceE/);
|
||||
expect(getFile('/index.ts')).toMatch(/MyServiceE {}\s+export class MyServiceF/);
|
||||
expect(getFile('/index.ts')).toMatch(/MyServiceF {}\s+export class MyServiceG/);
|
||||
});
|
||||
|
||||
it('should migrate provider once if referenced in multiple NgModule definitions', () => {
|
||||
writeFile('/index.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
export class ServiceA {}
|
||||
|
||||
@NgModule({providers: [ServiceA]})
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
writeFile('/second.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
import {ServiceA} from './index';
|
||||
|
||||
export class ServiceB {}
|
||||
|
||||
@NgModule({providers: [ServiceA, ServiceB]})
|
||||
export class SecondModule {}
|
||||
`);
|
||||
|
||||
runTSLint();
|
||||
|
||||
expect(getFile('/index.ts'))
|
||||
.toMatch(/@angular\/core';\s+@Injectable\(\)\s+export class ServiceA/);
|
||||
expect(getFile('/index.ts')).toMatch(/{ NgModule, Injectable } from '@angular\/core/);
|
||||
expect(getFile('/second.ts')).toMatch(/@Injectable\(\)\s+export class ServiceB/);
|
||||
expect(getFile('/second.ts')).toMatch(/{ NgModule, Injectable } from '@angular\/core/);
|
||||
});
|
||||
|
||||
it('should warn if a referenced provider could not be resolved', () => {
|
||||
writeFile('/index.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
@NgModule({providers: [NotPresent]})
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
const linter = runTSLint();
|
||||
const failures = linter.getResult().failures;
|
||||
|
||||
expect(failures.length).toBe(1);
|
||||
expect(failures[0].getFailure()).toMatch(/Provider is not statically analyzable./);
|
||||
expect(failures[0].getStartPosition().getLineAndCharacter()).toEqual({line: 3, character: 29});
|
||||
});
|
||||
|
||||
it('should warn if the module providers could not be resolved', () => {
|
||||
writeFile('/index.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
@NgModule({providers: NOT_ANALYZABLE)
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
const linter = runTSLint();
|
||||
const failures = linter.getResult().failures;
|
||||
|
||||
expect(failures.length).toBe(1);
|
||||
expect(failures[0].getFailure()).toMatch(/Providers of module.*not statically analyzable./);
|
||||
expect(failures[0].getStartPosition().getLineAndCharacter()).toEqual({line: 3, character: 28});
|
||||
});
|
||||
|
||||
it('should create new import for @Injectable when migrating provider', () => {
|
||||
writeFile('/index.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
import {MyService, MySecondService} from './service';
|
||||
|
||||
@NgModule({providers: [MyService, MySecondService]})
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
writeFile('/service.ts', `export class MyService {}
|
||||
|
||||
export class MySecondService {}
|
||||
`);
|
||||
|
||||
runTSLint();
|
||||
|
||||
expect(getFile('/service.ts')).toMatch(/@Injectable\(\)\s+export class MyService/);
|
||||
expect(getFile('/service.ts')).toMatch(/@Injectable\(\)\s+export class MySecondService/);
|
||||
expect(getFile('/service.ts')).toMatch(/import { Injectable } from "@angular\/core";/);
|
||||
});
|
||||
|
||||
it('should remove @Inject decorator for providers which are migrated', () => {
|
||||
writeFile('/index.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
import {MyService} from './service';
|
||||
|
||||
@NgModule({providers: [MyService]})
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
writeFile('/service.ts', `
|
||||
import {Inject} from '@angular/core';
|
||||
|
||||
@Inject()
|
||||
export class MyService {}
|
||||
`);
|
||||
|
||||
runTSLint();
|
||||
|
||||
expect(getFile('/service.ts')).toMatch(/core';\s+@Injectable\(\)\s+export class MyService/);
|
||||
expect(getFile('/service.ts')).toMatch(/import { Inject, Injectable } from '@angular\/core';/);
|
||||
});
|
||||
});
|
@ -0,0 +1,580 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {getSystemPath, normalize, virtualFs} from '@angular-devkit/core';
|
||||
import {TempScopedNodeJsSyncHost} from '@angular-devkit/core/node/testing';
|
||||
import {HostTree} from '@angular-devkit/schematics';
|
||||
import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing';
|
||||
import * as shx from 'shelljs';
|
||||
|
||||
describe('Missing injectable migration', () => {
|
||||
let runner: SchematicTestRunner;
|
||||
let host: TempScopedNodeJsSyncHost;
|
||||
let tree: UnitTestTree;
|
||||
let tmpDirPath: string;
|
||||
let previousWorkingDir: string;
|
||||
let warnOutput: string[];
|
||||
|
||||
beforeEach(() => {
|
||||
runner = new SchematicTestRunner('test', require.resolve('./test-migrations.json'));
|
||||
host = new TempScopedNodeJsSyncHost();
|
||||
tree = new UnitTestTree(new HostTree(host));
|
||||
|
||||
writeFile('/tsconfig.json', JSON.stringify({
|
||||
compilerOptions: {
|
||||
experimentalDecorators: true,
|
||||
lib: ['es2015'],
|
||||
}
|
||||
}));
|
||||
writeFile('/angular.json', JSON.stringify({
|
||||
projects: {t: {architect: {build: {options: {tsConfig: './tsconfig.json'}}}}}
|
||||
}));
|
||||
|
||||
warnOutput = [];
|
||||
runner.logger.subscribe(logEntry => {
|
||||
if (logEntry.level === 'warn') {
|
||||
warnOutput.push(logEntry.message);
|
||||
}
|
||||
});
|
||||
|
||||
previousWorkingDir = shx.pwd();
|
||||
tmpDirPath = getSystemPath(host.root);
|
||||
|
||||
// Switch into the temporary directory path. This allows us to run
|
||||
// the schematic against our custom unit test tree.
|
||||
shx.cd(tmpDirPath);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
shx.cd(previousWorkingDir);
|
||||
shx.rm('-r', tmpDirPath);
|
||||
});
|
||||
|
||||
function writeFile(filePath: string, contents: string) {
|
||||
host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents));
|
||||
}
|
||||
|
||||
async function runMigration() {
|
||||
await runner.runSchematicAsync('migration-missing-injectable', {}, tree).toPromise();
|
||||
}
|
||||
|
||||
it('should migrate type provider in NgModule', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
export class MyService {}
|
||||
|
||||
@NgModule({providers: [MyService]})
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnOutput.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/@Injectable\(\)\s+export class MyService/);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/{ NgModule, Injectable } from '@angular\/core/);
|
||||
});
|
||||
|
||||
it('should migrate object literal provider in NgModule', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
export class MyService {}
|
||||
|
||||
@NgModule({providers: [{provide: MyService}]})
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnOutput.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/@Injectable\(\)\s+export class MyService/);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/{ NgModule, Injectable } from '@angular\/core/);
|
||||
});
|
||||
|
||||
it('should not migrate object literal provider with "useValue" in NgModule', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
export class MyService {}
|
||||
|
||||
@NgModule({providers: [{provide: MyService, useValue: null }]})
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnOutput.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).not.toContain('@Injectable');
|
||||
});
|
||||
|
||||
it('should not migrate object literal provider with "useFactory" in NgModule', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
export class MyService {}
|
||||
|
||||
@NgModule({providers: [{provide: MyService, useFactory: () => null }]})
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnOutput.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).not.toContain('@Injectable');
|
||||
});
|
||||
|
||||
it('should migrate object literal provider with "useExisting" in NgModule', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
export class MyService {}
|
||||
export class MyToken {}
|
||||
|
||||
@NgModule({providers: [{provide: MyToken, useExisting: MyService}]})
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnOutput.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/@Injectable\(\)\s+export class MyService/);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/MyService {}\s+export class MyToken/);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/{ NgModule, Injectable } from '@angular\/core/);
|
||||
});
|
||||
|
||||
it('should migrate object literal provider with "useClass" in NgModule', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
export class MyService {}
|
||||
export class MyToken {}
|
||||
|
||||
@NgModule({providers: [{provide: MyToken, useClass: MyService}]})
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnOutput.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/@Injectable\(\)\s+export class MyService/);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/MyService {}\s+export class MyToken/);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/{ NgModule, Injectable } from '@angular\/core/);
|
||||
});
|
||||
|
||||
it('should not migrate provider which is already decorated with @Injectable', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import {Injectable, NgModule} from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
export class MyService {}
|
||||
|
||||
@NgModule({providers: [MyService]})
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnOutput.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts'))
|
||||
.toMatch(/@angular\/core';\s+@Injectable\(\)\s+export class MyService/);
|
||||
});
|
||||
|
||||
it('should not migrate provider which is already decorated with @Directive', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import {Directive, NgModule} from '@angular/core';
|
||||
|
||||
@Directive()
|
||||
export class MyService {}
|
||||
|
||||
@NgModule({providers: [MyService]})
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnOutput.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).not.toContain('@Injectable');
|
||||
});
|
||||
|
||||
it('should not migrate provider which is already decorated with @Component', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import {Component, NgModule} from '@angular/core';
|
||||
|
||||
@Component()
|
||||
export class MyService {}
|
||||
|
||||
@NgModule({providers: [MyService]})
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnOutput.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).not.toContain('@Injectable');
|
||||
});
|
||||
|
||||
it('should not migrate provider which is already decorated with @Pipe', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import {Pipe, NgModule} from '@angular/core';
|
||||
|
||||
@Pipe()
|
||||
export class MyService {}
|
||||
|
||||
@NgModule({providers: [MyService]})
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnOutput.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).not.toContain('@Injectable');
|
||||
});
|
||||
|
||||
it('should migrate multiple providers in same NgModule', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
export class ServiceA {}
|
||||
export class ServiceB {}
|
||||
|
||||
@NgModule({providers: [ServiceA, ServiceB]})
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnOutput.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/@Injectable\(\)\s+export class ServiceA/);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/@Injectable\(\)\s+export class ServiceB/);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/{ NgModule, Injectable } from '@angular\/core/);
|
||||
});
|
||||
|
||||
it('should migrate multiple mixed providers in same NgModule', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
export class ServiceA {}
|
||||
export class ServiceB {}
|
||||
export class ServiceC {}
|
||||
|
||||
@NgModule({
|
||||
providers: [
|
||||
ServiceA,
|
||||
{provide: ServiceB},
|
||||
{provide: SomeToken, useClass: ServiceC},
|
||||
]})
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnOutput.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/@Injectable\(\)\s+export class ServiceA/);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/@Injectable\(\)\s+export class ServiceB/);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/@Injectable\(\)\s+export class ServiceC/);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/{ NgModule, Injectable } from '@angular\/core/);
|
||||
});
|
||||
|
||||
|
||||
it('should migrate multiple nested providers in same NgModule', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
export class ServiceA {}
|
||||
export class ServiceB {}
|
||||
export class ServiceC {}
|
||||
|
||||
@NgModule({
|
||||
providers: [
|
||||
ServiceA,
|
||||
[
|
||||
{provide: ServiceB},
|
||||
ServiceC,
|
||||
],
|
||||
]})
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnOutput.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/@Injectable\(\)\s+export class ServiceA/);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/@Injectable\(\)\s+export class ServiceB/);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/@Injectable\(\)\s+export class ServiceC/);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/{ NgModule, Injectable } from '@angular\/core/);
|
||||
});
|
||||
|
||||
it('should migrate providers referenced through identifier', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
export class ServiceA {}
|
||||
export class ServiceB {}
|
||||
|
||||
const PROVIDERS = [ServiceA, ServiceB];
|
||||
|
||||
@NgModule({
|
||||
providers: PROVIDERS,
|
||||
})
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnOutput.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/@Injectable\(\)\s+export class ServiceA/);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/@Injectable\(\)\s+export class ServiceB/);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/{ NgModule, Injectable } from '@angular\/core/);
|
||||
});
|
||||
|
||||
it('should migrate providers created through static analyzable function call', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
export class ServiceA {}
|
||||
export class ServiceB {}
|
||||
|
||||
export function createProviders(x: any) {
|
||||
return [ServiceA, x]
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
providers: createProviders(ServiceB),
|
||||
})
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnOutput.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/@Injectable\(\)\s+export class ServiceA/);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/@Injectable\(\)\s+export class ServiceB/);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/{ NgModule, Injectable } from '@angular\/core/);
|
||||
});
|
||||
|
||||
it('should migrate providers which are computed through spread operator', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
export class ServiceA {}
|
||||
export class ServiceB {}
|
||||
|
||||
const otherServices = [ServiceB];
|
||||
|
||||
@NgModule({
|
||||
providers: [ServiceA, ...otherServices],
|
||||
})
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnOutput.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/@Injectable\(\)\s+export class ServiceA/);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/@Injectable\(\)\s+export class ServiceB/);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/{ NgModule, Injectable } from '@angular\/core/);
|
||||
});
|
||||
|
||||
it('should migrate provider once if referenced in multiple NgModule definitions', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
export class ServiceA {}
|
||||
|
||||
@NgModule({providers: [ServiceA]})
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
writeFile('/second.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
import {ServiceA} from './index';
|
||||
|
||||
export class ServiceB {}
|
||||
|
||||
@NgModule({providers: [ServiceA, ServiceB]})
|
||||
export class SecondModule {}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnOutput.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts'))
|
||||
.toMatch(/@angular\/core';\s+@Injectable\(\)\s+export class ServiceA/);
|
||||
expect(tree.readContent('/index.ts')).toMatch(/{ NgModule, Injectable } from '@angular\/core/);
|
||||
expect(tree.readContent('/second.ts')).toMatch(/@Injectable\(\)\s+export class ServiceB/);
|
||||
expect(tree.readContent('/second.ts')).toMatch(/{ NgModule, Injectable } from '@angular\/core/);
|
||||
});
|
||||
|
||||
it('should create new import for @Injectable when migrating provider', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
import {MyService, MySecondService} from './service';
|
||||
|
||||
@NgModule({providers: [MyService, MySecondService]})
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
writeFile('/service.ts', `export class MyService {}
|
||||
|
||||
export class MySecondService {}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnOutput.length).toBe(0);
|
||||
expect(tree.readContent('/service.ts')).toMatch(/@Injectable\(\)\s+export class MyService/);
|
||||
expect(tree.readContent('/service.ts'))
|
||||
.toMatch(/@Injectable\(\)\s+export class MySecondService/);
|
||||
expect(tree.readContent('/service.ts')).toMatch(/import { Injectable } from "@angular\/core";/);
|
||||
});
|
||||
|
||||
it('should re-use existing namespace import for importing @Injectable when migrating provider',
|
||||
async() => {
|
||||
writeFile('/index.ts', `
|
||||
import * as core from '@angular/core';
|
||||
|
||||
export class MyService {
|
||||
constructor() {
|
||||
console.log(core.isDevMode());
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
writeFile('/app.module.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
import {MyService} from './index';
|
||||
|
||||
@NgModule({providers: [MyService]})
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnOutput.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts'))
|
||||
.toMatch(/@core.Injectable\(\)\s+export class MyService/);
|
||||
});
|
||||
|
||||
it('should warn if a referenced provider could not be resolved', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
@NgModule({providers: [NotPresent]})
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnOutput.length).toBe(1);
|
||||
expect(warnOutput[0]).toMatch(/\s+index\.ts@4:30: Provider is not statically analyzable./);
|
||||
});
|
||||
|
||||
it('should warn if the module providers could not be resolved', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
@NgModule({providers: NOT_ANALYZABLE)
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnOutput.length).toBe(1);
|
||||
expect(warnOutput[0])
|
||||
.toMatch(/\s+index\.ts@4:29: Providers of module.*not statically analyzable./);
|
||||
});
|
||||
|
||||
it('should not throw if an empty @NgModule is analyzed', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
@NgModule()
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
try {
|
||||
await runMigration();
|
||||
} catch (e) {
|
||||
fail(e);
|
||||
}
|
||||
|
||||
expect(warnOutput.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should create new import for injectable after full end of last import statement', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
import {MyService} from './service';
|
||||
|
||||
@NgModule({providers: [MyService]})
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
writeFile('/service.ts', `
|
||||
import * as a from 'a';
|
||||
import * as a from 'b'; // some comment
|
||||
|
||||
export class MyService {}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnOutput.length).toBe(0);
|
||||
expect(tree.readContent('/service.ts')).toMatch(/@Injectable\(\)\s+export class MyService/);
|
||||
expect(tree.readContent('/service.ts'))
|
||||
.toMatch(/'b'; \/\/ some comment\s+import { Injectable } from "@angular\/core";/);
|
||||
});
|
||||
|
||||
it('should create new import at source file start with trailing new-line', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
import {MyService} from './service';
|
||||
|
||||
@NgModule({providers: [MyService]})
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
writeFile('/service.ts', `/* @license */
|
||||
export class MyService {}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnOutput.length).toBe(0);
|
||||
expect(tree.readContent('/service.ts'))
|
||||
.toMatch(
|
||||
/^import { Injectable } from "@angular\/core";\s+\/\* @license \*\/\s+@Injectable\(\)\s+export class MyService/);
|
||||
});
|
||||
|
||||
it('should remove @Inject decorator for providers which are migrated', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
import {MyService} from './service';
|
||||
|
||||
@NgModule({providers: [MyService]})
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
writeFile('/service.ts', `
|
||||
import {Inject} from '@angular/core';
|
||||
|
||||
@Inject()
|
||||
export class MyService {}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnOutput.length).toBe(0);
|
||||
expect(tree.readContent('/service.ts'))
|
||||
.toMatch(/core';\s+@Injectable\(\)\s+export class MyService/);
|
||||
expect(tree.readContent('/service.ts'))
|
||||
.toMatch(/import { Inject, Injectable } from '@angular\/core';/);
|
||||
});
|
||||
|
||||
});
|
@ -5,6 +5,10 @@
|
||||
"migration-injectable-pipe": {
|
||||
"description": "Migrates all Pipe classes so that they have an Injectable annotation",
|
||||
"factory": "../migrations/injectable-pipe/index"
|
||||
},
|
||||
"migration-missing-injectable": {
|
||||
"description": "Migrates all declared undecorated providers with the @Injectable decorator",
|
||||
"factory": "../migrations/missing-injectable/index"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user