diff --git a/packages/core/schematics/BUILD.bazel b/packages/core/schematics/BUILD.bazel index d7611840e0f..a60dbd252de 100644 --- a/packages/core/schematics/BUILD.bazel +++ b/packages/core/schematics/BUILD.bazel @@ -13,27 +13,7 @@ pkg_npm( ], visibility = ["//packages/core:__pkg__"], deps = [ - "//packages/core/schematics/migrations/abstract-control-parent", - "//packages/core/schematics/migrations/activated-route-snapshot-fragment", - "//packages/core/schematics/migrations/can-activate-with-redirect-to", - "//packages/core/schematics/migrations/deep-shadow-piercing-selector", - "//packages/core/schematics/migrations/dynamic-queries", - "//packages/core/schematics/migrations/initial-navigation", - "//packages/core/schematics/migrations/missing-injectable", - "//packages/core/schematics/migrations/module-with-providers", - "//packages/core/schematics/migrations/move-document", - "//packages/core/schematics/migrations/native-view-encapsulation", - "//packages/core/schematics/migrations/navigation-extras-omissions", - "//packages/core/schematics/migrations/relative-link-resolution", - "//packages/core/schematics/migrations/renderer-to-renderer2", "//packages/core/schematics/migrations/router-link-empty-expression", - "//packages/core/schematics/migrations/router-preserve-query-params", - "//packages/core/schematics/migrations/static-queries", - "//packages/core/schematics/migrations/template-var-assignment", "//packages/core/schematics/migrations/testbed-teardown", - "//packages/core/schematics/migrations/undecorated-classes-with-decorated-fields", - "//packages/core/schematics/migrations/undecorated-classes-with-di", - "//packages/core/schematics/migrations/wait-for-async", - "//packages/core/schematics/migrations/xhr-factory", ], ) diff --git a/packages/core/schematics/migrations.json b/packages/core/schematics/migrations.json index 5c85a231027..05f02c3dec0 100644 --- a/packages/core/schematics/migrations.json +++ b/packages/core/schematics/migrations.json @@ -1,105 +1,5 @@ { "schematics": { - "migration-v8-move-document": { - "version": "8.0.0-beta", - "description": "@DOCUMENT migration. Migrates DOCUMENT Injection token from platform-browser imports to common import.", - "factory": "./migrations/move-document/index" - }, - "migration-v8-static-queries": { - "version": "8.0.0-beta", - "description": "ViewChild and ContentChild to explicit query timing migration. In Angular version 8, developers need to explicitly specify the timing of ViewChild and ContentChild queries. Read more about this here: https://v8.angular.io/guide/static-query-migration", - "factory": "./migrations/static-queries/index" - }, - "migration-v8-template-local-variables": { - "version": "8.0.0-beta", - "description": "Template Variable Assignment migration. Warns developers if values are assigned to template variables.", - "factory": "./migrations/template-var-assignment/index" - }, - "migration-v9-renderer-to-renderer2": { - "version": "9.0.0-beta", - "description": "Renderer to Renderer2 migration. As of Angular 9, the Renderer class is no longer available. Renderer2 should be used instead. Read more about this here: https://v9.angular.io/guide/migration-renderer", - "factory": "./migrations/renderer-to-renderer2/index" - }, - "migration-v9-undecorated-classes-with-di": { - "version": "9.0.0-beta", - "description": "Undecorated classes with DI migration. As of Angular 9, it is no longer supported to use Angular DI on a class that does not have an Angular decorator. Read more about this here: https://v9.angular.io/guide/migration-undecorated-classes", - "factory": "./migrations/undecorated-classes-with-di/index" - }, - "migration-v9-dynamic-queries": { - "version": "9-beta", - "description": "Static flag migration. Removes the `static` flag from dynamic queries. As of Angular 9, the \"static\" flag defaults to false and is no longer required for your view and content queries. Read more about this here: https://v9.angular.io/guide/migration-dynamic-flag", - "factory": "./migrations/dynamic-queries/index" - }, - "migration-v10-missing-injectable": { - "version": "10.0.0-beta", - "description": "Missing @Injectable and incomplete provider definition migration. As of Angular 9, enforcement of @Injectable decorators for DI is a bit stricter and incomplete provider definitions behave differently. Read more about this here: https://v9.angular.io/guide/migration-injectable", - "factory": "./migrations/missing-injectable/index" - }, - "migration-v10-module-with-providers": { - "version": "10.0.0-beta", - "description": "ModuleWithProviders migration. As of Angular 10, the ModuleWithProviders type requires a generic. This migration adds the generic where it is missing. Read more about this here: https://v10.angular.io/guide/migration-module-with-providers", - "factory": "./migrations/module-with-providers/index" - }, - "migration-v10-undecorated-classes-with-decorated-fields": { - "version": "10.0.0-beta", - "description": "Undecorated classes with Angular features migration. In version 10, classes that use Angular features and do not have an Angular decorator are no longer supported. Read more about this here: https://v10.angular.io/guide/migration-undecorated-classes", - "factory": "./migrations/undecorated-classes-with-decorated-fields/index" - }, - "migration-v11-navigation-extras-omissions": { - "version": "11.0.0-beta", - "description": "NavigationExtras omissions migration. In version 11, some unsupported properties were omitted from the `extras` parameter of the `Router.navigateByUrl` and `Router.createUrlTree` methods.", - "factory": "./migrations/navigation-extras-omissions/index" - }, - "migration-v11-router-relative-link-resolution-default": { - "version": "11.0.0-beta", - "description": "The default value for `relativeLinkResolution` is changing from 'legacy' to 'corrected'.\nThis migration updates `RouterModule` configurations that use the default value to \nnow specifically use 'legacy' to prevent breakages when updating.", - "factory": "./migrations/relative-link-resolution/index" - }, - "migration-v11-abstract-control-parent": { - "version": "11.0.0-beta", - "description": "In Angular version 11, the type of `AbstractControl.parent` can be `null` to reflect the runtime value more accurately. This migration automatically adds non-null assertions to existing accesses of the `parent` property on types like `FormControl`, `FormArray` and `FormGroup`.", - "factory": "./migrations/abstract-control-parent/index" - }, - "migration-v11-native-view-encapsulation": { - "version": "11.0.0-beta", - "description": "ViewEncapsulation.Native has been removed as of Angular version 11. This migration replaces any usages with ViewEncapsulation.ShadowDom.", - "factory": "./migrations/native-view-encapsulation/index" - }, - "migration-v11-wait-for-async": { - "version": "11.0.0-beta", - "description": "`async` to `waitForAsync` migration. The `async` testing function has been renamed to `waitForAsync` to avoid confusion with the native `async` keyword.", - "factory": "./migrations/wait-for-async/index" - }, - "migration-v11-router-preserve-query-params": { - "version": "11.0.0-beta", - "description": "NavigationExtras.preserveQueryParams has been removed as of Angular version 11. This migration replaces any usages with the appropriate assignment of the queryParamsHandling key.", - "factory": "./migrations/router-preserve-query-params/index" - }, - "migration-v11-router-initial-navigation-options": { - "version": "11.0.0-beta", - "description": "Updates the `initialNavigation` property for `RouterModule.forRoot`.", - "factory": "./migrations/initial-navigation/index" - }, - "migration-v11.1-can-activate-with-redirect-to": { - "version": "11.1.0-beta", - "description": "Removes `canActivate` from a `Route` config when `redirectTo` is also present", - "factory": "./migrations/can-activate-with-redirect-to/index" - }, - "migration-v12-activated-route-snapshot-fragment": { - "version": "12.0.0-beta", - "description": "In Angular version 12, the type of ActivatedRouteSnapshot.fragment is nullable. This migration automatically adds non-null assertions to it.", - "factory": "./migrations/activated-route-snapshot-fragment/index" - }, - "migration-v12-xhr-factory": { - "version": "12.0.0-next.6", - "description": "`XhrFactory` has been moved from `@angular/common/http` to `@angular/common`.", - "factory": "./migrations/xhr-factory/index" - }, - "migration-v12-deep-shadow-piercing-selector": { - "version": "12.0.2", - "description": "Automatically migrates shadow-piercing selector from `/deep/` to the recommended alternative `::ng-deep`.", - "factory": "./migrations/deep-shadow-piercing-selector/index" - }, "migration-v13-router-link-empty-expression": { "version": "13.0.0-beta", "description": "Migrates `[routerLink]=\"\"` in templates to `[routerLink]=\"[]\"` because these links are likely intended to route to the current page with updated fragment/query params.", diff --git a/packages/core/schematics/migrations/abstract-control-parent/BUILD.bazel b/packages/core/schematics/migrations/abstract-control-parent/BUILD.bazel deleted file mode 100644 index c78e69f55f1..00000000000 --- a/packages/core/schematics/migrations/abstract-control-parent/BUILD.bazel +++ /dev/null @@ -1,18 +0,0 @@ -load("//tools:defaults.bzl", "ts_library") - -ts_library( - name = "abstract-control-parent", - srcs = glob(["**/*.ts"]), - tsconfig = "//packages/core/schematics:tsconfig.json", - visibility = [ - "//packages/core/schematics:__pkg__", - "//packages/core/schematics/migrations/google3:__pkg__", - "//packages/core/schematics/test:__pkg__", - ], - deps = [ - "//packages/core/schematics/utils", - "@npm//@angular-devkit/schematics", - "@npm//@types/node", - "@npm//typescript", - ], -) diff --git a/packages/core/schematics/migrations/abstract-control-parent/README.md b/packages/core/schematics/migrations/abstract-control-parent/README.md deleted file mode 100644 index b40ca8deda7..00000000000 --- a/packages/core/schematics/migrations/abstract-control-parent/README.md +++ /dev/null @@ -1,34 +0,0 @@ -## `AbstractControl.parent` migration - -As of Angular v11, the type of `AbstractControl.parent` can be null. This migration automatically -identifies usages and adds non-null assertions. - -#### Before -```ts -import { Component } from '@angular/core'; -import { FormControl } from '@angular/forms'; - -@Component() -export class MyComponent { - private _control = new FormControl(); - - getParentValue() { - return this._control.parent.value; // <- Compilation error in v11. - } -} -``` - -#### After -```ts -import { Component } from '@angular/core'; -import { FormControl } from '@angular/forms'; - -@Component() -export class MyComponent { - private _control = new FormControl(); - - getParentValue() { - return this._control.parent!.value; // <- Non-null assertion added during the migration. - } -} -``` diff --git a/packages/core/schematics/migrations/abstract-control-parent/index.ts b/packages/core/schematics/migrations/abstract-control-parent/index.ts deleted file mode 100644 index 3291b5b45b0..00000000000 --- a/packages/core/schematics/migrations/abstract-control-parent/index.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * @license - * Copyright Google LLC 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, SchematicsException, Tree} from '@angular-devkit/schematics'; -import {relative} from 'path'; - -import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; -import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host'; -import {findParentAccesses} from './util'; - - -/** Migration that marks accesses of `AbstractControl.parent` as non-null. */ -export default function(): Rule { - return async (tree: Tree) => { - const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); - const basePath = process.cwd(); - const allPaths = [...buildPaths, ...testPaths]; - - if (!allPaths.length) { - throw new SchematicsException( - 'Could not find any tsconfig file. Cannot migrate AbstractControl.parent accesses.'); - } - - for (const tsconfigPath of allPaths) { - runNativeAbstractControlParentMigration(tree, tsconfigPath, basePath); - } - }; -} - -function runNativeAbstractControlParentMigration( - tree: Tree, tsconfigPath: string, basePath: string) { - const {program} = createMigrationProgram(tree, tsconfigPath, basePath); - const typeChecker = program.getTypeChecker(); - const sourceFiles = - program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program)); - - sourceFiles.forEach(sourceFile => { - // We sort the nodes based on their position in the file and we offset the positions by one - // for each non-null assertion that we've added. We have to do it this way, rather than - // creating and printing a new AST node like in other migrations, because property access - // expressions can be nested (e.g. `control.parent.parent.value`), but the node positions - // aren't being updated as we're inserting new code. If we were to go through the AST, - // we'd have to update the `SourceFile` and start over after each operation. - findParentAccesses(typeChecker, sourceFile) - .sort((a, b) => a.getStart() - b.getStart()) - .forEach((node, index) => { - const update = tree.beginUpdate(relative(basePath, sourceFile.fileName)); - update.insertRight(node.getStart() + node.getWidth() + index, '!'); - tree.commitUpdate(update); - }); - }); -} diff --git a/packages/core/schematics/migrations/abstract-control-parent/util.ts b/packages/core/schematics/migrations/abstract-control-parent/util.ts deleted file mode 100644 index da6965e4e99..00000000000 --- a/packages/core/schematics/migrations/abstract-control-parent/util.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 {normalize} from 'path'; -import ts from 'typescript'; -import {isNullCheck, isSafeAccess} from '../../utils/typescript/nodes'; -import {hasOneOfTypes, isNullableType} from '../../utils/typescript/symbol'; - -/** Names of symbols from `@angular/forms` whose `parent` accesses have to be migrated. */ -const abstractControlSymbols = ['AbstractControl', 'FormArray', 'FormControl', 'FormGroup']; - -/** - * Finds the `PropertyAccessExpression`-s that are accessing the `parent` property in - * such a way that may result in a compilation error after the v11 type changes. - */ -export function findParentAccesses( - typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile): ts.PropertyAccessExpression[] { - const results: ts.PropertyAccessExpression[] = []; - - sourceFile.forEachChild(function walk(node: ts.Node) { - if (ts.isPropertyAccessExpression(node) && node.name.text === 'parent' && !isNullCheck(node) && - !isSafeAccess(node) && results.indexOf(node) === -1 && - isAbstractControlReference(typeChecker, node) && isNullableType(typeChecker, node)) { - results.unshift(node); - } - - node.forEachChild(walk); - }); - - return results; -} - -/** Checks whether a property access is on an `AbstractControl` coming from `@angular/forms`. */ -function isAbstractControlReference( - typeChecker: ts.TypeChecker, node: ts.PropertyAccessExpression): boolean { - let current: ts.Expression = node; - const formsPattern = /node_modules\/?.*\/@angular\/forms/; - // Walks up the property access chain and tries to find a symbol tied to a `SourceFile`. - // If such a node is found, we check whether the type is one of the `AbstractControl` symbols - // and whether it comes from the `@angular/forms` directory in the `node_modules`. - while (ts.isPropertyAccessExpression(current)) { - const symbol = typeChecker.getTypeAtLocation(current.expression)?.getSymbol(); - if (symbol) { - const sourceFile = symbol.valueDeclaration?.getSourceFile(); - return sourceFile != null && - formsPattern.test(normalize(sourceFile.fileName).replace(/\\/g, '/')) && - hasOneOfTypes(typeChecker, current.expression, abstractControlSymbols); - } - current = current.expression; - } - return false; -} diff --git a/packages/core/schematics/migrations/activated-route-snapshot-fragment/BUILD.bazel b/packages/core/schematics/migrations/activated-route-snapshot-fragment/BUILD.bazel deleted file mode 100644 index ff42f9dc839..00000000000 --- a/packages/core/schematics/migrations/activated-route-snapshot-fragment/BUILD.bazel +++ /dev/null @@ -1,18 +0,0 @@ -load("//tools:defaults.bzl", "ts_library") - -ts_library( - name = "activated-route-snapshot-fragment", - srcs = glob(["**/*.ts"]), - tsconfig = "//packages/core/schematics:tsconfig.json", - visibility = [ - "//packages/core/schematics:__pkg__", - "//packages/core/schematics/migrations/google3:__pkg__", - "//packages/core/schematics/test:__pkg__", - ], - deps = [ - "//packages/core/schematics/utils", - "@npm//@angular-devkit/schematics", - "@npm//@types/node", - "@npm//typescript", - ], -) diff --git a/packages/core/schematics/migrations/activated-route-snapshot-fragment/README.md b/packages/core/schematics/migrations/activated-route-snapshot-fragment/README.md deleted file mode 100644 index b7bc129091b..00000000000 --- a/packages/core/schematics/migrations/activated-route-snapshot-fragment/README.md +++ /dev/null @@ -1,34 +0,0 @@ -## `ActivatedRouteSnapshot.fragment` migration - -The value if `ActivatedRouteSnapshot.fragment` is becoming nullable. This migration adds non-null -assertions to it. - -#### Before -```ts -import { Component } from '@angular/core'; -import { ActivatedRouteSnapshot } from '@angular/router'; - -@Component({}) -export class YourComponent { - private _activatedRouteSnapshot: ActivatedRouteSnapshot; - - getFragmentValue() { - return this._activatedRouteSnapshot.fragment.value; - } -} -``` - -#### After -```ts -import { Component } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; - -@Component({}) -export class YourComponent { - private _activatedRouteSnapshot: ActivatedRouteSnapshot; - - getFragmentValue() { - return this._activatedRouteSnapshot.fragment!.value; - } -} -``` diff --git a/packages/core/schematics/migrations/activated-route-snapshot-fragment/index.ts b/packages/core/schematics/migrations/activated-route-snapshot-fragment/index.ts deleted file mode 100644 index 0338570dc19..00000000000 --- a/packages/core/schematics/migrations/activated-route-snapshot-fragment/index.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * @license - * Copyright Google LLC 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, SchematicsException, Tree} from '@angular-devkit/schematics'; -import {relative} from 'path'; -import ts from 'typescript'; - -import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; -import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host'; -import {findFragmentAccesses, migrateActivatedRouteSnapshotFragment} from './util'; - - -/** - * Migration that marks accesses of `ActivatedRouteSnapshot.fragment` as non-null. - */ -export default function(): Rule { - return async (tree: Tree) => { - const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); - const basePath = process.cwd(); - const allPaths = [...buildPaths, ...testPaths]; - - if (!allPaths.length) { - throw new SchematicsException( - 'Could not find any tsconfig file. Cannot migrate ' + - '`ActivatedRouteSnapshot.fragment` accesses.'); - } - - for (const tsconfigPath of allPaths) { - runActivatedRouteSnapshotFragmentMigration(tree, tsconfigPath, basePath); - } - }; -} - -function runActivatedRouteSnapshotFragmentMigration( - tree: Tree, tsconfigPath: string, basePath: string) { - const {program} = createMigrationProgram(tree, tsconfigPath, basePath); - const typeChecker = program.getTypeChecker(); - const sourceFiles = - program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program)); - const printer = ts.createPrinter(); - - sourceFiles.forEach(sourceFile => { - const nodesToMigrate = findFragmentAccesses(typeChecker, sourceFile); - - if (nodesToMigrate.size > 0) { - const update = tree.beginUpdate(relative(basePath, sourceFile.fileName)); - nodesToMigrate.forEach(node => { - update.remove(node.getStart(), node.getWidth()); - update.insertRight( - node.getStart(), - printer.printNode( - ts.EmitHint.Unspecified, migrateActivatedRouteSnapshotFragment(node), sourceFile)); - }); - tree.commitUpdate(update); - } - }); -} diff --git a/packages/core/schematics/migrations/activated-route-snapshot-fragment/util.ts b/packages/core/schematics/migrations/activated-route-snapshot-fragment/util.ts deleted file mode 100644 index aba86d849fe..00000000000 --- a/packages/core/schematics/migrations/activated-route-snapshot-fragment/util.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; -import {isNullCheck, isSafeAccess} from '../../utils/typescript/nodes'; -import {hasOneOfTypes, isNullableType} from '../../utils/typescript/symbol'; - -/** - * Finds all the accesses of `ActivatedRouteSnapshot.fragment` - * that need to be migrated within a particular file. - */ -export function findFragmentAccesses( - typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile): Set { - const results = new Set(); - - sourceFile.forEachChild(function walk(node: ts.Node) { - if (ts.isPropertyAccessExpression(node) && node.name.text === 'fragment' && - !results.has(node) && !isNullCheck(node) && !isSafeAccess(node) && - hasOneOfTypes(typeChecker, node.expression, ['ActivatedRouteSnapshot']) && - isNullableType(typeChecker, node)) { - results.add(node); - } - - node.forEachChild(walk); - }); - - return results; -} - -/** Migrates an `ActivatedRouteSnapshot.fragment` access. */ -export function migrateActivatedRouteSnapshotFragment(node: ts.PropertyAccessExpression): ts.Node { - // Turns `foo.fragment` into `foo.fragment!`. - return ts.createNonNullExpression(node); -} diff --git a/packages/core/schematics/migrations/can-activate-with-redirect-to/BUILD.bazel b/packages/core/schematics/migrations/can-activate-with-redirect-to/BUILD.bazel deleted file mode 100644 index 38990660897..00000000000 --- a/packages/core/schematics/migrations/can-activate-with-redirect-to/BUILD.bazel +++ /dev/null @@ -1,18 +0,0 @@ -load("//tools:defaults.bzl", "ts_library") - -ts_library( - name = "can-activate-with-redirect-to", - srcs = glob(["**/*.ts"]), - tsconfig = "//packages/core/schematics:tsconfig.json", - visibility = [ - "//packages/core/schematics:__pkg__", - "//packages/core/schematics/migrations/google3:__pkg__", - "//packages/core/schematics/test:__pkg__", - ], - deps = [ - "//packages/core/schematics/utils", - "@npm//@angular-devkit/schematics", - "@npm//@types/node", - "@npm//typescript", - ], -) diff --git a/packages/core/schematics/migrations/can-activate-with-redirect-to/README.md b/packages/core/schematics/migrations/can-activate-with-redirect-to/README.md deleted file mode 100644 index e640403ec8c..00000000000 --- a/packages/core/schematics/migrations/can-activate-with-redirect-to/README.md +++ /dev/null @@ -1,23 +0,0 @@ -## Router migration to remove canActivate property from Routes that also have redirectTo - -The activation stage of the router happens after redirects so any `canActivate` guards -will not be executed. This invalid configuration is now an error. This migration -removes `canActivate` from the `Route` to fix pre-existing invalid configurations. - -#### Before -```ts -import { Routes } from '@angular/router'; - -const routes: Routes = [ - {path: '', redirectTo: 'other', canActivate: [MyGuard]} -]; -``` - -#### After -```ts -import { Routes } from '@angular/router'; - -const routes: Routes = [ - {path: '', redirectTo: 'other'} -]; -``` diff --git a/packages/core/schematics/migrations/can-activate-with-redirect-to/index.ts b/packages/core/schematics/migrations/can-activate-with-redirect-to/index.ts deleted file mode 100644 index 3b5a564d34d..00000000000 --- a/packages/core/schematics/migrations/can-activate-with-redirect-to/index.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * @license - * Copyright Google LLC 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, SchematicsException, Tree} from '@angular-devkit/schematics'; -import {relative} from 'path'; -import ts from 'typescript'; - -import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; -import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host'; -import {findLiteralsToMigrate, migrateLiteral} from './util'; - - -/** Migration that removes `canActivate` property from routes that also have `redirectTo`. */ -export default function(): Rule { - return async (tree: Tree) => { - const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); - const basePath = process.cwd(); - const allPaths = [...buildPaths, ...testPaths]; - - if (!allPaths.length) { - throw new SchematicsException( - 'Could not find any tsconfig file. Cannot migrate ' + - 'Router.navigateByUrl and Router.createUrlTree calls.'); - } - - for (const tsconfigPath of allPaths) { - runCanActivateWithRedirectToMigration(tree, tsconfigPath, basePath); - } - }; -} - -function runCanActivateWithRedirectToMigration(tree: Tree, tsconfigPath: string, basePath: string) { - const {program} = createMigrationProgram(tree, tsconfigPath, basePath); - const printer = ts.createPrinter(); - const sourceFiles = - program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program)); - - sourceFiles.forEach(sourceFile => { - const literalsToMigrate = findLiteralsToMigrate(sourceFile); - const update = tree.beginUpdate(relative(basePath, sourceFile.fileName)); - - for (const literal of Array.from(literalsToMigrate)) { - const migratedNode = migrateLiteral(literal); - update.remove(literal.getStart(), literal.getWidth()); - update.insertRight( - literal.getStart(), printer.printNode(ts.EmitHint.Unspecified, migratedNode, sourceFile)); - } - - tree.commitUpdate(update); - }); -} diff --git a/packages/core/schematics/migrations/can-activate-with-redirect-to/util.ts b/packages/core/schematics/migrations/can-activate-with-redirect-to/util.ts deleted file mode 100644 index f537f6a5906..00000000000 --- a/packages/core/schematics/migrations/can-activate-with-redirect-to/util.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; - -const CAN_ACTIVATE = 'canActivate'; -const REDIRECT_TO = 'redirectTo'; - -export function migrateLiteral(node: ts.ObjectLiteralExpression): ts.ObjectLiteralExpression { - const propertiesToKeep: ts.ObjectLiteralElementLike[] = []; - node.properties.forEach(property => { - // Only look for regular and shorthand property assignments since resolving things - // like spread operators becomes too complicated for this migration. - if ((ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) && - (ts.isStringLiteralLike(property.name) || ts.isNumericLiteral(property.name) || - ts.isIdentifier(property.name))) { - if (property.name.text !== CAN_ACTIVATE) { - propertiesToKeep.push(property); - } - } else { - propertiesToKeep.push(property); - } - }); - - return ts.createObjectLiteral(propertiesToKeep); -} - - -export function findLiteralsToMigrate(sourceFile: ts.SourceFile) { - const results = new Set(); - - sourceFile.forEachChild(function visitNode(node: ts.Node) { - if (!ts.isObjectLiteralExpression(node)) { - node.forEachChild(visitNode); - return; - } - if (hasProperty(node, REDIRECT_TO) && hasProperty(node, CAN_ACTIVATE)) { - results.add(node); - } - }); - - return results; -} - -function hasProperty(node: ts.ObjectLiteralExpression, propertyName: string): boolean { - for (const property of node.properties) { - // Only look for regular and shorthand property assignments since resolving things - // like spread operators becomes too complicated for this migration. - if ((ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) && - (ts.isStringLiteralLike(property.name) || ts.isNumericLiteral(property.name) || - ts.isIdentifier(property.name)) && - property.name.text === propertyName) { - return true; - } - } - return false; -} diff --git a/packages/core/schematics/migrations/deep-shadow-piercing-selector/BUILD.bazel b/packages/core/schematics/migrations/deep-shadow-piercing-selector/BUILD.bazel deleted file mode 100644 index 00f141fcd4a..00000000000 --- a/packages/core/schematics/migrations/deep-shadow-piercing-selector/BUILD.bazel +++ /dev/null @@ -1,16 +0,0 @@ -load("//tools:defaults.bzl", "ts_library") - -ts_library( - name = "deep-shadow-piercing-selector", - srcs = glob(["**/*.ts"]), - tsconfig = "//packages/core/schematics:tsconfig.json", - visibility = [ - "//packages/core/schematics:__pkg__", - "//packages/core/schematics/migrations/google3:__pkg__", - "//packages/core/schematics/test:__pkg__", - ], - deps = [ - "@npm//@angular-devkit/core", - "@npm//@angular-devkit/schematics", - ], -) diff --git a/packages/core/schematics/migrations/deep-shadow-piercing-selector/README.md b/packages/core/schematics/migrations/deep-shadow-piercing-selector/README.md deleted file mode 100644 index 4215e54ec53..00000000000 --- a/packages/core/schematics/migrations/deep-shadow-piercing-selector/README.md +++ /dev/null @@ -1,17 +0,0 @@ -## shadow-piercing selector `/deep/` to `::ng-deep` - -Automatically migrates shadow-piercing selector from `/deep/` to `::ng-deep`. - -#### Before -```css -:host /deep/ * { - cursor: pointer; -} -``` - -#### After -```css -:host ::ng-deep * { - cursor: pointer; -} -``` diff --git a/packages/core/schematics/migrations/deep-shadow-piercing-selector/index.ts b/packages/core/schematics/migrations/deep-shadow-piercing-selector/index.ts deleted file mode 100644 index 0e84ebbfeff..00000000000 --- a/packages/core/schematics/migrations/deep-shadow-piercing-selector/index.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 {extname, join} from '@angular-devkit/core'; -import {DirEntry, Rule} from '@angular-devkit/schematics'; - -const VALID_EXTENSIONS = ['.scss', '.sass', '.css', '.styl', '.less', '.ts']; - -function* visitFiles(directory: DirEntry): IterableIterator { - for (const path of directory.subfiles) { - const extension = extname(path); - if (VALID_EXTENSIONS.includes(extension)) { - yield join(directory.path, path); - } - } - - for (const path of directory.subdirs) { - if (path === 'node_modules' || path.startsWith('.') || path === 'dist') { - continue; - } - - yield* visitFiles(directory.dir(path)); - } -} - -export default function(): Rule { - return (tree) => { - // Visit all files in an Angular workspace monorepo. - for (const file of visitFiles(tree.root)) { - const content = tree.read(file)?.toString(); - if (content?.includes('/deep/ ')) { - tree.overwrite(file, content.replace(/\/deep\/ /g, '::ng-deep ')); - } - } - }; -} diff --git a/packages/core/schematics/migrations/dynamic-queries/BUILD.bazel b/packages/core/schematics/migrations/dynamic-queries/BUILD.bazel deleted file mode 100644 index 686593cb042..00000000000 --- a/packages/core/schematics/migrations/dynamic-queries/BUILD.bazel +++ /dev/null @@ -1,18 +0,0 @@ -load("//tools:defaults.bzl", "ts_library") - -ts_library( - name = "dynamic-queries", - srcs = glob(["**/*.ts"]), - tsconfig = "//packages/core/schematics:tsconfig.json", - visibility = [ - "//packages/core/schematics:__pkg__", - "//packages/core/schematics/migrations/google3:__pkg__", - "//packages/core/schematics/test:__pkg__", - ], - deps = [ - "//packages/core/schematics/utils", - "@npm//@angular-devkit/schematics", - "@npm//@types/node", - "@npm//typescript", - ], -) diff --git a/packages/core/schematics/migrations/dynamic-queries/README.md b/packages/core/schematics/migrations/dynamic-queries/README.md deleted file mode 100644 index 9a469d50932..00000000000 --- a/packages/core/schematics/migrations/dynamic-queries/README.md +++ /dev/null @@ -1,28 +0,0 @@ -## Dynamic queries migration - -Automatically migrates dynamic queries to remove their `static` flag. This flag will no -longer be necessary in version 9 for dynamic queries, as `false` is the default value. - -#### Before -```ts -import { Directive, ViewChild, ContentChild, ElementRef } from '@angular/core'; - -@Directive() -export class MyDirective { - @ViewChild('child', { static: false }) child: any; - @ViewChild('secondChild', { read: ElementRef, static: false }) secondChild: ElementRef; - @ContentChild('thirdChild', { static: false }) thirdChild: any; -} -``` - -#### After -```ts -import { Directive, ViewChild, ContentChild, ElementRef } from '@angular/core'; - -@Directive() -export class MyDirective { - @ViewChild('child') child: any; - @ViewChild('secondChild', { read: ElementRef }) secondChild: ElementRef; - @ContentChild('thirdChild') thirdChild: any; -} -``` diff --git a/packages/core/schematics/migrations/dynamic-queries/index.ts b/packages/core/schematics/migrations/dynamic-queries/index.ts deleted file mode 100644 index 265e7e9371e..00000000000 --- a/packages/core/schematics/migrations/dynamic-queries/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * @license - * Copyright Google LLC 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, SchematicsException, Tree} from '@angular-devkit/schematics'; -import {relative} from 'path'; -import ts from 'typescript'; - -import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; -import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host'; - -import {identifyDynamicQueryNodes, removeOptionsParameter, removeStaticFlag} from './util'; - - -/** - * Runs the dynamic queries migration for all TypeScript projects in the current CLI workspace. - */ -export default function(): Rule { - return async (tree: Tree) => { - const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); - const basePath = process.cwd(); - const allPaths = [...buildPaths, ...testPaths]; - - if (!allPaths.length) { - throw new SchematicsException( - 'Could not find any tsconfig file. Cannot migrate dynamic queries.'); - } - - for (const tsconfigPath of allPaths) { - runDynamicQueryMigration(tree, tsconfigPath, basePath); - } - }; -} - -function runDynamicQueryMigration(tree: Tree, tsconfigPath: string, basePath: string) { - const {program} = createMigrationProgram(tree, tsconfigPath, basePath); - const typeChecker = program.getTypeChecker(); - const sourceFiles = - program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program)); - const printer = ts.createPrinter(); - - sourceFiles.forEach(sourceFile => { - const result = identifyDynamicQueryNodes(typeChecker, sourceFile); - - if (result.removeProperty.length || result.removeParameter.length) { - const update = tree.beginUpdate(relative(basePath, sourceFile.fileName)); - - result.removeProperty.forEach(node => { - update.remove(node.getStart(), node.getWidth()); - update.insertRight( - node.getStart(), - printer.printNode(ts.EmitHint.Unspecified, removeStaticFlag(node), sourceFile)); - }); - - result.removeParameter.forEach(node => { - update.remove(node.getStart(), node.getWidth()); - update.insertRight( - node.getStart(), - printer.printNode(ts.EmitHint.Unspecified, removeOptionsParameter(node), sourceFile)); - }); - - tree.commitUpdate(update); - } - }); -} diff --git a/packages/core/schematics/migrations/dynamic-queries/util.ts b/packages/core/schematics/migrations/dynamic-queries/util.ts deleted file mode 100644 index f5cd7c861ac..00000000000 --- a/packages/core/schematics/migrations/dynamic-queries/util.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; -import {getAngularDecorators} from '../../utils/ng_decorators'; - -/** - * Identifies the nodes that should be migrated by the dynamic - * queries schematic. Splits the nodes into the following categories: - * - `removeProperty` - queries from which we should only remove the `static` property of the - * `options` parameter (e.g. `@ViewChild('child', {static: false, read: ElementRef})`). - * - `removeParameter` - queries from which we should drop the entire `options` parameter. - * (e.g. `@ViewChild('child', {static: false})`). - */ -export function identifyDynamicQueryNodes(typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile) { - const removeProperty: ts.ObjectLiteralExpression[] = []; - const removeParameter: ts.CallExpression[] = []; - - sourceFile.forEachChild(function walk(node: ts.Node) { - if (ts.isClassDeclaration(node)) { - node.members.forEach(member => { - const angularDecorators = - member.decorators && getAngularDecorators(typeChecker, member.decorators); - - if (angularDecorators) { - angularDecorators - // Filter out the queries that can have the `static` flag. - .filter(decorator => { - return decorator.name === 'ViewChild' || decorator.name === 'ContentChild'; - }) - // Filter out the queries where the `static` flag is explicitly set to `false`. - .filter(decorator => { - const options = decorator.node.expression.arguments[1]; - return options && ts.isObjectLiteralExpression(options) && - options.properties.some( - property => ts.isPropertyAssignment(property) && - property.initializer.kind === ts.SyntaxKind.FalseKeyword); - }) - .forEach(decorator => { - const options = - decorator.node.expression.arguments[1] as ts.ObjectLiteralExpression; - - // At this point we know that at least one property is the `static` flag. If this is - // the only property we can drop the entire object literal, otherwise we have to - // drop only the property. - if (options.properties.length === 1) { - removeParameter.push(decorator.node.expression); - } else { - removeProperty.push(options); - } - }); - } - }); - } - - node.forEachChild(walk); - }); - - return {removeProperty, removeParameter}; -} - -/** Removes the `options` parameter from the call expression of a query decorator. */ -export function removeOptionsParameter(node: ts.CallExpression): ts.CallExpression { - return ts.updateCall(node, node.expression, node.typeArguments, [node.arguments[0]]); -} - -/** Removes the `static` property from an object literal expression. */ -export function removeStaticFlag(node: ts.ObjectLiteralExpression): ts.ObjectLiteralExpression { - return ts.updateObjectLiteral( - node, - node.properties.filter(property => property.name && property.name.getText() !== 'static')); -} diff --git a/packages/core/schematics/migrations/google3/BUILD.bazel b/packages/core/schematics/migrations/google3/BUILD.bazel index 3a71b955b48..e7a14c4c7fc 100644 --- a/packages/core/schematics/migrations/google3/BUILD.bazel +++ b/packages/core/schematics/migrations/google3/BUILD.bazel @@ -6,28 +6,7 @@ ts_library( tsconfig = "//packages/core/schematics:tsconfig.json", visibility = ["//packages/core/schematics/test/google3:__pkg__"], deps = [ - "//packages/compiler", - "//packages/compiler-cli/src/ngtsc/annotations", - "//packages/compiler-cli/src/ngtsc/imports", - "//packages/compiler-cli/src/ngtsc/partial_evaluator", - "//packages/compiler-cli/src/ngtsc/reflection", - "//packages/core/schematics/migrations/activated-route-snapshot-fragment", - "//packages/core/schematics/migrations/can-activate-with-redirect-to", - "//packages/core/schematics/migrations/dynamic-queries", - "//packages/core/schematics/migrations/initial-navigation", - "//packages/core/schematics/migrations/initial-navigation/google3", - "//packages/core/schematics/migrations/missing-injectable", - "//packages/core/schematics/migrations/missing-injectable/google3", - "//packages/core/schematics/migrations/navigation-extras-omissions", - "//packages/core/schematics/migrations/relative-link-resolution", - "//packages/core/schematics/migrations/relative-link-resolution/google3", - "//packages/core/schematics/migrations/renderer-to-renderer2", - "//packages/core/schematics/migrations/static-queries", - "//packages/core/schematics/migrations/template-var-assignment", "//packages/core/schematics/migrations/testbed-teardown", - "//packages/core/schematics/migrations/undecorated-classes-with-decorated-fields", - "//packages/core/schematics/migrations/undecorated-classes-with-decorated-fields/google3", - "//packages/core/schematics/migrations/wait-for-async", "//packages/core/schematics/utils", "//packages/core/schematics/utils/tslint", "@npm//tslint", diff --git a/packages/core/schematics/migrations/google3/activatedRouteSnapshotFragmentRule.ts b/packages/core/schematics/migrations/google3/activatedRouteSnapshotFragmentRule.ts deleted file mode 100644 index 35d67753a9c..00000000000 --- a/packages/core/schematics/migrations/google3/activatedRouteSnapshotFragmentRule.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 {Replacement, RuleFailure, Rules} from 'tslint'; -import ts from 'typescript'; - -import {findFragmentAccesses, migrateActivatedRouteSnapshotFragment} from '../activated-route-snapshot-fragment/util'; - -export class Rule extends Rules.TypedRule { - override applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { - if (sourceFile.isDeclarationFile || program.isSourceFileFromExternalLibrary(sourceFile)) { - return []; - } - - const failures: RuleFailure[] = []; - const typeChecker = program.getTypeChecker(); - const nodesToMigrate = findFragmentAccesses(typeChecker, sourceFile); - - if (nodesToMigrate.size > 0) { - const printer = ts.createPrinter(); - nodesToMigrate.forEach(node => { - const sourceFile = node.getSourceFile(); - const migratedNode = migrateActivatedRouteSnapshotFragment(node); - const replacement = new Replacement( - node.getStart(), node.getWidth(), - printer.printNode(ts.EmitHint.Unspecified, migratedNode, sourceFile)); - failures.push(new RuleFailure( - sourceFile, node.getStart(), node.getEnd(), - '`ActivatedRouteSnapshot.fragment` is nullable.', this.ruleName, replacement)); - }); - } - - return failures; - } -} diff --git a/packages/core/schematics/migrations/google3/canActivateWithRedirectToRule.ts b/packages/core/schematics/migrations/google3/canActivateWithRedirectToRule.ts deleted file mode 100644 index b3ada7e001e..00000000000 --- a/packages/core/schematics/migrations/google3/canActivateWithRedirectToRule.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 {Replacement, RuleFailure, Rules} from 'tslint'; -import ts from 'typescript'; -import {findLiteralsToMigrate, migrateLiteral} from '../can-activate-with-redirect-to/util'; - - -/** TSLint rule that removes canActivate from Route configs that also have redirectTo. */ -export class Rule extends Rules.TypedRule { - override applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { - const failures: RuleFailure[] = []; - const printer = ts.createPrinter(); - const literalsToMigrate = findLiteralsToMigrate(sourceFile); - - for (const literal of Array.from(literalsToMigrate)) { - const migratedNode = migrateLiteral(literal); - failures.push(new RuleFailure( - sourceFile, literal.getStart(), literal.getEnd(), - 'canActivate cannot be used with redirectTo.', this.ruleName, - new Replacement( - literal.getStart(), literal.getWidth(), - printer.printNode(ts.EmitHint.Unspecified, migratedNode, sourceFile)))); - } - - return failures; - } -} diff --git a/packages/core/schematics/migrations/google3/dynamicQueriesRule.ts b/packages/core/schematics/migrations/google3/dynamicQueriesRule.ts deleted file mode 100644 index 42969743369..00000000000 --- a/packages/core/schematics/migrations/google3/dynamicQueriesRule.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 {Replacement, RuleFailure, Rules} from 'tslint'; -import ts from 'typescript'; - -import {identifyDynamicQueryNodes, removeOptionsParameter, removeStaticFlag} from '../dynamic-queries/util'; - -const RULE_NAME = 'dynamic-queries'; -const FAILURE_MESSAGE = - 'The static flag defaults to false, so setting it false manually is unnecessary.'; - -/** - * TSLint rule that removes the `static` flag from dynamic queries. - */ -export class Rule extends Rules.TypedRule { - override applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { - const printer = ts.createPrinter(); - const failures: RuleFailure[] = []; - const result = identifyDynamicQueryNodes(program.getTypeChecker(), sourceFile); - - result.removeProperty.forEach(node => { - failures.push(new RuleFailure( - sourceFile, node.getStart(), node.getEnd(), FAILURE_MESSAGE, RULE_NAME, - new Replacement( - node.getStart(), node.getWidth(), - printer.printNode(ts.EmitHint.Unspecified, removeStaticFlag(node), sourceFile)))); - }); - - result.removeParameter.forEach(node => { - failures.push(new RuleFailure( - sourceFile, node.getStart(), node.getEnd(), FAILURE_MESSAGE, RULE_NAME, - new Replacement( - node.getStart(), node.getWidth(), - printer.printNode( - ts.EmitHint.Unspecified, removeOptionsParameter(node), sourceFile)))); - }); - - return failures; - } -} diff --git a/packages/core/schematics/migrations/google3/explicitQueryTimingRule.ts b/packages/core/schematics/migrations/google3/explicitQueryTimingRule.ts deleted file mode 100644 index 667f929f454..00000000000 --- a/packages/core/schematics/migrations/google3/explicitQueryTimingRule.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 * as compiler from '@angular/compiler'; -import {Replacement, RuleFailure, Rules} from 'tslint'; -import ts from 'typescript'; - -import {NgComponentTemplateVisitor} from '../../utils/ng_component_template'; -import {NgQueryResolveVisitor} from '../static-queries/angular/ng_query_visitor'; -import {QueryTiming} from '../static-queries/angular/query-definition'; -import {QueryUsageStrategy} from '../static-queries/strategies/usage_strategy/usage_strategy'; -import {getTransformedQueryCallExpr} from '../static-queries/transform'; - -const FAILURE_MESSAGE = 'Query does not explicitly specify its timing. Read more here: ' + - 'https://github.com/angular/angular/pull/28810'; - -/** - * Rule that reports if an Angular "ViewChild" or "ContentChild" query is not explicitly - * specifying its timing. The rule also provides TSLint automatic replacements that can - * be applied in order to automatically migrate to the explicit query timing API. - */ -export class Rule extends Rules.TypedRule { - override applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { - const typeChecker = program.getTypeChecker(); - const queryVisitor = new NgQueryResolveVisitor(program.getTypeChecker()); - const templateVisitor = new NgComponentTemplateVisitor(typeChecker); - const rootSourceFiles = program.getRootFileNames().map(f => program.getSourceFile(f)!); - const printer = ts.createPrinter(); - const failures: RuleFailure[] = []; - - // Analyze source files by detecting queries, class relations and component templates. - rootSourceFiles.forEach(sourceFile => { - queryVisitor.visitNode(sourceFile); - templateVisitor.visitNode(sourceFile); - }); - - const {resolvedQueries, classMetadata} = queryVisitor; - - // Add all resolved templates to the class metadata so that we can also - // check component templates for static query usage. - templateVisitor.resolvedTemplates.forEach(template => { - if (classMetadata.has(template.container)) { - classMetadata.get(template.container)!.template = template; - } - }); - - const queries = resolvedQueries.get(sourceFile); - const usageStrategy = new QueryUsageStrategy(classMetadata, typeChecker, compiler); - - // No queries detected for the given source file. - if (!queries) { - return []; - } - - // Compute the query usage for all resolved queries and update the - // query definitions to explicitly declare the query timing (static or dynamic) - queries.forEach(q => { - const queryExpr = q.decorator.node.expression; - const {timing, message} = usageStrategy.detectTiming(q); - const result = getTransformedQueryCallExpr(q, timing, !!message); - - if (!result) { - return; - } - - const newText = printer.printNode(ts.EmitHint.Unspecified, result.node, sourceFile); - - // Replace the existing query decorator call expression with the - // updated call expression node. - const fix = new Replacement(queryExpr.getStart(), queryExpr.getWidth(), newText); - const failureMessage = `${FAILURE_MESSAGE}. Based on analysis of the query it can be ` + - `marked as "{static: ${(timing === QueryTiming.STATIC).toString()}}".`; - - failures.push(new RuleFailure( - sourceFile, queryExpr.getStart(), queryExpr.getEnd(), failureMessage, this.ruleName, - fix)); - }); - - return failures; - } -} diff --git a/packages/core/schematics/migrations/google3/initialNavigationRule.ts b/packages/core/schematics/migrations/google3/initialNavigationRule.ts deleted file mode 100644 index c8186550ede..00000000000 --- a/packages/core/schematics/migrations/google3/initialNavigationRule.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 {RuleFailure, Rules} from 'tslint'; -import ts from 'typescript'; - -import {InitialNavigationCollector} from '../initial-navigation/collector'; -import {TslintUpdateRecorder} from '../initial-navigation/google3/tslint_update_recorder'; -import {InitialNavigationTransform} from '../initial-navigation/transform'; - - - -/** - * TSLint rule that updates RouterModule `forRoot` options to be in line with v10 updates. - */ -export class Rule extends Rules.TypedRule { - override applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { - const ruleName = this.ruleName; - const typeChecker = program.getTypeChecker(); - const sourceFiles = program.getSourceFiles().filter( - s => !s.isDeclarationFile && !program.isSourceFileFromExternalLibrary(s)); - const initialNavigationCollector = new InitialNavigationCollector(typeChecker); - const failures: RuleFailure[] = []; - - // Analyze source files by detecting all ExtraOptions#InitialNavigation assignments - sourceFiles.forEach(sourceFile => initialNavigationCollector.visitNode(sourceFile)); - - const {assignments} = initialNavigationCollector; - const transformer = new InitialNavigationTransform(getUpdateRecorder); - const updateRecorders = new Map(); - - transformer.migrateInitialNavigationAssignments(Array.from(assignments)); - - if (updateRecorders.has(sourceFile)) { - failures.push(...updateRecorders.get(sourceFile)!.failures); - } - - return failures; - - /** Gets the update recorder for the specified source file. */ - function getUpdateRecorder(sourceFile: ts.SourceFile): TslintUpdateRecorder { - if (updateRecorders.has(sourceFile)) { - return updateRecorders.get(sourceFile)!; - } - const recorder = new TslintUpdateRecorder(ruleName, sourceFile); - updateRecorders.set(sourceFile, recorder); - return recorder; - } - } -} diff --git a/packages/core/schematics/migrations/google3/navigationExtrasOmissionsRule.ts b/packages/core/schematics/migrations/google3/navigationExtrasOmissionsRule.ts deleted file mode 100644 index f95c80ebb04..00000000000 --- a/packages/core/schematics/migrations/google3/navigationExtrasOmissionsRule.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 {Replacement, RuleFailure, Rules} from 'tslint'; -import ts from 'typescript'; -import {findLiteralsToMigrate, migrateLiteral} from '../../migrations/navigation-extras-omissions/util'; - - -/** TSLint rule that migrates `navigateByUrl` and `createUrlTree` calls to an updated signature. */ -export class Rule extends Rules.TypedRule { - override applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { - const failures: RuleFailure[] = []; - const typeChecker = program.getTypeChecker(); - const printer = ts.createPrinter(); - const literalsToMigrate = findLiteralsToMigrate(sourceFile, typeChecker); - - literalsToMigrate.forEach((instances, methodName) => instances.forEach(instance => { - const migratedNode = migrateLiteral(methodName, instance); - - if (migratedNode !== instance) { - failures.push(new RuleFailure( - sourceFile, instance.getStart(), instance.getEnd(), - 'Object used in navigateByUrl or createUrlTree call contains unsupported properties.', - this.ruleName, - new Replacement( - instance.getStart(), instance.getWidth(), - printer.printNode(ts.EmitHint.Unspecified, migratedNode, sourceFile)))); - } - })); - - return failures; - } -} diff --git a/packages/core/schematics/migrations/google3/noMissingInjectableRule.ts b/packages/core/schematics/migrations/google3/noMissingInjectableRule.ts deleted file mode 100644 index e1eabb0cac7..00000000000 --- a/packages/core/schematics/migrations/google3/noMissingInjectableRule.ts +++ /dev/null @@ -1,81 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 {forwardRefResolver} from '@angular/compiler-cli/src/ngtsc/annotations'; -import {Reference} from '@angular/compiler-cli/src/ngtsc/imports'; -import {DynamicValue, PartialEvaluator} from '@angular/compiler-cli/src/ngtsc/partial_evaluator'; -import {StaticInterpreter} from '@angular/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter'; -import {reflectObjectLiteral, TypeScriptReflectionHost} from '@angular/compiler-cli/src/ngtsc/reflection'; -import {RuleFailure, Rules} from 'tslint'; -import ts from 'typescript'; - -import {NgDefinitionCollector} from '../missing-injectable/definition_collector'; -import {TslintUpdateRecorder} from '../missing-injectable/google3/tslint_update_recorder'; -import {MissingInjectableTransform} from '../missing-injectable/transform'; - - - -/** - * TSLint rule that flags classes which are declared as providers in "NgModule", - * "Directive" or "Component" definitions while not being decorated with any - * Angular decorator (e.g. "@Injectable"). - */ -export class Rule extends Rules.TypedRule { - override applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { - const ruleName = this.ruleName; - const typeChecker = program.getTypeChecker(); - const sourceFiles = program.getSourceFiles().filter( - s => !s.isDeclarationFile && !program.isSourceFileFromExternalLibrary(s)); - const definitionCollector = new NgDefinitionCollector(typeChecker); - const failures: RuleFailure[] = []; - - // Analyze source files by detecting all "NgModule", "Directive" or - // "Component" definitions. - sourceFiles.forEach(sourceFile => definitionCollector.visitNode(sourceFile)); - - const {resolvedModules, resolvedDirectives} = definitionCollector; - const transformer = new MissingInjectableTransform(typeChecker, getUpdateRecorder, { - Reference, - DynamicValue, - PartialEvaluator, - StaticInterpreter, - TypeScriptReflectionHost, - forwardRefResolver, - reflectObjectLiteral, - }); - const updateRecorders = new Map(); - - [...transformer.migrateModules(resolvedModules), - ...transformer.migrateDirectives(resolvedDirectives), - ].forEach(({message, node}) => { - // Only report failures for the current source file that is visited. - if (node.getSourceFile() === sourceFile) { - failures.push(new RuleFailure(node.getSourceFile(), node.getStart(), 0, message, ruleName)); - } - }); - - // Record the changes collected in the import manager and NgModule manager. - transformer.recordChanges(); - - if (updateRecorders.has(sourceFile)) { - failures.push(...updateRecorders.get(sourceFile)!.failures); - } - - return failures; - - /** Gets the update recorder for the specified source file. */ - function getUpdateRecorder(sourceFile: ts.SourceFile): TslintUpdateRecorder { - if (updateRecorders.has(sourceFile)) { - return updateRecorders.get(sourceFile)!; - } - const recorder = new TslintUpdateRecorder(ruleName, sourceFile); - updateRecorders.set(sourceFile, recorder); - return recorder; - } - } -} diff --git a/packages/core/schematics/migrations/google3/noTemplateVariableAssignmentRule.ts b/packages/core/schematics/migrations/google3/noTemplateVariableAssignmentRule.ts deleted file mode 100644 index 771bb5d47a2..00000000000 --- a/packages/core/schematics/migrations/google3/noTemplateVariableAssignmentRule.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 * as compiler from '@angular/compiler'; -import {RuleFailure, Rules} from 'tslint'; -import ts from 'typescript'; - -import {NgComponentTemplateVisitor} from '../../utils/ng_component_template'; -import {createHtmlSourceFile} from '../../utils/tslint/tslint_html_source_file'; -import {analyzeResolvedTemplate} from '../template-var-assignment/analyze_template'; - -const FAILURE_MESSAGE = 'Found assignment to template variable. This does not work with Ivy and ' + - 'needs to be updated.'; - -/** - * Rule that reports if an Angular template contains property assignments to template variables. - */ -export class Rule extends Rules.TypedRule { - override applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { - const typeChecker = program.getTypeChecker(); - const templateVisitor = new NgComponentTemplateVisitor(typeChecker); - const failures: RuleFailure[] = []; - - // Analyze the current source files by detecting all referenced HTML templates. - templateVisitor.visitNode(sourceFile); - - const {resolvedTemplates} = templateVisitor; - - // Analyze each resolved template and print a warning for property writes to - // template variables. - resolvedTemplates.forEach(template => { - const filePath = template.filePath; - const nodes = analyzeResolvedTemplate(template, compiler); - const templateFile = - template.inline ? sourceFile : createHtmlSourceFile(filePath, template.content); - - if (!nodes) { - return; - } - - nodes.forEach(n => { - failures.push(new RuleFailure( - templateFile, template.start + n.start, template.start + n.end, FAILURE_MESSAGE, - this.ruleName)); - }); - }); - - return failures; - } -} diff --git a/packages/core/schematics/migrations/google3/relativeLinkResolutionDefaultRule.ts b/packages/core/schematics/migrations/google3/relativeLinkResolutionDefaultRule.ts deleted file mode 100644 index 7b21c10dd2d..00000000000 --- a/packages/core/schematics/migrations/google3/relativeLinkResolutionDefaultRule.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 {RuleFailure, Rules} from 'tslint'; -import ts from 'typescript'; - -import {RelativeLinkResolutionCollector} from '../relative-link-resolution/collector'; -import {TslintUpdateRecorder} from '../relative-link-resolution/google3/tslint_update_recorder'; -import {RelativeLinkResolutionTransform} from '../relative-link-resolution/transform'; - -export class Rule extends Rules.TypedRule { - override applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { - const typeChecker = program.getTypeChecker(); - const ruleName = this.ruleName; - const sourceFiles = program.getSourceFiles().filter( - s => !s.isDeclarationFile && !program.isSourceFileFromExternalLibrary(s)); - const updateRecorders = new Map(); - const relativeLinkResolutionCollector = new RelativeLinkResolutionCollector(typeChecker); - - // Analyze source files by detecting all modules. - sourceFiles.forEach(sourceFile => relativeLinkResolutionCollector.visitNode(sourceFile)); - - const {forRootCalls, extraOptionsLiterals} = relativeLinkResolutionCollector; - const transformer = new RelativeLinkResolutionTransform(getUpdateRecorder); - transformer.migrateRouterModuleForRootCalls(forRootCalls); - transformer.migrateObjectLiterals(extraOptionsLiterals); - - if (updateRecorders.has(sourceFile)) { - return updateRecorders.get(sourceFile)!.failures; - } - return []; - - /** Gets the update recorder for the specified source file. */ - function getUpdateRecorder(sourceFile: ts.SourceFile): TslintUpdateRecorder { - if (updateRecorders.has(sourceFile)) { - return updateRecorders.get(sourceFile)!; - } - const recorder = new TslintUpdateRecorder(ruleName, sourceFile); - updateRecorders.set(sourceFile, recorder); - return recorder; - } - } -} diff --git a/packages/core/schematics/migrations/google3/rendererToRenderer2Rule.ts b/packages/core/schematics/migrations/google3/rendererToRenderer2Rule.ts deleted file mode 100644 index 0182257933a..00000000000 --- a/packages/core/schematics/migrations/google3/rendererToRenderer2Rule.ts +++ /dev/null @@ -1,144 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 {Replacement, RuleFailure, Rules} from 'tslint'; -import ts from 'typescript'; - -import {getImportSpecifier, replaceImport} from '../../utils/typescript/imports'; -import {closestNode} from '../../utils/typescript/nodes'; -import {getHelper, HelperFunction} from '../renderer-to-renderer2/helpers'; -import {migrateExpression} from '../renderer-to-renderer2/migration'; -import {findRendererReferences} from '../renderer-to-renderer2/util'; - -/** - * TSLint rule that migrates from `Renderer` to `Renderer2`. More information on how it works: - * https://hackmd.angular.io/UTzUZTnPRA-cSa_4mHyfYw - */ -export class Rule extends Rules.TypedRule { - override applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { - const typeChecker = program.getTypeChecker(); - const printer = ts.createPrinter(); - const failures: RuleFailure[] = []; - const rendererImportSpecifier = getImportSpecifier(sourceFile, '@angular/core', 'Renderer'); - const rendererImport = rendererImportSpecifier ? - closestNode(rendererImportSpecifier, ts.SyntaxKind.NamedImports) : - null; - - // If there are no imports for the `Renderer`, we can exit early. - if (!rendererImportSpecifier || !rendererImport) { - return failures; - } - - const {typedNodes, methodCalls, forwardRefs} = - findRendererReferences(sourceFile, typeChecker, rendererImportSpecifier); - const helpersToAdd = new Set(); - - failures.push(this._getNamedImportsFailure(rendererImport, sourceFile, printer)); - typedNodes.forEach(node => failures.push(this._getTypedNodeFailure(node, sourceFile))); - forwardRefs.forEach(node => failures.push(this._getIdentifierNodeFailure(node, sourceFile))); - - methodCalls.forEach(call => { - const {failure, requiredHelpers} = - this._getMethodCallFailure(call, sourceFile, typeChecker, printer); - - failures.push(failure); - - if (requiredHelpers) { - requiredHelpers.forEach(helperName => helpersToAdd.add(helperName)); - } - }); - - // Some of the methods can't be mapped directly to `Renderer2` and need extra logic around them. - // The safest way to do so is to declare helper functions similar to the ones emitted by TS - // which encapsulate the extra "glue" logic. We should only emit these functions once per - // file and only if they're needed. - if (helpersToAdd.size) { - failures.push(this._getHelpersFailure(helpersToAdd, sourceFile, printer)); - } - - return failures; - } - - /** Gets a failure for an import of the Renderer. */ - private _getNamedImportsFailure( - node: ts.NamedImports, sourceFile: ts.SourceFile, printer: ts.Printer): RuleFailure { - const replacementText = printer.printNode( - ts.EmitHint.Unspecified, replaceImport(node, 'Renderer', 'Renderer2'), sourceFile); - - return new RuleFailure( - sourceFile, node.getStart(), node.getEnd(), - 'Imports of deprecated Renderer are not allowed. Please use Renderer2 instead.', - this.ruleName, new Replacement(node.getStart(), node.getWidth(), replacementText)); - } - - /** Gets a failure for a typed node (e.g. function parameter or property). */ - private _getTypedNodeFailure( - node: ts.ParameterDeclaration|ts.PropertyDeclaration|ts.AsExpression, - sourceFile: ts.SourceFile): RuleFailure { - const type = node.type!; - - return new RuleFailure( - sourceFile, type.getStart(), type.getEnd(), - 'References to deprecated Renderer are not allowed. Please use Renderer2 instead.', - this.ruleName, new Replacement(type.getStart(), type.getWidth(), 'Renderer2')); - } - - /** Gets a failure for an identifier node. */ - private _getIdentifierNodeFailure(node: ts.Identifier, sourceFile: ts.SourceFile): RuleFailure { - return new RuleFailure( - sourceFile, node.getStart(), node.getEnd(), - 'References to deprecated Renderer are not allowed. Please use Renderer2 instead.', - this.ruleName, new Replacement(node.getStart(), node.getWidth(), 'Renderer2')); - } - - /** Gets a failure for a Renderer method call. */ - private _getMethodCallFailure( - call: ts.CallExpression, sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker, - printer: ts.Printer): {failure: RuleFailure, requiredHelpers?: HelperFunction[]} { - const {node, requiredHelpers} = migrateExpression(call, typeChecker); - let fix: Replacement|undefined; - - if (node) { - // If we migrated the node to a new expression, replace only the call expression. - fix = new Replacement( - call.getStart(), call.getWidth(), - printer.printNode(ts.EmitHint.Unspecified, node, sourceFile)); - } else if (call.parent && ts.isExpressionStatement(call.parent)) { - // Otherwise if the call is inside an expression statement, drop the entire statement. - // This takes care of any trailing semicolons. We only need to drop nodes for cases like - // `setBindingDebugInfo` which have been noop for a while so they can be removed safely. - fix = new Replacement(call.parent.getStart(), call.parent.getWidth(), ''); - } - - return { - failure: new RuleFailure( - sourceFile, call.getStart(), call.getEnd(), 'Calls to Renderer methods are not allowed', - this.ruleName, fix), - requiredHelpers - }; - } - - /** Gets a failure that inserts the required helper functions at the bottom of the file. */ - private _getHelpersFailure( - helpersToAdd: Set, sourceFile: ts.SourceFile, - printer: ts.Printer): RuleFailure { - const helpers: Replacement[] = []; - const endOfFile = sourceFile.endOfFileToken; - - helpersToAdd.forEach(helperName => { - helpers.push(new Replacement( - endOfFile.getStart(), endOfFile.getWidth(), getHelper(helperName, sourceFile, printer))); - }); - - // Add a failure at the end of the file which we can use as an anchor to insert the helpers. - return new RuleFailure( - sourceFile, endOfFile.getStart(), endOfFile.getStart() + 1, - 'File should contain Renderer helper functions. Run tslint with --fix to generate them.', - this.ruleName, helpers); - } -} diff --git a/packages/core/schematics/migrations/google3/undecoratedClassesWithDecoratedFieldsRule.ts b/packages/core/schematics/migrations/google3/undecoratedClassesWithDecoratedFieldsRule.ts deleted file mode 100644 index 99492c0efb7..00000000000 --- a/packages/core/schematics/migrations/google3/undecoratedClassesWithDecoratedFieldsRule.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 {forwardRefResolver} from '@angular/compiler-cli/src/ngtsc/annotations'; -import {Reference} from '@angular/compiler-cli/src/ngtsc/imports'; -import {DynamicValue, PartialEvaluator} from '@angular/compiler-cli/src/ngtsc/partial_evaluator'; -import {StaticInterpreter} from '@angular/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter'; -import {reflectObjectLiteral, TypeScriptReflectionHost} from '@angular/compiler-cli/src/ngtsc/reflection'; -import {RuleFailure, Rules} from 'tslint'; -import ts from 'typescript'; -import {TslintUpdateRecorder} from '../undecorated-classes-with-decorated-fields/google3/tslint_update_recorder'; -import {UndecoratedClassesWithDecoratedFieldsTransform} from '../undecorated-classes-with-decorated-fields/transform'; - -/** - * TSLint rule that adds an Angular decorator to classes that have Angular field decorators. - * https://hackmd.io/vuQfavzfRG6KUCtU7oK_EA - */ -export class Rule extends Rules.TypedRule { - override applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { - const typeChecker = program.getTypeChecker(); - const ruleName = this.ruleName; - const sourceFiles = program.getSourceFiles().filter( - s => !s.isDeclarationFile && !program.isSourceFileFromExternalLibrary(s)); - const updateRecorders = new Map(); - const transform = - new UndecoratedClassesWithDecoratedFieldsTransform(typeChecker, getUpdateRecorder, { - Reference, - DynamicValue, - PartialEvaluator, - StaticInterpreter, - TypeScriptReflectionHost, - forwardRefResolver, - reflectObjectLiteral, - }); - - // Migrate all source files in the project. - transform.migrate(sourceFiles); - - // Record the changes collected in the import manager. - transform.recordChanges(); - - if (updateRecorders.has(sourceFile)) { - return updateRecorders.get(sourceFile)!.failures; - } - return []; - - /** Gets the update recorder for the specified source file. */ - function getUpdateRecorder(sourceFile: ts.SourceFile): TslintUpdateRecorder { - if (updateRecorders.has(sourceFile)) { - return updateRecorders.get(sourceFile)!; - } - const recorder = new TslintUpdateRecorder(ruleName, sourceFile); - updateRecorders.set(sourceFile, recorder); - return recorder; - } - } -} diff --git a/packages/core/schematics/migrations/google3/waitForAsyncRule.ts b/packages/core/schematics/migrations/google3/waitForAsyncRule.ts index 9379380a0ac..042db2c9ad7 100644 --- a/packages/core/schematics/migrations/google3/waitForAsyncRule.ts +++ b/packages/core/schematics/migrations/google3/waitForAsyncRule.ts @@ -9,9 +9,11 @@ import {Replacement, RuleFailure, Rules} from 'tslint'; import ts from 'typescript'; -import {findAsyncReferences} from '../../migrations/wait-for-async/util'; import {getImportSpecifier, replaceImport} from '../../utils/typescript/imports'; import {closestNode} from '../../utils/typescript/nodes'; +import {isReferenceToImport} from '../../utils/typescript/symbol'; + +// This rule is also used inside of Google by Typescript linting. /** Name of the deprecated function that we're removing. */ const deprecatedFunction = 'async'; @@ -34,8 +36,8 @@ export class Rule extends Rules.TypedRule { const typeChecker = program.getTypeChecker(); const printer = ts.createPrinter(); failures.push(this._getNamedImportsFailure(asyncImport, sourceFile, printer)); - findAsyncReferences(sourceFile, typeChecker, asyncImportSpecifier) - .forEach(node => failures.push(this._getIdentifierNodeFailure(node, sourceFile))); + this.findAsyncReferences(sourceFile, typeChecker, asyncImportSpecifier) + .forEach((node) => failures.push(this._getIdentifierNodeFailure(node, sourceFile))); } return failures; @@ -62,4 +64,22 @@ export class Rule extends Rules.TypedRule { newFunction} instead.`, this.ruleName, new Replacement(node.getStart(), node.getWidth(), newFunction)); } + + /** Finds calls to the `async` function. */ + private findAsyncReferences( + sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker, + asyncImportSpecifier: ts.ImportSpecifier) { + const results = new Set(); + ts.forEachChild(sourceFile, function visitNode(node: ts.Node) { + if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && + node.expression.text === deprecatedFunction && + isReferenceToImport(typeChecker, node.expression, asyncImportSpecifier)) { + results.add(node.expression); + } + + ts.forEachChild(node, visitNode); + }); + + return results; + } } diff --git a/packages/core/schematics/migrations/initial-navigation/BUILD.bazel b/packages/core/schematics/migrations/initial-navigation/BUILD.bazel deleted file mode 100644 index 43326967b01..00000000000 --- a/packages/core/schematics/migrations/initial-navigation/BUILD.bazel +++ /dev/null @@ -1,19 +0,0 @@ -load("//tools:defaults.bzl", "ts_library") - -ts_library( - name = "initial-navigation", - srcs = glob(["**/*.ts"]), - tsconfig = "//packages/core/schematics:tsconfig.json", - visibility = [ - "//packages/core/schematics:__pkg__", - "//packages/core/schematics/migrations/google3:__pkg__", - "//packages/core/schematics/migrations/initial-navigation/google3:__pkg__", - "//packages/core/schematics/test:__pkg__", - ], - deps = [ - "//packages/core/schematics/utils", - "@npm//@angular-devkit/schematics", - "@npm//@types/node", - "@npm//typescript", - ], -) diff --git a/packages/core/schematics/migrations/initial-navigation/README.md b/packages/core/schematics/migrations/initial-navigation/README.md deleted file mode 100644 index a279f273c76..00000000000 --- a/packages/core/schematics/migrations/initial-navigation/README.md +++ /dev/null @@ -1,43 +0,0 @@ -## initialNavigation migration - -Automatically migrates the `initialNavigation` property of the `RouterModule` to the newly -available options: `enabledNonBlocking` (default), `enabledBlocking`, and `disabled`. - -#### Before -```ts -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; - -@NgModule({ - imports: [ - RouterModule.forRoot(ROUTES, {initialNavigation: 'legacy_disabled'}), - ] -}) -export class AppModule { -} -``` - -#### After -```ts -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; - -@NgModule({ - imports: [ - RouterModule.forRoot(ROUTES, {initialNavigation: 'disabled'}), - ] -}) -export class AppModule { -} -``` - -### Disclaimer - -The migration only covers the most common patterns where developers set the `ExtraOptions#InitialNavigation` -option to an outdated value. Therefore, if a user declares the option using a number of other methods, -e.g. shorthand notation, variable declaration, or some other crafty method, they will have to migrate -those options by hand. Otherwise, the compiler will error if the types are sufficiently enforced. - -The basic migration strategy is as follows: -* `legacy_disabled` || `false` => `disabled` -* `legacy_enabled` || `true` => `enabledNonBlocking` (new default) \ No newline at end of file diff --git a/packages/core/schematics/migrations/initial-navigation/collector.ts b/packages/core/schematics/migrations/initial-navigation/collector.ts deleted file mode 100644 index a1c3d9d6bcb..00000000000 --- a/packages/core/schematics/migrations/initial-navigation/collector.ts +++ /dev/null @@ -1,117 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; -import {isExtraOptions, isRouterModuleForRoot} from './util'; - - -/** The property name for the options that need to be migrated */ -const INITIAL_NAVIGATION = 'initialNavigation'; - -/** - * Visitor that walks through specified TypeScript nodes and collects all - * found ExtraOptions#InitialNavigation assignments. - */ -export class InitialNavigationCollector { - public assignments: Set = new Set(); - - constructor(private readonly typeChecker: ts.TypeChecker) {} - - visitNode(node: ts.Node) { - let extraOptionsLiteral: ts.ObjectLiteralExpression|null = null; - if (isRouterModuleForRoot(this.typeChecker, node) && node.arguments.length > 0) { - if (node.arguments.length === 1) { - return; - } - - if (ts.isObjectLiteralExpression(node.arguments[1])) { - extraOptionsLiteral = node.arguments[1] as ts.ObjectLiteralExpression; - } else if (ts.isIdentifier(node.arguments[1])) { - extraOptionsLiteral = - this.getLiteralNeedingMigrationFromIdentifier(node.arguments[1] as ts.Identifier); - } - } else if (ts.isVariableDeclaration(node)) { - extraOptionsLiteral = this.getLiteralNeedingMigration(node); - } - - if (extraOptionsLiteral !== null) { - this.visitExtraOptionsLiteral(extraOptionsLiteral); - } else { - // no match found, continue iteration - ts.forEachChild(node, n => this.visitNode(n)); - } - } - - visitExtraOptionsLiteral(extraOptionsLiteral: ts.ObjectLiteralExpression) { - for (const prop of extraOptionsLiteral.properties) { - if (ts.isPropertyAssignment(prop) && - (ts.isIdentifier(prop.name) || ts.isStringLiteralLike(prop.name))) { - if (prop.name.text === INITIAL_NAVIGATION && isValidInitialNavigationValue(prop)) { - this.assignments.add(prop); - } - } else if (ts.isSpreadAssignment(prop) && ts.isIdentifier(prop.expression)) { - const literalFromSpreadAssignment = - this.getLiteralNeedingMigrationFromIdentifier(prop.expression); - if (literalFromSpreadAssignment !== null) { - this.visitExtraOptionsLiteral(literalFromSpreadAssignment); - } - } - } - } - - private getLiteralNeedingMigrationFromIdentifier(id: ts.Identifier): ts.ObjectLiteralExpression - |null { - const symbolForIdentifier = this.typeChecker.getSymbolAtLocation(id); - if (symbolForIdentifier === undefined) { - return null; - } - - if (symbolForIdentifier.declarations === undefined || - symbolForIdentifier.declarations.length === 0) { - return null; - } - - const declarationNode = symbolForIdentifier.declarations[0]; - if (!ts.isVariableDeclaration(declarationNode) || declarationNode.initializer === undefined || - !ts.isObjectLiteralExpression(declarationNode.initializer)) { - return null; - } - - return declarationNode.initializer; - } - - private getLiteralNeedingMigration(node: ts.VariableDeclaration): ts.ObjectLiteralExpression - |null { - if (node.initializer === undefined) { - return null; - } - - // declaration could be `x: ExtraOptions = {}` or `x = {} as ExtraOptions` - if (ts.isAsExpression(node.initializer) && - ts.isObjectLiteralExpression(node.initializer.expression) && - isExtraOptions(this.typeChecker, node.initializer.type)) { - return node.initializer.expression; - } else if ( - node.type !== undefined && ts.isObjectLiteralExpression(node.initializer) && - isExtraOptions(this.typeChecker, node.type)) { - return node.initializer; - } - - return null; - } -} - -/** - * Check whether the value assigned to an `initialNavigation` assignment - * conforms to the expected types for ExtraOptions#InitialNavigation - * @param node the property assignment to check - */ -function isValidInitialNavigationValue(node: ts.PropertyAssignment): boolean { - return ts.isStringLiteralLike(node.initializer) || - node.initializer.kind === ts.SyntaxKind.FalseKeyword || - node.initializer.kind === ts.SyntaxKind.TrueKeyword; -} diff --git a/packages/core/schematics/migrations/initial-navigation/google3/BUILD.bazel b/packages/core/schematics/migrations/initial-navigation/google3/BUILD.bazel deleted file mode 100644 index 28baae084ab..00000000000 --- a/packages/core/schematics/migrations/initial-navigation/google3/BUILD.bazel +++ /dev/null @@ -1,13 +0,0 @@ -load("//tools:defaults.bzl", "ts_library") - -ts_library( - name = "google3", - srcs = glob(["**/*.ts"]), - tsconfig = "//packages/core/schematics:tsconfig.json", - visibility = ["//packages/core/schematics/migrations/google3:__pkg__"], - deps = [ - "//packages/core/schematics/migrations/initial-navigation", - "@npm//tslint", - "@npm//typescript", - ], -) diff --git a/packages/core/schematics/migrations/initial-navigation/google3/tslint_update_recorder.ts b/packages/core/schematics/migrations/initial-navigation/google3/tslint_update_recorder.ts deleted file mode 100644 index c861ff4b39c..00000000000 --- a/packages/core/schematics/migrations/initial-navigation/google3/tslint_update_recorder.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 {Replacement, RuleFailure} from 'tslint'; -import ts from 'typescript'; - -import {UpdateRecorder} from '../update_recorder'; - -export class TslintUpdateRecorder implements UpdateRecorder { - failures: RuleFailure[] = []; - - constructor(private ruleName: string, private sourceFile: ts.SourceFile) {} - - updateNode(node: ts.Node, newText: string): void { - this.failures.push(new RuleFailure( - this.sourceFile, node.getStart(), node.getEnd(), `Node needs to be updated to: ${newText}`, - this.ruleName, Replacement.replaceFromTo(node.getStart(), node.getEnd(), newText))); - } - - commitUpdate() {} -} diff --git a/packages/core/schematics/migrations/initial-navigation/index.ts b/packages/core/schematics/migrations/initial-navigation/index.ts deleted file mode 100644 index c31824e3b27..00000000000 --- a/packages/core/schematics/migrations/initial-navigation/index.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * @license - * Copyright Google LLC 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, SchematicsException, Tree} from '@angular-devkit/schematics'; -import {relative} from 'path'; -import ts from 'typescript'; -import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; -import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host'; -import {InitialNavigationCollector} from './collector'; -import {InitialNavigationTransform} from './transform'; -import {UpdateRecorder} from './update_recorder'; - -/** Entry point for the v10 "initialNavigation RouterModule options" schematic. */ -export default function(): Rule { - return async (tree: Tree) => { - const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); - const basePath = process.cwd(); - - if (!buildPaths.length && !testPaths.length) { - throw new SchematicsException( - 'Could not find any tsconfig file. Cannot update the "initialNavigation" option for RouterModule'); - } - - for (const tsconfigPath of [...buildPaths, ...testPaths]) { - runInitialNavigationMigration(tree, tsconfigPath, basePath); - } - }; -} - -function runInitialNavigationMigration(tree: Tree, tsconfigPath: string, basePath: string) { - const {program} = createMigrationProgram(tree, tsconfigPath, basePath); - const typeChecker = program.getTypeChecker(); - const initialNavigationCollector = new InitialNavigationCollector(typeChecker); - const sourceFiles = - program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program)); - - // Analyze source files by detecting all modules. - sourceFiles.forEach(sourceFile => initialNavigationCollector.visitNode(sourceFile)); - - const {assignments} = initialNavigationCollector; - const transformer = new InitialNavigationTransform(getUpdateRecorder); - const updateRecorders = new Map(); - transformer.migrateInitialNavigationAssignments(Array.from(assignments)); - - // Walk through each update recorder and commit the update. We need to commit the - // updates in batches per source file as there can be only one recorder per source - // file in order to avoid shift character offsets. - updateRecorders.forEach(recorder => recorder.commitUpdate()); - - /** Gets the update recorder for the specified source file. */ - function getUpdateRecorder(sourceFile: ts.SourceFile): UpdateRecorder { - if (updateRecorders.has(sourceFile)) { - return updateRecorders.get(sourceFile)!; - } - const treeRecorder = tree.beginUpdate(relative(basePath, sourceFile.fileName)); - const recorder: UpdateRecorder = { - updateNode(node: ts.Node, newText: string) { - treeRecorder.remove(node.getStart(), node.getWidth()); - treeRecorder.insertRight(node.getStart(), newText); - }, - commitUpdate() { - tree.commitUpdate(treeRecorder); - } - }; - updateRecorders.set(sourceFile, recorder); - return recorder; - } -} diff --git a/packages/core/schematics/migrations/initial-navigation/transform.ts b/packages/core/schematics/migrations/initial-navigation/transform.ts deleted file mode 100644 index 07677fb74ce..00000000000 --- a/packages/core/schematics/migrations/initial-navigation/transform.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; - -import {UpdateRecorder} from './update_recorder'; - - -export class InitialNavigationTransform { - private printer = ts.createPrinter(); - - constructor(private getUpdateRecorder: (sf: ts.SourceFile) => UpdateRecorder) {} - - /** Migrate the ExtraOptions#InitialNavigation property assignments. */ - migrateInitialNavigationAssignments(literals: ts.PropertyAssignment[]) { - literals.forEach(l => this.migrateAssignment(l)); - } - - /** Migrate an ExtraOptions#InitialNavigation expression to use the new options format. */ - migrateAssignment(assignment: ts.PropertyAssignment) { - const newInitializer = getUpdatedInitialNavigationValue(assignment.initializer); - if (newInitializer) { - const newAssignment = - ts.updatePropertyAssignment(assignment, assignment.name, newInitializer); - this._updateNode(assignment, newAssignment); - } - } - - private _updateNode(node: ts.Node, newNode: ts.Node) { - const newText = this.printer.printNode(ts.EmitHint.Unspecified, newNode, node.getSourceFile()); - const recorder = this.getUpdateRecorder(node.getSourceFile()); - recorder.updateNode(node, newText); - } -} - -/** - * Updates the deprecated initialNavigation options to their v10 equivalents - * (or as close as we can get). - * @param initializer the old initializer to update - */ -function getUpdatedInitialNavigationValue(initializer: ts.Expression): ts.Expression|null { - const oldText: string|boolean = ts.isStringLiteralLike(initializer) ? - initializer.text : - initializer.kind === ts.SyntaxKind.TrueKeyword; - let newText: string|undefined; - switch (oldText) { - case false: - case 'legacy_disabled': - newText = 'disabled'; - break; - case true: - case 'legacy_enabled': - newText = 'enabledNonBlocking'; - break; - } - - return !!newText ? ts.createIdentifier(`'${newText}'`) : null; -} diff --git a/packages/core/schematics/migrations/initial-navigation/update_recorder.ts b/packages/core/schematics/migrations/initial-navigation/update_recorder.ts deleted file mode 100644 index 6156375460f..00000000000 --- a/packages/core/schematics/migrations/initial-navigation/update_recorder.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; - -/** - * Update recorder interface that is used to transform source files in a non-colliding - * way. Also this indirection makes it possible to re-use logic for both TSLint rules - * and CLI devkit schematic updates. - */ -export interface UpdateRecorder { - updateNode(node: ts.Node, newText: string): void; - commitUpdate(): void; -} diff --git a/packages/core/schematics/migrations/initial-navigation/util.ts b/packages/core/schematics/migrations/initial-navigation/util.ts deleted file mode 100644 index 6f8a5a36f02..00000000000 --- a/packages/core/schematics/migrations/initial-navigation/util.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; -import {getImportOfIdentifier} from '../../utils/typescript/imports'; - -/** Determine whether a node is a ModuleWithProviders type reference node without a generic type */ -export function isRouterModuleForRoot( - typeChecker: ts.TypeChecker, node: ts.Node): node is ts.CallExpression { - if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression) || - !ts.isIdentifier(node.expression.expression) || node.expression.name.text !== 'forRoot') { - return false; - } - const imp = getImportOfIdentifier(typeChecker, node.expression.expression); - return !!imp && imp.name === 'RouterModule' && imp.importModule === '@angular/router' && - !node.typeArguments; -} - -export function isExtraOptions( - typeChecker: ts.TypeChecker, node: ts.Node): node is ts.TypeReferenceNode { - if (!ts.isTypeReferenceNode(node) || !ts.isIdentifier(node.typeName)) { - return false; - } - - const imp = getImportOfIdentifier(typeChecker, node.typeName); - return imp !== null && imp.name === 'ExtraOptions' && imp.importModule === '@angular/router' && - !node.typeArguments; -} diff --git a/packages/core/schematics/migrations/missing-injectable/BUILD.bazel b/packages/core/schematics/migrations/missing-injectable/BUILD.bazel deleted file mode 100644 index b32c50e9229..00000000000 --- a/packages/core/schematics/migrations/missing-injectable/BUILD.bazel +++ /dev/null @@ -1,20 +0,0 @@ -load("//tools:defaults.bzl", "ts_library") - -ts_library( - name = "missing-injectable", - srcs = glob(["**/*.ts"]), - tsconfig = "//packages/core/schematics:tsconfig.json", - visibility = [ - "//packages/core/schematics:__pkg__", - "//packages/core/schematics/migrations/google3:__pkg__", - "//packages/core/schematics/migrations/missing-injectable/google3:__pkg__", - "//packages/core/schematics/test:__pkg__", - ], - deps = [ - "//packages/compiler-cli/private", - "//packages/core/schematics/utils", - "@npm//@angular-devkit/schematics", - "@npm//@types/node", - "@npm//typescript", - ], -) diff --git a/packages/core/schematics/migrations/missing-injectable/definition_collector.ts b/packages/core/schematics/migrations/missing-injectable/definition_collector.ts deleted file mode 100644 index dff28076095..00000000000 --- a/packages/core/schematics/migrations/missing-injectable/definition_collector.ts +++ /dev/null @@ -1,104 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; - -import {getAngularDecorators, NgDecorator} from '../../utils/ng_decorators'; -import {getPropertyNameText} from '../../utils/typescript/property_name'; - -export interface ResolvedNgModule { - name: string; - node: ts.ClassDeclaration; - decorator: NgDecorator; - providersExpr: ts.Expression|null; -} - -export interface ResolvedDirective { - name: string; - node: ts.ClassDeclaration; - decorator: NgDecorator; - providersExpr: ts.Expression|null; - viewProvidersExpr: ts.Expression|null; -} - -/** - * Visitor that walks through specified TypeScript nodes and collects all - * found NgModule, Directive or Component definitions. - */ -export class NgDefinitionCollector { - resolvedModules: ResolvedNgModule[] = []; - resolvedDirectives: ResolvedDirective[] = []; - - constructor(public typeChecker: ts.TypeChecker) {} - - visitNode(node: ts.Node) { - if (ts.isClassDeclaration(node)) { - this.visitClassDeclaration(node); - } - - ts.forEachChild(node, n => this.visitNode(n)); - } - - private visitClassDeclaration(node: ts.ClassDeclaration) { - if (!node.decorators || !node.decorators.length) { - return; - } - - const ngDecorators = getAngularDecorators(this.typeChecker, node.decorators); - const directiveDecorator = - ngDecorators.find(({name}) => name === 'Component' || name == 'Directive'); - const ngModuleDecorator = ngDecorators.find(({name}) => name === 'NgModule'); - - if (ngModuleDecorator) { - this._visitNgModuleClass(node, ngModuleDecorator); - } else if (directiveDecorator) { - this._visitDirectiveClass(node, directiveDecorator); - } - } - - private _visitDirectiveClass(node: ts.ClassDeclaration, decorator: NgDecorator) { - const decoratorCall = decorator.node.expression; - const metadata = decoratorCall.arguments[0]; - - if (!metadata || !ts.isObjectLiteralExpression(metadata)) { - return; - } - - const providersNode = metadata.properties.filter(ts.isPropertyAssignment) - .find(p => getPropertyNameText(p.name) === 'providers'); - - const viewProvidersNode = metadata.properties.filter(ts.isPropertyAssignment) - .find(p => getPropertyNameText(p.name) === 'viewProviders'); - - this.resolvedDirectives.push({ - name: node.name ? node.name.text : 'default', - node, - decorator, - providersExpr: providersNode !== undefined ? providersNode.initializer : null, - viewProvidersExpr: viewProvidersNode !== undefined ? viewProvidersNode.initializer : null, - }); - } - - private _visitNgModuleClass(node: ts.ClassDeclaration, decorator: NgDecorator) { - const decoratorCall = decorator.node.expression; - const metadata = decoratorCall.arguments[0]; - - if (!metadata || !ts.isObjectLiteralExpression(metadata)) { - return; - } - - const providersNode = metadata.properties.filter(ts.isPropertyAssignment) - .find(p => getPropertyNameText(p.name) === 'providers'); - this.resolvedModules.push({ - name: node.name ? node.name.text : 'default', - node, - decorator, - providersExpr: providersNode !== undefined ? providersNode.initializer : null, - }); - } -} diff --git a/packages/core/schematics/migrations/missing-injectable/google3/BUILD.bazel b/packages/core/schematics/migrations/missing-injectable/google3/BUILD.bazel deleted file mode 100644 index e5e4217f554..00000000000 --- a/packages/core/schematics/migrations/missing-injectable/google3/BUILD.bazel +++ /dev/null @@ -1,13 +0,0 @@ -load("//tools:defaults.bzl", "ts_library") - -ts_library( - name = "google3", - srcs = glob(["**/*.ts"]), - tsconfig = "//packages/core/schematics:tsconfig.json", - visibility = ["//packages/core/schematics/migrations/google3:__pkg__"], - deps = [ - "//packages/core/schematics/migrations/missing-injectable", - "@npm//tslint", - "@npm//typescript", - ], -) diff --git a/packages/core/schematics/migrations/missing-injectable/google3/tslint_update_recorder.ts b/packages/core/schematics/migrations/missing-injectable/google3/tslint_update_recorder.ts deleted file mode 100644 index 4ecdc52023c..00000000000 --- a/packages/core/schematics/migrations/missing-injectable/google3/tslint_update_recorder.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 {Replacement, RuleFailure} from 'tslint'; -import ts from 'typescript'; - -import {UpdateRecorder} from '../update_recorder'; - -export class TslintUpdateRecorder implements UpdateRecorder { - failures: RuleFailure[] = []; - - constructor(private ruleName: string, private sourceFile: ts.SourceFile) {} - - addClassDecorator(node: ts.ClassDeclaration, decoratorText: string, className: string) { - // Adding a decorator should be the last replacement. Replacements/rule failures - // are handled in reverse and in case a decorator and import are inserted at - // the start of the file, the class decorator should come after the import. - this.failures.unshift(new RuleFailure( - this.sourceFile, node.getStart(), 0, - `Class needs to be decorated with ` + - `"${decoratorText}" because it has been provided by "${className}".`, - this.ruleName, Replacement.appendText(node.getStart(), `${decoratorText}\n`))); - } - - addNewImport(start: number, importText: string) { - this.failures.push(new RuleFailure( - this.sourceFile, start, 0, `Source file needs to have import: "${importText}"`, - this.ruleName, Replacement.appendText(start, importText))); - } - - updateExistingImport(namedBindings: ts.NamedImports, newNamedBindings: string): void { - const fix = [ - Replacement.deleteText(namedBindings.getStart(), namedBindings.getWidth()), - Replacement.appendText(namedBindings.getStart(), newNamedBindings), - ]; - this.failures.push(new RuleFailure( - this.sourceFile, namedBindings.getStart(), namedBindings.getEnd(), - `Import needs to be updated to import symbols: "${newNamedBindings}"`, this.ruleName, fix)); - } - - replaceDecorator(decorator: ts.Node, newText: string, className: string): void { - const fix = [ - Replacement.deleteText(decorator.getStart(), decorator.getWidth()), - Replacement.appendText(decorator.getStart(), newText), - ]; - this.failures.push(new RuleFailure( - this.sourceFile, decorator.getStart(), decorator.getEnd(), - `Decorator needs to be replaced with "${newText}" because it has been provided ` + - `by "${className}"`, - this.ruleName, fix)); - } - - - updateObjectLiteral(node: ts.ObjectLiteralExpression, newText: string): void { - this.failures.push(new RuleFailure( - this.sourceFile, node.getStart(), node.getEnd(), - `Object literal needs to be updated to: ${newText}`, this.ruleName, - Replacement.replaceFromTo(node.getStart(), node.getEnd(), newText))); - } - - commitUpdate() {} -} diff --git a/packages/core/schematics/migrations/missing-injectable/index.ts b/packages/core/schematics/migrations/missing-injectable/index.ts deleted file mode 100644 index 5052f5ff454..00000000000 --- a/packages/core/schematics/migrations/missing-injectable/index.ts +++ /dev/null @@ -1,134 +0,0 @@ -/** - * @license - * Copyright Google LLC 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, SchematicsException, Tree} from '@angular-devkit/schematics'; -import {relative} from 'path'; -import ts from 'typescript'; - -import {loadCompilerCliMigrationsModule, loadEsmModule} from '../../utils/load_esm'; -import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; -import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host'; -import {NgDefinitionCollector} from './definition_collector'; -import {MissingInjectableTransform} from './transform'; -import {UpdateRecorder} from './update_recorder'; - -/** Entry point for the V9 "missing @Injectable" schematic. */ -export default function(): Rule { - return async (tree: Tree, ctx: SchematicContext) => { - const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); - const basePath = process.cwd(); - const failures: string[] = []; - - if (!buildPaths.length && !testPaths.length) { - throw new SchematicsException( - 'Could not find any tsconfig file. Cannot add the "@Injectable" decorator to providers ' + - 'which don\'t have that decorator set.'); - } - - let compilerCliMigrationsModule; - try { - // Load ESM `@angular/compiler/private/migrations` using the TypeScript dynamic import - // workaround. Once TypeScript provides support for keeping the dynamic import this workaround - // can be changed to a direct dynamic import. - compilerCliMigrationsModule = await loadCompilerCliMigrationsModule(); - } catch (e) { - throw new SchematicsException( - `Unable to load the '@angular/compiler-cli' package. Details: ${e.message}`); - } - - for (const tsconfigPath of [...buildPaths, ...testPaths]) { - failures.push(...runMissingInjectableMigration( - tree, tsconfigPath, basePath, compilerCliMigrationsModule)); - } - - if (failures.length) { - ctx.logger.info('Could not migrate all providers automatically. Please'); - ctx.logger.info('manually migrate the following instances:'); - failures.forEach(message => ctx.logger.warn(`⮑ ${message}`)); - } - }; -} - -function runMissingInjectableMigration( - tree: Tree, tsconfigPath: string, basePath: string, - compilerCliMigrationsModule: typeof import('@angular/compiler-cli/private/migrations')): - string[] { - const {program} = createMigrationProgram(tree, tsconfigPath, basePath); - const failures: string[] = []; - const typeChecker = program.getTypeChecker(); - const definitionCollector = new NgDefinitionCollector(typeChecker); - const sourceFiles = - program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program)); - - // Analyze source files by detecting all modules, directives and components. - sourceFiles.forEach(sourceFile => definitionCollector.visitNode(sourceFile)); - - const {resolvedModules, resolvedDirectives} = definitionCollector; - const transformer = - new MissingInjectableTransform(typeChecker, getUpdateRecorder, compilerCliMigrationsModule); - const updateRecorders = new Map(); - - [...transformer.migrateModules(resolvedModules), - ...transformer.migrateDirectives(resolvedDirectives), - ].forEach(({message, node}) => { - const nodeSourceFile = node.getSourceFile(); - const relativeFilePath = relative(basePath, nodeSourceFile.fileName); - const {line, character} = - ts.getLineAndCharacterOfPosition(node.getSourceFile(), node.getStart()); - failures.push(`${relativeFilePath}@${line + 1}:${character + 1}: ${message}`); - }); - - // Record the changes collected in the import manager and transformer. - transformer.recordChanges(); - - // Walk through each update recorder and commit the update. We need to commit the - // updates in batches per source file as there can be only one recorder per source - // file in order to avoid shift character offsets. - updateRecorders.forEach(recorder => recorder.commitUpdate()); - - return failures; - - /** Gets the update recorder for the specified source file. */ - function getUpdateRecorder(sourceFile: ts.SourceFile): UpdateRecorder { - if (updateRecorders.has(sourceFile)) { - return updateRecorders.get(sourceFile)!; - } - const treeRecorder = tree.beginUpdate(relative(basePath, sourceFile.fileName)); - const recorder: UpdateRecorder = { - addClassDecorator(node: ts.ClassDeclaration, text: string) { - // New imports should be inserted at the left while decorators should be inserted - // at the right in order to ensure that imports are inserted before the decorator - // if the start position of import and decorator is the source file start. - treeRecorder.insertRight(node.getStart(), `${text}\n`); - }, - replaceDecorator(decorator: ts.Decorator, newText: string) { - treeRecorder.remove(decorator.getStart(), decorator.getWidth()); - treeRecorder.insertRight(decorator.getStart(), newText); - }, - addNewImport(start: number, importText: string) { - // New imports should be inserted at the left while decorators should be inserted - // at the right in order to ensure that imports are inserted before the decorator - // if the start position of import and decorator is the source file start. - treeRecorder.insertLeft(start, importText); - }, - updateExistingImport(namedBindings: ts.NamedImports, newNamedBindings: string) { - treeRecorder.remove(namedBindings.getStart(), namedBindings.getWidth()); - treeRecorder.insertRight(namedBindings.getStart(), newNamedBindings); - }, - updateObjectLiteral(node: ts.ObjectLiteralExpression, newText: string) { - treeRecorder.remove(node.getStart(), node.getWidth()); - treeRecorder.insertRight(node.getStart(), newText); - }, - commitUpdate() { - tree.commitUpdate(treeRecorder); - } - }; - updateRecorders.set(sourceFile, recorder); - return recorder; - } -} diff --git a/packages/core/schematics/migrations/missing-injectable/providers_evaluator.ts b/packages/core/schematics/migrations/missing-injectable/providers_evaluator.ts deleted file mode 100644 index 1d7928454c4..00000000000 --- a/packages/core/schematics/migrations/missing-injectable/providers_evaluator.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; - -import type {ResolvedValue, TypeScriptReflectionHost} from '@angular/compiler-cli/private/migrations'; - -export interface ProviderLiteral { - node: ts.ObjectLiteralExpression; - resolvedValue: ResolvedValue; -} - -/** - * A factory function to create an evaluator for providers. This is required to be a - * factory function because the underlying class extends a class that is only available - * from within a dynamically imported module (`@angular/compiler-cli/private/migrations`) - * and is therefore not available at module evaluation time. - */ -export function createProvidersEvaluator( - compilerCliMigrationsModule: typeof import('@angular/compiler-cli/private/migrations'), - host: TypeScriptReflectionHost, checker: ts.TypeChecker): { - evaluate: - (expr: ts.Expression) => { - resolvedValue: ResolvedValue, literals: ProviderLiteral[] - } -} { - /** - * Providers evaluator that extends the ngtsc static interpreter. This is necessary because - * the static interpreter by default only exposes the resolved value, but we are also interested - * in the TypeScript nodes that declare providers. It would be possible to manually traverse the - * AST to collect these nodes, but that would mean that we need to re-implement the static - * interpreter in order to handle all possible scenarios. (e.g. spread operator, function calls, - * callee scope). This can be avoided by simply extending the static interpreter and intercepting - * the "visitObjectLiteralExpression" method. - */ - class ProvidersEvaluator extends compilerCliMigrationsModule.StaticInterpreter { - private _providerLiterals: ProviderLiteral[] = []; - - override visitObjectLiteralExpression(node: ts.ObjectLiteralExpression, context: any) { - const resolvedValue = - super.visitObjectLiteralExpression(node, {...context, insideProviderDef: true}); - // do not collect nested object literals. e.g. a provider could use a - // spread assignment (which resolves to another object literal). In that - // case the referenced object literal is not a provider object literal. - if (!context.insideProviderDef) { - this._providerLiterals.push({node, resolvedValue}); - } - return resolvedValue; - } - - /** - * Evaluates the given expression and returns its statically resolved value - * and a list of object literals which define Angular providers. - */ - evaluate(expr: ts.Expression) { - this._providerLiterals = []; - const resolvedValue = this.visit(expr, { - originatingFile: expr.getSourceFile(), - absoluteModuleName: null, - resolutionContext: expr.getSourceFile().fileName, - scope: new Map(), - foreignFunctionResolver: compilerCliMigrationsModule.forwardRefResolver - }); - return {resolvedValue, literals: this._providerLiterals}; - } - } - - return new ProvidersEvaluator(host, checker, /* dependencyTracker */ null); -} diff --git a/packages/core/schematics/migrations/missing-injectable/transform.ts b/packages/core/schematics/migrations/missing-injectable/transform.ts deleted file mode 100644 index 0c996748e3e..00000000000 --- a/packages/core/schematics/migrations/missing-injectable/transform.ts +++ /dev/null @@ -1,239 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 type {ResolvedValue} from '@angular/compiler-cli/private/migrations'; -import ts from 'typescript'; - -import {ImportManager} from '../../utils/import_manager'; -import {getAngularDecorators} from '../../utils/ng_decorators'; - -import {ResolvedDirective, ResolvedNgModule} from './definition_collector'; -import {createProvidersEvaluator, ProviderLiteral} from './providers_evaluator'; -import {UpdateRecorder} from './update_recorder'; - -/** - * Name of decorators which imply that a given class does not need to be migrated. - * - `@Injectable`, `@Directive`, `@Component` and `@Pipe` instruct the compiler - * to generate a factory definition. - * - `@NgModule` instructs the compiler to generate a provider definition that holds - * the factory function. - */ -const NO_MIGRATE_DECORATORS = ['Injectable', 'Directive', 'Component', 'Pipe', 'NgModule']; - -export interface AnalysisFailure { - node: ts.Node; - message: string; -} - -export class MissingInjectableTransform { - private printer = ts.createPrinter(); - private importManager = new ImportManager(this.getUpdateRecorder, this.printer); - private providersEvaluator; - - /** Set of provider class declarations which were already checked or migrated. */ - private visitedProviderClasses = new Set(); - - /** Set of provider object literals which were already checked or migrated. */ - private visitedProviderLiterals = new Set(); - - constructor( - private typeChecker: ts.TypeChecker, - private getUpdateRecorder: (sf: ts.SourceFile) => UpdateRecorder, - private compilerCliMigrationsModule: - typeof import('@angular/compiler-cli/private/migrations')) { - this.providersEvaluator = createProvidersEvaluator( - compilerCliMigrationsModule, - new compilerCliMigrationsModule.TypeScriptReflectionHost(typeChecker), typeChecker); - } - - recordChanges() { - this.importManager.recordChanges(); - } - - /** - * Migrates all specified NgModule's by walking through referenced providers - * and decorating them with "@Injectable" if needed. - */ - migrateModules(modules: ResolvedNgModule[]): AnalysisFailure[] { - return modules.reduce( - (failures, node) => failures.concat(this.migrateModule(node)), [] as AnalysisFailure[]); - } - - /** - * Migrates all specified directives by walking through referenced providers - * and decorating them with "@Injectable" if needed. - */ - migrateDirectives(directives: ResolvedDirective[]): AnalysisFailure[] { - return directives.reduce( - (failures, node) => failures.concat(this.migrateDirective(node)), [] as AnalysisFailure[]); - } - - /** Migrates a given NgModule by walking through the referenced providers. */ - migrateModule(module: ResolvedNgModule): AnalysisFailure[] { - if (module.providersExpr === null) { - return []; - } - - const {resolvedValue, literals} = this.providersEvaluator.evaluate(module.providersExpr); - this._migrateLiteralProviders(literals); - - if (!Array.isArray(resolvedValue)) { - return [ - {node: module.providersExpr, message: 'Providers of module are not statically analyzable.'} - ]; - } - - return this._visitProviderResolvedValue(resolvedValue, module); - } - - - /** - * Migrates a given directive by walking through defined providers. This method - * also handles components with "viewProviders" defined. - */ - migrateDirective(directive: ResolvedDirective): AnalysisFailure[] { - const failures: AnalysisFailure[] = []; - - // Migrate "providers" on directives and components if defined. - if (directive.providersExpr) { - const {resolvedValue, literals} = this.providersEvaluator.evaluate(directive.providersExpr); - this._migrateLiteralProviders(literals); - if (!Array.isArray(resolvedValue)) { - return [ - {node: directive.providersExpr, message: `Providers are not statically analyzable.`} - ]; - } - failures.push(...this._visitProviderResolvedValue(resolvedValue, directive)); - } - - // Migrate "viewProviders" on components if defined. - if (directive.viewProvidersExpr) { - const {resolvedValue, literals} = - this.providersEvaluator.evaluate(directive.viewProvidersExpr); - this._migrateLiteralProviders(literals); - if (!Array.isArray(resolvedValue)) { - return [ - {node: directive.viewProvidersExpr, message: `Providers are not statically analyzable.`} - ]; - } - failures.push(...this._visitProviderResolvedValue(resolvedValue, directive)); - } - return failures; - } - - /** - * Migrates a given provider class if it is not decorated with - * any Angular decorator. - */ - migrateProviderClass(node: ts.ClassDeclaration, context: ResolvedNgModule|ResolvedDirective) { - if (this.visitedProviderClasses.has(node)) { - return; - } - this.visitedProviderClasses.add(node); - - const sourceFile = node.getSourceFile(); - - // We cannot migrate provider classes outside of source files. This is because the - // migration for third-party library files should happen in "ngcc", and in general - // would also involve metadata parsing. - if (sourceFile.isDeclarationFile) { - return; - } - - const ngDecorators = - node.decorators ? getAngularDecorators(this.typeChecker, node.decorators) : null; - - if (ngDecorators !== null && - ngDecorators.some(d => NO_MIGRATE_DECORATORS.indexOf(d.name) !== -1)) { - return; - } - - const updateRecorder = this.getUpdateRecorder(sourceFile); - const importExpr = - this.importManager.addImportToSourceFile(sourceFile, 'Injectable', '@angular/core'); - const newDecoratorExpr = ts.createDecorator(ts.createCall(importExpr, undefined, undefined)); - const newDecoratorText = - this.printer.printNode(ts.EmitHint.Unspecified, newDecoratorExpr, sourceFile); - - - // In case the class is already decorated with "@Inject(..)", we replace the "@Inject" - // decorator with "@Injectable()" since using "@Inject(..)" on a class is a noop and - // most likely was meant to be "@Injectable()". - const existingInjectDecorator = - ngDecorators !== null ? ngDecorators.find(d => d.name === 'Inject') : null; - if (existingInjectDecorator) { - updateRecorder.replaceDecorator(existingInjectDecorator.node, newDecoratorText, context.name); - } else { - updateRecorder.addClassDecorator(node, newDecoratorText, context.name); - } - } - - /** - * Migrates object literal providers which do not use "useValue", "useClass", - * "useExisting" or "useFactory". These providers behave differently in Ivy. e.g. - * - * ```ts - * {provide: X} -> {provide: X, useValue: undefined} // this is how it behaves in VE - * {provide: X} -> {provide: X, useClass: X} // this is how it behaves in Ivy - * ``` - * - * To ensure forward compatibility, we migrate these empty object literal providers - * to explicitly use `useValue: undefined`. - */ - private _migrateLiteralProviders(literals: ProviderLiteral[]) { - for (let {node, resolvedValue} of literals) { - if (this.visitedProviderLiterals.has(node)) { - continue; - } - this.visitedProviderLiterals.add(node); - - if (!resolvedValue || !(resolvedValue instanceof Map) || !resolvedValue.has('provide') || - resolvedValue.has('useClass') || resolvedValue.has('useValue') || - resolvedValue.has('useExisting') || resolvedValue.has('useFactory')) { - continue; - } - - const sourceFile = node.getSourceFile(); - const newObjectLiteral = ts.updateObjectLiteral( - node, - node.properties.concat( - ts.createPropertyAssignment('useValue', ts.createIdentifier('undefined')))); - - this.getUpdateRecorder(sourceFile) - .updateObjectLiteral( - node, this.printer.printNode(ts.EmitHint.Unspecified, newObjectLiteral, sourceFile)); - } - } - - /** - * Visits the given resolved value of a provider. Providers can be nested in - * arrays and we need to recursively walk through the providers to be able to - * migrate all referenced provider classes. e.g. "providers: [[A, [B]]]". - */ - private _visitProviderResolvedValue(value: ResolvedValue, module: ResolvedNgModule): - AnalysisFailure[] { - if (value instanceof this.compilerCliMigrationsModule.Reference && - ts.isClassDeclaration(value.node)) { - this.migrateProviderClass(value.node, module); - } else if (value instanceof Map) { - // If a "ClassProvider" has the "deps" property set, then we do not need to - // decorate the class. This is because the class is instantiated through the - // specified "deps" and the class does not need a factory definition. - if (value.has('provide') && value.has('useClass') && value.get('deps') == null) { - return this._visitProviderResolvedValue(value.get('useClass')!, module); - } - } else if (Array.isArray(value)) { - return value.reduce( - (res, v) => res.concat(this._visitProviderResolvedValue(v, module)), - [] as AnalysisFailure[]); - } else if (value instanceof this.compilerCliMigrationsModule.DynamicValue) { - return [{node: value.node, message: `Provider is not statically analyzable.`}]; - } - return []; - } -} diff --git a/packages/core/schematics/migrations/missing-injectable/update_recorder.ts b/packages/core/schematics/migrations/missing-injectable/update_recorder.ts deleted file mode 100644 index 8dbb0789028..00000000000 --- a/packages/core/schematics/migrations/missing-injectable/update_recorder.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; -import {ImportManagerUpdateRecorder} from '../../utils/import_manager'; - -/** - * Update recorder interface that is used to transform source files in a non-colliding - * way. Also this indirection makes it possible to re-use logic for both TSLint rules - * and CLI devkit schematic updates. - */ -export interface UpdateRecorder extends ImportManagerUpdateRecorder { - addClassDecorator(node: ts.ClassDeclaration, text: string, className: string): void; - replaceDecorator(node: ts.Decorator, newText: string, className: string): void; - updateObjectLiteral(node: ts.ObjectLiteralExpression, newText: string): void; - commitUpdate(): void; -} diff --git a/packages/core/schematics/migrations/module-with-providers/BUILD.bazel b/packages/core/schematics/migrations/module-with-providers/BUILD.bazel deleted file mode 100644 index 076e9cb67dd..00000000000 --- a/packages/core/schematics/migrations/module-with-providers/BUILD.bazel +++ /dev/null @@ -1,18 +0,0 @@ -load("//tools:defaults.bzl", "ts_library") - -ts_library( - name = "module-with-providers", - srcs = glob(["**/*.ts"]), - tsconfig = "//packages/core/schematics:tsconfig.json", - visibility = [ - "//packages/core/schematics:__pkg__", - "//packages/core/schematics/test:__pkg__", - ], - deps = [ - "//packages/compiler-cli/private", - "//packages/core/schematics/utils", - "@npm//@angular-devkit/schematics", - "@npm//@types/node", - "@npm//typescript", - ], -) diff --git a/packages/core/schematics/migrations/module-with-providers/README.md b/packages/core/schematics/migrations/module-with-providers/README.md deleted file mode 100644 index 29470051201..00000000000 --- a/packages/core/schematics/migrations/module-with-providers/README.md +++ /dev/null @@ -1,28 +0,0 @@ -## ModuleWithProviders migration - -`ModuleWithProviders` type will not default to the `any` type for its generic in a future version of Angular. -This migration adds a generic to any `ModuleWithProvider` types found. - -#### Before -```ts -import { NgModule, ModuleWithProviders } from '@angular/core'; - -@NgModule({}) -export class MyModule { - static forRoot(): ModuleWithProviders { - ngModule: MyModule - } -} -``` - -#### After -```ts -import { NgModule, ModuleWithProviders } from '@angular/core'; - -@NgModule({}) -export class MyModule { - static forRoot(): ModuleWithProviders { - ngModule: MyModule - } -} -``` diff --git a/packages/core/schematics/migrations/module-with-providers/collector.ts b/packages/core/schematics/migrations/module-with-providers/collector.ts deleted file mode 100644 index 39cc70e5476..00000000000 --- a/packages/core/schematics/migrations/module-with-providers/collector.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; - -import {getAngularDecorators, NgDecorator} from '../../utils/ng_decorators'; - -import {isModuleWithProvidersNotGeneric} from './util'; - -export interface ResolvedNgModule { - name: string; - node: ts.ClassDeclaration; - decorator: NgDecorator; - /** - * List of found static method declarations on the module which do not - * declare an explicit return type. - */ - staticMethodsWithoutType: ts.MethodDeclaration[]; -} - -/** - * Visitor that walks through specified TypeScript nodes and collects all - * found NgModule static methods without types and all ModuleWithProviders - * usages without generic types attached. - */ -export class Collector { - resolvedModules: ResolvedNgModule[] = []; - resolvedNonGenerics: ts.TypeReferenceNode[] = []; - - constructor(public typeChecker: ts.TypeChecker) {} - - visitNode(node: ts.Node) { - if (ts.isClassDeclaration(node)) { - this.visitClassDeclaration(node); - } else if (isModuleWithProvidersNotGeneric(this.typeChecker, node)) { - this.resolvedNonGenerics.push(node); - } - - ts.forEachChild(node, n => this.visitNode(n)); - } - - private visitClassDeclaration(node: ts.ClassDeclaration) { - if (!node.decorators || !node.decorators.length) { - return; - } - - const ngDecorators = getAngularDecorators(this.typeChecker, node.decorators); - const ngModuleDecorator = ngDecorators.find(({name}) => name === 'NgModule'); - - if (ngModuleDecorator) { - this._visitNgModuleClass(node, ngModuleDecorator); - } - } - - private _visitNgModuleClass(node: ts.ClassDeclaration, decorator: NgDecorator) { - const decoratorCall = decorator.node.expression; - const metadata = decoratorCall.arguments[0]; - - if (!metadata || !ts.isObjectLiteralExpression(metadata)) { - return; - } - - this.resolvedModules.push({ - name: node.name ? node.name.text : 'default', - node, - decorator, - staticMethodsWithoutType: node.members.filter(isStaticMethodNoType), - }); - } -} - -function isStaticMethodNoType(node: ts.ClassElement): node is ts.MethodDeclaration { - return ts.isMethodDeclaration(node) && !!node.modifiers && - node.modifiers.findIndex(m => m.kind === ts.SyntaxKind.StaticKeyword) > -1 && !node.type; -} diff --git a/packages/core/schematics/migrations/module-with-providers/index.ts b/packages/core/schematics/migrations/module-with-providers/index.ts deleted file mode 100644 index b315211ac42..00000000000 --- a/packages/core/schematics/migrations/module-with-providers/index.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * @license - * Copyright Google LLC 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, SchematicsException, Tree, UpdateRecorder} from '@angular-devkit/schematics'; -import {relative} from 'path'; -import ts from 'typescript'; - -import {loadCompilerCliMigrationsModule, loadEsmModule} from '../../utils/load_esm'; -import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; -import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host'; - -import {Collector} from './collector'; -import {AnalysisFailure, ModuleWithProvidersTransform} from './transform'; - - -/** - * Runs the ModuleWithProviders migration for all TypeScript projects in the current CLI workspace. - */ -export default function(): Rule { - return async (tree: Tree, ctx: SchematicContext) => { - const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); - const basePath = process.cwd(); - const allPaths = [...buildPaths, ...testPaths]; - const failures: string[] = []; - - if (!allPaths.length) { - throw new SchematicsException( - 'Could not find any tsconfig file. Cannot migrate ModuleWithProviders.'); - } - - let compilerCliMigrationsModule; - try { - // Load ESM `@angular/compiler/private/migrations` using the TypeScript dynamic import - // workaround. Once TypeScript provides support for keeping the dynamic import this workaround - // can be changed to a direct dynamic import. - compilerCliMigrationsModule = await loadCompilerCliMigrationsModule(); - } catch (e) { - throw new SchematicsException( - `Unable to load the '@angular/compiler-cli' package. Details: ${e.message}`); - } - - for (const tsconfigPath of allPaths) { - failures.push(...runModuleWithProvidersMigration( - tree, tsconfigPath, basePath, compilerCliMigrationsModule)); - } - - if (failures.length) { - ctx.logger.info('Could not migrate all instances of ModuleWithProviders'); - ctx.logger.info('Please manually fix the following failures:'); - failures.forEach(message => ctx.logger.warn(`⮑ ${message}`)); - } - }; -} - -function runModuleWithProvidersMigration( - tree: Tree, tsconfigPath: string, basePath: string, - compilerCliMigrationsModule: typeof import('@angular/compiler-cli/private/migrations')) { - const {program} = createMigrationProgram(tree, tsconfigPath, basePath); - const failures: string[] = []; - const typeChecker = program.getTypeChecker(); - const collector = new Collector(typeChecker); - const sourceFiles = - program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program)); - - // Analyze source files by detecting all modules. - sourceFiles.forEach(sourceFile => collector.visitNode(sourceFile)); - - const {resolvedModules, resolvedNonGenerics} = collector; - const transformer = - new ModuleWithProvidersTransform(typeChecker, getUpdateRecorder, compilerCliMigrationsModule); - const updateRecorders = new Map(); - - [...resolvedModules.reduce( - (failures, m) => failures.concat(transformer.migrateModule(m)), [] as AnalysisFailure[]), - ...resolvedNonGenerics.reduce( - (failures, t) => failures.concat(transformer.migrateType(t)), [] as AnalysisFailure[])] - .forEach(({message, node}) => { - const nodeSourceFile = node.getSourceFile(); - const relativeFilePath = relative(basePath, nodeSourceFile.fileName); - const {line, character} = - ts.getLineAndCharacterOfPosition(node.getSourceFile(), node.getStart()); - failures.push(`${relativeFilePath}@${line + 1}:${character + 1}: ${message}`); - }); - - // Walk through each update recorder and commit the update. We need to commit the - // updates in batches per source file as there can be only one recorder per source - // file in order to avoid shift character offsets. - updateRecorders.forEach(recorder => tree.commitUpdate(recorder)); - - return failures; - - /** Gets the update recorder for the specified source file. */ - function getUpdateRecorder(sourceFile: ts.SourceFile): UpdateRecorder { - if (updateRecorders.has(sourceFile)) { - return updateRecorders.get(sourceFile)!; - } - const recorder = tree.beginUpdate(relative(basePath, sourceFile.fileName)); - updateRecorders.set(sourceFile, recorder); - return recorder; - } -} diff --git a/packages/core/schematics/migrations/module-with-providers/transform.ts b/packages/core/schematics/migrations/module-with-providers/transform.ts deleted file mode 100644 index 9064cfb530a..00000000000 --- a/packages/core/schematics/migrations/module-with-providers/transform.ts +++ /dev/null @@ -1,171 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 {UpdateRecorder} from '@angular-devkit/schematics'; -import type {ResolvedValue, ResolvedValueMap} from '@angular/compiler-cli/private/migrations'; -import ts from 'typescript'; - -import {ResolvedNgModule} from './collector'; -import {createModuleWithProvidersType} from './util'; - -export interface AnalysisFailure { - node: ts.Node; - message: string; -} - -const TODO_COMMENT = 'TODO: The following node requires a generic type for `ModuleWithProviders`'; - -export class ModuleWithProvidersTransform { - private printer = ts.createPrinter(); - private partialEvaluator = new this.compilerCliMigrationsModule.PartialEvaluator( - new this.compilerCliMigrationsModule.TypeScriptReflectionHost(this.typeChecker), - this.typeChecker, - /* dependencyTracker */ null); - - constructor( - private typeChecker: ts.TypeChecker, - private getUpdateRecorder: (sf: ts.SourceFile) => UpdateRecorder, - private compilerCliMigrationsModule: - typeof import('@angular/compiler-cli/private/migrations')) {} - - /** Migrates a given NgModule by walking through the referenced providers and static methods. */ - migrateModule(module: ResolvedNgModule): AnalysisFailure[] { - return module.staticMethodsWithoutType.map(this._migrateStaticNgModuleMethod.bind(this)) - .filter(v => v) as AnalysisFailure[]; - } - - /** Migrates a ModuleWithProviders type definition that has no explicit generic type */ - migrateType(type: ts.TypeReferenceNode): AnalysisFailure[] { - const parent = type.parent; - let moduleText: string|undefined; - if ((ts.isFunctionDeclaration(parent) || ts.isMethodDeclaration(parent)) && parent.body) { - const returnStatement = parent.body.statements.find(ts.isReturnStatement); - - // No return type found, exit - if (!returnStatement || !returnStatement.expression) { - return [{node: parent, message: `Return type is not statically analyzable.`}]; - } - - moduleText = this._getNgModuleTypeOfExpression(returnStatement.expression); - } else if (ts.isPropertyDeclaration(parent) || ts.isVariableDeclaration(parent)) { - if (!parent.initializer) { - addTodoToNode(type, TODO_COMMENT); - this._updateNode(type, type); - return [{node: parent, message: `Unable to determine type for declaration.`}]; - } - - moduleText = this._getNgModuleTypeOfExpression(parent.initializer); - } - - if (moduleText) { - this._addGenericToTypeReference(type, moduleText); - return []; - } - - return [{node: parent, message: `Type is not statically analyzable.`}]; - } - - /** Add a given generic to a type reference node */ - private _addGenericToTypeReference(node: ts.TypeReferenceNode, typeName: string) { - const newGenericExpr = createModuleWithProvidersType(typeName, node); - this._updateNode(node, newGenericExpr); - } - - /** - * Migrates a given static method if its ModuleWithProviders does not provide - * a generic type. - */ - private _updateStaticMethodType(method: ts.MethodDeclaration, typeName: string) { - const newGenericExpr = - createModuleWithProvidersType(typeName, method.type as ts.TypeReferenceNode); - const newMethodDecl = ts.updateMethod( - method, method.decorators, method.modifiers, method.asteriskToken, method.name, - method.questionToken, method.typeParameters, method.parameters, newGenericExpr, - method.body); - - this._updateNode(method, newMethodDecl); - } - - /** Whether the resolved value map represents a ModuleWithProviders object */ - isModuleWithProvidersType(value: ResolvedValueMap): boolean { - const ngModule = value.get('ngModule') !== undefined; - const providers = value.get('providers') !== undefined; - - return ngModule && (value.size === 1 || (providers && value.size === 2)); - } - - /** - * Determine the generic type of a suspected ModuleWithProviders return type and add it - * explicitly - */ - private _migrateStaticNgModuleMethod(node: ts.MethodDeclaration): AnalysisFailure|null { - const returnStatement = node.body && - node.body.statements.find(n => ts.isReturnStatement(n)) as ts.ReturnStatement | undefined; - - // No return type found, exit - if (!returnStatement || !returnStatement.expression) { - return {node: node, message: `Return type is not statically analyzable.`}; - } - - const moduleText = this._getNgModuleTypeOfExpression(returnStatement.expression); - - if (moduleText) { - this._updateStaticMethodType(node, moduleText); - return null; - } - - return {node: node, message: `Method type is not statically analyzable.`}; - } - - /** Evaluate and return the ngModule type from an expression */ - private _getNgModuleTypeOfExpression(expr: ts.Expression): string|undefined { - const evaluatedExpr = this.partialEvaluator.evaluate(expr); - return this._getTypeOfResolvedValue(evaluatedExpr); - } - - /** - * Visits a given object literal expression to determine the ngModule type. If the expression - * cannot be resolved, add a TODO to alert the user. - */ - private _getTypeOfResolvedValue(value: ResolvedValue): string|undefined { - if (value instanceof Map && this.isModuleWithProvidersType(value)) { - const mapValue = value.get('ngModule')!; - if (mapValue instanceof this.compilerCliMigrationsModule.Reference && - ts.isClassDeclaration(mapValue.node) && mapValue.node.name) { - return mapValue.node.name.text; - } else if (mapValue instanceof this.compilerCliMigrationsModule.DynamicValue) { - addTodoToNode(mapValue.node, TODO_COMMENT); - this._updateNode(mapValue.node, mapValue.node); - } - } - - return undefined; - } - - private _updateNode(node: ts.Node, newNode: ts.Node) { - const newText = this.printer.printNode(ts.EmitHint.Unspecified, newNode, node.getSourceFile()); - const recorder = this.getUpdateRecorder(node.getSourceFile()); - - recorder.remove(node.getStart(), node.getWidth()); - recorder.insertRight(node.getStart(), newText); - } -} - -/** - * Adds a to-do to the given TypeScript node which alerts developers to fix - * potential issues identified by the migration. - */ -function addTodoToNode(node: ts.Node, text: string) { - ts.setSyntheticLeadingComments(node, [{ - pos: -1, - end: -1, - hasTrailingNewLine: false, - kind: ts.SyntaxKind.MultiLineCommentTrivia, - text: ` ${text} ` - }]); -} diff --git a/packages/core/schematics/migrations/module-with-providers/util.ts b/packages/core/schematics/migrations/module-with-providers/util.ts deleted file mode 100644 index a76c906bee8..00000000000 --- a/packages/core/schematics/migrations/module-with-providers/util.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; -import {getImportOfIdentifier} from '../../utils/typescript/imports'; - -/** Add a generic type to a type reference. */ -export function createModuleWithProvidersType( - type: string, node?: ts.TypeReferenceNode): ts.TypeReferenceNode { - const typeNode = node || ts.createTypeReferenceNode('ModuleWithProviders', []); - const typeReferenceNode = ts.createTypeReferenceNode(ts.createIdentifier(type), []); - return ts.updateTypeReferenceNode( - typeNode, typeNode.typeName, ts.createNodeArray([typeReferenceNode])); -} - -/** Determine whether a node is a ModuleWithProviders type reference node without a generic type */ -export function isModuleWithProvidersNotGeneric( - typeChecker: ts.TypeChecker, node: ts.Node): node is ts.TypeReferenceNode { - if (!ts.isTypeReferenceNode(node) || !ts.isIdentifier(node.typeName)) { - return false; - } - - const imp = getImportOfIdentifier(typeChecker, node.typeName); - return !!imp && imp.name === 'ModuleWithProviders' && imp.importModule === '@angular/core' && - !node.typeArguments; -} diff --git a/packages/core/schematics/migrations/move-document/BUILD.bazel b/packages/core/schematics/migrations/move-document/BUILD.bazel deleted file mode 100644 index 4cf0ffcd360..00000000000 --- a/packages/core/schematics/migrations/move-document/BUILD.bazel +++ /dev/null @@ -1,18 +0,0 @@ -load("//tools:defaults.bzl", "ts_library") - -ts_library( - name = "move-document", - srcs = glob(["**/*.ts"]), - tsconfig = "//packages/core/schematics:tsconfig.json", - visibility = [ - "//packages/core/schematics:__pkg__", - "//packages/core/schematics/migrations/move-document/google3:__pkg__", - "//packages/core/schematics/test:__pkg__", - ], - deps = [ - "//packages/core/schematics/utils", - "@npm//@angular-devkit/schematics", - "@npm//@types/node", - "@npm//typescript", - ], -) diff --git a/packages/core/schematics/migrations/move-document/document_import_visitor.ts b/packages/core/schematics/migrations/move-document/document_import_visitor.ts deleted file mode 100644 index f215bb776cb..00000000000 --- a/packages/core/schematics/migrations/move-document/document_import_visitor.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; - -export const COMMON_IMPORT = '@angular/common'; -export const PLATFORM_BROWSER_IMPORT = '@angular/platform-browser'; -export const DOCUMENT_TOKEN_NAME = 'DOCUMENT'; - -/** This contains the metadata necessary to move items from one import to another */ -export interface ResolvedDocumentImport { - platformBrowserImport: ts.NamedImports|null; - commonImport: ts.NamedImports|null; - documentElement: ts.ImportSpecifier|null; -} - -/** Visitor that can be used to find a set of imports in a TypeScript file. */ -export class DocumentImportVisitor { - importsMap: Map = new Map(); - - constructor(public typeChecker: ts.TypeChecker) {} - - visitNode(node: ts.Node) { - if (ts.isNamedImports(node)) { - this.visitNamedImport(node); - } - - ts.forEachChild(node, node => this.visitNode(node)); - } - - private visitNamedImport(node: ts.NamedImports) { - if (!node.elements || !node.elements.length) { - return; - } - - const importDeclaration = node.parent.parent; - // If this is not a StringLiteral it will be a grammar error - const moduleSpecifier = importDeclaration.moduleSpecifier as ts.StringLiteral; - const sourceFile = node.getSourceFile(); - let imports = this.importsMap.get(sourceFile); - if (!imports) { - imports = { - platformBrowserImport: null, - commonImport: null, - documentElement: null, - }; - } - - if (moduleSpecifier.text === PLATFORM_BROWSER_IMPORT) { - const documentElement = this.getDocumentElement(node); - if (documentElement) { - imports.platformBrowserImport = node; - imports.documentElement = documentElement; - } - } else if (moduleSpecifier.text === COMMON_IMPORT) { - imports.commonImport = node; - } else { - return; - } - this.importsMap.set(sourceFile, imports); - } - - private getDocumentElement(node: ts.NamedImports): ts.ImportSpecifier|undefined { - const elements = node.elements; - return elements.find(el => (el.propertyName || el.name).escapedText === DOCUMENT_TOKEN_NAME); - } -} diff --git a/packages/core/schematics/migrations/move-document/index.ts b/packages/core/schematics/migrations/move-document/index.ts deleted file mode 100644 index 387d0569a77..00000000000 --- a/packages/core/schematics/migrations/move-document/index.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * @license - * Copyright Google LLC 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, SchematicsException, Tree} from '@angular-devkit/schematics'; -import {relative} from 'path'; -import ts from 'typescript'; - -import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; -import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host'; - -import {COMMON_IMPORT, DOCUMENT_TOKEN_NAME, DocumentImportVisitor, ResolvedDocumentImport} from './document_import_visitor'; -import {addToImport, createImport, removeFromImport} from './move-import'; - - -/** Entry point for the V8 move-document migration. */ -export default function(): Rule { - return async (tree: Tree) => { - const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); - const basePath = process.cwd(); - - if (!buildPaths.length && !testPaths.length) { - throw new SchematicsException(`Could not find any tsconfig file. Cannot migrate DOCUMENT - to new import source.`); - } - - for (const tsconfigPath of [...buildPaths, ...testPaths]) { - runMoveDocumentMigration(tree, tsconfigPath, basePath); - } - }; -} - -/** - * Runs the DOCUMENT InjectionToken import migration for the given TypeScript project. The - * schematic analyzes the imports within the project and moves the deprecated symbol to the - * new import source. - */ -function runMoveDocumentMigration(tree: Tree, tsconfigPath: string, basePath: string) { - const {program} = createMigrationProgram(tree, tsconfigPath, basePath); - const typeChecker = program.getTypeChecker(); - const visitor = new DocumentImportVisitor(typeChecker); - const sourceFiles = - program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program)); - - // Analyze source files by finding imports. - sourceFiles.forEach(sourceFile => visitor.visitNode(sourceFile)); - - const {importsMap} = visitor; - - // Walk through all source files that contain resolved queries and update - // the source files if needed. Note that we need to update multiple queries - // within a source file within the same recorder in order to not throw off - // the TypeScript node offsets. - importsMap.forEach((resolvedImport: ResolvedDocumentImport, sourceFile: ts.SourceFile) => { - const {platformBrowserImport, commonImport, documentElement} = resolvedImport; - if (!documentElement || !platformBrowserImport) { - return; - } - const update = tree.beginUpdate(relative(basePath, sourceFile.fileName)); - - const platformBrowserDeclaration = platformBrowserImport.parent.parent; - const newPlatformBrowserText = - removeFromImport(platformBrowserImport, sourceFile, DOCUMENT_TOKEN_NAME); - const newCommonText = commonImport ? - addToImport(commonImport, sourceFile, documentElement.name, documentElement.propertyName) : - createImport(COMMON_IMPORT, sourceFile, documentElement.name, documentElement.propertyName); - - // Replace the existing query decorator call expression with the updated - // call expression node. - update.remove(platformBrowserDeclaration.getStart(), platformBrowserDeclaration.getWidth()); - update.insertRight(platformBrowserDeclaration.getStart(), newPlatformBrowserText); - - if (commonImport) { - const commonDeclaration = commonImport.parent.parent; - update.remove(commonDeclaration.getStart(), commonDeclaration.getWidth()); - update.insertRight(commonDeclaration.getStart(), newCommonText); - } else { - update.insertRight(platformBrowserDeclaration.getStart(), newCommonText); - } - - tree.commitUpdate(update); - }); -} diff --git a/packages/core/schematics/migrations/move-document/move-import.ts b/packages/core/schematics/migrations/move-document/move-import.ts deleted file mode 100644 index c7ef2610419..00000000000 --- a/packages/core/schematics/migrations/move-document/move-import.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; - -export function removeFromImport( - importNode: ts.NamedImports, sourceFile: ts.SourceFile, importName: string): string { - const printer = ts.createPrinter(); - const elements = importNode.elements.filter( - el => String((el.propertyName || el.name).escapedText) !== importName); - - if (!elements.length) { - return ''; - } - - const oldDeclaration = importNode.parent.parent; - const newImport = ts.createNamedImports(elements); - const importClause = ts.createImportClause(undefined, newImport); - const newDeclaration = ts.createImportDeclaration( - undefined, undefined, importClause, oldDeclaration.moduleSpecifier); - - return printer.printNode(ts.EmitHint.Unspecified, newDeclaration, sourceFile); -} - -export function addToImport( - importNode: ts.NamedImports, sourceFile: ts.SourceFile, name: ts.Identifier, - propertyName?: ts.Identifier): string { - const printer = ts.createPrinter(); - const propertyNameIdentifier = - propertyName ? ts.createIdentifier(String(propertyName.escapedText)) : undefined; - const nameIdentifier = ts.createIdentifier(String(name.escapedText)); - const newSpecfier = ts.createImportSpecifier(propertyNameIdentifier, nameIdentifier); - const elements = [...importNode.elements]; - - elements.push(newSpecfier); - - const oldDeclaration = importNode.parent.parent; - const newImport = ts.createNamedImports(elements); - const importClause = ts.createImportClause(undefined, newImport); - const newDeclaration = ts.createImportDeclaration( - undefined, undefined, importClause, oldDeclaration.moduleSpecifier); - - return printer.printNode(ts.EmitHint.Unspecified, newDeclaration, sourceFile); -} - -export function createImport( - importSource: string, sourceFile: ts.SourceFile, name: ts.Identifier, - propertyName?: ts.Identifier) { - const printer = ts.createPrinter(); - const propertyNameIdentifier = - propertyName ? ts.createIdentifier(String(propertyName.escapedText)) : undefined; - const nameIdentifier = ts.createIdentifier(String(name.escapedText)); - const newSpecfier = ts.createImportSpecifier(propertyNameIdentifier, nameIdentifier); - const newNamedImports = ts.createNamedImports([newSpecfier]); - const importClause = ts.createImportClause(undefined, newNamedImports); - const moduleSpecifier = ts.createStringLiteral(importSource); - const newImport = ts.createImportDeclaration(undefined, undefined, importClause, moduleSpecifier); - - return printer.printNode(ts.EmitHint.Unspecified, newImport, sourceFile); -} diff --git a/packages/core/schematics/migrations/native-view-encapsulation/BUILD.bazel b/packages/core/schematics/migrations/native-view-encapsulation/BUILD.bazel deleted file mode 100644 index 4c08ab63bf6..00000000000 --- a/packages/core/schematics/migrations/native-view-encapsulation/BUILD.bazel +++ /dev/null @@ -1,18 +0,0 @@ -load("//tools:defaults.bzl", "ts_library") - -ts_library( - name = "native-view-encapsulation", - srcs = glob(["**/*.ts"]), - tsconfig = "//packages/core/schematics:tsconfig.json", - visibility = [ - "//packages/core/schematics:__pkg__", - "//packages/core/schematics/migrations/google3:__pkg__", - "//packages/core/schematics/test:__pkg__", - ], - deps = [ - "//packages/core/schematics/utils", - "@npm//@angular-devkit/schematics", - "@npm//@types/node", - "@npm//typescript", - ], -) diff --git a/packages/core/schematics/migrations/native-view-encapsulation/README.md b/packages/core/schematics/migrations/native-view-encapsulation/README.md deleted file mode 100644 index 0a9ab191a05..00000000000 --- a/packages/core/schematics/migrations/native-view-encapsulation/README.md +++ /dev/null @@ -1,34 +0,0 @@ -## `ViewEncapsulation.Native` migration - -Automatically migrates usages of `ViewEncapsulation.Native` to `ViewEncapsulation.ShadowDom`. -For most practical purposes the `Native` mode is compatible with the `ShadowDom` mode. - -The migration covers any reference to the `Native` value that can be traced to `@angular/core`. -Some examples: -* Inside the `encapsulation` property of `Component` decorators. -* In property assignments for the `COMPILER_OPTIONS` provider. -* In variables. - -#### Before -```ts -import { Component, ViewEncapsulation } from '@angular/core'; - -@Component({ - template: '...', - encapsulation: ViewEncapsulation.Native -}) -export class App { -} -``` - -#### After -```ts -import { Component, ViewEncapsulation } from '@angular/core'; - -@Component({ - template: '...', - encapsulation: ViewEncapsulation.ShadowDom -}) -export class App { -} -``` diff --git a/packages/core/schematics/migrations/native-view-encapsulation/index.ts b/packages/core/schematics/migrations/native-view-encapsulation/index.ts deleted file mode 100644 index c0895057986..00000000000 --- a/packages/core/schematics/migrations/native-view-encapsulation/index.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * @license - * Copyright Google LLC 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, SchematicsException, Tree} from '@angular-devkit/schematics'; -import {relative} from 'path'; - -import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; -import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host'; -import {findNativeEncapsulationNodes} from './util'; - - -/** Migration that switches from `ViewEncapsulation.Native` to `ViewEncapsulation.ShadowDom`. */ -export default function(): Rule { - return async (tree: Tree) => { - const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); - const basePath = process.cwd(); - const allPaths = [...buildPaths, ...testPaths]; - - if (!allPaths.length) { - throw new SchematicsException( - 'Could not find any tsconfig file. Cannot migrate away from Native view encapsulation.'); - } - - for (const tsconfigPath of allPaths) { - runNativeViewEncapsulationMigration(tree, tsconfigPath, basePath); - } - }; -} - -function runNativeViewEncapsulationMigration(tree: Tree, tsconfigPath: string, basePath: string) { - const {program} = createMigrationProgram(tree, tsconfigPath, basePath); - const typeChecker = program.getTypeChecker(); - const sourceFiles = - program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program)); - - sourceFiles.forEach(sourceFile => { - const update = tree.beginUpdate(relative(basePath, sourceFile.fileName)); - const identifiers = findNativeEncapsulationNodes(typeChecker, sourceFile); - - identifiers.forEach(node => { - update.remove(node.getStart(), node.getWidth()); - update.insertRight(node.getStart(), 'ShadowDom'); - }); - - tree.commitUpdate(update); - }); -} diff --git a/packages/core/schematics/migrations/native-view-encapsulation/util.ts b/packages/core/schematics/migrations/native-view-encapsulation/util.ts deleted file mode 100644 index 4d56019d8ee..00000000000 --- a/packages/core/schematics/migrations/native-view-encapsulation/util.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; - -import {getImportOfIdentifier} from '../../utils/typescript/imports'; - -/** Finds all the Identifier nodes in a file that refer to `Native` view encapsulation. */ -export function findNativeEncapsulationNodes( - typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile): Set { - const results = new Set(); - - sourceFile.forEachChild(function walkNode(node: ts.Node) { - // Note that we look directly for nodes in the form of `.Native`, rather than going - // for `Component` class decorators, because it's much simpler and it allows us to handle cases - // where `ViewEncapsulation.Native` might be used in a different context (e.g. a variable). - // Using the encapsulation outside of a decorator is an edge case, but we do have public APIs - // where it can be passed in (see the `defaultViewEncapsulation` property on the - // `COMPILER_OPTIONS` provider). - if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.name) && - node.name.text === 'Native' && ts.isIdentifier(node.expression)) { - const expressionImport = getImportOfIdentifier(typeChecker, node.expression); - if (expressionImport && expressionImport.name === 'ViewEncapsulation' && - expressionImport.importModule === '@angular/core') { - results.add(node.name); - } - } else { - node.forEachChild(walkNode); - } - }); - - return results; -} diff --git a/packages/core/schematics/migrations/navigation-extras-omissions/BUILD.bazel b/packages/core/schematics/migrations/navigation-extras-omissions/BUILD.bazel deleted file mode 100644 index dc793c97023..00000000000 --- a/packages/core/schematics/migrations/navigation-extras-omissions/BUILD.bazel +++ /dev/null @@ -1,18 +0,0 @@ -load("//tools:defaults.bzl", "ts_library") - -ts_library( - name = "navigation-extras-omissions", - srcs = glob(["**/*.ts"]), - tsconfig = "//packages/core/schematics:tsconfig.json", - visibility = [ - "//packages/core/schematics:__pkg__", - "//packages/core/schematics/migrations/google3:__pkg__", - "//packages/core/schematics/test:__pkg__", - ], - deps = [ - "//packages/core/schematics/utils", - "@npm//@angular-devkit/schematics", - "@npm//@types/node", - "@npm//typescript", - ], -) diff --git a/packages/core/schematics/migrations/navigation-extras-omissions/README.md b/packages/core/schematics/migrations/navigation-extras-omissions/README.md deleted file mode 100644 index e8236f03adc..00000000000 --- a/packages/core/schematics/migrations/navigation-extras-omissions/README.md +++ /dev/null @@ -1,35 +0,0 @@ -## Router.navigateByUrl and Router.createUrlTree extras migration - -Previously the `extras` parameter of `Router.navigateByUrl` and `Router.createUrlTree` accepted the -full `NavigationExtras` object, even though only a subset of properties was supported. This -migration removes the unsupported properties from the relevant method call sites. - -#### Before -```ts -import { Component } from '@angular/core'; -import { Router } from '@angular/router'; - -@Component({}) -export class MyComponent { - constructor(private _router: Router) {} - - goHome() { - this._router.navigateByUrl('/', {skipLocationChange: false, fragment: 'foo'}); - } -} -``` - -#### After -```ts -import { Component } from '@angular/core'; -import { Router } from '@angular/router'; - -@Component({}) -export class MyComponent { - constructor(private _router: Router) {} - - goHome() { - this._router.navigateByUrl('/', { /* Removed unsupported properties by Angular migration: fragment. */ skipLocationChange: false }); - } -} -``` diff --git a/packages/core/schematics/migrations/navigation-extras-omissions/index.ts b/packages/core/schematics/migrations/navigation-extras-omissions/index.ts deleted file mode 100644 index fe7eb998b7b..00000000000 --- a/packages/core/schematics/migrations/navigation-extras-omissions/index.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * @license - * Copyright Google LLC 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, SchematicsException, Tree} from '@angular-devkit/schematics'; -import {relative} from 'path'; -import ts from 'typescript'; - -import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; -import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host'; -import {findLiteralsToMigrate, migrateLiteral} from './util'; - - -/** Migration that switches `Router.navigateByUrl` and `Router.createUrlTree` to a new signature. */ -export default function(): Rule { - return async (tree: Tree) => { - const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); - const basePath = process.cwd(); - const allPaths = [...buildPaths, ...testPaths]; - - if (!allPaths.length) { - throw new SchematicsException( - 'Could not find any tsconfig file. Cannot migrate ' + - 'Router.navigateByUrl and Router.createUrlTree calls.'); - } - - for (const tsconfigPath of allPaths) { - runNavigationExtrasOmissionsMigration(tree, tsconfigPath, basePath); - } - }; -} - -function runNavigationExtrasOmissionsMigration(tree: Tree, tsconfigPath: string, basePath: string) { - const {program} = createMigrationProgram(tree, tsconfigPath, basePath); - const typeChecker = program.getTypeChecker(); - const printer = ts.createPrinter(); - const sourceFiles = - program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program)); - - sourceFiles.forEach(sourceFile => { - const literalsToMigrate = findLiteralsToMigrate(sourceFile, typeChecker); - const update = tree.beginUpdate(relative(basePath, sourceFile.fileName)); - - literalsToMigrate.forEach((instances, methodName) => instances.forEach(instance => { - const migratedNode = migrateLiteral(methodName, instance); - - if (migratedNode !== instance) { - update.remove(instance.getStart(), instance.getWidth()); - update.insertRight( - instance.getStart(), - printer.printNode(ts.EmitHint.Unspecified, migratedNode, sourceFile)); - } - })); - - tree.commitUpdate(update); - }); -} diff --git a/packages/core/schematics/migrations/navigation-extras-omissions/util.ts b/packages/core/schematics/migrations/navigation-extras-omissions/util.ts deleted file mode 100644 index f6858a21163..00000000000 --- a/packages/core/schematics/migrations/navigation-extras-omissions/util.ts +++ /dev/null @@ -1,125 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; - -import {getImportSpecifier} from '../../utils/typescript/imports'; -import {isReferenceToImport} from '../../utils/typescript/symbol'; - -/** - * Configures the methods that the migration should be looking for - * and the properties from `NavigationExtras` that should be preserved. - */ -const methodConfig = new Map>([ - ['navigateByUrl', new Set(['skipLocationChange', 'replaceUrl', 'state'])], - [ - 'createUrlTree', new Set([ - 'relativeTo', 'queryParams', 'fragment', 'preserveQueryParams', 'queryParamsHandling', - 'preserveFragment' - ]) - ] -]); - -export function migrateLiteral( - methodName: string, node: ts.ObjectLiteralExpression): ts.ObjectLiteralExpression { - const allowedProperties = methodConfig.get(methodName); - - if (!allowedProperties) { - throw Error(`Attempting to migrate unconfigured method called ${methodName}.`); - } - - const propertiesToKeep: ts.ObjectLiteralElementLike[] = []; - const removedPropertyNames: string[] = []; - - node.properties.forEach(property => { - // Only look for regular and shorthand property assignments since resolving things - // like spread operators becomes too complicated for this migration. - if ((ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) && - (ts.isStringLiteralLike(property.name) || ts.isNumericLiteral(property.name) || - ts.isIdentifier(property.name))) { - if (allowedProperties.has(property.name.text)) { - propertiesToKeep.push(property); - } else { - removedPropertyNames.push(property.name.text); - } - } else { - propertiesToKeep.push(property); - } - }); - - // Don't modify the node if there's nothing to remove. - if (removedPropertyNames.length === 0) { - return node; - } - - // Note that the trailing/leading spaces are necessary so the comment looks good. - const removalComment = - ` Removed unsupported properties by Angular migration: ${removedPropertyNames.join(', ')}. `; - - if (propertiesToKeep.length > 0) { - propertiesToKeep[0] = addUniqueLeadingComment(propertiesToKeep[0], removalComment); - return ts.createObjectLiteral(propertiesToKeep); - } else { - return addUniqueLeadingComment(ts.createObjectLiteral(propertiesToKeep), removalComment); - } -} - -export function findLiteralsToMigrate(sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker) { - const results = new Map>( - Array.from(methodConfig.keys(), key => [key, new Set()])); - const routerImport = getImportSpecifier(sourceFile, '@angular/router', 'Router'); - const seenLiterals = new Map(); - - if (routerImport) { - sourceFile.forEachChild(function visitNode(node: ts.Node) { - // Look for calls that look like `foo.` with more than one parameter. - if (ts.isCallExpression(node) && node.arguments.length > 1 && - ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.name) && - methodConfig.has(node.expression.name.text)) { - // Check whether the type of the object on which the - // function is called refers to the Router import. - if (isReferenceToImport(typeChecker, node.expression.expression, routerImport)) { - const methodName = node.expression.name.text; - const parameterDeclaration = - typeChecker.getTypeAtLocation(node.arguments[1]).getSymbol()?.valueDeclaration; - - // Find the source of the object literal. - if (parameterDeclaration && ts.isObjectLiteralExpression(parameterDeclaration)) { - if (!seenLiterals.has(parameterDeclaration)) { - results.get(methodName)!.add(parameterDeclaration); - seenLiterals.set(parameterDeclaration, methodName); - // If the same literal has been passed into multiple different methods, we can't - // migrate it, because the supported properties are different. When we detect such - // a case, we drop it from the results so that it gets ignored. If it's used multiple - // times for the same method, it can still be migrated. - } else if (seenLiterals.get(parameterDeclaration) !== methodName) { - results.forEach(literals => literals.delete(parameterDeclaration)); - } - } - } - } else { - node.forEachChild(visitNode); - } - }); - } - - return results; -} - -/** Adds a leading comment to a node, if the node doesn't have such a comment already. */ -function addUniqueLeadingComment(node: T, comment: string): T { - const existingComments = ts.getSyntheticLeadingComments(node); - - // This logic is primarily to ensure that we don't add the same comment multiple - // times when tslint runs over the same file again with outdated information. - if (!existingComments || existingComments.every(c => c.text !== comment)) { - return ts.addSyntheticLeadingComment(node, ts.SyntaxKind.MultiLineCommentTrivia, comment); - } - - return node; -} diff --git a/packages/core/schematics/migrations/relative-link-resolution/BUILD.bazel b/packages/core/schematics/migrations/relative-link-resolution/BUILD.bazel deleted file mode 100644 index b233da5b30a..00000000000 --- a/packages/core/schematics/migrations/relative-link-resolution/BUILD.bazel +++ /dev/null @@ -1,20 +0,0 @@ -load("//tools:defaults.bzl", "ts_library") - -ts_library( - name = "relative-link-resolution", - srcs = glob(["**/*.ts"]), - tsconfig = "//packages/core/schematics:tsconfig.json", - visibility = [ - "//packages/core/schematics:__pkg__", - "//packages/core/schematics/migrations/google3:__pkg__", - "//packages/core/schematics/migrations/relative-link-resolution/google3:__pkg__", - "//packages/core/schematics/test:__pkg__", - ], - deps = [ - "//packages/compiler-cli", - "//packages/core/schematics/utils", - "@npm//@angular-devkit/schematics", - "@npm//@types/node", - "@npm//typescript", - ], -) diff --git a/packages/core/schematics/migrations/relative-link-resolution/README.md b/packages/core/schematics/migrations/relative-link-resolution/README.md deleted file mode 100644 index 3d99628992e..00000000000 --- a/packages/core/schematics/migrations/relative-link-resolution/README.md +++ /dev/null @@ -1,33 +0,0 @@ -## relativeLinkResolution migration - -The default value for `relativeLinkResolution` is changing from 'legacy' to 'corrected'. -This migration updates `RouterModule` configurations that use the default value to -now specifically use 'legacy' to prevent breakages when updating. - -#### Before -```ts -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; - -@NgModule({ - imports: [ - RouterModule.forRoot(ROUTES), - ] -}) -export class AppModule { -} -``` - -#### After -```ts -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; - -@NgModule({ - imports: [ - RouterModule.forRoot(ROUTES, {relativeLinkResolution: 'legacy'}), - ] -}) -export class AppModule { -} -``` \ No newline at end of file diff --git a/packages/core/schematics/migrations/relative-link-resolution/collector.ts b/packages/core/schematics/migrations/relative-link-resolution/collector.ts deleted file mode 100644 index a47976ff67c..00000000000 --- a/packages/core/schematics/migrations/relative-link-resolution/collector.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; - -import {isExtraOptions, isRouterModuleForRoot} from './util'; - - -/** - * Visitor that walks through specified TypeScript nodes and collects all - * found ExtraOptions#RelativeLinkResolution assignments. - */ -export class RelativeLinkResolutionCollector { - readonly forRootCalls: ts.CallExpression[] = []; - readonly extraOptionsLiterals: ts.ObjectLiteralExpression[] = []; - - constructor(private readonly typeChecker: ts.TypeChecker) {} - - visitNode(node: ts.Node) { - let forRootCall: ts.CallExpression|null = null; - let literal: ts.ObjectLiteralExpression|null = null; - if (isRouterModuleForRoot(this.typeChecker, node) && node.arguments.length > 0) { - if (node.arguments.length === 1) { - forRootCall = node; - } else if (ts.isObjectLiteralExpression(node.arguments[1])) { - literal = node.arguments[1] as ts.ObjectLiteralExpression; - } else if (ts.isIdentifier(node.arguments[1])) { - literal = this.getLiteralNeedingMigrationFromIdentifier(node.arguments[1] as ts.Identifier); - } - } else if (ts.isVariableDeclaration(node)) { - literal = this.getLiteralNeedingMigration(node); - } - - if (literal !== null) { - this.extraOptionsLiterals.push(literal); - } else if (forRootCall !== null) { - this.forRootCalls.push(forRootCall); - } else { - // no match found, continue iteration - ts.forEachChild(node, n => this.visitNode(n)); - } - } - - private getLiteralNeedingMigrationFromIdentifier(id: ts.Identifier): ts.ObjectLiteralExpression - |null { - const symbolForIdentifier = this.typeChecker.getSymbolAtLocation(id); - if (symbolForIdentifier === undefined) { - return null; - } - - if (symbolForIdentifier.declarations === undefined || - symbolForIdentifier.declarations.length === 0) { - return null; - } - - const declarationNode = symbolForIdentifier.declarations[0]; - if (!ts.isVariableDeclaration(declarationNode) || declarationNode.initializer === undefined || - !ts.isObjectLiteralExpression(declarationNode.initializer)) { - return null; - } - - return declarationNode.initializer; - } - - private getLiteralNeedingMigration(node: ts.VariableDeclaration): ts.ObjectLiteralExpression - |null { - if (node.initializer === undefined) { - return null; - } - - // declaration could be `x: ExtraOptions = {}` or `x = {} as ExtraOptions` - if (ts.isAsExpression(node.initializer) && - ts.isObjectLiteralExpression(node.initializer.expression) && - isExtraOptions(this.typeChecker, node.initializer.type)) { - return node.initializer.expression; - } else if ( - node.type !== undefined && ts.isObjectLiteralExpression(node.initializer) && - isExtraOptions(this.typeChecker, node.type)) { - return node.initializer; - } - - return null; - } -} diff --git a/packages/core/schematics/migrations/relative-link-resolution/google3/BUILD.bazel b/packages/core/schematics/migrations/relative-link-resolution/google3/BUILD.bazel deleted file mode 100644 index 1a2d4e6b789..00000000000 --- a/packages/core/schematics/migrations/relative-link-resolution/google3/BUILD.bazel +++ /dev/null @@ -1,13 +0,0 @@ -load("//tools:defaults.bzl", "ts_library") - -ts_library( - name = "google3", - srcs = glob(["**/*.ts"]), - tsconfig = "//packages/core/schematics:tsconfig.json", - visibility = ["//packages/core/schematics/migrations/google3:__pkg__"], - deps = [ - "//packages/core/schematics/migrations/relative-link-resolution", - "@npm//tslint", - "@npm//typescript", - ], -) diff --git a/packages/core/schematics/migrations/relative-link-resolution/google3/tslint_update_recorder.ts b/packages/core/schematics/migrations/relative-link-resolution/google3/tslint_update_recorder.ts deleted file mode 100644 index cbf73d8a1f1..00000000000 --- a/packages/core/schematics/migrations/relative-link-resolution/google3/tslint_update_recorder.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 {Replacement, RuleFailure} from 'tslint'; -import ts from 'typescript'; - -import {UpdateRecorder} from '../update_recorder'; - -export class TslintUpdateRecorder implements UpdateRecorder { - failures: RuleFailure[] = []; - - constructor(private ruleName: string, private sourceFile: ts.SourceFile) {} - - updateNode(node: ts.Node, newText: string) { - this.failures.unshift(new RuleFailure( - this.sourceFile, node.getStart(), 0, - 'The relativeLinkResolution default is changing from `legacy` to `corrected`. To keep behavior consistent' + - ' when the change is merged, specify `legacy` rather than using the default.', - this.ruleName, Replacement.replaceFromTo(node.getStart(), node.getEnd(), `${newText}`))); - } - - commitUpdate() {} -} diff --git a/packages/core/schematics/migrations/relative-link-resolution/index.ts b/packages/core/schematics/migrations/relative-link-resolution/index.ts deleted file mode 100644 index b0deb46b8c4..00000000000 --- a/packages/core/schematics/migrations/relative-link-resolution/index.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * @license - * Copyright Google LLC 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, SchematicsException, Tree} from '@angular-devkit/schematics'; -import {relative} from 'path'; -import ts from 'typescript'; -import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; -import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host'; -import {RelativeLinkResolutionCollector} from './collector'; -import {RelativeLinkResolutionTransform} from './transform'; -import {UpdateRecorder} from './update_recorder'; - -/** Entry point for the v11 "relativeLinkResolution RouterModule options" schematic. */ -export default function(): Rule { - return async (tree: Tree) => { - const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); - const basePath = process.cwd(); - - if (!buildPaths.length && !testPaths.length) { - throw new SchematicsException( - 'Could not find any tsconfig file. Cannot update the "relativeLinkResolution" option for RouterModule'); - } - - for (const tsconfigPath of [...buildPaths, ...testPaths]) { - runRelativeLinkResolutionMigration(tree, tsconfigPath, basePath); - } - }; -} - -function runRelativeLinkResolutionMigration(tree: Tree, tsconfigPath: string, basePath: string) { - const {program} = createMigrationProgram(tree, tsconfigPath, basePath); - const typeChecker = program.getTypeChecker(); - const relativeLinkResolutionCollector = new RelativeLinkResolutionCollector(typeChecker); - const sourceFiles = - program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program)); - - // Analyze source files by detecting all modules. - sourceFiles.forEach(sourceFile => relativeLinkResolutionCollector.visitNode(sourceFile)); - - const {forRootCalls, extraOptionsLiterals} = relativeLinkResolutionCollector; - const transformer = new RelativeLinkResolutionTransform(getUpdateRecorder); - const updateRecorders = new Map(); - transformer.migrateRouterModuleForRootCalls(forRootCalls); - transformer.migrateObjectLiterals(extraOptionsLiterals); - - // Walk through each update recorder and commit the update. We need to commit the - // updates in batches per source file as there can be only one recorder per source - // file in order to avoid shift character offsets. - updateRecorders.forEach(recorder => recorder.commitUpdate()); - - /** Gets the update recorder for the specified source file. */ - function getUpdateRecorder(sourceFile: ts.SourceFile): UpdateRecorder { - if (updateRecorders.has(sourceFile)) { - return updateRecorders.get(sourceFile)!; - } - const treeRecorder = tree.beginUpdate(relative(basePath, sourceFile.fileName)); - const recorder: UpdateRecorder = { - updateNode(node: ts.Node, newText: string) { - treeRecorder.remove(node.getStart(), node.getWidth()); - treeRecorder.insertRight(node.getStart(), newText); - }, - commitUpdate() { - tree.commitUpdate(treeRecorder); - } - }; - updateRecorders.set(sourceFile, recorder); - return recorder; - } -} diff --git a/packages/core/schematics/migrations/relative-link-resolution/transform.ts b/packages/core/schematics/migrations/relative-link-resolution/transform.ts deleted file mode 100644 index 844f012a695..00000000000 --- a/packages/core/schematics/migrations/relative-link-resolution/transform.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; - -import {UpdateRecorder} from './update_recorder'; - - -const RELATIVE_LINK_RESOLUTION = 'relativeLinkResolution'; - -export class RelativeLinkResolutionTransform { - private printer = ts.createPrinter(); - - constructor(private getUpdateRecorder: (sf: ts.SourceFile) => UpdateRecorder) {} - - /** Migrate the ExtraOptions#RelativeLinkResolution property assignments. */ - migrateRouterModuleForRootCalls(calls: ts.CallExpression[]) { - calls.forEach(c => { - this._updateCallExpressionWithoutExtraOptions(c); - }); - } - - migrateObjectLiterals(vars: ts.ObjectLiteralExpression[]) { - vars.forEach(v => this._maybeUpdateLiteral(v)); - } - - private _updateCallExpressionWithoutExtraOptions(callExpression: ts.CallExpression) { - const args = callExpression.arguments; - const emptyLiteral = ts.createObjectLiteral(); - const newNode = ts.updateCall( - callExpression, callExpression.expression, callExpression.typeArguments, - [args[0], this._getMigratedLiteralExpression(emptyLiteral)]); - this._updateNode(callExpression, newNode); - } - - private _getMigratedLiteralExpression(literal: ts.ObjectLiteralExpression) { - if (literal.properties.some( - prop => ts.isPropertyAssignment(prop) && - prop.name.getText() === RELATIVE_LINK_RESOLUTION)) { - // literal already defines a value for relativeLinkResolution. Skip it - return literal; - } - const legacyExpression = - ts.createPropertyAssignment(RELATIVE_LINK_RESOLUTION, ts.createIdentifier(`'legacy'`)); - return ts.updateObjectLiteral(literal, [...literal.properties, legacyExpression]); - } - - private _maybeUpdateLiteral(literal: ts.ObjectLiteralExpression) { - const updatedLiteral = this._getMigratedLiteralExpression(literal); - if (updatedLiteral !== literal) { - this._updateNode(literal, updatedLiteral); - } - } - - private _updateNode(node: ts.Node, newNode: ts.Node) { - const newText = this.printer.printNode(ts.EmitHint.Unspecified, newNode, node.getSourceFile()); - const recorder = this.getUpdateRecorder(node.getSourceFile()); - recorder.updateNode(node, newText); - } -} diff --git a/packages/core/schematics/migrations/relative-link-resolution/update_recorder.ts b/packages/core/schematics/migrations/relative-link-resolution/update_recorder.ts deleted file mode 100644 index 6156375460f..00000000000 --- a/packages/core/schematics/migrations/relative-link-resolution/update_recorder.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; - -/** - * Update recorder interface that is used to transform source files in a non-colliding - * way. Also this indirection makes it possible to re-use logic for both TSLint rules - * and CLI devkit schematic updates. - */ -export interface UpdateRecorder { - updateNode(node: ts.Node, newText: string): void; - commitUpdate(): void; -} diff --git a/packages/core/schematics/migrations/relative-link-resolution/util.ts b/packages/core/schematics/migrations/relative-link-resolution/util.ts deleted file mode 100644 index 6f8a5a36f02..00000000000 --- a/packages/core/schematics/migrations/relative-link-resolution/util.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; -import {getImportOfIdentifier} from '../../utils/typescript/imports'; - -/** Determine whether a node is a ModuleWithProviders type reference node without a generic type */ -export function isRouterModuleForRoot( - typeChecker: ts.TypeChecker, node: ts.Node): node is ts.CallExpression { - if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression) || - !ts.isIdentifier(node.expression.expression) || node.expression.name.text !== 'forRoot') { - return false; - } - const imp = getImportOfIdentifier(typeChecker, node.expression.expression); - return !!imp && imp.name === 'RouterModule' && imp.importModule === '@angular/router' && - !node.typeArguments; -} - -export function isExtraOptions( - typeChecker: ts.TypeChecker, node: ts.Node): node is ts.TypeReferenceNode { - if (!ts.isTypeReferenceNode(node) || !ts.isIdentifier(node.typeName)) { - return false; - } - - const imp = getImportOfIdentifier(typeChecker, node.typeName); - return imp !== null && imp.name === 'ExtraOptions' && imp.importModule === '@angular/router' && - !node.typeArguments; -} diff --git a/packages/core/schematics/migrations/renderer-to-renderer2/BUILD.bazel b/packages/core/schematics/migrations/renderer-to-renderer2/BUILD.bazel deleted file mode 100644 index 58b6c7c6952..00000000000 --- a/packages/core/schematics/migrations/renderer-to-renderer2/BUILD.bazel +++ /dev/null @@ -1,18 +0,0 @@ -load("//tools:defaults.bzl", "ts_library") - -ts_library( - name = "renderer-to-renderer2", - srcs = glob(["**/*.ts"]), - tsconfig = "//packages/core/schematics:tsconfig.json", - visibility = [ - "//packages/core/schematics:__pkg__", - "//packages/core/schematics/migrations/google3:__pkg__", - "//packages/core/schematics/test:__pkg__", - ], - deps = [ - "//packages/core/schematics/utils", - "@npm//@angular-devkit/schematics", - "@npm//@types/node", - "@npm//typescript", - ], -) diff --git a/packages/core/schematics/migrations/renderer-to-renderer2/README.md b/packages/core/schematics/migrations/renderer-to-renderer2/README.md deleted file mode 100644 index b816cdd0911..00000000000 --- a/packages/core/schematics/migrations/renderer-to-renderer2/README.md +++ /dev/null @@ -1,33 +0,0 @@ -## Renderer -> Renderer2 migration - -Automatically migrates from `Renderer` to `Renderer2` by changing method calls, renaming imports -and renaming types. Tries to either map method calls directly from one renderer to the other, or -if that's not possible, inserts custom helper functions at the bottom of the file. - -#### Before -```ts -import { Renderer, ElementRef } from '@angular/core'; - -@Component({}) -export class MyComponent { - constructor(private _renderer: Renderer, private _elementRef: ElementRef) {} - - changeColor() { - this._renderer.setElementStyle(this._element.nativeElement, 'color', 'purple'); - } -} -``` - -#### After -```ts -import { Renderer2, ElementRef } from '@angular/core'; - -@Component({}) -export class MyComponent { - constructor(private _renderer: Renderer2, private _elementRef: ElementRef) {} - - changeColor() { - this._renderer.setStyle(this._element.nativeElement, 'color', 'purple'); - } -} -``` diff --git a/packages/core/schematics/migrations/renderer-to-renderer2/helpers.ts b/packages/core/schematics/migrations/renderer-to-renderer2/helpers.ts deleted file mode 100644 index 13a770b6692..00000000000 --- a/packages/core/schematics/migrations/renderer-to-renderer2/helpers.ts +++ /dev/null @@ -1,403 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; - -/** Names of the helper functions that are supported for this migration. */ -export const enum HelperFunction { - any = 'AnyDuringRendererMigration', - createElement = '__ngRendererCreateElementHelper', - createText = '__ngRendererCreateTextHelper', - createTemplateAnchor = '__ngRendererCreateTemplateAnchorHelper', - projectNodes = '__ngRendererProjectNodesHelper', - animate = '__ngRendererAnimateHelper', - destroyView = '__ngRendererDestroyViewHelper', - detachView = '__ngRendererDetachViewHelper', - attachViewAfter = '__ngRendererAttachViewAfterHelper', - splitNamespace = '__ngRendererSplitNamespaceHelper', - setElementAttribute = '__ngRendererSetElementAttributeHelper' -} - -/** Gets the string representation of a helper function. */ -export function getHelper( - name: HelperFunction, sourceFile: ts.SourceFile, printer: ts.Printer): string { - const helperDeclaration = getHelperDeclaration(name); - return '\n' + printer.printNode(ts.EmitHint.Unspecified, helperDeclaration, sourceFile) + '\n'; -} - -/** Creates a function declaration for the specified helper name. */ -function getHelperDeclaration(name: HelperFunction): ts.Node { - switch (name) { - case HelperFunction.any: - return createAnyTypeHelper(); - case HelperFunction.createElement: - return getCreateElementHelper(); - case HelperFunction.createText: - return getCreateTextHelper(); - case HelperFunction.createTemplateAnchor: - return getCreateTemplateAnchorHelper(); - case HelperFunction.projectNodes: - return getProjectNodesHelper(); - case HelperFunction.animate: - return getAnimateHelper(); - case HelperFunction.destroyView: - return getDestroyViewHelper(); - case HelperFunction.detachView: - return getDetachViewHelper(); - case HelperFunction.attachViewAfter: - return getAttachViewAfterHelper(); - case HelperFunction.setElementAttribute: - return getSetElementAttributeHelper(); - case HelperFunction.splitNamespace: - return getSplitNamespaceHelper(); - } -} - -/** Creates a helper for a custom `any` type during the migration. */ -function createAnyTypeHelper(): ts.TypeAliasDeclaration { - // type AnyDuringRendererMigration = any; - return ts.createTypeAliasDeclaration( - [], [], HelperFunction.any, [], ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); -} - -/** Creates a function parameter that is typed as `any`. */ -function getAnyTypedParameter( - parameterName: string|ts.Identifier, isRequired = true): ts.ParameterDeclaration { - // Declare the parameter as `any` so we don't have to add extra logic to ensure that the - // generated code will pass type checking. Use our custom `any` type so people have an incentive - // to clean it up afterwards and to avoid potentially introducing lint warnings in G3. - const type = ts.createTypeReferenceNode(HelperFunction.any, []); - return ts.createParameter( - [], [], undefined, parameterName, - isRequired ? undefined : ts.createToken(ts.SyntaxKind.QuestionToken), type); -} - -/** Creates a helper for `createElement`. */ -function getCreateElementHelper(): ts.FunctionDeclaration { - const renderer = ts.createIdentifier('renderer'); - const parent = ts.createIdentifier('parent'); - const namespaceAndName = ts.createIdentifier('namespaceAndName'); - const name = ts.createIdentifier('name'); - const namespace = ts.createIdentifier('namespace'); - - // [namespace, name] = splitNamespace(namespaceAndName); - const namespaceAndNameVariable = ts.createVariableDeclaration( - ts.createArrayBindingPattern( - [namespace, name].map(id => ts.createBindingElement(undefined, undefined, id))), - undefined, - ts.createCall(ts.createIdentifier(HelperFunction.splitNamespace), [], [namespaceAndName])); - - // `renderer.createElement(name, namespace)`. - const creationCall = - ts.createCall(ts.createPropertyAccess(renderer, 'createElement'), [], [name, namespace]); - - return getCreationHelper( - HelperFunction.createElement, creationCall, renderer, parent, [namespaceAndName], - [ts.createVariableStatement( - undefined, - ts.createVariableDeclarationList([namespaceAndNameVariable], ts.NodeFlags.Const))]); -} - -/** Creates a helper for `createText`. */ -function getCreateTextHelper(): ts.FunctionDeclaration { - const renderer = ts.createIdentifier('renderer'); - const parent = ts.createIdentifier('parent'); - const value = ts.createIdentifier('value'); - - // `renderer.createText(value)`. - const creationCall = ts.createCall(ts.createPropertyAccess(renderer, 'createText'), [], [value]); - - return getCreationHelper(HelperFunction.createText, creationCall, renderer, parent, [value]); -} - -/** Creates a helper for `createTemplateAnchor`. */ -function getCreateTemplateAnchorHelper(): ts.FunctionDeclaration { - const renderer = ts.createIdentifier('renderer'); - const parent = ts.createIdentifier('parent'); - - // `renderer.createComment('')`. - const creationCall = ts.createCall( - ts.createPropertyAccess(renderer, 'createComment'), [], [ts.createStringLiteral('')]); - - return getCreationHelper(HelperFunction.createTemplateAnchor, creationCall, renderer, parent); -} - -/** - * Gets the function declaration for a creation helper. This is reused between `createElement`, - * `createText` and `createTemplateAnchor` which follow a very similar pattern. - * @param functionName Function that the helper should have. - * @param creationCall Expression that is used to create a node inside the function. - * @param rendererParameter Parameter for the `renderer`. - * @param parentParameter Parameter for the `parent` inside the function. - * @param extraParameters Extra parameters to be added to the end. - * @param precedingVariables Extra variables to be added before the one that creates the `node`. - */ -function getCreationHelper( - functionName: HelperFunction, creationCall: ts.CallExpression, renderer: ts.Identifier, - parent: ts.Identifier, extraParameters: ts.Identifier[] = [], - precedingVariables: ts.VariableStatement[] = []): ts.FunctionDeclaration { - const node = ts.createIdentifier('node'); - - // `const node = {{creationCall}}`. - const nodeVariableStatement = ts.createVariableStatement( - undefined, - ts.createVariableDeclarationList( - [ts.createVariableDeclaration(node, undefined, creationCall)], ts.NodeFlags.Const)); - - // `if (parent) { renderer.appendChild(parent, node) }`. - const guardedAppendChildCall = ts.createIf( - parent, - ts.createBlock( - [ts.createExpressionStatement( - ts.createCall(ts.createPropertyAccess(renderer, 'appendChild'), [], [parent, node]))], - true)); - - return ts.createFunctionDeclaration( - [], [], undefined, functionName, [], - [renderer, parent, ...extraParameters].map(name => getAnyTypedParameter(name)), undefined, - ts.createBlock( - [ - ...precedingVariables, nodeVariableStatement, guardedAppendChildCall, - ts.createReturn(node) - ], - true)); -} - -/** Creates a helper for `projectNodes`. */ -function getProjectNodesHelper(): ts.FunctionDeclaration { - const renderer = ts.createIdentifier('renderer'); - const parent = ts.createIdentifier('parent'); - const nodes = ts.createIdentifier('nodes'); - const incrementor = ts.createIdentifier('i'); - - // for (let i = 0; i < nodes.length; i++) { - // renderer.appendChild(parent, nodes[i]); - // } - const loopInitializer = ts.createVariableDeclarationList( - [ts.createVariableDeclaration(incrementor, undefined, ts.createNumericLiteral('0'))], - ts.NodeFlags.Let); - const loopCondition = ts.createBinary( - incrementor, ts.SyntaxKind.LessThanToken, - ts.createPropertyAccess(nodes, ts.createIdentifier('length'))); - const appendStatement = ts.createExpressionStatement(ts.createCall( - ts.createPropertyAccess(renderer, 'appendChild'), [], - [parent, ts.createElementAccess(nodes, incrementor)])); - const loop = ts.createFor( - loopInitializer, loopCondition, ts.createPostfix(incrementor, ts.SyntaxKind.PlusPlusToken), - ts.createBlock([appendStatement])); - - return ts.createFunctionDeclaration( - [], [], undefined, HelperFunction.projectNodes, [], - [renderer, parent, nodes].map(name => getAnyTypedParameter(name)), undefined, - ts.createBlock([loop], true)); -} - -/** Creates a helper for `animate`. */ -function getAnimateHelper(): ts.FunctionDeclaration { - // throw new Error('...'); - const throwStatement = ts.createThrow(ts.createNew( - ts.createIdentifier('Error'), [], - [ts.createStringLiteral('Renderer.animate is no longer supported!')])); - - return ts.createFunctionDeclaration( - [], [], undefined, HelperFunction.animate, [], [], undefined, - ts.createBlock([throwStatement], true)); -} - -/** Creates a helper for `destroyView`. */ -function getDestroyViewHelper(): ts.FunctionDeclaration { - const renderer = ts.createIdentifier('renderer'); - const allNodes = ts.createIdentifier('allNodes'); - const incrementor = ts.createIdentifier('i'); - - // for (let i = 0; i < allNodes.length; i++) { - // renderer.destroyNode(allNodes[i]); - // } - const loopInitializer = ts.createVariableDeclarationList( - [ts.createVariableDeclaration(incrementor, undefined, ts.createNumericLiteral('0'))], - ts.NodeFlags.Let); - const loopCondition = ts.createBinary( - incrementor, ts.SyntaxKind.LessThanToken, - ts.createPropertyAccess(allNodes, ts.createIdentifier('length'))); - const destroyStatement = ts.createExpressionStatement(ts.createCall( - ts.createPropertyAccess(renderer, 'destroyNode'), [], - [ts.createElementAccess(allNodes, incrementor)])); - const loop = ts.createFor( - loopInitializer, loopCondition, ts.createPostfix(incrementor, ts.SyntaxKind.PlusPlusToken), - ts.createBlock([destroyStatement])); - - return ts.createFunctionDeclaration( - [], [], undefined, HelperFunction.destroyView, [], - [renderer, allNodes].map(name => getAnyTypedParameter(name)), undefined, - ts.createBlock([loop], true)); -} - -/** Creates a helper for `detachView`. */ -function getDetachViewHelper(): ts.FunctionDeclaration { - const renderer = ts.createIdentifier('renderer'); - const rootNodes = ts.createIdentifier('rootNodes'); - const incrementor = ts.createIdentifier('i'); - const node = ts.createIdentifier('node'); - - // for (let i = 0; i < rootNodes.length; i++) { - // const node = rootNodes[i]; - // renderer.removeChild(renderer.parentNode(node), node); - // } - const loopInitializer = ts.createVariableDeclarationList( - [ts.createVariableDeclaration(incrementor, undefined, ts.createNumericLiteral('0'))], - ts.NodeFlags.Let); - const loopCondition = ts.createBinary( - incrementor, ts.SyntaxKind.LessThanToken, - ts.createPropertyAccess(rootNodes, ts.createIdentifier('length'))); - - // const node = rootNodes[i]; - const nodeVariableStatement = ts.createVariableStatement( - undefined, - ts.createVariableDeclarationList( - [ts.createVariableDeclaration( - node, undefined, ts.createElementAccess(rootNodes, incrementor))], - ts.NodeFlags.Const)); - // renderer.removeChild(renderer.parentNode(node), node); - const removeCall = ts.createCall( - ts.createPropertyAccess(renderer, 'removeChild'), [], - [ts.createCall(ts.createPropertyAccess(renderer, 'parentNode'), [], [node]), node]); - - const loop = ts.createFor( - loopInitializer, loopCondition, ts.createPostfix(incrementor, ts.SyntaxKind.PlusPlusToken), - ts.createBlock([nodeVariableStatement, ts.createExpressionStatement(removeCall)])); - - return ts.createFunctionDeclaration( - [], [], undefined, HelperFunction.detachView, [], - [renderer, rootNodes].map(name => getAnyTypedParameter(name)), undefined, - ts.createBlock([loop], true)); -} - -/** Creates a helper for `attachViewAfter` */ -function getAttachViewAfterHelper(): ts.FunctionDeclaration { - const renderer = ts.createIdentifier('renderer'); - const node = ts.createIdentifier('node'); - const rootNodes = ts.createIdentifier('rootNodes'); - const parent = ts.createIdentifier('parent'); - const nextSibling = ts.createIdentifier('nextSibling'); - const incrementor = ts.createIdentifier('i'); - const createConstWithMethodCallInitializer = (constName: ts.Identifier, methodToCall: string) => { - return ts.createVariableStatement( - undefined, - ts.createVariableDeclarationList( - [ts.createVariableDeclaration( - constName, undefined, - ts.createCall(ts.createPropertyAccess(renderer, methodToCall), [], [node]))], - ts.NodeFlags.Const)); - }; - - // const parent = renderer.parentNode(node); - const parentVariableStatement = createConstWithMethodCallInitializer(parent, 'parentNode'); - - // const nextSibling = renderer.nextSibling(node); - const nextSiblingVariableStatement = - createConstWithMethodCallInitializer(nextSibling, 'nextSibling'); - - // for (let i = 0; i < rootNodes.length; i++) { - // renderer.insertBefore(parentElement, rootNodes[i], nextSibling); - // } - const loopInitializer = ts.createVariableDeclarationList( - [ts.createVariableDeclaration(incrementor, undefined, ts.createNumericLiteral('0'))], - ts.NodeFlags.Let); - const loopCondition = ts.createBinary( - incrementor, ts.SyntaxKind.LessThanToken, - ts.createPropertyAccess(rootNodes, ts.createIdentifier('length'))); - const insertBeforeCall = ts.createCall( - ts.createPropertyAccess(renderer, 'insertBefore'), [], - [parent, ts.createElementAccess(rootNodes, incrementor), nextSibling]); - const loop = ts.createFor( - loopInitializer, loopCondition, ts.createPostfix(incrementor, ts.SyntaxKind.PlusPlusToken), - ts.createBlock([ts.createExpressionStatement(insertBeforeCall)])); - - return ts.createFunctionDeclaration( - [], [], undefined, HelperFunction.attachViewAfter, [], - [renderer, node, rootNodes].map(name => getAnyTypedParameter(name)), undefined, - ts.createBlock([parentVariableStatement, nextSiblingVariableStatement, loop], true)); -} - -/** Creates a helper for `setElementAttribute` */ -function getSetElementAttributeHelper(): ts.FunctionDeclaration { - const renderer = ts.createIdentifier('renderer'); - const element = ts.createIdentifier('element'); - const namespaceAndName = ts.createIdentifier('namespaceAndName'); - const value = ts.createIdentifier('value'); - const name = ts.createIdentifier('name'); - const namespace = ts.createIdentifier('namespace'); - - // [namespace, name] = splitNamespace(namespaceAndName); - const namespaceAndNameVariable = ts.createVariableDeclaration( - ts.createArrayBindingPattern( - [namespace, name].map(id => ts.createBindingElement(undefined, undefined, id))), - undefined, - ts.createCall(ts.createIdentifier(HelperFunction.splitNamespace), [], [namespaceAndName])); - - // renderer.setAttribute(element, name, value, namespace); - const setCall = ts.createCall( - ts.createPropertyAccess(renderer, 'setAttribute'), [], [element, name, value, namespace]); - - // renderer.removeAttribute(element, name, namespace); - const removeCall = ts.createCall( - ts.createPropertyAccess(renderer, 'removeAttribute'), [], [element, name, namespace]); - - // if (value != null) { setCall() } else { removeCall } - const ifStatement = ts.createIf( - ts.createBinary(value, ts.SyntaxKind.ExclamationEqualsToken, ts.createNull()), - ts.createBlock([ts.createExpressionStatement(setCall)], true), - ts.createBlock([ts.createExpressionStatement(removeCall)], true)); - - const functionBody = ts.createBlock( - [ - ts.createVariableStatement( - undefined, - ts.createVariableDeclarationList([namespaceAndNameVariable], ts.NodeFlags.Const)), - ifStatement - ], - true); - - return ts.createFunctionDeclaration( - [], [], undefined, HelperFunction.setElementAttribute, [], - [ - getAnyTypedParameter(renderer), getAnyTypedParameter(element), - getAnyTypedParameter(namespaceAndName), getAnyTypedParameter(value, false) - ], - undefined, functionBody); -} - -/** Creates a helper for splitting a name that might contain a namespace. */ -function getSplitNamespaceHelper(): ts.FunctionDeclaration { - const name = ts.createIdentifier('name'); - const match = ts.createIdentifier('match'); - const regex = ts.createRegularExpressionLiteral('/^:([^:]+):(.+)$/'); - const matchCall = ts.createCall(ts.createPropertyAccess(name, 'match'), [], [regex]); - - // const match = name.split(regex); - const matchVariable = ts.createVariableDeclarationList( - [ts.createVariableDeclaration(match, undefined, matchCall)], ts.NodeFlags.Const); - - // return [match[1], match[2]]; - const matchReturn = ts.createReturn( - ts.createArrayLiteral([ts.createElementAccess(match, 1), ts.createElementAccess(match, 2)])); - - // if (name[0] === ':') { const match = ...; return ...; } - const ifStatement = ts.createIf( - ts.createBinary( - ts.createElementAccess(name, 0), ts.SyntaxKind.EqualsEqualsEqualsToken, - ts.createStringLiteral(':')), - ts.createBlock([ts.createVariableStatement([], matchVariable), matchReturn], true)); - - // return ['', name]; - const elseReturn = ts.createReturn(ts.createArrayLiteral([ts.createStringLiteral(''), name])); - - return ts.createFunctionDeclaration( - [], [], undefined, HelperFunction.splitNamespace, [], [getAnyTypedParameter(name)], undefined, - ts.createBlock([ifStatement, elseReturn], true)); -} diff --git a/packages/core/schematics/migrations/renderer-to-renderer2/index.ts b/packages/core/schematics/migrations/renderer-to-renderer2/index.ts deleted file mode 100644 index af563f68330..00000000000 --- a/packages/core/schematics/migrations/renderer-to-renderer2/index.ts +++ /dev/null @@ -1,140 +0,0 @@ -/** - * @license - * Copyright Google LLC 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, SchematicsException, Tree} from '@angular-devkit/schematics'; -import {basename, join, relative} from 'path'; -import ts from 'typescript'; - -import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; -import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host'; -import {getImportSpecifier, replaceImport} from '../../utils/typescript/imports'; -import {closestNode} from '../../utils/typescript/nodes'; - -import {getHelper, HelperFunction} from './helpers'; -import {migrateExpression} from './migration'; -import {findRendererReferences} from './util'; - -const MODULE_AUGMENTATION_FILENAME = 'ɵɵRENDERER_MIGRATION_CORE_AUGMENTATION.d.ts'; - -/** - * Migration that switches from `Renderer` to `Renderer2`. More information on how it works: - * https://hackmd.angular.io/UTzUZTnPRA-cSa_4mHyfYw - */ -export default function(): Rule { - return async (tree: Tree) => { - const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); - const basePath = process.cwd(); - const allPaths = [...buildPaths, ...testPaths]; - - if (!allPaths.length) { - throw new SchematicsException( - 'Could not find any tsconfig file. Cannot migrate Renderer usages to Renderer2.'); - } - - for (const tsconfigPath of allPaths) { - runRendererToRenderer2Migration(tree, tsconfigPath, basePath); - } - }; -} - -function runRendererToRenderer2Migration(tree: Tree, tsconfigPath: string, basePath: string) { - // Technically we can get away with using `MODULE_AUGMENTATION_FILENAME` as the path, but as of - // TS 4.2, the module resolution caching seems to be more aggressive which causes the file to be - // retained between test runs. We can avoid it by using the full path. - const augmentedFilePath = join(basePath, MODULE_AUGMENTATION_FILENAME); - const {program} = createMigrationProgram(tree, tsconfigPath, basePath, fileName => { - // In case the module augmentation file has been requested, we return a source file that - // augments "@angular/core" to include a named export called "Renderer". This ensures that - // we can rely on the type checker for this migration in v9 where "Renderer" has been removed. - if (basename(fileName) === MODULE_AUGMENTATION_FILENAME) { - return ` - import '@angular/core'; - declare module "@angular/core" { - class Renderer {} - } - `; - } - return undefined; - }, [augmentedFilePath]); - const typeChecker = program.getTypeChecker(); - const printer = ts.createPrinter(); - const sourceFiles = - program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program)); - - sourceFiles.forEach(sourceFile => { - const rendererImportSpecifier = getImportSpecifier(sourceFile, '@angular/core', 'Renderer'); - const rendererImport = rendererImportSpecifier ? - closestNode(rendererImportSpecifier, ts.SyntaxKind.NamedImports) : - null; - - // If there are no imports for the `Renderer`, we can exit early. - if (!rendererImportSpecifier || !rendererImport) { - return; - } - - const {typedNodes, methodCalls, forwardRefs} = - findRendererReferences(sourceFile, typeChecker, rendererImportSpecifier); - const update = tree.beginUpdate(relative(basePath, sourceFile.fileName)); - const helpersToAdd = new Set(); - - // Change the `Renderer` import to `Renderer2`. - update.remove(rendererImport.getStart(), rendererImport.getWidth()); - update.insertRight( - rendererImport.getStart(), - printer.printNode( - ts.EmitHint.Unspecified, replaceImport(rendererImport, 'Renderer', 'Renderer2'), - sourceFile)); - - // Change the method parameter and property types to `Renderer2`. - typedNodes.forEach(node => { - const type = node.type; - - if (type) { - update.remove(type.getStart(), type.getWidth()); - update.insertRight(type.getStart(), 'Renderer2'); - } - }); - - // Change all identifiers inside `forwardRef` referring to the `Renderer`. - forwardRefs.forEach(identifier => { - update.remove(identifier.getStart(), identifier.getWidth()); - update.insertRight(identifier.getStart(), 'Renderer2'); - }); - - // Migrate all of the method calls. - methodCalls.forEach(call => { - const {node, requiredHelpers} = migrateExpression(call, typeChecker); - - if (node) { - // If we migrated the node to a new expression, replace only the call expression. - update.remove(call.getStart(), call.getWidth()); - update.insertRight( - call.getStart(), printer.printNode(ts.EmitHint.Unspecified, node, sourceFile)); - } else if (call.parent && ts.isExpressionStatement(call.parent)) { - // Otherwise if the call is inside an expression statement, drop the entire statement. - // This takes care of any trailing semicolons. We only need to drop nodes for cases like - // `setBindingDebugInfo` which have been noop for a while so they can be removed safely. - update.remove(call.parent.getStart(), call.parent.getWidth()); - } - - if (requiredHelpers) { - requiredHelpers.forEach(helperName => helpersToAdd.add(helperName)); - } - }); - - // Some of the methods can't be mapped directly to `Renderer2` and need extra logic around them. - // The safest way to do so is to declare helper functions similar to the ones emitted by TS - // which encapsulate the extra "glue" logic. We should only emit these functions once per file. - helpersToAdd.forEach(helperName => { - update.insertLeft( - sourceFile.endOfFileToken.getStart(), getHelper(helperName, sourceFile, printer)); - }); - - tree.commitUpdate(update); - }); -} diff --git a/packages/core/schematics/migrations/renderer-to-renderer2/migration.ts b/packages/core/schematics/migrations/renderer-to-renderer2/migration.ts deleted file mode 100644 index 0922d6e1aaa..00000000000 --- a/packages/core/schematics/migrations/renderer-to-renderer2/migration.ts +++ /dev/null @@ -1,244 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; - -import {HelperFunction} from './helpers'; - -/** A call expression that is based on a property access. */ -type PropertyAccessCallExpression = ts.CallExpression&{expression: ts.PropertyAccessExpression}; - -/** - * Migrates a function call expression from `Renderer` to `Renderer2`. - * Returns null if the expression should be dropped. - */ -export function migrateExpression(node: ts.CallExpression, typeChecker: ts.TypeChecker): - {node: ts.Node|null, requiredHelpers?: HelperFunction[]} { - if (isPropertyAccessCallExpression(node)) { - switch (node.expression.name.getText()) { - case 'setElementProperty': - return {node: renameMethodCall(node, 'setProperty')}; - case 'setText': - return {node: renameMethodCall(node, 'setValue')}; - case 'listenGlobal': - return {node: renameMethodCall(node, 'listen')}; - case 'selectRootElement': - return {node: migrateSelectRootElement(node)}; - case 'setElementClass': - return {node: migrateSetElementClass(node)}; - case 'setElementStyle': - return {node: migrateSetElementStyle(node, typeChecker)}; - case 'invokeElementMethod': - return {node: migrateInvokeElementMethod(node)}; - case 'setBindingDebugInfo': - return {node: null}; - case 'createViewRoot': - return {node: migrateCreateViewRoot(node)}; - case 'setElementAttribute': - return { - node: switchToHelperCall(node, HelperFunction.setElementAttribute, node.arguments), - requiredHelpers: [ - HelperFunction.any, HelperFunction.splitNamespace, HelperFunction.setElementAttribute - ] - }; - case 'createElement': - return { - node: switchToHelperCall(node, HelperFunction.createElement, node.arguments.slice(0, 2)), - requiredHelpers: - [HelperFunction.any, HelperFunction.splitNamespace, HelperFunction.createElement] - }; - case 'createText': - return { - node: switchToHelperCall(node, HelperFunction.createText, node.arguments.slice(0, 2)), - requiredHelpers: [HelperFunction.any, HelperFunction.createText] - }; - case 'createTemplateAnchor': - return { - node: switchToHelperCall( - node, HelperFunction.createTemplateAnchor, node.arguments.slice(0, 1)), - requiredHelpers: [HelperFunction.any, HelperFunction.createTemplateAnchor] - }; - case 'projectNodes': - return { - node: switchToHelperCall(node, HelperFunction.projectNodes, node.arguments), - requiredHelpers: [HelperFunction.any, HelperFunction.projectNodes] - }; - case 'animate': - return { - node: migrateAnimateCall(), - requiredHelpers: [HelperFunction.any, HelperFunction.animate] - }; - case 'destroyView': - return { - node: switchToHelperCall(node, HelperFunction.destroyView, [node.arguments[1]]), - requiredHelpers: [HelperFunction.any, HelperFunction.destroyView] - }; - case 'detachView': - return { - node: switchToHelperCall(node, HelperFunction.detachView, [node.arguments[0]]), - requiredHelpers: [HelperFunction.any, HelperFunction.detachView] - }; - case 'attachViewAfter': - return { - node: switchToHelperCall(node, HelperFunction.attachViewAfter, node.arguments), - requiredHelpers: [HelperFunction.any, HelperFunction.attachViewAfter] - }; - } - } - - return {node}; -} - -/** Checks whether a node is a PropertyAccessExpression. */ -function isPropertyAccessCallExpression(node: ts.Node): node is PropertyAccessCallExpression { - return ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression); -} - -/** Renames a method call while keeping all of the parameters in place. */ -function renameMethodCall(node: PropertyAccessCallExpression, newName: string): ts.CallExpression { - const newExpression = ts.updatePropertyAccess( - node.expression, node.expression.expression, ts.createIdentifier(newName)); - - return ts.updateCall(node, newExpression, node.typeArguments, node.arguments); -} - -/** - * Migrates a `selectRootElement` call by removing the last argument which is no longer supported. - */ -function migrateSelectRootElement(node: ts.CallExpression): ts.Node { - // The only thing we need to do is to drop the last argument - // (`debugInfo`), if the consumer was passing it in. - if (node.arguments.length > 1) { - return ts.updateCall(node, node.expression, node.typeArguments, [node.arguments[0]]); - } - - return node; -} - -/** - * Migrates a call to `setElementClass` either to a call to `addClass` or `removeClass`, or - * to an expression like `isAdd ? addClass(el, className) : removeClass(el, className)`. - */ -function migrateSetElementClass(node: PropertyAccessCallExpression): ts.Node { - // Clone so we don't mutate by accident. Note that we assume that - // the user's code is providing all three required arguments. - const outputMethodArgs = node.arguments.slice(); - const isAddArgument = outputMethodArgs.pop()!; - const createRendererCall = (isAdd: boolean) => { - const innerExpression = node.expression.expression; - const topExpression = - ts.createPropertyAccess(innerExpression, isAdd ? 'addClass' : 'removeClass'); - return ts.createCall(topExpression, [], node.arguments.slice(0, 2)); - }; - - // If the call has the `isAdd` argument as a literal boolean, we can map it directly to - // `addClass` or `removeClass`. Note that we can't use the type checker here, because it - // won't tell us whether the value resolves to true or false. - if (isAddArgument.kind === ts.SyntaxKind.TrueKeyword || - isAddArgument.kind === ts.SyntaxKind.FalseKeyword) { - return createRendererCall(isAddArgument.kind === ts.SyntaxKind.TrueKeyword); - } - - // Otherwise create a ternary on the variable. - return ts.createConditional(isAddArgument, createRendererCall(true), createRendererCall(false)); -} - -/** - * Migrates a call to `setElementStyle` call either to a call to - * `setStyle` or `removeStyle`. or to an expression like - * `value == null ? removeStyle(el, key) : setStyle(el, key, value)`. - */ -function migrateSetElementStyle( - node: PropertyAccessCallExpression, typeChecker: ts.TypeChecker): ts.Node { - const args = node.arguments; - const addMethodName = 'setStyle'; - const removeMethodName = 'removeStyle'; - const lastArgType = args[2] ? - typeChecker.typeToString( - typeChecker.getTypeAtLocation(args[2]), node, ts.TypeFormatFlags.AddUndefined) : - null; - - // Note that for a literal null, TS considers it a `NullKeyword`, - // whereas a literal `undefined` is just an Identifier. - if (args.length === 2 || lastArgType === 'null' || lastArgType === 'undefined') { - // If we've got a call with two arguments, or one with three arguments where the last one is - // `undefined` or `null`, we can safely switch to a `removeStyle` call. - const innerExpression = node.expression.expression; - const topExpression = ts.createPropertyAccess(innerExpression, removeMethodName); - return ts.createCall(topExpression, [], args.slice(0, 2)); - } else if (args.length === 3) { - // We need the checks for string literals, because the type of something - // like `"blue"` is the literal `blue`, not `string`. - if (lastArgType === 'string' || lastArgType === 'number' || ts.isStringLiteral(args[2]) || - ts.isNoSubstitutionTemplateLiteral(args[2]) || ts.isNumericLiteral(args[2])) { - // If we've got three arguments and the last one is a string literal or a number, we - // can safely rename to `setStyle`. - return renameMethodCall(node, addMethodName); - } else { - // Otherwise migrate to a ternary that looks like: - // `value == null ? removeStyle(el, key) : setStyle(el, key, value)` - const condition = ts.createBinary(args[2], ts.SyntaxKind.EqualsEqualsToken, ts.createNull()); - const whenNullCall = renameMethodCall( - ts.createCall(node.expression, [], args.slice(0, 2)) as PropertyAccessCallExpression, - removeMethodName); - return ts.createConditional(condition, whenNullCall, renameMethodCall(node, addMethodName)); - } - } - - return node; -} - -/** - * Migrates a call to `invokeElementMethod(target, method, [arg1, arg2])` either to - * `target.method(arg1, arg2)` or `(target as any)[method].apply(target, [arg1, arg2])`. - */ -function migrateInvokeElementMethod(node: ts.CallExpression): ts.Node { - const [target, name, args] = node.arguments; - const isNameStatic = ts.isStringLiteral(name) || ts.isNoSubstitutionTemplateLiteral(name); - const isArgsStatic = !args || ts.isArrayLiteralExpression(args); - - if (isNameStatic && isArgsStatic) { - // If the name is a static string and the arguments are an array literal, - // we can safely convert the node into a call expression. - const expression = ts.createPropertyAccess( - target, (name as ts.StringLiteral | ts.NoSubstitutionTemplateLiteral).text); - const callArguments = args ? (args as ts.ArrayLiteralExpression).elements : []; - return ts.createCall(expression, [], callArguments); - } else { - // Otherwise create an expression in the form of `(target as any)[name].apply(target, args)`. - const asExpression = ts.createParen( - ts.createAsExpression(target, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword))); - const elementAccess = ts.createElementAccess(asExpression, name); - const applyExpression = ts.createPropertyAccess(elementAccess, 'apply'); - return ts.createCall(applyExpression, [], args ? [target, args] : [target]); - } -} - -/** Migrates a call to `createViewRoot` to whatever node was passed in as the first argument. */ -function migrateCreateViewRoot(node: ts.CallExpression): ts.Node { - return node.arguments[0]; -} - -/** Migrates a call to `migrate` a direct call to the helper. */ -function migrateAnimateCall() { - return ts.createCall(ts.createIdentifier(HelperFunction.animate), [], []); -} - -/** - * Switches out a call to the `Renderer` to a call to one of our helper functions. - * Most of the helpers accept an instance of `Renderer2` as the first argument and all - * subsequent arguments differ. - * @param node Node of the original method call. - * @param helper Name of the helper with which to replace the original call. - * @param args Arguments that should be passed into the helper after the renderer argument. - */ -function switchToHelperCall( - node: PropertyAccessCallExpression, helper: HelperFunction, - args: ts.Expression[]|ts.NodeArray): ts.Node { - return ts.createCall(ts.createIdentifier(helper), [], [node.expression.expression, ...args]); -} diff --git a/packages/core/schematics/migrations/renderer-to-renderer2/util.ts b/packages/core/schematics/migrations/renderer-to-renderer2/util.ts deleted file mode 100644 index 269cb537a42..00000000000 --- a/packages/core/schematics/migrations/renderer-to-renderer2/util.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; - -import {getImportSpecifier} from '../../utils/typescript/imports'; -import {isReferenceToImport} from '../../utils/typescript/symbol'; - -/** - * Finds typed nodes (e.g. function parameters or class properties) that are referencing the old - * `Renderer`, as well as calls to the `Renderer` methods. - */ -export function findRendererReferences( - sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker, - rendererImportSpecifier: ts.ImportSpecifier) { - const typedNodes = new Set(); - const methodCalls = new Set(); - const forwardRefs = new Set(); - const forwardRefSpecifier = getImportSpecifier(sourceFile, '@angular/core', 'forwardRef'); - - ts.forEachChild(sourceFile, function visitNode(node: ts.Node) { - if ((ts.isParameter(node) || ts.isPropertyDeclaration(node)) && - isReferenceToImport(typeChecker, node.name, rendererImportSpecifier)) { - typedNodes.add(node); - } else if ( - ts.isAsExpression(node) && - isReferenceToImport(typeChecker, node.type, rendererImportSpecifier)) { - typedNodes.add(node); - } else if (ts.isCallExpression(node)) { - if (ts.isPropertyAccessExpression(node.expression) && - isReferenceToImport(typeChecker, node.expression.expression, rendererImportSpecifier)) { - methodCalls.add(node); - } else if ( - // If we're dealing with a forwardRef that's returning a Renderer. - forwardRefSpecifier && ts.isIdentifier(node.expression) && - isReferenceToImport(typeChecker, node.expression, forwardRefSpecifier) && - node.arguments.length) { - const rendererIdentifier = - findRendererIdentifierInForwardRef(typeChecker, node, rendererImportSpecifier); - if (rendererIdentifier) { - forwardRefs.add(rendererIdentifier); - } - } - } - - ts.forEachChild(node, visitNode); - }); - - return {typedNodes, methodCalls, forwardRefs}; -} - -/** Finds the identifier referring to the `Renderer` inside a `forwardRef` call expression. */ -function findRendererIdentifierInForwardRef( - typeChecker: ts.TypeChecker, node: ts.CallExpression, - rendererImport: ts.ImportSpecifier|null): ts.Identifier|null { - const firstArg = node.arguments[0]; - - if (ts.isArrowFunction(firstArg) && rendererImport) { - // Check if the function is `forwardRef(() => Renderer)`. - if (ts.isIdentifier(firstArg.body) && - isReferenceToImport(typeChecker, firstArg.body, rendererImport)) { - return firstArg.body; - } else if (ts.isBlock(firstArg.body) && ts.isReturnStatement(firstArg.body.statements[0])) { - // Otherwise check if the expression is `forwardRef(() => { return Renderer })`. - const returnStatement = firstArg.body.statements[0] as ts.ReturnStatement; - - if (returnStatement.expression && ts.isIdentifier(returnStatement.expression) && - isReferenceToImport(typeChecker, returnStatement.expression, rendererImport)) { - return returnStatement.expression; - } - } - } - - return null; -} diff --git a/packages/core/schematics/migrations/router-preserve-query-params/BUILD.bazel b/packages/core/schematics/migrations/router-preserve-query-params/BUILD.bazel deleted file mode 100644 index 58d793e1a6c..00000000000 --- a/packages/core/schematics/migrations/router-preserve-query-params/BUILD.bazel +++ /dev/null @@ -1,18 +0,0 @@ -load("//tools:defaults.bzl", "ts_library") - -ts_library( - name = "router-preserve-query-params", - srcs = glob(["**/*.ts"]), - tsconfig = "//packages/core/schematics:tsconfig.json", - visibility = [ - "//packages/core/schematics:__pkg__", - "//packages/core/schematics/migrations/google3:__pkg__", - "//packages/core/schematics/test:__pkg__", - ], - deps = [ - "//packages/core/schematics/utils", - "@npm//@angular-devkit/schematics", - "@npm//@types/node", - "@npm//typescript", - ], -) diff --git a/packages/core/schematics/migrations/router-preserve-query-params/README.md b/packages/core/schematics/migrations/router-preserve-query-params/README.md deleted file mode 100644 index 77edc68558d..00000000000 --- a/packages/core/schematics/migrations/router-preserve-query-params/README.md +++ /dev/null @@ -1,35 +0,0 @@ -## Router's NavigationExtras.preserveQueryParams migration - -Previously the `NatigationExtras` property of `preserveQueryParams` defined what should be done with -query parameters on navigation. This migration updates the usages of `preserveQueryParams` to -instead use the `queryParamsHandling` property. - -#### Before -```ts -import { Component } from '@angular/core'; -import { Router } from '@angular/router'; - -@Component({}) -export class MyComponent { - constructor(private _router: Router) {} - - goHome() { - this._router.navigate('/', {preserveQueryParams: true, skipLocationChange: 'foo'}); - } -} -``` - -#### After -```ts -import { Component } from '@angular/core'; -import { Router } from '@angular/router'; - -@Component({}) -export class MyComponent { - constructor(private _router: Router) {} - - goHome() { - this._router.navigate('/', { queryParamsHandling: 'preserve', skipLocationChange: 'foo' }); - } -} -``` diff --git a/packages/core/schematics/migrations/router-preserve-query-params/index.ts b/packages/core/schematics/migrations/router-preserve-query-params/index.ts deleted file mode 100644 index 3562ab1b472..00000000000 --- a/packages/core/schematics/migrations/router-preserve-query-params/index.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * @license - * Copyright Google LLC 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, SchematicsException, Tree} from '@angular-devkit/schematics'; -import {relative} from 'path'; -import ts from 'typescript'; - -import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; -import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host'; -import {findLiteralsToMigrate, migrateLiteral} from './util'; - - -/** - * Migration that switches `NavigationExtras.preserveQueryParams` to set the coresponding value via - * `NavigationExtras`'s `queryParamsHandling` attribute. - */ -export default function(): Rule { - return async (tree: Tree) => { - const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); - const basePath = process.cwd(); - const allPaths = [...buildPaths, ...testPaths]; - - if (!allPaths.length) { - throw new SchematicsException( - 'Could not find any tsconfig file. Cannot migrate ' + - 'NavigationExtras.preserveQueryParams usages.'); - } - - for (const tsconfigPath of allPaths) { - runPreserveQueryParamsMigration(tree, tsconfigPath, basePath); - } - }; -} - -function runPreserveQueryParamsMigration(tree: Tree, tsconfigPath: string, basePath: string) { - const {program} = createMigrationProgram(tree, tsconfigPath, basePath); - const typeChecker = program.getTypeChecker(); - const printer = ts.createPrinter(); - const sourceFiles = - program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program)); - - sourceFiles.forEach(sourceFile => { - const literalsToMigrate = findLiteralsToMigrate(sourceFile, typeChecker); - const update = tree.beginUpdate(relative(basePath, sourceFile.fileName)); - - literalsToMigrate.forEach((instances, methodName) => instances.forEach(instance => { - const migratedNode = migrateLiteral(methodName, instance); - - if (migratedNode !== instance) { - update.remove(instance.getStart(), instance.getWidth()); - update.insertRight( - instance.getStart(), - printer.printNode(ts.EmitHint.Unspecified, migratedNode, sourceFile)); - } - })); - - tree.commitUpdate(update); - }); -} diff --git a/packages/core/schematics/migrations/router-preserve-query-params/util.ts b/packages/core/schematics/migrations/router-preserve-query-params/util.ts deleted file mode 100644 index f8d92894bbb..00000000000 --- a/packages/core/schematics/migrations/router-preserve-query-params/util.ts +++ /dev/null @@ -1,105 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; - -import {getImportSpecifier} from '../../utils/typescript/imports'; -import {isReferenceToImport} from '../../utils/typescript/symbol'; - -/** - * Configures the methods that the migration should be looking for - * and the properties from `NavigationExtras` that should be preserved. - */ -const methodConfig = new Set(['navigate', 'createUrlTree']); - -const preserveQueryParamsKey = 'preserveQueryParams'; - -export function migrateLiteral( - methodName: string, node: ts.ObjectLiteralExpression): ts.ObjectLiteralExpression { - const isMigratableMethod = methodConfig.has(methodName); - - if (!isMigratableMethod) { - throw Error(`Attempting to migrate unconfigured method called ${methodName}.`); - } - - - const propertiesToKeep: ts.ObjectLiteralElementLike[] = []; - let propertyToMigrate: ts.PropertyAssignment|ts.ShorthandPropertyAssignment|undefined = undefined; - - for (const property of node.properties) { - // Only look for regular and shorthand property assignments since resolving things - // like spread operators becomes too complicated for this migration. - if ((ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) && - (ts.isStringLiteralLike(property.name) || ts.isNumericLiteral(property.name) || - ts.isIdentifier(property.name)) && - (property.name.text === preserveQueryParamsKey)) { - propertyToMigrate = property; - continue; - } - propertiesToKeep.push(property); - } - - // Don't modify the node if there's nothing to migrate. - if (propertyToMigrate === undefined) { - return node; - } - - if ((ts.isShorthandPropertyAssignment(propertyToMigrate) && - propertyToMigrate.objectAssignmentInitializer?.kind === ts.SyntaxKind.TrueKeyword) || - (ts.isPropertyAssignment(propertyToMigrate) && - propertyToMigrate.initializer.kind === ts.SyntaxKind.TrueKeyword)) { - return ts.updateObjectLiteral( - node, - propertiesToKeep.concat( - ts.createPropertyAssignment('queryParamsHandling', ts.createIdentifier(`'preserve'`)))); - } - - return ts.updateObjectLiteral(node, propertiesToKeep); -} - -export function findLiteralsToMigrate(sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker) { - const results = new Map>( - Array.from(methodConfig.keys(), key => [key, new Set()])); - const routerImport = getImportSpecifier(sourceFile, '@angular/router', 'Router'); - const seenLiterals = new Map(); - - if (routerImport) { - sourceFile.forEachChild(function visitNode(node: ts.Node) { - // Look for calls that look like `foo.` with more than one parameter. - if (ts.isCallExpression(node) && node.arguments.length > 1 && - ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.name) && - methodConfig.has(node.expression.name.text)) { - // Check whether the type of the object on which the - // function is called refers to the Router import. - if (isReferenceToImport(typeChecker, node.expression.expression, routerImport)) { - const methodName = node.expression.name.text; - const parameterDeclaration = - typeChecker.getTypeAtLocation(node.arguments[1]).getSymbol()?.valueDeclaration; - - // Find the source of the object literal. - if (parameterDeclaration && ts.isObjectLiteralExpression(parameterDeclaration)) { - if (!seenLiterals.has(parameterDeclaration)) { - results.get(methodName)!.add(parameterDeclaration); - seenLiterals.set(parameterDeclaration, methodName); - // If the same literal has been passed into multiple different methods, we can't - // migrate it, because the supported properties are different. When we detect such - // a case, we drop it from the results so that it gets ignored. If it's used multiple - // times for the same method, it can still be migrated. - } else if (seenLiterals.get(parameterDeclaration) !== methodName) { - results.forEach(literals => literals.delete(parameterDeclaration)); - } - } - } - } else { - node.forEachChild(visitNode); - } - }); - } - - return results; -} diff --git a/packages/core/schematics/migrations/static-queries/BUILD.bazel b/packages/core/schematics/migrations/static-queries/BUILD.bazel deleted file mode 100644 index 528d322366e..00000000000 --- a/packages/core/schematics/migrations/static-queries/BUILD.bazel +++ /dev/null @@ -1,22 +0,0 @@ -load("//tools:defaults.bzl", "ts_library") - -ts_library( - name = "static-queries", - srcs = glob(["**/*.ts"]), - tsconfig = "//packages/core/schematics:tsconfig.json", - visibility = [ - "//packages/core/schematics:__pkg__", - "//packages/core/schematics/migrations/google3:__pkg__", - "//packages/core/schematics/test:__pkg__", - ], - deps = [ - "//packages/compiler", - "//packages/compiler-cli", - "//packages/core/schematics/utils", - "@npm//@angular-devkit/core", - "@npm//@angular-devkit/schematics", - "@npm//@types/node", - "@npm//rxjs", - "@npm//typescript", - ], -) diff --git a/packages/core/schematics/migrations/static-queries/angular/directive_inputs.ts b/packages/core/schematics/migrations/static-queries/angular/directive_inputs.ts deleted file mode 100644 index c5ef338f938..00000000000 --- a/packages/core/schematics/migrations/static-queries/angular/directive_inputs.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; -import {getAngularDecorators} from '../../../utils/ng_decorators'; -import {getPropertyNameText, hasPropertyNameText} from '../../../utils/typescript/property_name'; - -/** Analyzes the given class and resolves the name of all inputs which are declared. */ -export function getInputNamesOfClass( - node: ts.ClassDeclaration, typeChecker: ts.TypeChecker): string[] { - const resolvedInputSetters: string[] = []; - - // Determines the names of all inputs defined in the current class declaration by - // checking whether a given property/getter/setter has the "@Input" decorator applied. - node.members.forEach(m => { - if (!m.decorators || !m.decorators.length || - !ts.isPropertyDeclaration(m) && !ts.isSetAccessor(m) && !ts.isGetAccessor(m)) { - return; - } - - const inputDecorator = - getAngularDecorators(typeChecker, m.decorators!).find(d => d.name === 'Input'); - - if (inputDecorator && hasPropertyNameText(m.name)) { - resolvedInputSetters.push(m.name.text); - } - }); - - // Besides looking for immediate setters in the current class declaration, developers - // can also define inputs in the directive metadata using the "inputs" property. We - // also need to determine these inputs which are declared in the directive metadata. - const metadataInputs = getInputNamesFromMetadata(node, typeChecker); - - if (metadataInputs) { - resolvedInputSetters.push(...metadataInputs); - } - - return resolvedInputSetters; -} - -/** - * Determines the names of all inputs declared in the directive/component metadata - * of the given class. - */ -function getInputNamesFromMetadata( - node: ts.ClassDeclaration, typeChecker: ts.TypeChecker): string[]|null { - if (!node.decorators || !node.decorators.length) { - return null; - } - - const decorator = getAngularDecorators(typeChecker, node.decorators) - .find(d => d.name === 'Directive' || d.name === 'Component'); - - // In case no directive/component decorator could be found for this class, just - // return null as there is no metadata where an input could be declared. - if (!decorator) { - return null; - } - - const decoratorCall = decorator.node.expression; - - // In case the decorator does define any metadata, there is no metadata - // where inputs could be declared. This is an edge case because there - // always needs to be an object literal, but in case there isn't we just - // want to skip the invalid decorator and return null. - if (decoratorCall.arguments.length !== 1 || - !ts.isObjectLiteralExpression(decoratorCall.arguments[0])) { - return null; - } - - const metadata = decoratorCall.arguments[0] as ts.ObjectLiteralExpression; - const inputs = metadata.properties.filter(ts.isPropertyAssignment) - .find(p => getPropertyNameText(p.name) === 'inputs'); - - // In case there is no "inputs" property in the directive metadata, - // just return "null" as no inputs can be declared for this class. - if (!inputs || !ts.isArrayLiteralExpression(inputs.initializer)) { - return null; - } - - return inputs.initializer.elements.filter(ts.isStringLiteralLike) - .map(element => element.text.split(':')[0].trim()); -} diff --git a/packages/core/schematics/migrations/static-queries/angular/ng_query_visitor.ts b/packages/core/schematics/migrations/static-queries/angular/ng_query_visitor.ts deleted file mode 100644 index 5328d438f54..00000000000 --- a/packages/core/schematics/migrations/static-queries/angular/ng_query_visitor.ts +++ /dev/null @@ -1,158 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; - -import {ResolvedTemplate} from '../../../utils/ng_component_template'; -import {getAngularDecorators} from '../../../utils/ng_decorators'; -import {findParentClassDeclaration, getBaseTypeIdentifiers} from '../../../utils/typescript/class_declaration'; -import {getPropertyNameText} from '../../../utils/typescript/property_name'; - -import {getInputNamesOfClass} from './directive_inputs'; -import {NgQueryDefinition, QueryType} from './query-definition'; - - -/** Resolved metadata of a given class. */ -export interface ClassMetadata { - /** List of class declarations that derive from the given class. */ - derivedClasses: ts.ClassDeclaration[]; - /** Super class of the given class. */ - superClass: ts.ClassDeclaration|null; - /** List of property names that declare an Angular input within the given class. */ - ngInputNames: string[]; - /** Component template that belongs to that class if present. */ - template?: ResolvedTemplate; -} - -/** Type that describes a map which can be used to get a class declaration's metadata. */ -export type ClassMetadataMap = Map; - -/** - * Visitor that can be used to determine Angular queries within given TypeScript nodes. - * Besides resolving queries, the visitor also records class relations and searches for - * Angular input setters which can be used to analyze the timing usage of a given query. - */ -export class NgQueryResolveVisitor { - /** Resolved Angular query definitions. */ - resolvedQueries = new Map(); - - /** Maps a class declaration to its class metadata. */ - classMetadata: ClassMetadataMap = new Map(); - - constructor(public typeChecker: ts.TypeChecker) {} - - visitNode(node: ts.Node) { - switch (node.kind) { - case ts.SyntaxKind.PropertyDeclaration: - this.visitPropertyDeclaration(node as ts.PropertyDeclaration); - break; - case ts.SyntaxKind.ClassDeclaration: - this.visitClassDeclaration(node as ts.ClassDeclaration); - break; - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - this.visitAccessorDeclaration(node as ts.AccessorDeclaration); - break; - } - - ts.forEachChild(node, n => this.visitNode(n)); - } - - private visitPropertyDeclaration(node: ts.PropertyDeclaration) { - this._recordQueryDeclaration(node, node, getPropertyNameText(node.name)); - } - - private visitAccessorDeclaration(node: ts.AccessorDeclaration) { - this._recordQueryDeclaration(node, null, getPropertyNameText(node.name)); - } - - private visitClassDeclaration(node: ts.ClassDeclaration) { - this._recordClassInputSetters(node); - this._recordClassInheritances(node); - } - - private _recordQueryDeclaration( - node: ts.Node, property: ts.PropertyDeclaration|null, queryName: string|null) { - if (!node.decorators || !node.decorators.length) { - return; - } - - const ngDecorators = getAngularDecorators(this.typeChecker, node.decorators); - const queryDecorator = - ngDecorators.find(({name}) => name === 'ViewChild' || name === 'ContentChild'); - - // Ensure that the current property declaration is defining a query. - if (!queryDecorator) { - return; - } - - const queryContainer = findParentClassDeclaration(node); - - // If the query is not located within a class declaration, skip this node. - if (!queryContainer) { - return; - } - - const sourceFile = node.getSourceFile(); - const newQueries = this.resolvedQueries.get(sourceFile) || []; - - this.resolvedQueries.set(sourceFile, newQueries.concat({ - name: queryName, - type: queryDecorator.name === 'ViewChild' ? QueryType.ViewChild : QueryType.ContentChild, - node, - property, - decorator: queryDecorator, - container: queryContainer, - })); - } - - private _recordClassInputSetters(node: ts.ClassDeclaration) { - const resolvedInputNames = getInputNamesOfClass(node, this.typeChecker); - - if (resolvedInputNames) { - const classMetadata = this._getClassMetadata(node); - - classMetadata.ngInputNames = resolvedInputNames; - this.classMetadata.set(node, classMetadata); - } - } - - private _recordClassInheritances(node: ts.ClassDeclaration) { - const baseTypes = getBaseTypeIdentifiers(node); - - if (!baseTypes || baseTypes.length !== 1) { - return; - } - - const superClass = baseTypes[0]; - const baseClassMetadata = this._getClassMetadata(node); - - // We need to resolve the value declaration through the resolved type as the base - // class could be declared in different source files and the local symbol won't - // contain a value declaration as the value is not declared locally. - const symbol = this.typeChecker.getTypeAtLocation(superClass).getSymbol(); - - if (symbol && symbol.valueDeclaration && ts.isClassDeclaration(symbol.valueDeclaration)) { - const extendedClass = symbol.valueDeclaration; - const classMetadataExtended = this._getClassMetadata(extendedClass); - - // Record all classes that derive from the given class. This makes it easy to - // determine all classes that could potentially use inherited queries statically. - classMetadataExtended.derivedClasses.push(node); - this.classMetadata.set(extendedClass, classMetadataExtended); - - // Record the super class of the current class. - baseClassMetadata.superClass = extendedClass; - this.classMetadata.set(node, baseClassMetadata); - } - } - - private _getClassMetadata(node: ts.ClassDeclaration): ClassMetadata { - return this.classMetadata.get(node) || {derivedClasses: [], superClass: null, ngInputNames: []}; - } -} diff --git a/packages/core/schematics/migrations/static-queries/angular/query-definition.ts b/packages/core/schematics/migrations/static-queries/angular/query-definition.ts deleted file mode 100644 index ac867762b06..00000000000 --- a/packages/core/schematics/migrations/static-queries/angular/query-definition.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; -import {NgDecorator} from '../../../utils/ng_decorators'; - -/** Timing of a given query. Either static or dynamic. */ -export enum QueryTiming { - STATIC, - DYNAMIC, -} - -/** Type of a given query. */ -export enum QueryType { - ViewChild, - ContentChild -} - -export interface NgQueryDefinition { - /** Name of the query. Set to "null" in case the query name is not statically analyzable. */ - name: string|null; - /** Type of the query definition. */ - type: QueryType; - /** Node that declares this query. */ - node: ts.Node; - /** - * Property declaration that refers to the query value. For accessors there - * is no property that is guaranteed to access the query value. - */ - property: ts.PropertyDeclaration|null; - /** Decorator that declares this as a query. */ - decorator: NgDecorator; - /** Class declaration that holds this query. */ - container: ts.ClassDeclaration; -} diff --git a/packages/core/schematics/migrations/static-queries/angular/super_class.ts b/packages/core/schematics/migrations/static-queries/angular/super_class.ts deleted file mode 100644 index cc50c6389a2..00000000000 --- a/packages/core/schematics/migrations/static-queries/angular/super_class.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; -import {ClassMetadataMap} from './ng_query_visitor'; - -/** - * Gets all chained super-class TypeScript declarations for the given class - * by using the specified class metadata map. - */ -export function getSuperClassDeclarations( - classDecl: ts.ClassDeclaration, classMetadataMap: ClassMetadataMap) { - const declarations: ts.ClassDeclaration[] = []; - - let current = classMetadataMap.get(classDecl); - while (current && current.superClass) { - declarations.push(current.superClass); - current = classMetadataMap.get(current.superClass); - } - - return declarations; -} diff --git a/packages/core/schematics/migrations/static-queries/index.ts b/packages/core/schematics/migrations/static-queries/index.ts deleted file mode 100644 index bc058565b02..00000000000 --- a/packages/core/schematics/migrations/static-queries/index.ts +++ /dev/null @@ -1,274 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 {logging} from '@angular-devkit/core'; -import {Rule, SchematicContext, SchematicsException, Tree} from '@angular-devkit/schematics'; -import {relative} from 'path'; -import ts from 'typescript'; - -import {loadEsmModule} from '../../utils/load_esm'; -import {NgComponentTemplateVisitor} from '../../utils/ng_component_template'; -import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; -import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host'; - -import {NgQueryResolveVisitor} from './angular/ng_query_visitor'; -import {QueryTemplateStrategy} from './strategies/template_strategy/template_strategy'; -import {QueryTestStrategy} from './strategies/test_strategy/test_strategy'; -import {TimingStrategy} from './strategies/timing-strategy'; -import {QueryUsageStrategy} from './strategies/usage_strategy/usage_strategy'; -import {getTransformedQueryCallExpr} from './transform'; - -enum SELECTED_STRATEGY { - TEMPLATE, - USAGE, - TESTS, -} - -interface AnalyzedProject { - program: ts.Program; - host: ts.CompilerHost; - queryVisitor: NgQueryResolveVisitor; - sourceFiles: ts.SourceFile[]; - basePath: string; - typeChecker: ts.TypeChecker; - tsconfigPath: string; -} - -/** Entry point for the V8 static-query migration. */ -export default function(): Rule { - return runMigration; -} - -/** Runs the V8 migration static-query migration for all determined TypeScript projects. */ -async function runMigration(tree: Tree, context: SchematicContext) { - const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); - const basePath = process.cwd(); - const logger = context.logger; - - if (!buildPaths.length && !testPaths.length) { - throw new SchematicsException( - 'Could not find any tsconfig file. Cannot migrate queries ' + - 'to add static flag.'); - } - - const analyzedFiles = new Set(); - const buildProjects = new Set(); - const failures = []; - const strategy = process.env['NG_STATIC_QUERY_USAGE_STRATEGY'] === 'true' ? - SELECTED_STRATEGY.USAGE : - SELECTED_STRATEGY.TEMPLATE; - - for (const tsconfigPath of buildPaths) { - const project = analyzeProject(tree, tsconfigPath, basePath, analyzedFiles, logger); - if (project) { - buildProjects.add(project); - } - } - - let compilerModule; - try { - // Load ESM `@angular/compiler` using the TypeScript dynamic import workaround. - // Once TypeScript provides support for keeping the dynamic import this workaround can be - // changed to a direct dynamic import. - compilerModule = await loadEsmModule('@angular/compiler'); - } catch (e) { - throw new SchematicsException( - `Unable to load the '@angular/compiler' package. Details: ${e.message}`); - } - - let compilerCliModule; - try { - // Load ESM `@angular/compiler-cli` using the TypeScript dynamic import workaround. - // Once TypeScript provides support for keeping the dynamic import this workaround can be - // changed to a direct dynamic import. - compilerCliModule = - await loadEsmModule('@angular/compiler-cli'); - } catch (e) { - throw new SchematicsException( - `Unable to load the '@angular/compiler' package. Details: ${e.message}`); - } - - if (buildProjects.size) { - for (let project of Array.from(buildProjects.values())) { - failures.push(...await runStaticQueryMigration( - tree, project, strategy, logger, compilerModule, compilerCliModule)); - } - } - - // For the "test" tsconfig projects we always want to use the test strategy as - // we can't detect the proper timing within spec files. - for (const tsconfigPath of testPaths) { - const project = await analyzeProject(tree, tsconfigPath, basePath, analyzedFiles, logger); - if (project) { - failures.push(...await runStaticQueryMigration( - tree, project, SELECTED_STRATEGY.TESTS, logger, compilerModule, compilerCliModule)); - } - } - - if (failures.length) { - logger.info(''); - logger.info('Some queries could not be migrated automatically. Please go'); - logger.info('through these manually and apply the appropriate timing.'); - logger.info('For more info on how to choose a flag, please see: '); - logger.info('https://v8.angular.io/guide/static-query-migration'); - failures.forEach(failure => logger.warn(`⮑ ${failure}`)); - } -} - -/** - * Analyzes the given TypeScript project by looking for queries that need to be - * migrated. In case there are no queries that can be migrated, null is returned. - */ -function analyzeProject( - tree: Tree, tsconfigPath: string, basePath: string, analyzedFiles: Set, - logger: logging.LoggerApi): AnalyzedProject|null { - const {program, host} = createMigrationProgram(tree, tsconfigPath, basePath); - const syntacticDiagnostics = program.getSyntacticDiagnostics(); - - // Syntactic TypeScript errors can throw off the query analysis and therefore we want - // to notify the developer that we couldn't analyze parts of the project. Developers - // can just re-run the migration after fixing these failures. - if (syntacticDiagnostics.length) { - logger.warn( - `\nTypeScript project "${tsconfigPath}" has syntactical errors which could cause ` + - `an incomplete migration. Please fix the following failures and rerun the migration:`); - logger.error(ts.formatDiagnostics(syntacticDiagnostics, host)); - logger.info( - 'Migration can be rerun with: "ng update @angular/core --from 7 --to 8 --migrate-only"\n'); - } - - const typeChecker = program.getTypeChecker(); - const sourceFiles = - program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program)); - const queryVisitor = new NgQueryResolveVisitor(typeChecker); - - // Analyze all project source-files and collect all queries that - // need to be migrated. - sourceFiles.forEach(sourceFile => { - const relativePath = relative(basePath, sourceFile.fileName); - - // Only look for queries within the current source files if the - // file has not been analyzed before. - if (!analyzedFiles.has(relativePath)) { - analyzedFiles.add(relativePath); - queryVisitor.visitNode(sourceFile); - } - }); - - if (queryVisitor.resolvedQueries.size === 0) { - return null; - } - - return {program, host, tsconfigPath, typeChecker, basePath, queryVisitor, sourceFiles}; -} - -/** - * Runs the static query migration for the given project. The schematic analyzes all - * queries within the project and sets up the query timing based on the current usage - * of the query property. e.g. a view query that is not used in any lifecycle hook does - * not need to be static and can be set up with "static: false". - */ -async function runStaticQueryMigration( - tree: Tree, project: AnalyzedProject, selectedStrategy: SELECTED_STRATEGY, - logger: logging.LoggerApi, compilerModule: typeof import('@angular/compiler'), - compilerCliModule: typeof import('@angular/compiler-cli')): Promise { - const {sourceFiles, typeChecker, host, queryVisitor, tsconfigPath, basePath} = project; - const printer = ts.createPrinter(); - const failureMessages: string[] = []; - const templateVisitor = new NgComponentTemplateVisitor(typeChecker); - - // If the "usage" strategy is selected, we also need to add the query visitor - // to the analysis visitors so that query usage in templates can be also checked. - if (selectedStrategy === SELECTED_STRATEGY.USAGE) { - sourceFiles.forEach(s => templateVisitor.visitNode(s)); - } - - const {resolvedQueries, classMetadata} = queryVisitor; - const {resolvedTemplates} = templateVisitor; - - if (selectedStrategy === SELECTED_STRATEGY.USAGE) { - // Add all resolved templates to the class metadata if the usage strategy is used. This - // is necessary in order to be able to check component templates for static query usage. - resolvedTemplates.forEach(template => { - if (classMetadata.has(template.container)) { - classMetadata.get(template.container)!.template = template; - } - }); - } - - let strategy: TimingStrategy; - if (selectedStrategy === SELECTED_STRATEGY.USAGE) { - strategy = new QueryUsageStrategy(classMetadata, typeChecker, compilerModule); - } else if (selectedStrategy === SELECTED_STRATEGY.TESTS) { - strategy = new QueryTestStrategy(); - } else { - strategy = new QueryTemplateStrategy( - tsconfigPath, classMetadata, host, compilerModule, compilerCliModule); - } - - try { - strategy.setup(); - } catch (e) { - if (selectedStrategy === SELECTED_STRATEGY.TEMPLATE) { - logger.warn( - `\nThe template migration strategy uses the Angular compiler ` + - `internally and therefore projects that no longer build successfully after ` + - `the update cannot use the template migration strategy. Please ensure ` + - `there are no AOT compilation errors.\n`); - } - // In case the strategy could not be set up properly, we just exit the - // migration. We don't want to throw an exception as this could mean - // that other migrations are interrupted. - logger.warn( - `Could not setup migration strategy for "${project.tsconfigPath}". The ` + - `following error has been reported:\n`); - logger.error(`${e.toString()}\n`); - logger.info( - 'Migration can be rerun with: "ng update @angular/core --from 7 --to 8 --migrate-only"\n'); - return []; - } - - // Walk through all source files that contain resolved queries and update - // the source files if needed. Note that we need to update multiple queries - // within a source file within the same recorder in order to not throw off - // the TypeScript node offsets. - resolvedQueries.forEach((queries, sourceFile) => { - const relativePath = relative(basePath, sourceFile.fileName); - const update = tree.beginUpdate(relativePath); - - // Compute the query timing for all resolved queries and update the - // query definitions to explicitly set the determined query timing. - queries.forEach(q => { - const queryExpr = q.decorator.node.expression; - const {timing, message} = strategy.detectTiming(q); - const result = getTransformedQueryCallExpr(q, timing, !!message); - - if (!result) { - return; - } - - const newText = printer.printNode(ts.EmitHint.Unspecified, result.node, sourceFile); - - // Replace the existing query decorator call expression with the updated - // call expression node. - update.remove(queryExpr.getStart(), queryExpr.getWidth()); - update.insertRight(queryExpr.getStart(), newText); - - if (result.failureMessage || message) { - const {line, character} = - ts.getLineAndCharacterOfPosition(sourceFile, q.decorator.node.getStart()); - failureMessages.push( - `${relativePath}@${line + 1}:${character + 1}: ${result.failureMessage || message}`); - } - }); - - tree.commitUpdate(update); - }); - - return failureMessages; -} diff --git a/packages/core/schematics/migrations/static-queries/strategies/template_strategy/template_strategy.ts b/packages/core/schematics/migrations/static-queries/strategies/template_strategy/template_strategy.ts deleted file mode 100644 index 054c37fa26f..00000000000 --- a/packages/core/schematics/migrations/static-queries/strategies/template_strategy/template_strategy.ts +++ /dev/null @@ -1,238 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 type {AotCompiler, CompileDirectiveMetadata, CompileMetadataResolver, CompileNgModuleMetadata, CompileStylesheetMetadata, NgAnalyzedModules, QueryMatch, StaticSymbol, TemplateAst} from '@angular/compiler'; -import {resolve} from 'path'; -import ts from 'typescript'; - -import {ClassMetadataMap} from '../../angular/ng_query_visitor'; -import {NgQueryDefinition, QueryTiming, QueryType} from '../../angular/query-definition'; -import {TimingResult, TimingStrategy} from '../timing-strategy'; - -const QUERY_NOT_DECLARED_IN_COMPONENT_MESSAGE = 'Timing could not be determined. This happens ' + - 'if the query is not declared in any component.'; - -export class QueryTemplateStrategy implements TimingStrategy { - private compiler: AotCompiler|null = null; - private metadataResolver: CompileMetadataResolver|null = null; - private analyzedQueries = new Map(); - - constructor( - private projectPath: string, private classMetadata: ClassMetadataMap, - private host: ts.CompilerHost, private compilerModule: typeof import('@angular/compiler'), - private compilerCliModule: typeof import('@angular/compiler-cli')) {} - - /** - * Sets up the template strategy by creating the AngularCompilerProgram. Returns false if - * the AOT compiler program could not be created due to failure diagnostics. - */ - setup() { - const {rootNames, options} = this.compilerCliModule.readConfiguration(this.projectPath); - - // https://github.com/angular/angular/commit/ec4381dd401f03bded652665b047b6b90f2b425f made Ivy - // the default. This breaks the assumption that "createProgram" from compiler-cli returns the - // NGC program. In order to ensure that the migration runs properly, we set "enableIvy" to - // false. - options.enableIvy = false; - - const aotProgram = this.compilerCliModule.createProgram({rootNames, options, host: this.host}); - - // The "AngularCompilerProgram" does not expose the "AotCompiler" instance, nor does it - // expose the logic that is necessary to analyze the determined modules. We work around - // this by just accessing the necessary private properties using the bracket notation. - this.compiler = (aotProgram as any)['compiler']; - this.metadataResolver = this.compiler!['_metadataResolver']; - - // Modify the "DirectiveNormalizer" to not normalize any referenced external stylesheets. - // This is necessary because in CLI projects preprocessor files are commonly referenced - // and we don't want to parse them in order to extract relative style references. This - // breaks the analysis of the project because we instantiate a standalone AOT compiler - // program which does not contain the custom logic by the Angular CLI Webpack compiler plugin. - const directiveNormalizer = this.metadataResolver!['_directiveNormalizer']; - directiveNormalizer['_normalizeStylesheet'] = (metadata: CompileStylesheetMetadata) => { - return new this.compilerModule.CompileStylesheetMetadata( - {styles: metadata.styles, styleUrls: [], moduleUrl: metadata.moduleUrl!}); - }; - - // Retrieves the analyzed modules of the current program. This data can be - // used to determine the timing for registered queries. - const analyzedModules = (aotProgram as any)['analyzedModules'] as NgAnalyzedModules; - - const ngStructuralDiagnostics = aotProgram.getNgStructuralDiagnostics(); - if (ngStructuralDiagnostics.length) { - throw new Error(this.compilerCliModule.formatDiagnostics(ngStructuralDiagnostics, this.host)); - } - - analyzedModules.files.forEach(file => { - file.directives.forEach(directive => this._analyzeDirective(directive, analyzedModules)); - }); - } - - /** Analyzes a given directive by determining the timing of all matched view queries. */ - private _analyzeDirective(symbol: StaticSymbol, analyzedModules: NgAnalyzedModules) { - const metadata = this.metadataResolver!.getDirectiveMetadata(symbol); - const ngModule = analyzedModules.ngModuleByPipeOrDirective.get(symbol); - - if (!metadata.isComponent || !ngModule) { - return; - } - - const parsedTemplate = this._parseTemplate(metadata, ngModule); - const queryTimingMap = this.findStaticQueryIds(parsedTemplate); - const {staticQueryIds} = staticViewQueryIds(queryTimingMap); - - metadata.viewQueries.forEach((query, index) => { - // Query ids are computed by adding "one" to the index. This is done within - // the "view_compiler.ts" in order to support using a bloom filter for queries. - const queryId = index + 1; - const queryKey = - this._getViewQueryUniqueKey(symbol.filePath, symbol.name, query.propertyName); - this.analyzedQueries.set( - queryKey, staticQueryIds.has(queryId) ? QueryTiming.STATIC : QueryTiming.DYNAMIC); - }); - } - - /** Detects the timing of the query definition. */ - detectTiming(query: NgQueryDefinition): TimingResult { - if (query.type === QueryType.ContentChild) { - return {timing: null, message: 'Content queries cannot be migrated automatically.'}; - } else if (!query.name) { - // In case the query property name is not statically analyzable, we mark this - // query as unresolved. NGC currently skips these view queries as well. - return {timing: null, message: 'Query is not statically analyzable.'}; - } - - const propertyName = query.name; - const classMetadata = this.classMetadata.get(query.container); - - // In case there is no class metadata or there are no derived classes that - // could access the current query, we just look for the query analysis of - // the class that declares the query. e.g. only the template of the class - // that declares the view query affects the query timing. - if (!classMetadata || !classMetadata.derivedClasses.length) { - const timing = this._getQueryTimingFromClass(query.container, propertyName); - - if (timing === null) { - return {timing: null, message: QUERY_NOT_DECLARED_IN_COMPONENT_MESSAGE}; - } - - return {timing}; - } - - let resolvedTiming: QueryTiming|null = null; - let timingMismatch = false; - - // In case there are multiple components that use the same query (e.g. through inheritance), - // we need to check if all components use the query with the same timing. If that is not - // the case, the query timing is ambiguous and the developer needs to fix the query manually. - [query.container, ...classMetadata.derivedClasses].forEach(classDecl => { - const classTiming = this._getQueryTimingFromClass(classDecl, propertyName); - - if (classTiming === null) { - return; - } - - // In case there is no resolved timing yet, save the new timing. Timings from other - // components that use the query with a different timing, cause the timing to be - // mismatched. In that case we can't detect a working timing for all components. - if (resolvedTiming === null) { - resolvedTiming = classTiming; - } else if (resolvedTiming !== classTiming) { - timingMismatch = true; - } - }); - - if (resolvedTiming === null) { - return {timing: QueryTiming.DYNAMIC, message: QUERY_NOT_DECLARED_IN_COMPONENT_MESSAGE}; - } else if (timingMismatch) { - return {timing: null, message: 'Multiple components use the query with different timings.'}; - } - return {timing: resolvedTiming}; - } - - /** - * Gets the timing that has been resolved for a given query when it's used within the - * specified class declaration. e.g. queries from an inherited class can be used. - */ - private _getQueryTimingFromClass(classDecl: ts.ClassDeclaration, queryName: string): QueryTiming - |null { - if (!classDecl.name) { - return null; - } - const filePath = classDecl.getSourceFile().fileName; - const queryKey = this._getViewQueryUniqueKey(filePath, classDecl.name.text, queryName); - - if (this.analyzedQueries.has(queryKey)) { - return this.analyzedQueries.get(queryKey)!; - } - return null; - } - - private _parseTemplate(component: CompileDirectiveMetadata, ngModule: CompileNgModuleMetadata): - TemplateAst[] { - return this - .compiler!['_parseTemplate'](component, ngModule, ngModule.transitiveModule.directives) - .template; - } - - private _getViewQueryUniqueKey(filePath: string, className: string, propName: string) { - return `${resolve(filePath)}#${className}-${propName}`; - } - - /** Figures out which queries are static and which ones are dynamic. */ - private findStaticQueryIds( - nodes: TemplateAst[], result = new Map()): - Map { - nodes.forEach((node) => { - const staticQueryIds = new Set(); - const dynamicQueryIds = new Set(); - let queryMatches: QueryMatch[] = undefined!; - if (node instanceof this.compilerModule.ElementAst) { - this.findStaticQueryIds(node.children, result); - node.children.forEach((child) => { - const childData = result.get(child)!; - childData.staticQueryIds.forEach(queryId => staticQueryIds.add(queryId)); - childData.dynamicQueryIds.forEach(queryId => dynamicQueryIds.add(queryId)); - }); - queryMatches = node.queryMatches; - } else if (node instanceof this.compilerModule.EmbeddedTemplateAst) { - this.findStaticQueryIds(node.children, result); - node.children.forEach((child) => { - const childData = result.get(child)!; - childData.staticQueryIds.forEach(queryId => dynamicQueryIds.add(queryId)); - childData.dynamicQueryIds.forEach(queryId => dynamicQueryIds.add(queryId)); - }); - queryMatches = node.queryMatches; - } - if (queryMatches) { - queryMatches.forEach((match) => staticQueryIds.add(match.queryId)); - } - dynamicQueryIds.forEach(queryId => staticQueryIds.delete(queryId)); - result.set(node, {staticQueryIds, dynamicQueryIds}); - }); - return result; - } -} - -interface StaticAndDynamicQueryIds { - staticQueryIds: Set; - dynamicQueryIds: Set; -} - -/** Splits queries into static and dynamic. */ -function staticViewQueryIds(nodeStaticQueryIds: Map): - StaticAndDynamicQueryIds { - const staticQueryIds = new Set(); - const dynamicQueryIds = new Set(); - Array.from(nodeStaticQueryIds.values()).forEach((entry) => { - entry.staticQueryIds.forEach(queryId => staticQueryIds.add(queryId)); - entry.dynamicQueryIds.forEach(queryId => dynamicQueryIds.add(queryId)); - }); - dynamicQueryIds.forEach(queryId => staticQueryIds.delete(queryId)); - return {staticQueryIds, dynamicQueryIds}; -} diff --git a/packages/core/schematics/migrations/static-queries/strategies/test_strategy/test_strategy.ts b/packages/core/schematics/migrations/static-queries/strategies/test_strategy/test_strategy.ts deleted file mode 100644 index 7f39f3bdee7..00000000000 --- a/packages/core/schematics/migrations/static-queries/strategies/test_strategy/test_strategy.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 {NgQueryDefinition} from '../../angular/query-definition'; -import {TimingResult, TimingStrategy} from '../timing-strategy'; - -/** - * Query timing strategy that is used for queries used within test files. The query - * timing is not analyzed for test files as the template strategy cannot work within - * spec files (due to missing component modules) and the usage strategy is not capable - * of detecting the timing of queries based on how they are used in tests. - */ -export class QueryTestStrategy implements TimingStrategy { - setup() {} - - /** - * Detects the timing for a given query. For queries within tests, we always - * add a TODO and print a message saying that the timing can't be detected for tests. - */ - detectTiming(query: NgQueryDefinition): TimingResult { - return {timing: null, message: 'Timing within tests cannot be detected.'}; - } -} diff --git a/packages/core/schematics/migrations/static-queries/strategies/timing-strategy.ts b/packages/core/schematics/migrations/static-queries/strategies/timing-strategy.ts deleted file mode 100644 index 5db65156dd9..00000000000 --- a/packages/core/schematics/migrations/static-queries/strategies/timing-strategy.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 {NgQueryDefinition, QueryTiming} from '../angular/query-definition'; - -export interface TimingStrategy { - /** Sets up the given strategy. Throws if the strategy could not be set up. */ - setup(): void; - /** Detects the timing result for a given query. */ - detectTiming(query: NgQueryDefinition): TimingResult; -} - -export type TimingResult = { - timing: QueryTiming|null; - message?: string; -}; diff --git a/packages/core/schematics/migrations/static-queries/strategies/usage_strategy/declaration_usage_visitor.ts b/packages/core/schematics/migrations/static-queries/strategies/usage_strategy/declaration_usage_visitor.ts deleted file mode 100644 index cda8ea8b388..00000000000 --- a/packages/core/schematics/migrations/static-queries/strategies/usage_strategy/declaration_usage_visitor.ts +++ /dev/null @@ -1,420 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; -import {isFunctionLikeDeclaration, unwrapExpression} from '../../../../utils/typescript/functions'; -import {getPropertyNameText} from '../../../../utils/typescript/property_name'; - -export type FunctionContext = Map; - -export enum ResolvedUsage { - SYNCHRONOUS, - ASYNCHRONOUS, - AMBIGUOUS, -} - -/** - * List of TypeScript syntax tokens that can be used within a binary expression as - * compound assignment. These imply a read and write of the left-side expression. - */ -const BINARY_COMPOUND_TOKENS = [ - ts.SyntaxKind.CaretEqualsToken, - ts.SyntaxKind.AsteriskEqualsToken, - ts.SyntaxKind.AmpersandEqualsToken, - ts.SyntaxKind.BarEqualsToken, - ts.SyntaxKind.AsteriskAsteriskEqualsToken, - ts.SyntaxKind.PlusEqualsToken, - ts.SyntaxKind.MinusEqualsToken, - ts.SyntaxKind.SlashEqualsToken, -]; - -/** - * List of known asynchronous external call expressions which aren't analyzable - * but are guaranteed to not execute the passed argument synchronously. - */ -const ASYNC_EXTERNAL_CALLS = [ - {parent: ['Promise'], name: 'then'}, - {parent: ['Promise'], name: 'catch'}, - {parent: [null, 'Window'], name: 'requestAnimationFrame'}, - {parent: [null, 'Window'], name: 'setTimeout'}, - {parent: [null, 'Window'], name: 'setInterval'}, - {parent: ['*'], name: 'addEventListener'}, -]; - -/** - * Class that can be used to determine if a given TypeScript node is used within - * other given TypeScript nodes. This is achieved by walking through all children - * of the given node and checking for usages of the given declaration. The visitor - * also handles potential control flow changes caused by call/new expressions. - */ -export class DeclarationUsageVisitor { - /** Set of visited symbols that caused a jump in control flow. */ - private visitedJumpExprNodes = new Set(); - - /** - * Queue of nodes that need to be checked for declaration usage and - * are guaranteed to be executed synchronously. - */ - private nodeQueue: ts.Node[] = []; - - /** - * Nodes which need to be checked for declaration usage but aren't - * guaranteed to execute synchronously. - */ - private ambiguousNodeQueue: ts.Node[] = []; - - /** - * Function context that holds the TypeScript node values for all parameters - * of the currently analyzed function block. - */ - private context: FunctionContext = new Map(); - - constructor( - private declaration: ts.Node, private typeChecker: ts.TypeChecker, - private baseContext: FunctionContext = new Map()) {} - - private isReferringToSymbol(node: ts.Node): boolean { - const symbol = this.typeChecker.getSymbolAtLocation(node); - return !!symbol && symbol.valueDeclaration === this.declaration; - } - - private addJumpExpressionToQueue(callExpression: ts.CallExpression) { - const node = unwrapExpression(callExpression.expression); - - // In case the given expression is already referring to a function-like declaration, - // we don't need to resolve the symbol of the expression as the jump expression is - // defined inline and we can just add the given node to the queue. - if (isFunctionLikeDeclaration(node) && node.body) { - this.nodeQueue.push(node.body); - return; - } - - const callExprSymbol = this._getDeclarationSymbolOfNode(node); - - if (!callExprSymbol || !callExprSymbol.valueDeclaration) { - this.peekIntoJumpExpression(callExpression); - return; - } - - const expressionDecl = this._resolveNodeFromContext(callExprSymbol.valueDeclaration); - - // Note that we should not add previously visited symbols to the queue as - // this could cause cycles. - if (!isFunctionLikeDeclaration(expressionDecl) || - this.visitedJumpExprNodes.has(expressionDecl) || !expressionDecl.body) { - this.peekIntoJumpExpression(callExpression); - return; - } - - // Update the context for the new jump expression and its specified arguments. - this._updateContext(callExpression.arguments, expressionDecl.parameters); - - this.visitedJumpExprNodes.add(expressionDecl); - this.nodeQueue.push(expressionDecl.body); - } - - private addNewExpressionToQueue(node: ts.NewExpression) { - const newExprSymbol = this._getDeclarationSymbolOfNode(unwrapExpression(node.expression)); - - // Only handle new expressions which resolve to classes. Technically "new" could - // also call void functions or objects with a constructor signature. Also note that - // we should not visit already visited symbols as this could cause cycles. - if (!newExprSymbol || !newExprSymbol.valueDeclaration || - !ts.isClassDeclaration(newExprSymbol.valueDeclaration)) { - this.peekIntoJumpExpression(node); - return; - } - - const targetConstructor = - newExprSymbol.valueDeclaration.members.find(ts.isConstructorDeclaration); - - if (targetConstructor && targetConstructor.body && - !this.visitedJumpExprNodes.has(targetConstructor)) { - // Update the context for the new expression and its specified constructor - // parameters if arguments are passed to the class constructor. - if (node.arguments) { - this._updateContext(node.arguments, targetConstructor.parameters); - } - - this.visitedJumpExprNodes.add(targetConstructor); - this.nodeQueue.push(targetConstructor.body); - } else { - this.peekIntoJumpExpression(node); - } - } - - private visitPropertyAccessors( - node: ts.PropertyAccessExpression, checkSetter: boolean, checkGetter: boolean) { - const propertySymbol = this._getPropertyAccessSymbol(node); - - if (propertySymbol?.declarations === undefined || propertySymbol.declarations.length === 0 || - (propertySymbol.getFlags() & ts.SymbolFlags.Accessor) === 0) { - return; - } - - // Since we checked the symbol flags and the symbol is describing an accessor, the - // declarations are guaranteed to only contain the getters and setters. - const accessors = propertySymbol.declarations as ts.AccessorDeclaration[]; - - accessors - .filter( - d => (checkSetter && ts.isSetAccessor(d) || checkGetter && ts.isGetAccessor(d)) && - d.body && !this.visitedJumpExprNodes.has(d)) - .forEach(d => { - this.visitedJumpExprNodes.add(d); - this.nodeQueue.push(d.body!); - }); - } - - private visitBinaryExpression(node: ts.BinaryExpression): boolean { - const leftExpr = unwrapExpression(node.left); - - if (!ts.isPropertyAccessExpression(leftExpr)) { - return false; - } - - if (BINARY_COMPOUND_TOKENS.indexOf(node.operatorToken.kind) !== -1) { - // Compound assignments always cause the getter and setter to be called. - // Therefore we need to check the setter and getter of the property access. - this.visitPropertyAccessors(leftExpr, /* setter */ true, /* getter */ true); - } else if (node.operatorToken.kind === ts.SyntaxKind.EqualsToken) { - // Value assignments using the equals token only cause the "setter" to be called. - // Therefore we need to analyze the setter declaration of the property access. - this.visitPropertyAccessors(leftExpr, /* setter */ true, /* getter */ false); - } else { - // If the binary expression is not an assignment, it's a simple property read and - // we need to check the getter declaration if present. - this.visitPropertyAccessors(leftExpr, /* setter */ false, /* getter */ true); - } - return true; - } - - getResolvedNodeUsage(searchNode: ts.Node): ResolvedUsage { - this.nodeQueue = [searchNode]; - this.visitedJumpExprNodes.clear(); - this.context.clear(); - - // Copy base context values into the current function block context. The - // base context is useful if nodes need to be mapped to other nodes. e.g. - // abstract super class methods are mapped to their implementation node of - // the derived class. - this.baseContext.forEach((value, key) => this.context.set(key, value)); - - return this.isSynchronouslyUsedInNode(searchNode); - } - - private isSynchronouslyUsedInNode(searchNode: ts.Node): ResolvedUsage { - this.ambiguousNodeQueue = []; - - while (this.nodeQueue.length) { - const node = this.nodeQueue.shift()!; - - if (ts.isIdentifier(node) && this.isReferringToSymbol(node)) { - return ResolvedUsage.SYNCHRONOUS; - } - - // Handle call expressions within TypeScript nodes that cause a jump in control - // flow. We resolve the call expression value declaration and add it to the node queue. - if (ts.isCallExpression(node)) { - this.addJumpExpressionToQueue(node); - } - - // Handle new expressions that cause a jump in control flow. We resolve the - // constructor declaration of the target class and add it to the node queue. - if (ts.isNewExpression(node)) { - this.addNewExpressionToQueue(node); - } - - // We also need to handle binary expressions where a value can be either assigned to - // the property, or a value is read from a property expression. Depending on the - // binary expression operator, setters or getters need to be analyzed. - if (ts.isBinaryExpression(node)) { - // In case the binary expression contained a property expression on the left side, we - // don't want to continue visiting this property expression on its own. This is necessary - // because visiting the expression on its own causes a loss of context. e.g. property - // access expressions *do not* always cause a value read (e.g. property assignments) - if (this.visitBinaryExpression(node)) { - this.nodeQueue.push(node.right); - continue; - } - } - - // Handle property access expressions. Property expressions which are part of binary - // expressions won't be added to the node queue, so these access expressions are - // guaranteed to be "read" accesses and we need to check the "getter" declaration. - if (ts.isPropertyAccessExpression(node)) { - this.visitPropertyAccessors(node, /* setter */ false, /* getter */ true); - } - - // Do not visit nodes that declare a block of statements but are not executed - // synchronously (e.g. function declarations). We only want to check TypeScript - // nodes which are synchronously executed in the control flow. - if (!isFunctionLikeDeclaration(node)) { - this.nodeQueue.push(...node.getChildren()); - } - } - - if (this.ambiguousNodeQueue.length) { - // Update the node queue to all stored ambiguous nodes. These nodes are not - // guaranteed to be executed and therefore in case of a synchronous usage - // within one of those nodes, the resolved usage is ambiguous. - this.nodeQueue = this.ambiguousNodeQueue; - const usage = this.isSynchronouslyUsedInNode(searchNode); - return usage === ResolvedUsage.SYNCHRONOUS ? ResolvedUsage.AMBIGUOUS : usage; - } - return ResolvedUsage.ASYNCHRONOUS; - } - - /** - * Peeks into the given jump expression by adding all function like declarations - * which are referenced in the jump expression arguments to the ambiguous node - * queue. These arguments could technically access the given declaration but it's - * not guaranteed that the jump expression is executed. In that case the resolved - * usage is ambiguous. - */ - private peekIntoJumpExpression(jumpExp: ts.CallExpression|ts.NewExpression) { - if (!jumpExp.arguments) { - return; - } - - // For some call expressions we don't want to add the arguments to the - // ambiguous node queue. e.g. "setTimeout" is not analyzable but is - // guaranteed to execute its argument asynchronously. We handle a subset - // of these call expressions by having a hardcoded list of some. - if (ts.isCallExpression(jumpExp)) { - const symbol = this._getDeclarationSymbolOfNode(jumpExp.expression); - if (symbol && symbol.valueDeclaration) { - const parentNode = symbol.valueDeclaration.parent; - if (parentNode && (ts.isInterfaceDeclaration(parentNode) || ts.isSourceFile(parentNode)) && - (ts.isMethodSignature(symbol.valueDeclaration) || - ts.isFunctionDeclaration(symbol.valueDeclaration)) && - symbol.valueDeclaration.name) { - const parentName = ts.isInterfaceDeclaration(parentNode) ? parentNode.name.text : null; - const callName = getPropertyNameText(symbol.valueDeclaration.name); - if (ASYNC_EXTERNAL_CALLS.some( - c => - (c.name === callName && - (c.parent.indexOf(parentName) !== -1 || c.parent.indexOf('*') !== -1)))) { - return; - } - } - } - } - - jumpExp.arguments!.forEach((node: ts.Node) => { - node = this._resolveDeclarationOfNode(node); - - if (ts.isVariableDeclaration(node) && node.initializer) { - node = node.initializer; - } - - if (isFunctionLikeDeclaration(node) && !!node.body) { - this.ambiguousNodeQueue.push(node.body); - } - }); - } - - /** - * Resolves a given node from the context. In case the node is not mapped in - * the context, the original node is returned. - */ - private _resolveNodeFromContext(node: ts.Node): ts.Node { - if (this.context.has(node)) { - return this.context.get(node)!; - } - return node; - } - - /** - * Updates the context to reflect the newly set parameter values. This allows future - * references to function parameters to be resolved to the actual node through the context. - */ - private _updateContext( - callArgs: ts.NodeArray, parameters: ts.NodeArray) { - parameters.forEach((parameter, index) => { - let argumentNode: ts.Node = callArgs[index]; - - if (!argumentNode) { - if (!parameter.initializer) { - return; - } - - // Argument can be undefined in case the function parameter has a default - // value. In that case we want to store the parameter default value in the context. - argumentNode = parameter.initializer; - } - - if (ts.isIdentifier(argumentNode)) { - this.context.set(parameter, this._resolveDeclarationOfNode(argumentNode)); - } else { - this.context.set(parameter, argumentNode); - } - }); - } - - /** - * Resolves the declaration of a given TypeScript node. For example an identifier can - * refer to a function parameter. This parameter can then be resolved through the - * function context. - */ - private _resolveDeclarationOfNode(node: ts.Node): ts.Node { - const symbol = this._getDeclarationSymbolOfNode(node); - - if (!symbol || !symbol.valueDeclaration) { - return node; - } - - return this._resolveNodeFromContext(symbol.valueDeclaration); - } - - /** - * Gets the declaration symbol of a given TypeScript node. Resolves aliased - * symbols to the symbol containing the value declaration. - */ - private _getDeclarationSymbolOfNode(node: ts.Node): ts.Symbol|null { - let symbol = this.typeChecker.getSymbolAtLocation(node); - - if (!symbol) { - return null; - } - - // Resolve the symbol to it's original declaration symbol. - while (symbol.flags & ts.SymbolFlags.Alias) { - symbol = this.typeChecker.getAliasedSymbol(symbol); - } - - return symbol; - } - - /** Gets the symbol of the given property access expression. */ - private _getPropertyAccessSymbol(node: ts.PropertyAccessExpression): ts.Symbol|null { - let propertySymbol = this._getDeclarationSymbolOfNode(node.name); - - if (!propertySymbol || !propertySymbol.valueDeclaration) { - return null; - } - - if (!this.context.has(propertySymbol.valueDeclaration)) { - return propertySymbol; - } - - // In case the context has the value declaration of the given property access - // name identifier, we need to replace the "propertySymbol" with the symbol - // referring to the resolved symbol based on the context. e.g. abstract properties - // can ultimately resolve into an accessor declaration based on the implementation. - const contextNode = this._resolveNodeFromContext(propertySymbol.valueDeclaration); - - if (!ts.isAccessor(contextNode)) { - return null; - } - - // Resolve the symbol referring to the "accessor" using the name identifier - // of the accessor declaration. - return this._getDeclarationSymbolOfNode(contextNode.name); - } -} diff --git a/packages/core/schematics/migrations/static-queries/strategies/usage_strategy/super_class_context.ts b/packages/core/schematics/migrations/static-queries/strategies/usage_strategy/super_class_context.ts deleted file mode 100644 index 8e4e738ceec..00000000000 --- a/packages/core/schematics/migrations/static-queries/strategies/usage_strategy/super_class_context.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; - -import {isFunctionLikeDeclaration} from '../../../../utils/typescript/functions'; -import {hasModifier} from '../../../../utils/typescript/nodes'; -import {getPropertyNameText} from '../../../../utils/typescript/property_name'; -import {ClassMetadataMap} from '../../angular/ng_query_visitor'; -import {getSuperClassDeclarations} from '../../angular/super_class'; - -import {FunctionContext} from './declaration_usage_visitor'; - - -/** - * Updates the specified function context to map abstract super-class class members - * to their implementation TypeScript nodes. This allows us to run the declaration visitor - * for the super class with the context of the "baseClass" (e.g. with implemented abstract - * class members) - */ -export function updateSuperClassAbstractMembersContext( - baseClass: ts.ClassDeclaration, context: FunctionContext, classMetadataMap: ClassMetadataMap) { - getSuperClassDeclarations(baseClass, classMetadataMap).forEach(superClassDecl => { - superClassDecl.members.forEach(superClassMember => { - if (!superClassMember.name || !hasModifier(superClassMember, ts.SyntaxKind.AbstractKeyword)) { - return; - } - - // Find the matching implementation of the abstract declaration from the super class. - const baseClassImpl = baseClass.members.find( - baseClassMethod => !!baseClassMethod.name && - getPropertyNameText(baseClassMethod.name) === - getPropertyNameText(superClassMember.name!)); - - if (!baseClassImpl || !isFunctionLikeDeclaration(baseClassImpl) || !baseClassImpl.body) { - return; - } - - if (!context.has(superClassMember)) { - context.set(superClassMember, baseClassImpl); - } - }); - }); -} diff --git a/packages/core/schematics/migrations/static-queries/strategies/usage_strategy/template_usage_visitor.ts b/packages/core/schematics/migrations/static-queries/strategies/usage_strategy/template_usage_visitor.ts deleted file mode 100644 index 6035592ab7c..00000000000 --- a/packages/core/schematics/migrations/static-queries/strategies/usage_strategy/template_usage_visitor.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 type {ParseSourceSpan, PropertyRead, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstBoundText, TmplAstElement, TmplAstNode, TmplAstTemplate} from '@angular/compiler'; -import {TemplateAstVisitor} from '../../../../utils/template_ast_visitor'; - -/** - * AST visitor that traverses the Render3 HTML AST in order to check if the given - * query property is accessed statically in the template. - */ -export class TemplateUsageVisitor extends TemplateAstVisitor { - private hasQueryTemplateReference = false; - private expressionAstVisitor; - - constructor( - public queryPropertyName: string, compilerModule: typeof import('@angular/compiler')) { - super(compilerModule); - - // AST visitor that checks if the given expression contains property reads that - // refer to the specified query property name. - // This class must be defined within the template visitor due to the need to extend from a class - // value found within `@angular/compiler` which is dynamically imported and provided to the - // visitor. - this.expressionAstVisitor = new (class extends compilerModule.RecursiveAstVisitor { - hasQueryPropertyRead = false; - - - override visitPropertyRead(node: PropertyRead, span: ParseSourceSpan): any { - // The receiver of the property read needs to be "implicit" as queries are accessed - // from the component instance and not from other objects. - if (node.receiver instanceof compilerModule.ImplicitReceiver && - node.name === queryPropertyName) { - this.hasQueryPropertyRead = true; - return; - } - - super.visitPropertyRead(node, span); - } - })(); - } - - /** Checks whether the given query is statically accessed within the specified HTML nodes. */ - isQueryUsedStatically(htmlNodes: TmplAstNode[]): boolean { - this.hasQueryTemplateReference = false; - this.expressionAstVisitor.hasQueryPropertyRead = false; - - // Visit all AST nodes and check if the query property is used statically. - this.visitAll(htmlNodes); - - return !this.hasQueryTemplateReference && this.expressionAstVisitor.hasQueryPropertyRead; - } - - override visitElement(element: TmplAstElement): void { - // In case there is a template references variable that matches the query property - // name, we can finish this visitor as such a template variable can be used in the - // entire template and the query therefore can't be accessed from the template. - if (element.references.some(r => r.name === this.queryPropertyName)) { - this.hasQueryTemplateReference = true; - return; - } - - this.visitAll(element.attributes); - this.visitAll(element.inputs); - this.visitAll(element.outputs); - this.visitAll(element.children); - } - - override visitTemplate(template: TmplAstTemplate): void { - this.visitAll(template.attributes); - this.visitAll(template.inputs); - this.visitAll(template.outputs); - - // We don't want to visit any children of the template as these never can't - // access a query statically. The templates can be rendered in the ngAfterViewInit" - // lifecycle hook at the earliest. - } - - override visitBoundAttribute(attribute: TmplAstBoundAttribute) { - attribute.value.visit(this.expressionAstVisitor, attribute.sourceSpan); - } - - override visitBoundText(text: TmplAstBoundText) { - text.value.visit(this.expressionAstVisitor, text.sourceSpan); - } - - override visitBoundEvent(node: TmplAstBoundEvent) { - node.handler.visit(this.expressionAstVisitor, node.handlerSpan); - } -} diff --git a/packages/core/schematics/migrations/static-queries/strategies/usage_strategy/usage_strategy.ts b/packages/core/schematics/migrations/static-queries/strategies/usage_strategy/usage_strategy.ts deleted file mode 100644 index 91423dae0d0..00000000000 --- a/packages/core/schematics/migrations/static-queries/strategies/usage_strategy/usage_strategy.ts +++ /dev/null @@ -1,186 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; - -import {parseHtmlGracefully} from '../../../../utils/parse_html'; -import {hasPropertyNameText} from '../../../../utils/typescript/property_name'; -import {ClassMetadataMap} from '../../angular/ng_query_visitor'; -import {NgQueryDefinition, QueryTiming, QueryType} from '../../angular/query-definition'; -import {TimingResult, TimingStrategy} from '../timing-strategy'; - -import {DeclarationUsageVisitor, FunctionContext, ResolvedUsage} from './declaration_usage_visitor'; -import {updateSuperClassAbstractMembersContext} from './super_class_context'; -import {TemplateUsageVisitor} from './template_usage_visitor'; - - -/** - * Object that maps a given type of query to a list of lifecycle hooks that - * could be used to access such a query statically. - */ -const STATIC_QUERY_LIFECYCLE_HOOKS = { - [QueryType.ViewChild]: - ['ngOnChanges', 'ngOnInit', 'ngDoCheck', 'ngAfterContentInit', 'ngAfterContentChecked'], - [QueryType.ContentChild]: ['ngOnChanges', 'ngOnInit', 'ngDoCheck'], -}; - -/** - * Query timing strategy that determines the timing of a given query by inspecting how - * the query is accessed within the project's TypeScript source files. Read more about - * this strategy here: https://hackmd.io/s/Hymvc2OKE - */ -export class QueryUsageStrategy implements TimingStrategy { - constructor( - private classMetadata: ClassMetadataMap, private typeChecker: ts.TypeChecker, - private compilerModule: typeof import('@angular/compiler')) {} - - setup() {} - - /** - * Analyzes the usage of the given query and determines the query timing based - * on the current usage of the query. - */ - detectTiming(query: NgQueryDefinition): TimingResult { - if (query.property === null) { - return {timing: null, message: 'Queries defined on accessors cannot be analyzed.'}; - } - - const usage = this.analyzeQueryUsage(query.container, query, []); - - if (usage === ResolvedUsage.AMBIGUOUS) { - return { - timing: QueryTiming.STATIC, - message: 'Query timing is ambiguous. Please check if the query can be marked as dynamic.' - }; - } else if (usage === ResolvedUsage.SYNCHRONOUS) { - return {timing: QueryTiming.STATIC}; - } else { - return {timing: QueryTiming.DYNAMIC}; - } - } - - /** - * Checks whether a given query is used statically within the given class, its super - * class or derived classes. - */ - private analyzeQueryUsage( - classDecl: ts.ClassDeclaration, query: NgQueryDefinition, knownInputNames: string[], - functionCtx: FunctionContext = new Map(), visitInheritedClasses = true): ResolvedUsage { - const usageVisitor = - new DeclarationUsageVisitor(query.property!, this.typeChecker, functionCtx); - const classMetadata = this.classMetadata.get(classDecl); - let usage: ResolvedUsage = ResolvedUsage.ASYNCHRONOUS; - - // In case there is metadata for the current class, we collect all resolved Angular input - // names and add them to the list of known inputs that need to be checked for usages of - // the current query. e.g. queries used in an @Input() *setter* are always static. - if (classMetadata) { - knownInputNames.push(...classMetadata.ngInputNames); - } - - // Array of TypeScript nodes which can contain usages of the given query in - // order to access it statically. - const possibleStaticQueryNodes = filterQueryClassMemberNodes(classDecl, query, knownInputNames); - - // In case nodes that can possibly access a query statically have been found, check - // if the query declaration is synchronously used within any of these nodes. - if (possibleStaticQueryNodes.length) { - possibleStaticQueryNodes.forEach( - n => usage = combineResolvedUsage(usage, usageVisitor.getResolvedNodeUsage(n))); - } - - if (!classMetadata) { - return usage; - } - - // In case there is a component template for the current class, we check if the - // template statically accesses the current query. In case that's true, the query - // can be marked as static. - if (classMetadata.template && hasPropertyNameText(query.property!.name)) { - const template = classMetadata.template; - const parsedHtml = - parseHtmlGracefully(template.content, template.filePath, this.compilerModule); - const htmlVisitor = new TemplateUsageVisitor(query.property!.name.text, this.compilerModule); - - if (parsedHtml && htmlVisitor.isQueryUsedStatically(parsedHtml)) { - return ResolvedUsage.SYNCHRONOUS; - } - } - - // In case derived classes should also be analyzed, we determine the classes that derive - // from the current class and check if these have input setters or lifecycle hooks that - // use the query statically. - if (visitInheritedClasses) { - classMetadata.derivedClasses.forEach(derivedClass => { - usage = combineResolvedUsage( - usage, this.analyzeQueryUsage(derivedClass, query, knownInputNames)); - }); - } - - // In case the current class has a super class, we determine declared abstract function-like - // declarations in the super-class that are implemented in the current class. The super class - // will then be analyzed with the abstract declarations mapped to the implemented TypeScript - // nodes. This allows us to handle queries which are used in super classes through derived - // abstract method declarations. - if (classMetadata.superClass) { - const superClassDecl = classMetadata.superClass; - - // Update the function context to map abstract declaration nodes to their implementation - // node in the base class. This ensures that the declaration usage visitor can analyze - // abstract class member declarations. - updateSuperClassAbstractMembersContext(classDecl, functionCtx, this.classMetadata); - - usage = combineResolvedUsage( - usage, this.analyzeQueryUsage(superClassDecl, query, [], functionCtx, false)); - } - - return usage; - } -} - -/** - * Combines two resolved usages based on a fixed priority. "Synchronous" takes - * precedence over "Ambiguous" whereas ambiguous takes precedence over "Asynchronous". - */ -function combineResolvedUsage(base: ResolvedUsage, target: ResolvedUsage): ResolvedUsage { - if (base === ResolvedUsage.SYNCHRONOUS) { - return base; - } else if (target !== ResolvedUsage.ASYNCHRONOUS) { - return target; - } else { - return ResolvedUsage.ASYNCHRONOUS; - } -} - -/** - * Filters all class members from the class declaration that can access the - * given query statically (e.g. ngOnInit lifecycle hook or @Input setters) - */ -function filterQueryClassMemberNodes( - classDecl: ts.ClassDeclaration, query: NgQueryDefinition, - knownInputNames: string[]): ts.Block[] { - // Returns an array of TypeScript nodes which can contain usages of the given query - // in order to access it statically. e.g. - // (1) queries used in the "ngOnInit" lifecycle hook are static. - // (2) inputs with setters can access queries statically. - return classDecl.members - .filter( - (m): - m is(ts.SetAccessorDeclaration | ts.MethodDeclaration) => { - if (ts.isMethodDeclaration(m) && m.body && hasPropertyNameText(m.name) && - STATIC_QUERY_LIFECYCLE_HOOKS[query.type].indexOf(m.name.text) !== -1) { - return true; - } else if ( - knownInputNames && ts.isSetAccessor(m) && m.body && - hasPropertyNameText(m.name) && knownInputNames.indexOf(m.name.text) !== -1) { - return true; - } - return false; - }) - .map(member => member.body!); -} diff --git a/packages/core/schematics/migrations/static-queries/transform.ts b/packages/core/schematics/migrations/static-queries/transform.ts deleted file mode 100644 index 69b179dd2af..00000000000 --- a/packages/core/schematics/migrations/static-queries/transform.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; -import {getPropertyNameText} from '../../utils/typescript/property_name'; -import {NgQueryDefinition, QueryTiming} from './angular/query-definition'; - -export type TransformedQueryResult = null|{ - /** Transformed call expression. */ - node: ts.CallExpression; - /** Failure message which is set when the query could not be transformed successfully. */ - failureMessage: string|null; -}; - -const TODO_SPECIFY_COMMENT = 'TODO: add static flag'; -const TODO_CHECK_COMMENT = 'TODO: check static flag'; - -/** - * Transforms the given query decorator by explicitly specifying the timing based on the - * determined timing. The updated decorator call expression node will be returned. - */ -export function getTransformedQueryCallExpr( - query: NgQueryDefinition, timing: QueryTiming|null, - createTodo: boolean): TransformedQueryResult { - const queryExpr = query.decorator.node.expression; - const queryArguments = queryExpr.arguments; - const queryPropertyAssignments = timing === null ? - [] : - [ts.createPropertyAssignment( - 'static', timing === QueryTiming.STATIC ? ts.createTrue() : ts.createFalse())]; - - // If the query decorator is already called with two arguments, we need to - // keep the existing options untouched and just add the new property if possible. - if (queryArguments.length === 2) { - const existingOptions = queryArguments[1]; - const existingOptionsText = existingOptions.getFullText(); - const hasTodoComment = existingOptionsText.includes(TODO_SPECIFY_COMMENT) || - existingOptionsText.includes(TODO_CHECK_COMMENT); - let newOptionsNode: ts.Expression; - let failureMessage: string|null = null; - - if (ts.isObjectLiteralExpression(existingOptions)) { - // In case the options already contains a property for the "static" flag, - // we just skip this query and leave it untouched. - if (existingOptions.properties.some( - p => !!p.name && getPropertyNameText(p.name) === 'static')) { - return null; - } - - newOptionsNode = ts.updateObjectLiteral( - existingOptions, existingOptions.properties.concat(queryPropertyAssignments)); - - // In case we want to add a todo and the options do not have the todo - // yet, we add the query timing todo as synthetic multi-line comment. - if (createTodo && !hasTodoComment) { - addQueryTimingTodoToNode(newOptionsNode, timing === null); - } - } else { - // In case the options query parameter is not an object literal expression, and - // we want to set the query timing, we just preserve the existing query parameter. - newOptionsNode = existingOptions; - // We always want to add a TODO in case the query options cannot be updated. - if (!hasTodoComment) { - addQueryTimingTodoToNode(existingOptions, true); - } - // If there is a new explicit timing that has been determined for the given query, - // we create a transformation failure message that shows developers that they need - // to set the query timing manually to the determined query timing. - if (timing !== null) { - failureMessage = 'Cannot update query to set explicit timing. Please manually ' + - `set the query timing to: "{static: ${(timing === QueryTiming.STATIC).toString()}}"`; - } - } - - return { - failureMessage, - node: ts.updateCall( - queryExpr, queryExpr.expression, queryExpr.typeArguments, - [queryArguments[0], newOptionsNode!]) - }; - } - - const optionsNode = ts.createObjectLiteral(queryPropertyAssignments); - - if (createTodo) { - addQueryTimingTodoToNode(optionsNode, timing === null); - } - - return { - failureMessage: null, - node: ts.updateCall( - queryExpr, queryExpr.expression, queryExpr.typeArguments, [queryArguments[0], optionsNode]) - }; -} - -/** - * Adds a to-do to the given TypeScript node which reminds developers to specify - * an explicit query timing or to double-check the updated timing. - */ -function addQueryTimingTodoToNode(node: ts.Node, addSpecifyTimingTodo: boolean) { - ts.setSyntheticLeadingComments( - node, [{ - pos: -1, - end: -1, - hasTrailingNewLine: false, - kind: ts.SyntaxKind.MultiLineCommentTrivia, - text: ` ${addSpecifyTimingTodo ? TODO_SPECIFY_COMMENT : TODO_CHECK_COMMENT} ` - }]); -} diff --git a/packages/core/schematics/migrations/template-var-assignment/BUILD.bazel b/packages/core/schematics/migrations/template-var-assignment/BUILD.bazel deleted file mode 100644 index a08d0b45789..00000000000 --- a/packages/core/schematics/migrations/template-var-assignment/BUILD.bazel +++ /dev/null @@ -1,20 +0,0 @@ -load("//tools:defaults.bzl", "ts_library") - -ts_library( - name = "template-var-assignment", - srcs = glob(["**/*.ts"]), - tsconfig = "//packages/core/schematics:tsconfig.json", - visibility = [ - "//packages/core/schematics:__pkg__", - "//packages/core/schematics/migrations/google3:__pkg__", - "//packages/core/schematics/test:__pkg__", - ], - deps = [ - "//packages/compiler", - "//packages/core/schematics/utils", - "@npm//@angular-devkit/core", - "@npm//@angular-devkit/schematics", - "@npm//@types/node", - "@npm//typescript", - ], -) diff --git a/packages/core/schematics/migrations/template-var-assignment/README.md b/packages/core/schematics/migrations/template-var-assignment/README.md deleted file mode 100644 index 2cf411fec7d..00000000000 --- a/packages/core/schematics/migrations/template-var-assignment/README.md +++ /dev/null @@ -1,27 +0,0 @@ -## Assignments to template variables - -With Ivy, assignments to template variables are no longer supported -as template variables are effectively constants. - -This means that assignments to template variables will break your -application once Ivy is enabled by default. For example: - -```html - -``` - -In the example from above, a value is assigned to the `option` -template variable on `click`. This will ultimately break your -application and therefore the logic needs to be adjusted to not -update the `option` variable, but rather the given element in -the `options` array: - -```html - -``` \ No newline at end of file diff --git a/packages/core/schematics/migrations/template-var-assignment/analyze_template.ts b/packages/core/schematics/migrations/template-var-assignment/analyze_template.ts deleted file mode 100644 index a74aba88d22..00000000000 --- a/packages/core/schematics/migrations/template-var-assignment/analyze_template.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 type {PropertyWrite} from '@angular/compiler'; -import {ResolvedTemplate} from '../../utils/ng_component_template'; -import {parseHtmlGracefully} from '../../utils/parse_html'; -import {HtmlVariableAssignmentVisitor} from './angular/html_variable_assignment_visitor'; - -export interface TemplateVariableAssignment { - node: PropertyWrite; - start: number; - end: number; -} - -/** - * Analyzes a given resolved template by looking for property assignments to local - * template variables within bound events. - */ -export function analyzeResolvedTemplate( - template: ResolvedTemplate, - compilerModule: typeof import('@angular/compiler')): TemplateVariableAssignment[]|null { - const templateNodes = parseHtmlGracefully(template.content, template.filePath, compilerModule); - - if (!templateNodes) { - return null; - } - - const visitor = new HtmlVariableAssignmentVisitor(compilerModule); - - // Analyze the Angular Render3 HTML AST and collect all template variable assignments. - visitor.visitAll(templateNodes); - - return visitor.variableAssignments.map( - ({node, start, end}) => ({node, start: start + node.span.start, end})); -} diff --git a/packages/core/schematics/migrations/template-var-assignment/angular/html_variable_assignment_visitor.ts b/packages/core/schematics/migrations/template-var-assignment/angular/html_variable_assignment_visitor.ts deleted file mode 100644 index afc443e1544..00000000000 --- a/packages/core/schematics/migrations/template-var-assignment/angular/html_variable_assignment_visitor.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 type {ImplicitReceiver, ParseSourceSpan, PropertyWrite, RecursiveAstVisitor, TmplAstBoundEvent, TmplAstElement, TmplAstTemplate, TmplAstVariable} from '@angular/compiler'; -import {TemplateAstVisitor} from '../../../utils/template_ast_visitor'; - - -export interface TemplateVariableAssignment { - start: number; - end: number; - node: PropertyWrite; -} - -/** - * HTML AST visitor that traverses the Render3 HTML AST in order to find all - * expressions that write to local template variables within bound events. - */ -export class HtmlVariableAssignmentVisitor extends TemplateAstVisitor { - variableAssignments: TemplateVariableAssignment[] = []; - - private currentVariables: TmplAstVariable[] = []; - private expressionAstVisitor; - - constructor(compilerModule: typeof import('@angular/compiler')) { - super(compilerModule); - - // AST visitor that resolves all variable assignments within a given expression AST. - // This class must be defined within the template visitor due to the need to extend from a class - // value found within `@angular/compiler` which is dynamically imported and provided to the - // visitor. - this.expressionAstVisitor = new (class extends compilerModule.RecursiveAstVisitor { - constructor( - private variableAssignments: TemplateVariableAssignment[], - private currentVariables: TmplAstVariable[]) { - super(); - } - - override visitPropertyWrite(node: PropertyWrite, span: ParseSourceSpan) { - if (node.receiver instanceof compilerModule.ImplicitReceiver && - this.currentVariables.some(v => v.name === node.name)) { - this.variableAssignments.push({ - node: node, - start: span.start.offset, - end: span.end.offset, - }); - } - super.visitPropertyWrite(node, span); - } - })(this.variableAssignments, this.currentVariables); - } - - override visitElement(element: TmplAstElement): void { - this.visitAll(element.outputs); - this.visitAll(element.children); - } - - override visitTemplate(template: TmplAstTemplate): void { - // Keep track of the template variables which can be accessed by the template - // child nodes through the implicit receiver. - this.currentVariables.push(...template.variables); - - // Visit all children of the template. The template proxies the outputs of the - // immediate child elements, so we just ignore outputs on the "Template" in order - // to not visit similar bound events twice. - this.visitAll(template.children); - - // Remove all previously added variables since all children that could access - // these have been visited already. - template.variables.forEach(v => { - const variableIdx = this.currentVariables.indexOf(v); - - if (variableIdx !== -1) { - this.currentVariables.splice(variableIdx, 1); - } - }); - } - - override visitBoundEvent(node: TmplAstBoundEvent) { - node.handler.visit(this.expressionAstVisitor, node.handlerSpan); - } -} diff --git a/packages/core/schematics/migrations/template-var-assignment/index.ts b/packages/core/schematics/migrations/template-var-assignment/index.ts deleted file mode 100644 index 4e5a228a3d1..00000000000 --- a/packages/core/schematics/migrations/template-var-assignment/index.ts +++ /dev/null @@ -1,101 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 {logging, normalize} from '@angular-devkit/core'; -import {Rule, SchematicContext, SchematicsException, Tree} from '@angular-devkit/schematics'; -import {relative} from 'path'; - -import {loadEsmModule} from '../../utils/load_esm'; -import {NgComponentTemplateVisitor} from '../../utils/ng_component_template'; -import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; -import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host'; - -import {analyzeResolvedTemplate} from './analyze_template'; - -type Logger = logging.LoggerApi; - -const README_URL = 'https://v8.angular.io/guide/deprecations#cannot-assign-to-template-variables'; -const FAILURE_MESSAGE = `Found assignment to template variable.`; - -/** Entry point for the V8 template variable assignment schematic. */ -export default function(): Rule { - return async (tree: Tree, context: SchematicContext) => { - const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); - const basePath = process.cwd(); - - if (!buildPaths.length && !testPaths.length) { - throw new SchematicsException( - 'Could not find any tsconfig file. Cannot check templates for template variable ' + - 'assignments.'); - } - - let compilerModule; - try { - // Load ESM `@angular/compiler` using the TypeScript dynamic import workaround. - // Once TypeScript provides support for keeping the dynamic import this workaround can be - // changed to a direct dynamic import. - compilerModule = await loadEsmModule('@angular/compiler'); - } catch (e) { - throw new SchematicsException( - `Unable to load the '@angular/compiler' package. Details: ${e.message}`); - } - - for (const tsconfigPath of [...buildPaths, ...testPaths]) { - runTemplateVariableAssignmentCheck( - tree, tsconfigPath, basePath, context.logger, compilerModule); - } - }; -} - -/** - * Runs the template variable assignment check. Warns developers - * if values are assigned to template variables within output bindings. - */ -function runTemplateVariableAssignmentCheck( - tree: Tree, tsconfigPath: string, basePath: string, logger: Logger, - compilerModule: typeof import('@angular/compiler')) { - const {program} = createMigrationProgram(tree, tsconfigPath, basePath); - const typeChecker = program.getTypeChecker(); - const templateVisitor = new NgComponentTemplateVisitor(typeChecker); - const sourceFiles = - program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program)); - - // Analyze source files by detecting HTML templates. - sourceFiles.forEach(sourceFile => templateVisitor.visitNode(sourceFile)); - - const {resolvedTemplates} = templateVisitor; - const collectedFailures: string[] = []; - - // Analyze each resolved template and print a warning for property writes to - // template variables. - resolvedTemplates.forEach(template => { - const filePath = template.filePath; - const nodes = analyzeResolvedTemplate(template, compilerModule); - - if (!nodes) { - return; - } - - const displayFilePath = normalize(relative(basePath, filePath)); - - nodes.forEach(n => { - const {line, character} = template.getCharacterAndLineOfPosition(n.start); - collectedFailures.push(`${displayFilePath}@${line + 1}:${character + 1}: ${FAILURE_MESSAGE}`); - }); - }); - - if (collectedFailures.length) { - logger.info('---- Template Variable Assignment schematic ----'); - logger.info('Assignments to template variables will no longer work with Ivy as'); - logger.info('template variables are effectively constants in Ivy. Read more about'); - logger.info(`this change here: ${README_URL}`); - logger.info(''); - logger.info('The following template assignments were found:'); - collectedFailures.forEach(failure => logger.warn(`⮑ ${failure}`)); - } -} diff --git a/packages/core/schematics/migrations/undecorated-classes-with-decorated-fields/BUILD.bazel b/packages/core/schematics/migrations/undecorated-classes-with-decorated-fields/BUILD.bazel deleted file mode 100644 index e0eff880da2..00000000000 --- a/packages/core/schematics/migrations/undecorated-classes-with-decorated-fields/BUILD.bazel +++ /dev/null @@ -1,20 +0,0 @@ -load("//tools:defaults.bzl", "ts_library") - -ts_library( - name = "undecorated-classes-with-decorated-fields", - srcs = glob(["**/*.ts"]), - tsconfig = "//packages/core/schematics:tsconfig.json", - visibility = [ - "//packages/core/schematics:__pkg__", - "//packages/core/schematics/migrations/google3:__pkg__", - "//packages/core/schematics/migrations/undecorated-classes-with-decorated-fields/google3:__pkg__", - "//packages/core/schematics/test:__pkg__", - ], - deps = [ - "//packages/compiler-cli/private", - "//packages/core/schematics/utils", - "@npm//@angular-devkit/schematics", - "@npm//@types/node", - "@npm//typescript", - ], -) diff --git a/packages/core/schematics/migrations/undecorated-classes-with-decorated-fields/README.md b/packages/core/schematics/migrations/undecorated-classes-with-decorated-fields/README.md deleted file mode 100644 index 2c89d7b2b8d..00000000000 --- a/packages/core/schematics/migrations/undecorated-classes-with-decorated-fields/README.md +++ /dev/null @@ -1,27 +0,0 @@ -## Undecorated classes with decorated fields migration - -Automatically adds a `Directive` decorator to undecorated classes that use Angular features. A -class is considered using Angular features if a class member is decorated (e.g. `@Input()`), or -if the class defines any lifecycle hooks. - -This matches the undecorated classes compatibility logic in ngtsc that will be removed -as part of v10 so that the new mental model is enforced. - -#### Before -```ts -import { Input } from '@angular/core'; - -export class Base { - @Input() isActive: boolean; -} -``` - -#### After -```ts -import { Input, Directive } from '@angular/core'; - -@Directive() -export class Base { - @Input() isActive: boolean; -} -``` diff --git a/packages/core/schematics/migrations/undecorated-classes-with-decorated-fields/google3/BUILD.bazel b/packages/core/schematics/migrations/undecorated-classes-with-decorated-fields/google3/BUILD.bazel deleted file mode 100644 index 8950f566525..00000000000 --- a/packages/core/schematics/migrations/undecorated-classes-with-decorated-fields/google3/BUILD.bazel +++ /dev/null @@ -1,13 +0,0 @@ -load("//tools:defaults.bzl", "ts_library") - -ts_library( - name = "google3", - srcs = glob(["**/*.ts"]), - tsconfig = "//packages/core/schematics:tsconfig.json", - visibility = ["//packages/core/schematics/migrations/google3:__pkg__"], - deps = [ - "//packages/core/schematics/migrations/undecorated-classes-with-decorated-fields", - "@npm//tslint", - "@npm//typescript", - ], -) diff --git a/packages/core/schematics/migrations/undecorated-classes-with-decorated-fields/google3/tslint_update_recorder.ts b/packages/core/schematics/migrations/undecorated-classes-with-decorated-fields/google3/tslint_update_recorder.ts deleted file mode 100644 index 4ca54b989e9..00000000000 --- a/packages/core/schematics/migrations/undecorated-classes-with-decorated-fields/google3/tslint_update_recorder.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 {Replacement, RuleFailure} from 'tslint'; -import ts from 'typescript'; - -import {UpdateRecorder} from '../update_recorder'; - -export class TslintUpdateRecorder implements UpdateRecorder { - failures: RuleFailure[] = []; - - constructor(private ruleName: string, private sourceFile: ts.SourceFile) {} - - addClassTodo(node: ts.ClassDeclaration, message: string) { - this.failures.push(new RuleFailure( - this.sourceFile, node.getStart(), 0, message, this.ruleName, - Replacement.appendText(node.getStart(), `// TODO: ${message}`))); - } - - /** Adds the specified decorator to the given class declaration. */ - addClassDecorator(node: ts.ClassDeclaration, decoratorText: string) { - // Adding a decorator should be the last replacement. Replacements/rule failures - // are handled in reverse and in case a decorator and import are inserted at - // the start of the file, the class decorator should come after the import. - this.failures.unshift(new RuleFailure( - this.sourceFile, node.getStart(), 0, - `Class needs to be decorated with ` + - `"${decoratorText}" because it uses Angular features.`, - this.ruleName, Replacement.appendText(node.getStart(), `${decoratorText}\n`))); - } - - /** Adds the specified import to the source file at the given position */ - addNewImport(start: number, importText: string) { - this.failures.push(new RuleFailure( - this.sourceFile, start, 0, `Source file needs to have import: "${importText}"`, - this.ruleName, Replacement.appendText(start, importText))); - } - - /** Updates existing named imports to the given new named imports. */ - updateExistingImport(namedBindings: ts.NamedImports, newNamedBindings: string): void { - const fix = [ - Replacement.deleteText(namedBindings.getStart(), namedBindings.getWidth()), - Replacement.appendText(namedBindings.getStart(), newNamedBindings), - ]; - this.failures.push(new RuleFailure( - this.sourceFile, namedBindings.getStart(), namedBindings.getEnd(), - `Import needs to be updated to import symbols: "${newNamedBindings}"`, this.ruleName, fix)); - } - - commitUpdate() {} -} diff --git a/packages/core/schematics/migrations/undecorated-classes-with-decorated-fields/index.ts b/packages/core/schematics/migrations/undecorated-classes-with-decorated-fields/index.ts deleted file mode 100644 index fea367105eb..00000000000 --- a/packages/core/schematics/migrations/undecorated-classes-with-decorated-fields/index.ts +++ /dev/null @@ -1,125 +0,0 @@ -/** - * @license - * Copyright Google LLC 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, SchematicsException, Tree,} from '@angular-devkit/schematics'; -import {relative} from 'path'; -import ts from 'typescript'; - -import {loadCompilerCliMigrationsModule, loadEsmModule} from '../../utils/load_esm'; -import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; -import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host'; - -import {UndecoratedClassesWithDecoratedFieldsTransform} from './transform'; -import {UpdateRecorder} from './update_recorder'; - -/** - * Migration that adds an Angular decorator to classes that have Angular field decorators. - * https://hackmd.io/vuQfavzfRG6KUCtU7oK_EA - */ -export default function(): Rule { - return async (tree: Tree, ctx: SchematicContext) => { - const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); - const basePath = process.cwd(); - const allPaths = [...buildPaths, ...testPaths]; - const failures: string[] = []; - - if (!allPaths.length) { - throw new SchematicsException( - 'Could not find any tsconfig file. Cannot add an Angular decorator to undecorated classes.'); - } - - let compilerCliMigrationsModule; - try { - // Load ESM `@angular/compiler/private/migrations` using the TypeScript dynamic import - // workaround. Once TypeScript provides support for keeping the dynamic import this workaround - // can be changed to a direct dynamic import. - compilerCliMigrationsModule = await loadCompilerCliMigrationsModule(); - } catch (e) { - throw new SchematicsException( - `Unable to load the '@angular/compiler-cli' package. Details: ${e.message}`); - } - - for (const tsconfigPath of allPaths) { - failures.push(...runUndecoratedClassesMigration( - tree, tsconfigPath, basePath, compilerCliMigrationsModule)); - } - - if (failures.length) { - ctx.logger.info('Could not migrate all undecorated classes that use Angular features.'); - ctx.logger.info('Please manually fix the following failures:'); - failures.forEach(message => ctx.logger.warn(`⮑ ${message}`)); - } - }; -} - -function runUndecoratedClassesMigration( - tree: Tree, tsconfigPath: string, basePath: string, - compilerCliMigrationsModule: typeof import('@angular/compiler-cli/private/migrations')): - string[] { - const failures: string[] = []; - const {program} = createMigrationProgram(tree, tsconfigPath, basePath); - const typeChecker = program.getTypeChecker(); - const sourceFiles = - program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program)); - const updateRecorders = new Map(); - const transform = new UndecoratedClassesWithDecoratedFieldsTransform( - typeChecker, getUpdateRecorder, compilerCliMigrationsModule); - - // Migrate all source files in the project. - transform.migrate(sourceFiles).forEach(({node, message}) => { - const nodeSourceFile = node.getSourceFile(); - const relativeFilePath = relative(basePath, nodeSourceFile.fileName); - const {line, character} = - ts.getLineAndCharacterOfPosition(node.getSourceFile(), node.getStart()); - failures.push(`${relativeFilePath}@${line + 1}:${character + 1}: ${message}`); - }); - - // Record the changes collected in the import manager. - transform.recordChanges(); - - // Walk through each update recorder and commit the update. We need to commit the - // updates in batches per source file as there can be only one recorder per source - // file in order to avoid shifted character offsets. - updateRecorders.forEach(recorder => recorder.commitUpdate()); - - return failures; - - /** Gets the update recorder for the specified source file. */ - function getUpdateRecorder(sourceFile: ts.SourceFile): UpdateRecorder { - if (updateRecorders.has(sourceFile)) { - return updateRecorders.get(sourceFile)!; - } - const treeRecorder = tree.beginUpdate(relative(basePath, sourceFile.fileName)); - const recorder: UpdateRecorder = { - addClassTodo(node: ts.ClassDeclaration, message: string) { - treeRecorder.insertRight(node.getStart(), `// TODO: ${message}\n`); - }, - addClassDecorator(node: ts.ClassDeclaration, text: string) { - // New imports should be inserted at the left while decorators should be inserted - // at the right in order to ensure that imports are inserted before the decorator - // if the start position of import and decorator is the source file start. - treeRecorder.insertRight(node.getStart(), `${text}\n`); - }, - addNewImport(start: number, importText: string) { - // New imports should be inserted at the left while decorators should be inserted - // at the right in order to ensure that imports are inserted before the decorator - // if the start position of import and decorator is the source file start. - treeRecorder.insertLeft(start, importText); - }, - updateExistingImport(namedBindings: ts.NamedImports, newNamedBindings: string) { - treeRecorder.remove(namedBindings.getStart(), namedBindings.getWidth()); - treeRecorder.insertRight(namedBindings.getStart(), newNamedBindings); - }, - commitUpdate() { - tree.commitUpdate(treeRecorder); - } - }; - updateRecorders.set(sourceFile, recorder); - return recorder; - } -} diff --git a/packages/core/schematics/migrations/undecorated-classes-with-decorated-fields/transform.ts b/packages/core/schematics/migrations/undecorated-classes-with-decorated-fields/transform.ts deleted file mode 100644 index 6f5c2500787..00000000000 --- a/packages/core/schematics/migrations/undecorated-classes-with-decorated-fields/transform.ts +++ /dev/null @@ -1,333 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; - -import {ImportManager} from '../../utils/import_manager'; -import {getAngularDecorators, NgDecorator} from '../../utils/ng_decorators'; -import {findBaseClassDeclarations} from '../../utils/typescript/find_base_classes'; -import {unwrapExpression} from '../../utils/typescript/functions'; -import {getPropertyNameText} from '../../utils/typescript/property_name'; - -import {UpdateRecorder} from './update_recorder'; - -/** - * Set of known decorators that indicate that the current class needs a directive - * definition. These decorators are always specific to directives. - */ -const DIRECTIVE_FIELD_DECORATORS = new Set([ - 'Input', 'Output', 'ViewChild', 'ViewChildren', 'ContentChild', 'ContentChildren', 'HostBinding', - 'HostListener' -]); - -/** - * Set of known lifecycle hooks that indicate that the current class needs a directive - * definition. These lifecycle hooks are always specific to directives. - */ -const DIRECTIVE_LIFECYCLE_HOOKS = new Set([ - 'ngOnChanges', 'ngOnInit', 'ngDoCheck', 'ngAfterViewInit', 'ngAfterViewChecked', - 'ngAfterContentInit', 'ngAfterContentChecked' -]); - -/** - * Set of known lifecycle hooks that indicate that a given class uses Angular - * features, but it's ambiguous whether it is a directive or service. - */ -const AMBIGUOUS_LIFECYCLE_HOOKS = new Set(['ngOnDestroy']); - -/** Describes how a given class is used in the context of Angular. */ -enum InferredKind { - DIRECTIVE, - AMBIGUOUS, - UNKNOWN, -} - -/** Describes possible types of Angular declarations. */ -enum DeclarationType { - DIRECTIVE, - COMPONENT, - ABSTRACT_DIRECTIVE, - PIPE, - INJECTABLE, -} - -/** Analyzed class declaration. */ -interface AnalyzedClass { - /** Type of declaration that is determined through an applied decorator. */ - decoratedType: DeclarationType|null; - /** Inferred class kind in terms of Angular. */ - inferredKind: InferredKind; -} - -interface AnalysisFailure { - node: ts.Node; - message: string; -} - -/** TODO message that is added to ambiguous classes using Angular features. */ -const AMBIGUOUS_CLASS_TODO = 'Add Angular decorator.'; - -export class UndecoratedClassesWithDecoratedFieldsTransform { - private printer = ts.createPrinter(); - private importManager = new ImportManager(this.getUpdateRecorder, this.printer); - private reflectionHost = - new this.compilerCliMigrationsModule.TypeScriptReflectionHost(this.typeChecker); - private partialEvaluator = new this.compilerCliMigrationsModule.PartialEvaluator( - this.reflectionHost, this.typeChecker, null); - - constructor( - private typeChecker: ts.TypeChecker, - private getUpdateRecorder: (sf: ts.SourceFile) => UpdateRecorder, - private compilerCliMigrationsModule: - typeof import('@angular/compiler-cli/private/migrations')) {} - - /** - * Migrates the specified source files. The transform adds the abstract `@Directive` - * decorator to undecorated classes that use Angular features. Class members which - * are decorated with any Angular decorator, or class members for lifecycle hooks are - * indicating that a given class uses Angular features. https://hackmd.io/vuQfavzfRG6KUCtU7oK_EA - */ - migrate(sourceFiles: ts.SourceFile[]): AnalysisFailure[] { - const {detectedAbstractDirectives, ambiguousClasses} = - this._findUndecoratedAbstractDirectives(sourceFiles); - - detectedAbstractDirectives.forEach(node => { - const sourceFile = node.getSourceFile(); - const recorder = this.getUpdateRecorder(sourceFile); - const directiveExpr = - this.importManager.addImportToSourceFile(sourceFile, 'Directive', '@angular/core'); - const decoratorExpr = ts.createDecorator(ts.createCall(directiveExpr, undefined, undefined)); - recorder.addClassDecorator( - node, this.printer.printNode(ts.EmitHint.Unspecified, decoratorExpr, sourceFile)); - }); - - // Ambiguous classes clearly use Angular features, but the migration is unable to - // determine whether the class is used as directive, service or pipe. The migration - // could potentially determine the type by checking NgModule definitions or inheritance - // of other known declarations, but this is out of scope and a TODO/failure is sufficient. - return Array.from(ambiguousClasses).reduce((failures, node) => { - // If the class has been reported as ambiguous before, skip adding a TODO and - // printing an error. A class could be visited multiple times when it's part - // of multiple build targets in the CLI project. - if (this._hasBeenReportedAsAmbiguous(node)) { - return failures; - } - - const sourceFile = node.getSourceFile(); - const recorder = this.getUpdateRecorder(sourceFile); - - // Add a TODO to the class that uses Angular features but is not decorated. - recorder.addClassTodo(node, AMBIGUOUS_CLASS_TODO); - - // Add an error for the class that will be printed in the `ng update` output. - return failures.concat({ - node, - message: 'Class uses Angular features but cannot be migrated automatically. Please ' + - 'add an appropriate Angular decorator.' - }); - }, [] as AnalysisFailure[]); - } - - /** Records all changes that were made in the import manager. */ - recordChanges() { - this.importManager.recordChanges(); - } - - /** - * Finds undecorated abstract directives in the specified source files. Also returns - * a set of undecorated classes which could not be detected as guaranteed abstract - * directives. Those are ambiguous and could be either Directive, Pipe or service. - */ - private _findUndecoratedAbstractDirectives(sourceFiles: ts.SourceFile[]) { - const ambiguousClasses = new Set(); - const declarations = new WeakMap(); - const detectedAbstractDirectives = new Set(); - const undecoratedClasses = new Set(); - - const visitNode = (node: ts.Node) => { - node.forEachChild(visitNode); - if (!ts.isClassDeclaration(node)) { - return; - } - const {inferredKind, decoratedType} = this._analyzeClassDeclaration(node); - - if (decoratedType !== null) { - declarations.set(node, decoratedType); - return; - } - - if (inferredKind === InferredKind.DIRECTIVE) { - detectedAbstractDirectives.add(node); - } else if (inferredKind === InferredKind.AMBIGUOUS) { - ambiguousClasses.add(node); - } else { - undecoratedClasses.add(node); - } - }; - - sourceFiles.forEach(sourceFile => sourceFile.forEachChild(visitNode)); - - /** - * Checks the inheritance of the given set of classes. It removes classes from the - * detected abstract directives set when they inherit from a non-abstract Angular - * declaration. e.g. an abstract directive can never extend from a component. - * - * If a class inherits from an abstract directive though, we will migrate them too - * as derived classes also need to be decorated. This has been done for a simpler mental - * model and reduced complexity in the Angular compiler. See migration plan document. - */ - const checkInheritanceOfClasses = (classes: Set) => { - classes.forEach(node => { - for (const {node: baseClass} of findBaseClassDeclarations(node, this.typeChecker)) { - if (!declarations.has(baseClass)) { - continue; - } - // If the undecorated class inherits from an abstract directive, always migrate it. - // Derived undecorated classes of abstract directives are always also considered - // abstract directives and need to be decorated too. This is necessary as otherwise - // the inheritance chain cannot be resolved by the Angular compiler. e.g. when it - // flattens directive metadata for type checking. In the other case, we never want - // to migrate a class if it extends from a non-abstract Angular declaration. That - // is an unsupported pattern as of v9 and was previously handled with the - // `undecorated-classes-with-di` migration (which copied the inherited decorator). - if (declarations.get(baseClass) === DeclarationType.ABSTRACT_DIRECTIVE) { - detectedAbstractDirectives.add(node); - } else { - detectedAbstractDirectives.delete(node); - } - ambiguousClasses.delete(node); - break; - } - }); - }; - - // Check inheritance of any detected abstract directive. We want to remove - // classes that are not eligible abstract directives due to inheritance. i.e. - // if a class extends from a component, it cannot be a derived abstract directive. - checkInheritanceOfClasses(detectedAbstractDirectives); - // Update the class declarations to reflect the detected abstract directives. This is - // then used later when we check for undecorated classes that inherit from an abstract - // directive and need to be decorated. - detectedAbstractDirectives.forEach( - n => declarations.set(n, DeclarationType.ABSTRACT_DIRECTIVE)); - // Check ambiguous and undecorated classes if they inherit from an abstract directive. - // If they do, we want to migrate them too. See function definition for more details. - checkInheritanceOfClasses(ambiguousClasses); - checkInheritanceOfClasses(undecoratedClasses); - - return {detectedAbstractDirectives, ambiguousClasses}; - } - - /** - * Analyzes the given class declaration by determining whether the class - * is a directive, is an abstract directive, or uses Angular features. - */ - private _analyzeClassDeclaration(node: ts.ClassDeclaration): AnalyzedClass { - const ngDecorators = node.decorators && getAngularDecorators(this.typeChecker, node.decorators); - const inferredKind = this._determineClassKind(node); - if (ngDecorators === undefined || ngDecorators.length === 0) { - return {decoratedType: null, inferredKind}; - } - const directiveDecorator = ngDecorators.find(({name}) => name === 'Directive'); - const componentDecorator = ngDecorators.find(({name}) => name === 'Component'); - const pipeDecorator = ngDecorators.find(({name}) => name === 'Pipe'); - const injectableDecorator = ngDecorators.find(({name}) => name === 'Injectable'); - const isAbstractDirective = - directiveDecorator !== undefined && this._isAbstractDirective(directiveDecorator); - - let decoratedType: DeclarationType|null = null; - if (isAbstractDirective) { - decoratedType = DeclarationType.ABSTRACT_DIRECTIVE; - } else if (componentDecorator !== undefined) { - decoratedType = DeclarationType.COMPONENT; - } else if (directiveDecorator !== undefined) { - decoratedType = DeclarationType.DIRECTIVE; - } else if (pipeDecorator !== undefined) { - decoratedType = DeclarationType.PIPE; - } else if (injectableDecorator !== undefined) { - decoratedType = DeclarationType.INJECTABLE; - } - return {decoratedType, inferredKind}; - } - - /** - * Checks whether the given decorator resolves to an abstract directive. An directive is - * considered "abstract" if there is no selector specified. - */ - private _isAbstractDirective({node}: NgDecorator): boolean { - const metadataArgs = node.expression.arguments; - if (metadataArgs.length === 0) { - return true; - } - const metadataExpr = unwrapExpression(metadataArgs[0]); - if (!ts.isObjectLiteralExpression(metadataExpr)) { - return false; - } - const metadata = this.compilerCliMigrationsModule.reflectObjectLiteral(metadataExpr); - if (!metadata.has('selector')) { - return false; - } - const selector = this.partialEvaluator.evaluate(metadata.get('selector')!); - return selector == null; - } - - /** - * Determines the kind of a given class in terms of Angular. The method checks - * whether the given class has members that indicate the use of Angular features. - * e.g. lifecycle hooks or decorated members like `@Input` or `@Output` are - * considered Angular features.. - */ - private _determineClassKind(node: ts.ClassDeclaration): InferredKind { - let usage = InferredKind.UNKNOWN; - - for (const member of node.members) { - const propertyName = member.name !== undefined ? getPropertyNameText(member.name) : null; - - // If the class declares any of the known directive lifecycle hooks, we can - // immediately exit the loop as the class is guaranteed to be a directive. - if (propertyName !== null && DIRECTIVE_LIFECYCLE_HOOKS.has(propertyName)) { - return InferredKind.DIRECTIVE; - } - - const ngDecorators = member.decorators !== undefined ? - getAngularDecorators(this.typeChecker, member.decorators) : - []; - for (const {name} of ngDecorators) { - if (DIRECTIVE_FIELD_DECORATORS.has(name)) { - return InferredKind.DIRECTIVE; - } - } - - // If the class declares any of the lifecycle hooks that do not guarantee that - // the given class is a directive, update the kind and continue looking for other - // members that would unveil a more specific kind (i.e. being a directive). - if (propertyName !== null && AMBIGUOUS_LIFECYCLE_HOOKS.has(propertyName)) { - usage = InferredKind.AMBIGUOUS; - } - } - - return usage; - } - - /** - * Checks whether a given class has been reported as ambiguous in previous - * migration run. e.g. when build targets are migrated first, and then test - * targets that have an overlap with build source files, the same class - * could be detected as ambiguous. - */ - private _hasBeenReportedAsAmbiguous(node: ts.ClassDeclaration): boolean { - const sourceFile = node.getSourceFile(); - const leadingComments = ts.getLeadingCommentRanges(sourceFile.text, node.pos); - if (leadingComments === undefined) { - return false; - } - return leadingComments.some( - ({kind, pos, end}) => kind === ts.SyntaxKind.SingleLineCommentTrivia && - sourceFile.text.substring(pos, end).includes(`TODO: ${AMBIGUOUS_CLASS_TODO}`)); - } -} diff --git a/packages/core/schematics/migrations/undecorated-classes-with-decorated-fields/update_recorder.ts b/packages/core/schematics/migrations/undecorated-classes-with-decorated-fields/update_recorder.ts deleted file mode 100644 index baee6f67f70..00000000000 --- a/packages/core/schematics/migrations/undecorated-classes-with-decorated-fields/update_recorder.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; -import {ImportManagerUpdateRecorder} from '../../utils/import_manager'; - -/** - * Update recorder interface that is used to transform source files - * in a non-colliding way. - */ -export interface UpdateRecorder extends ImportManagerUpdateRecorder { - addClassDecorator(node: ts.ClassDeclaration, text: string): void; - addClassTodo(node: ts.ClassDeclaration, message: string): void; - commitUpdate(): void; -} diff --git a/packages/core/schematics/migrations/undecorated-classes-with-di/BUILD.bazel b/packages/core/schematics/migrations/undecorated-classes-with-di/BUILD.bazel deleted file mode 100644 index 138542abe74..00000000000 --- a/packages/core/schematics/migrations/undecorated-classes-with-di/BUILD.bazel +++ /dev/null @@ -1,22 +0,0 @@ -load("//tools:defaults.bzl", "ts_library") - -ts_library( - name = "undecorated-classes-with-di", - srcs = glob(["**/*.ts"]), - tsconfig = "//packages/core/schematics:tsconfig.json", - visibility = [ - "//packages/core/schematics:__pkg__", - "//packages/core/schematics/migrations/undecorated-classes/google3:__pkg__", - "//packages/core/schematics/test:__pkg__", - ], - deps = [ - "//packages/compiler", - "//packages/compiler-cli", - "//packages/core", - "//packages/core/schematics/utils", - "@npm//@angular-devkit/core", - "@npm//@angular-devkit/schematics", - "@npm//@types/node", - "@npm//typescript", - ], -) diff --git a/packages/core/schematics/migrations/undecorated-classes-with-di/create_ngc_program.ts b/packages/core/schematics/migrations/undecorated-classes-with-di/create_ngc_program.ts deleted file mode 100644 index a97f4aa16d0..00000000000 --- a/packages/core/schematics/migrations/undecorated-classes-with-di/create_ngc_program.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 type {AotCompiler} from '@angular/compiler'; -import type {CompilerHost} from '@angular/compiler-cli'; -import ts from 'typescript'; - -/** Creates an NGC program that can be used to read and parse metadata for files. */ -export function createNgcProgram( - compilerCliModule: typeof import('@angular/compiler-cli'), - createHost: (options: ts.CompilerOptions) => CompilerHost, tsconfigPath: string) { - const {rootNames, options} = compilerCliModule.readConfiguration(tsconfigPath); - - // https://github.com/angular/angular/commit/ec4381dd401f03bded652665b047b6b90f2b425f made Ivy - // the default. This breaks the assumption that "createProgram" from compiler-cli returns the - // NGC program. In order to ensure that the migration runs properly, we set "enableIvy" to false. - options.enableIvy = false; - - // Libraries which have been generated with CLI versions past v6.2.0, explicitly set the - // flat-module options in their tsconfig files. This is problematic because by default, - // those tsconfig files do not specify explicit source files which can be considered as - // entry point for the flat-module bundle. Therefore the `@angular/compiler-cli` is unable - // to determine the flat module entry point and throws a compile error. This is not an issue - // for the libraries built with `ng-packagr`, because the tsconfig files are modified in-memory - // to specify an explicit flat module entry-point. Our migrations don't distinguish between - // libraries and applications, and also don't run `ng-packagr`. To ensure that such libraries - // can be successfully migrated, we remove the flat-module options to eliminate the flat module - // entry-point requirement. More context: https://github.com/angular/angular/issues/34985. - options.flatModuleId = undefined; - options.flatModuleOutFile = undefined; - - const host = createHost(options); - - // For this migration, we never need to read resources and can just return - // an empty string for requested resources. We need to handle requested resources - // because our created NGC compiler program does not know about special resolutions - // which are set up by the Angular CLI. i.e. resolving stylesheets through "tilde". - host.readResource = () => ''; - host.resourceNameToFileName = () => '$fake-file$'; - - const ngcProgram = compilerCliModule.createProgram({rootNames, options, host}); - - // The "AngularCompilerProgram" does not expose the "AotCompiler" instance, nor does it - // expose the logic that is necessary to analyze the determined modules. We work around - // this by just accessing the necessary private properties using the bracket notation. - const compiler: AotCompiler = (ngcProgram as any)['compiler']; - const program = ngcProgram.getTsProgram(); - - return {host, ngcProgram, program, compiler}; -} diff --git a/packages/core/schematics/migrations/undecorated-classes-with-di/decorator_rewrite/convert_directive_metadata.ts b/packages/core/schematics/migrations/undecorated-classes-with-di/decorator_rewrite/convert_directive_metadata.ts deleted file mode 100644 index d70ec740b27..00000000000 --- a/packages/core/schematics/migrations/undecorated-classes-with-di/decorator_rewrite/convert_directive_metadata.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 type {StaticSymbol} from '@angular/compiler'; -import ts from 'typescript'; - -/** Error that will be thrown if an unexpected value needs to be converted. */ -export class UnexpectedMetadataValueError extends Error {} - -/** - * Converts a directive metadata object into a TypeScript expression. Throws - * if metadata cannot be cleanly converted. - */ -export function convertDirectiveMetadataToExpression( - compilerModule: typeof import('@angular/compiler'), metadata: any, - resolveSymbolImport: (symbol: StaticSymbol) => string | null, - createImport: (moduleName: string, name: string) => ts.Expression, - convertProperty?: (key: string, value: any) => ts.Expression | null): ts.Expression { - if (typeof metadata === 'string') { - return ts.createStringLiteral(metadata); - } else if (Array.isArray(metadata)) { - return ts.createArrayLiteral(metadata.map( - el => convertDirectiveMetadataToExpression( - compilerModule, el, resolveSymbolImport, createImport, convertProperty))); - } else if (typeof metadata === 'number') { - return ts.createNumericLiteral(metadata.toString()); - } else if (typeof metadata === 'boolean') { - return metadata ? ts.createTrue() : ts.createFalse(); - } else if (typeof metadata === 'undefined') { - return ts.createIdentifier('undefined'); - } else if (typeof metadata === 'bigint') { - return ts.createBigIntLiteral(metadata.toString()); - } else if (typeof metadata === 'object') { - // In case there is a static symbol object part of the metadata, try to resolve - // the import expression of the symbol. If no import path could be resolved, an - // error will be thrown as the symbol cannot be converted into TypeScript AST. - if (metadata instanceof compilerModule.StaticSymbol) { - const resolvedImport = resolveSymbolImport(metadata); - if (resolvedImport === null) { - throw new UnexpectedMetadataValueError(); - } - return createImport(resolvedImport, metadata.name); - } - - const literalProperties: ts.PropertyAssignment[] = []; - - for (const key of Object.keys(metadata)) { - const metadataValue = metadata[key]; - let propertyValue: ts.Expression|null = null; - - // Allows custom conversion of properties in an object. This is useful for special - // cases where we don't want to store the enum values as integers, but rather use the - // real enum symbol. e.g. instead of `2` we want to use `ViewEncapsulation.None`. - if (convertProperty) { - propertyValue = convertProperty(key, metadataValue); - } - - // In case the property value has not been assigned to an expression, we convert - // the resolved metadata value into a TypeScript expression. - if (propertyValue === null) { - propertyValue = convertDirectiveMetadataToExpression( - compilerModule, metadataValue, resolveSymbolImport, createImport, convertProperty); - } - - literalProperties.push(ts.createPropertyAssignment(getPropertyName(key), propertyValue)); - } - - return ts.createObjectLiteral(literalProperties, true); - } - - throw new UnexpectedMetadataValueError(); -} - -/** - * Gets a valid property name from the given text. If the text cannot be used - * as unquoted identifier, the name will be wrapped in a string literal. - */ -function getPropertyName(name: string): string|ts.StringLiteral { - // Matches the most common identifiers that do not need quotes. Constructing a - // regular expression that matches the ECMAScript specification in order to determine - // whether quotes are needed is out of scope for this migration. For those more complex - // property names, we just always use quotes (when constructing AST from metadata). - if (/^[a-zA-Z_$]+$/.test(name)) { - return name; - } - return ts.createStringLiteral(name); -} diff --git a/packages/core/schematics/migrations/undecorated-classes-with-di/decorator_rewrite/decorator_rewriter.ts b/packages/core/schematics/migrations/undecorated-classes-with-di/decorator_rewrite/decorator_rewriter.ts deleted file mode 100644 index 12bb8f98a21..00000000000 --- a/packages/core/schematics/migrations/undecorated-classes-with-di/decorator_rewrite/decorator_rewriter.ts +++ /dev/null @@ -1,135 +0,0 @@ - -/** - * @license - * Copyright Google LLC 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 type {AotCompiler} from '@angular/compiler'; -import ts from 'typescript'; - -import {ImportManager} from '../../../utils/import_manager'; -import {NgDecorator} from '../../../utils/ng_decorators'; -import {unwrapExpression} from '../../../utils/typescript/functions'; - -import {ImportRewriteTransformerFactory, UnresolvedIdentifierError} from './import_rewrite_visitor'; - - -/** - * Class that can be used to copy decorators to a new location. The rewriter ensures that - * identifiers and imports are rewritten to work in the new file location. Fields in a - * decorator that cannot be cleanly copied will be copied with a comment explaining that - * imports and identifiers need to be adjusted manually. - */ -export class DecoratorRewriter { - previousSourceFile: ts.SourceFile|null = null; - newSourceFile: ts.SourceFile|null = null; - - newProperties: ts.ObjectLiteralElementLike[] = []; - nonCopyableProperties: ts.ObjectLiteralElementLike[] = []; - - private importRewriterFactory = new ImportRewriteTransformerFactory( - this.importManager, this.typeChecker, this.compiler['_host']); - - constructor( - private importManager: ImportManager, private typeChecker: ts.TypeChecker, - private compiler: AotCompiler) {} - - rewrite(ngDecorator: NgDecorator, newSourceFile: ts.SourceFile): ts.Decorator { - const decorator = ngDecorator.node; - - // Reset the previous state of the decorator rewriter. - this.newProperties = []; - this.nonCopyableProperties = []; - this.newSourceFile = newSourceFile; - this.previousSourceFile = decorator.getSourceFile(); - - // If the decorator will be added to the same source file it currently - // exists in, we don't need to rewrite any paths or add new imports. - if (this.previousSourceFile === newSourceFile) { - return this._createDecorator(decorator.expression); - } - - const oldCallExpr = decorator.expression; - - if (!oldCallExpr.arguments.length) { - // Re-use the original decorator if there are no arguments and nothing needs - // to be sanitized or rewritten. - return this._createDecorator(decorator.expression); - } - - const metadata = unwrapExpression(oldCallExpr.arguments[0]); - if (!ts.isObjectLiteralExpression(metadata)) { - // Re-use the original decorator as there is no metadata that can be sanitized. - return this._createDecorator(decorator.expression); - } - - metadata.properties.forEach(prop => { - // We don't handle spread assignments, accessors or method declarations automatically - // as it involves more advanced static analysis and these type of properties are not - // picked up by ngc either. - if (ts.isSpreadAssignment(prop) || ts.isAccessor(prop) || ts.isMethodDeclaration(prop)) { - this.nonCopyableProperties.push(prop); - return; - } - - const sanitizedProp = this._sanitizeMetadataProperty(prop); - if (sanitizedProp !== null) { - this.newProperties.push(sanitizedProp); - } else { - this.nonCopyableProperties.push(prop); - } - }); - - // In case there is at least one non-copyable property, we add a leading comment to - // the first property assignment in order to ask the developer to manually manage - // imports and do path rewriting for these properties. - if (this.nonCopyableProperties.length !== 0) { - ['The following fields were copied from the base class,', - 'but could not be updated automatically to work in the', - 'new file location. Please add any required imports for', 'the properties below:'] - .forEach( - text => ts.addSyntheticLeadingComment( - this.nonCopyableProperties[0], ts.SyntaxKind.SingleLineCommentTrivia, ` ${text}`, - true)); - } - - // Note that we don't update the decorator as we don't want to copy potential leading - // comments of the decorator. This is necessary because otherwise comments from the - // copied decorator end up describing the new class (which is not always correct). - return this._createDecorator(ts.createCall( - this.importManager.addImportToSourceFile( - newSourceFile, ngDecorator.name, ngDecorator.moduleName), - undefined, [ts.updateObjectLiteral( - metadata, [...this.newProperties, ...this.nonCopyableProperties])])); - } - - /** Creates a new decorator with the given expression. */ - private _createDecorator(expr: ts.Expression): ts.Decorator { - // Note that we don't update the decorator as we don't want to copy potential leading - // comments of the decorator. This is necessary because otherwise comments from the - // copied decorator end up describing the new class (which is not always correct). - return ts.createDecorator(expr); - } - - /** - * Sanitizes a metadata property by ensuring that all contained identifiers - * are imported in the target source file. - */ - private _sanitizeMetadataProperty(prop: ts.ObjectLiteralElementLike): ts.ObjectLiteralElementLike - |null { - try { - return ts - .transform(prop, [ctx => this.importRewriterFactory.create(ctx, this.newSourceFile!)]) - .transformed[0]; - } catch (e) { - // If the error is for an unresolved identifier, we want to return "null" because - // such object literal elements could be added to the non-copyable properties. - if (e instanceof UnresolvedIdentifierError) { - return null; - } - throw e; - } - } -} diff --git a/packages/core/schematics/migrations/undecorated-classes-with-di/decorator_rewrite/import_rewrite_visitor.ts b/packages/core/schematics/migrations/undecorated-classes-with-di/decorator_rewrite/import_rewrite_visitor.ts deleted file mode 100644 index 4f2f1698e43..00000000000 --- a/packages/core/schematics/migrations/undecorated-classes-with-di/decorator_rewrite/import_rewrite_visitor.ts +++ /dev/null @@ -1,143 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 type {AotCompilerHost} from '@angular/compiler'; -import {dirname, resolve} from 'path'; -import ts from 'typescript'; - -import {ImportManager} from '../../../utils/import_manager'; -import {getImportOfIdentifier, Import} from '../../../utils/typescript/imports'; -import {getValueSymbolOfDeclaration} from '../../../utils/typescript/symbol'; - -import {getPosixPath} from './path_format'; -import {getExportSymbolsOfFile, ResolvedExport} from './source_file_exports'; - - -/** - * Factory that creates a TypeScript transformer which ensures that - * referenced identifiers are available at the target file location. - * - * Imports cannot be just added as sometimes identifiers collide in the - * target source file and the identifier needs to be aliased. - */ -export class ImportRewriteTransformerFactory { - private sourceFileExports = new Map(); - - constructor( - private importManager: ImportManager, private typeChecker: ts.TypeChecker, - private compilerHost: AotCompilerHost) {} - - create(ctx: ts.TransformationContext, newSourceFile: ts.SourceFile): - ts.Transformer { - const visitNode: ts.Visitor = (node: ts.Node) => { - if (ts.isIdentifier(node)) { - // Record the identifier reference and return the new identifier. The identifier - // name can change if the generated import uses an namespaced import or aliased - // import identifier (to avoid collisions). - return this._recordIdentifierReference(node, newSourceFile); - } - - return ts.visitEachChild(node, visitNode, ctx); - }; - - return (node: T) => ts.visitNode(node, visitNode); - } - - private _recordIdentifierReference(node: ts.Identifier, targetSourceFile: ts.SourceFile): - ts.Node { - // For object literal elements we don't want to check identifiers that describe the - // property name. These identifiers do not refer to a value but rather to a property - // name and therefore don't need to be imported. The exception is that for shorthand - // property assignments the "name" identifier is both used as value and property name. - if (ts.isObjectLiteralElementLike(node.parent) && - !ts.isShorthandPropertyAssignment(node.parent) && node.parent.name === node) { - return node; - } - - const resolvedImport = getImportOfIdentifier(this.typeChecker, node); - const sourceFile = node.getSourceFile(); - - if (resolvedImport) { - const symbolName = resolvedImport.name; - const moduleFileName = - this.compilerHost.moduleNameToFileName(resolvedImport.importModule, sourceFile.fileName); - - // In case the identifier refers to an export in the target source file, we need to use - // the local identifier in the scope of the target source file. This is necessary because - // the export could be aliased and the alias is not available to the target source file. - if (moduleFileName && resolve(moduleFileName) === resolve(targetSourceFile.fileName)) { - const resolvedExport = - this._getSourceFileExports(targetSourceFile).find(e => e.exportName === symbolName); - if (resolvedExport) { - return resolvedExport.identifier; - } - } - - return this.importManager.addImportToSourceFile( - targetSourceFile, symbolName, - this._rewriteModuleImport(resolvedImport, targetSourceFile)); - } else { - let symbol = getValueSymbolOfDeclaration(node, this.typeChecker); - - if (symbol) { - // If the symbol refers to a shorthand property assignment, we want to resolve the - // value symbol of the shorthand property assignment. This is necessary because the - // value symbol is ambiguous for shorthand property assignment identifiers as the - // identifier resolves to both property name and property value. - if (symbol.valueDeclaration && ts.isShorthandPropertyAssignment(symbol.valueDeclaration)) { - symbol = this.typeChecker.getShorthandAssignmentValueSymbol(symbol.valueDeclaration); - } - - const resolvedExport = - this._getSourceFileExports(sourceFile).find(e => e.symbol === symbol); - - if (resolvedExport) { - return this.importManager.addImportToSourceFile( - targetSourceFile, resolvedExport.exportName, - getPosixPath(this.compilerHost.fileNameToModuleName( - sourceFile.fileName, targetSourceFile.fileName))); - } - } - - // The referenced identifier cannot be imported. In that case we throw an exception - // which can be handled outside of the transformer. - throw new UnresolvedIdentifierError(); - } - } - - /** - * Gets the resolved exports of a given source file. Exports are cached - * for subsequent calls. - */ - private _getSourceFileExports(sourceFile: ts.SourceFile): ResolvedExport[] { - if (this.sourceFileExports.has(sourceFile)) { - return this.sourceFileExports.get(sourceFile)!; - } - - const sourceFileExports = getExportSymbolsOfFile(sourceFile, this.typeChecker); - this.sourceFileExports.set(sourceFile, sourceFileExports); - return sourceFileExports; - } - - /** Rewrites a module import to be relative to the target file location. */ - private _rewriteModuleImport(resolvedImport: Import, newSourceFile: ts.SourceFile): string { - if (!resolvedImport.importModule.startsWith('.')) { - return resolvedImport.importModule; - } - - const importFilePath = resolvedImport.node.getSourceFile().fileName; - const resolvedModulePath = resolve(dirname(importFilePath), resolvedImport.importModule); - const relativeModuleName = - this.compilerHost.fileNameToModuleName(resolvedModulePath, newSourceFile.fileName); - - return getPosixPath(relativeModuleName); - } -} - -/** Error that will be thrown if a given identifier cannot be resolved. */ -export class UnresolvedIdentifierError extends Error {} diff --git a/packages/core/schematics/migrations/undecorated-classes-with-di/decorator_rewrite/path_format.ts b/packages/core/schematics/migrations/undecorated-classes-with-di/decorator_rewrite/path_format.ts deleted file mode 100644 index 1528bfc8008..00000000000 --- a/packages/core/schematics/migrations/undecorated-classes-with-di/decorator_rewrite/path_format.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 {normalize} from 'path'; - -/** Normalizes the specified path to conform with the posix path format. */ -export function getPosixPath(pathString: string) { - const normalized = normalize(pathString).replace(/\\/g, '/'); - if (!normalized.startsWith('.')) { - return `./${normalized}`; - } - return normalized; -} diff --git a/packages/core/schematics/migrations/undecorated-classes-with-di/decorator_rewrite/source_file_exports.ts b/packages/core/schematics/migrations/undecorated-classes-with-di/decorator_rewrite/source_file_exports.ts deleted file mode 100644 index 93ae905421a..00000000000 --- a/packages/core/schematics/migrations/undecorated-classes-with-di/decorator_rewrite/source_file_exports.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; -import {getValueSymbolOfDeclaration} from '../../../utils/typescript/symbol'; - -export interface ResolvedExport { - symbol: ts.Symbol; - exportName: string; - identifier: ts.Identifier; -} - -/** Computes the resolved exports of a given source file. */ -export function getExportSymbolsOfFile( - sf: ts.SourceFile, typeChecker: ts.TypeChecker): ResolvedExport[] { - const exports: {exportName: string, identifier: ts.Identifier}[] = []; - const resolvedExports: ResolvedExport[] = []; - - ts.forEachChild(sf, function visitNode(node) { - if (ts.isClassDeclaration(node) || ts.isFunctionDeclaration(node) || - ts.isInterfaceDeclaration(node) && - (ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Export) !== 0) { - if (node.name) { - exports.push({exportName: node.name.text, identifier: node.name}); - } - } else if (ts.isVariableStatement(node)) { - for (const decl of node.declarationList.declarations) { - visitNode(decl); - } - } else if (ts.isVariableDeclaration(node)) { - if ((ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) != 0 && - ts.isIdentifier(node.name)) { - exports.push({exportName: node.name.text, identifier: node.name}); - } - } else if (ts.isExportDeclaration(node)) { - const {moduleSpecifier, exportClause} = node; - if (!moduleSpecifier && exportClause && ts.isNamedExports(exportClause)) { - exportClause.elements.forEach(el => exports.push({ - exportName: el.name.text, - identifier: el.propertyName ? el.propertyName : el.name - })); - } - } - }); - - exports.forEach(({identifier, exportName}) => { - const symbol = getValueSymbolOfDeclaration(identifier, typeChecker); - if (symbol) { - resolvedExports.push({symbol, identifier, exportName}); - } - }); - - return resolvedExports; -} diff --git a/packages/core/schematics/migrations/undecorated-classes-with-di/index.ts b/packages/core/schematics/migrations/undecorated-classes-with-di/index.ts deleted file mode 100644 index 95479905fed..00000000000 --- a/packages/core/schematics/migrations/undecorated-classes-with-di/index.ts +++ /dev/null @@ -1,253 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 {logging} from '@angular-devkit/core'; -import {Rule, SchematicContext, SchematicsException, Tree} from '@angular-devkit/schematics'; -import type {AotCompiler} from '@angular/compiler'; -import type {Diagnostic as NgDiagnostic} from '@angular/compiler-cli'; -import {relative} from 'path'; -import ts from 'typescript'; - -import {loadCompilerCliMigrationsModule, loadEsmModule} from '../../utils/load_esm'; -import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; -import {canMigrateFile, createMigrationCompilerHost} from '../../utils/typescript/compiler_host'; - -import {createNgcProgram} from './create_ngc_program'; -import {NgDeclarationCollector} from './ng_declaration_collector'; -import {UndecoratedClassesTransform} from './transform'; -import {UpdateRecorder} from './update_recorder'; - -const MIGRATION_RERUN_MESSAGE = 'Migration can be rerun with: "ng update @angular/core ' + - '--migrate-only migration-v9-undecorated-classes-with-di"'; - -const MIGRATION_AOT_FAILURE = 'This migration uses the Angular compiler internally and ' + - 'therefore projects that no longer build successfully after the update cannot run ' + - 'the migration. Please ensure there are no AOT compilation errors and rerun the migration.'; - -/** Entry point for the V9 "undecorated-classes-with-di" migration. */ -export default function(): Rule { - return async (tree: Tree, ctx: SchematicContext) => { - const {buildPaths} = await getProjectTsConfigPaths(tree); - const basePath = process.cwd(); - const failures: string[] = []; - let programError = false; - - if (!buildPaths.length) { - throw new SchematicsException( - 'Could not find any tsconfig file. Cannot migrate undecorated derived classes and ' + - 'undecorated base classes which use DI.'); - } - - let compilerModule; - try { - // Load ESM `@angular/compiler` using the TypeScript dynamic import workaround. - // Once TypeScript provides support for keeping the dynamic import this workaround can be - // changed to a direct dynamic import. - compilerModule = await loadEsmModule('@angular/compiler'); - } catch (e) { - throw new SchematicsException( - `Unable to load the '@angular/compiler' package. Details: ${e.message}`); - } - - let compilerCliModule; - try { - // Load ESM `@angular/compiler-cli` using the TypeScript dynamic import workaround. - // Once TypeScript provides support for keeping the dynamic import this workaround can be - // changed to a direct dynamic import. - compilerCliModule = - await loadEsmModule('@angular/compiler-cli'); - } catch (e) { - throw new SchematicsException( - `Unable to load the '@angular/compiler-cli' package. Details: ${e.message}`); - } - - let coreModule; - try { - // Load ESM `@angular/compiler-cli` using the TypeScript dynamic import workaround. - // Once TypeScript provides support for keeping the dynamic import this workaround can be - // changed to a direct dynamic import. - coreModule = await loadEsmModule('@angular/core'); - } catch (e) { - throw new SchematicsException( - `Unable to load the '@angular/core' package. Details: ${e.message}`); - } - - let compilerCliMigrationsModule; - try { - // Load ESM `@angular/compiler/private/migrations` using the TypeScript dynamic import - // workaround. Once TypeScript provides support for keeping the dynamic import this workaround - // can be changed to a direct dynamic import. - compilerCliMigrationsModule = await loadCompilerCliMigrationsModule(); - } catch (e) { - throw new SchematicsException( - `Unable to load the '@angular/compiler-cli' package. Details: ${e.message}`); - } - - for (const tsconfigPath of buildPaths) { - const result = runUndecoratedClassesMigration( - tree, tsconfigPath, basePath, ctx.logger, compilerModule, compilerCliModule, - compilerCliMigrationsModule, coreModule); - failures.push(...result.failures); - programError = programError || !!result.programError; - } - - if (programError) { - ctx.logger.info('Could not migrate all undecorated classes that use dependency'); - ctx.logger.info('injection. Some project targets could not be analyzed due to'); - ctx.logger.info('TypeScript program failures.\n'); - ctx.logger.info(`${MIGRATION_RERUN_MESSAGE}\n`); - - if (failures.length) { - ctx.logger.info('Please manually fix the following failures and re-run the'); - ctx.logger.info('migration once the TypeScript program failures are resolved.'); - failures.forEach(message => ctx.logger.warn(`⮑ ${message}`)); - } - } else if (failures.length) { - ctx.logger.info('Could not migrate all undecorated classes that use dependency'); - ctx.logger.info('injection. Please manually fix the following failures:'); - failures.forEach(message => ctx.logger.warn(`⮑ ${message}`)); - } - }; -} - -function runUndecoratedClassesMigration( - tree: Tree, tsconfigPath: string, basePath: string, logger: logging.LoggerApi, - compilerModule: typeof import('@angular/compiler'), - compilerCliModule: typeof import('@angular/compiler-cli'), - compilerCliMigrationsModule: typeof import('@angular/compiler-cli/private/migrations'), - coreModule: typeof import('@angular/core')): {failures: string[], programError?: boolean} { - const failures: string[] = []; - const programData = - gracefullyCreateProgram(tree, basePath, tsconfigPath, logger, compilerCliModule); - - // Gracefully exit if the program could not be created. - if (programData === null) { - return {failures: [], programError: true}; - } - - const {program, compiler} = programData; - const typeChecker = program.getTypeChecker(); - - const declarationCollector = new NgDeclarationCollector(typeChecker, compilerCliMigrationsModule); - const sourceFiles = - program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program)); - - // Analyze source files by detecting all directives, components and providers. - sourceFiles.forEach(sourceFile => declarationCollector.visitNode(sourceFile)); - - const {decoratedDirectives, decoratedProviders, undecoratedDeclarations} = declarationCollector; - const transform = new UndecoratedClassesTransform( - typeChecker, compiler, getUpdateRecorder, compilerModule, coreModule); - const updateRecorders = new Map(); - - // Run the migrations for decorated providers and both decorated and undecorated - // directives. The transform failures are collected and converted into human-readable - // failures which can be printed to the console. - [...transform.migrateDecoratedDirectives(decoratedDirectives), - ...transform.migrateDecoratedProviders(decoratedProviders), - ...transform.migrateUndecoratedDeclarations(Array.from(undecoratedDeclarations))] - .forEach(({node, message}) => { - const nodeSourceFile = node.getSourceFile(); - const relativeFilePath = relative(basePath, nodeSourceFile.fileName); - const {line, character} = - ts.getLineAndCharacterOfPosition(node.getSourceFile(), node.getStart()); - failures.push(`${relativeFilePath}@${line + 1}:${character + 1}: ${message}`); - }); - - // Record the changes collected in the import manager and transformer. - transform.recordChanges(); - - // Walk through each update recorder and commit the update. We need to commit the - // updates in batches per source file as there can be only one recorder per source - // file in order to avoid shifted character offsets. - updateRecorders.forEach(recorder => recorder.commitUpdate()); - - return {failures}; - - /** Gets the update recorder for the specified source file. */ - function getUpdateRecorder(sourceFile: ts.SourceFile): UpdateRecorder { - if (updateRecorders.has(sourceFile)) { - return updateRecorders.get(sourceFile)!; - } - const treeRecorder = tree.beginUpdate(relative(basePath, sourceFile.fileName)); - const recorder: UpdateRecorder = { - addClassComment(node: ts.ClassDeclaration, text: string) { - treeRecorder.insertLeft(node.members.pos, `\n // ${text}\n`); - }, - addClassDecorator(node: ts.ClassDeclaration, text: string) { - // New imports should be inserted at the left while decorators should be inserted - // at the right in order to ensure that imports are inserted before the decorator - // if the start position of import and decorator is the source file start. - treeRecorder.insertRight(node.getStart(), `${text}\n`); - }, - addNewImport(start: number, importText: string) { - // New imports should be inserted at the left while decorators should be inserted - // at the right in order to ensure that imports are inserted before the decorator - // if the start position of import and decorator is the source file start. - treeRecorder.insertLeft(start, importText); - }, - updateExistingImport(namedBindings: ts.NamedImports, newNamedBindings: string) { - treeRecorder.remove(namedBindings.getStart(), namedBindings.getWidth()); - treeRecorder.insertRight(namedBindings.getStart(), newNamedBindings); - }, - commitUpdate() { - tree.commitUpdate(treeRecorder); - } - }; - updateRecorders.set(sourceFile, recorder); - return recorder; - } -} - -function getErrorDiagnostics(diagnostics: ReadonlyArray) { - return diagnostics.filter(d => d.category === ts.DiagnosticCategory.Error); -} - -function gracefullyCreateProgram( - tree: Tree, basePath: string, tsconfigPath: string, logger: logging.LoggerApi, - compilerCliModule: typeof import('@angular/compiler-cli')): - {compiler: AotCompiler, program: ts.Program}|null { - try { - const {ngcProgram, host, program, compiler} = createNgcProgram( - compilerCliModule, (options) => createMigrationCompilerHost(tree, options, basePath), - tsconfigPath); - const syntacticDiagnostics = getErrorDiagnostics(ngcProgram.getTsSyntacticDiagnostics()); - const structuralDiagnostics = getErrorDiagnostics(ngcProgram.getNgStructuralDiagnostics()); - const configDiagnostics = getErrorDiagnostics( - [...program.getOptionsDiagnostics(), ...ngcProgram.getNgOptionDiagnostics()]); - - if (configDiagnostics.length) { - logger.warn( - `\nTypeScript project "${tsconfigPath}" has configuration errors. This could cause ` + - `an incomplete migration. Please fix the following failures and rerun the migration:`); - logger.error(ts.formatDiagnostics(configDiagnostics, host)); - return null; - } - - // Syntactic TypeScript errors can throw off the query analysis and therefore we want - // to notify the developer that we couldn't analyze parts of the project. Developers - // can just re-run the migration after fixing these failures. - if (syntacticDiagnostics.length) { - logger.warn( - `\nTypeScript project "${tsconfigPath}" has syntactical errors which could cause ` + - `an incomplete migration. Please fix the following failures and rerun the migration:`); - logger.error(ts.formatDiagnostics(syntacticDiagnostics, host)); - return null; - } - - if (structuralDiagnostics.length) { - throw new Error(ts.formatDiagnostics(structuralDiagnostics, host)); - } - - return {program, compiler}; - } catch (e) { - logger.warn(`\n${MIGRATION_AOT_FAILURE} The following project failed: ${tsconfigPath}\n`); - logger.error(`${e.toString()}\n`); - return null; - } -} diff --git a/packages/core/schematics/migrations/undecorated-classes-with-di/ng_declaration_collector.ts b/packages/core/schematics/migrations/undecorated-classes-with-di/ng_declaration_collector.ts deleted file mode 100644 index 99218da5228..00000000000 --- a/packages/core/schematics/migrations/undecorated-classes-with-di/ng_declaration_collector.ts +++ /dev/null @@ -1,144 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; - -import {getAngularDecorators, NgDecorator} from '../../utils/ng_decorators'; -import {getPropertyNameText} from '../../utils/typescript/property_name'; - - -/** - * Visitor that walks through specified TypeScript nodes and collects all defined - * directives and provider classes. Directives are separated by decorated and - * undecorated directives. - */ -export class NgDeclarationCollector { - /** List of resolved directives which are decorated. */ - decoratedDirectives: ts.ClassDeclaration[] = []; - - /** List of resolved providers which are decorated. */ - decoratedProviders: ts.ClassDeclaration[] = []; - - /** Set of resolved Angular declarations which are not decorated. */ - undecoratedDeclarations = new Set(); - - private evaluator; - - constructor( - public typeChecker: ts.TypeChecker, - private compilerCliMigrationsModule: - typeof import('@angular/compiler-cli/private/migrations')) { - this.evaluator = new compilerCliMigrationsModule.PartialEvaluator( - new compilerCliMigrationsModule.TypeScriptReflectionHost(typeChecker), typeChecker, - /* dependencyTracker */ null); - } - - visitNode(node: ts.Node) { - if (ts.isClassDeclaration(node)) { - this._visitClassDeclaration(node); - } - - ts.forEachChild(node, n => this.visitNode(n)); - } - - private _visitClassDeclaration(node: ts.ClassDeclaration) { - if (!node.decorators || !node.decorators.length) { - return; - } - - const ngDecorators = getAngularDecorators(this.typeChecker, node.decorators); - const ngModuleDecorator = ngDecorators.find(({name}) => name === 'NgModule'); - - if (hasDirectiveDecorator(node, this.typeChecker, ngDecorators)) { - this.decoratedDirectives.push(node); - } else if (hasInjectableDecorator(node, this.typeChecker, ngDecorators)) { - this.decoratedProviders.push(node); - } else if (ngModuleDecorator) { - this._visitNgModuleDecorator(ngModuleDecorator); - } - } - - private _visitNgModuleDecorator(decorator: NgDecorator) { - const decoratorCall = decorator.node.expression; - const metadata = decoratorCall.arguments[0]; - - if (!metadata || !ts.isObjectLiteralExpression(metadata)) { - return; - } - - let entryComponentsNode: ts.Expression|null = null; - let declarationsNode: ts.Expression|null = null; - - metadata.properties.forEach(p => { - if (!ts.isPropertyAssignment(p)) { - return; - } - - const name = getPropertyNameText(p.name); - - if (name === 'entryComponents') { - entryComponentsNode = p.initializer; - } else if (name === 'declarations') { - declarationsNode = p.initializer; - } - }); - - const values = []; - - // In case the module specifies the "entryComponents" field, walk through all - // resolved entry components and collect the referenced directives. - if (entryComponentsNode) { - values.push(this.evaluator.evaluate(entryComponentsNode)); - } - - // In case the module specifies the "declarations" field, walk through all - // resolved declarations and collect the referenced directives. - if (declarationsNode) { - values.push(this.evaluator.evaluate(declarationsNode)); - } - - // Flatten values and analyze references - for (const value of values.flat(Infinity)) { - if (value instanceof this.compilerCliMigrationsModule.Reference && - ts.isClassDeclaration(value.node) && - !hasNgDeclarationDecorator(value.node, this.typeChecker)) { - this.undecoratedDeclarations.add(value.node); - } - } - } -} - -/** Checks whether the given node has the "@Directive" or "@Component" decorator set. */ -export function hasDirectiveDecorator( - node: ts.ClassDeclaration, typeChecker: ts.TypeChecker, ngDecorators?: NgDecorator[]): boolean { - return (ngDecorators || getNgClassDecorators(node, typeChecker)) - .some(({name}) => name === 'Directive' || name === 'Component'); -} - - - -/** Checks whether the given node has the "@Injectable" decorator set. */ -export function hasInjectableDecorator( - node: ts.ClassDeclaration, typeChecker: ts.TypeChecker, ngDecorators?: NgDecorator[]): boolean { - return (ngDecorators || getNgClassDecorators(node, typeChecker)) - .some(({name}) => name === 'Injectable'); -} -/** Whether the given node has an explicit decorator that describes an Angular declaration. */ -export function hasNgDeclarationDecorator(node: ts.ClassDeclaration, typeChecker: ts.TypeChecker) { - return getNgClassDecorators(node, typeChecker) - .some(({name}) => name === 'Component' || name === 'Directive' || name === 'Pipe'); -} - -/** Gets all Angular decorators of a given class declaration. */ -export function getNgClassDecorators( - node: ts.ClassDeclaration, typeChecker: ts.TypeChecker): NgDecorator[] { - if (!node.decorators) { - return []; - } - return getAngularDecorators(typeChecker, node.decorators); -} diff --git a/packages/core/schematics/migrations/undecorated-classes-with-di/transform.ts b/packages/core/schematics/migrations/undecorated-classes-with-di/transform.ts deleted file mode 100644 index 2d0e8127786..00000000000 --- a/packages/core/schematics/migrations/undecorated-classes-with-di/transform.ts +++ /dev/null @@ -1,459 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 type {AotCompiler, AotCompilerHost, CompileMetadataResolver, StaticSymbol, StaticSymbolResolver, SummaryResolver} from '@angular/compiler'; -import ts from 'typescript'; - -import {ImportManager} from '../../utils/import_manager'; -import {getAngularDecorators} from '../../utils/ng_decorators'; -import {hasExplicitConstructor} from '../../utils/typescript/class_declaration'; -import {findBaseClassDeclarations} from '../../utils/typescript/find_base_classes'; -import {getImportOfIdentifier} from '../../utils/typescript/imports'; - -import {convertDirectiveMetadataToExpression, UnexpectedMetadataValueError} from './decorator_rewrite/convert_directive_metadata'; -import {DecoratorRewriter} from './decorator_rewrite/decorator_rewriter'; -import {hasDirectiveDecorator, hasInjectableDecorator} from './ng_declaration_collector'; -import {UpdateRecorder} from './update_recorder'; - - - -/** Resolved metadata of a declaration. */ -interface DeclarationMetadata { - metadata: any; - type: 'Component'|'Directive'|'Pipe'; -} - -export interface TransformFailure { - node: ts.Node; - message: string; -} - -export class UndecoratedClassesTransform { - private printer = ts.createPrinter({newLine: ts.NewLineKind.LineFeed}); - private importManager = new ImportManager(this.getUpdateRecorder, this.printer); - private decoratorRewriter = - new DecoratorRewriter(this.importManager, this.typeChecker, this.compiler); - - private compilerHost: AotCompilerHost; - private symbolResolver: StaticSymbolResolver; - private metadataResolver: CompileMetadataResolver; - - /** Set of class declarations which have been decorated with "@Directive". */ - private decoratedDirectives = new Set(); - /** Set of class declarations which have been decorated with "@Injectable" */ - private decoratedProviders = new Set(); - /** - * Set of class declarations which have been analyzed and need to specify - * an explicit constructor. - */ - private missingExplicitConstructorClasses = new Set(); - - constructor( - private typeChecker: ts.TypeChecker, private compiler: AotCompiler, - private getUpdateRecorder: (sf: ts.SourceFile) => UpdateRecorder, - private compilerModule: typeof import('@angular/compiler'), - private coreModule: typeof import('@angular/core')) { - this.symbolResolver = compiler['_symbolResolver']; - this.compilerHost = compiler['_host']; - this.metadataResolver = compiler['_metadataResolver']; - - // Unset the default error recorder so that the reflector will throw an exception - // if metadata cannot be resolved. - this.compiler.reflector['errorRecorder'] = undefined; - - // Disables that static symbols are resolved through summaries from within the static - // reflector. Summaries cannot be used for decorator serialization as decorators are - // omitted in summaries and the decorator can't be reconstructed from the directive summary. - this._disableSummaryResolution(); - } - - /** - * Migrates decorated directives which can potentially inherit a constructor - * from an undecorated base class. All base classes until the first one - * with an explicit constructor will be decorated with the abstract "@Directive()" - * decorator. See case 1 in the migration plan: https://hackmd.io/@alx/S1XKqMZeS - */ - migrateDecoratedDirectives(directives: ts.ClassDeclaration[]): TransformFailure[] { - return directives.reduce( - (failures, node) => failures.concat(this._migrateDirectiveBaseClass(node)), - [] as TransformFailure[]); - } - - /** - * Migrates decorated providers which can potentially inherit a constructor - * from an undecorated base class. All base classes until the first one - * with an explicit constructor will be decorated with the "@Injectable()". - */ - migrateDecoratedProviders(providers: ts.ClassDeclaration[]): TransformFailure[] { - return providers.reduce( - (failures, node) => failures.concat(this._migrateProviderBaseClass(node)), - [] as TransformFailure[]); - } - - private _migrateProviderBaseClass(node: ts.ClassDeclaration): TransformFailure[] { - return this._migrateDecoratedClassWithInheritedCtor( - node, symbol => this.metadataResolver.isInjectable(symbol), - node => this._addInjectableDecorator(node)); - } - - private _migrateDirectiveBaseClass(node: ts.ClassDeclaration): TransformFailure[] { - return this._migrateDecoratedClassWithInheritedCtor( - node, symbol => this.metadataResolver.isDirective(symbol), - node => this._addAbstractDirectiveDecorator(node)); - } - - - private _migrateDecoratedClassWithInheritedCtor( - node: ts.ClassDeclaration, isClassDecorated: (symbol: StaticSymbol) => boolean, - addClassDecorator: (node: ts.ClassDeclaration) => void): TransformFailure[] { - // In case the provider has an explicit constructor, we don't need to do anything - // because the class is already decorated and does not inherit a constructor. - if (hasExplicitConstructor(node)) { - return []; - } - - const orderedBaseClasses = findBaseClassDeclarations(node, this.typeChecker); - const undecoratedBaseClasses: ts.ClassDeclaration[] = []; - - for (let {node: baseClass, identifier} of orderedBaseClasses) { - const baseClassFile = baseClass.getSourceFile(); - - if (hasExplicitConstructor(baseClass)) { - // All classes in between the decorated class and the undecorated class - // that defines the constructor need to be decorated as well. - undecoratedBaseClasses.forEach(b => addClassDecorator(b)); - - if (baseClassFile.isDeclarationFile) { - const staticSymbol = this._getStaticSymbolOfIdentifier(identifier); - - // If the base class is decorated through metadata files, we don't - // need to add a comment to the derived class for the external base class. - if (staticSymbol && isClassDecorated(staticSymbol)) { - break; - } - - // Find the last class in the inheritance chain that is decorated and will be - // used as anchor for a comment explaining that the class that defines the - // constructor cannot be decorated automatically. - const lastDecoratedClass = - undecoratedBaseClasses[undecoratedBaseClasses.length - 1] || node; - return this._addMissingExplicitConstructorTodo(lastDecoratedClass); - } - - // Decorate the class that defines the constructor that is inherited. - addClassDecorator(baseClass); - break; - } - - // Add the class decorator for all base classes in the inheritance chain until - // the base class with the explicit constructor. The decorator will be only - // added for base classes which can be modified. - if (!baseClassFile.isDeclarationFile) { - undecoratedBaseClasses.push(baseClass); - } - } - return []; - } - - /** - * Adds the abstract "@Directive()" decorator to the given class in case there - * is no existing directive decorator. - */ - private _addAbstractDirectiveDecorator(baseClass: ts.ClassDeclaration) { - if (hasDirectiveDecorator(baseClass, this.typeChecker) || - this.decoratedDirectives.has(baseClass)) { - return; - } - - const baseClassFile = baseClass.getSourceFile(); - const recorder = this.getUpdateRecorder(baseClassFile); - const directiveExpr = - this.importManager.addImportToSourceFile(baseClassFile, 'Directive', '@angular/core'); - - const newDecorator = ts.createDecorator(ts.createCall(directiveExpr, undefined, [])); - const newDecoratorText = - this.printer.printNode(ts.EmitHint.Unspecified, newDecorator, baseClassFile); - - recorder.addClassDecorator(baseClass, newDecoratorText); - this.decoratedDirectives.add(baseClass); - } - - /** - * Adds the abstract "@Injectable()" decorator to the given class in case there - * is no existing directive decorator. - */ - private _addInjectableDecorator(baseClass: ts.ClassDeclaration) { - if (hasInjectableDecorator(baseClass, this.typeChecker) || - this.decoratedProviders.has(baseClass)) { - return; - } - - const baseClassFile = baseClass.getSourceFile(); - const recorder = this.getUpdateRecorder(baseClassFile); - const injectableExpr = - this.importManager.addImportToSourceFile(baseClassFile, 'Injectable', '@angular/core'); - - const newDecorator = ts.createDecorator(ts.createCall(injectableExpr, undefined, [])); - const newDecoratorText = - this.printer.printNode(ts.EmitHint.Unspecified, newDecorator, baseClassFile); - - recorder.addClassDecorator(baseClass, newDecoratorText); - this.decoratedProviders.add(baseClass); - } - - /** Adds a comment for adding an explicit constructor to the given class declaration. */ - private _addMissingExplicitConstructorTodo(node: ts.ClassDeclaration): TransformFailure[] { - // In case a todo comment has been already inserted to the given class, we don't - // want to add a comment or transform failure multiple times. - if (this.missingExplicitConstructorClasses.has(node)) { - return []; - } - this.missingExplicitConstructorClasses.add(node); - const recorder = this.getUpdateRecorder(node.getSourceFile()); - recorder.addClassComment(node, 'TODO: add explicit constructor'); - return [{node: node, message: 'Class needs to declare an explicit constructor.'}]; - } - - /** - * Migrates undecorated directives which were referenced in NgModule declarations. - * These directives inherit the metadata from a parent base class, but with Ivy - * these classes need to explicitly have a decorator for locality. The migration - * determines the inherited decorator and copies it to the undecorated declaration. - * - * Note that the migration serializes the metadata for external declarations - * where the decorator is not part of the source file AST. - * - * See case 2 in the migration plan: https://hackmd.io/@alx/S1XKqMZeS - */ - migrateUndecoratedDeclarations(directives: ts.ClassDeclaration[]): TransformFailure[] { - return directives.reduce( - (failures, node) => failures.concat(this._migrateDerivedDeclaration(node)), - [] as TransformFailure[]); - } - - private _migrateDerivedDeclaration(node: ts.ClassDeclaration): TransformFailure[] { - const targetSourceFile = node.getSourceFile(); - const orderedBaseClasses = findBaseClassDeclarations(node, this.typeChecker); - let newDecoratorText: string|null = null; - - for (let {node: baseClass, identifier} of orderedBaseClasses) { - // Before looking for decorators within the metadata or summary files, we - // try to determine the directive decorator through the source file AST. - if (baseClass.decorators) { - const ngDecorator = - getAngularDecorators(this.typeChecker, baseClass.decorators) - .find(({name}) => name === 'Component' || name === 'Directive' || name === 'Pipe'); - - if (ngDecorator) { - const newDecorator = this.decoratorRewriter.rewrite(ngDecorator, node.getSourceFile()); - newDecoratorText = this.printer.printNode( - ts.EmitHint.Unspecified, newDecorator, ngDecorator.node.getSourceFile()); - break; - } - } - - // If no metadata could be found within the source-file AST, try to find - // decorator data through Angular metadata and summary files. - const staticSymbol = this._getStaticSymbolOfIdentifier(identifier); - - // Check if the static symbol resolves to a class declaration with - // pipe or directive metadata. - if (!staticSymbol || - !(this.metadataResolver.isPipe(staticSymbol) || - this.metadataResolver.isDirective(staticSymbol))) { - continue; - } - - const metadata = this._resolveDeclarationMetadata(staticSymbol); - - // If no metadata could be resolved for the static symbol, print a failure message - // and ask the developer to manually migrate the class. This case is rare because - // usually decorator metadata is always present but just can't be read if a program - // only has access to summaries (this is a special case in google3). - if (!metadata) { - return [{ - node, - message: `Class cannot be migrated as the inherited metadata from ` + - `${identifier.getText()} cannot be converted into a decorator. Please manually - decorate the class.`, - }]; - } - - const newDecorator = this._constructDecoratorFromMetadata(metadata, targetSourceFile); - if (!newDecorator) { - const annotationType = metadata.type; - return [{ - node, - message: `Class cannot be migrated as the inherited @${annotationType} decorator ` + - `cannot be copied. Please manually add a @${annotationType} decorator.`, - }]; - } - - // In case the decorator could be constructed from the resolved metadata, use - // that decorator for the derived undecorated classes. - newDecoratorText = - this.printer.printNode(ts.EmitHint.Unspecified, newDecorator, targetSourceFile); - break; - } - - if (!newDecoratorText) { - return [{ - node, - message: - 'Class cannot be migrated as no directive/component/pipe metadata could be found. ' + - 'Please manually add a @Directive, @Component or @Pipe decorator.' - }]; - } - - this.getUpdateRecorder(targetSourceFile).addClassDecorator(node, newDecoratorText); - return []; - } - - /** Records all changes that were made in the import manager. */ - recordChanges() { - this.importManager.recordChanges(); - } - - /** - * Constructs a TypeScript decorator node from the specified declaration metadata. Returns - * null if the metadata could not be simplified/resolved. - */ - private _constructDecoratorFromMetadata( - directiveMetadata: DeclarationMetadata, targetSourceFile: ts.SourceFile): ts.Decorator|null { - try { - const decoratorExpr = convertDirectiveMetadataToExpression( - this.compilerModule, directiveMetadata.metadata, - staticSymbol => - this.compilerHost - .fileNameToModuleName(staticSymbol.filePath, targetSourceFile.fileName) - .replace(/\/index$/, ''), - (moduleName: string, name: string) => - this.importManager.addImportToSourceFile(targetSourceFile, name, moduleName), - (propertyName, value) => { - // Only normalize properties called "changeDetection" and "encapsulation" - // for "@Directive" and "@Component" annotations. - if (directiveMetadata.type === 'Pipe') { - return null; - } - - // Instead of using the number as value for the "changeDetection" and - // "encapsulation" properties, we want to use the actual enum symbols. - if (propertyName === 'changeDetection' && typeof value === 'number') { - return ts.createPropertyAccess( - this.importManager.addImportToSourceFile( - targetSourceFile, 'ChangeDetectionStrategy', '@angular/core'), - this.coreModule.ChangeDetectionStrategy[value]); - } else if (propertyName === 'encapsulation' && typeof value === 'number') { - return ts.createPropertyAccess( - this.importManager.addImportToSourceFile( - targetSourceFile, 'ViewEncapsulation', '@angular/core'), - this.coreModule.ViewEncapsulation[value]); - } - return null; - }); - - return ts.createDecorator(ts.createCall( - this.importManager.addImportToSourceFile( - targetSourceFile, directiveMetadata.type, '@angular/core'), - undefined, [decoratorExpr])); - } catch (e) { - if (e instanceof UnexpectedMetadataValueError) { - return null; - } - throw e; - } - } - - /** - * Resolves the declaration metadata of a given static symbol. The metadata - * is determined by resolving metadata for the static symbol. - */ - private _resolveDeclarationMetadata(symbol: StaticSymbol): null|DeclarationMetadata { - try { - // Note that this call can throw if the metadata is not computable. In that - // case we are not able to serialize the metadata into a decorator and we return - // null. - const annotations = this.compiler.reflector.annotations(symbol).find( - s => s.ngMetadataName === 'Component' || s.ngMetadataName === 'Directive' || - s.ngMetadataName === 'Pipe'); - - if (!annotations) { - return null; - } - - const {ngMetadataName, ...metadata} = annotations; - - // Delete the "ngMetadataName" property as we don't want to generate - // a property assignment in the new decorator for that internal property. - delete metadata['ngMetadataName']; - - return {type: ngMetadataName, metadata}; - } catch (e) { - return null; - } - } - - private _getStaticSymbolOfIdentifier(node: ts.Identifier): StaticSymbol|null { - const sourceFile = node.getSourceFile(); - const resolvedImport = getImportOfIdentifier(this.typeChecker, node); - - if (!resolvedImport) { - return null; - } - - const moduleName = - this.compilerHost.moduleNameToFileName(resolvedImport.importModule, sourceFile.fileName); - - if (!moduleName) { - return null; - } - - // Find the declaration symbol as symbols could be aliased due to - // metadata re-exports. - return this.compiler.reflector.findSymbolDeclaration( - this.symbolResolver.getStaticSymbol(moduleName, resolvedImport.name)); - } - - /** - * Disables that static symbols are resolved through summaries. Summaries - * cannot be used for decorator analysis as decorators are omitted in summaries. - */ - private _disableSummaryResolution() { - // We never want to resolve symbols through summaries. Summaries never contain - // decorators for class symbols and therefore summaries will cause every class - // to be considered as undecorated. See reason for this in: "ToJsonSerializer". - // In order to ensure that metadata is not retrieved through summaries, we - // need to disable summary resolution, clear previous symbol caches. This way - // future calls to "StaticReflector#annotations" are based on metadata files. - this.symbolResolver['_resolveSymbolFromSummary'] = () => null; - this.symbolResolver['resolvedSymbols'].clear(); - this.symbolResolver['symbolFromFile'].clear(); - this.compiler.reflector['annotationCache'].clear(); - - // Original summary resolver used by the AOT compiler. - const summaryResolver = this.symbolResolver['summaryResolver']; - - // Additionally we need to ensure that no files are treated as "library" files when - // resolving metadata. This is necessary because by default the symbol resolver discards - // class metadata for library files. See "StaticSymbolResolver#createResolvedSymbol". - // Patching this function **only** for the static symbol resolver ensures that metadata - // is not incorrectly omitted. Note that we only want to do this for the symbol resolver - // because otherwise we could break the summary loading logic which is used to detect - // if a static symbol is either a directive, component or pipe (see MetadataResolver). - this.symbolResolver['summaryResolver'] = >{ - fromSummaryFileName: summaryResolver.fromSummaryFileName.bind(summaryResolver), - addSummary: summaryResolver.addSummary.bind(summaryResolver), - getImportAs: summaryResolver.getImportAs.bind(summaryResolver), - getKnownModuleName: summaryResolver.getKnownModuleName.bind(summaryResolver), - resolveSummary: summaryResolver.resolveSummary.bind(summaryResolver), - toSummaryFileName: summaryResolver.toSummaryFileName.bind(summaryResolver), - getSymbolsOf: summaryResolver.getSymbolsOf.bind(summaryResolver), - isLibraryFile: () => false, - }; - } -} diff --git a/packages/core/schematics/migrations/undecorated-classes-with-di/update_recorder.ts b/packages/core/schematics/migrations/undecorated-classes-with-di/update_recorder.ts deleted file mode 100644 index b696373d6b8..00000000000 --- a/packages/core/schematics/migrations/undecorated-classes-with-di/update_recorder.ts +++ /dev/null @@ -1,22 +0,0 @@ - -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; -import {ImportManagerUpdateRecorder} from '../../utils/import_manager'; - -/** - * Update recorder interface that is used to transform source files in a non-colliding - * way. Also this indirection makes it possible to re-use transformation logic with - * different replacement tools (e.g. TSLint or CLI devkit). - */ -export interface UpdateRecorder extends ImportManagerUpdateRecorder { - addClassDecorator(node: ts.ClassDeclaration, text: string): void; - addClassComment(node: ts.ClassDeclaration, text: string): void; - commitUpdate(): void; -} diff --git a/packages/core/schematics/migrations/wait-for-async/BUILD.bazel b/packages/core/schematics/migrations/wait-for-async/BUILD.bazel deleted file mode 100644 index 18cf7800c80..00000000000 --- a/packages/core/schematics/migrations/wait-for-async/BUILD.bazel +++ /dev/null @@ -1,18 +0,0 @@ -load("//tools:defaults.bzl", "ts_library") - -ts_library( - name = "wait-for-async", - srcs = glob(["**/*.ts"]), - tsconfig = "//packages/core/schematics:tsconfig.json", - visibility = [ - "//packages/core/schematics:__pkg__", - "//packages/core/schematics/migrations/google3:__pkg__", - "//packages/core/schematics/test:__pkg__", - ], - deps = [ - "//packages/core/schematics/utils", - "@npm//@angular-devkit/schematics", - "@npm//@types/node", - "@npm//typescript", - ], -) diff --git a/packages/core/schematics/migrations/wait-for-async/README.md b/packages/core/schematics/migrations/wait-for-async/README.md deleted file mode 100644 index 468fb934966..00000000000 --- a/packages/core/schematics/migrations/wait-for-async/README.md +++ /dev/null @@ -1,21 +0,0 @@ -## async -> waitForAsync migration - -Automatically migrates from `async` to `waitForAsync` by changing function calls and renaming imports. - -#### Before -```ts -import { async } from '@angular/core/testing'; - -it('should work', async(() => { - // async testing logic -})); -``` - -#### After -```ts -import { waitForAsync } from '@angular/core/testing'; - -it('should work', waitForAsync(() => { - // async testing logic -})); -``` diff --git a/packages/core/schematics/migrations/wait-for-async/index.ts b/packages/core/schematics/migrations/wait-for-async/index.ts deleted file mode 100644 index a9d3a326643..00000000000 --- a/packages/core/schematics/migrations/wait-for-async/index.ts +++ /dev/null @@ -1,96 +0,0 @@ -/** - * @license - * Copyright Google LLC 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, SchematicsException, Tree} from '@angular-devkit/schematics'; -import {basename, join, relative} from 'path'; -import ts from 'typescript'; - -import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; -import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host'; -import {getImportSpecifier, replaceImport} from '../../utils/typescript/imports'; -import {closestNode} from '../../utils/typescript/nodes'; - -import {findAsyncReferences} from './util'; - -const MODULE_AUGMENTATION_FILENAME = 'ɵɵASYNC_MIGRATION_CORE_AUGMENTATION.d.ts'; - -/** Migration that switches from `async` to `waitForAsync`. */ -export default function(): Rule { - return async (tree: Tree) => { - const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); - const basePath = process.cwd(); - const allPaths = [...buildPaths, ...testPaths]; - - if (!allPaths.length) { - throw new SchematicsException( - 'Could not find any tsconfig file. Cannot migrate async usages to waitForAsync.'); - } - - for (const tsconfigPath of allPaths) { - runWaitForAsyncMigration(tree, tsconfigPath, basePath); - } - }; -} - -function runWaitForAsyncMigration(tree: Tree, tsconfigPath: string, basePath: string) { - // Technically we can get away with using `MODULE_AUGMENTATION_FILENAME` as the path, but as of - // TS 4.2, the module resolution caching seems to be more aggressive which causes the file to be - // retained between test runs. We can avoid it by using the full path. - const augmentedFilePath = join(basePath, MODULE_AUGMENTATION_FILENAME); - const {program} = createMigrationProgram(tree, tsconfigPath, basePath, fileName => { - // In case the module augmentation file has been requested, we return a source file that - // augments "@angular/core/testing" to include a named export called "async". This ensures that - // we can rely on the type checker for this migration after `async` has been removed. - if (basename(fileName) === MODULE_AUGMENTATION_FILENAME) { - return ` - import '@angular/core/testing'; - declare module "@angular/core/testing" { - function async(fn: Function): any; - } - `; - } - return undefined; - }, [augmentedFilePath]); - const typeChecker = program.getTypeChecker(); - const printer = ts.createPrinter(); - const sourceFiles = - program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program)); - const deprecatedFunction = 'async'; - const newFunction = 'waitForAsync'; - - sourceFiles.forEach(sourceFile => { - const asyncImportSpecifier = - getImportSpecifier(sourceFile, '@angular/core/testing', deprecatedFunction); - const asyncImport = asyncImportSpecifier ? - closestNode(asyncImportSpecifier, ts.SyntaxKind.NamedImports) : - null; - - // If there are no imports for `async`, we can exit early. - if (!asyncImportSpecifier || !asyncImport) { - return; - } - - const update = tree.beginUpdate(relative(basePath, sourceFile.fileName)); - - // Change the `async` import to `waitForAsync`. - update.remove(asyncImport.getStart(), asyncImport.getWidth()); - update.insertRight( - asyncImport.getStart(), - printer.printNode( - ts.EmitHint.Unspecified, replaceImport(asyncImport, deprecatedFunction, newFunction), - sourceFile)); - - // Change `async` calls to `waitForAsync`. - findAsyncReferences(sourceFile, typeChecker, asyncImportSpecifier).forEach(node => { - update.remove(node.getStart(), node.getWidth()); - update.insertRight(node.getStart(), newFunction); - }); - - tree.commitUpdate(update); - }); -} diff --git a/packages/core/schematics/migrations/wait-for-async/util.ts b/packages/core/schematics/migrations/wait-for-async/util.ts deleted file mode 100644 index 5e879dc5e4b..00000000000 --- a/packages/core/schematics/migrations/wait-for-async/util.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; - -import {isReferenceToImport} from '../../utils/typescript/symbol'; - -/** Finds calls to the `async` function. */ -export function findAsyncReferences( - sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker, - asyncImportSpecifier: ts.ImportSpecifier) { - const results = new Set(); - - ts.forEachChild(sourceFile, function visitNode(node: ts.Node) { - if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && - node.expression.text === 'async' && - isReferenceToImport(typeChecker, node.expression, asyncImportSpecifier)) { - results.add(node.expression); - } - - ts.forEachChild(node, visitNode); - }); - - return results; -} diff --git a/packages/core/schematics/migrations/xhr-factory/BUILD.bazel b/packages/core/schematics/migrations/xhr-factory/BUILD.bazel deleted file mode 100644 index 570ee390560..00000000000 --- a/packages/core/schematics/migrations/xhr-factory/BUILD.bazel +++ /dev/null @@ -1,17 +0,0 @@ -load("//tools:defaults.bzl", "ts_library") - -ts_library( - name = "xhr-factory", - srcs = glob(["**/*.ts"]), - tsconfig = "//packages/core/schematics:tsconfig.json", - visibility = [ - "//packages/core/schematics:__pkg__", - "//packages/core/schematics/test:__pkg__", - ], - deps = [ - "//packages/core/schematics/utils", - "@npm//@angular-devkit/schematics", - "@npm//@types/node", - "@npm//typescript", - ], -) diff --git a/packages/core/schematics/migrations/xhr-factory/README.md b/packages/core/schematics/migrations/xhr-factory/README.md deleted file mode 100644 index 968e76e94d3..00000000000 --- a/packages/core/schematics/migrations/xhr-factory/README.md +++ /dev/null @@ -1,13 +0,0 @@ -## XhrFactory migration - -Automatically migrates `XhrFactory` from `@angular/common/http` to `@angular/common`. - -#### Before -```ts -import { XhrFactory } from '@angular/common/http'; -``` - -#### After -```ts -import { XhrFactory } from '@angular/common'; -``` diff --git a/packages/core/schematics/migrations/xhr-factory/index.ts b/packages/core/schematics/migrations/xhr-factory/index.ts deleted file mode 100644 index 58d28a5160c..00000000000 --- a/packages/core/schematics/migrations/xhr-factory/index.ts +++ /dev/null @@ -1,132 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 {DirEntry, Rule, UpdateRecorder} from '@angular-devkit/schematics'; -import ts from 'typescript'; -import {findImportSpecifier} from '../../utils/typescript/imports'; - -function* visit(directory: DirEntry): IterableIterator { - for (const path of directory.subfiles) { - if (path.endsWith('.ts') && !path.endsWith('.d.ts')) { - const entry = directory.file(path); - if (entry) { - const content = entry.content; - if (content.includes('XhrFactory')) { - const source = ts.createSourceFile( - entry.path, - content.toString().replace(/^\uFEFF/, ''), - ts.ScriptTarget.Latest, - true, - ); - - yield source; - } - } - } - } - - for (const path of directory.subdirs) { - if (path === 'node_modules' || path.startsWith('.')) { - continue; - } - - yield* visit(directory.dir(path)); - } -} - -export default function(): Rule { - return tree => { - const printer = ts.createPrinter({newLine: ts.NewLineKind.LineFeed}); - - for (const sourceFile of visit(tree.root)) { - let recorder: UpdateRecorder|undefined; - - const allImportDeclarations = - sourceFile.statements.filter(n => ts.isImportDeclaration(n)) as ts.ImportDeclaration[]; - if (allImportDeclarations.length === 0) { - continue; - } - - const httpCommonImport = findImportDeclaration('@angular/common/http', allImportDeclarations); - if (!httpCommonImport) { - continue; - } - - const commonHttpNamedBinding = getNamedImports(httpCommonImport); - if (commonHttpNamedBinding) { - const commonHttpNamedImports = commonHttpNamedBinding.elements; - const xhrFactorySpecifier = findImportSpecifier(commonHttpNamedImports, 'XhrFactory'); - - if (!xhrFactorySpecifier) { - continue; - } - - recorder = tree.beginUpdate(sourceFile.fileName); - - // Remove 'XhrFactory' from '@angular/common/http' - if (commonHttpNamedImports.length > 1) { - // Remove 'XhrFactory' named import - const index = commonHttpNamedBinding.getStart(); - const length = commonHttpNamedBinding.getWidth(); - - const newImports = printer.printNode( - ts.EmitHint.Unspecified, - ts.factory.updateNamedImports( - commonHttpNamedBinding, - commonHttpNamedBinding.elements.filter(e => e !== xhrFactorySpecifier)), - sourceFile); - recorder.remove(index, length).insertLeft(index, newImports); - } else { - // Remove '@angular/common/http' import - const index = httpCommonImport.getFullStart(); - const length = httpCommonImport.getFullWidth(); - recorder.remove(index, length); - } - - // Import XhrFactory from @angular/common - const commonImport = findImportDeclaration('@angular/common', allImportDeclarations); - const commonNamedBinding = getNamedImports(commonImport); - if (commonNamedBinding) { - // Already has an import for '@angular/common', just add the named import. - const index = commonNamedBinding.getStart(); - const length = commonNamedBinding.getWidth(); - const newImports = printer.printNode( - ts.EmitHint.Unspecified, - ts.factory.updateNamedImports( - commonNamedBinding, [...commonNamedBinding.elements, xhrFactorySpecifier]), - sourceFile); - - recorder.remove(index, length).insertLeft(index, newImports); - } else { - // Add import to '@angular/common' - const index = httpCommonImport.getFullStart(); - recorder.insertLeft(index, `\nimport { XhrFactory } from '@angular/common';`); - } - } - - if (recorder) { - tree.commitUpdate(recorder); - } - } - }; -} - -function findImportDeclaration(moduleSpecifier: string, importDeclarations: ts.ImportDeclaration[]): - ts.ImportDeclaration|undefined { - return importDeclarations.find( - n => ts.isStringLiteral(n.moduleSpecifier) && n.moduleSpecifier.text === moduleSpecifier); -} - -function getNamedImports(importDeclaration: ts.ImportDeclaration|undefined): ts.NamedImports| - undefined { - const namedBindings = importDeclaration?.importClause?.namedBindings; - if (namedBindings && ts.isNamedImports(namedBindings)) { - return namedBindings; - } - - return undefined; -} diff --git a/packages/core/schematics/test/BUILD.bazel b/packages/core/schematics/test/BUILD.bazel index 09c9f860ce2..225cc068b64 100644 --- a/packages/core/schematics/test/BUILD.bazel +++ b/packages/core/schematics/test/BUILD.bazel @@ -8,28 +8,8 @@ ts_library( "//packages/core/schematics:migrations.json", ], deps = [ - "//packages/core/schematics/migrations/abstract-control-parent", - "//packages/core/schematics/migrations/activated-route-snapshot-fragment", - "//packages/core/schematics/migrations/can-activate-with-redirect-to", - "//packages/core/schematics/migrations/deep-shadow-piercing-selector", - "//packages/core/schematics/migrations/dynamic-queries", - "//packages/core/schematics/migrations/initial-navigation", - "//packages/core/schematics/migrations/missing-injectable", - "//packages/core/schematics/migrations/module-with-providers", - "//packages/core/schematics/migrations/move-document", - "//packages/core/schematics/migrations/native-view-encapsulation", - "//packages/core/schematics/migrations/navigation-extras-omissions", - "//packages/core/schematics/migrations/relative-link-resolution", - "//packages/core/schematics/migrations/renderer-to-renderer2", "//packages/core/schematics/migrations/router-link-empty-expression", - "//packages/core/schematics/migrations/router-preserve-query-params", - "//packages/core/schematics/migrations/static-queries", - "//packages/core/schematics/migrations/template-var-assignment", "//packages/core/schematics/migrations/testbed-teardown", - "//packages/core/schematics/migrations/undecorated-classes-with-decorated-fields", - "//packages/core/schematics/migrations/undecorated-classes-with-di", - "//packages/core/schematics/migrations/wait-for-async", - "//packages/core/schematics/migrations/xhr-factory", "//packages/core/schematics/utils", "@npm//@angular-devkit/core", "@npm//@angular-devkit/schematics", diff --git a/packages/core/schematics/test/abstract_control_parent_migration_spec.ts b/packages/core/schematics/test/abstract_control_parent_migration_spec.ts deleted file mode 100644 index ad38b34117f..00000000000 --- a/packages/core/schematics/test/abstract_control_parent_migration_spec.ts +++ /dev/null @@ -1,290 +0,0 @@ -/** - * @license - * Copyright Google LLC 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('AbstractControl.parent migration', () => { - let runner: SchematicTestRunner; - let host: TempScopedNodeJsSyncHost; - let tree: UnitTestTree; - let tmpDirPath: string; - let previousWorkingDir: string; - - beforeEach(() => { - runner = new SchematicTestRunner('test', require.resolve('../migrations.json')); - host = new TempScopedNodeJsSyncHost(); - tree = new UnitTestTree(new HostTree(host)); - - writeFile('/tsconfig.json', JSON.stringify({ - compilerOptions: {lib: ['es2015'], strictNullChecks: true}, - })); - writeFile('/angular.json', JSON.stringify({ - version: 1, - projects: {t: {architect: {build: {options: {tsConfig: './tsconfig.json'}}}}} - })); - // We need to declare the Angular symbols we're testing for, otherwise type checking won't work. - writeFile('/node_modules/@angular/forms/index.d.ts', ` - export declare abstract class AbstractControl { - get dirty(): boolean; - get disabled(): boolean; - get parent(): FormGroup | FormArray | null; - } - - export declare class FormArray extends AbstractControl { - getRawValue(): any[]; - } - - export declare class FormControl extends AbstractControl { - setValue(value: any): void; - } - - export declare class FormGroup extends AbstractControl { - getRawValue(): any; - } - `); - - // Fake non-Angular package to make sure that we don't migrate packages we don't own. - writeFile('/node_modules/@not-angular/forms/index.d.ts', ` - export declare abstract class AbstractControl { - get dirty(): boolean; - get disabled(): boolean; - get parent(): FormGroup | FormArray | null; - } - - export declare class FormControl extends AbstractControl { - setValue(value: any): void; - } - `); - - 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); - }); - - it('should add non-null assertions to accesses of AbstractControl.parent', async () => { - writeFile('/index.ts', ` - import {AbstractControl} from '@angular/forms'; - - class App { - private _control: AbstractControl; - - getParentValue() { - return this._control.parent.value; - } - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toContain(`return this._control.parent!.value;`); - }); - - it('should add non-null assertions to accesses of FormArray.parent', async () => { - writeFile('/index.ts', ` - import {FormArray} from '@angular/forms'; - - class App { - getParentValueOf(control: FormArray) { - return control.parent.value; - } - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toContain(`return control.parent!.value;`); - }); - - it('should add non-null assertions to accesses of FormControl.parent', async () => { - writeFile('/index.ts', ` - import {FormControl} from '@angular/forms'; - - class App { - getBlankControlParentValue() { - return this._getControl().parent.value; - } - - private _getControl() { - return new FormControl(); - } - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toContain(`return this._getControl().parent!.value;`); - }); - - it('should add non-null assertions to accesses of FormGroup.parent', async () => { - writeFile('/index.ts', ` - import {FormGroup} from '@angular/forms'; - - class App { - getGlobalGroupParentValue() { - const parent = (window.foo as FormGroup).parent; - return parent.value; - } - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`const parent = (window.foo as FormGroup).parent!;`); - }); - - it('should add non-null assertions to nested accesses of `AbstractControl.parent`', async () => { - writeFile('/index.ts', ` - import {FormControl} from '@angular/forms'; - - class App { - private _control = new FormControl(); - - getGreatGrandParentValue() { - return this._control.parent.parent.parent.value; - } - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`return this._control.parent!.parent!.parent!.value;`); - }); - - it('should not add non-null assertions if the `parent` has been null checked in an if statement', - async () => { - writeFile('/index.ts', ` - import {FormControl} from '@angular/forms'; - - function getParentValue(control: FormControl) { - if (control.parent) { - return control.parent.value; - } - - return null; - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain(`if (control.parent) {`); - expect(content).toContain(`return control.parent.value;`); - }); - - it('should not add non-null assertions if the `parent` has been null checked in an else if statement', - async () => { - writeFile('/index.ts', ` - import {FormControl} from '@angular/forms'; - - function getParentValue(foo: boolean, control: FormControl) { - if (foo) { - return foo; - } else if (control.parent) { - return control.parent.value; - } - - return null; - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain(`} else if (control.parent) {`); - expect(content).toContain(`return control.parent.value;`); - }); - - it('should not add non-null assertions if the `parent` has been null checked in a ternary expression', - async () => { - writeFile('/index.ts', ` - import {FormControl} from '@angular/forms'; - - function getParentValue(control: FormControl) { - return control.parent ? control.parent.value : null; - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`return control.parent ? control.parent.value : null;`); - }); - - it('should not add non-null assertions if a nested `parent` has been null checked', async () => { - writeFile('/index.ts', ` - import {FormControl} from '@angular/forms'; - - function getGreatGrandParentValue(control: FormControl) { - return control.parent && control.parent.parent && control.parent.parent.parent && control.parent.parent.parent.value; - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain( - `return control.parent && control.parent.parent && control.parent.parent.parent && control.parent.parent.parent.value;`); - }); - - it('should not add non-null assertions if there is one already', async () => { - writeFile('/index.ts', ` - import {FormControl} from '@angular/forms'; - - function getParentValue(control: FormControl) { - return control.parent!.value; - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toContain(`return control.parent!.value;`); - }); - - it('should not add non-null assertions if there is a safe access', async () => { - writeFile('/index.ts', ` - import {FormControl} from '@angular/forms'; - - function getParentValue(control: FormControl) { - return control.parent?.value; - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toContain(`return control.parent?.value;`); - }); - - it('should not add non-null assertions if the symbol does not come from @angular/forms', - async () => { - writeFile('/index.ts', ` - import {FormControl} from '@not-angular/forms'; - - function getParentValue(control: FormControl) { - return control.parent.value; - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toContain(`return control.parent.value;`); - }); - - function writeFile(filePath: string, contents: string) { - host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); - } - - function runMigration() { - return runner.runSchematicAsync('migration-v11-abstract-control-parent', {}, tree).toPromise(); - } -}); diff --git a/packages/core/schematics/test/activated_route_snapshot_fragment_migration_spec.ts b/packages/core/schematics/test/activated_route_snapshot_fragment_migration_spec.ts deleted file mode 100644 index 4d95fd29aa8..00000000000 --- a/packages/core/schematics/test/activated_route_snapshot_fragment_migration_spec.ts +++ /dev/null @@ -1,187 +0,0 @@ -/** - * @license - * Copyright Google LLC 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('ActivatedRouteSnapshot.fragment migration', () => { - let runner: SchematicTestRunner; - let host: TempScopedNodeJsSyncHost; - let tree: UnitTestTree; - let tmpDirPath: string; - let previousWorkingDir: string; - - beforeEach(() => { - runner = new SchematicTestRunner('test', require.resolve('../migrations.json')); - host = new TempScopedNodeJsSyncHost(); - tree = new UnitTestTree(new HostTree(host)); - - writeFile('/tsconfig.json', JSON.stringify({ - compilerOptions: {lib: ['es2015'], strictNullChecks: true}, - })); - writeFile('/angular.json', JSON.stringify({ - version: 1, - projects: {t: {architect: {build: {options: {tsConfig: './tsconfig.json'}}}}} - })); - // We need to declare the Angular symbols we're testing for, otherwise type checking won't work. - writeFile('/node_modules/@angular/router.d.ts', ` - export declare class ActivatedRoute { - get children(): ActivatedRoute[]; - fragment: Observable; - snapshot: ActivatedRouteSnapshot; - url: Observable; - } - - export declare class ActivatedRouteSnapshot { - fragment: string | null; - url: unknown[]; - } - `); - - 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); - }); - - it('should add non-null assertions to accesses of `ActivatedRouteSnapshot.fragment`', - async () => { - writeFile('/index.ts', ` - import {ActivatedRoute} from '@angular/router'; - - class App { - private _route: ActivatedRoute; - - getFragment() { - return this._getSnapshot().fragment.foo; - } - - private _getSnapshot() { - return this._route.snapshot; - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')).toContain('return this._getSnapshot().fragment!.foo'); - }); - - it('should not add non-null assertions to accesses of `ActivatedRouteSnapshot.fragment` if there is one already', - async () => { - writeFile('/index.ts', ` - import {ActivatedRoute} from '@angular/router'; - - class App { - private _route: ActivatedRoute; - - getFragment() { - return this._route.snapshot.fragment!.foo; - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain('return this._route.snapshot.fragment!.foo;'); - }); - - it('should not add non-null assertions if the `ActivatedRouteSnapshot.fragment` has been null checked in an if statement', - async () => { - writeFile('/index.ts', ` - import {ActivatedRouteSnapshot} from '@angular/router'; - - function getFragmentValue(snapshot: ActivatedRouteSnapshot) { - if (snapshot.fragment) { - return snapshot.fragment.value; - } - - return null; - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain(`if (snapshot.fragment) {`); - expect(content).toContain(`return snapshot.fragment.value;`); - }); - - it('should not add non-null assertions if the `ActivatedRouteSnapshot.fragment` has been null checked in an else if statement', - async () => { - writeFile('/index.ts', ` - import {ActivatedRouteSnapshot} from '@angular/router'; - - function getSnapshotValue(foo: boolean, snapshot: ActivatedRouteSnapshot) { - if (foo) { - return foo; - } else if (snapshot.fragment) { - return snapshot.fragment.value; - } - - return null; - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain(`} else if (snapshot.fragment) {`); - expect(content).toContain(`return snapshot.fragment.value;`); - }); - - it('should not add non-null assertions if the `ActivatedRouteSnapshot.fragment` has been null checked in a ternary expression', - async () => { - writeFile('/index.ts', ` - import {ActivatedRouteSnapshot} from '@angular/router'; - - function getSnapshotValue(snapshot: ActivatedRouteSnapshot) { - return snapshot.fragment ? snapshot.fragment.value : null; - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`return snapshot.fragment ? snapshot.fragment.value : null;`); - }); - - it('should not add non-null assertion to `ActivatedRouteSnapshot.fragment` if there is a safe access', - async () => { - writeFile('/index.ts', ` - import {ActivatedRouteSnapshot} from '@angular/router'; - - function getSnapshotValue(snapshot: ActivatedRouteSnapshot) { - return snapshot.fragment?.value; - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toContain(`return snapshot.fragment?.value;`); - }); - - function writeFile(filePath: string, contents: string) { - host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); - } - - function runMigration() { - return runner.runSchematicAsync('migration-v12-activated-route-snapshot-fragment', {}, tree) - .toPromise(); - } -}); diff --git a/packages/core/schematics/test/can_activate_with_redirect_migration_spec.ts b/packages/core/schematics/test/can_activate_with_redirect_migration_spec.ts deleted file mode 100644 index f8ba5581214..00000000000 --- a/packages/core/schematics/test/can_activate_with_redirect_migration_spec.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * @license - * Copyright Google LLC 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('canActivate removal with redirectTo', () => { - let runner: SchematicTestRunner; - let host: TempScopedNodeJsSyncHost; - let tree: UnitTestTree; - let tmpDirPath: string; - let previousWorkingDir: string; - - beforeEach(() => { - runner = new SchematicTestRunner('test', require.resolve('../migrations.json')); - host = new TempScopedNodeJsSyncHost(); - tree = new UnitTestTree(new HostTree(host)); - - writeFile('/tsconfig.json', JSON.stringify({ - compilerOptions: { - lib: ['es2015'], - strictNullChecks: true, - }, - })); - writeFile('/angular.json', JSON.stringify({ - version: 1, - projects: {t: {architect: {build: {options: {tsConfig: './tsconfig.json'}}}}} - })); - - 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); - }); - - it('should not remove canActivate when redirectTo is not present', async () => { - writeFile('/index.ts', `const route = {path: '', canActivate: ['my_guard_token']}`); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toEqual(`const route = {path: '', canActivate: ['my_guard_token']}`); - }); - - it('removes canActivate when redirectTo is present', async () => { - writeFile( - '/index.ts', - `const route = {path: '', redirectTo: 'other', canActivate: ['my_guard_token']}`); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toEqual(`const route = { path: '', redirectTo: 'other' }`); - }); - - function writeFile(filePath: string, contents: string) { - host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); - } - - function runMigration() { - return runner.runSchematicAsync('migration-v11.1-can-activate-with-redirect-to', {}, tree) - .toPromise(); - } -}); diff --git a/packages/core/schematics/test/deep-shadow-piercing-selector_spec.ts b/packages/core/schematics/test/deep-shadow-piercing-selector_spec.ts deleted file mode 100644 index 912642c5a4f..00000000000 --- a/packages/core/schematics/test/deep-shadow-piercing-selector_spec.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 {tags} from '@angular-devkit/core'; -import {EmptyTree} from '@angular-devkit/schematics'; -import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing'; - -describe('`/deep/` to `::ng-deep` migration', () => { - let tree: UnitTestTree; - const runner = new SchematicTestRunner('test', require.resolve('../migrations.json')); - - beforeEach(() => { - tree = new UnitTestTree(new EmptyTree()); - }); - - it(`should replace '/deep/' with '::ng-deep' in inline component styles`, async () => { - const fileName = '/index.ts'; - const getFileContent = (contentToReplace: string) => tags.stripIndents` - import { Component } from '@angular/core'; - - @Component({ - selector: 'app-root', - template: '', - styles: ['` + - contentToReplace + `'], - }) - export class AppComponent { } - `; - - tree.create(fileName, getFileContent(':host /deep/ * { cursor: pointer; }')); - await runMigration(); - expect(tree.readContent(fileName)) - .toBe(getFileContent(':host ::ng-deep * { cursor: pointer; }')); - }); - - for (const styleExtension of ['scss', 'sass', 'css', 'styl', 'less']) { - it(`should replace '/deep/' with '::ng-deep' in ${styleExtension} file`, async () => { - const fileName = `/index.${styleExtension}`; - tree.create(fileName, ':host /deep/ * { cursor: pointer; }'); - await runMigration(); - expect(tree.readContent(fileName)).toBe(':host ::ng-deep * { cursor: pointer; }'); - }); - } - - it(`should replace '/deep/' with '::ng-deep' when used as root selector`, async () => { - const fileName = '/index.css'; - tree.create(fileName, '/deep/ * { cursor: pointer; }'); - await runMigration(); - expect(tree.readContent(fileName)).toBe('::ng-deep * { cursor: pointer; }'); - }); - - it(`should not replace '/deep/' with '::ng-deep' in unknown file extension`, async () => { - const fileName = '/index.foo'; - const content = 'this is a not /deep/ selector'; - tree.create(fileName, content); - await runMigration(); - expect(tree.readContent(fileName)).toBe(content); - }); - - async function runMigration(): Promise { - await runner.runSchematicAsync('migration-v12-deep-shadow-piercing-selector', {}, tree) - .toPromise(); - } -}); diff --git a/packages/core/schematics/test/dynamic_queries_migration_spec.ts b/packages/core/schematics/test/dynamic_queries_migration_spec.ts deleted file mode 100644 index 6950c35b968..00000000000 --- a/packages/core/schematics/test/dynamic_queries_migration_spec.ts +++ /dev/null @@ -1,166 +0,0 @@ -/** - * @license - * Copyright Google LLC 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('dynamic queries migration', () => { - let runner: SchematicTestRunner; - let host: TempScopedNodeJsSyncHost; - let tree: UnitTestTree; - let tmpDirPath: string; - let previousWorkingDir: string; - - beforeEach(() => { - runner = new SchematicTestRunner('test', require.resolve('../migrations.json')); - host = new TempScopedNodeJsSyncHost(); - tree = new UnitTestTree(new HostTree(host)); - - writeFile('/tsconfig.json', JSON.stringify({ - compilerOptions: { - lib: ['es2015'], - } - })); - writeFile('/angular.json', JSON.stringify({ - version: 1, - projects: {t: {architect: {build: {options: {tsConfig: './tsconfig.json'}}}}} - })); - - 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); - }); - - it('should remove the options object from a dynamic ViewChild query that only has one property', - async () => { - writeFile('/index.ts', ` - import { Directive, ViewChild } from '@angular/core'; - - @Directive() - export class MyDirective { - @ViewChild('child', { static: false }) child: any; - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toContain(`@ViewChild('child') child: any;`); - }); - - it('should remove the options object from a dynamic ContentChild query that only has one property', - async () => { - writeFile('/index.ts', ` - import { Directive, ContentChild } from '@angular/core'; - - @Directive() - export class MyComponent { - @ContentChild('child', { static: false }) child: any; - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toContain(`@ContentChild('child') child: any;`); - }); - - it('should only remove the `static` flag from a ViewChild query if it has more than one property', - async () => { - writeFile('/index.ts', ` - import { Directive, ViewChild, ElementRef } from '@angular/core'; - - @Directive() - export class MyDirective { - @ViewChild('child', { read: ElementRef, static: false }) child: ElementRef; - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`@ViewChild('child', { read: ElementRef }) child: ElementRef;`); - }); - - it('should only remove the `static` flag from a ContentChild query if it has more than one property', - async () => { - writeFile('/index.ts', ` - import { Directive, ContentChild, ElementRef } from '@angular/core'; - - @Directive() - export class MyDirective { - @ContentChild('child', { static: false, read: ElementRef }) child: ElementRef; - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`@ContentChild('child', { read: ElementRef }) child: ElementRef;`); - }); - - it('should not change static ViewChild queries', async () => { - writeFile('/index.ts', ` - import { Directive, ViewChild, ElementRef } from '@angular/core'; - - @Directive() - export class MyDirective { - @ViewChild('child', { read: ElementRef, static: true }) child: ElementRef; - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`@ViewChild('child', { read: ElementRef, static: true }) child: ElementRef;`); - }); - - it('should not change static ContentChild queries', async () => { - writeFile('/index.ts', ` - import { Directive, ContentChild, ElementRef } from '@angular/core'; - - @Directive() - export class MyDirective { - @ContentChild('child', { static: true, read: ElementRef }) child: ElementRef; - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`@ContentChild('child', { static: true, read: ElementRef }) child: ElementRef;`); - }); - - it('should migrate dynamic queries on a setter', async () => { - writeFile('/index.ts', ` - import { Directive, ContentChild, ViewChild } from '@angular/core'; - - @Directive() - export class MyDirective { - @ContentChild('child', { static: false }) set child(c: any) {} - @ViewChild('otherChild', { static: false }) set otherChild(c: any) {} - } - `); - - await runMigration(); - const content = tree.readContent('/index.ts'); - expect(content).toContain(`@ContentChild('child') set child(c: any) {}`); - expect(content).toContain(`@ViewChild('otherChild') set otherChild(c: any) {}`); - }); - - function writeFile(filePath: string, contents: string) { - host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); - } - - function runMigration() { - return runner.runSchematicAsync('migration-v9-dynamic-queries', {}, tree).toPromise(); - } -}); diff --git a/packages/core/schematics/test/google3/activated_route_snapshot_fragment_spec.ts b/packages/core/schematics/test/google3/activated_route_snapshot_fragment_spec.ts deleted file mode 100644 index 19232cf1a90..00000000000 --- a/packages/core/schematics/test/google3/activated_route_snapshot_fragment_spec.ts +++ /dev/null @@ -1,210 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ActivatedRouteSnapshot.fragment TSLint rule', () => { - const rulesDirectory = - dirname(require.resolve('../../migrations/google3/activatedRouteSnapshotFragmentRule')); - - let tmpDir: string; - - beforeEach(() => { - tmpDir = join(process.env['TEST_TMPDIR']!, 'google3-test'); - shx.mkdir('-p', tmpDir); - - // We need to declare the Angular symbols we're testing for, otherwise type checking won't work. - writeFile('router.d.ts', ` - export declare class ActivatedRoute { - get children(): ActivatedRoute[]; - fragment: Observable; - snapshot: ActivatedRouteSnapshot; - url: Observable; - } - - export declare class ActivatedRouteSnapshot { - fragment: string | null; - url: UrlSegment[]; - } - `); - - writeFile('tsconfig.json', JSON.stringify({ - compilerOptions: { - module: 'es2015', - baseUrl: './', - strictNullChecks: true, - paths: { - '@angular/router': ['router.d.ts'], - } - }, - })); - }); - - afterEach(() => shx.rm('-r', tmpDir)); - - function runTSLint(fix: boolean) { - const program = Linter.createProgram(join(tmpDir, 'tsconfig.json')); - const linter = new Linter({fix, rulesDirectory: [rulesDirectory]}, program); - const config = - Configuration.parseConfigFile({rules: {'activated-route-snapshot-fragment': 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 flag accesses to `ActivatedRouteSnapshot.fragment`', () => { - writeFile('/index.ts', ` - import {ActivatedRoute} from '@angular/router'; - - class App { - private _route: ActivatedRoute; - - ngOnInit() { - this._route.fragment.subscribe(); - } - - getFragment() { - return this._route.snapshot.fragment.foo; - } - } - `); - - const linter = runTSLint(false); - const failures = linter.getResult().failures.map(failure => failure.getFailure()); - expect(failures).toEqual(['`ActivatedRouteSnapshot.fragment` is nullable.']); - }); - - it('should add non-null assertions to accesses of `ActivatedRouteSnapshot.fragment`', () => { - writeFile('/index.ts', ` - import {ActivatedRoute} from '@angular/router'; - - class App { - private _route: ActivatedRoute; - - getFragment() { - return this._getSnapshot().fragment.foo; - } - - private _getSnapshot() { - return this._route.snapshot; - } - } - `); - - runTSLint(true); - - expect(getFile('/index.ts')).toContain('return this._getSnapshot().fragment!.foo'); - }); - - it('should not add non-null assertions to accesses of `ActivatedRouteSnapshot.fragment` if there is one already', - () => { - writeFile('/index.ts', ` - import {ActivatedRoute} from '@angular/router'; - - class App { - private _route: ActivatedRoute; - - getFragment() { - return this._route.snapshot.fragment!.foo; - } - } - `); - - runTSLint(true); - - expect(getFile('/index.ts')).toContain('return this._route.snapshot.fragment!.foo;'); - }); - - it('should not add non-null assertions if the `ActivatedRouteSnapshot.fragment` has been null checked in an if statement', - () => { - writeFile('/index.ts', ` - import {ActivatedRouteSnapshot} from '@angular/router'; - - function getFragmentValue(snapshot: ActivatedRouteSnapshot) { - if (snapshot.fragment) { - return snapshot.fragment.value; - } - - return null; - } - `); - - runTSLint(true); - - const content = getFile('/index.ts'); - expect(content).toContain(`if (snapshot.fragment) {`); - expect(content).toContain(`return snapshot.fragment.value;`); - }); - - it('should not add non-null assertions if the `ActivatedRouteSnapshot.fragment` has been null checked in an else if statement', - () => { - writeFile('/index.ts', ` - import {ActivatedRouteSnapshot} from '@angular/router'; - - function getSnapshotValue(foo: boolean, snapshot: ActivatedRouteSnapshot) { - if (foo) { - return foo; - } else if (snapshot.fragment) { - return snapshot.fragment.value; - } - - return null; - } - `); - - runTSLint(true); - - const content = getFile('/index.ts'); - expect(content).toContain(`} else if (snapshot.fragment) {`); - expect(content).toContain(`return snapshot.fragment.value;`); - }); - - it('should not add non-null assertions if the `ActivatedRouteSnapshot.fragment` has been null checked in a ternary expression', - () => { - writeFile('/index.ts', ` - import {ActivatedRouteSnapshot} from '@angular/router'; - - function getSnapshotValue(snapshot: ActivatedRouteSnapshot) { - return snapshot.fragment ? snapshot.fragment.value : null; - } - `); - - runTSLint(true); - - expect(getFile('/index.ts')) - .toContain(`return snapshot.fragment ? snapshot.fragment.value : null;`); - }); - - it('should not add non-null assertion to `ActivatedRouteSnapshot.fragment` if there is a safe access', - () => { - writeFile('/index.ts', ` - import {ActivatedRouteSnapshot} from '@angular/router'; - - function getSnapshotValue(snapshot: ActivatedRouteSnapshot) { - return snapshot.fragment?.value; - } - `); - - runTSLint(true); - expect(getFile('/index.ts')).toContain(`return snapshot.fragment?.value;`); - }); -}); diff --git a/packages/core/schematics/test/google3/can_activate_with_redirect_rule_spec.ts b/packages/core/schematics/test/google3/can_activate_with_redirect_rule_spec.ts deleted file mode 100644 index 0f3a479c5cf..00000000000 --- a/packages/core/schematics/test/google3/can_activate_with_redirect_rule_spec.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 canActivate with redirectTo', () => { - const rulesDirectory = - dirname(require.resolve('../../migrations/google3/canActivateWithRedirectToRule')); - - let tmpDir: string; - - beforeEach(() => { - tmpDir = join(process.env['TEST_TMPDIR']!, 'google3-test'); - shx.mkdir('-p', tmpDir); - - writeFile('tsconfig.json', JSON.stringify({ - compilerOptions: { - module: 'es2015', - baseUrl: './', - }, - })); - }); - - afterEach(() => shx.rm('-r', tmpDir)); - - function runTSLint(fix: boolean) { - const program = Linter.createProgram(join(tmpDir, 'tsconfig.json')); - const linter = new Linter({fix, rulesDirectory: [rulesDirectory]}, program); - const config = Configuration.parseConfigFile({rules: {'can-activate-with-redirect-to': 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 not flag canActivate when redirectTo is not present', async () => { - writeFile('/index.ts', `const route = {path: '', canActivate: ['my_guard_token']}`); - - const linter = runTSLint(false); - const failures = linter.getResult().failures.map(failure => failure.getFailure()); - - expect(failures.length).toBe(0); - }); - - it('should flag when canActivate when redirectTo is present', async () => { - writeFile( - '/index.ts', - `const route = {path: '', redirectTo: 'other', canActivate: ['my_guard_token']}`); - - const linter = runTSLint(false); - const failures = linter.getResult().failures.map(failure => failure.getFailure()); - expect(failures.length).toBe(1); - expect(failures[0]).toMatch(/canActivate cannot be used with redirectTo./); - }); - - it('should fix when canActivate when redirectTo is present', async () => { - writeFile( - '/index.ts', - `const route = {path: '', redirectTo: 'other', canActivate: ['my_guard_token']}`); - - runTSLint(true); - const content = getFile('/index.ts'); - expect(content).toContain(`const route = { path: '', redirectTo: 'other' }`); - }); -}); diff --git a/packages/core/schematics/test/google3/dynamic_queries_spec.ts b/packages/core/schematics/test/google3/dynamic_queries_spec.ts deleted file mode 100644 index 106b61e7088..00000000000 --- a/packages/core/schematics/test/google3/dynamic_queries_spec.ts +++ /dev/null @@ -1,177 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 dynamic queries TSLint rule', () => { - const rulesDirectory = dirname(require.resolve('../../migrations/google3/dynamicQueriesRule')); - - 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: {'dynamic-queries': 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 flag dynamic queries', () => { - writeFile('/index.ts', ` - import { Directive, ViewChild, ContentChild } from '@angular/core'; - - @Directive() - export class MyDirective { - @ViewChild('child', { static: false }) child: any; - @ContentChild('otherChild', { static: false }) otherChild: any; - } - `); - - const linter = runTSLint(false); - const failures = linter.getResult().failures; - - expect(failures.length).toBe(2); - expect(failures[0].getFailure()) - .toMatch('The static flag defaults to false, so setting it false manually is unnecessary.'); - expect(failures[1].getFailure()) - .toMatch('The static flag defaults to false, so setting it false manually is unnecessary.'); - }); - - it('should remove the options object from a dynamic ViewChild query that only has one property', - () => { - writeFile('/index.ts', ` - import { Directive, ViewChild } from '@angular/core'; - - @Directive() - export class MyDirective { - @ViewChild('child', { static: false }) child: any; - } - `); - - runTSLint(true); - expect(getFile('/index.ts')).toContain(`@ViewChild('child') child: any;`); - }); - - it('should remove the options object from a dynamic ContentChild query that only has one property', - () => { - writeFile('/index.ts', ` - import { Directive, ContentChild } from '@angular/core'; - - @Directive() - export class MyComponent { - @ContentChild('child', { static: false }) child: any; - } - `); - - runTSLint(true); - expect(getFile('/index.ts')).toContain(`@ContentChild('child') child: any;`); - }); - - it('should only remove the `static` flag from a ViewChild query if it has more than one property', - () => { - writeFile('/index.ts', ` - import { Directive, ViewChild, ElementRef } from '@angular/core'; - - @Directive() - export class MyDirective { - @ViewChild('child', { read: ElementRef, static: false }) child: ElementRef; - } - `); - - runTSLint(true); - expect(getFile('/index.ts')) - .toContain(`@ViewChild('child', { read: ElementRef }) child: ElementRef;`); - }); - - it('should only remove the `static` flag from a ContentChild query if it has more than one property', - () => { - writeFile('/index.ts', ` - import { Directive, ContentChild, ElementRef } from '@angular/core'; - - @Directive() - export class MyDirective { - @ContentChild('child', { static: false, read: ElementRef }) child: ElementRef; - } - `); - - runTSLint(true); - expect(getFile('/index.ts')) - .toContain(`@ContentChild('child', { read: ElementRef }) child: ElementRef;`); - }); - - it('should not change static ViewChild queries', () => { - writeFile('/index.ts', ` - import { Directive, ViewChild, ElementRef } from '@angular/core'; - - @Directive() - export class MyDirective { - @ViewChild('child', { read: ElementRef, static: true }) child: ElementRef; - } - `); - - runTSLint(true); - expect(getFile('/index.ts')) - .toContain(`@ViewChild('child', { read: ElementRef, static: true }) child: ElementRef;`); - }); - - it('should not change static ContentChild queries', () => { - writeFile('/index.ts', ` - import { Directive, ContentChild, ElementRef } from '@angular/core'; - - @Directive() - export class MyDirective { - @ContentChild('child', { static: true, read: ElementRef }) child: ElementRef; - } - `); - - runTSLint(true); - expect(getFile('/index.ts')) - .toContain(`@ContentChild('child', { static: true, read: ElementRef }) child: ElementRef;`); - }); - - it('should migrate dynamic queries on a setter', () => { - writeFile('/index.ts', ` - import { Directive, ContentChild, ViewChild } from '@angular/core'; - - @Directive() - export class MyDirective { - @ContentChild('child', { static: false }) set child(c: any) {} - @ViewChild('otherChild', { static: false }) set otherChild(c: any) {} - } - `); - - runTSLint(true); - const content = getFile('/index.ts'); - expect(content).toContain(`@ContentChild('child') set child(c: any) {}`); - expect(content).toContain(`@ViewChild('otherChild') set otherChild(c: any) {}`); - }); -}); diff --git a/packages/core/schematics/test/google3/explicit_query_timing_rule_spec.ts b/packages/core/schematics/test/google3/explicit_query_timing_rule_spec.ts deleted file mode 100644 index f791863b5ae..00000000000 --- a/packages/core/schematics/test/google3/explicit_query_timing_rule_spec.ts +++ /dev/null @@ -1,149 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 explicitQueryTiming TSLint rule', () => { - /** - * Path to the static-query schematic rules directory. The path needs to be resolved through - * the Bazel runfiles, because on Windows runfiles are not symlinked into the working directory. - */ - const rulesDirectory = - dirname(require.resolve('../../migrations/google3/explicitQueryTimingRule')); - - 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)); - - /** - * Runs TSLint with the static-query timing TSLint rule. By default the rule fixes - * are automatically applied. - */ - 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: {'explicit-query-timing': true}}); - - program.getRootFileNames().forEach(fileName => { - linter.lint(fileName, program.getSourceFile(fileName)!.getFullText(), config); - }); - - return linter; - } - - /** Writes a file to the current temporary directory. */ - function writeFile(fileName: string, content: string) { - writeFileSync(join(tmpDir, fileName), content); - } - - /** Expects a given file in the temporary directory to contain the specified string. */ - function expectFileToContain(fileName: string, match: string) { - expect(readFileSync(join(tmpDir, fileName), 'utf8')).toContain(match); - } - - it('should properly apply query timing replacements', () => { - writeFile('index.ts', ` - import {Component, ViewChild} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - @ViewChild('test') query: any; - @ViewChild('test') query2: any; - @ViewChild('test') query3: any; - - ngAfterContentInit() { - this.query.classList.add('test'); - } - } - `); - - writeFile('external.ts', ` - import {MyComp} from './index'; - - export class Test extends MyComp { - ngOnInit() { - this.query3.doSomething(); - } - } - `); - - runTSLint(); - - expectFileToContain('index.ts', `@ViewChild('test', { static: true }) query: any;`); - expectFileToContain('index.ts', `@ViewChild('test', { static: false }) query2: any;`); - expectFileToContain('index.ts', `@ViewChild('test', { static: true }) query3: any;`); - }); - - it('should report non-explicit static query definitions', () => { - writeFile('index.ts', ` - import {Component, ViewChild} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - @ViewChild('test') query: any; - - ngAfterContentInit() { - this.query.classList.add('test'); - } - } - `); - - const linter = runTSLint(false); - const failures = linter.getResult().failures; - - expect(failures.length).toBe(1); - expect(failures[0].getFailure()).toMatch(/analysis of the query.*"{static: true}"/); - }); - - it('should report non-explicit dynamic query definitions', () => { - writeFile('index.ts', ` - import {Component, ContentChild} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - @ContentChild('test') query: any; - } - `); - - const linter = runTSLint(false); - const failures = linter.getResult().failures; - - expect(failures.length).toBe(1); - expect(failures[0].getFailure()).toMatch(/analysis of the query.*"{static: false}"/); - }); - - it('should detect query usage in component template', () => { - writeFile('index.ts', ` - import {Component, ViewChild} from '@angular/core'; - - @Component({ - template: \` - - - \` - }) - export class MyComp { - @ViewChild('test') query: any; - } - `); - - runTSLint(); - - expectFileToContain('index.ts', `@ViewChild('test', { static: true }) query: any;`); - }); -}); diff --git a/packages/core/schematics/test/google3/initial_navigation_rule_spec.ts b/packages/core/schematics/test/google3/initial_navigation_rule_spec.ts deleted file mode 100644 index 07cddb8fdb2..00000000000 --- a/packages/core/schematics/test/google3/initial_navigation_rule_spec.ts +++ /dev/null @@ -1,211 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 initial navigation tslint rule', () => { - const rulesDirectory = dirname(require.resolve('../../migrations/google3/initialNavigationRule')); - - 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: {'initial-navigation': 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 migrate legacy_disabled to disabled', () => { - writeFile('/index.ts', ` - import { NgModule } from '@angular/core'; - import { RouterModule } from '@angular/router'; - - @NgModule({ - imports: [ - RouterModule.forRoot([], {initialNavigation: 'legacy_disabled'}), - ] - }) - export class AppModule { - } - `); - - - runTSLint(); - - expect(getFile('/index.ts')).toContain(`{initialNavigation: 'disabled'}`); - }); - - it(`should migrate false to disabled`, () => { - writeFile('/index.ts', ` - import { NgModule } from '@angular/core'; - import { RouterModule } from '@angular/router'; - - @NgModule({ - imports: [ - RouterModule.forRoot([], {initialNavigation: false}), - ] - }) - export class AppModule { - } - `); - - runTSLint(); - - expect(getFile('/index.ts')).toContain(`{initialNavigation: 'disabled'}`); - }); - - it('should migrate legacy_enabled to enabledNonBlocking', () => { - writeFile('/index.ts', ` - import { NgModule } from '@angular/core'; - import { RouterModule } from '@angular/router'; - - @NgModule({ - imports: [ - RouterModule.forRoot([], {initialNavigation: 'legacy_enabled'}), - ] - }) - export class AppModule { - } - `); - - runTSLint(true); - - expect(getFile('/index.ts')).toContain(`{initialNavigation: 'enabledNonBlocking'}`); - }); - - it(`should migrate true to enabledNonBlocking`, () => { - writeFile('/index.ts', ` - mport { NgModule } from '@angular/core'; - import { RouterModule } from '@angular/router'; - - @NgModule({ - imports: [ - RouterModule.forRoot([], {initialNavigation: true}), - ] - }) - export class AppModule { - } - `); - - runTSLint(true); - - expect(getFile('/index.ts')).toContain(`{initialNavigation: 'enabledNonBlocking'}`); - }); - - it('should migrate nested objects', () => { - writeFile('/index.ts', ` - import { NgModule } from '@angular/core'; - import { RouterModule } from '@angular/router'; - - const options = {initialNavigation: 'legacy_enabled'}; - - @NgModule({ - imports: [ - RouterModule.forRoot([], {initialNavigation: 'legacy_disabled', ...options}), - ] - }) - export class AppModule { - } - `); - - runTSLint(true); - expect(getFile('/index.ts')) - .toContain(`const options = {initialNavigation: 'enabledNonBlocking'};`); - expect(getFile('/index.ts')).toContain(`{initialNavigation: 'disabled', ...options}`); - }); - - it('should migrate nested objects mixed validity', () => { - writeFile('/index.ts', ` - import { NgModule } from '@angular/core'; - import { RouterModule } from '@angular/router'; - - const options = {initialNavigation: 'legacy_enabled'}; - - @NgModule({ - imports: [ - RouterModule.forRoot([], {initialNavigation: 'disabled', ...options}), - ] - }) - export class AppModule { - } - `); - - runTSLint(true); - expect(getFile('/index.ts')) - .toContain(`const options = {initialNavigation: 'enabledNonBlocking'};`); - }); - - it('should migrate nested objects opposite order', () => { - writeFile('/index.ts', ` - import { NgModule } from '@angular/core'; - import { RouterModule } from '@angular/router'; - - const options = {initialNavigation: 'legacy_enabled'}; - - @NgModule({ - imports: [ - RouterModule.forRoot([], {...options, initialNavigation: 'legacy_disabled'}), - ] - }) - export class AppModule { - } - `); - - runTSLint(true); - expect(getFile('/index.ts')) - .toContain(`const options = {initialNavigation: 'enabledNonBlocking'};`); - expect(getFile('/index.ts')).toContain(`{...options, initialNavigation: 'disabled'}`); - }); - - it('should migrate nested objects mixed validity opposite order', () => { - writeFile('/index.ts', ` - import { NgModule } from '@angular/core'; - import { RouterModule } from '@angular/router'; - - const options = {initialNavigation: 'legacy_enabled'}; - - @NgModule({ - imports: [ - RouterModule.forRoot([], {...options, initialNavigation: 'disabled'}), - ] - }) - export class AppModule { - } - `); - - runTSLint(true); - expect(getFile('/index.ts')) - .toContain(`const options = {initialNavigation: 'enabledNonBlocking'};`); - expect(getFile('/index.ts')).toContain(`{...options, initialNavigation: 'disabled'}`); - }); -}); diff --git a/packages/core/schematics/test/google3/missing_injectable_rule_spec.ts b/packages/core/schematics/test/google3/missing_injectable_rule_spec.ts deleted file mode 100644 index de6f4503790..00000000000 --- a/packages/core/schematics/test/google3/missing_injectable_rule_spec.ts +++ /dev/null @@ -1,271 +0,0 @@ -/** - * @license - * Copyright Google LLC 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/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}}); - - 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'); - } - - describe('NgModule', () => createTests('NgModule', 'providers')); - describe('Directive', () => createTests('Directive', 'providers')); - - describe('Component', () => { - createTests('Component', 'providers'); - createTests('Component', 'viewProviders'); - - it('should migrate all providers defined in "viewProviders" and "providers" in the ' + - 'same component', - () => { - writeFile('/index.ts', ` - import {Component} from '@angular/core'; - - export class MyService {} - export class MySecondService {} - - @Component({ - providers: [MyService], - viewProviders: [MySecondService], - }) - export class TestClass {} - `); - - const result = runTSLint().getResult(); - - expect(result.errorCount).toBe(0); - expect(getFile('/index.ts')).toMatch(/@Injectable\(\)\s+export class MyService/); - expect(getFile('/index.ts')).toMatch(/@Injectable\(\)\s+export class MySecondService/); - expect(getFile('/index.ts')).toContain(`{ Component, Injectable } from '@angular/core`); - }); - }); - - function createTests( - type: 'NgModule'|'Directive'|'Component', propName: 'providers'|'viewProviders') { - it('should create proper failures for missing injectable providers', () => { - writeFile('index.ts', ` - import { ${type} } from '@angular/core'; - - export class A {} - - @${type}({${propName}: [A]}) - export class TestClass {} - `); - - 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 "TestClass"/); - expect(failures[0].getStartPosition().getLineAndCharacter()).toEqual({line: 3, character: 8}); - expect(failures[1].getFailure()).toMatch(/Import needs to be updated to import.*Injectable/); - expect(failures[1].getStartPosition().getLineAndCharacter()) - .toEqual({line: 1, character: 15}); - }); - - 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 {} - export class MyServiceH {} - - @${type}({${propName}: [ - WithPipe, - [ - WithDirective, - WithComponent, - MyServiceA, - ] - MyServiceB, - {provide: MyServiceC}, - {provide: null, useClass: MyServiceD}, - {provide: null, useExisting: MyServiceE}, - {provide: MyServiceF, useFactory: () => null}, - {provide: MyServiceG, useValue: null}, - {provide: MyServiceH, deps: []}, - ]}) - export class TestClass {} - `); - - - 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(/MyServiceB {}\s+export class MyServiceC/); - expect(getFile('/index.ts')).toMatch(/@Injectable\(\)\s+export class MyServiceD/); - expect(getFile('/index.ts')).toMatch(/MyServiceD {}\s+export class MyServiceE/); - expect(getFile('/index.ts')).toMatch(/MyServiceE {}\s+export class MyServiceF/); - expect(getFile('/index.ts')).toMatch(/MyServiceF {}\s+export class MyServiceG/); - expect(getFile('/index.ts')).toMatch(/MyServiceG {}\s+export class MyServiceH/); - expect(getFile('/index.ts')).toContain(`{ provide: MyServiceC, useValue: undefined },`); - }); - - it(`should migrate provider once if referenced in multiple ${type} definitions`, () => { - writeFile('/index.ts', ` - import {${type}} from '@angular/core'; - - export class ServiceA {} - - @${type}({${propName}: [ServiceA]}) - export class TestClass {} - `); - - writeFile('/second.ts', ` - import {${type}} from '@angular/core'; - import {ServiceA} from './index'; - - export class ServiceB {} - - @${type}({${propName}: [ServiceA, ServiceB]}) - export class TestClass2 {} - `); - - runTSLint(); - - expect(getFile('/index.ts')) - .toMatch(/@angular\/core';\s+@Injectable\(\)\s+export class ServiceA/); - expect(getFile('/index.ts')).toContain(`{ ${type}, Injectable } from '@angular/core`); - expect(getFile('/second.ts')).toMatch(/@Injectable\(\)\s+export class ServiceB/); - expect(getFile('/second.ts')).toContain(`{ ${type}, Injectable } from '@angular/core`); - }); - - it('should warn if a referenced provider could not be resolved', () => { - writeFile('/index.ts', ` - import {${type}} from '@angular/core'; - - @${type}({${propName}: [NotPresent]}) - export class TestClass {} - `); - - 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: 14 + type.length + propName.length}); - }); - - it(`should warn if the "${propName}" value could not be resolved`, () => { - writeFile('/index.ts', ` - import {${type}} from '@angular/core'; - - @${type}({${propName}: NOT_ANALYZABLE) - export class TestClass {} - `); - - const linter = runTSLint(); - const failures = linter.getResult().failures; - - expect(failures.length).toBe(1); - expect(failures[0].getFailure()).toMatch(/Providers.*not statically analyzable./); - expect(failures[0].getStartPosition().getLineAndCharacter()) - .toEqual({line: 3, character: 13 + type.length + propName.length}); - }); - - it('should create new import for @Injectable when migrating provider', () => { - writeFile('/index.ts', ` - import {${type}} from '@angular/core'; - import {MyService, MySecondService} from './service'; - - @${type}({${propName}: [MyService, MySecondService]}) - export class TestClass {} - `); - - 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 {${type}} from '@angular/core'; - import {MyService} from './service'; - - @${type}({${propName}: [MyService]}) - export class TestClass {} - `); - - 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';/); - }); - } -}); diff --git a/packages/core/schematics/test/google3/navigation_extras_omissions_spec.ts b/packages/core/schematics/test/google3/navigation_extras_omissions_spec.ts deleted file mode 100644 index 42325de7c02..00000000000 --- a/packages/core/schematics/test/google3/navigation_extras_omissions_spec.ts +++ /dev/null @@ -1,334 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 NavigationExtras omissions TSLint rule', () => { - const rulesDirectory = - dirname(require.resolve('../../migrations/google3/navigationExtrasOmissionsRule')); - - let tmpDir: string; - - beforeEach(() => { - tmpDir = join(process.env['TEST_TMPDIR']!, 'google3-test'); - shx.mkdir('-p', tmpDir); - - // We need to declare the Angular symbols we're testing for, otherwise type checking won't work. - writeFile('router.d.ts', ` - export declare class Router { - navigateByUrl(url: string, extras?: any); - createUrlTree(commands: any[], extras?: any); - } - `); - - writeFile('tsconfig.json', JSON.stringify({ - compilerOptions: { - module: 'es2015', - baseUrl: './', - paths: { - '@angular/router': ['router.d.ts'], - } - }, - })); - }); - - afterEach(() => shx.rm('-r', tmpDir)); - - function runTSLint(fix: boolean) { - const program = Linter.createProgram(join(tmpDir, 'tsconfig.json')); - const linter = new Linter({fix, rulesDirectory: [rulesDirectory]}, program); - const config = Configuration.parseConfigFile({rules: {'navigation-extras-omissions': 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 flag objects with invalid properties used inside the relevant method calls', () => { - writeFile('/index.ts', ` - import {Router} from '@angular/router'; - - class Navigator { - constructor(private _router: Router) {} - - goHome() { - this._router.navigateByUrl('/', {fragment: 'foo'}); - } - - createTree() { - return this._router.createUrlTree(['/'], {state: {}}); - } - - goAway() { - this._router.navigateByUrl('/away'); - } - } - `); - - const linter = runTSLint(false); - const failures = linter.getResult().failures.map(failure => failure.getFailure()); - - expect(failures.length).toBe(2); - expect(failures[0]) - .toMatch( - /Object used in navigateByUrl or createUrlTree call contains unsupported properties/); - expect(failures[1]) - .toMatch( - /Object used in navigateByUrl or createUrlTree call contains unsupported properties/); - }); - - it('should not change calls with a single argument', () => { - writeFile('/index.ts', ` - import {Router} from '@angular/router'; - - class Navigator { - constructor(private _router: Router) {} - - goHome() { - this._router.navigateByUrl('/'); - } - } - - function createTree(router: Router) { - return router.createUrlTree(['/']); - } - `); - - runTSLint(true); - - const content = getFile('/index.ts'); - expect(content).toContain(`this._router.navigateByUrl('/');`); - expect(content).toContain(`return router.createUrlTree(['/']);`); - }); - - it('should not change calls with an empty object literal', () => { - writeFile('/index.ts', ` - import {Router} from '@angular/router'; - - class Navigator { - constructor(private _router: Router) {} - - goHome() { - this._router.navigateByUrl('/', {}); - } - } - - function createTree(router: Router) { - return router.createUrlTree(['/'], {}); - } - `); - - runTSLint(true); - - const content = getFile('/index.ts'); - expect(content).toContain(`this._router.navigateByUrl('/', {});`); - expect(content).toContain(`return router.createUrlTree(['/'], {});`); - }); - - it('should not change objects that are used in multiple different methods', () => { - writeFile('/index.ts', ` - import {Router} from '@angular/router'; - - const config = {replaceUrl: true, fragment: 'foo', state: {}}; - - class Navigator { - constructor(private _router: Router) {} - - goHome() { - this._router.navigateByUrl('/', config); - return this._router.createUrlTree(['/'], config); - } - } - `); - - runTSLint(true); - - const content = getFile('/index.ts'); - expect(content).toContain(`const config = {replaceUrl: true, fragment: 'foo', state: {}};`); - }); - - it('should preserve calls if the router does not come from @angular/router', () => { - writeFile('/index.ts', ` - import {Router} from '@custom/router'; - - function createTree(router: Router) { - return router.createUrlTree(['/'], {foo: 1, bar: 2}); - } - `); - - runTSLint(true); - - const content = getFile('/index.ts'); - expect(content).toContain(`return router.createUrlTree(['/'], {foo: 1, bar: 2});`); - }); - - it('should change invalid navigateByUrl calls', () => { - writeFile('/index.ts', ` - import {Router} from '@angular/router'; - - class Navigator { - constructor(private _router: Router) {} - - goHome() { - this._router.navigateByUrl('/', {preserveFragment: false, skipLocationChange: false, fragment: 'foo'}); - } - } - `); - - runTSLint(true); - - const content = getFile('/index.ts'); - expect(content).toContain( - `this._router.navigateByUrl('/', { /* Removed unsupported properties by Angular migration: preserveFragment, fragment. */ skipLocationChange: false });`); - }); - - it('should change invalid navigateByUrl calls', () => { - writeFile('/index.ts', ` - import {Router} from '@angular/router'; - - function createTree(router: Router) { - return router.createUrlTree(['/'], {replaceUrl: true, preserveFragment: true, state: {}}); - } - `); - - runTSLint(true); - - const content = getFile('/index.ts'); - expect(content).toContain( - `return router.createUrlTree(['/'], { /* Removed unsupported properties by Angular migration: replaceUrl, state. */ preserveFragment: true });`); - }); - - it('should set the comment outside the object if all properties were removed', () => { - writeFile('/index.ts', ` - import {Router} from '@angular/router'; - - function navigate(router: Router) { - router.navigateByUrl('/', {fragment: 'foo'}); - } - `); - - runTSLint(true); - - const content = getFile('/index.ts'); - expect(content).toContain( - `router.navigateByUrl('/', /* Removed unsupported properties by Angular migration: fragment. */ {});`); - }); - - it('should migrate object literals defined as variables', () => { - writeFile('/index.ts', ` - import {Router} from '@angular/router'; - - const config = {skipLocationChange: false, fragment: 'foo'}; - const proxy = config; - - function navigate(router: Router) { - router.navigateByUrl('/', proxy); - } - `); - - runTSLint(true); - - const content = getFile('/index.ts'); - expect(content).toContain( - `const config = { /* Removed unsupported properties by Angular migration: fragment. */ skipLocationChange: false };`); - expect(content).toContain(`const proxy = config;`); - expect(content).toContain(`router.navigateByUrl('/', proxy);`); - }); - - it('should pick up calls where the router is returned by a function', () => { - writeFile('/index.ts', ` - import {Router} from '@angular/router'; - - function navigate(router: Router) { - getRouter().navigateByUrl('/', {fragment: 'foo'}); - } - - function getRouter() { - return {} as Router; - } - `); - - runTSLint(true); - - const content = getFile('/index.ts'); - expect(content).toContain( - `getRouter().navigateByUrl('/', /* Removed unsupported properties by Angular migration: fragment. */ {});`); - }); - - it('should pick up calls where the router is aliased', () => { - writeFile('/index.ts', ` - import {Router as AliasedRouter} from '@angular/router'; - - function navigate(router: AliasedRouter) { - router.navigateByUrl('/', {fragment: 'foo'}); - } - `); - - runTSLint(true); - - const content = getFile('/index.ts'); - expect(content).toContain( - `router.navigateByUrl('/', /* Removed unsupported properties by Angular migration: fragment. */ {});`); - }); - - it('should preserve object spread assignments', () => { - writeFile('/index.ts', ` - import {Router} from '@angular/router'; - - function navigate(router: Router) { - const overrides = {foo: 1}; - router.navigateByUrl('/', {replaceUrl: true, fragment: 'foo', ...overrides}); - } - `); - - runTSLint(true); - - const content = getFile('/index.ts'); - expect(content).toContain( - `router.navigateByUrl('/', { /* Removed unsupported properties by Angular migration: fragment. */ replaceUrl: true, ...overrides });`); - }); - - it('should migrate objects that are used in multiple calls of the same method', () => { - writeFile('/index.ts', ` - import {Router} from '@angular/router'; - - const config = {skipLocationChange: false, fragment: 'foo'}; - - class Navigator { - constructor(private _router: Router) {} - - goHome() { - this._router.navigateByUrl('/', config); - } - - goFish() { - this._router.navigateByUrl('/fish', config); - } - } - `); - - runTSLint(true); - - const content = getFile('/index.ts'); - expect(content).toContain( - `const config = { /* Removed unsupported properties by Angular migration: fragment. */ skipLocationChange: false };`); - }); -}); diff --git a/packages/core/schematics/test/google3/no_template_variable_assignment_rule_spec.ts b/packages/core/schematics/test/google3/no_template_variable_assignment_rule_spec.ts deleted file mode 100644 index 44ee0095972..00000000000 --- a/packages/core/schematics/test/google3/no_template_variable_assignment_rule_spec.ts +++ /dev/null @@ -1,110 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 {writeFileSync} from 'fs'; -import {dirname, join} from 'path'; -import * as shx from 'shelljs'; -import {Configuration, Linter} from 'tslint'; - -describe('Google3 noTemplateVariableAssignment TSLint rule', () => { - const rulesDirectory = - dirname(require.resolve('../../migrations/google3/noTemplateVariableAssignmentRule')); - - 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)); - - /** Runs TSLint with the no-template-variable TSLint rule.*/ - function runTSLint() { - const program = Linter.createProgram(join(tmpDir, 'tsconfig.json')); - const linter = new Linter({fix: false, rulesDirectory: [rulesDirectory]}, program); - const config = - Configuration.parseConfigFile({rules: {'no-template-variable-assignment': true}}); - - program.getRootFileNames().forEach(fileName => { - linter.lint(fileName, program.getSourceFile(fileName)!.getFullText(), config); - }); - - return linter; - } - - /** Writes a file to the current temporary directory. */ - function writeFile(fileName: string, content: string) { - writeFileSync(join(tmpDir, fileName), content); - } - - it('should create failure for detected two-way data binding assignment', () => { - writeFile('index.ts', ` - import {Component} from '@angular/core'; - - @Component({template: ''}) - export class MyComp {} - `); - - const linter = runTSLint(); - const failures = linter.getResult().failures; - - expect(failures.length).toBe(1); - expect(failures[0].getFileName()).toContain('index.ts'); - expect(failures[0].getStartPosition().getLineAndCharacter()).toEqual({line: 3, character: 68}); - expect(failures[0].getEndPosition().getLineAndCharacter()).toEqual({line: 3, character: 69}); - expect(failures[0].getFailure()).toMatch(/^Found assignment to template variable./); - }); - - it('should create failure with correct offsets for external templates', () => { - writeFile('index.ts', ` - import {Component} from '@angular/core'; - - @Component({templateUrl: './my-tmpl.html'}) - export class MyComp {} - `); - - writeFile(`my-tmpl.html`, ` - - `); - - const linter = runTSLint(); - const failures = linter.getResult().failures; - - expect(failures.length).toBe(1); - expect(failures[0].getFileName()).toContain('my-tmpl.html'); - expect(failures[0].getStartPosition().getLineAndCharacter()).toEqual({line: 1, character: 50}); - expect(failures[0].getEndPosition().getLineAndCharacter()).toEqual({line: 1, character: 56}); - expect(failures[0].getFailure()).toMatch(/^Found assignment to template variable./); - }); - - it('should create failure for template variable assignment within output', () => { - writeFile('index.ts', ` - import {Component} from '@angular/core'; - - @Component({templateUrl: './my-tmpl.html'}) - export class MyComp {} - `); - - writeFile(`my-tmpl.html`, ` - - - `); - - const linter = runTSLint(); - const failures = linter.getResult().failures; - - expect(failures.length).toBe(1); - expect(failures[0].getFileName()).toContain('my-tmpl.html'); - expect(failures[0].getStartPosition().getLineAndCharacter()).toEqual({line: 2, character: 52}); - expect(failures[0].getEndPosition().getLineAndCharacter()).toEqual({line: 2, character: 65}); - expect(failures[0].getFailure()).toMatch(/^Found assignment to template variable./); - }); -}); diff --git a/packages/core/schematics/test/google3/relative_link_resolution_default_spec.ts b/packages/core/schematics/test/google3/relative_link_resolution_default_spec.ts deleted file mode 100644 index 700e291983a..00000000000 --- a/packages/core/schematics/test/google3/relative_link_resolution_default_spec.ts +++ /dev/null @@ -1,202 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 relativeLinkResolution TSLint rule', () => { - const rulesDirectory = - dirname(require.resolve('../../migrations/google3/relativeLinkResolutionDefaultRule')); - - 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: boolean) { - const program = Linter.createProgram(join(tmpDir, 'tsconfig.json')); - const linter = new Linter({fix, rulesDirectory: [rulesDirectory]}, program); - const config = Configuration.parseConfigFile({ - rules: {'relative-link-resolution-default': 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 flag forRoot with no options', () => { - writeFile('/index.ts', ` - import { NgModule } from '@angular/core'; - import { RouterModule } from '@angular/router'; - @NgModule({ - imports: [ - RouterModule.forRoot([]), - ] - }) - export class AppModule { - } - `); - - const linter = runTSLint(false); - const failures = linter.getResult().failures.map(failure => failure.getFailure()); - - expect(failures.length).toBe(1); - expect(failures[0]) - .toBe( - 'The relativeLinkResolution default is changing from `legacy` to `corrected`. To keep' + - ' behavior consistent when the change is merged, specify `legacy` rather than using the default.'); - }); - - it('should migrate forRoot with no options', () => { - writeFile('/index.ts', ` - import { NgModule } from '@angular/core'; - import { RouterModule } from '@angular/router'; - @NgModule({ - imports: [ - RouterModule.forRoot([]), - ] - }) - export class AppModule { - } - `); - - runTSLint(true); - expect(getFile('/index.ts')) - .toContain(`RouterModule.forRoot([], { relativeLinkResolution: 'legacy' })`); - }); - - it('should migrate options without relativeLinkResolution', () => { - writeFile('/index.ts', ` - import { NgModule } from '@angular/core'; - import { RouterModule } from '@angular/router'; - @NgModule({ - imports: [ - RouterModule.forRoot([], {useHash: true}), - ] - }) - export class AppModule { - } - `); - - runTSLint(true); - expect(getFile('/index.ts')) - .toContain(`RouterModule.forRoot([], { useHash: true, relativeLinkResolution: 'legacy' })`); - }); - - it('should not migrate options containing relativeLinkResolution', () => { - writeFile('/index.ts', ` - import { NgModule } from '@angular/core'; - import { RouterModule } from '@angular/router'; - @NgModule({ - imports: [ - RouterModule.forRoot([], {relativeLinkResolution: 'corrected'}), - ] - }) - export class AppModule { - } - `); - - runTSLint(true); - expect(getFile('/index.ts')) - .toContain(`RouterModule.forRoot([], {relativeLinkResolution: 'corrected'})`); - }); - - it('should migrate when options is a variable with AsExpression', () => { - writeFile('/index.ts', ` - import { ExtraOptions } from '@angular/router'; - const options = {useHash: true} as ExtraOptions; - `); - - runTSLint(true); - expect(getFile('/index.ts')) - .toContain( - `const options = { useHash: true, relativeLinkResolution: 'legacy' } as ExtraOptions;`); - }); - - it('should migrate when options is a variable', () => { - writeFile('/index.ts', ` - import { ExtraOptions } from '@angular/router'; - const options: ExtraOptions = {useHash: true}; - `); - - runTSLint(true); - expect(getFile('/index.ts')) - .toContain( - `const options: ExtraOptions = { useHash: true, relativeLinkResolution: 'legacy' };`); - }); - - it('should migrate when options is a variable with no type', () => { - writeFile('/index.ts', ` - import { NgModule } from '@angular/core'; - import { ExtraOptions, RouterModule } from '@angular/router'; - - const options = {useHash: true}; - - @NgModule({ - imports: [ - RouterModule.forRoot([], options), - ] - }) - export class AppModule { - } - `); - - runTSLint(true); - expect(getFile('/index.ts')) - .toContain(`const options = { useHash: true, relativeLinkResolution: 'legacy' };`); - expect(getFile('/index.ts')).toContain(`RouterModule.forRoot([], options)`); - }); - - it('should migrate when aliased options is a variable', () => { - writeFile('/index.ts', ` - import { ExtraOptions as RouterExtraOptions } from '@angular/router'; - const options: RouterExtraOptions = {useHash: true}; - `); - - runTSLint(true); - expect(getFile('/index.ts')) - .toContain( - `const options: RouterExtraOptions = { useHash: true, relativeLinkResolution: 'legacy' };`); - }); - - it('should migrate aliased RouterModule.forRoot', () => { - writeFile('/index.ts', ` - import { NgModule } from '@angular/core'; - import { RouterModule as AngularRouterModule} from '@angular/router'; - @NgModule({ - imports: [ - AngularRouterModule.forRoot([]), - ] - }) - export class AppModule { - } - `); - - runTSLint(true); - expect(getFile('/index.ts')) - .toContain(`AngularRouterModule.forRoot([], { relativeLinkResolution: 'legacy' }),`); - }); -}); diff --git a/packages/core/schematics/test/google3/renderer_to_renderer2_spec.ts b/packages/core/schematics/test/google3/renderer_to_renderer2_spec.ts deleted file mode 100644 index c71620cf5f4..00000000000 --- a/packages/core/schematics/test/google3/renderer_to_renderer2_spec.ts +++ /dev/null @@ -1,435 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 Renderer to Renderer2 TSLint rule', () => { - const rulesDirectory = - dirname(require.resolve('../../migrations/google3/rendererToRenderer2Rule')); - - let tmpDir: string; - - beforeEach(() => { - tmpDir = join(process.env['TEST_TMPDIR']!, 'google3-test'); - shx.mkdir('-p', tmpDir); - - // We need to declare the Angular symbols we're testing for, otherwise type checking won't work. - writeFile('angular.d.ts', ` - export declare abstract class Renderer {} - export declare function forwardRef(fn: () => any): any {} - `); - - writeFile('tsconfig.json', JSON.stringify({ - compilerOptions: { - module: 'es2015', - baseUrl: './', - paths: { - '@angular/core': ['angular.d.ts'], - } - }, - })); - }); - - afterEach(() => shx.rm('-r', tmpDir)); - - function runTSLint(fix: boolean) { - const program = Linter.createProgram(join(tmpDir, 'tsconfig.json')); - const linter = new Linter({fix, rulesDirectory: [rulesDirectory]}, program); - const config = Configuration.parseConfigFile({rules: {'renderer-to-renderer2': 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 flag Renderer imports and typed nodes', () => { - writeFile('/index.ts', ` - import { Renderer, Component } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - public renderer: Renderer; - - constructor(renderer: Renderer) { - this.renderer = renderer; - } - } - `); - - const linter = runTSLint(false); - const failures = linter.getResult().failures.map(failure => failure.getFailure()); - - expect(failures.length).toBe(3); - expect(failures[0]).toMatch(/Imports of deprecated Renderer are not allowed/); - expect(failures[1]).toMatch(/References to deprecated Renderer are not allowed/); - expect(failures[2]).toMatch(/References to deprecated Renderer are not allowed/); - }); - - it('should change Renderer imports and typed nodes to Renderer2', () => { - writeFile('/index.ts', ` - import { Renderer, Component } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - public renderer: Renderer; - - constructor(renderer: Renderer) { - this.renderer = renderer; - } - } - `); - - runTSLint(true); - const content = getFile('index.ts'); - - expect(content).toContain(`import { Component, Renderer2 } from '@angular/core';`); - expect(content).toContain('public renderer: Renderer2;'); - expect(content).toContain('(renderer: Renderer2)'); - }); - - it('should not change Renderer imports if Renderer2 is already imported', () => { - writeFile('/index.ts', ` - import { Renderer, Component, Renderer2 } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - public renderer: Renderer; - - constructor(renderer: Renderer) { - this.renderer = renderer; - } - } - `); - - runTSLint(true); - const content = getFile('index.ts'); - - expect(content).toContain(`import { Renderer, Component, Renderer2 } from '@angular/core';`); - }); - - it('should change Renderer inside single-line forwardRefs to Renderer2', () => { - writeFile('/index.ts', ` - import { Renderer, Component, forwardRef, Inject } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(@Inject(forwardRef(() => Renderer)) private _renderer: Renderer) {} - } - `); - - runTSLint(true); - const content = getFile('index.ts'); - - expect(content).toContain( - `constructor(@Inject(forwardRef(() => Renderer2)) private _renderer: Renderer2) {}`); - }); - - it('should change Renderer inside multi-line forwardRefs to Renderer2', () => { - writeFile('/index.ts', ` - import { Renderer, Component, forwardRef, Inject } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(@Inject(forwardRef(() => { return Renderer; })) private _renderer: Renderer) {} - } - `); - - runTSLint(true); - const content = getFile('index.ts'); - - expect(content).toContain( - `constructor(@Inject(forwardRef(() => { return Renderer2; })) private _renderer: Renderer2) {}`); - }); - - it('should flag something that was cast to Renderer', () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - setColor(maybeRenderer: any, element: ElementRef) { - const renderer = maybeRenderer as Renderer; - renderer.setElementStyle(element.nativeElement, 'color', 'red'); - } - } - `); - - const linter = runTSLint(false); - const failures = linter.getResult().failures.map(failure => failure.getFailure()); - - expect(failures.length).toBe(3); - expect(failures[0]).toMatch(/Imports of deprecated Renderer are not allowed/); - expect(failures[1]).toMatch(/References to deprecated Renderer are not allowed/); - expect(failures[2]).toMatch(/Calls to Renderer methods are not allowed/); - }); - - it('should change the type of something that was cast to Renderer', () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - setColor(maybeRenderer: any, element: ElementRef) { - const renderer = maybeRenderer as Renderer; - renderer.setElementStyle(element.nativeElement, 'color', 'red'); - } - } - `); - - runTSLint(true); - const content = getFile('index.ts'); - - expect(content).toContain(`const renderer = maybeRenderer as Renderer2;`); - expect(content).toContain(`renderer.setStyle(element.nativeElement, 'color', 'red');`); - }); - - it('should be able to insert helper functions', () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(renderer: Renderer, element: ElementRef) { - const el = renderer.createElement(element.nativeElement, 'div'); - renderer.setElementAttribute(el, 'title', 'hello'); - renderer.projectNodes(element.nativeElement, [el]); - } - } - `); - - runTSLint(true); - const content = getFile('index.ts'); - - expect(content).toContain(`function __ngRendererCreateElementHelper(`); - expect(content).toContain(`function __ngRendererSetElementAttributeHelper(`); - expect(content).toContain(`function __ngRendererProjectNodesHelper(`); - }); - - it('should only insert each helper only once per file', () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(renderer: Renderer, element: ElementRef) { - const el = renderer.createElement(element.nativeElement, 'div'); - renderer.setElementAttribute(el, 'title', 'hello'); - - const el1 = renderer.createElement(element.nativeElement, 'div'); - renderer.setElementAttribute(el2, 'title', 'hello'); - - const el2 = renderer.createElement(element.nativeElement, 'div'); - renderer.setElementAttribute(el2, 'title', 'hello'); - } - } - `); - - runTSLint(true); - const content = getFile('index.ts'); - - expect(content.match(/function __ngRendererCreateElementHelper\(/g)!.length).toBe(1); - expect(content.match(/function __ngRendererSetElementAttributeHelper\(/g)!.length).toBe(1); - }); - - it('should insert helpers after the user\'s code', () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(renderer: Renderer, element: ElementRef) { - const el = renderer.createElement(element.nativeElement, 'div'); - renderer.setElementAttribute(el, 'title', 'hello'); - } - } - - //--- - `); - - runTSLint(true); - const content = getFile('index.ts'); - const [contentBeforeSeparator, contentAfterSeparator] = content.split('//---'); - - expect(contentBeforeSeparator).not.toContain('function __ngRendererCreateElementHelper('); - expect(contentAfterSeparator).toContain('function __ngRendererCreateElementHelper('); - }); - - // Note that this is intended primarily as a sanity test. All of the replacement logic is the - // same between the lint rule and the CLI migration so there's not much value in repeating and - // maintaining the same tests twice. The migration's tests are more exhaustive. - it('should flag calls to Renderer methods', () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _renderer: Renderer, private _element: ElementRef) { - const span = _renderer.createElement(_element.nativeElement, 'span'); - const greeting = _renderer.createText(_element.nativeElement, 'hello'); - const color = 'red'; - - _renderer.setElementProperty(_element.nativeElement, 'disabled', true); - _renderer.listenGlobal('window', 'resize', () => console.log('resized')); - _renderer.setElementAttribute(_element.nativeElement, 'title', 'hello'); - _renderer.createViewRoot(_element.nativeElement); - _renderer.animate(_element.nativeElement); - _renderer.detachView([]); - _renderer.destroyView(_element.nativeElement, []); - _renderer.invokeElementMethod(_element.nativeElement, 'focus', []); - _renderer.setElementStyle(_element.nativeElement, 'color', color); - _renderer.setText(_element.nativeElement.querySelector('span'), 'Hello'); - } - - getRootElement() { - return this._renderer.selectRootElement(this._element.nativeElement, {}); - } - - toggleClass(className: string, shouldAdd: boolean) { - this._renderer.setElementClass(this._element.nativeElement, className, shouldAdd); - } - - setInfo() { - this._renderer.setBindingDebugInfo(this._element.nativeElement, 'prop', 'value'); - } - - createAndAppendAnchor() { - return this._renderer.createTemplateAnchor(this._element.nativeElement); - } - - attachViewAfter(rootNodes) { - this._renderer.attachViewAfter(this._element.nativeElement, rootNodes); - } - - projectNodes(nodesToProject: Node[]) { - this._renderer.projectNodes(this._element.nativeElement, nodesToProject); - } - } - `); - - const linter = runTSLint(false); - const failures = linter.getResult().failures.map(failure => failure.getFailure()); - - // One failure for the import, one for the constructor param, one at the end that is used as - // an anchor for inserting helper functions and the rest are for method calls. - expect(failures.length).toBe(21); - expect(failures[0]).toMatch(/Imports of deprecated Renderer are not allowed/); - expect(failures[1]).toMatch(/References to deprecated Renderer are not allowed/); - expect(failures[failures.length - 1]).toMatch(/File should contain Renderer helper functions/); - expect(failures.slice(2, -1).every(message => { - return /Calls to Renderer methods are not allowed/.test(message); - })).toBe(true); - }); - - // Note that this is intended primarily as a sanity test. All of the replacement logic is the - // same between the lint rule and the CLI migration so there's not much value in repeating and - // maintaining the same tests twice. The migration's tests are more exhaustive. - it('should fix calls to Renderer methods', () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _renderer: Renderer, private _element: ElementRef) { - const span = _renderer.createElement(_element.nativeElement, 'span'); - const greeting = _renderer.createText(_element.nativeElement, 'hello'); - const color = 'red'; - - _renderer.setElementProperty(_element.nativeElement, 'disabled', true); - _renderer.listenGlobal('window', 'resize', () => console.log('resized')); - _renderer.setElementAttribute(_element.nativeElement, 'title', 'hello'); - _renderer.animate(_element.nativeElement); - _renderer.detachView([]); - _renderer.destroyView(_element.nativeElement, []); - _renderer.invokeElementMethod(_element.nativeElement, 'focus', []); - _renderer.setElementStyle(_element.nativeElement, 'color', color); - _renderer.setText(_element.nativeElement.querySelector('span'), 'Hello'); - } - - createRoot() { - return this._renderer.createViewRoot(this._element.nativeElement); - } - - getRootElement() { - return this._renderer.selectRootElement(this._element.nativeElement, {}); - } - - toggleClass(className: string, shouldAdd: boolean) { - this._renderer.setElementClass(this._element.nativeElement, className, shouldAdd); - } - - setInfo() { - this._renderer.setBindingDebugInfo(this._element.nativeElement, 'prop', 'value'); - } - - createAndAppendAnchor() { - return this._renderer.createTemplateAnchor(this._element.nativeElement); - } - - attachViewAfter(rootNodes: Node[]) { - this._renderer.attachViewAfter(this._element.nativeElement, rootNodes); - } - - projectNodes(nodesToProject: Node[]) { - this._renderer.projectNodes(this._element.nativeElement, nodesToProject); - } - } - `); - - runTSLint(true); - const content = getFile('index.ts'); - - expect(content).toContain( - `const span = __ngRendererCreateElementHelper(_renderer, _element.nativeElement, 'span');`); - expect(content).toContain( - `const greeting = __ngRendererCreateTextHelper(_renderer, _element.nativeElement, 'hello');`); - expect(content).toContain(`_renderer.setProperty(_element.nativeElement, 'disabled', true);`); - expect(content).toContain( - `_renderer.listen('window', 'resize', () => console.log('resized'));`); - expect(content).toContain( - `__ngRendererSetElementAttributeHelper(_renderer, _element.nativeElement, 'title', 'hello');`); - expect(content).toContain('__ngRendererAnimateHelper();'); - expect(content).toContain('__ngRendererDetachViewHelper(_renderer, []);'); - expect(content).toContain('__ngRendererDestroyViewHelper(_renderer, []);'); - expect(content).toContain(`_element.nativeElement.focus()`); - expect(content).toContain( - `color == null ? _renderer.removeStyle(_element.nativeElement, 'color') : ` + - `_renderer.setStyle(_element.nativeElement, 'color', color);`); - expect(content).toContain( - `_renderer.setValue(_element.nativeElement.querySelector('span'), 'Hello')`); - expect(content).toContain( - `return this._renderer.selectRootElement(this._element.nativeElement);`); - expect(content).toContain( - `shouldAdd ? this._renderer.addClass(this._element.nativeElement, className) : ` + - `this._renderer.removeClass(this._element.nativeElement, className);`); - expect(content).toContain( - `return __ngRendererCreateTemplateAnchorHelper(this._renderer, this._element.nativeElement);`); - expect(content).toContain( - `__ngRendererAttachViewAfterHelper(this._renderer, this._element.nativeElement, rootNodes);`); - expect(content).toContain( - `__ngRendererProjectNodesHelper(this._renderer, this._element.nativeElement, nodesToProject);`); - - // Expect the `createRoot` only to return `this._element.nativeElement`. - expect(content).toMatch(/createRoot\(\) \{\s+return this\._element\.nativeElement;\s+\}/); - - // Expect the `setInfo` method to only contain whitespace. - expect(content).toMatch(/setInfo\(\) \{\s+\}/); - }); -}); diff --git a/packages/core/schematics/test/google3/undecorated_classes_with_decorated_fields_spec.ts b/packages/core/schematics/test/google3/undecorated_classes_with_decorated_fields_spec.ts deleted file mode 100644 index 04afcf0f307..00000000000 --- a/packages/core/schematics/test/google3/undecorated_classes_with_decorated_fields_spec.ts +++ /dev/null @@ -1,299 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 undecorated classes with decorated fields TSLint rule', () => { - const rulesDirectory = dirname( - require.resolve('../../migrations/google3/undecoratedClassesWithDecoratedFieldsRule')); - - 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: boolean) { - const program = Linter.createProgram(join(tmpDir, 'tsconfig.json')); - const linter = new Linter({fix, rulesDirectory: [rulesDirectory]}, program); - const config = Configuration.parseConfigFile({ - rules: {'undecorated-classes-with-decorated-fields': 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 flag undecorated classes with decorated fields', () => { - writeFile('/index.ts', ` - import { Input, Directive } from '@angular/core'; - - @Directive() - export class ValidClass { - @Input() isActive: boolean; - } - - export class InvalidClass { - @Input() isActive: boolean; - } - `); - - const linter = runTSLint(false); - const failures = linter.getResult().failures.map(failure => failure.getFailure()); - - expect(failures.length).toBe(1); - expect(failures[0]) - .toBe('Class needs to be decorated with "@Directive()" because it uses Angular features.'); - }); - - it(`should add an import for Directive if there isn't one already`, () => { - writeFile('/index.ts', ` - import { Input } from '@angular/core'; - - export class Base { - @Input() isActive: boolean; - } - `); - - runTSLint(true); - expect(getFile('/index.ts')).toContain(`import { Input, Directive } from '@angular/core';`); - }); - - it('should not change the imports if there is an import for Directive already', () => { - writeFile('/index.ts', ` - import { Directive, Input } from '@angular/core'; - - export class Base { - @Input() isActive: boolean; - } - - @Directive() - export class Child extends Base { - } - `); - - runTSLint(true); - expect(getFile('/index.ts')).toContain(`import { Directive, Input } from '@angular/core';`); - }); - - it('should not generate conflicting imports there is a different `Directive` symbol', - async () => { - writeFile('/index.ts', ` - import { HostBinding } from '@angular/core'; - - export class Directive { - // Simulates a scenario where a library defines a class named "Directive". - // We don't want to generate a conflicting import. - } - - export class MyLibrarySharedBaseClass { - @HostBinding('class.active') isActive: boolean; - } - `); - - runTSLint(true); - const fileContent = getFile('/index.ts'); - expect(fileContent) - .toContain(`import { HostBinding, Directive as Directive_1 } from '@angular/core';`); - expect(fileContent).toMatch(/@Directive_1\(\)\s+export class MyLibrarySharedBaseClass/); - }); - - it('should add @Directive to undecorated classes that have @Input', () => { - writeFile('/index.ts', ` - import { Input } from '@angular/core'; - - export class Base { - @Input() isActive: boolean; - } - `); - - runTSLint(true); - expect(getFile('/index.ts')).toContain(`@Directive()\nexport class Base {`); - }); - - it('should not change decorated classes', () => { - writeFile('/index.ts', ` - import { Input, Component, Directive, Pipe, Injectable } from '@angular/core'; - - @Component({}) - export class MyComp { - @Input() isActive: boolean; - } - - @Directive({selector: 'dir'}) - export class MyDir { - @Input() isActive: boolean; - } - - @Injectable() - export class MyService { - ngOnDestroy() {} - } - - @Pipe({name: 'my-pipe'}) - export class MyPipe { - ngOnDestroy() {} - } - `); - - runTSLint(true); - const content = getFile('/index.ts'); - expect(content).toMatch(/@Component\({}\)\s+export class MyComp {/); - expect(content).toMatch(/@Directive\({selector: 'dir'}\)\s+export class MyDir {/); - expect(content).toMatch(/@Injectable\(\)\s+export class MyService {/); - expect(content).toMatch(/@Pipe\({name: 'my-pipe'}\)\s+export class MyPipe {/); - expect(content).not.toContain('TODO'); - }); - - it('should add @Directive to undecorated classes that have @Output', () => { - writeFile('/index.ts', ` - import { Output, EventEmitter } from '@angular/core'; - - export class Base { - @Output() clicked = new EventEmitter(); - } - `); - - runTSLint(true); - expect(getFile('/index.ts')).toContain(`@Directive()\nexport class Base {`); - }); - - it('should add @Directive to undecorated classes that have a host binding', () => { - writeFile('/index.ts', ` - import { HostBinding } from '@angular/core'; - - export class Base { - @HostBinding('attr.id') - get id() { - return 'id-' + Date.now(); - } - } - `); - - runTSLint(true); - expect(getFile('/index.ts')).toContain(`@Directive()\nexport class Base {`); - }); - - it('should add @Directive to undecorated classes that have a host listener', () => { - writeFile('/index.ts', ` - import { HostListener } from '@angular/core'; - - export class Base { - @HostListener('keydown') - handleKeydown() { - console.log('Key has been pressed'); - } - } - `); - - runTSLint(true); - expect(getFile('/index.ts')).toContain(`@Directive()\nexport class Base {`); - }); - - it('should add @Directive to undecorated classes that have a ViewChild query', () => { - writeFile('/index.ts', ` - import { ViewChild, ElementRef } from '@angular/core'; - - export class Base { - @ViewChild('button') button: ElementRef; - } - `); - - runTSLint(true); - expect(getFile('/index.ts')).toContain(`@Directive()\nexport class Base {`); - }); - - it('should add @Directive to undecorated classes that have a ViewChildren query', () => { - writeFile('/index.ts', ` - import { ViewChildren, ElementRef } from '@angular/core'; - - export class Base { - @ViewChildren('button') button: ElementRef; - } - `); - - runTSLint(true); - expect(getFile('/index.ts')).toContain(`@Directive()\nexport class Base {`); - }); - - it('should add @Directive to undecorated classes that have a ContentChild query', () => { - writeFile('/index.ts', ` - import { ContentChild, ElementRef } from '@angular/core'; - - export class Base { - @ContentChild('button') button: ElementRef; - } - `); - - runTSLint(true); - expect(getFile('/index.ts')).toContain(`@Directive()\nexport class Base {`); - }); - - it('should add @Directive to undecorated classes that have a ContentChildren query', () => { - writeFile('/index.ts', ` - import { ContentChildren, ElementRef } from '@angular/core'; - - export class Base { - @ContentChildren('button') button: ElementRef; - } - `); - - runTSLint(true); - expect(getFile('/index.ts')).toContain(`@Directive()\nexport class Base {`); - }); - - it('should add @Directive to undecorated derived classes of a migrated class', async () => { - writeFile('/index.ts', ` - import { Input, Directive, NgModule } from '@angular/core'; - - export class Base { - @Input() isActive: boolean; - } - - export class DerivedA extends Base {} - export class DerivedB extends DerivedA {} - export class DerivedC extends DerivedB {} - - @Directive({selector: 'my-comp'}) - export class MyComp extends DerivedC {} - - export class MyCompWrapped extends MyComp {} - - @NgModule({declarations: [MyComp, MyCompWrapped]}) - export class AppModule {} - `); - - runTSLint(true); - const fileContent = getFile('/index.ts'); - expect(fileContent).toContain(`import { Input, Directive, NgModule } from '@angular/core';`); - expect(fileContent).toMatch(/@Directive\(\)\s+export class Base/); - expect(fileContent).toMatch(/@Directive\(\)\s+export class DerivedA/); - expect(fileContent).toMatch(/@Directive\(\)\s+export class DerivedB/); - expect(fileContent).toMatch(/@Directive\(\)\s+export class DerivedC/); - expect(fileContent).toMatch(/}\s+@Directive\(\{selector: 'my-comp'}\)\s+export class MyComp/); - expect(fileContent).toMatch(/}\s+export class MyCompWrapped/); - }); -}); diff --git a/packages/core/schematics/test/initial_navigation_migration_spec.ts b/packages/core/schematics/test/initial_navigation_migration_spec.ts deleted file mode 100644 index 77d5c930e33..00000000000 --- a/packages/core/schematics/test/initial_navigation_migration_spec.ts +++ /dev/null @@ -1,240 +0,0 @@ -/** - * @license - * Copyright Google LLC 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('initial navigation migration', () => { - let runner: SchematicTestRunner; - let host: TempScopedNodeJsSyncHost; - let tree: UnitTestTree; - let tmpDirPath: string; - let previousWorkingDir: string; - - beforeEach(() => { - runner = new SchematicTestRunner('test', require.resolve('../migrations.json')); - host = new TempScopedNodeJsSyncHost(); - tree = new UnitTestTree(new HostTree(host)); - - writeFile('/tsconfig.json', JSON.stringify({ - compilerOptions: { - lib: ['es2015'], - } - })); - writeFile('/angular.json', JSON.stringify({ - version: 1, - projects: {t: {architect: {build: {options: {tsConfig: './tsconfig.json'}}}}} - })); - - 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); - }); - - it('should migrate legacy_disabled to disabled', async () => { - writeFile('/index.ts', ` - import { NgModule } from '@angular/core'; - import { RouterModule } from '@angular/router'; - - @NgModule({ - imports: [ - RouterModule.forRoot([], {initialNavigation: 'legacy_disabled'}), - ] - }) - export class AppModule { - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toContain(`{initialNavigation: 'disabled'}`); - }); - - it('should migrate false to disabled', async () => { - writeFile('/index.ts', ` - import { NgModule } from '@angular/core'; - import { RouterModule } from '@angular/router'; - - @NgModule({ - imports: [ - RouterModule.forRoot([], {initialNavigation: false}), - ] - }) - export class AppModule { - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toContain(`{initialNavigation: 'disabled'}`); - }); - - it('should migrate legacy_enabled to enabledNonBlocking', async () => { - writeFile('/index.ts', ` - import { NgModule } from '@angular/core'; - import { RouterModule } from '@angular/router'; - - @NgModule({ - imports: [ - RouterModule.forRoot([], {initialNavigation: 'legacy_enabled'}), - ] - }) - export class AppModule { - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toContain(`{initialNavigation: 'enabledNonBlocking'}`); - }); - - it('should migrate true to enabledNonBlocking', async () => { - writeFile('/index.ts', ` - import { NgModule } from '@angular/core'; - import { RouterModule } from '@angular/router'; - - @NgModule({ - imports: [ - RouterModule.forRoot([], {initialNavigation: true}), - ] - }) - export class AppModule { - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toContain(`{initialNavigation: 'enabledNonBlocking'}`); - }); - - it('should migrate nested objects', async () => { - writeFile('/index.ts', ` - import { NgModule } from '@angular/core'; - import { RouterModule } from '@angular/router'; - - const options = {initialNavigation: 'legacy_enabled'}; - - @NgModule({ - imports: [ - RouterModule.forRoot([], {initialNavigation: 'legacy_disabled', ...options}), - ] - }) - export class AppModule { - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`const options = {initialNavigation: 'enabledNonBlocking'};`); - expect(tree.readContent('/index.ts')).toContain(`{initialNavigation: 'disabled', ...options}`); - }); - - it('should migrate nested objects mixed validity', async () => { - writeFile('/index.ts', ` - import { NgModule } from '@angular/core'; - import { RouterModule } from '@angular/router'; - - const options = {initialNavigation: 'legacy_enabled'}; - - @NgModule({ - imports: [ - RouterModule.forRoot([], {initialNavigation: 'disabled', ...options}), - ] - }) - export class AppModule { - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`const options = {initialNavigation: 'enabledNonBlocking'};`); - }); - - - it('should migrate nested objects opposite order', async () => { - writeFile('/index.ts', ` - import { NgModule } from '@angular/core'; - import { RouterModule } from '@angular/router'; - - const options = {initialNavigation: 'legacy_enabled'}; - - @NgModule({ - imports: [ - RouterModule.forRoot([], {...options, initialNavigation: 'legacy_disabled'}), - ] - }) - export class AppModule { - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`const options = {initialNavigation: 'enabledNonBlocking'};`); - expect(tree.readContent('/index.ts')).toContain(`{...options, initialNavigation: 'disabled'}`); - }); - - it('should migrate nested objects mixed validity opposite order', async () => { - writeFile('/index.ts', ` - import { NgModule } from '@angular/core'; - import { RouterModule } from '@angular/router'; - - const options = {initialNavigation: 'legacy_enabled'}; - - @NgModule({ - imports: [ - RouterModule.forRoot([], {...options, initialNavigation: 'disabled'}), - ] - }) - export class AppModule { - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`const options = {initialNavigation: 'enabledNonBlocking'};`); - expect(tree.readContent('/index.ts')).toContain(`disabled`); - }); - - it('should not migrate variable not used in forRoot', async () => { - writeFile('/index.ts', ` - import { NgModule } from '@angular/core'; - import { RouterModule } from '@angular/router'; - - const options = {initialNavigation: 'legacy_enabled'}; - - @NgModule({ - imports: [ - RouterModule.forRoot([]), - ] - }) - export class AppModule { - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`const options = {initialNavigation: 'legacy_enabled'};`); - expect(tree.readContent('/index.ts')).toContain(`RouterModule.forRoot([])`); - }); - - function writeFile(filePath: string, contents: string) { - host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); - } - - function runMigration() { - return runner.runSchematicAsync('migration-v11-router-initial-navigation-options', {}, tree) - .toPromise(); - } -}); diff --git a/packages/core/schematics/test/line_mappings_spec.ts b/packages/core/schematics/test/line_mappings_spec.ts deleted file mode 100644 index 76719cc0149..00000000000 --- a/packages/core/schematics/test/line_mappings_spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 {computeLineStartsMap, getLineAndCharacterFromPosition} from '../utils/line_mappings'; - -describe('line mappings', () => { - it('should properly compute line starts', - () => { - expect(computeLineStartsMap(` - 1 - 2`)).toEqual([0, 1, 9, 16]); - }); - - it('should properly get line and character from line starts', () => { - const lineStarts = computeLineStartsMap(` - 1 - 2`); - - expect(getLineAndCharacterFromPosition(lineStarts, 8)).toEqual({ - line: 1, - character: 7, - }); - }); -}); diff --git a/packages/core/schematics/test/missing_injectable_migration_spec.ts b/packages/core/schematics/test/missing_injectable_migration_spec.ts deleted file mode 100644 index 4624fca7ff3..00000000000 --- a/packages/core/schematics/test/missing_injectable_migration_spec.ts +++ /dev/null @@ -1,807 +0,0 @@ -/** - * @license - * Copyright Google LLC 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('../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({ - version: 1, - 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); - - writeFile('/node_modules/@angular/core/index.d.ts', ` - export declare function forwardRef(fn: Function); - `); - }); - - afterEach(() => { - shx.cd(previousWorkingDir); - shx.rm('-r', tmpDirPath); - }); - - function writeFile(filePath: string, contents: string) { - host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); - } - - function runMigration() { - return runner.runSchematicAsync('migration-v10-missing-injectable', {}, tree).toPromise(); - } - - describe('NgModule', () => createTests('NgModule', 'providers')); - describe('Directive', () => createTests('Directive', 'providers')); - - describe('Component', () => { - createTests('Component', 'providers'); - createTests('Component', 'viewProviders'); - - it('should migrate all providers defined in "viewProviders" and "providers" in the ' + - 'same component', - async () => { - writeFile('/index.ts', ` - import {Component} from '@angular/core'; - - export class MyService {} - export class MySecondService {} - - @Component({ - providers: [MyService], - viewProviders: [MySecondService], - }) - export class TestClass {} - `); - - await runMigration(); - - expect(warnOutput.length).toBe(0); - expect(tree.readContent('/index.ts')).toMatch(/@Injectable\(\)\s+export class MyService/); - expect(tree.readContent('/index.ts')) - .toMatch(/@Injectable\(\)\s+export class MySecondService/); - expect(tree.readContent('/index.ts')) - .toContain(`{ Component, Injectable } from '@angular/core`); - }); - }); - - function createTests( - type: 'NgModule'|'Directive'|'Component', propName: 'providers'|'viewProviders') { - it(`should migrate type provider in ${type}`, async () => { - writeFile('/index.ts', ` - import {${type}} from '@angular/core'; - - export class MyService {} - - @${type}({${propName}: [MyService]}) - export class TestClass {} - `); - - await runMigration(); - - expect(warnOutput.length).toBe(0); - expect(tree.readContent('/index.ts')).toMatch(/@Injectable\(\)\s+export class MyService/); - expect(tree.readContent('/index.ts')) - .toContain(`{ ${type}, Injectable } from '@angular/core`); - }); - - it(`should migrate object literal provider in ${type} to explicit value provider`, async () => { - writeFile('/index.ts', ` - import {${type}} from '@angular/core'; - - export class MyService {} - - @${type}({${propName}: [{provide: MyService}]}) - export class TestClass {} - `); - - await runMigration(); - - expect(warnOutput.length).toBe(0); - expect(tree.readContent('/index.ts')).toMatch(/@angular\/core';\s+export class MyService/); - expect(tree.readContent('/index.ts')) - .toContain(`${propName}: [{ provide: MyService, useValue: undefined }]`); - expect(tree.readContent('/index.ts')).toContain(`{${type}} from '@angular/core`); - }); - - it(`should migrate object literal provider with forwardRef in ${type}`, async () => { - writeFile('/index.ts', ` - import {${type}, forwardRef} from '@angular/core'; - - @${type}({${propName}: [forwardRef(() => MyService)]}) - export class TestClass {} - - export class MyService {} - `); - - await runMigration(); - - expect(warnOutput.length).toBe(0); - expect(tree.readContent('/index.ts')).toMatch(/@Injectable\(\)\s+export class MyService/); - expect(tree.readContent('/index.ts')) - .toContain(`{ ${type}, forwardRef, Injectable } from '@angular/core`); - }); - - it(`should not migrate object literal provider with "useValue" in ${type}`, async () => { - writeFile('/index.ts', ` - import {${type}} from '@angular/core'; - - export class MyService {} - - @${type}({${propName}: [{provide: MyService, useValue: null }]}) - export class TestClass {} - `); - - await runMigration(); - - expect(warnOutput.length).toBe(0); - expect(tree.readContent('/index.ts')).not.toContain('@Injectable'); - }); - - it(`should not migrate provider with "useClass" and "deps" in ${type}`, async () => { - writeFile('/index.ts', ` - import {${type}} from '@angular/core'; - - export class MyService {} - - @${type}({${propName}: [{provide: MyService, deps: []}]}) - export class TestClass {} - `); - - 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 ${type}`, async () => { - writeFile('/index.ts', ` - import {${type}} from '@angular/core'; - - export class MyService {} - - @${type}({${propName}: [{provide: MyService, useFactory: () => null }]}) - export class TestClass {} - `); - - await runMigration(); - - expect(warnOutput.length).toBe(0); - expect(tree.readContent('/index.ts')).not.toContain('@Injectable'); - }); - - it(`should not migrate object literal provider with "useExisting" in ${type}`, async () => { - writeFile('/index.ts', ` - import {${type}} from '@angular/core'; - - export class MyService {} - export class MyToken {} - - @${type}({${propName}: [ - {provide: MyService: useValue: null}, - {provide: MyToken, useExisting: MyService}, - ]}) - export class TestClass {} - `); - - await runMigration(); - - expect(warnOutput.length).toBe(0); - expect(tree.readContent('/index.ts')).not.toContain('@Injectable'); - }); - - it(`should migrate object literal provider with "useClass" in ${type}`, async () => { - writeFile('/index.ts', ` - import {${type}} from '@angular/core'; - - export class MyService {} - export class MyToken {} - - @${type}({${propName}: [{provide: MyToken, useClass: MyService}]}) - export class TestClass {} - `); - - 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')) - .toContain(`{ ${type}, Injectable } from '@angular/core`); - }); - - it(`should not migrate references for providers with "useExisting" in ${type}, but migrate ` + - `existing token if declared in other ${type}`, - async () => { - writeFile('/index.ts', ` - import {${type}} from '@angular/core'; - - export class MyService {} - export class MyToken {} - - @${type}({ - ${propName}: [ - {provide: MyToken, useExisting: MyService}, - ], - }) - export class TestClass {} - `); - - writeFile('/other.ts', ` - import {${type} from '@angular/core'; - import {MyService} from './index'; - - export @${type}({ - ${propName}: [{provide: MyService, useClass: MyService}], - }) - export class OtherClass {} - `); - - await runMigration(); - - expect(warnOutput.length).toBe(0); - expect(tree.readContent('/index.ts')) - .toMatch(/@angular\/core';\s+@Injectable\(\)\s+export class MyService/); - expect(tree.readContent('/index.ts')).toMatch(/MyService {}\s+export class MyToken/); - }); - - it('should not migrate provider which is already decorated with @Injectable', async () => { - writeFile('/index.ts', ` - import {Injectable, ${type}} from '@angular/core'; - - @Injectable() - export class MyService {} - - @${type}({${propName}: [MyService]}) - export class TestClass {} - `); - - 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, ${type}} from '@angular/core'; - - @Directive() - export class MyService {} - - @${type}({${propName}: [MyService]}) - export class TestClass {} - `); - - 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, ${type}} from '@angular/core'; - - @Component() - export class MyService {} - - @${type}({${propName}: [MyService]}) - export class TestClass {} - `); - - 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, ${type}} from '@angular/core'; - - @Pipe() - export class MyService {} - - @${type}({${propName}: [MyService]}) - export class TestClass {} - `); - - 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 @NgModule', async () => { - const importedSymbols = type !== 'NgModule' ? ['NgModule', type] : ['NgModule']; - writeFile('/index.ts', ` - import {${importedSymbols.join(', ')}} from '@angular/core'; - - @NgModule() - export class MyOtherModule {} - - @${type}({${propName}: [MyOtherModule]}) - export class TestClass {} - `); - - await runMigration(); - - expect(warnOutput.length).toBe(0); - expect(tree.readContent('/index.ts')).not.toContain('@Injectable'); - }); - - it(`should migrate multiple providers in same ${type}`, async () => { - writeFile('/index.ts', ` - import {${type}} from '@angular/core'; - - export class ServiceA {} - export class ServiceB {} - - @${type}({${propName}: [ServiceA, ServiceB]}) - export class TestClass {} - `); - - 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')) - .toContain(`{ ${type}, Injectable } from '@angular/core`); - }); - - it(`should migrate multiple mixed providers in same ${type}`, async () => { - writeFile('/index.ts', ` - import {${type}} from '@angular/core'; - - export class ServiceA {} - export class ServiceB {} - export class ServiceC {} - - @${type}({ - ${propName}: [ - ServiceA, - {provide: ServiceB}, - {provide: SomeToken, useClass: ServiceC}, - ]}) - export class TestClass {} - `); - - await runMigration(); - - expect(warnOutput.length).toBe(0); - expect(tree.readContent('/index.ts')).toMatch(/@Injectable\(\)\s+export class ServiceA/); - expect(tree.readContent('/index.ts')).toMatch(/ServiceA {}\s+export class ServiceB/); - expect(tree.readContent('/index.ts')).toMatch(/@Injectable\(\)\s+export class ServiceC/); - expect(tree.readContent('/index.ts')) - .toContain(`{ ${type}, Injectable } from '@angular/core`); - expect(tree.readContent('/index.ts')) - .toContain(`{ provide: ServiceB, useValue: undefined },`); - }); - - it(`should migrate multiple nested providers in same ${type}`, async () => { - writeFile('/index.ts', ` - import {${type}} from '@angular/core'; - - export class ServiceA {} - export class ServiceB {} - export class ServiceC {} - export class ServiceD {} - - @${type}({ - ${propName}: [ - ServiceA, - [ - {provide: ServiceB, useClass: ServiceB}, - ServiceC, - {provide: ServiceD}, - ], - ]}) - export class TestClass {} - `); - - 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(/ServiceC {}\s+export class ServiceD/); - expect(tree.readContent('/index.ts')) - .toContain(`{ ${type}, Injectable } from '@angular/core`); - expect(tree.readContent('/index.ts')) - .toContain(`{ provide: ServiceD, useValue: undefined },`); - }); - - it('should migrate providers referenced through identifier', async () => { - writeFile('/index.ts', ` - import {${type}} from '@angular/core'; - - export class ServiceA {} - export class ServiceB {} - export class ServiceC {} - - const PROVIDERS = [ - ServiceA, - ServiceB, - // trailing comma is by intention to check if do not create - // an invalid object literal. - {provide: ServiceC, }, - ]; - - @${type}({ - ${propName}: PROVIDERS, - }) - export class TestClass {} - `); - - 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(/ServiceB {}\s+export class ServiceC/); - expect(tree.readContent('/index.ts')) - .toContain(`{ ${type}, Injectable } from '@angular/core`); - expect(tree.readContent('/index.ts')) - .toContain(`{ provide: ServiceC, useValue: undefined },`); - }); - - it('should migrate providers created through static analyzable function call', async () => { - writeFile('/index.ts', ` - import {${type}} from '@angular/core'; - - export class ServiceA {} - export class ServiceB {} - export class ServiceC {} - - export function createProviders(x: any, b: any) { - return [ServiceA, x, b] - } - - @${type}({ - ${propName}: createProviders(ServiceB, {provide: ServiceC}), - }) - export class TestClass {} - `); - - 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(/ServiceB {}\s+export class ServiceC/); - expect(tree.readContent('/index.ts')) - .toContain(`{ ${type}, Injectable } from '@angular/core`); - expect(tree.readContent('/index.ts')) - .toContain(`ServiceB, { provide: ServiceC, useValue: undefined }),`); - }); - - it('should migrate providers which are computed through spread operator', async () => { - writeFile('/index.ts', ` - import {${type}} from '@angular/core'; - - export class ServiceA {} - export class ServiceB {} - export class ServiceC {} - - const otherServices = [ServiceB, {provide: ServiceC}]; - - @${type}({ - ${propName}: [ServiceA, ...otherServices], - }) - export class TestClass {} - `); - - 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(/ServiceB {}\s+export class ServiceC/); - expect(tree.readContent('/index.ts')) - .toContain(`{ ${type}, Injectable } from '@angular/core`); - expect(tree.readContent('/index.ts')) - .toContain(`ServiceB, { provide: ServiceC, useValue: undefined }];`); - }); - - it(`should migrate provider once if referenced in multiple ${type} definitions`, async () => { - writeFile('/index.ts', ` - import {${type}} from '@angular/core'; - - export class ServiceA {} - - @${type}({${propName}: [ServiceA]}) - export class TestClassA {} - `); - - writeFile('/second.ts', ` - import {${type}} from '@angular/core'; - import {ServiceA} from './index'; - - export class ServiceB {} - - @${type}({${propName}: [ServiceA, ServiceB]}) - export class TestClassB {} - `); - - 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')) - .toContain(`{ ${type}, Injectable } from '@angular/core`); - expect(tree.readContent('/second.ts')).toMatch(/@Injectable\(\)\s+export class ServiceB/); - expect(tree.readContent('/second.ts')) - .toContain(`{ ${type}, Injectable } from '@angular/core`); - }); - - it(`should only migrate empty object provider literal once if referenced multiple times ` + - `in ${type} definitions`, - async () => { - writeFile('/provider.ts', ` - export class MyService {} - - export const PROVIDER = {provide: MyService}; - `); - - writeFile('/index.ts', ` - import {${type}} from '@angular/core'; - import {PROVIDER} from './provider'; - - @${type}({${propName}: [PROVIDER]}) - export class TestClassA {} - `); - - writeFile('/second.ts', ` - import {${type}} from '@angular/core'; - import {PROVIDER} from './provider'; - - export class ServiceB {} - - @${type}({${propName}: [PROVIDER, ServiceB]}) - export class TestClassB {} - `); - - await runMigration(); - - expect(warnOutput.length).toBe(0); - expect(tree.readContent('/provider.ts')).toMatch(/^\s+export class MyService {}/); - expect(tree.readContent('/provider.ts')) - .toContain(`const PROVIDER = { provide: MyService, useValue: undefined };`); - }); - - it('should create new import for @Injectable when migrating provider', async () => { - writeFile('/index.ts', ` - import {${type}} from '@angular/core'; - import {MyService, MySecondService} from './service'; - - @${type}({${propName}: [MyService, MySecondService]}) - export class TestClass {} - `); - - 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 {${type}} from '@angular/core'; - import {MyService} from './index'; - - @${type}({${propName}: [MyService]}) - export class TestClass {} - `); - - 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 individual provider could not be resolved', async () => { - writeFile('/index.ts', ` - import {${type}} from '@angular/core'; - - @${type}({${propName}: [NotPresent]}) - export class TestClass {} - `); - - await runMigration(); - - const providerSourceTextColumn = 15 + type.length + propName.length; - expect(warnOutput.length).toBe(1); - expect(warnOutput[0]).toMatch(/\s+index\.ts@4:.+Provider is not statically analyzable./); - expect(warnOutput[0]).toContain(`4:${providerSourceTextColumn}:`); - }); - - it(`should warn if ${propName} value could not be resolved`, async () => { - writeFile('/index.ts', ` - import {${type}} from '@angular/core'; - - @${type}({${propName}: NOT_ANALYZABLE) - export class TestClass {} - `); - - await runMigration(); - - const propValueSourceTextColumn = 14 + type.length + propName.length; - expect(warnOutput.length).toBe(1); - expect(warnOutput[0]).toMatch(/\s+index\.ts@4:.+Providers.*not statically analyzable./); - expect(warnOutput[0]).toContain(`4:${propValueSourceTextColumn}:`); - }); - - it(`should not throw if an empty @${type} is analyzed`, async () => { - writeFile('/index.ts', ` - import {${type}} from '@angular/core'; - - @${type}() - 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 {${type}} from '@angular/core'; - import {MyService} from './service'; - - @${type}({${propName}: [MyService]}) - export class TestClass {} - `); - - 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 {${type}} from '@angular/core'; - import {MyService} from './service'; - - @${type}({${propName}: [MyService]}) - export class TestClass {} - `); - - 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 {${type}} from '@angular/core'; - import {MyService} from './service'; - - @${type}({${propName}: [MyService]}) - export class TestClass {} - `); - - 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/); - // "inject" import will be preserved. We don't want to bother with detecting - // if the import is unused or not. We leave this up to the developers. - expect(tree.readContent('/service.ts')) - .toMatch(/import { Inject, Injectable } from '@angular\/core';/); - }); - - it('should not migrate provider classes in library type definitions', async () => { - writeFile('/node_modules/my-lib/index.d.ts', ` - export declare class MyService {} - `); - - writeFile('/index.ts', ` - import {MyService} from 'my-lib'; - import {Pipe, ${type}} from '@angular/core'; - - @${type}({${propName}: [MyService]}) - export class TestClass {} - `); - - await runMigration(); - - expect(warnOutput.length).toBe(0); - expect(tree.readContent('/node_modules/my-lib/index.d.ts')).not.toContain('@Injectable'); - }); - } -}); diff --git a/packages/core/schematics/test/module_with_providers_migration_spec.ts b/packages/core/schematics/test/module_with_providers_migration_spec.ts deleted file mode 100644 index 1ebbb689789..00000000000 --- a/packages/core/schematics/test/module_with_providers_migration_spec.ts +++ /dev/null @@ -1,236 +0,0 @@ -/** - * @license - * Copyright Google LLC 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('ModuleWithProviders migration', () => { - let runner: SchematicTestRunner; - let host: TempScopedNodeJsSyncHost; - let tree: UnitTestTree; - let tmpDirPath: string; - let previousWorkingDir: string; - - beforeEach(() => { - runner = new SchematicTestRunner('test', require.resolve('../migrations.json')); - host = new TempScopedNodeJsSyncHost(); - tree = new UnitTestTree(new HostTree(host)); - - writeFile('/tsconfig.json', JSON.stringify({ - compilerOptions: { - lib: ['es2015'], - } - })); - writeFile('/angular.json', JSON.stringify({ - version: 1, - projects: {t: {architect: {build: {options: {tsConfig: './tsconfig.json'}}}}} - })); - - 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); - }); - - it('should add generic type for function return', async () => { - writeFile('/index.ts', ` - import {NgModule, ModuleWithProviders} from '@angular/core'; - - @NgModule({}) - export class BaseModule {} - - export function getProvider() { - return {ngModule: BaseModule} - } - - @NgModule({}) - export class TestModule { - static forRoot(): ModuleWithProviders { - return getProvider(); - } - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toContain(`ModuleWithProviders`); - }); - - it('should add generic type for function return; external file', async () => { - writeFile('/module.ts', ` - import {NgModule} from '@angular/core'; - - @NgModule({}) - export class BaseModule {} - `); - writeFile('/index.ts', ` - import {NgModule, ModuleWithProviders} from '@angular/core'; - import {BaseModule} from './module'; - - export function getProvider() { - return {ngModule: BaseModule} - } - - @NgModule({}) - export class TestModule { - static forRoot(): ModuleWithProviders { - return getProvider(); - } - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toContain(`ModuleWithProviders`); - }); - - it('should add generic type for function return without explicit type', async () => { - writeFile('/index.ts', ` - import {NgModule} from '@angular/core'; - - @NgModule({}) - export class BaseModule {} - - export function getProvider() { - return {ngModule: BaseModule} - } - - @NgModule({}) - export class TestModule { - static forRoot() { - return getProvider(); - } - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toContain(`ModuleWithProviders`); - }); - - it('should add generic type for const variable', async () => { - writeFile('/index.ts', ` - import {ModuleWithProviders, NgModule} from '@angular/core'; - - @NgModule({}) - export class BaseModule {} - - export const myModuleWithProviders = {ngModule: BaseModule}; - - @NgModule({}) - export class TestModule { - static forRoot(): ModuleWithProviders { - return myModuleWithProviders; - } - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toContain(`ModuleWithProviders`); - }); - - it('should add generic type for const variable without explicit type', async () => { - writeFile('/index.ts', ` - import {NgModule} from '@angular/core'; - - @NgModule({}) - export class BaseModule {} - - export const myModuleWithProviders = {ngModule: BaseModule}; - - @NgModule({}) - export class TestModule { - static forRoot() { - return myModuleWithProviders; - } - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toContain(`ModuleWithProviders`); - }); - - it('should not add generic type for const variable with invalid base object', async () => { - writeFile('/index.ts', ` - import {NgModule} from '@angular/core'; - - @NgModule({}) - export class BaseModule {} - - export const myModuleWithProviders = {ngModule: BaseModule, otherKey: 'a'}; - - @NgModule({}) - export class TestModule { - static forRoot() { - return myModuleWithProviders; - } - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).not.toContain(`ModuleWithProviders`); - }); - - it('should add generic type for const variables and functions with incomplete type', async () => { - writeFile('/index.ts', ` - import {ModuleWithProviders, NgModule} from '@angular/core'; - - @NgModule({}) - export class BaseModule {} - - export const myModuleWithProviders: ModuleWithProviders = {ngModule: BaseModule}; - - export function mwpFunction(): ModuleWithProviders { - return myModuleWithProviders; - } - - export class MwpClass { - mwp: ModuleWithProviders = myModuleWithProviders; - private _mwp: ModuleWithProviders = myModuleWithProviders; - - getMwp(): ModuleWithProviders { - return myModuleWithProviders; - } - - static initMwp(): ModuleWithProviders { - return myModuleWithProviders; - } - } - `); - - await runMigration(); - // Note the explicit space at the end here - expect(tree.readContent('/index.ts')).not.toContain(`ModuleWithProviders `); - }); - - it('should not add generic type for const variables without initialization', async () => { - writeFile('/index.ts', ` - import {ModuleWithProviders} from '@angular/core'; - - export const myModuleWithProviders: ModuleWithProviders; - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toContain(`TODO`); - }); - - function writeFile(filePath: string, contents: string) { - host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); - } - - function runMigration() { - return runner.runSchematicAsync('migration-v10-module-with-providers', {}, tree).toPromise(); - } -}); diff --git a/packages/core/schematics/test/move_document_migration_spec.ts b/packages/core/schematics/test/move_document_migration_spec.ts deleted file mode 100644 index d07e5167a17..00000000000 --- a/packages/core/schematics/test/move_document_migration_spec.ts +++ /dev/null @@ -1,171 +0,0 @@ -/** - * @license - * Copyright Google LLC 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('move-document migration', () => { - let runner: SchematicTestRunner; - let host: TempScopedNodeJsSyncHost; - let tree: UnitTestTree; - let tmpDirPath: string; - let previousWorkingDir: string; - - beforeEach(() => { - runner = new SchematicTestRunner('test', require.resolve('../migrations.json')); - host = new TempScopedNodeJsSyncHost(); - tree = new UnitTestTree(new HostTree(host)); - - writeFile('/tsconfig.json', JSON.stringify({ - compilerOptions: { - lib: ['es2015'], - }, - })); - writeFile('/angular.json', JSON.stringify({ - version: 1, - projects: {t: {architect: {build: {options: {tsConfig: './tsconfig.json'}}}}} - })); - - 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); - }); - - describe('move-document', () => { - it('should properly apply import replacement', async () => { - writeFile('/index.ts', ` - import {DOCUMENT} from '@angular/platform-browser'; - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - - expect(content).toContain(`import { DOCUMENT } from "@angular/common";`); - expect(content).not.toContain(`import {DOCUMENT} from '@angular/platform-browser';`); - }); - - it('should properly apply import replacement (BOM)', async () => { - writeFile('/index.ts', `\uFEFF - import {DOCUMENT} from '@angular/platform-browser'; - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - - expect(content).toContain(`import { DOCUMENT } from "@angular/common";`); - expect(content).not.toContain(`import {DOCUMENT} from '@angular/platform-browser';`); - }); - - it('should properly apply import replacement with existing import', async () => { - writeFile('/index.ts', ` - import {DOCUMENT} from '@angular/platform-browser'; - import {someImport} from '@angular/common'; - `); - - writeFile('/reverse.ts', ` - import {someImport} from '@angular/common'; - import {DOCUMENT} from '@angular/platform-browser'; - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - const contentReverse = tree.readContent('/reverse.ts'); - - expect(content).toContain(`import { someImport, DOCUMENT } from '@angular/common';`); - expect(content).not.toContain(`import {DOCUMENT} from '@angular/platform-browser';`); - - expect(contentReverse).toContain(`import { someImport, DOCUMENT } from '@angular/common';`); - expect(contentReverse).not.toContain(`import {DOCUMENT} from '@angular/platform-browser';`); - }); - - it('should properly apply import replacement with existing import w/ comments', async () => { - writeFile('/index.ts', ` - /** - * this is a comment - */ - import {someImport} from '@angular/common'; - import {DOCUMENT} from '@angular/platform-browser'; - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - - expect(content).toContain(`import { someImport, DOCUMENT } from '@angular/common';`); - expect(content).not.toContain(`import {DOCUMENT} from '@angular/platform-browser';`); - - expect(content).toMatch(/.*this is a comment.*/); - }); - - it('should properly apply import replacement with existing and redundant imports', async () => { - writeFile('/index.ts', ` - import {DOCUMENT} from '@angular/platform-browser'; - import {anotherImport} from '@angular/platform-browser-dynamic'; - import {someImport} from '@angular/common'; - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - - expect(content).toContain(`import { someImport, DOCUMENT } from '@angular/common';`); - expect(content).not.toContain(`import {DOCUMENT} from '@angular/platform-browser';`); - }); - - it('should properly apply import replacement with existing import and leave original import', - async () => { - writeFile('/index.ts', ` - import {DOCUMENT, anotherImport} from '@angular/platform-browser'; - import {someImport} from '@angular/common'; - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - - expect(content).toContain(`import { someImport, DOCUMENT } from '@angular/common';`); - expect(content).toContain(`import { anotherImport } from '@angular/platform-browser';`); - }); - - it('should properly apply import replacement with existing import and alias', async () => { - writeFile('/index.ts', ` - import {DOCUMENT as doc, anotherImport} from '@angular/platform-browser'; - import {someImport} from '@angular/common'; - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - - expect(content).toContain(`import { someImport, DOCUMENT as doc } from '@angular/common';`); - expect(content).toContain(`import { anotherImport } from '@angular/platform-browser';`); - }); - }); - - function writeFile(filePath: string, contents: string) { - host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); - } - - function runMigration() { - return runner.runSchematicAsync('migration-v8-move-document', {}, tree).toPromise(); - } -}); diff --git a/packages/core/schematics/test/native_view_encapsulation_migration_spec.ts b/packages/core/schematics/test/native_view_encapsulation_migration_spec.ts deleted file mode 100644 index d2c94daff10..00000000000 --- a/packages/core/schematics/test/native_view_encapsulation_migration_spec.ts +++ /dev/null @@ -1,170 +0,0 @@ -/** - * @license - * Copyright Google LLC 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('ViewEncapsulation.Native migration', () => { - let runner: SchematicTestRunner; - let host: TempScopedNodeJsSyncHost; - let tree: UnitTestTree; - let tmpDirPath: string; - let previousWorkingDir: string; - - beforeEach(() => { - runner = new SchematicTestRunner('test', require.resolve('../migrations.json')); - host = new TempScopedNodeJsSyncHost(); - tree = new UnitTestTree(new HostTree(host)); - - writeFile('/tsconfig.json', JSON.stringify({ - compilerOptions: { - lib: ['es2015'], - strictNullChecks: true, - }, - })); - writeFile('/angular.json', JSON.stringify({ - version: 1, - projects: {t: {architect: {build: {options: {tsConfig: './tsconfig.json'}}}}} - })); - - 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); - }); - - it('should change Native view encapsulation usages to ShadowDom', async () => { - writeFile('/index.ts', ` - import {Component, ViewEncapsulation} from '@angular/core'; - - @Component({ - template: 'hello', - encapsulation: ViewEncapsulation.Native - }) - class App {} - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain('encapsulation: ViewEncapsulation.ShadowDom'); - }); - - it('should change Native view encapsulation usages if the enum is aliased', async () => { - writeFile('/index.ts', ` - import {Component, ViewEncapsulation as Encapsulation} from '@angular/core'; - - @Component({ - template: 'hello', - encapsulation: Encapsulation.Native - }) - class App {} - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain('encapsulation: Encapsulation.ShadowDom'); - }); - - it('should change Native view encapsulation usages inside a variable', async () => { - writeFile('/index.ts', ` - import {Component, ViewEncapsulation} from '@angular/core'; - - const encapsulation = ViewEncapsulation.Native; - - @Component({template: 'hello', encapsulation}) - class App {} - - @Component({template: 'click me', encapsulation}) - class Button {} - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain('const encapsulation = ViewEncapsulation.ShadowDom;'); - }); - - it('should not change components that do not set an encapsulation', async () => { - const source = ` - import {Component} from '@angular/core'; - - @Component({ - template: 'hello' - }) - class App {} - `; - - writeFile('/index.ts', source); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toBe(source); - }); - - it('should not change components that use an encapsulation different from Native', async () => { - const source = ` - import {Component, ViewEncapsulation} from '@angular/core'; - - @Component({ - template: 'hello', - encapsulation: ViewEncapsulation.None - }) - class App {} - `; - - writeFile('/index.ts', source); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toBe(source); - }); - - it('should not change cases where ViewEncapsulation does not come from @angular/core', - async () => { - const source = ` - import {Component} from '@angular/core'; - import {ViewEncapsulation} from '@not-angular/core'; - - @Component({ - template: 'hello', - encapsulation: ViewEncapsulation.Native - }) - class App {} - `; - - writeFile('/index.ts', source); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toBe(source); - }); - - function writeFile(filePath: string, contents: string) { - host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); - } - - function runMigration() { - return runner.runSchematicAsync('migration-v11-native-view-encapsulation', {}, tree) - .toPromise(); - } -}); diff --git a/packages/core/schematics/test/navigation_extras_omissions_migration_spec.ts b/packages/core/schematics/test/navigation_extras_omissions_migration_spec.ts deleted file mode 100644 index f30cf78e373..00000000000 --- a/packages/core/schematics/test/navigation_extras_omissions_migration_spec.ts +++ /dev/null @@ -1,303 +0,0 @@ -/** - * @license - * Copyright Google LLC 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('NavigationExtras omissions migration', () => { - let runner: SchematicTestRunner; - let host: TempScopedNodeJsSyncHost; - let tree: UnitTestTree; - let tmpDirPath: string; - let previousWorkingDir: string; - - beforeEach(() => { - runner = new SchematicTestRunner('test', require.resolve('../migrations.json')); - host = new TempScopedNodeJsSyncHost(); - tree = new UnitTestTree(new HostTree(host)); - - writeFile('/tsconfig.json', JSON.stringify({ - compilerOptions: { - lib: ['es2015'], - strictNullChecks: true, - }, - })); - writeFile('/angular.json', JSON.stringify({ - version: 1, - projects: {t: {architect: {build: {options: {tsConfig: './tsconfig.json'}}}}} - })); - // We need to declare the Angular symbols we're testing for, otherwise type checking won't work. - writeFile('/node_modules/@angular/router/index.d.ts', ` - export declare class Router { - navigateByUrl(url: string, extras?: any); - createUrlTree(commands: any[], extras?: any); - } - `); - - 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); - }); - - it('should not change calls with a single argument', async () => { - writeFile('/index.ts', ` - import {Router} from '@angular/router'; - - class Navigator { - constructor(private _router: Router) {} - - goHome() { - this._router.navigateByUrl('/'); - } - } - - function createTree(router: Router) { - return router.createUrlTree(['/']); - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain(`this._router.navigateByUrl('/');`); - expect(content).toContain(`return router.createUrlTree(['/']);`); - }); - - it('should not change calls with an empty object literal', async () => { - writeFile('/index.ts', ` - import {Router} from '@angular/router'; - - class Navigator { - constructor(private _router: Router) {} - - goHome() { - this._router.navigateByUrl('/', {}); - } - } - - function createTree(router: Router) { - return router.createUrlTree(['/'], {}); - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain(`this._router.navigateByUrl('/', {});`); - expect(content).toContain(`return router.createUrlTree(['/'], {});`); - }); - - it('should not change objects that are used in multiple different methods', async () => { - writeFile('/index.ts', ` - import {Router} from '@angular/router'; - - const config = {replaceUrl: true, fragment: 'foo', state: {}}; - - class Navigator { - constructor(private _router: Router) {} - - goHome() { - this._router.navigateByUrl('/', config); - return this._router.createUrlTree(['/'], config); - } - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain(`const config = {replaceUrl: true, fragment: 'foo', state: {}};`); - }); - - it('should preserve calls if the router does not come from @angular/router', async () => { - writeFile('/index.ts', ` - import {Router} from '@custom/router'; - - function createTree(router: Router) { - return router.createUrlTree(['/'], {foo: 1, bar: 2}); - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain(`return router.createUrlTree(['/'], {foo: 1, bar: 2});`); - }); - - it('should change invalid navigateByUrl calls', async () => { - writeFile('/index.ts', ` - import {Router} from '@angular/router'; - - class Navigator { - constructor(private _router: Router) {} - - goHome() { - this._router.navigateByUrl('/', {preserveFragment: false, skipLocationChange: false, fragment: 'foo'}); - } - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain( - `this._router.navigateByUrl('/', { /* Removed unsupported properties by Angular migration: preserveFragment, fragment. */ skipLocationChange: false });`); - }); - - it('should change invalid navigateByUrl calls', async () => { - writeFile('/index.ts', ` - import {Router} from '@angular/router'; - - function createTree(router: Router) { - return router.createUrlTree(['/'], {replaceUrl: true, preserveFragment: true, state: {}}); - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain( - `return router.createUrlTree(['/'], { /* Removed unsupported properties by Angular migration: replaceUrl, state. */ preserveFragment: true });`); - }); - - it('should set the comment outside the object if all properties were removed', async () => { - writeFile('/index.ts', ` - import {Router} from '@angular/router'; - - function navigate(router: Router) { - router.navigateByUrl('/', {fragment: 'foo'}); - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain( - `router.navigateByUrl('/', /* Removed unsupported properties by Angular migration: fragment. */ {});`); - }); - - it('should migrate object literals defined as variables', async () => { - writeFile('/index.ts', ` - import {Router} from '@angular/router'; - - const config = {skipLocationChange: false, fragment: 'foo'}; - const proxy = config; - - function navigate(router: Router) { - router.navigateByUrl('/', proxy); - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain( - `const config = { /* Removed unsupported properties by Angular migration: fragment. */ skipLocationChange: false };`); - expect(content).toContain(`const proxy = config;`); - expect(content).toContain(`router.navigateByUrl('/', proxy);`); - }); - - it('should pick up calls where the router is returned by a function', async () => { - writeFile('/index.ts', ` - import {Router} from '@angular/router'; - - function navigate(router: Router) { - getRouter().navigateByUrl('/', {fragment: 'foo'}); - } - - function getRouter() { - return {} as Router; - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain( - `getRouter().navigateByUrl('/', /* Removed unsupported properties by Angular migration: fragment. */ {});`); - }); - - it('should pick up calls where the router is aliased', async () => { - writeFile('/index.ts', ` - import {Router as AliasedRouter} from '@angular/router'; - - function navigate(router: AliasedRouter) { - router.navigateByUrl('/', {fragment: 'foo'}); - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain( - `router.navigateByUrl('/', /* Removed unsupported properties by Angular migration: fragment. */ {});`); - }); - - it('should preserve object spread assignments', async () => { - writeFile('/index.ts', ` - import {Router} from '@angular/router'; - - function navigate(router: Router) { - const overrides = {foo: 1}; - router.navigateByUrl('/', {replaceUrl: true, fragment: 'foo', ...overrides}); - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain( - `router.navigateByUrl('/', { /* Removed unsupported properties by Angular migration: fragment. */ replaceUrl: true, ...overrides });`); - }); - - it('should migrate objects that are used in multiple calls of the same method', async () => { - writeFile('/index.ts', ` - import {Router} from '@angular/router'; - - const config = {skipLocationChange: false, fragment: 'foo'}; - - class Navigator { - constructor(private _router: Router) {} - - goHome() { - this._router.navigateByUrl('/', config); - } - - goFish() { - this._router.navigateByUrl('/fish', config); - } - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain( - `const config = { /* Removed unsupported properties by Angular migration: fragment. */ skipLocationChange: false };`); - }); - - function writeFile(filePath: string, contents: string) { - host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); - } - - function runMigration() { - return runner.runSchematicAsync('migration-v11-navigation-extras-omissions', {}, tree) - .toPromise(); - } -}); diff --git a/packages/core/schematics/test/preserve_query_params_migration_spec.ts b/packages/core/schematics/test/preserve_query_params_migration_spec.ts deleted file mode 100644 index e0f46e14a78..00000000000 --- a/packages/core/schematics/test/preserve_query_params_migration_spec.ts +++ /dev/null @@ -1,233 +0,0 @@ -/** - * @license - * Copyright Google LLC 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('NavigationExtras preserveQueryParams migration', () => { - let runner: SchematicTestRunner; - let host: TempScopedNodeJsSyncHost; - let tree: UnitTestTree; - let tmpDirPath: string; - let previousWorkingDir: string; - - beforeEach(() => { - runner = new SchematicTestRunner('test', require.resolve('../migrations.json')); - host = new TempScopedNodeJsSyncHost(); - tree = new UnitTestTree(new HostTree(host)); - - writeFile('/tsconfig.json', JSON.stringify({ - compilerOptions: { - lib: ['es2015'], - strictNullChecks: true, - }, - })); - writeFile('/angular.json', JSON.stringify({ - version: 1, - projects: {t: {architect: {build: {options: {tsConfig: './tsconfig.json'}}}}} - })); - // We need to declare the Angular symbols we're testing for, otherwise type checking won't work. - writeFile('/node_modules/@angular/router/index.d.ts', ` - export declare class Router { - navigate(url: string, extras?: any); - createUrlTree(commands: any[], extras?: any); - } - `); - - 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); - }); - - describe('updates the', () => { - it('`navigate` function', async () => { - writeFile('/index.ts', ` - import {Router} from '@angular/router'; - - class Navigator { - constructor(private _router: Router) {} - - goHome() { - this._router.navigate('/', {preserveQueryParams: true}); - } - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain(`this._router.navigate('/', { queryParamsHandling: 'preserve' });`); - }); - - it('`createUrlTree` function', async () => { - writeFile('/index.ts', ` - import {Router} from '@angular/router'; - - class Navigator { - constructor(private _router: Router) {} - - goHome() { - this._router.createUrlTree('/', {preserveQueryParams: false}); - } - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain(`this._router.createUrlTree('/', {});`); - }); - }); - - describe('updates an object which is used for the parameter', () => { - it('should migrate when the value is `true`', async () => { - writeFile('/index.ts', ` - import {Router} from '@angular/router'; - - const config = {preserveQueryParams: true, replaceUrl: true, fragment: 'foo', state: {}}; - - class Navigator { - constructor(private _router: Router) {} - - goHome() { - this._router.createUrlTree(['/'], config); - } - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain( - `const config = { replaceUrl: true, fragment: 'foo', state: {}, queryParamsHandling: 'preserve' };`); - }); - - it('should remove when the value is `false`', async () => { - writeFile('/index.ts', ` - import {Router} from '@angular/router'; - - const config = {preserveQueryParams: false, replaceUrl: true, fragment: 'foo', state: {}}; - - class Navigator { - constructor(private _router: Router) {} - - goHome() { - this._router.createUrlTree(['/'], config); - } - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain(`const config = { replaceUrl: true, fragment: 'foo', state: {} };`); - }); - - it('should not modify when the property is no present', async () => { - writeFile('/index.ts', ` - import {Router} from '@angular/router'; - - const config = {replaceUrl: true, fragment: 'foo', state: {}}; - - class Navigator { - constructor(private _router: Router) {} - - goHome() { - this._router.navigate('/', config); - } - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain(`const config = {replaceUrl: true, fragment: 'foo', state: {}};`); - }); - }); - - describe('updates an the locally defined parameter in the method', () => { - it('should migrate when the value is `true`', async () => { - writeFile('/index.ts', ` - import {Router} from '@angular/router'; - - class Navigator { - constructor(private _router: Router) {} - - goHome() { - this._router.navigate('/', {preserveQueryParams: true, replaceUrl: true, fragment: 'foo', state: {}}); - } - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain( - `this._router.navigate('/', { replaceUrl: true, fragment: 'foo', state: {}, queryParamsHandling: 'preserve' });`); - }); - - it('should remove when the value is `false`', async () => { - writeFile('/index.ts', ` - import {Router} from '@angular/router'; - - class Navigator { - constructor(private _router: Router) {} - - goHome() { - this._router.createUrlTree(['/'], {preserveQueryParams: false, replaceUrl: true, fragment: 'foo', state: {}};); - } - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain( - `this._router.createUrlTree(['/'], { replaceUrl: true, fragment: 'foo', state: {} };`); - }); - - it('should not modify when the property is not present', async () => { - writeFile('/index.ts', ` - import {Router} from '@angular/router'; - - class Navigator { - constructor(private _router: Router) {} - - goHome() { - this._router.navigate('/', {replaceUrl: true, fragment: 'foo', state: {}}); - } - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain( - `this._router.navigate('/', {replaceUrl: true, fragment: 'foo', state: {}});`); - }); - }); - function writeFile(filePath: string, contents: string) { - host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); - } - - function runMigration() { - return runner.runSchematicAsync('migration-v11-router-preserve-query-params', {}, tree) - .toPromise(); - } -}); diff --git a/packages/core/schematics/test/relative_link_resolution_spec.ts b/packages/core/schematics/test/relative_link_resolution_spec.ts deleted file mode 100644 index 1a51ec3d301..00000000000 --- a/packages/core/schematics/test/relative_link_resolution_spec.ts +++ /dev/null @@ -1,189 +0,0 @@ -/** - * @license - * Copyright Google LLC 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('initial navigation migration', () => { - let runner: SchematicTestRunner; - let host: TempScopedNodeJsSyncHost; - let tree: UnitTestTree; - let tmpDirPath: string; - let previousWorkingDir: string; - - beforeEach(() => { - runner = new SchematicTestRunner('test', require.resolve('../migrations.json')); - host = new TempScopedNodeJsSyncHost(); - tree = new UnitTestTree(new HostTree(host)); - - writeFile('/tsconfig.json', JSON.stringify({ - compilerOptions: { - lib: ['es2015'], - } - })); - writeFile('/angular.json', JSON.stringify({ - version: 1, - projects: {t: {architect: {build: {options: {tsConfig: './tsconfig.json'}}}}} - })); - - 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); - }); - - it('should migrate forRoot with no options', async () => { - writeFile('/index.ts', ` - import { NgModule } from '@angular/core'; - import { RouterModule } from '@angular/router'; - @NgModule({ - imports: [ - RouterModule.forRoot([]), - ] - }) - export class AppModule { - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`RouterModule.forRoot([], { relativeLinkResolution: 'legacy' })`); - }); - - it('should migrate options without relativeLinkResolution', async () => { - writeFile('/index.ts', ` - import { NgModule } from '@angular/core'; - import { RouterModule } from '@angular/router'; - @NgModule({ - imports: [ - RouterModule.forRoot([], {useHash: true}), - ] - }) - export class AppModule { - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`RouterModule.forRoot([], { useHash: true, relativeLinkResolution: 'legacy' })`); - }); - - it('should not migrate options containing relativeLinkResolution', async () => { - writeFile('/index.ts', ` - import { NgModule } from '@angular/core'; - import { RouterModule } from '@angular/router'; - @NgModule({ - imports: [ - RouterModule.forRoot([], {relativeLinkResolution: 'corrected'}), - ] - }) - export class AppModule { - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`RouterModule.forRoot([], {relativeLinkResolution: 'corrected'})`); - }); - - it('should migrate when options is a variable with AsExpression', async () => { - writeFile('/index.ts', ` - import { ExtraOptions } from '@angular/router'; - const options = {useHash: true} as ExtraOptions; - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain( - `const options = { useHash: true, relativeLinkResolution: 'legacy' } as ExtraOptions;`); - }); - - it('should migrate when options is a variable', async () => { - writeFile('/index.ts', ` - import { ExtraOptions } from '@angular/router'; - const options: ExtraOptions = {useHash: true}; - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain( - `const options: ExtraOptions = { useHash: true, relativeLinkResolution: 'legacy' };`); - }); - - it('should migrate when options is a variable with no type', async () => { - writeFile('/index.ts', ` - import { NgModule } from '@angular/core'; - import { ExtraOptions, RouterModule } from '@angular/router'; - - const options = {useHash: true}; - - @NgModule({ - imports: [ - RouterModule.forRoot([], options), - ] - }) - export class AppModule { - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`const options = { useHash: true, relativeLinkResolution: 'legacy' };`); - expect(tree.readContent('/index.ts')).toContain(`RouterModule.forRoot([], options)`); - }); - - it('should migrate when aliased options is a variable', async () => { - writeFile('/index.ts', ` - import { ExtraOptions as RouterExtraOptions } from '@angular/router'; - const options: RouterExtraOptions = {useHash: true}; - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain( - `const options: RouterExtraOptions = { useHash: true, relativeLinkResolution: 'legacy' };`); - }); - - it('should migrate aliased RouterModule.forRoot', async () => { - writeFile('/index.ts', ` - import { NgModule } from '@angular/core'; - import { RouterModule as AngularRouterModule} from '@angular/router'; - @NgModule({ - imports: [ - AngularRouterModule.forRoot([]), - ] - }) - export class AppModule { - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`AngularRouterModule.forRoot([], { relativeLinkResolution: 'legacy' }),`); - }); - - function writeFile(filePath: string, contents: string) { - host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); - } - - function runMigration() { - return runner - .runSchematicAsync('migration-v11-router-relative-link-resolution-default', {}, tree) - .toPromise(); - } -}); diff --git a/packages/core/schematics/test/renderer_to_renderer2_migration_spec.ts b/packages/core/schematics/test/renderer_to_renderer2_migration_spec.ts deleted file mode 100644 index 3d3c2828c12..00000000000 --- a/packages/core/schematics/test/renderer_to_renderer2_migration_spec.ts +++ /dev/null @@ -1,1239 +0,0 @@ -/** - * @license - * Copyright Google LLC 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('Renderer to Renderer2 migration', () => { - let runner: SchematicTestRunner; - let host: TempScopedNodeJsSyncHost; - let tree: UnitTestTree; - let tmpDirPath: string; - let previousWorkingDir: string; - - beforeEach(() => { - runner = new SchematicTestRunner('test', require.resolve('../migrations.json')); - host = new TempScopedNodeJsSyncHost(); - tree = new UnitTestTree(new HostTree(host)); - - writeFile('/tsconfig.json', JSON.stringify({ - compilerOptions: { - lib: ['es2015'], - strictNullChecks: true, - }, - })); - writeFile('/angular.json', JSON.stringify({ - version: 1, - projects: {t: {architect: {build: {options: {tsConfig: './tsconfig.json'}}}}} - })); - // We need to declare the Angular symbols we're testing for, otherwise type checking won't work. - writeFile('/node_modules/@angular/core/index.d.ts', ` - export declare function forwardRef(fn: () => any): any {} - `); - - 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); - }); - - describe('import renaming', () => { - it('should change Renderer imports to Renderer2', async () => { - writeFile('/index.ts', ` - import { Renderer, Component } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(renderer: Renderer) {} - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`import { Component, Renderer2 } from '@angular/core';`); - }); - - it('should change aliased Renderer imports to Renderer2', async () => { - writeFile('/index.ts', ` - import { Renderer as RenamedRenderer, Component } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(renderer: RenamedRenderer) {} - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`import { Component, Renderer2 as RenamedRenderer } from '@angular/core';`); - }); - - it('should not change Renderer imports if they are not from @angular/core', async () => { - writeFile('/index.ts', ` - import { Component } from '@angular/core'; - import { Renderer } from './my-renderer'; - - @Component({template: ''}) - export class MyComp { - constructor(renderer: Renderer) {} - } - `); - - await runMigration(); - const content = tree.readContent('/index.ts'); - - expect(content).toContain(`import { Component } from '@angular/core';`); - expect(content).toContain(`import { Renderer } from './my-renderer';`); - }); - - it('should not change imports if Renderer2 was already imported', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, Renderer2 } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(renderer: Renderer) {} - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`import { Renderer, Component, Renderer2 } from '@angular/core';`); - }); - }); - - describe('type renaming', () => { - it('should change type of constructor parameter from Renderer to Renderer2', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(element: ElementRef, renderer: Renderer) {} - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain('constructor(element: ElementRef, renderer: Renderer2)'); - }); - - it('should change type of method parameter from Renderer to Renderer2', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - disable(renderer: Renderer, element: HTMLElement, isDisabled: boolean) { - renderer.setElementProperty(element, 'disabled', isDisabled); - } - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain('disable(renderer: Renderer2, element: HTMLElement, isDisabled: boolean)'); - }); - - it('should change type of property declarations', async () => { - writeFile('/index.ts', ` - import { Renderer, Component } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - public renderer: Renderer; - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toContain('public renderer: Renderer2;'); - }); - - it('should change type of properties initialized via the constructor', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(element: ElementRef, private _renderer: Renderer) {} - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain('constructor(element: ElementRef, private _renderer: Renderer2)'); - }); - - it('should change the type of something that was cast to Renderer', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - setColor(maybeRenderer: any, element: ElementRef) { - const renderer = maybeRenderer as Renderer; - renderer.setElementStyle(element.nativeElement, 'color', 'red'); - } - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - - expect(content).toContain(`const renderer = maybeRenderer as Renderer2;`); - expect(content).toContain(`renderer.setStyle(element.nativeElement, 'color', 'red');`); - }); - - it('should not rename types called Renderer that do not come from Angular', async () => { - // Write a dummy renderer file so type checking picks it up. - writeFile('/my-renderer.ts', `export abstract class Renderer {}`); - - writeFile('/index.ts', ` - import { Renderer as ActualAngularRenderer, Component, ElementRef } from '@angular/core'; - import { Renderer } from './my-renderer'; - - @Component({template: ''}) - export class MyComp { - constructor(element: ElementRef, renderer: Renderer) {} - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain('constructor(element: ElementRef, renderer: Renderer)'); - }); - - it('should rename inside single-line forwardRef', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef, forwardRef, Inject } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(@Inject(forwardRef(() => Renderer)) private _renderer: Renderer) {} - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - - expect(content).toContain( - `constructor(@Inject(forwardRef(() => Renderer2)) private _renderer: Renderer2)`); - }); - - it('should rename inside multi-line forwardRef', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef, forwardRef, Inject } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(@Inject(forwardRef(() => { return Renderer; })) private _renderer: Renderer) {} - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - - expect(content).toContain( - `constructor(@Inject(forwardRef(() => { return Renderer2; })) private _renderer: Renderer2) {}`); - }); - }); - - describe('helper insertion', () => { - it('should only declare each helper once per file', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _element: ElementRef, private _renderer: Renderer) { - _renderer.createElement(_element.nativeElement, 'span'); - _renderer.createElement(_element.nativeElement, 'button'); - _renderer.createElement(_element.nativeElement, 'div'); - } - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - - expect(content.match(/function __ngRendererCreateElementHelper\(/g)!.length) - .toBe(1, 'Expected exactly one helper for createElement.'); - }); - - it('should insert helpers after the user\'s code', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _element: ElementRef, private _renderer: Renderer) { - _renderer.createElement(_element.nativeElement, 'span'); - } - } - - //--- - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - const [contentBeforeSeparator, contentAfterSeparator] = content.split('//---'); - - expect(contentBeforeSeparator).not.toContain('function __ngRendererCreateElementHelper('); - expect(contentAfterSeparator).toContain('function __ngRendererCreateElementHelper('); - }); - - it('should be able to handle multiple helpers per file', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _element: ElementRef, private _renderer: Renderer) { - _renderer.createText(_element.nativeElement, 'hello'); - _renderer.createText(_element.nativeElement, 'there'); - _renderer.createText(_element.nativeElement, '!'); - } - - createElements(parent: HTMLElement) { - this._renderer.createElement(parent, 'span'); - this._renderer.createElement(parent, 'button'); - } - - createAnchor() { - this._renderer.createTemplateAnchor(this._element.nativeElement); - } - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - - expect(content.match(/function __ngRendererCreateTextHelper\(/g)!.length) - .toBe(1, 'Expected exactly one helper for createElement.'); - expect(content.match(/function __ngRendererCreateElementHelper\(/g)!.length) - .toBe(1, 'Expected exactly one helper for createText.'); - expect(content.match(/function __ngRendererCreateTemplateAnchorHelper\(/g)!.length) - .toBe(1, 'Expected exactly one helper for createTemplateAnchor.'); - }); - - it('should create the __ngRendererSplitNamespaceHelper', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(element: ElementRef, renderer: Renderer) { - renderer.createElement(element.nativeElement, 'span'); - } - } - `); - - await runMigration(); - - expect(stripWhitespace(tree.readContent('/index.ts'))).toContain(stripWhitespace(` - function __ngRendererSplitNamespaceHelper(name: AnyDuringRendererMigration) { - if (name[0] === ":") { - const match = name.match(/^:([^:]+):(.+)$/); - return [match[1], match[2]]; - } - return ["", name]; - } - `)); - }); - - it('should declare our custom any type', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(element: ElementRef, renderer: Renderer) { - renderer.createElement(element.nativeElement, 'span'); - } - } - `); - - await runMigration(); - - expect(stripWhitespace(tree.readContent('/index.ts'))).toContain(stripWhitespace(` - type AnyDuringRendererMigration = any; - `)); - }); - }); - - describe('setElementProperty migration', () => { - it('should migrate setElementProperty calls', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _element: ElementRef, private _renderer: Renderer) {} - - disable() { - this._renderer.setElementProperty(this._element.nativeElement, 'disabled', true); - } - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`this._renderer.setProperty(this._element.nativeElement, 'disabled', true);`); - }); - }); - - describe('setText migration', () => { - it('should migrate setText calls', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _element: ElementRef, private _renderer: Renderer) {} - - sayHello() { - this._renderer.setText(this._element.nativeElement.querySelector('span'), 'Hello'); - } - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain( - `this._renderer.setValue(this._element.nativeElement.querySelector('span'), 'Hello');`); - }); - }); - - describe('listenGlobal migration', () => { - it('should migrate listenGlobal calls', async () => { - writeFile('/index.ts', ` - import { Renderer, Component } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _renderer: Renderer) {} - - listenToResize() { - this._renderer.listenGlobal('window', 'resize', () => console.log('resized')); - } - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`this._renderer.listen('window', 'resize', () => console.log('resized'));`); - }); - }); - - describe('selectRootElement migration', () => { - it('should migrate selectRootElement calls', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _element: ElementRef, private _renderer: Renderer) {} - - getRootElement() { - return this._renderer.selectRootElement(this._element.nativeElement, {}); - } - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`return this._renderer.selectRootElement(this._element.nativeElement);`); - }); - }); - - describe('setElementClass migration', () => { - it('should migrate calls with inline isAdd value', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _element: ElementRef, private _renderer: Renderer) {} - - addClass(className: string) { - this._renderer.setElementClass(this._element.nativeElement, className, true); - } - - removeClass(className: string) { - this._renderer.setElementClass(this._element.nativeElement, className, false); - } - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`this._renderer.addClass(this._element.nativeElement, className);`); - expect(tree.readContent('/index.ts')) - .toContain(`this._renderer.removeClass(this._element.nativeElement, className);`); - }); - - it('should migrate calls with variable isAdd value', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _element: ElementRef, private _renderer: Renderer) {} - - toggleClass(className: string, shouldAdd: any) { - this._renderer.setElementClass(this._element.nativeElement, className, shouldAdd); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain( - `shouldAdd ? this._renderer.addClass(this._element.nativeElement, className) : ` + - `this._renderer.removeClass(this._element.nativeElement, className);`); - }); - }); - - describe('setElementStyle migration', () => { - it('should migrate calls with two arguments to a removeStyle call', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _element: ElementRef, private _renderer: Renderer) {} - - removeColor() { - this._renderer.setElementStyle(this._element.nativeElement, 'color'); - } - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`this._renderer.removeStyle(this._element.nativeElement, 'color');`); - }); - - it('should migrate calls with static third arguments to a setStyle call', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _element: ElementRef, private _renderer: Renderer) {} - - setStyles(height: number) { - this._renderer.setElementStyle(this._element.nativeElement, 'color', 'red'); - this._renderer.setElementStyle(this._element.nativeElement, 'background-color', \`blue\`); - this._renderer.setElementStyle(this._element.nativeElement, 'width', 3); - this._renderer.setElementStyle(this._element.nativeElement, 'height', \`\${height\}px\`); - } - } - `); - - await runMigration(); - const content = tree.readContent('/index.ts'); - expect(content).toContain( - `this._renderer.setStyle(this._element.nativeElement, 'color', 'red');`); - expect(content).toContain( - `this._renderer.setStyle(this._element.nativeElement, 'background-color', \`blue\`);`); - expect(content).toContain( - `this._renderer.setStyle(this._element.nativeElement, 'width', 3);`); - expect(content).toContain( - `this._renderer.setStyle(this._element.nativeElement, 'height', \`\${height\}px\`);`); - }); - - it('should migrate calls with null or undefined value for last argument to a removeStyle call', - async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _element: ElementRef, private _renderer: Renderer) {} - - removeColors() { - this._renderer.setElementStyle(this._element.nativeElement, 'color', null); - this._renderer.setElementStyle(this._element.nativeElement, 'background-color', undefined); - } - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`this._renderer.removeStyle(this._element.nativeElement, 'color');`); - expect(tree.readContent('/index.ts')) - .toContain( - `this._renderer.removeStyle(this._element.nativeElement, 'background-color');`); - }); - - it('should migrate calls with a variable third argument', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _element: ElementRef, private _renderer: Renderer) {} - - setColor(value: string | null) { - this._renderer.setElementStyle(this._element.nativeElement, 'color', value); - } - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain( - `value == null ? this._renderer.removeStyle(this._element.nativeElement, 'color') : ` + - `this._renderer.setStyle(this._element.nativeElement, 'color', value);`); - }); - - it('should migrate calls with a variable third argument whose value can be inferred', - async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _element: ElementRef, private _renderer: Renderer) {} - - setColor(color: string, backgroundColor: null, width: number) { - this._renderer.setElementStyle(this._element.nativeElement, 'color', color); - this._renderer.setElementStyle(this._element.nativeElement, 'background-color', backgroundColor); - this._renderer.setElementStyle(this._element.nativeElement, 'width', width + 'px'); - } - } - `); - - await runMigration(); - const content = tree.readContent('/index.ts'); - expect(content).toContain( - `this._renderer.setStyle(this._element.nativeElement, 'color', color);`); - expect(content).toContain( - `this._renderer.removeStyle(this._element.nativeElement, 'background-color');`); - expect(content).toContain( - `this._renderer.setStyle(this._element.nativeElement, 'width', width + 'px');`); - }); - }); - - describe('setElementAttribute migration', () => { - it('should migrate to calls to the __ngRendererSetElementAttributeHelper', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _element: ElementRef, private _renderer: Renderer) { - _renderer.setElementAttribute(_element.nativeElement, 'title', 'hello'); - } - - removeAttribute(name: string) { - this._renderer.setElementAttribute(this._element.nativeElement, name); - } - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - - expect(content).toContain( - `__ngRendererSetElementAttributeHelper(_renderer, _element.nativeElement, 'title', 'hello');`); - expect(content).toContain( - '__ngRendererSetElementAttributeHelper(this._renderer, this._element.nativeElement, name);'); - }); - - it('should declare the __ngRendererSetElementAttributeHelper', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(element: ElementRef, renderer: Renderer) { - renderer.setElementAttribute(element.nativeElement, 'foo', 'bar'); - } - } - `); - - await runMigration(); - - const content = stripWhitespace(tree.readContent('/index.ts')); - - expect(content).toContain(stripWhitespace(` - function __ngRendererSetElementAttributeHelper(renderer: AnyDuringRendererMigration, element: AnyDuringRendererMigration, namespaceAndName: AnyDuringRendererMigration, value?: AnyDuringRendererMigration) { - const [namespace, name] = __ngRendererSplitNamespaceHelper(namespaceAndName); - if (value != null) { - renderer.setAttribute(element, name, value, namespace); - } else { - renderer.removeAttribute(element, name, namespace); - } - } - `)); - - expect(content).toContain(stripWhitespace('function __ngRendererSplitNamespaceHelper(')); - }); - }); - - describe('invokeElementMethod migration', () => { - it('should migrate calls to a direct method call if the method name and arguments are static', - async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _element: ElementRef, private _renderer: Renderer) {} - - focus() { - this._renderer.invokeElementMethod(this._element.nativeElement, 'focus', []); - this._renderer.invokeElementMethod(this._element.nativeElement, 'focusEvenMore'); - this._renderer.invokeElementMethod(this._element.nativeElement, 'doSomething', [1, true, 'three']); - } - } - `); - - await runMigration(); - const content = tree.readContent('/index.ts'); - expect(content).not.toContain('this._renderer'); - expect(content).toContain(`this._element.nativeElement.focus()`); - expect(content).toContain(`this._element.nativeElement.focusEvenMore()`); - expect(content).toContain(`this._element.nativeElement.doSomething(1, true, 'three')`); - }); - - it('should migrate calls to a property access if the method name or arguments are dynamic', - async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _element: ElementRef, private _renderer: Renderer) {} - - callMethod(name: string, args: any[]) { - this._renderer.invokeElementMethod(this._element.nativeElement, name, [1, true]); - } - - callOtherMethod(args: any[]) { - this._renderer.invokeElementMethod(this._element.nativeElement, 'otherMethod', args); - } - } - `); - - await runMigration(); - const content = tree.readContent('/index.ts'); - expect(content).not.toContain('this._renderer'); - expect(content).toContain( - `(this._element.nativeElement as any)[name].apply(this._element.nativeElement, [1, true]);`); - - expect(content).toContain( - `(this._element.nativeElement as any)['otherMethod'].apply(this._element.nativeElement, args);`); - }); - - it('should handle calls without an `args` array', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _element: ElementRef, private _renderer: Renderer) {} - - callMethod(name: string) { - this._renderer.invokeElementMethod(this._element.nativeElement, name); - } - } - `); - - await runMigration(); - const content = tree.readContent('/index.ts'); - expect(content).not.toContain('this._renderer'); - expect(content).toContain( - `(this._element.nativeElement as any)[name].apply(this._element.nativeElement);`); - }); - }); - - describe('setBindingDebugInfo migration', () => { - it('should drop calls to setBindingDebugInfo', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _element: ElementRef, private _renderer: Renderer) {} - - setInfo() { - this._renderer.setBindingDebugInfo(this._element.nativeElement, 'prop', 'value'); - } - } - `); - - await runMigration(); - - // Expect the `setInfo` method to only contain whitespace. - expect(tree.readContent('/index.ts')).toMatch(/setInfo\(\) \{\s+\}/); - }); - }); - - describe('createViewRoot migration', () => { - it('should replace createViewRoot calls with a reference to the first argument', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _element: ElementRef, private _renderer: Renderer) {} - - createRoot() { - return this._renderer.createViewRoot(this._element.nativeElement); - } - } - `); - - await runMigration(); - - // Expect the `createRoot` only to return `this._element.nativeElement`. - expect(tree.readContent('/index.ts')) - .toMatch(/createRoot\(\) \{\s+return this\._element\.nativeElement;\s+\}/); - }); - }); - - describe('createElement migration', () => { - it('should migrate to calls to the __ngRendererCreateElementHelper', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _element: ElementRef, private _renderer: Renderer) { - const message = _renderer.createElement(_element.nativeElement, 'span', {}); - message.textContent = 'hello'; - } - - createAndAppendElement(nodeName: string) { - return this._renderer.createElement(this._element.nativeElement, nodeName); - } - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - - expect(content).toContain( - `const message = __ngRendererCreateElementHelper(_renderer, _element.nativeElement, 'span');`); - expect(content).toContain( - 'return __ngRendererCreateElementHelper(this._renderer, this._element.nativeElement, nodeName);'); - }); - - it('should declare the __ngRendererCreateElementHelper', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(element: ElementRef, renderer: Renderer) { - renderer.createElement(element.nativeElement, 'span'); - } - } - `); - - await runMigration(); - - const content = stripWhitespace(tree.readContent('/index.ts')); - - expect(content).toContain(stripWhitespace(` - function __ngRendererCreateElementHelper(renderer: AnyDuringRendererMigration, parent: AnyDuringRendererMigration, namespaceAndName: AnyDuringRendererMigration) { - const [namespace, name] = __ngRendererSplitNamespaceHelper(namespaceAndName); - const node = renderer.createElement(name, namespace); - if (parent) { - renderer.appendChild(parent, node); - } - return node; - } - `)); - - expect(content).toContain(stripWhitespace('function __ngRendererSplitNamespaceHelper(')); - }); - }); - - describe('createText migration', () => { - it('should migrate to calls to the __ngRendererCreateTextHelper', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _element: ElementRef, private _renderer: Renderer) { - const message = _renderer.createText(_element.nativeElement, 'hello', {}); - message.textContent += ' world'; - } - - createAndAppendText(value: string) { - return this._renderer.createText(this._element.nativeElement, value); - } - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - - expect(content).toContain( - `const message = __ngRendererCreateTextHelper(_renderer, _element.nativeElement, 'hello');`); - expect(content).toContain( - 'return __ngRendererCreateTextHelper(this._renderer, this._element.nativeElement, value);'); - }); - - it('should declare the __ngRendererCreateTextHelper', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(element: ElementRef, renderer: Renderer) { - renderer.createText(element.nativeElement, 'hi'); - } - } - `); - - await runMigration(); - - expect(stripWhitespace(tree.readContent('/index.ts'))).toContain(stripWhitespace(` - function __ngRendererCreateTextHelper(renderer: AnyDuringRendererMigration, parent: AnyDuringRendererMigration, value: AnyDuringRendererMigration) { - const node = renderer.createText(value); - if (parent) { - renderer.appendChild(parent, node); - } - return node; - } - `)); - }); - }); - - describe('createTemplateAnchor migration', () => { - it('should migrate to calls to the __ngRendererCreateTemplateAnchorHelper', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _element: ElementRef, private _renderer: Renderer) { - console.log(_renderer.createTemplateAnchor(_element.nativeElement)); - } - - createAndAppendAnchor() { - return this._renderer.createTemplateAnchor(this._element.nativeElement); - } - } - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - - expect(content).toContain( - `console.log(__ngRendererCreateTemplateAnchorHelper(_renderer, _element.nativeElement));`); - expect(content).toContain( - 'return __ngRendererCreateTemplateAnchorHelper(this._renderer, this._element.nativeElement);'); - }); - - it('should declare the __ngRendererCreateTemplateAnchorHelper', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(element: ElementRef, renderer: Renderer) { - renderer.createTemplateAnchor(element.nativeElement); - } - } - `); - - await runMigration(); - - expect(stripWhitespace(tree.readContent('/index.ts'))).toContain(stripWhitespace(` - function __ngRendererCreateTemplateAnchorHelper(renderer: AnyDuringRendererMigration, parent: AnyDuringRendererMigration) { - const node = renderer.createComment(""); - if (parent) { - renderer.appendChild(parent, node); - } - return node; - } - `)); - }); - }); - - describe('projectNodes migration', () => { - it('should migrate to calls to the __ngRendererProjectNodesHelper', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _element: ElementRef, private _renderer: Renderer) { - } - - projectNodes(nodesToProject: Node[]) { - this._renderer.projectNodes(this._element.nativeElement, nodesToProject); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain( - '__ngRendererProjectNodesHelper(this._renderer, this._element.nativeElement, nodesToProject);'); - }); - - it('should declare the __ngRendererProjectNodesHelper', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(element: ElementRef, renderer: Renderer) { - renderer.projectNodes(element.nativeElement, []); - } - } - `); - - await runMigration(); - - expect(stripWhitespace(tree.readContent('/index.ts'))).toContain(stripWhitespace(` - function __ngRendererProjectNodesHelper(renderer: AnyDuringRendererMigration, parent: AnyDuringRendererMigration, nodes: AnyDuringRendererMigration) { - for (let i = 0; i < nodes.length; i++) { - renderer.appendChild(parent, nodes[i]); - } - } - `)); - }); - }); - - describe('animate migration', () => { - it('should migrate to calls to the __ngRendererAnimateHelper', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _element: ElementRef, private _renderer: Renderer) { - } - - animate() { - this._renderer.animate(this._element.nativeElement); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')).toContain('__ngRendererAnimateHelper();'); - }); - - it('should declare the __ngRendererAnimateHelper', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(element: ElementRef, renderer: Renderer) { - renderer.animate(element.nativeElement); - } - } - `); - - await runMigration(); - - expect(stripWhitespace(tree.readContent('/index.ts'))).toContain(stripWhitespace(` - function __ngRendererAnimateHelper() { - throw new Error("Renderer.animate is no longer supported!"); - } - `)); - }); - }); - - describe('destroyView migration', () => { - it('should migrate to calls to the __ngRendererDestroyViewHelper', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _element: ElementRef, private _renderer: Renderer) { - } - - destroyView(allNodes: Node[]) { - this._renderer.destroyView(this._element.nativeElement, allNodes); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain('__ngRendererDestroyViewHelper(this._renderer, allNodes);'); - }); - - it('should declare the __ngRendererDestroyViewHelper', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _element: ElementRef, private _renderer: Renderer) { - } - - destroyView(allNodes: Node[]) { - this._renderer.destroyView(this._element.nativeElement, allNodes); - } - } - `); - - await runMigration(); - - expect(stripWhitespace(tree.readContent('/index.ts'))).toContain(stripWhitespace(` - function __ngRendererDestroyViewHelper(renderer: AnyDuringRendererMigration, allNodes: AnyDuringRendererMigration) { - for (let i = 0; i < allNodes.length; i++) { - renderer.destroyNode(allNodes[i]); - } - } - `)); - }); - }); - - describe('detachView migration', () => { - it('should migrate to calls to the __ngRendererDetachViewHelper', async () => { - writeFile('/index.ts', ` - import { Renderer, Component } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _renderer: Renderer) { - } - - detachView(rootNodes: Node[]) { - this._renderer.detachView(rootNodes); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain('__ngRendererDetachViewHelper(this._renderer, rootNodes);'); - }); - - it('should declare the __ngRendererDetachViewHelper', async () => { - writeFile('/index.ts', ` - import { Renderer, Component } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _renderer: Renderer) { - } - - detachView(rootNodes: Node[]) { - this._renderer.detachView(rootNodes); - } - } - `); - - await runMigration(); - - expect(stripWhitespace(tree.readContent('/index.ts'))).toContain(stripWhitespace(` - function __ngRendererDetachViewHelper(renderer: AnyDuringRendererMigration, rootNodes: AnyDuringRendererMigration) { - for (let i = 0; i < rootNodes.length; i++) { - const node = rootNodes[i]; - renderer.removeChild(renderer.parentNode(node), node); - } - } - `)); - }); - }); - - describe('attachViewAfter migration', () => { - it('should migrate to calls to the __ngRendererAttachViewAfterHelper', async () => { - writeFile('/index.ts', ` - import { Renderer, Component, ElementRef } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _element: ElementRef, private _renderer: Renderer) { - } - - attachViewAfter(rootNodes: Node[]) { - this._renderer.attachViewAfter(this._element.nativeElement, rootNodes); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain( - '__ngRendererAttachViewAfterHelper(this._renderer, this._element.nativeElement, rootNodes);'); - }); - - it('should declare the __ngRendererAttachViewAfterHelper', async () => { - writeFile('/index.ts', ` - import { Renderer, Component } from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - constructor(private _renderer: Renderer) { - } - - attachViewAfter(node: Node, rootNodes: Node[]) { - this._renderer.attachViewAfter(node, rootNodes); - } - } - `); - - await runMigration(); - - expect(stripWhitespace(tree.readContent('/index.ts'))).toContain(stripWhitespace(` - function __ngRendererAttachViewAfterHelper(renderer: AnyDuringRendererMigration, node: AnyDuringRendererMigration, rootNodes: AnyDuringRendererMigration) { - const parent = renderer.parentNode(node); - const nextSibling = renderer.nextSibling(node); - for (let i = 0; i < rootNodes.length; i++) { - renderer.insertBefore(parent, rootNodes[i], nextSibling); - } - } - `)); - }); - }); - - function writeFile(filePath: string, contents: string) { - host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); - } - - function runMigration() { - return runner.runSchematicAsync('migration-v9-renderer-to-renderer2', {}, tree).toPromise(); - } - - function stripWhitespace(contents: string) { - return contents.replace(/\s/g, ''); - } -}); diff --git a/packages/core/schematics/test/static_queries_migration_template_spec.ts b/packages/core/schematics/test/static_queries_migration_template_spec.ts deleted file mode 100644 index bdb4fbf8f91..00000000000 --- a/packages/core/schematics/test/static_queries_migration_template_spec.ts +++ /dev/null @@ -1,855 +0,0 @@ -/** - * @license - * Copyright Google LLC 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('static-queries migration with template strategy', () => { - let runner: SchematicTestRunner; - let host: TempScopedNodeJsSyncHost; - let tree: UnitTestTree; - let tmpDirPath: string; - let previousWorkingDir: string; - let warnOutput: string[]; - let errorOutput: string[]; - - beforeEach(() => { - runner = new SchematicTestRunner('test', require.resolve('../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({ - version: 1, - projects: {t: {architect: {build: {options: {tsConfig: './tsconfig.json'}}}}} - })); - - warnOutput = []; - errorOutput = []; - runner.logger.subscribe(logEntry => { - if (logEntry.level === 'warn') { - warnOutput.push(logEntry.message); - } else if (logEntry.level === 'error') { - errorOutput.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); - - writeFakeAngular(); - }); - - afterEach(() => { - shx.cd(previousWorkingDir); - shx.rm('-r', tmpDirPath); - }); - - function writeFakeAngular() { - writeFile('/node_modules/@angular/core/index.d.ts', ``); - } - - function writeFakeLibrary(selectorName = 'my-lib-selector') { - writeFile('/node_modules/my-lib/index.d.ts', `export * from './public-api';`); - writeFile('/node_modules/my-lib/public-api.d.ts', `export declare class MyLibComponent {}`); - writeFile('/node_modules/my-lib/index.metadata.json', JSON.stringify({ - __symbolic: 'module', - version: 4, - metadata: { - MyLibComponent: { - __symbolic: 'class', - decorators: [{ - __symbolic: 'call', - expression: { - __symbolic: 'reference', - module: '@angular/core', - name: 'Component', - line: 0, - character: 0 - }, - arguments: [{ - selector: selectorName, - template: `My Lib Component`, - }] - }], - members: {} - }, - }, - origins: { - MyLibComponent: './public-api', - }, - importAs: 'my-lib', - })); - } - - function writeFile(filePath: string, contents: string) { - host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); - } - - function runMigration() { - return runner.runSchematicAsync('migration-v8-static-queries', {}, tree).toPromise(); - } - - describe('ViewChild', () => { - it('should detect queries selecting elements through template reference', async () => { - writeFile('/index.ts', ` - import {Component, NgModule, ViewChild} from '@angular/core'; - - @Component({template: \` - - - -
- -
- \`}) - export class MyComp { - private @ViewChild('myButton') query: any; - private @ViewChild('myStaticButton') query2: any; - } - - @NgModule({declarations: [MyComp]}) - export class MyModule {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@ViewChild('myButton', { static: false }) query: any;`); - expect(tree.readContent('/index.ts')) - .toContain(`@ViewChild('myStaticButton', { static: true }) query2: any;`); - }); - - it('should detect queries selecting ng-template as static', async () => { - writeFile('/index.ts', ` - import {Component, NgModule, ViewChild} from '@angular/core'; - - @Component({template: \` - - My template - - \`}) - export class MyComp { - private @ViewChild('myTmpl') query: any; - } - - @NgModule({declarations: [MyComp]}) - export class MyModule {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@ViewChild('myTmpl', { static: true }) query: any;`); - }); - - it('should detect queries selecting ng-template as static (BOM)', async () => { - writeFile('/index.ts', `\uFEFF - import {Component, NgModule, ViewChild} from '@angular/core'; - - @Component({template: \` - - My template - - \`}) - export class MyComp { - private @ViewChild('myTmpl') query: any; - } - - @NgModule({declarations: [MyComp]}) - export class MyModule {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@ViewChild('myTmpl', { static: true }) query: any;`); - }); - - it('should detect queries selecting component view providers through string token', - async () => { - writeFile('/index.ts', ` - import {Component, Directive, NgModule, ViewChild} from '@angular/core'; - - @Directive({ - selector: '[myDirective]', - providers: [ - {provide: 'my-token', useValue: 'test'} - ] - }) - export class MyDirective {} - - @Directive({ - selector: '[myDirective2]', - providers: [ - {provide: 'my-token-2', useValue: 'test'} - ] - }) - export class MyDirective2 {} - - @Component({templateUrl: './my-tmpl.html'}) - export class MyComp { - private @ViewChild('my-token') query: any; - private @ViewChild('my-token-2') query2: any; - } - - @NgModule({declarations: [MyComp, MyDirective, MyDirective2]}) - export class MyModule {} - `); - - writeFile(`/my-tmpl.html`, ` - - - - - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@ViewChild('my-token', { static: true }) query: any;`); - expect(tree.readContent('/index.ts')) - .toContain(`@ViewChild('my-token-2', { static: false }) query2: any;`); - }); - - it('should detect queries selecting component view providers using class token', async () => { - writeFile('/index.ts', ` - import {Component, Directive, NgModule, ViewChild} from '@angular/core'; - - export class MyService {} - export class MyService2 {} - - @Directive({ - selector: '[myDirective]', - providers: [MyService] - }) - export class MyDirective {} - - @Directive({ - selector: '[myDirective2]', - providers: [MyService2] - }) - export class MyDirective2 {} - - @Component({templateUrl: './my-tmpl.html'}) - export class MyComp { - private @ViewChild(MyService) query: any; - private @ViewChild(MyService2) query2: any; - } - - @NgModule({declarations: [MyComp, MyDirective, MyDirective2]}) - export class MyModule {} - `); - - writeFile(`/my-tmpl.html`, ` - - - - - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@ViewChild(MyService, { static: true }) query: any;`); - expect(tree.readContent('/index.ts')) - .toContain(`@ViewChild(MyService2, { static: false }) query2: any;`); - }); - - it('should detect queries selecting component', async () => { - writeFile('/index.ts', ` - import {Component, NgModule, ViewChild} from '@angular/core'; - import {HomeComponent, HomeComponent2} from './home-comp'; - - @Component({ - template: \` - - - - - \` - }) - export class MyComp { - private @ViewChild(HomeComponent) query: any; - private @ViewChild(HomeComponent2) query2: any; - } - - @NgModule({declarations: [MyComp, HomeComponent, HomeComponent2]}) - export class MyModule {} - `); - - writeFile(`/home-comp.ts`, ` - import {Component} from '@angular/core'; - - @Component({ - selector: 'home-comp', - template: 'Home' - }) - export class HomeComponent {} - - @Component({ - selector: 'home-comp2', - template: 'Home 2' - }) - export class HomeComponent2 {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@ViewChild(HomeComponent, { static: true }) query: any;`); - expect(tree.readContent('/index.ts')) - .toContain(`@ViewChild(HomeComponent2, { static: false }) query2: any;`); - }); - - it('should detect queries selecting third-party component', async () => { - writeFakeLibrary(); - writeFile('/index.ts', ` - import {Component, NgModule, ViewChild} from '@angular/core'; - import {MyLibComponent} from 'my-lib'; - - @Component({templateUrl: './my-tmpl.html'}) - export class MyComp { - private @ViewChild(MyLibComponent) query: any; - } - - @NgModule({declarations: [MyComp, MyLibComponent]}) - export class MyModule {} - `); - - writeFile('/my-tmpl.html', ` - My projected content - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@ViewChild(MyLibComponent, { static: true }) query: any;`); - }); - - it('should detect queries selecting third-party component with multiple selectors', - async () => { - writeFakeLibrary('a-selector, test-selector'); - writeFile('/index.ts', ` - import {Component, NgModule, ViewChild} from '@angular/core'; - import {MyLibComponent} from 'my-lib'; - - @Component({templateUrl: './my-tmpl.html'}) - export class MyComp { - private @ViewChild(MyLibComponent) query: any; - } - - @NgModule({declarations: [MyComp, MyLibComponent]}) - export class MyModule {} - `); - - writeFile('/my-tmpl.html', ` - Match 1 - - Match 2 - - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@ViewChild(MyLibComponent, { static: false }) query: any;`); - }); - - it('should detect queries within structural directive', async () => { - writeFile('/index.ts', ` - import {Component, Directive, NgModule, ViewChild} from '@angular/core'; - - @Directive({selector: '[ngIf]'}) - export class FakeNgIf {} - - @Component({templateUrl: 'my-tmpl.html'}) - export class MyComp { - private @ViewChild('myRef') query: any; - private @ViewChild('myRef2') query2: any; - } - - @NgModule({declarations: [MyComp, FakeNgIf]}) - export class MyModule {} - `); - - writeFile(`/my-tmpl.html`, ` - No asterisk - With asterisk - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@ViewChild('myRef', { static: true }) query: any;`); - expect(tree.readContent('/index.ts')) - .toContain(`@ViewChild('myRef2', { static: false }) query2: any;`); - }); - - it('should detect inherited queries', async () => { - writeFile('/index.ts', ` - import {Component, NgModule, ViewChild} from '@angular/core'; - - export class BaseClass { - @ViewChild('myRef') query: any; - } - - @Component({templateUrl: 'my-tmpl.html'}) - export class MyComp extends BaseClass {} - - @NgModule({declarations: [MyComp]}) - export class MyModule {} - `); - - writeFile(`/my-tmpl.html`, ` - My Ref - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@ViewChild('myRef', { static: true }) query: any;`); - }); - - it('should detect queries declared on setter', async () => { - writeFile('/index.ts', ` - import {Component, NgModule, ViewChild} from '@angular/core'; - - @Component({templateUrl: 'my-tmpl.html'}) - export class MyComp { - @ViewChild('myRef') - set query(result: any) { /* noop */} - } - - @NgModule({declarations: [MyComp]}) - export class MyModule {} - `); - - writeFile(`/my-tmpl.html`, ` - My Ref - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toMatch(/@ViewChild\('myRef', { static: true }\)\s+set query/); - }); - - it('should detect queries declared on getter', async () => { - writeFile('/index.ts', ` - import {Component, NgModule, ViewChild} from '@angular/core'; - - @Component({templateUrl: 'my-tmpl.html'}) - export class MyComp { - @ViewChild('myRef') - get query() { return null; } - set query(result: any) { /* noop */} - } - - @NgModule({declarations: [MyComp]}) - export class MyModule {} - `); - - writeFile(`/my-tmpl.html`, ` - My Ref - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toMatch(/@ViewChild\('myRef', { static: true }\)\s+get query/); - }); - - it('should add a todo if a query is not declared in any component', async () => { - writeFile('/index.ts', ` - import {Component, NgModule, ViewChild, SomeToken} from '@angular/core'; - - export class NotAComponent { - @ViewChild('myRef', {read: SomeToken}) query: any; - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain( - `@ViewChild('myRef', /* TODO: add static flag */ { read: SomeToken }) query: any;`); - expect(warnOutput.length).toBe(1); - expect(warnOutput[0]) - .toMatch( - /^⮑ {3}index.ts@5:11:.+could not be determined.+not declared in any component/); - }); - - it('should add a todo if a query is used multiple times with different timing', async () => { - writeFile('/index.ts', ` - import {Component, NgModule, ViewChild} from '@angular/core'; - - export class BaseClass { - @ViewChild('myRef') query: any; - } - - @Component({template: '

'}) - export class FirstComp extends BaseClass {} - - @Component({template: ''}) - export class SecondComp extends BaseClass {} - - @NgModule({declarations: [FirstComp, SecondComp]}) - export class MyModule {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@ViewChild('myRef', /* TODO: add static flag */ {}) query: any;`); - expect(warnOutput.length).toBe(1); - expect(warnOutput[0]) - .toMatch( - /^⮑ {3}index.ts@5:11: Multiple components use the query with different timings./); - }); - - it('should be able to migrate an application with type checking failure which ' + - 'does not affect analysis', - async () => { - // Fakes the `@angular/package` by creating a `ViewChild` decorator - // function that requires developers to specify the "static" flag. - writeFile('/node_modules/@angular/core/index.d.ts', ` - export interface ViewChildDecorator { - (selector: Type | Function | string, opts: { - static: boolean; - read?: any; - }): any; - } - - export declare const ViewChild: ViewChildDecorator; - `); - - writeFile('/index.ts', ` - import {NgModule, Component, ViewChild} from '@angular/core'; - - @Component({ - template: '

' - }) - export class MyComp { - @ViewChild('myRef') query: any; - } - `); - - writeFile('/my-module.ts', ` - import {NgModule} from '@angular/core'; - import {MyComp} from './index'; - - @NgModule({declarations: [MyComp]}) - export class MyModule {} - `); - - await runMigration(); - - expect(errorOutput.length).toBe(0); - expect(tree.readContent('/index.ts')) - .toContain(`@ViewChild('myRef', { static: false }) query: any;`); - }); - - it('should be able to migrate applications with template type checking failure ' + - 'which does not affect analysis', - async () => { - writeFile('/index.ts', ` - import {NgModule, Component, ViewChild} from '@angular/core'; - - @Component({ - template: '

{{myVar.hello()}}

' - }) - export class MyComp { - // This causes a type checking exception as the template - // tries to call a function called "hello()" on this variable. - myVar: boolean = false; - - @ViewChild('myRef') query: any; - } - `); - - writeFile('/my-module.ts', ` - import {NgModule} from '@angular/core'; - import {MyComp} from './index'; - - @NgModule({declarations: [MyComp]}) - export class MyModule {} - `); - - await runMigration(); - - expect(errorOutput.length).toBe(0); - expect(tree.readContent('/index.ts')) - .toContain(`@ViewChild('myRef', { static: true }) query: any;`); - }); - - it('should notify user if project has syntax errors which can affect analysis', async () => { - writeFile('/index.ts', ` - import {Component, ViewChild} from '@angular/core'; - - @Component({ - template: '

' - }) - export class MyComp { - @ViewChild('myRef') query: any; - } - `); - - writeFile('/file-with-syntax-error.ts', ` - export classX ClassWithSyntaxError { - // ... - } - `); - - writeFile('/my-module.ts', ` - import {NgModule} from '@angular/core'; - import {MyComp} from './index'; - - @NgModule({declarations: [MyComp]}) - export class MyModule {} - `); - - await runMigration(); - - expect(errorOutput.length).toBe(1); - expect(errorOutput[0]).toMatch(/file-with-syntax-error\.ts\(2,9\): error TS1128.*/); - expect(tree.readContent('/index.ts')) - .toContain(`@ViewChild('myRef', { static: true }) query: any;`); - }); - - it('should gracefully exit migration if queries could not be analyzed', async () => { - writeFile('/index.ts', ` - import {Component, ViewChild} from '@angular/core'; - - @Component({template: '

'}) - export class MyComp { - @ViewChild('myRef') query: any; - } - - // **NOTE**: Analysis will fail as there is no "NgModule" that declares the component. - `); - - // We don't expect an error to be thrown as this could interrupt other - // migrations which are scheduled with "ng update" in the CLI. - await runMigration(); - - expect(errorOutput.length).toBe(1); - expect(errorOutput[0]).toMatch(/Cannot determine the module for class MyComp/); - }); - - it('should gracefully exit migration if AOT compiler throws exception', async () => { - writeFile('/my-component.ts', ` - import {Component, ViewChild} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - @ViewChild('test') query: any; - } - `); - writeFile('/app-module.ts', ` - import {NgModule} from '@angular/core'; - import {MyComp} from './components'; - - @NgModule({declarations: [MyComp]}) - export class MyModule {} - `); - - writeFile('/components.ts', `export * from './my-component';`); - writeFile('/index.ts', `export * from './app-module';`); - - // Enable flat-module bundling in order to simulate a common AOT compiler - // failure that can happen in CLI projects that use flat-module bundling - // e.g. with ng-packagr. https://github.com/angular/angular/issues/20931 - writeFile('/tsconfig.json', JSON.stringify({ - compilerOptions: { - experimentalDecorators: true, - lib: ['es2015'], - }, - angularCompilerOptions: { - flatModuleId: 'flat-module', - flatModuleOutFile: 'flat-module-bundle.js', - }, - files: ['index.ts'] - })); - - await runMigration(); - - expect(errorOutput.length).toBe(1); - expect(errorOutput[0]).toMatch(/^TypeError: Cannot read property 'module' of undefined/); - }); - - it('should add a todo for content queries which are not detectable', async () => { - writeFile('/index.ts', ` - import {Component, NgModule, ContentChild} from '@angular/core'; - - @Component({template: '

'}) - export class MyComp { - @ContentChild('myRef') query: any; - } - - @NgModule({declarations: [MyComp]}) - export class MyModule {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@ContentChild('myRef', /* TODO: add static flag */ {}) query: any;`); - expect(warnOutput.length).toBe(1); - expect(warnOutput[0]) - .toMatch(/^⮑ {3}index.ts@6:11: Content queries cannot be migrated automatically\./); - }); - - it('should add a todo if query options cannot be migrated inline', async () => { - writeFile('/index.ts', ` - import {Component, NgModule, ViewChild} from '@angular/core'; - - const myOptionsVar = {}; - - @Component({template: '

'}) - export class MyComp { - @ViewChild('myRef', myOptionsVar) query: any; - } - - @NgModule({declarations: [MyComp]}) - export class MyModule {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@ViewChild('myRef', /* TODO: add static flag */ myOptionsVar) query: any;`); - expect(warnOutput.length).toBe(1); - expect(warnOutput[0]) - .toMatch(/^⮑ {3}index.ts@8:11: Cannot update query to set explicit timing./); - expect(warnOutput[0]).toMatch(/Please manually set the query timing to.*static: true/); - }); - - it('should not normalize stylesheets which are referenced in component', async () => { - writeFile('sub_dir/index.ts', ` - import {Component, NgModule, ContentChild} from '@angular/core'; - - @Component({ - template: '

', - styleUrls: ['./my-comp.scss'] - }) - export class MyComp {} - - @NgModule({declarations: [MyComp]}) - export class MyModule {} - `); - - // In order to check that the stylesheet is not normalized, we add an "@import" statement - // that would be extracted by the "DirectiveNormalizer" and fail because the URL resolver - // is not able to resolve the "../shared" relative import to the SCSS file extension. - writeFile('/sub_dir/my-comp.scss', `@import '../shared'`); - writeFile('/shared.scss', `shared {}`); - - spyOn(console, 'error').and.callThrough(); - - await runMigration(); - - expect(console.error).toHaveBeenCalledTimes(0); - }); - - it('should always use the test migration strategy for test tsconfig files', async () => { - writeFile('/src/tsconfig.spec.json', JSON.stringify({ - compilerOptions: { - experimentalDecorators: true, - lib: ['es2015'], - }, - files: [ - 'test.ts', - ], - })); - - writeFile('/src/test.ts', ` - import {ViewChild} from '@angular/core'; - import {AppComponent} from './app.component'; - - @Component({template: 'Test'}) - class MyTestComponent { - @ViewChild('test') query: any; - } - `); - - writeFile('/src/app.component.ts', ` - import {Component, ViewChild} from '@angular/core'; - - @Component({template: ''}) - export class AppComponent { - @ViewChild('test') query: any; - } - `); - - writeFile('/src/app.module.ts', ` - import {NgModule} from '@angular/core'; - import {AppComponent} from './app.component'; - - @NgModule({declarations: [AppComponent]}) - export class MyModule {} - `); - - await runMigration(); - - expect(errorOutput.length).toBe(0); - expect(tree.readContent('/src/test.ts')) - .toContain(`@ViewChild('test', /* TODO: add static flag */ {}) query: any;`); - expect(tree.readContent('/src/app.component.ts')) - .toContain(`@ViewChild('test', { static: true }) query: any;`); - }); - - it('should not fall back to test strategy if selected strategy fails', async () => { - writeFile('/src/tsconfig.spec.json', JSON.stringify({ - compilerOptions: { - experimentalDecorators: true, - lib: ['es2015'], - }, - files: [ - 'test.ts', - ], - })); - - writeFile('/src/test.ts', `import * as mod from './app.module';`); - writeFile('/src/app.component.ts', ` - import {Component, ViewChild} from '@angular/core'; - - @Component({template: 'Test'}) - export class AppComponent { - @ViewChild('test') query: any; - } - `); - - writeFile('/src/app.module.ts', ` - import {NgModule} from '@angular/core'; - import {AppComponent} from './app.component'; - - @NgModule({declarations: [AppComponent, ThisCausesAnError]}) - export class MyModule {} - `); - - await runMigration(); - - expect(errorOutput.length).toBe(1); - expect(errorOutput[0]).toMatch(/Unexpected value 'undefined'/); - expect(tree.readContent('/src/app.component.ts')).toContain(`@ViewChild('test') query: any;`); - }); - }); -}); diff --git a/packages/core/schematics/test/static_queries_migration_usage_spec.ts b/packages/core/schematics/test/static_queries_migration_usage_spec.ts deleted file mode 100644 index e11e602f58b..00000000000 --- a/packages/core/schematics/test/static_queries_migration_usage_spec.ts +++ /dev/null @@ -1,1559 +0,0 @@ -/** - * @license - * Copyright Google LLC 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('static-queries migration with usage strategy', () => { - let runner: SchematicTestRunner; - let host: TempScopedNodeJsSyncHost; - let tree: UnitTestTree; - let tmpDirPath: string; - let previousWorkingDir: string; - let warnOutput: string[] = []; - - // Enables the query usage strategy when running the `static-query` migration. By - // default the schematic runs the template strategy and there is currently no easy - // way to pass options to the migration without using environment variables. - beforeAll(() => process.env['NG_STATIC_QUERY_USAGE_STRATEGY'] = 'true'); - afterAll(() => process.env['NG_STATIC_QUERY_USAGE_STRATEGY'] = ''); - - beforeEach(() => { - runner = new SchematicTestRunner('test', require.resolve('../migrations.json')); - host = new TempScopedNodeJsSyncHost(); - tree = new UnitTestTree(new HostTree(host)); - - writeFile('/tsconfig.json', JSON.stringify({ - compilerOptions: { - lib: ['es2015'], - }, - })); - writeFile('/angular.json', JSON.stringify({ - version: 1, - 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); - }); - - describe('ViewChild', () => { - createQueryTests('ViewChild'); - - it('should mark view queries used in "ngAfterContentInit" as static', async () => { - writeFile('/index.ts', ` - import {Component, ViewChild} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - @ViewChild('test') query: any; - - ngAfterContentInit() { - this.query.classList.add('test'); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@ViewChild('test', { static: true }) query: any;`); - }); - - it('should mark view queries used in "ngAfterContentChecked" as static', async () => { - writeFile('/index.ts', ` - import {Component, ViewChild} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - @ViewChild('test') query: any; - - ngAfterContentChecked() { - this.query.classList.add('test'); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@ViewChild('test', { static: true }) query: any;`); - }); - }); - - describe('ContentChild', () => { - createQueryTests('ContentChild'); - - it('should not mark content queries used in "ngAfterContentInit" as static', async () => { - writeFile('/index.ts', ` - import {Component, ContentChild} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - @ContentChild('test') query: any; - - ngAfterContentInit() { - this.query.classList.add('test'); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@ContentChild('test', { static: false }) query: any;`); - }); - - it('should not mark content queries used in "ngAfterContentInit" as static (BOM)', async () => { - writeFile('/index.ts', `\uFEFF - import {Component, ContentChild} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - @ContentChild('test') query: any; - - ngAfterContentInit() { - this.query.classList.add('test'); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@ContentChild('test', { static: false }) query: any;`); - }); - - it('should not mark content queries used in "ngAfterContentChecked" as static', async () => { - writeFile('/index.ts', ` - import {Component, ContentChild} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - @ContentChild('test') query: any; - - ngAfterContentChecked() { - this.query.classList.add('test'); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@ContentChild('test', { static: false }) query: any;`); - }); - }); - - function writeFile(filePath: string, contents: string) { - host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); - } - - function runMigration() { - return runner.runSchematicAsync('migration-v8-static-queries', {}, tree).toPromise(); - } - - function createQueryTests(queryType: 'ViewChild'|'ContentChild') { - it('should mark queries as dynamic', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - @${queryType}('test') unused: any; - @${queryType}('dynamic') dynamic: any; - - onClick() { - this.dynamicQuery.classList.add('test'); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: false }) unused: any;`); - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('dynamic', { static: false }) dynamic: any`); - }); - - it('should mark queries used in "ngOnChanges" as static', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - @${queryType}('test') query: any; - - ngOnChanges() { - this.query.classList.add('test'); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - }); - - it('should mark queries used in "ngOnInit" as static', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - @${queryType}('test') query: any; - - ngOnInit() { - this.query.classList.add('test'); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - }); - - it('should mark queries used in "ngDoCheck" as static', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - @${queryType}('test') query: any; - - ngDoCheck() { - this.query.classList.add('test'); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - }); - - it('should keep existing query options when updating timing', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - @${queryType}('test', { /* test */ read: null }) query: any; - - ngOnInit() { - this.query.classList.add('test'); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { /* test */ read: null, static: true }) query: any;`); - }); - - it('should add a todo for queries declared on setter', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - @${queryType}('test') - set query(result: any) {}; - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', /* TODO: add static flag */ {})`); - expect(warnOutput.length).toBe(1); - expect(warnOutput[0]) - .toMatch(/index.ts@6:11: Queries defined on accessors cannot be analyzed.$/); - }); - - it('should add a todo for queries declared on getter', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - @${queryType}('test') - get query() { return null; } - set query(result: any) {} - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', /* TODO: add static flag */ {})`); - expect(warnOutput.length).toBe(1); - expect(warnOutput[0]) - .toMatch(/index.ts@6:11: Queries defined on accessors cannot be analyzed.$/); - }); - - it('should not overwrite existing explicit query timing', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - @${queryType}('test', {static: /* untouched */ someVal}) query: any; - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', {static: /* untouched */ someVal}) query: any;`); - }); - - it('should detect queries used in deep method chain', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - // We intentionally add this comma for the second parameter in order - // to ensure that the migration does not incorrectly create an invalid - // decorator call with three parameters. e.g. "ViewQuery('test', {...}, )" - @${queryType}('test', ) query: any; - - ngOnInit() { - this.a(); - } - - a() { - this.b(); - } - - b() { - this.c(); - } - - c() { - console.log(this.query); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - }); - - it('should properly exit if recursive function is analyzed', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - @${queryType}('test') query: any; - - ngOnInit() { - this.recursive(); - } - - recursive() { - this.recursive(); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: false }) query: any;`); - }); - - it('should detect queries used in newly instantiated classes', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - @${queryType}('test') query: any; - @${queryType}('test') query2: any; - - ngOnInit() { - new A(this); - - new class Inline { - constructor(private ctx: MyComp) { - this.a(); - } - - a() { - this.ctx.query2.useStatically(); - } - }(this); - } - } - - export class A { - constructor(ctx: MyComp) { - ctx.query.test(); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query2: any;`); - }); - - it('should detect queries used in parenthesized new expressions', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - @${queryType}('test') query: any; - - ngOnInit() { - new ((A))(this); - } - } - - export class A { - constructor(ctx: MyComp) { - ctx.query.test(); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - }); - - it('should detect queries in lifecycle hook with string literal name', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - @${queryType}('test') query: any; - - 'ngOnInit'() { - this.query.test(); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - }); - - it('should detect static queries within nested inheritance', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - @${queryType}('test') query: any; - } - - export class A extends MyComp {} - export class B extends A { - - ngOnInit() { - this.query.testFn(); - } - - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - }); - - it('should detect static queries used within input setters', async () => { - writeFile('/index.ts', ` - import {Component, Input, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - @${queryType}('test') query: any; - - @Input() - get myVal() { return null; } - set myVal(newVal: any) { - this.query.classList.add('setter'); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - }); - - it('should detect inputs defined in metadata', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({ - template: '', - inputs: ["myVal"], - }) - export class MyComp { - @${queryType}('test') query: any; - - // We don't use the input decorator here as we want to verify - // that it properly detects the input through the component metadata. - get myVal() { return null; } - set myVal(newVal: any) { - this.query.classList.add('setter'); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - }); - - it('should detect aliased inputs declared in metadata', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({ - template: '', - inputs: ['prop: publicName'], - }) - export class MyComp { - @${queryType}('test') query: any; - - set prop(val: any) { - this.query.test(); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - }); - - it('should not mark query as static if query is used in non-input setter', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - @${queryType}('test') query: any; - - set myProperty(val: any) { - this.query.test(); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: false }) query: any;`); - }); - - it('should detect input decorator on setter', async () => { - writeFile('/index.ts', ` - import {Input, Component, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - @${queryType}('test') query: any; - - get myProperty() { return null; } - - // Usually the decorator is set on the get accessor, but it's also possible - // to declare the input on the setter. This ensures that it is handled properly. - @Input() - set myProperty(val: any) { - this.query.test(); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - }); - - it('should detect setter inputs in derived classes', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({ - template: '', - inputs: ['childSetter'], - }) - export class MyComp { - protected @${queryType}('test') query: any; - } - - export class B extends MyComp { - set childSetter(newVal: any) { - this.query.test(); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - }); - - it('should properly detect static query in external derived class', async () => { - writeFile('/src/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - @${queryType}('test') query: any; - } - `); - - writeFile('/src/external.ts', ` - import {MyComp} from './index'; - - export class ExternalComp extends MyComp { - ngOnInit() { - this.query.test(); - } - } - `); - - // Move the tsconfig into a subdirectory. This ensures that the update is properly - // recorded for TypeScript projects not at the schematic tree root. - host.sync.rename(normalize('/tsconfig.json'), normalize('/src/tsconfig.json')); - writeFile('/angular.json', JSON.stringify({ - version: 1, - projects: {t: {architect: {build: {options: {tsConfig: './src/tsconfig.json'}}}}} - })); - - await runMigration(); - - expect(tree.readContent('/src/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - }); - - it('should not mark queries used in promises as static', async () => { - writeFile('/es2015.dom.d.ts', ` - interface PromiseConstructor { - resolve(): Promise; - reject(): Promise; - } - - interface Promise { - then(cb: Function): Promise; - catch(cb: Function): Promise; - } - - declare var Promise: PromiseConstructor; - `); - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - private @${queryType}('test') query: any; - private @${queryType}('test') query2: any; - - ngOnInit() { - const a = Promise.resolve(); - - Promise.resolve().then(() => { - this.query.doSomething(); - }); - - Promise.reject().catch(() => { - this.query.doSomething(); - }); - - a.then(() => {}).then(() => { - this.query.doSomething(); - }); - - Promise.resolve().then(this.createPromiseCb()); - } - - createPromiseCb() { - this.query2.doSomething(); - return () => { /* empty callback */} - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: false }) query: any;`); - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query2: any;`); - }); - - it('should handle function callbacks which statically access queries', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - private @${queryType}('test') query: any; - - ngOnInit() { - this.callSync(() => this.query.doSomething()); - } - - callSync(cb: Function) { - this.callSync2(cb); - } - - callSync2(cb: Function) { - cb(); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - }); - - it('should handle class instantiations with specified callbacks that access queries', - async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - import {External} from './external'; - - @Component({template: ''}) - export class MyComp { - private @${queryType}('test') query: any; - - ngOnInit() { - new External(() => this.query.doSomething()); - } - } - `); - - writeFile('/external.ts', ` - export class External { - constructor(cb: () => void) { - // Add extra parentheses to ensure that expression is unwrapped. - ((cb))(); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - }); - - it('should handle nested functions with arguments from parent closure', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - private @${queryType}('test') query: any; - - ngOnInit() { - this.callSync(() => this.query.doSomething()); - } - - callSync(cb: Function) { - function callSyncNested() { - // The "cb" identifier comes from the "callSync" function. - cb(); - } - - callSyncNested(); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - }); - - it('should not mark queries used in setTimeout as static', async () => { - writeFile('/lib.dom.d.ts', `declare function setTimeout(cb: Function);`); - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - private @${queryType}('test') query: any; - private @${queryType}('test') query2: any; - private @${queryType}('test') query3: any; - - ngOnInit() { - setTimeout(function() { - this.query.doSomething(); - }); - - setTimeout(createCallback(this)); - } - } - - function createCallback(instance: MyComp) { - instance.query2.doSomething(); - return () => instance.query3.doSomething(); - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: false }) query: any;`); - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query2: any;`); - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: false }) query3: any;`); - }); - - it('should not mark queries used in "addEventListener" as static', async () => { - writeFile('/lib.dom.d.ts', ` - interface HTMLElement { - addEventListener(cb: Function); - } - `); - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - private @${queryType}('test') query: any; - - constructor(private elementRef: HTMLElement) {} - - ngOnInit() { - this.elementRef.addEventListener(() => { - this.query.classList.add('test'); - }); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: false }) query: any;`); - }); - - it('should not mark queries used in "requestAnimationFrame" as static', async () => { - writeFile('/lib.dom.d.ts', `declare function requestAnimationFrame(cb: Function);`); - writeFile('/index.ts', ` - import {Component, ElementRef, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - private @${queryType}('test') query: any; - - constructor(private elementRef: ElementRef) {} - - ngOnInit() { - requestAnimationFrame(() => { - this.query.classList.add('test'); - }); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: false }) query: any;`); - }); - - it('should mark queries used in immediately-invoked function expression as static', - async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - private @${queryType}('test') query: any; - private @${queryType}('test') query2: any; - - ngOnInit() { - (() => { - this.query.usedStatically(); - })(); - - (function(ctx) { - ctx.query2.useStatically(); - })(this); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query2: any;`); - }); - - it('should detect static queries used in external function-like declaration', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - import {externalFn} from './external'; - - @Component({template: ''}) - export class MyComp { - private @${queryType}('test') query: any; - - ngOnInit() { - externalFn(this); - } - } - `); - - writeFile('/external.ts', ` - import {MyComp} from './index'; - - export function externalFn(ctx: MyComp) { - ctx.query.usedStatically(); - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - }); - - it('should detect static queries used through getter property access', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - private @${queryType}('test') query: any; - - get myProp() { - return this.query.myValue; - } - - ngOnInit() { - this.myProp.test(); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - }); - - it('should detect static queries used through external getter access', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - import {External} from './external'; - - @Component({template: ''}) - export class MyComp { - @${queryType}('test') query: any; - - private external = new External(this); - - get myProp() { - return this.query.myValue; - } - - ngOnInit() { - console.log(this.external.query); - } - } - `); - - writeFile('/external.ts', ` - import {MyComp} from './index'; - - export class External { - constructor(private comp: MyComp) {} - - set query() { /** noop */ } - get query() { return this.comp.query; } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - }); - - it('should not mark queries as static if a value is assigned to accessor property', - async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - private @${queryType}('test') query: any; - - set myProp(value: any) { /* noop */} - get myProp() { - return this.query.myValue; - } - - ngOnInit() { - this.myProp = true; - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: false }) query: any;`); - }); - - it('should mark queries as static if non-input setter uses query', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - private @${queryType}('test') query: any; - - get myProp() { return null; } - set myProp(value: any) { - this.query.doSomething(); - } - - ngOnInit() { - this.myProp = 'newValue'; - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - }); - - it('should check setter and getter when using compound assignment', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - private @${queryType}('test') query: any; - private @${queryType}('test') query2: any; - - get myProp() { return this.query2 } - set myProp(value: any) { - this.query.doSomething(); - } - - ngOnInit() { - this.myProp *= 5; - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query2: any;`); - }); - - it('should check getters when using comparison operator in binary expression', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - private @${queryType}('test') query: any; - - get myProp() { return this.query } - set myProp(value: any) { /* noop */ } - - ngOnInit() { - if (this.myProp === 3) { - // noop - } - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - }); - - it('should check derived abstract class methods', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - export abstract class RootBaseClass { - abstract getQuery(): any; - - ngOnInit() { - this.getQuery().doSomething(); - } - } - - export abstract class BaseClass extends RootBaseClass { - abstract getQuery2(): any; - - getQuery() { - this.getQuery2(); - } - } - - @Component({template: ''}) - export class Subclass extends BaseClass { - @${queryType}('test') query: any; - - getQuery2(): any { - return this.query; - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - }); - - it('should detect queries accessed through deep abstract class method', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - export abstract class RootBaseClass { - abstract getQuery(): any; - - ngOnInit() { - this.getQuery().doSomething(); - } - } - - export abstract class BaseClass extends RootBaseClass { - /* additional layer of indirection */ - } - - @Component({template: ''}) - export class Subclass extends BaseClass { - @${queryType}('test') query: any; - - getQuery(): any { - return this.query; - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - }); - - it('should detect queries accessed through abstract property getter', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - export abstract class BaseClass { - abstract myQuery: any; - - ngOnInit() { - this.myQuery.doSomething(); - } - } - - @Component({template: ''}) - export class Subclass extends BaseClass { - @${queryType}('test') query: any; - - get myQuery() { return this.query; } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - }); - - it('should detect queries accessed through abstract property setter', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - export abstract class BaseClass { - abstract myQuery: any; - - ngOnInit() { - this.myQuery = "trigger"; - } - } - - @Component({template: ''}) - export class Subclass extends BaseClass { - @${queryType}('test') query: any; - - set myQuery(val: any) { this.query.doSomething() } - get myQuery() { /* noop */ } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - }); - - it('should detect query usage in abstract class methods accessing inherited query', - async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - export abstract class RootBaseClass { - abstract getQuery(): any; - - ngOnInit() { - this.getQuery().doSomething(); - } - } - - export abstract class BaseClass extends RootBaseClass { - @${queryType}('test') query: any; - abstract getQuery2(): any; - - getQuery() { - this.getQuery2(); - } - } - - @Component({template: ''}) - export class Subclass extends BaseClass { - - getQuery2(): any { - return this.query; - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - }); - - it('should detect query usage within component template', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({templateUrl: 'my-template.html'}) - export class MyComponent { - @${queryType}('test') query: any; - } - `); - - writeFile(`/my-template.html`, ` - - - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - }); - - it('should detect query usage with nested property read within component template', - async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({templateUrl: 'my-template.html'}) - export class MyComponent { - @${queryType}('test') query: any; - } - `); - - writeFile(`/my-template.html`, ` - - - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - }); - - it('should not mark query as static if template has template reference with same name', - async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({templateUrl: 'my-template.html'}) - export class MyComponent { - @${queryType}('test') query: any; - } - `); - - writeFile(`/my-template.html`, ` - - - - - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: false }) query: any;`); - }); - - it('should not mark query as static if template has property read with query name but different receiver', - async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({templateUrl: 'my-template.html'}) - export class MyComponent { - myObject: {someProp: any}; - @${queryType}('test') someProp: any; - } - `); - - // This test ensures that we don't accidentally treat template property reads - // which do not refer to the query of the component instance, but have the same - // "render3Ast.PropertyRead" name, as references to the query declaration. - writeFile(`/my-template.html`, ` - - - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: false }) someProp: any;`); - }); - - it('should ignore queries accessed within element', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({templateUrl: 'my-template.html'}) - export class MyComponent { - @${queryType}('test') query: any; - } - `); - - writeFile(`/my-template.html`, ` - - - - - - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: false }) query: any;`); - }); - - it('should detect inherited queries used in templates', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - export class ParentClass { - @${queryType}('test') query: any; - } - - @Component({templateUrl: 'my-template.html'}) - export class MyComponent extends ParentClass {} - `); - - writeFile(`/my-template.html`, ` - - - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - }); - - it('should mark queries which could be accessed statically within third-party calls as ambiguous', - async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - import {thirdPartySync} from 'my-lib'; - - @Component({template: 'Template'}) - export class MyComponent { - @${queryType}('test') query: any; - @${queryType}('test') query2: any; - - ngOnInit() { - const myVarFn = () => this.query2.doSomething(); - - thirdPartySync(() => this.query.doSomething()); - thirdPartySync(myVarFn); - } - } - `); - - writeFile( - '/node_modules/my-lib/index.d.ts', `export function thirdPartySync(fn: Function);`); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${ - queryType}('test', /* TODO: check static flag */ { static: true }) query: any;`); - expect(tree.readContent('/index.ts')) - .toContain(`@${ - queryType}('test', /* TODO: check static flag */ { static: true }) query2: any;`); - expect(warnOutput.length).toBe(2); - expect(warnOutput[0]).toContain('Query timing is ambiguous.'); - expect(warnOutput[1]).toContain('Query timing is ambiguous.'); - }); - - it('should mark queries which could be accessed statically within third-party new expressions as ambiguous', - async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - import {ThirdParty} from 'my-lib'; - - @Component({template: 'Template'}) - export class MyComponent { - @${queryType}('test') query: any; - - ngOnInit() { - new ThirdParty(() => this.query.doSomething()); - } - } - `); - - writeFile('/node_modules/my-lib/index.d.ts', ` - export declare class ThirdParty { - constructor(cb: Function); - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${ - queryType}('test', /* TODO: check static flag */ { static: true }) query: any;`); - expect(warnOutput.length).toBe(1); - expect(warnOutput[0]) - .toContain( - 'Query timing is ambiguous. Please check if the query can be marked as dynamic'); - }); - - it('should properly handle multiple tsconfig files', async () => { - writeFile('/src/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class MyComp { - private @${queryType}('test') query: any; - } - `); - - writeFile('/src/tsconfig.json', JSON.stringify({ - compilerOptions: { - lib: ['es2015'], - } - })); - - // The migration runs for "/tsconfig.json" and "/src/tsconfig.json" which both - // contain the "src/index.ts" file. This test ensures that we don't incorrectly - // apply the code transformation multiple times with outdated offsets. - await runMigration(); - - expect(tree.readContent('/src/index.ts')) - .toContain(`@${queryType}('test', { static: false }) query: any;`); - }); - - it('should support function call with default parameter value', async () => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({template: 'Test'}) - export class MyComponent { - @${queryType}('test') query: any; - - ngOnInit() { - this.myFunction(); - } - - myFunction(unused?: string, cb = () => this.query.doSomething) { - cb(); - } - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`@${queryType}('test', { static: true }) query: any;`); - }); - } -}); diff --git a/packages/core/schematics/test/template_var_assignment_migration_spec.ts b/packages/core/schematics/test/template_var_assignment_migration_spec.ts deleted file mode 100644 index 3c5a5534d94..00000000000 --- a/packages/core/schematics/test/template_var_assignment_migration_spec.ts +++ /dev/null @@ -1,301 +0,0 @@ -/** - * @license - * Copyright Google LLC 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('template variable assignment 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('../migrations.json')); - host = new TempScopedNodeJsSyncHost(); - tree = new UnitTestTree(new HostTree(host)); - - writeFile('/tsconfig.json', JSON.stringify({ - compilerOptions: { - lib: ['es2015'], - }, - })); - writeFile('/angular.json', JSON.stringify({ - version: 1, - 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)); - } - - function runMigration() { - return runner.runSchematicAsync('migration-v8-template-local-variables', {}, tree).toPromise(); - } - - it('should warn for two-way data binding variable assignment', async () => { - writeFile('/index.ts', ` - import {Component} from '@angular/core'; - - @Component({ - template: '', - }) - export class MyComp {} - `); - - await runMigration(); - - expect(warnOutput.length).toBe(1); - expect(warnOutput[0]).toMatch(/^⮑ {3}index.ts@5:69: Found assignment/); - }); - - it('should warn for two-way data binding assigning to "as" variable', async () => { - writeFile('/index.ts', ` - import {Component} from '@angular/core'; - - @Component({ - templateUrl: './tmpl.html', - }) - export class MyComp {} - `); - - writeFile('/tmpl.html', ` -
- -
- `); - - await runMigration(); - - expect(warnOutput.length).toBe(1); - expect(warnOutput).toMatch(/^⮑ {3}tmpl.html@3:31: Found assignment/); - }); - - it('should warn for bound event assignments to "as" variable', async () => { - writeFile('/index.ts', ` - import {Component} from '@angular/core'; - - @Component({ - templateUrl: './sub_dir/tmpl.html', - }) - export class MyComp {} - `); - - writeFile('/sub_dir/tmpl.html', ` -
-
Hide
-
Show
-
- `); - - await runMigration(); - - expect(warnOutput.length).toBe(2); - expect(warnOutput[0]).toMatch(/^⮑ {3}sub_dir\/tmpl.html@3:25: Found assignment/); - expect(warnOutput[1]).toMatch(/^⮑ {3}sub_dir\/tmpl.html@4:25: Found assignment/); - }); - - it('should warn for bound event assignments to template "let" variables', async () => { - writeFile('/index.ts', ` - import {Component} from '@angular/core'; - - @Component({ - templateUrl: './sub_dir/tmpl.html', - }) - export class MyComp {} - `); - - writeFile('/sub_dir/tmpl.html', ` - -
Hide
-
Show
-
- `); - - await runMigration(); - - expect(warnOutput.length).toBe(2); - expect(warnOutput[0]).toMatch(/^⮑ {3}sub_dir\/tmpl.html@3:25: Found assignment/); - expect(warnOutput[1]).toMatch(/^⮑ {3}sub_dir\/tmpl.html@4:25: Found assignment/); - }); - - it('should not warn for bound event assignments to component property', async () => { - writeFile('/index.ts', ` - import {Component} from '@angular/core'; - - @Component({ - templateUrl: './sub_dir/tmpl.html', - }) - export class MyComp {} - `); - - writeFile('/sub_dir/tmpl.html', ``); - - await runMigration(); - - expect(warnOutput.length).toBe(0); - }); - - it('should not warn for bound event assignments to template variable object property', - async () => { - writeFile('/index.ts', ` - import {Component} from '@angular/core'; - - @Component({ - templateUrl: './sub_dir/tmpl.html', - }) - export class MyComp {} - `); - - writeFile('/sub_dir/tmpl.html', ` - - `); - - await runMigration(); - - expect(warnOutput.length).toBe(0); - }); - - it('should not warn for property writes with template variable name but different receiver', - async () => { - writeFile('/index.ts', ` - import {Component} from '@angular/core'; - - @Component({ - templateUrl: './sub_dir/tmpl.html', - }) - export class MyComp { - someProp = { - element: 'someValue', - }; - } - `); - - writeFile('/sub_dir/tmpl.html', ` - - `); - - await runMigration(); - - expect(warnOutput.length).toBe(0); - }); - - it('should warn for template variable assignments in expression conditional', async () => { - writeFile('/index.ts', ` - import {Component} from '@angular/core'; - - @Component({ - templateUrl: './sub_dir/tmpl.html', - }) - export class MyComp { - otherVar = false; - } - `); - - writeFile('/sub_dir/tmpl.html', ` - -

-
- `); - - await runMigration(); - - expect(warnOutput.length).toBe(1); - expect(warnOutput[0]).toMatch(/^⮑ {3}sub_dir\/tmpl.html@3:31: Found assignment/); - }); - - it('should not warn for property writes with template variable name but different scope', - async () => { - writeFile('/index.ts', ` - import {Component} from '@angular/core'; - - @Component({ - templateUrl: './sub_dir/tmpl.html', - }) - export class MyComp { - element = 'someValue'; - } - `); - - writeFile('/sub_dir/tmpl.html', ` - - - `); - - await runMigration(); - - expect(warnOutput.length).toBe(0); - }); - - - it('should not throw an error if a detected template fails parsing', async () => { - writeFile('/index.ts', ` - import {Component} from '@angular/core'; - - @Component({ - templateUrl: './sub_dir/tmpl.html', - }) - export class MyComp {} - `); - - writeFile('/sub_dir/tmpl.html', ``); - - await runMigration(); - - expect(warnOutput.length).toBe(0); - }); - - it('should be able to report multiple templates within the same source file', async () => { - writeFile('/index.ts', ` - import {Component} from '@angular/core'; - - @Component({ - template: '', - }) - export class MyComp {} - - @Component({ - template: '', - }) - export class MyComp2 {} - `); - - await runMigration(); - - expect(warnOutput.length).toBe(2); - expect(warnOutput[0]).toMatch(/^⮑ {3}index.ts@5:56: Found assignment/); - expect(warnOutput[1]).toMatch(/^⮑ {3}index.ts@10:53: Found assignment/); - }); -}); diff --git a/packages/core/schematics/test/undecorated_classes_with_decorated_fields_migration_spec.ts b/packages/core/schematics/test/undecorated_classes_with_decorated_fields_migration_spec.ts deleted file mode 100644 index 7d4d7dd66fd..00000000000 --- a/packages/core/schematics/test/undecorated_classes_with_decorated_fields_migration_spec.ts +++ /dev/null @@ -1,622 +0,0 @@ -/** - * @license - * Copyright Google LLC 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'; -import {dedent} from './helpers'; - -describe('Undecorated classes with decorated fields migration', () => { - let runner: SchematicTestRunner; - let host: TempScopedNodeJsSyncHost; - let tree: UnitTestTree; - let tmpDirPath: string; - let previousWorkingDir: string; - let warnings: string[]; - - beforeEach(() => { - runner = new SchematicTestRunner('test', require.resolve('../migrations.json')); - host = new TempScopedNodeJsSyncHost(); - tree = new UnitTestTree(new HostTree(host)); - - writeFile('/tsconfig.json', JSON.stringify({compilerOptions: {lib: ['es2015']}})); - writeFile('/angular.json', JSON.stringify({ - version: 1, - projects: {t: {architect: {build: {options: {tsConfig: './tsconfig.json'}}}}} - })); - - warnings = []; - runner.logger.subscribe(entry => { - if (entry.level === 'warn') { - warnings.push(entry.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); - }); - - it(`should add an import for Directive if there isn't one already`, async () => { - writeFile('/index.ts', ` - import { Input } from '@angular/core'; - - export class Base { - @Input() isActive: boolean; - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`import { Input, Directive } from '@angular/core';`); - }); - - it('should not change the imports if there is an import for Directive already', async () => { - writeFile('/index.ts', ` - import { Directive, Input } from '@angular/core'; - - export class Base { - @Input() isActive: boolean; - } - - @Directive() - export class Child extends Base { - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`import { Directive, Input } from '@angular/core';`); - }); - - it('should not generate conflicting imports there is a different `Directive` symbol', - async () => { - writeFile('/index.ts', ` - import { HostBinding } from '@angular/core'; - - export class Directive { - // Simulates a scenario where a library defines a class named "Directive". - // We don't want to generate a conflicting import. - } - - export class MyLibrarySharedBaseClass { - @HostBinding('class.active') isActive: boolean; - } - `); - - await runMigration(); - const fileContent = tree.readContent('/index.ts'); - expect(fileContent) - .toContain(`import { HostBinding, Directive as Directive_1 } from '@angular/core';`); - expect(fileContent).toMatch(/@Directive_1\(\)\s+export class MyLibrarySharedBaseClass/); - }); - - it('should add @Directive to undecorated classes that have @Input', async () => { - writeFile('/index.ts', ` - import { Input } from '@angular/core'; - - export class Base { - @Input() isActive: boolean; - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toContain(`@Directive()\nexport class Base {`); - }); - - it('should not migrate classes decorated with @Component', async () => { - writeFile('/index.ts', dedent` - import {Input, Component} from '@angular/core'; - - @Component({selector: 'hello', template: 'hello'}) - export class Base { - @Input() isActive: boolean; - } - - @Component({selector: 'hello', template: 'hello'}) - export class Derived extends Base { - ngOnDestroy() {} - } - `); - - await runMigration(); - - expect(warnings.length).toBe(0); - expect(tree.readContent('/index.ts')).toBe(dedent` - import {Input, Component} from '@angular/core'; - - @Component({selector: 'hello', template: 'hello'}) - export class Base { - @Input() isActive: boolean; - } - - @Component({selector: 'hello', template: 'hello'}) - export class Derived extends Base { - ngOnDestroy() {} - } - `); - }); - - it('should not migrate classes decorated with @Directive', async () => { - writeFile('/index.ts', dedent` - import {Input, Directive} from '@angular/core'; - - @Directive() - export class Base { - @Input() isActive: boolean; - } - - @Directive({selector: 'other'}) - export class Other extends Base { - ngOnDestroy() {} - } - `); - - await runMigration(); - - expect(warnings.length).toBe(0); - expect(tree.readContent('/index.ts')).toBe(dedent` - import {Input, Directive} from '@angular/core'; - - @Directive() - export class Base { - @Input() isActive: boolean; - } - - @Directive({selector: 'other'}) - export class Other extends Base { - ngOnDestroy() {} - } - `); - }); - - it('should not migrate when class inherits from component', async () => { - writeFile('/index.ts', dedent` - import {Input, Component} from '@angular/core'; - - @Component({selector: 'my-comp', template: 'my-comp'}) - export class MyComp {} - - export class WithDisabled extends MyComp { - @Input() disabled: boolean; - } - `); - - await runMigration(); - - expect(warnings.length).toBe(0); - expect(tree.readContent('/index.ts')).toBe(dedent` - import {Input, Component} from '@angular/core'; - - @Component({selector: 'my-comp', template: 'my-comp'}) - export class MyComp {} - - export class WithDisabled extends MyComp { - @Input() disabled: boolean; - } - `); - }); - - it('should not migrate when class inherits from pipe', async () => { - writeFile('/index.ts', dedent` - import {Pipe} from '@angular/core'; - - @Pipe({name: 'my-pipe'}) - export class MyPipe {} - - export class PipeDerived extends MyPipe { - ngOnDestroy() {} - } - `); - - await runMigration(); - - expect(warnings.length).toBe(0); - expect(tree.readContent('/index.ts')).toBe(dedent` - import {Pipe} from '@angular/core'; - - @Pipe({name: 'my-pipe'}) - export class MyPipe {} - - export class PipeDerived extends MyPipe { - ngOnDestroy() {} - } - `); - }); - - it('should not migrate when class inherits from injectable', async () => { - writeFile('/index.ts', dedent` - import {Injectable} from '@angular/core'; - - @Injectable() - export class MyService {} - - export class ServiceDerived extends MyService { - ngOnDestroy() {} - } - `); - - await runMigration(); - - expect(warnings.length).toBe(0); - expect(tree.readContent('/index.ts')).toBe(dedent` - import {Injectable} from '@angular/core'; - - @Injectable() - export class MyService {} - - export class ServiceDerived extends MyService { - ngOnDestroy() {} - } - `); - }); - - it('should not migrate when class inherits from directive', async () => { - writeFile('/index.ts', dedent` - import {Directive} from '@angular/core'; - - @Directive({selector: 'hello'}) - export class MyDir {} - - export class DirDerived extends MyDir { - ngOnDestroy() {} - } - `); - - await runMigration(); - - expect(warnings.length).toBe(0); - expect(tree.readContent('/index.ts')).toBe(dedent` - import {Directive} from '@angular/core'; - - @Directive({selector: 'hello'}) - export class MyDir {} - - export class DirDerived extends MyDir { - ngOnDestroy() {} - } - `); - }); - - it('should not add multiple TODOs for ambiguous classes', async () => { - writeFile('/angular.json', JSON.stringify({ - version: 1, - projects: { - test: { - architect: { - build: {options: {tsConfig: './tsconfig.json'}}, - test: {options: {tsConfig: './tsconfig.json'}}, - } - } - } - })); - writeFile('/index.ts', dedent` - export class MyService { - ngOnDestroy() {} - } - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')).toBe(dedent` - // TODO: Add Angular decorator. - export class MyService { - ngOnDestroy() {} - } - `); - }); - - it('should not report pipe using `ngOnDestroy` as ambiguous', async () => { - writeFile('/index.ts', dedent` - import {Pipe} from '@angular/core'; - - @Pipe({name: 'my-pipe'}) - export class MyPipe { - ngOnDestroy() {} - transform() {} - } - `); - - await runMigration(); - - expect(warnings.length).toBe(0); - expect(tree.readContent('/index.ts')).toBe(dedent` - import {Pipe} from '@angular/core'; - - @Pipe({name: 'my-pipe'}) - export class MyPipe { - ngOnDestroy() {} - transform() {} - } - `); - }); - - it('should not report injectable using `ngOnDestroy` as ambiguous', async () => { - writeFile('/index.ts', dedent` - import {Injectable} from '@angular/core'; - - @Injectable({providedIn: 'root'}) - export class MyService { - ngOnDestroy() {} - } - `); - - await runMigration(); - - expect(warnings.length).toBe(0); - expect(tree.readContent('/index.ts')).toBe(dedent` - import {Injectable} from '@angular/core'; - - @Injectable({providedIn: 'root'}) - export class MyService { - ngOnDestroy() {} - } - `); - }); - - it('should add @Directive to undecorated classes that have @Output', async () => { - writeFile('/index.ts', ` - import { Output, EventEmitter } from '@angular/core'; - - export class Base { - @Output() clicked = new EventEmitter(); - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toContain(`@Directive()\nexport class Base {`); - }); - - it('should add @Directive to undecorated classes that have a host binding', async () => { - writeFile('/index.ts', ` - import { HostBinding } from '@angular/core'; - - export class Base { - @HostBinding('attr.id') - get id() { - return 'id-' + Date.now(); - } - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toContain(`@Directive()\nexport class Base {`); - }); - - it('should add @Directive to undecorated classes that have a host listener', async () => { - writeFile('/index.ts', ` - import { HostListener } from '@angular/core'; - - export class Base { - @HostListener('keydown') - handleKeydown() { - console.log('Key has been pressed'); - } - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toContain(`@Directive()\nexport class Base {`); - }); - - it('should add @Directive to undecorated classes that have a ViewChild query', async () => { - writeFile('/index.ts', ` - import { ViewChild, ElementRef } from '@angular/core'; - - export class Base { - @ViewChild('button') button: ElementRef; - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toContain(`@Directive()\nexport class Base {`); - }); - - it('should add @Directive to undecorated classes that have a ViewChildren query', async () => { - writeFile('/index.ts', ` - import { ViewChildren, ElementRef } from '@angular/core'; - - export class Base { - @ViewChildren('button') button: ElementRef; - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toContain(`@Directive()\nexport class Base {`); - }); - - it('should add @Directive to undecorated classes that have a ContentChild query', async () => { - writeFile('/index.ts', ` - import { ContentChild, ElementRef } from '@angular/core'; - - export class Base { - @ContentChild('button') button: ElementRef; - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toContain(`@Directive()\nexport class Base {`); - }); - - it('should add @Directive to undecorated classes that have a ContentChildren query', async () => { - writeFile('/index.ts', ` - import { ContentChildren, ElementRef } from '@angular/core'; - - export class Base { - @ContentChildren('button') button: ElementRef; - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toContain(`@Directive()\nexport class Base {`); - }); - - it('should migrate undecorated class that uses "ngOnChanges" lifecycle hook', - () => assertLifecycleHookMigrated('ngOnChanges')); - it('should migrate undecorated class that uses "ngOnInit" lifecycle hook', - () => assertLifecycleHookMigrated('ngOnInit')); - it('should migrate undecorated class that uses "ngDoCheck" lifecycle hook', - () => assertLifecycleHookMigrated('ngDoCheck')); - it('should migrate undecorated class that uses "ngAfterViewInit" lifecycle hook', - () => assertLifecycleHookMigrated('ngAfterViewInit')); - it('should migrate undecorated class that uses "ngAfterViewChecked" lifecycle hook', - () => assertLifecycleHookMigrated('ngAfterViewChecked')); - it('should migrate undecorated class that uses "ngAfterContentInit" lifecycle hook', - () => assertLifecycleHookMigrated('ngAfterContentInit')); - it('should migrate undecorated class that uses "ngAfterContentChecked" lifecycle hook', - () => assertLifecycleHookMigrated('ngAfterContentChecked')); - - it(`should report an error and add a TODO for undecorated classes that only define ` + - `the "ngOnDestroy" lifecycle hook`, - async () => { - writeFile('/index.ts', ` - import { Input } from '@angular/core'; - - export class SomeClassWithAngularFeatures { - ngOnDestroy() { - // noop for testing - } - } - `); - - await runMigration(); - - expect(warnings.length).toBe(1); - expect(warnings[0]) - .toMatch( - 'index.ts@4:7: Class uses Angular features but cannot be migrated automatically. ' + - 'Please add an appropriate Angular decorator.'); - expect(tree.readContent('/index.ts')) - .toMatch(/TODO: Add Angular decorator\.\nexport class SomeClassWithAngularFeatures {/); - }); - - it('should add @Directive to undecorated derived classes of a migrated class', async () => { - writeFile('/index.ts', ` - import { Input, Directive, NgModule } from '@angular/core'; - - export class Base { - @Input() isActive: boolean; - } - - export class DerivedA extends Base {} - export class DerivedB extends DerivedA {} - export class DerivedC extends DerivedB {} - - @Directive({selector: 'my-comp'}) - export class MyComp extends DerivedC {} - - export class MyCompWrapped extends MyComp {} - - @NgModule({declarations: [MyComp, MyCompWrapped]}) - export class AppModule {} - `); - - await runMigration(); - const fileContent = tree.readContent('/index.ts'); - - expect(warnings.length).toBe(0); - expect(fileContent).toContain(`import { Input, Directive, NgModule } from '@angular/core';`); - expect(fileContent).toMatch(/@Directive\(\)\s+export class Base/); - expect(fileContent).toMatch(/@Directive\(\)\s+export class DerivedA/); - expect(fileContent).toMatch(/@Directive\(\)\s+export class DerivedB/); - expect(fileContent).toMatch(/@Directive\(\)\s+export class DerivedC/); - expect(fileContent).toMatch(/}\s+@Directive\(\{selector: 'my-comp'}\)\s+export class MyComp/); - expect(fileContent).toMatch(/}\s+export class MyCompWrapped/); - expect(fileContent).not.toContain('TODO: Add Angular decorator'); - }); - - it('should add @Directive to derived undecorated classes of abstract directives', async () => { - writeFile('/index.ts', ` - import { Input, Directive, NgModule } from '@angular/core'; - - @Directive() - export class Base { - // ... - } - - export class DerivedA extends Base {} - export class DerivedB extends DerivedA {} - export class DerivedC extends DerivedB {} - - @Directive({selector: 'my-comp'}) - export class MyComp extends DerivedC {} - - export class MyCompWrapped extends MyComp {} - - @NgModule({declarations: [MyComp, MyCompWrapped]}) - export class AppModule {} - `); - - await runMigration(); - const fileContent = tree.readContent('/index.ts'); - expect(fileContent).toContain(`import { Input, Directive, NgModule } from '@angular/core';`); - expect(fileContent).toMatch(/core';\s+@Directive\(\)\s+export class Base/); - expect(fileContent).toMatch(/@Directive\(\)\s+export class DerivedA/); - expect(fileContent).toMatch(/@Directive\(\)\s+export class DerivedB/); - expect(fileContent).toMatch(/@Directive\(\)\s+export class DerivedC/); - expect(fileContent).toMatch(/}\s+@Directive\(\{selector: 'my-comp'}\)\s+export class MyComp/); - expect(fileContent).toMatch(/}\s+export class MyCompWrapped/); - }); - - it('should not throw if undecorated class extends from unresolved declaration', async () => { - writeFile('/lib.d.ts', ` - // Fakes the ES5 error default lib types. Since we are in a virtual tree, - // the default lib types from TypeScript are not available. - interface ErrorConstructor {} - declare var Error: ErrorConstructor; - `); - writeFile('/index.ts', ` - export class MyCustomErrorClass extends Error {} - `); - - let error: any = null; - try { - await runMigration(); - } catch (e) { - error = e; - } - - expect(error).toBe(null); - }); - - async function assertLifecycleHookMigrated(lifecycleHookName: string) { - writeFile('/index.ts', ` - import { Input } from '@angular/core'; - - export class SomeClassWithAngularFeatures { - ${lifecycleHookName}() { - // noop for testing - } - } - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`@Directive()\nexport class SomeClassWithAngularFeatures {`); - } - - function writeFile(filePath: string, contents: string) { - host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); - } - - function runMigration() { - return runner - .runSchematicAsync('migration-v10-undecorated-classes-with-decorated-fields', {}, tree) - .toPromise(); - } -}); diff --git a/packages/core/schematics/test/undecorated_classes_with_di_migration_spec.ts b/packages/core/schematics/test/undecorated_classes_with_di_migration_spec.ts deleted file mode 100644 index 82a9f973351..00000000000 --- a/packages/core/schematics/test/undecorated_classes_with_di_migration_spec.ts +++ /dev/null @@ -1,1562 +0,0 @@ -/** - * @license - * Copyright Google LLC 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'; -import {dedent} from './helpers'; - -describe('Undecorated classes with DI migration', () => { - let runner: SchematicTestRunner; - let host: TempScopedNodeJsSyncHost; - let tree: UnitTestTree; - let tmpDirPath: string; - let previousWorkingDir: string; - let warnOutput: string[]; - let errorOutput: string[]; - let infoOutput: string[]; - - beforeEach(() => { - runner = new SchematicTestRunner('test', require.resolve('../migrations.json')); - host = new TempScopedNodeJsSyncHost(); - tree = new UnitTestTree(new HostTree(host)); - - writeFakeAngular(); - writeFile('/tsconfig.json', JSON.stringify({ - compilerOptions: { - lib: ['es2015'], - }, - })); - writeFile('/angular.json', JSON.stringify({ - version: 1, - projects: {t: {architect: {build: {options: {tsConfig: './tsconfig.json'}}}}} - })); - - warnOutput = []; - errorOutput = []; - infoOutput = []; - runner.logger.subscribe(logEntry => { - if (logEntry.level === 'warn') { - warnOutput.push(logEntry.message); - } else if (logEntry.level === 'error') { - errorOutput.push(logEntry.message); - } else if (logEntry.level === 'info') { - infoOutput.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)); - } - - function runMigration() { - return runner.runSchematicAsync('migration-v9-undecorated-classes-with-di', {}, tree) - .toPromise(); - } - - function writeFakeAngular() { - writeFile('/node_modules/@angular/core/index.d.ts', ` - export declare class PipeTransform {} - export declare class NgZone {} - export declare enum ViewEncapsulation { - None = 2 - } - `); - } - - it('should print a failure message base class is declared through type definition', async () => { - writeFile('/node_modules/my-lib/package.json', JSON.stringify({ - version: '0.0.0', - main: './index.js', - typings: './index.d.ts', - })); - writeFile('/node_modules/my-lib/index.d.ts', ` - import {NgZone} from '@angular/core'; - - export declare class SuperBaseClass { - constructor(zone: NgZone); - } - `); - - writeFile('/index.ts', ` - import {Component, NgModule} from '@angular/core'; - import {SuperBaseClass} from 'my-lib'; - - export class BaseClass extends SuperBaseClass {} - - @Component({template: ''}) - export class MyComponent extends BaseClass {} - - @NgModule({declarations: [MyComponent]}) - export class MyModule {} - `); - - await runMigration(); - - expect(errorOutput.length).toBe(0); - expect(warnOutput.length).toBe(1); - expect(warnOutput[0]).toMatch(/Class needs to declare an explicit constructor./); - expect(infoOutput.join(' ')) - .toContain( - 'Could not migrate all undecorated classes that use ' + - 'dependency injection. Please manually fix the following failures'); - }); - - it('should add @Directive() decorator to extended base class', async () => { - writeFile('/index.ts', ` - import {Component, NgModule, NgZone} from '@angular/core'; - - export class BaseClass { - constructor(zone: NgZone) {} - } - - export class BaseClass2 { - constructor(zone: NgZone) {} - } - - @Component({template: ''}) - export class MyComponent extends BaseClass {} - - @Component({template: ''}) - export class MyComponent2 extends BaseClass2 {} - - @NgModule({declarations: [MyComponent, MyComponent2]}) - export class AppModule {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')).toMatch(/@Directive\(\)\nexport class BaseClass {/); - expect(tree.readContent('/index.ts')).toMatch(/@Directive\(\)\nexport class BaseClass2 {/); - }); - - it('not decorated base class multiple times if extended multiple times', async () => { - writeFile('/index.ts', dedent` - import {Component, NgModule, NgZone} from '@angular/core'; - - export class BaseClass { - constructor(zone: NgZone) {} - } - - @Component({template: ''}) - export class MyComponent extends BaseClass {} - - @Component({template: ''}) - export class MyComponent2 extends BaseClass {} - - @NgModule({declarations: [MyComponent, MyComponent2]}) - export class AppModule {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')).toContain(dedent` - - @Directive() - export class BaseClass { - constructor(zone: NgZone) {} - }`); - }); - - it('should add @Injectable() decorator to extended base class', async () => { - writeFile('/index.ts', ` - import {Injectable, NgModule, NgZone} from '@angular/core'; - - export class BaseClass { - constructor(zone: NgZone) {} - } - - @Injectable({template: ''}) - export class MyService extends BaseClass {} - - @NgModule({providers: [MyService]}) - export class AppModule {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')).toMatch(/@Injectable\(\)\nexport class BaseClass {/); - }); - - it('should not decorate base class for decorated pipe', async () => { - writeFile('/index.ts', dedent` - import {Component, NgModule, Pipe, PipeTransform} from '@angular/core'; - - @Pipe({name: 'test'}) - export class MyPipe extends PipeTransform {} - - @NgModule({declarations: [MyPipe]}) - export class AppModule {} - `); - - await runMigration(); - - expect(errorOutput.length).toBe(0); - expect(warnOutput.length).toBe(0); - - expect(tree.readContent('/index.ts')).toContain(dedent` - @Pipe({name: 'test'}) - export class MyPipe extends PipeTransform {}`); - }); - - it('should not decorate base class if no constructor is inherited', async () => { - writeFile('/index.ts', dedent` - import {Component, NgModule, Directive} from '@angular/core'; - - export class BaseClassWithoutCtor { - someUnrelatedProp = true; - } - - @Directive({selector: 'my-dir'}) - export class MyDirective extends BaseClassWithoutCtor {} - - @Pipe() - export class MyPipe extends BaseClassWithoutCtor {} - - @NgModule({declarations: [MyDirective, MyPipe]}) - export class AppModule {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')).toContain(dedent` - - export class BaseClassWithoutCtor { - someUnrelatedProp = true; - } - - @Directive({selector: 'my-dir'}) - export class MyDirective extends BaseClassWithoutCtor {} - - @Pipe() - export class MyPipe extends BaseClassWithoutCtor {}`); - }); - - it('should not decorate base class if directive/component/provider defines a constructor', - async () => { - writeFile('/index.ts', dedent` - import {Component, Injectable, NgModule, NgZone} from '@angular/core'; - - export class BaseClass { - constructor(zone: NgZone) {} - } - - export class BaseClass { - constructor(zone: NgZone) {} - } - - @Component({template: ''}) - export class MyComponent extends BaseClass { - constructor(zone: NgZone) { - super(zone); - } - } - - @Injectable() - export class MyService extends BaseClass { - constructor(zone: NgZone) { - super(zone); - } - } - - @NgModule({declarations: [MyComponent], providers: [MyService]}) - export class AppModule {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')).toContain(dedent` - - export class BaseClass { - constructor(zone: NgZone) {} - }`); - }); - - it('should not decorate base class if it already has decorator', async () => { - writeFile('/index.ts', dedent` - import {Component, Directive, NgModule, NgZone} from '@angular/core'; - - @Directive({selector: 'base-class'}) - export class BaseClass { - constructor(zone: NgZone) {} - } - - @Component({template: ''}) - export class MyComponent extends BaseClass {} - - @NgModule({declarations: [MyComponent]}) - export class AppModule {} - - @NgModule({declarations: [BaseClass]}) - export class LibModule {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')).toContain(dedent` - - @Directive({selector: 'base-class'}) - export class BaseClass {`); - }); - - it('should add a comment if the base class is declared through type definition', async () => { - writeFile('/node_modules/my-lib/package.json', JSON.stringify({ - version: '0.0.0', - main: './index.js', - typings: './index.d.ts', - })); - writeFile('/node_modules/my-lib/index.d.ts', ` - import {NgZone} from '@angular/core'; - - export declare class SuperBaseClass { - constructor(zone: NgZone); - } - `); - - writeFile('/index.ts', dedent` - import {Component, Injectable, NgModule} from '@angular/core'; - import {SuperBaseClass} from 'my-lib'; - - export class BaseClass extends SuperBaseClass {} - - export class BaseClass2 extends SuperBaseClass {} - - export class PassThroughClass extends BaseClass {} - - // should cause "BaseClass" to get a todo comment. - @Component({template: ''}) - export class MyComponent extends PassThroughClass {} - - // should cause "BaseClass2" to get a todo comment. - @Injectable() - export class MyService extends BaseClass2 {} - - // should cause "BaseClass" to get a todo comment. - @Component({template: ''}) - export class MyComponent2 extends BaseClass {} - - // should get a todo comment because there are no base classes - // in between. - @Component({template: ''}) - export class MyComponent3 extends SuperBaseClass {} - - @NgModule({declarations: [MyComponent, MyComponent2, MyComponent3], providers: [MyService]}) - export class MyModule {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')).toContain(dedent` - @Directive() - export class BaseClass extends SuperBaseClass { - // TODO: add explicit constructor - }`); - - expect(tree.readContent('/index.ts')).toContain(dedent` - @Injectable() - export class BaseClass2 extends SuperBaseClass { - // TODO: add explicit constructor - }`); - - expect(tree.readContent('/index.ts')).toContain(dedent` - @Directive() - export class PassThroughClass extends BaseClass {}`); - - expect(tree.readContent('/index.ts')).toContain(dedent` - @Component({template: ''}) - export class MyComponent extends PassThroughClass {}`); - - expect(tree.readContent('/index.ts')).toContain(dedent` - @Component({template: ''}) - export class MyComponent3 extends SuperBaseClass { - // TODO: add explicit constructor - }`); - - expect(tree.readContent('/index.ts')).toContain(dedent` - @Injectable() - export class MyService extends BaseClass2 {}`); - }); - - it('should not add a comment if the base class is declared through type definition but is' + - 'decorated', - async () => { - writeFakeLibrary(); - writeFile('/index.ts', dedent` - import {Component, NgModule} from '@angular/core'; - import {BaseComponent} from 'my-lib'; - - @Component({template: ''}) - export class MyComponent extends BaseComponent {} - - @NgModule({declarations: [MyComponent]}) - export class MyModule {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')).toContain(dedent` - @Component({template: ''}) - export class MyComponent extends BaseComponent {}`); - }); - - it('should not decorate base class in typings if it misses an explicit constructor', async () => { - writeFakeLibrary(); - writeFile('/index.ts', dedent` - import {Component, NgModule} from '@angular/core'; - import {BaseDirective} from 'my-lib'; - - @Component({template: ''}) - export class MyComponent extends BaseDirective {} - - @NgModule({declarations: [MyComponent]}) - export class MyModule {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')).toContain(dedent` - @Component({template: ''}) - export class MyComponent extends BaseDirective {}`); - expect(tree.readContent('/node_modules/my-lib/public-api.d.ts')).not.toContain('@Directive'); - }); - - it('should detect decorated classes by respecting summary files', async () => { - writeSummaryOnlyThirdPartyLibrary(); - - writeFile('/index.ts', dedent` - import {Component, NgModule} from '@angular/core'; - import {BaseComponent} from 'my-lib'; - - @Component({template: ''}) - export class MyComponent extends BaseComponent {} - - @NgModule({declarations: [MyComponent]}) - export class MyModule {} - `); - - await runMigration(); - - expect(warnOutput.length).toBe(0); - expect(errorOutput.length).toBe(0); - expect(tree.readContent('/index.ts')).toContain(dedent` - @Component({template: ''}) - export class MyComponent extends BaseComponent {}`); - }); - - it('should decorate all undecorated directives of inheritance chain', async () => { - writeFile('/index.ts', ` - import {Component, NgModule, NgZone} from '@angular/core'; - - export class SuperBaseClass { - constructor(zone: NgZone) {} - } - - export class BaseClass extends SuperBaseClass {} - - @Component({template: ''}) - export class MyComponent extends BaseClass {} - - @NgModule({declarations: [MyComponent]}) - export class MyModule {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')).toMatch(/@Directive\(\)\nexport class SuperBaseClass {/); - expect(tree.readContent('/index.ts')) - .toMatch(/}\s+@Directive\(\)\nexport class BaseClass extends SuperBaseClass {/); - }); - - it('should decorate all undecorated providers of inheritance chain', async () => { - writeFile('/index.ts', ` - import {Injectable, NgModule, NgZone} from '@angular/core'; - - export class SuperBaseClass { - constructor(zone: NgZone) {} - } - - export class BaseClass extends SuperBaseClass {} - - @Injectable() - export class MyService extends BaseClass {} - - @NgModule({providers: [MyService]}) - export class MyModule {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')).toMatch(/@Injectable\(\)\nexport class SuperBaseClass {/); - expect(tree.readContent('/index.ts')) - .toMatch(/}\s+@Injectable\(\)\nexport class BaseClass extends SuperBaseClass {/); - }); - - it('should properly update import if @Directive can be accessed through existing namespace import', - async () => { - writeFile('/index.ts', ` - import {Component, NgModule, NgZone} from '@angular/core'; - import {BaseClass} from './base'; - - @Component({template: ''}) - export class A extends BaseClass {} - - @NgModule({declarations: [A]}) - export class MyModule {} - `); - - writeFile('/base.ts', ` - import * as core from '@angular/core'; - - export class BaseClass { - constructor(zone: core.NgZone) {} - } - `); - - await runMigration(); - - expect(tree.readContent('/base.ts')).toMatch(/@core.Directive\(\)\nexport class BaseClass/); - }); - - it('should properly update existing import with aliased specifier if identifier is already used', - async () => { - writeFile('/index.ts', ` - import {Component, NgModule, NgZone} from '@angular/core'; - import {Directive} from './third_party_directive'; - - export class BaseClass { - constructor(zone: NgZone) {} - } - - @Component({template: ''}) - export class MyComponent extends BaseClass {} - - @NgModule({declarations: [MyComponent]}) - export class AppModule {} - `); - - await runMigration(); - - expect(tree.readContent(`/index.ts`)) - .toContain(`{ Component, NgModule, NgZone, Directive as Directive_1 }`); - expect(tree.readContent('/index.ts')).toMatch(/@Directive_1\(\)\nexport class BaseClass/); - }); - - it('should properly create new import with aliased specifier if identifier is already used', - async () => { - writeFile('/index.ts', ` - import {Component, NgModule, NgZone} from '@angular/core'; - import {BaseClass} from './base'; - - @Component({template: ''}) - export class A extends BaseClass {} - - @NgModule({declarations: [A]}) - export class MyModule {} - `); - - writeFile('/base.ts', ` - import {Directive} from './external'; - - export class MyService {} - - export class BaseClass { - constructor(zone: MyService) {} - } - `); - - await runMigration(); - - expect(tree.readContent('/base.ts')).toMatch(/@Directive_1\(\)\nexport class BaseClass/); - expect(tree.readContent(`/base.ts`)) - .toContain(`{ Directive as Directive_1 } from "@angular/core";`); - }); - - it('should use existing aliased import of @Directive instead of creating new import', - async () => { - writeFile('/index.ts', ` - import {Component, NgModule} from '@angular/core'; - import {BaseClass} from './base'; - - @Component({template: ''}) - export class A extends BaseClass {} - - @NgModule({declarations: [A]}) - export class MyModule {} - `); - - writeFile('/base.ts', ` - import {Directive as AliasedDir, NgZone} from '@angular/core'; - - export class BaseClass { - constructor(zone: NgZone) {} - } - `); - - await runMigration(); - - expect(tree.readContent('/base.ts')).toMatch(/@AliasedDir\(\)\nexport class BaseClass {/); - }); - - describe('decorator copying', async () => { - it('should be able to copy the "templateUrl" field', async () => { - writeFile('/index.ts', dedent` - import {NgModule} from '@angular/core'; - import {BaseClass} from './lib/base'; - - export class MyDir extends BaseClass {} - - @NgModule({declarations: [MyDir]}) - export class MyModule {} - `); - - writeFile('/lib/base.ts', dedent` - import {Directive, NgModule} from '@angular/core'; - - @Directive({ - selector: 'my-dir', - templateUrl: './my-dir.html', - }) - export class BaseClass {} - - @NgModule({declarations: [BaseClass]}) - export class LibModule {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`import { NgModule, Directive } from '@angular/core';`); - expect(tree.readContent('/index.ts')).toContain(dedent` - @Directive({ - selector: 'my-dir', - templateUrl: './my-dir.html' - }) - export class MyDir extends BaseClass {}`); - }); - - it('should be able to copy the "styleUrls" field', async () => { - writeFile('/index.ts', dedent` - import {NgModule} from '@angular/core'; - import {BaseClass} from './lib/base'; - - export class MyDir extends BaseClass {} - - @NgModule({declarations: [MyDir]}) - export class MyModule {} - `); - - writeFile('/lib/base.ts', dedent` - import {Directive, NgModule} from '@angular/core'; - - /** my comment */ - @Directive({ - selector: 'my-dir', - styleUrls: ['./my-dir.css'], - }) - export class BaseClass {} - - @NgModule({declarations: [BaseClass]}) - export class LibModule {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')).toContain(dedent` - import {BaseClass} from './lib/base'; - - @Directive({ - selector: 'my-dir', - styleUrls: ['./my-dir.css'] - }) - export class MyDir extends BaseClass {}`); - }); - - it('should be able to copy @Pipe decorator', async () => { - writeFile('/index.ts', dedent` - import {NgModule} from '@angular/core'; - import {BasePipe} from './lib/base'; - - export class MyPipe extends BasePipe {} - - @NgModule({declarations: [MyPipe]}) - export class MyModule {} - `); - - writeFile('/lib/base.ts', dedent` - import {Pipe, NgModule} from '@angular/core'; - - @Pipe({name: 'my-pipe-name'}) - export class BasePipe {} - - @NgModule({declarations: [BasePipe]}) - export class LibModule {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`import { NgModule, Pipe } from '@angular/core';`); - expect(tree.readContent('/index.ts')).toContain(dedent` - @Pipe({ name: 'my-pipe-name' }) - export class MyPipe extends BasePipe {}`); - }); - - it('should be able to copy decorator in same source file', async () => { - writeFile( - '/node_modules/@angular/cdk/table/index.d.ts', - `export declare const CDK_TABLE_TEMPLATE = '';`); - writeFile('/index.ts', dedent` - import {NgModule, Component} from '@angular/core'; - import {CDK_TABLE_TEMPLATE} from '@angular/cdk/table'; - - const A = 'hello'; - - @Component({ - selector: 'my-dir', - template: CDK_TABLE_TEMPLATE, - styles: [A], - }) - export class BaseClass {} - - export class MyDir extends BaseClass {} - - @NgModule({declarations: [BaseClass]}) - export class LibModule {} - - @NgModule({declarations: [MyDir]}) - export class MyModule {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')).toContain(dedent` - @Component({ - selector: 'my-dir', - template: CDK_TABLE_TEMPLATE, - styles: [A], - }) - export class MyDir extends BaseClass {}`); - }); - - it('should be able to create new imports for copied identifier references', async () => { - writeFile('/index.ts', dedent` - import {NgModule} from '@angular/core'; - import {BaseClass} from './lib/base'; - - export class MyDir extends BaseClass {} - - @NgModule({declarations: [[MyDir]]}) - export class MyModule {} - `); - - writeFile( - '/node_modules/@angular/cdk/table/index.d.ts', - `export declare const CDK_TABLE_TEMPLATE = '';`); - writeFile('/styles.ts', `export const STYLE_THROUGH_VAR = 'external';`); - writeFile('/lib/base.ts', dedent` - import {Component, NgModule} from '@angular/core'; - import {CDK_TABLE_TEMPLATE as tableTmpl} from '@angular/cdk/table'; - import {STYLE_THROUGH_VAR} from '../styles'; - - export const LOCAL_STYLE = 'local_style'; - - @Component({ - selector: 'my-dir', - template: tableTmpl, - styles: [STYLE_THROUGH_VAR, LOCAL_STYLE] - }) - export class BaseClass {} - - @NgModule({declarations: [BaseClass]}) - export class LibModule {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`import { CDK_TABLE_TEMPLATE } from "@angular/cdk/table";`); - expect(tree.readContent('/index.ts')) - .toContain(`import { STYLE_THROUGH_VAR } from "./styles";`); - expect(tree.readContent('/index.ts')) - .toContain(`import { BaseClass, LOCAL_STYLE } from './lib/base';`); - expect(tree.readContent('/index.ts')).toContain(dedent` - @Component({ - selector: 'my-dir', - template: CDK_TABLE_TEMPLATE, - styles: [STYLE_THROUGH_VAR, LOCAL_STYLE] - }) - export class MyDir extends BaseClass {}`); - }); - - it('should copy decorator once if directive is referenced multiple times', async () => { - writeFile('/index.ts', dedent` - import {NgModule} from '@angular/core'; - import {BaseClass} from './lib/base'; - - export class MyComp extends BaseClass {} - - @NgModule({entryComponents: [MyComp]}) - export class MyModule {} - `); - - writeFile('/second-module.ts', dedent` - import {NgModule, Directive} from '@angular/core'; - import {MyComp} from './index'; - - @Directive({selector: 'other-dir'}) - export class OtherDir {} - - @NgModule({declarations: [OtherDir, [MyComp]], entryComponents: [MyComp]}) - export class MySecondModule {} - `); - - writeFile('/lib/base.ts', dedent` - import {Component, NgModule} from '@angular/core'; - - @Component({ - selector: 'my-dir', - template: '', - }) - export class BaseClass {} - - @NgModule({declarations: [BaseClass]}) - export class LibModule {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')).toContain(dedent` - import {BaseClass} from './lib/base'; - - @Component({ - selector: 'my-dir', - template: '' - }) - export class MyComp extends BaseClass {}`); - }); - - it('should create aliased imports to avoid collisions for referenced identifiers', async () => { - writeFile('/index.ts', dedent` - import {NgModule} from '@angular/core'; - import {BaseClass} from './lib/base'; - - // this will conflict if "MY_TEMPLATE" from the base class is imported. The - // import to that export from base class should be aliased to avoid the collision. - const MY_TEMPLATE = ''; - - export class MyComp extends BaseClass {} - - @NgModule({declarations: [MyComp]}) - export class MyModule {} - `); - - writeFile('/lib/base.ts', dedent` - import {Component, NgModule} from '@angular/core'; - - export const MY_TEMPLATE = ''; - - @Component({ - selector: 'my-dir', - template: MY_TEMPLATE, - }) - export class BaseClass {} - - @NgModule({declarations: [BaseClass]}) - export class LibModule {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`import { BaseClass, MY_TEMPLATE as MY_TEMPLATE_1 } from './lib/base';`); - expect(tree.readContent('/index.ts')).toContain(dedent` - @Component({ - selector: 'my-dir', - template: MY_TEMPLATE_1 - }) - export class MyComp extends BaseClass {}`); - }); - - it('should add comment for metadata fields which cannot be copied', async () => { - writeFile('/index.ts', dedent` - import {NgModule} from '@angular/core'; - import {BaseClass} from './lib/base'; - - export class MyComp extends BaseClass {} - - @NgModule({declarations: [MyComp]}) - export class MyModule {} - `); - - writeFile('/lib/base.ts', dedent` - import {Component, NgModule, Document} from '@angular/core'; - - // this variable cannot be imported automatically. - const someProviders = [{provide: Document, useValue: null}] - - @Component({ - selector: 'my-dir', - template: '', - providers: [...someProviders], - }) - export class BaseClass {} - - @NgModule({declarations: [BaseClass]}) - export class LibModule {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')).toContain(dedent` - @Component({ - selector: 'my-dir', - template: '', - // The following fields were copied from the base class, - // but could not be updated automatically to work in the - // new file location. Please add any required imports for - // the properties below: - providers: [...someProviders] - }) - export class MyComp extends BaseClass {}`); - }); - - it('should add comment for metadata fields which are added through spread operator', - async () => { - writeFile('/index.ts', dedent` - import {NgModule} from '@angular/core'; - import {BaseClass} from './lib/base'; - - export class MyComp extends BaseClass {} - - @NgModule({declarations: [[MyComp]]}) - export class MyModule {} - `); - - writeFile('/lib/base.ts', dedent` - import {Component, NgModule} from '@angular/core'; - - export const metadataThroughVar = { - styleUrls: ['./test.css'], - } - - @Component({ - selector: 'my-dir', - template: '', - ...metadataThroughVar, - }) - export class BaseClass {} - - @NgModule({declarations: [BaseClass]}) - export class LibModule {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')).toContain(dedent` - @Component({ - selector: 'my-dir', - template: '', - // The following fields were copied from the base class, - // but could not be updated automatically to work in the - // new file location. Please add any required imports for - // the properties below: - ...metadataThroughVar - }) - export class MyComp extends BaseClass {}`); - }); - - it('should be able to copy fields specified through shorthand assignment', async () => { - writeFile('/hello.css', ''); - writeFile('/my-tmpl.html', ''); - writeFile('/index.ts', dedent` - import {NgModule} from '@angular/core'; - import {BaseClass} from './lib/base'; - - export class MyComp extends BaseClass {} - - @NgModule({declarations: [MyComp]}) - export class MyModule {} - `); - - writeFile('/lib/hello.css', ''); - writeFile('/lib/my-tmpl.html', ''); - writeFile('/lib/base.ts', dedent` - import {Component, NgModule} from '@angular/core'; - - export const host = {}; - export const templateUrl = './my-tmpl.html'; - const styleUrls = ["hello.css"]; - - @Component({ - selector: 'my-dir', - templateUrl, - styleUrls, - host, - }) - export class BaseClass {} - - @NgModule({declarations: [BaseClass]}) - export class LibModule {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`import { BaseClass, templateUrl, host } from './lib/base';`); - expect(tree.readContent('/index.ts')).toContain(dedent` - @Component({ - selector: 'my-dir', - templateUrl, - host, - // The following fields were copied from the base class, - // but could not be updated automatically to work in the - // new file location. Please add any required imports for - // the properties below: - styleUrls - }) - export class MyComp extends BaseClass {}`); - }); - - it('should serialize metadata from base class without source code', async () => { - writeFakeLibrary(); - - writeFile('/index.ts', dedent` - import {NgModule} from '@angular/core'; - import {BaseComponent, BasePipe} from 'my-lib'; - - export class PassThrough extends BaseComponent {} - - @NgModule({declarations: [PassThrough]}) - export class MyPassThroughMod {} - - export class MyComp extends PassThrough {} - - export class MyPipe extends BasePipe {} - - @NgModule({declarations: [MyComp, MyPipe]}) - export class MyModule {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain( - `import { NgModule, ChangeDetectionStrategy, ViewEncapsulation, NG_VALIDATORS, Component, Pipe } from '@angular/core';`); - expect(tree.readContent('/index.ts')).toContain(dedent` - @Component({ - changeDetection: ChangeDetectionStrategy.Default, - selector: "comp-selector", - template: "My Lib Component", - encapsulation: ViewEncapsulation.None, - providers: [{ - provide: NG_VALIDATORS, - useExisting: BaseComponent, - multi: true - }], - host: { - "[class.is-enabled]": "isEnabled === true" - } - }) - export class PassThrough extends BaseComponent {}`); - expect(tree.readContent('/index.ts')).toContain(dedent` - @Component({ - changeDetection: ChangeDetectionStrategy.Default, - selector: "comp-selector", - template: "My Lib Component", - encapsulation: ViewEncapsulation.None, - providers: [{ - provide: NG_VALIDATORS, - useExisting: BaseComponent, - multi: true - }], - host: { - "[class.is-enabled]": "isEnabled === true" - } - }) - export class MyComp extends PassThrough {}`); - expect(tree.readContent('/index.ts')).toContain(dedent` - @Pipe({ - pure: true, - name: "external-pipe-name" - }) - export class MyPipe extends BasePipe {}`); - }); - - it('should serialize metadata with external references from class without source code', async () => { - writeFakeLibrary({useImportedTemplate: true}); - writeFile( - '/node_modules/@angular/cdk/table/index.d.ts', - `export declare const CDK_TABLE_TEMPLATE = 'Template of CDK Table.';`); - writeFile('/index.ts', dedent` - import {NgModule} from '@angular/core'; - import {BaseComponent} from 'my-lib'; - - export class MyComp extends BaseComponent {} - - @NgModule({declarations: [MyComp]}) - export class MyModule {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain( - `import { NgModule, ChangeDetectionStrategy, ViewEncapsulation, NG_VALIDATORS, Component } from '@angular/core';`); - expect(tree.readContent('/index.ts')).toContain(dedent` - @Component({ - changeDetection: ChangeDetectionStrategy.Default, - selector: "comp-selector", - template: "Template of CDK Table.", - encapsulation: ViewEncapsulation.None, - providers: [{ - provide: NG_VALIDATORS, - useExisting: BaseComponent, - multi: true - }], - host: { - "[class.is-enabled]": "isEnabled === true" - } - }) - export class MyComp extends BaseComponent {}`); - }); - - it('should not throw if metadata from base class without source code is not serializable', - async () => { - writeFakeLibrary({insertInvalidReference: true}); - - writeFile('/index.ts', dedent` - import {NgModule} from '@angular/core'; - import {BaseComponent} from 'my-lib'; - - export class MyComp extends BaseComponent {} - - @NgModule({declarations: [MyComp]}) - export class MyModule {} - `); - - await runMigration(); - - expect(errorOutput.length).toBe(1); - expect(errorOutput[0]).toMatch(/Could not resolve non-existent/); - }); - - it('should not create imports for identifiers resolving to target source file', async () => { - writeFile('/index.ts', dedent` - import {NgModule} from '@angular/core'; - import {BaseClass} from './lib/base'; - - export const SHARED_TEMPLATE_URL = ''; - export const LOCAL_NAME = ''; - - export class MyDir extends BaseClass {} - - @NgModule({declarations: [MyDir]}) - export class MyModule {} - - export {LOCAL_NAME as PUBLIC_NAME}; - `); - - writeFile('/lib/base.ts', dedent` - import {Directive, NgModule} from '@angular/core'; - import {SHARED_TEMPLATE_URL, PUBLIC_NAME} from '..'; - - @Directive({ - selector: 'my-dir', - template: SHARED_TEMPLATE_URL, - styleUrls: [PUBLIC_NAME] - }) - export class BaseClass {} - - @NgModule({declarations: [BaseClass]}) - export class LibModule {} - `); - - await runMigration(); - - expect(tree.readContent('/index.ts')) - .toContain(`import { NgModule, Directive } from '@angular/core';`); - expect(tree.readContent('/index.ts')).toContain(dedent` - @Directive({ - selector: 'my-dir', - template: SHARED_TEMPLATE_URL, - styleUrls: [LOCAL_NAME] - }) - export class MyDir extends BaseClass {}`); - }); - }); - - function writeFakeLibrary(options?: { - insertInvalidReference?: boolean, - useImportedTemplate?: boolean, - }) { - writeFile('/node_modules/my-lib/package.json', JSON.stringify({ - name: 'my-lib', - version: '0.0.0', - main: './index.js', - typings: './index.d.ts', - })); - writeFile('/node_modules/my-lib/index.d.ts', `export * from './public-api';`); - writeFile('/node_modules/my-lib/public-api.d.ts', ` - import {NgZone} from '@angular/core'; - - export const testValidators: any; - export declare class BasePipe {} - export declare class BaseDirective {} - export declare class BaseComponent { - constructor(zone: NgZone); - } - `); - writeFile('/node_modules/my-lib/index.metadata.json', JSON.stringify({ - __symbolic: 'module', - version: 4, - metadata: { - MyLibModule: { - __symbolic: 'class', - decorators: [{ - __symbolic: 'call', - expression: { - __symbolic: 'reference', - module: '@angular/core', - name: 'NgModule', - }, - arguments: [{ - declarations: [ - {__symbolic: 'reference', name: 'BaseComponent'}, - {__symbolic: 'reference', name: 'BasePipe'} - ] - }], - }], - }, - BasePipe: { - __symbolic: 'class', - decorators: [{ - __symbolic: 'call', - expression: {__symbolic: 'reference', module: '@angular/core', name: 'Pipe'}, - arguments: [{name: 'external-pipe-name'}], - }] - }, - testValidators: { - 'provide': - {'__symbolic': 'reference', 'module': '@angular/core', 'name': 'NG_VALIDATORS'}, - 'useExisting': {'__symbolic': 'reference', 'name': 'BaseComponent'}, - 'multi': true - }, - BaseComponent: { - __symbolic: 'class', - decorators: [{ - __symbolic: 'call', - expression: { - __symbolic: 'reference', - module: '@angular/core', - name: 'Component', - }, - arguments: [{ - selector: 'comp-selector', - template: options && options.useImportedTemplate ? { - __symbolic: 'reference', - module: '@angular/cdk/table', - name: 'CDK_TABLE_TEMPLATE', - } : - `My Lib Component`, - encapsulation: { - __symbolic: 'select', - expression: { - __symbolic: 'reference', - module: options && options.insertInvalidReference ? 'non-existent' : - '@angular/core', - name: options && options.insertInvalidReference ? 'NonExistent' : - 'ViewEncapsulation', - }, - member: 'None' - }, - providers: [{__symbolic: 'reference', name: 'testValidators'}], - host: { - '[class.is-enabled]': 'isEnabled === true', - } - }] - }], - members: {} - }, - }, - origins: { - BaseComponent: './public-api', - }, - importAs: 'my-lib', - })); - } - - function writeSummaryOnlyThirdPartyLibrary() { - writeFile('/tsconfig.json', JSON.stringify({ - compilerOptions: { - lib: ['es2015'], - }, - angularCompilerOptions: { - generateCodeForLibraries: false, - allowEmptyCodegenFiles: true, - enableSummariesForJit: true, - } - })); - - writeFile('/node_modules/my-lib/package.json', JSON.stringify({ - name: 'my-lib', - version: '0.0.0', - main: './index.js', - typings: './index.d.ts', - })); - writeFile('/node_modules/my-lib/index.d.ts', `export * from './public-api';`); - writeFile('/node_modules/my-lib/public-api.d.ts', ` - import {NgZone} from '@angular/core'; - - export declare class BaseComponent { - constructor(zone: NgZone); - } - `); - - writeFile('/node_modules/my-lib/index.ngsummary.json', JSON.stringify({ - 'moduleName': null, - 'summaries': [ - {'symbol': {'__symbol': 0, 'members': []}, 'metadata': {'__symbol': 1, 'members': []}}, - ], - 'symbols': [ - {'__symbol': 0, 'name': 'BaseComponent', 'filePath': './index'}, - {'__symbol': 1, 'name': 'BaseComponent', 'filePath': './public-api'}, - ] - })); - - writeFile('/node_modules/my-lib/public-api.ngsummary.json', JSON.stringify({ - 'moduleName': null, - 'summaries': [{ - 'symbol': {'__symbol': 0, 'members': []}, - 'metadata': {'__symbolic': 'class', 'members': {}}, - 'type': { - 'summaryKind': 1, - 'type': { - 'reference': {'__symbol': 0, 'members': []}, - 'diDeps': [{ - 'isAttribute': false, - 'isHost': false, - 'isSelf': false, - 'isSkipSelf': false, - 'isOptional': false, - 'token': {'identifier': {'reference': {'__symbol': 4, 'members': []}}} - }], - 'lifecycleHooks': [] - }, - 'isComponent': false, - 'selector': 'button[cdkStepperNext]', - 'exportAs': null, - 'inputs': {'type': 'type'}, - 'outputs': {}, - 'hostListeners': {'click': '_handleClick()'}, - 'hostProperties': {'type': 'type'}, - 'hostAttributes': {}, - 'providers': [], - 'viewProviders': [], - 'queries': [], - 'guards': {}, - 'viewQueries': [], - 'entryComponents': [], - 'changeDetection': null, - 'template': null, - 'componentViewType': null, - 'rendererType': null, - 'componentFactory': null - } - }], - 'symbols': [{'__symbol': 0, 'name': 'BaseComponent', 'filePath': './public-api'}] - })); - } - - it('should not run for test tsconfig files', async () => { - writeFile('/src/tsconfig.spec.json', JSON.stringify({ - compilerOptions: { - lib: ['es2015'], - }, - files: ['./index.spec.ts'] - })); - - writeFile('/src/index.spec.ts', ` - // This imports "AppComponent" but *not* the actual module. Therefore - // the module is not part of the TypeScript project and NGC would error - // since the component is not part of any NgModule. This is way we can't - // create the Angular compiler program for test tsconfig files. - import {AppComponent} from './app.component'; - `); - - writeFile('/src/app.component.ts', ` - import {Component} from '@angular/core'; - - @Component({template: ''}) - export class AppComponent {} - `); - - writeFile('/src/app.module.ts', ` - import {NgModule} from '@angular/core'; - import {AppComponent} from './app.component'; - - @NgModule({declarations: [AppComponent]}) - export class AppModule {} - `); - - await runMigration(); - - // If the test project would run as part of the migration, there would be - // error messages because test projects are not guaranteed to always contain - // all source files. In this test it misses the "AppModule" which means that - // NGC would fail because the app component is not part of any module. - expect(warnOutput.length).toBe(0); - expect(errorOutput.length).toBe(0); - }); - - describe('diagnostics', async () => { - it('should gracefully exit migration if project fails with structural diagnostic', async () => { - writeFile('/index.ts', ` - import {Component, NgModule} from '@angular/core'; - - @Component({template: ''}) - export class TestComp {} - - @NgModule({declarations: [/* TestComp not added */]}) - export class MyModule {} - `); - - await runMigration(); - - expect(warnOutput.length).toBe(1); - expect(warnOutput[0]) - .toMatch( - /ensure there are no AOT compilation errors and rerun the migration. The following project failed: tsconfig\.json/); - expect(errorOutput.length).toBe(1); - expect(errorOutput[0]).toMatch(/Cannot determine the module for class TestComp/); - expect(infoOutput.join(' ')) - .toContain( - 'Some project targets could not be analyzed due to ' + - 'TypeScript program failures'); - }); - - it('should gracefully exit migration if project fails with syntactical diagnostic', - async () => { - writeFile('/index.ts', ` - import {Component, NgModule} /* missing "from" */ '@angular/core'; - `); - - await runMigration(); - - expect(warnOutput.length).toBe(1); - expect(warnOutput[0]) - .toMatch(/project "tsconfig.json" has syntactical errors which could cause/); - expect(errorOutput.length).toBe(1); - expect(errorOutput[0]).toMatch(/error TS1005: 'from' expected/); - expect(infoOutput.join(' ')) - .toContain( - 'Some project targets could not be analyzed due to ' + - 'TypeScript program failures'); - }); - - // Regression test for: https://github.com/angular/angular/issues/34985. - it('should be able to migrate libraries with multiple source files and flat-module ' + - 'options set', - async () => { - writeFile('/tsconfig.json', JSON.stringify({ - compilerOptions: { - lib: ['es2015'], - }, - angularCompilerOptions: - {flatModuleId: 'AUTOGENERATED', flatModuleOutFile: 'AUTOGENERATED'} - })); - - // This file doesn't do anything, but it's necessary in order to hit the code path for - // the assertion. As of TS 4.2 it needs to have _some_ kind of content, otherwise the - // compiler will throw an error. - writeFile('/second.ts', `export const foo = 1;`); - writeFile('/test.ts', ` - import {Injectable, NgModule, NgZone} from '@angular/core'; - - export class BaseClass { - constructor(zone: NgZone) {} - } - - @Injectable({template: ''}) - export class MyService extends BaseClass {} - - @NgModule({providers: [MyService]}) - export class AppModule {} - `); - - await runMigration(); - - expect(errorOutput.length).toBe(0); - expect(warnOutput.length).toBe(0); - expect(tree.readContent('/test.ts')).toMatch(/@Injectable\(\)\nexport class BaseClass {/); - }); - - it('should not throw if resources could not be read', async () => { - writeFile('/index.ts', ` - import {Component, NgModule} from '@angular/core'; - - @Component({ - templateUrl: './my-template.pug', - styleUrls: ["./test.scss", "./some-special-file.custom"], - }) - export class TestComp {} - - @NgModule({declarations: [TestComp]}) - export class MyModule {} - `); - - writeFile('/test.scss', `@import '~theme.scss';`); - - await runMigration(); - - expect(warnOutput.length).toBe(0); - expect(errorOutput.length).toBe(0); - }); - - it('should not throw if tsconfig references non-existent source file', async () => { - writeFile('/tsconfig.json', JSON.stringify({ - compilerOptions: { - lib: ['es2015'], - }, - files: [ - './non-existent.ts', - ] - })); - - let failed = false; - try { - await runMigration(); - } catch (e) { - failed = true; - } - - expect(failed).toBe(false, 'Expected the migration not to fail.'); - expect(warnOutput.length).toBe(1); - expect(errorOutput.length).toBe(1); - expect(warnOutput[0]) - .toContain( - 'TypeScript project "tsconfig.json" has configuration errors. This could cause an ' + - 'incomplete migration. Please fix the following failures and rerun the migration:'); - expect(errorOutput[0]).toMatch(/non-existent\.ts' not found/); - }); - }); -}); diff --git a/packages/core/schematics/test/wait_for_async_migration_spec.ts b/packages/core/schematics/test/wait_for_async_migration_spec.ts deleted file mode 100644 index 58ea9ba8ba4..00000000000 --- a/packages/core/schematics/test/wait_for_async_migration_spec.ts +++ /dev/null @@ -1,162 +0,0 @@ -/** - * @license - * Copyright Google LLC 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('waitForAsync migration', () => { - let runner: SchematicTestRunner; - let host: TempScopedNodeJsSyncHost; - let tree: UnitTestTree; - let tmpDirPath: string; - let previousWorkingDir: string; - - beforeEach(() => { - runner = new SchematicTestRunner('test', require.resolve('../migrations.json')); - host = new TempScopedNodeJsSyncHost(); - tree = new UnitTestTree(new HostTree(host)); - - writeFile('/tsconfig.json', JSON.stringify({ - compilerOptions: { - lib: ['es2015'], - strictNullChecks: true, - }, - })); - writeFile('/angular.json', JSON.stringify({ - version: 1, - projects: {t: {architect: {build: {options: {tsConfig: './tsconfig.json'}}}}} - })); - // We need to declare the Angular symbols we're testing for, otherwise type checking won't work. - writeFile('/node_modules/@angular/core/testing/index.d.ts', ` - export declare function async(fn: Function): any; - `); - - 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); - }); - - it('should change async imports to waitForAsync', async () => { - writeFile('/index.ts', ` - import { async, inject } from '@angular/core/testing'; - - it('should work', async(() => { - expect(inject('foo')).toBe('foo'); - })); - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`import { inject, waitForAsync } from '@angular/core/testing';`); - }); - - it('should change aliased async imports to waitForAsync', async () => { - writeFile('/index.ts', ` - import { async as renamedAsync, inject } from '@angular/core/testing'; - - it('should work', renamedAsync(() => { - expect(inject('foo')).toBe('foo'); - })); - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`import { inject, waitForAsync as renamedAsync } from '@angular/core/testing';`); - }); - - it('should not change async imports if they are not from @angular/core/testing', async () => { - writeFile('/index.ts', ` - import { inject } from '@angular/core/testing'; - import { async } from './my-test-library'; - - it('should work', async(() => { - expect(inject('foo')).toBe('foo'); - })); - `); - - await runMigration(); - const content = tree.readContent('/index.ts'); - expect(content).toContain(`import { inject } from '@angular/core/testing';`); - expect(content).toContain(`import { async } from './my-test-library';`); - }); - - it('should not change imports if waitForAsync was already imported', async () => { - writeFile('/index.ts', ` - import { async, inject, waitForAsync } from '@angular/core/testing'; - - it('should work', async(() => { - expect(inject('foo')).toBe('foo'); - })); - - it('should also work', waitForAsync(() => { - expect(inject('bar')).toBe('bar'); - })); - `); - - await runMigration(); - expect(tree.readContent('/index.ts')) - .toContain(`import { async, inject, waitForAsync } from '@angular/core/testing';`); - }); - - it('should change calls from `async` to `waitForAsync`', async () => { - writeFile('/index.ts', ` - import { async, inject } from '@angular/core/testing'; - - it('should work', async(() => { - expect(inject('foo')).toBe('foo'); - })); - - it('should also work', async(() => { - expect(inject('bar')).toBe('bar'); - })); - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain(`import { inject, waitForAsync } from '@angular/core/testing';`); - expect(content).toContain(`it('should work', waitForAsync(() => {`); - expect(content).toContain(`it('should also work', waitForAsync(() => {`); - }); - - it('should not change aliased calls', async () => { - writeFile('/index.ts', ` - import { async as renamedAsync, inject } from '@angular/core/testing'; - - it('should work', renamedAsync(() => { - expect(inject('foo')).toBe('foo'); - })); - `); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain( - `import { inject, waitForAsync as renamedAsync } from '@angular/core/testing';`); - expect(content).toContain(`it('should work', renamedAsync(() => {`); - }); - - function writeFile(filePath: string, contents: string) { - host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); - } - - function runMigration() { - return runner.runSchematicAsync('migration-v11-wait-for-async', {}, tree).toPromise(); - } -}); diff --git a/packages/core/schematics/test/xhr_factory_spec.ts b/packages/core/schematics/test/xhr_factory_spec.ts deleted file mode 100644 index ced959648a1..00000000000 --- a/packages/core/schematics/test/xhr_factory_spec.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 {tags} from '@angular-devkit/core'; -import {EmptyTree} from '@angular-devkit/schematics'; -import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing'; - -describe('XhrFactory migration', () => { - let tree: UnitTestTree; - const runner = new SchematicTestRunner('test', require.resolve('../migrations.json')); - - beforeEach(() => { - tree = new UnitTestTree(new EmptyTree()); - }); - - it(`should replace 'XhrFactory' from '@angular/common/http' to '@angular/common'`, async () => { - tree.create('/index.ts', tags.stripIndents` - import { HttpClient } from '@angular/common'; - import { HttpErrorResponse, HttpResponse, XhrFactory } from '@angular/common/http'; - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toBe(tags.stripIndents` - import { HttpClient, XhrFactory } from '@angular/common'; - import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; - `); - }); - - it(`should replace import for 'XhrFactory' to '@angular/common'`, async () => { - tree.create('/index.ts', tags.stripIndents` - import { Injecable } from '@angular/core'; - import { XhrFactory } from '@angular/common/http'; - import { BrowserModule } from '@angular/platform-browser'; - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toBe(tags.stripIndents` - import { Injecable } from '@angular/core'; - import { XhrFactory } from '@angular/common'; - import { BrowserModule } from '@angular/platform-browser'; - `); - }); - - it(`should remove http import when 'XhrFactory' is the only imported symbol`, async () => { - tree.create('/index.ts', tags.stripIndents` - import { HttpClient } from '@angular/common'; - import { XhrFactory as XhrFactory2 } from '@angular/common/http'; - import { Injecable } from '@angular/core'; - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toBe(tags.stripIndents` - import { HttpClient, XhrFactory as XhrFactory2 } from '@angular/common'; - import { Injecable } from '@angular/core'; - `); - }); - - it(`should add named import when '@angular/common' is a namespace import`, async () => { - tree.create('/index.ts', tags.stripIndents` - import * as common from '@angular/common'; - import { XhrFactory } from '@angular/common/http'; - `); - - await runMigration(); - expect(tree.readContent('/index.ts')).toBe(tags.stripIndents` - import * as common from '@angular/common'; - import { XhrFactory } from '@angular/common'; - `); - }); - - async function runMigration(): Promise { - await runner.runSchematicAsync('migration-v12-xhr-factory', {}, tree).toPromise(); - } -});