@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.
- package/README.md +274 -21
- package/dist/bun-target.d.ts +58 -0
- package/dist/bun-target.js +231 -0
- package/dist/celld-target.d.ts +39 -0
- package/dist/celld-target.js +274 -0
- 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 +126 -11
- package/dist/main.js +5 -5
- package/dist/package.d.ts +13 -5
- package/dist/package.js +33 -8
- package/dist/target.d.ts +78 -7
- package/dist/target.js +67 -12
- package/dist/transform.d.ts +25 -1
- package/dist/transform.js +150 -11
- package/dist/workerd-target.d.ts +123 -0
- package/dist/workerd-target.js +450 -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 +27 -4
- package/runtime/bootstrap.mjs +78 -7
- package/runtime/bun-channel.d.mts +3 -0
- package/runtime/bun-channel.mjs +58 -0
- 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/evaluator.d.mts +18 -0
- package/runtime/evaluator.mjs +83 -0
- package/runtime/node-hooks.d.mts +11 -0
- package/runtime/node-hooks.mjs +24 -12
- 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/runtime/workerd-worker.mjs +189 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { CdpParams } from "./cdp.ts";
|
|
2
|
+
import type { TargetEvents } from "./target.ts";
|
|
3
|
+
import { WorkerdTarget } from "./workerd-target.ts";
|
|
4
|
+
/** The project keys the generated config keeps: bindings and compatibility. */
|
|
5
|
+
export declare const PROJECT_KEYS: string[];
|
|
6
|
+
/**
|
|
7
|
+
* The celld config for the driver: the project's bindings (or none), the
|
|
8
|
+
* pre-built driver as its one module, and the kernel's token and import.meta
|
|
9
|
+
* as vars, which the driver hides from cells' `env`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function celldConfig(project: CdpParams | null, { token, meta }: {
|
|
12
|
+
token: string;
|
|
13
|
+
meta: {
|
|
14
|
+
url: string;
|
|
15
|
+
dirname: string;
|
|
16
|
+
filename: string;
|
|
17
|
+
};
|
|
18
|
+
}): CdpParams;
|
|
19
|
+
/**
|
|
20
|
+
* celld's installer puts it in ~/.local/bin, which a Jupyter server's PATH
|
|
21
|
+
* often lacks, so look there before falling back to PATH.
|
|
22
|
+
*/
|
|
23
|
+
export declare function findCelld(home?: string): string;
|
|
24
|
+
/** The version in `celld --version`'s output ("celld X.Y.Z"). */
|
|
25
|
+
export declare function celldVersionOf(output: string): string;
|
|
26
|
+
/** The driver, and a project's main, as the single module celld runs. */
|
|
27
|
+
export declare function bundleDriver(runtime: string, projectMain?: string, define?: {
|
|
28
|
+
[name: string]: string;
|
|
29
|
+
}): Promise<string>;
|
|
30
|
+
/** A free loopback port: celld dev needs a number, and rejects 0. */
|
|
31
|
+
export declare function freePort(): Promise<number>;
|
|
32
|
+
/**
|
|
33
|
+
* A celld target. In a celld project its bindings come along; a project
|
|
34
|
+
* that can't load is reported in the first cell, and the driver runs alone.
|
|
35
|
+
*/
|
|
36
|
+
export declare function launchCelld(events: TargetEvents, options?: {
|
|
37
|
+
cwd?: string;
|
|
38
|
+
executable?: string;
|
|
39
|
+
}): Promise<WorkerdTarget>;
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// The celld target (#25). celld (denoland/celld) runs Workers and Durable
|
|
2
|
+
// Objects on its own storage; like workerd it has no I/O outside a request,
|
|
3
|
+
// so it runs the same driver Worker (runtime/workerd-worker.mjs) over the
|
|
4
|
+
// same WebSocket session (src/workerd-target.ts). The differences:
|
|
5
|
+
// - celld has no UnsafeEval binding and doesn't need one: the driver uses
|
|
6
|
+
// a plain global eval.
|
|
7
|
+
// - It listens only on TCP, so the session is a loopback WebSocket opened
|
|
8
|
+
// with a per-launch token (in the query string: the host's built-in
|
|
9
|
+
// WebSocket can't set headers).
|
|
10
|
+
// - It takes one module, so the host bundles the driver (and a project's
|
|
11
|
+
// main) with esbuild and hands celld the result with `no_bundle`.
|
|
12
|
+
// In a celld project (a wrangler.jsonc or wrangler.json at or above the
|
|
13
|
+
// notebook), its bindings come along: the generated config copies them, and
|
|
14
|
+
// lives in the project's .celld/cdp-kernel, so state stays beside, not in,
|
|
15
|
+
// `celld dev`'s own.
|
|
16
|
+
import process from "node:process";
|
|
17
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
18
|
+
import { randomBytes } from "node:crypto";
|
|
19
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { createServer } from "node:net";
|
|
21
|
+
import { homedir, tmpdir } from "node:os";
|
|
22
|
+
import { dirname, join, resolve } from "node:path";
|
|
23
|
+
import { build } from "esbuild";
|
|
24
|
+
import { parseJsonc } from "./deno-config.js";
|
|
25
|
+
import { runtimeDir } from "./package.js";
|
|
26
|
+
import { cellMeta, WorkerdTarget } from "./workerd-target.js";
|
|
27
|
+
import { builtinAliases, findWranglerConfig } from "./workers-project.js";
|
|
28
|
+
/** The project keys the generated config keeps: bindings and compatibility. */
|
|
29
|
+
export const PROJECT_KEYS = [
|
|
30
|
+
"compatibility_date",
|
|
31
|
+
"compatibility_flags",
|
|
32
|
+
"durable_objects",
|
|
33
|
+
"migrations",
|
|
34
|
+
"vars",
|
|
35
|
+
"d1_databases",
|
|
36
|
+
"kv_namespaces",
|
|
37
|
+
"r2_buckets",
|
|
38
|
+
"queues",
|
|
39
|
+
"workflows",
|
|
40
|
+
"services",
|
|
41
|
+
"worker_loaders",
|
|
42
|
+
];
|
|
43
|
+
/**
|
|
44
|
+
* The celld config for the driver: the project's bindings (or none), the
|
|
45
|
+
* pre-built driver as its one module, and the kernel's token and import.meta
|
|
46
|
+
* as vars, which the driver hides from cells' `env`.
|
|
47
|
+
*/
|
|
48
|
+
export function celldConfig(project, { token, meta }) {
|
|
49
|
+
const config = { name: "cdpk", main: "driver.mjs", no_bundle: true, compatibility_date: "2026-09-01" };
|
|
50
|
+
for (const key of PROJECT_KEYS)
|
|
51
|
+
if (project && project[key] !== undefined)
|
|
52
|
+
config[key] = project[key];
|
|
53
|
+
const vars = project && typeof project.vars === "object" && project.vars !== null ? project.vars : {};
|
|
54
|
+
config.vars = { ...vars, TOKEN: token, META: JSON.stringify(meta) };
|
|
55
|
+
return config;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* celld's installer puts it in ~/.local/bin, which a Jupyter server's PATH
|
|
59
|
+
* often lacks, so look there before falling back to PATH.
|
|
60
|
+
*/
|
|
61
|
+
export function findCelld(home = homedir()) {
|
|
62
|
+
const installed = join(home, ".local", "bin", "celld");
|
|
63
|
+
return existsSync(installed) ? installed : "celld";
|
|
64
|
+
}
|
|
65
|
+
/** The version in `celld --version`'s output ("celld X.Y.Z"). */
|
|
66
|
+
export function celldVersionOf(output) {
|
|
67
|
+
return /\bcelld (\d+\.\d+\.\d+\S*)/.exec(output)?.[1] ?? "";
|
|
68
|
+
}
|
|
69
|
+
/** The driver, and a project's main, as the single module celld runs. */
|
|
70
|
+
export async function bundleDriver(runtime, projectMain, define = {}) {
|
|
71
|
+
const driver = JSON.stringify(join(runtime, "workerd-worker.mjs"));
|
|
72
|
+
const contents = projectMain === undefined ? `export { default } from ${driver};\n` : [
|
|
73
|
+
`import * as project from ${JSON.stringify(projectMain)};`,
|
|
74
|
+
`export * from ${JSON.stringify(projectMain)};`,
|
|
75
|
+
`export { default } from ${driver};`,
|
|
76
|
+
"globalThis.worker = project.default;",
|
|
77
|
+
"",
|
|
78
|
+
].join("\n");
|
|
79
|
+
const result = await build({
|
|
80
|
+
stdin: { contents, resolveDir: runtime, loader: "js" },
|
|
81
|
+
bundle: true,
|
|
82
|
+
write: false,
|
|
83
|
+
format: "esm",
|
|
84
|
+
platform: "browser",
|
|
85
|
+
conditions: ["workerd", "worker", "browser"],
|
|
86
|
+
mainFields: ["browser", "module", "main"],
|
|
87
|
+
alias: builtinAliases(),
|
|
88
|
+
external: ["node:*", "cloudflare:*"],
|
|
89
|
+
define,
|
|
90
|
+
target: "es2024",
|
|
91
|
+
logLevel: "silent",
|
|
92
|
+
});
|
|
93
|
+
return result.outputFiles[0].text;
|
|
94
|
+
}
|
|
95
|
+
/** A free loopback port: celld dev needs a number, and rejects 0. */
|
|
96
|
+
export function freePort() {
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
const server = createServer();
|
|
99
|
+
server.once("error", reject);
|
|
100
|
+
server.listen(0, "127.0.0.1", () => {
|
|
101
|
+
const address = server.address();
|
|
102
|
+
const port = typeof address === "object" && address !== null ? address.port : 0;
|
|
103
|
+
server.close(() => resolve(port));
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/** The host's built-in WebSocket as the session channel. */
|
|
108
|
+
function channelOf(ws) {
|
|
109
|
+
return {
|
|
110
|
+
send: (text) => ws.send(text),
|
|
111
|
+
onMessage: (listener) => ws.addEventListener("message", (event) => listener(String(event.data))),
|
|
112
|
+
onClose: (listener) => ws.addEventListener("close", () => listener()),
|
|
113
|
+
close() {
|
|
114
|
+
try {
|
|
115
|
+
ws.close();
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Already closed.
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/** celld dev's own children, which run in their own process groups. */
|
|
124
|
+
function childPids(pid) {
|
|
125
|
+
try {
|
|
126
|
+
return execFileSync("pgrep", ["-P", String(pid)], { encoding: "utf8" }).split(/\s+/).filter(Boolean).map(Number);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return []; // none
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** Kills celld dev's node, then the supervisor itself. */
|
|
133
|
+
function killTree(child) {
|
|
134
|
+
for (const pid of childPids(child.pid ?? -1)) {
|
|
135
|
+
try {
|
|
136
|
+
process.kill(pid, "SIGKILL");
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// Gone already.
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
child.kill("SIGKILL");
|
|
143
|
+
}
|
|
144
|
+
/** The driver on `celld dev`. */
|
|
145
|
+
class CelldHost {
|
|
146
|
+
runtime;
|
|
147
|
+
project;
|
|
148
|
+
channel;
|
|
149
|
+
#child;
|
|
150
|
+
constructor(child, channel, runtime, project) {
|
|
151
|
+
this.#child = child;
|
|
152
|
+
this.channel = channel;
|
|
153
|
+
this.runtime = runtime;
|
|
154
|
+
this.project = project;
|
|
155
|
+
}
|
|
156
|
+
static async start(cwd, projectConfig, executable) {
|
|
157
|
+
const celld = executable ?? findCelld();
|
|
158
|
+
const version = celldVersionOf(execFileSync(celld, ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }));
|
|
159
|
+
const source = await runtimeDir(new URL("../runtime/", import.meta.url).href);
|
|
160
|
+
let project = null;
|
|
161
|
+
let dir;
|
|
162
|
+
if (projectConfig !== null) {
|
|
163
|
+
if (projectConfig.endsWith(".toml"))
|
|
164
|
+
throw new Error("celld reads wrangler.jsonc or wrangler.json, not wrangler.toml");
|
|
165
|
+
const parsed = parseJsonc(readFileSync(projectConfig, "utf8"));
|
|
166
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
167
|
+
throw new Error(`${projectConfig} is not an object`);
|
|
168
|
+
project = parsed;
|
|
169
|
+
dir = join(dirname(projectConfig), ".celld", "cdp-kernel");
|
|
170
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
dir = mkdtempSync(join(tmpdir(), "cdpk-celld-"));
|
|
174
|
+
}
|
|
175
|
+
const main = project && typeof project.main === "string" ? resolve(dirname(projectConfig ?? cwd), project.main) : undefined;
|
|
176
|
+
const define = project && typeof project.define === "object" && project.define !== null
|
|
177
|
+
? project.define
|
|
178
|
+
: {};
|
|
179
|
+
const token = randomBytes(32).toString("hex");
|
|
180
|
+
// 0600: the config carries this launch's token.
|
|
181
|
+
writeFileSync(join(dir, "driver.mjs"), await bundleDriver(source, main, define));
|
|
182
|
+
writeFileSync(join(dir, "wrangler.jsonc"), JSON.stringify(celldConfig(project, { token, meta: cellMeta(cwd) }), null, 2), {
|
|
183
|
+
mode: 0o600,
|
|
184
|
+
});
|
|
185
|
+
const port = await freePort();
|
|
186
|
+
const child = spawn(celld, ["dev", dir, "--host", "127.0.0.1", "--port", String(port), "--no-watch"], {
|
|
187
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
188
|
+
});
|
|
189
|
+
child.once("exit", () => {
|
|
190
|
+
if (projectConfig === null)
|
|
191
|
+
rmSync(dir, { recursive: true, force: true });
|
|
192
|
+
else
|
|
193
|
+
for (const name of ["driver.mjs", "wrangler.jsonc"])
|
|
194
|
+
rmSync(join(dir, name), { force: true });
|
|
195
|
+
});
|
|
196
|
+
// Cell output is captured in the driver; celld's own lines go to the
|
|
197
|
+
// server log.
|
|
198
|
+
let early = "";
|
|
199
|
+
const log = (chunk) => {
|
|
200
|
+
early += chunk.toString();
|
|
201
|
+
process.stderr.write(`[cdp-kernel celld] ${chunk}`);
|
|
202
|
+
};
|
|
203
|
+
child.stdout?.on("data", log);
|
|
204
|
+
child.stderr?.on("data", log);
|
|
205
|
+
const exited = new Promise((_, reject) => {
|
|
206
|
+
child.once("error", reject);
|
|
207
|
+
child.once("exit", (code) => reject(new Error(`celld exited (${code}) before it was ready:\n${early}`)));
|
|
208
|
+
});
|
|
209
|
+
exited.catch(() => { });
|
|
210
|
+
const url = `127.0.0.1:${port}/?token=${token}`;
|
|
211
|
+
const ready = (async () => {
|
|
212
|
+
// The driver answers 426 to a plain request once celld serves it.
|
|
213
|
+
for (let i = 0;; i++) {
|
|
214
|
+
const status = await fetch(`http://${url}`).then((r) => r.status, () => 0);
|
|
215
|
+
if (status === 426)
|
|
216
|
+
break;
|
|
217
|
+
if (i >= 600)
|
|
218
|
+
throw new Error(`celld never served the driver (last status ${status}):\n${early}`);
|
|
219
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
220
|
+
}
|
|
221
|
+
const ws = new WebSocket(`ws://${url}`);
|
|
222
|
+
await new Promise((resolve, reject) => {
|
|
223
|
+
ws.addEventListener("open", () => resolve(), { once: true });
|
|
224
|
+
ws.addEventListener("error", () => reject(new Error("the session WebSocket didn't open")), { once: true });
|
|
225
|
+
});
|
|
226
|
+
return channelOf(ws);
|
|
227
|
+
})();
|
|
228
|
+
try {
|
|
229
|
+
return new CelldHost(child, await Promise.race([ready, exited]), `celld ${version}`.trim(), projectConfig ?? undefined);
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
killTree(child);
|
|
233
|
+
throw err;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* celld dev's graceful stop waits for the app, which a busy cell never
|
|
238
|
+
* lets go, and its node runs in its own process group. So a forced stop
|
|
239
|
+
* kills the node, then the supervisor; a graceful one falls back to that.
|
|
240
|
+
*/
|
|
241
|
+
stop(now) {
|
|
242
|
+
if (now)
|
|
243
|
+
return killTree(this.#child);
|
|
244
|
+
this.#child.kill("SIGTERM");
|
|
245
|
+
setTimeout(() => {
|
|
246
|
+
if (this.#child.exitCode === null && this.#child.signalCode === null)
|
|
247
|
+
killTree(this.#child);
|
|
248
|
+
}, 3000).unref();
|
|
249
|
+
}
|
|
250
|
+
onExit(listener) {
|
|
251
|
+
this.#child.once("exit", listener);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* A celld target. In a celld project its bindings come along; a project
|
|
256
|
+
* that can't load is reported in the first cell, and the driver runs alone.
|
|
257
|
+
*/
|
|
258
|
+
export async function launchCelld(events, options = {}) {
|
|
259
|
+
const cwd = options.cwd ?? process.cwd();
|
|
260
|
+
const project = findWranglerConfig(cwd);
|
|
261
|
+
let host = null;
|
|
262
|
+
let notice;
|
|
263
|
+
if (project !== null) {
|
|
264
|
+
try {
|
|
265
|
+
host = await CelldHost.start(cwd, project, options.executable);
|
|
266
|
+
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
269
|
+
notice = `[cdp-kernel] couldn't load the celld project at ${project}: ${reason}; running without its bindings`;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
host ??= await CelldHost.start(cwd, null, options.executable);
|
|
273
|
+
return await WorkerdTarget.open(host, cwd, events, "celld", notice);
|
|
274
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { CellTransformOptions, JsxOptions } from "./transform.ts";
|
|
2
|
+
export type { CellTransformOptions, JsxOptions };
|
|
3
|
+
/** Deno looks for these, in this order, in each directory. */
|
|
4
|
+
export declare const DENO_CONFIG_NAMES: readonly ["deno.json", "deno.jsonc"];
|
|
5
|
+
/** The nearest deno.json(c) at or above `startDir`, as Deno resolves it. */
|
|
6
|
+
export declare function findDenoConfig(startDir: string): string | null;
|
|
7
|
+
/**
|
|
8
|
+
* Parses JSON with comments and trailing commas (deno.jsonc). Comments and
|
|
9
|
+
* trailing commas are removed outside strings, then the rest goes to
|
|
10
|
+
* JSON.parse, so errors are JSON.parse's SyntaxErrors.
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseJsonc(text: string): unknown;
|
|
13
|
+
/** Maps a deno.json's compilerOptions onto the cell transformer's options. */
|
|
14
|
+
export declare function transformOptionsFromConfig(config: unknown): CellTransformOptions;
|
|
15
|
+
export interface DenoProject {
|
|
16
|
+
configPath: string;
|
|
17
|
+
transform: CellTransformOptions;
|
|
18
|
+
}
|
|
19
|
+
/** The Deno project a notebook directory belongs to, if any. */
|
|
20
|
+
export declare function loadDenoProject(notebookDir: string): DenoProject | null;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// A notebook's Deno project config, for Deno targets.
|
|
2
|
+
//
|
|
3
|
+
// `deno jupyter` honours the project's deno.json: its import map,
|
|
4
|
+
// nodeModulesDir and JSX/decorator compilerOptions. Here those come from two
|
|
5
|
+
// places: the target runtime gets `--config <path>` (import map, npm
|
|
6
|
+
// resolution), and the host's cell transformer gets the compiler options,
|
|
7
|
+
// because cells are transpiled by esbuild in the host, not by Deno.
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
/** Deno looks for these, in this order, in each directory. */
|
|
11
|
+
export const DENO_CONFIG_NAMES = ["deno.json", "deno.jsonc"];
|
|
12
|
+
/** The nearest deno.json(c) at or above `startDir`, as Deno resolves it. */
|
|
13
|
+
export function findDenoConfig(startDir) {
|
|
14
|
+
for (let dir = startDir;; dir = dirname(dir)) {
|
|
15
|
+
for (const name of DENO_CONFIG_NAMES) {
|
|
16
|
+
const candidate = join(dir, name);
|
|
17
|
+
if (existsSync(candidate))
|
|
18
|
+
return candidate;
|
|
19
|
+
}
|
|
20
|
+
if (dirname(dir) === dir)
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Parses JSON with comments and trailing commas (deno.jsonc). Comments and
|
|
26
|
+
* trailing commas are removed outside strings, then the rest goes to
|
|
27
|
+
* JSON.parse, so errors are JSON.parse's SyntaxErrors.
|
|
28
|
+
*/
|
|
29
|
+
export function parseJsonc(text) {
|
|
30
|
+
let out = "";
|
|
31
|
+
let i = 0;
|
|
32
|
+
while (i < text.length) {
|
|
33
|
+
const ch = text[i];
|
|
34
|
+
if (ch === '"') {
|
|
35
|
+
const start = i++;
|
|
36
|
+
while (i < text.length && text[i] !== '"')
|
|
37
|
+
i += text[i] === "\\" ? 2 : 1;
|
|
38
|
+
out += text.slice(start, ++i);
|
|
39
|
+
}
|
|
40
|
+
else if (ch === "/" && text[i + 1] === "/") {
|
|
41
|
+
while (i < text.length && text[i] !== "\n")
|
|
42
|
+
i++;
|
|
43
|
+
}
|
|
44
|
+
else if (ch === "/" && text[i + 1] === "*") {
|
|
45
|
+
const end = text.indexOf("*/", i + 2);
|
|
46
|
+
i = end === -1 ? text.length : end + 2;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
out += ch;
|
|
50
|
+
i++;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return JSON.parse(stripTrailingCommas(out));
|
|
54
|
+
}
|
|
55
|
+
/** Drops commas that only whitespace separates from a closing } or ]. */
|
|
56
|
+
function stripTrailingCommas(json) {
|
|
57
|
+
// Walks strings intact for the same reason as above: a string can
|
|
58
|
+
// legitimately contain ",}".
|
|
59
|
+
let out = "";
|
|
60
|
+
for (let i = 0; i < json.length; i++) {
|
|
61
|
+
const ch = json[i];
|
|
62
|
+
if (ch === '"') {
|
|
63
|
+
const start = i++;
|
|
64
|
+
while (i < json.length && json[i] !== '"')
|
|
65
|
+
i += json[i] === "\\" ? 2 : 1;
|
|
66
|
+
out += json.slice(start, i + 1);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (ch === ",") {
|
|
70
|
+
let j = i + 1;
|
|
71
|
+
while (j < json.length && /\s/.test(json[j]))
|
|
72
|
+
j++;
|
|
73
|
+
if (json[j] === "}" || json[j] === "]")
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
out += ch;
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
function stringField(obj, key) {
|
|
81
|
+
const value = obj[key];
|
|
82
|
+
return typeof value === "string" ? value : undefined;
|
|
83
|
+
}
|
|
84
|
+
/** Maps a deno.json's compilerOptions onto the cell transformer's options. */
|
|
85
|
+
export function transformOptionsFromConfig(config) {
|
|
86
|
+
if (typeof config !== "object" || config === null)
|
|
87
|
+
return {};
|
|
88
|
+
const compilerOptions = config.compilerOptions;
|
|
89
|
+
if (typeof compilerOptions !== "object" || compilerOptions === null)
|
|
90
|
+
return {};
|
|
91
|
+
const opts = compilerOptions;
|
|
92
|
+
const result = {};
|
|
93
|
+
const jsx = stringField(opts, "jsx");
|
|
94
|
+
if (jsx === "react-jsx" || jsx === "react-jsxdev" || jsx === "precompile") {
|
|
95
|
+
// precompile is Deno's server-side optimisation of the automatic
|
|
96
|
+
// runtime; for a notebook cell the automatic runtime is equivalent.
|
|
97
|
+
const importSource = stringField(opts, "jsxImportSource");
|
|
98
|
+
result.jsx = { mode: "automatic", development: jsx === "react-jsxdev" };
|
|
99
|
+
if (importSource !== undefined)
|
|
100
|
+
result.jsx.importSource = importSource;
|
|
101
|
+
}
|
|
102
|
+
else if (jsx === "react") {
|
|
103
|
+
const factory = stringField(opts, "jsxFactory");
|
|
104
|
+
const fragment = stringField(opts, "jsxFragmentFactory");
|
|
105
|
+
const classic = { mode: "classic" };
|
|
106
|
+
if (factory !== undefined)
|
|
107
|
+
classic.factory = factory;
|
|
108
|
+
if (fragment !== undefined)
|
|
109
|
+
classic.fragment = fragment;
|
|
110
|
+
result.jsx = classic;
|
|
111
|
+
}
|
|
112
|
+
if (opts.experimentalDecorators === true)
|
|
113
|
+
result.experimentalDecorators = true;
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
/** The Deno project a notebook directory belongs to, if any. */
|
|
117
|
+
export function loadDenoProject(notebookDir) {
|
|
118
|
+
const configPath = findDenoConfig(notebookDir);
|
|
119
|
+
if (configPath === null)
|
|
120
|
+
return null;
|
|
121
|
+
let config;
|
|
122
|
+
try {
|
|
123
|
+
config = parseJsonc(readFileSync(configPath, "utf8"));
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
127
|
+
throw new Error(`cannot read Deno config ${configPath}: ${reason}`);
|
|
128
|
+
}
|
|
129
|
+
return { configPath, transform: transformOptionsFromConfig(config) };
|
|
130
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ambient `Deno` namespace for Node notebooks run with `--deno-compat`
|
|
3
|
+
* (kernelspec `cdp-node-deno`), on top of the `jupyter` globals.
|
|
4
|
+
*
|
|
5
|
+
* Opt-in and separate from `./globals` because on a real Deno target, or
|
|
6
|
+
* with Deno's own types loaded, `Deno` is already declared.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import type { DenoShim } from "../runtime/deno-compat.mjs";
|
|
11
|
+
import type { JupyterNamespace } from "./globals.ts";
|
|
12
|
+
export type { DenoShim };
|
|
13
|
+
declare global {
|
|
14
|
+
/**
|
|
15
|
+
* The subset of Deno's namespace the compatibility shim implements on Node.
|
|
16
|
+
* APIs outside it (`Deno.serve`, `Deno.Command`, ...) are absent at runtime.
|
|
17
|
+
*/
|
|
18
|
+
var Deno: Omit<DenoShim, "jupyter"> & {
|
|
19
|
+
jupyter: JupyterNamespace;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ambient `Deno` namespace for Node notebooks run with `--deno-compat`
|
|
3
|
+
* (kernelspec `cdp-node-deno`), on top of the `jupyter` globals.
|
|
4
|
+
*
|
|
5
|
+
* Opt-in and separate from `./globals` because on a real Deno target, or
|
|
6
|
+
* with Deno's own types loaded, `Deno` is already declared.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ambient types for notebook cells run by cdp-kernel: the `jupyter` display
|
|
3
|
+
* namespace and the stdin-backed `prompt`/`confirm`.
|
|
4
|
+
*
|
|
5
|
+
* Load it once per project, for example
|
|
6
|
+
* `/// <reference types="@tlockney/cdp-kernel/globals" />` (npm) or
|
|
7
|
+
* `import type {} from "jsr:@tlockney/cdp-kernel/globals";` (Deno).
|
|
8
|
+
*
|
|
9
|
+
* A custom display is a class with a `[jupyter.$display]()` method returning
|
|
10
|
+
* a media bundle. At runtime that key is `Symbol.for("Jupyter.display")`, but
|
|
11
|
+
* only `jupyter.$display` carries the precise type.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import type { Displayable, DisplayOptions, JupyterNamespace, MediaBundle } from "../runtime/display.mjs";
|
|
16
|
+
export type { Displayable, DisplayOptions, JupyterNamespace, MediaBundle };
|
|
17
|
+
/** Options for the notebook's `prompt()`. */
|
|
18
|
+
export interface PromptOptions {
|
|
19
|
+
/** Ask the frontend to hide what is typed. */
|
|
20
|
+
password?: boolean;
|
|
21
|
+
}
|
|
22
|
+
declare global {
|
|
23
|
+
/**
|
|
24
|
+
* The notebook display API: `display`, `md`/`html`/`svg` templates,
|
|
25
|
+
* `image`, `format`, `broadcast`. The same object as `Deno.jupyter` in
|
|
26
|
+
* `deno jupyter`.
|
|
27
|
+
*/
|
|
28
|
+
var jupyter: JupyterNamespace;
|
|
29
|
+
/**
|
|
30
|
+
* Ask the frontend for a line of input over the Jupyter stdin channel.
|
|
31
|
+
* Returns `null` when the frontend didn't allow input for this cell.
|
|
32
|
+
*/
|
|
33
|
+
function prompt(message?: string, defaultValue?: string, options?: PromptOptions): string | null;
|
|
34
|
+
/** Ask a yes/no question over the Jupyter stdin channel. */
|
|
35
|
+
function confirm(message?: string): boolean;
|
|
36
|
+
}
|
package/dist/globals.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ambient types for notebook cells run by cdp-kernel: the `jupyter` display
|
|
3
|
+
* namespace and the stdin-backed `prompt`/`confirm`.
|
|
4
|
+
*
|
|
5
|
+
* Load it once per project, for example
|
|
6
|
+
* `/// <reference types="@tlockney/cdp-kernel/globals" />` (npm) or
|
|
7
|
+
* `import type {} from "jsr:@tlockney/cdp-kernel/globals";` (Deno).
|
|
8
|
+
*
|
|
9
|
+
* A custom display is a class with a `[jupyter.$display]()` method returning
|
|
10
|
+
* a media bundle. At runtime that key is `Symbol.for("Jupyter.display")`, but
|
|
11
|
+
* only `jupyter.$display` carries the precise type.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
export {};
|
package/dist/kernel.d.ts
CHANGED
|
@@ -14,7 +14,8 @@ export declare function parseConnectionInfo(json: string): ConnectionInfo;
|
|
|
14
14
|
export declare class Kernel {
|
|
15
15
|
#private;
|
|
16
16
|
constructor(conn: ConnectionInfo, runtime: RuntimeName, options?: {
|
|
17
|
-
denoCompat?: boolean;
|
|
17
|
+
denoCompat?: boolean; /** Defaults to the process cwd, which Jupyter sets. */
|
|
18
|
+
notebookDir?: string;
|
|
18
19
|
});
|
|
19
20
|
run(): Promise<void>;
|
|
20
21
|
}
|