sandlot 0.1.4 → 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 +1399 -2010
  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 +16 -6
  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 -152
  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 -1942
  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 -85
  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 -575
  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 -398
  112. package/src/shared-modules.ts +0 -280
  113. package/src/shared-resources.ts +0 -166
  114. package/src/ts-libs.ts +0 -218
  115. package/src/typechecker.ts +0 -635
@@ -0,0 +1,630 @@
1
+ /**
2
+ * Shared bundler utilities used by both browser and node bundlers.
3
+ *
4
+ * This module contains the VFS plugin, path resolution, and shared module
5
+ * code generation logic that is common to both esbuild and esbuild-wasm.
6
+ */
7
+
8
+ import type {
9
+ ISharedModuleRegistry,
10
+ BundleWarning,
11
+ BundleError,
12
+ BundleLocation,
13
+ Filesystem,
14
+ } from "../types";
15
+
16
+ // =============================================================================
17
+ // Types
18
+ // =============================================================================
19
+
20
+ /**
21
+ * Minimal esbuild types needed for the shared utilities.
22
+ * These are compatible with both esbuild and esbuild-wasm.
23
+ */
24
+ export interface EsbuildMessage {
25
+ text: string;
26
+ location?: {
27
+ file: string;
28
+ line: number;
29
+ column: number;
30
+ lineText: string;
31
+ } | null;
32
+ }
33
+
34
+ export interface EsbuildPlugin {
35
+ name: string;
36
+ setup: (build: EsbuildPluginBuild) => void;
37
+ }
38
+
39
+ export interface EsbuildPluginBuild {
40
+ onResolve: (
41
+ options: { filter: RegExp; namespace?: string },
42
+ callback: (args: EsbuildResolveArgs) => Promise<EsbuildResolveResult | null | undefined> | EsbuildResolveResult | null | undefined
43
+ ) => void;
44
+ onLoad: (
45
+ options: { filter: RegExp; namespace?: string },
46
+ callback: (args: EsbuildLoadArgs) => Promise<EsbuildLoadResult | null | undefined> | EsbuildLoadResult | null | undefined
47
+ ) => void;
48
+ }
49
+
50
+ export interface EsbuildResolveArgs {
51
+ path: string;
52
+ kind: string;
53
+ resolveDir: string;
54
+ importer: string;
55
+ namespace: string;
56
+ }
57
+
58
+ export interface EsbuildResolveResult {
59
+ path?: string;
60
+ namespace?: string;
61
+ external?: boolean;
62
+ errors?: Array<{ text: string }>;
63
+ }
64
+
65
+ export interface EsbuildLoadArgs {
66
+ path: string;
67
+ }
68
+
69
+ export interface EsbuildLoadResult {
70
+ contents?: string;
71
+ loader?: string;
72
+ resolveDir?: string;
73
+ errors?: Array<{ text: string }>;
74
+ }
75
+
76
+ export type EsbuildLoader =
77
+ | "js"
78
+ | "jsx"
79
+ | "ts"
80
+ | "tsx"
81
+ | "json"
82
+ | "css"
83
+ | "text";
84
+
85
+ // =============================================================================
86
+ // Error Handling Helpers
87
+ // =============================================================================
88
+
89
+ /**
90
+ * Type guard for esbuild BuildFailure
91
+ */
92
+ export function isEsbuildBuildFailure(
93
+ err: unknown
94
+ ): err is { errors: EsbuildMessage[]; warnings: EsbuildMessage[] } {
95
+ return (
96
+ typeof err === "object" &&
97
+ err !== null &&
98
+ "errors" in err &&
99
+ Array.isArray((err as { errors: unknown }).errors)
100
+ );
101
+ }
102
+
103
+ /**
104
+ * Convert esbuild Message to our BundleError/BundleWarning format
105
+ */
106
+ export function convertEsbuildMessage(
107
+ msg: EsbuildMessage
108
+ ): BundleError | BundleWarning {
109
+ let location: BundleLocation | undefined;
110
+
111
+ if (msg.location) {
112
+ location = {
113
+ file: msg.location.file,
114
+ line: msg.location.line,
115
+ column: msg.location.column,
116
+ lineText: msg.location.lineText,
117
+ };
118
+ }
119
+
120
+ return {
121
+ text: msg.text,
122
+ location,
123
+ };
124
+ }
125
+
126
+ // =============================================================================
127
+ // VFS Plugin
128
+ // =============================================================================
129
+
130
+ export interface VfsPluginOptions {
131
+ fs: Filesystem;
132
+ entryPoint: string;
133
+ installedPackages: Record<string, string>;
134
+ sharedModules: Set<string>;
135
+ sharedModuleRegistry: ISharedModuleRegistry | null;
136
+ cdnBaseUrl: string;
137
+ includedFiles: Set<string>;
138
+ /**
139
+ * If true, CDN imports (http/https URLs) will be bundled by esbuild
140
+ * rather than marked as external. This is required for Node/Bun
141
+ * since they cannot resolve HTTP imports at runtime.
142
+ *
143
+ * - Browser: false (external) - browser can fetch at runtime
144
+ * - Node/Bun: true (bundle) - native esbuild fetches during build
145
+ *
146
+ * @default false
147
+ */
148
+ bundleCdnImports?: boolean;
149
+ }
150
+
151
+ /**
152
+ * Get the registry key for shared module access.
153
+ * Returns null if no registry is provided.
154
+ */
155
+ function getRegistryKey(registry: ISharedModuleRegistry | null): string | null {
156
+ return registry?.registryKey ?? null;
157
+ }
158
+
159
+ /**
160
+ * Create an esbuild plugin that reads from a virtual filesystem.
161
+ */
162
+ export function createVfsPlugin(options: VfsPluginOptions): EsbuildPlugin {
163
+ const {
164
+ fs,
165
+ entryPoint,
166
+ installedPackages,
167
+ sharedModules,
168
+ sharedModuleRegistry,
169
+ cdnBaseUrl,
170
+ includedFiles,
171
+ bundleCdnImports = false,
172
+ } = options;
173
+
174
+ return {
175
+ name: "sandlot-vfs",
176
+ setup(build) {
177
+ // ---------------------------------------------------------------------
178
+ // Resolution
179
+ // ---------------------------------------------------------------------
180
+
181
+ build.onResolve({ filter: /.*/ }, async (args) => {
182
+ // Skip if this is a resolution from the http namespace
183
+ // (those are handled by the http-specific onResolve handler)
184
+ if (args.namespace === "http") {
185
+ return undefined;
186
+ }
187
+
188
+ // Entry point → VFS namespace
189
+ if (args.kind === "entry-point") {
190
+ return { path: entryPoint, namespace: "vfs" };
191
+ }
192
+
193
+ // HTTP/HTTPS URLs handling
194
+ // - Browser: mark as external (browser fetches at runtime)
195
+ // - Node/Bun: use http namespace to fetch and bundle
196
+ if (args.path.startsWith("http://") || args.path.startsWith("https://")) {
197
+ if (bundleCdnImports) {
198
+ // Put in http namespace so our onLoad handler can fetch it
199
+ return { path: args.path, namespace: "http" };
200
+ }
201
+ return { path: args.path, external: true };
202
+ }
203
+
204
+ // Bare imports (not starting with . or /)
205
+ if (isBareImport(args.path)) {
206
+ // Check if this is a shared module
207
+ const sharedMatch = matchSharedModule(args.path, sharedModules);
208
+ if (sharedMatch) {
209
+ return { path: sharedMatch, namespace: "sandlot-shared" };
210
+ }
211
+
212
+ // Rewrite to CDN URL if package is installed
213
+ const cdnUrl = resolveToEsmUrl(args.path, installedPackages, cdnBaseUrl);
214
+ if (cdnUrl) {
215
+ if (bundleCdnImports) {
216
+ // Use http namespace so our onLoad handler can fetch it
217
+ return { path: cdnUrl, namespace: "http" };
218
+ }
219
+ return { path: cdnUrl, external: true };
220
+ }
221
+
222
+ // Not installed - mark as external (will fail at runtime if not available)
223
+ return { path: args.path, external: true };
224
+ }
225
+
226
+ // Relative or absolute imports → resolve in VFS
227
+ const resolved = resolveVfsPath(fs, args.resolveDir, args.path);
228
+ if (resolved) {
229
+ return { path: resolved, namespace: "vfs" };
230
+ }
231
+
232
+ return {
233
+ errors: [{ text: `Cannot resolve: ${args.path} from ${args.resolveDir}` }],
234
+ };
235
+ });
236
+
237
+ // ---------------------------------------------------------------------
238
+ // Loading: VFS files
239
+ // ---------------------------------------------------------------------
240
+
241
+ build.onLoad({ filter: /.*/, namespace: "vfs" }, async (args) => {
242
+ try {
243
+ const contents = fs.readFile(args.path);
244
+ includedFiles.add(args.path);
245
+
246
+ return {
247
+ contents,
248
+ loader: getLoader(args.path),
249
+ resolveDir: dirname(args.path),
250
+ };
251
+ } catch (err) {
252
+ return {
253
+ errors: [{ text: `Failed to read ${args.path}: ${err}` }],
254
+ };
255
+ }
256
+ });
257
+
258
+ // ---------------------------------------------------------------------
259
+ // Loading: Shared modules
260
+ // ---------------------------------------------------------------------
261
+
262
+ build.onLoad({ filter: /.*/, namespace: "sandlot-shared" }, (args) => {
263
+ const moduleId = args.path;
264
+
265
+ // Generate code that accesses the shared module registry at runtime
266
+ const runtimeCode = generateSharedModuleCode(
267
+ moduleId,
268
+ sharedModuleRegistry
269
+ );
270
+
271
+ return {
272
+ contents: runtimeCode,
273
+ loader: "js",
274
+ };
275
+ });
276
+
277
+ // ---------------------------------------------------------------------
278
+ // Loading & Resolution: HTTP/HTTPS URLs (for Node/Bun bundling)
279
+ // ---------------------------------------------------------------------
280
+
281
+ if (bundleCdnImports) {
282
+ // Resolve imports from within HTTP modules
283
+ // The importer will be the full HTTP URL
284
+ build.onResolve({ filter: /.*/, namespace: "http" }, (args) => {
285
+ const importerUrl = args.importer; // e.g., https://esm.sh/nanoid@latest
286
+
287
+ // Node.js built-in modules should be external (resolved at runtime)
288
+ if (args.path.startsWith("node:")) {
289
+ return { path: args.path, external: true };
290
+ }
291
+
292
+ if (args.path.startsWith("http://") || args.path.startsWith("https://")) {
293
+ // Already a full URL
294
+ return { path: args.path, namespace: "http" };
295
+ }
296
+
297
+ if (args.path.startsWith("/")) {
298
+ // Absolute path - resolve against the origin
299
+ const origin = new URL(importerUrl).origin;
300
+ return { path: origin + args.path, namespace: "http" };
301
+ }
302
+
303
+ if (args.path.startsWith(".")) {
304
+ // Relative path - resolve against the importer's directory
305
+ const resolved = new URL(args.path, importerUrl).href;
306
+ return { path: resolved, namespace: "http" };
307
+ }
308
+
309
+ // Bare import from within an HTTP module - check if it's a known package
310
+ // This handles cases where a CDN module imports another package
311
+ const cdnUrl = resolveToEsmUrl(args.path, installedPackages, cdnBaseUrl);
312
+ if (cdnUrl) {
313
+ return { path: cdnUrl, namespace: "http" };
314
+ }
315
+
316
+ // Unknown bare import - try to resolve from the CDN with latest version
317
+ // (esm.sh and similar CDNs can resolve packages automatically)
318
+ const fallbackUrl = `${cdnBaseUrl}/${args.path}`;
319
+ return { path: fallbackUrl, namespace: "http" };
320
+ });
321
+
322
+ // Load HTTP modules by fetching them
323
+ build.onLoad({ filter: /.*/, namespace: "http" }, async (args) => {
324
+ try {
325
+ const response = await fetch(args.path);
326
+ if (!response.ok) {
327
+ return {
328
+ errors: [{ text: `Failed to fetch ${args.path}: ${response.status} ${response.statusText}` }],
329
+ };
330
+ }
331
+
332
+ const contents = await response.text();
333
+
334
+ // Determine loader from URL
335
+ const loader = getLoaderFromUrl(args.path);
336
+
337
+ return {
338
+ contents,
339
+ loader,
340
+ // Don't set resolveDir - we'll handle resolution via namespace
341
+ };
342
+ } catch (err) {
343
+ return {
344
+ errors: [{ text: `Failed to fetch ${args.path}: ${err}` }],
345
+ };
346
+ }
347
+ });
348
+ }
349
+ },
350
+ };
351
+ }
352
+
353
+ /**
354
+ * Get the appropriate loader based on URL path
355
+ */
356
+ function getLoaderFromUrl(url: string): EsbuildLoader {
357
+ try {
358
+ const pathname = new URL(url).pathname;
359
+ return getLoader(pathname);
360
+ } catch {
361
+ return "js";
362
+ }
363
+ }
364
+
365
+ // =============================================================================
366
+ // Resolution Helpers
367
+ // =============================================================================
368
+
369
+ /**
370
+ * Check if a path is a bare import (npm package, not relative/absolute)
371
+ */
372
+ export function isBareImport(path: string): boolean {
373
+ return !path.startsWith(".") && !path.startsWith("/");
374
+ }
375
+
376
+ /**
377
+ * Check if an import matches a shared module.
378
+ * Handles exact matches and subpath imports.
379
+ */
380
+ export function matchSharedModule(
381
+ importPath: string,
382
+ sharedModules: Set<string>
383
+ ): string | null {
384
+ // Exact match
385
+ if (sharedModules.has(importPath)) {
386
+ return importPath;
387
+ }
388
+
389
+ // Check if any shared module is a prefix (for subpath imports)
390
+ for (const moduleId of sharedModules) {
391
+ if (importPath.startsWith(moduleId + "/")) {
392
+ // The full import path should be registered
393
+ // e.g., if "react-dom/client" is shared, match it exactly
394
+ // This allows partial sharing where only specific subpaths are shared
395
+ if (sharedModules.has(importPath)) {
396
+ return importPath;
397
+ }
398
+ }
399
+ }
400
+
401
+ return null;
402
+ }
403
+
404
+ /**
405
+ * Parse an import path into package name and subpath.
406
+ */
407
+ export function parseImportPath(importPath: string): {
408
+ packageName: string;
409
+ subpath?: string;
410
+ } {
411
+ // Scoped packages: @scope/name or @scope/name/subpath
412
+ if (importPath.startsWith("@")) {
413
+ const parts = importPath.split("/");
414
+ if (parts.length >= 2) {
415
+ const packageName = `${parts[0]}/${parts[1]}`;
416
+ const subpath = parts.length > 2 ? parts.slice(2).join("/") : undefined;
417
+ return { packageName, subpath };
418
+ }
419
+ return { packageName: importPath };
420
+ }
421
+
422
+ // Regular packages: name or name/subpath
423
+ const slashIndex = importPath.indexOf("/");
424
+ if (slashIndex === -1) {
425
+ return { packageName: importPath };
426
+ }
427
+
428
+ return {
429
+ packageName: importPath.slice(0, slashIndex),
430
+ subpath: importPath.slice(slashIndex + 1),
431
+ };
432
+ }
433
+
434
+ /**
435
+ * Resolve a bare import to an esm.sh CDN URL.
436
+ */
437
+ export function resolveToEsmUrl(
438
+ importPath: string,
439
+ installedPackages: Record<string, string>,
440
+ cdnBaseUrl: string
441
+ ): string | null {
442
+ const { packageName, subpath } = parseImportPath(importPath);
443
+
444
+ const version = installedPackages[packageName];
445
+ if (!version) {
446
+ return null;
447
+ }
448
+
449
+ const baseUrl = `${cdnBaseUrl}/${packageName}@${version}`;
450
+ return subpath ? `${baseUrl}/${subpath}` : baseUrl;
451
+ }
452
+
453
+ /**
454
+ * Resolve a relative or absolute path in the VFS.
455
+ * Tries extensions and index files as needed.
456
+ */
457
+ export function resolveVfsPath(
458
+ fs: Filesystem,
459
+ resolveDir: string,
460
+ importPath: string
461
+ ): string | null {
462
+ // Resolve the path relative to resolveDir
463
+ const resolved = resolvePath(resolveDir, importPath);
464
+
465
+ // Extensions to try
466
+ const extensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".json"];
467
+
468
+ // Check if path already has an extension we recognize
469
+ const hasExtension = extensions.some((ext) => resolved.endsWith(ext));
470
+
471
+ if (hasExtension) {
472
+ if (fs.exists(resolved)) {
473
+ return resolved;
474
+ }
475
+ return null;
476
+ }
477
+
478
+ // Try adding extensions
479
+ for (const ext of extensions) {
480
+ const withExt = resolved + ext;
481
+ if (fs.exists(withExt)) {
482
+ return withExt;
483
+ }
484
+ }
485
+
486
+ // Try index files (for directory imports)
487
+ for (const ext of extensions) {
488
+ const indexPath = `${resolved}/index${ext}`;
489
+ if (fs.exists(indexPath)) {
490
+ return indexPath;
491
+ }
492
+ }
493
+
494
+ return null;
495
+ }
496
+
497
+ /**
498
+ * Simple path resolution (handles . and ..)
499
+ */
500
+ export function resolvePath(from: string, to: string): string {
501
+ if (to.startsWith("/")) {
502
+ return normalizePath(to);
503
+ }
504
+
505
+ const fromParts = from.split("/").filter(Boolean);
506
+ const toParts = to.split("/");
507
+
508
+ // Start from the 'from' directory
509
+ const result = [...fromParts];
510
+
511
+ for (const part of toParts) {
512
+ if (part === "." || part === "") {
513
+ continue;
514
+ } else if (part === "..") {
515
+ result.pop();
516
+ } else {
517
+ result.push(part);
518
+ }
519
+ }
520
+
521
+ return "/" + result.join("/");
522
+ }
523
+
524
+ /**
525
+ * Normalize a path (remove . and ..)
526
+ */
527
+ export function normalizePath(path: string): string {
528
+ const parts = path.split("/").filter(Boolean);
529
+ const result: string[] = [];
530
+
531
+ for (const part of parts) {
532
+ if (part === ".") {
533
+ continue;
534
+ } else if (part === "..") {
535
+ result.pop();
536
+ } else {
537
+ result.push(part);
538
+ }
539
+ }
540
+
541
+ return "/" + result.join("/");
542
+ }
543
+
544
+ /**
545
+ * Get the directory name of a path
546
+ */
547
+ export function dirname(path: string): string {
548
+ const lastSlash = path.lastIndexOf("/");
549
+ if (lastSlash <= 0) return "/";
550
+ return path.slice(0, lastSlash);
551
+ }
552
+
553
+ /**
554
+ * Get the appropriate esbuild loader based on file extension
555
+ */
556
+ export function getLoader(path: string): EsbuildLoader {
557
+ const ext = path.split(".").pop()?.toLowerCase();
558
+ switch (ext) {
559
+ case "ts":
560
+ return "ts";
561
+ case "tsx":
562
+ return "tsx";
563
+ case "jsx":
564
+ return "jsx";
565
+ case "js":
566
+ case "mjs":
567
+ return "js";
568
+ case "json":
569
+ return "json";
570
+ case "css":
571
+ return "css";
572
+ case "txt":
573
+ return "text";
574
+ default:
575
+ return "js";
576
+ }
577
+ }
578
+
579
+ // =============================================================================
580
+ // Shared Module Code Generation
581
+ // =============================================================================
582
+
583
+ /**
584
+ * Generate JavaScript code that accesses a shared module at runtime.
585
+ */
586
+ export function generateSharedModuleCode(
587
+ moduleId: string,
588
+ registry: ISharedModuleRegistry | null
589
+ ): string {
590
+ const registryKey = getRegistryKey(registry);
591
+
592
+ if (!registryKey) {
593
+ return `throw new Error("Shared module '${moduleId}' requested but no registry configured");`;
594
+ }
595
+
596
+ // Generate the runtime access code using the instance-specific registry key
597
+ const runtimeAccess = `
598
+ (function() {
599
+ var registry = globalThis["${registryKey}"];
600
+ if (!registry) {
601
+ throw new Error(
602
+ 'Sandlot SharedModuleRegistry not found at "${registryKey}". ' +
603
+ 'Ensure sharedModules are configured in createSandlot() options.'
604
+ );
605
+ }
606
+ return registry.get(${JSON.stringify(moduleId)});
607
+ })()
608
+ `.trim();
609
+
610
+ // Get export names if registry is available (for generating named exports)
611
+ const exportNames = registry?.getExportNames(moduleId) ?? [];
612
+
613
+ // Build the module code
614
+ let code = `const __sandlot_mod__ = ${runtimeAccess};\n`;
615
+
616
+ // Default export (handle both { default: x } and direct export)
617
+ code += `export default __sandlot_mod__.default ?? __sandlot_mod__;\n`;
618
+
619
+ // Named exports
620
+ if (exportNames.length > 0) {
621
+ for (const name of exportNames) {
622
+ code += `export const ${name} = __sandlot_mod__.${name};\n`;
623
+ }
624
+ } else {
625
+ code += `// No named exports discovered for "${moduleId}"\n`;
626
+ code += `// Use: import mod from "${moduleId}"; mod.exportName\n`;
627
+ }
628
+
629
+ return code;
630
+ }