@tlockney/cdp-kernel 0.7.0 → 0.7.2

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/debugger.js CHANGED
@@ -8,7 +8,7 @@
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, rmSync, writeFileSync } from "node:fs";
11
+ import { lstatSync, 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";
@@ -23,6 +23,9 @@ const EXCEPTION_FILTERS = [
23
23
  { filter: "caught", label: "Caught Exceptions", default: false },
24
24
  { filter: "uncaught", label: "Uncaught Exceptions", default: false },
25
25
  ];
26
+ /** URLs of the runtime's built-in modules: Deno's `ext:`, Node's `node:`. */
27
+ const RUNTIME_SCRIPTS = ["^ext:", "^node:"];
28
+ const RUNTIME_SCRIPT = /^(ext|node):/;
26
29
  /** Bindings the kernel itself installs, hidden from the variable views. */
27
30
  const KERNEL_GLOBALS = new Set(["__cdpk", "__cdpk_send", "jupyter", "prompt", "confirm"]);
28
31
  const SCOPE_NAMES = {
@@ -52,6 +55,18 @@ function isCallFrame(value) {
52
55
  typeof value.location.lineNumber === "number" && Array.isArray(value.scopeChain) &&
53
56
  value.scopeChain.every((scope) => isRecord(scope) && typeof scope.type === "string" && isRemoteObject(scope.object));
54
57
  }
58
+ /**
59
+ * Whether `dir` is a directory, not a symlink, owned by this user and
60
+ * closed to others. Windows has no uids; its tmpdir() is per-user.
61
+ */
62
+ function isOwnPrivateDir(dir) {
63
+ const stat = lstatSync(dir);
64
+ if (!stat.isDirectory())
65
+ return false;
66
+ if (process.getuid === undefined)
67
+ return true;
68
+ return stat.uid === process.getuid() && (stat.mode & 0o077) === 0;
69
+ }
55
70
  /** A DAP value string for a CDP RemoteObject. */
56
71
  export function describeRemote(object) {
57
72
  if (object.type === "string")
@@ -256,6 +271,8 @@ export class Debugger {
256
271
  if (!channel)
257
272
  return;
258
273
  await channel.send("Debugger.enable");
274
+ // Stepping doesn't enter the runtime's own code (#380).
275
+ await channel.send("Debugger.setBlackboxPatterns", { patterns: RUNTIME_SCRIPTS });
259
276
  await channel.send("Debugger.setPauseOnExceptions", { state: this.#pauseOnExceptions });
260
277
  this.#cdpBreakpoints.clear();
261
278
  for (const path of this.#breakpoints.keys())
@@ -332,21 +349,38 @@ export class Debugger {
332
349
  }
333
350
  // --- stopped state ----------------------------------------------------------
334
351
  #onPaused(params) {
352
+ const hit = Array.isArray(params.hitBreakpoints) && params.hitBreakpoints.length > 0;
353
+ const exception = params.reason === "exception" || params.reason === "promiseRejection";
354
+ if (exception && !hit && this.#inRuntime(params.callFrames)) {
355
+ // Thrown in the runtime's own code, as Chrome's ignore list treats it.
356
+ // Deno's lazy node:process setup throws and catches one there, which
357
+ // blackboxing doesn't always skip, and it held the cell (#380). The
358
+ // cell still sees an error that reaches it.
359
+ void this.#channel?.send("Debugger.resume").catch(() => { });
360
+ return;
361
+ }
335
362
  this.#frames = Array.isArray(params.callFrames) ? params.callFrames.filter(isCallFrame) : [];
336
363
  this.#stopped = true;
337
364
  for (const notify of this.#pauseWaiters.splice(0))
338
365
  notify();
339
366
  this.#refs.clear();
340
- const hit = Array.isArray(params.hitBreakpoints) && params.hitBreakpoints.length > 0;
341
367
  const reason = hit
342
368
  ? "breakpoint"
343
- : params.reason === "exception" || params.reason === "promiseRejection"
369
+ : exception
344
370
  ? "exception"
345
371
  : params.reason === "other"
346
372
  ? "step"
347
373
  : String(params.reason ?? "pause");
348
374
  this.#event("stopped", { reason, threadId: THREAD_ID, allThreadsStopped: true });
349
375
  }
376
+ /** Whether the top frame is the runtime's own code. */
377
+ #inRuntime(callFrames) {
378
+ const top = Array.isArray(callFrames) ? callFrames[0] : undefined;
379
+ if (!isRecord(top) || !isRecord(top.location))
380
+ return false;
381
+ const url = this.#scripts.get(String(top.location.scriptId)) ?? (typeof top.url === "string" ? top.url : "");
382
+ return RUNTIME_SCRIPT.test(url);
383
+ }
350
384
  #resetStop() {
351
385
  this.#stopped = false;
352
386
  this.#frames = [];
@@ -526,12 +560,24 @@ export class Debugger {
526
560
  }
527
561
  #writeCell(path, code) {
528
562
  try {
563
+ mkdirSync(this.prefix, { recursive: true, mode: 0o700 });
564
+ // The path is predictable (the frontend computes it too), so another
565
+ // user could have made the directory first and planted symlinks in it:
566
+ // write only into a directory that is ours and closed to others (#339).
567
+ if (!isOwnPrivateDir(this.prefix))
568
+ return;
529
569
  if (!this.#cleanupRegistered) {
530
570
  // The files only serve this kernel's frontend: remove them with it.
531
- process.once("exit", () => rmSync(this.prefix, { recursive: true, force: true }));
571
+ process.once("exit", () => {
572
+ try {
573
+ rmSync(this.prefix, { recursive: true, force: true });
574
+ }
575
+ catch {
576
+ // Best effort; exiting matters more.
577
+ }
578
+ });
532
579
  this.#cleanupRegistered = true;
533
580
  }
534
- mkdirSync(this.prefix, { recursive: true });
535
581
  writeFileSync(path, code);
536
582
  }
537
583
  catch {
@@ -37,6 +37,8 @@ export interface KernelHost {
37
37
  /** The target's name, for the banner and messages. */
38
38
  readonly runtime: string;
39
39
  readonly version: string;
40
+ /** The TypeScript version cells are checked against; TYPESCRIPT_VERSION if not given. */
41
+ readonly languageVersion?: string;
40
42
  readonly channels: KernelChannels;
41
43
  launchTarget(events: TargetEvents): Promise<KernelTarget>;
42
44
  /** What strips cells' types; esbuild by default (#238). */
@@ -46,6 +48,8 @@ export interface KernelHost {
46
48
  /** Shown once, in the first cell: e.g. why a project config wasn't used. */
47
49
  readonly warning?: string | null;
48
50
  readonly types?: TypeService | null;
51
+ /** Returned as `capabilities` in kernel_info_reply, when set: what the host offers cells (#384). */
52
+ readonly capabilities?: JsonObject;
49
53
  /** JupyterLab's visual debugger, for targets driven over CDP (#18). */
50
54
  createDebugger?(publish: (event: JsonObject) => void, transformOptions: () => CellTransformOptions): Debugger;
51
55
  /** A file's source, for the lines around a traceback frame (#322). */
@@ -56,6 +60,11 @@ export interface KernelHost {
56
60
  /** After shutdown_reply has gone out. */
57
61
  exit(): void;
58
62
  }
63
+ /**
64
+ * kernel_info's language_info.version when the host doesn't know the
65
+ * installed TypeScript's: the major version package.json depends on.
66
+ */
67
+ export declare const TYPESCRIPT_VERSION = "6";
59
68
  export declare class KernelCore {
60
69
  #private;
61
70
  constructor(host: KernelHost);
@@ -14,6 +14,11 @@ import { CellSyntaxError, loadTypeStripper, objectLiteralCandidate, transformCel
14
14
  import { checkIsComplete, codePointToUtf16Index, exprAtCursor, inspectTarget, utf16ToCodePointIndex } from "./text.js";
15
15
  import { queryHistory } from "./history.js";
16
16
  import { buildTraceback, cellName, TRACEBACK_MIME } from "./traceback.js";
17
+ /**
18
+ * kernel_info's language_info.version when the host doesn't know the
19
+ * installed TypeScript's: the major version package.json depends on.
20
+ */
21
+ export const TYPESCRIPT_VERSION = "6";
17
22
  function fromBase64(text) {
18
23
  return Uint8Array.from(atob(text), (c) => c.charCodeAt(0));
19
24
  }
@@ -90,6 +95,8 @@ export class KernelCore {
90
95
  #input = null;
91
96
  /** An interrupt is waiting on the target. */
92
97
  #interrupting = false;
98
+ /** Ends the cell waiting for the target to start, while one is (#336). */
99
+ #interruptStart = null;
93
100
  /** The shell message being handled, and since when. */
94
101
  #handling = null;
95
102
  // Open comms, for comm_info_request: comm_id -> target_name (#9).
@@ -225,6 +232,23 @@ export class KernelCore {
225
232
  });
226
233
  return this.#targetStarting;
227
234
  }
235
+ /**
236
+ * The target for a cell. An interrupt while it starts ends the cell; the
237
+ * start goes on, bounded by the target's own deadline, for the next one (#336).
238
+ */
239
+ async #targetForCell() {
240
+ if (this.#target)
241
+ return this.#target;
242
+ const interrupted = new Promise((_, reject) => {
243
+ this.#interruptStart = () => reject(new Interrupted("Execution interrupted"));
244
+ });
245
+ try {
246
+ return await Promise.race([this.#ensureTarget(), interrupted]);
247
+ }
248
+ finally {
249
+ this.#interruptStart = null;
250
+ }
251
+ }
228
252
  #targetEvents() {
229
253
  return {
230
254
  stream: (name, text) => this.#publish("stream", { name, text }),
@@ -274,6 +298,10 @@ export class KernelCore {
274
298
  input.settle({ value: null, interrupted: true });
275
299
  return;
276
300
  }
301
+ if (this.#interruptStart !== null) {
302
+ this.#interruptStart();
303
+ return;
304
+ }
277
305
  if (!this.#target || !this.#executing)
278
306
  return;
279
307
  this.#interrupting = true;
@@ -306,6 +334,8 @@ export class KernelCore {
306
334
  async #loop(channel, handle) {
307
335
  while (!this.#shuttingDown) {
308
336
  const received = await this.#channels.recv(channel);
337
+ // Before the drop below: shellQueued() counted this message too, so
338
+ // a dropped one takes its own abort slot, not a queued cell's (#340).
309
339
  if (channel === "shell") {
310
340
  this.#aborting = this.#abortQueued > 0;
311
341
  if (this.#aborting)
@@ -338,7 +368,7 @@ export class KernelCore {
338
368
  implementation_version: this.#host.version,
339
369
  language_info: {
340
370
  name: "typescript",
341
- version: "5",
371
+ version: this.#host.languageVersion ?? TYPESCRIPT_VERSION,
342
372
  mimetype: "text/x.typescript",
343
373
  file_extension: ".ts",
344
374
  pygments_lexer: "typescript",
@@ -352,6 +382,7 @@ export class KernelCore {
352
382
  // What current ipykernel advertises; JupyterLab itself goes by the
353
383
  // kernelspec's metadata.debugger.
354
384
  supported_features: this.#debugger !== null ? ["debugger"] : [],
385
+ ...(this.#host.capabilities ? { capabilities: this.#host.capabilities } : {}),
355
386
  };
356
387
  }
357
388
  // --- control channel ------------------------------------------------------
@@ -648,6 +679,41 @@ export class KernelCore {
648
679
  }
649
680
  return target.evaluate(transform(code));
650
681
  }
682
+ /** execute_request's user_expressions, each as its value's MIME bundle or the error it threw (#340). */
683
+ async #userExpressions(target, expressions) {
684
+ const results = {};
685
+ if (typeof expressions !== "object" || expressions === null)
686
+ return results;
687
+ for (const [name, expression] of Object.entries(expressions)) {
688
+ results[name] = await this.#userExpression(target, String(expression));
689
+ }
690
+ return results;
691
+ }
692
+ async #userExpression(target, expression) {
693
+ try {
694
+ // On their own lines, so a trailing line comment can't swallow the paren.
695
+ const source = `(\n${expression}\n)`;
696
+ const result = await target.evaluate(`${target.transform(source, this.#transformOptions())}\n//# sourceURL=$cdpk$user-expression.ts`);
697
+ if (result.exceptionDetails) {
698
+ const exception = result.exceptionDetails.exception;
699
+ const described = exception
700
+ ? await target.describeError(exception)
701
+ : { name: "Error", message: result.exceptionDetails.text, stack: "" };
702
+ return {
703
+ status: "error",
704
+ ename: described.name || "Error",
705
+ evalue: described.message,
706
+ traceback: (described.stack || `${described.name}: ${described.message}`).split("\n"),
707
+ };
708
+ }
709
+ return { status: "ok", data: await target.format(result.result), metadata: {} };
710
+ }
711
+ catch (err) {
712
+ const ename = err instanceof CellSyntaxError ? "SyntaxError" : err instanceof Interrupted ? "Interrupted" : "Error";
713
+ const evalue = err instanceof Error ? err.message : String(err);
714
+ return { status: "error", ename, evalue, traceback: [`${ename}: ${evalue}`] };
715
+ }
716
+ }
651
717
  async #execute(msg, peer) {
652
718
  const parent = msg.header;
653
719
  if (this.#aborting) {
@@ -676,11 +742,12 @@ export class KernelCore {
676
742
  this.#projectWarning = null;
677
743
  }
678
744
  let error = null;
745
+ let userExpressions = {};
679
746
  // nteract's structured traceback, sent as the error message's metadata (#322).
680
747
  let structured = null;
681
748
  this.#executing = true;
682
749
  try {
683
- const target = await this.#ensureTarget();
750
+ const target = await this.#targetForCell();
684
751
  if (target.notice !== undefined && this.#noticeShownFor !== target) {
685
752
  // Once per target, in the first cell it runs.
686
753
  this.#noticeShownFor = target;
@@ -709,6 +776,9 @@ export class KernelCore {
709
776
  else if (!silent && result.result.type !== "undefined") {
710
777
  await target.broadcastResult(count, result.result);
711
778
  }
779
+ // As in ipykernel: only after a cell that succeeded (#340).
780
+ if (!result.exceptionDetails)
781
+ userExpressions = await this.#userExpressions(target, msg.content.user_expressions);
712
782
  await target.flush();
713
783
  }
714
784
  catch (err) {
@@ -746,7 +816,7 @@ export class KernelCore {
746
816
  status: "ok",
747
817
  execution_count: count,
748
818
  payload: [],
749
- user_expressions: {},
819
+ user_expressions: userExpressions,
750
820
  });
751
821
  }
752
822
  this.#status("idle", parent);
package/dist/kernel.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // file's ZeroMQ ports, with a target runtime launched as a child process,
3
3
  // the language service in a worker thread, and the visual debugger.
4
4
  import { existsSync, readFileSync, statSync } from "node:fs";
5
+ import { createRequire } from "node:module";
5
6
  import { dirname, join } from "node:path";
6
7
  import { fileURLToPath } from "node:url";
7
8
  import process from "node:process";
@@ -17,6 +18,16 @@ import { cacheRoot, VERSION } from "./package.js";
17
18
  import { watchParent } from "./parent.js";
18
19
  import { KernelCore } from "./kernel-core.js";
19
20
  import { ZmqChannels } from "./zmq-channels.js";
21
+ /** The installed TypeScript's version, for kernel_info's language_info (#340). */
22
+ function typescriptVersion() {
23
+ try {
24
+ const { version } = createRequire(import.meta.url)("typescript/package.json");
25
+ return typeof version === "string" ? version : undefined;
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ }
20
31
  /** Larger files get no source lines in a traceback. */
21
32
  const MAX_SOURCE_BYTES = 1 << 20;
22
33
  /** A traceback frame's file, read from disk when it is a local one (#322). */
@@ -133,6 +144,7 @@ export class Kernel {
133
144
  this.#core = new KernelCore({
134
145
  runtime,
135
146
  version: VERSION,
147
+ languageVersion: typescriptVersion(),
136
148
  channels: new ZmqChannels(conn, log),
137
149
  launchTarget(events) {
138
150
  if (runtime === "bun")
package/dist/main.js CHANGED
@@ -31,7 +31,17 @@ function kernelsDir() {
31
31
  return join(process.env.APPDATA ?? homedir(), "jupyter", "kernels");
32
32
  return join(process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"), "jupyter", "kernels");
33
33
  }
34
+ // Jupyter's kernel names; anything else could point outside the kernels
35
+ // directory.
36
+ const KERNEL_NAME = /^[A-Za-z0-9._-]+$/;
37
+ function isKernelName(name) {
38
+ return KERNEL_NAME.test(name) && name !== "." && name !== "..";
39
+ }
34
40
  function install(runtime, denoCompat, typeStrip, languageService, name, display) {
41
+ if (!isKernelName(name)) {
42
+ console.error(`install: invalid kernelspec name ${JSON.stringify(name)}; use letters, digits, '.', '_' and '-'`);
43
+ process.exit(2);
44
+ }
35
45
  const dir = join(kernelsDir(), name);
36
46
  mkdirSync(dir, { recursive: true });
37
47
  const spec = {
@@ -97,9 +107,6 @@ function list() {
97
107
  console.log(`${s.name.padEnd(width)} ${s.runtime.padEnd(7)} ${(s.version ?? "-").padEnd(7)} ${s.dir}`);
98
108
  }
99
109
  }
100
- // Jupyter's kernel names; anything else could point outside the kernels
101
- // directory.
102
- const KERNEL_NAME = /^[A-Za-z0-9._-]+$/;
103
110
  function uninstall(names, all) {
104
111
  if (all)
105
112
  names = installedSpecs().map((s) => s.name);
@@ -110,7 +117,7 @@ function uninstall(names, all) {
110
117
  }
111
118
  let failed = false;
112
119
  for (const name of names) {
113
- const valid = KERNEL_NAME.test(name) && name !== "." && name !== "..";
120
+ const valid = isKernelName(name);
114
121
  const spec = valid ? readOwnSpec(name) : undefined;
115
122
  if (!spec) {
116
123
  const exists = valid && existsSync(join(kernelsDir(), name));
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.7.0";
2
+ export declare const VERSION = "0.7.2";
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-10-19";
8
+ export declare const RELEASE_DATE = "2026-11-02";
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
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.7.0";
17
+ export const VERSION = "0.7.2";
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-10-19";
23
+ export const RELEASE_DATE = "2026-11-02";
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
@@ -24,6 +24,8 @@ export interface KernelTarget {
24
24
  /** Resolves once all output and messages sent so far have been delivered. */
25
25
  flush(): Promise<void>;
26
26
  broadcastResult(executionCount: number, result: RemoteObject): Promise<void>;
27
+ /** A value's MIME bundle, as a cell's result would show it: for user_expressions (#340). */
28
+ format(value: RemoteObject): Promise<CdpParams>;
27
29
  dispatchComm(msgType: string, content: CdpParams, metadata: CdpParams, buffers: Uint8Array[]): Promise<{
28
30
  handled: boolean;
29
31
  } | {
@@ -66,6 +68,16 @@ export type TargetMessage = {
66
68
  password: boolean;
67
69
  barrier: number;
68
70
  };
71
+ /** A MIME bundle the target sent as JSON text; empty if it isn't one. */
72
+ export declare function parseBundle(text: unknown): CdpParams;
73
+ /** How long a target may take from launch to a usable session. */
74
+ export declare const START_TIMEOUT_MS = 60000;
75
+ /**
76
+ * `start`, or a rejection once `ms` pass without it settling. The caller
77
+ * stops whatever was starting: a runtime that never becomes ready would
78
+ * otherwise wedge the first cell (#52, #336).
79
+ */
80
+ export declare function withStartTimeout<T>(start: Promise<T>, what: string, ms?: number): Promise<T>;
69
81
  /** The parsed value, or undefined for text that isn't JSON. */
70
82
  export declare function parseJson(text: string): unknown;
71
83
  export declare function isTargetMessage(value: unknown): value is TargetMessage;
@@ -1,6 +1,32 @@
1
1
  // What the kernel needs from a target runtime, and what a target sends
2
2
  // back: the part of src/target.ts with no Node imports, so the kernel's
3
3
  // message handling (src/kernel-core.ts) also loads in a browser.
4
+ /** A MIME bundle the target sent as JSON text; empty if it isn't one. */
5
+ export function parseBundle(text) {
6
+ const bundle = typeof text === "string" ? parseJson(text) : undefined;
7
+ return typeof bundle === "object" && bundle !== null && !Array.isArray(bundle) ? bundle : {};
8
+ }
9
+ /** How long a target may take from launch to a usable session. */
10
+ export const START_TIMEOUT_MS = 60_000;
11
+ /**
12
+ * `start`, or a rejection once `ms` pass without it settling. The caller
13
+ * stops whatever was starting: a runtime that never becomes ready would
14
+ * otherwise wedge the first cell (#52, #336).
15
+ */
16
+ export async function withStartTimeout(start, what, ms = START_TIMEOUT_MS) {
17
+ // Its own rejection, once the deadline has passed, is the caller's to ignore.
18
+ start.catch(() => { });
19
+ let timer;
20
+ const stalled = new Promise((_, reject) => {
21
+ timer = setTimeout(() => reject(new Error(`${what} did not start within ${ms / 1000}s`)), ms);
22
+ });
23
+ try {
24
+ return await Promise.race([start, stalled]);
25
+ }
26
+ finally {
27
+ clearTimeout(timer);
28
+ }
29
+ }
4
30
  /** The parsed value, or undefined for text that isn't JSON. */
5
31
  export function parseJson(text) {
6
32
  try {
package/dist/target.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { type CdpParams, type EvaluateResult, type RemoteObject } from "./cdp.ts";
2
2
  import { transformCell } from "./transform.ts";
3
3
  import type { DebugChannel } from "./debugger.ts";
4
- import { type CdpRuntime, Interrupted, isTargetMessage, type KernelTarget, parseJson, type TargetEvents, type TargetMessage } from "./target-api.ts";
5
- export { Interrupted, isTargetMessage, parseJson };
4
+ import { type CdpRuntime, Interrupted, isTargetMessage, type KernelTarget, parseBundle, parseJson, type TargetEvents, type TargetMessage } from "./target-api.ts";
5
+ export { Interrupted, isTargetMessage, parseBundle, parseJson };
6
6
  export type { CdpRuntime, KernelTarget, RuntimeName, TargetEvents, TargetMessage } from "./target-api.ts";
7
7
  export interface LaunchOptions {
8
8
  /** Runtime binary; defaults to `deno`, or `node` (this one when hosted by Node). */
@@ -116,6 +116,7 @@ export declare class Target implements KernelTarget {
116
116
  callFunctionOn(functionDeclaration: string, args: CdpParams[]): Promise<CdpParams>;
117
117
  /** The broadcastResult step of Deno's execute flow (jupyter/mod.rs). */
118
118
  broadcastResult(executionCount: number, result: RemoteObject): Promise<void>;
119
+ format(value: RemoteObject): Promise<CdpParams>;
119
120
  /**
120
121
  * Hands a frontend comm message to the target's comms. Resolves to
121
122
  * whether a comm or registered target took it, or to the error a handler
package/dist/target.js CHANGED
@@ -11,8 +11,8 @@ import { asEvaluateResult, CdpAbandoned, CdpError, CdpSession } from "./cdp.js";
11
11
  import { SHIM, transformCell } from "./transform.js";
12
12
  import { hostRuntime, isInsideNodeModules, runtimeDir } from "./package.js";
13
13
  import { killOnExit } from "./children.js";
14
- import { Interrupted, isTargetMessage, parseJson } from "./target-api.js";
15
- export { Interrupted, isTargetMessage, parseJson };
14
+ import { Interrupted, isTargetMessage, parseBundle, parseJson, withStartTimeout, } from "./target-api.js";
15
+ export { Interrupted, isTargetMessage, parseBundle, parseJson };
16
16
  /**
17
17
  * The inspector keeps every object it returns a remote reference to alive
18
18
  * until it is released, so a cell's result would never be collected (#286).
@@ -21,8 +21,6 @@ export { Interrupted, isTargetMessage, parseJson };
21
21
  */
22
22
  const CELL_GROUP = "cdpk-cell";
23
23
  const LOOKUP_GROUP = "cdpk-lookup";
24
- /** How long a target may take from spawn to a usable session. */
25
- const START_TIMEOUT_MS = 60_000;
26
24
  const MARKER_RE = /\x1b\[cdpk:(\d+)(m?)\]\x1b/;
27
25
  const MAX_MARKER_LEN = 24;
28
26
  // Lines the runtimes print to stderr on their own: inspector notices, and
@@ -386,13 +384,8 @@ export class Target {
386
384
  // A start that fails or stalls must not leave the runtime running: its
387
385
  // pipes and inspector sockets would keep this process alive (#52).
388
386
  const sessions = [];
389
- const timeoutMs = options.startTimeoutMs ?? START_TIMEOUT_MS;
390
- let timer;
391
- const stalled = new Promise((_, reject) => {
392
- timer = setTimeout(() => reject(new Error(`${cmd} did not start within ${timeoutMs / 1000}s`)), timeoutMs);
393
- });
394
387
  try {
395
- return await Promise.race([Target.#start(runtime, events, cmd, child, sessions), stalled]);
388
+ return await withStartTimeout(Target.#start(runtime, events, cmd, child, sessions), cmd, options.startTimeoutMs);
396
389
  }
397
390
  catch (error) {
398
391
  for (const session of sessions)
@@ -400,9 +393,6 @@ export class Target {
400
393
  child.kill();
401
394
  throw error;
402
395
  }
403
- finally {
404
- clearTimeout(timer);
405
- }
406
396
  }
407
397
  static async #start(runtime, events, cmd, child, sessions) {
408
398
  child.stdout.setEncoding("utf8");
@@ -470,6 +460,15 @@ export class Target {
470
460
  if (params.executionContextId === contextId && target)
471
461
  target.#onExit();
472
462
  });
463
+ // A session that closes on its own (e.g. a frame that isn't JSON, #337)
464
+ // leaves a target nothing can drive: stop it, so the next cell starts
465
+ // a fresh one.
466
+ for (const session of [shell, control]) {
467
+ session.onClose(() => {
468
+ if (target)
469
+ target.#onExit();
470
+ });
471
+ }
473
472
  // Wait for the bootstrap's top-level await to finish installing the
474
473
  // shim. There is no cap here: launch's start timeout bounds the wait and
475
474
  // kills the child, so a slow bootstrap never yields a target without
@@ -716,6 +715,19 @@ export class Target {
716
715
  arg,
717
716
  ]).catch(interrupted("Execution"));
718
717
  }
718
+ async format(value) {
719
+ const arg = value.objectId
720
+ ? { objectId: value.objectId }
721
+ : value.unserializableValue !== undefined
722
+ ? { unserializableValue: value.unserializableValue }
723
+ : { value: value.value };
724
+ const res = await this.#callAsyncFunctionOn(`function (v) { return globalThis.${SHIM}.formatJson(v); }`, [arg])
725
+ .catch(interrupted("Execution"));
726
+ const thrown = res.exceptionDetails;
727
+ if (thrown)
728
+ throw new Error(thrown.exception?.description ?? thrown.text ?? "the value could not be formatted");
729
+ return parseBundle(res.result?.value);
730
+ }
719
731
  /**
720
732
  * Hands a frontend comm message to the target's comms. Resolves to
721
733
  * whether a comm or registered target took it, or to the error a handler
@@ -18,6 +18,16 @@ export declare function workerdConfig({ socket, compatibilityDate }: {
18
18
  * otherwise use the one on PATH.
19
19
  */
20
20
  export declare function findWorkerd(cwd: string): string;
21
+ /**
22
+ * The native workerd binary behind `executable`. npm's workerd package
23
+ * installs bin/workerd as a Node script that runs the platform package's
24
+ * binary with execFileSync, and only its postinstall swaps the binary in.
25
+ * When install scripts don't run (the npm that comes with Node 27 blocks
26
+ * those not in allowScripts), killing that script leaves the binary running
27
+ * with the driver socket open, so it's never what the target spawns (#397).
28
+ * Anything that isn't the workerd package's script is returned as given.
29
+ */
30
+ export declare function nativeWorkerd(executable: string): string;
21
31
  /** "workerd 2026-10-15" -> "2026-10-15": the newest date it supports. */
22
32
  export declare function compatibilityDateOf(versionOutput: string): string;
23
33
  /** The literal specifiers a transformed cell imports, in order. */
@@ -89,12 +99,16 @@ export declare class WorkerdTarget implements KernelTarget {
89
99
  static launch(events: TargetEvents, options?: {
90
100
  cwd?: string;
91
101
  executable?: string;
102
+ startTimeoutMs?: number;
92
103
  }): Promise<WorkerdTarget>;
93
104
  /**
94
105
  * A target over a started host's session. `notice` (a project that
95
106
  * failed to load) is replaced by the bindings when the host has a project.
96
107
  */
97
- static open(host: WorkerHost, cwd: string, events: TargetEvents, runtime: WorkersRuntime, notice?: string): Promise<WorkerdTarget>;
108
+ static open(host: WorkerHost, cwd: string, events: TargetEvents, runtime: WorkersRuntime, options?: {
109
+ notice?: string;
110
+ startTimeoutMs?: number;
111
+ }): Promise<WorkerdTarget>;
98
112
  evaluate(script: string): Promise<EvaluateResult>;
99
113
  /**
100
114
  * A cell that's waiting on something leaves the driver free: it answers a
@@ -115,6 +129,7 @@ export declare class WorkerdTarget implements KernelTarget {
115
129
  stack: string;
116
130
  };
117
131
  }>;
132
+ format(value: RemoteObject): Promise<CdpParams>;
118
133
  describeError(exception: RemoteObject): Promise<{
119
134
  name: string;
120
135
  message: string;