Skip to content

Commit 62d7533

Browse files
tomer953thePunderWoman
authored andcommitted
fix(migrations): prevent duplicate imports in common-to-standalone migration
The common-to-standalone migration did not check for existing imports when adding needed imports after removing CommonModule. This change adds deduplication logic to filter out imports that already exist in the imports array before adding them, ensuring each import appears only once. (cherry picked from commit 008e20c)
1 parent a59f51f commit 62d7533

File tree

2 files changed

+72
-14
lines changed

2 files changed

+72
-14
lines changed

packages/core/schematics/migrations/common-to-standalone-migration/util.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,18 @@ function createCommonModuleImportsArrayRemoval(
136136
return !isCommonModuleFromAngularCommon(typeChecker, el);
137137
});
138138

139-
const newElements = [
140-
...filteredElements,
141-
...neededImports.sort().map((imp) => ts.factory.createIdentifier(imp)),
142-
];
139+
// Get existing import names to avoid duplicates
140+
const existingImportNames = new Set(
141+
filteredElements.filter((el) => ts.isIdentifier(el)).map((el) => el.getText()),
142+
);
143+
144+
// Only add imports that don't already exist
145+
const importsToAdd = neededImports
146+
.filter((imp) => !existingImportNames.has(imp))
147+
.sort()
148+
.map((imp) => ts.factory.createIdentifier(imp));
149+
150+
const newElements = [...filteredElements, ...importsToAdd];
143151

144152
if (newElements.length === originalElements.length && neededImports.length === 0) {
145153
return null;

packages/core/schematics/test/common_to_standalone_migration_spec.ts

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,31 @@ function expectImportsToContain(content: string, ...importNames: string[]): void
2929

3030
// Helper function to check import declarations with flexible formatting
3131
function expectImportDeclarationToContain(content: string, ...importNames: string[]): void {
32-
// Create individual patterns for each import name with flexible spacing
33-
const sortedImports = importNames
34-
.sort()
35-
.map((name) => `\\s*${name}\\s*`)
36-
.join(',');
37-
// Create regex pattern that matches import with flexible spacing: import { NgIf } or import {NgIf}
38-
const importPattern = new RegExp(
39-
`import\\s*\\{${sortedImports}\\}\\s*from\\s*['"]@angular\\/common['"]`,
40-
);
41-
expect(content).toMatch(importPattern);
32+
// Sort the import names to match the sorted order in the actual imports
33+
const sortedImports = [...importNames].sort();
34+
35+
// Match both single-line and multi-line import formats
36+
// Pattern matches: import { A, B, C } from '@angular/common'
37+
// or: import {\n A,\n B,\n C\n} from '@angular/common'
38+
const importRegex = /import\s*\{([^}]+)\}\s*from\s*['"]@angular\/common['"]/;
39+
const match = content.match(importRegex);
40+
41+
expect(match).toBeTruthy('Should have an import from @angular/common');
42+
43+
// Extract and normalize the imported names
44+
const importedNames = match![1]
45+
.split(',')
46+
.map((name) => name.trim())
47+
.filter((name) => name.length > 0)
48+
.sort();
49+
50+
// Check that all expected imports are present
51+
for (const expectedImport of sortedImports) {
52+
expect(importedNames).toContain(expectedImport);
53+
}
54+
55+
// Check that we have the exact same imports (no extras)
56+
expect(importedNames).toEqual(sortedImports);
4257
}
4358

4459
describe('Common → standalone imports migration', () => {
@@ -1493,6 +1508,41 @@ describe('Common → standalone imports migration', () => {
14931508
expect(content).not.toContain('CommonModule');
14941509
});
14951510
});
1511+
it('should not create duplicate imports when imports already exist', async () => {
1512+
writeFile(
1513+
'/comp.ts',
1514+
dedent`
1515+
import {Component} from '@angular/core';
1516+
import {CommonModule, AsyncPipe} from '@angular/common';
1517+
import {SomeOtherModule} from './some-other-module';
1518+
1519+
@Component({
1520+
selector: 'app-duplicate-test',
1521+
imports: [CommonModule, AsyncPipe, SomeOtherModule],
1522+
template: \`<div>{{ data$ | async }}</div>\`
1523+
})
1524+
export class DuplicateTestComponent {
1525+
data$ = Promise.resolve('test');
1526+
}
1527+
`,
1528+
);
1529+
1530+
await runMigration();
1531+
const content = tree.readContent('/comp.ts');
1532+
1533+
// Should remove CommonModule and keep AsyncPipe without duplicating it
1534+
expect(content).not.toContain('CommonModule');
1535+
expectImportsToContain(content, 'AsyncPipe', 'SomeOtherModule');
1536+
1537+
// Verify AsyncPipe appears only once in the imports array
1538+
const importsMatch = content.match(/imports:\s*\[([\s\S]*?)\]/);
1539+
expect(importsMatch).toBeTruthy();
1540+
const importsArray = importsMatch![1];
1541+
const asyncPipeMatches = importsArray.match(/AsyncPipe/g);
1542+
expect(asyncPipeMatches?.length).toBe(1);
1543+
1544+
expectImportDeclarationToContain(content, 'AsyncPipe');
1545+
});
14961546

14971547
describe('Robustness and edge cases', () => {
14981548
it('should handle very large templates without performance issues', async () => {

0 commit comments

Comments
 (0)