sandlot 0.1.4 → 0.2.1
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/dist/browser/bundler.d.ts +68 -0
- package/dist/browser/bundler.d.ts.map +1 -0
- package/dist/browser/executor.d.ts +46 -0
- package/dist/browser/executor.d.ts.map +1 -0
- package/dist/browser/index.d.ts +9 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +2690 -0
- package/dist/browser/preset.d.ts +63 -0
- package/dist/browser/preset.d.ts.map +1 -0
- package/dist/commands/index.d.ts +20 -11
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/types.d.ts +37 -130
- package/dist/commands/types.d.ts.map +1 -1
- package/dist/core/bundler-utils.d.ts +142 -0
- package/dist/core/bundler-utils.d.ts.map +1 -0
- package/dist/core/esm-types-resolver.d.ts +125 -0
- package/dist/core/esm-types-resolver.d.ts.map +1 -0
- package/dist/core/executor.d.ts +35 -0
- package/dist/core/executor.d.ts.map +1 -0
- package/dist/{fs.d.ts → core/fs.d.ts} +27 -29
- package/dist/core/fs.d.ts.map +1 -0
- package/dist/core/sandbox.d.ts +30 -0
- package/dist/core/sandbox.d.ts.map +1 -0
- package/dist/core/sandlot.d.ts +30 -0
- package/dist/core/sandlot.d.ts.map +1 -0
- package/dist/core/shared-module-registry.d.ts +46 -0
- package/dist/core/shared-module-registry.d.ts.map +1 -0
- package/dist/core/typechecker.d.ts +60 -0
- package/dist/core/typechecker.d.ts.map +1 -0
- package/dist/index.d.ts +11 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1398 -2010
- package/dist/node/bundler.d.ts +48 -0
- package/dist/node/bundler.d.ts.map +1 -0
- package/dist/node/executor.d.ts +48 -0
- package/dist/node/executor.d.ts.map +1 -0
- package/dist/node/index.d.ts +9 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +2644 -0
- package/dist/node/preset.d.ts +62 -0
- package/dist/node/preset.d.ts.map +1 -0
- package/dist/types.d.ts +528 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +16 -6
- package/src/browser/bundler.ts +294 -0
- package/src/browser/executor.ts +71 -0
- package/src/browser/index.ts +57 -0
- package/src/browser/preset.ts +179 -0
- package/src/commands/index.ts +498 -37
- package/src/commands/types.ts +117 -145
- package/src/core/bundler-utils.ts +630 -0
- package/src/core/esm-types-resolver.ts +432 -0
- package/src/core/executor.ts +161 -0
- package/src/{fs.ts → core/fs.ts} +59 -37
- package/src/core/sandbox.ts +624 -0
- package/src/core/sandlot.ts +77 -0
- package/src/core/shared-module-registry.ts +138 -0
- package/src/core/typechecker.ts +609 -0
- package/src/index.ts +106 -139
- package/src/node/bundler.ts +194 -0
- package/src/node/executor.ts +87 -0
- package/src/node/index.ts +39 -0
- package/src/node/preset.ts +178 -0
- package/src/types.ts +672 -0
- package/README.md +0 -243
- package/dist/build-emitter.d.ts +0 -47
- package/dist/build-emitter.d.ts.map +0 -1
- package/dist/builder.d.ts +0 -370
- package/dist/builder.d.ts.map +0 -1
- package/dist/bundler.d.ts +0 -152
- package/dist/bundler.d.ts.map +0 -1
- package/dist/commands/compile.d.ts +0 -13
- package/dist/commands/compile.d.ts.map +0 -1
- package/dist/commands/packages.d.ts +0 -17
- package/dist/commands/packages.d.ts.map +0 -1
- package/dist/commands/run.d.ts +0 -40
- package/dist/commands/run.d.ts.map +0 -1
- package/dist/commands.d.ts +0 -179
- package/dist/commands.d.ts.map +0 -1
- package/dist/fs.d.ts.map +0 -1
- package/dist/internal.d.ts +0 -79
- package/dist/internal.d.ts.map +0 -1
- package/dist/internal.js +0 -1942
- package/dist/loader.d.ts +0 -164
- package/dist/loader.d.ts.map +0 -1
- package/dist/packages.d.ts +0 -199
- package/dist/packages.d.ts.map +0 -1
- package/dist/runner.d.ts +0 -314
- package/dist/runner.d.ts.map +0 -1
- package/dist/sandbox-manager.d.ts +0 -261
- package/dist/sandbox-manager.d.ts.map +0 -1
- package/dist/sandbox.d.ts +0 -267
- package/dist/sandbox.d.ts.map +0 -1
- package/dist/shared-modules.d.ts +0 -148
- package/dist/shared-modules.d.ts.map +0 -1
- package/dist/shared-resources.d.ts +0 -102
- package/dist/shared-resources.d.ts.map +0 -1
- package/dist/ts-libs.d.ts +0 -85
- package/dist/ts-libs.d.ts.map +0 -1
- package/dist/typechecker.d.ts +0 -127
- package/dist/typechecker.d.ts.map +0 -1
- package/src/build-emitter.ts +0 -64
- package/src/builder.ts +0 -498
- package/src/bundler.ts +0 -575
- package/src/commands/compile.ts +0 -236
- package/src/commands/packages.ts +0 -154
- package/src/commands/run.ts +0 -245
- package/src/internal.ts +0 -119
- package/src/loader.ts +0 -229
- package/src/packages.ts +0 -936
- package/src/sandbox.ts +0 -398
- package/src/shared-modules.ts +0 -280
- package/src/shared-resources.ts +0 -166
- package/src/ts-libs.ts +0 -218
- 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
|
+
}
|