refactor(build): Don't use closure-calculate-chunks

Rewrite the getChunkOptions function to not use
closure-calculate-chunks, but instead just chunk the input files
(more or less) by subdirectory: first chunk is core/, second is
blocks/, etc.

This does make a material change to blockly_compressed.js,
because we end up feeding several empty modules that contain
only typescript interface declarations and which tsc
compiles to "export {};" in the input to Closure Compiler
(closure-calculate-chunks is smart enough to notice that
no other module depends on these), which results in ~1.7KiB of
superflous

    var module$build$src$core$interfaces$i_ast_node_location_svg={};

declarations.  This can be avoided by filtering such empty modules
out but that has been left for a future commit.

This adds the glob NPM package as a dev dependency, but gulp
and several other existing dev dependencies already depend on
it.

Build time is sped up by about a factor of 3x, due to removal
of the buildDeps step that was really slow:

$ time npm run build

before:
real    0m24.410s
user    0m16.010s
sys     0m1.140s

after:
real    0m8.397s
user    0m11.976s
sys     0m0.694s
This commit is contained in:
Christopher Allen
2023-09-08 18:08:32 +01:00
parent 9390796e83
commit fc18cbdad8
3 changed files with 348 additions and 658 deletions

View File

@@ -18,6 +18,7 @@ const fs = require('fs');
const fsPromises = require('fs/promises');
const {exec, execSync} = require('child_process');
const {globSync} = require('glob');
const closureCompiler = require('google-closure-compiler').gulp();
const argv = require('yargs').argv;
const {rimraf} = require('rimraf');
@@ -84,8 +85,10 @@ const NAMESPACE_PROPERTY = '__namespace__';
* - .name: the name of the chunk. Used to label it when describing
* it to Closure Compiler and forms the prefix of filename the chunk
* will be written to.
* - .files: A glob or array of globs, relative to TSC_OUTPUT_DIR,
* matching the files to include in the chunk.
* - .entry: the source .js file which is the entrypoint for the
* chunk.
* chunk, relative to TSC_OUTPUT_DIR.
* - .exports: an expression evaluating to the exports/Module object
* of module that is the chunk's entrypoint / top level module.
* - .scriptExport: When the chunk is loaded as a script (e.g., via a
@@ -97,20 +100,15 @@ const NAMESPACE_PROPERTY = '__namespace__';
* loaded as a script, the specified named exports will be saved at
* the specified locations (which again must be global variables or
* properties on already-existing objects). Optional.
*
* The function getChunkOptions will, after running
* closure-calculate-chunks, update each chunk to add the following
* properties:
*
* - .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.
* - .parent: the parent chunk of the given chunk; null for the root
* chunk.
*
* Output files will be named <chunk.name><COMPILED_SUFFIX>.js.
*/
const chunks = [
{
name: 'blockly',
files: 'core/**/*.js',
entry: path.join(TSC_OUTPUT_DIR, 'core', 'main.js'),
moduleEntry: path.join(TSC_OUTPUT_DIR, 'core', 'blockly.js'),
exports: 'module$build$src$core$blockly',
@@ -118,12 +116,14 @@ const chunks = [
},
{
name: 'blocks',
files: 'blocks/**/*.js',
entry: path.join(TSC_OUTPUT_DIR, 'blocks', 'blocks.js'),
exports: 'module$build$src$blocks$blocks',
scriptExport: 'Blockly.libraryBlocks',
},
{
name: 'javascript',
files: ['generators/javascript.js', 'generators/javascript/**/*.js'],
entry: path.join(TSC_OUTPUT_DIR, 'generators', 'javascript.js'),
exports: 'module$build$src$generators$javascript',
scriptExport: 'javascript',
@@ -131,6 +131,7 @@ const chunks = [
},
{
name: 'python',
files: ['generators/python.js', 'generators/python/**/*.js'],
entry: path.join(TSC_OUTPUT_DIR, 'generators', 'python.js'),
exports: 'module$build$src$generators$python',
scriptExport: 'python',
@@ -138,6 +139,7 @@ const chunks = [
},
{
name: 'php',
files: ['generators/php.js', 'generators/php/**/*.js'],
entry: path.join(TSC_OUTPUT_DIR, 'generators', 'php.js'),
exports: 'module$build$src$generators$php',
scriptExport: 'php',
@@ -145,6 +147,7 @@ const chunks = [
},
{
name: 'lua',
files: ['generators/lua.js', 'generators/lua/**/*.js'],
entry: path.join(TSC_OUTPUT_DIR, 'generators', 'lua.js'),
exports: 'module$build$src$generators$lua',
scriptExport: 'lua',
@@ -152,13 +155,19 @@ const chunks = [
},
{
name: 'dart',
files: ['generators/dart.js', 'generators/dart/**/*.js'],
entry: path.join(TSC_OUTPUT_DIR, 'generators', 'dart.js'),
exports: 'module$build$src$generators$dart',
scriptExport: 'dart',
scriptNamedExports: {'Blockly.Dart': 'dartGenerator'},
}
},
];
chunks[0].parent = null;
for (let i = 1; i < chunks.length; i++) {
chunks[i].parent = chunks[0];
}
const licenseRegex = `\\/\\*\\*
\\* @license
\\* (Copyright \\d+ (Google LLC|Massachusetts Institute of Technology))
@@ -507,87 +516,59 @@ return ${chunk.exports};
}
/**
* Get chunking options to pass to Closure Compiler by using
* closure-calculate-chunks (hereafter "ccc") to generate them based
* on the deps.js file (which must be up to date!).
* Compute the chunking options to pass to Closure Compiler. Output
* is in the form:
*
* The generated options are modified to use the original chunk names
* given in chunks instead of the entry-point based names used by ccc.
* {
* "chunk": [
* "blockly:286",
* "blocks:10:blockly",
* "javascript:11:blockly",
* // ... one per chunk
* ],
* "js": [
* "build/src/core/any_aliases.js",
* "build/src/core/block.js",
* "build/src/core/block_animations.js",
* // ... many more files, in order by chunk
* ],
* "chunk_wrapper": [
* "blockly:// Do not edit this file...",
* "blocks:// Do not edit this file...",
* // ... one per chunk
* ]
* }
*
* @return {{chunk: !Array<string>, js: !Array<string>}} The chunking
* information, in the same form as emitted by
* closure-calculate-chunks.
* This is designed to be passed directly as-is as the options object
* to the Closure Compiler node API, and be compatible with that
* emitted by closure-calculate-chunks.
*
* @return {{chunk: !Array<string>,
* js: !Array<string>,
* chunk_wrapper: !Array<string>}}
* The chunking options, in the format described above.
*/
function getChunkOptions() {
const cccArgs = [
`--deps-file './${DEPS_FILE}'`,
...(chunks.map(chunk => `--entrypoint '${chunk.entry}'`)),
];
const cccCommand = `closure-calculate-chunks ${cccArgs.join(' ')}`;
const chunkOptions = [];
const allFiles = [];
const rawOptions = JSON.parse(execSync(cccCommand));
// rawOptions should now be of the form:
//
// {
// chunk: [
// 'blockly:258',
// 'all:10:blockly',
// 'all1:11:blockly',
// 'all2:11:blockly',
// /* ... remaining handful of chunks */
// ],
// js: [
// './build/src/core/serialization/workspaces.js',
// './build/src/core/serialization/variables.js',
// /* ... remaining several hundred files */
// ],
// }
//
// 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. 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);
// Copy and convert to posix js file paths.
// Result will be modified via `.splice`!
const jsFiles = rawOptions.js.map(p => posixPath(p));
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 => {
return f.endsWith('/' + chunk.entry.replaceAll('\\', '/'));
}
));
if (!chunk) throw new Error('Unable to identify chunk');
// Replace nicknames with the names we chose.
chunkByNickname[nickname] = chunk;
if (!parentNick) { // Chunk has no parent.
chunk.parent = null;
return `${chunk.name}:${numJsFiles}`;
}
chunk.parent = chunkByNickname[parentNick];
return `${chunk.name}:${numJsFiles}:${chunk.parent.name}`;
});
// Generate a chunk wrapper for each chunk.
for (const chunk of chunks) {
chunk.wrapper = chunkWrapper(chunk);
const globs = typeof chunk.files === 'string' ? [chunk.files] : chunk.files;
const files = globs
.flatMap((glob) => globSync(glob, {cwd: TSC_OUTPUT_DIR}))
.map((s) => `${TSC_OUTPUT_DIR}/${s}`);
chunkOptions.push(
`${chunk.name}:${files.length}` +
(chunk.parent ? `:${chunk.parent.name}` : ''),
);
allFiles.push(...files);
}
const chunkWrappers = chunks.map(chunk => `${chunk.name}:${chunk.wrapper}`);
return {chunk: chunkList, js: rawOptions.js, chunk_wrapper: chunkWrappers};
const chunkWrappers = chunks.map(
(chunk) => `${chunk.name}:${chunkWrapper(chunk)}`,
);
return {chunk: chunkOptions, js: allFiles, chunk_wrapper: chunkWrappers};
}
/**
@@ -630,10 +611,6 @@ function compile(options) {
/**
* This task compiles the core library, blocks and generators, creating
* blockly_compressed.js, blocks_compressed.js, etc.
*
* The deps.js file must be up-to-date.
*
* Prerequisite: buildDeps.
*/
function buildCompiled() {
// Get chunking.
@@ -765,7 +742,7 @@ function cleanBuildDir() {
exports.cleanBuildDir = cleanBuildDir;
exports.langfiles = buildLangfiles; // Build build/msg/*.js from msg/json/*.
exports.tsc = buildJavaScript;
exports.minify = gulp.series(exports.tsc, buildDeps, buildCompiled, buildShims);
exports.minify = gulp.series(exports.tsc, buildCompiled, buildShims);
exports.build = gulp.parallel(exports.minify, exports.langfiles);
// Manually-invokable targets, with prerequisites where required.