@tlockney/cdp-kernel 0.4.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.
@@ -9,9 +9,11 @@ import { Buffer } from "node:buffer";
9
9
  import process from "node:process";
10
10
  import { spawn } from "node:child_process";
11
11
  import { join } from "node:path";
12
+ import { asEvaluateResult } from "./cdp.js";
12
13
  import { transformCellForEval } from "./transform.js";
13
14
  import { runtimeDir } from "./package.js";
14
- import { Gates, Interrupted, isTargetMessage, PipeDemux } from "./target.js";
15
+ import { killOnExit } from "./children.js";
16
+ import { Gates, Interrupted, isTargetMessage, parseJson, PipeDemux } from "./target.js";
15
17
  /**
16
18
  * How long an interrupted target has to answer a ping. A cell waiting on a
17
19
  * promise leaves the event loop free, so the answer is near-instant; a busy
@@ -45,6 +47,9 @@ export class BunTarget {
45
47
  #gates = new Gates();
46
48
  #pending = new Map();
47
49
  #nextId = 0;
50
+ /** Ids of requests interrupt() may cut short: eval and comm. */
51
+ #interruptibleIds = new Set();
52
+ #exitedOnce = false;
48
53
  #messages = Promise.resolve();
49
54
  #interrupted = false;
50
55
  #stopNote;
@@ -60,6 +65,7 @@ export class BunTarget {
60
65
  const env = { ...process.env, FORCE_COLOR: "1" };
61
66
  delete env.NO_COLOR;
62
67
  const child = spawn(cmd, args, { cwd: options.cwd, stdio: ["pipe", "pipe", "pipe", "pipe"], env });
68
+ killOnExit(child);
63
69
  const channel = child.stdio[3];
64
70
  if (!child.stdout || !child.stderr || !isDuplex(channel)) {
65
71
  child.kill();
@@ -97,7 +103,8 @@ export class BunTarget {
97
103
  this.#channel.on("data", (chunk) => {
98
104
  buffer += chunk;
99
105
  for (let nl = buffer.indexOf("\n"); nl !== -1; nl = buffer.indexOf("\n")) {
100
- const msg = JSON.parse(buffer.slice(0, nl));
106
+ // Cells can write here through __cdpk_send too (#122).
107
+ const msg = parseJson(buffer.slice(0, nl));
101
108
  buffer = buffer.slice(nl + 1);
102
109
  if (isTargetMessage(msg))
103
110
  this.#deliver(msg, events);
@@ -116,12 +123,14 @@ export class BunTarget {
116
123
  if (!pending)
117
124
  return; // abandoned
118
125
  this.#pending.delete(id);
126
+ this.#interruptibleIds.delete(id);
119
127
  if (typeof reply.failure === "string")
120
128
  pending.reject(new Error(reply.failure));
121
129
  else
122
130
  pending.resolve(reply);
123
131
  }
124
132
  #exited(code, events) {
133
+ this.#exitedOnce = true;
125
134
  this.#gates.openAll();
126
135
  this.#stdout.close();
127
136
  this.#stderr.close();
@@ -132,6 +141,7 @@ export class BunTarget {
132
141
  for (const pending of this.#pending.values())
133
142
  pending.reject(new Abandoned(reason));
134
143
  this.#pending.clear();
144
+ this.#interruptibleIds.clear();
135
145
  }
136
146
  // Binding messages wait until both pipes have caught up to their marker,
137
147
  // and are delivered strictly in order (as in Target).
@@ -142,10 +152,15 @@ export class BunTarget {
142
152
  this.#gates.open(msg.barrier);
143
153
  });
144
154
  }
145
- #request(op, params = {}) {
155
+ #request(op, params = {}, interruptible = false) {
156
+ // Bun is gone; a write to its channel would never be answered.
157
+ if (this.#exitedOnce)
158
+ return Promise.reject(new Abandoned("the bun session is closed"));
146
159
  const id = ++this.#nextId;
147
160
  return new Promise((resolve, reject) => {
148
161
  this.#pending.set(id, { resolve, reject });
162
+ if (interruptible)
163
+ this.#interruptibleIds.add(id);
149
164
  this.#channel.write(JSON.stringify({ id, op, ...params }) + "\n");
150
165
  });
151
166
  }
@@ -153,7 +168,7 @@ export class BunTarget {
153
168
  async #interruptible(op, params, what) {
154
169
  this.#interrupted = false;
155
170
  try {
156
- return await this.#request(op, params);
171
+ return await this.#request(op, params, true);
157
172
  }
158
173
  catch (err) {
159
174
  if (err instanceof Abandoned || this.#interrupted)
@@ -162,7 +177,7 @@ export class BunTarget {
162
177
  }
163
178
  }
164
179
  async evaluate(script) {
165
- return (await this.#interruptible("eval", { code: script }, "Execution"));
180
+ return asEvaluateResult(await this.#interruptible("eval", { code: script }, "Execution"));
166
181
  }
167
182
  /**
168
183
  * There's no terminateExecution, but a cell that's waiting on a promise
@@ -173,12 +188,19 @@ export class BunTarget {
173
188
  */
174
189
  async interrupt() {
175
190
  this.#interrupted = true;
191
+ const running = [...this.#interruptibleIds];
176
192
  const answered = await Promise.race([
177
193
  this.#request("ping").then(() => true, () => false),
178
194
  new Promise((resolve) => setTimeout(() => resolve(false), INTERRUPT_GRACE_MS)),
179
195
  ]);
180
196
  if (answered) {
181
- this.#abandonPending("interrupted");
197
+ // Only the cell or comm handler: a barrier or broadcast request in
198
+ // flight would otherwise fail as a KernelError (#135).
199
+ for (const id of running) {
200
+ this.#pending.get(id)?.reject(new Abandoned("interrupted"));
201
+ this.#pending.delete(id);
202
+ this.#interruptibleIds.delete(id);
203
+ }
182
204
  return;
183
205
  }
184
206
  this.#stopNote = RESTART_NOTE;
@@ -227,8 +249,8 @@ export class BunTarget {
227
249
  lexicalNames() {
228
250
  return Promise.resolve([]);
229
251
  }
230
- replyInput(value) {
231
- this.#child.stdin?.write(JSON.stringify({ value }) + "\n");
252
+ replyInput(value, interrupted = false) {
253
+ this.#child.stdin?.write(JSON.stringify({ value, interrupted }) + "\n");
232
254
  }
233
255
  kill() {
234
256
  this.#child.kill();
package/dist/cdp.d.ts CHANGED
@@ -29,7 +29,6 @@ export declare class CdpAbandoned extends Error {
29
29
  }
30
30
  export declare class CdpSession {
31
31
  #private;
32
- readonly closed: Promise<void>;
33
32
  private constructor();
34
33
  static connect(url: string): Promise<CdpSession>;
35
34
  on(method: string, listener: (params: CdpParams) => void): void;
@@ -38,4 +37,10 @@ export declare class CdpSession {
38
37
  abandonPending(): void;
39
38
  close(): void;
40
39
  }
40
+ export declare function isRecord(value: unknown): value is CdpParams;
41
+ export declare function isRemoteObject(value: unknown): value is RemoteObject;
42
+ /**
43
+ * A Runtime.evaluate-shaped reply, checked: a value, or exceptionDetails.
44
+ * The Bun and workerd drivers send no `result` with a thrown exception.
45
+ */
41
46
  export declare function asEvaluateResult(raw: CdpParams): EvaluateResult;
package/dist/cdp.js CHANGED
@@ -21,7 +21,6 @@ export class CdpSession {
21
21
  #nextId = 0;
22
22
  #pending = new Map();
23
23
  #listeners = new Map();
24
- closed;
25
24
  constructor(ws) {
26
25
  this.#ws = ws;
27
26
  ws.addEventListener("message", (event) => {
@@ -43,14 +42,11 @@ export class CdpSession {
43
42
  listener(msg.params);
44
43
  }
45
44
  });
46
- this.closed = new Promise((resolve) => {
47
- ws.addEventListener("close", () => {
48
- for (const pending of this.#pending.values()) {
49
- pending.reject(new Error("CDP connection closed"));
50
- }
51
- this.#pending.clear();
52
- resolve();
53
- });
45
+ ws.addEventListener("close", () => {
46
+ for (const pending of this.#pending.values()) {
47
+ pending.reject(new Error("CDP connection closed"));
48
+ }
49
+ this.#pending.clear();
54
50
  });
55
51
  }
56
52
  static async connect(url) {
@@ -67,6 +63,10 @@ export class CdpSession {
67
63
  this.#listeners.set(method, list);
68
64
  }
69
65
  send(method, params = {}) {
66
+ // send() on a closing or closed socket drops the frame silently, so
67
+ // nothing would ever settle the call (#133).
68
+ if (this.#ws.readyState !== WebSocket.OPEN)
69
+ return Promise.reject(new Error("CDP connection closed"));
70
70
  const id = ++this.#nextId;
71
71
  return new Promise((resolve, reject) => {
72
72
  this.#pending.set(id, { method, resolve, reject });
@@ -84,6 +84,21 @@ export class CdpSession {
84
84
  this.#ws.close();
85
85
  }
86
86
  }
87
+ export function isRecord(value) {
88
+ return typeof value === "object" && value !== null;
89
+ }
90
+ export function isRemoteObject(value) {
91
+ return isRecord(value) && typeof value.type === "string";
92
+ }
93
+ /**
94
+ * A Runtime.evaluate-shaped reply, checked: a value, or exceptionDetails.
95
+ * The Bun and workerd drivers send no `result` with a thrown exception.
96
+ */
87
97
  export function asEvaluateResult(raw) {
88
- return raw;
98
+ const details = raw.exceptionDetails;
99
+ if (details !== undefined ? !isRecord(details) || typeof details.text !== "string" : !isRemoteObject(raw.result)) {
100
+ throw new Error(`the target sent a malformed evaluate result: ${JSON.stringify(raw).slice(0, 200)}`);
101
+ }
102
+ const result = isRemoteObject(raw.result) ? raw.result : { type: "undefined" };
103
+ return details === undefined ? { result } : { result, exceptionDetails: details };
89
104
  }
@@ -1,4 +1,4 @@
1
- import type { CdpParams } from "./cdp.ts";
1
+ import { type CdpParams } from "./cdp.ts";
2
2
  import type { TargetEvents } from "./target.ts";
3
3
  import { WorkerdTarget } from "./workerd-target.ts";
4
4
  /** The project keys the generated config keeps: bindings and compatibility. */
@@ -21,8 +21,10 @@ import { createServer } from "node:net";
21
21
  import { homedir, tmpdir } from "node:os";
22
22
  import { dirname, join, resolve } from "node:path";
23
23
  import { build } from "esbuild";
24
+ import { isRecord } from "./cdp.js";
24
25
  import { parseJsonc } from "./deno-config.js";
25
26
  import { runtimeDir } from "./package.js";
27
+ import { killOnExit } from "./children.js";
26
28
  import { cellMeta, WorkerdTarget } from "./workerd-target.js";
27
29
  import { builtinAliases, findWranglerConfig } from "./workers-project.js";
28
30
  /** The project keys the generated config keeps: bindings and compatibility. */
@@ -141,6 +143,8 @@ function killTree(child) {
141
143
  }
142
144
  child.kill("SIGKILL");
143
145
  }
146
+ /** Starts tried when a port is taken (see CelldHost.start). */
147
+ const PORT_ATTEMPTS = 3;
144
148
  /** The driver on `celld dev`. */
145
149
  class CelldHost {
146
150
  runtime;
@@ -153,7 +157,23 @@ class CelldHost {
153
157
  this.runtime = runtime;
154
158
  this.project = project;
155
159
  }
160
+ /**
161
+ * celld dev, retried when a port is taken: both its --port (from
162
+ * freePort) and its node's own listener are picked free and bound later,
163
+ * so another process can take one in between (#54).
164
+ */
156
165
  static async start(cwd, projectConfig, executable) {
166
+ for (let attempt = 1;; attempt++) {
167
+ try {
168
+ return await CelldHost.#start(cwd, projectConfig, executable);
169
+ }
170
+ catch (err) {
171
+ if (attempt >= PORT_ATTEMPTS || !(err instanceof Error && /Address already in use/.test(err.message)))
172
+ throw err;
173
+ }
174
+ }
175
+ }
176
+ static async #start(cwd, projectConfig, executable) {
157
177
  const celld = executable ?? findCelld();
158
178
  const version = celldVersionOf(execFileSync(celld, ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }));
159
179
  const source = await runtimeDir(new URL("../runtime/", import.meta.url).href);
@@ -163,7 +183,7 @@ class CelldHost {
163
183
  if (projectConfig.endsWith(".toml"))
164
184
  throw new Error("celld reads wrangler.jsonc or wrangler.json, not wrangler.toml");
165
185
  const parsed = parseJsonc(readFileSync(projectConfig, "utf8"));
166
- if (typeof parsed !== "object" || parsed === null)
186
+ if (!isRecord(parsed) || Array.isArray(parsed))
167
187
  throw new Error(`${projectConfig} is not an object`);
168
188
  project = parsed;
169
189
  dir = join(dirname(projectConfig), ".celld", "cdp-kernel");
@@ -173,9 +193,14 @@ class CelldHost {
173
193
  dir = mkdtempSync(join(tmpdir(), "cdpk-celld-"));
174
194
  }
175
195
  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
- : {};
196
+ const define = {};
197
+ if (project && isRecord(project.define)) {
198
+ for (const [name, value] of Object.entries(project.define)) {
199
+ if (typeof value !== "string")
200
+ throw new Error(`define.${name} in ${projectConfig} must be a string`);
201
+ define[name] = value;
202
+ }
203
+ }
179
204
  const token = randomBytes(32).toString("hex");
180
205
  // 0600: the config carries this launch's token.
181
206
  writeFileSync(join(dir, "driver.mjs"), await bundleDriver(source, main, define));
@@ -186,25 +211,35 @@ class CelldHost {
186
211
  const child = spawn(celld, ["dev", dir, "--host", "127.0.0.1", "--port", String(port), "--no-watch"], {
187
212
  stdio: ["ignore", "pipe", "pipe"],
188
213
  });
189
- child.once("exit", () => {
214
+ const removeFiles = () => {
190
215
  if (projectConfig === null)
191
216
  rmSync(dir, { recursive: true, force: true });
192
217
  else
193
218
  for (const name of ["driver.mjs", "wrangler.jsonc"])
194
219
  rmSync(join(dir, name), { force: true });
220
+ };
221
+ child.once("exit", removeFiles);
222
+ killOnExit(child, () => {
223
+ killTree(child);
224
+ removeFiles();
195
225
  });
196
226
  // Cell output is captured in the driver; celld's own lines go to the
197
227
  // server log.
228
+ // `early` keeps that output for a startup error only, and stops growing
229
+ // once the session is open.
198
230
  let early = "";
231
+ let starting = true;
199
232
  const log = (chunk) => {
200
- early += chunk.toString();
233
+ if (starting)
234
+ early += chunk.toString();
201
235
  process.stderr.write(`[cdp-kernel celld] ${chunk}`);
202
236
  };
203
237
  child.stdout?.on("data", log);
204
238
  child.stderr?.on("data", log);
205
239
  const exited = new Promise((_, reject) => {
206
240
  child.once("error", reject);
207
- child.once("exit", (code) => reject(new Error(`celld exited (${code}) before it was ready:\n${early}`)));
241
+ // On close, not exit, so its last output is in the message.
242
+ child.once("close", (code) => reject(new Error(`celld exited (${code}) before it was ready:\n${early}`)));
208
243
  });
209
244
  exited.catch(() => { });
210
245
  const url = `127.0.0.1:${port}/?token=${token}`;
@@ -226,7 +261,10 @@ class CelldHost {
226
261
  return channelOf(ws);
227
262
  })();
228
263
  try {
229
- return new CelldHost(child, await Promise.race([ready, exited]), `celld ${version}`.trim(), projectConfig ?? undefined);
264
+ const host = new CelldHost(child, await Promise.race([ready, exited]), `celld ${version}`.trim(), projectConfig ?? undefined);
265
+ starting = false;
266
+ early = "";
267
+ return host;
230
268
  }
231
269
  catch (err) {
232
270
  killTree(child);
@@ -0,0 +1,7 @@
1
+ import type { ChildProcess } from "node:child_process";
2
+ /**
3
+ * Kills `child` if it's still running when this process exits. `cleanup`
4
+ * replaces the default SIGKILL; it must be synchronous, as exit hooks are,
5
+ * and do whatever the child's own exit listener would have done.
6
+ */
7
+ export declare function killOnExit(child: ChildProcess, cleanup?: () => void): void;
@@ -0,0 +1,32 @@
1
+ // Runtimes this process started, killed when it exits however it exits
2
+ // short of SIGKILL: a target left behind keeps running, reparented to pid 1,
3
+ // with whatever temp directory it uses (#127, #218).
4
+ import process from "node:process";
5
+ const cleanups = new Set();
6
+ function runAll() {
7
+ for (const cleanup of cleanups) {
8
+ try {
9
+ cleanup();
10
+ }
11
+ catch {
12
+ // Best effort: the process is exiting.
13
+ }
14
+ }
15
+ cleanups.clear();
16
+ }
17
+ let hooked = false;
18
+ /**
19
+ * Kills `child` if it's still running when this process exits. `cleanup`
20
+ * replaces the default SIGKILL; it must be synchronous, as exit hooks are,
21
+ * and do whatever the child's own exit listener would have done.
22
+ */
23
+ export function killOnExit(child, cleanup = () => child.kill("SIGKILL")) {
24
+ if (!hooked) {
25
+ hooked = true;
26
+ process.on("exit", runAll);
27
+ }
28
+ if (child.exitCode !== null || child.signalCode !== null)
29
+ return;
30
+ cleanups.add(cleanup);
31
+ child.once("exit", () => cleanups.delete(cleanup));
32
+ }
@@ -1,4 +1,4 @@
1
- import type { CdpParams, RemoteObject } from "./cdp.ts";
1
+ import { type CdpParams, type RemoteObject } from "./cdp.ts";
2
2
  import { type CellTransformOptions } from "./transform.ts";
3
3
  /** What the debugger needs from a CDP target: its session, with the Debugger domain. */
4
4
  export interface DebugChannel {
package/dist/debugger.js CHANGED
@@ -8,13 +8,14 @@
8
8
  // as their //# sourceURL, so breakpoints bind and frames point at the cell.
9
9
  // Lines go through the cell's source map (src/sourcemap.ts), since esbuild
10
10
  // rewrites TypeScript.
11
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
11
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
12
12
  import { tmpdir } from "node:os";
13
13
  import { join, sep } from "node:path";
14
14
  import process from "node:process";
15
15
  import { fileURLToPath } from "node:url";
16
+ import { isRecord, isRemoteObject } from "./cdp.js";
16
17
  import { murmur2 } from "./murmur2.js";
17
- import { transformCellWithLines } from "./transform.js";
18
+ import { SETTLE, transformCellWithLines } from "./transform.js";
18
19
  /** ipykernel's seed, so a kernel switch doesn't move JupyterLab's breakpoints. */
19
20
  export const HASH_SEED = 0xc70f6907;
20
21
  export const THREAD_ID = 1;
@@ -35,9 +36,6 @@ const SCOPE_NAMES = {
35
36
  eval: "Eval",
36
37
  global: "Globals",
37
38
  };
38
- function isRecord(value) {
39
- return typeof value === "object" && value !== null;
40
- }
41
39
  /**
42
40
  * CDPK_DEBUG_TRACE=1 logs every debug request, reply and event to stderr
43
41
  * (the Jupyter server's log), for diagnosing a frontend.
@@ -48,8 +46,11 @@ function trace(kind, ...parts) {
48
46
  const text = parts.map((p) => JSON.stringify(p)).join(" -> ");
49
47
  process.stderr.write(`[cdp-kernel debugger] ${kind} ${text.length > 600 ? `${text.slice(0, 600)}…` : text}\n`);
50
48
  }
51
- function isRemoteObject(value) {
52
- return isRecord(value) && typeof value.type === "string";
49
+ function isCallFrame(value) {
50
+ return isRecord(value) && typeof value.callFrameId === "string" && typeof value.functionName === "string" &&
51
+ isRecord(value.location) && typeof value.location.scriptId === "string" &&
52
+ typeof value.location.lineNumber === "number" && Array.isArray(value.scopeChain) &&
53
+ value.scopeChain.every((scope) => isRecord(scope) && typeof scope.type === "string" && isRemoteObject(scope.object));
53
54
  }
54
55
  /** A DAP value string for a CDP RemoteObject. */
55
56
  export function describeRemote(object) {
@@ -88,6 +89,7 @@ export class Debugger {
88
89
  #pauseWaiters = [];
89
90
  #emit;
90
91
  #transformOptions;
92
+ #cleanupRegistered = false;
91
93
  constructor(emit, transformOptions = () => ({})) {
92
94
  this.#emit = emit;
93
95
  this.#transformOptions = transformOptions;
@@ -330,7 +332,7 @@ export class Debugger {
330
332
  }
331
333
  // --- stopped state ----------------------------------------------------------
332
334
  #onPaused(params) {
333
- this.#frames = Array.isArray(params.callFrames) ? params.callFrames : [];
335
+ this.#frames = Array.isArray(params.callFrames) ? params.callFrames.filter(isCallFrame) : [];
334
336
  this.#stopped = true;
335
337
  for (const notify of this.#pauseWaiters.splice(0))
336
338
  notify();
@@ -453,7 +455,6 @@ export class Debugger {
453
455
  const res = await this.#unlessPaused(channel.send("Runtime.evaluate", {
454
456
  expression: name,
455
457
  contextId: channel.contextId,
456
- replMode: true,
457
458
  throwOnSideEffect: true,
458
459
  }));
459
460
  if (res === null)
@@ -470,9 +471,14 @@ export class Debugger {
470
471
  throw new Error("no target to debug");
471
472
  // While paused, only a frame can evaluate: use the top one if none was named.
472
473
  const frame = typeof frameId === "number" ? this.#frames[frameId] : this.#stopped ? this.#frames[0] : undefined;
474
+ // replMode (for the console's `await`) returns a promise the inspector
475
+ // holds only weakly: SETTLE closes that window, as for cells (#55). A
476
+ // side-effect-free lookup can't await, so it uses a plain evaluate.
473
477
  const res = frame
474
478
  ? await channel.send("Debugger.evaluateOnCallFrame", { callFrameId: frame.callFrameId, expression, throwOnSideEffect: pure })
475
- : await channel.send("Runtime.evaluate", { expression, contextId: channel.contextId, replMode: true, throwOnSideEffect: pure });
479
+ : pure
480
+ ? await channel.send("Runtime.evaluate", { expression, contextId: channel.contextId, throwOnSideEffect: true })
481
+ : await channel.send("Runtime.evaluate", { expression: expression + SETTLE, contextId: channel.contextId, replMode: true });
476
482
  if (isRecord(res.exceptionDetails)) {
477
483
  const exception = res.exceptionDetails.exception;
478
484
  throw new Error(exception ? describeRemote(exception) : String(res.exceptionDetails.text ?? "evaluation failed"));
@@ -519,6 +525,11 @@ export class Debugger {
519
525
  }
520
526
  #writeCell(path, code) {
521
527
  try {
528
+ if (!this.#cleanupRegistered) {
529
+ // The files only serve this kernel's frontend: remove them with it.
530
+ process.once("exit", () => rmSync(this.prefix, { recursive: true, force: true }));
531
+ this.#cleanupRegistered = true;
532
+ }
522
533
  mkdirSync(this.prefix, { recursive: true });
523
534
  writeFileSync(path, code);
524
535
  }
package/dist/globals.d.ts CHANGED
@@ -2,9 +2,9 @@
2
2
  * Ambient types for notebook cells run by cdp-kernel: the `jupyter` display
3
3
  * namespace and the stdin-backed `prompt`/`confirm`.
4
4
  *
5
- * Load it once per project, for example
6
- * `/// <reference types="@tlockney/cdp-kernel/globals" />` (npm) or
7
- * `import type {} from "jsr:@tlockney/cdp-kernel/globals";` (Deno).
5
+ * Load it once per project, for example with
6
+ * `/// <reference types="@tlockney/cdp-kernel/globals" />`. It ships in the
7
+ * npm package only: Deno notebooks use `Deno.jupyter`, which Deno types.
8
8
  *
9
9
  * A custom display is a class with a `[jupyter.$display]()` method returning
10
10
  * a media bundle. At runtime that key is `Symbol.for("Jupyter.display")`, but
package/dist/globals.js CHANGED
@@ -2,9 +2,9 @@
2
2
  * Ambient types for notebook cells run by cdp-kernel: the `jupyter` display
3
3
  * namespace and the stdin-backed `prompt`/`confirm`.
4
4
  *
5
- * Load it once per project, for example
6
- * `/// <reference types="@tlockney/cdp-kernel/globals" />` (npm) or
7
- * `import type {} from "jsr:@tlockney/cdp-kernel/globals";` (Deno).
5
+ * Load it once per project, for example with
6
+ * `/// <reference types="@tlockney/cdp-kernel/globals" />`. It ships in the
7
+ * npm package only: Deno notebooks use `Deno.jupyter`, which Deno types.
8
8
  *
9
9
  * A custom display is a class with a `[jupyter.$display]()` method returning
10
10
  * a media bundle. At runtime that key is `Symbol.for("Jupyter.display")`, but