@tlockney/cdp-kernel 0.4.0 → 0.6.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 +78 -478
- package/THIRD-PARTY-NOTICES +62 -0
- package/dist/bun-target.d.ts +2 -2
- package/dist/bun-target.js +30 -8
- package/dist/cdp.d.ts +8 -1
- package/dist/cdp.js +31 -11
- package/dist/celld-target.d.ts +1 -1
- package/dist/celld-target.js +54 -19
- package/dist/children.d.ts +29 -0
- package/dist/children.js +114 -0
- package/dist/debugger.d.ts +1 -1
- package/dist/debugger.js +21 -10
- package/dist/deno-types.d.ts +30 -0
- package/dist/deno-types.js +93 -0
- package/dist/globals.d.ts +3 -3
- package/dist/globals.js +3 -3
- package/dist/kernel.d.ts +7 -1
- package/dist/kernel.js +348 -68
- 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/mod.d.ts +6 -3
- package/dist/mod.js +6 -3
- 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 +3 -3
- 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.d.ts +58 -6
- package/dist/target.js +412 -54
- package/dist/text.d.ts +5 -1
- package/dist/text.js +55 -2
- package/dist/transform.d.ts +30 -1
- package/dist/transform.js +68 -44
- package/dist/wire.d.ts +8 -1
- package/dist/wire.js +3 -1
- package/dist/workerd-target.d.ts +7 -1
- package/dist/workerd-target.js +102 -8
- package/dist/workers-project.js +13 -2
- package/dist/ws-client.d.ts +5 -1
- package/dist/ws-client.js +46 -13
- package/dist/zmtp.d.ts +9 -1
- package/dist/zmtp.js +42 -1
- package/package.json +17 -6
- package/runtime/bootstrap.mjs +233 -42
- package/runtime/bun-channel.d.mts +3 -0
- package/runtime/bun-channel.mjs +2 -2
- package/runtime/comm.mjs +11 -1
- package/runtime/deno-compat.d.mts +2 -0
- package/runtime/deno-compat.mjs +14 -1
- package/runtime/display.mjs +8 -1
- package/runtime/introspect.mjs +1 -1
- package/runtime/node-hooks.d.mts +48 -0
- package/runtime/node-hooks.mjs +257 -41
- package/runtime/workerd-worker.mjs +77 -6
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,10 +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
|
-
|
|
58
|
-
|
|
69
|
+
if (isBun ? typeof globalThis.__cdpk_send !== "function" : !attached) return; // host not attached yet
|
|
70
|
+
// Serialize before writing the marker: if the payload can't be
|
|
71
|
+
// serialized (a BigInt, a cycle), the error reaches the cell without a
|
|
72
|
+
// gated marker the host would wait on forever (#129). `payload` always
|
|
73
|
+
// has a `kind`, so `body` is never "{}".
|
|
74
|
+
const body = JSON.stringify(payload);
|
|
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)}`);
|
|
59
82
|
}
|
|
60
83
|
|
|
61
84
|
const jupyter = createJupyter({
|
|
@@ -69,9 +92,16 @@ const jupyter = createJupyter({
|
|
|
69
92
|
// stdin for its JSON-line reply.
|
|
70
93
|
input(prompt, password) {
|
|
71
94
|
send({ kind: "input_request", prompt, password });
|
|
72
|
-
const line =
|
|
95
|
+
const line = readInputReply();
|
|
73
96
|
if (line === null) return null;
|
|
74
97
|
const reply = JSON.parse(line);
|
|
98
|
+
if (reply.interrupted === true) {
|
|
99
|
+
// Interrupted while waiting for the answer: unwind the cell, as
|
|
100
|
+
// KeyboardInterrupt does in Python's input() (#121).
|
|
101
|
+
const err = new Error("Execution interrupted");
|
|
102
|
+
err.name = "Interrupted";
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
75
105
|
return typeof reply.value === "string" ? reply.value : null;
|
|
76
106
|
},
|
|
77
107
|
});
|
|
@@ -131,9 +161,37 @@ function bunResolve(specifier) {
|
|
|
131
161
|
return Bun.resolveSync(specifier, cwd);
|
|
132
162
|
}
|
|
133
163
|
|
|
164
|
+
// The inspector holds a promise it awaits (callFunctionOn with awaitPromise)
|
|
165
|
+
// only weakly until its then-callback runs, so a GC after the promise
|
|
166
|
+
// settles but before that callback fails the call with "Promise was
|
|
167
|
+
// collected" (#55). hold() keeps the promise until a macrotask after it
|
|
168
|
+
// settles, by which time the inspector's callback has run.
|
|
169
|
+
const held = new Set();
|
|
170
|
+
function hold(value) {
|
|
171
|
+
const promise = Promise.resolve(value);
|
|
172
|
+
held.add(promise);
|
|
173
|
+
const release = () => setTimeout(() => held.delete(promise), 0);
|
|
174
|
+
promise.then(release, release);
|
|
175
|
+
return promise;
|
|
176
|
+
}
|
|
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
|
+
|
|
186
|
+
// Adds the cause to an error from a Deno member the --deno-compat shim
|
|
187
|
+
// lacks (#172); the identity everywhere else.
|
|
188
|
+
let explainError = (text) => text;
|
|
189
|
+
|
|
134
190
|
globalThis.__cdpk = {
|
|
191
|
+
// The host calls this once it reads message records (Node and Deno).
|
|
192
|
+
attach: () => void (attached = true),
|
|
135
193
|
// Cell code awaits this first; see PRELUDE in src/transform.ts.
|
|
136
|
-
tick:
|
|
194
|
+
tick: yieldTask,
|
|
137
195
|
// Node: the resolve hook re-parents these imports. Deno would resolve
|
|
138
196
|
// relative paths against this file, so denoImport does it here.
|
|
139
197
|
import: (specifier, options) =>
|
|
@@ -149,6 +207,7 @@ globalThis.__cdpk = {
|
|
|
149
207
|
resolve: (specifier) => new URL(specifier, replUrl).href,
|
|
150
208
|
},
|
|
151
209
|
barrier,
|
|
210
|
+
hold,
|
|
152
211
|
// inspect_request (#10). CDP targets find the value with
|
|
153
212
|
// throwOnSideEffect and describe it here; Bun resolves the path here
|
|
154
213
|
// too, by descriptor, so nothing runs.
|
|
@@ -165,13 +224,24 @@ globalThis.__cdpk = {
|
|
|
165
224
|
// Frontend comm messages (#9), from the host's shell channel. Handlers
|
|
166
225
|
// run after one macrotask, like cells, so interrupt can reach them.
|
|
167
226
|
async commDispatch(msgType, content, metadata, buffers64) {
|
|
168
|
-
await
|
|
227
|
+
await yieldTask();
|
|
169
228
|
const buffers = buffers64.map((b) => Uint8Array.from(atob(b), (c) => c.charCodeAt(0)));
|
|
170
229
|
return await jupyter.commDispatch(msgType, content, metadata, buffers);
|
|
171
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
|
+
},
|
|
172
240
|
describeError(value) {
|
|
173
241
|
if (value instanceof Error) {
|
|
174
|
-
|
|
242
|
+
const message = explainError(value.message);
|
|
243
|
+
const stack = stackOf(value).replace(value.message, () => message);
|
|
244
|
+
return JSON.stringify({ name: value.name, message, stack });
|
|
175
245
|
}
|
|
176
246
|
return JSON.stringify({ name: "Error", message: inspect(value), stack: "" });
|
|
177
247
|
},
|
|
@@ -184,26 +254,48 @@ globalThis.confirm = jupyter.confirm;
|
|
|
184
254
|
if (isDeno) {
|
|
185
255
|
// Existing Deno notebooks use Deno.jupyter, but outside `deno jupyter`
|
|
186
256
|
// that is a non-configurable getter that throws. The global binding
|
|
187
|
-
// itself is configurable, so swap in
|
|
188
|
-
//
|
|
257
|
+
// itself is configurable, so swap in an object with its own `jupyter`
|
|
258
|
+
// that inherits everything else from the real namespace. Not a Proxy:
|
|
259
|
+
// completion can't list a Proxy's properties, and Shift-Tab's
|
|
260
|
+
// side-effect-free evaluation can't read through one (#132). So
|
|
261
|
+
// Object.keys(Deno) and console.log(Deno) show only `jupyter`, and
|
|
262
|
+
// Deno's functions are called with `this` set to this object (they
|
|
263
|
+
// don't use it).
|
|
189
264
|
const real = globalThis.Deno;
|
|
190
265
|
const descriptor = Object.getOwnPropertyDescriptor(globalThis, "Deno");
|
|
191
|
-
const
|
|
192
|
-
|
|
266
|
+
const shim = Object.create(real, {
|
|
267
|
+
jupyter: { value: jupyter.namespace, enumerable: true, configurable: true, writable: true },
|
|
193
268
|
});
|
|
194
|
-
Object.defineProperty(globalThis, "Deno", { ...descriptor, value:
|
|
269
|
+
Object.defineProperty(globalThis, "Deno", { ...descriptor, value: shim });
|
|
195
270
|
} else if (process.env.CDPK_DENO_COMPAT === "1" && globalThis.Deno === undefined) {
|
|
196
271
|
// Opt-in (install --deno-compat): a Deno namespace for notebooks written
|
|
197
272
|
// against `deno jupyter`. See runtime/deno-compat.mjs for the trade-off.
|
|
198
273
|
// Absolute URL: the resolve hook re-parents this module's relative
|
|
199
274
|
// imports to the notebook directory.
|
|
200
|
-
const { createDenoShim } = await import(new URL("./deno-compat.mjs", import.meta.url).href);
|
|
201
|
-
|
|
275
|
+
const { createDenoShim, explainMissing } = await import(new URL("./deno-compat.mjs", import.meta.url).href);
|
|
276
|
+
const shim = createDenoShim({ jupyter: jupyter.namespace });
|
|
277
|
+
globalThis.Deno = shim;
|
|
278
|
+
explainError = (text) => explainMissing(text, shim);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* An error's stack, with its first line naming the error. Bun now and then
|
|
283
|
+
* (about one throw in twenty, #216) records a stack whose first line is a
|
|
284
|
+
* bare "Error", though name and message are set; the frames are right, so
|
|
285
|
+
* only the header is rebuilt.
|
|
286
|
+
*/
|
|
287
|
+
function stackOf(err) {
|
|
288
|
+
const stack = err.stack;
|
|
289
|
+
if (typeof stack !== "string") return "";
|
|
290
|
+
const nl = stack.indexOf("\n");
|
|
291
|
+
const first = nl === -1 ? stack : stack.slice(0, nl);
|
|
292
|
+
if (first !== "Error" || !err.message) return stack;
|
|
293
|
+
return `${err.name}: ${err.message}${nl === -1 ? "" : stack.slice(nl)}`;
|
|
202
294
|
}
|
|
203
295
|
|
|
204
296
|
// An error thrown later by a cell's timer or promise must not kill the kernel.
|
|
205
297
|
function reportUncaught(kind, err) {
|
|
206
|
-
writeErr(`${kind}: ${err instanceof Error ? (err
|
|
298
|
+
writeErr(`${kind}: ${explainError(err instanceof Error ? (stackOf(err) || err.message) : inspect(err))}\n`);
|
|
207
299
|
}
|
|
208
300
|
if (isDeno) {
|
|
209
301
|
globalThis.addEventListener("unhandledrejection", (e) => {
|
|
@@ -222,6 +314,29 @@ if (isDeno) {
|
|
|
222
314
|
// Keep the event loop alive between cells.
|
|
223
315
|
setInterval(() => {}, 1 << 30);
|
|
224
316
|
|
|
317
|
+
// A host killed by SIGKILL can't stop its runtime, and nothing visible to
|
|
318
|
+
// JS here changes when its inspector sessions drop, so watch for the host
|
|
319
|
+
// itself going: reparented, or no longer there (#219). Bun exits on its
|
|
320
|
+
// channel's EOF instead.
|
|
321
|
+
if (!isBun) {
|
|
322
|
+
// Held here, so a cell that reassigns them can't break the watch.
|
|
323
|
+
const proc = process;
|
|
324
|
+
const exit = process.exit.bind(process);
|
|
325
|
+
const host = proc.ppid;
|
|
326
|
+
const hostGone = () => {
|
|
327
|
+
if (proc.ppid !== host) return true;
|
|
328
|
+
try {
|
|
329
|
+
proc.kill(host, 0);
|
|
330
|
+
return false;
|
|
331
|
+
} catch (err) {
|
|
332
|
+
return err?.code === "ESRCH";
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
setInterval(() => {
|
|
336
|
+
if (hostGone()) exit(1);
|
|
337
|
+
}, 2000);
|
|
338
|
+
}
|
|
339
|
+
|
|
225
340
|
if (isBun) {
|
|
226
341
|
const { startChannel } = await import("./bun-channel.mjs");
|
|
227
342
|
const { createHandler } = await import("./evaluator.mjs");
|
|
@@ -230,47 +345,123 @@ if (isBun) {
|
|
|
230
345
|
});
|
|
231
346
|
}
|
|
232
347
|
|
|
348
|
+
function writeAllSync(stream, bytes) {
|
|
349
|
+
while (bytes.length > 0) bytes = bytes.subarray(stream.writeSync(bytes));
|
|
350
|
+
}
|
|
351
|
+
|
|
233
352
|
async function platform() {
|
|
234
353
|
if (isDeno) {
|
|
235
|
-
const stdinBuf = new Uint8Array(1);
|
|
236
354
|
return {
|
|
237
355
|
inspect: (v) => Deno.inspect(v, { colors: true, depth: 4 }),
|
|
238
356
|
inspectPlain: (v) => Deno.inspect(v, { colors: false, depth: 2 }),
|
|
239
357
|
readFile: (p) => Deno.readFileSync(p),
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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,
|
|
243
362
|
};
|
|
244
363
|
}
|
|
245
364
|
const util = await import("node:util");
|
|
246
365
|
const fs = await import("node:fs");
|
|
247
|
-
|
|
366
|
+
// Node writes to a pipe without blocking and queues what doesn't fit for
|
|
367
|
+
// the event loop to flush. input() blocks the main thread in readSync, so
|
|
368
|
+
// queued output, and the input_request's barrier marker after it, would
|
|
369
|
+
// never reach the host (#131). Blocking writes finish before readSync
|
|
370
|
+
// starts, as Node already does on Windows. Bun has no _handle and needs
|
|
371
|
+
// its own fix (#180).
|
|
372
|
+
if (isBun) await syncBunOutput();
|
|
373
|
+
else for (const stream of [process.stdout, process.stderr]) stream._handle?.setBlocking?.(true);
|
|
248
374
|
return {
|
|
249
375
|
inspect: (v) => util.inspect(v, { colors: true, depth: 4 }),
|
|
250
376
|
inspectPlain: (v) => util.inspect(v, { colors: false, depth: 2 }),
|
|
251
377
|
readFile: (p) => fs.readFileSync(p),
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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);
|
|
263
389
|
}
|
|
264
|
-
}
|
|
390
|
+
}
|
|
391
|
+
},
|
|
265
392
|
};
|
|
266
393
|
}
|
|
267
394
|
|
|
268
|
-
|
|
395
|
+
// Bun makes stdout and stderr non-blocking once process.stdout or
|
|
396
|
+
// process.stderr exists, and then queues what doesn't fit in the pipe for
|
|
397
|
+
// its event loop, console.log's output included. input() blocks the main
|
|
398
|
+
// thread in readSync, so queued output, and the input_request's barrier
|
|
399
|
+
// marker after it, would never reach the host (#180). Here every write
|
|
400
|
+
// finishes before it returns: process.stdout.write and process.stderr.write
|
|
401
|
+
// write through, and console is Node's Console over them, so console output
|
|
402
|
+
// is formatted as on Node.
|
|
403
|
+
async function syncBunOutput() {
|
|
404
|
+
const { writeAll } = await import("./bun-channel.mjs");
|
|
405
|
+
const { Console } = await import("node:console");
|
|
406
|
+
for (const [stream, fd] of [[process.stdout, 1], [process.stderr, 2]]) {
|
|
407
|
+
stream.write = (chunk, encoding, callback) => {
|
|
408
|
+
if (typeof encoding === "function") [callback, encoding] = [encoding, undefined];
|
|
409
|
+
writeAll(fd, typeof chunk === "string" ? Buffer.from(chunk, encoding) : chunk);
|
|
410
|
+
if (callback) process.nextTick(callback);
|
|
411
|
+
return true;
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
const sync = new Console({ stdout: process.stdout, stderr: process.stderr, colorMode: true });
|
|
415
|
+
for (const key of Object.keys(console)) {
|
|
416
|
+
if (typeof sync[key] === "function") console[key] = sync[key].bind(sync);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
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() {
|
|
269
437
|
const bytes = [];
|
|
270
438
|
for (;;) {
|
|
271
|
-
const byte =
|
|
439
|
+
const byte = readExactly(1)?.[0] ?? null;
|
|
272
440
|
if (byte === null) return bytes.length ? new TextDecoder().decode(new Uint8Array(bytes)) : null;
|
|
273
441
|
if (byte === 0x0a) return new TextDecoder().decode(new Uint8Array(bytes));
|
|
274
442
|
bytes.push(byte);
|
|
275
443
|
}
|
|
276
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
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
import type { Handler } from "./evaluator.mjs";
|
|
2
2
|
|
|
3
3
|
export function startChannel(handle: Handler, ready: { [key: string]: unknown }, fd?: number): void;
|
|
4
|
+
|
|
5
|
+
/** Writes all of `text` to `fd` before returning, even if the fd is non-blocking. */
|
|
6
|
+
export function writeAll(fd: number, text: string | Uint8Array): void;
|
package/runtime/bun-channel.mjs
CHANGED
|
@@ -14,8 +14,8 @@ import fs from "node:fs";
|
|
|
14
14
|
const CHANNEL_FD = 3;
|
|
15
15
|
|
|
16
16
|
/** Synchronous, like a CDP binding call, so output order is kept. */
|
|
17
|
-
function writeAll(fd, text) {
|
|
18
|
-
const bytes = Buffer.from(text);
|
|
17
|
+
export function writeAll(fd, text) {
|
|
18
|
+
const bytes = typeof text === "string" ? Buffer.from(text) : Buffer.from(text.buffer, text.byteOffset, text.byteLength);
|
|
19
19
|
for (let offset = 0; offset < bytes.length;) {
|
|
20
20
|
try {
|
|
21
21
|
offset += fs.writeSync(fd, bytes, offset);
|
package/runtime/comm.mjs
CHANGED
|
@@ -89,7 +89,17 @@ export function createComms(send, newId = () => crypto.randomUUID()) {
|
|
|
89
89
|
if (msgType === "comm_open") {
|
|
90
90
|
const handler = targets.get(content.target_name);
|
|
91
91
|
if (!handler) return false;
|
|
92
|
-
|
|
92
|
+
const comm = track(content.comm_id, content.target_name);
|
|
93
|
+
try {
|
|
94
|
+
await handler(comm, msg);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
// The host answers a failed comm_open with comm_close, so the comm
|
|
97
|
+
// is gone on the frontend: drop it here too, without running close
|
|
98
|
+
// handlers or sending anything (#137).
|
|
99
|
+
comm.closed = true;
|
|
100
|
+
forget(comm.id);
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
93
103
|
return true;
|
|
94
104
|
}
|
|
95
105
|
const comm = comms.get(content.comm_id);
|
|
@@ -114,3 +114,5 @@ export interface DenoShim {
|
|
|
114
114
|
|
|
115
115
|
export function denoError<E>(err: E): E | NotFound | AlreadyExists | PermissionDenied | IsADirectory | NotADirectory;
|
|
116
116
|
export function createDenoShim(options: { jupyter: unknown }): DenoShim;
|
|
117
|
+
/** Adds the cause to "Deno.x is not a function" when `shim` lacks `x`. */
|
|
118
|
+
export function explainMissing(message: string, shim: object): string;
|
package/runtime/deno-compat.mjs
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
// shim installed they take their Deno code paths, which may call APIs not
|
|
8
8
|
// covered here (Deno.serve, Deno.Command, Deno.openKv, permissions...).
|
|
9
9
|
// Uncovered APIs are simply absent rather than throwing on access, so
|
|
10
|
-
// feature checks like `typeof Deno.serve === "function"` still work.
|
|
10
|
+
// feature checks like `typeof Deno.serve === "function"` still work. The
|
|
11
|
+
// error from calling one says so instead (explainMissing, #172).
|
|
11
12
|
|
|
12
13
|
import fs from "node:fs";
|
|
13
14
|
import fsp from "node:fs/promises";
|
|
@@ -100,6 +101,18 @@ const writeFlag = (options) => (options?.append ? "a" : options?.createNew ? "wx
|
|
|
100
101
|
const OS_NAMES = { darwin: "darwin", linux: "linux", win32: "windows", freebsd: "freebsd" };
|
|
101
102
|
const ARCH_NAMES = { arm64: "aarch64", x64: "x86_64" };
|
|
102
103
|
|
|
104
|
+
const MEMBERS_DOC = "https://cdp-kernel.org/docs/jupyter-api#notebooks-written-for-deno";
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Adds the cause to V8's "Deno.serve is not a function" when `shim` lacks
|
|
108
|
+
* that member (#172), or returns `message` unchanged.
|
|
109
|
+
*/
|
|
110
|
+
export function explainMissing(message, shim) {
|
|
111
|
+
return message.replace(/\b(Deno\.([\w$]+)) is not a (?:function|constructor)\b/, (all, expr, name) =>
|
|
112
|
+
name in shim ? all : `${all}: ${expr} is not implemented by cdp-kernel's --deno-compat namespace (see ${MEMBERS_DOC})`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
103
116
|
export function createDenoShim({ jupyter }) {
|
|
104
117
|
return {
|
|
105
118
|
jupyter,
|
package/runtime/display.mjs
CHANGED
|
@@ -180,7 +180,14 @@ export function createJupyter(host) {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
function image(obj) {
|
|
183
|
-
if (typeof obj === "string")
|
|
183
|
+
if (typeof obj === "string") {
|
|
184
|
+
// As Deno does: a path that can't be read gets the TypeError below.
|
|
185
|
+
try {
|
|
186
|
+
obj = host.readFile(obj);
|
|
187
|
+
} catch {
|
|
188
|
+
// fall through
|
|
189
|
+
}
|
|
190
|
+
}
|
|
184
191
|
if (isJpg(obj)) return { [$display]: () => ({ "image/jpeg": host.base64(obj) }) };
|
|
185
192
|
if (isPng(obj)) return { [$display]: () => ({ "image/png": host.base64(obj) }) };
|
|
186
193
|
throw new TypeError(
|
package/runtime/introspect.mjs
CHANGED
|
@@ -77,7 +77,7 @@ function safeSource(fn) {
|
|
|
77
77
|
function propertyNames(value) {
|
|
78
78
|
if ((typeof value !== "object" && typeof value !== "function") || value === null) return [];
|
|
79
79
|
try {
|
|
80
|
-
// A function's own internals aren't worth listing; older V8 (Node 24)
|
|
80
|
+
// A function's own internals aren't worth listing; older V8 (Node 22 and 24)
|
|
81
81
|
// also gives sloppy-mode functions own `arguments` and `caller`.
|
|
82
82
|
const internal = ["length", "name", "prototype", "arguments", "caller"];
|
|
83
83
|
return Object.getOwnPropertyNames(value).filter((n) => !(typeof value === "function" && internal.includes(n)));
|
package/runtime/node-hooks.d.mts
CHANGED
|
@@ -8,12 +8,24 @@ export function parsePackageSpecifier(spec: string): PackageSpecifier;
|
|
|
8
8
|
export function jsrToNpm(spec: string): PackageSpecifier;
|
|
9
9
|
export function aliasFor(name: string, range: string): string;
|
|
10
10
|
export type Installer = "npm" | "bun";
|
|
11
|
+
/** Where `installer` keeps its packages: $XDG_CACHE_HOME/cdp-kernel/<installer>, or CDPK_CACHE_DIR for npm. */
|
|
12
|
+
export function cacheDirPath(
|
|
13
|
+
installer: Installer,
|
|
14
|
+
env?: { [name: string]: string | undefined },
|
|
15
|
+
home?: string,
|
|
16
|
+
): string;
|
|
11
17
|
export function installCommand(
|
|
12
18
|
installer: Installer,
|
|
13
19
|
alias: string,
|
|
14
20
|
name: string,
|
|
15
21
|
range: string,
|
|
16
22
|
): { cmd: string; args: string[]; env?: { [name: string]: string } };
|
|
23
|
+
/** How to spawn an install command; on Windows, npm runs through cmd.exe. */
|
|
24
|
+
export function spawnCommand(
|
|
25
|
+
cmd: string,
|
|
26
|
+
args: string[],
|
|
27
|
+
platform?: string,
|
|
28
|
+
): { cmd: string; args: string[]; shell: boolean };
|
|
17
29
|
/** Runs an install; `env` is added to the process environment. */
|
|
18
30
|
export type InstallRunner = (
|
|
19
31
|
cmd: string,
|
|
@@ -26,6 +38,21 @@ export function ensureInstalled(
|
|
|
26
38
|
installer?: Installer,
|
|
27
39
|
options?: { lockFile?: string | null; run?: InstallRunner },
|
|
28
40
|
): { dir: string; alias: string };
|
|
41
|
+
/** ensureInstalled without blocking the event loop; `run` may return a promise. */
|
|
42
|
+
export function ensureInstalledAsync(
|
|
43
|
+
pkg: { name: string; range: string },
|
|
44
|
+
installer?: Installer,
|
|
45
|
+
options?: {
|
|
46
|
+
lockFile?: string | null;
|
|
47
|
+
run?: (...args: Parameters<InstallRunner>) => ReturnType<InstallRunner> | Promise<ReturnType<InstallRunner>>;
|
|
48
|
+
},
|
|
49
|
+
): Promise<{ dir: string; alias: string }>;
|
|
50
|
+
/** Where a package is installed in the cache, or null; never installs. */
|
|
51
|
+
export function installedPackageDir(
|
|
52
|
+
pkg: { name: string; range: string },
|
|
53
|
+
installer?: Installer,
|
|
54
|
+
options?: { lockFile?: string | null },
|
|
55
|
+
): { dir: string; alias: string } | null;
|
|
29
56
|
|
|
30
57
|
export const LOCK_FILE: string;
|
|
31
58
|
export interface Lock {
|
|
@@ -36,4 +63,25 @@ export function findLockFile(dir: string): string | null;
|
|
|
36
63
|
export function readLock(path: string): Lock;
|
|
37
64
|
export function writeLock(path: string, lock: Lock): void;
|
|
38
65
|
export function withInstallLock<T>(dir: string, fn: () => T, options?: { timeoutMs?: number }): T;
|
|
66
|
+
export function withInstallLockAsync<T>(dir: string, fn: () => T | Promise<T>, options?: { timeoutMs?: number }): Promise<T>;
|
|
67
|
+
/** Compiles one TypeScript or JSX file with the esbuild binary at `binary`; `run` is replaceable in tests. */
|
|
68
|
+
export function compileTypeScript(
|
|
69
|
+
source: string,
|
|
70
|
+
filename: string,
|
|
71
|
+
binary: string,
|
|
72
|
+
run?: (cmd: string, args: string[], options: object) => { status: number | null; stdout: string; stderr: string; error?: Error },
|
|
73
|
+
loader?: "ts" | "tsx" | "jsx",
|
|
74
|
+
): string;
|
|
75
|
+
export interface LoadResult {
|
|
76
|
+
format?: string | null;
|
|
77
|
+
source?: string | ArrayBuffer | ArrayBufferView | null;
|
|
78
|
+
shortCircuit?: boolean;
|
|
79
|
+
}
|
|
80
|
+
/** The load hook: Node's own result unless its type stripping would reject the file; .tsx/.jsx always compiled. */
|
|
81
|
+
export function loadTypeScript(
|
|
82
|
+
url: string,
|
|
83
|
+
context: object,
|
|
84
|
+
nextLoad: (url: string, context: object) => LoadResult,
|
|
85
|
+
binary: string | undefined,
|
|
86
|
+
): LoadResult;
|
|
39
87
|
export function installNodeResolveHooks(urls: { shimUrl: string; replUrl: string }): void;
|