From 9d62f92a2ffddac6fb5fcb3c3fa7405250b4f47d Mon Sep 17 00:00:00 2001 From: Christopher Allen Date: Mon, 21 Mar 2022 20:10:48 +0000 Subject: [PATCH] refactor(build): Prepare UMD wrapper generation for transition to ES modules (#5993) * fix(build): Correctly handle out-of-order chunks It turns out that closure-calculate-chunks does not guarantee that calculated chunks will be output in the same order as the entrypoints, so modify getChunkOptions so that it no longer makes that assumption. * refactor(build): Introduce NAMESPACE_PROPERTY; rename NAMESPACE_OBJECT Rename the constant NAMESPACE_OBJECT to NAMESPACE_VARIABLE to better explain its actual meaning, and introduce NAMESPACE_PROPERTY to specify what property the namespace object will be stored in (and change the previous value, "internal_", to "__namespace__" to reduce the chance of conflicts with properties created by the output of Closure Compiler). * refactor(build): Always save namespace object on chunk exports object This is so that chunks whose parent chunk is not the root chunk (chunks[0]) can obtain the namespace object. (See following commit.) * fix(build): Correct handling of chunk dependencies Previously getChunkOptions and chunkWrapper incorrectly assumed that a chunk could have more than one dependency. In fact, 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. Update getChunkOptions and chunkWrapper (making the latter longer but more readable) accordingly. * refactor(build): Rename/repurpose chunk.exports -> .reexport And remove chunk.importAs, since it was no longer being used anywhere. * fix: remove unnecessary s from reexports Co-authored-by: alschmiedt --- scripts/gulpfiles/build_tasks.js | 155 +++++++++++++++++-------------- 1 file changed, 87 insertions(+), 68 deletions(-) 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. };