Skip to content
5 changes: 4 additions & 1 deletion src/lib/libatomic.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'],

Expand Down
8 changes: 6 additions & 2 deletions src/lib/libcore.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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'});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just call this child_process? It seems unlikely enough to collide and is more in common with how most node porgrams name this (I assume)?


var _W_EXITCODE = (ret, sig) => ((ret) << 8 | (sig));

Expand Down
2 changes: 1 addition & 1 deletion src/lib/libnodepath.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
18 changes: 17 additions & 1 deletion src/lib/libsockfs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we still need createRequire in this case?

$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: {},
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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}`);
Expand Down
11 changes: 9 additions & 2 deletions src/lib/libwasi.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion src/lib/libwasm_worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
},
Expand Down
23 changes: 23 additions & 0 deletions src/parseTools.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -1241,6 +1262,8 @@ addToCompileTimeContext({
makeModuleReceive,
makeModuleReceiveExpr,
makeModuleReceiveWithVar,
makeNodeFilePath,
makeNodeImport,
makeRemovedFSAssert,
makeRetainedCompilerSettings,
makeReturn64,
Expand Down
2 changes: 1 addition & 1 deletion src/preamble.js
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
2 changes: 1 addition & 1 deletion src/runtime_common.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 7 additions & 6 deletions src/runtime_debug.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 14 additions & 15 deletions src/shell.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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 + '/';
Expand Down Expand Up @@ -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') }}}
Expand Down
12 changes: 6 additions & 6 deletions src/shell_minimal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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');
}
Expand Down Expand Up @@ -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');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this code (which uses __dirname) simply not work with EXPORT_EST today?

If not, this seems like maybe a separate fix that we could land in isolation. e.g. Fix for EXPORT_ES6 + MINIMAL_RUNTIME + ???

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The __dirname usage in shell_minimal.js indeed doesn't work with EXPORT_ES6 today — it's the same class of issue as require() not being available in ESM.

The fix uses makeNodeFilePath() which switches between:

  • ESM: new URL('file.wasm', import.meta.url)
  • CJS: __dirname + '/file.wasm'

I kept it in this PR because the changes are on the same lines as the require()makeNodeImport() replacements — splitting them would create merge conflicts between the two PRs for no benefit. But happy to separate if you prefer.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If shell_minimal.js already doesn't work it seems fine to leave it out of this change. shell_minimal.js is not ever used by default, and compatibility with all settings is not important here IMHO.

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
}
Expand Down
9 changes: 9 additions & 0 deletions test/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
7 changes: 7 additions & 0 deletions test/require_polyfill.js
Original file line number Diff line number Diff line change
@@ -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);
}
12 changes: 11 additions & 1 deletion test/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand All @@ -5846,6 +5848,7 @@ def test_fs_nodefs_dup(self):

@requires_node
def test_fs_nodefs_home(self):
self.add_require_polyfill()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't love having to inject the polyfill like this.

Can this test instead be updated reference nodePath maybe? (rather that doing require(path) itself)?

self.do_runf('fs/test_nodefs_home.c', 'success', cflags=['-sFORCE_FILESYSTEM', '-lnodefs.js'])

@requires_node
Expand Down Expand Up @@ -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}')
Expand Down
Loading