Skip to content

Commit 8f6affd

Browse files
fix(migrations): fixes control flow migration common module removal (#53076)
Common module removal would not happen when a component used a templateUrl due to the checks being in separate files. This change passes the removal analysis back to the original source file to safely remove CommonModule. PR Close #53076
1 parent aab7fb8 commit 8f6affd

File tree

5 files changed

+85
-11
lines changed

5 files changed

+85
-11
lines changed

packages/core/schematics/ng-generate/control-flow-migration/index.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,12 @@ function runControlFlowMigration(
7878
analyze(sourceFile, analysis);
7979
}
8080

81-
for (const [path, file] of analysis) {
81+
// sort files with .html files first
82+
// this ensures class files know if it's safe to remove CommonModule
83+
const paths = sortFilePaths([...analysis.keys()]);
84+
85+
for (const path of paths) {
86+
const file = analysis.get(path)!;
8287
const ranges = file.getSortedRanges();
8388
const relativePath = relative(basePath, path);
8489
const content = tree.readText(relativePath);
@@ -89,7 +94,7 @@ function runControlFlowMigration(
8994
const length = (end ?? content.length) - start;
9095

9196
const {migrated, errors} =
92-
migrateTemplate(template, type, node, file, schematicOptions.format);
97+
migrateTemplate(template, type, node, file, schematicOptions.format, analysis);
9398

9499
if (migrated !== null) {
95100
update.remove(start, length);
@@ -113,6 +118,12 @@ function runControlFlowMigration(
113118
return errorList;
114119
}
115120

121+
function sortFilePaths(names: string[]): string[] {
122+
const templateFiles = names.filter(n => n.endsWith('.html'));
123+
const classFiles = names.filter(n => !n.endsWith('.html'));
124+
return [...templateFiles, ...classFiles];
125+
}
126+
116127
function generateErrorMessage(path: string, errors: MigrateError[]): string {
117128
let errorMessage = `Template "${path}" encountered ${errors.length} errors during migration:\n`;
118129
errorMessage += errors.map(e => ` - ${e.type}: ${e.error}\n`);

packages/core/schematics/ng-generate/control-flow-migration/migration.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import {canRemoveCommonModule, formatTemplate, processNgTemplates, removeImports
2020
*/
2121
export function migrateTemplate(
2222
template: string, templateType: string, node: ts.Node, file: AnalyzedFile,
23-
format: boolean = true): {migrated: string, errors: MigrateError[]} {
23+
format: boolean = true,
24+
analyzedFiles: Map<string, AnalyzedFile>|null): {migrated: string, errors: MigrateError[]} {
2425
let errors: MigrateError[] = [];
2526
let migrated = template;
2627
if (templateType === 'template' || templateType === 'templateUrl') {
@@ -36,6 +37,15 @@ export function migrateTemplate(
3637
}
3738
file.removeCommonModule = canRemoveCommonModule(template);
3839

40+
// when migrating an external template, we have to pass back
41+
// whether it's safe to remove the CommonModule to the
42+
// original component class source file
43+
if (templateType === 'templateUrl' && analyzedFiles !== null &&
44+
analyzedFiles.has(file.sourceFilePath)) {
45+
const componentFile = analyzedFiles.get(file.sourceFilePath)!;
46+
componentFile.removeCommonModule = file.removeCommonModule;
47+
}
48+
3949
errors = [
4050
...ifResult.errors,
4151
...forResult.errors,

packages/core/schematics/ng-generate/control-flow-migration/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,11 @@ export class Template {
205205
export class AnalyzedFile {
206206
private ranges: Range[] = [];
207207
removeCommonModule = false;
208+
sourceFilePath: string = '';
208209

209210
/** Returns the ranges in the order in which they should be migrated. */
210211
getSortedRanges(): Range[] {
212+
// templates first for checking on whether certain imports can be safely removed
211213
const templateRanges = this.ranges.slice()
212214
.filter(x => x.type !== 'import')
213215
.sort((aStart, bStart) => bStart.start - aStart.start);
@@ -223,11 +225,14 @@ export class AnalyzedFile {
223225
* @param analyzedFiles Map keeping track of all the analyzed files.
224226
* @param range Range to be added.
225227
*/
226-
static addRange(path: string, analyzedFiles: Map<string, AnalyzedFile>, range: Range): void {
228+
static addRange(
229+
path: string, sourceFilePath: string, analyzedFiles: Map<string, AnalyzedFile>,
230+
range: Range): void {
227231
let analysis = analyzedFiles.get(path);
228232

229233
if (!analysis) {
230234
analysis = new AnalyzedFile();
235+
analysis.sourceFilePath = sourceFilePath;
231236
analyzedFiles.set(path, analysis);
232237
}
233238

packages/core/schematics/ng-generate/control-flow-migration/util.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,21 @@ function updateImportClause(clause: ts.ImportClause, removeCommonModule: boolean
7777
}
7878

7979
function updateClassImports(
80-
propAssignment: ts.PropertyAssignment, removeCommonModule: boolean): string {
81-
const printer = ts.createPrinter();
80+
propAssignment: ts.PropertyAssignment, removeCommonModule: boolean): string|null {
81+
// removeComments is set to true to prevent duplication of comments
82+
// when the import declaration is at the top of the file, but right after a comment
83+
// without this, the comment gets duplicated when the declaration is updated.
84+
// the typescript AST includes that preceding comment as part of the import declaration full text.
85+
const printer = ts.createPrinter({
86+
removeComments: true,
87+
});
8288
const importList = propAssignment.initializer as ts.ArrayLiteralExpression;
8389
const removals = removeCommonModule ? importWithCommonRemovals : importRemovals;
8490
const elements = importList.elements.filter(el => !removals.includes(el.getText()));
91+
if (elements.length === importList.elements.length) {
92+
// nothing changed
93+
return null;
94+
}
8595
const updatedElements = ts.factory.updateArrayLiteralExpression(importList, elements);
8696
const updatedAssignment =
8797
ts.factory.updatePropertyAssignment(propAssignment, propAssignment.name, updatedElements);
@@ -101,7 +111,7 @@ function analyzeImportDeclarations(
101111
clause.namedBindings.elements.filter(el => importWithCommonRemovals.includes(el.getText()));
102112
if (elements.length > 0) {
103113
AnalyzedFile.addRange(
104-
sourceFile.fileName, analyzedFiles,
114+
sourceFile.fileName, sourceFile.fileName, analyzedFiles,
105115
{start: node.getStart(), end: node.getEnd(), node, type: 'import'});
106116
}
107117
}
@@ -139,7 +149,7 @@ function analyzeDecorators(
139149
switch (prop.name.text) {
140150
case 'template':
141151
// +1/-1 to exclude the opening/closing characters from the range.
142-
AnalyzedFile.addRange(sourceFile.fileName, analyzedFiles, {
152+
AnalyzedFile.addRange(sourceFile.fileName, sourceFile.fileName, analyzedFiles, {
143153
start: prop.initializer.getStart() + 1,
144154
end: prop.initializer.getEnd() - 1,
145155
node: prop,
@@ -148,7 +158,7 @@ function analyzeDecorators(
148158
break;
149159

150160
case 'imports':
151-
AnalyzedFile.addRange(sourceFile.fileName, analyzedFiles, {
161+
AnalyzedFile.addRange(sourceFile.fileName, sourceFile.fileName, analyzedFiles, {
152162
start: prop.name.getStart(),
153163
end: prop.initializer.getEnd(),
154164
node: prop,
@@ -160,7 +170,9 @@ function analyzeDecorators(
160170
// Leave the end as undefined which means that the range is until the end of the file.
161171
if (ts.isStringLiteralLike(prop.initializer)) {
162172
const path = join(dirname(sourceFile.fileName), prop.initializer.text);
163-
AnalyzedFile.addRange(path, analyzedFiles, {start: 0, node: prop, type: 'templateUrl'});
173+
AnalyzedFile.addRange(
174+
path, sourceFile.fileName, analyzedFiles,
175+
{start: 0, node: prop, type: 'templateUrl'});
164176
}
165177
break;
166178
}
@@ -353,7 +365,8 @@ export function canRemoveCommonModule(template: string): boolean {
353365
export function removeImports(
354366
template: string, node: ts.Node, removeCommonModule: boolean): string {
355367
if (template.startsWith('imports') && ts.isPropertyAssignment(node)) {
356-
return updateClassImports(node, removeCommonModule);
368+
const updatedImport = updateClassImports(node, removeCommonModule);
369+
return updatedImport ?? template;
357370
} else if (ts.isImportDeclaration(node) && checkIfShouldChange(node, removeCommonModule)) {
358371
return updateImportDeclaration(node, removeCommonModule);
359372
}

packages/core/schematics/test/control_flow_migration_spec.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4020,6 +4020,41 @@ describe('control flow migration', () => {
40204020

40214021
expect(actual).toBe(expected);
40224022
});
4023+
4024+
it('should remove common module post migration if using external template', async () => {
4025+
writeFile('/comp.ts', [
4026+
`import {Component} from '@angular/core';`,
4027+
`import {CommonModule} from '@angular/common';\n`,
4028+
`@Component({`,
4029+
` imports: [CommonModule],`,
4030+
` templateUrl: './comp.html',`,
4031+
`})`,
4032+
`class Comp {`,
4033+
` toggle = false;`,
4034+
`}`,
4035+
].join('\n'));
4036+
4037+
writeFile('/comp.html', [
4038+
`<div>`,
4039+
`<span *ngIf="show">Content here</span>`,
4040+
`</div>`,
4041+
].join('\n'));
4042+
4043+
await runMigration();
4044+
const actual = tree.readContent('/comp.ts');
4045+
const expected = [
4046+
`import {Component} from '@angular/core';\n\n`,
4047+
`@Component({`,
4048+
` imports: [],`,
4049+
` templateUrl: './comp.html',`,
4050+
`})`,
4051+
`class Comp {`,
4052+
` toggle = false;`,
4053+
`}`,
4054+
].join('\n');
4055+
4056+
expect(actual).toBe(expected);
4057+
});
40234058
});
40244059

40254060
describe('no migration needed', () => {

0 commit comments

Comments
 (0)