@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 +93 -16
- package/dist/bun-target.d.ts +2 -0
- package/dist/bun-target.js +5 -0
- package/dist/debugger.d.ts +42 -0
- package/dist/debugger.js +534 -0
- package/dist/history.d.ts +18 -0
- package/dist/history.js +45 -0
- package/dist/kernel.js +65 -8
- package/dist/main.js +3 -1
- package/dist/murmur2.d.ts +2 -0
- package/dist/murmur2.js +41 -0
- package/dist/package.d.ts +16 -2
- package/dist/package.js +25 -2
- package/dist/sourcemap.d.ts +22 -0
- package/dist/sourcemap.js +86 -0
- package/dist/target.d.ts +8 -0
- package/dist/target.js +51 -1
- package/dist/text.d.ts +6 -0
- package/dist/text.js +17 -0
- package/dist/transform.d.ts +10 -0
- package/dist/transform.js +38 -5
- package/dist/workerd-target.d.ts +2 -0
- package/dist/workerd-target.js +8 -3
- package/package.json +2 -2
- package/runtime/bootstrap.mjs +17 -2
- package/runtime/deno-compat.mjs +3 -1
- package/runtime/display.d.mts +2 -0
- package/runtime/display.mjs +9 -1
- package/runtime/evaluator.d.mts +4 -0
- package/runtime/evaluator.mjs +8 -0
- package/runtime/introspect.d.mts +13 -0
- package/runtime/introspect.mjs +120 -0
- package/runtime/node-hooks.d.mts +19 -1
- package/runtime/node-hooks.mjs +144 -16
- package/runtime/workerd-worker.mjs +5 -0
package/dist/kernel.js
CHANGED
|
@@ -11,11 +11,13 @@ import { decodeMessage, encodeMessage, PROTOCOL_VERSION, verify } from "./wire.j
|
|
|
11
11
|
import { PubSocket, RouterSocket, startHeartbeat } from "./zmtp.js";
|
|
12
12
|
import { Interrupted, Target } from "./target.js";
|
|
13
13
|
import { BunTarget } from "./bun-target.js";
|
|
14
|
+
import { Debugger } from "./debugger.js";
|
|
14
15
|
import { WorkerdTarget } from "./workerd-target.js";
|
|
15
16
|
import { launchCelld } from "./celld-target.js";
|
|
16
17
|
import { CellSyntaxError, objectLiteralCandidate } from "./transform.js";
|
|
17
18
|
import { loadDenoProject } from "./deno-config.js";
|
|
18
|
-
import { checkIsComplete, codePointToUtf16Index, exprAtCursor, utf16ToCodePointIndex } from "./text.js";
|
|
19
|
+
import { checkIsComplete, codePointToUtf16Index, exprAtCursor, inspectTarget, utf16ToCodePointIndex } from "./text.js";
|
|
20
|
+
import { queryHistory } from "./history.js";
|
|
19
21
|
import { VERSION } from "./package.js";
|
|
20
22
|
export function parseConnectionInfo(json) {
|
|
21
23
|
const raw = JSON.parse(json);
|
|
@@ -52,6 +54,10 @@ export class Kernel {
|
|
|
52
54
|
#project;
|
|
53
55
|
#projectWarning = null;
|
|
54
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 = [];
|
|
55
61
|
#session = randomUUID();
|
|
56
62
|
#shell;
|
|
57
63
|
#control;
|
|
@@ -71,6 +77,9 @@ export class Kernel {
|
|
|
71
77
|
this.#runtime = runtime;
|
|
72
78
|
this.#denoCompat = options.denoCompat ?? false;
|
|
73
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;
|
|
74
83
|
startHeartbeat(conn.hb_port, conn.ip);
|
|
75
84
|
this.#shell = new RouterSocket(conn.shell_port, conn.ip);
|
|
76
85
|
this.#control = new RouterSocket(conn.control_port, conn.ip);
|
|
@@ -181,7 +190,9 @@ export class Kernel {
|
|
|
181
190
|
#ensureTarget() {
|
|
182
191
|
if (this.#target)
|
|
183
192
|
return Promise.resolve(this.#target);
|
|
184
|
-
this.#targetStarting ??= this.#launchTarget().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);
|
|
185
196
|
this.#target = target;
|
|
186
197
|
this.#targetStarting = null;
|
|
187
198
|
return target;
|
|
@@ -197,6 +208,7 @@ export class Kernel {
|
|
|
197
208
|
message: (msg) => void this.#onTargetMessage(msg),
|
|
198
209
|
exit: (code, note) => {
|
|
199
210
|
this.#target = null;
|
|
211
|
+
void this.#debugger?.attachTarget(null);
|
|
200
212
|
if (!this.#shuttingDown) {
|
|
201
213
|
const text = note ?? `[cdp-kernel] ${this.#runtime} exited (${code}); restarting on next cell`;
|
|
202
214
|
this.#publish("stream", { name: "stderr", text: `\n${text}\n` });
|
|
@@ -272,7 +284,10 @@ export class Kernel {
|
|
|
272
284
|
},
|
|
273
285
|
banner: `cdp-kernel on ${this.#runtime} ${versions?.runtime ?? ""} (${versions?.engine || "?"})`,
|
|
274
286
|
help_links: [],
|
|
275
|
-
debugger:
|
|
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"] : [],
|
|
276
291
|
};
|
|
277
292
|
}
|
|
278
293
|
// --- control channel ------------------------------------------------------
|
|
@@ -285,6 +300,29 @@ export class Kernel {
|
|
|
285
300
|
await this.#interrupt();
|
|
286
301
|
this.#reply(this.#control, peer, msg, "interrupt_reply", { status: "ok" });
|
|
287
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
|
+
}
|
|
288
326
|
case "shutdown_request": {
|
|
289
327
|
const restart = msg.content.restart === true;
|
|
290
328
|
this.#reply(this.#control, peer, msg, "shutdown_reply", { status: "ok", restart });
|
|
@@ -316,10 +354,10 @@ export class Kernel {
|
|
|
316
354
|
this.#reply(this.#shell, peer, msg, "is_complete_reply", checkIsComplete(String(msg.content.code ?? "")));
|
|
317
355
|
break;
|
|
318
356
|
case "inspect_request":
|
|
319
|
-
this.#reply(this.#shell, peer, msg, "inspect_reply",
|
|
357
|
+
this.#reply(this.#shell, peer, msg, "inspect_reply", await this.#inspect(msg));
|
|
320
358
|
break;
|
|
321
359
|
case "history_request":
|
|
322
|
-
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) });
|
|
323
361
|
break;
|
|
324
362
|
case "comm_info_request":
|
|
325
363
|
this.#reply(this.#shell, peer, msg, "comm_info_reply", { status: "ok", comms: this.#commInfo(msg.content) });
|
|
@@ -335,6 +373,19 @@ export class Kernel {
|
|
|
335
373
|
this.#status("idle", msg.header);
|
|
336
374
|
}
|
|
337
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
|
+
}
|
|
338
389
|
async #complete(msg) {
|
|
339
390
|
const code = String(msg.content.code ?? "");
|
|
340
391
|
const cursorCp = typeof msg.content.cursor_pos === "number"
|
|
@@ -387,10 +438,14 @@ export class Kernel {
|
|
|
387
438
|
async #evaluateCell(target, code) {
|
|
388
439
|
// Object-literal wrapping (Deno's evaluate_line_with_object_wrapping):
|
|
389
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());
|
|
390
445
|
const wrapped = objectLiteralCandidate(code);
|
|
391
446
|
if (wrapped !== null) {
|
|
392
447
|
try {
|
|
393
|
-
const result = await target.evaluate(
|
|
448
|
+
const result = await target.evaluate(transform(wrapped));
|
|
394
449
|
if (!result.exceptionDetails)
|
|
395
450
|
return result;
|
|
396
451
|
}
|
|
@@ -399,14 +454,16 @@ export class Kernel {
|
|
|
399
454
|
throw err;
|
|
400
455
|
}
|
|
401
456
|
}
|
|
402
|
-
return target.evaluate(
|
|
457
|
+
return target.evaluate(transform(code));
|
|
403
458
|
}
|
|
404
459
|
async #execute(msg, peer) {
|
|
405
460
|
const parent = msg.header;
|
|
406
461
|
const code = String(msg.content.code ?? "");
|
|
407
462
|
const silent = msg.content.silent === true;
|
|
408
|
-
if (!silent && msg.content.store_history !== false)
|
|
463
|
+
if (!silent && msg.content.store_history !== false) {
|
|
409
464
|
this.#executionCount++;
|
|
465
|
+
this.#history.push({ line: this.#executionCount, input: code });
|
|
466
|
+
}
|
|
410
467
|
const count = this.#executionCount;
|
|
411
468
|
this.#parent = parent;
|
|
412
469
|
this.#allowStdin = msg.content.allow_stdin === true;
|
package/dist/main.js
CHANGED
|
@@ -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
|
-
|
|
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}`);
|
package/dist/murmur2.js
ADDED
|
@@ -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.
|
|
3
|
-
|
|
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,11 +14,32 @@ 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.
|
|
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",
|
|
20
40
|
"bun-channel.mjs",
|
|
21
41
|
"evaluator.mjs",
|
|
42
|
+
"introspect.mjs",
|
|
22
43
|
"workerd-worker.mjs",
|
|
23
44
|
"display.mjs",
|
|
24
45
|
"comm.mjs",
|
|
@@ -54,7 +75,9 @@ export function kernelArgv(host, execPath, origin, args) {
|
|
|
54
75
|
}
|
|
55
76
|
// Deno: a checkout needs its deno.jsonc import map; a JSR package brings
|
|
56
77
|
// its own, so skip whatever config the notebook's directory has.
|
|
57
|
-
const flags = origin.kind === "jsr"
|
|
78
|
+
const flags = origin.kind === "jsr"
|
|
79
|
+
? ["--no-config", "--no-lock", `--min-dep-age=${dependencyCutoff()}`]
|
|
80
|
+
: ["--no-lock"];
|
|
58
81
|
const entry = origin.kind === "jsr" ? origin.specifier : origin.path;
|
|
59
82
|
return [execPath, "run", "--allow-all", ...flags, entry, ...args];
|
|
60
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,6 @@
|
|
|
1
1
|
import { type CdpParams, type EvaluateResult, type RemoteObject } from "./cdp.ts";
|
|
2
2
|
import { type CellTransformOptions, transformCell } from "./transform.ts";
|
|
3
|
+
import type { DebugChannel } from "./debugger.ts";
|
|
3
4
|
/** Targets driven over CDP. */
|
|
4
5
|
export type CdpRuntime = "node" | "deno";
|
|
5
6
|
/** Bun and workerd have their own backends: src/bun-target.ts, src/workerd-target.ts. */
|
|
@@ -37,6 +38,10 @@ export interface KernelTarget {
|
|
|
37
38
|
}>;
|
|
38
39
|
propertyNames(expression: string): Promise<string[]>;
|
|
39
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;
|
|
40
45
|
replyInput(value: string | null): void;
|
|
41
46
|
kill(): void;
|
|
42
47
|
}
|
|
@@ -114,6 +119,8 @@ export declare class Target implements KernelTarget {
|
|
|
114
119
|
denoCompat?: boolean;
|
|
115
120
|
denoConfig?: string;
|
|
116
121
|
}): Promise<Target>;
|
|
122
|
+
/** The Debugger domain on this target's session, for src/debugger.ts. */
|
|
123
|
+
get debug(): DebugChannel;
|
|
117
124
|
/** Resolves once all output and messages sent so far have been delivered. */
|
|
118
125
|
flush(): Promise<void>;
|
|
119
126
|
/** Runs transformed cell code. Rejects with Interrupted after interrupt(). */
|
|
@@ -149,6 +156,7 @@ export declare class Target implements KernelTarget {
|
|
|
149
156
|
stack: string;
|
|
150
157
|
}>;
|
|
151
158
|
propertyNames(expression: string): Promise<string[]>;
|
|
159
|
+
inspect(path: string, detail: number): Promise<string | null>;
|
|
152
160
|
lexicalNames(): Promise<string[]>;
|
|
153
161
|
replyInput(value: string | null): void;
|
|
154
162
|
kill(): void;
|
package/dist/target.js
CHANGED
|
@@ -179,6 +179,8 @@ export class Target {
|
|
|
179
179
|
#contextId;
|
|
180
180
|
#messages = Promise.resolve();
|
|
181
181
|
#interrupted = false;
|
|
182
|
+
#paused = false;
|
|
183
|
+
#baselineGlobals = new Set();
|
|
182
184
|
versions;
|
|
183
185
|
constructor(runtime, child, shell, control, stdout, stderr, gates, contextId, versions) {
|
|
184
186
|
this.runtime = runtime;
|
|
@@ -271,8 +273,26 @@ export class Target {
|
|
|
271
273
|
}
|
|
272
274
|
const versions = await target.#evaluateRaw(`JSON.stringify(typeof Deno !== "undefined" ? { runtime: Deno.version.deno, engine: "V8 " + Deno.version.v8 } : { runtime: process.versions.node, engine: "V8 " + process.versions.v8 })`);
|
|
273
275
|
Object.assign(target.versions, JSON.parse(String(versions.result.value)));
|
|
276
|
+
// The runtime's own enumerable globals, before any cell: the debugger's
|
|
277
|
+
// variable views show only what cells added (#18).
|
|
278
|
+
const globals = asEvaluateResult(await shell.send("Runtime.evaluate", { expression: "Object.keys(globalThis)", contextId, returnByValue: true }));
|
|
279
|
+
const names = globals.result.value;
|
|
280
|
+
if (Array.isArray(names))
|
|
281
|
+
for (const name of names)
|
|
282
|
+
target.#baselineGlobals.add(String(name));
|
|
283
|
+
shell.on("Debugger.paused", () => (target.#paused = true));
|
|
284
|
+
shell.on("Debugger.resumed", () => (target.#paused = false));
|
|
274
285
|
return target;
|
|
275
286
|
}
|
|
287
|
+
/** The Debugger domain on this target's session, for src/debugger.ts. */
|
|
288
|
+
get debug() {
|
|
289
|
+
return {
|
|
290
|
+
send: (method, params = {}) => this.#shell.send(method, params),
|
|
291
|
+
on: (method, listener) => this.#shell.on(method, listener),
|
|
292
|
+
contextId: this.#contextId,
|
|
293
|
+
baselineGlobals: this.#baselineGlobals,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
276
296
|
// Binding messages wait until both pipes have caught up to their marker,
|
|
277
297
|
// and are delivered strictly in order.
|
|
278
298
|
#deliver(msg, events) {
|
|
@@ -315,6 +335,9 @@ export class Target {
|
|
|
315
335
|
*/
|
|
316
336
|
async interrupt() {
|
|
317
337
|
this.#interrupted = true;
|
|
338
|
+
// A paused isolate runs nothing to terminate: let it go first (#18).
|
|
339
|
+
if (this.#paused)
|
|
340
|
+
await this.#shell.send("Debugger.resume").catch(() => { });
|
|
318
341
|
await this.#control.send("Runtime.terminateExecution");
|
|
319
342
|
this.#shell.abandonPending();
|
|
320
343
|
}
|
|
@@ -381,8 +404,18 @@ export class Target {
|
|
|
381
404
|
const value = res.result?.value;
|
|
382
405
|
return JSON.parse(typeof value === "string" ? value : '{"name":"Error","message":"","stack":""}');
|
|
383
406
|
}
|
|
407
|
+
/**
|
|
408
|
+
* Evaluates without side effects: V8 throws instead of running anything
|
|
409
|
+
* that would change state (a call, a getter that writes), so completing
|
|
410
|
+
* or inspecting `obj.` never runs user code (#10).
|
|
411
|
+
*/
|
|
412
|
+
#evaluatePure(expression) {
|
|
413
|
+
return this.#shell
|
|
414
|
+
.send("Runtime.evaluate", { expression, contextId: this.#contextId, replMode: true, throwOnSideEffect: true })
|
|
415
|
+
.then(asEvaluateResult, () => null);
|
|
416
|
+
}
|
|
384
417
|
async propertyNames(expression) {
|
|
385
|
-
const evaluated = await this.#
|
|
418
|
+
const evaluated = await this.#evaluatePure(`(${expression})`);
|
|
386
419
|
const objectId = evaluated?.result.objectId;
|
|
387
420
|
if (!objectId || evaluated?.exceptionDetails)
|
|
388
421
|
return [];
|
|
@@ -390,6 +423,23 @@ export class Target {
|
|
|
390
423
|
const list = props.result;
|
|
391
424
|
return (list ?? []).map((p) => p.name);
|
|
392
425
|
}
|
|
426
|
+
async inspect(path, detail) {
|
|
427
|
+
const found = await this.#evaluatePure(path);
|
|
428
|
+
if (!found || found.exceptionDetails)
|
|
429
|
+
return null;
|
|
430
|
+
const value = found.result;
|
|
431
|
+
const arg = value.objectId
|
|
432
|
+
? { objectId: value.objectId }
|
|
433
|
+
: value.unserializableValue !== undefined
|
|
434
|
+
? { unserializableValue: value.unserializableValue }
|
|
435
|
+
: { value: value.value };
|
|
436
|
+
const res = await this.callFunctionOn(`function (v, d) { return globalThis.${SHIM}.describeForInspect(v, d); }`, [
|
|
437
|
+
arg,
|
|
438
|
+
{ value: detail },
|
|
439
|
+
]);
|
|
440
|
+
const text = res.result?.value;
|
|
441
|
+
return typeof text === "string" ? text : null;
|
|
442
|
+
}
|
|
393
443
|
async lexicalNames() {
|
|
394
444
|
const res = await this.#shell.send("Runtime.globalLexicalScopeNames", { executionContextId: this.#contextId });
|
|
395
445
|
return res.names ?? [];
|
package/dist/text.d.ts
CHANGED
|
@@ -10,5 +10,11 @@ export type IsCompleteReply = {
|
|
|
10
10
|
export declare function checkIsComplete(code: string): IsCompleteReply;
|
|
11
11
|
/** The dotted expression ending at the cursor, e.g. "Math.fl". */
|
|
12
12
|
export declare function exprAtCursor(code: string, cursor: number): string;
|
|
13
|
+
/**
|
|
14
|
+
* The dotted name an inspect_request means (#10): the one the cursor is in
|
|
15
|
+
* or just after, or, right after "name(" while typing a call, that callee.
|
|
16
|
+
* Null when it isn't a plain path; nothing else is evaluated.
|
|
17
|
+
*/
|
|
18
|
+
export declare function inspectTarget(code: string, cursor: number): string | null;
|
|
13
19
|
export declare function codePointToUtf16Index(str: string, cpOffset: number): number;
|
|
14
20
|
export declare function utf16ToCodePointIndex(str: string, utf16Offset: number): number;
|
package/dist/text.js
CHANGED
|
@@ -51,6 +51,23 @@ export function exprAtCursor(code, cursor) {
|
|
|
51
51
|
const m = /[\w$.]+$/.exec(code.slice(0, cursor));
|
|
52
52
|
return m ? m[0] : "";
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* The dotted name an inspect_request means (#10): the one the cursor is in
|
|
56
|
+
* or just after, or, right after "name(" while typing a call, that callee.
|
|
57
|
+
* Null when it isn't a plain path; nothing else is evaluated.
|
|
58
|
+
*/
|
|
59
|
+
export function inspectTarget(code, cursor) {
|
|
60
|
+
let end = cursor;
|
|
61
|
+
// Inside a call's parentheses with nothing typed yet: "add(|".
|
|
62
|
+
const before = code.slice(0, cursor);
|
|
63
|
+
const call = /([\w$.]+)\(\s*$/.exec(before);
|
|
64
|
+
if (call)
|
|
65
|
+
end = cursor - (before.length - before.lastIndexOf("(", cursor));
|
|
66
|
+
const head = /[\w$.]*$/.exec(code.slice(0, end))?.[0] ?? "";
|
|
67
|
+
const tail = call ? "" : (/^[\w$]*/.exec(code.slice(end))?.[0] ?? "");
|
|
68
|
+
const path = (head + tail).replace(/\.$/, "");
|
|
69
|
+
return /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(path) ? path : null;
|
|
70
|
+
}
|
|
54
71
|
// Jupyter measures cursor_pos in Unicode codepoints; JS strings index UTF-16
|
|
55
72
|
// code units (denoland/deno#22771).
|
|
56
73
|
export function codePointToUtf16Index(str, cpOffset) {
|
package/dist/transform.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { LineMap } from "./sourcemap.ts";
|
|
1
2
|
export declare const SHIM = "__cdpk";
|
|
2
3
|
export declare class CellSyntaxError extends Error {
|
|
3
4
|
name: string;
|
|
@@ -23,6 +24,15 @@ export interface CellTransformOptions {
|
|
|
23
24
|
/** Rewrites module-only syntax in plain JavaScript into script syntax. */
|
|
24
25
|
export declare function rewriteModuleSyntax(js: string): string;
|
|
25
26
|
export declare function transformCell(code: string, options?: CellTransformOptions): string;
|
|
27
|
+
/**
|
|
28
|
+
* transformCell, plus the map between the cell's lines and the script's,
|
|
29
|
+
* for the debugger (#18). The rewrite keeps lines (see applyEdits), so the
|
|
30
|
+
* script's lines are esbuild's, after the prelude's one.
|
|
31
|
+
*/
|
|
32
|
+
export declare function transformCellWithLines(code: string, options?: CellTransformOptions): {
|
|
33
|
+
script: string;
|
|
34
|
+
lines: LineMap;
|
|
35
|
+
};
|
|
26
36
|
/**
|
|
27
37
|
* Like transformCell, for a target that evaluates cells itself, as global
|
|
28
38
|
* code (an indirect eval), instead of through the inspector's replMode: see
|