diff --git a/closure/goog/goog.js b/closure/goog/goog.js index 6e81b6987..f044ececb 100644 --- a/closure/goog/goog.js +++ b/closure/goog/goog.js @@ -72,9 +72,9 @@ export const global = globalThis; // export const scope = goog.scope; // export const defineClass = goog.defineClass; export const declareModuleId = function(namespace) { - if (window.goog && window.goog.declareModuleId) { - window.goog.declareModuleId.call(this, namespace); - } + // Use globalThis instead of window to find goog, so this can be + // imported in node.js (e.g. when running buildShims gulp task). + globalThis?.goog?.declareModuleId.call(this, namespace); }; diff --git a/scripts/gulpfiles/build_tasks.js b/scripts/gulpfiles/build_tasks.js index 4479c0be7..ed81e7cc1 100644 --- a/scripts/gulpfiles/build_tasks.js +++ b/scripts/gulpfiles/build_tasks.js @@ -15,6 +15,7 @@ gulp.sourcemaps = require('gulp-sourcemaps'); const path = require('path'); const fs = require('fs'); +const fsPromises = require('fs/promises'); const {exec, execSync} = require('child_process'); const closureCompiler = require('google-closure-compiler').gulp(); @@ -24,7 +25,7 @@ const {rimraf} = require('rimraf'); const {BUILD_DIR, DEPS_FILE, RELEASE_DIR, TEST_DEPS_FILE, TSC_OUTPUT_DIR, TYPINGS_BUILD_DIR} = require('./config'); const {getPackageJson} = require('./helper_tasks'); -const {posixPath} = require('../helpers'); +const {posixPath, quote} = require('../helpers'); //////////////////////////////////////////////////////////// // Build // @@ -106,6 +107,7 @@ const chunks = [ { name: 'blockly', entry: path.join(TSC_OUTPUT_DIR, 'core', 'main.js'), + moduleEntry: path.join(TSC_OUTPUT_DIR, 'core', 'blockly.js'), exports: 'module$build$src$core$blockly', scriptExport: 'Blockly', }, @@ -672,6 +674,57 @@ function buildCompiled() { .pipe(gulp.dest(RELEASE_DIR)); } +/** + * This task builds the shims used by the playgrounds and tests to + * load Blockly in either compressed or uncompressed mode, creating + * build/blockly.loader.mjs, blocks.loader.mjs, javascript.loader.mjs, + * etc. + * + * Prerequisite: getChunkOptions (via buildCompiled, for chunks[].parent). + */ +async function buildShims() { + // Install a package.json file in BUILD_DIR to tell node.js that the + // .js files therein are ESM not CJS, so we can import the + // entrypoints to enumerate their exported names. + // + // N.B.: There is an exception: core/main.js is a goog.module not + // ESM, but fortunately we don't attempt to import or require this + // file from node.js - we only feed it to Closure Compiler, which + // uses the type information in deps.js rather than package.json. + await fsPromises.writeFile( + path.join(BUILD_DIR, 'package.json'), + '{"type": "module"}' + ); + + // Import each entrypoint module, enumerate its exports, and write + // a shim to load the chunk either by importing the entrypoint + // module or by loading the compiled script. + await Promise.all(chunks.map(async (chunk) => { + const modulePath = posixPath(chunk.moduleEntry ?? chunk.entry); + 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}.mjs`)};` : ''; + const exports = await import(`../../${modulePath}`); + + await fsPromises.writeFile(shimPath, + `import {loadChunk} from '../tests/scripts/load.mjs'; +${parentImport} + +export const { +${Object.keys(exports).map((name) => ` ${name},`).join('\n')} +} = await loadChunk( + ${quote(modulePath)}, + ${quote(scriptPath)}, + ${quote(chunk.scriptExport)}, +); +`); + })); +} + + + /** * This task builds Blockly core, blocks and generators together and uses * Closure Compiler's ADVANCED_COMPILATION mode. @@ -728,7 +781,7 @@ exports.cleanBuildDir = cleanBuildDir; exports.langfiles = buildLangfiles; // Build build/msg/*.js from msg/json/*. exports.tsc = buildJavaScript; exports.deps = gulp.series(exports.tsc, buildDeps); -exports.minify = gulp.series(exports.deps, buildCompiled); +exports.minify = gulp.series(exports.deps, buildCompiled, buildShims); exports.build = gulp.parallel(exports.minify, exports.langfiles); // Manually-invokable targets, with prerequisites where required. diff --git a/scripts/helpers.js b/scripts/helpers.js index 8dd013835..8150593dd 100644 --- a/scripts/helpers.js +++ b/scripts/helpers.js @@ -11,25 +11,64 @@ const path = require('path'); -/** - * Escape regular expression pattern - * @param {string} pattern regular expression pattern - * @return {string} escaped regular expression pattern - */ -function escapeRegex(pattern) { - return pattern.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); -} - /** * Replaces OS-specific path with POSIX style path. + * Simplified implementation based on + * https://stackoverflow.com/a/63251716/4969945 + * * @param {string} target target path * @return {string} posix path */ function posixPath(target) { - const osSpecificSep = new RegExp(escapeRegex(path.sep), 'g'); - return target.replace(osSpecificSep, path.posix.sep); + return target.split(path.sep).join(path.posix.sep); +} + +/** + * Convert a string into a string literal. Strictly speaking we + * only need to escape backslash, \r, \n, \u2028 (line separator), + * \u2029 (paragraph separator) and whichever quote character we're + * using, but for simplicity we escape all the control characters. + * + * Based on https://github.com/google/CodeCity/blob/master/server/code.js + * + * @param {string} str The string to convert. + * @return {string} The value s as a eval-able string literal. + */ +function quote(str) { + /* eslint-disable no-control-regex, no-multi-spaces */ + /** Regexp for characters to be escaped in a single-quoted string. */ + const singleRE = /[\x00-\x1f\\\u2028\u2029']/g; + + /** Map of control character replacements. */ + const replacements = { + '\x00': '\\0', + '\x01': '\\x01', + '\x02': '\\x02', + '\x03': '\\x03', + '\x04': '\\x04', + '\x05': '\\x05', + '\x06': '\\x06', + '\x07': '\\x07', + '\x08': '\\b', + '\x09': '\\t', + '\x0a': '\\n', + '\x0b': '\\v', + '\x0c': '\\f', + '\x0d': '\\r', + '\x0e': '\\x0e', + '\x0f': '\\x0f', + '"': '\\"', + "'": "\\'", + '\\': '\\\\', + '\u2028': '\\u2028', + '\u2029': '\\u2029', + }; + /* eslint-enable no-control-regex, no-multi-spaces */ + + return "'" + str.replace(singleRE, (c) => replacements[c]) + "'"; } module.exports = { posixPath, + quote, }; diff --git a/tests/multi_playground.html b/tests/multi_playground.html index 0c235d6a3..f929de8d8 100644 --- a/tests/multi_playground.html +++ b/tests/multi_playground.html @@ -4,14 +4,13 @@