Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 31 additions & 5 deletions packages/@cdklabs/typewriter/src/renderer/typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,16 +96,42 @@ export class TypeScriptRenderer extends Renderer {
}

protected renderImports(mod: Module) {
const selectiveBySource = new Map<string, SelectiveModuleImport[]>();
const aliased: AliasedModuleImport[] = [];

for (const imp of mod.imports) {
if (imp instanceof AliasedModuleImport) {
this.emit(`import * as ${imp.importAlias} from "${imp.moduleSource}";\n`);
aliased.push(imp);
} else if (imp instanceof SelectiveModuleImport) {
const names = imp.importedNames.map((name) => {
const imports = selectiveBySource.get(imp.moduleSource) ?? [];
imports.push(imp);
selectiveBySource.set(imp.moduleSource, imports);
}
}

for (const imp of aliased) {
this.emit(`import * as ${imp.importAlias} from "${imp.moduleSource}";\n`);
}

for (const [source, imports] of selectiveBySource) {
const allImports = new Map<string, string | undefined>();
for (const imp of imports) {
for (const name of imp.importedNames) {
const alias = imp.getAlias(name);
return alias ? `${name} as ${alias}` : name;
});
this.emit(`import { ${names.join(', ')} } from "${imp.moduleSource}";\n`);
const key = alias ? `${name}:${alias}` : name;
allImports.set(key, alias);
}
}
const sorted = Array.from(allImports.entries()).sort((a, b) => {
const nameA = a[0].split(':')[0];
const nameB = b[0].split(':')[0];
return nameA.localeCompare(nameB);
});
const names = sorted.map(([key, alias]) => {
const name = key.split(':')[0];
return alias ? `${name} as ${alias}` : name;
});
this.emit(`import { ${names.join(', ')} } from "${source}";\n`);
}

if (mod.imports.length > 0) {
Expand Down
4 changes: 2 additions & 2 deletions packages/@cdklabs/typewriter/test/aliased-imports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ test('selective import with multiple aliases using tuples', () => {
const ts = new TypeScriptRenderer();
expect(ts.render(target)).toMatchInlineSnapshot(`
"/* eslint-disable prettier/prettier, @stylistic/max-len */
import { foo as bar, baz as qux } from "source";"
import { baz as qux, foo as bar } from "source";"
`);
});

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

Expand Down
167 changes: 167 additions & 0 deletions packages/@cdklabs/typewriter/test/import-deduplication.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { Module, TypeScriptRenderer } from '../src';

test('duplicate selective imports from same source should merge', () => {
const source = new Module('source');
const target = new Module('target');

source.importSelective(target, ['foo']);
source.importSelective(target, ['bar']);

const ts = new TypeScriptRenderer();
expect(ts.render(target)).toMatchInlineSnapshot(`
"/* eslint-disable prettier/prettier, @stylistic/max-len */
import { bar, foo } from "source";"
`);
});

test('selective imports with aliases from same source should merge', () => {
const source = new Module('source');
const target = new Module('target');

source.importSelective(target, [['foo', 'f']]);
source.importSelective(target, [['bar', 'b']]);

const ts = new TypeScriptRenderer();
expect(ts.render(target)).toMatchInlineSnapshot(`
"/* eslint-disable prettier/prettier, @stylistic/max-len */
import { bar as b, foo as f } from "source";"
`);
});

test('mixed regular and aliased selective imports from same source should merge', () => {
const source = new Module('source');
const target = new Module('target');

source.importSelective(target, ['foo']);
source.importSelective(target, [['bar', 'b']]);
source.importSelective(target, ['baz']);

const ts = new TypeScriptRenderer();
expect(ts.render(target)).toMatchInlineSnapshot(`
"/* eslint-disable prettier/prettier, @stylistic/max-len */
import { bar as b, baz, foo } from "source";"
`);
});

test('imports from different sources should remain separate', () => {
const source1 = new Module('source1');
const source2 = new Module('source2');
const target = new Module('target');

source1.importSelective(target, ['foo']);
source2.importSelective(target, ['bar']);

const ts = new TypeScriptRenderer();
expect(ts.render(target)).toMatchInlineSnapshot(`
"/* eslint-disable prettier/prettier, @stylistic/max-len */
import { foo } from "source1";
import { bar } from "source2";"
`);
});

test('aliased module imports should not merge with selective imports', () => {
const source = new Module('source');
const target = new Module('target');

source.import(target, 'S');
source.importSelective(target, ['foo']);

const ts = new TypeScriptRenderer();
expect(ts.render(target)).toMatchInlineSnapshot(`
"/* eslint-disable prettier/prettier, @stylistic/max-len */
import * as S from "source";
import { foo } from "source";"
`);
});

test('multiple aliased module imports from same source should remain separate', () => {
const source = new Module('source');
const target = new Module('target');

source.import(target, 'S1');
source.import(target, 'S2');

const ts = new TypeScriptRenderer();
expect(ts.render(target)).toMatchInlineSnapshot(`
"/* eslint-disable prettier/prettier, @stylistic/max-len */
import * as S1 from "source";
import * as S2 from "source";"
`);
});

test('deduplication with custom fromLocation', () => {
const source = new Module('source');
const target = new Module('target');

source.importSelective(target, ['foo'], { fromLocation: './custom' });
source.importSelective(target, ['bar'], { fromLocation: './custom' });

const ts = new TypeScriptRenderer();
expect(ts.render(target)).toMatchInlineSnapshot(`
"/* eslint-disable prettier/prettier, @stylistic/max-len */
import { bar, foo } from "./custom";"
`);
});

test('different fromLocations should not merge', () => {
const source = new Module('source');
const target = new Module('target');

source.importSelective(target, ['foo'], { fromLocation: './path1' });
source.importSelective(target, ['bar'], { fromLocation: './path2' });

const ts = new TypeScriptRenderer();
expect(ts.render(target)).toMatchInlineSnapshot(`
"/* eslint-disable prettier/prettier, @stylistic/max-len */
import { foo } from "./path1";
import { bar } from "./path2";"
`);
});

test('complex scenario with multiple sources and mixed imports', () => {
const source1 = new Module('source1');
const source2 = new Module('source2');
const target = new Module('target');

source1.importSelective(target, ['a']);
source2.import(target, 'S2');
source1.importSelective(target, [['b', 'B']]);
source2.importSelective(target, ['c']);
source1.importSelective(target, ['d']);

const ts = new TypeScriptRenderer();
expect(ts.render(target)).toMatchInlineSnapshot(`
"/* eslint-disable prettier/prettier, @stylistic/max-len */
import * as S2 from "source2";
import { a, b as B, d } from "source1";
import { c } from "source2";"
`);
});

test('same import added twice should deduplicate', () => {
const source = new Module('source');
const target = new Module('target');

source.importSelective(target, ['foo']);
source.importSelective(target, ['foo']);

const ts = new TypeScriptRenderer();
expect(ts.render(target)).toMatchInlineSnapshot(`
"/* eslint-disable prettier/prettier, @stylistic/max-len */
import { foo } from "source";"
`);
});

test('same import with different aliases should keep both', () => {
const source = new Module('source');
const target = new Module('target');

source.importSelective(target, [['foo', 'f1']]);
source.importSelective(target, [['foo', 'f2']]);

const ts = new TypeScriptRenderer();
expect(ts.render(target)).toMatchInlineSnapshot(`
"/* eslint-disable prettier/prettier, @stylistic/max-len */
import { foo as f1, foo as f2 } from "source";"
`);
});
Loading