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.
- 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 +2690 -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 +37 -130
- 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 +1398 -2010
- 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 +2644 -0
- package/dist/node/preset.d.ts +62 -0
- package/dist/node/preset.d.ts.map +1 -0
- package/dist/types.d.ts +528 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +16 -6
- 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 +498 -37
- package/src/commands/types.ts +117 -145
- 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 +624 -0
- package/src/core/sandlot.ts +77 -0
- package/src/core/shared-module-registry.ts +138 -0
- package/src/core/typechecker.ts +609 -0
- package/src/index.ts +106 -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 +672 -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 -152
- 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 -1942
- 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 -85
- 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 -575
- 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 -398
- package/src/shared-modules.ts +0 -280
- package/src/shared-resources.ts +0 -166
- package/src/ts-libs.ts +0 -218
- package/src/typechecker.ts +0 -635
|
@@ -0,0 +1,2690 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined")
|
|
5
|
+
return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// src/core/typechecker.ts
|
|
10
|
+
import ts from "typescript";
|
|
11
|
+
var TS_VERSION = "5.9.3";
|
|
12
|
+
var DEFAULT_CDN_BASE = `https://cdn.jsdelivr.net/npm/typescript@${TS_VERSION}/lib`;
|
|
13
|
+
var DEFAULT_LIBS = ["es2020", "dom", "dom.iterable"];
|
|
14
|
+
var LIB_PATH_PREFIX = "/node_modules/typescript/lib/";
|
|
15
|
+
function parseLibReferences(content) {
|
|
16
|
+
const refs = [];
|
|
17
|
+
const regex = /\/\/\/\s*<reference\s+lib="([^"]+)"\s*\/>/g;
|
|
18
|
+
let match;
|
|
19
|
+
while ((match = regex.exec(content)) !== null) {
|
|
20
|
+
if (match[1]) {
|
|
21
|
+
refs.push(match[1]);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return refs;
|
|
25
|
+
}
|
|
26
|
+
function libNameToFileName(name) {
|
|
27
|
+
return `lib.${name}.d.ts`;
|
|
28
|
+
}
|
|
29
|
+
async function fetchLibFile(name, baseUrl) {
|
|
30
|
+
const fileName = libNameToFileName(name);
|
|
31
|
+
const url = `${baseUrl}/${fileName}`;
|
|
32
|
+
const response = await fetch(url);
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
|
|
35
|
+
}
|
|
36
|
+
return response.text();
|
|
37
|
+
}
|
|
38
|
+
async function fetchAllLibs(libs, baseUrl) {
|
|
39
|
+
const result = new Map;
|
|
40
|
+
const pending = new Set(libs);
|
|
41
|
+
const fetched = new Set;
|
|
42
|
+
while (pending.size > 0) {
|
|
43
|
+
const batch = Array.from(pending);
|
|
44
|
+
pending.clear();
|
|
45
|
+
const results = await Promise.all(batch.map(async (name) => {
|
|
46
|
+
if (fetched.has(name)) {
|
|
47
|
+
return { name, content: null };
|
|
48
|
+
}
|
|
49
|
+
fetched.add(name);
|
|
50
|
+
try {
|
|
51
|
+
const content = await fetchLibFile(name, baseUrl);
|
|
52
|
+
return { name, content };
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.warn(`[typechecker] Failed to fetch lib.${name}.d.ts:`, err);
|
|
55
|
+
return { name, content: null };
|
|
56
|
+
}
|
|
57
|
+
}));
|
|
58
|
+
for (const { name, content } of results) {
|
|
59
|
+
if (content === null)
|
|
60
|
+
continue;
|
|
61
|
+
result.set(name, content);
|
|
62
|
+
const refs = parseLibReferences(content);
|
|
63
|
+
for (const ref of refs) {
|
|
64
|
+
if (!fetched.has(ref) && !pending.has(ref)) {
|
|
65
|
+
pending.add(ref);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
function normalizePath(path) {
|
|
73
|
+
if (!path.startsWith("/")) {
|
|
74
|
+
return "/" + path;
|
|
75
|
+
}
|
|
76
|
+
return path;
|
|
77
|
+
}
|
|
78
|
+
function getLibContent(fileName, libFiles) {
|
|
79
|
+
const match = fileName.match(/lib\.([^/]+)\.d\.ts$/);
|
|
80
|
+
if (match?.[1]) {
|
|
81
|
+
return libFiles.get(match[1]);
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
function createCompilerHost(fs, libFiles, options) {
|
|
86
|
+
return {
|
|
87
|
+
getSourceFile(fileName, languageVersion, onError) {
|
|
88
|
+
const normalizedPath = normalizePath(fileName);
|
|
89
|
+
try {
|
|
90
|
+
if (fs.exists(normalizedPath)) {
|
|
91
|
+
const stat = fs.stat(normalizedPath);
|
|
92
|
+
if (stat.isFile) {
|
|
93
|
+
const content = fs.readFile(normalizedPath);
|
|
94
|
+
return ts.createSourceFile(normalizedPath, content, languageVersion, true);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch {}
|
|
98
|
+
try {
|
|
99
|
+
if (fs.exists(fileName)) {
|
|
100
|
+
const stat = fs.stat(fileName);
|
|
101
|
+
if (stat.isFile) {
|
|
102
|
+
const content = fs.readFile(fileName);
|
|
103
|
+
return ts.createSourceFile(fileName, content, languageVersion, true);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} catch {}
|
|
107
|
+
const libContent = getLibContent(fileName, libFiles);
|
|
108
|
+
if (libContent !== undefined) {
|
|
109
|
+
return ts.createSourceFile(fileName, libContent, languageVersion, true);
|
|
110
|
+
}
|
|
111
|
+
if (onError) {
|
|
112
|
+
onError(`File not found: ${fileName}`);
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
},
|
|
116
|
+
getDefaultLibFileName(opts) {
|
|
117
|
+
return LIB_PATH_PREFIX + ts.getDefaultLibFileName(opts);
|
|
118
|
+
},
|
|
119
|
+
writeFile() {},
|
|
120
|
+
getCurrentDirectory() {
|
|
121
|
+
return "/";
|
|
122
|
+
},
|
|
123
|
+
getCanonicalFileName(fileName) {
|
|
124
|
+
return fileName;
|
|
125
|
+
},
|
|
126
|
+
useCaseSensitiveFileNames() {
|
|
127
|
+
return true;
|
|
128
|
+
},
|
|
129
|
+
getNewLine() {
|
|
130
|
+
return `
|
|
131
|
+
`;
|
|
132
|
+
},
|
|
133
|
+
fileExists(fileName) {
|
|
134
|
+
const normalizedPath = normalizePath(fileName);
|
|
135
|
+
try {
|
|
136
|
+
if (fs.exists(normalizedPath)) {
|
|
137
|
+
return fs.stat(normalizedPath).isFile;
|
|
138
|
+
}
|
|
139
|
+
} catch {}
|
|
140
|
+
return getLibContent(fileName, libFiles) !== undefined;
|
|
141
|
+
},
|
|
142
|
+
readFile(fileName) {
|
|
143
|
+
const normalizedPath = normalizePath(fileName);
|
|
144
|
+
try {
|
|
145
|
+
if (fs.exists(normalizedPath)) {
|
|
146
|
+
return fs.readFile(normalizedPath);
|
|
147
|
+
}
|
|
148
|
+
} catch {}
|
|
149
|
+
return getLibContent(fileName, libFiles);
|
|
150
|
+
},
|
|
151
|
+
directoryExists(directoryName) {
|
|
152
|
+
const normalizedDir = normalizePath(directoryName);
|
|
153
|
+
try {
|
|
154
|
+
if (fs.exists(normalizedDir)) {
|
|
155
|
+
return fs.stat(normalizedDir).isDirectory;
|
|
156
|
+
}
|
|
157
|
+
} catch {}
|
|
158
|
+
if (normalizedDir === "/node_modules/typescript/lib" || normalizedDir === "/node_modules/typescript" || normalizedDir === "/node_modules") {
|
|
159
|
+
return libFiles.size > 0;
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
},
|
|
163
|
+
getDirectories(path) {
|
|
164
|
+
const normalizedPath = normalizePath(path);
|
|
165
|
+
try {
|
|
166
|
+
if (!fs.exists(normalizedPath)) {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
const stat = fs.stat(normalizedPath);
|
|
170
|
+
if (!stat.isDirectory) {
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
const entries = fs.readdir(normalizedPath);
|
|
174
|
+
const dirs = [];
|
|
175
|
+
for (const name of entries) {
|
|
176
|
+
const childPath = normalizedPath === "/" ? `/${name}` : `${normalizedPath}/${name}`;
|
|
177
|
+
try {
|
|
178
|
+
if (fs.stat(childPath).isDirectory) {
|
|
179
|
+
dirs.push(name);
|
|
180
|
+
}
|
|
181
|
+
} catch {}
|
|
182
|
+
}
|
|
183
|
+
return dirs;
|
|
184
|
+
} catch {
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
realpath(path) {
|
|
189
|
+
return path;
|
|
190
|
+
},
|
|
191
|
+
getEnvironmentVariable() {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function getDefaultCompilerOptions() {
|
|
197
|
+
return {
|
|
198
|
+
target: ts.ScriptTarget.ES2020,
|
|
199
|
+
module: ts.ModuleKind.ESNext,
|
|
200
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
201
|
+
esModuleInterop: true,
|
|
202
|
+
strict: true,
|
|
203
|
+
skipLibCheck: true,
|
|
204
|
+
noEmit: true,
|
|
205
|
+
jsx: ts.JsxEmit.ReactJSX,
|
|
206
|
+
allowJs: true,
|
|
207
|
+
resolveJsonModule: true,
|
|
208
|
+
lib: ["lib.es2020.d.ts", "lib.dom.d.ts", "lib.dom.iterable.d.ts"]
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function parseTsConfig(fs, configPath) {
|
|
212
|
+
try {
|
|
213
|
+
if (!fs.exists(configPath)) {
|
|
214
|
+
return getDefaultCompilerOptions();
|
|
215
|
+
}
|
|
216
|
+
const configText = fs.readFile(configPath);
|
|
217
|
+
const { config, error } = ts.parseConfigFileTextToJson(configPath, configText);
|
|
218
|
+
if (error) {
|
|
219
|
+
console.warn("[typechecker] Error parsing tsconfig:", error.messageText);
|
|
220
|
+
return getDefaultCompilerOptions();
|
|
221
|
+
}
|
|
222
|
+
const parseHost = {
|
|
223
|
+
useCaseSensitiveFileNames: true,
|
|
224
|
+
readDirectory: () => [],
|
|
225
|
+
fileExists: (path) => fs.exists(normalizePath(path)),
|
|
226
|
+
readFile: (path) => {
|
|
227
|
+
try {
|
|
228
|
+
return fs.readFile(normalizePath(path));
|
|
229
|
+
} catch {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
const parsed = ts.parseJsonConfigFileContent(config, parseHost, "/", undefined, configPath);
|
|
235
|
+
const relevantErrors = parsed.errors.filter((e) => e.code !== 18003);
|
|
236
|
+
if (relevantErrors.length > 0) {
|
|
237
|
+
console.warn("[typechecker] tsconfig parse errors:", relevantErrors.map((e) => e.messageText));
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
...parsed.options,
|
|
241
|
+
noEmit: true
|
|
242
|
+
};
|
|
243
|
+
} catch (err) {
|
|
244
|
+
console.warn("[typechecker] Error reading tsconfig:", err);
|
|
245
|
+
return getDefaultCompilerOptions();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function categoryToSeverity(category) {
|
|
249
|
+
switch (category) {
|
|
250
|
+
case ts.DiagnosticCategory.Error:
|
|
251
|
+
return "error";
|
|
252
|
+
case ts.DiagnosticCategory.Warning:
|
|
253
|
+
return "warning";
|
|
254
|
+
default:
|
|
255
|
+
return "info";
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function convertDiagnostic(diag) {
|
|
259
|
+
let file;
|
|
260
|
+
let line;
|
|
261
|
+
let column;
|
|
262
|
+
if (diag.file && diag.start !== undefined) {
|
|
263
|
+
file = diag.file.fileName;
|
|
264
|
+
const pos = diag.file.getLineAndCharacterOfPosition(diag.start);
|
|
265
|
+
line = pos.line + 1;
|
|
266
|
+
column = pos.character + 1;
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
file,
|
|
270
|
+
line,
|
|
271
|
+
column,
|
|
272
|
+
message: ts.flattenDiagnosticMessageText(diag.messageText, `
|
|
273
|
+
`),
|
|
274
|
+
severity: categoryToSeverity(diag.category)
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
class Typechecker {
|
|
279
|
+
options;
|
|
280
|
+
libCache = new Map;
|
|
281
|
+
initPromise = null;
|
|
282
|
+
constructor(options = {}) {
|
|
283
|
+
this.options = options;
|
|
284
|
+
}
|
|
285
|
+
async initialize() {
|
|
286
|
+
if (this.initPromise) {
|
|
287
|
+
await this.initPromise;
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (this.libCache.size > 0) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
this.initPromise = this.fetchLibs();
|
|
294
|
+
await this.initPromise;
|
|
295
|
+
}
|
|
296
|
+
async fetchLibs() {
|
|
297
|
+
const libs = this.options.libs ?? DEFAULT_LIBS;
|
|
298
|
+
const baseUrl = this.options.libsBaseUrl ?? DEFAULT_CDN_BASE;
|
|
299
|
+
console.log(`[typechecker] Fetching TypeScript libs: ${libs.join(", ")}...`);
|
|
300
|
+
const fetched = await fetchAllLibs(libs, baseUrl);
|
|
301
|
+
console.log(`[typechecker] Fetched ${fetched.size} lib files`);
|
|
302
|
+
this.libCache = fetched;
|
|
303
|
+
}
|
|
304
|
+
async typecheck(options) {
|
|
305
|
+
await this.initialize();
|
|
306
|
+
const { fs, entryPoint, tsconfigPath = "/tsconfig.json" } = options;
|
|
307
|
+
const normalizedEntry = normalizePath(entryPoint);
|
|
308
|
+
if (!fs.exists(normalizedEntry)) {
|
|
309
|
+
return {
|
|
310
|
+
success: false,
|
|
311
|
+
diagnostics: [
|
|
312
|
+
{
|
|
313
|
+
file: normalizedEntry,
|
|
314
|
+
message: `Entry point not found: ${normalizedEntry}`,
|
|
315
|
+
severity: "error"
|
|
316
|
+
}
|
|
317
|
+
]
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
const compilerOptions = parseTsConfig(fs, tsconfigPath);
|
|
321
|
+
const host = createCompilerHost(fs, this.libCache, compilerOptions);
|
|
322
|
+
const program = ts.createProgram([normalizedEntry], compilerOptions, host);
|
|
323
|
+
const allDiagnostics = [
|
|
324
|
+
...program.getSyntacticDiagnostics(),
|
|
325
|
+
...program.getSemanticDiagnostics(),
|
|
326
|
+
...program.getDeclarationDiagnostics()
|
|
327
|
+
];
|
|
328
|
+
const diagnostics = allDiagnostics.map(convertDiagnostic);
|
|
329
|
+
const success = !diagnostics.some((d) => d.severity === "error");
|
|
330
|
+
return {
|
|
331
|
+
success,
|
|
332
|
+
diagnostics
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function createTypechecker(options) {
|
|
337
|
+
return new Typechecker(options);
|
|
338
|
+
}
|
|
339
|
+
// src/core/bundler-utils.ts
|
|
340
|
+
function isEsbuildBuildFailure(err) {
|
|
341
|
+
return typeof err === "object" && err !== null && "errors" in err && Array.isArray(err.errors);
|
|
342
|
+
}
|
|
343
|
+
function convertEsbuildMessage(msg) {
|
|
344
|
+
let location;
|
|
345
|
+
if (msg.location) {
|
|
346
|
+
location = {
|
|
347
|
+
file: msg.location.file,
|
|
348
|
+
line: msg.location.line,
|
|
349
|
+
column: msg.location.column,
|
|
350
|
+
lineText: msg.location.lineText
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
text: msg.text,
|
|
355
|
+
location
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
function getRegistryKey(registry) {
|
|
359
|
+
return registry?.registryKey ?? null;
|
|
360
|
+
}
|
|
361
|
+
function createVfsPlugin(options) {
|
|
362
|
+
const {
|
|
363
|
+
fs,
|
|
364
|
+
entryPoint,
|
|
365
|
+
installedPackages,
|
|
366
|
+
sharedModules,
|
|
367
|
+
sharedModuleRegistry,
|
|
368
|
+
cdnBaseUrl,
|
|
369
|
+
includedFiles,
|
|
370
|
+
bundleCdnImports = false
|
|
371
|
+
} = options;
|
|
372
|
+
return {
|
|
373
|
+
name: "sandlot-vfs",
|
|
374
|
+
setup(build) {
|
|
375
|
+
build.onResolve({ filter: /.*/ }, async (args) => {
|
|
376
|
+
if (args.namespace === "http") {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (args.kind === "entry-point") {
|
|
380
|
+
return { path: entryPoint, namespace: "vfs" };
|
|
381
|
+
}
|
|
382
|
+
if (args.path.startsWith("http://") || args.path.startsWith("https://")) {
|
|
383
|
+
if (bundleCdnImports) {
|
|
384
|
+
return { path: args.path, namespace: "http" };
|
|
385
|
+
}
|
|
386
|
+
return { path: args.path, external: true };
|
|
387
|
+
}
|
|
388
|
+
if (isBareImport(args.path)) {
|
|
389
|
+
const sharedMatch = matchSharedModule(args.path, sharedModules);
|
|
390
|
+
if (sharedMatch) {
|
|
391
|
+
return { path: sharedMatch, namespace: "sandlot-shared" };
|
|
392
|
+
}
|
|
393
|
+
const cdnUrl = resolveToEsmUrl(args.path, installedPackages, cdnBaseUrl);
|
|
394
|
+
if (cdnUrl) {
|
|
395
|
+
if (bundleCdnImports) {
|
|
396
|
+
return { path: cdnUrl, namespace: "http" };
|
|
397
|
+
}
|
|
398
|
+
return { path: cdnUrl, external: true };
|
|
399
|
+
}
|
|
400
|
+
return { path: args.path, external: true };
|
|
401
|
+
}
|
|
402
|
+
const resolved = resolveVfsPath(fs, args.resolveDir, args.path);
|
|
403
|
+
if (resolved) {
|
|
404
|
+
return { path: resolved, namespace: "vfs" };
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
errors: [{ text: `Cannot resolve: ${args.path} from ${args.resolveDir}` }]
|
|
408
|
+
};
|
|
409
|
+
});
|
|
410
|
+
build.onLoad({ filter: /.*/, namespace: "vfs" }, async (args) => {
|
|
411
|
+
try {
|
|
412
|
+
const contents = fs.readFile(args.path);
|
|
413
|
+
includedFiles.add(args.path);
|
|
414
|
+
return {
|
|
415
|
+
contents,
|
|
416
|
+
loader: getLoader(args.path),
|
|
417
|
+
resolveDir: dirname(args.path)
|
|
418
|
+
};
|
|
419
|
+
} catch (err) {
|
|
420
|
+
return {
|
|
421
|
+
errors: [{ text: `Failed to read ${args.path}: ${err}` }]
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
build.onLoad({ filter: /.*/, namespace: "sandlot-shared" }, (args) => {
|
|
426
|
+
const moduleId = args.path;
|
|
427
|
+
const runtimeCode = generateSharedModuleCode(moduleId, sharedModuleRegistry);
|
|
428
|
+
return {
|
|
429
|
+
contents: runtimeCode,
|
|
430
|
+
loader: "js"
|
|
431
|
+
};
|
|
432
|
+
});
|
|
433
|
+
if (bundleCdnImports) {
|
|
434
|
+
build.onResolve({ filter: /.*/, namespace: "http" }, (args) => {
|
|
435
|
+
const importerUrl = args.importer;
|
|
436
|
+
if (args.path.startsWith("node:")) {
|
|
437
|
+
return { path: args.path, external: true };
|
|
438
|
+
}
|
|
439
|
+
if (args.path.startsWith("http://") || args.path.startsWith("https://")) {
|
|
440
|
+
return { path: args.path, namespace: "http" };
|
|
441
|
+
}
|
|
442
|
+
if (args.path.startsWith("/")) {
|
|
443
|
+
const origin = new URL(importerUrl).origin;
|
|
444
|
+
return { path: origin + args.path, namespace: "http" };
|
|
445
|
+
}
|
|
446
|
+
if (args.path.startsWith(".")) {
|
|
447
|
+
const resolved = new URL(args.path, importerUrl).href;
|
|
448
|
+
return { path: resolved, namespace: "http" };
|
|
449
|
+
}
|
|
450
|
+
const cdnUrl = resolveToEsmUrl(args.path, installedPackages, cdnBaseUrl);
|
|
451
|
+
if (cdnUrl) {
|
|
452
|
+
return { path: cdnUrl, namespace: "http" };
|
|
453
|
+
}
|
|
454
|
+
const fallbackUrl = `${cdnBaseUrl}/${args.path}`;
|
|
455
|
+
return { path: fallbackUrl, namespace: "http" };
|
|
456
|
+
});
|
|
457
|
+
build.onLoad({ filter: /.*/, namespace: "http" }, async (args) => {
|
|
458
|
+
try {
|
|
459
|
+
const response = await fetch(args.path);
|
|
460
|
+
if (!response.ok) {
|
|
461
|
+
return {
|
|
462
|
+
errors: [{ text: `Failed to fetch ${args.path}: ${response.status} ${response.statusText}` }]
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
const contents = await response.text();
|
|
466
|
+
const loader = getLoaderFromUrl(args.path);
|
|
467
|
+
return {
|
|
468
|
+
contents,
|
|
469
|
+
loader
|
|
470
|
+
};
|
|
471
|
+
} catch (err) {
|
|
472
|
+
return {
|
|
473
|
+
errors: [{ text: `Failed to fetch ${args.path}: ${err}` }]
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function getLoaderFromUrl(url) {
|
|
482
|
+
try {
|
|
483
|
+
const pathname = new URL(url).pathname;
|
|
484
|
+
return getLoader(pathname);
|
|
485
|
+
} catch {
|
|
486
|
+
return "js";
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
function isBareImport(path) {
|
|
490
|
+
return !path.startsWith(".") && !path.startsWith("/");
|
|
491
|
+
}
|
|
492
|
+
function matchSharedModule(importPath, sharedModules) {
|
|
493
|
+
if (sharedModules.has(importPath)) {
|
|
494
|
+
return importPath;
|
|
495
|
+
}
|
|
496
|
+
for (const moduleId of sharedModules) {
|
|
497
|
+
if (importPath.startsWith(moduleId + "/")) {
|
|
498
|
+
if (sharedModules.has(importPath)) {
|
|
499
|
+
return importPath;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
function parseImportPath(importPath) {
|
|
506
|
+
if (importPath.startsWith("@")) {
|
|
507
|
+
const parts = importPath.split("/");
|
|
508
|
+
if (parts.length >= 2) {
|
|
509
|
+
const packageName = `${parts[0]}/${parts[1]}`;
|
|
510
|
+
const subpath = parts.length > 2 ? parts.slice(2).join("/") : undefined;
|
|
511
|
+
return { packageName, subpath };
|
|
512
|
+
}
|
|
513
|
+
return { packageName: importPath };
|
|
514
|
+
}
|
|
515
|
+
const slashIndex = importPath.indexOf("/");
|
|
516
|
+
if (slashIndex === -1) {
|
|
517
|
+
return { packageName: importPath };
|
|
518
|
+
}
|
|
519
|
+
return {
|
|
520
|
+
packageName: importPath.slice(0, slashIndex),
|
|
521
|
+
subpath: importPath.slice(slashIndex + 1)
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
function resolveToEsmUrl(importPath, installedPackages, cdnBaseUrl) {
|
|
525
|
+
const { packageName, subpath } = parseImportPath(importPath);
|
|
526
|
+
const version = installedPackages[packageName];
|
|
527
|
+
if (!version) {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
const baseUrl = `${cdnBaseUrl}/${packageName}@${version}`;
|
|
531
|
+
return subpath ? `${baseUrl}/${subpath}` : baseUrl;
|
|
532
|
+
}
|
|
533
|
+
function resolveVfsPath(fs, resolveDir, importPath) {
|
|
534
|
+
const resolved = resolvePath(resolveDir, importPath);
|
|
535
|
+
const extensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".json"];
|
|
536
|
+
const hasExtension = extensions.some((ext) => resolved.endsWith(ext));
|
|
537
|
+
if (hasExtension) {
|
|
538
|
+
if (fs.exists(resolved)) {
|
|
539
|
+
return resolved;
|
|
540
|
+
}
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
for (const ext of extensions) {
|
|
544
|
+
const withExt = resolved + ext;
|
|
545
|
+
if (fs.exists(withExt)) {
|
|
546
|
+
return withExt;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
for (const ext of extensions) {
|
|
550
|
+
const indexPath = `${resolved}/index${ext}`;
|
|
551
|
+
if (fs.exists(indexPath)) {
|
|
552
|
+
return indexPath;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
function resolvePath(from, to) {
|
|
558
|
+
if (to.startsWith("/")) {
|
|
559
|
+
return normalizePath2(to);
|
|
560
|
+
}
|
|
561
|
+
const fromParts = from.split("/").filter(Boolean);
|
|
562
|
+
const toParts = to.split("/");
|
|
563
|
+
const result = [...fromParts];
|
|
564
|
+
for (const part of toParts) {
|
|
565
|
+
if (part === "." || part === "") {
|
|
566
|
+
continue;
|
|
567
|
+
} else if (part === "..") {
|
|
568
|
+
result.pop();
|
|
569
|
+
} else {
|
|
570
|
+
result.push(part);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return "/" + result.join("/");
|
|
574
|
+
}
|
|
575
|
+
function normalizePath2(path) {
|
|
576
|
+
const parts = path.split("/").filter(Boolean);
|
|
577
|
+
const result = [];
|
|
578
|
+
for (const part of parts) {
|
|
579
|
+
if (part === ".") {
|
|
580
|
+
continue;
|
|
581
|
+
} else if (part === "..") {
|
|
582
|
+
result.pop();
|
|
583
|
+
} else {
|
|
584
|
+
result.push(part);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return "/" + result.join("/");
|
|
588
|
+
}
|
|
589
|
+
function dirname(path) {
|
|
590
|
+
const lastSlash = path.lastIndexOf("/");
|
|
591
|
+
if (lastSlash <= 0)
|
|
592
|
+
return "/";
|
|
593
|
+
return path.slice(0, lastSlash);
|
|
594
|
+
}
|
|
595
|
+
function getLoader(path) {
|
|
596
|
+
const ext = path.split(".").pop()?.toLowerCase();
|
|
597
|
+
switch (ext) {
|
|
598
|
+
case "ts":
|
|
599
|
+
return "ts";
|
|
600
|
+
case "tsx":
|
|
601
|
+
return "tsx";
|
|
602
|
+
case "jsx":
|
|
603
|
+
return "jsx";
|
|
604
|
+
case "js":
|
|
605
|
+
case "mjs":
|
|
606
|
+
return "js";
|
|
607
|
+
case "json":
|
|
608
|
+
return "json";
|
|
609
|
+
case "css":
|
|
610
|
+
return "css";
|
|
611
|
+
case "txt":
|
|
612
|
+
return "text";
|
|
613
|
+
default:
|
|
614
|
+
return "js";
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
function generateSharedModuleCode(moduleId, registry) {
|
|
618
|
+
const registryKey = getRegistryKey(registry);
|
|
619
|
+
if (!registryKey) {
|
|
620
|
+
return `throw new Error("Shared module '${moduleId}' requested but no registry configured");`;
|
|
621
|
+
}
|
|
622
|
+
const runtimeAccess = `
|
|
623
|
+
(function() {
|
|
624
|
+
var registry = globalThis["${registryKey}"];
|
|
625
|
+
if (!registry) {
|
|
626
|
+
throw new Error(
|
|
627
|
+
'Sandlot SharedModuleRegistry not found at "${registryKey}". ' +
|
|
628
|
+
'Ensure sharedModules are configured in createSandlot() options.'
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
return registry.get(${JSON.stringify(moduleId)});
|
|
632
|
+
})()
|
|
633
|
+
`.trim();
|
|
634
|
+
const exportNames = registry?.getExportNames(moduleId) ?? [];
|
|
635
|
+
let code = `const __sandlot_mod__ = ${runtimeAccess};
|
|
636
|
+
`;
|
|
637
|
+
code += `export default __sandlot_mod__.default ?? __sandlot_mod__;
|
|
638
|
+
`;
|
|
639
|
+
if (exportNames.length > 0) {
|
|
640
|
+
for (const name of exportNames) {
|
|
641
|
+
code += `export const ${name} = __sandlot_mod__.${name};
|
|
642
|
+
`;
|
|
643
|
+
}
|
|
644
|
+
} else {
|
|
645
|
+
code += `// No named exports discovered for "${moduleId}"
|
|
646
|
+
`;
|
|
647
|
+
code += `// Use: import mod from "${moduleId}"; mod.exportName
|
|
648
|
+
`;
|
|
649
|
+
}
|
|
650
|
+
return code;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// src/browser/bundler.ts
|
|
654
|
+
var ESBUILD_VERSION = "0.27.2";
|
|
655
|
+
var GLOBAL_KEY = "__sandlot_esbuild__";
|
|
656
|
+
function getGlobalState() {
|
|
657
|
+
const g = globalThis;
|
|
658
|
+
if (!g[GLOBAL_KEY]) {
|
|
659
|
+
g[GLOBAL_KEY] = {
|
|
660
|
+
esbuild: null,
|
|
661
|
+
initialized: false,
|
|
662
|
+
initPromise: null
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
return g[GLOBAL_KEY];
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
class EsbuildWasmBundler {
|
|
669
|
+
options;
|
|
670
|
+
constructor(options = {}) {
|
|
671
|
+
this.options = {
|
|
672
|
+
cdnBaseUrl: "https://esm.sh",
|
|
673
|
+
...options
|
|
674
|
+
};
|
|
675
|
+
if (options.eagerInit) {
|
|
676
|
+
this.initialize();
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
async initialize() {
|
|
680
|
+
const state = getGlobalState();
|
|
681
|
+
if (state.initialized && state.esbuild) {
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (state.initPromise) {
|
|
685
|
+
await state.initPromise;
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
state.initPromise = this.doInitialize(state);
|
|
689
|
+
await state.initPromise;
|
|
690
|
+
}
|
|
691
|
+
async doInitialize(state) {
|
|
692
|
+
this.checkCrossOriginIsolation();
|
|
693
|
+
const esbuildUrl = this.options.esbuildUrl ?? `https://esm.sh/esbuild-wasm@${ESBUILD_VERSION}`;
|
|
694
|
+
const mod = await import(esbuildUrl);
|
|
695
|
+
const esbuild = mod.default ?? mod;
|
|
696
|
+
if (typeof esbuild?.initialize !== "function") {
|
|
697
|
+
throw new Error("Failed to load esbuild-wasm: initialize function not found");
|
|
698
|
+
}
|
|
699
|
+
const wasmUrl = this.options.wasmUrl ?? `https://unpkg.com/esbuild-wasm@${ESBUILD_VERSION}/esbuild.wasm`;
|
|
700
|
+
await esbuild.initialize({ wasmURL: wasmUrl });
|
|
701
|
+
state.esbuild = esbuild;
|
|
702
|
+
state.initialized = true;
|
|
703
|
+
}
|
|
704
|
+
getEsbuild() {
|
|
705
|
+
const state = getGlobalState();
|
|
706
|
+
if (!state.esbuild) {
|
|
707
|
+
throw new Error("esbuild not initialized - call initialize() first");
|
|
708
|
+
}
|
|
709
|
+
return state.esbuild;
|
|
710
|
+
}
|
|
711
|
+
checkCrossOriginIsolation() {
|
|
712
|
+
if (typeof window === "undefined")
|
|
713
|
+
return;
|
|
714
|
+
if (!window.crossOriginIsolated) {
|
|
715
|
+
console.warn(`[sandlot] Cross-origin isolation is not enabled. esbuild-wasm may have reduced performance or fail on some browsers.
|
|
716
|
+
To enable, add these headers to your server:
|
|
717
|
+
Cross-Origin-Embedder-Policy: require-corp
|
|
718
|
+
Cross-Origin-Opener-Policy: same-origin`);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
async bundle(options) {
|
|
722
|
+
await this.initialize();
|
|
723
|
+
const esbuild = this.getEsbuild();
|
|
724
|
+
const {
|
|
725
|
+
fs,
|
|
726
|
+
entryPoint,
|
|
727
|
+
installedPackages = {},
|
|
728
|
+
sharedModules = [],
|
|
729
|
+
sharedModuleRegistry,
|
|
730
|
+
external = [],
|
|
731
|
+
format = "esm",
|
|
732
|
+
minify = false,
|
|
733
|
+
sourcemap = false,
|
|
734
|
+
target = ["es2020"]
|
|
735
|
+
} = options;
|
|
736
|
+
const normalizedEntry = entryPoint.startsWith("/") ? entryPoint : `/${entryPoint}`;
|
|
737
|
+
if (!fs.exists(normalizedEntry)) {
|
|
738
|
+
return {
|
|
739
|
+
success: false,
|
|
740
|
+
errors: [{ text: `Entry point not found: ${normalizedEntry}` }],
|
|
741
|
+
warnings: []
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
const includedFiles = new Set;
|
|
745
|
+
const plugin = createVfsPlugin({
|
|
746
|
+
fs,
|
|
747
|
+
entryPoint: normalizedEntry,
|
|
748
|
+
installedPackages,
|
|
749
|
+
sharedModules: new Set(sharedModules),
|
|
750
|
+
sharedModuleRegistry: sharedModuleRegistry ?? null,
|
|
751
|
+
cdnBaseUrl: this.options.cdnBaseUrl,
|
|
752
|
+
includedFiles
|
|
753
|
+
});
|
|
754
|
+
try {
|
|
755
|
+
const result = await esbuild.build({
|
|
756
|
+
entryPoints: [normalizedEntry],
|
|
757
|
+
bundle: true,
|
|
758
|
+
write: false,
|
|
759
|
+
format,
|
|
760
|
+
minify,
|
|
761
|
+
sourcemap: sourcemap ? "inline" : false,
|
|
762
|
+
target,
|
|
763
|
+
external,
|
|
764
|
+
plugins: [plugin],
|
|
765
|
+
jsx: "automatic"
|
|
766
|
+
});
|
|
767
|
+
const code = result.outputFiles?.[0]?.text ?? "";
|
|
768
|
+
const warnings = result.warnings.map((w) => convertEsbuildMessage(w));
|
|
769
|
+
return {
|
|
770
|
+
success: true,
|
|
771
|
+
code,
|
|
772
|
+
warnings,
|
|
773
|
+
includedFiles: Array.from(includedFiles)
|
|
774
|
+
};
|
|
775
|
+
} catch (err) {
|
|
776
|
+
if (isEsbuildBuildFailure(err)) {
|
|
777
|
+
const errors = err.errors.map((e) => convertEsbuildMessage(e));
|
|
778
|
+
const warnings = err.warnings.map((w) => convertEsbuildMessage(w));
|
|
779
|
+
return {
|
|
780
|
+
success: false,
|
|
781
|
+
errors,
|
|
782
|
+
warnings
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
786
|
+
return {
|
|
787
|
+
success: false,
|
|
788
|
+
errors: [{ text: message }],
|
|
789
|
+
warnings: []
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
// src/core/executor.ts
|
|
795
|
+
function createBasicExecutor(loadModule, options = {}) {
|
|
796
|
+
const defaultTimeout = options.defaultTimeout ?? 30000;
|
|
797
|
+
return {
|
|
798
|
+
async execute(code, execOptions = {}) {
|
|
799
|
+
const {
|
|
800
|
+
entryExport = "main",
|
|
801
|
+
context = {},
|
|
802
|
+
timeout = defaultTimeout
|
|
803
|
+
} = execOptions;
|
|
804
|
+
const startTime = performance.now();
|
|
805
|
+
const logs = [];
|
|
806
|
+
const originalConsole = {
|
|
807
|
+
log: console.log,
|
|
808
|
+
warn: console.warn,
|
|
809
|
+
error: console.error,
|
|
810
|
+
info: console.info,
|
|
811
|
+
debug: console.debug
|
|
812
|
+
};
|
|
813
|
+
const formatArgs = (...args) => args.map((v) => typeof v === "object" ? JSON.stringify(v) : String(v)).join(" ");
|
|
814
|
+
const captureLog = (...args) => {
|
|
815
|
+
logs.push(formatArgs(...args));
|
|
816
|
+
originalConsole.log.apply(console, args);
|
|
817
|
+
};
|
|
818
|
+
const captureWarn = (...args) => {
|
|
819
|
+
logs.push(`[warn] ${formatArgs(...args)}`);
|
|
820
|
+
originalConsole.warn.apply(console, args);
|
|
821
|
+
};
|
|
822
|
+
const captureError = (...args) => {
|
|
823
|
+
logs.push(`[error] ${formatArgs(...args)}`);
|
|
824
|
+
originalConsole.error.apply(console, args);
|
|
825
|
+
};
|
|
826
|
+
const captureInfo = (...args) => {
|
|
827
|
+
logs.push(`[info] ${formatArgs(...args)}`);
|
|
828
|
+
originalConsole.info.apply(console, args);
|
|
829
|
+
};
|
|
830
|
+
const captureDebug = (...args) => {
|
|
831
|
+
logs.push(`[debug] ${formatArgs(...args)}`);
|
|
832
|
+
originalConsole.debug.apply(console, args);
|
|
833
|
+
};
|
|
834
|
+
const restoreConsole = () => {
|
|
835
|
+
console.log = originalConsole.log;
|
|
836
|
+
console.warn = originalConsole.warn;
|
|
837
|
+
console.error = originalConsole.error;
|
|
838
|
+
console.info = originalConsole.info;
|
|
839
|
+
console.debug = originalConsole.debug;
|
|
840
|
+
};
|
|
841
|
+
console.log = captureLog;
|
|
842
|
+
console.warn = captureWarn;
|
|
843
|
+
console.error = captureError;
|
|
844
|
+
console.info = captureInfo;
|
|
845
|
+
console.debug = captureDebug;
|
|
846
|
+
try {
|
|
847
|
+
const module = await loadModule(code);
|
|
848
|
+
let returnValue;
|
|
849
|
+
const executeExport = async () => {
|
|
850
|
+
if (entryExport === "main" && typeof module.main === "function") {
|
|
851
|
+
returnValue = await module.main(context);
|
|
852
|
+
} else if (entryExport === "default" && typeof module.default === "function") {
|
|
853
|
+
returnValue = await module.default();
|
|
854
|
+
} else if (entryExport === "default" && module.default !== undefined) {
|
|
855
|
+
returnValue = module.default;
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
if (timeout > 0) {
|
|
859
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
860
|
+
setTimeout(() => reject(new Error(`Execution timed out after ${timeout}ms`)), timeout);
|
|
861
|
+
});
|
|
862
|
+
await Promise.race([executeExport(), timeoutPromise]);
|
|
863
|
+
} else {
|
|
864
|
+
await executeExport();
|
|
865
|
+
}
|
|
866
|
+
const executionTimeMs = performance.now() - startTime;
|
|
867
|
+
restoreConsole();
|
|
868
|
+
return {
|
|
869
|
+
success: true,
|
|
870
|
+
logs,
|
|
871
|
+
returnValue,
|
|
872
|
+
executionTimeMs
|
|
873
|
+
};
|
|
874
|
+
} catch (err) {
|
|
875
|
+
const executionTimeMs = performance.now() - startTime;
|
|
876
|
+
restoreConsole();
|
|
877
|
+
return {
|
|
878
|
+
success: false,
|
|
879
|
+
logs,
|
|
880
|
+
error: err instanceof Error ? err.message : String(err),
|
|
881
|
+
executionTimeMs
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// src/browser/executor.ts
|
|
889
|
+
async function loadModuleFromBlobUrl(code) {
|
|
890
|
+
const blob = new Blob([code], { type: "application/javascript" });
|
|
891
|
+
const url = URL.createObjectURL(blob);
|
|
892
|
+
try {
|
|
893
|
+
return await import(url);
|
|
894
|
+
} finally {
|
|
895
|
+
URL.revokeObjectURL(url);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
class MainThreadExecutor {
|
|
900
|
+
executor;
|
|
901
|
+
constructor(options = {}) {
|
|
902
|
+
this.executor = createBasicExecutor(loadModuleFromBlobUrl, options);
|
|
903
|
+
}
|
|
904
|
+
execute = (...args) => this.executor.execute(...args);
|
|
905
|
+
}
|
|
906
|
+
function createMainThreadExecutor(options) {
|
|
907
|
+
return new MainThreadExecutor(options);
|
|
908
|
+
}
|
|
909
|
+
// src/core/shared-module-registry.ts
|
|
910
|
+
var instanceCounter = 0;
|
|
911
|
+
function generateInstanceId() {
|
|
912
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
913
|
+
return crypto.randomUUID().slice(0, 8);
|
|
914
|
+
}
|
|
915
|
+
return `${Date.now().toString(36)}_${(++instanceCounter).toString(36)}`;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
class SharedModuleRegistry {
|
|
919
|
+
modules;
|
|
920
|
+
exportNamesMap;
|
|
921
|
+
_registryKey;
|
|
922
|
+
constructor(modules) {
|
|
923
|
+
this._registryKey = `__sandlot_${generateInstanceId()}__`;
|
|
924
|
+
this.modules = new Map(Object.entries(modules));
|
|
925
|
+
this.exportNamesMap = new Map;
|
|
926
|
+
for (const [id, mod] of this.modules) {
|
|
927
|
+
this.exportNamesMap.set(id, this.introspectExports(mod));
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
get registryKey() {
|
|
931
|
+
return this._registryKey;
|
|
932
|
+
}
|
|
933
|
+
exposeGlobally() {
|
|
934
|
+
globalThis[this._registryKey] = this;
|
|
935
|
+
return this;
|
|
936
|
+
}
|
|
937
|
+
removeFromGlobal() {
|
|
938
|
+
if (globalThis[this._registryKey] === this) {
|
|
939
|
+
delete globalThis[this._registryKey];
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
get(moduleId) {
|
|
943
|
+
const mod = this.modules.get(moduleId);
|
|
944
|
+
if (mod === undefined && !this.modules.has(moduleId)) {
|
|
945
|
+
throw new Error(`Shared module "${moduleId}" not registered.`);
|
|
946
|
+
}
|
|
947
|
+
return mod;
|
|
948
|
+
}
|
|
949
|
+
has(moduleId) {
|
|
950
|
+
return this.modules.has(moduleId);
|
|
951
|
+
}
|
|
952
|
+
getExportNames(moduleId) {
|
|
953
|
+
return this.exportNamesMap.get(moduleId) ?? [];
|
|
954
|
+
}
|
|
955
|
+
list() {
|
|
956
|
+
return [...this.modules.keys()];
|
|
957
|
+
}
|
|
958
|
+
introspectExports(module) {
|
|
959
|
+
if (module === null || module === undefined) {
|
|
960
|
+
return [];
|
|
961
|
+
}
|
|
962
|
+
if (typeof module !== "object" && typeof module !== "function") {
|
|
963
|
+
return [];
|
|
964
|
+
}
|
|
965
|
+
const exports = [];
|
|
966
|
+
for (const key of Object.keys(module)) {
|
|
967
|
+
if (this.isValidIdentifier(key)) {
|
|
968
|
+
exports.push(key);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
return exports;
|
|
972
|
+
}
|
|
973
|
+
isValidIdentifier(name) {
|
|
974
|
+
if (name.length === 0)
|
|
975
|
+
return false;
|
|
976
|
+
if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name))
|
|
977
|
+
return false;
|
|
978
|
+
const reserved = [
|
|
979
|
+
"default",
|
|
980
|
+
"class",
|
|
981
|
+
"function",
|
|
982
|
+
"var",
|
|
983
|
+
"let",
|
|
984
|
+
"const",
|
|
985
|
+
"import",
|
|
986
|
+
"export"
|
|
987
|
+
];
|
|
988
|
+
return !reserved.includes(name);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
function createSharedModuleRegistry(modules) {
|
|
992
|
+
if (!modules || Object.keys(modules).length === 0) {
|
|
993
|
+
return null;
|
|
994
|
+
}
|
|
995
|
+
const registry = new SharedModuleRegistry(modules);
|
|
996
|
+
registry.exposeGlobally();
|
|
997
|
+
return registry;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// src/core/sandbox.ts
|
|
1001
|
+
import { Bash } from "just-bash/browser";
|
|
1002
|
+
|
|
1003
|
+
// src/core/fs.ts
|
|
1004
|
+
var DEFAULT_FILE_MODE = 420;
|
|
1005
|
+
var DEFAULT_DIR_MODE = 493;
|
|
1006
|
+
var DEFAULT_SYMLINK_MODE = 511;
|
|
1007
|
+
var DEFAULT_MAX_SIZE_BYTES = 50 * 1024 * 1024;
|
|
1008
|
+
|
|
1009
|
+
class Filesystem {
|
|
1010
|
+
entries;
|
|
1011
|
+
maxSizeBytes;
|
|
1012
|
+
constructor(entries, maxSizeBytes) {
|
|
1013
|
+
this.entries = entries;
|
|
1014
|
+
this.maxSizeBytes = maxSizeBytes;
|
|
1015
|
+
}
|
|
1016
|
+
static create(options = {}) {
|
|
1017
|
+
const maxSizeBytes = options.maxSizeBytes ?? DEFAULT_MAX_SIZE_BYTES;
|
|
1018
|
+
const entries = new Map;
|
|
1019
|
+
entries.set("/", {
|
|
1020
|
+
type: "directory",
|
|
1021
|
+
mode: DEFAULT_DIR_MODE,
|
|
1022
|
+
mtime: new Date
|
|
1023
|
+
});
|
|
1024
|
+
if (options.initialFiles) {
|
|
1025
|
+
for (const [path, value] of Object.entries(options.initialFiles)) {
|
|
1026
|
+
const normalizedPath = Filesystem.normalizePath(path);
|
|
1027
|
+
const init = Filesystem.parseFileInit(value);
|
|
1028
|
+
Filesystem.ensureParentDirs(entries, normalizedPath);
|
|
1029
|
+
entries.set(normalizedPath, {
|
|
1030
|
+
type: "file",
|
|
1031
|
+
content: init.content,
|
|
1032
|
+
mode: init.mode ?? DEFAULT_FILE_MODE,
|
|
1033
|
+
mtime: init.mtime ?? new Date
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
return new Filesystem(entries, maxSizeBytes);
|
|
1038
|
+
}
|
|
1039
|
+
getFiles() {
|
|
1040
|
+
const files = {};
|
|
1041
|
+
for (const [path, entry] of this.entries) {
|
|
1042
|
+
if (entry.type === "file") {
|
|
1043
|
+
if (typeof entry.content === "string") {
|
|
1044
|
+
files[path] = entry.content;
|
|
1045
|
+
} else {
|
|
1046
|
+
const base64 = this.encodeBase64(entry.content);
|
|
1047
|
+
files[path] = `data:application/octet-stream;base64,${base64}`;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return files;
|
|
1052
|
+
}
|
|
1053
|
+
getSize() {
|
|
1054
|
+
let size = 0;
|
|
1055
|
+
for (const [path, entry] of this.entries) {
|
|
1056
|
+
size += path.length * 2;
|
|
1057
|
+
if (entry.type === "file") {
|
|
1058
|
+
if (typeof entry.content === "string") {
|
|
1059
|
+
size += entry.content.length * 2;
|
|
1060
|
+
} else {
|
|
1061
|
+
size += entry.content.byteLength;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
return size;
|
|
1066
|
+
}
|
|
1067
|
+
readFile(path, options) {
|
|
1068
|
+
const normalizedPath = this.normalizePath(path);
|
|
1069
|
+
const entry = this.resolveSymlinks(normalizedPath);
|
|
1070
|
+
if (!entry) {
|
|
1071
|
+
throw new Error(`ENOENT: no such file or directory, open '${path}'`);
|
|
1072
|
+
}
|
|
1073
|
+
if (entry.type !== "file") {
|
|
1074
|
+
throw new Error(`EISDIR: illegal operation on a directory, read '${path}'`);
|
|
1075
|
+
}
|
|
1076
|
+
const content = entry.content;
|
|
1077
|
+
if (typeof content === "string") {
|
|
1078
|
+
return content;
|
|
1079
|
+
}
|
|
1080
|
+
const encoding = this.getEncoding(options) ?? "utf8";
|
|
1081
|
+
return this.decodeBuffer(content, encoding);
|
|
1082
|
+
}
|
|
1083
|
+
readFileBuffer(path) {
|
|
1084
|
+
const normalizedPath = this.normalizePath(path);
|
|
1085
|
+
const entry = this.resolveSymlinks(normalizedPath);
|
|
1086
|
+
if (!entry) {
|
|
1087
|
+
throw new Error(`ENOENT: no such file or directory, open '${path}'`);
|
|
1088
|
+
}
|
|
1089
|
+
if (entry.type !== "file") {
|
|
1090
|
+
throw new Error(`EISDIR: illegal operation on a directory, read '${path}'`);
|
|
1091
|
+
}
|
|
1092
|
+
const content = entry.content;
|
|
1093
|
+
if (content instanceof Uint8Array) {
|
|
1094
|
+
return content;
|
|
1095
|
+
}
|
|
1096
|
+
return new TextEncoder().encode(content);
|
|
1097
|
+
}
|
|
1098
|
+
writeFile(path, content, _options) {
|
|
1099
|
+
const normalizedPath = this.normalizePath(path);
|
|
1100
|
+
this.checkSizeLimit(content);
|
|
1101
|
+
this.ensureParentDirs(normalizedPath);
|
|
1102
|
+
const existing = this.entries.get(normalizedPath);
|
|
1103
|
+
if (existing && existing.type === "directory") {
|
|
1104
|
+
throw new Error(`EISDIR: illegal operation on a directory, open '${path}'`);
|
|
1105
|
+
}
|
|
1106
|
+
this.entries.set(normalizedPath, {
|
|
1107
|
+
type: "file",
|
|
1108
|
+
content,
|
|
1109
|
+
mode: existing?.mode ?? DEFAULT_FILE_MODE,
|
|
1110
|
+
mtime: new Date
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
appendFile(path, content, options) {
|
|
1114
|
+
const normalizedPath = this.normalizePath(path);
|
|
1115
|
+
let existing = "";
|
|
1116
|
+
try {
|
|
1117
|
+
existing = this.readFile(normalizedPath);
|
|
1118
|
+
} catch {}
|
|
1119
|
+
const newContent = typeof existing === "string" && typeof content === "string" ? existing + content : this.concatBuffers(typeof existing === "string" ? new TextEncoder().encode(existing) : existing, typeof content === "string" ? new TextEncoder().encode(content) : content);
|
|
1120
|
+
this.writeFile(normalizedPath, newContent, options);
|
|
1121
|
+
}
|
|
1122
|
+
exists(path) {
|
|
1123
|
+
const normalizedPath = this.normalizePath(path);
|
|
1124
|
+
return this.entries.has(normalizedPath);
|
|
1125
|
+
}
|
|
1126
|
+
stat(path) {
|
|
1127
|
+
const normalizedPath = this.normalizePath(path);
|
|
1128
|
+
const entry = this.resolveSymlinks(normalizedPath);
|
|
1129
|
+
if (!entry) {
|
|
1130
|
+
throw new Error(`ENOENT: no such file or directory, stat '${path}'`);
|
|
1131
|
+
}
|
|
1132
|
+
return this.entryToStat(entry);
|
|
1133
|
+
}
|
|
1134
|
+
lstat(path) {
|
|
1135
|
+
const normalizedPath = this.normalizePath(path);
|
|
1136
|
+
const entry = this.entries.get(normalizedPath);
|
|
1137
|
+
if (!entry) {
|
|
1138
|
+
throw new Error(`ENOENT: no such file or directory, lstat '${path}'`);
|
|
1139
|
+
}
|
|
1140
|
+
return this.entryToStat(entry);
|
|
1141
|
+
}
|
|
1142
|
+
mkdir(path, options) {
|
|
1143
|
+
const normalizedPath = this.normalizePath(path);
|
|
1144
|
+
if (this.entries.has(normalizedPath)) {
|
|
1145
|
+
if (options?.recursive) {
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
throw new Error(`EEXIST: file already exists, mkdir '${path}'`);
|
|
1149
|
+
}
|
|
1150
|
+
if (options?.recursive) {
|
|
1151
|
+
this.ensureParentDirs(normalizedPath);
|
|
1152
|
+
} else {
|
|
1153
|
+
const parent = this.getParentPath(normalizedPath);
|
|
1154
|
+
if (parent && !this.entries.has(parent)) {
|
|
1155
|
+
throw new Error(`ENOENT: no such file or directory, mkdir '${path}'`);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
this.entries.set(normalizedPath, {
|
|
1159
|
+
type: "directory",
|
|
1160
|
+
mode: DEFAULT_DIR_MODE,
|
|
1161
|
+
mtime: new Date
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
readdir(path) {
|
|
1165
|
+
const normalizedPath = this.normalizePath(path);
|
|
1166
|
+
const entry = this.resolveSymlinks(normalizedPath);
|
|
1167
|
+
if (!entry) {
|
|
1168
|
+
throw new Error(`ENOENT: no such file or directory, scandir '${path}'`);
|
|
1169
|
+
}
|
|
1170
|
+
if (entry.type !== "directory") {
|
|
1171
|
+
throw new Error(`ENOTDIR: not a directory, scandir '${path}'`);
|
|
1172
|
+
}
|
|
1173
|
+
const prefix = normalizedPath === "/" ? "/" : normalizedPath + "/";
|
|
1174
|
+
const names = [];
|
|
1175
|
+
for (const entryPath of this.entries.keys()) {
|
|
1176
|
+
if (entryPath === normalizedPath)
|
|
1177
|
+
continue;
|
|
1178
|
+
if (!entryPath.startsWith(prefix))
|
|
1179
|
+
continue;
|
|
1180
|
+
const relative = entryPath.slice(prefix.length);
|
|
1181
|
+
if (!relative.includes("/")) {
|
|
1182
|
+
names.push(relative);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
return names.sort();
|
|
1186
|
+
}
|
|
1187
|
+
readdirWithFileTypes(path) {
|
|
1188
|
+
const normalizedPath = this.normalizePath(path);
|
|
1189
|
+
const entry = this.resolveSymlinks(normalizedPath);
|
|
1190
|
+
if (!entry) {
|
|
1191
|
+
throw new Error(`ENOENT: no such file or directory, scandir '${path}'`);
|
|
1192
|
+
}
|
|
1193
|
+
if (entry.type !== "directory") {
|
|
1194
|
+
throw new Error(`ENOTDIR: not a directory, scandir '${path}'`);
|
|
1195
|
+
}
|
|
1196
|
+
const prefix = normalizedPath === "/" ? "/" : normalizedPath + "/";
|
|
1197
|
+
const dirents = [];
|
|
1198
|
+
for (const [entryPath, e] of this.entries) {
|
|
1199
|
+
if (entryPath === normalizedPath)
|
|
1200
|
+
continue;
|
|
1201
|
+
if (!entryPath.startsWith(prefix))
|
|
1202
|
+
continue;
|
|
1203
|
+
const relative = entryPath.slice(prefix.length);
|
|
1204
|
+
if (!relative.includes("/")) {
|
|
1205
|
+
dirents.push({
|
|
1206
|
+
name: relative,
|
|
1207
|
+
isFile: e.type === "file",
|
|
1208
|
+
isDirectory: e.type === "directory",
|
|
1209
|
+
isSymbolicLink: e.type === "symlink"
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
return dirents.sort((a, b) => a.name.localeCompare(b.name));
|
|
1214
|
+
}
|
|
1215
|
+
rm(path, options) {
|
|
1216
|
+
const normalizedPath = this.normalizePath(path);
|
|
1217
|
+
const entry = this.entries.get(normalizedPath);
|
|
1218
|
+
if (!entry) {
|
|
1219
|
+
if (options?.force)
|
|
1220
|
+
return;
|
|
1221
|
+
throw new Error(`ENOENT: no such file or directory, rm '${path}'`);
|
|
1222
|
+
}
|
|
1223
|
+
if (entry.type === "directory") {
|
|
1224
|
+
const children = this.readdir(normalizedPath);
|
|
1225
|
+
if (children.length > 0 && !options?.recursive) {
|
|
1226
|
+
throw new Error(`ENOTEMPTY: directory not empty, rm '${path}'`);
|
|
1227
|
+
}
|
|
1228
|
+
if (options?.recursive) {
|
|
1229
|
+
const prefix = normalizedPath === "/" ? "/" : normalizedPath + "/";
|
|
1230
|
+
for (const entryPath of [...this.entries.keys()]) {
|
|
1231
|
+
if (entryPath.startsWith(prefix)) {
|
|
1232
|
+
this.entries.delete(entryPath);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
this.entries.delete(normalizedPath);
|
|
1238
|
+
}
|
|
1239
|
+
cp(src, dest, options) {
|
|
1240
|
+
const srcPath = this.normalizePath(src);
|
|
1241
|
+
const destPath = this.normalizePath(dest);
|
|
1242
|
+
const entry = this.entries.get(srcPath);
|
|
1243
|
+
if (!entry) {
|
|
1244
|
+
throw new Error(`ENOENT: no such file or directory, cp '${src}'`);
|
|
1245
|
+
}
|
|
1246
|
+
if (entry.type === "directory") {
|
|
1247
|
+
if (!options?.recursive) {
|
|
1248
|
+
throw new Error(`EISDIR: cp called on directory without recursive '${src}'`);
|
|
1249
|
+
}
|
|
1250
|
+
this.ensureParentDirs(destPath);
|
|
1251
|
+
this.entries.set(destPath, { ...entry, mtime: new Date });
|
|
1252
|
+
const prefix = srcPath === "/" ? "/" : srcPath + "/";
|
|
1253
|
+
for (const [entryPath, e] of this.entries) {
|
|
1254
|
+
if (entryPath.startsWith(prefix)) {
|
|
1255
|
+
const relative = entryPath.slice(srcPath.length);
|
|
1256
|
+
const newPath = destPath + relative;
|
|
1257
|
+
this.entries.set(newPath, this.cloneEntry(e));
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
} else {
|
|
1261
|
+
this.ensureParentDirs(destPath);
|
|
1262
|
+
this.entries.set(destPath, this.cloneEntry(entry));
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
mv(src, dest) {
|
|
1266
|
+
const srcPath = this.normalizePath(src);
|
|
1267
|
+
const destPath = this.normalizePath(dest);
|
|
1268
|
+
const entry = this.entries.get(srcPath);
|
|
1269
|
+
if (!entry) {
|
|
1270
|
+
throw new Error(`ENOENT: no such file or directory, mv '${src}'`);
|
|
1271
|
+
}
|
|
1272
|
+
this.ensureParentDirs(destPath);
|
|
1273
|
+
if (entry.type === "directory") {
|
|
1274
|
+
const prefix = srcPath === "/" ? "/" : srcPath + "/";
|
|
1275
|
+
const toMove = [];
|
|
1276
|
+
for (const [entryPath, e] of this.entries) {
|
|
1277
|
+
if (entryPath === srcPath || entryPath.startsWith(prefix)) {
|
|
1278
|
+
const relative = entryPath.slice(srcPath.length);
|
|
1279
|
+
toMove.push([destPath + relative, e]);
|
|
1280
|
+
this.entries.delete(entryPath);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
for (const [newPath, e] of toMove) {
|
|
1284
|
+
this.entries.set(newPath, e);
|
|
1285
|
+
}
|
|
1286
|
+
} else {
|
|
1287
|
+
this.entries.delete(srcPath);
|
|
1288
|
+
this.entries.set(destPath, entry);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
resolvePath(base, path) {
|
|
1292
|
+
if (path.startsWith("/")) {
|
|
1293
|
+
return this.normalizePath(path);
|
|
1294
|
+
}
|
|
1295
|
+
const baseParts = base.split("/").filter(Boolean);
|
|
1296
|
+
const pathParts = path.split("/").filter(Boolean);
|
|
1297
|
+
for (const part of pathParts) {
|
|
1298
|
+
if (part === ".") {
|
|
1299
|
+
continue;
|
|
1300
|
+
} else if (part === "..") {
|
|
1301
|
+
baseParts.pop();
|
|
1302
|
+
} else {
|
|
1303
|
+
baseParts.push(part);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
return "/" + baseParts.join("/");
|
|
1307
|
+
}
|
|
1308
|
+
getAllPaths() {
|
|
1309
|
+
return [...this.entries.keys()].sort();
|
|
1310
|
+
}
|
|
1311
|
+
chmod(path, mode) {
|
|
1312
|
+
const normalizedPath = this.normalizePath(path);
|
|
1313
|
+
const entry = this.entries.get(normalizedPath);
|
|
1314
|
+
if (!entry) {
|
|
1315
|
+
throw new Error(`ENOENT: no such file or directory, chmod '${path}'`);
|
|
1316
|
+
}
|
|
1317
|
+
entry.mode = mode;
|
|
1318
|
+
entry.mtime = new Date;
|
|
1319
|
+
}
|
|
1320
|
+
symlink(target, linkPath) {
|
|
1321
|
+
const normalizedLinkPath = this.normalizePath(linkPath);
|
|
1322
|
+
if (this.entries.has(normalizedLinkPath)) {
|
|
1323
|
+
throw new Error(`EEXIST: file already exists, symlink '${linkPath}'`);
|
|
1324
|
+
}
|
|
1325
|
+
this.ensureParentDirs(normalizedLinkPath);
|
|
1326
|
+
this.entries.set(normalizedLinkPath, {
|
|
1327
|
+
type: "symlink",
|
|
1328
|
+
target,
|
|
1329
|
+
mode: DEFAULT_SYMLINK_MODE,
|
|
1330
|
+
mtime: new Date
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
link(existingPath, newPath) {
|
|
1334
|
+
const srcPath = this.normalizePath(existingPath);
|
|
1335
|
+
const destPath = this.normalizePath(newPath);
|
|
1336
|
+
const entry = this.entries.get(srcPath);
|
|
1337
|
+
if (!entry) {
|
|
1338
|
+
throw new Error(`ENOENT: no such file or directory, link '${existingPath}'`);
|
|
1339
|
+
}
|
|
1340
|
+
if (entry.type !== "file") {
|
|
1341
|
+
throw new Error(`EPERM: operation not permitted, link '${existingPath}'`);
|
|
1342
|
+
}
|
|
1343
|
+
if (this.entries.has(destPath)) {
|
|
1344
|
+
throw new Error(`EEXIST: file already exists, link '${newPath}'`);
|
|
1345
|
+
}
|
|
1346
|
+
this.ensureParentDirs(destPath);
|
|
1347
|
+
this.entries.set(destPath, {
|
|
1348
|
+
type: "file",
|
|
1349
|
+
content: entry.content,
|
|
1350
|
+
mode: entry.mode,
|
|
1351
|
+
mtime: new Date
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
readlink(path) {
|
|
1355
|
+
const normalizedPath = this.normalizePath(path);
|
|
1356
|
+
const entry = this.entries.get(normalizedPath);
|
|
1357
|
+
if (!entry) {
|
|
1358
|
+
throw new Error(`ENOENT: no such file or directory, readlink '${path}'`);
|
|
1359
|
+
}
|
|
1360
|
+
if (entry.type !== "symlink") {
|
|
1361
|
+
throw new Error(`EINVAL: invalid argument, readlink '${path}'`);
|
|
1362
|
+
}
|
|
1363
|
+
return entry.target;
|
|
1364
|
+
}
|
|
1365
|
+
realpath(path) {
|
|
1366
|
+
const normalizedPath = this.normalizePath(path);
|
|
1367
|
+
const parts = normalizedPath.split("/").filter(Boolean);
|
|
1368
|
+
let resolved = "/";
|
|
1369
|
+
for (const part of parts) {
|
|
1370
|
+
resolved = resolved === "/" ? `/${part}` : `${resolved}/${part}`;
|
|
1371
|
+
const entry = this.entries.get(resolved);
|
|
1372
|
+
if (!entry) {
|
|
1373
|
+
throw new Error(`ENOENT: no such file or directory, realpath '${path}'`);
|
|
1374
|
+
}
|
|
1375
|
+
if (entry.type === "symlink") {
|
|
1376
|
+
const target = entry.target;
|
|
1377
|
+
if (target.startsWith("/")) {
|
|
1378
|
+
resolved = this.normalizePath(target);
|
|
1379
|
+
} else {
|
|
1380
|
+
const parent = this.getParentPath(resolved) ?? "/";
|
|
1381
|
+
resolved = this.resolvePath(parent, target);
|
|
1382
|
+
}
|
|
1383
|
+
const targetEntry = this.entries.get(resolved);
|
|
1384
|
+
if (!targetEntry) {
|
|
1385
|
+
throw new Error(`ENOENT: no such file or directory, realpath '${path}'`);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
return resolved;
|
|
1390
|
+
}
|
|
1391
|
+
utimes(path, atime, mtime) {
|
|
1392
|
+
const normalizedPath = this.normalizePath(path);
|
|
1393
|
+
const entry = this.entries.get(normalizedPath);
|
|
1394
|
+
if (!entry) {
|
|
1395
|
+
throw new Error(`ENOENT: no such file or directory, utimes '${path}'`);
|
|
1396
|
+
}
|
|
1397
|
+
entry.mtime = mtime;
|
|
1398
|
+
}
|
|
1399
|
+
normalizePath(path) {
|
|
1400
|
+
return Filesystem.normalizePath(path);
|
|
1401
|
+
}
|
|
1402
|
+
static normalizePath(path) {
|
|
1403
|
+
if (!path || path === ".")
|
|
1404
|
+
return "/";
|
|
1405
|
+
if (!path.startsWith("/")) {
|
|
1406
|
+
path = "/" + path;
|
|
1407
|
+
}
|
|
1408
|
+
const parts = path.split("/").filter(Boolean);
|
|
1409
|
+
const normalized = [];
|
|
1410
|
+
for (const part of parts) {
|
|
1411
|
+
if (part === ".")
|
|
1412
|
+
continue;
|
|
1413
|
+
if (part === "..") {
|
|
1414
|
+
normalized.pop();
|
|
1415
|
+
} else {
|
|
1416
|
+
normalized.push(part);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
return "/" + normalized.join("/");
|
|
1420
|
+
}
|
|
1421
|
+
getParentPath(path) {
|
|
1422
|
+
if (path === "/")
|
|
1423
|
+
return null;
|
|
1424
|
+
const lastSlash = path.lastIndexOf("/");
|
|
1425
|
+
return lastSlash === 0 ? "/" : path.slice(0, lastSlash);
|
|
1426
|
+
}
|
|
1427
|
+
ensureParentDirs(path) {
|
|
1428
|
+
Filesystem.ensureParentDirs(this.entries, path);
|
|
1429
|
+
}
|
|
1430
|
+
static ensureParentDirs(entries, path) {
|
|
1431
|
+
const parts = path.split("/").filter(Boolean);
|
|
1432
|
+
let current = "";
|
|
1433
|
+
for (let i = 0;i < parts.length - 1; i++) {
|
|
1434
|
+
current += "/" + parts[i];
|
|
1435
|
+
if (!entries.has(current)) {
|
|
1436
|
+
entries.set(current, {
|
|
1437
|
+
type: "directory",
|
|
1438
|
+
mode: DEFAULT_DIR_MODE,
|
|
1439
|
+
mtime: new Date
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
resolveSymlinks(path, maxDepth = 10) {
|
|
1445
|
+
let current = path;
|
|
1446
|
+
let depth = 0;
|
|
1447
|
+
while (depth < maxDepth) {
|
|
1448
|
+
const entry = this.entries.get(current);
|
|
1449
|
+
if (!entry)
|
|
1450
|
+
return null;
|
|
1451
|
+
if (entry.type !== "symlink")
|
|
1452
|
+
return entry;
|
|
1453
|
+
const target = entry.target;
|
|
1454
|
+
current = target.startsWith("/") ? this.normalizePath(target) : this.resolvePath(this.getParentPath(current) ?? "/", target);
|
|
1455
|
+
depth++;
|
|
1456
|
+
}
|
|
1457
|
+
throw new Error(`ELOOP: too many levels of symbolic links, stat '${path}'`);
|
|
1458
|
+
}
|
|
1459
|
+
entryToStat(entry) {
|
|
1460
|
+
return {
|
|
1461
|
+
isFile: entry.type === "file",
|
|
1462
|
+
isDirectory: entry.type === "directory",
|
|
1463
|
+
isSymbolicLink: entry.type === "symlink",
|
|
1464
|
+
mode: entry.mode,
|
|
1465
|
+
size: entry.type === "file" ? this.getContentSize(entry.content) : 0,
|
|
1466
|
+
mtime: entry.mtime
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
getContentSize(content) {
|
|
1470
|
+
if (typeof content === "string") {
|
|
1471
|
+
return new TextEncoder().encode(content).byteLength;
|
|
1472
|
+
}
|
|
1473
|
+
return content.byteLength;
|
|
1474
|
+
}
|
|
1475
|
+
cloneEntry(entry) {
|
|
1476
|
+
if (entry.type === "file") {
|
|
1477
|
+
return {
|
|
1478
|
+
type: "file",
|
|
1479
|
+
content: entry.content instanceof Uint8Array ? new Uint8Array(entry.content) : entry.content,
|
|
1480
|
+
mode: entry.mode,
|
|
1481
|
+
mtime: new Date
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
return { ...entry, mtime: new Date };
|
|
1485
|
+
}
|
|
1486
|
+
checkSizeLimit(content) {
|
|
1487
|
+
const currentSize = this.getSize();
|
|
1488
|
+
const newSize = typeof content === "string" ? content.length * 2 : content.byteLength;
|
|
1489
|
+
if (currentSize + newSize > this.maxSizeBytes) {
|
|
1490
|
+
throw new Error(`ENOSPC: filesystem size limit exceeded (${this.maxSizeBytes} bytes)`);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
getEncoding(options) {
|
|
1494
|
+
if (!options)
|
|
1495
|
+
return null;
|
|
1496
|
+
if (typeof options === "string")
|
|
1497
|
+
return options;
|
|
1498
|
+
return options.encoding ?? null;
|
|
1499
|
+
}
|
|
1500
|
+
decodeBuffer(buffer, encoding) {
|
|
1501
|
+
if (encoding === "utf8" || encoding === "utf-8") {
|
|
1502
|
+
return new TextDecoder("utf-8").decode(buffer);
|
|
1503
|
+
}
|
|
1504
|
+
if (encoding === "base64") {
|
|
1505
|
+
return this.encodeBase64(buffer);
|
|
1506
|
+
}
|
|
1507
|
+
if (encoding === "hex") {
|
|
1508
|
+
return Array.from(buffer).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1509
|
+
}
|
|
1510
|
+
return new TextDecoder("utf-8").decode(buffer);
|
|
1511
|
+
}
|
|
1512
|
+
encodeBase64(buffer) {
|
|
1513
|
+
let binary = "";
|
|
1514
|
+
for (let i = 0;i < buffer.byteLength; i++) {
|
|
1515
|
+
binary += String.fromCharCode(buffer[i]);
|
|
1516
|
+
}
|
|
1517
|
+
return btoa(binary);
|
|
1518
|
+
}
|
|
1519
|
+
concatBuffers(a, b) {
|
|
1520
|
+
const result = new Uint8Array(a.byteLength + b.byteLength);
|
|
1521
|
+
result.set(a, 0);
|
|
1522
|
+
result.set(b, a.byteLength);
|
|
1523
|
+
return result;
|
|
1524
|
+
}
|
|
1525
|
+
static parseFileInit(value) {
|
|
1526
|
+
if (typeof value === "string" || value instanceof Uint8Array) {
|
|
1527
|
+
return { content: value };
|
|
1528
|
+
}
|
|
1529
|
+
return value;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
function createFilesystem(options) {
|
|
1533
|
+
return Filesystem.create(options);
|
|
1534
|
+
}
|
|
1535
|
+
function wrapFilesystemForJustBash(fs) {
|
|
1536
|
+
return {
|
|
1537
|
+
readFile: async (path, options) => fs.readFile(path, options),
|
|
1538
|
+
writeFile: async (path, content, options) => fs.writeFile(path, content, options),
|
|
1539
|
+
appendFile: async (path, content, options) => fs.appendFile(path, content, options),
|
|
1540
|
+
exists: async (path) => fs.exists(path),
|
|
1541
|
+
stat: async (path) => fs.stat(path),
|
|
1542
|
+
lstat: async (path) => fs.lstat(path),
|
|
1543
|
+
mkdir: async (path, options) => fs.mkdir(path, options),
|
|
1544
|
+
readdir: async (path) => fs.readdir(path),
|
|
1545
|
+
readdirWithFileTypes: async (path) => fs.readdirWithFileTypes(path),
|
|
1546
|
+
rm: async (path, options) => fs.rm(path, options),
|
|
1547
|
+
cp: async (src, dest, options) => fs.cp(src, dest, options),
|
|
1548
|
+
mv: async (src, dest) => fs.mv(src, dest),
|
|
1549
|
+
chmod: async (path, mode) => fs.chmod(path, mode),
|
|
1550
|
+
symlink: async (target, linkPath) => fs.symlink(target, linkPath),
|
|
1551
|
+
link: async (existingPath, newPath) => fs.link(existingPath, newPath),
|
|
1552
|
+
readlink: async (path) => fs.readlink(path),
|
|
1553
|
+
realpath: async (path) => fs.realpath(path),
|
|
1554
|
+
utimes: async (path, atime, mtime) => fs.utimes(path, atime, mtime),
|
|
1555
|
+
readFileBuffer: async (path) => fs.readFileBuffer(path),
|
|
1556
|
+
resolvePath: (base, path) => fs.resolvePath(base, path),
|
|
1557
|
+
getAllPaths: () => fs.getAllPaths()
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// src/commands/index.ts
|
|
1562
|
+
import { defineCommand } from "just-bash/browser";
|
|
1563
|
+
|
|
1564
|
+
// src/commands/types.ts
|
|
1565
|
+
function formatSize(bytes) {
|
|
1566
|
+
if (bytes < 1024)
|
|
1567
|
+
return `${bytes} B`;
|
|
1568
|
+
if (bytes < 1024 * 1024)
|
|
1569
|
+
return `${(bytes / 1024).toFixed(2)} KB`;
|
|
1570
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
1571
|
+
}
|
|
1572
|
+
function formatDiagnostics(diagnostics) {
|
|
1573
|
+
if (diagnostics.length === 0)
|
|
1574
|
+
return "";
|
|
1575
|
+
return diagnostics.map((d) => {
|
|
1576
|
+
const severity = d.severity.toUpperCase();
|
|
1577
|
+
if (d.file) {
|
|
1578
|
+
const loc = `${d.file}${d.line ? `:${d.line}` : ""}${d.column ? `:${d.column}` : ""}`;
|
|
1579
|
+
return `${severity}: ${loc}: ${d.message}`;
|
|
1580
|
+
}
|
|
1581
|
+
return `${severity}: ${d.message}`;
|
|
1582
|
+
}).join(`
|
|
1583
|
+
`);
|
|
1584
|
+
}
|
|
1585
|
+
function formatBundleErrors(errors) {
|
|
1586
|
+
if (errors.length === 0)
|
|
1587
|
+
return "";
|
|
1588
|
+
return errors.map((e) => {
|
|
1589
|
+
let output = "";
|
|
1590
|
+
if (e.location) {
|
|
1591
|
+
const loc = `${e.location.file}:${e.location.line}${e.location.column ? `:${e.location.column}` : ""}`;
|
|
1592
|
+
output += `ERROR: ${loc}: ${e.text}`;
|
|
1593
|
+
if (e.location.lineText) {
|
|
1594
|
+
output += `
|
|
1595
|
+
${e.location.line} | ${e.location.lineText}`;
|
|
1596
|
+
if (e.location.column) {
|
|
1597
|
+
const padding = " ".repeat(String(e.location.line).length + 3 + e.location.column - 1);
|
|
1598
|
+
output += `
|
|
1599
|
+
${padding}^`;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
} else {
|
|
1603
|
+
output += `ERROR: ${e.text}`;
|
|
1604
|
+
}
|
|
1605
|
+
return output;
|
|
1606
|
+
}).join(`
|
|
1607
|
+
|
|
1608
|
+
`);
|
|
1609
|
+
}
|
|
1610
|
+
function formatBuildFailure(failure, prefix = "Build failed") {
|
|
1611
|
+
switch (failure.phase) {
|
|
1612
|
+
case "entry":
|
|
1613
|
+
return `${prefix}: ${failure.message}
|
|
1614
|
+
`;
|
|
1615
|
+
case "typecheck":
|
|
1616
|
+
if (failure.diagnostics && failure.diagnostics.length > 0) {
|
|
1617
|
+
const errors = failure.diagnostics.filter((d) => d.severity === "error");
|
|
1618
|
+
if (errors.length > 0) {
|
|
1619
|
+
return `${prefix}: Type check errors
|
|
1620
|
+
|
|
1621
|
+
${formatDiagnostics(errors)}
|
|
1622
|
+
`;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
return `${prefix}: Type check errors
|
|
1626
|
+
`;
|
|
1627
|
+
case "bundle":
|
|
1628
|
+
if (failure.bundleErrors && failure.bundleErrors.length > 0) {
|
|
1629
|
+
return `${prefix}: Bundle errors
|
|
1630
|
+
|
|
1631
|
+
${formatBundleErrors(failure.bundleErrors)}
|
|
1632
|
+
`;
|
|
1633
|
+
}
|
|
1634
|
+
return `${prefix}: Bundle error
|
|
1635
|
+
`;
|
|
1636
|
+
default:
|
|
1637
|
+
return `${prefix}: Unknown error
|
|
1638
|
+
`;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
// src/commands/index.ts
|
|
1642
|
+
function createSandlotCommand(sandboxRef) {
|
|
1643
|
+
return defineCommand("sandlot", async (args, ctx) => {
|
|
1644
|
+
const subcommand = args[0];
|
|
1645
|
+
if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
|
|
1646
|
+
return showHelp();
|
|
1647
|
+
}
|
|
1648
|
+
switch (subcommand) {
|
|
1649
|
+
case "build":
|
|
1650
|
+
return handleBuild(sandboxRef, args.slice(1));
|
|
1651
|
+
case "typecheck":
|
|
1652
|
+
case "tsc":
|
|
1653
|
+
return handleTypecheck(sandboxRef, args.slice(1));
|
|
1654
|
+
case "install":
|
|
1655
|
+
case "add":
|
|
1656
|
+
case "i":
|
|
1657
|
+
return handleInstall(sandboxRef, args.slice(1));
|
|
1658
|
+
case "uninstall":
|
|
1659
|
+
case "remove":
|
|
1660
|
+
case "rm":
|
|
1661
|
+
return handleUninstall(sandboxRef, args.slice(1));
|
|
1662
|
+
case "run":
|
|
1663
|
+
return handleRun(sandboxRef, args.slice(1));
|
|
1664
|
+
default:
|
|
1665
|
+
return {
|
|
1666
|
+
stdout: "",
|
|
1667
|
+
stderr: `Unknown command: sandlot ${subcommand}
|
|
1668
|
+
|
|
1669
|
+
Run 'sandlot help' for available commands.
|
|
1670
|
+
`,
|
|
1671
|
+
exitCode: 1
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
function showHelp() {
|
|
1677
|
+
return {
|
|
1678
|
+
stdout: `sandlot - In-browser TypeScript sandbox
|
|
1679
|
+
|
|
1680
|
+
Usage: sandlot <command> [options]
|
|
1681
|
+
|
|
1682
|
+
Commands:
|
|
1683
|
+
build Build the project (typecheck, bundle)
|
|
1684
|
+
run Build and execute code
|
|
1685
|
+
typecheck Type check without building (alias: tsc)
|
|
1686
|
+
install Install packages (aliases: add, i)
|
|
1687
|
+
uninstall Remove packages (aliases: remove, rm)
|
|
1688
|
+
help Show this help message
|
|
1689
|
+
|
|
1690
|
+
Run 'sandlot <command> --help' for command-specific options.
|
|
1691
|
+
|
|
1692
|
+
Examples:
|
|
1693
|
+
sandlot build
|
|
1694
|
+
sandlot run
|
|
1695
|
+
sandlot run --skip-typecheck --timeout 5000
|
|
1696
|
+
sandlot install react react-dom
|
|
1697
|
+
sandlot typecheck
|
|
1698
|
+
`,
|
|
1699
|
+
stderr: "",
|
|
1700
|
+
exitCode: 0
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
async function handleBuild(sandboxRef, args) {
|
|
1704
|
+
let entryPoint;
|
|
1705
|
+
let skipTypecheck = false;
|
|
1706
|
+
let minify = false;
|
|
1707
|
+
let format = "esm";
|
|
1708
|
+
for (let i = 0;i < args.length; i++) {
|
|
1709
|
+
const arg = args[i];
|
|
1710
|
+
if (arg === "--skip-typecheck" || arg === "-s") {
|
|
1711
|
+
skipTypecheck = true;
|
|
1712
|
+
} else if (arg === "--minify" || arg === "-m") {
|
|
1713
|
+
minify = true;
|
|
1714
|
+
} else if ((arg === "--format" || arg === "-f") && args[i + 1]) {
|
|
1715
|
+
const f = args[++i].toLowerCase();
|
|
1716
|
+
if (f === "esm" || f === "iife" || f === "cjs") {
|
|
1717
|
+
format = f;
|
|
1718
|
+
}
|
|
1719
|
+
} else if ((arg === "--entry" || arg === "-e") && args[i + 1]) {
|
|
1720
|
+
entryPoint = args[++i];
|
|
1721
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
1722
|
+
return {
|
|
1723
|
+
stdout: `Usage: sandlot build [options]
|
|
1724
|
+
|
|
1725
|
+
Options:
|
|
1726
|
+
--entry, -e <path> Entry point (default: from package.json main)
|
|
1727
|
+
--skip-typecheck, -s Skip type checking
|
|
1728
|
+
--minify, -m Minify output
|
|
1729
|
+
--format, -f <fmt> Output format (esm|iife|cjs)
|
|
1730
|
+
--help, -h Show this help message
|
|
1731
|
+
|
|
1732
|
+
Examples:
|
|
1733
|
+
sandlot build
|
|
1734
|
+
sandlot build --entry /src/main.ts
|
|
1735
|
+
sandlot build --skip-typecheck --minify
|
|
1736
|
+
`,
|
|
1737
|
+
stderr: "",
|
|
1738
|
+
exitCode: 0
|
|
1739
|
+
};
|
|
1740
|
+
} else if (arg && !arg.startsWith("-") && !entryPoint) {
|
|
1741
|
+
entryPoint = arg;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
const result = await sandboxRef.build({
|
|
1745
|
+
entryPoint,
|
|
1746
|
+
skipTypecheck,
|
|
1747
|
+
minify,
|
|
1748
|
+
format
|
|
1749
|
+
});
|
|
1750
|
+
if (!result.success) {
|
|
1751
|
+
return {
|
|
1752
|
+
stdout: "",
|
|
1753
|
+
stderr: formatBuildFailure(result),
|
|
1754
|
+
exitCode: 1
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
let output = `Build successful!
|
|
1758
|
+
`;
|
|
1759
|
+
output += `Size: ${formatSize(result.code.length)}
|
|
1760
|
+
`;
|
|
1761
|
+
output += `Files: ${result.includedFiles.length}
|
|
1762
|
+
`;
|
|
1763
|
+
if (result.warnings.length > 0) {
|
|
1764
|
+
output += `
|
|
1765
|
+
Warnings:
|
|
1766
|
+
`;
|
|
1767
|
+
for (const warning of result.warnings) {
|
|
1768
|
+
if (warning.location) {
|
|
1769
|
+
output += ` ${warning.location.file}:${warning.location.line}: ${warning.text}
|
|
1770
|
+
`;
|
|
1771
|
+
} else {
|
|
1772
|
+
output += ` ${warning.text}
|
|
1773
|
+
`;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
return {
|
|
1778
|
+
stdout: output,
|
|
1779
|
+
stderr: "",
|
|
1780
|
+
exitCode: 0
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
async function handleTypecheck(sandboxRef, args) {
|
|
1784
|
+
let entryPoint;
|
|
1785
|
+
for (let i = 0;i < args.length; i++) {
|
|
1786
|
+
const arg = args[i];
|
|
1787
|
+
if ((arg === "--entry" || arg === "-e") && args[i + 1]) {
|
|
1788
|
+
entryPoint = args[++i];
|
|
1789
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
1790
|
+
return {
|
|
1791
|
+
stdout: `Usage: sandlot typecheck [options]
|
|
1792
|
+
|
|
1793
|
+
Options:
|
|
1794
|
+
--entry, -e <path> Entry point (default: from package.json main)
|
|
1795
|
+
--help, -h Show this help message
|
|
1796
|
+
|
|
1797
|
+
Aliases: sandlot tsc
|
|
1798
|
+
|
|
1799
|
+
Examples:
|
|
1800
|
+
sandlot typecheck
|
|
1801
|
+
sandlot typecheck --entry /src/main.ts
|
|
1802
|
+
`,
|
|
1803
|
+
stderr: "",
|
|
1804
|
+
exitCode: 0
|
|
1805
|
+
};
|
|
1806
|
+
} else if (arg && !arg.startsWith("-") && !entryPoint) {
|
|
1807
|
+
entryPoint = arg;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
try {
|
|
1811
|
+
const result = await sandboxRef.typecheck({ entryPoint });
|
|
1812
|
+
if (!result.success) {
|
|
1813
|
+
const errors = result.diagnostics.filter((d) => d.severity === "error");
|
|
1814
|
+
const formatted = formatDiagnostics(errors);
|
|
1815
|
+
return {
|
|
1816
|
+
stdout: "",
|
|
1817
|
+
stderr: `Type check failed
|
|
1818
|
+
|
|
1819
|
+
${formatted}
|
|
1820
|
+
`,
|
|
1821
|
+
exitCode: 1
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
const warnings = result.diagnostics.filter((d) => d.severity === "warning");
|
|
1825
|
+
let output = `Type check passed
|
|
1826
|
+
`;
|
|
1827
|
+
if (warnings.length > 0) {
|
|
1828
|
+
output += `
|
|
1829
|
+
Warnings:
|
|
1830
|
+
|
|
1831
|
+
${formatDiagnostics(warnings)}
|
|
1832
|
+
`;
|
|
1833
|
+
}
|
|
1834
|
+
return {
|
|
1835
|
+
stdout: output,
|
|
1836
|
+
stderr: "",
|
|
1837
|
+
exitCode: 0
|
|
1838
|
+
};
|
|
1839
|
+
} catch (err) {
|
|
1840
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1841
|
+
return {
|
|
1842
|
+
stdout: "",
|
|
1843
|
+
stderr: `Type check error: ${message}
|
|
1844
|
+
`,
|
|
1845
|
+
exitCode: 1
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
async function handleInstall(sandboxRef, args) {
|
|
1850
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
1851
|
+
return {
|
|
1852
|
+
stdout: `Usage: sandlot install <package>[@version] [...packages]
|
|
1853
|
+
|
|
1854
|
+
Examples:
|
|
1855
|
+
sandlot install react
|
|
1856
|
+
sandlot install lodash@4.17.21
|
|
1857
|
+
sandlot install @tanstack/react-query@5
|
|
1858
|
+
sandlot install react react-dom
|
|
1859
|
+
|
|
1860
|
+
Aliases: sandlot add, sandlot i
|
|
1861
|
+
`,
|
|
1862
|
+
stderr: "",
|
|
1863
|
+
exitCode: 0
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1866
|
+
const packages = args.filter((a) => !!a && !a.startsWith("-"));
|
|
1867
|
+
if (packages.length === 0) {
|
|
1868
|
+
return {
|
|
1869
|
+
stdout: "",
|
|
1870
|
+
stderr: `Usage: sandlot install <package>[@version] [...packages]
|
|
1871
|
+
|
|
1872
|
+
Run 'sandlot install --help' for more information.
|
|
1873
|
+
`,
|
|
1874
|
+
exitCode: 1
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
const results = [];
|
|
1878
|
+
let hasError = false;
|
|
1879
|
+
for (const packageSpec of packages) {
|
|
1880
|
+
try {
|
|
1881
|
+
const result = await sandboxRef.install(packageSpec);
|
|
1882
|
+
let status = `+ ${result.name}@${result.version}`;
|
|
1883
|
+
if (result.typesInstalled) {
|
|
1884
|
+
status += ` (${result.typeFilesCount} type file${result.typeFilesCount !== 1 ? "s" : ""})`;
|
|
1885
|
+
if (result.fromCache) {
|
|
1886
|
+
status += " [cached]";
|
|
1887
|
+
}
|
|
1888
|
+
} else if (result.typesError) {
|
|
1889
|
+
status += ` (no types: ${result.typesError})`;
|
|
1890
|
+
}
|
|
1891
|
+
results.push(status);
|
|
1892
|
+
} catch (err) {
|
|
1893
|
+
hasError = true;
|
|
1894
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1895
|
+
results.push(`x ${packageSpec}: ${message}`);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
const output = results.join(`
|
|
1899
|
+
`) + `
|
|
1900
|
+
`;
|
|
1901
|
+
return hasError ? { stdout: "", stderr: output, exitCode: 1 } : { stdout: output, stderr: "", exitCode: 0 };
|
|
1902
|
+
}
|
|
1903
|
+
async function handleUninstall(sandboxRef, args) {
|
|
1904
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
1905
|
+
return {
|
|
1906
|
+
stdout: `Usage: sandlot uninstall <package> [...packages]
|
|
1907
|
+
|
|
1908
|
+
Examples:
|
|
1909
|
+
sandlot uninstall lodash
|
|
1910
|
+
sandlot uninstall react react-dom
|
|
1911
|
+
|
|
1912
|
+
Aliases: sandlot remove, sandlot rm
|
|
1913
|
+
`,
|
|
1914
|
+
stderr: "",
|
|
1915
|
+
exitCode: 0
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
const packages = args.filter((a) => !!a && !a.startsWith("-"));
|
|
1919
|
+
if (packages.length === 0) {
|
|
1920
|
+
return {
|
|
1921
|
+
stdout: "",
|
|
1922
|
+
stderr: `Usage: sandlot uninstall <package> [...packages]
|
|
1923
|
+
|
|
1924
|
+
Run 'sandlot uninstall --help' for more information.
|
|
1925
|
+
`,
|
|
1926
|
+
exitCode: 1
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
const results = [];
|
|
1930
|
+
let hasError = false;
|
|
1931
|
+
for (const packageName of packages) {
|
|
1932
|
+
try {
|
|
1933
|
+
const result = await sandboxRef.uninstall(packageName);
|
|
1934
|
+
if (result.removed) {
|
|
1935
|
+
results.push(`- ${result.name}`);
|
|
1936
|
+
} else {
|
|
1937
|
+
results.push(`x ${packageName}: not installed`);
|
|
1938
|
+
hasError = true;
|
|
1939
|
+
}
|
|
1940
|
+
} catch (err) {
|
|
1941
|
+
hasError = true;
|
|
1942
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1943
|
+
results.push(`x ${packageName}: ${message}`);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
const output = results.join(`
|
|
1947
|
+
`) + `
|
|
1948
|
+
`;
|
|
1949
|
+
return hasError ? { stdout: "", stderr: output, exitCode: 1 } : { stdout: output, stderr: "", exitCode: 0 };
|
|
1950
|
+
}
|
|
1951
|
+
async function handleRun(sandboxRef, args) {
|
|
1952
|
+
let entryPoint;
|
|
1953
|
+
let skipTypecheck = false;
|
|
1954
|
+
let timeout = 30000;
|
|
1955
|
+
let entryExport = "main";
|
|
1956
|
+
for (let i = 0;i < args.length; i++) {
|
|
1957
|
+
const arg = args[i];
|
|
1958
|
+
if (arg === "--skip-typecheck" || arg === "-s") {
|
|
1959
|
+
skipTypecheck = true;
|
|
1960
|
+
} else if ((arg === "--timeout" || arg === "-t") && args[i + 1]) {
|
|
1961
|
+
const t = parseInt(args[++i], 10);
|
|
1962
|
+
if (!isNaN(t))
|
|
1963
|
+
timeout = t;
|
|
1964
|
+
} else if ((arg === "--entry" || arg === "-e") && args[i + 1]) {
|
|
1965
|
+
entryPoint = args[++i];
|
|
1966
|
+
} else if ((arg === "--export" || arg === "-x") && args[i + 1]) {
|
|
1967
|
+
const e = args[++i].toLowerCase();
|
|
1968
|
+
if (e === "main" || e === "default") {
|
|
1969
|
+
entryExport = e;
|
|
1970
|
+
}
|
|
1971
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
1972
|
+
return {
|
|
1973
|
+
stdout: `Usage: sandlot run [options]
|
|
1974
|
+
|
|
1975
|
+
Options:
|
|
1976
|
+
--entry, -e <path> Entry point (default: from package.json main)
|
|
1977
|
+
--skip-typecheck, -s Skip type checking
|
|
1978
|
+
--timeout, -t <ms> Execution timeout (default: 30000, 0 = none)
|
|
1979
|
+
--export, -x <name> Export to call: main or default (default: main)
|
|
1980
|
+
--help, -h Show this help message
|
|
1981
|
+
|
|
1982
|
+
Examples:
|
|
1983
|
+
sandlot run
|
|
1984
|
+
sandlot run --entry /src/main.ts
|
|
1985
|
+
sandlot run --skip-typecheck --timeout 5000
|
|
1986
|
+
sandlot run --export default
|
|
1987
|
+
`,
|
|
1988
|
+
stderr: "",
|
|
1989
|
+
exitCode: 0
|
|
1990
|
+
};
|
|
1991
|
+
} else if (arg && !arg.startsWith("-") && !entryPoint) {
|
|
1992
|
+
entryPoint = arg;
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
try {
|
|
1996
|
+
const result = await sandboxRef.run({
|
|
1997
|
+
entryPoint,
|
|
1998
|
+
skipTypecheck,
|
|
1999
|
+
timeout,
|
|
2000
|
+
entryExport
|
|
2001
|
+
});
|
|
2002
|
+
if (!result.success) {
|
|
2003
|
+
let stderr = "";
|
|
2004
|
+
if (result.buildFailure) {
|
|
2005
|
+
stderr = formatBuildFailure(result.buildFailure, "Run failed");
|
|
2006
|
+
} else {
|
|
2007
|
+
stderr = `Run failed: ${result.error ?? "Unknown error"}
|
|
2008
|
+
`;
|
|
2009
|
+
}
|
|
2010
|
+
let stdout = "";
|
|
2011
|
+
if (result.logs.length > 0) {
|
|
2012
|
+
stdout = result.logs.join(`
|
|
2013
|
+
`) + `
|
|
2014
|
+
`;
|
|
2015
|
+
}
|
|
2016
|
+
return {
|
|
2017
|
+
stdout,
|
|
2018
|
+
stderr,
|
|
2019
|
+
exitCode: 1
|
|
2020
|
+
};
|
|
2021
|
+
}
|
|
2022
|
+
let output = "";
|
|
2023
|
+
if (result.logs.length > 0) {
|
|
2024
|
+
output = result.logs.join(`
|
|
2025
|
+
`) + `
|
|
2026
|
+
`;
|
|
2027
|
+
}
|
|
2028
|
+
if (result.returnValue !== undefined) {
|
|
2029
|
+
const returnStr = typeof result.returnValue === "object" ? JSON.stringify(result.returnValue, null, 2) : String(result.returnValue);
|
|
2030
|
+
output += `[return] ${returnStr}
|
|
2031
|
+
`;
|
|
2032
|
+
}
|
|
2033
|
+
if (result.executionTimeMs !== undefined) {
|
|
2034
|
+
output += `
|
|
2035
|
+
Completed in ${result.executionTimeMs.toFixed(2)}ms
|
|
2036
|
+
`;
|
|
2037
|
+
}
|
|
2038
|
+
return {
|
|
2039
|
+
stdout: output,
|
|
2040
|
+
stderr: "",
|
|
2041
|
+
exitCode: 0
|
|
2042
|
+
};
|
|
2043
|
+
} catch (err) {
|
|
2044
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2045
|
+
return {
|
|
2046
|
+
stdout: "",
|
|
2047
|
+
stderr: `Run error: ${message}
|
|
2048
|
+
`,
|
|
2049
|
+
exitCode: 1
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
function createDefaultCommands(sandboxRef) {
|
|
2054
|
+
return [createSandlotCommand(sandboxRef)];
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
// src/core/sandbox.ts
|
|
2058
|
+
var DEFAULT_ENTRY_POINT = "./index.ts";
|
|
2059
|
+
var TSCONFIG_PATH = "/tsconfig.json";
|
|
2060
|
+
var PACKAGE_JSON_PATH = "/package.json";
|
|
2061
|
+
var DEFAULT_PACKAGE_JSON = {
|
|
2062
|
+
main: DEFAULT_ENTRY_POINT,
|
|
2063
|
+
dependencies: {}
|
|
2064
|
+
};
|
|
2065
|
+
var DEFAULT_TSCONFIG = {
|
|
2066
|
+
compilerOptions: {
|
|
2067
|
+
target: "ES2020",
|
|
2068
|
+
lib: ["ES2020", "DOM", "DOM.Iterable"],
|
|
2069
|
+
module: "ESNext",
|
|
2070
|
+
moduleResolution: "bundler",
|
|
2071
|
+
jsx: "react-jsx",
|
|
2072
|
+
strict: true,
|
|
2073
|
+
noEmit: true,
|
|
2074
|
+
esModuleInterop: true,
|
|
2075
|
+
skipLibCheck: true,
|
|
2076
|
+
resolveJsonModule: true,
|
|
2077
|
+
isolatedModules: true
|
|
2078
|
+
},
|
|
2079
|
+
include: ["**/*.ts", "**/*.tsx"],
|
|
2080
|
+
exclude: ["node_modules"]
|
|
2081
|
+
};
|
|
2082
|
+
function parsePackageSpec(spec) {
|
|
2083
|
+
if (spec.startsWith("@")) {
|
|
2084
|
+
const slashIndex = spec.indexOf("/");
|
|
2085
|
+
if (slashIndex === -1) {
|
|
2086
|
+
return { name: spec };
|
|
2087
|
+
}
|
|
2088
|
+
const afterSlash = spec.slice(slashIndex + 1);
|
|
2089
|
+
const atIndex2 = afterSlash.indexOf("@");
|
|
2090
|
+
if (atIndex2 === -1) {
|
|
2091
|
+
return { name: spec };
|
|
2092
|
+
}
|
|
2093
|
+
return {
|
|
2094
|
+
name: spec.slice(0, slashIndex + 1 + atIndex2),
|
|
2095
|
+
version: afterSlash.slice(atIndex2 + 1)
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
const atIndex = spec.indexOf("@");
|
|
2099
|
+
if (atIndex === -1) {
|
|
2100
|
+
return { name: spec };
|
|
2101
|
+
}
|
|
2102
|
+
return {
|
|
2103
|
+
name: spec.slice(0, atIndex),
|
|
2104
|
+
version: spec.slice(atIndex + 1)
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
function readPackageJson(fs) {
|
|
2108
|
+
try {
|
|
2109
|
+
if (fs.exists(PACKAGE_JSON_PATH)) {
|
|
2110
|
+
const content = fs.readFile(PACKAGE_JSON_PATH);
|
|
2111
|
+
return JSON.parse(content);
|
|
2112
|
+
}
|
|
2113
|
+
} catch {}
|
|
2114
|
+
return {};
|
|
2115
|
+
}
|
|
2116
|
+
function getEntryPoint(fs) {
|
|
2117
|
+
const pkg = readPackageJson(fs);
|
|
2118
|
+
const main = pkg.main ?? DEFAULT_ENTRY_POINT;
|
|
2119
|
+
if (main.startsWith("/")) {
|
|
2120
|
+
return main;
|
|
2121
|
+
}
|
|
2122
|
+
if (main.startsWith("./")) {
|
|
2123
|
+
return "/" + main.slice(2);
|
|
2124
|
+
}
|
|
2125
|
+
return "/" + main;
|
|
2126
|
+
}
|
|
2127
|
+
function getInstalledPackages(fs) {
|
|
2128
|
+
const pkg = readPackageJson(fs);
|
|
2129
|
+
return pkg.dependencies ?? {};
|
|
2130
|
+
}
|
|
2131
|
+
function saveInstalledPackages(fs, dependencies) {
|
|
2132
|
+
let existing = {};
|
|
2133
|
+
try {
|
|
2134
|
+
if (fs.exists(PACKAGE_JSON_PATH)) {
|
|
2135
|
+
const content = fs.readFile(PACKAGE_JSON_PATH);
|
|
2136
|
+
existing = JSON.parse(content);
|
|
2137
|
+
}
|
|
2138
|
+
} catch {}
|
|
2139
|
+
const updated = {
|
|
2140
|
+
...existing,
|
|
2141
|
+
dependencies
|
|
2142
|
+
};
|
|
2143
|
+
fs.writeFile(PACKAGE_JSON_PATH, JSON.stringify(updated, null, 2));
|
|
2144
|
+
}
|
|
2145
|
+
function ensureDir(fs, path) {
|
|
2146
|
+
if (path === "/" || path === "")
|
|
2147
|
+
return;
|
|
2148
|
+
if (fs.exists(path)) {
|
|
2149
|
+
const stat = fs.stat(path);
|
|
2150
|
+
if (stat.isDirectory)
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
const parent = path.substring(0, path.lastIndexOf("/")) || "/";
|
|
2154
|
+
ensureDir(fs, parent);
|
|
2155
|
+
fs.mkdir(path);
|
|
2156
|
+
}
|
|
2157
|
+
async function createSandboxImpl(fs, options, context) {
|
|
2158
|
+
const {
|
|
2159
|
+
bundler,
|
|
2160
|
+
typechecker,
|
|
2161
|
+
typesResolver,
|
|
2162
|
+
sharedModuleRegistry,
|
|
2163
|
+
executor
|
|
2164
|
+
} = context;
|
|
2165
|
+
let lastBuild = null;
|
|
2166
|
+
const onBuildCallbacks = new Set;
|
|
2167
|
+
if (options.onBuild) {
|
|
2168
|
+
onBuildCallbacks.add(options.onBuild);
|
|
2169
|
+
}
|
|
2170
|
+
if (options.initialFiles) {
|
|
2171
|
+
for (const [path, content] of Object.entries(options.initialFiles)) {
|
|
2172
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
2173
|
+
const dir = normalizedPath.substring(0, normalizedPath.lastIndexOf("/"));
|
|
2174
|
+
if (dir && dir !== "/") {
|
|
2175
|
+
ensureDir(fs, dir);
|
|
2176
|
+
}
|
|
2177
|
+
fs.writeFile(normalizedPath, content);
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
if (!fs.exists(PACKAGE_JSON_PATH)) {
|
|
2181
|
+
fs.writeFile(PACKAGE_JSON_PATH, JSON.stringify(DEFAULT_PACKAGE_JSON, null, 2));
|
|
2182
|
+
}
|
|
2183
|
+
if (!fs.exists(TSCONFIG_PATH)) {
|
|
2184
|
+
fs.writeFile(TSCONFIG_PATH, JSON.stringify(DEFAULT_TSCONFIG, null, 2));
|
|
2185
|
+
}
|
|
2186
|
+
async function install(packageSpec) {
|
|
2187
|
+
const { name, version } = parsePackageSpec(packageSpec);
|
|
2188
|
+
let resolvedVersion = version ?? "latest";
|
|
2189
|
+
let typesInstalled = false;
|
|
2190
|
+
let typeFilesCount = 0;
|
|
2191
|
+
let typesError;
|
|
2192
|
+
const fromCache = false;
|
|
2193
|
+
if (typesResolver) {
|
|
2194
|
+
try {
|
|
2195
|
+
const typeFiles = await typesResolver.resolveTypes(name, version);
|
|
2196
|
+
const packageDir = `/node_modules/${name}`;
|
|
2197
|
+
ensureDir(fs, packageDir);
|
|
2198
|
+
let typesEntry = "index.d.ts";
|
|
2199
|
+
for (const [filePath, content] of Object.entries(typeFiles)) {
|
|
2200
|
+
const fullPath = filePath.startsWith("/") ? filePath : `${packageDir}/${filePath}`;
|
|
2201
|
+
const dir = fullPath.substring(0, fullPath.lastIndexOf("/"));
|
|
2202
|
+
ensureDir(fs, dir);
|
|
2203
|
+
fs.writeFile(fullPath, content);
|
|
2204
|
+
typeFilesCount++;
|
|
2205
|
+
const relativePath = fullPath.replace(`${packageDir}/`, "");
|
|
2206
|
+
if (relativePath === "index.d.ts") {
|
|
2207
|
+
typesEntry = "index.d.ts";
|
|
2208
|
+
} else if (typesEntry === "index.d.ts" && relativePath.endsWith(".d.ts") && !relativePath.includes("/")) {
|
|
2209
|
+
typesEntry = relativePath;
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
typesInstalled = typeFilesCount > 0;
|
|
2213
|
+
if (typesInstalled) {
|
|
2214
|
+
const pkgJsonPath = `${packageDir}/package.json`;
|
|
2215
|
+
const pkgJson = {
|
|
2216
|
+
name,
|
|
2217
|
+
version: resolvedVersion,
|
|
2218
|
+
types: typesEntry,
|
|
2219
|
+
main: typesEntry.replace(/\.d\.ts$/, ".js")
|
|
2220
|
+
};
|
|
2221
|
+
fs.writeFile(pkgJsonPath, JSON.stringify(pkgJson, null, 2));
|
|
2222
|
+
}
|
|
2223
|
+
if (!version) {
|
|
2224
|
+
resolvedVersion = "latest";
|
|
2225
|
+
}
|
|
2226
|
+
} catch (err) {
|
|
2227
|
+
typesError = err instanceof Error ? err.message : String(err);
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
const dependencies = getInstalledPackages(fs);
|
|
2231
|
+
dependencies[name] = resolvedVersion;
|
|
2232
|
+
saveInstalledPackages(fs, dependencies);
|
|
2233
|
+
return {
|
|
2234
|
+
name,
|
|
2235
|
+
version: resolvedVersion,
|
|
2236
|
+
typesInstalled,
|
|
2237
|
+
typeFilesCount,
|
|
2238
|
+
typesError,
|
|
2239
|
+
fromCache
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
2242
|
+
async function uninstall(packageName) {
|
|
2243
|
+
const dependencies = getInstalledPackages(fs);
|
|
2244
|
+
if (!(packageName in dependencies)) {
|
|
2245
|
+
return { name: packageName, removed: false };
|
|
2246
|
+
}
|
|
2247
|
+
delete dependencies[packageName];
|
|
2248
|
+
saveInstalledPackages(fs, dependencies);
|
|
2249
|
+
const typesPath = `/node_modules/${packageName}`;
|
|
2250
|
+
if (fs.exists(typesPath)) {
|
|
2251
|
+
fs.rm(typesPath, { recursive: true, force: true });
|
|
2252
|
+
}
|
|
2253
|
+
return { name: packageName, removed: true };
|
|
2254
|
+
}
|
|
2255
|
+
async function build(buildOptions) {
|
|
2256
|
+
const buildEntryPoint = buildOptions?.entryPoint ?? getEntryPoint(fs);
|
|
2257
|
+
const skipTypecheck = buildOptions?.skipTypecheck ?? false;
|
|
2258
|
+
const minify = buildOptions?.minify ?? false;
|
|
2259
|
+
const format = buildOptions?.format ?? "esm";
|
|
2260
|
+
if (!fs.exists(buildEntryPoint)) {
|
|
2261
|
+
return {
|
|
2262
|
+
success: false,
|
|
2263
|
+
phase: "entry",
|
|
2264
|
+
message: `Entry point not found: ${buildEntryPoint}`
|
|
2265
|
+
};
|
|
2266
|
+
}
|
|
2267
|
+
if (!skipTypecheck && typechecker) {
|
|
2268
|
+
const typecheckResult = await typechecker.typecheck({
|
|
2269
|
+
fs,
|
|
2270
|
+
entryPoint: buildEntryPoint,
|
|
2271
|
+
tsconfigPath: TSCONFIG_PATH
|
|
2272
|
+
});
|
|
2273
|
+
if (!typecheckResult.success) {
|
|
2274
|
+
return {
|
|
2275
|
+
success: false,
|
|
2276
|
+
phase: "typecheck",
|
|
2277
|
+
diagnostics: typecheckResult.diagnostics
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
const installedPackages = getInstalledPackages(fs);
|
|
2282
|
+
const bundleResult = await bundler.bundle({
|
|
2283
|
+
fs,
|
|
2284
|
+
entryPoint: buildEntryPoint,
|
|
2285
|
+
installedPackages,
|
|
2286
|
+
sharedModules: sharedModuleRegistry?.list() ?? [],
|
|
2287
|
+
sharedModuleRegistry: sharedModuleRegistry ?? undefined,
|
|
2288
|
+
format,
|
|
2289
|
+
minify
|
|
2290
|
+
});
|
|
2291
|
+
if (!bundleResult.success) {
|
|
2292
|
+
return {
|
|
2293
|
+
success: false,
|
|
2294
|
+
phase: "bundle",
|
|
2295
|
+
bundleErrors: bundleResult.errors,
|
|
2296
|
+
bundleWarnings: bundleResult.warnings
|
|
2297
|
+
};
|
|
2298
|
+
}
|
|
2299
|
+
const output = {
|
|
2300
|
+
success: true,
|
|
2301
|
+
code: bundleResult.code,
|
|
2302
|
+
includedFiles: bundleResult.includedFiles,
|
|
2303
|
+
warnings: bundleResult.warnings
|
|
2304
|
+
};
|
|
2305
|
+
lastBuild = output;
|
|
2306
|
+
for (const callback of onBuildCallbacks) {
|
|
2307
|
+
try {
|
|
2308
|
+
await callback(output);
|
|
2309
|
+
} catch (err) {
|
|
2310
|
+
console.error("[sandlot] onBuild callback error:", err);
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
return output;
|
|
2314
|
+
}
|
|
2315
|
+
async function typecheck(typecheckOptions) {
|
|
2316
|
+
if (!typechecker) {
|
|
2317
|
+
return { success: true, diagnostics: [] };
|
|
2318
|
+
}
|
|
2319
|
+
const checkEntryPoint = typecheckOptions?.entryPoint ?? getEntryPoint(fs);
|
|
2320
|
+
if (!fs.exists(checkEntryPoint)) {
|
|
2321
|
+
return {
|
|
2322
|
+
success: false,
|
|
2323
|
+
diagnostics: [
|
|
2324
|
+
{
|
|
2325
|
+
message: `Entry point not found: ${checkEntryPoint}`,
|
|
2326
|
+
severity: "error"
|
|
2327
|
+
}
|
|
2328
|
+
]
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
2331
|
+
return typechecker.typecheck({
|
|
2332
|
+
fs,
|
|
2333
|
+
entryPoint: checkEntryPoint,
|
|
2334
|
+
tsconfigPath: TSCONFIG_PATH
|
|
2335
|
+
});
|
|
2336
|
+
}
|
|
2337
|
+
async function run(runOptions) {
|
|
2338
|
+
if (!executor) {
|
|
2339
|
+
throw new Error("[sandlot] No executor configured. Provide an executor when creating Sandlot to use run().");
|
|
2340
|
+
}
|
|
2341
|
+
const buildResult = await build({
|
|
2342
|
+
entryPoint: runOptions?.entryPoint,
|
|
2343
|
+
skipTypecheck: runOptions?.skipTypecheck
|
|
2344
|
+
});
|
|
2345
|
+
if (!buildResult.success) {
|
|
2346
|
+
return {
|
|
2347
|
+
success: false,
|
|
2348
|
+
logs: [],
|
|
2349
|
+
error: buildResult.message ?? `Build failed in ${buildResult.phase} phase`,
|
|
2350
|
+
buildFailure: {
|
|
2351
|
+
phase: buildResult.phase,
|
|
2352
|
+
message: buildResult.message,
|
|
2353
|
+
diagnostics: buildResult.diagnostics,
|
|
2354
|
+
bundleErrors: buildResult.bundleErrors,
|
|
2355
|
+
bundleWarnings: buildResult.bundleWarnings
|
|
2356
|
+
}
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
2359
|
+
const executeResult = await executor.execute(buildResult.code, {
|
|
2360
|
+
entryExport: runOptions?.entryExport ?? "main",
|
|
2361
|
+
context: runOptions?.context,
|
|
2362
|
+
timeout: runOptions?.timeout
|
|
2363
|
+
});
|
|
2364
|
+
return {
|
|
2365
|
+
success: executeResult.success,
|
|
2366
|
+
logs: executeResult.logs,
|
|
2367
|
+
returnValue: executeResult.returnValue,
|
|
2368
|
+
error: executeResult.error,
|
|
2369
|
+
executionTimeMs: executeResult.executionTimeMs
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2372
|
+
const sandboxRef = {
|
|
2373
|
+
fs,
|
|
2374
|
+
install,
|
|
2375
|
+
uninstall,
|
|
2376
|
+
build,
|
|
2377
|
+
typecheck,
|
|
2378
|
+
run
|
|
2379
|
+
};
|
|
2380
|
+
let bashInstance = null;
|
|
2381
|
+
function getBash() {
|
|
2382
|
+
if (!bashInstance) {
|
|
2383
|
+
const commands = createDefaultCommands(sandboxRef);
|
|
2384
|
+
bashInstance = new Bash({
|
|
2385
|
+
cwd: "/",
|
|
2386
|
+
fs: wrapFilesystemForJustBash(fs),
|
|
2387
|
+
customCommands: commands
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
return bashInstance;
|
|
2391
|
+
}
|
|
2392
|
+
async function exec(command) {
|
|
2393
|
+
const bash = getBash();
|
|
2394
|
+
const result = await bash.exec(command);
|
|
2395
|
+
return {
|
|
2396
|
+
exitCode: result.exitCode,
|
|
2397
|
+
stdout: result.stdout,
|
|
2398
|
+
stderr: result.stderr
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
2401
|
+
return {
|
|
2402
|
+
fs,
|
|
2403
|
+
exec,
|
|
2404
|
+
get lastBuild() {
|
|
2405
|
+
return lastBuild;
|
|
2406
|
+
},
|
|
2407
|
+
getState() {
|
|
2408
|
+
return { files: fs.getFiles() };
|
|
2409
|
+
},
|
|
2410
|
+
onBuild(callback) {
|
|
2411
|
+
onBuildCallbacks.add(callback);
|
|
2412
|
+
return () => {
|
|
2413
|
+
onBuildCallbacks.delete(callback);
|
|
2414
|
+
};
|
|
2415
|
+
},
|
|
2416
|
+
install,
|
|
2417
|
+
uninstall,
|
|
2418
|
+
build,
|
|
2419
|
+
typecheck,
|
|
2420
|
+
run,
|
|
2421
|
+
readFile: (path) => fs.readFile(path),
|
|
2422
|
+
writeFile: (path, content) => fs.writeFile(path, content)
|
|
2423
|
+
};
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
// src/core/sandlot.ts
|
|
2427
|
+
function createSandlot(options) {
|
|
2428
|
+
const {
|
|
2429
|
+
bundler,
|
|
2430
|
+
typechecker,
|
|
2431
|
+
typesResolver,
|
|
2432
|
+
executor,
|
|
2433
|
+
sharedModules,
|
|
2434
|
+
sandboxDefaults = {}
|
|
2435
|
+
} = options;
|
|
2436
|
+
const sharedModuleRegistry = createSharedModuleRegistry(sharedModules);
|
|
2437
|
+
const sandboxContext = {
|
|
2438
|
+
bundler,
|
|
2439
|
+
typechecker,
|
|
2440
|
+
typesResolver,
|
|
2441
|
+
sharedModuleRegistry,
|
|
2442
|
+
executor
|
|
2443
|
+
};
|
|
2444
|
+
return {
|
|
2445
|
+
async createSandbox(sandboxOptions = {}) {
|
|
2446
|
+
const fs = Filesystem.create({
|
|
2447
|
+
maxSizeBytes: sandboxOptions.maxFilesystemSize ?? sandboxDefaults.maxFilesystemSize
|
|
2448
|
+
});
|
|
2449
|
+
return createSandboxImpl(fs, sandboxOptions, sandboxContext);
|
|
2450
|
+
},
|
|
2451
|
+
get sharedModules() {
|
|
2452
|
+
return sharedModuleRegistry;
|
|
2453
|
+
}
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
// src/core/esm-types-resolver.ts
|
|
2458
|
+
class InMemoryTypesCache {
|
|
2459
|
+
cache = new Map;
|
|
2460
|
+
async get(key) {
|
|
2461
|
+
return this.cache.get(key) ?? null;
|
|
2462
|
+
}
|
|
2463
|
+
async set(key, value) {
|
|
2464
|
+
this.cache.set(key, value);
|
|
2465
|
+
}
|
|
2466
|
+
async has(key) {
|
|
2467
|
+
return this.cache.has(key);
|
|
2468
|
+
}
|
|
2469
|
+
clear() {
|
|
2470
|
+
this.cache.clear();
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
class EsmTypesResolver {
|
|
2475
|
+
baseUrl;
|
|
2476
|
+
cache;
|
|
2477
|
+
tryTypesPackages;
|
|
2478
|
+
constructor(options = {}) {
|
|
2479
|
+
this.baseUrl = options.baseUrl ?? "https://esm.sh";
|
|
2480
|
+
this.cache = options.cache ?? null;
|
|
2481
|
+
this.tryTypesPackages = options.tryTypesPackages ?? true;
|
|
2482
|
+
}
|
|
2483
|
+
async resolveTypes(specifier, version) {
|
|
2484
|
+
const resolved = await this.resolve(specifier, version);
|
|
2485
|
+
if (!resolved) {
|
|
2486
|
+
return {};
|
|
2487
|
+
}
|
|
2488
|
+
const result = {};
|
|
2489
|
+
const pkgPath = `/node_modules/${resolved.packageName}`;
|
|
2490
|
+
for (const [relativePath, content] of Object.entries(resolved.files)) {
|
|
2491
|
+
result[`${pkgPath}/${relativePath}`] = content;
|
|
2492
|
+
}
|
|
2493
|
+
return result;
|
|
2494
|
+
}
|
|
2495
|
+
async resolve(specifier, version) {
|
|
2496
|
+
const { packageName, subpath } = parseSpecifier(specifier);
|
|
2497
|
+
const cacheKey = makeCacheKey(packageName, subpath, version);
|
|
2498
|
+
if (this.cache) {
|
|
2499
|
+
const cached = await this.cache.get(cacheKey);
|
|
2500
|
+
if (cached) {
|
|
2501
|
+
return cached;
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
let result = await this.tryResolve(packageName, subpath, version);
|
|
2505
|
+
if (!result && this.tryTypesPackages && !packageName.startsWith("@types/")) {
|
|
2506
|
+
const typesPackageName = toTypesPackageName(packageName);
|
|
2507
|
+
result = await this.tryResolve(typesPackageName, subpath, version);
|
|
2508
|
+
if (result) {
|
|
2509
|
+
result.fromTypesPackage = true;
|
|
2510
|
+
result.packageName = packageName;
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
if (result && this.cache) {
|
|
2514
|
+
await this.cache.set(cacheKey, result);
|
|
2515
|
+
}
|
|
2516
|
+
return result;
|
|
2517
|
+
}
|
|
2518
|
+
async tryResolve(packageName, subpath, version) {
|
|
2519
|
+
try {
|
|
2520
|
+
const versionSuffix = version ? `@${version}` : "";
|
|
2521
|
+
const pathSuffix = subpath ? `/${subpath}` : "";
|
|
2522
|
+
const url = `${this.baseUrl}/${packageName}${versionSuffix}${pathSuffix}`;
|
|
2523
|
+
const response = await fetch(url, { method: "HEAD" });
|
|
2524
|
+
if (!response.ok) {
|
|
2525
|
+
return null;
|
|
2526
|
+
}
|
|
2527
|
+
const resolvedVersion = this.extractVersion(response, packageName, version);
|
|
2528
|
+
const typesHeader = response.headers.get("X-TypeScript-Types");
|
|
2529
|
+
if (!typesHeader) {
|
|
2530
|
+
return null;
|
|
2531
|
+
}
|
|
2532
|
+
const typesUrl = new URL(typesHeader, response.url).href;
|
|
2533
|
+
const files = await this.fetchTypesRecursively(typesUrl, subpath);
|
|
2534
|
+
if (Object.keys(files).length === 0) {
|
|
2535
|
+
return null;
|
|
2536
|
+
}
|
|
2537
|
+
return {
|
|
2538
|
+
packageName,
|
|
2539
|
+
version: resolvedVersion,
|
|
2540
|
+
files,
|
|
2541
|
+
fromTypesPackage: packageName.startsWith("@types/")
|
|
2542
|
+
};
|
|
2543
|
+
} catch {
|
|
2544
|
+
return null;
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
async fetchTypesRecursively(entryUrl, subpath, visited = new Set) {
|
|
2548
|
+
if (visited.has(entryUrl)) {
|
|
2549
|
+
return {};
|
|
2550
|
+
}
|
|
2551
|
+
visited.add(entryUrl);
|
|
2552
|
+
const response = await fetch(entryUrl);
|
|
2553
|
+
if (!response.ok) {
|
|
2554
|
+
return {};
|
|
2555
|
+
}
|
|
2556
|
+
const content = await response.text();
|
|
2557
|
+
const files = {};
|
|
2558
|
+
const fileName = subpath ? `${subpath}.d.ts` : "index.d.ts";
|
|
2559
|
+
files[fileName] = content;
|
|
2560
|
+
if (subpath) {
|
|
2561
|
+
files[`${subpath}/index.d.ts`] = content;
|
|
2562
|
+
}
|
|
2563
|
+
const refs = parseReferences(content);
|
|
2564
|
+
for (const ref of refs.paths) {
|
|
2565
|
+
const refUrl = new URL(ref, entryUrl).href;
|
|
2566
|
+
const refFiles = await this.fetchTypesRecursively(refUrl, undefined, visited);
|
|
2567
|
+
for (const [refPath, refContent] of Object.entries(refFiles)) {
|
|
2568
|
+
const normalizedRef = ref.replace(/^\.\//, "");
|
|
2569
|
+
if (refPath === "index.d.ts") {
|
|
2570
|
+
files[normalizedRef] = refContent;
|
|
2571
|
+
} else {
|
|
2572
|
+
const dir = normalizedRef.replace(/\.d\.ts$/, "");
|
|
2573
|
+
files[`${dir}/${refPath}`] = refContent;
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
return files;
|
|
2578
|
+
}
|
|
2579
|
+
extractVersion(response, packageName, requestedVersion) {
|
|
2580
|
+
const esmId = response.headers.get("x-esm-id");
|
|
2581
|
+
if (esmId) {
|
|
2582
|
+
const match = esmId.match(new RegExp(`${escapeRegex(packageName)}@([^/]+)`));
|
|
2583
|
+
if (match?.[1]) {
|
|
2584
|
+
return match[1];
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
const urlMatch = response.url.match(new RegExp(`${escapeRegex(packageName)}@([^/]+)`));
|
|
2588
|
+
if (urlMatch?.[1]) {
|
|
2589
|
+
return urlMatch[1];
|
|
2590
|
+
}
|
|
2591
|
+
return requestedVersion ?? "latest";
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
function parseSpecifier(specifier) {
|
|
2595
|
+
if (specifier.startsWith("@")) {
|
|
2596
|
+
const parts = specifier.split("/");
|
|
2597
|
+
if (parts.length >= 2) {
|
|
2598
|
+
const packageName = `${parts[0]}/${parts[1]}`;
|
|
2599
|
+
const subpath = parts.length > 2 ? parts.slice(2).join("/") : undefined;
|
|
2600
|
+
return { packageName, subpath };
|
|
2601
|
+
}
|
|
2602
|
+
return { packageName: specifier, subpath: undefined };
|
|
2603
|
+
}
|
|
2604
|
+
const slashIndex = specifier.indexOf("/");
|
|
2605
|
+
if (slashIndex === -1) {
|
|
2606
|
+
return { packageName: specifier, subpath: undefined };
|
|
2607
|
+
}
|
|
2608
|
+
return {
|
|
2609
|
+
packageName: specifier.slice(0, slashIndex),
|
|
2610
|
+
subpath: specifier.slice(slashIndex + 1)
|
|
2611
|
+
};
|
|
2612
|
+
}
|
|
2613
|
+
function toTypesPackageName(packageName) {
|
|
2614
|
+
if (packageName.startsWith("@")) {
|
|
2615
|
+
return "@types/" + packageName.slice(1).replace("/", "__");
|
|
2616
|
+
}
|
|
2617
|
+
return `@types/${packageName}`;
|
|
2618
|
+
}
|
|
2619
|
+
function parseReferences(content) {
|
|
2620
|
+
const paths = [];
|
|
2621
|
+
const types = [];
|
|
2622
|
+
const pathRegex = /\/\/\/\s*<reference\s+path="([^"]+)"\s*\/>/g;
|
|
2623
|
+
let match;
|
|
2624
|
+
while ((match = pathRegex.exec(content)) !== null) {
|
|
2625
|
+
if (match[1])
|
|
2626
|
+
paths.push(match[1]);
|
|
2627
|
+
}
|
|
2628
|
+
const typesRegex = /\/\/\/\s*<reference\s+types="([^"]+)"\s*\/>/g;
|
|
2629
|
+
while ((match = typesRegex.exec(content)) !== null) {
|
|
2630
|
+
if (match[1])
|
|
2631
|
+
types.push(match[1]);
|
|
2632
|
+
}
|
|
2633
|
+
return { paths, types };
|
|
2634
|
+
}
|
|
2635
|
+
function makeCacheKey(packageName, subpath, version) {
|
|
2636
|
+
const base = version ? `${packageName}@${version}` : packageName;
|
|
2637
|
+
return subpath ? `${base}/${subpath}` : base;
|
|
2638
|
+
}
|
|
2639
|
+
function escapeRegex(str) {
|
|
2640
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
// src/browser/preset.ts
|
|
2644
|
+
async function createBrowserSandlot(options = {}) {
|
|
2645
|
+
const { bundler, typechecker, typesResolver, executor, ...rest } = options;
|
|
2646
|
+
const bundlerInstance = isBundler(bundler) ? bundler : new EsbuildWasmBundler(bundler);
|
|
2647
|
+
await bundlerInstance.initialize();
|
|
2648
|
+
const typecheckerInstance = typechecker === false ? undefined : isTypechecker(typechecker) ? typechecker : new Typechecker(typechecker);
|
|
2649
|
+
const typesResolverInstance = typesResolver === false ? undefined : isTypesResolver(typesResolver) ? typesResolver : new EsmTypesResolver(typesResolver);
|
|
2650
|
+
const executorInstance = executor === false ? undefined : isExecutor(executor) ? executor : new MainThreadExecutor(executor);
|
|
2651
|
+
return createSandlot({
|
|
2652
|
+
...rest,
|
|
2653
|
+
bundler: bundlerInstance,
|
|
2654
|
+
typechecker: typecheckerInstance,
|
|
2655
|
+
typesResolver: typesResolverInstance,
|
|
2656
|
+
executor: executorInstance
|
|
2657
|
+
});
|
|
2658
|
+
}
|
|
2659
|
+
function isBundler(value) {
|
|
2660
|
+
return typeof value === "object" && value !== null && "bundle" in value && typeof value.bundle === "function";
|
|
2661
|
+
}
|
|
2662
|
+
function isTypechecker(value) {
|
|
2663
|
+
return typeof value === "object" && value !== null && "typecheck" in value && typeof value.typecheck === "function";
|
|
2664
|
+
}
|
|
2665
|
+
function isTypesResolver(value) {
|
|
2666
|
+
return typeof value === "object" && value !== null && "resolveTypes" in value && typeof value.resolveTypes === "function";
|
|
2667
|
+
}
|
|
2668
|
+
function isExecutor(value) {
|
|
2669
|
+
return typeof value === "object" && value !== null && "execute" in value && typeof value.execute === "function";
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
// src/browser/index.ts
|
|
2673
|
+
if (typeof window !== "undefined" && typeof globalThis.process === "undefined") {
|
|
2674
|
+
globalThis.process = {
|
|
2675
|
+
env: {},
|
|
2676
|
+
platform: "browser",
|
|
2677
|
+
version: "v20.0.0",
|
|
2678
|
+
browser: true,
|
|
2679
|
+
cwd: () => "/",
|
|
2680
|
+
nextTick: (fn) => setTimeout(fn, 0)
|
|
2681
|
+
};
|
|
2682
|
+
}
|
|
2683
|
+
export {
|
|
2684
|
+
createTypechecker,
|
|
2685
|
+
createMainThreadExecutor,
|
|
2686
|
+
createBrowserSandlot,
|
|
2687
|
+
Typechecker,
|
|
2688
|
+
MainThreadExecutor,
|
|
2689
|
+
EsbuildWasmBundler
|
|
2690
|
+
};
|