abstrakt syntakstræ - Hvordan opdaterer eller indsætter man import ved hjælp af TypeScript-kompilatorens API?
Jeg bruger transform-funktionen fra TypeScript-kompilatorens API til at ændre min kode.
Denne funktion er rekursiv og besøger hver node.
Når jeg fandt en StringLiteral af foo, vil jeg tilføje foo til importen, hvor my-lib som dette:
import { foo } from 'my-lib';
Koden kan allerede have importeret noget andet fra my-lib som:
import { bar } from 'my-lib';
Og jeg vil gerne undgå dette resultat (duplikerede imports):
import { foo } from 'my-lib';
import { bar } from 'my-lib';
Den tætteste løsning, jeg var i stand til at finde, er denne:
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
]);
Men funktionerne er forældede, og jeg kan ikke returnere ts.createImportDeclaration, fordi jeg vil få import og ikke koden for StringLiteral af foo.
Er der en funktion, der siger "opdater eller indsæt y fra x til import-sætningerne"?
Koden jeg har kunnet lave indtil videre:
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
Svar #1
Du kan bruge besøgsfunktionen til at gå gennem AST'en og gemme, hvilke medlemmer der er blevet brugt, uden at redigere nogen noder. Derefter (mens du stadig er i transformeringen) kan du finde/opdatere ImportDeclaration på sourceFile, hvor medlemmer er blevet tilgået:
/**
* 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
]);
}
};
Fordi denne kode tjekker, hvilke medlemmer der faktisk anvendes, erstatter den de eksisterende namedImports. Hvis du ønsker at bevare de eksisterende (selv hvis de ikke er blevet brugt eller ikke er inkluderet i availableMembers), kan du slå utilizedMembers-sættet sammen med det eksisterende importDeclaration.importClause.namedImports array i updateNamedImports. Dette håndterer heller ikke tilfælde med eksisterende udsagn import * as myLib from "my-lib";/import "my-lib";, men det burde være nemt at tilføje afhængigt af, hvordan du ønsker at håndtere det.
