Skip to content

Commit 32dd907

Browse files
author
Maarten van Sambeek
committed
Added support for filtering unused named imports.Uses the AST to check if named imports are used in the transpiled code. If not, filters out the import.This way non-emitting types will never be imported so we don't get the following error:[!] Error: '<Interface>' is not exported by <TypeScriptFile>.ts, imported by <SvelteFile>.svelte
1 parent 2dbac89 commit 32dd907

File tree

3 files changed

+132
-2
lines changed

3 files changed

+132
-2
lines changed

src/transformers/typescript.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,112 @@ const importTransformer: ts.TransformerFactory<ts.SourceFile> = context => {
8080
return node => ts.visitNode(node, visit);
8181
};
8282

83+
function findImportUsages(
84+
node: ts.Node,
85+
context: ts.TransformationContext,
86+
): { [name: string]: number } {
87+
const usages: { [name: string]: number } = {};
88+
let locals = new Set<string>();
89+
90+
const enterScope = <T>(action: () => T) => {
91+
const parentLocals = locals;
92+
locals = new Set([...locals]);
93+
const result = action();
94+
locals = parentLocals;
95+
return result;
96+
};
97+
98+
const findUsages: ts.Visitor = node => {
99+
if (ts.isImportClause(node)) {
100+
const bindings = node.namedBindings;
101+
102+
if (bindings && 'elements' in bindings) {
103+
bindings.elements.forEach(
104+
binding => (usages[binding.name.getText()] = 0),
105+
);
106+
}
107+
108+
return node;
109+
}
110+
111+
if (ts.isFunctionDeclaration(node)) {
112+
return enterScope(() => {
113+
node.parameters
114+
.map(p => p.name)
115+
.filter(ts.isIdentifier)
116+
.forEach(p => locals.add(p.getText()));
117+
return ts.visitEachChild(node, child => findUsages(child), context);
118+
});
119+
}
120+
121+
if (ts.isBlock(node)) {
122+
return enterScope(() =>
123+
ts.visitEachChild(node, child => findUsages(child), context),
124+
);
125+
}
126+
127+
if (ts.isVariableDeclaration(node)) {
128+
if (ts.isIdentifier(node.name)) {
129+
locals.add(node.name.getText());
130+
}
131+
} else if (ts.isIdentifier(node)) {
132+
const identifier = node.getText();
133+
if (!locals.has(identifier) && usages[identifier] != undefined) {
134+
usages[identifier]++;
135+
}
136+
}
137+
138+
return ts.visitEachChild(node, child => findUsages(child), context);
139+
};
140+
141+
ts.visitNode(node, findUsages);
142+
143+
return usages;
144+
}
145+
146+
const removeNonEmittingImports: ts.TransformerFactory<ts.SourceFile> = context => {
147+
function createVisitor(usages: { [name: string]: number }) {
148+
const visit: ts.Visitor = node => {
149+
if (ts.isImportDeclaration(node)) {
150+
let importClause = node.importClause;
151+
const bindings = importClause.namedBindings;
152+
153+
if (bindings && ts.isNamedImports(bindings)) {
154+
const namedImports = bindings.elements.filter(
155+
element => usages[element.name.getText()] > 0,
156+
);
157+
158+
if (namedImports.length !== bindings.elements.length) {
159+
return ts.createImportDeclaration(
160+
node.decorators,
161+
node.modifiers,
162+
namedImports.length == 0
163+
? undefined
164+
: ts.createImportClause(
165+
importClause.name,
166+
ts.createNamedImports(namedImports)
167+
),
168+
node.moduleSpecifier,
169+
);
170+
}
171+
}
172+
173+
return node;
174+
}
175+
176+
return ts.visitEachChild(node, child => visit(child), context);
177+
};
178+
179+
return visit;
180+
}
181+
182+
return node =>
183+
ts.visitNode(node, createVisitor(findImportUsages(node, context)));
184+
};
185+
83186
const TS_TRANSFORMERS = {
84187
before: [importTransformer],
188+
after: [removeNonEmittingImports],
85189
};
86190

87191
const TS2552_REGEX = /Cannot find name '\$([a-zA-Z0-9_]+)'. Did you mean '([a-zA-Z0-9_]+)'\?/i;

test/fixtures/exports.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export interface IFace {
2+
readonly value: string;
3+
}
4+
5+
export class ExportedClass {
6+
}

test/transformers/typescript.test.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,27 @@ describe('transformer - typescript', () => {
3131
'script.ts',
3232
)}</script>`;
3333

34+
it('should remove unused named imports', async () => {
35+
const { code } = await transpile(
36+
'import { IFace, ExportedClass } from "./fixtures/exports"',
37+
);
38+
expect(code).toBe('import "./fixtures/exports";\n');
39+
});
40+
41+
it('should remove unused named imports but leave used ones', async () => {
42+
const { code } = await transpile(
43+
'import { IFace, ExportedClass } from "./fixtures/exports"\nnew ExportedClass()',
44+
);
45+
expect(code).toBe('import { ExportedClass } from "./fixtures/exports";\nnew ExportedClass();\n');
46+
});
47+
48+
it('should remove unused named imports that conflict with local variables', async () => {
49+
const { code } = await transpile(
50+
'import { IFace as name } from "./fixtures/exports"\nexport let iface: name\nfunction func(name: string) { return name }',
51+
);
52+
expect(code).toBe('import "./fixtures/exports";\nexport let iface;\nfunction func(name) { return name; }\n');
53+
});
54+
3455
it('should disallow transpilation to es5 or lower', async () => {
3556
expect(
3657
transpile('export let foo = 10', { target: 'es3' }),
@@ -151,7 +172,7 @@ describe('transformer - typescript', () => {
151172
);
152173
expect(diagnostics.some(d => d.code === 2552)).toBe(false);
153174
});
154-
175+
155176
it('should report a mismatched variable name error', async () => {
156177
const { diagnostics } = await transpile(
157178
`
@@ -161,6 +182,5 @@ describe('transformer - typescript', () => {
161182
);
162183
expect(diagnostics.some(d => d.code === 2552)).toBe(true);
163184
});
164-
165185
});
166186
});

0 commit comments

Comments
 (0)