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
package/README.md ADDED
@@ -0,0 +1,616 @@
1
+ # Sandlot
2
+
3
+ A browser-based TypeScript sandbox for AI agent-driven code generation.
4
+
5
+ San dy provides a complete in-browser development environment with a virtual filesystem, TypeScript type checking, esbuild bundling, and npm package management—all without a server.
6
+
7
+ ## Features
8
+
9
+ - **Virtual Filesystem** - In-memory or IndexedDB-backed file storage
10
+ - **TypeScript Type Checking** - Full type checking
11
+ - **esbuild Bundling** - Fast bundling with automatic npm import handling via esbuild-wasm
12
+ - **Package Management** - Install npm packages via esm.sh CDN
13
+ - **Bash Shell** - Familiar command interface (`tsc`, `build`, `install`, etc.) via just-bash
14
+ - **React Integration** - Share your React instance with dynamic components
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install sandlot
20
+ # or
21
+ bun add sandlot
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ ```typescript
27
+ import { createSandbox, loadModule } from "sandlot";
28
+
29
+ // Create a sandbox
30
+ const sandbox = await createSandbox({
31
+ fsOptions: {
32
+ initialFiles: {
33
+ "/tsconfig.json": JSON.stringify({
34
+ compilerOptions: { target: "ES2020", module: "ESNext", strict: true },
35
+ }),
36
+ },
37
+ },
38
+ });
39
+
40
+ // Write code
41
+ await sandbox.fs.writeFile(
42
+ "/src/index.ts",
43
+ `export function greet(name: string) {
44
+ return \`Hello, \${name}!\`;
45
+ }`,
46
+ );
47
+
48
+ // Capture build result via callback
49
+ let bundle: BundleResult | null = null;
50
+ const unsubscribe = sandbox.onBuild((result) => {
51
+ bundle = result;
52
+ });
53
+
54
+ // Build
55
+ const result = await sandbox.bash.exec("build /src/index.ts");
56
+ unsubscribe();
57
+
58
+ if (result.exitCode === 0 && bundle) {
59
+ const mod = await loadModule<{ greet: (name: string) => string }>(bundle);
60
+ console.log(mod.greet("World")); // "Hello, World!"
61
+ }
62
+
63
+ sandbox.close();
64
+ ```
65
+
66
+ ## Sandbox API
67
+
68
+ The Sandbox API provides direct access to the filesystem and bash shell.
69
+
70
+ ### Creating a Sandbox
71
+
72
+ ```typescript
73
+ import { createSandbox, createInMemorySandbox } from "sandlot";
74
+
75
+ // Persistent sandbox (IndexedDB-backed)
76
+ const sandbox = await createSandbox({
77
+ fsOptions: {
78
+ dbName: "my-project",
79
+ initialFiles: {
80
+ "/src/index.ts": "export const x = 1;",
81
+ },
82
+ },
83
+ });
84
+
85
+ // In-memory sandbox (no persistence)
86
+ const memSandbox = await createInMemorySandbox({
87
+ initialFiles: {
88
+ "/src/index.ts": "export const x = 1;",
89
+ },
90
+ });
91
+ ```
92
+
93
+ ### Using Bash Commands
94
+
95
+ ```typescript
96
+ // Write files using bash
97
+ await sandbox.bash.exec('echo "export const x = 1;" > /src/index.ts');
98
+
99
+ // Type check (entry point required)
100
+ const tscResult = await sandbox.bash.exec("tsc /src/index.ts");
101
+ console.log(tscResult.stdout);
102
+
103
+ // Build (entry point required) - capture result via onBuild callback
104
+ let bundleResult: BundleResult | null = null;
105
+ const unsubscribe = sandbox.onBuild((result) => {
106
+ bundleResult = result;
107
+ });
108
+
109
+ const buildCmd = await sandbox.bash.exec("build /src/index.ts");
110
+ if (buildCmd.exitCode === 0 && bundleResult) {
111
+ console.log(bundleResult.code); // Compiled JavaScript
112
+ }
113
+ unsubscribe();
114
+
115
+ // Install packages
116
+ await sandbox.bash.exec("install react lodash@4.17.21");
117
+
118
+ // List installed packages
119
+ const listResult = await sandbox.bash.exec("list");
120
+ console.log(listResult.stdout);
121
+
122
+ // Run code (entry point required)
123
+ await sandbox.bash.exec("run /src/script.ts");
124
+ ```
125
+
126
+ ### Build Command Options
127
+
128
+ ```bash
129
+ build <entry> [options]
130
+
131
+ Arguments:
132
+ entry Entry point file (required, e.g., /src/index.ts)
133
+
134
+ Options:
135
+ --skip-typecheck, -s Skip type checking
136
+ --minify, -m Enable minification
137
+ --format, -f <format> Output format: esm (default), iife, cjs
138
+ ```
139
+
140
+ ### Sandbox Events
141
+
142
+ ```typescript
143
+ // Subscribe to build events to capture build results
144
+ let bundleResult: BundleResult | null = null;
145
+ const unsubscribe = sandbox.onBuild((result) => {
146
+ bundleResult = result;
147
+ console.log("Build completed:", result.code.length, "bytes");
148
+ });
149
+
150
+ // Build with entry point (required)
151
+ const buildCmd = await sandbox.bash.exec("build /src/index.ts");
152
+ if (buildCmd.exitCode !== 0) {
153
+ console.error("Build failed:", buildCmd.stderr);
154
+ } else if (bundleResult) {
155
+ console.log(bundleResult.code);
156
+ }
157
+ unsubscribe();
158
+
159
+ // Check for unsaved changes
160
+ if (sandbox.isDirty()) {
161
+ await sandbox.save(); // Persist to IndexedDB
162
+ }
163
+
164
+ // Clean up
165
+ sandbox.close();
166
+ ```
167
+
168
+ > **Note:** The `onBuild` callback is invoked synchronously during successful builds.
169
+ > Use it to capture the `BundleResult` for loading modules.
170
+
171
+ ### Sandbox Manager
172
+
173
+ For managing multiple sandboxes with shared resources:
174
+
175
+ ```typescript
176
+ import { createSandboxManager } from "sandlot";
177
+
178
+ const manager = await createSandboxManager({
179
+ sharedModules: ["react", "react-dom/client"],
180
+ });
181
+
182
+ // Create multiple sandboxes - they share TypeScript libs and bundler
183
+ const sandbox1 = await manager.createSandbox({ id: "agent-1" });
184
+ const sandbox2 = await manager.createSandbox({ id: "agent-2" });
185
+
186
+ // Run operations in parallel
187
+ await Promise.all([sandbox1.bash.exec("build"), sandbox2.bash.exec("build")]);
188
+
189
+ // Save all with unsaved changes
190
+ await manager.saveAll();
191
+
192
+ // Get dirty sandbox IDs
193
+ const unsaved = manager.getDirtySandboxes();
194
+
195
+ // Clean up
196
+ manager.destroyAll();
197
+ ```
198
+
199
+ ### Sandbox Options
200
+
201
+ ```typescript
202
+ interface SandboxOptions {
203
+ // Filesystem configuration
204
+ fsOptions?: {
205
+ dbName?: string; // IndexedDB database name
206
+ initialFiles?: Record<string, string>;
207
+ maxSizeBytes?: number; // Filesystem size limit
208
+ };
209
+
210
+ // Build configuration
211
+ tsconfigPath?: string; // Default: "/tsconfig.json"
212
+
213
+ // Module sharing (see React Integration)
214
+ sharedModules?: string[];
215
+
216
+ // Build callback - use to capture successful build results
217
+ onBuild?: (result: BundleResult) => void | Promise<void>;
218
+
219
+ // Custom bash commands
220
+ customCommands?: Command[];
221
+ }
222
+ ```
223
+
224
+ ---
225
+
226
+ ## React Integration
227
+
228
+ Sandlot solves the "multiple React instances" problem by letting dynamic components use your host application's React.
229
+
230
+ ### Setup
231
+
232
+ ```typescript
233
+ import * as React from "react";
234
+ import * as ReactDOM from "react-dom/client";
235
+ import { registerSharedModules, createSandbox } from "sandlot";
236
+ import { DynamicMount } from "sandlot/react";
237
+
238
+ // Register your React instances (do this once at app startup)
239
+ registerSharedModules({
240
+ react: React,
241
+ "react-dom/client": ReactDOM,
242
+ });
243
+ ```
244
+
245
+ ### Generating React Components
246
+
247
+ ```typescript
248
+ import { createSandbox, loadModule, BundleResult } from "sandlot";
249
+
250
+ const sandbox = await createSandbox({
251
+ fsOptions: {
252
+ initialFiles: {
253
+ "/tsconfig.json": JSON.stringify({
254
+ compilerOptions: {
255
+ target: "ES2020",
256
+ module: "ESNext",
257
+ jsx: "react-jsx",
258
+ strict: true,
259
+ },
260
+ }),
261
+ },
262
+ },
263
+ sharedModules: ["react", "react-dom/client"],
264
+ });
265
+
266
+ // Install React types for TypeScript compilation
267
+ // (sharedModules provides runtime React, but types are still needed)
268
+ await sandbox.bash.exec("install react react-dom");
269
+
270
+ await sandbox.fs.writeFile(
271
+ "/src/index.tsx",
272
+ `
273
+ import React, { useState } from "react";
274
+ import { createRoot } from "react-dom/client";
275
+
276
+ function Counter() {
277
+ const [count, setCount] = useState(0);
278
+ return (
279
+ <button onClick={() => setCount(c => c + 1)}>
280
+ Count: {count}
281
+ </button>
282
+ );
283
+ }
284
+
285
+ export function render(container: HTMLElement) {
286
+ const root = createRoot(container);
287
+ root.render(<Counter />);
288
+ return () => root.unmount();
289
+ }
290
+ `,
291
+ );
292
+
293
+ // Capture build result
294
+ let bundle: BundleResult | null = null;
295
+ const unsubscribe = sandbox.onBuild((result) => {
296
+ bundle = result;
297
+ });
298
+
299
+ const buildResult = await sandbox.bash.exec("build /src/index.tsx");
300
+ unsubscribe();
301
+
302
+ if (buildResult.exitCode === 0 && bundle) {
303
+ // Load the module and call render
304
+ const mod = await loadModule<{ render: (el: HTMLElement) => () => void }>(
305
+ bundle,
306
+ );
307
+ // mod.render is ready to use
308
+ }
309
+
310
+ sandbox.close();
311
+ ```
312
+
313
+ ### Rendering Dynamic Components
314
+
315
+ Use `DynamicMount` to render the generated component:
316
+
317
+ ```tsx
318
+ import { DynamicMount } from "sandlot/react";
319
+
320
+ function App() {
321
+ const [module, setModule] = useState(null);
322
+
323
+ const generate = async () => {
324
+ // ... build with sandbox, capture bundle via onBuild ...
325
+ setModule(bundle);
326
+ };
327
+
328
+ return (
329
+ <div>
330
+ <button onClick={generate}>Generate</button>
331
+ <DynamicMount
332
+ module={module}
333
+ props={{ name: "World" }}
334
+ fallback={<div>Click to generate...</div>}
335
+ onMount={() => console.log("Mounted")}
336
+ onError={(err) => console.error(err)}
337
+ />
338
+ </div>
339
+ );
340
+ }
341
+ ```
342
+
343
+ ### Using the Hook
344
+
345
+ For more control, use `useDynamicComponent`:
346
+
347
+ ```tsx
348
+ import { useDynamicComponent } from "sandlot/react";
349
+
350
+ function App() {
351
+ const { containerRef, isMounted, error, unmount } = useDynamicComponent(
352
+ module,
353
+ { name: "World" },
354
+ );
355
+
356
+ return (
357
+ <div>
358
+ <div ref={containerRef} />
359
+ {isMounted && <button onClick={unmount}>Remove</button>}
360
+ {error && <div>Error: {error.message}</div>}
361
+ </div>
362
+ );
363
+ }
364
+ ```
365
+
366
+ ### Render Function Pattern
367
+
368
+ Dynamic components must export a `render` function:
369
+
370
+ ```typescript
371
+ export function render(container: HTMLElement, props?: MyProps) {
372
+ const root = createRoot(container);
373
+ root.render(<MyComponent {...props} />);
374
+ return () => root.unmount(); // Cleanup function
375
+ }
376
+ ```
377
+
378
+ ---
379
+
380
+ ## Package Management
381
+
382
+ Sandlot installs packages via esm.sh CDN and fetches TypeScript type definitions.
383
+
384
+ ### Installing Packages
385
+
386
+ ```typescript
387
+ // Via bash
388
+ await sandbox.bash.exec("install lodash date-fns@3.0.0");
389
+
390
+ // Via direct API
391
+ import { installPackage } from "sandlot";
392
+ await installPackage(fs, "lodash@4.17.21");
393
+ ```
394
+
395
+ ### How It Works
396
+
397
+ 1. Resolves package version from esm.sh CDN
398
+ 2. Fetches TypeScript type definitions
399
+ 3. Stores types in `/node_modules/<package>/`
400
+ 4. Updates `/package.json` with installed version
401
+ 5. At bundle time, imports are rewritten to esm.sh URLs
402
+
403
+ ### Package Commands
404
+
405
+ ```bash
406
+ install <package>[@version] [...] # Install packages
407
+ uninstall <package> [...] # Remove packages
408
+ list # Show installed packages
409
+ ```
410
+
411
+ ---
412
+
413
+ ## Loading Modules
414
+
415
+ After building, use loaders to access the compiled code:
416
+
417
+ ```typescript
418
+ import { loadModule, loadExport, loadDefault } from "sandlot";
419
+
420
+ // Load all exports
421
+ const mod = await loadModule<{ add: Function; multiply: Function }>(
422
+ result.bundle,
423
+ );
424
+
425
+ // Load a specific export
426
+ const add = await loadExport<(a: number, b: number) => number>(
427
+ result.bundle,
428
+ "add",
429
+ );
430
+
431
+ // Load default export
432
+ const Calculator = await loadDefault<typeof Calculator>(result.bundle);
433
+
434
+ // Check what's exported
435
+ const names = await getExportNames(result.bundle); // ["add", "multiply", "default"]
436
+ const hasAdd = await hasExport(result.bundle, "add"); // true
437
+ ```
438
+
439
+ ---
440
+
441
+ ## Advanced Usage
442
+
443
+ ### Direct Bundler Access
444
+
445
+ ```typescript
446
+ import { bundle, initBundler } from "sandlot";
447
+
448
+ // Pre-warm the bundler (optional)
449
+ await initBundler();
450
+
451
+ const result = await bundle({
452
+ fs: myFilesystem,
453
+ entryPoint: "/src/index.ts",
454
+ format: "esm", // "esm" | "iife" | "cjs"
455
+ minify: false,
456
+ sourcemap: false,
457
+ sharedModules: ["react"],
458
+ npmImports: "cdn", // "cdn" | "external" | "bundle"
459
+ });
460
+
461
+ console.log(result.code);
462
+ console.log(result.warnings);
463
+ console.log(result.includedFiles);
464
+ ```
465
+
466
+ ### Direct Type Checking
467
+
468
+ ```typescript
469
+ import {
470
+ typecheck,
471
+ formatDiagnostics,
472
+ formatDiagnosticsForAgent,
473
+ } from "sandlot";
474
+
475
+ const result = await typecheck({
476
+ fs: myFilesystem,
477
+ entryPoint: "/src/index.ts",
478
+ tsconfigPath: "/tsconfig.json",
479
+ libFiles: myLibFiles, // Map of lib name to content
480
+ });
481
+
482
+ if (result.hasErrors) {
483
+ // Human-readable format
484
+ console.log(formatDiagnostics(result.diagnostics));
485
+
486
+ // Agent-friendly format
487
+ console.log(formatDiagnosticsForAgent(result.diagnostics));
488
+ }
489
+ ```
490
+
491
+ ### Custom Filesystem
492
+
493
+ ```typescript
494
+ import { createIndexedDbFs, IndexedDbFs } from "sandlot";
495
+
496
+ // Persistent filesystem
497
+ const fs = await createIndexedDbFs({
498
+ dbName: "my-project",
499
+ initialFiles: { "/README.md": "# Hello" },
500
+ maxSizeBytes: 50 * 1024 * 1024, // 50MB limit
501
+ });
502
+
503
+ // In-memory filesystem
504
+ const memFs = IndexedDbFs.createInMemory({
505
+ initialFiles: { "/README.md": "# Hello" },
506
+ });
507
+
508
+ // File operations
509
+ await fs.writeFile("/src/index.ts", "export const x = 1;");
510
+ const content = await fs.readFile("/src/index.ts");
511
+ const exists = await fs.exists("/src/index.ts");
512
+ await fs.mkdir("/src/utils", { recursive: true });
513
+ await fs.rm("/src/old.ts");
514
+
515
+ // Persistence
516
+ if (fs.isDirty()) {
517
+ await fs.save();
518
+ }
519
+
520
+ fs.close();
521
+ ```
522
+
523
+ ### Shared Resources
524
+
525
+ Share TypeScript libs and bundler across multiple sandboxes:
526
+
527
+ ```typescript
528
+ import { createSharedResources, getDefaultResources } from "sandlot";
529
+
530
+ // Create custom resources
531
+ const resources = await createSharedResources({
532
+ libs: ["ES2022", "DOM", "DOM.Iterable"],
533
+ });
534
+
535
+ // Use with sandbox
536
+ const sandbox = await createSandbox({
537
+ resources,
538
+ });
539
+
540
+ // Or use the default singleton
541
+ const defaultResources = await getDefaultResources();
542
+ ```
543
+
544
+ ---
545
+
546
+ ## TypeScript Configuration
547
+
548
+ Sandlot respects your `tsconfig.json`:
549
+
550
+ ```json
551
+ {
552
+ "compilerOptions": {
553
+ "target": "ES2020",
554
+ "module": "ESNext",
555
+ "moduleResolution": "bundler",
556
+ "jsx": "react-jsx",
557
+ "strict": true,
558
+ "esModuleInterop": true,
559
+ "skipLibCheck": true,
560
+ "lib": ["ES2020", "DOM", "DOM.Iterable"]
561
+ }
562
+ }
563
+ ```
564
+
565
+ Supported options:
566
+
567
+ - `target`: ES5 through ESNext
568
+ - `module`: CommonJS, ES2015, ES2020, ESNext, Node16, NodeNext
569
+ - `moduleResolution`: Classic, Node, Node16, NodeNext, Bundler
570
+ - `jsx`: preserve, react, react-jsx, react-jsxdev, react-native
571
+ - `strict`, `noImplicitAny`, `strictNullChecks`
572
+ - `esModuleInterop`, `allowJs`, `resolveJsonModule`
573
+ - `lib`: Array of lib names
574
+
575
+ ---
576
+
577
+ ## API Reference
578
+
579
+ ### Exports from `sandlot`
580
+
581
+ ```typescript
582
+ // Sandbox API
583
+ export { createSandbox, createInMemorySandbox } from "sandlot";
584
+ export { SandboxManager, createSandboxManager } from "sandlot";
585
+
586
+ // Module Loading
587
+ export { loadModule, loadExport, loadDefault } from "sandlot";
588
+ export { getExportNames, hasExport } from "sandlot";
589
+
590
+ // Shared Modules
591
+ export { registerSharedModules, clearSharedModules } from "sandlot";
592
+
593
+ // Types
594
+ export type { Sandbox, SandboxOptions } from "sandlot";
595
+ export type { BundleResult, TypecheckResult } from "sandlot";
596
+ ```
597
+
598
+ ### Exports from `sandlot/react`
599
+
600
+ ```typescript
601
+ export { DynamicMount, useDynamicComponent } from "sandlot/react";
602
+ export { generateRenderFunction, REACT_RENDER_TEMPLATE } from "sandlot/react";
603
+ export type { DynamicRenderModule, DynamicMountProps } from "sandlot/react";
604
+ ```
605
+
606
+ ---
607
+
608
+ ## Requirements
609
+
610
+ - Modern browser with ES2020 support
611
+ - IndexedDB (for persistent filesystems)
612
+ - WebAssembly (for esbuild)
613
+
614
+ ## License
615
+
616
+ MIT