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/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", | ||||
|  | ||||
| @ -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" | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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/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", | ||||
|  | ||||
| @ -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