Skip to content

Instantly share code, notes, and snippets.

@AviVahl
Last active August 29, 2019 12:34
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save AviVahl/40e031bd72c7264890f349020d04130a to your computer and use it in GitHub Desktop.
Save AviVahl/40e031bd72c7264890f349020d04130a to your computer and use it in GitHub Desktop.
TypeScript transformer to remove .js from import and export statements
// https://github.com/Microsoft/TypeScript/issues/16577#issuecomment-343699395
const path = require('path');
const ts = require('typescript');
const { isImportDeclaration, isExportDeclaration, isStringLiteral } = require('tsutils/typeguard/node');
function getCustomTransformers() {
return { before: [stripJsExt] }
function stripJsExt(context) {
return sourceFile => visitNode(sourceFile);
function visitNode(node) {
if ((isImportDeclaration(node) || isExportDeclaration(node)) &&
node.moduleSpecifier && isStringLiteral(node.moduleSpecifier)) {
const targetModule = node.moduleSpecifier.text;
if (targetModule.endsWith('.js')) {
const newTarget = targetModule.slice(0, targetModule.length - 3);
return isImportDeclaration(node) ?
ts.updateImportDeclaration(
node,
node.decorators,
node.modifiers,
node.importClause,
ts.createLiteral(newTarget)
) :
ts.updateExportDeclaration(
node,
node.decorators,
node.modifiers,
node.exportClause,
ts.createLiteral(newTarget)
);
}
}
return ts.visitEachChild(node, visitNode, context);
}
}
}
module.exports = {
entry: './src/index.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'app.bundle.js'
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.json']
},
module: {
rules: [
{ test: /\.tsx?$/, loader: 'ts-loader', options: { getCustomTransformers } }
]
}
};
@lppedd
Copy link

lppedd commented Aug 29, 2019

@AviVahl Thanks for the example. What about removing a single import in, e.g.

import { One, Two } from './module`;

@AviVahl
Copy link
Author

AviVahl commented Aug 29, 2019

@lppedd do you mean removing a single imported value? like One or Two? that would require updating the importClause.
or did I miss-understand your question?

On a side note: a useful AST viewer to understand which nodes are involved:
https://ts-ast-viewer.com

@lppedd
Copy link

lppedd commented Aug 29, 2019

@AviVahl no no, you understood correctly.
I've managed to code this piece, which deletes imports included in an inputted array.

if (ts.isNamedImports(node)) {
  const namedImports = node as ts.NamedImports;
  const newElements = namedImports.elements.filter(
    v => !decoratorNames.includes(v.name.text)
  );
  
  ts.updateNamedImports(namedImports, newElements);
}

Seems good to you?
I'd also like to entirely remove the import if newElements.length === 0, but I wasn't able to do it.

@lppedd
Copy link

lppedd commented Aug 29, 2019

I've managed to do it, but maybe there is a cleaner way. Basically I save the ImportDeclaration reference and later I look for it.
The complete code is

export default (decorators: string[]) => {
  const importDeclarationsToRemove = [] as ts.ImportDeclaration[];

  const updateNamedImports = (node: ts.NamedImports) => {
    const newElements = node.elements.filter(v => !decorators.includes(v.name.getText()));

    if (newElements.length > 0) {
      ts.updateNamedImports(node, newElements);
    } else {
      importDeclarationsToRemove.push(node.parent.parent);
    }
  };

  const createVisitor = (
    context: ts.TransformationContext
  ): ((node: ts.Node) => ts.VisitResult<ts.Node>) => {
    const visitor: ts.Visitor = (node: ts.Node): ts.VisitResult<any> => {
      // Remove Decorators from imports
      if (ts.isNamedImports(node)) {
        updateNamedImports(node);
      }

      // Remove Decorators applied to elements
      if (ts.isDecorator(node)) {
        const decorator = node as ts.Decorator;
        const identifier = decorator.getChildAt(1) as ts.Identifier;

        if (decorators.includes(identifier.getText())) {
          return undefined;
        }
      }

      const resultNode = ts.visitEachChild(node, visitor, context);
      const index = importDeclarationsToRemove.findIndex(id => id === resultNode);

      if (index !== -1) {
        importDeclarationsToRemove.splice(index, 1);
        return undefined;
      }

      return resultNode;
    };

    return visitor;
  };

  return (context: ts.TransformationContext) => (sourceFile: ts.SourceFile) =>
    sourceFile.fileName.endsWith('component.ts')
      ? ts.visitNode(sourceFile, createVisitor(context))
      : sourceFile;
};

@AviVahl
Copy link
Author

AviVahl commented Aug 29, 2019

you can update the sourceFile's statements field... filtering out top level imports/exports you want to filter.
I've got several cool transformers (in several patterns) implemented here: https://github.com/AviVahl/ts-tools/tree/master/packages/robotrix
might be useful for reference.

@lppedd
Copy link

lppedd commented Aug 29, 2019

@AviVahl Thanks! So you're saying I can simply .statements.filter(...) inside the visitor?

@AviVahl
Copy link
Author

AviVahl commented Aug 29, 2019

you need to ts.updateSourceFile with the filtered statements, but yes.

@lppedd
Copy link

lppedd commented Aug 29, 2019

@AviVahl understood. Thanks again for the useful advices!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment