Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy Markdown
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
4 changes: 2 additions & 2 deletions src/lib/libnoderawfs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
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
Copy Markdown
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 @@ -570,14 +570,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 @@ -953,6 +953,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 @@ -1240,6 +1261,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 @@ -549,7 +549,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
27 changes: 21 additions & 6 deletions src/shell_minimal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) }}};
global.Worker = worker_threads.Worker;
ENVIRONMENT_IS_WORKER = !worker_threads.isMainThread;
}
Expand Down Expand Up @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -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');
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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);
}
Loading