Skip to content

Commit ab6357a

Browse files
authored
feat(typewriter): support aliased imports with tuple syntax (#2272)
This PR adds support for aliased selective imports using an intuitive tuple syntax in the typewriter package. ## Changes - Updated `importSelective()` to accept mixed arrays of strings and tuples: `['regular', ['name', 'alias']]` - Added `addAliasedImport()` method for dynamic aliased imports - Introduced internal `aliasMap` to track name-to-alias mappings - Updated `linkSymbol()` to handle both regular and aliased imports - Modified TypeScript renderer to emit proper `import { name as alias }` syntax - Added comprehensive test coverage with inline snapshots - Added extensive JSDoc documentation with examples ## Usage ```ts // Mixed regular and aliased imports source.importSelective(target, ['RegularImport', ['LongName', 'Short']]); // Generates: import { RegularImport, LongName as Short } from "source"; // Avoiding name conflicts reactModule.importSelective(myModule, [['Component', 'ReactComponent'], 'useState']); // Generates: import { Component as ReactComponent, useState } from "react"; ```
1 parent 4740f3e commit ab6357a

File tree

3 files changed

+165
-9
lines changed

3 files changed

+165
-9
lines changed

packages/@cdklabs/typewriter/src/module.ts

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,35 @@ export class Module extends ScopeImpl {
9393
}
9494

9595
/**
96-
* Import the current module into the target module
96+
* Import the current module into the target module with selective imports
97+
*
98+
* @param intoModule - The module to import into
99+
* @param names - Array of import names. Each element can be:
100+
* - A string for regular imports: `'foo'` → `import { foo } from '...'`
101+
* - A tuple for aliased imports: `['foo', 'bar']` → `import { foo as bar } from '...'`
102+
* @param props - Additional import properties
103+
* @returns The SelectiveModuleImport instance for further customization
104+
*
105+
* @example Regular and aliased imports
106+
* ```ts
107+
* source.importSelective(target, ['RegularImport', ['LongName', 'Short']]);
108+
* // Generates: import { RegularImport, LongName as Short } from "source";
109+
* ```
110+
*
111+
* @example Avoiding name conflicts
112+
* ```ts
113+
* reactModule.importSelective(myModule, [['Component', 'ReactComponent'], 'useState']);
114+
* // Generates: import { Component as ReactComponent, useState } from "react";
115+
* ```
116+
*
117+
* @example Dynamic imports using returned instance
118+
* ```ts
119+
* const imp = source.importSelective(target, ['existing']);
120+
* imp.addAliasedImport('foo', 'bar');
121+
* // Generates: import { existing, foo as bar } from "source";
122+
* ```
97123
*/
98-
public importSelective(intoModule: Module, names: string[], props: ModuleImportProps = {}) {
124+
public importSelective(intoModule: Module, names: Array<string | [string, string]>, props: ModuleImportProps = {}) {
99125
const imp = new SelectiveModuleImport(this, props.fromLocation ?? this.importName, names);
100126
intoModule.addImport(imp);
101127
return imp;
@@ -157,35 +183,94 @@ export class AliasedModuleImport extends ModuleImport {
157183
* A selective import statement
158184
*
159185
* Import statements get rendered into the source module, and also count as scope linkage.
186+
* Supports both regular imports and aliased imports.
187+
*
188+
* @example Constructor with mixed imports
189+
* ```ts
190+
* new SelectiveModuleImport(module, 'source', ['foo', ['bar', 'baz']]);
191+
* // Generates: import { foo, bar as baz } from "source";
192+
* ```
193+
*
194+
* @example Adding imports dynamically
195+
* ```ts
196+
* const imp = new SelectiveModuleImport(module, 'source');
197+
* imp.addImportedName('foo');
198+
* imp.addAliasedImport('bar', 'baz');
199+
* // Generates: import { foo, bar as baz } from "source";
200+
* ```
160201
*/
161202
export class SelectiveModuleImport extends ModuleImport {
162203
public readonly importedNames: string[] = [];
204+
private readonly aliasMap = new Map<string, string>();
163205
private targetScope?: IScope;
164206

165-
constructor(module: Module, moduleSource: string, importedNames: string[] = []) {
207+
constructor(module: Module, moduleSource: string, importedNames: Array<string | [string, string]> = []) {
166208
super(module, moduleSource);
167209

168210
for (const name of importedNames) {
169-
this.addImportedName(name);
211+
if (Array.isArray(name)) {
212+
this.addAliasedImport(name[0], name[1]);
213+
} else {
214+
this.addImportedName(name);
215+
}
170216
}
171217
}
172218

219+
/**
220+
* Add a regular import name
221+
*
222+
* @param name - The name to import
223+
* @example
224+
* ```ts
225+
* imp.addImportedName('MyClass');
226+
* // Adds to: import { MyClass } from "...";
227+
* ```
228+
*/
173229
public addImportedName(name: string) {
174230
this.importedNames.push(name);
175231
if (this.targetScope) {
176-
this.linkSymbol(name, this.module);
232+
this.linkSymbol(name, name, this.targetScope);
177233
}
178234
}
179235

236+
/**
237+
* Add an aliased import
238+
*
239+
* @param name - The original name in the source module
240+
* @param alias - The alias to use in the target module
241+
* @example
242+
* ```ts
243+
* imp.addAliasedImport('VeryLongClassName', 'Short');
244+
* // Adds to: import { VeryLongClassName as Short } from "...";
245+
* ```
246+
*/
247+
public addAliasedImport(name: string, alias: string) {
248+
this.importedNames.push(name);
249+
this.aliasMap.set(name, alias);
250+
if (this.targetScope) {
251+
this.linkSymbol(name, alias, this.targetScope);
252+
}
253+
}
254+
255+
/**
256+
* Get the alias for an imported name, if any
257+
* @param name - The original name
258+
* @returns The alias, or undefined if no alias exists
259+
*/
260+
public getAlias(name: string): string | undefined {
261+
return this.aliasMap.get(name);
262+
}
263+
180264
public linkInto(scope: IScope): void {
181265
for (const name of this.importedNames) {
182-
this.linkSymbol(name, scope);
266+
const alias = this.aliasMap.get(name) ?? name;
267+
this.linkSymbol(name, alias, scope);
183268
}
184269
this.targetScope = scope;
185270
}
186271

187-
private linkSymbol(name: string, scope: IScope) {
188-
scope.linkSymbol(new ThingSymbol(name, this.module), expr.ident(name));
272+
private linkSymbol(name: string, alias: string, scope: IScope) {
273+
scope.linkSymbol(new ThingSymbol(name, this.module), expr.ident(alias));
189274
}
190275
}
191276

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,11 @@ export class TypeScriptRenderer extends Renderer {
100100
if (imp instanceof AliasedModuleImport) {
101101
this.emit(`import * as ${imp.importAlias} from "${imp.moduleSource}";\n`);
102102
} else if (imp instanceof SelectiveModuleImport) {
103-
this.emit(`import { ${imp.importedNames.join(', ')} } from "${imp.moduleSource}";\n`);
103+
const names = imp.importedNames.map((name) => {
104+
const alias = imp.getAlias(name);
105+
return alias ? `${name} as ${alias}` : name;
106+
});
107+
this.emit(`import { ${names.join(', ')} } from "${imp.moduleSource}";\n`);
104108
}
105109
}
106110

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { Module, TypeScriptRenderer } from '../src';
2+
3+
test('selective import with alias using tuple', () => {
4+
const source = new Module('source');
5+
const target = new Module('target');
6+
7+
source.importSelective(target, [['foo', 'bar']]);
8+
9+
const ts = new TypeScriptRenderer();
10+
expect(ts.render(target)).toMatchInlineSnapshot(`
11+
"/* eslint-disable prettier/prettier, @stylistic/max-len */
12+
import { foo as bar } from "source";"
13+
`);
14+
});
15+
16+
test('selective import with multiple aliases using tuples', () => {
17+
const source = new Module('source');
18+
const target = new Module('target');
19+
20+
source.importSelective(target, [
21+
['foo', 'bar'],
22+
['baz', 'qux'],
23+
]);
24+
25+
const ts = new TypeScriptRenderer();
26+
expect(ts.render(target)).toMatchInlineSnapshot(`
27+
"/* eslint-disable prettier/prettier, @stylistic/max-len */
28+
import { foo as bar, baz as qux } from "source";"
29+
`);
30+
});
31+
32+
test('selective import with mixed regular and aliased', () => {
33+
const source = new Module('source');
34+
const target = new Module('target');
35+
36+
source.importSelective(target, ['regular', ['foo', 'bar']]);
37+
38+
const ts = new TypeScriptRenderer();
39+
expect(ts.render(target)).toMatchInlineSnapshot(`
40+
"/* eslint-disable prettier/prettier, @stylistic/max-len */
41+
import { regular, foo as bar } from "source";"
42+
`);
43+
});
44+
45+
test('aliased import creates proper symbol link', () => {
46+
const source = new Module('source');
47+
const target = new Module('target');
48+
49+
source.importSelective(target, [['foo', 'bar']]);
50+
51+
const sym = target.symbolToExpression({ name: 'foo', scope: source } as any);
52+
expect(sym).toBeDefined();
53+
});
54+
55+
test('addAliasedImport method still works', () => {
56+
const source = new Module('source');
57+
const target = new Module('target');
58+
59+
const imp = source.importSelective(target, []);
60+
imp.addAliasedImport('foo', 'bar');
61+
62+
const ts = new TypeScriptRenderer();
63+
expect(ts.render(target)).toMatchInlineSnapshot(`
64+
"/* eslint-disable prettier/prettier, @stylistic/max-len */
65+
import { foo as bar } from "source";"
66+
`);
67+
});

0 commit comments

Comments
 (0)