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 ee9cfcedef751..e0de96703f5a8 100644 --- a/src/lib/libcore.js +++ b/src/lib/libcore.js @@ -372,6 +372,12 @@ addToLibrary({ }, #endif +#if ENVIRONMENT_MAY_BE_NODE + $nodeOs: "{{{ makeNodeImport('node:os') }}}", + $nodeChildProcess: "{{{ makeNodeImport('node:child_process') }}}", + $nodeFs: "{{{ makeNodeImport('node:fs') }}}", + _emscripten_system__deps: ['$nodeChildProcess'], +#endif _emscripten_system: (command) => { #if ENVIRONMENT_MAY_BE_NODE if (ENVIRONMENT_IS_NODE) { @@ -380,8 +386,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/libnoderawfs.js b/src/lib/libnoderawfs.js index 57387ad233674..542be206027e0 100644 --- a/src/lib/libnoderawfs.js +++ b/src/lib/libnoderawfs.js @@ -5,12 +5,12 @@ */ addToLibrary({ - $NODERAWFS__deps: ['$ERRNO_CODES', '$FS', '$NODEFS', '$mmapAlloc', '$FS_modeStringToFlags', '$NODERAWFS_stream_funcs'], + $nodeTTY: "{{{ makeNodeImport('node:tty', false) }}}", + $NODERAWFS__deps: ['$ERRNO_CODES', '$FS', '$NODEFS', '$mmapAlloc', '$FS_modeStringToFlags', '$NODERAWFS_stream_funcs', '$nodeTTY'], $NODERAWFS__postset: ` if (!ENVIRONMENT_IS_NODE) { throw new Error("NODERAWFS is currently only supported on Node.js environment.") } - var nodeTTY = require('node:tty'); function _wrapNodeError(func) { return (...args) => { try { 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 ca3a218553d99..a6c4c4224859a 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 af8b8930104d5..0a52a0578ab46 100644 --- a/src/lib/libwasm_worker.js +++ b/src/lib/libwasm_worker.js @@ -298,9 +298,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 544d7bd808250..b1b024d45646d 100644 --- a/src/parseTools.mjs +++ b/src/parseTools.mjs @@ -953,6 +953,37 @@ 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) { + // Use the same EMSCRIPTEN$AWAIT$IMPORT placeholder that `preprocess()` emits + // for hand-written `await import` calls in source files. It is restored to + // `await import` by `fix_js_mangling()` after Closure runs. See parseTools + // preprocess() and tools/link.py fix_js_mangling(). + // + // The `/* webpackIgnore: true */` hint is required because webpack does + // NOT auto-handle `node:`-prefixed URIs for dynamic import; without it, + // webpack fails with `UnhandledSchemeError: Reading from "node:xxx" is + // not handled by plugins` (see test_webpack_esm_output_clean). + // `/* @vite-ignore */` similarly silences vite's dynamic-import analysis. + expr = `EMSCRIPTEN$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(); @@ -1243,6 +1274,8 @@ addToCompileTimeContext({ makeModuleReceive, makeModuleReceiveExpr, makeModuleReceiveWithVar, + makeNodeFilePath, + makeNodeImport, makeRemovedFSAssert, makeRetainedCompilerSettings, makeReturn64, diff --git a/src/preamble.js b/src/preamble.js index bb7d09e98ce46..20578bcf0f044 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -547,7 +547,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_debug.js b/src/runtime_debug.js index 4752179a8d188..bd8e73e18506d 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 3968682f855ed..a6361deb297dd 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) }}}; globalThis.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 0e71500ef2404..4bd1217f25dcb 100644 --- a/src/shell_minimal.js +++ b/src/shell_minimal.js @@ -59,7 +59,7 @@ var ENVIRONMENT_IS_WORKER = !!globalThis.WorkerGlobalScope; #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) }}}; globalThis.Worker = worker_threads.Worker; ENVIRONMENT_IS_WORKER = !worker_threads.isMainThread; } @@ -104,9 +104,14 @@ 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'); +#if (ASSERTIONS || RUNTIME_DEBUG || AUTODEBUG) + var utils = {{{ makeNodeImport('node:util', false) }}}; + var dbg_node_fs = fs; + var dbg_node_utils = utils; +#endif } var out = defaultPrint; var err = defaultPrintErr; @@ -115,6 +120,16 @@ var out = (...args) => console.log(...args); var err = (...args) => console.error(...args); #endif +#if !PTHREADS && WASM_WORKERS && ENVIRONMENT_MAY_BE_NODE && (ASSERTIONS || RUNTIME_DEBUG || AUTODEBUG) +// Initialize dbg() node module references for WASM_WORKERS without PTHREADS. +// (With PTHREADS these are set in the print setup block above.) +var dbg_node_fs, dbg_node_utils; +if (ENVIRONMENT_IS_NODE) { + dbg_node_fs = {{{ makeNodeImport('node:fs', false) }}}; + dbg_node_utils = {{{ makeNodeImport('node:util', false) }}}; +} +#endif + // Override this function in a --pre-js file to get a signal for when // compilation is ready. In that callback, call the function run() to start // the program. @@ -182,13 +197,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 a72420fd524e4..a30293e0474fa 100644 --- a/test/common.py +++ b/test/common.py @@ -596,6 +596,9 @@ 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 setup_nodefs_test(self): self.require_node() if self.get_setting('WASMFS'): diff --git a/test/fs/test_nodefs_home.c b/test/fs/test_nodefs_home.c index 2a49c965e370f..0712fe7de71bc 100644 --- a/test/fs/test_nodefs_home.c +++ b/test/fs/test_nodefs_home.c @@ -8,16 +8,17 @@ #include #include +EM_JS_DEPS(deps, "$nodePath"); + int main(void) { EM_ASM( - var path = require("path"); var home = process.env.HOME; // On Windows HOME environment variable doesn't exist, but concatenating HOMEDRIVE and HOMEPATH // does the same thing. if (!home) home = process.env.HOMEDRIVE + process.env.HOMEPATH; - var parent = path.dirname(home); - var relative = path.relative(parent, home); + var parent = nodePath.dirname(home); + var relative = nodePath.relative(parent, home); FS.mkdir('/nodefs_home'); FS.mount(NODEFS, { root: parent }, '/nodefs_home'); // Reading C:/Users/(username) on Windows, /home/(username) on Linux diff --git a/test/fs/test_nodefs_rw.c b/test/fs/test_nodefs_rw.c index 48cebcba932ff..87ea4dc8754f9 100644 --- a/test/fs/test_nodefs_rw.c +++ b/test/fs/test_nodefs_rw.c @@ -11,6 +11,8 @@ #include #include +EM_JS_DEPS(deps, "$nodeFs"); + int main() { FILE *file; int res; @@ -18,8 +20,7 @@ int main() { // write something locally with node EM_ASM( - var fs = require('fs'); - fs.writeFileSync('foobar.txt', 'yeehaw'); + nodeFs.writeFileSync('foobar.txt', 'yeehaw'); ); // read and validate the contents of the file @@ -42,8 +43,7 @@ int main() { // validate the changes were persisted to the underlying fs EM_ASM( - var fs = require('fs'); - var contents = fs.readFileSync('foobar.txt', { encoding: 'utf8' }); + var contents = nodeFs.readFileSync('foobar.txt', { encoding: 'utf8' }); assert(contents === 'cheez'); ); diff --git a/test/test_core.py b/test/test_core.py index 5dd5fd029a8f7..9c6a1f4ce1c5e 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -5856,7 +5856,7 @@ def test_fs_nodefs_dup(self): @requires_node def test_fs_nodefs_home(self): - self.do_runf('fs/test_nodefs_home.c', 'success', cflags=['-sFORCE_FILESYSTEM', '-lnodefs.js']) + self.do_runf('fs/test_nodefs_home.c', 'success', cflags=['-sFORCE_FILESYSTEM', '-lnodefs.js', '-lnodepath.js']) @requires_node def test_fs_nodefs_nofollow(self): @@ -8669,7 +8669,12 @@ 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. + if self.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 780c164773f3a..11508874e06a7 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/building.py b/tools/building.py index 0405a8813dcef..0c753e08053a3 100644 --- a/tools/building.py +++ b/tools/building.py @@ -541,7 +541,7 @@ def version_split(v): @ToolchainProfiler.profile() def transpile(filename): config = { - 'sourceType': 'script', + 'sourceType': 'module' if settings.EXPORT_ES6 else 'script', 'presets': ['@babel/preset-env'], 'targets': {}, }