@tlockney/cdp-kernel 0.2.0 → 0.3.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/dist/transform.js CHANGED
@@ -16,7 +16,7 @@
16
16
  // callback was not specified").
17
17
  import { transformSync } from "esbuild";
18
18
  import { parse } from "acorn";
19
- import { full } from "acorn-walk";
19
+ import { full, recursive } from "acorn-walk";
20
20
  export const SHIM = "__cdpk";
21
21
  // Each cell first yields one macrotask. User code then runs as an ordinary
22
22
  // task instead of nested inside the inspector's dispatch of Runtime.evaluate,
@@ -80,11 +80,9 @@ function rewriteImport(code, node, index) {
80
80
  });
81
81
  return `var ${ns} = ${load}; var ${bindings.join(", ")};`;
82
82
  }
83
- /** Rewrites module-only syntax in plain JavaScript into script syntax. */
84
- export function rewriteModuleSyntax(js) {
85
- let program;
83
+ function parseCell(js) {
86
84
  try {
87
- program = parse(js, {
85
+ return parse(js, {
88
86
  ecmaVersion: "latest",
89
87
  sourceType: "module",
90
88
  allowAwaitOutsideFunction: true,
@@ -94,6 +92,10 @@ export function rewriteModuleSyntax(js) {
94
92
  catch (err) {
95
93
  throw new CellSyntaxError(err instanceof Error ? err.message : String(err));
96
94
  }
95
+ }
96
+ /** Rewrites module-only syntax in plain JavaScript into script syntax. */
97
+ export function rewriteModuleSyntax(js) {
98
+ const program = parseCell(js);
97
99
  const edits = [];
98
100
  let importIndex = 0;
99
101
  full(program, (node) => {
@@ -136,15 +138,142 @@ export function rewriteModuleSyntax(js) {
136
138
  });
137
139
  // Nested edits can't occur (imports/exports are top level, import()'s
138
140
  // keyword edit doesn't overlap its arguments), so apply back to front.
139
- edits.sort((a, b) => b.start - a.start);
141
+ return applyEdits(js, edits);
142
+ }
143
+ function applyEdits(js, edits) {
144
+ // Back to front; at a shared start, a replacement goes before an insertion.
145
+ edits.sort((a, b) => b.start - a.start || b.end - a.end);
140
146
  let out = js;
141
147
  for (const edit of edits)
142
148
  out = out.slice(0, edit.start) + edit.text + out.slice(edit.end);
143
149
  return out;
144
150
  }
151
+ /** Adds the names a binding pattern declares. */
152
+ function patternNames(pattern, names) {
153
+ switch (pattern.type) {
154
+ case "Identifier":
155
+ names.add(pattern.name);
156
+ break;
157
+ case "ObjectPattern":
158
+ for (const prop of pattern.properties)
159
+ patternNames(prop.type === "Property" ? prop.value : prop, names);
160
+ break;
161
+ case "ArrayPattern":
162
+ for (const element of pattern.elements)
163
+ if (element)
164
+ patternNames(element, names);
165
+ break;
166
+ case "RestElement":
167
+ patternNames(pattern.argument, names);
168
+ break;
169
+ case "AssignmentPattern":
170
+ patternNames(pattern.left, names);
171
+ break;
172
+ }
173
+ }
174
+ /**
175
+ * Makes a cell's declarations global without replMode, the tslab/IJavascript
176
+ * technique: the cell runs inside an async function (for top-level await),
177
+ * and every name it declares at the top level is declared with `var` in the
178
+ * surrounding script, which a global-code eval turns into a property of
179
+ * globalThis. Inside the function the declarations become assignments to
180
+ * those globals:
181
+ * const { a } = o; class K {} -> var a, K;
182
+ * (async () => { ({ a } = o); K = class K {}; })()
183
+ * So later cells, and closures from this one, all see one binding. A `var`
184
+ * in a top-level block or loop head is global too, as in a script; block
185
+ * scoped and function-local declarations stay where they are. Functions
186
+ * keep their in-cell hoisting and are copied to globalThis on entry.
187
+ *
188
+ * A trailing expression statement becomes `return [value]`; the box keeps a
189
+ * promise from being awaited, so the cell's result is the promise itself, as
190
+ * in Deno.
191
+ */
192
+ function hoistForEval(js) {
193
+ const program = parseCell(js);
194
+ const names = new Set();
195
+ const functions = [];
196
+ const edits = [];
197
+ const src = (node) => js.slice(node.start, node.end);
198
+ const replace = (node, text) => edits.push({ start: node.start, end: node.end, text });
199
+ // A declarator without an initializer resets the name, except under var,
200
+ // where redeclaring keeps the value.
201
+ const assignments = (decl) => decl.declarations.flatMap((d) => {
202
+ patternNames(d.id, names);
203
+ if (d.init)
204
+ return [`${src(d.id)} = ${src(d.init)}`];
205
+ return decl.kind === "var" ? [] : [`${src(d.id)} = undefined`];
206
+ });
207
+ const statement = (decl) => {
208
+ const parts = assignments(decl);
209
+ replace(decl, parts.length > 0 ? `(${parts.join(", ")});` : ";");
210
+ };
211
+ const isVar = (node) => node?.type === "VariableDeclaration" && node.kind === "var";
212
+ const loopHead = (left) => {
213
+ if (!isVar(left))
214
+ return;
215
+ const id = left.declarations[0].id;
216
+ patternNames(id, names);
217
+ replace(left, src(id));
218
+ };
219
+ // Expressions can't hold statements outside a function or class, so only
220
+ // statement bodies are walked.
221
+ const nestedVars = (node) => recursive(node, null, {
222
+ Function() { },
223
+ Class() { },
224
+ VariableDeclaration(decl) {
225
+ if (decl.kind === "var")
226
+ statement(decl);
227
+ },
228
+ ForStatement(loop, state, walk) {
229
+ if (isVar(loop.init))
230
+ replace(loop.init, assignments(loop.init).join(", "));
231
+ walk(loop.body, state);
232
+ },
233
+ ForInStatement(loop, state, walk) {
234
+ loopHead(loop.left);
235
+ walk(loop.body, state);
236
+ },
237
+ ForOfStatement(loop, state, walk) {
238
+ loopHead(loop.left);
239
+ walk(loop.body, state);
240
+ },
241
+ });
242
+ program.body.forEach((stmt, i) => {
243
+ if (stmt.type === "VariableDeclaration" && (stmt.kind === "var" || stmt.kind === "let" || stmt.kind === "const")) {
244
+ statement(stmt);
245
+ }
246
+ else if (stmt.type === "FunctionDeclaration") {
247
+ names.add(stmt.id.name);
248
+ functions.push(stmt.id.name);
249
+ }
250
+ else if (stmt.type === "ClassDeclaration") {
251
+ names.add(stmt.id.name);
252
+ edits.push({ start: stmt.start, end: stmt.start, text: `${stmt.id.name} = ` });
253
+ edits.push({ start: stmt.end, end: stmt.end, text: ";" });
254
+ }
255
+ else if (stmt.type === "ExpressionStatement" && i === program.body.length - 1) {
256
+ replace(stmt, `return [${src(stmt.expression)}];`);
257
+ }
258
+ else {
259
+ nestedVars(stmt);
260
+ }
261
+ });
262
+ const declare = names.size > 0 ? `var ${[...names].join(", ")};\n` : "";
263
+ const publish = functions.map((name) => ` globalThis.${name} = ${name};`).join("");
264
+ return `${declare}(async () => { 'use strict';${publish}\n${applyEdits(js, edits)}\n})()`;
265
+ }
145
266
  export function transformCell(code, options = {}) {
146
267
  return PRELUDE + rewriteModuleSyntax(stripTypes(code, options));
147
268
  }
269
+ /**
270
+ * Like transformCell, for a target that evaluates cells itself, as global
271
+ * code (an indirect eval), instead of through the inspector's replMode: see
272
+ * hoistForEval. The script's value is a promise of the boxed result.
273
+ */
274
+ export function transformCellForEval(code, options = {}) {
275
+ return hoistForEval(rewriteModuleSyntax(stripTypes(code, options)));
276
+ }
148
277
  /**
149
278
  * Deno treats a cell like `{ "a": 1 }` as an object literal rather than a
150
279
  * block, as browser consoles do. Returns the wrapped form to try first, or
@@ -0,0 +1,123 @@
1
+ import type { CdpParams, EvaluateResult, RemoteObject } from "./cdp.ts";
2
+ import { transformCellForEval } from "./transform.ts";
3
+ import { type KernelTarget, type TargetEvents } from "./target.ts";
4
+ import { type WsChannel } from "./ws-client.ts";
5
+ /** The driver Worker's modules; the first is its main module. */
6
+ export declare const WORKER_MODULES: string[];
7
+ /** See BunTarget: how long an interrupted cell has to show it isn't busy. */
8
+ export declare const INTERRUPT_GRACE_MS = 500;
9
+ export declare function restartNote(runtime: string): string;
10
+ export declare const RESTART_NOTE: string;
11
+ export declare function workerdConfig({ socket, compatibilityDate }: {
12
+ socket: string;
13
+ compatibilityDate: string;
14
+ }): string;
15
+ /**
16
+ * A Workers project has workerd in its node_modules (wrangler and miniflare
17
+ * depend on it), so look there first, from the notebook's directory up;
18
+ * otherwise use the one on PATH.
19
+ */
20
+ export declare function findWorkerd(cwd: string): string;
21
+ /** "workerd 2026-10-15" -> "2026-10-15": the newest date it supports. */
22
+ export declare function compatibilityDateOf(versionOutput: string): string;
23
+ /** The literal specifiers a transformed cell imports, in order. */
24
+ export declare function importSpecifiers(script: string): string[];
25
+ /**
26
+ * Bundles `specifier`, as imported from `cwd`, into global code whose value
27
+ * is a promise of the module's namespace. npm: and jsr: install into the
28
+ * npm cache, as on a Node target. Builtins stay external and are handed to
29
+ * the bundle as `require` (workerd-worker.mjs's requireFor). A specifier
30
+ * that can't be bundled becomes code that throws when imported, so the
31
+ * error appears in the cell.
32
+ */
33
+ export declare function bundleModule(specifier: string, cwd: string): Promise<string>;
34
+ /** What import.meta says in a cell: the notebook's directory, not workerd's. */
35
+ export declare function cellMeta(cwd: string): {
36
+ url: string;
37
+ dirname: string;
38
+ filename: string;
39
+ };
40
+ /**
41
+ * How the target reaches its driver Worker: a workerd of its own
42
+ * (RawWorkerd, below), or a Workers project's Miniflare
43
+ * (src/workers-project.ts), which adds the project's bindings.
44
+ */
45
+ export interface WorkerHost {
46
+ /** The runtime's name for the banner: workerd's date, or Miniflare's version. */
47
+ readonly runtime: string;
48
+ /** The wrangler config whose bindings the driver has, if any. */
49
+ readonly project?: string;
50
+ /** The open session with the driver. */
51
+ readonly channel: WsChannel;
52
+ /** Stops the runtime; `now` even while a cell is busy. */
53
+ stop(now: boolean): void;
54
+ onExit(listener: (code: number | null) => void): void;
55
+ }
56
+ /** The driver on a workerd of its own, behind a Unix socket. */
57
+ export declare class RawWorkerd implements WorkerHost {
58
+ #private;
59
+ readonly runtime: string;
60
+ readonly channel: WsChannel;
61
+ private constructor();
62
+ static start(cwd: string, executable?: string): Promise<RawWorkerd>;
63
+ stop(now: boolean): void;
64
+ onExit(listener: (code: number | null) => void): void;
65
+ }
66
+ /** A runtime that runs the driver Worker: workerd, or celld (src/celld-target.ts). */
67
+ export type WorkersRuntime = "workerd" | "celld";
68
+ export declare class WorkerdTarget implements KernelTarget {
69
+ #private;
70
+ readonly runtime: WorkersRuntime;
71
+ readonly versions: {
72
+ runtime: string;
73
+ engine: string;
74
+ };
75
+ notice: string | undefined;
76
+ readonly transform: typeof transformCellForEval;
77
+ private constructor();
78
+ /**
79
+ * In a Workers project (a wrangler config at or above `cwd`), the
80
+ * project's own Miniflare runs the driver with the project's bindings;
81
+ * otherwise a workerd of its own does (#26).
82
+ */
83
+ static launch(events: TargetEvents, options?: {
84
+ cwd?: string;
85
+ executable?: string;
86
+ }): Promise<WorkerdTarget>;
87
+ /**
88
+ * A target over a started host's session. `notice` (a project that
89
+ * failed to load) is replaced by the bindings when the host has a project.
90
+ */
91
+ static open(host: WorkerHost, cwd: string, events: TargetEvents, runtime: WorkersRuntime, notice?: string): Promise<WorkerdTarget>;
92
+ evaluate(script: string): Promise<EvaluateResult>;
93
+ /**
94
+ * A cell that's waiting on something leaves the driver free: it answers a
95
+ * ping, and the cell is abandoned with the session intact (its pending
96
+ * work still runs, as on Bun). A busy cell can't be reached, so workerd is
97
+ * stopped and restarts on the next cell.
98
+ */
99
+ interrupt(): Promise<void>;
100
+ /** Output arrives on the session before the reply it precedes. */
101
+ flush(): Promise<void>;
102
+ broadcastResult(executionCount: number, result: RemoteObject): Promise<void>;
103
+ dispatchComm(msgType: string, content: CdpParams, metadata: CdpParams, buffers: Uint8Array[]): Promise<{
104
+ handled: boolean;
105
+ } | {
106
+ error: {
107
+ name: string;
108
+ message: string;
109
+ stack: string;
110
+ };
111
+ }>;
112
+ describeError(exception: RemoteObject): Promise<{
113
+ name: string;
114
+ message: string;
115
+ stack: string;
116
+ }>;
117
+ propertyNames(expression: string): Promise<string[]>;
118
+ /** None: top-level declarations here are properties of globalThis. */
119
+ lexicalNames(): Promise<string[]>;
120
+ /** Never asked for: prompt() in workerd answers null without asking. */
121
+ replyInput(): void;
122
+ kill(): void;
123
+ }