refactor(migrations): Don't migrate twice the same file on the self-closing tag migration (#60065)

This commit fixes an issue when ts files are referenced multiple times (and thus analyzed multiple times) for example from a `tsconfig.json` and `tsconfig.spec.json`.

PR Close #60065
This commit is contained in:
Matthieu Riegler 2025-02-23 01:26:33 +01:00 committed by Miles Malerba
parent b09f4a5c79
commit ddfaf0cd46
2 changed files with 80 additions and 20 deletions

View file

@ -56,6 +56,7 @@ export class SelfClosingTagsMigration extends TsurgeFunnelMigration<
for (const sf of sourceFiles) {
ts.forEachChild(sf, (node: ts.Node) => {
// Skipping any non component declarations
if (!ts.isClassDeclaration(node)) {
return;
}
@ -75,26 +76,28 @@ export class SelfClosingTagsMigration extends TsurgeFunnelMigration<
template.content,
);
if (changed) {
const fileToMigrate = template.inline
? file
: projectFile(template.filePath as AbsoluteFsPath, info);
const end = template.start + template.content.length;
if (!changed) {
return;
}
const replacements = [
prepareTextReplacement(fileToMigrate, migrated, template.start, end),
];
const fileToMigrate = template.inline
? file
: projectFile(template.filePath as AbsoluteFsPath, info);
const end = template.start + template.content.length;
const fileReplacements = tagReplacements.find(
(tagReplacement) => tagReplacement.file === file,
);
const replacements = [
prepareTextReplacement(fileToMigrate, migrated, template.start, end),
];
if (fileReplacements) {
fileReplacements.replacements.push(...replacements);
fileReplacements.replacementCount += replacementCount;
} else {
tagReplacements.push({file, replacements, replacementCount});
}
const fileReplacements = tagReplacements.find(
(tagReplacement) => tagReplacement.file === file,
);
if (fileReplacements) {
fileReplacements.replacements.push(...replacements);
fileReplacements.replacementCount += replacementCount;
} else {
tagReplacements.push({file, replacements, replacementCount});
}
});
});
@ -107,9 +110,12 @@ export class SelfClosingTagsMigration extends TsurgeFunnelMigration<
unitA: SelfClosingTagsCompilationUnitData,
unitB: SelfClosingTagsCompilationUnitData,
): Promise<Serializable<SelfClosingTagsCompilationUnitData>> {
return confirmAsSerializable({
tagReplacements: unitA.tagReplacements.concat(unitB.tagReplacements),
});
const uniqueReplacements = removeDuplicateReplacements([
...unitA.tagReplacements,
...unitB.tagReplacements,
]);
return confirmAsSerializable({tagReplacements: uniqueReplacements});
}
override async globalMeta(
@ -159,3 +165,20 @@ function prepareTextReplacement(
}),
);
}
function removeDuplicateReplacements(
replacements: SelfClosingTagsMigrationData[],
): SelfClosingTagsMigrationData[] {
const uniqueFiles = new Set<string>();
const result: SelfClosingTagsMigrationData[] = [];
for (const replacement of replacements) {
const fileId = replacement.file.id;
if (!uniqueFiles.has(fileId)) {
uniqueFiles.add(fileId);
result.push(replacement);
}
}
return result;
}

View file

@ -19,6 +19,7 @@ describe('self-closing-tags migration', () => {
let tree: UnitTestTree;
let tmpDirPath: string;
let previousWorkingDir: string;
let logs: string[];
function writeFile(filePath: string, contents: string) {
host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents));
@ -32,6 +33,7 @@ describe('self-closing-tags migration', () => {
runner = new SchematicTestRunner('test', runfiles.resolvePackageRelative('../collection.json'));
host = new TempScopedNodeJsSyncHost();
tree = new UnitTestTree(new HostTree(host));
logs = [];
writeFile('/tsconfig.json', '{}');
writeFile(
@ -44,6 +46,7 @@ describe('self-closing-tags migration', () => {
previousWorkingDir = shx.pwd();
tmpDirPath = getSystemPath(host.root);
runner.logger.subscribe((log) => logs.push(log.message));
shx.cd(tmpDirPath);
});
@ -66,5 +69,39 @@ describe('self-closing-tags migration', () => {
const content = tree.readContent('/app.component.ts').replace(/\s+/g, ' ');
expect(content).toContain('<my-cmp />');
expect(logs.pop()).toBe(
' -> Migrated 1 components to self-closing tags in 1 component files.',
);
});
it('should handle a file that is present in multiple projects', async () => {
writeFile('/tsconfig-2.json', '{}');
writeFile(
'/angular.json',
JSON.stringify({
version: 1,
projects: {
a: {root: '', architect: {build: {options: {tsConfig: './tsconfig.json'}}}},
b: {root: '', architect: {build: {options: {tsConfig: './tsconfig-2.json'}}}},
},
}),
);
writeFile(
'/app.component.ts',
`
import {Component} from '@angular/core';
@Component({ template: '<my-cmp></my-cmp>' })
export class Cmp {}
`,
);
await runMigration();
const content = tree.readContent('/app.component.ts').replace(/\s+/g, ' ');
expect(content).toContain('<my-cmp />');
expect(logs.pop()).toBe(
' -> Migrated 1 components to self-closing tags in 1 component files.',
);
});
});