Search code examples
javascriptabstract-syntax-treesource-mapsesprima

How to generate a JavaScript sourcemap based on an AST transform?


Suppose I AST transform the content of a JavaScript file from state A to state B.

How might I make an accompanying sourcemap? I'm using esprima and estravese (estraverse.replace) to traverse an AST (I have the sourcemap corresponding to the initial AST) and transform it into another AST (but I don't have the resulting sourcemap).

How might I get that sourcemap?

EDIT: I'm using esprima and estraverse to do my AST transformation. My transformation looks like this:

module.exports = {

    type: 'replace', // or traverse

    enter(node, parent) {

        if (
            node.type == 'ExpressionStatement'
            && parent.type == 'Program'
            && node.expression.type == 'CallExpression'
            && node.expression.callee.name == 'module'
        ) {
            // rename `module` to `define`
            node.expression.callee.name = 'define'

            // The dependency object (the `{a:'./a', b:'./b'}` in `module({a:'./a', b:'./b'}, function(imports) {})`) will be...
            const dependenciesObjectExpression = node.expression.arguments[0]

            // ...converted into an array of paths (the `['./a', './b']` in `define(['./a', './b'], function(a,b) {})`), and...
            const dependencyPathLiterals =
                dependenciesObjectExpression.properties.map(prop => prop.value)

            // ...the dependency names will be converted into parameters of the module body function (the `a,b` in `define(['./a', './b'], function(a,b) {})`).
            const dependencyNameIdentifiers =
                dependenciesObjectExpression.properties.map(prop => prop.key)

            // set the new define call's arguments
            node.expression.arguments[0] = {
                type: 'ArrayExpression',
                elements: dependencyPathLiterals,
            }
            node.expression.arguments[1].params = dependencyNameIdentifiers

            return node
        }

        // if we see `imports.foo`, convert to `foo`
        if (
            node.type == 'MemberExpression'
            && node.object.type == 'Identifier'
            && node.object.name == 'imports'
        ) {
            return {
                type: 'Identifier',
                name: node.property.name,
            }
        }
    },

    leave(node, parent) {
        //
    }

}

Solution

  • For each tree transformation you write, you have write a corresponding transformation on the source map.

    Because tree transforms are essentially arbitrary, the corresponding source transforms will be arbitrary, too. Similarly, complex tree transformations will cause correspondingly complex source map transforms.

    One way to implement this might be co-opt the (I assume these exist) tree transformation operations DeleteNode, ReplaceNode, ReplaceChildWithIdentifier, ReplaceChildWithLiteral, ReplaceChildWithOperator. Using only these operations you should still be able to make arbitrary tree changes. By modifying these operatios to update the source map (each one does something very specific to the source map), you should get "for free" the updated sourcemap. Obviously you can't use other tree modifying operations unless they are implemented using these primitives.

    A couple tools in the community for this purpose: