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
package/src/packages.ts DELETED
@@ -1,936 +0,0 @@
1
- /**
2
- * Package management for sandbox environments.
3
- *
4
- * Provides npm-like package installation using esm.sh CDN:
5
- * - Fetches TypeScript type definitions for editor/typecheck support
6
- * - Stores installed versions in package.json
7
- * - Resolves bare imports to CDN URLs at bundle time
8
- * - Supports @types/* packages (fetches package content as types)
9
- * - Supports subpath exports (react-dom/client, react/jsx-runtime)
10
- *
11
- * @example
12
- * ```ts
13
- * // Install a package
14
- * const result = await installPackage(fs, "react");
15
- * // result: { name: "react", version: "18.2.0", typesInstalled: true }
16
- *
17
- * // Get installed packages
18
- * const manifest = await getPackageManifest(fs);
19
- * // manifest.dependencies: { "react": "18.2.0" }
20
- *
21
- * // Resolve import to CDN URL
22
- * const url = resolveToEsmUrl("react", "18.2.0");
23
- * // url: "https://esm.sh/react@18.2.0"
24
- * ```
25
- */
26
-
27
- import type { IFileSystem } from "just-bash/browser";
28
-
29
- /**
30
- * CDN base URL for esm.sh
31
- */
32
- const ESM_CDN_BASE = "https://esm.sh";
33
-
34
- /**
35
- * Known subpaths that should be auto-fetched for common packages.
36
- * These are subpath exports that are commonly used and need separate type definitions.
37
- */
38
- const KNOWN_SUBPATHS: Record<string, string[]> = {
39
- react: ["jsx-runtime", "jsx-dev-runtime"],
40
- "react-dom": ["client", "server"],
41
- };
42
-
43
- /**
44
- * Package manifest (subset of package.json)
45
- */
46
- export interface PackageManifest {
47
- dependencies: Record<string, string>;
48
- }
49
-
50
- /**
51
- * Cache for storing fetched type definitions.
52
- * Used to avoid redundant network fetches when multiple sandboxes
53
- * install the same packages.
54
- */
55
- export interface TypesCache {
56
- /**
57
- * Get cached type definitions for a package version.
58
- * Returns null if not cached.
59
- */
60
- get(name: string, version: string): Map<string, string> | null;
61
-
62
- /**
63
- * Store type definitions in the cache.
64
- */
65
- set(name: string, version: string, types: Map<string, string>): void;
66
-
67
- /**
68
- * Check if a package version is cached.
69
- */
70
- has(name: string, version: string): boolean;
71
-
72
- /**
73
- * Remove a package version from the cache.
74
- */
75
- delete(name: string, version: string): boolean;
76
-
77
- /**
78
- * Clear all cached entries.
79
- */
80
- clear(): void;
81
- }
82
-
83
- /**
84
- * In-memory implementation of TypesCache.
85
- * Suitable for sharing across multiple sandboxes within a session.
86
- */
87
- export class InMemoryTypesCache implements TypesCache {
88
- private cache = new Map<string, Map<string, string>>();
89
-
90
- private key(name: string, version: string): string {
91
- return `${name}@${version}`;
92
- }
93
-
94
- get(name: string, version: string): Map<string, string> | null {
95
- return this.cache.get(this.key(name, version)) ?? null;
96
- }
97
-
98
- set(name: string, version: string, types: Map<string, string>): void {
99
- this.cache.set(this.key(name, version), types);
100
- }
101
-
102
- has(name: string, version: string): boolean {
103
- return this.cache.has(this.key(name, version));
104
- }
105
-
106
- delete(name: string, version: string): boolean {
107
- return this.cache.delete(this.key(name, version));
108
- }
109
-
110
- clear(): void {
111
- this.cache.clear();
112
- }
113
-
114
- /**
115
- * Get the number of cached packages (for diagnostics).
116
- */
117
- get size(): number {
118
- return this.cache.size;
119
- }
120
- }
121
-
122
- /**
123
- * Result of installing a package
124
- */
125
- export interface InstallResult {
126
- /** Package name */
127
- name: string;
128
- /** Resolved version */
129
- version: string;
130
- /** Whether type definitions were installed */
131
- typesInstalled: boolean;
132
- /** Number of type definition files installed */
133
- typeFilesCount: number;
134
- /** Error message if types failed (but package still usable) */
135
- typesError?: string;
136
- /** Whether types were loaded from cache */
137
- fromCache?: boolean;
138
- }
139
-
140
- /**
141
- * Options for installing a package
142
- */
143
- export interface InstallOptions {
144
- /**
145
- * Cache to use for storing/retrieving type definitions.
146
- * When provided, avoids redundant network fetches for packages
147
- * that have already been installed in other sandboxes.
148
- */
149
- cache?: TypesCache;
150
- }
151
-
152
- /**
153
- * Package info from esm.sh headers
154
- */
155
- interface EsmPackageInfo {
156
- /** Resolved version (e.g., "18.2.0") */
157
- version: string;
158
- /** URL to TypeScript types, if available */
159
- typesUrl?: string;
160
- }
161
-
162
- /**
163
- * Path to package.json in the virtual filesystem
164
- */
165
- const PACKAGE_JSON_PATH = "/package.json";
166
-
167
- /**
168
- * Parse package specifier into name and version
169
- * Examples:
170
- * "react" -> { name: "react", version: undefined }
171
- * "react@18" -> { name: "react", version: "18" }
172
- * "@tanstack/react-query@5" -> { name: "@tanstack/react-query", version: "5" }
173
- */
174
- export function parsePackageSpec(spec: string): { name: string; version?: string } {
175
- // Handle scoped packages: @scope/name@version
176
- if (spec.startsWith("@")) {
177
- const slashIndex = spec.indexOf("/");
178
- if (slashIndex === -1) {
179
- return { name: spec };
180
- }
181
- const afterSlash = spec.slice(slashIndex + 1);
182
- const atIndex = afterSlash.indexOf("@");
183
- if (atIndex === -1) {
184
- return { name: spec };
185
- }
186
- return {
187
- name: spec.slice(0, slashIndex + 1 + atIndex),
188
- version: afterSlash.slice(atIndex + 1),
189
- };
190
- }
191
-
192
- // Regular packages: name@version
193
- const atIndex = spec.indexOf("@");
194
- if (atIndex === -1) {
195
- return { name: spec };
196
- }
197
- return {
198
- name: spec.slice(0, atIndex),
199
- version: spec.slice(atIndex + 1),
200
- };
201
- }
202
-
203
- /**
204
- * Check if a package is a @types/* package
205
- */
206
- function isTypesPackage(name: string): boolean {
207
- return name.startsWith("@types/");
208
- }
209
-
210
- /**
211
- * Extract version from esm.sh URL
212
- * Handles various URL formats including scoped packages
213
- */
214
- function extractVersionFromUrl(url: string, packageName: string): string | null {
215
- // Try exact package name match first
216
- const exactRegex = new RegExp(`${escapeRegExp(packageName)}@([^/]+)`);
217
- const exactMatch = url.match(exactRegex);
218
- if (exactMatch?.[1]) {
219
- return exactMatch[1];
220
- }
221
-
222
- // For scoped packages, try matching after the scope
223
- if (packageName.startsWith("@")) {
224
- const scopedParts = packageName.split("/");
225
- if (scopedParts.length === 2 && scopedParts[1]) {
226
- // Try matching just the package part after scope
227
- const partialRegex = new RegExp(`${escapeRegExp(scopedParts[1])}@([^/]+)`);
228
- const partialMatch = url.match(partialRegex);
229
- if (partialMatch?.[1]) {
230
- return partialMatch[1];
231
- }
232
- }
233
- }
234
-
235
- // Generic fallback: look for any @version pattern at the end of a path segment
236
- const genericMatch = url.match(/@(\d+\.\d+\.\d+[^/]*)/);
237
- if (genericMatch?.[1]) {
238
- return genericMatch[1];
239
- }
240
-
241
- return null;
242
- }
243
-
244
- /**
245
- * Fetch the latest version of a package from npm registry
246
- */
247
- async function fetchVersionFromNpm(name: string): Promise<string> {
248
- const registryUrl = `https://registry.npmjs.org/${name}/latest`;
249
- const response = await fetch(registryUrl);
250
- if (!response.ok) {
251
- throw new Error(`Failed to fetch version from npm: ${response.status}`);
252
- }
253
- const data = await response.json();
254
- return data.version;
255
- }
256
-
257
- /**
258
- * Extract version from esm.sh response headers
259
- * esm.sh includes version info in various headers
260
- */
261
- function extractVersionFromHeaders(headers: Headers, packageName: string): string | null {
262
- // Try x-esm-id header (contains the resolved module path with version)
263
- const esmId = headers.get("x-esm-id");
264
- if (esmId) {
265
- const version = extractVersionFromUrl(esmId, packageName);
266
- if (version) return version;
267
- }
268
-
269
- // Try extracting from X-TypeScript-Types header
270
- // e.g., "/@types/react@18.3.1/index.d.ts" - but note this is @types version
271
- // We can use it as a hint for the main package version
272
- const typesHeader = headers.get("X-TypeScript-Types");
273
- if (typesHeader) {
274
- // Extract version - for react, types header might have /v18.3.1/ or similar
275
- const versionMatch = typesHeader.match(/@(\d+\.\d+\.\d+[^/]*)/);
276
- if (versionMatch?.[1]) {
277
- return versionMatch[1];
278
- }
279
- }
280
-
281
- return null;
282
- }
283
-
284
- /**
285
- * Fetch package info from esm.sh (version and types URL)
286
- */
287
- async function fetchPackageInfo(name: string, version?: string, subpath?: string): Promise<EsmPackageInfo> {
288
- // Build URL with optional subpath
289
- let url = version ? `${ESM_CDN_BASE}/${name}@${version}` : `${ESM_CDN_BASE}/${name}`;
290
- if (subpath) {
291
- url += `/${subpath}`;
292
- }
293
-
294
- const response = await fetch(url, { method: "HEAD" });
295
- if (!response.ok) {
296
- throw new Error(`Package not found: ${name}${version ? `@${version}` : ""}${subpath ? `/${subpath}` : ""}`);
297
- }
298
-
299
- // Extract version from response URL or headers
300
- const resolvedUrl = response.url;
301
- let resolvedVersion = extractVersionFromUrl(resolvedUrl, name);
302
-
303
- // Try headers if URL extraction failed
304
- if (!resolvedVersion) {
305
- resolvedVersion = extractVersionFromHeaders(response.headers, name);
306
- }
307
-
308
- // Fall back to provided version if it's specific
309
- if (!resolvedVersion && version && version !== "latest") {
310
- resolvedVersion = version;
311
- }
312
-
313
- // Last resort: query npm registry for latest version
314
- if (!resolvedVersion) {
315
- try {
316
- resolvedVersion = await fetchVersionFromNpm(name);
317
- } catch (err) {
318
- console.warn(`Could not resolve version for ${name}:`, err);
319
- resolvedVersion = "latest"; // Absolute last resort
320
- }
321
- }
322
-
323
- // Get TypeScript types URL from header
324
- const typesUrl = response.headers.get("X-TypeScript-Types") ?? undefined;
325
-
326
- return {
327
- version: resolvedVersion,
328
- typesUrl: typesUrl ? new URL(typesUrl, resolvedUrl).href : undefined,
329
- };
330
- }
331
-
332
- /**
333
- * Escape special regex characters
334
- */
335
- function escapeRegExp(string: string): string {
336
- return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
337
- }
338
-
339
- /**
340
- * Fetch type definitions from esm.sh
341
- * Returns a map of file paths to contents
342
- */
343
- async function fetchTypeDefinitions(
344
- typesUrl: string,
345
- packageName: string
346
- ): Promise<Map<string, string>> {
347
- const types = new Map<string, string>();
348
-
349
- try {
350
- const response = await fetch(typesUrl);
351
- if (!response.ok) {
352
- throw new Error(`Failed to fetch types: ${response.status}`);
353
- }
354
-
355
- const content = await response.text();
356
-
357
- // Store as index.d.ts for the package
358
- const typePath = `/node_modules/${packageName}/index.d.ts`;
359
- types.set(typePath, content);
360
-
361
- // Parse and fetch referenced types (similar to ts-libs.ts)
362
- const refs = parseTypeReferences(content);
363
- await fetchReferencedTypes(refs, typesUrl, packageName, types);
364
- } catch (err) {
365
- console.warn(`Failed to fetch types for ${packageName}:`, err);
366
- throw err;
367
- }
368
-
369
- return types;
370
- }
371
-
372
- /**
373
- * Parse `/// <reference path="..." />` and `/// <reference types="..." />` from .d.ts
374
- */
375
- function parseTypeReferences(content: string): { paths: string[]; types: string[] } {
376
- const paths: string[] = [];
377
- const types: string[] = [];
378
-
379
- // Match /// <reference path="..." />
380
- const pathRegex = /\/\/\/\s*<reference\s+path="([^"]+)"\s*\/>/g;
381
- let match;
382
- while ((match = pathRegex.exec(content)) !== null) {
383
- if (match[1]) paths.push(match[1]);
384
- }
385
-
386
- // Match /// <reference types="..." />
387
- const typesRegex = /\/\/\/\s*<reference\s+types="([^"]+)"\s*\/>/g;
388
- while ((match = typesRegex.exec(content)) !== null) {
389
- if (match[1]) types.push(match[1]);
390
- }
391
-
392
- return { paths, types };
393
- }
394
-
395
- /**
396
- * Fetch referenced type files recursively
397
- */
398
- async function fetchReferencedTypes(
399
- refs: { paths: string[]; types: string[] },
400
- baseUrl: string,
401
- packageName: string,
402
- collected: Map<string, string>,
403
- visited = new Set<string>()
404
- ): Promise<void> {
405
- // Handle path references (relative files)
406
- for (const pathRef of refs.paths) {
407
- const refUrl = new URL(pathRef, baseUrl).href;
408
- if (visited.has(refUrl)) continue;
409
- visited.add(refUrl);
410
-
411
- try {
412
- const response = await fetch(refUrl);
413
- if (!response.ok) continue;
414
-
415
- const content = await response.text();
416
-
417
- // Determine the file path in node_modules
418
- const fileName = pathRef.split("/").pop() ?? "types.d.ts";
419
- const typePath = `/node_modules/${packageName}/${fileName}`;
420
- collected.set(typePath, content);
421
-
422
- // Recursively fetch references
423
- const nestedRefs = parseTypeReferences(content);
424
- await fetchReferencedTypes(nestedRefs, refUrl, packageName, collected, visited);
425
- } catch {
426
- // Skip failed references
427
- }
428
- }
429
-
430
- // Handle types references (other packages)
431
- // These would require installing those packages - skip for now
432
- // The user can install them explicitly if needed
433
- }
434
-
435
- /**
436
- * Fetch type definitions for a subpath export (e.g., react-dom/client)
437
- * Returns a map of file paths to contents
438
- */
439
- async function fetchSubpathTypes(
440
- packageName: string,
441
- subpath: string,
442
- version: string
443
- ): Promise<Map<string, string>> {
444
- const types = new Map<string, string>();
445
-
446
- try {
447
- // Fetch the subpath to get its types URL
448
- const info = await fetchPackageInfo(packageName, version, subpath);
449
-
450
- if (!info.typesUrl) {
451
- // No types available for this subpath - that's okay, not all subpaths have types
452
- return types;
453
- }
454
-
455
- const response = await fetch(info.typesUrl);
456
- if (!response.ok) {
457
- return types;
458
- }
459
-
460
- const content = await response.text();
461
-
462
- // Store at the correct subpath location
463
- // e.g., /node_modules/react-dom/client.d.ts or /node_modules/react-dom/client/index.d.ts
464
- const typePath = `/node_modules/${packageName}/${subpath}.d.ts`;
465
- types.set(typePath, content);
466
-
467
- // Also create a directory version for imports like "react-dom/client"
468
- // TypeScript might look for /node_modules/react-dom/client/index.d.ts
469
- const indexTypePath = `/node_modules/${packageName}/${subpath}/index.d.ts`;
470
- types.set(indexTypePath, content);
471
-
472
- // Parse and fetch referenced types
473
- const refs = parseTypeReferences(content);
474
- await fetchReferencedTypes(refs, info.typesUrl, packageName, types);
475
- } catch (err) {
476
- // Subpath type fetching is best-effort
477
- console.warn(`Failed to fetch types for ${packageName}/${subpath}:`, err);
478
- }
479
-
480
- return types;
481
- }
482
-
483
- /**
484
- * Fetch types for a @types/* package
485
- * These packages ARE the type definitions, so we fetch the package content directly
486
- */
487
- async function fetchTypesPackageContent(
488
- name: string,
489
- version?: string
490
- ): Promise<{ version: string; types: Map<string, string> }> {
491
- // e.g., @types/react -> fetch https://esm.sh/@types/react/index.d.ts
492
- const url = version
493
- ? `${ESM_CDN_BASE}/${name}@${version}`
494
- : `${ESM_CDN_BASE}/${name}`;
495
-
496
- // First, get the resolved version via HEAD request
497
- const headResponse = await fetch(url, { method: "HEAD" });
498
- if (!headResponse.ok) {
499
- throw new Error(`Package not found: ${name}${version ? `@${version}` : ""}`);
500
- }
501
-
502
- const resolvedVersion = extractVersionFromUrl(headResponse.url, name) ?? version ?? "latest";
503
-
504
- // Now fetch the actual index.d.ts content
505
- const indexUrl = `${ESM_CDN_BASE}/${name}@${resolvedVersion}/index.d.ts`;
506
- const response = await fetch(indexUrl);
507
-
508
- if (!response.ok) {
509
- throw new Error(`Failed to fetch types from ${name}: ${response.status}`);
510
- }
511
-
512
- const content = await response.text();
513
- const types = new Map<string, string>();
514
-
515
- // Store as index.d.ts for the @types package
516
- const typePath = `/node_modules/${name}/index.d.ts`;
517
- types.set(typePath, content);
518
-
519
- // Parse and fetch referenced types
520
- const refs = parseTypeReferences(content);
521
-
522
- // For path references, we need to fetch them from the same package
523
- for (const pathRef of refs.paths) {
524
- try {
525
- const refUrl = new URL(pathRef, indexUrl).href;
526
- const refResponse = await fetch(refUrl);
527
- if (refResponse.ok) {
528
- const refContent = await refResponse.text();
529
- // Determine the file path - preserve the relative structure
530
- const fileName = pathRef.startsWith("./") ? pathRef.slice(2) : pathRef;
531
- const refTypePath = `/node_modules/${name}/${fileName}`;
532
- types.set(refTypePath, refContent);
533
- }
534
- } catch {
535
- // Skip failed references
536
- }
537
- }
538
-
539
- return { version: resolvedVersion, types };
540
- }
541
-
542
- /**
543
- * Read the package manifest from the filesystem
544
- */
545
- export async function getPackageManifest(fs: IFileSystem): Promise<PackageManifest> {
546
- try {
547
- if (await fs.exists(PACKAGE_JSON_PATH)) {
548
- const content = await fs.readFile(PACKAGE_JSON_PATH);
549
- const parsed = JSON.parse(content);
550
- return {
551
- dependencies: parsed.dependencies ?? {},
552
- };
553
- }
554
- } catch {
555
- // Invalid JSON or read error - return empty manifest
556
- }
557
- return { dependencies: {} };
558
- }
559
-
560
- /**
561
- * Write the package manifest to the filesystem
562
- */
563
- async function savePackageManifest(
564
- fs: IFileSystem,
565
- manifest: PackageManifest
566
- ): Promise<void> {
567
- let existing: Record<string, unknown> = {};
568
-
569
- try {
570
- if (await fs.exists(PACKAGE_JSON_PATH)) {
571
- const content = await fs.readFile(PACKAGE_JSON_PATH);
572
- existing = JSON.parse(content);
573
- }
574
- } catch {
575
- // Start fresh if invalid
576
- }
577
-
578
- const updated = {
579
- ...existing,
580
- dependencies: manifest.dependencies,
581
- };
582
-
583
- await fs.writeFile(PACKAGE_JSON_PATH, JSON.stringify(updated, null, 2));
584
- }
585
-
586
- /**
587
- * Install a package from npm via esm.sh
588
- *
589
- * This fetches type definitions and stores them in the virtual filesystem,
590
- * then updates package.json with the installed version.
591
- *
592
- * Special handling:
593
- * - @types/* packages: Fetches package content directly as types
594
- * - Known packages (react, react-dom): Auto-fetches subpath types
595
- *
596
- * @param fs - The virtual filesystem
597
- * @param packageSpec - Package name with optional version (e.g., "react", "lodash@4")
598
- * @returns Install result with version and type info
599
- *
600
- * @example
601
- * ```ts
602
- * // Install latest version
603
- * await installPackage(fs, "react");
604
- *
605
- * // Install specific version
606
- * await installPackage(fs, "lodash@4.17.21");
607
- *
608
- * // Install scoped package
609
- * await installPackage(fs, "@tanstack/react-query@5");
610
- *
611
- * // Install @types package
612
- * await installPackage(fs, "@types/lodash");
613
- * ```
614
- */
615
- export async function installPackage(
616
- fs: IFileSystem,
617
- packageSpec: string,
618
- options?: InstallOptions
619
- ): Promise<InstallResult> {
620
- const { name, version } = parsePackageSpec(packageSpec);
621
- const { cache } = options ?? {};
622
-
623
- // Handle @types/* packages specially - they ARE the type definitions
624
- if (isTypesPackage(name)) {
625
- return installTypesPackage(fs, name, version, cache);
626
- }
627
-
628
- // Fetch package info from esm.sh (need version for cache key)
629
- const info = await fetchPackageInfo(name, version);
630
-
631
- // Ensure node_modules directory for this package exists
632
- const packageDir = `/node_modules/${name}`;
633
- await ensureDir(fs, packageDir);
634
-
635
- // Create a minimal package.json for TypeScript resolution
636
- const packageJsonPath = `${packageDir}/package.json`;
637
- const packageJson = {
638
- name,
639
- version: info.version,
640
- types: "./index.d.ts",
641
- main: "./index.js",
642
- };
643
- await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
644
-
645
- // Try to get types from cache first
646
- let typeFiles: Map<string, string> | null = null;
647
- let fromCache = false;
648
-
649
- if (cache) {
650
- typeFiles = cache.get(name, info.version);
651
- if (typeFiles) {
652
- fromCache = true;
653
- }
654
- }
655
-
656
- // If not cached, fetch from network
657
- let typesError: string | undefined;
658
- if (!typeFiles) {
659
- typeFiles = new Map<string, string>();
660
-
661
- if (info.typesUrl) {
662
- try {
663
- const mainTypes = await fetchTypeDefinitions(info.typesUrl, name);
664
- for (const [path, content] of mainTypes) {
665
- typeFiles.set(path, content);
666
- }
667
- } catch (err) {
668
- typesError = err instanceof Error ? err.message : String(err);
669
- }
670
- } else {
671
- typesError = "No TypeScript types available from esm.sh";
672
- }
673
-
674
- // Fetch types for known subpaths (e.g., react-dom/client, react/jsx-runtime)
675
- const knownSubpaths = KNOWN_SUBPATHS[name];
676
- if (knownSubpaths && knownSubpaths.length > 0) {
677
- const subpathResults = await Promise.allSettled(
678
- knownSubpaths.map((subpath) => fetchSubpathTypes(name, subpath, info.version))
679
- );
680
-
681
- for (const result of subpathResults) {
682
- if (result.status === "fulfilled") {
683
- for (const [path, content] of result.value) {
684
- typeFiles.set(path, content);
685
- }
686
- }
687
- }
688
- }
689
-
690
- // Store in cache for other sandboxes
691
- if (cache && typeFiles.size > 0) {
692
- cache.set(name, info.version, typeFiles);
693
- }
694
- }
695
-
696
- // Write type files to VFS
697
- for (const [path, content] of typeFiles) {
698
- const dir = path.substring(0, path.lastIndexOf("/"));
699
- await ensureDir(fs, dir);
700
- await fs.writeFile(path, content);
701
- }
702
-
703
- // Update package.json
704
- const manifest = await getPackageManifest(fs);
705
- manifest.dependencies[name] = info.version;
706
- await savePackageManifest(fs, manifest);
707
-
708
- return {
709
- name,
710
- version: info.version,
711
- typesInstalled: typeFiles.size > 0,
712
- typeFilesCount: typeFiles.size,
713
- typesError,
714
- fromCache,
715
- };
716
- }
717
-
718
- /**
719
- * Install a @types/* package
720
- * These packages ARE the type definitions, so we fetch the package content directly
721
- */
722
- async function installTypesPackage(
723
- fs: IFileSystem,
724
- name: string,
725
- version?: string,
726
- cache?: TypesCache
727
- ): Promise<InstallResult> {
728
- // For @types packages, we need to resolve the version first to check the cache
729
- // We'll do a HEAD request to get the resolved version
730
- const url = version
731
- ? `${ESM_CDN_BASE}/${name}@${version}`
732
- : `${ESM_CDN_BASE}/${name}`;
733
-
734
- const headResponse = await fetch(url, { method: "HEAD" });
735
- if (!headResponse.ok) {
736
- throw new Error(`Package not found: ${name}${version ? `@${version}` : ""}`);
737
- }
738
-
739
- const resolvedVersion = extractVersionFromUrl(headResponse.url, name) ?? version ?? "latest";
740
-
741
- // Check cache first
742
- let types: Map<string, string> | null = null;
743
- let fromCache = false;
744
-
745
- if (cache) {
746
- types = cache.get(name, resolvedVersion);
747
- if (types) {
748
- fromCache = true;
749
- }
750
- }
751
-
752
- // If not cached, fetch from network
753
- if (!types) {
754
- const result = await fetchTypesPackageContent(name, version);
755
- types = result.types;
756
-
757
- // Store in cache
758
- if (cache && types.size > 0) {
759
- cache.set(name, resolvedVersion, types);
760
- }
761
- }
762
-
763
- // Ensure node_modules directory for this package exists
764
- const packageDir = `/node_modules/${name}`;
765
- await ensureDir(fs, packageDir);
766
-
767
- // Create a minimal package.json
768
- const packageJsonPath = `${packageDir}/package.json`;
769
- const packageJson = {
770
- name,
771
- version: resolvedVersion,
772
- types: "./index.d.ts",
773
- };
774
- await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
775
-
776
- // Write all type files
777
- for (const [path, content] of types) {
778
- const dir = path.substring(0, path.lastIndexOf("/"));
779
- await ensureDir(fs, dir);
780
- await fs.writeFile(path, content);
781
- }
782
-
783
- // Update package.json manifest
784
- const manifest = await getPackageManifest(fs);
785
- manifest.dependencies[name] = resolvedVersion;
786
- await savePackageManifest(fs, manifest);
787
-
788
- return {
789
- name,
790
- version: resolvedVersion,
791
- typesInstalled: types.size > 0,
792
- typeFilesCount: types.size,
793
- fromCache,
794
- };
795
- }
796
-
797
- /**
798
- * Ensure a directory exists, creating parent directories as needed
799
- */
800
- async function ensureDir(fs: IFileSystem, path: string): Promise<void> {
801
- if (path === "/" || path === "") return;
802
-
803
- if (await fs.exists(path)) {
804
- const stat = await fs.stat(path);
805
- if (stat.isDirectory) return;
806
- }
807
-
808
- // Ensure parent exists first
809
- const parent = path.substring(0, path.lastIndexOf("/")) || "/";
810
- await ensureDir(fs, parent);
811
-
812
- // Create this directory
813
- await fs.mkdir(path);
814
- }
815
-
816
- /**
817
- * Uninstall a package
818
- *
819
- * Removes the package from package.json and deletes type definitions.
820
- */
821
- export async function uninstallPackage(
822
- fs: IFileSystem,
823
- packageName: string
824
- ): Promise<boolean> {
825
- const manifest = await getPackageManifest(fs);
826
-
827
- if (!(packageName in manifest.dependencies)) {
828
- return false;
829
- }
830
-
831
- // Remove from manifest
832
- delete manifest.dependencies[packageName];
833
- await savePackageManifest(fs, manifest);
834
-
835
- // Remove type definitions
836
- const typesPath = `/node_modules/${packageName}`;
837
- if (await fs.exists(typesPath)) {
838
- await removePath(fs, typesPath);
839
- }
840
-
841
- return true;
842
- }
843
-
844
- /**
845
- * Recursively remove a directory or file
846
- */
847
- async function removePath(fs: IFileSystem, path: string): Promise<void> {
848
- if (!(await fs.exists(path))) return;
849
- await fs.rm(path, { recursive: true, force: true });
850
- }
851
-
852
- /**
853
- * Resolve a bare import to an esm.sh URL
854
- *
855
- * @param importPath - The import path (e.g., "react", "lodash/debounce")
856
- * @param installedPackages - Map of package name to version
857
- * @returns The CDN URL, or null if package not installed
858
- *
859
- * @example
860
- * ```ts
861
- * const packages = { "react": "18.2.0", "lodash-es": "4.17.21" };
862
- *
863
- * resolveToEsmUrl("react", packages);
864
- * // "https://esm.sh/react@18.2.0"
865
- *
866
- * resolveToEsmUrl("lodash-es/debounce", packages);
867
- * // "https://esm.sh/lodash-es@4.17.21/debounce"
868
- *
869
- * resolveToEsmUrl("unknown", packages);
870
- * // null
871
- * ```
872
- */
873
- export function resolveToEsmUrl(
874
- importPath: string,
875
- installedPackages: Record<string, string>
876
- ): string | null {
877
- // Parse the import path to get package name and subpath
878
- const { packageName, subpath } = parseImportPath(importPath);
879
-
880
- const version = installedPackages[packageName];
881
- if (!version) {
882
- return null;
883
- }
884
-
885
- const baseUrl = `${ESM_CDN_BASE}/${packageName}@${version}`;
886
- return subpath ? `${baseUrl}/${subpath}` : baseUrl;
887
- }
888
-
889
- /**
890
- * Parse an import path into package name and subpath
891
- *
892
- * @example
893
- * parseImportPath("react") // { packageName: "react", subpath: undefined }
894
- * parseImportPath("lodash/debounce") // { packageName: "lodash", subpath: "debounce" }
895
- * parseImportPath("@tanstack/react-query") // { packageName: "@tanstack/react-query", subpath: undefined }
896
- * parseImportPath("@tanstack/react-query/devtools") // { packageName: "@tanstack/react-query", subpath: "devtools" }
897
- */
898
- export function parseImportPath(importPath: string): {
899
- packageName: string;
900
- subpath?: string;
901
- } {
902
- // Handle scoped packages
903
- if (importPath.startsWith("@")) {
904
- const parts = importPath.split("/");
905
- if (parts.length >= 2) {
906
- const packageName = `${parts[0]}/${parts[1]}`;
907
- const subpath = parts.slice(2).join("/") || undefined;
908
- return { packageName, subpath };
909
- }
910
- return { packageName: importPath };
911
- }
912
-
913
- // Regular packages
914
- const slashIndex = importPath.indexOf("/");
915
- if (slashIndex === -1) {
916
- return { packageName: importPath };
917
- }
918
-
919
- return {
920
- packageName: importPath.slice(0, slashIndex),
921
- subpath: importPath.slice(slashIndex + 1),
922
- };
923
- }
924
-
925
- /**
926
- * List all installed packages
927
- */
928
- export async function listPackages(
929
- fs: IFileSystem
930
- ): Promise<Array<{ name: string; version: string }>> {
931
- const manifest = await getPackageManifest(fs);
932
- return Object.entries(manifest.dependencies).map(([name, version]) => ({
933
- name,
934
- version,
935
- }));
936
- }