@tlockney/cdp-kernel 0.1.1 → 0.3.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 +274 -21
- package/dist/bun-target.d.ts +58 -0
- package/dist/bun-target.js +231 -0
- package/dist/celld-target.d.ts +39 -0
- package/dist/celld-target.js +274 -0
- package/dist/deno-config.d.ts +20 -0
- package/dist/deno-config.js +130 -0
- package/dist/globals-deno-compat.d.ts +21 -0
- package/dist/globals-deno-compat.js +10 -0
- package/dist/globals.d.ts +36 -0
- package/dist/globals.js +15 -0
- package/dist/kernel.d.ts +2 -1
- package/dist/kernel.js +126 -11
- package/dist/main.js +5 -5
- package/dist/package.d.ts +13 -5
- package/dist/package.js +33 -8
- package/dist/target.d.ts +78 -7
- package/dist/target.js +67 -12
- package/dist/transform.d.ts +25 -1
- package/dist/transform.js +150 -11
- package/dist/workerd-target.d.ts +123 -0
- package/dist/workerd-target.js +450 -0
- package/dist/workers-project.d.ts +27 -0
- package/dist/workers-project.js +195 -0
- package/dist/ws-client.d.ts +32 -0
- package/dist/ws-client.js +166 -0
- package/package.json +27 -4
- package/runtime/bootstrap.mjs +78 -7
- package/runtime/bun-channel.d.mts +3 -0
- package/runtime/bun-channel.mjs +58 -0
- package/runtime/comm.d.mts +46 -0
- package/runtime/comm.mjs +108 -0
- package/runtime/deno-compat.mjs +1 -0
- package/runtime/display.d.mts +11 -0
- package/runtime/display.mjs +11 -1
- package/runtime/evaluator.d.mts +18 -0
- package/runtime/evaluator.mjs +83 -0
- package/runtime/node-hooks.d.mts +11 -0
- package/runtime/node-hooks.mjs +24 -12
- package/runtime/node-modules-resolve.d.mts +8 -0
- package/runtime/node-modules-resolve.mjs +106 -0
- package/runtime/widget.d.mts +40 -0
- package/runtime/widget.mjs +208 -0
- package/runtime/workerd-worker.mjs +189 -0
package/dist/kernel.js
CHANGED
|
@@ -10,8 +10,13 @@ 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 {
|
|
13
|
+
import { BunTarget } from "./bun-target.js";
|
|
14
|
+
import { WorkerdTarget } from "./workerd-target.js";
|
|
15
|
+
import { launchCelld } from "./celld-target.js";
|
|
16
|
+
import { CellSyntaxError, objectLiteralCandidate } from "./transform.js";
|
|
17
|
+
import { loadDenoProject } from "./deno-config.js";
|
|
14
18
|
import { checkIsComplete, codePointToUtf16Index, exprAtCursor, utf16ToCodePointIndex } from "./text.js";
|
|
19
|
+
import { VERSION } from "./package.js";
|
|
15
20
|
export function parseConnectionInfo(json) {
|
|
16
21
|
const raw = JSON.parse(json);
|
|
17
22
|
if (typeof raw !== "object" || raw === null)
|
|
@@ -43,6 +48,10 @@ export class Kernel {
|
|
|
43
48
|
#conn;
|
|
44
49
|
#runtime;
|
|
45
50
|
#denoCompat;
|
|
51
|
+
// A Deno target in a Deno project uses its config (#13).
|
|
52
|
+
#project;
|
|
53
|
+
#projectWarning = null;
|
|
54
|
+
#noticeShownFor = null;
|
|
46
55
|
#session = randomUUID();
|
|
47
56
|
#shell;
|
|
48
57
|
#control;
|
|
@@ -54,11 +63,14 @@ export class Kernel {
|
|
|
54
63
|
#parent = {};
|
|
55
64
|
#allowStdin = false;
|
|
56
65
|
#executing = false;
|
|
66
|
+
// Open comms, for comm_info_request: comm_id -> target_name (#9).
|
|
67
|
+
#comms = new Map();
|
|
57
68
|
#shuttingDown = false;
|
|
58
69
|
constructor(conn, runtime, options = {}) {
|
|
59
70
|
this.#conn = conn;
|
|
60
71
|
this.#runtime = runtime;
|
|
61
72
|
this.#denoCompat = options.denoCompat ?? false;
|
|
73
|
+
this.#project = runtime === "deno" ? this.#loadProject(options.notebookDir ?? process.cwd()) : null;
|
|
62
74
|
startHeartbeat(conn.hb_port, conn.ip);
|
|
63
75
|
this.#shell = new RouterSocket(conn.shell_port, conn.ip);
|
|
64
76
|
this.#control = new RouterSocket(conn.control_port, conn.ip);
|
|
@@ -75,11 +87,101 @@ export class Kernel {
|
|
|
75
87
|
#log(text) {
|
|
76
88
|
process.stderr.write(`[cdp-kernel] ${text}\n`);
|
|
77
89
|
}
|
|
90
|
+
#loadProject(notebookDir) {
|
|
91
|
+
try {
|
|
92
|
+
return loadDenoProject(notebookDir);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
// A broken deno.json mustn't stop the kernel: run as in a plain
|
|
96
|
+
// directory and say why in the first cell.
|
|
97
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
98
|
+
this.#projectWarning = `${reason}; running without the project config`;
|
|
99
|
+
this.#log(this.#projectWarning);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
#transformOptions() {
|
|
104
|
+
return this.#project?.transform ?? {};
|
|
105
|
+
}
|
|
106
|
+
// --- comms (#9) ------------------------------------------------------------
|
|
107
|
+
#trackComm(msgType, content) {
|
|
108
|
+
const commId = typeof content.comm_id === "string" ? content.comm_id : null;
|
|
109
|
+
if (commId === null)
|
|
110
|
+
return;
|
|
111
|
+
if (msgType === "comm_open" && typeof content.target_name === "string")
|
|
112
|
+
this.#comms.set(commId, content.target_name);
|
|
113
|
+
else if (msgType === "comm_close")
|
|
114
|
+
this.#comms.delete(commId);
|
|
115
|
+
}
|
|
116
|
+
#commInfo(content) {
|
|
117
|
+
const filter = typeof content.target_name === "string" ? content.target_name : null;
|
|
118
|
+
const comms = {};
|
|
119
|
+
for (const [commId, targetName] of this.#comms) {
|
|
120
|
+
if (filter === null || filter === targetName)
|
|
121
|
+
comms[commId] = { target_name: targetName };
|
|
122
|
+
}
|
|
123
|
+
return comms;
|
|
124
|
+
}
|
|
125
|
+
/** A frontend comm message. The busy/idle status around it is the caller's. */
|
|
126
|
+
async #handleComm(msg) {
|
|
127
|
+
const type = msg.header.msg_type;
|
|
128
|
+
const commId = String(msg.content.comm_id ?? "");
|
|
129
|
+
const target = await this.#ensureTarget();
|
|
130
|
+
// Kernel messages the handlers send are replies to this message.
|
|
131
|
+
this.#parent = msg.header;
|
|
132
|
+
this.#allowStdin = false;
|
|
133
|
+
this.#executing = true;
|
|
134
|
+
let handled = false;
|
|
135
|
+
try {
|
|
136
|
+
const result = await target.dispatchComm(type, msg.content, msg.metadata, msg.buffers);
|
|
137
|
+
if ("error" in result) {
|
|
138
|
+
const { name, message, stack } = result.error;
|
|
139
|
+
this.#publish("stream", { name: "stderr", text: `Error in ${type} handler: ${stack || `${name}: ${message}`}\n` });
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
handled = result.handled;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
if (!(err instanceof Interrupted))
|
|
147
|
+
throw err;
|
|
148
|
+
this.#publish("stream", { name: "stderr", text: `${type} handler interrupted\n` });
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
this.#executing = false;
|
|
152
|
+
}
|
|
153
|
+
await target.flush().catch(() => { });
|
|
154
|
+
if (type === "comm_open") {
|
|
155
|
+
if (handled) {
|
|
156
|
+
this.#comms.set(commId, String(msg.content.target_name ?? ""));
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
// An unknown target, or a handler that failed: the protocol asks the
|
|
160
|
+
// kernel to close the comm so the frontend doesn't wait on it.
|
|
161
|
+
this.#publish("comm_close", { comm_id: commId, data: {} });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else if (type === "comm_close") {
|
|
165
|
+
this.#comms.delete(commId);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
78
168
|
// --- target lifecycle -----------------------------------------------------
|
|
169
|
+
#launchTarget() {
|
|
170
|
+
if (this.#runtime === "bun")
|
|
171
|
+
return BunTarget.launch(this.#targetEvents());
|
|
172
|
+
if (this.#runtime === "workerd")
|
|
173
|
+
return WorkerdTarget.launch(this.#targetEvents());
|
|
174
|
+
if (this.#runtime === "celld")
|
|
175
|
+
return launchCelld(this.#targetEvents());
|
|
176
|
+
return Target.launch(this.#runtime, this.#targetEvents(), {
|
|
177
|
+
denoCompat: this.#denoCompat,
|
|
178
|
+
denoConfig: this.#project?.configPath,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
79
181
|
#ensureTarget() {
|
|
80
182
|
if (this.#target)
|
|
81
183
|
return Promise.resolve(this.#target);
|
|
82
|
-
this.#targetStarting ??=
|
|
184
|
+
this.#targetStarting ??= this.#launchTarget().then((target) => {
|
|
83
185
|
this.#target = target;
|
|
84
186
|
this.#targetStarting = null;
|
|
85
187
|
return target;
|
|
@@ -93,16 +195,18 @@ export class Kernel {
|
|
|
93
195
|
return {
|
|
94
196
|
stream: (name, text) => this.#publish("stream", { name, text }),
|
|
95
197
|
message: (msg) => void this.#onTargetMessage(msg),
|
|
96
|
-
exit: (code) => {
|
|
198
|
+
exit: (code, note) => {
|
|
97
199
|
this.#target = null;
|
|
98
200
|
if (!this.#shuttingDown) {
|
|
99
|
-
|
|
201
|
+
const text = note ?? `[cdp-kernel] ${this.#runtime} exited (${code}); restarting on next cell`;
|
|
202
|
+
this.#publish("stream", { name: "stderr", text: `\n${text}\n` });
|
|
100
203
|
}
|
|
101
204
|
},
|
|
102
205
|
};
|
|
103
206
|
}
|
|
104
207
|
async #onTargetMessage(msg) {
|
|
105
208
|
if (msg.kind === "iopub") {
|
|
209
|
+
this.#trackComm(msg.msg_type, msg.content);
|
|
106
210
|
this.#publish(msg.msg_type, msg.content, {
|
|
107
211
|
metadata: msg.metadata,
|
|
108
212
|
buffers: msg.buffers.map((b) => new Uint8Array(Buffer.from(b, "base64"))),
|
|
@@ -156,7 +260,7 @@ export class Kernel {
|
|
|
156
260
|
status: "ok",
|
|
157
261
|
protocol_version: PROTOCOL_VERSION,
|
|
158
262
|
implementation: "cdp-kernel",
|
|
159
|
-
implementation_version:
|
|
263
|
+
implementation_version: VERSION,
|
|
160
264
|
language_info: {
|
|
161
265
|
name: "typescript",
|
|
162
266
|
version: "5",
|
|
@@ -166,7 +270,7 @@ export class Kernel {
|
|
|
166
270
|
codemirror_mode: "typescript",
|
|
167
271
|
nbconvert_exporter: "script",
|
|
168
272
|
},
|
|
169
|
-
banner: `cdp-kernel on ${this.#runtime} ${versions?.runtime ?? ""} (
|
|
273
|
+
banner: `cdp-kernel on ${this.#runtime} ${versions?.runtime ?? ""} (${versions?.engine || "?"})`,
|
|
170
274
|
help_links: [],
|
|
171
275
|
debugger: false,
|
|
172
276
|
};
|
|
@@ -218,11 +322,12 @@ export class Kernel {
|
|
|
218
322
|
this.#reply(this.#shell, peer, msg, "history_reply", { status: "ok", history: [] });
|
|
219
323
|
break;
|
|
220
324
|
case "comm_info_request":
|
|
221
|
-
this.#reply(this.#shell, peer, msg, "comm_info_reply", { status: "ok", comms:
|
|
325
|
+
this.#reply(this.#shell, peer, msg, "comm_info_reply", { status: "ok", comms: this.#commInfo(msg.content) });
|
|
222
326
|
break;
|
|
223
327
|
case "comm_open":
|
|
224
|
-
|
|
225
|
-
|
|
328
|
+
case "comm_msg":
|
|
329
|
+
case "comm_close":
|
|
330
|
+
await this.#handleComm(msg);
|
|
226
331
|
break;
|
|
227
332
|
}
|
|
228
333
|
}
|
|
@@ -285,7 +390,7 @@ export class Kernel {
|
|
|
285
390
|
const wrapped = objectLiteralCandidate(code);
|
|
286
391
|
if (wrapped !== null) {
|
|
287
392
|
try {
|
|
288
|
-
const result = await target.evaluate(
|
|
393
|
+
const result = await target.evaluate(target.transform(wrapped, this.#transformOptions()));
|
|
289
394
|
if (!result.exceptionDetails)
|
|
290
395
|
return result;
|
|
291
396
|
}
|
|
@@ -294,7 +399,7 @@ export class Kernel {
|
|
|
294
399
|
throw err;
|
|
295
400
|
}
|
|
296
401
|
}
|
|
297
|
-
return target.evaluate(
|
|
402
|
+
return target.evaluate(target.transform(code, this.#transformOptions()));
|
|
298
403
|
}
|
|
299
404
|
async #execute(msg, peer) {
|
|
300
405
|
const parent = msg.header;
|
|
@@ -307,10 +412,20 @@ export class Kernel {
|
|
|
307
412
|
this.#allowStdin = msg.content.allow_stdin === true;
|
|
308
413
|
this.#status("busy", parent);
|
|
309
414
|
this.#publish("execute_input", { code, execution_count: count });
|
|
415
|
+
if (this.#projectWarning !== null) {
|
|
416
|
+
// Shown once, in the first cell, where the user will see it.
|
|
417
|
+
this.#publish("stream", { name: "stderr", text: `[cdp-kernel] ${this.#projectWarning}\n` });
|
|
418
|
+
this.#projectWarning = null;
|
|
419
|
+
}
|
|
310
420
|
let error = null;
|
|
311
421
|
this.#executing = true;
|
|
312
422
|
try {
|
|
313
423
|
const target = await this.#ensureTarget();
|
|
424
|
+
if (target.notice !== undefined && this.#noticeShownFor !== target) {
|
|
425
|
+
// Once per target, in the first cell it runs.
|
|
426
|
+
this.#noticeShownFor = target;
|
|
427
|
+
this.#publish("stream", { name: "stderr", text: `${target.notice}\n` });
|
|
428
|
+
}
|
|
314
429
|
const result = await this.#evaluateCell(target, code);
|
|
315
430
|
if (result.exceptionDetails) {
|
|
316
431
|
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)
|
|
@@ -59,7 +59,7 @@ const { positionals, values } = parseArgs({
|
|
|
59
59
|
});
|
|
60
60
|
const runtime = values.runtime;
|
|
61
61
|
if (!isRuntime(runtime)) {
|
|
62
|
-
console.error(`unknown runtime ${runtime}; expected node or
|
|
62
|
+
console.error(`unknown runtime ${runtime}; expected node, deno, bun, workerd or celld`);
|
|
63
63
|
process.exit(2);
|
|
64
64
|
}
|
|
65
65
|
// Deno targets have the real namespace; the shim is for Node.
|
|
@@ -83,6 +83,6 @@ switch (positionals[0]) {
|
|
|
83
83
|
console.log(VERSION);
|
|
84
84
|
break;
|
|
85
85
|
default:
|
|
86
|
-
console.error("usage: cdp-kernel install [--runtime node|deno] [--deno-compat] | kernel --runtime node|deno --conn FILE | version");
|
|
86
|
+
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
87
|
process.exit(2);
|
|
88
88
|
}
|
package/dist/package.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Kept equal to package.json and deno.jsonc by test/package.test.ts. */
|
|
2
|
-
export declare const VERSION = "0.
|
|
3
|
-
export declare const RUNTIME_FILES: readonly ["bootstrap.mjs", "display.mjs", "node-hooks.mjs", "deno-compat.mjs"];
|
|
2
|
+
export declare const VERSION = "0.3.0";
|
|
3
|
+
export declare const RUNTIME_FILES: readonly ["bootstrap.mjs", "bun-channel.mjs", "evaluator.mjs", "workerd-worker.mjs", "display.mjs", "comm.mjs", "widget.mjs", "node-hooks.mjs", "deno-compat.mjs", "node-modules-resolve.mjs"];
|
|
4
4
|
export type HostRuntime = "node" | "deno";
|
|
5
5
|
export declare function hostRuntime(): HostRuntime;
|
|
6
6
|
/** Where the CLI module was loaded from. */
|
|
@@ -20,10 +20,18 @@ export declare function moduleOrigin(moduleUrl: string): Origin;
|
|
|
20
20
|
export declare function kernelArgv(host: HostRuntime, execPath: string, origin: Origin, args: string[]): string[];
|
|
21
21
|
export declare function cacheRoot(): string;
|
|
22
22
|
export type FetchText = (url: string) => Promise<string>;
|
|
23
|
+
/** True when a file: URL points inside a node_modules directory (an npm install). */
|
|
24
|
+
export declare function isInsideNodeModules(url: string): boolean;
|
|
23
25
|
/**
|
|
24
26
|
* A local directory holding runtime/*.mjs. `baseUrl` is the runtime/
|
|
25
27
|
* directory's URL relative to the calling module. A file: URL is used in
|
|
26
|
-
* place; anything else is copied once into a cache
|
|
27
|
-
* URL.
|
|
28
|
+
* place unless `copy` is set; anything else is copied once into a cache
|
|
29
|
+
* keyed by version and URL.
|
|
30
|
+
*
|
|
31
|
+
* `copy` exists for Deno targets of an npm install: Deno resolves imports
|
|
32
|
+
* made by a module under node_modules as npm-package imports, without the
|
|
33
|
+
* notebook project's import map, so the bootstrap must load from outside it.
|
|
28
34
|
*/
|
|
29
|
-
export declare function runtimeDir(baseUrl: string, cache?: string, get?: FetchText
|
|
35
|
+
export declare function runtimeDir(baseUrl: string, cache?: string, get?: FetchText, { copy }?: {
|
|
36
|
+
copy?: boolean;
|
|
37
|
+
}): Promise<string>;
|
package/dist/package.js
CHANGED
|
@@ -8,14 +8,25 @@
|
|
|
8
8
|
// import its bootstrap from an https URL, so a JSR install copies those
|
|
9
9
|
// files into a local cache first.
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
|
-
import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
|
-
import { join } from "node:path";
|
|
13
|
+
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.
|
|
18
|
-
export const RUNTIME_FILES = [
|
|
17
|
+
export const VERSION = "0.3.0";
|
|
18
|
+
export const RUNTIME_FILES = [
|
|
19
|
+
"bootstrap.mjs",
|
|
20
|
+
"bun-channel.mjs",
|
|
21
|
+
"evaluator.mjs",
|
|
22
|
+
"workerd-worker.mjs",
|
|
23
|
+
"display.mjs",
|
|
24
|
+
"comm.mjs",
|
|
25
|
+
"widget.mjs",
|
|
26
|
+
"node-hooks.mjs",
|
|
27
|
+
"deno-compat.mjs",
|
|
28
|
+
"node-modules-resolve.mjs",
|
|
29
|
+
];
|
|
19
30
|
export function hostRuntime() {
|
|
20
31
|
return process.versions.deno === undefined ? "node" : "deno";
|
|
21
32
|
}
|
|
@@ -57,15 +68,29 @@ async function fetchText(url) {
|
|
|
57
68
|
throw new Error(`fetching ${url}: ${res.status} ${res.statusText}`);
|
|
58
69
|
return await res.text();
|
|
59
70
|
}
|
|
71
|
+
/** True when a file: URL points inside a node_modules directory (an npm install). */
|
|
72
|
+
export function isInsideNodeModules(url) {
|
|
73
|
+
return url.startsWith("file:") && fileURLToPath(url).split(sep).includes("node_modules");
|
|
74
|
+
}
|
|
75
|
+
function readLocal(url) {
|
|
76
|
+
return Promise.resolve(readFileSync(fileURLToPath(url), "utf8"));
|
|
77
|
+
}
|
|
60
78
|
/**
|
|
61
79
|
* A local directory holding runtime/*.mjs. `baseUrl` is the runtime/
|
|
62
80
|
* directory's URL relative to the calling module. A file: URL is used in
|
|
63
|
-
* place; anything else is copied once into a cache
|
|
64
|
-
* URL.
|
|
81
|
+
* place unless `copy` is set; anything else is copied once into a cache
|
|
82
|
+
* keyed by version and URL.
|
|
83
|
+
*
|
|
84
|
+
* `copy` exists for Deno targets of an npm install: Deno resolves imports
|
|
85
|
+
* made by a module under node_modules as npm-package imports, without the
|
|
86
|
+
* notebook project's import map, so the bootstrap must load from outside it.
|
|
65
87
|
*/
|
|
66
|
-
export async function runtimeDir(baseUrl, cache = cacheRoot(), get = fetchText) {
|
|
67
|
-
|
|
88
|
+
export async function runtimeDir(baseUrl, cache = cacheRoot(), get = fetchText, { copy = false } = {}) {
|
|
89
|
+
const local = baseUrl.startsWith("file:");
|
|
90
|
+
if (local && !copy)
|
|
68
91
|
return fileURLToPath(baseUrl);
|
|
92
|
+
if (local)
|
|
93
|
+
get = readLocal;
|
|
69
94
|
const key = createHash("sha256").update(baseUrl).digest("hex").slice(0, 12);
|
|
70
95
|
const dir = join(cache, "runtime", `${VERSION}-${key}`);
|
|
71
96
|
if (RUNTIME_FILES.every((name) => existsSync(join(dir, name))))
|
package/dist/target.d.ts
CHANGED
|
@@ -1,7 +1,53 @@
|
|
|
1
1
|
import { type CdpParams, type EvaluateResult, type RemoteObject } from "./cdp.ts";
|
|
2
|
-
|
|
2
|
+
import { type CellTransformOptions, transformCell } from "./transform.ts";
|
|
3
|
+
/** Targets driven over CDP. */
|
|
4
|
+
export type CdpRuntime = "node" | "deno";
|
|
5
|
+
/** Bun and workerd have their own backends: src/bun-target.ts, src/workerd-target.ts. */
|
|
6
|
+
export type RuntimeName = CdpRuntime | "bun" | "workerd" | "celld";
|
|
7
|
+
/** What the kernel needs from a target runtime. */
|
|
8
|
+
export interface KernelTarget {
|
|
9
|
+
readonly runtime: RuntimeName;
|
|
10
|
+
readonly versions: {
|
|
11
|
+
runtime: string;
|
|
12
|
+
engine: string;
|
|
13
|
+
};
|
|
14
|
+
/** Shown once, in the first cell: e.g. which project's bindings loaded. */
|
|
15
|
+
readonly notice?: string;
|
|
16
|
+
/** Turns a cell into the script evaluate() takes. */
|
|
17
|
+
transform(code: string, options: CellTransformOptions): string;
|
|
18
|
+
/** Runs a transformed cell. Rejects with Interrupted after interrupt(). */
|
|
19
|
+
evaluate(script: string): Promise<EvaluateResult>;
|
|
20
|
+
interrupt(): Promise<void>;
|
|
21
|
+
/** Resolves once all output and messages sent so far have been delivered. */
|
|
22
|
+
flush(): Promise<void>;
|
|
23
|
+
broadcastResult(executionCount: number, result: RemoteObject): Promise<void>;
|
|
24
|
+
dispatchComm(msgType: string, content: CdpParams, metadata: CdpParams, buffers: Uint8Array[]): Promise<{
|
|
25
|
+
handled: boolean;
|
|
26
|
+
} | {
|
|
27
|
+
error: {
|
|
28
|
+
name: string;
|
|
29
|
+
message: string;
|
|
30
|
+
stack: string;
|
|
31
|
+
};
|
|
32
|
+
}>;
|
|
33
|
+
describeError(exception: RemoteObject): Promise<{
|
|
34
|
+
name: string;
|
|
35
|
+
message: string;
|
|
36
|
+
stack: string;
|
|
37
|
+
}>;
|
|
38
|
+
propertyNames(expression: string): Promise<string[]>;
|
|
39
|
+
lexicalNames(): Promise<string[]>;
|
|
40
|
+
replyInput(value: string | null): void;
|
|
41
|
+
kill(): void;
|
|
42
|
+
}
|
|
43
|
+
export interface LaunchOptions {
|
|
44
|
+
/** Runtime binary; defaults to `deno`, or `node` (this one when hosted by Node). */
|
|
45
|
+
executable?: string;
|
|
46
|
+
/** A Deno target's project config (deno.json), passed as --config. */
|
|
47
|
+
denoConfig?: string;
|
|
48
|
+
}
|
|
3
49
|
/** `bootstrap` is the local path of runtime/bootstrap.mjs (see runtimeDir). */
|
|
4
|
-
export declare function launchCommand(runtime:
|
|
50
|
+
export declare function launchCommand(runtime: CdpRuntime, bootstrap: string, { executable, denoConfig }?: LaunchOptions): {
|
|
5
51
|
cmd: string;
|
|
6
52
|
args: string[];
|
|
7
53
|
};
|
|
@@ -19,6 +65,7 @@ export type TargetMessage = {
|
|
|
19
65
|
password: boolean;
|
|
20
66
|
barrier: number;
|
|
21
67
|
};
|
|
68
|
+
export declare function isTargetMessage(value: unknown): value is TargetMessage;
|
|
22
69
|
/**
|
|
23
70
|
* Splits one output pipe into text and barrier markers, and releases them
|
|
24
71
|
* in order. A gated marker (one that announces a binding message) pauses
|
|
@@ -36,26 +83,36 @@ export declare class PipeDemux {
|
|
|
36
83
|
/** Release everything (the process died). */
|
|
37
84
|
close(): void;
|
|
38
85
|
}
|
|
86
|
+
/** Promises keyed by barrier number, resolvable before or after first use. */
|
|
87
|
+
export declare class Gates {
|
|
88
|
+
#private;
|
|
89
|
+
wait(n: number): Promise<void>;
|
|
90
|
+
open(n: number): void;
|
|
91
|
+
openAll(): void;
|
|
92
|
+
}
|
|
39
93
|
export interface TargetEvents {
|
|
40
94
|
stream(name: "stdout" | "stderr", text: string): void;
|
|
41
95
|
message(msg: TargetMessage): void;
|
|
42
|
-
|
|
96
|
+
/** `note` says why, when the target was stopped on purpose. */
|
|
97
|
+
exit(code: number | null, note?: string): void;
|
|
43
98
|
}
|
|
44
99
|
export declare class Interrupted extends Error {
|
|
45
100
|
name: string;
|
|
46
101
|
}
|
|
47
|
-
export declare class Target {
|
|
102
|
+
export declare class Target implements KernelTarget {
|
|
48
103
|
#private;
|
|
49
|
-
readonly runtime:
|
|
104
|
+
readonly runtime: CdpRuntime;
|
|
105
|
+
readonly transform: typeof transformCell;
|
|
50
106
|
readonly versions: {
|
|
51
107
|
runtime: string;
|
|
52
|
-
|
|
108
|
+
engine: string;
|
|
53
109
|
};
|
|
54
110
|
private constructor();
|
|
55
|
-
static launch(runtime:
|
|
111
|
+
static launch(runtime: CdpRuntime, events: TargetEvents, options?: {
|
|
56
112
|
cwd?: string;
|
|
57
113
|
executable?: string;
|
|
58
114
|
denoCompat?: boolean;
|
|
115
|
+
denoConfig?: string;
|
|
59
116
|
}): Promise<Target>;
|
|
60
117
|
/** Resolves once all output and messages sent so far have been delivered. */
|
|
61
118
|
flush(): Promise<void>;
|
|
@@ -72,6 +129,20 @@ export declare class Target {
|
|
|
72
129
|
callFunctionOn(functionDeclaration: string, args: CdpParams[]): Promise<CdpParams>;
|
|
73
130
|
/** The broadcastResult step of Deno's execute flow (jupyter/mod.rs). */
|
|
74
131
|
broadcastResult(executionCount: number, result: RemoteObject): Promise<void>;
|
|
132
|
+
/**
|
|
133
|
+
* Hands a frontend comm message to the target's comms. Resolves to
|
|
134
|
+
* whether a comm or registered target took it, or to the error a handler
|
|
135
|
+
* threw. Rejects with Interrupted after interrupt().
|
|
136
|
+
*/
|
|
137
|
+
dispatchComm(msgType: string, content: CdpParams, metadata: CdpParams, buffers: Uint8Array[]): Promise<{
|
|
138
|
+
handled: boolean;
|
|
139
|
+
} | {
|
|
140
|
+
error: {
|
|
141
|
+
name: string;
|
|
142
|
+
message: string;
|
|
143
|
+
stack: string;
|
|
144
|
+
};
|
|
145
|
+
}>;
|
|
75
146
|
describeError(exception: RemoteObject): Promise<{
|
|
76
147
|
name: string;
|
|
77
148
|
message: string;
|
package/dist/target.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// A target runtime driven over CDP: the stand-in for Deno's REPL isolate and
|
|
2
2
|
// the ops in cli/ops/jupyter.rs. Any runtime whose --inspect speaks CDP and
|
|
3
3
|
// can load runtime/bootstrap.mjs works; Node and Deno are wired up here.
|
|
4
|
+
import { Buffer } from "node:buffer";
|
|
4
5
|
import process from "node:process";
|
|
5
6
|
import { spawn } from "node:child_process";
|
|
6
7
|
import { join } from "node:path";
|
|
7
8
|
import { asEvaluateResult, CdpAbandoned, CdpSession } from "./cdp.js";
|
|
8
|
-
import { SHIM } from "./transform.js";
|
|
9
|
-
import { hostRuntime, runtimeDir } from "./package.js";
|
|
9
|
+
import { SHIM, transformCell } from "./transform.js";
|
|
10
|
+
import { hostRuntime, isInsideNodeModules, runtimeDir } from "./package.js";
|
|
10
11
|
const BINDING = "__cdpk_send";
|
|
11
12
|
const MARKER_RE = /\x1b\[cdpk:(\d+)(m?)\]\x1b/;
|
|
12
13
|
const MAX_MARKER_LEN = 24;
|
|
@@ -15,8 +16,12 @@ const MAX_MARKER_LEN = 24;
|
|
|
15
16
|
// sends to the server log rather than the notebook).
|
|
16
17
|
const INSPECTOR_CHATTER = /^(?:\x1b\[[0-9;]*m)*(Debugger listening on |Debugger attached\.|For help, see: |Visit chrome:\/\/inspect|Waiting for the debugger to disconnect|Download(?:\x1b\[[0-9;]*m)* https?:\/\/).*\n/gm;
|
|
17
18
|
/** `bootstrap` is the local path of runtime/bootstrap.mjs (see runtimeDir). */
|
|
18
|
-
export function launchCommand(runtime, bootstrap, executable) {
|
|
19
|
-
|
|
19
|
+
export function launchCommand(runtime, bootstrap, { executable, denoConfig } = {}) {
|
|
20
|
+
// The CDP WebSocket has no authentication, so its URL must stay private:
|
|
21
|
+
// by default the inspector lists it at /json/list for any local process
|
|
22
|
+
// to read. Publishing it only on stderr, which only this host reads,
|
|
23
|
+
// leaves a random id nobody else can learn (#22).
|
|
24
|
+
const inspect = ["--inspect=127.0.0.1:0", "--inspect-publish-uid=stderr"];
|
|
20
25
|
if (runtime === "deno") {
|
|
21
26
|
// Deno discovers config from the main module's location, which would
|
|
22
27
|
// put npm: imports in BYONM mode against this package's own
|
|
@@ -25,14 +30,18 @@ export function launchCommand(runtime, bootstrap, executable) {
|
|
|
25
30
|
// into this package.
|
|
26
31
|
return {
|
|
27
32
|
cmd: executable ?? "deno",
|
|
28
|
-
|
|
33
|
+
// In a project, its config (import map, nodeModulesDir, ...) applies
|
|
34
|
+
// instead; --no-lock still keeps the kernel out of the project's lock.
|
|
35
|
+
args: denoConfig === undefined
|
|
36
|
+
? ["run", "--allow-all", "--node-modules-dir=none", "--no-lock", ...inspect, bootstrap]
|
|
37
|
+
: ["run", "--allow-all", "--config", denoConfig, "--no-lock", ...inspect, bootstrap],
|
|
29
38
|
};
|
|
30
39
|
}
|
|
31
40
|
// Under a Deno host, process.execPath is deno; find node on PATH instead.
|
|
32
41
|
const node = hostRuntime() === "node" ? process.execPath : "node";
|
|
33
|
-
return { cmd: executable ?? node, args: [inspect, bootstrap] };
|
|
42
|
+
return { cmd: executable ?? node, args: [...inspect, bootstrap] };
|
|
34
43
|
}
|
|
35
|
-
function isTargetMessage(value) {
|
|
44
|
+
export function isTargetMessage(value) {
|
|
36
45
|
if (typeof value !== "object" || value === null)
|
|
37
46
|
return false;
|
|
38
47
|
const kind = value.kind;
|
|
@@ -131,7 +140,7 @@ export class PipeDemux {
|
|
|
131
140
|
}
|
|
132
141
|
}
|
|
133
142
|
/** Promises keyed by barrier number, resolvable before or after first use. */
|
|
134
|
-
class Gates {
|
|
143
|
+
export class Gates {
|
|
135
144
|
#map = new Map();
|
|
136
145
|
#entry(n) {
|
|
137
146
|
let entry = this.#map.get(n);
|
|
@@ -160,6 +169,7 @@ export class Interrupted extends Error {
|
|
|
160
169
|
}
|
|
161
170
|
export class Target {
|
|
162
171
|
runtime;
|
|
172
|
+
transform = transformCell;
|
|
163
173
|
#child;
|
|
164
174
|
#shell;
|
|
165
175
|
#control;
|
|
@@ -182,11 +192,20 @@ export class Target {
|
|
|
182
192
|
this.versions = versions;
|
|
183
193
|
}
|
|
184
194
|
static async launch(runtime, events, options = {}) {
|
|
185
|
-
const
|
|
186
|
-
|
|
195
|
+
const runtimeUrl = new URL("../runtime/", import.meta.url).href;
|
|
196
|
+
// A Deno target ignores the project's import map for imports made from
|
|
197
|
+
// under node_modules, so an npm install's runtime files are copied out.
|
|
198
|
+
const copy = runtime === "deno" && isInsideNodeModules(runtimeUrl);
|
|
199
|
+
const bootstrap = join(await runtimeDir(runtimeUrl, undefined, undefined, { copy }), "bootstrap.mjs");
|
|
200
|
+
const { cmd, args } = launchCommand(runtime, bootstrap, {
|
|
201
|
+
executable: options.executable,
|
|
202
|
+
denoConfig: options.denoConfig,
|
|
203
|
+
});
|
|
187
204
|
const env = { ...process.env, FORCE_COLOR: "1" };
|
|
188
205
|
if (options.denoCompat)
|
|
189
206
|
env.CDPK_DENO_COMPAT = "1";
|
|
207
|
+
if (runtime === "deno" && options.denoConfig !== undefined)
|
|
208
|
+
env.CDPK_DENO_PROJECT = "1";
|
|
190
209
|
delete env.NO_COLOR;
|
|
191
210
|
const child = spawn(cmd, args, { cwd: options.cwd, stdio: ["pipe", "pipe", "pipe"], env });
|
|
192
211
|
child.stdout.setEncoding("utf8");
|
|
@@ -235,7 +254,7 @@ export class Target {
|
|
|
235
254
|
await shell.send("Runtime.enable");
|
|
236
255
|
await shell.send("Runtime.addBinding", { name: BINDING });
|
|
237
256
|
const contextId = await contextReady;
|
|
238
|
-
const target = new Target(runtime, child, shell, control, stdout, stderr, gates, contextId, { runtime: "",
|
|
257
|
+
const target = new Target(runtime, child, shell, control, stdout, stderr, gates, contextId, { runtime: "", engine: "" });
|
|
239
258
|
shell.on("Runtime.bindingCalled", (params) => {
|
|
240
259
|
if (params.name !== BINDING || typeof params.payload !== "string")
|
|
241
260
|
return;
|
|
@@ -250,7 +269,7 @@ export class Target {
|
|
|
250
269
|
break;
|
|
251
270
|
await new Promise((r) => setTimeout(r, 25));
|
|
252
271
|
}
|
|
253
|
-
const versions = await target.#evaluateRaw(`JSON.stringify(typeof Deno !== "undefined" ? { runtime: Deno.version.deno,
|
|
272
|
+
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 })`);
|
|
254
273
|
Object.assign(target.versions, JSON.parse(String(versions.result.value)));
|
|
255
274
|
return target;
|
|
256
275
|
}
|
|
@@ -320,6 +339,42 @@ export class Target {
|
|
|
320
339
|
arg,
|
|
321
340
|
]);
|
|
322
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* Hands a frontend comm message to the target's comms. Resolves to
|
|
344
|
+
* whether a comm or registered target took it, or to the error a handler
|
|
345
|
+
* threw. Rejects with Interrupted after interrupt().
|
|
346
|
+
*/
|
|
347
|
+
async dispatchComm(msgType, content, metadata, buffers) {
|
|
348
|
+
this.#interrupted = false;
|
|
349
|
+
let res;
|
|
350
|
+
try {
|
|
351
|
+
res = await this.#shell.send("Runtime.callFunctionOn", {
|
|
352
|
+
functionDeclaration: `function (t, c, m, b) { return globalThis.${SHIM}.commDispatch(t, c, m, b); }`,
|
|
353
|
+
arguments: [
|
|
354
|
+
{ value: msgType },
|
|
355
|
+
{ value: content },
|
|
356
|
+
{ value: metadata },
|
|
357
|
+
{ value: buffers.map((b) => Buffer.from(b).toString("base64")) },
|
|
358
|
+
],
|
|
359
|
+
executionContextId: this.#contextId,
|
|
360
|
+
awaitPromise: true,
|
|
361
|
+
returnByValue: true,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
if (err instanceof CdpAbandoned || this.#interrupted)
|
|
366
|
+
throw new Interrupted("Comm handler interrupted");
|
|
367
|
+
throw err;
|
|
368
|
+
}
|
|
369
|
+
const details = res.exceptionDetails;
|
|
370
|
+
if (details) {
|
|
371
|
+
const error = details.exception
|
|
372
|
+
? await this.describeError(details.exception)
|
|
373
|
+
: { name: "Error", message: details.text, stack: "" };
|
|
374
|
+
return { error };
|
|
375
|
+
}
|
|
376
|
+
return { handled: res.result?.value === true };
|
|
377
|
+
}
|
|
323
378
|
async describeError(exception) {
|
|
324
379
|
const arg = exception.objectId ? { objectId: exception.objectId } : { value: exception.value };
|
|
325
380
|
const res = await this.callFunctionOn(`function (e) { return globalThis.${SHIM}.describeError(e); }`, [arg]);
|