UNPKG

12.2 kBJavaScriptView Raw
1var asp = require('bluebird').promisify;
2var Promise = require('bluebird');
3var glob = require('glob');
4var path = require('path');
5var url = require('url');
6
7var getLoadDependencies = require('./trace').getLoadDependencies;
8
9var fromFileURL = require('./utils').fromFileURL;
10var toFileURL = require('./utils').toFileURL;
11
12var verifyTree = require('./utils').verifyTree;
13
14function parseExpression(expressionString) {
15 expressionString = ' + ' + expressionString;
16
17 var index = 0;
18 var operations = [];
19 var operatorRegex = /[\+\-\&]/;
20 var errorMessagesFromIndex = 3;
21
22 function getNextIdentifier() {
23 eatWhitespace();
24 var firstChar = expressionString.charAt(index);
25
26 if (operatorRegex.test(firstChar)){
27 throw 'Syntax Error: Identifier or sub expression expected after <' + expressionString.slice(errorMessagesFromIndex).substr(0, index - errorMessagesFromIndex) + '> but found <' + firstChar + '> instead';
28 }
29
30 if (firstChar === '(') {
31 var closingParenIndex = index,
32 numOpenBeforeSelf = 0;
33
34 while (++closingParenIndex < expressionString.length){
35 if (expressionString.charAt(closingParenIndex) === '('){
36 numOpenBeforeSelf++;
37 } else if (expressionString.charAt(closingParenIndex) === ')') {
38 if (numOpenBeforeSelf){
39 numOpenBeforeSelf--;
40 } else {
41 break;
42 }
43 }
44 }
45 if (expressionString.charAt(closingParenIndex) !== ')'){
46 throw 'Syntax Error: Expression <' + expressionString.substr(index) + '> is never terminated. Did you forget to add a closing ")"?';
47 }
48
49 var wholeExpression = expressionString.substring(index + 1, closingParenIndex);
50 index = closingParenIndex + 1;
51 return { bulkOperation: wholeExpression };
52 }
53
54 var result = "";
55 //scan the identifier
56 for (; index < expressionString.length; index++) {
57 var currentChar = expressionString.charAt(index);
58 //can have spaces in file names - so we need whitespace, operator, whitespace.
59 if (/^\s+[\+\-\&]\s+/.test(expressionString.substr(index))) {
60 return result;
61 } else {
62 result += currentChar;
63 }
64 }
65 return result.replace(/\s+$/, ''); //it appears as though trailing whitespace is trimmed downstream, but I'm snipping here to be safe
66 }
67
68 function getNextOperator() {
69 eatWhitespace();
70 if (index === expressionString.length) return null;
71
72 var candidateResult = expressionString.charAt(index++); //all operators are single characters at the moment
73
74 if (!operatorRegex.test(candidateResult)){
75 throw 'Syntax Error: An operator was expected after <' + expressionString.slice(errorMessagesFromIndex).substr(0, index - 1 - errorMessagesFromIndex) + '> but found <' + expressionString.substring(index - 1) + '> instead';
76 }
77
78 return candidateResult;
79 }
80
81 function eatWhitespace() {
82 //wind past whitespace
83 for (; index < expressionString.length; index++) {
84 if (/\S/.test(expressionString.charAt(index))) {
85 break;
86 }
87 }
88 }
89
90 var operator;
91 while (index < expressionString.length && (operator = getNextOperator())) {
92 var moduleNameOrSubExpression = getNextIdentifier();
93
94 if (typeof moduleNameOrSubExpression === 'object'){
95 operations.push({
96 operator: operator,
97 bulkOperation: moduleNameOrSubExpression.bulkOperation
98 });
99 } else {
100 // detect [moduleName] syntax for individual modules not trees
101 var singleModule = moduleNameOrSubExpression.substr(0, 1) == '[' && moduleNameOrSubExpression.substr(moduleNameOrSubExpression.length - 1, 1) == ']';
102 if (singleModule) {
103 moduleNameOrSubExpression = moduleNameOrSubExpression.substr(1, moduleNameOrSubExpression.length - 2);
104 }
105
106 var canonicalized = moduleNameOrSubExpression.substr(0, 1) == '`' && moduleNameOrSubExpression.substr(moduleNameOrSubExpression.length - 1, 1) == '`';
107 if (canonicalized) {
108 moduleNameOrSubExpression = moduleNameOrSubExpression.substr(1, moduleNameOrSubExpression.length - 2);
109 }
110
111 operations.push({
112 operator: operator,
113 moduleName: moduleNameOrSubExpression,
114 singleModule: singleModule,
115 canonicalized: canonicalized
116 });
117 }
118 }
119
120 return operations;
121}
122
123function getTreeOperation(symbol) {
124 if (symbol == '+')
125 return addTrees;
126 else if (symbol == '-')
127 return subtractTrees;
128 else if (symbol == '&')
129 return intersectTrees;
130 else
131 throw new TypeError('Unknown operator ' + symbol);
132}
133
134function getTreeModuleOperation(builder, symbol, traceOpts) {
135 if (symbol == '+')
136 return function(tree, canonical) {
137 var addedTree = {};
138 for (var p in tree)
139 addedTree[p] = tree[p];
140
141 return builder.tracer.getLoadRecord(canonical, traceOpts).then(function(load) {
142 addedTree[canonical] = load;
143 return addedTree;
144 });
145 };
146 else if (symbol == '-')
147 return function(tree, canonical) {
148 var subtractedTree = {};
149 for (var p in tree) {
150 if (p != canonical)
151 subtractedTree[p] = tree[p];
152 }
153 return subtractedTree;
154 };
155 else if (symbol == '&')
156 throw new TypeError('Single modules cannot be intersected.');
157 else
158 throw new TypeError('Unknown operator ' + symbol);
159}
160
161function expandGlobAndCanonicalize(builder, operation) {
162 var loader = builder.loader;
163
164 // no glob -> just canonicalize
165 if (operation.moduleName.indexOf('*') == -1) {
166 if (operation.canonicalized)
167 return [operation];
168
169 return loader.normalize(operation.moduleName)
170 .then(function(normalized) {
171 operation.moduleName = builder.getCanonicalName(normalized);
172 return [operation];
173 });
174 }
175
176 // globbing
177 var metadata = {};
178 var globSuffix = operation.moduleName[operation.moduleName.length - 1] == '*';
179 var pluginSyntax = operation.moduleName.indexOf('!') != -1;
180
181 return Promise.resolve()
182 .then(function() {
183 if (operation.canonicalized) {
184 return loader.decanonicalize(operation.moduleName);
185 }
186 else {
187 // normalizeSync avoids package config loading which we don't want for wildcards
188 return loader.normalizeSync(operation.moduleName);
189 }
190 })
191 .then(function(normalized) {
192 // remove ALL extension adding when globbing
193 if (globSuffix && !operation.canonicalized) {
194 var extIndex = normalized.lastIndexOf('.');
195 if (extIndex != -1 && normalized[extIndex - 1] == '*')
196 normalized = normalized.substr(0, extIndex);
197 }
198
199 return loader.locate({ name: normalized, metadata: metadata });
200 })
201 .then(function(address) {
202 // now we have a file path to glob -> glob the pattern
203 return asp(glob)(fromFileURL(address), {
204 nobrace: true,
205 noext: true,
206 nodir: true
207 });
208 })
209 .then(function(addresses) {
210 return (metadata.loader && pluginSyntax ? loader.normalize(metadata.loader) : Promise.resolve())
211 .then(function(loaderSyntaxName) {
212 return addresses.map(function(file) {
213 return {
214 operator: operation.operator,
215 moduleName: builder.getCanonicalName(toFileURL(file) + (loaderSyntaxName ? '!' + loader.getCanonicalName(loaderSyntaxName) : '')),
216 singleModule: operation.singleModule
217 };
218 });
219 });
220 });
221}
222
223exports.traceExpression = function(builder, expression, traceOpts) {
224 if (!expression)
225 throw new Error('A module expression must be provided to trace.');
226
227 if (expression instanceof Array) {
228 var tree = {};
229 return Promise.all(expression.map(function(moduleName) {
230 return builder.loader.normalize(moduleName)
231 .then(function(normalized) {
232 var canonical = builder.getCanonicalName(normalized);
233 return builder.tracer.getLoadRecord(canonical, traceOpts)
234 .then(function(load) {
235 tree[canonical] = load;
236 });
237 });
238 }))
239 .then(function() {
240 return tree;
241 });
242 }
243
244 return Promise
245 .resolve(expandAndCanonicalizeExpression(builder, expression))
246 .then(function processExpandedOperations(expandedOperations) {
247 // chain the operations, applying them with the trace of the next module
248 return expandedOperations.reduce(function(p, op) {
249 return p.then(function(curTree) {
250 // tree . module
251 if (op.singleModule)
252 return getTreeModuleOperation(builder, op.operator, traceOpts)(curTree, op.moduleName);
253
254 if (op.operationsTree){
255 return processExpandedOperations(op.operationsTree).then(function(expandedTree){
256 return getTreeOperation(op.operator)(curTree, expandedTree);
257 });
258 }
259 // tree . tree
260 return builder.tracer.traceCanonical(op.moduleName, traceOpts)
261 .then(function(nextTrace) {
262 return getTreeOperation(op.operator)(curTree, nextTrace.tree);
263 });
264 });
265 }, Promise.resolve({}));
266 });
267};
268
269function expandAndCanonicalizeExpression(builder, expression) {
270 var operations = parseExpression(expression);
271 var expandPromise = Promise.resolve(1);
272 var expandedOperations = [];
273
274 operations.forEach(function(operation){
275 if (operation.bulkOperation) {
276 var expandedTreePromise = expandAndCanonicalizeExpression(builder, operation.bulkOperation);
277 expandPromise = expandPromise.then(function() {
278 return Promise.resolve(expandedTreePromise)
279 .then(function(expressionsOperations){
280 expandedOperations = expandedOperations.concat({ operator: operation.operator, operationsTree: expressionsOperations });
281 });
282 });
283 } else {
284 expandPromise = expandPromise.then(function() {
285 return Promise.resolve(expandGlobAndCanonicalize(builder, operation))
286 .then(function (expanded) {
287 expandedOperations = expandedOperations.concat(expanded);
288 });
289 })
290 }
291 });
292
293 return Promise.resolve(expandPromise).then(function(){ return expandedOperations; });
294}
295
296// returns a new tree containing tree1 n tree2
297exports.intersectTrees = intersectTrees;
298function intersectTrees(tree1, tree2) {
299 verifyTree(tree1);
300 verifyTree(tree2);
301
302 var name;
303 var intersectTree = {};
304
305 var tree1Names = [];
306 for (name in tree1)
307 tree1Names.push(name);
308
309 for (name in tree2) {
310 if (tree1Names.indexOf(name) == -1)
311 continue;
312 // intersect deps layer (load: false) and actual bundle includes separately
313 if (tree1[name] === false && tree2[name] === false)
314 continue;
315
316 intersectTree[name] = tree1[name] || tree2[name];
317 }
318
319 return intersectTree;
320}
321
322// returns a new tree containing tree1 + tree2
323exports.addTrees = addTrees;
324function addTrees(tree1, tree2) {
325 verifyTree(tree1);
326 verifyTree(tree2);
327
328 var name;
329 var unionTree = {};
330
331 for (name in tree2)
332 unionTree[name] = tree2[name];
333
334 for (name in tree1)
335 if (!(name in unionTree))
336 unionTree[name] = tree1[name];
337
338 return unionTree;
339}
340
341// returns a new tree containing tree1 - tree2
342exports.subtractTrees = subtractTrees;
343function subtractTrees(tree1, tree2) {
344 verifyTree(tree1);
345 verifyTree(tree2);
346
347 var name;
348 var subtractTree = {};
349
350 for (name in tree1)
351 subtractTree[name] = tree1[name];
352
353 for (name in tree2) {
354 if (tree2[name] !== false)
355 delete subtractTree[name];
356 }
357
358 return subtractTree;
359}
360
361// pre-order tree traversal with a visitor and stop condition
362exports.traverseTree = traverseTree;
363function traverseTree(tree, moduleName, visitor, traceOpts, reversePost, parent, seen) {
364 if (!seen) {
365 // NB traceOpts.conditions should be strictly canonicalized on the first run
366 traceOpts = traceOpts || {};
367 verifyTree(tree);
368 }
369
370 seen = seen || [];
371 seen.push(moduleName);
372 parent = parent || null;
373
374 var curNode = tree[moduleName];
375
376 if (curNode && visitor(moduleName, parent) !== false) {
377 var deps = getLoadDependencies(curNode, traceOpts, traceOpts.conditions, traceOpts.inlineConditions);
378 if (reversePost)
379 deps = deps.reverse();
380 deps.forEach(function(dep) {
381 if (seen.indexOf(dep) == -1)
382 traverseTree(tree, dep, visitor, traceOpts, reversePost, moduleName, seen);
383 });
384 }
385}
\No newline at end of file