diff --git a/src/lib/libatomic.js b/src/lib/libatomic.js index e4e92a2370b87..60748a0faaf0e 100644 --- a/src/lib/libatomic.js +++ b/src/lib/libatomic.js @@ -172,7 +172,7 @@ addToLibrary({ emscripten_num_logical_cores: () => #if ENVIRONMENT_MAY_BE_NODE - ENVIRONMENT_IS_NODE ? require('node:os').cpus().length : + ENVIRONMENT_IS_NODE ? getBuiltinModule('os').cpus().length : #endif navigator['hardwareConcurrency'], diff --git a/src/lib/libcore.js b/src/lib/libcore.js index e12adf857b47f..8491e182e5933 100644 --- a/src/lib/libcore.js +++ b/src/lib/libcore.js @@ -377,7 +377,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 cp = getBuiltinModule('child_process'); var ret = cp.spawnSync(cmdstr, [], {shell:true, stdio:'inherit'}); var _W_EXITCODE = (ret, sig) => ((ret) << 8 | (sig)); diff --git a/src/lib/libembind_gen.js b/src/lib/libembind_gen.js index 6198845ea22e5..4fdad8d2c4076 100644 --- a/src/lib/libembind_gen.js +++ b/src/lib/libembind_gen.js @@ -924,7 +924,7 @@ var LibraryEmbind = { const printer = new TsPrinter(moduleDefinitions); #endif const output = printer.print(); - var fs = require('node:fs'); + var fs = getBuiltinModule('fs'); fs.writeFileSync(process.argv[2], output + '\n'); }, diff --git a/src/lib/libnodepath.js b/src/lib/libnodepath.js index d891bf7339662..6ddb4c5cc96b9 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: "getBuiltinModule('path')", $PATH__deps: ['$nodePath'], $PATH: `{ isAbs: nodePath.isAbsolute, diff --git a/src/lib/libnoderawfs.js b/src/lib/libnoderawfs.js index 57387ad233674..62653e77efddf 100644 --- a/src/lib/libnoderawfs.js +++ b/src/lib/libnoderawfs.js @@ -10,7 +10,7 @@ addToLibrary({ if (!ENVIRONMENT_IS_NODE) { throw new Error("NODERAWFS is currently only supported on Node.js environment.") } - var nodeTTY = require('node:tty'); + var nodeTTY = getBuiltinModule('tty'); function _wrapNodeError(func) { return (...args) => { try { diff --git a/src/lib/libsockfs.js b/src/lib/libsockfs.js index a9e99be7729ee..5d03b6936cd92 100644 --- a/src/lib/libsockfs.js +++ b/src/lib/libsockfs.js @@ -5,10 +5,25 @@ */ addToLibrary({ +#if ENVIRONMENT_MAY_BE_NODE && EXPORT_ES6 + // ESM has no native `require`. Lazily construct one (node-gated, so it never + // runs during web startup) for loading the non-builtin `ws` module. The call + // site stays a literal `require('ws')` so third-party bundlers can still + // statically discover the dependency. + $require: undefined, + $ensureCreateRequire__deps: ['$require'], + $ensureCreateRequire: () => { + require ??= getBuiltinModule('module').createRequire(import.meta.url); + }, +#endif $SOCKFS__postset: () => { addAtInit('SOCKFS.root = FS.mount(SOCKFS, {}, null);'); }, +#if ENVIRONMENT_MAY_BE_NODE && EXPORT_ES6 + $SOCKFS__deps: ['$FS', '$ensureCreateRequire'], +#else $SOCKFS__deps: ['$FS'], +#endif $SOCKFS: { #if expectToReceiveOnModule('websocket') websocketArgs: {}, @@ -216,6 +231,9 @@ addToLibrary({ var WebSocketConstructor; #if ENVIRONMENT_MAY_BE_NODE if (ENVIRONMENT_IS_NODE) { +#if EXPORT_ES6 + ensureCreateRequire(); +#endif WebSocketConstructor = /** @type{(typeof WebSocket)} */(require('ws')); } else #endif // ENVIRONMENT_MAY_BE_NODE @@ -518,6 +536,9 @@ addToLibrary({ if (sock.server) { throw new FS.ErrnoError({{{ cDefs.EINVAL }}}); // already listening } +#if EXPORT_ES6 + ensureCreateRequire(); +#endif var WebSocketServer = require('ws').Server; var host = sock.saddr; #if SOCKET_DEBUG diff --git a/src/lib/libwasi.js b/src/lib/libwasi.js index c058362e28157..937b5eda551de 100644 --- a/src/lib/libwasi.js +++ b/src/lib/libwasi.js @@ -570,7 +570,7 @@ var WasiLibrary = { #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'); + var nodeCrypto = getBuiltinModule('crypto'); return (view) => (nodeCrypto.randomFillSync(view), 0); } #endif // ENVIRONMENT_MAY_BE_NODE diff --git a/src/lib/libwasm_worker.js b/src/lib/libwasm_worker.js index 35767b196b455..3544e9a26c251 100644 --- a/src/lib/libwasm_worker.js +++ b/src/lib/libwasm_worker.js @@ -295,7 +295,7 @@ if (ENVIRONMENT_IS_WASM_WORKER emscripten_navigator_hardware_concurrency: () => { #if ENVIRONMENT_MAY_BE_NODE - if (ENVIRONMENT_IS_NODE) return require('node:os').cpus().length; + if (ENVIRONMENT_IS_NODE) return getBuiltinModule('os').cpus().length; #endif return navigator['hardwareConcurrency']; }, diff --git a/src/preamble.js b/src/preamble.js index 97f6366910344..828403c04b61c 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -553,7 +553,7 @@ function instantiateSync(file, info) { var binary = getBinarySync(file); #if NODE_CODE_CACHING if (ENVIRONMENT_IS_NODE) { - var v8 = require('node:v8'); + var v8 = getBuiltinModule('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_debug.js b/src/runtime_debug.js index 4752179a8d188..b3b7618599c93 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 = getBuiltinModule('fs'); + var utils = getBuiltinModule('util'); function stringify(a) { switch (typeof a) { case 'object': return utils.inspect(a); diff --git a/src/shell.js b/src/shell.js index ad4fd53554e63..289b55034d735 100644 --- a/src/shell.js +++ b/src/shell.js @@ -104,18 +104,26 @@ if (ENVIRONMENT_IS_PTHREAD) { #endif #endif -#if ENVIRONMENT_MAY_BE_NODE && (EXPORT_ES6 || PTHREADS || WASM_WORKERS) +#if ENVIRONMENT_MAY_BE_NODE +// `process.getBuiltinModule()` loads builtins under both CJS and ESM. On older +// node (pre v18.20.4/v20.16/v22.3) fall back to `require()`. +var getBuiltinModule; if (ENVIRONMENT_IS_NODE) { + if (process['getBuiltinModule']) { + getBuiltinModule = process['getBuiltinModule']; + } else { #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); + getBuiltinModule = (await import('node:module')).createRequire(import.meta.url); +#else + getBuiltinModule = require; #endif + } +} +#endif // ENVIRONMENT_MAY_BE_NODE -#if PTHREADS || WASM_WORKERS - var worker_threads = require('node:worker_threads'); +#if ENVIRONMENT_MAY_BE_NODE && (PTHREADS || WASM_WORKERS) +if (ENVIRONMENT_IS_NODE) { + var worker_threads = getBuiltinModule('worker_threads'); globalThis.Worker = worker_threads.Worker; ENVIRONMENT_IS_WORKER = !worker_threads.isMainThread; #if PTHREADS @@ -126,9 +134,8 @@ 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 && (EXPORT_ES6 || PTHREADS || WASM_WORKERS) +#endif // ENVIRONMENT_MAY_BE_NODE && (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) @@ -197,11 +204,11 @@ 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 = getBuiltinModule('fs'); #if EXPORT_ES6 if (_scriptName.startsWith('file:')) { - scriptDirectory = require('node:path').dirname(require('node:url').fileURLToPath(_scriptName)) + '/'; + scriptDirectory = getBuiltinModule('path').dirname(getBuiltinModule('url').fileURLToPath(_scriptName)) + '/'; } #else scriptDirectory = __dirname + '/'; @@ -346,7 +353,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 = getBuiltinModule('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 9615ea8e7a651..59d29175d59c1 100644 --- a/src/shell_minimal.js +++ b/src/shell_minimal.js @@ -33,6 +33,21 @@ var Module = {{{ EXPORT_NAME }}}; #if ENVIRONMENT_MAY_BE_NODE var ENVIRONMENT_IS_NODE = {{{ nodeDetectionCode() }}}; + +// `process.getBuiltinModule()` loads builtins under both CJS and ESM. On older +// node (pre v18.20.4/v20.16/v22.3) fall back to `require()`. +var getBuiltinModule; +if (ENVIRONMENT_IS_NODE) { + if (process['getBuiltinModule']) { + getBuiltinModule = process['getBuiltinModule']; + } else { +#if EXPORT_ES6 + getBuiltinModule = (await import('node:module')).createRequire(import.meta.url); +#else + getBuiltinModule = require; +#endif + } +} #endif #if ENVIRONMENT_MAY_BE_SHELL @@ -59,7 +74,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 = getBuiltinModule('worker_threads'); globalThis.Worker = worker_threads.Worker; ENVIRONMENT_IS_WORKER = !worker_threads.isMainThread; } @@ -104,7 +119,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 = getBuiltinModule('fs'); defaultPrint = (...args) => fs.writeSync(1, args.join(' ') + '\n'); defaultPrintErr = (...args) => fs.writeSync(2, args.join(' ') + '\n'); } @@ -179,7 +194,7 @@ if (!ENVIRONMENT_IS_PTHREAD) { // Wasm or Wasm2JS loading: if (ENVIRONMENT_IS_NODE) { - var fs = require('node:fs'); + var fs = getBuiltinModule('fs'); #if WASM == 2 if (globalThis.WebAssembly) Module['wasm'] = fs.readFileSync(__dirname + '/{{{ TARGET_BASENAME }}}.wasm'); else eval(fs.readFileSync(__dirname + '/{{{ TARGET_BASENAME }}}.wasm.js')+''); diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index 8b8a7d2739452..cbb49aa0e954b 100644 --- a/test/codesize/test_codesize_hello_dylink_all.json +++ b/test/codesize/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { - "a.out.js": 268278, + "a.out.js": 268326, "a.out.nodebug.wasm": 587640, - "total": 855918, + "total": 855966, "sent": [ "IMG_Init", "IMG_Load", diff --git a/test/codesize/test_unoptimized_code_size.json b/test/codesize/test_unoptimized_code_size.json index a97b0aed49593..5e1744befc19e 100644 --- a/test/codesize/test_unoptimized_code_size.json +++ b/test/codesize/test_unoptimized_code_size.json @@ -1,16 +1,16 @@ { - "hello_world.js": 55076, - "hello_world.js.gz": 17352, + "hello_world.js": 55410, + "hello_world.js.gz": 17457, "hello_world.wasm": 15115, "hello_world.wasm.gz": 7464, - "no_asserts.js": 25903, - "no_asserts.js.gz": 8768, + "no_asserts.js": 26237, + "no_asserts.js.gz": 8876, "no_asserts.wasm": 12229, "no_asserts.wasm.gz": 6004, - "strict.js": 52833, - "strict.js.gz": 16583, + "strict.js": 53167, + "strict.js.gz": 16688, "strict.wasm": 15115, "strict.wasm.gz": 7461, - "total": 176271, - "total_gz": 63632 + "total": 177273, + "total_gz": 63950 } diff --git a/test/fs/test_nodefs_home.c b/test/fs/test_nodefs_home.c index b4e4e71ddbccd..13c7813ba09dd 100644 --- a/test/fs/test_nodefs_home.c +++ b/test/fs/test_nodefs_home.c @@ -11,7 +11,7 @@ int main(void) { EM_ASM( - var path = require("path"); + var path = process.getBuiltinModule("path"); var home = process.env.HOME; // On Windows HOME environment variable doesn't exist, but concatenating HOMEDRIVE and HOMEPATH // does the same thing. diff --git a/test/fs/test_nodefs_rw.c b/test/fs/test_nodefs_rw.c index 895345761993e..c03625a311da2 100644 --- a/test/fs/test_nodefs_rw.c +++ b/test/fs/test_nodefs_rw.c @@ -18,7 +18,7 @@ int main() { // write something locally with node EM_ASM( - var fs = require('fs'); + var fs = process.getBuiltinModule('fs'); fs.writeFileSync('foobar.txt', 'yeehaw'); ); @@ -42,7 +42,7 @@ int main() { // validate the changes were persisted to the underlying fs EM_ASM( - var fs = require('fs'); + var fs = process.getBuiltinModule('fs'); var contents = fs.readFileSync('foobar.txt', { encoding: 'utf8' }); assert(contents === 'cheez'); ); diff --git a/test/test_other.py b/test/test_other.py index 93a124c51e40e..6dc687ea07376 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -1665,16 +1665,14 @@ def test_minimal_runtime_export_all_modularize(self): self.emcc('main.c', ['-sMODULARIZE=1', '-sMINIMAL_RUNTIME=2', '-sEXPORT_ALL', '-sEXPORT_ES6', '-o', 'test.mjs']) - # We must expose __dirname and require globally because emscripten - # uses those under the hood. + # We must expose __dirname globally because emscripten uses it under the + # hood (builtin modules are loaded via process.getBuiltinModule). create_file('main.mjs', ''' import { dirname } from 'node:path'; - import { createRequire } from 'node:module'; import { fileURLToPath } from 'node:url'; // `fileURLToPath` is used to get a valid path on Windows. globalThis.__dirname = dirname(fileURLToPath(import.meta.url)); - globalThis.require = createRequire(import.meta.url); import Test from './test.mjs'; async function main() { diff --git a/tools/file_packager.py b/tools/file_packager.py index f35a5e413dc8d..1fc6cd8ba9fdb 100755 --- a/tools/file_packager.py +++ b/tools/file_packager.py @@ -627,11 +627,16 @@ def generate_preload_js(data_target, data_files, metadata): if options.support_node: ret += " var isNode = globalThis.process && globalThis.process.versions && globalThis.process.versions.node && globalThis.process.type != 'renderer';\n" - if options.support_node and options.export_es6: + if options.support_node: ret += '''if (isNode) { - const { createRequire } = await import('node:module'); - /** @suppress{duplicate} */ - var require = createRequire(import.meta.url); + if (process['getBuiltinModule']) { + var getBuiltinModule = process['getBuiltinModule']; + } else {\n''' + if options.export_es6: + ret += ''' var getBuiltinModule = (await import('node:module')).createRequire(import.meta.url);\n''' + else: + ret += ''' var getBuiltinModule = require;\n''' + ret += ''' } }\n''' if options.export_es6: @@ -910,7 +915,7 @@ def generate_preload_js(data_target, data_files, metadata): if options.support_node: node_support_code = ''' if (isNode) { - var contents = require('fs').readFileSync(packageName); + var contents = getBuiltinModule('fs').readFileSync(packageName); return new Uint8Array(contents).buffer; }'''.strip() @@ -1045,7 +1050,7 @@ def generate_preload_js(data_target, data_files, metadata): if options.support_node: node_support_code = ''' if (isNode) { - var contents = require('fs').readFileSync(metadataUrl, 'utf8'); + var contents = getBuiltinModule('fs').readFileSync(metadataUrl, 'utf8'); // The await here is needed, even though JSON.parse is a sync API. It works // around a issue with `removeRunDependency` otherwise being called to early // on the metadata object.