Skip to content

SyntheticModuleRecord: exports whose values are produced on first binding - #408

Merged
Jarred-Sumner merged 1 commit into
mainfrom
farm/68bba673/lazy-synthetic-exports
Aug 11, 2026
Merged

SyntheticModuleRecord: exports whose values are produced on first binding#408
Jarred-Sumner merged 1 commit into
mainfrom
farm/68bba673/lazy-synthetic-exports

Conversation

@robobun

@robobun robobun commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Problem

Bun exposes its builtin modules (node:fs, node:timers, ...) to ES module importers as SyntheticModuleRecords: the generator behind SyntheticSourceProvider walks the builtin's CommonJS exports object and hands tryCreateWithExportNamesAndValues() one value per own enumerable property. Several of those exports objects define accessors precisely so that the expensive part of the module is only loaded when somebody touches it (fs.ReadStream / WriteStream / FileReadStream / FileWriteStream / Utf8Stream each require() the fs stream implementation, which pulls in the whole node:stream stack; timers.promises, assert.AssertionError, ... do the same thing). Because a synthetic record needs every value up front, import fs from "node:fs" runs all of those accessors at fetch time and the laziness only helps require() callers. On a release build that roughly doubles the cost of importing node:fs (about 13ms vs 7ms for require, of which ~5ms is the stream stack); on debug builds it is several hundred ms per process.

There is no way to fix this on the embedder side: once the record exists, imported bindings are read straight out of the exporting JSModuleEnvironment slot (ModuleVar in JSScope::abstractAccess), and namespace reads go through getValue() in JSModuleNamespaceObject::getOwnPropertySlotCommon, so the slot has to hold the real value before anything binds to it.

Change

All under USE(BUN_JSC_ADDITIONS) except the SyntheticSourceProvider plumbing, which is already Bun-only code.

  • SyntheticModuleRecord::tryCreateWithExportNamesAndValues() gains an overload taking a JSObject* lazyExportsSource. An export whose value is the empty JSValue is declared but not initialized: its slot keeps the TDZ value JSModuleEnvironment::create() gave it, and the record remembers the source object (m_lazyExportsSource, visited by visitChildren). The existing overload forwards with nullptr and behaves as before.
  • SyntheticModuleRecord::materializeLazyExport(globalObject, localName) fills such a slot in: it returns immediately if the record has no lazy exports, the name is *namespace*, or the slot already holds a value (provided up front, materialized earlier, or written directly by JSModuleNamespaceObject::overrideExportValue, which is how Bun's mock.module() patches an already imported module and must keep winning). Otherwise it does source->get(name) and stores the result with symbolTablePutTouchWatchpointSet, i.e. the same first write the eager path does, so the watchpoint / IC / DFG tryGetConstantClosureVar behaviour after materialization is identical to an eagerly created record. A getter that re-enters and fills the slot itself wins; the outer call keeps that value. Exceptions from the getter propagate and leave the slot untouched, so the next binding attempt retries. The static overload taking an AbstractModuleRecord* is the no-cost check the three call sites below use.
  • Every place that turns a Resolution into a value read materializes first:
    • CyclicModuleRecord::initializeEnvironment, step 7.c.iv: a named import that resolved to the binding (directly or through export { x } from / export * chains, since the resolution carries the final module). This is the point JSC otherwise "handles through lazy resolution", and it runs before any code of the importer can read the binding. Errors become link errors like the other throws in that loop.
    • JSModuleNamespaceObject::getOwnPropertySlotCommon: when the slot read comes back empty, materialize on exportEntry.moduleRecord (the target record, which matters for export * namespaces) and read again, so the value is then returned through setValueModuleNamespace and the namespace IC applies to it exactly as before. This is the same shape as the existing *namespace* handling a few lines up. [[HasProperty]] ("x" in ns) and VMInquiry do not read the slot and still do not evaluate anything; Object.keys(ns) / spread materialize everything, which is what the eager behaviour did anyway. The baseline IC (emitModuleNamespaceLoad) already bails to this slow path on an empty slot, and the DFG only folds loads that went through an installed IC, so neither tier can observe a TDZ slot for a lazy export.
    • WebAssemblyModuleRecord::initializeImports: snapshots the slot directly, so it materializes first. Bun does not currently route wasm through this record type (runtime: implement WebAssembly/ESM integration for .wasm imports bun#35587 would); the hook keeps the engine feature complete rather than fixing a reachable bug.
  • SyntheticSourceProvider gets createWithLazyExports() taking a generator that returns the source object (or nullptr), generate() returns it, and JSModuleLoader::makeModule passes it on. create() and existing generators are untouched (MarkedArgumentBuffer accepts empty values; addMarkSet ignores non-cells).

Records created any other way (JSON modules via parseJSONModule, Bun's vm.SyntheticModule, every create() caller that passes values) never get a source object, so hasLazyExports() is false and the new code is a pointer test on their paths.

Verification

The five touched TUs compile with -fsyntax-only against this tree plus the current prebuilt's derived headers, with USE_BUN_JSC_ADDITIONS and ENABLE_WEBASSEMBLY on. The feature is only reachable through an embedder generator, so the behavioural tests live in the companion Bun change (named import, namespace import, dynamic import(), export * / export { x } from re-exports, in, Object.keys, and mock.module on an already imported builtin, each checking both that the accessor did not run on import and that the binding is the right object once used). That PR is oven-sh/bun#37525; it pins this PR's preview build, and its new test file fails 7/8 cases when bun is built against this preview with the old generator (so this change is inert until an embedder opts in) and passes 8/8 with the new one.

…ding

A synthetic module generator can now declare an export without giving it
a value (an empty JSValue in exportValues) and hand back the object the
value should be read from. The binding stays in its TDZ state until
something actually binds to it: CyclicModuleRecord::initializeEnvironment
when an importer links a named import that resolves to it,
JSModuleNamespaceObject::getOwnPropertySlotCommon when it is read off a
namespace object, and WebAssemblyModuleRecord::initializeImports when a
wasm module imports it. At that point SyntheticModuleRecord::
materializeLazyExport reads source[name] once and stores it in the
environment slot, after which every tier sees an ordinary module binding.

Bindings that were written directly (overrideExportValue) are left alone,
and a re-entrant materialization keeps whichever value landed first.

SyntheticSourceProvider::createWithLazyExports takes the generator variant
that returns the source object; the existing create() and the existing
tryCreateWithExportNamesAndValues overload are unchanged.

Bun uses this so that importing a builtin such as node:fs as an ES module
no longer runs every lazy accessor on the module's exports object (which
loads the whole stream stack) just to snapshot the namespace.
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 22 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e8bca308-3865-47ea-a398-ecc00972b221

📥 Commits

Reviewing files that changed from the base of the PR and between 4485572 and e1b5cb7.

📒 Files selected for processing (7)
  • Source/JavaScriptCore/parser/SourceProvider.h
  • Source/JavaScriptCore/runtime/CyclicModuleRecord.cpp
  • Source/JavaScriptCore/runtime/JSModuleLoader.cpp
  • Source/JavaScriptCore/runtime/JSModuleNamespaceObject.cpp
  • Source/JavaScriptCore/runtime/SyntheticModuleRecord.cpp
  • Source/JavaScriptCore/runtime/SyntheticModuleRecord.h
  • Source/JavaScriptCore/wasm/js/WebAssemblyModuleRecord.cpp

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

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I reviewed this PR and didn't find any bugs. Because it introduces new module-binding semantics that touch linking, namespace property access, and JIT/IC assumptions across several core files, a human look would still be worthwhile.

Checked: m_lazyExportsSource is visited in visitChildrenImpl and only set via WriteBarrier::set; the three materialization call sites all RETURN_IF_EXCEPTION after the getter may run JS; the re-entrancy guard re-reads the slot after source->get(); the namespace-object hook only fires in the Get/GetOwnProperty branch, so HasProperty/VMInquiry stay side-effect-free; and non-lazy callers (create()/JSON path) reach the new code with nullptr and behave as before.

Extended reasoning...

Overview

This PR adds a lazy-export mechanism to SyntheticModuleRecord: an export can be declared with an empty JSValue, leaving its JSModuleEnvironment slot at TDZ until the first binding read, at which point materializeLazyExport() calls a getter on a stored source object and writes the result via symbolTablePutTouchWatchpointSet. It threads a JSObject* lazyExportsSource through SyntheticSourceProvider::generate()JSModuleLoader::makeModuletryCreateWithExportNamesAndValues, adds a WriteBarrier<JSObject> field with GC visiting, and hooks materialization into three read sites: CyclicModuleRecord::initializeEnvironment (named-import linking), JSModuleNamespaceObject::getOwnPropertySlotCommon (namespace reads), and WebAssemblyModuleRecord::initializeImports (wasm snapshot). Almost everything is guarded by USE(BUN_JSC_ADDITIONS).

Security risks

No auth/crypto/permissions surface. The main risk class is engine correctness: running arbitrary JS (source->get()) inside module linking and namespace [[Get]], which introduces re-entrancy and exception paths where none existed before. The PR handles both — it re-checks the slot after the getter and propagates exceptions with RETURN_IF_EXCEPTION at every call site — but the consequences of getting this wrong (TDZ leaks visible to optimized code, IC/watchpoint invariants, GC across the getter call) are subtle enough that they merit human eyes.

Level of scrutiny

High. This is a semantic extension to module binding in JavaScriptCore's runtime, with explicit reasoning in the PR description about how the baseline IC (emitModuleNamespaceLoad) and DFG constant-folding interact with an empty slot. Those claims look correct on reading the diff, but they depend on invariants outside the changed files. The change also alters when user code can observe side effects during initializeEnvironment, which is spec-adjacent territory. This is not a mechanical or config-level change.

Other factors

  • The feature is unreachable without an embedder using createWithLazyExports, so existing behaviour is preserved by construction (fast path is a dynamicDowncast + null check).
  • Verification is -fsyntax-only plus behavioural tests in a companion Bun PR that isn't linked yet; there are no in-tree tests.
  • The SyntheticSourceProvider constructor signature change is not #if-guarded, but that class is Bun-only per the description.
  • No prior human or bot review has weighed in on the design (CodeRabbit was rate-limited).

Given the scope and the JIT/IC/re-entrancy reasoning involved, deferring to a human reviewer.

@github-actions

Copy link
Copy Markdown

Preview Builds

Commit Release Date
e1b5cb7d autobuild-preview-pr-408-e1b5cb7d 2026-08-11 13:45:41 UTC

@Jarred-Sumner
Jarred-Sumner merged commit 7b76394 into main Aug 11, 2026
43 checks passed
robobun added a commit to oven-sh/bun that referenced this pull request Aug 11, 2026
…s ESM

generateInternalModuleSourceCode turns a builtin's CommonJS exports object
into the synthetic module ESM importers see. It read every own property
with get(), so the accessors builtins use to defer loading parts of
themselves (node:fs's stream classes, which pull in the whole node:stream
stack; tls.rootCertificates; http.globalAgent; timers.promises; ...) all
ran on every import of the module, and only require() callers got the
laziness.

Data properties are still snapshotted at import. Accessors are now
declared as lazy exports of the synthetic record and the exports object is
handed over as their source; JSC reads the property the first time an
importer links the binding or reads it off the namespace object. A builtin
whose exports object has no accessors produces the same record as before.

The engine half is oven-sh/WebKit#408; WEBKIT_VERSION moves from
09e477744721 to its merge commit 7b763944f0ec, which is the only change
between the two.
robobun added a commit to oven-sh/bun that referenced this pull request Aug 11, 2026
…s ESM

generateInternalModuleSourceCode turns a builtin's CommonJS exports object
into the synthetic module ESM importers see. It read every own property
with get(), so the accessors builtins use to defer loading parts of
themselves (node:fs's stream classes, which pull in the whole node:stream
stack; tls.rootCertificates; http.globalAgent; timers.promises; ...) all
ran on every import of the module, and only require() callers got the
laziness.

Data properties are still snapshotted at import. Accessors are now
declared as lazy exports of the synthetic record and the exports object is
handed over as their source; JSC reads the property the first time an
importer links the binding or reads it off the namespace object. A builtin
whose exports object has no accessors produces the same record as before.

The engine half is oven-sh/WebKit#408; WEBKIT_VERSION moves from
09e477744721 to its merge commit 7b763944f0ec, which is the only change
between the two.
robobun added a commit to oven-sh/bun that referenced this pull request Aug 11, 2026
…s ESM

generateInternalModuleSourceCode turns a builtin's CommonJS exports object
into the synthetic module ESM importers see. It read every own property
with get(), so the accessors builtins use to defer loading parts of
themselves (node:fs's stream classes, which pull in the whole node:stream
stack; tls.rootCertificates; http.globalAgent; timers.promises; ...) all
ran on every import of the module, and only require() callers got the
laziness.

Data properties are still snapshotted at import. Accessors are now
declared as lazy exports of the synthetic record and the exports object is
handed over as their source; JSC reads the property the first time an
importer links the binding or reads it off the namespace object. A builtin
whose exports object has no accessors produces the same record as before.

The engine half is oven-sh/WebKit#408; WEBKIT_VERSION moves from
09e477744721 to its merge commit 7b763944f0ec, which is the only change
between the two.
Jarred-Sumner pushed a commit to oven-sh/bun 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 -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants