Repository: denoland/deno
Pull request: fix(ext/node): add `openAsBlob` export to `node:fs`

## Summary
- Implements `fs.openAsBlob()` which reads a file and returns a `Promise<Blob>`, matching the Node.js API
- Adds the function to both named and default exports of `node:fs`
- Adds unit tests covering basic usage, the `type` option, and error handling for non-existent files

Closes #31036

## Test plan
- [x] `cargo test unit_node::fs_test` passes
- [x] Manual verification: `import { openAsBlob } from "node:fs"` works correctly
- [x] Lint and format checks pass

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

Changed files:
filename: ext/node/polyfills/fs.ts
patch: @@ -141,9 +141,19 @@ import {
 } from "ext:deno_node/internal/fs/streams.mjs";
 import {
   Dirent,
+  getValidatedPath,
   toUnixTimestamp as _toUnixTimestamp,
 } from "ext:deno_node/internal/fs/utils.mjs";
 import { glob, globPromise, globSync } from "ext:deno_node/_fs/_fs_glob.ts";
+import {
+  validateObject,
+  validateString,
+} from "ext:deno_node/internal/validators.mjs";
+import type { Buffer } from "node:buffer";
+import { op_fs_read_file_async } from "ext:core/ops";
+import { primordials } from "ext:core/mod.js";
+
+const { PromisePrototypeThen } = primordials;
 
 const {
   F_OK,
@@ -166,6 +176,23 @@ const {
   O_EXCL,
 } = constants;
 
+/**
+ * Returns a `Blob` whose data is read from the given file.
+ */
+function openAsBlob(
+  path: string | Buffer | URL,
+  options: { type?: string } = { __proto__: null },
+): Promise<Blob> {
+  validateObject(options, "options");
+  const type = options.type || "";
+  validateString(type, "options.type");
+  path = getValidatedPath(path);
+  return PromisePrototypeThen(
+    op_fs_read_file_async(path as string, undefined, 0),
+    (data: Uint8Array) => new Blob([data], { type }),
+  );
+}
+
 const promises = {
   access: accessPromise,
   constants,
@@ -270,6 +297,7 @@ export default {
   O_TRUNC,
   O_WRONLY,
   open,
+  openAsBlob,
   openSync,
   opendir,
   opendirSync,
@@ -391,6 +419,7 @@ export {
   O_TRUNC,
   O_WRONLY,
   open,
+  openAsBlob,
   opendir,
   opendirSync,
   openSync,

filename: tests/unit_node/fs_test.ts
patch: @@ -23,6 +23,7 @@ import {
   fchownSync,
   lstatSync,
   mkdtempSync,
+  openAsBlob,
   openSync,
   promises,
   promises as fsPromises,
@@ -492,3 +493,38 @@ Deno.test("[node/fs] constants are correct across platforms", () => {
     assert(constants.O_SYMLINK === undefined);
   }
 });
+
+Deno.test(
+  "[node/fs openAsBlob] returns a Blob with file contents",
+  async () => {
+    const filename = mkdtempSync(join(tmpdir(), "foo-")) + "/test.txt";
+    const data = "Hello, openAsBlob!";
+    writeFileSync(filename, data);
+
+    const blob = await openAsBlob(filename);
+    assertEquals(blob instanceof Blob, true);
+    assertEquals(await blob.text(), data);
+    assertEquals(blob.type, "");
+  },
+);
+
+Deno.test(
+  "[node/fs openAsBlob] respects type option",
+  async () => {
+    const filename = mkdtempSync(join(tmpdir(), "foo-")) + "/test.txt";
+    writeFileSync(filename, "content");
+
+    const blob = await openAsBlob(filename, { type: "text/plain" });
+    assertEquals(blob.type, "text/plain");
+    assertEquals(await blob.text(), "content");
+  },
+);
+
+Deno.test(
+  "[node/fs openAsBlob] rejects for non-existent file",
+  async () => {
+    await assertRejects(
+      () => openAsBlob("/non/existent/file.txt"),
+    );
+  },
+);