Skip to content

mock.module: keep accessor exports live instead of snapshotting them - #36677

Closed
robobun wants to merge 11 commits into
mainfrom
farm/24ce6e23/mock-module-live-bindings
Closed

robobun wants to merge 11 commits into
mainfrom
farm/24ce6e23/mock-module-live-bindings

Conversation

@robobun

@robobun robobun commented Aug 1, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #9874. Fixes #7128.

Repro

import { test, expect, mock } from "bun:test";

mock.module("./module", () => {
  let foo = "foo";
  return {
    get foo() { return foo; },
    updateFoo: () => { foo = "bar"; },
  };
});

test("mock.module live binding", async () => {
  const esm = await import("./module");
  expect(esm.foo).toBe("foo");
  esm.updateFoo();
  expect(esm.foo).toBe("bar");   // fails: Received "foo"
});

Cause

generateObjectModuleSourceCode calls object->get(globalObject, entry) for each own property of the factory result and appends the value to the synthetic module's exportValues. For an accessor that evaluates the getter once; SyntheticModuleRecord::tryCreateWithExportNamesAndValues then stores the result in a JSModuleEnvironment slot, and JSModuleNamespaceObject::getOwnPropertySlotCommon reads the slot directly on every subsequent access. fetchCommonJSModule routes the require() path through the same code, so both await import() and require() observe a snapshot. The plugin build.module / onLoad loader: "object" path (#7128) shares generateObjectModuleSourceCode, so it has the same limitation.

Fix

require(): when the virtual module came from mock.module, handleVirtualModuleResult assigns the factory object as module.exports directly (matching what JSMock__jsModuleMock already does for a re-mock of an already-required module). Property access goes through the original getters. A captured require() result therefore does not observe a later re-mock (Node/Jest semantics); a fresh require() does.

await import(): generateObjectModuleSourceCode still snapshots each property into the module environment (so static import { x } bindings, which every tier reads as a raw variableAt(offset) load, see a plain value), but when any own property is an accessor it also appends the factory object as a trailing exportValue with no matching exportName. On the WebKit side (oven-sh/WebKit#380) SyntheticModuleRecord::tryCreateWithExportNamesAndValues picks that up as m_liveExportsSource, and JSModuleNamespaceObject::getOwnPropertySlotCommon forwards namespace reads through it via an uncacheable slot.setValue for properties the source actually owns (others fall back to the env slot, so partial re-mocks keep un-overridden keys). Because setValueModuleNamespace is never called for those properties, the JIT's ModuleNamespaceAccessCase IC (and the DFG/FTL lowering to GetClosureVar that hangs off it) is never installed.

re-mock / spyOn: JSModuleNamespaceObject::overrideExportValue also writes through to the backing object when one is set, so spyOn(ns, key) and mock.restore() are observed by namespace reads; and JSMock__jsModuleMock swaps the backing object to the new factory on re-mock when the new factory has an accessor or a source was already set (so a data-only partial re-mock of a builtin keeps the module-namespace IC).

Verification

New tests in test/js/bun/test/mock/mock-module.test.ts (ESM + CJS live bindings, mixed data/accessor, re-mock of an accessor-backed namespace, spyOn on one, partial re-mock of data-only and accessor-backed, captured-require contract) and test/js/bun/plugin/plugins.test.ts (loader: "object" accessor export):

before (bun 1.3.14):            10 pass / 7 fail in mock-module.test.ts
after  (bun bd, preview WebKit): 17 pass / 0 fail; plugins.test.ts 36/0

The existing mock.module tests (6874/, 6879/, mock-module-*) and test-vm-module-synthetic.js are unchanged.

Also in this WebKit range (34c01d1339..)

Note

WEBKIT_VERSION is pinned to the preview tag autobuild-preview-pr-380-1b4afe55 while oven-sh/WebKit#380 is open. It must be swapped to the merged main sha before this lands.


[decide:webkit] gate passed · iteration 2 · 8 files touched

fails on main (without fix)
ASAN without fix: 10 failed, 1 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/plugin/plugins.test.ts test/js/bun/test/mock/mock-module.test.ts
bun test v1.4.0 (433b57572)

test/js/bun/plugin/plugins.test.ts:
If bundling, conditions should include development or production. If not bundling, conditions or NODE_ENV should include development or production. See https://www.npmjs.com/package/esm-env for tips on setting conditions in popular bundlers and runtimes.
(pass) require > SSRs `<h1>Hello world!</h1>` with Svelte [1107.03ms]
(pass) require > beep:boop returns 42 [9.04ms]
(pass) require > object module works [7.55ms]
(pass) module > throws with require() [12.72ms]
(pass) module > async module works with async import [22.96ms]
(pass) module > sync module module works with require() [4.33ms]
(pass) module > sync module module works with require.resolve() [2.99ms]
(pass) module > sync module module works with import [4.82ms]
(pass) module > modules are overridable [86.50ms]
306 |   it("accessor exports are live", async () => {
307 |     // @ts-expect-error
308 |     const m = await import("my-virtual-m
... (truncated)

release without fix: 1 skipped
bun test v1.4.0-canary.1 (cf2519804)

test/js/bun/plugin/plugins.test.ts:
If bundling, conditions should include development or production. If not bundling, conditions or NODE_ENV should include development or production. See https://www.npmjs.com/package/esm-env for tips on setting conditions in popular bundlers and runtimes.
(pass) require > SSRs `<h1>Hello world!</h1>` with Svelte [29.95ms]
(pass) require > beep:boop returns 42 [0.15ms]
(pass) require > object module works [0.09ms]
(pass) module > throws with require() [0.13ms]
(pass) module > async module works with async import [1.29ms]
(pass) module > sync module module works with require() [0.07ms]
(pass) module > sync module module works with require.resolve() [0.05ms]
(pass) module > sync module module works with import [0.07ms]
(pass) module > modules are overridable [0.21ms]
(pass) module > accessor exports are live [0.08ms]
(pass) dynamic import > SSRs `<h1>Hello world!</h1>` with Svelte [0.08ms]
(pass) dynamic import > beep:boop returns 42 [0.07ms]
(pass) dynamic import > async:onLoad returns 42 [1.38ms]
(pass) dynamic import > async object loader returns 42 [1.25ms]
(pass) import statement > SSRs `<h1>
... (truncated)
passes on PR (with fix)
ASAN with fix: 1 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/plugin/plugins.test.ts test/js/bun/test/mock/mock-module.test.ts
bun test v1.4.0 (433b57572)

test/js/bun/plugin/plugins.test.ts:
If bundling, conditions should include development or production. If not bundling, conditions or NODE_ENV should include development or production. See https://www.npmjs.com/package/esm-env for tips on setting conditions in popular bundlers and runtimes.
(pass) require > SSRs `<h1>Hello world!</h1>` with Svelte [1140.34ms]
(pass) require > beep:boop returns 42 [9.70ms]
(pass) require > object module works [8.83ms]
(pass) module > throws with require() [13.96ms]
(pass) module > async module works with async import [28.74ms]
(pass) module > sync module module works with require() [4.87ms]
(pass) module > sync module module works with require.resolve() [2.94ms]
(pass) module > sync module module works with import [6.46ms]
(pass) module > modules are overridable [91.96ms]
(pass) module > accessor exports are live [8.34ms]
(pass) dynamic import > SSRs `<h1>Hello world!</h1>` with Svelte [6.53ms]
(pass
... (truncated)

release with fix: 1 skipped
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 688ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/52] cxx obj/src/jsc/bindings/highway_sourcemap.cpp.o
[2/52] cxx obj/src/jsc/bindings/webcore/streams/BunAsyncIterableSource.cpp.o
[3/52] cxx obj/src/jsc/bindings/BunObject.cpp.o
[4/52] cxx obj/unified/UnifiedSource-src_jsc_bindings_node-0.cpp.o
[5/52] cxx obj/src/jsc/bindings/napi.cpp.o
[6/52] cxx obj/unified/UnifiedSource-src_jsc_modules-0.cpp.o
[7/52] cxx obj/src/jsc/bindings/webcore/streams/BunStreamConsumers.cpp.o
[8/52] cxx obj/unified/UnifiedSource-src_jsc_bindings_webcore-4.cpp.o
[9/52] cxx obj/unified/UnifiedSource-src_jsc_bindings-4.cpp.o
[10/52] cxx obj/unified/UnifiedSource-src_jsc_bindings_webcore-3.cpp.o
[11/52] cxx obj/src/jsc/bindings/bindings.cpp.o
[12/52] cxx obj/unified/UnifiedSource-src_jsc_bindings-2.cpp.o
[13/52] cxx obj/unified/UnifiedSource-src_jsc_bindings-1.cpp.o
[14/52] cxx obj/src/jsc/bindings/webcore/streams/BunStreamSource.cpp.o
[15/52] cxx obj/unified/UnifiedSource-src_jsc_bindings-3.cpp.o
[16/52] gen cpp.rs (cppbind)
[17/52] cxx obj/src/jsc/bindings/webcore/streams/CrossRe
... (truncated)
diff hotspot
scripts/build/deps/webkit.ts              |   2 +-
 src/jsc/bindings/BunPlugin.cpp            |  23 +++-
 src/jsc/bindings/JSMockFunction.cpp       |   1 +
 src/jsc/bindings/ModuleLoader.cpp         |   5 +
 src/jsc/modules/ObjectModule.cpp          |  16 ++-
 test/js/bun/plugin/module-plugins.ts      |  13 ++
 test/js/bun/plugin/plugins.test.ts        |   9 ++
 test/js/bun/test/mock/mock-module.test.ts | 206 ++++++++++++++++++++++++++++++
 8 files changed, 272 insertions(+), 3 deletions(-)

gate history · 7 passed · 0 rejected · iteration 2

evidence per changed file
file                                       reads  edits  tests
scripts/build/deps/webkit.ts                   2      6      0
src/jsc/bindings/BunPlugin.cpp                 8      9      0
src/jsc/bindings/JSMockFunction.cpp            4      1      0
src/jsc/bindings/ModuleLoader.cpp              3      1      0
src/jsc/modules/ObjectModule.cpp               2      3      0
test/js/bun/plugin/module-plugins.ts           1      1      0
test/js/bun/plugin/plugins.test.ts             1      1      0
test/js/bun/test/mock/mock-module.test.ts      4      7      0

Fixes #9874. When a mock.module factory returns an object with accessor
properties (`{ get foo() { ... } }`), the exports were being evaluated
once at module creation time and frozen into the synthetic module's
environment slots, so subsequent reads never saw the getter again.

For require(), the mock object now becomes module.exports directly, so
property access goes through the original getters.

For await import(), generateObjectModuleSourceCode detects accessor
properties and passes the factory object to SyntheticModuleRecord as a
live-exports backing source (via a trailing exportValue with no matching
exportName). JSModuleNamespaceObject::getOwnPropertySlotCommon then
forwards namespace reads through that object instead of returning the
raw environment slot. The module-namespace IC is not installed for these
properties, so DFG/FTL do not lower the read to a GetClosureVar raw slot
load. The environment slots still hold the first-read snapshot, so
static `import { x }` bindings (which every tier reads as a raw
variableAt(offset) load) are unaffected.

Requires oven-sh/WebKit#380. WEBKIT_VERSION is pinned to its preview
build; swap to the merged main sha before landing.
@coderabbitai

coderabbitai Bot commented Aug 1, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

The module mock implementation now preserves live accessor exports for ESM and CommonJS modules, handles property access exceptions, and validates re-mocking and caching behavior. The WebKit build dependency now uses an autobuild preview identifier.

Module mock live exports

Layer / File(s) Summary
Accessor export source tracking
src/jsc/modules/ObjectModule.cpp, src/jsc/bindings/BunPlugin.cpp
Object and synthetic module exports inspect own property slots, preserve accessor-backed sources, and map lookup exceptions to undefined.
Mock loading and exception handling
src/jsc/bindings/ModuleLoader.cpp, src/jsc/bindings/JSMockFunction.cpp
Virtual object mocks become evaluated CommonJS exports. Spy property access now propagates exceptions.
Live export behavior validation
test/js/bun/plugin/*, test/js/bun/test/mock/mock-module.test.ts
Tests cover mutable accessor exports, re-mocking, spying, partial mocks, mixed properties, namespace cache invalidation, and CommonJS caching.

WebKit preview update

Layer / File(s) Summary
WebKit preview identifier
scripts/build/deps/webkit.ts
WEBKIT_VERSION now uses the autobuild-preview-pr-380-1b4afe55 identifier.

Possibly related PRs

Suggested reviewers: jarred-sumner, sosukesuzuki

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address #9874 for mock.module and #7128 for loader: "object" modules with live accessor exports.
Out of Scope Changes check ✅ Passed All code, test, and WebKit version changes support the linked objectives and stated implementation requirements.
Title check ✅ Passed The title clearly and concisely identifies the main change: preserving live accessor exports in mock.module.
Description check ✅ Passed The description explains the problem, cause, fix, verification results, and related issues, although it does not use the template headings exactly.

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

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Found 2 issues this PR may fix:

  1. Make it possible to create an export let-like binding using the loader: "object" #7128 - Requests export let-like live bindings via getter properties in loader: "object" — this PR's ObjectModule.cpp change to detect accessors via getOwnPropertySlot implements exactly that
  2. mock.module: captured module namespace is mutated in-place, breaking partial-stub delegation pattern #30242 - Namespace mutation-in-place caused by snapshotting exports at mock creation time — this PR's live-binding fix addresses the root cause

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

Fixes #7128
Fixes #30242

🤖 Generated with Claude Code

@robobun

robobun commented Aug 1, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 7:28 PM PT - Aug 1st, 2026

❌ @robobun, your commit 433b575 has 2 failures in Build #87475 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 36677

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

bun-36677 --bun

Comment thread src/jsc/modules/ObjectModule.cpp Resolved
@robobun

robobun commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

On the related-issue suggestions:

Comment thread src/jsc/modules/ObjectModule.cpp Resolved
overrideExportValue now writes through to SyntheticModuleRecord's
live-exports backing object (WebKit side), and JSMock__jsModuleMock
swaps the source to the new factory object when re-mocking a
synthetic record, so spyOn(ns, key) and a subsequent mock.module on
an accessor-backed namespace are observed by namespace reads.

Bumps WEBKIT_VERSION to the updated preview build.

@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.

The re-mock / spyOn regression I flagged earlier looks addressed in 1a5eb0a + oven-sh/WebKit@1ee717ba, and the new tests cover both paths. I didn't find further issues, but this needs a human look: it bumps WEBKIT_VERSION (still pinned to the preview tag, and the range also pulls in oven-sh/WebKit#333 CodeBlock aging), and the fix hinges on new JSC-side semantics (m_liveExportsSource, overrideExportValue write-through) that I can't verify from this repo alone.

Checked: the re-mock setLiveExportsSource call is inside the object branch so the non-object defaultKeyword path is unaffected; the new wasModuleMock CJS branch in handleVirtualModuleResult is ordered after the __esModule check so that path still wins; the trailing exportValues.append(object) only fires when an own accessor is present, so plain-object modules keep the old snapshot behavior.

Extended reasoning...

Overview

This PR makes accessor properties on mock.module / loader: "object" factory results behave as live bindings instead of one-time snapshots. It touches src/jsc/modules/ObjectModule.cpp (detect accessors, append the factory object as a trailing name-less export value), src/jsc/bindings/ModuleLoader.cpp (assign the factory object directly as module.exports for the CJS require() path when wasModuleMock), and src/jsc/bindings/BunPlugin.cpp (swap SyntheticModuleRecord::liveExportsSource to the new factory object on re-mock). It bumps WEBKIT_VERSION to a preview autobuild tag for oven-sh/WebKit#380, which introduces m_liveExportsSource on SyntheticModuleRecord and makes JSModuleNamespaceObject::getOwnPropertySlotCommon / overrideExportValue respect it. Five new tests cover ESM/CJS live bindings, mixed data+accessor exports, accessor→data→accessor re-mock, and spyOn + mock.restore() on an accessor-backed namespace; one plugin test covers the build.module path.

Security risks

None identified. This is test-runner / module-mock plumbing; no untrusted-input parsing, auth, or network surface is touched.

Level of scrutiny

High. This is a coordinated Bun + WebKit change to core module-loading semantics in JSC bindings. The correctness of the fix depends entirely on the WebKit-side implementation of m_liveExportsSource (GC rooting via WriteBarrier, precedence vs. env-slot reads across all JIT tiers, overrideExportValue write-through), none of which is reviewable from this diff. The WebKit range also carries oven-sh/WebKit#333 (execution-count CodeBlock aging, gated off) and #377 — unrelated changes a maintainer should be aware are riding along. Per the repo guidance, dependency bumps and JSC GC-adjacent changes warrant maintainer review.

Other factors

  • WEBKIT_VERSION is currently a preview tag (autobuild-preview-pr-380-1ee717ba); the PR description explicitly says it must be swapped to the merged main sha before landing, so this cannot merge as-is regardless.
  • My earlier finding (re-mock and spyOn writing only to the env slot while reads forward through the stale live source) was addressed on both sides and is now covered by the two new tests. This run's finder also raised "re-mock unconditionally sets liveExportsSource" — verifiers ruled it out, and I agree: the call is guarded by object != nullptr and by dynamicDowncast<SyntheticModuleRecord>, so non-synthetic (real ESM) records and non-object re-mocks are untouched.
  • CI for 1a5eb0a was still building at the time of the last robobun update; results should be checked before merge.

JSObject::get() on a module namespace object was always throwable (the
TDZ path), and now additionally forwards through a live-exports source.
The validator caught that the result was used (via copyNameAndLength)
without a RETURN_IF_EXCEPTION in between.

@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.

The follow-up commits address my earlier concern — setLiveExportsSource on re-mock, overrideExportValue write-through on the WebKit side, and the added RETURN_IF_EXCEPTION in spyOn for the ASAN validateExceptionChecks failure all look right, and the new re-mock/spyOn tests cover them. Deferring to a human because this is a coordinated Bun+WebKit change (new SyntheticModuleRecord::m_liveExportsSource routing namespace reads past the JIT's module-namespace IC) pinned to a preview WebKit tag that the description says must be swapped before landing.

What was reviewed:

  • ObjectModule.cpp accessor detection via getOwnPropertySlot and the trailing-value protocol; falls through to object->get when the own slot is absent, matching prior behavior.
  • BunPlugin.cpp re-mock now swaps the record's live source only for SyntheticModuleRecord (real JSModuleRecords are left alone, so #30242 stays unfixed as stated).
  • ModuleLoader.cpp wasModuleMock short-circuit for CJS is placed after the existing __esModule handling and only when a CJS target exists.
  • The WebKit range also pulls in oven-sh/WebKit#333 (execution-count CodeBlock aging, gated off) and #377 — worth a maintainer glance.
Extended reasoning...

Overview

The PR makes accessor properties on mock.module / plugin loader: "object" factory results behave as live bindings instead of one-time snapshots. On the Bun side it touches ObjectModule.cpp (detect accessors, append the factory object as a trailing export value), ModuleLoader.cpp (assign the factory object directly as module.exports for the require() path when the source was a module mock), BunPlugin.cpp (swap the synthetic record's live-source on re-mock), and JSMockFunction.cpp (an exception check after reading the spy target's value). The behavior depends on oven-sh/WebKit#380, which adds SyntheticModuleRecord::m_liveExportsSource / setLiveExportsSource, makes JSModuleNamespaceObject::getOwnPropertySlotCommon forward reads through it, and makes overrideExportValue write through to it. WEBKIT_VERSION is pinned to autobuild-preview-pr-380-1ee717ba.

Since my previous review

I previously flagged that once a namespace was backed by m_liveExportsSource, the re-mock loop and spyOn/clearSpy (which only wrote via overrideExportValue) would be silently ignored. That was addressed in 1a5eb0a (Bun) + 1ee717ba (WebKit): overrideExportValue now writes through to the live source, and JSMock__jsModuleMock calls synthetic->setLiveExportsSource(vm, object) after the override loop so the new factory object replaces the old one. New tests cover accessor→data→accessor re-mock and spyOn+mock.restore() on an accessor-backed namespace. The subsequent ASAN-lane failure (unchecked exception at copyNameAndLength triggered by the new spyOn test) is what 3138133 fixes by adding RETURN_IF_EXCEPTION after slot.getValue/object->get in JSMock__jsSpyOn — now that namespace reads can forward through a user object, that read is observably fallible.

Security risks

None identified. This is test-runner / module-loader plumbing; the trailing-value protocol is between Bun-authored C++ and Bun's WebKit fork, not user-controllable in a way that widens attack surface.

Level of scrutiny

High. This is a cross-repo design change to how synthetic module namespaces resolve property reads, with explicit reasoning about why the DFG/FTL ModuleNamespaceAccessCase IC won't cache stale values (setValueModuleNamespace is never called for live-source-backed keys). I can't review the WebKit half from here, and the correctness of the Bun half (particularly spyOn write-through and re-mock semantics for mixed data/accessor keys) hinges on exactly what overrideExportValue does on the WebKit side. The PR also bumps WebKit across a range that includes an unrelated CodeBlock-aging change (#333, gated off by default).

Other factors

The PR description explicitly flags that WEBKIT_VERSION is pinned to a preview tag and must be swapped to the merged main sha before landing — that alone means a maintainer needs to coordinate the merge. CI on 1a5eb0a showed the mock-module ASAN failure that 3138133 targets, plus three unrelated failures (a Windows bun-upgrade flake, a Windows regression test, and two agent-creation infra errors); a fresh run on 3138133 hasn't been reported in the thread yet.

@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.

Beyond the inline findings: the RETURN_IF_EXCEPTION added at JSMockFunction.cpp:1526 is the fix for the ASAN validateExceptionChecks CI failure on 1a5eb0a — slot.getValue() on a namespace now forwards through the live source's user getter, which can throw, so the check is necessary and correctly placed. Also confirmed the new wasModuleMock CJS branch in ModuleLoader.cpp does not affect the plugin loader: "object" path (only mock.module sets wasModuleMock), so require() of a builder.module object still returns a namespace as before.

Extended reasoning...

The fix to my earlier comment (swap liveExportsSource on re-mock) over-applies — it now installs a live source on every re-mock of a SyntheticModuleRecord, which changes data-only partial re-mocks of already-imported builtins from patch-per-key to replace-wholesale. That's covered by the inline finding. Separately I traced the JSMockFunction.cpp:1526 exception check back to the ASAN failure in build 87111 and confirmed it's the right fix for the new throwing path, and verified the ModuleLoader.cpp CJS short-circuit is scoped to mock.module only. WEBKIT_VERSION is still on the preview tag, which the description already flags as a pre-land blocker.

Comment thread src/jsc/bindings/BunPlugin.cpp Resolved
Comment thread src/jsc/bindings/ModuleLoader.cpp Resolved
The unconditional setLiveExportsSource on re-mock was replacing the
backing object even for data-only re-mocks, so a partial mock.module of
an already-imported synthetic module (including builtins) made every
un-named export read as undefined.

Two halves:
- WebKit: getOwnPropertySlotCommon only forwards through the live source
  for properties the source actually owns; others fall through to the
  module-environment slot.
- Bun: JSMock__jsModuleMock only installs/swaps the live source when the
  new factory has an accessor or a source was already set, so a
  data-only re-mock keeps the module-namespace IC fast path.

Also pins the new require() contract with a test: a captured require()
result is the factory object (matching Node/Jest), so it does not
observe a subsequent re-mock; a fresh require() does.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/build/deps/webkit.ts`:
- Line 6: Update the WEBKIT_VERSION constant to the merged WebKit `#380` revision
and release identifier, replacing the temporary
autobuild-preview-pr-380-3cf0ca7b value so release builds no longer use the
preview artifact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7382f855-de47-4044-8b36-bc1714117e43

📥 Commits

Reviewing files that changed from the base of the PR and between a7838c5 and 4683caf.

📒 Files selected for processing (8)
  • scripts/build/deps/webkit.ts
  • src/jsc/bindings/BunPlugin.cpp
  • src/jsc/bindings/JSMockFunction.cpp
  • src/jsc/bindings/ModuleLoader.cpp
  • src/jsc/modules/ObjectModule.cpp
  • test/js/bun/plugin/module-plugins.ts
  • test/js/bun/plugin/plugins.test.ts
  • test/js/bun/test/mock/mock-module.test.ts

Comment thread scripts/build/deps/webkit.ts Resolved
Comment thread src/jsc/bindings/BunPlugin.cpp Resolved
A data-only first mock leaves the SyntheticModuleRecord with no live
source, so namespace reads install ModuleNamespaceAccessCase / lower to
GetClosureVar (raw env-slot load). A later accessor re-mock left those
compiled sites reading the stale slot while the interpreter forwarded
through the live source.

WebKit side: SyntheticModuleRecord carries an InlineWatchpointSet that
both the access case and the DFG lowering watch; setLiveExportsSource
fires it on the first install, which resets the stubs / jettisons that
code. getOwnPropertySlotCommon also returns the env-slot fallthrough as
uncacheable once the watchpoint has fired so the IC is never
re-installed on a record that has ever had a live source.

New test tiers up a single call site on a data-only mock, then re-mocks
with an accessor and asserts the same site observes the live value.
Comment thread src/jsc/bindings/BunPlugin.cpp Resolved
overrideExportValue writes through to the record's current live-exports
source. The re-mock loop ran with the previous factory object still
installed, so each per-key write landed on that object (which, since
require() of a mock returns it directly, can be a user-captured
reference), overwriting its accessors with snapshot values.

Clear the live source before the overrideExportValue loop and install
the new factory object after. setLiveExportsSource now accepts null
(setMayBeNull) and fires the IC watchpoint only on the first-ever
install, so the clear/set pair does not re-fire it.
Comment thread src/jsc/bindings/BunPlugin.cpp Resolved

@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.

The latest revision (aa89898 + oven-sh/WebKit@1b4afe55) addresses the write-through-to-previous-factory issue; I didn't find anything new this pass. Deferring to a maintainer for the cross-repo landing — WEBKIT_VERSION is still on the preview tag and the WebKit-side watchpoint/IC changes should get human eyes alongside oven-sh/WebKit#380.

What was reviewed:

  • BunPlugin.cpp re-mock loop now clears the live source before overrideExportValue and reinstalls after; verified this preserves partial-re-mock semantics for both data-only and accessor-backed cases and no longer mutates a captured require() result.
  • generateObjectModuleSourceCode trailing-value protocol and the wasModuleMock short-circuit in ModuleLoader.cpp; the __esModule branch above it still takes precedence.
  • Checked the spyOn write-through path — it still mutates the live-source object (the factory result), which is consistent with spyOn on the same object obtained via require().
  • The new RETURN_IF_EXCEPTION after slot.getValue in JSMock__jsSpyOn is correct for the now-forwarded getter path.
Extended reasoning...

Overview

This PR makes mock.module factory accessors behave as live bindings on both the ESM namespace and the require() result, and applies the same to plugin loader: "object" exports. The Bun-side diff is ~60 lines of C++ across ObjectModule.cpp, BunPlugin.cpp, ModuleLoader.cpp, JSMockFunction.cpp, plus ~200 lines of new tests. The load-bearing half is in the paired oven-sh/WebKit#380: a new SyntheticModuleRecord::m_liveExportsSource slot, forwarding in JSModuleNamespaceObject::getOwnPropertySlotCommon, write-through in overrideExportValue, and an InlineWatchpointSet that ModuleNamespaceAccessCase and DFGByteCodeParser::handleModuleNamespaceLoad observe so a data-only → accessor re-mock invalidates already-compiled namespace ICs.

Review history

I've reviewed four prior revisions of this PR and each surfaced a real correctness issue (re-mock/spyOn ignored on accessor-backed namespaces; unconditional live-source install regressing partial builtin mocks; missing IC invalidation on the null→object transition; write-through mutating the previous factory object). All four are now addressed with dedicated tests, and this pass — over aa89898 / cf25198 — found nothing new.

Security risks

None. This is test-runner mocking machinery; the accessor forwarding runs user-provided getters on user-provided objects, which is the intended behavior.

Level of scrutiny

High, and specifically needs a maintainer, for two reasons this bot can't discharge:

  1. Cross-repo coordination. WEBKIT_VERSION is pinned to autobuild-preview-pr-380-1b4afe55; the PR description states it must be swapped to the merged main sha before landing. oven-sh/WebKit#380 is still open.
  2. JIT-level design review. The correctness of the null→object watchpoint (fires once, observed by both the polymorphic IC via additionalSetImpl and the DFG GetClosureVar lowering, and permanently disables caching on the env-slot fallthrough) lives entirely in the WebKit diff. I've reasoned about it from the PR thread's description and the tests exercise it in-process, but I can't read the WebKit source here — a JSC-familiar reviewer should confirm the watchpoint plumbing on that side.

Other factors

  • Test coverage is thorough for the shapes that broke across iterations: ESM/CJS live bindings, mixed data/accessor, re-mock (accessor→data→accessor), spyOn+restore on an accessor-backed namespace, partial re-mock (both data-only and accessor-backed), IC invalidation after a hot data-only read, captured-require() contract, and re-mock non-mutation of the previous factory. The plugin loader: "object" path is covered too.
  • The 500k-iteration IC-invalidation test is the one place I'd flag for a maintainer to eyeball wall-time under debug+ASAN, though it's a tight integer loop.
  • Existing mock.module regression tests (6874, 6879, mock-module-*) and test-vm-module-synthetic.js are stated unchanged; I did not re-run them.

Comment thread src/jsc/bindings/BunPlugin.cpp Resolved
The non-object re-mock branch (reached when a factory returns an
already-fulfilled Promise resolving to a primitive) also calls
overrideExportValue, so clear the previous live source before both
arms.

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/jsc/bindings/BunPlugin.cpp (1)

658-689: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Propagate accessor failures.

Line 675 and Line 684 clear exceptions from getOwnPropertySlot() and propSlot.getValue(). A Proxy trap or accessor can throw. The code then replaces the export with undefined and lets mock.module() succeed.

Propagate the exception to the caller. Add a regression test with a throwing accessor.

As per coding guidelines, calls that can run user code must propagate errors and must not clear exceptions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/jsc/bindings/BunPlugin.cpp` around lines 658 - 689, Update the property
inspection loop in the mock.module export handling around `getOwnPropertySlot()`
and `PropertySlot::getValue()` so exceptions from Proxy traps or accessors are
propagated immediately to the caller instead of being cleared and replaced with
undefined. Remove the exception-clearing fallback branches while preserving
normal export override behavior, and add a regression test using a throwing
accessor to verify mock.module() fails with that exception.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/jsc/bindings/BunPlugin.cpp`:
- Around line 658-689: Update the property inspection loop in the mock.module
export handling around `getOwnPropertySlot()` and `PropertySlot::getValue()` so
exceptions from Proxy traps or accessors are propagated immediately to the
caller instead of being cleared and replaced with undefined. Remove the
exception-clearing fallback branches while preserving normal export override
behavior, and add a regression test using a throwing accessor to verify
mock.module() fails with that exception.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e25601ef-7fd2-47f6-9bb6-b7f3b3e8ab3f

📥 Commits

Reviewing files that changed from the base of the PR and between cc86db2 and 433b575.

📒 Files selected for processing (3)
  • scripts/build/deps/webkit.ts
  • src/jsc/bindings/BunPlugin.cpp
  • test/js/bun/test/mock/mock-module.test.ts

@robobun

robobun commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

On the outside-diff-range comment about clearing exceptions from accessor reads in the JSMock__jsModuleMock loop: that is pre-existing behavior I preserved (the original object->get() + tryClearException() at these lines, the // consistent with regular esm handling code comment, and the matching paths in generateObjectModuleSourceCode / populateESMExports). All of Bun's module-export enumeration swallows getter exceptions and exports undefined for that key, matching Node's named-export enumeration. Changing that contract is orthogonal to #9874 / #7128; keeping it out of this PR.

Comment thread src/jsc/bindings/BunPlugin.cpp Resolved
@robobun

robobun commented Aug 1, 2026 •

Copy link
Copy Markdown
Collaborator Author

CI status: build #87475 on 433b575728 completed 193/196 passed. None of the remaining failures touch this diff:

  • test/cli/install/bun-upgrade.test.ts on Windows aarch64: "Canary builds are not available for this platform yet" (main break, already reported)
  • 3 agent-creation infra failures (alpine x64, linux x64-musl verify-baseline, windows 2019 x64)
  • everything else is [flaky] (passed on retry or when run alone)

test/js/bun/test/mock/mock-module.test.ts and test/js/bun/plugin/plugins.test.ts passed on every lane that ran them. The three preceding runs (#87233, #87397, #87446) all expired every build job before running.

Ready for review. WEBKIT_VERSION is on the preview tag autobuild-preview-pr-380-1b4afe55; oven-sh/WebKit#380 should merge first, then this can be swapped to the merged main sha.

Jarred-Sumner pushed a commit that referenced this pull request Aug 12, 2026
…s ESM (#37525)

Builtins implemented in `src/js` are handed to ES module importers by
`generateInternalModuleSourceCode`
(`src/jsc/bindings/ModuleLoader.cpp`), which snapshots the builtin's
CommonJS exports object into a synthetic module record. It did that with
`object->get()` on every own enumerable property, so every accessor on
the exports object ran at import time. Those accessors are the builtins'
lazy-loading mechanism, which means they only ever helped `require()`
callers:

- `node:fs`: `ReadStream`, `WriteStream`, `FileReadStream`,
`FileWriteStream`, `Utf8Stream` each `require("internal/fs/streams")`,
which loads the whole `node:stream` stack.
- `node:tls`: `rootCertificates` parses the bundled CA store,
`DEFAULT_CIPHERS` queries BoringSSL.
- `node:http`: `globalAgent` instantiates the agent. `node:timers` /
`node:stream`: `promises` load `timers/promises` and `stream/promises`.
`node:assert`: `AssertionError` loads `internal/assert/assertion_error`.
`node:repl`, `node:events`, `node:buffer`, `node:os` have a few more.

#35541 is about to turn more of these into accessors (`fs.promises`
among them), which this path would immediately defeat for the most
common import style there is.

## Repro

```js
// probe.mjs, run as `bun --expose-internals probe.mjs esm` vs `... require`
const mode = process.argv[2];
const { createRequire } = await import("node:module");
let t = performance.now();
if (mode === "esm") await import("node:fs"); else createRequire(import.meta.url)("node:fs");
const fsMs = performance.now() - t;
t = performance.now();
createRequire(import.meta.url)("internal/fs/streams");
console.log(mode, "node:fs", fsMs.toFixed(1), "ms; internal/fs/streams afterwards", (performance.now() - t).toFixed(1), "ms");
```

bun 1.4.0 (release): `esm node:fs 13.3 ms; internal/fs/streams
afterwards 0.2 ms` (already loaded) vs `require node:fs 7.8 ms;
internal/fs/streams afterwards 5.0 ms`. On a debug build the difference
is ~370ms per process, and `test/harness.ts` does `import fs from
"node:fs"`, so every test file paid it.

The directly observable form, which the tests use: the `node:fs` getters
replace themselves with data properties when they run, so after `import
fs from "node:fs"`, `Object.getOwnPropertyDescriptor(fs, "ReadStream")`
had a `value` instead of a `get`.

## Fix

An ES module binding cannot be made lazy from the embedder side: once
the record exists, named imports read the exporting environment's slot
directly (`ModuleVar`) and namespace reads go through `getValue()` in
`JSModuleNamespaceObject`, so the slot has to hold the value before
anything binds to it. The engine half is oven-sh/WebKit#408, merged as
7b763944f0ec. `WEBKIT_VERSION` moves from 09e477744721 (what main pins)
to that sha; it is the only commit between the two:

- `SyntheticModuleRecord::tryCreateWithExportNamesAndValues()` gets an
overload taking a source object; an export whose value is the empty
`JSValue` is declared but left in TDZ.
- `materializeLazyExport()` fills such a slot from `source[name]` the
first time something binds to it:
`CyclicModuleRecord::initializeEnvironment` when an importer links a
named import that resolves to it (directly or through `export *` /
`export { x } from` chains),
`JSModuleNamespaceObject::getOwnPropertySlotCommon` when a namespace
read finds the slot empty (it then re-reads and installs the value the
same way as before, so the namespace IC still applies), and
`WebAssemblyModuleRecord::initializeImports`, which snapshots imported
bindings directly (unreachable in bun today, reachable once #35587
lands). It is a no-op for a slot that already has a value, so
`overrideExportValue` (what `mock.module` and `spyOn` use) keeps
winning, `*namespace*` is skipped, a getter that re-enters keeps the
first value that landed, and an exception from the getter propagates and
leaves the slot for the next attempt. The write uses the same
`symbolTablePutTouchWatchpointSet` the eager path used for its one
write, so IC / DFG behaviour after materialization is identical to an
eagerly built record; the baseline namespace IC already bails to the
slow path on an empty slot and the DFG only folds loads that went
through an installed IC.
- `SyntheticSourceProvider::createWithLazyExports()` takes a generator
that returns the source object, and `makeModule` passes it through.
Records built any other way (JSON modules, `vm.SyntheticModule`, every
existing `create()` caller) have no source object and take a pointer
test on the new paths.

The bun half is `generateInternalModuleSourceCode`: it now looks each
property up with `getOwnPropertySlot`, snapshots data properties exactly
as before, declares everything else (accessors) as a lazy export, and
returns the exports object as the source. A builtin without accessors
(most of them) produces a record identical to the old one. `require()`
of a builtin never went through this function (`fetchCommonJSModule`
returns the registry object), so it is unaffected. The engine change is
inert without this opt-in: building the new WebKit with the old
`ModuleLoader.cpp` still fails the new tests the same way the release
does.

### Why this is the right behaviour

Node builds the ESM facade of a builtin by reading the getters too, so
the old behaviour was not wrong, just expensive, and there is no compat
reason to keep it. The only observable difference is *when* an accessor
export is sampled: at import before, at the point something first binds
to it now. Data properties still snapshot at import. A namespace export
or named import of an accessor is a snapshot either way (that is what
`module.syncBuiltinESMExports()` exists for), and the later sample is
never staler than the old one. Everything an importer can do with the
export resolves to the same object `require()` would hand out, which is
what the tests check for each binding path.

`Object.keys(ns)`, `hasOwnProperty`, spread, and `console.log(ns)` still
materialize what they touch, because `[[GetOwnProperty]]` has to produce
the value; `in` ([[HasProperty]]) does not. Plugin `loader: "object"`
modules and user CommonJS imported from ESM are deliberately not
touched: those expose user objects whose getter timing is user-visible
(#36677 is about that), whereas builtins' accessors are our own
lazy-loading idiom.

## Tests

`test/js/bun/resolve/builtin-esm-lazy-exports.test.ts`, next to the
other module-system behaviour tests in that directory. Each case runs in
its own process (a binding can only be materialized once) and checks
both that importing left the `node:fs` accessors untouched and that the
binding is the real class once used, for: default + named data import,
named import of an accessor (exactly that one materializes), `import *
as`, `import defer * as`, dynamic `import()` including `Object.keys` and
that the export list is unchanged, `export *` and `export { x as y }
from` re-exports (named import through the star and a read off the
re-exporter's namespace, which exercises materializing on a record other
than the namespace's own), accessors on `node:assert` (a function
exports object), `node:timers` and `node:stream`, and `spyOn` /
`mock.module` on an already imported builtin. 7 of the 8 cases fail on
the current eager code (everything reports `value`); the
`assert`/`timers`/`stream` identity case is a regression guard.

With the fix, the probe above reports `internal/fs/streams` as not
loaded after `import("node:fs")` (debug build: 338ms to load it
afterwards in both modes, versus 10ms before because the import had
already loaded it). Also run on the new build:
`test/js/node/fs/fs.test.ts` (covers the existing `export { ReadStream,
WriteStream } from "node:fs"` and `export * from "node:fs"` fixtures),
the `test/js/bun/resolve` module tests,
`test/js/bun/test/mock/mock-module*.test.ts`,
`node-module-module.test.js`, `buffer-inspectmaxbytes.test.ts` (which
pins that a named import of an accessor is a snapshot), and the events /
os / timers / assert / tls-internals / node-http suites, all green apart
from two failures that are identical on the released binary in this
environment (`os.userInfo`, the http proxy test).

<!-- robobun:evidence:begin -->

---

**[decide:webkit]** gate passed · iteration 0 · 3 files touched

<details><summary>fails on main (without fix)</summary>

```console
ASAN without fix: 7 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/resolve/builtin-esm-lazy-exports.test.ts
bun test v1.4.0 (435641a)

test/js/bun/resolve/builtin-esm-lazy-exports.test.ts:
132 |       afterKeys: kinds(),
133 |       keysMatch: namespaceKeys.sort().join() === [...exportsKeys, "default"].sort().join(),
134 |       allAreTheClasses: STREAMS.every(name => typeof ns[name] === "function" && ns[name] === fs[name]),
135 |     });
136 |   `);
137 |   expect(result).toEqual({
                       ^
error: expect(received).toEqual(expected)

  {
    "afterImport": {
-     "FileReadStream": "accessor",
-     "FileWriteStream": "accessor",
-     "ReadStream": "accessor",
-     "Utf8Stream": "accessor",
-     "WriteStream": "accessor",
+     "FileReadStream": "value",
+     "FileWriteStream": "value",
+     "ReadStream": "value",
+     "Utf8Stream": "value",
+     "WriteStream": "value",
    },
    "afterKeys": {
      "FileReadStream": "value",
      "FileWriteStream": "value",
      "ReadStream": "value",
      "Utf8Stream": "value",
      "WriteStream": "value",
    },
    "after
... (truncated)

release without fix: 7 FAILED
bun test v1.4.0-canary.1 (9008ae7)

test/js/bun/resolve/builtin-esm-lazy-exports.test.ts:
68 |   const result = await runEntry(`
69 |     import { ReadStream } from "node:fs";
70 |     import { fs, kinds, print } from "./helper.mjs";
71 |     print({ kinds: kinds(), isTheClass: typeof ReadStream === "function" && ReadStream === fs.ReadStream });
72 |   `);
73 |   expect(result).toEqual({ kinds: kinds("ReadStream"), isTheClass: true });
                      ^
error: expect(received).toEqual(expected)

  {
    "isTheClass": true,
    "kinds": {
-     "FileReadStream": "accessor",
-     "FileWriteStream": "accessor",
+     "FileReadStream": "value",
+     "FileWriteStream": "value",
      "ReadStream": "value",
-     "Utf8Stream": "accessor",
-     "WriteStream": "accessor",
+     "Utf8Stream": "value",
+     "WriteStream": "value",
    },
  }

- Expected  - 4
+ Received  + 4

      at <anonymous> (/workspace/bun/test/js/bun/resolve/builtin-esm-lazy-exports.test.ts:73:18)
89 |       afterRead: kinds(),
90 |       isTheClass: typeof WriteStream === "function" && WriteStream === fs.WriteStream,
91 |       secondReadIsStable: ns.WriteStream === WriteStream,
92 |     })
... (truncated)
```

</details>

<details><summary>passes on PR (with fix)</summary>

```console
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/resolve/builtin-esm-lazy-exports.test.ts
bun test v1.4.0 (435641a)

test/js/bun/resolve/builtin-esm-lazy-exports.test.ts:
(pass) importing the module does not run its accessors [1746.65ms]
(pass) a named import materializes exactly the binding it links [2145.14ms]
(pass) a namespace export materializes when it is read, not on import or `in` [2175.43ms]
(pass) a deferred namespace (import defer) materializes on read as well [2720.80ms]
(pass) import() namespace: same export list as before, enumerating it materializes everything [2873.44ms]
(pass) re-exports bind through to the builtin's own binding [2232.58ms]
(pass) spyOn and mock.module on an imported builtin [2125.50ms]
(pass) accessors on other builtins bind to what require() returns [2735.47ms]

 8 pass
 0 fail
 24 expect() calls
Ran 8 tests across 1 file. [8.97s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     435641a
  features     baseline

22 deps, 107 codegen, 1176 objects in 1540ms

ninja: Entering directory `/workspace/bun/build/release'
[1/1238] gen ErrorCode+*.h
[2/1238] gen bindgenv2
[3/1238] gen JSBuffer.lut.h
Generating /workspace/bun/build/release/codegen/JSBuffer.lut.h from /workspace/bun/src/jsc/bindings/JSBuffer.cpp
[4/1238] fetch zlib
[zlib] up to date
[5/1238] fetch libjpeg-turbo
[libjpeg-turbo] up to date
[6/1238] fetch tinycc
[tinycc] up to date
[7/1237] fetch picohttpparser
[picohttpparser] up to date
[8/1237] gen .bind.ts → GeneratedBindings.cpp
[9/1237] gen ProcessBindingBuffer.lut.h
Generating /workspace/bun/build/release/codegen/ProcessBindingBuffer.lut.h from /workspace/bun/src/jsc/bindings/ProcessBindingBuffer.cpp
[10/1237] gen ProcessBindingConstants.lut.h
Generating /workspace/bun/build/release/codegen/ProcessBindingConstants.lut.h from /workspace/bun/src/jsc/bindings/ProcessBindingConstants.cpp
[11/1237] gen ProcessBindingHTTPParser.lut.h
Generating /workspac
... (truncated)
```

</details>

<details><summary>diff hotspot</summary>

```
scripts/build/deps/webkit.ts                       |   2 +-
 src/jsc/bindings/ModuleLoader.cpp                  |  28 ++-
 .../bun/resolve/builtin-esm-lazy-exports.test.ts   | 232 +++++++++++++++++++++
 3 files changed, 254 insertions(+), 8 deletions(-)
```

</details>

**gate history** · 1 passed · 0 rejected · iteration 0

<details><summary>evidence per changed file</summary>

```
file                                                  reads  edits  tests
scripts/build/deps/webkit.ts                              3      3      0
src/jsc/bindings/ModuleLoader.cpp                         1      3      0
test/js/bun/resolve/builtin-esm-lazy-exports.test.ts      0      0      0
```

</details>

<!-- robobun:evidence:end -->
@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

Closing this PR because it has been inactive for more than 90 days.

@github-actions github-actions Bot closed this Nov 1, 2026
@robobun

robobun commented Nov 1, 2026

Copy link
Copy Markdown
Collaborator Author

#44833 supersedes this PR. It landed live exports for module mocks on top of oven-sh/WebKit#781.

I checked main at 2b0ace8 with a local debug build (plus one missing #include so that the debug profile compiles here):

oven-sh/WebKit#380 is closed too.

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.

Make it possible to create an export let-like binding using the loader: "object" [bun:test] mocking mutable exports (let) with mock.module is broken

2 participants