UNPKG

lmd

Version:

LMD: Lazy Module Declaration

1,583 lines (1,412 loc) 73.8 kB
var fs = require('fs'), path = require('path'), fileExists = fs.existsSync || path.existsSync, uglifyCompress = require("uglify-js"), parser = uglifyCompress.parser, uglify = uglifyCompress.uglify, template = require('lodash-template'), glob = require('glob'), Module = require('module').Module; require('colors'); var DEFAULT_DEPENDS_MASK = "*.lmd.json"; var SOURCE_TWEAK_FLAGS = ["warn", "log", "pack", "lazy", "optimize"]; exports.SOURCE_TWEAK_FLAGS = SOURCE_TWEAK_FLAGS; var INHERITABLE_FIELDS = SOURCE_TWEAK_FLAGS.concat(['version', 'main', 'global', 'pack_options', 'mixins', 'bundles_callback', 'banner']); var MASTER_FIELDS = INHERITABLE_FIELDS.concat(['output', 'path', 'root', "sourcemap", "sourcemap_inline", "sourcemap_www", "sourcemap_url", "www_root", "name", "description", 'styles_output']); exports.MASTER_FIELDS = MASTER_FIELDS; var EXTRA_OPTIONS_FIELDS = MASTER_FIELDS.filter(function (field) { return field !== 'mixins'; }); var FILED_ALIASES = {"path": "root"}; // Fields that will come from package into bundle. // There is some other fields like sourcemap_** or bundles_callback and output var BUNDLE_INHERIT_FROM_PACKAGE = ['root', 'warn', 'log', 'lazy', 'pack', 'pack_options', 'optimize', 'banner'], SUB_BUNDLE_SEPARATOR = ' → ', ROOT_BUNDLE_ID = ''; var MODULE_TYPE_HINTS = { 'json': 'json', 'string': 'string', 'fd': 'fd', 'fe': 'fe', 'plain': 'plain', 'commonjs': 'plain', 'cjs': 'plain', '3-party': '3-party', 'amd': 'amd' }; exports.SUB_BUNDLE_SEPARATOR = SUB_BUNDLE_SEPARATOR; exports.ROOT_BUNDLE_ID = ROOT_BUNDLE_ID; exports.GLOBALS = { Array : 1, Boolean : 1, Date : 1, decodeURI : 1, decodeURIComponent : 1, encodeURI : 1, encodeURIComponent : 1, Error : 1, 'eval' : 1, EvalError : 1, Function : 1, hasOwnProperty : 1, isFinite : 1, isNaN : 1, JSON : 1, Math : 1, Number : 1, Object : 1, parseInt : 1, parseFloat : 1, RangeError : 1, ReferenceError : 1, RegExp : 1, String : 1, SyntaxError : 1, TypeError : 1, URIError : 1, ArrayBuffer : 1, ArrayBufferView : 1, Audio : 1, Blob : 1, addEventListener : 1, applicationCache : 1, atob : 1, blur : 1, btoa : 1, clearInterval : 1, clearTimeout : 1, close : 1, closed : 1, DataView : 1, DOMParser : 1, defaultStatus : 1, document : 1, Element : 1, event : 1, FileReader : 1, Float32Array : 1, Float64Array : 1, FormData : 1, focus : 1, frames : 1, getComputedStyle : 1, HTMLElement : 1, HTMLAnchorElement : 1, HTMLBaseElement : 1, HTMLBlockquoteElement: 1, HTMLBodyElement : 1, HTMLBRElement : 1, HTMLButtonElement : 1, HTMLCanvasElement : 1, HTMLDirectoryElement : 1, HTMLDivElement : 1, HTMLDListElement : 1, HTMLFieldSetElement : 1, HTMLFontElement : 1, HTMLFormElement : 1, HTMLFrameElement : 1, HTMLFrameSetElement : 1, HTMLHeadElement : 1, HTMLHeadingElement : 1, HTMLHRElement : 1, HTMLHtmlElement : 1, HTMLIFrameElement : 1, HTMLImageElement : 1, HTMLInputElement : 1, HTMLIsIndexElement : 1, HTMLLabelElement : 1, HTMLLayerElement : 1, HTMLLegendElement : 1, HTMLLIElement : 1, HTMLLinkElement : 1, HTMLMapElement : 1, HTMLMenuElement : 1, HTMLMetaElement : 1, HTMLModElement : 1, HTMLObjectElement : 1, HTMLOListElement : 1, HTMLOptGroupElement : 1, HTMLOptionElement : 1, HTMLParagraphElement : 1, HTMLParamElement : 1, HTMLPreElement : 1, HTMLQuoteElement : 1, HTMLScriptElement : 1, HTMLSelectElement : 1, HTMLStyleElement : 1, HTMLTableCaptionElement: 1, HTMLTableCellElement : 1, HTMLTableColElement : 1, HTMLTableElement : 1, HTMLTableRowElement : 1, HTMLTableSectionElement: 1, HTMLTextAreaElement : 1, HTMLTitleElement : 1, HTMLUListElement : 1, HTMLVideoElement : 1, history : 1, Int16Array : 1, Int32Array : 1, Int8Array : 1, Image : 1, length : 1, localStorage : 1, location : 1, MessageChannel : 1, MessageEvent : 1, MessagePort : 1, moveBy : 1, moveTo : 1, MutationObserver : 1, name : 1, Node : 1, NodeFilter : 1, navigator : 1, onbeforeunload : 1, onblur : 1, onerror : 1, onfocus : 1, onload : 1, onresize : 1, onunload : 1, open : 1, openDatabase : 1, opener : 1, Option : 1, parent : 1, print : 1, removeEventListener : 1, resizeBy : 1, resizeTo : 1, screen : 1, scroll : 1, scrollBy : 1, scrollTo : 1, sessionStorage : 1, setInterval : 1, setTimeout : 1, SharedWorker : 1, status : 1, top : 1, Uint16Array : 1, Uint32Array : 1, Uint8Array : 1, WebSocket : 1, window : 1, Worker : 1, XMLHttpRequest : 1, XMLSerializer : 1, XPathEvaluator : 1, XPathException : 1, XPathExpression : 1, XPathNamespace : 1, XPathNSResolver : 1, XPathResult : 1, escape : 1, unescape: 1, alert : 1, confirm: 1, console: 1, Debug : 1, opera : 1, prompt : 1 }; exports.WORKER_GLOBALS = { importScripts: 1, postMessage : 1, self : 1 }; exports.NODE_GLOBALS = { __filename : 1, __dirname : 1, Buffer : 1, console : 1, exports : 1, GLOBAL : 1, global : 1, module : 1, process : 1, require : 1, setTimeout : 1, clearTimeout : 1, setInterval : 1, clearInterval: 1 }; exports.LMD_GLOBALS = { exports: 1, module: 1, require: 1 }; // required to check string for being template var templateParts = /<%|\$\{/, reLmdFile = /\.lmd\.(json|js)$/, globPattern = /\*|\{|\}/, maxInterpolateRecursion = 10; exports.RE_LMD_FILE = reLmdFile; /** * It uses _.template to interpolate config strings * { * "output": "index-<%= version %>.js", * "version": "1.0.1" * } * * -> * * { * "output": "index-1.0.1.js", * "version": "1.0.1" * } * * @param {Object} config * @param {Object} [data] * * @return {Object} config' */ function interpolateConfigStrings(config, data) { data = data || config; for (var key in config) { var value = config[key]; if (typeof value === "object") { config[key] = interpolateConfigStrings(value, data); } else if (typeof value === "string") { var currentInterpolation = 0; while (templateParts.test(value)) { currentInterpolation++; if (currentInterpolation > maxInterpolateRecursion) { break; } config[key] = value = template(value, data); } } } return config; } var readConfig = function (file/*, filePart, filePart*/) { file = path.join.apply(path, arguments); var fileContent = fs.readFileSync(file, 'utf8'), config; if (path.extname(file) === '.json') { // require() is Caches json files config = JSON.parse(fileContent); } else { var mod = new Module('.', null); mod.load(file); config = mod.exports; } // Some extra variables var data = { __dirname: path.dirname(file), __filename: file }; for (var key in config) { data[key] = config[key]; } return interpolateConfigStrings(config, data); }; exports.readConfig = readConfig; var LMD_JS_SRC_PATH = path.join(__dirname, '../src'); exports.LMD_JS_SRC_PATH = LMD_JS_SRC_PATH; var LMD_PLUGINS = readConfig(LMD_JS_SRC_PATH, 'lmd_plugins.json'); exports.LMD_PLUGINS = LMD_PLUGINS; /** * Merges mixins with config * * @param {Object} config * @param {String[]} mixins */ var mergeMixins = function (config, mixins) { if (Array.isArray(config.mixins) && Array.isArray(mixins)) { config.mixins.push.apply(config.mixins, mixins); return config; } return deepDestructableMerge(config, { mixins: mixins }); }; /** * Config files deep merge * * @param {Object} left * @param {Object} right */ var deepDestructableMerge = function (left, right) { for (var prop in right) { if (right.hasOwnProperty(prop)) { if (typeof left[prop] === "object") { deepDestructableMerge(left[prop], right[prop]); } else { left[prop] = right[prop]; } } } return left; }; exports.deepDestructableMerge = deepDestructableMerge; /** * Merges all config files in module's lineage * * @param {Object} config * @param {String} configDir */ var tryExtend = function (config, configDir) { config = config || {}; if (typeof config.extends !== "string") { return config; } var parentConfig = tryExtend(readConfig(configDir, config.extends), configDir); return deepDestructableMerge(parentConfig, config); }; exports.tryExtend = tryExtend; /** * Returns depends config file of this module * * @param {String|Array} modulePath * @param {String} dependsFileMask * * @return {Array} */ var getDependsConfigOf = function (modulePath, dependsFileMask) { modulePath = [].concat(modulePath); return modulePath.map(function (modulePath) { var fileName = modulePath.replace(/^.*\/|\.[a-z0-9]+$/g, ''); return path.join(path.dirname(modulePath), dependsFileMask.replace('*', fileName)); }); }; /** * Merges configs flags * * @param {Object} configA * @param {Object} configB * @param {String[]} flagsNames * @param {Boolean} isMasterConfig */ var mergeFlags = function (configA, configB, flagsNames, isMasterConfig) { // Apply Flags flagsNames.forEach(function (optionsName) { // if master -> B if (typeof configB[optionsName] === "undefined") { return; } if (isMasterConfig) { configA[optionsName] = configB[optionsName]; } else { // if A literal B array -> B if (configB[optionsName] instanceof Array && !(configA[optionsName] instanceof Array) ) { configA[optionsName] = configB[optionsName]; } else if (configB[optionsName] instanceof Array && configA[optionsName] instanceof Array) { // if A array B array -> A concat B configA[optionsName] = configA[optionsName].concat(configB[optionsName]); } else { // if A literal B literal -> union // if A array B literal -> A configA[optionsName] = configA[optionsName] || configB[optionsName]; } // else {} } }); }; /** * * @param {Object} moduleA * @param {Object} moduleB * @returns {boolean} */ var isModulesEqual = function (moduleA, moduleB) { var fields = ['name', 'path', 'extra_exports', 'extra_require', 'extra_bind', 'is_multi_path_module', 'is_lazy', 'is_multi_path_module', 'depends']; return moduleA && moduleB && fields.every(function (field) { return moduleA[field] === moduleB[field]; }); }; /** * Merges configs * * @param {Object} configA * @param {Object} configB * @param {String[]} flagsNames * @param {Array} inheritableFields * @param {Boolean} isMasterConfig some parameters from configA will be overwritten using configB * @param {String} context configB description * * @return {*} */ var mergeConfigs = function (configA, configB, flagsNames, inheritableFields, isMasterConfig, context) { if (isMasterConfig) { // Apply master fields inheritableFields.forEach(function (fieldName) { if (typeof configB[fieldName] !== "undefined") { configA[fieldName] = configB[fieldName]; if (FILED_ALIASES.hasOwnProperty(fieldName)) { configA[FILED_ALIASES[fieldName]] = configB[fieldName]; } } }); } // Save errors configA.errors = configA.errors || []; configB.errors = configB.errors || []; configA.errors = configA.errors.concat(configB.errors); // Apply Flags mergeFlags(configA, configB, flagsNames, isMasterConfig); // Apply Modules configA.modules = configA.modules || {}; configB.modules = configB.modules || {}; for (var moduleName in configB.modules) { // Warn if module exists an its not a master config if (!isMasterConfig && configA.modules[moduleName]) { if (!isModulesEqual(configA.modules[moduleName], configB.modules[moduleName])) { configA.errors.push('Name conflict! Module **"' + moduleName + '"** will be overwritten by ' + context); } } configA.modules[moduleName] = configB.modules[moduleName]; } // Apply styles configA.styles = configA.styles || []; configB.styles = configB.styles || []; configA.styles = configA.styles.concat(configB.styles); // Apply Bundles configA.bundles = configA.bundles || {}; configB.bundles = configB.bundles || {}; for (var bundleName in configB.bundles) { if (configB.bundles[bundleName]) { if (!configA.bundles[bundleName]) { configA.bundles[bundleName] = {}; } // Bundle is not exists if (configB.bundles[bundleName] instanceof Error) { configA.bundles[bundleName] = configB.bundles[bundleName]; } else { mergeConfigs(configA.bundles[bundleName], configB.bundles[bundleName], flagsNames, MASTER_FIELDS, true, context); } } } // Apply User Plugins configA.plugins = configA.plugins || {}; configB.plugins = configB.plugins || {}; for (var pluginName in configB.plugins) { // Warn if module exists an its not a master if (!isMasterConfig && configA.plugins[pluginName]) { configA.errors.push('Name conflict! User plugin **"' + pluginName + '"** will be overwritten by ' + context); } configA.plugins[pluginName] = configB.plugins[pluginName]; } return configA; // not rly need... }; /** * Creates LMD config: applies depends and extends * * @param {Object} rawConfig * @param {String} configFile * @param {String} configDir * @param {String[]} [flagsNames] * @param {Object} [extraOptions] * @param {Object} [usedConfigs] * * @return {Object} */ var assembleLmdConfigAsObject = function (rawConfig, configFile, configDir, flagsNames, extraOptions, usedConfigs) { flagsNames = flagsNames || Object.keys(LMD_PLUGINS); var isFirstRun = typeof usedConfigs === "undefined"; usedConfigs = usedConfigs || {}; usedConfigs[configFile] = true; // mark config as used var configs = [], resultConfig = { modules: {}, errors: [], plugins_depends: {} }; if (extraOptions && extraOptions.mixins) { rawConfig = mergeMixins(rawConfig, extraOptions.mixins); } if (extraOptions && extraOptions.styles) { rawConfig = deepDestructableMerge(rawConfig, { styles: extraOptions.styles }); } // collect modules and module options var modules = collectModules(rawConfig, configDir); if (rawConfig.depends) { var /*dependsMask = rawConfig.depends === true ? DEFAULT_DEPENDS_MASK : rawConfig.depends,*/ dependsConfigPath, dependsMask; for (var moduleName in modules) { if (!modules[moduleName].is_shortcut && !modules[moduleName].is_ignored) { dependsMask = modules[moduleName].depends; dependsConfigPath = getDependsConfigOf(modules[moduleName].path, dependsMask); dependsConfigPath.forEach(function (dependsConfigPath) { if (fileExists(dependsConfigPath)) { if (!usedConfigs[dependsConfigPath]) { configs.unshift({ context: 'depends config **' + dependsConfigPath + '**', config: assembleLmdConfig(dependsConfigPath, flagsNames, null, usedConfigs) }); } } }); } } } // extend parent config if (typeof rawConfig['extends'] === "string") { var parentConfigFile = fs.realpathSync(configDir + '/' + rawConfig['extends']); if (!usedConfigs[parentConfigFile]) { var parentConfig = assembleLmdConfig(parentConfigFile, flagsNames, null, usedConfigs); } } var processedConfig = { modules: modules, styles: collectStyles(rawConfig, configDir), bundles: collectBundles(rawConfig, configDir), plugins: collectUserPlugins(rawConfig, configDir) }; // keep fields MASTER_FIELDS.forEach(function (fieldName) { processedConfig[fieldName] = rawConfig[fieldName]; if (FILED_ALIASES.hasOwnProperty(fieldName)) { processedConfig[FILED_ALIASES[fieldName]] = rawConfig[fieldName]; } }); // keep flags flagsNames.forEach(function (fieldName) { processedConfig[fieldName] = rawConfig[fieldName]; }); if (parentConfig) { mergeConfigs(resultConfig, parentConfig, flagsNames, INHERITABLE_FIELDS, true, 'parent config **' + parentConfigFile + '**'); } for (var i = 0, c = configs.length, dependsMainModuleName; i < c; i++) { // Cleanup main module from depends dependsMainModuleName = configs[i].config.main || "main"; if (configs[i].config.modules) { delete configs[i].config.modules[dependsMainModuleName]; } mergeConfigs(resultConfig, configs[i].config, flagsNames, [], false, configs[i].context); } mergeConfigs(resultConfig, processedConfig, flagsNames, MASTER_FIELDS, true, 'main config **' + configFile + '**'); if (isFirstRun) { // Apply mixins var mixins = resultConfig.mixins; if (Array.isArray(mixins)) { mixins.forEach(function (mixinName) { var mixinConfigFile = fs.realpathSync(configDir + '/' + mixinName), processedMixin = assembleLmdConfig(mixinConfigFile, flagsNames, null, usedConfigs); mergeConfigs(resultConfig, processedMixin, flagsNames, INHERITABLE_FIELDS, true, 'mixin config **' + mixinConfigFile + '**'); }); } if (extraOptions) { extraOptions.modules = collectModules(extraOptions, configDir); extraOptions.bundles = collectBundles(extraOptions, configDir); extraOptions.styles = collectStyles(extraOptions, configDir); mergeConfigs(resultConfig, extraOptions, flagsNames, EXTRA_OPTIONS_FIELDS, true, 'CLI options'); } reapplyModuleOptions(resultConfig); addPluginsFromBundles(resultConfig); addPluginsDepends(resultConfig); flattenBundles(resultConfig); resolveStyles(resultConfig); bundlesInheritFieldsFromPackage(resultConfig); removeIgnoredModules(resultConfig); } return resultConfig; }; /** * Creates LMD config: applies depends and extends * * @param {Object} configFile * @param {String[]} [flagsNames] * @param {Object} [extraOptions] * @param {Object} [usedConfigs] * * @return {Object} */ var assembleLmdConfig = function (configFile, flagsNames, extraOptions, usedConfigs) { var configDir = path.dirname(configFile), rawConfig = readConfig(configFile); configFile = fs.realpathSync(configFile); return assembleLmdConfigAsObject(rawConfig, configFile, configDir, flagsNames, extraOptions, usedConfigs); }; exports.assembleLmdConfig = assembleLmdConfig; /** * Resolve and add all plugins depends * * @param resultConfig */ function addPluginsDepends(resultConfig) { for (var pluginName in LMD_PLUGINS) { if (typeof resultConfig[pluginName] !== "undefined") { addOnePluginDepends(pluginName, resultConfig); } } } /** * For now add plugins from build to bundle * * @param resultConfig */ function addPluginsFromBundles(resultConfig) { if (resultConfig.bundles) { var bundles = Object.keys(resultConfig.bundles), lmdPlugins = Object.keys(LMD_PLUGINS); // Apply flags from bundles bundles.forEach(function (bundleName) { mergeFlags(resultConfig, resultConfig.bundles[bundleName], lmdPlugins, false); }); // Set bundle plugin if (bundles.length) { resultConfig.bundle = true; } } } /** * * @param config * @param {String} [namespace] */ function flattenBundles(config, namespace) { namespace = namespace || ''; var bundles = {}; if (!config.bundles) { return; } Object.keys(config.bundles).forEach(function (bundleName) { var bundle = config.bundles[bundleName]; bundles[namespace + bundleName] = bundle; // Flatten sub-bundles if (bundle.bundles) { flattenBundles(bundle, bundleName + SUB_BUNDLE_SEPARATOR); var subBundles = bundle.bundles; for (var subBundleName in subBundles) { bundles[subBundleName] = subBundles[subBundleName]; } delete bundle.bundles; } }); config.bundles = bundles; } function resolveStyles(config) { function unique(array, item) { if (array.indexOf(item) < 0) { array.push(item); } return array; } config.styles = config.styles .reduce(unique, []) // resolve full paths .reduce(function (styles, cssPath) { var someStyles = [cssPath]; if (globPattern.test(cssPath)) { someStyles = glob.sync(cssPath, { nosort: true }) || someStyles; } return styles.concat(someStyles); }, []) .reduce(unique, []) .map(function (path) { return { path: path, is_exists: fs.existsSync(path) }; }); } function collectUserPlugins(resultConfig, configDir) { var plugins = resultConfig.plugins, collectedPlugins = {}; if (typeof plugins !== "object") { return collectedPlugins; } var rootDir = path.resolve(configDir, resultConfig.path || resultConfig.root), invalidPluginNames = [], plugin, pluginName, pluginOriginalPath, pluginPath, pluginExists, pluginConflict, pluginValid, pluginOptions, pluginCode; // format plugin descriptor struct for (pluginName in plugins) { plugin = plugins[pluginName]; pluginOptions = (typeof plugin === "string" ? null : plugin.options) || null; pluginOriginalPath = (typeof plugin === "string" ? plugin : plugin.path) || ''; pluginPath = path.join(rootDir, pluginOriginalPath); pluginExists = true; try { pluginPath = fs.realpathSync(pluginPath); } catch (e) { pluginExists = false; } pluginConflict = pluginName in LMD_PLUGINS; pluginValid = false; pluginCode = null; if (!pluginConflict && pluginExists) { pluginCode = fs.readFileSync(pluginPath, 'utf8'); pluginValid = validateUserPlugin(pluginCode); } collectedPlugins[pluginName] = { name: pluginName, isConflict: pluginConflict, isExists: pluginExists, isValid: pluginValid, isOk: !pluginConflict && pluginExists && pluginValid, originalPath: pluginOriginalPath, path: pluginPath, code: pluginCode, options: pluginOptions }; } return collectedPlugins; } function validateUserPlugin(code) { var ast; try { ast = parser.parse(code); } catch (e) { return e.toString(); } // should match this pattern // ["toplevel", [["stat", ["call", ["function", null, ["sb"],[*]],[["name", "sandbox"]]]]]]; return ( ast && ast.length === 2 && ast[1] && ast[1].length === 1 && ast[1][0][0] === "stat" && ast[1][0][1] && ast[1][0][1][0] === "call" && ast[1][0][1][1] && ast[1][0][1][1][0] === "function" ); } function addOnePluginDepends(pluginName, resultConfig) { var plugin = LMD_PLUGINS[pluginName]; if (plugin.depends) { plugin.depends.forEach(function (flagName) { // check that depends is plugin and that plugin is not included if (flagName in LMD_PLUGINS && typeof resultConfig[flagName] === "undefined") { // add flag as true resultConfig[flagName] = true; // add flag to plugins_depends list if (!resultConfig.plugins_depends[flagName]) { resultConfig.plugins_depends[flagName] = []; } resultConfig.plugins_depends[flagName].push(pluginName); // recursively check plugin depends addOnePluginDepends(flagName, resultConfig); } }); } } function isCoverage(config, moduleName) { if (!config.stats_coverage) { return false; } if (config.stats_coverage === true) { return true; } if (config.stats_coverage instanceof Array) { return config.stats_coverage.indexOf(moduleName) !== -1; } return false; } function subdirToString() { return this.length > 0 ? this.reverse().join('/') + '/' : ''; } function createSubdirTemplateVariable(modulesDirPath, moduleRealPath) { var subdir = path.dirname(path.relative(modulesDirPath, moduleRealPath)).split(path.sep).reverse(); if (subdir[0] === '.') { subdir = []; } subdir.toString = subdirToString; return subdir; } /** * @name LmdModuleStruct * @class * * @field {String} name module name * @field {String} path full path to module * @field {String} depends depends file mask * @field {Boolean} is_lazy is lazy module? * @field {Boolean} is_sandbox is module sandboxed? * @field {Boolean} is_greedy module is collected using wildcard * @field {Boolean} is_shortcut is module shortcut? * @field {Boolean} is_coverage is module under code coverage? * @field {Boolean} is_third_party uses custom export/require? * */ /** * Collecting module using merged config * * @param config * * @returns {Object} */ var collectModules = function (config, configDir) { var modules = {}, globalLazy = config.lazy || false, globalDepends = (config.depends === true ? DEFAULT_DEPENDS_MASK : config.depends) || false, moduleLazy = false, moduleTypeHint, moduleName, modulePath, moduleRealPath, moduleExists, moduleExports, moduleRequire, moduleBind, moduleFileName, moduleFilePath, moduleDesciptor, wildcardRegex, isMultiPathModule, moduleData, isThirdPartyModule, modulesDirPath = config.root || config.path || ''; modulesDirPath = path.resolve(configDir, modulesDirPath); // grep paths for (moduleName in config.modules) { moduleDesciptor = config.modules[moduleName]; // case "moduleName": null // case "moduleName": "path/to/module.js" if (moduleDesciptor === null || typeof moduleDesciptor === "string" || Array.isArray(moduleDesciptor)) { moduleTypeHint = false; moduleExports = false; moduleRequire = false; moduleBind = false; modulePath = moduleDesciptor; moduleLazy = globalLazy; } else { // case "moduleName": {"path": "path/to/module.js", "lazy": false} moduleTypeHint = moduleDesciptor.type || false; moduleExports = moduleDesciptor.exports || false; moduleRequire = moduleDesciptor.require || false; moduleBind = moduleDesciptor.bind || moduleDesciptor['this'] || false; modulePath = moduleDesciptor.path; moduleLazy = moduleDesciptor.lazy || false; moduleTypeHint = moduleDesciptor.type || false; } isMultiPathModule = false; if (Array.isArray(modulePath)) { // Try to glob // case when ['jquery*.js'] modulePath = modulePath.reduce(function (paths, modulePath) { if (globPattern.test(modulePath)) { modulePath = glob.sync(modulePath, { cwd: modulesDirPath, nosort: true }) || []; } return paths.concat(modulePath); }, []); // case when ['jquery.js'] if (modulePath.length === 1) { modulePath = modulePath[0]; } else { isMultiPathModule = true; } } isThirdPartyModule = !!moduleExports || !!moduleRequire || !!moduleBind; // Override if cache flag = true if (config.cache) { moduleLazy = true; } // its a glob pattern // @see https://github.com/isaacs/node-glob if (!isMultiPathModule && globPattern.test(modulePath)) { var globModules = glob.sync(modulePath, { cwd: modulesDirPath, nosort: true }); // * -> <%= file => for backward capability var moduleNameTemplate = template(moduleName.replace('*', '<%= file %>')); globModules.forEach(function (module) { var moduleRealPath = path.join(modulesDirPath, module), basename = path.basename(moduleRealPath), fileParts = basename.split('.'), ext = fileParts.pop(), file = fileParts.join('.'), dir = path.dirname(moduleRealPath).split(path.sep).reverse(), subdir = createSubdirTemplateVariable(modulesDirPath, moduleRealPath); // modify module name using template var newModuleName = moduleNameTemplate({ basename: basename, file: file, ext: ext, dir: dir, subdir: subdir }); moduleExists = true; try { moduleRealPath = fs.realpathSync(moduleRealPath); } catch (e) { moduleExists = false; } moduleData = { originalModuleDesciptor: moduleDesciptor, name: newModuleName, path: moduleRealPath, originalPath: modulePath, lines: 0, extra_exports: moduleExports, extra_require: moduleRequire, extra_bind: moduleBind, type_hint: moduleTypeHint, is_exists: moduleExists, is_third_party: isThirdPartyModule, is_lazy: moduleLazy, is_greedy: true, is_shortcut: false, is_coverage: isCoverage(config, newModuleName), is_ignored: false, is_sandbox: moduleDesciptor.sandbox || false, is_multi_path_module: isMultiPathModule, depends: typeof moduleDesciptor.depends === "undefined" ? globalDepends : moduleDesciptor.depends }; // wildcard have a low priority // if module was directly named it pass if (!(modules[newModuleName] && !modules[newModuleName].is_greedy)) { modules[newModuleName] = moduleData; } }); } else if (!isMultiPathModule && /^@/.test(modulePath)) { // shortcut modules[moduleName] = { originalModuleDesciptor: moduleDesciptor, name: moduleName, originalPath: modulePath, lines: 0, path: modulePath, extra_exports: moduleExports, extra_require: moduleRequire, extra_bind: moduleBind, type_hint: moduleTypeHint, is_exists: true, is_third_party: isThirdPartyModule, is_lazy: moduleLazy, is_greedy: false, is_shortcut: true, is_coverage: false, is_ignored: false, is_sandbox: moduleDesciptor.sandbox || false, is_multi_path_module: isMultiPathModule, depends: typeof moduleDesciptor.depends === "undefined" ? globalDepends : moduleDesciptor.depends }; } else if (modulePath === null) { modules[moduleName] = { originalModuleDesciptor: moduleDesciptor, name: moduleName, originalPath: modulePath, lines: 0, path: modulePath, extra_exports: moduleExports, extra_require: moduleRequire, extra_bind: moduleBind, type_hint: moduleTypeHint, is_exists: true, is_third_party: false, is_lazy: false, is_greedy: false, is_shortcut: false, is_coverage: false, is_ignored: true, is_sandbox: false, is_multi_path_module: false, depends: globalDepends }; } else { modulePath = [].concat(modulePath); moduleExists = true; moduleRealPath = modulePath .map(function (modulePath) { return path.join(modulesDirPath, modulePath); }) .map(function (moduleRealPath) { try { return fs.realpathSync(moduleRealPath); } catch (e) { moduleExists = false; } return moduleRealPath; }); // normal name // "name": "name.js" modules[moduleName] = { originalModuleDesciptor: moduleDesciptor, name: moduleName, path: isMultiPathModule ? moduleRealPath : moduleRealPath[0], originalPath: isMultiPathModule ? modulePath : modulePath[0], lines: 0, extra_exports: moduleExports, extra_require: moduleRequire, extra_bind: moduleBind, type_hint: moduleTypeHint, is_exists: moduleExists, is_third_party: isThirdPartyModule, is_lazy: moduleLazy, is_greedy: false, is_shortcut: false, // Cant use code coverage with multi path module is_coverage: !isMultiPathModule && isCoverage(config, moduleName), is_ignored: false, is_sandbox: moduleDesciptor.sandbox || false, is_multi_path_module: isMultiPathModule, depends: typeof moduleDesciptor.depends === "undefined" ? globalDepends : moduleDesciptor.depends }; } } return modules; }; exports.collectModules = collectModules; var bundlesInheritFieldsFromPackage = function (resultConfig) { var bundles = resultConfig.bundles || {}; Object.keys(bundles).forEach(function (bundleName) { bundleInheritFieldsFromPackage(bundleName, bundles[bundleName], resultConfig); }); }; var bundleInheritFieldsFromPackage = function (bundleName, bundle, config) { BUNDLE_INHERIT_FROM_PACKAGE.forEach(function (name) { if (typeof bundle[name] === "undefined" && typeof config[name] !== "undefined") { bundle[name] = config[name]; } }); // Set output path if (typeof bundle.output === 'undefined' && config.output) { // using parent output, set output path for bundle // ../index.js -> ../index.%bundleName%.js bundle.output = config.output.replace(/\.js$/, '') + '.' + bundleName + '.js'; } if (typeof bundle.styles_output === 'undefined' && config.styles_output) { // using parent output, set output path for bundle // ../index.css -> ../index.%bundleName%.css bundle.styles_output = config.styles_output.replace(/\.css$/, '') + '.' + bundleName + '.css'; } if (typeof bundle.styles_output === 'undefined' && config.output) { // using parent output, set output path for bundle // ../index.js -> ../index.%bundleName%.css bundle.styles_output = config.output.replace(/\.js$/, '') + '.' + bundleName + '.css'; } // Set sourcemap if (bundle.output && config.sourcemap) { bundle.sourcemap = bundle.output.replace(/\.js/, '') + '.map'; bundle.sourcemap_inline = bundle.sourcemap_inline || config.sourcemap_inline; bundle.sourcemap_www = bundle.sourcemap_www || config.sourcemap_www; bundle.sourcemap_url = bundle.sourcemap_url || config.sourcemap_url; } // Continue inherit bundlesInheritFieldsFromPackage(bundle); }; var removeIgnoredModules = function (resultConfig) { var modules = resultConfig.modules; for (var moduleName in modules) { if (modules[moduleName].is_ignored) { delete modules[moduleName]; } } }; var collectStyles = function (config, configDir) { return (config.styles || []).map(function (style) { return path.join(path.resolve(configDir, config.root || ''), style); }); }; var collectBundles = function (config, configDir) { var bundles = {}, bundleName, bundlePath, bundle, pluginNames = Object.keys(LMD_PLUGINS), options = {}; for (bundleName in config.bundles) { bundle = config.bundles[bundleName]; // Path to lmd.js file if (typeof bundle === "string") { bundlePath = path.join(configDir, bundle); if (fileExists(bundlePath)) { bundle = assembleLmdConfig(bundlePath, pluginNames, options); bundleInheritFieldsFromPackage(bundleName, bundle, config); } else { // not found bundle = new Error(bundlePath); } } else { bundleInheritFieldsFromPackage(bundleName, bundle, config); bundle = assembleLmdConfigAsObject(bundle, 'Bundle: ' + bundleName, configDir, pluginNames, options); } // Keep sub-bundles bundles[bundleName] = bundle; } return bundles; }; var reapplyModuleOptions = function (config) { var globalLazy = config.lazy || false, moduleName, moduleDesciptor, moduleLazy; for (moduleName in config.modules) { moduleDesciptor = config.modules[moduleName].originalModuleDesciptor; if (typeof moduleDesciptor === "string" || moduleDesciptor === null) { moduleLazy = globalLazy; } else { moduleLazy = moduleDesciptor.lazy || false; } // Override if cache flag = true if (config.cache) { moduleLazy = true; } delete config.modules[moduleName].originalModuleDesciptor; config.modules[moduleName].is_lazy = moduleLazy; config.modules[moduleName].is_coverage = isCoverage(config, moduleName); } }; /** * * @param {Object} config * @returns {Object} */ var collectModulesInfo = function (config) { var result = {}; var bundles = groupModulesByBundles(config); iterateModulesInfo(bundles, function (moduleOptions, moduleName, bundleName) { if (!result[bundleName]) { result[bundleName] = {}; } result[bundleName][moduleName] = analiseModuleContent(moduleOptions); }); return result; }; exports.collectModulesInfo = collectModulesInfo; /** * Wrapper for plain files * * @param {String} code * * @returns {String} wrapped code */ var wrapPlainModule = function (code) { return '(function (require, exports, module) { /* wrapped by builder */\n' + code + '\n})'; }; /** * Wrapper for AMD files * * @param {String} code * * @returns {String} wrapped code */ var wrapAmdModule = function (code) { return '(function (require) { /* wrapped by builder */\nvar define = require.define;\n' + code + '\n})'; }; /** * Wrapper for non-lmd modules files * * @param {String} code * @param {Object} options * @param {Object|String} options.extra_exports * @param {Object|String|String} options.extra_require * @param {Object|String|String} options.extra_bind * * @returns {String} wrapped code */ var wrap3partyModule = function (code, options) { var exports = [], requires = [], bind = [], extra_exports = options.extra_exports, extra_require = options.extra_require, extra_bind = options.extra_bind, exportCode, bindModuleName; // add exports to the module end // extra_exports = {name: code, name: code} if (typeof extra_exports === "object") { for (var exportName in extra_exports) { exportCode = extra_exports[exportName]; exports.push(' ' + JSON.stringify(exportName) + ': ' + exportCode); } code += '\n\n/* added by builder */\nreturn {\n' + exports.join(',\n') + '\n};'; } else if (extra_exports) { // extra_exports = string code += '\n\n/* added by builder */\nreturn ' + extra_exports + ';'; } // change context of module (this) // and proxy return value // return function(){}.call({name: require('name')}); if (typeof extra_bind === "object") { for (var bindName in extra_bind) { bindModuleName = extra_bind[bindName]; bind.push(' ' + JSON.stringify(bindName) + ': require(' + JSON.stringify(bindModuleName) + ')'); } code = '\nreturn function(){\n\n' + code + '\n}.call({\n' + bind.join(',\n') + '\n});'; } else if (extra_bind) { // return function(){}.call(require('name')); code = '\nreturn function(){\n\n' + code + '\n}.call(require(' + JSON.stringify(extra_bind) + '));'; } // add require to the module start if (typeof extra_require === "object") { // extra_require = [name, name, name] if (extra_require instanceof Array) { for (var i = 0, c = extra_require.length, moduleName; i < c; i++) { moduleName = extra_require[i]; requires.push('require(' + JSON.stringify(moduleName) + ');'); } code = '/* added by builder */\n' + requires.join('\n') + '\n\n' + code; } else { // extra_require = {name: name, name: name} for (var localName in extra_require) { moduleName = extra_require[localName]; requires.push(localName + ' = require(' + JSON.stringify(moduleName) + ')'); } code = '/* added by builder */\nvar ' + requires.join(',\n ') + ';\n\n' + code; } } else if (extra_require) { // extra_require = string code = '/* added by builder */\nrequire(' + JSON.stringify(extra_require) + ');\n\n' + code; } return '(function (require) { /* wrapped by builder */\n' + code + '\n})'; }; /** * Removes tail semicolons * * @param {String} code * * @return {String} */ var removeTailSemicolons = function (code) { return code.replace(/\n*;\n*$/, ''); }; /** * Aggregates all module wrappers * * @param code * @param moduleOptions * @param moduleType * * @return {String} */ var wrapModule = function (code, moduleOptions, moduleType) { switch (moduleType) { case "3-party": // create lmd module from non-lmd module code = wrap3partyModule(code, moduleOptions); break; case "plain": // wrap plain module code = wrapPlainModule(code); break; case "amd": // AMD RequireJS code = wrapAmdModule(code); break; case "fd": case "fe": // wipe tail ; code = removeTailSemicolons(code); } return code; }; exports.wrapModule = wrapModule; var astLookupExtraRequireName = function (ast, itemIndex) { if (ast[itemIndex][0] === "array") { var index = -1; ast[itemIndex][1].forEach(function (item, i) { if (item[1] === "require") { index = i; } }); // get require index if (index !== -1) { if (ast[itemIndex + 1][0] === "function") { return ast[itemIndex + 1][2][index]; } } } else if (ast[itemIndex][0] === "function") { return ast[itemIndex][2][0]; } else if (ast[itemIndex][0] === "string") { if (ast[itemIndex + 1]) { return astLookupExtraRequireName(ast, itemIndex + 1); } } }; var findLastDefine = function (ast) { var define = uglify.ast_walker(), lastDefineAst; define.with_walkers({ "call": function () { if (this[1][0] === "name" && this[1][1] === "define") { lastDefineAst = this; } } }, function () { return define.walk(ast); }); return lastDefineAst; }; var findRequireAccesses = function (ast, moduleType) { var requireName, extraRequireName, requireAccesses = []; switch (moduleType) { case "plain": case "3-party": requireName = "require"; break; case "amd": requireName = "require"; var lastDefineAst = findLastDefine(ast); if (lastDefineAst) { extraRequireName = astLookupExtraRequireName(lastDefineAst[2], 0); } break; case "fd": case "fe": if (ast[1] && ast[1][0] && ast[1][0][0] === "stat") { requireName = ast[1] && ast[1][0] && ast[1][0][1] && ast[1][0][1][2] && ast[1][0][1][2][0]; } else { requireName = ast[1] && ast[1][0] && ast[1][0][2] && ast[1][0][2][0]; } break; default: return requireAccesses; } var walker = uglify.ast_walker(); walker.with_walkers({ "name": function () { if (this[1] === requireName || this[1] === extraRequireName) { var stack = walker.stack(), last = stack.length - 1; while (last >= 0) { /* ["var", [ ["x", ["dot", ["name", "require"], "js"]], ["y"] ]], ["dot", ["name", "require"], "js"], ["name", "require"] */ /* ["assign", true, ["name", "y"], ["dot", ["name", "require"], "css"] ], ["dot", ["name", "require"], "css"], ["name", "require"] */ if (stack[last][0] === "assign" || stack[last][0] === "var") { requireAccesses.push(stack[last + 1]); break; } /* ["call", ["dot", ["name", "require"], "define"], [ ["function", null, [], [] ] ] ], ["dot", ["name", "require"], "define"], ["name", "require"] */ if (stack[last][0] === "call") { requireAccesses.push(stack[last]); // add parent also for require.js().then() if (stack[last - 1]) { requireAccesses.push(stack[last - 1]); } b