@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.
- package/README.md +27 -6
- package/dist/cdp.d.ts +2 -0
- package/dist/cdp.js +6 -1
- package/dist/celld-target.js +9 -12
- package/dist/children.d.ts +25 -3
- package/dist/children.js +84 -2
- package/dist/deno-types.d.ts +30 -0
- package/dist/deno-types.js +93 -0
- package/dist/kernel.d.ts +7 -1
- package/dist/kernel.js +187 -29
- package/dist/language-service-worker.d.ts +1 -0
- package/dist/language-service-worker.js +88 -0
- package/dist/language-service.d.ts +64 -0
- package/dist/language-service.js +128 -0
- package/dist/main.js +102 -8
- package/dist/mod.d.ts +1 -0
- package/dist/mod.js +1 -0
- package/dist/notebook-types.d.ts +62 -0
- package/dist/notebook-types.js +310 -0
- package/dist/package.d.ts +2 -2
- package/dist/package.js +2 -2
- package/dist/parent.d.ts +12 -0
- package/dist/parent.js +39 -0
- package/dist/port-holder.d.ts +13 -0
- package/dist/port-holder.js +105 -0
- package/dist/sourcemap.d.ts +7 -2
- package/dist/sourcemap.js +9 -3
- package/dist/strip-esbuild.d.ts +2 -0
- package/dist/strip-esbuild.js +44 -0
- package/dist/strip-sucrase.d.ts +2 -0
- package/dist/strip-sucrase.js +78 -0
- package/dist/target.d.ts +27 -3
- package/dist/target.js +193 -36
- package/dist/transform.d.ts +21 -1
- package/dist/transform.js +38 -42
- package/dist/workerd-target.js +1 -1
- package/dist/workers-project.js +13 -2
- package/dist/zmtp.d.ts +2 -0
- package/dist/zmtp.js +12 -0
- package/package.json +9 -4
- package/runtime/bootstrap.mjs +113 -33
- package/runtime/introspect.mjs +1 -1
- package/runtime/node-hooks.d.mts +6 -0
- package/runtime/node-hooks.mjs +11 -0
package/dist/kernel.js
CHANGED
|
@@ -5,20 +5,25 @@
|
|
|
5
5
|
// The thirteen op_jupyter_* calls into Deno's REPL isolate are replaced by
|
|
6
6
|
// a Target driven over CDP (src/target.ts).
|
|
7
7
|
import { Buffer } from "node:buffer";
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
8
10
|
import process from "node:process";
|
|
9
11
|
import { randomUUID } from "node:crypto";
|
|
10
12
|
import { decodeMessage, encodeMessage, makeHeader, PROTOCOL_VERSION, verify } from "./wire.js";
|
|
11
|
-
import { PubSocket, RouterSocket, startHeartbeat } from "./zmtp.js";
|
|
13
|
+
import { listening, PubSocket, RouterSocket, startHeartbeat } from "./zmtp.js";
|
|
12
14
|
import { Interrupted, Target } from "./target.js";
|
|
13
15
|
import { BunTarget } from "./bun-target.js";
|
|
14
16
|
import { Debugger } from "./debugger.js";
|
|
15
17
|
import { WorkerdTarget } from "./workerd-target.js";
|
|
16
18
|
import { launchCelld } from "./celld-target.js";
|
|
17
|
-
import { CellSyntaxError, objectLiteralCandidate } from "./transform.js";
|
|
19
|
+
import { CellSyntaxError, loadTypeStripper, objectLiteralCandidate, } from "./transform.js";
|
|
18
20
|
import { loadDenoProject } from "./deno-config.js";
|
|
19
21
|
import { checkIsComplete, codePointToUtf16Index, exprAtCursor, inspectTarget, utf16ToCodePointIndex } from "./text.js";
|
|
20
22
|
import { queryHistory } from "./history.js";
|
|
23
|
+
import { LanguageService } from "./language-service.js";
|
|
24
|
+
import { findWranglerConfig } from "./workers-project.js";
|
|
21
25
|
import { VERSION } from "./package.js";
|
|
26
|
+
import { watchParent } from "./parent.js";
|
|
22
27
|
export function parseConnectionInfo(json) {
|
|
23
28
|
const raw = JSON.parse(json);
|
|
24
29
|
if (typeof raw !== "object" || raw === null)
|
|
@@ -50,8 +55,11 @@ export function parseConnectionInfo(json) {
|
|
|
50
55
|
hb_port: info.hb_port,
|
|
51
56
|
};
|
|
52
57
|
}
|
|
53
|
-
/**
|
|
54
|
-
|
|
58
|
+
/**
|
|
59
|
+
* How long interrupt_request may wait on the target before it's answered
|
|
60
|
+
* anyway (#121). Under the 5 s clients allow control requests (#253).
|
|
61
|
+
*/
|
|
62
|
+
const INTERRUPT_REPLY_WAIT_MS = 1_000;
|
|
55
63
|
const NO_INPUT = { value: null, interrupted: false };
|
|
56
64
|
/** Shell requests other than execute_request, and their reply types. */
|
|
57
65
|
const SHELL_REPLIES = {
|
|
@@ -62,16 +70,73 @@ const SHELL_REPLIES = {
|
|
|
62
70
|
history_request: "history_reply",
|
|
63
71
|
comm_info_request: "comm_info_reply",
|
|
64
72
|
};
|
|
73
|
+
/** The target Node's major version, for @types/node: the host's, which is usually the same binary. */
|
|
74
|
+
function nodeMajor() {
|
|
75
|
+
return process.versions.node?.split(".")[0] ?? "*";
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Where the language service gets each target's globals and imports from
|
|
79
|
+
* (#273, #275). A `--deno-compat` Node target gets Node's types only: its
|
|
80
|
+
* `Deno` namespace is partial, and Deno's declarations would promise
|
|
81
|
+
* members it doesn't have.
|
|
82
|
+
*/
|
|
83
|
+
function languageServiceOptions(runtime, notebookDir, project) {
|
|
84
|
+
const base = { notebookDir, npmCache: "npm", deno: null, denoTypes: false, typeFiles: [], typePackages: [] };
|
|
85
|
+
switch (runtime) {
|
|
86
|
+
case "node":
|
|
87
|
+
return { ...base, typePackages: [{ name: "@types/node", range: nodeMajor() }] };
|
|
88
|
+
case "deno":
|
|
89
|
+
return { ...base, npmCache: null, deno: { config: project?.configPath ?? null }, denoTypes: true };
|
|
90
|
+
case "bun":
|
|
91
|
+
return { ...base, npmCache: "bun", typePackages: [{ name: "@types/bun", range: "1" }] };
|
|
92
|
+
case "workerd":
|
|
93
|
+
case "celld": {
|
|
94
|
+
// A Workers project's own types (`wrangler types`) know its bindings.
|
|
95
|
+
const config = findWranglerConfig(notebookDir);
|
|
96
|
+
const generated = config === null ? null : join(dirname(config), "worker-configuration.d.ts");
|
|
97
|
+
return generated !== null && existsSync(generated)
|
|
98
|
+
? { ...base, typeFiles: [generated] }
|
|
99
|
+
: { ...base, typePackages: [{ name: "@cloudflare/workers-types", range: "*" }] };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** Runtime inspection text without its Type and Signature lines, which the declared type replaces. */
|
|
104
|
+
function withoutSignature(text) {
|
|
105
|
+
return text.split("\n").filter((line) => !/^(?:Type|Signature): /.test(line)).join("\n");
|
|
106
|
+
}
|
|
107
|
+
/** A TypeScript completion kind as a Jupyter completion type (JupyterLab picks its icon by these). */
|
|
108
|
+
function jupyterType(kind) {
|
|
109
|
+
switch (kind) {
|
|
110
|
+
case undefined:
|
|
111
|
+
return "<unknown>";
|
|
112
|
+
case "var":
|
|
113
|
+
case "let":
|
|
114
|
+
case "const":
|
|
115
|
+
case "local var":
|
|
116
|
+
case "parameter":
|
|
117
|
+
return "variable";
|
|
118
|
+
case "local function":
|
|
119
|
+
return "function";
|
|
120
|
+
case "type":
|
|
121
|
+
case "alias":
|
|
122
|
+
return "type";
|
|
123
|
+
default:
|
|
124
|
+
return kind;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
65
127
|
export class Kernel {
|
|
66
128
|
#conn;
|
|
67
129
|
#runtime;
|
|
68
130
|
#denoCompat;
|
|
131
|
+
#typeStrip;
|
|
69
132
|
// A Deno target in a Deno project uses its config (#13).
|
|
70
133
|
#project;
|
|
71
134
|
#projectWarning = null;
|
|
72
135
|
#noticeShownFor = null;
|
|
73
136
|
/** JupyterLab's visual debugger, for targets driven over CDP (#18). */
|
|
74
137
|
#debugger;
|
|
138
|
+
/** Typed completion from TypeScript's language service (#273). */
|
|
139
|
+
#types;
|
|
75
140
|
/** Cells run with store_history, for history_request (#10). */
|
|
76
141
|
#history = [];
|
|
77
142
|
#session = randomUUID();
|
|
@@ -79,6 +144,8 @@ export class Kernel {
|
|
|
79
144
|
#control;
|
|
80
145
|
#stdin;
|
|
81
146
|
#iopub;
|
|
147
|
+
/** Settles once all five ports are bound (#257). */
|
|
148
|
+
#bound;
|
|
82
149
|
#target = null;
|
|
83
150
|
#targetStarting = null;
|
|
84
151
|
#executionCount = 0;
|
|
@@ -95,6 +162,8 @@ export class Kernel {
|
|
|
95
162
|
#input = null;
|
|
96
163
|
/** An interrupt is waiting on the target. */
|
|
97
164
|
#interrupting = false;
|
|
165
|
+
/** The shell message being handled, and since when. */
|
|
166
|
+
#handling = null;
|
|
98
167
|
// Open comms, for comm_info_request: comm_id -> target_name (#9).
|
|
99
168
|
#comms = new Map();
|
|
100
169
|
#shuttingDown = false;
|
|
@@ -102,15 +171,21 @@ export class Kernel {
|
|
|
102
171
|
this.#conn = conn;
|
|
103
172
|
this.#runtime = runtime;
|
|
104
173
|
this.#denoCompat = options.denoCompat ?? false;
|
|
105
|
-
this.#
|
|
174
|
+
this.#typeStrip = options.typeStrip ?? "esbuild";
|
|
175
|
+
const notebookDir = options.notebookDir ?? process.cwd();
|
|
176
|
+
this.#project = runtime === "deno" ? this.#loadProject(notebookDir) : null;
|
|
177
|
+
this.#types = options.languageService === false
|
|
178
|
+
? null
|
|
179
|
+
: new LanguageService(languageServiceOptions(runtime, notebookDir, this.#project), { log: (text) => this.#log(text) });
|
|
106
180
|
this.#debugger = runtime === "node" || runtime === "deno"
|
|
107
181
|
? new Debugger((event) => this.#publish("debug_event", event), () => this.#transformOptions())
|
|
108
182
|
: null;
|
|
109
|
-
startHeartbeat(conn.hb_port, conn.ip);
|
|
183
|
+
const heartbeat = startHeartbeat(conn.hb_port, conn.ip);
|
|
110
184
|
this.#shell = new RouterSocket(conn.shell_port, conn.ip);
|
|
111
185
|
this.#control = new RouterSocket(conn.control_port, conn.ip);
|
|
112
186
|
this.#stdin = new RouterSocket(conn.stdin_port, conn.ip);
|
|
113
187
|
this.#iopub = new PubSocket(conn.iopub_port, conn.ip);
|
|
188
|
+
this.#bound = Promise.all([heartbeat, this.#shell.server, this.#control.server, this.#stdin.server, this.#iopub.server].map(listening));
|
|
114
189
|
}
|
|
115
190
|
async run() {
|
|
116
191
|
// Start the runtime eagerly so the first cell doesn't pay for it.
|
|
@@ -124,6 +199,18 @@ export class Kernel {
|
|
|
124
199
|
process.exit(code);
|
|
125
200
|
});
|
|
126
201
|
}
|
|
202
|
+
// A server killed before it could shut the kernel down (#254).
|
|
203
|
+
watchParent(() => {
|
|
204
|
+
this.#log(`parent ${process.env.JPY_PARENT_PID} exited; shutting down`);
|
|
205
|
+
this.#shuttingDown = true;
|
|
206
|
+
process.exit(1);
|
|
207
|
+
});
|
|
208
|
+
// A state dump on stderr, for a kernel that has stopped answering (#237).
|
|
209
|
+
if (process.platform !== "win32")
|
|
210
|
+
process.on("SIGUSR2", () => void this.#logState());
|
|
211
|
+
// Transforms are synchronous, so the stripper loads before any cell.
|
|
212
|
+
if (typeof this.#typeStrip === "string")
|
|
213
|
+
await loadTypeStripper(this.#typeStrip);
|
|
127
214
|
await Promise.all([this.#loop(this.#shell, (m, p) => this.#handleShell(m, p)),
|
|
128
215
|
this.#loop(this.#control, (m, p) => this.#handleControl(m, p)),
|
|
129
216
|
this.#loop(this.#stdin, (m) => this.#handleStdin(m))]);
|
|
@@ -131,6 +218,21 @@ export class Kernel {
|
|
|
131
218
|
#log(text) {
|
|
132
219
|
process.stderr.write(`[cdp-kernel] ${text}\n`);
|
|
133
220
|
}
|
|
221
|
+
async #logState() {
|
|
222
|
+
const handling = this.#handling;
|
|
223
|
+
const target = this.#target;
|
|
224
|
+
let targetState = target === null ? (this.#targetStarting === null ? null : "starting") : target.runtime;
|
|
225
|
+
if (target?.diagnose)
|
|
226
|
+
targetState = await target.diagnose().catch((err) => `diagnose failed: ${err}`);
|
|
227
|
+
this.#log(`state: ${JSON.stringify({
|
|
228
|
+
shell: handling && { msgType: handling.msgType, msgId: handling.msgId, ms: Date.now() - handling.since },
|
|
229
|
+
queued: this.#shell.incoming.size,
|
|
230
|
+
executing: this.#executing,
|
|
231
|
+
interrupting: this.#interrupting,
|
|
232
|
+
awaitingInput: this.#input !== null,
|
|
233
|
+
target: targetState,
|
|
234
|
+
})}`);
|
|
235
|
+
}
|
|
134
236
|
#loadProject(notebookDir) {
|
|
135
237
|
try {
|
|
136
238
|
return loadDenoProject(notebookDir);
|
|
@@ -145,7 +247,7 @@ export class Kernel {
|
|
|
145
247
|
}
|
|
146
248
|
}
|
|
147
249
|
#transformOptions() {
|
|
148
|
-
return this.#project?.transform
|
|
250
|
+
return { ...this.#project?.transform, typeStrip: this.#typeStrip };
|
|
149
251
|
}
|
|
150
252
|
// --- comms (#9) ------------------------------------------------------------
|
|
151
253
|
#trackComm(msgType, content) {
|
|
@@ -225,7 +327,10 @@ export class Kernel {
|
|
|
225
327
|
#ensureTarget() {
|
|
226
328
|
if (this.#target)
|
|
227
329
|
return Promise.resolve(this.#target);
|
|
228
|
-
|
|
330
|
+
// Not before the kernel's own ports are bound: a Deno runtime binds its
|
|
331
|
+
// inspector to port 0 within milliseconds of starting and can take a
|
|
332
|
+
// connection-file port the kernel is still binding (#257).
|
|
333
|
+
this.#targetStarting ??= this.#bound.then(() => this.#launchTarget()).then(async (target) => {
|
|
229
334
|
// Before any cell runs on it, so breakpoints already apply.
|
|
230
335
|
await this.#debugger?.attachTarget(target.debug ?? null);
|
|
231
336
|
this.#target = target;
|
|
@@ -243,6 +348,8 @@ export class Kernel {
|
|
|
243
348
|
message: (msg) => void this.#onTargetMessage(msg).catch((err) => this.#log(`error handling target message: ${err}`)),
|
|
244
349
|
exit: (code, note) => {
|
|
245
350
|
this.#target = null;
|
|
351
|
+
// What its cells declared died with it.
|
|
352
|
+
this.#types?.reset();
|
|
246
353
|
// Its prompt, if any, died with it; a late reply is dropped.
|
|
247
354
|
this.#input = null;
|
|
248
355
|
void this.#debugger?.attachTarget(null);
|
|
@@ -320,11 +427,17 @@ export class Kernel {
|
|
|
320
427
|
// Unsigned or forged messages are dropped, as in Deno.
|
|
321
428
|
if (!verify(this.#conn.key, msg.signedParts, msg.sig))
|
|
322
429
|
continue;
|
|
430
|
+
if (socket === this.#shell)
|
|
431
|
+
this.#handling = { msgType: msg.header.msg_type, msgId: msg.header.msg_id, since: Date.now() };
|
|
323
432
|
await handle(msg, peer);
|
|
324
433
|
}
|
|
325
434
|
catch (err) {
|
|
326
435
|
this.#log(`error handling message: ${err instanceof Error ? err.stack : err}`);
|
|
327
436
|
}
|
|
437
|
+
finally {
|
|
438
|
+
if (socket === this.#shell)
|
|
439
|
+
this.#handling = null;
|
|
440
|
+
}
|
|
328
441
|
}
|
|
329
442
|
}
|
|
330
443
|
#kernelInfo() {
|
|
@@ -343,7 +456,8 @@ export class Kernel {
|
|
|
343
456
|
codemirror_mode: "typescript",
|
|
344
457
|
nbconvert_exporter: "script",
|
|
345
458
|
},
|
|
346
|
-
|
|
459
|
+
// The runtime's versions are known once it is up.
|
|
460
|
+
banner: versions ? `cdp-kernel on ${this.#runtime} ${versions.runtime} (${versions.engine || "?"})` : `cdp-kernel on ${this.#runtime}`,
|
|
347
461
|
help_links: [],
|
|
348
462
|
debugger: this.#debugger !== null,
|
|
349
463
|
// What current ipykernel advertises; JupyterLab itself goes by the
|
|
@@ -413,7 +527,8 @@ export class Kernel {
|
|
|
413
527
|
try {
|
|
414
528
|
switch (type) {
|
|
415
529
|
case "kernel_info_request":
|
|
416
|
-
|
|
530
|
+
// Without waiting for the runtime: clients probe with kernel_info
|
|
531
|
+
// while it starts, and re-send until answered (#253).
|
|
417
532
|
this.#reply(this.#shell, peer, msg, "kernel_info_reply", this.#kernelInfo());
|
|
418
533
|
break;
|
|
419
534
|
case "complete_request":
|
|
@@ -461,15 +576,24 @@ export class Kernel {
|
|
|
461
576
|
this.#status("idle", msg.header);
|
|
462
577
|
}
|
|
463
578
|
}
|
|
464
|
-
/**
|
|
579
|
+
/**
|
|
580
|
+
* The tooltip (Shift-Tab) for the name at the cursor (#10): its declared
|
|
581
|
+
* type, signature and JSDoc when the language service knows them (#274),
|
|
582
|
+
* then what the runtime finds.
|
|
583
|
+
*/
|
|
465
584
|
async #inspect(msg) {
|
|
466
585
|
const code = String(msg.content.code ?? "");
|
|
467
586
|
const cursorCp = typeof msg.content.cursor_pos === "number"
|
|
468
587
|
? msg.content.cursor_pos
|
|
469
588
|
: utf16ToCodePointIndex(code, code.length);
|
|
470
|
-
const
|
|
589
|
+
const cursor = codePointToUtf16Index(code, cursorCp);
|
|
590
|
+
const path = inspectTarget(code, cursor);
|
|
471
591
|
const detail = msg.content.detail_level === 1 ? 1 : 0;
|
|
472
|
-
const
|
|
592
|
+
const [runtime, typed] = await Promise.all([
|
|
593
|
+
path === null ? null : (async () => (await this.#ensureTarget()).inspect(path, detail))(),
|
|
594
|
+
this.#types?.quickInfo(code, cursor) ?? null,
|
|
595
|
+
]);
|
|
596
|
+
const text = typed === null ? runtime : runtime === null ? typed : `${typed}\n\n${withoutSignature(runtime)}`;
|
|
473
597
|
return text === null
|
|
474
598
|
? { status: "ok", found: false, data: {}, metadata: {} }
|
|
475
599
|
: { status: "ok", found: true, data: { "text/plain": text }, metadata: {} };
|
|
@@ -480,29 +604,59 @@ export class Kernel {
|
|
|
480
604
|
? msg.content.cursor_pos
|
|
481
605
|
: utf16ToCodePointIndex(code, code.length);
|
|
482
606
|
const cursor = codePointToUtf16Index(code, cursorCp);
|
|
607
|
+
// Both at once: the language service may not answer within its budget.
|
|
608
|
+
const [runtime, typed] = await Promise.all([
|
|
609
|
+
this.#runtimeCompletions(code, cursor),
|
|
610
|
+
this.#types?.complete(code, cursor) ?? null,
|
|
611
|
+
]);
|
|
612
|
+
// Declared types first, then what only the runtime knows (a property
|
|
613
|
+
// added at runtime, a global defined by a library).
|
|
614
|
+
let matches = runtime.matches;
|
|
615
|
+
let start = runtime.start;
|
|
616
|
+
const kinds = new Map();
|
|
617
|
+
if (typed !== null && typed.start === runtime.start) {
|
|
618
|
+
for (const entry of typed.entries)
|
|
619
|
+
kinds.set(entry.name, entry.kind);
|
|
620
|
+
matches = [...kinds.keys(), ...runtime.matches.filter((name) => !kinds.has(name))];
|
|
621
|
+
}
|
|
622
|
+
else if (typed !== null && typed.entries.length > 0 && runtime.matches.length === 0) {
|
|
623
|
+
// An expression the runtime can't complete, e.g. inside a call.
|
|
624
|
+
for (const entry of typed.entries)
|
|
625
|
+
kinds.set(entry.name, entry.kind);
|
|
626
|
+
matches = [...kinds.keys()];
|
|
627
|
+
start = typed.start;
|
|
628
|
+
}
|
|
629
|
+
const cursorStart = utf16ToCodePointIndex(code, start);
|
|
630
|
+
return {
|
|
631
|
+
status: "ok",
|
|
632
|
+
matches,
|
|
633
|
+
cursor_start: cursorStart,
|
|
634
|
+
cursor_end: cursorCp,
|
|
635
|
+
metadata: kinds.size === 0 ? {} : {
|
|
636
|
+
// JupyterLab shows each match's type as an icon.
|
|
637
|
+
_jupyter_types_experimental: matches.map((text) => ({
|
|
638
|
+
start: cursorStart,
|
|
639
|
+
end: cursorCp,
|
|
640
|
+
text,
|
|
641
|
+
type: jupyterType(kinds.get(text)),
|
|
642
|
+
})),
|
|
643
|
+
},
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
/** Completion from the runtime's values: property names, globals, lexical names. */
|
|
647
|
+
async #runtimeCompletions(code, cursor) {
|
|
483
648
|
const expr = exprAtCursor(code, cursor);
|
|
484
649
|
const target = await this.#ensureTarget();
|
|
485
|
-
let matches;
|
|
486
|
-
let start;
|
|
487
650
|
const dot = expr.lastIndexOf(".");
|
|
488
651
|
if (dot !== -1) {
|
|
489
652
|
const prefix = expr.slice(dot + 1);
|
|
490
|
-
matches = (await target.propertyNames(expr.slice(0, dot)))
|
|
653
|
+
const matches = (await target.propertyNames(expr.slice(0, dot)))
|
|
491
654
|
.filter((name) => name.startsWith(prefix) && !name.startsWith("Symbol("));
|
|
492
|
-
start
|
|
655
|
+
return { matches, start: cursor - prefix.length };
|
|
493
656
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
start = cursor - expr.length;
|
|
498
|
-
}
|
|
499
|
-
return {
|
|
500
|
-
status: "ok",
|
|
501
|
-
matches,
|
|
502
|
-
cursor_start: utf16ToCodePointIndex(code, start),
|
|
503
|
-
cursor_end: cursorCp,
|
|
504
|
-
metadata: {},
|
|
505
|
-
};
|
|
657
|
+
const names = new Set([...(await target.propertyNames("globalThis")), ...(await target.lexicalNames())]);
|
|
658
|
+
const matches = [...names].filter((name) => name.startsWith(expr) && !name.startsWith("__cdpk")).sort();
|
|
659
|
+
return { matches, start: cursor - expr.length };
|
|
506
660
|
}
|
|
507
661
|
async #requestInput(prompt, password) {
|
|
508
662
|
// The target asked while an interrupt was already waiting on it.
|
|
@@ -599,6 +753,8 @@ export class Kernel {
|
|
|
599
753
|
this.#publish("stream", { name: "stderr", text: `${target.notice}\n` });
|
|
600
754
|
}
|
|
601
755
|
const result = await this.#evaluateCell(target, code);
|
|
756
|
+
// It ran, so its declarations exist, even if it threw part way.
|
|
757
|
+
this.#types?.addCell(code);
|
|
602
758
|
if (result.exceptionDetails) {
|
|
603
759
|
const exception = result.exceptionDetails.exception;
|
|
604
760
|
const described = exception
|
|
@@ -632,6 +788,8 @@ export class Kernel {
|
|
|
632
788
|
finally {
|
|
633
789
|
this.#executing = false;
|
|
634
790
|
}
|
|
791
|
+
// The result and error are shown: nothing refers to them any more (#286).
|
|
792
|
+
await this.#target?.releaseResults?.();
|
|
635
793
|
if (error) {
|
|
636
794
|
if (!silent && msg.content.stop_on_error !== false) {
|
|
637
795
|
// Abort what was queued behind this cell (JupyterLab's Run All sends
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// The worker thread that runs TypeScript's language service for the kernel
|
|
2
|
+
// (#273). It owns a NotebookTypes (src/notebook-types.ts) and answers the
|
|
3
|
+
// host's requests (src/language-service.ts) in order. Loading TypeScript
|
|
4
|
+
// and the first program take most of a second, which here doesn't hold up
|
|
5
|
+
// heartbeats, shell or control messages.
|
|
6
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import ts from "typescript";
|
|
10
|
+
import { ensureInstalledAsync } from "../runtime/node-hooks.mjs";
|
|
11
|
+
import { kernelGlobals, NotebookTypes } from "./notebook-types.js";
|
|
12
|
+
import { DenoResolver, denoTypes } from "./deno-types.js";
|
|
13
|
+
const options = workerData;
|
|
14
|
+
const types = new NotebookTypes(ts, {
|
|
15
|
+
notebookDir: options.notebookDir,
|
|
16
|
+
npmCache: options.npmCache,
|
|
17
|
+
deno: options.deno === null
|
|
18
|
+
? undefined
|
|
19
|
+
: new DenoResolver({ config: options.deno.config, root: join(options.notebookDir, "__cdpk_remote__") }),
|
|
20
|
+
types: [],
|
|
21
|
+
typeFiles: options.typeFiles,
|
|
22
|
+
globals: kernelGlobals(options.display),
|
|
23
|
+
});
|
|
24
|
+
if (options.denoTypes) {
|
|
25
|
+
const declarations = denoTypes();
|
|
26
|
+
if (declarations !== null)
|
|
27
|
+
types.addVirtualTypeFile("__cdpk_deno__.d.ts", declarations);
|
|
28
|
+
else
|
|
29
|
+
process.stderr.write("[cdp-kernel] no Deno types for completion: `deno types` failed\n");
|
|
30
|
+
}
|
|
31
|
+
/** A package's declaration entry point: package.json "types"/"typings", else index.d.ts. */
|
|
32
|
+
function typesEntry(dir) {
|
|
33
|
+
try {
|
|
34
|
+
const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
35
|
+
const entry = manifest.types ?? manifest.typings;
|
|
36
|
+
if (typeof entry === "string")
|
|
37
|
+
return join(dir, entry);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// No readable package.json: fall back to the convention.
|
|
41
|
+
}
|
|
42
|
+
return join(dir, "index.d.ts");
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A type package for the target's globals (@types/node on Node, @types/bun,
|
|
46
|
+
* @cloudflare/workers-types): the
|
|
47
|
+
* notebook project's own when it has one, else installed once into the
|
|
48
|
+
* kernel's npm cache, in the background. Completion goes on without it
|
|
49
|
+
* meanwhile, and without it at all if the install fails (offline).
|
|
50
|
+
*/
|
|
51
|
+
async function addTypePackage({ name, range }) {
|
|
52
|
+
const short = name.replace(/^@types\//, "");
|
|
53
|
+
if (types.resolvesTypes(short)) {
|
|
54
|
+
types.addTypeNames([short]);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const { dir, alias } = await ensureInstalledAsync({ name, range });
|
|
59
|
+
types.addTypeFiles([typesEntry(join(dir, "node_modules", alias))]);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
process.stderr.write(`[cdp-kernel] no ${name} for completion: ${err instanceof Error ? err.message : err}\n`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
for (const pkg of options.typePackages)
|
|
66
|
+
void addTypePackage(pkg);
|
|
67
|
+
parentPort?.on("message", (request) => {
|
|
68
|
+
let response;
|
|
69
|
+
switch (request.type) {
|
|
70
|
+
case "cell":
|
|
71
|
+
types.addCell(request.code);
|
|
72
|
+
return;
|
|
73
|
+
case "reset":
|
|
74
|
+
types.reset();
|
|
75
|
+
return;
|
|
76
|
+
case "complete":
|
|
77
|
+
case "quickInfo":
|
|
78
|
+
try {
|
|
79
|
+
response = request.type === "complete"
|
|
80
|
+
? { id: request.id, result: types.complete(request.code, request.cursor) }
|
|
81
|
+
: { id: request.id, result: types.quickInfo(request.code, request.cursor) };
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
response = { id: request.id, error: err instanceof Error ? err.stack ?? err.message : String(err) };
|
|
85
|
+
}
|
|
86
|
+
parentPort?.postMessage(response);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { CompletionResult } from "./notebook-types.ts";
|
|
2
|
+
export type { CompletionEntry, CompletionResult } from "./notebook-types.ts";
|
|
3
|
+
/** What the worker is started with. */
|
|
4
|
+
export interface WorkerOptions {
|
|
5
|
+
notebookDir: string;
|
|
6
|
+
/** See NotebookTypesOptions.npmCache. */
|
|
7
|
+
npmCache: "npm" | "bun" | null;
|
|
8
|
+
/** A Deno target: resolve imports with `deno info`, under this project config if any. */
|
|
9
|
+
deno: {
|
|
10
|
+
config: string | null;
|
|
11
|
+
} | null;
|
|
12
|
+
/** Add `deno types` (the Deno namespace) to the globals. */
|
|
13
|
+
denoTypes: boolean;
|
|
14
|
+
/** Declaration files for the target's globals, e.g. a Workers project's worker-configuration.d.ts. */
|
|
15
|
+
typeFiles: string[];
|
|
16
|
+
/** runtime/display.mjs, which types `jupyter`; null if not a local file. */
|
|
17
|
+
display: string | null;
|
|
18
|
+
/** Type packages for the target's globals, e.g. @types/node. */
|
|
19
|
+
typePackages: {
|
|
20
|
+
name: string;
|
|
21
|
+
range: string;
|
|
22
|
+
}[];
|
|
23
|
+
}
|
|
24
|
+
export type WorkerRequest = {
|
|
25
|
+
type: "cell";
|
|
26
|
+
code: string;
|
|
27
|
+
} | {
|
|
28
|
+
type: "reset";
|
|
29
|
+
} | {
|
|
30
|
+
type: "complete" | "quickInfo";
|
|
31
|
+
id: number;
|
|
32
|
+
code: string;
|
|
33
|
+
cursor: number;
|
|
34
|
+
};
|
|
35
|
+
export type WorkerResponse = {
|
|
36
|
+
id: number;
|
|
37
|
+
result: CompletionResult | string | null;
|
|
38
|
+
} | {
|
|
39
|
+
id: number;
|
|
40
|
+
error: string;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* What to start the worker from. Deno's node:worker_threads takes only
|
|
44
|
+
* file: and data: URLs, so a kernel run from JSR (https:) starts a data:
|
|
45
|
+
* module that imports the worker.
|
|
46
|
+
*/
|
|
47
|
+
export declare function workerEntry(url?: URL): URL;
|
|
48
|
+
export declare class LanguageService {
|
|
49
|
+
#private;
|
|
50
|
+
constructor(options: Omit<WorkerOptions, "display">, { budgetMs, log }?: {
|
|
51
|
+
budgetMs?: number;
|
|
52
|
+
log?: (text: string) => void;
|
|
53
|
+
});
|
|
54
|
+
/** A cell that ran (even if it threw): its declarations exist now. */
|
|
55
|
+
addCell(code: string): void;
|
|
56
|
+
/** The target restarted, so what the cells declared is gone. */
|
|
57
|
+
reset(): void;
|
|
58
|
+
/** Typed completions, or null if the service isn't ready within the budget. */
|
|
59
|
+
complete(code: string, cursor: number): Promise<CompletionResult | null>;
|
|
60
|
+
/** Type, signature and JSDoc for the name at the cursor, or null. */
|
|
61
|
+
quickInfo(code: string, cursor: number): Promise<string | null>;
|
|
62
|
+
/** Ends the worker; the next request starts another. */
|
|
63
|
+
close(): void;
|
|
64
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// The kernel's side of typed completion and inspection (#273, #274): starts the language-service
|
|
2
|
+
// worker (src/language-service-worker.ts) on the first request, keeps it fed
|
|
3
|
+
// with executed cells, and gives each request a time budget, after which
|
|
4
|
+
// the kernel answers from the runtime alone. The worker is ended after a
|
|
5
|
+
// while without requests, which gives its memory back (about 150 MB).
|
|
6
|
+
import { Worker } from "node:worker_threads";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
/**
|
|
9
|
+
* How long a request waits for the worker. JupyterLab gives a kernel's
|
|
10
|
+
* completions a second, and the runtime's half runs alongside.
|
|
11
|
+
*/
|
|
12
|
+
const BUDGET_MS = 500;
|
|
13
|
+
const IDLE_MS = 10 * 60 * 1000;
|
|
14
|
+
// Compiled to dist/*.js for the npm package, run as .ts from a checkout.
|
|
15
|
+
const WORKER_URL = new URL(import.meta.url.endsWith(".ts") ? "./language-service-worker.ts" : "./language-service-worker.js", import.meta.url);
|
|
16
|
+
const DISPLAY_URL = new URL("../runtime/display.mjs", import.meta.url);
|
|
17
|
+
/**
|
|
18
|
+
* What to start the worker from. Deno's node:worker_threads takes only
|
|
19
|
+
* file: and data: URLs, so a kernel run from JSR (https:) starts a data:
|
|
20
|
+
* module that imports the worker.
|
|
21
|
+
*/
|
|
22
|
+
export function workerEntry(url = WORKER_URL) {
|
|
23
|
+
if (url.protocol === "file:")
|
|
24
|
+
return url;
|
|
25
|
+
return new URL(`data:text/javascript,${encodeURIComponent(`import ${JSON.stringify(url.href)};`)}`);
|
|
26
|
+
}
|
|
27
|
+
export class LanguageService {
|
|
28
|
+
#options;
|
|
29
|
+
#budgetMs;
|
|
30
|
+
#log;
|
|
31
|
+
/** Executed cells, replayed to a new worker. */
|
|
32
|
+
#cells = [];
|
|
33
|
+
#pending = new Map();
|
|
34
|
+
#worker = null;
|
|
35
|
+
#nextId = 1;
|
|
36
|
+
#idle = null;
|
|
37
|
+
/** The worker couldn't start: don't keep trying. */
|
|
38
|
+
#broken = false;
|
|
39
|
+
constructor(options, { budgetMs = BUDGET_MS, log = () => { } } = {}) {
|
|
40
|
+
this.#options = { ...options, display: DISPLAY_URL.protocol === "file:" ? fileURLToPath(DISPLAY_URL) : null };
|
|
41
|
+
this.#budgetMs = budgetMs;
|
|
42
|
+
this.#log = log;
|
|
43
|
+
}
|
|
44
|
+
/** A cell that ran (even if it threw): its declarations exist now. */
|
|
45
|
+
addCell(code) {
|
|
46
|
+
this.#cells.push(code);
|
|
47
|
+
this.#worker?.postMessage({ type: "cell", code });
|
|
48
|
+
}
|
|
49
|
+
/** The target restarted, so what the cells declared is gone. */
|
|
50
|
+
reset() {
|
|
51
|
+
this.#cells.length = 0;
|
|
52
|
+
this.#worker?.postMessage({ type: "reset" });
|
|
53
|
+
}
|
|
54
|
+
/** Typed completions, or null if the service isn't ready within the budget. */
|
|
55
|
+
async complete(code, cursor) {
|
|
56
|
+
return (await this.#request("complete", code, cursor));
|
|
57
|
+
}
|
|
58
|
+
/** Type, signature and JSDoc for the name at the cursor, or null. */
|
|
59
|
+
async quickInfo(code, cursor) {
|
|
60
|
+
return (await this.#request("quickInfo", code, cursor));
|
|
61
|
+
}
|
|
62
|
+
#request(type, code, cursor) {
|
|
63
|
+
const worker = this.#ensureWorker();
|
|
64
|
+
if (worker === null)
|
|
65
|
+
return Promise.resolve(null);
|
|
66
|
+
const id = this.#nextId++;
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
const timer = setTimeout(() => settle(null), this.#budgetMs);
|
|
69
|
+
const settle = (response) => {
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
this.#pending.delete(id);
|
|
72
|
+
if (response !== null && "error" in response)
|
|
73
|
+
this.#log(`${type} failed: ${response.error}`);
|
|
74
|
+
resolve(response !== null && "result" in response ? response.result : null);
|
|
75
|
+
};
|
|
76
|
+
this.#pending.set(id, settle);
|
|
77
|
+
worker.postMessage({ type, id, code, cursor });
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/** Ends the worker; the next request starts another. */
|
|
81
|
+
close() {
|
|
82
|
+
if (this.#idle !== null)
|
|
83
|
+
clearTimeout(this.#idle);
|
|
84
|
+
this.#idle = null;
|
|
85
|
+
const worker = this.#worker;
|
|
86
|
+
this.#worker = null;
|
|
87
|
+
for (const settle of this.#pending.values())
|
|
88
|
+
settle(null);
|
|
89
|
+
void worker?.terminate();
|
|
90
|
+
}
|
|
91
|
+
#ensureWorker() {
|
|
92
|
+
if (this.#broken)
|
|
93
|
+
return null;
|
|
94
|
+
if (this.#idle !== null)
|
|
95
|
+
clearTimeout(this.#idle);
|
|
96
|
+
this.#idle = setTimeout(() => this.close(), IDLE_MS);
|
|
97
|
+
this.#idle.unref();
|
|
98
|
+
if (this.#worker !== null)
|
|
99
|
+
return this.#worker;
|
|
100
|
+
let worker;
|
|
101
|
+
try {
|
|
102
|
+
worker = new Worker(workerEntry(), { workerData: this.#options });
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
this.#broken = true;
|
|
106
|
+
this.#log(`typed completion unavailable: ${err instanceof Error ? err.message : err}`);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
// An idle worker mustn't keep the kernel alive.
|
|
110
|
+
worker.unref();
|
|
111
|
+
worker.on("message", (response) => this.#pending.get(response.id)?.(response));
|
|
112
|
+
worker.on("error", (err) => {
|
|
113
|
+
// E.g. typescript failed to load. Completion goes on from the runtime.
|
|
114
|
+
this.#broken = true;
|
|
115
|
+
this.#log(`typed completion unavailable: ${err instanceof Error ? err.message : err}`);
|
|
116
|
+
if (this.#worker === worker)
|
|
117
|
+
this.close();
|
|
118
|
+
});
|
|
119
|
+
worker.on("exit", () => {
|
|
120
|
+
if (this.#worker === worker)
|
|
121
|
+
this.close();
|
|
122
|
+
});
|
|
123
|
+
for (const code of this.#cells)
|
|
124
|
+
worker.postMessage({ type: "cell", code });
|
|
125
|
+
this.#worker = worker;
|
|
126
|
+
return worker;
|
|
127
|
+
}
|
|
128
|
+
}
|