lmd
Version:
LMD: Lazy Module Declaration
1,644 lines (1,439 loc) • 56.1 kB
JavaScript
/**
* LMD Builder
*
* @author Mikhail Davydov
* @licence MIT
*/
var fs = require('fs'),
inherits = require('util').inherits,
path = require('path'),
uglifyCompress = require("uglify-js"),
csso = require('csso'),
SourceMapGenerator = require('source-map').SourceMapGenerator,
SourceMapConsumer = require('source-map').SourceMapConsumer,
colors = require('colors'),
parser = uglifyCompress.parser,
uglify = uglifyCompress.uglify,
DataStream = require(__dirname + '/../lib/data_stream.js'),
lmdCoverage = require(__dirname + '/../lib/coverage_apply.js'),
common = require(__dirname + '/../lib/lmd_common.js'),
Writer = require(__dirname + '/../lib/lmd_writer.js'),
Cli = require(__dirname + '/cli_messages.js'),
assembleLmdConfig = common.assembleLmdConfig;
var LMD_JS_SRC_PATH = common.LMD_JS_SRC_PATH;
var LMD_PLUGINS = common.LMD_PLUGINS;
/**
* LmdBuilder LMD Package Builder
*
* LmdBuilder is readable stream
*
* @constructor
*
* @inherits Stream
*
* @param {String} configFile
* @param {Object} [options]
*
* @example
*
* new LmdBuilder("config.json", {
* warn: true
* })
* .pipe(process.stdout);
*/
var LmdBuilder = function (configFile, options) {
DataStream.call(this);
this.options = options || {};
var self = this;
// apply config
this.configFile = configFile;
this.init();
// Bundles streams
this.bundles = {};
// Let return instance before build
this.buildConfig = this.compileConfig(configFile, self.options);
this.makeEmptyStreamsUnreadable(this.buildConfig);
var isFatalErrors = !this.isAllModulesExists(this.buildConfig);
if (isFatalErrors) {
this.readable = false;
this.style.readable = false;
this.sourceMap.readable = false;
} else {
this._initBundlesStreams(this.buildConfig.bundles);
}
process.nextTick(function () {
if (!isFatalErrors) {
if (configFile) {
var buildResult = self.build(self.buildConfig);
self.write(buildResult.source);
self.style.write(buildResult.style);
self.sourceMap.write(buildResult.sourceMap.toString());
self._streamBundles(buildResult.bundles);
} else {
self.log.write('lmd usage:\n\t ' + 'lmd'.blue + ' ' + 'config.lmd.js(on)'.green + ' [output.lmd.js]\n');
}
} else {
self.printFatalErrors(self.buildConfig);
}
self.closeStreams();
});
};
/**
* LMD Package Watcher
*
* @constructor
*
* @inherits Stream
*
* @param {String} configFile
* @param {Object} [options]
*
* @example
*
* new LmdBuilder.watch("config.json", {
* warn: true
* })
* .log.pipe(process.stdout);
*/
LmdBuilder.watch = function (configFile, options) {
DataStream.call(this);
this.options = options || {};
var self = this;
this.configFile = configFile;
this.init();
this.sourceMap.readable = false;
this.readable = false;
// Let return instance before build
this.watchConfig = this.compileConfig(self.configFile, self.options);
this.makeEmptyStreamsUnreadable(this.watchConfig);
var isFatalErrors = !this.isAllModulesExists(this.watchConfig);
if (isFatalErrors) {
this.readable = false;
}
process.nextTick(function () {
if (!isFatalErrors) {
if (!configFile) {
return;
}
if (typeof self.watchConfig.output === 'string') {
self.fsWatch(self.watchConfig);
return;
} else {
self.log.write('ERRO'.red.inverse + ':' + ' Check your config file. "output" parameter should be a {String} eg "../path"'.red + '\n');
return;
}
} else {
self.printFatalErrors(self.watchConfig);
}
self.closeStreams();
});
};
inherits(LmdBuilder, DataStream);
// Share prototype
LmdBuilder.watch.prototype = LmdBuilder.prototype;
/**
* Applies defaults
*
* @param {Object} options
*
* @return {Object}
*/
LmdBuilder.prototype.defaults = function (options) {
options = options || {};
if (typeof options.warn === "undefined") {
options.warn = true;
}
if (typeof options.log === "undefined") {
options.log = true;
}
if (typeof options.output === 'undefined') {
options.output = false;
}
if (typeof options.styles_output === 'undefined' && typeof options.output === 'string') {
options.styles_output = path.join(
path.dirname(options.output),
path.basename(options.output, path.extname(options.output)) + '.css'
);
}
if (typeof options.bundles_callback === 'undefined') {
options.bundles_callback = '_' + Math.round(Math.random() * 0xFFFFFFFF).toString(16);
}
Object.keys(options.bundles || {}).forEach(function (name) {
options.bundles[name].bundles_callback = options.bundles_callback;
});
return options;
};
/**
* Common init for LmdBuilder and LmdBuilder.watch
*/
LmdBuilder.prototype.init = function () {
this._closeStreams = this.closeStreams.bind(this);
this.configDir = path.dirname(this.configFile);
this.flagToOptionNameMap = LMD_PLUGINS;
/**
* Build log
*
* @type {Stream}
*/
this.log = new DataStream();
/**
* Source Map
*
* @type {Stream}
*/
this.sourceMap = new DataStream();
/**
* Style
*
* @type {Stream}
*/
this.style = new DataStream();
process.once('exit', this._closeStreams);
};
/**
* Makes empty streams unreadable
* @param config
*/
LmdBuilder.prototype.makeEmptyStreamsUnreadable = function (config) {
// modules
this.readable = this._has(config, 'modules');
// source map
this.sourceMap.readable = this._has(config, 'modules');
// styles
this.style.readable = this._has(config, 'styles');
};
/**
*
* @param configFile
* @param options
*/
LmdBuilder.prototype.compileConfig = function (configFile, options) {
return this.defaults(assembleLmdConfig(configFile, Object.keys(this.flagToOptionNameMap), options));
};
/**
*
* @param buildConfig
* @return {Boolean}
*/
LmdBuilder.prototype.isAllModulesExists = function (buildConfig) {
var self = this;
var modules = buildConfig.modules || {},
bundles = buildConfig.bundles || {},
styles = buildConfig.styles || [],
bundle;
for (var moduleName in modules) {
if (!modules[moduleName].is_exists) {
return false;
}
}
for (var i = 0, c = styles.length; i < c; i++) {
if (!styles[i].is_exists) {
return false;
}
}
// Check bundles and sub-bundles
for (var bundleName in bundles) {
bundle = bundles[bundleName];
if (bundle instanceof Error) {
return false;
}
var isModulesExists = Object.keys(bundles).every(function (bundleName) {
return self.isAllModulesExists(bundles[bundleName]);
});
if (!isModulesExists) {
return false;
}
}
return true;
};
/**
*
* @param buildConfig
*/
LmdBuilder.prototype.printFatalErrors = function (buildConfig) {
var modules = buildConfig.modules || {},
bundles = buildConfig.bundles || {},
styles = buildConfig.styles || {},
bundle,
projectRoot = buildConfig.path || buildConfig.root,
errorMessage;
for (var moduleName in modules) {
if (!modules[moduleName].is_exists) {
errorMessage = 'Module "' + moduleName.cyan + '": "' +
modules[moduleName].originalPath.toString().red;
errorMessage += '" (';
// check multi path modules
errorMessage += [].concat(modules[moduleName].path).map(function (path) {
return String(path)[fs.existsSync(path) ? 'green' : 'red'];
}).join(', ');
errorMessage += ') is not exists. Project root: "' + String(projectRoot).green + '". ';
this.error(errorMessage);
}
}
for (var i = 0, c = styles.length; i < c; i++) {
if (!styles[i].is_exists) {
errorMessage = 'Style (' +
styles[i].path.toString().red + ') is not exists. ' +
'Project root: "' + String(projectRoot).green + '". ';
this.error(errorMessage);
}
}
// Check bundles and sub-bundles
for (var bundleName in bundles) {
bundle = bundles[bundleName];
if (bundle instanceof Error) {
errorMessage = 'Bundle "' + bundleName.cyan + '": (' +
bundle.message.red + ') is not exists. ' +
'Project root: "' + String(projectRoot).green + '". ';
this.error(errorMessage);
} else {
// check modules of bundle
this.printFatalErrors(bundle);
}
}
};
/**
* Closes all streams and make it unreadable
*/
LmdBuilder.prototype.closeStreams = function () {
this.end();
this.log.end();
this.style.end();
this.sourceMap.end();
this._closeBundleStreams();
process.removeListener('exit', this._closeStreams);
};
/**
* LMD template
*
* @param {Object} data
*/
LmdBuilder.prototype.templatePackage = function (data) {
return (data.build_info ? data.build_info + '\n' : '') +
data.lmd_js +
'\n(' +
data.global + ',' +
data.lmd_main + ',' +
data.lmd_modules + ',' +
data.modules_options + ',' +
data.options +
');\n';
};
/**
* LMD template
*
* @param {Object} data
* @param {Object} data.bundles_callback
* @param {Object} data.build_info
* @param {Object} data.lmd_main
* @param {Object} data.lmd_modules
* @param {Object} data.modules_options
*/
LmdBuilder.prototype.templateBundle = function (data) {
return (data.build_info ? data.build_info + '\n' : '') +
data.bundles_callback + '(' +
(data.lmd_main ? data.lmd_main + ',' : '') +
data.lmd_modules + ',' +
data.modules_options +
');\n';
};
/**
* Compress code using UglifyJS
*
* @param {String} code
* @param {Object} pack_options
* @param {Boolean} pack_options.strict_semicolons
* @param {Object} pack_options.mangle_options
* @param {Object} pack_options.squeeze_options
* @param {Object} pack_options.gen_options
*
* @returns {String} compressed code
*/
LmdBuilder.prototype.compress = function (code, pack_options) {
pack_options = typeof pack_options === "object" ? pack_options : {};
return uglifyCompress(code, pack_options);
};
/**
* Optimizes lmd code
*
* @param {String} lmd_js_code
*
* @returns {String}
*/
LmdBuilder.prototype.optimizeLmdSource = function (lmd_js_code) {
var walker = uglify.ast_walker();
/**
* Uses variable sandbox for create replacement map
*
* @param {Object} ast toplevel AST
*
* @return {Object} {name: replaceName} map
*/
function getSandboxMap(ast) {
var map = {};
walker.with_walkers({
// looking for first var with sandbox item;
"var" : function (vars) {
for (var i = 0, c = vars.length, varItem; i < c; i++) {
varItem = vars[i];
if (varItem[0] === 'sandbox') {
varItem[1][1].forEach(function (objectVar) {
map[objectVar[0]] = objectVar[1][1];
});
throw 0;
}
}
}
}, function () {
try {
return walker.walk(ast);
} catch (e) {}
});
return map;
}
/**
* Brakes sendbox in one module
*
* @param {Object} ast
* @param {Object} replaceMap
*
* @return {Object} call AST
*/
function breakSandbox(ast, replaceMap) {
var sandboxName = ast[2][0] || 'sb';
var newAst = walker.with_walkers({
// lookup for dot
// looking for this pattern
// ["dot", ["name", "sb"], "require"] -> ["name", map["require"]]
"dot" : function () {
if (this[1] && this[1][0] === "name" && this[1][1] === sandboxName) {
var sourceName = this[2];
return ["name", replaceMap[sourceName]];
}
}
}, function () {
return walker.walk(ast);
});
// remove IEFE's `sb` or whatever argument
newAst[1][2] = [];
return newAst;
}
/**
* Brake sandbox: Using UglifyJS AST and sandbox variable in lmd.js file
* replace all sb.smth with actual value of sandbox[smth]
* than delete sandbox variable from lmd.js and all modules
*
* @param {Object} ast
*
* @returns {Object} toplevel AST
*/
function brakeSandboxes(ast) {
var map = getSandboxMap(ast),
isSandboxVariableWiped = false;
return walker.with_walkers({
// lookup for modules
// looking for this pattern
// [ 'call',
// [ 'function', null, [ 'sb' ], [ [Object] ] ],
// [ [ 'name', 'sandbox' ] ] ]
"call" : function (content) {
if (this[2] &&
this[2].length > 0 &&
this[2][0][0] === "name" &&
this[2][0][1] === "sandbox" &&
this[1] &&
this[1][0] === "function"
) {
// 1. remove sandbox argument
this[2] = [];
// 2. break sandbox in each module
return breakSandbox(this, map);
}
},
// wipe sandobx variable
"var": function () {
if (isSandboxVariableWiped) {
return;
}
for (var i = 0, c = this[1].length, varItem; i < c; i++) {
varItem = this[1][i];
if (varItem[0] === 'sandbox') {
isSandboxVariableWiped = true;
this[1].splice(i, 1);
return this;
}
}
}
}, function () {
return walker.walk(ast);
});
}
/**
* Collects all plugins events with usage and event index
*
* {
* eventIndex: 3, // relative event index
* on: 1, // number of listeners
* trigger: 2 // number of triggers
* }
*
* @param {Object} ast toplevel AST
*
* @return {Object} toplevel AST
*/
function getEvents(ast) {
var usage = {},
eventIndex = 0;
walker.with_walkers({
// looking for first var with sandbox item;
"call" : function () {
if (this[1] && this[2][0]) {
var functionName = this[1][1];
switch (functionName) {
case "lmd_on":
case "lmd_trigger":
var eventName = this[2][0][1];
if (!usage[eventName]) {
usage[eventName] = {
on: 0,
trigger: 0,
eventIndex: eventIndex
};
eventIndex++;
}
if (functionName === "lmd_on") {
usage[eventName].on++;
} else {
usage[eventName].trigger++;
}
break;
}
}
}
}, function () {
return walker.walk(ast);
});
return usage;
}
/**
* Wipes lmd_on, lmd_trigger, lmd_events variables from source
*
* @param {Object} ast
*
* @return {Object} modified ast
*/
function wipeLmdEvents(ast) {
var itemsToWipe = ['lmd_on', 'lmd_trigger', 'lmd_events'];
return walker.with_walkers({
// wipe lmdEvents variables
"var": function () {
if (!itemsToWipe.length) {
return;
}
for (var i = 0, c = this[1].length, varItem; i < c; i++) {
varItem = this[1][i];
if (varItem) {
var itemIndex = itemsToWipe.indexOf(varItem[0]);
if (itemIndex !== -1) {
itemsToWipe.splice(itemIndex, 1);
this[1].splice(i, 1);
i--;
}
}
}
}
}, function () {
return walker.walk(ast);
});
}
/**
* Optimizes number of lmd_{on|trigger} calls
*
* @param {Object} ast toplevel AST
*
* @return {Object} loplevel AST
*/
function reduceAndShortenLmdEvents(ast) {
var events = getEvents(ast),
isWipeLmdEvents = true;
for (var eventName in events) {
if (isWipeLmdEvents) {
if (events[eventName].on !== 0 && events[eventName].trigger !== 0) {
// something is calling events
isWipeLmdEvents = false;
}
}
}
// If no lmd_trigger and lmd_on calls
// than delete them plus lmd_events from lmd.js code
if (isWipeLmdEvents) {
ast = wipeLmdEvents(ast);
}
ast = walker.with_walkers({
// looking for first var with sandbox item;
"call" : function () {
if (this[1] && this[2][0]) {
var functionName = this[1][1],
eventName,
eventDescriptor;
switch (functionName) {
case "lmd_on":
eventName = this[2][0][1];
eventDescriptor = events[eventName];
// if no event triggers (no lmd_trigger(event_name,...))
// delete all lmd_on(event_name,...) statements
if (eventDescriptor.trigger === 0) {
return ["stat"]; // wipe statement = return empty statement - ;
}
// Shorten event names: Using UglifyJS AST find all event names
// from lmd_trigger and lmd_on and replace them with corresponding numbers
//console.log(this);
this[2][0] = ["num", eventDescriptor.eventIndex];
break;
case "lmd_trigger":
eventName = this[2][0][1];
eventDescriptor = events[eventName];
// if no event listeners (no lmd_on(event_name,...))
// replace all lmd_trigger(event_name, argument, argument)
// expressions with array [argument, argument]
if (eventDescriptor.on === 0) {
// if parent is statement -> return void
// to prevent loony arrays eg ["pewpew", "ololo"];
if (walker.parent()[0] === "stat") {
return ["stat"]; // wipe statement = return empty statement - ;
}
/*
[
"call",
["name", "lmd_trigger"],
[
["string", "lmd-register:call-sandboxed-module"],
["name", "moduleName"],
["name", "require"]
]
]
--->
[
"array",
[
["name", "moduleName"],
["name", "require"]
]
]
*/
return ["array", this[2].slice(1)];
}
// Shorten event names: Using UglifyJS AST find all event names
// from lmd_trigger and lmd_on and replace them with corresponding numbers
this[2][0] = ["num", eventDescriptor.eventIndex];
break;
}
}
}
}, function () {
return walker.walk(ast);
});
// #52 optimise constant expressions like [main][0]
ast = walker.with_walkers({
"sub": function () {
// Looking for this pattern
// [ 'sub', [ 'array', [ [Object], [Object] ] ], [ 'num', 1 ] ]
if (this[1][0] === "array" && this[2][0] === "num") {
var isConstantArray = this[1][1].every(function (item) {
return item[0] === "num" ||
item[0] === "string" ||
item[0] === "name" ||
item[0] === "array" ||
item[0] === "object";
});
if (isConstantArray) {
var index = this[2][1];
/*
[main][0]
--->
main
*/
return this[1][1][index];
}
}
}
}, function () {
return walker.walk(ast);
});
return ast;
}
var ast = parser.parse(lmd_js_code);
ast = brakeSandboxes(ast);
ast = reduceAndShortenLmdEvents(ast);
var code = uglify.gen_code(ast);
// wipe tail ;
code = this.removeTailSemicolons(code);
return code;
};
/**
* Removes tail semicolons
*
* @param {String} code
*
* @return {String}
*/
LmdBuilder.prototype.removeTailSemicolons = function (code) {
return code.replace(/\n*;$/, '');
};
/**
* JSON escaper
*
* @param file
*/
LmdBuilder.prototype.escape = function (file) {
return JSON.stringify(file);
};
/**
* Module code renderer
*
* @param {Array} config
* @param {Object} modulesBundle
* @param {String} modulesBundle.main
* @param {Array} modulesBundle.modules
* @param {Object} modulesBundle.options
* @param {Boolean} isOptimizeLmd
*
* @returns {String}
*/
LmdBuilder.prototype.renderLmdPackage = function (config, modulesBundle, isOptimizeLmd) {
var lmd_js = fs.readFileSync(path.join(LMD_JS_SRC_PATH, 'lmd.js'), 'utf8'),
lmd_main = modulesBundle.main,
lmd_modules = modulesBundle.modules,
modules_options = modulesBundle.options,
result;
// Apply patch if LMD package in cache Mode
lmd_js = this.patchLmdSource(lmd_js, config);
if (isOptimizeLmd) {
lmd_js = this.optimizeLmdSource(lmd_js);
}
lmd_modules = '{\n' + lmd_modules.join(',\n') + '\n}';
var options = {},
version = config.cache ? config.version : false,
stats_host = config.stats_auto || false,
promise = config.promise || false,
bundle = config.bundle ? config.bundles_callback : false;
// if version passed -> module will be cached
if (version) {
options.version = version;
}
if (stats_host) {
options.stats_host = stats_host;
}
if (promise) {
options.promise = promise;
}
if (bundle) {
options.bundle = bundle;
}
var userPlugin;
for (var userPluginName in config.plugins) {
userPlugin = config.plugins[userPluginName];
if (userPlugin.isOk && userPlugin.options && !options[userPlugin.name]) {
options[userPlugin.name] = userPlugin.options;
}
}
options = JSON.stringify(options);
result = this.templatePackage({
build_info: this._getBundleBanner(config),
lmd_js: lmd_js,
global: config.global || 'this',
lmd_main: lmd_main || 'function(){}',
lmd_modules: lmd_modules,
modules_options: JSON.stringify(modules_options),
options: options
});
return result;
};
/**
* Styles renderer
*
* @param {Array} styles
* @param {Boolean} isOptimizeCss
*
* @returns {String}
*/
LmdBuilder.prototype.renderStyles = function (styles, isOptimizeCss) {
if (!styles || !styles.length) {
return '';
}
var allCss = styles
.filter(function (style) {
return style.is_exists;
})
.map(function (style) {
return fs.readFileSync(style.path, 'utf8');
})
.join('\n');
return isOptimizeCss ? csso.justDoIt(allCss, true) : allCss;
};
/**
* Module code renderer
*
* @param {Array} config
* @param {Object} modulesBundle
* @param {String} modulesBundle.main
* @param {Array} modulesBundle.modules
* @param {Object} modulesBundle.options
*
* @returns {String}
*/
LmdBuilder.prototype.renderLmdBundle = function (config, modulesBundle) {
return this.templateBundle({
lmd_main: modulesBundle.main,
bundles_callback: config.bundles_callback,
build_info: this._getBundleBanner(config),
lmd_modules: '{\n' + modulesBundle.modules.join(',\n') + '\n}',
modules_options: JSON.stringify(modulesBundle.options)
});
};
LmdBuilder.prototype._getBundleBanner = function (config) {
// If exists return
if (typeof config.banner === 'string') {
return config.banner;
}
// Else create default
var configFile = path.basename(this.configFile),
mixinFiles = (config.mixins || []).map(function (mixin) {
return path.basename(mixin);
});
return '// This file was automatically generated from "' + configFile + '"' +
(mixinFiles.length ? ' using mixins "' + mixinFiles.join('", "') + '"' : '');
};
/**
* Patches lmd source
*
* @param {String} lmd_js
* @param {Object} config
*
* @returns {String}
*/
LmdBuilder.prototype.patchLmdSource = function (lmd_js, config) {
var optionNames,
flagName;
/**
* Applies or removes block from lmd_js
*
* @param {String} optionName block name eg $P.CACHE
* @param isApply apply or remove block
* @param isInline block is inline (based on block comment)
*
* @returns {Boolean} true if block was found, false - not found
*/
var preProcessBlock = function (optionName, isApply, isInline) {
// /*if ($P.CSS || $P.JS || $P.ASYNC) {*/
var inlinePreprocessorBlock = isInline ? '/*if (' + optionName + ') {*/' : 'if (' + optionName + ') {',
bracesCounter = 0,
startIndex = lmd_js.indexOf(inlinePreprocessorBlock),
startLength = inlinePreprocessorBlock.length,
endIndex = startIndex + inlinePreprocessorBlock.length,
endLength = isInline ? 5 : 1;
if (startIndex === -1) {
return false;
}
// lookup for own }
while (lmd_js.length > endIndex) {
if (lmd_js[endIndex] === '{') {
bracesCounter++;
}
if (lmd_js[endIndex] === '}') {
bracesCounter--;
}
// found!
if (bracesCounter === -1) {
if (isInline) {
// step back
endIndex -= 2;
} else {
// remove leading spaces from open part
while (startIndex) {
startIndex--;
startLength++;
if (lmd_js[startIndex] !== '\t' && lmd_js[startIndex] !== ' ') {
startIndex++;
startLength--;
break;
}
}
// remove leading spaces from close part
while (endIndex) {
endIndex--;
endLength++;
if (lmd_js[endIndex] !== '\t' && lmd_js[endIndex] !== ' ') {
endIndex++;
endLength--;
break;
}
}
// add front \n
endLength++;
startLength++;
}
if (isApply) {
// wipe preprocessor blocks only
// open
lmd_js = lmd_js.substr(0, startIndex) + lmd_js.substr(startIndex + startLength);
// close
lmd_js = lmd_js.substr(0, endIndex - startLength) + lmd_js.substr(endIndex + endLength - startLength);
if (!isInline) {
// indent block back
var blockForIndent = lmd_js.substr(startIndex, endIndex - startLength - startIndex);
blockForIndent = blockForIndent
.split('\n')
.map(function (line) {
return line.replace(/^\s{4}/, '');
})
.join('\n');
lmd_js = lmd_js.substr(0, startIndex) + blockForIndent + lmd_js.substr(endIndex - startLength);
}
} else {
// wipe all
lmd_js = lmd_js.substr(0, startIndex) + lmd_js.substr(endIndex + endLength);
}
break;
}
endIndex++;
}
return true;
};
// Add plugins
var pluginsRequireList = {},
pluginsCode = '';
// Collect plugins code
for (flagName in this.flagToOptionNameMap) {
var plugins = this.flagToOptionNameMap[flagName].require;
if (typeof plugins !== "undefined") {
if (typeof plugins === "string") {
plugins = [plugins];
}
plugins.forEach(function (pluginName) {
// require once
if (config[flagName] && !pluginsRequireList[pluginName]) {
pluginsCode += fs.readFileSync(path.join(LMD_JS_SRC_PATH, 'plugin', pluginName), 'utf8') + "\n\n";
pluginsRequireList[pluginName] = true;
}
});
}
}
// Collect user plugins
var userPlugin;
for (var userPluginName in config.plugins) {
userPlugin = config.plugins[userPluginName];
if (userPlugin.isOk) {
pluginsCode += userPlugin.code + "\n\n";
}
}
// Apply plugins code
lmd_js = lmd_js.replace("/*{{LMD_PLUGINS_LOCATION}}*/", pluginsCode);
// Add includes
for (flagName in this.flagToOptionNameMap) {
optionNames = this.flagToOptionNameMap[flagName].preprocess || [];
optionNames.forEach(function (optionName) {
/*if ($P.STATS) include('stats.js');*/
var includePattern = new RegExp('\\/\\*\\if \\(' + optionName.replace(/\$/g, '\\$').replace(/\|/g, '\\|') + '\\)\\s+include\\(\'([a-z-\\/_\\.]+)\'\\);?\\s*\\*\\/', ''),
patchContent = '',
match;
// Add plugin
while (true) {
if (config[flagName]) {
// apply: remove left & right side
match = lmd_js.match(includePattern);
if (match && match[1]) {
patchContent = fs.readFileSync(path.join(LMD_JS_SRC_PATH, 'plugin', match[1]), 'utf8');
} else {
break;
}
} else {
break;
}
lmd_js = lmd_js.replace(includePattern, patchContent);
}
});
}
// Apply IF statements
for (flagName in this.flagToOptionNameMap) {
optionNames = this.flagToOptionNameMap[flagName].preprocess || [];
// first are inline
optionNames.forEach(function (optionName) {
// apply all blocks
if (config[flagName]) {
// 1. inline
while (preProcessBlock(optionName, true, true));
// 2. blocks
while (preProcessBlock(optionName, true, false));
}
});
}
// Wipe IF statements
for (flagName in this.flagToOptionNameMap) {
optionNames = this.flagToOptionNameMap[flagName].preprocess || [];
// first are inline
optionNames.forEach(function (optionName) {
// wipe all blocks
if (!config[flagName]) {
// 1. inline
while (preProcessBlock(optionName, false, true));
// 2. blocks
while (preProcessBlock(optionName, false, false));
}
});
}
return lmd_js;
};
/**
* Performs configuration
*/
LmdBuilder.prototype.configure = function () {
if (!this.configFile) {
this.log.write('lmd usage:\n\t lmd config.lmd.js(on) [output.lmd.js]\n');
return false;
}
return true;
};
/**
* Collecting sandboxed modules using merged config
*
* @param modulesStruct
*
* @returns {Array}
*/
LmdBuilder.prototype.getSandboxedModules = function (modulesStruct, config) {
// TODO(azproduction) Backward capability
var result = config.sandbox || {};
for (var moduleName in modulesStruct) {
if (modulesStruct[moduleName].is_sandbox) {
result[moduleName] = true;
}
}
return result;
};
/**
* Watch the module files, rebuilding when a change is detected
*/
LmdBuilder.prototype.fsWatch = function (config) {
var self = this;
for (var i = 0, c = config.errors.length; i < c; i++) {
this.warn(config.errors[i], config.warn);
}
var log = function (test) {
self.log.write(test);
},
rebuild = function (stat, filename) {
if (stat && filename) {
log('info'.green + ': Change detected in ' + path.basename(filename).toString().green + ' at ' + stat.mtime.toString().blue);
} else if (stat) {
log('info'.green + ': Change detected at ' + stat.mtime.toString().blue);
} else {
log('info'.green + ': ');
}
log(' Rebuilding...\n');
var buildResult = new LmdBuilder(self.configFile, self.options);
new Writer(buildResult)
.writeAll(function (err) {
if (!buildResult.buildConfig.output) {
return;
}
if (err) {
log('info'.red + ': Build failed'.red + '\n');
} else {
log('info'.green + ': Build complete'.green + '\n');
}
});
},
watch = function (event, filename) {
if (event === 'change') {
if (filename) {
rebuild(fs.stat(filename), filename);
} else {
rebuild();
}
}
},
watchFile = function (curr, prev, filename) {
if (curr.mtime > prev.mtime) {
rebuild(curr, filename);
}
},
addWatcherFor = function (modulePath) {
try {
// a mess....
fs.watchFile(modulePath, { interval: 1000 }, function (curr, prev) {
watchFile(curr, prev, modulePath);
});
} catch (e) {
fs.watch(modulePath, watch);
}
},
collectModuleNames = function (config) {
var names = [],
module,
bundle,
modules = config.modules,
styles = config.styles,
bundles = config.bundles;
// Collect modules
for (var moduleName in modules) {
module = modules[moduleName];
if (Array.isArray(module.path)) {
names = names.concat(module.path);
} else {
// Skip shortcuts
if (module.path.charAt(0) === '@') continue;
names.push(module.path);
}
}
for (var i = 0, c = styles.length; i < c; i++) {
names.push(styles[i].path);
}
// Collect modules from bundles
for (var bundleName in bundles) {
bundle = bundles[bundleName];
if (!(bundle instanceof Error)) {
collectModuleNames(bundle).forEach(function (moduleName) {
// uniq only
if (names.indexOf(moduleName) === -1) {
names.push(moduleName);
}
});
}
}
return names;
};
var modules = collectModuleNames(config),
watchedModulesCount = modules.length;
// is there any modules?
if (watchedModulesCount) {
// add all modules
modules.forEach(function (modulePath) {
addWatcherFor(modulePath);
});
// add lmd.json too
addWatcherFor(this.configFile);
log('info'.green + ': Now watching ' + watchedModulesCount.toString().green + ' files. Ctrl+C to stop\n');
// Rebuild at startup
rebuild();
}
};
/**
* Formats log message
*
* @param text
* @return {*}
*/
LmdBuilder.prototype.formatLog = function (text) {
return text.replace(/\*\*([^\*]*)\*\*/g, function (str) {
return str.replace(/^\*\*|\*\*$/g, '').green;
});
};
/**
* text\ntext + info: -> info: text
* -> info: text
*
* @param {String} padding
* @param {String} text
* @return {String}
*/
LmdBuilder.prototype.addPaddingToText = function (padding, text) {
return text.split('\n').map(function (line) {
return padding + ': ' + line;
}).join('\n') + '\n';
};
/**
* Formats and prints an error
*
* @param {String} text simple markdown syntax
*
* @example
* Pewpew **ololo** - ololo will be green
*/
LmdBuilder.prototype.error = function (text) {
text = this.formatLog(text);
this.log.write(this.addPaddingToText('ERRO'.red.inverse, text));
};
/**
* Formats and prints an warning
*
* @param {String} text simple markdown syntax
*
* @example
* Pewpew **ololo** - ololo will be green
*/
LmdBuilder.prototype.warn = function (text, isWarn) {
if (isWarn) {
text = this.formatLog(text);
this.log.write(this.addPaddingToText('warn'.yellow, text));
}
};
/**
* Formats and prints an info
*
* @param {String} text simple markdown syntax
*
* @example
* Pewpew **ololo** - ololo will be green
*/
LmdBuilder.prototype.info = function (text) {
text = this.formatLog(text);
this.log.write(this.addPaddingToText('info'.green, text));
};
/**
* Generates module token
*
* @param {String} modulePath
*/
LmdBuilder.prototype.createToken = function (modulePath) {
// issue 178 String modules getting corrupted on Windows when using source map.
// replace \ with / in order to fix JSON.stringify escaping issue
modulePath = modulePath.replace(/\\/g, '/');
return '/**[[LMD_TOKEN]]:' + modulePath + '**/';
};
/**
* Calculates module offset relative to source file
*
* @param {String} source result source with tokens
* @param {Number} tokenIndex module offset
*
* @return {Object} {column, line}
*/
LmdBuilder.prototype.getModuleOffset = function (source, tokenIndex) {
var cols = 0,
rows = 1;
for (var i = 0, symbol; i < tokenIndex; i++) {
symbol = source[i];
if (symbol === '\n') {
rows++;
cols = 0;
} else {
cols++;
}
}
return {
column: cols,
line: rows
};
};
/**
* Generates source map, removes source map tokens
*
* @param {Object} modules in package modules
* @param {Object} modulesInfo information about package modules
* @param {String} sourceWithTokens source with sourcemap tokens
* @param {String} config module config
*
* @return {Object} {source: cleanSource, sourceMap: sourceMap}
*/
LmdBuilder.prototype.createSourceMap = function (modules, modulesInfo, sourceWithTokens, config) {
var configRoot = String(config.root || config.path || ''),
configOutput = String(config.output || ''),
configWwwRoot = String(config.www_root || ''),
// #174 apply default value only for non-strings
configSourcemapWww = typeof config.sourcemap_www === 'string' ? config.sourcemap_www : '/',
configSourcemap = String(config.sourcemap || ''),
configSourceMappingURL = String(config.sourcemap_url || '');
var generatedFile = path.resolve(this.configDir, configRoot, configOutput),
root = path.resolve(this.configDir, configRoot, configWwwRoot),
sourceMapFile = path.resolve(this.configDir, configRoot, configSourcemap),
isInline = config.sourcemap_inline || false,
isWarn = config.warn;
var self = this,
module,
moduleInfo,
originalSourceMapDetect = /sourceMappingURL=(.*)*/,
hasOriginalSourceMap,
originalSourceMapPath,
originalSourceMap,
first,
sourceMapsApplied = 0,
sourceMapSkipped = [];
root = fs.realpathSync(root);
var sourceMap = new SourceMapGenerator({
file: path.relative(root, generatedFile),
sourceRoot: configSourcemapWww
});
for (var moduleName in modules) {
module = modules[moduleName];
moduleInfo = modulesInfo[moduleName];
if (this.isModuleCanBeUnderSourceMap(module)) {
var token = self.createToken(module.path),
tokenIndex = sourceWithTokens.indexOf(token);
if (tokenIndex === -1) {
continue;
}
var offset = self.getModuleOffset(sourceWithTokens, tokenIndex),
// #174 replace back slashes with front slashes
source = path.relative(root, module.path).replace(/\\/g, '/');
hasOriginalSourceMap = false;
try {
// detect original sourceMap in module code [sourceMappingURL=...]
if (originalSourceMapDetect.test(moduleInfo.code)) {
originalSourceMapPath = path.resolve(path.dirname(path.relative(root, module.path)),
originalSourceMapDetect.exec(moduleInfo.code)[1]);
if (fs.existsSync(originalSourceMapPath)) {
hasOriginalSourceMap = true;
originalSourceMap = JSON.parse(fs.readFileSync(originalSourceMapPath));
}
}
// find sourceMap in module directory
if (!hasOriginalSourceMap && fs.existsSync(module.path + '.map')) {
hasOriginalSourceMap = true;
originalSourceMap = JSON.parse(fs.readFileSync(module.path + '.map'));
}
} catch (e) {}
if (hasOriginalSourceMap) {
originalSourceMap = new SourceMapConsumer(originalSourceMap);
first = true;
// move original source map => result source map
originalSourceMap.eachMapping(function (mapping) {
sourceMap.addMapping({
generated: {
// only first line can be with column offset
column: (first ? offset.column : 0) + mapping.generatedColumn,
line: offset.line - 1 + mapping.generatedLine
},
original: {
column: mapping.originalColumn,
line: mapping.originalLine
},
source: mapping.source
});
first = false;
});
} else {
// add mapping for each line
for (var i = 0; i < module.lines; i++) {
sourceMap.addMapping({
generated: {
// only first line can be with column offset
column: i ? 0 : offset.column,
line: offset.line + i
},
original: {
column: 0,
line: i + 1
},
source: source
});
}
}
// remove token
sourceWithTokens = sourceWithTokens.replace(token, '');
sourceMapsApplied++;
} else {
if (!module.is_shortcut) {
sourceMapSkipped.push(moduleName);
}
}
}
if (sourceMapsApplied === 0) {
this.warn('There is no modules under Source Map!', isWarn);
} else if (sourceMapSkipped.length) {
this.warn('Source Map is not applied for these modules: **' + sourceMapSkipped.join('**, **') + '**', isWarn);
}
configSourceMappingURL = configSourceMappingURL || '/' + path.relative(root, sourceMapFile) + '?' + Math.random();
if (isInline && sourceMapsApplied !== 0) {
// append helper
sourceWithTokens += '\n\n//@ sourceMappingURL=' + configSourceMappingURL + '\n';
}
return {
source: sourceWithTokens,
sourceMap: sourceMap
};
};
/**
*
* @param {String} source
*
* @return {Number}
*/
LmdBuilder.prototype.calculateModuleLines = function (source) {
var lines = 1;
for (var i = 0, c = source.length; i < c; i++) {
if (source[i] === "\n") {
lines++;
}
}
return lines;
};
/**
*
* @param {Object} moduleDescriptor
*
* @return {Boolean}
*/
LmdBuilder.prototype.isModuleCanBeUnderSourceMap = function (moduleDescriptor) {
return !moduleDescriptor.is_shortcut &&
!moduleDescriptor.is_coverage &&
!moduleDescriptor.is_lazy &&
!moduleDescriptor.is_multi_path_module;
};
LmdBuilder.prototype.applySourceMap = function (module, moduleInfo) {
if (this.isModuleCanBeUnderSourceMap(module)) {
var originalCodeWithToken = this.createToken(module.path) + moduleInfo.originalCode;
// reapply wrapper
moduleInfo.code = common.wrapModule(originalCodeWithToken, module, moduleInfo.type);
module.lines = this.calculateModuleLines(moduleInfo.code);
}
};
/**
* Format one module
*
* @param moduleInfo
*
* @return {*}
*/
LmdBuilder.prototype.formatModule = function (module, moduleInfo, modulesBundle, config, isPack) {
var mainModuleName = config.main;
switch (moduleInfo.type) {
case "amd":
case "fd":
case "fe":
case "plain":
case "3-party":
// #26 Code coverage
if (module.is_coverage) {
var skipLines = ({
fd: 1,
fe: 1,
plain: 0,
amd: -1
})[moduleInfo.type];
var coverageResult = lmdCoverage.interpret(module.name, module.path, moduleInfo.code, skipLines);
modulesBundle.options[module.name] = coverageResult.options;
modulesBundle.options[module.name].coverage = 1;
moduleInfo.code = coverageResult.code;
}
if (module.is_lazy || isPack) {
moduleInfo.code = this.compress(moduleInfo.code, config.pack_options);
}
if (module.name !== mainModuleName && module.is_lazy) {
moduleInfo.code = moduleInfo.code.replace(/^function[^\(]*/, 'function');
if (moduleInfo.code.indexOf('(function(') !== 0) {
moduleInfo.code = '(' + moduleInfo.code + ')';
}
moduleInfo.code = this.escape(moduleInfo.code);
}
break;
case "string":
moduleInfo.code = this.escape(moduleInfo.code);
break;
}
};
/**
* Format Modules
*
* @param config
*
* @returns {Object} {main: 'string', modules: ['"name": moduleContent'], options: {"name": {}}}
*/
LmdBuilder.prototype.formatModules = function (modules, modulesInfo, config, isPack) {
var self = this;
var modulesBundle = {
main: '',
modules: [],
options: {}
};
if (!config.modules) {