sandlot 0.1.2 → 0.1.3

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 (54) hide show
  1. package/README.md +138 -408
  2. package/dist/build-emitter.d.ts +31 -13
  3. package/dist/build-emitter.d.ts.map +1 -1
  4. package/dist/builder.d.ts +370 -0
  5. package/dist/builder.d.ts.map +1 -0
  6. package/dist/bundler.d.ts +1 -1
  7. package/dist/bundler.d.ts.map +1 -1
  8. package/dist/commands/compile.d.ts +13 -0
  9. package/dist/commands/compile.d.ts.map +1 -0
  10. package/dist/commands/index.d.ts +17 -0
  11. package/dist/commands/index.d.ts.map +1 -0
  12. package/dist/commands/packages.d.ts +17 -0
  13. package/dist/commands/packages.d.ts.map +1 -0
  14. package/dist/commands/run.d.ts +40 -0
  15. package/dist/commands/run.d.ts.map +1 -0
  16. package/dist/commands/types.d.ts +141 -0
  17. package/dist/commands/types.d.ts.map +1 -0
  18. package/dist/fs.d.ts +53 -49
  19. package/dist/fs.d.ts.map +1 -1
  20. package/dist/index.d.ts +5 -4
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +249 -427
  23. package/dist/internal.js +111 -87
  24. package/dist/runner.d.ts +314 -0
  25. package/dist/runner.d.ts.map +1 -0
  26. package/dist/sandbox-manager.d.ts +45 -21
  27. package/dist/sandbox-manager.d.ts.map +1 -1
  28. package/dist/sandbox.d.ts +144 -62
  29. package/dist/sandbox.d.ts.map +1 -1
  30. package/dist/shared-modules.d.ts +22 -3
  31. package/dist/shared-modules.d.ts.map +1 -1
  32. package/dist/shared-resources.d.ts +0 -3
  33. package/dist/shared-resources.d.ts.map +1 -1
  34. package/dist/typechecker.d.ts +1 -1
  35. package/package.json +2 -11
  36. package/src/build-emitter.ts +32 -29
  37. package/src/builder.ts +498 -0
  38. package/src/bundler.ts +24 -36
  39. package/src/commands/compile.ts +236 -0
  40. package/src/commands/index.ts +51 -0
  41. package/src/commands/packages.ts +154 -0
  42. package/src/commands/run.ts +245 -0
  43. package/src/commands/types.ts +172 -0
  44. package/src/fs.ts +82 -221
  45. package/src/index.ts +17 -12
  46. package/src/sandbox.ts +217 -149
  47. package/src/shared-modules.ts +74 -4
  48. package/src/shared-resources.ts +0 -3
  49. package/src/typechecker.ts +1 -1
  50. package/dist/react.d.ts +0 -159
  51. package/dist/react.d.ts.map +0 -1
  52. package/dist/react.js +0 -149
  53. package/src/commands.ts +0 -733
  54. package/src/sandbox-manager.ts +0 -409
package/dist/index.js CHANGED
@@ -7,47 +7,14 @@ var DEFAULT_DIR_MODE = 493;
7
7
  var DEFAULT_SYMLINK_MODE = 511;
8
8
  var DEFAULT_MAX_SIZE_BYTES = 50 * 1024 * 1024;
9
9
 
10
- class IndexedDbFs {
10
+ class Filesystem {
11
11
  entries;
12
- db = null;
13
- dbName;
14
12
  maxSizeBytes;
15
- dirty = false;
16
- constructor(entries, db, dbName, maxSizeBytes) {
13
+ constructor(entries, maxSizeBytes) {
17
14
  this.entries = entries;
18
- this.db = db;
19
- this.dbName = dbName;
20
15
  this.maxSizeBytes = maxSizeBytes;
21
16
  }
22
- static async create(options = {}) {
23
- const dbName = options.dbName ?? "sandlot-fs";
24
- const maxSizeBytes = options.maxSizeBytes ?? DEFAULT_MAX_SIZE_BYTES;
25
- const db = await IndexedDbFs.openDatabase(dbName);
26
- const entries = await IndexedDbFs.loadEntries(db);
27
- if (entries.size === 0) {
28
- entries.set("/", {
29
- type: "directory",
30
- mode: DEFAULT_DIR_MODE,
31
- mtime: new Date
32
- });
33
- if (options.initialFiles) {
34
- for (const [path, value] of Object.entries(options.initialFiles)) {
35
- const normalizedPath = IndexedDbFs.normalizePath(path);
36
- const init = IndexedDbFs.parseFileInit(value);
37
- IndexedDbFs.ensureParentDirs(entries, normalizedPath);
38
- entries.set(normalizedPath, {
39
- type: "file",
40
- content: init.content,
41
- mode: init.mode ?? DEFAULT_FILE_MODE,
42
- mtime: init.mtime ?? new Date
43
- });
44
- }
45
- }
46
- }
47
- const fs = new IndexedDbFs(entries, db, dbName, maxSizeBytes);
48
- return fs;
49
- }
50
- static createInMemory(options = {}) {
17
+ static create(options = {}) {
51
18
  const maxSizeBytes = options.maxSizeBytes ?? DEFAULT_MAX_SIZE_BYTES;
52
19
  const entries = new Map;
53
20
  entries.set("/", {
@@ -57,9 +24,9 @@ class IndexedDbFs {
57
24
  });
58
25
  if (options.initialFiles) {
59
26
  for (const [path, value] of Object.entries(options.initialFiles)) {
60
- const normalizedPath = IndexedDbFs.normalizePath(path);
61
- const init = IndexedDbFs.parseFileInit(value);
62
- IndexedDbFs.ensureParentDirs(entries, normalizedPath);
27
+ const normalizedPath = Filesystem.normalizePath(path);
28
+ const init = Filesystem.parseFileInit(value);
29
+ Filesystem.ensureParentDirs(entries, normalizedPath);
63
30
  entries.set(normalizedPath, {
64
31
  type: "file",
65
32
  content: init.content,
@@ -68,31 +35,21 @@ class IndexedDbFs {
68
35
  });
69
36
  }
70
37
  }
71
- return new IndexedDbFs(entries, null, "", maxSizeBytes);
38
+ return new Filesystem(entries, maxSizeBytes);
72
39
  }
73
- async save() {
74
- if (!this.db || !this.dirty) {
75
- return false;
76
- }
77
- const tx = this.db.transaction("entries", "readwrite");
78
- const store = tx.objectStore("entries");
79
- await this.promisifyRequest(store.clear());
40
+ getFiles() {
41
+ const files = {};
80
42
  for (const [path, entry] of this.entries) {
81
- store.put({ path, entry: this.serializeEntry(entry) });
82
- }
83
- await this.promisifyTransaction(tx);
84
- this.dirty = false;
85
- return true;
86
- }
87
- async reload() {
88
- if (!this.db) {
89
- return;
43
+ if (entry.type === "file") {
44
+ if (typeof entry.content === "string") {
45
+ files[path] = entry.content;
46
+ } else {
47
+ const base64 = this.encodeBase64(entry.content);
48
+ files[path] = `data:application/octet-stream;base64,${base64}`;
49
+ }
50
+ }
90
51
  }
91
- this.entries = await IndexedDbFs.loadEntries(this.db);
92
- this.dirty = false;
93
- }
94
- isDirty() {
95
- return this.dirty;
52
+ return files;
96
53
  }
97
54
  getSize() {
98
55
  let size = 0;
@@ -108,12 +65,6 @@ class IndexedDbFs {
108
65
  }
109
66
  return size;
110
67
  }
111
- close() {
112
- if (this.db) {
113
- this.db.close();
114
- this.db = null;
115
- }
116
- }
117
68
  async readFile(path, options) {
118
69
  const normalizedPath = this.normalizePath(path);
119
70
  const entry = this.resolveSymlinks(normalizedPath);
@@ -159,7 +110,6 @@ class IndexedDbFs {
159
110
  mode: existing?.mode ?? DEFAULT_FILE_MODE,
160
111
  mtime: new Date
161
112
  });
162
- this.dirty = true;
163
113
  }
164
114
  async appendFile(path, content, options) {
165
115
  const normalizedPath = this.normalizePath(path);
@@ -211,7 +161,6 @@ class IndexedDbFs {
211
161
  mode: DEFAULT_DIR_MODE,
212
162
  mtime: new Date
213
163
  });
214
- this.dirty = true;
215
164
  }
216
165
  async readdir(path) {
217
166
  const normalizedPath = this.normalizePath(path);
@@ -287,7 +236,6 @@ class IndexedDbFs {
287
236
  }
288
237
  }
289
238
  this.entries.delete(normalizedPath);
290
- this.dirty = true;
291
239
  }
292
240
  async cp(src, dest, options) {
293
241
  const srcPath = this.normalizePath(src);
@@ -314,7 +262,6 @@ class IndexedDbFs {
314
262
  this.ensureParentDirs(destPath);
315
263
  this.entries.set(destPath, this.cloneEntry(entry));
316
264
  }
317
- this.dirty = true;
318
265
  }
319
266
  async mv(src, dest) {
320
267
  const srcPath = this.normalizePath(src);
@@ -341,7 +288,6 @@ class IndexedDbFs {
341
288
  this.entries.delete(srcPath);
342
289
  this.entries.set(destPath, entry);
343
290
  }
344
- this.dirty = true;
345
291
  }
346
292
  resolvePath(base, path) {
347
293
  if (path.startsWith("/")) {
@@ -371,7 +317,6 @@ class IndexedDbFs {
371
317
  }
372
318
  entry.mode = mode;
373
319
  entry.mtime = new Date;
374
- this.dirty = true;
375
320
  }
376
321
  async symlink(target, linkPath) {
377
322
  const normalizedLinkPath = this.normalizePath(linkPath);
@@ -385,7 +330,6 @@ class IndexedDbFs {
385
330
  mode: DEFAULT_SYMLINK_MODE,
386
331
  mtime: new Date
387
332
  });
388
- this.dirty = true;
389
333
  }
390
334
  async link(existingPath, newPath) {
391
335
  const srcPath = this.normalizePath(existingPath);
@@ -407,7 +351,6 @@ class IndexedDbFs {
407
351
  mode: entry.mode,
408
352
  mtime: new Date
409
353
  });
410
- this.dirty = true;
411
354
  }
412
355
  async readlink(path) {
413
356
  const normalizedPath = this.normalizePath(path);
@@ -453,10 +396,9 @@ class IndexedDbFs {
453
396
  throw new Error(`ENOENT: no such file or directory, utimes '${path}'`);
454
397
  }
455
398
  entry.mtime = mtime;
456
- this.dirty = true;
457
399
  }
458
400
  normalizePath(path) {
459
- return IndexedDbFs.normalizePath(path);
401
+ return Filesystem.normalizePath(path);
460
402
  }
461
403
  static normalizePath(path) {
462
404
  if (!path || path === ".")
@@ -484,8 +426,7 @@ class IndexedDbFs {
484
426
  return lastSlash === 0 ? "/" : path.slice(0, lastSlash);
485
427
  }
486
428
  ensureParentDirs(path) {
487
- IndexedDbFs.ensureParentDirs(this.entries, path);
488
- this.dirty = true;
429
+ Filesystem.ensureParentDirs(this.entries, path);
489
430
  }
490
431
  static ensureParentDirs(entries, path) {
491
432
  const parts = path.split("/").filter(Boolean);
@@ -562,17 +503,20 @@ class IndexedDbFs {
562
503
  return new TextDecoder("utf-8").decode(buffer);
563
504
  }
564
505
  if (encoding === "base64") {
565
- let binary = "";
566
- for (let i = 0;i < buffer.byteLength; i++) {
567
- binary += String.fromCharCode(buffer[i]);
568
- }
569
- return btoa(binary);
506
+ return this.encodeBase64(buffer);
570
507
  }
571
508
  if (encoding === "hex") {
572
509
  return Array.from(buffer).map((b) => b.toString(16).padStart(2, "0")).join("");
573
510
  }
574
511
  return new TextDecoder("utf-8").decode(buffer);
575
512
  }
513
+ encodeBase64(buffer) {
514
+ let binary = "";
515
+ for (let i = 0;i < buffer.byteLength; i++) {
516
+ binary += String.fromCharCode(buffer[i]);
517
+ }
518
+ return btoa(binary);
519
+ }
576
520
  concatBuffers(a, b) {
577
521
  const result = new Uint8Array(a.byteLength + b.byteLength);
578
522
  result.set(a, 0);
@@ -585,77 +529,9 @@ class IndexedDbFs {
585
529
  }
586
530
  return value;
587
531
  }
588
- static openDatabase(dbName) {
589
- return new Promise((resolve, reject) => {
590
- const request = indexedDB.open(dbName, 1);
591
- request.onerror = () => reject(request.error);
592
- request.onsuccess = () => resolve(request.result);
593
- request.onupgradeneeded = (event) => {
594
- const db = event.target.result;
595
- if (!db.objectStoreNames.contains("entries")) {
596
- db.createObjectStore("entries", { keyPath: "path" });
597
- }
598
- };
599
- });
600
- }
601
- static async loadEntries(db) {
602
- const tx = db.transaction("entries", "readonly");
603
- const store = tx.objectStore("entries");
604
- return new Promise((resolve, reject) => {
605
- const request = store.getAll();
606
- request.onerror = () => reject(request.error);
607
- request.onsuccess = () => {
608
- const entries = new Map;
609
- for (const record of request.result) {
610
- entries.set(record.path, IndexedDbFs.deserializeEntry(record.entry));
611
- }
612
- resolve(entries);
613
- };
614
- });
615
- }
616
- serializeEntry(entry) {
617
- if (entry.type === "file" && entry.content instanceof Uint8Array) {
618
- return {
619
- ...entry,
620
- content: Array.from(entry.content),
621
- contentType: "uint8array",
622
- mtime: entry.mtime.toISOString()
623
- };
624
- }
625
- return {
626
- ...entry,
627
- mtime: entry.mtime.toISOString()
628
- };
629
- }
630
- static deserializeEntry(data) {
631
- const mtime = new Date(data.mtime);
632
- if (data.type === "file") {
633
- let content = data.content;
634
- if (data.contentType === "uint8array" && Array.isArray(content)) {
635
- content = new Uint8Array(content);
636
- }
637
- return { type: "file", content, mode: data.mode, mtime };
638
- }
639
- if (data.type === "symlink") {
640
- return { type: "symlink", target: data.target, mode: data.mode, mtime };
641
- }
642
- return { type: "directory", mode: data.mode, mtime };
643
- }
644
- promisifyRequest(request) {
645
- return new Promise((resolve, reject) => {
646
- request.onerror = () => reject(request.error);
647
- request.onsuccess = () => resolve(request.result);
648
- });
649
- }
650
- promisifyTransaction(tx) {
651
- return new Promise((resolve, reject) => {
652
- tx.onerror = () => reject(tx.error);
653
- tx.oncomplete = () => resolve();
654
- });
655
- }
656
532
  }
657
- function createInMemoryFs(initialFiles) {
658
- return IndexedDbFs.createInMemory({ initialFiles });
533
+ function createFilesystem(options) {
534
+ return Filesystem.create(options);
659
535
  }
660
536
 
661
537
  // src/packages.ts
@@ -1122,16 +998,18 @@ var GLOBAL_KEY = "__sandlot_shared_modules__";
1122
998
 
1123
999
  class SharedModuleRegistry {
1124
1000
  modules = new Map;
1001
+ exportNames = new Map;
1125
1002
  constructor() {
1126
1003
  globalThis[GLOBAL_KEY] = this;
1127
1004
  }
1128
1005
  register(moduleId, module) {
1129
1006
  this.modules.set(moduleId, module);
1007
+ this.exportNames.set(moduleId, introspectExports(module));
1130
1008
  return this;
1131
1009
  }
1132
1010
  registerAll(modules) {
1133
1011
  for (const [id, mod] of Object.entries(modules)) {
1134
- this.modules.set(id, mod);
1012
+ this.register(id, mod);
1135
1013
  }
1136
1014
  return this;
1137
1015
  }
@@ -1152,13 +1030,44 @@ class SharedModuleRegistry {
1152
1030
  list() {
1153
1031
  return [...this.modules.keys()];
1154
1032
  }
1033
+ getExportNames(moduleId) {
1034
+ return this.exportNames.get(moduleId) ?? [];
1035
+ }
1155
1036
  clear() {
1156
1037
  this.modules.clear();
1038
+ this.exportNames.clear();
1157
1039
  }
1158
1040
  get size() {
1159
1041
  return this.modules.size;
1160
1042
  }
1161
1043
  }
1044
+ function introspectExports(module) {
1045
+ if (module === null || module === undefined) {
1046
+ return [];
1047
+ }
1048
+ if (typeof module !== "object" && typeof module !== "function") {
1049
+ return [];
1050
+ }
1051
+ const exports = [];
1052
+ for (const key of Object.keys(module)) {
1053
+ if (isValidIdentifier(key)) {
1054
+ exports.push(key);
1055
+ }
1056
+ }
1057
+ return exports;
1058
+ }
1059
+ function isValidIdentifier(name) {
1060
+ if (name.length === 0)
1061
+ return false;
1062
+ if (!/^[a-zA-Z_$]/.test(name))
1063
+ return false;
1064
+ if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name))
1065
+ return false;
1066
+ const reserved = ["default", "class", "function", "var", "let", "const", "import", "export"];
1067
+ if (reserved.includes(name))
1068
+ return false;
1069
+ return true;
1070
+ }
1162
1071
  var defaultRegistry = null;
1163
1072
  function getSharedModuleRegistry() {
1164
1073
  if (!defaultRegistry) {
@@ -1178,6 +1087,9 @@ function unregisterSharedModule(moduleId) {
1178
1087
  function clearSharedModules() {
1179
1088
  getSharedModuleRegistry().clear();
1180
1089
  }
1090
+ function getSharedModuleExports(moduleId) {
1091
+ return getSharedModuleRegistry().getExportNames(moduleId);
1092
+ }
1181
1093
  function getSharedModuleRuntimeCode(moduleId) {
1182
1094
  return `
1183
1095
  (function() {
@@ -1375,55 +1287,12 @@ ${generateNamedExports(args.path)}
1375
1287
  };
1376
1288
  }
1377
1289
  function generateNamedExports(moduleId) {
1378
- const knownExports = {
1379
- react: [
1380
- "useState",
1381
- "useEffect",
1382
- "useContext",
1383
- "useReducer",
1384
- "useCallback",
1385
- "useMemo",
1386
- "useRef",
1387
- "useImperativeHandle",
1388
- "useLayoutEffect",
1389
- "useDebugValue",
1390
- "useDeferredValue",
1391
- "useTransition",
1392
- "useId",
1393
- "useSyncExternalStore",
1394
- "useInsertionEffect",
1395
- "useOptimistic",
1396
- "useActionState",
1397
- "createElement",
1398
- "cloneElement",
1399
- "createContext",
1400
- "forwardRef",
1401
- "lazy",
1402
- "memo",
1403
- "startTransition",
1404
- "Children",
1405
- "Component",
1406
- "PureComponent",
1407
- "Fragment",
1408
- "Profiler",
1409
- "StrictMode",
1410
- "Suspense",
1411
- "version",
1412
- "isValidElement"
1413
- ],
1414
- "react-dom": ["createPortal", "flushSync", "version"],
1415
- "react-dom/client": ["createRoot", "hydrateRoot"],
1416
- "react-dom/server": ["renderToString", "renderToStaticMarkup", "renderToPipeableStream"]
1417
- };
1418
- const exports = knownExports[moduleId];
1419
- if (!exports) {
1420
- return `
1421
- // Dynamic re-export for unknown module
1422
- export const __moduleProxy__ = __sandlot_mod__;
1423
- `;
1424
- }
1425
- return exports.map((name) => `export const ${name} = __sandlot_mod__.${name};`).join(`
1290
+ const exports = getSharedModuleExports(moduleId);
1291
+ if (exports.length > 0) {
1292
+ return exports.map((name) => `export const ${name} = __sandlot_mod__.${name};`).join(`
1426
1293
  `);
1294
+ }
1295
+ return `// No exports discovered for "${moduleId}" - use default import or call registerSharedModules() first`;
1427
1296
  }
1428
1297
  async function bundle(options) {
1429
1298
  await initBundler();
@@ -1466,7 +1335,8 @@ async function bundle(options) {
1466
1335
  globalName,
1467
1336
  target,
1468
1337
  external,
1469
- plugins: [plugin]
1338
+ plugins: [plugin],
1339
+ jsx: "automatic"
1470
1340
  });
1471
1341
  const code = result.outputFiles?.[0]?.text ?? "";
1472
1342
  return {
@@ -1489,7 +1359,21 @@ async function bundleAndImport(options) {
1489
1359
  }
1490
1360
  }
1491
1361
 
1492
- // src/commands.ts
1362
+ // src/commands/types.ts
1363
+ function formatEsbuildMessages(messages) {
1364
+ if (messages.length === 0)
1365
+ return "";
1366
+ return messages.map((msg) => {
1367
+ if (msg.location) {
1368
+ const { file, line, column } = msg.location;
1369
+ const loc = file ? `${file}${line ? `:${line}` : ""}${column ? `:${column}` : ""}` : "";
1370
+ return loc ? `${loc}: ${msg.text}` : msg.text;
1371
+ }
1372
+ return msg.text;
1373
+ }).join(`
1374
+ `);
1375
+ }
1376
+ // src/commands/compile.ts
1493
1377
  import { defineCommand } from "just-bash/browser";
1494
1378
 
1495
1379
  // src/typechecker.ts
@@ -1880,20 +1764,7 @@ async function hasExport(result, exportName) {
1880
1764
  return exportName in module;
1881
1765
  }
1882
1766
 
1883
- // src/commands.ts
1884
- function formatEsbuildMessages(messages) {
1885
- if (messages.length === 0)
1886
- return "";
1887
- return messages.map((msg) => {
1888
- if (msg.location) {
1889
- const { file, line, column } = msg.location;
1890
- const loc = file ? `${file}${line ? `:${line}` : ""}${column ? `:${column}` : ""}` : "";
1891
- return loc ? `${loc}: ${msg.text}` : msg.text;
1892
- }
1893
- return msg.text;
1894
- }).join(`
1895
- `);
1896
- }
1767
+ // src/commands/compile.ts
1897
1768
  function createTscCommand(deps) {
1898
1769
  const { fs, libFiles, tsconfigPath } = deps;
1899
1770
  return defineCommand("tsc", async (args, _ctx) => {
@@ -1958,7 +1829,7 @@ ${formatDiagnosticsForAgent(result.diagnostics.filter((d) => d.category === "war
1958
1829
  });
1959
1830
  }
1960
1831
  function createBuildCommand(deps) {
1961
- const { fs, libFiles, tsconfigPath, onBuild, sharedModules } = deps;
1832
+ const { fs, libFiles, tsconfigPath, onBuild, getValidation, sharedModules } = deps;
1962
1833
  return defineCommand("build", async (args, _ctx) => {
1963
1834
  let entryPoint = null;
1964
1835
  let skipTypecheck = false;
@@ -2030,8 +1901,39 @@ ${formatted}
2030
1901
  minify,
2031
1902
  sharedModules
2032
1903
  });
1904
+ let loadedModule;
1905
+ try {
1906
+ loadedModule = await loadModule(bundleResult);
1907
+ } catch (err) {
1908
+ const errorMessage = err instanceof Error ? err.message : String(err);
1909
+ return {
1910
+ stdout: "",
1911
+ stderr: `Build failed: Module failed to load.
1912
+
1913
+ ${errorMessage}
1914
+ `,
1915
+ exitCode: 1
1916
+ };
1917
+ }
1918
+ const validateFn = getValidation?.();
1919
+ let validatedModule = loadedModule;
1920
+ if (validateFn) {
1921
+ try {
1922
+ validatedModule = validateFn(loadedModule);
1923
+ } catch (err) {
1924
+ const errorMessage = err instanceof Error ? err.message : String(err);
1925
+ return {
1926
+ stdout: "",
1927
+ stderr: `Build failed: Validation error.
1928
+
1929
+ ${errorMessage}
1930
+ `,
1931
+ exitCode: 1
1932
+ };
1933
+ }
1934
+ }
2033
1935
  if (onBuild) {
2034
- await onBuild(bundleResult);
1936
+ await onBuild({ bundle: bundleResult, module: validatedModule });
2035
1937
  }
2036
1938
  let output = `Build successful!
2037
1939
  `;
@@ -2047,6 +1949,15 @@ ${formatted}
2047
1949
  }
2048
1950
  output += `Bundled: ${bundleResult.includedFiles.length} file(s)
2049
1951
  `;
1952
+ const exportNames = Object.keys(loadedModule).filter((k) => !k.startsWith("__"));
1953
+ if (exportNames.length > 0) {
1954
+ output += `Exports: ${exportNames.join(", ")}
1955
+ `;
1956
+ }
1957
+ if (validateFn) {
1958
+ output += `Validation: passed
1959
+ `;
1960
+ }
2050
1961
  if (bundleResult.warnings.length > 0) {
2051
1962
  output += `
2052
1963
  Build warnings:
@@ -2078,9 +1989,11 @@ ${formatDiagnosticsForAgent(warnings)}
2078
1989
  }
2079
1990
  });
2080
1991
  }
1992
+ // src/commands/packages.ts
1993
+ import { defineCommand as defineCommand2 } from "just-bash/browser";
2081
1994
  function createInstallCommand(deps) {
2082
1995
  const { fs, typesCache } = deps;
2083
- return defineCommand("install", async (args, _ctx) => {
1996
+ return defineCommand2("install", async (args, _ctx) => {
2084
1997
  if (args.length === 0) {
2085
1998
  return {
2086
1999
  stdout: "",
@@ -2134,7 +2047,7 @@ Examples:
2134
2047
  }
2135
2048
  function createUninstallCommand(deps) {
2136
2049
  const { fs } = deps;
2137
- return defineCommand("uninstall", async (args, _ctx) => {
2050
+ return defineCommand2("uninstall", async (args, _ctx) => {
2138
2051
  if (args.length === 0) {
2139
2052
  return {
2140
2053
  stdout: "",
@@ -2179,7 +2092,7 @@ function createUninstallCommand(deps) {
2179
2092
  }
2180
2093
  function createListCommand(deps) {
2181
2094
  const { fs } = deps;
2182
- return defineCommand("list", async (_args, _ctx) => {
2095
+ return defineCommand2("list", async (_args, _ctx) => {
2183
2096
  try {
2184
2097
  const packages = await listPackages(fs);
2185
2098
  if (packages.length === 0) {
@@ -2209,9 +2122,11 @@ function createListCommand(deps) {
2209
2122
  }
2210
2123
  });
2211
2124
  }
2125
+ // src/commands/run.ts
2126
+ import { defineCommand as defineCommand3 } from "just-bash/browser";
2212
2127
  function createRunCommand(deps) {
2213
2128
  const { fs, libFiles, tsconfigPath, runOptions = {}, sharedModules } = deps;
2214
- return defineCommand("run", async (args, _ctx) => {
2129
+ return defineCommand3("run", async (args, _ctx) => {
2215
2130
  let entryPoint = null;
2216
2131
  let skipTypecheck = runOptions.skipTypecheck ?? false;
2217
2132
  let timeout = runOptions.timeout ?? 30000;
@@ -2388,6 +2303,7 @@ ${err.stack}` : "";
2388
2303
  }
2389
2304
  });
2390
2305
  }
2306
+ // src/commands/index.ts
2391
2307
  function createDefaultCommands(deps) {
2392
2308
  return [
2393
2309
  createTscCommand(deps),
@@ -2597,9 +2513,7 @@ function hasDefaultResources() {
2597
2513
  // src/build-emitter.ts
2598
2514
  class BuildEmitter {
2599
2515
  listeners = new Set;
2600
- lastResult = null;
2601
2516
  emit = async (result) => {
2602
- this.lastResult = result;
2603
2517
  const promises = [];
2604
2518
  for (const listener of this.listeners) {
2605
2519
  const ret = listener(result);
@@ -2615,245 +2529,155 @@ class BuildEmitter {
2615
2529
  this.listeners.delete(callback);
2616
2530
  };
2617
2531
  }
2618
- waitFor() {
2619
- if (this.lastResult) {
2620
- const result = this.lastResult;
2621
- this.lastResult = null;
2622
- return Promise.resolve(result);
2623
- }
2624
- return new Promise((resolve) => {
2625
- const unsub = this.on((result) => {
2626
- unsub();
2627
- this.lastResult = null;
2628
- resolve(result);
2629
- });
2630
- });
2631
- }
2632
2532
  }
2633
2533
 
2634
2534
  // src/sandbox.ts
2635
2535
  async function createSandbox(options = {}) {
2636
- const {
2637
- fsOptions = {},
2638
- tsconfigPath = "/tsconfig.json",
2639
- resources: providedResources,
2640
- onBuild: onBuildCallback,
2641
- customCommands = [],
2642
- sharedModules
2643
- } = options;
2644
- const fsPromise = IndexedDbFs.create(fsOptions);
2645
- const resourcesPromise = providedResources ? Promise.resolve(providedResources) : getDefaultResources();
2646
- const bundlerPromise = initBundler();
2647
- const [fs, resources] = await Promise.all([fsPromise, resourcesPromise, bundlerPromise]);
2648
- const libFiles = resources.libFiles;
2649
- const typesCache = resources.typesCache;
2650
- const buildEmitter = new BuildEmitter;
2651
- if (onBuildCallback) {
2652
- buildEmitter.on(onBuildCallback);
2653
- }
2654
- const commandDeps = {
2655
- fs,
2656
- libFiles,
2657
- tsconfigPath,
2658
- onBuild: buildEmitter.emit,
2659
- typesCache,
2660
- sharedModules
2661
- };
2662
- const defaultCommands = createDefaultCommands(commandDeps);
2663
- const bash = new Bash({
2664
- fs,
2665
- cwd: "/",
2666
- customCommands: [...defaultCommands, ...customCommands]
2667
- });
2668
- return {
2669
- fs,
2670
- bash,
2671
- isDirty: () => fs.isDirty(),
2672
- save: () => fs.save(),
2673
- close: () => fs.close(),
2674
- onBuild: (callback) => buildEmitter.on(callback)
2675
- };
2676
- }
2677
- async function createInMemorySandbox(options = {}) {
2678
2536
  const {
2679
2537
  initialFiles,
2538
+ maxFilesystemSize,
2680
2539
  tsconfigPath = "/tsconfig.json",
2681
2540
  resources: providedResources,
2682
2541
  onBuild: onBuildCallback,
2683
2542
  customCommands = [],
2684
- sharedModules
2543
+ sharedModules,
2544
+ bashOptions = {}
2685
2545
  } = options;
2546
+ const fs = Filesystem.create({
2547
+ initialFiles,
2548
+ maxSizeBytes: maxFilesystemSize
2549
+ });
2686
2550
  const resourcesPromise = providedResources ? Promise.resolve(providedResources) : getDefaultResources();
2687
2551
  const bundlerPromise = initBundler();
2688
- const fs = IndexedDbFs.createInMemory({ initialFiles });
2689
2552
  const [resources] = await Promise.all([resourcesPromise, bundlerPromise]);
2690
2553
  const libFiles = resources.libFiles;
2691
2554
  const typesCache = resources.typesCache;
2555
+ if (sharedModules && sharedModules.length > 0) {
2556
+ const basePackages = new Set;
2557
+ for (const moduleId of sharedModules) {
2558
+ const { packageName } = parseImportPath(moduleId);
2559
+ basePackages.add(packageName);
2560
+ }
2561
+ await Promise.all(Array.from(basePackages).map(async (packageName) => {
2562
+ try {
2563
+ await installPackage(fs, packageName, { cache: typesCache });
2564
+ } catch (err) {
2565
+ console.warn(`[sandlot] Failed to install types for shared module "${packageName}":`, err);
2566
+ }
2567
+ }));
2568
+ }
2692
2569
  const buildEmitter = new BuildEmitter;
2570
+ let lastBuild = null;
2571
+ buildEmitter.on((result) => {
2572
+ lastBuild = result;
2573
+ });
2693
2574
  if (onBuildCallback) {
2694
2575
  buildEmitter.on(onBuildCallback);
2695
2576
  }
2577
+ let validationFn = null;
2696
2578
  const commandDeps = {
2697
2579
  fs,
2698
2580
  libFiles,
2699
2581
  tsconfigPath,
2700
2582
  onBuild: buildEmitter.emit,
2583
+ getValidation: () => validationFn,
2701
2584
  typesCache,
2702
2585
  sharedModules
2703
2586
  };
2704
2587
  const defaultCommands = createDefaultCommands(commandDeps);
2705
2588
  const bash = new Bash({
2589
+ ...bashOptions,
2706
2590
  fs,
2707
- cwd: "/",
2708
2591
  customCommands: [...defaultCommands, ...customCommands]
2709
2592
  });
2710
2593
  return {
2711
2594
  fs,
2712
2595
  bash,
2713
- isDirty: () => fs.isDirty(),
2714
- save: () => Promise.resolve(false),
2715
- close: () => {},
2716
- onBuild: (callback) => buildEmitter.on(callback)
2596
+ get lastBuild() {
2597
+ return lastBuild;
2598
+ },
2599
+ getState: () => ({ files: fs.getFiles() }),
2600
+ onBuild: (callback) => buildEmitter.on(callback),
2601
+ setValidation: (fn) => {
2602
+ validationFn = fn;
2603
+ },
2604
+ clearValidation: () => {
2605
+ validationFn = null;
2606
+ }
2717
2607
  };
2718
2608
  }
2719
- // src/sandbox-manager.ts
2720
- import { Bash as Bash2 } from "just-bash/browser";
2721
- class SandboxManager {
2722
- resources = null;
2723
- sandboxes = new Map;
2724
- initialized = false;
2725
- initPromise = null;
2726
- nextId = 1;
2727
- options;
2728
- constructor(options = {}) {
2729
- this.options = {
2730
- libs: options.libs ?? getDefaultBrowserLibs(),
2731
- ...options
2732
- };
2733
- }
2734
- async initialize() {
2735
- if (this.initialized)
2736
- return;
2737
- if (this.initPromise) {
2738
- await this.initPromise;
2739
- return;
2740
- }
2741
- this.initPromise = this.doInitialize();
2742
- await this.initPromise;
2743
- this.initialized = true;
2744
- }
2745
- async doInitialize() {
2746
- this.resources = await createSharedResources({
2747
- libs: this.options.libs,
2748
- skipLibs: this.options.skipLibs,
2749
- skipBundler: this.options.skipBundler
2750
- });
2751
- }
2752
- async createSandbox(options = {}) {
2753
- await this.initialize();
2754
- const {
2755
- id = `sandbox-${this.nextId++}`,
2756
- fsOptions = {},
2757
- initialFiles,
2758
- tsconfigPath = "/tsconfig.json",
2759
- onBuild,
2760
- customCommands = [],
2761
- inMemory = true,
2762
- sharedModules = this.options.sharedModules
2763
- } = options;
2764
- let fs;
2765
- if (inMemory) {
2766
- fs = IndexedDbFs.createInMemory({
2767
- initialFiles,
2768
- maxSizeBytes: fsOptions.maxSizeBytes
2769
- });
2770
- } else {
2771
- fs = await IndexedDbFs.create({
2772
- dbName: fsOptions.dbName ?? id,
2773
- initialFiles,
2774
- maxSizeBytes: fsOptions.maxSizeBytes
2775
- });
2609
+ // src/builder.ts
2610
+ function createBuilder(options) {
2611
+ return async (prompt, callOptions) => {
2612
+ const sandbox = options.sandbox ?? await createSandbox(options.sandboxOptions);
2613
+ if (callOptions?.validate) {
2614
+ sandbox.setValidation(callOptions.validate);
2776
2615
  }
2777
- const buildEmitter = new BuildEmitter;
2778
- if (onBuild) {
2779
- buildEmitter.on(onBuild);
2780
- }
2781
- const commandDeps = {
2782
- fs,
2783
- libFiles: this.resources.libFiles,
2784
- tsconfigPath,
2785
- onBuild: buildEmitter.emit,
2786
- typesCache: this.resources.typesCache,
2787
- sharedModules
2788
- };
2789
- const defaultCommands = createDefaultCommands(commandDeps);
2790
- const bash = new Bash2({
2791
- fs,
2792
- cwd: "/",
2793
- customCommands: [...defaultCommands, ...customCommands]
2616
+ const captured = { output: null };
2617
+ const unsubscribe = sandbox.onBuild((output) => {
2618
+ captured.output = output;
2794
2619
  });
2795
- const sandbox = {
2796
- id,
2797
- fs,
2798
- bash,
2799
- isDirty: () => fs.isDirty(),
2800
- save: () => fs.save(),
2801
- close: () => {
2802
- fs.close();
2803
- this.sandboxes.delete(id);
2804
- },
2805
- onBuild: (callback) => buildEmitter.on(callback)
2806
- };
2807
- this.sandboxes.set(id, sandbox);
2808
- return sandbox;
2809
- }
2810
- getSandbox(id) {
2811
- return this.sandboxes.get(id);
2812
- }
2813
- getAllSandboxes() {
2814
- return Array.from(this.sandboxes.values());
2815
- }
2816
- closeSandbox(id) {
2817
- const sandbox = this.sandboxes.get(id);
2818
- if (sandbox) {
2819
- sandbox.close();
2820
- return true;
2821
- }
2822
- return false;
2823
- }
2824
- destroyAll() {
2825
- for (const sandbox of this.sandboxes.values()) {
2826
- sandbox.fs.close();
2620
+ const { timeout, signal } = callOptions ?? {};
2621
+ let timeoutId;
2622
+ let abortController;
2623
+ if (timeout !== undefined || signal !== undefined) {
2624
+ abortController = new AbortController;
2625
+ if (timeout !== undefined) {
2626
+ timeoutId = setTimeout(() => {
2627
+ abortController.abort(new Error(`Build timed out after ${timeout}ms`));
2628
+ }, timeout);
2629
+ }
2630
+ if (signal !== undefined) {
2631
+ if (signal.aborted) {
2632
+ abortController.abort(signal.reason);
2633
+ } else {
2634
+ signal.addEventListener("abort", () => {
2635
+ abortController.abort(signal.reason);
2636
+ }, { once: true });
2637
+ }
2638
+ }
2827
2639
  }
2828
- this.sandboxes.clear();
2829
- }
2830
- async saveAll() {
2831
- const results = new Map;
2832
- for (const [id, sandbox] of this.sandboxes) {
2833
- const saved = await sandbox.save();
2834
- results.set(id, saved);
2640
+ let result;
2641
+ let error = null;
2642
+ try {
2643
+ const buildPromise = options.build(sandbox, prompt);
2644
+ if (abortController) {
2645
+ const abortPromise = new Promise((_, reject) => {
2646
+ abortController.signal.addEventListener("abort", () => {
2647
+ const err = abortController.signal.reason instanceof Error ? abortController.signal.reason : new Error("Build aborted");
2648
+ err.name = "AbortError";
2649
+ reject(err);
2650
+ }, { once: true });
2651
+ if (abortController.signal.aborted) {
2652
+ const err = abortController.signal.reason instanceof Error ? abortController.signal.reason : new Error("Build aborted");
2653
+ err.name = "AbortError";
2654
+ reject(err);
2655
+ }
2656
+ });
2657
+ result = await Promise.race([buildPromise, abortPromise]);
2658
+ } else {
2659
+ result = await buildPromise;
2660
+ }
2661
+ } catch (err) {
2662
+ error = err instanceof Error ? err : new Error(String(err));
2663
+ } finally {
2664
+ if (timeoutId !== undefined) {
2665
+ clearTimeout(timeoutId);
2666
+ }
2667
+ unsubscribe();
2668
+ if (callOptions?.validate) {
2669
+ sandbox.clearValidation();
2670
+ }
2835
2671
  }
2836
- return results;
2837
- }
2838
- getDirtySandboxes() {
2839
- return Array.from(this.sandboxes.entries()).filter(([_, sandbox]) => sandbox.isDirty()).map(([id]) => id);
2840
- }
2841
- getStats() {
2672
+ const buildOutput = captured.output;
2842
2673
  return {
2843
- initialized: this.initialized,
2844
- activeSandboxes: this.sandboxes.size,
2845
- libFilesCount: this.resources?.libFiles.size ?? 0,
2846
- sandboxIds: Array.from(this.sandboxes.keys())
2674
+ result,
2675
+ error,
2676
+ bundle: buildOutput?.bundle ?? null,
2677
+ module: buildOutput?.module ?? null,
2678
+ sandbox
2847
2679
  };
2848
- }
2849
- getResources() {
2850
- return this.resources;
2851
- }
2852
- }
2853
- async function createSandboxManager(options = {}) {
2854
- const manager = new SandboxManager(options);
2855
- await manager.initialize();
2856
- return manager;
2680
+ };
2857
2681
  }
2858
2682
 
2859
2683
  // src/index.ts
@@ -2890,22 +2714,20 @@ export {
2890
2714
  createUninstallCommand,
2891
2715
  createTscCommand,
2892
2716
  createSharedResources,
2893
- createSandboxManager,
2894
2717
  createSandbox,
2895
2718
  createRunCommand,
2896
2719
  createListCommand,
2897
2720
  createInstallCommand,
2898
- createInMemorySandbox,
2899
- createInMemoryFs,
2721
+ createFilesystem,
2900
2722
  createDefaultCommands,
2723
+ createBuilder,
2901
2724
  createBuildCommand,
2902
2725
  clearSharedModules,
2903
2726
  clearDefaultResources,
2904
2727
  bundleToUrl,
2905
2728
  bundleAndImport,
2906
2729
  bundle,
2907
- SandboxManager,
2908
2730
  ModuleLoadError,
2909
- IndexedDbFs,
2731
+ Filesystem,
2910
2732
  ExportNotFoundError
2911
2733
  };