diff --git a/package.json b/package.json index 893335ecff3d..31c87061436b 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ ] }, "dependencies": { + "@phenomnomnominal/tsquery": "3.0.0", "@types/debug": "^4.1.2", "@types/node-fetch": "^2.1.6", "@types/progress": "^2.0.3", diff --git a/packages/schematics/angular/BUILD b/packages/schematics/angular/BUILD index 809ca81cb12e..507b196b67a3 100644 --- a/packages/schematics/angular/BUILD +++ b/packages/schematics/angular/BUILD @@ -50,8 +50,10 @@ ts_library( "//packages/angular_devkit/schematics", "//packages/angular_devkit/schematics:tasks", "//packages/schematics/angular/third_party/github.com/Microsoft/TypeScript", + "@npm//@phenomnomnominal/tsquery", "@npm//@types/node", "@npm//rxjs", + "@npm//tslint", ], ) @@ -83,12 +85,15 @@ ts_library( deps = ALL_SCHEMA_DEPS + [ ":angular", "//packages/angular_devkit/core", + "//packages/angular_devkit/core:node_testing", "//packages/angular_devkit/schematics", "//packages/angular_devkit/schematics:testing", "//packages/schematics/angular/third_party/github.com/Microsoft/TypeScript", + "@npm//@phenomnomnominal/tsquery", "@npm//@types/node", "@npm//@types/jasmine", "@npm//rxjs", + "@npm//tslint", ], testonly = True, # @external_begin diff --git a/packages/schematics/angular/migrations/migration-collection.json b/packages/schematics/angular/migrations/migration-collection.json index ec05605cd907..3cd1b2c82c79 100644 --- a/packages/schematics/angular/migrations/migration-collection.json +++ b/packages/schematics/angular/migrations/migration-collection.json @@ -34,6 +34,11 @@ "version": "8.0.0-beta.12", "factory": "./update-8", "description": "Update an Angular CLI project to version 8." + }, + "migration-08": { + "version": "8.0.0-beta.14", + "factory": "./update-8/#updateLazyModulePaths", + "description": "Update an Angular CLI project to version 8." } } } diff --git a/packages/schematics/angular/migrations/update-8/index.ts b/packages/schematics/angular/migrations/update-8/index.ts index 9a5c0cca49d8..2916796169f5 100644 --- a/packages/schematics/angular/migrations/update-8/index.ts +++ b/packages/schematics/angular/migrations/update-8/index.ts @@ -14,6 +14,8 @@ import { updatePackageJson, updateTsLintConfig } from './codelyzer-5'; import { updateES5Projects } from './differential-loading'; import { dropES2015Polyfills } from './drop-es6-polyfills'; +export { updateLazyModulePaths } from './update-lazy-module-paths'; + export default function(): Rule { return () => { return chain([ diff --git a/packages/schematics/angular/migrations/update-8/rules/noLazyModulePathsRule.ts b/packages/schematics/angular/migrations/update-8/rules/noLazyModulePathsRule.ts new file mode 100644 index 000000000000..24ef937cf9d0 --- /dev/null +++ b/packages/schematics/angular/migrations/update-8/rules/noLazyModulePathsRule.ts @@ -0,0 +1,54 @@ +/** + * @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 { tsquery } from '@phenomnomnominal/tsquery'; +import { + Replacement, + RuleFailure, + Rules, +} from 'tslint'; // tslint:disable-line:no-implicit-dependencies +import * as ts from '../../../third_party/github.com/Microsoft/TypeScript/lib/typescript'; + + // Constants: +const LOAD_CHILDREN_SPLIT = '#'; +const NOT_CHILDREN_QUERY = `:not(:has(Identifier[name="children"]))`; +const HAS_LOAD_CHILDREN_QUERY = `:has(Identifier[name="loadChildren"])`; +const LAZY_VALUE_QUERY = `StringLiteral[value=/.*${LOAD_CHILDREN_SPLIT}.*/]`; +const LOAD_CHILDREN_ASSIGNMENT_QUERY = + `PropertyAssignment${NOT_CHILDREN_QUERY}${HAS_LOAD_CHILDREN_QUERY}:has(${LAZY_VALUE_QUERY})`; + +const FAILURE_MESSAGE = 'Found magic `loadChildren` string. Use a function with `import` instead.'; + +export class Rule extends Rules.AbstractRule { + public apply (ast: ts.SourceFile): Array { + return tsquery(ast, LOAD_CHILDREN_ASSIGNMENT_QUERY).map(result => { + const [valueNode] = tsquery(result, LAZY_VALUE_QUERY); + let fix = this._promiseReplacement(valueNode.text); + + // Try to fix indentation in replacement: + const { character } = ast.getLineAndCharacterOfPosition(result.getStart()); + fix = fix.replace(/\n/g, `\n${' '.repeat(character)}`); + + const replacement = new Replacement(valueNode.getStart(), valueNode.getWidth(), fix); + const start = result.getStart(); + const end = result.getEnd(); + + return new RuleFailure(ast, start, end, FAILURE_MESSAGE, this.ruleName, replacement); + }); + } + + private _promiseReplacement (loadChildren: string): string { + const [path, moduleName] = this._getChunks(loadChildren); + + return `() => import('${path}').then(m => m.${moduleName})`; + } + + private _getChunks (loadChildren: string): Array { + return loadChildren.split(LOAD_CHILDREN_SPLIT); + } +} diff --git a/packages/schematics/angular/migrations/update-8/update-lazy-module-paths.ts b/packages/schematics/angular/migrations/update-8/update-lazy-module-paths.ts new file mode 100644 index 000000000000..4a524b2326f3 --- /dev/null +++ b/packages/schematics/angular/migrations/update-8/update-lazy-module-paths.ts @@ -0,0 +1,30 @@ +/** + * @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 { + Rule, + SchematicContext, + Tree, +} from '@angular-devkit/schematics'; +import { + TslintFixTask, +} from '@angular-devkit/schematics/tasks'; +import * as path from 'path'; + +export const updateLazyModulePaths = (): Rule => { + return (_: Tree, context: SchematicContext) => { + context.addTask(new TslintFixTask({ + rulesDirectory: path.join(__dirname, 'rules'), + rules: { + 'no-lazy-module-paths': [true], + }, + }, { + includes: '**/*.ts', + silent: false, + })); + }; +}; diff --git a/packages/schematics/angular/migrations/update-8/update-lazy-module-paths_spec.ts b/packages/schematics/angular/migrations/update-8/update-lazy-module-paths_spec.ts new file mode 100644 index 000000000000..22f6ca921f94 --- /dev/null +++ b/packages/schematics/angular/migrations/update-8/update-lazy-module-paths_spec.ts @@ -0,0 +1,85 @@ +/** + * @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'; + +describe('Migration to version 8', () => { + const schematicRunner = new SchematicTestRunner( + 'migrations', + require.resolve('../migration-collection.json'), + ); + + let tree: UnitTestTree; + let host: TempScopedNodeJsSyncHost; + + const lazyRoutePath = normalize('src/lazy-route.ts'); + const lazyRoute = virtualFs.stringToFileBuffer(` + import { Route } from '@angular/router'; + const routes: Array = [ + { + path: '', + loadChildren: './lazy/lazy.module#LazyModule' + } + ]; + `); + + const lazyChildRoute = virtualFs.stringToFileBuffer(` + import { Route } from '@angular/router'; + const routes: Array = [ + { + path: '', + children: [{ + path: 'child', + loadChildren: './lazy/lazy.module#LazyModule' + }] + } + ]; + `); + + describe('Migration to import() style lazy routes', () => { + beforeEach(async () => { + host = new TempScopedNodeJsSyncHost(); + tree = new UnitTestTree(new HostTree(host)); + tree.create('/package.json', JSON.stringify({})); + process.chdir(getSystemPath(host.root)); + }); + + it('should replace the module path string', async () => { + await host.write(lazyRoutePath, lazyRoute).toPromise(); + + schematicRunner.runSchematic('migration-08', {}, tree); + await schematicRunner.engine.executePostTasks().toPromise(); + + const routes = await host.read(lazyRoutePath) + .toPromise() + .then(virtualFs.fileBufferToString); + + expect(routes).not.toContain('./lazy/lazy.module#LazyModule'); + expect(routes).toContain( + `loadChildren: () => import('./lazy/lazy.module').then(m => m.LazyModule)`); + }); + + it('should replace the module path string in a child path', async () => { + await host.write(lazyRoutePath, lazyChildRoute).toPromise(); + + schematicRunner.runSchematic('migration-08', {}, tree); + await schematicRunner.engine.executePostTasks().toPromise(); + + const routes = await host.read(lazyRoutePath) + .toPromise() + .then(virtualFs.fileBufferToString); + + expect(routes).not.toContain('./lazy/lazy.module#LazyModule'); + + expect(routes).toContain( + `loadChildren: () => import('./lazy/lazy.module').then(m => m.LazyModule)`); + }); + }); +}); diff --git a/packages/schematics/angular/package.json b/packages/schematics/angular/package.json index 783b2a424160..e1291b75a864 100644 --- a/packages/schematics/angular/package.json +++ b/packages/schematics/angular/package.json @@ -9,6 +9,7 @@ ], "schematics": "./collection.json", "dependencies": { + "@phenomnomnominal/tsquery": "3.0.0", "@angular-devkit/core": "0.0.0", "@angular-devkit/schematics": "0.0.0" } diff --git a/yarn.lock b/yarn.lock index 483eb9ae4e17..5539e09be698 100644 --- a/yarn.lock +++ b/yarn.lock @@ -221,6 +221,13 @@ resolved "https://registry.yarnpkg.com/@ngtools/json-schema/-/json-schema-1.1.0.tgz#c3a0c544d62392acc2813a42c8a0dc6f58f86922" integrity sha1-w6DFRNYjkqzCgTpCyKDcb1j4aSI= +"@phenomnomnominal/tsquery@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@phenomnomnominal/tsquery/-/tsquery-3.0.0.tgz#6f2f4dbf6304ff52b12cc7a5b979f20c3794a22a" + integrity sha512-SW8lKitBHWJ9fAYkJ9kJivuctwNYCh3BUxLdH0+XiR1GPBiu+7qiZzh8p8jqlj1LgVC1TbvfNFroaEsmYlL8Iw== + dependencies: + esquery "^1.0.1" + "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" @@ -3341,6 +3348,13 @@ esprima@^4.0.0: resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== +esquery@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708" + integrity sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA== + dependencies: + estraverse "^4.0.0" + esrecurse@^4.1.0: version "4.2.1" resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" @@ -3353,7 +3367,7 @@ estraverse@^1.9.1: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44" integrity sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q= -estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: +estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=