systemjs-builder
Version:
SystemJS Build Tool
356 lines (303 loc) • 12.7 kB
JavaScript
var rollup = require('rollup');
var traverseTree = require('./arithmetic').traverseTree;
var getConditionModule = require('./trace').getConditionModule;
var extend = require('./utils').extend;
var getAlias = require('./utils').getAlias;
var pluginBundleHook = require('./compile').pluginBundleHook;
exports.rollupTree = function(loader, tree, entryPoints, traceOpts, compileOpts, outputOpts) {
/*
* 1. Determine the tree entry points and optimization points
*
* eg for the tree:
*
* A -> B -> C
* D -> C
*
* A and D are the entry points.
* Optimization points are ES module entry points to be optimized
*
*/
entryPoints = entryPoints.concat([]);
var optimizationPoints = [];
var entryMap = {};
function isESM(moduleName) {
return tree[moduleName] && tree[moduleName].metadata && tree[moduleName].metadata.format == 'esm' && !tree[moduleName].metadata.originalSource && !tree[moduleName].source.match(/\s+import\s*\(/);
}
// for each module in the tree, we traverse the whole tree
// we then relate each module in the tree to the first traced entry point
Object.keys(tree).forEach(function(entryPoint) {
traverseTree(tree, entryPoint, function(depName, parentName) {
// esm from a non-esm parent means this is an optimization entry point from the linking alogorithm perspective
if (parentName && isESM(depName) && !isESM(parentName) && optimizationPoints.indexOf(depName) == -1)
optimizationPoints.push(depName);
// if we have a entryMap for the given module, then stop
if (entryMap[depName])
return false;
if (parentName)
entryMap[depName] = entryPoint;
}, traceOpts);
});
// the entry points are then the modules not represented in entryMap
Object.keys(tree).forEach(function(entryPoint) {
if (!entryMap[entryPoint] && tree[entryPoint] && entryPoints.indexOf(entryPoint) == -1)
entryPoints.push(entryPoint);
});
// if all the entry points are ES modules,
// then we can create a single dummy entry point
// that represents the tree
var esmEntryPoints = 0;
entryPoints.forEach(function(entryPoint) {
if (tree[entryPoint].metadata && tree[entryPoint].metadata.format == 'esm')
esmEntryPoints ++;
});
if (esmEntryPoints > 1 && esmEntryPoints == entryPoints.length) {
var dummySource = 'export * from "' + entryPoints[0] + '";\n';
var dummyDepMap = {};
entryPoints.forEach(function(entryPoint) {
dummyDepMap[entryPoint] = entryPoint;
dummySource += 'import "' + entryPoint + '";';
});
tree['@dummy-entry-point'] = {
name: '@dummy-entry-point',
path: null,
metadata: { format: 'esm' },
deps: entryPoints,
depMap: dummyDepMap,
source: dummySource
};
entryPoints = ['@dummy-entry-point'];
}
// optimization points are then es module entry points
entryPoints.forEach(function(entryPoint) {
if (isESM(entryPoint) && optimizationPoints.indexOf(entryPoint) == -1)
optimizationPoints.push(entryPoint);
});
/*
* 2. Determine unoptimizable modules, splitting them out into their own optimization points
*
* eg for the tree:
* A -> B -> C -> D
* E -> C -> D
*
* A, E are top-level entry points detected by the previous step
* (and hence optimization points if they are es modules)
* C is not optimizable because it has two unique parent entry points
* (which is what this step looks for)
* So C becomes its own optimization point
* Leading to D inlining into C and B inlining into A
*
*/
// for each module in the tree, we track its parent optimization point
// as soon as a module has two parent entry points, it is not optimizable
// and we set it to undefined here. It then becomes its own optimizationPoint.
var optimizationParentMap = {};
// build up the parent entry point map as above
// we use for over forEach because this list will grow as we go
for (var i = 0; i < optimizationPoints.length; i++) {
var entryPoint = optimizationPoints[i];
traverseTree(tree, entryPoint, function(depName, parentName) {
// we only traverse ES module tree subgraphs
if (!isESM(depName))
return false;
if (depName == entryPoint)
return;
// dont traverse through other entry points
if (optimizationPoints.indexOf(depName) != -1)
return false;
if (!optimizationParentMap[depName]) {
optimizationParentMap[depName] = entryPoint;
return;
}
// module in two separate entry point graphs -> it becomes its own optimization entry point
if (optimizationParentMap[depName] != entryPoint) {
optimizationParentMap[depName] = undefined;
// this new optimization point will then be traversed in turn as part of this loop later
optimizationPoints.push(depName);
}
}, traceOpts);
}
/*
* 3. Given complete optimization points, populate subgraph externals
*
* eg for the graph
* A -> B -> C
*
* Where A is the optimization point, and C is not ESM, another optimization point,
* or not contained in our build tree, then we mark 'C' as an external.
*
* That is, optimizationGraphExternals[A] = [C]
*
* This externals input is used in the Rollup API.
* This way we just optimize B into A, retaining an explicit dependency on C.
*/
var inlinedModules = [];
var optimizationGraphExternals = {};
optimizationPoints.forEach(function(entryPoint) {
// the subgraph object is the list of modules in the subgraph
// and the list of modules that are "external" boundaries of the subgraph
var externals = [];
// this traversal is a bit odd, since we need to traverse the full
// dependency graph to detect externals, not just the direct build graph
traverseTree(tree, entryPoint, function(depName, parentName) {
if (!isESM(depName) || (depName != entryPoint && optimizationPoints.indexOf(depName) != -1))
return false;
var depLoad = tree[depName];
depLoad.deps && depLoad.deps.forEach(function(depName) {
depName = depLoad.depMap[depName];
if (depName == entryPoint)
return;
// anything not ESM, not in the tree, or an optimization point, is external
if (!isESM(depName) || optimizationPoints.indexOf(depName) != -1) {
if (externals.indexOf(depName) == -1)
externals.push(depName);
}
else {
if (inlinedModules.indexOf(depName) == -1)
inlinedModules.push(depName);
}
}, traceOpts);
});
optimizationGraphExternals[entryPoint] = externals;
});
// finally we rollup each optimization graph
var rolledUpTree = {};
Object.keys(tree).forEach(function(moduleName) {
if (inlinedModules.indexOf(moduleName) == -1)
rolledUpTree[moduleName] = tree[moduleName];
});
// compute the inlineMap
var inlineMap = {};
inlinedModules.forEach(function(moduleName) {
var optimizationParent = optimizationParentMap[moduleName];
(inlineMap[optimizationParent] = inlineMap[optimizationParent] || []).push(moduleName);
});
// if every module in the tree is rolled-up, then we can do a full tree rollup
var fullTreeRollup = entryPoints.length == 1 && optimizationPoints.length == 1 && Object.keys(optimizationGraphExternals).length == 1;
return Promise.all(Object.keys(optimizationGraphExternals).map(function(entryPoint) {
var externals = optimizationGraphExternals[entryPoint];
var loadList = [];
var entryPointLoad;
// if all externals are outside the tree then this really is a full tree rollup
// also @node/x requires mean we do need sfx core (pending a better output of these)
if (fullTreeRollup)
externals.forEach(function(external) {
if (external.substr(0, 5) == '@node' || tree[external])
fullTreeRollup = false;
});
var aliasedExternals = externals.map(function(external) {
var alias = getAlias(loader, external) || externals;
if (alias.indexOf('#:') != -1)
alias = alias.replace('#:', '/');
return alias;
});
return rollup.rollup({
entry: entryPoint,
external: aliasedExternals,
acorn: {
allowReserved: true,
ecmaVersion: 8
},
plugins: [{
resolveId: function(id, importer, options) {
var resolved = importer ? tree[importer].depMap[id] : id;
var externalIndex = externals.indexOf(resolved);
if (externalIndex != -1)
return aliasedExternals[externalIndex];
return resolved;
},
load: function(id, options) {
if (loadList.indexOf(tree[id]) == -1)
loadList.push(tree[id]);
loadList.push(tree[id]);
return {
code: tree[id].metadata.originalSource || tree[id].source,
map: tree[id].metadata.sourceMap
};
}
}],
onwarn: function(message) {}
})
.then(function(bundle) {
entryPointLoad = tree[entryPoint];
var defaultExport = compileOpts.defaultExport;
if (entryPointLoad.metadata.format == 'register')
throw new Error('Assertion failed: internal format should be "system" not "register".');
if (entryPointLoad.metadata.format != 'esm' && entryPointLoad.metadata.format != 'system')
defaultExport = true;
var generateOptions = {
format: 'es',
sourceMap: !!compileOpts.sourceMaps,
exports: defaultExport ? 'default' : 'named',
dest: 'output.js' // workaround for rollup/rollup#1015
};
// for a full tree rollup, we pass all the output options into rollup itself
if (fullTreeRollup) {
generateOptions.format = compileOpts.format;
if (generateOptions.format == 'global')
generateOptions.format = 'iife';
if (generateOptions.format == 'esm')
generateOptions.format = 'es';
if ((generateOptions.format == 'iife' || generateOptions.format == 'umd') &&
!compileOpts.globalName)
throw new Error('The globalName option must be set for full-tree rollup global and UMD builds.');
if (compileOpts.globalName)
generateOptions.name = compileOpts.globalName;
if (compileOpts.globalDeps)
generateOptions.globals = compileOpts.globalDeps;
}
return bundle.generate(generateOptions);
})
.then(function(output) {
// convert sources list into paths
if (output.map) {
output.map.sources = output.map.sources.map(function(name) {
name = loader.getCanonicalName(loader.decanonicalize(name));
return tree[name] && tree[name].path || loader.decanonicalize(name);
});
}
if (fullTreeRollup)
return {
source: output.code,
sourceMap: output.map
};
// replace the entry point module itself with the inlined subgraph module
var curInlined = inlineMap[entryPoint] || [];
// the process of running rollup will itself normalize all dependencies
// the depMap then just becomes the identity map for non-externals
var inlinedDepMap = {};
aliasedExternals.forEach(function(dep, index) {
inlinedDepMap[dep] = externals[index];
});
rolledUpTree[entryPoint] = extend(extend({}, entryPointLoad), {
deps: aliasedExternals,
depMap: inlinedDepMap,
metadata: extend(extend({}, entryPointLoad.metadata), {
originalSource: undefined,
sourceMap: output.map
}),
source: output.code,
compactedLoads: loadList
});
});
}))
.then(function(outputs) {
if (fullTreeRollup) {
// for the full tree rollup case, we need to run the plugin bundle hook as we skip compile entirely
return pluginBundleHook(loader, Object.keys(tree).map(function(name) {
return tree[name];
}).filter(function(load) {
return load;
}), compileOpts, outputOpts)
.then(function(pluginResult) {
return {
outputs: outputs.concat(pluginResult.outputs),
assetList: pluginResult.assetList
};
});
}
return {
tree: rolledUpTree,
inlineMap: inlineMap
};
});
};