@tlockney/cdp-kernel 0.2.0 → 0.4.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.
Files changed (43) hide show
  1. package/README.md +259 -33
  2. package/dist/bun-target.d.ts +60 -0
  3. package/dist/bun-target.js +236 -0
  4. package/dist/celld-target.d.ts +39 -0
  5. package/dist/celld-target.js +274 -0
  6. package/dist/debugger.d.ts +42 -0
  7. package/dist/debugger.js +534 -0
  8. package/dist/history.d.ts +18 -0
  9. package/dist/history.js +45 -0
  10. package/dist/kernel.js +91 -15
  11. package/dist/main.js +8 -6
  12. package/dist/murmur2.d.ts +2 -0
  13. package/dist/murmur2.js +41 -0
  14. package/dist/package.d.ts +16 -2
  15. package/dist/package.js +28 -2
  16. package/dist/sourcemap.d.ts +22 -0
  17. package/dist/sourcemap.js +86 -0
  18. package/dist/target.d.ts +65 -7
  19. package/dist/target.js +57 -6
  20. package/dist/text.d.ts +6 -0
  21. package/dist/text.js +17 -0
  22. package/dist/transform.d.ts +16 -0
  23. package/dist/transform.js +173 -11
  24. package/dist/workerd-target.d.ts +125 -0
  25. package/dist/workerd-target.js +455 -0
  26. package/dist/workers-project.d.ts +27 -0
  27. package/dist/workers-project.js +195 -0
  28. package/dist/ws-client.d.ts +32 -0
  29. package/dist/ws-client.js +166 -0
  30. package/package.json +18 -4
  31. package/runtime/bootstrap.mjs +55 -5
  32. package/runtime/bun-channel.d.mts +3 -0
  33. package/runtime/bun-channel.mjs +58 -0
  34. package/runtime/deno-compat.mjs +3 -1
  35. package/runtime/display.d.mts +2 -0
  36. package/runtime/display.mjs +9 -1
  37. package/runtime/evaluator.d.mts +22 -0
  38. package/runtime/evaluator.mjs +91 -0
  39. package/runtime/introspect.d.mts +13 -0
  40. package/runtime/introspect.mjs +120 -0
  41. package/runtime/node-hooks.d.mts +29 -0
  42. package/runtime/node-hooks.mjs +160 -20
  43. package/runtime/workerd-worker.mjs +194 -0
@@ -0,0 +1,39 @@
1
+ import type { CdpParams } from "./cdp.ts";
2
+ import type { TargetEvents } from "./target.ts";
3
+ import { WorkerdTarget } from "./workerd-target.ts";
4
+ /** The project keys the generated config keeps: bindings and compatibility. */
5
+ export declare const PROJECT_KEYS: string[];
6
+ /**
7
+ * The celld config for the driver: the project's bindings (or none), the
8
+ * pre-built driver as its one module, and the kernel's token and import.meta
9
+ * as vars, which the driver hides from cells' `env`.
10
+ */
11
+ export declare function celldConfig(project: CdpParams | null, { token, meta }: {
12
+ token: string;
13
+ meta: {
14
+ url: string;
15
+ dirname: string;
16
+ filename: string;
17
+ };
18
+ }): CdpParams;
19
+ /**
20
+ * celld's installer puts it in ~/.local/bin, which a Jupyter server's PATH
21
+ * often lacks, so look there before falling back to PATH.
22
+ */
23
+ export declare function findCelld(home?: string): string;
24
+ /** The version in `celld --version`'s output ("celld X.Y.Z"). */
25
+ export declare function celldVersionOf(output: string): string;
26
+ /** The driver, and a project's main, as the single module celld runs. */
27
+ export declare function bundleDriver(runtime: string, projectMain?: string, define?: {
28
+ [name: string]: string;
29
+ }): Promise<string>;
30
+ /** A free loopback port: celld dev needs a number, and rejects 0. */
31
+ export declare function freePort(): Promise<number>;
32
+ /**
33
+ * A celld target. In a celld project its bindings come along; a project
34
+ * that can't load is reported in the first cell, and the driver runs alone.
35
+ */
36
+ export declare function launchCelld(events: TargetEvents, options?: {
37
+ cwd?: string;
38
+ executable?: string;
39
+ }): Promise<WorkerdTarget>;
@@ -0,0 +1,274 @@
1
+ // The celld target (#25). celld (denoland/celld) runs Workers and Durable
2
+ // Objects on its own storage; like workerd it has no I/O outside a request,
3
+ // so it runs the same driver Worker (runtime/workerd-worker.mjs) over the
4
+ // same WebSocket session (src/workerd-target.ts). The differences:
5
+ // - celld has no UnsafeEval binding and doesn't need one: the driver uses
6
+ // a plain global eval.
7
+ // - It listens only on TCP, so the session is a loopback WebSocket opened
8
+ // with a per-launch token (in the query string: the host's built-in
9
+ // WebSocket can't set headers).
10
+ // - It takes one module, so the host bundles the driver (and a project's
11
+ // main) with esbuild and hands celld the result with `no_bundle`.
12
+ // In a celld project (a wrangler.jsonc or wrangler.json at or above the
13
+ // notebook), its bindings come along: the generated config copies them, and
14
+ // lives in the project's .celld/cdp-kernel, so state stays beside, not in,
15
+ // `celld dev`'s own.
16
+ import process from "node:process";
17
+ import { execFileSync, spawn } from "node:child_process";
18
+ import { randomBytes } from "node:crypto";
19
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
20
+ import { createServer } from "node:net";
21
+ import { homedir, tmpdir } from "node:os";
22
+ import { dirname, join, resolve } from "node:path";
23
+ import { build } from "esbuild";
24
+ import { parseJsonc } from "./deno-config.js";
25
+ import { runtimeDir } from "./package.js";
26
+ import { cellMeta, WorkerdTarget } from "./workerd-target.js";
27
+ import { builtinAliases, findWranglerConfig } from "./workers-project.js";
28
+ /** The project keys the generated config keeps: bindings and compatibility. */
29
+ export const PROJECT_KEYS = [
30
+ "compatibility_date",
31
+ "compatibility_flags",
32
+ "durable_objects",
33
+ "migrations",
34
+ "vars",
35
+ "d1_databases",
36
+ "kv_namespaces",
37
+ "r2_buckets",
38
+ "queues",
39
+ "workflows",
40
+ "services",
41
+ "worker_loaders",
42
+ ];
43
+ /**
44
+ * The celld config for the driver: the project's bindings (or none), the
45
+ * pre-built driver as its one module, and the kernel's token and import.meta
46
+ * as vars, which the driver hides from cells' `env`.
47
+ */
48
+ export function celldConfig(project, { token, meta }) {
49
+ const config = { name: "cdpk", main: "driver.mjs", no_bundle: true, compatibility_date: "2026-09-01" };
50
+ for (const key of PROJECT_KEYS)
51
+ if (project && project[key] !== undefined)
52
+ config[key] = project[key];
53
+ const vars = project && typeof project.vars === "object" && project.vars !== null ? project.vars : {};
54
+ config.vars = { ...vars, TOKEN: token, META: JSON.stringify(meta) };
55
+ return config;
56
+ }
57
+ /**
58
+ * celld's installer puts it in ~/.local/bin, which a Jupyter server's PATH
59
+ * often lacks, so look there before falling back to PATH.
60
+ */
61
+ export function findCelld(home = homedir()) {
62
+ const installed = join(home, ".local", "bin", "celld");
63
+ return existsSync(installed) ? installed : "celld";
64
+ }
65
+ /** The version in `celld --version`'s output ("celld X.Y.Z"). */
66
+ export function celldVersionOf(output) {
67
+ return /\bcelld (\d+\.\d+\.\d+\S*)/.exec(output)?.[1] ?? "";
68
+ }
69
+ /** The driver, and a project's main, as the single module celld runs. */
70
+ export async function bundleDriver(runtime, projectMain, define = {}) {
71
+ const driver = JSON.stringify(join(runtime, "workerd-worker.mjs"));
72
+ const contents = projectMain === undefined ? `export { default } from ${driver};\n` : [
73
+ `import * as project from ${JSON.stringify(projectMain)};`,
74
+ `export * from ${JSON.stringify(projectMain)};`,
75
+ `export { default } from ${driver};`,
76
+ "globalThis.worker = project.default;",
77
+ "",
78
+ ].join("\n");
79
+ const result = await build({
80
+ stdin: { contents, resolveDir: runtime, loader: "js" },
81
+ bundle: true,
82
+ write: false,
83
+ format: "esm",
84
+ platform: "browser",
85
+ conditions: ["workerd", "worker", "browser"],
86
+ mainFields: ["browser", "module", "main"],
87
+ alias: builtinAliases(),
88
+ external: ["node:*", "cloudflare:*"],
89
+ define,
90
+ target: "es2024",
91
+ logLevel: "silent",
92
+ });
93
+ return result.outputFiles[0].text;
94
+ }
95
+ /** A free loopback port: celld dev needs a number, and rejects 0. */
96
+ export function freePort() {
97
+ return new Promise((resolve, reject) => {
98
+ const server = createServer();
99
+ server.once("error", reject);
100
+ server.listen(0, "127.0.0.1", () => {
101
+ const address = server.address();
102
+ const port = typeof address === "object" && address !== null ? address.port : 0;
103
+ server.close(() => resolve(port));
104
+ });
105
+ });
106
+ }
107
+ /** The host's built-in WebSocket as the session channel. */
108
+ function channelOf(ws) {
109
+ return {
110
+ send: (text) => ws.send(text),
111
+ onMessage: (listener) => ws.addEventListener("message", (event) => listener(String(event.data))),
112
+ onClose: (listener) => ws.addEventListener("close", () => listener()),
113
+ close() {
114
+ try {
115
+ ws.close();
116
+ }
117
+ catch {
118
+ // Already closed.
119
+ }
120
+ },
121
+ };
122
+ }
123
+ /** celld dev's own children, which run in their own process groups. */
124
+ function childPids(pid) {
125
+ try {
126
+ return execFileSync("pgrep", ["-P", String(pid)], { encoding: "utf8" }).split(/\s+/).filter(Boolean).map(Number);
127
+ }
128
+ catch {
129
+ return []; // none
130
+ }
131
+ }
132
+ /** Kills celld dev's node, then the supervisor itself. */
133
+ function killTree(child) {
134
+ for (const pid of childPids(child.pid ?? -1)) {
135
+ try {
136
+ process.kill(pid, "SIGKILL");
137
+ }
138
+ catch {
139
+ // Gone already.
140
+ }
141
+ }
142
+ child.kill("SIGKILL");
143
+ }
144
+ /** The driver on `celld dev`. */
145
+ class CelldHost {
146
+ runtime;
147
+ project;
148
+ channel;
149
+ #child;
150
+ constructor(child, channel, runtime, project) {
151
+ this.#child = child;
152
+ this.channel = channel;
153
+ this.runtime = runtime;
154
+ this.project = project;
155
+ }
156
+ static async start(cwd, projectConfig, executable) {
157
+ const celld = executable ?? findCelld();
158
+ const version = celldVersionOf(execFileSync(celld, ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }));
159
+ const source = await runtimeDir(new URL("../runtime/", import.meta.url).href);
160
+ let project = null;
161
+ let dir;
162
+ if (projectConfig !== null) {
163
+ if (projectConfig.endsWith(".toml"))
164
+ throw new Error("celld reads wrangler.jsonc or wrangler.json, not wrangler.toml");
165
+ const parsed = parseJsonc(readFileSync(projectConfig, "utf8"));
166
+ if (typeof parsed !== "object" || parsed === null)
167
+ throw new Error(`${projectConfig} is not an object`);
168
+ project = parsed;
169
+ dir = join(dirname(projectConfig), ".celld", "cdp-kernel");
170
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
171
+ }
172
+ else {
173
+ dir = mkdtempSync(join(tmpdir(), "cdpk-celld-"));
174
+ }
175
+ const main = project && typeof project.main === "string" ? resolve(dirname(projectConfig ?? cwd), project.main) : undefined;
176
+ const define = project && typeof project.define === "object" && project.define !== null
177
+ ? project.define
178
+ : {};
179
+ const token = randomBytes(32).toString("hex");
180
+ // 0600: the config carries this launch's token.
181
+ writeFileSync(join(dir, "driver.mjs"), await bundleDriver(source, main, define));
182
+ writeFileSync(join(dir, "wrangler.jsonc"), JSON.stringify(celldConfig(project, { token, meta: cellMeta(cwd) }), null, 2), {
183
+ mode: 0o600,
184
+ });
185
+ const port = await freePort();
186
+ const child = spawn(celld, ["dev", dir, "--host", "127.0.0.1", "--port", String(port), "--no-watch"], {
187
+ stdio: ["ignore", "pipe", "pipe"],
188
+ });
189
+ child.once("exit", () => {
190
+ if (projectConfig === null)
191
+ rmSync(dir, { recursive: true, force: true });
192
+ else
193
+ for (const name of ["driver.mjs", "wrangler.jsonc"])
194
+ rmSync(join(dir, name), { force: true });
195
+ });
196
+ // Cell output is captured in the driver; celld's own lines go to the
197
+ // server log.
198
+ let early = "";
199
+ const log = (chunk) => {
200
+ early += chunk.toString();
201
+ process.stderr.write(`[cdp-kernel celld] ${chunk}`);
202
+ };
203
+ child.stdout?.on("data", log);
204
+ child.stderr?.on("data", log);
205
+ const exited = new Promise((_, reject) => {
206
+ child.once("error", reject);
207
+ child.once("exit", (code) => reject(new Error(`celld exited (${code}) before it was ready:\n${early}`)));
208
+ });
209
+ exited.catch(() => { });
210
+ const url = `127.0.0.1:${port}/?token=${token}`;
211
+ const ready = (async () => {
212
+ // The driver answers 426 to a plain request once celld serves it.
213
+ for (let i = 0;; i++) {
214
+ const status = await fetch(`http://${url}`).then((r) => r.status, () => 0);
215
+ if (status === 426)
216
+ break;
217
+ if (i >= 600)
218
+ throw new Error(`celld never served the driver (last status ${status}):\n${early}`);
219
+ await new Promise((r) => setTimeout(r, 50));
220
+ }
221
+ const ws = new WebSocket(`ws://${url}`);
222
+ await new Promise((resolve, reject) => {
223
+ ws.addEventListener("open", () => resolve(), { once: true });
224
+ ws.addEventListener("error", () => reject(new Error("the session WebSocket didn't open")), { once: true });
225
+ });
226
+ return channelOf(ws);
227
+ })();
228
+ try {
229
+ return new CelldHost(child, await Promise.race([ready, exited]), `celld ${version}`.trim(), projectConfig ?? undefined);
230
+ }
231
+ catch (err) {
232
+ killTree(child);
233
+ throw err;
234
+ }
235
+ }
236
+ /**
237
+ * celld dev's graceful stop waits for the app, which a busy cell never
238
+ * lets go, and its node runs in its own process group. So a forced stop
239
+ * kills the node, then the supervisor; a graceful one falls back to that.
240
+ */
241
+ stop(now) {
242
+ if (now)
243
+ return killTree(this.#child);
244
+ this.#child.kill("SIGTERM");
245
+ setTimeout(() => {
246
+ if (this.#child.exitCode === null && this.#child.signalCode === null)
247
+ killTree(this.#child);
248
+ }, 3000).unref();
249
+ }
250
+ onExit(listener) {
251
+ this.#child.once("exit", listener);
252
+ }
253
+ }
254
+ /**
255
+ * A celld target. In a celld project its bindings come along; a project
256
+ * that can't load is reported in the first cell, and the driver runs alone.
257
+ */
258
+ export async function launchCelld(events, options = {}) {
259
+ const cwd = options.cwd ?? process.cwd();
260
+ const project = findWranglerConfig(cwd);
261
+ let host = null;
262
+ let notice;
263
+ if (project !== null) {
264
+ try {
265
+ host = await CelldHost.start(cwd, project, options.executable);
266
+ }
267
+ catch (err) {
268
+ const reason = err instanceof Error ? err.message : String(err);
269
+ notice = `[cdp-kernel] couldn't load the celld project at ${project}: ${reason}; running without its bindings`;
270
+ }
271
+ }
272
+ host ??= await CelldHost.start(cwd, null, options.executable);
273
+ return await WorkerdTarget.open(host, cwd, events, "celld", notice);
274
+ }
@@ -0,0 +1,42 @@
1
+ import type { CdpParams, RemoteObject } from "./cdp.ts";
2
+ import { type CellTransformOptions } from "./transform.ts";
3
+ /** What the debugger needs from a CDP target: its session, with the Debugger domain. */
4
+ export interface DebugChannel {
5
+ send(method: string, params?: CdpParams): Promise<CdpParams>;
6
+ on(method: string, listener: (params: CdpParams) => void): void;
7
+ /** The execution context cells run in. */
8
+ readonly contextId: number;
9
+ /** Enumerable globals the runtime had before any cell ran. */
10
+ readonly baselineGlobals: ReadonlySet<string>;
11
+ }
12
+ export type DapMessage = {
13
+ [key: string]: unknown;
14
+ };
15
+ /** ipykernel's seed, so a kernel switch doesn't move JupyterLab's breakpoints. */
16
+ export declare const HASH_SEED = 3339675911;
17
+ export declare const THREAD_ID = 1;
18
+ /** A DAP value string for a CDP RemoteObject. */
19
+ export declare function describeRemote(object: RemoteObject): string;
20
+ export declare class Debugger {
21
+ #private;
22
+ readonly prefix: string;
23
+ readonly suffix = ".ts";
24
+ constructor(emit: (event: DapMessage) => void, transformOptions?: () => CellTransformOptions);
25
+ get started(): boolean;
26
+ get stopped(): boolean;
27
+ /** A cell's path, as JupyterLab computes it for the same code. */
28
+ pathFor(code: string): string;
29
+ /**
30
+ * The target to debug: called with each target the kernel launches. A
31
+ * restarted target gets the session's breakpoints again.
32
+ */
33
+ attachTarget(channel: DebugChannel | null): Promise<void>;
34
+ /**
35
+ * The script for a cell while debugging: transformed with its line map
36
+ * kept, and named for its path. `source` is what actually runs (the cell,
37
+ * or the cell wrapped as an object literal); the path comes from `code`,
38
+ * the cell as the frontend sent it.
39
+ */
40
+ cellScript(code: string, source: string): string;
41
+ handle(request: DapMessage): Promise<DapMessage>;
42
+ }