I using transform
function from typescript compiler api to change my code.
This function is recursive and visit each node.
When I found a StringLiteral
of foo
I want to add foo
to import where my-lib
like this:
import { foo } from 'my-lib';
The code can have already import something else from my-lib
like:
import { bar } from 'my-lib';
And I want to avoid this result (duplicate imports):
import { foo } from 'my-lib';
import { bar } from 'my-lib';
The closest solution I was able to find is this:
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
]);
But the functions are deprecates. and I can't return ts.createImportDeclaration
becuase I will get the import
and not the code of StringLiteral
of foo
.
Is there a function say "update or insert y from x to the imports statements"?
The code I was able to do so far:
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 });
You can use the visitor to walk the AST and store which members have been utilized without editing any nodes. Afterward (while still in the transform), you can find/upsert the ImportDeclaration
on the sourceFile
with which members have been accessed:
/**
* 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
]);
}
};
Because this code checks which members are actually utilized, it replaces the existing namedImports
. If you want to preserve the existing ones (even if they haven't been used or are not included in availableMembers
, you can merge the utilizedMembers
set with the existing importDeclaration.importClause.namedImports
array in updateNamedImports
. This also doesn't handle the case of existing statements import * as myLib from "my-lib";
/import "my-lib";
, but that should be easy to add depending on how you want to handle it.