In this post I will discuss JavaScript ASTs and show how you can easily generate an AST of your code using Rollup.
AST stands for Abstract Syntax Tree. Basically it's a tree based breakdown of the structure of your code. By following the branches in the AST you will be able to follow the flow of your program at a very detailed level.
ASTs are of interest to people who are doing static analysis of code. One example of this is Tree Shaking. By following the trail of statements through a program you can infer if included code is actually needed to run the program. The map is right there in the AST.
In the following example I will show how we can use Rollup to extract an AST as part of bundling the application.
The application is intentionally kept simple:
greeting-service.js
export class GreetingService {
sayHello() {
return 'Hello';
}
sayHi() {
return 'Hi';
}
}
main.js
import {GreetingService} from './greeting-service';
class Main {
greet(type) {
let greetingService = new GreetingService();
if(type === 'hello') {
return greetingService.sayHello();
}
else {
return greetingService.sayHi();
}
}
}
document.getElementById('greet').addEventListener('click', function() {
let main = new Main();
alert(main.greet());
});
As you can tell the program boils down to a simple application that produces a flexible greeting.
Now lets run this through the Rollup bundler.
var rollup = require('rollup');
rollup.rollup( {
entry: 'app/main.js',
dest: 'dist/standard.js',
format: 'iife',
moduleName: 'greetingModule'
}).then(function(bundle){
//bundle contains the AST
})
The above code will bundle and Tree Shake the application. In this particular program there is not much to Tree Shake, but the point of this exercise is to generate the AST.
The cool thing about Rollup is that it gives us the full AST for the original application. This is the AST for the application before Tree Shaking took place.
What does the AST look like?
Even in this trivial application the AST expands to a relatively deep tree. Below is a snapshot of the Rollup generated AST:
{ imports: [],
exports: [],
modules:
[ { id: '/Users/tor/Development/tree-shaking/app/greeting-service.js',
code: 'export class GreetingService {\n sayHello() {\n return \'Hello\';\n }\n sayHi() {\n return \'Hi\';\n }\n}',
originalCode: 'export class GreetingService {\n sayHello() {\n return \'Hello\';\n }\n sayHi() {\n return \'Hi\';\n }\n}',
ast:
Node {
type: 'Program',
start: 0,
end: 104,
body:
[ Node {
type: 'ExportNamedDeclaration',
start: 0,
end: 104,
declaration:
Node {
type: 'ClassDeclaration',
start: 7,
end: 104,
id:
Node {
type: 'Identifier',
start: 13,
end: 28,
name: 'GreetingService' },
superClass: null,
body:
Node {
type: 'ClassBody',
start: 29,
end: 104,
body:
[ Node {
type: 'MethodDefinition',
start: 33,
end: 69,
computed: false,
key: Node { type: 'Identifier', start: 33, end: 41, name: 'sayHello' },
static: false,
kind: 'method',
value:
Node {
type: 'FunctionExpression',
start: 41,
end: 69,
id: null,
generator: false,
expression: false,
params: [],
body:
Node {
type: 'BlockStatement',
start: 44,
end: 69,
body:
[ Node {
type: 'ReturnStatement',
start: 50,
end: 65,
argument:
Node {
type: 'Literal',
start: 57,
end: 64,
value: 'Hello',
raw: '\'Hello\'' } } ] } } },
Node {
type: 'MethodDefinition',
start: 72,
end: 102,
computed: false,
key: Node { type: 'Identifier', start: 72, end: 77, name: 'sayHi' },
static: false,
kind: 'method',
value:
Node {
type: 'FunctionExpression',
start: 77,
end: 102,
id: null,
generator: false,
expression: false,
params: [],
body:
Node {
type: 'BlockStatement',
start: 80,
end: 102,
body:
[ Node {
type: 'ReturnStatement',
start: 86,
end: 98,
argument: Node { type: 'Literal', start: 93, end: 97, value: 'Hi', raw: '\'Hi\'' } } ] } } } ] } },
specifiers: [],
source: null } ],
sourceType: 'module' },
sourceMapChain: [],
resolvedIds: {} },
{ id: '/Users/tor/Development/tree-shaking/app/main.js',
code: 'import {GreetingService} from \'./greeting-service\';\n\nclass Main {\n greet(type) {\n let greetingService = new GreetingService();\n\n if(type === \'hello\') {\n return greetingService.sayHello();\n }\n else {\n return greetingService.sayHi();\n }\n }\n}\n\ndocument.getElementById(\'greet\').addEventListener(\'click\', function() {\n let main = new Main();\n alert(main.greet());\n});\n',
originalCode: 'import {GreetingService} from \'./greeting-service\';\n\nclass Main {\n greet(type) {\n let greetingService = new GreetingService();\n\n if(type === \'hello\') {\n return greetingService.sayHello();\n }\n else {\n return greetingService.sayHi();\n }\n }\n}\n\ndocument.getElementById(\'greet\').addEventListener(\'click\', function() {\n let main = new Main();\n alert(main.greet());\n});\n',
ast:
Node {
type: 'Program',
start: 0,
end: 392,
body:
[ Node {
type: 'ImportDeclaration',
start: 0,
end: 51,
specifiers:
[ Node {
type: 'ImportSpecifier',
start: 8,
end: 23,
imported: Node { type: 'Identifier', start: 8, end: 23, name: 'GreetingService' },
local: Node { type: 'Identifier', start: 8, end: 23, name: 'GreetingService' } } ],
source:
Node {
type: 'Literal',
start: 30,
end: 50,
value: './greeting-service',
raw: '\'./greeting-service\'' } },
Node {
type: 'ClassDeclaration',
start: 53,
end: 266,
id: Node { type: 'Identifier', start: 59, end: 63, name: 'Main' },
superClass: null,
body:
Node {
type: 'ClassBody',
start: 64,
end: 266,
body:
[ Node {
type: 'MethodDefinition',
start: 68,
end: 264,
computed: false,
key: Node { type: 'Identifier', start: 68, end: 73, name: 'greet' },
static: false,
kind: 'method',
value:
Node {
type: 'FunctionExpression',
start: 73,
end: 264,
id: null,
generator: false,
expression: false,
params: [ Node { type: 'Identifier', start: 74, end: 78, name: 'type' } ],
body:
Node {
type: 'BlockStatement',
start: 80,
end: 264,
body:
[ Node {
type: 'VariableDeclaration',
start: 86,
end: 130,
declarations:
[ Node {
type: 'VariableDeclarator',
start: 90,
end: 129,
id:
Node {
type: 'Identifier',
start: 90,
end: 105,
name: 'greetingService' },
init:
Node {
type: 'NewExpression',
start: 108,
end: 129,
callee:
Node {
type: 'Identifier',
start: 112,
end: 127,
name: 'GreetingService' },
arguments: [] } } ],
kind: 'let' },
Node {
type: 'IfStatement',
start: 136,
end: 260,
test:
Node {
type: 'BinaryExpression',
start: 139,
end: 155,
left: Node { type: 'Identifier', start: 139, end: 143, name: 'type' },
operator: '===',
right:
Node {
type: 'Literal',
start: 148,
end: 155,
value: 'hello',
raw: '\'hello\'' } },
consequent:
Node {
type: 'BlockStatement',
start: 157,
end: 205,
body:
[ Node {
type: 'ReturnStatement',
start: 165,
end: 199,
argument:
Node {
type: 'CallExpression',
start: 172,
end: 198,
callee:
Node {
type: 'MemberExpression',
start: 172,
end: 196,
object:
Node {
type: 'Identifier',
start: 172,
end: 187,
name: 'greetingService' },
property: Node { type: 'Identifier', start: 188, end: 196, name: 'sayHello' },
computed: false },
arguments: [] } } ] },
alternate:
Node {
type: 'BlockStatement',
start: 215,
end: 260,
body:
[ Node {
type: 'ReturnStatement',
start: 223,
end: 254,
argument:
Node {
type: 'CallExpression',
start: 230,
end: 253,
callee:
Node {
type: 'MemberExpression',
start: 230,
end: 251,
object:
Node {
type: 'Identifier',
start: 230,
end: 245,
name: 'greetingService' },
property: Node { type: 'Identifier', start: 246, end: 251, name: 'sayHi' },
computed: false },
arguments: [] } } ] } } ] } } } ] } },
Node {
type: 'ExpressionStatement',
start: 268,
end: 391,
expression:
Node {
type: 'CallExpression',
start: 268,
end: 390,
callee:
Node {
type: 'MemberExpression',
start: 268,
end: 317,
object:
Node {
type: 'CallExpression',
start: 268,
end: 300,
callee:
Node {
type: 'MemberExpression',
start: 268,
end: 291,
object: Node { type: 'Identifier', start: 268, end: 276, name: 'document' },
property:
Node {
type: 'Identifier',
start: 277,
end: 291,
name: 'getElementById' },
computed: false },
arguments:
[ Node {
type: 'Literal',
start: 292,
end: 299,
value: 'greet',
raw: '\'greet\'' } ] },
property:
Node {
type: 'Identifier',
start: 301,
end: 317,
name: 'addEventListener' },
computed: false },
arguments:
[ Node {
type: 'Literal',
start: 318,
end: 325,
value: 'click',
raw: '\'click\'' },
Node {
type: 'FunctionExpression',
start: 327,
end: 389,
id: null,
generator: false,
expression: false,
params: [],
body:
Node {
type: 'BlockStatement',
start: 338,
end: 389,
body:
[ Node {
type: 'VariableDeclaration',
start: 342,
end: 364,
declarations:
[ Node {
type: 'VariableDeclarator',
start: 346,
end: 363,
id: Node { type: 'Identifier', start: 346, end: 350, name: 'main' },
init:
Node {
type: 'NewExpression',
start: 353,
end: 363,
callee: Node { type: 'Identifier', start: 357, end: 361, name: 'Main' },
arguments: [] } } ],
kind: 'let' },
Node {
type: 'ExpressionStatement',
start: 367,
end: 387,
expression:
Node {
type: 'CallExpression',
start: 367,
end: 386,
callee: Node { type: 'Identifier', start: 367, end: 372, name: 'alert' },
arguments:
[ Node {
type: 'CallExpression',
start: 373,
end: 385,
callee:
Node {
type: 'MemberExpression',
start: 373,
end: 383,
object: Node { type: 'Identifier', start: 373, end: 377, name: 'main' },
property: Node { type: 'Identifier', start: 378, end: 383, name: 'greet' },
computed: false },
arguments: [] } ] } } ] } } ] } } ],
sourceType: 'module' },
sourceMapChain: [],
resolvedIds: { './greeting-service': '/Users/tor/Development/tree-shaking/app/greeting-service.js' } } ],
generate: [Function: generate],
write: [Function] }
It's interesting to follow the level of detail in the AST. Anything from module imports, if statements to return statements are covered. In addition to the various programming constructs, you also get their exact coordinates in the source code (start, end coordinates).