@tlockney/cdp-kernel 0.2.0 → 0.4.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 +259 -33
- package/dist/bun-target.d.ts +60 -0
- package/dist/bun-target.js +236 -0
- package/dist/celld-target.d.ts +39 -0
- package/dist/celld-target.js +274 -0
- package/dist/debugger.d.ts +42 -0
- package/dist/debugger.js +534 -0
- package/dist/history.d.ts +18 -0
- package/dist/history.js +45 -0
- package/dist/kernel.js +91 -15
- package/dist/main.js +8 -6
- package/dist/murmur2.d.ts +2 -0
- package/dist/murmur2.js +41 -0
- package/dist/package.d.ts +16 -2
- package/dist/package.js +28 -2
- package/dist/sourcemap.d.ts +22 -0
- package/dist/sourcemap.js +86 -0
- package/dist/target.d.ts +65 -7
- package/dist/target.js +57 -6
- package/dist/text.d.ts +6 -0
- package/dist/text.js +17 -0
- package/dist/transform.d.ts +16 -0
- package/dist/transform.js +173 -11
- package/dist/workerd-target.d.ts +125 -0
- package/dist/workerd-target.js +455 -0
- package/dist/workers-project.d.ts +27 -0
- package/dist/workers-project.js +195 -0
- package/dist/ws-client.d.ts +32 -0
- package/dist/ws-client.js +166 -0
- package/package.json +18 -4
- package/runtime/bootstrap.mjs +55 -5
- package/runtime/bun-channel.d.mts +3 -0
- package/runtime/bun-channel.mjs +58 -0
- package/runtime/deno-compat.mjs +3 -1
- package/runtime/display.d.mts +2 -0
- package/runtime/display.mjs +9 -1
- package/runtime/evaluator.d.mts +22 -0
- package/runtime/evaluator.mjs +91 -0
- package/runtime/introspect.d.mts +13 -0
- package/runtime/introspect.mjs +120 -0
- package/runtime/node-hooks.d.mts +29 -0
- package/runtime/node-hooks.mjs +160 -20
- package/runtime/workerd-worker.mjs +194 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// What inspect_request (#10) shows for a value: the text of a JupyterLab
|
|
2
|
+
// tooltip (Shift-Tab) or inspector panel. Shared by every target; the host
|
|
3
|
+
// decides how the value is found (src/target.ts: CDP's throwOnSideEffect;
|
|
4
|
+
// in-process targets: resolvePath, below).
|
|
5
|
+
//
|
|
6
|
+
// No imports: this file must load in every target. `inspect` (util.inspect
|
|
7
|
+
// or Deno.inspect) is passed in.
|
|
8
|
+
|
|
9
|
+
const PREVIEW_CHARS = 1500;
|
|
10
|
+
const SOURCE_CHARS = 4000;
|
|
11
|
+
const MAX_PROPERTIES = 40;
|
|
12
|
+
|
|
13
|
+
const IDENTIFIER_PATH = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Looks up a dotted path such as "a.b.c" from globalThis, by property
|
|
17
|
+
* descriptor, so nothing runs: a getter along the way is reported rather
|
|
18
|
+
* than called. In-process targets (Bun, workerd, celld) have no way to
|
|
19
|
+
* evaluate without side effects, so inspection and completion use this.
|
|
20
|
+
*/
|
|
21
|
+
export function resolvePath(path, root = globalThis) {
|
|
22
|
+
if (!IDENTIFIER_PATH.test(path)) return { found: false };
|
|
23
|
+
let value = root;
|
|
24
|
+
for (const name of path.split(".")) {
|
|
25
|
+
if (value === null || value === undefined) return { found: false };
|
|
26
|
+
let descriptor;
|
|
27
|
+
for (let o = Object(value); o !== null && descriptor === undefined; o = Object.getPrototypeOf(o)) {
|
|
28
|
+
descriptor = Object.getOwnPropertyDescriptor(o, name);
|
|
29
|
+
}
|
|
30
|
+
if (descriptor === undefined) return { found: false };
|
|
31
|
+
if (!("value" in descriptor)) return { found: true, accessor: true, name };
|
|
32
|
+
value = descriptor.value;
|
|
33
|
+
}
|
|
34
|
+
return { found: true, value };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function truncate(text, limit) {
|
|
38
|
+
return text.length > limit ? `${text.slice(0, limit)}… (${text.length - limit} more characters)` : text;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** "function add(a, b) {...}" -> "add(a, b)"; "class K {...}" -> "class K". */
|
|
42
|
+
export function signatureOf(fn) {
|
|
43
|
+
let source;
|
|
44
|
+
try {
|
|
45
|
+
source = Function.prototype.toString.call(fn);
|
|
46
|
+
} catch {
|
|
47
|
+
return `${fn.name || "anonymous"}(…)`;
|
|
48
|
+
}
|
|
49
|
+
if (/^class\b/.test(source)) return `class ${fn.name || "(anonymous)"}`;
|
|
50
|
+
const params = /^[^(]*\(([^)]*)\)/.exec(source)?.[1];
|
|
51
|
+
const native = /\{\s*\[native code\]\s*\}$/.test(source);
|
|
52
|
+
// One line, however the parameters were spread over the source.
|
|
53
|
+
const written = (params ?? "").replace(/\s+/g, " ").replace(/\s*,\s*/g, ", ").trim();
|
|
54
|
+
const args = native ? Array.from({ length: fn.length }, (_, i) => `arg${i}`).join(", ") : written;
|
|
55
|
+
const arrow = !/^(?:async\s+)?function\b/.test(source) && /=>/.test(source.split("{")[0] ?? "");
|
|
56
|
+
const name = fn.name || (arrow ? "(arrow function)" : "anonymous");
|
|
57
|
+
const prefix = /^async\b/.test(source) ? "async " : "";
|
|
58
|
+
return `${prefix}${name}(${args})`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function typeOf(value) {
|
|
62
|
+
if (value === null) return "null";
|
|
63
|
+
if (typeof value !== "object" && typeof value !== "function") return typeof value;
|
|
64
|
+
const name = Object.getPrototypeOf(value)?.constructor?.name;
|
|
65
|
+
if (typeof value === "function") return /^class\b/.test(safeSource(value)) ? "class" : "function";
|
|
66
|
+
return name && name !== "Object" ? `object (${name})` : "object";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function safeSource(fn) {
|
|
70
|
+
try {
|
|
71
|
+
return Function.prototype.toString.call(fn);
|
|
72
|
+
} catch {
|
|
73
|
+
return "";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function propertyNames(value) {
|
|
78
|
+
if ((typeof value !== "object" && typeof value !== "function") || value === null) return [];
|
|
79
|
+
try {
|
|
80
|
+
// A function's own internals aren't worth listing; older V8 (Node 24)
|
|
81
|
+
// also gives sloppy-mode functions own `arguments` and `caller`.
|
|
82
|
+
const internal = ["length", "name", "prototype", "arguments", "caller"];
|
|
83
|
+
return Object.getOwnPropertyNames(value).filter((n) => !(typeof value === "function" && internal.includes(n)));
|
|
84
|
+
} catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The tooltip text for a value. `detail` 1 (Shift-Tab twice, or the
|
|
91
|
+
* inspector panel) adds a function's source.
|
|
92
|
+
*/
|
|
93
|
+
export function describeValue(value, { inspect, detail = 0 }) {
|
|
94
|
+
const lines = [`Type: ${typeOf(value)}`];
|
|
95
|
+
if (typeof value === "function") lines.push(`Signature: ${signatureOf(value)}`);
|
|
96
|
+
let preview;
|
|
97
|
+
try {
|
|
98
|
+
preview = inspect(value);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
preview = `(inspect failed: ${err instanceof Error ? err.message : String(err)})`;
|
|
101
|
+
}
|
|
102
|
+
lines.push(`Value: ${truncate(preview, PREVIEW_CHARS)}`);
|
|
103
|
+
const names = propertyNames(value);
|
|
104
|
+
if (names.length > 0) {
|
|
105
|
+
const shown = names.slice(0, MAX_PROPERTIES).join(", ");
|
|
106
|
+
lines.push(`Properties: ${shown}${names.length > MAX_PROPERTIES ? `, … (${names.length - MAX_PROPERTIES} more)` : ""}`);
|
|
107
|
+
}
|
|
108
|
+
if (detail > 0 && typeof value === "function") lines.push(`Source:\n${truncate(safeSource(value), SOURCE_CHARS)}`);
|
|
109
|
+
return lines.join("\n");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** inspect_request for in-process targets: resolvePath, then describeValue. */
|
|
113
|
+
export function inspectPath(path, { inspect, detail = 0, root = globalThis }) {
|
|
114
|
+
const resolved = resolvePath(path, root);
|
|
115
|
+
if (!resolved.found) return null;
|
|
116
|
+
if (resolved.accessor) {
|
|
117
|
+
return `Type: accessor property\n${resolved.name} has a getter, which isn't called for inspection: it could have side effects.`;
|
|
118
|
+
}
|
|
119
|
+
return describeValue(resolved.value, { inspect, detail });
|
|
120
|
+
}
|
package/runtime/node-hooks.d.mts
CHANGED
|
@@ -7,4 +7,33 @@ export interface PackageSpecifier {
|
|
|
7
7
|
export function parsePackageSpecifier(spec: string): PackageSpecifier;
|
|
8
8
|
export function jsrToNpm(spec: string): PackageSpecifier;
|
|
9
9
|
export function aliasFor(name: string, range: string): string;
|
|
10
|
+
export type Installer = "npm" | "bun";
|
|
11
|
+
export function installCommand(
|
|
12
|
+
installer: Installer,
|
|
13
|
+
alias: string,
|
|
14
|
+
name: string,
|
|
15
|
+
range: string,
|
|
16
|
+
): { cmd: string; args: string[]; env?: { [name: string]: string } };
|
|
17
|
+
/** Runs an install; `env` is added to the process environment. */
|
|
18
|
+
export type InstallRunner = (
|
|
19
|
+
cmd: string,
|
|
20
|
+
args: string[],
|
|
21
|
+
cwd: string,
|
|
22
|
+
env?: { [name: string]: string },
|
|
23
|
+
) => { status: number | null; stderr?: string; error?: Error };
|
|
24
|
+
export function ensureInstalled(
|
|
25
|
+
pkg: { name: string; range: string },
|
|
26
|
+
installer?: Installer,
|
|
27
|
+
options?: { lockFile?: string | null; run?: InstallRunner },
|
|
28
|
+
): { dir: string; alias: string };
|
|
29
|
+
|
|
30
|
+
export const LOCK_FILE: string;
|
|
31
|
+
export interface Lock {
|
|
32
|
+
version: 1;
|
|
33
|
+
packages: { [nameAtRange: string]: string };
|
|
34
|
+
}
|
|
35
|
+
export function findLockFile(dir: string): string | null;
|
|
36
|
+
export function readLock(path: string): Lock;
|
|
37
|
+
export function writeLock(path: string, lock: Lock): void;
|
|
38
|
+
export function withInstallLock<T>(dir: string, fn: () => T, options?: { timeoutMs?: number }): T;
|
|
10
39
|
export function installNodeResolveHooks(urls: { shimUrl: string; replUrl: string }): void;
|
package/runtime/node-hooks.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-self-types="./node-hooks.d.mts"
|
|
1
2
|
// Node-only module resolution for cell code: the part of Deno's module
|
|
2
3
|
// loader a notebook leans on most.
|
|
3
4
|
// - Imports issued by cell code arrive with the bootstrap module as their
|
|
@@ -11,13 +12,15 @@
|
|
|
11
12
|
// Uses module.registerHooks (synchronous, in-thread; Node >= 23.5), so the
|
|
12
13
|
// install can run with spawnSync inside resolve.
|
|
13
14
|
|
|
14
|
-
import
|
|
15
|
+
// Not a named import: Bun, which loads this file for its npm:/jsr: helpers,
|
|
16
|
+
// has no registerHooks, and a missing named export fails at link time.
|
|
17
|
+
import module from "node:module";
|
|
15
18
|
import { spawnSync } from "node:child_process";
|
|
16
19
|
import { createHash } from "node:crypto";
|
|
17
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
18
21
|
import { homedir } from "node:os";
|
|
19
|
-
import { join } from "node:path";
|
|
20
|
-
import { pathToFileURL } from "node:url";
|
|
22
|
+
import { dirname, join } from "node:path";
|
|
23
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
21
24
|
|
|
22
25
|
const SPEC_RE = /^((?:@[^/@]+\/)?[^/@]+)(?:@([^/]+))?(\/.*)?$/;
|
|
23
26
|
|
|
@@ -41,8 +44,12 @@ export function aliasFor(name, range) {
|
|
|
41
44
|
return `cdpk-${digest}`;
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
// Each installer keeps its own cache, so npm's and Bun's lockfiles never
|
|
48
|
+
// meet; both read the @jsr registry from .npmrc.
|
|
49
|
+
function cacheDir(installer) {
|
|
50
|
+
const dir = installer === "npm"
|
|
51
|
+
? process.env.CDPK_CACHE_DIR ?? join(homedir(), ".cache", "cdp-kernel", "npm")
|
|
52
|
+
: join(homedir(), ".cache", "cdp-kernel", installer);
|
|
46
53
|
if (!existsSync(join(dir, "package.json"))) {
|
|
47
54
|
mkdirSync(dir, { recursive: true });
|
|
48
55
|
writeFileSync(join(dir, "package.json"), '{ "private": true }\n');
|
|
@@ -51,30 +58,163 @@ function cacheDir() {
|
|
|
51
58
|
return dir;
|
|
52
59
|
}
|
|
53
60
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
/** The command that installs name@range under `alias` with `installer`. */
|
|
62
|
+
// - Saved exactly (#40): npm records `npm:react@19.0.0` as `^19.0.0` by
|
|
63
|
+
// default, and every later install into the shared cache re-resolves the
|
|
64
|
+
// ranges, so pinned packages drifted (react 19.3.1 beside react-dom
|
|
65
|
+
// 19.0.0). With an exact save, a version stays put and a range is frozen
|
|
66
|
+
// at what it first resolved to. Bun already saves exactly; the flag says so.
|
|
67
|
+
// - JSR packages come as tarballs from npm.jsr.io, which npm 12 refuses as
|
|
68
|
+
// "remote" unless allowed (#39). `root` is the narrowest value that works
|
|
69
|
+
// (`true`, which looks right, is invalid and warns), and only the JSR
|
|
70
|
+
// install gets it.
|
|
71
|
+
export function installCommand(installer, alias, name, range) {
|
|
72
|
+
const spec = `${alias}@npm:${name}@${range}`;
|
|
73
|
+
// The Bun target runs this inside Bun, so it uses its own binary.
|
|
74
|
+
if (installer === "bun") return { cmd: process.execPath, args: ["add", "--silent", "--exact", spec] };
|
|
75
|
+
const args = ["install", "--no-audit", "--no-fund", "--loglevel=error", "--save-exact", spec];
|
|
76
|
+
return name.startsWith("@jsr/") ? { cmd: "npm", args, env: { npm_config_allow_remote: "root" } } : { cmd: "npm", args };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// --- Lockfile (#14) -----------------------------------------------------------
|
|
80
|
+
// A cdp-kernel.lock.json at or above the notebook pins each npm:/jsr:
|
|
81
|
+
// request to the exact version it first resolved to, so the same notebook
|
|
82
|
+
// gets the same packages on another machine. Opt-in: create the file (with
|
|
83
|
+
// `{}` in it) to start locking; the kernel then fills it in.
|
|
84
|
+
|
|
85
|
+
export const LOCK_FILE = "cdp-kernel.lock.json";
|
|
86
|
+
|
|
87
|
+
/** The nearest cdp-kernel.lock.json at or above `dir`, or null. */
|
|
88
|
+
export function findLockFile(dir) {
|
|
89
|
+
for (let at = dir;; at = dirname(at)) {
|
|
90
|
+
if (existsSync(join(at, LOCK_FILE))) return join(at, LOCK_FILE);
|
|
91
|
+
if (dirname(at) === at) return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** `{ version: 1, packages: { "name@range": "exact version" } }`; `{}` counts as empty. */
|
|
96
|
+
export function readLock(path) {
|
|
97
|
+
const parsed = JSON.parse(readFileSync(path, "utf8") || "{}");
|
|
98
|
+
const packages = parsed && typeof parsed.packages === "object" && parsed.packages !== null ? parsed.packages : {};
|
|
99
|
+
return { version: 1, packages };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Sorted, so a lockfile's diff shows only what changed. */
|
|
103
|
+
export function writeLock(path, lock) {
|
|
104
|
+
const packages = Object.fromEntries(Object.entries(lock.packages).sort(([a], [b]) => a.localeCompare(b)));
|
|
105
|
+
writeFileSync(path, JSON.stringify({ version: 1, packages }, null, 2) + "\n");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- Serialized installs (#14) -------------------------------------------------
|
|
109
|
+
// Kernels share one cache per installer, and npm/bun both rewrite its
|
|
110
|
+
// package.json and node_modules, so installs take a lock: a directory, which
|
|
111
|
+
// mkdir creates atomically. A lock left by a process that died is taken over.
|
|
112
|
+
|
|
113
|
+
const LOCK_DIR = ".install-lock";
|
|
114
|
+
const STALE_MS = 10 * 60 * 1000;
|
|
115
|
+
|
|
116
|
+
function sleep(ms) {
|
|
117
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function holderAlive(lockDir) {
|
|
121
|
+
try {
|
|
122
|
+
const pid = Number(readFileSync(join(lockDir, "pid"), "utf8"));
|
|
123
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
124
|
+
process.kill(pid, 0);
|
|
125
|
+
return true;
|
|
126
|
+
} catch (err) {
|
|
127
|
+
// EPERM: alive, but someone else's.
|
|
128
|
+
return err?.code === "EPERM";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Runs `fn` holding the cache's install lock. Synchronous, like the resolve hook. */
|
|
133
|
+
export function withInstallLock(dir, fn, { timeoutMs = STALE_MS } = {}) {
|
|
134
|
+
const lockDir = join(dir, LOCK_DIR);
|
|
135
|
+
const deadline = Date.now() + timeoutMs;
|
|
136
|
+
for (;;) {
|
|
137
|
+
try {
|
|
138
|
+
mkdirSync(lockDir);
|
|
139
|
+
writeFileSync(join(lockDir, "pid"), String(process.pid));
|
|
140
|
+
break;
|
|
141
|
+
} catch (err) {
|
|
142
|
+
if (err?.code !== "EEXIST") throw err;
|
|
143
|
+
let age = 0;
|
|
144
|
+
try {
|
|
145
|
+
age = Date.now() - statSync(lockDir).mtimeMs;
|
|
146
|
+
} catch {
|
|
147
|
+
continue; // released meanwhile
|
|
148
|
+
}
|
|
149
|
+
// A holder that died, or one stuck for longer than any install takes.
|
|
150
|
+
if ((age > 1000 && !holderAlive(lockDir)) || age > STALE_MS) {
|
|
151
|
+
rmSync(lockDir, { recursive: true, force: true });
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (Date.now() > deadline) throw new Error(`timed out waiting for the install lock ${lockDir}`);
|
|
155
|
+
sleep(100);
|
|
65
156
|
}
|
|
66
157
|
}
|
|
67
|
-
|
|
158
|
+
try {
|
|
159
|
+
return fn();
|
|
160
|
+
} finally {
|
|
161
|
+
rmSync(lockDir, { recursive: true, force: true });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function installedVersion(dir, alias) {
|
|
166
|
+
try {
|
|
167
|
+
return JSON.parse(readFileSync(join(dir, "node_modules", alias, "package.json"), "utf8")).version ?? null;
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function spawnInstall(cmd, args, cwd, env = {}) {
|
|
174
|
+
return spawnSync(cmd, args, { cwd, encoding: "utf8", env: { ...process.env, ...env } });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Installs a package into the installer's cache unless it's there already.
|
|
179
|
+
* With a lockfile, a locked request installs its exact version, and an
|
|
180
|
+
* unlocked one is recorded once installed. `run` is the install command
|
|
181
|
+
* runner, replaceable in tests.
|
|
182
|
+
*/
|
|
183
|
+
export function ensureInstalled({ name, range }, installer = "npm", { lockFile = null, run = spawnInstall } = {}) {
|
|
184
|
+
const dir = cacheDir(installer);
|
|
185
|
+
const key = `${name}@${range}`;
|
|
186
|
+
return withInstallLock(dir, () => {
|
|
187
|
+
const lock = lockFile ? readLock(lockFile) : null;
|
|
188
|
+
const locked = lock?.packages[key];
|
|
189
|
+
const version = typeof locked === "string" ? locked : range;
|
|
190
|
+
const alias = aliasFor(name, version);
|
|
191
|
+
if (installedVersion(dir, alias) === null) {
|
|
192
|
+
const { cmd, args, env } = installCommand(installer, alias, name, version);
|
|
193
|
+
const result = run(cmd, args, dir, env);
|
|
194
|
+
if (result.status !== 0) {
|
|
195
|
+
throw new Error(`${installer} install ${name}@${version} failed:\n${result.stderr || result.error}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (lock && typeof locked !== "string") {
|
|
199
|
+
const resolved = installedVersion(dir, alias);
|
|
200
|
+
if (resolved !== null) {
|
|
201
|
+
lock.packages[key] = resolved;
|
|
202
|
+
writeLock(lockFile, lock);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return { dir, alias };
|
|
206
|
+
});
|
|
68
207
|
}
|
|
69
208
|
|
|
70
209
|
export function installNodeResolveHooks({ shimUrl, replUrl }) {
|
|
71
|
-
registerHooks({
|
|
210
|
+
module.registerHooks({
|
|
72
211
|
resolve(specifier, context, nextResolve) {
|
|
73
212
|
let pkg = null;
|
|
74
213
|
if (specifier.startsWith("npm:")) pkg = parsePackageSpecifier(specifier.slice(4));
|
|
75
214
|
else if (specifier.startsWith("jsr:")) pkg = jsrToNpm(specifier.slice(4));
|
|
76
215
|
if (pkg) {
|
|
77
|
-
const
|
|
216
|
+
const lockFile = findLockFile(fileURLToPath(new URL(".", replUrl)));
|
|
217
|
+
const { dir, alias } = ensureInstalled(pkg, "npm", { lockFile });
|
|
78
218
|
const parentURL = pathToFileURL(join(dir, "package.json")).href;
|
|
79
219
|
return nextResolve(alias + pkg.subpath, { ...context, parentURL });
|
|
80
220
|
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// The driver Worker for the workerd target (#8).
|
|
2
|
+
//
|
|
3
|
+
// workerd's inspector can evaluate code, but outside a request there is no
|
|
4
|
+
// IoContext: timers, fetch and crypto.randomUUID throw "Disallowed operation
|
|
5
|
+
// called within global scope". So the host opens one WebSocket session to
|
|
6
|
+
// this Worker (over a Unix socket only it can reach, or through a project's
|
|
7
|
+
// Miniflare) and every request runs inside that session's request. The cell
|
|
8
|
+
// code itself comes from transformCellForEval and is run by the UnsafeEval
|
|
9
|
+
// binding as global code, so its declarations persist (see evaluator.mjs).
|
|
10
|
+
//
|
|
11
|
+
// Console output, iopub messages and replies all go back over the one
|
|
12
|
+
// socket, in order, so no barrier markers are needed.
|
|
13
|
+
//
|
|
14
|
+
// workerd can't load modules at runtime beyond its builtins, so the host
|
|
15
|
+
// bundles anything else a cell imports (src/workerd-target.ts) and sends it
|
|
16
|
+
// with the cell.
|
|
17
|
+
|
|
18
|
+
import { formatWithOptions, inspect } from "node:util";
|
|
19
|
+
import { createJupyter } from "./display.mjs";
|
|
20
|
+
import { createHandler } from "./evaluator.mjs";
|
|
21
|
+
import { describeValue, inspectPath, resolvePath } from "./introspect.mjs";
|
|
22
|
+
|
|
23
|
+
const BUILTIN = /^(?:node|cloudflare|workerd):/;
|
|
24
|
+
|
|
25
|
+
function base64(bytes) {
|
|
26
|
+
let binary = "";
|
|
27
|
+
for (let i = 0; i < bytes.length; i += 0x8000) {
|
|
28
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
|
29
|
+
}
|
|
30
|
+
return btoa(binary);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// --- Output -----------------------------------------------------------------
|
|
34
|
+
// Lines go to the host's session. Anything emitted before it connects waits.
|
|
35
|
+
let sink = null;
|
|
36
|
+
const outbox = [];
|
|
37
|
+
|
|
38
|
+
function emit(message) {
|
|
39
|
+
const line = JSON.stringify(message);
|
|
40
|
+
if (sink) sink(line);
|
|
41
|
+
else outbox.push(line);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const stream = (name) => (...args) => emit({ kind: "stream", name, text: formatWithOptions({ colors: true }, ...args) + "\n" });
|
|
45
|
+
Object.assign(console, {
|
|
46
|
+
log: stream("stdout"),
|
|
47
|
+
info: stream("stdout"),
|
|
48
|
+
debug: stream("stdout"),
|
|
49
|
+
dir: (value) => emit({ kind: "stream", name: "stdout", text: inspect(value, { colors: true, depth: 4 }) + "\n" }),
|
|
50
|
+
warn: stream("stderr"),
|
|
51
|
+
error: stream("stderr"),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const jupyter = createJupyter({
|
|
55
|
+
inspect: (value) => inspect(value, { colors: true, depth: 4 }),
|
|
56
|
+
base64,
|
|
57
|
+
readFile() {
|
|
58
|
+
throw new Error("workerd has no file system to read from; pass the image bytes instead");
|
|
59
|
+
},
|
|
60
|
+
send(msgType, content, metadata, buffers) {
|
|
61
|
+
emit({ kind: "iopub", msg_type: msgType, content, metadata, buffers: buffers.map(base64), barrier: 0 });
|
|
62
|
+
},
|
|
63
|
+
// A request can't block for an answer, so prompt() and confirm() behave
|
|
64
|
+
// as when the frontend has no stdin.
|
|
65
|
+
input: () => null,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// --- Modules ----------------------------------------------------------------
|
|
69
|
+
const modules = new Map();
|
|
70
|
+
|
|
71
|
+
/** `require` for a host-built bundle's external imports (builtins). */
|
|
72
|
+
async function requireFor(specifiers) {
|
|
73
|
+
const loaded = new Map();
|
|
74
|
+
for (const specifier of specifiers) {
|
|
75
|
+
// Packages require Node builtins by bare name too ("fs").
|
|
76
|
+
const ns = await import(BUILTIN.test(specifier) ? specifier : `node:${specifier}`);
|
|
77
|
+
loaded.set(specifier, ns.default ?? ns);
|
|
78
|
+
}
|
|
79
|
+
return (specifier) => {
|
|
80
|
+
if (!loaded.has(specifier)) throw new Error(`cannot require ${specifier} in workerd`);
|
|
81
|
+
return loaded.get(specifier);
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function cellImport(specifier, options) {
|
|
86
|
+
if (BUILTIN.test(specifier)) return import(specifier, options);
|
|
87
|
+
if (modules.has(specifier)) return modules.get(specifier);
|
|
88
|
+
return Promise.reject(new TypeError(`Cannot import "${specifier}": the host didn't bundle it`));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// --- The shim cells and the evaluator use -----------------------------------
|
|
92
|
+
let handle = null;
|
|
93
|
+
|
|
94
|
+
function install(env) {
|
|
95
|
+
const meta = JSON.parse(env.META);
|
|
96
|
+
globalThis.__cdpk = {
|
|
97
|
+
import: cellImport,
|
|
98
|
+
meta: { ...meta, resolve: (specifier) => new URL(specifier, meta.url).href },
|
|
99
|
+
requireFor,
|
|
100
|
+
// inspect_request (#10): by descriptor, so nothing runs.
|
|
101
|
+
describeForInspect: (value, detail) => describeValue(value, { inspect: (v) => inspect(v, { colors: false, depth: 2 }), detail }),
|
|
102
|
+
inspectPath: (path, detail) => inspectPath(path, { inspect: (v) => inspect(v, { colors: false, depth: 2 }), detail }),
|
|
103
|
+
resolvePath,
|
|
104
|
+
barrier: () => 0,
|
|
105
|
+
broadcastResult: jupyter.broadcastResult,
|
|
106
|
+
async commDispatch(msgType, content, metadata, buffers64) {
|
|
107
|
+
const buffers = buffers64.map((b) => Uint8Array.from(atob(b), (c) => c.charCodeAt(0)));
|
|
108
|
+
return await jupyter.commDispatch(msgType, content, metadata, buffers);
|
|
109
|
+
},
|
|
110
|
+
describeError(value) {
|
|
111
|
+
if (value instanceof Error) {
|
|
112
|
+
return JSON.stringify({ name: value.name, message: value.message, stack: value.stack ?? "" });
|
|
113
|
+
}
|
|
114
|
+
return JSON.stringify({ name: "Error", message: inspect(value), stack: "" });
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
Object.defineProperty(globalThis, "__cdpk", { enumerable: false });
|
|
118
|
+
globalThis.jupyter = jupyter.namespace;
|
|
119
|
+
globalThis.prompt = jupyter.prompt;
|
|
120
|
+
globalThis.confirm = jupyter.confirm;
|
|
121
|
+
// A cell has no fetch(request, env), so the Worker's bindings (#26) are a
|
|
122
|
+
// global, without the kernel's own.
|
|
123
|
+
const { UNSAFE, TOKEN, META, ...bindings } = env;
|
|
124
|
+
globalThis.env = bindings;
|
|
125
|
+
// workerd forbids eval without its UnsafeEval binding; celld (#25) allows
|
|
126
|
+
// a plain global eval and has no such binding.
|
|
127
|
+
const evaluate = env.UNSAFE ? (code) => env.UNSAFE.eval(code, "cell") : (code) => (0, eval)(code);
|
|
128
|
+
handle = createHandler(globalThis.__cdpk, evaluate);
|
|
129
|
+
return evaluate;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let evaluate = null;
|
|
133
|
+
|
|
134
|
+
/** Evaluates the bundles a cell brings, once each. */
|
|
135
|
+
async function loadModules(bundles = {}) {
|
|
136
|
+
for (const [specifier, script] of Object.entries(bundles)) {
|
|
137
|
+
if (modules.has(specifier)) continue;
|
|
138
|
+
const loading = Promise.resolve().then(() => evaluate(script));
|
|
139
|
+
// A module that failed to load is retried by the next import of it.
|
|
140
|
+
loading.catch(() => modules.delete(specifier));
|
|
141
|
+
modules.set(specifier, loading);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function answer(req) {
|
|
146
|
+
if (req.op === "info") {
|
|
147
|
+
return {
|
|
148
|
+
v8: globalThis.process?.versions?.v8 ?? "",
|
|
149
|
+
bindings: Object.keys(globalThis.env),
|
|
150
|
+
// A Workers project's default export (src/workers-project.ts).
|
|
151
|
+
worker: globalThis.worker !== undefined,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (req.op === "eval") await loadModules(req.modules);
|
|
155
|
+
return await handle(req);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export default {
|
|
159
|
+
async fetch(request, env) {
|
|
160
|
+
// A header, or the query string for a WebSocket client that can't set
|
|
161
|
+
// headers (celld's session uses the host's built-in WebSocket).
|
|
162
|
+
const token = request.headers.get("x-cdpk-token") ?? new URL(request.url).searchParams.get("token");
|
|
163
|
+
if (token !== env.TOKEN) return new Response("forbidden", { status: 403 });
|
|
164
|
+
if (request.headers.get("upgrade") !== "websocket") return new Response("expected a WebSocket", { status: 426 });
|
|
165
|
+
evaluate ??= install(env);
|
|
166
|
+
|
|
167
|
+
// The session: every request from the host arrives as a message, and
|
|
168
|
+
// all of them run in this one request's IoContext. So I/O objects (a
|
|
169
|
+
// Durable Object stub, a stream) made in one cell work in the next, and
|
|
170
|
+
// work a cell leaves running outlives it (#26).
|
|
171
|
+
const { 0: client, 1: server } = new WebSocketPair();
|
|
172
|
+
server.accept();
|
|
173
|
+
const send = (line) => {
|
|
174
|
+
try {
|
|
175
|
+
server.send(line);
|
|
176
|
+
} catch {
|
|
177
|
+
// The host is gone.
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
sink = send;
|
|
181
|
+
for (const line of outbox.splice(0)) send(line);
|
|
182
|
+
server.addEventListener("message", (event) => {
|
|
183
|
+
const req = JSON.parse(event.data);
|
|
184
|
+
answer(req).then(
|
|
185
|
+
(reply) => send(JSON.stringify({ kind: "reply", id: req.id, ...reply })),
|
|
186
|
+
(err) => send(JSON.stringify({ kind: "reply", id: req.id, failure: String(err?.stack ?? err) })),
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
server.addEventListener("close", () => {
|
|
190
|
+
if (sink === send) sink = null;
|
|
191
|
+
});
|
|
192
|
+
return new Response(null, { status: 101, webSocket: client });
|
|
193
|
+
},
|
|
194
|
+
};
|