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
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typechecker - TypeScript type checking.
|
|
3
|
+
*
|
|
4
|
+
* Uses TypeScript's compiler API
|
|
5
|
+
* Fetches TypeScript lib files (lib.dom.d.ts, etc.) from CDN and caches them.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import ts from "typescript";
|
|
9
|
+
import type { Filesystem } from "./fs";
|
|
10
|
+
import type {
|
|
11
|
+
ITypechecker,
|
|
12
|
+
TypecheckOptions,
|
|
13
|
+
TypecheckResult,
|
|
14
|
+
Diagnostic,
|
|
15
|
+
} from "../types";
|
|
16
|
+
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// Configuration
|
|
19
|
+
// =============================================================================
|
|
20
|
+
|
|
21
|
+
/** TypeScript version to fetch libs for - should match package.json */
|
|
22
|
+
const TS_VERSION = "5.9.3";
|
|
23
|
+
|
|
24
|
+
/** CDN base URL for TypeScript lib files */
|
|
25
|
+
const DEFAULT_CDN_BASE = `https://cdn.jsdelivr.net/npm/typescript@${TS_VERSION}/lib`;
|
|
26
|
+
|
|
27
|
+
/** Default libs for environment */
|
|
28
|
+
const DEFAULT_LIBS = ["es2020", "dom", "dom.iterable"];
|
|
29
|
+
|
|
30
|
+
/** Virtual path where lib files are "located" for TypeScript */
|
|
31
|
+
const LIB_PATH_PREFIX = "/node_modules/typescript/lib/";
|
|
32
|
+
|
|
33
|
+
// =============================================================================
|
|
34
|
+
// Types
|
|
35
|
+
// =============================================================================
|
|
36
|
+
|
|
37
|
+
export interface TypecheckerOptions {
|
|
38
|
+
/**
|
|
39
|
+
* TypeScript lib names to include (e.g., "dom", "es2020").
|
|
40
|
+
* If not provided, uses sensible defaults: ["es2020", "dom", "dom.iterable"]
|
|
41
|
+
*/
|
|
42
|
+
libs?: string[];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Base URL to fetch TypeScript lib files from.
|
|
46
|
+
* Defaults to jsDelivr CDN.
|
|
47
|
+
*/
|
|
48
|
+
libsBaseUrl?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// =============================================================================
|
|
52
|
+
// Lib File Fetching
|
|
53
|
+
// =============================================================================
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Parse `/// <reference lib="..." />` directives from a lib file.
|
|
57
|
+
*/
|
|
58
|
+
function parseLibReferences(content: string): string[] {
|
|
59
|
+
const refs: string[] = [];
|
|
60
|
+
const regex = /\/\/\/\s*<reference\s+lib="([^"]+)"\s*\/>/g;
|
|
61
|
+
let match;
|
|
62
|
+
|
|
63
|
+
while ((match = regex.exec(content)) !== null) {
|
|
64
|
+
if (match[1]) {
|
|
65
|
+
refs.push(match[1]);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return refs;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Convert a lib name to its filename.
|
|
74
|
+
* e.g., "es2020" -> "lib.es2020.d.ts"
|
|
75
|
+
*/
|
|
76
|
+
function libNameToFileName(name: string): string {
|
|
77
|
+
return `lib.${name}.d.ts`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Fetch a single lib file from CDN.
|
|
82
|
+
*/
|
|
83
|
+
async function fetchLibFile(name: string, baseUrl: string): Promise<string> {
|
|
84
|
+
const fileName = libNameToFileName(name);
|
|
85
|
+
const url = `${baseUrl}/${fileName}`;
|
|
86
|
+
|
|
87
|
+
const response = await fetch(url);
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return response.text();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Fetch all lib files, following reference directives.
|
|
97
|
+
*/
|
|
98
|
+
async function fetchAllLibs(
|
|
99
|
+
libs: string[],
|
|
100
|
+
baseUrl: string
|
|
101
|
+
): Promise<Map<string, string>> {
|
|
102
|
+
const result = new Map<string, string>();
|
|
103
|
+
const pending = new Set<string>(libs);
|
|
104
|
+
const fetched = new Set<string>();
|
|
105
|
+
|
|
106
|
+
while (pending.size > 0) {
|
|
107
|
+
const batch = Array.from(pending);
|
|
108
|
+
pending.clear();
|
|
109
|
+
|
|
110
|
+
const results = await Promise.all(
|
|
111
|
+
batch.map(async (name) => {
|
|
112
|
+
if (fetched.has(name)) {
|
|
113
|
+
return { name, content: null };
|
|
114
|
+
}
|
|
115
|
+
fetched.add(name);
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const content = await fetchLibFile(name, baseUrl);
|
|
119
|
+
return { name, content };
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.warn(`[typechecker] Failed to fetch lib.${name}.d.ts:`, err);
|
|
122
|
+
return { name, content: null };
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
for (const { name, content } of results) {
|
|
128
|
+
if (content === null) continue;
|
|
129
|
+
|
|
130
|
+
result.set(name, content);
|
|
131
|
+
|
|
132
|
+
// Parse references and queue unfetched ones
|
|
133
|
+
const refs = parseLibReferences(content);
|
|
134
|
+
for (const ref of refs) {
|
|
135
|
+
if (!fetched.has(ref) && !pending.has(ref)) {
|
|
136
|
+
pending.add(ref);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// =============================================================================
|
|
146
|
+
// Compiler Host
|
|
147
|
+
// =============================================================================
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Normalize a path to absolute form.
|
|
151
|
+
*/
|
|
152
|
+
function normalizePath(path: string): string {
|
|
153
|
+
if (!path.startsWith("/")) {
|
|
154
|
+
return "/" + path;
|
|
155
|
+
}
|
|
156
|
+
return path;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get lib content from the lib cache.
|
|
161
|
+
* Handles various path formats TypeScript might request.
|
|
162
|
+
*/
|
|
163
|
+
function getLibContent(
|
|
164
|
+
fileName: string,
|
|
165
|
+
libFiles: Map<string, string>
|
|
166
|
+
): string | undefined {
|
|
167
|
+
// Extract lib name from path: "lib.dom.d.ts" or "/node_modules/typescript/lib/lib.dom.d.ts"
|
|
168
|
+
const match = fileName.match(/lib\.([^/]+)\.d\.ts$/);
|
|
169
|
+
if (match?.[1]) {
|
|
170
|
+
return libFiles.get(match[1]);
|
|
171
|
+
}
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Create a TypeScript compiler host that reads from our filesystem.
|
|
177
|
+
*/
|
|
178
|
+
function createCompilerHost(
|
|
179
|
+
fs: Filesystem,
|
|
180
|
+
libFiles: Map<string, string>,
|
|
181
|
+
options: ts.CompilerOptions
|
|
182
|
+
): ts.CompilerHost {
|
|
183
|
+
return {
|
|
184
|
+
getSourceFile(
|
|
185
|
+
fileName: string,
|
|
186
|
+
languageVersion: ts.ScriptTarget,
|
|
187
|
+
onError?: (message: string) => void
|
|
188
|
+
): ts.SourceFile | undefined {
|
|
189
|
+
// Try filesystem first (user files + node_modules with package types)
|
|
190
|
+
const normalizedPath = normalizePath(fileName);
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
if (fs.exists(normalizedPath)) {
|
|
194
|
+
const stat = fs.stat(normalizedPath);
|
|
195
|
+
if (stat.isFile) {
|
|
196
|
+
const content = fs.readFile(normalizedPath);
|
|
197
|
+
return ts.createSourceFile(normalizedPath, content, languageVersion, true);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
// Not found in filesystem, continue to lib files
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Try without leading slash
|
|
205
|
+
try {
|
|
206
|
+
if (fs.exists(fileName)) {
|
|
207
|
+
const stat = fs.stat(fileName);
|
|
208
|
+
if (stat.isFile) {
|
|
209
|
+
const content = fs.readFile(fileName);
|
|
210
|
+
return ts.createSourceFile(fileName, content, languageVersion, true);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
// Not found, continue
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Try lib files
|
|
218
|
+
const libContent = getLibContent(fileName, libFiles);
|
|
219
|
+
if (libContent !== undefined) {
|
|
220
|
+
return ts.createSourceFile(fileName, libContent, languageVersion, true);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (onError) {
|
|
224
|
+
onError(`File not found: ${fileName}`);
|
|
225
|
+
}
|
|
226
|
+
return undefined;
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
getDefaultLibFileName(opts: ts.CompilerOptions): string {
|
|
230
|
+
return LIB_PATH_PREFIX + ts.getDefaultLibFileName(opts);
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
writeFile(): void {
|
|
234
|
+
// No-op: we don't emit files
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
getCurrentDirectory(): string {
|
|
238
|
+
return "/";
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
getCanonicalFileName(fileName: string): string {
|
|
242
|
+
return fileName;
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
useCaseSensitiveFileNames(): boolean {
|
|
246
|
+
return true;
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
getNewLine(): string {
|
|
250
|
+
return "\n";
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
fileExists(fileName: string): boolean {
|
|
254
|
+
const normalizedPath = normalizePath(fileName);
|
|
255
|
+
|
|
256
|
+
// Check filesystem
|
|
257
|
+
try {
|
|
258
|
+
if (fs.exists(normalizedPath)) {
|
|
259
|
+
return fs.stat(normalizedPath).isFile;
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
// Not found
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Check lib files
|
|
266
|
+
return getLibContent(fileName, libFiles) !== undefined;
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
readFile(fileName: string): string | undefined {
|
|
270
|
+
const normalizedPath = normalizePath(fileName);
|
|
271
|
+
|
|
272
|
+
// Try filesystem
|
|
273
|
+
try {
|
|
274
|
+
if (fs.exists(normalizedPath)) {
|
|
275
|
+
return fs.readFile(normalizedPath);
|
|
276
|
+
}
|
|
277
|
+
} catch {
|
|
278
|
+
// Not found
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Try lib files
|
|
282
|
+
return getLibContent(fileName, libFiles);
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
directoryExists(directoryName: string): boolean {
|
|
286
|
+
const normalizedDir = normalizePath(directoryName);
|
|
287
|
+
|
|
288
|
+
// Check filesystem directly - it tracks directories!
|
|
289
|
+
try {
|
|
290
|
+
if (fs.exists(normalizedDir)) {
|
|
291
|
+
return fs.stat(normalizedDir).isDirectory;
|
|
292
|
+
}
|
|
293
|
+
} catch {
|
|
294
|
+
// Not found
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Virtual directories for lib files
|
|
298
|
+
if (
|
|
299
|
+
normalizedDir === "/node_modules/typescript/lib" ||
|
|
300
|
+
normalizedDir === "/node_modules/typescript" ||
|
|
301
|
+
normalizedDir === "/node_modules"
|
|
302
|
+
) {
|
|
303
|
+
return libFiles.size > 0;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return false;
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
getDirectories(path: string): string[] {
|
|
310
|
+
const normalizedPath = normalizePath(path);
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
if (!fs.exists(normalizedPath)) {
|
|
314
|
+
return [];
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const stat = fs.stat(normalizedPath);
|
|
318
|
+
if (!stat.isDirectory) {
|
|
319
|
+
return [];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Use readdir and filter to directories
|
|
323
|
+
const entries = fs.readdir(normalizedPath);
|
|
324
|
+
const dirs: string[] = [];
|
|
325
|
+
|
|
326
|
+
for (const name of entries) {
|
|
327
|
+
const childPath =
|
|
328
|
+
normalizedPath === "/" ? `/${name}` : `${normalizedPath}/${name}`;
|
|
329
|
+
try {
|
|
330
|
+
if (fs.stat(childPath).isDirectory) {
|
|
331
|
+
dirs.push(name);
|
|
332
|
+
}
|
|
333
|
+
} catch {
|
|
334
|
+
// Skip if can't stat
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return dirs;
|
|
339
|
+
} catch {
|
|
340
|
+
return [];
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
realpath(path: string): string {
|
|
345
|
+
return path;
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
getEnvironmentVariable(): string | undefined {
|
|
349
|
+
return undefined;
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// =============================================================================
|
|
355
|
+
// tsconfig Parsing
|
|
356
|
+
// =============================================================================
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Get default compiler options for TypeScript.
|
|
360
|
+
*/
|
|
361
|
+
function getDefaultCompilerOptions(): ts.CompilerOptions {
|
|
362
|
+
return {
|
|
363
|
+
target: ts.ScriptTarget.ES2020,
|
|
364
|
+
module: ts.ModuleKind.ESNext,
|
|
365
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
366
|
+
esModuleInterop: true,
|
|
367
|
+
strict: true,
|
|
368
|
+
skipLibCheck: true,
|
|
369
|
+
noEmit: true,
|
|
370
|
+
jsx: ts.JsxEmit.ReactJSX,
|
|
371
|
+
allowJs: true,
|
|
372
|
+
resolveJsonModule: true,
|
|
373
|
+
lib: ["lib.es2020.d.ts", "lib.dom.d.ts", "lib.dom.iterable.d.ts"],
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Parse tsconfig.json content into compiler options.
|
|
379
|
+
*
|
|
380
|
+
* Uses TypeScript's built-in parsing instead of manual enum mapping.
|
|
381
|
+
*/
|
|
382
|
+
function parseTsConfig(
|
|
383
|
+
fs: Filesystem,
|
|
384
|
+
configPath: string
|
|
385
|
+
): ts.CompilerOptions {
|
|
386
|
+
try {
|
|
387
|
+
if (!fs.exists(configPath)) {
|
|
388
|
+
return getDefaultCompilerOptions();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const configText = fs.readFile(configPath);
|
|
392
|
+
const { config, error } = ts.parseConfigFileTextToJson(configPath, configText);
|
|
393
|
+
|
|
394
|
+
if (error) {
|
|
395
|
+
console.warn("[typechecker] Error parsing tsconfig:", error.messageText);
|
|
396
|
+
return getDefaultCompilerOptions();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Create a minimal parse host for config parsing
|
|
400
|
+
const parseHost: ts.ParseConfigHost = {
|
|
401
|
+
useCaseSensitiveFileNames: true,
|
|
402
|
+
readDirectory: () => [],
|
|
403
|
+
fileExists: (path) => fs.exists(normalizePath(path)),
|
|
404
|
+
readFile: (path) => {
|
|
405
|
+
try {
|
|
406
|
+
return fs.readFile(normalizePath(path));
|
|
407
|
+
} catch {
|
|
408
|
+
return undefined;
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const parsed = ts.parseJsonConfigFileContent(
|
|
414
|
+
config,
|
|
415
|
+
parseHost,
|
|
416
|
+
"/", // base path
|
|
417
|
+
undefined, // existing options
|
|
418
|
+
configPath
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
if (parsed.errors.length > 0) {
|
|
422
|
+
console.warn(
|
|
423
|
+
"[typechecker] tsconfig parse errors:",
|
|
424
|
+
parsed.errors.map((e) => e.messageText)
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Ensure noEmit is always true for type checking
|
|
429
|
+
return {
|
|
430
|
+
...parsed.options,
|
|
431
|
+
noEmit: true,
|
|
432
|
+
};
|
|
433
|
+
} catch (err) {
|
|
434
|
+
console.warn("[typechecker] Error reading tsconfig:", err);
|
|
435
|
+
return getDefaultCompilerOptions();
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// =============================================================================
|
|
440
|
+
// Diagnostic Conversion
|
|
441
|
+
// =============================================================================
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Convert TypeScript diagnostic category to our severity.
|
|
445
|
+
*/
|
|
446
|
+
function categoryToSeverity(
|
|
447
|
+
category: ts.DiagnosticCategory
|
|
448
|
+
): "error" | "warning" | "info" {
|
|
449
|
+
switch (category) {
|
|
450
|
+
case ts.DiagnosticCategory.Error:
|
|
451
|
+
return "error";
|
|
452
|
+
case ts.DiagnosticCategory.Warning:
|
|
453
|
+
return "warning";
|
|
454
|
+
default:
|
|
455
|
+
return "info";
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Convert TypeScript diagnostic to our format.
|
|
461
|
+
*/
|
|
462
|
+
function convertDiagnostic(diag: ts.Diagnostic): Diagnostic {
|
|
463
|
+
let file: string | undefined;
|
|
464
|
+
let line: number | undefined;
|
|
465
|
+
let column: number | undefined;
|
|
466
|
+
|
|
467
|
+
if (diag.file && diag.start !== undefined) {
|
|
468
|
+
file = diag.file.fileName;
|
|
469
|
+
const pos = diag.file.getLineAndCharacterOfPosition(diag.start);
|
|
470
|
+
line = pos.line + 1; // Convert to 1-based
|
|
471
|
+
column = pos.character + 1;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
file,
|
|
476
|
+
line,
|
|
477
|
+
column,
|
|
478
|
+
message: ts.flattenDiagnosticMessageText(diag.messageText, "\n"),
|
|
479
|
+
severity: categoryToSeverity(diag.category),
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// =============================================================================
|
|
484
|
+
// Typechecker
|
|
485
|
+
// =============================================================================
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Typechecker using TypeScript compiler API.
|
|
489
|
+
*
|
|
490
|
+
* Fetches TypeScript lib files from CDN and caches them.
|
|
491
|
+
* Uses filesystem access for efficient type checking.
|
|
492
|
+
*
|
|
493
|
+
* @example
|
|
494
|
+
* ```ts
|
|
495
|
+
* const typechecker = new Typechecker();
|
|
496
|
+
*
|
|
497
|
+
* const result = await typechecker.typecheck({
|
|
498
|
+
* fs: myFilesystem,
|
|
499
|
+
* entryPoint: "/src/index.ts",
|
|
500
|
+
* });
|
|
501
|
+
*
|
|
502
|
+
* if (!result.success) {
|
|
503
|
+
* console.log("Type errors:", result.diagnostics);
|
|
504
|
+
* }
|
|
505
|
+
* ```
|
|
506
|
+
*/
|
|
507
|
+
export class Typechecker implements ITypechecker {
|
|
508
|
+
private options: TypecheckerOptions;
|
|
509
|
+
private libCache: Map<string, string> = new Map();
|
|
510
|
+
private initPromise: Promise<void> | null = null;
|
|
511
|
+
|
|
512
|
+
constructor(options: TypecheckerOptions = {}) {
|
|
513
|
+
this.options = options;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Pre-fetch TypeScript lib files.
|
|
518
|
+
* Called automatically on first typecheck() if not already done.
|
|
519
|
+
*/
|
|
520
|
+
async initialize(): Promise<void> {
|
|
521
|
+
if (this.initPromise) {
|
|
522
|
+
await this.initPromise;
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (this.libCache.size > 0) {
|
|
527
|
+
return; // Already initialized
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
this.initPromise = this.fetchLibs();
|
|
531
|
+
await this.initPromise;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
private async fetchLibs(): Promise<void> {
|
|
535
|
+
const libs = this.options.libs ?? DEFAULT_LIBS;
|
|
536
|
+
const baseUrl = this.options.libsBaseUrl ?? DEFAULT_CDN_BASE;
|
|
537
|
+
|
|
538
|
+
console.log(`[typechecker] Fetching TypeScript libs: ${libs.join(", ")}...`);
|
|
539
|
+
const fetched = await fetchAllLibs(libs, baseUrl);
|
|
540
|
+
console.log(`[typechecker] Fetched ${fetched.size} lib files`);
|
|
541
|
+
|
|
542
|
+
this.libCache = fetched;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Type check files in a filesystem.
|
|
547
|
+
*/
|
|
548
|
+
async typecheck(options: TypecheckOptions): Promise<TypecheckResult> {
|
|
549
|
+
await this.initialize();
|
|
550
|
+
|
|
551
|
+
const { fs, entryPoint, tsconfigPath = "/tsconfig.json" } = options;
|
|
552
|
+
const normalizedEntry = normalizePath(entryPoint);
|
|
553
|
+
|
|
554
|
+
// Verify entry point exists
|
|
555
|
+
if (!fs.exists(normalizedEntry)) {
|
|
556
|
+
return {
|
|
557
|
+
success: false,
|
|
558
|
+
diagnostics: [
|
|
559
|
+
{
|
|
560
|
+
file: normalizedEntry,
|
|
561
|
+
message: `Entry point not found: ${normalizedEntry}`,
|
|
562
|
+
severity: "error",
|
|
563
|
+
},
|
|
564
|
+
],
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Parse tsconfig
|
|
569
|
+
const compilerOptions = parseTsConfig(fs, tsconfigPath);
|
|
570
|
+
|
|
571
|
+
// Create compiler host
|
|
572
|
+
const host = createCompilerHost(fs, this.libCache, compilerOptions);
|
|
573
|
+
|
|
574
|
+
// Create program and collect diagnostics
|
|
575
|
+
const program = ts.createProgram([normalizedEntry], compilerOptions, host);
|
|
576
|
+
|
|
577
|
+
const allDiagnostics = [
|
|
578
|
+
...program.getSyntacticDiagnostics(),
|
|
579
|
+
...program.getSemanticDiagnostics(),
|
|
580
|
+
...program.getDeclarationDiagnostics(),
|
|
581
|
+
];
|
|
582
|
+
|
|
583
|
+
// Convert diagnostics
|
|
584
|
+
const diagnostics = allDiagnostics.map(convertDiagnostic);
|
|
585
|
+
|
|
586
|
+
// Check for errors
|
|
587
|
+
const success = !diagnostics.some((d) => d.severity === "error");
|
|
588
|
+
|
|
589
|
+
return {
|
|
590
|
+
success,
|
|
591
|
+
diagnostics,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// =============================================================================
|
|
597
|
+
// Factory Function
|
|
598
|
+
// =============================================================================
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Create a typechecker instance.
|
|
602
|
+
*/
|
|
603
|
+
export function createTypechecker(
|
|
604
|
+
options?: TypecheckerOptions
|
|
605
|
+
): ITypechecker {
|
|
606
|
+
return new Typechecker(options);
|
|
607
|
+
}
|