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