sandlot 0.1.1 → 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 (59) hide show
  1. package/README.md +145 -518
  2. package/dist/build-emitter.d.ts +47 -0
  3. package/dist/build-emitter.d.ts.map +1 -0
  4. package/dist/builder.d.ts +370 -0
  5. package/dist/builder.d.ts.map +1 -0
  6. package/dist/bundler.d.ts +3 -3
  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 +60 -42
  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 +304 -491
  23. package/dist/internal.d.ts +5 -0
  24. package/dist/internal.d.ts.map +1 -1
  25. package/dist/internal.js +174 -95
  26. package/dist/runner.d.ts +314 -0
  27. package/dist/runner.d.ts.map +1 -0
  28. package/dist/sandbox-manager.d.ts +45 -33
  29. package/dist/sandbox-manager.d.ts.map +1 -1
  30. package/dist/sandbox.d.ts +144 -70
  31. package/dist/sandbox.d.ts.map +1 -1
  32. package/dist/shared-modules.d.ts +22 -3
  33. package/dist/shared-modules.d.ts.map +1 -1
  34. package/dist/shared-resources.d.ts +0 -3
  35. package/dist/shared-resources.d.ts.map +1 -1
  36. package/dist/typechecker.d.ts +1 -1
  37. package/package.json +3 -17
  38. package/src/build-emitter.ts +64 -0
  39. package/src/builder.ts +498 -0
  40. package/src/bundler.ts +86 -57
  41. package/src/commands/compile.ts +236 -0
  42. package/src/commands/index.ts +51 -0
  43. package/src/commands/packages.ts +154 -0
  44. package/src/commands/run.ts +245 -0
  45. package/src/commands/types.ts +172 -0
  46. package/src/fs.ts +90 -216
  47. package/src/index.ts +34 -12
  48. package/src/internal.ts +5 -2
  49. package/src/sandbox.ts +214 -220
  50. package/src/shared-modules.ts +74 -4
  51. package/src/shared-resources.ts +0 -3
  52. package/src/ts-libs.ts +1 -1
  53. package/src/typechecker.ts +1 -1
  54. package/dist/react.d.ts +0 -159
  55. package/dist/react.d.ts.map +0 -1
  56. package/dist/react.js +0 -149
  57. package/src/commands.ts +0 -733
  58. package/src/react.tsx +0 -331
  59. package/src/sandbox-manager.ts +0 -490
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Run command for executing code in the sandbox.
3
+ */
4
+
5
+ import { defineCommand, type CommandContext } from "just-bash/browser";
6
+ import { typecheck, formatDiagnosticsForAgent } from "../typechecker";
7
+ import { bundle } from "../bundler";
8
+ import { loadModule } from "../loader";
9
+ import type { CommandDeps, RunContext } from "./types";
10
+
11
+ /**
12
+ * Create the `run` command for executing code in the sandbox.
13
+ *
14
+ * The run command:
15
+ * 1. Builds the entry point (with type checking by default)
16
+ * 2. Dynamically imports the bundle
17
+ * 3. If a `main` export exists, calls it with a RunContext
18
+ * 4. Captures all console output (log, warn, error)
19
+ * 5. Returns the captured output and any return value from main()
20
+ *
21
+ * Usage:
22
+ * run [entry] [--skip-typecheck|-s] [--timeout|-t <ms>] [-- args...]
23
+ *
24
+ * Code can be written in two styles:
25
+ *
26
+ * 1. Script style (top-level code, runs on import):
27
+ * ```ts
28
+ * console.log("Hello from script!");
29
+ * const result = 2 + 2;
30
+ * console.log("Result:", result);
31
+ * ```
32
+ *
33
+ * 2. Main function style (with context access):
34
+ * ```ts
35
+ * import type { RunContext } from "sandlot";
36
+ *
37
+ * export async function main(ctx: RunContext) {
38
+ * ctx.log("Reading file...");
39
+ * const content = await ctx.fs.readFile("/data/input.txt");
40
+ * ctx.log("Content:", content);
41
+ * return { success: true };
42
+ * }
43
+ * ```
44
+ */
45
+ export function createRunCommand(deps: CommandDeps) {
46
+ const { fs, libFiles, tsconfigPath, runOptions = {}, sharedModules } = deps;
47
+
48
+ return defineCommand("run", async (args, _ctx: CommandContext) => {
49
+ // Parse arguments
50
+ let entryPoint: string | null = null;
51
+ let skipTypecheck = runOptions.skipTypecheck ?? false;
52
+ let timeout = runOptions.timeout ?? 30000;
53
+ const scriptArgs: string[] = [];
54
+ let collectingArgs = false;
55
+
56
+ for (let i = 0; i < args.length; i++) {
57
+ const arg = args[i];
58
+
59
+ if (collectingArgs) {
60
+ scriptArgs.push(arg!);
61
+ continue;
62
+ }
63
+
64
+ if (arg === "--") {
65
+ collectingArgs = true;
66
+ } else if (arg === "--skip-typecheck" || arg === "-s") {
67
+ skipTypecheck = true;
68
+ } else if ((arg === "--timeout" || arg === "-t") && args[i + 1]) {
69
+ timeout = parseInt(args[++i]!, 10);
70
+ if (isNaN(timeout)) timeout = 30000;
71
+ } else if (!arg!.startsWith("-")) {
72
+ entryPoint = arg!;
73
+ }
74
+ }
75
+
76
+ // Entry point is required
77
+ if (!entryPoint) {
78
+ return {
79
+ stdout: "",
80
+ stderr: `Usage: run <entry-point> [options] [-- args...]\n\nOptions:\n --skip-typecheck, -s Skip type checking\n --timeout, -t <ms> Execution timeout (default: 30000)\n\nExample: run /src/index.ts\n`,
81
+ exitCode: 1,
82
+ };
83
+ }
84
+
85
+ // Capture console output
86
+ const logs: string[] = [];
87
+ const originalConsole = {
88
+ log: console.log,
89
+ warn: console.warn,
90
+ error: console.error,
91
+ info: console.info,
92
+ debug: console.debug,
93
+ };
94
+
95
+ const formatArgs = (...a: unknown[]) =>
96
+ a.map((v) => (typeof v === "object" ? JSON.stringify(v) : String(v))).join(" ");
97
+
98
+ const captureLog = (...a: unknown[]) => {
99
+ logs.push(formatArgs(...a));
100
+ originalConsole.log.apply(console, a);
101
+ };
102
+ const captureWarn = (...a: unknown[]) => {
103
+ logs.push(`[warn] ${formatArgs(...a)}`);
104
+ originalConsole.warn.apply(console, a);
105
+ };
106
+ const captureError = (...a: unknown[]) => {
107
+ logs.push(`[error] ${formatArgs(...a)}`);
108
+ originalConsole.error.apply(console, a);
109
+ };
110
+ const captureInfo = (...a: unknown[]) => {
111
+ logs.push(`[info] ${formatArgs(...a)}`);
112
+ originalConsole.info.apply(console, a);
113
+ };
114
+ const captureDebug = (...a: unknown[]) => {
115
+ logs.push(`[debug] ${formatArgs(...a)}`);
116
+ originalConsole.debug.apply(console, a);
117
+ };
118
+
119
+ const restoreConsole = () => {
120
+ console.log = originalConsole.log;
121
+ console.warn = originalConsole.warn;
122
+ console.error = originalConsole.error;
123
+ console.info = originalConsole.info;
124
+ console.debug = originalConsole.debug;
125
+ };
126
+
127
+ try {
128
+ // Check if entry point exists
129
+ if (!(await fs.exists(entryPoint))) {
130
+ return {
131
+ stdout: "",
132
+ stderr: `Error: Entry point not found: ${entryPoint}\n`,
133
+ exitCode: 1,
134
+ };
135
+ }
136
+
137
+ // Type check (unless skipped)
138
+ if (!skipTypecheck) {
139
+ const typecheckResult = await typecheck({
140
+ fs,
141
+ entryPoint,
142
+ tsconfigPath,
143
+ libFiles,
144
+ });
145
+
146
+ if (typecheckResult.hasErrors) {
147
+ const formatted = formatDiagnosticsForAgent(typecheckResult.diagnostics);
148
+ return {
149
+ stdout: "",
150
+ stderr: `Type errors:\n${formatted}\n`,
151
+ exitCode: 1,
152
+ };
153
+ }
154
+ }
155
+
156
+ // Bundle the code
157
+ const bundleResult = await bundle({
158
+ fs,
159
+ entryPoint,
160
+ format: "esm",
161
+ sharedModules,
162
+ });
163
+
164
+ // Install console interceptors
165
+ console.log = captureLog;
166
+ console.warn = captureWarn;
167
+ console.error = captureError;
168
+ console.info = captureInfo;
169
+ console.debug = captureDebug;
170
+
171
+ // Create the run context
172
+ const context: RunContext = {
173
+ fs,
174
+ env: { ...runOptions.env },
175
+ args: scriptArgs,
176
+ log: captureLog,
177
+ error: captureError,
178
+ };
179
+
180
+ // Execute the code with optional timeout
181
+ const startTime = performance.now();
182
+ let returnValue: unknown;
183
+
184
+ const executeCode = async () => {
185
+ // Load the module (this executes top-level code)
186
+ const module = await loadModule<{ main?: (ctx: RunContext) => unknown }>(bundleResult);
187
+
188
+ // If there's a main export, call it with context
189
+ if (typeof module.main === "function") {
190
+ returnValue = await module.main(context);
191
+ }
192
+ };
193
+
194
+ if (timeout > 0) {
195
+ const timeoutPromise = new Promise<never>((_, reject) => {
196
+ setTimeout(() => reject(new Error(`Execution timed out after ${timeout}ms`)), timeout);
197
+ });
198
+ await Promise.race([executeCode(), timeoutPromise]);
199
+ } else {
200
+ await executeCode();
201
+ }
202
+
203
+ const executionTimeMs = performance.now() - startTime;
204
+
205
+ // Restore console before building output
206
+ restoreConsole();
207
+
208
+ // Build output
209
+ let output = "";
210
+ if (logs.length > 0) {
211
+ output = logs.join("\n") + "\n";
212
+ }
213
+ if (returnValue !== undefined) {
214
+ const returnStr =
215
+ typeof returnValue === "object"
216
+ ? JSON.stringify(returnValue, null, 2)
217
+ : String(returnValue);
218
+ output += `[return] ${returnStr}\n`;
219
+ }
220
+ output += `\nExecution completed in ${executionTimeMs.toFixed(2)}ms\n`;
221
+
222
+ return {
223
+ stdout: output,
224
+ stderr: "",
225
+ exitCode: 0,
226
+ };
227
+ } catch (err) {
228
+ restoreConsole();
229
+
230
+ const errorMessage = err instanceof Error ? err.message : String(err);
231
+ const errorStack = err instanceof Error && err.stack ? `\n${err.stack}` : "";
232
+
233
+ let output = "";
234
+ if (logs.length > 0) {
235
+ output = logs.join("\n") + "\n\n";
236
+ }
237
+
238
+ return {
239
+ stdout: output,
240
+ stderr: `Runtime error: ${errorMessage}${errorStack}\n`,
241
+ exitCode: 1,
242
+ };
243
+ }
244
+ });
245
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Types and utilities for sandbox bash commands.
3
+ */
4
+
5
+ import type { IFileSystem } from "just-bash/browser";
6
+ import type { BundleResult } from "../bundler";
7
+ import type { TypesCache } from "../packages";
8
+
9
+ /**
10
+ * The result of a successful build, including the bundle and loaded module.
11
+ */
12
+ export interface BuildOutput {
13
+ /**
14
+ * The compiled bundle (code, metadata, etc.)
15
+ */
16
+ bundle: BundleResult;
17
+
18
+ /**
19
+ * The loaded module exports.
20
+ * If validation was provided, this is the validated module.
21
+ */
22
+ module: Record<string, unknown>;
23
+ }
24
+
25
+ /**
26
+ * Validation function type for module validation.
27
+ * Takes the raw module exports and returns validated exports (or throws).
28
+ */
29
+ export type ValidateFn = (module: Record<string, unknown>) => Record<string, unknown>;
30
+
31
+ /**
32
+ * Dependencies required by command factories
33
+ */
34
+ export interface CommandDeps {
35
+ /**
36
+ * The virtual filesystem to operate on
37
+ */
38
+ fs: IFileSystem;
39
+
40
+ /**
41
+ * Pre-loaded TypeScript lib files for type checking
42
+ */
43
+ libFiles: Map<string, string>;
44
+
45
+ /**
46
+ * Path to tsconfig.json in the virtual filesystem
47
+ */
48
+ tsconfigPath: string;
49
+
50
+ /**
51
+ * Callback invoked when a build succeeds (after loading and validation).
52
+ */
53
+ onBuild?: (result: BuildOutput) => void | Promise<void>;
54
+
55
+ /**
56
+ * Getter for the current validation function.
57
+ * Called during build to check if validation should be performed.
58
+ */
59
+ getValidation?: () => ValidateFn | null;
60
+
61
+ /**
62
+ * Cache for package type definitions.
63
+ * When provided, avoids redundant network fetches for packages
64
+ * that have already been installed in other sandboxes.
65
+ */
66
+ typesCache?: TypesCache;
67
+
68
+ /**
69
+ * Options for the `run` command
70
+ */
71
+ runOptions?: RunOptions;
72
+
73
+ /**
74
+ * Module IDs that should be resolved from the host's SharedModuleRegistry
75
+ * instead of esm.sh CDN. The host must have registered these modules.
76
+ *
77
+ * Example: ['react', 'react-dom/client']
78
+ */
79
+ sharedModules?: string[];
80
+ }
81
+
82
+ /**
83
+ * Runtime context passed to the `main()` function when code is executed.
84
+ * This provides sandboxed code with access to sandbox capabilities.
85
+ */
86
+ export interface RunContext {
87
+ /**
88
+ * The virtual filesystem - read/write files within the sandbox
89
+ */
90
+ fs: IFileSystem;
91
+
92
+ /**
93
+ * Environment variables (configurable per-sandbox)
94
+ */
95
+ env: Record<string, string>;
96
+
97
+ /**
98
+ * Command-line arguments passed to `run`
99
+ */
100
+ args: string[];
101
+
102
+ /**
103
+ * Explicit logging function (alternative to console.log)
104
+ */
105
+ log: (...args: unknown[]) => void;
106
+
107
+ /**
108
+ * Explicit error logging function (alternative to console.error)
109
+ */
110
+ error: (...args: unknown[]) => void;
111
+ }
112
+
113
+ /**
114
+ * Options for configuring the `run` command behavior
115
+ */
116
+ export interface RunOptions {
117
+ /**
118
+ * Environment variables available via ctx.env
119
+ */
120
+ env?: Record<string, string>;
121
+
122
+ /**
123
+ * Maximum execution time in milliseconds (default: 30000 = 30s)
124
+ * Set to 0 to disable timeout.
125
+ */
126
+ timeout?: number;
127
+
128
+ /**
129
+ * Whether to skip type checking before running (default: false)
130
+ */
131
+ skipTypecheck?: boolean;
132
+ }
133
+
134
+ /**
135
+ * Result of running code via the `run` command
136
+ */
137
+ export interface RunResult {
138
+ /**
139
+ * Captured console output (log, warn, error)
140
+ */
141
+ logs: string[];
142
+
143
+ /**
144
+ * Return value from main() if present
145
+ */
146
+ returnValue?: unknown;
147
+
148
+ /**
149
+ * Execution time in milliseconds
150
+ */
151
+ executionTimeMs: number;
152
+ }
153
+
154
+ /**
155
+ * Format esbuild messages (warnings/errors) for display
156
+ */
157
+ export function formatEsbuildMessages(
158
+ messages: { text: string; location?: { file?: string; line?: number; column?: number } | null }[]
159
+ ): string {
160
+ if (messages.length === 0) return "";
161
+
162
+ return messages
163
+ .map((msg) => {
164
+ if (msg.location) {
165
+ const { file, line, column } = msg.location;
166
+ const loc = file ? `${file}${line ? `:${line}` : ""}${column ? `:${column}` : ""}` : "";
167
+ return loc ? `${loc}: ${msg.text}` : msg.text;
168
+ }
169
+ return msg.text;
170
+ })
171
+ .join("\n");
172
+ }