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/bundler.ts DELETED
@@ -1,542 +0,0 @@
1
- import type { IFileSystem } from "just-bash/browser";
2
- import type * as EsbuildTypes from "esbuild-wasm";
3
- import { getPackageManifest, resolveToEsmUrl } from "./packages";
4
- import { getSharedModuleRuntimeCode, getSharedModuleExports } from "./shared-modules";
5
-
6
- // Lazily loaded esbuild module - loaded from CDN to avoid bundler issues
7
- let esbuild: typeof EsbuildTypes | null = null;
8
-
9
- async function getEsbuild(): Promise<typeof EsbuildTypes> {
10
- if (esbuild) return esbuild;
11
-
12
- // Load esbuild-wasm from esm.sh CDN to avoid bundler transformation issues
13
- // esm.sh provides proper ESM wrappers for npm packages
14
- const cdnUrl = `https://esm.sh/esbuild-wasm@${ESBUILD_VERSION}`;
15
-
16
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
- const mod: any = await import(/* @vite-ignore */ cdnUrl);
18
-
19
- // esm.sh typically provides both default and named exports
20
- esbuild = mod.default ?? mod;
21
-
22
- // Verify we have the initialize function
23
- if (typeof esbuild?.initialize !== 'function') {
24
- console.error('esbuild-wasm module structure:', mod);
25
- throw new Error('Failed to load esbuild-wasm: initialize function not found');
26
- }
27
-
28
- return esbuild;
29
- }
30
-
31
- /**
32
- * How to handle npm package imports (bare imports like "react").
33
- *
34
- * - "cdn" (default): Rewrite to esm.sh CDN URLs using installed package versions.
35
- * Requires packages to be installed via the `install` command.
36
- * - "external": Mark as external, don't rewrite. The consumer must handle
37
- * module resolution (useful for SSR or custom bundling).
38
- * - "bundle": Attempt to bundle from node_modules. Rarely useful in browser
39
- * since node_modules typically doesn't exist in the virtual filesystem.
40
- */
41
- export type NpmImportsMode = "cdn" | "external" | "bundle";
42
-
43
- /**
44
- * Options for bundling
45
- */
46
- export interface BundleOptions {
47
- /**
48
- * The virtual filesystem to read source files from
49
- */
50
- fs: IFileSystem;
51
-
52
- /**
53
- * Entry point path (absolute path in the virtual filesystem)
54
- */
55
- entryPoint: string;
56
-
57
- /**
58
- * Module names to mark as external (won't be bundled).
59
- * These are in addition to bare imports when using npmImports: "external".
60
- */
61
- external?: string[];
62
-
63
- /**
64
- * How to handle npm package imports (bare imports like "react").
65
- *
66
- * - "cdn" (default): Rewrite to esm.sh CDN URLs using installed package versions
67
- * - "external": Mark as external, don't rewrite (consumer must handle)
68
- * - "bundle": Attempt to bundle from node_modules (rarely useful in browser)
69
- */
70
- npmImports?: NpmImportsMode;
71
-
72
- /**
73
- * Module IDs that should be resolved from the host's SharedModuleRegistry
74
- * instead of esm.sh CDN. The host must have registered these modules.
75
- *
76
- * Example: ['react', 'react-dom/client']
77
- *
78
- * When specified, imports of these modules will use the host's instances,
79
- * allowing dynamic components to share React context, hooks, etc.
80
- */
81
- sharedModules?: string[];
82
-
83
- /**
84
- * Output format: 'esm' (default), 'iife', or 'cjs'
85
- */
86
- format?: "esm" | "iife" | "cjs";
87
-
88
- /**
89
- * Enable minification
90
- * Default: false
91
- */
92
- minify?: boolean;
93
-
94
- /**
95
- * Enable source maps (inline)
96
- * Default: false
97
- */
98
- sourcemap?: boolean;
99
-
100
- /**
101
- * Global name for IIFE format
102
- */
103
- globalName?: string;
104
-
105
- /**
106
- * Target environment(s)
107
- * Default: ['es2020']
108
- */
109
- target?: string[];
110
- }
111
-
112
- /**
113
- * Result of bundling
114
- */
115
- export interface BundleResult {
116
- /**
117
- * The bundled JavaScript code
118
- */
119
- code: string;
120
-
121
- /**
122
- * Any warnings from esbuild
123
- */
124
- warnings: EsbuildTypes.Message[];
125
-
126
- /**
127
- * List of files that were included in the bundle
128
- */
129
- includedFiles: string[];
130
- }
131
-
132
- /**
133
- * esbuild-wasm version - MUST match the version in package.json dependencies
134
- */
135
- const ESBUILD_VERSION = "0.27.2";
136
-
137
- // Track initialization state
138
- let initialized = false;
139
- let initPromise: Promise<void> | null = null;
140
-
141
- /**
142
- * Get the esbuild-wasm binary URL based on the installed version
143
- */
144
- function getWasmUrl(): string {
145
- return `https://unpkg.com/esbuild-wasm@${ESBUILD_VERSION}/esbuild.wasm`;
146
- }
147
-
148
- /**
149
- * Check if the browser environment supports cross-origin isolation.
150
- * This is needed for SharedArrayBuffer which esbuild-wasm may use.
151
- */
152
- function checkCrossOriginIsolation(): void {
153
- if (typeof window === "undefined") return; // Not in browser
154
-
155
- // crossOriginIsolated is true when COOP/COEP headers are set correctly
156
- if (!window.crossOriginIsolated) {
157
- console.warn(
158
- "[sandlot] Cross-origin isolation is not enabled. " +
159
- "esbuild-wasm may have reduced performance or fail on some browsers.\n" +
160
- "To enable, add these headers to your dev server:\n" +
161
- " Cross-Origin-Embedder-Policy: require-corp\n" +
162
- " Cross-Origin-Opener-Policy: same-origin\n" +
163
- "In Vite, add a plugin to configureServer. See sandlot README for details."
164
- );
165
- }
166
- }
167
-
168
- /**
169
- * Initialize esbuild-wasm. Called automatically on first bundle.
170
- * Can be called explicitly to pre-warm.
171
- */
172
- export async function initBundler(): Promise<void> {
173
- if (initialized) return;
174
-
175
- if (initPromise) {
176
- await initPromise;
177
- return;
178
- }
179
-
180
- checkCrossOriginIsolation();
181
-
182
- initPromise = (async () => {
183
- const es = await getEsbuild();
184
- await es.initialize({
185
- wasmURL: getWasmUrl(),
186
- });
187
- })();
188
-
189
- await initPromise;
190
- initialized = true;
191
- }
192
-
193
- /**
194
- * Check if a path is a bare import (not relative or absolute)
195
- */
196
- function isBareImport(path: string): boolean {
197
- return !path.startsWith(".") && !path.startsWith("/");
198
- }
199
-
200
- /**
201
- * Get the appropriate loader based on file extension
202
- */
203
- function getLoader(path: string): EsbuildTypes.Loader {
204
- const ext = path.split(".").pop()?.toLowerCase();
205
- switch (ext) {
206
- case "ts":
207
- return "ts";
208
- case "tsx":
209
- return "tsx";
210
- case "jsx":
211
- return "jsx";
212
- case "js":
213
- case "mjs":
214
- return "js";
215
- case "json":
216
- return "json";
217
- case "css":
218
- return "css";
219
- case "txt":
220
- return "text";
221
- default:
222
- return "js";
223
- }
224
- }
225
-
226
- /**
227
- * Options for the VFS plugin
228
- */
229
- interface VfsPluginOptions {
230
- fs: IFileSystem;
231
- entryPoint: string;
232
- npmImports: NpmImportsMode;
233
- installedPackages: Record<string, string>;
234
- includedFiles: Set<string>;
235
- /** Module IDs to resolve from SharedModuleRegistry */
236
- sharedModuleIds: Set<string>;
237
- }
238
-
239
- /**
240
- * Check if an import path matches a shared module ID.
241
- * Handles both exact matches and subpath imports (e.g., 'react-dom/client').
242
- */
243
- function matchesSharedModule(importPath: string, sharedModuleIds: Set<string>): string | null {
244
- // Check exact match first
245
- if (sharedModuleIds.has(importPath)) {
246
- return importPath;
247
- }
248
-
249
- // Check if any shared module is a prefix (for subpath imports)
250
- // e.g., if 'react-dom' is registered, 'react-dom/client' should match
251
- for (const moduleId of sharedModuleIds) {
252
- if (importPath === moduleId || importPath.startsWith(moduleId + '/')) {
253
- return importPath;
254
- }
255
- }
256
-
257
- return null;
258
- }
259
-
260
- /**
261
- * Create an esbuild plugin that reads from a virtual filesystem
262
- */
263
- function createVfsPlugin(options: VfsPluginOptions): EsbuildTypes.Plugin {
264
- const {
265
- fs,
266
- entryPoint,
267
- npmImports,
268
- installedPackages,
269
- includedFiles,
270
- sharedModuleIds,
271
- } = options;
272
-
273
- return {
274
- name: "virtual-fs",
275
- setup(build) {
276
- // Resolve all imports
277
- build.onResolve({ filter: /.*/ }, async (args) => {
278
- // Handle the virtual entry point
279
- if (args.kind === "entry-point") {
280
- return { path: entryPoint, namespace: "vfs" };
281
- }
282
-
283
- // Handle bare imports
284
- if (isBareImport(args.path)) {
285
- // Check if this module should use the shared registry
286
- const sharedMatch = matchesSharedModule(args.path, sharedModuleIds);
287
- if (sharedMatch) {
288
- return {
289
- path: sharedMatch,
290
- namespace: "sandlot-shared"
291
- };
292
- }
293
-
294
- // Handle based on npmImports mode
295
- switch (npmImports) {
296
- case "cdn": {
297
- // Try to rewrite to esm.sh URL if package is installed
298
- const esmUrl = resolveToEsmUrl(args.path, installedPackages);
299
- if (esmUrl) {
300
- return { path: esmUrl, external: true };
301
- }
302
- // Fall back to external if not installed
303
- return { path: args.path, external: true };
304
- }
305
-
306
- case "external":
307
- // Mark as external, don't rewrite
308
- return { path: args.path, external: true };
309
-
310
- case "bundle": {
311
- // Try to resolve from VFS node_modules
312
- const resolved = fs.resolvePath(args.resolveDir, `node_modules/${args.path}`);
313
- const exists = await fs.exists(resolved);
314
- if (exists) {
315
- return { path: resolved, namespace: "vfs" };
316
- }
317
- // Fall back to external if not found
318
- return { path: args.path, external: true };
319
- }
320
- }
321
- }
322
-
323
- // Resolve relative/absolute paths
324
- const resolved = fs.resolvePath(args.resolveDir, args.path);
325
-
326
- // Try with extensions if no extension provided
327
- const extensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".json"];
328
- const hasExtension = extensions.some((ext) => resolved.endsWith(ext));
329
-
330
- if (hasExtension) {
331
- const exists = await fs.exists(resolved);
332
- if (exists) {
333
- return { path: resolved, namespace: "vfs" };
334
- }
335
- return { errors: [{ text: `File not found: ${resolved}` }] };
336
- }
337
-
338
- // Try adding extensions
339
- for (const ext of extensions) {
340
- const withExt = resolved + ext;
341
- if (await fs.exists(withExt)) {
342
- return { path: withExt, namespace: "vfs" };
343
- }
344
- }
345
-
346
- // Try index files
347
- for (const ext of extensions) {
348
- const indexPath = `${resolved}/index${ext}`;
349
- if (await fs.exists(indexPath)) {
350
- return { path: indexPath, namespace: "vfs" };
351
- }
352
- }
353
-
354
- return { errors: [{ text: `Cannot resolve: ${args.path} from ${args.resolveDir}` }] };
355
- });
356
-
357
- // Load shared modules from the registry
358
- build.onLoad({ filter: /.*/, namespace: "sandlot-shared" }, (args) => {
359
- // Generate ESM code that re-exports from the shared module registry
360
- const contents = `
361
- const __sandlot_mod__ = ${getSharedModuleRuntimeCode(args.path)};
362
- export default __sandlot_mod__.default ?? __sandlot_mod__;
363
- ${generateNamedExports(args.path)}
364
- `;
365
- return {
366
- contents: contents.trim(),
367
- loader: 'js'
368
- };
369
- });
370
-
371
- // Load files from VFS
372
- build.onLoad({ filter: /.*/, namespace: "vfs" }, async (args) => {
373
- try {
374
- const contents = await fs.readFile(args.path);
375
- includedFiles.add(args.path);
376
- return {
377
- contents,
378
- loader: getLoader(args.path),
379
- resolveDir: args.path.substring(0, args.path.lastIndexOf("/")),
380
- };
381
- } catch (err) {
382
- return {
383
- errors: [{ text: `Failed to read ${args.path}: ${err}` }],
384
- };
385
- }
386
- });
387
- },
388
- };
389
- }
390
-
391
- /**
392
- * Generate named export statements for shared modules.
393
- *
394
- * Uses dynamically discovered exports from the SharedModuleRegistry,
395
- * which are populated when registerSharedModules() is called.
396
- *
397
- * If the module wasn't registered (or has no enumerable exports),
398
- * returns a comment - named imports won't work but default import will.
399
- */
400
- function generateNamedExports(moduleId: string): string {
401
- const exports = getSharedModuleExports(moduleId);
402
-
403
- if (exports.length > 0) {
404
- return exports
405
- .map(name => `export const ${name} = __sandlot_mod__.${name};`)
406
- .join('\n');
407
- }
408
-
409
- // Module not registered or has no enumerable exports
410
- // Default import will still work: import foo from 'module'
411
- return `// No exports discovered for "${moduleId}" - use default import or call registerSharedModules() first`;
412
- }
413
-
414
- /**
415
- * Bundle TypeScript/JavaScript files from a virtual filesystem
416
- *
417
- * @example
418
- * ```ts
419
- * const fs = Filesystem.create({
420
- * initialFiles: {
421
- * "/src/index.ts": "export const hello = 'world';",
422
- * "/src/utils.ts": "export function add(a: number, b: number) { return a + b; }",
423
- * }
424
- * });
425
- *
426
- * const result = await bundle({
427
- * fs,
428
- * entryPoint: "/src/index.ts",
429
- * });
430
- *
431
- * console.log(result.code);
432
- * ```
433
- */
434
- export async function bundle(options: BundleOptions): Promise<BundleResult> {
435
- await initBundler();
436
-
437
- const {
438
- fs,
439
- entryPoint,
440
- external = [],
441
- npmImports = "cdn",
442
- sharedModules = [],
443
- format = "esm",
444
- minify = false,
445
- sourcemap = false,
446
- globalName,
447
- target = ["es2020"],
448
- } = options;
449
-
450
- // Normalize entry point
451
- const normalizedEntry = entryPoint.startsWith("/") ? entryPoint : `/${entryPoint}`;
452
-
453
- // Verify entry point exists
454
- if (!(await fs.exists(normalizedEntry))) {
455
- throw new Error(`Entry point not found: ${normalizedEntry}`);
456
- }
457
-
458
- // Get installed packages for ESM URL rewriting
459
- const manifest = await getPackageManifest(fs);
460
- const installedPackages = manifest.dependencies;
461
-
462
- // Create set of shared module IDs for fast lookup
463
- const sharedModuleIds = new Set(sharedModules);
464
-
465
- const includedFiles = new Set<string>();
466
- const plugin = createVfsPlugin({
467
- fs,
468
- entryPoint: normalizedEntry,
469
- npmImports,
470
- installedPackages,
471
- includedFiles,
472
- sharedModuleIds,
473
- });
474
-
475
- const es = await getEsbuild();
476
- const result = await es.build({
477
- entryPoints: [normalizedEntry],
478
- bundle: true,
479
- write: false,
480
- format,
481
- minify,
482
- sourcemap: sourcemap ? "inline" : false,
483
- globalName,
484
- target,
485
- external,
486
- plugins: [plugin],
487
- jsx: "automatic",
488
- });
489
-
490
- const code = result.outputFiles?.[0]?.text ?? "";
491
-
492
- return {
493
- code,
494
- warnings: result.warnings,
495
- includedFiles: Array.from(includedFiles),
496
- };
497
- }
498
-
499
- /**
500
- * Bundle and return a blob URL for dynamic import
501
- *
502
- * @example
503
- * ```ts
504
- * const url = await bundleToUrl({
505
- * fs,
506
- * entryPoint: "/src/index.ts",
507
- * });
508
- *
509
- * const module = await import(url);
510
- * console.log(module.hello); // 'world'
511
- *
512
- * // Clean up when done
513
- * URL.revokeObjectURL(url);
514
- * ```
515
- */
516
- export async function bundleToUrl(options: BundleOptions): Promise<string> {
517
- const result = await bundle(options);
518
- const blob = new Blob([result.code], { type: "application/javascript" });
519
- return URL.createObjectURL(blob);
520
- }
521
-
522
- /**
523
- * Bundle and immediately import the module
524
- *
525
- * @example
526
- * ```ts
527
- * const module = await bundleAndImport<{ hello: string }>({
528
- * fs,
529
- * entryPoint: "/src/index.ts",
530
- * });
531
- *
532
- * console.log(module.hello); // 'world'
533
- * ```
534
- */
535
- export async function bundleAndImport<T = unknown>(options: BundleOptions): Promise<T> {
536
- const url = await bundleToUrl(options);
537
- try {
538
- return await import(/* @vite-ignore */ url);
539
- } finally {
540
- URL.revokeObjectURL(url);
541
- }
542
- }