@tlockney/cdp-kernel 0.4.0 → 0.5.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 +60 -481
- 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 +6 -1
- package/dist/cdp.js +25 -10
- package/dist/celld-target.d.ts +1 -1
- package/dist/celld-target.js +46 -8
- package/dist/children.d.ts +7 -0
- package/dist/children.js +32 -0
- package/dist/debugger.d.ts +1 -1
- package/dist/debugger.js +21 -10
- package/dist/globals.d.ts +3 -3
- package/dist/globals.js +3 -3
- package/dist/kernel.js +163 -41
- package/dist/mod.d.ts +5 -3
- package/dist/mod.js +5 -3
- package/dist/package.d.ts +2 -2
- package/dist/package.js +3 -3
- package/dist/target.d.ts +31 -3
- package/dist/target.js +233 -32
- package/dist/text.d.ts +5 -1
- package/dist/text.js +55 -2
- package/dist/transform.d.ts +9 -0
- package/dist/transform.js +37 -9
- 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/ws-client.d.ts +5 -1
- package/dist/ws-client.js +46 -13
- package/dist/zmtp.d.ts +7 -1
- package/dist/zmtp.js +30 -1
- package/package.json +9 -3
- package/runtime/bootstrap.mjs +121 -10
- 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/node-hooks.d.mts +42 -0
- package/runtime/node-hooks.mjs +246 -41
- package/runtime/workerd-worker.mjs +77 -6
package/dist/target.js
CHANGED
|
@@ -4,17 +4,33 @@
|
|
|
4
4
|
import { Buffer } from "node:buffer";
|
|
5
5
|
import process from "node:process";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
|
+
import { createRequire } from "node:module";
|
|
7
8
|
import { join } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
8
10
|
import { asEvaluateResult, CdpAbandoned, CdpSession } from "./cdp.js";
|
|
9
11
|
import { SHIM, transformCell } from "./transform.js";
|
|
10
12
|
import { hostRuntime, isInsideNodeModules, runtimeDir } from "./package.js";
|
|
13
|
+
import { killOnExit } from "./children.js";
|
|
11
14
|
const BINDING = "__cdpk_send";
|
|
15
|
+
/** How long a target may take from spawn to a usable session. */
|
|
16
|
+
const START_TIMEOUT_MS = 60_000;
|
|
12
17
|
const MARKER_RE = /\x1b\[cdpk:(\d+)(m?)\]\x1b/;
|
|
13
18
|
const MAX_MARKER_LEN = 24;
|
|
14
19
|
// Lines the runtimes print to stderr on their own: inspector notices, and
|
|
15
20
|
// Deno's npm/jsr "Download https://..." progress (which `deno jupyter`
|
|
16
|
-
// sends to the server log rather than the notebook).
|
|
17
|
-
|
|
21
|
+
// sends to the server log rather than the notebook). Each entry is the
|
|
22
|
+
// line's start once colour codes are removed.
|
|
23
|
+
const INSPECTOR_CHATTER = [
|
|
24
|
+
"Debugger listening on ",
|
|
25
|
+
"Debugger attached.",
|
|
26
|
+
"For help, see: https://nodejs.org/",
|
|
27
|
+
"Visit chrome://inspect",
|
|
28
|
+
"Waiting for the debugger to disconnect",
|
|
29
|
+
];
|
|
30
|
+
const DENO_CHATTER = [...INSPECTOR_CHATTER, "Download http://", "Download https://"];
|
|
31
|
+
const ANSI_SGR = /\x1b\[[0-9;]*m/g;
|
|
32
|
+
/** A colour code cut off at the end of a chunk. */
|
|
33
|
+
const PARTIAL_SGR = /\x1b(?:\[[0-9;]*)?$/;
|
|
18
34
|
/** `bootstrap` is the local path of runtime/bootstrap.mjs (see runtimeDir). */
|
|
19
35
|
export function launchCommand(runtime, bootstrap, { executable, denoConfig } = {}) {
|
|
20
36
|
// The CDP WebSocket has no authentication, so its URL must stay private:
|
|
@@ -41,6 +57,33 @@ export function launchCommand(runtime, bootstrap, { executable, denoConfig } = {
|
|
|
41
57
|
const node = hostRuntime() === "node" ? process.execPath : "node";
|
|
42
58
|
return { cmd: executable ?? node, args: [...inspect, bootstrap] };
|
|
43
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* The esbuild binary this host's esbuild runs, for a Node target to compile
|
|
62
|
+
* imported TypeScript Node can't strip (#73); null when it can't be found.
|
|
63
|
+
* Resolved from esbuild's own location, so it's found in a node_modules
|
|
64
|
+
* install and in Deno's npm cache alike.
|
|
65
|
+
*/
|
|
66
|
+
export function esbuildBinary(resolve = import.meta.resolve, platform = process.platform, arch = process.arch) {
|
|
67
|
+
if (process.env.ESBUILD_BINARY_PATH)
|
|
68
|
+
return process.env.ESBUILD_BINARY_PATH;
|
|
69
|
+
const subpath = platform === "win32" ? "esbuild.exe" : "bin/esbuild";
|
|
70
|
+
try {
|
|
71
|
+
const main = fileURLToPath(resolve("esbuild"));
|
|
72
|
+
return createRequire(main).resolve(`@esbuild/${platform}-${arch}/${subpath}`);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** The parsed value, or undefined for text that isn't JSON. */
|
|
79
|
+
export function parseJson(text) {
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(text);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
44
87
|
export function isTargetMessage(value) {
|
|
45
88
|
if (typeof value !== "object" || value === null)
|
|
46
89
|
return false;
|
|
@@ -139,6 +182,66 @@ export class PipeDemux {
|
|
|
139
182
|
void this.#pump();
|
|
140
183
|
}
|
|
141
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Removes the runtime's own stderr lines (see INSPECTOR_CHATTER) and passes
|
|
187
|
+
* everything else on. A chatter line can arrive split across chunks, so a
|
|
188
|
+
* trailing partial line that could still turn out to be one is held back
|
|
189
|
+
* until its newline arrives; any other partial line passes straight through.
|
|
190
|
+
*/
|
|
191
|
+
export class StderrFilter {
|
|
192
|
+
#starts;
|
|
193
|
+
#emit;
|
|
194
|
+
#held = "";
|
|
195
|
+
#atLineStart = true;
|
|
196
|
+
constructor(runtime, emit) {
|
|
197
|
+
this.#starts = runtime === "deno" ? DENO_CHATTER : INSPECTOR_CHATTER;
|
|
198
|
+
this.#emit = emit;
|
|
199
|
+
}
|
|
200
|
+
push(chunk) {
|
|
201
|
+
let text = this.#held + chunk;
|
|
202
|
+
this.#held = "";
|
|
203
|
+
let out = "";
|
|
204
|
+
if (!this.#atLineStart) {
|
|
205
|
+
// The rest of a line that already went out unfiltered.
|
|
206
|
+
const nl = text.indexOf("\n");
|
|
207
|
+
if (nl === -1) {
|
|
208
|
+
this.#emit(text);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
out = text.slice(0, nl + 1);
|
|
212
|
+
text = text.slice(nl + 1);
|
|
213
|
+
}
|
|
214
|
+
const lines = text.split("\n");
|
|
215
|
+
const last = lines.pop();
|
|
216
|
+
for (const line of lines)
|
|
217
|
+
if (!this.#isChatter(line, false))
|
|
218
|
+
out += line + "\n";
|
|
219
|
+
if (last && this.#isChatter(last, true))
|
|
220
|
+
this.#held = last;
|
|
221
|
+
else
|
|
222
|
+
out += last;
|
|
223
|
+
this.#atLineStart = last === "" || this.#held !== "";
|
|
224
|
+
if (out)
|
|
225
|
+
this.#emit(out);
|
|
226
|
+
}
|
|
227
|
+
/** Releases a held partial line (the process died). */
|
|
228
|
+
flush() {
|
|
229
|
+
if (this.#held)
|
|
230
|
+
this.#emit(this.#held);
|
|
231
|
+
this.#held = "";
|
|
232
|
+
this.#atLineStart = true;
|
|
233
|
+
}
|
|
234
|
+
/** Whether `line` is chatter or, when `partial`, could still become chatter. */
|
|
235
|
+
#isChatter(line, partial) {
|
|
236
|
+
let plain = line.replace(ANSI_SGR, "");
|
|
237
|
+
if (partial)
|
|
238
|
+
plain = plain.replace(PARTIAL_SGR, "");
|
|
239
|
+
// Any other escape may be a PipeDemux marker, which must never be dropped or held.
|
|
240
|
+
if (plain.includes("\x1b"))
|
|
241
|
+
return false;
|
|
242
|
+
return this.#starts.some((start) => plain.startsWith(start) || (partial && start.startsWith(plain)));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
142
245
|
/** Promises keyed by barrier number, resolvable before or after first use. */
|
|
143
246
|
export class Gates {
|
|
144
247
|
#map = new Map();
|
|
@@ -167,6 +270,12 @@ export class Gates {
|
|
|
167
270
|
export class Interrupted extends Error {
|
|
168
271
|
name = "Interrupted";
|
|
169
272
|
}
|
|
273
|
+
/** A catch handler: a call interrupt() abandoned becomes Interrupted. */
|
|
274
|
+
function interrupted(what) {
|
|
275
|
+
return (err) => {
|
|
276
|
+
throw err instanceof CdpAbandoned ? new Interrupted(`${what} interrupted`) : err;
|
|
277
|
+
};
|
|
278
|
+
}
|
|
170
279
|
export class Target {
|
|
171
280
|
runtime;
|
|
172
281
|
transform = transformCell;
|
|
@@ -180,11 +289,20 @@ export class Target {
|
|
|
180
289
|
#messages = Promise.resolve();
|
|
181
290
|
#interrupted = false;
|
|
182
291
|
#paused = false;
|
|
292
|
+
/** Set once the main context is gone: the runtime is exiting (#130). */
|
|
293
|
+
#exiting = false;
|
|
294
|
+
#exited;
|
|
183
295
|
#baselineGlobals = new Set();
|
|
184
296
|
versions;
|
|
185
297
|
constructor(runtime, child, shell, control, stdout, stderr, gates, contextId, versions) {
|
|
186
298
|
this.runtime = runtime;
|
|
187
299
|
this.#child = child;
|
|
300
|
+
this.#exited = new Promise((resolve) => {
|
|
301
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
302
|
+
resolve();
|
|
303
|
+
else
|
|
304
|
+
child.once("exit", () => resolve());
|
|
305
|
+
});
|
|
188
306
|
this.#shell = shell;
|
|
189
307
|
this.#control = control;
|
|
190
308
|
this.#stdout = stdout;
|
|
@@ -208,18 +326,41 @@ export class Target {
|
|
|
208
326
|
env.CDPK_DENO_COMPAT = "1";
|
|
209
327
|
if (runtime === "deno" && options.denoConfig !== undefined)
|
|
210
328
|
env.CDPK_DENO_PROJECT = "1";
|
|
329
|
+
const esbuild = runtime === "node" ? esbuildBinary() : null;
|
|
330
|
+
if (esbuild !== null)
|
|
331
|
+
env.CDPK_ESBUILD_BINARY = esbuild;
|
|
211
332
|
delete env.NO_COLOR;
|
|
212
333
|
const child = spawn(cmd, args, { cwd: options.cwd, stdio: ["pipe", "pipe", "pipe"], env });
|
|
334
|
+
killOnExit(child);
|
|
335
|
+
// A start that fails or stalls must not leave the runtime running: its
|
|
336
|
+
// pipes and inspector sockets would keep this process alive (#52).
|
|
337
|
+
const sessions = [];
|
|
338
|
+
const timeoutMs = options.startTimeoutMs ?? START_TIMEOUT_MS;
|
|
339
|
+
let timer;
|
|
340
|
+
const stalled = new Promise((_, reject) => {
|
|
341
|
+
timer = setTimeout(() => reject(new Error(`${cmd} did not start within ${timeoutMs / 1000}s`)), timeoutMs);
|
|
342
|
+
});
|
|
343
|
+
try {
|
|
344
|
+
return await Promise.race([Target.#start(runtime, events, cmd, child, sessions), stalled]);
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
for (const session of sessions)
|
|
348
|
+
session.close();
|
|
349
|
+
child.kill();
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
352
|
+
finally {
|
|
353
|
+
clearTimeout(timer);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
static async #start(runtime, events, cmd, child, sessions) {
|
|
213
357
|
child.stdout.setEncoding("utf8");
|
|
214
358
|
child.stderr.setEncoding("utf8");
|
|
215
359
|
const gates = new Gates();
|
|
216
360
|
const stdout = new PipeDemux((text) => events.stream("stdout", text), (n) => gates.wait(n));
|
|
217
361
|
const stderr = new PipeDemux((text) => events.stream("stderr", text), (n) => gates.wait(n));
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
if (text)
|
|
221
|
-
stderr.push(text);
|
|
222
|
-
};
|
|
362
|
+
const filter = new StderrFilter(runtime, (text) => stderr.push(text));
|
|
363
|
+
const pushStderr = (chunk) => filter.push(chunk);
|
|
223
364
|
// Until the inspector URL appears, stderr carries the runtime's banner.
|
|
224
365
|
const wsUrl = await new Promise((resolve, reject) => {
|
|
225
366
|
let banner = "";
|
|
@@ -240,12 +381,15 @@ export class Target {
|
|
|
240
381
|
child.stdout.on("data", (c) => stdout.push(c));
|
|
241
382
|
child.once("exit", (code) => {
|
|
242
383
|
gates.openAll();
|
|
384
|
+
filter.flush();
|
|
243
385
|
stdout.close();
|
|
244
386
|
stderr.close();
|
|
245
387
|
events.exit(code);
|
|
246
388
|
});
|
|
247
389
|
const shell = await CdpSession.connect(wsUrl);
|
|
390
|
+
sessions.push(shell);
|
|
248
391
|
const control = await CdpSession.connect(wsUrl);
|
|
392
|
+
sessions.push(control);
|
|
249
393
|
const contextReady = new Promise((resolve) => {
|
|
250
394
|
shell.on("Runtime.executionContextCreated", (params) => {
|
|
251
395
|
const context = params.context;
|
|
@@ -257,21 +401,33 @@ export class Target {
|
|
|
257
401
|
await shell.send("Runtime.addBinding", { name: BINDING });
|
|
258
402
|
const contextId = await contextReady;
|
|
259
403
|
const target = new Target(runtime, child, shell, control, stdout, stderr, gates, contextId, { runtime: "", engine: "" });
|
|
404
|
+
// process.exit() or Deno.exit() destroys the context, then the runtime
|
|
405
|
+
// waits for its inspector clients to disconnect before it exits. Let go,
|
|
406
|
+
// so it exits and the kernel starts a fresh one (#130).
|
|
407
|
+
shell.on("Runtime.executionContextDestroyed", (params) => {
|
|
408
|
+
if (params.executionContextId === contextId)
|
|
409
|
+
target.#onExit();
|
|
410
|
+
});
|
|
260
411
|
shell.on("Runtime.bindingCalled", (params) => {
|
|
261
412
|
if (params.name !== BINDING || typeof params.payload !== "string")
|
|
262
413
|
return;
|
|
263
|
-
|
|
414
|
+
// __cdpk_send is an ordinary global, so a cell can call it with
|
|
415
|
+
// anything; that mustn't take the host down (#122).
|
|
416
|
+
const msg = parseJson(params.payload);
|
|
264
417
|
if (isTargetMessage(msg))
|
|
265
418
|
target.#deliver(msg, events);
|
|
266
419
|
});
|
|
267
|
-
// Wait for the bootstrap's top-level await to finish installing the
|
|
268
|
-
|
|
269
|
-
|
|
420
|
+
// Wait for the bootstrap's top-level await to finish installing the
|
|
421
|
+
// shim. There is no cap here: launch's start timeout bounds the wait and
|
|
422
|
+
// kills the child, so a slow bootstrap never yields a target without
|
|
423
|
+
// the shim (#134).
|
|
424
|
+
for (;;) {
|
|
425
|
+
const probe = await target.#evaluateSync(`typeof globalThis.${SHIM}?.tick === "function"`);
|
|
270
426
|
if (probe.result.value === true)
|
|
271
427
|
break;
|
|
272
428
|
await new Promise((r) => setTimeout(r, 25));
|
|
273
429
|
}
|
|
274
|
-
const versions = await target.#
|
|
430
|
+
const versions = await target.#evaluateSync(`JSON.stringify(typeof Deno !== "undefined" ? { runtime: Deno.version.deno, engine: "V8 " + Deno.version.v8 } : { runtime: process.versions.node, engine: "V8 " + process.versions.v8 })`);
|
|
275
431
|
Object.assign(target.versions, JSON.parse(String(versions.result.value)));
|
|
276
432
|
// The runtime's own enumerable globals, before any cell: the debugger's
|
|
277
433
|
// variable views show only what cells added (#18).
|
|
@@ -304,11 +460,23 @@ export class Target {
|
|
|
304
460
|
}
|
|
305
461
|
/** Resolves once all output and messages sent so far have been delivered. */
|
|
306
462
|
async flush() {
|
|
307
|
-
const n = await this.#
|
|
463
|
+
const n = await this.#evaluateSync(`${SHIM}.barrier()`).catch(interrupted("Execution"));
|
|
308
464
|
const barrier = Number(n.result.value);
|
|
309
465
|
await Promise.all([this.#stdout.waitFor(barrier), this.#stderr.waitFor(barrier)]);
|
|
310
466
|
await this.#messages;
|
|
311
467
|
}
|
|
468
|
+
/**
|
|
469
|
+
* For expressions that return a value without awaiting. With replMode or
|
|
470
|
+
* awaitPromise, the inspector holds the result promise only weakly until
|
|
471
|
+
* its then-callback runs, even when the expression returns a plain value:
|
|
472
|
+
* a GC in that window fails the call with "Promise was collected" (#50).
|
|
473
|
+
* A plain evaluate returns the value directly.
|
|
474
|
+
*/
|
|
475
|
+
#evaluateSync(expression) {
|
|
476
|
+
return this.#shell
|
|
477
|
+
.send("Runtime.evaluate", { expression, contextId: this.#contextId, returnByValue: true })
|
|
478
|
+
.then(asEvaluateResult);
|
|
479
|
+
}
|
|
312
480
|
#evaluateRaw(expression, session = this.#shell) {
|
|
313
481
|
return session
|
|
314
482
|
.send("Runtime.evaluate", { expression, contextId: this.#contextId, replMode: true, awaitPromise: true })
|
|
@@ -323,9 +491,27 @@ export class Target {
|
|
|
323
491
|
catch (err) {
|
|
324
492
|
if (err instanceof CdpAbandoned || this.#interrupted)
|
|
325
493
|
throw new Interrupted("Execution interrupted");
|
|
494
|
+
if (this.#exiting) {
|
|
495
|
+
// The child's exit, which drops this target in the kernel, comes
|
|
496
|
+
// first, so the next cell starts a fresh one.
|
|
497
|
+
await this.#exited;
|
|
498
|
+
throw new Error(`${this.runtime} exited`);
|
|
499
|
+
}
|
|
326
500
|
throw err;
|
|
327
501
|
}
|
|
328
502
|
}
|
|
503
|
+
#onExit() {
|
|
504
|
+
if (this.#exiting)
|
|
505
|
+
return;
|
|
506
|
+
this.#exiting = true;
|
|
507
|
+
this.#shell.close();
|
|
508
|
+
this.#control.close();
|
|
509
|
+
// In case it still doesn't exit once both sessions are gone.
|
|
510
|
+
setTimeout(() => {
|
|
511
|
+
if (this.#child.exitCode === null && this.#child.signalCode === null)
|
|
512
|
+
this.#child.kill("SIGKILL");
|
|
513
|
+
}, 2000).unref();
|
|
514
|
+
}
|
|
329
515
|
/**
|
|
330
516
|
* Deno calls IsolateHandle::terminate_execution from another thread. The
|
|
331
517
|
* CDP equivalent is Runtime.terminateExecution, sent on a second session
|
|
@@ -341,11 +527,27 @@ export class Target {
|
|
|
341
527
|
await this.#control.send("Runtime.terminateExecution");
|
|
342
528
|
this.#shell.abandonPending();
|
|
343
529
|
}
|
|
530
|
+
/**
|
|
531
|
+
* For synchronous functions. With awaitPromise the inspector would wrap
|
|
532
|
+
* even a plain return value in a promise it holds only weakly (#55).
|
|
533
|
+
*/
|
|
344
534
|
callFunctionOn(functionDeclaration, args) {
|
|
345
535
|
return this.#shell.send("Runtime.callFunctionOn", {
|
|
346
536
|
functionDeclaration,
|
|
347
537
|
arguments: args,
|
|
348
538
|
executionContextId: this.#contextId,
|
|
539
|
+
returnByValue: true,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* For functions that return a promise. The target holds it until after
|
|
544
|
+
* the inspector's then-callback has run, so a GC can't collect it first.
|
|
545
|
+
*/
|
|
546
|
+
#callAsyncFunctionOn(functionDeclaration, args) {
|
|
547
|
+
return this.#shell.send("Runtime.callFunctionOn", {
|
|
548
|
+
functionDeclaration: `function (...args) { return globalThis.${SHIM}.hold((${functionDeclaration}).apply(this, args)); }`,
|
|
549
|
+
arguments: args,
|
|
550
|
+
executionContextId: this.#contextId,
|
|
349
551
|
awaitPromise: true,
|
|
350
552
|
returnByValue: true,
|
|
351
553
|
});
|
|
@@ -357,10 +559,12 @@ export class Target {
|
|
|
357
559
|
: result.unserializableValue !== undefined
|
|
358
560
|
? { unserializableValue: result.unserializableValue }
|
|
359
561
|
: { value: result.value };
|
|
360
|
-
|
|
562
|
+
// An async $display method can still be running when interrupt()
|
|
563
|
+
// abandons the call (#135).
|
|
564
|
+
await this.#callAsyncFunctionOn(`function (n, r) { return globalThis.${SHIM}.broadcastResult(n, r); }`, [
|
|
361
565
|
{ value: executionCount },
|
|
362
566
|
arg,
|
|
363
|
-
]);
|
|
567
|
+
]).catch(interrupted("Execution"));
|
|
364
568
|
}
|
|
365
569
|
/**
|
|
366
570
|
* Hands a frontend comm message to the target's comms. Resolves to
|
|
@@ -371,18 +575,12 @@ export class Target {
|
|
|
371
575
|
this.#interrupted = false;
|
|
372
576
|
let res;
|
|
373
577
|
try {
|
|
374
|
-
res = await this.#
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
{ value: buffers.map((b) => Buffer.from(b).toString("base64")) },
|
|
381
|
-
],
|
|
382
|
-
executionContextId: this.#contextId,
|
|
383
|
-
awaitPromise: true,
|
|
384
|
-
returnByValue: true,
|
|
385
|
-
});
|
|
578
|
+
res = await this.#callAsyncFunctionOn(`function (t, c, m, b) { return globalThis.${SHIM}.commDispatch(t, c, m, b); }`, [
|
|
579
|
+
{ value: msgType },
|
|
580
|
+
{ value: content },
|
|
581
|
+
{ value: metadata },
|
|
582
|
+
{ value: buffers.map((b) => Buffer.from(b).toString("base64")) },
|
|
583
|
+
]);
|
|
386
584
|
}
|
|
387
585
|
catch (err) {
|
|
388
586
|
if (err instanceof CdpAbandoned || this.#interrupted)
|
|
@@ -407,12 +605,15 @@ export class Target {
|
|
|
407
605
|
/**
|
|
408
606
|
* Evaluates without side effects: V8 throws instead of running anything
|
|
409
607
|
* that would change state (a call, a getter that writes), so completing
|
|
410
|
-
* or inspecting `obj.` never runs user code (#10).
|
|
608
|
+
* or inspecting `obj.` never runs user code (#10). Not replMode, which
|
|
609
|
+
* returns a promise the inspector holds only weakly (#55); a plain
|
|
610
|
+
* evaluate sees the cells' let and const too.
|
|
411
611
|
*/
|
|
412
612
|
#evaluatePure(expression) {
|
|
413
613
|
return this.#shell
|
|
414
|
-
.send("Runtime.evaluate", { expression, contextId: this.#contextId,
|
|
415
|
-
.then(asEvaluateResult
|
|
614
|
+
.send("Runtime.evaluate", { expression, contextId: this.#contextId, throwOnSideEffect: true })
|
|
615
|
+
.then(asEvaluateResult)
|
|
616
|
+
.catch(() => null);
|
|
416
617
|
}
|
|
417
618
|
async propertyNames(expression) {
|
|
418
619
|
const evaluated = await this.#evaluatePure(`(${expression})`);
|
|
@@ -444,8 +645,8 @@ export class Target {
|
|
|
444
645
|
const res = await this.#shell.send("Runtime.globalLexicalScopeNames", { executionContextId: this.#contextId });
|
|
445
646
|
return res.names ?? [];
|
|
446
647
|
}
|
|
447
|
-
replyInput(value) {
|
|
448
|
-
this.#child.stdin.write(JSON.stringify({ value }) + "\n");
|
|
648
|
+
replyInput(value, interrupted = false) {
|
|
649
|
+
this.#child.stdin.write(JSON.stringify({ value, interrupted }) + "\n");
|
|
449
650
|
}
|
|
450
651
|
kill() {
|
|
451
652
|
this.#shell.close();
|
package/dist/text.d.ts
CHANGED
|
@@ -6,7 +6,11 @@ export type IsCompleteReply = {
|
|
|
6
6
|
} | {
|
|
7
7
|
status: "invalid";
|
|
8
8
|
};
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Bracket/quote/comment scanner deciding whether a cell is finished. Regex
|
|
11
|
+
* literals are recognised heuristically (see startsRegex), and a template's
|
|
12
|
+
* `${...}` is skipped like the rest of the template.
|
|
13
|
+
*/
|
|
10
14
|
export declare function checkIsComplete(code: string): IsCompleteReply;
|
|
11
15
|
/** The dotted expression ending at the cursor, e.g. "Math.fl". */
|
|
12
16
|
export declare function exprAtCursor(code: string, cursor: number): string;
|
package/dist/text.js
CHANGED
|
@@ -2,7 +2,53 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Ported from Deno's cli/js/jupyter_kernel.js (v2.9.6), which is
|
|
4
4
|
// Copyright 2018-2026 the Deno authors, MIT license.
|
|
5
|
-
|
|
5
|
+
// Keywords after which a `/` starts a regex literal rather than dividing.
|
|
6
|
+
const REGEX_AFTER_WORD = new Set([
|
|
7
|
+
"return", "typeof", "instanceof", "in", "of", "new", "delete", "void", "throw",
|
|
8
|
+
"case", "do", "else", "yield", "await",
|
|
9
|
+
]);
|
|
10
|
+
/**
|
|
11
|
+
* Whether a `/` at `i` (not starting a comment) begins a regex literal: the
|
|
12
|
+
* usual heuristic, true unless the previous token can end an operand (a
|
|
13
|
+
* name, number, closing bracket or string). It can't tell `a++ / b` or a
|
|
14
|
+
* block's `} /re/` apart; those are rare enough in a console.
|
|
15
|
+
*/
|
|
16
|
+
function startsRegex(code, i) {
|
|
17
|
+
let j = i - 1;
|
|
18
|
+
while (j >= 0 && /\s/.test(code[j]))
|
|
19
|
+
j--;
|
|
20
|
+
if (j < 0)
|
|
21
|
+
return true;
|
|
22
|
+
const prev = code[j];
|
|
23
|
+
if (/[\w$]/.test(prev)) {
|
|
24
|
+
const word = /[\w$]+$/.exec(code.slice(0, j + 1))?.[0] ?? "";
|
|
25
|
+
return REGEX_AFTER_WORD.has(word);
|
|
26
|
+
}
|
|
27
|
+
return !/[)\]}'"`]/.test(prev);
|
|
28
|
+
}
|
|
29
|
+
/** The index just past a regex literal starting at `i`, or -1 if it doesn't end on its line. */
|
|
30
|
+
function regexEnd(code, i) {
|
|
31
|
+
let inClass = false;
|
|
32
|
+
for (let j = i + 1; j < code.length; j++) {
|
|
33
|
+
const c = code[j];
|
|
34
|
+
if (c === "\n")
|
|
35
|
+
return -1;
|
|
36
|
+
if (c === "\\")
|
|
37
|
+
j++;
|
|
38
|
+
else if (c === "[")
|
|
39
|
+
inClass = true;
|
|
40
|
+
else if (c === "]")
|
|
41
|
+
inClass = false;
|
|
42
|
+
else if (c === "/" && !inClass)
|
|
43
|
+
return j + 1;
|
|
44
|
+
}
|
|
45
|
+
return -1;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Bracket/quote/comment scanner deciding whether a cell is finished. Regex
|
|
49
|
+
* literals are recognised heuristically (see startsRegex), and a template's
|
|
50
|
+
* `${...}` is skipped like the rest of the template.
|
|
51
|
+
*/
|
|
6
52
|
export function checkIsComplete(code) {
|
|
7
53
|
const stack = [];
|
|
8
54
|
const closers = { ")": "(", "]": "[", "}": "{" };
|
|
@@ -21,11 +67,18 @@ export function checkIsComplete(code) {
|
|
|
21
67
|
i = end + 2;
|
|
22
68
|
continue;
|
|
23
69
|
}
|
|
70
|
+
if (ch === "/" && startsRegex(code, i)) {
|
|
71
|
+
const end = regexEnd(code, i);
|
|
72
|
+
if (end !== -1) {
|
|
73
|
+
i = end;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
24
77
|
if (ch === "'" || ch === '"' || ch === "`") {
|
|
25
78
|
i++;
|
|
26
79
|
let closed = false;
|
|
27
80
|
while (i < code.length) {
|
|
28
|
-
if (code[i] === "\\"
|
|
81
|
+
if (code[i] === "\\") {
|
|
29
82
|
i += 2;
|
|
30
83
|
continue;
|
|
31
84
|
}
|
package/dist/transform.d.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { LineMap } from "./sourcemap.ts";
|
|
2
2
|
export declare const SHIM = "__cdpk";
|
|
3
|
+
/**
|
|
4
|
+
* Each replMode cell also ends by yielding one macrotask, so the script
|
|
5
|
+
* settles with no other microtasks queued ahead of the inspector's
|
|
6
|
+
* then-callback. Until that callback runs, the inspector holds the result
|
|
7
|
+
* promise only weakly, and a GC in between fails the evaluate with "Promise
|
|
8
|
+
* was collected" (#55). A declaration has no completion value, so the
|
|
9
|
+
* cell's value is unchanged; replMode lets `let` be declared again.
|
|
10
|
+
*/
|
|
11
|
+
export declare const SETTLE = "\n;let __cdpk_settled = await __cdpk.tick();";
|
|
3
12
|
export declare class CellSyntaxError extends Error {
|
|
4
13
|
name: string;
|
|
5
14
|
}
|
package/dist/transform.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// TypeScript/JSX part and acorn finds the module syntax that a script can't
|
|
7
7
|
// contain:
|
|
8
8
|
// import x, { y as z } from "m" -> var __cdpk_m0 = await __cdpk.import("m");
|
|
9
|
-
// var x = __cdpk_m0
|
|
9
|
+
// var x = __cdpk_m0["default"], z = __cdpk_m0["y"];
|
|
10
10
|
// import("m") -> __cdpk.import("m")
|
|
11
11
|
// import.meta -> __cdpk.meta
|
|
12
12
|
// export const a = 1 -> const a = 1
|
|
@@ -25,6 +25,15 @@ export const SHIM = "__cdpk";
|
|
|
25
25
|
// interrupt a busy loop. (Within that dispatch the inspector processes no
|
|
26
26
|
// other messages, so an interrupt would never be seen.)
|
|
27
27
|
const PRELUDE = `'use strict'; await ${SHIM}.tick();\n`;
|
|
28
|
+
/**
|
|
29
|
+
* Each replMode cell also ends by yielding one macrotask, so the script
|
|
30
|
+
* settles with no other microtasks queued ahead of the inspector's
|
|
31
|
+
* then-callback. Until that callback runs, the inspector holds the result
|
|
32
|
+
* promise only weakly, and a GC in between fails the evaluate with "Promise
|
|
33
|
+
* was collected" (#55). A declaration has no completion value, so the
|
|
34
|
+
* cell's value is unchanged; replMode lets `let` be declared again.
|
|
35
|
+
*/
|
|
36
|
+
export const SETTLE = `\n;let ${SHIM}_settled = await ${SHIM}.tick();`;
|
|
28
37
|
export class CellSyntaxError extends Error {
|
|
29
38
|
name = "SyntaxError";
|
|
30
39
|
}
|
|
@@ -34,8 +43,14 @@ function jsxSettings(jsx) {
|
|
|
34
43
|
}
|
|
35
44
|
return { jsx: "transform", jsxFactory: jsx?.factory, jsxFragment: jsx?.fragment };
|
|
36
45
|
}
|
|
46
|
+
// esbuild compiles `import x = require("m")` to a `require` call, which a
|
|
47
|
+
// cell doesn't have (#143); `import type x = require("m")` is erased.
|
|
48
|
+
const IMPORT_REQUIRE = /^\s*(?:export\s+)?import\s+(?!type\s)[\w$]+\s*=\s*require\s*\(/m;
|
|
37
49
|
/** esbuild's output, with its source map's `mappings` when asked for. */
|
|
38
50
|
function stripTypesMapped(code, options, sourcemap) {
|
|
51
|
+
if (IMPORT_REQUIRE.test(code)) {
|
|
52
|
+
throw new CellSyntaxError('import x = require("m") is not supported in a cell; use import x from "m"');
|
|
53
|
+
}
|
|
39
54
|
const compilerOptions = {
|
|
40
55
|
verbatimModuleSyntax: true,
|
|
41
56
|
...(options.experimentalDecorators ? { experimentalDecorators: true } : {}),
|
|
@@ -47,6 +62,9 @@ function stripTypesMapped(code, options, sourcemap) {
|
|
|
47
62
|
const result = transformSync(code, {
|
|
48
63
|
loader,
|
|
49
64
|
target: "esnext",
|
|
65
|
+
// Neither acorn nor V8 parses standard (TC39) decorators or
|
|
66
|
+
// `accessor` yet, so esbuild lowers them (#142).
|
|
67
|
+
supported: { decorators: false },
|
|
50
68
|
...jsxSettings(options.jsx),
|
|
51
69
|
tsconfigRaw: { compilerOptions },
|
|
52
70
|
logLevel: "silent",
|
|
@@ -80,8 +98,9 @@ function rewriteImport(code, node, index) {
|
|
|
80
98
|
? `, { with: { ${node.attributes.map((a) => code.slice(a.start, a.end)).join(", ")} } }`
|
|
81
99
|
: "";
|
|
82
100
|
const load = `await ${SHIM}.import(${source}${attrs})`;
|
|
101
|
+
// `void`: as a cell's last statement, the import isn't its result (#139).
|
|
83
102
|
if (node.specifiers.length === 0)
|
|
84
|
-
return
|
|
103
|
+
return `void ${load};`;
|
|
85
104
|
const ns = `${SHIM}_m${index}`;
|
|
86
105
|
const bindings = node.specifiers.map((spec) => {
|
|
87
106
|
const local = String(spec.local.name);
|
|
@@ -140,7 +159,13 @@ export function rewriteModuleSyntax(js) {
|
|
|
140
159
|
}
|
|
141
160
|
case "ExportDefaultDeclaration": {
|
|
142
161
|
const decl = node;
|
|
143
|
-
|
|
162
|
+
const anonymous = (decl.declaration.type === "FunctionDeclaration" || decl.declaration.type === "ClassDeclaration") &&
|
|
163
|
+
decl.declaration.id === null;
|
|
164
|
+
// An anonymous `function () {}` or `class {}` isn't a statement on
|
|
165
|
+
// its own; parenthesized, it's an expression and the cell's result (#143).
|
|
166
|
+
edits.push({ start: decl.start, end: decl.declaration.start, text: anonymous ? "(" : "" });
|
|
167
|
+
if (anonymous)
|
|
168
|
+
edits.push({ start: decl.declaration.end, end: decl.declaration.end, text: ");" });
|
|
144
169
|
break;
|
|
145
170
|
}
|
|
146
171
|
case "ExportAllDeclaration":
|
|
@@ -202,8 +227,8 @@ function patternNames(pattern, names) {
|
|
|
202
227
|
* surrounding script, which a global-code eval turns into a property of
|
|
203
228
|
* globalThis. Inside the function the declarations become assignments to
|
|
204
229
|
* those globals:
|
|
205
|
-
* const { a } = o; class K {} -> var a, K;
|
|
206
|
-
* (
|
|
230
|
+
* const { a } = o; class K {} -> var a, K; (async () => {
|
|
231
|
+
* ({ a } = o); K = class K {}; })()
|
|
207
232
|
* So later cells, and closures from this one, all see one binding. A `var`
|
|
208
233
|
* in a top-level block or loop head is global too, as in a script; block
|
|
209
234
|
* scoped and function-local declarations stay where they are. Functions
|
|
@@ -277,18 +302,21 @@ function hoistForEval(js) {
|
|
|
277
302
|
edits.push({ start: stmt.end, end: stmt.end, text: ";" });
|
|
278
303
|
}
|
|
279
304
|
else if (stmt.type === "ExpressionStatement" && i === program.body.length - 1) {
|
|
280
|
-
|
|
305
|
+
// Parenthesized, so `1, 2` returns [2] rather than [1, 2] (#140).
|
|
306
|
+
replace(stmt, `return [(${src(stmt.expression)})];`);
|
|
281
307
|
}
|
|
282
308
|
else {
|
|
283
309
|
nestedVars(stmt);
|
|
284
310
|
}
|
|
285
311
|
});
|
|
286
|
-
|
|
312
|
+
// All on the first line, so the cell always starts on line 2, as after
|
|
313
|
+
// transformCell's one-line prelude (#150).
|
|
314
|
+
const declare = names.size > 0 ? `var ${[...names].join(", ")}; ` : "";
|
|
287
315
|
const publish = functions.map((name) => ` globalThis.${name} = ${name};`).join("");
|
|
288
316
|
return `${declare}(async () => { 'use strict';${publish}\n${applyEdits(js, edits)}\n})()`;
|
|
289
317
|
}
|
|
290
318
|
export function transformCell(code, options = {}) {
|
|
291
|
-
return PRELUDE + rewriteModuleSyntax(stripTypes(code, options));
|
|
319
|
+
return PRELUDE + rewriteModuleSyntax(stripTypes(code, options)) + SETTLE;
|
|
292
320
|
}
|
|
293
321
|
/**
|
|
294
322
|
* transformCell, plus the map between the cell's lines and the script's,
|
|
@@ -297,7 +325,7 @@ export function transformCell(code, options = {}) {
|
|
|
297
325
|
*/
|
|
298
326
|
export function transformCellWithLines(code, options = {}) {
|
|
299
327
|
const stripped = stripTypesMapped(code, options, true);
|
|
300
|
-
return { script: PRELUDE + rewriteModuleSyntax(stripped.code), lines: new LineMap(stripped.mappings, newlines(PRELUDE)) };
|
|
328
|
+
return { script: PRELUDE + rewriteModuleSyntax(stripped.code) + SETTLE, lines: new LineMap(stripped.mappings, newlines(PRELUDE)) };
|
|
301
329
|
}
|
|
302
330
|
/**
|
|
303
331
|
* Like transformCell, for a target that evaluates cells itself, as global
|
package/dist/wire.d.ts
CHANGED
|
@@ -11,12 +11,17 @@ export interface Header extends JsonObject {
|
|
|
11
11
|
msg_type: string;
|
|
12
12
|
version: string;
|
|
13
13
|
}
|
|
14
|
+
/** A header as received: the fields the kernel relies on, plus whatever else came. */
|
|
15
|
+
export interface ReceivedHeader extends JsonObject {
|
|
16
|
+
msg_id: string;
|
|
17
|
+
msg_type: string;
|
|
18
|
+
}
|
|
14
19
|
export interface Message {
|
|
15
20
|
identities: Uint8Array[];
|
|
16
21
|
sig: string;
|
|
17
22
|
/** Raw bytes of header, parent_header, metadata, content as received. */
|
|
18
23
|
signedParts: Uint8Array[];
|
|
19
|
-
header:
|
|
24
|
+
header: ReceivedHeader;
|
|
20
25
|
parentHeader: JsonObject;
|
|
21
26
|
metadata: JsonObject;
|
|
22
27
|
content: JsonObject;
|
|
@@ -26,6 +31,8 @@ export declare function sign(key: string, parts: Uint8Array[]): string;
|
|
|
26
31
|
export declare function verify(key: string, parts: Uint8Array[], sig: string): boolean;
|
|
27
32
|
export declare function makeHeader(session: string, msgType: string): Header;
|
|
28
33
|
export interface EncodeOptions {
|
|
34
|
+
/** Defaults to a fresh header; pass one to know its msg_id. */
|
|
35
|
+
header?: Header;
|
|
29
36
|
identities?: Uint8Array[];
|
|
30
37
|
parentHeader?: JsonObject;
|
|
31
38
|
metadata?: JsonObject;
|