@tlockney/cdp-kernel 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,7 +45,8 @@ Each install writes a Jupyter kernelspec: `cdp-node`, `cdp-deno`,
45
45
  `cdp-node-deno` (Node with the Deno compatibility namespace), `cdp-bun`,
46
46
  `cdp-workerd`, or `cdp-celld`. Then pick the kernel in JupyterLab or VS Code.
47
47
 
48
- **From npm** (host runs on Node 24+; Node 26 is what's been tested):
48
+ **From npm** (host runs on Node 24+). CI tests Node 24 and 26 with Deno 2.9
49
+ and the latest 2.x, on Linux and macOS. Windows isn't supported yet.
49
50
 
50
51
  ```sh
51
52
  npm install -g @tlockney/cdp-kernel
@@ -80,10 +81,17 @@ Windows build).
80
81
  Deno 2.9+ refuses package versions less than 24 hours old by default
81
82
  ([minimum dependency age](https://docs.deno.com/go/minimum-dependency-age)).
82
83
  In a release's first day, the install command fails with "blocked by the
83
- minimum dependency age policy". Wait out the day, or install an earlier
84
- release. Passing `--min-dep-age=0` gets the install through, but the
85
- kernelspec doesn't carry the flag, so the kernel still won't launch until
86
- the release is a day old.
84
+ minimum dependency age policy". Pass `--min-dep-age=0` to the install
85
+ command to get it through:
86
+
87
+ ```sh
88
+ deno run -A --min-dep-age=0 jsr:@tlockney/cdp-kernel/cli install --runtime deno
89
+ ```
90
+
91
+ The kernelspec it writes works on release day too. It relaunches the kernel
92
+ with a dependency cutoff of two days after the release
93
+ (`--min-dep-age=<date>`), so it never resolves anything published after
94
+ that.
87
95
 
88
96
  **From a checkout**, for working on the kernel itself:
89
97
 
@@ -116,7 +124,20 @@ In a cell:
116
124
  from the notebook's `node_modules`, `node:` builtins, and `npm:pkg@range`
117
125
  or `jsr:@scope/pkg@range`.
118
126
  - On Node, `npm:` and `jsr:` install on first use into
119
- `~/.cache/cdp-kernel/npm`.
127
+ `~/.cache/cdp-kernel/npm`. An exact version (`npm:react@19.0.0`) installs
128
+ exactly that version. A range (`npm:react@19`) is resolved the first
129
+ time it's imported and stays at that version, even as other packages
130
+ are installed later. Caches written by cdp-kernel 0.3.0 or earlier
131
+ recorded ranges, which later installs could re-resolve. Delete
132
+ `~/.cache/cdp-kernel/npm` to start fresh. `jsr:` works with npm 12,
133
+ which otherwise refuses JSR's registry tarballs.
134
+ - **Lockfile** (cdp-kernel 0.4.0 or later). To pin versions, create a `cdp-kernel.lock.json` in the
135
+ notebook's directory (or above it) containing `{}`. From then on, each
136
+ `npm:`/`jsr:` range is recorded with the exact version it resolved to,
137
+ and installed at that version, so the notebook gets the same packages
138
+ on another machine. Commit it next to the notebook. Deno targets use
139
+ Deno's own resolution and lock instead. Installs into the shared cache
140
+ are serialized, so two kernels installing at once can't corrupt it.
120
141
  - On Deno they use Deno's global cache, unless the notebook is in a Deno
121
142
  project (below).
122
143
  - On Bun they install with `bun add` into `~/.cache/cdp-kernel/bun`. Other
@@ -143,6 +164,13 @@ In a cell:
143
164
  - `prompt()` and `confirm()` work over the stdin channel, except on
144
165
  workerd. Interrupt keeps state, except for a busy cell on Bun or workerd
145
166
  (below).
167
+ - **Shift-Tab** shows a tooltip for the name at the cursor: its type, a
168
+ function's signature (and its source at the second press), a preview, and
169
+ its properties. Nothing is run to get it. On Node and Deno the value is
170
+ evaluated with V8's side-effect check (`throwOnSideEffect`), which also
171
+ guards completion. Bun, workerd and celld look the name up by property,
172
+ without calling getters. Executed cells are kept for `history_request`
173
+ (tail, range and search).
146
174
 
147
175
  ## Bun
148
176
 
@@ -248,6 +276,35 @@ all work as on workerd. The differences:
248
276
  waits for it to stop gracefully, which a busy cell never allows. So an
249
277
  interrupt of a busy cell kills the node, then `celld dev`.
250
278
 
279
+ ## Debugger
280
+
281
+ On Node and Deno targets, JupyterLab's visual debugger works: the bug
282
+ button in the notebook toolbar, breakpoints in the cell gutter, the call
283
+ stack, variables and scopes, stepping, pause on exceptions, the debug
284
+ console, and the rich variable viewer. The kernel translates the Jupyter
285
+ debug protocol (DAP over `debug_request`) to V8's `Debugger` domain on the
286
+ session it already holds (`src/debugger.ts`):
287
+
288
+ - **Cell paths.** Each cell is named as JupyterLab names it: a temp-dir
289
+ prefix plus the Murmur2 hash of its code (`src/murmur2.ts`, matched
290
+ against ipykernel's). It's evaluated with that name as its
291
+ `//# sourceURL`, so breakpoints bind to the cell and frames point back at
292
+ it.
293
+ - **Lines.** Breakpoint and frame lines go through the cell's source map
294
+ (`src/sourcemap.ts`), since esbuild rewrites the TypeScript. A breakpoint
295
+ on a blank or type-only line moves to the next statement.
296
+ - **Nothing runs on its own.** The variable explorer between cells reads
297
+ the cells' declarations with V8's side-effect check, so a getter never
298
+ runs. Frames, scopes and the rich viewer evaluate on the paused frame.
299
+ - **Kernelspec.** `metadata.debugger` is set for Node and Deno kernelspecs,
300
+ which is what JupyterLab checks. Reinstall older kernelspecs to get it.
301
+
302
+ `CDPK_DEBUG_TRACE=1` logs every debug request, reply and event to the
303
+ Jupyter server's log.
304
+
305
+ Bun, workerd and celld don't run over an inspector, so they have no
306
+ debugger.
307
+
251
308
  ## Editor types
252
309
 
253
310
  Cells run untyped, as in `deno jupyter`. Notebook code your editor
@@ -357,12 +414,15 @@ CLI: an installed npm bin, or `deno run -A src/main.ts` to test a Deno host.
357
414
  One version number lives in three places: `package.json`, `deno.jsonc` and
358
415
  `src/package.ts`. `test/package.test.ts` fails if they disagree. To release:
359
416
 
360
- 1. Bump all three, commit, and merge to `main`.
417
+ 1. Bump all three, and set `RELEASE_DATE` in `src/package.ts` to today
418
+ (UTC). Commit, and merge to `main`.
361
419
  2. Tag and push: `git tag v0.1.1 && git push origin v0.1.1`.
362
420
 
363
421
  The `publish` workflow then:
364
422
 
365
- 1. Checks that the tag matches the version.
423
+ 1. Checks that the tag matches the version, and that `RELEASE_DATE` is
424
+ today or yesterday. A JSR kernelspec's dependency cutoff comes from that
425
+ date, so a stale one would block the release it ships in.
366
426
  2. Runs the type check, the unit tests, and the e2e suite against the packed
367
427
  npm tarball and against a Deno host.
368
428
  3. Publishes to JSR (`deno publish`).
@@ -381,15 +441,32 @@ that failed.
381
441
 
382
442
  - Widgets are anywidget-style (`jupyter.widget`). The classic ipywidgets
383
443
  controls (sliders, dropdowns) are Python classes, so they're not available.
384
- - No `inspect_request` content and no history. Deno has neither.
385
- - No debugger.
386
- - `npm:` on Node:
387
- - one npm alias per `name@range`;
388
- - no lockfile;
389
- - peer dependencies resolve by real name from the shared cache.
444
+ - Inspection tooltips show what's found at runtime (type, signature,
445
+ preview, properties), not TypeScript types or JSDoc. On Bun, workerd and
446
+ celld, a getter on the path is reported but not called. History is kept
447
+ for the current kernel session only.
448
+ - The debugger is for Node and Deno targets only. Breakpoints are per line,
449
+ conditional breakpoints and logpoints aren't supported, variables can't be
450
+ set from the debugger, and "copy to globals" isn't offered.
451
+ - `npm:` on Node, Bun, workerd and celld:
452
+ - one npm alias per `name@range` (or exact version, when locked);
453
+ - the cache is shared by every notebook, so peer dependencies resolve by
454
+ real name from it;
455
+ - a project's own `package.json` doesn't decide an `npm:` import's version
456
+ (bare imports do use its `node_modules`).
390
457
  - No type checking. Deno doesn't type-check REPL cells either.
391
- - Results are formatted with `util.inspect` on Node, so they look slightly
392
- different from Deno's `Deno.inspect`.
458
+ - Results are formatted by each runtime's own inspector: `util.inspect` on
459
+ Node, Bun, workerd and celld, and `Deno.inspect` on Deno. On a fixture of
460
+ 30 typical values, 23 format identically on Node and Deno. The rest
461
+ differ in two ways:
462
+ - strings are quoted `'…'` on Node and `"…"` on Deno, wherever they
463
+ appear;
464
+ - a displayed `Error`'s stack also lists Node's own module-loader frames.
465
+
466
+ That's deliberate: each runtime's users see the format they expect, and
467
+ Node's inspector has no quote-style option, short of porting Deno's
468
+ formatter. `test/formatting.test.ts` runs both runtimes on the fixture
469
+ and fails if a new kind of difference appears (#17).
393
470
  - WebGPU texture and buffer display is dropped.
394
471
  - On Bun:
395
472
  - Interrupting a busy cell restarts Bun and loses state. An awaiting cell
@@ -51,6 +51,8 @@ export declare class BunTarget implements KernelTarget {
51
51
  stack: string;
52
52
  }>;
53
53
  propertyNames(expression: string): Promise<string[]>;
54
+ /** Resolved by property descriptor in the target, so nothing runs (#10). */
55
+ inspect(path: string, detail: number): Promise<string | null>;
54
56
  /** None: top-level declarations here are properties of globalThis. */
55
57
  lexicalNames(): Promise<string[]>;
56
58
  replyInput(value: string | null): void;
@@ -218,6 +218,11 @@ export class BunTarget {
218
218
  const reply = await this.#request("props", { expr: expression }).catch(() => null);
219
219
  return Array.isArray(reply?.names) ? reply.names.filter((n) => typeof n === "string") : [];
220
220
  }
221
+ /** Resolved by property descriptor in the target, so nothing runs (#10). */
222
+ async inspect(path, detail) {
223
+ const reply = await this.#request("inspect", { path, detail }).catch(() => null);
224
+ return typeof reply?.text === "string" ? reply.text : null;
225
+ }
221
226
  /** None: top-level declarations here are properties of globalThis. */
222
227
  lexicalNames() {
223
228
  return Promise.resolve([]);
@@ -0,0 +1,42 @@
1
+ import type { CdpParams, RemoteObject } from "./cdp.ts";
2
+ import { type CellTransformOptions } from "./transform.ts";
3
+ /** What the debugger needs from a CDP target: its session, with the Debugger domain. */
4
+ export interface DebugChannel {
5
+ send(method: string, params?: CdpParams): Promise<CdpParams>;
6
+ on(method: string, listener: (params: CdpParams) => void): void;
7
+ /** The execution context cells run in. */
8
+ readonly contextId: number;
9
+ /** Enumerable globals the runtime had before any cell ran. */
10
+ readonly baselineGlobals: ReadonlySet<string>;
11
+ }
12
+ export type DapMessage = {
13
+ [key: string]: unknown;
14
+ };
15
+ /** ipykernel's seed, so a kernel switch doesn't move JupyterLab's breakpoints. */
16
+ export declare const HASH_SEED = 3339675911;
17
+ export declare const THREAD_ID = 1;
18
+ /** A DAP value string for a CDP RemoteObject. */
19
+ export declare function describeRemote(object: RemoteObject): string;
20
+ export declare class Debugger {
21
+ #private;
22
+ readonly prefix: string;
23
+ readonly suffix = ".ts";
24
+ constructor(emit: (event: DapMessage) => void, transformOptions?: () => CellTransformOptions);
25
+ get started(): boolean;
26
+ get stopped(): boolean;
27
+ /** A cell's path, as JupyterLab computes it for the same code. */
28
+ pathFor(code: string): string;
29
+ /**
30
+ * The target to debug: called with each target the kernel launches. A
31
+ * restarted target gets the session's breakpoints again.
32
+ */
33
+ attachTarget(channel: DebugChannel | null): Promise<void>;
34
+ /**
35
+ * The script for a cell while debugging: transformed with its line map
36
+ * kept, and named for its path. `source` is what actually runs (the cell,
37
+ * or the cell wrapped as an object literal); the path comes from `code`,
38
+ * the cell as the frontend sent it.
39
+ */
40
+ cellScript(code: string, source: string): string;
41
+ handle(request: DapMessage): Promise<DapMessage>;
42
+ }