@tlockney/cdp-kernel 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,116 @@
1
+ export class NotFound extends Error {
2
+ code?: string;
3
+ }
4
+ export class AlreadyExists extends Error {
5
+ code?: string;
6
+ }
7
+ export class PermissionDenied extends Error {
8
+ code?: string;
9
+ }
10
+ export class IsADirectory extends Error {
11
+ code?: string;
12
+ }
13
+ export class NotADirectory extends Error {
14
+ code?: string;
15
+ }
16
+
17
+ export interface DenoErrors {
18
+ NotFound: typeof NotFound;
19
+ AlreadyExists: typeof AlreadyExists;
20
+ PermissionDenied: typeof PermissionDenied;
21
+ IsADirectory: typeof IsADirectory;
22
+ NotADirectory: typeof NotADirectory;
23
+ }
24
+
25
+ export interface FileInfo {
26
+ isFile: boolean;
27
+ isDirectory: boolean;
28
+ isSymlink: boolean;
29
+ size: number;
30
+ mtime: Date | null;
31
+ atime: Date | null;
32
+ birthtime: Date | null;
33
+ mode: number;
34
+ uid: number;
35
+ gid: number;
36
+ }
37
+
38
+ export interface DirEntry {
39
+ name: string;
40
+ isFile: boolean;
41
+ isDirectory: boolean;
42
+ isSymlink: boolean;
43
+ }
44
+
45
+ export type PathLike = string | URL;
46
+
47
+ export interface WriteFileOptions {
48
+ append?: boolean;
49
+ createNew?: boolean;
50
+ }
51
+
52
+ export interface MakeTempOptions {
53
+ dir?: string;
54
+ prefix?: string;
55
+ suffix?: string;
56
+ }
57
+
58
+ export interface DenoEnv {
59
+ get(key: string): string | undefined;
60
+ set(key: string, value: string): void;
61
+ delete(key: string): void;
62
+ has(key: string): boolean;
63
+ toObject(): Record<string, string | undefined>;
64
+ }
65
+
66
+ export interface DenoWriter {
67
+ write(bytes: Uint8Array): Promise<number>;
68
+ writeSync(bytes: Uint8Array): number;
69
+ }
70
+
71
+ export interface DenoShim {
72
+ jupyter: unknown;
73
+ version: { deno: string; v8: string; typescript: string };
74
+ build: { os: string; arch: string; target: string };
75
+ errors: DenoErrors;
76
+ pid: number;
77
+ args: string[];
78
+ noColor: boolean;
79
+ execPath: () => string;
80
+ cwd: () => string;
81
+ chdir: (dir: PathLike) => void;
82
+ exit: (code?: number) => never;
83
+ env: DenoEnv;
84
+ inspect: (value: unknown, options?: object) => string;
85
+
86
+ readTextFile: (path: PathLike) => Promise<string>;
87
+ readTextFileSync: (path: PathLike) => string;
88
+ readFile: (path: PathLike) => Promise<Uint8Array>;
89
+ readFileSync: (path: PathLike) => Uint8Array;
90
+ writeTextFile: (path: PathLike, data: string, options?: WriteFileOptions) => Promise<void>;
91
+ writeTextFileSync: (path: PathLike, data: string, options?: WriteFileOptions) => void;
92
+ writeFile: (path: PathLike, data: Uint8Array | string, options?: WriteFileOptions) => Promise<void>;
93
+ writeFileSync: (path: PathLike, data: Uint8Array | string, options?: WriteFileOptions) => void;
94
+ readDir: (path: PathLike) => AsyncIterable<DirEntry>;
95
+ readDirSync: (path: PathLike) => DirEntry[];
96
+ stat: (path: PathLike) => Promise<FileInfo>;
97
+ statSync: (path: PathLike) => FileInfo;
98
+ lstat: (path: PathLike) => Promise<FileInfo>;
99
+ lstatSync: (path: PathLike) => FileInfo;
100
+ mkdir: (path: PathLike, options?: { recursive?: boolean }) => Promise<void>;
101
+ mkdirSync: (path: PathLike, options?: { recursive?: boolean }) => void;
102
+ remove: (path: PathLike, options?: { recursive?: boolean }) => Promise<void>;
103
+ removeSync: (path: PathLike, options?: { recursive?: boolean }) => void;
104
+ rename: (from: PathLike, to: PathLike) => Promise<void>;
105
+ renameSync: (from: PathLike, to: PathLike) => void;
106
+ realPath: (path: PathLike) => Promise<string>;
107
+ realPathSync: (path: PathLike) => string;
108
+ makeTempDir: (options?: MakeTempOptions) => Promise<string>;
109
+ makeTempDirSync: (options?: MakeTempOptions) => string;
110
+ makeTempFile: (options?: MakeTempOptions) => Promise<string>;
111
+ stdout: DenoWriter;
112
+ stderr: DenoWriter;
113
+ }
114
+
115
+ export function denoError<E>(err: E): E | NotFound | AlreadyExists | PermissionDenied | IsADirectory | NotADirectory;
116
+ export function createDenoShim(options: { jupyter: unknown }): DenoShim;
@@ -0,0 +1,185 @@
1
+ // An opt-in `Deno` namespace for Node targets, so notebooks written for
2
+ // `deno jupyter` keep working: Deno.jupyter plus the file, env and process
3
+ // APIs notebooks commonly reach for, implemented on Node's.
4
+ //
5
+ // Opt-in because libraries feature-detect Deno with `typeof Deno`; with a
6
+ // shim installed they take their Deno code paths, which may call APIs not
7
+ // covered here (Deno.serve, Deno.Command, Deno.openKv, permissions...).
8
+ // Uncovered APIs are simply absent rather than throwing on access, so
9
+ // feature checks like `typeof Deno.serve === "function"` still work.
10
+
11
+ import fs from "node:fs";
12
+ import fsp from "node:fs/promises";
13
+ import os from "node:os";
14
+ import path from "node:path";
15
+ import util from "node:util";
16
+
17
+ class NotFound extends Error {
18
+ name = "NotFound";
19
+ }
20
+ class AlreadyExists extends Error {
21
+ name = "AlreadyExists";
22
+ }
23
+ class PermissionDenied extends Error {
24
+ name = "PermissionDenied";
25
+ }
26
+ class IsADirectory extends Error {
27
+ name = "IsADirectory";
28
+ }
29
+ class NotADirectory extends Error {
30
+ name = "NotADirectory";
31
+ }
32
+
33
+ const ERRNO_TO_ERROR = {
34
+ ENOENT: NotFound,
35
+ EEXIST: AlreadyExists,
36
+ EACCES: PermissionDenied,
37
+ EPERM: PermissionDenied,
38
+ EISDIR: IsADirectory,
39
+ ENOTDIR: NotADirectory,
40
+ };
41
+
42
+ /** Rethrows Node fs errors as the Deno.errors class Deno would use. */
43
+ export function denoError(err) {
44
+ const Ctor = err && ERRNO_TO_ERROR[err.code];
45
+ if (!Ctor) return err;
46
+ const mapped = new Ctor(err.message, { cause: err });
47
+ mapped.code = err.code;
48
+ return mapped;
49
+ }
50
+
51
+ function sync(fn) {
52
+ return (...args) => {
53
+ try {
54
+ return fn(...args);
55
+ } catch (err) {
56
+ throw denoError(err);
57
+ }
58
+ };
59
+ }
60
+
61
+ function async(fn) {
62
+ return async (...args) => {
63
+ try {
64
+ return await fn(...args);
65
+ } catch (err) {
66
+ throw denoError(err);
67
+ }
68
+ };
69
+ }
70
+
71
+ const toPath = (p) => (p instanceof URL ? p : String(p));
72
+
73
+ function fileInfo(stats) {
74
+ return {
75
+ isFile: stats.isFile(),
76
+ isDirectory: stats.isDirectory(),
77
+ isSymlink: stats.isSymbolicLink(),
78
+ size: stats.size,
79
+ mtime: stats.mtime,
80
+ atime: stats.atime,
81
+ birthtime: stats.birthtime,
82
+ mode: stats.mode,
83
+ uid: stats.uid,
84
+ gid: stats.gid,
85
+ };
86
+ }
87
+
88
+ function dirEntry(dirent) {
89
+ return {
90
+ name: dirent.name,
91
+ isFile: dirent.isFile(),
92
+ isDirectory: dirent.isDirectory(),
93
+ isSymlink: dirent.isSymbolicLink(),
94
+ };
95
+ }
96
+
97
+ const writeFlag = (options) => (options?.append ? "a" : options?.createNew ? "wx" : "w");
98
+
99
+ const OS_NAMES = { darwin: "darwin", linux: "linux", win32: "windows", freebsd: "freebsd" };
100
+ const ARCH_NAMES = { arm64: "aarch64", x64: "x86_64" };
101
+
102
+ export function createDenoShim({ jupyter }) {
103
+ return {
104
+ jupyter,
105
+ version: { deno: "0.0.0-cdp-kernel", v8: process.versions.v8, typescript: "5" },
106
+ build: {
107
+ os: OS_NAMES[process.platform] ?? process.platform,
108
+ arch: ARCH_NAMES[process.arch] ?? process.arch,
109
+ target: `${ARCH_NAMES[process.arch] ?? process.arch}-node-${process.platform}`,
110
+ },
111
+ errors: { NotFound, AlreadyExists, PermissionDenied, IsADirectory, NotADirectory },
112
+ pid: process.pid,
113
+ args: process.argv.slice(2),
114
+ noColor: Boolean(process.env.NO_COLOR),
115
+ execPath: () => process.execPath,
116
+ cwd: () => process.cwd(),
117
+ chdir: sync((dir) => process.chdir(toPath(dir))),
118
+ exit: (code) => process.exit(code),
119
+ env: {
120
+ get: (key) => process.env[key],
121
+ set: (key, value) => {
122
+ process.env[key] = value;
123
+ },
124
+ delete: (key) => {
125
+ delete process.env[key];
126
+ },
127
+ has: (key) => Object.hasOwn(process.env, key),
128
+ toObject: () => ({ ...process.env }),
129
+ },
130
+ inspect: (value, options) => util.inspect(value, options),
131
+
132
+ readTextFile: async((p) => fsp.readFile(toPath(p), "utf8")),
133
+ readTextFileSync: sync((p) => fs.readFileSync(toPath(p), "utf8")),
134
+ readFile: async(async (p) => new Uint8Array(await fsp.readFile(toPath(p)))),
135
+ readFileSync: sync((p) => new Uint8Array(fs.readFileSync(toPath(p)))),
136
+ writeTextFile: async((p, data, options) => fsp.writeFile(toPath(p), data, { flag: writeFlag(options) })),
137
+ writeTextFileSync: sync((p, data, options) => fs.writeFileSync(toPath(p), data, { flag: writeFlag(options) })),
138
+ writeFile: async((p, data, options) => fsp.writeFile(toPath(p), data, { flag: writeFlag(options) })),
139
+ writeFileSync: sync((p, data, options) => fs.writeFileSync(toPath(p), data, { flag: writeFlag(options) })),
140
+ async *readDir(p) {
141
+ let entries;
142
+ try {
143
+ entries = await fsp.readdir(toPath(p), { withFileTypes: true });
144
+ } catch (err) {
145
+ throw denoError(err);
146
+ }
147
+ for (const entry of entries) yield dirEntry(entry);
148
+ },
149
+ readDirSync: sync((p) => fs.readdirSync(toPath(p), { withFileTypes: true }).map(dirEntry)),
150
+ stat: async(async (p) => fileInfo(await fsp.stat(toPath(p)))),
151
+ statSync: sync((p) => fileInfo(fs.statSync(toPath(p)))),
152
+ lstat: async(async (p) => fileInfo(await fsp.lstat(toPath(p)))),
153
+ lstatSync: sync((p) => fileInfo(fs.lstatSync(toPath(p)))),
154
+ mkdir: async((p, options) => fsp.mkdir(toPath(p), { recursive: options?.recursive ?? false }).then(() => {})),
155
+ mkdirSync: sync((p, options) => {
156
+ fs.mkdirSync(toPath(p), { recursive: options?.recursive ?? false });
157
+ }),
158
+ remove: async((p, options) => fsp.rm(toPath(p), { recursive: options?.recursive ?? false })),
159
+ removeSync: sync((p, options) => fs.rmSync(toPath(p), { recursive: options?.recursive ?? false })),
160
+ rename: async((from, to) => fsp.rename(toPath(from), toPath(to))),
161
+ renameSync: sync((from, to) => fs.renameSync(toPath(from), toPath(to))),
162
+ realPath: async((p) => fsp.realpath(toPath(p))),
163
+ realPathSync: sync((p) => fs.realpathSync(toPath(p))),
164
+ makeTempDir: async((options) =>
165
+ fsp.mkdtemp(path.join(options?.dir ?? os.tmpdir(), options?.prefix ?? "deno-"))
166
+ ),
167
+ makeTempDirSync: sync((options) =>
168
+ fs.mkdtempSync(path.join(options?.dir ?? os.tmpdir(), options?.prefix ?? "deno-"))
169
+ ),
170
+ makeTempFile: async(async (options) => {
171
+ const dir = await fsp.mkdtemp(path.join(options?.dir ?? os.tmpdir(), "deno-"));
172
+ const file = path.join(dir, `${options?.prefix ?? ""}tmp${options?.suffix ?? ""}`);
173
+ await fsp.writeFile(file, "");
174
+ return file;
175
+ }),
176
+ stdout: {
177
+ write: (bytes) => new Promise((resolve) => process.stdout.write(bytes, () => resolve(bytes.length))),
178
+ writeSync: (bytes) => (process.stdout.write(bytes), bytes.length),
179
+ },
180
+ stderr: {
181
+ write: (bytes) => new Promise((resolve) => process.stderr.write(bytes, () => resolve(bytes.length))),
182
+ writeSync: (bytes) => (process.stderr.write(bytes), bytes.length),
183
+ },
184
+ };
185
+ }
@@ -0,0 +1,48 @@
1
+ export const $display: unique symbol;
2
+
3
+ export type MediaBundle = { [mediaType: string]: unknown };
4
+
5
+ export interface DisplayHost {
6
+ inspect: (value: unknown) => string;
7
+ base64: (bytes: Uint8Array) => string;
8
+ readFile: (path: string) => Uint8Array;
9
+ send: (msgType: string, content: object, metadata: object, buffers: Uint8Array[]) => void;
10
+ input: (prompt: string, password: boolean) => string | null;
11
+ }
12
+
13
+ export interface DisplayOptions {
14
+ raw?: boolean;
15
+ update?: boolean;
16
+ display_id?: string;
17
+ }
18
+
19
+ export interface Displayable {
20
+ [$display]: () => MediaBundle | Promise<MediaBundle>;
21
+ }
22
+
23
+ export interface JupyterNamespace {
24
+ broadcast(
25
+ msgType: string,
26
+ content: object,
27
+ options?: { metadata?: object; buffers?: Uint8Array[] },
28
+ ): void;
29
+ display(obj: unknown, options?: DisplayOptions): Promise<void>;
30
+ format(obj: unknown): Promise<MediaBundle>;
31
+ md(strings: TemplateStringsArray, ...values: unknown[]): Displayable;
32
+ html(strings: TemplateStringsArray, ...values: unknown[]): Displayable;
33
+ svg(strings: TemplateStringsArray, ...values: unknown[]): Displayable;
34
+ image(obj: string | Uint8Array): Displayable;
35
+ $display: typeof $display;
36
+ }
37
+
38
+ export interface Jupyter {
39
+ namespace: JupyterNamespace;
40
+ broadcastResult(executionCount: number, result: unknown): Promise<void>;
41
+ confirm(message?: string): boolean;
42
+ prompt(message?: string, defaultValue?: string, options?: { password?: boolean }): string | null;
43
+ }
44
+
45
+ export function escapeHTML(str: string): string;
46
+ export function isJpg(obj: unknown): boolean;
47
+ export function isPng(obj: unknown): boolean;
48
+ export function createJupyter(host: DisplayHost): Jupyter;
@@ -0,0 +1,237 @@
1
+ // The Jupyter display API that Deno exposes as `Deno.jupyter`, made
2
+ // runtime-neutral. Runs inside the target runtime (Node or Deno).
3
+ //
4
+ // Ported from Deno's cli/js/40_jupyter.js (v2.9.6), which is
5
+ // Copyright 2018-2026 the Deno authors, MIT license. The formatting rules
6
+ // (display symbol, canvas, Vega, Polars-style DataFrames, JPEG/PNG bytes,
7
+ // HTML/SVG elements) are unchanged; the Deno internals it used are injected:
8
+ // Deno[Deno.internal].inspectArgs -> host.inspect
9
+ // op_base64_encode_from_buffer -> host.base64
10
+ // Deno.readFileSync -> host.readFile
11
+ // op_jupyter_broadcast -> host.send
12
+ // WebGPU texture/buffer display depends on Deno's wgpu bindings and is
13
+ // omitted.
14
+
15
+ export const $display = Symbol.for("Jupyter.display");
16
+
17
+ const rawToEntity = new Map([
18
+ ["&", "&amp;"],
19
+ ["<", "&lt;"],
20
+ [">", "&gt;"],
21
+ ['"', "&quot;"],
22
+ ["'", "&#39;"],
23
+ ]);
24
+ const rawRe = new RegExp(`[${[...rawToEntity.keys()].join("")}]`, "g");
25
+
26
+ export function escapeHTML(str) {
27
+ return str.replaceAll(rawRe, (m) => rawToEntity.get(m) ?? m);
28
+ }
29
+
30
+ function isVegaLike(obj) {
31
+ return obj !== null && typeof obj === "object" && "toSpec" in obj;
32
+ }
33
+
34
+ function extractVega(obj) {
35
+ const spec = obj.toSpec();
36
+ if (typeof spec !== "object" || spec === null || !("$schema" in spec)) return null;
37
+ let mediaType = "application/vnd.vega.v5+json";
38
+ if (spec.$schema === "https://vega.github.io/schema/vega-lite/v4.json") {
39
+ mediaType = "application/vnd.vegalite.v4+json";
40
+ } else if (spec.$schema === "https://vega.github.io/schema/vega-lite/v5.json") {
41
+ mediaType = "application/vnd.vegalite.v5+json";
42
+ }
43
+ return { [mediaType]: spec };
44
+ }
45
+
46
+ function isDataFrameLike(obj) {
47
+ return obj !== null && typeof obj === "object" &&
48
+ obj.schema !== undefined && typeof obj.schema === "object" &&
49
+ typeof obj.head === "function" && typeof obj.toRecords === "function";
50
+ }
51
+
52
+ function mapPolarsTypeToJSONSchema(colType) {
53
+ const typeMapping = {
54
+ Null: "null",
55
+ Bool: "boolean",
56
+ Int8: "integer",
57
+ Int16: "integer",
58
+ Int32: "integer",
59
+ Int64: "integer",
60
+ UInt8: "integer",
61
+ UInt16: "integer",
62
+ UInt32: "integer",
63
+ UInt64: "integer",
64
+ Float32: "number",
65
+ Float64: "number",
66
+ Date: "string",
67
+ Datetime: "string",
68
+ Utf8: "string",
69
+ Categorical: "string",
70
+ List: "array",
71
+ Struct: "object",
72
+ };
73
+ // Polars DataTypes only expose their name through toJSON().
74
+ const dataType = typeof colType?.toJSON === "function" ? colType.toJSON()["DataType"] : undefined;
75
+ return typeMapping[dataType] || "string";
76
+ }
77
+
78
+ function extractDataFrame(df) {
79
+ const fields = [];
80
+ for (const [name, colType] of Object.entries(df.schema)) {
81
+ fields.push({ name, type: mapPolarsTypeToJSONSchema(colType) });
82
+ }
83
+ const data = df.head(50).toRecords();
84
+ let html = "<table><thead><tr>";
85
+ for (const field of fields) html += `<th>${escapeHTML(String(field.name))}</th>`;
86
+ html += "</tr></thead><tbody>";
87
+ for (const row of df.head(10).toRecords()) {
88
+ html += "<tr>";
89
+ for (const field of fields) html += `<td>${escapeHTML(String(row[field.name]))}</td>`;
90
+ html += "</tr>";
91
+ }
92
+ html += "</tbody></table>";
93
+ return {
94
+ "application/vnd.dataresource+json": { data, schema: { fields } },
95
+ "text/html": html,
96
+ };
97
+ }
98
+
99
+ function isCanvasLike(obj) {
100
+ return obj !== null && typeof obj === "object" && "toDataURL" in obj;
101
+ }
102
+
103
+ export function isJpg(obj) {
104
+ return obj instanceof Uint8Array && obj.length >= 4 &&
105
+ obj[0] === 0xff && obj[1] === 0xd8 &&
106
+ obj[obj.length - 2] === 0xff && obj[obj.length - 1] === 0xd9;
107
+ }
108
+
109
+ const PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
110
+
111
+ export function isPng(obj) {
112
+ return obj instanceof Uint8Array && obj.length >= PNG_SIGNATURE.length &&
113
+ PNG_SIGNATURE.every((byte, i) => obj[i] === byte);
114
+ }
115
+
116
+ function isSVGElementLike(obj) {
117
+ return obj !== null && typeof obj === "object" && typeof obj.outerHTML === "string" &&
118
+ obj.outerHTML.startsWith("<svg");
119
+ }
120
+
121
+ function isHTMLElementLike(obj) {
122
+ return obj !== null && typeof obj === "object" && typeof obj.outerHTML === "string";
123
+ }
124
+
125
+ function hasDisplaySymbol(obj) {
126
+ return obj !== null && typeof obj === "object" && typeof obj[$display] === "function";
127
+ }
128
+
129
+ function isMediaBundle(obj) {
130
+ return obj !== null && typeof obj === "object" && !Array.isArray(obj);
131
+ }
132
+
133
+ /**
134
+ * @param {{
135
+ * inspect: (value: unknown) => string,
136
+ * base64: (bytes: Uint8Array) => string,
137
+ * readFile: (path: string) => Uint8Array,
138
+ * send: (msgType: string, content: object, metadata: object, buffers: Uint8Array[]) => void,
139
+ * input: (prompt: string, password: boolean) => string | null,
140
+ * }} host
141
+ */
142
+ export function createJupyter(host) {
143
+ async function format(obj) {
144
+ if (hasDisplaySymbol(obj)) return await obj[$display]();
145
+ if (typeof obj !== "object" || obj === null) return { "text/plain": host.inspect(obj) };
146
+ if (isCanvasLike(obj)) {
147
+ const [prefix, data] = obj.toDataURL().split(",");
148
+ return { [prefix.split(":")[1].split(";")[0]]: data };
149
+ }
150
+ if (isVegaLike(obj)) {
151
+ const vega = extractVega(obj);
152
+ if (vega) return vega;
153
+ }
154
+ if (isDataFrameLike(obj)) return extractDataFrame(obj);
155
+ if (isJpg(obj)) return { "image/jpeg": host.base64(obj) };
156
+ if (isPng(obj)) return { "image/png": host.base64(obj) };
157
+ if (isSVGElementLike(obj)) return { "image/svg+xml": obj.outerHTML };
158
+ if (isHTMLElementLike(obj)) return { "text/html": obj.outerHTML };
159
+ return { "text/plain": host.inspect(obj) };
160
+ }
161
+
162
+ function taggedTemplate(mediatype) {
163
+ return (strings, ...values) => {
164
+ const payload = strings.reduce((acc, string, i) =>
165
+ acc + string + (values[i] !== undefined ? values[i] : ""), "");
166
+ return { [$display]: () => ({ [mediatype]: payload }) };
167
+ };
168
+ }
169
+
170
+ function image(obj) {
171
+ if (typeof obj === "string") obj = host.readFile(obj);
172
+ if (isJpg(obj)) return { [$display]: () => ({ "image/jpeg": host.base64(obj) }) };
173
+ if (isPng(obj)) return { [$display]: () => ({ "image/png": host.base64(obj) }) };
174
+ throw new TypeError(
175
+ "Object is not a valid image or a path to an image. `jupyter.image` supports displaying JPG or PNG images.",
176
+ );
177
+ }
178
+
179
+ function broadcast(msgType, content, { metadata = {}, buffers = [] } = {}) {
180
+ host.send(msgType, content, metadata, buffers);
181
+ }
182
+
183
+ async function display(obj, options = { raw: false, update: false }) {
184
+ const bundle = options.raw && isMediaBundle(obj) ? obj : await format(obj);
185
+ broadcast(options.update ? "update_display_data" : "display_data", {
186
+ data: bundle,
187
+ metadata: {},
188
+ transient: options.display_id ? { display_id: options.display_id } : {},
189
+ });
190
+ }
191
+
192
+ async function broadcastResult(executionCount, result) {
193
+ if (result === undefined) return;
194
+ try {
195
+ broadcast("execute_result", {
196
+ execution_count: executionCount,
197
+ data: await format(result),
198
+ metadata: {},
199
+ // nbclient crashes on a null `transient`; Deno learned this too.
200
+ transient: {},
201
+ });
202
+ } catch (err) {
203
+ broadcast("error", {
204
+ ename: err instanceof Error ? err.name : "Error",
205
+ evalue: err instanceof Error ? err.message : String(err),
206
+ traceback: err instanceof Error && err.stack ? err.stack.split("\n") : [],
207
+ });
208
+ }
209
+ }
210
+
211
+ function confirm(message = "Confirm") {
212
+ const answer = host.input(`${message} [y/N] `, false);
213
+ return answer === "Y" || answer === "y";
214
+ }
215
+
216
+ function prompt(message = "Prompt", defaultValue = "", { password = false } = {}) {
217
+ if (defaultValue != "") message += ` [${defaultValue}]`;
218
+ const answer = host.input(message, password);
219
+ return answer === "" ? defaultValue : answer;
220
+ }
221
+
222
+ return {
223
+ namespace: {
224
+ broadcast,
225
+ display,
226
+ format,
227
+ md: taggedTemplate("text/markdown"),
228
+ html: taggedTemplate("text/html"),
229
+ svg: taggedTemplate("image/svg+xml"),
230
+ image,
231
+ $display,
232
+ },
233
+ broadcastResult,
234
+ confirm,
235
+ prompt,
236
+ };
237
+ }
@@ -0,0 +1,10 @@
1
+ export interface PackageSpecifier {
2
+ name: string;
3
+ range: string;
4
+ subpath: string;
5
+ }
6
+
7
+ export function parsePackageSpecifier(spec: string): PackageSpecifier;
8
+ export function jsrToNpm(spec: string): PackageSpecifier;
9
+ export function aliasFor(name: string, range: string): string;
10
+ export function installNodeResolveHooks(urls: { shimUrl: string; replUrl: string }): void;
@@ -0,0 +1,87 @@
1
+ // Node-only module resolution for cell code: the part of Deno's module
2
+ // loader a notebook leans on most.
3
+ // - Imports issued by cell code arrive with the bootstrap module as their
4
+ // parent; re-parent them to the notebook's directory so "./x.ts" and
5
+ // bare package names resolve where the user expects.
6
+ // - npm:<name>[@range][/subpath] installs on first use into a per-user
7
+ // cache, each request under its own npm alias so different ranges of
8
+ // one package can coexist (Deno keeps a global cache the same way).
9
+ // - jsr:@scope/name[@range][/subpath] maps onto JSR's npm compatibility
10
+ // registry (npm.jsr.io) as @jsr/scope__name.
11
+ // Uses module.registerHooks (synchronous, in-thread; Node >= 23.5), so the
12
+ // install can run with spawnSync inside resolve.
13
+
14
+ import { registerHooks } from "node:module";
15
+ import { spawnSync } from "node:child_process";
16
+ import { createHash } from "node:crypto";
17
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
18
+ import { homedir } from "node:os";
19
+ import { join } from "node:path";
20
+ import { pathToFileURL } from "node:url";
21
+
22
+ const SPEC_RE = /^((?:@[^/@]+\/)?[^/@]+)(?:@([^/]+))?(\/.*)?$/;
23
+
24
+ /** Splits "name@range/sub/path" (name may be scoped). */
25
+ export function parsePackageSpecifier(spec) {
26
+ const m = SPEC_RE.exec(spec);
27
+ if (!m) throw new TypeError(`invalid package specifier: ${spec}`);
28
+ return { name: m[1], range: m[2] ?? "*", subpath: m[3] ?? "" };
29
+ }
30
+
31
+ /** jsr:@std/path@^1/posix -> npm @jsr/std__path@^1 /posix */
32
+ export function jsrToNpm(spec) {
33
+ const { name, range, subpath } = parsePackageSpecifier(spec);
34
+ const m = /^@([^/]+)\/(.+)$/.exec(name);
35
+ if (!m) throw new TypeError(`jsr packages are scoped: jsr:${spec}`);
36
+ return { name: `@jsr/${m[1]}__${m[2]}`, range, subpath };
37
+ }
38
+
39
+ export function aliasFor(name, range) {
40
+ const digest = createHash("sha256").update(`${name}@${range}`).digest("hex").slice(0, 12);
41
+ return `cdpk-${digest}`;
42
+ }
43
+
44
+ function cacheDir() {
45
+ const dir = process.env.CDPK_CACHE_DIR ?? join(homedir(), ".cache", "cdp-kernel", "npm");
46
+ if (!existsSync(join(dir, "package.json"))) {
47
+ mkdirSync(dir, { recursive: true });
48
+ writeFileSync(join(dir, "package.json"), '{ "private": true }\n');
49
+ writeFileSync(join(dir, ".npmrc"), "@jsr:registry=https://npm.jsr.io\n");
50
+ }
51
+ return dir;
52
+ }
53
+
54
+ function ensureInstalled({ name, range }) {
55
+ const dir = cacheDir();
56
+ const alias = aliasFor(name, range);
57
+ if (!existsSync(join(dir, "node_modules", alias, "package.json"))) {
58
+ const result = spawnSync(
59
+ "npm",
60
+ ["install", "--no-audit", "--no-fund", "--loglevel=error", `${alias}@npm:${name}@${range}`],
61
+ { cwd: dir, encoding: "utf8" },
62
+ );
63
+ if (result.status !== 0) {
64
+ throw new Error(`npm install ${name}@${range} failed:\n${result.stderr || result.error}`);
65
+ }
66
+ }
67
+ return { dir, alias };
68
+ }
69
+
70
+ export function installNodeResolveHooks({ shimUrl, replUrl }) {
71
+ registerHooks({
72
+ resolve(specifier, context, nextResolve) {
73
+ let pkg = null;
74
+ if (specifier.startsWith("npm:")) pkg = parsePackageSpecifier(specifier.slice(4));
75
+ else if (specifier.startsWith("jsr:")) pkg = jsrToNpm(specifier.slice(4));
76
+ if (pkg) {
77
+ const { dir, alias } = ensureInstalled(pkg);
78
+ const parentURL = pathToFileURL(join(dir, "package.json")).href;
79
+ return nextResolve(alias + pkg.subpath, { ...context, parentURL });
80
+ }
81
+ if (context.parentURL === shimUrl) {
82
+ return nextResolve(specifier, { ...context, parentURL: replUrl });
83
+ }
84
+ return nextResolve(specifier, context);
85
+ },
86
+ });
87
+ }