@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/CHANGELOG.md +24 -0
- package/package.json +2 -3
- package/src/index.ts +0 -32
- package/src/isolate-types.ts +0 -1778
- package/src/typecheck.ts +0 -291
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
|
-
}
|