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:
Kristiyan Kostadinov 2020-09-17 15:12:08 +02:00 committed by atscott
parent 0a16e60afa
commit 0e733f3689
8 changed files with 318 additions and 0 deletions

View File

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

View File

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

View File

@ -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",
],
)

View File

@ -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 {
}
```

View File

@ -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);
});
}

View File

@ -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;
}

View File

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

View File

@ -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();
}
});