Skip to content

Added support for filtering unused named imports. #155

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
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
20 changes: 16 additions & 4 deletions src/autoProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export function autoPreprocess(
): PreprocessorGroup {
markupTagName = markupTagName.toLocaleLowerCase();

const markupCache: Record<string, string> = {};
const optionsCache: Record<string, any> = {};
const transformers = rest.transformers || (rest as Transformers);
const markupPattern = new RegExp(
Expand Down Expand Up @@ -141,7 +142,11 @@ export function autoPreprocess(
const transformed = await runTransformer(
lang,
getTransformerOptions(lang, alias),
{ content: stripIndent(content), filename },
{
content: stripIndent(content),
filename,
markup: markupCache[svelteFile.filename],
},
);

return {
Expand Down Expand Up @@ -170,7 +175,7 @@ export function autoPreprocess(
const transformed = await runTransformer(
'replace',
transformers.replace,
{ content, filename },
{ content, filename, markup: content },
);

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

/** If no <template> was found, just return the original markup */
if (!templateMatch) {

markupCache[filename] = content;

return { code: content };
}

Expand Down Expand Up @@ -207,14 +215,16 @@ export function autoPreprocess(
content.slice(0, templateMatch.index) +
code +
content.slice(templateMatch.index + fullMatch.length);

markupCache[filename] = code;

return { code, map, dependencies };
},
async script({ content, attributes, filename }) {
const transformResult: Processed = await scriptTransformer({
content,
attributes,
filename,
filename
});

if (transformResult == null) return;
Expand All @@ -226,6 +236,7 @@ export function autoPreprocess(
content: code,
map,
filename,
markup: markupCache[filename]
});

code = transformed.code;
Expand All @@ -251,7 +262,7 @@ export function autoPreprocess(
const transformed = await runTransformer(
'postcss',
transformers.postcss,
{ content: code, map, filename },
{ content: code, map, filename, markup: markupCache[filename] },
);

code = transformed.code;
Expand All @@ -264,6 +275,7 @@ export function autoPreprocess(
content: code,
map,
filename,
markup: markupCache[filename],
});

code = transformed.code;
Expand Down
14 changes: 13 additions & 1 deletion src/processors/typescript.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { Options, PreprocessorGroup } from '../types';
import { concat, parseFile } from '../utils';

const markupCache: Record<string, string> = {};

export default (options: Options.Typescript): PreprocessorGroup => ({
markup({ content, filename }: { content: string; filename: string }) {
markupCache[filename] = content;
return { code: content };
},
async script(svelteFile) {
const { default: transformer } = await import('../transformers/typescript');
const { content, filename, lang, dependencies } = await parseFile(
Expand All @@ -10,7 +16,13 @@ export default (options: Options.Typescript): PreprocessorGroup => ({
);
if (lang !== 'typescript') return { code: content };

const transformed = await transformer({ content, filename, options });
const transformed = await transformer({
content,
filename,
options,
markup: markupCache[svelteFile.filename],
});

return {
...transformed,
dependencies: concat(dependencies, transformed.dependencies),
Expand Down
175 changes: 166 additions & 9 deletions src/transformers/typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,130 @@ const importTransformer: ts.TransformerFactory<ts.SourceFile> = context => {
return node => ts.visitNode(node, visit);
};

const TS_TRANSFORMERS = {
before: [importTransformer],
function findImportUsages(
node: ts.Node,
context: ts.TransformationContext,
): { [name: string]: number } {
const usages: { [name: string]: number } = {};

let locals = new Set<string>();

const enterScope = <T>(action: () => T) => {
const oldLocals = locals;
locals = new Set([...locals]);
const result = action();
locals = oldLocals;
return result;
};

const findUsages: ts.Visitor = node => {
if (ts.isImportClause(node)) {
const bindings = node.namedBindings;
if (bindings && 'elements' in bindings) {
bindings.elements.forEach(
binding => (usages[binding.name.escapedText as string] = 0),
);
}
return node;
}

if (ts.isFunctionDeclaration(node)) {
return enterScope(() => {
node.parameters
.map(p => p.name)
.filter(ts.isIdentifier)
.forEach(p => locals.add(p.escapedText as string));
return ts.visitEachChild(node, child => findUsages(child), context);
});
}

if (ts.isBlock(node)) {
return enterScope(() =>
ts.visitEachChild(node, child => findUsages(child), context),
);
}

if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
locals.add(node.name.escapedText as string);
} else if (ts.isIdentifier(node)) {
const identifier = node.escapedText as string;
if (!locals.has(identifier) && usages[identifier] != undefined) {
usages[identifier]++;
}
}

return ts.visitEachChild(node, child => findUsages(child), context);
};

ts.visitNode(node, findUsages);

return usages;
}

const removeNonEmittingImports: (
mainFile?: string,
) => ts.TransformerFactory<ts.SourceFile> = mainFile => context => {
function createVisitor(usages: { [name: string]: number }) {
const visit: ts.Visitor = node => {
if (ts.isImportDeclaration(node)) {
let importClause = node.importClause;
const bindings = importClause.namedBindings;

if (bindings && ts.isNamedImports(bindings)) {
const namedImports = bindings.elements.filter(
element => usages[element.name.getText()] > 0,
);

if (namedImports.length !== bindings.elements.length) {
return ts.createImportDeclaration(
node.decorators,
node.modifiers,
namedImports.length == 0
? undefined
: ts.createImportClause(
importClause.name,
ts.createNamedImports(namedImports),
),
node.moduleSpecifier,
);
}
}

return node;
}

if (
ts.isVariableStatement(node) &&
node.modifiers &&
node.modifiers[0] &&
node.modifiers[0].kind == ts.SyntaxKind.ExportKeyword
) {
const name = node.declarationList.declarations[0].name;

if (ts.isIdentifier(name) && name.escapedText == '___used_tags__') {
return undefined;
}
}

return ts.visitEachChild(node, child => visit(child), context);
};

return visit;
}

return node =>
!mainFile || ts.sys.resolvePath(node.fileName) === mainFile
? ts.visitNode(node, createVisitor(findImportUsages(node, context)))
: node;
};

function createTransforms(mainFile?: string) {
return {
before: [importTransformer],
after: [removeNonEmittingImports(mainFile)],
};
}

const TS2552_REGEX = /Cannot find name '\$([a-zA-Z0-9_]+)'. Did you mean '([a-zA-Z0-9_]+)'\?/i;
function isValidSvelteReactiveValueDiagnostic(
filename: string,
Expand All @@ -103,6 +223,25 @@ function isValidSvelteReactiveValueDiagnostic(
return !(usedVar && proposedVar && usedVar === proposedVar);
}

function findTagsInMarkup(markup: string) {
if (!markup) {
return [];
}

let match: RegExpExecArray;
const result: string[] = [];
const findTag = /<([A-Z][^\s\/>]*)([\s\S]*?)>/g;
const template = markup
.replace(/<script([\s\S]*?)(?:>([\s\S]*)<\/script>|\/>)/g, '')
.replace(/<style([\s\S]*?)(?:>([\s\S]*)<\/style>|\/>)/g, '');

while ((match = findTag.exec(template)) !== null) {
result.push(match[1]);
}

return result;
}

function compileFileFromMemory(
compilerOptions: CompilerOptions,
{ filename, content }: { filename: string; content: string },
Expand All @@ -112,7 +251,7 @@ function compileFileFromMemory(

const realHost = ts.createCompilerHost(compilerOptions, true);
const dummyFileName = ts.sys.resolvePath(filename);

const dummyBaseName = basename(dummyFileName);
const isDummyFile = (fileName: string) =>
ts.sys.resolvePath(fileName) === dummyFileName;

Expand Down Expand Up @@ -142,10 +281,13 @@ function compileFileFromMemory(
readFile: fileName =>
isDummyFile(fileName) ? content : realHost.readFile(fileName),
writeFile: (fileName, data) => {
if (fileName.endsWith('.map')) {
map = data;
} else {
code = data;
switch (basename(fileName)) {
case dummyBaseName + '.js.map':
map = data;
break;
case dummyBaseName + '.js':
code = data;
break;
}
},
directoryExists:
Expand All @@ -162,12 +304,13 @@ function compileFileFromMemory(
};

const program = ts.createProgram([dummyFileName], compilerOptions, host);

const emitResult = program.emit(
undefined,
undefined,
undefined,
undefined,
TS_TRANSFORMERS,
createTransforms(dummyFileName),
);

// collect diagnostics without svelte import errors
Expand All @@ -187,6 +330,7 @@ const transformer: Transformer<Options.Typescript> = ({
content,
filename,
options,
markup,
}) => {
// default options
const compilerOptionsJSON = {
Expand Down Expand Up @@ -242,15 +386,28 @@ const transformer: Transformer<Options.Typescript> = ({
);
}

// Force module kind to es2015, so we keep the correct names.
compilerOptions.module = ts.ModuleKind.ES2015;

// Generate separate source maps.
compilerOptions.sourceMap = true;
compilerOptions.inlineSourceMap = false;

const tagsInMarkup = findTagsInMarkup(markup);

let code, map, diagnostics;

// Append all used tags
content += '\nexport const __used_tags__=[' + tagsInMarkup.join(',') + '];';

if (options.transpileOnly || compilerOptions.transpileOnly) {
({ outputText: code, sourceMapText: map, diagnostics } = ts.transpileModule(
content,
{
fileName: filename,
compilerOptions: compilerOptions,
reportDiagnostics: options.reportDiagnostics !== false,
transformers: TS_TRANSFORMERS,
transformers: createTransforms(),
},
));
} else {
Expand Down
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface TransformerArgs<T> {
map?: string | object;
dianostics?: Array<unknown>;
options?: T;
markup?: string;
}

export type Processed = SvelteProcessed & {
Expand Down
1 change: 1 addition & 0 deletions src/types/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,5 @@ export interface Typescript {
tsconfigDirectory?: string | boolean;
transpileOnly?: boolean;
reportDiagnostics?: boolean;
removeNonEmittingImports?: boolean;
}
5 changes: 3 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,13 +130,13 @@ const TRANSFORMERS = {} as {
export const runTransformer = async (
name: string,
options: TransformerOptions<any>,
{ content, map, filename }: TransformerArgs<any>,
{ content, map, filename, markup }: TransformerArgs<any>,
): Promise<ReturnType<Transformer<unknown>>> => {
// remove any unnecessary indentation (useful for coffee, pug and sugarss)
content = stripIndent(content);

if (typeof options === 'function') {
return options({ content, map, filename });
return options({ content, map, filename, markup });
}

try {
Expand All @@ -150,6 +150,7 @@ export const runTransformer = async (
return TRANSFORMERS[name]({
content,
filename,
markup,
map,
options: typeof options === 'boolean' ? null : options,
});
Expand Down
Loading