Skip to content

Commit f75e4e4

Browse files
author
benholloway
committed
esprima implementation of @ngInject pre-minifier, esmangle now extends esprima-tools
1 parent a190b38 commit f75e4e4

File tree

5 files changed

+392
-485
lines changed

5 files changed

+392
-485
lines changed
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
'use strict';
2+
3+
var codegen = require('escodegen'),
4+
esprima = require('esprima'),
5+
through = require('through2'),
6+
convert = require('convert-source-map'),
7+
sourcemapToAst = require('sourcemap-to-ast');
8+
9+
var WHITE_LIST = /^(?!id|loc|comments|parent).*$/;
10+
11+
/**
12+
* Create a Browserify transform that works on an esprima syntax tree
13+
* @param {function} updater A function that works on the esprima AST
14+
* @param {object} [format] An optional format for escodegen
15+
* @returns {function} A browserify transform
16+
*/
17+
function createTransform(updater, format) {
18+
19+
// transform
20+
return function browserifyTransform(file) {
21+
var chunks = [];
22+
return through(transform, flush);
23+
24+
function transform(chunk, encoding, done) {
25+
/* jshint validthis:true */
26+
chunks.push(chunk);
27+
done();
28+
}
29+
30+
function flush(done) {
31+
/* jshint validthis:true */
32+
var content = chunks.join('');
33+
34+
// parse code to AST using esprima
35+
var ast;
36+
try {
37+
ast = esprima.parse(content, {
38+
loc : true,
39+
comment: true,
40+
source : file
41+
});
42+
} catch(exception) {
43+
return done(exception);
44+
}
45+
46+
// associate comments with nodes they annotate
47+
associateComments(ast);
48+
49+
// make sure the AST has the data from the original source map
50+
var converter = convert.fromSource(content);
51+
var originalMap = converter && converter.toObject();
52+
var sourceContent = content;
53+
if (originalMap) {
54+
sourcemapToAst(ast, originalMap);
55+
sourceContent = originalMap.sourcesContent[0];
56+
}
57+
58+
// update the AST
59+
var updated;
60+
try {
61+
updated = ((typeof updater === 'function') && updater(file, ast)) || ast;
62+
} catch(exception) {
63+
return done(exception);
64+
}
65+
66+
// generate compressed code from the AST
67+
var pair = codegen.generate(updated, {
68+
sourceMap : true,
69+
sourceMapWithCode: true,
70+
format : format || {}
71+
});
72+
73+
// ensure that the source map has sourcesContent or browserify will not work
74+
pair.map.setSourceContent(file, sourceContent);
75+
var mapComment = convert.fromJSON(pair.map.toString()).toComment();
76+
77+
// push to the output
78+
this.push(new Buffer(pair.code + mapComment));
79+
done();
80+
}
81+
};
82+
}
83+
84+
/**
85+
* Sort nodes by location, include comments, create parent reference
86+
* @param {object} ast The esprima syntax tree
87+
*/
88+
function orderNodes(ast) {
89+
return (Array.isArray(ast.comments) ? ast.comments.slice() : [])
90+
.concat(findNodes(ast, ast.parent))
91+
.sort(compareLocation);
92+
}
93+
94+
/**
95+
* Create a setter that will replace the given node.
96+
* @param {object} candidate An esprima AST node to match
97+
* @param {number} [offset] 0 to replace, -1 to prepend, +1 to append
98+
* @returns {function|null} A setter that will replace the given node or Null on bad node
99+
*/
100+
function nodeSplicer(candidate, offset) {
101+
var found = findReferrer(candidate);
102+
if (found) {
103+
var key = found.key;
104+
var obj = found.object;
105+
var array = Array.isArray(obj) && obj;
106+
if (offset && !array) {
107+
throw new Error('Cannot splice with offset since the container is not an array');
108+
}
109+
else if (!array) {
110+
return function setter(value) {
111+
obj[key] = value;
112+
};
113+
}
114+
else if (offset < 0) {
115+
return function setter(value) {
116+
array.splice(key, 0, value);
117+
};
118+
}
119+
else if (offset > 0) {
120+
return function setter(value) {
121+
array.splice(key + 1, 0, value);
122+
};
123+
}
124+
else {
125+
return function setter(value) {
126+
array.splice(key, 1, value);
127+
};
128+
}
129+
}
130+
}
131+
132+
module.exports = {
133+
createTransform: createTransform,
134+
orderNodes : orderNodes,
135+
nodeSplicer : nodeSplicer
136+
};
137+
138+
/**
139+
* Associate comments with the node that follows them per an <code>annotates</code> property.
140+
* @param {object} ast An esprima AST with comments array
141+
*/
142+
function associateComments(ast) {
143+
var sorted = orderNodes(ast);
144+
ast.comments
145+
.forEach(function eachComment(comment) {
146+
147+
// decorate the comment with the node that follows it in the sorted node list
148+
var index = sorted.indexOf(comment);
149+
var annotates = sorted[index + 1];
150+
if (annotates) {
151+
comment.annotates = annotates;
152+
}
153+
154+
// comments generally can't be converted by source-map and won't be considered by sourcemap-to-ast
155+
delete comment.loc;
156+
});
157+
}
158+
159+
/**
160+
* Recursively find all nodes specified within the given node.
161+
* @param {object} node An esprima node
162+
* @param {object|undefined} [parent] The parent of the given node, where known
163+
* @returns {Array} A list of nodes
164+
*/
165+
function findNodes(node, parent) {
166+
var results = [];
167+
168+
// valid node so push it to the list and set new parent
169+
if ('type' in node) {
170+
node.parent = parent;
171+
parent = node;
172+
results.push(node);
173+
}
174+
175+
// recurse object members using the queue
176+
for (var key in node) {
177+
if (WHITE_LIST.test(key)) {
178+
var value = node[key];
179+
if (value && (typeof value === 'object')) {
180+
results.push.apply(results, findNodes(value, parent));
181+
}
182+
}
183+
}
184+
185+
// complete
186+
return results;
187+
}
188+
189+
/**
190+
* Compare function for nodes with location.
191+
* @param {object} nodeA First node
192+
* @param {object} nodeB Second node
193+
* @returns {number} -1 where a follows b, +1 where b follows a, 0 otherwise
194+
*/
195+
function compareLocation(nodeA, nodeB) {
196+
var locA = nodeA && nodeA.loc;
197+
var locB = nodeB && nodeB.loc;
198+
if (!locA && !locB) {
199+
return 0;
200+
}
201+
else if (Boolean(locA) !== Boolean(locB)) {
202+
return locB ? +1 : locA ? -1 : 0;
203+
}
204+
else {
205+
var result =
206+
isOrdered(locB.end, locA.start) ? +1 : isOrdered(locA.end, locB.start) ? -1 : // non-overlapping
207+
isOrdered(locB.start, locA.start) ? +1 : isOrdered(locA.start, locB.start) ? -1 : // overlapping
208+
isOrdered(locA.end, locB.end ) ? +1 : isOrdered(locB.end, locA.end ) ? -1 : // enclosed
209+
0;
210+
return result;
211+
}
212+
}
213+
214+
/**
215+
* Check the order of the given location tuples.
216+
* @param {{line:number, column:number}} tupleA The first tuple
217+
* @param {{line:number, column:number}} tupleB The second tuple
218+
* @returns {boolean} True where tupleA precedes tupleB
219+
*/
220+
function isOrdered(tupleA, tupleB) {
221+
return (tupleA.line < tupleB.line) || ((tupleA.line === tupleB.line) && (tupleA.column < tupleB.column));
222+
}
223+
224+
/**
225+
* Find the object and field that refers to the given node.
226+
* @param {object} candidate An esprima AST node to match
227+
* @param {object} [container] Optional container to search within or the candidate parent where omitted
228+
* @returns {{object:object, key:*}} The object and its key where the candidate node is a value
229+
*/
230+
function findReferrer(candidate, container) {
231+
var result;
232+
if (candidate) {
233+
234+
// initially for the parent of the candidate node
235+
container = container || candidate.parent;
236+
237+
// consider keys in the node until we have a result
238+
var keys = getKeys(container);
239+
for (var i = 0; !result && (i < keys.length); i++) {
240+
var key = keys[i];
241+
if (WHITE_LIST.test(key)) {
242+
var value = container[key];
243+
244+
// found
245+
if (value === candidate) {
246+
result = {
247+
object: container,
248+
key : key
249+
};
250+
}
251+
// recurse
252+
else if (value && (typeof value === 'object')) {
253+
result = findReferrer(candidate, value);
254+
}
255+
}
256+
}
257+
}
258+
259+
// complete
260+
return result;
261+
}
262+
263+
/**
264+
* Get the keys of an object as strings or those of an array as integers.
265+
* @param {object|Array} container A hash or array
266+
* @returns {Array.<string|number>} The keys of the container
267+
*/
268+
function getKeys(container) {
269+
function arrayIndex(value, i) {
270+
return i;
271+
}
272+
if (typeof container === 'object') {
273+
return Array.isArray(container) ? container.map(arrayIndex) : Object.keys(container);
274+
} else {
275+
return [];
276+
}
277+
}

0 commit comments

Comments
 (0)