diff --git a/src/lib/libatomic.js b/src/lib/libatomic.js index e1dbc5bc4b08a..f9677713c44f4 100644 --- a/src/lib/libatomic.js +++ b/src/lib/libatomic.js @@ -159,9 +159,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 1fb5e90f2597b..b960b572a13f1 100644 --- a/src/lib/libcore.js +++ b/src/lib/libcore.js @@ -343,6 +343,11 @@ addToLibrary({ }, #endif +#if ENVIRONMENT_MAY_BE_NODE + $nodeOs: "ENVIRONMENT_IS_NODE ? {{{ makeNodeImport('node:os') }}} : undefined", + $nodeChildProcess: "ENVIRONMENT_IS_NODE ? {{{ makeNodeImport('node:child_process') }}} : undefined", + _emscripten_system__deps: ['$nodeChildProcess'], +#endif _emscripten_system: (command) => { #if ENVIRONMENT_MAY_BE_NODE if (ENVIRONMENT_IS_NODE) { @@ -351,8 +356,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..d73f6e675d7f8 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') }}}", $PATH__deps: ['$nodePath'], $PATH: `{ isAbs: nodePath.isAbsolute, diff --git a/src/lib/libsockfs.js b/src/lib/libsockfs.js index 01d6f831da2bf..a7eb620b134af 100644 --- a/src/lib/libsockfs.js +++ b/src/lib/libsockfs.js @@ -5,10 +5,19 @@ */ addToLibrary({ +#if ENVIRONMENT_MAY_BE_NODE +#if EXPORT_ES6 + $nodeWs: "ENVIRONMENT_IS_NODE ? ({{{ makeNodeImport('ws') }}}).default : undefined", +#else + $nodeWs: "ENVIRONMENT_IS_NODE ? {{{ makeNodeImport('ws') }}} : undefined", +#endif + $SOCKFS__deps: ['$FS', '$nodeWs'], +#else + $SOCKFS__deps: ['$FS'], +#endif $SOCKFS__postset: () => { addAtInit('SOCKFS.root = FS.mount(SOCKFS, {}, null);'); }, - $SOCKFS__deps: ['$FS'], $SOCKFS: { #if expectToReceiveOnModule('websocket') websocketArgs: {}, @@ -216,7 +225,7 @@ addToLibrary({ var WebSocketConstructor; #if ENVIRONMENT_MAY_BE_NODE if (ENVIRONMENT_IS_NODE) { - WebSocketConstructor = /** @type{(typeof WebSocket)} */(require('ws')); + WebSocketConstructor = /** @type{(typeof WebSocket)} */(nodeWs); } else #endif // ENVIRONMENT_MAY_BE_NODE { @@ -522,7 +531,7 @@ addToLibrary({ if (sock.server) { throw new FS.ErrnoError({{{ cDefs.EINVAL }}}); // already listening } - var WebSocketServer = require('ws').Server; + var WebSocketServer = nodeWs.Server; 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..6fbc9908e49d8 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: "ENVIRONMENT_IS_NODE ? {{{ makeNodeImport('node:crypto') }}} : undefined", +#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 0deeb0a8dc810..03e4892db9530 100644 --- a/src/lib/libwasm_worker.js +++ b/src/lib/libwasm_worker.js @@ -289,9 +289,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..710844413c3d7 100644 --- a/src/parseTools.mjs +++ b/src/parseTools.mjs @@ -954,6 +954,20 @@ function makeModuleReceiveWithVar(localName, moduleName, defaultValue) { return ret; } +function makeNodeImport(module) { + if (EXPORT_ES6) { + return `await import('${module}')`; + } + return `require('${module}')`; +} + +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 +1255,8 @@ addToCompileTimeContext({ makeModuleReceive, makeModuleReceiveExpr, makeModuleReceiveWithVar, + makeNodeFilePath, + makeNodeImport, makeRemovedFSAssert, makeRetainedCompilerSettings, makeReturn64, diff --git a/src/preamble.js b/src/preamble.js index 5547fbc7ae302..c7b01e204b35a 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -555,7 +555,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') }}}; // 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 836a097e975e3..e21fdff437d54 100644 --- a/src/runtime_common.js +++ b/src/runtime_common.js @@ -171,7 +171,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') }}}).performance; } #endif diff --git a/src/runtime_debug.js b/src/runtime_debug.js index 7c1170a3b3010..56f41a0c49fd6 100644 --- a/src/runtime_debug.js +++ b/src/runtime_debug.js @@ -15,8 +15,8 @@ function dbg(...args) { // 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'); + var fs = {{{ makeNodeImport('node:fs') }}}; + var utils = {{{ makeNodeImport('node:util') }}}; function stringify(a) { switch (typeof a) { case 'object': return utils.inspect(a); diff --git a/src/shell.js b/src/shell.js index ec40ac6cf3412..3f8ef986d02f7 100644 --- a/src/shell.js +++ b/src/shell.js @@ -107,18 +107,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') }}}; global.Worker = worker_threads.Worker; ENVIRONMENT_IS_WORKER = !worker_threads.isMainThread; #if PTHREADS @@ -129,7 +120,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 @@ -200,11 +190,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') }}}; #if EXPORT_ES6 if (_scriptName.startsWith('file:')) { - scriptDirectory = require('node:path').dirname(require('node:url').fileURLToPath(_scriptName)) + '/'; + var nodePath = {{{ makeNodeImport('node:path') }}}; + var nodeUrl = {{{ makeNodeImport('node:url') }}}; + scriptDirectory = nodePath.dirname(nodeUrl.fileURLToPath(_scriptName)) + '/'; } #else scriptDirectory = __dirname + '/'; @@ -352,7 +344,7 @@ 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') }}}; 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'); diff --git a/src/shell_minimal.js b/src/shell_minimal.js index a291c8130ab2a..1156947231e70 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') }}}; 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') }}}; 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') }}}; #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/test_other.py b/test/test_other.py index a724fff8ef662..fe3ef440306a9 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -465,6 +465,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'(?