From 0e733f3689a47ca5cb13f4a0fdd543e963db2fbd Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 17 Sep 2020 15:12:08 +0200 Subject: [PATCH] feat(core): add automated migration to replace ViewEncapsulation.Native (#38882) Adds an automated migration that replaces any usages of the deprecated `ViewEncapsulation.Native` with `ViewEncapsulation.ShadowDom`. PR Close #38882 --- packages/core/schematics/BUILD.bazel | 1 + packages/core/schematics/migrations.json | 5 + .../native-view-encapsulation/BUILD.bazel | 18 ++ .../native-view-encapsulation/README.md | 34 ++++ .../native-view-encapsulation/index.ts | 52 ++++++ .../native-view-encapsulation/util.ts | 38 ++++ packages/core/schematics/test/BUILD.bazel | 1 + ...ative_view_encapsulation_migration_spec.ts | 169 ++++++++++++++++++ 8 files changed, 318 insertions(+) create mode 100644 packages/core/schematics/migrations/native-view-encapsulation/BUILD.bazel create mode 100644 packages/core/schematics/migrations/native-view-encapsulation/README.md create mode 100644 packages/core/schematics/migrations/native-view-encapsulation/index.ts create mode 100644 packages/core/schematics/migrations/native-view-encapsulation/util.ts create mode 100644 packages/core/schematics/test/native_view_encapsulation_migration_spec.ts diff --git a/packages/core/schematics/BUILD.bazel b/packages/core/schematics/BUILD.bazel index 9d665c5763..bcf0d6ecb3 100644 --- a/packages/core/schematics/BUILD.bazel +++ b/packages/core/schematics/BUILD.bazel @@ -15,6 +15,7 @@ pkg_npm( "//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", diff --git a/packages/core/schematics/migrations.json b/packages/core/schematics/migrations.json index 4e5191af2a..2ba3e20101 100644 --- a/packages/core/schematics/migrations.json +++ b/packages/core/schematics/migrations.json @@ -59,6 +59,11 @@ "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" } } } diff --git a/packages/core/schematics/migrations/native-view-encapsulation/BUILD.bazel b/packages/core/schematics/migrations/native-view-encapsulation/BUILD.bazel new file mode 100644 index 0000000000..4c08ab63bf --- /dev/null +++ b/packages/core/schematics/migrations/native-view-encapsulation/BUILD.bazel @@ -0,0 +1,18 @@ +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 new file mode 100644 index 0000000000..0a9ab191a0 --- /dev/null +++ b/packages/core/schematics/migrations/native-view-encapsulation/README.md @@ -0,0 +1,34 @@ +## `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 new file mode 100644 index 0000000000..766e8c4ca2 --- /dev/null +++ b/packages/core/schematics/migrations/native-view-encapsulation/index.ts @@ -0,0 +1,52 @@ +/** + * @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 {createMigrationProgram} from '../../utils/typescript/compiler_host'; +import {findNativeEncapsulationNodes} from './util'; + + +/** Migration that switches from `ViewEncapsulation.Native` to `ViewEncapsulation.ShadowDom`. */ +export default function(): Rule { + return (tree: Tree) => { + const {buildPaths, testPaths} = 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( + f => !f.isDeclarationFile && !program.isSourceFileFromExternalLibrary(f)); + + 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 new file mode 100644 index 0000000000..c7188b01d7 --- /dev/null +++ b/packages/core/schematics/migrations/native-view-encapsulation/util.ts @@ -0,0 +1,38 @@ +/** + * @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 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/test/BUILD.bazel b/packages/core/schematics/test/BUILD.bazel index a8b958bdd7..0a8fd5d13d 100644 --- a/packages/core/schematics/test/BUILD.bazel +++ b/packages/core/schematics/test/BUILD.bazel @@ -13,6 +13,7 @@ ts_library( "//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", diff --git a/packages/core/schematics/test/native_view_encapsulation_migration_spec.ts b/packages/core/schematics/test/native_view_encapsulation_migration_spec.ts new file mode 100644 index 0000000000..e3cdd6a24e --- /dev/null +++ b/packages/core/schematics/test/native_view_encapsulation_migration_spec.ts @@ -0,0 +1,169 @@ +/** + * @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({ + 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(); + } +});