@ricsam/isolate-test-utils 0.0.1 → 0.1.2

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.
@@ -0,0 +1,412 @@
1
+ import ivm from "isolated-vm";
2
+
3
+ export interface TestRuntime {
4
+ logs: Record<string, unknown>;
5
+ result: unknown;
6
+ }
7
+
8
+ export interface TestRunner {
9
+ input(inputs: Record<string, unknown>): TestRuntime;
10
+ }
11
+
12
+ /**
13
+ * Run isolate code with native objects as input and capture logs
14
+ *
15
+ * This utility allows testing whether native objects passed INTO the isolate
16
+ * behave like isolate instances. It converts native web API classes (Headers,
17
+ * Request, Response, URL, Blob, File, FormData) to their isolate equivalents
18
+ * before executing the test code.
19
+ *
20
+ * @example
21
+ * const runtime = runTestCode(ctx.context, `
22
+ * const headers = testingInput.headers;
23
+ * log("instanceof", headers instanceof Headers);
24
+ * log("contentType", headers.get("content-type"));
25
+ * `).input({
26
+ * headers: new Headers({ "content-type": "application/json" })
27
+ * });
28
+ *
29
+ * expect(runtime.logs.instanceof).toBe(true);
30
+ * expect(runtime.logs.contentType).toBe("application/json");
31
+ */
32
+ export function runTestCode(context: ivm.Context, code: string): TestRunner {
33
+ return {
34
+ input(inputs: Record<string, unknown>): TestRuntime {
35
+ const logs: Record<string, unknown> = {};
36
+
37
+ // Setup log capture - log(tag, value) stores as logs[tag] = value
38
+ // Values are unmarshalled back to native types for bidirectional testing
39
+ const logCallback = new ivm.Callback(
40
+ (tag: string, valueJson: string) => {
41
+ const value = JSON.parse(valueJson);
42
+ logs[tag] = unmarshalFromJson(value);
43
+ }
44
+ );
45
+ context.global.setSync("__log_callback__", logCallback);
46
+
47
+ // Create a wrapper log function that serializes values
48
+ context.evalSync(`
49
+ globalThis.log = function(tag, value) {
50
+ __log_callback__(tag, JSON.stringify(__serializeForLog__(value)));
51
+ };
52
+
53
+ globalThis.__serializeForLog__ = function(value) {
54
+ if (typeof Headers !== 'undefined' && value instanceof Headers) {
55
+ const pairs = [];
56
+ for (const [k, v] of value) pairs.push([k, v]);
57
+ return { __type__: 'Headers', pairs };
58
+ }
59
+ if (typeof Request !== 'undefined' && value instanceof Request) {
60
+ const headers = [];
61
+ for (const [k, v] of value.headers) headers.push([k, v]);
62
+ return {
63
+ __type__: 'Request',
64
+ url: value.url,
65
+ method: value.method,
66
+ headers,
67
+ mode: value.mode,
68
+ credentials: value.credentials,
69
+ cache: value.cache,
70
+ redirect: value.redirect,
71
+ referrer: value.referrer,
72
+ referrerPolicy: value.referrerPolicy,
73
+ integrity: value.integrity,
74
+ };
75
+ }
76
+ if (typeof Response !== 'undefined' && value instanceof Response) {
77
+ const headers = [];
78
+ for (const [k, v] of value.headers) headers.push([k, v]);
79
+ return {
80
+ __type__: 'Response',
81
+ status: value.status,
82
+ statusText: value.statusText,
83
+ ok: value.ok,
84
+ headers,
85
+ type: value.type,
86
+ redirected: value.redirected,
87
+ url: value.url,
88
+ };
89
+ }
90
+ if (typeof FormData !== 'undefined' && value instanceof FormData) {
91
+ const entries = [];
92
+ for (const [k, v] of value) {
93
+ if (typeof File !== 'undefined' && v instanceof File) {
94
+ entries.push([k, { __type__: 'File', name: v.name, type: v.type, lastModified: v.lastModified }]);
95
+ } else {
96
+ entries.push([k, v]);
97
+ }
98
+ }
99
+ return { __type__: 'FormData', entries };
100
+ }
101
+ if (value instanceof URL) {
102
+ return { __type__: 'URL', href: value.href };
103
+ }
104
+ if (value instanceof File) {
105
+ return { __type__: 'File', name: value.name, type: value.type, lastModified: value.lastModified };
106
+ }
107
+ if (value instanceof Blob) {
108
+ return { __type__: 'Blob', type: value.type, size: value.size };
109
+ }
110
+ if (Array.isArray(value)) {
111
+ return value.map(v => __serializeForLog__(v));
112
+ }
113
+ if (value && typeof value === 'object' && value.constructor === Object) {
114
+ const result = {};
115
+ for (const [k, v] of Object.entries(value)) {
116
+ result[k] = __serializeForLog__(v);
117
+ }
118
+ return result;
119
+ }
120
+ return value;
121
+ };
122
+ `);
123
+
124
+ // Marshal inputs with special handling for native web API classes
125
+ marshalInputs(context, inputs);
126
+
127
+ // Run the code
128
+ let returnValue: unknown = undefined;
129
+ try {
130
+ returnValue = context.evalSync(code);
131
+ } catch (error) {
132
+ // Clean up before re-throwing
133
+ context.evalSync(`
134
+ delete globalThis.testingInput;
135
+ delete globalThis.log;
136
+ delete globalThis.__log_callback__;
137
+ delete globalThis.__serializeForLog__;
138
+ `);
139
+ throw error;
140
+ }
141
+
142
+ // Cleanup
143
+ context.evalSync(`
144
+ delete globalThis.testingInput;
145
+ delete globalThis.log;
146
+ delete globalThis.__log_callback__;
147
+ delete globalThis.__serializeForLog__;
148
+ `);
149
+
150
+ return { logs, result: returnValue };
151
+ },
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Marshal inputs into the isolate, converting native web API classes
157
+ */
158
+ function marshalInputs(
159
+ context: ivm.Context,
160
+ inputs: Record<string, unknown>
161
+ ): void {
162
+ // Create the testingInput object in the isolate
163
+ context.evalSync(`globalThis.testingInput = {};`);
164
+
165
+ for (const [key, value] of Object.entries(inputs)) {
166
+ marshalValue(context, `testingInput.${key}`, value);
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Marshal a single value into the isolate at the given path
172
+ */
173
+ function marshalValue(
174
+ context: ivm.Context,
175
+ path: string,
176
+ value: unknown
177
+ ): void {
178
+ // Check for native Headers
179
+ if (value instanceof Headers) {
180
+ const pairs: [string, string][] = [];
181
+ value.forEach((v, k) => pairs.push([k, v]));
182
+ const pairsJson = JSON.stringify(pairs);
183
+ context.evalSync(`${path} = new Headers(${pairsJson});`);
184
+ return;
185
+ }
186
+
187
+ // Check for native Request
188
+ if (value instanceof Request) {
189
+ // First marshal the headers
190
+ const headerPairs: [string, string][] = [];
191
+ value.headers.forEach((v, k) => headerPairs.push([k, v]));
192
+ const headersJson = JSON.stringify(headerPairs);
193
+
194
+ const urlJson = JSON.stringify(value.url);
195
+ const methodJson = JSON.stringify(value.method);
196
+ const modeJson = JSON.stringify(value.mode);
197
+ const credentialsJson = JSON.stringify(value.credentials);
198
+ const cacheJson = JSON.stringify(value.cache);
199
+ const redirectJson = JSON.stringify(value.redirect);
200
+ const referrerJson = JSON.stringify(value.referrer);
201
+ const referrerPolicyJson = JSON.stringify(value.referrerPolicy);
202
+ const integrityJson = JSON.stringify(value.integrity);
203
+
204
+ context.evalSync(`
205
+ ${path} = new Request(${urlJson}, {
206
+ method: ${methodJson},
207
+ headers: new Headers(${headersJson}),
208
+ mode: ${modeJson},
209
+ credentials: ${credentialsJson},
210
+ cache: ${cacheJson},
211
+ redirect: ${redirectJson},
212
+ referrer: ${referrerJson},
213
+ referrerPolicy: ${referrerPolicyJson},
214
+ integrity: ${integrityJson},
215
+ });
216
+ `);
217
+ return;
218
+ }
219
+
220
+ // Check for native Response
221
+ if (value instanceof Response) {
222
+ const headerPairs: [string, string][] = [];
223
+ value.headers.forEach((v, k) => headerPairs.push([k, v]));
224
+ const headersJson = JSON.stringify(headerPairs);
225
+
226
+ const statusJson = JSON.stringify(value.status);
227
+ const statusTextJson = JSON.stringify(value.statusText);
228
+
229
+ context.evalSync(`
230
+ ${path} = new Response(null, {
231
+ status: ${statusJson},
232
+ statusText: ${statusTextJson},
233
+ headers: new Headers(${headersJson}),
234
+ });
235
+ `);
236
+ return;
237
+ }
238
+
239
+ // Check for native FormData
240
+ if (value instanceof FormData) {
241
+ context.evalSync(`${path} = new FormData();`);
242
+
243
+ for (const [key, entryValue] of value.entries()) {
244
+ const keyJson = JSON.stringify(key);
245
+
246
+ if (entryValue instanceof File) {
247
+ const nameJson = JSON.stringify(entryValue.name);
248
+ const typeJson = JSON.stringify(entryValue.type);
249
+ const lastModifiedJson = JSON.stringify(entryValue.lastModified);
250
+ context.evalSync(`
251
+ ${path}.append(${keyJson}, new File([], ${nameJson}, { type: ${typeJson}, lastModified: ${lastModifiedJson} }));
252
+ `);
253
+ } else {
254
+ const valueJson = JSON.stringify(entryValue);
255
+ context.evalSync(`${path}.append(${keyJson}, ${valueJson});`);
256
+ }
257
+ }
258
+ return;
259
+ }
260
+
261
+ // Check for native URL
262
+ if (value instanceof URL) {
263
+ const hrefJson = JSON.stringify(value.href);
264
+ context.evalSync(`${path} = new URL(${hrefJson});`);
265
+ return;
266
+ }
267
+
268
+ // Check for native File (before Blob, since File extends Blob)
269
+ if (value instanceof File) {
270
+ const nameJson = JSON.stringify(value.name);
271
+ const typeJson = JSON.stringify(value.type);
272
+ const lastModifiedJson = JSON.stringify(value.lastModified);
273
+ context.evalSync(
274
+ `${path} = new File([], ${nameJson}, { type: ${typeJson}, lastModified: ${lastModifiedJson} });`
275
+ );
276
+ return;
277
+ }
278
+
279
+ // Check for native Blob
280
+ if (value instanceof Blob) {
281
+ const typeJson = JSON.stringify(value.type);
282
+ context.evalSync(`${path} = new Blob([], { type: ${typeJson} });`);
283
+ return;
284
+ }
285
+
286
+ // Handle arrays recursively
287
+ if (Array.isArray(value)) {
288
+ context.evalSync(`${path} = [];`);
289
+ for (let i = 0; i < value.length; i++) {
290
+ marshalValue(context, `${path}[${i}]`, value[i]);
291
+ }
292
+ return;
293
+ }
294
+
295
+ // Handle plain objects recursively
296
+ if (value && typeof value === "object" && value.constructor === Object) {
297
+ context.evalSync(`${path} = {};`);
298
+ for (const [key, val] of Object.entries(value)) {
299
+ // Use bracket notation for safe property access
300
+ marshalValue(context, `${path}[${JSON.stringify(key)}]`, val);
301
+ }
302
+ return;
303
+ }
304
+
305
+ // For primitives, set directly via JSON
306
+ const valueJson = JSON.stringify(value);
307
+ context.evalSync(`${path} = ${valueJson};`);
308
+ }
309
+
310
+ /**
311
+ * Unmarshal a value from JSON, converting special __type__ markers back to native instances
312
+ */
313
+ function unmarshalFromJson(value: unknown): unknown {
314
+ if (value === null || value === undefined) {
315
+ return value;
316
+ }
317
+
318
+ if (Array.isArray(value)) {
319
+ return value.map((v) => unmarshalFromJson(v));
320
+ }
321
+
322
+ if (typeof value === "object") {
323
+ const obj = value as Record<string, unknown>;
324
+
325
+ // Check for special type markers
326
+ if (obj.__type__ === "Headers") {
327
+ const pairs = obj.pairs as [string, string][];
328
+ const headers = new Headers();
329
+ for (const [k, v] of pairs) {
330
+ headers.append(k, v);
331
+ }
332
+ return headers;
333
+ }
334
+
335
+ if (obj.__type__ === "Request") {
336
+ const headers = new Headers();
337
+ for (const [k, v] of obj.headers as [string, string][]) {
338
+ headers.append(k, v);
339
+ }
340
+ return new Request(obj.url as string, {
341
+ method: obj.method as string,
342
+ headers,
343
+ mode: obj.mode as Request["mode"],
344
+ credentials: obj.credentials as Request["credentials"],
345
+ cache: obj.cache as Request["cache"],
346
+ redirect: obj.redirect as Request["redirect"],
347
+ referrer: obj.referrer as string,
348
+ referrerPolicy: obj.referrerPolicy as Request["referrerPolicy"],
349
+ integrity: obj.integrity as string,
350
+ });
351
+ }
352
+
353
+ if (obj.__type__ === "Response") {
354
+ const headers = new Headers();
355
+ for (const [k, v] of obj.headers as [string, string][]) {
356
+ headers.append(k, v);
357
+ }
358
+ return new Response(null, {
359
+ status: obj.status as number,
360
+ statusText: obj.statusText as string,
361
+ headers,
362
+ });
363
+ }
364
+
365
+ if (obj.__type__ === "FormData") {
366
+ const formData = new FormData();
367
+ for (const [k, v] of obj.entries as [string, unknown][]) {
368
+ if (
369
+ typeof v === "object" &&
370
+ v !== null &&
371
+ (v as Record<string, unknown>).__type__ === "File"
372
+ ) {
373
+ const fileObj = v as Record<string, unknown>;
374
+ formData.append(
375
+ k,
376
+ new File([], fileObj.name as string, {
377
+ type: fileObj.type as string,
378
+ lastModified: fileObj.lastModified as number,
379
+ })
380
+ );
381
+ } else {
382
+ formData.append(k, v as string);
383
+ }
384
+ }
385
+ return formData;
386
+ }
387
+
388
+ if (obj.__type__ === "URL") {
389
+ return new URL(obj.href as string);
390
+ }
391
+
392
+ if (obj.__type__ === "File") {
393
+ return new File([], obj.name as string, {
394
+ type: obj.type as string,
395
+ lastModified: obj.lastModified as number,
396
+ });
397
+ }
398
+
399
+ if (obj.__type__ === "Blob") {
400
+ return new Blob([], { type: obj.type as string });
401
+ }
402
+
403
+ // Plain object - recursively unmarshal properties
404
+ const result: Record<string, unknown> = {};
405
+ for (const [k, v] of Object.entries(obj)) {
406
+ result[k] = unmarshalFromJson(v);
407
+ }
408
+ return result;
409
+ }
410
+
411
+ return value;
412
+ }
@@ -0,0 +1,120 @@
1
+ import type ivm from "isolated-vm";
2
+ import { MockFileSystem } from "./mock-fs.ts";
3
+
4
+ export interface MockResponse {
5
+ status?: number;
6
+ body?: string;
7
+ headers?: Record<string, string>;
8
+ }
9
+
10
+ export interface RuntimeTestContextOptions {
11
+ /** Enable file system APIs with mock file system */
12
+ fs?: boolean;
13
+ }
14
+
15
+ export interface RuntimeTestContext {
16
+ isolate: ivm.Isolate;
17
+ context: ivm.Context;
18
+ /** Advance virtual time and process pending timers */
19
+ tick(ms?: number): Promise<void>;
20
+ dispose(): void;
21
+ /** Captured console.log calls */
22
+ logs: Array<{ level: string; args: unknown[] }>;
23
+ /** Captured fetch calls */
24
+ fetchCalls: Array<{ url: string; method: string; headers: [string, string][] }>;
25
+ /** Set the mock response for the next fetch call */
26
+ setMockResponse(response: MockResponse): void;
27
+ /** Mock file system (only available if fs option is true) */
28
+ mockFs: MockFileSystem;
29
+ }
30
+
31
+ /**
32
+ * Create a full runtime test context with all APIs set up.
33
+ * Includes console logging capture, fetch mocking, and optionally file system.
34
+ *
35
+ * @example
36
+ * const ctx = await createRuntimeTestContext({ fs: true });
37
+ *
38
+ * // Set up mock response for fetch
39
+ * ctx.setMockResponse({ status: 200, body: '{"data": "test"}' });
40
+ *
41
+ * // Run code
42
+ * await ctx.context.eval(`
43
+ * (async () => {
44
+ * console.log("Starting fetch...");
45
+ * const response = await fetch("https://api.example.com/data");
46
+ * const data = await response.json();
47
+ * console.log("Got data:", data);
48
+ * })()
49
+ * `, { promise: true });
50
+ *
51
+ * // Check logs
52
+ * console.log(ctx.logs); // [{ level: "log", args: ["Starting fetch..."] }, ...]
53
+ *
54
+ * // Check fetch calls
55
+ * console.log(ctx.fetchCalls); // [{ url: "https://api.example.com/data", method: "GET", ... }]
56
+ *
57
+ * ctx.dispose();
58
+ */
59
+ export async function createRuntimeTestContext(
60
+ options?: RuntimeTestContextOptions
61
+ ): Promise<RuntimeTestContext> {
62
+ const opts = options ?? {};
63
+ const { createRuntime } = await import("@ricsam/isolate-runtime");
64
+ const { clearAllInstanceState } = await import("@ricsam/isolate-core");
65
+
66
+ // Clear any previous instance state
67
+ clearAllInstanceState();
68
+
69
+ // State for capturing logs and fetch calls
70
+ const logs: Array<{ level: string; args: unknown[] }> = [];
71
+ const fetchCalls: Array<{
72
+ url: string;
73
+ method: string;
74
+ headers: [string, string][];
75
+ }> = [];
76
+
77
+ let mockResponse: MockResponse = { status: 200, body: "" };
78
+
79
+ // Create mock file system
80
+ const mockFs = new MockFileSystem();
81
+
82
+ // Create runtime with configured handlers
83
+ const runtime = await createRuntime({
84
+ console: {
85
+ onLog: (level: string, ...args: unknown[]) => {
86
+ logs.push({ level, args });
87
+ },
88
+ },
89
+ fetch: {
90
+ onFetch: async (request: Request) => {
91
+ // Capture fetch call
92
+ fetchCalls.push({
93
+ url: request.url,
94
+ method: request.method,
95
+ headers: [...request.headers.entries()],
96
+ });
97
+
98
+ // Return mock response
99
+ return new Response(mockResponse.body ?? "", {
100
+ status: mockResponse.status ?? 200,
101
+ headers: mockResponse.headers,
102
+ });
103
+ },
104
+ },
105
+ fs: opts.fs ? { getDirectory: async () => mockFs } : undefined,
106
+ });
107
+
108
+ return {
109
+ isolate: runtime.isolate,
110
+ context: runtime.context,
111
+ tick: runtime.tick.bind(runtime),
112
+ dispose: runtime.dispose.bind(runtime),
113
+ logs,
114
+ fetchCalls,
115
+ setMockResponse(response: MockResponse) {
116
+ mockResponse = response;
117
+ },
118
+ mockFs,
119
+ };
120
+ }
package/src/server.ts ADDED
@@ -0,0 +1,150 @@
1
+ import { createServer, type Server, type IncomingMessage, type ServerResponse } from "node:http";
2
+
3
+ export interface MockServerResponse {
4
+ status?: number;
5
+ body?: string;
6
+ headers?: Record<string, string>;
7
+ }
8
+
9
+ export interface RecordedRequest {
10
+ method: string;
11
+ path: string;
12
+ headers: Record<string, string>;
13
+ body?: string;
14
+ }
15
+
16
+ export interface IntegrationServer {
17
+ /** The base URL of the server (e.g., "http://localhost:3000") */
18
+ url: string;
19
+ /** The port the server is listening on */
20
+ port: number;
21
+ /** Close the server */
22
+ close(): Promise<void>;
23
+ /** Set the response for a specific path */
24
+ setResponse(path: string, response: MockServerResponse): void;
25
+ /** Set a default response for any unmatched path */
26
+ setDefaultResponse(response: MockServerResponse): void;
27
+ /** Get all recorded requests */
28
+ getRequests(): RecordedRequest[];
29
+ /** Clear all recorded requests */
30
+ clearRequests(): void;
31
+ /** Clear all configured responses */
32
+ clearResponses(): void;
33
+ }
34
+
35
+ /**
36
+ * Start an HTTP server for integration tests.
37
+ * Useful for testing fetch operations against a real server.
38
+ *
39
+ * @example
40
+ * const server = await startIntegrationServer();
41
+ *
42
+ * server.setResponse("/api/data", {
43
+ * status: 200,
44
+ * body: JSON.stringify({ message: "Hello" }),
45
+ * headers: { "Content-Type": "application/json" }
46
+ * });
47
+ *
48
+ * // In your test
49
+ * const response = await fetch(`${server.url}/api/data`);
50
+ * const data = await response.json();
51
+ *
52
+ * // Check what requests were made
53
+ * const requests = server.getRequests();
54
+ * console.log(requests[0].path); // "/api/data"
55
+ *
56
+ * await server.close();
57
+ */
58
+ export async function startIntegrationServer(
59
+ port?: number
60
+ ): Promise<IntegrationServer> {
61
+ const responses = new Map<string, MockServerResponse>();
62
+ const requests: RecordedRequest[] = [];
63
+ let defaultResponse: MockServerResponse = { status: 404, body: "Not Found" };
64
+
65
+ const server: Server = createServer(
66
+ async (req: IncomingMessage, res: ServerResponse) => {
67
+ const path = req.url ?? "/";
68
+ const method = req.method ?? "GET";
69
+
70
+ // Read request body
71
+ const chunks: Buffer[] = [];
72
+ for await (const chunk of req) {
73
+ chunks.push(chunk as Buffer);
74
+ }
75
+ const body = chunks.length > 0 ? Buffer.concat(chunks).toString() : undefined;
76
+
77
+ // Record the request
78
+ const headers: Record<string, string> = {};
79
+ for (const [key, value] of Object.entries(req.headers)) {
80
+ if (typeof value === "string") {
81
+ headers[key] = value;
82
+ } else if (Array.isArray(value)) {
83
+ headers[key] = value.join(", ");
84
+ }
85
+ }
86
+ requests.push({ method, path, headers, body });
87
+
88
+ // Find and send response
89
+ const mockResponse = responses.get(path) ?? defaultResponse;
90
+
91
+ res.statusCode = mockResponse.status ?? 200;
92
+
93
+ if (mockResponse.headers) {
94
+ for (const [key, value] of Object.entries(mockResponse.headers)) {
95
+ res.setHeader(key, value);
96
+ }
97
+ }
98
+
99
+ res.end(mockResponse.body ?? "");
100
+ }
101
+ );
102
+
103
+ // Find an available port
104
+ const actualPort = await new Promise<number>((resolve, reject) => {
105
+ server.listen(port ?? 0, () => {
106
+ const address = server.address();
107
+ if (address && typeof address === "object") {
108
+ resolve(address.port);
109
+ } else {
110
+ reject(new Error("Failed to get server address"));
111
+ }
112
+ });
113
+ server.on("error", reject);
114
+ });
115
+
116
+ return {
117
+ url: `http://localhost:${actualPort}`,
118
+ port: actualPort,
119
+
120
+ async close() {
121
+ return new Promise((resolve, reject) => {
122
+ server.close((err) => {
123
+ if (err) reject(err);
124
+ else resolve();
125
+ });
126
+ });
127
+ },
128
+
129
+ setResponse(path: string, response: MockServerResponse) {
130
+ responses.set(path, response);
131
+ },
132
+
133
+ setDefaultResponse(response: MockServerResponse) {
134
+ defaultResponse = response;
135
+ },
136
+
137
+ getRequests() {
138
+ return [...requests];
139
+ },
140
+
141
+ clearRequests() {
142
+ requests.length = 0;
143
+ },
144
+
145
+ clearResponses() {
146
+ responses.clear();
147
+ defaultResponse = { status: 404, body: "Not Found" };
148
+ },
149
+ };
150
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src"
5
+ },
6
+ "include": ["src/**/*"],
7
+ "exclude": ["node_modules", "dist", "**/*.test.ts"]
8
+ }