UNPKG

steal-conditional

Version:
410 lines (356 loc) 12.6 kB
define(["module", "exports", "@loader"], function(module, exports, System) { exports.extensionBuilder = "steal-conditional/slim"; function addConditionals(loader) { var conditionalRegEx = /#\{[^\}]+\}|#\?.+$/; var isNode = typeof process === "object" && {}.toString.call(process) === "[object process]"; if (loader._extensions) { loader._extensions.push(addConditionals); } loader.set("@@conditional-helpers", loader.newModule({ isConditionalModuleName: function(moduleName){ return conditionalRegEx.test(moduleName); } })); var normalize = loader.normalize; function readMemberExpression(p, value) { var pParts = p.split("."); while (pParts.length) { value = value[pParts.shift()]; } return value; } function includeInBuild(loader, name) { var load = loader.getModuleLoad(name); load.metadata.includeInBuild = true; } // get some node modules through @node-require which is a noop in the browser function getGlob() { if (isNode) { return loader.import("@node-require", { name: module.id }) .then(function(nodeRequire) { return nodeRequire("glob"); }); } return Promise.resolve(); } function pushIfUnique(array, item) { return array.indexOf(item) === -1 ? array.push(item) : array.length; } /** * Returns the bundle module name for a string substitution * @param {string} nameWithConditional The module identifier including the condition * @param {string} variation A match of the glob pattern * @return {string} The bundle module name */ function getModuleName(nameWithConditional, variation) { var modName; var conditionIndex = nameWithConditional.search(conditionalRegEx); // look for any "/" after the condition var lastSlashIndex = nameWithConditional.indexOf("/", nameWithConditional.indexOf("}")); // substitution of a folder name if (lastSlashIndex !== -1) { modName = nameWithConditional.substr(0, conditionIndex) + variation; } else { modName = nameWithConditional.replace(conditionalRegEx, variation); } return modName; } loader.normalize = function(name, parentName, parentAddress, pluginNormalize) { var loader = this; var conditionalMatch = name.match(conditionalRegEx); if (conditionalMatch) { var substitution = conditionalMatch[0][1] !== "?"; var conditionModule = substitution ? conditionalMatch[0].substr(2, conditionalMatch[0].length - 3) : conditionalMatch[0].substr(2); var conditionExport = "default"; var conditionExportParts = conditionModule .match(/^(?:\.\/|\.\.\/)+/); // split './' or '../' in relative names var conditionExportIndex = conditionModule.indexOf(".", conditionExportParts && conditionExportParts[0].length); // only look for export if (conditionExportIndex !== -1) { conditionExport = conditionModule.substr(conditionExportIndex + 1); conditionModule = conditionModule.substr(0, conditionExportIndex); } var booleanNegation = !substitution && conditionModule[0] === "~"; if (booleanNegation) { conditionModule = conditionModule.substr(1); } var handleConditionalBuild = function() {}; /** * Adds bundles needed to load conditional modules in production * * Conditional modules are loaded dynamically, these modules are * not part of the dependency graph and steal-tools has to create * individual bundles for each of them. * * ** The boolean syntax ** * * After removing the condition from the module identifier, the * result is the identifier of the module that could be loaded * during runtime, e.g: * * With an indentifier like: `es5-shim#?conditions.needs-es5shim`, * removing everything after the condition gives us back `es5-shim` * which is then configured to be made its own bundle by steal-tools. * * ** The substitution syntax ** * * This one is more involved, since there is no way statically to * figure out all of the possible string substitution value. The * following algorithm is used to detect them: * * 1) The condition is replaced by a placeholder in the module * identifier; e.g: `jquery/#{browser.grade}` is turned into * `jquery/__PLACEHOLDER__`. * * 2) The result of step 1 is normalised * * 3) The placeholder is replaced by `*` which results in a module * name that looks like: `app@0.0.1#jquery/*` * * 4) The module name from step 3 is run through `locate` which * returns the address where the variations are located. * * 5) The address is then turned into a glob pattern and used to * match the files that could be loaded * * 6) The process is then reverted; the modules addresses are turned * into module names which will be set to `loader.bundle` so * bundles are created for each of them. */ //!steal-remove-start handleConditionalBuild = function() { var nameWithConditional = name; var PLACEHOLDER = "__PLACEHOLDER__"; // remove the conditional and the trailing slash var nameSansConditional = nameWithConditional .replace(conditionalRegEx, PLACEHOLDER) .replace(/\/+$/, ""); var normalizeConditionModule = loader.normalize( conditionModule, parentName, parentAddress, pluginNormalize ); var normalizeNameSansConditional = loader.normalize( nameSansConditional, parentName, parentAddress, pluginNormalize ); var setLoaderConfig = Promise .all([ normalizeNameSansConditional, normalizeConditionModule ]) // normalize the identifiers that make the condition // on their own then re-build the condition .then(function(names) { var res; var id = names[0]; var cond = names[1]; if (conditionExportIndex !== -1) { cond = cond + "." + conditionExport; } if (substitution) { res = id.replace(PLACEHOLDER, "#{" + cond + "}"); } else { var prefix = booleanNegation ? "#?~" : "#?"; res = id.replace(PLACEHOLDER, prefix + cond); } return [res, names[1]]; }) .then(function(names) { if (loader.normalizeMap) { loader.normalizeMap[nameWithConditional] = names[0]; } if (loader.slimConfig) { pushIfUnique(loader.slimConfig.extensions, module.id); pushIfUnique(loader.slimConfig.identifiersToResolve, names[0]); pushIfUnique(loader.slimConfig.toMap, names[1]); } }); // make sure loader.bundle is an array loader.bundle = typeof loader.bundle === "undefined" ? [] : loader.bundle; if (substitution) { var glob = null; setLoaderConfig = setLoaderConfig .then(function() { return getGlob(); }) .then(function(nodeGlob) { // in the browser we don't load the node modules if (!nodeGlob) { throw new Error("glob module not loaded"); } // make glob available down the pipeline glob = nodeGlob; // call the full normalize in case the condition // module is using the tilde lookup scheme or the // package name return normalizeNameSansConditional; }) .then(function(normalized) { return loader.locate({ metadata: {}, name: normalized.replace(PLACEHOLDER, "*") }); }) .then(function(address) { var path = address.replace("file:", ""); var cwd = path.substr(0, path.indexOf("*")); var pattern = path.substring(path.indexOf("*")); return new Promise(function(resolve, reject) { var options = { cwd: cwd, dot: true, nobrace: true, noglobstar: true, noext: true, nodir: true }; glob(pattern, options, function(err, files) { if (err) { reject(err); } resolve(files); }); }); }) .then(function(variations) { var promises = []; /* * With an conditional import like this: * * import 'locate/#{lang}'; * * and an app tree that looks like the one below: * * locale * ├── ar.js * ├── en.js * ├── es.js * ├── hi.js * └── zh.js * * `variations` will be an array of the files * relative to the `locale` folder, e.g: * * `['ar.js', 'en.js', 'es.js', 'hi.js', 'zh.js']` * * we iterate over the array, remove the extension, * then take the original module name with the * substitution syntax and replace it with the * variation name, after this we get the following * modules names: * * `['locale/ar', 'local/en', ..., 'locale/zh']` * * We run each of those module names through the * `normalize` hook to handle any relative paths, * and finally we add them to `loader.bundle` (if * not added already) to make sure we get bundles * for each variation when the app is built. */ for (var i = 0; i < variations.length; i += 1) { var variation = variations[i]; // remove the extension variation = variation.substr(0, variation.lastIndexOf(".")); var modName = getModuleName(nameWithConditional, variation); var promise = loader.normalize.call(loader, modName, parentName, parentAddress, pluginNormalize); promises.push(promise.then(function(normalized) { var isBundle = loader.bundle.indexOf(normalized) !== -1; if (!isBundle) { loader.bundle.push(normalized); } })); } return Promise.all(promises); }); } // boolean conditional syntax else { var bundleName = name.replace(conditionalRegEx, ""); var isBundle = loader.bundle.indexOf(bundleName) !== -1; if(!isBundle) { loader.bundle.push(bundleName); } } name = "@empty"; return setLoaderConfig.then(function() { return normalize.call( loader, name, parentName, parentAddress, pluginNormalize ); }); }; //!steal-remove-end var handleConditionalEval = function(m) { var conditionValue = (typeof m === "object") ? readMemberExpression(conditionExport, m) : m; if (substitution) { if (typeof conditionValue !== "string") { throw new TypeError( "The condition value for " + conditionalMatch[0] + " doesn't resolve to a string." ); } name = name.replace(conditionalRegEx, conditionValue); } else { if (typeof conditionValue !== "boolean") { throw new TypeError( "The condition value for " + conditionalMatch[0] + " isn't resolving to a boolean." ); } if (booleanNegation) { conditionValue = !conditionValue; } if (!conditionValue) { name = "@empty"; } else { name = name.replace(conditionalRegEx, ""); } } if (name === "@empty") { return normalize.call(loader, name, parentName, parentAddress, pluginNormalize); } else { // call the full normalize in case the module name // is an npm package (that needs to be normalized) return loader.normalize.call(loader, name, parentName, parentAddress, pluginNormalize); } }; var isBuild = (loader.env || "").indexOf("build") === 0; var pluginLoader = isBuild ? loader : (loader.pluginLoader || loader); return pluginLoader["import"](conditionModule, { name: parentName, address: parentAddress }) .then(function(m) { return pluginLoader .normalize(conditionModule, parentName, parentAddress, pluginNormalize) .then(function(fullName) { includeInBuild(pluginLoader, fullName); return m; }); }) .then(function(m) { return isBuild ? handleConditionalBuild() : handleConditionalEval(m); }); } return Promise.resolve(normalize.call(loader, name, parentName, parentAddress, pluginNormalize)); }; } if (typeof System !== "undefined") { addConditionals(System); } });