@tlockney/cdp-kernel 0.3.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 -404
- package/THIRD-PARTY-NOTICES +62 -0
- package/dist/bun-target.d.ts +4 -2
- package/dist/bun-target.js +35 -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 +42 -0
- package/dist/debugger.js +545 -0
- package/dist/globals.d.ts +3 -3
- package/dist/globals.js +3 -3
- package/dist/history.d.ts +18 -0
- package/dist/history.js +45 -0
- package/dist/kernel.js +211 -32
- package/dist/main.js +3 -1
- package/dist/mod.d.ts +5 -3
- package/dist/mod.js +5 -3
- package/dist/murmur2.d.ts +2 -0
- package/dist/murmur2.js +41 -0
- package/dist/package.d.ts +16 -2
- package/dist/package.js +26 -3
- package/dist/sourcemap.d.ts +22 -0
- package/dist/sourcemap.js +86 -0
- package/dist/target.d.ts +39 -3
- package/dist/target.js +281 -30
- package/dist/text.d.ts +11 -1
- package/dist/text.js +72 -2
- package/dist/transform.d.ts +19 -0
- package/dist/transform.js +74 -13
- package/dist/wire.d.ts +8 -1
- package/dist/wire.js +3 -1
- package/dist/workerd-target.d.ts +9 -1
- package/dist/workerd-target.js +108 -9
- 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 +10 -4
- package/runtime/bootstrap.mjs +138 -12
- 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 +17 -2
- package/runtime/display.d.mts +2 -0
- package/runtime/display.mjs +17 -2
- package/runtime/evaluator.d.mts +4 -0
- package/runtime/evaluator.mjs +8 -0
- package/runtime/introspect.d.mts +13 -0
- package/runtime/introspect.mjs +120 -0
- package/runtime/node-hooks.d.mts +61 -1
- package/runtime/node-hooks.mjs +354 -21
- package/runtime/workerd-worker.mjs +82 -6
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
|
}
|
|
@@ -51,6 +104,23 @@ export function exprAtCursor(code, cursor) {
|
|
|
51
104
|
const m = /[\w$.]+$/.exec(code.slice(0, cursor));
|
|
52
105
|
return m ? m[0] : "";
|
|
53
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* The dotted name an inspect_request means (#10): the one the cursor is in
|
|
109
|
+
* or just after, or, right after "name(" while typing a call, that callee.
|
|
110
|
+
* Null when it isn't a plain path; nothing else is evaluated.
|
|
111
|
+
*/
|
|
112
|
+
export function inspectTarget(code, cursor) {
|
|
113
|
+
let end = cursor;
|
|
114
|
+
// Inside a call's parentheses with nothing typed yet: "add(|".
|
|
115
|
+
const before = code.slice(0, cursor);
|
|
116
|
+
const call = /([\w$.]+)\(\s*$/.exec(before);
|
|
117
|
+
if (call)
|
|
118
|
+
end = cursor - (before.length - before.lastIndexOf("(", cursor));
|
|
119
|
+
const head = /[\w$.]*$/.exec(code.slice(0, end))?.[0] ?? "";
|
|
120
|
+
const tail = call ? "" : (/^[\w$]*/.exec(code.slice(end))?.[0] ?? "");
|
|
121
|
+
const path = (head + tail).replace(/\.$/, "");
|
|
122
|
+
return /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(path) ? path : null;
|
|
123
|
+
}
|
|
54
124
|
// Jupyter measures cursor_pos in Unicode codepoints; JS strings index UTF-16
|
|
55
125
|
// code units (denoland/deno#22771).
|
|
56
126
|
export function codePointToUtf16Index(str, cpOffset) {
|
package/dist/transform.d.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
+
import { LineMap } from "./sourcemap.ts";
|
|
1
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();";
|
|
2
12
|
export declare class CellSyntaxError extends Error {
|
|
3
13
|
name: string;
|
|
4
14
|
}
|
|
@@ -23,6 +33,15 @@ export interface CellTransformOptions {
|
|
|
23
33
|
/** Rewrites module-only syntax in plain JavaScript into script syntax. */
|
|
24
34
|
export declare function rewriteModuleSyntax(js: string): string;
|
|
25
35
|
export declare function transformCell(code: string, options?: CellTransformOptions): string;
|
|
36
|
+
/**
|
|
37
|
+
* transformCell, plus the map between the cell's lines and the script's,
|
|
38
|
+
* for the debugger (#18). The rewrite keeps lines (see applyEdits), so the
|
|
39
|
+
* script's lines are esbuild's, after the prelude's one.
|
|
40
|
+
*/
|
|
41
|
+
export declare function transformCellWithLines(code: string, options?: CellTransformOptions): {
|
|
42
|
+
script: string;
|
|
43
|
+
lines: LineMap;
|
|
44
|
+
};
|
|
26
45
|
/**
|
|
27
46
|
* Like transformCell, for a target that evaluates cells itself, as global
|
|
28
47
|
* code (an indirect eval), instead of through the inspector's replMode: see
|
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
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { transformSync } from "esbuild";
|
|
18
18
|
import { parse } from "acorn";
|
|
19
19
|
import { full, recursive } from "acorn-walk";
|
|
20
|
+
import { LineMap } from "./sourcemap.js";
|
|
20
21
|
export const SHIM = "__cdpk";
|
|
21
22
|
// Each cell first yields one macrotask. User code then runs as an ordinary
|
|
22
23
|
// task instead of nested inside the inspector's dispatch of Runtime.evaluate,
|
|
@@ -24,6 +25,15 @@ export const SHIM = "__cdpk";
|
|
|
24
25
|
// interrupt a busy loop. (Within that dispatch the inspector processes no
|
|
25
26
|
// other messages, so an interrupt would never be seen.)
|
|
26
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();`;
|
|
27
37
|
export class CellSyntaxError extends Error {
|
|
28
38
|
name = "SyntaxError";
|
|
29
39
|
}
|
|
@@ -33,7 +43,14 @@ function jsxSettings(jsx) {
|
|
|
33
43
|
}
|
|
34
44
|
return { jsx: "transform", jsxFactory: jsx?.factory, jsxFragment: jsx?.fragment };
|
|
35
45
|
}
|
|
36
|
-
|
|
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;
|
|
49
|
+
/** esbuild's output, with its source map's `mappings` when asked for. */
|
|
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
|
+
}
|
|
37
54
|
const compilerOptions = {
|
|
38
55
|
verbatimModuleSyntax: true,
|
|
39
56
|
...(options.experimentalDecorators ? { experimentalDecorators: true } : {}),
|
|
@@ -42,13 +59,22 @@ function stripTypes(code, options) {
|
|
|
42
59
|
let firstError;
|
|
43
60
|
for (const loader of ["tsx", "ts"]) {
|
|
44
61
|
try {
|
|
45
|
-
|
|
62
|
+
const result = transformSync(code, {
|
|
46
63
|
loader,
|
|
47
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 },
|
|
48
68
|
...jsxSettings(options.jsx),
|
|
49
69
|
tsconfigRaw: { compilerOptions },
|
|
50
70
|
logLevel: "silent",
|
|
51
|
-
|
|
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 };
|
|
52
78
|
}
|
|
53
79
|
catch (err) {
|
|
54
80
|
firstError ??= err;
|
|
@@ -57,6 +83,9 @@ function stripTypes(code, options) {
|
|
|
57
83
|
const message = firstError instanceof Error ? firstError.message : String(firstError);
|
|
58
84
|
throw new CellSyntaxError(message.replace(/^Transform failed with \d+ errors?:\n/, ""));
|
|
59
85
|
}
|
|
86
|
+
function stripTypes(code, options) {
|
|
87
|
+
return stripTypesMapped(code, options, false).code;
|
|
88
|
+
}
|
|
60
89
|
function importedName(spec) {
|
|
61
90
|
const imported = spec.imported;
|
|
62
91
|
if (!imported)
|
|
@@ -69,8 +98,9 @@ function rewriteImport(code, node, index) {
|
|
|
69
98
|
? `, { with: { ${node.attributes.map((a) => code.slice(a.start, a.end)).join(", ")} } }`
|
|
70
99
|
: "";
|
|
71
100
|
const load = `await ${SHIM}.import(${source}${attrs})`;
|
|
101
|
+
// `void`: as a cell's last statement, the import isn't its result (#139).
|
|
72
102
|
if (node.specifiers.length === 0)
|
|
73
|
-
return
|
|
103
|
+
return `void ${load};`;
|
|
74
104
|
const ns = `${SHIM}_m${index}`;
|
|
75
105
|
const bindings = node.specifiers.map((spec) => {
|
|
76
106
|
const local = String(spec.local.name);
|
|
@@ -129,7 +159,13 @@ export function rewriteModuleSyntax(js) {
|
|
|
129
159
|
}
|
|
130
160
|
case "ExportDefaultDeclaration": {
|
|
131
161
|
const decl = node;
|
|
132
|
-
|
|
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: ");" });
|
|
133
169
|
break;
|
|
134
170
|
}
|
|
135
171
|
case "ExportAllDeclaration":
|
|
@@ -140,12 +176,25 @@ export function rewriteModuleSyntax(js) {
|
|
|
140
176
|
// keyword edit doesn't overlap its arguments), so apply back to front.
|
|
141
177
|
return applyEdits(js, edits);
|
|
142
178
|
}
|
|
179
|
+
function newlines(text) {
|
|
180
|
+
let count = 0;
|
|
181
|
+
for (const char of text)
|
|
182
|
+
if (char === "\n")
|
|
183
|
+
count++;
|
|
184
|
+
return count;
|
|
185
|
+
}
|
|
143
186
|
function applyEdits(js, edits) {
|
|
144
187
|
// Back to front; at a shared start, a replacement goes before an insertion.
|
|
145
188
|
edits.sort((a, b) => b.start - a.start || b.end - a.end);
|
|
146
189
|
let out = js;
|
|
147
|
-
for (const edit of edits)
|
|
148
|
-
|
|
190
|
+
for (const edit of edits) {
|
|
191
|
+
// Keep every later line where it was, so a cell's lines still map to
|
|
192
|
+
// esbuild's (the debugger relies on it, #18): a multi-line import
|
|
193
|
+
// rewritten to one line is padded with the newlines it replaced.
|
|
194
|
+
const lost = newlines(js.slice(edit.start, edit.end)) - newlines(edit.text);
|
|
195
|
+
const text = lost > 0 ? edit.text + "\n".repeat(lost) : edit.text;
|
|
196
|
+
out = out.slice(0, edit.start) + text + out.slice(edit.end);
|
|
197
|
+
}
|
|
149
198
|
return out;
|
|
150
199
|
}
|
|
151
200
|
/** Adds the names a binding pattern declares. */
|
|
@@ -178,8 +227,8 @@ function patternNames(pattern, names) {
|
|
|
178
227
|
* surrounding script, which a global-code eval turns into a property of
|
|
179
228
|
* globalThis. Inside the function the declarations become assignments to
|
|
180
229
|
* those globals:
|
|
181
|
-
* const { a } = o; class K {} -> var a, K;
|
|
182
|
-
* (
|
|
230
|
+
* const { a } = o; class K {} -> var a, K; (async () => {
|
|
231
|
+
* ({ a } = o); K = class K {}; })()
|
|
183
232
|
* So later cells, and closures from this one, all see one binding. A `var`
|
|
184
233
|
* in a top-level block or loop head is global too, as in a script; block
|
|
185
234
|
* scoped and function-local declarations stay where they are. Functions
|
|
@@ -253,18 +302,30 @@ function hoistForEval(js) {
|
|
|
253
302
|
edits.push({ start: stmt.end, end: stmt.end, text: ";" });
|
|
254
303
|
}
|
|
255
304
|
else if (stmt.type === "ExpressionStatement" && i === program.body.length - 1) {
|
|
256
|
-
|
|
305
|
+
// Parenthesized, so `1, 2` returns [2] rather than [1, 2] (#140).
|
|
306
|
+
replace(stmt, `return [(${src(stmt.expression)})];`);
|
|
257
307
|
}
|
|
258
308
|
else {
|
|
259
309
|
nestedVars(stmt);
|
|
260
310
|
}
|
|
261
311
|
});
|
|
262
|
-
|
|
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(", ")}; ` : "";
|
|
263
315
|
const publish = functions.map((name) => ` globalThis.${name} = ${name};`).join("");
|
|
264
316
|
return `${declare}(async () => { 'use strict';${publish}\n${applyEdits(js, edits)}\n})()`;
|
|
265
317
|
}
|
|
266
318
|
export function transformCell(code, options = {}) {
|
|
267
|
-
return PRELUDE + rewriteModuleSyntax(stripTypes(code, options));
|
|
319
|
+
return PRELUDE + rewriteModuleSyntax(stripTypes(code, options)) + SETTLE;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* transformCell, plus the map between the cell's lines and the script's,
|
|
323
|
+
* for the debugger (#18). The rewrite keeps lines (see applyEdits), so the
|
|
324
|
+
* script's lines are esbuild's, after the prelude's one.
|
|
325
|
+
*/
|
|
326
|
+
export function transformCellWithLines(code, options = {}) {
|
|
327
|
+
const stripped = stripTypesMapped(code, options, true);
|
|
328
|
+
return { script: PRELUDE + rewriteModuleSyntax(stripped.code) + SETTLE, lines: new LineMap(stripped.mappings, newlines(PRELUDE)) };
|
|
268
329
|
}
|
|
269
330
|
/**
|
|
270
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;
|
package/dist/wire.js
CHANGED
|
@@ -41,7 +41,7 @@ export function makeHeader(session, msgType) {
|
|
|
41
41
|
}
|
|
42
42
|
export function encodeMessage(session, key, msgType, content, options = {}) {
|
|
43
43
|
const parts = [
|
|
44
|
-
makeHeader(session, msgType),
|
|
44
|
+
options.header ?? makeHeader(session, msgType),
|
|
45
45
|
options.parentHeader ?? {},
|
|
46
46
|
options.metadata ?? {},
|
|
47
47
|
content,
|
|
@@ -70,6 +70,8 @@ function parseObject(frame) {
|
|
|
70
70
|
}
|
|
71
71
|
return value;
|
|
72
72
|
}
|
|
73
|
+
// Only msg_id and msg_type are required of a received header; nothing
|
|
74
|
+
// reads the rest, so a client that leaves them out still works.
|
|
73
75
|
function isHeader(value) {
|
|
74
76
|
return typeof value.msg_id === "string" && typeof value.msg_type === "string";
|
|
75
77
|
}
|
package/dist/workerd-target.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type CdpParams, type EvaluateResult, type RemoteObject } from "./cdp.ts";
|
|
2
2
|
import { transformCellForEval } from "./transform.ts";
|
|
3
3
|
import { type KernelTarget, type TargetEvents } from "./target.ts";
|
|
4
4
|
import { type WsChannel } from "./ws-client.ts";
|
|
@@ -53,6 +53,12 @@ export interface WorkerHost {
|
|
|
53
53
|
stop(now: boolean): void;
|
|
54
54
|
onExit(listener: (code: number | null) => void): void;
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Removes workerd directories under `root` whose kernel has died without
|
|
58
|
+
* cleaning up (SIGKILL runs no exit hook), killing their workerd if it's
|
|
59
|
+
* still running (#220).
|
|
60
|
+
*/
|
|
61
|
+
export declare function sweepStaleWorkerdDirs(root?: string): void;
|
|
56
62
|
/** The driver on a workerd of its own, behind a Unix socket. */
|
|
57
63
|
export declare class RawWorkerd implements WorkerHost {
|
|
58
64
|
#private;
|
|
@@ -115,6 +121,8 @@ export declare class WorkerdTarget implements KernelTarget {
|
|
|
115
121
|
stack: string;
|
|
116
122
|
}>;
|
|
117
123
|
propertyNames(expression: string): Promise<string[]>;
|
|
124
|
+
/** Resolved by property descriptor in the driver, so nothing runs (#10). */
|
|
125
|
+
inspect(path: string, detail: number): Promise<string | null>;
|
|
118
126
|
/** None: top-level declarations here are properties of globalThis. */
|
|
119
127
|
lexicalNames(): Promise<string[]>;
|
|
120
128
|
/** Never asked for: prompt() in workerd answers null without asking. */
|
package/dist/workerd-target.js
CHANGED
|
@@ -18,20 +18,22 @@ import { Buffer } from "node:buffer";
|
|
|
18
18
|
import process from "node:process";
|
|
19
19
|
import { execFileSync, spawn } from "node:child_process";
|
|
20
20
|
import { randomBytes } from "node:crypto";
|
|
21
|
-
import { copyFileSync, existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { copyFileSync, existsSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, rmSync, writeFileSync } from "node:fs";
|
|
22
22
|
import { builtinModules } from "node:module";
|
|
23
23
|
import { tmpdir } from "node:os";
|
|
24
24
|
import { dirname, join } from "node:path";
|
|
25
25
|
import { pathToFileURL } from "node:url";
|
|
26
26
|
import { build } from "esbuild";
|
|
27
|
+
import { asEvaluateResult } from "./cdp.js";
|
|
27
28
|
import { transformCellForEval } from "./transform.js";
|
|
28
29
|
import { runtimeDir } from "./package.js";
|
|
30
|
+
import { killOnExit } from "./children.js";
|
|
29
31
|
import { Interrupted, isTargetMessage } from "./target.js";
|
|
30
32
|
import { connectUnixWebSocket } from "./ws-client.js";
|
|
31
33
|
import { findWranglerConfig, startMiniflare } from "./workers-project.js";
|
|
32
|
-
import {
|
|
34
|
+
import { ensureInstalledAsync, findLockFile, jsrToNpm, parsePackageSpecifier } from "../runtime/node-hooks.mjs";
|
|
33
35
|
/** The driver Worker's modules; the first is its main module. */
|
|
34
|
-
export const WORKER_MODULES = ["workerd-worker.mjs", "display.mjs", "comm.mjs", "widget.mjs", "evaluator.mjs"];
|
|
36
|
+
export const WORKER_MODULES = ["workerd-worker.mjs", "display.mjs", "comm.mjs", "widget.mjs", "evaluator.mjs", "introspect.mjs"];
|
|
35
37
|
/** See BunTarget: how long an interrupted cell has to show it isn't busy. */
|
|
36
38
|
export const INTERRUPT_GRACE_MS = 500;
|
|
37
39
|
export function restartNote(runtime) {
|
|
@@ -121,7 +123,8 @@ export async function bundleModule(specifier, cwd) {
|
|
|
121
123
|
? jsrToNpm(specifier.slice(4))
|
|
122
124
|
: null;
|
|
123
125
|
if (pkg) {
|
|
124
|
-
|
|
126
|
+
// Async: a sync install would stall the heartbeat and interrupts (#149).
|
|
127
|
+
const { dir, alias } = await ensureInstalledAsync(pkg, "npm", { lockFile: findLockFile(cwd) });
|
|
125
128
|
entry = alias + pkg.subpath;
|
|
126
129
|
resolveDir = dir;
|
|
127
130
|
}
|
|
@@ -168,6 +171,65 @@ export function cellMeta(cwd) {
|
|
|
168
171
|
const replUrl = new URL("$cdpk$repl.mjs", pathToFileURL(cwd.endsWith("/") ? cwd : cwd + "/"));
|
|
169
172
|
return { url: replUrl.href, dirname: cwd, filename: join(cwd, "$cdpk$repl.mjs") };
|
|
170
173
|
}
|
|
174
|
+
const WORKERD_DIR_PREFIX = "cdpk-workerd-";
|
|
175
|
+
/** In each workerd directory: `{ host, workerd }` pids. */
|
|
176
|
+
const OWNER_FILE = "owner.json";
|
|
177
|
+
function pidAlive(pid) {
|
|
178
|
+
try {
|
|
179
|
+
process.kill(pid, 0);
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
return err.code === "EPERM";
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** Whether `pid` is still the workerd serving `dir`, not a reused pid. */
|
|
187
|
+
function isWorkerdIn(pid, dir) {
|
|
188
|
+
try {
|
|
189
|
+
if (process.platform === "linux")
|
|
190
|
+
return readlinkSync(`/proc/${pid}/cwd`) === dir;
|
|
191
|
+
const args = execFileSync("ps", ["-o", "args=", "-p", String(pid)], { encoding: "utf8" });
|
|
192
|
+
return /workerd/.test(args) && args.includes("config.capnp");
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Removes workerd directories under `root` whose kernel has died without
|
|
200
|
+
* cleaning up (SIGKILL runs no exit hook), killing their workerd if it's
|
|
201
|
+
* still running (#220).
|
|
202
|
+
*/
|
|
203
|
+
export function sweepStaleWorkerdDirs(root = tmpdir()) {
|
|
204
|
+
let names;
|
|
205
|
+
try {
|
|
206
|
+
names = readdirSync(root).filter((name) => name.startsWith(WORKERD_DIR_PREFIX));
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
for (const name of names) {
|
|
212
|
+
const dir = join(root, name);
|
|
213
|
+
let owner;
|
|
214
|
+
try {
|
|
215
|
+
owner = JSON.parse(readFileSync(join(dir, OWNER_FILE), "utf8"));
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
continue; // Another user's, from before owner files, or being set up.
|
|
219
|
+
}
|
|
220
|
+
if (typeof owner.host !== "number" || pidAlive(owner.host))
|
|
221
|
+
continue;
|
|
222
|
+
if (typeof owner.workerd === "number" && isWorkerdIn(owner.workerd, dir)) {
|
|
223
|
+
try {
|
|
224
|
+
process.kill(owner.workerd, "SIGKILL");
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// Gone already.
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
rmSync(dir, { recursive: true, force: true });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
171
233
|
/** The driver on a workerd of its own, behind a Unix socket. */
|
|
172
234
|
export class RawWorkerd {
|
|
173
235
|
runtime;
|
|
@@ -184,7 +246,9 @@ export class RawWorkerd {
|
|
|
184
246
|
const source = await runtimeDir(new URL("../runtime/", import.meta.url).href);
|
|
185
247
|
// mkdtemp makes the directory 0700: only this user reaches the socket,
|
|
186
248
|
// and the token stops anything else running as them by accident.
|
|
187
|
-
|
|
249
|
+
sweepStaleWorkerdDirs();
|
|
250
|
+
const dir = mkdtempSync(join(tmpdir(), WORKERD_DIR_PREFIX));
|
|
251
|
+
writeFileSync(join(dir, OWNER_FILE), JSON.stringify({ host: process.pid }));
|
|
188
252
|
const socket = join(dir, "sock");
|
|
189
253
|
for (const name of WORKER_MODULES)
|
|
190
254
|
copyFileSync(join(source, name), join(dir, name));
|
|
@@ -194,11 +258,21 @@ export class RawWorkerd {
|
|
|
194
258
|
// --experimental: the UnsafeEval binding.
|
|
195
259
|
const child = spawn(workerd, ["serve", "config.capnp", "--experimental"], { cwd: dir, stdio: ["ignore", "pipe", "pipe"], env });
|
|
196
260
|
child.once("exit", () => rmSync(dir, { recursive: true, force: true }));
|
|
261
|
+
if (child.pid !== undefined)
|
|
262
|
+
writeFileSync(join(dir, OWNER_FILE), JSON.stringify({ host: process.pid, workerd: child.pid }));
|
|
263
|
+
killOnExit(child, () => {
|
|
264
|
+
child.kill("SIGKILL");
|
|
265
|
+
rmSync(dir, { recursive: true, force: true });
|
|
266
|
+
});
|
|
197
267
|
// Cell output is captured inside the Worker; what workerd itself prints
|
|
198
268
|
// (its own warnings, errors from background work) goes to the server log.
|
|
269
|
+
// `early` keeps that output for a startup error only, and stops growing
|
|
270
|
+
// once the session is open.
|
|
199
271
|
let early = "";
|
|
272
|
+
let starting = true;
|
|
200
273
|
const log = (chunk) => {
|
|
201
|
-
|
|
274
|
+
if (starting)
|
|
275
|
+
early += chunk.toString();
|
|
202
276
|
process.stderr.write(`[cdp-kernel workerd] ${chunk}`);
|
|
203
277
|
};
|
|
204
278
|
child.stdout?.on("data", log);
|
|
@@ -221,7 +295,10 @@ export class RawWorkerd {
|
|
|
221
295
|
}
|
|
222
296
|
})();
|
|
223
297
|
try {
|
|
224
|
-
|
|
298
|
+
const host = new RawWorkerd(child, await Promise.race([ready, exited]), compatibilityDate);
|
|
299
|
+
starting = false;
|
|
300
|
+
early = "";
|
|
301
|
+
return host;
|
|
225
302
|
}
|
|
226
303
|
catch (err) {
|
|
227
304
|
child.kill();
|
|
@@ -229,7 +306,16 @@ export class RawWorkerd {
|
|
|
229
306
|
}
|
|
230
307
|
}
|
|
231
308
|
stop(now) {
|
|
232
|
-
|
|
309
|
+
if (now) {
|
|
310
|
+
this.#child.kill("SIGKILL");
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
this.#child.kill("SIGTERM");
|
|
314
|
+
// A busy isolate keeps workerd's event loop from ever seeing SIGTERM (#128).
|
|
315
|
+
setTimeout(() => {
|
|
316
|
+
if (this.#child.exitCode === null && this.#child.signalCode === null)
|
|
317
|
+
this.#child.kill("SIGKILL");
|
|
318
|
+
}, 3000).unref();
|
|
233
319
|
}
|
|
234
320
|
onExit(listener) {
|
|
235
321
|
this.#child.once("exit", listener);
|
|
@@ -342,6 +428,9 @@ export class WorkerdTarget {
|
|
|
342
428
|
this.#pending.clear();
|
|
343
429
|
}
|
|
344
430
|
#request(op, params = {}, interruptible = false) {
|
|
431
|
+
// The channel drops writes after it closes, so nothing would answer.
|
|
432
|
+
if (this.#exitedOnce)
|
|
433
|
+
return Promise.reject(new Abandoned("the workerd session is closed"));
|
|
345
434
|
const id = ++this.#nextId;
|
|
346
435
|
return new Promise((resolve, reject) => {
|
|
347
436
|
this.#pending.set(id, { resolve, reject });
|
|
@@ -377,7 +466,12 @@ export class WorkerdTarget {
|
|
|
377
466
|
if (!modules[specifier].startsWith("Promise.reject("))
|
|
378
467
|
this.#sent.add(specifier);
|
|
379
468
|
}
|
|
380
|
-
|
|
469
|
+
const { failedModules, ...reply } = await this.#interruptibly("eval", { code: script, modules }, "Execution");
|
|
470
|
+
// A module that threw while loading is rebuilt for its next import (#147).
|
|
471
|
+
if (Array.isArray(failedModules))
|
|
472
|
+
for (const specifier of failedModules)
|
|
473
|
+
this.#sent.delete(specifier);
|
|
474
|
+
return asEvaluateResult(reply);
|
|
381
475
|
}
|
|
382
476
|
/**
|
|
383
477
|
* A cell that's waiting on something leaves the driver free: it answers a
|
|
@@ -435,6 +529,11 @@ export class WorkerdTarget {
|
|
|
435
529
|
const reply = await this.#request("props", { expr: expression }).catch(() => null);
|
|
436
530
|
return Array.isArray(reply?.names) ? reply.names.filter((n) => typeof n === "string") : [];
|
|
437
531
|
}
|
|
532
|
+
/** Resolved by property descriptor in the driver, so nothing runs (#10). */
|
|
533
|
+
async inspect(path, detail) {
|
|
534
|
+
const reply = await this.#request("inspect", { path, detail }).catch(() => null);
|
|
535
|
+
return typeof reply?.text === "string" ? reply.text : null;
|
|
536
|
+
}
|
|
438
537
|
/** None: top-level declarations here are properties of globalThis. */
|
|
439
538
|
lexicalNames() {
|
|
440
539
|
return Promise.resolve([]);
|
package/dist/ws-client.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
export declare const OP_CONTINUATION = 0;
|
|
3
3
|
export declare const OP_TEXT = 1;
|
|
4
|
+
export declare const OP_BINARY = 2;
|
|
4
5
|
export declare const OP_CLOSE = 8;
|
|
5
6
|
export declare const OP_PING = 9;
|
|
6
7
|
export declare const OP_PONG = 10;
|
|
@@ -11,7 +12,10 @@ export interface Frame {
|
|
|
11
12
|
}
|
|
12
13
|
/** One frame; clients must mask what they send. */
|
|
13
14
|
export declare function encodeFrame(opcode: number, payload: Buffer, mask?: Buffer | null): Buffer;
|
|
14
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Splits a byte stream into frames, holding back a partial one. Chunks of a
|
|
17
|
+
* large frame are only joined once the whole frame has arrived.
|
|
18
|
+
*/
|
|
15
19
|
export declare class FrameDecoder {
|
|
16
20
|
#private;
|
|
17
21
|
push(chunk: Buffer): Frame[];
|