【问题标题】:How to update or insert to import using typescript compiler api?如何使用 typescript 编译器 api 更新或插入导入?
【发布时间】:2021-05-28 01:36:54
【问题描述】:

我使用来自 typescript 编译器 api 的 transform 函数来更改我的代码。

这个函数是递归的,访问每个节点。

当我找到fooStringLiteral 时,我想添加foo 来导入my-lib,如下所示:

import { foo } from 'my-lib';

代码可能已经从 my-lib 导入了其他内容,例如:

import { bar } from 'my-lib';

我想避免这种结果(重复导入):

import { foo } from 'my-lib';
import { bar } from 'my-lib';

我能找到的最接近的解决方案是:

const file = (node as ts.Node) as ts.SourceFile;
const update = ts.updateSourceFileNode(file, [
  ts.createImportDeclaration(
    undefined,
    undefined,
    ts.createImportClause(
      undefined,
      ts.createNamedImports([ts.createImportSpecifier(ts.createIdentifier("default"), ts.createIdentifier("salami"))])
    ),
    ts.createLiteral('salami')
  ),
  ...file.statements
]);

但这些功能已被弃用。我不能返回ts.createImportDeclaration,因为我会得到import而不是fooStringLiteral的代码。

是否有函数说“从 x 更新或插入 y 到导入语句”?

到目前为止我能做的代码:

import * as ts from "typescript";

const code = `
console.log('foo');
`;

const node = ts.createSourceFile("x.ts", code, ts.ScriptTarget.Latest);

const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });

export const upsertImport = (context) => (rootNode) => {
  const { factory } = context;

  function visit(node) {
    if (ts.isStringLiteral(node) && node.text === "foo") {
      // need to add: import { foo } from 'my-lib'; but how??
      console.log("need to add: import { foo } from my-lib; but how??");
    }

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

  return ts.visitNode(rootNode, visit);
};

const result = ts.transform(node, [upsertImport]);

const transformedSourceFile = result.transformed[0];

const out = printer.printFile(transformedSourceFile);

console.log({ out });

codesandbox.io

【问题讨论】:

    标签: typescript abstract-syntax-tree


    【解决方案1】:

    您可以使用访问者遍历 AST 并存储已使用的成员,而无需编辑任何节点。之后(仍在转换中),您可以在sourceFile 上找到/插入ImportDeclaration,使用该sourceFile 访问成员:

    /**
     * Helper function for updating an import declaration statement with a new set of members
     * unhandled edge cases include:
     * existing `import "my-lib";`
     * existing `import * as myLib from "my-lib";
     */
    const updateNamedImports = (factory: ts.NodeFactory, node: ts.ImportDeclaration, utilizedMembers: Set<string>): ts.ImportDeclaration => {
        // We're not using updateNamedImports since we've verified
        // which imports are actually being used already
        const namedImports = factory.createNamedImports(
            Array.from(utilizedMembers)
                .map(name =>
                    factory.createImportSpecifier(
                        undefined,
                        factory.createIdentifier(name)
                    )
                )
        )
        let importClause: ts.ImportClause;
        if (node.importClause && node.importClause.namedBindings) {
            importClause = factory.updateImportClause(
                node.importClause,
                node.importClause.isTypeOnly,
                node.importClause.name,
                namedImports
            );
        }
        else {
            importClause = factory.createImportClause(
                false,
                undefined,
                namedImports
            )
        }
        return factory.updateImportDeclaration(
            node,
            node.decorators,
            node.modifiers,
            importClause,
            node.moduleSpecifier
        );
    }
    
    /**
     * Main transform function
     */
    const upsertImport: ts.TransformerFactory<ts.SourceFile> = (context) => (rootNode) => {
        const { factory } = context;
    
        const MY_LIB = "my-lib";
    
        // use a set to keep track of members which have been accessed,
        // and guarantee uniqueness
        const utilizedMembers = new Set<string>();
        // I ventured a guess you want to check more than just "foo"
        const availableMembers = new Set([ "foo", "bar", "baz" ]);
    
        // Find which imports you need
        function collectUtilizedMembers(node: ts.Node): ts.Node {
            if (ts.isStringLiteral(node) && availableMembers.has(node.text)) {
                utilizedMembers.add(node.text);
            }
            return ts.visitEachChild(node, collectUtilizedMembers, context);
        }
    
        // run your visitor which will fill up the `utilizedMembers` set.
        ts.visitNode(rootNode, collectUtilizedMembers);
    
        // find the existing import if it exists using findIndex
        // so we can splice it back into the existing statements
        const matchedImportIdx = rootNode.statements
            .findIndex(s => ts.isImportDeclaration(s)
                && (s.moduleSpecifier as ts.StringLiteral).text === MY_LIB
            );
    
        // if it exists, update it
        if (matchedImportIdx !== -1) {
            const node = rootNode.statements[matchedImportIdx] as ts.ImportDeclaration;
            // update source file with updated import statement
            return factory.updateSourceFile(rootNode, [
                ...rootNode.statements.slice(0, matchedImportIdx),
                updateNamedImports(factory, node, utilizedMembers),
                ...rootNode.statements.slice(matchedImportIdx + 1)
            ]);
        }
        else {
            // if it doesn't exist, create it and insert it at
            // the top of the source file
            return factory.updateSourceFile(rootNode, [
                factory.createImportDeclaration(
                    undefined,
                    undefined,
                    factory.createImportClause(
                        false,
                        undefined,
                        factory.createNamedImports(
                            Array.from(utilizedMembers).map(name =>
                                factory.createImportSpecifier(
                                    undefined,
                                    factory.createIdentifier(name)
                                )
                            )
                        )
                    ),
                    factory.createStringLiteral(MY_LIB)
                ),
                ...rootNode.statements
            ]);
        }
    };
    

    因为此代码检查实际使用了哪些成员,所以它替换了现有的namedImports。如果您想保留现有的(即使它们尚未使用或未包含在availableMembers 中,您可以将utilizedMembers 集合与updateNamedImports 中现有的importDeclaration.importClause.namedImports 数组合并。这也不会t 处理现有语句 import * as myLib from "my-lib";/import "my-lib"; 的情况,但这应该很容易添加,具体取决于您要如何处理它。

    【讨论】:

      猜你喜欢
      • 2020-08-07
      • 1970-01-01
      • 2020-10-23
      • 2018-11-04
      • 1970-01-01
      • 2016-08-15
      • 1970-01-01
      • 1970-01-01
      • 2019-09-18
      相关资源
      最近更新 更多