@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
@@ -0,0 +1,105 @@
1
+ // Who holds a local TCP port, for the error when a connection-file port
2
+ // stays taken past the startup retry (#257). Linux only: it reads
3
+ // /proc/net/tcp{,6} and matches socket inodes against /proc/<pid>/fd.
4
+ import { readdirSync, readFileSync, readlinkSync } from "node:fs";
5
+ const STATES = {
6
+ "01": "ESTABLISHED",
7
+ "02": "SYN_SENT",
8
+ "03": "SYN_RECV",
9
+ "04": "FIN_WAIT1",
10
+ "05": "FIN_WAIT2",
11
+ "06": "TIME_WAIT",
12
+ "07": "CLOSE",
13
+ "08": "CLOSE_WAIT",
14
+ "09": "LAST_ACK",
15
+ "0A": "LISTEN",
16
+ "0B": "CLOSING",
17
+ };
18
+ /** The sockets in a /proc/net/tcp table whose local port is `port`. */
19
+ export function socketsOnPort(table, port) {
20
+ const found = [];
21
+ for (const line of table.split("\n").slice(1)) {
22
+ const fields = line.trim().split(/\s+/);
23
+ if (fields.length < 10)
24
+ continue;
25
+ const [, local, remote, state] = fields;
26
+ if (parseInt(local.slice(local.lastIndexOf(":") + 1), 16) !== port)
27
+ continue;
28
+ found.push({ remote, state: STATES[state] ?? state, inode: fields[9] });
29
+ }
30
+ return found;
31
+ }
32
+ function describePid(pid) {
33
+ let cmdline = "?";
34
+ let ppid = "?";
35
+ try {
36
+ cmdline = readFileSync(`/proc/${pid}/cmdline`, "utf8").split("\0").filter(Boolean).join(" ");
37
+ }
38
+ catch {
39
+ // Exited since.
40
+ }
41
+ try {
42
+ // The command name in parentheses may hold spaces; ppid is the second field after it.
43
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
44
+ ppid = stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1];
45
+ }
46
+ catch {
47
+ // Exited since.
48
+ }
49
+ return `pid ${pid} (ppid ${ppid}): ${cmdline}`;
50
+ }
51
+ /**
52
+ * One line per socket on `port`: its state, remote end and owning process,
53
+ * or an empty list when it can't be told (not Linux, or nothing found).
54
+ */
55
+ export function portHolders(port) {
56
+ const sockets = [];
57
+ for (const table of ["/proc/net/tcp", "/proc/net/tcp6"]) {
58
+ try {
59
+ sockets.push(...socketsOnPort(readFileSync(table, "utf8"), port));
60
+ }
61
+ catch {
62
+ // No /proc, or no IPv6.
63
+ }
64
+ }
65
+ if (sockets.length === 0)
66
+ return [];
67
+ const owners = new Map();
68
+ const wanted = new Set(sockets.map((s) => `socket:[${s.inode}]`));
69
+ let pids = [];
70
+ try {
71
+ pids = readdirSync("/proc").filter((name) => /^\d+$/.test(name));
72
+ }
73
+ catch {
74
+ // Leave owners unknown.
75
+ }
76
+ for (const pid of pids) {
77
+ let fds;
78
+ try {
79
+ fds = readdirSync(`/proc/${pid}/fd`);
80
+ }
81
+ catch {
82
+ continue; // Another user's, or exited.
83
+ }
84
+ for (const fd of fds) {
85
+ let link;
86
+ try {
87
+ link = readlinkSync(`/proc/${pid}/fd/${fd}`);
88
+ }
89
+ catch {
90
+ continue;
91
+ }
92
+ if (!wanted.has(link))
93
+ continue;
94
+ const list = owners.get(link) ?? [];
95
+ if (!list.includes(pid))
96
+ list.push(pid);
97
+ owners.set(link, list);
98
+ }
99
+ }
100
+ return sockets.map((s) => {
101
+ const pidsOf = owners.get(`socket:[${s.inode}]`) ?? [];
102
+ const who = pidsOf.length > 0 ? pidsOf.map(describePid).join("; ") : s.inode === "0" ? "no process" : "owner not visible";
103
+ return `${s.state} to ${s.remote}, ${who}`;
104
+ });
105
+ }
@@ -9,8 +9,13 @@ export declare function generatedToOriginalLines(mappings: string): number[];
9
9
  /** Both directions between the lines of a cell and of its script (0-based). */
10
10
  export declare class LineMap {
11
11
  #private;
12
- /** `offset`: lines the script adds before esbuild's output (the prelude). */
13
- constructor(mappings: string, offset?: number);
12
+ /**
13
+ * `offset`: lines the script adds before esbuild's output (the prelude).
14
+ * `generated`: that output. A stripper that keeps lines blanks a
15
+ * type-only line in place and still maps it; with the output given, a
16
+ * blank line counts as unmapped, so a breakpoint there moves down (#239).
17
+ */
18
+ constructor(mappings: string, offset?: number, generated?: string);
14
19
  /** The cell line a script line came from: its own mapping, or the nearest one above. */
15
20
  toOriginal(generated: number): number;
16
21
  /**
package/dist/sourcemap.js CHANGED
@@ -52,9 +52,15 @@ export function generatedToOriginalLines(mappings) {
52
52
  export class LineMap {
53
53
  #toOriginal;
54
54
  #toGenerated = new Map();
55
- /** `offset`: lines the script adds before esbuild's output (the prelude). */
56
- constructor(mappings, offset = 0) {
57
- const fromEsbuild = generatedToOriginalLines(mappings);
55
+ /**
56
+ * `offset`: lines the script adds before esbuild's output (the prelude).
57
+ * `generated`: that output. A stripper that keeps lines blanks a
58
+ * type-only line in place and still maps it; with the output given, a
59
+ * blank line counts as unmapped, so a breakpoint there moves down (#239).
60
+ */
61
+ constructor(mappings, offset = 0, generated) {
62
+ const blank = generated?.split("\n").map((line) => line.trim() === "");
63
+ const fromEsbuild = generatedToOriginalLines(mappings).map((original, line) => blank?.[line] ? -1 : original);
58
64
  this.#toOriginal = [...Array(offset).fill(-1), ...fromEsbuild];
59
65
  this.#toOriginal.forEach((original, generated) => {
60
66
  if (original !== -1 && !this.#toGenerated.has(original))
@@ -0,0 +1,2 @@
1
+ import { type TypeStripper } from "./transform.ts";
2
+ export declare const stripTypes: TypeStripper;
@@ -0,0 +1,44 @@
1
+ // The default type stripper: esbuild. Loaded by loadTypeStripper (see
2
+ // src/transform.ts) only when a kernel uses it, so a host that can't run
3
+ // esbuild's native binary never imports it (#238).
4
+ import { transformSync } from "esbuild";
5
+ import { CellSyntaxError } from "./transform.js";
6
+ function jsxSettings(jsx) {
7
+ if (jsx?.mode === "automatic") {
8
+ return { jsx: "automatic", jsxImportSource: jsx.importSource, jsxDev: jsx.development };
9
+ }
10
+ return { jsx: "transform", jsxFactory: jsx?.factory, jsxFragment: jsx?.fragment };
11
+ }
12
+ export const stripTypes = (code, options, sourcemap) => {
13
+ const compilerOptions = {
14
+ verbatimModuleSyntax: true,
15
+ ...(options.experimentalDecorators ? { experimentalDecorators: true } : {}),
16
+ };
17
+ // Like Deno: prefer TSX, fall back to TS for `<T>x` assertions.
18
+ let firstError;
19
+ for (const loader of ["tsx", "ts"]) {
20
+ try {
21
+ const result = transformSync(code, {
22
+ loader,
23
+ target: "esnext",
24
+ // Neither acorn nor V8 parses standard (TC39) decorators or
25
+ // `accessor` yet, so esbuild lowers them (#142).
26
+ supported: { decorators: false },
27
+ ...jsxSettings(options.jsx),
28
+ tsconfigRaw: { compilerOptions },
29
+ logLevel: "silent",
30
+ ...(sourcemap ? { sourcemap: "external" } : {}),
31
+ });
32
+ const map = sourcemap ? JSON.parse(result.map) : null;
33
+ const mappings = typeof map?.mappings === "string"
34
+ ? map.mappings
35
+ : "";
36
+ return { code: result.code, mappings };
37
+ }
38
+ catch (err) {
39
+ firstError ??= err;
40
+ }
41
+ }
42
+ const message = firstError instanceof Error ? firstError.message : String(firstError);
43
+ throw new CellSyntaxError(message.replace(/^Transform failed with \d+ errors?:\n/, ""));
44
+ };
@@ -0,0 +1,2 @@
1
+ import { type TypeStripper } from "./transform.ts";
2
+ export declare const stripTypes: TypeStripper;
@@ -0,0 +1,78 @@
1
+ // A type stripper in plain JavaScript, for a kernel host that can't run
2
+ // esbuild's native binary or WebAssembly (#234). sucrase is an optional
3
+ // dependency, imported only when a kernel picks it (see loadTypeStripper).
4
+ //
5
+ // sucrase removes types without parsing them fully or lowering anything,
6
+ // and keeps every line where it was. What it leaves alone, a cell can't
7
+ // use: decorators and `accessor` fields (which acorn and V8 don't parse
8
+ // yet, #142) and namespaces with values (which it drops), so those are
9
+ // rejected here with a clear error.
10
+ import { parse } from "acorn";
11
+ import { transform } from "sucrase";
12
+ import { CellSyntaxError } from "./transform.js";
13
+ const FILE = "cell.tsx";
14
+ function jsxSettings(options) {
15
+ const jsx = options.jsx;
16
+ if (jsx?.mode === "automatic") {
17
+ return { jsxRuntime: "automatic", jsxImportSource: jsx.importSource, production: !jsx.development };
18
+ }
19
+ return {
20
+ jsxRuntime: "classic",
21
+ jsxPragma: jsx?.factory ?? "React.createElement",
22
+ jsxFragmentPragma: jsx?.fragment ?? "React.Fragment",
23
+ production: true,
24
+ };
25
+ }
26
+ // A namespace that isn't `declare`d: sucrase drops it, values and all.
27
+ const NAMESPACE = /^[ \t]*(?:export\s+)?(?:namespace|module)\s+[\w$.]+\s*\{/m;
28
+ /** A clearer error than acorn's for syntax sucrase leaves in the output. */
29
+ function unsupported(js) {
30
+ if (!js.includes("@") && !js.includes("accessor"))
31
+ return null;
32
+ try {
33
+ parse(js, { ecmaVersion: "latest", sourceType: "module", allowAwaitOutsideFunction: true });
34
+ return null;
35
+ }
36
+ catch (err) {
37
+ const pos = err.pos;
38
+ if (typeof pos !== "number")
39
+ return null;
40
+ if (js[pos] === "@")
41
+ return "decorators are not supported with the sucrase type stripper";
42
+ if (/\baccessor\s*$/.test(js.slice(0, pos)))
43
+ return "accessor fields are not supported with the sucrase type stripper";
44
+ return null;
45
+ }
46
+ }
47
+ export const stripTypes = (code, options, sourcemap) => {
48
+ if (NAMESPACE.test(code)) {
49
+ throw new CellSyntaxError("namespaces are not supported with the sucrase type stripper; use declare namespace for types only");
50
+ }
51
+ // Like the esbuild stripper: prefer TSX, fall back to TS for `<T>x`
52
+ // assertions.
53
+ let firstError;
54
+ for (const transforms of [["typescript", "jsx"], ["typescript"]]) {
55
+ try {
56
+ const result = transform(code, {
57
+ transforms: [...transforms],
58
+ ...jsxSettings(options),
59
+ // As verbatimModuleSyntax: only `import type` goes.
60
+ keepUnusedImports: true,
61
+ disableESTransforms: true,
62
+ filePath: FILE,
63
+ ...(sourcemap ? { sourceMapOptions: { compiledFilename: FILE } } : {}),
64
+ });
65
+ const reason = unsupported(result.code);
66
+ if (reason !== null)
67
+ throw new CellSyntaxError(reason);
68
+ return { code: result.code, mappings: result.sourceMap?.mappings ?? "" };
69
+ }
70
+ catch (err) {
71
+ if (err instanceof CellSyntaxError)
72
+ throw err;
73
+ firstError ??= err;
74
+ }
75
+ }
76
+ const message = firstError instanceof Error ? firstError.message : String(firstError);
77
+ throw new CellSyntaxError(message.replace(`Error transforming ${FILE}: `, ""));
78
+ };
package/dist/target.d.ts CHANGED
@@ -44,6 +44,10 @@ export interface KernelTarget {
44
44
  readonly debug?: DebugChannel;
45
45
  /** Answers the target's pending prompt; `interrupted` makes it throw instead (#121). */
46
46
  replyInput(value: string | null, interrupted?: boolean): void;
47
+ /** What the host waits on, for a kernel that has stopped answering (#237). */
48
+ diagnose?(): Promise<CdpParams>;
49
+ /** Lets go of the last cell's result and error, once they are shown (#286). */
50
+ releaseResults?(): Promise<void>;
47
51
  kill(): void;
48
52
  }
49
53
  export interface LaunchOptions {
@@ -64,7 +68,7 @@ export declare function launchCommand(runtime: CdpRuntime, bootstrap: string, {
64
68
  * install and in Deno's npm cache alike.
65
69
  */
66
70
  export declare function esbuildBinary(resolve?: (specifier: string) => string, platform?: string, arch?: string): string | null;
67
- /** Messages the bootstrap sends through the CDP binding. */
71
+ /** Messages the bootstrap sends: message records on stdout, or Bun's channel. */
68
72
  export type TargetMessage = {
69
73
  kind: "iopub";
70
74
  msg_type: string;
@@ -83,18 +87,30 @@ export declare function parseJson(text: string): unknown;
83
87
  export declare function isTargetMessage(value: unknown): value is TargetMessage;
84
88
  /**
85
89
  * Splits one output pipe into text and barrier markers, and releases them
86
- * in order. A gated marker (one that announces a binding message) pauses
90
+ * in order. A gated marker (one that announces a message) pauses
87
91
  * the pipe until `gate(n)` resolves, i.e. until that message has been
88
92
  * delivered, so later output can't overtake it. A possible partial marker
89
93
  * at the end of a chunk is held back until the next chunk decides it.
94
+ *
95
+ * With `onMessage`, the pipe also carries message records. A record stands
96
+ * where its gated marker would, and pauses the pipe the same way.
90
97
  */
91
98
  export declare class PipeDemux {
92
99
  #private;
93
100
  readonly onText: (text: string) => void;
94
101
  readonly gate: (n: number) => Promise<void>;
95
- constructor(onText: (text: string) => void, gate?: (n: number) => Promise<void>);
102
+ readonly onMessage: ((msg: TargetMessage) => void) | null;
103
+ constructor(onText: (text: string) => void, gate?: (n: number) => Promise<void>, onMessage?: ((msg: TargetMessage) => void) | null);
96
104
  push(chunk: string): void;
97
105
  waitFor(n: number): Promise<void>;
106
+ /** Where the pipe stands, for a state dump (#237). */
107
+ state(): {
108
+ reached: number;
109
+ waitingFor: number[];
110
+ gatedOn: number | null;
111
+ queued: number;
112
+ held: number;
113
+ };
98
114
  /** Release everything (the process died). */
99
115
  close(): void;
100
116
  }
@@ -145,10 +161,18 @@ export declare class Target implements KernelTarget {
145
161
  }): Promise<Target>;
146
162
  /** The Debugger domain on this target's session, for src/debugger.ts. */
147
163
  get debug(): DebugChannel;
164
+ /**
165
+ * What the host is waiting on, whether the runtime still evaluates, runs
166
+ * immediates and runs timers, and where its JavaScript is if it is
167
+ * running any, for a kernel that has stopped answering (#237). The probes
168
+ * go over the control session, which no cell holds.
169
+ */
170
+ diagnose(): Promise<CdpParams>;
148
171
  /** Resolves once all output and messages sent so far have been delivered. */
149
172
  flush(): Promise<void>;
150
173
  /** Runs transformed cell code. Rejects with Interrupted after interrupt(). */
151
174
  evaluate(script: string): Promise<EvaluateResult>;
175
+ releaseResults(): Promise<void>;
152
176
  /**
153
177
  * Deno calls IsolateHandle::terminate_execution from another thread. The
154
178
  * CDP equivalent is Runtime.terminateExecution, sent on a second session