@volter/tabnode 0.5.15 → 0.5.17

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.
@@ -936,8 +936,10 @@ export function initChildProcess(vfs: VirtualFS): void {
936
936
  // engine's own registries. A run that still holds a handle or a timer
937
937
  // is not that -- Node's loop runs while one is open, for as long as it
938
938
  // 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;
939
+ // a server, which is every extension host and every pty host. Work the
940
+ // host holds for it (a build, a pre-bundle) is seen work too: a dev
941
+ // server whose start passed a minute mid pre-bundle was cut with exit 0.
942
+ if (!isLongRunning && !stillWorking() && heldWork().count === 0 && Date.now() - startTime >= MAX_TOTAL_MS) break;
941
943
  }
942
944
 
943
945
  return { stdout, stderr, exitCode: exitCalled ? exitCode : (typeof proc !== 'undefined' && typeof proc.exitCode === 'number' ? proc.exitCode : 0) };
@@ -1011,6 +1013,10 @@ export function initChildProcess(vfs: VirtualFS): void {
1011
1013
  case 'ls':
1012
1014
  case 'list':
1013
1015
  return handleNpmList(ctx);
1016
+ case 'init':
1017
+ case 'create':
1018
+ case 'innit':
1019
+ return handleNpmInit(args.slice(1), ctx);
1014
1020
  default:
1015
1021
  return {
1016
1022
  stdout: '',
@@ -1289,6 +1295,46 @@ async function handleNpmInstall(args: string[], ctx: CommandContext): Promise<Ju
1289
1295
  }
1290
1296
  }
1291
1297
 
1298
+ /**
1299
+ * `npm init -y`: npm's defaults written to package.json, the fields a
1300
+ * package.json already has kept, and the file printed as npm prints it. The
1301
+ * questionnaire without `-y` needs a person at npm's prompts; it is refused by
1302
+ * name, as is an initializer (`npm init <pkg>`), which runs another package.
1303
+ */
1304
+ async function handleNpmInit(args: string[], ctx: CommandContext): Promise<JustBashExecResult> {
1305
+ const yes = args.includes('-y') || args.includes('--yes');
1306
+ const initializer = args.find((a) => !a.startsWith('-'));
1307
+ if (initializer) {
1308
+ return { stdout: '', stderr: `npm ERR! npm init ${initializer} runs an initializer package, which this engine's npm does not; write package.json, or run \`npm init -y\`.\n`, exitCode: 1 };
1309
+ }
1310
+ if (!yes) {
1311
+ return { stdout: '', stderr: 'npm ERR! npm init asks its questions at a prompt, which this engine\'s npm does not; `npm init -y` writes the defaults.\n', exitCode: 1 };
1312
+ }
1313
+ const vfs = currentVfs!;
1314
+ const file = __resolvePath(ctx.cwd, 'package.json');
1315
+ let existing: Record<string, unknown> = {};
1316
+ if (vfs.existsSync(file)) {
1317
+ try { existing = JSON.parse(String(vfs.readFileSync(file, 'utf8'))) as Record<string, unknown>; }
1318
+ catch { return { stdout: '', stderr: `npm ERR! ${file} is not valid JSON\n`, exitCode: 1 }; }
1319
+ }
1320
+ const base = ctx.cwd.split('/').filter(Boolean).pop() ?? 'package';
1321
+ const name = base.toLowerCase().replace(/^[._]+/, '').replace(/[^a-z0-9._~-]+/g, '-') || 'package';
1322
+ const manifest = {
1323
+ name,
1324
+ version: '1.0.0',
1325
+ description: '',
1326
+ main: 'index.js',
1327
+ scripts: { test: 'echo "Error: no test specified" && exit 1' },
1328
+ keywords: [],
1329
+ author: '',
1330
+ license: 'ISC',
1331
+ ...existing,
1332
+ };
1333
+ const text = `${JSON.stringify(manifest, null, 2)}\n`;
1334
+ vfs.writeFileSync(file, text);
1335
+ return { stdout: `Wrote to ${file}:\n\n${text}\n\n`, stderr: '', exitCode: 0 };
1336
+ }
1337
+
1292
1338
  /**
1293
1339
  * Handle `npm ls` — list installed packages
1294
1340
  */
@@ -1711,6 +1757,15 @@ function startChildRun(request: RunRequest): StartedRun {
1711
1757
  const admittedNode = nodeProcessHostInstalled() && engineProgramFor(request.file, request.cwd) === 'node';
1712
1758
  const hostTerminal = request.terminal !== undefined && hostExecutor() !== null && !admittedNode;
1713
1759
  let wakeInput: (() => void) | undefined;
1760
+ /**
1761
+ * A piped fd 0 that stays open for a program the host runs (a program pack,
1762
+ * not a builtin of the engine's shell): what the parent writes reaches it
1763
+ * as it is written, through the same live stream a terminal gets. A
1764
+ * builtin reads its input once, before it runs; a server that speaks
1765
+ * JSON-RPC over stdio (`supercode harness serve`) read that one string,
1766
+ * saw EOF and exited before its client's first request.
1767
+ */
1768
+ let liveInput = false;
1714
1769
  const hostInput: AsyncIterable<Uint8Array> = {
1715
1770
  async *[Symbol.asyncIterator]() {
1716
1771
  while (!finished && !controller.signal.aborted) {
@@ -1810,6 +1865,12 @@ function startChildRun(request: RunRequest): StartedRun {
1810
1865
  const begin = (): void => {
1811
1866
  if (started || finished) return;
1812
1867
  started = true;
1868
+ const engineFirst = !hostTerminal && programExists(request.file, request.cwd, request.env);
1869
+ liveInput = !hostTerminal && !admittedNode && request.stdinIsPipe && !engineFirst;
1870
+ if (liveInput) {
1871
+ pendingStdin.unshift(...initialStdin);
1872
+ initialStdin.length = 0;
1873
+ }
1813
1874
  const stdin = initialStdin.length > 0 ? Buffer.concat(initialStdin).toString('utf8') : undefined;
1814
1875
  void (async () => {
1815
1876
  let outcome: CommandOutcome;
@@ -1823,11 +1884,11 @@ function startChildRun(request: RunRequest): StartedRun {
1823
1884
  filesystem: currentVfs!, env, streams, ...(stdin !== undefined ? { stdin } : {}),
1824
1885
  }) : await enterRun(token, () => routeCommand({
1825
1886
  command: __substrateLineFor(request.file, request.args, request.cwd),
1826
- engineFirst: !hostTerminal && programExists(request.file, request.cwd, request.env),
1887
+ engineFirst,
1827
1888
  cwd: request.cwd,
1828
1889
  env: { ...request.env, [PROCESS_TOKEN_ENV]: token },
1829
1890
  stdin,
1830
- ...(hostTerminal ? { stdinStream: hostInput, terminal: request.terminal } : {}),
1891
+ ...(hostTerminal ? { stdinStream: hostInput, terminal: request.terminal } : liveInput ? { stdinStream: hostInput } : {}),
1831
1892
  signal: controller.signal,
1832
1893
  onStdout: streams.onStdout,
1833
1894
  onStderr: streams.onStderr,
@@ -1868,18 +1929,20 @@ function startChildRun(request: RunRequest): StartedRun {
1868
1929
  return 0;
1869
1930
  },
1870
1931
  writeStdin(bytes: Uint8Array): void | Promise<void> {
1871
- if (hostTerminal) { pendingStdin.push(bytes); wakeInput?.(); wakeInput = undefined; return; }
1932
+ if (hostTerminal || liveInput) { pendingStdin.push(bytes); wakeInput?.(); wakeInput = undefined; return; }
1872
1933
  if (!started) { initialStdin.push(bytes); return; }
1873
1934
  pendingStdin.push(bytes);
1874
1935
  return flushStdin();
1875
1936
  },
1876
1937
  endStdin(): void {
1877
- if (hostTerminal) { pendingStdin.push(null); wakeInput?.(); wakeInput = undefined; return; }
1938
+ if (hostTerminal || liveInput) { pendingStdin.push(null); wakeInput?.(); wakeInput = undefined; return; }
1878
1939
  if (!started) {
1879
1940
  // The writer closed before the command began: the run's fd 0 is the
1880
1941
  // string already collected and nothing more, so it is not held open.
1881
1942
  streams.stdinOpen = false;
1882
1943
  begin();
1944
+ // A live pipe closed before it began still ends where it was closed.
1945
+ if (liveInput) { pendingStdin.push(null); wakeInput?.(); wakeInput = undefined; }
1883
1946
  return;
1884
1947
  }
1885
1948
  pendingStdin.push(null);
@@ -1907,3 +1970,4 @@ export default {
1907
1970
  runCommand,
1908
1971
  sendStdin,
1909
1972
  };
1973
+
@@ -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[] = [];
@@ -308,6 +308,7 @@ function base64End(this: StringDecoder, buf?: unknown): string {
308
308
  // ---------------------------------------------------------------------------
309
309
 
310
310
  function simpleWrite(this: StringDecoder, buf: unknown): string {
311
+ if (typeof buf === 'string') return buf;
311
312
  return decoderInput(this, buf).toString(this.encoding as BufferEncoding);
312
313
  }
313
314
 
@@ -353,6 +354,10 @@ const decoderPrototype = StringDecoderImpl.prototype as unknown as StringDecoder
353
354
 
354
355
  decoderPrototype.write = function write(this: unknown, buffer: unknown): string {
355
356
  const self = assertDecoder(this);
357
+ // Node returns a string as it is given: readline decodes every chunk of its
358
+ // input, and an input stream with an encoding (`createReadStream(path,
359
+ // 'utf8')`) hands it strings.
360
+ if (typeof buffer === 'string') return buffer;
356
361
  const buf = decoderInput(self, buffer);
357
362
  if (buf.length === 0) return '';
358
363
  let r: string | undefined;
package/src/shims/tls.ts CHANGED
@@ -102,6 +102,17 @@ const DEFAULT_MIN_VERSION = 'TLSv1.2';
102
102
 
103
103
  const rootCertificates: string[] = [];
104
104
 
105
+ // Node's certificate-store door, read by callers that build their own agents
106
+ // (VS Code's proxy agent, on every fetch it patches). The engine holds no
107
+ // store: certificates are the browser's, so every kind answers the same empty
108
+ // set `rootCertificates` does.
109
+ const getCACertificates = (type: string = 'default'): string[] => {
110
+ if (!['default', 'system', 'bundled', 'extra'].includes(type)) {
111
+ throw Object.assign(new TypeError(`The argument 'type' must be one of: 'default', 'system', 'bundled', 'extra'. Received '${String(type)}'`), { code: 'ERR_INVALID_ARG_VALUE' });
112
+ }
113
+ return [];
114
+ };
115
+
105
116
  return {
106
117
  TLSSocket,
107
118
  Server,
@@ -109,6 +120,7 @@ return {
109
120
  connect,
110
121
  createSecureContext,
111
122
  getCiphers,
123
+ getCACertificates,
112
124
  DEFAULT_ECDH_CURVE,
113
125
  DEFAULT_MAX_VERSION,
114
126
  DEFAULT_MIN_VERSION,
@@ -117,7 +129,7 @@ return {
117
129
 
118
130
  }
119
131
  const tls = createTlsModule();
120
- export const { TLSSocket, Server, createServer, connect, createSecureContext, getCiphers,
132
+ export const { TLSSocket, Server, createServer, connect, createSecureContext, getCiphers, getCACertificates,
121
133
  DEFAULT_ECDH_CURVE, DEFAULT_MAX_VERSION, DEFAULT_MIN_VERSION, rootCertificates } = tls;
122
134
  export type TLSSocket = InstanceType<typeof TLSSocket>;
123
135
  export type Server = InstanceType<typeof Server>;
@@ -88,6 +88,23 @@ export function markAsUntransferable(object: unknown): void {
88
88
  // No-op
89
89
  }
90
90
 
91
+ /**
92
+ * Node 22's `markAsUncloneable` and its query: an object so marked is refused
93
+ * by `structuredClone` and `postMessage`. undici's WebIDL layer marks every
94
+ * `CacheStorage`, `Request` and `Response` it makes, at construction, so a
95
+ * program that loads undici 7.x+ or 8.x died on `markAsUncloneable is not a
96
+ * function` before its first line. The mark is kept; this engine's message
97
+ * channels do not yet read it, so a marked object that is posted is copied
98
+ * rather than refused.
99
+ */
100
+ const uncloneable = new WeakSet<object>();
101
+ export function markAsUncloneable(object: unknown): void {
102
+ if (object !== null && (typeof object === 'object' || typeof object === 'function')) uncloneable.add(object as object);
103
+ }
104
+ export function isMarkedAsUncloneable(object: unknown): boolean {
105
+ return object !== null && (typeof object === 'object' || typeof object === 'function') && uncloneable.has(object as object);
106
+ }
107
+
91
108
  export function getEnvironmentData(key: unknown): unknown {
92
109
  return undefined;
93
110
  }
@@ -109,6 +126,8 @@ export default {
109
126
  receiveMessageOnPort,
110
127
  SHARE_ENV,
111
128
  markAsUntransferable,
129
+ markAsUncloneable,
130
+ isMarkedAsUncloneable,
112
131
  getEnvironmentData,
113
132
  setEnvironmentData,
114
133
  };