@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/README.md CHANGED
@@ -10,9 +10,19 @@ inspector speaks CDP works too.
10
10
  **Website:** <https://cdp-kernel.org/> ·
11
11
  **Docs:** [cdp-kernel.org/docs](https://cdp-kernel.org/docs/)
12
12
 
13
+ [![Try it on Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/tlockney/cdp-kernel/main?urlpath=lab/tree/getting-started.ipynb)
14
+ [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/tlockney/cdp-kernel?quickstart=1)
15
+
16
+ Binder opens JupyterLab with every runtime and the example notebooks,
17
+ nothing installed. A Codespace does the same in VS Code, with the
18
+ repository checked out ([Install › In a container](https://cdp-kernel.org/docs/install#in-a-container)).
19
+
13
20
  This is early software. It passes a conformance suite derived from Deno's
14
21
  own Jupyter integration tests on every target, and it's packaged for npm and
15
22
  JSR as `@tlockney/cdp-kernel`, but it hasn't seen much real use yet.
23
+ The [support policy](https://cdp-kernel.org/docs/support) says which
24
+ runtime versions and frontends are supported, and what may change before
25
+ 1.0.
16
26
 
17
27
  ## Features
18
28
 
@@ -29,7 +39,7 @@ JSR as `@tlockney/cdp-kernel`, but it hasn't seen much real use yet.
29
39
 
30
40
  ## Install
31
41
 
32
- Node 24+ host, from npm:
42
+ Node 22.18+ host, from npm:
33
43
 
34
44
  ```sh
35
45
  npm install -g @tlockney/cdp-kernel
@@ -43,11 +53,19 @@ deno run -A jsr:@tlockney/cdp-kernel/cli install --runtime deno
43
53
  ```
44
54
 
45
55
  Each install writes a Jupyter kernelspec (`cdp-node`, `cdp-deno`, …). Pick
46
- it in JupyterLab or VS Code. Linux and macOS are supported; Windows isn't
47
- yet. See [Install](https://cdp-kernel.org/docs/install)
48
- for `--deno-compat`, release-day installs on Deno, target runtime
56
+ it in JupyterLab or VS Code. To run notebooks with no Python installed,
57
+ serve JupyterLab with
58
+ [Ganymede](https://ganymede-server.com/docs/workstation). Linux and macOS
59
+ are supported; Windows isn't yet. See
60
+ [Install](https://cdp-kernel.org/docs/install) for `--deno-compat`, release-day installs on Deno, target runtime
49
61
  requirements, and installing from a checkout.
50
62
 
63
+ JupyterLab with every runtime and the examples, in a container:
64
+
65
+ ```sh
66
+ docker run --rm -p 8888:8888 ghcr.io/tlockney/cdp-kernel
67
+ ```
68
+
51
69
  ## Quick start
52
70
 
53
71
  ```ts
@@ -68,10 +86,13 @@ New to Jupyter? Start with
68
86
  [Widgets and comms](https://cdp-kernel.org/docs/widgets),
69
87
  [Debugger](https://cdp-kernel.org/docs/debugger)
70
88
  - Reference: [Editor types](https://cdp-kernel.org/docs/editor-types),
71
- [Known gaps](https://cdp-kernel.org/docs/known-gaps)
89
+ [Known gaps](https://cdp-kernel.org/docs/known-gaps),
90
+ [Support policy](https://cdp-kernel.org/docs/support)
72
91
  - Internals: [Architecture](https://cdp-kernel.org/docs/architecture),
73
92
  [Runtime integrations](https://cdp-kernel.org/docs/integrations)
74
- - Contributing: [Development](https://cdp-kernel.org/docs/development)
93
+ - Contributing: [CONTRIBUTING.md](CONTRIBUTING.md),
94
+ [Development](https://cdp-kernel.org/docs/development),
95
+ [Security policy](SECURITY.md)
75
96
 
76
97
  The [feasibility study](https://cdp-kernel.org/docs/feasibility-study)
77
98
  behind the project covers the design, the alternatives considered (workerd,
package/dist/cdp.d.ts CHANGED
@@ -33,6 +33,8 @@ export declare class CdpSession {
33
33
  static connect(url: string): Promise<CdpSession>;
34
34
  on(method: string, listener: (params: CdpParams) => void): void;
35
35
  send(method: string, params?: CdpParams): Promise<CdpParams>;
36
+ /** In-flight calls, oldest first, with how long each has waited: for a state dump (#237). */
37
+ pendingCalls(): string[];
36
38
  /** Reject every in-flight call; their late responses are then ignored. */
37
39
  abandonPending(): void;
38
40
  close(): void;
package/dist/cdp.js CHANGED
@@ -69,10 +69,15 @@ export class CdpSession {
69
69
  return Promise.reject(new Error("CDP connection closed"));
70
70
  const id = ++this.#nextId;
71
71
  return new Promise((resolve, reject) => {
72
- this.#pending.set(id, { method, resolve, reject });
72
+ this.#pending.set(id, { method, sent: Date.now(), resolve, reject });
73
73
  this.#ws.send(JSON.stringify({ id, method, params }));
74
74
  });
75
75
  }
76
+ /** In-flight calls, oldest first, with how long each has waited: for a state dump (#237). */
77
+ pendingCalls() {
78
+ const now = Date.now();
79
+ return [...this.#pending.values()].map((p) => `${p.method} (${now - p.sent} ms)`);
80
+ }
76
81
  /** Reject every in-flight call; their late responses are then ignored. */
77
82
  abandonPending() {
78
83
  for (const pending of this.#pending.values()) {
@@ -24,7 +24,7 @@ import { build } from "esbuild";
24
24
  import { isRecord } from "./cdp.js";
25
25
  import { parseJsonc } from "./deno-config.js";
26
26
  import { runtimeDir } from "./package.js";
27
- import { killOnExit } from "./children.js";
27
+ import { childPids, killOnExit, reapWith } from "./children.js";
28
28
  import { cellMeta, WorkerdTarget } from "./workerd-target.js";
29
29
  import { builtinAliases, findWranglerConfig } from "./workers-project.js";
30
30
  /** The project keys the generated config keeps: bindings and compatibility. */
@@ -122,16 +122,10 @@ function channelOf(ws) {
122
122
  },
123
123
  };
124
124
  }
125
- /** celld dev's own children, which run in their own process groups. */
126
- function childPids(pid) {
127
- try {
128
- return execFileSync("pgrep", ["-P", String(pid)], { encoding: "utf8" }).split(/\s+/).filter(Boolean).map(Number);
129
- }
130
- catch {
131
- return []; // none
132
- }
133
- }
134
- /** Kills celld dev's node, then the supervisor itself. */
125
+ /**
126
+ * Kills celld dev's own children, which run in their own process groups,
127
+ * then the supervisor itself.
128
+ */
135
129
  function killTree(child) {
136
130
  for (const pid of childPids(child.pid ?? -1)) {
137
131
  try {
@@ -222,7 +216,7 @@ class CelldHost {
222
216
  killOnExit(child, () => {
223
217
  killTree(child);
224
218
  removeFiles();
225
- });
219
+ }, { dir: projectConfig === null ? dir : undefined });
226
220
  // Cell output is captured in the driver; celld's own lines go to the
227
221
  // server log.
228
222
  // `early` keeps that output for a startup error only, and stops growing
@@ -262,6 +256,9 @@ class CelldHost {
262
256
  })();
263
257
  try {
264
258
  const host = new CelldHost(child, await Promise.race([ready, exited]), `celld ${version}`.trim(), projectConfig ?? undefined);
259
+ // Its server, in its own process group, outlives a SIGKILLed celld dev.
260
+ for (const pid of childPids(child.pid ?? -1))
261
+ reapWith(pid, child);
265
262
  starting = false;
266
263
  early = "";
267
264
  return host;
@@ -1,7 +1,29 @@
1
- import type { ChildProcess } from "node:child_process";
1
+ import { type ChildProcess } from "node:child_process";
2
+ /**
3
+ * The helper's script. Lines on stdin: `+pid` registers a pid, `-pid`
4
+ * forgets it (it exited, and the pid may be reused), `d<path>` registers a
5
+ * directory. In its own session, so a killpg of this process's group (as
6
+ * Jupyter's kill does) doesn't take it down first.
7
+ */
8
+ export declare const REAPER_SCRIPT = "trap '' HUP INT\npids=\" \"\ndirs=\"\"\nwhile IFS= read -r line; do\n case $line in\n +*) pids=\"$pids${line#?} \" ;;\n -*) pids=$(printf '%s' \"$pids\" | sed \"s/ ${line#?} / /\") ;;\n d*) dirs=\"$dirs${line#?}\n\" ;;\n esac\ndone\nkill -9 $pids 2>/dev/null\nprintf '%s' \"$dirs\" | while IFS= read -r dir; do rm -rf -- \"$dir\"; done\n";
9
+ /**
10
+ * Kills `pid` if this process dies, until the returned function is called
11
+ * (when the process is known to have exited: its pid may be reused).
12
+ */
13
+ export declare function reap(pid: number): () => void;
14
+ /**
15
+ * Kills `pid`, which isn't this process's child (a runtime's own child in
16
+ * its own process group), if this process dies while `owner` runs.
17
+ */
18
+ export declare function reapWith(pid: number, owner: ChildProcess): void;
19
+ /** `pid`'s children whose command line matches `pattern` (pgrep's -f). */
20
+ export declare function childPids(pid: number, pattern?: string): number[];
2
21
  /**
3
22
  * Kills `child` if it's still running when this process exits. `cleanup`
4
23
  * replaces the default SIGKILL; it must be synchronous, as exit hooks are,
5
- * and do whatever the child's own exit listener would have done.
24
+ * and do whatever the child's own exit listener would have done. If this
25
+ * process is SIGKILLed, the helper kills `child` and removes `dir`.
6
26
  */
7
- export declare function killOnExit(child: ChildProcess, cleanup?: () => void): void;
27
+ export declare function killOnExit(child: ChildProcess, cleanup?: () => void, { dir }?: {
28
+ dir?: string;
29
+ }): void;
package/dist/children.js CHANGED
@@ -1,7 +1,15 @@
1
1
  // Runtimes this process started, killed when it exits however it exits
2
2
  // short of SIGKILL: a target left behind keeps running, reparented to pid 1,
3
3
  // with whatever temp directory it uses (#127, #218).
4
+ //
5
+ // SIGKILL runs no exit hook, so a helper outside this process also knows the
6
+ // runtimes' pids (#240): a detached sh reading this process's pipe, which
7
+ // kills whatever is still registered, and removes the registered temp
8
+ // directories, once the pipe closes. That covers what nothing in a runtime
9
+ // can notice: celld dev and its server, and a Node or Deno runtime stuck in
10
+ // a cell's synchronous loop, where the #219 watchdog never gets to run.
4
11
  import process from "node:process";
12
+ import { execFileSync, spawn } from "node:child_process";
5
13
  const cleanups = new Set();
6
14
  function runAll() {
7
15
  for (const cleanup of cleanups) {
@@ -15,12 +23,82 @@ function runAll() {
15
23
  cleanups.clear();
16
24
  }
17
25
  let hooked = false;
26
+ /**
27
+ * The helper's script. Lines on stdin: `+pid` registers a pid, `-pid`
28
+ * forgets it (it exited, and the pid may be reused), `d<path>` registers a
29
+ * directory. In its own session, so a killpg of this process's group (as
30
+ * Jupyter's kill does) doesn't take it down first.
31
+ */
32
+ export const REAPER_SCRIPT = `trap '' HUP INT
33
+ pids=" "
34
+ dirs=""
35
+ while IFS= read -r line; do
36
+ case $line in
37
+ +*) pids="$pids\${line#?} " ;;
38
+ -*) pids=$(printf '%s' "$pids" | sed "s/ \${line#?} / /") ;;
39
+ d*) dirs="$dirs\${line#?}
40
+ " ;;
41
+ esac
42
+ done
43
+ kill -9 $pids 2>/dev/null
44
+ printf '%s' "$dirs" | while IFS= read -r dir; do rm -rf -- "$dir"; done
45
+ `;
46
+ let reaper;
47
+ /** Sends `line` to the helper, starting it first; a no-op on Windows. */
48
+ function tellReaper(line) {
49
+ if (reaper === undefined) {
50
+ reaper = null;
51
+ if (process.platform === "win32")
52
+ return;
53
+ try {
54
+ const child = spawn("/bin/sh", ["-c", REAPER_SCRIPT], { cwd: "/", detached: true, stdio: ["pipe", "ignore", "ignore"] });
55
+ child.once("error", () => (reaper = null));
56
+ child.stdin?.on("error", () => (reaper = null));
57
+ // Neither the helper nor its pipe keeps this process running.
58
+ child.unref();
59
+ child.stdin?.unref?.();
60
+ reaper = child;
61
+ }
62
+ catch {
63
+ return;
64
+ }
65
+ }
66
+ reaper?.stdin?.write(`${line}\n`);
67
+ }
68
+ /**
69
+ * Kills `pid` if this process dies, until the returned function is called
70
+ * (when the process is known to have exited: its pid may be reused).
71
+ */
72
+ export function reap(pid) {
73
+ tellReaper(`+${pid}`);
74
+ return () => tellReaper(`-${pid}`);
75
+ }
76
+ /**
77
+ * Kills `pid`, which isn't this process's child (a runtime's own child in
78
+ * its own process group), if this process dies while `owner` runs.
79
+ */
80
+ export function reapWith(pid, owner) {
81
+ if (owner.exitCode !== null || owner.signalCode !== null)
82
+ return;
83
+ owner.once("exit", reap(pid));
84
+ }
85
+ /** `pid`'s children whose command line matches `pattern` (pgrep's -f). */
86
+ export function childPids(pid, pattern) {
87
+ try {
88
+ const args = ["-P", String(pid), ...(pattern === undefined ? [] : ["-f", pattern])];
89
+ return execFileSync("pgrep", args, { encoding: "utf8" }).split(/\s+/).filter(Boolean).map(Number);
90
+ }
91
+ catch {
92
+ return []; // none, or no pgrep
93
+ }
94
+ }
18
95
  /**
19
96
  * Kills `child` if it's still running when this process exits. `cleanup`
20
97
  * replaces the default SIGKILL; it must be synchronous, as exit hooks are,
21
- * and do whatever the child's own exit listener would have done.
98
+ * and do whatever the child's own exit listener would have done. If this
99
+ * process is SIGKILLed, the helper kills `child` and removes `dir`.
22
100
  */
23
- export function killOnExit(child, cleanup = () => child.kill("SIGKILL")) {
101
+ export function killOnExit(child, cleanup = () => child.kill("SIGKILL"), { dir } = {}) {
24
102
  if (!hooked) {
25
103
  hooked = true;
26
104
  process.on("exit", runAll);
@@ -29,4 +107,8 @@ export function killOnExit(child, cleanup = () => child.kill("SIGKILL")) {
29
107
  return;
30
108
  cleanups.add(cleanup);
31
109
  child.once("exit", () => cleanups.delete(cleanup));
110
+ if (dir !== undefined)
111
+ tellReaper(`d${dir}`);
112
+ if (child.pid !== undefined)
113
+ reapWith(child.pid, child);
32
114
  }
@@ -0,0 +1,30 @@
1
+ export type DenoResolution = {
2
+ kind: "npm";
3
+ dir: string;
4
+ subpath: string;
5
+ } | {
6
+ kind: "remote";
7
+ path: string;
8
+ };
9
+ export type RunDeno = (args: string[]) => {
10
+ status: number | null;
11
+ stdout: string;
12
+ };
13
+ export declare class DenoResolver {
14
+ #private;
15
+ /** Where remote modules appear to the language service. */
16
+ readonly root: string;
17
+ constructor({ deno, config, root }: {
18
+ deno?: string;
19
+ config?: string | null;
20
+ root: string;
21
+ }, run?: RunDeno);
22
+ /** The virtual path for a remote module's URL, e.g. <root>/https/jsr.io/@std/path/1.1.6/mod.ts. */
23
+ pathFor(url: string): string | null;
24
+ /** The cached file behind a virtual path, if a lookup has found it. */
25
+ localFile(path: string): string | undefined;
26
+ /** Where `specifier` resolves for Deno, from what's already in its cache. */
27
+ lookup(specifier: string): DenoResolution | null;
28
+ }
29
+ /** `deno types`: declarations for the Deno namespace and its web globals. */
30
+ export declare function denoTypes(run?: RunDeno): string | null;
@@ -0,0 +1,93 @@
1
+ // Types for a Deno target's imports (#275). Deno resolves npm:, jsr:, URLs
2
+ // and its project's import map itself and keeps what it fetched in its own
3
+ // cache, so the language service asks it where a specifier went:
4
+ // `deno info --json` over a one-line module importing it. npm packages map
5
+ // to their directory in Deno's npm cache; remote modules (jsr:, https:)
6
+ // are stored under hashed names without an extension, so the language
7
+ // service sees each under a virtual path built from its URL, which keeps
8
+ // relative imports between them working.
9
+ import { spawnSync } from "node:child_process";
10
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
11
+ import { tmpdir } from "node:os";
12
+ import { join } from "node:path";
13
+ function runDeno(deno) {
14
+ return (args) => {
15
+ const result = spawnSync(deno, args, { encoding: "utf8", timeout: 30_000, maxBuffer: 64 * 1024 * 1024 });
16
+ return { status: result.status, stdout: result.stdout ?? "" };
17
+ };
18
+ }
19
+ export class DenoResolver {
20
+ #run;
21
+ #config;
22
+ /** Where remote modules appear to the language service. */
23
+ root;
24
+ #lookups = new Map();
25
+ /** Virtual path -> the cached file holding that URL's module. */
26
+ #remote = new Map();
27
+ constructor({ deno = "deno", config = null, root }, run = runDeno(deno)) {
28
+ this.#run = run;
29
+ this.#config = config;
30
+ this.root = root;
31
+ }
32
+ /** The virtual path for a remote module's URL, e.g. <root>/https/jsr.io/@std/path/1.1.6/mod.ts. */
33
+ pathFor(url) {
34
+ const m = /^(https?):\/\/(.+)$/.exec(url);
35
+ return m ? join(this.root, m[1], m[2]) : null;
36
+ }
37
+ /** The cached file behind a virtual path, if a lookup has found it. */
38
+ localFile(path) {
39
+ return this.#remote.get(path);
40
+ }
41
+ /** Where `specifier` resolves for Deno, from what's already in its cache. */
42
+ lookup(specifier) {
43
+ if (this.#lookups.has(specifier))
44
+ return this.#lookups.get(specifier) ?? null;
45
+ const resolution = this.#lookup(specifier);
46
+ this.#lookups.set(specifier, resolution);
47
+ return resolution;
48
+ }
49
+ #lookup(specifier) {
50
+ const dir = mkdtempSync(join(tmpdir(), "cdpk-deno-info-"));
51
+ try {
52
+ const entry = join(dir, "entry.ts");
53
+ writeFileSync(entry, `import ${JSON.stringify(specifier)};\n`);
54
+ const flags = this.#config === null ? ["--node-modules-dir=none"] : ["--config", this.#config];
55
+ const result = this.#run(["info", "--json", "--no-lock", ...flags, entry]);
56
+ if (result.status !== 0)
57
+ return null;
58
+ return this.#read(JSON.parse(result.stdout));
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ finally {
64
+ rmSync(dir, { recursive: true, force: true });
65
+ }
66
+ }
67
+ #read(info) {
68
+ for (const mod of info.modules) {
69
+ const path = mod.local === undefined ? null : this.pathFor(mod.specifier);
70
+ if (path !== null && mod.local !== undefined)
71
+ this.#remote.set(path, mod.local);
72
+ }
73
+ const root = info.modules.find((m) => m.specifier === info.roots[0]);
74
+ const dep = root?.dependencies?.[0];
75
+ let target = dep?.code?.specifier ?? dep?.specifier;
76
+ if (target === undefined)
77
+ return null;
78
+ for (let i = 0; i < 10 && info.redirects[target] !== undefined; i++)
79
+ target = info.redirects[target];
80
+ const npm = /^npm:\/?((?:@[^/]+\/)?[^/@]+@[^/]+)(\/.*)?$/.exec(target);
81
+ if (npm) {
82
+ const dir = info.npmPackages[npm[1]]?.localPath;
83
+ return dir === undefined ? null : { kind: "npm", dir, subpath: npm[2] ?? "" };
84
+ }
85
+ const path = this.pathFor(target);
86
+ return path !== null && this.#remote.has(path) ? { kind: "remote", path } : null;
87
+ }
88
+ }
89
+ /** `deno types`: declarations for the Deno namespace and its web globals. */
90
+ export function denoTypes(run = runDeno("deno")) {
91
+ const result = run(["types"]);
92
+ return result.status === 0 ? result.stdout : null;
93
+ }
package/dist/kernel.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type RuntimeName } from "./target.ts";
2
+ import { type TypeStripper, type TypeStripperName } from "./transform.ts";
2
3
  export interface ConnectionInfo {
3
4
  ip: string;
4
5
  key: string;
@@ -14,8 +15,13 @@ export declare function parseConnectionInfo(json: string): ConnectionInfo;
14
15
  export declare class Kernel {
15
16
  #private;
16
17
  constructor(conn: ConnectionInfo, runtime: RuntimeName, options?: {
17
- denoCompat?: boolean; /** Defaults to the process cwd, which Jupyter sets. */
18
+ denoCompat?: boolean;
19
+ /** Defaults to the process cwd, which Jupyter sets. */
18
20
  notebookDir?: string;
21
+ /** What strips cells' types; esbuild by default (#238). */
22
+ typeStrip?: TypeStripperName | TypeStripper;
23
+ /** Typed completion from TypeScript's language service; on by default (#273). */
24
+ languageService?: boolean;
19
25
  });
20
26
  run(): Promise<void>;
21
27
  }