sandlot 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/dist/browser/bundler.d.ts +68 -0
  2. package/dist/browser/bundler.d.ts.map +1 -0
  3. package/dist/browser/executor.d.ts +46 -0
  4. package/dist/browser/executor.d.ts.map +1 -0
  5. package/dist/browser/index.d.ts +9 -0
  6. package/dist/browser/index.d.ts.map +1 -0
  7. package/dist/browser/index.js +2692 -0
  8. package/dist/browser/preset.d.ts +63 -0
  9. package/dist/browser/preset.d.ts.map +1 -0
  10. package/dist/commands/index.d.ts +20 -11
  11. package/dist/commands/index.d.ts.map +1 -1
  12. package/dist/commands/types.d.ts +31 -132
  13. package/dist/commands/types.d.ts.map +1 -1
  14. package/dist/core/bundler-utils.d.ts +142 -0
  15. package/dist/core/bundler-utils.d.ts.map +1 -0
  16. package/dist/core/esm-types-resolver.d.ts +125 -0
  17. package/dist/core/esm-types-resolver.d.ts.map +1 -0
  18. package/dist/core/executor.d.ts +35 -0
  19. package/dist/core/executor.d.ts.map +1 -0
  20. package/dist/{fs.d.ts → core/fs.d.ts} +27 -29
  21. package/dist/core/fs.d.ts.map +1 -0
  22. package/dist/core/sandbox.d.ts +30 -0
  23. package/dist/core/sandbox.d.ts.map +1 -0
  24. package/dist/core/sandlot.d.ts +30 -0
  25. package/dist/core/sandlot.d.ts.map +1 -0
  26. package/dist/core/shared-module-registry.d.ts +46 -0
  27. package/dist/core/shared-module-registry.d.ts.map +1 -0
  28. package/dist/core/typechecker.d.ts +60 -0
  29. package/dist/core/typechecker.d.ts.map +1 -0
  30. package/dist/index.d.ts +11 -16
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +1405 -2049
  33. package/dist/node/bundler.d.ts +48 -0
  34. package/dist/node/bundler.d.ts.map +1 -0
  35. package/dist/node/executor.d.ts +48 -0
  36. package/dist/node/executor.d.ts.map +1 -0
  37. package/dist/node/index.d.ts +9 -0
  38. package/dist/node/index.d.ts.map +1 -0
  39. package/dist/node/index.js +2646 -0
  40. package/dist/node/preset.d.ts +62 -0
  41. package/dist/node/preset.d.ts.map +1 -0
  42. package/dist/types.d.ts +525 -0
  43. package/dist/types.d.ts.map +1 -0
  44. package/package.json +27 -8
  45. package/src/browser/bundler.ts +294 -0
  46. package/src/browser/executor.ts +71 -0
  47. package/src/browser/index.ts +57 -0
  48. package/src/browser/preset.ts +179 -0
  49. package/src/commands/index.ts +526 -43
  50. package/src/commands/types.ts +82 -146
  51. package/src/core/bundler-utils.ts +630 -0
  52. package/src/core/esm-types-resolver.ts +432 -0
  53. package/src/core/executor.ts +161 -0
  54. package/src/{fs.ts → core/fs.ts} +59 -37
  55. package/src/core/sandbox.ts +621 -0
  56. package/src/core/sandlot.ts +77 -0
  57. package/src/core/shared-module-registry.ts +138 -0
  58. package/src/core/typechecker.ts +607 -0
  59. package/src/index.ts +104 -139
  60. package/src/node/bundler.ts +194 -0
  61. package/src/node/executor.ts +87 -0
  62. package/src/node/index.ts +39 -0
  63. package/src/node/preset.ts +178 -0
  64. package/src/types.ts +668 -0
  65. package/README.md +0 -243
  66. package/dist/build-emitter.d.ts +0 -47
  67. package/dist/build-emitter.d.ts.map +0 -1
  68. package/dist/builder.d.ts +0 -370
  69. package/dist/builder.d.ts.map +0 -1
  70. package/dist/bundler.d.ts +0 -148
  71. package/dist/bundler.d.ts.map +0 -1
  72. package/dist/commands/compile.d.ts +0 -13
  73. package/dist/commands/compile.d.ts.map +0 -1
  74. package/dist/commands/packages.d.ts +0 -17
  75. package/dist/commands/packages.d.ts.map +0 -1
  76. package/dist/commands/run.d.ts +0 -40
  77. package/dist/commands/run.d.ts.map +0 -1
  78. package/dist/commands.d.ts +0 -179
  79. package/dist/commands.d.ts.map +0 -1
  80. package/dist/fs.d.ts.map +0 -1
  81. package/dist/internal.d.ts +0 -79
  82. package/dist/internal.d.ts.map +0 -1
  83. package/dist/internal.js +0 -1976
  84. package/dist/loader.d.ts +0 -164
  85. package/dist/loader.d.ts.map +0 -1
  86. package/dist/packages.d.ts +0 -199
  87. package/dist/packages.d.ts.map +0 -1
  88. package/dist/runner.d.ts +0 -314
  89. package/dist/runner.d.ts.map +0 -1
  90. package/dist/sandbox-manager.d.ts +0 -261
  91. package/dist/sandbox-manager.d.ts.map +0 -1
  92. package/dist/sandbox.d.ts +0 -267
  93. package/dist/sandbox.d.ts.map +0 -1
  94. package/dist/shared-modules.d.ts +0 -148
  95. package/dist/shared-modules.d.ts.map +0 -1
  96. package/dist/shared-resources.d.ts +0 -102
  97. package/dist/shared-resources.d.ts.map +0 -1
  98. package/dist/ts-libs.d.ts +0 -98
  99. package/dist/ts-libs.d.ts.map +0 -1
  100. package/dist/typechecker.d.ts +0 -127
  101. package/dist/typechecker.d.ts.map +0 -1
  102. package/src/build-emitter.ts +0 -64
  103. package/src/builder.ts +0 -498
  104. package/src/bundler.ts +0 -542
  105. package/src/commands/compile.ts +0 -236
  106. package/src/commands/packages.ts +0 -154
  107. package/src/commands/run.ts +0 -245
  108. package/src/internal.ts +0 -119
  109. package/src/loader.ts +0 -229
  110. package/src/packages.ts +0 -936
  111. package/src/sandbox.ts +0 -396
  112. package/src/shared-modules.ts +0 -280
  113. package/src/shared-resources.ts +0 -166
  114. package/src/ts-libs.ts +0 -320
  115. package/src/typechecker.ts +0 -635
package/src/sandbox.ts DELETED
@@ -1,396 +0,0 @@
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
- import { getDefaultResources, type SharedResources } from "./shared-resources";
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).
12
- */
13
- export type SandboxBashOptions = Omit<BashOptions, 'fs' | 'customCommands' | 'files'>;
14
-
15
- /**
16
- * Options for creating a sandbox environment
17
- */
18
- export interface SandboxOptions {
19
- /**
20
- * Initial files to populate the filesystem with.
21
- *
22
- * @example
23
- * ```ts
24
- * const sandbox = await createSandbox({
25
- * initialFiles: {
26
- * '/src/index.ts': 'export const x = 1;',
27
- * '/tsconfig.json': JSON.stringify({ compilerOptions: { strict: true } }),
28
- * },
29
- * });
30
- * ```
31
- */
32
- initialFiles?: Record<string, string>;
33
-
34
- /**
35
- * Maximum filesystem size in bytes (default: 50MB)
36
- */
37
- maxFilesystemSize?: number;
38
-
39
- /**
40
- * Path to tsconfig.json in the virtual filesystem.
41
- * Default: "/tsconfig.json"
42
- */
43
- tsconfigPath?: string;
44
-
45
- /**
46
- * Shared resources (lib files, bundler).
47
- * If not provided, uses the default singleton resources.
48
- *
49
- * Provide this to share resources across multiple sandboxes,
50
- * or to use custom TypeScript libs.
51
- */
52
- resources?: SharedResources;
53
-
54
- /**
55
- * Callback invoked when a build succeeds.
56
- * Receives the build output with the bundle and loaded module.
57
- *
58
- * For agent workflows, prefer using `createBuilder()` which handles
59
- * build capture automatically.
60
- */
61
- onBuild?: (result: BuildOutput) => void | Promise<void>;
62
-
63
- /**
64
- * Additional custom commands to add to the bash environment
65
- */
66
- customCommands?: ReturnType<typeof defineCommand>[];
67
-
68
- /**
69
- * Module IDs that should be resolved from the host's SharedModuleRegistry
70
- * instead of esm.sh CDN. The host must have registered these modules
71
- * using `registerSharedModules()` before loading dynamic code.
72
- *
73
- * This solves the "multiple React instances" problem by allowing dynamic
74
- * components to share the same React instance as the host application.
75
- *
76
- * Type definitions are automatically installed for these modules so that
77
- * TypeScript can typecheck code that imports them.
78
- *
79
- * @example
80
- * ```ts
81
- * // Host setup
82
- * import * as React from 'react';
83
- * import * as ReactDOM from 'react-dom/client';
84
- * import { registerSharedModules } from 'sandlot';
85
- *
86
- * registerSharedModules({
87
- * 'react': React,
88
- * 'react-dom/client': ReactDOM,
89
- * });
90
- *
91
- * // Create sandbox with shared modules (types auto-installed)
92
- * const sandbox = await createSandbox({
93
- * sharedModules: ['react', 'react-dom/client'],
94
- * });
95
- * ```
96
- */
97
- sharedModules?: string[];
98
-
99
- /**
100
- * Options passed through to the just-bash Bash constructor.
101
- * Use this to configure environment variables, execution limits,
102
- * network access, logging, and other bash-level settings.
103
- *
104
- * Note: `fs`, `customCommands`, and `files` are controlled by sandlot
105
- * and cannot be overridden here.
106
- *
107
- * @example
108
- * ```ts
109
- * const sandbox = await createSandbox({
110
- * bashOptions: {
111
- * cwd: '/src',
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>;
130
- }
131
-
132
- /**
133
- * The sandbox environment containing the filesystem and bash shell
134
- */
135
- export interface Sandbox {
136
- /**
137
- * The virtual filesystem
138
- */
139
- fs: Filesystem;
140
-
141
- /**
142
- * The just-bash shell environment
143
- */
144
- bash: Bash;
145
-
146
- /**
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
- *
152
- * @example
153
- * ```ts
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;
161
- * ```
162
- */
163
- lastBuild: BuildOutput | null;
164
-
165
- /**
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.
170
- *
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
- * ```
181
- */
182
- getState(): SandboxState;
183
-
184
- /**
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
- * ```
206
- */
207
- onBuild(callback: (result: BuildOutput) => void | Promise<void>): () => void;
208
-
209
- /**
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
- *
217
- * @example
218
- * ```ts
219
- * sandbox.setValidation((mod) => {
220
- * if (!mod.App) throw new Error("Must export App component");
221
- * return { App: mod.App as React.ComponentType };
222
- * });
223
- *
224
- * // Now build will fail if App is missing
225
- * await sandbox.bash.exec('build /src/index.ts');
226
- * ```
227
- */
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;
235
- }
236
-
237
- /**
238
- * Create an in-browser agent sandbox with a virtual filesystem, TypeScript
239
- * type checking, and bundling capabilities.
240
- *
241
- * The sandbox provides a just-bash shell with custom commands:
242
- * - `tsc [entry]` - Type check the project
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
248
- *
249
- * Build options:
250
- * - `--format, -f <esm|iife|cjs>` - Output format (default: esm)
251
- * - `--minify, -m` - Enable minification
252
- * - `--skip-typecheck, -s` - Skip type checking
253
- *
254
- * @example
255
- * ```ts
256
- * let bundleResult: BundleResult | null = null;
257
- *
258
- * const sandbox = await createSandbox({
259
- * initialFiles: {
260
- * '/src/index.ts': 'export const hello = "world";',
261
- * '/tsconfig.json': JSON.stringify({
262
- * compilerOptions: { target: 'ES2020', strict: true }
263
- * }),
264
- * },
265
- * onBuild: (result) => {
266
- * bundleResult = result;
267
- * },
268
- * });
269
- *
270
- * // Use bash commands
271
- * await sandbox.bash.exec('echo "console.log(1);" > /src/index.ts');
272
- *
273
- * // Type check
274
- * const tscResult = await sandbox.bash.exec('tsc /src/index.ts');
275
- * console.log(tscResult.stdout);
276
- *
277
- * // Build (includes typecheck, triggers onBuild callback)
278
- * const buildResult = await sandbox.bash.exec('build /src/index.ts');
279
- * console.log(buildResult.stdout);
280
- * console.log(bundleResult?.code); // The compiled bundle
281
- *
282
- * // Save state for later
283
- * const state = sandbox.getState();
284
- * localStorage.setItem('my-project', JSON.stringify(state));
285
- * ```
286
- */
287
- export async function createSandbox(options: SandboxOptions = {}): Promise<Sandbox> {
288
- const {
289
- initialFiles,
290
- maxFilesystemSize,
291
- tsconfigPath = "/tsconfig.json",
292
- resources: providedResources,
293
- onBuild: onBuildCallback,
294
- customCommands = [],
295
- sharedModules,
296
- bashOptions = {},
297
- } = options;
298
-
299
- // Create filesystem (synchronous)
300
- const fs = Filesystem.create({
301
- initialFiles,
302
- maxSizeBytes: maxFilesystemSize,
303
- });
304
-
305
- // Load shared resources and bundler in parallel
306
- const resourcesPromise = providedResources
307
- ? Promise.resolve(providedResources)
308
- : getDefaultResources();
309
-
310
- const bundlerPromise = initBundler();
311
-
312
- // Wait for async initialization
313
- const [resources] = await Promise.all([resourcesPromise, bundlerPromise]);
314
-
315
- // Extract lib files and types cache from resources
316
- const libFiles = resources.libFiles;
317
- const typesCache = resources.typesCache;
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
-
344
- // Create build event emitter
345
- const buildEmitter = new BuildEmitter();
346
-
347
- // Track the last successful build
348
- let lastBuild: BuildOutput | null = null;
349
- buildEmitter.on((result) => {
350
- lastBuild = result;
351
- });
352
-
353
- // If a callback was provided in options, subscribe it
354
- if (onBuildCallback) {
355
- buildEmitter.on(onBuildCallback);
356
- }
357
-
358
- // Validation function (can be set/cleared dynamically)
359
- let validationFn: ValidateFn | null = null;
360
-
361
- // Create commands
362
- const commandDeps: CommandDeps = {
363
- fs,
364
- libFiles,
365
- tsconfigPath,
366
- onBuild: buildEmitter.emit,
367
- getValidation: () => validationFn,
368
- typesCache,
369
- sharedModules,
370
- };
371
- const defaultCommands = createDefaultCommands(commandDeps);
372
-
373
- // Create bash environment with the custom filesystem
374
- const bash = new Bash({
375
- ...bashOptions,
376
- fs,
377
- customCommands: [...defaultCommands, ...customCommands],
378
- });
379
-
380
- return {
381
- fs,
382
- bash,
383
- get lastBuild() {
384
- return lastBuild;
385
- },
386
- getState: () => ({ files: fs.getFiles() }),
387
- onBuild: (callback) => buildEmitter.on(callback),
388
- setValidation: (fn: ValidateFn) => {
389
- validationFn = fn;
390
- },
391
- clearValidation: () => {
392
- validationFn = null;
393
- },
394
- };
395
- }
396
-
@@ -1,280 +0,0 @@
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 createSandbox({
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
- private exportNames = new Map<string, string[]>();
43
-
44
- constructor() {
45
- // Make available globally for dynamic imports
46
- (globalThis as Record<string, unknown>)[GLOBAL_KEY] = this;
47
- }
48
-
49
- /**
50
- * Register a module to be shared with dynamic bundles.
51
- * Automatically introspects the module to discover its exports.
52
- *
53
- * @param moduleId - The import specifier (e.g., 'react', 'react-dom/client')
54
- * @param module - The module's exports object
55
- * @returns this for chaining
56
- */
57
- register(moduleId: string, module: unknown): this {
58
- this.modules.set(moduleId, module);
59
- // Introspect the module to get its export names
60
- this.exportNames.set(moduleId, introspectExports(module));
61
- return this;
62
- }
63
-
64
- /**
65
- * Register multiple modules at once.
66
- * Automatically introspects each module to discover its exports.
67
- *
68
- * @param modules - Object mapping module IDs to their exports
69
- * @returns this for chaining
70
- */
71
- registerAll(modules: Record<string, unknown>): this {
72
- for (const [id, mod] of Object.entries(modules)) {
73
- this.register(id, mod);
74
- }
75
- return this;
76
- }
77
-
78
- /**
79
- * Unregister a previously registered module
80
- *
81
- * @param moduleId - The import specifier to remove
82
- * @returns true if the module was registered and removed
83
- */
84
- unregister(moduleId: string): boolean {
85
- return this.modules.delete(moduleId);
86
- }
87
-
88
- /**
89
- * Get a registered module (used by dynamic bundles at runtime)
90
- *
91
- * @param moduleId - The import specifier
92
- * @returns The registered module exports
93
- * @throws Error if the module is not registered
94
- */
95
- get(moduleId: string): unknown {
96
- const mod = this.modules.get(moduleId);
97
- if (mod === undefined && !this.modules.has(moduleId)) {
98
- const available = this.list();
99
- throw new Error(
100
- `Shared module "${moduleId}" not registered. ` +
101
- `Available: ${available.length > 0 ? available.join(', ') : '(none)'}. ` +
102
- `Call registerSharedModules({ '${moduleId}': ... }) in your host application.`
103
- );
104
- }
105
- return mod;
106
- }
107
-
108
- /**
109
- * Check if a module is registered
110
- *
111
- * @param moduleId - The import specifier to check
112
- */
113
- has(moduleId: string): boolean {
114
- return this.modules.has(moduleId);
115
- }
116
-
117
- /**
118
- * Get list of all registered module IDs
119
- */
120
- list(): string[] {
121
- return [...this.modules.keys()];
122
- }
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
-
135
- /**
136
- * Clear all registrations
137
- */
138
- clear(): void {
139
- this.modules.clear();
140
- this.exportNames.clear();
141
- }
142
-
143
- /**
144
- * Get the number of registered modules
145
- */
146
- get size(): number {
147
- return this.modules.size;
148
- }
149
- }
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
-
193
- // Singleton instance
194
- let defaultRegistry: SharedModuleRegistry | null = null;
195
-
196
- /**
197
- * Get the default shared module registry.
198
- * Creates it if it doesn't exist.
199
- */
200
- export function getSharedModuleRegistry(): SharedModuleRegistry {
201
- if (!defaultRegistry) {
202
- defaultRegistry = new SharedModuleRegistry();
203
- }
204
- return defaultRegistry;
205
- }
206
-
207
- /**
208
- * Check if a shared module registry exists on globalThis
209
- */
210
- export function hasSharedModuleRegistry(): boolean {
211
- return GLOBAL_KEY in globalThis;
212
- }
213
-
214
- /**
215
- * Convenience function to register modules with the default registry.
216
- *
217
- * @param modules - Object mapping module IDs to their exports
218
- *
219
- * @example
220
- * ```ts
221
- * import * as React from 'react';
222
- * import * as ReactDOM from 'react-dom/client';
223
- * import { registerSharedModules } from 'sandlot';
224
- *
225
- * registerSharedModules({
226
- * 'react': React,
227
- * 'react-dom/client': ReactDOM,
228
- * });
229
- * ```
230
- */
231
- export function registerSharedModules(modules: Record<string, unknown>): void {
232
- getSharedModuleRegistry().registerAll(modules);
233
- }
234
-
235
- /**
236
- * Convenience function to unregister a module from the default registry.
237
- *
238
- * @param moduleId - The import specifier to remove
239
- * @returns true if the module was registered and removed
240
- */
241
- export function unregisterSharedModule(moduleId: string): boolean {
242
- return getSharedModuleRegistry().unregister(moduleId);
243
- }
244
-
245
- /**
246
- * Clear all shared modules from the default registry.
247
- */
248
- export function clearSharedModules(): void {
249
- getSharedModuleRegistry().clear();
250
- }
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
-
263
- /**
264
- * Generate the runtime code that dynamic bundles use to access shared modules.
265
- * This is injected into bundles when they import from shared modules.
266
- */
267
- export function getSharedModuleRuntimeCode(moduleId: string): string {
268
- return `
269
- (function() {
270
- var registry = globalThis["${GLOBAL_KEY}"];
271
- if (!registry) {
272
- throw new Error(
273
- 'Sandlot SharedModuleRegistry not found. ' +
274
- 'Call registerSharedModules() in your host application before loading dynamic modules.'
275
- );
276
- }
277
- return registry.get(${JSON.stringify(moduleId)});
278
- })()
279
- `.trim();
280
- }