Skip to content

Commit de557c4

Browse files
author
benholloway
committed
common code for esprima transform
1 parent 07f4038 commit de557c4

File tree

1 file changed

+221
-0
lines changed

1 file changed

+221
-0
lines changed
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
'use strict';
2+
3+
var codegen = require('escodegen'),
4+
esprima = require('esprima'),
5+
through = require('through2'),
6+
convert = require('convert-source-map'),
7+
sourceMap = require('source-map');
8+
9+
10+
/**
11+
* Create a Browserify transform that works on an esprima syntax tree
12+
* @param {function} updater A function that works on the esprima AST
13+
* @param {object} [format] An optional format for escodegen
14+
* @returns {function} A browserify transform
15+
*/
16+
function browserifyEsprimaTransform(updater, format) {
17+
18+
// transform
19+
return function browserifyTransform(file) {
20+
var chunks = [];
21+
return through(transfrom, flush);
22+
23+
function transfrom(chunk, encoding, done) {
24+
/* jshint validthis:true */
25+
chunks.push(chunk);
26+
done();
27+
}
28+
29+
function flush(done) {
30+
/* jshint validthis:true */
31+
var content = chunks.join('');
32+
33+
// parse code to AST using esprima
34+
var ast;
35+
try {
36+
ast = esprima.parse(content, {
37+
loc : true,
38+
comment: true,
39+
source : file
40+
});
41+
} catch(exception) {
42+
return done(exception);
43+
}
44+
45+
// make sure the AST has the data from the original source map
46+
var converter = convert.fromSource(content);
47+
var originalMap = converter && converter.toObject();
48+
var sourceContent = content;
49+
if (originalMap) {
50+
sourceMapToAst(ast, originalMap);
51+
sourceContent = originalMap.sourcesContent[0];
52+
}
53+
54+
// update the AST
55+
var updated;
56+
try {
57+
updated = ((typeof updater === 'function') && updater(file, ast)) || ast;
58+
} catch(exception) {
59+
return done(exception);
60+
}
61+
62+
// generate compressed code from the AST
63+
var pair = codegen.generate(updated, {
64+
sourceMap : true,
65+
sourceMapWithCode: true,
66+
format : format || {}
67+
});
68+
69+
// ensure that the source map has sourcesContent or browserify will not work
70+
pair.map.setSourceContent(file, sourceContent);
71+
var mapComment = convert.fromJSON(pair.map.toString()).toComment();
72+
73+
// push to the output
74+
this.push(new Buffer(pair.code + mapComment));
75+
done();
76+
}
77+
};
78+
}
79+
80+
module.exports = browserifyEsprimaTransform;
81+
82+
/**
83+
* Reinstate original source map locations on the AST
84+
* @param {object} ast The esprima syntax tree
85+
* @param {object} map The source map
86+
*/
87+
function sourceMapToAst(ast, map) {
88+
89+
// we will need a source-map consumer
90+
var consumer = new sourceMap.SourceMapConsumer(map);
91+
92+
// create a list of nodes that have 'loc' properties
93+
var nodes = ast.comments.concat();
94+
95+
// use a queue to avoid recursion stack for large trees
96+
var queue = [ast];
97+
while (queue.length) {
98+
var node = queue.shift();
99+
for (var key in node) {
100+
101+
// consider only object members
102+
var candidate = node[key];
103+
if (candidate && (typeof candidate === 'object')) {
104+
if (key === 'loc') {
105+
nodes.push(node);
106+
} else {
107+
queue.push(candidate);
108+
}
109+
}
110+
}
111+
}
112+
113+
// comments
114+
ast.comments
115+
.forEach(function eachComment(comment) {
116+
nodes.push(comment);
117+
});
118+
119+
// sort nodes by current position
120+
nodes.sort(compareLocation);
121+
122+
// calculate the original locations before applying them
123+
var calculated = nodes.map(calculateLocation);
124+
nodes.forEach(applyLocToNode(calculated));
125+
126+
// complete
127+
return ast;
128+
129+
/**
130+
* Map the location back to its original location, using adjacent values where necessary.
131+
* @param {object} node The node to consider
132+
* @param {number} i Index of this node object
133+
* @param {Array} nodes The nodes array
134+
* @returns {object} Amended location object
135+
*/
136+
function calculateLocation(node, i, nodes) {
137+
var j, n, loc;
138+
139+
// find the start location at the value or to its left
140+
var listStart = nodes.slice(0, i).concat(node);
141+
var origStart;
142+
for (n = 0, j = listStart.length - 1; !origStart && (j >= 0); j--, n++) {
143+
loc = (n === 0) ? listStart[j].loc.start : listStart[j].loc.end;
144+
origStart = consumer.originalPositionFor(loc);
145+
}
146+
origStart = origStart || {
147+
line : 0,
148+
column: 0
149+
};
150+
151+
// find the end location at the value or to its right
152+
var listEnd = [node].concat(nodes.slice(i));
153+
var origEnd;
154+
for (n = 0, j = 0; !origEnd && (i < listEnd.length); i++, n++) {
155+
loc = (n === 0) ? listEnd[j].loc.end : listEnd[j].loc.start;
156+
origEnd = consumer.originalPositionFor(loc);
157+
}
158+
origEnd = origEnd || {
159+
line : Number.MAX_VALUE,
160+
column: Number.MAX_VALUE
161+
};
162+
163+
// where both start and end locations are present
164+
if (origStart && origEnd) {
165+
return {
166+
start : {
167+
line : origStart.line,
168+
column: origStart.column
169+
},
170+
end : {
171+
line : origEnd.line,
172+
column: origEnd.column
173+
},
174+
source: origStart.source,
175+
name : origStart.name
176+
};
177+
} else {
178+
return null;
179+
}
180+
}
181+
}
182+
183+
/**
184+
* Compare function for nodes with location.
185+
* @param {object} nodeA First node
186+
* @param {object} nodeB Second node
187+
* @returns {number} -1 where a follows b, +1 where b follows a, 0 otherwise
188+
*/
189+
function compareLocation(nodeA, nodeB) {
190+
var hasA = nodeA && nodeA.loc;
191+
var hasB = nodeB && nodeB.loc;
192+
if (!hasA && !hasB) {
193+
return 0;
194+
}
195+
else if (hasA !== hasB) {
196+
return hasB ? +1 : hasA ? -1 : 0;
197+
}
198+
else {
199+
var startA = nodeA.loc.start;
200+
var endA = nodeA.loc.end;
201+
var startB = nodeB.loc.start;
202+
var endB = nodeB.loc.end;
203+
return (endB < startA) ? -1 : (endA < startB) ? +1 : 0;
204+
}
205+
}
206+
207+
/**
208+
* Get an <code>Array.forEach()</code> method that will apply the corresponding location from the given list.
209+
* @param {Array} locations A list of location objects to apply
210+
* @returns {function} A method suitable for <code>Array.forEach()</code>
211+
*/
212+
function applyLocToNode(locations) {
213+
return function eachNode(node, i) {
214+
var location = locations[i];
215+
if (location) {
216+
node.loc = location;
217+
} else {
218+
delete node.loc;
219+
}
220+
};
221+
}

0 commit comments

Comments
 (0)