@volter/tabnode 0.5.16 → 0.5.18

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.
Files changed (36) hide show
  1. package/dist/assets/{runtime-worker-Bw8vbYik.js → runtime-worker-C-l9pCp3.js} +4374 -365
  2. package/dist/code-transforms.d.ts +2 -1
  3. package/dist/index.mjs +4319 -322
  4. package/dist/node-lib/binding/zlib.d.ts +14 -1
  5. package/dist/node-lib/internals/amaro.d.ts +31 -0
  6. package/dist/node-lib/internals/runtime.d.ts +1 -0
  7. package/dist/node-lib/node-versions.d.ts +1 -0
  8. package/dist/node-lib/typescript-module.d.ts +11 -0
  9. package/dist/process-tokens.d.ts +2 -0
  10. package/dist/shims/perf_hooks.d.ts +0 -4
  11. package/dist/shims/process.d.ts +3 -0
  12. package/dist/shims/shell-jobs.d.ts +12 -0
  13. package/dist/shims/streamed-output.d.ts +14 -0
  14. package/dist/shims/tls.d.ts +3 -1
  15. package/dist/shims/worker_threads.d.ts +4 -0
  16. package/package.json +2 -1
  17. package/src/code-transforms.ts +28 -13
  18. package/src/node-lib/binding/zlib.ts +80 -3
  19. package/src/node-lib/internal/modules/typescript.js +183 -0
  20. package/src/node-lib/internals/amaro.ts +51 -0
  21. package/src/node-lib/internals/index.ts +2 -0
  22. package/src/node-lib/internals/runtime.ts +22 -3
  23. package/src/node-lib/node-versions.ts +4 -1
  24. package/src/node-lib/sources.ts +2 -0
  25. package/src/node-lib/typescript-module.ts +50 -0
  26. package/src/process-tokens.ts +6 -0
  27. package/src/runtime.ts +97 -19
  28. package/src/shims/child_process.ts +41 -13
  29. package/src/shims/esbuild.ts +12 -1
  30. package/src/shims/perf_hooks.ts +18 -0
  31. package/src/shims/process.ts +2 -2
  32. package/src/shims/shell-jobs.ts +451 -0
  33. package/src/shims/streamed-output.ts +25 -0
  34. package/src/shims/string_decoder.ts +5 -0
  35. package/src/shims/tls.ts +13 -1
  36. package/src/shims/worker_threads.ts +19 -0
@@ -0,0 +1,51 @@
1
+ /**
2
+ * `internal/deps/amaro/dist/index`: the type stripper Node carries.
3
+ *
4
+ * Node v22.18.0 runs a `.ts`, `.mts` or `.cts` file by erasing its types with
5
+ * amaro 1.1.0 (swc's stripper, built to WebAssembly), vendored under
6
+ * `deps/amaro`, and its own `lib/internal/modules/typescript.js` (vendored
7
+ * here unmodified) is what calls it. amaro is not native: it is JavaScript and
8
+ * WebAssembly, and this engine takes the same release as a dependency rather
9
+ * than writing a stripper of its own. esbuild, which the engine also carries,
10
+ * is not a substitute: it reprints the program (a stack's line and column move)
11
+ * and it transforms `enum`, `namespace` and parameter properties where Node, in
12
+ * strip-only mode, refuses them.
13
+ *
14
+ * amaro's glue loads its WebAssembly synchronously from bytes inlined in the
15
+ * package and reads `util` and `buffer`, so it is loaded through the host's own
16
+ * `require` where the host is Node. A browser realm has no such door: there the
17
+ * page supplies its stripper as `globalThis.__substrateStripTypes`, which the
18
+ * module loader asks first, and this answers absent.
19
+ */
20
+ type AmaroTransform = (source: string, options: Record<string, unknown>) => { code: string; map?: string };
21
+ interface Amaro { transformSync: AmaroTransform }
22
+
23
+ /** The host's `process`, taken before a guest's takes the global name. */
24
+ const hostProcess = typeof process !== 'undefined' && process !== null
25
+ ? (process as unknown as { getBuiltinModule?: (name: string) => unknown })
26
+ : null;
27
+
28
+ let loaded: Amaro | null | undefined;
29
+
30
+ /** amaro, loaded once from beside this engine, or null where the realm cannot load it. */
31
+ export function hostAmaro(): Amaro | null {
32
+ if (loaded !== undefined) return loaded;
33
+ loaded = null;
34
+ try {
35
+ const moduleBuiltin = typeof hostProcess?.getBuiltinModule === 'function'
36
+ ? hostProcess.getBuiltinModule('module') as { createRequire?: (from: string) => (id: string) => unknown } | undefined
37
+ : undefined;
38
+ if (moduleBuiltin && typeof moduleBuiltin.createRequire === 'function') {
39
+ const amaro = moduleBuiltin.createRequire(import.meta.url)('amaro') as Amaro;
40
+ if (typeof amaro?.transformSync === 'function') loaded = amaro;
41
+ }
42
+ } catch { /* no host require, or amaro not installed beside the engine */ }
43
+ return loaded;
44
+ }
45
+
46
+ /** The module Node's `typescript.js` requires; absent, the error Node raises when built without amaro. */
47
+ export function internalAmaro(): Amaro {
48
+ const amaro = hostAmaro();
49
+ if (amaro) return amaro;
50
+ throw Object.assign(new Error('Node.js is not compiled with TypeScript support'), { code: 'ERR_NO_TYPESCRIPT' });
51
+ }
@@ -26,6 +26,7 @@ import {
26
26
  internalAbortController, internalBlob, internalFile, internalWebStreamsAdapters, createWebStreamsAdapters,
27
27
  } from './buffer-and-streams';
28
28
  import { internalBootstrapRealm, internalUrl, internalEncoding } from './modules';
29
+ import { internalAmaro } from './amaro';
29
30
 
30
31
  /** Built on the first ask, for the reason `./binding/index.ts` gives. */
31
32
  // eslint-disable-next-line no-var, vars-on-top
@@ -69,6 +70,7 @@ export function nodeLibInternal(name: string, require?: (name: string) => any, p
69
70
  'internal/process/warning': () => internalProcessWarning,
70
71
  'internal/source_map/source_map_cache': () => internalSourceMapCache,
71
72
  'internal/deps/undici/undici': () => internalUndici,
73
+ 'internal/deps/amaro/dist/index': () => internalAmaro(),
72
74
  'internal/readline/interface': () => internalReadlineInterface,
73
75
  'internal/worker/js_transferable': () => internalJsTransferable,
74
76
  };;
@@ -219,8 +219,9 @@ export const internalEventTarget = {
219
219
  };
220
220
 
221
221
  /**
222
- * `internal/options`: the command-line options Node's `net` reads. The engine
223
- * takes no command line of its own, so each is Node's own default.
222
+ * `internal/options`: the command-line options Node's `net`, `http` and
223
+ * `util` read. The engine takes no command line of its own, so each is
224
+ * Node's own default.
224
225
  */
225
226
  const optionValues: Record<string, unknown> = {
226
227
  '--network-family-autoselection': true,
@@ -231,10 +232,28 @@ const optionValues: Record<string, unknown> = {
231
232
  '--pending-deprecation': false,
232
233
  '--report-uncaught-exception': false,
233
234
  '--experimental-print-required-tla': false,
235
+ '--max-http-header-size': 16 * 1024,
236
+ '--insecure-http-parser': false,
237
+ '--enable-source-maps': false,
238
+ '--experimental-transform-types': false,
234
239
  };
235
240
 
241
+ /**
242
+ * Options one run was started with, answered only while a call made for that
243
+ * run is on the stack. Node reads an option of its own process; the engine
244
+ * holds many processes in one realm, so a caller that knows whose call it is
245
+ * (the module loader stripping a `.ts` file for a run started with
246
+ * `--experimental-transform-types`) says so for the length of that call.
247
+ */
248
+ let optionOverrides: Record<string, unknown> | null = null;
249
+ export function withOptionValues<T>(overrides: Record<string, unknown>, fn: () => T): T {
250
+ const previous = optionOverrides;
251
+ optionOverrides = { ...(previous ?? {}), ...overrides };
252
+ try { return fn(); } finally { optionOverrides = previous; }
253
+ }
254
+
236
255
  export const internalOptions = {
237
- getOptionValue: (name: string): unknown => optionValues[name],
256
+ getOptionValue: (name: string): unknown => (optionOverrides !== null && name in optionOverrides ? optionOverrides[name] : optionValues[name]),
238
257
  getEmbedderOptions: () => ({ shouldNotRegisterESMLoader: false, noGlobalSearchPaths: false, noBrowserGlobals: false }),
239
258
  };
240
259
 
@@ -25,10 +25,13 @@ export function nodeVersions(node: string = NODE_LTS_VERSION): {
25
25
  uv: string;
26
26
  webcontainer: string;
27
27
  openssl: string;
28
+ amaro: string;
28
29
  } {
29
30
  // `https.js` calls `assertCrypto()`, which is `!process.versions.openssl`.
30
31
  // The engine has crypto (the noble-hashes binding); Node reports openssl
31
32
  // whenever it does. A worker compiles `internal/util` against this table
32
33
  // before any guest process exists, so the name has to live here.
33
- return { node, v8: "11.3.244.8", uv: "1.44.2", webcontainer: "1", openssl: "3.0.15" };
34
+ // `amaro` is the type stripper a `.ts` file runs through, the release Node
35
+ // v22.18.0 carries; `internal/util` reads it for `assertTypeScript()`.
36
+ return { node, v8: "11.3.244.8", uv: "1.44.2", webcontainer: "1", openssl: "3.0.15", amaro: "1.1.0" };
34
37
  }
@@ -43,6 +43,7 @@ import INTERNAL_VALIDATORS from './internal/validators.js?raw';
43
43
  import INTERNAL_ABORT_LISTENER from './internal/events/abort_listener.js?raw';
44
44
  import INTERNAL_CHILD_PROCESS_SERIALIZATION from './internal/child_process/serialization.js?raw';
45
45
  import INTERNAL_MODULES_CUSTOMIZATION_HOOKS from './internal/modules/customization_hooks.js?raw';
46
+ import INTERNAL_MODULES_TYPESCRIPT from './internal/modules/typescript.js?raw';
46
47
  import EVENTS from './events.js?raw';
47
48
  import INTERNAL_CONSTANTS from './internal/constants.js?raw';
48
49
  import INTERNAL_MIME from './internal/mime.js?raw';
@@ -142,6 +143,7 @@ export const NODE_LIB_SOURCES: Record<string, string> = {
142
143
  'internal/events/abort_listener': INTERNAL_ABORT_LISTENER,
143
144
  'internal/child_process/serialization': INTERNAL_CHILD_PROCESS_SERIALIZATION,
144
145
  'internal/modules/customization_hooks': INTERNAL_MODULES_CUSTOMIZATION_HOOKS,
146
+ 'internal/modules/typescript': INTERNAL_MODULES_TYPESCRIPT,
145
147
  'events': EVENTS,
146
148
  'internal/constants': INTERNAL_CONSTANTS,
147
149
  'internal/mime': INTERNAL_MIME,
@@ -0,0 +1,50 @@
1
+ /**
2
+ * A `.ts`, `.mts` or `.cts` module's types erased the way Node's loader erases
3
+ * them: `Module.prototype._compile` hands the source to
4
+ * `stripTypeScriptModuleTypes` of Node's own `internal/modules/typescript.js`
5
+ * (vendored, unmodified), which runs amaro in strip-only mode -- types become
6
+ * whitespace, so every line and column stays where the file has it -- refuses
7
+ * `enum`, `namespace` with values, parameter properties and the rest with
8
+ * `ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX`, and refuses any file under
9
+ * `node_modules` with `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`.
10
+ *
11
+ * The file is the process's own, from the process's builtin graph: Node reads
12
+ * `--experimental-transform-types` once per process (`getLazy`) and emits its
13
+ * experimental warning on that process, which `--no-warnings` silences. The
14
+ * options a run was started with are answered for the length of the call;
15
+ * `--experimental-transform-types` implies `--enable-source-maps`, as Node's
16
+ * option table says.
17
+ */
18
+ import { loadNodeLibFor } from './load';
19
+ import { withOptionValues } from './internals/runtime';
20
+ import { hostAmaro } from './internals/amaro';
21
+
22
+ interface TypeScriptModule { stripTypeScriptModuleTypes(source: string, filename: string): string }
23
+ type RunProcess = { execArgv?: readonly string[]; env?: Record<string, string | undefined> };
24
+
25
+ /** Whether this realm can erase types itself: amaro is loadable here. */
26
+ export function canStripTypes(): boolean {
27
+ return hostAmaro() !== null;
28
+ }
29
+
30
+ /** Whether a process was started with an option, on its command line or in `NODE_OPTIONS` (both options here are allowed there). */
31
+ function startedWith(process: RunProcess, option: string): boolean {
32
+ if (Array.isArray(process.execArgv) && process.execArgv.includes(option)) return true;
33
+ const nodeOptions = process.env?.NODE_OPTIONS;
34
+ return typeof nodeOptions === 'string' && nodeOptions.split(/\s+/u).includes(option);
35
+ }
36
+
37
+ /** Whether a process transforms TypeScript-only syntax rather than refusing it. */
38
+ export function transformsTypes(process: RunProcess): boolean {
39
+ return startedWith(process, '--experimental-transform-types');
40
+ }
41
+
42
+ /** The module's source with its types erased (or, under `--experimental-transform-types`, transformed), for this process. */
43
+ export function stripModuleTypes(source: string, filename: string, process: object): string {
44
+ const typescript = loadNodeLibFor(process, 'internal/modules/typescript') as TypeScriptModule;
45
+ const transform = transformsTypes(process as RunProcess);
46
+ return withOptionValues({
47
+ '--experimental-transform-types': transform,
48
+ '--enable-source-maps': transform || startedWith(process as RunProcess, '--enable-source-maps'),
49
+ }, () => typescript.stripTypeScriptModuleTypes(source, filename));
50
+ }
@@ -237,6 +237,12 @@ export function forgetRunPid(token: ProcessToken): void {
237
237
  pidsOfRuns.delete(token);
238
238
  }
239
239
 
240
+ /** The named run a pid belongs to in this realm, for a signal sent by number (the shell's `kill`). */
241
+ export function tokenOfPid(pid: number): ProcessToken | null {
242
+ for (const [token, numbers] of pidsOfRuns) if (numbers.pid === pid) return token;
243
+ return null;
244
+ }
245
+
240
246
  /** Whether a live run carries this number, which is what `kill(pid, 0)` asks. */
241
247
  export function pidIsLive(pid: number): boolean {
242
248
  return processRegistry.lookup(pid) !== undefined;
package/src/runtime.ts CHANGED
@@ -66,8 +66,9 @@ import * as domainShim from './shims/domain';
66
66
  import { createWasiModule, type WasiHostFs, type WasiModule } from './shims/wasi';
67
67
 
68
68
  import { resolve as resolveExports, imports as resolveImports } from 'resolve.exports';
69
- import { transformEsmToCjsSimple, setNodeLowering, __cjsExports, __substrateHasEsmSyntax } from './code-transforms';
69
+ import { transformEsmToCjsSimple, setNodeLowering, __cjsExports, __cjsObject, __substrateHasEsmSyntax } from './code-transforms';
70
70
  import { applySourceEdits } from './source-edits';
71
+ import { canStripTypes, stripModuleTypes, transformsTypes } from './node-lib/typescript-module';
71
72
  import * as acorn from 'acorn';
72
73
 
73
74
  /**
@@ -114,6 +115,31 @@ function __substrateGuestConstructor(Constructor: unknown, process: Process): un
114
115
  return __substrateFunctionScope(__substrateGuestGlobal(process), String(compiled));
115
116
  } });
116
117
  }
118
+ /**
119
+ * A `.ts`, `.mts` or `.cts` file runs with its types stripped, as Node runs
120
+ * one since 22.18, before the module's imports are read -- the entry a `node`
121
+ * command names and every module it loads alike. A page that loaded a
122
+ * stripper into the realm supplies it; elsewhere the engine erases the types
123
+ * itself, through Node's own `typescript.js` over amaro
124
+ * (`node-lib/typescript-module.ts`): strip-only, positions kept, the syntax
125
+ * Node refuses refused with Node's error. Before this, a realm with no stripper
126
+ * registered -- the engine under Node -- ran the file as JavaScript and died on
127
+ * its first type (`Unexpected identifier 'SlackReply'`), and an entry was never
128
+ * stripped anywhere. A realm that can load neither runs the file as it is.
129
+ *
130
+ * `format` is what a load hook named, and it decides instead of the extension
131
+ * where a hook gave one.
132
+ */
133
+ function __substrateModuleTypes(code: string, resolvedPath: string, format: string | undefined, process: unknown): string {
134
+ const typescript = format === undefined
135
+ ? /[.](?:ts|mts|cts)$/u.test(resolvedPath) && !resolvedPath.endsWith('.d.ts')
136
+ : format === 'typescript' || format === 'commonjs-typescript' || format === 'module-typescript';
137
+ if (!typescript) return code;
138
+ const registered = (globalThis as { __substrateStripTypes?: (code: string, filename: string) => string }).__substrateStripTypes;
139
+ if (typeof registered === 'function') return registered(code, resolvedPath);
140
+ if (!canStripTypes()) return code;
141
+ return stripModuleTypes(code, resolvedPath, process as object);
142
+ }
117
143
  /**
118
144
  * The name the script of a module's body carries.
119
145
  *
@@ -263,7 +289,12 @@ function __substrateGuestGlobal(process: Process): Record<string, unknown> {
263
289
  if (key === "fetch") { fetch = value; return true; }
264
290
  if (key === "Buffer") { buffer = value; return true; }
265
291
  if (shadowed(target, key)) return Reflect.set(target, key, value, target);
266
- return Reflect.set(host, key, value, host);
292
+ if (Reflect.set(host, key, value, host)) return true;
293
+ // A global the host holds read-only (a confined realm's \`WebSocket\`) is
294
+ // still the guest's to replace, as Node's globals are: undici's
295
+ // \`install()\` assigns \`globalThis.WebSocket\` and Pi died on the refusal.
296
+ // The value becomes the guest's own binding; the host's is untouched.
297
+ return Reflect.defineProperty(target, key, { value, writable: true, configurable: true, enumerable: true });
267
298
  },
268
299
  has(target, key) { return isLocalGlobal(key) ? Reflect.has(target, key) : key === "global" || shadowed(target, key) || key in host; },
269
300
  ownKeys(target) {
@@ -448,7 +479,7 @@ function transformEsmToCjsAst(code: string, filename: string): string {
448
479
  transformed = transformEsmToCjsSimple(transformed);
449
480
 
450
481
  if (hasExportDecl) {
451
- transformed = `Object.defineProperty(${__cjsExports()}, "__esModule", { value: true });\n` + transformed;
482
+ transformed = `${__cjsObject()}.defineProperty(${__cjsExports()}, "__esModule", { value: true });\n` + transformed;
452
483
  }
453
484
  }
454
485
 
@@ -476,7 +507,7 @@ function transformEsmToCjsRegexFallback(code: string, filename: string): string
476
507
  if (hasImport || hasExport) {
477
508
  transformed = transformEsmToCjsSimple(transformed);
478
509
  if (hasExport) {
479
- transformed = `Object.defineProperty(${__cjsExports()}, "__esModule", { value: true });\n` + transformed;
510
+ transformed = `${__cjsObject()}.defineProperty(${__cjsExports()}, "__esModule", { value: true });\n` + transformed;
480
511
  }
481
512
  }
482
513
 
@@ -1770,7 +1801,7 @@ function createRequire(
1770
1801
  const prepareModuleCode = (rawCode: string, resolvedPath: string, format?: string): string => {
1771
1802
  // Check processed code cache (useful for HMR when module cache is cleared but code hasn't changed)
1772
1803
  // Use a simple hash of the content for cache key to handle content changes
1773
- const codeCacheKey = `${resolvedPath}|${format ?? ''}|${simpleHash(rawCode)}`;
1804
+ const codeCacheKey = `${resolvedPath}|${format ?? ''}|${transformsTypes(process as { execArgv?: string[]; env?: Record<string, string> }) ? 'transform-types|' : ''}${simpleHash(rawCode)}`;
1774
1805
  let code = processedCodeCache?.get(codeCacheKey);
1775
1806
 
1776
1807
  if (!code) {
@@ -1781,16 +1812,7 @@ function createRequire(
1781
1812
  code = code.slice(code.indexOf('\n') + 1);
1782
1813
  }
1783
1814
 
1784
- // A `.ts`, `.mts` or `.cts` file runs with its types stripped, as Node
1785
- // runs one since 22.18: the source goes to the type stripper the realm
1786
- // loaded, amaro, before the module's imports are read. A realm without one
1787
- // runs the file as it is.
1788
- const __typescript = format === undefined
1789
- ? /[.](?:ts|mts|cts)$/u.test(resolvedPath) && !resolvedPath.endsWith('.d.ts')
1790
- : format === 'typescript' || format === 'commonjs-typescript' || format === 'module-typescript';
1791
- if (__typescript && typeof (globalThis as any).__substrateStripTypes === 'function') {
1792
- code = (globalThis as any).__substrateStripTypes(code, resolvedPath) as string;
1793
- }
1815
+ code = __substrateModuleTypes(code, resolvedPath, format, process);
1794
1816
 
1795
1817
  // Transform ESM to CJS if needed (for .mjs files or ESM that wasn't pre-transformed)
1796
1818
  // transformEsmToCjs uses AST to handle import/export, import.meta, and dynamic imports
@@ -1799,12 +1821,15 @@ function createRequire(
1799
1821
  ? true
1800
1822
  : format === 'module' || format === 'module-typescript'
1801
1823
  ? false
1802
- : resolvedPath.endsWith('.cjs');
1824
+ : resolvedPath.endsWith('.cjs') || resolvedPath.endsWith('.cts');
1803
1825
  if (!__commonjs) {
1804
1826
  // An ES module is strict wherever it runs; the mark says so to the
1805
1827
  // compile below, which is otherwise sloppy as a CommonJS body is.
1828
+ // The directive shares the body's first line, as Node's one-line
1829
+ // wrapper does, so a frame of an ES module counts the file's own lines;
1830
+ // on a line of its own it put every frame one line below the file's.
1806
1831
  const lowered = transformEsmToCjs(code, resolvedPath);
1807
- code = lowered === code ? code : `${__substrateModuleMarker}"use strict";\n${lowered}`;
1832
+ code = lowered === code ? code : `${__substrateModuleMarker}"use strict";${lowered}`;
1808
1833
  } else {
1809
1834
  // A `.cjs` module skips the ESM transform, and with it the rewrite of
1810
1835
  // `import(...)` to the engine's dynamic import, so its
@@ -2369,10 +2394,13 @@ export class Runtime {
2369
2394
  code = code.slice(code.indexOf('\n') + 1);
2370
2395
  }
2371
2396
 
2397
+ code = __substrateModuleTypes(code, filename, undefined, this.process);
2398
+
2372
2399
  // Transform ESM to CJS if needed (AST-based, handles import.meta and dynamic imports too)
2373
- if (!filename.endsWith('.cjs')) {
2400
+ if (!filename.endsWith('.cjs') && !filename.endsWith('.cts')) {
2374
2401
  const lowered = transformEsmToCjs(code, filename);
2375
- code = lowered === code ? code : `${__substrateModuleMarker}"use strict";\n${lowered}`;
2402
+ // The directive shares the body's first line, as `prepareModuleCode`'s does.
2403
+ code = lowered === code ? code : `${__substrateModuleMarker}"use strict";${lowered}`;
2376
2404
  } else {
2377
2405
  // A `.cjs` module skips the ESM transform, and with it the rewrite of
2378
2406
  // `import(...)` to the engine's dynamic import, so its
@@ -2680,6 +2708,56 @@ forGuestRealm(() => {
2680
2708
  }
2681
2709
  });
2682
2710
 
2711
+ forGuestRealm(() => {
2712
+ // Node's `stream.finished()` on a web stream waits on a promise Node's own
2713
+ // web streams carry (`nodejs.webstream.isClosedPromise`), settled when the
2714
+ // stream closes or errors, and undici's fetch finalizes every response that
2715
+ // way. The worker's streams are the browser's and carry none, so a stream a
2716
+ // guest constructs is given one, settled by its own controller. The
2717
+ // constructor stays the browser's (a proxy of it), so a stream the platform
2718
+ // made is still `instanceof ReadableStream`.
2719
+ const kIsClosedPromise = Symbol.for('nodejs.webstream.isClosedPromise');
2720
+ const Native = globalThis.ReadableStream;
2721
+ if (typeof Native !== 'function' || (Native as unknown as { __substrateClosedPromise?: boolean }).__substrateClosedPromise) return;
2722
+ type Source = { start?: (controller: unknown) => unknown; pull?: (controller: unknown) => unknown; cancel?: (reason: unknown) => unknown };
2723
+ const Tracked: typeof ReadableStream = new Proxy(Native, {
2724
+ construct(target, args, newTarget): object {
2725
+ let resolve!: () => void;
2726
+ let reject!: (error: unknown) => void;
2727
+ const promise = new Promise<void>((yes, no) => { resolve = yes; reject = no; });
2728
+ promise.then(undefined, () => {});
2729
+ const source = args[0] as Source | null | undefined;
2730
+ const track = (controller: { close?: (...rest: unknown[]) => unknown; error?: (...rest: unknown[]) => unknown }): void => {
2731
+ const close = controller.close;
2732
+ const error = controller.error;
2733
+ if (typeof close === 'function') controller.close = function (this: unknown, ...rest: unknown[]) { const result = close.apply(this, rest); resolve(); return result; };
2734
+ if (typeof error === 'function') controller.error = function (this: unknown, ...rest: unknown[]) { reject(rest[0]); return error.apply(this, rest); };
2735
+ };
2736
+ const failed = (cause: unknown): never => { reject(cause); throw cause; };
2737
+ const wrapped: Source = Object.create(source ?? null);
2738
+ wrapped.start = (controller) => {
2739
+ track(controller as Parameters<typeof track>[0]);
2740
+ try { return source?.start?.call(source, controller); } catch (cause) { return failed(cause); }
2741
+ };
2742
+ if (typeof source?.pull === 'function') {
2743
+ const pull = source.pull;
2744
+ wrapped.pull = (controller) => {
2745
+ try { return Promise.resolve(pull.call(source, controller)).catch(failed); } catch (cause) { return failed(cause); }
2746
+ };
2747
+ }
2748
+ wrapped.cancel = (reason) => {
2749
+ resolve();
2750
+ return source?.cancel?.call(source, reason);
2751
+ };
2752
+ const stream: object = Reflect.construct(target, [wrapped, ...args.slice(1)], newTarget === Tracked ? target : newTarget);
2753
+ Object.defineProperty(stream, kIsClosedPromise, { value: { promise, resolve, reject }, configurable: true });
2754
+ return stream;
2755
+ },
2756
+ });
2757
+ Object.defineProperty(Tracked, '__substrateClosedPromise', { value: true });
2758
+ takeFromHost(globalThis, 'ReadableStream', Tracked);
2759
+ });
2760
+
2683
2761
  // Polyfill Error.captureStackTrace/prepareStackTrace for Safari/WebKit
2684
2762
  // (V8-specific API used by Express's depd and other npm packages)
2685
2763
  forGuestRealm(__substrateStackTracePolyfill);
@@ -70,6 +70,8 @@ import { Runtime, pendingGuestTimers, stopGuestTimers, __substratePendingOf } fr
70
70
  import { __nodeResolverFor } from '../node-resolver';
71
71
  import { resolve as __resolvePath } from './path';
72
72
  import { setSyncChildVfs, warmSyncChild } from './sync-child';
73
+ import { installShellJobs } from './shell-jobs';
74
+ import { unstreamedOutput } from './streamed-output';
73
75
  import type { PackageJson } from '../types/package-json';
74
76
 
75
77
  // Capture host capabilities before the guest's authority is restricted. These
@@ -936,8 +938,10 @@ export function initChildProcess(vfs: VirtualFS): void {
936
938
  // engine's own registries. A run that still holds a handle or a timer
937
939
  // is not that -- Node's loop runs while one is open, for as long as it
938
940
  // is open -- and cutting it at a minute ended a forked child that was
939
- // a server, which is every extension host and every pty host.
940
- if (!isLongRunning && !stillWorking() && Date.now() - startTime >= MAX_TOTAL_MS) break;
941
+ // a server, which is every extension host and every pty host. Work the
942
+ // host holds for it (a build, a pre-bundle) is seen work too: a dev
943
+ // server whose start passed a minute mid pre-bundle was cut with exit 0.
944
+ if (!isLongRunning && !stillWorking() && heldWork().count === 0 && Date.now() - startTime >= MAX_TOTAL_MS) break;
941
945
  }
942
946
 
943
947
  return { stdout, stderr, exitCode: exitCalled ? exitCode : (typeof proc !== 'undefined' && typeof proc.exitCode === 'number' ? proc.exitCode : 0) };
@@ -1034,6 +1038,9 @@ export function initChildProcess(vfs: VirtualFS): void {
1034
1038
  },
1035
1039
  customCommands: [nodeCommand, npmCommand],
1036
1040
  });
1041
+ // A statement ending in `&` runs in the background, with `$!`, `wait` and
1042
+ // `kill` over the engine's process table (`shell-jobs.ts` says how).
1043
+ installShellJobs(bashInstance, { runTokenOf, runStreamsFor, registerRunStreams, releaseRunStreams });
1037
1044
  shells.set(tree, { bash: bashInstance, adapter: vfsAdapter });
1038
1045
 
1039
1046
  if (processRunnerInstalled) return;
@@ -1755,6 +1762,15 @@ function startChildRun(request: RunRequest): StartedRun {
1755
1762
  const admittedNode = nodeProcessHostInstalled() && engineProgramFor(request.file, request.cwd) === 'node';
1756
1763
  const hostTerminal = request.terminal !== undefined && hostExecutor() !== null && !admittedNode;
1757
1764
  let wakeInput: (() => void) | undefined;
1765
+ /**
1766
+ * A piped fd 0 that stays open for a program the host runs (a program pack,
1767
+ * not a builtin of the engine's shell): what the parent writes reaches it
1768
+ * as it is written, through the same live stream a terminal gets. A
1769
+ * builtin reads its input once, before it runs; a server that speaks
1770
+ * JSON-RPC over stdio (`supercode harness serve`) read that one string,
1771
+ * saw EOF and exited before its client's first request.
1772
+ */
1773
+ let liveInput = false;
1758
1774
  const hostInput: AsyncIterable<Uint8Array> = {
1759
1775
  async *[Symbol.asyncIterator]() {
1760
1776
  while (!finished && !controller.signal.aborted) {
@@ -1794,11 +1810,11 @@ function startChildRun(request: RunRequest): StartedRun {
1794
1810
  }
1795
1811
  };
1796
1812
 
1797
- let streamedOut = 0;
1798
- let streamedErr = 0;
1813
+ const streamedOut: string[] = [];
1814
+ const streamedErr: string[] = [];
1799
1815
  const streams: RunStreams = {
1800
- onStdout: (data: string) => { streamedOut += data.length; request.stdout?.(data); },
1801
- onStderr: (data: string) => { streamedErr += data.length; request.stderr?.(data); },
1816
+ onStdout: (data: string) => { streamedOut.push(data); request.stdout?.(data); },
1817
+ onStderr: (data: string) => { streamedErr.push(data); request.stderr?.(data); },
1802
1818
  signal: controller.signal,
1803
1819
  held: false,
1804
1820
  stdinOpen: request.stdinIsPipe,
@@ -1854,6 +1870,12 @@ function startChildRun(request: RunRequest): StartedRun {
1854
1870
  const begin = (): void => {
1855
1871
  if (started || finished) return;
1856
1872
  started = true;
1873
+ const engineFirst = !hostTerminal && programExists(request.file, request.cwd, request.env);
1874
+ liveInput = !hostTerminal && !admittedNode && request.stdinIsPipe && !engineFirst;
1875
+ if (liveInput) {
1876
+ pendingStdin.unshift(...initialStdin);
1877
+ initialStdin.length = 0;
1878
+ }
1857
1879
  const stdin = initialStdin.length > 0 ? Buffer.concat(initialStdin).toString('utf8') : undefined;
1858
1880
  void (async () => {
1859
1881
  let outcome: CommandOutcome;
@@ -1867,11 +1889,11 @@ function startChildRun(request: RunRequest): StartedRun {
1867
1889
  filesystem: currentVfs!, env, streams, ...(stdin !== undefined ? { stdin } : {}),
1868
1890
  }) : await enterRun(token, () => routeCommand({
1869
1891
  command: __substrateLineFor(request.file, request.args, request.cwd),
1870
- engineFirst: !hostTerminal && programExists(request.file, request.cwd, request.env),
1892
+ engineFirst,
1871
1893
  cwd: request.cwd,
1872
1894
  env: { ...request.env, [PROCESS_TOKEN_ENV]: token },
1873
1895
  stdin,
1874
- ...(hostTerminal ? { stdinStream: hostInput, terminal: request.terminal } : {}),
1896
+ ...(hostTerminal ? { stdinStream: hostInput, terminal: request.terminal } : liveInput ? { stdinStream: hostInput } : {}),
1875
1897
  signal: controller.signal,
1876
1898
  onStdout: streams.onStdout,
1877
1899
  onStderr: streams.onStderr,
@@ -1882,9 +1904,12 @@ function startChildRun(request: RunRequest): StartedRun {
1882
1904
  }
1883
1905
  // A command the engine's shell ran to the end and did not stream --
1884
1906
  // every builtin -- delivers its output here; the `node` command streamed
1885
- // its own as it went, and only what it has not already sent is left.
1886
- if (outcome.stdout.length > streamedOut) request.stdout?.(outcome.stdout.slice(streamedOut));
1887
- if (outcome.stderr.length > streamedErr) request.stderr?.(outcome.stderr.slice(streamedErr));
1907
+ // its own as it went, and only what it has not already sent is left
1908
+ // (`streamed-output.ts` says why that is not a cut at a length).
1909
+ const restOut = unstreamedOutput(outcome.stdout, streamedOut);
1910
+ const restErr = unstreamedOutput(outcome.stderr, streamedErr);
1911
+ if (restOut) request.stdout?.(restOut);
1912
+ if (restErr) request.stderr?.(restErr);
1888
1913
  end(outcome.exitCode, outcome.signal ?? null);
1889
1914
  })();
1890
1915
  };
@@ -1912,18 +1937,20 @@ function startChildRun(request: RunRequest): StartedRun {
1912
1937
  return 0;
1913
1938
  },
1914
1939
  writeStdin(bytes: Uint8Array): void | Promise<void> {
1915
- if (hostTerminal) { pendingStdin.push(bytes); wakeInput?.(); wakeInput = undefined; return; }
1940
+ if (hostTerminal || liveInput) { pendingStdin.push(bytes); wakeInput?.(); wakeInput = undefined; return; }
1916
1941
  if (!started) { initialStdin.push(bytes); return; }
1917
1942
  pendingStdin.push(bytes);
1918
1943
  return flushStdin();
1919
1944
  },
1920
1945
  endStdin(): void {
1921
- if (hostTerminal) { pendingStdin.push(null); wakeInput?.(); wakeInput = undefined; return; }
1946
+ if (hostTerminal || liveInput) { pendingStdin.push(null); wakeInput?.(); wakeInput = undefined; return; }
1922
1947
  if (!started) {
1923
1948
  // The writer closed before the command began: the run's fd 0 is the
1924
1949
  // string already collected and nothing more, so it is not held open.
1925
1950
  streams.stdinOpen = false;
1926
1951
  begin();
1952
+ // A live pipe closed before it began still ends where it was closed.
1953
+ if (liveInput) { pendingStdin.push(null); wakeInput?.(); wakeInput = undefined; }
1927
1954
  return;
1928
1955
  }
1929
1956
  pendingStdin.push(null);
@@ -1951,3 +1978,4 @@ export default {
1951
1978
  runCommand,
1952
1979
  sendStdin,
1953
1980
  };
1981
+
@@ -398,6 +398,16 @@ export interface EsbuildHost {
398
398
  }
399
399
  let currentHost: EsbuildHost | null = null;
400
400
 
401
+ /**
402
+ * esbuild-wasm starts its compiler on a Worker unless told not to. A realm
403
+ * that runs guests may withhold `Worker` (the execution worker confines its
404
+ * guests that way), and there the compiler runs on the calling thread, which
405
+ * is esbuild's own `worker: false`.
406
+ */
407
+ function realmCanStartWorker(): boolean {
408
+ return typeof (globalThis as { Worker?: unknown }).Worker === 'function';
409
+ }
410
+
401
411
  let inlineEsbuild: Promise<typeof import('esbuild-wasm')> | null = null;
402
412
  function ownEsbuild(): Promise<typeof import('esbuild-wasm')> {
403
413
  // A build that brings plugins of its own, a bundler such as tsup does,
@@ -408,7 +418,7 @@ function ownEsbuild(): Promise<typeof import('esbuild-wasm')> {
408
418
  /* @vite-ignore */
409
419
  esbuildModuleURL
410
420
  ).then(async (esbuild) => {
411
- await esbuild.initialize({ wasmURL });
421
+ await esbuild.initialize({ wasmURL, worker: realmCanStartWorker() });
412
422
  return esbuild;
413
423
  }).catch((cause) => {
414
424
  // A load that failed, a page whose server was away for a moment, is not kept: the next build tries again.
@@ -527,6 +537,7 @@ export async function initialize(options?: { wasmURL?: string }): Promise<void>
527
537
 
528
538
  await esbuild.initialize({
529
539
  wasmURL: options?.wasmURL || wasmURL,
540
+ worker: realmCanStartWorker(),
530
541
  });
531
542
 
532
543
  esbuildInstance = esbuild;
@@ -2,6 +2,7 @@
2
2
  * perf_hooks shim - Performance measurement APIs
3
3
  * Wraps browser Performance API
4
4
  */
5
+ import { forGuestRealm } from '../host-globals';
5
6
 
6
7
  export const performance = globalThis.performance || {
7
8
  now: () => Date.now(),
@@ -16,6 +17,23 @@ export const performance = globalThis.performance || {
16
17
  clearResourceTimings: () => {},
17
18
  };
18
19
 
20
+ /**
21
+ * Node's `performance.markResourceTiming`, which a browser's `performance`
22
+ * does not have. undici's fetch reads it off the global `performance` at load
23
+ * and calls it after every response, so each fetch through an undici a
24
+ * program installed (Pi's does) threw once the body was read. Node records a
25
+ * PerformanceResourceTiming entry there; this engine keeps no resource
26
+ * timeline for a guest's own HTTP, so the call records nothing.
27
+ */
28
+ forGuestRealm(() => {
29
+ const own = globalThis.performance as (Performance & { markResourceTiming?: unknown }) | undefined;
30
+ if (own && typeof own.markResourceTiming !== 'function') {
31
+ try {
32
+ Object.defineProperty(own, 'markResourceTiming', { value: () => undefined, writable: true, configurable: true, enumerable: false });
33
+ } catch { /* a performance object that refuses the name keeps its own shape */ }
34
+ }
35
+ });
36
+
19
37
  export class PerformanceObserver {
20
38
  private callback: (list: PerformanceObserverEntryList) => void;
21
39
  private entryTypes: string[] = [];
@@ -363,8 +363,8 @@ function trailingCallback(args: readonly unknown[]): (() => void) | undefined {
363
363
  }
364
364
 
365
365
  /** The signal numbers a guest sees, and the signals a tab must ignore. */
366
- const __substrateSignals: Record<string, number> = { SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGILL: 4, SIGTRAP: 5, SIGABRT: 6, SIGBUS: 7, SIGFPE: 8, SIGKILL: 9, SIGUSR1: 10, SIGSEGV: 11, SIGUSR2: 12, SIGPIPE: 13, SIGALRM: 14, SIGTERM: 15, SIGCHLD: 17, SIGCONT: 18, SIGSTOP: 19, SIGTSTP: 20, SIGTTIN: 21, SIGTTOU: 22, SIGURG: 23, SIGWINCH: 28 };
367
- const __substrateSignalNames: Record<number, string> = Object.fromEntries(Object.entries(__substrateSignals).map(([name, number]) => [number, name]));
366
+ export const __substrateSignals: Record<string, number> = { SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGILL: 4, SIGTRAP: 5, SIGABRT: 6, SIGBUS: 7, SIGFPE: 8, SIGKILL: 9, SIGUSR1: 10, SIGSEGV: 11, SIGUSR2: 12, SIGPIPE: 13, SIGALRM: 14, SIGTERM: 15, SIGCHLD: 17, SIGCONT: 18, SIGSTOP: 19, SIGTSTP: 20, SIGTTIN: 21, SIGTTOU: 22, SIGURG: 23, SIGWINCH: 28 };
367
+ export const __substrateSignalNames: Record<number, string> = Object.fromEntries(Object.entries(__substrateSignals).map(([name, number]) => [number, name]));
368
368
  // Node prints the `process.binding` deprecation once per process; the engine
369
369
  // runs many guests in one realm, so the record is per guest, not per load.
370
370
  const __substrateBindingWarned = new WeakSet<object>();