Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to merge two Typescript ASTs

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)
like image 798
Martin Mecir Avatar asked Feb 02 '26 19:02

Martin Mecir


1 Answers

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.

like image 186
David Sherret Avatar answered Feb 05 '26 07:02

David Sherret