UNPKG

18.6 kBJavaScriptView Raw
1var Promise = require('bluebird');
2var asp = require('bluebird').promisify;
3var fs = require('fs');
4var path = require('path');
5var url = require('url');
6var createHash = require('crypto').createHash;
7var template = require('es6-template-strings');
8var getAlias = require('./utils').getAlias;
9var extend = require('./utils').extend;
10var traverseTree = require('./arithmetic').traverseTree;
11var verifyTree = require('./utils').verifyTree;
12var getFormatHint = require('./utils').getFormatHint;
13
14var compilerMap = {
15 'amd': '../compilers/amd',
16 'cjs': '../compilers/cjs',
17 'esm': '../compilers/esm',
18 'global': '../compilers/global',
19 'system': '../compilers/register',
20 'json': '../compilers/json'
21};
22
23// create a compile hash based on path + source + metadata + compileOpts
24// one implication here is that plugins shouldn't rely on System.x checks
25// as these will not be cache-invalidated but within the bundle hook is fine
26function getCompileHash(load, compileOpts) {
27 return createHash('md5')
28 .update(JSON.stringify({
29 source: load.source,
30 metadata: load.metadata,
31 path: compileOpts.sourceMaps && load.path,
32
33 normalize: compileOpts.normalize,
34 anonymous: compileOpts.anonymous,
35 systemGlobal: compileOpts.systemGlobal,
36 static: compileOpts.static,
37 encodeNames: compileOpts.encodeNames,
38 sourceMaps: compileOpts.sourceMaps,
39 lowResSourceMaps: compileOpts.lowResSourceMaps
40 }))
41 .digest('hex');
42}
43
44function getEncoding(canonical, encodings, loader) {
45 // dont encode system modules
46 if (canonical[0] == '@' && canonical != '@dummy-entry-point' && loader.has(canonical))
47 return canonical;
48
49 // return existing encoding if present
50 if (encodings[canonical])
51 return encodings[canonical];
52
53 // search for the first available key
54 var highestEncoding = 9;
55 Object.keys(encodings).forEach(function(canonical) {
56 var encoding = encodings[canonical];
57 highestEncoding = Math.max(parseInt(encoding, '16'), highestEncoding);
58 });
59
60 highestEncoding++;
61
62 return encodings[canonical] = highestEncoding.toString(16);
63}
64function getName(encoding, encodings) {
65 var match
66 Object.keys(encodings).some(function(e) {
67 if (encodings[e] == encoding) {
68 match = e;
69 return true;
70 }
71 });
72 return match;
73}
74
75// used to support leading #!/usr/bin/env in scripts as supported in Node
76var hashBangRegEx = /^\#\!.*/;
77
78exports.compileLoad = compileLoad;
79function compileLoad(loader, load, compileOpts, cache) {
80 // use cached if we have it
81 var cached = cache.loads[load.name];
82 if (cached && cached.hash == getCompileHash(load, compileOpts))
83 return Promise.resolve(cached.output);
84
85 // create a new load record with any necessary final mappings
86 function remapLoadRecord(load, mapFunction) {
87 load = extend({}, load);
88 load.name = mapFunction(load.name, load.name);
89 var depMap = {};
90 Object.keys(load.depMap).forEach(function(dep) {
91 depMap[dep] = mapFunction(load.depMap[dep], dep);
92 });
93 load.depMap = depMap;
94 return load;
95 }
96 var mappedLoad = remapLoadRecord(load, function(name, original) {
97 // do SFX encodings
98 if (compileOpts.encodeNames)
99 return getEncoding(name, cache.encodings, loader);
100
101 if (compileOpts.normalize && name.indexOf('#:') != -1)
102 throw new Error('Unable to build dependency ' + name + '. normalize must be disabled for bundles containing conditionals.');
103
104 return name;
105 });
106
107 var format = load.metadata.format;
108
109 if (format == 'defined')
110 return Promise.resolve({ source: compileOpts.systemGlobal + '.register("' + mappedLoad.name + '", [], function() { return { setters: [], execute: function() {} } });\n' });
111
112 if (format in compilerMap) {
113 if (format == 'cjs')
114 mappedLoad.source = mappedLoad.source.replace(hashBangRegEx, '');
115 return Promise.resolve()
116 .then(function() {
117 return require(compilerMap[format]).compile(mappedLoad, compileOpts, loader);
118 })
119 .then(function(output) {
120 // store compiled output in cache
121 cache.loads[load.name] = {
122 hash: getCompileHash(load, compileOpts),
123 output: output
124 };
125
126 return output;
127 })
128 .catch(function(err) {
129 // Traceur has a habit of throwing array errors
130 if (err instanceof Array)
131 err = err[0];
132 err.message = 'Error compiling ' + format + ' module "' + load.name + '" at ' + load.path + '\n\t' + err.message;
133 throw err;
134 });
135 }
136
137 return Promise.reject(new TypeError('Unknown module format ' + format));
138}
139
140// sort in reverse pre-order, filter modules to actually built loads (excluding conditionals, build: false)
141// (exported for unit testing)
142exports.getTreeModulesPostOrder = getTreeModulesPostOrder;
143function getTreeModulesPostOrder(tree, traceOpts) {
144 var entryPoints = [];
145
146 // build up the map of parents of the graph
147 var entryMap = {};
148
149 var moduleList = Object.keys(tree).filter(function(module) {
150 return tree[module] !== false;
151 }).sort();
152
153 // for each module in the tree, we traverse the whole tree
154 // we then relate each module in the tree to the first traced entry point
155 moduleList.forEach(function(entryPoint) {
156 traverseTree(tree, entryPoint, function(depName, parentName) {
157 // if we have a entryMap for the given module, then stop
158 if (entryMap[depName])
159 return false;
160
161 if (parentName)
162 entryMap[depName] = entryPoint;
163 }, traceOpts);
164 });
165
166 // the entry points are then the modules not represented in entryMap
167 moduleList.forEach(function(entryPoint) {
168 if (!entryMap[entryPoint])
169 entryPoints.push(entryPoint);
170 });
171
172 // now that we have the entry points, sort them alphabetically and
173 // run the traversal to get the ordered module list
174 entryPoints = entryPoints.sort();
175
176 var modules = [];
177
178 entryPoints.reverse().forEach(function(moduleName) {
179 traverseTree(tree, moduleName, function(depName, parentName) {
180 if (modules.indexOf(depName) == -1)
181 modules.push(depName);
182 }, traceOpts, true);
183 });
184
185 return {
186 entryPoints: entryPoints,
187 modules: modules.reverse()
188 };
189}
190
191// run the plugin bundle hook on the list of loads
192// returns the assetList
193exports.pluginBundleHook = pluginBundleHook;
194function pluginBundleHook(loader, loads, compileOpts, outputOpts) {
195 var outputs = [];
196 // plugins have the ability to report an asset list during builds
197 var assetList = [];
198 var pluginLoads = {};
199
200 // store just plugin loads
201 loads.forEach(function(load) {
202 if (load.metadata.loader) {
203 var pluginLoad = extend({}, load);
204 pluginLoad.address = loader.baseURL + load.path;
205 (pluginLoads[load.metadata.loader] = pluginLoads[load.metadata.loader] || []).push(pluginLoad);
206 }
207 });
208
209 return Promise.all(Object.keys(pluginLoads).map(function(pluginName) {
210 var loads = pluginLoads[pluginName];
211 var loaderModule = loads[0].metadata.loaderModule;
212
213 if (loaderModule.listAssets)
214 return Promise.resolve(loaderModule.listAssets.call(loader.pluginLoader, loads, compileOpts, outputOpts))
215 .then(function(_assetList) {
216 assetList = assetList.concat(_assetList.map(function(asset) {
217 return {
218 url: asset.url,
219 type: asset.type,
220 source: asset.source,
221 sourceMap: asset.sourceMap
222 };
223 }));
224 });
225 }))
226 .then(function() {
227 return Promise.all(Object.keys(pluginLoads).map(function(pluginName) {
228 var loads = pluginLoads[pluginName];
229 var loaderModule = loads[0].metadata.loaderModule;
230
231 if (compileOpts.inlinePlugins) {
232 if (loaderModule.inline) {
233 return Promise.resolve(loaderModule.inline.call(loader.pluginLoader, loads, compileOpts, outputOpts));
234 }
235 // NB deprecate bundle hook for inline hook
236 else if (loaderModule.bundle) {
237 // NB deprecate the 2 argument form
238 if (loaderModule.bundle.length < 3)
239 return Promise.resolve(loaderModule.bundle.call(loader.pluginLoader, loads, extend(extend({}, compileOpts), outputOpts)));
240 else
241 return Promise.resolve(loaderModule.bundle.call(loader.pluginLoader, loads, compileOpts, outputOpts));
242 }
243 }
244 }));
245 })
246 .then(function(compiled) {
247 var outputs = [];
248 compiled = compiled || [];
249 compiled.forEach(function(output) {
250 if (output instanceof Array)
251 outputs = outputs.concat(output);
252 else if (output)
253 outputs.push(output);
254 });
255
256 return {
257 outputs: outputs,
258 assetList: assetList
259 };
260 });
261}
262
263exports.compileTree = compileTree;
264function compileTree(loader, tree, traceOpts, compileOpts, outputOpts, cache) {
265
266 // verify that the tree is a tree
267 verifyTree(tree);
268
269 var ordered = getTreeModulesPostOrder(tree, traceOpts);
270
271 var inputEntryPoints;
272
273 // get entrypoints from graph algorithm
274 var entryPoints;
275
276 var modules;
277
278 var outputs = [];
279
280 var compilers = {};
281
282 return Promise.resolve()
283 .then(function() {
284 // compileOpts.entryPoints can be unnormalized
285 if (!compileOpts.entryPoints)
286 return [];
287
288 return Promise.all(compileOpts.entryPoints.map(function(entryPoint) {
289 return loader.normalize(entryPoint)
290 .then(function(normalized) {
291 return loader.getCanonicalName(normalized);
292 });
293 }))
294 .filter(function(inputEntryPoint) {
295 // skip conditional entry points and entry points not in the tree (eg rollup optimized out)
296 return !inputEntryPoint.match(/\#\:|\#\?|\#{/) && tree[inputEntryPoint];
297 })
298 })
299 .then(function(inputEntryPoints) {
300 entryPoints = inputEntryPoints || [];
301
302 ordered.entryPoints.forEach(function(entryPoint) {
303 if (entryPoints.indexOf(entryPoint) == -1)
304 entryPoints.push(entryPoint);
305 });
306
307 modules = ordered.modules.filter(function(moduleName) {
308 var load = tree[moduleName];
309 if (load.runtimePlugin && compileOpts.static)
310 throw new TypeError('Plugin ' + load.plugin + ' does not support static builds, compiling ' + load.name + '.');
311 return load && !load.conditional && !load.runtimePlugin;
312 });
313
314 if (compileOpts.encodeNames)
315 entryPoints = entryPoints.map(function(name) {
316 return getEncoding(name, cache.encodings, loader);
317 });
318 })
319
320 // create load output objects
321 .then(function() {
322 return Promise.all(modules.map(function(name) {
323 return Promise.resolve()
324 .then(function() {
325 var load = tree[name];
326
327 if (load === true)
328 throw new TypeError(name + ' was defined via a bundle, so can only be used for subtraction or union operations.');
329
330 return compileLoad(loader, tree[name], compileOpts, cache);
331 });
332 }));
333 })
334 .then(function(compiled) {
335 outputs = outputs.concat(compiled);
336 })
337
338 // run plugin bundle hook
339 .then(function() {
340 var pluginLoads = [];
341
342 modules.forEach(function(name) {
343 var load = tree[name];
344
345 pluginLoads.push(load);
346
347 // if we used Rollup, we should still run the bundle hook for the child loads that were compacted
348 if (load.compactedLoads)
349 load.compactedLoads.forEach(function(load) {
350 pluginLoads.push(load);
351 });
352 });
353
354 return pluginBundleHook(loader, pluginLoads, compileOpts, outputOpts);
355 })
356 .then(function(pluginResults) {
357 outputs = outputs.concat(pluginResults.outputs);
358 var assetList = pluginResults.assetList;
359
360 return Promise.resolve()
361 .then(function() {
362 // if any module in the bundle is AMD, add a "bundle" meta to the bundle
363 // this can be deprecated if https://github.com/systemjs/builder/issues/264 lands
364 if (modules.some(function(name) {
365 return tree[name].metadata.format == 'amd';
366 }) && !compileOpts.static)
367 outputs.unshift('"bundle";');
368
369 // static bundle wraps with a self-executing loader
370 if (compileOpts.static)
371 return wrapSFXOutputs(loader, tree, modules, outputs, entryPoints, compileOpts, cache);
372
373 return outputs;
374 })
375 .then(function(outputs) {
376 // NB also include all aliases of all entryPoints along with entryPoints
377 return {
378 outputs: outputs,
379 entryPoints: entryPoints,
380 assetList: assetList,
381 modules: modules.reverse()
382 };
383 });
384 });
385}
386
387exports.wrapSFXOutputs = wrapSFXOutputs;
388function wrapSFXOutputs(loader, tree, modules, outputs, entryPoints, compileOpts, cache) {
389 var compilers = {};
390 var externalDeps = [];
391
392 Object.keys(tree).forEach(function(module) {
393 if (tree[module] === false && !loader.has(module))
394 externalDeps.push(module);
395 });
396
397 externalDeps.sort();
398
399 // determine compilers used
400 var legacyTranspiler = false;
401 modules.forEach(function(name) {
402 if (!legacyTranspiler && tree[name].metadata.originalSource)
403 legacyTranspiler = true;
404 compilers[tree[name].metadata.format] = true;
405 });
406
407 // include compiler helpers at the beginning of outputs
408 Object.keys(compilerMap).forEach(function(format) {
409 if (!compilers[format])
410 return;
411 var compiler = require(compilerMap[format]);
412 if (compiler.sfx)
413 outputs.unshift(compiler.sfx(loader));
414 });
415
416 // determine if the SFX bundle has any external dependencies it relies on
417 var globalDeps = [];
418 modules.forEach(function(name) {
419 var load = tree[name];
420
421 // check all deps are present
422 load.deps.forEach(function(dep) {
423 var key = load.depMap[dep];
424 if (!(key in tree) && !loader.has(key)) {
425 if (compileOpts.format == 'esm')
426 throw new TypeError('ESM static builds with externals only work when all modules in the build are ESM.');
427
428 if (externalDeps.indexOf(key) == -1)
429 externalDeps.push(key);
430 }
431 });
432 });
433
434 var externalDepIds = externalDeps.map(function(dep) {
435 if (compileOpts.format == 'global' ||
436 compileOpts.format == 'umd' && (compileOpts.globalName || Object.keys(compileOpts.globalDeps).length > 0)) {
437 var alias = getAlias(loader, dep);
438 var globalDep = compileOpts.globalDeps[dep] || compileOpts.globalDeps[alias];
439 if (!globalDep)
440 throw new TypeError('Global SFX bundle dependency "' + alias +
441 '" must be configured to an environment global via the globalDeps option.');
442
443 globalDeps.push(globalDep);
444 }
445
446 // remove external deps from calculated entry points list
447 var entryPointIndex = entryPoints.indexOf(dep);
448 if (entryPointIndex != -1)
449 entryPoints.splice(entryPointIndex, 1);
450
451 if (compileOpts.encodeNames)
452 return getEncoding(dep, cache.encodings, loader);
453 else
454 return dep;
455 });
456
457 // next wrap with the core code
458 return asp(fs.readFile)(path.resolve(__dirname, '../templates/sfx-core.min.js'))
459 .then(function(sfxcore) {
460 // for NodeJS execution to work correctly, we need to ensure the scoped module, exports and require variables are nulled out
461 outputs.unshift("var require = this.require, exports = this.exports, module = this.module;");
462
463 // if the first entry point is a dynamic module, then it is exportDefault always by default
464 var exportDefault = compileOpts.exportDefault;
465 var exportedLoad = tree[compileOpts.encodeNames && getName(entryPoints[0], cache.encodings) || entryPoints[0]];
466 if (exportedLoad && exportedLoad.metadata.format != 'system' && exportedLoad.metadata.format != 'esm')
467 exportDefault = true;
468
469 outputs.unshift(sfxcore.toString(), "(" + JSON.stringify(entryPoints) + ", " + JSON.stringify(externalDepIds) + ", " +
470 (exportDefault ? "true" : "false") + ", function(" + compileOpts.systemGlobal + ") {");
471
472 outputs.push("})");
473 return asp(fs.readFile)(path.resolve(__dirname, '../templates/sfx-' + compileOpts.format + '.js'));
474 })
475 // then include the sfx module format wrapper
476 .then(function(formatWrapper) {
477 outputs.push(template(formatWrapper.toString(), {
478 deps: externalDeps.map(function(dep) {
479 if (dep.indexOf('#:') != -1)
480 dep = dep.replace('#:/', '/');
481 var name = getAlias(loader, dep);
482 return name;
483 }),
484 globalDeps: globalDeps,
485 globalName: compileOpts.globalName
486 }));
487 })
488 // then wrap with the runtime
489 .then(function() {
490 if (!legacyTranspiler)
491 return;
492
493 // NB legacy runtime wrappings
494 var usesBabelHelpersGlobal = modules.some(function(name) {
495 return tree[name].metadata.usesBabelHelpersGlobal;
496 });
497 // regenerator runtime check
498 if (!usesBabelHelpersGlobal)
499 usesBabelHelpersGlobal = modules.some(function(name) {
500 return tree[name].metadata.format == 'esm' && cache.loads[name].output.source.match(/regeneratorRuntime/);
501 });
502 if (compileOpts.runtime && usesBabelHelpersGlobal)
503 return getModuleSource(loader, 'babel/external-helpers')
504 .then(function(source) {
505 outputs.unshift(source);
506 });
507 })
508 .then(function() {
509 if (!legacyTranspiler)
510 return;
511
512 // NB legacy runtime wrappings to eb deprecated
513 var usesTraceurRuntimeGlobal = modules.some(function(name) {
514 return tree[name].metadata.usesTraceurRuntimeGlobal;
515 });
516 if (compileOpts.runtime && usesTraceurRuntimeGlobal)
517 return getModuleSource(loader, 'traceur-runtime')
518 .then(function(source) {
519 // protect System global clobbering
520 outputs.unshift("(function(){ var curSystem = typeof System != 'undefined' ? System : undefined;\n" + source + "\nSystem = curSystem; })();");
521 });
522 })
523 // for AMD, CommonJS and global SFX outputs, add a "format " meta to support SystemJS loading
524 .then(function() {
525 if (compileOpts.formatHint)
526 outputs.unshift(getFormatHint(compileOpts));
527 })
528 .then(function() {
529 return outputs;
530 });
531}
532
533exports.attachCompilers = function(loader) {
534 Object.keys(compilerMap).forEach(function(compiler) {
535 var attach = require(compilerMap[compiler]).attach;
536 if (attach)
537 attach(loader);
538 });
539};
540
541function getModuleSource(loader, module) {
542 return loader.normalize(module)
543 .then(function(normalized) {
544 return loader.locate({ name: normalized, metadata: {} });
545 })
546 .then(function(address) {
547 return loader.fetch({ address: address, metadata: {} });
548 })
549 .then(function(fetched) {
550 // allow to be a redirection module
551 var redirection = fetched.toString().match(/^\s*module\.exports = require\(\"([^\"]+)\"\);\s*$/);
552 if (redirection)
553 return getModuleSource(loader, redirection[1]);
554 return fetched;
555 })
556 .catch(function(err) {
557 console.log('Unable to find helper module "' + module + '". Make sure it is configured in the builder.');
558 throw err;
559 });
560}