@tlockney/cdp-kernel 0.2.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.
Files changed (43) hide show
  1. package/README.md +259 -33
  2. package/dist/bun-target.d.ts +60 -0
  3. package/dist/bun-target.js +236 -0
  4. package/dist/celld-target.d.ts +39 -0
  5. package/dist/celld-target.js +274 -0
  6. package/dist/debugger.d.ts +42 -0
  7. package/dist/debugger.js +534 -0
  8. package/dist/history.d.ts +18 -0
  9. package/dist/history.js +45 -0
  10. package/dist/kernel.js +91 -15
  11. package/dist/main.js +8 -6
  12. package/dist/murmur2.d.ts +2 -0
  13. package/dist/murmur2.js +41 -0
  14. package/dist/package.d.ts +16 -2
  15. package/dist/package.js +28 -2
  16. package/dist/sourcemap.d.ts +22 -0
  17. package/dist/sourcemap.js +86 -0
  18. package/dist/target.d.ts +65 -7
  19. package/dist/target.js +57 -6
  20. package/dist/text.d.ts +6 -0
  21. package/dist/text.js +17 -0
  22. package/dist/transform.d.ts +16 -0
  23. package/dist/transform.js +173 -11
  24. package/dist/workerd-target.d.ts +125 -0
  25. package/dist/workerd-target.js +455 -0
  26. package/dist/workers-project.d.ts +27 -0
  27. package/dist/workers-project.js +195 -0
  28. package/dist/ws-client.d.ts +32 -0
  29. package/dist/ws-client.js +166 -0
  30. package/package.json +18 -4
  31. package/runtime/bootstrap.mjs +55 -5
  32. package/runtime/bun-channel.d.mts +3 -0
  33. package/runtime/bun-channel.mjs +58 -0
  34. package/runtime/deno-compat.mjs +3 -1
  35. package/runtime/display.d.mts +2 -0
  36. package/runtime/display.mjs +9 -1
  37. package/runtime/evaluator.d.mts +22 -0
  38. package/runtime/evaluator.mjs +91 -0
  39. package/runtime/introspect.d.mts +13 -0
  40. package/runtime/introspect.mjs +120 -0
  41. package/runtime/node-hooks.d.mts +29 -0
  42. package/runtime/node-hooks.mjs +160 -20
  43. package/runtime/workerd-worker.mjs +194 -0
package/dist/kernel.js CHANGED
@@ -10,9 +10,14 @@ import { randomUUID } from "node:crypto";
10
10
  import { decodeMessage, encodeMessage, PROTOCOL_VERSION, verify } from "./wire.js";
11
11
  import { PubSocket, RouterSocket, startHeartbeat } from "./zmtp.js";
12
12
  import { Interrupted, Target } from "./target.js";
13
- import { CellSyntaxError, objectLiteralCandidate, transformCell } from "./transform.js";
13
+ import { BunTarget } from "./bun-target.js";
14
+ import { Debugger } from "./debugger.js";
15
+ import { WorkerdTarget } from "./workerd-target.js";
16
+ import { launchCelld } from "./celld-target.js";
17
+ import { CellSyntaxError, objectLiteralCandidate } from "./transform.js";
14
18
  import { loadDenoProject } from "./deno-config.js";
15
- import { checkIsComplete, codePointToUtf16Index, exprAtCursor, utf16ToCodePointIndex } from "./text.js";
19
+ import { checkIsComplete, codePointToUtf16Index, exprAtCursor, inspectTarget, utf16ToCodePointIndex } from "./text.js";
20
+ import { queryHistory } from "./history.js";
16
21
  import { VERSION } from "./package.js";
17
22
  export function parseConnectionInfo(json) {
18
23
  const raw = JSON.parse(json);
@@ -48,6 +53,11 @@ export class Kernel {
48
53
  // A Deno target in a Deno project uses its config (#13).
49
54
  #project;
50
55
  #projectWarning = null;
56
+ #noticeShownFor = null;
57
+ /** JupyterLab's visual debugger, for targets driven over CDP (#18). */
58
+ #debugger;
59
+ /** Cells run with store_history, for history_request (#10). */
60
+ #history = [];
51
61
  #session = randomUUID();
52
62
  #shell;
53
63
  #control;
@@ -67,6 +77,9 @@ export class Kernel {
67
77
  this.#runtime = runtime;
68
78
  this.#denoCompat = options.denoCompat ?? false;
69
79
  this.#project = runtime === "deno" ? this.#loadProject(options.notebookDir ?? process.cwd()) : null;
80
+ this.#debugger = runtime === "node" || runtime === "deno"
81
+ ? new Debugger((event) => this.#publish("debug_event", event), () => this.#transformOptions())
82
+ : null;
70
83
  startHeartbeat(conn.hb_port, conn.ip);
71
84
  this.#shell = new RouterSocket(conn.shell_port, conn.ip);
72
85
  this.#control = new RouterSocket(conn.control_port, conn.ip);
@@ -162,13 +175,24 @@ export class Kernel {
162
175
  }
163
176
  }
164
177
  // --- target lifecycle -----------------------------------------------------
178
+ #launchTarget() {
179
+ if (this.#runtime === "bun")
180
+ return BunTarget.launch(this.#targetEvents());
181
+ if (this.#runtime === "workerd")
182
+ return WorkerdTarget.launch(this.#targetEvents());
183
+ if (this.#runtime === "celld")
184
+ return launchCelld(this.#targetEvents());
185
+ return Target.launch(this.#runtime, this.#targetEvents(), {
186
+ denoCompat: this.#denoCompat,
187
+ denoConfig: this.#project?.configPath,
188
+ });
189
+ }
165
190
  #ensureTarget() {
166
191
  if (this.#target)
167
192
  return Promise.resolve(this.#target);
168
- this.#targetStarting ??= Target.launch(this.#runtime, this.#targetEvents(), {
169
- denoCompat: this.#denoCompat,
170
- denoConfig: this.#project?.configPath,
171
- }).then((target) => {
193
+ this.#targetStarting ??= this.#launchTarget().then(async (target) => {
194
+ // Before any cell runs on it, so breakpoints already apply.
195
+ await this.#debugger?.attachTarget(target.debug ?? null);
172
196
  this.#target = target;
173
197
  this.#targetStarting = null;
174
198
  return target;
@@ -182,10 +206,12 @@ export class Kernel {
182
206
  return {
183
207
  stream: (name, text) => this.#publish("stream", { name, text }),
184
208
  message: (msg) => void this.#onTargetMessage(msg),
185
- exit: (code) => {
209
+ exit: (code, note) => {
186
210
  this.#target = null;
211
+ void this.#debugger?.attachTarget(null);
187
212
  if (!this.#shuttingDown) {
188
- this.#publish("stream", { name: "stderr", text: `\n[cdp-kernel] ${this.#runtime} exited (${code}); restarting on next cell\n` });
213
+ const text = note ?? `[cdp-kernel] ${this.#runtime} exited (${code}); restarting on next cell`;
214
+ this.#publish("stream", { name: "stderr", text: `\n${text}\n` });
189
215
  }
190
216
  },
191
217
  };
@@ -256,9 +282,12 @@ export class Kernel {
256
282
  codemirror_mode: "typescript",
257
283
  nbconvert_exporter: "script",
258
284
  },
259
- banner: `cdp-kernel on ${this.#runtime} ${versions?.runtime ?? ""} (V8 ${versions?.v8 ?? "?"})`,
285
+ banner: `cdp-kernel on ${this.#runtime} ${versions?.runtime ?? ""} (${versions?.engine || "?"})`,
260
286
  help_links: [],
261
- debugger: false,
287
+ debugger: this.#debugger !== null,
288
+ // What current ipykernel advertises; JupyterLab itself goes by the
289
+ // kernelspec's metadata.debugger.
290
+ supported_features: this.#debugger !== null ? ["debugger"] : [],
262
291
  };
263
292
  }
264
293
  // --- control channel ------------------------------------------------------
@@ -271,6 +300,29 @@ export class Kernel {
271
300
  await this.#interrupt();
272
301
  this.#reply(this.#control, peer, msg, "interrupt_reply", { status: "ok" });
273
302
  break;
303
+ case "debug_request": {
304
+ // JupyterLab treats a debug request as done only once the kernel
305
+ // reports idle for it, as ipykernel does for control messages.
306
+ // (Jupyter Server leaves debug_request out of the kernel's status.)
307
+ this.#status("busy", msg.header);
308
+ let reply;
309
+ try {
310
+ if (this.#debugger === null) {
311
+ reply = { type: "response", request_seq: msg.content.seq, command: msg.content.command, success: false, message: `${this.#runtime} targets have no debugger`, body: {} };
312
+ }
313
+ else {
314
+ // Everything but debugInfo talks to the target, so make sure there is one.
315
+ if (msg.content.command !== "debugInfo")
316
+ await this.#ensureTarget().catch(() => null);
317
+ reply = await this.#debugger.handle(msg.content);
318
+ }
319
+ this.#reply(this.#control, peer, msg, "debug_reply", reply);
320
+ }
321
+ finally {
322
+ this.#status("idle", msg.header);
323
+ }
324
+ break;
325
+ }
274
326
  case "shutdown_request": {
275
327
  const restart = msg.content.restart === true;
276
328
  this.#reply(this.#control, peer, msg, "shutdown_reply", { status: "ok", restart });
@@ -302,10 +354,10 @@ export class Kernel {
302
354
  this.#reply(this.#shell, peer, msg, "is_complete_reply", checkIsComplete(String(msg.content.code ?? "")));
303
355
  break;
304
356
  case "inspect_request":
305
- this.#reply(this.#shell, peer, msg, "inspect_reply", { status: "ok", found: false, data: {}, metadata: {} });
357
+ this.#reply(this.#shell, peer, msg, "inspect_reply", await this.#inspect(msg));
306
358
  break;
307
359
  case "history_request":
308
- this.#reply(this.#shell, peer, msg, "history_reply", { status: "ok", history: [] });
360
+ this.#reply(this.#shell, peer, msg, "history_reply", { status: "ok", history: queryHistory(this.#history, msg.content) });
309
361
  break;
310
362
  case "comm_info_request":
311
363
  this.#reply(this.#shell, peer, msg, "comm_info_reply", { status: "ok", comms: this.#commInfo(msg.content) });
@@ -321,6 +373,19 @@ export class Kernel {
321
373
  this.#status("idle", msg.header);
322
374
  }
323
375
  }
376
+ /** The tooltip (Shift-Tab) for the name at the cursor (#10). */
377
+ async #inspect(msg) {
378
+ const code = String(msg.content.code ?? "");
379
+ const cursorCp = typeof msg.content.cursor_pos === "number"
380
+ ? msg.content.cursor_pos
381
+ : utf16ToCodePointIndex(code, code.length);
382
+ const path = inspectTarget(code, codePointToUtf16Index(code, cursorCp));
383
+ const detail = msg.content.detail_level === 1 ? 1 : 0;
384
+ const text = path === null ? null : await (await this.#ensureTarget()).inspect(path, detail);
385
+ return text === null
386
+ ? { status: "ok", found: false, data: {}, metadata: {} }
387
+ : { status: "ok", found: true, data: { "text/plain": text }, metadata: {} };
388
+ }
324
389
  async #complete(msg) {
325
390
  const code = String(msg.content.code ?? "");
326
391
  const cursorCp = typeof msg.content.cursor_pos === "number"
@@ -373,10 +438,14 @@ export class Kernel {
373
438
  async #evaluateCell(target, code) {
374
439
  // Object-literal wrapping (Deno's evaluate_line_with_object_wrapping):
375
440
  // try `({ ... })` first and fall back to the code as written.
441
+ // While debugging, cells carry the path JupyterLab gave them and keep a
442
+ // line map, so breakpoints bind and frames point at the cell (#18).
443
+ const debugging = this.#debugger?.started === true && target.debug !== undefined;
444
+ const transform = (source) => debugging && this.#debugger ? this.#debugger.cellScript(code, source) : target.transform(source, this.#transformOptions());
376
445
  const wrapped = objectLiteralCandidate(code);
377
446
  if (wrapped !== null) {
378
447
  try {
379
- const result = await target.evaluate(transformCell(wrapped, this.#transformOptions()));
448
+ const result = await target.evaluate(transform(wrapped));
380
449
  if (!result.exceptionDetails)
381
450
  return result;
382
451
  }
@@ -385,14 +454,16 @@ export class Kernel {
385
454
  throw err;
386
455
  }
387
456
  }
388
- return target.evaluate(transformCell(code, this.#transformOptions()));
457
+ return target.evaluate(transform(code));
389
458
  }
390
459
  async #execute(msg, peer) {
391
460
  const parent = msg.header;
392
461
  const code = String(msg.content.code ?? "");
393
462
  const silent = msg.content.silent === true;
394
- if (!silent && msg.content.store_history !== false)
463
+ if (!silent && msg.content.store_history !== false) {
395
464
  this.#executionCount++;
465
+ this.#history.push({ line: this.#executionCount, input: code });
466
+ }
396
467
  const count = this.#executionCount;
397
468
  this.#parent = parent;
398
469
  this.#allowStdin = msg.content.allow_stdin === true;
@@ -407,6 +478,11 @@ export class Kernel {
407
478
  this.#executing = true;
408
479
  try {
409
480
  const target = await this.#ensureTarget();
481
+ if (target.notice !== undefined && this.#noticeShownFor !== target) {
482
+ // Once per target, in the first cell it runs.
483
+ this.#noticeShownFor = target;
484
+ this.#publish("stream", { name: "stderr", text: `${target.notice}\n` });
485
+ }
410
486
  const result = await this.#evaluateCell(target, code);
411
487
  if (result.exceptionDetails) {
412
488
  const exception = result.exceptionDetails.exception;
package/dist/main.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // cdp-kernel CLI.
3
- // cdp-kernel install [--runtime node|deno] [--deno-compat] [--name NAME] [--display NAME]
4
- // cdp-kernel kernel --runtime node|deno [--deno-compat] --conn CONNECTION_FILE
3
+ // cdp-kernel install [--runtime node|deno|bun|workerd|celld] [--deno-compat] [--name NAME] [--display NAME]
4
+ // cdp-kernel kernel --runtime node|deno|bun|workerd|celld [--deno-compat] --conn CONNECTION_FILE
5
5
  // cdp-kernel version
6
6
  //
7
7
  // --deno-compat (Node only) adds a `Deno` namespace for notebooks written for
@@ -14,7 +14,7 @@ import { parseArgs } from "node:util";
14
14
  import { Kernel, parseConnectionInfo } from "./kernel.js";
15
15
  import { hostRuntime, kernelArgv, moduleOrigin, VERSION } from "./package.js";
16
16
  function isRuntime(value) {
17
- return value === "node" || value === "deno";
17
+ return value === "node" || value === "deno" || value === "bun" || value === "workerd" || value === "celld";
18
18
  }
19
19
  function kernelsDir() {
20
20
  if (process.env.JUPYTER_DATA_DIR)
@@ -42,7 +42,9 @@ function install(runtime, denoCompat, name, display) {
42
42
  // Interrupts arrive as interrupt_request on the control channel rather
43
43
  // than as SIGINT to this process.
44
44
  interrupt_mode: "message",
45
- metadata: { runtime, denoCompat, version: VERSION },
45
+ // JupyterLab enables its visual debugger from metadata.debugger (#18);
46
+ // only the targets driven over CDP have one.
47
+ metadata: { runtime, denoCompat, version: VERSION, debugger: runtime === "node" || runtime === "deno" },
46
48
  };
47
49
  writeFileSync(join(dir, "kernel.json"), JSON.stringify(spec, null, 2) + "\n");
48
50
  console.log(`installed kernelspec ${name} -> ${dir}`);
@@ -59,7 +61,7 @@ const { positionals, values } = parseArgs({
59
61
  });
60
62
  const runtime = values.runtime;
61
63
  if (!isRuntime(runtime)) {
62
- console.error(`unknown runtime ${runtime}; expected node or deno`);
64
+ console.error(`unknown runtime ${runtime}; expected node, deno, bun, workerd or celld`);
63
65
  process.exit(2);
64
66
  }
65
67
  // Deno targets have the real namespace; the shim is for Node.
@@ -83,6 +85,6 @@ switch (positionals[0]) {
83
85
  console.log(VERSION);
84
86
  break;
85
87
  default:
86
- console.error("usage: cdp-kernel install [--runtime node|deno] [--deno-compat] | kernel --runtime node|deno --conn FILE | version");
88
+ console.error("usage: cdp-kernel install [--runtime node|deno|bun|workerd|celld] [--deno-compat] | kernel --runtime node|deno|bun|workerd|celld --conn FILE | version");
87
89
  process.exit(2);
88
90
  }
@@ -0,0 +1,2 @@
1
+ /** The hash of a string's UTF-8 bytes. */
2
+ export declare function murmur2(str: string, seed: number): number;
@@ -0,0 +1,41 @@
1
+ // MurmurHash2 (32-bit), as the Jupyter debugger uses it to name a cell's
2
+ // source (#18). JupyterLab computes a cell's path itself, as tmpFilePrefix +
3
+ // murmur2(code, hashSeed) + tmpFileSuffix, and the kernel must name the
4
+ // cell's script the same way, so this follows JupyterLab's
5
+ // packages/debugger/src/hash.ts (Modified BSD License, Copyright (c) Jupyter
6
+ // Development Team), itself adapted from Gary Court's murmurhash2_gc.js
7
+ // (MIT License, Copyright (c) 2011 Gary Court).
8
+ const M = 0x5bd1e995;
9
+ const ENCODER = new TextEncoder();
10
+ /** 32-bit multiply by M, without leaving the safe-integer range. */
11
+ function mul(x) {
12
+ return (x & 0xffff) * M + ((((x >>> 16) * M) & 0xffff) << 16);
13
+ }
14
+ /** The hash of a string's UTF-8 bytes. */
15
+ export function murmur2(str, seed) {
16
+ const data = ENCODER.encode(str);
17
+ let len = data.length;
18
+ let h = seed ^ len;
19
+ let i = 0;
20
+ while (len >= 4) {
21
+ let k = data[i] | (data[i + 1] << 8) | (data[i + 2] << 16) | (data[i + 3] << 24);
22
+ k = mul(k);
23
+ k ^= k >>> 24;
24
+ k = mul(k);
25
+ h = mul(h) ^ k;
26
+ len -= 4;
27
+ i += 4;
28
+ }
29
+ if (len === 3)
30
+ h ^= data[i + 2] << 16;
31
+ if (len >= 2)
32
+ h ^= data[i + 1] << 8;
33
+ if (len >= 1) {
34
+ h ^= data[i];
35
+ h = mul(h);
36
+ }
37
+ h ^= h >>> 13;
38
+ h = mul(h);
39
+ h ^= h >>> 15;
40
+ return h >>> 0;
41
+ }
package/dist/package.d.ts CHANGED
@@ -1,6 +1,20 @@
1
1
  /** Kept equal to package.json and deno.jsonc by test/package.test.ts. */
2
- export declare const VERSION = "0.2.0";
3
- export declare const RUNTIME_FILES: readonly ["bootstrap.mjs", "display.mjs", "comm.mjs", "widget.mjs", "node-hooks.mjs", "deno-compat.mjs", "node-modules-resolve.mjs"];
2
+ export declare const VERSION = "0.4.0";
3
+ /**
4
+ * The UTC day this version was released, set with VERSION. The publish
5
+ * workflow refuses a tag unless it's today or yesterday, so it can't go
6
+ * stale: see dependencyCutoff.
7
+ */
8
+ export declare const RELEASE_DATE = "2026-10-15";
9
+ /**
10
+ * Deno blocks packages less than 24 hours old (minimum dependency age), so
11
+ * a kernelspec relaunching a release from JSR would fail on release day. It
12
+ * relaunches with this cutoff instead (#12): anything published up to two
13
+ * days after the release is allowed (the release itself included, however
14
+ * late in the day its tag was pushed), and nothing newer ever is.
15
+ */
16
+ export declare function dependencyCutoff(releaseDate?: string): string;
17
+ export declare const RUNTIME_FILES: readonly ["bootstrap.mjs", "bun-channel.mjs", "evaluator.mjs", "introspect.mjs", "workerd-worker.mjs", "display.mjs", "comm.mjs", "widget.mjs", "node-hooks.mjs", "deno-compat.mjs", "node-modules-resolve.mjs"];
4
18
  export type HostRuntime = "node" | "deno";
5
19
  export declare function hostRuntime(): HostRuntime;
6
20
  /** Where the CLI module was loaded from. */
package/dist/package.js CHANGED
@@ -14,9 +14,33 @@ import { join, sep } from "node:path";
14
14
  import process from "node:process";
15
15
  import { fileURLToPath } from "node:url";
16
16
  /** Kept equal to package.json and deno.jsonc by test/package.test.ts. */
17
- export const VERSION = "0.2.0";
17
+ export const VERSION = "0.4.0";
18
+ /**
19
+ * The UTC day this version was released, set with VERSION. The publish
20
+ * workflow refuses a tag unless it's today or yesterday, so it can't go
21
+ * stale: see dependencyCutoff.
22
+ */
23
+ export const RELEASE_DATE = "2026-10-15";
24
+ /**
25
+ * Deno blocks packages less than 24 hours old (minimum dependency age), so
26
+ * a kernelspec relaunching a release from JSR would fail on release day. It
27
+ * relaunches with this cutoff instead (#12): anything published up to two
28
+ * days after the release is allowed (the release itself included, however
29
+ * late in the day its tag was pushed), and nothing newer ever is.
30
+ */
31
+ export function dependencyCutoff(releaseDate = RELEASE_DATE) {
32
+ const day = new Date(`${releaseDate}T00:00:00Z`);
33
+ if (Number.isNaN(day.getTime()))
34
+ throw new Error(`not a date: ${releaseDate}`);
35
+ day.setUTCDate(day.getUTCDate() + 2);
36
+ return day.toISOString().slice(0, 10);
37
+ }
18
38
  export const RUNTIME_FILES = [
19
39
  "bootstrap.mjs",
40
+ "bun-channel.mjs",
41
+ "evaluator.mjs",
42
+ "introspect.mjs",
43
+ "workerd-worker.mjs",
20
44
  "display.mjs",
21
45
  "comm.mjs",
22
46
  "widget.mjs",
@@ -51,7 +75,9 @@ export function kernelArgv(host, execPath, origin, args) {
51
75
  }
52
76
  // Deno: a checkout needs its deno.jsonc import map; a JSR package brings
53
77
  // its own, so skip whatever config the notebook's directory has.
54
- const flags = origin.kind === "jsr" ? ["--no-config", "--no-lock"] : ["--no-lock"];
78
+ const flags = origin.kind === "jsr"
79
+ ? ["--no-config", "--no-lock", `--min-dep-age=${dependencyCutoff()}`]
80
+ : ["--no-lock"];
55
81
  const entry = origin.kind === "jsr" ? origin.specifier : origin.path;
56
82
  return [execPath, "run", "--allow-all", ...flags, entry, ...args];
57
83
  }
@@ -0,0 +1,22 @@
1
+ /** One segment's VLQ fields: generated column, source, line, column, name. */
2
+ export declare function decodeVlq(segment: string): number[];
3
+ /**
4
+ * For each generated line (0-based), the first original line (0-based) a
5
+ * segment on it maps to, or -1. Source-map fields are relative to the
6
+ * previous segment's; only the original line is tracked.
7
+ */
8
+ export declare function generatedToOriginalLines(mappings: string): number[];
9
+ /** Both directions between the lines of a cell and of its script (0-based). */
10
+ export declare class LineMap {
11
+ #private;
12
+ /** `offset`: lines the script adds before esbuild's output (the prelude). */
13
+ constructor(mappings: string, offset?: number);
14
+ /** The cell line a script line came from: its own mapping, or the nearest one above. */
15
+ toOriginal(generated: number): number;
16
+ /**
17
+ * The script line for a cell line, or for the next cell line that has
18
+ * code (a breakpoint on a blank or comment line moves down, as debuggers
19
+ * do), or null past the end.
20
+ */
21
+ toGenerated(original: number): number | null;
22
+ }
@@ -0,0 +1,86 @@
1
+ // Line mapping between a cell as written and the script that runs, for the
2
+ // debugger (#18): a breakpoint set on a cell's line must land on the line
3
+ // esbuild's output put it on, and a paused frame must report the line the
4
+ // user wrote. Only lines: Jupyter's breakpoints are per line.
5
+ const BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
6
+ const DIGIT = new Map([...BASE64].map((c, i) => [c, i]));
7
+ /** One segment's VLQ fields: generated column, source, line, column, name. */
8
+ export function decodeVlq(segment) {
9
+ const values = [];
10
+ let value = 0;
11
+ let shift = 0;
12
+ for (const char of segment) {
13
+ const digit = DIGIT.get(char);
14
+ if (digit === undefined)
15
+ throw new Error(`bad base64 VLQ digit ${JSON.stringify(char)}`);
16
+ value += (digit & 31) << shift;
17
+ if (digit & 32) {
18
+ shift += 5;
19
+ continue;
20
+ }
21
+ values.push(value & 1 ? -(value >>> 1) : value >>> 1);
22
+ value = 0;
23
+ shift = 0;
24
+ }
25
+ return values;
26
+ }
27
+ /**
28
+ * For each generated line (0-based), the first original line (0-based) a
29
+ * segment on it maps to, or -1. Source-map fields are relative to the
30
+ * previous segment's; only the original line is tracked.
31
+ */
32
+ export function generatedToOriginalLines(mappings) {
33
+ const lines = [];
34
+ let original = 0;
35
+ for (const line of mappings.split(";")) {
36
+ let first = -1;
37
+ for (const segment of line.split(",")) {
38
+ if (!segment)
39
+ continue;
40
+ const fields = decodeVlq(segment);
41
+ if (fields.length >= 4) {
42
+ original += fields[2];
43
+ if (first === -1)
44
+ first = original;
45
+ }
46
+ }
47
+ lines.push(first);
48
+ }
49
+ return lines;
50
+ }
51
+ /** Both directions between the lines of a cell and of its script (0-based). */
52
+ export class LineMap {
53
+ #toOriginal;
54
+ #toGenerated = new Map();
55
+ /** `offset`: lines the script adds before esbuild's output (the prelude). */
56
+ constructor(mappings, offset = 0) {
57
+ const fromEsbuild = generatedToOriginalLines(mappings);
58
+ this.#toOriginal = [...Array(offset).fill(-1), ...fromEsbuild];
59
+ this.#toOriginal.forEach((original, generated) => {
60
+ if (original !== -1 && !this.#toGenerated.has(original))
61
+ this.#toGenerated.set(original, generated);
62
+ });
63
+ }
64
+ /** The cell line a script line came from: its own mapping, or the nearest one above. */
65
+ toOriginal(generated) {
66
+ for (let line = Math.min(generated, this.#toOriginal.length - 1); line >= 0; line--) {
67
+ if (this.#toOriginal[line] !== -1)
68
+ return this.#toOriginal[line];
69
+ }
70
+ return 0;
71
+ }
72
+ /**
73
+ * The script line for a cell line, or for the next cell line that has
74
+ * code (a breakpoint on a blank or comment line moves down, as debuggers
75
+ * do), or null past the end.
76
+ */
77
+ toGenerated(original) {
78
+ const last = Math.max(-1, ...this.#toGenerated.keys());
79
+ for (let line = original; line <= last; line++) {
80
+ const generated = this.#toGenerated.get(line);
81
+ if (generated !== undefined)
82
+ return generated;
83
+ }
84
+ return null;
85
+ }
86
+ }
package/dist/target.d.ts CHANGED
@@ -1,5 +1,50 @@
1
1
  import { type CdpParams, type EvaluateResult, type RemoteObject } from "./cdp.ts";
2
- export type RuntimeName = "node" | "deno";
2
+ import { type CellTransformOptions, transformCell } from "./transform.ts";
3
+ import type { DebugChannel } from "./debugger.ts";
4
+ /** Targets driven over CDP. */
5
+ export type CdpRuntime = "node" | "deno";
6
+ /** Bun and workerd have their own backends: src/bun-target.ts, src/workerd-target.ts. */
7
+ export type RuntimeName = CdpRuntime | "bun" | "workerd" | "celld";
8
+ /** What the kernel needs from a target runtime. */
9
+ export interface KernelTarget {
10
+ readonly runtime: RuntimeName;
11
+ readonly versions: {
12
+ runtime: string;
13
+ engine: string;
14
+ };
15
+ /** Shown once, in the first cell: e.g. which project's bindings loaded. */
16
+ readonly notice?: string;
17
+ /** Turns a cell into the script evaluate() takes. */
18
+ transform(code: string, options: CellTransformOptions): string;
19
+ /** Runs a transformed cell. Rejects with Interrupted after interrupt(). */
20
+ evaluate(script: string): Promise<EvaluateResult>;
21
+ interrupt(): Promise<void>;
22
+ /** Resolves once all output and messages sent so far have been delivered. */
23
+ flush(): Promise<void>;
24
+ broadcastResult(executionCount: number, result: RemoteObject): Promise<void>;
25
+ dispatchComm(msgType: string, content: CdpParams, metadata: CdpParams, buffers: Uint8Array[]): Promise<{
26
+ handled: boolean;
27
+ } | {
28
+ error: {
29
+ name: string;
30
+ message: string;
31
+ stack: string;
32
+ };
33
+ }>;
34
+ describeError(exception: RemoteObject): Promise<{
35
+ name: string;
36
+ message: string;
37
+ stack: string;
38
+ }>;
39
+ propertyNames(expression: string): Promise<string[]>;
40
+ lexicalNames(): Promise<string[]>;
41
+ /** inspect_request (#10): text describing a dotted path's value, without running anything. */
42
+ inspect(path: string, detail: number): Promise<string | null>;
43
+ /** The Debugger domain, on targets driven over CDP (#18). */
44
+ readonly debug?: DebugChannel;
45
+ replyInput(value: string | null): void;
46
+ kill(): void;
47
+ }
3
48
  export interface LaunchOptions {
4
49
  /** Runtime binary; defaults to `deno`, or `node` (this one when hosted by Node). */
5
50
  executable?: string;
@@ -7,7 +52,7 @@ export interface LaunchOptions {
7
52
  denoConfig?: string;
8
53
  }
9
54
  /** `bootstrap` is the local path of runtime/bootstrap.mjs (see runtimeDir). */
10
- export declare function launchCommand(runtime: RuntimeName, bootstrap: string, { executable, denoConfig }?: LaunchOptions): {
55
+ export declare function launchCommand(runtime: CdpRuntime, bootstrap: string, { executable, denoConfig }?: LaunchOptions): {
11
56
  cmd: string;
12
57
  args: string[];
13
58
  };
@@ -25,6 +70,7 @@ export type TargetMessage = {
25
70
  password: boolean;
26
71
  barrier: number;
27
72
  };
73
+ export declare function isTargetMessage(value: unknown): value is TargetMessage;
28
74
  /**
29
75
  * Splits one output pipe into text and barrier markers, and releases them
30
76
  * in order. A gated marker (one that announces a binding message) pauses
@@ -42,28 +88,39 @@ export declare class PipeDemux {
42
88
  /** Release everything (the process died). */
43
89
  close(): void;
44
90
  }
91
+ /** Promises keyed by barrier number, resolvable before or after first use. */
92
+ export declare class Gates {
93
+ #private;
94
+ wait(n: number): Promise<void>;
95
+ open(n: number): void;
96
+ openAll(): void;
97
+ }
45
98
  export interface TargetEvents {
46
99
  stream(name: "stdout" | "stderr", text: string): void;
47
100
  message(msg: TargetMessage): void;
48
- exit(code: number | null): void;
101
+ /** `note` says why, when the target was stopped on purpose. */
102
+ exit(code: number | null, note?: string): void;
49
103
  }
50
104
  export declare class Interrupted extends Error {
51
105
  name: string;
52
106
  }
53
- export declare class Target {
107
+ export declare class Target implements KernelTarget {
54
108
  #private;
55
- readonly runtime: RuntimeName;
109
+ readonly runtime: CdpRuntime;
110
+ readonly transform: typeof transformCell;
56
111
  readonly versions: {
57
112
  runtime: string;
58
- v8: string;
113
+ engine: string;
59
114
  };
60
115
  private constructor();
61
- static launch(runtime: RuntimeName, events: TargetEvents, options?: {
116
+ static launch(runtime: CdpRuntime, events: TargetEvents, options?: {
62
117
  cwd?: string;
63
118
  executable?: string;
64
119
  denoCompat?: boolean;
65
120
  denoConfig?: string;
66
121
  }): Promise<Target>;
122
+ /** The Debugger domain on this target's session, for src/debugger.ts. */
123
+ get debug(): DebugChannel;
67
124
  /** Resolves once all output and messages sent so far have been delivered. */
68
125
  flush(): Promise<void>;
69
126
  /** Runs transformed cell code. Rejects with Interrupted after interrupt(). */
@@ -99,6 +156,7 @@ export declare class Target {
99
156
  stack: string;
100
157
  }>;
101
158
  propertyNames(expression: string): Promise<string[]>;
159
+ inspect(path: string, detail: number): Promise<string | null>;
102
160
  lexicalNames(): Promise<string[]>;
103
161
  replyInput(value: string | null): void;
104
162
  kill(): void;