Sitelet https://github.com/angular/angular/commit/0e733f3
Skip to content

Commit 0e733f3

Browse files
crisbetoatscott
authored andcommitted
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
1 parent 0a16e60 commit 0e733f3

8 files changed

Lines changed: 318 additions & 0 deletions

File tree

‎packages/core/schematics/BUILD.bazel‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pkg_npm(
1515
"//packages/core/schematics/migrations/missing-injectable",
1616
"//packages/core/schematics/migrations/module-with-providers",
1717
"//packages/core/schematics/migrations/move-document",
18+
"//packages/core/schematics/migrations/native-view-encapsulation",
1819
"//packages/core/schematics/migrations/navigation-extras-omissions",
1920
"//packages/core/schematics/migrations/relative-link-resolution",
2021
"//packages/core/schematics/migrations/renderer-to-renderer2",

‎packages/core/schematics/migrations.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@
5959
"version": "11.0.0-beta",
6060
"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`.",
6161
"factory": "./migrations/abstract-control-parent/index"
62+
},
63+
"migration-v11-native-view-encapsulation": {
64+
"version": "11.0.0-beta",
65+
"description": "ViewEncapsulation.Native has been removed as of Angular version 11. This migration replaces any usages with ViewEncapsulation.ShadowDom.",
66+
"factory": "./migrations/native-view-encapsulation/index"
6267
}
6368
}
6469
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
load("//tools:defaults.bzl", "ts_library")
2+
3+
ts_library(
4+
name = "native-view-encapsulation",
5+
srcs = glob(["**/*.ts"]),
6+
tsconfig = "//packages/core/schematics:tsconfig.json",
7+
visibility = [
8+
"//packages/core/schematics:__pkg__",
9+
"//packages/core/schematics/migrations/google3:__pkg__",
10+
"//packages/core/schematics/test:__pkg__",
11+
],
12+
deps = [
13+
"//packages/core/schematics/utils",
14+
"@npm//@angular-devkit/schematics",
15+
"@npm//@types/node",
16+
"@npm//typescript",
17+
],
18+
)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
## `ViewEncapsulation.Native` migration
2+
3+
Automatically migrates usages of `ViewEncapsulation.Native` to `ViewEncapsulation.ShadowDom`.
4+
For most practical purposes the `Native` mode is compatible with the `ShadowDom` mode.
5+
6+
The migration covers any reference to the `Native` value that can be traced to `@angular/core`.
7+
Some examples:
8+
* Inside the `encapsulation` property of `Component` decorators.
9+
* In property assignments for the `COMPILER_OPTIONS` provider.
10+
* In variables.
11+
12+
#### Before
13+
```ts
14+
import { Component, ViewEncapsulation } from '@angular/core';
15+
16+
@Component({
17+
template: '...',
18+
encapsulation: ViewEncapsulation.Native
19+
})
20+
export class App {
21+
}
22+
```
23+
24+
#### After
25+
```ts
26+
import { Component, ViewEncapsulation } from '@angular/core';
27+
28+
@Component({
29+
template: '...',
30+
encapsulation: ViewEncapsulation.ShadowDom
31+
})
32+
export class App {
33+
}
34+
```
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Rule, SchematicsException, Tree} from '@angular-devkit/schematics';
10+
import {relative} from 'path';
11+
12+
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
13+
import {createMigrationProgram} from '../../utils/typescript/compiler_host';
14+
import {findNativeEncapsulationNodes} from './util';
15+
16+
17+
/** Migration that switches from `ViewEncapsulation.Native` to `ViewEncapsulation.ShadowDom`. */
18+
export default function(): Rule {
19+
return (tree: Tree) => {
20+
const {buildPaths, testPaths} = getProjectTsConfigPaths(tree);
21+
const basePath = process.cwd();
22+
const allPaths = [...buildPaths, ...testPaths];
23+
24+
if (!allPaths.length) {
25+
throw new SchematicsException(
26+
'Could not find any tsconfig file. Cannot migrate away from Native view encapsulation.');
27+
}
28+
29+
for (const tsconfigPath of allPaths) {
30+
runNativeViewEncapsulationMigration(tree, tsconfigPath, basePath);
31+
}
32+
};
33+
}
34+
35+
function runNativeViewEncapsulationMigration(tree: Tree, tsconfigPath: string, basePath: string) {
36+
const {program} = createMigrationProgram(tree, tsconfigPath, basePath);
37+
const typeChecker = program.getTypeChecker();
38+
const sourceFiles = program.getSourceFiles().filter(
39+
f => !f.isDeclarationFile && !program.isSourceFileFromExternalLibrary(f));
40+
41+
sourceFiles.forEach(sourceFile => {
42+
const update = tree.beginUpdate(relative(basePath, sourceFile.fileName));
43+
const identifiers = findNativeEncapsulationNodes(typeChecker, sourceFile);
44+
45+
identifiers.forEach(node => {
46+
update.remove(node.getStart(), node.getWidth());
47+
update.insertRight(node.getStart(), 'ShadowDom');
48+
});
49+
50+
tree.commitUpdate(update);
51+
});
52+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import * as ts from 'typescript';
10+
11+
import {getImportOfIdentifier} from '../../utils/typescript/imports';
12+
13+
/** Finds all the Identifier nodes in a file that refer to `Native` view encapsulation. */
14+
export function findNativeEncapsulationNodes(
15+
typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile): Set<ts.Identifier> {
16+
const results = new Set<ts.Identifier>();
17+
18+
sourceFile.forEachChild(function walkNode(node: ts.Node) {
19+
// Note that we look directly for nodes in the form of `<something>.Native`, rather than going
20+
// for `Component` class decorators, because it's much simpler and it allows us to handle cases
21+
// where `ViewEncapsulation.Native` might be used in a different context (e.g. a variable).
22+
// Using the encapsulation outside of a decorator is an edge case, but we do have public APIs
23+
// where it can be passed in (see the `defaultViewEncapsulation` property on the
24+
// `COMPILER_OPTIONS` provider).
25+
if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.name) &&
26+
node.name.text === 'Native' && ts.isIdentifier(node.expression)) {
27+
const expressionImport = getImportOfIdentifier(typeChecker, node.expression);
28+
if (expressionImport && expressionImport.name === 'ViewEncapsulation' &&
29+
expressionImport.importModule === '@angular/core') {
30+
results.add(node.name);
31+
}
32+
} else {
33+
node.forEachChild(walkNode);
34+
}
35+
});
36+
37+
return results;
38+
}

‎packages/core/schematics/test/BUILD.bazel‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ ts_library(
1313
"//packages/core/schematics/migrations/missing-injectable",
1414
"//packages/core/schematics/migrations/module-with-providers",
1515
"//packages/core/schematics/migrations/move-document",
16+
"//packages/core/schematics/migrations/native-view-encapsulation",
1617
"//packages/core/schematics/migrations/navigation-extras-omissions",
1718
"//packages/core/schematics/migrations/relative-link-resolution",
1819
"//packages/core/schematics/migrations/renderer-to-renderer2",
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {getSystemPath, normalize, virtualFs} from '@angular-devkit/core';
10+
import {TempScopedNodeJsSyncHost} from '@angular-devkit/core/node/testing';
11+
import {HostTree} from '@angular-devkit/schematics';
12+
import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing';
13+
import * as shx from 'shelljs';
14+
15+
describe('ViewEncapsulation.Native migration', () => {
16+
let runner: SchematicTestRunner;
17+
let host: TempScopedNodeJsSyncHost;
18+
let tree: UnitTestTree;
19+
let tmpDirPath: string;
20+
let previousWorkingDir: string;
21+
22+
beforeEach(() => {
23+
runner = new SchematicTestRunner('test', require.resolve('../migrations.json'));
24+
host = new TempScopedNodeJsSyncHost();
25+
tree = new UnitTestTree(new HostTree(host));
26+
27+
writeFile('/tsconfig.json', JSON.stringify({
28+
compilerOptions: {
29+
lib: ['es2015'],
30+
strictNullChecks: true,
31+
},
32+
}));
33+
writeFile('/angular.json', JSON.stringify({
34+
projects: {t: {architect: {build: {options: {tsConfig: './tsconfig.json'}}}}}
35+
}));
36+
37+
previousWorkingDir = shx.pwd();
38+
tmpDirPath = getSystemPath(host.root);
39+
40+
// Switch into the temporary directory path. This allows us to run
41+
// the schematic against our custom unit test tree.
42+
shx.cd(tmpDirPath);
43+
});
44+
45+
afterEach(() => {
46+
shx.cd(previousWorkingDir);
47+
shx.rm('-r', tmpDirPath);
48+
});
49+
50+
it('should change Native view encapsulation usages to ShadowDom', async () => {
51+
writeFile('/index.ts', `
52+
import {Component, ViewEncapsulation} from '@angular/core';
53+
54+
@Component({
55+
template: 'hello',
56+
encapsulation: ViewEncapsulation.Native
57+
})
58+
class App {}
59+
`);
60+
61+
await runMigration();
62+
63+
const content = tree.readContent('/index.ts');
64+
expect(content).toContain('encapsulation: ViewEncapsulation.ShadowDom');
65+
});
66+
67+
it('should change Native view encapsulation usages if the enum is aliased', async () => {
68+
writeFile('/index.ts', `
69+
import {Component, ViewEncapsulation as Encapsulation} from '@angular/core';
70+
71+
@Component({
72+
template: 'hello',
73+
encapsulation: Encapsulation.Native
74+
})
75+
class App {}
76+
`);
77+
78+
await runMigration();
79+
80+
const content = tree.readContent('/index.ts');
81+
expect(content).toContain('encapsulation: Encapsulation.ShadowDom');
82+
});
83+
84+
it('should change Native view encapsulation usages inside a variable', async () => {
85+
writeFile('/index.ts', `
86+
import {Component, ViewEncapsulation} from '@angular/core';
87+
88+
const encapsulation = ViewEncapsulation.Native;
89+
90+
@Component({template: 'hello', encapsulation})
91+
class App {}
92+
93+
@Component({template: 'click me', encapsulation})
94+
class Button {}
95+
`);
96+
97+
await runMigration();
98+
99+
const content = tree.readContent('/index.ts');
100+
expect(content).toContain('const encapsulation = ViewEncapsulation.ShadowDom;');
101+
});
102+
103+
it('should not change components that do not set an encapsulation', async () => {
104+
const source = `
105+
import {Component} from '@angular/core';
106+
107+
@Component({
108+
template: 'hello'
109+
})
110+
class App {}
111+
`;
112+
113+
writeFile('/index.ts', source);
114+
115+
await runMigration();
116+
117+
const content = tree.readContent('/index.ts');
118+
expect(content).toBe(source);
119+
});
120+
121+
it('should not change components that use an encapsulation different from Native', async () => {
122+
const source = `
123+
import {Component, ViewEncapsulation} from '@angular/core';
124+
125+
@Component({
126+
template: 'hello',
127+
encapsulation: ViewEncapsulation.None
128+
})
129+
class App {}
130+
`;
131+
132+
writeFile('/index.ts', source);
133+
134+
await runMigration();
135+
136+
const content = tree.readContent('/index.ts');
137+
expect(content).toBe(source);
138+
});
139+
140+
it('should not change cases where ViewEncapsulation does not come from @angular/core',
141+
async () => {
142+
const source = `
143+
import {Component} from '@angular/core';
144+
import {ViewEncapsulation} from '@not-angular/core';
145+
146+
@Component({
147+
template: 'hello',
148+
encapsulation: ViewEncapsulation.Native
149+
})
150+
class App {}
151+
`;
152+
153+
writeFile('/index.ts', source);
154+
155+
await runMigration();
156+
157+
const content = tree.readContent('/index.ts');
158+
expect(content).toBe(source);
159+
});
160+
161+
function writeFile(filePath: string, contents: string) {
162+
host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents));
163+
}
164+
165+
function runMigration() {
166+
return runner.runSchematicAsync('migration-v11-native-view-encapsulation', {}, tree)
167+
.toPromise();
168+
}
169+
});

0 commit comments

Comments
 (0)