UNPKG

33.8 kBJavaScriptView Raw
1var getCanonicalName = require('./utils').getCanonicalName;
2var glob = require('glob');
3var toFileURL = require('./utils').toFileURL;
4var fromFileURL = require('./utils').fromFileURL;
5var asp = require('bluebird').promisify;
6var fs = require('fs');
7var path = require('path');
8var extend = require('./utils').extend;
9var Promise = require('bluebird');
10var getPackage = require('./utils').getPackage;
11var getPackageConfigPath = require('./utils').getPackageConfigPath;
12var isPackageConfig = require('./utils').isPackageConfig;
13var parseCondition = require('./utils').parseCondition;
14var serializeCondition = require('./utils').serializeCondition;
15var dataUriToBuffer = require('data-uri-to-buffer');
16
17module.exports = Trace;
18
19function Trace(loader, traceCache) {
20 // when creating a new trace, we by default invalidate the freshness of the trace cache
21 Object.keys(traceCache).forEach(function(canonical) {
22 var load = traceCache[canonical];
23
24 if (load && !load.conditional)
25 load.fresh = false;
26 });
27
28
29 this.loader = loader;
30 // stored traced load records
31 this.loads = traceCache || {};
32 // in progress traces
33 this.tracing = {};
34}
35
36/*
37 * High-level functions
38 */
39var namedRegisterRegEx = /(System\.register(Dynamic)?|define)\(('[^']+'|"[^"]+")/g;
40Trace.prototype.traceModule = function(moduleName, traceOpts) {
41 var loader = this.loader;
42
43 return Promise.resolve(loader.normalize(moduleName))
44 .then(function(normalized) {
45 return traceCanonical(getCanonicalName(loader, normalized), traceOpts);
46 });
47};
48
49// given a module, return the conditional environment variation space
50Trace.getConditionalEnv = function(builder, loads, traceOpts) {
51 var conditionalEnvVariations = {};
52
53 Object.keys(loads).forEach(function(canonical) {
54 var load = loads[canonical];
55 if (!load.conditional)
56 return;
57
58 var envVariations = Trace.getConditionalResolutions(load.conditional, traceOpts.conditions, traceOpts.inlineConditions).conditionalEnvVariations;
59
60 Object.keys(envVariations).forEach(function(condition) {
61 if (envVariations[condition] instanceof Array) {
62 conditionalEnvVariations[condition] = conditionalEnvVariations[condition] || [];
63 envVariations[condition].forEach(function(conditionValue) {
64 if (conditionalEnvVariations[condition].indexOf(conditionValue) === -1)
65 conditionalEnvVariations[condition].push(conditionValue);
66 });
67 }
68 else {
69 conditionalEnvVariations[condition] = conditionalEnvVariations[condition] || envVariations[condition];
70 }
71 });
72 });
73 return conditionalEnvVariations;
74};
75
76Trace.prototype.traceCanonical = function(canonical, traceOpts) {
77 var self = this;
78
79 return self.getAllLoadRecords(canonical, traceOpts, traceOpts.conditions, traceOpts.inlineConditions, {}, [])
80 .then(function(loads) {
81 // if it is a bundle, we just use a regex to extract the list of loads
82 // as "true" records for subtraction arithmetic use only
83 var thisLoad = loads[canonical];
84
85 if (thisLoad && !thisLoad.conditional && thisLoad.metadata.bundle) {
86 namedRegisterRegEx.lastIndex = 0;
87 var curMatch;
88 while ((curMatch = namedRegisterRegEx.exec(thisLoad.source)))
89 loads[curMatch[3].substr(1, curMatch[3].length - 2)] = true;
90 }
91
92 return {
93 moduleName: canonical,
94 tree: loads
95 };
96 });
97}
98
99function isLoadFresh(load, loader, loads) {
100 if (load === undefined)
101 return false;
102
103 if (load === false)
104 return true;
105
106 if (load.configHash != loader.configHash)
107 return false;
108
109 if (load.fresh)
110 return true;
111
112 if (load.conditional)
113 return false;
114
115 if (load.plugin) {
116 if (!loader.pluginLoader.has(loader.pluginLoader.decanonicalize(load.plugin)))
117 return false;
118 }
119
120 // stat to check freshness
121 try {
122 var timestamp = fs.statSync(path.resolve(fromFileURL(loader.baseURL), load.path)).mtime.getTime();
123 }
124 catch(e) {}
125 return load.fresh = timestamp == load.timestamp;
126}
127
128/*
129 * Low-level functions
130 */
131// runs the pipeline hooks, returning the load record for a module
132Trace.prototype.getLoadRecord = function(canonical, traceOpts, parentStack) {
133 var loader = this.loader;
134 var loads = this.loads;
135
136 if (isLoadFresh(loads[canonical], loader, loads))
137 return Promise.resolve(loads[canonical]);
138
139 if (this.tracing[canonical])
140 return this.tracing[canonical];
141
142 var self = this;
143 var isPackageConditional = canonical.indexOf('/#:') != -1;
144
145 var curMap = loader.map;
146 loader.map = {};
147 var normalized = loader.decanonicalize(canonical);
148 loader.map = curMap;
149
150 return this.tracing[canonical] = Promise.resolve(normalized)
151 .then(function(normalized) {
152 // modules already set in the registry are system modules
153 if (loader.has(normalized))
154 return false;
155
156 // package conditional fallback normalization
157 if (!isPackageConditional)
158 normalized = normalized.replace('/#:', '/');
159
160 // -- conditional load record creation: sourceless intermediate load record --
161
162 // boolean conditional
163 var booleanIndex = canonical.lastIndexOf('#?');
164 if (booleanIndex != -1) {
165 var condition = canonical.substr(booleanIndex + 2);
166 if (condition.indexOf('|') == -1)
167 condition += '|default';
168 return {
169 name: canonical,
170 fresh: true,
171 conditional: {
172 condition: condition,
173 branch: canonical.substr(0, booleanIndex)
174 }
175 };
176 }
177
178 // package environment conditional
179 var pkgEnvIndex = canonical.indexOf('/#:');
180 if (pkgEnvIndex != -1) {
181 // NB handle a package plugin load here too
182 if (canonical.indexOf('!') != -1)
183 throw new Error('Unable to trace ' + canonical + ' - building package environment mappings of plugins is not currently supported.');
184
185 var pkgName = canonical.substr(0, pkgEnvIndex);
186 var subPath = canonical.substr(pkgEnvIndex + 3);
187
188 var normalizedPkgName = loader.decanonicalize(pkgName);
189
190 // record package config paths
191 var loadPackageConfig;
192 var packageConfigPath = getPackageConfigPath(loader.packageConfigPaths, normalizedPkgName);
193 if (packageConfigPath) {
194 loadPackageConfig = getCanonicalName(loader, packageConfigPath);
195 (loader.meta[packageConfigPath] = loader.meta[packageConfigPath] || {}).format = 'json';
196 }
197
198 // effective analog of the same function in SystemJS packages.js
199 // to work out the path with defaultExtension added.
200 // we cheat here and use normalizeSync to apply the right checks, while
201 // skipping any map entry by temporarily removing it.
202 var absURLRegEx = /^[^\/]+:\/\//;
203 function isPlain(name) {
204 return name[0] != '.' && name[0] != '/' && !name.match(absURLRegEx);
205 }
206 function getMapMatch(map, name) {
207 var bestMatch, bestMatchLength = 0;
208
209 for (var p in map) {
210 if (name.substr(0, p.length) == p && (name.length == p.length || name[p.length] == '/')) {
211 var curMatchLength = p.split('/').length;
212 if (curMatchLength <= bestMatchLength)
213 continue;
214 bestMatch = p;
215 bestMatchLength = curMatchLength;
216 }
217 }
218
219 return bestMatch;
220 }
221 function toPackagePath(subPath, isFallback) {
222 if (isPlain(subPath)) {
223 // plain name -> apply map
224 if (!isFallback)
225 return loader.normalize(subPath, normalizedPkgName + '/');
226 // if a fallback conditional map, only do global map, not package map
227 else
228 return loader.normalize(subPath);
229 }
230 else if (subPath == '.') {
231 return Promise.resolve(normalizedPkgName);
232 }
233 else if (subPath.substr(0, 2) == './') {
234 var pkgMap = pkg.map;
235 pkg.map = {};
236 var normalized = loader.normalizeSync(pkgName + '/' + subPath.substr(2));
237 pkg.map = pkgMap;
238 return Promise.resolve(normalized);
239 }
240 else {
241 return Promise.resolve(normalized);
242 }
243 }
244
245 var pkg
246 var envMap;
247 var metadata = {};
248
249 // ensure we have loaded any package config first
250 // NB this does not properly deal with package config file invalidation
251 // we should handle some kind of invalidation process where a package config file change
252 // must invalidate all load records as we can't know the scope of the normalization changes
253 return loader.normalize(normalizedPkgName)
254 .then(function() {
255 pkg = loader.packages[normalizedPkgName];
256
257 var mapMatch = getMapMatch(pkg.map, subPath);
258 envMap = pkg.map[mapMatch];
259
260 if (!envMap)
261 throw new Error('Package conditional ' + canonical + ' has no package conditional map present.');
262
263 // resolve the fallback
264 return toPackagePath(subPath, true);
265 })
266 .then(function(resolvedPath) {
267 // if the fallback is itself a conditional then use that directly
268 if (resolvedPath.match(/\#[\:\?\{]/))
269 return getCanonicalName(loader, resolvedPath);
270
271 // if the fallback is a system module then that is it
272 if (isPlain(resolvedPath))
273 return resolvedPath;
274
275 return loader.locate({ name: resolvedPath, metadata: metadata })
276 .then(function(address) {
277 // allow build: false trace opt-out
278 if (metadata.build === false)
279 return false;
280
281 // check if the fallback exists
282 return new Promise(function(resolve) {
283 fs.exists(fromFileURL(address), resolve);
284 })
285 .then(function(fallbackExists) {
286 if (fallbackExists)
287 return getCanonicalName(loader, resolvedPath);
288 });
289 });
290 })
291 .then(function(fallback) {
292 // environment trace
293 return Promise.all(Object.keys(envMap).map(function(envCondition) {
294 var mapping = envMap[envCondition];
295 var conditionObj = parseCondition(envCondition);
296
297 return loader.normalize(conditionObj.module, normalizedPkgName)
298 .then(function(conditionModule) {
299 conditionObj.module = getCanonicalName(loader, conditionModule);
300 return toPackagePath(mapping, false);
301 })
302 .then(function(normalizedMapping) {
303 return {
304 condition: serializeCondition(conditionObj),
305 branch: getCanonicalName(loader, normalizedMapping)
306 };
307 });
308 }))
309 .then(function(envs) {
310 return {
311 name: canonical,
312 fresh: true,
313 packageConfig: loadPackageConfig,
314 conditional: {
315 envs: envs,
316 fallback: fallback
317 }
318 };
319 });
320 });
321 }
322
323 // conditional interpolation
324 var interpolationRegEx = /#\{[^\}]+\}/;
325 var interpolationMatch = canonical.match(interpolationRegEx);
326 if (interpolationMatch) {
327 var condition = interpolationMatch[0].substr(2, interpolationMatch[0].length - 3);
328
329 if (condition.indexOf('|') == -1)
330 condition += '|default';
331
332 var metadata = {};
333 return Promise.resolve(loader.locate({ name: normalized.replace(interpolationRegEx, '*'), metadata: metadata }))
334 .then(function(address) {
335 if (address.substr(0, 8) != 'file:///' && !load.metadata.loader)
336 metadata.build = false;
337
338 // allow build: false trace opt-out
339 if (metadata.build === false)
340 return false;
341
342 // glob the conditional interpolation variations from the filesystem
343 var globIndex = address.indexOf('*');
344 return asp(glob)(fromFileURL(address), { dot: true, nobrace: true, noglobstar: true, noext: true, nodir: true })
345 .then(function(paths) {
346 var branches = {};
347 paths.forEach(function(path) {
348 path = toFileURL(path);
349
350 var pathCanonical = getCanonicalName(loader, path);
351 var interpolate = pathCanonical.substr(interpolationMatch.index, path.length - address.length + 1);
352
353 if (normalized.indexOf('!') != -1) {
354 if (loader.pluginFirst)
355 pathCanonical = getCanonicalName(loader, metadata.loader) + '!' + pathCanonical;
356 else
357 pathCanonical = pathCanonical + '!' + getCanonicalName(loader, metadata.loader);
358 }
359 branches[interpolate] = pathCanonical;
360 });
361
362 // if the condition values have been provided via traceOpts.conditions
363 // then add these to the glob variations as well
364 if (traceOpts.conditions[condition])
365 traceOpts.conditions[condition].forEach(function(c) {
366 if (branches[c])
367 return;
368 var branchCanonical = canonical.substr(0, interpolationMatch.index) + c + canonical.substr(interpolationMatch[0].length + interpolationMatch.index);
369 branches[c] = branchCanonical;
370 });
371
372 return {
373 name: canonical,
374 fresh: false, // we never cache conditional interpolates and always reglob
375 conditional: {
376 condition: condition,
377 branches: branches
378 }
379 };
380 });
381 });
382 }
383
384 // -- trace loader hooks --
385 var load = {
386 name: canonical,
387 // baseURL-relative path to address
388 path: null,
389 metadata: {},
390 deps: [],
391 depMap: {},
392 source: null,
393
394 // this is falsified by builder.reset to indicate we should restat
395 fresh: true,
396 // timestamp from statting the underlying file source at path
397 timestamp: null,
398 // each load stores a hash of the configuration from the time of trace
399 // configHash is set by the loader.config function of the builder
400 configHash: loader.configHash,
401
402 plugin: null,
403 runtimePlugin: false,
404
405 // plugins via syntax must build in the plugin package config
406 pluginConfig: null,
407
408 // packages have a config file that must be built in for bundles
409 packageConfig: null,
410 isPackageConfig: isPackageConfig(loader, canonical),
411
412 // these are only populated by the separate builder.getDeferredImports(tree) method
413 deferredImports: null,
414
415 // in the case of Rollup, a single load can represent several compacted loads
416 compactedLoads: null,
417 };
418 var curHook = 'locate';
419 var originalSource;
420 return Promise.resolve(loader.locate({ name: normalized, metadata: load.metadata}))
421 .then(function(address) {
422 curHook = '';
423
424 if (address.substr(0, 8) != 'file:///' && !load.metadata.loader)
425 load.metadata.build = false;
426
427 // build: false build config - null load record
428 if (load.metadata.build === false)
429 return false;
430
431 if (address.substr(0, 8) == 'file:///')
432 load.path = path.relative(fromFileURL(loader.baseURL), fromFileURL(address));
433
434 return Promise.resolve()
435 .then(function() {
436 // set load.plugin to canonical plugin name if a plugin load
437 if (load.metadata.loaderModule)
438 return Promise.resolve(loader.pluginLoader.normalize(load.metadata.loader, normalized))
439 .then(function(pluginNormalized) {
440 load.plugin = getCanonicalName(loader, pluginNormalized);
441
442 if (load.metadata.loaderModule &&
443 (load.metadata.loaderModule.build === false || Object.keys(load.metadata.loaderModule).length == 0 ||
444 (typeof load.metadata.loaderModule === 'function' || load.metadata.loaderModule.toString && load.metadata.loaderModule.toString() === 'Module' && typeof load.metadata.loaderModule.default === 'function')))
445 load.runtimePlugin = true;
446
447 if (pluginNormalized.indexOf('!') == -1 && !load.runtimePlugin && getPackage(loader.packages, pluginNormalized)) {
448 var packageConfigPath = getPackageConfigPath(loader.packageConfigPaths, pluginNormalized);
449 if (packageConfigPath) {
450 load.pluginConfig = getCanonicalName(loader, packageConfigPath);
451 (loader.meta[packageConfigPath] = loader.meta[packageConfigPath] || {}).format = 'json';
452 }
453 }
454 });
455 })
456 .then(function() {
457 if (load.runtimePlugin)
458 return load;
459
460 curHook = 'fetch';
461
462
463 return loader
464 .fetch({ name: normalized, metadata: load.metadata, address: address })
465
466 // Parse source map definitions inside of the source and apply it to the metadata if present.
467 .then(function (source) {
468 if (!traceOpts.sourceMaps || load.metadata.sourceMap)
469 return source;
470
471 // Search for the specified sourceMap definition in the files source.
472 var sourceMap = source.match(/^\s*\/\/\s*[#@] sourceMappingURL=([^\s'"]*)/m);
473
474 if (!sourceMap)
475 return source;
476
477 // Once the sourceMappingURL starts with `data:`, we have to parse it as an inline source map.
478 if (sourceMap[1].startsWith('data:')) {
479 load.metadata.sourceMap = JSON.parse(dataUriToBuffer(sourceMap[1]).toString('utf8'));
480 return source;
481 } else {
482 // Retrieve the path of the sourceMappingURL in relative to the
483 // relative path to the .map file
484 var sourceMapPath = path.join(path.dirname(fromFileURL(address)), sourceMap[1]);
485
486 return asp(fs.readFile)(sourceMapPath, 'utf8').then(function (sourceMap) {
487 load.metadata.sourceMap = JSON.parse(sourceMap);
488 return source;
489 })
490 // dont let a missing source map fail the entire build
491 .catch(function() {
492 return source;
493 });
494 }
495 })
496
497 .then(function(source) {
498 if (typeof source != 'string')
499 throw new TypeError('Loader fetch hook did not return a source string');
500 originalSource = source;
501 curHook = 'translate';
502
503 // default loader fetch hook will set load.metadata.timestamp
504 if (load.metadata.timestamp) {
505 load.timestamp = load.metadata.timestamp;
506 load.metadata.timestamp = undefined;
507 }
508
509 return loader.translate({ name: normalized, metadata: load.metadata, address: address, source: source }, traceOpts);
510 })
511 .then(function(source) {
512
513 load.source = source;
514 curHook = 'dependency parsing';
515
516 return loader.instantiate({ name: normalized, metadata: load.metadata, address: address, source: source });
517 })
518 .then(function(result) {
519
520 // allow late build opt-out
521 if (load.metadata.build === false)
522 return;
523
524 curHook = '';
525 if (!result)
526 throw new TypeError('Native ES Module builds not supported. Ensure transpilation is included in the loader pipeline.');
527
528 load.deps = result.deps;
529
530 // legacy es module transpilation translates to get the dependencies, so we need to revert for re-compilation
531 if (load.metadata.format == 'esm' && load.metadata.originalSource)
532 load.source = originalSource;
533
534 // record package config paths
535 if (getPackage(loader.packages, normalized) && !load.isPackageConfig) {
536 var packageConfigPath = getPackageConfigPath(loader.packageConfigPaths, normalized);
537 if (packageConfigPath) {
538 load.packageConfig = getCanonicalName(loader, packageConfigPath);
539 (loader.meta[packageConfigPath] = loader.meta[packageConfigPath] || {}).format = 'json';
540 }
541 }
542
543 // normalize dependencies to populate depMap
544 return Promise.all(result.deps.map(function(dep) {
545 return loader.normalize(dep, normalized, address)
546 .then(function(normalized) {
547 try {
548 load.depMap[dep] = getCanonicalName(loader, normalized);
549 }
550 catch(e) {
551 if (!traceOpts.excludeURLs || normalized.substr(0, 7) == 'file://')
552 throw e;
553 load.depMap[dep] = normalized;
554 }
555 });
556 }));
557 });
558 })
559 .catch(function(err) {
560 var msg = (curHook ? ('Error on ' + curHook + ' for ') : 'Error tracing ') + canonical + ' at ' + normalized;
561
562 if (parentStack)
563 parentStack.reverse().forEach(function(parent) {
564 msg += '\n\tLoading ' + parent;
565 });
566
567 // rethrow loader hook errors with the hook information
568 var newMsg = msg + '\n\t' + (err.message || err);
569 var newErr = new Error(newMsg, err.fileName, err.lineNumber);
570 newErr.originalErr = err.originalErr || err;
571 newErr.stack = msg + '\n\t' + (err.stack || err);
572 throw newErr;
573 })
574 .then(function() {
575 // format: 'system' as primary detector
576 if (load.metadata.format === 'register')
577 load.metadata.format = 'system';
578
579 // remove unnecessary metadata for trace
580 load.metadata.entry = undefined;
581 load.metadata.builderExecute = undefined;
582 load.metadata.parseTree = undefined;
583 load.metadata.ast = undefined;
584
585 if (load.metadata.build === false)
586 return false;
587
588 return load;
589 });
590 });
591 })
592 .then(function(load) {
593 self.tracing[canonical] = undefined;
594 return loads[canonical] = load;
595 }).catch(function(err) {
596 self.tracing[canonical] = undefined;
597 throw err;
598 });
599};
600
601/*
602 * Returns the full trace tree of a module
603 *
604 * - conditionalEnv represents the conditional tracing environment module values to impose on the trace
605 * -
606 *
607 * conditionalEnv provides canonical condition tracing rules of the form:
608 *
609 * {
610 * 'some/interpolation|value': true, // include ALL variations
611 * 'another/interpolation|value': false, // include NONE
612 * 'custom/interpolation|value': ['specific', 'values']
613 *
614 * // default BOOLEAN entry::
615 * '@system-env|browser': false,
616 * '@system-env|~browser': false
617 *
618 * // custom boolean entry
619 * // boolean only checks weak truthiness to allow overlaps
620 * '@system-env|~node': true
621 * }
622 *
623 */
624var systemModules = ['@empty', '@system-env', '@@cjs-helpers', '@@global-helpers'];
625Trace.prototype.getAllLoadRecords = function(canonical, traceOpts, canonicalConditionalEnv, canonicalInlineConditionals, curLoads, parentStack) {
626 var loader = this.loader;
627
628 curLoads = curLoads || {};
629
630 if (canonical in curLoads)
631 return curLoads;
632
633 var self = this;
634 return this.getLoadRecord(canonical, traceOpts, parentStack)
635 .then(function(load) {
636
637 // check this again – might have been loaded asynchronously since the last check
638 if (canonical in curLoads)
639 return curLoads;
640
641 // conditionals, build: false and system modules are falsy loads in the trace trees
642 // (that is, part of depcache, but not built)
643 // we skip system modules though
644 if (systemModules.indexOf(canonical) == -1)
645 curLoads[canonical] = load;
646
647 if (load) {
648 parentStack = parentStack.concat([canonical]);
649 return Promise.all(Trace.getLoadDependencies(load, traceOpts, canonicalConditionalEnv, canonicalInlineConditionals).map(function(dep) {
650 return self.getAllLoadRecords(dep, traceOpts, canonicalConditionalEnv, canonicalInlineConditionals, curLoads, parentStack);
651 }));
652 }
653 })
654 .then(function() {
655 return curLoads;
656 });
657};
658
659function conditionalComplement(condition) {
660 var conditionObj = parseCondition(condition);
661 conditionObj.negate = !conditionObj.negate;
662 return serializeCondition(conditionObj);
663}
664
665Trace.toCanonicalConditionalEnv = toCanonicalConditionalEnv;
666function toCanonicalConditionalEnv(loader, conditionalEnv) {
667 var canonicalConditionalEnv = {};
668
669 // convert boolean conditions of the form
670 // @system-env|browser: [true, false]
671 // @system-env|~browser: true
672 // into just:
673 // @system-env|browser: [true, false]
674 // to be deprecated in due course
675 Object.keys(conditionalEnv).map(function(m) {
676 var conditionObj = parseCondition(m);
677
678 if (!(conditionalEnv[m] instanceof Array)) {
679 if (!conditionObj.negate) {
680 conditionalEnv[m] = [conditionalEnv[m]];
681 }
682 else {
683 var complement = conditionalComplement(m);
684 if (conditionalEnv[complement]) {
685 if (conditionalEnv[complement] instanceof Array) {
686 if (conditionalEnv[complement].indexOf(!conditionalEnv[m]) === -1)
687 conditionalEnv[complement].push(!conditionalEnv[m]);
688 }
689 else {
690 if (!conditionalEnv[m] !== conditionalEnv[complement])
691 conditionalEnv[conditionalComplement(m)] = [conditionalEnv[conditionalComplement(m)], !conditionalEnv[m]];
692 }
693 }
694 else {
695 conditionalEnv[conditionalComplement(m)] = [!conditionalEnv[m]];
696 }
697 delete conditionalEnv[m];
698 }
699 }
700 });
701
702 return Promise.all(Object.keys(conditionalEnv).map(function(m) {
703 var conditionObj = parseCondition(m);
704 return loader.normalize(conditionObj.module)
705 .then(function(normalized) {
706 conditionObj.module = getCanonicalName(loader, normalized);
707 var canonicalCondition = serializeCondition(conditionObj);
708 canonicalConditionalEnv[canonicalCondition] = conditionalEnv[m];
709 });
710 }))
711 .then(function() {
712 return canonicalConditionalEnv;
713 });
714}
715
716/*
717 * to support static conditional builds, we use the conditional tracing options
718 * to inline resolved conditions for the trace
719 * basically rewriting the tree without any conditionals
720 * where conditions are still present or conflicting we throw an error
721 */
722Trace.prototype.inlineConditions = function(tree, loader, traceOpts) {
723 var self = this;
724
725 var inconsistencyErrorMsg = 'For static condition inlining only an exact environment resolution can be built, pending https://github.com/systemjs/builder/issues/311.';
726
727 // ensure we have no condition conflicts
728 for (var c in traceOpts.inlineConditions) {
729 if (traceOpts.inlineConditions[c].length > 1)
730 throw new TypeError('Error building condition ' + c + '. ' + inconsistencyErrorMsg);
731 }
732
733 var conditionalResolutions = {};
734 var importsSystemEnv = false;
735
736 // get the list of all conditional dependencies in the tree
737 var allConditionals = [];
738 Object.keys(tree).forEach(function(m) {
739 if (m.match(/#[\:\?\{]/) && allConditionals.indexOf(m) === -1)
740 allConditionals.push(m);
741
742 if (!tree[m] || tree[m].conditional)
743 return;
744
745 Object.keys(tree[m].depMap).forEach(function(d) {
746 var dep = tree[m].depMap[d];
747
748 if (dep.match(/#[\:\?\{]/) && allConditionals.indexOf(dep) === -1)
749 allConditionals.push(dep);
750 });
751 });
752
753 // determine the conditional resolutions
754 return Promise.all(allConditionals.map(function(c) {
755 return self.getLoadRecord(c, traceOpts)
756 .then(function(load) {
757 var branches = Trace.getConditionalResolutions(load.conditional, traceOpts.conditions, traceOpts.inlineConditions).branches;
758
759 if (branches.length === 1)
760 conditionalResolutions[c] = branches[0];
761 });
762 }))
763 .then(function() {
764 // resolve any chained conditionals
765 Object.keys(conditionalResolutions).forEach(function(c) {
766 var resolution = conditionalResolutions[c];
767
768 var seen = [];
769 while (conditionalResolutions[resolution] && seen.indexOf(resolution) == -1) {
770 seen.push(resolution);
771 resolution = conditionalResolutions[resolution];
772 conditionalResolutions[c] = resolution;
773 }
774 if (seen.indexOf(resolution) != -1)
775 throw new Error('Circular conditional resolution ' + seen.join(' -> ') + ' -> ' + resolution);
776 });
777
778 // finally we do a deep clone of the tree, applying the conditional resolutions as we go
779 // if we have a dependency on a condition not in the tree, we throw as it would be an unresolved external
780 var inlinedTree = {};
781 Object.keys(tree).forEach(function(m) {
782 var load = tree[m];
783
784 if (typeof load == 'boolean') {
785 inlinedTree[m] = load;
786 return;
787 }
788
789 if (load.conditional)
790 return;
791
792 var clonedLoad = extend({}, load);
793 clonedLoad.depMap = {};
794 Object.keys(load.depMap).forEach(function(d) {
795 var normalizedDep = load.depMap[d];
796
797 normalizedDep = conditionalResolutions[normalizedDep] || normalizedDep;
798
799 if (normalizedDep == '@system-env')
800 importsSystemEnv = true;
801
802 clonedLoad.depMap[d] = normalizedDep;
803 });
804
805 inlinedTree[m] = clonedLoad;
806 });
807
808 // if we explicitly import from the system environment, then we need to build it into a static build
809 // this is normally excluded as it is a system module in SystemJS but won't be available in static
810 // builds which is exactly what this function acts on
811 if (importsSystemEnv) {
812 inlinedTree['@system-env'] = {
813 name: '@system-env',
814 path: null,
815 metadata: {
816 format: 'json'
817 },
818 deps: [],
819 depMap: {},
820 source: JSON.stringify({
821 production: traceOpts.inlineConditions['@system-env|production'],
822 browser: traceOpts.inlineConditions['@system-env|browser'],
823 node: traceOpts.inlineConditions['@system-env|node'],
824 dev: traceOpts.inlineConditions['@system-env|dev'],
825 default: true
826 }),
827 fresh: true,
828 timestamp: null,
829 configHash: loader.configHash,
830 };
831 }
832
833 return inlinedTree;
834 });
835};
836
837/*
838 * Given a conditional load record, a conditional environment, and an inline conditional environment
839 * returns the list of possible conditional branch outcomes, the condition modules necessary
840 * in runtime to determine non-inlined conditions and the conditionalEnvVariations
841 * object representing the resultant condition space
842 */
843Trace.getConditionalResolutions = function(conditional, conditionalEnv, inlineConditionals) {
844 // flatten all conditions into a filtered array of condition, module branches
845 var branches = [];
846 var conditionModules = [];
847 var conditionalEnvVariations = {};
848
849 function traceCondition(condition, value) {
850 var conditionModule = parseCondition(condition).module;
851
852 if (inlineConditionals[condition])
853 return inlineConditionals[condition][0] === value;
854
855 if (conditionalEnv[condition] && conditionalEnv[condition].indexOf(value) === -1)
856 return false;
857
858 conditionalEnvVariations[condition] = conditionalEnvVariations[condition] || [];
859 if (conditionalEnvVariations[condition].indexOf(value) === -1)
860 conditionalEnvVariations[condition].push(value);
861
862 if (conditionModules.indexOf(conditionModule) == -1)
863 conditionModules.push(conditionModule);
864
865 return true;
866 }
867
868 // whether or not to include condition branch given our conditionalEnv
869 function booleanEnvTrace(condition) {
870 var conditionObj = parseCondition(condition);
871
872 if (conditionObj.negate)
873 return traceCondition(conditionalComplement(condition), false);
874 else
875 return traceCondition(condition, true);
876 }
877
878 // { condition, branch } boolean conditional
879 if (conditional.branch) {
880 branches.push(booleanEnvTrace(conditional.condition) ? conditional.branch: '@empty');
881 }
882
883 // { envs: [{condition, branch},...], fallback } package environment map
884 else if (conditional.envs) {
885 var doFallback = true;
886 conditional.envs.forEach(function(env) {
887 if (doFallback && booleanEnvTrace(env.condition))
888 branches.push(env.branch);
889
890 // if we're specifically not tracing the negative of this condition
891 // then we stop the fallback branch from building
892 if (!booleanEnvTrace(conditionalComplement(env.condition)))
893 doFallback = false;
894 });
895 if (doFallback && conditional.fallback)
896 branches.push(conditional.fallback);
897 }
898
899 // { condition, branches } conditional interpolation
900 else if (conditional.branches) {
901 Object.keys(conditional.branches).forEach(function(branch) {
902 if (traceCondition(conditional.condition, branch))
903 branches.push(conditional.branches[branch]);
904 });
905 }
906
907 return {
908 branches: branches,
909 conditionModules: conditionModules,
910 conditionalEnvVariations: conditionalEnvVariations
911 };
912};
913
914// Returns the ordered immediate dependency array from the trace of a module
915Trace.getLoadDependencies = function(load, traceOpts, canonicalConditionalEnv, canonicalInlineConditionals) {
916 canonicalConditionalEnv = canonicalConditionalEnv || {};
917
918 var deps = [];
919
920 // conditional load records have their branches all included in the trace
921 if (load.conditional) {
922 var resolutions = Trace.getConditionalResolutions(load.conditional, canonicalConditionalEnv, canonicalInlineConditionals);
923
924 if (traceOpts.tracePackageConfig && load.packageConfig)
925 deps.push(load.packageConfig);
926
927 resolutions.conditionModules.forEach(function(conditionModule) {
928 if (deps.indexOf(conditionModule) == -1)
929 deps.push(conditionModule);
930 });
931
932 return deps.concat(resolutions.branches);
933 }
934
935 // trace the plugin as a dependency
936 if (traceOpts.traceRuntimePlugin && load.runtimePlugin)
937 deps.push(load.plugin);
938
939 // plugins by syntax build in their config
940 if (traceOpts.tracePackageConfig && load.pluginConfig)
941 deps.push(load.pluginConfig);
942
943 // add the dependencies
944 load.deps.forEach(function(dep) {
945 deps.push(load.depMap[dep]);
946 });
947
948 // trace the package config if necessary
949 if (traceOpts.tracePackageConfig && load.packageConfig)
950 deps.push(load.packageConfig);
951
952 return deps;
953};