Skip to content

Commit 332a931

Browse files
committed
feat(migrations): add migration to convert ngStyle to use style
Add migration to convert ngStyle to use style
1 parent e75b04f commit 332a931

11 files changed

Lines changed: 1216 additions & 0 deletions

File tree

packages/core/schematics/BUILD.bazel

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ npm_package(
4444
":bundles",
4545
"//packages/core/schematics/migrations/control-flow-migration:static_files",
4646
"//packages/core/schematics/migrations/ngclass-to-class-migration:static_files",
47+
"//packages/core/schematics/migrations/ngstyle-to-style-migration:static_files",
4748
"//packages/core/schematics/ng-generate/cleanup-unused-imports:static_files",
4849
"//packages/core/schematics/ng-generate/inject-migration:static_files",
4950
"//packages/core/schematics/ng-generate/output-migration:static_files",
@@ -102,6 +103,10 @@ bundle_entrypoints = [
102103
"ngclass-to-class-migration",
103104
"packages/core/schematics/migrations/ngclass-to-class-migration/index.js",
104105
],
106+
[
107+
"ngstyle-to-style-migration",
108+
"packages/core/schematics/migrations/ngstyle-to-style-migration/index.js",
109+
],
105110
[
106111
"router-current-navigation",
107112
"packages/core/schematics/migrations/router-current-navigation/index.js",
@@ -123,6 +128,7 @@ rollup.rollup(
123128
"//packages/core/schematics:tsconfig_build",
124129
"//packages/core/schematics/migrations/control-flow-migration",
125130
"//packages/core/schematics/migrations/ngclass-to-class-migration",
131+
"//packages/core/schematics/migrations/ngstyle-to-style-migration",
126132
"//packages/core/schematics/migrations/router-current-navigation",
127133
"//packages/core/schematics/migrations/router-last-successful-navigation",
128134
"//packages/core/schematics/ng-generate/cleanup-unused-imports",

packages/core/schematics/collection.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@
6363
"factory": "./bundles/ngclass-to-class-migration.cjs#migrate",
6464
"schema": "./migrations/ngclass-to-class-migration/schema.json",
6565
"aliases": ["ngclass-to-class"]
66+
},
67+
"ngstyle-to-style-migration": {
68+
"description": "Updates usages of `ngStyle` directives to the `style` bindings where possible",
69+
"factory": "./bundles/ngstyle-to-style-migration.cjs#migrate",
70+
"schema": "./migrations/ngstyle-to-style-migration/schema.json",
71+
"aliases": ["ngstyle-to-style"]
6672
}
6773
}
6874
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
load("//tools:defaults.bzl", "copy_to_bin", "ts_project")
2+
3+
package(
4+
default_visibility = [
5+
"//packages/core/schematics:__pkg__",
6+
"//packages/core/schematics/test:__pkg__",
7+
],
8+
)
9+
10+
copy_to_bin(
11+
name = "static_files",
12+
srcs = ["schema.json"],
13+
)
14+
15+
ts_project(
16+
name = "ngstyle-to-style-migration",
17+
srcs = glob(["**/*.ts"]),
18+
data = ["schema.json"],
19+
deps = [
20+
"//:node_modules/@angular-devkit/schematics",
21+
"//:node_modules/@types/node",
22+
"//:node_modules/typescript",
23+
"//packages/compiler",
24+
"//packages/compiler-cli",
25+
"//packages/compiler-cli/private",
26+
"//packages/compiler-cli/src/ngtsc/annotations",
27+
"//packages/compiler-cli/src/ngtsc/annotations/directive",
28+
"//packages/compiler-cli/src/ngtsc/file_system",
29+
"//packages/compiler-cli/src/ngtsc/imports",
30+
"//packages/compiler-cli/src/ngtsc/metadata",
31+
"//packages/compiler-cli/src/ngtsc/reflection",
32+
"//packages/core/schematics/utils",
33+
"//packages/core/schematics/utils/tsurge",
34+
"//packages/core/schematics/utils/tsurge/helpers/angular_devkit",
35+
],
36+
)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
2+
# ngStyle to style migration
3+
This schematic helps developers to convert ngStyle directive usages to style bindings where possible.
4+
5+
## How to run this migration?
6+
The migration can be run using the following command:
7+
8+
```bash
9+
ng generate @angular/core:ngstyle-to-style
10+
```
11+
12+
By default, the migration will go over the entire application. If you want to apply this migration to a subset of the files, you can pass the path argument as shown below:
13+
14+
```bash
15+
ng generate @angular/core:ngstyle-to-style --path src/app/sub-component
16+
```
17+
18+
### How does it work?
19+
The schematic will attempt to find all the places in the templates where the component selectors are used. And check if they can be converted to self-closing tags.
20+
21+
Example:
22+
23+
```html
24+
<!-- Before -->
25+
<div [ngStyle]="{'background-color': 'red'}">
26+
27+
<!-- After -->
28+
<div [style]="{'background-color': 'red'}">
29+
```
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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.dev/license
7+
*/
8+
9+
import {Rule} from '@angular-devkit/schematics';
10+
import {NgStyleMigration} from './ngstyle-to-style-migration';
11+
import {MigrationStage, runMigrationInDevkit} from '../../utils/tsurge/helpers/angular_devkit';
12+
13+
interface Options {
14+
path: string;
15+
analysisDir: string;
16+
}
17+
18+
export function migrate(options: Options): Rule {
19+
return async (tree, context) => {
20+
await runMigrationInDevkit({
21+
tree,
22+
getMigration: (fs) =>
23+
new NgStyleMigration({
24+
shouldMigrate: (file) => {
25+
return (
26+
file.rootRelativePath.startsWith(fs.normalize(options.path)) &&
27+
!/(^|\/)node_modules\//.test(file.rootRelativePath)
28+
);
29+
},
30+
}),
31+
beforeProgramCreation: (tsconfigPath: string, stage: MigrationStage) => {
32+
if (stage === MigrationStage.Analysis) {
33+
context.logger.info(`Preparing analysis for: ${tsconfigPath}...`);
34+
} else {
35+
context.logger.info(`Running migration for: ${tsconfigPath}...`);
36+
}
37+
},
38+
beforeUnitAnalysis: (tsconfigPath: string) => {
39+
context.logger.info(`Scanning for component tags: ${tsconfigPath}...`);
40+
},
41+
afterAllAnalyzed: () => {
42+
context.logger.info(``);
43+
context.logger.info(`Processing analysis data between targets...`);
44+
context.logger.info(``);
45+
},
46+
afterAnalysisFailure: () => {
47+
context.logger.error('Migration failed unexpectedly with no analysis data');
48+
},
49+
whenDone: ({
50+
touchedFilesCount,
51+
replacementCount,
52+
}: {
53+
touchedFilesCount: number;
54+
replacementCount: number;
55+
}) => {
56+
context.logger.info('');
57+
context.logger.info(`Successfully migrated to style from ngStyle 🎉`);
58+
context.logger.info(
59+
` -> Migrated ${replacementCount} ngStyle to style in ${touchedFilesCount} files.`,
60+
);
61+
},
62+
});
63+
};
64+
}
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
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.dev/license
7+
*/
8+
9+
import ts from 'typescript';
10+
import {
11+
confirmAsSerializable,
12+
ProgramInfo,
13+
projectFile,
14+
ProjectFile,
15+
ProjectFileID,
16+
Replacement,
17+
Serializable,
18+
TextUpdate,
19+
TsurgeFunnelMigration,
20+
} from '../../utils/tsurge';
21+
import {
22+
migrateNgStyleBindings,
23+
calculateImportReplacements,
24+
createNgStyleImportsArrayRemoval,
25+
} from './util';
26+
import {AbsoluteFsPath} from '@angular/compiler-cli';
27+
import {NgComponentTemplateVisitor} from '../../utils/ng_component_template';
28+
import {MigrationConfig} from './types';
29+
30+
export interface NgStyleMigrationData {
31+
file: ProjectFile;
32+
replacementCount: number;
33+
replacements: Replacement[];
34+
}
35+
36+
export interface NgStyleCompilationUnitData {
37+
ngStyleReplacements: Array<NgStyleMigrationData>;
38+
importReplacements: Record<ProjectFileID, {add: Replacement[]; addAndRemove: Replacement[]}>;
39+
}
40+
41+
export class NgStyleMigration extends TsurgeFunnelMigration<
42+
NgStyleCompilationUnitData,
43+
NgStyleCompilationUnitData
44+
> {
45+
constructor(private readonly config: MigrationConfig = {}) {
46+
super();
47+
}
48+
49+
override async analyze(info: ProgramInfo): Promise<Serializable<NgStyleCompilationUnitData>> {
50+
const {sourceFiles, program} = info;
51+
const typeChecker = program.getTypeChecker();
52+
const ngStyleReplacements: Array<NgStyleMigrationData> = [];
53+
const filesWithNgStyleDeclarations = new Set<ts.SourceFile>();
54+
55+
for (const sf of sourceFiles) {
56+
ts.forEachChild(sf, (node: ts.Node) => {
57+
if (!ts.isClassDeclaration(node)) {
58+
return;
59+
}
60+
61+
const file = projectFile(sf, info);
62+
63+
if (this.config.shouldMigrate && !this.config.shouldMigrate(file)) {
64+
return;
65+
}
66+
67+
const templateVisitor = new NgComponentTemplateVisitor(typeChecker);
68+
templateVisitor.visitNode(node);
69+
70+
const replacementsForStyle: Replacement[] = [];
71+
let replacementCountForStyle = 0;
72+
73+
templateVisitor.resolvedTemplates.forEach((template) => {
74+
const {migrated, changed, replacementCount} = migrateNgStyleBindings(
75+
template.content,
76+
this.config,
77+
node,
78+
typeChecker,
79+
);
80+
81+
if (!changed) {
82+
return;
83+
}
84+
85+
replacementCountForStyle += replacementCount;
86+
87+
const fileToMigrate = template.inline
88+
? file
89+
: projectFile(template.filePath as AbsoluteFsPath, info);
90+
const end = template.start + template.content.length;
91+
92+
replacementsForStyle.push(
93+
prepareTextReplacement(fileToMigrate, migrated, template.start, end),
94+
);
95+
});
96+
97+
if (replacementCountForStyle === 0) {
98+
return;
99+
}
100+
101+
filesWithNgStyleDeclarations.add(sf);
102+
103+
const importArrayRemoval = createNgStyleImportsArrayRemoval(node, file, typeChecker);
104+
if (importArrayRemoval) {
105+
replacementsForStyle.push(importArrayRemoval);
106+
}
107+
108+
const existing = ngStyleReplacements.find((entry) => entry.file === file);
109+
if (existing) {
110+
existing.replacements.push(...replacementsForStyle);
111+
existing.replacementCount += replacementCountForStyle;
112+
} else {
113+
ngStyleReplacements.push({
114+
file,
115+
replacements: replacementsForStyle,
116+
replacementCount: replacementCountForStyle,
117+
});
118+
}
119+
});
120+
}
121+
122+
const importReplacements = calculateImportReplacements(info, filesWithNgStyleDeclarations);
123+
return confirmAsSerializable({ngStyleReplacements, importReplacements});
124+
}
125+
126+
override async combine(
127+
unitA: NgStyleCompilationUnitData,
128+
unitB: NgStyleCompilationUnitData,
129+
): Promise<Serializable<NgStyleCompilationUnitData>> {
130+
const importReplacements: Record<
131+
ProjectFileID,
132+
{add: Replacement[]; addAndRemove: Replacement[]}
133+
> = {};
134+
135+
for (const unit of [unitA, unitB]) {
136+
for (const fileIDStr of Object.keys(unit.importReplacements)) {
137+
const fileID = fileIDStr as ProjectFileID;
138+
importReplacements[fileID] = unit.importReplacements[fileID];
139+
}
140+
}
141+
142+
return confirmAsSerializable({
143+
ngStyleReplacements: [...unitA.ngStyleReplacements, ...unitB.ngStyleReplacements],
144+
importReplacements,
145+
});
146+
}
147+
148+
override async globalMeta(
149+
combinedData: NgStyleCompilationUnitData,
150+
): Promise<Serializable<NgStyleCompilationUnitData>> {
151+
return confirmAsSerializable({
152+
ngStyleReplacements: combinedData.ngStyleReplacements,
153+
importReplacements: combinedData.importReplacements,
154+
});
155+
}
156+
157+
override async stats(globalMetadata: NgStyleCompilationUnitData) {
158+
const touchedFilesCount = globalMetadata.ngStyleReplacements.length;
159+
const replacementCount = globalMetadata.ngStyleReplacements.reduce(
160+
(acc, cur) => acc + cur.replacementCount,
161+
0,
162+
);
163+
164+
return confirmAsSerializable({
165+
touchedFilesCount,
166+
replacementCount,
167+
});
168+
}
169+
170+
override async migrate(globalData: NgStyleCompilationUnitData) {
171+
const replacements: Replacement[] = [];
172+
173+
replacements.push(...globalData.ngStyleReplacements.flatMap(({replacements}) => replacements));
174+
175+
for (const fileIDStr of Object.keys(globalData.importReplacements)) {
176+
const fileID = fileIDStr as ProjectFileID;
177+
const importReplacements = globalData.importReplacements[fileID];
178+
replacements.push(...importReplacements.addAndRemove);
179+
}
180+
181+
return {replacements};
182+
}
183+
}
184+
185+
function prepareTextReplacement(
186+
file: ProjectFile,
187+
replacement: string,
188+
start: number,
189+
end: number,
190+
): Replacement {
191+
return new Replacement(
192+
file,
193+
new TextUpdate({
194+
position: start,
195+
end: end,
196+
toInsert: replacement,
197+
}),
198+
);
199+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema",
3+
"$id": "AngularNgStyleToStyleMigration",
4+
"title": "Angular ngStyle to style Migration Schema",
5+
"type": "object",
6+
"properties": {
7+
"path": {
8+
"type": "string",
9+
"description": "Path to the directory where all templates should be migrated.",
10+
"x-prompt": "Which directory do you want to migrate?",
11+
"default": "./"
12+
}
13+
}
14+
}

0 commit comments

Comments
 (0)