sandlot 0.1.4 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/bundler.d.ts +68 -0
- package/dist/browser/bundler.d.ts.map +1 -0
- package/dist/browser/executor.d.ts +46 -0
- package/dist/browser/executor.d.ts.map +1 -0
- package/dist/browser/index.d.ts +9 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +2690 -0
- package/dist/browser/preset.d.ts +63 -0
- package/dist/browser/preset.d.ts.map +1 -0
- package/dist/commands/index.d.ts +20 -11
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/types.d.ts +37 -130
- package/dist/commands/types.d.ts.map +1 -1
- package/dist/core/bundler-utils.d.ts +142 -0
- package/dist/core/bundler-utils.d.ts.map +1 -0
- package/dist/core/esm-types-resolver.d.ts +125 -0
- package/dist/core/esm-types-resolver.d.ts.map +1 -0
- package/dist/core/executor.d.ts +35 -0
- package/dist/core/executor.d.ts.map +1 -0
- package/dist/{fs.d.ts → core/fs.d.ts} +27 -29
- package/dist/core/fs.d.ts.map +1 -0
- package/dist/core/sandbox.d.ts +30 -0
- package/dist/core/sandbox.d.ts.map +1 -0
- package/dist/core/sandlot.d.ts +30 -0
- package/dist/core/sandlot.d.ts.map +1 -0
- package/dist/core/shared-module-registry.d.ts +46 -0
- package/dist/core/shared-module-registry.d.ts.map +1 -0
- package/dist/core/typechecker.d.ts +60 -0
- package/dist/core/typechecker.d.ts.map +1 -0
- package/dist/index.d.ts +11 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1398 -2010
- package/dist/node/bundler.d.ts +48 -0
- package/dist/node/bundler.d.ts.map +1 -0
- package/dist/node/executor.d.ts +48 -0
- package/dist/node/executor.d.ts.map +1 -0
- package/dist/node/index.d.ts +9 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +2644 -0
- package/dist/node/preset.d.ts +62 -0
- package/dist/node/preset.d.ts.map +1 -0
- package/dist/types.d.ts +528 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +16 -6
- package/src/browser/bundler.ts +294 -0
- package/src/browser/executor.ts +71 -0
- package/src/browser/index.ts +57 -0
- package/src/browser/preset.ts +179 -0
- package/src/commands/index.ts +498 -37
- package/src/commands/types.ts +117 -145
- package/src/core/bundler-utils.ts +630 -0
- package/src/core/esm-types-resolver.ts +432 -0
- package/src/core/executor.ts +161 -0
- package/src/{fs.ts → core/fs.ts} +59 -37
- package/src/core/sandbox.ts +624 -0
- package/src/core/sandlot.ts +77 -0
- package/src/core/shared-module-registry.ts +138 -0
- package/src/core/typechecker.ts +609 -0
- package/src/index.ts +106 -139
- package/src/node/bundler.ts +194 -0
- package/src/node/executor.ts +87 -0
- package/src/node/index.ts +39 -0
- package/src/node/preset.ts +178 -0
- package/src/types.ts +672 -0
- package/README.md +0 -243
- package/dist/build-emitter.d.ts +0 -47
- package/dist/build-emitter.d.ts.map +0 -1
- package/dist/builder.d.ts +0 -370
- package/dist/builder.d.ts.map +0 -1
- package/dist/bundler.d.ts +0 -152
- package/dist/bundler.d.ts.map +0 -1
- package/dist/commands/compile.d.ts +0 -13
- package/dist/commands/compile.d.ts.map +0 -1
- package/dist/commands/packages.d.ts +0 -17
- package/dist/commands/packages.d.ts.map +0 -1
- package/dist/commands/run.d.ts +0 -40
- package/dist/commands/run.d.ts.map +0 -1
- package/dist/commands.d.ts +0 -179
- package/dist/commands.d.ts.map +0 -1
- package/dist/fs.d.ts.map +0 -1
- package/dist/internal.d.ts +0 -79
- package/dist/internal.d.ts.map +0 -1
- package/dist/internal.js +0 -1942
- package/dist/loader.d.ts +0 -164
- package/dist/loader.d.ts.map +0 -1
- package/dist/packages.d.ts +0 -199
- package/dist/packages.d.ts.map +0 -1
- package/dist/runner.d.ts +0 -314
- package/dist/runner.d.ts.map +0 -1
- package/dist/sandbox-manager.d.ts +0 -261
- package/dist/sandbox-manager.d.ts.map +0 -1
- package/dist/sandbox.d.ts +0 -267
- package/dist/sandbox.d.ts.map +0 -1
- package/dist/shared-modules.d.ts +0 -148
- package/dist/shared-modules.d.ts.map +0 -1
- package/dist/shared-resources.d.ts +0 -102
- package/dist/shared-resources.d.ts.map +0 -1
- package/dist/ts-libs.d.ts +0 -85
- package/dist/ts-libs.d.ts.map +0 -1
- package/dist/typechecker.d.ts +0 -127
- package/dist/typechecker.d.ts.map +0 -1
- package/src/build-emitter.ts +0 -64
- package/src/builder.ts +0 -498
- package/src/bundler.ts +0 -575
- package/src/commands/compile.ts +0 -236
- package/src/commands/packages.ts +0 -154
- package/src/commands/run.ts +0 -245
- package/src/internal.ts +0 -119
- package/src/loader.ts +0 -229
- package/src/packages.ts +0 -936
- package/src/sandbox.ts +0 -398
- package/src/shared-modules.ts +0 -280
- package/src/shared-resources.ts +0 -166
- package/src/ts-libs.ts +0 -218
- package/src/typechecker.ts +0 -635
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types Resolver - Fetches TypeScript type definitions for npm packages.
|
|
3
|
+
*
|
|
4
|
+
* This is a platform-independent implementation that works anywhere with fetch().
|
|
5
|
+
* (Browser, Node 18+, Bun, Deno, Cloudflare Workers)
|
|
6
|
+
*
|
|
7
|
+
* Design principles:
|
|
8
|
+
* 1. Single responsibility: resolve package → types. No VFS writing.
|
|
9
|
+
* 2. CDN-agnostic interface with esm.sh implementation
|
|
10
|
+
* 3. Transparent @types fallback (caller doesn't need to know)
|
|
11
|
+
* 4. Subpaths resolved on-demand, not pre-fetched
|
|
12
|
+
* 5. Caching is external/injectable (platform-specific cache implementations)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ITypesResolver } from "../types";
|
|
16
|
+
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// Types
|
|
19
|
+
// =============================================================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolved type definitions for a package.
|
|
23
|
+
*/
|
|
24
|
+
export interface ResolvedTypes {
|
|
25
|
+
/**
|
|
26
|
+
* The package name (may differ from request if @types fallback was used)
|
|
27
|
+
*/
|
|
28
|
+
packageName: string;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The resolved version
|
|
32
|
+
*/
|
|
33
|
+
version: string;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Map of relative file paths to content.
|
|
37
|
+
* Paths are relative to the package root (e.g., "index.d.ts", "utils.d.ts")
|
|
38
|
+
*/
|
|
39
|
+
files: Record<string, string>;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether types came from @types/* package
|
|
43
|
+
*/
|
|
44
|
+
fromTypesPackage: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Cache interface for type definitions.
|
|
49
|
+
* Implementations can be in-memory, IndexedDB, KV store, filesystem, etc.
|
|
50
|
+
*/
|
|
51
|
+
export interface ITypesCache {
|
|
52
|
+
get(key: string): Promise<ResolvedTypes | null>;
|
|
53
|
+
set(key: string, value: ResolvedTypes): Promise<void>;
|
|
54
|
+
has(key: string): Promise<boolean>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Simple in-memory cache implementation.
|
|
59
|
+
* Works on all platforms.
|
|
60
|
+
*/
|
|
61
|
+
export class InMemoryTypesCache implements ITypesCache {
|
|
62
|
+
private cache = new Map<string, ResolvedTypes>();
|
|
63
|
+
|
|
64
|
+
async get(key: string): Promise<ResolvedTypes | null> {
|
|
65
|
+
return this.cache.get(key) ?? null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async set(key: string, value: ResolvedTypes): Promise<void> {
|
|
69
|
+
this.cache.set(key, value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async has(key: string): Promise<boolean> {
|
|
73
|
+
return this.cache.has(key);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
clear(): void {
|
|
77
|
+
this.cache.clear();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// =============================================================================
|
|
82
|
+
// EsmTypesResolver
|
|
83
|
+
// =============================================================================
|
|
84
|
+
|
|
85
|
+
export interface EsmTypesResolverOptions {
|
|
86
|
+
/**
|
|
87
|
+
* Base URL for esm.sh (default: "https://esm.sh")
|
|
88
|
+
*/
|
|
89
|
+
baseUrl?: string;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* External cache. If not provided, no caching is done.
|
|
93
|
+
* This allows sharing cache across resolver instances.
|
|
94
|
+
*/
|
|
95
|
+
cache?: ITypesCache;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Whether to try @types/* packages as fallback when main package
|
|
99
|
+
* doesn't have bundled types. Default: true
|
|
100
|
+
*/
|
|
101
|
+
tryTypesPackages?: boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Types resolver using esm.sh CDN.
|
|
106
|
+
*
|
|
107
|
+
* Platform-independent - works anywhere with fetch().
|
|
108
|
+
*
|
|
109
|
+
* Resolution strategy:
|
|
110
|
+
* 1. Fetch package from esm.sh, check X-TypeScript-Types header
|
|
111
|
+
* 2. If no types, try @types/{package} as fallback
|
|
112
|
+
* 3. Fetch .d.ts files and any /// <reference> dependencies
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```ts
|
|
116
|
+
* const resolver = new EsmTypesResolver();
|
|
117
|
+
*
|
|
118
|
+
* // Resolve types for a package
|
|
119
|
+
* const types = await resolver.resolve("lodash", "4.17.21");
|
|
120
|
+
* // types.files: { "index.d.ts": "...", "common.d.ts": "..." }
|
|
121
|
+
*
|
|
122
|
+
* // Resolve subpath types
|
|
123
|
+
* const clientTypes = await resolver.resolve("react-dom/client", "18.2.0");
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export class EsmTypesResolver implements ITypesResolver {
|
|
127
|
+
private baseUrl: string;
|
|
128
|
+
private cache: ITypesCache | null;
|
|
129
|
+
private tryTypesPackages: boolean;
|
|
130
|
+
|
|
131
|
+
constructor(options: EsmTypesResolverOptions = {}) {
|
|
132
|
+
this.baseUrl = options.baseUrl ?? "https://esm.sh";
|
|
133
|
+
this.cache = options.cache ?? null;
|
|
134
|
+
this.tryTypesPackages = options.tryTypesPackages ?? true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Resolve type definitions for a package.
|
|
139
|
+
*
|
|
140
|
+
* @param specifier - Package specifier with optional subpath (e.g., "react", "react-dom/client")
|
|
141
|
+
* @param version - Optional version constraint
|
|
142
|
+
* @returns Map of file paths to content, or empty object if no types found
|
|
143
|
+
*/
|
|
144
|
+
async resolveTypes(
|
|
145
|
+
specifier: string,
|
|
146
|
+
version?: string
|
|
147
|
+
): Promise<Record<string, string>> {
|
|
148
|
+
const resolved = await this.resolve(specifier, version);
|
|
149
|
+
if (!resolved) {
|
|
150
|
+
return {};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Prefix paths with package location for VFS
|
|
154
|
+
const result: Record<string, string> = {};
|
|
155
|
+
const pkgPath = `/node_modules/${resolved.packageName}`;
|
|
156
|
+
|
|
157
|
+
for (const [relativePath, content] of Object.entries(resolved.files)) {
|
|
158
|
+
result[`${pkgPath}/${relativePath}`] = content;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Resolve with full metadata (useful for advanced use cases).
|
|
166
|
+
*/
|
|
167
|
+
async resolve(
|
|
168
|
+
specifier: string,
|
|
169
|
+
version?: string
|
|
170
|
+
): Promise<ResolvedTypes | null> {
|
|
171
|
+
const { packageName, subpath } = parseSpecifier(specifier);
|
|
172
|
+
const cacheKey = makeCacheKey(packageName, subpath, version);
|
|
173
|
+
|
|
174
|
+
// Check cache first
|
|
175
|
+
if (this.cache) {
|
|
176
|
+
const cached = await this.cache.get(cacheKey);
|
|
177
|
+
if (cached) {
|
|
178
|
+
return cached;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Try to resolve types
|
|
183
|
+
let result = await this.tryResolve(packageName, subpath, version);
|
|
184
|
+
|
|
185
|
+
// Fallback to @types package if enabled and no types found
|
|
186
|
+
if (!result && this.tryTypesPackages && !packageName.startsWith("@types/")) {
|
|
187
|
+
const typesPackageName = toTypesPackageName(packageName);
|
|
188
|
+
result = await this.tryResolve(typesPackageName, subpath, version);
|
|
189
|
+
if (result) {
|
|
190
|
+
result.fromTypesPackage = true;
|
|
191
|
+
// Keep original package name for the result so caller knows what they asked for
|
|
192
|
+
result.packageName = packageName;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Cache the result
|
|
197
|
+
if (result && this.cache) {
|
|
198
|
+
await this.cache.set(cacheKey, result);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Attempt to resolve types for a specific package.
|
|
206
|
+
*/
|
|
207
|
+
private async tryResolve(
|
|
208
|
+
packageName: string,
|
|
209
|
+
subpath: string | undefined,
|
|
210
|
+
version: string | undefined
|
|
211
|
+
): Promise<ResolvedTypes | null> {
|
|
212
|
+
try {
|
|
213
|
+
// Build URL
|
|
214
|
+
const versionSuffix = version ? `@${version}` : "";
|
|
215
|
+
const pathSuffix = subpath ? `/${subpath}` : "";
|
|
216
|
+
const url = `${this.baseUrl}/${packageName}${versionSuffix}${pathSuffix}`;
|
|
217
|
+
|
|
218
|
+
// Fetch to get headers (types URL, resolved version)
|
|
219
|
+
const response = await fetch(url, { method: "HEAD" });
|
|
220
|
+
if (!response.ok) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Extract resolved version from URL or headers
|
|
225
|
+
const resolvedVersion = this.extractVersion(response, packageName, version);
|
|
226
|
+
|
|
227
|
+
// Get types URL from header
|
|
228
|
+
const typesHeader = response.headers.get("X-TypeScript-Types");
|
|
229
|
+
if (!typesHeader) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const typesUrl = new URL(typesHeader, response.url).href;
|
|
234
|
+
|
|
235
|
+
// Fetch the types
|
|
236
|
+
const files = await this.fetchTypesRecursively(typesUrl, subpath);
|
|
237
|
+
|
|
238
|
+
if (Object.keys(files).length === 0) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
packageName,
|
|
244
|
+
version: resolvedVersion,
|
|
245
|
+
files,
|
|
246
|
+
fromTypesPackage: packageName.startsWith("@types/"),
|
|
247
|
+
};
|
|
248
|
+
} catch {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Fetch a .d.ts file and any files it references.
|
|
255
|
+
*/
|
|
256
|
+
private async fetchTypesRecursively(
|
|
257
|
+
entryUrl: string,
|
|
258
|
+
subpath: string | undefined,
|
|
259
|
+
visited = new Set<string>()
|
|
260
|
+
): Promise<Record<string, string>> {
|
|
261
|
+
if (visited.has(entryUrl)) {
|
|
262
|
+
return {};
|
|
263
|
+
}
|
|
264
|
+
visited.add(entryUrl);
|
|
265
|
+
|
|
266
|
+
const response = await fetch(entryUrl);
|
|
267
|
+
if (!response.ok) {
|
|
268
|
+
return {};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const content = await response.text();
|
|
272
|
+
const files: Record<string, string> = {};
|
|
273
|
+
|
|
274
|
+
// Determine the file path
|
|
275
|
+
// For main entry: "index.d.ts"
|
|
276
|
+
// For subpath "client": "client.d.ts" (or "client/index.d.ts")
|
|
277
|
+
const fileName = subpath ? `${subpath}.d.ts` : "index.d.ts";
|
|
278
|
+
files[fileName] = content;
|
|
279
|
+
|
|
280
|
+
// If this is a subpath, also create the directory form
|
|
281
|
+
// e.g., react-dom/client can be imported, needs client/index.d.ts too
|
|
282
|
+
if (subpath) {
|
|
283
|
+
files[`${subpath}/index.d.ts`] = content;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Parse and fetch referenced files
|
|
287
|
+
const refs = parseReferences(content);
|
|
288
|
+
|
|
289
|
+
for (const ref of refs.paths) {
|
|
290
|
+
const refUrl = new URL(ref, entryUrl).href;
|
|
291
|
+
const refFiles = await this.fetchTypesRecursively(refUrl, undefined, visited);
|
|
292
|
+
|
|
293
|
+
// Add referenced files with their relative paths
|
|
294
|
+
for (const [refPath, refContent] of Object.entries(refFiles)) {
|
|
295
|
+
// Compute relative path from the reference
|
|
296
|
+
const normalizedRef = ref.replace(/^\.\//, "");
|
|
297
|
+
if (refPath === "index.d.ts") {
|
|
298
|
+
files[normalizedRef] = refContent;
|
|
299
|
+
} else {
|
|
300
|
+
const dir = normalizedRef.replace(/\.d\.ts$/, "");
|
|
301
|
+
files[`${dir}/${refPath}`] = refContent;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return files;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Extract the resolved version from the response.
|
|
311
|
+
*/
|
|
312
|
+
private extractVersion(
|
|
313
|
+
response: Response,
|
|
314
|
+
packageName: string,
|
|
315
|
+
requestedVersion: string | undefined
|
|
316
|
+
): string {
|
|
317
|
+
// Try x-esm-id header first (most reliable)
|
|
318
|
+
const esmId = response.headers.get("x-esm-id");
|
|
319
|
+
if (esmId) {
|
|
320
|
+
const match = esmId.match(new RegExp(`${escapeRegex(packageName)}@([^/]+)`));
|
|
321
|
+
if (match?.[1]) {
|
|
322
|
+
return match[1];
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Try extracting from final URL
|
|
327
|
+
const urlMatch = response.url.match(new RegExp(`${escapeRegex(packageName)}@([^/]+)`));
|
|
328
|
+
if (urlMatch?.[1]) {
|
|
329
|
+
return urlMatch[1];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Fall back to requested version or "latest"
|
|
333
|
+
return requestedVersion ?? "latest";
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// =============================================================================
|
|
338
|
+
// Utilities
|
|
339
|
+
// =============================================================================
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Parse a package specifier into package name and optional subpath.
|
|
343
|
+
*
|
|
344
|
+
* @example
|
|
345
|
+
* parseSpecifier("react") // { packageName: "react", subpath: undefined }
|
|
346
|
+
* parseSpecifier("react-dom/client") // { packageName: "react-dom", subpath: "client" }
|
|
347
|
+
* parseSpecifier("@tanstack/react-query") // { packageName: "@tanstack/react-query", subpath: undefined }
|
|
348
|
+
* parseSpecifier("@tanstack/react-query/devtools") // { packageName: "@tanstack/react-query", subpath: "devtools" }
|
|
349
|
+
*/
|
|
350
|
+
function parseSpecifier(specifier: string): {
|
|
351
|
+
packageName: string;
|
|
352
|
+
subpath: string | undefined;
|
|
353
|
+
} {
|
|
354
|
+
if (specifier.startsWith("@")) {
|
|
355
|
+
// Scoped package: @scope/name or @scope/name/subpath
|
|
356
|
+
const parts = specifier.split("/");
|
|
357
|
+
if (parts.length >= 2) {
|
|
358
|
+
const packageName = `${parts[0]}/${parts[1]}`;
|
|
359
|
+
const subpath = parts.length > 2 ? parts.slice(2).join("/") : undefined;
|
|
360
|
+
return { packageName, subpath };
|
|
361
|
+
}
|
|
362
|
+
return { packageName: specifier, subpath: undefined };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Regular package: name or name/subpath
|
|
366
|
+
const slashIndex = specifier.indexOf("/");
|
|
367
|
+
if (slashIndex === -1) {
|
|
368
|
+
return { packageName: specifier, subpath: undefined };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
packageName: specifier.slice(0, slashIndex),
|
|
373
|
+
subpath: specifier.slice(slashIndex + 1),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Convert a package name to its @types/* equivalent.
|
|
379
|
+
*
|
|
380
|
+
* @example
|
|
381
|
+
* toTypesPackageName("lodash") // "@types/lodash"
|
|
382
|
+
* toTypesPackageName("@tanstack/react-query") // "@types/tanstack__react-query"
|
|
383
|
+
*/
|
|
384
|
+
function toTypesPackageName(packageName: string): string {
|
|
385
|
+
if (packageName.startsWith("@")) {
|
|
386
|
+
// Scoped: @scope/name -> @types/scope__name
|
|
387
|
+
return "@types/" + packageName.slice(1).replace("/", "__");
|
|
388
|
+
}
|
|
389
|
+
return `@types/${packageName}`;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Parse /// <reference> directives from a .d.ts file.
|
|
394
|
+
*/
|
|
395
|
+
function parseReferences(content: string): { paths: string[]; types: string[] } {
|
|
396
|
+
const paths: string[] = [];
|
|
397
|
+
const types: string[] = [];
|
|
398
|
+
|
|
399
|
+
// /// <reference path="..." />
|
|
400
|
+
const pathRegex = /\/\/\/\s*<reference\s+path="([^"]+)"\s*\/>/g;
|
|
401
|
+
let match;
|
|
402
|
+
while ((match = pathRegex.exec(content)) !== null) {
|
|
403
|
+
if (match[1]) paths.push(match[1]);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// /// <reference types="..." />
|
|
407
|
+
const typesRegex = /\/\/\/\s*<reference\s+types="([^"]+)"\s*\/>/g;
|
|
408
|
+
while ((match = typesRegex.exec(content)) !== null) {
|
|
409
|
+
if (match[1]) types.push(match[1]);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return { paths, types };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Create a cache key for a package resolution.
|
|
417
|
+
*/
|
|
418
|
+
function makeCacheKey(
|
|
419
|
+
packageName: string,
|
|
420
|
+
subpath: string | undefined,
|
|
421
|
+
version: string | undefined
|
|
422
|
+
): string {
|
|
423
|
+
const base = version ? `${packageName}@${version}` : packageName;
|
|
424
|
+
return subpath ? `${base}/${subpath}` : base;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Escape special regex characters.
|
|
429
|
+
*/
|
|
430
|
+
function escapeRegex(str: string): string {
|
|
431
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
432
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base executor implementation shared by browser and node executors.
|
|
3
|
+
*
|
|
4
|
+
* This module provides the core execution logic with console capture,
|
|
5
|
+
* timeout handling, and export invocation. Platform-specific executors
|
|
6
|
+
* only need to provide a function to load code as a module.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { IExecutor, ExecuteOptions, ExecuteResult } from "../types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Function that loads JavaScript code as a module.
|
|
13
|
+
* Platform-specific implementations convert code to an importable URL.
|
|
14
|
+
*/
|
|
15
|
+
export type ModuleLoader = (code: string) => Promise<Record<string, unknown>>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Options for creating a basic executor.
|
|
19
|
+
*/
|
|
20
|
+
export interface BasicExecutorOptions {
|
|
21
|
+
/**
|
|
22
|
+
* Default timeout in milliseconds.
|
|
23
|
+
* @default 30000
|
|
24
|
+
*/
|
|
25
|
+
defaultTimeout?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create an executor using the provided module loader.
|
|
30
|
+
*
|
|
31
|
+
* This is the shared implementation used by both browser and node executors.
|
|
32
|
+
* The only platform-specific part is how code is loaded as a module.
|
|
33
|
+
*
|
|
34
|
+
* @param loadModule - Function that loads code as a module
|
|
35
|
+
* @param options - Executor options
|
|
36
|
+
* @returns An executor instance
|
|
37
|
+
*/
|
|
38
|
+
export function createBasicExecutor(
|
|
39
|
+
loadModule: ModuleLoader,
|
|
40
|
+
options: BasicExecutorOptions = {}
|
|
41
|
+
): IExecutor {
|
|
42
|
+
const defaultTimeout = options.defaultTimeout ?? 30000;
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
async execute(code: string, execOptions: ExecuteOptions = {}): Promise<ExecuteResult> {
|
|
46
|
+
const {
|
|
47
|
+
entryExport = "main",
|
|
48
|
+
context = {},
|
|
49
|
+
timeout = defaultTimeout,
|
|
50
|
+
} = execOptions;
|
|
51
|
+
|
|
52
|
+
const startTime = performance.now();
|
|
53
|
+
const logs: string[] = [];
|
|
54
|
+
|
|
55
|
+
// Capture console output
|
|
56
|
+
const originalConsole = {
|
|
57
|
+
log: console.log,
|
|
58
|
+
warn: console.warn,
|
|
59
|
+
error: console.error,
|
|
60
|
+
info: console.info,
|
|
61
|
+
debug: console.debug,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const formatArgs = (...args: unknown[]) =>
|
|
65
|
+
args
|
|
66
|
+
.map((v) => (typeof v === "object" ? JSON.stringify(v) : String(v)))
|
|
67
|
+
.join(" ");
|
|
68
|
+
|
|
69
|
+
const captureLog = (...args: unknown[]) => {
|
|
70
|
+
logs.push(formatArgs(...args));
|
|
71
|
+
originalConsole.log.apply(console, args);
|
|
72
|
+
};
|
|
73
|
+
const captureWarn = (...args: unknown[]) => {
|
|
74
|
+
logs.push(`[warn] ${formatArgs(...args)}`);
|
|
75
|
+
originalConsole.warn.apply(console, args);
|
|
76
|
+
};
|
|
77
|
+
const captureError = (...args: unknown[]) => {
|
|
78
|
+
logs.push(`[error] ${formatArgs(...args)}`);
|
|
79
|
+
originalConsole.error.apply(console, args);
|
|
80
|
+
};
|
|
81
|
+
const captureInfo = (...args: unknown[]) => {
|
|
82
|
+
logs.push(`[info] ${formatArgs(...args)}`);
|
|
83
|
+
originalConsole.info.apply(console, args);
|
|
84
|
+
};
|
|
85
|
+
const captureDebug = (...args: unknown[]) => {
|
|
86
|
+
logs.push(`[debug] ${formatArgs(...args)}`);
|
|
87
|
+
originalConsole.debug.apply(console, args);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const restoreConsole = () => {
|
|
91
|
+
console.log = originalConsole.log;
|
|
92
|
+
console.warn = originalConsole.warn;
|
|
93
|
+
console.error = originalConsole.error;
|
|
94
|
+
console.info = originalConsole.info;
|
|
95
|
+
console.debug = originalConsole.debug;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Install console interceptors
|
|
99
|
+
console.log = captureLog;
|
|
100
|
+
console.warn = captureWarn;
|
|
101
|
+
console.error = captureError;
|
|
102
|
+
console.info = captureInfo;
|
|
103
|
+
console.debug = captureDebug;
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
// Load the module using the platform-specific loader
|
|
107
|
+
const module = await loadModule(code);
|
|
108
|
+
|
|
109
|
+
// Execute the appropriate export
|
|
110
|
+
let returnValue: unknown;
|
|
111
|
+
|
|
112
|
+
const executeExport = async () => {
|
|
113
|
+
if (entryExport === "main" && typeof module.main === "function") {
|
|
114
|
+
// Call main(context)
|
|
115
|
+
returnValue = await module.main(context);
|
|
116
|
+
} else if (entryExport === "default" && typeof module.default === "function") {
|
|
117
|
+
// Call default export (no args)
|
|
118
|
+
returnValue = await module.default();
|
|
119
|
+
} else if (entryExport === "default" && module.default !== undefined) {
|
|
120
|
+
// Default export is a value, not a function
|
|
121
|
+
returnValue = module.default;
|
|
122
|
+
}
|
|
123
|
+
// If neither export exists, top-level code already ran on import
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Execute with optional timeout
|
|
127
|
+
if (timeout > 0) {
|
|
128
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
129
|
+
setTimeout(
|
|
130
|
+
() => reject(new Error(`Execution timed out after ${timeout}ms`)),
|
|
131
|
+
timeout
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
await Promise.race([executeExport(), timeoutPromise]);
|
|
135
|
+
} else {
|
|
136
|
+
await executeExport();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const executionTimeMs = performance.now() - startTime;
|
|
140
|
+
restoreConsole();
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
success: true,
|
|
144
|
+
logs,
|
|
145
|
+
returnValue,
|
|
146
|
+
executionTimeMs,
|
|
147
|
+
};
|
|
148
|
+
} catch (err) {
|
|
149
|
+
const executionTimeMs = performance.now() - startTime;
|
|
150
|
+
restoreConsole();
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
success: false,
|
|
154
|
+
logs,
|
|
155
|
+
error: err instanceof Error ? err.message : String(err),
|
|
156
|
+
executionTimeMs,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|