diff --git a/package.json b/package.json index cf338bfc098..82f701dea5f 100644 --- a/package.json +++ b/package.json @@ -57,40 +57,49 @@ "exports": { ".": { "types": "./index.d.ts", + "import": "./index.mjs", "umd": "./blockly.min.js", "default": "./index.js" }, "./core": { "types": "./core.d.ts", "node": "./core-node.js", + "import": "./blockly.mjs", "default": "./blockly_compressed.js" }, "./blocks": { "types": "./blocks.d.ts", + "import": "./blocks.mjs", "default": "./blocks_compressed.js" }, "./dart": { "types": "./dart.d.ts", + "import": "./dart.mjs", "default": "./dart_compressed.js" }, "./lua": { "types": "./lua.d.ts", + "import": "./lua.mjs", "default": "./lua_compressed.js" }, "./javascript": { "types": "./javascript.d.ts", + "import": "./javascript.mjs", "default": "./javascript_compressed.js" }, "./php": { "types": "./php.d.ts", + "import": "./php.mjs", "default": "./php_compressed.js" }, "./python": { "types": "./python.d.ts", + "import": "./python.mjs", "default": "./python_compressed.js" }, "./msg/*": { "types": "./msg/*.d.ts", + "import": "./msg/*.mjs", "default": "./msg/*.js" } }, diff --git a/scripts/gulpfiles/build_tasks.js b/scripts/gulpfiles/build_tasks.js index 0bdbb90f0f8..d7b118f325c 100644 --- a/scripts/gulpfiles/build_tasks.js +++ b/scripts/gulpfiles/build_tasks.js @@ -344,6 +344,24 @@ this removal! done(); } +var languages = null; + +/** + * Get list of languages to build langfiles and/or shims for, based on .json + * files in msg/json/, skipping certain entries that do not correspond to an + * actual language). Results are cached as this is called from both + * buildLangfiles and buildLangfileShims. + */ +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$/, '')); + } + return languages; +} + /** * This task builds Blockly's lang files. * msg/*.js @@ -353,10 +371,8 @@ function buildLangfiles(done) { fs.mkdirSync(LANG_BUILD_DIR, {recursive: true}); // Run create_messages.py. - let json_files = fs.readdirSync(path.join('msg', 'json')); - json_files = json_files.filter(file => file.endsWith('json') && - !(new RegExp(/(keys|synonyms|qqq|constants)\.json$/).test(file))); - json_files = json_files.map(file => path.join('msg', 'json', file)); + 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')} \ @@ -364,7 +380,7 @@ function buildLangfiles(done) { --source_constants_file ${path.join('msg', 'json', 'constants.json')} \ --key_file ${path.join('msg', 'json', 'keys.json')} \ --output_dir ${LANG_BUILD_DIR} \ - --quiet ${json_files.join(' ')}`; + --quiet ${inputFiles.join(' ')}`; execSync(createMessagesCmd, {stdio: 'inherit'}); done(); @@ -565,7 +581,10 @@ function buildCompiled() { } /** - * This task builds the shims used by the playgrounds and tests to + * This task builds the ESM wrappers used by the chunk "import" + * entrypoints declared in package.json. + * + * Also 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. @@ -579,11 +598,38 @@ async function buildShims() { const TMP_PACKAGE_JSON = path.join(BUILD_DIR, 'package.json'); await fsPromises.writeFile(TMP_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) => { + // 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(/.*\./, ''); + + 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`), + `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`); @@ -591,14 +637,13 @@ async function buildShims() { chunk.parent ? `import ${quote(`./${chunk.parent.name}.loader.mjs`)};` : ''; - const exports = await import(`../../${entryPath}`); await fsPromises.writeFile(shimPath, `import {loadChunk} from '../tests/scripts/load.mjs'; ${parentImport} export const { -${Object.keys(exports).map((name) => ` ${name},`).join('\n')} +${exportedNames.map((name) => ` ${name},`).join('\n')} } = await loadChunk( ${quote(entryPath)}, ${quote(scriptPath)}, @@ -610,7 +655,37 @@ ${Object.keys(exports).map((name) => ` ${name},`).join('\n')} await fsPromises.rm(TMP_PACKAGE_JSON); } - +/** + * This task builds the ESM wrappers used by the langfiles "import" + * entrypoints declared in package.json. + */ +async function buildLangfileShims() { + // Create output directory. + fs.mkdirSync(path.join(RELEASE_DIR, 'msg'), {recursive: true}); + + // Get the names of the exports from the langfile by require()ing + // msg/messages.js and letting it mutate the (global) Blockly.Msg. + // (We have to do it this way because messages.js is a script and + // not a CJS module with exports.) + globalThis.Blockly = {Msg: {}}; + require('../../msg/messages.js'); + 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`); + + await fsPromises.writeFile(wrapperPath, + `import ${lang} from '${cjsPath}'; +export const { +${exportedNames.map((name) => ` ${name},`).join('\n')} +} = ${lang}; +`); + })); +} /** * This task builds Blockly core, blocks and generators together and uses @@ -662,7 +737,7 @@ function cleanBuildDir() { // Main sequence targets. Each should invoke any immediate prerequisite(s). exports.cleanBuildDir = cleanBuildDir; -exports.langfiles = buildLangfiles; // Build build/msg/*.js from msg/json/*. +exports.langfiles = gulp.parallel(buildLangfiles, buildLangfileShims); exports.tsc = buildJavaScript; exports.minify = gulp.series(exports.tsc, buildCompiled, buildShims); exports.build = gulp.parallel(exports.minify, exports.langfiles); diff --git a/typings/msg/yue.d.ts b/typings/msg/yue.d.ts deleted file mode 100644 index 5f2d13710bd..00000000000 --- a/typings/msg/yue.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * @license - * Copyright 2022 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -export * from './msg'; -