diff --git a/src/closure-externs/closure-externs.js b/src/closure-externs/closure-externs.js index be55c75d58154..8199fe8460569 100644 --- a/src/closure-externs/closure-externs.js +++ b/src/closure-externs/closure-externs.js @@ -11,8 +11,7 @@ * The closure_compiler() method in tools/shared.py refers to this file when calling closure. */ -// Special placeholder for `import.meta` and `await import`. -var EMSCRIPTEN$IMPORT$META; +// Special placeholder for `await import` and `await`. var EMSCRIPTEN$AWAIT$IMPORT; var EMSCRIPTEN$AWAIT; @@ -188,13 +187,6 @@ var devicePixelRatio; */ var id; -/** - * Used in MODULARIZE mode as the name of the incoming module argument. - * This is generated outside of the code we pass to closure so from closure's - * POV this is "extern". - */ -var moduleArg; - /** * This was removed from upstream closure compiler in * https://github.com/google/closure-compiler/commit/f83322c1b. diff --git a/src/closure-externs/modularize-externs.js b/src/closure-externs/modularize-externs.js index e9d8a58598998..c0471f5ef49a4 100644 --- a/src/closure-externs/modularize-externs.js +++ b/src/closure-externs/modularize-externs.js @@ -1,12 +1,18 @@ -// In MODULARIZE mode the JS code may be executed later, after `document.currentScript` is gone, so we store -// it to `_scriptName` outside the wrapper function. Therefore, it cannot be minified. -// In EXPORT_ES6 mode we use `import.meta.url` and for Node.js CommonJS builds we use `__filename`. - /** + * In MODULARIZE mode the JS code may be executed later, after `document.currentScript` is gone, so + * we store it to `_scriptName` outside the wrapper function. Therefore, it cannot be minified. + * In EXPORT_ES6 mode we use `import.meta.url` and for Node.js CommonJS builds we use `__filename`. * @suppress {duplicate, undefinedVars} */ var _scriptName; +/** + * Used in MODULARIZE mode as the name of the incoming module argument. + * This is generated outside of the code we pass to closure so from closure's + * POV this is "extern". + */ +var moduleArg; + /** * @suppress {duplicate, undefinedVars} */ diff --git a/src/jsifier.mjs b/src/jsifier.mjs index c8bf14834dd95..84dc251827b75 100644 --- a/src/jsifier.mjs +++ b/src/jsifier.mjs @@ -397,6 +397,12 @@ export async function runJSify(outputFile, symbolsOnly) { } async function writeOutput(str) { + // Unmangle previously mangled `import.meta` references. + // See also: `mangleUnsupportedSyntax` in parseTools.mjs. + if (EXPORT_ES6) { + str = str.replaceAll('EMSCRIPTEN$IMPORT$META', 'import.meta'); + } + await outputHandle.write(str + '\n'); } diff --git a/src/modularize.js b/src/modularize.js index 5abbeac466c11..58a3eb6783b49 100644 --- a/src/modularize.js +++ b/src/modularize.js @@ -29,6 +29,7 @@ var {{{ EXPORT_NAME }}} = (() => { var _scriptName = globalThis.document?.currentScript?.src; #endif return async function(moduleArg = {}) { + var Module = moduleArg; "<<< INNER_JS_CODE >>>" return Module; @@ -38,6 +39,7 @@ var {{{ EXPORT_NAME }}} = (() => { // When targeting node and ES6 we use `await import ..` in the generated code // so the outer function needs to be marked as async. async function {{{ EXPORT_NAME }}}(moduleArg = {}) { + var Module = moduleArg; "<<< INNER_JS_CODE >>>" return Module; diff --git a/src/parseTools.mjs b/src/parseTools.mjs index ba24c30f07ab9..61e4d20565de1 100644 --- a/src/parseTools.mjs +++ b/src/parseTools.mjs @@ -36,21 +36,24 @@ function mangleUnsupportedSyntax(text) { // Do special keyword replacement after macro processing, so that // macros can generate keywords (easier to read preprocessed code). if (EXPORT_ES6) { - // `eval`, Terser and Closure don't support module syntax; to allow it, - // we need to temporarily replace `import.meta` and `await import` usages - // with placeholders during preprocess phase, and back after all the other ops. - // See also: `phase_final_emitting` in emcc.py. - text = text - .replace(/\bimport\.meta\b/g, 'EMSCRIPTEN$IMPORT$META') - .replace(/\bawait import\b/g, 'EMSCRIPTEN$AWAIT$IMPORT'); - } - if (MODULARIZE) { - // Same for our use of "top-level-await" which is not actually top level - // in the case of MODULARIZE. - text = text.replace(/\bawait createWasm\(\)/g, 'EMSCRIPTEN$AWAIT(createWasm())'); - text = text.replace(/\bawait run\(\)/g, 'EMSCRIPTEN$AWAIT(run())'); - text = text.replace(/\bawait instantiatePromise\b/g, 'EMSCRIPTEN$AWAIT(instantiatePromise)'); - text = text.replace(/\bawait init\(\)/g, 'EMSCRIPTEN$AWAIT(init())'); + // `vm.runInContext` doesn't support module syntax; to allow it, we need to + // temporarily replace `import.meta` usages with placeholders. + // See also: `writeOutput` in jsifier.mjs. + text = text.replaceAll('import.meta', 'EMSCRIPTEN$IMPORT$META'); + } + if (MODULARIZE && USE_CLOSURE_COMPILER) { + // Closure doesn't support "top-level await" which is not actually the top + // level in case of MODULARIZE. Temporarily replace `await` usages with + // placeholders during preprocess phase, and back after all the other ops. + // See also: `fix_js_mangling` in link.py. + // FIXME: Remove after https://github.com/google/closure-compiler/issues/3835 is fixed. + if (EXPORT_ES6) { + text = text.replaceAll('await import', 'EMSCRIPTEN$AWAIT$IMPORT'); + } + text = text.replaceAll('await createWasm()', 'EMSCRIPTEN$AWAIT(createWasm())'); + text = text.replaceAll('await run()', 'EMSCRIPTEN$AWAIT(run())'); + text = text.replaceAll('await instantiatePromise', 'EMSCRIPTEN$AWAIT(instantiatePromise)'); + text = text.replaceAll('await init()', 'EMSCRIPTEN$AWAIT(init())'); } return text; } diff --git a/src/shell.js b/src/shell.js index 1b31a3065acc4..ad4fd53554e63 100644 --- a/src/shell.js +++ b/src/shell.js @@ -25,8 +25,6 @@ #if MODULARIZE #if MODULARIZE == 'instance' var Module = {}; -#else -var Module = moduleArg; #endif #elif USE_CLOSURE_COMPILER /** @type{Object} */ @@ -130,7 +128,7 @@ if (ENVIRONMENT_IS_NODE) { #endif #endif // PTHREADS || WASM_WORKERS } -#endif // ENVIRONMENT_MAY_BE_NODE +#endif // ENVIRONMENT_MAY_BE_NODE && (EXPORT_ES6 || PTHREADS || WASM_WORKERS) // --pre-jses are emitted after the Module integration code, so that they can // refer to Module (if they choose; they can also define Module) diff --git a/src/shell_minimal.js b/src/shell_minimal.js index 8ddc5c2d8a70a..9615ea8e7a651 100644 --- a/src/shell_minimal.js +++ b/src/shell_minimal.js @@ -6,9 +6,8 @@ #include "minimum_runtime_check.js" -#if MODULARIZE -var Module = moduleArg; -#elif USE_CLOSURE_COMPILER +#if !MODULARIZE +#if USE_CLOSURE_COMPILER /** @type{Object} */ var Module; // if (!Module) is crucial for Closure Compiler here as it will @@ -30,6 +29,7 @@ var Module = globalThis.{{{ EXPORT_NAME }}} || {}; #else var Module = {{{ EXPORT_NAME }}}; #endif +#endif // !MODULARIZE #if ENVIRONMENT_MAY_BE_NODE var ENVIRONMENT_IS_NODE = {{{ nodeDetectionCode() }}}; diff --git a/test/codesize/test_codesize_minimal_esm.json b/test/codesize/test_codesize_minimal_esm.json index 817e871ca770c..374bdf1acb564 100644 --- a/test/codesize/test_codesize_minimal_esm.json +++ b/test/codesize/test_codesize_minimal_esm.json @@ -1,10 +1,10 @@ { - "a.out.js": 2433, - "a.out.js.gz": 1138, + "a.out.js": 2368, + "a.out.js.gz": 1122, "a.out.nodebug.wasm": 75, "a.out.nodebug.wasm.gz": 87, - "total": 2508, - "total_gz": 1225, + "total": 2443, + "total_gz": 1209, "sent": [], "imports": [], "exports": [ diff --git a/tools/acorn-optimizer.mjs b/tools/acorn-optimizer.mjs index e1b5604981450..1c9df11859182 100755 --- a/tools/acorn-optimizer.mjs +++ b/tools/acorn-optimizer.mjs @@ -1726,6 +1726,7 @@ const params = { ecmaVersion: 'latest', sourceType: exportES6 ? 'module' : 'script', allowAwaitOutsideFunction: true, + allowImportExportEverywhere: exportES6, }; if (closureFriendly) { const currentComments = []; diff --git a/tools/building.py b/tools/building.py index bb891c556d5dc..62a0d8e9399e4 100644 --- a/tools/building.py +++ b/tools/building.py @@ -585,6 +585,14 @@ def closure_compiler(filename, advanced=True, extra_closure_args=None): args = ['--compilation_level', 'ADVANCED_OPTIMIZATIONS' if advanced else 'SIMPLE_OPTIMIZATIONS'] args += ['--language_in', 'UNSTABLE'] + # Make Closure aware of the ES6 module syntax; + # i.e. the `import.meta` and `await import` usages + if settings.EXPORT_ES6: + args += ['--chunk_output_type', 'ES_MODULES'] + if settings.ENVIRONMENT_MAY_BE_NODE: + args += ['--module_resolution', 'NODE'] + # https://github.com/google/closure-compiler/issues/3740 + args += ['--jscomp_off=moduleLoad'] # We currently only use closure compiler for minification, not transpilation. args += ['--language_out', 'NO_TRANSPILE'] # Tell closure never to inject the 'use strict' directive. diff --git a/tools/link.py b/tools/link.py index f2f7b97850685..eded3cd7e37db 100644 --- a/tools/link.py +++ b/tools/link.py @@ -2073,19 +2073,28 @@ def phase_source_transforms(options): save_intermediate('transformed') -# Unmangle previously mangled `import.meta` and `await import` references in +# Unmangle previously mangled `await import` and `await` references in # both main code and libraries. -# See also: `preprocess` in parseTools.js. +# See also: `mangleUnsupportedSyntax` in parseTools.mjs. def fix_js_mangling(js_file): - # We don't apply these mangliings except in MODULARIZE/EXPORT_ES6 modes. - if not settings.MODULARIZE: + # Mangling only takes place under closure in MODULARIZE mode. + if not settings.MODULARIZE or not settings.USE_CLOSURE_COMPILER: return src = read_file(js_file) - write_file(js_file, src - .replace('EMSCRIPTEN$IMPORT$META', 'import.meta') - .replace('EMSCRIPTEN$AWAIT$IMPORT', 'await import') - .replace('EMSCRIPTEN$AWAIT(', 'await (')) + + if settings.EXPORT_ES6: + # Also remove the line containing `export{};`, which is inserted by + # Closure to mark the file as an ES6 module. + # https://github.com/google/closure-compiler/issues/4084#issuecomment-1505056519 + # https://github.com/google/closure-compiler/blob/v20260401/src/com/google/javascript/jscomp/ConvertChunksToESModules.java#L111-L113 + src = src \ + .replace('EMSCRIPTEN$AWAIT$IMPORT', 'await import') \ + .replace('export{};\n', '') + + src = src.replace('EMSCRIPTEN$AWAIT(', 'await (') + + write_file(js_file, src) save_intermediate('js-mangling')