【问题标题】:Is there a way to modify decorators option at build time?有没有办法在构建时修改装饰器选项?
【发布时间】:2019-07-21 22:11:18
【问题描述】:

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

  • login.component.ts

@Component({ selector: 'login' .... })

进入

@Component({ selector: 'not-use-this-login' .... })

  • custom-login.component.ts

@Component({ selector: 'custom-login' .... })

进入

@Component({ selector: 'login' .... })

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

【问题讨论】:

  • 查看Netanel basal netbasal.com/…的链接
  • 感谢您的回复。自定义装饰器在运行时进行评估,当创建类时,为时已晚。

标签: angular typescript tsc typescript-compiler-api


【解决方案1】:

此答案仅使用编译器 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 {
}

【讨论】:

  • 我已经在 Angular 构建中实现了解决方案,但似乎 updatePropertyAssignment 创建了另一个 stringLiteral 并且不替换前一个。现在在节点中我有 2 个选择器:'login' 和 'not-use-this-login' 也许我错过了一些东西。
  • 在这个例子中有效。也许在问题中发布一些显示问题发生的代码?
  • 通过使用 util ( var util = require('util'); ) 我终于明白了节点对象。你是对的,代码是正确的。我看到两个选择器对象,因为一个被包装到 original: 对象中。无论如何,Angular 编译器并不关心我的修改。我将研究 Angular 代码以更好地理解附加内容。感谢您的支持。
猜你喜欢
  • 2019-06-16
  • 1970-01-01
  • 1970-01-01
  • 2012-05-11
  • 1970-01-01
  • 1970-01-01
  • 2021-03-23
  • 2020-04-14
  • 2016-02-05
相关资源
最近更新 更多