myjsbook 1.0.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/LICENSE +201 -0
- package/README.md +263 -0
- package/package.json +38 -0
- package/public/app.js +6686 -0
- package/public/components/constants.js +421 -0
- package/public/components/elements.js +118 -0
- package/public/components/state.js +53 -0
- package/public/icons/audio.svg +1 -0
- package/public/icons/azure.svg +1 -0
- package/public/icons/babel.svg +1 -0
- package/public/icons/bun.svg +1 -0
- package/public/icons/bun_light.svg +1 -0
- package/public/icons/c.svg +1 -0
- package/public/icons/chrome.svg +1 -0
- package/public/icons/citation.svg +1 -0
- package/public/icons/claude.svg +1 -0
- package/public/icons/console.svg +1 -0
- package/public/icons/cpp.svg +1 -0
- package/public/icons/css-map.svg +1 -0
- package/public/icons/css.svg +1 -0
- package/public/icons/database.svg +1 -0
- package/public/icons/docker.svg +1 -0
- package/public/icons/document.svg +1 -0
- package/public/icons/ejs.svg +1 -0
- package/public/icons/exe.svg +1 -0
- package/public/icons/favicon.svg +1 -0
- package/public/icons/figma.svg +1 -0
- package/public/icons/firebase.svg +1 -0
- package/public/icons/folder-admin-open.svg +1 -0
- package/public/icons/folder-admin.svg +1 -0
- package/public/icons/folder-api-open.svg +1 -0
- package/public/icons/folder-api.svg +1 -0
- package/public/icons/folder-app-open.svg +1 -0
- package/public/icons/folder-app.svg +1 -0
- package/public/icons/folder-archive-open.svg +1 -0
- package/public/icons/folder-archive.svg +1 -0
- package/public/icons/folder-attachment-open.svg +1 -0
- package/public/icons/folder-attachment.svg +1 -0
- package/public/icons/folder-aws-open.svg +1 -0
- package/public/icons/folder-aws.svg +1 -0
- package/public/icons/folder-backup-open.svg +1 -0
- package/public/icons/folder-backup.svg +1 -0
- package/public/icons/folder-class-open.svg +1 -0
- package/public/icons/folder-class.svg +1 -0
- package/public/icons/folder-claude-open.svg +1 -0
- package/public/icons/folder-claude.svg +1 -0
- package/public/icons/folder-client-open.svg +1 -0
- package/public/icons/folder-client.svg +1 -0
- package/public/icons/folder-command-open.svg +1 -0
- package/public/icons/folder-command.svg +1 -0
- package/public/icons/folder-components-open.svg +1 -0
- package/public/icons/folder-components.svg +1 -0
- package/public/icons/folder-config-open.svg +1 -0
- package/public/icons/folder-config.svg +1 -0
- package/public/icons/folder-connection-open.svg +1 -0
- package/public/icons/folder-connection.svg +1 -0
- package/public/icons/folder-console-open.svg +1 -0
- package/public/icons/folder-console.svg +1 -0
- package/public/icons/folder-container-open.svg +1 -0
- package/public/icons/folder-container.svg +1 -0
- package/public/icons/folder-content-open.svg +1 -0
- package/public/icons/folder-content.svg +1 -0
- package/public/icons/folder-context-open.svg +1 -0
- package/public/icons/folder-context.svg +1 -0
- package/public/icons/folder-controller-open.svg +1 -0
- package/public/icons/folder-controller.svg +1 -0
- package/public/icons/folder-core-open.svg +1 -0
- package/public/icons/folder-core.svg +1 -0
- package/public/icons/folder-css-open.svg +1 -0
- package/public/icons/folder-css.svg +1 -0
- package/public/icons/folder-custom-open.svg +1 -0
- package/public/icons/folder-custom.svg +1 -0
- package/public/icons/folder-database-open.svg +1 -0
- package/public/icons/folder-database.svg +1 -0
- package/public/icons/folder-decorators-open.svg +1 -0
- package/public/icons/folder-decorators.svg +1 -0
- package/public/icons/folder-desktop-open.svg +1 -0
- package/public/icons/folder-desktop.svg +1 -0
- package/public/icons/folder-dist-open.svg +1 -0
- package/public/icons/folder-dist.svg +1 -0
- package/public/icons/folder-docs-open.svg +1 -0
- package/public/icons/folder-docs.svg +1 -0
- package/public/icons/folder-download-open.svg +1 -0
- package/public/icons/folder-download.svg +1 -0
- package/public/icons/folder-dtos-open.svg +1 -0
- package/public/icons/folder-dtos.svg +1 -0
- package/public/icons/folder-element-open.svg +1 -0
- package/public/icons/folder-element.svg +1 -0
- package/public/icons/folder-environment-open.svg +1 -0
- package/public/icons/folder-environment.svg +1 -0
- package/public/icons/folder-error-open.svg +1 -0
- package/public/icons/folder-error.svg +1 -0
- package/public/icons/folder-event-open.svg +1 -0
- package/public/icons/folder-event.svg +1 -0
- package/public/icons/folder-examples-open.svg +1 -0
- package/public/icons/folder-examples.svg +1 -0
- package/public/icons/folder-expo-open.svg +1 -0
- package/public/icons/folder-expo.svg +1 -0
- package/public/icons/folder-export-open.svg +1 -0
- package/public/icons/folder-export.svg +1 -0
- package/public/icons/folder-features-open.svg +1 -0
- package/public/icons/folder-features.svg +1 -0
- package/public/icons/folder-filter-open.svg +1 -0
- package/public/icons/folder-filter.svg +1 -0
- package/public/icons/folder-firebase-open.svg +1 -0
- package/public/icons/folder-firebase.svg +1 -0
- package/public/icons/folder-firestore-open.svg +1 -0
- package/public/icons/folder-firestore.svg +1 -0
- package/public/icons/folder-font-open.svg +1 -0
- package/public/icons/folder-font.svg +1 -0
- package/public/icons/folder-functions-open.svg +1 -0
- package/public/icons/folder-functions.svg +1 -0
- package/public/icons/folder-gemini-ai-open.svg +1 -0
- package/public/icons/folder-gemini-ai.svg +1 -0
- package/public/icons/folder-git-open.svg +1 -0
- package/public/icons/folder-git.svg +1 -0
- package/public/icons/folder-github-open.svg +1 -0
- package/public/icons/folder-github.svg +1 -0
- package/public/icons/folder-helper-open.svg +1 -0
- package/public/icons/folder-helper.svg +1 -0
- package/public/icons/folder-home-open.svg +1 -0
- package/public/icons/folder-home.svg +1 -0
- package/public/icons/folder-icons-open.svg +1 -0
- package/public/icons/folder-icons.svg +1 -0
- package/public/icons/folder-images-open.svg +1 -0
- package/public/icons/folder-images.svg +1 -0
- package/public/icons/folder-interface-open.svg +1 -0
- package/public/icons/folder-interface.svg +1 -0
- package/public/icons/folder-ios-open.svg +1 -0
- package/public/icons/folder-ios.svg +1 -0
- package/public/icons/folder-java-open.svg +1 -0
- package/public/icons/folder-java.svg +1 -0
- package/public/icons/folder-javascript-open.svg +1 -0
- package/public/icons/folder-javascript.svg +1 -0
- package/public/icons/folder-middleware-open.svg +1 -0
- package/public/icons/folder-middleware.svg +1 -0
- package/public/icons/folder-migrations-open.svg +1 -0
- package/public/icons/folder-migrations.svg +1 -0
- package/public/icons/folder-other-open.svg +1 -0
- package/public/icons/folder-other.svg +1 -0
- package/public/icons/folder-packages-open.svg +1 -0
- package/public/icons/folder-packages.svg +1 -0
- package/public/icons/folder-pdf-open.svg +1 -0
- package/public/icons/folder-pdf.svg +1 -0
- package/public/icons/folder-plugin-open.svg +1 -0
- package/public/icons/folder-plugin.svg +1 -0
- package/public/icons/folder-project-open.svg +1 -0
- package/public/icons/folder-project.svg +1 -0
- package/public/icons/folder-public-open.svg +1 -0
- package/public/icons/folder-public.svg +1 -0
- package/public/icons/folder-python-open.svg +1 -0
- package/public/icons/folder-python.svg +1 -0
- package/public/icons/folder-repository-open.svg +1 -0
- package/public/icons/folder-repository.svg +1 -0
- package/public/icons/folder-routes-open.svg +1 -0
- package/public/icons/folder-routes.svg +1 -0
- package/public/icons/folder-rules-open.svg +1 -0
- package/public/icons/folder-rules.svg +1 -0
- package/public/icons/folder-sass-open.svg +1 -0
- package/public/icons/folder-sass.svg +1 -0
- package/public/icons/folder-scripts-open.svg +1 -0
- package/public/icons/folder-scripts.svg +1 -0
- package/public/icons/folder-server-open.svg +1 -0
- package/public/icons/folder-server.svg +1 -0
- package/public/icons/folder-serverless-open.svg +1 -0
- package/public/icons/folder-serverless.svg +1 -0
- package/public/icons/folder-skills-open.svg +1 -0
- package/public/icons/folder-skills.svg +1 -0
- package/public/icons/folder-src-open.svg +1 -0
- package/public/icons/folder-src.svg +1 -0
- package/public/icons/folder-stack-open.svg +1 -0
- package/public/icons/folder-stack.svg +1 -0
- package/public/icons/folder-store-open.svg +1 -0
- package/public/icons/folder-store.svg +1 -0
- package/public/icons/folder-supabase-open.svg +1 -0
- package/public/icons/folder-supabase.svg +1 -0
- package/public/icons/folder-svg-open.svg +1 -0
- package/public/icons/folder-svg.svg +1 -0
- package/public/icons/folder-target-open.svg +1 -0
- package/public/icons/folder-target.svg +1 -0
- package/public/icons/folder-tasks-open.svg +1 -0
- package/public/icons/folder-tasks.svg +1 -0
- package/public/icons/folder-temp-open.svg +1 -0
- package/public/icons/folder-temp.svg +1 -0
- package/public/icons/folder-template-open.svg +1 -0
- package/public/icons/folder-template.svg +1 -0
- package/public/icons/folder-test-open.svg +1 -0
- package/public/icons/folder-test.svg +1 -0
- package/public/icons/folder-tools-open.svg +1 -0
- package/public/icons/folder-tools.svg +1 -0
- package/public/icons/folder-typescript-open.svg +1 -0
- package/public/icons/folder-typescript.svg +1 -0
- package/public/icons/folder-ui-open.svg +1 -0
- package/public/icons/folder-ui.svg +1 -0
- package/public/icons/folder-upload-open.svg +1 -0
- package/public/icons/folder-upload.svg +1 -0
- package/public/icons/folder-utils-open.svg +1 -0
- package/public/icons/folder-utils.svg +1 -0
- package/public/icons/folder-video-open.svg +1 -0
- package/public/icons/folder-video.svg +1 -0
- package/public/icons/folder-views-open.svg +1 -0
- package/public/icons/folder-views.svg +1 -0
- package/public/icons/font.svg +1 -0
- package/public/icons/gemini-ai.svg +1 -0
- package/public/icons/gemini.svg +1 -0
- package/public/icons/git.svg +1 -0
- package/public/icons/google.svg +1 -0
- package/public/icons/graphql.svg +1 -0
- package/public/icons/html.svg +1 -0
- package/public/icons/image.svg +1 -0
- package/public/icons/java.svg +1 -0
- package/public/icons/javaclass.svg +1 -0
- package/public/icons/javascript.svg +1 -0
- package/public/icons/jsconfig.svg +1 -0
- package/public/icons/json.svg +1 -0
- package/public/icons/markdown.svg +1 -0
- package/public/icons/nodejs.svg +1 -0
- package/public/icons/nodejs_alt.svg +1 -0
- package/public/icons/nodemon.svg +1 -0
- package/public/icons/npm.svg +1 -0
- package/public/icons/pdf.svg +1 -0
- package/public/icons/prettier.svg +1 -0
- package/public/icons/prisma.svg +1 -0
- package/public/icons/python.svg +1 -0
- package/public/icons/react.svg +1 -0
- package/public/icons/react_ts.svg +1 -0
- package/public/icons/readme.svg +1 -0
- package/public/icons/remark.svg +1 -0
- package/public/icons/sass.svg +1 -0
- package/public/icons/svg.svg +1 -0
- package/public/icons/tailwindcss.svg +1 -0
- package/public/icons/typescript-def.svg +1 -0
- package/public/icons/typescript.svg +1 -0
- package/public/icons/zip.svg +1 -0
- package/public/index.html +1342 -0
- package/public/styles.css +4736 -0
- package/src/cli.js +175 -0
- package/src/lib/files.js +143 -0
- package/src/lib/notebook.js +141 -0
- package/src/lib/package-exports.js +331 -0
- package/src/lib/session.js +1003 -0
- package/src/server.js +2232 -0
|
@@ -0,0 +1,1003 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import util from "node:util";
|
|
5
|
+
import vm from "node:vm";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { builtinModules } from "node:module";
|
|
8
|
+
|
|
9
|
+
import { parse } from "acorn";
|
|
10
|
+
import ts from "typescript";
|
|
11
|
+
|
|
12
|
+
function formatValue(value) {
|
|
13
|
+
return util.inspect(value, {
|
|
14
|
+
depth: 6,
|
|
15
|
+
colors: false,
|
|
16
|
+
compact: false,
|
|
17
|
+
breakLength: 100
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sanitizeError(error) {
|
|
22
|
+
return {
|
|
23
|
+
name: error?.name ?? "Error",
|
|
24
|
+
message: error?.message ?? String(error),
|
|
25
|
+
stack: error?.stack ?? String(error)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isPlainObject(value) {
|
|
30
|
+
if (!value || typeof value !== "object") {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const prototype = Object.getPrototypeOf(value);
|
|
35
|
+
return prototype === Object.prototype || prototype === null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function looksLikeImageSource(value) {
|
|
39
|
+
if (typeof value !== "string") {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const trimmed = value.trim();
|
|
44
|
+
return (
|
|
45
|
+
trimmed.startsWith("data:image/") ||
|
|
46
|
+
/^https?:\/\/.+\.(png|jpe?g|gif|svg|webp|avif)(\?.*)?$/i.test(trimmed)
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeTableRows(value) {
|
|
51
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (value.every((item) => isPlainObject(item))) {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (value.every((item) => item === null || ["string", "number", "boolean"].includes(typeof item))) {
|
|
60
|
+
return value.map((item, index) => ({ index, value: item }));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isNodeTimeout(value) {
|
|
67
|
+
return (
|
|
68
|
+
value !== null &&
|
|
69
|
+
typeof value === "object" &&
|
|
70
|
+
typeof value._idleTimeout === "number" &&
|
|
71
|
+
typeof value._onTimeout === "function"
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function createTimeoutHooks(ctx, outputs) {
|
|
76
|
+
const original = {
|
|
77
|
+
setTimeout: ctx.setTimeout,
|
|
78
|
+
clearTimeout: ctx.clearTimeout
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const pendingTasks = new Set();
|
|
82
|
+
const pendingTimeouts = new Map();
|
|
83
|
+
|
|
84
|
+
const registerTask = () => {
|
|
85
|
+
let resolve;
|
|
86
|
+
const promise = new Promise((res) => {
|
|
87
|
+
resolve = res;
|
|
88
|
+
});
|
|
89
|
+
pendingTasks.add(promise);
|
|
90
|
+
promise.finally(() => pendingTasks.delete(promise));
|
|
91
|
+
return resolve;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
ctx.setTimeout = (fn, delay = 0, ...args) => {
|
|
95
|
+
let handle;
|
|
96
|
+
const settle = registerTask();
|
|
97
|
+
const wrapped = (...cbArgs) => {
|
|
98
|
+
try {
|
|
99
|
+
return fn?.apply(ctx, cbArgs);
|
|
100
|
+
} finally {
|
|
101
|
+
settle();
|
|
102
|
+
pendingTimeouts.delete(handle);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
handle = original.setTimeout(wrapped, delay, ...args);
|
|
106
|
+
pendingTimeouts.set(handle, settle);
|
|
107
|
+
return handle;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
ctx.clearTimeout = (handle) => {
|
|
111
|
+
const settle = pendingTimeouts.get(handle);
|
|
112
|
+
if (settle) {
|
|
113
|
+
settle();
|
|
114
|
+
pendingTimeouts.delete(handle);
|
|
115
|
+
}
|
|
116
|
+
return original.clearTimeout(handle);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const flush = async (maxMs = 10_000) => {
|
|
120
|
+
const start = Date.now();
|
|
121
|
+
let forcedCleanup = false;
|
|
122
|
+
|
|
123
|
+
while (pendingTasks.size > 0 && Date.now() - start < maxMs) {
|
|
124
|
+
// Wait for any pending timeout callback to settle
|
|
125
|
+
await Promise.race([
|
|
126
|
+
Promise.allSettled([...pendingTasks]),
|
|
127
|
+
new Promise((resolve) => setTimeout(resolve, 10))
|
|
128
|
+
]);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (pendingTasks.size > 0 || pendingTimeouts.size > 0) {
|
|
132
|
+
forcedCleanup = true;
|
|
133
|
+
for (const handle of pendingTimeouts.keys()) {
|
|
134
|
+
try {
|
|
135
|
+
original.clearTimeout(handle);
|
|
136
|
+
} catch (_e) { /* ignore */ }
|
|
137
|
+
}
|
|
138
|
+
pendingTimeouts.clear();
|
|
139
|
+
pendingTasks.clear();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (forcedCleanup) {
|
|
143
|
+
outputs.push({
|
|
144
|
+
type: "warn",
|
|
145
|
+
text: "setTimeout callbacks were auto-cleared after 10s to finish the cell. Use clearTimeout inside the cell to cancel long waits.",
|
|
146
|
+
dataType: "text"
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const restore = () => {
|
|
152
|
+
ctx.setTimeout = original.setTimeout;
|
|
153
|
+
ctx.clearTimeout = original.clearTimeout;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Expose to cancelExecution heuristics
|
|
157
|
+
ctx.__nodebookPendingTimeouts = pendingTimeouts;
|
|
158
|
+
|
|
159
|
+
return { flush, restore };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function createIntervalHooks(ctx, outputs) {
|
|
163
|
+
const original = {
|
|
164
|
+
setInterval: ctx.setInterval,
|
|
165
|
+
clearInterval: ctx.clearInterval
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const activeIntervals = new Set();
|
|
169
|
+
|
|
170
|
+
ctx.setInterval = (fn, delay = 0, ...args) => {
|
|
171
|
+
const wrapped = (...cbArgs) => {
|
|
172
|
+
return fn?.apply(ctx, cbArgs);
|
|
173
|
+
};
|
|
174
|
+
const handle = original.setInterval(wrapped, delay, ...args);
|
|
175
|
+
activeIntervals.add(handle);
|
|
176
|
+
return handle;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
ctx.clearInterval = (handle) => {
|
|
180
|
+
activeIntervals.delete(handle);
|
|
181
|
+
return original.clearInterval(handle);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const waitForDrain = async (cancelPromise) => {
|
|
185
|
+
let cancelled = false;
|
|
186
|
+
const cancelWait = cancelPromise ? cancelPromise.then(() => { cancelled = true; }) : null;
|
|
187
|
+
while (activeIntervals.size > 0 && !cancelled) {
|
|
188
|
+
await Promise.race([
|
|
189
|
+
new Promise((resolve) => setTimeout(resolve, 50)),
|
|
190
|
+
cancelWait ?? new Promise(() => {})
|
|
191
|
+
]);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const restore = () => {
|
|
196
|
+
ctx.setInterval = original.setInterval;
|
|
197
|
+
ctx.clearInterval = original.clearInterval;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
ctx.__nodebookActiveIntervals = activeIntervals;
|
|
201
|
+
|
|
202
|
+
return { waitForDrain, restore, activeIntervals };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function serializeOutputValue(value, type = "result") {
|
|
206
|
+
if (value && typeof value === "object" && value.__nodebookDisplay) {
|
|
207
|
+
return {
|
|
208
|
+
type,
|
|
209
|
+
text: value.text ?? formatValue(value.payload),
|
|
210
|
+
dataType: value.__nodebookDisplay,
|
|
211
|
+
data: value.payload ?? null
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (typeof value === "string") {
|
|
216
|
+
if (looksLikeImageSource(value)) {
|
|
217
|
+
return {
|
|
218
|
+
type,
|
|
219
|
+
text: value,
|
|
220
|
+
dataType: "image",
|
|
221
|
+
data: { src: value, alt: "" }
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
type,
|
|
227
|
+
text: value,
|
|
228
|
+
dataType: "text"
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const tableRows = normalizeTableRows(value);
|
|
233
|
+
if (tableRows) {
|
|
234
|
+
return {
|
|
235
|
+
type,
|
|
236
|
+
text: formatValue(value),
|
|
237
|
+
dataType: "array",
|
|
238
|
+
data: value,
|
|
239
|
+
tableData: tableRows
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (Array.isArray(value) || isPlainObject(value)) {
|
|
244
|
+
return {
|
|
245
|
+
type,
|
|
246
|
+
text: formatValue(value),
|
|
247
|
+
dataType: "json",
|
|
248
|
+
data: value
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
type,
|
|
254
|
+
text: formatValue(value),
|
|
255
|
+
dataType: "text"
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function getVariableKind(value) {
|
|
260
|
+
if (Array.isArray(value)) return "array";
|
|
261
|
+
if (value === null) return "null";
|
|
262
|
+
if (value instanceof Map) return "map";
|
|
263
|
+
if (value instanceof Set) return "set";
|
|
264
|
+
return typeof value;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function getBoundIdentifiers(pattern, identifiers = []) {
|
|
268
|
+
if (!pattern) {
|
|
269
|
+
return identifiers;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
switch (pattern.type) {
|
|
273
|
+
case "Identifier":
|
|
274
|
+
identifiers.push(pattern.name);
|
|
275
|
+
break;
|
|
276
|
+
case "RestElement":
|
|
277
|
+
getBoundIdentifiers(pattern.argument, identifiers);
|
|
278
|
+
break;
|
|
279
|
+
case "AssignmentPattern":
|
|
280
|
+
getBoundIdentifiers(pattern.left, identifiers);
|
|
281
|
+
break;
|
|
282
|
+
case "ArrayPattern":
|
|
283
|
+
for (const element of pattern.elements) {
|
|
284
|
+
getBoundIdentifiers(element, identifiers);
|
|
285
|
+
}
|
|
286
|
+
break;
|
|
287
|
+
case "ObjectPattern":
|
|
288
|
+
for (const property of pattern.properties) {
|
|
289
|
+
if (property.type === "RestElement") {
|
|
290
|
+
getBoundIdentifiers(property.argument, identifiers);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
getBoundIdentifiers(property.value, identifiers);
|
|
295
|
+
}
|
|
296
|
+
break;
|
|
297
|
+
default:
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return identifiers;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function transformImportDeclaration(node, importIndex) {
|
|
305
|
+
const referenceName = `__nodebook_import_${importIndex}`;
|
|
306
|
+
const lines = [`const ${referenceName} = await globalThis.__nodebookImport(${JSON.stringify(node.source.value)});`];
|
|
307
|
+
|
|
308
|
+
for (const specifier of node.specifiers) {
|
|
309
|
+
if (specifier.type === "ImportDefaultSpecifier") {
|
|
310
|
+
lines.push(`globalThis.__nodebookSet(${JSON.stringify(specifier.local.name)}, ${referenceName}.default ?? ${referenceName});`);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (specifier.type === "ImportNamespaceSpecifier") {
|
|
315
|
+
lines.push(`globalThis.__nodebookSet(${JSON.stringify(specifier.local.name)}, ${referenceName}.__nodebook_namespace ?? ${referenceName});`);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const importedName =
|
|
320
|
+
specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value;
|
|
321
|
+
lines.push(`globalThis.__nodebookSet(${JSON.stringify(specifier.local.name)}, ${referenceName}[${JSON.stringify(importedName)}]);`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return lines.join("\n");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function transformHoistedStatement(node, source, cellId = null) {
|
|
328
|
+
const names =
|
|
329
|
+
node.type === "VariableDeclaration"
|
|
330
|
+
? node.declarations.flatMap((declaration) => getBoundIdentifiers(declaration.id))
|
|
331
|
+
: node.id?.name
|
|
332
|
+
? [node.id.name]
|
|
333
|
+
: [];
|
|
334
|
+
const statement = source.slice(node.start, node.end);
|
|
335
|
+
|
|
336
|
+
if (names.length === 0) {
|
|
337
|
+
return statement;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const setLines = names.map((name) => `globalThis.__nodebookSet(${JSON.stringify(name)}, ${name});`);
|
|
341
|
+
|
|
342
|
+
// For const declarations: register protection so that bare `name = value` assignments
|
|
343
|
+
// in later cells throw. Re-declarations via `const name = ...` are always allowed
|
|
344
|
+
// (they reset the old const) so notebooks remain freely re-runnable.
|
|
345
|
+
if (
|
|
346
|
+
node.type === "VariableDeclaration" &&
|
|
347
|
+
node.kind === "const" &&
|
|
348
|
+
cellId !== null
|
|
349
|
+
) {
|
|
350
|
+
const cellIdStr = JSON.stringify(cellId);
|
|
351
|
+
const registerLines = names.map(
|
|
352
|
+
(name) => `if (globalThis.__nodebookRegisterConst) globalThis.__nodebookRegisterConst(${JSON.stringify(name)}, ${cellIdStr});`
|
|
353
|
+
);
|
|
354
|
+
return `{\n${statement}\n${setLines.join("\n")}\n${registerLines.join("\n")}\n}`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return `{\n${statement}\n${setLines.join("\n")}\n}`;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function transformTopLevelStatement(node, source, importIndex, cellId = null) {
|
|
361
|
+
if (node.type === "ImportDeclaration") {
|
|
362
|
+
return transformImportDeclaration(node, importIndex);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (node.type === "VariableDeclaration" || node.type === "FunctionDeclaration" || node.type === "ClassDeclaration") {
|
|
366
|
+
return transformHoistedStatement(node, source, cellId);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (node.type === "ExportNamedDeclaration") {
|
|
370
|
+
// Skip bare `export {}` — TypeScript module sentinel, has no runtime effect
|
|
371
|
+
if (!node.declaration && node.specifiers.length === 0 && !node.source) {
|
|
372
|
+
return "";
|
|
373
|
+
}
|
|
374
|
+
if (node.declaration) {
|
|
375
|
+
return transformTopLevelStatement(node.declaration, source, importIndex, cellId);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Skip `export default` / `export * from` at top level — not needed in notebook context
|
|
380
|
+
if (node.type === "ExportDefaultDeclaration") {
|
|
381
|
+
return source.slice(node.declaration.start, node.declaration.end);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (node.type === "ExportAllDeclaration") {
|
|
385
|
+
return "";
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return source.slice(node.start, node.end);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function stripTypeScriptTypes(source) {
|
|
392
|
+
const result = ts.transpileModule(source, {
|
|
393
|
+
compilerOptions: {
|
|
394
|
+
target: ts.ScriptTarget.ESNext,
|
|
395
|
+
module: ts.ModuleKind.ESNext,
|
|
396
|
+
// Preserve runtime imports even when a cell only imports a symbol for later cells.
|
|
397
|
+
// Type-only imports are still erased or reduced to `import {}` by TypeScript.
|
|
398
|
+
verbatimModuleSyntax: true,
|
|
399
|
+
removeComments: false,
|
|
400
|
+
sourceMap: false
|
|
401
|
+
},
|
|
402
|
+
reportDiagnostics: false
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// TypeScript appends `export {};` to mark transpiled output as an ES module.
|
|
406
|
+
// This is a no-op sentinel that is invalid inside a vm.Script async IIFE wrapper,
|
|
407
|
+
// so we strip it before handing the code to Acorn / our AST transformer.
|
|
408
|
+
return result.outputText.replace(/\nexport\s*\{\s*\};\s*\n?$/, "\n");
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Walk an AST and collect start offsets of every prompt()/input() CallExpression
|
|
413
|
+
* that is NOT already wrapped in an AwaitExpression. Inserting `await ` at those
|
|
414
|
+
* offsets lets users write `const name = prompt("Name: ")` without an explicit
|
|
415
|
+
* `await` and still have the execution pause until the user provides input.
|
|
416
|
+
*/
|
|
417
|
+
function collectInputCallOffsets(ast) {
|
|
418
|
+
const offsets = [];
|
|
419
|
+
|
|
420
|
+
function walk(node, parentIsAwait) {
|
|
421
|
+
if (!node || typeof node !== "object" || !node.type) return;
|
|
422
|
+
|
|
423
|
+
if (node.type === "AwaitExpression") {
|
|
424
|
+
// The direct argument is already awaited — mark it so we don't double-wrap.
|
|
425
|
+
if (node.argument) walk(node.argument, true);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (
|
|
430
|
+
node.type === "CallExpression" &&
|
|
431
|
+
!parentIsAwait &&
|
|
432
|
+
node.callee?.type === "Identifier" &&
|
|
433
|
+
(node.callee.name === "prompt" || node.callee.name === "input")
|
|
434
|
+
) {
|
|
435
|
+
offsets.push(node.start);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
for (const key of Object.keys(node)) {
|
|
439
|
+
if (key === "type" || key === "start" || key === "end") continue;
|
|
440
|
+
const child = node[key];
|
|
441
|
+
if (Array.isArray(child)) {
|
|
442
|
+
for (const item of child) {
|
|
443
|
+
if (item && typeof item === "object" && item.type) walk(item, false);
|
|
444
|
+
}
|
|
445
|
+
} else if (child && typeof child === "object" && child.type) {
|
|
446
|
+
walk(child, false);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
walk(ast, false);
|
|
452
|
+
return offsets.sort((a, b) => a - b);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function compileNotebookCode(source, language = "typescript", cellId = null) {
|
|
456
|
+
const jsSource = language === "typescript" ? stripTypeScriptTypes(source) : source;
|
|
457
|
+
const ast = parse(jsSource, {
|
|
458
|
+
ecmaVersion: "latest",
|
|
459
|
+
sourceType: "module",
|
|
460
|
+
allowAwaitOutsideFunction: true,
|
|
461
|
+
allowReturnOutsideFunction: true
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// Automatically insert `await` before bare prompt()/input() calls so that
|
|
465
|
+
// `const name = prompt("Name: ")` blocks until the user provides input,
|
|
466
|
+
// matching the synchronous-feeling browser prompt() API.
|
|
467
|
+
const promptOffsets = collectInputCallOffsets(ast);
|
|
468
|
+
let finalSource = jsSource;
|
|
469
|
+
let finalAst = ast;
|
|
470
|
+
|
|
471
|
+
if (promptOffsets.length > 0) {
|
|
472
|
+
// Insert `await ` from right to left so earlier offsets stay valid.
|
|
473
|
+
for (let i = promptOffsets.length - 1; i >= 0; i--) {
|
|
474
|
+
const offset = promptOffsets[i];
|
|
475
|
+
finalSource = finalSource.slice(0, offset) + "await " + finalSource.slice(offset);
|
|
476
|
+
}
|
|
477
|
+
// Re-parse so node offsets align with the new source for slicing below.
|
|
478
|
+
finalAst = parse(finalSource, {
|
|
479
|
+
ecmaVersion: "latest",
|
|
480
|
+
sourceType: "module",
|
|
481
|
+
allowAwaitOutsideFunction: true,
|
|
482
|
+
allowReturnOutsideFunction: true
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const statements = [];
|
|
487
|
+
let importIndex = 0;
|
|
488
|
+
|
|
489
|
+
for (let index = 0; index < finalAst.body.length; index += 1) {
|
|
490
|
+
const node = finalAst.body[index];
|
|
491
|
+
const isLast = index === finalAst.body.length - 1;
|
|
492
|
+
|
|
493
|
+
if (isLast && node.type === "ExpressionStatement") {
|
|
494
|
+
statements.push(`return (${finalSource.slice(node.expression.start, node.expression.end)});`);
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
statements.push(transformTopLevelStatement(node, finalSource, importIndex, cellId));
|
|
499
|
+
|
|
500
|
+
if (node.type === "ImportDeclaration") {
|
|
501
|
+
importIndex += 1;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return `(async () => {\n${statements.join("\n\n")}\n})()`;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export class KernelSession {
|
|
509
|
+
constructor(notebookPath) {
|
|
510
|
+
this.notebookPath = notebookPath;
|
|
511
|
+
this.workingDirectory = path.dirname(notebookPath);
|
|
512
|
+
this.importBridgeDirectory = path.join(this.workingDirectory, ".nodebook-cache");
|
|
513
|
+
this.executionCount = 0;
|
|
514
|
+
this.context = this.createContext();
|
|
515
|
+
|
|
516
|
+
// Counter used to give each bridge file a unique URL so Node.js's ESM loader
|
|
517
|
+
// never reuses a cached (possibly failed) module resolution across attempts.
|
|
518
|
+
this._importBridgeCounter = 0;
|
|
519
|
+
|
|
520
|
+
// Maps a specifier to the bridge URL that was last successfully imported.
|
|
521
|
+
// Once an import succeeds we reuse the same URL so Node.js can serve it
|
|
522
|
+
// from its own module cache without re-reading the bridge file.
|
|
523
|
+
this._resolvedBridges = new Map();
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
createContext() {
|
|
527
|
+
const importModule = (specifier) => this.importModule(specifier);
|
|
528
|
+
// CommonJS require() is intentionally disabled — notebooks use ES Modules only.
|
|
529
|
+
// Calling require() at runtime throws a clear error pointing users to ESM syntax.
|
|
530
|
+
const require = (id) => {
|
|
531
|
+
throw new Error(
|
|
532
|
+
`CommonJS (CJS) is not supported in this notebook.\n` +
|
|
533
|
+
`Use ES Modules (ESM) instead:\n\n` +
|
|
534
|
+
` import ... from "${id}"`
|
|
535
|
+
);
|
|
536
|
+
};
|
|
537
|
+
const sandbox = {
|
|
538
|
+
globalThis: null,
|
|
539
|
+
__nodebookImport: importModule,
|
|
540
|
+
__nodebookSet: null,
|
|
541
|
+
require,
|
|
542
|
+
process: this.createProcessObject({}),
|
|
543
|
+
Buffer,
|
|
544
|
+
URL,
|
|
545
|
+
URLSearchParams,
|
|
546
|
+
TextEncoder,
|
|
547
|
+
TextDecoder,
|
|
548
|
+
setTimeout,
|
|
549
|
+
clearTimeout,
|
|
550
|
+
setInterval,
|
|
551
|
+
clearInterval,
|
|
552
|
+
setImmediate,
|
|
553
|
+
clearImmediate,
|
|
554
|
+
fetch,
|
|
555
|
+
structuredClone,
|
|
556
|
+
module: { exports: {} },
|
|
557
|
+
exports: {},
|
|
558
|
+
display: this.createDisplayApi([]),
|
|
559
|
+
__dirname: this.workingDirectory,
|
|
560
|
+
__filename: path.join(this.workingDirectory, "__cell__.js")
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
sandbox.globalThis = sandbox;
|
|
564
|
+
sandbox.console = this.createConsole([]);
|
|
565
|
+
|
|
566
|
+
const ctx = vm.createContext(sandbox, {
|
|
567
|
+
name: `Nodebook:${path.basename(this.notebookPath)}`
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// Define __nodebookSet AFTER contextification so it closes over ctx
|
|
571
|
+
// (the contextified object) and sets properties on it from outside the vm.
|
|
572
|
+
// Always bypasses any existing setter/getter so initial declarations in a cell
|
|
573
|
+
// always succeed — even if a previous run left a const accessor on the property.
|
|
574
|
+
ctx.__nodebookSet = (name, value) => {
|
|
575
|
+
const descriptor = Object.getOwnPropertyDescriptor(ctx, name);
|
|
576
|
+
if (descriptor && (descriptor.get || descriptor.set)) {
|
|
577
|
+
// Property is an accessor (const-protected). Update the backing store directly
|
|
578
|
+
// so the getter returns the new value, and then re-protect as const.
|
|
579
|
+
if (ctx.__nodebookConstStorage) {
|
|
580
|
+
ctx.__nodebookConstStorage[name] = value;
|
|
581
|
+
}
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
ctx[name] = value;
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
// Const protection tracking: prevents bare `x = value` assignments to const variables.
|
|
588
|
+
// Re-declarations via `const x = ...` are always allowed (notebooks must be re-runnable).
|
|
589
|
+
ctx.__nodebookConsts = new Set(); // names currently protected as const
|
|
590
|
+
ctx.__nodebookCellConsts = new Map(); // cellId → Set<name> for cleanup on cell re-run
|
|
591
|
+
ctx.__nodebookConstStorage = Object.create(null); // backing store for const getter/setters
|
|
592
|
+
|
|
593
|
+
ctx.__nodebookRegisterConst = (name, cellId) => {
|
|
594
|
+
ctx.__nodebookConsts.add(name);
|
|
595
|
+
if (!ctx.__nodebookCellConsts.has(cellId)) ctx.__nodebookCellConsts.set(cellId, new Set());
|
|
596
|
+
ctx.__nodebookCellConsts.get(cellId).add(name);
|
|
597
|
+
// Snapshot the current value and install a throwing setter so bare `name = value`
|
|
598
|
+
// assignments in any subsequent code throw TypeError, while __nodebookSet bypasses it.
|
|
599
|
+
ctx.__nodebookConstStorage[name] = ctx[name];
|
|
600
|
+
try {
|
|
601
|
+
Object.defineProperty(ctx, name, {
|
|
602
|
+
get() { return ctx.__nodebookConstStorage[name]; },
|
|
603
|
+
set(_v) { throw new TypeError(`Assignment to constant variable '${name}'.`); },
|
|
604
|
+
configurable: true,
|
|
605
|
+
enumerable: true
|
|
606
|
+
});
|
|
607
|
+
} catch (_err) { /* ignore */ }
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
return ctx;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
createProcessObject(envOverrides) {
|
|
614
|
+
return {
|
|
615
|
+
...process,
|
|
616
|
+
env: {
|
|
617
|
+
...process.env,
|
|
618
|
+
...envOverrides
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
createConsole(outputs) {
|
|
624
|
+
const push = (kind, values) => {
|
|
625
|
+
if (values.length === 1) {
|
|
626
|
+
const entry = serializeOutputValue(values[0], kind);
|
|
627
|
+
outputs.push(entry);
|
|
628
|
+
this.onOutput?.(entry);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const entry = {
|
|
633
|
+
type: kind,
|
|
634
|
+
text: values.map((value) => formatValue(value)).join(" "),
|
|
635
|
+
dataType: "text"
|
|
636
|
+
};
|
|
637
|
+
outputs.push(entry);
|
|
638
|
+
this.onOutput?.(entry);
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
return {
|
|
642
|
+
log: (...values) => push("log", values),
|
|
643
|
+
info: (...values) => push("info", values),
|
|
644
|
+
warn: (...values) => push("warn", values),
|
|
645
|
+
error: (...values) => push("error", values),
|
|
646
|
+
dir: (value) => push("log", [value]),
|
|
647
|
+
table: (value) => {
|
|
648
|
+
const rows = normalizeTableRows(value) ?? [{ value: formatValue(value) }];
|
|
649
|
+
const entry = {
|
|
650
|
+
type: "log",
|
|
651
|
+
text: formatValue(value),
|
|
652
|
+
dataType: "table",
|
|
653
|
+
data: rows
|
|
654
|
+
};
|
|
655
|
+
outputs.push(entry);
|
|
656
|
+
this.onOutput?.(entry);
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
createDisplayApi(outputs) {
|
|
662
|
+
const push = (dataType, payload, text = formatValue(payload), outputType = "result") => {
|
|
663
|
+
const entry = {
|
|
664
|
+
type: outputType,
|
|
665
|
+
text,
|
|
666
|
+
dataType,
|
|
667
|
+
data: payload
|
|
668
|
+
};
|
|
669
|
+
outputs.push(entry);
|
|
670
|
+
this.onOutput?.(entry);
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
// Individual named methods
|
|
674
|
+
const methods = {
|
|
675
|
+
text: (value) => push("text", { value: String(value ?? "") }, String(value ?? "")),
|
|
676
|
+
markdown: (value) => push("markdown", { markdown: String(value ?? "") }, String(value ?? "")),
|
|
677
|
+
html: (value) => push("html", { html: String(value ?? "") }, String(value ?? "")),
|
|
678
|
+
image: (src, alt="") => push("image", { src: String(src ?? ""), alt: String(alt ?? "") }, String(src ?? "")),
|
|
679
|
+
table: (rows) => push("table", normalizeTableRows(rows) ?? rows, formatValue(rows)),
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
// Also callable as display({ type, ... }) so both styles work:
|
|
683
|
+
// display({ type: 'html', html: '...' }) ← object style
|
|
684
|
+
// display.html('...') ← method style
|
|
685
|
+
const displayFn = (arg) => {
|
|
686
|
+
if (arg && typeof arg === "object" && typeof arg.type === "string") {
|
|
687
|
+
const { type, ...rest } = arg;
|
|
688
|
+
switch (type) {
|
|
689
|
+
case "text": return methods.text(rest.text ?? rest.value ?? "");
|
|
690
|
+
case "markdown": return methods.markdown(rest.markdown ?? rest.value ?? "");
|
|
691
|
+
case "html": return methods.html(rest.html ?? rest.value ?? "");
|
|
692
|
+
case "image": return methods.image(rest.src ?? rest.url ?? "", rest.alt ?? "");
|
|
693
|
+
case "table": return methods.table(rest.data ?? rest.rows ?? []);
|
|
694
|
+
default: return methods.text(formatValue(arg));
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
// Plain value fallback — format and show as text
|
|
698
|
+
return methods.text(formatValue(arg));
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
// Attach named methods so display.html() etc. still work
|
|
702
|
+
Object.assign(displayFn, methods);
|
|
703
|
+
return displayFn;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
resolveDynamicImport(specifier) {
|
|
707
|
+
if (specifier.startsWith("node:")) {
|
|
708
|
+
return specifier;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (specifier.startsWith(".") || specifier.startsWith("/")) {
|
|
712
|
+
const absolutePath = specifier.startsWith("/")
|
|
713
|
+
? specifier
|
|
714
|
+
: path.resolve(this.workingDirectory, specifier);
|
|
715
|
+
|
|
716
|
+
return pathToFileURL(absolutePath).href;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return specifier;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Create a brand-new bridge .mjs file for the given specifier.
|
|
724
|
+
*
|
|
725
|
+
* Each call produces a file with a unique, counter-suffixed name so that
|
|
726
|
+
* successive import attempts always get a URL that Node.js has never seen
|
|
727
|
+
* before. This is the key mechanism that defeats the ESM loader's
|
|
728
|
+
* per-URL failure cache: a previously failed URL is simply abandoned and a
|
|
729
|
+
* fresh URL is used instead.
|
|
730
|
+
*/
|
|
731
|
+
async _createFreshImportBridge(specifier) {
|
|
732
|
+
await fs.promises.mkdir(this.importBridgeDirectory, { recursive: true });
|
|
733
|
+
|
|
734
|
+
this._importBridgeCounter += 1;
|
|
735
|
+
const fileHash = createHash("sha1").update(specifier).digest("hex");
|
|
736
|
+
const bridgePath = path.join(
|
|
737
|
+
this.importBridgeDirectory,
|
|
738
|
+
`${fileHash}_${this._importBridgeCounter}.mjs`
|
|
739
|
+
);
|
|
740
|
+
|
|
741
|
+
const bridgeSource = [
|
|
742
|
+
`export * from ${JSON.stringify(specifier)};`,
|
|
743
|
+
`import * as __nodebook_namespace from ${JSON.stringify(specifier)};`,
|
|
744
|
+
"export { __nodebook_namespace };",
|
|
745
|
+
"export default ('default' in __nodebook_namespace ? __nodebook_namespace.default : __nodebook_namespace);"
|
|
746
|
+
].join("\n");
|
|
747
|
+
|
|
748
|
+
await fs.promises.writeFile(bridgePath, bridgeSource);
|
|
749
|
+
return pathToFileURL(bridgePath).href;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
async importModule(specifier) {
|
|
753
|
+
if (specifier.startsWith("node:") || specifier.startsWith(".") || specifier.startsWith("/")) {
|
|
754
|
+
return import(this.resolveDynamicImport(specifier));
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// If we already have a bridge URL that was successfully imported in this
|
|
758
|
+
// session, reuse it — Node.js will serve the result from its own cache.
|
|
759
|
+
const cachedUrl = this._resolvedBridges.get(specifier);
|
|
760
|
+
if (cachedUrl) {
|
|
761
|
+
return import(cachedUrl);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// No successful import yet (either first attempt, or a previous attempt
|
|
765
|
+
// failed because the package wasn't installed at that point).
|
|
766
|
+
// Always create a FRESH bridge file with a new unique URL so we bypass any
|
|
767
|
+
// failure that Node.js's ESM loader may have cached for an earlier URL.
|
|
768
|
+
const bridgeUrl = await this._createFreshImportBridge(specifier);
|
|
769
|
+
|
|
770
|
+
try {
|
|
771
|
+
const result = await import(bridgeUrl);
|
|
772
|
+
// Mark this URL as the canonical bridge for this specifier so that
|
|
773
|
+
// subsequent cells reuse it without creating yet another file.
|
|
774
|
+
this._resolvedBridges.set(specifier, bridgeUrl);
|
|
775
|
+
return result;
|
|
776
|
+
} catch (error) {
|
|
777
|
+
// Import failed (e.g. package not yet installed). The bridge URL is
|
|
778
|
+
// abandoned — the next call will create a new one and try again.
|
|
779
|
+
throw error;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
applyEnvOverrides(envOverrides) {
|
|
784
|
+
const previousValues = new Map();
|
|
785
|
+
|
|
786
|
+
for (const [key, value] of Object.entries(envOverrides)) {
|
|
787
|
+
previousValues.set(key, process.env[key]);
|
|
788
|
+
process.env[key] = value;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return () => {
|
|
792
|
+
for (const [key, previousValue] of previousValues.entries()) {
|
|
793
|
+
if (previousValue === undefined) {
|
|
794
|
+
delete process.env[key];
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
process.env[key] = previousValue;
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
async execute(code, cellId, envOverrides = {}, language = "typescript", onOutput = null, inputProvider = async () => "") {
|
|
804
|
+
const outputs = [];
|
|
805
|
+
const ctx = this.context;
|
|
806
|
+
this.executionCount += 1;
|
|
807
|
+
this.onOutput = onOutput;
|
|
808
|
+
ctx.console = this.createConsole(outputs);
|
|
809
|
+
ctx.display = this.createDisplayApi(outputs);
|
|
810
|
+
ctx.process = this.createProcessObject(envOverrides);
|
|
811
|
+
const provideInput = typeof inputProvider === "function" ? inputProvider : async () => "";
|
|
812
|
+
// Expose async input helpers for browser-driven prompts; also mirror on window for familiarity.
|
|
813
|
+
ctx.input = provideInput;
|
|
814
|
+
ctx.prompt = provideInput;
|
|
815
|
+
ctx.window = ctx.window ?? ctx;
|
|
816
|
+
ctx.window.input = provideInput;
|
|
817
|
+
ctx.window.prompt = provideInput;
|
|
818
|
+
const timeouts = createTimeoutHooks(ctx, outputs);
|
|
819
|
+
const intervals = createIntervalHooks(ctx, outputs);
|
|
820
|
+
const cancelPromise = new Promise((resolve) => {
|
|
821
|
+
this.cancelResolver = () => resolve();
|
|
822
|
+
});
|
|
823
|
+
const restoreEnv = this.applyEnvOverrides(envOverrides);
|
|
824
|
+
|
|
825
|
+
// Clear previous const declarations for this cell to allow re-running the same cell.
|
|
826
|
+
// Also clears consts from OTHER cells for any variable name that this cell will re-declare
|
|
827
|
+
// (cell IDs can change on notebook reload — notebooks must always be re-runnable).
|
|
828
|
+
if (cellId) {
|
|
829
|
+
const prevConsts = ctx.__nodebookCellConsts?.get(cellId);
|
|
830
|
+
if (prevConsts) {
|
|
831
|
+
for (const name of prevConsts) {
|
|
832
|
+
ctx.__nodebookConsts?.delete(name);
|
|
833
|
+
// Reset accessor property back to a plain writable data property
|
|
834
|
+
try {
|
|
835
|
+
Object.defineProperty(ctx, name, {
|
|
836
|
+
value: ctx.__nodebookConstStorage?.[name],
|
|
837
|
+
writable: true,
|
|
838
|
+
configurable: true,
|
|
839
|
+
enumerable: true
|
|
840
|
+
});
|
|
841
|
+
} catch (_e) { /* ignore */ }
|
|
842
|
+
}
|
|
843
|
+
ctx.__nodebookCellConsts.delete(cellId);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
try {
|
|
848
|
+
const compiledCode = compileNotebookCode(code, language, cellId);
|
|
849
|
+
const script = new vm.Script(compiledCode, {
|
|
850
|
+
filename: cellId ? `${cellId}.mjs` : "__cell__.mjs"
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
ctx.__nodebookImport = (specifier) => this.importModule(specifier);
|
|
854
|
+
|
|
855
|
+
let value = script.runInContext(ctx, {
|
|
856
|
+
timeout: 10_000
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
if (value && typeof value.then === "function") {
|
|
860
|
+
value = await value;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
await timeouts.flush();
|
|
864
|
+
|
|
865
|
+
// Suppress raw Node.js Timeout objects returned by setInterval/setTimeout
|
|
866
|
+
if (value !== undefined && isNodeTimeout(value)) {
|
|
867
|
+
value = undefined;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
if (intervals.activeIntervals.size > 0) {
|
|
871
|
+
await intervals.waitForDrain(cancelPromise);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (value !== undefined) {
|
|
875
|
+
const entry = serializeOutputValue(value, "result");
|
|
876
|
+
outputs.push(entry);
|
|
877
|
+
this.onOutput?.(entry);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
return {
|
|
881
|
+
ok: true,
|
|
882
|
+
executionCount: this.executionCount,
|
|
883
|
+
outputs
|
|
884
|
+
};
|
|
885
|
+
} catch (error) {
|
|
886
|
+
await timeouts.flush();
|
|
887
|
+
|
|
888
|
+
const entry = {
|
|
889
|
+
type: "error",
|
|
890
|
+
text: sanitizeError(error).stack
|
|
891
|
+
};
|
|
892
|
+
outputs.push(entry);
|
|
893
|
+
this.onOutput?.(entry);
|
|
894
|
+
|
|
895
|
+
return {
|
|
896
|
+
ok: false,
|
|
897
|
+
executionCount: this.executionCount,
|
|
898
|
+
outputs,
|
|
899
|
+
error: sanitizeError(error)
|
|
900
|
+
};
|
|
901
|
+
} finally {
|
|
902
|
+
this.onOutput = null;
|
|
903
|
+
timeouts.restore();
|
|
904
|
+
intervals.restore();
|
|
905
|
+
restoreEnv();
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
async listInstalledModules() {
|
|
910
|
+
const packages = new Set(
|
|
911
|
+
builtinModules
|
|
912
|
+
.filter((moduleName) => !moduleName.startsWith("_"))
|
|
913
|
+
.map((moduleName) => moduleName.replace(/^node:/, ""))
|
|
914
|
+
);
|
|
915
|
+
|
|
916
|
+
for (const dep of await this.listDeclaredPackages()) {
|
|
917
|
+
packages.add(dep);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
return Array.from(packages).sort((left, right) => left.localeCompare(right));
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
async listDeclaredPackages() {
|
|
924
|
+
const packageJsonPath = path.join(this.workingDirectory, "package.json");
|
|
925
|
+
|
|
926
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
927
|
+
return [];
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const raw = await fs.promises.readFile(packageJsonPath, "utf8");
|
|
931
|
+
const packageJson = JSON.parse(raw);
|
|
932
|
+
const deps = Object.keys(packageJson.dependencies ?? {});
|
|
933
|
+
const devDeps = Object.keys(packageJson.devDependencies ?? {});
|
|
934
|
+
|
|
935
|
+
return Array.from(new Set([...deps, ...devDeps])).sort((left, right) => left.localeCompare(right));
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Invalidate the import bridge cache for one specific package or for ALL
|
|
940
|
+
* packages (when no specifier is given).
|
|
941
|
+
*
|
|
942
|
+
* Call this after any npm install / uninstall / update operation so that the
|
|
943
|
+
* next import() creates a fresh bridge file with a new URL, bypassing
|
|
944
|
+
* Node.js's ESM module cache. This ensures:
|
|
945
|
+
* - Newly installed packages become importable immediately.
|
|
946
|
+
* - Uninstalled packages stop being importable immediately (no stale cache).
|
|
947
|
+
*/
|
|
948
|
+
invalidateImportCache(specifier = null) {
|
|
949
|
+
if (specifier) {
|
|
950
|
+
this._resolvedBridges.delete(specifier);
|
|
951
|
+
} else {
|
|
952
|
+
this._resolvedBridges.clear();
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
cancelExecution() {
|
|
957
|
+
const ctx = this.context;
|
|
958
|
+
try {
|
|
959
|
+
// Find all Timeout-like entries on the context (limited heuristic)
|
|
960
|
+
for (const key of Object.keys(ctx)) {
|
|
961
|
+
const val = ctx[key];
|
|
962
|
+
if (val !== null && typeof val === "object" && typeof val._idleTimeout === "number") {
|
|
963
|
+
clearTimeout(val);
|
|
964
|
+
clearInterval(val);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
if (ctx.__nodebookActiveIntervals) {
|
|
968
|
+
for (const handle of ctx.__nodebookActiveIntervals) {
|
|
969
|
+
try { clearInterval(handle); } catch (_e) { /* ignore */ }
|
|
970
|
+
}
|
|
971
|
+
ctx.__nodebookActiveIntervals.clear();
|
|
972
|
+
}
|
|
973
|
+
if (ctx.__nodebookPendingTimeouts) {
|
|
974
|
+
for (const handle of ctx.__nodebookPendingTimeouts.keys()) {
|
|
975
|
+
try { clearTimeout(handle); } catch (_e) { /* ignore */ }
|
|
976
|
+
}
|
|
977
|
+
ctx.__nodebookPendingTimeouts.clear();
|
|
978
|
+
}
|
|
979
|
+
} catch (_e) { /* ignore */ }
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
listVariables() {
|
|
983
|
+
const internalNames = new Set([
|
|
984
|
+
"globalThis", "__nodebookImport", "require", "process", "Buffer", "URL", "URLSearchParams",
|
|
985
|
+
"TextEncoder", "TextDecoder", "setTimeout", "clearTimeout", "setInterval", "clearInterval",
|
|
986
|
+
"setImmediate", "clearImmediate", "fetch", "structuredClone", "module", "exports",
|
|
987
|
+
"__dirname", "__filename", "console", "display"
|
|
988
|
+
]);
|
|
989
|
+
|
|
990
|
+
return Object.keys(this.context)
|
|
991
|
+
.filter((name) => !internalNames.has(name) && !name.startsWith("__nodebook"))
|
|
992
|
+
.map((name) => {
|
|
993
|
+
const value = this.context[name];
|
|
994
|
+
return {
|
|
995
|
+
name,
|
|
996
|
+
kind: getVariableKind(value),
|
|
997
|
+
preview: formatValue(value),
|
|
998
|
+
dataType: serializeOutputValue(value).dataType ?? "text"
|
|
999
|
+
};
|
|
1000
|
+
})
|
|
1001
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
1002
|
+
}
|
|
1003
|
+
}
|