@tlockney/cdp-kernel 0.4.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 +78 -478
- package/THIRD-PARTY-NOTICES +62 -0
- package/dist/bun-target.d.ts +2 -2
- package/dist/bun-target.js +30 -8
- package/dist/cdp.d.ts +8 -1
- package/dist/cdp.js +31 -11
- package/dist/celld-target.d.ts +1 -1
- package/dist/celld-target.js +54 -19
- package/dist/children.d.ts +29 -0
- package/dist/children.js +114 -0
- package/dist/debugger.d.ts +1 -1
- package/dist/debugger.js +21 -10
- package/dist/deno-types.d.ts +30 -0
- package/dist/deno-types.js +93 -0
- package/dist/globals.d.ts +3 -3
- package/dist/globals.js +3 -3
- package/dist/kernel.d.ts +7 -1
- package/dist/kernel.js +348 -68
- 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 +6 -3
- package/dist/mod.js +6 -3
- 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 +3 -3
- 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 +58 -6
- package/dist/target.js +412 -54
- package/dist/text.d.ts +5 -1
- package/dist/text.js +55 -2
- package/dist/transform.d.ts +30 -1
- package/dist/transform.js +68 -44
- package/dist/wire.d.ts +8 -1
- package/dist/wire.js +3 -1
- package/dist/workerd-target.d.ts +7 -1
- package/dist/workerd-target.js +102 -8
- package/dist/workers-project.js +13 -2
- package/dist/ws-client.d.ts +5 -1
- package/dist/ws-client.js +46 -13
- package/dist/zmtp.d.ts +9 -1
- package/dist/zmtp.js +42 -1
- package/package.json +17 -6
- package/runtime/bootstrap.mjs +233 -42
- package/runtime/bun-channel.d.mts +3 -0
- package/runtime/bun-channel.mjs +2 -2
- package/runtime/comm.mjs +11 -1
- package/runtime/deno-compat.d.mts +2 -0
- package/runtime/deno-compat.mjs +14 -1
- package/runtime/display.mjs +8 -1
- package/runtime/introspect.mjs +1 -1
- package/runtime/node-hooks.d.mts +48 -0
- package/runtime/node-hooks.mjs +257 -41
- package/runtime/workerd-worker.mjs +77 -6
|
@@ -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.
|
|
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-
|
|
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.
|
|
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-
|
|
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
|
|
@@ -82,7 +82,7 @@ export function kernelArgv(host, execPath, origin, args) {
|
|
|
82
82
|
return [execPath, "run", "--allow-all", ...flags, entry, ...args];
|
|
83
83
|
}
|
|
84
84
|
export function cacheRoot() {
|
|
85
|
-
const base = process.env.XDG_CACHE_HOME
|
|
85
|
+
const base = process.env.XDG_CACHE_HOME || join(homedir(), ".cache");
|
|
86
86
|
return join(base, "cdp-kernel");
|
|
87
87
|
}
|
|
88
88
|
async function fetchText(url) {
|
package/dist/parent.d.ts
ADDED
|
@@ -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[];
|
|
@@ -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
|
+
}
|
package/dist/sourcemap.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
13
|
-
|
|
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
|
-
/**
|
|
56
|
-
|
|
57
|
-
|
|
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,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
|
+
};
|