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.
- package/README.md +145 -518
- package/dist/build-emitter.d.ts +47 -0
- package/dist/build-emitter.d.ts.map +1 -0
- package/dist/builder.d.ts +370 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/bundler.d.ts +3 -3
- package/dist/bundler.d.ts.map +1 -1
- package/dist/commands/compile.d.ts +13 -0
- package/dist/commands/compile.d.ts.map +1 -0
- package/dist/commands/index.d.ts +17 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/packages.d.ts +17 -0
- package/dist/commands/packages.d.ts.map +1 -0
- package/dist/commands/run.d.ts +40 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/types.d.ts +141 -0
- package/dist/commands/types.d.ts.map +1 -0
- package/dist/fs.d.ts +60 -42
- package/dist/fs.d.ts.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +304 -491
- package/dist/internal.d.ts +5 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +174 -95
- package/dist/runner.d.ts +314 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/sandbox-manager.d.ts +45 -33
- package/dist/sandbox-manager.d.ts.map +1 -1
- package/dist/sandbox.d.ts +144 -70
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/shared-modules.d.ts +22 -3
- package/dist/shared-modules.d.ts.map +1 -1
- package/dist/shared-resources.d.ts +0 -3
- package/dist/shared-resources.d.ts.map +1 -1
- package/dist/typechecker.d.ts +1 -1
- package/package.json +3 -17
- package/src/build-emitter.ts +64 -0
- package/src/builder.ts +498 -0
- package/src/bundler.ts +86 -57
- package/src/commands/compile.ts +236 -0
- package/src/commands/index.ts +51 -0
- package/src/commands/packages.ts +154 -0
- package/src/commands/run.ts +245 -0
- package/src/commands/types.ts +172 -0
- package/src/fs.ts +90 -216
- package/src/index.ts +34 -12
- package/src/internal.ts +5 -2
- package/src/sandbox.ts +214 -220
- package/src/shared-modules.ts +74 -4
- package/src/shared-resources.ts +0 -3
- package/src/ts-libs.ts +1 -1
- package/src/typechecker.ts +1 -1
- package/dist/react.d.ts +0 -159
- package/dist/react.d.ts.map +0 -1
- package/dist/react.js +0 -149
- package/src/commands.ts +0 -733
- package/src/react.tsx +0 -331
- 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
|
+
}
|