@volter/tabnode 0.5.25 → 0.5.27

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/src/runtime.ts CHANGED
@@ -12,6 +12,8 @@ import { forGuestRealm, installGuestRealm, takeFromHost, defineOnHost, heldWork
12
12
  import type { IRuntime, IExecuteResult, IRuntimeOptions } from './runtime-interface';
13
13
  import type { PackageJson } from './types/package-json';
14
14
  import { simpleHash } from './utils/hash';
15
+ import { sha256 } from '@noble/hashes/sha2.js';
16
+ import { bytesToHex } from '@noble/hashes/utils.js';
15
17
  import { uint8ToBase64, uint8ToHex } from './utils/binary-encoding';
16
18
  import { createFsShim, FsShim } from './shims/fs';
17
19
  import * as pathShim from './shims/path';
@@ -423,6 +425,66 @@ Object.defineProperty(globalThis, "__substrateEvalSource", {
423
425
  configurable: true,
424
426
  value: (source: unknown): unknown => typeof source === "string" ? __substrateRewriteDynamicImportsInScript(source) : source,
425
427
  });
428
+ /** A file's source made a body: shebang stripped, types stripped, ESM lowered, dynamic imports rewritten. */
429
+ function __substratePrepareBody(rawCode: string, resolvedPath: string, format: string | undefined, process: unknown): string {
430
+ let code = rawCode;
431
+
432
+ // Strip shebang line if present (e.g. #!/usr/bin/env node)
433
+ if (code.startsWith('#!')) {
434
+ code = code.slice(code.indexOf('\n') + 1);
435
+ }
436
+
437
+ code = __substrateModuleTypes(code, resolvedPath, format, process);
438
+
439
+ // Transform ESM to CJS if needed (for .mjs files or ESM that wasn't pre-transformed)
440
+ // transformEsmToCjs uses AST to handle import/export, import.meta, and dynamic imports
441
+ // It also handles already-CJS files safely (AST finds no ESM nodes → no-op)
442
+ const __commonjs = format === 'commonjs' || format === 'commonjs-typescript'
443
+ ? true
444
+ : format === 'module' || format === 'module-typescript'
445
+ ? false
446
+ : resolvedPath.endsWith('.cjs') || resolvedPath.endsWith('.cts');
447
+ if (!__commonjs) {
448
+ // An ES module is strict wherever it runs; the mark says so to the
449
+ // compile below, which is otherwise sloppy as a CommonJS body is.
450
+ // The directive shares the body's first line, as Node's one-line
451
+ // wrapper does, so a frame of an ES module counts the file's own lines;
452
+ // on a line of its own it put every frame one line below the file's.
453
+ const lowered = transformEsmToCjs(code, resolvedPath);
454
+ code = lowered === code ? code : `${__substrateModuleMarker}"use strict";${lowered}`;
455
+ } else {
456
+ // A `.cjs` module skips the ESM transform, and with it the rewrite of
457
+ // `import(...)` to the engine's dynamic import, so its
458
+ // `import("fs/promises")` reached the browser's own import and failed on
459
+ // the bare specifier. The dynamic-import rewrite applies to `.cjs` too.
460
+ code = transformDynamicImportsRegex(code);
461
+ }
462
+
463
+ return code;
464
+ }
465
+
466
+ /**
467
+ * Module bodies prepared where the image is built. Parsing a package's files
468
+ * for the passes above is most of a `require` in a tab: 36 of 45 s for
469
+ * playwright-core's, measured in a tab, against 0.9 s under Node on the same
470
+ * machine. The image carries each body under the hash of the file it was made
471
+ * from, in this directory of the tab's filesystem, and the loader takes it in
472
+ * place of the passes when the file it read hashes to one.
473
+ */
474
+ export const PREPARED_MODULES_DIR = '/.tabnode/prepared';
475
+ const PREPARED_MODULES_FORMAT = 'tabnode-prepared-1';
476
+ /** The name a prepared body goes under: the hash of the file as read, and how it is compiled. Undefined for a file no body is prepared for. */
477
+ export function preparedModuleKey(rawCode: string, resolvedPath: string): string | undefined {
478
+ const extension = /\.(js|cjs|mjs)$/u.exec(resolvedPath)?.[1];
479
+ if (!extension) return undefined;
480
+ const kind = extension === 'cjs' ? 'cjs' : 'js';
481
+ return bytesToHex(sha256(new TextEncoder().encode(`${PREPARED_MODULES_FORMAT}|${kind}|${rawCode}`)));
482
+ }
483
+ /** The body the loader would compile for this file, with no load hooks and no type stripping: what the image carries. */
484
+ export function prepareModuleForImage(rawCode: string, resolvedPath: string): string {
485
+ return __substrateScopeGlobalCalls(__substratePrepareBody(rawCode, resolvedPath, undefined, { execArgv: [], env: {} }));
486
+ }
487
+
426
488
  function transformEsmToCjs(code: string, filename: string): string {
427
489
  // Quick check: does the code have any ESM-like patterns?
428
490
  const maybeEsm = __substrateHasEsmSyntax(code);
@@ -1789,6 +1851,14 @@ function createRequire(
1789
1851
  const rawCode = source === undefined || source === null ? defaultSource() : String(source);
1790
1852
  const dirname = resolvedPath.startsWith('data:') ? currentDir : pathShim.dirname(resolvedPath);
1791
1853
 
1854
+ if (format === undefined && !transformsTypes(process as { execArgv?: string[]; env?: Record<string, string> }) && vfs.existsSync(PREPARED_MODULES_DIR)) {
1855
+ const key = preparedModuleKey(rawCode, resolvedPath);
1856
+ const prepared = key ? `${PREPARED_MODULES_DIR}/${key}` : undefined;
1857
+ if (prepared && vfs.existsSync(prepared)) {
1858
+ runModuleBody(module, vfs.readFileSync(prepared, 'utf8'), resolvedPath, dirname, true);
1859
+ return module;
1860
+ }
1861
+ }
1792
1862
  runModuleBody(module, prepareModuleCode(rawCode, resolvedPath, format), resolvedPath, dirname);
1793
1863
  return module;
1794
1864
  };
@@ -1808,45 +1878,10 @@ function createRequire(
1808
1878
  // Use a simple hash of the content for cache key to handle content changes
1809
1879
  const codeCacheKey = `${resolvedPath}|${format ?? ''}|${transformsTypes(process as { execArgv?: string[]; env?: Record<string, string> }) ? 'transform-types|' : ''}${simpleHash(rawCode)}`;
1810
1880
  let code = processedCodeCache?.get(codeCacheKey);
1811
-
1812
1881
  if (!code) {
1813
- code = rawCode;
1814
-
1815
- // Strip shebang line if present (e.g. #!/usr/bin/env node)
1816
- if (code.startsWith('#!')) {
1817
- code = code.slice(code.indexOf('\n') + 1);
1818
- }
1819
-
1820
- code = __substrateModuleTypes(code, resolvedPath, format, process);
1821
-
1822
- // Transform ESM to CJS if needed (for .mjs files or ESM that wasn't pre-transformed)
1823
- // transformEsmToCjs uses AST to handle import/export, import.meta, and dynamic imports
1824
- // It also handles already-CJS files safely (AST finds no ESM nodes → no-op)
1825
- const __commonjs = format === 'commonjs' || format === 'commonjs-typescript'
1826
- ? true
1827
- : format === 'module' || format === 'module-typescript'
1828
- ? false
1829
- : resolvedPath.endsWith('.cjs') || resolvedPath.endsWith('.cts');
1830
- if (!__commonjs) {
1831
- // An ES module is strict wherever it runs; the mark says so to the
1832
- // compile below, which is otherwise sloppy as a CommonJS body is.
1833
- // The directive shares the body's first line, as Node's one-line
1834
- // wrapper does, so a frame of an ES module counts the file's own lines;
1835
- // on a line of its own it put every frame one line below the file's.
1836
- const lowered = transformEsmToCjs(code, resolvedPath);
1837
- code = lowered === code ? code : `${__substrateModuleMarker}"use strict";${lowered}`;
1838
- } else {
1839
- // A `.cjs` module skips the ESM transform, and with it the rewrite of
1840
- // `import(...)` to the engine's dynamic import, so its
1841
- // `import("fs/promises")` reached the browser's own import and failed on
1842
- // the bare specifier. The dynamic-import rewrite applies to `.cjs` too.
1843
- code = transformDynamicImportsRegex(code);
1844
- }
1845
-
1846
- // Cache the processed code
1882
+ code = __substratePrepareBody(rawCode, resolvedPath, format, process);
1847
1883
  processedCodeCache?.set(codeCacheKey, code);
1848
1884
  }
1849
-
1850
1885
  return code;
1851
1886
  };
1852
1887
 
@@ -1858,7 +1893,7 @@ function createRequire(
1858
1893
  * hands the result to it; the engine's own pipeline is what makes the
1859
1894
  * body Node-shaped here.
1860
1895
  */
1861
- const runModuleBody = (module: Module, prepared: string, resolvedPath: string, dirname: string): void => {
1896
+ const runModuleBody = (module: Module, prepared: string, resolvedPath: string, dirname: string, scoped = false): void => {
1862
1897
  let code = prepared;
1863
1898
  // Create require for this module
1864
1899
  const moduleRequire = createRequire(
@@ -1884,7 +1919,7 @@ function createRequire(
1884
1919
  try {
1885
1920
  const importMetaUrl = resolvedPath.startsWith('data:') ? resolvedPath : 'file://' + resolvedPath;
1886
1921
  const strictBody = code.startsWith(__substrateModuleMarker);
1887
- code = __substrateScopeGlobalCalls(code);
1922
+ if (!scoped) code = __substrateScopeGlobalCalls(code);
1888
1923
  // The wrapper is one line and the body begins on it, as Node's
1889
1924
  // `Module.wrap` is one line, so a module's line N is line N of the
1890
1925
  // script V8 compiles and a stack names the module's own lines. The