@tlockney/cdp-kernel 0.8.0 → 0.10.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.
@@ -160,6 +160,9 @@ export class KernelCore {
160
160
  // Transforms are synchronous, so the stripper loads before any cell.
161
161
  if (typeof this.#typeStrip === "string")
162
162
  await loadTypeStripper(this.#typeStrip);
163
+ // No shell or control reply until every port is bound: a client that
164
+ // has kernel_info_reply goes on to the heartbeat port (#438).
165
+ await this.#channels.ready;
163
166
  await Promise.all([this.#readShell(), this.#runShell(this.#mainShell),
164
167
  this.#loop("control", (m, p) => this.#handleControl(m, p)),
165
168
  this.#loop("stdin", (m) => this.#handleStdin(m))]);
@@ -322,6 +325,14 @@ export class KernelCore {
322
325
  // Its prompt, if any, died with it; a late reply is dropped.
323
326
  this.#input = null;
324
327
  void this.#debugger?.attachTarget(null);
328
+ // Its comms died with it: tell the frontends, so a widget or a
329
+ // Ganymede host comm isn't left waiting on a runtime that is gone,
330
+ // and the next runtime can open its own (#461).
331
+ if (!this.#shuttingDown) {
332
+ for (const commId of this.#comms.keys())
333
+ this.#publish("comm_close", { comm_id: commId, data: {} });
334
+ }
335
+ this.#comms.clear();
325
336
  if (!this.#shuttingDown) {
326
337
  const text = note ?? `[cdp-kernel] ${this.#runtime} exited (${code}); restarting on next cell`;
327
338
  this.#publish("stream", { name: "stderr", text: `\n${text}\n` });
package/dist/package.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  /** Kept equal to package.json and deno.jsonc by test/package.test.ts. */
2
- export declare const VERSION = "0.8.0";
2
+ export declare const VERSION = "0.10.0";
3
3
  /**
4
4
  * The UTC day this version was released, set with VERSION. The publish
5
5
  * workflow refuses a tag unless it's today or yesterday, so it can't go
6
6
  * stale: see dependencyCutoff.
7
7
  */
8
- export declare const RELEASE_DATE = "2026-11-06";
8
+ export declare const RELEASE_DATE = "2026-11-11";
9
9
  /**
10
10
  * Deno blocks packages less than 24 hours old (minimum dependency age), so
11
11
  * a kernelspec relaunching a release from JSR would fail on release day. It
@@ -14,7 +14,7 @@ export declare const RELEASE_DATE = "2026-11-06";
14
14
  * late in the day its tag was pushed), and nothing newer ever is.
15
15
  */
16
16
  export declare function dependencyCutoff(releaseDate?: string): string;
17
- export declare const RUNTIME_FILES: readonly ["bootstrap.mjs", "bun-channel.mjs", "evaluator.mjs", "introspect.mjs", "workerd-worker.mjs", "display.mjs", "comm.mjs", "widget.mjs", "node-hooks.mjs", "deno-compat.mjs", "node-modules-resolve.mjs"];
17
+ export declare const RUNTIME_FILES: readonly ["bootstrap.mjs", "bun-channel.mjs", "evaluator.mjs", "introspect.mjs", "workerd-worker.mjs", "display.mjs", "comm.mjs", "host.mjs", "widget.mjs", "node-hooks.mjs", "deno-compat.mjs", "node-modules-resolve.mjs"];
18
18
  export type HostRuntime = "node" | "deno";
19
19
  export declare function hostRuntime(): HostRuntime;
20
20
  /** Where the CLI module was loaded from. */
package/dist/package.js CHANGED
@@ -14,13 +14,13 @@ import { join, sep } from "node:path";
14
14
  import process from "node:process";
15
15
  import { fileURLToPath } from "node:url";
16
16
  /** Kept equal to package.json and deno.jsonc by test/package.test.ts. */
17
- export const VERSION = "0.8.0";
17
+ export const VERSION = "0.10.0";
18
18
  /**
19
19
  * The UTC day this version was released, set with VERSION. The publish
20
20
  * workflow refuses a tag unless it's today or yesterday, so it can't go
21
21
  * stale: see dependencyCutoff.
22
22
  */
23
- export const RELEASE_DATE = "2026-11-06";
23
+ export const RELEASE_DATE = "2026-11-11";
24
24
  /**
25
25
  * Deno blocks packages less than 24 hours old (minimum dependency age), so
26
26
  * a kernelspec relaunching a release from JSR would fail on release day. It
@@ -43,6 +43,7 @@ export const RUNTIME_FILES = [
43
43
  "workerd-worker.mjs",
44
44
  "display.mjs",
45
45
  "comm.mjs",
46
+ "host.mjs",
46
47
  "widget.mjs",
47
48
  "node-hooks.mjs",
48
49
  "deno-compat.mjs",
@@ -51,7 +51,16 @@ export declare function isTypeStripperName(value: unknown): value is TypeStrippe
51
51
  * imported (#238).
52
52
  */
53
53
  export declare function loadTypeStripper(name: TypeStripperName): Promise<TypeStripper>;
54
- /** Rewrites module-only syntax in plain JavaScript into script syntax. */
54
+ /**
55
+ * Rewrites module-only syntax in plain JavaScript into script syntax.
56
+ *
57
+ * Top-level `const` becomes `let`, as do import bindings (#444). Under
58
+ * replMode a later evaluation may redeclare a top-level name only with the
59
+ * same kind of declaration: `const a` then `let a`, or `const a` then
60
+ * `import { a }`, fails with "already been declared" until a restart. With
61
+ * one kind, cells can be re-run and edited freely; the cost is that
62
+ * assigning to a top-level `const` from another cell doesn't throw.
63
+ */
55
64
  export declare function rewriteModuleSyntax(js: string): string;
56
65
  export declare function transformCell(code: string, options?: CellTransformOptions): string;
57
66
  /**
package/dist/transform.js CHANGED
@@ -113,7 +113,9 @@ function rewriteImport(code, node, index) {
113
113
  return `${local} = ${ns}`;
114
114
  return `${local} = ${ns}[${JSON.stringify(importedName(spec))}]`;
115
115
  });
116
- return `var ${ns} = ${load}; var ${bindings.join(", ")};`;
116
+ // `let`, like top-level `const` (see toLet): replMode lets a later
117
+ // cell redeclare a name only with the same kind of declaration.
118
+ return `var ${ns} = ${load}; let ${bindings.join(", ")};`;
117
119
  }
118
120
  function parseCell(js) {
119
121
  try {
@@ -128,10 +130,26 @@ function parseCell(js) {
128
130
  throw new CellSyntaxError(err instanceof Error ? err.message : String(err));
129
131
  }
130
132
  }
131
- /** Rewrites module-only syntax in plain JavaScript into script syntax. */
133
+ /**
134
+ * Rewrites module-only syntax in plain JavaScript into script syntax.
135
+ *
136
+ * Top-level `const` becomes `let`, as do import bindings (#444). Under
137
+ * replMode a later evaluation may redeclare a top-level name only with the
138
+ * same kind of declaration: `const a` then `let a`, or `const a` then
139
+ * `import { a }`, fails with "already been declared" until a restart. With
140
+ * one kind, cells can be re-run and edited freely; the cost is that
141
+ * assigning to a top-level `const` from another cell doesn't throw.
142
+ */
132
143
  export function rewriteModuleSyntax(js) {
133
144
  const program = parseCell(js);
134
145
  const edits = [];
146
+ for (const statement of program.body) {
147
+ const decl = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
148
+ if (decl?.type === "VariableDeclaration" && decl.kind === "const") {
149
+ // Padded to `const`'s length, so columns stay where the debugger expects them.
150
+ edits.push({ start: decl.start, end: decl.start + "const".length, text: "let " });
151
+ }
152
+ }
135
153
  let importIndex = 0;
136
154
  full(program, (node) => {
137
155
  switch (node.type) {
@@ -35,7 +35,7 @@ import { connectUnixWebSocket } from "./ws-client.js";
35
35
  import { findWranglerConfig, startMiniflare } from "./workers-project.js";
36
36
  import { ensureInstalledAsync, findLockFile, jsrToNpm, parsePackageSpecifier } from "../runtime/node-hooks.mjs";
37
37
  /** The driver Worker's modules; the first is its main module. */
38
- export const WORKER_MODULES = ["workerd-worker.mjs", "display.mjs", "comm.mjs", "widget.mjs", "evaluator.mjs", "introspect.mjs"];
38
+ export const WORKER_MODULES = ["workerd-worker.mjs", "display.mjs", "comm.mjs", "host.mjs", "widget.mjs", "evaluator.mjs", "introspect.mjs"];
39
39
  /** See BunTarget: how long an interrupted cell has to show it isn't busy. */
40
40
  export const INTERRUPT_GRACE_MS = 500;
41
41
  export function restartNote(runtime) {
@@ -71,6 +71,7 @@ const driver :Workerd.Worker = (
71
71
  (name = "UNSAFE", unsafeEval = void),
72
72
  (name = "TOKEN", fromEnvironment = "CDPK_TOKEN"),
73
73
  (name = "META", fromEnvironment = "CDPK_META"),
74
+ (name = "CDPK_PARAMS", fromEnvironment = "CDPK_PARAMS"),
74
75
  ],
75
76
  );
76
77
  `;
@@ -332,7 +333,10 @@ export class RawWorkerd {
332
333
  copyFileSync(join(source, name), join(dir, name));
333
334
  writeFileSync(join(dir, "config.capnp"), workerdConfig({ socket, compatibilityDate }));
334
335
  const token = randomBytes(32).toString("hex");
335
- const env = { ...process.env, CDPK_TOKEN: token, CDPK_META: JSON.stringify(cellMeta(cwd)) };
336
+ const env = { ...process.env, CDPK_TOKEN: token, CDPK_META: JSON.stringify(cellMeta(cwd)),
337
+ // jupyter.params (#456); always set, so the binding exists.
338
+ CDPK_PARAMS: process.env.GANYMEDE_PARAMS ?? "",
339
+ };
336
340
  // --experimental: the UnsafeEval binding.
337
341
  const child = spawn(workerd, ["serve", "config.capnp", "--experimental"], { cwd: dir, stdio: ["ignore", "pipe", "pipe"], env });
338
342
  child.once("exit", () => rmSync(dir, { recursive: true, force: true }));
@@ -185,7 +185,7 @@ export async function startMiniflare(configPath, cwd, startTimeoutMs) {
185
185
  unsafeEvalBinding: "UNSAFE",
186
186
  compatibilityDate: date,
187
187
  compatibilityFlags: compatibilityFlagsFor(workerOptions.compatibilityFlags ?? []),
188
- bindings: { ...workerOptions.bindings, TOKEN: token, META: JSON.stringify(cellMeta(cwd)) },
188
+ bindings: { ...workerOptions.bindings, TOKEN: token, META: JSON.stringify(cellMeta(cwd)), CDPK_PARAMS: process.env.GANYMEDE_PARAMS ?? "" },
189
189
  // Miniflare 5 renamed Miniflare 4's defaultPersistRoot.
190
190
  ...(v5 ? { resourcePersistencePath: state } : { defaultPersistRoot: state }),
191
191
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tlockney/cdp-kernel",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Deno's Jupyter kernel, extracted to run notebooks on Node, Deno, Bun, workerd and celld",
5
5
  "license": "MIT",
6
6
  "author": "Thomas Lockney <thomas@lockney.net>",
@@ -77,11 +77,11 @@
77
77
  "dependencies": {
78
78
  "acorn": "^8.15.0",
79
79
  "acorn-walk": "^8.3.4",
80
- "esbuild": "^0.25.10",
80
+ "esbuild": "^0.28.2",
81
81
  "typescript": "^6.0.3"
82
82
  },
83
83
  "devDependencies": {
84
- "@types/node": "^24.19.2",
84
+ "@types/node": "^26.6.6",
85
85
  "medium-zoom": "^1.1.0",
86
86
  "vitepress": "^1.6.4",
87
87
  "workerd": "1.20261015.1",
@@ -83,6 +83,8 @@ function send(payload) {
83
83
 
84
84
  const jupyter = createJupyter({
85
85
  inspect: (value) => inspect(value),
86
+ // jupyter.params (#456): the kernel's environment reaches the target.
87
+ params: () => (isDeno ? Deno.env.get("GANYMEDE_PARAMS") : process.env.GANYMEDE_PARAMS),
86
88
  base64,
87
89
  readFile,
88
90
  send(msgType, content, metadata, buffers) {
@@ -65,7 +65,10 @@ const jupyter = createJupyter({
65
65
  // A synchronous prompt would need SharedArrayBuffer and Atomics.wait, so
66
66
  // a cross-origin isolated page; until then, as with no stdin.
67
67
  input: () => null,
68
+ // jupyter.params (#456): the target's `params` option, sent with info.
69
+ params: () => paramsText,
68
70
  });
71
+ let paramsText;
69
72
 
70
73
  // --- Modules ----------------------------------------------------------------
71
74
  let cdn = DEFAULT_CDN;
@@ -150,7 +153,8 @@ async function preload(urls, base) {
150
153
  // --- The shim cells and the evaluator use -----------------------------------
151
154
  let handle = null;
152
155
 
153
- function install({ base, cdn: cdnBase }) {
156
+ function install({ base, cdn: cdnBase, params }) {
157
+ if (typeof params === "string") paramsText = params;
154
158
  if (typeof cdnBase === "string") cdn = cdnBase.endsWith("/") ? cdnBase : cdnBase + "/";
155
159
  const url = new URL("$cdpk$repl.mjs", base).href;
156
160
  globalThis.__cdpk = {
@@ -1,30 +1,116 @@
1
1
  /** A comm message as a handler receives it. */
2
2
  export interface CommMessage {
3
+ /** The message's JSON data. */
3
4
  data: { [key: string]: unknown };
5
+ /** The message's metadata. */
4
6
  metadata: { [key: string]: unknown };
7
+ /** Binary buffers sent with the message. */
5
8
  buffers: Uint8Array[];
6
9
  }
7
10
 
11
+ /** Options for sending, opening or closing a comm. */
8
12
  export interface CommSendOptions {
13
+ /** Metadata for the message. Default `{}`. */
9
14
  metadata?: { [key: string]: unknown };
15
+ /** Binary buffers sent with the message. Default `[]`. */
10
16
  buffers?: Uint8Array[];
11
17
  }
12
18
 
13
- /** One end of a Jupyter comm channel. */
19
+ /** One end of a Jupyter comm channel, from `jupyter.comm.open` or a target handler. */
14
20
  export interface Comm {
21
+ /** The comm id, shared with the frontend's end. */
15
22
  readonly id: string;
23
+ /** The target the comm was opened on. */
16
24
  readonly targetName: string;
25
+ /** Whether either end has closed the comm. */
17
26
  readonly closed: boolean;
27
+ /**
28
+ * Send a message to the frontend's end. Does nothing once closed.
29
+ *
30
+ * @param data The message's JSON data. Default `{}`.
31
+ * @param options Metadata and binary buffers.
32
+ * @example
33
+ * ```ts
34
+ * const comm = jupyter.comm.open("my.target");
35
+ * comm.send({ ping: true }, { buffers: [new Uint8Array([1, 2, 3])] });
36
+ * ```
37
+ */
18
38
  send(data?: { [key: string]: unknown }, options?: CommSendOptions): void;
39
+ /**
40
+ * Close the comm and tell the frontend. Its `onClose` handlers start
41
+ * right away, without being awaited. Does nothing once closed.
42
+ *
43
+ * @param data Data sent with the close message. Default `{}`.
44
+ * @param options Metadata and binary buffers.
45
+ * @example
46
+ * ```ts
47
+ * const comm = jupyter.comm.open("my.target");
48
+ * comm.close({ reason: "done" });
49
+ * ```
50
+ */
19
51
  close(data?: { [key: string]: unknown }, options?: CommSendOptions): void;
52
+ /**
53
+ * Handle messages from the frontend's end. A handler runs like a cell:
54
+ * its output goes to the notebook, and an exception shows on stderr.
55
+ *
56
+ * @param handler Called with each message; may be async.
57
+ * @example
58
+ * ```ts
59
+ * const comm = jupyter.comm.open("my.target");
60
+ * comm.onMsg(({ data, buffers }) => console.log(data, buffers.length));
61
+ * ```
62
+ */
20
63
  onMsg(handler: (msg: CommMessage) => void | Promise<void>): void;
64
+ /**
65
+ * Handle the comm closing, from either end.
66
+ *
67
+ * @param handler Called with the close message; may be async.
68
+ * @example
69
+ * ```ts
70
+ * const comm = jupyter.comm.open("my.target");
71
+ * comm.onClose(() => console.log("closed"));
72
+ * ```
73
+ */
21
74
  onClose(handler: (msg: CommMessage) => void | Promise<void>): void;
22
75
  }
23
76
 
24
77
  /** `jupyter.comm`: open comms, and accept comms the frontend opens. */
25
78
  export interface CommApi {
79
+ /**
80
+ * Open a comm to a target the frontend registered.
81
+ *
82
+ * @param targetName The frontend's target name.
83
+ * @param data Data sent with the open message. Default `{}`.
84
+ * @param options Metadata and binary buffers.
85
+ * @returns The kernel's end of the comm.
86
+ * @example
87
+ * ```ts
88
+ * const comm = jupyter.comm.open("my.target", { hello: 1 });
89
+ * comm.onMsg((m) => console.log(m.data));
90
+ * ```
91
+ */
26
92
  open(targetName: string, data?: { [key: string]: unknown }, options?: CommSendOptions): Comm;
93
+ /**
94
+ * Accept comms the frontend opens on `targetName`. Registering the same
95
+ * name again replaces the handler.
96
+ *
97
+ * @param targetName The target name the frontend opens comms on.
98
+ * @param handler Called with the new comm and its open message.
99
+ * @example
100
+ * ```ts
101
+ * jupyter.comm.registerTarget("echo", (comm) => comm.onMsg((m) => comm.send(m.data)));
102
+ * ```
103
+ */
27
104
  registerTarget(targetName: string, handler: (comm: Comm, msg: CommMessage) => void | Promise<void>): void;
105
+ /**
106
+ * Stop accepting new comms on `targetName`. Comms already open stay open.
107
+ *
108
+ * @param targetName The target to remove.
109
+ * @example
110
+ * ```ts
111
+ * jupyter.comm.unregisterTarget("echo");
112
+ * ```
113
+ */
28
114
  unregisterTarget(targetName: string): void;
29
115
  }
30
116