@ricsam/isolate-runtime 0.1.10 → 0.1.12

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
@@ -126,7 +126,7 @@ interface PlaywrightOptions {
126
126
  /** Print browser console logs to stdout */
127
127
  console?: boolean;
128
128
  /** Browser console log callback (from the page, not sandbox) */
129
- onBrowserConsoleLog?: (entry: { level: string; args: unknown[]; timestamp: number }) => void;
129
+ onBrowserConsoleLog?: (entry: { level: string; stdout: string; timestamp: number }) => void;
130
130
  onNetworkRequest?: (info: { url: string; method: string; headers: Record<string, string>; timestamp: number }) => void;
131
131
  onNetworkResponse?: (info: { url: string; status: number; headers: Record<string, string>; timestamp: number }) => void;
132
132
  }
@@ -134,15 +134,20 @@ interface PlaywrightOptions {
134
134
 
135
135
  ## Module Loader
136
136
 
137
- Provide custom ES modules:
137
+ Provide custom ES modules. The loader receives the module specifier and importer info, and returns an object with the source code and `resolveDir` (used to resolve nested relative imports):
138
138
 
139
139
  ```typescript
140
140
  const runtime = await createRuntime({
141
- moduleLoader: async (moduleName) => {
141
+ moduleLoader: async (moduleName, importer) => {
142
+ // importer.path = resolved path of importing module
143
+ // importer.resolveDir = directory for relative resolution
142
144
  if (moduleName === "@/utils") {
143
- return `
144
- export function add(a, b) { return a + b; }
145
- `;
145
+ return {
146
+ code: `
147
+ export function add(a, b) { return a + b; }
148
+ `,
149
+ resolveDir: "/modules",
150
+ };
146
151
  }
147
152
  throw new Error(`Unknown module: ${moduleName}`);
148
153
  },
@@ -355,7 +360,7 @@ const runtime = await createRuntime({
355
360
  playwright: {
356
361
  page,
357
362
  baseUrl: "https://example.com",
358
- onBrowserConsoleLog: (entry) => console.log("[browser]", ...entry.args),
363
+ onBrowserConsoleLog: (entry) => console.log("[browser]", entry.stdout),
359
364
  },
360
365
  });
361
366
 
@@ -71,6 +71,7 @@ __export(exports_src, {
71
71
  setupCore: () => import_isolate_core2.setupCore,
72
72
  setupConsole: () => import_isolate_console2.setupConsole,
73
73
  runTests: () => import_isolate_test_environment2.runTests,
74
+ normalizeEntryFilename: () => import_isolate_protocol2.normalizeEntryFilename,
74
75
  hasTests: () => import_isolate_test_environment2.hasTests,
75
76
  getTestCount: () => import_isolate_test_environment2.getTestCount,
76
77
  createRuntime: () => createRuntime,
@@ -79,7 +80,10 @@ __export(exports_src, {
79
80
  });
80
81
  module.exports = __toCommonJS(exports_src);
81
82
  var import_isolated_vm = __toESM(require("isolated-vm"));
83
+ var import_node_path = __toESM(require("node:path"));
82
84
  var import_isolate_core = require("@ricsam/isolate-core");
85
+ var import_isolate_protocol = require("@ricsam/isolate-protocol");
86
+ var import_isolate_protocol2 = require("@ricsam/isolate-protocol");
83
87
  var import_isolate_console = require("@ricsam/isolate-console");
84
88
  var import_isolate_encoding = require("@ricsam/isolate-encoding");
85
89
  var import_isolate_timers = require("@ricsam/isolate-timers");
@@ -89,6 +93,7 @@ var import_isolate_fetch = require("@ricsam/isolate-fetch");
89
93
  var import_isolate_fs = require("@ricsam/isolate-fs");
90
94
  var import_isolate_test_environment = require("@ricsam/isolate-test-environment");
91
95
  var import_isolate_playwright = require("@ricsam/isolate-playwright");
96
+ var import_isolate_protocol3 = require("@ricsam/isolate-protocol");
92
97
  var import_isolate_core2 = require("@ricsam/isolate-core");
93
98
  var import_isolate_console2 = require("@ricsam/isolate-console");
94
99
  var import_utils = require("@ricsam/isolate-console/utils");
@@ -103,6 +108,152 @@ var import_isolate_playwright2 = require("@ricsam/isolate-playwright");
103
108
  __reExport(exports_src, require("./internal.cjs"), module.exports);
104
109
  var iteratorSessions = new Map;
105
110
  var nextIteratorId = 1;
111
+ var ISOLATE_MARSHAL_CODE = `
112
+ (function() {
113
+ // Marshal a value (JavaScript → Ref)
114
+ function marshalForHost(value, depth = 0) {
115
+ if (depth > 100) throw new Error('Maximum marshalling depth exceeded');
116
+
117
+ if (value === null) return null;
118
+ if (value === undefined) return { __type: 'UndefinedRef' };
119
+
120
+ const type = typeof value;
121
+ if (type === 'string' || type === 'number' || type === 'boolean') return value;
122
+ if (type === 'bigint') return { __type: 'BigIntRef', value: value.toString() };
123
+ if (type === 'function') throw new Error('Cannot marshal functions from isolate');
124
+ if (type === 'symbol') throw new Error('Cannot marshal Symbol values');
125
+
126
+ if (type === 'object') {
127
+ if (value instanceof Date) {
128
+ return { __type: 'DateRef', timestamp: value.getTime() };
129
+ }
130
+ if (value instanceof RegExp) {
131
+ return { __type: 'RegExpRef', source: value.source, flags: value.flags };
132
+ }
133
+ if (value instanceof URL) {
134
+ return { __type: 'URLRef', href: value.href };
135
+ }
136
+ if (typeof Headers !== 'undefined' && value instanceof Headers) {
137
+ const pairs = [];
138
+ value.forEach((v, k) => pairs.push([k, v]));
139
+ return { __type: 'HeadersRef', pairs };
140
+ }
141
+ if (value instanceof Uint8Array) {
142
+ return { __type: 'Uint8ArrayRef', data: Array.from(value) };
143
+ }
144
+ if (value instanceof ArrayBuffer) {
145
+ return { __type: 'Uint8ArrayRef', data: Array.from(new Uint8Array(value)) };
146
+ }
147
+ if (typeof Request !== 'undefined' && value instanceof Request) {
148
+ throw new Error('Cannot marshal Request from isolate. Use fetch callback instead.');
149
+ }
150
+ if (typeof Response !== 'undefined' && value instanceof Response) {
151
+ throw new Error('Cannot marshal Response from isolate. Return plain objects instead.');
152
+ }
153
+ if (typeof File !== 'undefined' && value instanceof File) {
154
+ throw new Error('Cannot marshal File from isolate.');
155
+ }
156
+ if (typeof Blob !== 'undefined' && value instanceof Blob) {
157
+ throw new Error('Cannot marshal Blob from isolate.');
158
+ }
159
+ if (typeof FormData !== 'undefined' && value instanceof FormData) {
160
+ throw new Error('Cannot marshal FormData from isolate.');
161
+ }
162
+ if (Array.isArray(value)) {
163
+ return value.map(v => marshalForHost(v, depth + 1));
164
+ }
165
+ // Plain object
166
+ const result = {};
167
+ for (const key of Object.keys(value)) {
168
+ result[key] = marshalForHost(value[key], depth + 1);
169
+ }
170
+ return result;
171
+ }
172
+ return value;
173
+ }
174
+
175
+ // Unmarshal a value (Ref → JavaScript)
176
+ function unmarshalFromHost(value, depth = 0) {
177
+ if (depth > 100) throw new Error('Maximum unmarshalling depth exceeded');
178
+
179
+ if (value === null) return null;
180
+ if (typeof value !== 'object') return value;
181
+
182
+ if (value.__type) {
183
+ switch (value.__type) {
184
+ case 'UndefinedRef': return undefined;
185
+ case 'DateRef': return new Date(value.timestamp);
186
+ case 'RegExpRef': return new RegExp(value.source, value.flags);
187
+ case 'BigIntRef': return BigInt(value.value);
188
+ case 'URLRef': return new URL(value.href);
189
+ case 'HeadersRef': return new Headers(value.pairs);
190
+ case 'Uint8ArrayRef': return new Uint8Array(value.data);
191
+ case 'RequestRef': {
192
+ const init = {
193
+ method: value.method,
194
+ headers: value.headers,
195
+ body: value.body ? new Uint8Array(value.body) : null,
196
+ };
197
+ if (value.mode) init.mode = value.mode;
198
+ if (value.credentials) init.credentials = value.credentials;
199
+ if (value.cache) init.cache = value.cache;
200
+ if (value.redirect) init.redirect = value.redirect;
201
+ if (value.referrer) init.referrer = value.referrer;
202
+ if (value.referrerPolicy) init.referrerPolicy = value.referrerPolicy;
203
+ if (value.integrity) init.integrity = value.integrity;
204
+ return new Request(value.url, init);
205
+ }
206
+ case 'ResponseRef': {
207
+ return new Response(value.body ? new Uint8Array(value.body) : null, {
208
+ status: value.status,
209
+ statusText: value.statusText,
210
+ headers: value.headers,
211
+ });
212
+ }
213
+ case 'FileRef': {
214
+ if (!value.name) {
215
+ return new Blob([new Uint8Array(value.data)], { type: value.type });
216
+ }
217
+ return new File([new Uint8Array(value.data)], value.name, {
218
+ type: value.type,
219
+ lastModified: value.lastModified,
220
+ });
221
+ }
222
+ case 'FormDataRef': {
223
+ const fd = new FormData();
224
+ for (const [key, entry] of value.entries) {
225
+ if (typeof entry === 'string') {
226
+ fd.append(key, entry);
227
+ } else {
228
+ const file = unmarshalFromHost(entry, depth + 1);
229
+ fd.append(key, file);
230
+ }
231
+ }
232
+ return fd;
233
+ }
234
+ default:
235
+ // Unknown ref type, return as-is
236
+ break;
237
+ }
238
+ }
239
+
240
+ if (Array.isArray(value)) {
241
+ return value.map(v => unmarshalFromHost(v, depth + 1));
242
+ }
243
+
244
+ // Plain object - recursively unmarshal
245
+ const result = {};
246
+ for (const key of Object.keys(value)) {
247
+ result[key] = unmarshalFromHost(value[key], depth + 1);
248
+ }
249
+ return result;
250
+ }
251
+
252
+ // Expose as globals
253
+ globalThis.__marshalForHost = marshalForHost;
254
+ globalThis.__unmarshalFromHost = unmarshalFromHost;
255
+ })();
256
+ `;
106
257
  async function setupCustomFunctions(context, customFunctions) {
107
258
  const global = context.global;
108
259
  const invokeCallbackRef = new import_isolated_vm.default.Reference(async (name, argsJson) => {
@@ -116,10 +267,12 @@ async function setupCustomFunctions(context, customFunctions) {
116
267
  }
117
268
  });
118
269
  }
119
- const args = JSON.parse(argsJson);
270
+ const rawArgs = JSON.parse(argsJson);
271
+ const args = import_isolate_protocol3.unmarshalValue(rawArgs);
120
272
  try {
121
273
  const result = def.type === "async" ? await def.fn(...args) : def.fn(...args);
122
- return JSON.stringify({ ok: true, value: result });
274
+ const marshalledResult = await import_isolate_protocol3.marshalValue(result);
275
+ return JSON.stringify({ ok: true, value: marshalledResult });
123
276
  } catch (error) {
124
277
  const err = error;
125
278
  return JSON.stringify({
@@ -141,7 +294,8 @@ async function setupCustomFunctions(context, customFunctions) {
141
294
  });
142
295
  }
143
296
  try {
144
- const args = JSON.parse(argsJson);
297
+ const rawArgs = JSON.parse(argsJson);
298
+ const args = import_isolate_protocol3.unmarshalValue(rawArgs);
145
299
  const fn = def.fn;
146
300
  const iterator = fn(...args);
147
301
  const iteratorId = nextIteratorId++;
@@ -172,10 +326,11 @@ async function setupCustomFunctions(context, customFunctions) {
172
326
  if (result.done) {
173
327
  iteratorSessions.delete(iteratorId);
174
328
  }
329
+ const marshalledValue = await import_isolate_protocol3.marshalValue(result.value);
175
330
  return JSON.stringify({
176
331
  ok: true,
177
332
  done: result.done,
178
- value: result.value
333
+ value: marshalledValue
179
334
  });
180
335
  } catch (error) {
181
336
  const err = error;
@@ -193,10 +348,12 @@ async function setupCustomFunctions(context, customFunctions) {
193
348
  return JSON.stringify({ ok: true, done: true, value: undefined });
194
349
  }
195
350
  try {
196
- const value = valueJson ? JSON.parse(valueJson) : undefined;
351
+ const rawValue = valueJson ? JSON.parse(valueJson) : undefined;
352
+ const value = import_isolate_protocol3.unmarshalValue(rawValue);
197
353
  const result = await session.iterator.return?.(value);
198
354
  iteratorSessions.delete(iteratorId);
199
- return JSON.stringify({ ok: true, done: true, value: result?.value });
355
+ const marshalledValue = await import_isolate_protocol3.marshalValue(result?.value);
356
+ return JSON.stringify({ ok: true, done: true, value: marshalledValue });
200
357
  } catch (error) {
201
358
  const err = error;
202
359
  iteratorSessions.delete(iteratorId);
@@ -225,10 +382,11 @@ async function setupCustomFunctions(context, customFunctions) {
225
382
  });
226
383
  const result = await session.iterator.throw?.(error);
227
384
  iteratorSessions.delete(iteratorId);
385
+ const marshalledValue = await import_isolate_protocol3.marshalValue(result?.value);
228
386
  return JSON.stringify({
229
387
  ok: true,
230
388
  done: result?.done ?? true,
231
- value: result?.value
389
+ value: marshalledValue
232
390
  });
233
391
  } catch (error) {
234
392
  const err = error;
@@ -240,18 +398,20 @@ async function setupCustomFunctions(context, customFunctions) {
240
398
  }
241
399
  });
242
400
  global.setSync("__iter_throw", iterThrowRef);
401
+ context.evalSync(ISOLATE_MARSHAL_CODE);
243
402
  for (const name of Object.keys(customFunctions)) {
244
403
  const def = customFunctions[name];
245
404
  if (def.type === "async") {
246
405
  context.evalSync(`
247
406
  globalThis.${name} = async function(...args) {
407
+ const marshalledArgs = __marshalForHost(args);
248
408
  const resultJson = __customFn_invoke.applySyncPromise(
249
409
  undefined,
250
- ["${name}", JSON.stringify(args)]
410
+ ["${name}", JSON.stringify(marshalledArgs)]
251
411
  );
252
412
  const result = JSON.parse(resultJson);
253
413
  if (result.ok) {
254
- return result.value;
414
+ return __unmarshalFromHost(result.value);
255
415
  } else {
256
416
  const error = new Error(result.error.message);
257
417
  error.name = result.error.name;
@@ -262,13 +422,14 @@ async function setupCustomFunctions(context, customFunctions) {
262
422
  } else if (def.type === "sync") {
263
423
  context.evalSync(`
264
424
  globalThis.${name} = function(...args) {
425
+ const marshalledArgs = __marshalForHost(args);
265
426
  const resultJson = __customFn_invoke.applySyncPromise(
266
427
  undefined,
267
- ["${name}", JSON.stringify(args)]
428
+ ["${name}", JSON.stringify(marshalledArgs)]
268
429
  );
269
430
  const result = JSON.parse(resultJson);
270
431
  if (result.ok) {
271
- return result.value;
432
+ return __unmarshalFromHost(result.value);
272
433
  } else {
273
434
  const error = new Error(result.error.message);
274
435
  error.name = result.error.name;
@@ -279,7 +440,8 @@ async function setupCustomFunctions(context, customFunctions) {
279
440
  } else if (def.type === "asyncIterator") {
280
441
  context.evalSync(`
281
442
  globalThis.${name} = function(...args) {
282
- const startResult = JSON.parse(__iter_start.applySyncPromise(undefined, ["${name}", JSON.stringify(args)]));
443
+ const marshalledArgs = __marshalForHost(args);
444
+ const startResult = JSON.parse(__iter_start.applySyncPromise(undefined, ["${name}", JSON.stringify(marshalledArgs)]));
283
445
  if (!startResult.ok) {
284
446
  throw Object.assign(new Error(startResult.error.message), { name: startResult.error.name });
285
447
  }
@@ -291,18 +453,18 @@ async function setupCustomFunctions(context, customFunctions) {
291
453
  if (!result.ok) {
292
454
  throw Object.assign(new Error(result.error.message), { name: result.error.name });
293
455
  }
294
- return { done: result.done, value: result.value };
456
+ return { done: result.done, value: __unmarshalFromHost(result.value) };
295
457
  },
296
458
  async return(v) {
297
- const result = JSON.parse(__iter_return.applySyncPromise(undefined, [iteratorId, JSON.stringify(v)]));
298
- return { done: true, value: result.value };
459
+ const result = JSON.parse(__iter_return.applySyncPromise(undefined, [iteratorId, JSON.stringify(__marshalForHost(v))]));
460
+ return { done: true, value: __unmarshalFromHost(result.value) };
299
461
  },
300
462
  async throw(e) {
301
463
  const result = JSON.parse(__iter_throw.applySyncPromise(undefined, [iteratorId, JSON.stringify({ message: e.message, name: e.name })]));
302
464
  if (!result.ok) {
303
465
  throw Object.assign(new Error(result.error.message), { name: result.error.name });
304
466
  }
305
- return { done: result.done, value: result.value };
467
+ return { done: result.done, value: __unmarshalFromHost(result.value) };
306
468
  }
307
469
  };
308
470
  };
@@ -312,17 +474,24 @@ async function setupCustomFunctions(context, customFunctions) {
312
474
  return invokeCallbackRef;
313
475
  }
314
476
  function createModuleResolver(state) {
315
- return async (specifier, _referrer) => {
477
+ return async (specifier, referrer) => {
316
478
  const cached = state.moduleCache.get(specifier);
317
479
  if (cached)
318
480
  return cached;
319
481
  if (!state.moduleLoader) {
320
482
  throw new Error(`No module loader registered. Cannot import: ${specifier}`);
321
483
  }
322
- const code = await state.moduleLoader(specifier);
484
+ const importerPath = state.moduleToFilename.get(referrer) ?? "<unknown>";
485
+ const importerResolveDir = import_node_path.default.posix.dirname(importerPath);
486
+ const { code, resolveDir } = await state.moduleLoader(specifier, {
487
+ path: importerPath,
488
+ resolveDir: importerResolveDir
489
+ });
323
490
  const mod = await state.isolate.compileModule(code, {
324
491
  filename: specifier
325
492
  });
493
+ const resolvedPath = import_node_path.default.posix.join(resolveDir, import_node_path.default.posix.basename(specifier));
494
+ state.moduleToFilename.set(mod, resolvedPath);
326
495
  state.moduleCache.set(specifier, mod);
327
496
  const resolver = createModuleResolver(state);
328
497
  await mod.instantiate(state.context, resolver);
@@ -351,6 +520,7 @@ async function createRuntime(options) {
351
520
  context,
352
521
  handles: {},
353
522
  moduleCache: new Map,
523
+ moduleToFilename: new Map,
354
524
  moduleLoader: opts.moduleLoader,
355
525
  customFunctions: opts.customFunctions
356
526
  };
@@ -384,7 +554,7 @@ async function createRuntime(options) {
384
554
  consoleHandler({
385
555
  type: "browserOutput",
386
556
  level: event.level,
387
- args: event.args,
557
+ stdout: event.stdout,
388
558
  timestamp: event.timestamp
389
559
  });
390
560
  }
@@ -520,9 +690,11 @@ async function createRuntime(options) {
520
690
  playwright: playwrightHandle,
521
691
  async eval(code, filenameOrOptions) {
522
692
  const options2 = typeof filenameOrOptions === "string" ? { filename: filenameOrOptions } : filenameOrOptions;
693
+ const filename = import_isolate_protocol.normalizeEntryFilename(options2?.filename);
523
694
  const mod = await state.isolate.compileModule(code, {
524
- filename: options2?.filename ?? "<eval>"
695
+ filename
525
696
  });
697
+ state.moduleToFilename.set(mod, filename);
526
698
  const resolver = createModuleResolver(state);
527
699
  await mod.instantiate(state.context, resolver);
528
700
  await mod.evaluate(options2?.maxExecutionMs ? { timeout: options2.maxExecutionMs } : undefined);
@@ -548,4 +720,4 @@ async function createRuntime(options) {
548
720
  };
549
721
  }
550
722
 
551
- //# debugId=6D096DCA13F6758764756E2164756E21
723
+ //# debugId=B6BA58D3B2A5225064756E2164756E21