@ricsam/isolate-runtime 0.1.4 → 0.1.8

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/README.md CHANGED
@@ -1,46 +1,388 @@
1
1
  # @ricsam/isolate-runtime
2
2
 
3
- Umbrella package that combines all APIs.
3
+ Complete isolated-vm V8 sandbox runtime with all APIs.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm add @ricsam/isolate-runtime isolated-vm
9
+ ```
10
+
11
+ ## Usage
4
12
 
5
13
  ```typescript
6
14
  import { createRuntime } from "@ricsam/isolate-runtime";
7
15
 
8
16
  const runtime = await createRuntime({
9
- // Memory limit in MB
10
- memoryLimit: 128,
11
- // Console API
17
+ memoryLimitMB: 128,
12
18
  console: {
13
- onLog: (level, ...args) => console.log(`[${level}]`, ...args),
19
+ onEntry: (entry) => console.log("[sandbox]", entry),
20
+ },
21
+ fetch: async (request) => fetch(request),
22
+ });
23
+
24
+ // Run code as ES module (supports top-level await)
25
+ await runtime.eval(`
26
+ const response = await fetch("https://api.example.com/data");
27
+ console.log(await response.json());
28
+ `);
29
+
30
+ // Set up HTTP server
31
+ await runtime.eval(`
32
+ serve({
33
+ fetch(request) {
34
+ return Response.json({ message: "Hello!" });
35
+ }
36
+ });
37
+ `);
38
+
39
+ // Dispatch requests via fetch handle
40
+ const response = await runtime.fetch.dispatchRequest(
41
+ new Request("http://localhost/api")
42
+ );
43
+
44
+ // Timers fire automatically with real time
45
+ // Clear all pending timers if needed
46
+ runtime.timers.clearAll();
47
+
48
+ // Console state access
49
+ const counters = runtime.console.getCounters();
50
+ const timers = runtime.console.getTimers();
51
+ runtime.console.reset();
52
+
53
+ // Cleanup
54
+ await runtime.dispose();
55
+ ```
56
+
57
+ ## Runtime Interface
58
+
59
+ ```typescript
60
+ interface RuntimeHandle {
61
+ readonly id: string;
62
+ eval(code: string, filename?: string): Promise<void>;
63
+ dispose(): Promise<void>;
64
+
65
+ // Module handles
66
+ readonly fetch: RuntimeFetchHandle;
67
+ readonly timers: RuntimeTimersHandle;
68
+ readonly console: RuntimeConsoleHandle;
69
+ readonly testEnvironment: RuntimeTestEnvironmentHandle;
70
+ readonly playwright: RuntimePlaywrightHandle;
71
+ }
72
+
73
+ interface RuntimeFetchHandle {
74
+ dispatchRequest(request: Request, options?: DispatchOptions): Promise<Response>;
75
+ hasServeHandler(): boolean;
76
+ hasActiveConnections(): boolean;
77
+ getUpgradeRequest(): UpgradeRequest | null;
78
+ // WebSocket methods...
79
+ }
80
+
81
+ interface RuntimeTimersHandle {
82
+ clearAll(): void;
83
+ }
84
+
85
+ interface RuntimeConsoleHandle {
86
+ reset(): void;
87
+ getTimers(): Map<string, number>;
88
+ getCounters(): Map<string, number>;
89
+ getGroupDepth(): number;
90
+ }
91
+
92
+ interface RuntimeTestEnvironmentHandle {
93
+ runTests(timeout?: number): Promise<RunResults>;
94
+ hasTests(): boolean;
95
+ getTestCount(): number;
96
+ reset(): void;
97
+ }
98
+
99
+ interface RuntimePlaywrightHandle {
100
+ getCollectedData(): CollectedData;
101
+ clearCollectedData(): void;
102
+ }
103
+ ```
104
+
105
+ ## Options
106
+
107
+ ```typescript
108
+ interface RuntimeOptions {
109
+ memoryLimitMB?: number;
110
+ console?: ConsoleCallbacks;
111
+ fetch?: FetchCallback;
112
+ fs?: FsOptions;
113
+ moduleLoader?: ModuleLoaderCallback;
114
+ customFunctions?: CustomFunctions;
115
+ cwd?: string;
116
+ /** Enable test environment (describe, it, expect) */
117
+ testEnvironment?: boolean | TestEnvironmentOptions;
118
+ /** Playwright options - user provides page object */
119
+ playwright?: PlaywrightOptions;
120
+ }
121
+
122
+ interface PlaywrightOptions {
123
+ page: import("playwright").Page;
124
+ timeout?: number;
125
+ baseUrl?: string;
126
+ /** Print browser console logs to stdout */
127
+ console?: boolean;
128
+ /** Browser console log callback (from the page, not sandbox) */
129
+ onBrowserConsoleLog?: (entry: { level: string; args: unknown[]; timestamp: number }) => void;
130
+ onNetworkRequest?: (info: { url: string; method: string; headers: Record<string, string>; timestamp: number }) => void;
131
+ onNetworkResponse?: (info: { url: string; status: number; headers: Record<string, string>; timestamp: number }) => void;
132
+ }
133
+ ```
134
+
135
+ ## Module Loader
136
+
137
+ Provide custom ES modules:
138
+
139
+ ```typescript
140
+ const runtime = await createRuntime({
141
+ moduleLoader: async (moduleName) => {
142
+ if (moduleName === "@/utils") {
143
+ return `
144
+ export function add(a, b) { return a + b; }
145
+ `;
146
+ }
147
+ throw new Error(`Unknown module: ${moduleName}`);
14
148
  },
15
- // Fetch API
16
- fetch: {
17
- onFetch: async (req) => fetch(req),
149
+ });
150
+
151
+ await runtime.eval(`
152
+ import { add } from "@/utils";
153
+ console.log(add(2, 3)); // 5
154
+ `);
155
+ ```
156
+
157
+ ## Custom Functions
158
+
159
+ Expose host functions to the isolate. Each function must specify its `type`:
160
+
161
+ - `'sync'` - Synchronous function, returns value directly
162
+ - `'async'` - Asynchronous function, returns a Promise
163
+ - `'asyncIterator'` - Async generator, yields values via `for await...of`
164
+
165
+ ```typescript
166
+ const runtime = await createRuntime({
167
+ customFunctions: {
168
+ // Async function
169
+ hashPassword: {
170
+ fn: async (password) => bcrypt.hash(password, 10),
171
+ type: 'async',
172
+ },
173
+ // Sync function
174
+ getConfig: {
175
+ fn: () => ({ env: "production" }),
176
+ type: 'sync',
177
+ },
178
+ // Async iterator (generator)
179
+ streamData: {
180
+ fn: async function* (count: number) {
181
+ for (let i = 0; i < count; i++) {
182
+ yield { chunk: i, timestamp: Date.now() };
183
+ }
184
+ },
185
+ type: 'asyncIterator',
186
+ },
18
187
  },
19
- // File System API (optional)
20
- fs: {
21
- getDirectory: async (path) => createNodeFileSystemHandler(`./data${path}`),
188
+ });
189
+
190
+ await runtime.eval(`
191
+ const hash = await hashPassword("secret");
192
+ const config = getConfig(); // sync function, no await needed
193
+
194
+ // Consume async iterator
195
+ for await (const data of streamData(5)) {
196
+ console.log(data.chunk); // 0, 1, 2, 3, 4
197
+ }
198
+ `);
199
+ ```
200
+
201
+ ### Supported Data Types
202
+
203
+ Custom function arguments and return values support the following types:
204
+
205
+ | Category | Types |
206
+ |----------|-------|
207
+ | **Primitives** | `string`, `number`, `boolean`, `null`, `undefined`, `bigint` |
208
+ | **Complex** | `Date`, `RegExp`, `URL`, `Headers` |
209
+ | **Binary** | `Uint8Array`, `ArrayBuffer` |
210
+ | **Web API** | `Request`, `Response`, `File`, `Blob`, `FormData` |
211
+ | **Containers** | Arrays, plain objects (nested) |
212
+ | **Async** | `Promise` (nested), `AsyncIterator` (nested), `Function` (returned) |
213
+
214
+ **Advanced return types:**
215
+
216
+ ```typescript
217
+ const runtime = await createRuntime({
218
+ customFunctions: {
219
+ // Return a function - callable from isolate
220
+ getMultiplier: {
221
+ fn: (factor: number) => (x: number) => x * factor,
222
+ type: 'sync',
223
+ },
224
+ // Return nested promises - awaitable from isolate
225
+ fetchBoth: {
226
+ fn: () => ({
227
+ users: fetch('/api/users').then(r => r.json()),
228
+ posts: fetch('/api/posts').then(r => r.json()),
229
+ }),
230
+ type: 'sync',
231
+ },
22
232
  },
23
233
  });
24
234
 
25
- // The runtime includes:
26
- // - runtime.isolate: The V8 isolate
27
- // - runtime.context: The execution context
28
- // - runtime.tick(): Process pending timers
29
- // - runtime.dispose(): Clean up all resources
235
+ await runtime.eval(`
236
+ const double = getMultiplier(2);
237
+ console.log(double(5)); // 10
30
238
 
31
- // Run code
32
- await runtime.context.eval(`
33
- console.log("Hello from sandbox!");
34
- `, { promise: true });
239
+ const { users, posts } = fetchBoth();
240
+ console.log(await users, await posts);
241
+ `);
242
+ ```
35
243
 
36
- // Process timers
37
- await runtime.tick(100);
244
+ **Unsupported types:**
245
+ - Custom class instances (use plain objects instead)
246
+ - `Symbol`
247
+ - Circular references
38
248
 
39
- // Cleanup
40
- runtime.dispose();
249
+ ## Test Environment
250
+
251
+ Enable test environment to run tests inside the sandbox:
252
+
253
+ ```typescript
254
+ import { createRuntime } from "@ricsam/isolate-runtime";
255
+
256
+ const runtime = await createRuntime({
257
+ testEnvironment: {
258
+ onEvent: (event) => {
259
+ // Receive lifecycle events during test execution
260
+ if (event.type === "testEnd") {
261
+ const icon = event.test.status === "pass" ? "✓" : "✗";
262
+ console.log(`${icon} ${event.test.fullName}`);
263
+ }
264
+ },
265
+ },
266
+ });
267
+
268
+ await runtime.eval(`
269
+ describe("math", () => {
270
+ it("adds numbers", () => {
271
+ expect(1 + 1).toBe(2);
272
+ });
273
+ it.todo("subtract numbers");
274
+ });
275
+ `);
276
+
277
+ // Check if tests exist before running
278
+ if (runtime.testEnvironment.hasTests()) {
279
+ console.log(`Found ${runtime.testEnvironment.getTestCount()} tests`);
280
+ }
281
+
282
+ const results = await runtime.testEnvironment.runTests();
283
+ console.log(`${results.passed}/${results.total} passed, ${results.todo} todo`);
284
+
285
+ // Reset for new tests
286
+ runtime.testEnvironment.reset();
287
+ ```
288
+
289
+ ### TestEnvironmentOptions
290
+
291
+ ```typescript
292
+ interface TestEnvironmentOptions {
293
+ onEvent?: (event: TestEvent) => void;
294
+ testTimeout?: number;
295
+ }
296
+
297
+ type TestEvent =
298
+ | { type: "runStart"; testCount: number; suiteCount: number }
299
+ | { type: "suiteStart"; suite: SuiteInfo }
300
+ | { type: "suiteEnd"; suite: SuiteResult }
301
+ | { type: "testStart"; test: TestInfo }
302
+ | { type: "testEnd"; test: TestResult }
303
+ | { type: "runEnd"; results: RunResults };
304
+ ```
305
+
306
+ ## Playwright Integration
307
+
308
+ Run browser automation with untrusted code. **You provide the Playwright page object**:
309
+
310
+ ### Script Mode (No Tests)
311
+
312
+ ```typescript
313
+ import { createRuntime } from "@ricsam/isolate-runtime";
314
+ import { chromium } from "playwright";
315
+
316
+ const browser = await chromium.launch({ headless: true });
317
+ const page = await browser.newPage();
318
+
319
+ const runtime = await createRuntime({
320
+ playwright: {
321
+ page,
322
+ baseUrl: "https://example.com",
323
+ console: true, // Print browser console to stdout
324
+ },
325
+ });
326
+
327
+ // Run automation script - no test framework needed
328
+ await runtime.eval(`
329
+ await page.goto("/");
330
+ const title = await page.title();
331
+ console.log("Page title:", title);
332
+ `);
333
+
334
+ // Get collected data
335
+ const data = runtime.playwright.getCollectedData();
336
+ console.log("Network requests:", data.networkRequests);
337
+
338
+ await runtime.dispose();
339
+ await browser.close();
340
+ ```
341
+
342
+ ### Test Mode (With Test Framework)
343
+
344
+ Combine `testEnvironment` and `playwright` for browser testing. Playwright extends `expect` with locator matchers:
345
+
346
+ ```typescript
347
+ import { createRuntime } from "@ricsam/isolate-runtime";
348
+ import { chromium } from "playwright";
349
+
350
+ const browser = await chromium.launch({ headless: true });
351
+ const page = await browser.newPage();
352
+
353
+ const runtime = await createRuntime({
354
+ testEnvironment: true, // Provides describe, it, expect
355
+ playwright: {
356
+ page,
357
+ baseUrl: "https://example.com",
358
+ onBrowserConsoleLog: (entry) => console.log("[browser]", ...entry.args),
359
+ },
360
+ });
361
+
362
+ await runtime.eval(`
363
+ describe("homepage", () => {
364
+ it("loads correctly", async () => {
365
+ await page.goto("/");
366
+ await expect(page.getByText("Example Domain")).toBeVisible(); // Locator matcher
367
+ expect(await page.title()).toBe("Example Domain"); // Primitive matcher
368
+ });
369
+ });
370
+ `);
371
+
372
+ // Run tests via test-environment
373
+ const results = await runtime.testEnvironment.runTests();
374
+ console.log(`${results.passed}/${results.total} passed`);
375
+
376
+ // Get browser data
377
+ const data = runtime.playwright.getCollectedData();
378
+ console.log("Browser logs:", data.browserConsoleLogs);
379
+
380
+ await runtime.dispose();
381
+ await browser.close();
41
382
  ```
42
383
 
43
- **What's Included:**
384
+ ## Included APIs
385
+
44
386
  - Core (Blob, File, streams, URL, TextEncoder/Decoder)
45
387
  - Console
46
388
  - Encoding (atob/btoa)
@@ -48,4 +390,23 @@ runtime.dispose();
48
390
  - Path utilities
49
391
  - Crypto (randomUUID, getRandomValues, subtle)
50
392
  - Fetch API
51
- - File System (if handler provided)
393
+ - File System (if handler provided)
394
+ - Test Environment (if enabled)
395
+ - Playwright (if page provided)
396
+
397
+ ## Legacy API
398
+
399
+ For backwards compatibility with code that needs direct isolate/context access:
400
+
401
+ ```typescript
402
+ import { createLegacyRuntime } from "@ricsam/isolate-runtime";
403
+
404
+ const runtime = await createLegacyRuntime();
405
+ // runtime.isolate and runtime.context are available
406
+ await runtime.context.eval(`console.log("Hello")`);
407
+ runtime.dispose(); // sync
408
+ ```
409
+
410
+ ## License
411
+
412
+ MIT