如何使用打字稿编译器 api 更新或插入导入?

How to update or insert to import using typescript compiler api?

我使用打字稿编译器 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
]);

但这些功能已弃用。我不能 return ts.createImportDeclaration 因为我会得到 import 而不是 foo.

StringLiteral 代码

是否有函数说“从 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

您可以使用访问者遍历 AST 并存储哪些成员已被使用,而无需编辑任何节点。之后(仍在转换中),您可以 find/upsert sourceFile 上的 ImportDeclaration 访问了哪些成员:

/**
 * 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集与importDeclaration.importClause.namedImports中现有的importDeclaration.importClause.namedImports数组合并17=]。这也不处理现有语句 import * as myLib from "my-lib";/import "my-lib"; 的情况,但应该很容易添加,具体取决于您要如何处理它。