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…
Reference in New Issue