sandlot 0.1.3 → 0.2.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.
- package/dist/browser/bundler.d.ts +68 -0
- package/dist/browser/bundler.d.ts.map +1 -0
- package/dist/browser/executor.d.ts +46 -0
- package/dist/browser/executor.d.ts.map +1 -0
- package/dist/browser/index.d.ts +9 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +2692 -0
- package/dist/browser/preset.d.ts +63 -0
- package/dist/browser/preset.d.ts.map +1 -0
- package/dist/commands/index.d.ts +20 -11
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/types.d.ts +31 -132
- package/dist/commands/types.d.ts.map +1 -1
- package/dist/core/bundler-utils.d.ts +142 -0
- package/dist/core/bundler-utils.d.ts.map +1 -0
- package/dist/core/esm-types-resolver.d.ts +125 -0
- package/dist/core/esm-types-resolver.d.ts.map +1 -0
- package/dist/core/executor.d.ts +35 -0
- package/dist/core/executor.d.ts.map +1 -0
- package/dist/{fs.d.ts → core/fs.d.ts} +27 -29
- package/dist/core/fs.d.ts.map +1 -0
- package/dist/core/sandbox.d.ts +30 -0
- package/dist/core/sandbox.d.ts.map +1 -0
- package/dist/core/sandlot.d.ts +30 -0
- package/dist/core/sandlot.d.ts.map +1 -0
- package/dist/core/shared-module-registry.d.ts +46 -0
- package/dist/core/shared-module-registry.d.ts.map +1 -0
- package/dist/core/typechecker.d.ts +60 -0
- package/dist/core/typechecker.d.ts.map +1 -0
- package/dist/index.d.ts +11 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1405 -2049
- package/dist/node/bundler.d.ts +48 -0
- package/dist/node/bundler.d.ts.map +1 -0
- package/dist/node/executor.d.ts +48 -0
- package/dist/node/executor.d.ts.map +1 -0
- package/dist/node/index.d.ts +9 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +2646 -0
- package/dist/node/preset.d.ts +62 -0
- package/dist/node/preset.d.ts.map +1 -0
- package/dist/types.d.ts +525 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +27 -8
- package/src/browser/bundler.ts +294 -0
- package/src/browser/executor.ts +71 -0
- package/src/browser/index.ts +57 -0
- package/src/browser/preset.ts +179 -0
- package/src/commands/index.ts +526 -43
- package/src/commands/types.ts +82 -146
- package/src/core/bundler-utils.ts +630 -0
- package/src/core/esm-types-resolver.ts +432 -0
- package/src/core/executor.ts +161 -0
- package/src/{fs.ts → core/fs.ts} +59 -37
- package/src/core/sandbox.ts +621 -0
- package/src/core/sandlot.ts +77 -0
- package/src/core/shared-module-registry.ts +138 -0
- package/src/core/typechecker.ts +607 -0
- package/src/index.ts +104 -139
- package/src/node/bundler.ts +194 -0
- package/src/node/executor.ts +87 -0
- package/src/node/index.ts +39 -0
- package/src/node/preset.ts +178 -0
- package/src/types.ts +668 -0
- package/README.md +0 -243
- package/dist/build-emitter.d.ts +0 -47
- package/dist/build-emitter.d.ts.map +0 -1
- package/dist/builder.d.ts +0 -370
- package/dist/builder.d.ts.map +0 -1
- package/dist/bundler.d.ts +0 -148
- package/dist/bundler.d.ts.map +0 -1
- package/dist/commands/compile.d.ts +0 -13
- package/dist/commands/compile.d.ts.map +0 -1
- package/dist/commands/packages.d.ts +0 -17
- package/dist/commands/packages.d.ts.map +0 -1
- package/dist/commands/run.d.ts +0 -40
- package/dist/commands/run.d.ts.map +0 -1
- package/dist/commands.d.ts +0 -179
- package/dist/commands.d.ts.map +0 -1
- package/dist/fs.d.ts.map +0 -1
- package/dist/internal.d.ts +0 -79
- package/dist/internal.d.ts.map +0 -1
- package/dist/internal.js +0 -1976
- package/dist/loader.d.ts +0 -164
- package/dist/loader.d.ts.map +0 -1
- package/dist/packages.d.ts +0 -199
- package/dist/packages.d.ts.map +0 -1
- package/dist/runner.d.ts +0 -314
- package/dist/runner.d.ts.map +0 -1
- package/dist/sandbox-manager.d.ts +0 -261
- package/dist/sandbox-manager.d.ts.map +0 -1
- package/dist/sandbox.d.ts +0 -267
- package/dist/sandbox.d.ts.map +0 -1
- package/dist/shared-modules.d.ts +0 -148
- package/dist/shared-modules.d.ts.map +0 -1
- package/dist/shared-resources.d.ts +0 -102
- package/dist/shared-resources.d.ts.map +0 -1
- package/dist/ts-libs.d.ts +0 -98
- package/dist/ts-libs.d.ts.map +0 -1
- package/dist/typechecker.d.ts +0 -127
- package/dist/typechecker.d.ts.map +0 -1
- package/src/build-emitter.ts +0 -64
- package/src/builder.ts +0 -498
- package/src/bundler.ts +0 -542
- package/src/commands/compile.ts +0 -236
- package/src/commands/packages.ts +0 -154
- package/src/commands/run.ts +0 -245
- package/src/internal.ts +0 -119
- package/src/loader.ts +0 -229
- package/src/packages.ts +0 -936
- package/src/sandbox.ts +0 -396
- package/src/shared-modules.ts +0 -280
- package/src/shared-resources.ts +0 -166
- package/src/ts-libs.ts +0 -320
- package/src/typechecker.ts +0 -635
|
@@ -0,0 +1,621 @@
|
|
|
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
|
+
},
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Step 2: Execute via the executor
|
|
521
|
+
const executeResult = await executor.execute(buildResult.code, {
|
|
522
|
+
entryExport: runOptions?.entryExport ?? "main",
|
|
523
|
+
context: runOptions?.context,
|
|
524
|
+
timeout: runOptions?.timeout,
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
// Return the execution result
|
|
528
|
+
return {
|
|
529
|
+
success: executeResult.success,
|
|
530
|
+
logs: executeResult.logs,
|
|
531
|
+
returnValue: executeResult.returnValue,
|
|
532
|
+
error: executeResult.error,
|
|
533
|
+
executionTimeMs: executeResult.executionTimeMs,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// ---------------------------------------------------------------------------
|
|
538
|
+
// Shell Environment (lazy initialization)
|
|
539
|
+
// ---------------------------------------------------------------------------
|
|
540
|
+
|
|
541
|
+
// Create a SandboxRef for commands to use
|
|
542
|
+
const sandboxRef: SandboxRef = {
|
|
543
|
+
fs,
|
|
544
|
+
install,
|
|
545
|
+
uninstall,
|
|
546
|
+
build,
|
|
547
|
+
typecheck,
|
|
548
|
+
run,
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
// Lazily initialized Bash instance
|
|
552
|
+
let bashInstance: Bash | null = null;
|
|
553
|
+
|
|
554
|
+
function getBash(): Bash {
|
|
555
|
+
if (!bashInstance) {
|
|
556
|
+
const commands = createDefaultCommands(sandboxRef);
|
|
557
|
+
bashInstance = new Bash({
|
|
558
|
+
cwd: "/",
|
|
559
|
+
fs: wrapFilesystemForJustBash(fs),
|
|
560
|
+
customCommands: commands,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
return bashInstance;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Execute a shell command using just-bash.
|
|
568
|
+
*
|
|
569
|
+
* Supports standard bash commands (echo, cat, cd, etc.) plus:
|
|
570
|
+
* - sandlot build [options]
|
|
571
|
+
* - sandlot typecheck [options]
|
|
572
|
+
* - sandlot install <pkg> [...]
|
|
573
|
+
* - sandlot uninstall <pkg> [...]
|
|
574
|
+
* - sandlot help
|
|
575
|
+
*/
|
|
576
|
+
async function exec(command: string): Promise<ExecResult> {
|
|
577
|
+
const bash = getBash();
|
|
578
|
+
const result = await bash.exec(command);
|
|
579
|
+
return {
|
|
580
|
+
exitCode: result.exitCode,
|
|
581
|
+
stdout: result.stdout,
|
|
582
|
+
stderr: result.stderr,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// ---------------------------------------------------------------------------
|
|
587
|
+
// Return Sandbox Interface
|
|
588
|
+
// ---------------------------------------------------------------------------
|
|
589
|
+
|
|
590
|
+
return {
|
|
591
|
+
fs,
|
|
592
|
+
|
|
593
|
+
exec,
|
|
594
|
+
|
|
595
|
+
get lastBuild() {
|
|
596
|
+
return lastBuild;
|
|
597
|
+
},
|
|
598
|
+
|
|
599
|
+
getState(): SandboxState {
|
|
600
|
+
return { files: fs.getFiles() };
|
|
601
|
+
},
|
|
602
|
+
|
|
603
|
+
onBuild(callback) {
|
|
604
|
+
onBuildCallbacks.add(callback);
|
|
605
|
+
return () => {
|
|
606
|
+
onBuildCallbacks.delete(callback);
|
|
607
|
+
};
|
|
608
|
+
},
|
|
609
|
+
|
|
610
|
+
// Direct methods
|
|
611
|
+
install,
|
|
612
|
+
uninstall,
|
|
613
|
+
build,
|
|
614
|
+
typecheck,
|
|
615
|
+
run,
|
|
616
|
+
|
|
617
|
+
// File operations (fs handles path normalization and parent dir creation)
|
|
618
|
+
readFile: (path: string) => fs.readFile(path),
|
|
619
|
+
writeFile: (path: string, content: string) => fs.writeFile(path, content),
|
|
620
|
+
};
|
|
621
|
+
}
|
|
@@ -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
|
+
}
|