@tlockney/cdp-kernel 0.1.0 → 0.2.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.
- package/README.md +118 -8
- package/dist/deno-config.d.ts +20 -0
- package/dist/deno-config.js +130 -0
- package/dist/globals-deno-compat.d.ts +21 -0
- package/dist/globals-deno-compat.js +10 -0
- package/dist/globals.d.ts +36 -0
- package/dist/globals.js +15 -0
- package/dist/kernel.d.ts +2 -1
- package/dist/kernel.js +103 -7
- package/dist/package.d.ts +13 -5
- package/dist/package.js +30 -8
- package/dist/target.d.ts +22 -1
- package/dist/target.js +61 -7
- package/dist/transform.d.ts +19 -1
- package/dist/transform.js +15 -5
- package/package.json +11 -2
- package/runtime/bootstrap.mjs +40 -4
- package/runtime/comm.d.mts +46 -0
- package/runtime/comm.mjs +108 -0
- package/runtime/deno-compat.mjs +1 -0
- package/runtime/display.d.mts +11 -0
- package/runtime/display.mjs +11 -1
- package/runtime/node-modules-resolve.d.mts +8 -0
- package/runtime/node-modules-resolve.mjs +106 -0
- package/runtime/widget.d.mts +40 -0
- package/runtime/widget.mjs +208 -0
package/dist/package.js
CHANGED
|
@@ -8,14 +8,22 @@
|
|
|
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.
|
|
18
|
-
export const RUNTIME_FILES = [
|
|
17
|
+
export const VERSION = "0.2.0";
|
|
18
|
+
export const RUNTIME_FILES = [
|
|
19
|
+
"bootstrap.mjs",
|
|
20
|
+
"display.mjs",
|
|
21
|
+
"comm.mjs",
|
|
22
|
+
"widget.mjs",
|
|
23
|
+
"node-hooks.mjs",
|
|
24
|
+
"deno-compat.mjs",
|
|
25
|
+
"node-modules-resolve.mjs",
|
|
26
|
+
];
|
|
19
27
|
export function hostRuntime() {
|
|
20
28
|
return process.versions.deno === undefined ? "node" : "deno";
|
|
21
29
|
}
|
|
@@ -57,15 +65,29 @@ async function fetchText(url) {
|
|
|
57
65
|
throw new Error(`fetching ${url}: ${res.status} ${res.statusText}`);
|
|
58
66
|
return await res.text();
|
|
59
67
|
}
|
|
68
|
+
/** True when a file: URL points inside a node_modules directory (an npm install). */
|
|
69
|
+
export function isInsideNodeModules(url) {
|
|
70
|
+
return url.startsWith("file:") && fileURLToPath(url).split(sep).includes("node_modules");
|
|
71
|
+
}
|
|
72
|
+
function readLocal(url) {
|
|
73
|
+
return Promise.resolve(readFileSync(fileURLToPath(url), "utf8"));
|
|
74
|
+
}
|
|
60
75
|
/**
|
|
61
76
|
* A local directory holding runtime/*.mjs. `baseUrl` is the runtime/
|
|
62
77
|
* directory's URL relative to the calling module. A file: URL is used in
|
|
63
|
-
* place; anything else is copied once into a cache
|
|
64
|
-
* URL.
|
|
78
|
+
* place unless `copy` is set; anything else is copied once into a cache
|
|
79
|
+
* keyed by version and URL.
|
|
80
|
+
*
|
|
81
|
+
* `copy` exists for Deno targets of an npm install: Deno resolves imports
|
|
82
|
+
* made by a module under node_modules as npm-package imports, without the
|
|
83
|
+
* notebook project's import map, so the bootstrap must load from outside it.
|
|
65
84
|
*/
|
|
66
|
-
export async function runtimeDir(baseUrl, cache = cacheRoot(), get = fetchText) {
|
|
67
|
-
|
|
85
|
+
export async function runtimeDir(baseUrl, cache = cacheRoot(), get = fetchText, { copy = false } = {}) {
|
|
86
|
+
const local = baseUrl.startsWith("file:");
|
|
87
|
+
if (local && !copy)
|
|
68
88
|
return fileURLToPath(baseUrl);
|
|
89
|
+
if (local)
|
|
90
|
+
get = readLocal;
|
|
69
91
|
const key = createHash("sha256").update(baseUrl).digest("hex").slice(0, 12);
|
|
70
92
|
const dir = join(cache, "runtime", `${VERSION}-${key}`);
|
|
71
93
|
if (RUNTIME_FILES.every((name) => existsSync(join(dir, name))))
|
package/dist/target.d.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { type CdpParams, type EvaluateResult, type RemoteObject } from "./cdp.ts";
|
|
2
2
|
export type RuntimeName = "node" | "deno";
|
|
3
|
+
export interface LaunchOptions {
|
|
4
|
+
/** Runtime binary; defaults to `deno`, or `node` (this one when hosted by Node). */
|
|
5
|
+
executable?: string;
|
|
6
|
+
/** A Deno target's project config (deno.json), passed as --config. */
|
|
7
|
+
denoConfig?: string;
|
|
8
|
+
}
|
|
3
9
|
/** `bootstrap` is the local path of runtime/bootstrap.mjs (see runtimeDir). */
|
|
4
|
-
export declare function launchCommand(runtime: RuntimeName, bootstrap: string, executable?:
|
|
10
|
+
export declare function launchCommand(runtime: RuntimeName, bootstrap: string, { executable, denoConfig }?: LaunchOptions): {
|
|
5
11
|
cmd: string;
|
|
6
12
|
args: string[];
|
|
7
13
|
};
|
|
@@ -56,6 +62,7 @@ export declare class Target {
|
|
|
56
62
|
cwd?: string;
|
|
57
63
|
executable?: string;
|
|
58
64
|
denoCompat?: boolean;
|
|
65
|
+
denoConfig?: string;
|
|
59
66
|
}): Promise<Target>;
|
|
60
67
|
/** Resolves once all output and messages sent so far have been delivered. */
|
|
61
68
|
flush(): Promise<void>;
|
|
@@ -72,6 +79,20 @@ export declare class Target {
|
|
|
72
79
|
callFunctionOn(functionDeclaration: string, args: CdpParams[]): Promise<CdpParams>;
|
|
73
80
|
/** The broadcastResult step of Deno's execute flow (jupyter/mod.rs). */
|
|
74
81
|
broadcastResult(executionCount: number, result: RemoteObject): Promise<void>;
|
|
82
|
+
/**
|
|
83
|
+
* Hands a frontend comm message to the target's comms. Resolves to
|
|
84
|
+
* whether a comm or registered target took it, or to the error a handler
|
|
85
|
+
* threw. Rejects with Interrupted after interrupt().
|
|
86
|
+
*/
|
|
87
|
+
dispatchComm(msgType: string, content: CdpParams, metadata: CdpParams, buffers: Uint8Array[]): Promise<{
|
|
88
|
+
handled: boolean;
|
|
89
|
+
} | {
|
|
90
|
+
error: {
|
|
91
|
+
name: string;
|
|
92
|
+
message: string;
|
|
93
|
+
stack: string;
|
|
94
|
+
};
|
|
95
|
+
}>;
|
|
75
96
|
describeError(exception: RemoteObject): Promise<{
|
|
76
97
|
name: string;
|
|
77
98
|
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
9
|
import { SHIM } from "./transform.js";
|
|
9
|
-
import { hostRuntime, runtimeDir } from "./package.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
|
-
|
|
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,12 +30,16 @@ export function launchCommand(runtime, bootstrap, executable) {
|
|
|
25
30
|
// into this package.
|
|
26
31
|
return {
|
|
27
32
|
cmd: executable ?? "deno",
|
|
28
|
-
|
|
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
44
|
function isTargetMessage(value) {
|
|
36
45
|
if (typeof value !== "object" || value === null)
|
|
@@ -182,11 +191,20 @@ export class Target {
|
|
|
182
191
|
this.versions = versions;
|
|
183
192
|
}
|
|
184
193
|
static async launch(runtime, events, options = {}) {
|
|
185
|
-
const
|
|
186
|
-
|
|
194
|
+
const runtimeUrl = new URL("../runtime/", import.meta.url).href;
|
|
195
|
+
// A Deno target ignores the project's import map for imports made from
|
|
196
|
+
// under node_modules, so an npm install's runtime files are copied out.
|
|
197
|
+
const copy = runtime === "deno" && isInsideNodeModules(runtimeUrl);
|
|
198
|
+
const bootstrap = join(await runtimeDir(runtimeUrl, undefined, undefined, { copy }), "bootstrap.mjs");
|
|
199
|
+
const { cmd, args } = launchCommand(runtime, bootstrap, {
|
|
200
|
+
executable: options.executable,
|
|
201
|
+
denoConfig: options.denoConfig,
|
|
202
|
+
});
|
|
187
203
|
const env = { ...process.env, FORCE_COLOR: "1" };
|
|
188
204
|
if (options.denoCompat)
|
|
189
205
|
env.CDPK_DENO_COMPAT = "1";
|
|
206
|
+
if (runtime === "deno" && options.denoConfig !== undefined)
|
|
207
|
+
env.CDPK_DENO_PROJECT = "1";
|
|
190
208
|
delete env.NO_COLOR;
|
|
191
209
|
const child = spawn(cmd, args, { cwd: options.cwd, stdio: ["pipe", "pipe", "pipe"], env });
|
|
192
210
|
child.stdout.setEncoding("utf8");
|
|
@@ -320,6 +338,42 @@ export class Target {
|
|
|
320
338
|
arg,
|
|
321
339
|
]);
|
|
322
340
|
}
|
|
341
|
+
/**
|
|
342
|
+
* Hands a frontend comm message to the target's comms. Resolves to
|
|
343
|
+
* whether a comm or registered target took it, or to the error a handler
|
|
344
|
+
* threw. Rejects with Interrupted after interrupt().
|
|
345
|
+
*/
|
|
346
|
+
async dispatchComm(msgType, content, metadata, buffers) {
|
|
347
|
+
this.#interrupted = false;
|
|
348
|
+
let res;
|
|
349
|
+
try {
|
|
350
|
+
res = await this.#shell.send("Runtime.callFunctionOn", {
|
|
351
|
+
functionDeclaration: `function (t, c, m, b) { return globalThis.${SHIM}.commDispatch(t, c, m, b); }`,
|
|
352
|
+
arguments: [
|
|
353
|
+
{ value: msgType },
|
|
354
|
+
{ value: content },
|
|
355
|
+
{ value: metadata },
|
|
356
|
+
{ value: buffers.map((b) => Buffer.from(b).toString("base64")) },
|
|
357
|
+
],
|
|
358
|
+
executionContextId: this.#contextId,
|
|
359
|
+
awaitPromise: true,
|
|
360
|
+
returnByValue: true,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
catch (err) {
|
|
364
|
+
if (err instanceof CdpAbandoned || this.#interrupted)
|
|
365
|
+
throw new Interrupted("Comm handler interrupted");
|
|
366
|
+
throw err;
|
|
367
|
+
}
|
|
368
|
+
const details = res.exceptionDetails;
|
|
369
|
+
if (details) {
|
|
370
|
+
const error = details.exception
|
|
371
|
+
? await this.describeError(details.exception)
|
|
372
|
+
: { name: "Error", message: details.text, stack: "" };
|
|
373
|
+
return { error };
|
|
374
|
+
}
|
|
375
|
+
return { handled: res.result?.value === true };
|
|
376
|
+
}
|
|
323
377
|
async describeError(exception) {
|
|
324
378
|
const arg = exception.objectId ? { objectId: exception.objectId } : { value: exception.value };
|
|
325
379
|
const res = await this.callFunctionOn(`function (e) { return globalThis.${SHIM}.describeError(e); }`, [arg]);
|
package/dist/transform.d.ts
CHANGED
|
@@ -2,9 +2,27 @@ export declare const SHIM = "__cdpk";
|
|
|
2
2
|
export declare class CellSyntaxError extends Error {
|
|
3
3
|
name: string;
|
|
4
4
|
}
|
|
5
|
+
export type JsxOptions = {
|
|
6
|
+
mode: "automatic";
|
|
7
|
+
importSource?: string;
|
|
8
|
+
development: boolean;
|
|
9
|
+
} | {
|
|
10
|
+
mode: "classic";
|
|
11
|
+
factory?: string;
|
|
12
|
+
fragment?: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Compiler options for cells, e.g. from a Deno project's compilerOptions
|
|
16
|
+
* (see src/deno-config.ts). Without any, JSX compiles to classic
|
|
17
|
+
* React.createElement calls, Deno's own default.
|
|
18
|
+
*/
|
|
19
|
+
export interface CellTransformOptions {
|
|
20
|
+
jsx?: JsxOptions;
|
|
21
|
+
experimentalDecorators?: boolean;
|
|
22
|
+
}
|
|
5
23
|
/** Rewrites module-only syntax in plain JavaScript into script syntax. */
|
|
6
24
|
export declare function rewriteModuleSyntax(js: string): string;
|
|
7
|
-
export declare function transformCell(code: string): string;
|
|
25
|
+
export declare function transformCell(code: string, options?: CellTransformOptions): string;
|
|
8
26
|
/**
|
|
9
27
|
* Deno treats a cell like `{ "a": 1 }` as an object literal rather than a
|
|
10
28
|
* block, as browser consoles do. Returns the wrapped form to try first, or
|
package/dist/transform.js
CHANGED
|
@@ -27,7 +27,17 @@ const PRELUDE = `'use strict'; await ${SHIM}.tick();\n`;
|
|
|
27
27
|
export class CellSyntaxError extends Error {
|
|
28
28
|
name = "SyntaxError";
|
|
29
29
|
}
|
|
30
|
-
function
|
|
30
|
+
function jsxSettings(jsx) {
|
|
31
|
+
if (jsx?.mode === "automatic") {
|
|
32
|
+
return { jsx: "automatic", jsxImportSource: jsx.importSource, jsxDev: jsx.development };
|
|
33
|
+
}
|
|
34
|
+
return { jsx: "transform", jsxFactory: jsx?.factory, jsxFragment: jsx?.fragment };
|
|
35
|
+
}
|
|
36
|
+
function stripTypes(code, options) {
|
|
37
|
+
const compilerOptions = {
|
|
38
|
+
verbatimModuleSyntax: true,
|
|
39
|
+
...(options.experimentalDecorators ? { experimentalDecorators: true } : {}),
|
|
40
|
+
};
|
|
31
41
|
// Like Deno: prefer TSX, fall back to TS for `<T>x` assertions.
|
|
32
42
|
let firstError;
|
|
33
43
|
for (const loader of ["tsx", "ts"]) {
|
|
@@ -35,8 +45,8 @@ function stripTypes(code) {
|
|
|
35
45
|
return transformSync(code, {
|
|
36
46
|
loader,
|
|
37
47
|
target: "esnext",
|
|
38
|
-
jsx
|
|
39
|
-
tsconfigRaw: { compilerOptions
|
|
48
|
+
...jsxSettings(options.jsx),
|
|
49
|
+
tsconfigRaw: { compilerOptions },
|
|
40
50
|
logLevel: "silent",
|
|
41
51
|
}).code;
|
|
42
52
|
}
|
|
@@ -132,8 +142,8 @@ export function rewriteModuleSyntax(js) {
|
|
|
132
142
|
out = out.slice(0, edit.start) + edit.text + out.slice(edit.end);
|
|
133
143
|
return out;
|
|
134
144
|
}
|
|
135
|
-
export function transformCell(code) {
|
|
136
|
-
return PRELUDE + rewriteModuleSyntax(stripTypes(code));
|
|
145
|
+
export function transformCell(code, options = {}) {
|
|
146
|
+
return PRELUDE + rewriteModuleSyntax(stripTypes(code, options));
|
|
137
147
|
}
|
|
138
148
|
/**
|
|
139
149
|
* Deno treats a cell like `{ "a": 1 }` as an object literal rather than a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tlockney/cdp-kernel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Deno's Jupyter kernel, extracted to run notebooks on any V8 runtime over the Chrome DevTools Protocol",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Thomas Lockney <thomas@lockney.net>",
|
|
@@ -20,6 +20,14 @@
|
|
|
20
20
|
".": {
|
|
21
21
|
"types": "./dist/mod.d.ts",
|
|
22
22
|
"default": "./dist/mod.js"
|
|
23
|
+
},
|
|
24
|
+
"./globals": {
|
|
25
|
+
"types": "./dist/globals.d.ts",
|
|
26
|
+
"default": "./dist/globals.js"
|
|
27
|
+
},
|
|
28
|
+
"./globals/deno-compat": {
|
|
29
|
+
"types": "./dist/globals-deno-compat.d.ts",
|
|
30
|
+
"default": "./dist/globals-deno-compat.js"
|
|
23
31
|
}
|
|
24
32
|
},
|
|
25
33
|
"files": [
|
|
@@ -38,7 +46,8 @@
|
|
|
38
46
|
"install:node-deno": "node src/main.ts install --runtime node --deno-compat",
|
|
39
47
|
"prepack": "npm run build",
|
|
40
48
|
"test": "node --test test/*.test.ts",
|
|
41
|
-
"e2e": "uv run --with nbclient --with nbformat --with jupyter_client python test/e2e.py"
|
|
49
|
+
"e2e": "uv run --with nbclient --with nbformat --with jupyter_client python test/e2e.py",
|
|
50
|
+
"test:lab": "uv run --with jupyterlab --with anywidget --with playwright --with nbformat python test/lab_widgets.py"
|
|
42
51
|
},
|
|
43
52
|
"dependencies": {
|
|
44
53
|
"acorn": "^8.15.0",
|
package/runtime/bootstrap.mjs
CHANGED
|
@@ -8,13 +8,18 @@
|
|
|
8
8
|
// Plain JavaScript on purpose: the same file must load unmodified in Node
|
|
9
9
|
// and in Deno.
|
|
10
10
|
|
|
11
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
11
12
|
import { createJupyter } from "./display.mjs";
|
|
12
13
|
|
|
13
14
|
const isDeno = typeof globalThis.Deno?.version?.deno === "string";
|
|
14
15
|
const ENC = new TextEncoder();
|
|
15
16
|
|
|
16
17
|
const cwd = isDeno ? Deno.cwd() : process.cwd();
|
|
17
|
-
|
|
18
|
+
// pathToFileURL, not string concatenation: a directory name containing
|
|
19
|
+
// "#", "?" or "%" would otherwise cut the path short or turn into a query,
|
|
20
|
+
// breaking relative imports and import.meta (#22). It also handles Windows
|
|
21
|
+
// drive letters.
|
|
22
|
+
const cwdUrl = pathToFileURL(cwd.endsWith("/") ? cwd : cwd + "/");
|
|
18
23
|
// Stand-in referrer for cell code, like Deno's ./$deno$jupyter_repl.mts.
|
|
19
24
|
const replUrl = new URL("$cdpk$repl.mjs", cwdUrl);
|
|
20
25
|
|
|
@@ -79,20 +84,51 @@ if (!isDeno) {
|
|
|
79
84
|
installNodeResolveHooks({ shimUrl: import.meta.url, replUrl: replUrl.href });
|
|
80
85
|
}
|
|
81
86
|
|
|
87
|
+
// In a Deno project (#13), Deno's own resolution (import map, npm:, jsr:)
|
|
88
|
+
// goes first, as in `deno jupyter`. A bare package it can't place, because
|
|
89
|
+
// package.json dependencies resolve relative to *this* module rather than
|
|
90
|
+
// the notebook, is retried from the project's node_modules.
|
|
91
|
+
const inDenoProject = isDeno && Deno.env.get("CDPK_DENO_PROJECT") === "1";
|
|
92
|
+
const { resolveFromNodeModules } = inDenoProject ? await import("./node-modules-resolve.mjs") : {};
|
|
93
|
+
|
|
94
|
+
function isResolutionError(err, specifier) {
|
|
95
|
+
return err instanceof TypeError && err.message.includes(`"${specifier}"`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function denoImport(specifier, options) {
|
|
99
|
+
const resolved = resolveRelative(specifier);
|
|
100
|
+
try {
|
|
101
|
+
return await import(resolved, options);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
const fromNodeModules = resolveFromNodeModules && isResolutionError(err, specifier)
|
|
104
|
+
? resolveFromNodeModules(specifier, cwd)
|
|
105
|
+
: null;
|
|
106
|
+
if (fromNodeModules === null) throw err;
|
|
107
|
+
return await import(fromNodeModules, options);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
82
111
|
globalThis.__cdpk = {
|
|
83
112
|
// Cell code awaits this first; see PRELUDE in src/transform.ts.
|
|
84
113
|
tick: () => new Promise((resolve) => setTimeout(resolve, 0)),
|
|
85
114
|
// Node: the resolve hook re-parents these imports. Deno would resolve
|
|
86
|
-
// relative paths against this file, so
|
|
87
|
-
import: (specifier, options) =>
|
|
115
|
+
// relative paths against this file, so denoImport does it here.
|
|
116
|
+
import: (specifier, options) => (isDeno ? denoImport(specifier, options) : import(specifier, options)),
|
|
88
117
|
meta: {
|
|
89
118
|
url: replUrl.href,
|
|
90
119
|
dirname: cwd,
|
|
91
|
-
filename: replUrl
|
|
120
|
+
filename: fileURLToPath(replUrl),
|
|
92
121
|
resolve: (specifier) => new URL(specifier, replUrl).href,
|
|
93
122
|
},
|
|
94
123
|
barrier,
|
|
95
124
|
broadcastResult: jupyter.broadcastResult,
|
|
125
|
+
// Frontend comm messages (#9), from the host's shell channel. Handlers
|
|
126
|
+
// run after one macrotask, like cells, so interrupt can reach them.
|
|
127
|
+
async commDispatch(msgType, content, metadata, buffers64) {
|
|
128
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
129
|
+
const buffers = buffers64.map((b) => Uint8Array.from(atob(b), (c) => c.charCodeAt(0)));
|
|
130
|
+
return await jupyter.commDispatch(msgType, content, metadata, buffers);
|
|
131
|
+
},
|
|
96
132
|
describeError(value) {
|
|
97
133
|
if (value instanceof Error) {
|
|
98
134
|
return JSON.stringify({ name: value.name, message: value.message, stack: value.stack ?? "" });
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** A comm message as a handler receives it. */
|
|
2
|
+
export interface CommMessage {
|
|
3
|
+
data: { [key: string]: unknown };
|
|
4
|
+
metadata: { [key: string]: unknown };
|
|
5
|
+
buffers: Uint8Array[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface CommSendOptions {
|
|
9
|
+
metadata?: { [key: string]: unknown };
|
|
10
|
+
buffers?: Uint8Array[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** One end of a Jupyter comm channel. */
|
|
14
|
+
export interface Comm {
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly targetName: string;
|
|
17
|
+
readonly closed: boolean;
|
|
18
|
+
send(data?: { [key: string]: unknown }, options?: CommSendOptions): void;
|
|
19
|
+
close(data?: { [key: string]: unknown }, options?: CommSendOptions): void;
|
|
20
|
+
onMsg(handler: (msg: CommMessage) => void | Promise<void>): void;
|
|
21
|
+
onClose(handler: (msg: CommMessage) => void | Promise<void>): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** `jupyter.comm`: open comms, and accept comms the frontend opens. */
|
|
25
|
+
export interface CommApi {
|
|
26
|
+
open(targetName: string, data?: { [key: string]: unknown }, options?: CommSendOptions): Comm;
|
|
27
|
+
registerTarget(targetName: string, handler: (comm: Comm, msg: CommMessage) => void | Promise<void>): void;
|
|
28
|
+
unregisterTarget(targetName: string): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Comms {
|
|
32
|
+
api: CommApi;
|
|
33
|
+
/** Routes a frontend comm message; resolves to whether a comm or target took it. */
|
|
34
|
+
dispatch(
|
|
35
|
+
msgType: string,
|
|
36
|
+
content: { [key: string]: unknown },
|
|
37
|
+
metadata: { [key: string]: unknown },
|
|
38
|
+
buffers: Uint8Array[],
|
|
39
|
+
): Promise<boolean>;
|
|
40
|
+
list(): { commId: string; targetName: string }[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createComms(
|
|
44
|
+
send: (msgType: string, content: object, metadata: object, buffers: Uint8Array[]) => void,
|
|
45
|
+
newId?: () => string,
|
|
46
|
+
): Comms;
|
package/runtime/comm.mjs
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// @ts-self-types="./comm.d.mts"
|
|
2
|
+
// Jupyter comms: bidirectional channels between kernel and frontend, the
|
|
3
|
+
// transport ipywidgets and anywidget are built on (#9). Runs inside the
|
|
4
|
+
// target runtime.
|
|
5
|
+
//
|
|
6
|
+
// Kernel-to-frontend messages (comm_open, comm_msg, comm_close) go out
|
|
7
|
+
// through `send`, the same ordered path as display messages. Frontend
|
|
8
|
+
// messages arrive from the host through `dispatch`, which returns whether
|
|
9
|
+
// anything in the kernel handled them, so the host can answer an unknown
|
|
10
|
+
// target's comm_open with a comm_close, as the protocol asks.
|
|
11
|
+
|
|
12
|
+
class Comm {
|
|
13
|
+
#send;
|
|
14
|
+
#onClosed;
|
|
15
|
+
#msgHandlers = [];
|
|
16
|
+
#closeHandlers = [];
|
|
17
|
+
closed = false;
|
|
18
|
+
|
|
19
|
+
constructor(id, targetName, send, onClosed) {
|
|
20
|
+
this.id = id;
|
|
21
|
+
this.targetName = targetName;
|
|
22
|
+
this.#send = send;
|
|
23
|
+
this.#onClosed = onClosed;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
send(data = {}, { metadata = {}, buffers = [] } = {}) {
|
|
27
|
+
if (this.closed) return;
|
|
28
|
+
this.#send("comm_msg", { comm_id: this.id, data }, metadata, buffers);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
close(data = {}, { metadata = {}, buffers = [] } = {}) {
|
|
32
|
+
if (this.closed) return;
|
|
33
|
+
this.#send("comm_close", { comm_id: this.id, data }, metadata, buffers);
|
|
34
|
+
// close() is synchronous for callers; its close handlers run detached.
|
|
35
|
+
void this._closed({ data, metadata, buffers });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
onMsg(handler) {
|
|
39
|
+
this.#msgHandlers.push(handler);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
onClose(handler) {
|
|
43
|
+
this.#closeHandlers.push(handler);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async _receive(msg) {
|
|
47
|
+
for (const handler of this.#msgHandlers) await handler(msg);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async _closed(msg) {
|
|
51
|
+
if (this.closed) return;
|
|
52
|
+
this.closed = true;
|
|
53
|
+
this.#onClosed(this.id);
|
|
54
|
+
for (const handler of this.#closeHandlers) await handler(msg);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {(msgType: string, content: object, metadata: object, buffers: Uint8Array[]) => void} send
|
|
60
|
+
* @param {() => string} [newId]
|
|
61
|
+
*/
|
|
62
|
+
export function createComms(send, newId = () => crypto.randomUUID()) {
|
|
63
|
+
const comms = new Map(); // comm_id -> Comm
|
|
64
|
+
const targets = new Map(); // target_name -> handler
|
|
65
|
+
const forget = (id) => comms.delete(id);
|
|
66
|
+
|
|
67
|
+
function track(id, targetName) {
|
|
68
|
+
const comm = new Comm(id, targetName, send, forget);
|
|
69
|
+
comms.set(id, comm);
|
|
70
|
+
return comm;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const api = {
|
|
74
|
+
open(targetName, data = {}, { metadata = {}, buffers = [] } = {}) {
|
|
75
|
+
const comm = track(newId(), targetName);
|
|
76
|
+
send("comm_open", { comm_id: comm.id, target_name: targetName, data }, metadata, buffers);
|
|
77
|
+
return comm;
|
|
78
|
+
},
|
|
79
|
+
registerTarget(targetName, handler) {
|
|
80
|
+
targets.set(targetName, handler);
|
|
81
|
+
},
|
|
82
|
+
unregisterTarget(targetName) {
|
|
83
|
+
targets.delete(targetName);
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
async function dispatch(msgType, content, metadata, buffers) {
|
|
88
|
+
const msg = { data: content.data ?? {}, metadata: metadata ?? {}, buffers: buffers ?? [] };
|
|
89
|
+
if (msgType === "comm_open") {
|
|
90
|
+
const handler = targets.get(content.target_name);
|
|
91
|
+
if (!handler) return false;
|
|
92
|
+
await handler(track(content.comm_id, content.target_name), msg);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
const comm = comms.get(content.comm_id);
|
|
96
|
+
if (!comm) return false;
|
|
97
|
+
if (msgType === "comm_msg") await comm._receive(msg);
|
|
98
|
+
else if (msgType === "comm_close") await comm._closed(msg);
|
|
99
|
+
else return false;
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function list() {
|
|
104
|
+
return [...comms.values()].map((comm) => ({ commId: comm.id, targetName: comm.targetName }));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { api, dispatch, list };
|
|
108
|
+
}
|
package/runtime/deno-compat.mjs
CHANGED
package/runtime/display.d.mts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import type { CommApi, Comms } from "./comm.d.mts";
|
|
2
|
+
import type { Widget, WidgetOptions } from "./widget.d.mts";
|
|
3
|
+
|
|
4
|
+
export type { CommApi, Widget, WidgetOptions };
|
|
5
|
+
|
|
1
6
|
export const $display: unique symbol;
|
|
2
7
|
|
|
3
8
|
export type MediaBundle = { [mediaType: string]: unknown };
|
|
@@ -33,6 +38,10 @@ export interface JupyterNamespace {
|
|
|
33
38
|
svg(strings: TemplateStringsArray, ...values: unknown[]): Displayable;
|
|
34
39
|
image(obj: string | Uint8Array): Displayable;
|
|
35
40
|
$display: typeof $display;
|
|
41
|
+
/** Jupyter comms: bidirectional channels to the frontend. */
|
|
42
|
+
comm: CommApi;
|
|
43
|
+
/** An anywidget-compatible widget; display the result to show it. */
|
|
44
|
+
widget(options: WidgetOptions): Widget;
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
export interface Jupyter {
|
|
@@ -40,6 +49,8 @@ export interface Jupyter {
|
|
|
40
49
|
broadcastResult(executionCount: number, result: unknown): Promise<void>;
|
|
41
50
|
confirm(message?: string): boolean;
|
|
42
51
|
prompt(message?: string, defaultValue?: string, options?: { password?: boolean }): string | null;
|
|
52
|
+
/** Routes a frontend comm message into the kernel's comms. */
|
|
53
|
+
commDispatch: Comms["dispatch"];
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
export function escapeHTML(str: string): string;
|
package/runtime/display.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-self-types="./display.d.mts"
|
|
1
2
|
// The Jupyter display API that Deno exposes as `Deno.jupyter`, made
|
|
2
3
|
// runtime-neutral. Runs inside the target runtime (Node or Deno).
|
|
3
4
|
//
|
|
@@ -10,7 +11,11 @@
|
|
|
10
11
|
// Deno.readFileSync -> host.readFile
|
|
11
12
|
// op_jupyter_broadcast -> host.send
|
|
12
13
|
// WebGPU texture/buffer display depends on Deno's wgpu bindings and is
|
|
13
|
-
// omitted.
|
|
14
|
+
// omitted. Comms and widgets (jupyter.comm, jupyter.widget) are additions
|
|
15
|
+
// Deno doesn't have; see comm.mjs and widget.mjs.
|
|
16
|
+
|
|
17
|
+
import { createComms } from "./comm.mjs";
|
|
18
|
+
import { createWidgetFactory } from "./widget.mjs";
|
|
14
19
|
|
|
15
20
|
export const $display = Symbol.for("Jupyter.display");
|
|
16
21
|
|
|
@@ -219,6 +224,8 @@ export function createJupyter(host) {
|
|
|
219
224
|
return answer === "" ? defaultValue : answer;
|
|
220
225
|
}
|
|
221
226
|
|
|
227
|
+
const comms = createComms((msgType, content, metadata, buffers) => host.send(msgType, content, metadata, buffers));
|
|
228
|
+
|
|
222
229
|
return {
|
|
223
230
|
namespace: {
|
|
224
231
|
broadcast,
|
|
@@ -229,9 +236,12 @@ export function createJupyter(host) {
|
|
|
229
236
|
svg: taggedTemplate("image/svg+xml"),
|
|
230
237
|
image,
|
|
231
238
|
$display,
|
|
239
|
+
comm: comms.api,
|
|
240
|
+
widget: createWidgetFactory(comms.api, $display),
|
|
232
241
|
},
|
|
233
242
|
broadcastResult,
|
|
234
243
|
confirm,
|
|
235
244
|
prompt,
|
|
245
|
+
commDispatch: comms.dispatch,
|
|
236
246
|
};
|
|
237
247
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** True for "pkg", "@scope/pkg", "pkg/sub"; false for paths, URLs and aliases like "@/x". */
|
|
2
|
+
export function isBareSpecifier(specifier: string): boolean;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* An absolute file URL for a bare package import, resolved from the
|
|
6
|
+
* node_modules at or above `fromDir`, or null when none has the package.
|
|
7
|
+
*/
|
|
8
|
+
export function resolveFromNodeModules(specifier: string, fromDir: string): string | null;
|