Skip to content

Commit

Permalink
refactor(build): Prepare UMD wrapper generation for transition to ES …
Browse files Browse the repository at this point in the history
…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 <aschmiedt@google.com>
  • Loading branch information
cpcallen and alschmiedt authored Mar 21, 2022
1 parent 243fc52 commit 9d62f92
Showing 1 changed file with 87 additions and 68 deletions.
155 changes: 87 additions & 68 deletions scripts/gulpfiles/build_tasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -73,81 +82,57 @@ 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 <chunk.name><COMPILED_SUFFIX>.js.
*/
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))
Expand Down Expand Up @@ -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
Expand All @@ -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};
}));
`;
};
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
};
Expand Down

0 comments on commit 9d62f92

Please sign in to comment.