@ricsam/isolate-test-utils 0.0.1 → 0.1.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/CHANGELOG.md +13 -0
- package/package.json +52 -7
- package/src/fetch-context.ts +33 -0
- package/src/fs-context.ts +65 -0
- package/src/index.test.ts +473 -0
- package/src/index.ts +209 -0
- package/src/isolate-types.ts +1778 -0
- package/src/mock-fs.ts +246 -0
- package/src/native-input-test.ts +412 -0
- package/src/runtime-context.ts +120 -0
- package/src/server.ts +150 -0
- package/src/typecheck.ts +291 -0
- package/tsconfig.json +8 -0
- package/README.md +0 -45
|
@@ -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/src/typecheck.ts
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
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
|
+
}
|
package/tsconfig.json
ADDED
package/README.md
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# @ricsam/isolate-test-utils
|
|
2
|
-
|
|
3
|
-
## ⚠️ IMPORTANT NOTICE ⚠️
|
|
4
|
-
|
|
5
|
-
**This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
|
|
6
|
-
|
|
7
|
-
This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
|
|
8
|
-
|
|
9
|
-
## Purpose
|
|
10
|
-
|
|
11
|
-
This package exists to:
|
|
12
|
-
1. Configure OIDC trusted publishing for the package name `@ricsam/isolate-test-utils`
|
|
13
|
-
2. Enable secure, token-less publishing from CI/CD workflows
|
|
14
|
-
3. Establish provenance for packages published under this name
|
|
15
|
-
|
|
16
|
-
## What is OIDC Trusted Publishing?
|
|
17
|
-
|
|
18
|
-
OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
|
|
19
|
-
|
|
20
|
-
## Setup Instructions
|
|
21
|
-
|
|
22
|
-
To properly configure OIDC trusted publishing for this package:
|
|
23
|
-
|
|
24
|
-
1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
|
|
25
|
-
2. Configure the trusted publisher (e.g., GitHub Actions)
|
|
26
|
-
3. Specify the repository and workflow that should be allowed to publish
|
|
27
|
-
4. Use the configured workflow to publish your actual package
|
|
28
|
-
|
|
29
|
-
## DO NOT USE THIS PACKAGE
|
|
30
|
-
|
|
31
|
-
This package is a placeholder for OIDC configuration only. It:
|
|
32
|
-
- Contains no executable code
|
|
33
|
-
- Provides no functionality
|
|
34
|
-
- Should not be installed as a dependency
|
|
35
|
-
- Exists only for administrative purposes
|
|
36
|
-
|
|
37
|
-
## More Information
|
|
38
|
-
|
|
39
|
-
For more details about npm's trusted publishing feature, see:
|
|
40
|
-
- [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
|
|
41
|
-
- [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
|
|
42
|
-
|
|
43
|
-
---
|
|
44
|
-
|
|
45
|
-
**Maintained for OIDC setup purposes only**
|