sandlot 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +145 -518
- package/dist/build-emitter.d.ts +47 -0
- package/dist/build-emitter.d.ts.map +1 -0
- package/dist/builder.d.ts +370 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/bundler.d.ts +3 -3
- package/dist/bundler.d.ts.map +1 -1
- package/dist/commands/compile.d.ts +13 -0
- package/dist/commands/compile.d.ts.map +1 -0
- package/dist/commands/index.d.ts +17 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/packages.d.ts +17 -0
- package/dist/commands/packages.d.ts.map +1 -0
- package/dist/commands/run.d.ts +40 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/types.d.ts +141 -0
- package/dist/commands/types.d.ts.map +1 -0
- package/dist/fs.d.ts +60 -42
- package/dist/fs.d.ts.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +304 -491
- package/dist/internal.d.ts +5 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +174 -95
- package/dist/runner.d.ts +314 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/sandbox-manager.d.ts +45 -33
- package/dist/sandbox-manager.d.ts.map +1 -1
- package/dist/sandbox.d.ts +144 -70
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/shared-modules.d.ts +22 -3
- package/dist/shared-modules.d.ts.map +1 -1
- package/dist/shared-resources.d.ts +0 -3
- package/dist/shared-resources.d.ts.map +1 -1
- package/dist/typechecker.d.ts +1 -1
- package/package.json +3 -17
- package/src/build-emitter.ts +64 -0
- package/src/builder.ts +498 -0
- package/src/bundler.ts +86 -57
- package/src/commands/compile.ts +236 -0
- package/src/commands/index.ts +51 -0
- package/src/commands/packages.ts +154 -0
- package/src/commands/run.ts +245 -0
- package/src/commands/types.ts +172 -0
- package/src/fs.ts +90 -216
- package/src/index.ts +34 -12
- package/src/internal.ts +5 -2
- package/src/sandbox.ts +214 -220
- package/src/shared-modules.ts +74 -4
- package/src/shared-resources.ts +0 -3
- package/src/ts-libs.ts +1 -1
- package/src/typechecker.ts +1 -1
- package/dist/react.d.ts +0 -159
- package/dist/react.d.ts.map +0 -1
- package/dist/react.js +0 -149
- package/src/commands.ts +0 -733
- package/src/react.tsx +0 -331
- package/src/sandbox-manager.ts +0 -490
package/src/bundler.ts
CHANGED
|
@@ -1,7 +1,32 @@
|
|
|
1
1
|
import type { IFileSystem } from "just-bash/browser";
|
|
2
|
-
import * as
|
|
2
|
+
import type * as EsbuildTypes from "esbuild-wasm";
|
|
3
3
|
import { getPackageManifest, resolveToEsmUrl } from "./packages";
|
|
4
|
-
import { getSharedModuleRuntimeCode } from "./shared-modules";
|
|
4
|
+
import { getSharedModuleRuntimeCode, getSharedModuleExports } from "./shared-modules";
|
|
5
|
+
|
|
6
|
+
// Lazily loaded esbuild module - loaded from CDN to avoid bundler issues
|
|
7
|
+
let esbuild: typeof EsbuildTypes | null = null;
|
|
8
|
+
|
|
9
|
+
async function getEsbuild(): Promise<typeof EsbuildTypes> {
|
|
10
|
+
if (esbuild) return esbuild;
|
|
11
|
+
|
|
12
|
+
// Load esbuild-wasm from esm.sh CDN to avoid bundler transformation issues
|
|
13
|
+
// esm.sh provides proper ESM wrappers for npm packages
|
|
14
|
+
const cdnUrl = `https://esm.sh/esbuild-wasm@${ESBUILD_VERSION}`;
|
|
15
|
+
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
+
const mod: any = await import(/* @vite-ignore */ cdnUrl);
|
|
18
|
+
|
|
19
|
+
// esm.sh typically provides both default and named exports
|
|
20
|
+
esbuild = mod.default ?? mod;
|
|
21
|
+
|
|
22
|
+
// Verify we have the initialize function
|
|
23
|
+
if (typeof esbuild?.initialize !== 'function') {
|
|
24
|
+
console.error('esbuild-wasm module structure:', mod);
|
|
25
|
+
throw new Error('Failed to load esbuild-wasm: initialize function not found');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return esbuild;
|
|
29
|
+
}
|
|
5
30
|
|
|
6
31
|
/**
|
|
7
32
|
* How to handle npm package imports (bare imports like "react").
|
|
@@ -96,7 +121,7 @@ export interface BundleResult {
|
|
|
96
121
|
/**
|
|
97
122
|
* Any warnings from esbuild
|
|
98
123
|
*/
|
|
99
|
-
warnings:
|
|
124
|
+
warnings: EsbuildTypes.Message[];
|
|
100
125
|
|
|
101
126
|
/**
|
|
102
127
|
* List of files that were included in the bundle
|
|
@@ -104,6 +129,11 @@ export interface BundleResult {
|
|
|
104
129
|
includedFiles: string[];
|
|
105
130
|
}
|
|
106
131
|
|
|
132
|
+
/**
|
|
133
|
+
* esbuild-wasm version - MUST match the version in package.json dependencies
|
|
134
|
+
*/
|
|
135
|
+
const ESBUILD_VERSION = "0.27.2";
|
|
136
|
+
|
|
107
137
|
// Track initialization state
|
|
108
138
|
let initialized = false;
|
|
109
139
|
let initPromise: Promise<void> | null = null;
|
|
@@ -112,9 +142,27 @@ let initPromise: Promise<void> | null = null;
|
|
|
112
142
|
* Get the esbuild-wasm binary URL based on the installed version
|
|
113
143
|
*/
|
|
114
144
|
function getWasmUrl(): string {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
145
|
+
return `https://unpkg.com/esbuild-wasm@${ESBUILD_VERSION}/esbuild.wasm`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Check if the browser environment supports cross-origin isolation.
|
|
150
|
+
* This is needed for SharedArrayBuffer which esbuild-wasm may use.
|
|
151
|
+
*/
|
|
152
|
+
function checkCrossOriginIsolation(): void {
|
|
153
|
+
if (typeof window === "undefined") return; // Not in browser
|
|
154
|
+
|
|
155
|
+
// crossOriginIsolated is true when COOP/COEP headers are set correctly
|
|
156
|
+
if (!window.crossOriginIsolated) {
|
|
157
|
+
console.warn(
|
|
158
|
+
"[sandlot] Cross-origin isolation is not enabled. " +
|
|
159
|
+
"esbuild-wasm may have reduced performance or fail on some browsers.\n" +
|
|
160
|
+
"To enable, add these headers to your dev server:\n" +
|
|
161
|
+
" Cross-Origin-Embedder-Policy: require-corp\n" +
|
|
162
|
+
" Cross-Origin-Opener-Policy: same-origin\n" +
|
|
163
|
+
"In Vite, add a plugin to configureServer. See sandlot README for details."
|
|
164
|
+
);
|
|
165
|
+
}
|
|
118
166
|
}
|
|
119
167
|
|
|
120
168
|
/**
|
|
@@ -129,9 +177,14 @@ export async function initBundler(): Promise<void> {
|
|
|
129
177
|
return;
|
|
130
178
|
}
|
|
131
179
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
180
|
+
checkCrossOriginIsolation();
|
|
181
|
+
|
|
182
|
+
initPromise = (async () => {
|
|
183
|
+
const es = await getEsbuild();
|
|
184
|
+
await es.initialize({
|
|
185
|
+
wasmURL: getWasmUrl(),
|
|
186
|
+
});
|
|
187
|
+
})();
|
|
135
188
|
|
|
136
189
|
await initPromise;
|
|
137
190
|
initialized = true;
|
|
@@ -147,7 +200,7 @@ function isBareImport(path: string): boolean {
|
|
|
147
200
|
/**
|
|
148
201
|
* Get the appropriate loader based on file extension
|
|
149
202
|
*/
|
|
150
|
-
function getLoader(path: string):
|
|
203
|
+
function getLoader(path: string): EsbuildTypes.Loader {
|
|
151
204
|
const ext = path.split(".").pop()?.toLowerCase();
|
|
152
205
|
switch (ext) {
|
|
153
206
|
case "ts":
|
|
@@ -207,7 +260,7 @@ function matchesSharedModule(importPath: string, sharedModuleIds: Set<string>):
|
|
|
207
260
|
/**
|
|
208
261
|
* Create an esbuild plugin that reads from a virtual filesystem
|
|
209
262
|
*/
|
|
210
|
-
function createVfsPlugin(options: VfsPluginOptions):
|
|
263
|
+
function createVfsPlugin(options: VfsPluginOptions): EsbuildTypes.Plugin {
|
|
211
264
|
const {
|
|
212
265
|
fs,
|
|
213
266
|
entryPoint,
|
|
@@ -303,27 +356,14 @@ function createVfsPlugin(options: VfsPluginOptions): esbuild.Plugin {
|
|
|
303
356
|
|
|
304
357
|
// Load shared modules from the registry
|
|
305
358
|
build.onLoad({ filter: /.*/, namespace: "sandlot-shared" }, (args) => {
|
|
306
|
-
// Generate code that
|
|
307
|
-
const contents = `
|
|
308
|
-
export * from ${JSON.stringify(args.path)};
|
|
309
|
-
// Re-export all named exports by importing from registry
|
|
310
|
-
const __mod__ = ${getSharedModuleRuntimeCode(args.path)};
|
|
311
|
-
for (const __k__ in __mod__) {
|
|
312
|
-
if (__k__ !== 'default') Object.defineProperty(exports, __k__, {
|
|
313
|
-
enumerable: true,
|
|
314
|
-
get: function() { return __mod__[__k__]; }
|
|
315
|
-
});
|
|
316
|
-
}`;
|
|
317
|
-
|
|
318
|
-
// For ESM format, we need a different approach
|
|
319
|
-
// Generate a simple module that re-exports from registry
|
|
320
|
-
const esmContents = `
|
|
359
|
+
// Generate ESM code that re-exports from the shared module registry
|
|
360
|
+
const contents = `
|
|
321
361
|
const __sandlot_mod__ = ${getSharedModuleRuntimeCode(args.path)};
|
|
322
362
|
export default __sandlot_mod__.default ?? __sandlot_mod__;
|
|
323
363
|
${generateNamedExports(args.path)}
|
|
324
364
|
`;
|
|
325
365
|
return {
|
|
326
|
-
contents:
|
|
366
|
+
contents: contents.trim(),
|
|
327
367
|
loader: 'js'
|
|
328
368
|
};
|
|
329
369
|
});
|
|
@@ -349,39 +389,26 @@ ${generateNamedExports(args.path)}
|
|
|
349
389
|
}
|
|
350
390
|
|
|
351
391
|
/**
|
|
352
|
-
* Generate named export statements for
|
|
353
|
-
*
|
|
354
|
-
*
|
|
392
|
+
* Generate named export statements for shared modules.
|
|
393
|
+
*
|
|
394
|
+
* Uses dynamically discovered exports from the SharedModuleRegistry,
|
|
395
|
+
* which are populated when registerSharedModules() is called.
|
|
396
|
+
*
|
|
397
|
+
* If the module wasn't registered (or has no enumerable exports),
|
|
398
|
+
* returns a comment - named imports won't work but default import will.
|
|
355
399
|
*/
|
|
356
400
|
function generateNamedExports(moduleId: string): string {
|
|
357
|
-
|
|
358
|
-
const knownExports: Record<string, string[]> = {
|
|
359
|
-
'react': [
|
|
360
|
-
'useState', 'useEffect', 'useContext', 'useReducer', 'useCallback',
|
|
361
|
-
'useMemo', 'useRef', 'useImperativeHandle', 'useLayoutEffect',
|
|
362
|
-
'useDebugValue', 'useDeferredValue', 'useTransition', 'useId',
|
|
363
|
-
'useSyncExternalStore', 'useInsertionEffect', 'useOptimistic', 'useActionState',
|
|
364
|
-
'createElement', 'cloneElement', 'createContext', 'forwardRef', 'lazy', 'memo',
|
|
365
|
-
'startTransition', 'Children', 'Component', 'PureComponent', 'Fragment',
|
|
366
|
-
'Profiler', 'StrictMode', 'Suspense', 'version', 'isValidElement',
|
|
367
|
-
],
|
|
368
|
-
'react-dom': ['createPortal', 'flushSync', 'version'],
|
|
369
|
-
'react-dom/client': ['createRoot', 'hydrateRoot'],
|
|
370
|
-
'react-dom/server': ['renderToString', 'renderToStaticMarkup', 'renderToPipeableStream'],
|
|
371
|
-
};
|
|
401
|
+
const exports = getSharedModuleExports(moduleId);
|
|
372
402
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
// Dynamic re-export for unknown module
|
|
378
|
-
export const __moduleProxy__ = __sandlot_mod__;
|
|
379
|
-
`;
|
|
403
|
+
if (exports.length > 0) {
|
|
404
|
+
return exports
|
|
405
|
+
.map(name => `export const ${name} = __sandlot_mod__.${name};`)
|
|
406
|
+
.join('\n');
|
|
380
407
|
}
|
|
381
408
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
409
|
+
// Module not registered or has no enumerable exports
|
|
410
|
+
// Default import will still work: import foo from 'module'
|
|
411
|
+
return `// No exports discovered for "${moduleId}" - use default import or call registerSharedModules() first`;
|
|
385
412
|
}
|
|
386
413
|
|
|
387
414
|
/**
|
|
@@ -389,7 +416,7 @@ export const __moduleProxy__ = __sandlot_mod__;
|
|
|
389
416
|
*
|
|
390
417
|
* @example
|
|
391
418
|
* ```ts
|
|
392
|
-
* const fs =
|
|
419
|
+
* const fs = Filesystem.create({
|
|
393
420
|
* initialFiles: {
|
|
394
421
|
* "/src/index.ts": "export const hello = 'world';",
|
|
395
422
|
* "/src/utils.ts": "export function add(a: number, b: number) { return a + b; }",
|
|
@@ -445,7 +472,8 @@ export async function bundle(options: BundleOptions): Promise<BundleResult> {
|
|
|
445
472
|
sharedModuleIds,
|
|
446
473
|
});
|
|
447
474
|
|
|
448
|
-
const
|
|
475
|
+
const es = await getEsbuild();
|
|
476
|
+
const result = await es.build({
|
|
449
477
|
entryPoints: [normalizedEntry],
|
|
450
478
|
bundle: true,
|
|
451
479
|
write: false,
|
|
@@ -456,6 +484,7 @@ export async function bundle(options: BundleOptions): Promise<BundleResult> {
|
|
|
456
484
|
target,
|
|
457
485
|
external,
|
|
458
486
|
plugins: [plugin],
|
|
487
|
+
jsx: "automatic",
|
|
459
488
|
});
|
|
460
489
|
|
|
461
490
|
const code = result.outputFiles?.[0]?.text ?? "";
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile-related commands: tsc and build
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { defineCommand, type CommandContext } from "just-bash/browser";
|
|
6
|
+
import { typecheck, formatDiagnosticsForAgent, type TypecheckResult } from "../typechecker";
|
|
7
|
+
import { bundle } from "../bundler";
|
|
8
|
+
import { loadModule } from "../loader";
|
|
9
|
+
import { type CommandDeps, formatEsbuildMessages } from "./types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create the `tsc` command for type checking
|
|
13
|
+
*/
|
|
14
|
+
export function createTscCommand(deps: CommandDeps) {
|
|
15
|
+
const { fs, libFiles, tsconfigPath } = deps;
|
|
16
|
+
|
|
17
|
+
return defineCommand("tsc", async (args, _ctx: CommandContext) => {
|
|
18
|
+
const entryPoint = args[0];
|
|
19
|
+
if (!entryPoint) {
|
|
20
|
+
return {
|
|
21
|
+
stdout: "",
|
|
22
|
+
stderr: `Usage: tsc <entry-point>\n\nExample: tsc /src/index.ts\n`,
|
|
23
|
+
exitCode: 1,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// Check if entry point exists
|
|
29
|
+
if (!(await fs.exists(entryPoint))) {
|
|
30
|
+
return {
|
|
31
|
+
stdout: "",
|
|
32
|
+
stderr: `Error: Entry point not found: ${entryPoint}\n`,
|
|
33
|
+
exitCode: 1,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const result = await typecheck({
|
|
38
|
+
fs,
|
|
39
|
+
entryPoint,
|
|
40
|
+
tsconfigPath,
|
|
41
|
+
libFiles,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (result.hasErrors) {
|
|
45
|
+
const formatted = formatDiagnosticsForAgent(result.diagnostics);
|
|
46
|
+
return {
|
|
47
|
+
stdout: "",
|
|
48
|
+
stderr: formatted + "\n",
|
|
49
|
+
exitCode: 1,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const checkedCount = result.checkedFiles.length;
|
|
54
|
+
const warningCount = result.diagnostics.filter((d) => d.category === "warning").length;
|
|
55
|
+
|
|
56
|
+
let output = `Type check passed. Checked ${checkedCount} file(s).\n`;
|
|
57
|
+
if (warningCount > 0) {
|
|
58
|
+
output += `\nWarnings:\n${formatDiagnosticsForAgent(result.diagnostics.filter((d) => d.category === "warning"))}\n`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
stdout: output,
|
|
63
|
+
stderr: "",
|
|
64
|
+
exitCode: 0,
|
|
65
|
+
};
|
|
66
|
+
} catch (err) {
|
|
67
|
+
return {
|
|
68
|
+
stdout: "",
|
|
69
|
+
stderr: `Type check failed: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
70
|
+
exitCode: 1,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create the `build` command for bundling (with automatic type checking)
|
|
78
|
+
*/
|
|
79
|
+
export function createBuildCommand(deps: CommandDeps) {
|
|
80
|
+
const { fs, libFiles, tsconfigPath, onBuild, getValidation, sharedModules } = deps;
|
|
81
|
+
|
|
82
|
+
return defineCommand("build", async (args, _ctx: CommandContext) => {
|
|
83
|
+
// Parse arguments
|
|
84
|
+
let entryPoint: string | null = null;
|
|
85
|
+
let skipTypecheck = false;
|
|
86
|
+
let minify = false;
|
|
87
|
+
let format: "esm" | "iife" | "cjs" = "esm";
|
|
88
|
+
|
|
89
|
+
for (let i = 0; i < args.length; i++) {
|
|
90
|
+
const arg = args[i];
|
|
91
|
+
if (arg === "--skip-typecheck" || arg === "-s") {
|
|
92
|
+
skipTypecheck = true;
|
|
93
|
+
} else if (arg === "--minify" || arg === "-m") {
|
|
94
|
+
minify = true;
|
|
95
|
+
} else if ((arg === "--format" || arg === "-f") && args[i + 1]) {
|
|
96
|
+
const f = args[++i]!.toLowerCase();
|
|
97
|
+
if (f === "esm" || f === "iife" || f === "cjs") {
|
|
98
|
+
format = f;
|
|
99
|
+
}
|
|
100
|
+
} else if (!arg!.startsWith("-")) {
|
|
101
|
+
entryPoint = arg!;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Entry point is required
|
|
106
|
+
if (!entryPoint) {
|
|
107
|
+
return {
|
|
108
|
+
stdout: "",
|
|
109
|
+
stderr: `Usage: build <entry-point> [options]\n\nOptions:\n --skip-typecheck, -s Skip type checking\n --minify, -m Minify output\n --format, -f <fmt> Output format (esm|iife|cjs)\n\nExample: build /src/index.ts\n`,
|
|
110
|
+
exitCode: 1,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
// Check if entry point exists
|
|
116
|
+
if (!(await fs.exists(entryPoint))) {
|
|
117
|
+
return {
|
|
118
|
+
stdout: "",
|
|
119
|
+
stderr: `Error: Entry point not found: ${entryPoint}\n`,
|
|
120
|
+
exitCode: 1,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Step 1: Type check (unless skipped)
|
|
125
|
+
let typecheckResult: TypecheckResult | null = null;
|
|
126
|
+
if (!skipTypecheck) {
|
|
127
|
+
typecheckResult = await typecheck({
|
|
128
|
+
fs,
|
|
129
|
+
entryPoint,
|
|
130
|
+
tsconfigPath,
|
|
131
|
+
libFiles,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (typecheckResult.hasErrors) {
|
|
135
|
+
const formatted = formatDiagnosticsForAgent(typecheckResult.diagnostics);
|
|
136
|
+
return {
|
|
137
|
+
stdout: "",
|
|
138
|
+
stderr: `Build failed: Type errors found.\n\n${formatted}\n`,
|
|
139
|
+
exitCode: 1,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Step 2: Bundle
|
|
145
|
+
const bundleResult = await bundle({
|
|
146
|
+
fs,
|
|
147
|
+
entryPoint,
|
|
148
|
+
format,
|
|
149
|
+
minify,
|
|
150
|
+
sharedModules,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Step 3: Load module
|
|
154
|
+
let loadedModule: Record<string, unknown>;
|
|
155
|
+
try {
|
|
156
|
+
loadedModule = await loadModule<Record<string, unknown>>(bundleResult);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
159
|
+
return {
|
|
160
|
+
stdout: "",
|
|
161
|
+
stderr: `Build failed: Module failed to load.\n\n${errorMessage}\n`,
|
|
162
|
+
exitCode: 1,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Step 4: Validate (if validation function is set)
|
|
167
|
+
const validateFn = getValidation?.();
|
|
168
|
+
let validatedModule = loadedModule;
|
|
169
|
+
|
|
170
|
+
if (validateFn) {
|
|
171
|
+
try {
|
|
172
|
+
validatedModule = validateFn(loadedModule);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
175
|
+
return {
|
|
176
|
+
stdout: "",
|
|
177
|
+
stderr: `Build failed: Validation error.\n\n${errorMessage}\n`,
|
|
178
|
+
exitCode: 1,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Invoke callback with build output (bundle + validated module)
|
|
184
|
+
if (onBuild) {
|
|
185
|
+
await onBuild({ bundle: bundleResult, module: validatedModule });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Build success message
|
|
189
|
+
let output = `Build successful!\n`;
|
|
190
|
+
output += `Entry: ${entryPoint}\n`;
|
|
191
|
+
output += `Format: ${format}\n`;
|
|
192
|
+
output += `Size: ${(bundleResult.code.length / 1024).toFixed(2)} KB\n`;
|
|
193
|
+
|
|
194
|
+
if (typecheckResult) {
|
|
195
|
+
output += `Type checked: ${typecheckResult.checkedFiles.length} file(s)\n`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
output += `Bundled: ${bundleResult.includedFiles.length} file(s)\n`;
|
|
199
|
+
|
|
200
|
+
// Show exports for visibility
|
|
201
|
+
const exportNames = Object.keys(loadedModule).filter((k) => !k.startsWith("__"));
|
|
202
|
+
if (exportNames.length > 0) {
|
|
203
|
+
output += `Exports: ${exportNames.join(", ")}\n`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (validateFn) {
|
|
207
|
+
output += `Validation: passed\n`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Include warnings if any
|
|
211
|
+
if (bundleResult.warnings.length > 0) {
|
|
212
|
+
output += `\nBuild warnings:\n${formatEsbuildMessages(bundleResult.warnings)}\n`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (typecheckResult) {
|
|
216
|
+
const warnings = typecheckResult.diagnostics.filter((d) => d.category === "warning");
|
|
217
|
+
if (warnings.length > 0) {
|
|
218
|
+
output += `\nType warnings:\n${formatDiagnosticsForAgent(warnings)}\n`;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
stdout: output,
|
|
224
|
+
stderr: "",
|
|
225
|
+
exitCode: 0,
|
|
226
|
+
};
|
|
227
|
+
} catch (err) {
|
|
228
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
229
|
+
return {
|
|
230
|
+
stdout: "",
|
|
231
|
+
stderr: `Build failed: ${errorMessage}\n`,
|
|
232
|
+
exitCode: 1,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command factories for sandbox bash environments.
|
|
3
|
+
*
|
|
4
|
+
* Pure factories that create commands for type checking, bundling,
|
|
5
|
+
* package management, and code execution.
|
|
6
|
+
* No global state - all dependencies are passed in explicitly.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Types and utilities
|
|
10
|
+
export {
|
|
11
|
+
type CommandDeps,
|
|
12
|
+
type BuildOutput,
|
|
13
|
+
type ValidateFn,
|
|
14
|
+
type RunContext,
|
|
15
|
+
type RunOptions,
|
|
16
|
+
type RunResult,
|
|
17
|
+
formatEsbuildMessages,
|
|
18
|
+
} from "./types";
|
|
19
|
+
|
|
20
|
+
// Compile commands (tsc, build)
|
|
21
|
+
export { createTscCommand, createBuildCommand } from "./compile";
|
|
22
|
+
|
|
23
|
+
// Package management commands (install, uninstall, list)
|
|
24
|
+
export {
|
|
25
|
+
createInstallCommand,
|
|
26
|
+
createUninstallCommand,
|
|
27
|
+
createListCommand,
|
|
28
|
+
} from "./packages";
|
|
29
|
+
|
|
30
|
+
// Run command
|
|
31
|
+
export { createRunCommand } from "./run";
|
|
32
|
+
|
|
33
|
+
// Re-import for createDefaultCommands
|
|
34
|
+
import type { CommandDeps } from "./types";
|
|
35
|
+
import { createTscCommand, createBuildCommand } from "./compile";
|
|
36
|
+
import { createInstallCommand, createUninstallCommand, createListCommand } from "./packages";
|
|
37
|
+
import { createRunCommand } from "./run";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create all default sandbox commands
|
|
41
|
+
*/
|
|
42
|
+
export function createDefaultCommands(deps: CommandDeps) {
|
|
43
|
+
return [
|
|
44
|
+
createTscCommand(deps),
|
|
45
|
+
createBuildCommand(deps),
|
|
46
|
+
createRunCommand(deps),
|
|
47
|
+
createInstallCommand(deps),
|
|
48
|
+
createUninstallCommand(deps),
|
|
49
|
+
createListCommand(deps),
|
|
50
|
+
];
|
|
51
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package management commands: install, uninstall, list
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { defineCommand, type CommandContext } from "just-bash/browser";
|
|
6
|
+
import { installPackage, uninstallPackage, listPackages } from "../packages";
|
|
7
|
+
import type { CommandDeps } from "./types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create the `install` command for adding packages from npm
|
|
11
|
+
*/
|
|
12
|
+
export function createInstallCommand(deps: CommandDeps) {
|
|
13
|
+
const { fs, typesCache } = deps;
|
|
14
|
+
|
|
15
|
+
return defineCommand("install", async (args, _ctx: CommandContext) => {
|
|
16
|
+
if (args.length === 0) {
|
|
17
|
+
return {
|
|
18
|
+
stdout: "",
|
|
19
|
+
stderr: "Usage: install <package>[@version] [...packages]\n\nExamples:\n install react\n install lodash@4.17.21\n install @tanstack/react-query@5\n",
|
|
20
|
+
exitCode: 1,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const results: string[] = [];
|
|
25
|
+
let hasError = false;
|
|
26
|
+
|
|
27
|
+
for (const packageSpec of args) {
|
|
28
|
+
try {
|
|
29
|
+
const result = await installPackage(fs, packageSpec!, { cache: typesCache });
|
|
30
|
+
|
|
31
|
+
let status = `+ ${result.name}@${result.version}`;
|
|
32
|
+
if (result.typesInstalled) {
|
|
33
|
+
status += ` (${result.typeFilesCount} type file${result.typeFilesCount !== 1 ? "s" : ""})`;
|
|
34
|
+
if (result.fromCache) {
|
|
35
|
+
status += " [cached]";
|
|
36
|
+
}
|
|
37
|
+
} else if (result.typesError) {
|
|
38
|
+
status += ` (no types: ${result.typesError})`;
|
|
39
|
+
}
|
|
40
|
+
results.push(status);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
hasError = true;
|
|
43
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
44
|
+
results.push(`x ${packageSpec}: ${message}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const output = results.join("\n") + "\n";
|
|
49
|
+
|
|
50
|
+
if (hasError) {
|
|
51
|
+
return {
|
|
52
|
+
stdout: "",
|
|
53
|
+
stderr: output,
|
|
54
|
+
exitCode: 1,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
stdout: output,
|
|
60
|
+
stderr: "",
|
|
61
|
+
exitCode: 0,
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create the `uninstall` command for removing packages
|
|
68
|
+
*/
|
|
69
|
+
export function createUninstallCommand(deps: CommandDeps) {
|
|
70
|
+
const { fs } = deps;
|
|
71
|
+
|
|
72
|
+
return defineCommand("uninstall", async (args, _ctx: CommandContext) => {
|
|
73
|
+
if (args.length === 0) {
|
|
74
|
+
return {
|
|
75
|
+
stdout: "",
|
|
76
|
+
stderr: "Usage: uninstall <package> [...packages]\n",
|
|
77
|
+
exitCode: 1,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const results: string[] = [];
|
|
82
|
+
let hasError = false;
|
|
83
|
+
|
|
84
|
+
for (const packageName of args) {
|
|
85
|
+
try {
|
|
86
|
+
const removed = await uninstallPackage(fs, packageName!);
|
|
87
|
+
if (removed) {
|
|
88
|
+
results.push(`- ${packageName}`);
|
|
89
|
+
} else {
|
|
90
|
+
results.push(`x ${packageName}: not installed`);
|
|
91
|
+
hasError = true;
|
|
92
|
+
}
|
|
93
|
+
} catch (err) {
|
|
94
|
+
hasError = true;
|
|
95
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
96
|
+
results.push(`x ${packageName}: ${message}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const output = results.join("\n") + "\n";
|
|
101
|
+
|
|
102
|
+
if (hasError) {
|
|
103
|
+
return {
|
|
104
|
+
stdout: "",
|
|
105
|
+
stderr: output,
|
|
106
|
+
exitCode: 1,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
stdout: output,
|
|
112
|
+
stderr: "",
|
|
113
|
+
exitCode: 0,
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Create the `list` command (alias: `ls`) for showing installed packages
|
|
120
|
+
*/
|
|
121
|
+
export function createListCommand(deps: CommandDeps) {
|
|
122
|
+
const { fs } = deps;
|
|
123
|
+
|
|
124
|
+
return defineCommand("list", async (_args, _ctx: CommandContext) => {
|
|
125
|
+
try {
|
|
126
|
+
const packages = await listPackages(fs);
|
|
127
|
+
|
|
128
|
+
if (packages.length === 0) {
|
|
129
|
+
return {
|
|
130
|
+
stdout: "No packages installed.\n",
|
|
131
|
+
stderr: "",
|
|
132
|
+
exitCode: 0,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const output = packages
|
|
137
|
+
.map((pkg) => `${pkg.name}@${pkg.version}`)
|
|
138
|
+
.join("\n") + "\n";
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
stdout: output,
|
|
142
|
+
stderr: "",
|
|
143
|
+
exitCode: 0,
|
|
144
|
+
};
|
|
145
|
+
} catch (err) {
|
|
146
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
147
|
+
return {
|
|
148
|
+
stdout: "",
|
|
149
|
+
stderr: `Failed to list packages: ${message}\n`,
|
|
150
|
+
exitCode: 1,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|