1

If references are statically defined (are not dynamically created), does the TypeScript compiler API offer a way to easily serialize a node and its dependencies?

For instance:

file-a.ts

export const a = () => {
  console.log("a");
}

file-b.ts

import { a } from "./file-a"

const b = () => {
  a()
  console.log("b")
}

When I encounter file-b's b declaration, I'd like to extract and print it as this:

const b = () => {
  (() => {
    console.log("a")
  })()
  console.log("b")
}

... if doing this serialization is not a feature of the compiler API, does one need to traverse and inline all declarations? Or is there a better approach?

Harry Solovay
  • 483
  • 3
  • 14
  • 2
    Is this to modify the TypeScript files in place—can you edit the text directly? Or is going to be done via the transformation api before emitting the code? Also, will you have a type checker? – David Sherret Dec 04 '19 at 01:39
  • Yes, I'll have a TypeChecker. No, it is not to modify the TypeScript files in place. It's for use in a solution builder. The goal is to create a new source file (doesn't really matter what we do with it). The new source file would contain only the node you wish to serialize (in this case `b`), and any of its statically-analyzable dependents inlined... these may need to be extracted from other source files). – Harry Solovay Dec 04 '19 at 19:52

1 Answers1

1

There's not a built in way to serialize a node and all its dependencies, but I don't believe you need to do any serialization here (depending on what you mean by that).

To solve this, you could build up your own graph of how everything is connected and then traverse that to create a final single function (could be thought of as collapsing it). That would help you cache work that's already done. You could do it without that though and just construct the statements as you go along.

In short and probably not explaining it well enough:

  1. Within that function (b), traverse all the nodes finding nodes that reference something outside the function.
  2. Follow all those nodes to their declarations (ex. use the type checker to follow the symbols of a call expression's identifier until you reach the declaration).
  3. Repeat until everything is accounted for.

Here's a function that might be useful for making a deep mutable clone of a node (kind of untested and there might be something better... I'm not sure if there's a way to do this without bothering with a context). You could use this to construct copies of nodes.

function getDeepMutableClone<T extends ts.Node>(node: T): T {
    return ts.transform(node, [
        context => node => deepCloneWithContext(node, context)
    ]).transformed[0];

    function deepCloneWithContext<T extends ts.Node>(
        node: T,
        context: ts.TransformationContext
    ): T {
        const clonedNode = ts.visitEachChild(
            stripRanges(ts.getMutableClone(node)),
            child => deepCloneWithContext(child, context),
            context
        );
        clonedNode.parent = undefined as any;
        ts.forEachChild(clonedNode, child => { child.parent = clonedNode; });
        return clonedNode;
    }
}

// See https://stackoverflow.com/a/57367717/188246 for
// why this is necessary.
function stripRanges<T extends ts.Node>(node: T) {
    node.pos = -1;
    node.end = -1;
    return node;
}
David Sherret
  • 101,669
  • 28
  • 188
  • 178
  • An alternative to using `visitEachChild` that might work is to inspect each property to check if it's an array of nodes or a node, then create a copy in that case ([example here](https://gist.github.com/dsherret/81ce9e28531f91790678e407f10b78e9)). – David Sherret Dec 11 '19 at 15:50