@ricsam/isolate-test-utils 0.1.1 → 0.1.3

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/src/typecheck.ts DELETED
@@ -1,291 +0,0 @@
1
- /**
2
- * Type-checking utility for isolated-vm user code using ts-morph.
3
- *
4
- * This utility allows you to validate TypeScript code strings against
5
- * the isolate global type definitions before running them in the sandbox.
6
- *
7
- * @example
8
- * import { typecheckIsolateCode } from "@ricsam/isolate-test-utils";
9
- *
10
- * const result = typecheckIsolateCode(`
11
- * serve({
12
- * fetch(request, server) {
13
- * return new Response("Hello!");
14
- * }
15
- * });
16
- * `, { include: ["fetch"] });
17
- *
18
- * if (!result.success) {
19
- * console.error("Type errors:", result.errors);
20
- * }
21
- */
22
-
23
- import { Project, ts } from "ts-morph";
24
- import { TYPE_DEFINITIONS, type TypeDefinitionKey } from "./isolate-types.ts";
25
-
26
- /**
27
- * Result of type-checking isolate code.
28
- */
29
- export interface TypecheckResult {
30
- /**
31
- * Whether the code passed type checking.
32
- */
33
- success: boolean;
34
-
35
- /**
36
- * Array of type errors found in the code.
37
- */
38
- errors: TypecheckError[];
39
- }
40
-
41
- /**
42
- * A single type-checking error.
43
- */
44
- export interface TypecheckError {
45
- /**
46
- * The error message from TypeScript.
47
- */
48
- message: string;
49
-
50
- /**
51
- * The line number where the error occurred (1-indexed).
52
- */
53
- line?: number;
54
-
55
- /**
56
- * The column number where the error occurred (1-indexed).
57
- */
58
- column?: number;
59
-
60
- /**
61
- * The TypeScript error code.
62
- */
63
- code?: number;
64
- }
65
-
66
- /**
67
- * A single library type file to inject into the virtual file system.
68
- */
69
- export interface LibraryTypeFile {
70
- /** The file content (e.g., .d.ts or package.json content) */
71
- content: string;
72
- /** The virtual path (e.g., "node_modules/zod/index.d.ts") */
73
- path: string;
74
- }
75
-
76
- /**
77
- * Library types bundle for a single package.
78
- */
79
- export interface LibraryTypes {
80
- files: LibraryTypeFile[];
81
- }
82
-
83
- /**
84
- * Options for type-checking isolate code.
85
- */
86
- export interface TypecheckOptions {
87
- /**
88
- * Which isolate global types to include.
89
- * @default ["core", "fetch", "fs"]
90
- */
91
- include?: Array<"core" | "fetch" | "fs" | "console" | "encoding" | "timers" | "testEnvironment">;
92
-
93
- /**
94
- * Library type definitions to inject for import resolution.
95
- * These are added to the virtual node_modules/ for module resolution.
96
- *
97
- * Use the build-library-types.ts script to generate these bundles from
98
- * your project's node_modules, then pass them here.
99
- *
100
- * @example
101
- * import { LIBRARY_TYPES } from "./my-library-types.ts";
102
- *
103
- * typecheckIsolateCode(code, {
104
- * libraryTypes: {
105
- * zod: LIBRARY_TYPES.zod,
106
- * "@richie-rpc/core": LIBRARY_TYPES["@richie-rpc/core"],
107
- * }
108
- * });
109
- */
110
- libraryTypes?: Record<string, LibraryTypes>;
111
-
112
- /**
113
- * Additional compiler options to pass to TypeScript.
114
- */
115
- compilerOptions?: Partial<ts.CompilerOptions>;
116
- }
117
-
118
- /**
119
- * Get the message text from a TypeScript diagnostic message.
120
- * Handles both string messages and DiagnosticMessageChain objects.
121
- */
122
- function getMessageText(messageText: unknown): string {
123
- if (typeof messageText === "string") {
124
- return messageText;
125
- }
126
-
127
- // Handle ts-morph DiagnosticMessageChain wrapper
128
- if (
129
- messageText &&
130
- typeof messageText === "object" &&
131
- "getMessageText" in messageText &&
132
- typeof (messageText as { getMessageText: unknown }).getMessageText ===
133
- "function"
134
- ) {
135
- return (messageText as { getMessageText: () => string }).getMessageText();
136
- }
137
-
138
- // Handle raw TypeScript DiagnosticMessageChain
139
- if (
140
- messageText &&
141
- typeof messageText === "object" &&
142
- "messageText" in messageText
143
- ) {
144
- return String((messageText as { messageText: unknown }).messageText);
145
- }
146
-
147
- return String(messageText);
148
- }
149
-
150
-
151
- /**
152
- * Type-check isolate user code against the package type definitions.
153
- *
154
- * @param code - The TypeScript/JavaScript code to check
155
- * @param options - Configuration options
156
- * @returns The result of type checking
157
- *
158
- * @example
159
- * // Check code that uses the fetch API
160
- * const result = typecheckIsolateCode(`
161
- * const response = await fetch("https://api.example.com/data");
162
- * const data = await response.json();
163
- * `, { include: ["core", "fetch"] });
164
- *
165
- * @example
166
- * // Check code that uses serve()
167
- * const result = typecheckIsolateCode(`
168
- * serve({
169
- * fetch(request, server) {
170
- * return new Response("Hello!");
171
- * }
172
- * });
173
- * `, { include: ["fetch"] });
174
- *
175
- * @example
176
- * // Check code that uses the file system API
177
- * const result = typecheckIsolateCode(`
178
- * const root = await getDirectory("/data");
179
- * const file = await root.getFileHandle("config.json");
180
- * `, { include: ["core", "fs"] });
181
- */
182
- export function typecheckIsolateCode(
183
- code: string,
184
- options?: TypecheckOptions
185
- ): TypecheckResult {
186
- const include = options?.include ?? ["core", "fetch", "fs"];
187
- const libraryTypes = options?.libraryTypes ?? {};
188
- const hasLibraries = Object.keys(libraryTypes).length > 0;
189
-
190
- // Create a project with in-memory file system
191
- const project = new Project({
192
- useInMemoryFileSystem: true,
193
- compilerOptions: {
194
- target: ts.ScriptTarget.ESNext,
195
- module: ts.ModuleKind.ESNext,
196
- // Use NodeJs resolution for node_modules/ lookup when libraries are included
197
- moduleResolution: hasLibraries
198
- ? ts.ModuleResolutionKind.NodeJs
199
- : undefined,
200
- lib: ["lib.esnext.d.ts", "lib.dom.d.ts"],
201
- strict: true,
202
- noEmit: true,
203
- skipLibCheck: true,
204
- esModuleInterop: true,
205
- allowSyntheticDefaultImports: true,
206
- ...options?.compilerOptions,
207
- },
208
- });
209
-
210
- const memFs = project.getFileSystem();
211
-
212
- // Add type definition files from embedded strings (isolate globals)
213
- for (const pkg of include) {
214
- const content = TYPE_DEFINITIONS[pkg as TypeDefinitionKey];
215
- if (content) {
216
- project.createSourceFile(`${pkg}.d.ts`, content);
217
- }
218
- }
219
-
220
- // Add library type definitions to virtual node_modules/
221
- for (const [_libName, lib] of Object.entries(libraryTypes)) {
222
- for (const typeFile of lib.files) {
223
- // JSON files go to file system, TS files as source files
224
- if (typeFile.path.endsWith(".json")) {
225
- memFs.writeFileSync(`/${typeFile.path}`, typeFile.content);
226
- } else {
227
- project.createSourceFile(`/${typeFile.path}`, typeFile.content, { overwrite: true });
228
- }
229
- }
230
- }
231
-
232
- // Add the user code
233
- const sourceFile = project.createSourceFile("usercode.ts", code);
234
-
235
- // Get diagnostics
236
- const diagnostics = sourceFile.getPreEmitDiagnostics();
237
-
238
- // Convert diagnostics to our error format
239
- const errors: TypecheckError[] = diagnostics.map((diagnostic) => {
240
- const start = diagnostic.getStart();
241
- const sourceFile = diagnostic.getSourceFile();
242
-
243
- let line: number | undefined;
244
- let column: number | undefined;
245
-
246
- if (start !== undefined && sourceFile) {
247
- const lineAndChar = sourceFile.getLineAndColumnAtPos(start);
248
- line = lineAndChar.line;
249
- column = lineAndChar.column;
250
- }
251
-
252
- return {
253
- message: getMessageText(diagnostic.getMessageText()),
254
- line,
255
- column,
256
- code: diagnostic.getCode(),
257
- };
258
- });
259
-
260
- return {
261
- success: errors.length === 0,
262
- errors,
263
- };
264
- }
265
-
266
- /**
267
- * Format type-check errors for display.
268
- *
269
- * @param result - The type-check result
270
- * @returns A formatted string of errors
271
- *
272
- * @example
273
- * const result = typecheckIsolateCode(code);
274
- * if (!result.success) {
275
- * console.error(formatTypecheckErrors(result));
276
- * }
277
- */
278
- export function formatTypecheckErrors(result: TypecheckResult): string {
279
- if (result.success) {
280
- return "No type errors found.";
281
- }
282
-
283
- return result.errors
284
- .map((error) => {
285
- const location =
286
- error.line !== undefined ? `:${error.line}:${error.column ?? 1}` : "";
287
- const code = error.code ? ` (TS${error.code})` : "";
288
- return `usercode.ts${location}${code}: ${error.message}`;
289
- })
290
- .join("\n");
291
- }