如何使用打字稿编译器 api 更新或插入导入?
How to update or insert to import using typescript compiler api?
我使用打字稿编译器 api 中的 transform
函数来更改我的代码。
这个函数是递归的,访问每个节点。
当我找到 foo
的 StringLiteral
时,我想添加 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 });
您可以使用访问者遍历 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";
的情况,但应该很容易添加,具体取决于您要如何处理它。
我使用打字稿编译器 api 中的 transform
函数来更改我的代码。
这个函数是递归的,访问每个节点。
当我找到 foo
的 StringLiteral
时,我想添加 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 });
您可以使用访问者遍历 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";
的情况,但应该很容易添加,具体取决于您要如何处理它。