sandlot 0.1.2 → 0.1.4
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 +138 -408
- package/dist/build-emitter.d.ts +31 -13
- package/dist/build-emitter.d.ts.map +1 -1
- package/dist/builder.d.ts +370 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/bundler.d.ts +6 -2
- 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 +53 -49
- 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 +300 -511
- package/dist/internal.js +161 -171
- package/dist/runner.d.ts +314 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/sandbox-manager.d.ts +45 -21
- package/dist/sandbox-manager.d.ts.map +1 -1
- package/dist/sandbox.d.ts +144 -62
- 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/ts-libs.d.ts +7 -20
- package/dist/ts-libs.d.ts.map +1 -1
- package/dist/typechecker.d.ts +1 -1
- package/package.json +5 -5
- package/src/build-emitter.ts +32 -29
- package/src/builder.ts +498 -0
- package/src/bundler.ts +76 -55
- 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 +82 -221
- package/src/index.ts +17 -12
- package/src/sandbox.ts +219 -149
- package/src/shared-modules.ts +74 -4
- package/src/shared-resources.ts +0 -3
- package/src/ts-libs.ts +19 -121
- 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/sandbox-manager.ts +0 -409
package/src/sandbox.ts
CHANGED
|
@@ -1,18 +1,41 @@
|
|
|
1
|
-
import { Bash, defineCommand } from "just-bash/browser";
|
|
2
|
-
import {
|
|
3
|
-
import { initBundler
|
|
4
|
-
import { createDefaultCommands, type CommandDeps } from "./commands";
|
|
1
|
+
import { Bash, defineCommand, type BashOptions } from "just-bash/browser";
|
|
2
|
+
import { Filesystem, type FilesystemOptions } from "./fs";
|
|
3
|
+
import { initBundler } from "./bundler";
|
|
4
|
+
import { createDefaultCommands, type CommandDeps, type BuildOutput, type ValidateFn } from "./commands";
|
|
5
5
|
import { getDefaultResources, type SharedResources } from "./shared-resources";
|
|
6
6
|
import { BuildEmitter } from "./build-emitter";
|
|
7
|
+
import { installPackage, parseImportPath } from "./packages";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Options that can be passed through to the just-bash Bash constructor.
|
|
11
|
+
* Excludes options that sandlot controls internally (fs, customCommands, files, cwd).
|
|
12
|
+
* The working directory is always root (/).
|
|
13
|
+
*/
|
|
14
|
+
export type SandboxBashOptions = Omit<BashOptions, 'fs' | 'customCommands' | 'files' | 'cwd'>;
|
|
7
15
|
|
|
8
16
|
/**
|
|
9
17
|
* Options for creating a sandbox environment
|
|
10
18
|
*/
|
|
11
19
|
export interface SandboxOptions {
|
|
12
20
|
/**
|
|
13
|
-
*
|
|
21
|
+
* Initial files to populate the filesystem with.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* const sandbox = await createSandbox({
|
|
26
|
+
* initialFiles: {
|
|
27
|
+
* '/src/index.ts': 'export const x = 1;',
|
|
28
|
+
* '/tsconfig.json': JSON.stringify({ compilerOptions: { strict: true } }),
|
|
29
|
+
* },
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
initialFiles?: Record<string, string>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Maximum filesystem size in bytes (default: 50MB)
|
|
14
37
|
*/
|
|
15
|
-
|
|
38
|
+
maxFilesystemSize?: number;
|
|
16
39
|
|
|
17
40
|
/**
|
|
18
41
|
* Path to tsconfig.json in the virtual filesystem.
|
|
@@ -31,10 +54,12 @@ export interface SandboxOptions {
|
|
|
31
54
|
|
|
32
55
|
/**
|
|
33
56
|
* Callback invoked when a build succeeds.
|
|
34
|
-
* Receives the
|
|
35
|
-
*
|
|
57
|
+
* Receives the build output with the bundle and loaded module.
|
|
58
|
+
*
|
|
59
|
+
* For agent workflows, prefer using `createBuilder()` which handles
|
|
60
|
+
* build capture automatically.
|
|
36
61
|
*/
|
|
37
|
-
onBuild?: (result:
|
|
62
|
+
onBuild?: (result: BuildOutput) => void | Promise<void>;
|
|
38
63
|
|
|
39
64
|
/**
|
|
40
65
|
* Additional custom commands to add to the bash environment
|
|
@@ -45,29 +70,63 @@ export interface SandboxOptions {
|
|
|
45
70
|
* Module IDs that should be resolved from the host's SharedModuleRegistry
|
|
46
71
|
* instead of esm.sh CDN. The host must have registered these modules
|
|
47
72
|
* using `registerSharedModules()` before loading dynamic code.
|
|
48
|
-
*
|
|
73
|
+
*
|
|
49
74
|
* This solves the "multiple React instances" problem by allowing dynamic
|
|
50
75
|
* components to share the same React instance as the host application.
|
|
51
|
-
*
|
|
76
|
+
*
|
|
77
|
+
* Type definitions are automatically installed for these modules so that
|
|
78
|
+
* TypeScript can typecheck code that imports them.
|
|
79
|
+
*
|
|
52
80
|
* @example
|
|
53
81
|
* ```ts
|
|
54
82
|
* // Host setup
|
|
55
83
|
* import * as React from 'react';
|
|
56
84
|
* import * as ReactDOM from 'react-dom/client';
|
|
57
85
|
* import { registerSharedModules } from 'sandlot';
|
|
58
|
-
*
|
|
86
|
+
*
|
|
59
87
|
* registerSharedModules({
|
|
60
88
|
* 'react': React,
|
|
61
89
|
* 'react-dom/client': ReactDOM,
|
|
62
90
|
* });
|
|
63
|
-
*
|
|
64
|
-
* // Create sandbox with shared modules
|
|
65
|
-
* const sandbox = await
|
|
91
|
+
*
|
|
92
|
+
* // Create sandbox with shared modules (types auto-installed)
|
|
93
|
+
* const sandbox = await createSandbox({
|
|
66
94
|
* sharedModules: ['react', 'react-dom/client'],
|
|
67
95
|
* });
|
|
68
96
|
* ```
|
|
69
97
|
*/
|
|
70
98
|
sharedModules?: string[];
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Options passed through to the just-bash Bash constructor.
|
|
102
|
+
* Use this to configure environment variables, execution limits,
|
|
103
|
+
* network access, logging, and other bash-level settings.
|
|
104
|
+
*
|
|
105
|
+
* Note: `fs`, `customCommands`, `files`, and `cwd` are controlled by sandlot
|
|
106
|
+
* and cannot be overridden here. The working directory is always root (/).
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```ts
|
|
110
|
+
* const sandbox = await createSandbox({
|
|
111
|
+
* bashOptions: {
|
|
112
|
+
* env: { NODE_ENV: 'development' },
|
|
113
|
+
* executionLimits: { maxCommandCount: 1000 },
|
|
114
|
+
* },
|
|
115
|
+
* });
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
bashOptions?: SandboxBashOptions;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Sandbox state that can be serialized for persistence.
|
|
123
|
+
*/
|
|
124
|
+
export interface SandboxState {
|
|
125
|
+
/**
|
|
126
|
+
* All files in the filesystem as path -> content mapping.
|
|
127
|
+
* Can be passed as `initialFiles` when creating a new sandbox.
|
|
128
|
+
*/
|
|
129
|
+
files: Record<string, string>;
|
|
71
130
|
}
|
|
72
131
|
|
|
73
132
|
/**
|
|
@@ -75,9 +134,9 @@ export interface SandboxOptions {
|
|
|
75
134
|
*/
|
|
76
135
|
export interface Sandbox {
|
|
77
136
|
/**
|
|
78
|
-
* The virtual filesystem
|
|
137
|
+
* The virtual filesystem
|
|
79
138
|
*/
|
|
80
|
-
fs:
|
|
139
|
+
fs: Filesystem;
|
|
81
140
|
|
|
82
141
|
/**
|
|
83
142
|
* The just-bash shell environment
|
|
@@ -85,55 +144,94 @@ export interface Sandbox {
|
|
|
85
144
|
bash: Bash;
|
|
86
145
|
|
|
87
146
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
147
|
+
* The last successful build output, or null if no build has succeeded yet.
|
|
148
|
+
*
|
|
149
|
+
* This is updated automatically whenever a `build` command succeeds.
|
|
150
|
+
* Contains both the bundle and the loaded (and validated, if applicable) module.
|
|
151
|
+
*
|
|
90
152
|
* @example
|
|
91
153
|
* ```ts
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* await
|
|
95
|
-
*
|
|
154
|
+
* // Agent loop pattern
|
|
155
|
+
* while (!sandbox.lastBuild) {
|
|
156
|
+
* const response = await agent.step();
|
|
157
|
+
* await sandbox.bash.exec(response.command);
|
|
158
|
+
* }
|
|
159
|
+
* // Build succeeded, sandbox.lastBuild contains bundle + module
|
|
160
|
+
* const App = sandbox.lastBuild.module.App;
|
|
96
161
|
* ```
|
|
97
162
|
*/
|
|
98
|
-
|
|
163
|
+
lastBuild: BuildOutput | null;
|
|
99
164
|
|
|
100
165
|
/**
|
|
101
|
-
*
|
|
166
|
+
* Get the current sandbox state for persistence.
|
|
167
|
+
*
|
|
168
|
+
* Returns a serializable object containing all files that can be
|
|
169
|
+
* JSON-serialized and used to restore the sandbox later.
|
|
102
170
|
*
|
|
103
|
-
* @
|
|
171
|
+
* @example
|
|
172
|
+
* ```ts
|
|
173
|
+
* // Save sandbox state
|
|
174
|
+
* const state = sandbox.getState();
|
|
175
|
+
* localStorage.setItem('my-project', JSON.stringify(state));
|
|
176
|
+
*
|
|
177
|
+
* // Later, restore the sandbox
|
|
178
|
+
* const saved = JSON.parse(localStorage.getItem('my-project'));
|
|
179
|
+
* const sandbox2 = await createSandbox({ initialFiles: saved.files });
|
|
180
|
+
* ```
|
|
104
181
|
*/
|
|
105
|
-
|
|
182
|
+
getState(): SandboxState;
|
|
106
183
|
|
|
107
184
|
/**
|
|
108
|
-
*
|
|
185
|
+
* Subscribe to build events. Called whenever a build succeeds.
|
|
186
|
+
* Returns an unsubscribe function.
|
|
187
|
+
*
|
|
188
|
+
* For agent workflows, prefer using `createBuilder()` which handles
|
|
189
|
+
* build capture automatically. Use `onBuild` directly when you need
|
|
190
|
+
* more control over the subscription lifecycle.
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```ts
|
|
194
|
+
* let lastBuild: BuildOutput | null = null;
|
|
195
|
+
* const unsubscribe = sandbox.onBuild((result) => {
|
|
196
|
+
* lastBuild = result;
|
|
197
|
+
* });
|
|
198
|
+
*
|
|
199
|
+
* await sandbox.bash.exec('build /src/index.ts');
|
|
200
|
+
* unsubscribe();
|
|
201
|
+
*
|
|
202
|
+
* if (lastBuild) {
|
|
203
|
+
* const App = lastBuild.module.App as React.ComponentType;
|
|
204
|
+
* }
|
|
205
|
+
* ```
|
|
109
206
|
*/
|
|
110
|
-
|
|
207
|
+
onBuild(callback: (result: BuildOutput) => void | Promise<void>): () => void;
|
|
111
208
|
|
|
112
209
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
210
|
+
* Set a validation function for the build command.
|
|
211
|
+
*
|
|
212
|
+
* When set, the build command will run this function after loading
|
|
213
|
+
* the module. If validation throws, the build fails and the agent
|
|
214
|
+
* sees the error. If validation passes, the validated module is
|
|
215
|
+
* available in the build output.
|
|
216
|
+
*
|
|
119
217
|
* @example
|
|
120
218
|
* ```ts
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
* onBuild: (result) => {
|
|
125
|
-
* bundle = result;
|
|
126
|
-
* console.log('Built:', result.code.length, 'bytes');
|
|
127
|
-
* },
|
|
219
|
+
* sandbox.setValidation((mod) => {
|
|
220
|
+
* if (!mod.App) throw new Error("Must export App component");
|
|
221
|
+
* return { App: mod.App as React.ComponentType };
|
|
128
222
|
* });
|
|
129
|
-
*
|
|
130
|
-
* //
|
|
131
|
-
*
|
|
132
|
-
* // After successful build, bundle is available
|
|
133
|
-
* console.log(bundle?.code);
|
|
223
|
+
*
|
|
224
|
+
* // Now build will fail if App is missing
|
|
225
|
+
* await sandbox.bash.exec('build /src/index.ts');
|
|
134
226
|
* ```
|
|
135
227
|
*/
|
|
136
|
-
|
|
228
|
+
setValidation(fn: ValidateFn): void;
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Clear the validation function.
|
|
232
|
+
* After calling this, builds will not perform validation.
|
|
233
|
+
*/
|
|
234
|
+
clearValidation(): void;
|
|
137
235
|
}
|
|
138
236
|
|
|
139
237
|
/**
|
|
@@ -143,9 +241,12 @@ export interface Sandbox {
|
|
|
143
241
|
* The sandbox provides a just-bash shell with custom commands:
|
|
144
242
|
* - `tsc [entry]` - Type check the project
|
|
145
243
|
* - `build [entry] [options]` - Build the project (runs typecheck first)
|
|
244
|
+
* - `install <pkg>` - Install npm packages
|
|
245
|
+
* - `uninstall <pkg>` - Remove packages
|
|
246
|
+
* - `list` - List installed packages
|
|
247
|
+
* - `run <entry>` - Run a script
|
|
146
248
|
*
|
|
147
249
|
* Build options:
|
|
148
|
-
* - `--output, -o <path>` - Output path (default: /dist/bundle.js)
|
|
149
250
|
* - `--format, -f <esm|iife|cjs>` - Output format (default: esm)
|
|
150
251
|
* - `--minify, -m` - Enable minification
|
|
151
252
|
* - `--skip-typecheck, -s` - Skip type checking
|
|
@@ -155,18 +256,14 @@ export interface Sandbox {
|
|
|
155
256
|
* let bundleResult: BundleResult | null = null;
|
|
156
257
|
*
|
|
157
258
|
* const sandbox = await createSandbox({
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* compilerOptions: { target: "ES2020", strict: true }
|
|
164
|
-
* }),
|
|
165
|
-
* },
|
|
259
|
+
* initialFiles: {
|
|
260
|
+
* '/src/index.ts': 'export const hello = "world";',
|
|
261
|
+
* '/tsconfig.json': JSON.stringify({
|
|
262
|
+
* compilerOptions: { target: 'ES2020', strict: true }
|
|
263
|
+
* }),
|
|
166
264
|
* },
|
|
167
265
|
* onBuild: (result) => {
|
|
168
266
|
* bundleResult = result;
|
|
169
|
-
* // Could also: dynamically import, halt agent, etc.
|
|
170
267
|
* },
|
|
171
268
|
* });
|
|
172
269
|
*
|
|
@@ -174,155 +271,128 @@ export interface Sandbox {
|
|
|
174
271
|
* await sandbox.bash.exec('echo "console.log(1);" > /src/index.ts');
|
|
175
272
|
*
|
|
176
273
|
* // Type check
|
|
177
|
-
* const tscResult = await sandbox.bash.exec(
|
|
274
|
+
* const tscResult = await sandbox.bash.exec('tsc /src/index.ts');
|
|
178
275
|
* console.log(tscResult.stdout);
|
|
179
276
|
*
|
|
180
277
|
* // Build (includes typecheck, triggers onBuild callback)
|
|
181
|
-
* const buildResult = await sandbox.bash.exec(
|
|
278
|
+
* const buildResult = await sandbox.bash.exec('build /src/index.ts');
|
|
182
279
|
* console.log(buildResult.stdout);
|
|
183
280
|
* console.log(bundleResult?.code); // The compiled bundle
|
|
184
281
|
*
|
|
185
|
-
* // Save
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
* // Clean up
|
|
189
|
-
* sandbox.close();
|
|
282
|
+
* // Save state for later
|
|
283
|
+
* const state = sandbox.getState();
|
|
284
|
+
* localStorage.setItem('my-project', JSON.stringify(state));
|
|
190
285
|
* ```
|
|
191
286
|
*/
|
|
192
287
|
export async function createSandbox(options: SandboxOptions = {}): Promise<Sandbox> {
|
|
193
288
|
const {
|
|
194
|
-
|
|
289
|
+
initialFiles,
|
|
290
|
+
maxFilesystemSize,
|
|
195
291
|
tsconfigPath = "/tsconfig.json",
|
|
196
292
|
resources: providedResources,
|
|
197
293
|
onBuild: onBuildCallback,
|
|
198
294
|
customCommands = [],
|
|
199
295
|
sharedModules,
|
|
296
|
+
bashOptions = {},
|
|
200
297
|
} = options;
|
|
201
298
|
|
|
202
|
-
//
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
// Determine which resources to use:
|
|
206
|
-
// 1. Provided resources (preferred)
|
|
207
|
-
// 2. Default resources singleton
|
|
208
|
-
const resourcesPromise = providedResources
|
|
209
|
-
? Promise.resolve(providedResources)
|
|
210
|
-
: getDefaultResources();
|
|
211
|
-
|
|
212
|
-
const bundlerPromise = initBundler();
|
|
213
|
-
|
|
214
|
-
// Wait for all initialization
|
|
215
|
-
const [fs, resources] = await Promise.all([fsPromise, resourcesPromise, bundlerPromise]);
|
|
216
|
-
|
|
217
|
-
// Extract lib files and types cache from resources
|
|
218
|
-
const libFiles = resources.libFiles;
|
|
219
|
-
const typesCache = resources.typesCache;
|
|
220
|
-
|
|
221
|
-
// Create build event emitter
|
|
222
|
-
const buildEmitter = new BuildEmitter();
|
|
223
|
-
|
|
224
|
-
// If a callback was provided in options, subscribe it
|
|
225
|
-
if (onBuildCallback) {
|
|
226
|
-
buildEmitter.on(onBuildCallback);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Create commands using the extracted factories
|
|
230
|
-
// Commands emit to the build emitter
|
|
231
|
-
const commandDeps: CommandDeps = {
|
|
232
|
-
fs,
|
|
233
|
-
libFiles,
|
|
234
|
-
tsconfigPath,
|
|
235
|
-
onBuild: buildEmitter.emit,
|
|
236
|
-
typesCache,
|
|
237
|
-
sharedModules,
|
|
238
|
-
};
|
|
239
|
-
const defaultCommands = createDefaultCommands(commandDeps);
|
|
240
|
-
|
|
241
|
-
// Create bash environment with the custom filesystem
|
|
242
|
-
const bash = new Bash({
|
|
243
|
-
fs,
|
|
244
|
-
cwd: "/",
|
|
245
|
-
customCommands: [...defaultCommands, ...customCommands],
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
return {
|
|
249
|
-
fs,
|
|
250
|
-
bash,
|
|
251
|
-
isDirty: () => fs.isDirty(),
|
|
252
|
-
save: () => fs.save(),
|
|
253
|
-
close: () => fs.close(),
|
|
254
|
-
onBuild: (callback) => buildEmitter.on(callback),
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Create an in-memory sandbox (no IndexedDB persistence).
|
|
260
|
-
* Useful for testing or temporary workspaces.
|
|
261
|
-
*/
|
|
262
|
-
export async function createInMemorySandbox(
|
|
263
|
-
options: Omit<SandboxOptions, "fsOptions"> & {
|
|
264
|
-
initialFiles?: Record<string, string>;
|
|
265
|
-
} = {}
|
|
266
|
-
): Promise<Sandbox> {
|
|
267
|
-
const {
|
|
299
|
+
// Create filesystem (synchronous)
|
|
300
|
+
const fs = Filesystem.create({
|
|
268
301
|
initialFiles,
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
onBuild: onBuildCallback,
|
|
272
|
-
customCommands = [],
|
|
273
|
-
sharedModules,
|
|
274
|
-
} = options;
|
|
302
|
+
maxSizeBytes: maxFilesystemSize,
|
|
303
|
+
});
|
|
275
304
|
|
|
276
|
-
//
|
|
305
|
+
// Load shared resources and bundler in parallel
|
|
277
306
|
const resourcesPromise = providedResources
|
|
278
307
|
? Promise.resolve(providedResources)
|
|
279
308
|
: getDefaultResources();
|
|
280
309
|
|
|
281
310
|
const bundlerPromise = initBundler();
|
|
282
311
|
|
|
283
|
-
//
|
|
284
|
-
const fs = IndexedDbFs.createInMemory({ initialFiles });
|
|
285
|
-
|
|
286
|
-
// Wait for resources and bundler
|
|
312
|
+
// Wait for async initialization
|
|
287
313
|
const [resources] = await Promise.all([resourcesPromise, bundlerPromise]);
|
|
288
314
|
|
|
289
315
|
// Extract lib files and types cache from resources
|
|
290
316
|
const libFiles = resources.libFiles;
|
|
291
317
|
const typesCache = resources.typesCache;
|
|
292
318
|
|
|
319
|
+
// Auto-install types for shared modules so TypeScript can typecheck them
|
|
320
|
+
// Only install base packages, not subpath exports (e.g., "react" not "react/jsx-runtime")
|
|
321
|
+
// Subpath types are fetched automatically when the base package is installed
|
|
322
|
+
if (sharedModules && sharedModules.length > 0) {
|
|
323
|
+
// Extract unique base package names
|
|
324
|
+
const basePackages = new Set<string>();
|
|
325
|
+
for (const moduleId of sharedModules) {
|
|
326
|
+
const { packageName } = parseImportPath(moduleId);
|
|
327
|
+
basePackages.add(packageName);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
await Promise.all(
|
|
331
|
+
Array.from(basePackages).map(async (packageName) => {
|
|
332
|
+
try {
|
|
333
|
+
// Install the package to get its type definitions
|
|
334
|
+
// The runtime will use the shared module, but we need types for typechecking
|
|
335
|
+
await installPackage(fs, packageName, { cache: typesCache });
|
|
336
|
+
} catch (err) {
|
|
337
|
+
// Log but don't fail - module might not have types available
|
|
338
|
+
console.warn(`[sandlot] Failed to install types for shared module "${packageName}":`, err);
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
293
344
|
// Create build event emitter
|
|
294
345
|
const buildEmitter = new BuildEmitter();
|
|
295
346
|
|
|
347
|
+
// Track the last successful build
|
|
348
|
+
let lastBuild: BuildOutput | null = null;
|
|
349
|
+
buildEmitter.on((result) => {
|
|
350
|
+
lastBuild = result;
|
|
351
|
+
});
|
|
352
|
+
|
|
296
353
|
// If a callback was provided in options, subscribe it
|
|
297
354
|
if (onBuildCallback) {
|
|
298
355
|
buildEmitter.on(onBuildCallback);
|
|
299
356
|
}
|
|
300
357
|
|
|
301
|
-
//
|
|
302
|
-
|
|
358
|
+
// Validation function (can be set/cleared dynamically)
|
|
359
|
+
let validationFn: ValidateFn | null = null;
|
|
360
|
+
|
|
361
|
+
// Create commands
|
|
303
362
|
const commandDeps: CommandDeps = {
|
|
304
363
|
fs,
|
|
305
364
|
libFiles,
|
|
306
365
|
tsconfigPath,
|
|
307
366
|
onBuild: buildEmitter.emit,
|
|
367
|
+
getValidation: () => validationFn,
|
|
308
368
|
typesCache,
|
|
309
369
|
sharedModules,
|
|
310
370
|
};
|
|
311
371
|
const defaultCommands = createDefaultCommands(commandDeps);
|
|
312
372
|
|
|
313
373
|
// Create bash environment with the custom filesystem
|
|
374
|
+
// Always start in root directory (/) for consistent behavior
|
|
314
375
|
const bash = new Bash({
|
|
376
|
+
...bashOptions,
|
|
377
|
+
cwd: '/',
|
|
315
378
|
fs,
|
|
316
|
-
cwd: "/",
|
|
317
379
|
customCommands: [...defaultCommands, ...customCommands],
|
|
318
380
|
});
|
|
319
381
|
|
|
320
382
|
return {
|
|
321
383
|
fs,
|
|
322
384
|
bash,
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
385
|
+
get lastBuild() {
|
|
386
|
+
return lastBuild;
|
|
387
|
+
},
|
|
388
|
+
getState: () => ({ files: fs.getFiles() }),
|
|
326
389
|
onBuild: (callback) => buildEmitter.on(callback),
|
|
390
|
+
setValidation: (fn: ValidateFn) => {
|
|
391
|
+
validationFn = fn;
|
|
392
|
+
},
|
|
393
|
+
clearValidation: () => {
|
|
394
|
+
validationFn = null;
|
|
395
|
+
},
|
|
327
396
|
};
|
|
328
397
|
}
|
|
398
|
+
|
package/src/shared-modules.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* });
|
|
22
22
|
*
|
|
23
23
|
* // Now create sandbox with sharedModules option
|
|
24
|
-
* const sandbox = await
|
|
24
|
+
* const sandbox = await createSandbox({
|
|
25
25
|
* sharedModules: ['react', 'react-dom/client'],
|
|
26
26
|
* });
|
|
27
27
|
* ```
|
|
@@ -39,6 +39,7 @@ const GLOBAL_KEY = '__sandlot_shared_modules__';
|
|
|
39
39
|
*/
|
|
40
40
|
export class SharedModuleRegistry {
|
|
41
41
|
private modules = new Map<string, unknown>();
|
|
42
|
+
private exportNames = new Map<string, string[]>();
|
|
42
43
|
|
|
43
44
|
constructor() {
|
|
44
45
|
// Make available globally for dynamic imports
|
|
@@ -46,7 +47,8 @@ export class SharedModuleRegistry {
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
/**
|
|
49
|
-
* Register a module to be shared with dynamic bundles
|
|
50
|
+
* Register a module to be shared with dynamic bundles.
|
|
51
|
+
* Automatically introspects the module to discover its exports.
|
|
50
52
|
*
|
|
51
53
|
* @param moduleId - The import specifier (e.g., 'react', 'react-dom/client')
|
|
52
54
|
* @param module - The module's exports object
|
|
@@ -54,18 +56,21 @@ export class SharedModuleRegistry {
|
|
|
54
56
|
*/
|
|
55
57
|
register(moduleId: string, module: unknown): this {
|
|
56
58
|
this.modules.set(moduleId, module);
|
|
59
|
+
// Introspect the module to get its export names
|
|
60
|
+
this.exportNames.set(moduleId, introspectExports(module));
|
|
57
61
|
return this;
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
/**
|
|
61
|
-
* Register multiple modules at once
|
|
65
|
+
* Register multiple modules at once.
|
|
66
|
+
* Automatically introspects each module to discover its exports.
|
|
62
67
|
*
|
|
63
68
|
* @param modules - Object mapping module IDs to their exports
|
|
64
69
|
* @returns this for chaining
|
|
65
70
|
*/
|
|
66
71
|
registerAll(modules: Record<string, unknown>): this {
|
|
67
72
|
for (const [id, mod] of Object.entries(modules)) {
|
|
68
|
-
this.
|
|
73
|
+
this.register(id, mod);
|
|
69
74
|
}
|
|
70
75
|
return this;
|
|
71
76
|
}
|
|
@@ -116,11 +121,23 @@ export class SharedModuleRegistry {
|
|
|
116
121
|
return [...this.modules.keys()];
|
|
117
122
|
}
|
|
118
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Get the export names for a registered module.
|
|
126
|
+
* These were discovered by introspecting the module at registration time.
|
|
127
|
+
*
|
|
128
|
+
* @param moduleId - The import specifier
|
|
129
|
+
* @returns Array of export names, or empty array if not registered
|
|
130
|
+
*/
|
|
131
|
+
getExportNames(moduleId: string): string[] {
|
|
132
|
+
return this.exportNames.get(moduleId) ?? [];
|
|
133
|
+
}
|
|
134
|
+
|
|
119
135
|
/**
|
|
120
136
|
* Clear all registrations
|
|
121
137
|
*/
|
|
122
138
|
clear(): void {
|
|
123
139
|
this.modules.clear();
|
|
140
|
+
this.exportNames.clear();
|
|
124
141
|
}
|
|
125
142
|
|
|
126
143
|
/**
|
|
@@ -131,6 +148,48 @@ export class SharedModuleRegistry {
|
|
|
131
148
|
}
|
|
132
149
|
}
|
|
133
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Introspect a module to discover its export names.
|
|
153
|
+
* Filters out non-identifier keys and internal properties.
|
|
154
|
+
*/
|
|
155
|
+
function introspectExports(module: unknown): string[] {
|
|
156
|
+
if (module === null || module === undefined) {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (typeof module !== 'object' && typeof module !== 'function') {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const exports: string[] = [];
|
|
165
|
+
|
|
166
|
+
// Get own enumerable properties
|
|
167
|
+
for (const key of Object.keys(module as object)) {
|
|
168
|
+
// Filter out non-valid JavaScript identifiers
|
|
169
|
+
if (isValidIdentifier(key)) {
|
|
170
|
+
exports.push(key);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return exports;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Check if a string is a valid JavaScript identifier.
|
|
179
|
+
* Used to filter out keys that can't be used as named exports.
|
|
180
|
+
*/
|
|
181
|
+
function isValidIdentifier(name: string): boolean {
|
|
182
|
+
if (name.length === 0) return false;
|
|
183
|
+
// Must start with letter, underscore, or $
|
|
184
|
+
if (!/^[a-zA-Z_$]/.test(name)) return false;
|
|
185
|
+
// Rest must be alphanumeric, underscore, or $
|
|
186
|
+
if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)) return false;
|
|
187
|
+
// Exclude reserved words that would cause issues
|
|
188
|
+
const reserved = ['default', 'class', 'function', 'var', 'let', 'const', 'import', 'export'];
|
|
189
|
+
if (reserved.includes(name)) return false;
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
134
193
|
// Singleton instance
|
|
135
194
|
let defaultRegistry: SharedModuleRegistry | null = null;
|
|
136
195
|
|
|
@@ -190,6 +249,17 @@ export function clearSharedModules(): void {
|
|
|
190
249
|
getSharedModuleRegistry().clear();
|
|
191
250
|
}
|
|
192
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Get the export names for a registered shared module.
|
|
254
|
+
* Used by the bundler to generate proper re-export statements.
|
|
255
|
+
*
|
|
256
|
+
* @param moduleId - The import specifier
|
|
257
|
+
* @returns Array of export names, or empty array if not registered
|
|
258
|
+
*/
|
|
259
|
+
export function getSharedModuleExports(moduleId: string): string[] {
|
|
260
|
+
return getSharedModuleRegistry().getExportNames(moduleId);
|
|
261
|
+
}
|
|
262
|
+
|
|
193
263
|
/**
|
|
194
264
|
* Generate the runtime code that dynamic bundles use to access shared modules.
|
|
195
265
|
* This is injected into bundles when they import from shared modules.
|
package/src/shared-resources.ts
CHANGED
|
@@ -6,9 +6,6 @@
|
|
|
6
6
|
* - esbuild WASM (~10MB) - singleton bundler initialization
|
|
7
7
|
* - Types cache - avoids redundant network fetches when multiple sandboxes
|
|
8
8
|
* install the same packages
|
|
9
|
-
*
|
|
10
|
-
* This module consolidates what was previously scattered across
|
|
11
|
-
* sandbox.ts and sandbox-manager.ts into a single source of truth.
|
|
12
9
|
*/
|
|
13
10
|
|
|
14
11
|
import { fetchAndCacheLibs, getDefaultBrowserLibs } from "./ts-libs";
|