Skip to content

bun-types: defer Event and EventTarget to lib.dom when it is loaded - #40576

Merged
alii merged 1 commit into
mainfrom
farm/8e0af94b/bun-types-event-libdom
Aug 29, 2026
Merged

alii merged 1 commit into
mainfrom
farm/8e0af94b/bun-types-event-libdom

Conversation

@robobun

@robobun robobun commented Aug 26, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • With lib: ["dom"], the global Event merges lib.dom's composedPath(): EventTarget[] with bun-types' composedPath(): [EventTarget?] (packages/bun-types/globals.d.ts:283). ReturnType<Event["composedPath"]> resolves to the tuple, so a real DOM path is rejected: error TS2322: Type 'EventTarget[]' is not assignable to type '[(EventTarget | undefined)?]'.
  • Every DOM type that extends EventTarget (Node, Element, Document) inherits the problem. Fixes bun-types global Event merges with lib.dom, making composedPath() unsatisfiable #40574.

Fix

  • Move the Node-style Event and EventTarget members into Bun.__internal.BunEvent and BunEventTarget, gated on LibDomIsLoaded. The global interfaces extend the gated types, so with lib.dom loaded bun-types contributes {} and lib.dom's declarations win.
  • This is the pattern the file already uses for ReadableStream, CompressionStream, and TextDecoder (LibEmptyOr* on LibDomIsLoaded).
  • Without lib.dom nothing changes: the tuple type and all other members stay as before.
  • Verified: test/integration/bun-types/bun-types.test.ts gets two new tsc checks (with and without lib.dom) plus a fixture check in fixture/dom.ts. The with-dom check fails on main with the exact TS2322 above. All 19 tests in the file pass with the fix.

Background

  • bun-types declares globals that also exist in lib.dom. For conflicting ones it detects lib.dom through typeof globalThis extends { onabort: any } (Bun.__internal.LibDomIsLoaded) and contributes an empty type when lib.dom is present.
  • Event and EventTarget were bare global interfaces, so they merged with lib.dom instead of deferring. Methods with different signatures merge as overloads, which is why tsc reports no merge error, only an unsatisfiable overload pair.
  • The declare var Event and declare var EventTarget constructors stay as they are. Their types are identical to lib.dom's, so the merge of the value declarations was never the problem.

[review] gate passed · iteration 0 · 3 files touched

fails on main (without fix)
ASAN without fix: 1 failed, 12 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/integration/bun-types/bun-types.test.ts
bun test v1.4.1 (adc354d99)

test/integration/bun-types/bun-types.test.ts:
(pass) @types/bun integration test > building and packing bun-types leaves packages/bun-types untouched [2.92ms]
(pass) @types/bun integration test > packed bun-types includes CLAUDE.md [6.21ms]
(skip) @types/bun integration test > basic type checks > checks without lib.dom.d.ts
(skip) @types/bun integration test > tsgo (TypeScript 7 native preview) > checks without lib.dom.d.ts
(pass) @types/bun integration test > Bun.mmap > MMapOptions accepts offset and size [747.11ms]
(pass) @types/bun integration test > TextDecoder > accepts the encoding labels the runtime supports [771.61ms]
(pass) @types/bun integration test > TextDecoder > the fixture label table matches the runtime [39.04ms]
497 |       });
498 | 
499 |       const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
500 | 
501 |       expect(stderr.trim()).toBe("");
502 |       expect(stdout.trim()).toBe("");
... (truncated)

release without fix: 2 FAILED
bun test v1.4.1-canary.1 (adc354d99)

test/integration/bun-types/bun-types.test.ts:
(pass) @types/bun integration test > building and packing bun-types leaves packages/bun-types untouched [0.10ms]
(pass) @types/bun integration test > packed bun-types includes CLAUDE.md [0.23ms]
(pass) @types/bun integration test > basic type checks > checks without lib.dom.d.ts [3637.24ms]
(pass) @types/bun integration test > tsgo (TypeScript 7 native preview) > checks without lib.dom.d.ts [696.04ms]
(pass) @types/bun integration test > Bun.mmap > MMapOptions accepts offset and size [76.78ms]
(pass) @types/bun integration test > TextDecoder > accepts the encoding labels the runtime supports [63.86ms]
(pass) @types/bun integration test > TextDecoder > the fixture label table matches the runtime [0.83ms]
497 |       });
498 | 
499 |       const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
500 | 
501 |       expect(stderr.trim()).toBe("");
502 |       expect(stdout.trim()).toBe("");
                                  ^
error: expect(received).toBe(expected)

- ""
+ "event-check.ts(4,23): error TS2322: Type 'EventTarget[]' is not 
... (truncated)
passes on PR (with fix)
ASAN with fix: 12 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/integration/bun-types/bun-types.test.ts
bun test v1.4.1 (adc354d99)

test/integration/bun-types/bun-types.test.ts:
(pass) @types/bun integration test > building and packing bun-types leaves packages/bun-types untouched [2.37ms]
(pass) @types/bun integration test > packed bun-types includes CLAUDE.md [6.33ms]
(skip) @types/bun integration test > basic type checks > checks without lib.dom.d.ts
(skip) @types/bun integration test > tsgo (TypeScript 7 native preview) > checks without lib.dom.d.ts
(pass) @types/bun integration test > Bun.mmap > MMapOptions accepts offset and size [938.07ms]
(pass) @types/bun integration test > TextDecoder > accepts the encoding labels the runtime supports [1058.21ms]
(pass) @types/bun integration test > TextDecoder > the fixture label table matches the runtime [42.60ms]
(pass) @types/bun integration test > Event and EventTarget > lib.dom's composedPath() declaration wins when lib.dom is loaded [797.98ms]
(pass) @types/bun integration test > Event and EventTarget > the Node-style composedPath() tuple app
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 678ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/5] gen generated_host_exports.rs
generated_host_exports.rs: 122 exports (host=5, lazy=10, generic=107, rust=0); 243 extern-C blocks audited
[1/5] cargo bun_runtime → libbun_runtime.a (--target x86_64-unknown-linux-gnu)

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

�[1m�[92m   Compiling�[0m bun_http v0.0.0 (/workspace/bun/src/http)
�[1m�[92m   Compiling�[0m bun_standalone_graph v0.0.0 (/workspace/bun/src/standalone_graph)
�[1m�[92m   Compiling�[0m bun_bunfig v0.0.0 (/workspace/bun/src/bunfig)
�[1m�[92m   Compiling�[0m bun_install v0.0.0 (/workspace/bun/src/install)
�[1m�[92m   Compiling�[0m bun_jsc v0.0.0 (/workspace/bun/src/jsc)
�[1m�[92m   Compiling�[0m bun_js_parser_jsc v0.0.0 (/workspace/bun/src/js_parser_jsc)
�[1m�[92m   Compiling�[0m bun_css_jsc v0.0.0 (/workspace/bun/src/css_jsc)
�[1m�[92m   Compiling�[0m bun_sql_jsc v0.0.0 (/workspace/bun/src/sql_jsc)
�[1m�[92m   Compiling�[0m bun_ast_jsc v0.0.0 (/workspace/bun/src/ast_jsc)
�[1m�[92m   C
... (truncated)
diff hotspot
packages/bun-types/globals.d.ts              | 138 +++++++++++++++------------
 test/integration/bun-types/bun-types.test.ts |  50 ++++++++++
 test/integration/bun-types/fixture/dom.ts    |  12 +++
 3 files changed, 140 insertions(+), 60 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                          reads  edits  tests
packages/bun-types/globals.d.ts                   1      2      0
test/integration/bun-types/bun-types.test.ts      1      1      0
test/integration/bun-types/fixture/dom.ts         1      1      0

root cause · written by the author bot

The bug was that bun-types declared its Node-style Event and EventTarget members directly on the global interfaces, so when lib.dom was also loaded the declarations merged and the Node-flavoured composedPath(): [EventTarget?] signature conflicted with lib.dom's composedPath(): EventTarget[], making the merged type unsatisfiable by any DOM implementation. The fix moves those member bodies into Bun.__internal.BunEvent and BunEventTarget behind the existing LibDomIsLoaded conditional, following the same pattern already used for types like ReadableStream and TextDecoder. As a result, the global…

With lib.dom loaded, the bare global Event interface merged the
Node-style composedPath(): [EventTarget?] with lib.dom's
composedPath(): EventTarget[], which made the merged type
unsatisfiable. Gate the Node-style members on LibDomIsLoaded, the
same pattern used for ReadableStream and TextDecoder.

Fixes #40574
@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

On-demand reviews are free for the next 25 days. After that, they cost $0.25 per reviewed file.

Or wait 2 minutes for your next included review.

View limit details

Limit details: You’ve used all 5 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 17ebfb52-5ebc-4b70-bf9a-f491aabbbe0b

📥 Commits

Reviewing files that changed from the base of the PR and between f4d864b and 9d59126.

📒 Files selected for processing (3)
  • packages/bun-types/globals.d.ts
  • test/integration/bun-types/bun-types.test.ts
  • test/integration/bun-types/fixture/dom.ts

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

@robobun

robobun commented Aug 26, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 3:07 PM PT - Aug 26th, 2026

❌ @robobun, your commit 9d59126 has 3 failures in Build #106384 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 40576

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

bun-40576 --bun

@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 and didn't find any bugs — the change follows the existing LibEmptyOr* / LibDomIsLoaded pattern exactly and is covered by both with-dom and without-dom tsc checks. Leaving final sign-off to a human since packages/bun-types/ has a CODEOWNERS entry.

What was reviewed

  • Confirmed BunEvent/BunEventTarget members are a straight move of the previous global bodies; only difference is the Bun.-qualified listener/option types, which resolve to the same interfaces in bun.d.ts.
  • Checked the new conditional types match the ~30 sibling LibEmptyOr* declarations already in globals.d.ts.
  • Verified the two new tsc test cases exercise both branches (lib.dom present → EventTarget[] assignable; absent → tuple type retained) and follow the neighboring Bun.mmap test's spawn/assert shape.
Extended reasoning...

Overview

This is a types-only fix in packages/bun-types/globals.d.ts for #40574: Bun's global Event/EventTarget interface bodies unconditionally merged with lib.dom.d.ts, and the composedPath(): [EventTarget?] tuple signature conflicted with lib.dom's composedPath(): EventTarget[]. The fix relocates the member declarations into Bun.__internal.BunEvent / BunEventTarget and gates them behind LibDomIsLoaded-conditional LibEmptyOrBunEvent / LibEmptyOrBunEventTarget types, so the global interfaces contribute {} when lib.dom is present. Two new tsc-driven tests in test/integration/bun-types/bun-types.test.ts and a satisfies check in fixture/dom.ts cover both configurations.

Security risks

None. This touches only ambient TypeScript declarations and test fixtures that invoke tsc; no runtime code, no I/O, no untrusted input handling.

Level of scrutiny

Low-to-moderate. The change is mechanical and mirrors the exact pattern already used ~30 times in the same file (ReadableStream, WebSocket, TextDecoder, PerformanceEntry, etc. all use interface X extends Bun.__internal.LibEmptyOr...). The moved member list is byte-identical to the removed block aside from Bun.-prefixing the listener/option parameter types, which is required now that the interface lives inside declare module "bun" — I confirmed Bun.EventListener, Bun.EventListenerObject, Bun.AddEventListenerOptions, and Bun.EventListenerOptions all exist in bun.d.ts. The declare var Event / declare var EventTarget constructor declarations are left untouched, which is correct since they were never the source of the conflict.

Other factors

.github/CODEOWNERS assigns /packages/bun-types/ to a specific owner, so per the approval guidelines I'm deferring rather than approving outright. The tests are well-structured (stderr/stdout asserted before exit code, await using on the spawned process, both branches of the conditional covered), and the fixture addition in dom.ts gives ongoing coverage in the main fixture run. No prior review comments exist on the PR.

@alii
alii merged commit 1ab272b into main Aug 29, 2026
8 of 10 checks passed
@alii
alii deleted the farm/8e0af94b/bun-types-event-libdom branch August 29, 2026 08:48
@robobun

robobun commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the review. The remaining red buildkite lanes are failures that also reproduce without this diff (bun-build-api snapshot hashes, bun-patch on Windows 2019, an ICU-dependent URL test on darwin x64). They are reported separately. The TypeScript types check and the bun-types integration tests are green.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bun-types global Event merges with lib.dom, making composedPath() unsatisfiable

2 participants