Skip to content

plugin: consult both onResolve registration forms for namespaced specifiers - #36402

Open
robobun wants to merge 8 commits into
mainfrom
farm/144305cc/plugin-onresolve-crossed-dispatch
Open

robobun wants to merge 8 commits into
mainfrom
farm/144305cc/plugin-onresolve-crossed-dispatch

Conversation

@robobun

@robobun robobun commented Jul 29, 2026 •

Copy link
Copy Markdown
Collaborator

What

For an import like import "virt:x", the runtime module loader and Bun.build consulted disjoint onResolve registrations:

registration runtime import "virt:x" Bun.build
onResolve({ filter: /^virt:/ }) never fires fires, args.path = "virt:x"
onResolve({ filter: /.*/, namespace: "virt" }) fires, args.path = "x" never fires

So a plugin written for esbuild (filter: /^virt:/) bundled fine and failed at runtime with Cannot find package 'virt:x'; one written against the runtime's namespace parsing ran fine and failed Bun.build with Could not resolve: "virt:x". The only workaround was to register both forms.

Repro:

const setup = b => {
  b.onResolve({ filter: /^virt:/ }, () => ({ path: "a", namespace: "va" }));            // (A)
  b.onResolve({ filter: /.*/, namespace: "virt" }, () => ({ path: "b", namespace: "vb" })); // (B)
  b.onLoad({ filter: /.*/, namespace: "va" }, () => ({ contents: `export default "VIA-A";`, loader: "js" }));
  b.onLoad({ filter: /.*/, namespace: "vb" }, () => ({ contents: `export default "VIA-B";`, loader: "js" }));
};
Bun.plugin({ name: "dual", setup });
console.log("RUNTIME", (await import("virt:x")).default);          // VIA-B
require("fs").writeFileSync("/tmp/xe.ts", `import x from "virt:x"; console.log(x);`);
const r = await Bun.build({ entrypoints: ["/tmp/xe.ts"], plugins: [{ name: "dual", setup }] });
console.log("BUILD  ", (await r.outputs[0].text()).match(/VIA-[AB]/)[0]);  // VIA-A

Cause

  • Runtime: BunPlugin::OnResolve::run looks up the group for the parsed prefix namespace and runs only that group. If nothing is registered under that namespace it returns early; the default-namespace (file) group is never consulted for ns:... specifiers.
  • Bundler: anyMatchesCrossThread / runOnResolvePlugins key the lookup on the importer's namespace (always file for a first-level import) and test filters against the full specifier. A callback registered under namespace: "virt" is never reached.

Fix

Make each side fall back to the other form when its primary lookup produces no match:

  • BunPlugin::OnResolve::run (runtime): after trying the prefix-namespace group, also try the default-namespace group with the reconstructed full specifier ns:path.
  • BundlerPlugin::anyMatchesCrossThread (bundler pre-filter, onResolve only): when the importer namespace is file/empty and the specifier has a ns: prefix, also test the ns group against the stripped path.
  • runOnResolvePlugins (bundler JS dispatch): after the importer-namespace pass, try the prefix-namespace group with the stripped path.

The previous primary lookup runs first in each context, so plugins that register both forms (the current workaround) keep their existing behavior. A single-letter X: prefix is treated as a drive letter and skipped, consistent with the existing checks.

Testing

test/js/bun/plugin/plugin-onresolve-namespace-prefix.test.ts spawns a subprocess per case (form A only / form B only / both) and asserts that both the runtime import and Bun.build resolve through the plugin, with the expected args.path in each form.

(pass) onResolve({ filter: /^virt:/ }) resolves `import "virt:x"` at runtime and in Bun.build
(pass) onResolve({ namespace: "virt" }) resolves `import "virt:x"` at runtime and in Bun.build
(pass) registering both forms resolves `import "virt:x"` at runtime and in Bun.build

Existing plugin suites (test/js/bun/plugin/plugins.test.ts, test/bundler/bundler_plugin.test.ts, test/bundler/bundler_plugin_chain.test.ts, test/bundler/bun-build-api.test.ts) pass unchanged.

Fixes #9863


[review] gate passed · iteration 6 · 4 files touched

fails on main (without fix)
ASAN without fix: 3 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/plugin/plugin-onresolve-namespace-prefix.test.ts
bun test v1.4.1 (adc354d99)

test/js/bun/plugin/plugin-onresolve-namespace-prefix.test.ts:
62 | }
63 | 
64 | describe.concurrent("onResolve namespace-prefix dispatch is consistent between runtime and Bun.build", () => {
65 |   test('onResolve({ filter: /^virt:/ }) resolves `import "virt:x"` at runtime and in Bun.build', async () => {
66 |     const { stdout, stderr, exitCode } = await run("A");
67 |     expect(stderr).toBe("");
                        ^
error: expect(received).toBe(expected)

- ""
+ "error: Cannot find package 'virt:thing' from '/tmp/plugin-onresolve-ns_ltDgBN/index.ts'
+ 
+ Bun v1.4.1-debug+adc354d99 (Linux x64)
+ "

- Expected  - 1
+ Received  + 4

      at <anonymous> (/workspace/bun/test/js/bun/plugin/plugin-onresolve-namespace-prefix.test.ts:67:20)
(fail) onResolve namespace-prefix dispatch is consistent between runtime and Bun.build > onResolve({ filter: /^virt:/ }) resolves `import "virt:x"` at runtime and in Bun.build [398.18ms]
127 |       env: bunEn
... (truncated)

release without fix: all passed
bun test v1.4.1-canary.1 (9ecb46465)

test/js/bun/plugin/plugin-onresolve-namespace-prefix.test.ts:
(pass) onResolve namespace-prefix dispatch is consistent between runtime and Bun.build > onResolve({ namespace: "virt" }) returning { path } without namespace resolves to a file on disk in both [11.49ms]
(pass) onResolve namespace-prefix dispatch is consistent between runtime and Bun.build > onResolve({ namespace: "virt" }) resolves `import "virt:x"` at runtime and in Bun.build [14.38ms]
(pass) onResolve namespace-prefix dispatch is consistent between runtime and Bun.build > onResolve({ filter: /^virt:/ }) resolves `import "virt:x"` at runtime and in Bun.build [16.26ms]
(pass) onResolve namespace-prefix dispatch is consistent between runtime and Bun.build > registering both forms keeps each context's existing primary lookup [13.35ms]

 4 pass
 0 fail
 22 expect() calls
Ran 4 tests across 1 file. [106.00ms]
__F:0:S:0
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/plugin/plugin-onresolve-namespace-prefix.test.ts
bun test v1.4.1 (adc354d99)

test/js/bun/plugin/plugin-onresolve-namespace-prefix.test.ts:
(pass) onResolve namespace-prefix dispatch is consistent between runtime and Bun.build > onResolve({ namespace: "virt" }) returning { path } without namespace resolves to a file on disk in both [542.48ms]
(pass) onResolve namespace-prefix dispatch is consistent between runtime and Bun.build > onResolve({ filter: /^virt:/ }) resolves `import "virt:x"` at runtime and in Bun.build [812.15ms]
(pass) onResolve namespace-prefix dispatch is consistent between runtime and Bun.build > registering both forms keeps each context's existing primary lookup [729.39ms]
(pass) onResolve namespace-prefix dispatch is consistent between runtime and Bun.build > onResolve({ namespace: "virt" }) resolves `import "virt:x"` at runtime and in Bun.build [746.04ms]

 4 pass
 0 fail
 22 expect() calls
Ran 4 tests across 1 file. [3.05s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 717ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/22] gen cpp.rs (cppbind)
[2/22] gen JS modules (bundle-modules)
Preprocess modules (7921ms)
Bundle modules (49ms)
Postprocesss modules (30ms)
Bundle Functions (477ms)
Generate Code (38ms)

[8.53s] Bundled "src/js" for production
  2595 kb
  197 internal modules
  13 native modules
  50 internal functions across 16 files
[2/11] cargo bun_runtime → libbun_runtime.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m   Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m   Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
�[1m�[92m   Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
�[1m�[92m   Compiling�[0m bun_base64 v0.0.0 (/workspace/bun/src/base64)
�[1m�[92m   Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares
... (truncated)
diff hotspot
src/js/builtins/BundlerPlugin.ts                   | 143 +++++++++++++--------
 src/jsc/bindings/BunPlugin.cpp                     |  37 ++++--
 src/jsc/bindings/JSBundlerPlugin.cpp               |  36 ++++--
 .../plugin-onresolve-namespace-prefix.test.ts      | 137 ++++++++++++++++++++
 4 files changed, 276 insertions(+), 77 deletions(-)

gate history · 6 passed · 0 rejected · iteration 6

evidence per changed file
file                                                      reads  edits  tests
src/js/builtins/BundlerPlugin.ts                              6      7      0
src/jsc/bindings/BunPlugin.cpp                                5      5      0
src/jsc/bindings/JSBundlerPlugin.cpp                          4      4      0
…js/bun/plugin/plugin-onresolve-namespace-prefix.test.ts      3      4      0

@coderabbitai

coderabbitai Bot commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

The bundler plugin now validates resolver results through a shared async path and supports ns:rest namespace-prefixed resolution. Runtime bindings and cross-thread matching use normalized strings and equivalent fallback matching. Tests cover regex and explicit namespace registrations across runtime imports and Bun.build.

Namespace-prefixed resolution

Layer / File(s) Summary
Namespace matching and input normalization
src/jsc/bindings/JSBundlerPlugin.cpp, src/jsc/bindings/BunPlugin.cpp
Matching converts inputs once, selects configured filters, and recognizes namespace-prefixed paths while guarding Windows drive-letter paths.
Callback dispatch and result validation
src/js/builtins/BundlerPlugin.ts, src/jsc/bindings/BunPlugin.cpp
Resolver callbacks validate paths, namespaces, external results, and namespace-specific constraints, then retry file inputs using ns:rest dispatch.
Runtime and bundling coverage
test/js/bun/plugin/plugin-onresolve-namespace-prefix.test.ts
Tests compare regex and explicit namespace handlers across runtime imports and Bun.build, including filesystem path results.

Possibly related issues

Suggested reviewers: jarred-sumner, sosukesuzuki

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main change: consulting both onResolve registration forms for namespaced specifiers.
Description check ✅ Passed The description fully explains the problem, cause, fix, compatibility behavior, and verification results. It uses different headings from the template but includes the required change summary and test…
Full details: Description check

Explanation

The description fully explains the problem, cause, fix, compatibility behavior, and verification results. It uses different headings from the template but includes the required change summary and testing information.


Comment @coderabbitai help to get the list of available commands.

Comment thread src/js/builtins/BundlerPlugin.ts Outdated
Comment thread src/jsc/bindings/BunPlugin.cpp Outdated
Comment thread src/jsc/bindings/JSBundlerPlugin.cpp Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. Bun runtime plugin onResolve doesn't filter non-file: protocol imports #9863 - Runtime onResolve({ filter: /^demo:/ }) (esbuild-style, no explicit namespace) never fires for import "demo:2", which is exactly the runtime-side fallback this PR adds

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #9863

🤖 Generated with Claude Code

Comment thread src/jsc/bindings/BunPlugin.cpp Outdated
Comment thread src/js/builtins/BundlerPlugin.ts Outdated
Comment thread src/jsc/bindings/JSBundlerPlugin.cpp Outdated
Comment thread test/js/bun/plugin/plugin-onresolve-namespace-prefix.test.ts Outdated
Comment thread src/js/builtins/BundlerPlugin.ts Outdated
Comment thread src/js/builtins/BundlerPlugin.ts Outdated
Comment thread test/js/bun/plugin/plugin-onresolve-namespace-prefix.test.ts Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No new issues found — the six earlier findings (result-namespace default, BunString leak on the empty-list early return, drive-letter guard, file: prefix double-dispatch, describe.concurrent, and the toBe ordering assertions) are all addressed in c395e86 and c6f6a25. Deferring because this changes user-facing plugin dispatch semantics: each context still prefers a different primary form (runtime→{namespace:"virt"} with stripped path, bundler→{filter:/^virt:/} with full specifier), and the test now locks that asymmetry in — worth a maintainer confirming that's the intended contract vs. unifying on one.

What was reviewed

  • BunPlugin::OnResolve::run fallback: checked both Rust callers normalize "file"→empty before the call, so !nsString.isEmpty() cannot re-run fileNamespace for file-namespace imports.
  • anyMatchesCrossThread: transferToWTFString() now precedes the empty-list return; the #if OS(WINDOWS) drive-letter skip requires a following separator, matching extract_namespace.
  • runOnResolvePlugins: userNamespace defaults to inputNamespace; prefix !== "file" prevents re-running the default group for file: specifiers.
Extended reasoning...

Overview

The PR makes runtime Bun.plugin and Bun.build each fall back to the other onResolve registration form when their primary lookup misses, so a plugin registered as either {filter: /^virt:/} or {filter: /.*/, namespace: "virt"} handles import "virt:x" in both contexts. It touches three dispatch sites — BunPlugin::OnResolve::run (runtime C++), BundlerPlugin::anyMatchesCrossThread (bundler C++ pre-filter), and runOnResolvePlugins (bundler builtin JS) — plus a new 4-case subprocess test file.

Security risks

None identified. The change is scoped to plugin callback dispatch; no new untrusted input parsing beyond splitting a specifier at :. The drive-letter guard is Windows-gated and requires a following separator, matching the existing extract_namespace. No path escaping, credential, or privilege surface is touched.

Level of scrutiny

High. This is a user-facing API-semantics change to the plugin system across both the runtime module loader and the bundler, touching C++ JSC bindings (exception scopes, BunString ownership) and a hot-path builtin. Two prior review rounds surfaced six issues (a result-namespace default divergence, a WTFStringImpl leak from a reordered early return, an over-broad drive-letter skip, a file:-prefix double-dispatch in the JS Map, serial subprocess tests, and vacuous toContain assertions); all are fixed in c395e86/c6f6a255 and I re-verified each against the current diff. I also traced both Rust callers (PluginRunner::on_resolve, plugin_runner_on_resolve_jsc) to confirm they normalize "file"→empty before Bun__runOnResolvePlugins, so the new C++ fallback's !nsString.isEmpty() gate cannot re-run fileNamespace for file-namespace imports.

Other factors

The remaining reason to defer is design, not correctness: the fix preserves each context's existing primary lookup (runtime prefers the namespace-keyed group with a stripped path; bundler prefers the file-namespace group with the full specifier) and adds the other as a fallback. That means a plugin registering both forms still sees different args.path shapes in each context, and the "both forms" test now pins runtime === VIA-B / bundled === VIA-A. That's a defensible backward-compat choice, but it's a contract a maintainer should explicitly sign off on rather than have it locked in by a bot approval.

@robobun

robobun commented Jul 29, 2026 •

Copy link
Copy Markdown
Collaborator Author

CI status: the diff itself is green. The new plugin-onresolve-namespace-prefix.test.ts and the existing plugin/bundler suites pass on every lane that ran.

Rebased onto main at 9ecb464. One trivial conflict in BunPlugin.cpp: main changed OnResolve::run to take const BunString* parameters and added a RETURN_IF_EXCEPTION after the filter regex match. Both carried into the runOnResolveGroup helper. No logic changes.

51e3a31: runOnResolvePlugins now peeks the tryNamespace promise before awaiting, so a synchronous plugin callback keeps the pre-existing synchronous fast path (review follow-up).

Latest build 106428 has two non-flaky failures, both also present in 106412, both pre-existing on main and unrelated to this change:

  • test/bundler/bun-build-api.test.ts (debian-13-x64, ubuntu-25.04-x64): three BuildArtifact properties snapshot hashes (est79qzq vs g9c33042). The same hash is produced locally with this PR's src/ changes reverted to main, so the bundled output bytes changed upstream and the snapshot was not updated. Reported separately.
  • test/js/web/url/url.test.ts (darwin-x64): TypeError: Invalid URL for http://foo\u180E:80/ in the Unicode 16 IDNA table test. ICU/URL parser. Reported separately.

Everything else in 106428 is marked flaky. 106412 also had require-cache.test.ts (x64-asan timeout, reported separately). Earlier builds 85482 and 85500 failed only on unrelated flakes and infra.

Ready for review.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All six issues from the earlier review rounds are addressed in c395e86 and c6f6a25 — I have nothing further to flag. Deferring to a human because this is a user-facing plugin-dispatch semantics change coordinated across three separate codepaths (runtime C++, bundler C++ pre-filter, bundler JS builtins), and the deliberate choice to keep different primary orderings per context (runtime prefers the namespace group, Bun.build prefers the file group) is worth a maintainer sign-off.

What was reviewed: the tryNamespace refactor preserves the original loop's result handling and userNamespace default; transferToWTFString() now runs before every early return in anyMatchesCrossThread; the drive-letter skip is Windows-gated and requires a following separator in both the JS and C++ fallbacks; the prefix !== "file" guard prevents re-running the default group on file: specifiers; the both-forms test now asserts exact primary-wins ordering.

Extended reasoning...

Overview

The PR makes onResolve dispatch consistent between the runtime module loader and Bun.build for ns:path-style specifiers by having each side fall back to the other registration form when its primary lookup produces no match. It touches BunPlugin::OnResolve::run (runtime C++, refactored into a shared runOnResolveGroup helper plus a file-namespace fallback), BundlerPlugin::anyMatchesCrossThread (bundler C++ pre-filter, refactored to hoist transferToWTFString and add a prefix-namespace fallback), and runOnResolvePlugins in BundlerPlugin.ts (bundler JS dispatch, refactored into a tryNamespace closure plus a prefix-namespace fallback). A new 4-case subprocess test covers form-A-only, form-B-only, both-forms, and { path }-without-namespace.

Security risks

None identified. The change only affects which registered plugin callbacks are consulted for a given specifier; no untrusted-input parsing, no filesystem/network effects, no auth or crypto surface.

Level of scrutiny

Moderate-to-high. This is not a mechanical fix — it changes plugin API semantics in a way that affects every ns:-style import handled by a plugin, and it does so across three independently-maintained implementations that must agree. The refactor in BunPlugin.cpp also changes how the path argument is materialized (jsString(vm, pathString) instead of Bun::toJS(globalObject, *path)), which I traced as behavior-preserving. The intentional asymmetry (runtime primary = namespace group, bundler primary = file group) is sound for backward-compat but is a design call a maintainer should confirm.

Other factors

Two prior review rounds surfaced a result-namespace default regression, a WTFStringImpl leak on the empty-list early return, an over-broad drive-letter skip, a file:-prefix double-dispatch, a serial-test nit, and a weak both-forms assertion — all fixed and verified in the current diff. The bug-hunting pass on the latest revision found nothing new. Tests are hermetic (tempDir + Bun.spawn), concurrent, and assert stderr/exit code plus exact per-form outputs. Given the scope and the API-surface nature of the change, I'm deferring rather than approving.

@robobun

robobun commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator Author

This also fixes the reported repro in #40579 (a full-specifier filter like /^virtual:thing$/ at runtime). Verified locally together with #40398. Note: the branch now conflicts with main in src/jsc/bindings/BunPlugin.cpp because #40404 made the BunString parameters const. The resolution is mechanical: add const to the new runOnResolveGroup and OnResolve::run signatures.

robobun and others added 7 commits August 26, 2026 22:28
…ifiers

For an import like "virt:x", Bun.plugin at runtime would only consult
onResolve callbacks registered with { namespace: "virt" } (passing the
stripped path "x"), while Bun.build would only consult callbacks
registered with { filter: /^virt:/ } in the default namespace (passing
the full "virt:x"). A plugin written one way worked in one context and
failed in the other.

The runtime now falls back to the default-namespace group with the full
specifier when the prefix-namespace group has no match. Bun.build's
onResolve pre-filter and dispatch now also consult the prefix namespace
with the stripped path when the importer-namespace group has no match.

In each context the previous primary lookup runs first, so existing
plugins (including those that register both forms as a workaround) keep
their current behavior.
…trings before early return, Windows-only drive-letter skip, concurrent tests
@robobun
robobun force-pushed the farm/144305cc/plugin-onresolve-crossed-dispatch branch from 0c25495 to 9ecb464 Compare August 26, 2026 22:32
@robobun

robobun commented Aug 26, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 4:22 PM PT - Aug 26th, 2026

❌ @robobun, your commit 51e3a31 has 2 failures in Build #106428 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 36402

That installs a local version of the PR into your bun-36402 executable, so you can run:

bun-36402 --bun

Comment thread src/js/builtins/BundlerPlugin.ts Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-reviewed after the latest push — all seven earlier inline points are now addressed in the code, and this pass found no further bugs. Since this changes user-facing plugin dispatch semantics (which onResolve registration form fires for ns:... specifiers, and in what order), a maintainer sign-off on the behavior itself is still worthwhile.

What was reviewed:

  • tryNamespace now peeks the returned promise before awaiting, so the synchronous onResolveAsync fast path is preserved end-to-end (traced through lines 487-518).
  • transferToWTFString() is hoisted above the empty-list early return in anyMatchesCrossThread, so the caller's BunString allocations are consumed on every path.
  • userNamespace defaults to inputNamespace (not matchNamespace), the prefix !== "file" guard prevents double-dispatch on file: specifiers, and the drive-letter skip is now Windows-gated with a following-separator check in both the JS and C++ sides.
  • Tests use describe.concurrent, assert exact VIA-A/VIA-B per context, and cover the { path }-without-namespace → file-on-disk case in both runtime and Bun.build.
Extended reasoning...

Overview

This PR makes onResolve plugin dispatch symmetric between the runtime module loader (Bun.plugin) and the bundler (Bun.build) for specifiers of the form "ns:path". It touches three native/builtin dispatch sites — src/js/builtins/BundlerPlugin.ts (runOnResolvePlugins), src/jsc/bindings/JSBundlerPlugin.cpp (anyMatchesCrossThread), and src/jsc/bindings/BunPlugin.cpp (OnResolve::run) — each adding a fallback that consults the "other" registration form when the primary lookup produces no match. A new test file exercises form A only, form B only, both forms together, and the { path }-without-namespace case, in both runtime and Bun.build contexts.

Since my earlier inline reviews, commits have landed addressing every point: the result-namespace default is back to inputNamespace (with a dedicated test proving runtime/bundler agree when the callback omits namespace); the BunString::transferToWTFString() calls are hoisted above the empty-list early return so the leak on the "no plugins of this hook type" path is gone; the drive-letter guard is now process.platform === "win32" / #if OS(WINDOWS) and additionally requires a following / or \, matching the runtime's extract_namespace; the prefix !== "file" guard prevents re-running the default-namespace group on file: URLs; the tests use describe.concurrent and assert exact toBe("VIA-B") / toBe("VIA-A") per context; and the most recent commit restores the synchronous fast path by peeking $peekPromiseStatus/$peekPromiseSettledValue on the tryNamespace result before falling back to await.

Security risks

None identified. The change is dispatch routing inside plugin resolution — no new untrusted-input parsing beyond String.indexOf(":") / slice, no allocation sized from external data, and the C++ additions maintain DECLARE_THROW_SCOPE / RETURN_IF_EXCEPTION / RELEASE_AND_RETURN discipline around the extracted runOnResolveGroup helper. The jsString(vm, pathString) replacing Bun::toJS(globalObject, *path) is a like-for-like substitution now that pathString is a WTF::String local (and jsString doesn't throw for a valid WTF::String, so the removed RETURN_IF_EXCEPTION there is correct).

Level of scrutiny

Medium-high. The implementation is small and every earlier concern I raised has a corresponding code change plus test coverage, so I'm satisfied on correctness. What keeps this from a straight approve is that it's a user-facing API semantics change: after this PR, an onResolve({ namespace: "virt" }) callback that previously never fired in Bun.build will now fire (with a stripped path), and an onResolve({ filter: /^virt:/ }) callback that never fired at runtime will now fire (with the full specifier). The primary-then-fallback ordering preserves existing behavior for plugins that register both forms, but the design choice — whether to add this fallback at all, and whether the runtime→file-namespace fallback in BunPlugin.cpp should reconstruct "ns:path" vs. something else — is the kind of thing a maintainer should explicitly ack per the API-design section of .claude/docs/landing-prs.md.

Other factors

The PR's evidence block shows the new tests fail on main (both debug+ASAN and release) and pass with the fix, and the PR description lists the existing plugin suites as passing unchanged. The synchronous-throw path (tryNamespace rejects with status 2) now takes one await before reaching the outer .then(..., e => addError) handler — but pre-PR that error also went through the .then rejection handler (the outer peek only unwraps status 1), so the observable error reporting is unchanged; only the happy path's zero-await property mattered, and that's preserved. No CODEOWNERS-gated paths are apparent for these files, and there are no outstanding third-party CHANGES_REQUESTED reviews in the timeline.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bun runtime plugin onResolve doesn't filter non-file: protocol imports

2 participants