systemjs-builder
Version:
SystemJS Build Tool
561 lines (482 loc) • 19.2 kB
JavaScript
var Promise = require('bluebird');
var asp = require('bluebird').promisify;
var fs = require('fs');
var path = require('path');
var url = require('url');
var createHash = require('crypto').createHash;
var template = require('es6-template-strings');
var getAlias = require('./utils').getAlias;
var extend = require('./utils').extend;
var traverseTree = require('./arithmetic').traverseTree;
var verifyTree = require('./utils').verifyTree;
var getFormatHint = require('./utils').getFormatHint;
var compilerMap = {
'amd': '../compilers/amd',
'cjs': '../compilers/cjs',
'esm': '../compilers/esm',
'global': '../compilers/global',
'system': '../compilers/register',
'json': '../compilers/json'
};
// create a compile hash based on path + source + metadata + compileOpts
// one implication here is that plugins shouldn't rely on System.x checks
// as these will not be cache-invalidated but within the bundle hook is fine
function getCompileHash(load, compileOpts) {
return createHash('md5')
.update(JSON.stringify({
source: load.source,
metadata: load.metadata,
path: compileOpts.sourceMaps && load.path,
normalize: compileOpts.normalize,
anonymous: compileOpts.anonymous,
systemGlobal: compileOpts.systemGlobal,
static: compileOpts.static,
encodeNames: compileOpts.encodeNames,
sourceMaps: compileOpts.sourceMaps,
lowResSourceMaps: compileOpts.lowResSourceMaps
}))
.digest('hex');
}
function getEncoding(canonical, encodings, loader) {
// dont encode system modules
if (canonical[0] == '@' && canonical != '@dummy-entry-point' && loader.has(canonical))
return canonical;
// return existing encoding if present
if (encodings[canonical])
return encodings[canonical];
// search for the first available key
var highestEncoding = 9;
Object.keys(encodings).forEach(function(canonical) {
var encoding = encodings[canonical];
highestEncoding = Math.max(parseInt(encoding, '16'), highestEncoding);
});
highestEncoding++;
return encodings[canonical] = highestEncoding.toString(16);
}
function getName(encoding, encodings) {
var match
Object.keys(encodings).some(function(e) {
if (encodings[e] == encoding) {
match = e;
return true;
}
});
return match;
}
// used to support leading #!/usr/bin/env in scripts as supported in Node
var hashBangRegEx = /^\#\!.*/;
exports.compileLoad = compileLoad;
function compileLoad(loader, load, compileOpts, cache) {
// use cached if we have it
var cached = cache.loads[load.name];
if (cached && cached.hash == getCompileHash(load, compileOpts))
return Promise.resolve(cached.output);
// create a new load record with any necessary final mappings
function remapLoadRecord(load, mapFunction) {
load = extend({}, load);
load.name = mapFunction(load.name, load.name);
var depMap = {};
Object.keys(load.depMap).forEach(function(dep) {
depMap[dep] = mapFunction(load.depMap[dep], dep);
});
load.depMap = depMap;
return load;
}
var mappedLoad = remapLoadRecord(load, function(name, original) {
// do SFX encodings
if (compileOpts.encodeNames)
return getEncoding(name, cache.encodings, loader);
if (compileOpts.normalize && name.indexOf('#:') != -1)
throw new Error('Unable to build dependency ' + name + '. normalize must be disabled for bundles containing conditionals.');
return name;
});
var format = load.metadata.format;
if (format == 'defined')
return Promise.resolve({ source: compileOpts.systemGlobal + '.register("' + mappedLoad.name + '", [], function() { return { setters: [], execute: function() {} } });\n' });
if (format in compilerMap) {
if (format == 'cjs')
mappedLoad.source = mappedLoad.source.replace(hashBangRegEx, '');
return Promise.resolve()
.then(function() {
return require(compilerMap[format]).compile(mappedLoad, compileOpts, loader);
})
.then(function(output) {
// store compiled output in cache
cache.loads[load.name] = {
hash: getCompileHash(load, compileOpts),
output: output
};
return output;
})
.catch(function(err) {
// Traceur has a habit of throwing array errors
if (err instanceof Array)
err = err[0];
err.message = 'Error compiling ' + format + ' module "' + load.name + '" at ' + load.path + '\n\t' + err.message;
throw err;
});
}
return Promise.reject(new TypeError('Unknown module format ' + format));
}
// sort in reverse pre-order, filter modules to actually built loads (excluding conditionals, build: false)
// (exported for unit testing)
exports.getTreeModulesPostOrder = getTreeModulesPostOrder;
function getTreeModulesPostOrder(tree, traceOpts) {
var entryPoints = [];
// build up the map of parents of the graph
var entryMap = {};
var moduleList = Object.keys(tree).filter(function(module) {
return tree[module] !== false;
}).sort();
// 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
moduleList.forEach(function(entryPoint) {
traverseTree(tree, entryPoint, function(depName, parentName) {
// 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
moduleList.forEach(function(entryPoint) {
if (!entryMap[entryPoint])
entryPoints.push(entryPoint);
});
// now that we have the entry points, sort them alphabetically and
// run the traversal to get the ordered module list
entryPoints = entryPoints.sort();
var modules = [];
entryPoints.reverse().forEach(function(moduleName) {
traverseTree(tree, moduleName, function(depName, parentName) {
if (modules.indexOf(depName) == -1)
modules.push(depName);
}, traceOpts, true);
});
return {
entryPoints: entryPoints,
modules: modules.reverse()
};
}
// run the plugin bundle hook on the list of loads
// returns the assetList
exports.pluginBundleHook = pluginBundleHook;
function pluginBundleHook(loader, loads, compileOpts, outputOpts) {
var outputs = [];
// plugins have the ability to report an asset list during builds
var assetList = [];
var pluginLoads = {};
// store just plugin loads
loads.forEach(function(load) {
if (load.metadata.loader) {
var pluginLoad = extend({}, load);
pluginLoad.address = loader.baseURL + load.path;
(pluginLoads[load.metadata.loader] = pluginLoads[load.metadata.loader] || []).push(pluginLoad);
}
});
return Promise.all(Object.keys(pluginLoads).map(function(pluginName) {
var loads = pluginLoads[pluginName];
var loaderModule = loads[0].metadata.loaderModule;
if (loaderModule.listAssets)
return Promise.resolve(loaderModule.listAssets.call(loader.pluginLoader, loads, compileOpts, outputOpts))
.then(function(_assetList) {
assetList = assetList.concat(_assetList.map(function(asset) {
return {
url: asset.url,
type: asset.type,
source: asset.source,
sourceMap: asset.sourceMap
};
}));
});
}))
.then(function() {
return Promise.all(Object.keys(pluginLoads).map(function(pluginName) {
var loads = pluginLoads[pluginName];
var loaderModule = loads[0].metadata.loaderModule;
if (compileOpts.inlinePlugins) {
if (loaderModule.inline) {
return Promise.resolve(loaderModule.inline.call(loader.pluginLoader, loads, compileOpts, outputOpts));
}
// NB deprecate bundle hook for inline hook
else if (loaderModule.bundle) {
// NB deprecate the 2 argument form
if (loaderModule.bundle.length < 3)
return Promise.resolve(loaderModule.bundle.call(loader.pluginLoader, loads, extend(extend({}, compileOpts), outputOpts)));
else
return Promise.resolve(loaderModule.bundle.call(loader.pluginLoader, loads, compileOpts, outputOpts));
}
}
}));
})
.then(function(compiled) {
var outputs = [];
compiled = compiled || [];
compiled.forEach(function(output) {
if (output instanceof Array)
outputs = outputs.concat(output);
else if (output)
outputs.push(output);
});
return {
outputs: outputs,
assetList: assetList
};
});
}
exports.compileTree = compileTree;
function compileTree(loader, tree, traceOpts, compileOpts, outputOpts, cache) {
// verify that the tree is a tree
verifyTree(tree);
var ordered = getTreeModulesPostOrder(tree, traceOpts);
var inputEntryPoints;
// get entrypoints from graph algorithm
var entryPoints;
var modules;
var outputs = [];
var compilers = {};
return Promise.resolve()
.then(function() {
// compileOpts.entryPoints can be unnormalized
if (!compileOpts.entryPoints)
return [];
return Promise.all(compileOpts.entryPoints.map(function(entryPoint) {
return loader.normalize(entryPoint)
.then(function(normalized) {
return loader.getCanonicalName(normalized);
});
}))
.filter(function(inputEntryPoint) {
// skip conditional entry points and entry points not in the tree (eg rollup optimized out)
return !inputEntryPoint.match(/\#\:|\#\?|\#{/) && tree[inputEntryPoint];
})
})
.then(function(inputEntryPoints) {
entryPoints = inputEntryPoints || [];
ordered.entryPoints.forEach(function(entryPoint) {
if (entryPoints.indexOf(entryPoint) == -1)
entryPoints.push(entryPoint);
});
modules = ordered.modules.filter(function(moduleName) {
var load = tree[moduleName];
if (load.runtimePlugin && compileOpts.static)
throw new TypeError('Plugin ' + load.plugin + ' does not support static builds, compiling ' + load.name + '.');
return load && !load.conditional && !load.runtimePlugin;
});
if (compileOpts.encodeNames)
entryPoints = entryPoints.map(function(name) {
return getEncoding(name, cache.encodings, loader);
});
})
// create load output objects
.then(function() {
return Promise.all(modules.map(function(name) {
return Promise.resolve()
.then(function() {
var load = tree[name];
if (load === true)
throw new TypeError(name + ' was defined via a bundle, so can only be used for subtraction or union operations.');
return compileLoad(loader, tree[name], compileOpts, cache);
});
}));
})
.then(function(compiled) {
outputs = outputs.concat(compiled);
})
// run plugin bundle hook
.then(function() {
var pluginLoads = [];
modules.forEach(function(name) {
var load = tree[name];
pluginLoads.push(load);
// if we used Rollup, we should still run the bundle hook for the child loads that were compacted
if (load.compactedLoads)
load.compactedLoads.forEach(function(load) {
pluginLoads.push(load);
});
});
return pluginBundleHook(loader, pluginLoads, compileOpts, outputOpts);
})
.then(function(pluginResults) {
outputs = outputs.concat(pluginResults.outputs);
var assetList = pluginResults.assetList;
return Promise.resolve()
.then(function() {
// if any module in the bundle is AMD, add a "bundle" meta to the bundle
// this can be deprecated if https://github.com/systemjs/builder/issues/264 lands
if (modules.some(function(name) {
return tree[name].metadata.format == 'amd';
}) && !compileOpts.static)
outputs.unshift('"bundle";');
// static bundle wraps with a self-executing loader
if (compileOpts.static)
return wrapSFXOutputs(loader, tree, modules, outputs, entryPoints, compileOpts, cache);
return outputs;
})
.then(function(outputs) {
// NB also include all aliases of all entryPoints along with entryPoints
return {
outputs: outputs,
entryPoints: entryPoints,
assetList: assetList,
modules: modules.reverse()
};
});
});
}
exports.wrapSFXOutputs = wrapSFXOutputs;
function wrapSFXOutputs(loader, tree, modules, outputs, entryPoints, compileOpts, cache) {
var compilers = {};
var externalDeps = [];
Object.keys(tree).forEach(function(module) {
if (tree[module] === false && !loader.has(module))
externalDeps.push(module);
});
externalDeps.sort();
// determine compilers used
var legacyTranspiler = false;
modules.forEach(function(name) {
if (!legacyTranspiler && tree[name].metadata.originalSource)
legacyTranspiler = true;
compilers[tree[name].metadata.format] = true;
});
// include compiler helpers at the beginning of outputs
Object.keys(compilerMap).forEach(function(format) {
if (!compilers[format])
return;
var compiler = require(compilerMap[format]);
if (compiler.sfx)
outputs.unshift(compiler.sfx(loader));
});
// determine if the SFX bundle has any external dependencies it relies on
var globalDeps = [];
modules.forEach(function(name) {
var load = tree[name];
// check all deps are present
load.deps.forEach(function(dep) {
var key = load.depMap[dep];
if (!(key in tree) && !loader.has(key)) {
if (compileOpts.format == 'esm')
throw new TypeError('ESM static builds with externals only work when all modules in the build are ESM.');
if (externalDeps.indexOf(key) == -1)
externalDeps.push(key);
}
});
});
var externalDepIds = externalDeps.map(function(dep) {
if (compileOpts.format == 'global' ||
compileOpts.format == 'umd' && (compileOpts.globalName || Object.keys(compileOpts.globalDeps).length > 0)) {
var alias = getAlias(loader, dep);
var globalDep = compileOpts.globalDeps[dep] || compileOpts.globalDeps[alias];
if (!globalDep)
throw new TypeError('Global SFX bundle dependency "' + alias +
'" must be configured to an environment global via the globalDeps option.');
globalDeps.push(globalDep);
}
// remove external deps from calculated entry points list
var entryPointIndex = entryPoints.indexOf(dep);
if (entryPointIndex != -1)
entryPoints.splice(entryPointIndex, 1);
if (compileOpts.encodeNames)
return getEncoding(dep, cache.encodings, loader);
else
return dep;
});
// next wrap with the core code
return asp(fs.readFile)(path.resolve(__dirname, '../templates/sfx-core.min.js'))
.then(function(sfxcore) {
// for NodeJS execution to work correctly, we need to ensure the scoped module, exports and require variables are nulled out
outputs.unshift("var require = this.require, exports = this.exports, module = this.module;");
// if the first entry point is a dynamic module, then it is exportDefault always by default
var exportDefault = compileOpts.exportDefault;
var exportedLoad = tree[compileOpts.encodeNames && getName(entryPoints[0], cache.encodings) || entryPoints[0]];
if (exportedLoad && exportedLoad.metadata.format != 'system' && exportedLoad.metadata.format != 'esm')
exportDefault = true;
outputs.unshift(sfxcore.toString(), "(" + JSON.stringify(entryPoints) + ", " + JSON.stringify(externalDepIds) + ", " +
(exportDefault ? "true" : "false") + ", function(" + compileOpts.systemGlobal + ") {");
outputs.push("})");
return asp(fs.readFile)(path.resolve(__dirname, '../templates/sfx-' + compileOpts.format + '.js'));
})
// then include the sfx module format wrapper
.then(function(formatWrapper) {
outputs.push(template(formatWrapper.toString(), {
deps: externalDeps.map(function(dep) {
if (dep.indexOf('#:') != -1)
dep = dep.replace('#:/', '/');
var name = getAlias(loader, dep);
return name;
}),
globalDeps: globalDeps,
globalName: compileOpts.globalName
}));
})
// then wrap with the runtime
.then(function() {
if (!legacyTranspiler)
return;
// NB legacy runtime wrappings
var usesBabelHelpersGlobal = modules.some(function(name) {
return tree[name].metadata.usesBabelHelpersGlobal;
});
// regenerator runtime check
if (!usesBabelHelpersGlobal)
usesBabelHelpersGlobal = modules.some(function(name) {
return tree[name].metadata.format == 'esm' && cache.loads[name].output.source.match(/regeneratorRuntime/);
});
if (compileOpts.runtime && usesBabelHelpersGlobal)
return getModuleSource(loader, 'babel/external-helpers')
.then(function(source) {
outputs.unshift(source);
});
})
.then(function() {
if (!legacyTranspiler)
return;
// NB legacy runtime wrappings to eb deprecated
var usesTraceurRuntimeGlobal = modules.some(function(name) {
return tree[name].metadata.usesTraceurRuntimeGlobal;
});
if (compileOpts.runtime && usesTraceurRuntimeGlobal)
return getModuleSource(loader, 'traceur-runtime')
.then(function(source) {
// protect System global clobbering
outputs.unshift("(function(){ var curSystem = typeof System != 'undefined' ? System : undefined;\n" + source + "\nSystem = curSystem; })();");
});
})
// for AMD, CommonJS and global SFX outputs, add a "format " meta to support SystemJS loading
.then(function() {
if (compileOpts.formatHint)
outputs.unshift(getFormatHint(compileOpts));
})
.then(function() {
return outputs;
});
}
exports.attachCompilers = function(loader) {
Object.keys(compilerMap).forEach(function(compiler) {
var attach = require(compilerMap[compiler]).attach;
if (attach)
attach(loader);
});
};
function getModuleSource(loader, module) {
return loader.normalize(module)
.then(function(normalized) {
return loader.locate({ name: normalized, metadata: {} });
})
.then(function(address) {
return loader.fetch({ address: address, metadata: {} });
})
.then(function(fetched) {
// allow to be a redirection module
var redirection = fetched.toString().match(/^\s*module\.exports = require\(\"([^\"]+)\"\);\s*$/);
if (redirection)
return getModuleSource(loader, redirection[1]);
return fetched;
})
.catch(function(err) {
console.log('Unable to find helper module "' + module + '". Make sure it is configured in the builder.');
throw err;
});
}