@volter/tabnode 0.5.14 → 0.5.16

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.
@@ -3,6 +3,16 @@ import { Buffer as NodeBuffer } from '../buffer-module';
3
3
  /** The key the engine hangs a run's filesystem off its guest process. */
4
4
  export declare const kRunFilesystem: unique symbol;
5
5
  export declare function withFilesystem<T>(tree: VirtualFS, work: () => T): T;
6
+ /**
7
+ * Where a child's output goes when its stdio names a descriptor of the
8
+ * parent's (`stdio: ['ignore', fd, fd]` with `fd` from `openSync`): the file
9
+ * that descriptor has open, as the child's own duplicate of it, so it keeps
10
+ * writing after the parent closes its copy. Appending descriptors append;
11
+ * others write on from the position the parent's copy had. A descriptor that
12
+ * is a stream (a socket, a pipe) is written as a stream. Anything else is
13
+ * not a sink, and the child's output there goes nowhere, as `ignore` does.
14
+ */
15
+ export declare function descriptorWriter(fd: number): ((text: string) => void) | null;
6
16
  /** A request object `fs.js` hands an asynchronous call, answered on a tick. */
7
17
  interface FSReq {
8
18
  oncomplete?: (error: Error | null, ...rest: unknown[]) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/tabnode",
3
- "version": "0.5.14",
3
+ "version": "0.5.16",
4
4
  "description": "Node's own library, in a browser tab.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -168,6 +168,40 @@ function fileFor(fd: number): OpenFile {
168
168
  return file;
169
169
  }
170
170
 
171
+ /**
172
+ * Where a child's output goes when its stdio names a descriptor of the
173
+ * parent's (`stdio: ['ignore', fd, fd]` with `fd` from `openSync`): the file
174
+ * that descriptor has open, as the child's own duplicate of it, so it keeps
175
+ * writing after the parent closes its copy. Appending descriptors append;
176
+ * others write on from the position the parent's copy had. A descriptor that
177
+ * is a stream (a socket, a pipe) is written as a stream. Anything else is
178
+ * not a sink, and the child's output there goes nowhere, as `ignore` does.
179
+ */
180
+ export function descriptorWriter(fd: number): ((text: string) => void) | null {
181
+ const stream = handleForFd(fd);
182
+ if (stream instanceof LibuvStreamWrap) {
183
+ return (text) => { try { stream.writeBuffer(new WriteWrap(), new TextEncoder().encode(text)); } catch { /* a closed stream drops the child's output, as a closed pipe does */ } };
184
+ }
185
+ const file = openFiles.get(fd);
186
+ if (!file || file.directory || (file.flags & 3) === 0) return null;
187
+ const { tree, path } = file;
188
+ const append = (file.flags & flagBits().append) !== 0;
189
+ let position = file.position;
190
+ return (text) => {
191
+ const bytes = new TextEncoder().encode(text);
192
+ // the parent's copy, while it is open, holds the file's bytes as it last wrote them
193
+ const shared = openFiles.get(fd) === file ? file : null;
194
+ const existing = shared?.cached ?? (tree.existsSync(path) ? tree.readFileSync(path) as Uint8Array : new Uint8Array(0));
195
+ const at = append ? existing.length : position;
196
+ const next = new Uint8Array(Math.max(existing.length, at + bytes.length));
197
+ next.set(existing, 0);
198
+ next.set(bytes, at);
199
+ tree.writeFileSync(path, next);
200
+ position = at + bytes.length;
201
+ if (shared) shared.cached = next;
202
+ };
203
+ }
204
+
171
205
  /** Node's stat array: eighteen numbers, with each time a second and a nanosecond. */
172
206
  function statArray(stats: VfsStats, bigint: boolean, path?: string): Float64Array | BigInt64Array {
173
207
  const mtime = stats.mtime instanceof Date ? stats.mtime.getTime() : Number(stats.mtimeMs ?? 0);
@@ -29,6 +29,7 @@ import {
29
29
  } from './handles';
30
30
  import { __runFor, mintPid, type ProcessToken } from '../../process-tokens';
31
31
  import { handleForFd } from './fds';
32
+ import { descriptorWriter } from './fs';
32
33
  import { TTY, type TerminalState } from './tty_wrap';
33
34
 
34
35
  /** One entry of Node's `options.stdio`, as `getValidStdio` builds it. */
@@ -178,7 +179,8 @@ function spawningDirectory(token: ProcessToken | null): string | undefined {
178
179
 
179
180
  /** Where an `inherit` entry's bytes go: the spawning program's own stream. */
180
181
  function inheritedWriter(fd: number, token: ProcessToken | null): ((text: string) => void) | null {
181
- if (fd !== 1 && fd !== 2) return null;
182
+ // a descriptor of the parent's own (a log file it opened) is written as the file
183
+ if (fd !== 1 && fd !== 2) return descriptorWriter(fd);
182
184
  // Inherit the descriptor's original sink, not a guest replacement of
183
185
  // process.stderr.write (a child commonly redirects its console over IPC).
184
186
  if (token !== null) {
@@ -774,7 +774,9 @@ export class ServerBridge extends EventEmitter {
774
774
  const virtualServer = this.servers.get(port);
775
775
  if (!virtualServer) return false;
776
776
  const bodyBuffer = body ? Buffer.from(new Uint8Array(body)) : undefined;
777
- if (virtualServer.server === null) {
777
+ // A guest's listener (the old null sentinel, now a request adapter) is
778
+ // streamed off its connection; its adapter would answer whole.
779
+ if (virtualServer.server === null || this.guestServers.has(virtualServer.server)) {
778
780
  const named = Object.keys(headers).some((name) => name.toLowerCase() === 'host');
779
781
  const authority = `${virtualServer.hostname && virtualServer.hostname !== '0.0.0.0' && virtualServer.hostname !== '::' ? virtualServer.hostname : '127.0.0.1'}:${port}`;
780
782
  await __streamOverLoopback(
@@ -839,7 +841,7 @@ export class ServerBridge extends EventEmitter {
839
841
 
840
842
  // A guest's server is a port: its answer is streamed off the connection
841
843
  // as it arrives, which is what the page reads chunk by chunk.
842
- if (virtualServer.server === null) {
844
+ if (virtualServer.server === null || this.guestServers.has(virtualServer.server)) {
843
845
  const bodyBuffer = body ? Buffer.from(new Uint8Array(body)) : undefined;
844
846
  const named = Object.keys(headers).some((name) => name.toLowerCase() === 'host');
845
847
  const authority = `${virtualServer.hostname && virtualServer.hostname !== '0.0.0.0' && virtualServer.hostname !== '::' ? virtualServer.hostname : '127.0.0.1'}:${port}`;
@@ -1011,6 +1011,10 @@ export function initChildProcess(vfs: VirtualFS): void {
1011
1011
  case 'ls':
1012
1012
  case 'list':
1013
1013
  return handleNpmList(ctx);
1014
+ case 'init':
1015
+ case 'create':
1016
+ case 'innit':
1017
+ return handleNpmInit(args.slice(1), ctx);
1014
1018
  default:
1015
1019
  return {
1016
1020
  stdout: '',
@@ -1289,6 +1293,46 @@ async function handleNpmInstall(args: string[], ctx: CommandContext): Promise<Ju
1289
1293
  }
1290
1294
  }
1291
1295
 
1296
+ /**
1297
+ * `npm init -y`: npm's defaults written to package.json, the fields a
1298
+ * package.json already has kept, and the file printed as npm prints it. The
1299
+ * questionnaire without `-y` needs a person at npm's prompts; it is refused by
1300
+ * name, as is an initializer (`npm init <pkg>`), which runs another package.
1301
+ */
1302
+ async function handleNpmInit(args: string[], ctx: CommandContext): Promise<JustBashExecResult> {
1303
+ const yes = args.includes('-y') || args.includes('--yes');
1304
+ const initializer = args.find((a) => !a.startsWith('-'));
1305
+ if (initializer) {
1306
+ 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 };
1307
+ }
1308
+ if (!yes) {
1309
+ 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 };
1310
+ }
1311
+ const vfs = currentVfs!;
1312
+ const file = __resolvePath(ctx.cwd, 'package.json');
1313
+ let existing: Record<string, unknown> = {};
1314
+ if (vfs.existsSync(file)) {
1315
+ try { existing = JSON.parse(String(vfs.readFileSync(file, 'utf8'))) as Record<string, unknown>; }
1316
+ catch { return { stdout: '', stderr: `npm ERR! ${file} is not valid JSON\n`, exitCode: 1 }; }
1317
+ }
1318
+ const base = ctx.cwd.split('/').filter(Boolean).pop() ?? 'package';
1319
+ const name = base.toLowerCase().replace(/^[._]+/, '').replace(/[^a-z0-9._~-]+/g, '-') || 'package';
1320
+ const manifest = {
1321
+ name,
1322
+ version: '1.0.0',
1323
+ description: '',
1324
+ main: 'index.js',
1325
+ scripts: { test: 'echo "Error: no test specified" && exit 1' },
1326
+ keywords: [],
1327
+ author: '',
1328
+ license: 'ISC',
1329
+ ...existing,
1330
+ };
1331
+ const text = `${JSON.stringify(manifest, null, 2)}\n`;
1332
+ vfs.writeFileSync(file, text);
1333
+ return { stdout: `Wrote to ${file}:\n\n${text}\n\n`, stderr: '', exitCode: 0 };
1334
+ }
1335
+
1292
1336
  /**
1293
1337
  * Handle `npm ls` — list installed packages
1294
1338
  */