@tlockney/cdp-kernel 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +259 -33
- package/dist/bun-target.d.ts +60 -0
- package/dist/bun-target.js +236 -0
- package/dist/celld-target.d.ts +39 -0
- package/dist/celld-target.js +274 -0
- package/dist/debugger.d.ts +42 -0
- package/dist/debugger.js +534 -0
- package/dist/history.d.ts +18 -0
- package/dist/history.js +45 -0
- package/dist/kernel.js +91 -15
- package/dist/main.js +8 -6
- package/dist/murmur2.d.ts +2 -0
- package/dist/murmur2.js +41 -0
- package/dist/package.d.ts +16 -2
- package/dist/package.js +28 -2
- package/dist/sourcemap.d.ts +22 -0
- package/dist/sourcemap.js +86 -0
- package/dist/target.d.ts +65 -7
- package/dist/target.js +57 -6
- package/dist/text.d.ts +6 -0
- package/dist/text.js +17 -0
- package/dist/transform.d.ts +16 -0
- package/dist/transform.js +173 -11
- package/dist/workerd-target.d.ts +125 -0
- package/dist/workerd-target.js +455 -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 +18 -4
- package/runtime/bootstrap.mjs +55 -5
- package/runtime/bun-channel.d.mts +3 -0
- package/runtime/bun-channel.mjs +58 -0
- package/runtime/deno-compat.mjs +3 -1
- package/runtime/display.d.mts +2 -0
- package/runtime/display.mjs +9 -1
- package/runtime/evaluator.d.mts +22 -0
- package/runtime/evaluator.mjs +91 -0
- package/runtime/introspect.d.mts +13 -0
- package/runtime/introspect.mjs +120 -0
- package/runtime/node-hooks.d.mts +29 -0
- package/runtime/node-hooks.mjs +160 -20
- package/runtime/workerd-worker.mjs +194 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
export declare const OP_CONTINUATION = 0;
|
|
3
|
+
export declare const OP_TEXT = 1;
|
|
4
|
+
export declare const OP_CLOSE = 8;
|
|
5
|
+
export declare const OP_PING = 9;
|
|
6
|
+
export declare const OP_PONG = 10;
|
|
7
|
+
export interface Frame {
|
|
8
|
+
fin: boolean;
|
|
9
|
+
opcode: number;
|
|
10
|
+
payload: Buffer;
|
|
11
|
+
}
|
|
12
|
+
/** One frame; clients must mask what they send. */
|
|
13
|
+
export declare function encodeFrame(opcode: number, payload: Buffer, mask?: Buffer | null): Buffer;
|
|
14
|
+
/** Splits a byte stream into frames, holding back a partial one. */
|
|
15
|
+
export declare class FrameDecoder {
|
|
16
|
+
#private;
|
|
17
|
+
push(chunk: Buffer): Frame[];
|
|
18
|
+
}
|
|
19
|
+
export declare function acceptKey(key: string): string;
|
|
20
|
+
export interface WsChannel {
|
|
21
|
+
send(text: string): void;
|
|
22
|
+
onMessage(listener: (text: string) => void): void;
|
|
23
|
+
onClose(listener: () => void): void;
|
|
24
|
+
close(): void;
|
|
25
|
+
}
|
|
26
|
+
/** Opens a WebSocket over the Unix socket at `socketPath`. */
|
|
27
|
+
export declare function connectUnixWebSocket(socketPath: string, { path, headers }?: {
|
|
28
|
+
path?: string;
|
|
29
|
+
headers?: {
|
|
30
|
+
[name: string]: string;
|
|
31
|
+
};
|
|
32
|
+
}): Promise<WsChannel>;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// A minimal WebSocket client (RFC 6455) over a Unix socket, for the workerd
|
|
2
|
+
// target's session with its driver Worker (#26). The WebSocket that Node
|
|
3
|
+
// and Deno provide can't connect to a Unix socket, and a TCP listener would
|
|
4
|
+
// be reachable by every local process. Text messages only: that's all the
|
|
5
|
+
// session carries.
|
|
6
|
+
import { Buffer } from "node:buffer";
|
|
7
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
8
|
+
import { connect } from "node:net";
|
|
9
|
+
const GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
10
|
+
export const OP_CONTINUATION = 0x0;
|
|
11
|
+
export const OP_TEXT = 0x1;
|
|
12
|
+
export const OP_CLOSE = 0x8;
|
|
13
|
+
export const OP_PING = 0x9;
|
|
14
|
+
export const OP_PONG = 0xa;
|
|
15
|
+
/** One frame; clients must mask what they send. */
|
|
16
|
+
export function encodeFrame(opcode, payload, mask = randomBytes(4)) {
|
|
17
|
+
const len = payload.length;
|
|
18
|
+
const head = len < 126 ? 2 : len < 0x10000 ? 4 : 10;
|
|
19
|
+
const out = Buffer.alloc(head + (mask ? 4 : 0) + len);
|
|
20
|
+
out[0] = 0x80 | opcode;
|
|
21
|
+
const maskBit = mask ? 0x80 : 0;
|
|
22
|
+
if (len < 126) {
|
|
23
|
+
out[1] = maskBit | len;
|
|
24
|
+
}
|
|
25
|
+
else if (len < 0x10000) {
|
|
26
|
+
out[1] = maskBit | 126;
|
|
27
|
+
out.writeUInt16BE(len, 2);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
out[1] = maskBit | 127;
|
|
31
|
+
out.writeBigUInt64BE(BigInt(len), 2);
|
|
32
|
+
}
|
|
33
|
+
if (mask) {
|
|
34
|
+
mask.copy(out, head);
|
|
35
|
+
for (let i = 0; i < len; i++)
|
|
36
|
+
out[head + 4 + i] = payload[i] ^ mask[i & 3];
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
payload.copy(out, head);
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
/** Splits a byte stream into frames, holding back a partial one. */
|
|
44
|
+
export class FrameDecoder {
|
|
45
|
+
#buffer = Buffer.alloc(0);
|
|
46
|
+
push(chunk) {
|
|
47
|
+
this.#buffer = Buffer.concat([this.#buffer, chunk]);
|
|
48
|
+
const frames = [];
|
|
49
|
+
for (;;) {
|
|
50
|
+
const b = this.#buffer;
|
|
51
|
+
if (b.length < 2)
|
|
52
|
+
break;
|
|
53
|
+
const masked = (b[1] & 0x80) !== 0;
|
|
54
|
+
let len = b[1] & 0x7f;
|
|
55
|
+
let offset = 2;
|
|
56
|
+
if (len === 126) {
|
|
57
|
+
if (b.length < 4)
|
|
58
|
+
break;
|
|
59
|
+
len = b.readUInt16BE(2);
|
|
60
|
+
offset = 4;
|
|
61
|
+
}
|
|
62
|
+
else if (len === 127) {
|
|
63
|
+
if (b.length < 10)
|
|
64
|
+
break;
|
|
65
|
+
len = Number(b.readBigUInt64BE(2));
|
|
66
|
+
offset = 10;
|
|
67
|
+
}
|
|
68
|
+
const maskAt = offset;
|
|
69
|
+
if (masked)
|
|
70
|
+
offset += 4;
|
|
71
|
+
if (b.length < offset + len)
|
|
72
|
+
break;
|
|
73
|
+
const payload = Buffer.from(b.subarray(offset, offset + len));
|
|
74
|
+
if (masked)
|
|
75
|
+
for (let i = 0; i < len; i++)
|
|
76
|
+
payload[i] ^= b[maskAt + (i & 3)];
|
|
77
|
+
frames.push({ fin: (b[0] & 0x80) !== 0, opcode: b[0] & 0x0f, payload });
|
|
78
|
+
this.#buffer = b.subarray(offset + len);
|
|
79
|
+
}
|
|
80
|
+
return frames;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export function acceptKey(key) {
|
|
84
|
+
return createHash("sha1").update(key + GUID).digest("base64");
|
|
85
|
+
}
|
|
86
|
+
/** Opens a WebSocket over the Unix socket at `socketPath`. */
|
|
87
|
+
export function connectUnixWebSocket(socketPath, { path = "/", headers = {} } = {}) {
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
const socket = connect({ path: socketPath });
|
|
90
|
+
const key = randomBytes(16).toString("base64");
|
|
91
|
+
const messageListeners = [];
|
|
92
|
+
const closeListeners = [];
|
|
93
|
+
let open = false;
|
|
94
|
+
let closed = false;
|
|
95
|
+
let head = Buffer.alloc(0);
|
|
96
|
+
const decoder = new FrameDecoder();
|
|
97
|
+
let parts = [];
|
|
98
|
+
const finish = () => {
|
|
99
|
+
if (closed)
|
|
100
|
+
return;
|
|
101
|
+
closed = true;
|
|
102
|
+
if (!open)
|
|
103
|
+
reject(new Error("the WebSocket closed before it opened"));
|
|
104
|
+
for (const listener of closeListeners)
|
|
105
|
+
listener();
|
|
106
|
+
};
|
|
107
|
+
const write = (opcode, payload) => {
|
|
108
|
+
if (!closed)
|
|
109
|
+
socket.write(encodeFrame(opcode, payload));
|
|
110
|
+
};
|
|
111
|
+
const channel = {
|
|
112
|
+
send: (text) => write(OP_TEXT, Buffer.from(text)),
|
|
113
|
+
onMessage: (listener) => void messageListeners.push(listener),
|
|
114
|
+
onClose: (listener) => void closeListeners.push(listener),
|
|
115
|
+
close() {
|
|
116
|
+
write(OP_CLOSE, Buffer.alloc(0));
|
|
117
|
+
socket.end();
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
const onFrames = (frames) => {
|
|
121
|
+
for (const frame of frames) {
|
|
122
|
+
if (frame.opcode === OP_TEXT || frame.opcode === OP_CONTINUATION) {
|
|
123
|
+
parts.push(frame.payload);
|
|
124
|
+
if (frame.fin) {
|
|
125
|
+
const text = Buffer.concat(parts).toString("utf8");
|
|
126
|
+
parts = [];
|
|
127
|
+
for (const listener of messageListeners)
|
|
128
|
+
listener(text);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else if (frame.opcode === OP_PING) {
|
|
132
|
+
write(OP_PONG, frame.payload);
|
|
133
|
+
}
|
|
134
|
+
else if (frame.opcode === OP_CLOSE) {
|
|
135
|
+
socket.end();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
socket.on("data", (chunk) => {
|
|
140
|
+
if (open)
|
|
141
|
+
return onFrames(decoder.push(chunk));
|
|
142
|
+
head = Buffer.concat([head, chunk]);
|
|
143
|
+
const end = head.indexOf("\r\n\r\n");
|
|
144
|
+
if (end === -1)
|
|
145
|
+
return;
|
|
146
|
+
const response = head.subarray(0, end).toString("latin1");
|
|
147
|
+
const accept = /^sec-websocket-accept:\s*(\S+)/im.exec(response)?.[1];
|
|
148
|
+
if (!/^HTTP\/1\.1 101 /.test(response) || accept !== acceptKey(key)) {
|
|
149
|
+
socket.destroy();
|
|
150
|
+
reject(new Error(`the WebSocket handshake failed: ${response.split("\r\n")[0]}`));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
open = true;
|
|
154
|
+
resolve(channel);
|
|
155
|
+
onFrames(decoder.push(head.subarray(end + 4)));
|
|
156
|
+
});
|
|
157
|
+
socket.on("error", (err) => {
|
|
158
|
+
if (!open)
|
|
159
|
+
reject(err);
|
|
160
|
+
});
|
|
161
|
+
socket.on("close", finish);
|
|
162
|
+
const extra = Object.entries(headers).map(([name, value]) => `${name}: ${value}\r\n`).join("");
|
|
163
|
+
socket.write(`GET ${path} HTTP/1.1\r\nHost: cdpk\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n` +
|
|
164
|
+
`Sec-WebSocket-Key: ${key}\r\nSec-WebSocket-Version: 13\r\n${extra}\r\n`);
|
|
165
|
+
});
|
|
166
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tlockney/cdp-kernel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Deno's Jupyter kernel, extracted to run notebooks on any V8 runtime over the Chrome DevTools Protocol",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Thomas Lockney <thomas@lockney.net>",
|
|
@@ -8,7 +8,16 @@
|
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "git+https://github.com/tlockney/cdp-kernel.git"
|
|
10
10
|
},
|
|
11
|
-
"keywords": [
|
|
11
|
+
"keywords": [
|
|
12
|
+
"jupyter",
|
|
13
|
+
"kernel",
|
|
14
|
+
"typescript",
|
|
15
|
+
"notebook",
|
|
16
|
+
"deno",
|
|
17
|
+
"node",
|
|
18
|
+
"cdp",
|
|
19
|
+
"devtools-protocol"
|
|
20
|
+
],
|
|
12
21
|
"type": "module",
|
|
13
22
|
"engines": {
|
|
14
23
|
"node": ">=24"
|
|
@@ -44,8 +53,11 @@
|
|
|
44
53
|
"install:node": "node src/main.ts install --runtime node",
|
|
45
54
|
"install:deno": "node src/main.ts install --runtime deno",
|
|
46
55
|
"install:node-deno": "node src/main.ts install --runtime node --deno-compat",
|
|
56
|
+
"install:bun": "node src/main.ts install --runtime bun",
|
|
57
|
+
"install:workerd": "node src/main.ts install --runtime workerd",
|
|
58
|
+
"install:celld": "node src/main.ts install --runtime celld",
|
|
47
59
|
"prepack": "npm run build",
|
|
48
|
-
"test": "node --test test/*.test.ts",
|
|
60
|
+
"test": "node --test --test-timeout=300000 test/*.test.ts",
|
|
49
61
|
"e2e": "uv run --with nbclient --with nbformat --with jupyter_client python test/e2e.py",
|
|
50
62
|
"test:lab": "uv run --with jupyterlab --with anywidget --with playwright --with nbformat python test/lab_widgets.py"
|
|
51
63
|
},
|
|
@@ -56,6 +68,8 @@
|
|
|
56
68
|
},
|
|
57
69
|
"devDependencies": {
|
|
58
70
|
"@types/node": "^24.19.2",
|
|
59
|
-
"typescript": "^5.9.3"
|
|
71
|
+
"typescript": "^5.9.3",
|
|
72
|
+
"workerd": "1.20261015.1",
|
|
73
|
+
"wrangler": "4.151.0"
|
|
60
74
|
}
|
|
61
75
|
}
|
package/runtime/bootstrap.mjs
CHANGED
|
@@ -4,14 +4,18 @@
|
|
|
4
4
|
// then drives it over CDP. Everything the host needs from inside the
|
|
5
5
|
// runtime lives on globalThis.__cdpk; output to the host goes through the
|
|
6
6
|
// `__cdpk_send` CDP binding (Runtime.addBinding) plus plain stdout/stderr.
|
|
7
|
+
// Bun is launched as `bun runtime/bootstrap.mjs` and driven over a pipe
|
|
8
|
+
// instead; see bun-channel.mjs.
|
|
7
9
|
//
|
|
8
|
-
// Plain JavaScript on purpose: the same file must load unmodified in Node
|
|
9
|
-
// and
|
|
10
|
+
// Plain JavaScript on purpose: the same file must load unmodified in Node,
|
|
11
|
+
// Deno and Bun.
|
|
10
12
|
|
|
11
13
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
12
14
|
import { createJupyter } from "./display.mjs";
|
|
15
|
+
import { describeValue, inspectPath, resolvePath } from "./introspect.mjs";
|
|
13
16
|
|
|
14
17
|
const isDeno = typeof globalThis.Deno?.version?.deno === "string";
|
|
18
|
+
const isBun = typeof globalThis.Bun?.version === "string";
|
|
15
19
|
const ENC = new TextEncoder();
|
|
16
20
|
|
|
17
21
|
const cwd = isDeno ? Deno.cwd() : process.cwd();
|
|
@@ -23,7 +27,7 @@ const cwdUrl = pathToFileURL(cwd.endsWith("/") ? cwd : cwd + "/");
|
|
|
23
27
|
// Stand-in referrer for cell code, like Deno's ./$deno$jupyter_repl.mts.
|
|
24
28
|
const replUrl = new URL("$cdpk$repl.mjs", cwdUrl);
|
|
25
29
|
|
|
26
|
-
const { inspect, readFile, writeOut, writeErr, readStdinLine } = await platform();
|
|
30
|
+
const { inspect, inspectPlain, readFile, writeOut, writeErr, readStdinLine } = await platform();
|
|
27
31
|
|
|
28
32
|
function base64(bytes) {
|
|
29
33
|
let binary = "";
|
|
@@ -77,7 +81,7 @@ function resolveRelative(specifier) {
|
|
|
77
81
|
return /^\.{0,2}\//.test(specifier) ? new URL(specifier, replUrl).href : specifier;
|
|
78
82
|
}
|
|
79
83
|
|
|
80
|
-
if (!isDeno) {
|
|
84
|
+
if (!isDeno && !isBun) {
|
|
81
85
|
// Deno resolves npm:/jsr: natively; Node gets a resolve hook that adds
|
|
82
86
|
// them and re-parents cell imports to the notebook's directory.
|
|
83
87
|
const { installNodeResolveHooks } = await import("./node-hooks.mjs");
|
|
@@ -108,12 +112,36 @@ async function denoImport(specifier, options) {
|
|
|
108
112
|
}
|
|
109
113
|
}
|
|
110
114
|
|
|
115
|
+
// Bun has neither npm: nor jsr:, and would resolve a bare name from this
|
|
116
|
+
// file's directory. npm:/jsr: install into a per-user cache as on Node
|
|
117
|
+
// (with `bun add`); anything else bare resolves from the notebook's
|
|
118
|
+
// directory, so its node_modules, or Bun's auto-install, supplies it.
|
|
119
|
+
const packages = isBun ? await import("./node-hooks.mjs") : null;
|
|
120
|
+
|
|
121
|
+
function bunResolve(specifier) {
|
|
122
|
+
let pkg = null;
|
|
123
|
+
if (specifier.startsWith("npm:")) pkg = packages.parsePackageSpecifier(specifier.slice(4));
|
|
124
|
+
else if (specifier.startsWith("jsr:")) pkg = packages.jsrToNpm(specifier.slice(4));
|
|
125
|
+
if (pkg) {
|
|
126
|
+
const { dir, alias } = packages.ensureInstalled(pkg, "bun", { lockFile: packages.findLockFile(cwd) });
|
|
127
|
+
return Bun.resolveSync(alias + pkg.subpath, dir);
|
|
128
|
+
}
|
|
129
|
+
// Relative, absolute, or a URL scheme such as node:, bun: or file:.
|
|
130
|
+
if (/^(?:\.{0,2}\/|[a-z][a-z0-9+.-]*:)/i.test(specifier)) return resolveRelative(specifier);
|
|
131
|
+
return Bun.resolveSync(specifier, cwd);
|
|
132
|
+
}
|
|
133
|
+
|
|
111
134
|
globalThis.__cdpk = {
|
|
112
135
|
// Cell code awaits this first; see PRELUDE in src/transform.ts.
|
|
113
136
|
tick: () => new Promise((resolve) => setTimeout(resolve, 0)),
|
|
114
137
|
// Node: the resolve hook re-parents these imports. Deno would resolve
|
|
115
138
|
// relative paths against this file, so denoImport does it here.
|
|
116
|
-
import: (specifier, options) =>
|
|
139
|
+
import: (specifier, options) =>
|
|
140
|
+
isDeno
|
|
141
|
+
? denoImport(specifier, options)
|
|
142
|
+
: isBun
|
|
143
|
+
? import(bunResolve(specifier), options)
|
|
144
|
+
: import(specifier, options),
|
|
117
145
|
meta: {
|
|
118
146
|
url: replUrl.href,
|
|
119
147
|
dirname: cwd,
|
|
@@ -121,7 +149,19 @@ globalThis.__cdpk = {
|
|
|
121
149
|
resolve: (specifier) => new URL(specifier, replUrl).href,
|
|
122
150
|
},
|
|
123
151
|
barrier,
|
|
152
|
+
// inspect_request (#10). CDP targets find the value with
|
|
153
|
+
// throwOnSideEffect and describe it here; Bun resolves the path here
|
|
154
|
+
// too, by descriptor, so nothing runs.
|
|
155
|
+
describeForInspect: (value, detail) => describeValue(value, { inspect: inspectPlain, detail }),
|
|
156
|
+
inspectPath: (path, detail) => inspectPath(path, { inspect: inspectPlain, detail }),
|
|
157
|
+
resolvePath,
|
|
124
158
|
broadcastResult: jupyter.broadcastResult,
|
|
159
|
+
// The debugger's rich variable viewer (#18), synchronously: it runs while
|
|
160
|
+
// the target is paused. A value whose $display is async shows as text.
|
|
161
|
+
richBundle(value) {
|
|
162
|
+
const bundle = jupyter.formatNow(value);
|
|
163
|
+
return JSON.stringify(typeof bundle?.then === "function" ? { "text/plain": inspectPlain(value) } : bundle);
|
|
164
|
+
},
|
|
125
165
|
// Frontend comm messages (#9), from the host's shell channel. Handlers
|
|
126
166
|
// run after one macrotask, like cells, so interrupt can reach them.
|
|
127
167
|
async commDispatch(msgType, content, metadata, buffers64) {
|
|
@@ -182,11 +222,20 @@ if (isDeno) {
|
|
|
182
222
|
// Keep the event loop alive between cells.
|
|
183
223
|
setInterval(() => {}, 1 << 30);
|
|
184
224
|
|
|
225
|
+
if (isBun) {
|
|
226
|
+
const { startChannel } = await import("./bun-channel.mjs");
|
|
227
|
+
const { createHandler } = await import("./evaluator.mjs");
|
|
228
|
+
startChannel(createHandler(globalThis.__cdpk), {
|
|
229
|
+
versions: { runtime: Bun.version, engine: `JavaScriptCore (WebKit ${process.versions.webkit.slice(0, 12)})` },
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
185
233
|
async function platform() {
|
|
186
234
|
if (isDeno) {
|
|
187
235
|
const stdinBuf = new Uint8Array(1);
|
|
188
236
|
return {
|
|
189
237
|
inspect: (v) => Deno.inspect(v, { colors: true, depth: 4 }),
|
|
238
|
+
inspectPlain: (v) => Deno.inspect(v, { colors: false, depth: 2 }),
|
|
190
239
|
readFile: (p) => Deno.readFileSync(p),
|
|
191
240
|
writeOut: (s) => Deno.stdout.writeSync(ENC.encode(s)),
|
|
192
241
|
writeErr: (s) => Deno.stderr.writeSync(ENC.encode(s)),
|
|
@@ -198,6 +247,7 @@ async function platform() {
|
|
|
198
247
|
const buf = Buffer.alloc(1);
|
|
199
248
|
return {
|
|
200
249
|
inspect: (v) => util.inspect(v, { colors: true, depth: 4 }),
|
|
250
|
+
inspectPlain: (v) => util.inspect(v, { colors: false, depth: 2 }),
|
|
201
251
|
readFile: (p) => fs.readFileSync(p),
|
|
202
252
|
writeOut: (s) => process.stdout.write(s),
|
|
203
253
|
writeErr: (s) => process.stderr.write(s),
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// The Bun target's side of the host connection (#7), in place of CDP.
|
|
2
|
+
//
|
|
3
|
+
// Bun's --inspect speaks the WebKit Inspector Protocol, which has no
|
|
4
|
+
// replMode, Runtime.addBinding or Runtime.terminateExecution, so the host
|
|
5
|
+
// doesn't drive Bun over an inspector. It sends requests as JSON lines over
|
|
6
|
+
// fd 3, a pipe only the host holds (there is no port to discover), and the
|
|
7
|
+
// cells are evaluated here, in-process, by evaluator.mjs. Replies, and the
|
|
8
|
+
// iopub messages that the CDP binding carries for other targets, go back the
|
|
9
|
+
// same way. stdout and stderr stay ordinary pipes, ordered against those
|
|
10
|
+
// messages by bootstrap.mjs's barrier markers.
|
|
11
|
+
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
|
|
14
|
+
const CHANNEL_FD = 3;
|
|
15
|
+
|
|
16
|
+
/** Synchronous, like a CDP binding call, so output order is kept. */
|
|
17
|
+
function writeAll(fd, text) {
|
|
18
|
+
const bytes = Buffer.from(text);
|
|
19
|
+
for (let offset = 0; offset < bytes.length;) {
|
|
20
|
+
try {
|
|
21
|
+
offset += fs.writeSync(fd, bytes, offset);
|
|
22
|
+
} catch (err) {
|
|
23
|
+
// The reader's side of the pipe is full; the fd may be non-blocking.
|
|
24
|
+
if (err.code !== "EAGAIN") throw err;
|
|
25
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Serves `handle` (see evaluator.mjs) over fd 3 and installs
|
|
32
|
+
* globalThis.__cdpk_send, the function bootstrap.mjs sends iopub messages
|
|
33
|
+
* through. `ready` is sent first, once the target can take requests.
|
|
34
|
+
*/
|
|
35
|
+
export function startChannel(handle, ready, fd = CHANNEL_FD) {
|
|
36
|
+
const send = (message) => writeAll(fd, JSON.stringify(message) + "\n");
|
|
37
|
+
Object.defineProperty(globalThis, "__cdpk_send", {
|
|
38
|
+
value: (payload) => writeAll(fd, payload + "\n"),
|
|
39
|
+
enumerable: false,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
let buffer = "";
|
|
43
|
+
const input = fs.createReadStream(null, { fd, autoClose: false, encoding: "utf8" });
|
|
44
|
+
input.on("data", (chunk) => {
|
|
45
|
+
buffer += chunk;
|
|
46
|
+
for (let nl = buffer.indexOf("\n"); nl !== -1; nl = buffer.indexOf("\n")) {
|
|
47
|
+
const request = JSON.parse(buffer.slice(0, nl));
|
|
48
|
+
buffer = buffer.slice(nl + 1);
|
|
49
|
+
handle(request).then(
|
|
50
|
+
(reply) => send({ kind: "reply", id: request.id, ...reply }),
|
|
51
|
+
(err) => send({ kind: "reply", id: request.id, failure: String(err?.stack ?? err) }),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
// The host is gone; don't linger as an orphan.
|
|
56
|
+
input.on("end", () => process.exit(0));
|
|
57
|
+
send({ kind: "ready", ...ready });
|
|
58
|
+
}
|
package/runtime/deno-compat.mjs
CHANGED
|
@@ -161,7 +161,9 @@ export function createDenoShim({ jupyter }) {
|
|
|
161
161
|
rename: async((from, to) => fsp.rename(toPath(from), toPath(to))),
|
|
162
162
|
renameSync: sync((from, to) => fs.renameSync(toPath(from), toPath(to))),
|
|
163
163
|
realPath: async((p) => fsp.realpath(toPath(p))),
|
|
164
|
-
|
|
164
|
+
// The native call, like fsp.realpath above: the JavaScript one leaves
|
|
165
|
+
// Windows 8.3 names (RUNNER~1) unexpanded, so the two disagreed (#32).
|
|
166
|
+
realPathSync: sync((p) => fs.realpathSync.native(toPath(p))),
|
|
165
167
|
makeTempDir: async((options) =>
|
|
166
168
|
fsp.mkdtemp(path.join(options?.dir ?? os.tmpdir(), options?.prefix ?? "deno-"))
|
|
167
169
|
),
|
package/runtime/display.d.mts
CHANGED
|
@@ -47,6 +47,8 @@ export interface JupyterNamespace {
|
|
|
47
47
|
export interface Jupyter {
|
|
48
48
|
namespace: JupyterNamespace;
|
|
49
49
|
broadcastResult(executionCount: number, result: unknown): Promise<void>;
|
|
50
|
+
/** format without awaiting; a $display method's promise is returned as is. */
|
|
51
|
+
formatNow(obj: unknown): unknown;
|
|
50
52
|
confirm(message?: string): boolean;
|
|
51
53
|
prompt(message?: string, defaultValue?: string, options?: { password?: boolean }): string | null;
|
|
52
54
|
/** Routes a frontend comm message into the kernel's comms. */
|
package/runtime/display.mjs
CHANGED
|
@@ -146,7 +146,14 @@ function isMediaBundle(obj) {
|
|
|
146
146
|
*/
|
|
147
147
|
export function createJupyter(host) {
|
|
148
148
|
async function format(obj) {
|
|
149
|
-
|
|
149
|
+
return await formatNow(obj);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// format without awaiting: the debugger's variable viewer formats values
|
|
153
|
+
// while the target is paused, when no promise settles (#18). A $display
|
|
154
|
+
// method's promise is the caller's to handle.
|
|
155
|
+
function formatNow(obj) {
|
|
156
|
+
if (hasDisplaySymbol(obj)) return obj[$display]();
|
|
150
157
|
if (typeof obj !== "object" || obj === null) return { "text/plain": host.inspect(obj) };
|
|
151
158
|
if (isCanvasLike(obj)) {
|
|
152
159
|
const [prefix, data] = obj.toDataURL().split(",");
|
|
@@ -240,6 +247,7 @@ export function createJupyter(host) {
|
|
|
240
247
|
widget: createWidgetFactory(comms.api, $display),
|
|
241
248
|
},
|
|
242
249
|
broadcastResult,
|
|
250
|
+
formatNow,
|
|
243
251
|
confirm,
|
|
244
252
|
prompt,
|
|
245
253
|
commDispatch: comms.dispatch,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** The parts of globalThis.__cdpk the evaluator calls. */
|
|
2
|
+
export interface EvaluatorShim {
|
|
3
|
+
broadcastResult(executionCount: number, value: unknown): void | Promise<void>;
|
|
4
|
+
describeError(value: unknown): string;
|
|
5
|
+
commDispatch(msgType: string, content: object, metadata: object, buffers64: string[]): Promise<unknown>;
|
|
6
|
+
barrier(): number;
|
|
7
|
+
/** inspect_request's text for a dotted path, or null (#10). */
|
|
8
|
+
inspectPath(path: string, detail: number): string | null;
|
|
9
|
+
/** Resolves a dotted path without running getters; see introspect.mjs. */
|
|
10
|
+
resolvePath?(path: string): import("./introspect.mjs").Resolved;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A request from the host: an `op` plus its parameters. */
|
|
14
|
+
export interface EvaluatorRequest {
|
|
15
|
+
op: string;
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type Handler = (request: EvaluatorRequest) => Promise<{ [key: string]: unknown }>;
|
|
20
|
+
|
|
21
|
+
export function propertyNames(value: unknown): string[];
|
|
22
|
+
export function createHandler(shim: EvaluatorShim, evaluate?: (code: string) => unknown): Handler;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// The in-process evaluator behind the targets that don't use CDP: Bun (#7),
|
|
2
|
+
// over a pipe (bun-channel.mjs), and workerd (#8), inside a request
|
|
3
|
+
// (workerd-worker.mjs). Cells arrive as global code from
|
|
4
|
+
// src/transform.ts's transformCellForEval, whose value is a promise of the
|
|
5
|
+
// boxed result. Requests and replies mirror the CDP calls the kernel makes
|
|
6
|
+
// of the other targets.
|
|
7
|
+
//
|
|
8
|
+
// No imports: this file must load in every target.
|
|
9
|
+
|
|
10
|
+
/** Own and inherited property names, as Runtime.getProperties lists them. */
|
|
11
|
+
export function propertyNames(value) {
|
|
12
|
+
if (value === null || value === undefined) return [];
|
|
13
|
+
const names = new Set();
|
|
14
|
+
for (let o = Object(value); o !== null; o = Object.getPrototypeOf(o)) {
|
|
15
|
+
for (const name of Object.getOwnPropertyNames(o)) names.add(name);
|
|
16
|
+
}
|
|
17
|
+
return [...names];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Answers the host's requests. `shim` is globalThis.__cdpk; `evaluate` runs
|
|
22
|
+
* global code. Values the host refers to later (a result to display, an
|
|
23
|
+
* error to describe) stay here under a handle, the stand-in for a CDP
|
|
24
|
+
* objectId.
|
|
25
|
+
*/
|
|
26
|
+
export function createHandler(shim, evaluate = (code) => (0, eval)(code)) {
|
|
27
|
+
const handles = new Map();
|
|
28
|
+
let nextHandle = 0;
|
|
29
|
+
const remote = (value) => {
|
|
30
|
+
const objectId = `h${++nextHandle}`;
|
|
31
|
+
handles.set(objectId, value);
|
|
32
|
+
return { type: value === null ? "object" : typeof value, objectId };
|
|
33
|
+
};
|
|
34
|
+
const take = (objectId) => {
|
|
35
|
+
const value = handles.get(objectId);
|
|
36
|
+
handles.delete(objectId);
|
|
37
|
+
return value;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return async function handle(request) {
|
|
41
|
+
switch (request.op) {
|
|
42
|
+
case "ping":
|
|
43
|
+
return {};
|
|
44
|
+
case "eval": {
|
|
45
|
+
// The host uses a result's handle before its next cell.
|
|
46
|
+
handles.clear();
|
|
47
|
+
try {
|
|
48
|
+
const box = await evaluate(request.code);
|
|
49
|
+
return { result: box === undefined ? { type: "undefined" } : remote(box[0]) };
|
|
50
|
+
} catch (err) {
|
|
51
|
+
return {
|
|
52
|
+
exceptionDetails: { text: "Uncaught", lineNumber: 0, columnNumber: 0, exception: remote(err) },
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
case "broadcast":
|
|
57
|
+
await shim.broadcastResult(request.count, take(request.handle));
|
|
58
|
+
return {};
|
|
59
|
+
case "describe":
|
|
60
|
+
return { value: shim.describeError(take(request.handle)) };
|
|
61
|
+
case "comm":
|
|
62
|
+
try {
|
|
63
|
+
const handled = await shim.commDispatch(request.msgType, request.content, request.metadata, request.buffers);
|
|
64
|
+
return { handled: handled === true };
|
|
65
|
+
} catch (err) {
|
|
66
|
+
return { error: JSON.parse(shim.describeError(err)) };
|
|
67
|
+
}
|
|
68
|
+
case "props": {
|
|
69
|
+
// Completion's expression is a dotted path; resolved by descriptor
|
|
70
|
+
// where the shim can, so completing `obj.` never runs a getter (#10).
|
|
71
|
+
if (typeof shim.resolvePath === "function") {
|
|
72
|
+
const resolved = shim.resolvePath(request.expr);
|
|
73
|
+
return { names: resolved.found && !resolved.accessor ? propertyNames(resolved.value) : [] };
|
|
74
|
+
}
|
|
75
|
+
let value;
|
|
76
|
+
try {
|
|
77
|
+
value = evaluate(`(${request.expr})`);
|
|
78
|
+
} catch {
|
|
79
|
+
return { names: [] };
|
|
80
|
+
}
|
|
81
|
+
return { names: propertyNames(value) };
|
|
82
|
+
}
|
|
83
|
+
case "inspect":
|
|
84
|
+
return { text: shim.inspectPath(request.path, request.detail ?? 0) };
|
|
85
|
+
case "barrier":
|
|
86
|
+
return { value: shim.barrier() };
|
|
87
|
+
default:
|
|
88
|
+
throw new Error(`unknown request ${JSON.stringify(request.op)}`);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type Resolved =
|
|
2
|
+
| { found: false }
|
|
3
|
+
| { found: true; accessor: true; name: string }
|
|
4
|
+
| { found: true; value: unknown; accessor?: undefined };
|
|
5
|
+
|
|
6
|
+
export function resolvePath(path: string, root?: object): Resolved;
|
|
7
|
+
// deno-lint-ignore ban-types -- classes and functions alike
|
|
8
|
+
export function signatureOf(fn: Function): string;
|
|
9
|
+
export function describeValue(value: unknown, options: { inspect: (value: unknown) => string; detail?: number }): string;
|
|
10
|
+
export function inspectPath(
|
|
11
|
+
path: string,
|
|
12
|
+
options: { inspect: (value: unknown) => string; detail?: number; root?: object },
|
|
13
|
+
): string | null;
|