有没有一种方法可以在构建时修改装饰器选项?

时间:2019-07-21 22:11:18

标签: angular typescript tsc typescript-compiler-api

我有一个Angular标准应用程序,我想在构建时切换一些组件。 我想使用一个ast变形器更改@Component装饰器选项,如下所示:

  • login.component.ts

@Component({选择器:'登录'....})

进入

@Component({选择器:'not-use-this-login'....})

  • custom-login.component.ts

@Component({选择器:'custom-login'....})

进入

@Component({选择器:'登录'....})

如果我可以在Angular构建过程之前修改ts文件,我想Angular将呈现custom-login.component.ts而不是标准的。 这可能非常有用,因为我可以为许多客户编译应用程序,而无需更改标准代码。 我阅读了Angular的构建代码,并且它们的功能与注入内嵌html的模板选项非常相似。 我创建了一个github仓库来测试这个技巧: https://github.com/gioboa/ng-ts-transformer @ angular-builders / custom-webpack允许您定义一个额外的webpack配置。通过ts-loader,我将其称为变压器(transformer.js文件)。 我尝试了很多方法来替换选择器,但不幸的是没有成功。 AST API文档的使用情况非常差。

1 个答案:

答案 0 :(得分:0)

此答案仅使用编译器api,因为我对angular或生成过程不太熟悉,但是希望会对您有所帮助,并且您应该能够适应它。

  1. 找到与您要查找的内容匹配的组件装饰器。
  2. 在修饰器的调用表达式的第一个参数的对象文字的属性内转换要更改的字符串文字,该属性是名为“选择器”的初始值设定项的属性分配。

使用我的工具ts-ast-viewer.com帮助查看需要检查的内容...

// Note: This code mixes together the act of analyzing and transforming.
// You may want a stricter separation, but that requires creating an entire
// architecture around this.

import * as ts from "typescript";

// create a source file ast
const sourceFile = ts.createSourceFile("/file.ts", `import { Component } from 'whereever';

@Component({ selector: 'login' })
class Test {
}
`, ts.ScriptTarget.Latest);

// transform it
const transformerFactory: ts.TransformerFactory<ts.SourceFile> = context => {
    return file => visitChangingDecorators(file, context) as ts.SourceFile;
};
const transformationResult = ts.transform(sourceFile, [transformerFactory]);
const transformedSourceFile = transformationResult.transformed[0];

// see the result by printing it
console.log(ts.createPrinter().printFile(transformedSourceFile));

function visitChangingDecorators(node: ts.Node, context: ts.TransformationContext) {
    // visit all the nodes, changing any component decorators
    if (ts.isDecorator(node) && isComponentDecorator(node))
        return handleComponentDecorator(node);
    else {
        return ts.visitEachChild(node,
            child => visitChangingDecorators(child, context), context);
    }
}

function handleComponentDecorator(node: ts.Decorator) {
    const expr = node.expression;
    if (!ts.isCallExpression(expr))
        return node;

    const args = expr.arguments;
    if (args.length !== 1)
        return node;

    const arg = args[0];
    if (!ts.isObjectLiteralExpression(arg))
        return node;

    // Using these update functions on the call expression
    // and decorator is kind of useless. A better implementation
    // would only update the string literal that needs to be updated.
    const updatedCallExpr = ts.updateCall(
        expr,
        expr.expression,
        expr.typeArguments,
        [transformObjectLiteral(arg)]
    );

    return ts.updateDecorator(node, updatedCallExpr);

    function transformObjectLiteral(objectLiteral: ts.ObjectLiteralExpression) {
        return ts.updateObjectLiteral(objectLiteral, objectLiteral.properties.map(prop => {
            if (!ts.isPropertyAssignment(prop))
                return prop;

            if (!prop.name || !ts.isIdentifier(prop.name))
                return prop;

            if (prop.name.escapedText !== "selector")
                return prop;

            if (!ts.isStringLiteral(prop.initializer))
                return prop;

            if (prop.initializer.text === "login") {
                return ts.updatePropertyAssignment(
                    prop,
                    prop.name,
                    ts.createStringLiteral("not-use-this-login")
                );
            }

            return prop;
        }));
    }
}

function isComponentDecorator(node: ts.Decorator) {
    // You will probably want something more sophisticated
    // that analyzes the import declarations or possibly uses
    // the type checker in an initial pass of the source files
    // before transforming. This naively just checks if the
    // decorator is a call expression and if its expression
    // has the text "Component". This definitely won't work
    // in every scenario and might possibly get false positives.
    const expr = node.expression;
    if (!ts.isCallExpression(expr))
        return false;

    if (!ts.isIdentifier(expr.expression))
        return false;

    return expr.expression.escapedText === "Component";
}

输出:

import { Component } from "whereever";
@Component({ selector: "not-use-this-login" })
class Test {
}