Repository: denoland/deno
Pull request: feat: allow passing a ReadableStream to Deno.writeFile/Deno.writeTextFile

Closes #13229

Changed files:
filename: cli/tests/unit/write_file_test.ts
patch: @@ -393,3 +393,19 @@ function pathExists(path: string | URL) {
     return false;
   }
 }
+
+Deno.test(
+  { permissions: { read: true, write: true } },
+  async function writeFileStream() {
+    const stream = new ReadableStream({
+      pull(controller) {
+        controller.enqueue(new Uint8Array([1]));
+        controller.enqueue(new Uint8Array([2]));
+        controller.close();
+      },
+    });
+    const filename = Deno.makeTempDirSync() + "/test.txt";
+    await Deno.writeFile(filename, stream);
+    assertEquals(Deno.readFileSync(filename), new Uint8Array([1, 2]));
+  },
+);

filename: cli/tests/unit/write_text_file_test.ts
patch: @@ -198,3 +198,19 @@ Deno.test(
     assertEquals(Deno.readTextFileSync(filename), "Hello");
   },
 );
+
+Deno.test(
+  { permissions: { read: true, write: true } },
+  async function writeTextFileStream() {
+    const stream = new ReadableStream({
+      pull(controller) {
+        controller.enqueue("Hello");
+        controller.enqueue("World");
+        controller.close();
+      },
+    });
+    const filename = Deno.makeTempDirSync() + "/test.txt";
+    await Deno.writeTextFile(filename, stream);
+    assertEquals(Deno.readTextFileSync(filename), "HelloWorld");
+  },
+);

filename: cli/tsc/dts/lib.deno.ns.d.ts
patch: @@ -3364,7 +3364,7 @@ declare namespace Deno {
    */
   export function writeFile(
     path: string | URL,
-    data: Uint8Array,
+    data: Uint8Array | ReadableStream<Uint8Array>,
     options?: WriteFileOptions,
   ): Promise<void>;
 
@@ -3407,7 +3407,7 @@ declare namespace Deno {
    */
   export function writeTextFile(
     path: string | URL,
-    data: string,
+    data: string | ReadableStream<string>,
     options?: WriteFileOptions,
   ): Promise<void>;
 

filename: runtime/js/40_write_file.js
patch: @@ -5,6 +5,9 @@
   const ops = core.ops;
   const { abortSignal } = window.__bootstrap;
   const { pathFromURL } = window.__bootstrap.util;
+  const { open } = window.__bootstrap.files;
+  const { ReadableStreamPrototype } = window.__bootstrap.streams;
+  const { ObjectPrototypeIsPrototypeOf } = window.__bootstrap.primordials;
 
   function writeFileSync(
     path,
@@ -36,16 +39,29 @@
       options.signal[abortSignal.add](abortHandler);
     }
     try {
-      await core.opAsync(
-        "op_write_file_async",
-        pathFromURL(path),
-        options.mode,
-        options.append ?? false,
-        options.create ?? true,
-        options.createNew ?? false,
-        data,
-        cancelRid,
-      );
+      if (ObjectPrototypeIsPrototypeOf(ReadableStreamPrototype, data)) {
+        const file = await open(path, {
+          mode: options.mode,
+          append: options.append ?? false,
+          create: options.create ?? true,
+          createNew: options.createNew ?? false,
+          write: true,
+        });
+        await data.pipeTo(file.writable, {
+          signal: options.signal,
+        });
+      } else {
+        await core.opAsync(
+          "op_write_file_async",
+          pathFromURL(path),
+          options.mode,
+          options.append ?? false,
+          options.create ?? true,
+          options.createNew ?? false,
+          data,
+          cancelRid,
+        );
+      }
     } finally {
       if (options.signal) {
         options.signal[abortSignal.remove](abortHandler);
@@ -70,8 +86,16 @@
     data,
     options = {},
   ) {
-    const encoder = new TextEncoder();
-    return writeFile(path, encoder.encode(data), options);
+    if (ObjectPrototypeIsPrototypeOf(ReadableStreamPrototype, data)) {
+      return writeFile(
+        path,
+        data.pipeThrough(new TextEncoderStream()),
+        options,
+      );
+    } else {
+      const encoder = new TextEncoder();
+      return writeFile(path, encoder.encode(data), options);
+    }
   }
 
   window.__bootstrap.writeFile = {