Skip to content

Commit 6ea8e1e

Browse files
JeanMechethePunderWoman
authored andcommitted
feat(core): Add a schematics to migrate to standalone: false. (#57643)
With the framework enabling `standalone` by default (making module based an opt-in), the migration will migrate none-standalone existing components and add `standalone: false` to the decorator. PR Close #57643
1 parent 9624ab0 commit 6ea8e1e

File tree

6 files changed

+388
-1
lines changed

6 files changed

+388
-1
lines changed

packages/core/schematics/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ rollup_bundle(
3333
"//packages/core/schematics/ng-generate/inject-migration:index.ts": "inject-migration",
3434
"//packages/core/schematics/ng-generate/route-lazy-loading:index.ts": "route-lazy-loading",
3535
"//packages/core/schematics/ng-generate/standalone-migration:index.ts": "standalone-migration",
36+
"//packages/core/schematics/migrations/explicit-standalone-flag:index.ts": "explicit-standalone-flag",
3637
},
3738
format = "cjs",
3839
link_workspace_root = True,
@@ -42,6 +43,7 @@ rollup_bundle(
4243
"//packages/core/schematics/test:__pkg__",
4344
],
4445
deps = [
46+
"//packages/core/schematics/migrations/explicit-standalone-flag",
4547
"//packages/core/schematics/ng-generate/control-flow-migration",
4648
"//packages/core/schematics/ng-generate/inject-migration",
4749
"//packages/core/schematics/ng-generate/route-lazy-loading",
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
{
2-
"schematics": {}
2+
"schematics": {
3+
"explicit-standalone-flag": {
4+
"version": "19.0.0",
5+
"description": "Updates non-standalone Directives, Component and Pipes to standalone:false",
6+
"factory": "./bundles/explicit-standalone-flag#migrate"
7+
}
8+
}
39
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
load("//tools:defaults.bzl", "ts_library")
2+
3+
package(
4+
default_visibility = [
5+
"//packages/core/schematics:__pkg__",
6+
"//packages/core/schematics/migrations/google3:__pkg__",
7+
"//packages/core/schematics/test:__pkg__",
8+
],
9+
)
10+
11+
ts_library(
12+
name = "explicit-standalone-flag",
13+
srcs = glob(["**/*.ts"]),
14+
tsconfig = "//packages/core/schematics:tsconfig.json",
15+
deps = [
16+
"//packages/core/schematics/utils",
17+
"@npm//@angular-devkit/schematics",
18+
"@npm//@types/node",
19+
"@npm//typescript",
20+
],
21+
)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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, UpdateRecorder} from '@angular-devkit/schematics';
10+
import {relative} from 'path';
11+
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
12+
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host';
13+
import {migrateFile} from './migration';
14+
15+
export function migrate(): Rule {
16+
return async (tree: Tree) => {
17+
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
18+
const basePath = process.cwd();
19+
const allPaths = [...buildPaths, ...testPaths];
20+
21+
if (!allPaths.length) {
22+
throw new SchematicsException(
23+
'Could not find any tsconfig file. Cannot run the standalone:false migration.',
24+
);
25+
}
26+
27+
for (const tsconfigPath of allPaths) {
28+
runMigration(tree, tsconfigPath, basePath);
29+
}
30+
};
31+
}
32+
33+
function runMigration(tree: Tree, tsconfigPath: string, basePath: string) {
34+
const program = createMigrationProgram(tree, tsconfigPath, basePath);
35+
const sourceFiles = program
36+
.getSourceFiles()
37+
.filter((sourceFile) => canMigrateFile(basePath, sourceFile, program));
38+
39+
for (const sourceFile of sourceFiles) {
40+
let update: UpdateRecorder | null = null;
41+
42+
const rewriter = (startPos: number, width: number, text: string | null) => {
43+
if (update === null) {
44+
// Lazily initialize update, because most files will not require migration.
45+
update = tree.beginUpdate(relative(basePath, sourceFile.fileName));
46+
}
47+
update.remove(startPos, width);
48+
if (text !== null) {
49+
update.insertLeft(startPos, text);
50+
}
51+
};
52+
migrateFile(sourceFile, rewriter);
53+
54+
if (update !== null) {
55+
tree.commitUpdate(update);
56+
}
57+
}
58+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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 ts from 'typescript';
10+
import {ChangeTracker} from '../../utils/change_tracker';
11+
import {getImportSpecifier, getNamedImports} from '../../utils/typescript/imports';
12+
13+
const CORE = '@angular/core';
14+
const DIRECTIVE = 'Directive';
15+
const COMPONENT = 'Component';
16+
const PIPE = 'Pipe';
17+
18+
type RewriteFn = (startPos: number, width: number, text: string) => void;
19+
20+
export function migrateFile(sourceFile: ts.SourceFile, rewriteFn: RewriteFn) {
21+
const changeTracker = new ChangeTracker(ts.createPrinter());
22+
23+
// Check if there are any imports of the `AfterRenderPhase` enum.
24+
const coreImports = getNamedImports(sourceFile, CORE);
25+
if (!coreImports) {
26+
return;
27+
}
28+
const directive = getImportSpecifier(sourceFile, CORE, DIRECTIVE);
29+
const component = getImportSpecifier(sourceFile, CORE, COMPONENT);
30+
const pipe = getImportSpecifier(sourceFile, CORE, PIPE);
31+
32+
if (!directive && !component && !pipe) {
33+
return;
34+
}
35+
36+
ts.forEachChild(sourceFile, function visit(node: ts.Node) {
37+
ts.forEachChild(node, visit);
38+
39+
// First we need to check for class declarations
40+
// Decorators will come after
41+
if (!ts.isClassDeclaration(node)) {
42+
return;
43+
}
44+
45+
ts.getDecorators(node)?.forEach((decorator) => {
46+
if (!ts.isDecorator(decorator)) {
47+
return;
48+
}
49+
50+
const callExpression = decorator.expression;
51+
if (!ts.isCallExpression(callExpression)) {
52+
return;
53+
}
54+
55+
const decoratorIdentifier = callExpression.expression;
56+
if (!ts.isIdentifier(decoratorIdentifier)) {
57+
return;
58+
}
59+
60+
// Checking the identifier of the decorator by comparing to the import specifier
61+
switch (decoratorIdentifier.text) {
62+
case directive?.name.text:
63+
case component?.name.text:
64+
case pipe?.name.text:
65+
break;
66+
default:
67+
// It's not a decorator to migrate
68+
return;
69+
}
70+
71+
const [firstArg] = callExpression.arguments;
72+
if (!firstArg || !ts.isObjectLiteralExpression(firstArg)) {
73+
return;
74+
}
75+
const properties = firstArg.properties;
76+
const standaloneProp = getStandaloneProperty(properties);
77+
78+
// Need to take care of 3 cases
79+
// - standalone: true => remove the property
80+
// - standalone: false => nothing
81+
// - No standalone property => add a standalone: false property
82+
83+
let newProperties;
84+
if (!standaloneProp) {
85+
const standaloneFalseProperty = ts.factory.createPropertyAssignment(
86+
'standalone',
87+
ts.factory.createFalse(),
88+
);
89+
90+
newProperties = [...properties, standaloneFalseProperty];
91+
} else if (standaloneProp.value === ts.SyntaxKind.TrueKeyword) {
92+
newProperties = properties.filter((p) => p !== standaloneProp.property);
93+
}
94+
95+
if (newProperties) {
96+
// At this point we know that we need to add standalone: false or
97+
// remove an existing standalone: true property.
98+
const newPropsArr = ts.factory.createNodeArray(newProperties);
99+
const newFirstArg = ts.factory.createObjectLiteralExpression(newPropsArr, true);
100+
changeTracker.replaceNode(firstArg, newFirstArg);
101+
}
102+
});
103+
});
104+
105+
// Write the changes.
106+
for (const changesInFile of changeTracker.recordChanges().values()) {
107+
for (const change of changesInFile) {
108+
rewriteFn(change.start, change.removeLength ?? 0, change.text);
109+
}
110+
}
111+
}
112+
113+
function getStandaloneProperty(properties: ts.NodeArray<ts.ObjectLiteralElementLike>) {
114+
for (const prop of properties) {
115+
if (
116+
ts.isPropertyAssignment(prop) &&
117+
ts.isIdentifier(prop.name) &&
118+
prop.name.text === 'standalone' &&
119+
(prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
120+
prop.initializer.kind === ts.SyntaxKind.FalseKeyword)
121+
) {
122+
return {property: prop, value: prop.initializer.kind};
123+
}
124+
}
125+
return undefined;
126+
}

0 commit comments

Comments
 (0)