@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/README.md
CHANGED
|
@@ -11,7 +11,10 @@ own Jupyter integration tests on both targets, and it's packaged for npm and
|
|
|
11
11
|
JSR as `@tlockney/cdp-kernel`, but it hasn't seen much real use yet. The
|
|
12
12
|
feasibility study behind it covers the design,
|
|
13
13
|
the other options considered (workerd, Bun, rusty_v8, FFI), and the
|
|
14
|
-
evidence: [
|
|
14
|
+
evidence: [read it online](https://claude.ai/artifact/S66TnTmhCzPy2EW7jbeeue)
|
|
15
|
+
(also in the repo as
|
|
16
|
+
[`docs/life-after-deno-jupyter.html`](docs/life-after-deno-jupyter.html)). It
|
|
17
|
+
was written before packaging, so it calls the project a spike.
|
|
15
18
|
The scripts that produced its runtime findings are in [`probes/`](probes/).
|
|
16
19
|
|
|
17
20
|
## Why this shape
|
|
@@ -64,6 +67,14 @@ The kernelspec pins the release it was installed from
|
|
|
64
67
|
runtime modules are copied into `~/.cache/cdp-kernel/runtime/`. A Node
|
|
65
68
|
target needs `node` on `PATH`.
|
|
66
69
|
|
|
70
|
+
Deno 2.9+ refuses package versions less than 24 hours old by default
|
|
71
|
+
([minimum dependency age](https://docs.deno.com/go/minimum-dependency-age)).
|
|
72
|
+
In a release's first day, the install command fails with "blocked by the
|
|
73
|
+
minimum dependency age policy". Wait out the day, or install an earlier
|
|
74
|
+
release. Passing `--min-dep-age=0` gets the install through, but the
|
|
75
|
+
kernelspec doesn't carry the flag, so the kernel still won't launch until
|
|
76
|
+
the release is a day old.
|
|
77
|
+
|
|
67
78
|
**From a checkout**, for working on the kernel itself:
|
|
68
79
|
|
|
69
80
|
```sh
|
|
@@ -93,12 +104,103 @@ In a cell:
|
|
|
93
104
|
or `jsr:@scope/pkg@range`.
|
|
94
105
|
- On Node, `npm:` and `jsr:` install on first use into
|
|
95
106
|
`~/.cache/cdp-kernel/npm`.
|
|
96
|
-
- On Deno they use Deno's global cache
|
|
97
|
-
|
|
98
|
-
|
|
107
|
+
- On Deno they use Deno's global cache, unless the notebook is in a Deno
|
|
108
|
+
project (below).
|
|
109
|
+
- **Deno projects.** If a `deno.json` or `deno.jsonc` sits in the notebook's
|
|
110
|
+
directory or above it, a Deno target uses it, as `deno jupyter` does:
|
|
111
|
+
- its import map (`"@/": "./lib/"` aliases, mapped packages);
|
|
112
|
+
- its `nodeModulesDir` setting, with `package.json` dependencies from the
|
|
113
|
+
project's `node_modules`;
|
|
114
|
+
- its JSX and decorator `compilerOptions`: `jsx` (`react-jsx`,
|
|
115
|
+
`react-jsxdev`, `precompile`, `react`), `jsxImportSource`,
|
|
116
|
+
`jsxFactory`, `jsxFragmentFactory`, `experimentalDecorators`.
|
|
117
|
+
|
|
118
|
+
The import map wins over `node_modules`, as in Deno. The kernel never
|
|
119
|
+
writes to the project's lockfile. A `deno.json` that doesn't parse is
|
|
120
|
+
reported in the first cell's output, and the kernel runs as in a plain
|
|
121
|
+
directory. Outside a project, the target runs with
|
|
122
|
+
`--node-modules-dir=none --no-lock`.
|
|
99
123
|
- `prompt()` and `confirm()` work over the stdin channel. Interrupt keeps
|
|
100
124
|
state.
|
|
101
125
|
|
|
126
|
+
## Editor types
|
|
127
|
+
|
|
128
|
+
Cells run untyped, as in `deno jupyter`. Notebook code your editor
|
|
129
|
+
type-checks can declare the globals.
|
|
130
|
+
|
|
131
|
+
**Node notebooks.** The npm package ships the declarations. Add it as a dev
|
|
132
|
+
dependency, then reference it once from a cell or a `.d.ts` in the project:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
/// <reference types="@tlockney/cdp-kernel/globals" />
|
|
136
|
+
await jupyter.display(jupyter.md`typed`);
|
|
137
|
+
const name: string | null = prompt("Name?", "", { password: false });
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
That declares `jupyter`, `prompt` and `confirm`. With the `cdp-node-deno`
|
|
141
|
+
kernel, use `@tlockney/cdp-kernel/globals/deno-compat` instead: it adds the
|
|
142
|
+
`Deno` namespace the compatibility shim implements. A custom display is a
|
|
143
|
+
class with a `[jupyter.$display]()` method returning a MIME bundle.
|
|
144
|
+
`jupyter.$display` is `Symbol.for("Jupyter.display")` at runtime, but only
|
|
145
|
+
`jupyter.$display` carries the precise type.
|
|
146
|
+
|
|
147
|
+
**Deno notebooks.** Use `Deno.jupyter`, which Deno already types and which a
|
|
148
|
+
Deno target provides. The JSR package doesn't ship these globals, because
|
|
149
|
+
JSR doesn't allow packages to augment global types.
|
|
150
|
+
|
|
151
|
+
## Widgets and comms
|
|
152
|
+
|
|
153
|
+
`deno jupyter` has no comms. Here they're supported in both directions, so
|
|
154
|
+
anywidget-style widgets work in frontends with the `anywidget` extension:
|
|
155
|
+
JupyterLab, Notebook 7 and VS Code. Install it with
|
|
156
|
+
`pip install anywidget`; that's the frontend half, no Python kernel needed.
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
const counter = jupyter.widget({
|
|
160
|
+
esm: `export default {
|
|
161
|
+
render({ model, el }) {
|
|
162
|
+
const btn = document.createElement("button");
|
|
163
|
+
const update = () => { btn.textContent = \`count is \${model.get("count")}\`; };
|
|
164
|
+
btn.onclick = () => { model.set("count", model.get("count") + 1); model.save_changes(); };
|
|
165
|
+
model.on("change:count", update);
|
|
166
|
+
update();
|
|
167
|
+
el.append(btn);
|
|
168
|
+
},
|
|
169
|
+
};`,
|
|
170
|
+
state: { count: 0 },
|
|
171
|
+
});
|
|
172
|
+
counter.on("change:count", (value) => console.log("clicked to", value));
|
|
173
|
+
counter; // display it
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The model API:
|
|
177
|
+
|
|
178
|
+
- `get(key)`, and `set(key, value)` or `set({ ... })`, which syncs to every
|
|
179
|
+
view.
|
|
180
|
+
- `on("change:key" | "change" | "msg:custom", listener)` and `off(...)`.
|
|
181
|
+
- `send(content, buffers)` for custom messages.
|
|
182
|
+
- `close()`.
|
|
183
|
+
|
|
184
|
+
`Uint8Array`, `ArrayBuffer` and `DataView` values in the state travel as
|
|
185
|
+
binary buffers. JupyterLab restores widget state after a page reload over
|
|
186
|
+
the `jupyter.widget.control` comm.
|
|
187
|
+
|
|
188
|
+
The comm layer underneath is available directly, for other comm-based
|
|
189
|
+
protocols:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
const comm = jupyter.comm.open("my.target", { hello: 1 });
|
|
193
|
+
comm.onMsg(({ data, buffers }) => console.log(data, buffers.length));
|
|
194
|
+
comm.send({ ping: true });
|
|
195
|
+
jupyter.comm.registerTarget("echo", (comm) => comm.onMsg((m) => comm.send(m.data)));
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Comm handlers run like cells. Their output goes to the notebook as replies
|
|
199
|
+
to the message that triggered them, an exception is shown on stderr, and
|
|
200
|
+
interrupt stops a stuck handler. Messages are processed one at a time with
|
|
201
|
+
cell executions, as in ipykernel, so a widget event that arrives during a
|
|
202
|
+
long cell waits for the cell to finish.
|
|
203
|
+
|
|
102
204
|
## How the tricky parts work
|
|
103
205
|
|
|
104
206
|
| Problem | Approach |
|
|
@@ -115,6 +217,7 @@ In a cell:
|
|
|
115
217
|
npm run check # tsc --strict over src/ and test/
|
|
116
218
|
npm test # unit tests (node:test)
|
|
117
219
|
npm run e2e # conformance suite via jupyter_client/libzmq, on node, deno, node-deno
|
|
220
|
+
npm run test:lab # an anywidget counter in real JupyterLab, headless Chromium (node, deno)
|
|
118
221
|
uv run --with nbclient --with nbformat python examples/run_notebook.py # Deno-style notebook on deno + node-deno
|
|
119
222
|
```
|
|
120
223
|
|
|
@@ -137,15 +240,22 @@ The `publish` workflow then:
|
|
|
137
240
|
1. Checks that the tag matches the version.
|
|
138
241
|
2. Runs the type check, the unit tests, and the e2e suite against the packed
|
|
139
242
|
npm tarball and against a Deno host.
|
|
140
|
-
3. Publishes to JSR (`deno publish
|
|
243
|
+
3. Publishes to JSR (`deno publish`).
|
|
141
244
|
4. Publishes to npm (`npm publish`).
|
|
142
245
|
|
|
143
|
-
|
|
144
|
-
|
|
246
|
+
Both registries authenticate the workflow through GitHub OIDC, so no tokens
|
|
247
|
+
are stored:
|
|
248
|
+
- **npm:** a trusted publisher for `tlockney/cdp-kernel` + `publish.yml`.
|
|
249
|
+
- **JSR:** the package is linked to this repo in its JSR settings.
|
|
250
|
+
|
|
251
|
+
Neither registry lets a version be republished. Each job skips a version
|
|
252
|
+
its registry already has, so re-running a release only fills in the half
|
|
253
|
+
that failed.
|
|
145
254
|
|
|
146
255
|
## Known gaps
|
|
147
256
|
|
|
148
|
-
-
|
|
257
|
+
- Widgets are anywidget-style (`jupyter.widget`). The classic ipywidgets
|
|
258
|
+
controls (sliders, dropdowns) are Python classes, so they're not available.
|
|
149
259
|
- No `inspect_request` content and no history. Deno has neither.
|
|
150
260
|
- No debugger.
|
|
151
261
|
- `npm:` on Node:
|
|
@@ -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
|
}
|
package/dist/kernel.js
CHANGED
|
@@ -11,7 +11,9 @@ import { decodeMessage, encodeMessage, PROTOCOL_VERSION, verify } from "./wire.j
|
|
|
11
11
|
import { PubSocket, RouterSocket, startHeartbeat } from "./zmtp.js";
|
|
12
12
|
import { Interrupted, Target } from "./target.js";
|
|
13
13
|
import { CellSyntaxError, objectLiteralCandidate, transformCell } from "./transform.js";
|
|
14
|
+
import { loadDenoProject } from "./deno-config.js";
|
|
14
15
|
import { checkIsComplete, codePointToUtf16Index, exprAtCursor, utf16ToCodePointIndex } from "./text.js";
|
|
16
|
+
import { VERSION } from "./package.js";
|
|
15
17
|
export function parseConnectionInfo(json) {
|
|
16
18
|
const raw = JSON.parse(json);
|
|
17
19
|
if (typeof raw !== "object" || raw === null)
|
|
@@ -43,6 +45,9 @@ export class Kernel {
|
|
|
43
45
|
#conn;
|
|
44
46
|
#runtime;
|
|
45
47
|
#denoCompat;
|
|
48
|
+
// A Deno target in a Deno project uses its config (#13).
|
|
49
|
+
#project;
|
|
50
|
+
#projectWarning = null;
|
|
46
51
|
#session = randomUUID();
|
|
47
52
|
#shell;
|
|
48
53
|
#control;
|
|
@@ -54,11 +59,14 @@ export class Kernel {
|
|
|
54
59
|
#parent = {};
|
|
55
60
|
#allowStdin = false;
|
|
56
61
|
#executing = false;
|
|
62
|
+
// Open comms, for comm_info_request: comm_id -> target_name (#9).
|
|
63
|
+
#comms = new Map();
|
|
57
64
|
#shuttingDown = false;
|
|
58
65
|
constructor(conn, runtime, options = {}) {
|
|
59
66
|
this.#conn = conn;
|
|
60
67
|
this.#runtime = runtime;
|
|
61
68
|
this.#denoCompat = options.denoCompat ?? false;
|
|
69
|
+
this.#project = runtime === "deno" ? this.#loadProject(options.notebookDir ?? process.cwd()) : null;
|
|
62
70
|
startHeartbeat(conn.hb_port, conn.ip);
|
|
63
71
|
this.#shell = new RouterSocket(conn.shell_port, conn.ip);
|
|
64
72
|
this.#control = new RouterSocket(conn.control_port, conn.ip);
|
|
@@ -75,11 +83,92 @@ export class Kernel {
|
|
|
75
83
|
#log(text) {
|
|
76
84
|
process.stderr.write(`[cdp-kernel] ${text}\n`);
|
|
77
85
|
}
|
|
86
|
+
#loadProject(notebookDir) {
|
|
87
|
+
try {
|
|
88
|
+
return loadDenoProject(notebookDir);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
// A broken deno.json mustn't stop the kernel: run as in a plain
|
|
92
|
+
// directory and say why in the first cell.
|
|
93
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
94
|
+
this.#projectWarning = `${reason}; running without the project config`;
|
|
95
|
+
this.#log(this.#projectWarning);
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
#transformOptions() {
|
|
100
|
+
return this.#project?.transform ?? {};
|
|
101
|
+
}
|
|
102
|
+
// --- comms (#9) ------------------------------------------------------------
|
|
103
|
+
#trackComm(msgType, content) {
|
|
104
|
+
const commId = typeof content.comm_id === "string" ? content.comm_id : null;
|
|
105
|
+
if (commId === null)
|
|
106
|
+
return;
|
|
107
|
+
if (msgType === "comm_open" && typeof content.target_name === "string")
|
|
108
|
+
this.#comms.set(commId, content.target_name);
|
|
109
|
+
else if (msgType === "comm_close")
|
|
110
|
+
this.#comms.delete(commId);
|
|
111
|
+
}
|
|
112
|
+
#commInfo(content) {
|
|
113
|
+
const filter = typeof content.target_name === "string" ? content.target_name : null;
|
|
114
|
+
const comms = {};
|
|
115
|
+
for (const [commId, targetName] of this.#comms) {
|
|
116
|
+
if (filter === null || filter === targetName)
|
|
117
|
+
comms[commId] = { target_name: targetName };
|
|
118
|
+
}
|
|
119
|
+
return comms;
|
|
120
|
+
}
|
|
121
|
+
/** A frontend comm message. The busy/idle status around it is the caller's. */
|
|
122
|
+
async #handleComm(msg) {
|
|
123
|
+
const type = msg.header.msg_type;
|
|
124
|
+
const commId = String(msg.content.comm_id ?? "");
|
|
125
|
+
const target = await this.#ensureTarget();
|
|
126
|
+
// Kernel messages the handlers send are replies to this message.
|
|
127
|
+
this.#parent = msg.header;
|
|
128
|
+
this.#allowStdin = false;
|
|
129
|
+
this.#executing = true;
|
|
130
|
+
let handled = false;
|
|
131
|
+
try {
|
|
132
|
+
const result = await target.dispatchComm(type, msg.content, msg.metadata, msg.buffers);
|
|
133
|
+
if ("error" in result) {
|
|
134
|
+
const { name, message, stack } = result.error;
|
|
135
|
+
this.#publish("stream", { name: "stderr", text: `Error in ${type} handler: ${stack || `${name}: ${message}`}\n` });
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
handled = result.handled;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
if (!(err instanceof Interrupted))
|
|
143
|
+
throw err;
|
|
144
|
+
this.#publish("stream", { name: "stderr", text: `${type} handler interrupted\n` });
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
this.#executing = false;
|
|
148
|
+
}
|
|
149
|
+
await target.flush().catch(() => { });
|
|
150
|
+
if (type === "comm_open") {
|
|
151
|
+
if (handled) {
|
|
152
|
+
this.#comms.set(commId, String(msg.content.target_name ?? ""));
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
// An unknown target, or a handler that failed: the protocol asks the
|
|
156
|
+
// kernel to close the comm so the frontend doesn't wait on it.
|
|
157
|
+
this.#publish("comm_close", { comm_id: commId, data: {} });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else if (type === "comm_close") {
|
|
161
|
+
this.#comms.delete(commId);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
78
164
|
// --- target lifecycle -----------------------------------------------------
|
|
79
165
|
#ensureTarget() {
|
|
80
166
|
if (this.#target)
|
|
81
167
|
return Promise.resolve(this.#target);
|
|
82
|
-
this.#targetStarting ??= Target.launch(this.#runtime, this.#targetEvents(), {
|
|
168
|
+
this.#targetStarting ??= Target.launch(this.#runtime, this.#targetEvents(), {
|
|
169
|
+
denoCompat: this.#denoCompat,
|
|
170
|
+
denoConfig: this.#project?.configPath,
|
|
171
|
+
}).then((target) => {
|
|
83
172
|
this.#target = target;
|
|
84
173
|
this.#targetStarting = null;
|
|
85
174
|
return target;
|
|
@@ -103,6 +192,7 @@ export class Kernel {
|
|
|
103
192
|
}
|
|
104
193
|
async #onTargetMessage(msg) {
|
|
105
194
|
if (msg.kind === "iopub") {
|
|
195
|
+
this.#trackComm(msg.msg_type, msg.content);
|
|
106
196
|
this.#publish(msg.msg_type, msg.content, {
|
|
107
197
|
metadata: msg.metadata,
|
|
108
198
|
buffers: msg.buffers.map((b) => new Uint8Array(Buffer.from(b, "base64"))),
|
|
@@ -156,7 +246,7 @@ export class Kernel {
|
|
|
156
246
|
status: "ok",
|
|
157
247
|
protocol_version: PROTOCOL_VERSION,
|
|
158
248
|
implementation: "cdp-kernel",
|
|
159
|
-
implementation_version:
|
|
249
|
+
implementation_version: VERSION,
|
|
160
250
|
language_info: {
|
|
161
251
|
name: "typescript",
|
|
162
252
|
version: "5",
|
|
@@ -218,11 +308,12 @@ export class Kernel {
|
|
|
218
308
|
this.#reply(this.#shell, peer, msg, "history_reply", { status: "ok", history: [] });
|
|
219
309
|
break;
|
|
220
310
|
case "comm_info_request":
|
|
221
|
-
this.#reply(this.#shell, peer, msg, "comm_info_reply", { status: "ok", comms:
|
|
311
|
+
this.#reply(this.#shell, peer, msg, "comm_info_reply", { status: "ok", comms: this.#commInfo(msg.content) });
|
|
222
312
|
break;
|
|
223
313
|
case "comm_open":
|
|
224
|
-
|
|
225
|
-
|
|
314
|
+
case "comm_msg":
|
|
315
|
+
case "comm_close":
|
|
316
|
+
await this.#handleComm(msg);
|
|
226
317
|
break;
|
|
227
318
|
}
|
|
228
319
|
}
|
|
@@ -285,7 +376,7 @@ export class Kernel {
|
|
|
285
376
|
const wrapped = objectLiteralCandidate(code);
|
|
286
377
|
if (wrapped !== null) {
|
|
287
378
|
try {
|
|
288
|
-
const result = await target.evaluate(transformCell(wrapped));
|
|
379
|
+
const result = await target.evaluate(transformCell(wrapped, this.#transformOptions()));
|
|
289
380
|
if (!result.exceptionDetails)
|
|
290
381
|
return result;
|
|
291
382
|
}
|
|
@@ -294,7 +385,7 @@ export class Kernel {
|
|
|
294
385
|
throw err;
|
|
295
386
|
}
|
|
296
387
|
}
|
|
297
|
-
return target.evaluate(transformCell(code));
|
|
388
|
+
return target.evaluate(transformCell(code, this.#transformOptions()));
|
|
298
389
|
}
|
|
299
390
|
async #execute(msg, peer) {
|
|
300
391
|
const parent = msg.header;
|
|
@@ -307,6 +398,11 @@ export class Kernel {
|
|
|
307
398
|
this.#allowStdin = msg.content.allow_stdin === true;
|
|
308
399
|
this.#status("busy", parent);
|
|
309
400
|
this.#publish("execute_input", { code, execution_count: count });
|
|
401
|
+
if (this.#projectWarning !== null) {
|
|
402
|
+
// Shown once, in the first cell, where the user will see it.
|
|
403
|
+
this.#publish("stream", { name: "stderr", text: `[cdp-kernel] ${this.#projectWarning}\n` });
|
|
404
|
+
this.#projectWarning = null;
|
|
405
|
+
}
|
|
310
406
|
let error = null;
|
|
311
407
|
this.#executing = true;
|
|
312
408
|
try {
|
package/dist/package.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Kept equal to package.json and deno.jsonc by test/package.test.ts. */
|
|
2
|
-
export declare const VERSION = "0.
|
|
3
|
-
export declare const RUNTIME_FILES: readonly ["bootstrap.mjs", "display.mjs", "node-hooks.mjs", "deno-compat.mjs"];
|
|
2
|
+
export declare const VERSION = "0.2.0";
|
|
3
|
+
export declare const RUNTIME_FILES: readonly ["bootstrap.mjs", "display.mjs", "comm.mjs", "widget.mjs", "node-hooks.mjs", "deno-compat.mjs", "node-modules-resolve.mjs"];
|
|
4
4
|
export type HostRuntime = "node" | "deno";
|
|
5
5
|
export declare function hostRuntime(): HostRuntime;
|
|
6
6
|
/** Where the CLI module was loaded from. */
|
|
@@ -20,10 +20,18 @@ export declare function moduleOrigin(moduleUrl: string): Origin;
|
|
|
20
20
|
export declare function kernelArgv(host: HostRuntime, execPath: string, origin: Origin, args: string[]): string[];
|
|
21
21
|
export declare function cacheRoot(): string;
|
|
22
22
|
export type FetchText = (url: string) => Promise<string>;
|
|
23
|
+
/** True when a file: URL points inside a node_modules directory (an npm install). */
|
|
24
|
+
export declare function isInsideNodeModules(url: string): boolean;
|
|
23
25
|
/**
|
|
24
26
|
* A local directory holding runtime/*.mjs. `baseUrl` is the runtime/
|
|
25
27
|
* directory's URL relative to the calling module. A file: URL is used in
|
|
26
|
-
* place; anything else is copied once into a cache
|
|
27
|
-
* URL.
|
|
28
|
+
* place unless `copy` is set; anything else is copied once into a cache
|
|
29
|
+
* keyed by version and URL.
|
|
30
|
+
*
|
|
31
|
+
* `copy` exists for Deno targets of an npm install: Deno resolves imports
|
|
32
|
+
* made by a module under node_modules as npm-package imports, without the
|
|
33
|
+
* notebook project's import map, so the bootstrap must load from outside it.
|
|
28
34
|
*/
|
|
29
|
-
export declare function runtimeDir(baseUrl: string, cache?: string, get?: FetchText
|
|
35
|
+
export declare function runtimeDir(baseUrl: string, cache?: string, get?: FetchText, { copy }?: {
|
|
36
|
+
copy?: boolean;
|
|
37
|
+
}): Promise<string>;
|