sandlot 0.1.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 (47) hide show
  1. package/README.md +616 -0
  2. package/dist/bundler.d.ts +148 -0
  3. package/dist/bundler.d.ts.map +1 -0
  4. package/dist/commands.d.ts +179 -0
  5. package/dist/commands.d.ts.map +1 -0
  6. package/dist/fs.d.ts +125 -0
  7. package/dist/fs.d.ts.map +1 -0
  8. package/dist/index.d.ts +16 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +2920 -0
  11. package/dist/internal.d.ts +74 -0
  12. package/dist/internal.d.ts.map +1 -0
  13. package/dist/internal.js +1897 -0
  14. package/dist/loader.d.ts +164 -0
  15. package/dist/loader.d.ts.map +1 -0
  16. package/dist/packages.d.ts +199 -0
  17. package/dist/packages.d.ts.map +1 -0
  18. package/dist/react.d.ts +159 -0
  19. package/dist/react.d.ts.map +1 -0
  20. package/dist/react.js +149 -0
  21. package/dist/sandbox-manager.d.ts +249 -0
  22. package/dist/sandbox-manager.d.ts.map +1 -0
  23. package/dist/sandbox.d.ts +193 -0
  24. package/dist/sandbox.d.ts.map +1 -0
  25. package/dist/shared-modules.d.ts +129 -0
  26. package/dist/shared-modules.d.ts.map +1 -0
  27. package/dist/shared-resources.d.ts +105 -0
  28. package/dist/shared-resources.d.ts.map +1 -0
  29. package/dist/ts-libs.d.ts +98 -0
  30. package/dist/ts-libs.d.ts.map +1 -0
  31. package/dist/typechecker.d.ts +127 -0
  32. package/dist/typechecker.d.ts.map +1 -0
  33. package/package.json +64 -0
  34. package/src/bundler.ts +513 -0
  35. package/src/commands.ts +733 -0
  36. package/src/fs.ts +935 -0
  37. package/src/index.ts +149 -0
  38. package/src/internal.ts +116 -0
  39. package/src/loader.ts +229 -0
  40. package/src/packages.ts +936 -0
  41. package/src/react.tsx +331 -0
  42. package/src/sandbox-manager.ts +490 -0
  43. package/src/sandbox.ts +402 -0
  44. package/src/shared-modules.ts +210 -0
  45. package/src/shared-resources.ts +169 -0
  46. package/src/ts-libs.ts +320 -0
  47. package/src/typechecker.ts +635 -0
@@ -0,0 +1,490 @@
1
+ /**
2
+ * SandboxManager - Manages shared resources for multiple concurrent sandboxes.
3
+ *
4
+ * When running many sandboxes concurrently (e.g., for multiple AI agents),
5
+ * this manager ensures heavy resources are loaded once and shared:
6
+ *
7
+ * - TypeScript lib files (~5MB) - loaded once, shared across all sandboxes
8
+ * - esbuild WASM (~10MB) - already singleton, but pre-initialized here
9
+ *
10
+ * Usage:
11
+ * ```ts
12
+ * const manager = await createSandboxManager();
13
+ *
14
+ * // Create multiple sandboxes - all share the same libs and bundler
15
+ * const sandbox1 = await manager.createSandbox({ ... });
16
+ * const sandbox2 = await manager.createSandbox({ ... });
17
+ *
18
+ * // ... use sandboxes ...
19
+ *
20
+ * // Clean up
21
+ * manager.destroyAll();
22
+ * ```
23
+ */
24
+
25
+ import { Bash, defineCommand } from "just-bash/browser";
26
+ import { IndexedDbFs, type IndexedDbFsOptions } from "./fs";
27
+ import { type BundleResult } from "./bundler";
28
+ import { getDefaultBrowserLibs } from "./ts-libs";
29
+ import {
30
+ createSharedResources,
31
+ type SharedResources,
32
+ type SharedResourcesOptions,
33
+ } from "./shared-resources";
34
+ import { createDefaultCommands, type CommandDeps } from "./commands";
35
+ import type { Sandbox } from "./sandbox";
36
+
37
+ /**
38
+ * Simple typed event emitter for build results.
39
+ * Caches the last result so waitFor() can be called after build completes.
40
+ */
41
+ class BuildEmitter {
42
+ private listeners = new Set<(result: BundleResult) => void | Promise<void>>();
43
+ private lastResult: BundleResult | null = null;
44
+
45
+ /**
46
+ * Emit a build result to all listeners and cache it
47
+ */
48
+ emit = async (result: BundleResult): Promise<void> => {
49
+ this.lastResult = result;
50
+ const promises: Promise<void>[] = [];
51
+ for (const listener of this.listeners) {
52
+ const ret = listener(result);
53
+ if (ret instanceof Promise) {
54
+ promises.push(ret);
55
+ }
56
+ }
57
+ await Promise.all(promises);
58
+ };
59
+
60
+ /**
61
+ * Subscribe to build events. Returns an unsubscribe function.
62
+ */
63
+ on(callback: (result: BundleResult) => void | Promise<void>): () => void {
64
+ this.listeners.add(callback);
65
+ return () => {
66
+ this.listeners.delete(callback);
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Get the last build result, or wait for the next one if none exists.
72
+ * Clears the cached result after returning, so subsequent calls wait for new builds.
73
+ */
74
+ waitFor(): Promise<BundleResult> {
75
+ if (this.lastResult) {
76
+ const result = this.lastResult;
77
+ this.lastResult = null;
78
+ return Promise.resolve(result);
79
+ }
80
+ return new Promise((resolve) => {
81
+ const unsub = this.on((result) => {
82
+ unsub();
83
+ this.lastResult = null;
84
+ resolve(result);
85
+ });
86
+ });
87
+ }
88
+ }
89
+
90
+ // Re-export for convenience
91
+ export type { BundleResult } from "./bundler";
92
+ export type { TypecheckResult } from "./typechecker";
93
+ export type { SharedResources, TypesCache } from "./shared-resources";
94
+ export type { PackageManifest, InstallResult } from "./packages";
95
+ export { installPackage, uninstallPackage, listPackages, getPackageManifest } from "./packages";
96
+ export { InMemoryTypesCache } from "./shared-resources";
97
+
98
+ // Loader utilities
99
+ export {
100
+ loadModule,
101
+ loadExport,
102
+ loadDefault,
103
+ getExportNames,
104
+ hasExport,
105
+ createModuleUrl,
106
+ revokeModuleUrl,
107
+ ModuleLoadError,
108
+ ExportNotFoundError,
109
+ } from "./loader";
110
+
111
+ /**
112
+ * Options for creating a sandbox via the manager
113
+ */
114
+ export interface ManagedSandboxOptions {
115
+ /**
116
+ * Unique identifier for this sandbox.
117
+ *
118
+ * Also used as the IndexedDB database name if `fsOptions.dbName` is not provided
119
+ * and `inMemory` is false. If both are provided, `fsOptions.dbName` takes precedence.
120
+ *
121
+ * @default Auto-generated as "sandbox-1", "sandbox-2", etc.
122
+ */
123
+ id?: string;
124
+
125
+ /**
126
+ * Options for the IndexedDB filesystem.
127
+ *
128
+ * Note: `fsOptions.dbName` takes precedence over `id` for the database name.
129
+ * If neither is provided, the auto-generated `id` is used.
130
+ */
131
+ fsOptions?: IndexedDbFsOptions;
132
+
133
+ /**
134
+ * Initial files to populate the filesystem with
135
+ */
136
+ initialFiles?: Record<string, string>;
137
+
138
+ /**
139
+ * Path to tsconfig.json in the virtual filesystem.
140
+ * Default: "/tsconfig.json"
141
+ */
142
+ tsconfigPath?: string;
143
+
144
+ /**
145
+ * Callback invoked when a build succeeds.
146
+ * Receives the bundle result with the compiled code.
147
+ */
148
+ onBuild?: (result: BundleResult) => void | Promise<void>;
149
+
150
+ /**
151
+ * Additional custom commands to add to the bash environment
152
+ */
153
+ customCommands?: ReturnType<typeof defineCommand>[];
154
+
155
+ /**
156
+ * If true, use in-memory filesystem only (no IndexedDB persistence).
157
+ * Default: true
158
+ */
159
+ inMemory?: boolean;
160
+
161
+ /**
162
+ * Module IDs that should be resolved from the host's SharedModuleRegistry
163
+ * instead of esm.sh CDN. Overrides manager-level sharedModules for this sandbox.
164
+ *
165
+ * @see SandboxOptions.sharedModules for full documentation
166
+ */
167
+ sharedModules?: string[];
168
+ }
169
+
170
+ /**
171
+ * A sandbox instance managed by the SandboxManager.
172
+ *
173
+ * Extends the base Sandbox interface with an `id` property for
174
+ * identification within the manager. This ensures ManagedSandbox
175
+ * has full feature parity with Sandbox.
176
+ */
177
+ export interface ManagedSandbox extends Sandbox {
178
+ /**
179
+ * Unique identifier for this sandbox within the manager.
180
+ */
181
+ id: string;
182
+ }
183
+
184
+ /**
185
+ * Statistics about the sandbox manager
186
+ */
187
+ export interface SandboxManagerStats {
188
+ /**
189
+ * Whether shared resources have been initialized
190
+ */
191
+ initialized: boolean;
192
+
193
+ /**
194
+ * Number of currently active sandboxes
195
+ */
196
+ activeSandboxes: number;
197
+
198
+ /**
199
+ * Number of TypeScript lib files loaded
200
+ */
201
+ libFilesCount: number;
202
+
203
+ /**
204
+ * IDs of active sandboxes
205
+ */
206
+ sandboxIds: string[];
207
+ }
208
+
209
+ /**
210
+ * Options for creating a SandboxManager
211
+ */
212
+ export interface SandboxManagerOptions extends SharedResourcesOptions {
213
+ /**
214
+ * TypeScript libs to load. Defaults to browser libs (ES2020 + DOM).
215
+ */
216
+ libs?: string[];
217
+
218
+ /**
219
+ * Default shared modules for all sandboxes created by this manager.
220
+ * Individual sandboxes can override with their own sharedModules option.
221
+ *
222
+ * Module IDs that should be resolved from the host's SharedModuleRegistry
223
+ * instead of esm.sh CDN. The host must have registered these modules
224
+ * using `registerSharedModules()` before loading dynamic code.
225
+ *
226
+ * @example
227
+ * ```ts
228
+ * const manager = await createSandboxManager({
229
+ * sharedModules: ['react', 'react-dom/client'],
230
+ * });
231
+ * ```
232
+ */
233
+ sharedModules?: string[];
234
+ }
235
+
236
+ /**
237
+ * Manager for creating and managing multiple sandboxes with shared resources.
238
+ */
239
+ export class SandboxManager {
240
+ private resources: SharedResources | null = null;
241
+ private sandboxes: Map<string, ManagedSandbox> = new Map();
242
+ private initialized = false;
243
+ private initPromise: Promise<void> | null = null;
244
+ private nextId = 1;
245
+ private options: SandboxManagerOptions;
246
+
247
+ constructor(options: SandboxManagerOptions = {}) {
248
+ this.options = {
249
+ libs: options.libs ?? getDefaultBrowserLibs(),
250
+ ...options,
251
+ };
252
+ }
253
+
254
+ /**
255
+ * Initialize shared resources (libs and bundler).
256
+ * Called automatically on first sandbox creation, but can be called
257
+ * explicitly to pre-warm.
258
+ */
259
+ async initialize(): Promise<void> {
260
+ if (this.initialized) return;
261
+
262
+ if (this.initPromise) {
263
+ await this.initPromise;
264
+ return;
265
+ }
266
+
267
+ this.initPromise = this.doInitialize();
268
+ await this.initPromise;
269
+ this.initialized = true;
270
+ }
271
+
272
+ private async doInitialize(): Promise<void> {
273
+ this.resources = await createSharedResources({
274
+ libs: this.options.libs,
275
+ skipLibs: this.options.skipLibs,
276
+ skipBundler: this.options.skipBundler,
277
+ });
278
+ }
279
+
280
+ /**
281
+ * Create a new managed sandbox.
282
+ * Shares TypeScript libs and bundler with all other sandboxes.
283
+ */
284
+ async createSandbox(options: ManagedSandboxOptions = {}): Promise<ManagedSandbox> {
285
+ // Ensure resources are initialized
286
+ await this.initialize();
287
+
288
+ const {
289
+ id = `sandbox-${this.nextId++}`,
290
+ fsOptions = {},
291
+ initialFiles,
292
+ tsconfigPath = "/tsconfig.json",
293
+ onBuild,
294
+ customCommands = [],
295
+ inMemory = true,
296
+ sharedModules = this.options.sharedModules,
297
+ } = options;
298
+
299
+ // Create filesystem
300
+ let fs: IndexedDbFs;
301
+ if (inMemory) {
302
+ fs = IndexedDbFs.createInMemory({
303
+ initialFiles,
304
+ maxSizeBytes: fsOptions.maxSizeBytes,
305
+ });
306
+ } else {
307
+ fs = await IndexedDbFs.create({
308
+ dbName: fsOptions.dbName ?? id,
309
+ initialFiles,
310
+ maxSizeBytes: fsOptions.maxSizeBytes,
311
+ });
312
+ }
313
+
314
+ // Create build event emitter
315
+ const buildEmitter = new BuildEmitter();
316
+
317
+ // If a callback was provided in options, subscribe it
318
+ if (onBuild) {
319
+ buildEmitter.on(onBuild);
320
+ }
321
+
322
+ // Create commands using shared resources
323
+ const commandDeps: CommandDeps = {
324
+ fs,
325
+ libFiles: this.resources!.libFiles,
326
+ tsconfigPath,
327
+ onBuild: buildEmitter.emit,
328
+ typesCache: this.resources!.typesCache,
329
+ sharedModules,
330
+ };
331
+ const defaultCommands = createDefaultCommands(commandDeps);
332
+
333
+ // Create bash environment
334
+ const bash = new Bash({
335
+ fs,
336
+ cwd: "/",
337
+ customCommands: [...defaultCommands, ...customCommands],
338
+ });
339
+
340
+ const sandbox: ManagedSandbox = {
341
+ id,
342
+ fs,
343
+ bash,
344
+ isDirty: () => fs.isDirty(),
345
+ save: () => fs.save(),
346
+ close: () => {
347
+ fs.close();
348
+ this.sandboxes.delete(id);
349
+ },
350
+ onBuild: (callback) => buildEmitter.on(callback),
351
+ };
352
+
353
+ this.sandboxes.set(id, sandbox);
354
+ return sandbox;
355
+ }
356
+
357
+ /**
358
+ * Get a sandbox by ID
359
+ */
360
+ getSandbox(id: string): ManagedSandbox | undefined {
361
+ return this.sandboxes.get(id);
362
+ }
363
+
364
+ /**
365
+ * Get all active sandboxes
366
+ */
367
+ getAllSandboxes(): ManagedSandbox[] {
368
+ return Array.from(this.sandboxes.values());
369
+ }
370
+
371
+ /**
372
+ * Close a specific sandbox
373
+ */
374
+ closeSandbox(id: string): boolean {
375
+ const sandbox = this.sandboxes.get(id);
376
+ if (sandbox) {
377
+ sandbox.close();
378
+ return true;
379
+ }
380
+ return false;
381
+ }
382
+
383
+ /**
384
+ * Close all sandboxes and release resources
385
+ */
386
+ destroyAll(): void {
387
+ for (const sandbox of this.sandboxes.values()) {
388
+ sandbox.fs.close();
389
+ }
390
+ this.sandboxes.clear();
391
+ }
392
+
393
+ /**
394
+ * Save all sandboxes that have unsaved changes.
395
+ *
396
+ * @returns Map of sandbox ID to save result (true if saved, false if nothing to save)
397
+ *
398
+ * @example
399
+ * ```ts
400
+ * const results = await manager.saveAll();
401
+ * for (const [id, saved] of results) {
402
+ * console.log(`${id}: ${saved ? 'saved' : 'no changes'}`);
403
+ * }
404
+ * ```
405
+ */
406
+ async saveAll(): Promise<Map<string, boolean>> {
407
+ const results = new Map<string, boolean>();
408
+ for (const [id, sandbox] of this.sandboxes) {
409
+ const saved = await sandbox.save();
410
+ results.set(id, saved);
411
+ }
412
+ return results;
413
+ }
414
+
415
+ /**
416
+ * Get IDs of sandboxes with unsaved changes.
417
+ *
418
+ * @example
419
+ * ```ts
420
+ * const dirtyIds = manager.getDirtySandboxes();
421
+ * if (dirtyIds.length > 0) {
422
+ * console.log('Unsaved sandboxes:', dirtyIds.join(', '));
423
+ * }
424
+ * ```
425
+ */
426
+ getDirtySandboxes(): string[] {
427
+ return Array.from(this.sandboxes.entries())
428
+ .filter(([_, sandbox]) => sandbox.isDirty())
429
+ .map(([id]) => id);
430
+ }
431
+
432
+ /**
433
+ * Get statistics about the manager
434
+ */
435
+ getStats(): SandboxManagerStats {
436
+ return {
437
+ initialized: this.initialized,
438
+ activeSandboxes: this.sandboxes.size,
439
+ libFilesCount: this.resources?.libFiles.size ?? 0,
440
+ sandboxIds: Array.from(this.sandboxes.keys()),
441
+ };
442
+ }
443
+
444
+ /**
445
+ * Get the shared resources (for advanced use cases)
446
+ */
447
+ getResources(): SharedResources | null {
448
+ return this.resources;
449
+ }
450
+
451
+ /**
452
+ * Get the shared lib files (for advanced use cases)
453
+ * @deprecated Use getResources().libFiles instead
454
+ */
455
+ getLibFiles(): Map<string, string> {
456
+ return this.resources?.libFiles ?? new Map();
457
+ }
458
+ }
459
+
460
+ /**
461
+ * Create a new SandboxManager instance.
462
+ *
463
+ * @example
464
+ * ```ts
465
+ * const manager = await createSandboxManager();
466
+ *
467
+ * // Create multiple sandboxes concurrently
468
+ * const [sandbox1, sandbox2] = await Promise.all([
469
+ * manager.createSandbox({ id: "agent-1", initialFiles: { ... } }),
470
+ * manager.createSandbox({ id: "agent-2", initialFiles: { ... } }),
471
+ * ]);
472
+ *
473
+ * // Run operations in parallel
474
+ * const [result1, result2] = await Promise.all([
475
+ * sandbox1.bash.exec("build"),
476
+ * sandbox2.bash.exec("build"),
477
+ * ]);
478
+ *
479
+ * // Clean up
480
+ * manager.destroyAll();
481
+ * ```
482
+ */
483
+ export async function createSandboxManager(
484
+ options: SandboxManagerOptions = {}
485
+ ): Promise<SandboxManager> {
486
+ const manager = new SandboxManager(options);
487
+ await manager.initialize();
488
+ return manager;
489
+ }
490
+