Skip to content

Commit d1df002

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 d1df002

File tree

10 files changed

+297
-18
lines changed

10 files changed

+297
-18
lines changed

src/autoProcess.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export function autoPreprocess(
8383
): PreprocessorGroup {
8484
markupTagName = markupTagName.toLocaleLowerCase();
8585

86+
const markupCache: Record<string, string> = {};
8687
const optionsCache: Record<string, any> = {};
8788
const transformers = rest.transformers || (rest as Transformers);
8889
const markupPattern = new RegExp(
@@ -141,7 +142,11 @@ export function autoPreprocess(
141142
const transformed = await runTransformer(
142143
lang,
143144
getTransformerOptions(lang, alias),
144-
{ content: stripIndent(content), filename },
145+
{
146+
content: stripIndent(content),
147+
filename,
148+
markup: markupCache[svelteFile.filename],
149+
},
145150
);
146151

147152
return {
@@ -170,7 +175,7 @@ export function autoPreprocess(
170175
const transformed = await runTransformer(
171176
'replace',
172177
transformers.replace,
173-
{ content, filename },
178+
{ content, filename, markup: content },
174179
);
175180

176181
content = transformed.code;
@@ -180,6 +185,9 @@ export function autoPreprocess(
180185

181186
/** If no <template> was found, just return the original markup */
182187
if (!templateMatch) {
188+
189+
markupCache[filename] = content;
190+
183191
return { code: content };
184192
}
185193

@@ -207,14 +215,16 @@ export function autoPreprocess(
207215
content.slice(0, templateMatch.index) +
208216
code +
209217
content.slice(templateMatch.index + fullMatch.length);
218+
219+
markupCache[filename] = code;
210220

211221
return { code, map, dependencies };
212222
},
213223
async script({ content, attributes, filename }) {
214224
const transformResult: Processed = await scriptTransformer({
215225
content,
216226
attributes,
217-
filename,
227+
filename
218228
});
219229

220230
if (transformResult == null) return;
@@ -226,6 +236,7 @@ export function autoPreprocess(
226236
content: code,
227237
map,
228238
filename,
239+
markup: markupCache[filename]
229240
});
230241

231242
code = transformed.code;
@@ -251,7 +262,7 @@ export function autoPreprocess(
251262
const transformed = await runTransformer(
252263
'postcss',
253264
transformers.postcss,
254-
{ content: code, map, filename },
265+
{ content: code, map, filename, markup: markupCache[filename] },
255266
);
256267

257268
code = transformed.code;
@@ -264,6 +275,7 @@ export function autoPreprocess(
264275
content: code,
265276
map,
266277
filename,
278+
markup: markupCache[filename],
267279
});
268280

269281
code = transformed.code;

src/processors/typescript.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { Options, PreprocessorGroup } from '../types';
22
import { concat, parseFile } from '../utils';
33

4+
const markupCache: Record<string, string> = {};
5+
46
export default (options: Options.Typescript): PreprocessorGroup => ({
7+
markup({ content, filename }: { content: string; filename: string }) {
8+
markupCache[filename] = content;
9+
return { code: content };
10+
},
511
async script(svelteFile) {
612
const { default: transformer } = await import('../transformers/typescript');
713
const { content, filename, lang, dependencies } = await parseFile(
@@ -10,7 +16,13 @@ export default (options: Options.Typescript): PreprocessorGroup => ({
1016
);
1117
if (lang !== 'typescript') return { code: content };
1218

13-
const transformed = await transformer({ content, filename, options });
19+
const transformed = await transformer({
20+
content,
21+
filename,
22+
markup: markupCache[svelteFile.filename],
23+
options,
24+
});
25+
1426
return {
1527
...transformed,
1628
dependencies: concat(dependencies, transformed.dependencies),

src/transformers/typescript.ts

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

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

200+
function createTransforms(mainFile?: string) {
201+
return {
202+
before: [importTransformer],
203+
after: [removeNonEmittingImports(mainFile)],
204+
};
205+
}
206+
87207
const TS2552_REGEX = /Cannot find name '\$([a-zA-Z0-9_]+)'. Did you mean '([a-zA-Z0-9_]+)'\?/i;
88208
function isValidSvelteReactiveValueDiagnostic(
89209
filename: string,
@@ -103,6 +223,25 @@ function isValidSvelteReactiveValueDiagnostic(
103223
return !(usedVar && proposedVar && usedVar === proposedVar);
104224
}
105225

226+
function findTagsInMarkup(markup: string) {
227+
if (!markup) {
228+
return [];
229+
}
230+
231+
let match: RegExpExecArray;
232+
const result: string[] = [];
233+
const findTag = /<([A-Z][^\s\/>]*)([\s\S]*?)>/g;
234+
const template = markup
235+
.replace(/<script([\s\S]*?)(?:>([\s\S]*)<\/script>|\/>)/g, '')
236+
.replace(/<style([\s\S]*?)(?:>([\s\S]*)<\/style>|\/>)/g, '');
237+
238+
while ((match = findTag.exec(template)) !== null) {
239+
result.push(match[1]);
240+
}
241+
242+
return result;
243+
}
244+
106245
function compileFileFromMemory(
107246
compilerOptions: CompilerOptions,
108247
{ filename, content }: { filename: string; content: string },
@@ -112,7 +251,7 @@ function compileFileFromMemory(
112251

113252
const realHost = ts.createCompilerHost(compilerOptions, true);
114253
const dummyFileName = ts.sys.resolvePath(filename);
115-
254+
const dummyBaseName = basename(dummyFileName);
116255
const isDummyFile = (fileName: string) =>
117256
ts.sys.resolvePath(fileName) === dummyFileName;
118257

@@ -142,10 +281,13 @@ function compileFileFromMemory(
142281
readFile: fileName =>
143282
isDummyFile(fileName) ? content : realHost.readFile(fileName),
144283
writeFile: (fileName, data) => {
145-
if (fileName.endsWith('.map')) {
146-
map = data;
147-
} else {
148-
code = data;
284+
switch (basename(fileName)) {
285+
case dummyBaseName + '.js.map':
286+
map = data;
287+
break;
288+
case dummyBaseName + '.js':
289+
code = data.replace(/\/\/# sourceMappingURL=.*$/, '');
290+
break;
149291
}
150292
},
151293
directoryExists:
@@ -162,12 +304,13 @@ function compileFileFromMemory(
162304
};
163305

164306
const program = ts.createProgram([dummyFileName], compilerOptions, host);
307+
165308
const emitResult = program.emit(
166309
undefined,
167310
undefined,
168311
undefined,
169312
undefined,
170-
TS_TRANSFORMERS,
313+
createTransforms(dummyFileName),
171314
);
172315

173316
// collect diagnostics without svelte import errors
@@ -187,6 +330,7 @@ const transformer: Transformer<Options.Typescript> = ({
187330
content,
188331
filename,
189332
options,
333+
markup,
190334
}) => {
191335
// default options
192336
const compilerOptionsJSON = {
@@ -242,15 +386,24 @@ const transformer: Transformer<Options.Typescript> = ({
242386
);
243387
}
244388

389+
// Force module kind to es2015, so we keep the correct names.
390+
compilerOptions.module = ts.ModuleKind.ES2015;
391+
392+
const tagsInMarkup = findTagsInMarkup(markup);
393+
245394
let code, map, diagnostics;
395+
396+
// Append all used tags
397+
content += '\nexport const __used_tags__=[' + tagsInMarkup.join(',') + '];';
398+
246399
if (options.transpileOnly || compilerOptions.transpileOnly) {
247400
({ outputText: code, sourceMapText: map, diagnostics } = ts.transpileModule(
248401
content,
249402
{
250403
fileName: filename,
251404
compilerOptions: compilerOptions,
252405
reportDiagnostics: options.reportDiagnostics !== false,
253-
transformers: TS_TRANSFORMERS,
406+
transformers: createTransforms(),
254407
},
255408
));
256409
} else {

src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export interface TransformerArgs<T> {
2323
map?: string | object;
2424
dianostics?: Array<unknown>;
2525
options?: T;
26+
markup?: string;
2627
}
2728

2829
export type Processed = SvelteProcessed & {

src/types/options.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,5 @@ export interface Typescript {
6060
tsconfigDirectory?: string | boolean;
6161
transpileOnly?: boolean;
6262
reportDiagnostics?: boolean;
63+
removeNonEmittingImports?: boolean;
6364
}

src/utils.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,13 +130,13 @@ const TRANSFORMERS = {} as {
130130
export const runTransformer = async (
131131
name: string,
132132
options: TransformerOptions<any>,
133-
{ content, map, filename }: TransformerArgs<any>,
133+
{ content, map, filename, markup }: TransformerArgs<any>,
134134
): Promise<ReturnType<Transformer<unknown>>> => {
135135
// remove any unnecessary indentation (useful for coffee, pug and sugarss)
136136
content = stripIndent(content);
137137

138138
if (typeof options === 'function') {
139-
return options({ content, map, filename });
139+
return options({ content, map, filename, markup });
140140
}
141141

142142
try {
@@ -150,6 +150,7 @@ export const runTransformer = async (
150150
return TRANSFORMERS[name]({
151151
content,
152152
filename,
153+
markup,
153154
map,
154155
options: typeof options === 'boolean' ? null : options,
155156
});

test/fixtures/References.svelte

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script lang="typescript">
2+
import { Box, Circle, Triangle, Dot } from "./fixtures/shapes"
3+
4+
export let triangle: Dot;
5+
</script>
6+
7+
<Box color="green">
8+
<Circle color="orange">
9+
</Circle>
10+
</Box>

0 commit comments

Comments
 (0)