@tlockney/cdp-kernel 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -478
- package/THIRD-PARTY-NOTICES +62 -0
- package/dist/bun-target.d.ts +2 -2
- package/dist/bun-target.js +30 -8
- package/dist/cdp.d.ts +8 -1
- package/dist/cdp.js +31 -11
- package/dist/celld-target.d.ts +1 -1
- package/dist/celld-target.js +54 -19
- package/dist/children.d.ts +29 -0
- package/dist/children.js +114 -0
- package/dist/debugger.d.ts +1 -1
- package/dist/debugger.js +21 -10
- package/dist/deno-types.d.ts +30 -0
- package/dist/deno-types.js +93 -0
- package/dist/globals.d.ts +3 -3
- package/dist/globals.js +3 -3
- package/dist/kernel.d.ts +7 -1
- package/dist/kernel.js +348 -68
- package/dist/language-service-worker.d.ts +1 -0
- package/dist/language-service-worker.js +88 -0
- package/dist/language-service.d.ts +64 -0
- package/dist/language-service.js +128 -0
- package/dist/main.js +102 -8
- package/dist/mod.d.ts +6 -3
- package/dist/mod.js +6 -3
- package/dist/notebook-types.d.ts +62 -0
- package/dist/notebook-types.js +310 -0
- package/dist/package.d.ts +2 -2
- package/dist/package.js +3 -3
- package/dist/parent.d.ts +12 -0
- package/dist/parent.js +39 -0
- package/dist/port-holder.d.ts +13 -0
- package/dist/port-holder.js +105 -0
- package/dist/sourcemap.d.ts +7 -2
- package/dist/sourcemap.js +9 -3
- package/dist/strip-esbuild.d.ts +2 -0
- package/dist/strip-esbuild.js +44 -0
- package/dist/strip-sucrase.d.ts +2 -0
- package/dist/strip-sucrase.js +78 -0
- package/dist/target.d.ts +58 -6
- package/dist/target.js +412 -54
- package/dist/text.d.ts +5 -1
- package/dist/text.js +55 -2
- package/dist/transform.d.ts +30 -1
- package/dist/transform.js +68 -44
- package/dist/wire.d.ts +8 -1
- package/dist/wire.js +3 -1
- package/dist/workerd-target.d.ts +7 -1
- package/dist/workerd-target.js +102 -8
- package/dist/workers-project.js +13 -2
- package/dist/ws-client.d.ts +5 -1
- package/dist/ws-client.js +46 -13
- package/dist/zmtp.d.ts +9 -1
- package/dist/zmtp.js +42 -1
- package/package.json +17 -6
- package/runtime/bootstrap.mjs +233 -42
- package/runtime/bun-channel.d.mts +3 -0
- package/runtime/bun-channel.mjs +2 -2
- package/runtime/comm.mjs +11 -1
- package/runtime/deno-compat.d.mts +2 -0
- package/runtime/deno-compat.mjs +14 -1
- package/runtime/display.mjs +8 -1
- package/runtime/introspect.mjs +1 -1
- package/runtime/node-hooks.d.mts +48 -0
- package/runtime/node-hooks.mjs +257 -41
- package/runtime/workerd-worker.mjs +77 -6
package/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
|
}
|
|
@@ -20,14 +29,34 @@ export type JsxOptions = {
|
|
|
20
29
|
export interface CellTransformOptions {
|
|
21
30
|
jsx?: JsxOptions;
|
|
22
31
|
experimentalDecorators?: boolean;
|
|
32
|
+
/** What removes a cell's types and compiles its JSX; esbuild by default (#238, #234). */
|
|
33
|
+
typeStrip?: TypeStripperName | TypeStripper;
|
|
23
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>;
|
|
24
53
|
/** Rewrites module-only syntax in plain JavaScript into script syntax. */
|
|
25
54
|
export declare function rewriteModuleSyntax(js: string): string;
|
|
26
55
|
export declare function transformCell(code: string, options?: CellTransformOptions): string;
|
|
27
56
|
/**
|
|
28
57
|
* transformCell, plus the map between the cell's lines and the script's,
|
|
29
58
|
* for the debugger (#18). The rewrite keeps lines (see applyEdits), so the
|
|
30
|
-
* script's lines are
|
|
59
|
+
* script's lines are the stripper's, after the prelude's one.
|
|
31
60
|
*/
|
|
32
61
|
export declare function transformCellWithLines(code: string, options?: CellTransformOptions): {
|
|
33
62
|
script: string;
|
package/dist/transform.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
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
|
-
// 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
|
|
@@ -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";
|
|
@@ -25,45 +24,60 @@ export const SHIM = "__cdpk";
|
|
|
25
24
|
// interrupt a busy loop. (Within that dispatch the inspector processes no
|
|
26
25
|
// other messages, so an interrupt would never be seen.)
|
|
27
26
|
const PRELUDE = `'use strict'; await ${SHIM}.tick();\n`;
|
|
27
|
+
/**
|
|
28
|
+
* Each replMode cell also ends by yielding one macrotask, so the script
|
|
29
|
+
* settles with no other microtasks queued ahead of the inspector's
|
|
30
|
+
* then-callback. Until that callback runs, the inspector holds the result
|
|
31
|
+
* promise only weakly, and a GC in between fails the evaluate with "Promise
|
|
32
|
+
* was collected" (#55). A declaration has no completion value, so the
|
|
33
|
+
* cell's value is unchanged; replMode lets `let` be declared again.
|
|
34
|
+
*/
|
|
35
|
+
export const SETTLE = `\n;let ${SHIM}_settled = await ${SHIM}.tick();`;
|
|
28
36
|
export class CellSyntaxError extends Error {
|
|
29
37
|
name = "SyntaxError";
|
|
30
38
|
}
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
return { jsx: "automatic", jsxImportSource: jsx.importSource, jsxDev: jsx.development };
|
|
34
|
-
}
|
|
35
|
-
return { jsx: "transform", jsxFactory: jsx?.factory, jsxFragment: jsx?.fragment };
|
|
39
|
+
export function isTypeStripperName(value) {
|
|
40
|
+
return value === "esbuild" || value === "sucrase";
|
|
36
41
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
let
|
|
45
|
-
|
|
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) {
|
|
46
51
|
try {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
...jsxSettings(options.jsx),
|
|
51
|
-
tsconfigRaw: { compilerOptions },
|
|
52
|
-
logLevel: "silent",
|
|
53
|
-
...(sourcemap ? { sourcemap: "external" } : {}),
|
|
54
|
-
});
|
|
55
|
-
const map = sourcemap ? JSON.parse(result.map) : null;
|
|
56
|
-
const mappings = typeof map?.mappings === "string"
|
|
57
|
-
? map.mappings
|
|
58
|
-
: "";
|
|
59
|
-
return { code: result.code, mappings };
|
|
52
|
+
stripper = name === "sucrase"
|
|
53
|
+
? (await import("./strip-sucrase.js")).stripTypes
|
|
54
|
+
: (await import("./strip-esbuild.js")).stripTypes;
|
|
60
55
|
}
|
|
61
56
|
catch (err) {
|
|
62
|
-
|
|
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}`);
|
|
63
59
|
}
|
|
60
|
+
strippers.set(name, stripper);
|
|
61
|
+
}
|
|
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;
|
|
71
|
+
}
|
|
72
|
+
// esbuild compiles `import x = require("m")` to a `require` call, which a
|
|
73
|
+
// cell doesn't have (#143); `import type x = require("m")` is erased.
|
|
74
|
+
const IMPORT_REQUIRE = /^\s*(?:export\s+)?import\s+(?!type\s)[\w$]+\s*=\s*require\s*\(/m;
|
|
75
|
+
/** The stripper's output, with its source map's `mappings` when asked for. */
|
|
76
|
+
function stripTypesMapped(code, options, sourcemap) {
|
|
77
|
+
if (IMPORT_REQUIRE.test(code)) {
|
|
78
|
+
throw new CellSyntaxError('import x = require("m") is not supported in a cell; use import x from "m"');
|
|
64
79
|
}
|
|
65
|
-
|
|
66
|
-
throw new CellSyntaxError(message.replace(/^Transform failed with \d+ errors?:\n/, ""));
|
|
80
|
+
return typeStripper(options.typeStrip)(code, options, sourcemap);
|
|
67
81
|
}
|
|
68
82
|
function stripTypes(code, options) {
|
|
69
83
|
return stripTypesMapped(code, options, false).code;
|
|
@@ -80,8 +94,9 @@ function rewriteImport(code, node, index) {
|
|
|
80
94
|
? `, { with: { ${node.attributes.map((a) => code.slice(a.start, a.end)).join(", ")} } }`
|
|
81
95
|
: "";
|
|
82
96
|
const load = `await ${SHIM}.import(${source}${attrs})`;
|
|
97
|
+
// `void`: as a cell's last statement, the import isn't its result (#139).
|
|
83
98
|
if (node.specifiers.length === 0)
|
|
84
|
-
return
|
|
99
|
+
return `void ${load};`;
|
|
85
100
|
const ns = `${SHIM}_m${index}`;
|
|
86
101
|
const bindings = node.specifiers.map((spec) => {
|
|
87
102
|
const local = String(spec.local.name);
|
|
@@ -140,7 +155,13 @@ export function rewriteModuleSyntax(js) {
|
|
|
140
155
|
}
|
|
141
156
|
case "ExportDefaultDeclaration": {
|
|
142
157
|
const decl = node;
|
|
143
|
-
|
|
158
|
+
const anonymous = (decl.declaration.type === "FunctionDeclaration" || decl.declaration.type === "ClassDeclaration") &&
|
|
159
|
+
decl.declaration.id === null;
|
|
160
|
+
// An anonymous `function () {}` or `class {}` isn't a statement on
|
|
161
|
+
// its own; parenthesized, it's an expression and the cell's result (#143).
|
|
162
|
+
edits.push({ start: decl.start, end: decl.declaration.start, text: anonymous ? "(" : "" });
|
|
163
|
+
if (anonymous)
|
|
164
|
+
edits.push({ start: decl.declaration.end, end: decl.declaration.end, text: ");" });
|
|
144
165
|
break;
|
|
145
166
|
}
|
|
146
167
|
case "ExportAllDeclaration":
|
|
@@ -202,8 +223,8 @@ function patternNames(pattern, names) {
|
|
|
202
223
|
* surrounding script, which a global-code eval turns into a property of
|
|
203
224
|
* globalThis. Inside the function the declarations become assignments to
|
|
204
225
|
* those globals:
|
|
205
|
-
* const { a } = o; class K {} -> var a, K;
|
|
206
|
-
* (
|
|
226
|
+
* const { a } = o; class K {} -> var a, K; (async () => {
|
|
227
|
+
* ({ a } = o); K = class K {}; })()
|
|
207
228
|
* So later cells, and closures from this one, all see one binding. A `var`
|
|
208
229
|
* in a top-level block or loop head is global too, as in a script; block
|
|
209
230
|
* scoped and function-local declarations stay where they are. Functions
|
|
@@ -277,27 +298,30 @@ function hoistForEval(js) {
|
|
|
277
298
|
edits.push({ start: stmt.end, end: stmt.end, text: ";" });
|
|
278
299
|
}
|
|
279
300
|
else if (stmt.type === "ExpressionStatement" && i === program.body.length - 1) {
|
|
280
|
-
|
|
301
|
+
// Parenthesized, so `1, 2` returns [2] rather than [1, 2] (#140).
|
|
302
|
+
replace(stmt, `return [(${src(stmt.expression)})];`);
|
|
281
303
|
}
|
|
282
304
|
else {
|
|
283
305
|
nestedVars(stmt);
|
|
284
306
|
}
|
|
285
307
|
});
|
|
286
|
-
|
|
308
|
+
// All on the first line, so the cell always starts on line 2, as after
|
|
309
|
+
// transformCell's one-line prelude (#150).
|
|
310
|
+
const declare = names.size > 0 ? `var ${[...names].join(", ")}; ` : "";
|
|
287
311
|
const publish = functions.map((name) => ` globalThis.${name} = ${name};`).join("");
|
|
288
312
|
return `${declare}(async () => { 'use strict';${publish}\n${applyEdits(js, edits)}\n})()`;
|
|
289
313
|
}
|
|
290
314
|
export function transformCell(code, options = {}) {
|
|
291
|
-
return PRELUDE + rewriteModuleSyntax(stripTypes(code, options));
|
|
315
|
+
return PRELUDE + rewriteModuleSyntax(stripTypes(code, options)) + SETTLE;
|
|
292
316
|
}
|
|
293
317
|
/**
|
|
294
318
|
* transformCell, plus the map between the cell's lines and the script's,
|
|
295
319
|
* for the debugger (#18). The rewrite keeps lines (see applyEdits), so the
|
|
296
|
-
* script's lines are
|
|
320
|
+
* script's lines are the stripper's, after the prelude's one.
|
|
297
321
|
*/
|
|
298
322
|
export function transformCellWithLines(code, options = {}) {
|
|
299
323
|
const stripped = stripTypesMapped(code, options, true);
|
|
300
|
-
return { script: PRELUDE + rewriteModuleSyntax(stripped.code), lines: new LineMap(stripped.mappings, newlines(PRELUDE)) };
|
|
324
|
+
return { script: PRELUDE + rewriteModuleSyntax(stripped.code) + SETTLE, lines: new LineMap(stripped.mappings, newlines(PRELUDE), stripped.code) };
|
|
301
325
|
}
|
|
302
326
|
/**
|
|
303
327
|
* 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;
|
package/dist/workerd-target.js
CHANGED
|
@@ -18,18 +18,20 @@ 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
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. */
|
|
@@ -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
|
+
}, { dir });
|
|
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
|
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/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[];
|