Skip to content

Commit

Permalink
feat(fs/unstable): add readFile and readFileSync (#6394)
Browse files Browse the repository at this point in the history
  • Loading branch information
jbronder authored Feb 12, 2025
1 parent b7c76d5 commit a0a756f
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 0 deletions.
1 change: 1 addition & 0 deletions _tools/node_test_runner/run_test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import "../../collections/zip_test.ts";
import "../../fs/unstable_link_test.ts";
import "../../fs/unstable_make_temp_dir_test.ts";
import "../../fs/unstable_read_dir_test.ts";
import "../../fs/unstable_read_file_test.ts";
import "../../fs/unstable_read_link_test.ts";
import "../../fs/unstable_real_path_test.ts";
import "../../fs/unstable_rename_test.ts";
Expand Down
1 change: 1 addition & 0 deletions fs/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"./unstable-lstat": "./unstable_lstat.ts",
"./unstable-make-temp-dir": "./unstable_make_temp_dir.ts",
"./unstable-read-dir": "./unstable_read_dir.ts",
"./unstable-read-file": "./unstable_read_file.ts",
"./unstable-read-link": "./unstable_read_link.ts",
"./unstable-real-path": "./unstable_real_path.ts",
"./unstable-rename": "./unstable_rename.ts",
Expand Down
75 changes: 75 additions & 0 deletions fs/unstable_read_file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright 2018-2025 the Deno authors. MIT license.

import { getNodeFs, isDeno } from "./_utils.ts";
import type { ReadFileOptions } from "./unstable_types.ts";
import { mapError } from "./_map_error.ts";

/**
* Reads and resolves to the entire contents of a file as an array of bytes.
* `TextDecoder` can be used to transform the bytes to string if required.
*
* Requires `allow-read` permission.
*
* @example Usage
* ```ts
* import { readFile } from "@std/fs/unstable-read-file";
* const decoder = new TextDecoder("utf-8");
* const data = await readFile("README.md");
* console.log(decoder.decode(data));
* ```
*
* @tags allow-read
*
* @param path The path to the file.
* @param options Options when reading a file. See {@linkcode ReadFileOptions}.
* @returns A promise that resolves to a `Uint8Array` of the file contents.
*/
export async function readFile(
path: string | URL,
options?: ReadFileOptions,
): Promise<Uint8Array> {
if (isDeno) {
return Deno.readFile(path, { ...options });
} else {
const { signal } = options ?? {};
try {
const buf = await getNodeFs().promises.readFile(path, { signal });
return new Uint8Array(buf.buffer, buf.byteOffset, buf.length);
} catch (error) {
throw mapError(error);
}
}
}

/**
* Synchronously reads and returns the entire contents of a file as an array
* of bytes. `TextDecoder` can be used to transform the bytes to string if
* required.
*
* Requires `allow-read` permission.
*
* @example Usage
* ```ts
* import { readFileSync } from "@std/fs/unstable-read-file";
* const decoder = new TextDecoder("utf-8");
* const data = readFileSync("README.md");
* console.log(decoder.decode(data));
* ```
*
* @tags allow-read
*
* @param path The path to the file.
* @returns A `Uint8Array` of bytes representing the file contents.
*/
export function readFileSync(path: string | URL): Uint8Array {
if (isDeno) {
return Deno.readFileSync(path);
} else {
try {
const buf = getNodeFs().readFileSync(path);
return new Uint8Array(buf.buffer, buf.byteOffset, buf.length);
} catch (error) {
throw mapError(error);
}
}
}
119 changes: 119 additions & 0 deletions fs/unstable_read_file_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright 2018-2025 the Deno authors. MIT license.

import {
assert,
assertEquals,
assertRejects,
assertThrows,
unreachable,
} from "@std/assert";
import { readFile, readFileSync } from "./unstable_read_file.ts";
import { NotFound } from "./unstable_errors.js";
import { isDeno } from "./_utils.ts";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const moduleDir = dirname(fileURLToPath(import.meta.url));
const testdataDir = resolve(moduleDir, "testdata");
const testFile = join(testdataDir, "copy_file.txt");

Deno.test("readFile() reads a file", async () => {
const decoder = new TextDecoder("utf-8");
const data = await readFile(testFile);

assert(data.byteLength > 0);
assertEquals(decoder.decode(data), "txt");
});

Deno.test("readFile() is called repeatedly", async () => {
for (let i = 0; i < 256; i++) {
await readFile(testFile);
}
});

Deno.test("readFile() rejects with Error when reading a file path to a directory", async () => {
await assertRejects(async () => {
await readFile(testdataDir);
}, Error);
});

Deno.test("readFile() handles an AbortSignal", async () => {
const ac = new AbortController();
queueMicrotask(() => ac.abort());

const error = await assertRejects(async () => {
await readFile(testFile, { signal: ac.signal });
}, Error);
assertEquals(error.name, "AbortError");
});

Deno.test("readFile() handles an AbortSignal with a reason", async () => {
const ac = new AbortController();
const reasonErr = new Error();
queueMicrotask(() => ac.abort(reasonErr));

const error = await assertRejects(async () => {
await readFile(testFile, { signal: ac.signal });
}, Error);

if (isDeno) {
assertEquals(error, ac.signal.reason);
} else {
assertEquals(error.cause, ac.signal.reason);
}
});

Deno.test("readFile() handles an AbortSignal with a primitive reason value", async () => {
const ac = new AbortController();
const reasonErr = "Some string";
queueMicrotask(() => ac.abort(reasonErr));

try {
await readFile(testFile, { signal: ac.signal });
unreachable();
} catch (error) {
if (isDeno) {
assertEquals(error, ac.signal.reason);
} else {
const errorValue = error as Error;
assertEquals(errorValue.cause, ac.signal.reason);
}
}
});

Deno.test("readFile() handles cleanup of an AbortController", async () => {
const ac = new AbortController();
await readFile(testFile, { signal: ac.signal });
});

Deno.test("readFile() rejects with NotFound when reading from a non-existent file", async () => {
await assertRejects(async () => {
await readFile("non-existent-file.txt");
}, NotFound);
});

Deno.test("readFileSync() reads a file", () => {
const decoder = new TextDecoder("utf-8");
const data = readFileSync(testFile);

assert(data.byteLength > 0);
assertEquals(decoder.decode(data), "txt");
});

Deno.test("readFileSync() is called repeatedly", () => {
for (let i = 0; i < 256; i++) {
readFileSync(testFile);
}
});

Deno.test("readFileSync() throws with Error when reading a file path to a directory", () => {
assertThrows(() => {
readFileSync(testdataDir);
}, Error);
});

Deno.test("readFileSync() throws with NotFound when reading from a non-existent file", () => {
assertThrows(() => {
readFileSync("non-existent-file.txt");
}, NotFound);
});
11 changes: 11 additions & 0 deletions fs/unstable_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,17 @@ export interface SymlinkOptions {
type: "file" | "dir" | "junction";
}

/**
* Options which can be set when using {@linkcode readFile} or
* {@linkcode readTextFile}.
*/
export interface ReadFileOptions {
/** An abort signal to allow cancellation of the file read operation. If the
* signal becomes aborted the readFile operation will be stopped and the
* promise returned will be rejected with an AbortError. */
signal?: AbortSignal;
}

/**
* Options which can be set when using {@linkcode makeTempDir},
* {@linkcode makeTempDirSync}, {@linkcode makeTempFile}, and
Expand Down

0 comments on commit a0a756f

Please # to comment.