Skip to content

Commit 98f4773

Browse files
authored
feat(typewriter): deduplicate selective imports from same source (#2274)
Consolidates multiple selective imports from the same module source into a single import statement, reducing redundancy in generated TypeScript code. The TypeScript renderer now: - Groups selective imports by source module - Merges all imported names from the same source - Sorts imports alphabetically by name - Preserves aliased imports correctly - Keeps aliased module imports (`import * as`) separate from selective imports This improves code generation quality by eliminating duplicate import statements when multiple parts of the code import different symbols from the same module.
1 parent 1ce7a41 commit 98f4773

File tree

3 files changed

+200
-7
lines changed

3 files changed

+200
-7
lines changed

packages/@cdklabs/typewriter/src/renderer/typescript.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,16 +96,42 @@ export class TypeScriptRenderer extends Renderer {
9696
}
9797

9898
protected renderImports(mod: Module) {
99+
const selectiveBySource = new Map<string, SelectiveModuleImport[]>();
100+
const aliased: AliasedModuleImport[] = [];
101+
99102
for (const imp of mod.imports) {
100103
if (imp instanceof AliasedModuleImport) {
101-
this.emit(`import * as ${imp.importAlias} from "${imp.moduleSource}";\n`);
104+
aliased.push(imp);
102105
} else if (imp instanceof SelectiveModuleImport) {
103-
const names = imp.importedNames.map((name) => {
106+
const imports = selectiveBySource.get(imp.moduleSource) ?? [];
107+
imports.push(imp);
108+
selectiveBySource.set(imp.moduleSource, imports);
109+
}
110+
}
111+
112+
for (const imp of aliased) {
113+
this.emit(`import * as ${imp.importAlias} from "${imp.moduleSource}";\n`);
114+
}
115+
116+
for (const [source, imports] of selectiveBySource) {
117+
const allImports = new Map<string, string | undefined>();
118+
for (const imp of imports) {
119+
for (const name of imp.importedNames) {
104120
const alias = imp.getAlias(name);
105-
return alias ? `${name} as ${alias}` : name;
106-
});
107-
this.emit(`import { ${names.join(', ')} } from "${imp.moduleSource}";\n`);
121+
const key = alias ? `${name}:${alias}` : name;
122+
allImports.set(key, alias);
123+
}
108124
}
125+
const sorted = Array.from(allImports.entries()).sort((a, b) => {
126+
const nameA = a[0].split(':')[0];
127+
const nameB = b[0].split(':')[0];
128+
return nameA.localeCompare(nameB);
129+
});
130+
const names = sorted.map(([key, alias]) => {
131+
const name = key.split(':')[0];
132+
return alias ? `${name} as ${alias}` : name;
133+
});
134+
this.emit(`import { ${names.join(', ')} } from "${source}";\n`);
109135
}
110136

111137
if (mod.imports.length > 0) {

packages/@cdklabs/typewriter/test/aliased-imports.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ test('selective import with multiple aliases using tuples', () => {
2525
const ts = new TypeScriptRenderer();
2626
expect(ts.render(target)).toMatchInlineSnapshot(`
2727
"/* eslint-disable prettier/prettier, @stylistic/max-len */
28-
import { foo as bar, baz as qux } from "source";"
28+
import { baz as qux, foo as bar } from "source";"
2929
`);
3030
});
3131

@@ -38,7 +38,7 @@ test('selective import with mixed regular and aliased', () => {
3838
const ts = new TypeScriptRenderer();
3939
expect(ts.render(target)).toMatchInlineSnapshot(`
4040
"/* eslint-disable prettier/prettier, @stylistic/max-len */
41-
import { regular, foo as bar } from "source";"
41+
import { foo as bar, regular } from "source";"
4242
`);
4343
});
4444

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { Module, TypeScriptRenderer } from '../src';
2+
3+
test('duplicate selective imports from same source should merge', () => {
4+
const source = new Module('source');
5+
const target = new Module('target');
6+
7+
source.importSelective(target, ['foo']);
8+
source.importSelective(target, ['bar']);
9+
10+
const ts = new TypeScriptRenderer();
11+
expect(ts.render(target)).toMatchInlineSnapshot(`
12+
"/* eslint-disable prettier/prettier, @stylistic/max-len */
13+
import { bar, foo } from "source";"
14+
`);
15+
});
16+
17+
test('selective imports with aliases from same source should merge', () => {
18+
const source = new Module('source');
19+
const target = new Module('target');
20+
21+
source.importSelective(target, [['foo', 'f']]);
22+
source.importSelective(target, [['bar', 'b']]);
23+
24+
const ts = new TypeScriptRenderer();
25+
expect(ts.render(target)).toMatchInlineSnapshot(`
26+
"/* eslint-disable prettier/prettier, @stylistic/max-len */
27+
import { bar as b, foo as f } from "source";"
28+
`);
29+
});
30+
31+
test('mixed regular and aliased selective imports from same source should merge', () => {
32+
const source = new Module('source');
33+
const target = new Module('target');
34+
35+
source.importSelective(target, ['foo']);
36+
source.importSelective(target, [['bar', 'b']]);
37+
source.importSelective(target, ['baz']);
38+
39+
const ts = new TypeScriptRenderer();
40+
expect(ts.render(target)).toMatchInlineSnapshot(`
41+
"/* eslint-disable prettier/prettier, @stylistic/max-len */
42+
import { bar as b, baz, foo } from "source";"
43+
`);
44+
});
45+
46+
test('imports from different sources should remain separate', () => {
47+
const source1 = new Module('source1');
48+
const source2 = new Module('source2');
49+
const target = new Module('target');
50+
51+
source1.importSelective(target, ['foo']);
52+
source2.importSelective(target, ['bar']);
53+
54+
const ts = new TypeScriptRenderer();
55+
expect(ts.render(target)).toMatchInlineSnapshot(`
56+
"/* eslint-disable prettier/prettier, @stylistic/max-len */
57+
import { foo } from "source1";
58+
import { bar } from "source2";"
59+
`);
60+
});
61+
62+
test('aliased module imports should not merge with selective imports', () => {
63+
const source = new Module('source');
64+
const target = new Module('target');
65+
66+
source.import(target, 'S');
67+
source.importSelective(target, ['foo']);
68+
69+
const ts = new TypeScriptRenderer();
70+
expect(ts.render(target)).toMatchInlineSnapshot(`
71+
"/* eslint-disable prettier/prettier, @stylistic/max-len */
72+
import * as S from "source";
73+
import { foo } from "source";"
74+
`);
75+
});
76+
77+
test('multiple aliased module imports from same source should remain separate', () => {
78+
const source = new Module('source');
79+
const target = new Module('target');
80+
81+
source.import(target, 'S1');
82+
source.import(target, 'S2');
83+
84+
const ts = new TypeScriptRenderer();
85+
expect(ts.render(target)).toMatchInlineSnapshot(`
86+
"/* eslint-disable prettier/prettier, @stylistic/max-len */
87+
import * as S1 from "source";
88+
import * as S2 from "source";"
89+
`);
90+
});
91+
92+
test('deduplication with custom fromLocation', () => {
93+
const source = new Module('source');
94+
const target = new Module('target');
95+
96+
source.importSelective(target, ['foo'], { fromLocation: './custom' });
97+
source.importSelective(target, ['bar'], { fromLocation: './custom' });
98+
99+
const ts = new TypeScriptRenderer();
100+
expect(ts.render(target)).toMatchInlineSnapshot(`
101+
"/* eslint-disable prettier/prettier, @stylistic/max-len */
102+
import { bar, foo } from "./custom";"
103+
`);
104+
});
105+
106+
test('different fromLocations should not merge', () => {
107+
const source = new Module('source');
108+
const target = new Module('target');
109+
110+
source.importSelective(target, ['foo'], { fromLocation: './path1' });
111+
source.importSelective(target, ['bar'], { fromLocation: './path2' });
112+
113+
const ts = new TypeScriptRenderer();
114+
expect(ts.render(target)).toMatchInlineSnapshot(`
115+
"/* eslint-disable prettier/prettier, @stylistic/max-len */
116+
import { foo } from "./path1";
117+
import { bar } from "./path2";"
118+
`);
119+
});
120+
121+
test('complex scenario with multiple sources and mixed imports', () => {
122+
const source1 = new Module('source1');
123+
const source2 = new Module('source2');
124+
const target = new Module('target');
125+
126+
source1.importSelective(target, ['a']);
127+
source2.import(target, 'S2');
128+
source1.importSelective(target, [['b', 'B']]);
129+
source2.importSelective(target, ['c']);
130+
source1.importSelective(target, ['d']);
131+
132+
const ts = new TypeScriptRenderer();
133+
expect(ts.render(target)).toMatchInlineSnapshot(`
134+
"/* eslint-disable prettier/prettier, @stylistic/max-len */
135+
import * as S2 from "source2";
136+
import { a, b as B, d } from "source1";
137+
import { c } from "source2";"
138+
`);
139+
});
140+
141+
test('same import added twice should deduplicate', () => {
142+
const source = new Module('source');
143+
const target = new Module('target');
144+
145+
source.importSelective(target, ['foo']);
146+
source.importSelective(target, ['foo']);
147+
148+
const ts = new TypeScriptRenderer();
149+
expect(ts.render(target)).toMatchInlineSnapshot(`
150+
"/* eslint-disable prettier/prettier, @stylistic/max-len */
151+
import { foo } from "source";"
152+
`);
153+
});
154+
155+
test('same import with different aliases should keep both', () => {
156+
const source = new Module('source');
157+
const target = new Module('target');
158+
159+
source.importSelective(target, [['foo', 'f1']]);
160+
source.importSelective(target, [['foo', 'f2']]);
161+
162+
const ts = new TypeScriptRenderer();
163+
expect(ts.render(target)).toMatchInlineSnapshot(`
164+
"/* eslint-disable prettier/prettier, @stylistic/max-len */
165+
import { foo as f1, foo as f2 } from "source";"
166+
`);
167+
});

0 commit comments

Comments
 (0)