Repository navigation
node: implement node:wasi v26 WASI class surface (+23 tests) - #35709
Draft
cirospaciari wants to merge 15 commits into
Draft
cirospaciari wants to merge 15 commits into
cirospaciari wants to merge 15 commits into
Conversation
Rework the node:wasi entry point around Node v26's lib/wasi.js surface,
keeping the existing JS preview1 host functions as the backing engine:
- constructor: options/version/args/env/preopens/stdin/stdout/stderr/
returnOnExit validation with Node's exact error codes and check order;
version is now required ('preview1' or 'unstable')
- start()/initialize()/finalizeBindings()/getImportObject() with Node's
validation messages, ERR_WASI_ALREADY_STARTED on double start, and
returnOnExit (default true) implemented via a thrown sentinel so
start() returns the exit code
- host functions throw ERR_WASI_NOT_STARTED until an instance memory is
attached; failed preopen opens surface as UVWASI_<errno> errors like
uvwasi_init
- module load emits the WASI ExperimentalWarning (suppressed for the
built-in .wasm runner, which promises clean stderr)
Engine fixes found by Node's upstream wasi tests:
- fd_renumber had dup2 backwards: it moved 'to' into 'from' and closed
the freshly opened fd, breaking freopen()
- poll_oneoff wrote 16-byte event records; preview1 events are 32 bytes,
so multi-subscription polls corrupted the guest event array
- poll_oneoff mixed BigInt and Number when charging elapsed time,
throwing on any clock subscription (sleep/nanosleep)
- clock_res_get wrote through a stale/absent DataView; sock_accept was
missing (accept() on a bad fd now reports EBADF)
ERR_INVALID_ARG_TYPE with a single expected string now follows Node's
formatList buckets: 'undefined' renders 'must be undefined', class names
render 'must be an instance of X'. Pre-flattened list strings keep the
legacy 'of type' rendering.
The bun .wasm runner passes version/returnOnExit explicitly and calls
initialize() for reactor modules, preserving its exit-code behavior.
Vendor test/wasi from Node v26.3.0 byte-verbatim, including the wasm fixtures compiled from C, and refresh fixtures/wasi-preview-1.js to the v26 version (the previously vendored copy predated the threads target and parseArgs handling). Add js/node/test/wasi/ to the node-test runner's inclusion predicate; verified with a deliberately failing canary that the directory is collected and executed. Not vendored (fail on Bun): - test-wasi-poll.js: fd readiness (POLLOUT/POLLHUP) needs real poll(2); the JS host over node:fs cannot detect pipe hangup without consuming - test-wasi-pthread.js: wasm shared memory is not enabled in JSC here (WebAssembly.Module rejects shared memories); flaky upstream on Linux - test-wasi-worker-terminate.js: worker.terminate() cannot interrupt a wasm busy-loop, the process wedges and ignores SIGTERM
Collaborator
Contributor
|
Found 3 issues this PR may fix:
🤖 Generated with Claude Code |
Contributor
…aude/node-wasi # Conflicts: # src/jsc/bindings/ErrorCode.ts
…aude/node-wasi # Conflicts: # src/jsc/bindings/ErrorCode.cpp
- restore src/jsc/bindings/BunHeapProfiler.h (deleted by #36500 on main, still needed by $newCppFunction sites in src/js/node/v8.ts) - web_worker.rs: parent_ref was removed by #36571; deref parent directly - path.rs: re-expose resolve_{posix,windows}_t as pub(crate) for permission.rs - clap: re-expose Diagnostic fields as pub for Arguments.rs node-compat errors - permission.rs: switch std::sync::RwLock -> bun_threading::RwLock, std::env::var -> bun_core::env_var::NODE_OPTIONS (disallowed_types/methods) - run_command.rs: exec_check pub -> pub(crate) (unreachable_pub) - BunHeapProfiler.rs: then(|| ..) -> then_some (unnecessary_lazy_evaluations) - Timer.rs: move SAFETY comment onto the unsafe block it documents
…aude/node-wasi # Conflicts: # src/jsc/bindings/BunHeapProfiler.h
This was referenced Aug 15, 2026
Under --interactive the builtins sit on globalThis as lazy custom-value properties. getOwnPropertyDescriptor materializes each one, which loaded every builtin at REPL startup and let node:wasi print its ExperimentalWarning into sessions that never touched it.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
Implements Node v26's
node:wasiWASIclass contract on top of Bun's existing JS preview1 host functions, and vendors Node's upstreamtest/wasi/suite. 23 of Node's 26 wasi tests pass verbatim (0 passed before — there was no Node-compatible WASI surface).What changed
src/js/node/wasi.ts— newWASIclass wrapping the existing wasi-js engine, mirroringlib/wasi.jsfrom Node v26.3.0:versionof'preview1'|'unstable',argsarray,env/preopensobjects, int32stdin/stdout/stderr, booleanreturnOnExit) with Node's exact error codes and check orderingstart()/initialize()/finalizeBindings()/getImportObject();_start/_initialize/memoryexport validation with Node's exact messages;ERR_WASI_ALREADY_STARTEDon double startreturnOnExitdefaults to true:proc_exitrecords the code and throws a sentinel WebAssembly can't catch,start()returns the exit codeERR_WASI_NOT_STARTED(wasi.start() has not been called) until instance memory is attachedUVWASI_<errno>/uvwasi_initerrors, matching uvwasi's failure shapeExperimentalWarningon module load, matching Node (suppressed when Bun itself runs a.wasmentry point — the built-in runner promises clean stderr, covered bytest/js/bun/wasm/wasi.test.js)Engine bugs found by the upstream tests (each covered by a vendored test or a new unit test in
test/js/bun/wasm/wasi.test.js):fd_renumberhad dup2 backwards — it movedtointofromand closed the freshly opened fd, breaking wasi-libcfreopen()(test-wasi-io)poll_oneoffwrote 16-byte event records; preview1 events are 32 bytes, so any multi-subscription poll corrupted the guest event array (unit test)poll_oneoffthrewInvalid mix of BigInt and other typeon every clock subscription, breakingsleep()/nanosleep()(unit test)clock_res_getwrote through an absent DataView (test-wasi-clock_getres);sock_acceptwas missing —accept()on a bad fd now reportsEBADF(test-wasi-sock)src/jsc/bindings/ErrorCode.cpp— the single-expected-stringERR_INVALID_ARG_TYPEoverload now follows Node'sformatListbuckets:'undefined'rendersmust be undefined(required by test-wasi-start-validation / test-wasi-initialize-validation), class names rendermust be an instance of X. Pre-flattened"X, Y, or Z"strings keep the legacyof typerendering, so existing list-style call sites are unchanged. No in-repo test asserts the old renderings (grepped).src/js/wasi-runner.js— thebun foo.wasmrunner passesversion: "preview1"andreturnOnExit: falseexplicitly and callsinitialize()for modules without_start, preserving its historical exit-code and stderr behavior.scripts/runner.node.mjs—js/node/test/wasi/added to the node-test inclusion predicate. Verified with a deliberately failing canary that the directory is collected and executed by the runner (then removed the canary), and that a real wasi test passes under the runner.Tests
Vendored byte-verbatim from Node v26.3.0 (
test/wasi/+ wasm fixtures + refreshedfixtures/wasi-preview-1.js): 23 files, all passing CI-style (BUN_GARBAGE_COLLECTOR_LEVEL=1,bunfig.node-test.toml, ASAN debug build). The diff adds 47 files undertest/js/node/test/wasi/: the 23 test files plus the 24.wasmbinaries they load fromwasi/wasm/, which are not tests themselves.test/js/bun/wasm/wasi.test.jsupdated for the requiredversionoption and extended with 2 regression tests for the poll/renumber fixes; all 7 pass.Dropped (fail on Bun, not vendored):
test-wasi-poll.js— fd readiness (POLLOUT / stdin POLLHUP) needs realpoll(2); a JS host overnode:fscannot detect pipe hangup without consuming datatest-wasi-pthread.js— wasm shared memory not enabled in JSC here (WebAssembly.Module doesn't parse: shared memory is not enabled); flaky upstream on Linuxtest-wasi-worker-terminate.js—worker.terminate()cannot interrupt a wasm busy-loop; the process wedges and ignores SIGTERMBehavior notes
new WASI()withoutversionnow throwsERR_INVALID_ARG_TYPE, matching Node ≥20 (previously Bun ignoredversionentirely).test-wasi-options-validation.jsrequires this exact behavior.