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.
- 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 +2692 -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 +31 -132
- 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 +1405 -2049
- 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 +2646 -0
- package/dist/node/preset.d.ts +62 -0
- package/dist/node/preset.d.ts.map +1 -0
- package/dist/types.d.ts +525 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +27 -8
- 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 +526 -43
- package/src/commands/types.ts +82 -146
- 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 +621 -0
- package/src/core/sandlot.ts +77 -0
- package/src/core/shared-module-registry.ts +138 -0
- package/src/core/typechecker.ts +607 -0
- package/src/index.ts +104 -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 +668 -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 -148
- 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 -1976
- 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 -98
- 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 -542
- 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 -396
- package/src/shared-modules.ts +0 -280
- package/src/shared-resources.ts +0 -166
- package/src/ts-libs.ts +0 -320
- 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
|
-
}
|