sandlot 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +616 -0
- package/dist/bundler.d.ts +148 -0
- package/dist/bundler.d.ts.map +1 -0
- package/dist/commands.d.ts +179 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/fs.d.ts +125 -0
- package/dist/fs.d.ts.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2920 -0
- package/dist/internal.d.ts +74 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +1897 -0
- package/dist/loader.d.ts +164 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/packages.d.ts +199 -0
- package/dist/packages.d.ts.map +1 -0
- package/dist/react.d.ts +159 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +149 -0
- package/dist/sandbox-manager.d.ts +249 -0
- package/dist/sandbox-manager.d.ts.map +1 -0
- package/dist/sandbox.d.ts +193 -0
- package/dist/sandbox.d.ts.map +1 -0
- package/dist/shared-modules.d.ts +129 -0
- package/dist/shared-modules.d.ts.map +1 -0
- package/dist/shared-resources.d.ts +105 -0
- package/dist/shared-resources.d.ts.map +1 -0
- package/dist/ts-libs.d.ts +98 -0
- package/dist/ts-libs.d.ts.map +1 -0
- package/dist/typechecker.d.ts +127 -0
- package/dist/typechecker.d.ts.map +1 -0
- package/package.json +64 -0
- package/src/bundler.ts +513 -0
- package/src/commands.ts +733 -0
- package/src/fs.ts +935 -0
- package/src/index.ts +149 -0
- package/src/internal.ts +116 -0
- package/src/loader.ts +229 -0
- package/src/packages.ts +936 -0
- package/src/react.tsx +331 -0
- package/src/sandbox-manager.ts +490 -0
- package/src/sandbox.ts +402 -0
- package/src/shared-modules.ts +210 -0
- package/src/shared-resources.ts +169 -0
- package/src/ts-libs.ts +320 -0
- package/src/typechecker.ts +635 -0
package/src/sandbox.ts
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { Bash, defineCommand } from "just-bash/browser";
|
|
2
|
+
import { IndexedDbFs, type IndexedDbFsOptions } from "./fs";
|
|
3
|
+
import { initBundler, type BundleResult } from "./bundler";
|
|
4
|
+
import { createDefaultCommands, type CommandDeps } from "./commands";
|
|
5
|
+
import { getDefaultResources, type SharedResources } from "./shared-resources";
|
|
6
|
+
|
|
7
|
+
// Re-export for convenience
|
|
8
|
+
export type { BundleResult } from "./bundler";
|
|
9
|
+
export type { TypecheckResult } from "./typechecker";
|
|
10
|
+
export type { SharedResources, TypesCache } from "./shared-resources";
|
|
11
|
+
export type { PackageManifest, InstallResult } from "./packages";
|
|
12
|
+
export type { RunContext, RunOptions, RunResult } from "./commands";
|
|
13
|
+
export { installPackage, uninstallPackage, listPackages, getPackageManifest } from "./packages";
|
|
14
|
+
export { InMemoryTypesCache } from "./shared-resources";
|
|
15
|
+
|
|
16
|
+
// Loader utilities
|
|
17
|
+
export {
|
|
18
|
+
loadModule,
|
|
19
|
+
loadExport,
|
|
20
|
+
loadDefault,
|
|
21
|
+
getExportNames,
|
|
22
|
+
hasExport,
|
|
23
|
+
createModuleUrl,
|
|
24
|
+
revokeModuleUrl,
|
|
25
|
+
ModuleLoadError,
|
|
26
|
+
ExportNotFoundError,
|
|
27
|
+
} from "./loader";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Simple typed event emitter for build results.
|
|
31
|
+
* Caches the last result so waitFor() can be called after build completes.
|
|
32
|
+
*/
|
|
33
|
+
class BuildEmitter {
|
|
34
|
+
private listeners = new Set<(result: BundleResult) => void | Promise<void>>();
|
|
35
|
+
private lastResult: BundleResult | null = null;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Emit a build result to all listeners and cache it
|
|
39
|
+
*/
|
|
40
|
+
emit = async (result: BundleResult): Promise<void> => {
|
|
41
|
+
this.lastResult = result;
|
|
42
|
+
const promises: Promise<void>[] = [];
|
|
43
|
+
for (const listener of this.listeners) {
|
|
44
|
+
const ret = listener(result);
|
|
45
|
+
if (ret instanceof Promise) {
|
|
46
|
+
promises.push(ret);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
await Promise.all(promises);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Subscribe to build events. Returns an unsubscribe function.
|
|
54
|
+
*/
|
|
55
|
+
on(callback: (result: BundleResult) => void | Promise<void>): () => void {
|
|
56
|
+
this.listeners.add(callback);
|
|
57
|
+
return () => {
|
|
58
|
+
this.listeners.delete(callback);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get the last build result, or wait for the next one if none exists.
|
|
64
|
+
* Clears the cached result after returning, so subsequent calls wait for new builds.
|
|
65
|
+
*/
|
|
66
|
+
waitFor(): Promise<BundleResult> {
|
|
67
|
+
if (this.lastResult) {
|
|
68
|
+
const result = this.lastResult;
|
|
69
|
+
this.lastResult = null;
|
|
70
|
+
return Promise.resolve(result);
|
|
71
|
+
}
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
const unsub = this.on((result) => {
|
|
74
|
+
unsub();
|
|
75
|
+
this.lastResult = null;
|
|
76
|
+
resolve(result);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Options for creating a sandbox environment
|
|
84
|
+
*/
|
|
85
|
+
export interface SandboxOptions {
|
|
86
|
+
/**
|
|
87
|
+
* Options for the IndexedDB filesystem
|
|
88
|
+
*/
|
|
89
|
+
fsOptions?: IndexedDbFsOptions;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Path to tsconfig.json in the virtual filesystem.
|
|
93
|
+
* Default: "/tsconfig.json"
|
|
94
|
+
*/
|
|
95
|
+
tsconfigPath?: string;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Shared resources (lib files, bundler).
|
|
99
|
+
* If not provided, uses the default singleton resources.
|
|
100
|
+
*
|
|
101
|
+
* Provide this to share resources across multiple sandboxes,
|
|
102
|
+
* or to use custom TypeScript libs.
|
|
103
|
+
*/
|
|
104
|
+
resources?: SharedResources;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Callback invoked when a build succeeds.
|
|
108
|
+
* Receives the bundle result with the compiled code.
|
|
109
|
+
* Use this to dynamically import the bundle or halt the agent.
|
|
110
|
+
*/
|
|
111
|
+
onBuild?: (result: BundleResult) => void | Promise<void>;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Additional custom commands to add to the bash environment
|
|
115
|
+
*/
|
|
116
|
+
customCommands?: ReturnType<typeof defineCommand>[];
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Module IDs that should be resolved from the host's SharedModuleRegistry
|
|
120
|
+
* instead of esm.sh CDN. The host must have registered these modules
|
|
121
|
+
* using `registerSharedModules()` before loading dynamic code.
|
|
122
|
+
*
|
|
123
|
+
* This solves the "multiple React instances" problem by allowing dynamic
|
|
124
|
+
* components to share the same React instance as the host application.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```ts
|
|
128
|
+
* // Host setup
|
|
129
|
+
* import * as React from 'react';
|
|
130
|
+
* import * as ReactDOM from 'react-dom/client';
|
|
131
|
+
* import { registerSharedModules } from 'sandlot';
|
|
132
|
+
*
|
|
133
|
+
* registerSharedModules({
|
|
134
|
+
* 'react': React,
|
|
135
|
+
* 'react-dom/client': ReactDOM,
|
|
136
|
+
* });
|
|
137
|
+
*
|
|
138
|
+
* // Create sandbox with shared modules
|
|
139
|
+
* const sandbox = await createInMemorySandbox({
|
|
140
|
+
* sharedModules: ['react', 'react-dom/client'],
|
|
141
|
+
* });
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
sharedModules?: string[];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The sandbox environment containing the filesystem and bash shell
|
|
149
|
+
*/
|
|
150
|
+
export interface Sandbox {
|
|
151
|
+
/**
|
|
152
|
+
* The virtual filesystem (IndexedDB-backed)
|
|
153
|
+
*/
|
|
154
|
+
fs: IndexedDbFs;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The just-bash shell environment
|
|
158
|
+
*/
|
|
159
|
+
bash: Bash;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Check if there are unsaved changes in the filesystem.
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* ```ts
|
|
166
|
+
* await sandbox.bash.exec('echo "hello" > /test.txt');
|
|
167
|
+
* console.log(sandbox.isDirty()); // true
|
|
168
|
+
* await sandbox.save();
|
|
169
|
+
* console.log(sandbox.isDirty()); // false
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
isDirty(): boolean;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Save the filesystem to IndexedDB.
|
|
176
|
+
*
|
|
177
|
+
* @returns true if saved, false if nothing to save or in-memory only
|
|
178
|
+
*/
|
|
179
|
+
save(): Promise<boolean>;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Close all resources (filesystem, etc.)
|
|
183
|
+
*/
|
|
184
|
+
close(): void;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Subscribe to build events. Called whenever a build succeeds.
|
|
188
|
+
* Returns an unsubscribe function.
|
|
189
|
+
*
|
|
190
|
+
* Use this to capture the bundle result when build succeeds.
|
|
191
|
+
* The callback receives the BundleResult with the compiled code.
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```ts
|
|
195
|
+
* let bundle: BundleResult | null = null;
|
|
196
|
+
*
|
|
197
|
+
* const sandbox = await createInMemorySandbox({
|
|
198
|
+
* onBuild: (result) => {
|
|
199
|
+
* bundle = result;
|
|
200
|
+
* console.log('Built:', result.code.length, 'bytes');
|
|
201
|
+
* },
|
|
202
|
+
* });
|
|
203
|
+
*
|
|
204
|
+
* // ... agent writes files and calls build ...
|
|
205
|
+
*
|
|
206
|
+
* // After successful build, bundle is available
|
|
207
|
+
* console.log(bundle?.code);
|
|
208
|
+
* ```
|
|
209
|
+
*/
|
|
210
|
+
onBuild(callback: (result: BundleResult) => void | Promise<void>): () => void;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Create an in-browser agent sandbox with a virtual filesystem, TypeScript
|
|
215
|
+
* type checking, and bundling capabilities.
|
|
216
|
+
*
|
|
217
|
+
* The sandbox provides a just-bash shell with custom commands:
|
|
218
|
+
* - `tsc [entry]` - Type check the project
|
|
219
|
+
* - `build [entry] [options]` - Build the project (runs typecheck first)
|
|
220
|
+
*
|
|
221
|
+
* Build options:
|
|
222
|
+
* - `--output, -o <path>` - Output path (default: /dist/bundle.js)
|
|
223
|
+
* - `--format, -f <esm|iife|cjs>` - Output format (default: esm)
|
|
224
|
+
* - `--minify, -m` - Enable minification
|
|
225
|
+
* - `--skip-typecheck, -s` - Skip type checking
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```ts
|
|
229
|
+
* let bundleResult: BundleResult | null = null;
|
|
230
|
+
*
|
|
231
|
+
* const sandbox = await createSandbox({
|
|
232
|
+
* fsOptions: {
|
|
233
|
+
* dbName: "my-project",
|
|
234
|
+
* initialFiles: {
|
|
235
|
+
* "/src/index.ts": "export const hello = 'world';",
|
|
236
|
+
* "/tsconfig.json": JSON.stringify({
|
|
237
|
+
* compilerOptions: { target: "ES2020", strict: true }
|
|
238
|
+
* }),
|
|
239
|
+
* },
|
|
240
|
+
* },
|
|
241
|
+
* onBuild: (result) => {
|
|
242
|
+
* bundleResult = result;
|
|
243
|
+
* // Could also: dynamically import, halt agent, etc.
|
|
244
|
+
* },
|
|
245
|
+
* });
|
|
246
|
+
*
|
|
247
|
+
* // Use bash commands
|
|
248
|
+
* await sandbox.bash.exec('echo "console.log(1);" > /src/index.ts');
|
|
249
|
+
*
|
|
250
|
+
* // Type check
|
|
251
|
+
* const tscResult = await sandbox.bash.exec("tsc");
|
|
252
|
+
* console.log(tscResult.stdout);
|
|
253
|
+
*
|
|
254
|
+
* // Build (includes typecheck, triggers onBuild callback)
|
|
255
|
+
* const buildResult = await sandbox.bash.exec("build");
|
|
256
|
+
* console.log(buildResult.stdout);
|
|
257
|
+
* console.log(bundleResult?.code); // The compiled bundle
|
|
258
|
+
*
|
|
259
|
+
* // Save to IndexedDB
|
|
260
|
+
* await sandbox.save();
|
|
261
|
+
*
|
|
262
|
+
* // Clean up
|
|
263
|
+
* sandbox.close();
|
|
264
|
+
* ```
|
|
265
|
+
*/
|
|
266
|
+
export async function createSandbox(options: SandboxOptions = {}): Promise<Sandbox> {
|
|
267
|
+
const {
|
|
268
|
+
fsOptions = {},
|
|
269
|
+
tsconfigPath = "/tsconfig.json",
|
|
270
|
+
resources: providedResources,
|
|
271
|
+
onBuild: onBuildCallback,
|
|
272
|
+
customCommands = [],
|
|
273
|
+
sharedModules,
|
|
274
|
+
} = options;
|
|
275
|
+
|
|
276
|
+
// Start parallel initialization tasks
|
|
277
|
+
const fsPromise = IndexedDbFs.create(fsOptions);
|
|
278
|
+
|
|
279
|
+
// Determine which resources to use:
|
|
280
|
+
// 1. Provided resources (preferred)
|
|
281
|
+
// 2. Default resources singleton
|
|
282
|
+
const resourcesPromise = providedResources
|
|
283
|
+
? Promise.resolve(providedResources)
|
|
284
|
+
: getDefaultResources();
|
|
285
|
+
|
|
286
|
+
const bundlerPromise = initBundler();
|
|
287
|
+
|
|
288
|
+
// Wait for all initialization
|
|
289
|
+
const [fs, resources] = await Promise.all([fsPromise, resourcesPromise, bundlerPromise]);
|
|
290
|
+
|
|
291
|
+
// Extract lib files and types cache from resources
|
|
292
|
+
const libFiles = resources.libFiles;
|
|
293
|
+
const typesCache = resources.typesCache;
|
|
294
|
+
|
|
295
|
+
// Create build event emitter
|
|
296
|
+
const buildEmitter = new BuildEmitter();
|
|
297
|
+
|
|
298
|
+
// If a callback was provided in options, subscribe it
|
|
299
|
+
if (onBuildCallback) {
|
|
300
|
+
buildEmitter.on(onBuildCallback);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Create commands using the extracted factories
|
|
304
|
+
// Commands emit to the build emitter
|
|
305
|
+
const commandDeps: CommandDeps = {
|
|
306
|
+
fs,
|
|
307
|
+
libFiles,
|
|
308
|
+
tsconfigPath,
|
|
309
|
+
onBuild: buildEmitter.emit,
|
|
310
|
+
typesCache,
|
|
311
|
+
sharedModules,
|
|
312
|
+
};
|
|
313
|
+
const defaultCommands = createDefaultCommands(commandDeps);
|
|
314
|
+
|
|
315
|
+
// Create bash environment with the custom filesystem
|
|
316
|
+
const bash = new Bash({
|
|
317
|
+
fs,
|
|
318
|
+
cwd: "/",
|
|
319
|
+
customCommands: [...defaultCommands, ...customCommands],
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
fs,
|
|
324
|
+
bash,
|
|
325
|
+
isDirty: () => fs.isDirty(),
|
|
326
|
+
save: () => fs.save(),
|
|
327
|
+
close: () => fs.close(),
|
|
328
|
+
onBuild: (callback) => buildEmitter.on(callback),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Create an in-memory sandbox (no IndexedDB persistence).
|
|
334
|
+
* Useful for testing or temporary workspaces.
|
|
335
|
+
*/
|
|
336
|
+
export async function createInMemorySandbox(
|
|
337
|
+
options: Omit<SandboxOptions, "fsOptions"> & {
|
|
338
|
+
initialFiles?: Record<string, string>;
|
|
339
|
+
} = {}
|
|
340
|
+
): Promise<Sandbox> {
|
|
341
|
+
const {
|
|
342
|
+
initialFiles,
|
|
343
|
+
tsconfigPath = "/tsconfig.json",
|
|
344
|
+
resources: providedResources,
|
|
345
|
+
onBuild: onBuildCallback,
|
|
346
|
+
customCommands = [],
|
|
347
|
+
sharedModules,
|
|
348
|
+
} = options;
|
|
349
|
+
|
|
350
|
+
// Determine which resources to use
|
|
351
|
+
const resourcesPromise = providedResources
|
|
352
|
+
? Promise.resolve(providedResources)
|
|
353
|
+
: getDefaultResources();
|
|
354
|
+
|
|
355
|
+
const bundlerPromise = initBundler();
|
|
356
|
+
|
|
357
|
+
// Create in-memory filesystem synchronously
|
|
358
|
+
const fs = IndexedDbFs.createInMemory({ initialFiles });
|
|
359
|
+
|
|
360
|
+
// Wait for resources and bundler
|
|
361
|
+
const [resources] = await Promise.all([resourcesPromise, bundlerPromise]);
|
|
362
|
+
|
|
363
|
+
// Extract lib files and types cache from resources
|
|
364
|
+
const libFiles = resources.libFiles;
|
|
365
|
+
const typesCache = resources.typesCache;
|
|
366
|
+
|
|
367
|
+
// Create build event emitter
|
|
368
|
+
const buildEmitter = new BuildEmitter();
|
|
369
|
+
|
|
370
|
+
// If a callback was provided in options, subscribe it
|
|
371
|
+
if (onBuildCallback) {
|
|
372
|
+
buildEmitter.on(onBuildCallback);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Create commands using the extracted factories
|
|
376
|
+
// Commands emit to the build emitter
|
|
377
|
+
const commandDeps: CommandDeps = {
|
|
378
|
+
fs,
|
|
379
|
+
libFiles,
|
|
380
|
+
tsconfigPath,
|
|
381
|
+
onBuild: buildEmitter.emit,
|
|
382
|
+
typesCache,
|
|
383
|
+
sharedModules,
|
|
384
|
+
};
|
|
385
|
+
const defaultCommands = createDefaultCommands(commandDeps);
|
|
386
|
+
|
|
387
|
+
// Create bash environment with the custom filesystem
|
|
388
|
+
const bash = new Bash({
|
|
389
|
+
fs,
|
|
390
|
+
cwd: "/",
|
|
391
|
+
customCommands: [...defaultCommands, ...customCommands],
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
fs,
|
|
396
|
+
bash,
|
|
397
|
+
isDirty: () => fs.isDirty(),
|
|
398
|
+
save: () => Promise.resolve(false), // No persistence for in-memory
|
|
399
|
+
close: () => { }, // No resources to close
|
|
400
|
+
onBuild: (callback) => buildEmitter.on(callback),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Module Registry for Sandlot
|
|
3
|
+
*
|
|
4
|
+
* Allows host applications to register their module instances (like React)
|
|
5
|
+
* so that dynamically bundled code can use the same instances instead of
|
|
6
|
+
* loading separate copies from esm.sh CDN.
|
|
7
|
+
*
|
|
8
|
+
* This solves the "multiple React instances" problem where hooks fail
|
|
9
|
+
* because the host and dynamic code use different React copies.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* // Host application setup
|
|
14
|
+
* import * as React from 'react';
|
|
15
|
+
* import * as ReactDOM from 'react-dom/client';
|
|
16
|
+
* import { registerSharedModules } from 'sandlot';
|
|
17
|
+
*
|
|
18
|
+
* registerSharedModules({
|
|
19
|
+
* 'react': React,
|
|
20
|
+
* 'react-dom/client': ReactDOM,
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* // Now create sandbox with sharedModules option
|
|
24
|
+
* const sandbox = await createInMemorySandbox({
|
|
25
|
+
* sharedModules: ['react', 'react-dom/client'],
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Global key used to expose the registry for dynamic bundles
|
|
32
|
+
*/
|
|
33
|
+
const GLOBAL_KEY = '__sandlot_shared_modules__';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Registry for sharing host modules with dynamic bundles.
|
|
37
|
+
* Modules registered here will be used instead of esm.sh CDN
|
|
38
|
+
* when the sandbox is configured with matching sharedModules.
|
|
39
|
+
*/
|
|
40
|
+
export class SharedModuleRegistry {
|
|
41
|
+
private modules = new Map<string, unknown>();
|
|
42
|
+
|
|
43
|
+
constructor() {
|
|
44
|
+
// Make available globally for dynamic imports
|
|
45
|
+
(globalThis as Record<string, unknown>)[GLOBAL_KEY] = this;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Register a module to be shared with dynamic bundles
|
|
50
|
+
*
|
|
51
|
+
* @param moduleId - The import specifier (e.g., 'react', 'react-dom/client')
|
|
52
|
+
* @param module - The module's exports object
|
|
53
|
+
* @returns this for chaining
|
|
54
|
+
*/
|
|
55
|
+
register(moduleId: string, module: unknown): this {
|
|
56
|
+
this.modules.set(moduleId, module);
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Register multiple modules at once
|
|
62
|
+
*
|
|
63
|
+
* @param modules - Object mapping module IDs to their exports
|
|
64
|
+
* @returns this for chaining
|
|
65
|
+
*/
|
|
66
|
+
registerAll(modules: Record<string, unknown>): this {
|
|
67
|
+
for (const [id, mod] of Object.entries(modules)) {
|
|
68
|
+
this.modules.set(id, mod);
|
|
69
|
+
}
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Unregister a previously registered module
|
|
75
|
+
*
|
|
76
|
+
* @param moduleId - The import specifier to remove
|
|
77
|
+
* @returns true if the module was registered and removed
|
|
78
|
+
*/
|
|
79
|
+
unregister(moduleId: string): boolean {
|
|
80
|
+
return this.modules.delete(moduleId);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get a registered module (used by dynamic bundles at runtime)
|
|
85
|
+
*
|
|
86
|
+
* @param moduleId - The import specifier
|
|
87
|
+
* @returns The registered module exports
|
|
88
|
+
* @throws Error if the module is not registered
|
|
89
|
+
*/
|
|
90
|
+
get(moduleId: string): unknown {
|
|
91
|
+
const mod = this.modules.get(moduleId);
|
|
92
|
+
if (mod === undefined && !this.modules.has(moduleId)) {
|
|
93
|
+
const available = this.list();
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Shared module "${moduleId}" not registered. ` +
|
|
96
|
+
`Available: ${available.length > 0 ? available.join(', ') : '(none)'}. ` +
|
|
97
|
+
`Call registerSharedModules({ '${moduleId}': ... }) in your host application.`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return mod;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Check if a module is registered
|
|
105
|
+
*
|
|
106
|
+
* @param moduleId - The import specifier to check
|
|
107
|
+
*/
|
|
108
|
+
has(moduleId: string): boolean {
|
|
109
|
+
return this.modules.has(moduleId);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get list of all registered module IDs
|
|
114
|
+
*/
|
|
115
|
+
list(): string[] {
|
|
116
|
+
return [...this.modules.keys()];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Clear all registrations
|
|
121
|
+
*/
|
|
122
|
+
clear(): void {
|
|
123
|
+
this.modules.clear();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get the number of registered modules
|
|
128
|
+
*/
|
|
129
|
+
get size(): number {
|
|
130
|
+
return this.modules.size;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Singleton instance
|
|
135
|
+
let defaultRegistry: SharedModuleRegistry | null = null;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get the default shared module registry.
|
|
139
|
+
* Creates it if it doesn't exist.
|
|
140
|
+
*/
|
|
141
|
+
export function getSharedModuleRegistry(): SharedModuleRegistry {
|
|
142
|
+
if (!defaultRegistry) {
|
|
143
|
+
defaultRegistry = new SharedModuleRegistry();
|
|
144
|
+
}
|
|
145
|
+
return defaultRegistry;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Check if a shared module registry exists on globalThis
|
|
150
|
+
*/
|
|
151
|
+
export function hasSharedModuleRegistry(): boolean {
|
|
152
|
+
return GLOBAL_KEY in globalThis;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Convenience function to register modules with the default registry.
|
|
157
|
+
*
|
|
158
|
+
* @param modules - Object mapping module IDs to their exports
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* ```ts
|
|
162
|
+
* import * as React from 'react';
|
|
163
|
+
* import * as ReactDOM from 'react-dom/client';
|
|
164
|
+
* import { registerSharedModules } from 'sandlot';
|
|
165
|
+
*
|
|
166
|
+
* registerSharedModules({
|
|
167
|
+
* 'react': React,
|
|
168
|
+
* 'react-dom/client': ReactDOM,
|
|
169
|
+
* });
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
export function registerSharedModules(modules: Record<string, unknown>): void {
|
|
173
|
+
getSharedModuleRegistry().registerAll(modules);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Convenience function to unregister a module from the default registry.
|
|
178
|
+
*
|
|
179
|
+
* @param moduleId - The import specifier to remove
|
|
180
|
+
* @returns true if the module was registered and removed
|
|
181
|
+
*/
|
|
182
|
+
export function unregisterSharedModule(moduleId: string): boolean {
|
|
183
|
+
return getSharedModuleRegistry().unregister(moduleId);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Clear all shared modules from the default registry.
|
|
188
|
+
*/
|
|
189
|
+
export function clearSharedModules(): void {
|
|
190
|
+
getSharedModuleRegistry().clear();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Generate the runtime code that dynamic bundles use to access shared modules.
|
|
195
|
+
* This is injected into bundles when they import from shared modules.
|
|
196
|
+
*/
|
|
197
|
+
export function getSharedModuleRuntimeCode(moduleId: string): string {
|
|
198
|
+
return `
|
|
199
|
+
(function() {
|
|
200
|
+
var registry = globalThis["${GLOBAL_KEY}"];
|
|
201
|
+
if (!registry) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
'Sandlot SharedModuleRegistry not found. ' +
|
|
204
|
+
'Call registerSharedModules() in your host application before loading dynamic modules.'
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
return registry.get(${JSON.stringify(moduleId)});
|
|
208
|
+
})()
|
|
209
|
+
`.trim();
|
|
210
|
+
}
|