diff --git a/scripts/gulpfiles/build_tasks.js b/scripts/gulpfiles/build_tasks.js index 678c9ab4cd1..47b3241646a 100644 --- a/scripts/gulpfiles/build_tasks.js +++ b/scripts/gulpfiles/build_tasks.js @@ -61,7 +61,16 @@ const CHUNK_CACHE_FILE = 'scripts/gulpfiles/chunks.json' * wrapper argument, but as it will appear many times in the compiled * output it is preferable that it be short. */ -const NAMESPACE_OBJECT = '$'; +const NAMESPACE_VARIABLE = '$'; + +/** + * Property that will be used to store the value of the namespace + * object on each chunk's exported object. This is so that dependent + * chunks can retrieve the namespace object and thereby access modules + * defined in the parent chunk (or it's parent, etc.). This should be + * chosen so as to not collide with any exported name. + */ +const NAMESPACE_PROPERTY = '__namespace__'; /** * A list of chunks. Order matters: later chunks can depend on @@ -73,25 +82,16 @@ const NAMESPACE_OBJECT = '$'; * will be written to. * - .entry: the source .js file which is the entrypoint for the * chunk. - * - .exports: a variable or property that will (prefixed with - * NAMESPACE_OBJECT) be returned from the factory function and which - * (sans prefix) will be set in the global scope to that returned - * value if the module is loaded in a browser. - * - .importAs: the name that this chunk's exports object will be - * given when passed to the factory function of other chunks that - * depend on it. (Needs to be distinct from .exports since (e.g.) - * "Blockly.libraryBlocks" is not a valid variable name.) - * - .factoryPreamble: code to override the default wrapper factory - * function preamble. - * - .factoryPostamble: code to override the default wrapper factory - * function postabmle. + * - .reexport: if running in a browser, save the chunk's exports + * object at this location in the global namespace. * * The function getChunkOptions will, after running * closure-calculate-chunks, update each chunk to add the following * properties: * - * - .dependencies: a list of the chunks the chunk depends upon. - * - .wrapper: the chunk wrapper. + * - .parent: the parent chunk of the given chunk. Typically + * chunks[0], except for chunk[0].parent which will be null. + * - .wrapper: the generated chunk wrapper. * * Output files will be named .js. */ @@ -99,55 +99,40 @@ const chunks = [ { name: 'blockly', entry: 'core/blockly.js', - exports: 'Blockly', - importAs: 'Blockly', - factoryPreamble: `const ${NAMESPACE_OBJECT}={};`, - factoryPostamble: - `${NAMESPACE_OBJECT}.Blockly.internal_=${NAMESPACE_OBJECT};`, + reexport: 'Blockly', }, { name: 'blocks', entry: 'blocks/blocks.js', - exports: 'Blockly.libraryBlocks', - importAs: 'libraryBlocks', + reexport: 'Blockly.libraryBlocks', }, { name: 'javascript', entry: 'generators/javascript/all.js', - exports: 'Blockly.JavaScript', + reexport: 'Blockly.JavaScript', }, { name: 'python', entry: 'generators/python/all.js', - exports: 'Blockly.Python', + reexport: 'Blockly.Python', }, { name: 'php', entry: 'generators/php/all.js', - exports: 'Blockly.PHP', + reexport: 'Blockly.PHP', }, { name: 'lua', entry: 'generators/lua/all.js', - exports: 'Blockly.Lua', + reexport: 'Blockly.Lua', }, { name: 'dart', entry: 'generators/dart/all.js', - exports: 'Blockly.Dart', + reexport: 'Blockly.Dart', } ]; -/** - * The default factory function premable. - */ -const FACTORY_PREAMBLE = `const ${NAMESPACE_OBJECT}=Blockly.internal_;`; - -/** - * The default factory function postamble. - */ -const FACTORY_POSTAMBLE = ''; - const licenseRegex = `\\/\\*\\* \\* @license \\* (Copyright \\d+ (Google LLC|Massachusetts Institute of Technology)) @@ -356,13 +341,40 @@ function buildLangfiles(done) { * Definition. */ function chunkWrapper(chunk) { - const fileNames = chunk.dependencies.map( - d => JSON.stringify(`./${d.name}${COMPILED_SUFFIX}.js`)); - const amdDeps = fileNames.join(', '); - const cjsDeps = fileNames.map(f => `require(${f})`).join(', '); - const browserDeps = - chunk.dependencies.map(d => `root.${d.exports}`).join(', '); - const factoryParams = chunk.dependencies.map(d => d.importAs).join(', '); + // Each chunk can have only a single dependency, which is its parent + // chunk. It is used only to retrieve the namespace object, which + // is saved on to the exports object for the chunk so that any child + // chunk(s) can obtain it. + + // JavaScript expressions for the amd, cjs and browser dependencies. + let amdDepsExpr = ''; + let cjsDepsExpr = ''; + let browserDepsExpr = ''; + // Arguments for the factory function. + let factoryArgs = ''; + // Expression to get or create the namespace object. + let namespaceExpr = `{}`; + + if (chunk.parent) { + const parentFilename = + JSON.stringify(`./${chunk.parent.name}${COMPILED_SUFFIX}.js`); + amdDepsExpr = parentFilename; + cjsDepsExpr = `require(${parentFilename})`; + browserDepsExpr = `root.${chunk.parent.reexport}`; + factoryArgs = '__parent__'; + namespaceExpr = `${factoryArgs}.${NAMESPACE_PROPERTY}`; + } + + // Expression that evaluates the the value of the exports object for + // the specified chunk. For now we guess the name that is created + // by the module's goog.module.delcareLegacyNamespace call based on + // chunk.reexport. + const exportsExpression = `${NAMESPACE_VARIABLE}.${chunk.reexport}`; + // In near future we might try to guess the internally-generated + // name for the ES module's exports object. + // const exportsExpression = + // 'module$' + chunk.entry.replace(/\.m?js$/, '').replace(/\//g, '$'); + // Note that when loading in a browser the base of the exported path // (e.g. Blockly.blocks.all - see issue #5932) might not exist @@ -374,18 +386,18 @@ function chunkWrapper(chunk) { /* eslint-disable */ ;(function(root, factory) { if (typeof define === 'function' && define.amd) { // AMD - define([${amdDeps}], factory); + define([${amdDepsExpr}], factory); } else if (typeof exports === 'object') { // Node.js - module.exports = factory(${cjsDeps}); + module.exports = factory(${cjsDepsExpr}); } else { // Browser - var factoryExports = factory(${browserDeps}); - root.${chunk.exports} = factoryExports; + var factoryExports = factory(${browserDepsExpr}); + root.${chunk.reexport} = factoryExports; } -}(this, function(${factoryParams}) { -${chunk.factoryPreamble || FACTORY_PREAMBLE} +}(this, function(${factoryArgs}) { +var ${NAMESPACE_VARIABLE}=${namespaceExpr}; %output% -${chunk.factoryPostamble || FACTORY_POSTAMBLE} -return ${NAMESPACE_OBJECT}.${chunk.exports}; +${exportsExpression}.${NAMESPACE_PROPERTY}=${NAMESPACE_VARIABLE}; +return ${exportsExpression}; })); `; }; @@ -459,26 +471,33 @@ function getChunkOptions() { // This is designed to be passed directly as-is as the options // object to the Closure Compiler node API, but we want to replace // the unhelpful entry-point based chunk names (let's call these - // "nicknames") with the ones from chunks. Luckily they will be in - // the same order that the entry points were supplied in - i.e., - // they correspond 1:1 with the entries in chunks. + // "nicknames") with the ones from chunks. Unforutnately there's no + // guarnatee they will be in the same order that the entry points + // were supplied in (though it happens to work out that way if no + // chunk depends on any chunk but the first), so we look for + // one of the entrypoints amongst the files in each chunk. const chunkByNickname = Object.create(null); - let jsFiles = rawOptions.js; - const chunkList = rawOptions.chunk.map((element, index) => { - const [nickname, numJsFiles, dependencyNicks] = element.split(':'); - const chunk = chunks[index]; + const jsFiles = rawOptions.js.slice(); // Will be modified via .splice! + const chunkList = rawOptions.chunk.map((element) => { + const [nickname, numJsFiles, parentNick] = element.split(':'); + + // Get array of files for just this chunk. + const chunkFiles = jsFiles.splice(0, numJsFiles); + + // Figure out which chunk this is by looking for one of the + // known chunk entrypoints in chunkFiles. N.B.: O(n*m). :-( + const chunk = chunks.find( + chunk => chunkFiles.find(f => f.endsWith('/' + chunk.entry))); + if (!chunk) throw new Error('Unable to identify chunk'); - // Replace nicknames with our names. + // Replace nicknames with the names we chose. chunkByNickname[nickname] = chunk; - if (!dependencyNicks) { // Chunk has no dependencies. - chunk.dependencies = []; + if (!parentNick) { // Chunk has no parent. + chunk.parent = null; return `${chunk.name}:${numJsFiles}`; } - chunk.dependencies = - dependencyNicks.split(',').map(nick => chunkByNickname[nick]); - const dependencyNames = - chunk.dependencies.map(dependency => dependency.name).join(','); - return `${chunk.name}:${numJsFiles}:${dependencyNames}`; + chunk.parent = chunkByNickname[parentNick]; + return `${chunk.name}:${numJsFiles}:${chunk.parent.name}`; }); // Generate a chunk wrapper for each chunk. @@ -577,7 +596,7 @@ function buildCompiled() { define: 'Blockly.VERSION="' + packageJson.version + '"', chunk: chunkOptions.chunk, chunk_wrapper: chunkOptions.chunk_wrapper, - rename_prefix_namespace: NAMESPACE_OBJECT, + rename_prefix_namespace: NAMESPACE_VARIABLE, // Don't supply the list of source files in chunkOptions.js as an // option to Closure Compiler; instead feed them as input via gulp.src. };