Last active
March 13, 2019 12:14
-
-
Save Hp93/f20c6c52912933ba8cc449548b3b9875 to your computer and use it in GitHub Desktop.
durandal-bundler.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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