Repository: denoland/deno
Pull request: fix(ext/node): handle partial writes to process.stdout/stderr

## Summary

- Fix silent data truncation when piping >64KB through `process.stdout` in Node compat mode
- The `_write` handler in `createWritableStdioStream` discarded the return value of `writer.writeSync()`, assuming all bytes were always written. When stdout is a pipe and the OS buffer is near capacity (~64KB), `writeSync` returns a short count and the remaining bytes were silently lost.
- The fix loops on partial writes until the entire buffer is flushed.

Closes #26784

## Test plan

- Added `stdoutPipePartialWriteNotTruncated`: verifies `fs.createReadStream().pipe(process.stdout)` with >64KB does not truncate
- Added `stdoutWriteMultipleChunksNotTruncated`: verifies multiple `process.stdout.write()` calls totaling >64KB are not lost
- `cargo test unit_node::child_process_test`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Changed files:
filename: ext/node/polyfills/_process/streams.mjs
patch: @@ -47,11 +47,18 @@ export function createWritableStdioStream(writer, name, warmup = false) {
       // being created from a raw fd (new Socket({ fd: 1 })), process.stdout/stderr
       // should be switched to net.Socket for non-TTY cases and this can be removed.
       try {
-        writer.writeSync(
-          ObjectPrototypeIsPrototypeOf(Uint8ArrayPrototype, buf)
-            ? buf
-            : Buffer.from(buf, enc),
-        );
+        let data = ObjectPrototypeIsPrototypeOf(Uint8ArrayPrototype, buf)
+          ? buf
+          : Buffer.from(buf, enc);
+        // Handle partial writes - writeSync may not write all bytes at once
+        // (e.g., when stdout is a pipe and the pipe buffer is near capacity).
+        // deno-lint-ignore prefer-primordials
+        while (data.byteLength > 0) {
+          const nwritten = writer.writeSync(data);
+          // deno-lint-ignore prefer-primordials
+          if (nwritten >= data.byteLength) break;
+          data = TypedArrayPrototypeSlice(data, nwritten);
+        }
       } catch (e) {
         if (
           ObjectPrototypeIsPrototypeOf(Deno.errors.BrokenPipe.prototype, e)

filename: tests/unit_node/child_process_test.ts
patch: @@ -1278,3 +1278,51 @@ Deno.test(async function experimentalFlag() {
 
   await timeout.promise;
 });
+
+// Regression test for https://github.com/denoland/deno/issues/26784
+// process.stdout partial writes were silently dropped, causing data
+// truncation when piping >64KB through a subprocess.
+Deno.test(async function stdoutPipePartialWriteNotTruncated() {
+  // Child script: pipe a file to stdout via createReadStream
+  const pipeScript = await Deno.makeTempFile({ suffix: ".mjs" });
+  await Deno.writeTextFile(
+    pipeScript,
+    `import fs from "node:fs";fs.createReadStream(process.argv[2]).pipe(process.stdout);`,
+  );
+  // Create a file >65536 bytes with multiple lines (triggers chunked reads)
+  const dataFile = await Deno.makeTempFile();
+  const content = "x".repeat(40000) + "\n" + "y".repeat(40000) + "\n";
+  await Deno.writeTextFile(dataFile, content);
+  try {
+    const output = execFileSync(Deno.execPath(), [
+      "run",
+      "--allow-read",
+      pipeScript,
+      dataFile,
+    ], { encoding: "utf-8", maxBuffer: 50 * 1024 * 1024 });
+    assertEquals(output.length, content.length);
+    assertEquals(output, content);
+  } finally {
+    await Deno.remove(pipeScript);
+    await Deno.remove(dataFile);
+  }
+});
+
+Deno.test(async function stdoutWriteMultipleChunksNotTruncated() {
+  // Child script: write two large chunks to stdout directly
+  const script = await Deno.makeTempFile({ suffix: ".mjs" });
+  await Deno.writeTextFile(
+    script,
+    `const chunk1 = "A".repeat(50000);const chunk2 = "B".repeat(50000);process.stdout.write(chunk1);process.stdout.write(chunk2);`,
+  );
+  try {
+    const output = execFileSync(Deno.execPath(), ["run", script], {
+      encoding: "utf-8",
+      maxBuffer: 50 * 1024 * 1024,
+    });
+    assertEquals(output.length, 100000);
+    assertEquals(output, "A".repeat(50000) + "B".repeat(50000));
+  } finally {
+    await Deno.remove(script);
+  }
+});