@tlockney/cdp-kernel 0.1.1 → 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.
Files changed (44) hide show
  1. package/README.md +274 -21
  2. package/dist/bun-target.d.ts +58 -0
  3. package/dist/bun-target.js +231 -0
  4. package/dist/celld-target.d.ts +39 -0
  5. package/dist/celld-target.js +274 -0
  6. package/dist/deno-config.d.ts +20 -0
  7. package/dist/deno-config.js +130 -0
  8. package/dist/globals-deno-compat.d.ts +21 -0
  9. package/dist/globals-deno-compat.js +10 -0
  10. package/dist/globals.d.ts +36 -0
  11. package/dist/globals.js +15 -0
  12. package/dist/kernel.d.ts +2 -1
  13. package/dist/kernel.js +126 -11
  14. package/dist/main.js +5 -5
  15. package/dist/package.d.ts +13 -5
  16. package/dist/package.js +33 -8
  17. package/dist/target.d.ts +78 -7
  18. package/dist/target.js +67 -12
  19. package/dist/transform.d.ts +25 -1
  20. package/dist/transform.js +150 -11
  21. package/dist/workerd-target.d.ts +123 -0
  22. package/dist/workerd-target.js +450 -0
  23. package/dist/workers-project.d.ts +27 -0
  24. package/dist/workers-project.js +195 -0
  25. package/dist/ws-client.d.ts +32 -0
  26. package/dist/ws-client.js +166 -0
  27. package/package.json +27 -4
  28. package/runtime/bootstrap.mjs +78 -7
  29. package/runtime/bun-channel.d.mts +3 -0
  30. package/runtime/bun-channel.mjs +58 -0
  31. package/runtime/comm.d.mts +46 -0
  32. package/runtime/comm.mjs +108 -0
  33. package/runtime/deno-compat.mjs +1 -0
  34. package/runtime/display.d.mts +11 -0
  35. package/runtime/display.mjs +11 -1
  36. package/runtime/evaluator.d.mts +18 -0
  37. package/runtime/evaluator.mjs +83 -0
  38. package/runtime/node-hooks.d.mts +11 -0
  39. package/runtime/node-hooks.mjs +24 -12
  40. package/runtime/node-modules-resolve.d.mts +8 -0
  41. package/runtime/node-modules-resolve.mjs +106 -0
  42. package/runtime/widget.d.mts +40 -0
  43. package/runtime/widget.mjs +208 -0
  44. package/runtime/workerd-worker.mjs +189 -0
package/dist/kernel.js CHANGED
@@ -10,8 +10,13 @@ import { randomUUID } from "node:crypto";
10
10
  import { decodeMessage, encodeMessage, PROTOCOL_VERSION, verify } from "./wire.js";
11
11
  import { PubSocket, RouterSocket, startHeartbeat } from "./zmtp.js";
12
12
  import { Interrupted, Target } from "./target.js";
13
- import { CellSyntaxError, objectLiteralCandidate, transformCell } from "./transform.js";
13
+ import { BunTarget } from "./bun-target.js";
14
+ import { WorkerdTarget } from "./workerd-target.js";
15
+ import { launchCelld } from "./celld-target.js";
16
+ import { CellSyntaxError, objectLiteralCandidate } from "./transform.js";
17
+ import { loadDenoProject } from "./deno-config.js";
14
18
  import { checkIsComplete, codePointToUtf16Index, exprAtCursor, utf16ToCodePointIndex } from "./text.js";
19
+ import { VERSION } from "./package.js";
15
20
  export function parseConnectionInfo(json) {
16
21
  const raw = JSON.parse(json);
17
22
  if (typeof raw !== "object" || raw === null)
@@ -43,6 +48,10 @@ export class Kernel {
43
48
  #conn;
44
49
  #runtime;
45
50
  #denoCompat;
51
+ // A Deno target in a Deno project uses its config (#13).
52
+ #project;
53
+ #projectWarning = null;
54
+ #noticeShownFor = null;
46
55
  #session = randomUUID();
47
56
  #shell;
48
57
  #control;
@@ -54,11 +63,14 @@ export class Kernel {
54
63
  #parent = {};
55
64
  #allowStdin = false;
56
65
  #executing = false;
66
+ // Open comms, for comm_info_request: comm_id -> target_name (#9).
67
+ #comms = new Map();
57
68
  #shuttingDown = false;
58
69
  constructor(conn, runtime, options = {}) {
59
70
  this.#conn = conn;
60
71
  this.#runtime = runtime;
61
72
  this.#denoCompat = options.denoCompat ?? false;
73
+ this.#project = runtime === "deno" ? this.#loadProject(options.notebookDir ?? process.cwd()) : null;
62
74
  startHeartbeat(conn.hb_port, conn.ip);
63
75
  this.#shell = new RouterSocket(conn.shell_port, conn.ip);
64
76
  this.#control = new RouterSocket(conn.control_port, conn.ip);
@@ -75,11 +87,101 @@ export class Kernel {
75
87
  #log(text) {
76
88
  process.stderr.write(`[cdp-kernel] ${text}\n`);
77
89
  }
90
+ #loadProject(notebookDir) {
91
+ try {
92
+ return loadDenoProject(notebookDir);
93
+ }
94
+ catch (err) {
95
+ // A broken deno.json mustn't stop the kernel: run as in a plain
96
+ // directory and say why in the first cell.
97
+ const reason = err instanceof Error ? err.message : String(err);
98
+ this.#projectWarning = `${reason}; running without the project config`;
99
+ this.#log(this.#projectWarning);
100
+ return null;
101
+ }
102
+ }
103
+ #transformOptions() {
104
+ return this.#project?.transform ?? {};
105
+ }
106
+ // --- comms (#9) ------------------------------------------------------------
107
+ #trackComm(msgType, content) {
108
+ const commId = typeof content.comm_id === "string" ? content.comm_id : null;
109
+ if (commId === null)
110
+ return;
111
+ if (msgType === "comm_open" && typeof content.target_name === "string")
112
+ this.#comms.set(commId, content.target_name);
113
+ else if (msgType === "comm_close")
114
+ this.#comms.delete(commId);
115
+ }
116
+ #commInfo(content) {
117
+ const filter = typeof content.target_name === "string" ? content.target_name : null;
118
+ const comms = {};
119
+ for (const [commId, targetName] of this.#comms) {
120
+ if (filter === null || filter === targetName)
121
+ comms[commId] = { target_name: targetName };
122
+ }
123
+ return comms;
124
+ }
125
+ /** A frontend comm message. The busy/idle status around it is the caller's. */
126
+ async #handleComm(msg) {
127
+ const type = msg.header.msg_type;
128
+ const commId = String(msg.content.comm_id ?? "");
129
+ const target = await this.#ensureTarget();
130
+ // Kernel messages the handlers send are replies to this message.
131
+ this.#parent = msg.header;
132
+ this.#allowStdin = false;
133
+ this.#executing = true;
134
+ let handled = false;
135
+ try {
136
+ const result = await target.dispatchComm(type, msg.content, msg.metadata, msg.buffers);
137
+ if ("error" in result) {
138
+ const { name, message, stack } = result.error;
139
+ this.#publish("stream", { name: "stderr", text: `Error in ${type} handler: ${stack || `${name}: ${message}`}\n` });
140
+ }
141
+ else {
142
+ handled = result.handled;
143
+ }
144
+ }
145
+ catch (err) {
146
+ if (!(err instanceof Interrupted))
147
+ throw err;
148
+ this.#publish("stream", { name: "stderr", text: `${type} handler interrupted\n` });
149
+ }
150
+ finally {
151
+ this.#executing = false;
152
+ }
153
+ await target.flush().catch(() => { });
154
+ if (type === "comm_open") {
155
+ if (handled) {
156
+ this.#comms.set(commId, String(msg.content.target_name ?? ""));
157
+ }
158
+ else {
159
+ // An unknown target, or a handler that failed: the protocol asks the
160
+ // kernel to close the comm so the frontend doesn't wait on it.
161
+ this.#publish("comm_close", { comm_id: commId, data: {} });
162
+ }
163
+ }
164
+ else if (type === "comm_close") {
165
+ this.#comms.delete(commId);
166
+ }
167
+ }
78
168
  // --- target lifecycle -----------------------------------------------------
169
+ #launchTarget() {
170
+ if (this.#runtime === "bun")
171
+ return BunTarget.launch(this.#targetEvents());
172
+ if (this.#runtime === "workerd")
173
+ return WorkerdTarget.launch(this.#targetEvents());
174
+ if (this.#runtime === "celld")
175
+ return launchCelld(this.#targetEvents());
176
+ return Target.launch(this.#runtime, this.#targetEvents(), {
177
+ denoCompat: this.#denoCompat,
178
+ denoConfig: this.#project?.configPath,
179
+ });
180
+ }
79
181
  #ensureTarget() {
80
182
  if (this.#target)
81
183
  return Promise.resolve(this.#target);
82
- this.#targetStarting ??= Target.launch(this.#runtime, this.#targetEvents(), { denoCompat: this.#denoCompat }).then((target) => {
184
+ this.#targetStarting ??= this.#launchTarget().then((target) => {
83
185
  this.#target = target;
84
186
  this.#targetStarting = null;
85
187
  return target;
@@ -93,16 +195,18 @@ export class Kernel {
93
195
  return {
94
196
  stream: (name, text) => this.#publish("stream", { name, text }),
95
197
  message: (msg) => void this.#onTargetMessage(msg),
96
- exit: (code) => {
198
+ exit: (code, note) => {
97
199
  this.#target = null;
98
200
  if (!this.#shuttingDown) {
99
- this.#publish("stream", { name: "stderr", text: `\n[cdp-kernel] ${this.#runtime} exited (${code}); restarting on next cell\n` });
201
+ const text = note ?? `[cdp-kernel] ${this.#runtime} exited (${code}); restarting on next cell`;
202
+ this.#publish("stream", { name: "stderr", text: `\n${text}\n` });
100
203
  }
101
204
  },
102
205
  };
103
206
  }
104
207
  async #onTargetMessage(msg) {
105
208
  if (msg.kind === "iopub") {
209
+ this.#trackComm(msg.msg_type, msg.content);
106
210
  this.#publish(msg.msg_type, msg.content, {
107
211
  metadata: msg.metadata,
108
212
  buffers: msg.buffers.map((b) => new Uint8Array(Buffer.from(b, "base64"))),
@@ -156,7 +260,7 @@ export class Kernel {
156
260
  status: "ok",
157
261
  protocol_version: PROTOCOL_VERSION,
158
262
  implementation: "cdp-kernel",
159
- implementation_version: "0.1.0",
263
+ implementation_version: VERSION,
160
264
  language_info: {
161
265
  name: "typescript",
162
266
  version: "5",
@@ -166,7 +270,7 @@ export class Kernel {
166
270
  codemirror_mode: "typescript",
167
271
  nbconvert_exporter: "script",
168
272
  },
169
- banner: `cdp-kernel on ${this.#runtime} ${versions?.runtime ?? ""} (V8 ${versions?.v8 ?? "?"})`,
273
+ banner: `cdp-kernel on ${this.#runtime} ${versions?.runtime ?? ""} (${versions?.engine || "?"})`,
170
274
  help_links: [],
171
275
  debugger: false,
172
276
  };
@@ -218,11 +322,12 @@ export class Kernel {
218
322
  this.#reply(this.#shell, peer, msg, "history_reply", { status: "ok", history: [] });
219
323
  break;
220
324
  case "comm_info_request":
221
- this.#reply(this.#shell, peer, msg, "comm_info_reply", { status: "ok", comms: {} });
325
+ this.#reply(this.#shell, peer, msg, "comm_info_reply", { status: "ok", comms: this.#commInfo(msg.content) });
222
326
  break;
223
327
  case "comm_open":
224
- // No comm targets (widgets) yet: close it straight away, like Deno.
225
- this.#publish("comm_close", { comm_id: msg.content.comm_id ?? "", data: {} }, { parent: msg.header });
328
+ case "comm_msg":
329
+ case "comm_close":
330
+ await this.#handleComm(msg);
226
331
  break;
227
332
  }
228
333
  }
@@ -285,7 +390,7 @@ export class Kernel {
285
390
  const wrapped = objectLiteralCandidate(code);
286
391
  if (wrapped !== null) {
287
392
  try {
288
- const result = await target.evaluate(transformCell(wrapped));
393
+ const result = await target.evaluate(target.transform(wrapped, this.#transformOptions()));
289
394
  if (!result.exceptionDetails)
290
395
  return result;
291
396
  }
@@ -294,7 +399,7 @@ export class Kernel {
294
399
  throw err;
295
400
  }
296
401
  }
297
- return target.evaluate(transformCell(code));
402
+ return target.evaluate(target.transform(code, this.#transformOptions()));
298
403
  }
299
404
  async #execute(msg, peer) {
300
405
  const parent = msg.header;
@@ -307,10 +412,20 @@ export class Kernel {
307
412
  this.#allowStdin = msg.content.allow_stdin === true;
308
413
  this.#status("busy", parent);
309
414
  this.#publish("execute_input", { code, execution_count: count });
415
+ if (this.#projectWarning !== null) {
416
+ // Shown once, in the first cell, where the user will see it.
417
+ this.#publish("stream", { name: "stderr", text: `[cdp-kernel] ${this.#projectWarning}\n` });
418
+ this.#projectWarning = null;
419
+ }
310
420
  let error = null;
311
421
  this.#executing = true;
312
422
  try {
313
423
  const target = await this.#ensureTarget();
424
+ if (target.notice !== undefined && this.#noticeShownFor !== target) {
425
+ // Once per target, in the first cell it runs.
426
+ this.#noticeShownFor = target;
427
+ this.#publish("stream", { name: "stderr", text: `${target.notice}\n` });
428
+ }
314
429
  const result = await this.#evaluateCell(target, code);
315
430
  if (result.exceptionDetails) {
316
431
  const exception = result.exceptionDetails.exception;
package/dist/main.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // cdp-kernel CLI.
3
- // cdp-kernel install [--runtime node|deno] [--deno-compat] [--name NAME] [--display NAME]
4
- // cdp-kernel kernel --runtime node|deno [--deno-compat] --conn CONNECTION_FILE
3
+ // cdp-kernel install [--runtime node|deno|bun|workerd|celld] [--deno-compat] [--name NAME] [--display NAME]
4
+ // cdp-kernel kernel --runtime node|deno|bun|workerd|celld [--deno-compat] --conn CONNECTION_FILE
5
5
  // cdp-kernel version
6
6
  //
7
7
  // --deno-compat (Node only) adds a `Deno` namespace for notebooks written for
@@ -14,7 +14,7 @@ import { parseArgs } from "node:util";
14
14
  import { Kernel, parseConnectionInfo } from "./kernel.js";
15
15
  import { hostRuntime, kernelArgv, moduleOrigin, VERSION } from "./package.js";
16
16
  function isRuntime(value) {
17
- return value === "node" || value === "deno";
17
+ return value === "node" || value === "deno" || value === "bun" || value === "workerd" || value === "celld";
18
18
  }
19
19
  function kernelsDir() {
20
20
  if (process.env.JUPYTER_DATA_DIR)
@@ -59,7 +59,7 @@ const { positionals, values } = parseArgs({
59
59
  });
60
60
  const runtime = values.runtime;
61
61
  if (!isRuntime(runtime)) {
62
- console.error(`unknown runtime ${runtime}; expected node or deno`);
62
+ console.error(`unknown runtime ${runtime}; expected node, deno, bun, workerd or celld`);
63
63
  process.exit(2);
64
64
  }
65
65
  // Deno targets have the real namespace; the shim is for Node.
@@ -83,6 +83,6 @@ switch (positionals[0]) {
83
83
  console.log(VERSION);
84
84
  break;
85
85
  default:
86
- console.error("usage: cdp-kernel install [--runtime node|deno] [--deno-compat] | kernel --runtime node|deno --conn FILE | version");
86
+ console.error("usage: cdp-kernel install [--runtime node|deno|bun|workerd|celld] [--deno-compat] | kernel --runtime node|deno|bun|workerd|celld --conn FILE | version");
87
87
  process.exit(2);
88
88
  }
package/dist/package.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /** Kept equal to package.json and deno.jsonc by test/package.test.ts. */
2
- export declare const VERSION = "0.1.1";
3
- export declare const RUNTIME_FILES: readonly ["bootstrap.mjs", "display.mjs", "node-hooks.mjs", "deno-compat.mjs"];
2
+ export declare const VERSION = "0.3.0";
3
+ export declare const RUNTIME_FILES: readonly ["bootstrap.mjs", "bun-channel.mjs", "evaluator.mjs", "workerd-worker.mjs", "display.mjs", "comm.mjs", "widget.mjs", "node-hooks.mjs", "deno-compat.mjs", "node-modules-resolve.mjs"];
4
4
  export type HostRuntime = "node" | "deno";
5
5
  export declare function hostRuntime(): HostRuntime;
6
6
  /** Where the CLI module was loaded from. */
@@ -20,10 +20,18 @@ export declare function moduleOrigin(moduleUrl: string): Origin;
20
20
  export declare function kernelArgv(host: HostRuntime, execPath: string, origin: Origin, args: string[]): string[];
21
21
  export declare function cacheRoot(): string;
22
22
  export type FetchText = (url: string) => Promise<string>;
23
+ /** True when a file: URL points inside a node_modules directory (an npm install). */
24
+ export declare function isInsideNodeModules(url: string): boolean;
23
25
  /**
24
26
  * A local directory holding runtime/*.mjs. `baseUrl` is the runtime/
25
27
  * directory's URL relative to the calling module. A file: URL is used in
26
- * place; anything else is copied once into a cache keyed by version and
27
- * URL.
28
+ * place unless `copy` is set; anything else is copied once into a cache
29
+ * keyed by version and URL.
30
+ *
31
+ * `copy` exists for Deno targets of an npm install: Deno resolves imports
32
+ * made by a module under node_modules as npm-package imports, without the
33
+ * notebook project's import map, so the bootstrap must load from outside it.
28
34
  */
29
- export declare function runtimeDir(baseUrl: string, cache?: string, get?: FetchText): Promise<string>;
35
+ export declare function runtimeDir(baseUrl: string, cache?: string, get?: FetchText, { copy }?: {
36
+ copy?: boolean;
37
+ }): Promise<string>;
package/dist/package.js CHANGED
@@ -8,14 +8,25 @@
8
8
  // import its bootstrap from an https URL, so a JSR install copies those
9
9
  // files into a local cache first.
10
10
  import { createHash } from "node:crypto";
11
- import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
11
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
12
12
  import { homedir } from "node:os";
13
- import { join } from "node:path";
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.1.1";
18
- export const RUNTIME_FILES = ["bootstrap.mjs", "display.mjs", "node-hooks.mjs", "deno-compat.mjs"];
17
+ export const VERSION = "0.3.0";
18
+ export const RUNTIME_FILES = [
19
+ "bootstrap.mjs",
20
+ "bun-channel.mjs",
21
+ "evaluator.mjs",
22
+ "workerd-worker.mjs",
23
+ "display.mjs",
24
+ "comm.mjs",
25
+ "widget.mjs",
26
+ "node-hooks.mjs",
27
+ "deno-compat.mjs",
28
+ "node-modules-resolve.mjs",
29
+ ];
19
30
  export function hostRuntime() {
20
31
  return process.versions.deno === undefined ? "node" : "deno";
21
32
  }
@@ -57,15 +68,29 @@ async function fetchText(url) {
57
68
  throw new Error(`fetching ${url}: ${res.status} ${res.statusText}`);
58
69
  return await res.text();
59
70
  }
71
+ /** True when a file: URL points inside a node_modules directory (an npm install). */
72
+ export function isInsideNodeModules(url) {
73
+ return url.startsWith("file:") && fileURLToPath(url).split(sep).includes("node_modules");
74
+ }
75
+ function readLocal(url) {
76
+ return Promise.resolve(readFileSync(fileURLToPath(url), "utf8"));
77
+ }
60
78
  /**
61
79
  * A local directory holding runtime/*.mjs. `baseUrl` is the runtime/
62
80
  * directory's URL relative to the calling module. A file: URL is used in
63
- * place; anything else is copied once into a cache keyed by version and
64
- * URL.
81
+ * place unless `copy` is set; anything else is copied once into a cache
82
+ * keyed by version and URL.
83
+ *
84
+ * `copy` exists for Deno targets of an npm install: Deno resolves imports
85
+ * made by a module under node_modules as npm-package imports, without the
86
+ * notebook project's import map, so the bootstrap must load from outside it.
65
87
  */
66
- export async function runtimeDir(baseUrl, cache = cacheRoot(), get = fetchText) {
67
- if (baseUrl.startsWith("file:"))
88
+ export async function runtimeDir(baseUrl, cache = cacheRoot(), get = fetchText, { copy = false } = {}) {
89
+ const local = baseUrl.startsWith("file:");
90
+ if (local && !copy)
68
91
  return fileURLToPath(baseUrl);
92
+ if (local)
93
+ get = readLocal;
69
94
  const key = createHash("sha256").update(baseUrl).digest("hex").slice(0, 12);
70
95
  const dir = join(cache, "runtime", `${VERSION}-${key}`);
71
96
  if (RUNTIME_FILES.every((name) => existsSync(join(dir, name))))
package/dist/target.d.ts CHANGED
@@ -1,7 +1,53 @@
1
1
  import { type CdpParams, type EvaluateResult, type RemoteObject } from "./cdp.ts";
2
- export type RuntimeName = "node" | "deno";
2
+ import { type CellTransformOptions, transformCell } from "./transform.ts";
3
+ /** Targets driven over CDP. */
4
+ export type CdpRuntime = "node" | "deno";
5
+ /** Bun and workerd have their own backends: src/bun-target.ts, src/workerd-target.ts. */
6
+ export type RuntimeName = CdpRuntime | "bun" | "workerd" | "celld";
7
+ /** What the kernel needs from a target runtime. */
8
+ export interface KernelTarget {
9
+ readonly runtime: RuntimeName;
10
+ readonly versions: {
11
+ runtime: string;
12
+ engine: string;
13
+ };
14
+ /** Shown once, in the first cell: e.g. which project's bindings loaded. */
15
+ readonly notice?: string;
16
+ /** Turns a cell into the script evaluate() takes. */
17
+ transform(code: string, options: CellTransformOptions): string;
18
+ /** Runs a transformed cell. Rejects with Interrupted after interrupt(). */
19
+ evaluate(script: string): Promise<EvaluateResult>;
20
+ interrupt(): Promise<void>;
21
+ /** Resolves once all output and messages sent so far have been delivered. */
22
+ flush(): Promise<void>;
23
+ broadcastResult(executionCount: number, result: RemoteObject): Promise<void>;
24
+ dispatchComm(msgType: string, content: CdpParams, metadata: CdpParams, buffers: Uint8Array[]): Promise<{
25
+ handled: boolean;
26
+ } | {
27
+ error: {
28
+ name: string;
29
+ message: string;
30
+ stack: string;
31
+ };
32
+ }>;
33
+ describeError(exception: RemoteObject): Promise<{
34
+ name: string;
35
+ message: string;
36
+ stack: string;
37
+ }>;
38
+ propertyNames(expression: string): Promise<string[]>;
39
+ lexicalNames(): Promise<string[]>;
40
+ replyInput(value: string | null): void;
41
+ kill(): void;
42
+ }
43
+ export interface LaunchOptions {
44
+ /** Runtime binary; defaults to `deno`, or `node` (this one when hosted by Node). */
45
+ executable?: string;
46
+ /** A Deno target's project config (deno.json), passed as --config. */
47
+ denoConfig?: string;
48
+ }
3
49
  /** `bootstrap` is the local path of runtime/bootstrap.mjs (see runtimeDir). */
4
- export declare function launchCommand(runtime: RuntimeName, bootstrap: string, executable?: string): {
50
+ export declare function launchCommand(runtime: CdpRuntime, bootstrap: string, { executable, denoConfig }?: LaunchOptions): {
5
51
  cmd: string;
6
52
  args: string[];
7
53
  };
@@ -19,6 +65,7 @@ export type TargetMessage = {
19
65
  password: boolean;
20
66
  barrier: number;
21
67
  };
68
+ export declare function isTargetMessage(value: unknown): value is TargetMessage;
22
69
  /**
23
70
  * Splits one output pipe into text and barrier markers, and releases them
24
71
  * in order. A gated marker (one that announces a binding message) pauses
@@ -36,26 +83,36 @@ export declare class PipeDemux {
36
83
  /** Release everything (the process died). */
37
84
  close(): void;
38
85
  }
86
+ /** Promises keyed by barrier number, resolvable before or after first use. */
87
+ export declare class Gates {
88
+ #private;
89
+ wait(n: number): Promise<void>;
90
+ open(n: number): void;
91
+ openAll(): void;
92
+ }
39
93
  export interface TargetEvents {
40
94
  stream(name: "stdout" | "stderr", text: string): void;
41
95
  message(msg: TargetMessage): void;
42
- exit(code: number | null): void;
96
+ /** `note` says why, when the target was stopped on purpose. */
97
+ exit(code: number | null, note?: string): void;
43
98
  }
44
99
  export declare class Interrupted extends Error {
45
100
  name: string;
46
101
  }
47
- export declare class Target {
102
+ export declare class Target implements KernelTarget {
48
103
  #private;
49
- readonly runtime: RuntimeName;
104
+ readonly runtime: CdpRuntime;
105
+ readonly transform: typeof transformCell;
50
106
  readonly versions: {
51
107
  runtime: string;
52
- v8: string;
108
+ engine: string;
53
109
  };
54
110
  private constructor();
55
- static launch(runtime: RuntimeName, events: TargetEvents, options?: {
111
+ static launch(runtime: CdpRuntime, events: TargetEvents, options?: {
56
112
  cwd?: string;
57
113
  executable?: string;
58
114
  denoCompat?: boolean;
115
+ denoConfig?: string;
59
116
  }): Promise<Target>;
60
117
  /** Resolves once all output and messages sent so far have been delivered. */
61
118
  flush(): Promise<void>;
@@ -72,6 +129,20 @@ export declare class Target {
72
129
  callFunctionOn(functionDeclaration: string, args: CdpParams[]): Promise<CdpParams>;
73
130
  /** The broadcastResult step of Deno's execute flow (jupyter/mod.rs). */
74
131
  broadcastResult(executionCount: number, result: RemoteObject): Promise<void>;
132
+ /**
133
+ * Hands a frontend comm message to the target's comms. Resolves to
134
+ * whether a comm or registered target took it, or to the error a handler
135
+ * threw. Rejects with Interrupted after interrupt().
136
+ */
137
+ dispatchComm(msgType: string, content: CdpParams, metadata: CdpParams, buffers: Uint8Array[]): Promise<{
138
+ handled: boolean;
139
+ } | {
140
+ error: {
141
+ name: string;
142
+ message: string;
143
+ stack: string;
144
+ };
145
+ }>;
75
146
  describeError(exception: RemoteObject): Promise<{
76
147
  name: string;
77
148
  message: string;
package/dist/target.js CHANGED
@@ -1,12 +1,13 @@
1
1
  // A target runtime driven over CDP: the stand-in for Deno's REPL isolate and
2
2
  // the ops in cli/ops/jupyter.rs. Any runtime whose --inspect speaks CDP and
3
3
  // can load runtime/bootstrap.mjs works; Node and Deno are wired up here.
4
+ import { Buffer } from "node:buffer";
4
5
  import process from "node:process";
5
6
  import { spawn } from "node:child_process";
6
7
  import { join } from "node:path";
7
8
  import { asEvaluateResult, CdpAbandoned, CdpSession } from "./cdp.js";
8
- import { SHIM } from "./transform.js";
9
- import { hostRuntime, runtimeDir } from "./package.js";
9
+ import { SHIM, transformCell } from "./transform.js";
10
+ import { hostRuntime, isInsideNodeModules, runtimeDir } from "./package.js";
10
11
  const BINDING = "__cdpk_send";
11
12
  const MARKER_RE = /\x1b\[cdpk:(\d+)(m?)\]\x1b/;
12
13
  const MAX_MARKER_LEN = 24;
@@ -15,8 +16,12 @@ const MAX_MARKER_LEN = 24;
15
16
  // sends to the server log rather than the notebook).
16
17
  const INSPECTOR_CHATTER = /^(?:\x1b\[[0-9;]*m)*(Debugger listening on |Debugger attached\.|For help, see: |Visit chrome:\/\/inspect|Waiting for the debugger to disconnect|Download(?:\x1b\[[0-9;]*m)* https?:\/\/).*\n/gm;
17
18
  /** `bootstrap` is the local path of runtime/bootstrap.mjs (see runtimeDir). */
18
- export function launchCommand(runtime, bootstrap, executable) {
19
- const inspect = "--inspect=127.0.0.1:0";
19
+ export function launchCommand(runtime, bootstrap, { executable, denoConfig } = {}) {
20
+ // The CDP WebSocket has no authentication, so its URL must stay private:
21
+ // by default the inspector lists it at /json/list for any local process
22
+ // to read. Publishing it only on stderr, which only this host reads,
23
+ // leaves a random id nobody else can learn (#22).
24
+ const inspect = ["--inspect=127.0.0.1:0", "--inspect-publish-uid=stderr"];
20
25
  if (runtime === "deno") {
21
26
  // Deno discovers config from the main module's location, which would
22
27
  // put npm: imports in BYONM mode against this package's own
@@ -25,14 +30,18 @@ export function launchCommand(runtime, bootstrap, executable) {
25
30
  // into this package.
26
31
  return {
27
32
  cmd: executable ?? "deno",
28
- args: ["run", "--allow-all", "--node-modules-dir=none", "--no-lock", inspect, bootstrap],
33
+ // In a project, its config (import map, nodeModulesDir, ...) applies
34
+ // instead; --no-lock still keeps the kernel out of the project's lock.
35
+ args: denoConfig === undefined
36
+ ? ["run", "--allow-all", "--node-modules-dir=none", "--no-lock", ...inspect, bootstrap]
37
+ : ["run", "--allow-all", "--config", denoConfig, "--no-lock", ...inspect, bootstrap],
29
38
  };
30
39
  }
31
40
  // Under a Deno host, process.execPath is deno; find node on PATH instead.
32
41
  const node = hostRuntime() === "node" ? process.execPath : "node";
33
- return { cmd: executable ?? node, args: [inspect, bootstrap] };
42
+ return { cmd: executable ?? node, args: [...inspect, bootstrap] };
34
43
  }
35
- function isTargetMessage(value) {
44
+ export function isTargetMessage(value) {
36
45
  if (typeof value !== "object" || value === null)
37
46
  return false;
38
47
  const kind = value.kind;
@@ -131,7 +140,7 @@ export class PipeDemux {
131
140
  }
132
141
  }
133
142
  /** Promises keyed by barrier number, resolvable before or after first use. */
134
- class Gates {
143
+ export class Gates {
135
144
  #map = new Map();
136
145
  #entry(n) {
137
146
  let entry = this.#map.get(n);
@@ -160,6 +169,7 @@ export class Interrupted extends Error {
160
169
  }
161
170
  export class Target {
162
171
  runtime;
172
+ transform = transformCell;
163
173
  #child;
164
174
  #shell;
165
175
  #control;
@@ -182,11 +192,20 @@ export class Target {
182
192
  this.versions = versions;
183
193
  }
184
194
  static async launch(runtime, events, options = {}) {
185
- const bootstrap = join(await runtimeDir(new URL("../runtime/", import.meta.url).href), "bootstrap.mjs");
186
- const { cmd, args } = launchCommand(runtime, bootstrap, options.executable);
195
+ const runtimeUrl = new URL("../runtime/", import.meta.url).href;
196
+ // A Deno target ignores the project's import map for imports made from
197
+ // under node_modules, so an npm install's runtime files are copied out.
198
+ const copy = runtime === "deno" && isInsideNodeModules(runtimeUrl);
199
+ const bootstrap = join(await runtimeDir(runtimeUrl, undefined, undefined, { copy }), "bootstrap.mjs");
200
+ const { cmd, args } = launchCommand(runtime, bootstrap, {
201
+ executable: options.executable,
202
+ denoConfig: options.denoConfig,
203
+ });
187
204
  const env = { ...process.env, FORCE_COLOR: "1" };
188
205
  if (options.denoCompat)
189
206
  env.CDPK_DENO_COMPAT = "1";
207
+ if (runtime === "deno" && options.denoConfig !== undefined)
208
+ env.CDPK_DENO_PROJECT = "1";
190
209
  delete env.NO_COLOR;
191
210
  const child = spawn(cmd, args, { cwd: options.cwd, stdio: ["pipe", "pipe", "pipe"], env });
192
211
  child.stdout.setEncoding("utf8");
@@ -235,7 +254,7 @@ export class Target {
235
254
  await shell.send("Runtime.enable");
236
255
  await shell.send("Runtime.addBinding", { name: BINDING });
237
256
  const contextId = await contextReady;
238
- const target = new Target(runtime, child, shell, control, stdout, stderr, gates, contextId, { runtime: "", v8: "" });
257
+ const target = new Target(runtime, child, shell, control, stdout, stderr, gates, contextId, { runtime: "", engine: "" });
239
258
  shell.on("Runtime.bindingCalled", (params) => {
240
259
  if (params.name !== BINDING || typeof params.payload !== "string")
241
260
  return;
@@ -250,7 +269,7 @@ export class Target {
250
269
  break;
251
270
  await new Promise((r) => setTimeout(r, 25));
252
271
  }
253
- const versions = await target.#evaluateRaw(`JSON.stringify(typeof Deno !== "undefined" ? { runtime: Deno.version.deno, v8: Deno.version.v8 } : { runtime: process.versions.node, v8: process.versions.v8 })`);
272
+ const versions = await target.#evaluateRaw(`JSON.stringify(typeof Deno !== "undefined" ? { runtime: Deno.version.deno, engine: "V8 " + Deno.version.v8 } : { runtime: process.versions.node, engine: "V8 " + process.versions.v8 })`);
254
273
  Object.assign(target.versions, JSON.parse(String(versions.result.value)));
255
274
  return target;
256
275
  }
@@ -320,6 +339,42 @@ export class Target {
320
339
  arg,
321
340
  ]);
322
341
  }
342
+ /**
343
+ * Hands a frontend comm message to the target's comms. Resolves to
344
+ * whether a comm or registered target took it, or to the error a handler
345
+ * threw. Rejects with Interrupted after interrupt().
346
+ */
347
+ async dispatchComm(msgType, content, metadata, buffers) {
348
+ this.#interrupted = false;
349
+ let res;
350
+ try {
351
+ res = await this.#shell.send("Runtime.callFunctionOn", {
352
+ functionDeclaration: `function (t, c, m, b) { return globalThis.${SHIM}.commDispatch(t, c, m, b); }`,
353
+ arguments: [
354
+ { value: msgType },
355
+ { value: content },
356
+ { value: metadata },
357
+ { value: buffers.map((b) => Buffer.from(b).toString("base64")) },
358
+ ],
359
+ executionContextId: this.#contextId,
360
+ awaitPromise: true,
361
+ returnByValue: true,
362
+ });
363
+ }
364
+ catch (err) {
365
+ if (err instanceof CdpAbandoned || this.#interrupted)
366
+ throw new Interrupted("Comm handler interrupted");
367
+ throw err;
368
+ }
369
+ const details = res.exceptionDetails;
370
+ if (details) {
371
+ const error = details.exception
372
+ ? await this.describeError(details.exception)
373
+ : { name: "Error", message: details.text, stack: "" };
374
+ return { error };
375
+ }
376
+ return { handled: res.result?.value === true };
377
+ }
323
378
  async describeError(exception) {
324
379
  const arg = exception.objectId ? { objectId: exception.objectId } : { value: exception.value };
325
380
  const res = await this.callFunctionOn(`function (e) { return globalThis.${SHIM}.describeError(e); }`, [arg]);