@tlockney/cdp-kernel 0.5.0 → 0.6.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.
Files changed (44) hide show
  1. package/README.md +27 -6
  2. package/dist/cdp.d.ts +2 -0
  3. package/dist/cdp.js +6 -1
  4. package/dist/celld-target.js +9 -12
  5. package/dist/children.d.ts +25 -3
  6. package/dist/children.js +84 -2
  7. package/dist/deno-types.d.ts +30 -0
  8. package/dist/deno-types.js +93 -0
  9. package/dist/kernel.d.ts +7 -1
  10. package/dist/kernel.js +187 -29
  11. package/dist/language-service-worker.d.ts +1 -0
  12. package/dist/language-service-worker.js +88 -0
  13. package/dist/language-service.d.ts +64 -0
  14. package/dist/language-service.js +128 -0
  15. package/dist/main.js +102 -8
  16. package/dist/mod.d.ts +1 -0
  17. package/dist/mod.js +1 -0
  18. package/dist/notebook-types.d.ts +62 -0
  19. package/dist/notebook-types.js +310 -0
  20. package/dist/package.d.ts +2 -2
  21. package/dist/package.js +2 -2
  22. package/dist/parent.d.ts +12 -0
  23. package/dist/parent.js +39 -0
  24. package/dist/port-holder.d.ts +13 -0
  25. package/dist/port-holder.js +105 -0
  26. package/dist/sourcemap.d.ts +7 -2
  27. package/dist/sourcemap.js +9 -3
  28. package/dist/strip-esbuild.d.ts +2 -0
  29. package/dist/strip-esbuild.js +44 -0
  30. package/dist/strip-sucrase.d.ts +2 -0
  31. package/dist/strip-sucrase.js +78 -0
  32. package/dist/target.d.ts +27 -3
  33. package/dist/target.js +193 -36
  34. package/dist/transform.d.ts +21 -1
  35. package/dist/transform.js +38 -42
  36. package/dist/workerd-target.js +1 -1
  37. package/dist/workers-project.js +13 -2
  38. package/dist/zmtp.d.ts +2 -0
  39. package/dist/zmtp.js +12 -0
  40. package/package.json +9 -4
  41. package/runtime/bootstrap.mjs +113 -33
  42. package/runtime/introspect.mjs +1 -1
  43. package/runtime/node-hooks.d.mts +6 -0
  44. package/runtime/node-hooks.mjs +11 -0
package/dist/main.js CHANGED
@@ -1,17 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
  // cdp-kernel CLI.
3
- // cdp-kernel install [--runtime node|deno|bun|workerd|celld] [--deno-compat] [--name NAME] [--display NAME]
4
- // cdp-kernel kernel --runtime node|deno|bun|workerd|celld [--deno-compat] --conn CONNECTION_FILE
3
+ // cdp-kernel install [--runtime node|deno|bun|workerd|celld] [--deno-compat] [--type-strip esbuild|sucrase] [--no-language-service] [--name NAME] [--display NAME]
4
+ // cdp-kernel kernel --runtime node|deno|bun|workerd|celld [--deno-compat] [--type-strip esbuild|sucrase] [--no-language-service] --conn CONNECTION_FILE
5
+ // cdp-kernel list
6
+ // cdp-kernel uninstall [NAME...] [--name NAME] [--all]
5
7
  // cdp-kernel version
6
8
  //
7
9
  // --deno-compat (Node only) adds a `Deno` namespace for notebooks written for
8
- // `deno jupyter`; see runtime/deno-compat.mjs.
10
+ // `deno jupyter`; see runtime/deno-compat.mjs. --type-strip picks what
11
+ // removes cells' types (src/transform.ts). --no-language-service turns off
12
+ // typed completion (src/language-service.ts), which costs about 150 MB
13
+ // while in use.
9
14
  import process from "node:process";
10
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
15
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
11
16
  import { homedir, platform } from "node:os";
12
17
  import { join } from "node:path";
13
18
  import { parseArgs } from "node:util";
14
19
  import { Kernel, parseConnectionInfo } from "./kernel.js";
20
+ import { isTypeStripperName } from "./transform.js";
15
21
  import { hostRuntime, kernelArgv, moduleOrigin, VERSION } from "./package.js";
16
22
  function isRuntime(value) {
17
23
  return value === "node" || value === "deno" || value === "bun" || value === "workerd" || value === "celld";
@@ -25,7 +31,7 @@ function kernelsDir() {
25
31
  return join(process.env.APPDATA ?? homedir(), "jupyter", "kernels");
26
32
  return join(process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"), "jupyter", "kernels");
27
33
  }
28
- function install(runtime, denoCompat, name, display) {
34
+ function install(runtime, denoCompat, typeStrip, languageService, name, display) {
29
35
  const dir = join(kernelsDir(), name);
30
36
  mkdirSync(dir, { recursive: true });
31
37
  const spec = {
@@ -34,6 +40,8 @@ function install(runtime, denoCompat, name, display) {
34
40
  "--runtime",
35
41
  runtime,
36
42
  ...(denoCompat ? ["--deno-compat"] : []),
43
+ ...(typeStrip !== "esbuild" ? ["--type-strip", typeStrip] : []),
44
+ ...(languageService ? [] : ["--no-language-service"]),
37
45
  "--conn",
38
46
  "{connection_file}",
39
47
  ]),
@@ -49,6 +57,77 @@ function install(runtime, denoCompat, name, display) {
49
57
  writeFileSync(join(dir, "kernel.json"), JSON.stringify(spec, null, 2) + "\n");
50
58
  console.log(`installed kernelspec ${name} -> ${dir}`);
51
59
  }
60
+ // A kernelspec this package wrote: install() always records the runtime in
61
+ // metadata and passes the connection file to the kernel subcommand. Other
62
+ // kernels' specs in the same directory are left alone.
63
+ function readOwnSpec(name) {
64
+ const dir = join(kernelsDir(), name);
65
+ let spec;
66
+ try {
67
+ spec = JSON.parse(readFileSync(join(dir, "kernel.json"), "utf8"));
68
+ }
69
+ catch {
70
+ return undefined;
71
+ }
72
+ const argv = Array.isArray(spec.argv) ? spec.argv : [];
73
+ const runtime = spec.metadata?.runtime;
74
+ if (!isRuntime(runtime) || !argv.includes("kernel") || !argv.includes("{connection_file}"))
75
+ return undefined;
76
+ const version = typeof spec.metadata?.version === "string" ? spec.metadata.version : undefined;
77
+ return { name, dir, runtime, version };
78
+ }
79
+ function installedSpecs() {
80
+ const root = kernelsDir();
81
+ if (!existsSync(root))
82
+ return [];
83
+ return readdirSync(root, { withFileTypes: true })
84
+ .filter((entry) => entry.isDirectory())
85
+ .map((entry) => readOwnSpec(entry.name))
86
+ .filter((spec) => spec !== undefined)
87
+ .sort((a, b) => a.name.localeCompare(b.name));
88
+ }
89
+ function list() {
90
+ const specs = installedSpecs();
91
+ if (specs.length === 0) {
92
+ console.log(`no cdp-kernel kernelspecs in ${kernelsDir()}`);
93
+ return;
94
+ }
95
+ const width = Math.max(...specs.map((s) => s.name.length));
96
+ for (const s of specs) {
97
+ console.log(`${s.name.padEnd(width)} ${s.runtime.padEnd(7)} ${(s.version ?? "-").padEnd(7)} ${s.dir}`);
98
+ }
99
+ }
100
+ // Jupyter's kernel names; anything else could point outside the kernels
101
+ // directory.
102
+ const KERNEL_NAME = /^[A-Za-z0-9._-]+$/;
103
+ function uninstall(names, all) {
104
+ if (all)
105
+ names = installedSpecs().map((s) => s.name);
106
+ else if (names.length === 0) {
107
+ console.error("uninstall: name the kernelspecs to remove, or pass --all. Installed:");
108
+ list();
109
+ process.exit(2);
110
+ }
111
+ let failed = false;
112
+ for (const name of names) {
113
+ const valid = KERNEL_NAME.test(name) && name !== "." && name !== "..";
114
+ const spec = valid ? readOwnSpec(name) : undefined;
115
+ if (!spec) {
116
+ const exists = valid && existsSync(join(kernelsDir(), name));
117
+ console.error(exists
118
+ ? `uninstall: ${name} wasn't installed by cdp-kernel; leaving it (use jupyter kernelspec remove)`
119
+ : `uninstall: no kernelspec ${name} in ${kernelsDir()}`);
120
+ failed = true;
121
+ continue;
122
+ }
123
+ rmSync(spec.dir, { recursive: true, force: true });
124
+ console.log(`removed kernelspec ${name} (${spec.dir})`);
125
+ }
126
+ if (all && names.length === 0)
127
+ console.log(`no cdp-kernel kernelspecs in ${kernelsDir()}`);
128
+ if (failed)
129
+ process.exit(1);
130
+ }
52
131
  const { positionals, values } = parseArgs({
53
132
  allowPositionals: true,
54
133
  options: {
@@ -56,7 +135,10 @@ const { positionals, values } = parseArgs({
56
135
  conn: { type: "string" },
57
136
  name: { type: "string" },
58
137
  display: { type: "string" },
138
+ all: { type: "boolean", default: false },
59
139
  "deno-compat": { type: "boolean", default: false },
140
+ "type-strip": { type: "string", default: "esbuild" },
141
+ "no-language-service": { type: "boolean", default: false },
60
142
  },
61
143
  });
62
144
  const runtime = values.runtime;
@@ -64,12 +146,18 @@ if (!isRuntime(runtime)) {
64
146
  console.error(`unknown runtime ${runtime}; expected node, deno, bun, workerd or celld`);
65
147
  process.exit(2);
66
148
  }
149
+ const typeStrip = values["type-strip"];
150
+ if (!isTypeStripperName(typeStrip)) {
151
+ console.error(`unknown type stripper ${typeStrip}; expected esbuild or sucrase`);
152
+ process.exit(2);
153
+ }
154
+ const languageService = values["no-language-service"] !== true;
67
155
  // Deno targets have the real namespace; the shim is for Node.
68
156
  const denoCompat = values["deno-compat"] === true && runtime === "node";
69
157
  switch (positionals[0]) {
70
158
  case "install": {
71
159
  const suffix = denoCompat ? "-deno" : "";
72
- install(runtime, denoCompat, values.name ?? `cdp-${runtime}${suffix}`, values.display ?? `TypeScript (cdp-kernel, ${runtime}${denoCompat ? ", Deno compat" : ""})`);
160
+ install(runtime, denoCompat, typeStrip, languageService, values.name ?? `cdp-${runtime}${suffix}`, values.display ?? `TypeScript (cdp-kernel, ${runtime}${denoCompat ? ", Deno compat" : ""})`);
73
161
  break;
74
162
  }
75
163
  case "kernel": {
@@ -77,14 +165,20 @@ switch (positionals[0]) {
77
165
  console.error("kernel: --conn CONNECTION_FILE is required");
78
166
  process.exit(2);
79
167
  }
80
- const kernel = new Kernel(parseConnectionInfo(readFileSync(values.conn, "utf8")), runtime, { denoCompat });
168
+ const kernel = new Kernel(parseConnectionInfo(readFileSync(values.conn, "utf8")), runtime, { denoCompat, typeStrip, languageService });
81
169
  await kernel.run();
82
170
  break;
83
171
  }
172
+ case "list":
173
+ list();
174
+ break;
175
+ case "uninstall":
176
+ uninstall([...positionals.slice(1), ...(values.name ? [values.name] : [])], values.all === true);
177
+ break;
84
178
  case "version":
85
179
  console.log(VERSION);
86
180
  break;
87
181
  default:
88
- console.error("usage: cdp-kernel install [--runtime node|deno|bun|workerd|celld] [--deno-compat] | kernel --runtime node|deno|bun|workerd|celld --conn FILE | version");
182
+ console.error("usage: cdp-kernel install [--runtime node|deno|bun|workerd|celld] [--deno-compat] [--type-strip esbuild|sucrase] [--no-language-service] | list | uninstall [NAME...] [--all] | kernel --runtime node|deno|bun|workerd|celld --conn FILE | version");
89
183
  process.exit(2);
90
184
  }
package/dist/mod.d.ts CHANGED
@@ -22,4 +22,5 @@
22
22
  */
23
23
  export { type ConnectionInfo, Kernel, parseConnectionInfo } from "./kernel.ts";
24
24
  export type { RuntimeName } from "./target.ts";
25
+ export { CellSyntaxError, type CellTransformOptions, type JsxOptions, type TypeStripper } from "./transform.ts";
25
26
  export { VERSION } from "./package.ts";
package/dist/mod.js CHANGED
@@ -21,4 +21,5 @@
21
21
  * @module
22
22
  */
23
23
  export { Kernel, parseConnectionInfo } from "./kernel.js";
24
+ export { CellSyntaxError } from "./transform.js";
24
25
  export { VERSION } from "./package.js";
@@ -0,0 +1,62 @@
1
+ import type TS from "typescript";
2
+ import type { DenoResolver } from "./deno-types.ts";
3
+ export interface NotebookTypesOptions {
4
+ notebookDir: string;
5
+ /**
6
+ * The installer cache npm: and jsr: imports resolve from: where the
7
+ * target installs them (Bun's own for Bun, npm's for the rest). Null for
8
+ * Deno targets, which resolve them natively (see `deno`).
9
+ */
10
+ npmCache: "npm" | "bun" | null;
11
+ /**
12
+ * For Deno targets: where Deno put what the executed cells imported
13
+ * (npm:, jsr:, URLs, the project's import map).
14
+ */
15
+ deno?: DenoResolver;
16
+ /** compilerOptions.types: type packages found from the notebook's directory. */
17
+ types: string[];
18
+ /** Declaration files every cell sees, e.g. runtime types. */
19
+ typeFiles: string[];
20
+ /** Declarations for the kernel's own globals (jupyter, prompt, confirm). */
21
+ globals: string;
22
+ }
23
+ export interface CompletionEntry {
24
+ name: string;
25
+ /** TypeScript's ScriptElementKind: "method", "property", "const", ... */
26
+ kind: string;
27
+ }
28
+ export interface CompletionResult {
29
+ /** Where the completed name starts in the cell, as a UTF-16 index. */
30
+ start: number;
31
+ entries: CompletionEntry[];
32
+ }
33
+ export declare class NotebookTypes {
34
+ #private;
35
+ constructor(ts: typeof TS, options: NotebookTypesOptions);
36
+ /** An executed cell: its declarations are in scope for later ones. */
37
+ addCell(code: string): void;
38
+ /** The runtime restarted and lost its state. */
39
+ reset(): void;
40
+ /** Adds declaration files, e.g. runtime types installed after startup. */
41
+ addTypeFiles(files: string[]): void;
42
+ /** Adds a declaration file held in memory, e.g. the output of `deno types`. */
43
+ addVirtualTypeFile(name: string, text: string): void;
44
+ /** Adds type packages (compilerOptions.types) that resolve from the notebook. */
45
+ addTypeNames(names: string[]): void;
46
+ /** Whether a type package (compilerOptions.types) resolves from the notebook. */
47
+ resolvesTypes(name: string): boolean;
48
+ /** Completions at `cursor` (a UTF-16 index) in `code`, the cell being edited. */
49
+ complete(code: string, cursor: number): CompletionResult;
50
+ /**
51
+ * The type, signature and JSDoc of the name at `cursor`: the one the
52
+ * cursor is in or just after, or, right after "name(", that callee.
53
+ */
54
+ quickInfo(code: string, cursor: number): string | null;
55
+ }
56
+ /**
57
+ * Declarations for the names the kernel adds to every target, as in
58
+ * src/globals.ts. `display` is runtime/display.mjs's path; its sibling
59
+ * display.d.mts types `jupyter`. Without it (a kernel run from JSR, whose
60
+ * runtime files aren't local), `jupyter` is untyped.
61
+ */
62
+ export declare function kernelGlobals(display: string | null): string;
@@ -0,0 +1,310 @@
1
+ // TypeScript's language service over a notebook (#263): completion from
2
+ // declared types, where the runtime only knows values. Runs in a worker
3
+ // (src/language-service-worker.ts); see probes/ts-language-service.md for
4
+ // the design and its measurements.
5
+ //
6
+ // The service sees one virtual file, never written to disk, in the
7
+ // notebook's directory (so relative imports resolve from there): the cells
8
+ // executed so far, then the cell being edited. REPL cells redeclare names
9
+ // all the time (`const x = 1`, later `const x = "s"`), which in one file is
10
+ // an error that keeps the first type, so a declaration is dropped from the
11
+ // history when a later cell, or the one being edited, declares all of its
12
+ // names again.
13
+ import { join } from "node:path";
14
+ import { findLockFile, installedPackageDir, jsrToNpm, parsePackageSpecifier } from "../runtime/node-hooks.mjs";
15
+ // The document is a module, so top-level await in a cell isn't an error.
16
+ const PREFIX = "export {};\n";
17
+ export class NotebookTypes {
18
+ #ts;
19
+ #options;
20
+ #file;
21
+ #globalsFile;
22
+ #service;
23
+ #compilerOptions;
24
+ #resolutionCache;
25
+ /** ts.sys, plus the remote modules a Deno target fetched (virtual paths). */
26
+ #resolutionHost;
27
+ #cells = [];
28
+ /** Specifiers executed cells imported: the target has fetched them. */
29
+ #imported = new Set();
30
+ /** Declaration files that exist only in memory, e.g. `deno types`. */
31
+ #virtualTypes = new Map();
32
+ #typeFiles;
33
+ #text = PREFIX;
34
+ #version = 0;
35
+ #projectVersion = 0;
36
+ constructor(ts, options) {
37
+ this.#ts = ts;
38
+ this.#options = options;
39
+ this.#typeFiles = [...options.typeFiles];
40
+ this.#file = join(options.notebookDir, "__cdpk_notebook__.ts");
41
+ this.#globalsFile = join(options.notebookDir, "__cdpk_globals__.d.ts");
42
+ this.#compilerOptions = {
43
+ target: ts.ScriptTarget.ESNext,
44
+ module: ts.ModuleKind.Preserve,
45
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
46
+ allowImportingTsExtensions: true,
47
+ allowJs: true,
48
+ strict: true,
49
+ noEmit: true,
50
+ skipLibCheck: true,
51
+ lib: ["lib.esnext.d.ts"],
52
+ types: options.types,
53
+ };
54
+ const deno = options.deno;
55
+ this.#resolutionHost = {
56
+ ...ts.sys,
57
+ fileExists: (f) => deno?.localFile(f) !== undefined || ts.sys.fileExists(f),
58
+ readFile: (f) => {
59
+ const local = deno?.localFile(f);
60
+ return ts.sys.readFile(local ?? f);
61
+ },
62
+ directoryExists: (d) => (deno !== undefined && d.startsWith(deno.root)) || ts.sys.directoryExists(d),
63
+ getCurrentDirectory: () => options.notebookDir,
64
+ };
65
+ const host = this.#host();
66
+ this.#resolutionCache = ts.createModuleResolutionCache(options.notebookDir, (f) => f, this.#compilerOptions);
67
+ this.#service = ts.createLanguageService(host, ts.createDocumentRegistry());
68
+ }
69
+ /** An executed cell: its declarations are in scope for later ones. */
70
+ addCell(code) {
71
+ this.#cells.push({ code, statements: this.#statements(code) });
72
+ const ts = this.#ts;
73
+ const sf = ts.createSourceFile("cell.ts", code, ts.ScriptTarget.ESNext, false, ts.ScriptKind.TS);
74
+ for (const stmt of sf.statements) {
75
+ if ((ts.isImportDeclaration(stmt) || ts.isExportDeclaration(stmt)) && stmt.moduleSpecifier && ts.isStringLiteral(stmt.moduleSpecifier)) {
76
+ this.#imported.add(stmt.moduleSpecifier.text);
77
+ }
78
+ }
79
+ }
80
+ /** The runtime restarted and lost its state. */
81
+ reset() {
82
+ this.#cells.length = 0;
83
+ }
84
+ /** Adds declaration files, e.g. runtime types installed after startup. */
85
+ addTypeFiles(files) {
86
+ this.#typeFiles.push(...files.filter((f) => !this.#typeFiles.includes(f)));
87
+ this.#projectVersion++;
88
+ }
89
+ /** Adds a declaration file held in memory, e.g. the output of `deno types`. */
90
+ addVirtualTypeFile(name, text) {
91
+ this.#virtualTypes.set(join(this.#options.notebookDir, name), text);
92
+ this.#projectVersion++;
93
+ }
94
+ /** Adds type packages (compilerOptions.types) that resolve from the notebook. */
95
+ addTypeNames(names) {
96
+ const current = this.#compilerOptions.types ?? [];
97
+ // A new object: the service compares options by identity first.
98
+ this.#compilerOptions = { ...this.#compilerOptions, types: [...current, ...names.filter((n) => !current.includes(n))] };
99
+ this.#projectVersion++;
100
+ }
101
+ /** Whether a type package (compilerOptions.types) resolves from the notebook. */
102
+ resolvesTypes(name) {
103
+ // Default type roots are found up from the current directory.
104
+ const host = { ...this.#ts.sys, getCurrentDirectory: () => this.#options.notebookDir };
105
+ const resolved = this.#ts.resolveTypeReferenceDirective(name, this.#file, this.#compilerOptions, host);
106
+ return resolved.resolvedTypeReferenceDirective !== undefined;
107
+ }
108
+ /** Completions at `cursor` (a UTF-16 index) in `code`, the cell being edited. */
109
+ complete(code, cursor) {
110
+ const offset = this.#setDocument(code);
111
+ const prefix = /[\w$]*$/.exec(code.slice(0, cursor))?.[0] ?? "";
112
+ const info = this.#service.getCompletionsAtPosition(this.#file, offset + cursor, {
113
+ includeCompletionsForModuleExports: false,
114
+ includeCompletionsWithInsertText: true,
115
+ });
116
+ const ts = this.#ts;
117
+ const entries = (info?.entries ?? [])
118
+ .filter((e) => e.kind !== ts.ScriptElementKind.keyword &&
119
+ // Entries that would edit more than the name (`?.x`, `["a-b"]`).
120
+ e.insertText === undefined && e.replacementSpan === undefined &&
121
+ e.name.startsWith(prefix) && !e.name.startsWith("__cdpk"))
122
+ .sort((a, b) => (a.sortText < b.sortText ? -1 : a.sortText > b.sortText ? 1 : a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
123
+ .map((e) => ({ name: e.name, kind: e.kind }));
124
+ return { start: cursor - prefix.length, entries };
125
+ }
126
+ /**
127
+ * The type, signature and JSDoc of the name at `cursor`: the one the
128
+ * cursor is in or just after, or, right after "name(", that callee.
129
+ */
130
+ quickInfo(code, cursor) {
131
+ const offset = this.#setDocument(code);
132
+ const before = code.slice(0, cursor);
133
+ const call = /[\w$]\(\s*$/.exec(before);
134
+ // The name's last character when the cursor is just past it.
135
+ const position = call ? call.index : /[\w$]/.test(code[cursor - 1] ?? "") ? cursor - 1 : cursor;
136
+ const info = this.#service.getQuickInfoAtPosition(this.#file, offset + position);
137
+ if (info === undefined)
138
+ return null;
139
+ const ts = this.#ts;
140
+ // An imported name's "import x" line repeats the cell; alone, the import
141
+ // didn't resolve. "any" alone is what an undeclared name gets, e.g. a
142
+ // global a library set at runtime.
143
+ const display = ts.displayPartsToString(info.displayParts).split("\n").filter((l) => !/^import [\w$]+$/.test(l)).join("\n");
144
+ if (display === "" || display === "any")
145
+ return null;
146
+ const lines = [display];
147
+ const docs = ts.displayPartsToString(info.documentation);
148
+ if (docs !== "")
149
+ lines.push(docs);
150
+ for (const tag of info.tags ?? []) {
151
+ const text = ts.displayPartsToString(tag.text);
152
+ lines.push(`@${tag.name}${text === "" ? "" : ` ${text}`}`);
153
+ }
154
+ return lines.join("\n");
155
+ }
156
+ /** Sets the virtual file to the history plus `code`; returns where `code` starts. */
157
+ #setDocument(code) {
158
+ const history = this.#history(new Set(this.#statements(code).flatMap((s) => s.names)));
159
+ const text = `${PREFIX}${history}\n`;
160
+ this.#text = text + code;
161
+ this.#version++;
162
+ return text.length;
163
+ }
164
+ /** The executed cells, minus declarations a later cell (or `seen`) makes again. */
165
+ #history(seen) {
166
+ const kept = [];
167
+ for (let i = this.#cells.length - 1; i >= 0; i--) {
168
+ const { code, statements } = this.#cells[i];
169
+ let text = code;
170
+ // Back to front, so earlier offsets stay valid.
171
+ for (let j = statements.length - 1; j >= 0; j--) {
172
+ const { start, end, names } = statements[j];
173
+ if (names.length > 0 && names.every((n) => seen.has(n)))
174
+ text = text.slice(0, start) + text.slice(end);
175
+ }
176
+ for (const s of statements)
177
+ for (const n of s.names)
178
+ seen.add(n);
179
+ kept.push(text);
180
+ }
181
+ return kept.reverse().join("\n");
182
+ }
183
+ #statements(code) {
184
+ const ts = this.#ts;
185
+ const sf = ts.createSourceFile("cell.ts", code, ts.ScriptTarget.ESNext, false, ts.ScriptKind.TS);
186
+ return sf.statements.map((stmt) => ({ start: stmt.pos, end: stmt.end, names: this.#declaredNames(stmt) }));
187
+ }
188
+ #declaredNames(stmt) {
189
+ const ts = this.#ts;
190
+ const bound = (name) => ts.isIdentifier(name)
191
+ ? [name.text]
192
+ : name.elements.flatMap((e) => (ts.isOmittedExpression(e) ? [] : bound(e.name)));
193
+ if (ts.isVariableStatement(stmt))
194
+ return stmt.declarationList.declarations.flatMap((d) => bound(d.name));
195
+ if ((ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt) || ts.isInterfaceDeclaration(stmt) ||
196
+ ts.isTypeAliasDeclaration(stmt) || ts.isEnumDeclaration(stmt)) && stmt.name) {
197
+ return [stmt.name.text];
198
+ }
199
+ if (ts.isImportDeclaration(stmt) && stmt.importClause) {
200
+ const clause = stmt.importClause;
201
+ const names = clause.name ? [clause.name.text] : [];
202
+ const bindings = clause.namedBindings;
203
+ if (bindings && ts.isNamedImports(bindings))
204
+ names.push(...bindings.elements.map((e) => e.name.text));
205
+ if (bindings && ts.isNamespaceImport(bindings))
206
+ names.push(bindings.name.text);
207
+ return names;
208
+ }
209
+ return [];
210
+ }
211
+ /** Where an npm:/jsr: import is installed in the kernel's npm cache, if it is. */
212
+ #cachedPackage(specifier) {
213
+ try {
214
+ const pkg = specifier.startsWith("npm:")
215
+ ? parsePackageSpecifier(specifier.slice(4))
216
+ : jsrToNpm(specifier.slice(4));
217
+ const installer = this.#options.npmCache ?? "npm";
218
+ const installed = installedPackageDir(pkg, installer, { lockFile: findLockFile(this.#options.notebookDir) });
219
+ return installed === null ? null : { ...installed, subpath: pkg.subpath };
220
+ }
221
+ catch {
222
+ return null;
223
+ }
224
+ }
225
+ #resolve(name, containingFile) {
226
+ const ts = this.#ts;
227
+ const deno = this.#options.deno;
228
+ // Only what an executed cell imported (or a fetched module imports):
229
+ // looking a specifier up never makes Deno fetch something new.
230
+ if (deno !== undefined && !/^(?:\.{0,2}\/|node:)/.test(name) && (this.#imported.has(name) || containingFile.startsWith(deno.root))) {
231
+ const found = deno.lookup(name);
232
+ if (found?.kind === "npm") {
233
+ return ts.resolveModuleName(join(found.dir, found.subpath), containingFile, this.#compilerOptions, this.#resolutionHost, this.#resolutionCache);
234
+ }
235
+ const extension = found?.kind === "remote" ? extensionOf(ts, found.path) : undefined;
236
+ if (found?.kind === "remote" && extension !== undefined) {
237
+ return { resolvedModule: { resolvedFileName: found.path, extension, isExternalLibraryImport: false } };
238
+ }
239
+ }
240
+ if (this.#options.npmCache !== null && /^(?:npm|jsr):/.test(name)) {
241
+ const cached = this.#cachedPackage(name);
242
+ // Resolved as its alias from the cache directory, so package.json
243
+ // "exports" and "types" apply as for any package.
244
+ if (cached !== null) {
245
+ const from = join(cached.dir, "__cdpk__.ts");
246
+ return ts.resolveModuleName(cached.alias + cached.subpath, from, this.#compilerOptions, this.#resolutionHost, this.#resolutionCache);
247
+ }
248
+ }
249
+ return ts.resolveModuleName(name, containingFile, this.#compilerOptions, this.#resolutionHost, this.#resolutionCache);
250
+ }
251
+ #host() {
252
+ const ts = this.#ts;
253
+ const file = this.#file;
254
+ const globalsFile = this.#globalsFile;
255
+ const deno = this.#options.deno;
256
+ const virtual = (f) => {
257
+ if (f === file)
258
+ return this.#text;
259
+ if (f === globalsFile)
260
+ return this.#options.globals;
261
+ const local = deno?.localFile(f);
262
+ return local !== undefined ? ts.sys.readFile(local) : this.#virtualTypes.get(f);
263
+ };
264
+ return {
265
+ getCompilationSettings: () => this.#compilerOptions,
266
+ getProjectVersion: () => `${this.#projectVersion}.${this.#version}`,
267
+ getScriptFileNames: () => [file, globalsFile, ...this.#virtualTypes.keys(), ...this.#typeFiles],
268
+ getScriptVersion: (f) => (f === file ? String(this.#version) : "0"),
269
+ getScriptSnapshot: (f) => {
270
+ const text = virtual(f) ?? ts.sys.readFile(f);
271
+ return text === undefined ? undefined : ts.ScriptSnapshot.fromString(text);
272
+ },
273
+ getCurrentDirectory: () => this.#options.notebookDir,
274
+ getDefaultLibFileName: (o) => ts.getDefaultLibFilePath(o),
275
+ fileExists: (f) => f === file || f === globalsFile || this.#virtualTypes.has(f) || deno?.localFile(f) !== undefined || ts.sys.fileExists(f),
276
+ readFile: (f) => virtual(f) ?? ts.sys.readFile(f),
277
+ readDirectory: ts.sys.readDirectory,
278
+ directoryExists: ts.sys.directoryExists,
279
+ getDirectories: ts.sys.getDirectories,
280
+ resolveModuleNameLiterals: (literals, containingFile) => literals.map((literal) => this.#resolve(literal.text, containingFile)),
281
+ };
282
+ }
283
+ }
284
+ /** The extension TypeScript should load a remote module's path as, if it's one it can. */
285
+ function extensionOf(ts, path) {
286
+ for (const ext of [ts.Extension.Dts, ts.Extension.Dmts, ts.Extension.Ts, ts.Extension.Mts, ts.Extension.Tsx, ts.Extension.Js, ts.Extension.Mjs, ts.Extension.Jsx]) {
287
+ if (path.endsWith(ext))
288
+ return ext;
289
+ }
290
+ return undefined;
291
+ }
292
+ /**
293
+ * Declarations for the names the kernel adds to every target, as in
294
+ * src/globals.ts. `display` is runtime/display.mjs's path; its sibling
295
+ * display.d.mts types `jupyter`. Without it (a kernel run from JSR, whose
296
+ * runtime files aren't local), `jupyter` is untyped.
297
+ */
298
+ export function kernelGlobals(display) {
299
+ const jupyter = display === null ? "any" : `import(${JSON.stringify(display)}).JupyterNamespace`;
300
+ return `declare global {
301
+ /** The notebook display API: display, md/html/svg templates, image, format, broadcast. */
302
+ var jupyter: ${jupyter};
303
+ /** Asks the frontend for a line of input. Null when the frontend didn't allow input for this cell. */
304
+ function prompt(message?: string, defaultValue?: string, options?: { password?: boolean }): string | null;
305
+ /** Asks a yes/no question over the Jupyter stdin channel. */
306
+ function confirm(message?: string): boolean;
307
+ }
308
+ export {};
309
+ `;
310
+ }
package/dist/package.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  /** Kept equal to package.json and deno.jsonc by test/package.test.ts. */
2
- export declare const VERSION = "0.5.0";
2
+ export declare const VERSION = "0.6.0";
3
3
  /**
4
4
  * The UTC day this version was released, set with VERSION. The publish
5
5
  * workflow refuses a tag unless it's today or yesterday, so it can't go
6
6
  * stale: see dependencyCutoff.
7
7
  */
8
- export declare const RELEASE_DATE = "2026-10-16";
8
+ export declare const RELEASE_DATE = "2026-10-18";
9
9
  /**
10
10
  * Deno blocks packages less than 24 hours old (minimum dependency age), so
11
11
  * a kernelspec relaunching a release from JSR would fail on release day. It
package/dist/package.js CHANGED
@@ -14,13 +14,13 @@ import { join, sep } from "node:path";
14
14
  import process from "node:process";
15
15
  import { fileURLToPath } from "node:url";
16
16
  /** Kept equal to package.json and deno.jsonc by test/package.test.ts. */
17
- export const VERSION = "0.5.0";
17
+ export const VERSION = "0.6.0";
18
18
  /**
19
19
  * The UTC day this version was released, set with VERSION. The publish
20
20
  * workflow refuses a tag unless it's today or yesterday, so it can't go
21
21
  * stale: see dependencyCutoff.
22
22
  */
23
- export const RELEASE_DATE = "2026-10-16";
23
+ export const RELEASE_DATE = "2026-10-18";
24
24
  /**
25
25
  * Deno blocks packages less than 24 hours old (minimum dependency age), so
26
26
  * a kernelspec relaunching a release from JSR would fail on release day. It
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Calls `onGone` once the parent named by JPY_PARENT_PID has exited: its pid
3
+ * is gone, or this process was reparented. A no-op when the variable is
4
+ * unset, so a kernel started by hand keeps running; and on Windows, where
5
+ * ipykernel's variable is a handle, not a pid. A pid that doesn't exist at
6
+ * start (the server is in another pid namespace) is ignored, leaving only
7
+ * the reparenting check.
8
+ */
9
+ export declare function watchParent(onGone: () => void, { env, intervalMs }?: {
10
+ env?: NodeJS.ProcessEnv | undefined;
11
+ intervalMs?: number | undefined;
12
+ }): () => void;
package/dist/parent.js ADDED
@@ -0,0 +1,39 @@
1
+ // The server that started the kernel, watched so a server that dies without
2
+ // shutting the kernel down (SIGKILL, a crash) doesn't leave it running with
3
+ // its runtime and ports (#254). jupyter_server and Ganymede put their pid in
4
+ // JPY_PARENT_PID; ipykernel's ParentPollerUnix polls for the same thing.
5
+ import process from "node:process";
6
+ function exists(pid) {
7
+ try {
8
+ process.kill(pid, 0);
9
+ return true;
10
+ }
11
+ catch (err) {
12
+ // EPERM: it exists, under another user.
13
+ return err.code === "EPERM";
14
+ }
15
+ }
16
+ /**
17
+ * Calls `onGone` once the parent named by JPY_PARENT_PID has exited: its pid
18
+ * is gone, or this process was reparented. A no-op when the variable is
19
+ * unset, so a kernel started by hand keeps running; and on Windows, where
20
+ * ipykernel's variable is a handle, not a pid. A pid that doesn't exist at
21
+ * start (the server is in another pid namespace) is ignored, leaving only
22
+ * the reparenting check.
23
+ */
24
+ export function watchParent(onGone, { env = process.env, intervalMs = 1000 } = {}) {
25
+ const parent = Number(env.JPY_PARENT_PID);
26
+ if (process.platform === "win32" || !Number.isInteger(parent) || parent <= 1)
27
+ return () => { };
28
+ const ppid = process.ppid;
29
+ const checkPid = exists(parent);
30
+ const timer = setInterval(() => {
31
+ if (process.ppid === ppid && (!checkPid || exists(parent)))
32
+ return;
33
+ clearInterval(timer);
34
+ onGone();
35
+ }, intervalMs);
36
+ // The kernel's sockets keep it running; this mustn't.
37
+ timer.unref();
38
+ return () => clearInterval(timer);
39
+ }
@@ -0,0 +1,13 @@
1
+ export interface PortSocket {
2
+ /** The remote end as hex `address:port`, as /proc prints it. */
3
+ remote: string;
4
+ state: string;
5
+ inode: string;
6
+ }
7
+ /** The sockets in a /proc/net/tcp table whose local port is `port`. */
8
+ export declare function socketsOnPort(table: string, port: number): PortSocket[];
9
+ /**
10
+ * One line per socket on `port`: its state, remote end and owning process,
11
+ * or an empty list when it can't be told (not Linux, or nothing found).
12
+ */
13
+ export declare function portHolders(port: number): string[];