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.
Files changed (242) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +263 -0
  3. package/package.json +38 -0
  4. package/public/app.js +6686 -0
  5. package/public/components/constants.js +421 -0
  6. package/public/components/elements.js +118 -0
  7. package/public/components/state.js +53 -0
  8. package/public/icons/audio.svg +1 -0
  9. package/public/icons/azure.svg +1 -0
  10. package/public/icons/babel.svg +1 -0
  11. package/public/icons/bun.svg +1 -0
  12. package/public/icons/bun_light.svg +1 -0
  13. package/public/icons/c.svg +1 -0
  14. package/public/icons/chrome.svg +1 -0
  15. package/public/icons/citation.svg +1 -0
  16. package/public/icons/claude.svg +1 -0
  17. package/public/icons/console.svg +1 -0
  18. package/public/icons/cpp.svg +1 -0
  19. package/public/icons/css-map.svg +1 -0
  20. package/public/icons/css.svg +1 -0
  21. package/public/icons/database.svg +1 -0
  22. package/public/icons/docker.svg +1 -0
  23. package/public/icons/document.svg +1 -0
  24. package/public/icons/ejs.svg +1 -0
  25. package/public/icons/exe.svg +1 -0
  26. package/public/icons/favicon.svg +1 -0
  27. package/public/icons/figma.svg +1 -0
  28. package/public/icons/firebase.svg +1 -0
  29. package/public/icons/folder-admin-open.svg +1 -0
  30. package/public/icons/folder-admin.svg +1 -0
  31. package/public/icons/folder-api-open.svg +1 -0
  32. package/public/icons/folder-api.svg +1 -0
  33. package/public/icons/folder-app-open.svg +1 -0
  34. package/public/icons/folder-app.svg +1 -0
  35. package/public/icons/folder-archive-open.svg +1 -0
  36. package/public/icons/folder-archive.svg +1 -0
  37. package/public/icons/folder-attachment-open.svg +1 -0
  38. package/public/icons/folder-attachment.svg +1 -0
  39. package/public/icons/folder-aws-open.svg +1 -0
  40. package/public/icons/folder-aws.svg +1 -0
  41. package/public/icons/folder-backup-open.svg +1 -0
  42. package/public/icons/folder-backup.svg +1 -0
  43. package/public/icons/folder-class-open.svg +1 -0
  44. package/public/icons/folder-class.svg +1 -0
  45. package/public/icons/folder-claude-open.svg +1 -0
  46. package/public/icons/folder-claude.svg +1 -0
  47. package/public/icons/folder-client-open.svg +1 -0
  48. package/public/icons/folder-client.svg +1 -0
  49. package/public/icons/folder-command-open.svg +1 -0
  50. package/public/icons/folder-command.svg +1 -0
  51. package/public/icons/folder-components-open.svg +1 -0
  52. package/public/icons/folder-components.svg +1 -0
  53. package/public/icons/folder-config-open.svg +1 -0
  54. package/public/icons/folder-config.svg +1 -0
  55. package/public/icons/folder-connection-open.svg +1 -0
  56. package/public/icons/folder-connection.svg +1 -0
  57. package/public/icons/folder-console-open.svg +1 -0
  58. package/public/icons/folder-console.svg +1 -0
  59. package/public/icons/folder-container-open.svg +1 -0
  60. package/public/icons/folder-container.svg +1 -0
  61. package/public/icons/folder-content-open.svg +1 -0
  62. package/public/icons/folder-content.svg +1 -0
  63. package/public/icons/folder-context-open.svg +1 -0
  64. package/public/icons/folder-context.svg +1 -0
  65. package/public/icons/folder-controller-open.svg +1 -0
  66. package/public/icons/folder-controller.svg +1 -0
  67. package/public/icons/folder-core-open.svg +1 -0
  68. package/public/icons/folder-core.svg +1 -0
  69. package/public/icons/folder-css-open.svg +1 -0
  70. package/public/icons/folder-css.svg +1 -0
  71. package/public/icons/folder-custom-open.svg +1 -0
  72. package/public/icons/folder-custom.svg +1 -0
  73. package/public/icons/folder-database-open.svg +1 -0
  74. package/public/icons/folder-database.svg +1 -0
  75. package/public/icons/folder-decorators-open.svg +1 -0
  76. package/public/icons/folder-decorators.svg +1 -0
  77. package/public/icons/folder-desktop-open.svg +1 -0
  78. package/public/icons/folder-desktop.svg +1 -0
  79. package/public/icons/folder-dist-open.svg +1 -0
  80. package/public/icons/folder-dist.svg +1 -0
  81. package/public/icons/folder-docs-open.svg +1 -0
  82. package/public/icons/folder-docs.svg +1 -0
  83. package/public/icons/folder-download-open.svg +1 -0
  84. package/public/icons/folder-download.svg +1 -0
  85. package/public/icons/folder-dtos-open.svg +1 -0
  86. package/public/icons/folder-dtos.svg +1 -0
  87. package/public/icons/folder-element-open.svg +1 -0
  88. package/public/icons/folder-element.svg +1 -0
  89. package/public/icons/folder-environment-open.svg +1 -0
  90. package/public/icons/folder-environment.svg +1 -0
  91. package/public/icons/folder-error-open.svg +1 -0
  92. package/public/icons/folder-error.svg +1 -0
  93. package/public/icons/folder-event-open.svg +1 -0
  94. package/public/icons/folder-event.svg +1 -0
  95. package/public/icons/folder-examples-open.svg +1 -0
  96. package/public/icons/folder-examples.svg +1 -0
  97. package/public/icons/folder-expo-open.svg +1 -0
  98. package/public/icons/folder-expo.svg +1 -0
  99. package/public/icons/folder-export-open.svg +1 -0
  100. package/public/icons/folder-export.svg +1 -0
  101. package/public/icons/folder-features-open.svg +1 -0
  102. package/public/icons/folder-features.svg +1 -0
  103. package/public/icons/folder-filter-open.svg +1 -0
  104. package/public/icons/folder-filter.svg +1 -0
  105. package/public/icons/folder-firebase-open.svg +1 -0
  106. package/public/icons/folder-firebase.svg +1 -0
  107. package/public/icons/folder-firestore-open.svg +1 -0
  108. package/public/icons/folder-firestore.svg +1 -0
  109. package/public/icons/folder-font-open.svg +1 -0
  110. package/public/icons/folder-font.svg +1 -0
  111. package/public/icons/folder-functions-open.svg +1 -0
  112. package/public/icons/folder-functions.svg +1 -0
  113. package/public/icons/folder-gemini-ai-open.svg +1 -0
  114. package/public/icons/folder-gemini-ai.svg +1 -0
  115. package/public/icons/folder-git-open.svg +1 -0
  116. package/public/icons/folder-git.svg +1 -0
  117. package/public/icons/folder-github-open.svg +1 -0
  118. package/public/icons/folder-github.svg +1 -0
  119. package/public/icons/folder-helper-open.svg +1 -0
  120. package/public/icons/folder-helper.svg +1 -0
  121. package/public/icons/folder-home-open.svg +1 -0
  122. package/public/icons/folder-home.svg +1 -0
  123. package/public/icons/folder-icons-open.svg +1 -0
  124. package/public/icons/folder-icons.svg +1 -0
  125. package/public/icons/folder-images-open.svg +1 -0
  126. package/public/icons/folder-images.svg +1 -0
  127. package/public/icons/folder-interface-open.svg +1 -0
  128. package/public/icons/folder-interface.svg +1 -0
  129. package/public/icons/folder-ios-open.svg +1 -0
  130. package/public/icons/folder-ios.svg +1 -0
  131. package/public/icons/folder-java-open.svg +1 -0
  132. package/public/icons/folder-java.svg +1 -0
  133. package/public/icons/folder-javascript-open.svg +1 -0
  134. package/public/icons/folder-javascript.svg +1 -0
  135. package/public/icons/folder-middleware-open.svg +1 -0
  136. package/public/icons/folder-middleware.svg +1 -0
  137. package/public/icons/folder-migrations-open.svg +1 -0
  138. package/public/icons/folder-migrations.svg +1 -0
  139. package/public/icons/folder-other-open.svg +1 -0
  140. package/public/icons/folder-other.svg +1 -0
  141. package/public/icons/folder-packages-open.svg +1 -0
  142. package/public/icons/folder-packages.svg +1 -0
  143. package/public/icons/folder-pdf-open.svg +1 -0
  144. package/public/icons/folder-pdf.svg +1 -0
  145. package/public/icons/folder-plugin-open.svg +1 -0
  146. package/public/icons/folder-plugin.svg +1 -0
  147. package/public/icons/folder-project-open.svg +1 -0
  148. package/public/icons/folder-project.svg +1 -0
  149. package/public/icons/folder-public-open.svg +1 -0
  150. package/public/icons/folder-public.svg +1 -0
  151. package/public/icons/folder-python-open.svg +1 -0
  152. package/public/icons/folder-python.svg +1 -0
  153. package/public/icons/folder-repository-open.svg +1 -0
  154. package/public/icons/folder-repository.svg +1 -0
  155. package/public/icons/folder-routes-open.svg +1 -0
  156. package/public/icons/folder-routes.svg +1 -0
  157. package/public/icons/folder-rules-open.svg +1 -0
  158. package/public/icons/folder-rules.svg +1 -0
  159. package/public/icons/folder-sass-open.svg +1 -0
  160. package/public/icons/folder-sass.svg +1 -0
  161. package/public/icons/folder-scripts-open.svg +1 -0
  162. package/public/icons/folder-scripts.svg +1 -0
  163. package/public/icons/folder-server-open.svg +1 -0
  164. package/public/icons/folder-server.svg +1 -0
  165. package/public/icons/folder-serverless-open.svg +1 -0
  166. package/public/icons/folder-serverless.svg +1 -0
  167. package/public/icons/folder-skills-open.svg +1 -0
  168. package/public/icons/folder-skills.svg +1 -0
  169. package/public/icons/folder-src-open.svg +1 -0
  170. package/public/icons/folder-src.svg +1 -0
  171. package/public/icons/folder-stack-open.svg +1 -0
  172. package/public/icons/folder-stack.svg +1 -0
  173. package/public/icons/folder-store-open.svg +1 -0
  174. package/public/icons/folder-store.svg +1 -0
  175. package/public/icons/folder-supabase-open.svg +1 -0
  176. package/public/icons/folder-supabase.svg +1 -0
  177. package/public/icons/folder-svg-open.svg +1 -0
  178. package/public/icons/folder-svg.svg +1 -0
  179. package/public/icons/folder-target-open.svg +1 -0
  180. package/public/icons/folder-target.svg +1 -0
  181. package/public/icons/folder-tasks-open.svg +1 -0
  182. package/public/icons/folder-tasks.svg +1 -0
  183. package/public/icons/folder-temp-open.svg +1 -0
  184. package/public/icons/folder-temp.svg +1 -0
  185. package/public/icons/folder-template-open.svg +1 -0
  186. package/public/icons/folder-template.svg +1 -0
  187. package/public/icons/folder-test-open.svg +1 -0
  188. package/public/icons/folder-test.svg +1 -0
  189. package/public/icons/folder-tools-open.svg +1 -0
  190. package/public/icons/folder-tools.svg +1 -0
  191. package/public/icons/folder-typescript-open.svg +1 -0
  192. package/public/icons/folder-typescript.svg +1 -0
  193. package/public/icons/folder-ui-open.svg +1 -0
  194. package/public/icons/folder-ui.svg +1 -0
  195. package/public/icons/folder-upload-open.svg +1 -0
  196. package/public/icons/folder-upload.svg +1 -0
  197. package/public/icons/folder-utils-open.svg +1 -0
  198. package/public/icons/folder-utils.svg +1 -0
  199. package/public/icons/folder-video-open.svg +1 -0
  200. package/public/icons/folder-video.svg +1 -0
  201. package/public/icons/folder-views-open.svg +1 -0
  202. package/public/icons/folder-views.svg +1 -0
  203. package/public/icons/font.svg +1 -0
  204. package/public/icons/gemini-ai.svg +1 -0
  205. package/public/icons/gemini.svg +1 -0
  206. package/public/icons/git.svg +1 -0
  207. package/public/icons/google.svg +1 -0
  208. package/public/icons/graphql.svg +1 -0
  209. package/public/icons/html.svg +1 -0
  210. package/public/icons/image.svg +1 -0
  211. package/public/icons/java.svg +1 -0
  212. package/public/icons/javaclass.svg +1 -0
  213. package/public/icons/javascript.svg +1 -0
  214. package/public/icons/jsconfig.svg +1 -0
  215. package/public/icons/json.svg +1 -0
  216. package/public/icons/markdown.svg +1 -0
  217. package/public/icons/nodejs.svg +1 -0
  218. package/public/icons/nodejs_alt.svg +1 -0
  219. package/public/icons/nodemon.svg +1 -0
  220. package/public/icons/npm.svg +1 -0
  221. package/public/icons/pdf.svg +1 -0
  222. package/public/icons/prettier.svg +1 -0
  223. package/public/icons/prisma.svg +1 -0
  224. package/public/icons/python.svg +1 -0
  225. package/public/icons/react.svg +1 -0
  226. package/public/icons/react_ts.svg +1 -0
  227. package/public/icons/readme.svg +1 -0
  228. package/public/icons/remark.svg +1 -0
  229. package/public/icons/sass.svg +1 -0
  230. package/public/icons/svg.svg +1 -0
  231. package/public/icons/tailwindcss.svg +1 -0
  232. package/public/icons/typescript-def.svg +1 -0
  233. package/public/icons/typescript.svg +1 -0
  234. package/public/icons/zip.svg +1 -0
  235. package/public/index.html +1342 -0
  236. package/public/styles.css +4736 -0
  237. package/src/cli.js +175 -0
  238. package/src/lib/files.js +143 -0
  239. package/src/lib/notebook.js +141 -0
  240. package/src/lib/package-exports.js +331 -0
  241. package/src/lib/session.js +1003 -0
  242. 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
+ }