sandlot 0.1.4 → 0.2.1

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 (115) hide show
  1. package/dist/browser/bundler.d.ts +68 -0
  2. package/dist/browser/bundler.d.ts.map +1 -0
  3. package/dist/browser/executor.d.ts +46 -0
  4. package/dist/browser/executor.d.ts.map +1 -0
  5. package/dist/browser/index.d.ts +9 -0
  6. package/dist/browser/index.d.ts.map +1 -0
  7. package/dist/browser/index.js +2690 -0
  8. package/dist/browser/preset.d.ts +63 -0
  9. package/dist/browser/preset.d.ts.map +1 -0
  10. package/dist/commands/index.d.ts +20 -11
  11. package/dist/commands/index.d.ts.map +1 -1
  12. package/dist/commands/types.d.ts +37 -130
  13. package/dist/commands/types.d.ts.map +1 -1
  14. package/dist/core/bundler-utils.d.ts +142 -0
  15. package/dist/core/bundler-utils.d.ts.map +1 -0
  16. package/dist/core/esm-types-resolver.d.ts +125 -0
  17. package/dist/core/esm-types-resolver.d.ts.map +1 -0
  18. package/dist/core/executor.d.ts +35 -0
  19. package/dist/core/executor.d.ts.map +1 -0
  20. package/dist/{fs.d.ts → core/fs.d.ts} +27 -29
  21. package/dist/core/fs.d.ts.map +1 -0
  22. package/dist/core/sandbox.d.ts +30 -0
  23. package/dist/core/sandbox.d.ts.map +1 -0
  24. package/dist/core/sandlot.d.ts +30 -0
  25. package/dist/core/sandlot.d.ts.map +1 -0
  26. package/dist/core/shared-module-registry.d.ts +46 -0
  27. package/dist/core/shared-module-registry.d.ts.map +1 -0
  28. package/dist/core/typechecker.d.ts +60 -0
  29. package/dist/core/typechecker.d.ts.map +1 -0
  30. package/dist/index.d.ts +11 -16
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +1398 -2010
  33. package/dist/node/bundler.d.ts +48 -0
  34. package/dist/node/bundler.d.ts.map +1 -0
  35. package/dist/node/executor.d.ts +48 -0
  36. package/dist/node/executor.d.ts.map +1 -0
  37. package/dist/node/index.d.ts +9 -0
  38. package/dist/node/index.d.ts.map +1 -0
  39. package/dist/node/index.js +2644 -0
  40. package/dist/node/preset.d.ts +62 -0
  41. package/dist/node/preset.d.ts.map +1 -0
  42. package/dist/types.d.ts +528 -0
  43. package/dist/types.d.ts.map +1 -0
  44. package/package.json +16 -6
  45. package/src/browser/bundler.ts +294 -0
  46. package/src/browser/executor.ts +71 -0
  47. package/src/browser/index.ts +57 -0
  48. package/src/browser/preset.ts +179 -0
  49. package/src/commands/index.ts +498 -37
  50. package/src/commands/types.ts +117 -145
  51. package/src/core/bundler-utils.ts +630 -0
  52. package/src/core/esm-types-resolver.ts +432 -0
  53. package/src/core/executor.ts +161 -0
  54. package/src/{fs.ts → core/fs.ts} +59 -37
  55. package/src/core/sandbox.ts +624 -0
  56. package/src/core/sandlot.ts +77 -0
  57. package/src/core/shared-module-registry.ts +138 -0
  58. package/src/core/typechecker.ts +609 -0
  59. package/src/index.ts +106 -139
  60. package/src/node/bundler.ts +194 -0
  61. package/src/node/executor.ts +87 -0
  62. package/src/node/index.ts +39 -0
  63. package/src/node/preset.ts +178 -0
  64. package/src/types.ts +672 -0
  65. package/README.md +0 -243
  66. package/dist/build-emitter.d.ts +0 -47
  67. package/dist/build-emitter.d.ts.map +0 -1
  68. package/dist/builder.d.ts +0 -370
  69. package/dist/builder.d.ts.map +0 -1
  70. package/dist/bundler.d.ts +0 -152
  71. package/dist/bundler.d.ts.map +0 -1
  72. package/dist/commands/compile.d.ts +0 -13
  73. package/dist/commands/compile.d.ts.map +0 -1
  74. package/dist/commands/packages.d.ts +0 -17
  75. package/dist/commands/packages.d.ts.map +0 -1
  76. package/dist/commands/run.d.ts +0 -40
  77. package/dist/commands/run.d.ts.map +0 -1
  78. package/dist/commands.d.ts +0 -179
  79. package/dist/commands.d.ts.map +0 -1
  80. package/dist/fs.d.ts.map +0 -1
  81. package/dist/internal.d.ts +0 -79
  82. package/dist/internal.d.ts.map +0 -1
  83. package/dist/internal.js +0 -1942
  84. package/dist/loader.d.ts +0 -164
  85. package/dist/loader.d.ts.map +0 -1
  86. package/dist/packages.d.ts +0 -199
  87. package/dist/packages.d.ts.map +0 -1
  88. package/dist/runner.d.ts +0 -314
  89. package/dist/runner.d.ts.map +0 -1
  90. package/dist/sandbox-manager.d.ts +0 -261
  91. package/dist/sandbox-manager.d.ts.map +0 -1
  92. package/dist/sandbox.d.ts +0 -267
  93. package/dist/sandbox.d.ts.map +0 -1
  94. package/dist/shared-modules.d.ts +0 -148
  95. package/dist/shared-modules.d.ts.map +0 -1
  96. package/dist/shared-resources.d.ts +0 -102
  97. package/dist/shared-resources.d.ts.map +0 -1
  98. package/dist/ts-libs.d.ts +0 -85
  99. package/dist/ts-libs.d.ts.map +0 -1
  100. package/dist/typechecker.d.ts +0 -127
  101. package/dist/typechecker.d.ts.map +0 -1
  102. package/src/build-emitter.ts +0 -64
  103. package/src/builder.ts +0 -498
  104. package/src/bundler.ts +0 -575
  105. package/src/commands/compile.ts +0 -236
  106. package/src/commands/packages.ts +0 -154
  107. package/src/commands/run.ts +0 -245
  108. package/src/internal.ts +0 -119
  109. package/src/loader.ts +0 -229
  110. package/src/packages.ts +0 -936
  111. package/src/sandbox.ts +0 -398
  112. package/src/shared-modules.ts +0 -280
  113. package/src/shared-resources.ts +0 -166
  114. package/src/ts-libs.ts +0 -218
  115. package/src/typechecker.ts +0 -635
@@ -0,0 +1,624 @@
1
+ /**
2
+ * Sandbox implementation for v2.
3
+ *
4
+ * A sandbox is a single-project environment with its own:
5
+ * - Virtual filesystem (sync)
6
+ * - Installed packages (tracked in /package.json)
7
+ * - Build configuration (entry point, tsconfig)
8
+ *
9
+ * The sandbox exposes both direct methods (install, build, etc.) and
10
+ * shell commands via exec() for flexibility.
11
+ *
12
+ * Build produces a code string but does NOT load or execute it.
13
+ * Execution is handled by an external executor (main thread, worker, iframe, etc.)
14
+ * which provides appropriate isolation and security boundaries.
15
+ */
16
+
17
+ import { Bash } from "just-bash/browser";
18
+ import type {
19
+ IBundler,
20
+ ITypechecker,
21
+ ITypesResolver,
22
+ ISharedModuleRegistry,
23
+ IExecutor,
24
+ Sandbox,
25
+ SandboxOptions,
26
+ SandboxState,
27
+ SandboxBuildOptions,
28
+ SandboxTypecheckOptions,
29
+ BuildResult,
30
+ BuildSuccess,
31
+ InstallResult,
32
+ UninstallResult,
33
+ TypecheckResult,
34
+ ExecResult,
35
+ RunOptions,
36
+ RunResult,
37
+ } from "../types";
38
+ import { Filesystem, wrapFilesystemForJustBash } from "./fs";
39
+ import { createDefaultCommands, type SandboxRef } from "../commands";
40
+
41
+ // =============================================================================
42
+ // Default Configuration
43
+ // =============================================================================
44
+
45
+ const DEFAULT_ENTRY_POINT = "./index.ts";
46
+ const TSCONFIG_PATH = "/tsconfig.json";
47
+ const PACKAGE_JSON_PATH = "/package.json";
48
+
49
+ const DEFAULT_PACKAGE_JSON = {
50
+ main: DEFAULT_ENTRY_POINT,
51
+ dependencies: {},
52
+ };
53
+
54
+ const DEFAULT_TSCONFIG = {
55
+ compilerOptions: {
56
+ target: "ES2020",
57
+ lib: ["ES2020", "DOM", "DOM.Iterable"],
58
+ module: "ESNext",
59
+ moduleResolution: "bundler",
60
+ jsx: "react-jsx",
61
+ strict: true,
62
+ noEmit: true,
63
+ esModuleInterop: true,
64
+ skipLibCheck: true,
65
+ resolveJsonModule: true,
66
+ isolatedModules: true,
67
+ },
68
+ include: ["**/*.ts", "**/*.tsx"],
69
+ exclude: ["node_modules"],
70
+ };
71
+
72
+ // =============================================================================
73
+ // Package Management Helpers (Sync)
74
+ // =============================================================================
75
+
76
+ /**
77
+ * Parse package specifier into name and version
78
+ */
79
+ function parsePackageSpec(spec: string): { name: string; version?: string } {
80
+ if (spec.startsWith("@")) {
81
+ const slashIndex = spec.indexOf("/");
82
+ if (slashIndex === -1) {
83
+ return { name: spec };
84
+ }
85
+ const afterSlash = spec.slice(slashIndex + 1);
86
+ const atIndex = afterSlash.indexOf("@");
87
+ if (atIndex === -1) {
88
+ return { name: spec };
89
+ }
90
+ return {
91
+ name: spec.slice(0, slashIndex + 1 + atIndex),
92
+ version: afterSlash.slice(atIndex + 1),
93
+ };
94
+ }
95
+
96
+ const atIndex = spec.indexOf("@");
97
+ if (atIndex === -1) {
98
+ return { name: spec };
99
+ }
100
+ return {
101
+ name: spec.slice(0, atIndex),
102
+ version: spec.slice(atIndex + 1),
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Read and parse /package.json
108
+ */
109
+ function readPackageJson(
110
+ fs: Filesystem
111
+ ): { main?: string; dependencies?: Record<string, string> } {
112
+ try {
113
+ if (fs.exists(PACKAGE_JSON_PATH)) {
114
+ const content = fs.readFile(PACKAGE_JSON_PATH);
115
+ return JSON.parse(content);
116
+ }
117
+ } catch {
118
+ // Invalid JSON or read error
119
+ }
120
+ return {};
121
+ }
122
+
123
+ /**
124
+ * Get the entry point from package.json's main field
125
+ */
126
+ function getEntryPoint(fs: Filesystem): string {
127
+ const pkg = readPackageJson(fs);
128
+ const main = pkg.main ?? DEFAULT_ENTRY_POINT;
129
+ // Normalize: ensure it starts with /
130
+ if (main.startsWith("/")) {
131
+ return main;
132
+ }
133
+ if (main.startsWith("./")) {
134
+ return "/" + main.slice(2);
135
+ }
136
+ return "/" + main;
137
+ }
138
+
139
+ /**
140
+ * Read installed packages from /package.json
141
+ */
142
+ function getInstalledPackages(fs: Filesystem): Record<string, string> {
143
+ const pkg = readPackageJson(fs);
144
+ return pkg.dependencies ?? {};
145
+ }
146
+
147
+ /**
148
+ * Save installed packages to /package.json
149
+ */
150
+ function saveInstalledPackages(
151
+ fs: Filesystem,
152
+ dependencies: Record<string, string>
153
+ ): void {
154
+ let existing: Record<string, unknown> = {};
155
+
156
+ try {
157
+ if (fs.exists(PACKAGE_JSON_PATH)) {
158
+ const content = fs.readFile(PACKAGE_JSON_PATH);
159
+ existing = JSON.parse(content);
160
+ }
161
+ } catch {
162
+ // Start fresh if invalid
163
+ }
164
+
165
+ const updated = {
166
+ ...existing,
167
+ dependencies,
168
+ };
169
+
170
+ fs.writeFile(PACKAGE_JSON_PATH, JSON.stringify(updated, null, 2));
171
+ }
172
+
173
+ /**
174
+ * Ensure a directory exists
175
+ */
176
+ function ensureDir(fs: Filesystem, path: string): void {
177
+ if (path === "/" || path === "") return;
178
+
179
+ if (fs.exists(path)) {
180
+ const stat = fs.stat(path);
181
+ if (stat.isDirectory) return;
182
+ }
183
+
184
+ const parent = path.substring(0, path.lastIndexOf("/")) || "/";
185
+ ensureDir(fs, parent);
186
+ fs.mkdir(path);
187
+ }
188
+
189
+ // =============================================================================
190
+ // Sandbox Context (dependencies from Sandlot)
191
+ // =============================================================================
192
+
193
+ export interface SandboxContext {
194
+ bundler: IBundler;
195
+ typechecker?: ITypechecker;
196
+ typesResolver?: ITypesResolver;
197
+ sharedModuleRegistry: ISharedModuleRegistry | null;
198
+ executor?: IExecutor;
199
+ }
200
+
201
+ // =============================================================================
202
+ // Sandbox Implementation Factory
203
+ // =============================================================================
204
+
205
+ /**
206
+ * Create a sandbox instance.
207
+ * This is called by createSandlot().createSandbox().
208
+ */
209
+ export async function createSandboxImpl(
210
+ fs: Filesystem,
211
+ options: SandboxOptions,
212
+ context: SandboxContext
213
+ ): Promise<Sandbox> {
214
+ const {
215
+ bundler,
216
+ typechecker,
217
+ typesResolver,
218
+ sharedModuleRegistry,
219
+ executor,
220
+ } = context;
221
+
222
+ // ---------------------------------------------------------------------------
223
+ // Internal State
224
+ // ---------------------------------------------------------------------------
225
+
226
+ let lastBuild: BuildSuccess | null = null;
227
+ const onBuildCallbacks = new Set<
228
+ (result: BuildSuccess) => void | Promise<void>
229
+ >();
230
+
231
+ // Register initial onBuild callback if provided
232
+ if (options.onBuild) {
233
+ onBuildCallbacks.add(options.onBuild);
234
+ }
235
+
236
+ // ---------------------------------------------------------------------------
237
+ // Initialize Filesystem
238
+ // ---------------------------------------------------------------------------
239
+
240
+ // Write initial files first (user-provided files take precedence)
241
+ if (options.initialFiles) {
242
+ for (const [path, content] of Object.entries(options.initialFiles)) {
243
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
244
+ const dir = normalizedPath.substring(0, normalizedPath.lastIndexOf("/"));
245
+ if (dir && dir !== "/") {
246
+ ensureDir(fs, dir);
247
+ }
248
+ fs.writeFile(normalizedPath, content);
249
+ }
250
+ }
251
+
252
+ // Ensure package.json exists (with default entry point)
253
+ if (!fs.exists(PACKAGE_JSON_PATH)) {
254
+ fs.writeFile(
255
+ PACKAGE_JSON_PATH,
256
+ JSON.stringify(DEFAULT_PACKAGE_JSON, null, 2)
257
+ );
258
+ }
259
+
260
+ // Ensure tsconfig.json exists
261
+ if (!fs.exists(TSCONFIG_PATH)) {
262
+ fs.writeFile(TSCONFIG_PATH, JSON.stringify(DEFAULT_TSCONFIG, null, 2));
263
+ }
264
+
265
+ // ---------------------------------------------------------------------------
266
+ // Core Methods
267
+ // ---------------------------------------------------------------------------
268
+
269
+ /**
270
+ * Install a package
271
+ */
272
+ async function install(packageSpec: string): Promise<InstallResult> {
273
+ const { name, version } = parsePackageSpec(packageSpec);
274
+
275
+ // Resolve version and fetch types
276
+ let resolvedVersion = version ?? "latest";
277
+ let typesInstalled = false;
278
+ let typeFilesCount = 0;
279
+ let typesError: string | undefined;
280
+ const fromCache = false;
281
+
282
+ // If typesResolver is available, use it to get type definitions
283
+ if (typesResolver) {
284
+ try {
285
+ const typeFiles = await typesResolver.resolveTypes(name, version);
286
+
287
+ // Write type files to node_modules
288
+ const packageDir = `/node_modules/${name}`;
289
+ ensureDir(fs, packageDir);
290
+
291
+ // Determine the main types entry file
292
+ let typesEntry = "index.d.ts";
293
+
294
+ for (const [filePath, content] of Object.entries(typeFiles)) {
295
+ const fullPath = filePath.startsWith("/")
296
+ ? filePath
297
+ : `${packageDir}/${filePath}`;
298
+ const dir = fullPath.substring(0, fullPath.lastIndexOf("/"));
299
+ ensureDir(fs, dir);
300
+ fs.writeFile(fullPath, content);
301
+ typeFilesCount++;
302
+
303
+ // Track the first .d.ts file as the types entry if no index.d.ts
304
+ const relativePath = fullPath.replace(`${packageDir}/`, "");
305
+ if (relativePath === "index.d.ts") {
306
+ typesEntry = "index.d.ts";
307
+ } else if (typesEntry === "index.d.ts" && relativePath.endsWith(".d.ts") && !relativePath.includes("/")) {
308
+ // Use the first top-level .d.ts as fallback
309
+ typesEntry = relativePath;
310
+ }
311
+ }
312
+
313
+ typesInstalled = typeFilesCount > 0;
314
+
315
+ // Create a minimal package.json for the installed package
316
+ // This is required for TypeScript module resolution to find the types
317
+ if (typesInstalled) {
318
+ const pkgJsonPath = `${packageDir}/package.json`;
319
+ const pkgJson = {
320
+ name,
321
+ version: resolvedVersion,
322
+ types: typesEntry,
323
+ main: typesEntry.replace(/\.d\.ts$/, ".js"),
324
+ };
325
+ fs.writeFile(pkgJsonPath, JSON.stringify(pkgJson, null, 2));
326
+ }
327
+
328
+ // Try to extract version from type files or use provided
329
+ if (!version) {
330
+ resolvedVersion = "latest";
331
+ }
332
+ } catch (err) {
333
+ typesError = err instanceof Error ? err.message : String(err);
334
+ }
335
+ }
336
+
337
+ // Update package.json
338
+ const dependencies = getInstalledPackages(fs);
339
+ dependencies[name] = resolvedVersion;
340
+ saveInstalledPackages(fs, dependencies);
341
+
342
+ return {
343
+ name,
344
+ version: resolvedVersion,
345
+ typesInstalled,
346
+ typeFilesCount,
347
+ typesError,
348
+ fromCache,
349
+ };
350
+ }
351
+
352
+ /**
353
+ * Uninstall a package
354
+ */
355
+ async function uninstall(packageName: string): Promise<UninstallResult> {
356
+ const dependencies = getInstalledPackages(fs);
357
+
358
+ if (!(packageName in dependencies)) {
359
+ return { name: packageName, removed: false };
360
+ }
361
+
362
+ // Remove from dependencies
363
+ delete dependencies[packageName];
364
+ saveInstalledPackages(fs, dependencies);
365
+
366
+ // Remove type files
367
+ const typesPath = `/node_modules/${packageName}`;
368
+ if (fs.exists(typesPath)) {
369
+ fs.rm(typesPath, { recursive: true, force: true });
370
+ }
371
+
372
+ return { name: packageName, removed: true };
373
+ }
374
+
375
+ /**
376
+ * Build the project
377
+ */
378
+ async function build(buildOptions?: SandboxBuildOptions): Promise<BuildResult> {
379
+ // Get entry point: explicit option > package.json main > default
380
+ const buildEntryPoint = buildOptions?.entryPoint ?? getEntryPoint(fs);
381
+ const skipTypecheck = buildOptions?.skipTypecheck ?? false;
382
+ const minify = buildOptions?.minify ?? false;
383
+ const format = buildOptions?.format ?? "esm";
384
+
385
+ // Step 1: Verify entry point exists
386
+ if (!fs.exists(buildEntryPoint)) {
387
+ return {
388
+ success: false,
389
+ phase: "entry",
390
+ message: `Entry point not found: ${buildEntryPoint}`,
391
+ };
392
+ }
393
+
394
+ // Step 2: Type check (unless skipped or no typechecker)
395
+ if (!skipTypecheck && typechecker) {
396
+ const typecheckResult = await typechecker.typecheck({
397
+ fs,
398
+ entryPoint: buildEntryPoint,
399
+ tsconfigPath: TSCONFIG_PATH,
400
+ });
401
+
402
+ if (!typecheckResult.success) {
403
+ return {
404
+ success: false,
405
+ phase: "typecheck",
406
+ diagnostics: typecheckResult.diagnostics,
407
+ };
408
+ }
409
+ }
410
+
411
+ // Step 3: Read installed packages
412
+ const installedPackages = getInstalledPackages(fs);
413
+
414
+ // Step 4: Bundle
415
+ const bundleResult = await bundler.bundle({
416
+ fs,
417
+ entryPoint: buildEntryPoint,
418
+ installedPackages,
419
+ sharedModules: sharedModuleRegistry?.list() ?? [],
420
+ sharedModuleRegistry: sharedModuleRegistry ?? undefined,
421
+ format,
422
+ minify,
423
+ });
424
+
425
+ // Check for bundle errors
426
+ if (!bundleResult.success) {
427
+ return {
428
+ success: false,
429
+ phase: "bundle",
430
+ bundleErrors: bundleResult.errors,
431
+ bundleWarnings: bundleResult.warnings,
432
+ };
433
+ }
434
+
435
+ // Step 5: Create build output (no loading/execution - that's the executor's job)
436
+ const output: BuildSuccess = {
437
+ success: true,
438
+ code: bundleResult.code,
439
+ includedFiles: bundleResult.includedFiles,
440
+ warnings: bundleResult.warnings,
441
+ };
442
+
443
+ // Step 6: Update lastBuild and fire callbacks
444
+ lastBuild = output;
445
+ for (const callback of onBuildCallbacks) {
446
+ try {
447
+ await callback(output);
448
+ } catch (err) {
449
+ console.error("[sandlot] onBuild callback error:", err);
450
+ }
451
+ }
452
+
453
+ return output;
454
+ }
455
+
456
+ /**
457
+ * Type check the project
458
+ */
459
+ async function typecheck(
460
+ typecheckOptions?: SandboxTypecheckOptions
461
+ ): Promise<TypecheckResult> {
462
+ if (!typechecker) {
463
+ // No typechecker configured - return success with no diagnostics
464
+ return { success: true, diagnostics: [] };
465
+ }
466
+
467
+ // Get entry point: explicit option > package.json main > default
468
+ const checkEntryPoint = typecheckOptions?.entryPoint ?? getEntryPoint(fs);
469
+
470
+ // Verify entry point exists
471
+ if (!fs.exists(checkEntryPoint)) {
472
+ return {
473
+ success: false,
474
+ diagnostics: [
475
+ {
476
+ message: `Entry point not found: ${checkEntryPoint}`,
477
+ severity: "error",
478
+ },
479
+ ],
480
+ };
481
+ }
482
+
483
+ return typechecker.typecheck({
484
+ fs,
485
+ entryPoint: checkEntryPoint,
486
+ tsconfigPath: TSCONFIG_PATH,
487
+ });
488
+ }
489
+
490
+ /**
491
+ * Build and run code using the configured executor.
492
+ */
493
+ async function run(runOptions?: RunOptions): Promise<RunResult> {
494
+ // Ensure executor is configured
495
+ if (!executor) {
496
+ throw new Error(
497
+ "[sandlot] No executor configured. Provide an executor when creating Sandlot to use run()."
498
+ );
499
+ }
500
+
501
+ // Step 1: Build the code
502
+ const buildResult = await build({
503
+ entryPoint: runOptions?.entryPoint,
504
+ skipTypecheck: runOptions?.skipTypecheck,
505
+ });
506
+
507
+ // If build failed, return early with build failure info
508
+ if (!buildResult.success) {
509
+ return {
510
+ success: false,
511
+ logs: [],
512
+ error: buildResult.message ?? `Build failed in ${buildResult.phase} phase`,
513
+ buildFailure: {
514
+ phase: buildResult.phase,
515
+ message: buildResult.message,
516
+ diagnostics: buildResult.diagnostics,
517
+ bundleErrors: buildResult.bundleErrors,
518
+ bundleWarnings: buildResult.bundleWarnings,
519
+ },
520
+ };
521
+ }
522
+
523
+ // Step 2: Execute via the executor
524
+ const executeResult = await executor.execute(buildResult.code, {
525
+ entryExport: runOptions?.entryExport ?? "main",
526
+ context: runOptions?.context,
527
+ timeout: runOptions?.timeout,
528
+ });
529
+
530
+ // Return the execution result
531
+ return {
532
+ success: executeResult.success,
533
+ logs: executeResult.logs,
534
+ returnValue: executeResult.returnValue,
535
+ error: executeResult.error,
536
+ executionTimeMs: executeResult.executionTimeMs,
537
+ };
538
+ }
539
+
540
+ // ---------------------------------------------------------------------------
541
+ // Shell Environment (lazy initialization)
542
+ // ---------------------------------------------------------------------------
543
+
544
+ // Create a SandboxRef for commands to use
545
+ const sandboxRef: SandboxRef = {
546
+ fs,
547
+ install,
548
+ uninstall,
549
+ build,
550
+ typecheck,
551
+ run,
552
+ };
553
+
554
+ // Lazily initialized Bash instance
555
+ let bashInstance: Bash | null = null;
556
+
557
+ function getBash(): Bash {
558
+ if (!bashInstance) {
559
+ const commands = createDefaultCommands(sandboxRef);
560
+ bashInstance = new Bash({
561
+ cwd: "/",
562
+ fs: wrapFilesystemForJustBash(fs),
563
+ customCommands: commands,
564
+ });
565
+ }
566
+ return bashInstance;
567
+ }
568
+
569
+ /**
570
+ * Execute a shell command using just-bash.
571
+ *
572
+ * Supports standard bash commands (echo, cat, cd, etc.) plus:
573
+ * - sandlot build [options]
574
+ * - sandlot typecheck [options]
575
+ * - sandlot install <pkg> [...]
576
+ * - sandlot uninstall <pkg> [...]
577
+ * - sandlot help
578
+ */
579
+ async function exec(command: string): Promise<ExecResult> {
580
+ const bash = getBash();
581
+ const result = await bash.exec(command);
582
+ return {
583
+ exitCode: result.exitCode,
584
+ stdout: result.stdout,
585
+ stderr: result.stderr,
586
+ };
587
+ }
588
+
589
+ // ---------------------------------------------------------------------------
590
+ // Return Sandbox Interface
591
+ // ---------------------------------------------------------------------------
592
+
593
+ return {
594
+ fs,
595
+
596
+ exec,
597
+
598
+ get lastBuild() {
599
+ return lastBuild;
600
+ },
601
+
602
+ getState(): SandboxState {
603
+ return { files: fs.getFiles() };
604
+ },
605
+
606
+ onBuild(callback) {
607
+ onBuildCallbacks.add(callback);
608
+ return () => {
609
+ onBuildCallbacks.delete(callback);
610
+ };
611
+ },
612
+
613
+ // Direct methods
614
+ install,
615
+ uninstall,
616
+ build,
617
+ typecheck,
618
+ run,
619
+
620
+ // File operations (fs handles path normalization and parent dir creation)
621
+ readFile: (path: string) => fs.readFile(path),
622
+ writeFile: (path: string, content: string) => fs.writeFile(path, content),
623
+ };
624
+ }
@@ -0,0 +1,77 @@
1
+ import type {
2
+ Sandlot,
3
+ SandlotOptions,
4
+ SandboxOptions,
5
+ Sandbox,
6
+ ISharedModuleRegistry,
7
+ } from "../types";
8
+ import { createSharedModuleRegistry } from "./shared-module-registry";
9
+ import { createSandboxImpl, type SandboxContext } from "./sandbox";
10
+ import { Filesystem } from "./fs";
11
+
12
+ /**
13
+ * Create a new Sandlot instance with the provided implementations.
14
+ *
15
+ * This is the main factory function. You provide the bundler, typechecker,
16
+ * and other implementations appropriate for your runtime context.
17
+ *
18
+ * @example Browser usage
19
+ * ```ts
20
+ * import { createSandlot } from "sandlot";
21
+ * import { EsbuildWasmBundler } from "sandlot/browser";
22
+ *
23
+ * const sandlot = createSandlot({
24
+ * bundler: new EsbuildWasmBundler(),
25
+ * sharedModules: { react: React },
26
+ * });
27
+ * ```
28
+ *
29
+ * @example Node/Bun usage
30
+ * ```ts
31
+ * import { createSandlot } from "sandlot";
32
+ * import { EsbuildNativeBundler } from "sandlot/node";
33
+ *
34
+ * const sandlot = createSandlot({
35
+ * bundler: new EsbuildNativeBundler(),
36
+ * });
37
+ * ```
38
+ */
39
+ export function createSandlot(options: SandlotOptions): Sandlot {
40
+ const {
41
+ bundler,
42
+ typechecker,
43
+ typesResolver,
44
+ executor,
45
+ sharedModules,
46
+ sandboxDefaults = {},
47
+ } = options;
48
+
49
+ // Create shared module registry if modules were provided
50
+ const sharedModuleRegistry = createSharedModuleRegistry(sharedModules);
51
+
52
+ // Create the context that will be passed to each sandbox
53
+ const sandboxContext: SandboxContext = {
54
+ bundler,
55
+ typechecker,
56
+ typesResolver,
57
+ sharedModuleRegistry,
58
+ executor,
59
+ };
60
+
61
+ return {
62
+ async createSandbox(sandboxOptions: SandboxOptions = {}): Promise<Sandbox> {
63
+ // Create the virtual filesystem
64
+ const fs = Filesystem.create({
65
+ maxSizeBytes: sandboxOptions.maxFilesystemSize ?? sandboxDefaults.maxFilesystemSize,
66
+ // Note: initialFiles will be written by createSandboxImpl
67
+ });
68
+
69
+ // Create and return the sandbox
70
+ return createSandboxImpl(fs, sandboxOptions, sandboxContext);
71
+ },
72
+
73
+ get sharedModules(): ISharedModuleRegistry | null {
74
+ return sharedModuleRegistry;
75
+ },
76
+ };
77
+ }