i'm trying to merge two typescript AST's created from strings.
I create both with this method
const sourceFile = ts.createSourceFile(
"file.ts", // filePath
"function myFunction() {}", // fileText
ts.ScriptTarget.Latest, // scriptTarget
true // setParentNodes -- sets the `parent` property
);
However, this will create node of type ts.SourceFile, and if i try to merge these two ASTs i get error when trying to print the final sourceFile: "Unhandled SyntaxKind: SourceFile."
How should i properly merge two ASTs?
Thanks
EDIT: Here is full working code example. When i run this it throws me directly into debugger (in browser) with aforementionde error.
I also tried to purposefully extract the ArrowFunction node (in ast2 it is wrapped in SourceFile and ExpressionStatement), then the program runs without error, but completely changes the inner template literal into invalid code.
const createAST = (str: string) =>
ts.createSourceFile(
"file.ts", // filePath
str, // fileText
ts.ScriptTarget.Latest, // scriptTarget
true // setParentNodes -- sets the `parent` property
)
const template1 = `const arr = fields.map(() => {})`
const template2 = `(field) => <Toolbar id={\`\${field.id}.\${field.name}\`} />`
const ast1 = createAST(template1)
const ast2 = createAST(template2)
const replaceTransformer = <T extends ts.Node>(
newNode: T
): ts.TransformerFactory<T> => {
return context => {
const visit: ts.Visitor = node => {
if (ts.isArrowFunction(node)) return newNode
return ts.visitEachChild(node, child => visit(child), context)
}
return node => ts.visitNode(node, visit)
}
}
if (!ast1 || !ast2) return console.log("something went wrong")
const result = ts.transform(ast1, [replaceTransformer(ast2)])
const res = result.transformed[0]
const printer = ts.createPrinter()
const string = printer.printFile(res)
console.log(string)
The first issue is that the code has JSX in a .ts file (take a look at the result of ast2). To fix this, change the file name extension to .tsx so that it will parse as JSX.
The second issue is that putting a source file within a source file will lead to that error. Instead it's best to replace an expression with another expression so instead of providing the source file—ast2—provide (ast2.statements[0] as ts.ExpressionStatement).expression.
The third issue is that TypeScript is not designed to take the nodes from one source file and use them in another source file. For example, even with the fixes above, you will still get the following output:
const arr = fields.map((field) => <Toolbar id={) =field.idfield.name}/>);
...the reason for this is because the printer is using the positions from ast2 to index into the source file text of ast1, so it spits out garbage. What I've recommended to do in the past is to set the pos and end of all the nodes in the expression from ast2 to -1 (the default used for factory created nodes). It’s a bit of a hacky workaround though.
Full Example
const createAST = (str: string) =>
ts.createSourceFile(
"file.tsx", // filePath
str, // fileText
ts.ScriptTarget.Latest, // scriptTarget
true, // setParentNodes -- sets the `parent` property
);
const template1 = `const arr = fields.map(() => {})`;
const template2 = "(field) => <Toolbar id={`${field.id}.${field.name}`} />";
const ast1 = createAST(template1);
const ast2 = createAST(template2);
const replaceTransformer = (
newNode: ts.Node,
): ts.TransformerFactory<ts.SourceFile> => {
return (context) => {
const visit: ts.Visitor = (node) => {
if (ts.isArrowFunction(node)) {
stripRanges(newNode);
return newNode;
}
return ts.visitEachChild(node, (child) => visit(child), context);
};
return (node) => ts.visitNode(node, visit);
};
};
const newNode = (ast2.statements[0] as ts.ExpressionStatement).expression;
stripRanges(newNode);
const result = ts.transform(ast1, [
replaceTransformer(newNode),
]);
const res = result.transformed[0];
const printer = ts.createPrinter();
console.log(printer.printFile(res));
function stripRanges(node: ts.Node) {
(node as any).pos = -1;
(node as any).end = -1;
ts.forEachChild(node, stripRanges);
}
Alternatively instead of stripping the ranges, just not setting the parent nodes also seems to work (providing false for that argument) as the printer doesn't seem to consult the source file text in that case… again, another hacky workaround given the current implementation of the printer.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With