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
This commit is contained in:
		
							parent
							
								
									0a16e60afa
								
							
						
					
					
						commit
						0e733f3689
					
				| @ -15,6 +15,7 @@ pkg_npm( | |||||||
|         "//packages/core/schematics/migrations/missing-injectable", |         "//packages/core/schematics/migrations/missing-injectable", | ||||||
|         "//packages/core/schematics/migrations/module-with-providers", |         "//packages/core/schematics/migrations/module-with-providers", | ||||||
|         "//packages/core/schematics/migrations/move-document", |         "//packages/core/schematics/migrations/move-document", | ||||||
|  |         "//packages/core/schematics/migrations/native-view-encapsulation", | ||||||
|         "//packages/core/schematics/migrations/navigation-extras-omissions", |         "//packages/core/schematics/migrations/navigation-extras-omissions", | ||||||
|         "//packages/core/schematics/migrations/relative-link-resolution", |         "//packages/core/schematics/migrations/relative-link-resolution", | ||||||
|         "//packages/core/schematics/migrations/renderer-to-renderer2", |         "//packages/core/schematics/migrations/renderer-to-renderer2", | ||||||
|  | |||||||
| @ -59,6 +59,11 @@ | |||||||
|       "version": "11.0.0-beta", |       "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`.", |       "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" |       "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" | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -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", | ||||||
|  |     ], | ||||||
|  | ) | ||||||
| @ -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 { | ||||||
|  | } | ||||||
|  | ``` | ||||||
| @ -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); | ||||||
|  |   }); | ||||||
|  | } | ||||||
| @ -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<ts.Identifier> { | ||||||
|  |   const results = new Set<ts.Identifier>(); | ||||||
|  | 
 | ||||||
|  |   sourceFile.forEachChild(function walkNode(node: ts.Node) { | ||||||
|  |     // Note that we look directly for nodes in the form of `<something>.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; | ||||||
|  | } | ||||||
| @ -13,6 +13,7 @@ ts_library( | |||||||
|         "//packages/core/schematics/migrations/missing-injectable", |         "//packages/core/schematics/migrations/missing-injectable", | ||||||
|         "//packages/core/schematics/migrations/module-with-providers", |         "//packages/core/schematics/migrations/module-with-providers", | ||||||
|         "//packages/core/schematics/migrations/move-document", |         "//packages/core/schematics/migrations/move-document", | ||||||
|  |         "//packages/core/schematics/migrations/native-view-encapsulation", | ||||||
|         "//packages/core/schematics/migrations/navigation-extras-omissions", |         "//packages/core/schematics/migrations/navigation-extras-omissions", | ||||||
|         "//packages/core/schematics/migrations/relative-link-resolution", |         "//packages/core/schematics/migrations/relative-link-resolution", | ||||||
|         "//packages/core/schematics/migrations/renderer-to-renderer2", |         "//packages/core/schematics/migrations/renderer-to-renderer2", | ||||||
|  | |||||||
| @ -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(); | ||||||
|  |   } | ||||||
|  | }); | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user