From a04da67bc926c1f36310d2d49bfeca2a71ef32f2 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 21 May 2026 13:36:16 -0700 Subject: [PATCH 1/8] chore: Format build_tasks.mjs --- .../blockly/scripts/gulpfiles/build_tasks.mjs | 194 ++++++++++-------- 1 file changed, 108 insertions(+), 86 deletions(-) diff --git a/packages/blockly/scripts/gulpfiles/build_tasks.mjs b/packages/blockly/scripts/gulpfiles/build_tasks.mjs index 00e189e889f..884613b2ada 100644 --- a/packages/blockly/scripts/gulpfiles/build_tasks.mjs +++ b/packages/blockly/scripts/gulpfiles/build_tasks.mjs @@ -9,22 +9,28 @@ */ import * as gulp from 'gulp'; -import replace from 'gulp-replace'; import rename from 'gulp-rename'; +import replace from 'gulp-replace'; import sourcemaps from 'gulp-sourcemaps'; -import * as path from 'path'; +import {execSync} from 'child_process'; import * as fs from 'fs'; import * as fsPromises from 'fs/promises'; -import {exec, execSync} from 'child_process'; +import * as path from 'path'; import {globSync} from 'glob'; import {gulp as closureCompiler} from 'google-closure-compiler'; +import {rimraf} from 'rimraf'; import yargs from 'yargs'; import {hideBin} from 'yargs/helpers'; -import {rimraf} from 'rimraf'; -import {BUILD_DIR, LANG_BUILD_DIR, RELEASE_DIR, TSC_OUTPUT_DIR, TYPINGS_BUILD_DIR} from './config.mjs'; +import { + BUILD_DIR, + LANG_BUILD_DIR, + RELEASE_DIR, + TSC_OUTPUT_DIR, + TYPINGS_BUILD_DIR, +} from './config.mjs'; import {getPackageJson} from './helper_tasks.mjs'; import {posixPath, quote} from '../helpers.js'; @@ -214,7 +220,7 @@ const JSCOMP_ERROR = [ 'duplicateMessage', 'es5Strict', 'externsValidation', - 'extraRequire', // Undocumented but valid. + 'extraRequire', // Undocumented but valid. 'functionParams', // 'globalThis', // This types are stripped by tsc. 'invalidCasts', @@ -234,7 +240,7 @@ const JSCOMP_ERROR = [ // 'reportUnknownTypes', // VERY verbose. // 'strictCheckTypes', // Use --strict to enable. // 'strictMissingProperties', // Part of strictCheckTypes. - 'strictModuleChecks', // Undocumented but valid. + 'strictModuleChecks', // Undocumented but valid. 'strictModuleDepCheck', // 'strictPrimitiveOperators', // Part of strictCheckTypes. 'suspiciousCode', @@ -255,10 +261,7 @@ const JSCOMP_ERROR = [ * For most (all?) diagnostic groups this is the default level, so * it's generally sufficient to remove them from JSCOMP_ERROR. */ -const JSCOMP_WARNING = [ - 'deprecated', - 'deprecatedAnnotations', -]; +const JSCOMP_WARNING = ['deprecated', 'deprecatedAnnotations']; /** * Closure Compiler diagnostic groups we want to be ignored. These @@ -281,8 +284,8 @@ const JSCOMP_OFF = [ * DiagnosticGroup. */ 'checkTypes', - 'nonStandardJsDocs', // Due to @internal - 'unusedLocalVariables', // Due to code generated for merged namespaces. + 'nonStandardJsDocs', // Due to @internal + 'unusedLocalVariables', // Due to code generated for merged namespaces. /* In order to transition to ES modules, modules will need to import * one another by relative paths. This means that the previous @@ -310,8 +313,9 @@ const JSCOMP_OFF = [ */ export function tsc(done) { execSync( - `tsc -outDir "${TSC_OUTPUT_DIR}" -declarationDir "${TYPINGS_BUILD_DIR}"`, - {stdio: 'inherit'}); + `tsc -outDir "${TSC_OUTPUT_DIR}" -declarationDir "${TYPINGS_BUILD_DIR}"`, + {stdio: 'inherit'}, + ); execSync(`node scripts/tsick.js "${TSC_OUTPUT_DIR}"`, {stdio: 'inherit'}); done(); } @@ -357,9 +361,10 @@ var languages = null; function getLanguages() { if (!languages) { const skip = /^(keys|synonyms|qqq|constants)\.json$/; - languages = fs.readdirSync(path.join('msg', 'json')) - .filter(file => file.endsWith('json') && !skip.test(file)) - .map(file => file.replace(/\.json$/, '')); + languages = fs + .readdirSync(path.join('msg', 'json')) + .filter((file) => file.endsWith('json') && !skip.test(file)) + .map((file) => file.replace(/\.json$/, '')); } return languages; } @@ -373,8 +378,9 @@ function buildLangfiles(done) { fs.mkdirSync(LANG_BUILD_DIR, {recursive: true}); // Run create_messages.py. - const inputFiles = getLanguages().map( - lang => path.join('msg', 'json', `${lang}.json`)); + const inputFiles = getLanguages().map((lang) => + path.join('msg', 'json', `${lang}.json`), + ); const createMessagesCmd = `${PYTHON} ./scripts/i18n/create_messages.py \ --source_lang_file ${path.join('msg', 'json', 'en.json')} \ @@ -409,8 +415,9 @@ function chunkWrapper(chunk) { let namespaceExpr = `{}`; if (chunk.parent) { - const parentFilename = - JSON.stringify(`./${chunk.parent.name}${COMPILED_SUFFIX}.js`); + const parentFilename = JSON.stringify( + `./${chunk.parent.name}${COMPILED_SUFFIX}.js`, + ); amdDepsExpr = parentFilename; cjsDepsExpr = `require(${parentFilename})`; scriptDepsExpr = `root.${chunk.parent.scriptExport}`; @@ -426,8 +433,9 @@ function chunkWrapper(chunk) { ]; for (var location in chunk.scriptNamedExports) { const namedExport = chunk.scriptNamedExports[location]; - scriptExportStatements.push( - `root.${location} = root.${chunk.scriptExport}.${namedExport};`); + scriptExportStatements.push( + `root.${location} = root.${chunk.scriptExport}.${namedExport};`, + ); } // Note that when loading in a browser the base of the exported path @@ -530,9 +538,7 @@ function compile(options) { language_out: 'ECMASCRIPT_2015', jscomp_off: [...JSCOMP_OFF], rewrite_polyfills: true, - hide_warnings_for: [ - 'node_modules', - ], + hide_warnings_for: ['node_modules'], define: ['COMPILED=true'], }; if (argv.debug || argv.strict) { @@ -556,7 +562,7 @@ function buildCompiled() { // Get chunking. const chunkOptions = getChunkOptions(); // Closure Compiler options. - const packageJson = getPackageJson(); // For version number. + const packageJson = getPackageJson(); // For version number. const options = { // The documentation for @define claims you can't use it on a // non-global, but the Closure Compiler turns everything in to a @@ -573,13 +579,14 @@ function buildCompiled() { }; // Fire up compilation pipline. - return gulp.src(chunkOptions.js, {base: './'}) - .pipe(stripApacheLicense()) - .pipe(sourcemaps.init()) - .pipe(compile(options)) - .pipe(rename({suffix: COMPILED_SUFFIX})) - .pipe(sourcemaps.write('.')) - .pipe(gulp.dest(RELEASE_DIR)); + return gulp + .src(chunkOptions.js, {base: './'}) + .pipe(stripApacheLicense()) + .pipe(sourcemaps.init()) + .pipe(compile(options)) + .pipe(rename({suffix: COMPILED_SUFFIX})) + .pipe(sourcemaps.write('.')) + .pipe(gulp.dest(RELEASE_DIR)); } /** @@ -600,47 +607,54 @@ async function buildShims() { const TMP_PACKAGE_JSON = path.join(BUILD_DIR, 'package.json'); await fsPromises.writeFile(TMP_PACKAGE_JSON, '{"type": "module"}'); - await Promise.all(chunks.map(async (chunk) => { - // Import chunk entrypoint to get names of exports for chunk. - const entryPath = path.posix.join(TSC_OUTPUT_DIR_POSIX, chunk.entry); - const exportedNames = Object.keys(await import(`../../${entryPath}`)); + await Promise.all( + chunks.map(async (chunk) => { + // Import chunk entrypoint to get names of exports for chunk. + const entryPath = path.posix.join(TSC_OUTPUT_DIR_POSIX, chunk.entry); + const exportedNames = Object.keys(await import(`../../${entryPath}`)); - // Write an ESM wrapper that imports the CJS module and re-exports - // its named exports. - const cjsPath = `./${chunk.name}${COMPILED_SUFFIX}.js`; - const wrapperPath = path.join(RELEASE_DIR, `${chunk.name}.mjs`); - const importName = chunk.scriptExport.replace(/.*\./, ''); + // Write an ESM wrapper that imports the CJS module and re-exports + // its named exports. + const cjsPath = `./${chunk.name}${COMPILED_SUFFIX}.js`; + const wrapperPath = path.join(RELEASE_DIR, `${chunk.name}.mjs`); + const importName = chunk.scriptExport.replace(/.*\./, ''); - await fsPromises.writeFile(wrapperPath, + await fsPromises.writeFile( + wrapperPath, `import ${importName} from '${cjsPath}'; export const { ${exportedNames.map((name) => ` ${name},`).join('\n')} } = ${importName}; -`); - - // For first chunk, write an additional ESM wrapper for 'blockly' - // entrypoint since it has the same exports as 'blockly/core'. - if (chunk.name === 'blockly') { - await fsPromises.writeFile(path.join(RELEASE_DIR, `index.mjs`), +`, + ); + + // For first chunk, write an additional ESM wrapper for 'blockly' + // entrypoint since it has the same exports as 'blockly/core'. + if (chunk.name === 'blockly') { + await fsPromises.writeFile( + path.join(RELEASE_DIR, `index.mjs`), `import Blockly from './index.js'; export const { ${exportedNames.map((name) => ` ${name},`).join('\n')} } = Blockly; -`); - } - - // Write a loading shim that uses loadChunk to either import the - // chunk's entrypoint (e.g. build/src/core/blockly.js) or load the - // compressed chunk (e.g. dist/blockly_compressed.js) as a script. - const scriptPath = - path.posix.join(RELEASE_DIR, `${chunk.name}${COMPILED_SUFFIX}.js`); - const shimPath = path.join(BUILD_DIR, `${chunk.name}.loader.mjs`); - const parentImport = - chunk.parent ? - `import ${quote(`./${chunk.parent.name}.loader.mjs`)};` : - ''; - - await fsPromises.writeFile(shimPath, +`, + ); + } + + // Write a loading shim that uses loadChunk to either import the + // chunk's entrypoint (e.g. build/src/core/blockly.js) or load the + // compressed chunk (e.g. dist/blockly_compressed.js) as a script. + const scriptPath = path.posix.join( + RELEASE_DIR, + `${chunk.name}${COMPILED_SUFFIX}.js`, + ); + const shimPath = path.join(BUILD_DIR, `${chunk.name}.loader.mjs`); + const parentImport = chunk.parent + ? `import ${quote(`./${chunk.parent.name}.loader.mjs`)};` + : ''; + + await fsPromises.writeFile( + shimPath, `import {loadChunk} from '../tests/scripts/load.mjs'; ${parentImport} @@ -651,8 +665,10 @@ ${exportedNames.map((name) => ` ${name},`).join('\n')} ${quote(scriptPath)}, ${quote(chunk.scriptExport)}, ); -`); - })); +`, + ); + }), + ); await fsPromises.rm(TMP_PACKAGE_JSON); } @@ -674,20 +690,24 @@ async function buildLangfileShims() { const exportedNames = Object.keys(globalThis.Blockly.Msg); delete globalThis.Blockly; - await Promise.all(getLanguages().map(async (lang) => { - // Write an ESM wrapper that imports the CJS module and re-exports - // its named exports. - const cjsPath = `./${lang}.js`; - const wrapperPath = path.join(RELEASE_DIR, 'msg', `${lang}.mjs`); - const safeLang = lang.replace(/-/g, '_'); + await Promise.all( + getLanguages().map(async (lang) => { + // Write an ESM wrapper that imports the CJS module and re-exports + // its named exports. + const cjsPath = `./${lang}.js`; + const wrapperPath = path.join(RELEASE_DIR, 'msg', `${lang}.mjs`); + const safeLang = lang.replace(/-/g, '_'); - await fsPromises.writeFile(wrapperPath, + await fsPromises.writeFile( + wrapperPath, `import ${safeLang} from '${cjsPath}'; export const { ${exportedNames.map((name) => ` ${name},`).join('\n')} } = ${safeLang}; -`); - })); +`, + ); + }), + ); } /** @@ -720,13 +740,13 @@ function compileAdvancedCompilationTest() { entry_point: './tests/compile/main.js', js_output_file: 'main_compressed.js', }; - return gulp.src(srcs, {base: './'}) - .pipe(stripApacheLicense()) - .pipe(sourcemaps.init()) - .pipe(compile(options)) - .pipe(sourcemaps.write( - '.', {includeContent: false, sourceRoot: '../../'})) - .pipe(gulp.dest('./tests/compile/')); + return gulp + .src(srcs, {base: './'}) + .pipe(stripApacheLicense()) + .pipe(sourcemaps.init()) + .pipe(compile(options)) + .pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: '../../'})) + .pipe(gulp.dest('./tests/compile/')); } /** @@ -749,5 +769,7 @@ export const build = gulp.parallel(minify, langfiles); // Manually-invokable targets, with prerequisites where required. // function messages, above -export const buildAdvancedCompilationTest = - gulp.series(tsc, compileAdvancedCompilationTest); +export const buildAdvancedCompilationTest = gulp.series( + tsc, + compileAdvancedCompilationTest, +); From cfa3dfd96108ad776d8fe7189a705258ab9f39e4 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 21 May 2026 14:12:42 -0700 Subject: [PATCH 2/8] feat: Enable `assume_function_wrapper` build flag --- .../blockly/scripts/gulpfiles/build_tasks.mjs | 92 +++++++++++++++---- 1 file changed, 75 insertions(+), 17 deletions(-) diff --git a/packages/blockly/scripts/gulpfiles/build_tasks.mjs b/packages/blockly/scripts/gulpfiles/build_tasks.mjs index 884613b2ada..d6bd5f6a859 100644 --- a/packages/blockly/scripts/gulpfiles/build_tasks.mjs +++ b/packages/blockly/scripts/gulpfiles/build_tasks.mjs @@ -16,6 +16,7 @@ import sourcemaps from 'gulp-sourcemaps'; import {execSync} from 'child_process'; import * as fs from 'fs'; import * as fsPromises from 'fs/promises'; +import {finished} from 'node:stream/promises'; import * as path from 'path'; import {globSync} from 'glob'; @@ -86,6 +87,14 @@ const NAMESPACE_VARIABLE = '$'; */ const NAMESPACE_PROPERTY = '__namespace__'; +/** + * Property on the shared namespace object where each chunk's export + * object is stored before the UMD wrapper returns it. A string literal + * is used so that Closure Compiler will not rename it when + * assume_function_wrapper is enabled (see issue #5795). + */ +const CHUNK_EXPORT_PROPERTY = '__chunkExport__'; + /** * A list of chunks. Order matters: later chunks can depend on * earlier ones, but not vice-versa. All chunks are assumed to depend @@ -176,9 +185,58 @@ function modulePath(chunk) { return 'module$' + entryPath.replace(/\.js$/, '').replaceAll('/', '$'); } +/** + * Directory (relative to TSC_OUTPUT_DIR) where generated chunk export + * collector files are written. + */ +const CHUNK_EXPORTS_DIR = 'chunk_exports'; + +/** + * Return the path to the generated chunk export collector file for + * the given chunk, relative to TSC_OUTPUT_DIR. + * @param {{name: string}} chunk + * @return {string} + */ +function chunkExportPath(chunk) { + return path.posix.join(CHUNK_EXPORTS_DIR, `${chunk.name}_export.js`); +} + +/** + * Write generated chunk export collector files, one per chunk. Each + * file imports the chunk's entrypoint module and saves its exports on + * to the shared namespace object using a string-literal property name + * so that the UMD wrapper can return them after compilation with + * assume_function_wrapper enabled. + * + * The namespace object is declared by the chunk wrapper (see + * chunkWrapper); suppress undefined-variable diagnostics here because + * Closure Compiler analyses this file separately from the wrapper. + */ +async function writeChunkExportFiles() { + const outDir = path.join(TSC_OUTPUT_DIR, CHUNK_EXPORTS_DIR); + await fsPromises.mkdir(outDir, {recursive: true}); + + await Promise.all( + chunks.map(async (chunk) => { + const exportFile = chunkExportPath(chunk); + const importPath = posixPath( + path.posix.relative(path.posix.dirname(exportFile), chunk.entry), + ); + await fsPromises.writeFile( + path.join(TSC_OUTPUT_DIR, exportFile), + `/** @fileoverview @suppress {undefinedVars} */ + +import * as exports from '${importPath}'; +${NAMESPACE_VARIABLE}['${CHUNK_EXPORT_PROPERTY}'] = exports; +`, + ); + }), + ); +} + const licenseRegex = `\\/\\*\\* \\* @license - \\* (Copyright \\d+ (Google LLC|Massachusetts Institute of Technology)) + \\* (Copyright \\d+ (Google LLC|Massachusetts Institute of Technology|Raspberry Pi Foundation)) ( \\* All rights reserved. )? \\* SPDX-License-Identifier: Apache-2.0 \\*\\/`; @@ -457,8 +515,8 @@ function chunkWrapper(chunk) { }(this, function(${factoryArgs}) { var ${NAMESPACE_VARIABLE}=${namespaceExpr}; %output% -${modulePath(chunk)}.${NAMESPACE_PROPERTY}=${NAMESPACE_VARIABLE}; -return ${modulePath(chunk)}; +${NAMESPACE_VARIABLE}['${CHUNK_EXPORT_PROPERTY}'].${NAMESPACE_PROPERTY}=${NAMESPACE_VARIABLE}; +return ${NAMESPACE_VARIABLE}['${CHUNK_EXPORT_PROPERTY}']; })); `; } @@ -505,6 +563,7 @@ function getChunkOptions() { const files = globs .flatMap((glob) => globSync(glob, {cwd: TSC_OUTPUT_DIR_POSIX})) .map((file) => path.posix.join(TSC_OUTPUT_DIR_POSIX, file)); + files.push(path.posix.join(TSC_OUTPUT_DIR_POSIX, chunkExportPath(chunk))); chunkOptions.push( `${chunk.name}:${files.length}` + (chunk.parent ? `:${chunk.parent.name}` : ''), @@ -519,11 +578,6 @@ function getChunkOptions() { return {chunk: chunkOptions, js: allFiles, chunk_wrapper: chunkWrappers}; } -/** - * RegExp that globally matches path.sep (i.e., "/" or "\"). - */ -const pathSepRegExp = new RegExp(path.sep.replace(/\\/, '\\\\'), 'g'); - /** * Helper method for calling the Closure Compiler, establishing * default options (that can be overridden by the caller). @@ -558,7 +612,8 @@ function compile(options) { * This task compiles the core library, blocks and generators, creating * blockly_compressed.js, blocks_compressed.js, etc. */ -function buildCompiled() { +async function buildCompiled() { + await writeChunkExportFiles(); // Get chunking. const chunkOptions = getChunkOptions(); // Closure Compiler options. @@ -574,19 +629,22 @@ function buildCompiled() { chunk: chunkOptions.chunk, chunk_wrapper: chunkOptions.chunk_wrapper, rename_prefix_namespace: NAMESPACE_VARIABLE, + assume_function_wrapper: true, // 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. }; // Fire up compilation pipline. - return gulp - .src(chunkOptions.js, {base: './'}) - .pipe(stripApacheLicense()) - .pipe(sourcemaps.init()) - .pipe(compile(options)) - .pipe(rename({suffix: COMPILED_SUFFIX})) - .pipe(sourcemaps.write('.')) - .pipe(gulp.dest(RELEASE_DIR)); + await finished( + gulp + .src(chunkOptions.js, {base: './'}) + .pipe(stripApacheLicense()) + .pipe(sourcemaps.init()) + .pipe(compile(options)) + .pipe(rename({suffix: COMPILED_SUFFIX})) + .pipe(sourcemaps.write('.')) + .pipe(gulp.dest(RELEASE_DIR)), + ); } /** From 4afab1d054db21a705e3256c35b241bc68b10090 Mon Sep 17 00:00:00 2001 From: Christopher Allen Date: Fri, 5 Jun 2026 19:20:11 +0100 Subject: [PATCH 3/8] refactor(build): Separate chunk exporter creation from compilation Since the chunk export files are source files to Closure Compiler, separate the creation of the former from the invocation of the latter. Specifically: - Rename writeChunkExportFiles to buildChunkExporters. - Invoked as a separate task in the minify series, instead of calling it directly from buildCompiled. - Revert the changes to buildCompiled that made it an async function just so it could call writeChunkExportFiles. --- .../blockly/scripts/gulpfiles/build_tasks.mjs | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/blockly/scripts/gulpfiles/build_tasks.mjs b/packages/blockly/scripts/gulpfiles/build_tasks.mjs index d6bd5f6a859..be8017782d8 100644 --- a/packages/blockly/scripts/gulpfiles/build_tasks.mjs +++ b/packages/blockly/scripts/gulpfiles/build_tasks.mjs @@ -16,7 +16,6 @@ import sourcemaps from 'gulp-sourcemaps'; import {execSync} from 'child_process'; import * as fs from 'fs'; import * as fsPromises from 'fs/promises'; -import {finished} from 'node:stream/promises'; import * as path from 'path'; import {globSync} from 'glob'; @@ -212,7 +211,7 @@ function chunkExportPath(chunk) { * chunkWrapper); suppress undefined-variable diagnostics here because * Closure Compiler analyses this file separately from the wrapper. */ -async function writeChunkExportFiles() { +async function buildChunkExporters() { const outDir = path.join(TSC_OUTPUT_DIR, CHUNK_EXPORTS_DIR); await fsPromises.mkdir(outDir, {recursive: true}); @@ -612,8 +611,7 @@ function compile(options) { * This task compiles the core library, blocks and generators, creating * blockly_compressed.js, blocks_compressed.js, etc. */ -async function buildCompiled() { - await writeChunkExportFiles(); +function buildCompiled() { // Get chunking. const chunkOptions = getChunkOptions(); // Closure Compiler options. @@ -635,16 +633,14 @@ async function buildCompiled() { }; // Fire up compilation pipline. - await finished( - gulp - .src(chunkOptions.js, {base: './'}) - .pipe(stripApacheLicense()) - .pipe(sourcemaps.init()) - .pipe(compile(options)) - .pipe(rename({suffix: COMPILED_SUFFIX})) - .pipe(sourcemaps.write('.')) - .pipe(gulp.dest(RELEASE_DIR)), - ); + return gulp + .src(chunkOptions.js, {base: './'}) + .pipe(stripApacheLicense()) + .pipe(sourcemaps.init()) + .pipe(compile(options)) + .pipe(rename({suffix: COMPILED_SUFFIX})) + .pipe(sourcemaps.write('.')) + .pipe(gulp.dest(RELEASE_DIR)); } /** @@ -821,8 +817,13 @@ export function cleanBuildDir() { // Main sequence targets. Each should invoke any immediate prerequisite(s). // function cleanBuildDir, above export const langfiles = gulp.parallel(buildLangfiles, buildLangfileShims); -export const minify = gulp.series(tsc, buildCompiled, buildShims); // function tsc, above +export const minify = gulp.series( + tsc, + buildChunkExporters, + buildCompiled, + buildShims, +); export const build = gulp.parallel(minify, langfiles); // Manually-invokable targets, with prerequisites where required. From 52bc27a3f3e79dd516a984bd225e4605c4c82176 Mon Sep 17 00:00:00 2001 From: Christopher Allen Date: Mon, 8 Jun 2026 15:40:08 +0100 Subject: [PATCH 4/8] refactor(build): Use a separate property for each chunk's exports The existing code results in each chunk overwriting the same well-known property ($.__chunkExports__). Since these properties are only expected to be read once, in the same chunk's wrapper's factory function, this isn't strictly wrong - but it made understanding the minified bundles produced by PR #9912 a bit confusing. --- .../blockly/scripts/gulpfiles/build_tasks.mjs | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/blockly/scripts/gulpfiles/build_tasks.mjs b/packages/blockly/scripts/gulpfiles/build_tasks.mjs index be8017782d8..f8be51bdac7 100644 --- a/packages/blockly/scripts/gulpfiles/build_tasks.mjs +++ b/packages/blockly/scripts/gulpfiles/build_tasks.mjs @@ -87,12 +87,12 @@ const NAMESPACE_VARIABLE = '$'; const NAMESPACE_PROPERTY = '__namespace__'; /** - * Property on the shared namespace object where each chunk's export - * object is stored before the UMD wrapper returns it. A string literal - * is used so that Closure Compiler will not rename it when - * assume_function_wrapper is enabled (see issue #5795). + * Prefix for properties that will be used to store each chunk's + * export object on the namespace object. + * + * See buildChunkExporters for additional information. */ -const CHUNK_EXPORT_PROPERTY = '__chunkExport__'; +const CHUNK_EXPORTS_PREFIX = '__chunk_'; /** * A list of chunks. Order matters: later chunks can depend on @@ -226,7 +226,7 @@ async function buildChunkExporters() { `/** @fileoverview @suppress {undefinedVars} */ import * as exports from '${importPath}'; -${NAMESPACE_VARIABLE}['${CHUNK_EXPORT_PROPERTY}'] = exports; +${NAMESPACE_VARIABLE}['${CHUNK_EXPORTS_PREFIX}${chunk.name}'] = exports; `, ); }), @@ -495,6 +495,11 @@ function chunkWrapper(chunk) { ); } + // Expression evaluating to the location on the namespace object at + // which the chunk exporter will have saved the chunk's exports + // object. (See buildChunkExporters.) + const exportsObject = `${NAMESPACE_VARIABLE}.${CHUNK_EXPORTS_PREFIX}${chunk.name}`; + // Note that when loading in a browser the base of the exported path // (e.g. Blockly.blocks.all - see issue #5932) might not exist // before factory has been executed, so calling factory() and @@ -514,8 +519,8 @@ function chunkWrapper(chunk) { }(this, function(${factoryArgs}) { var ${NAMESPACE_VARIABLE}=${namespaceExpr}; %output% -${NAMESPACE_VARIABLE}['${CHUNK_EXPORT_PROPERTY}'].${NAMESPACE_PROPERTY}=${NAMESPACE_VARIABLE}; -return ${NAMESPACE_VARIABLE}['${CHUNK_EXPORT_PROPERTY}']; +${exportsObject}.${NAMESPACE_PROPERTY}=${NAMESPACE_VARIABLE}; +return ${exportsObject}; })); `; } From fa278a84d4cfa8b24ad7f0aa9ab239ef51bbda06 Mon Sep 17 00:00:00 2001 From: Christopher Allen Date: Mon, 8 Jun 2026 16:07:48 +0100 Subject: [PATCH 5/8] cleanup(build): Minor naming improvements --- packages/blockly/scripts/gulpfiles/build_tasks.mjs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/blockly/scripts/gulpfiles/build_tasks.mjs b/packages/blockly/scripts/gulpfiles/build_tasks.mjs index f8be51bdac7..08acadbd4db 100644 --- a/packages/blockly/scripts/gulpfiles/build_tasks.mjs +++ b/packages/blockly/scripts/gulpfiles/build_tasks.mjs @@ -196,7 +196,7 @@ const CHUNK_EXPORTS_DIR = 'chunk_exports'; * @param {{name: string}} chunk * @return {string} */ -function chunkExportPath(chunk) { +function chunkExporterPath(chunk) { return path.posix.join(CHUNK_EXPORTS_DIR, `${chunk.name}_export.js`); } @@ -217,12 +217,12 @@ async function buildChunkExporters() { await Promise.all( chunks.map(async (chunk) => { - const exportFile = chunkExportPath(chunk); + const filename = chunkExporterPath(chunk); const importPath = posixPath( - path.posix.relative(path.posix.dirname(exportFile), chunk.entry), + path.posix.relative(path.posix.dirname(filename), chunk.entry), ); await fsPromises.writeFile( - path.join(TSC_OUTPUT_DIR, exportFile), + path.join(TSC_OUTPUT_DIR, filename), `/** @fileoverview @suppress {undefinedVars} */ import * as exports from '${importPath}'; @@ -567,7 +567,7 @@ function getChunkOptions() { const files = globs .flatMap((glob) => globSync(glob, {cwd: TSC_OUTPUT_DIR_POSIX})) .map((file) => path.posix.join(TSC_OUTPUT_DIR_POSIX, file)); - files.push(path.posix.join(TSC_OUTPUT_DIR_POSIX, chunkExportPath(chunk))); + files.push(path.posix.join(TSC_OUTPUT_DIR_POSIX, chunkExporterPath(chunk))); chunkOptions.push( `${chunk.name}:${files.length}` + (chunk.parent ? `:${chunk.parent.name}` : ''), From e91613d9098fafc304ac22d78a3ca24ad13b54ae Mon Sep 17 00:00:00 2001 From: Christopher Allen Date: Mon, 8 Jun 2026 16:09:20 +0100 Subject: [PATCH 6/8] docs(build): Improve JSDocs + inline comments Note that some comments have been deleted without replacement; these made statements which are no longer true. --- packages/blockly/core/blockly.ts | 2 - .../blockly/scripts/gulpfiles/build_tasks.mjs | 125 ++++++++++++++---- 2 files changed, 97 insertions(+), 30 deletions(-) diff --git a/packages/blockly/core/blockly.ts b/packages/blockly/core/blockly.ts index 6cff67e8b67..44214a007f5 100644 --- a/packages/blockly/core/blockly.ts +++ b/packages/blockly/core/blockly.ts @@ -234,8 +234,6 @@ import {ZoomControls} from './zoom_controls.js'; * This constant is overridden by the build script (npm run build) to the value * of the version in package.json. This is done by the Closure Compiler in the * buildCompressed gulp task. - * For local builds, you can pass --define='Blockly.VERSION=X.Y.Z' to the - * compiler to override this constant. * * @define {string} */ diff --git a/packages/blockly/scripts/gulpfiles/build_tasks.mjs b/packages/blockly/scripts/gulpfiles/build_tasks.mjs index 08acadbd4db..b957faf0f77 100644 --- a/packages/blockly/scripts/gulpfiles/build_tasks.mjs +++ b/packages/blockly/scripts/gulpfiles/build_tasks.mjs @@ -101,7 +101,7 @@ const CHUNK_EXPORTS_PREFIX = '__chunk_'; * * - .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. + * will be written to. Should be a valid identifier. * - .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 @@ -176,23 +176,37 @@ for (let i = 1; i < chunks.length; i++) { } /** - * Return the name of the module object for the entrypoint of the given chunk, - * as munged by Closure Compiler. - */ + * Return the name of the module object for the entrypoint of the + * given chunk, as munged by Closure Compiler. + * + * Note that if either --assume_function_wrapper or + * --compilation_level ADVANCED_OPTIMIZATIONS is used then the name + * will be further munged in a later compliation step to replace it + * with an arbitrary, very short name. + * + * Nevertheless, this function can still be used to compute the + * location of @defined variables, because --define directives are + * processed before the final renaming occurs. + */ function modulePath(chunk) { const entryPath = path.posix.join(TSC_OUTPUT_DIR_POSIX, chunk.entry); return 'module$' + entryPath.replace(/\.js$/, '').replaceAll('/', '$'); } /** - * Directory (relative to TSC_OUTPUT_DIR) where generated chunk export - * collector files are written. + * Directory (relative to TSC_OUTPUT_DIR) where generated chunk + * exporters are written. + * + * See buildChunkExporters for additional information. */ const CHUNK_EXPORTS_DIR = 'chunk_exports'; /** - * Return the path to the generated chunk export collector file for - * the given chunk, relative to TSC_OUTPUT_DIR. + * Return the path to the generated chunk exporter for the given + * chunk, relative to TSC_OUTPUT_DIR. + * + * See buildChunkExporters for additional information. + * * @param {{name: string}} chunk * @return {string} */ @@ -201,15 +215,61 @@ function chunkExporterPath(chunk) { } /** - * Write generated chunk export collector files, one per chunk. Each - * file imports the chunk's entrypoint module and saves its exports on - * to the shared namespace object using a string-literal property name - * so that the UMD wrapper can return them after compilation with - * assume_function_wrapper enabled. - * - * The namespace object is declared by the chunk wrapper (see - * chunkWrapper); suppress undefined-variable diagnostics here because - * Closure Compiler analyses this file separately from the wrapper. + * This task generates the chunk exporters, one per chunk, which are + * source files included in the input to Closure Compiler to help the + * chunk wrappers locate the chunk's exports (module) object. + * + * Normally, when using --compilation_level SIMPLE_OPTIMIZATIONS and + * --chunk_output_type GLOBAL_NAMESPACE (the defaults), Closure + * Compiler will give each chunk's top-level exports (module) object a + * name of the form + * + * module$build$src$...$filename + * + * which can be computed by modulePath(chunk), thereby making it easy + * to locate the object that should be returned by the chunk wrapper's + * factory function. + * + * Unfortunately, if using using --assume_function_wrapper option (or + * --compilation_level ADVANCED_OPTIMIZATIONS), this variable is + * renamed in a later stage of the compiler to instead have an + * arbitrarily-chosen name of minimal length. + * + * To work around this, we create an extra source module for each + * chunk that imports the chunk's entrypoint and saves the resulting + * exports (module) object to a well-known location: a property on the + * shared namespace object. + * + * In order to prevent the name of that property from itself being + * renamed, well-known location, it is written using a computed member + * expression with a string literal property name (i.e., a['b'] rather + * than a.b). E.g., the the generated chunck exporter source file + * might contain + * + * import * as exports from '../core/blockly.js'; + * $['__chunk_blockly'] = exports; + * + * which would get compiled to something like: + * + * $.__chunk_blockly=R; + * + * The chunk wrapper's factory function can then retrieve + * and return $.__chunk_blockly. + * + * N.B.: Although Closure Compiler will not rename the literal + * property name, it will convet the computed member expression into a + * non-computed one (as shown in the example above) if it is a valid + * unquoted property name. In order to ensure that the compiled, + * wrapped chunk is itself valid input for Closure Compiler, it is + * necessary that the chunk wrapper use the same form to access it. + * Specifically, for this example the chunk wrapper factory function + * should + * + * return $.__chunk_blockly; + * + * rather than + * + * return $['__chunk_blockly']; */ async function buildChunkExporters() { const outDir = path.join(TSC_OUTPUT_DIR, CHUNK_EXPORTS_DIR); @@ -223,6 +283,9 @@ async function buildChunkExporters() { ); await fsPromises.writeFile( path.join(TSC_OUTPUT_DIR, filename), + // Suppress undefined-variable diagnostics, since Closure + // Compiler can't see the declaration of NAMESPACE_VARIABLE + // while compiling the chunk exporters. `/** @fileoverview @suppress {undefinedVars} */ import * as exports from '${importPath}'; @@ -500,11 +563,7 @@ function chunkWrapper(chunk) { // object. (See buildChunkExporters.) const exportsObject = `${NAMESPACE_VARIABLE}.${CHUNK_EXPORTS_PREFIX}${chunk.name}`; - // Note that when loading in a browser the base of the exported path - // (e.g. Blockly.blocks.all - see issue #5932) might not exist - // before factory has been executed, so calling factory() and - // assigning the result are done in separate statements to ensure - // they are sequenced correctly. + // Generate wrapper. return `// Do not edit this file; automatically generated. /* eslint-disable */ @@ -553,6 +612,13 @@ return ${exportsObject}; * to the Closure Compiler node API, and be compatible with that * emitted by closure-calculate-chunks. * + * N.B.: items in the .chunk array are of the form: + * + * "::" + * + * See https://github.com/google/closure-compiler/wiki/Flags-and-Options#code-splitting + * for more information. + * * @return {{chunk: !Array, * js: !Array, * chunk_wrapper: !Array}} @@ -626,15 +692,18 @@ function buildCompiled() { // non-global, but the Closure Compiler turns everything in to a // global - you just have to know what the new name is! With // declareLegacyNamespace this was very straightforward. Without - // it, we have to rely on implmentation details. See + // it, we have to rely on implmentation details. (Note that + // although --assume_function_wrapper will result in the global + // being renamed to something short, that won't have happened + // by the time --define is processed.) See // https://github.com/google/closure-compiler/issues/1601#issuecomment-483452226 define: `VERSION$$${modulePath(chunks[0])}='${packageJson.version}'`, chunk: chunkOptions.chunk, chunk_wrapper: chunkOptions.chunk_wrapper, - rename_prefix_namespace: NAMESPACE_VARIABLE, - assume_function_wrapper: true, // 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. + rename_prefix_namespace: NAMESPACE_VARIABLE, + assume_function_wrapper: true, }; // Fire up compilation pipline. @@ -770,12 +839,12 @@ ${exportedNames.map((name) => ` ${name},`).join('\n')} } /** - * This task uses Closure Compiler's ADVANCED_COMPILATION mode to + * This task uses Closure Compiler's ADVANCED_OPTIMIZATIONS mode to * compile together Blockly core, blocks and generators with a simple * test app; the purpose is to verify that Blockly is compatible with - * the ADVANCED_COMPILATION mode. + * the ADVANCED_OPTIMIZATIONS mode. * - * Prerequisite: buildJavaScript. + * Prerequisite: tsc. */ function compileAdvancedCompilationTest() { // If main_compressed.js exists (from a previous run) delete it so that From c916c8f1ac6195fe0a13b48f1edd3a93921daf86 Mon Sep 17 00:00:00 2001 From: Christopher Allen Date: Mon, 8 Jun 2026 16:16:29 +0100 Subject: [PATCH 7/8] cleanup(build): Reorder new chunk-exporters-related code Reorder the new code that generates the chunk exporters, to put it together with (but before) the code that generates the chunk wrappers, since the two are closely coupled. --- .../blockly/scripts/gulpfiles/build_tasks.mjs | 206 +++++++++--------- 1 file changed, 103 insertions(+), 103 deletions(-) diff --git a/packages/blockly/scripts/gulpfiles/build_tasks.mjs b/packages/blockly/scripts/gulpfiles/build_tasks.mjs index b957faf0f77..8ab4a223f1a 100644 --- a/packages/blockly/scripts/gulpfiles/build_tasks.mjs +++ b/packages/blockly/scripts/gulpfiles/build_tasks.mjs @@ -86,6 +86,14 @@ const NAMESPACE_VARIABLE = '$'; */ const NAMESPACE_PROPERTY = '__namespace__'; +/** + * Directory (relative to TSC_OUTPUT_DIR) where generated chunk + * exporters are written. + * + * See buildChunkExporters for additional information. + */ +const CHUNK_EXPORTERS_DIR = 'chunk_exports'; + /** * Prefix for properties that will be used to store each chunk's * export object on the namespace object. @@ -193,109 +201,6 @@ function modulePath(chunk) { return 'module$' + entryPath.replace(/\.js$/, '').replaceAll('/', '$'); } -/** - * Directory (relative to TSC_OUTPUT_DIR) where generated chunk - * exporters are written. - * - * See buildChunkExporters for additional information. - */ -const CHUNK_EXPORTS_DIR = 'chunk_exports'; - -/** - * Return the path to the generated chunk exporter for the given - * chunk, relative to TSC_OUTPUT_DIR. - * - * See buildChunkExporters for additional information. - * - * @param {{name: string}} chunk - * @return {string} - */ -function chunkExporterPath(chunk) { - return path.posix.join(CHUNK_EXPORTS_DIR, `${chunk.name}_export.js`); -} - -/** - * This task generates the chunk exporters, one per chunk, which are - * source files included in the input to Closure Compiler to help the - * chunk wrappers locate the chunk's exports (module) object. - * - * Normally, when using --compilation_level SIMPLE_OPTIMIZATIONS and - * --chunk_output_type GLOBAL_NAMESPACE (the defaults), Closure - * Compiler will give each chunk's top-level exports (module) object a - * name of the form - * - * module$build$src$...$filename - * - * which can be computed by modulePath(chunk), thereby making it easy - * to locate the object that should be returned by the chunk wrapper's - * factory function. - * - * Unfortunately, if using using --assume_function_wrapper option (or - * --compilation_level ADVANCED_OPTIMIZATIONS), this variable is - * renamed in a later stage of the compiler to instead have an - * arbitrarily-chosen name of minimal length. - * - * To work around this, we create an extra source module for each - * chunk that imports the chunk's entrypoint and saves the resulting - * exports (module) object to a well-known location: a property on the - * shared namespace object. - * - * In order to prevent the name of that property from itself being - * renamed, well-known location, it is written using a computed member - * expression with a string literal property name (i.e., a['b'] rather - * than a.b). E.g., the the generated chunck exporter source file - * might contain - * - * import * as exports from '../core/blockly.js'; - * $['__chunk_blockly'] = exports; - * - * which would get compiled to something like: - * - * $.__chunk_blockly=R; - * - * The chunk wrapper's factory function can then retrieve - * and return $.__chunk_blockly. - * - * N.B.: Although Closure Compiler will not rename the literal - * property name, it will convet the computed member expression into a - * non-computed one (as shown in the example above) if it is a valid - * unquoted property name. In order to ensure that the compiled, - * wrapped chunk is itself valid input for Closure Compiler, it is - * necessary that the chunk wrapper use the same form to access it. - * Specifically, for this example the chunk wrapper factory function - * should - * - * return $.__chunk_blockly; - * - * rather than - * - * return $['__chunk_blockly']; - */ -async function buildChunkExporters() { - const outDir = path.join(TSC_OUTPUT_DIR, CHUNK_EXPORTS_DIR); - await fsPromises.mkdir(outDir, {recursive: true}); - - await Promise.all( - chunks.map(async (chunk) => { - const filename = chunkExporterPath(chunk); - const importPath = posixPath( - path.posix.relative(path.posix.dirname(filename), chunk.entry), - ); - await fsPromises.writeFile( - path.join(TSC_OUTPUT_DIR, filename), - // Suppress undefined-variable diagnostics, since Closure - // Compiler can't see the declaration of NAMESPACE_VARIABLE - // while compiling the chunk exporters. - `/** @fileoverview @suppress {undefinedVars} */ - -import * as exports from '${importPath}'; -${NAMESPACE_VARIABLE}['${CHUNK_EXPORTS_PREFIX}${chunk.name}'] = exports; -`, - ); - }), - ); -} - const licenseRegex = `\\/\\*\\* \\* @license \\* (Copyright \\d+ (Google LLC|Massachusetts Institute of Technology|Raspberry Pi Foundation)) @@ -514,6 +419,101 @@ function buildLangfiles(done) { done(); } +/** + * Return the path to the generated chunk exporter for the given + * chunk, relative to TSC_OUTPUT_DIR. + * + * See buildChunkExporters for additional information. + * + * @param {{name: string}} chunk + * @return {string} + */ +function chunkExporterPath(chunk) { + return path.posix.join(CHUNK_EXPORTERS_DIR, `${chunk.name}_export.js`); +} + +/** + * This task generates the chunk exporters, one per chunk, which are + * source files included in the input to Closure Compiler to help the + * chunk wrappers locate the chunk's exports (module) object. + * + * Normally, when using --compilation_level SIMPLE_OPTIMIZATIONS and + * --chunk_output_type GLOBAL_NAMESPACE (the defaults), Closure + * Compiler will give each chunk's top-level exports (module) object a + * name of the form + * + * module$build$src$...$filename + * + * which can be computed by modulePath(chunk), thereby making it easy + * to locate the object that should be returned by the chunk wrapper's + * factory function. + * + * Unfortunately, if using using --assume_function_wrapper option (or + * --compilation_level ADVANCED_OPTIMIZATIONS), this variable is + * renamed in a later stage of the compiler to instead have an + * arbitrarily-chosen name of minimal length. + * + * To work around this, we create an extra source module for each + * chunk that imports the chunk's entrypoint and saves the resulting + * exports (module) object to a well-known location: a property on the + * shared namespace object. + * + * In order to prevent the name of that property from itself being + * renamed, well-known location, it is written using a computed member + * expression with a string literal property name (i.e., a['b'] rather + * than a.b). E.g., the the generated chunck exporter source file + * might contain + * + * import * as exports from '../core/blockly.js'; + * $['__chunk_blockly'] = exports; + * + * which would get compiled to something like: + * + * $.__chunk_blockly=R; + * + * The chunk wrapper's factory function can then retrieve + * and return $.__chunk_blockly. + * + * N.B.: Although Closure Compiler will not rename the literal + * property name, it will convet the computed member expression into a + * non-computed one (as shown in the example above) if it is a valid + * unquoted property name. In order to ensure that the compiled, + * wrapped chunk is itself valid input for Closure Compiler, it is + * necessary that the chunk wrapper use the same form to access it. + * Specifically, for this example the chunk wrapper factory function + * should + * + * return $.__chunk_blockly; + * + * rather than + * + * return $['__chunk_blockly']; + */ +async function buildChunkExporters() { + const outDir = path.join(TSC_OUTPUT_DIR, CHUNK_EXPORTERS_DIR); + await fsPromises.mkdir(outDir, {recursive: true}); + + await Promise.all( + chunks.map(async (chunk) => { + const filename = chunkExporterPath(chunk); + const importPath = posixPath( + path.posix.relative(path.posix.dirname(filename), chunk.entry), + ); + await fsPromises.writeFile( + path.join(TSC_OUTPUT_DIR, filename), + // Suppress undefined-variable diagnostics, since Closure + // Compiler can't see the declaration of NAMESPACE_VARIABLE + // while compiling the chunk exporters. + `/** @fileoverview @suppress {undefinedVars} */ + +import * as exports from '${importPath}'; +${NAMESPACE_VARIABLE}['${CHUNK_EXPORTS_PREFIX}${chunk.name}'] = exports; +`, + ); + }), + ); +} + /** * A helper method to return an Closure Compiler chunk wrapper that * wraps the compiler output for the given chunk in a Universal Module From 0ac56616a561ab53e59d296148a05c7967f51a34 Mon Sep 17 00:00:00 2001 From: Christopher Allen Date: Mon, 8 Jun 2026 17:10:59 +0100 Subject: [PATCH 8/8] fix(build): Rename chunk exporter's dir + filenames to "exporters" For consistency with code and docs, call the files that contain code which retrieves the chunks' export objects "chunk exporters", since "chunk exports" better describes the objects being exported. --- packages/blockly/scripts/gulpfiles/build_tasks.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/blockly/scripts/gulpfiles/build_tasks.mjs b/packages/blockly/scripts/gulpfiles/build_tasks.mjs index 8ab4a223f1a..5f141f5bf09 100644 --- a/packages/blockly/scripts/gulpfiles/build_tasks.mjs +++ b/packages/blockly/scripts/gulpfiles/build_tasks.mjs @@ -92,7 +92,7 @@ const NAMESPACE_PROPERTY = '__namespace__'; * * See buildChunkExporters for additional information. */ -const CHUNK_EXPORTERS_DIR = 'chunk_exports'; +const CHUNK_EXPORTERS_DIR = 'chunk_exporters'; /** * Prefix for properties that will be used to store each chunk's @@ -429,7 +429,7 @@ function buildLangfiles(done) { * @return {string} */ function chunkExporterPath(chunk) { - return path.posix.join(CHUNK_EXPORTERS_DIR, `${chunk.name}_export.js`); + return path.posix.join(CHUNK_EXPORTERS_DIR, `${chunk.name}_exporter.js`); } /**