Skip to content

bun-types: declare the Error base and the runtime members of BuildMessage and ResolveMessage - #38564

Open
robobun wants to merge 1 commit into
mainfrom
farm/eb8cad11/build-message-types
Open

bun-types: declare the Error base and the runtime members of BuildMessage and ResolveMessage#38564
robobun wants to merge 1 commit into
mainfrom
farm/eb8cad11/build-message-types

Conversation

@robobun

@robobun robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • declare class BuildMessage and declare class ResolveMessage in packages/bun-types/globals.d.ts declare 4 and 9 members and no base class. The runtime objects (src/jsc/resolve_message.classes.ts, getters in src/jsc/BuildMessage.rs and src/jsc/ResolveMessage.rs) have Error.prototype in their chain (prototypeBase: "Error") and also expose line, column, toString(), toJSON() and Symbol.toPrimitive on both, notes on BuildMessage, and stack and requireStack on ResolveMessage.
  • So ordinary code over Bun.build() logs or a caught import/require failure is a type error while working at runtime: log.line, log.column, log.stack, log.notes, err.requireStack, log.toJSON() all fail with TS2339 (Property 'line' does not exist on type 'BuildMessage | ResolveMessage').
  • Reproduces with bun 1.4.0 and current main; probe below.

Fix

  • Both classes now extends Error and declare the members above. Every declared member is one the runtime defines, with the type the getter returns:
    • line / column: number, documented as zero-based (get_line / get_column return location.line - 1, and 0 without a location), unlike the one-based position.line / position.column.
    • BuildMessage.notes: BuildMessage[] (get_notes wraps each note as a BuildMessage of kind note).
    • ResolveMessage.stack: string, mutable like the runtime accessor (it has a setter) and like Error.stack. BuildMessage gets no stack of its own because the runtime defines none there; the inherited optional Error.stack is what it actually has (undefined).
    • ResolveMessage.requireStack: string[] | undefined. The getter always exists and returns undefined for anything but a failed require() / require.resolve(), so it is declared as always present rather than optional.
    • toJSON(): Pick<...> of exactly the keys the runtime to_json writes (name, position, message, level, plus specifier, importKind, referrer on ResolveMessage). Pick keeps it in sync with the member declarations, including the level / importKind unions being corrected in bun-types: type BuildMessage/ResolveMessage level and importKind as the strings the runtime reports #38542.
    • [Symbol.toPrimitive]: string for the "default" / "string" hints, string | null otherwise, which is what to_primitive does.
  • The level and importKind literal unions are wrong too but are fixed by bun-types: type BuildMessage/ResolveMessage level and importKind as the strings the runtime reports #38542; those lines are left untouched here so the two changes compose. ResolveMessage/BuildMessage: add .stack property #35632 adds a real stack to BuildMessage at runtime and would tighten the inherited declaration when it lands.
  • docs/bundler/index.mdx carries two hand-written copies of these declarations; both updated to match (the first one also described ResolveMessage as extending BuildMessage, which it does not: resolveMessage instanceof BuildMessage is false).
  • Verified with test/integration/bun-types/bun-types.test.ts. The new case builds a file with a redeclared variable and a file with an unresolvable import, asserts the runtime shape (Error base, note array, zero-based line/column next to the one-based position, requireStack for require() only, toJSON() keys, toPrimitive results), then type-checks code using exactly that shape against the packed declarations with tsc. The tsc helper is shared with the existing Bun.mmap case and runs on debug builds too. test/integration/bun-types/fixture/build.ts pins the exact member types and is checked under each tsconfig the file runs (no lib, DOM, tsgo) on release builds.
  • Without the globals.d.ts change the new case fails with 22 tsc errors and the fixture fails under every tsconfig; with it the file passes under both bun test (release, 16 pass) and bun bd test (debug).

Background

  • Bun.build() returns each bundler diagnostic in logs as a BuildMessage, or a ResolveMessage when it came from failing to resolve an import. A failed import / import() / require() at runtime throws the same classes.
  • These are native classes generated from .classes.ts definitions: every member listed under proto there is a getter or method on the prototype, and prototypeBase: "Error" puts Error.prototype behind that prototype, which is why instanceof Error holds but the constructor-side statics of Error are not involved.
  • requireStack mirrors the property Node.js puts on its MODULE_NOT_FOUND errors; Bun records only the direct caller, so it is a one-element array.
Runtime probe (bun 1.4.0, same on a debug build of main)
// entry.ts: import "./does-not-exist";   bad.ts: let a = 1; let a = 2;
const r = await Bun.build({ entrypoints: ["./entry.ts", "./bad.ts"], throw: false });
const [m, b] = [r.logs.find(l => l instanceof ResolveMessage), r.logs.find(l => l instanceof BuildMessage)] as any[];
console.log(m instanceof Error, m instanceof BuildMessage, m.line, m.column, m.position.line, m.position.column, typeof m.stack, m.requireStack, Object.keys(m.toJSON()));
// true false 0 7 1 8 string undefined [ "name", "position", "message", "level", "specifier", "importKind", "referrer" ]
console.log(b instanceof Error, b.line, b.column, b.notes.map((n: any) => [n.constructor.name, n.level]), b.stack, Object.keys(b.toJSON()), b[Symbol.toPrimitive]("number"), `${b}`);
// true 0 15 [ [ "BuildMessage", "note" ] ] undefined [ "name", "position", "message", "level" ] null BuildMessage: "a" has already been declared
try { require("./missing"); } catch (e: any) { console.log(e.requireStack, e.stack); }
// [ "/tmp/x/probe.ts" ] ResolveMessage: Cannot find module './missing'\nRequire stack:\n- /tmp/x/probe.ts

tsc over the same property accesses with the declarations from main:

error TS2339: Property 'line' does not exist on type 'BuildMessage | ResolveMessage'.
error TS2339: Property 'stack' does not exist on type 'ResolveMessage'.
error TS2339: Property 'requireStack' does not exist on type 'ResolveMessage'.
error TS2339: Property 'notes' does not exist on type 'BuildMessage'.
error TS2339: Property 'toJSON' does not exist on type 'BuildMessage'.

…sage and ResolveMessage

Both classes put Error.prototype in their chain and expose line, column,
toString(), toJSON() and Symbol.toPrimitive; BuildMessage also has notes,
ResolveMessage also has stack and requireStack. The declarations in
globals.d.ts listed none of these, so reading them was a type error.

The hand-maintained copies of the declarations in docs/bundler/index.mdx
are updated to match. The bun-types test builds two files, checks the
runtime shape, and type-checks code that uses exactly that shape; the
fixture pins the exact member types under every tsconfig it is run with.
@robobun
robobun requested a review from alii as a code owner August 14, 2026 15:26
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

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

Next review available in: 37 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 64f03668-532d-4ecc-bf8a-2e68ba2adc18

📥 Commits

Reviewing files that changed from the base of the PR and between eabb96d and 40eb383.

📒 Files selected for processing (4)
  • docs/bundler/index.mdx
  • packages/bun-types/globals.d.ts
  • test/integration/bun-types/bun-types.test.ts
  • test/integration/bun-types/fixture/build.ts

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

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Status: types-only change, ready for review. Automated review found nothing to change; packages/bun-types/ is CODEOWNER-routed, so this is waiting on @alii.

CI: the Buildkite build for 40eb383 is green on all 177 jobs that ran. The only two jobs that did not pass are the two darwin 14 aarch64 - test-bun jobs, which never got an agent and expired after ~4.5 hours; the same two jobs expired at the same minute on every other PR build queued in that window, so it is mac capacity, not this change (a .d.ts and docs diff has nothing that lane could exercise). Retrying those two jobs, or a fresh build once the queue drains, is all that is left. The GitHub "TypeScript types" check, which runs the bun-types test including the new fixture assertions, passed.

Reproduced by running the API on bun 1.4.0 and a debug build of main: both classes are instanceof Error and carry line / column / toJSON() (plus notes on BuildMessage, stack / requireStack on ResolveMessage), while tsc rejects every one of those accesses with the declarations from main (TS2339: Property 'line' does not exist on type 'BuildMessage | ResolveMessage', 22 errors over the test's usage).

Verification:

  • test/integration/bun-types/bun-types.test.ts (new BuildMessage / ResolveMessage case): fails with the declarations from main, passes with this branch, under both the release binary (16 pass) and a debug build.
  • test/integration/bun-types/fixture/build.ts pins the exact member types; checked under the no-lib, DOM and tsgo configurations on the release run.

Related: #38542 fixes the level / importKind unions on the same declarations; those lines are intentionally untouched here so the two merge independently.

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

I reviewed this PR and didn't find any bugs. Deferring to a human because packages/bun-types/ is CODEOWNER-protected.

What was reviewed:

  • Each new member (line, column, notes, stack, requireStack, toJSON, toString, Symbol.toPrimitive, extends Error) traced to its .classes.ts proto entry and Rust getter — types and JSDoc match what the getters return, including the zero-based line/column and the exact toJSON key sets.
  • ResolveMessage.stack: string (non-optional, mutable) matches get_stack always returning a string and the writable: true accessor; BuildMessage correctly inherits the optional Error.stack instead.
  • The refactored expectToTypecheck helper preserves the existing Bun.mmap case's behavior; the new runtime-shape assertions and fixture @ts-expect-error guards are non-vacuous.
Extended reasoning...

Overview

This PR adds missing member declarations to the global BuildMessage and ResolveMessage classes in packages/bun-types/globals.d.ts: extends Error, line/column, notes, stack, requireStack, toString(), toJSON(), and [Symbol.toPrimitive]. It updates two hand-written copies in docs/bundler/index.mdx to match, extracts an expectToTypecheck helper in test/integration/bun-types/bun-types.test.ts (reused by the existing Bun.mmap case), adds a runtime+tsc test case for the new members, and extends test/integration/bun-types/fixture/build.ts with expectType pins.

Security risks

None. Pure .d.ts additions, docs, and test fixtures — no runtime code path changes.

Level of scrutiny

Medium-low for correctness (types-only, purely additive, no existing declarations narrowed), but packages/bun-types/ has a CODEOWNER entry, which per the approval guidelines means a human should sign off. I cross-checked every new declaration against src/jsc/resolve_message.classes.ts, src/jsc/BuildMessage.rs, and src/jsc/ResolveMessage.rs:

  • prototypeBase: "Error" on both → extends Error is accurate.
  • get_line/get_column return location.{line,column} - 1 or 0number, zero-based JSDoc is correct.
  • get_notes wraps each note as a BuildMessage with Kind::NoteBuildMessage[] is correct.
  • get_require_stack returns UNDEFINED unless ImportKind::Require | RequireResolve, else a string array → string[] | undefined is correct.
  • get_stack (ResolveMessage only) always returns a string; writable: true in the classes.ts → mutable stack: string is correct. BuildMessage has no stack in its proto, so inheriting Error's optional stack?: string is the right shape.
  • to_json writes exactly the keys the Pick<...> types name.
  • to_primitive returns the toString result for "default"/"string" hints and NULL otherwise → the two-overload signature is accurate.

Other factors

The refactor of the mmap test into expectToTypecheck is behavior-preserving (same tsconfig setup, same spawn, same assertions). The new test case both asserts runtime shape and drives tsc against the packed declarations, and the fixture adds @ts-expect-error guards so over-declaring would also fail. The docs change fixes a factual error (ResolveMessage extends BuildMessage was wrong; instanceof disproves it). The only reason not to auto-approve is the CODEOWNER on packages/bun-types/.

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.

1 participant