Skip to content

Instantly share code, notes, and snippets.

@Hp93
Last active March 13, 2019 12:14
Show Gist options
  • Save Hp93/f20c6c52912933ba8cc449548b3b9875 to your computer and use it in GitHub Desktop.
Save Hp93/f20c6c52912933ba8cc449548b3b9875 to your computer and use it in GitHub Desktop.
durandal-bundler.js
var _fs = require('fs'),
_path = require('path'),
_es = require('event-stream'),
_glob = require('glob'),
_ = require('lodash')._,
_gutil = require('gulp-util'),
_rjs = require('gulp-requirejs'),
_merge = require('deeply'),
_Q = require('q'),
_esprima = require('esprima'),
_uglifyjs = require('uglify-js');
var PLUGIN_NAME = 'gulp-durandaljs',
durandalDynamicTransitions = ['transitions/entrance'],
durandalDynamicPlugins = [
'plugins/dialog', 'plugins/history', 'plugins/http',
'plugins/observable', 'plugins/router', 'plugins/serializer', 'plugins/widget'
],
defOptions = {
baseDir: 'app',
main: 'main.js',
extraModules: [],
durandalDynamicModules: true,
verbose: false,
output: undefined,
minify: false,
require: undefined,
rjsConfigAdapter: function (cfg) { return cfg; },
almond: false,
moduleFilter: function (m) { return true; },
pluginMap: {
'.html': 'text',
'.json': 'text',
'.txt': 'text',
'.css': 'css'
},
outMain: 'main.js',
outBaseDir: 'app',
paths: { 'requireLib': '../Scripts/require', }, // path is relative to baseDir
include: ['requireLib'],
bundles: [
// { name: '', modules: [] }
],
dynamicBundles: [],
};
module.exports = function durandalBundler(userOptions) {
var stream = _es.through();
var options = _.defaults(userOptions || {}, defOptions);
var baseDir = options.baseDir;
var mainFile = options.main ? _path.join(baseDir, options.main) : undefined;
var almondWrapper = (function () {
var almond = options.almond,
almondPath = typeof (almond) === 'string' ? almond : _path.join(__dirname, 'res/custom-almond.js');
if (!almond) {
return undefined;
}
return {
start: '(function() {\n' + _fs.readFileSync(almondPath, { encoding: 'utf-8' }),
end: '}());'
};
})();
var dynamicModules = (function () {
if (!mainFile) {
return options.durandalDynamicModules ?
[].concat(durandalDynamicPlugins, durandalDynamicTransitions) : [];
}
var mainFileContent = _fs.readFileSync(mainFile, { encoding: 'utf-8' }),
plugins = mainFileContent.match(/['"]?plugins['"]?\s*:/) ? durandalDynamicPlugins : [],
transitions = mainFileContent.match(/['"]?transitions['"]?\s*:/) ? durandalDynamicTransitions : [];
return [].concat(plugins, transitions);
})();
// scan custom bundles' modules
var bundles = (function () {
var bundles = [];
for (let i = 0; i < options.bundles.length; i++) {
const bundleSetting = options.bundles[i];
const bundleName = bundleSetting.name;
const modules = bundleSetting.modules;
var bundle = {
name: bundleName,
modules: getExplicitModules(baseDir, options, modules),
};
bundles.push(bundle);
}
return bundles;
})();
// scan main.js file for requirejs config
var originalMainConfig = (function () {
var fileContents = _fs.readFileSync(mainFile, 'utf8');
var astRoot = _esprima.parse(fileContents, {
loc: true
});
var jsConfig, foundConfig;
traverse(astRoot, function (node) {
var arg,
requireType = 'requirejsConfig';
var argPropName = 'arguments';
if (requireType && (requireType === 'require' ||
requireType === 'requirejs' ||
requireType === 'requireConfig' ||
requireType === 'requirejsConfig')) {
arg = node[argPropName] && node[argPropName][0];
if (arg && arg.type === 'ObjectExpression') {
stringData = nodeToString(fileContents, arg);
jsConfig = stringData.value;
foundRange = stringData.range;
return false;
}
} else {
arg = getRequireObjectLiteral(node);
if (arg) {
stringData = nodeToString(fileContents, arg);
jsConfig = stringData.value;
foundRange = stringData.range;
return false;
}
}
});
if (jsConfig) {
// Eval the config
// var quoteRegExp = /(:\s|\[\s*)(['"])/;
// var quoteMatch = quoteRegExp.exec(jsConfig);
// quote = (quoteMatch && quoteMatch[2]) || '"';
foundConfig = eval('(' + jsConfig + ')');
}
return foundConfig;
})();
// scan all modules inside app directory
var scannedModules = (function () {
var relativeToBaseDir = _path.relative.bind(_path, baseDir),
jsFiles = (function () {
var expandedJsFiles = _.flatten([expand(baseDir, '/**/*.js')]);
return _.unique(mainFile ? [mainFile].concat(expandedJsFiles) : expandedJsFiles);
})(),
jsModules = jsFiles.map(relativeToBaseDir).map(stripExtension),
pluggedFiles = _.flatten(_.map(_.keys(options.pluginMap), function (ext) { return expand(baseDir, '/**/*' + ext); })),
pluggedModules = pluggedFiles.map(relativeToBaseDir).map(function (m) { return options.pluginMap[_path.extname(m)] + '!' + m; });
return { js: jsModules, plugged: pluggedModules };
})();
// get all modules to include in the main bundle (exclude modules from custom bundles)
var allMainModules = (function () {
var modules = _.flatten([scannedModules.js, options.extraModules || [], dynamicModules, scannedModules.plugged])
.map(fixSlashes);
var include = _.filter(modules, options.moduleFilter);
var exclude = _.reject(modules, options.moduleFilter);
// exclude modules from custom bundles
var allBundleModules = _.flatten(_.map(bundles, function (bundleSetting) {
return bundleSetting.modules;
}));
include = _.reject(include, function (e) {
return allBundleModules.map(fixSlashes).indexOf(e) >= 0;
});
return { include: _.unique(include), exclude: _.unique(exclude) };
})();
var insertRequireModules = (function () {
if (typeof (options.require) === 'string' || _.isArray(options.require)) {
return _.flatten([options.require]);
}
else if (options.require === true || (options.almond && options.require !== false)) {
return [allMainModules.include[0]];
}
return undefined;
})();
var rjsConfig = {
logLevel: options.verbose ? 0 : 4,
baseUrl: baseDir,
mainConfigFile: mainFile,
paths: _.assign(originalMainConfig.paths, options.paths),
include: _.flatten([allMainModules.include, ['requireLib',]]),
exclude: allMainModules.exclude,
out: options.outMain,
optimize: options.minify ? 'uglify2' : 'none',
preserveLicenseComments: !options.minify,
generateSourceMaps: false,
insertRequire: insertRequireModules,
wrap: almondWrapper
};
rjsConfig = options.rjsConfigAdapter(rjsConfig);
stream.on('error', function (e) {
_gutil.log('Durandal ' + _gutil.colors.red(e.message));
stream.end();
});
// First run r.js to produce its default (non-bundle-aware) output. In the process,
// we capture the list of modules it wrote.
var primaryPromise = getRjsOutput(rjsConfig);
// Next, take the above list of modules, and for each configured bundle, write out
// the bundle's .js file, excluding any modules included in the primary output. In
// the process, capture the list of modules included in each bundle file.
var bundlePromises = _.map(bundles || {}, function (bundleSetting) {
var bundleModules = bundleSetting.modules;
var bundleName = bundleSetting.name;
return primaryPromise.then(function (primaryOutput) {
return getRjsOutput({
out: bundleName + ".js",
baseUrl: rjsConfig.baseUrl,
paths: rjsConfig.paths,
include: bundleModules,
exclude: primaryOutput.modules,
optimize: options.minify ? 'uglify2' : 'none',
preserveLicenseComments: !options.minify,
}, bundleName);
});
});
// Next, produce the "final" primary output by waiting for all the above to complete, then
// concatenating the bundle config (list of modules in each bundle) to the end of the
// primary file.
var finalPrimaryPromise = _Q
.all([primaryPromise].concat(bundlePromises))
.then(function (allOutputs) {
var primaryOutput = allOutputs[0];
var allBundleOutputs = _.reject(allOutputs.slice(1), function (item) { return isDynamicBundle(options.dynamicBundles, item.itemName); });
var bundleConfig = _.object(allBundleOutputs.map(function (bundleOutput) {
return [bundleOutput.itemName, bundleOutput.modules]
}));
var bundleConfigCode = '\nrequire.config(' +
JSON.stringify({
baseUrl: options.outBaseDir,
bundles: bundleConfig
}, true, 2) + ');\n';
return new _gutil.File({
path: primaryOutput.file.path,
contents: new Buffer(primaryOutput.file.contents.toString() + bundleConfigCode),
});
});
// Convert the N+1 promises (N bundle files, 1 final primary file) into a single stream for gulp to await
var allFilePromises = pluckBundlePromiseArray(bundlePromises, options)
// .concat(pluckPromiseArray(dynamicBundlePromises, 'file'))
.concat(finalPrimaryPromise);
// .concat(createMapPromises);
return _es.merge.apply(_es, allFilePromises.map(promiseToStream));
};
function promiseToStream(promise) {
var stream = _es.pause();
promise.then(function (result) {
stream.resume();
stream.end(result);
}, function (err) {
throw err;
});
return stream;
}
function streamToPromise(stream) {
// Of course, this relies on the stream producing only one output. That is the case
// for all uses in this file (wrapping rjs output, which is always one file).
var deferred = _Q.defer();
stream.pipe(_es.through(function (item) {
deferred.resolve(item);
}));
stream.on('error', function(er){
console.log(er.message);
});
return deferred.promise;
}
function pluckBundlePromiseArray(promiseArray, options) {
return promiseArray.map(function (promise) {
return promise.then(function (result) {
if (options.dynamicBundles.indexOf(result.itemName) >= 0) {
// include bundle's map
var bundleConfig = {};
bundleConfig[result.itemName] = result.modules;
var bundleConfigCode = '\nrequire.config(' +
JSON.stringify({ bundles: bundleConfig }, true, 2) + ');\n';
var minified = _uglifyjs.minify({ 'file': bundleConfigCode });
return new _gutil.File({
path: result.file.path,
contents: new Buffer(result.file.contents.toString() + minified.code),
});
} else {
return result.file;
}
});
});
}
function getRjsOutput(options, itemName) {
// Capture the list of written modules by adding to an array on each onBuildWrite callback
var modulesList = [],
patchedOptions = _merge({}, options, {
onBuildWrite: function (moduleName, path, contents) {
modulesList.push(moduleName);
return contents;
}
}),
rjsOutputPromise = streamToPromise(_rjs(patchedOptions));
return rjsOutputPromise.then(function (file) {
return { itemName: itemName, file: file, modules: modulesList, exclude: patchedOptions.exclude };
});
}
function stripExtension(p) {
return p.substr(0, p.length - _path.extname(p).length);
}
function expand(baseDir, p) {
return _glob.sync(_path.normalize(_path.join(baseDir, p)));
}
function fixSlashes(p) {
try {
return p.replace(new RegExp('\\\\', 'g'), '/');
} catch (error) {
console.log(error);
}
}
function traverse(object, visitor) {
var child;
if (!object) {
return;
}
if (visitor.call(null, object) === false) {
return false;
}
for (var i = 0, keys = Object.keys(object); i < keys.length; i++) {
child = object[keys[i]];
if (typeof child === 'object' && child !== null) {
if (traverse(child, visitor) === false) {
return false;
}
}
}
}
function getRequireObjectLiteral(node) {
if (node.id && node.id.type === 'Identifier' &&
(node.id.name === 'require' || node.id.name === 'requirejs') &&
node.init && node.init.type === 'ObjectExpression') {
return node.init;
}
}
function nodeToString(contents, node) {
var extracted,
loc = node.loc,
lines = contents.split('\n'),
firstLine = loc.start.line > 1 ?
lines.slice(0, loc.start.line - 1).join('\n') + '\n' :
'',
preamble = firstLine +
lines[loc.start.line - 1].substring(0, loc.start.column);
if (loc.start.line === loc.end.line) {
extracted = lines[loc.start.line - 1].substring(loc.start.column,
loc.end.column);
} else {
extracted = lines[loc.start.line - 1].substring(loc.start.column) +
'\n' +
lines.slice(loc.start.line, loc.end.line - 1).join('\n') +
'\n' +
lines[loc.end.line - 1].substring(0, loc.end.column);
}
return {
value: extracted,
range: [
preamble.length,
preamble.length + extracted.length
]
};
}
function isDynamicBundle(dynamicBundleNames, bundleName) {
return dynamicBundleNames.indexOf(bundleName) >= 0;
}
function getExplicitModules(baseDir, options, modules) {
var relativeToBaseDir = _path.relative.bind(_path, baseDir);
var jsFiles = (function () {
var expandedJsFiles = _.flatten([_.map(modules, function (p) {
if (!p.trim()) {
return;
}
else if (/.+\/\*$/g.test(p)) {
// if path has format: abc/*
return expand(baseDir, p + '.js');
}
else if (/.+\/\*\*$/g.test(p)) {
// if path has format: abc/**
return expand(baseDir, p + '/*.js');
} else {
return _path.normalize(_path.join(baseDir, p));
}
})]);
return _.unique(expandedJsFiles);
})();
var jsModules = jsFiles.map(relativeToBaseDir).map(stripExtension);
var pluggedFiles =
_.unique(
_.flatten(_.map(modules, function (bundleModulePath) {
return _.map(_.keys(options.pluginMap), function (ext) {
if (!bundleModulePath.trim()) {
return;
}
else if (/.+\/\*$/g.test(bundleModulePath)) {
// if path has format: abc/*
return expand(baseDir, bundleModulePath + ext);
}
else if (/.+\/\*\*$/g.test(bundleModulePath)) {
// if path has format: abc/**
return expand(baseDir, bundleModulePath + '/*' + ext);
}
else if (new RegExp(".+" + ext + "$", "g").test(bundleModulePath)) {
return _path.normalize(_path.join(baseDir, bundleModulePath));
}
else {
return "";
}
})
}))
);
pluggedFiles = _.reject(pluggedFiles, function (e) { return !!!e; });
var pluggedModules = pluggedFiles.map(relativeToBaseDir).map(function (m) { return options.pluginMap[_path.extname(m)] + '!' + m; });
return _.flatten([jsModules.map(fixSlashes), pluggedModules.map(fixSlashes)])
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment