node: ESM resolver error-code parity — ERR_PACKAGE_PATH_NOT_EXPORTED and friends (+4 tests) - #35711
Draft
cirospaciari wants to merge 16 commits into
Draft
cirospaciari wants to merge 16 commits into
cirospaciari wants to merge 16 commits into
Conversation
The resolver collapsed every exports/imports-map failure, invalid package
name, and malformed package.json to a generic module-not-found before the
JS error was built, so Node's ERR_PACKAGE_PATH_NOT_EXPORTED,
ERR_PACKAGE_IMPORT_NOT_DEFINED, ERR_INVALID_PACKAGE_TARGET,
ERR_INVALID_PACKAGE_CONFIG, and ERR_INVALID_MODULE_SPECIFIER never
surfaced.
- resolver: capture the first Node-shaped failure (status + failing map
key/target detail) in an advisory side channel that never changes the
resolution outcome; only the runtime resolve hooks read it, after a
failed resolve
- ResolveMessage: new preformatted error tags map to Node's exact code,
error-class name, and bracketed display name; message text passes
through untouched
- fail fast on specifiers the default ESM loader can never load:
unsupported URL schemes (ERR_UNSUPPORTED_ESM_URL_SCHEME) and data: MIME
types without a module format (ERR_UNKNOWN_MODULE_FORMAT)
- uncaught tagged errors print Node's trailing { code: '...' } block
- vendor four upstream test/es-module tests this unlocks and register the
es-module directory with the CI runner
Collaborator
|
Updated 11:09 PM PT - Aug 21st, 2026
❌ @robobun, your commit 0cf9e01 has 3 failures in
🧪 To try this PR locally: bunx bun-pr 35711That installs a local version of the PR into your bun-35711 --bun |
Contributor
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
…erflows keep the generic not-found shape - the scheme/data: fail-fast check moves from the shared resolve core to the module loader's entry so Bun.resolveSync and import.meta.resolve still return http:// specifiers unchanged (Node's import.meta.resolve does the same) - exports targets that only overflow Bun's path-length limit after wildcard expansion keep reporting MODULE_NOT_FOUND (Node has no such limit and reports not-found for the expanded path) - update the percent-encoded-separator expectations in resolve.test.ts to the Node contract (TypeError ERR_INVALID_MODULE_SPECIFIER)
cirospaciari
force-pushed
the
claude/node-esm-error-codes
branch
from
July 25, 2026 13:35
50c8469 to
4cb2618
Compare
…ity' into claude/node-esm-error-codes # Conflicts: # src/jsc/VirtualMachine.rs # src/resolver/package_json.rs # src/runtime/jsc_hooks.rs
…ity' into claude/node-esm-error-codes
…clippy lints - src/jsc/bindings/BunHeapProfiler.h: restored (deleted by #36500 on main, still needed for $newCppFunction in src/js/node/v8.ts -> GeneratedJS2Native.h) - src/jsc/web_worker.rs: parent_ref binding was removed by the &self-only refactor merge; use the surrounding unsafe { (*parent).field } pattern - clippy: question_mark/manual_contains in ResolveMessage.rs, redundant_slicing in package_json.rs, unnecessary_lazy_evaluations in BunHeapProfiler.rs
…ity' into claude/node-esm-error-codes # Conflicts: # src/jsc/bindings/BunHeapProfiler.h
…ity' into claude/node-esm-error-codes
steipete
added a commit
to openclaw/bun
that referenced
this pull request
Oct 3, 2026
Retain selected package read and parse failures, reproduce Node's shallow metadata reader, and preserve lazy validation and bundler metadata semantics. Target Node 24.21 for unreadable metadata; both 24.19 and 24.21 reject the malformed dependency fixture. Cover resolution-only scopes, condition arrays, parent error ordering, and CommonJS diagnostics. Adapts oven-sh#33890 and oven-sh#35711. Uses the resolved-key distinction documented by oven-sh#44473 to preserve CommonJS scope rules. Co-authored-by: Ciro Spaciari MacBook <ciro@anthropic.com> Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
steipete
added a commit
to openclaw/bun
that referenced
this pull request
Oct 3, 2026
Runtime resolution currently discards selected malformed package metadata, allowing optional-dependency fallbacks that Node rejects. Retain package read/parse failures until resolution selects the package or its scope, then throw Node's error with the same code, class, path, and importer context. Explicit module extensions, nested scopes, unused conditions, and fields Node ignores keep their observed behavior. Selected dependency reads materialize both package maps and preserve Node's SyntaxError diagnostics, including UTF-16 context. CommonJS self lookup preserves lazy getters; #imports reads imports first. JSON-encoded maps in string fields follow Node's reader. The metadata reader follows Node's shallow package reader, rather than applying strict JSON validation to unused values. Runtime-specific escaped/duplicate-field semantics use a separate metadata view so Bun's bundler retains its existing field handling. ESM resolve-only lookups validate existing .js/.ts/extensionless scopes, with missing-file and explicit-format exemptions. The resolved-key distinction documented by oven-sh#44473 prevents reapplying ESM scope validation when Bun hands an already-resolved CommonJS file to its ESM loader; this does not port that PR's broader loader rewrite. Conditional target arrays continue after invalid/null alternatives and retain the final error. CommonJS missing-module diagnostics now retain parent filenames. This deliberately targets **Node 24.21** for unreadable selected metadata: it throws `ERR_INVALID_PACKAGE_CONFIG`, whereas 24.19 treated read failures as absent metadata. The compatibility documentation records that choice. This version difference is separate from OpenClaw's malformed dependency fixture: both 24.19 and 24.21 throw `ERR_INVALID_PACKAGE_CONFIG` for import and require, whether the dependency body exists or not (eight checks). Adapts the malformed-scope retention approach from oven-sh#33890 (closed, unmerged) and resolver error identity work from oven-sh#35711 (open). Neither is a complete upstream fix for current Node 24 package-reader behavior. Credit to @robobun and @cirospaciari. Validation of final head `0a08f0c3218d10a886b160ebc60eeb97ac847190`: - 944-case Node oracle: import/require across malformed JSON, field values, empty/missing/BOM metadata, explicit formats, nested scopes, and self references; the immutable final-head binary matches outcomes, codes, and normalized messages in all 944 cases. All 178 package-config errors have the expected Error name; Bun's existing ResolveMessage name remains unchanged for generic missing-module errors. - Final regression control: 87 failures on unpatched fork main across 138 tests. The final head passes 737 targeted tests, with 1 existing skip and 1 todo. All 12 Rust targets and formatting pass. The original 944-case oracle, expanded 376-case package-map oracle and 14 getter-order cases all match Node; 1,850 additional JSON diagnostic cases match. Local and required branch Codex P2 reviews are scoped-clean; the branch review uses merge-base `486288f80d`. - Final-head OpenClaw consumer comparison: conditions 40/44 → 42/44 (Node 44/44); interop 56/56 → 56/56; lazy-alias 24/24 → 24/24. The two remaining conditions failures are independently reproduced OpenClaw capture-adapter defects: missing retained symlink alias materialization and premature nested dependency capture during a compiler preview. No OpenClaw source changes. Final proof has zero skips and verifies the binary hash before and after execution. - Existing Linux #79 proof remains valid: lifetime 8/8 in Node and fork main, with end/close in all four socket combinations. It was not rerun for this change. The patch is rebased onto main `486288f80d` (#85), preserving the changelog append-only. Earlier surrounding execution also reproduced three unchanged `esModule-annotation.test.js` failures on the unpatched control; this PR does not claim that broader file is green. Both fork build/test CI lanes passed in https://github.com/openclaw/bun/actions/runs/37096840734 for exact head `0a08f0c3218d10a886b160ebc60eeb97ac847190`. The live merge gate confirms every non-skipped check is successful. Package-map resolution uses an explicit imports/exports context. Local workspace Clippy for Linux and the complete CI Rust lint workflow (Clippy, Mordant, Miri and vendored tests) pass on this exact head. Upstream submission: oven-sh#44512. Its adaptation uses upstream’s existing resolved-key handling and omits fork-only module-hook integration. Co-authored-by: Ciro Spaciari MacBook <ciro@anthropic.com> Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
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.
Stacked on #35527 (
claude/node-uncaught-printer-parity) — that branch owns ResolveMessage's Node-shape surface (name/toString/stack); this PR adds the error-code layer on top. The first commits here are that branch; this PR's diff is the commits after4319cb37c4.What this does
Bun's resolver internally distinguishes every Node ESM failure case (
Status::PackagePathNotExported,InvalidPackageTarget,InvalidModuleSpecifier, …) but collapsed them all toMatchStatus::NotFoundbefore the JS error was built, so every failure surfaced as a generic "Cannot find module/package" withERR_MODULE_NOT_FOUND. This PR threads the real failure reason through to the JS error surface, producing Node v26.3.0's exact codes and message strings:ERR_PACKAGE_PATH_NOT_EXPORTED—Package subpath './x' is not defined by "exports" in <pkg>/package.json[ imported from <base>]/No "exports" main defined in …ERR_PACKAGE_IMPORT_NOT_DEFINED(TypeError) —Package import specifier "#x" is not defined in package <pkg>/package.json imported from <base>ERR_INVALID_PACKAGE_TARGET—Invalid "exports|imports" [main ]target "<t>" defined [for '<key>' ]in the package config …[; targets must start with "./"]ERR_INVALID_MODULE_SPECIFIER(TypeError) — pattern mismatches, encoded/\separators, invalid package names (Node'sparsePackageNamerules), and#/#/imports specifiersERR_INVALID_PACKAGE_CONFIG— unparseable candidate package.json (Invalid package config <path> while importing "<spec>" from <base>.for ESM, trailing-period form for require)MODULE_NOT_FOUNDvsERR_MODULE_NOT_FOUND— exports-resolved targets that don't exist report the resolved filesystem path (Cannot find module '/abs/…/no-such-file.js'), spelled the historic way forrequire()(noRequire stackfor this shape, matching Node'screateEsmNotFoundErr)ERR_UNSUPPORTED_ESM_URL_SCHEME—import('http://…')previously resolved and died withENOENT reading "http://…"; now fails at resolve time with Node's message (incl. the http/https vs other-scheme supported-list wording split)ERR_UNKNOWN_MODULE_FORMAT(RangeError) —import('data:text/plain,…')previously executed the payload as JavaScript; data: URLs whose MIME type has no module format now reject like NodeHow
Advisory capture, zero resolution-behavior change. The resolver records the first Node-shaped failure (message head + failing key/target context from a new
Resolution.detail) in a newResolver.node_module_errorside channel. Nothing in the resolution flow branches on it; the bundler never reads it. The runtime's two resolve hooks clear it before resolving and read it only after a failed resolve, splicing in the referrer clause (whose wording depends on the error kind and import/require) and tagging theMsgwith newbun_ast::Errorvariants.ResolveMessagemaps the tags tocode/name(Error vs TypeError vs RangeError) / bracketed display name, passes the preformatted text through, and the uncaught printer appends Node's{\n code: 'ERR_…'\n}block for tagged errors.This keeps Bun's deliberate leniencies intact: resolution that succeeds where Node would throw (invalid package name that exists on disk,
"#fs": "node:fs"builtin imports targets per #4972, directory imports, JSON imports withoutwith { type: 'json' }) still succeeds — only the error identity of failures changes.The exact fixing line for the "collapse":
handle_esm_resolution's top guard insrc/resolver/resolver.rsnow callscapture_esm_resolution_failurebefore returningMatchStatus::NotFound.Tests
Vendored upstream (byte-identical to v26.3.0; each fails on the unfixed build, passes now, canary-verified to execute):
test/es-module/test-require-module-conditional-exports.jstest/es-module/test-esm-invalid-pjson.jstest/es-module/test-esm-pkgname.mjstest/es-module/test-esm-invalid-data-urls.jsPlus a 4-test matrix in
test/js/bun/resolve/resolve-error.test.tssnapshotting{name, code, message}for 30 import/require failure scenarios — every line oracle-diffed against node v26.3.0 output on the identical fixture tree (the only deliberate divergences:e.constructor.nameis stillResolveMessage, anddata:application/jsonimports stay allowed without an import attribute, Bun policy).scripts/runner.node.mjsregistersjs/node/test/es-module/(same one-line hunk as #35697/#35536; merges clean).Known divergences (documented, matrix tests that need them are not vendored here)
"1234")err.urlown property on ERR_MODULE_NOT_FOUND / ERR_UNSUPPORTED_DIR_IMPORT not yet exposedFollow-up in second commit (review of the full resolve suite)
Bun.resolveSync/import.meta.resolveSynckeep returninghttp://-style specifiers as-is (documented behavior; Node'simport.meta.resolvealso returns them) — the fail-fast scheme/data: check lives only on the module loader's resolve entry (VirtualMachine::resolve)MODULE_NOT_FOUNDshape (Node has no such limit and reports not-found for the expanded path; tagging them INVALID_MODULE_SPECIFIER would diverge)test/js/bun/resolve/resolve.test.tsencoded-separator expectations updated to the Node contract this PR implements (TypeErrorERR_INVALID_MODULE_SPECIFIER, oracle-verified; the invariant — rejection — is unchanged). The undecodable-%%case stays ERR_INVALID_MODULE_SPECIFIER where Node throws a code-less URIError (comment in test).Regression runs (debug build):
test/js/bun/resolvesuite — remaining failures are pre-existing (3runuser: Permission deniedroot-environment failures, and the twoload-same-js-file-a-lottimeouts reproduce identically on a clean build of the base commit, ~7ms/import debug on both).test/js/node/module(93 pass),require-extensions,import-meta, custom-condition/esModule-annotation suites green.bun buildoutput for exports failures unchanged (bundler never reads the capture).