@tlockney/cdp-kernel 0.5.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 +27 -6
- package/dist/cdp.d.ts +2 -0
- package/dist/cdp.js +6 -1
- package/dist/celld-target.js +9 -12
- package/dist/children.d.ts +25 -3
- package/dist/children.js +84 -2
- package/dist/deno-types.d.ts +30 -0
- package/dist/deno-types.js +93 -0
- package/dist/kernel.d.ts +7 -1
- package/dist/kernel.js +187 -29
- 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 +1 -0
- package/dist/mod.js +1 -0
- 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 +2 -2
- 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 +27 -3
- package/dist/target.js +193 -36
- package/dist/transform.d.ts +21 -1
- package/dist/transform.js +38 -42
- package/dist/workerd-target.js +1 -1
- package/dist/workers-project.js +13 -2
- package/dist/zmtp.d.ts +2 -0
- package/dist/zmtp.js +12 -0
- package/package.json +9 -4
- package/runtime/bootstrap.mjs +113 -33
- package/runtime/introspect.mjs +1 -1
- package/runtime/node-hooks.d.mts +6 -0
- package/runtime/node-hooks.mjs +11 -0
package/dist/target.js
CHANGED
|
@@ -7,11 +7,18 @@ import { spawn } from "node:child_process";
|
|
|
7
7
|
import { createRequire } from "node:module";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
-
import { asEvaluateResult, CdpAbandoned, CdpSession } from "./cdp.js";
|
|
10
|
+
import { asEvaluateResult, CdpAbandoned, CdpError, CdpSession } from "./cdp.js";
|
|
11
11
|
import { SHIM, transformCell } from "./transform.js";
|
|
12
12
|
import { hostRuntime, isInsideNodeModules, runtimeDir } from "./package.js";
|
|
13
13
|
import { killOnExit } from "./children.js";
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* The inspector keeps every object it returns a remote reference to alive
|
|
16
|
+
* until it is released, so a cell's result would never be collected (#286).
|
|
17
|
+
* Cells and lookups put theirs in these groups, which are released once the
|
|
18
|
+
* kernel is done with them.
|
|
19
|
+
*/
|
|
20
|
+
const CELL_GROUP = "cdpk-cell";
|
|
21
|
+
const LOOKUP_GROUP = "cdpk-lookup";
|
|
15
22
|
/** How long a target may take from spawn to a usable session. */
|
|
16
23
|
const START_TIMEOUT_MS = 60_000;
|
|
17
24
|
const MARKER_RE = /\x1b\[cdpk:(\d+)(m?)\]\x1b/;
|
|
@@ -27,7 +34,9 @@ const INSPECTOR_CHATTER = [
|
|
|
27
34
|
"Visit chrome://inspect",
|
|
28
35
|
"Waiting for the debugger to disconnect",
|
|
29
36
|
];
|
|
30
|
-
|
|
37
|
+
// Deno prints "Debugger session started." for each session the host opens
|
|
38
|
+
// (#281).
|
|
39
|
+
const DENO_CHATTER = [...INSPECTOR_CHATTER, "Debugger session started.", "Download http://", "Download https://"];
|
|
31
40
|
const ANSI_SGR = /\x1b\[[0-9;]*m/g;
|
|
32
41
|
/** A colour code cut off at the end of a chunk. */
|
|
33
42
|
const PARTIAL_SGR = /\x1b(?:\[[0-9;]*)?$/;
|
|
@@ -92,6 +101,10 @@ export function isTargetMessage(value) {
|
|
|
92
101
|
typeof value.barrier === "number";
|
|
93
102
|
}
|
|
94
103
|
const MARKER_PREFIX = "\x1b[cdpk:";
|
|
104
|
+
// A message record: the message's JSON, which never holds a raw ESC, between
|
|
105
|
+
// "\x1b[cdpk:<" and "\x1b]" (#271).
|
|
106
|
+
const RECORD_PREFIX = "\x1b[cdpk:<";
|
|
107
|
+
const RECORD_RE = /\x1b\[cdpk:<([^\x1b]*)\x1b\]/;
|
|
95
108
|
function isPartialMarker(tail) {
|
|
96
109
|
if (tail.length >= MAX_MARKER_LEN)
|
|
97
110
|
return false;
|
|
@@ -99,10 +112,13 @@ function isPartialMarker(tail) {
|
|
|
99
112
|
}
|
|
100
113
|
/**
|
|
101
114
|
* Splits one output pipe into text and barrier markers, and releases them
|
|
102
|
-
* in order. A gated marker (one that announces a
|
|
115
|
+
* in order. A gated marker (one that announces a message) pauses
|
|
103
116
|
* the pipe until `gate(n)` resolves, i.e. until that message has been
|
|
104
117
|
* delivered, so later output can't overtake it. A possible partial marker
|
|
105
118
|
* at the end of a chunk is held back until the next chunk decides it.
|
|
119
|
+
*
|
|
120
|
+
* With `onMessage`, the pipe also carries message records. A record stands
|
|
121
|
+
* where its gated marker would, and pauses the pipe the same way.
|
|
106
122
|
*/
|
|
107
123
|
export class PipeDemux {
|
|
108
124
|
#buffer = "";
|
|
@@ -111,28 +127,49 @@ export class PipeDemux {
|
|
|
111
127
|
#closed = false;
|
|
112
128
|
#reached = 0;
|
|
113
129
|
#waiters = [];
|
|
130
|
+
/** The gated marker the pipe is paused on, if any. */
|
|
131
|
+
#gatedOn = null;
|
|
114
132
|
onText;
|
|
115
133
|
gate;
|
|
116
|
-
|
|
134
|
+
onMessage;
|
|
135
|
+
constructor(onText, gate = () => Promise.resolve(), onMessage = null) {
|
|
117
136
|
this.onText = onText;
|
|
118
137
|
this.gate = gate;
|
|
138
|
+
this.onMessage = onMessage;
|
|
119
139
|
}
|
|
120
140
|
push(chunk) {
|
|
121
141
|
this.#buffer += chunk;
|
|
122
142
|
for (;;) {
|
|
123
143
|
const m = MARKER_RE.exec(this.#buffer);
|
|
124
|
-
|
|
144
|
+
const r = this.onMessage ? RECORD_RE.exec(this.#buffer) : null;
|
|
145
|
+
const first = m && r ? (m.index < r.index ? m : r) : m ?? r;
|
|
146
|
+
if (!first)
|
|
125
147
|
break;
|
|
126
|
-
if (
|
|
127
|
-
this.#items.push({ text: this.#buffer.slice(0,
|
|
128
|
-
|
|
129
|
-
|
|
148
|
+
if (first.index > 0)
|
|
149
|
+
this.#items.push({ text: this.#buffer.slice(0, first.index) });
|
|
150
|
+
if (first === m) {
|
|
151
|
+
this.#items.push({ barrier: Number(m[1]), gated: m[2] === "m" });
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
// Anything else a cell prints in this shape is dropped (#122).
|
|
155
|
+
const msg = parseJson(first[1]);
|
|
156
|
+
if (isTargetMessage(msg))
|
|
157
|
+
this.#items.push({ message: msg });
|
|
158
|
+
}
|
|
159
|
+
this.#buffer = this.#buffer.slice(first.index + first[0].length);
|
|
130
160
|
}
|
|
131
161
|
// Hold back a tail that could still grow into a marker, including one
|
|
132
|
-
// cut inside the "\x1b[cdpk:" prefix itself
|
|
162
|
+
// cut inside the "\x1b[cdpk:" prefix itself, or into a record: one
|
|
163
|
+
// whose only ESC after its prefix is a final one.
|
|
133
164
|
const esc = this.#buffer.lastIndexOf("\x1b");
|
|
134
165
|
const tail = esc === -1 ? "" : this.#buffer.slice(esc);
|
|
135
|
-
|
|
166
|
+
let holdFrom = esc !== -1 && isPartialMarker(tail) ? esc : this.#buffer.length;
|
|
167
|
+
const record = this.onMessage ? this.#buffer.lastIndexOf(RECORD_PREFIX) : -1;
|
|
168
|
+
if (record !== -1) {
|
|
169
|
+
const next = this.#buffer.indexOf("\x1b", record + RECORD_PREFIX.length);
|
|
170
|
+
if (next === -1 || next === this.#buffer.length - 1)
|
|
171
|
+
holdFrom = Math.min(holdFrom, record);
|
|
172
|
+
}
|
|
136
173
|
if (holdFrom > 0) {
|
|
137
174
|
this.#items.push({ text: this.#buffer.slice(0, holdFrom) });
|
|
138
175
|
this.#buffer = this.#buffer.slice(holdFrom);
|
|
@@ -149,9 +186,22 @@ export class PipeDemux {
|
|
|
149
186
|
this.onText(item.text);
|
|
150
187
|
continue;
|
|
151
188
|
}
|
|
189
|
+
if ("message" in item) {
|
|
190
|
+
this.#reach(item.message.barrier);
|
|
191
|
+
this.onMessage?.(item.message);
|
|
192
|
+
if (!this.#closed) {
|
|
193
|
+
this.#gatedOn = item.message.barrier;
|
|
194
|
+
await this.gate(item.message.barrier);
|
|
195
|
+
this.#gatedOn = null;
|
|
196
|
+
}
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
152
199
|
this.#reach(item.barrier);
|
|
153
|
-
if (item.gated && !this.#closed)
|
|
200
|
+
if (item.gated && !this.#closed) {
|
|
201
|
+
this.#gatedOn = item.barrier;
|
|
154
202
|
await this.gate(item.barrier);
|
|
203
|
+
this.#gatedOn = null;
|
|
204
|
+
}
|
|
155
205
|
}
|
|
156
206
|
}
|
|
157
207
|
finally {
|
|
@@ -172,6 +222,16 @@ export class PipeDemux {
|
|
|
172
222
|
return Promise.resolve();
|
|
173
223
|
return new Promise((resolve) => this.#waiters.push({ n, resolve }));
|
|
174
224
|
}
|
|
225
|
+
/** Where the pipe stands, for a state dump (#237). */
|
|
226
|
+
state() {
|
|
227
|
+
return {
|
|
228
|
+
reached: this.#reached,
|
|
229
|
+
waitingFor: this.#waiters.map((w) => w.n),
|
|
230
|
+
gatedOn: this.#gatedOn,
|
|
231
|
+
queued: this.#items.length,
|
|
232
|
+
held: this.#buffer.length,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
175
235
|
/** Release everything (the process died). */
|
|
176
236
|
close() {
|
|
177
237
|
this.#closed = true;
|
|
@@ -287,6 +347,8 @@ export class Target {
|
|
|
287
347
|
#gates;
|
|
288
348
|
#contextId;
|
|
289
349
|
#messages = Promise.resolve();
|
|
350
|
+
/** Messages received but not yet delivered. */
|
|
351
|
+
#undelivered = 0;
|
|
290
352
|
#interrupted = false;
|
|
291
353
|
#paused = false;
|
|
292
354
|
/** Set once the main context is gone: the runtime is exiting (#130). */
|
|
@@ -332,6 +394,9 @@ export class Target {
|
|
|
332
394
|
delete env.NO_COLOR;
|
|
333
395
|
const child = spawn(cmd, args, { cwd: options.cwd, stdio: ["pipe", "pipe", "pipe"], env });
|
|
334
396
|
killOnExit(child);
|
|
397
|
+
// Prompt replies and comm messages go to stdin; one written as the
|
|
398
|
+
// runtime exits fails with EPIPE, and the exit is handled on its own.
|
|
399
|
+
child.stdin.on("error", () => { });
|
|
335
400
|
// A start that fails or stalls must not leave the runtime running: its
|
|
336
401
|
// pipes and inspector sockets would keep this process alive (#52).
|
|
337
402
|
const sessions = [];
|
|
@@ -357,7 +422,12 @@ export class Target {
|
|
|
357
422
|
child.stdout.setEncoding("utf8");
|
|
358
423
|
child.stderr.setEncoding("utf8");
|
|
359
424
|
const gates = new Gates();
|
|
360
|
-
|
|
425
|
+
// Messages come only after attach(), so the target exists by then.
|
|
426
|
+
let target;
|
|
427
|
+
const stdout = new PipeDemux((text) => events.stream("stdout", text), (n) => gates.wait(n), (msg) => {
|
|
428
|
+
if (target)
|
|
429
|
+
target.#deliver(msg, events);
|
|
430
|
+
});
|
|
361
431
|
const stderr = new PipeDemux((text) => events.stream("stderr", text), (n) => gates.wait(n));
|
|
362
432
|
const filter = new StderrFilter(runtime, (text) => stderr.push(text));
|
|
363
433
|
const pushStderr = (chunk) => filter.push(chunk);
|
|
@@ -398,25 +468,22 @@ export class Target {
|
|
|
398
468
|
});
|
|
399
469
|
});
|
|
400
470
|
await shell.send("Runtime.enable");
|
|
401
|
-
await shell.send("Runtime.addBinding", { name: BINDING });
|
|
402
471
|
const contextId = await contextReady;
|
|
403
|
-
|
|
472
|
+
// The inspector socket leaves Nagle's algorithm on, so an event written
|
|
473
|
+
// just before an evaluate's reply holds that reply until the event is
|
|
474
|
+
// acknowledged, ~40 ms on Linux (#271). So the shell session, which runs
|
|
475
|
+
// the cells, takes no Runtime events (console calls among them), and
|
|
476
|
+
// messages come over stdout. The control session watches for exit.
|
|
477
|
+
await shell.send("Runtime.disable");
|
|
478
|
+
await control.send("Runtime.enable");
|
|
479
|
+
target = new Target(runtime, child, shell, control, stdout, stderr, gates, contextId, { runtime: "", engine: "" });
|
|
404
480
|
// process.exit() or Deno.exit() destroys the context, then the runtime
|
|
405
481
|
// waits for its inspector clients to disconnect before it exits. Let go,
|
|
406
482
|
// so it exits and the kernel starts a fresh one (#130).
|
|
407
|
-
|
|
408
|
-
if (params.executionContextId === contextId)
|
|
483
|
+
control.on("Runtime.executionContextDestroyed", (params) => {
|
|
484
|
+
if (params.executionContextId === contextId && target)
|
|
409
485
|
target.#onExit();
|
|
410
486
|
});
|
|
411
|
-
shell.on("Runtime.bindingCalled", (params) => {
|
|
412
|
-
if (params.name !== BINDING || typeof params.payload !== "string")
|
|
413
|
-
return;
|
|
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);
|
|
417
|
-
if (isTargetMessage(msg))
|
|
418
|
-
target.#deliver(msg, events);
|
|
419
|
-
});
|
|
420
487
|
// Wait for the bootstrap's top-level await to finish installing the
|
|
421
488
|
// shim. There is no cap here: launch's start timeout bounds the wait and
|
|
422
489
|
// kills the child, so a slow bootstrap never yields a target without
|
|
@@ -427,6 +494,7 @@ export class Target {
|
|
|
427
494
|
break;
|
|
428
495
|
await new Promise((r) => setTimeout(r, 25));
|
|
429
496
|
}
|
|
497
|
+
await target.#evaluateSync(`${SHIM}.attach()`);
|
|
430
498
|
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 })`);
|
|
431
499
|
Object.assign(target.versions, JSON.parse(String(versions.result.value)));
|
|
432
500
|
// The runtime's own enumerable globals, before any cell: the debugger's
|
|
@@ -449,15 +517,65 @@ export class Target {
|
|
|
449
517
|
baselineGlobals: this.#baselineGlobals,
|
|
450
518
|
};
|
|
451
519
|
}
|
|
452
|
-
//
|
|
520
|
+
// Messages wait until both pipes have caught up to their marker,
|
|
453
521
|
// and are delivered strictly in order.
|
|
454
522
|
#deliver(msg, events) {
|
|
523
|
+
this.#undelivered++;
|
|
455
524
|
this.#messages = this.#messages.then(async () => {
|
|
456
525
|
await Promise.all([this.#stdout.waitFor(msg.barrier), this.#stderr.waitFor(msg.barrier)]);
|
|
526
|
+
this.#undelivered--;
|
|
457
527
|
events.message(msg);
|
|
458
528
|
this.#gates.open(msg.barrier);
|
|
459
529
|
});
|
|
460
530
|
}
|
|
531
|
+
/**
|
|
532
|
+
* What the host is waiting on, whether the runtime still evaluates, runs
|
|
533
|
+
* immediates and runs timers, and where its JavaScript is if it is
|
|
534
|
+
* running any, for a kernel that has stopped answering (#237). The probes
|
|
535
|
+
* go over the control session, which no cell holds.
|
|
536
|
+
*/
|
|
537
|
+
async diagnose() {
|
|
538
|
+
const within = (promise, fallback) => Promise.race([promise, new Promise((resolve) => setTimeout(() => resolve(fallback), 2000).unref())]);
|
|
539
|
+
const probe = (expression, awaitPromise) => within(this.#control.send("Runtime.evaluate", { expression, contextId: this.#contextId, awaitPromise, returnByValue: true })
|
|
540
|
+
.then((res) => res.exceptionDetails ? `threw: ${JSON.stringify(res.exceptionDetails)}` : String(res.result?.value), (err) => `failed: ${err}`), "no answer in 2 s");
|
|
541
|
+
const [evaluates, immediates, timers] = await Promise.all([
|
|
542
|
+
probe("'yes'", false),
|
|
543
|
+
probe("new Promise((resolve) => setImmediate(() => resolve('yes')))", true),
|
|
544
|
+
probe("new Promise((resolve) => setTimeout(() => resolve('yes'), 0))", true),
|
|
545
|
+
]);
|
|
546
|
+
// A runtime whose event loop is stuck may be stuck in JavaScript: say
|
|
547
|
+
// where. (An idle one would take the pause in whatever runs next.)
|
|
548
|
+
let running = "event loop runs";
|
|
549
|
+
if (immediates !== "yes") {
|
|
550
|
+
try {
|
|
551
|
+
const paused = new Promise((resolve) => this.#control.on("Debugger.paused", resolve));
|
|
552
|
+
await this.#control.send("Debugger.enable");
|
|
553
|
+
await this.#control.send("Debugger.pause");
|
|
554
|
+
const event = await within(paused, null);
|
|
555
|
+
running = event === null ? "no" : event.callFrames
|
|
556
|
+
.slice(0, 10)
|
|
557
|
+
.map((f) => `${f.functionName || "(anonymous)"} ${f.url || "(no url)"}:${f.location.lineNumber + 1}`);
|
|
558
|
+
await this.#control.send("Debugger.resume").catch(() => { });
|
|
559
|
+
await this.#control.send("Debugger.disable");
|
|
560
|
+
}
|
|
561
|
+
catch (err) {
|
|
562
|
+
running = `failed: ${err}`;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return {
|
|
566
|
+
pid: this.#child.pid ?? null,
|
|
567
|
+
calls: { shell: this.#shell.pendingCalls(), control: this.#control.pendingCalls() },
|
|
568
|
+
stdout: this.#stdout.state(),
|
|
569
|
+
stderr: this.#stderr.state(),
|
|
570
|
+
undelivered: this.#undelivered,
|
|
571
|
+
paused: this.#paused,
|
|
572
|
+
exiting: this.#exiting,
|
|
573
|
+
evaluates,
|
|
574
|
+
immediates,
|
|
575
|
+
timers,
|
|
576
|
+
running,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
461
579
|
/** Resolves once all output and messages sent so far have been delivered. */
|
|
462
580
|
async flush() {
|
|
463
581
|
const n = await this.#evaluateSync(`${SHIM}.barrier()`).catch(interrupted("Execution"));
|
|
@@ -479,7 +597,13 @@ export class Target {
|
|
|
479
597
|
}
|
|
480
598
|
#evaluateRaw(expression, session = this.#shell) {
|
|
481
599
|
return session
|
|
482
|
-
.send("Runtime.evaluate", {
|
|
600
|
+
.send("Runtime.evaluate", {
|
|
601
|
+
expression,
|
|
602
|
+
contextId: this.#contextId,
|
|
603
|
+
replMode: true,
|
|
604
|
+
awaitPromise: true,
|
|
605
|
+
objectGroup: CELL_GROUP,
|
|
606
|
+
})
|
|
483
607
|
.then(asEvaluateResult);
|
|
484
608
|
}
|
|
485
609
|
/** Runs transformed cell code. Rejects with Interrupted after interrupt(). */
|
|
@@ -491,6 +615,10 @@ export class Target {
|
|
|
491
615
|
catch (err) {
|
|
492
616
|
if (err instanceof CdpAbandoned || this.#interrupted)
|
|
493
617
|
throw new Interrupted("Execution interrupted");
|
|
618
|
+
// The control session's executionContextDestroyed event can arrive
|
|
619
|
+
// after the shell session's error reply (#271 moved it there).
|
|
620
|
+
if (err instanceof CdpError && err.message.includes("Execution context was destroyed"))
|
|
621
|
+
this.#onExit();
|
|
494
622
|
if (this.#exiting) {
|
|
495
623
|
// The child's exit, which drops this target in the kernel, comes
|
|
496
624
|
// first, so the next cell starts a fresh one.
|
|
@@ -500,6 +628,11 @@ export class Target {
|
|
|
500
628
|
throw err;
|
|
501
629
|
}
|
|
502
630
|
}
|
|
631
|
+
async releaseResults() {
|
|
632
|
+
if (this.#exiting)
|
|
633
|
+
return;
|
|
634
|
+
await this.#shell.send("Runtime.releaseObjectGroup", { objectGroup: CELL_GROUP }).catch(() => { });
|
|
635
|
+
}
|
|
503
636
|
#onExit() {
|
|
504
637
|
if (this.#exiting)
|
|
505
638
|
return;
|
|
@@ -573,14 +706,19 @@ export class Target {
|
|
|
573
706
|
*/
|
|
574
707
|
async dispatchComm(msgType, content, metadata, buffers) {
|
|
575
708
|
this.#interrupted = false;
|
|
709
|
+
// Over stdin, not as call arguments: V8 caches an argument's source,
|
|
710
|
+
// so the data would stay in the target's memory (#287).
|
|
711
|
+
const record = Buffer.from(JSON.stringify({
|
|
712
|
+
msgType,
|
|
713
|
+
content,
|
|
714
|
+
metadata,
|
|
715
|
+
buffers: buffers.map((b) => Buffer.from(b).toString("base64")),
|
|
716
|
+
}));
|
|
717
|
+
this.#child.stdin.write(`comm ${record.length}\n`);
|
|
718
|
+
this.#child.stdin.write(record);
|
|
576
719
|
let res;
|
|
577
720
|
try {
|
|
578
|
-
res = await this.#callAsyncFunctionOn(`function (
|
|
579
|
-
{ value: msgType },
|
|
580
|
-
{ value: content },
|
|
581
|
-
{ value: metadata },
|
|
582
|
-
{ value: buffers.map((b) => Buffer.from(b).toString("base64")) },
|
|
583
|
-
]);
|
|
721
|
+
res = await this.#callAsyncFunctionOn(`function () { return globalThis.${SHIM}.commDispatchFromStdin(); }`, []);
|
|
584
722
|
}
|
|
585
723
|
catch (err) {
|
|
586
724
|
if (err instanceof CdpAbandoned || this.#interrupted)
|
|
@@ -611,11 +749,22 @@ export class Target {
|
|
|
611
749
|
*/
|
|
612
750
|
#evaluatePure(expression) {
|
|
613
751
|
return this.#shell
|
|
614
|
-
.send("Runtime.evaluate", { expression, contextId: this.#contextId, throwOnSideEffect: true })
|
|
752
|
+
.send("Runtime.evaluate", { expression, contextId: this.#contextId, throwOnSideEffect: true, objectGroup: LOOKUP_GROUP })
|
|
615
753
|
.then(asEvaluateResult)
|
|
616
754
|
.catch(() => null);
|
|
617
755
|
}
|
|
756
|
+
#releaseLookup() {
|
|
757
|
+
return this.#shell.send("Runtime.releaseObjectGroup", { objectGroup: LOOKUP_GROUP }).then(() => { }, () => { });
|
|
758
|
+
}
|
|
618
759
|
async propertyNames(expression) {
|
|
760
|
+
try {
|
|
761
|
+
return await this.#propertyNames(expression);
|
|
762
|
+
}
|
|
763
|
+
finally {
|
|
764
|
+
await this.#releaseLookup();
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
async #propertyNames(expression) {
|
|
619
768
|
const evaluated = await this.#evaluatePure(`(${expression})`);
|
|
620
769
|
const objectId = evaluated?.result.objectId;
|
|
621
770
|
if (!objectId || evaluated?.exceptionDetails)
|
|
@@ -625,6 +774,14 @@ export class Target {
|
|
|
625
774
|
return (list ?? []).map((p) => p.name);
|
|
626
775
|
}
|
|
627
776
|
async inspect(path, detail) {
|
|
777
|
+
try {
|
|
778
|
+
return await this.#inspect(path, detail);
|
|
779
|
+
}
|
|
780
|
+
finally {
|
|
781
|
+
await this.#releaseLookup();
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
async #inspect(path, detail) {
|
|
628
785
|
const found = await this.#evaluatePure(path);
|
|
629
786
|
if (!found || found.exceptionDetails)
|
|
630
787
|
return null;
|
package/dist/transform.d.ts
CHANGED
|
@@ -29,14 +29,34 @@ export type JsxOptions = {
|
|
|
29
29
|
export interface CellTransformOptions {
|
|
30
30
|
jsx?: JsxOptions;
|
|
31
31
|
experimentalDecorators?: boolean;
|
|
32
|
+
/** What removes a cell's types and compiles its JSX; esbuild by default (#238, #234). */
|
|
33
|
+
typeStrip?: TypeStripperName | TypeStripper;
|
|
32
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Turns a TypeScript/TSX cell into JavaScript with ES module syntax left in
|
|
37
|
+
* place, and with `mappings` (a source map's field, lines are enough) when
|
|
38
|
+
* `sourcemap` is set, for the debugger. Throws CellSyntaxError for code it
|
|
39
|
+
* rejects.
|
|
40
|
+
*/
|
|
41
|
+
export type TypeStripper = (code: string, options: CellTransformOptions, sourcemap: boolean) => {
|
|
42
|
+
code: string;
|
|
43
|
+
mappings: string;
|
|
44
|
+
};
|
|
45
|
+
export type TypeStripperName = "esbuild" | "sucrase";
|
|
46
|
+
export declare function isTypeStripperName(value: unknown): value is TypeStripperName;
|
|
47
|
+
/**
|
|
48
|
+
* Loads a built-in type stripper. Transforms are synchronous, so a kernel
|
|
49
|
+
* loads the one it uses before its first cell; only that one's module is
|
|
50
|
+
* imported (#238).
|
|
51
|
+
*/
|
|
52
|
+
export declare function loadTypeStripper(name: TypeStripperName): Promise<TypeStripper>;
|
|
33
53
|
/** Rewrites module-only syntax in plain JavaScript into script syntax. */
|
|
34
54
|
export declare function rewriteModuleSyntax(js: string): string;
|
|
35
55
|
export declare function transformCell(code: string, options?: CellTransformOptions): string;
|
|
36
56
|
/**
|
|
37
57
|
* transformCell, plus the map between the cell's lines and the script's,
|
|
38
58
|
* for the debugger (#18). The rewrite keeps lines (see applyEdits), so the
|
|
39
|
-
* script's lines are
|
|
59
|
+
* script's lines are the stripper's, after the prelude's one.
|
|
40
60
|
*/
|
|
41
61
|
export declare function transformCellWithLines(code: string, options?: CellTransformOptions): {
|
|
42
62
|
script: string;
|
package/dist/transform.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// CDP's Runtime.evaluate can run with replMode: true.
|
|
3
3
|
//
|
|
4
4
|
// Deno does this with deno_ast (swc) and `var_decl_imports: true`
|
|
5
|
-
// (cli/tools/repl/session.rs, evaluate_ts_expression). Here
|
|
6
|
-
// TypeScript/JSX part and acorn finds the
|
|
7
|
-
// contain:
|
|
5
|
+
// (cli/tools/repl/session.rs, evaluate_ts_expression). Here a type stripper
|
|
6
|
+
// (esbuild by default) does the TypeScript/JSX part and acorn finds the
|
|
7
|
+
// module syntax that a script can't contain:
|
|
8
8
|
// import x, { y as z } from "m" -> var __cdpk_m0 = await __cdpk.import("m");
|
|
9
9
|
// var x = __cdpk_m0["default"], z = __cdpk_m0["y"];
|
|
10
10
|
// import("m") -> __cdpk.import("m")
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
// import() through it matters: code evaluated over the inspector has no
|
|
15
15
|
// module referrer, so Node rejects a bare import() ("A dynamic import
|
|
16
16
|
// callback was not specified").
|
|
17
|
-
import { transformSync } from "esbuild";
|
|
18
17
|
import { parse } from "acorn";
|
|
19
18
|
import { full, recursive } from "acorn-walk";
|
|
20
19
|
import { LineMap } from "./sourcemap.js";
|
|
@@ -37,51 +36,48 @@ export const SETTLE = `\n;let ${SHIM}_settled = await ${SHIM}.tick();`;
|
|
|
37
36
|
export class CellSyntaxError extends Error {
|
|
38
37
|
name = "SyntaxError";
|
|
39
38
|
}
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
export function isTypeStripperName(value) {
|
|
40
|
+
return value === "esbuild" || value === "sucrase";
|
|
41
|
+
}
|
|
42
|
+
const strippers = new Map();
|
|
43
|
+
/**
|
|
44
|
+
* Loads a built-in type stripper. Transforms are synchronous, so a kernel
|
|
45
|
+
* loads the one it uses before its first cell; only that one's module is
|
|
46
|
+
* imported (#238).
|
|
47
|
+
*/
|
|
48
|
+
export async function loadTypeStripper(name) {
|
|
49
|
+
let stripper = strippers.get(name);
|
|
50
|
+
if (stripper === undefined) {
|
|
51
|
+
try {
|
|
52
|
+
stripper = name === "sucrase"
|
|
53
|
+
? (await import("./strip-sucrase.js")).stripTypes
|
|
54
|
+
: (await import("./strip-esbuild.js")).stripTypes;
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
58
|
+
throw new Error(`cannot load the ${name} type stripper (is the ${name} package installed?): ${reason}`);
|
|
59
|
+
}
|
|
60
|
+
strippers.set(name, stripper);
|
|
43
61
|
}
|
|
44
|
-
return
|
|
62
|
+
return stripper;
|
|
63
|
+
}
|
|
64
|
+
function typeStripper(choice = "esbuild") {
|
|
65
|
+
if (typeof choice === "function")
|
|
66
|
+
return choice;
|
|
67
|
+
const stripper = strippers.get(choice);
|
|
68
|
+
if (stripper === undefined)
|
|
69
|
+
throw new Error(`the ${choice} type stripper isn't loaded (see loadTypeStripper)`);
|
|
70
|
+
return stripper;
|
|
45
71
|
}
|
|
46
72
|
// esbuild compiles `import x = require("m")` to a `require` call, which a
|
|
47
73
|
// cell doesn't have (#143); `import type x = require("m")` is erased.
|
|
48
74
|
const IMPORT_REQUIRE = /^\s*(?:export\s+)?import\s+(?!type\s)[\w$]+\s*=\s*require\s*\(/m;
|
|
49
|
-
/**
|
|
75
|
+
/** The stripper's output, with its source map's `mappings` when asked for. */
|
|
50
76
|
function stripTypesMapped(code, options, sourcemap) {
|
|
51
77
|
if (IMPORT_REQUIRE.test(code)) {
|
|
52
78
|
throw new CellSyntaxError('import x = require("m") is not supported in a cell; use import x from "m"');
|
|
53
79
|
}
|
|
54
|
-
|
|
55
|
-
verbatimModuleSyntax: true,
|
|
56
|
-
...(options.experimentalDecorators ? { experimentalDecorators: true } : {}),
|
|
57
|
-
};
|
|
58
|
-
// Like Deno: prefer TSX, fall back to TS for `<T>x` assertions.
|
|
59
|
-
let firstError;
|
|
60
|
-
for (const loader of ["tsx", "ts"]) {
|
|
61
|
-
try {
|
|
62
|
-
const result = transformSync(code, {
|
|
63
|
-
loader,
|
|
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 },
|
|
68
|
-
...jsxSettings(options.jsx),
|
|
69
|
-
tsconfigRaw: { compilerOptions },
|
|
70
|
-
logLevel: "silent",
|
|
71
|
-
...(sourcemap ? { sourcemap: "external" } : {}),
|
|
72
|
-
});
|
|
73
|
-
const map = sourcemap ? JSON.parse(result.map) : null;
|
|
74
|
-
const mappings = typeof map?.mappings === "string"
|
|
75
|
-
? map.mappings
|
|
76
|
-
: "";
|
|
77
|
-
return { code: result.code, mappings };
|
|
78
|
-
}
|
|
79
|
-
catch (err) {
|
|
80
|
-
firstError ??= err;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
const message = firstError instanceof Error ? firstError.message : String(firstError);
|
|
84
|
-
throw new CellSyntaxError(message.replace(/^Transform failed with \d+ errors?:\n/, ""));
|
|
80
|
+
return typeStripper(options.typeStrip)(code, options, sourcemap);
|
|
85
81
|
}
|
|
86
82
|
function stripTypes(code, options) {
|
|
87
83
|
return stripTypesMapped(code, options, false).code;
|
|
@@ -321,11 +317,11 @@ export function transformCell(code, options = {}) {
|
|
|
321
317
|
/**
|
|
322
318
|
* transformCell, plus the map between the cell's lines and the script's,
|
|
323
319
|
* for the debugger (#18). The rewrite keeps lines (see applyEdits), so the
|
|
324
|
-
* script's lines are
|
|
320
|
+
* script's lines are the stripper's, after the prelude's one.
|
|
325
321
|
*/
|
|
326
322
|
export function transformCellWithLines(code, options = {}) {
|
|
327
323
|
const stripped = stripTypesMapped(code, options, true);
|
|
328
|
-
return { script: PRELUDE + rewriteModuleSyntax(stripped.code) + SETTLE, lines: new LineMap(stripped.mappings, newlines(PRELUDE)) };
|
|
324
|
+
return { script: PRELUDE + rewriteModuleSyntax(stripped.code) + SETTLE, lines: new LineMap(stripped.mappings, newlines(PRELUDE), stripped.code) };
|
|
329
325
|
}
|
|
330
326
|
/**
|
|
331
327
|
* Like transformCell, for a target that evaluates cells itself, as global
|
package/dist/workerd-target.js
CHANGED
|
@@ -263,7 +263,7 @@ export class RawWorkerd {
|
|
|
263
263
|
killOnExit(child, () => {
|
|
264
264
|
child.kill("SIGKILL");
|
|
265
265
|
rmSync(dir, { recursive: true, force: true });
|
|
266
|
-
});
|
|
266
|
+
}, { dir });
|
|
267
267
|
// Cell output is captured inside the Worker; what workerd itself prints
|
|
268
268
|
// (its own warnings, errors from background work) goes to the server log.
|
|
269
269
|
// `early` keeps that output for a startup error only, and stops growing
|
package/dist/workers-project.js
CHANGED
|
@@ -9,11 +9,13 @@
|
|
|
9
9
|
// bundled next to the driver, so its Durable Object classes are exported
|
|
10
10
|
// from the same Worker. Neither wrangler nor Miniflare is a dependency of
|
|
11
11
|
// cdp-kernel: they're loaded from the project, so they match its version.
|
|
12
|
+
import process from "node:process";
|
|
12
13
|
import { builtinModules, createRequire } from "node:module";
|
|
13
14
|
import { randomBytes } from "node:crypto";
|
|
14
15
|
import { existsSync, readFileSync } from "node:fs";
|
|
15
16
|
import { dirname, join } from "node:path";
|
|
16
17
|
import { build } from "esbuild";
|
|
18
|
+
import { childPids, reap } from "./children.js";
|
|
17
19
|
import { runtimeDir } from "./package.js";
|
|
18
20
|
import { cellMeta, WORKER_MODULES } from "./workerd-target.js";
|
|
19
21
|
export const WRANGLER_CONFIGS = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
|
|
@@ -128,8 +130,12 @@ class MiniflareHost {
|
|
|
128
130
|
#mf;
|
|
129
131
|
#exitListeners = [];
|
|
130
132
|
#stopped = false;
|
|
131
|
-
|
|
133
|
+
#forget;
|
|
134
|
+
constructor(mf, channel, version, project, workerd) {
|
|
132
135
|
this.#mf = mf;
|
|
136
|
+
// Miniflare kills its workerd only on a normal exit; the helper in
|
|
137
|
+
// src/children.ts kills it if this process is SIGKILLed (#241).
|
|
138
|
+
this.#forget = workerd.map(reap);
|
|
133
139
|
this.channel = channel;
|
|
134
140
|
this.runtime = `miniflare ${version}`.trim();
|
|
135
141
|
this.project = project;
|
|
@@ -140,6 +146,8 @@ class MiniflareHost {
|
|
|
140
146
|
this.#stopped = true;
|
|
141
147
|
// dispose() ends workerd even mid-cell.
|
|
142
148
|
void this.#mf.dispose().catch(() => { }).finally(() => {
|
|
149
|
+
for (const forget of this.#forget)
|
|
150
|
+
forget();
|
|
143
151
|
for (const listener of this.#exitListeners)
|
|
144
152
|
listener(null);
|
|
145
153
|
});
|
|
@@ -178,6 +186,8 @@ export async function startMiniflare(configPath, cwd) {
|
|
|
178
186
|
// Miniflare 5 renamed Miniflare 4's defaultPersistRoot.
|
|
179
187
|
...(v5 ? { resourcePersistencePath: state } : { defaultPersistRoot: state }),
|
|
180
188
|
};
|
|
189
|
+
// Miniflare's workerd is this process's child; it doesn't say which.
|
|
190
|
+
const before = new Set(childPids(process.pid, "workerd"));
|
|
181
191
|
const mf = new miniflare.Miniflare(v5 && miniflare.convertV4MiniflareOptions ? miniflare.convertV4MiniflareOptions(options) : options);
|
|
182
192
|
try {
|
|
183
193
|
await mf.ready;
|
|
@@ -186,7 +196,8 @@ export async function startMiniflare(configPath, cwd) {
|
|
|
186
196
|
const res = await mf.dispatchFetch("http://cdpk/", { headers: { Upgrade: "websocket", "x-cdpk-token": token } });
|
|
187
197
|
if (res.status !== 101 || res.webSocket === null)
|
|
188
198
|
throw new Error(`the driver answered ${res.status}: ${await res.text()}`);
|
|
189
|
-
|
|
199
|
+
const workerd = childPids(process.pid, "workerd").filter((pid) => !before.has(pid));
|
|
200
|
+
return new MiniflareHost(mf, channelOf(res.webSocket), version, configPath, workerd);
|
|
190
201
|
}
|
|
191
202
|
catch (err) {
|
|
192
203
|
await mf.dispose().catch(() => { });
|
package/dist/zmtp.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export declare class Queue<T> {
|
|
|
25
25
|
push(item: T): void;
|
|
26
26
|
pop(): Promise<T>;
|
|
27
27
|
}
|
|
28
|
+
/** Resolves once `server` is listening, after any retries. */
|
|
29
|
+
export declare function listening(server: Server): Promise<void>;
|
|
28
30
|
/** REP socket for the heartbeat channel: echo everything. */
|
|
29
31
|
export declare function startHeartbeat(port: number, host: string): Server;
|
|
30
32
|
export interface RoutedMessage {
|
package/dist/zmtp.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
import { Buffer } from "node:buffer";
|
|
9
9
|
import { createServer } from "node:net";
|
|
10
10
|
import { randomBytes } from "node:crypto";
|
|
11
|
+
import process from "node:process";
|
|
12
|
+
import { portHolders } from "./port-holder.js";
|
|
11
13
|
const ENC = new TextEncoder();
|
|
12
14
|
/** Pull-style reader over a socket's 'data' events. */
|
|
13
15
|
export class SocketReader {
|
|
@@ -154,6 +156,12 @@ function listen(port, host, onConn) {
|
|
|
154
156
|
const deadline = Date.now() + LISTEN_RETRY_MS;
|
|
155
157
|
const onError = (err) => {
|
|
156
158
|
if (err.code !== "EADDRINUSE" || Date.now() >= deadline) {
|
|
159
|
+
// Who kept it, for a port that never came free (#257).
|
|
160
|
+
if (err.code === "EADDRINUSE") {
|
|
161
|
+
const holders = portHolders(port).map((line) => ` ${line}\n`).join("") || " unknown\n";
|
|
162
|
+
process.stderr.write(`[cdp-kernel] port ${port} still in use after ${LISTEN_RETRY_MS} ms; this kernel is pid ${process.pid} ` +
|
|
163
|
+
`(ppid ${process.ppid}, ${process.execPath} ${process.version}); the port's sockets:\n${holders}`);
|
|
164
|
+
}
|
|
157
165
|
server.off("error", onError);
|
|
158
166
|
server.emit("error", err);
|
|
159
167
|
return;
|
|
@@ -187,6 +195,10 @@ export class Queue {
|
|
|
187
195
|
return new Promise((resolve) => this.#waiters.push(resolve));
|
|
188
196
|
}
|
|
189
197
|
}
|
|
198
|
+
/** Resolves once `server` is listening, after any retries. */
|
|
199
|
+
export function listening(server) {
|
|
200
|
+
return server.listening ? Promise.resolve() : new Promise((resolve) => server.once("listening", () => resolve()));
|
|
201
|
+
}
|
|
190
202
|
/** REP socket for the heartbeat channel: echo everything. */
|
|
191
203
|
export function startHeartbeat(port, host) {
|
|
192
204
|
return listen(port, host, (socket) => {
|