Search code examples
typescriptwebpackdecoratoruglifyjs

Strip Typescript decorator in release build


I have debug-time Typescript decorator @log, which logs input/output/stats of the decorated functions.

I'd like to totally strip this particular @log decorator when compiling release version.

It is easy to remove console.log statements from the release build or do things conditionally in the decorator code, but I would like to make sure there's no overhead with calling decorator function itself.

Is there any way this can be achieved with Typescript?

My project is webpack-based. If this is not possible with Typescript, maybe this can be done at later stage with Babel plugin, with UglifyJS or some other alternative plugin?


Solution

  • This compile-time Transformer removes Decorators from elements and from named imports.
    I'll keep the code updated, as it's just a (working) test.

    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;
    };
    

    program.emit(
      program.getSourceFile('test.component.ts'),
      undefined,
      undefined,
      undefined,
      {
        before: [stripDecorators(['Stateful', 'StatefulTwo', 'StatefulThree'])]
      }
    );
    

    Input:

    import { Stateful, StatefulThree, StatefulTwo } from './decorators';
    
    @Stateful
    @StatefulTwo
    @StatefulThree
    export class Example {
      private str = '';
    
      getStr(): string {
        return this.str;
      }
    }
    

    JS output:

    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    class Example {
        constructor() {
            this.str = '';
        }
        getStr() {
            return this.str;
        }
    }
    exports.Example = Example;