diff --git a/src/lib/libatomic.js b/src/lib/libatomic.js index c30dff83323cd..576be4fe4ecbd 100644 --- a/src/lib/libatomic.js +++ b/src/lib/libatomic.js @@ -154,9 +154,12 @@ addToLibrary({ emscripten_has_threading_support: () => !!globalThis.SharedArrayBuffer, +#if ENVIRONMENT_MAY_BE_NODE + emscripten_num_logical_cores__deps: ['$nodeOs'], +#endif emscripten_num_logical_cores: () => #if ENVIRONMENT_MAY_BE_NODE - ENVIRONMENT_IS_NODE ? require('node:os').cpus().length : + ENVIRONMENT_IS_NODE ? nodeOs.cpus().length : #endif navigator['hardwareConcurrency'], diff --git a/src/lib/libcore.js b/src/lib/libcore.js index f690ed0f907b2..30fd643e57d39 100644 --- a/src/lib/libcore.js +++ b/src/lib/libcore.js @@ -372,6 +372,11 @@ addToLibrary({ }, #endif +#if ENVIRONMENT_MAY_BE_NODE + $nodeOs: "{{{ makeNodeImport('node:os') }}}", + $nodeChildProcess: "{{{ makeNodeImport('node:child_process') }}}", + _emscripten_system__deps: ['$nodeChildProcess'], +#endif _emscripten_system: (command) => { #if ENVIRONMENT_MAY_BE_NODE if (ENVIRONMENT_IS_NODE) { @@ -380,8 +385,7 @@ addToLibrary({ var cmdstr = UTF8ToString(command); if (!cmdstr.length) return 0; // this is what glibc seems to do (shell works test?) - var cp = require('node:child_process'); - var ret = cp.spawnSync(cmdstr, [], {shell:true, stdio:'inherit'}); + var ret = nodeChildProcess.spawnSync(cmdstr, [], {shell:true, stdio:'inherit'}); var _W_EXITCODE = (ret, sig) => ((ret) << 8 | (sig)); diff --git a/src/lib/libnodepath.js b/src/lib/libnodepath.js index d891bf7339662..cd91a2a0cbaa1 100644 --- a/src/lib/libnodepath.js +++ b/src/lib/libnodepath.js @@ -12,7 +12,7 @@ // operations. Hence, using `nodePath` should be safe here. addToLibrary({ - $nodePath: "require('node:path')", + $nodePath: "{{{ makeNodeImport('node:path', false) }}}", $PATH__deps: ['$nodePath'], $PATH: `{ isAbs: nodePath.isAbsolute, diff --git a/src/lib/libsockfs.js b/src/lib/libsockfs.js index 01d6f831da2bf..15f27843d2519 100644 --- a/src/lib/libsockfs.js +++ b/src/lib/libsockfs.js @@ -5,10 +5,18 @@ */ addToLibrary({ +#if ENVIRONMENT_MAY_BE_NODE && EXPORT_ES6 + // In ESM mode, require() is not natively available. When SOCKFS is used, + // we need require() to lazily load the 'ws' npm package for WebSocket + // support on Node.js. Set up a createRequire-based polyfill. + $nodeRequire: `ENVIRONMENT_IS_NODE ? (await import('node:module')).createRequire(import.meta.url) : undefined`, + $SOCKFS__deps: ['$FS', '$nodeRequire'], +#else + $SOCKFS__deps: ['$FS'], +#endif $SOCKFS__postset: () => { addAtInit('SOCKFS.root = FS.mount(SOCKFS, {}, null);'); }, - $SOCKFS__deps: ['$FS'], $SOCKFS: { #if expectToReceiveOnModule('websocket') websocketArgs: {}, @@ -216,7 +224,11 @@ addToLibrary({ var WebSocketConstructor; #if ENVIRONMENT_MAY_BE_NODE if (ENVIRONMENT_IS_NODE) { +#if EXPORT_ES6 + WebSocketConstructor = /** @type{(typeof WebSocket)} */(nodeRequire('ws')); +#else WebSocketConstructor = /** @type{(typeof WebSocket)} */(require('ws')); +#endif } else #endif // ENVIRONMENT_MAY_BE_NODE { @@ -522,7 +534,11 @@ addToLibrary({ if (sock.server) { throw new FS.ErrnoError({{{ cDefs.EINVAL }}}); // already listening } +#if EXPORT_ES6 + var WebSocketServer = nodeRequire('ws').Server; +#else var WebSocketServer = require('ws').Server; +#endif var host = sock.saddr; #if SOCKET_DEBUG dbg(`websocket: listen: ${host}:${sock.sport}`); diff --git a/src/lib/libwasi.js b/src/lib/libwasi.js index bb67581f269a6..7a2f68a2903c0 100644 --- a/src/lib/libwasi.js +++ b/src/lib/libwasi.js @@ -569,14 +569,21 @@ var WasiLibrary = { // random.h -#if ENVIRONMENT_MAY_BE_SHELL +#if ENVIRONMENT_MAY_BE_NODE && MIN_NODE_VERSION < 190000 + $nodeCrypto: "{{{ makeNodeImport('node:crypto') }}}", +#endif + +#if ENVIRONMENT_MAY_BE_SHELL && ENVIRONMENT_MAY_BE_NODE && MIN_NODE_VERSION < 190000 + $initRandomFill__deps: ['$base64Decode', '$nodeCrypto'], +#elif ENVIRONMENT_MAY_BE_SHELL $initRandomFill__deps: ['$base64Decode'], +#elif ENVIRONMENT_MAY_BE_NODE && MIN_NODE_VERSION < 190000 + $initRandomFill__deps: ['$nodeCrypto'], #endif $initRandomFill: () => { #if ENVIRONMENT_MAY_BE_NODE && MIN_NODE_VERSION < 190000 // This block is not needed on v19+ since crypto.getRandomValues is builtin if (ENVIRONMENT_IS_NODE) { - var nodeCrypto = require('node:crypto'); return (view) => nodeCrypto.randomFillSync(view); } #endif // ENVIRONMENT_MAY_BE_NODE diff --git a/src/lib/libwasm_worker.js b/src/lib/libwasm_worker.js index e4eb8491389c1..8f131f8c7e10f 100644 --- a/src/lib/libwasm_worker.js +++ b/src/lib/libwasm_worker.js @@ -288,9 +288,12 @@ if (ENVIRONMENT_IS_WASM_WORKER _wasmWorkers[id].postMessage({'_wsc': funcPtr, 'x': readEmAsmArgs(sigPtr, varargs) }); }, +#if ENVIRONMENT_MAY_BE_NODE + emscripten_navigator_hardware_concurrency__deps: ['$nodeOs'], +#endif emscripten_navigator_hardware_concurrency: () => { #if ENVIRONMENT_MAY_BE_NODE - if (ENVIRONMENT_IS_NODE) return require('node:os').cpus().length; + if (ENVIRONMENT_IS_NODE) return nodeOs.cpus().length; #endif return navigator['hardwareConcurrency']; }, diff --git a/src/parseTools.mjs b/src/parseTools.mjs index b5c11288f31fa..f2b61f5990176 100644 --- a/src/parseTools.mjs +++ b/src/parseTools.mjs @@ -954,6 +954,27 @@ function makeModuleReceiveWithVar(localName, moduleName, defaultValue) { return ret; } +function makeNodeImport(module, guard = true) { + assert(ENVIRONMENT_MAY_BE_NODE, 'makeNodeImport called when environment can never be node'); + var expr; + if (EXPORT_ES6) { + expr = `await import(/* webpackIgnore: true */ /* @vite-ignore */ '${module}')`; + } else { + expr = `require('${module}')`; + } + if (guard) { + return `ENVIRONMENT_IS_NODE ? ${expr} : undefined`; + } + return expr; +} + +function makeNodeFilePath(filename) { + if (EXPORT_ES6) { + return `new URL('${filename}', import.meta.url)`; + } + return `__dirname + '/${filename}'`; +} + function makeRemovedFSAssert(fsName) { assert(ASSERTIONS); const lower = fsName.toLowerCase(); @@ -1241,6 +1262,8 @@ addToCompileTimeContext({ makeModuleReceive, makeModuleReceiveExpr, makeModuleReceiveWithVar, + makeNodeFilePath, + makeNodeImport, makeRemovedFSAssert, makeRetainedCompilerSettings, makeReturn64, diff --git a/src/preamble.js b/src/preamble.js index cbc818e952f3d..32fe152d5a5cf 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -550,7 +550,7 @@ function instantiateSync(file, info) { var binary = getBinarySync(file); #if NODE_CODE_CACHING if (ENVIRONMENT_IS_NODE) { - var v8 = require('node:v8'); + var v8 = {{{ makeNodeImport('node:v8', false) }}}; // Include the V8 version in the cache name, so that we don't try to // load cached code from another version, which fails silently (it seems // to load ok, but we do actually recompile the binary every time). diff --git a/src/runtime_common.js b/src/runtime_common.js index 13f56e427a012..3e3f59181623b 100644 --- a/src/runtime_common.js +++ b/src/runtime_common.js @@ -144,7 +144,7 @@ if (ENVIRONMENT_IS_NODE) { // depends on it for accurate timing. // Use `global` rather than `globalThis` here since older versions of node // don't have `globalThis`. - global.performance ??= require('perf_hooks').performance; + global.performance ??= ({{{ makeNodeImport('perf_hooks', false) }}}).performance; } #endif diff --git a/src/runtime_debug.js b/src/runtime_debug.js index 7c1170a3b3010..17b8a8de9addd 100644 --- a/src/runtime_debug.js +++ b/src/runtime_debug.js @@ -8,23 +8,24 @@ var runtimeDebug = true; // Switch to false at runtime to disable logging at the right times // Used by XXXXX_DEBUG settings to output debug messages. +#if ENVIRONMENT_MAY_BE_NODE && (PTHREADS || WASM_WORKERS) +// dbg_node_fs and dbg_node_utils are declared and initialized in shell.js +// when node modules (fs/utils) become available. +#endif function dbg(...args) { if (!runtimeDebug && typeof runtimeDebug != 'undefined') return; #if ENVIRONMENT_MAY_BE_NODE && (PTHREADS || WASM_WORKERS) // Avoid using the console for debugging in multi-threaded node applications // See https://github.com/emscripten-core/emscripten/issues/14804 - if (ENVIRONMENT_IS_NODE) { - // TODO(sbc): Unify with err/out implementation in shell.sh. - var fs = require('node:fs'); - var utils = require('node:util'); + if (ENVIRONMENT_IS_NODE && dbg_node_fs) { function stringify(a) { switch (typeof a) { - case 'object': return utils.inspect(a); + case 'object': return dbg_node_utils.inspect(a); case 'undefined': return 'undefined'; } return a; } - fs.writeSync(2, args.map(stringify).join(' ') + '\n'); + dbg_node_fs.writeSync(2, args.map(stringify).join(' ') + '\n'); } else #endif // TODO(sbc): Make this configurable somehow. Its not always convenient for diff --git a/src/shell.js b/src/shell.js index 41294749487a4..236807d9ecf5b 100644 --- a/src/shell.js +++ b/src/shell.js @@ -106,18 +106,9 @@ if (ENVIRONMENT_IS_PTHREAD) { #endif #endif -#if ENVIRONMENT_MAY_BE_NODE && (EXPORT_ES6 || PTHREADS || WASM_WORKERS) +#if ENVIRONMENT_MAY_BE_NODE && (PTHREADS || WASM_WORKERS) if (ENVIRONMENT_IS_NODE) { -#if EXPORT_ES6 - // When building an ES module `require` is not normally available. - // We need to use `createRequire()` to construct the require()` function. - const { createRequire } = await import('node:module'); - /** @suppress{duplicate} */ - var require = createRequire(import.meta.url); -#endif - -#if PTHREADS || WASM_WORKERS - var worker_threads = require('node:worker_threads'); + var worker_threads = {{{ makeNodeImport('node:worker_threads', false) }}}; global.Worker = worker_threads.Worker; ENVIRONMENT_IS_WORKER = !worker_threads.isMainThread; #if PTHREADS @@ -128,7 +119,6 @@ if (ENVIRONMENT_IS_NODE) { #if WASM_WORKERS ENVIRONMENT_IS_WASM_WORKER = ENVIRONMENT_IS_WORKER && worker_threads.workerData == 'em-ww' #endif -#endif // PTHREADS || WASM_WORKERS } #endif // ENVIRONMENT_MAY_BE_NODE @@ -199,11 +189,13 @@ if (ENVIRONMENT_IS_NODE) { // These modules will usually be used on Node.js. Load them eagerly to avoid // the complexity of lazy-loading. - var fs = require('node:fs'); + var fs = {{{ makeNodeImport('node:fs', false) }}}; #if EXPORT_ES6 if (_scriptName.startsWith('file:')) { - scriptDirectory = require('node:path').dirname(require('node:url').fileURLToPath(_scriptName)) + '/'; + var nodePath = {{{ makeNodeImport('node:path', false) }}}; + var nodeUrl = {{{ makeNodeImport('node:url', false) }}}; + scriptDirectory = nodePath.dirname(nodeUrl.fileURLToPath(_scriptName)) + '/'; } #else scriptDirectory = __dirname + '/'; @@ -351,10 +343,17 @@ if (!ENVIRONMENT_IS_AUDIO_WORKLET) var defaultPrint = console.log.bind(console); var defaultPrintErr = console.error.bind(console); if (ENVIRONMENT_IS_NODE) { - var utils = require('node:util'); + var utils = {{{ makeNodeImport('node:util', false) }}}; var stringify = (a) => typeof a == 'object' ? utils.inspect(a) : a; defaultPrint = (...args) => fs.writeSync(1, args.map(stringify).join(' ') + '\n'); defaultPrintErr = (...args) => fs.writeSync(2, args.map(stringify).join(' ') + '\n'); +#if (ASSERTIONS || RUNTIME_DEBUG || AUTODEBUG) && (PTHREADS || WASM_WORKERS) + // Initialize the lazy-loaded node modules for dbg() now that fs/utils are + // available. Declared here (before runtime_debug.js) to avoid Closure + // Compiler's JSC_REFERENCE_BEFORE_DECLARE warning. + var dbg_node_fs = fs; + var dbg_node_utils = utils; +#endif } {{{ makeModuleReceiveWithVar('out', 'print', 'defaultPrint') }}} {{{ makeModuleReceiveWithVar('err', 'printErr', 'defaultPrintErr') }}} diff --git a/src/shell_minimal.js b/src/shell_minimal.js index a291c8130ab2a..d6aca3a8e0fa8 100644 --- a/src/shell_minimal.js +++ b/src/shell_minimal.js @@ -55,7 +55,7 @@ var ENVIRONMENT_IS_WEB = !ENVIRONMENT_IS_NODE; #if ENVIRONMENT_MAY_BE_NODE && (PTHREADS || WASM_WORKERS) if (ENVIRONMENT_IS_NODE) { - var worker_threads = require('node:worker_threads'); + var worker_threads = {{{ makeNodeImport('node:worker_threads', false) }}}; global.Worker = worker_threads.Worker; } #endif @@ -99,7 +99,7 @@ if (ENVIRONMENT_IS_NODE && ENVIRONMENT_IS_SHELL) { var defaultPrint = console.log.bind(console); var defaultPrintErr = console.error.bind(console); if (ENVIRONMENT_IS_NODE) { - var fs = require('node:fs'); + var fs = {{{ makeNodeImport('node:fs', false) }}}; defaultPrint = (...args) => fs.writeSync(1, args.join(' ') + '\n'); defaultPrintErr = (...args) => fs.writeSync(2, args.join(' ') + '\n'); } @@ -181,13 +181,13 @@ if (!ENVIRONMENT_IS_PTHREAD) { // Wasm or Wasm2JS loading: if (ENVIRONMENT_IS_NODE) { - var fs = require('node:fs'); + var fs = {{{ makeNodeImport('node:fs', false) }}}; #if WASM == 2 - if (globalThis.WebAssembly) Module['wasm'] = fs.readFileSync(__dirname + '/{{{ TARGET_BASENAME }}}.wasm'); - else eval(fs.readFileSync(__dirname + '/{{{ TARGET_BASENAME }}}.wasm.js')+''); + if (globalThis.WebAssembly) Module['wasm'] = fs.readFileSync({{{ makeNodeFilePath(TARGET_BASENAME + '.wasm') }}}); + else eval(fs.readFileSync({{{ makeNodeFilePath(TARGET_BASENAME + '.wasm.js') }}})+''); #else #if !WASM2JS - Module['wasm'] = fs.readFileSync(__dirname + '/{{{ TARGET_BASENAME }}}.wasm'); + Module['wasm'] = fs.readFileSync({{{ makeNodeFilePath(TARGET_BASENAME + '.wasm') }}}); #endif #endif } diff --git a/test/common.py b/test/common.py index a40ac40ae0121..52b6d3edf1a2f 100644 --- a/test/common.py +++ b/test/common.py @@ -626,12 +626,21 @@ def require_wasm2js(self): if self.get_setting('WASM_ESM_INTEGRATION'): self.skipTest('wasm2js is not compatible with WASM_ESM_INTEGRATION') + def is_esm(self): + return self.get_setting('EXPORT_ES6') or self.get_setting('WASM_ESM_INTEGRATION') or self.get_setting('MODULARIZE') == 'instance' + + def add_require_polyfill(self): + """Add a require() polyfill for ESM mode using createRequire (Node 12.2+).""" + if self.is_esm(): + self.cflags += ['--pre-js', test_file('require_polyfill.js')] + def setup_nodefs_test(self): self.require_node() if self.get_setting('WASMFS'): # without this the JS setup code in setup_nodefs.js doesn't work self.set_setting('FORCE_FILESYSTEM') self.cflags += ['-DNODEFS', '-lnodefs.js', '--pre-js', test_file('setup_nodefs.js'), '-sINCOMING_MODULE_JS_API=onRuntimeInitialized'] + self.add_require_polyfill() def setup_noderawfs_test(self): self.require_node() diff --git a/test/require_polyfill.js b/test/require_polyfill.js new file mode 100644 index 0000000000000..a60e8df779520 --- /dev/null +++ b/test/require_polyfill.js @@ -0,0 +1,7 @@ +// Polyfill require() for ESM mode so that EM_ASM/EM_JS code using +// require('fs'), require('path'), etc. works in both CJS and ESM. +// createRequire is available since Node 12.2.0. +if (typeof require === 'undefined') { + var { createRequire } = await import('module'); + var require = createRequire(import.meta.url); +} diff --git a/test/test_core.py b/test/test_core.py index 7f2766fb1464e..2c4f6794a547d 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -5823,6 +5823,8 @@ def test_fs_base(self): def test_fs_nodefs_rw(self): if not self.get_setting('NODERAWFS'): self.setup_nodefs_test() + else: + self.add_require_polyfill() self.maybe_closure() self.do_runf('fs/test_nodefs_rw.c', 'success') @@ -5846,6 +5848,7 @@ def test_fs_nodefs_dup(self): @requires_node def test_fs_nodefs_home(self): + self.add_require_polyfill() self.do_runf('fs/test_nodefs_home.c', 'success', cflags=['-sFORCE_FILESYSTEM', '-lnodefs.js']) @requires_node @@ -8656,7 +8659,14 @@ def test(assert_returncode=0): js = read_file(self.output_name('test_hello_world.support')) else: js = read_file(self.output_name('test_hello_world')) - assert ('require(' in js) == ('node' in self.get_setting('ENVIRONMENT')), 'we should have require() calls only if node js specified' + # In ESM mode we use dynamic import() instead of require() for node modules. + # MODULARIZE=instance implies EXPORT_ES6 which triggers ESM output. + is_esm = self.get_setting('EXPORT_ES6') or self.get_setting('WASM_ESM_INTEGRATION') or self.get_setting('MODULARIZE') == 'instance' + if is_esm: + has_node_imports = 'import(' in js + else: + has_node_imports = 'require(' in js + assert has_node_imports == ('node' in self.get_setting('ENVIRONMENT')), 'we should have node imports only if node js specified' for engine in self.js_engines: print(f'engine: {engine}') diff --git a/test/test_other.py b/test/test_other.py index 936dff4783e1c..29b027e21453a 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -466,6 +466,31 @@ def test_esm_implies_modularize(self): def test_esm_requires_modularize(self): self.assert_fail([EMCC, test_file('hello_world.c'), '-sEXPORT_ES6', '-sMODULARIZE=0'], 'EXPORT_ES6 requires MODULARIZE to be set') + # Verify that EXPORT_ES6 output uses `await import()` instead of `require()` + # for Node.js built-in modules. Using `require()` in ESM files breaks + # bundlers (webpack, rollup, vite, esbuild) which cannot resolve CommonJS + # require() calls inside ES modules. + @crossplatform + @parameterized({ + 'default': ([],), + 'node': (['-sENVIRONMENT=node'],), + 'pthreads': (['-pthread', '-sPTHREAD_POOL_SIZE=1'],), + }) + def test_esm_no_require(self, args): + self.run_process([EMCC, '-o', 'hello_world.mjs', + '--extern-post-js', test_file('modularize_post_js.js'), + test_file('hello_world.c')] + args) + src = read_file('hello_world.mjs') + # EXPORT_ES6 output must not contain require() calls as these are + # incompatible with ES modules and break bundlers. + # The only acceptable require-like pattern is inside a string/comment. + require_calls = re.findall(r'(? { - assert(require('path')['isAbsolute'](scriptDirectory), `scriptDirectory (${scriptDirectory}) should be an absolute path`); + assert(nodePath.isAbsolute(scriptDirectory), `scriptDirectory (${scriptDirectory}) should be an absolute path`); return scriptDirectory + fileName; }; ''') diff --git a/test/vite/vite.config.js b/test/vite/vite.config.js index 019c96058a246..0f574d726c9d3 100644 --- a/test/vite/vite.config.js +++ b/test/vite/vite.config.js @@ -1,3 +1,22 @@ export default { base: './', + build: { + rollupOptions: { + onwarn(warning, defaultHandler) { + // Vite externalizes node built-in imports (node:fs, etc.) for browser + // compatibility. This is expected for dynamic import() calls guarded + // by ENVIRONMENT_IS_NODE. However, require() calls in ESM output are + // truly broken — vite cannot handle them. Detect require()-based + // externalization by checking for imports that don't use the node: scheme. + if (warning.message && warning.message.includes('externalized for browser compatibility')) { + // Accept node: scheme imports (dynamic import with bundler hints) + var match = warning.message.match(/Module "([^"]+)"/); + if (match && !match[1].startsWith('node:')) { + throw new Error(warning.message); + } + } + defaultHandler(warning); + }, + }, + }, } diff --git a/tools/link.py b/tools/link.py index 69dfa198b93f3..785fb25698081 100644 --- a/tools/link.py +++ b/tools/link.py @@ -2322,7 +2322,43 @@ def phase_binaryen(target, options, wasm_target): if options.use_closure_compiler: with ToolchainProfiler.profile_block('closure_compile'): - final_js = building.closure_compiler(final_js, extra_closure_args=settings.CLOSURE_ARGS) + # In EXPORT_ES6 mode, we use `await import('node:xyz')` for dynamic imports. + # Closure compiler runs before the MODULARIZE async wrapper is applied, so it + # sees `await` outside an async function and fails to parse it. + # Work around this by replacing dynamic imports with placeholders before Closure, + # then restoring them after. + closure_args = list(settings.CLOSURE_ARGS) if settings.CLOSURE_ARGS else [] + if settings.EXPORT_ES6: + src = utils.read_file(final_js) + # Find all node modules being dynamically imported (with optional bundler hints) + node_modules = set(re.findall(r"await\s+import\(\s*(?:/\*.*?\*/\s*)*['\"]node:(\w+)['\"]\s*\)", src)) + if node_modules: + # Replace: await import(/* ... */ 'node:fs') -> __EMSCRIPTEN_PRIVATE_AWAIT_IMPORT_fs__ + src = re.sub(r"await\s+import\(\s*(?:/\*.*?\*/\s*)*['\"]node:(\w+)['\"]\s*\)", + r'__EMSCRIPTEN_PRIVATE_AWAIT_IMPORT_\1__', src) + utils.write_file(final_js, src) + save_intermediate('closure_pre') + # Generate externs for the placeholder variables so Closure doesn't complain + externs_content = '\n'.join( + f'/** @type {{?}} */ var __EMSCRIPTEN_PRIVATE_AWAIT_IMPORT_{mod}__;' + for mod in node_modules + ) + externs_file = shared.get_temp_files().get('.js', prefix='node_import_externs_') + externs_file.write(externs_content.encode()) + externs_file.close() + closure_args += ['--externs', externs_file.name] + + final_js = building.closure_compiler(final_js, extra_closure_args=closure_args) + + if settings.EXPORT_ES6: + src = utils.read_file(final_js) + # Restore: __EMSCRIPTEN_PRIVATE_AWAIT_IMPORT_fs__ -> (await import(/* hints */ 'node:fs')) + # Parentheses are needed because Closure may inline the placeholder into + # expressions like `placeholder.method()` which needs `(await import(...)).method()` + src = re.sub(r'__EMSCRIPTEN_PRIVATE_AWAIT_IMPORT_(\w+)__', + r"(await import(/* webpackIgnore: true */ /* @vite-ignore */ 'node:\1'))", src) + utils.write_file(final_js, src) + save_intermediate('closure') if settings.TRANSPILE: