Skip to content

node:inspector: type the CDP adapter's JSC side against the protocol snapshot and typecheck it in CI - #39471

Draft
robobun wants to merge 4 commits into
mainfrom
farm/4a7e9fb7/cdp-jsc-protocol-types
Draft

node:inspector: type the CDP adapter's JSC side against the protocol snapshot and typecheck it in CI#39471
robobun wants to merge 4 commits into
mainfrom
farm/4a7e9fb7/cdp-jsc-protocol-types

Conversation

@robobun

@robobun robobun commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to #39051.

Problem

  • src/js/internal/inspector/cdp.ts reads every JSC event and response as AnyObject (Record<string, any>): #translateBackendEvent(method, params: AnyObject), #translateResult, #translateConsoleMessage, #translateStackTrace, the onResult callbacks, and the request objects it builds.
  • That is how params.module kept being read after WebKit renamed it to scriptType (node:inspector: derive scriptParsed isModule and scriptLanguage from JSC's scriptType #39051): the read type-checked and the field silently became undefined in Debugger.scriptParsed.
  • packages/bun-inspector-protocol/src/protocol/jsc/index.d.ts is generated from the pinned WebKit (bun-inspector-protocol: regenerate the JSC protocol snapshot from the pinned WebKit #39110) and already describes every shape this file touches, but nothing in src/js referenced it, and nothing in CI typechecks src/js (bun run typecheck covers neither the root solution file's zero sources nor, via test/tsconfig.json, this file; tsc -p src/js has unrelated pre-existing errors).

Fix

  • import type { JSC } from ".../bun-inspector-protocol/src/protocol/jsc/index.d.ts" in cdp.ts. Type-only, so the builtin bundler erases it; the bundled internal/inspector/cdp.js is unchanged apart from the one edit noted below.
  • #sendToBackend<M extends keyof JSC.RequestMap>(method: M, params?: JSC.RequestMap[M], ..., onResult?: (result: JSC.ResponseMap[M], error?) => void): command names, the request objects built for JSC (excess property checks catch a renamed parameter) and the chained onResult callbacks are typed per command. The two places that forward client params verbatim cast them to JSC.RequestMap[typeof method].
  • #translateBackendEvent({ method, params }: BackendEvent): BackendEvent is JSC.EventMap turned into a union discriminated on method, so each case narrows params to that event's type (JSC.Debugger.ScriptParsedEvent, PausedEvent, BreakpointResolvedEvent, JSC.Console.MessageAddedEvent). The Breakpoint pause reason reads data as JSC.Debugger.BreakpointPauseReason, as bun-debug-adapter-protocol does.
  • #translateResult(method, response): TranslatedResponses records which JSC response answers each reshaped CDP command (not always the namesake: getPossibleBreakpoints is served by getBreakpointLocations, evaluate with awaitPromise by Runtime.awaitPromise); each case narrows with response as TranslatedResponses[typeof method].
  • #translateConsoleMessage / #translateStackTrace take JSC.Console.ConsoleMessage / JSC.Console.StackTrace; SCOPE_TYPE_MAP, CONSOLE_TYPE_MAP and CONSOLE_LEVEL_MAP are keyed by the snapshot's enums, so a renamed scope type, console type or level fails too.
  • handleBackendMessage parses into a small BackendMessage envelope type (error.code is a number, per BackendDispatcher::sendPendingErrors).
  • The client-facing CDP shapes (#dispatchClientCommand params, everything passed to #replyToClient / #emitToClient) stay AnyObject, as requested.
  • No runtime behavior change. Existing defensive fallbacks (?? 0, ?.) are left in place even where the snapshot marks the field required; the one that does not type-check, params.message || {} on Console.messageAdded (where message is required), is dropped.
  • Test: test/cli/inspect/bun-inspector-protocol.test.ts already checks that the snapshot matches what this build of bun sends. It now also typechecks cdp.ts against the snapshot (cdp-protocol-types-fixture.mts, the options of tsc -p src/js restricted to this file), so regenerating the snapshot after a WebKit bump reports each field cdp.ts still reads or sends under the old name. It then renames an event parameter (scriptType), a response field (wasThrown) and a request parameter (doNotPauseOnExceptionsAndMuteConsole) in a copy of the snapshot and checks that each is reported at cdp.ts; against the current AnyObject version of cdp.ts nothing is reported, so this half fails without the cdp.ts change. The fixture runs under node (present on every CI lane, and already used this way by other tests) because the debug build of bun takes about 20 seconds just to load typescript.js; the test takes ~0.6s.
  • Verified:
    • bun bd test test/cli/inspect/bun-inspector-protocol.test.ts: passes; fails as described above with main's cdp.ts swapped in.
    • bun bd test test/js/node/inspector/inspector.test.ts (24 pass) and inspector-profiler.test.ts (45 pass).
    • tsc --noEmit -p src/js: no diagnostics in inspector/ before or after; the project's pre-existing error count elsewhere is unchanged. Renaming five further snapshot fields by hand produced 0 errors in cdp.ts on main and 7 on this branch (output below).
    • The fixture and the new test also pass on Windows (paths are compared with separators normalized, since TypeScript reports forward slashes there).

Sequencing

Background

  • node:inspector clients (DevTools, vscode-js-debug) speak V8's Chrome DevTools Protocol; Bun's inspector backend speaks WebKit's JSC inspector protocol. cdp.ts is the per-connection translator between the two: it rewrites client commands into JSC commands, correlates the responses, and rewrites JSC events into CDP events.
  • bun-inspector-protocol/src/protocol/jsc/index.d.ts is a generated TypeScript description of the JSC protocol: one type per event, request and response (JSC.Debugger.ScriptParsedEvent, JSC.Runtime.EvaluateRequest, ...) plus EventMap / RequestMap / ResponseMap indexing them by protocol method name. scripts/generate-protocol.ts regenerates it from the WebKit version bun bd links against, which is what makes a type error here equivalent to "WebKit changed this field".
  • A discriminated union is a union of object types that share a literal-typed property (method here); TypeScript narrows the whole object, including params, inside a switch on that property. typeof method inside a case is the narrowed literal type, so TranslatedResponses[typeof method] is the response type for exactly the commands listed in that case.
  • src/js/builtins.d.ts declares the $-prefixed intrinsics builtins use (map.$get, ...). The fixture loads it but drops its references to the build's codegen output (which is what types require() per module and does not exist in a test checkout) and declares require() loosely instead; only the JSC side is under test.
Simulated snapshot renames (manual, in addition to the three the test performs)

Applied to jsc/index.d.ts: scriptType -> module, pause reason "Breakpoint" -> "BreakpointHit", doNotPauseOnExceptionsAndMuteConsole -> muteConsole, StackTrace.parentStackTrace -> parent, GetPropertiesResponse.internalProperties -> internals.

On main: tsc --noEmit -p src/js 2>&1 | grep inspector/ prints nothing.

On this branch:

cdp.ts(280,11): error TS2353: Object literal may only specify known properties, and 'doNotPauseOnExceptionsAndMuteConsole' does not exist in type 'EvaluateRequest'.
cdp.ts(355,15): error TS2353: Object literal may only specify known properties, and 'doNotPauseOnExceptionsAndMuteConsole' does not exist in type 'CallFunctionOnRequest'.
cdp.ts(520,13): error TS2353: Object literal may only specify known properties, and 'doNotPauseOnExceptionsAndMuteConsole' does not exist in type 'EvaluateOnCallFrameRequest'.
cdp.ts(610,17): error TS2339: Property 'internalProperties' does not exist on type 'GetPropertiesResponse'.
cdp.ts(635,17): error TS2339: Property 'scriptType' does not exist on type 'ScriptParsedEvent'.
cdp.ts(676,16): error TS2678: Type '"Breakpoint"' is not comparable to type '"URL" | "assert" | ... | "BreakpointHit" | ... | "other"'.
cdp.ts(723,13): error TS2339: Property 'parentStackTrace' does not exist on type 'StackTrace'.

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

fails on main (without fix)
ASAN without fix: 1 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/cli/inspect/bun-inspector-protocol.test.ts
bun test v1.4.0 (8326d1bd3)

test/cli/inspect/bun-inspector-protocol.test.ts:
(pass) the protocol snapshot in packages/bun-inspector-protocol matches what bun sends [889.41ms]
315 |     // Failures here after regenerating the snapshot are the fields WebKit renamed or dropped that
316 |     // cdp.ts still reads or sends, one diagnostic per use site.
317 |     expect(diagnostics).toEqual([]);
318 |     expect(
319 |       renamed.filter(name => diagnosticsAfterRenames.some(diagnostic => diagnostic.includes(`'${name}'`))),
320 |     ).toEqual(renamed);
            ^
error: expect(received).toEqual(expected)

- [
-   "scriptType",
-   "wasThrown",
-   "doNotPauseOnExceptionsAndMuteConsole",
- ]
+ []

- Expected  - 5
+ Received  + 1

      at <anonymous> (/workspace/bun/test/cli/inspect/bun-inspector-protocol.test.ts:320:7)
(fail) src/js/internal/inspector/cdp.ts reads JSC messages through the snapshot's types [606.56ms]

 1 pass
 1 fail
 10 expect() calls
Ran 2 tests across 1 file. [3.87s]
e
... (truncated)

release without fix: 1 FAILED
bun test v1.4.0-canary.1 (8326d1bd3)

test/cli/inspect/bun-inspector-protocol.test.ts:
(pass) the protocol snapshot in packages/bun-inspector-protocol matches what bun sends [20.74ms]
315 |     // Failures here after regenerating the snapshot are the fields WebKit renamed or dropped that
316 |     // cdp.ts still reads or sends, one diagnostic per use site.
317 |     expect(diagnostics).toEqual([]);
318 |     expect(
319 |       renamed.filter(name => diagnosticsAfterRenames.some(diagnostic => diagnostic.includes(`'${name}'`))),
320 |     ).toEqual(renamed);
            ^
error: expect(received).toEqual(expected)

- [
-   "scriptType",
-   "wasThrown",
-   "doNotPauseOnExceptionsAndMuteConsole",
- ]
+ []

- Expected  - 5
+ Received  + 1

      at <anonymous> (/workspace/bun/test/cli/inspect/bun-inspector-protocol.test.ts:320:7)
(fail) src/js/internal/inspector/cdp.ts reads JSC messages through the snapshot's types [562.26ms]

 1 pass
 1 fail
 10 expect() calls
Ran 2 tests across 1 file. [736.00ms]
__F:1:S:0
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/cli/inspect/bun-inspector-protocol.test.ts
bun test v1.4.0 (8326d1bd3)

test/cli/inspect/bun-inspector-protocol.test.ts:
(pass) the protocol snapshot in packages/bun-inspector-protocol matches what bun sends [931.13ms]
(pass) src/js/internal/inspector/cdp.ts reads JSC messages through the snapshot's types [684.73ms]

 2 pass
 0 fail
 10 expect() calls
Ran 2 tests across 1 file. [4.00s]
__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     936d384c8f
  features     baseline

22 deps, 120 codegen, 1174 objects in 688ms

ninja: Entering directory `/workspace/bun/build/release'
[1/1235] install /workspace/bun
bun install v1.4.0-canary.1 (8326d1bd3)

Checked 26 installs across 63 packages (no changes) [11.00ms]
[2/1235] gen ErrorCode+*.h
[3/1235] gen bindgenv2
[4/1235] install /workspace/bun/packages/bun-error
bun install v1.4.0-canary.1 (8326d1bd3)

Checked 1 install across 2 packages (no changes) [2.00ms]
[5/1235] fetch tinycc
[tinycc] up to date
[6/1234] install /workspace/bun/src/node-fallbacks
bun install v1.4.0-canary.1 (8326d1bd3)

Checked 111 installs across 104 packages (no changes) [11.00ms]
[7/1234] fetch zlib
[zlib] up to date
[8/1234] gen ProcessBindingConstants.lut.h
Generating /workspace/bun/build/release/codegen/ProcessBindingConstants.lut.h from /workspace/bun/src/jsc/bindings/ProcessBindingConstants.cpp
[9/1234] fetch libjpeg-turbo
[libjpeg-turbo] up to date
[10/1234] gen .bind.ts → GeneratedBindings.cpp
[
... (truncated)
diff hotspot
src/js/internal/inspector/cdp.ts                | 107 +++++++++++++++++-------
 test/cli/inspect/bun-inspector-protocol.test.ts |  64 +++++++++++++-
 test/cli/inspect/cdp-protocol-types-fixture.mts |  53 ++++++++++++
 3 files changed, 189 insertions(+), 35 deletions(-)

gate history · 1 passed · 1 rejected · iteration 1

evidence per changed file
file                                             reads  edits  tests
src/js/internal/inspector/cdp.ts                    13     34      0
test/cli/inspect/bun-inspector-protocol.test.ts      5      6      0
test/cli/inspect/cdp-protocol-types-fixture.mts      1      1      0

…ocol snapshot

cdp.ts read every JSC event and response as Record<string, any>, which is
how `params.module` kept being read after WebKit renamed it to `scriptType`:
the field silently became undefined in Debugger.scriptParsed.

Import the JSC protocol types from bun-inspector-protocol (type-only, so the
builtin bundler erases it) and use them for everything that crosses to the
backend: #sendToBackend is generic over the command name so request params
and onResult callbacks are typed per command, #translateBackendEvent takes a
discriminated union keyed on the event name, #translateResult narrows each
reshaped response via TranslatedResponses, and the console/stack trace
helpers and the scope/console lookup tables take the snapshot's types.

No runtime change; the only non-type edit drops the `|| {}` fallback on
Console.messageAdded's required `message` parameter.
@robobun

robobun commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

Status: follow-up to #39051 requested by @alii. cdp.ts's backend side is typed against bun-inspector-protocol's JSC snapshot, and test/cli/inspect/bun-inspector-protocol.test.ts now typechecks it against the snapshot (verified locally on Linux and Windows; the typecheck half of the test fails against main's cdp.ts). Draft until the order relative to #34719 / #35752 / #36457 is decided; will rebase and type whatever those add to cdp.ts once they land.

@robobun

robobun commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 4:36 PM PT - Aug 17th, 2026

@robobun, your commit 936d384c8fa36c7686f6a3e8e64dc1bcd7d869b8 passed in Build #100230! 🎉


🧪   To try this PR locally:

bunx bun-pr 39471

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

bun-39471 --bun

bun-inspector-protocol.test.ts already checks that the snapshot matches what
this build of bun sends. Add the other half: typecheck cdp.ts against the
snapshot (under node, since the debug build takes tens of seconds just to
load typescript.js), and check that renaming an event parameter, a response
field and a request parameter in the snapshot is reported at cdp.ts's uses
of them, so the typecheck cannot silently stop covering the JSC side.
Comment thread src/js/internal/inspector/cdp.ts Outdated
Comment thread src/js/internal/inspector/cdp.ts Outdated
Comment thread src/js/internal/inspector/cdp.ts Outdated
Comment thread src/js/internal/inspector/cdp.ts Outdated
Comment thread src/js/internal/inspector/cdp.ts Outdated
Comment thread src/js/internal/inspector/cdp.ts Outdated
Comment thread src/js/internal/inspector/cdp.ts Outdated
Comment thread src/js/internal/inspector/cdp.ts Outdated
Comment thread src/js/internal/inspector/cdp.ts Outdated
Comment thread src/js/internal/inspector/cdp.ts Outdated
Comment thread src/js/internal/inspector/cdp.ts Outdated
@robobun robobun changed the title node:inspector: type the JSC side of the CDP adapter against the protocol snapshot node:inspector: type the CDP adapter's JSC side against the protocol snapshot and typecheck it in CI Aug 17, 2026
@robobun

robobun commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

Since the first revision: the added comments in cdp.ts were removed or cut to single lines (fc03f27, 936d384), and test/cli/inspect/bun-inspector-protocol.test.ts now typechecks cdp.ts against the snapshot and checks that renamed snapshot fields are reported at cdp.ts's use sites (e6973d9; passes on Linux and Windows, fails against main's cdp.ts). PR description updated to match.

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.

2 participants