@tlockney/cdp-kernel 0.5.0 → 0.7.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 +27 -6
- package/dist/browser/channels.d.ts +27 -0
- package/dist/browser/channels.js +90 -0
- package/dist/browser/kernel.js +29674 -0
- package/dist/browser/kernel.js.map +7 -0
- package/dist/browser/mod.d.ts +20 -0
- package/dist/browser/mod.js +38 -0
- package/dist/browser/target.d.ts +71 -0
- package/dist/browser/target.js +206 -0
- package/dist/browser/worker.js +1000 -0
- package/dist/browser/worker.js.map +7 -0
- package/dist/cdp.d.ts +11 -0
- package/dist/cdp.js +6 -1
- package/dist/celld-target.js +9 -12
- package/dist/children.d.ts +25 -3
- package/dist/children.js +84 -2
- package/dist/debugger.d.ts +2 -0
- package/dist/debugger.js +12 -6
- package/dist/deno-types.d.ts +30 -0
- package/dist/deno-types.js +93 -0
- package/dist/kernel-core.d.ts +69 -0
- package/dist/kernel-core.js +754 -0
- package/dist/kernel.d.ts +12 -1
- package/dist/kernel.js +132 -605
- package/dist/language-service-worker.d.ts +1 -0
- package/dist/language-service-worker.js +88 -0
- package/dist/language-service.d.ts +64 -0
- package/dist/language-service.js +128 -0
- package/dist/main.js +102 -8
- package/dist/message.d.ts +34 -0
- package/dist/message.js +14 -0
- package/dist/mod.d.ts +1 -0
- package/dist/mod.js +1 -0
- package/dist/notebook-types.d.ts +62 -0
- package/dist/notebook-types.js +310 -0
- package/dist/package.d.ts +2 -2
- package/dist/package.js +2 -2
- package/dist/parent.d.ts +12 -0
- package/dist/parent.js +39 -0
- package/dist/port-holder.d.ts +13 -0
- package/dist/port-holder.js +105 -0
- package/dist/sourcemap.d.ts +7 -2
- package/dist/sourcemap.js +9 -3
- package/dist/strip-esbuild.d.ts +2 -0
- package/dist/strip-esbuild.js +44 -0
- package/dist/strip-sucrase.d.ts +2 -0
- package/dist/strip-sucrase.js +78 -0
- package/dist/target-api.d.ts +80 -0
- package/dist/target-api.js +22 -0
- package/dist/target.d.ts +26 -74
- package/dist/target.js +232 -58
- package/dist/traceback.d.ts +75 -0
- package/dist/traceback.js +200 -0
- package/dist/transform.d.ts +22 -1
- package/dist/transform.js +46 -43
- package/dist/wire.d.ts +4 -24
- package/dist/wire.js +3 -12
- package/dist/workerd-target.js +1 -1
- package/dist/workers-project.js +13 -2
- package/dist/zmq-channels.d.ts +14 -0
- package/dist/zmq-channels.js +59 -0
- package/dist/zmtp.d.ts +2 -0
- package/dist/zmtp.js +12 -0
- package/package.json +15 -5
- package/runtime/bootstrap.mjs +113 -33
- package/runtime/browser-worker.mjs +119 -0
- package/runtime/cdn.d.mts +2 -0
- package/runtime/cdn.mjs +17 -0
- package/runtime/display.mjs +202 -1
- package/runtime/format.d.mts +2 -0
- package/runtime/format.mjs +125 -0
- package/runtime/introspect.mjs +1 -1
- package/runtime/node-hooks.d.mts +6 -0
- package/runtime/node-hooks.mjs +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tlockney/cdp-kernel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Deno's Jupyter kernel, extracted to run notebooks on Node, Deno, Bun, workerd and celld",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Thomas Lockney <thomas@lockney.net>",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
],
|
|
21
21
|
"type": "module",
|
|
22
22
|
"engines": {
|
|
23
|
-
"node": ">=
|
|
23
|
+
"node": ">=22.18"
|
|
24
24
|
},
|
|
25
25
|
"bin": {
|
|
26
26
|
"cdp-kernel": "dist/main.js"
|
|
@@ -34,6 +34,11 @@
|
|
|
34
34
|
"types": "./dist/globals.d.ts",
|
|
35
35
|
"default": "./dist/globals.js"
|
|
36
36
|
},
|
|
37
|
+
"./browser": {
|
|
38
|
+
"types": "./dist/browser/mod.d.ts",
|
|
39
|
+
"default": "./dist/browser/kernel.js"
|
|
40
|
+
},
|
|
41
|
+
"./browser/worker": "./dist/browser/worker.js",
|
|
37
42
|
"./globals/deno-compat": {
|
|
38
43
|
"types": "./dist/globals-deno-compat.d.ts",
|
|
39
44
|
"default": "./dist/globals-deno-compat.js"
|
|
@@ -49,7 +54,7 @@
|
|
|
49
54
|
"access": "public"
|
|
50
55
|
},
|
|
51
56
|
"scripts": {
|
|
52
|
-
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
57
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && node scripts/build-browser.mjs dist/browser",
|
|
53
58
|
"check": "tsc -p tsconfig.json",
|
|
54
59
|
"docs:dev": "vitepress dev docs",
|
|
55
60
|
"docs:build": "vitepress build docs",
|
|
@@ -64,18 +69,23 @@
|
|
|
64
69
|
"prepack": "npm run build",
|
|
65
70
|
"test": "node --test --test-timeout=300000 test/*.test.ts",
|
|
66
71
|
"e2e": "uv run --with nbclient --with nbformat --with jupyter_client python test/e2e.py",
|
|
72
|
+
"bench": "uv run --with jupyter_client python test/bench.py",
|
|
73
|
+
"soak": "uv run --with jupyter_client python test/soak.py",
|
|
67
74
|
"test:lab": "uv run --with jupyterlab --with anywidget --with playwright --with nbformat python test/lab_widgets.py"
|
|
68
75
|
},
|
|
69
76
|
"dependencies": {
|
|
70
77
|
"acorn": "^8.15.0",
|
|
71
78
|
"acorn-walk": "^8.3.4",
|
|
72
|
-
"esbuild": "^0.25.10"
|
|
79
|
+
"esbuild": "^0.25.10",
|
|
80
|
+
"typescript": "^6.0.3"
|
|
73
81
|
},
|
|
74
82
|
"devDependencies": {
|
|
75
83
|
"@types/node": "^24.19.2",
|
|
76
|
-
"typescript": "^5.9.3",
|
|
77
84
|
"vitepress": "^1.6.4",
|
|
78
85
|
"workerd": "1.20261015.1",
|
|
79
86
|
"wrangler": "4.151.0"
|
|
87
|
+
},
|
|
88
|
+
"optionalDependencies": {
|
|
89
|
+
"sucrase": "^3.35.1"
|
|
80
90
|
}
|
|
81
91
|
}
|
package/runtime/bootstrap.mjs
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// node --inspect=127.0.0.1:0 runtime/bootstrap.mjs
|
|
3
3
|
// deno run -A --inspect=127.0.0.1:0 runtime/bootstrap.mjs
|
|
4
4
|
// then drives it over CDP. Everything the host needs from inside the
|
|
5
|
-
// runtime lives on globalThis.__cdpk; output to the host goes
|
|
6
|
-
//
|
|
5
|
+
// runtime lives on globalThis.__cdpk; output to the host goes over plain
|
|
6
|
+
// stdout/stderr, with messages as records on stdout (see send below).
|
|
7
7
|
// Bun is launched as `bun runtime/bootstrap.mjs` and driven over a pipe
|
|
8
8
|
// instead; see bun-channel.mjs.
|
|
9
9
|
//
|
|
@@ -27,7 +27,7 @@ const cwdUrl = pathToFileURL(cwd.endsWith("/") ? cwd : cwd + "/");
|
|
|
27
27
|
// Stand-in referrer for cell code, like Deno's ./$deno$jupyter_repl.mts.
|
|
28
28
|
const replUrl = new URL("$cdpk$repl.mjs", cwdUrl);
|
|
29
29
|
|
|
30
|
-
const { inspect, inspectPlain, readFile, writeOut, writeErr,
|
|
30
|
+
const { inspect, inspectPlain, readFile, writeOut, writeErr, readStdin } = await platform();
|
|
31
31
|
|
|
32
32
|
function base64(bytes) {
|
|
33
33
|
let binary = "";
|
|
@@ -38,11 +38,12 @@ function base64(bytes) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
// --- Output ordering ----------------------------------------------------
|
|
41
|
-
// stdout/stderr reach the host over pipes
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
41
|
+
// stdout/stderr reach the host over pipes, and so do messages: as records
|
|
42
|
+
// on stdout on Node and Deno, over Bun's channel on Bun. Every message has
|
|
43
|
+
// a number; stderr gets a gated marker ("m") with it, and stdout the record
|
|
44
|
+
// (Bun: a gated marker on both pipes). The host holds the message until
|
|
45
|
+
// both pipes have reached that number, and holds later pipe output until
|
|
46
|
+
// the message has gone out.
|
|
46
47
|
let barrierSeq = 0;
|
|
47
48
|
function barrier(gated = false) {
|
|
48
49
|
const n = ++barrierSeq;
|
|
@@ -52,15 +53,32 @@ function barrier(gated = false) {
|
|
|
52
53
|
return n;
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
// Node and Deno: messages were a CDP binding's events, but an event just
|
|
57
|
+
// before an evaluate's reply delays the reply ~40 ms (#271). A record is
|
|
58
|
+
// the message's JSON, which holds no raw ESC, between "\x1b[cdpk:<" and
|
|
59
|
+
// "\x1b]". Cells can call this too; the host drops what isn't a message
|
|
60
|
+
// (#122).
|
|
61
|
+
let attached = false;
|
|
62
|
+
function sendRecord(text) {
|
|
63
|
+
text = String(text);
|
|
64
|
+
if (attached && !text.includes("\x1b")) writeOut(`\x1b[cdpk:<${text}\x1b]`);
|
|
65
|
+
}
|
|
66
|
+
if (!isBun) globalThis.__cdpk_send = sendRecord;
|
|
67
|
+
|
|
55
68
|
function send(payload) {
|
|
56
|
-
|
|
57
|
-
if (typeof binding !== "function") return; // host not attached yet
|
|
69
|
+
if (isBun ? typeof globalThis.__cdpk_send !== "function" : !attached) return; // host not attached yet
|
|
58
70
|
// Serialize before writing the marker: if the payload can't be
|
|
59
71
|
// serialized (a BigInt, a cycle), the error reaches the cell without a
|
|
60
72
|
// gated marker the host would wait on forever (#129). `payload` always
|
|
61
73
|
// has a `kind`, so `body` is never "{}".
|
|
62
74
|
const body = JSON.stringify(payload);
|
|
63
|
-
|
|
75
|
+
if (isBun) {
|
|
76
|
+
globalThis.__cdpk_send(`{"barrier":${barrier(true)},${body.slice(1)}`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const n = ++barrierSeq;
|
|
80
|
+
writeErr(`\x1b[cdpk:${n}m]\x1b`);
|
|
81
|
+
sendRecord(`{"barrier":${n},${body.slice(1)}`);
|
|
64
82
|
}
|
|
65
83
|
|
|
66
84
|
const jupyter = createJupyter({
|
|
@@ -74,7 +92,7 @@ const jupyter = createJupyter({
|
|
|
74
92
|
// stdin for its JSON-line reply.
|
|
75
93
|
input(prompt, password) {
|
|
76
94
|
send({ kind: "input_request", prompt, password });
|
|
77
|
-
const line =
|
|
95
|
+
const line = readInputReply();
|
|
78
96
|
if (line === null) return null;
|
|
79
97
|
const reply = JSON.parse(line);
|
|
80
98
|
if (reply.interrupted === true) {
|
|
@@ -157,13 +175,23 @@ function hold(value) {
|
|
|
157
175
|
return promise;
|
|
158
176
|
}
|
|
159
177
|
|
|
178
|
+
// Yields one macrotask. Code the inspector dispatches calls this, and Node
|
|
179
|
+
// and Deno dispatch a message that arrives while JavaScript runs from a V8
|
|
180
|
+
// interrupt, which can land inside their timer code. A setTimeout there can
|
|
181
|
+
// create a timer list while the timer heap is mid-update, and the heap then
|
|
182
|
+
// loses it: every later setTimeout of that duration never fires (#237).
|
|
183
|
+
// setImmediate only appends to a linked list.
|
|
184
|
+
const yieldTask = () => new Promise((resolve) => setImmediate(resolve));
|
|
185
|
+
|
|
160
186
|
// Adds the cause to an error from a Deno member the --deno-compat shim
|
|
161
187
|
// lacks (#172); the identity everywhere else.
|
|
162
188
|
let explainError = (text) => text;
|
|
163
189
|
|
|
164
190
|
globalThis.__cdpk = {
|
|
191
|
+
// The host calls this once it reads message records (Node and Deno).
|
|
192
|
+
attach: () => void (attached = true),
|
|
165
193
|
// Cell code awaits this first; see PRELUDE in src/transform.ts.
|
|
166
|
-
tick:
|
|
194
|
+
tick: yieldTask,
|
|
167
195
|
// Node: the resolve hook re-parents these imports. Deno would resolve
|
|
168
196
|
// relative paths against this file, so denoImport does it here.
|
|
169
197
|
import: (specifier, options) =>
|
|
@@ -196,10 +224,19 @@ globalThis.__cdpk = {
|
|
|
196
224
|
// Frontend comm messages (#9), from the host's shell channel. Handlers
|
|
197
225
|
// run after one macrotask, like cells, so interrupt can reach them.
|
|
198
226
|
async commDispatch(msgType, content, metadata, buffers64) {
|
|
199
|
-
await
|
|
227
|
+
await yieldTask();
|
|
200
228
|
const buffers = buffers64.map((b) => Uint8Array.from(atob(b), (c) => c.charCodeAt(0)));
|
|
201
229
|
return await jupyter.commDispatch(msgType, content, metadata, buffers);
|
|
202
230
|
},
|
|
231
|
+
// The same, for a message the host wrote to stdin (Node and Deno). A
|
|
232
|
+
// value passed as a CDP call argument is compiled as source, and V8's
|
|
233
|
+
// compilation cache keeps that source until the heap nears its limit, so
|
|
234
|
+
// each message's data would stay in memory (#287). Read before the first
|
|
235
|
+
// await, so messages are taken in the order the host wrote them.
|
|
236
|
+
commDispatchFromStdin() {
|
|
237
|
+
const { msgType, content, metadata, buffers } = readCommRecord();
|
|
238
|
+
return globalThis.__cdpk.commDispatch(msgType, content, metadata, buffers);
|
|
239
|
+
},
|
|
203
240
|
describeError(value) {
|
|
204
241
|
if (value instanceof Error) {
|
|
205
242
|
const message = explainError(value.message);
|
|
@@ -308,21 +345,24 @@ if (isBun) {
|
|
|
308
345
|
});
|
|
309
346
|
}
|
|
310
347
|
|
|
348
|
+
function writeAllSync(stream, bytes) {
|
|
349
|
+
while (bytes.length > 0) bytes = bytes.subarray(stream.writeSync(bytes));
|
|
350
|
+
}
|
|
351
|
+
|
|
311
352
|
async function platform() {
|
|
312
353
|
if (isDeno) {
|
|
313
|
-
const stdinBuf = new Uint8Array(1);
|
|
314
354
|
return {
|
|
315
355
|
inspect: (v) => Deno.inspect(v, { colors: true, depth: 4 }),
|
|
316
356
|
inspectPlain: (v) => Deno.inspect(v, { colors: false, depth: 2 }),
|
|
317
357
|
readFile: (p) => Deno.readFileSync(p),
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
358
|
+
// writeSync can write part of the buffer; a record must go out whole.
|
|
359
|
+
writeOut: (s) => writeAllSync(Deno.stdout, ENC.encode(s)),
|
|
360
|
+
writeErr: (s) => writeAllSync(Deno.stderr, ENC.encode(s)),
|
|
361
|
+
readStdin: (bytes) => Deno.stdin.readSync(bytes) ?? 0,
|
|
321
362
|
};
|
|
322
363
|
}
|
|
323
364
|
const util = await import("node:util");
|
|
324
365
|
const fs = await import("node:fs");
|
|
325
|
-
const buf = Buffer.alloc(1);
|
|
326
366
|
// Node writes to a pipe without blocking and queues what doesn't fit for
|
|
327
367
|
// the event loop to flush. input() blocks the main thread in readSync, so
|
|
328
368
|
// queued output, and the input_request's barrier marker after it, would
|
|
@@ -335,19 +375,20 @@ async function platform() {
|
|
|
335
375
|
inspect: (v) => util.inspect(v, { colors: true, depth: 4 }),
|
|
336
376
|
inspectPlain: (v) => util.inspect(v, { colors: false, depth: 2 }),
|
|
337
377
|
readFile: (p) => fs.readFileSync(p),
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
378
|
+
// Bound now: a cell or library that patches process.stdout.write to
|
|
379
|
+
// capture output must not swallow message records (#271).
|
|
380
|
+
writeOut: process.stdout.write.bind(process.stdout),
|
|
381
|
+
writeErr: process.stderr.write.bind(process.stderr),
|
|
382
|
+
readStdin: (bytes) => {
|
|
383
|
+
for (;;) {
|
|
384
|
+
try {
|
|
385
|
+
return fs.readSync(0, bytes, 0, bytes.length, null);
|
|
386
|
+
} catch (err) {
|
|
387
|
+
if (err.code !== "EAGAIN") throw err;
|
|
388
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
349
389
|
}
|
|
350
|
-
}
|
|
390
|
+
}
|
|
391
|
+
},
|
|
351
392
|
};
|
|
352
393
|
}
|
|
353
394
|
|
|
@@ -376,12 +417,51 @@ async function syncBunOutput() {
|
|
|
376
417
|
}
|
|
377
418
|
}
|
|
378
419
|
|
|
379
|
-
|
|
420
|
+
// The host writes two kinds of record to stdin, both read synchronously: a
|
|
421
|
+
// prompt's reply, as one JSON line, and a frontend comm message (#287), as
|
|
422
|
+
// a `comm <length>` line followed by that many bytes of JSON.
|
|
423
|
+
const COMM_HEADER = /^comm (\d+)$/;
|
|
424
|
+
|
|
425
|
+
/** `n` bytes from stdin, or null if it ends first. */
|
|
426
|
+
function readExactly(n) {
|
|
427
|
+
const bytes = new Uint8Array(n);
|
|
428
|
+
for (let at = 0; at < n;) {
|
|
429
|
+
const got = readStdin(bytes.subarray(at));
|
|
430
|
+
if (!got) return null;
|
|
431
|
+
at += got;
|
|
432
|
+
}
|
|
433
|
+
return bytes;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function readLine() {
|
|
380
437
|
const bytes = [];
|
|
381
438
|
for (;;) {
|
|
382
|
-
const byte =
|
|
439
|
+
const byte = readExactly(1)?.[0] ?? null;
|
|
383
440
|
if (byte === null) return bytes.length ? new TextDecoder().decode(new Uint8Array(bytes)) : null;
|
|
384
441
|
if (byte === 0x0a) return new TextDecoder().decode(new Uint8Array(bytes));
|
|
385
442
|
bytes.push(byte);
|
|
386
443
|
}
|
|
387
444
|
}
|
|
445
|
+
|
|
446
|
+
/** The next prompt reply's line, past any comm record left unread. */
|
|
447
|
+
function readInputReply() {
|
|
448
|
+
for (;;) {
|
|
449
|
+
const line = readLine();
|
|
450
|
+
const comm = line === null ? null : COMM_HEADER.exec(line);
|
|
451
|
+
if (comm === null) return line;
|
|
452
|
+
readExactly(Number(comm[1]));
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** The next comm message, past any prompt reply left unread. */
|
|
457
|
+
function readCommRecord() {
|
|
458
|
+
for (;;) {
|
|
459
|
+
const line = readLine();
|
|
460
|
+
if (line === null) throw new Error("stdin closed before a comm message arrived");
|
|
461
|
+
const comm = COMM_HEADER.exec(line);
|
|
462
|
+
if (comm === null) continue;
|
|
463
|
+
const body = readExactly(Number(comm[1]));
|
|
464
|
+
if (body === null) throw new Error("stdin closed in the middle of a comm message");
|
|
465
|
+
return JSON.parse(new TextDecoder().decode(body));
|
|
466
|
+
}
|
|
467
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// The driver for the browser target: a module Web Worker that runs cells.
|
|
2
|
+
//
|
|
3
|
+
// The page's kernel (src/browser/target.ts) sends each request as a
|
|
4
|
+
// message and gets a reply message back, as over the workerd driver's
|
|
5
|
+
// WebSocket (runtime/workerd-worker.mjs), whose shape this follows. Cells
|
|
6
|
+
// come from transformCellForEval and run as global code with indirect
|
|
7
|
+
// eval, so their declarations persist from cell to cell.
|
|
8
|
+
//
|
|
9
|
+
// A Worker has no module resolution beyond URLs, so `npm:` and `jsr:`
|
|
10
|
+
// specifiers, and bare package names, load from a CDN (esm.sh by default).
|
|
11
|
+
|
|
12
|
+
import { createJupyter } from "./display.mjs";
|
|
13
|
+
import { createHandler } from "./evaluator.mjs";
|
|
14
|
+
import { format, inspect } from "./format.mjs";
|
|
15
|
+
import { describeValue, inspectPath, resolvePath } from "./introspect.mjs";
|
|
16
|
+
import { DEFAULT_CDN, resolveSpecifier } from "./cdn.mjs";
|
|
17
|
+
|
|
18
|
+
function base64(bytes) {
|
|
19
|
+
let binary = "";
|
|
20
|
+
for (let i = 0; i < bytes.length; i += 0x8000) {
|
|
21
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
|
22
|
+
}
|
|
23
|
+
return btoa(binary);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const post = (message) => globalThis.postMessage(message);
|
|
27
|
+
|
|
28
|
+
// --- Output -----------------------------------------------------------------
|
|
29
|
+
const stream = (name) => (...args) => post({ kind: "stream", name, text: format(...args) + "\n" });
|
|
30
|
+
Object.assign(console, {
|
|
31
|
+
log: stream("stdout"),
|
|
32
|
+
info: stream("stdout"),
|
|
33
|
+
debug: stream("stdout"),
|
|
34
|
+
dir: (value) => post({ kind: "stream", name: "stdout", text: inspect(value, { depth: 4 }) + "\n" }),
|
|
35
|
+
warn: stream("stderr"),
|
|
36
|
+
error: stream("stderr"),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// An error thrown later by a cell's timer or promise goes to stderr, as on
|
|
40
|
+
// the other targets.
|
|
41
|
+
function reportUncaught(kind, err) {
|
|
42
|
+
post({ kind: "stream", name: "stderr", text: `${kind}: ${err instanceof Error ? (err.stack ?? err.message) : inspect(err)}\n` });
|
|
43
|
+
}
|
|
44
|
+
globalThis.addEventListener("unhandledrejection", (event) => {
|
|
45
|
+
event.preventDefault();
|
|
46
|
+
reportUncaught("Uncaught (in promise)", event.reason);
|
|
47
|
+
});
|
|
48
|
+
globalThis.addEventListener("error", (event) => {
|
|
49
|
+
event.preventDefault();
|
|
50
|
+
reportUncaught("Uncaught", event.error ?? event.message);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const jupyter = createJupyter({
|
|
54
|
+
inspect: (value) => inspect(value, { depth: 4 }),
|
|
55
|
+
base64,
|
|
56
|
+
readFile() {
|
|
57
|
+
throw new Error("the browser target has no file system to read from; pass the image bytes instead");
|
|
58
|
+
},
|
|
59
|
+
send(msgType, content, metadata, buffers) {
|
|
60
|
+
post({ kind: "iopub", msg_type: msgType, content, metadata, buffers: buffers.map(base64), barrier: 0 });
|
|
61
|
+
},
|
|
62
|
+
// A synchronous prompt would need SharedArrayBuffer and Atomics.wait, so
|
|
63
|
+
// a cross-origin isolated page; until then, as with no stdin.
|
|
64
|
+
input: () => null,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// --- Modules ----------------------------------------------------------------
|
|
68
|
+
let cdn = DEFAULT_CDN;
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
// --- The shim cells and the evaluator use -----------------------------------
|
|
72
|
+
let handle = null;
|
|
73
|
+
|
|
74
|
+
function install({ base, cdn: cdnBase }) {
|
|
75
|
+
if (typeof cdnBase === "string") cdn = cdnBase.endsWith("/") ? cdnBase : cdnBase + "/";
|
|
76
|
+
const url = new URL("$cdpk$repl.mjs", base).href;
|
|
77
|
+
globalThis.__cdpk = {
|
|
78
|
+
import: (specifier, options) => import(resolveSpecifier(specifier, url, cdn), options),
|
|
79
|
+
meta: { url, resolve: (specifier) => resolveSpecifier(specifier, url, cdn) },
|
|
80
|
+
// inspect_request (#10): by descriptor, so nothing runs.
|
|
81
|
+
describeForInspect: (value, detail) => describeValue(value, { inspect: (v) => inspect(v, { depth: 2 }), detail }),
|
|
82
|
+
inspectPath: (path, detail) => inspectPath(path, { inspect: (v) => inspect(v, { depth: 2 }), detail }),
|
|
83
|
+
resolvePath,
|
|
84
|
+
barrier: () => 0,
|
|
85
|
+
broadcastResult: jupyter.broadcastResult,
|
|
86
|
+
async commDispatch(msgType, content, metadata, buffers64) {
|
|
87
|
+
const buffers = buffers64.map((b) => Uint8Array.from(atob(b), (c) => c.charCodeAt(0)));
|
|
88
|
+
return await jupyter.commDispatch(msgType, content, metadata, buffers);
|
|
89
|
+
},
|
|
90
|
+
describeError(value) {
|
|
91
|
+
if (value instanceof Error) {
|
|
92
|
+
return JSON.stringify({ name: value.name, message: value.message, stack: value.stack ?? "" });
|
|
93
|
+
}
|
|
94
|
+
return JSON.stringify({ name: "Error", message: inspect(value), stack: "" });
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
Object.defineProperty(globalThis, "__cdpk", { enumerable: false });
|
|
98
|
+
globalThis.jupyter = jupyter.namespace;
|
|
99
|
+
globalThis.prompt = jupyter.prompt;
|
|
100
|
+
globalThis.confirm = jupyter.confirm;
|
|
101
|
+
handle = createHandler(globalThis.__cdpk, (code) => (0, eval)(code));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function answer(req) {
|
|
105
|
+
if (req.op === "info") {
|
|
106
|
+
install(req);
|
|
107
|
+
return { userAgent: globalThis.navigator?.userAgent ?? "" };
|
|
108
|
+
}
|
|
109
|
+
if (handle === null) throw new Error("the driver wasn't sent info first");
|
|
110
|
+
return await handle(req);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
globalThis.addEventListener("message", (event) => {
|
|
114
|
+
const req = event.data;
|
|
115
|
+
answer(req).then(
|
|
116
|
+
(reply) => post({ kind: "reply", id: req.id, ...reply }),
|
|
117
|
+
(err) => post({ kind: "reply", id: req.id, failure: String(err?.stack ?? err) }),
|
|
118
|
+
);
|
|
119
|
+
});
|
package/runtime/cdn.mjs
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Where a browser cell's imports load from. A Worker resolves only URLs, so
|
|
2
|
+
// packages come from a CDN that serves npm and jsr as ES modules: esm.sh by
|
|
3
|
+
// default, with its jsr/ prefix for jsr packages.
|
|
4
|
+
//
|
|
5
|
+
// No imports: this file must load in every target.
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_CDN = "https://esm.sh/";
|
|
8
|
+
|
|
9
|
+
/** The URL `specifier`, imported from `base`, loads from. */
|
|
10
|
+
export function resolveSpecifier(specifier, base, cdn = DEFAULT_CDN) {
|
|
11
|
+
if (/^(?:https?|data|blob):/.test(specifier)) return specifier;
|
|
12
|
+
if (/^\.{0,2}\//.test(specifier)) return new URL(specifier, base).href;
|
|
13
|
+
if (specifier.startsWith("npm:")) return cdn + specifier.slice(4);
|
|
14
|
+
if (specifier.startsWith("jsr:")) return `${cdn}jsr/${specifier.slice(4).replace(/^\//, "")}`;
|
|
15
|
+
// esm.sh serves polyfills for Node's builtins under node:.
|
|
16
|
+
return cdn + specifier;
|
|
17
|
+
}
|
package/runtime/display.mjs
CHANGED
|
@@ -45,7 +45,201 @@ function extractVega(obj) {
|
|
|
45
45
|
} else if (spec.$schema === "https://vega.github.io/schema/vega-lite/v5.json") {
|
|
46
46
|
mediaType = "application/vnd.vegalite.v5+json";
|
|
47
47
|
}
|
|
48
|
-
|
|
48
|
+
const summary = summarizeChart(spec);
|
|
49
|
+
return summary === undefined ? { [mediaType]: spec } : { [mediaType]: spec, "text/plain": summary };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// A few lines of text/plain next to a chart, for frontends that can't draw
|
|
53
|
+
// it (jupyter console, nbconvert to text, agents reading the notebook):
|
|
54
|
+
// mark, encodings with value ranges, title and row count (#321). The
|
|
55
|
+
// approach follows nteract's repr-llm summarize_viz (BSD-3).
|
|
56
|
+
const VL_TYPES = { Q: "quantitative", N: "nominal", O: "ordinal", T: "temporal", G: "geojson" };
|
|
57
|
+
const MAX_CHILDREN = 6;
|
|
58
|
+
|
|
59
|
+
function clip(str, max = 60) {
|
|
60
|
+
return str.length > max ? `${str.slice(0, max - 1)}…` : str;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function formatNumber(n) {
|
|
64
|
+
return Number.isInteger(n) ? String(n) : String(Number(n.toPrecision(4)));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function titleText(title) {
|
|
68
|
+
const text = typeof title === "object" && title !== null && !Array.isArray(title) ? title.text : title;
|
|
69
|
+
const str = Array.isArray(text) ? text.join(" ") : typeof text === "string" ? text : "";
|
|
70
|
+
return str ? ` "${clip(str, 80)}"` : "";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getField(row, field) {
|
|
74
|
+
if (row === null || typeof row !== "object") return undefined;
|
|
75
|
+
if (field in row) return row[field];
|
|
76
|
+
// Vega-Lite reads "a.b" as a nested path and "a\.b" as a key with a dot.
|
|
77
|
+
if (field.includes("\\")) return row[field.replace(/\\(.)/g, "$1")];
|
|
78
|
+
return field.split(".").reduce((obj, key) => (obj === null || typeof obj !== "object" ? undefined : obj[key]), row);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function fieldStats(rows, field, type) {
|
|
82
|
+
const values = [];
|
|
83
|
+
for (const row of rows) {
|
|
84
|
+
const value = getField(row, field);
|
|
85
|
+
if (value !== undefined && value !== null && value !== "") values.push(value);
|
|
86
|
+
}
|
|
87
|
+
if (values.length === 0) return "";
|
|
88
|
+
if (type === "quantitative") {
|
|
89
|
+
let min = Infinity;
|
|
90
|
+
let max = -Infinity;
|
|
91
|
+
for (const value of values) {
|
|
92
|
+
const n = Number(value);
|
|
93
|
+
if (!Number.isFinite(n)) continue;
|
|
94
|
+
if (n < min) min = n;
|
|
95
|
+
if (n > max) max = n;
|
|
96
|
+
}
|
|
97
|
+
if (min > max) return "";
|
|
98
|
+
return min === max ? formatNumber(min) : `${formatNumber(min)} to ${formatNumber(max)}`;
|
|
99
|
+
}
|
|
100
|
+
if (type === "temporal") {
|
|
101
|
+
let min;
|
|
102
|
+
let max;
|
|
103
|
+
for (const value of values) {
|
|
104
|
+
const t = value instanceof Date ? value.getTime() : typeof value === "number" ? value : Date.parse(String(value));
|
|
105
|
+
if (!Number.isFinite(t)) continue;
|
|
106
|
+
if (min === undefined || t < min.t) min = { t, value };
|
|
107
|
+
if (max === undefined || t > max.t) max = { t, value };
|
|
108
|
+
}
|
|
109
|
+
if (min === undefined) return "";
|
|
110
|
+
const show = ({ value }) =>
|
|
111
|
+
typeof value === "string" ? clip(value, 30) : new Date(value instanceof Date ? value.getTime() : value).toISOString();
|
|
112
|
+
return min.t === max.t ? show(min) : `${show(min)} to ${show(max)}`;
|
|
113
|
+
}
|
|
114
|
+
const distinct = new Set(values.map((value) => typeof value === "object" ? JSON.stringify(value) : String(value)));
|
|
115
|
+
return `${distinct.size} ${distinct.size === 1 ? "value" : "values"}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function encodingSummary(encoding, rows) {
|
|
119
|
+
const parts = [];
|
|
120
|
+
for (const [channel, def] of Object.entries(encoding)) {
|
|
121
|
+
if (channel === "tooltip" || def === null || typeof def !== "object" || Array.isArray(def)) continue;
|
|
122
|
+
if (def.field === undefined && def.aggregate === undefined) continue;
|
|
123
|
+
let name = typeof def.field === "string" ? def.field : def.field?.repeat ? `repeat ${def.field.repeat}` : "";
|
|
124
|
+
if (def.aggregate !== undefined) name = `${def.aggregate}(${name})`;
|
|
125
|
+
else if (def.timeUnit !== undefined) name = `${typeof def.timeUnit === "string" ? def.timeUnit : "time"}(${name})`;
|
|
126
|
+
const type = VL_TYPES[def.type] ?? def.type;
|
|
127
|
+
const details = [];
|
|
128
|
+
if (typeof type === "string") details.push(type);
|
|
129
|
+
if (rows && typeof def.field === "string" && def.aggregate === undefined) {
|
|
130
|
+
const stats = fieldStats(rows, def.field, type);
|
|
131
|
+
if (stats) details.push(stats);
|
|
132
|
+
}
|
|
133
|
+
parts.push(`${channel}=${clip(name, 40)}${details.length ? ` (${details.join(", ")})` : ""}`);
|
|
134
|
+
}
|
|
135
|
+
return parts;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function vlRows(data, datasets) {
|
|
139
|
+
if (data === null || typeof data !== "object") return undefined;
|
|
140
|
+
if (Array.isArray(data.values)) return data.values;
|
|
141
|
+
if (typeof data.name === "string" && Array.isArray(datasets?.[data.name])) return datasets[data.name];
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function vlDataNote(data, rows) {
|
|
146
|
+
if (rows) return `${rows.length} ${rows.length === 1 ? "row" : "rows"}`;
|
|
147
|
+
if (typeof data?.url === "string") return `data from ${clip(data.url)}`;
|
|
148
|
+
return "";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function fieldList(def) {
|
|
152
|
+
if (Array.isArray(def)) return def.join(", ");
|
|
153
|
+
if (def === null || typeof def !== "object") return "";
|
|
154
|
+
if (typeof def.field === "string") return def.field;
|
|
155
|
+
return Object.values(def).map(fieldList).filter(Boolean).join(", ");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function summarizeVegaLiteView(spec, label, inherited, datasets, depth, lines) {
|
|
159
|
+
const indent = " ".repeat(depth);
|
|
160
|
+
const data = spec.data ?? inherited.data;
|
|
161
|
+
const rows = vlRows(data, datasets);
|
|
162
|
+
const head = `${indent}${label}`;
|
|
163
|
+
const title = titleText(spec.title);
|
|
164
|
+
const children = spec.layer ?? spec.concat ?? spec.hconcat ?? spec.vconcat;
|
|
165
|
+
if (Array.isArray(children)) {
|
|
166
|
+
const noun = spec.layer ? "layer" : "view";
|
|
167
|
+
const note = [`${children.length} ${children.length === 1 ? noun : `${noun}s`}`, vlDataNote(data, rows)].filter(Boolean);
|
|
168
|
+
lines.push(`${head}${spec.layer ? "layered" : "concatenated"} chart${title}: ${note.join(", ")}`);
|
|
169
|
+
const encoding = spec.layer ? { ...inherited.encoding, ...spec.encoding } : {};
|
|
170
|
+
children.slice(0, MAX_CHILDREN).forEach((child, i) => {
|
|
171
|
+
if (child !== null && typeof child === "object") {
|
|
172
|
+
summarizeVegaLiteView(child, `${noun} ${i + 1}: `, { data, encoding }, datasets, depth + 1, lines);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
if (children.length > MAX_CHILDREN) lines.push(`${indent} … ${children.length - MAX_CHILDREN} more`);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if ((spec.facet !== undefined || spec.repeat !== undefined) && spec.spec !== null && typeof spec.spec === "object") {
|
|
179
|
+
const what = spec.facet !== undefined ? `faceted by ${fieldList(spec.facet)}` : `repeated over ${fieldList(spec.repeat)}`;
|
|
180
|
+
const note = vlDataNote(data, rows);
|
|
181
|
+
lines.push(`${head}chart${title} ${what}${note ? `, ${note}` : ""}`);
|
|
182
|
+
summarizeVegaLiteView(spec.spec, "each panel: ", { data, encoding: {} }, datasets, depth + 1, lines);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const mark = typeof spec.mark === "string" ? spec.mark : spec.mark?.type;
|
|
186
|
+
const encoding = { ...inherited.encoding, ...spec.encoding };
|
|
187
|
+
const parts = encodingSummary(encoding, rows);
|
|
188
|
+
// A layer's rows were already counted on the layered chart's line.
|
|
189
|
+
if (spec.data !== undefined || depth === 0) {
|
|
190
|
+
const note = vlDataNote(data, rows);
|
|
191
|
+
if (note) parts.push(note);
|
|
192
|
+
}
|
|
193
|
+
lines.push(`${head}${typeof mark === "string" ? `${mark} ` : ""}chart${title}${parts.length ? `: ${parts.join(", ")}` : ""}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function vegaMarkTypes(marks, types) {
|
|
197
|
+
if (!Array.isArray(marks)) return types;
|
|
198
|
+
for (const mark of marks) {
|
|
199
|
+
if (typeof mark?.type === "string") types.add(mark.type);
|
|
200
|
+
vegaMarkTypes(mark?.marks, types);
|
|
201
|
+
}
|
|
202
|
+
return types;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function summarizeVega(spec) {
|
|
206
|
+
const sections = [];
|
|
207
|
+
const marks = [...vegaMarkTypes(spec.marks, new Set())];
|
|
208
|
+
if (marks.length) sections.push(`marks ${marks.join(", ")}`);
|
|
209
|
+
const list = (items, describe) => {
|
|
210
|
+
const shown = items.slice(0, MAX_CHILDREN).map(describe);
|
|
211
|
+
if (items.length > MAX_CHILDREN) shown.push(`… ${items.length - MAX_CHILDREN} more`);
|
|
212
|
+
return shown.join(", ");
|
|
213
|
+
};
|
|
214
|
+
const data = Array.isArray(spec.data) ? spec.data.filter((d) => d !== null && typeof d === "object") : [];
|
|
215
|
+
if (data.length) {
|
|
216
|
+
sections.push(`data ${list(data, (d) => {
|
|
217
|
+
const name = clip(String(d.name ?? "?"), 30);
|
|
218
|
+
if (Array.isArray(d.values)) return `${name} (${d.values.length} ${d.values.length === 1 ? "row" : "rows"})`;
|
|
219
|
+
if (typeof d.url === "string") return `${name} (from ${clip(d.url)})`;
|
|
220
|
+
if (d.source !== undefined) return `${name} (from ${clip(String(d.source), 30)})`;
|
|
221
|
+
return name;
|
|
222
|
+
})}`);
|
|
223
|
+
}
|
|
224
|
+
const scales = Array.isArray(spec.scales) ? spec.scales.filter((s) => s !== null && typeof s === "object") : [];
|
|
225
|
+
if (scales.length) {
|
|
226
|
+
sections.push(`scales ${list(scales, (s) => `${clip(String(s.name ?? "?"), 30)} (${s.type ?? "linear"})`)}`);
|
|
227
|
+
}
|
|
228
|
+
return `Vega chart${titleText(spec.title)}${sections.length ? `: ${sections.join("; ")}` : ""}`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function summarizeChart(spec) {
|
|
232
|
+
try {
|
|
233
|
+
if (typeof spec.$schema === "string" && spec.$schema.includes("/vega-lite/")) {
|
|
234
|
+
const lines = [];
|
|
235
|
+
summarizeVegaLiteView(spec, "Vega-Lite ", { encoding: {} }, spec.datasets, 0, lines);
|
|
236
|
+
return lines.join("\n");
|
|
237
|
+
}
|
|
238
|
+
return summarizeVega(spec);
|
|
239
|
+
} catch {
|
|
240
|
+
// A summary is a fallback; a spec it doesn't understand still renders.
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
49
243
|
}
|
|
50
244
|
|
|
51
245
|
function isDataFrameLike(obj) {
|
|
@@ -95,9 +289,16 @@ function extractDataFrame(df) {
|
|
|
95
289
|
html += "</tr>";
|
|
96
290
|
}
|
|
97
291
|
html += "</tbody></table>";
|
|
292
|
+
// Polars has `height`; the row count is left out when there isn't one.
|
|
293
|
+
const size = [];
|
|
294
|
+
if (Number.isInteger(df.height)) size.push(`${df.height} ${df.height === 1 ? "row" : "rows"}`);
|
|
295
|
+
size.push(`${fields.length} ${fields.length === 1 ? "column" : "columns"}`);
|
|
296
|
+
const columns = fields.slice(0, 20).map((field) => `${clip(String(field.name), 40)} (${field.type})`);
|
|
297
|
+
if (fields.length > 20) columns.push(`… ${fields.length - 20} more`);
|
|
98
298
|
return {
|
|
99
299
|
"application/vnd.dataresource+json": { data, schema: { fields } },
|
|
100
300
|
"text/html": html,
|
|
301
|
+
"text/plain": `DataFrame (${size.join(", ")})${columns.length ? `: ${columns.join(", ")}` : ""}`,
|
|
101
302
|
};
|
|
102
303
|
}
|
|
103
304
|
|