@tlockney/cdp-kernel 0.7.0 → 0.7.2
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 +4 -2
- package/dist/browser/kernel.js +150 -13
- package/dist/browser/kernel.js.map +2 -2
- package/dist/browser/mod.d.ts +6 -1
- package/dist/browser/mod.js +4 -1
- package/dist/browser/target.d.ts +27 -3
- package/dist/browser/target.js +76 -7
- package/dist/browser/worker.js +83 -12
- package/dist/browser/worker.js.map +3 -3
- package/dist/bun-target.d.ts +2 -0
- package/dist/bun-target.js +15 -2
- package/dist/cdp.d.ts +2 -0
- package/dist/cdp.js +34 -7
- package/dist/celld-target.js +3 -2
- package/dist/debugger.js +51 -5
- package/dist/kernel-core.d.ts +9 -0
- package/dist/kernel-core.js +73 -3
- package/dist/kernel.js +12 -0
- package/dist/main.js +11 -4
- package/dist/package.d.ts +2 -2
- package/dist/package.js +2 -2
- package/dist/target-api.d.ts +12 -0
- package/dist/target-api.js +26 -0
- package/dist/target.d.ts +3 -2
- package/dist/target.js +25 -13
- package/dist/workerd-target.d.ts +16 -1
- package/dist/workerd-target.js +103 -15
- package/dist/workers-project.d.ts +1 -1
- package/dist/workers-project.js +4 -3
- package/package.json +3 -1
- package/runtime/bootstrap.mjs +2 -0
- package/runtime/browser-worker.mjs +83 -0
- package/runtime/evaluator.mjs +2 -0
- package/runtime/workerd-worker.mjs +2 -0
package/dist/workerd-target.js
CHANGED
|
@@ -18,17 +18,18 @@ import { Buffer } from "node:buffer";
|
|
|
18
18
|
import process from "node:process";
|
|
19
19
|
import { execFileSync, spawn } from "node:child_process";
|
|
20
20
|
import { randomBytes } from "node:crypto";
|
|
21
|
-
import { copyFileSync, existsSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, rmSync, writeFileSync } from "node:fs";
|
|
22
|
-
import { builtinModules } from "node:module";
|
|
21
|
+
import { copyFileSync, existsSync, lstatSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { builtinModules, createRequire } from "node:module";
|
|
23
23
|
import { tmpdir } from "node:os";
|
|
24
|
-
import { dirname, join } from "node:path";
|
|
24
|
+
import { basename, delimiter, dirname, isAbsolute, join } from "node:path";
|
|
25
25
|
import { pathToFileURL } from "node:url";
|
|
26
26
|
import { build } from "esbuild";
|
|
27
27
|
import { asEvaluateResult } from "./cdp.js";
|
|
28
28
|
import { transformCellForEval } from "./transform.js";
|
|
29
29
|
import { runtimeDir } from "./package.js";
|
|
30
30
|
import { killOnExit } from "./children.js";
|
|
31
|
-
import { Interrupted, isTargetMessage } from "./target.js";
|
|
31
|
+
import { Interrupted, isTargetMessage, parseBundle, parseJson } from "./target.js";
|
|
32
|
+
import { withStartTimeout } from "./target-api.js";
|
|
32
33
|
import { connectUnixWebSocket } from "./ws-client.js";
|
|
33
34
|
import { findWranglerConfig, startMiniflare } from "./workers-project.js";
|
|
34
35
|
import { ensureInstalledAsync, findLockFile, jsrToNpm, parsePackageSpecifier } from "../runtime/node-hooks.mjs";
|
|
@@ -87,6 +88,50 @@ export function findWorkerd(cwd) {
|
|
|
87
88
|
return "workerd";
|
|
88
89
|
}
|
|
89
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* The native workerd binary behind `executable`. npm's workerd package
|
|
93
|
+
* installs bin/workerd as a Node script that runs the platform package's
|
|
94
|
+
* binary with execFileSync, and only its postinstall swaps the binary in.
|
|
95
|
+
* When install scripts don't run (the npm that comes with Node 27 blocks
|
|
96
|
+
* those not in allowScripts), killing that script leaves the binary running
|
|
97
|
+
* with the driver socket open, so it's never what the target spawns (#397).
|
|
98
|
+
* Anything that isn't the workerd package's script is returned as given.
|
|
99
|
+
*/
|
|
100
|
+
export function nativeWorkerd(executable) {
|
|
101
|
+
const candidates = [];
|
|
102
|
+
let path = executable;
|
|
103
|
+
if (!isAbsolute(path) && !path.includes("/") && !path.includes("\\")) {
|
|
104
|
+
const dir = (process.env.PATH ?? "").split(delimiter).find((d) => d && existsSync(join(d, path)));
|
|
105
|
+
if (dir === undefined)
|
|
106
|
+
return executable;
|
|
107
|
+
path = join(dir, path);
|
|
108
|
+
}
|
|
109
|
+
// node_modules/.bin/workerd, a link or (on Windows) a .cmd shim.
|
|
110
|
+
if (basename(dirname(path)) === ".bin")
|
|
111
|
+
candidates.push(join(dirname(dirname(path)), "workerd"));
|
|
112
|
+
try {
|
|
113
|
+
// A link to <package>/bin/workerd, as a global install makes.
|
|
114
|
+
candidates.push(dirname(dirname(realpathSync(path))));
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return executable;
|
|
118
|
+
}
|
|
119
|
+
for (const pkg of candidates) {
|
|
120
|
+
try {
|
|
121
|
+
const { name } = JSON.parse(readFileSync(join(pkg, "package.json"), "utf8"));
|
|
122
|
+
if (name !== "workerd")
|
|
123
|
+
continue;
|
|
124
|
+
// The package's main export is the binary's path.
|
|
125
|
+
const binary = createRequire(join(pkg, "package.json"))(join(pkg, "lib", "main.js")).default;
|
|
126
|
+
if (typeof binary === "string" && existsSync(binary))
|
|
127
|
+
return binary;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Not the npm package, or one this doesn't understand: use it as given.
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return executable;
|
|
134
|
+
}
|
|
90
135
|
/** "workerd 2026-10-15" -> "2026-10-15": the newest date it supports. */
|
|
91
136
|
export function compatibilityDateOf(versionOutput) {
|
|
92
137
|
const m = /\b(\d{4}-\d{2}-\d{2})\b/.exec(versionOutput);
|
|
@@ -188,8 +233,30 @@ function isWorkerdIn(pid, dir) {
|
|
|
188
233
|
try {
|
|
189
234
|
if (process.platform === "linux")
|
|
190
235
|
return readlinkSync(`/proc/${pid}/cwd`) === dir;
|
|
191
|
-
|
|
192
|
-
|
|
236
|
+
// No cwd to compare elsewhere: the process must at least be ours and
|
|
237
|
+
// look like a workerd (#335).
|
|
238
|
+
const [uid, ...args] = execFileSync("ps", ["-o", "uid=,args=", "-p", String(pid)], { encoding: "utf8" }).trim().split(/\s+/);
|
|
239
|
+
const command = args.join(" ");
|
|
240
|
+
return Number(uid) === process.getuid?.() && /workerd/.test(command) && command.includes("config.capnp");
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Whether `dir` is a directory mkdtemp made for this user: not a symlink,
|
|
248
|
+
* owned by us, and closed to everyone else. Anyone can create entries in
|
|
249
|
+
* tmpdir(), so nothing in a directory that fails this is trusted (#335).
|
|
250
|
+
* Windows has no uids; its tmpdir() is per-user.
|
|
251
|
+
*/
|
|
252
|
+
function isOwnPrivateDir(dir) {
|
|
253
|
+
try {
|
|
254
|
+
const stat = lstatSync(dir);
|
|
255
|
+
if (!stat.isDirectory())
|
|
256
|
+
return false;
|
|
257
|
+
if (process.getuid === undefined)
|
|
258
|
+
return true;
|
|
259
|
+
return stat.uid === process.getuid() && (stat.mode & 0o077) === 0;
|
|
193
260
|
}
|
|
194
261
|
catch {
|
|
195
262
|
return false;
|
|
@@ -210,12 +277,14 @@ export function sweepStaleWorkerdDirs(root = tmpdir()) {
|
|
|
210
277
|
}
|
|
211
278
|
for (const name of names) {
|
|
212
279
|
const dir = join(root, name);
|
|
280
|
+
if (!isOwnPrivateDir(dir))
|
|
281
|
+
continue;
|
|
213
282
|
let owner;
|
|
214
283
|
try {
|
|
215
284
|
owner = JSON.parse(readFileSync(join(dir, OWNER_FILE), "utf8"));
|
|
216
285
|
}
|
|
217
286
|
catch {
|
|
218
|
-
continue; //
|
|
287
|
+
continue; // From before owner files, or being set up.
|
|
219
288
|
}
|
|
220
289
|
if (typeof owner.host !== "number" || pidAlive(owner.host))
|
|
221
290
|
continue;
|
|
@@ -227,7 +296,12 @@ export function sweepStaleWorkerdDirs(root = tmpdir()) {
|
|
|
227
296
|
// Gone already.
|
|
228
297
|
}
|
|
229
298
|
}
|
|
230
|
-
|
|
299
|
+
try {
|
|
300
|
+
rmSync(dir, { recursive: true, force: true });
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
// Cleanup is best effort; it must not stop this kernel starting.
|
|
304
|
+
}
|
|
231
305
|
}
|
|
232
306
|
}
|
|
233
307
|
/** The driver on a workerd of its own, behind a Unix socket. */
|
|
@@ -241,7 +315,7 @@ export class RawWorkerd {
|
|
|
241
315
|
this.runtime = runtime;
|
|
242
316
|
}
|
|
243
317
|
static async start(cwd, executable) {
|
|
244
|
-
const workerd = executable ?? findWorkerd(cwd);
|
|
318
|
+
const workerd = nativeWorkerd(executable ?? findWorkerd(cwd));
|
|
245
319
|
const compatibilityDate = compatibilityDateOf(execFileSync(workerd, ["--version"], { encoding: "utf8" }));
|
|
246
320
|
const source = await runtimeDir(new URL("../runtime/", import.meta.url).href);
|
|
247
321
|
// mkdtemp makes the directory 0700: only this user reaches the socket,
|
|
@@ -295,7 +369,7 @@ export class RawWorkerd {
|
|
|
295
369
|
}
|
|
296
370
|
})();
|
|
297
371
|
try {
|
|
298
|
-
const host = new RawWorkerd(child, await Promise.race([ready, exited]), compatibilityDate);
|
|
372
|
+
const host = new RawWorkerd(child, await withStartTimeout(Promise.race([ready, exited]), "workerd"), compatibilityDate);
|
|
299
373
|
starting = false;
|
|
300
374
|
early = "";
|
|
301
375
|
return host;
|
|
@@ -360,7 +434,7 @@ export class WorkerdTarget {
|
|
|
360
434
|
let notice;
|
|
361
435
|
if (project !== null) {
|
|
362
436
|
try {
|
|
363
|
-
host = await startMiniflare(project, cwd);
|
|
437
|
+
host = await startMiniflare(project, cwd, options.startTimeoutMs);
|
|
364
438
|
}
|
|
365
439
|
catch (err) {
|
|
366
440
|
// Like a broken deno.json (#13): say why in the first cell, and run
|
|
@@ -370,16 +444,18 @@ export class WorkerdTarget {
|
|
|
370
444
|
}
|
|
371
445
|
}
|
|
372
446
|
host ??= await RawWorkerd.start(cwd, options.executable);
|
|
373
|
-
return await WorkerdTarget.open(host, cwd, events, "workerd", notice);
|
|
447
|
+
return await WorkerdTarget.open(host, cwd, events, "workerd", { notice, startTimeoutMs: options.startTimeoutMs });
|
|
374
448
|
}
|
|
375
449
|
/**
|
|
376
450
|
* A target over a started host's session. `notice` (a project that
|
|
377
451
|
* failed to load) is replaced by the bindings when the host has a project.
|
|
378
452
|
*/
|
|
379
|
-
static async open(host, cwd, events, runtime,
|
|
453
|
+
static async open(host, cwd, events, runtime, options = {}) {
|
|
380
454
|
const target = new WorkerdTarget(host, cwd, events, runtime);
|
|
455
|
+
let notice = options.notice;
|
|
381
456
|
try {
|
|
382
|
-
|
|
457
|
+
// A driver that never answers is stopped (#336).
|
|
458
|
+
const { v8, bindings, worker } = await withStartTimeout(target.#request("info"), `the ${runtime} driver`, options.startTimeoutMs);
|
|
383
459
|
if (typeof v8 === "string" && v8)
|
|
384
460
|
target.versions.engine = `V8 ${v8}`;
|
|
385
461
|
if (host.project !== undefined) {
|
|
@@ -396,7 +472,15 @@ export class WorkerdTarget {
|
|
|
396
472
|
return target;
|
|
397
473
|
}
|
|
398
474
|
#receive(text) {
|
|
399
|
-
const msg =
|
|
475
|
+
const msg = parseJson(text);
|
|
476
|
+
if (msg === undefined) {
|
|
477
|
+
// The driver only sends JSON; a frame that isn't means the session is
|
|
478
|
+
// broken, which ends the target rather than the kernel (#337).
|
|
479
|
+
this.#stopNote = `[cdp-kernel] ${this.runtime} sent a message that isn't JSON, so it was stopped; ` +
|
|
480
|
+
"it restarts on the next cell, without the earlier cells' state";
|
|
481
|
+
this.kill();
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
400
484
|
if (isRecord(msg) && msg.kind === "stream") {
|
|
401
485
|
this.#events.stream(msg.name === "stderr" ? "stderr" : "stdout", String(msg.text));
|
|
402
486
|
}
|
|
@@ -519,6 +603,10 @@ export class WorkerdTarget {
|
|
|
519
603
|
}
|
|
520
604
|
return { handled: reply.handled === true };
|
|
521
605
|
}
|
|
606
|
+
async format(value) {
|
|
607
|
+
const reply = await this.#request("format", { handle: value.objectId });
|
|
608
|
+
return parseBundle(reply.value);
|
|
609
|
+
}
|
|
522
610
|
async describeError(exception) {
|
|
523
611
|
if (exception.objectId === undefined)
|
|
524
612
|
return { name: "Error", message: exception.description ?? "", stack: "" };
|
|
@@ -24,4 +24,4 @@ export declare function bundleProject(main: string, define?: {
|
|
|
24
24
|
[name: string]: string;
|
|
25
25
|
}): Promise<string>;
|
|
26
26
|
/** Starts the driver on the project's Miniflare with its bindings. */
|
|
27
|
-
export declare function startMiniflare(configPath: string, cwd: string): Promise<WorkerHost>;
|
|
27
|
+
export declare function startMiniflare(configPath: string, cwd: string, startTimeoutMs?: number): Promise<WorkerHost>;
|
package/dist/workers-project.js
CHANGED
|
@@ -17,6 +17,7 @@ import { dirname, join } from "node:path";
|
|
|
17
17
|
import { build } from "esbuild";
|
|
18
18
|
import { childPids, reap } from "./children.js";
|
|
19
19
|
import { runtimeDir } from "./package.js";
|
|
20
|
+
import { withStartTimeout } from "./target-api.js";
|
|
20
21
|
import { cellMeta, WORKER_MODULES } from "./workerd-target.js";
|
|
21
22
|
export const WRANGLER_CONFIGS = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
|
|
22
23
|
/** The nearest wrangler config at or above `cwd`, or null. */
|
|
@@ -157,7 +158,7 @@ class MiniflareHost {
|
|
|
157
158
|
}
|
|
158
159
|
}
|
|
159
160
|
/** Starts the driver on the project's Miniflare with its bindings. */
|
|
160
|
-
export async function startMiniflare(configPath, cwd) {
|
|
161
|
+
export async function startMiniflare(configPath, cwd, startTimeoutMs) {
|
|
161
162
|
const { wrangler, miniflare, version } = loadTooling(configPath);
|
|
162
163
|
const projectDir = dirname(configPath);
|
|
163
164
|
const { workerOptions, main, define } = wrangler.unstable_getMiniflareWorkerOptions(configPath);
|
|
@@ -190,10 +191,10 @@ export async function startMiniflare(configPath, cwd) {
|
|
|
190
191
|
const before = new Set(childPids(process.pid, "workerd"));
|
|
191
192
|
const mf = new miniflare.Miniflare(v5 && miniflare.convertV4MiniflareOptions ? miniflare.convertV4MiniflareOptions(options) : options);
|
|
192
193
|
try {
|
|
193
|
-
await mf.ready;
|
|
194
194
|
// Miniflare listens on a loopback TCP port; the token is what keeps
|
|
195
195
|
// other local processes from the driver.
|
|
196
|
-
const
|
|
196
|
+
const connect = mf.ready.then(() => mf.dispatchFetch("http://cdpk/", { headers: { Upgrade: "websocket", "x-cdpk-token": token } }));
|
|
197
|
+
const res = await withStartTimeout(connect, "Miniflare", startTimeoutMs);
|
|
197
198
|
if (res.status !== 101 || res.webSocket === null)
|
|
198
199
|
throw new Error(`the driver answered ${res.status}: ${await res.text()}`);
|
|
199
200
|
const workerd = childPids(process.pid, "workerd").filter((pid) => !before.has(pid));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tlockney/cdp-kernel",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
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>",
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"docs:dev": "vitepress dev docs",
|
|
60
60
|
"docs:build": "vitepress build docs",
|
|
61
61
|
"docs:notebooks": "node docs/.vitepress/run-notebooks.ts",
|
|
62
|
+
"docs:debugger": "uv run --with jupyterlab --with playwright --with nbformat python test/debugger_screenshot.py",
|
|
62
63
|
"docs:widgets": "uv run --with jupyterlab --with anywidget --with playwright --with nbformat python test/widget_screenshots.py",
|
|
63
64
|
"install:node": "node src/main.ts install --runtime node",
|
|
64
65
|
"install:deno": "node src/main.ts install --runtime deno",
|
|
@@ -81,6 +82,7 @@
|
|
|
81
82
|
},
|
|
82
83
|
"devDependencies": {
|
|
83
84
|
"@types/node": "^24.19.2",
|
|
85
|
+
"medium-zoom": "^1.1.0",
|
|
84
86
|
"vitepress": "^1.6.4",
|
|
85
87
|
"workerd": "1.20261015.1",
|
|
86
88
|
"wrangler": "4.151.0"
|
package/runtime/bootstrap.mjs
CHANGED
|
@@ -215,6 +215,8 @@ globalThis.__cdpk = {
|
|
|
215
215
|
inspectPath: (path, detail) => inspectPath(path, { inspect: inspectPlain, detail }),
|
|
216
216
|
resolvePath,
|
|
217
217
|
broadcastResult: jupyter.broadcastResult,
|
|
218
|
+
// execute_request's user_expressions (#340).
|
|
219
|
+
formatJson: async (value) => JSON.stringify(await jupyter.namespace.format(value)),
|
|
218
220
|
// The debugger's rich variable viewer (#18), synchronously: it runs while
|
|
219
221
|
// the target is paused. A value whose $display is async shows as text.
|
|
220
222
|
richBundle(value) {
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
//
|
|
9
9
|
// A Worker has no module resolution beyond URLs, so `npm:` and `jsr:`
|
|
10
10
|
// specifiers, and bare package names, load from a CDN (esm.sh by default).
|
|
11
|
+
//
|
|
12
|
+
// The page can also offer cells its own features (#384): `host` requests go
|
|
13
|
+
// to the page's handler, and `preload` modules run before the first cell.
|
|
11
14
|
|
|
12
15
|
import { createJupyter } from "./display.mjs";
|
|
13
16
|
import { createHandler } from "./evaluator.mjs";
|
|
@@ -68,6 +71,82 @@ const jupyter = createJupyter({
|
|
|
68
71
|
let cdn = DEFAULT_CDN;
|
|
69
72
|
|
|
70
73
|
|
|
74
|
+
// --- The host page (#384) -----------------------------------------------------
|
|
75
|
+
// Requests to the page's `host` handler (src/browser/target.ts), and events
|
|
76
|
+
// it sends. Preload modules get this object; cells only through them.
|
|
77
|
+
const hostCalls = new Map();
|
|
78
|
+
const hostListeners = new Set();
|
|
79
|
+
let nextHostId = 0;
|
|
80
|
+
|
|
81
|
+
function hostError(error) {
|
|
82
|
+
const err = new Error(String(error?.message ?? "the host request failed"));
|
|
83
|
+
if (typeof error?.name === "string") err.name = error.name;
|
|
84
|
+
if (error?.code !== undefined) err.code = error.code;
|
|
85
|
+
return err;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const host = {
|
|
89
|
+
/** Resolves with the page's answer; aborting `signal` cancels the call on the page too. */
|
|
90
|
+
request(method, params, { signal, transfer } = {}) {
|
|
91
|
+
if (signal?.aborted) return Promise.reject(signal.reason);
|
|
92
|
+
const id = ++nextHostId;
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const onAbort = () => {
|
|
95
|
+
hostCalls.delete(id);
|
|
96
|
+
post({ kind: "host", id, cancel: true });
|
|
97
|
+
reject(signal.reason);
|
|
98
|
+
};
|
|
99
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
100
|
+
const done = () => signal?.removeEventListener("abort", onAbort);
|
|
101
|
+
try {
|
|
102
|
+
globalThis.postMessage({ kind: "host", id, method, params }, transfer ?? []);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
done(); // params structured clone can't copy
|
|
105
|
+
throw err;
|
|
106
|
+
}
|
|
107
|
+
hostCalls.set(id, { resolve, reject, done });
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
/** Calls `listener(event, value)` for each event the page sends; returns a function that stops it. */
|
|
111
|
+
onEvent(listener) {
|
|
112
|
+
hostListeners.add(listener);
|
|
113
|
+
return () => hostListeners.delete(listener);
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
function receiveHost(msg) {
|
|
118
|
+
if (msg.id === undefined) {
|
|
119
|
+
for (const listener of hostListeners) {
|
|
120
|
+
try {
|
|
121
|
+
listener(msg.event, msg.value);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
reportUncaught("Uncaught", err);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const call = hostCalls.get(msg.id);
|
|
129
|
+
if (!call) return; // cancelled
|
|
130
|
+
hostCalls.delete(msg.id);
|
|
131
|
+
call.done();
|
|
132
|
+
if (msg.error !== undefined) call.reject(hostError(msg.error));
|
|
133
|
+
else call.resolve(msg.result);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Each module is imported in order, and a default export that is a function
|
|
137
|
+
// is called with the host. A module that fails is reported, and the kernel
|
|
138
|
+
// runs without it.
|
|
139
|
+
async function preload(urls, base) {
|
|
140
|
+
for (const specifier of Array.isArray(urls) ? urls : []) {
|
|
141
|
+
try {
|
|
142
|
+
const mod = await import(new URL(specifier, base).href);
|
|
143
|
+
if (typeof mod.default === "function") await mod.default({ host, jupyter: jupyter.namespace });
|
|
144
|
+
} catch (err) {
|
|
145
|
+
reportUncaught(`Preloading ${specifier} failed`, err);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
71
150
|
// --- The shim cells and the evaluator use -----------------------------------
|
|
72
151
|
let handle = null;
|
|
73
152
|
|
|
@@ -83,6 +162,8 @@ function install({ base, cdn: cdnBase }) {
|
|
|
83
162
|
resolvePath,
|
|
84
163
|
barrier: () => 0,
|
|
85
164
|
broadcastResult: jupyter.broadcastResult,
|
|
165
|
+
// execute_request's user_expressions (#340).
|
|
166
|
+
formatJson: async (value) => JSON.stringify(await jupyter.namespace.format(value)),
|
|
86
167
|
async commDispatch(msgType, content, metadata, buffers64) {
|
|
87
168
|
const buffers = buffers64.map((b) => Uint8Array.from(atob(b), (c) => c.charCodeAt(0)));
|
|
88
169
|
return await jupyter.commDispatch(msgType, content, metadata, buffers);
|
|
@@ -104,6 +185,7 @@ function install({ base, cdn: cdnBase }) {
|
|
|
104
185
|
async function answer(req) {
|
|
105
186
|
if (req.op === "info") {
|
|
106
187
|
install(req);
|
|
188
|
+
await preload(req.preload, req.base);
|
|
107
189
|
return { userAgent: globalThis.navigator?.userAgent ?? "" };
|
|
108
190
|
}
|
|
109
191
|
if (handle === null) throw new Error("the driver wasn't sent info first");
|
|
@@ -112,6 +194,7 @@ async function answer(req) {
|
|
|
112
194
|
|
|
113
195
|
globalThis.addEventListener("message", (event) => {
|
|
114
196
|
const req = event.data;
|
|
197
|
+
if (req?.op === "host") return receiveHost(req);
|
|
115
198
|
answer(req).then(
|
|
116
199
|
(reply) => post({ kind: "reply", id: req.id, ...reply }),
|
|
117
200
|
(err) => post({ kind: "reply", id: req.id, failure: String(err?.stack ?? err) }),
|
package/runtime/evaluator.mjs
CHANGED
|
@@ -56,6 +56,8 @@ export function createHandler(shim, evaluate = (code) => (0, eval)(code)) {
|
|
|
56
56
|
case "broadcast":
|
|
57
57
|
await shim.broadcastResult(request.count, take(request.handle));
|
|
58
58
|
return {};
|
|
59
|
+
case "format":
|
|
60
|
+
return { value: await shim.formatJson(request.handle === undefined ? undefined : take(request.handle)) };
|
|
59
61
|
case "describe":
|
|
60
62
|
return { value: shim.describeError(take(request.handle)) };
|
|
61
63
|
case "comm":
|
|
@@ -141,6 +141,8 @@ function install(env) {
|
|
|
141
141
|
resolvePath,
|
|
142
142
|
barrier: () => 0,
|
|
143
143
|
broadcastResult: jupyter.broadcastResult,
|
|
144
|
+
// execute_request's user_expressions (#340).
|
|
145
|
+
formatJson: async (value) => JSON.stringify(await jupyter.namespace.format(value)),
|
|
144
146
|
async commDispatch(msgType, content, metadata, buffers64) {
|
|
145
147
|
const buffers = buffers64.map((b) => Uint8Array.from(atob(b), (c) => c.charCodeAt(0)));
|
|
146
148
|
return await jupyter.commDispatch(msgType, content, metadata, buffers);
|