Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(fs/unstable): add readDir #6338

Merged
merged 10 commits into from
Jan 14, 2025
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 @@ -49,6 +49,7 @@ import "../../collections/union_test.ts";
import "../../collections/unzip_test.ts";
import "../../collections/without_all_test.ts";
import "../../collections/zip_test.ts";
import "../../fs/unstable_read_dir_test.ts";
import "../../fs/unstable_stat_test.ts";
import "../../fs/unstable_lstat_test.ts";

Expand Down
12 changes: 12 additions & 0 deletions fs/_to_dir_entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright 2018-2025 the Deno authors. MIT license.

import type { DirEntry } from "./unstable_types.ts";

export function toDirEntry(s: import("node:fs").Dirent): DirEntry {
return {
name: s.name,
isFile: s.isFile(),
isDirectory: s.isDirectory(),
isSymlink: s.isSymbolicLink(),
};
}
1 change: 1 addition & 0 deletions fs/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"./expand-glob": "./expand_glob.ts",
"./move": "./move.ts",
"./unstable-lstat": "./unstable_lstat.ts",
"./unstable-read-dir": "./unstable_read_dir.ts",
"./unstable-stat": "./unstable_stat.ts",
"./unstable-types": "./unstable_types.ts",
"./walk": "./walk.ts"
Expand Down
50 changes: 50 additions & 0 deletions fs/unstable_read_dir.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright 2018-2025 the Deno authors. MIT license.

import { getNodeFsPromises, isDeno } from "./_utils.ts";
import { mapError } from "./_map_error.ts";
import { toDirEntry } from "./_to_dir_entry.ts";
import type { DirEntry } from "./unstable_types.ts";

async function* getDirEntries(path: string | URL): AsyncIterable<DirEntry> {
const fsPromises = getNodeFsPromises();
try {
const dir = await fsPromises.opendir(path);
for await (const entry of dir) {
const dirEntry = toDirEntry(entry);
yield dirEntry;
}
} catch (error) {
throw error;
}
}

/** Reads the directory given by `path` and returns an async iterable of
* {@linkcode DirEntry}. The order of entries is not guaranteed.
*
* ```ts
* import { readDir } from "@std/fs/unstable-read-dir";
*
* for await (const dirEntry of readDir("/")) {
* console.log(dirEntry.name);
* }
* ```
*
* Throws error if `path` is not a directory.
*
* Requires `allow-read` permission.
*
* @tags allow-read
* @category File System
*/
export function readDir(path: string | URL): AsyncIterable<DirEntry> {
if (isDeno) {
return Deno.readDir(path);
} else {
try {
const dirEntries = getDirEntries(path);
return dirEntries;
} catch (error) {
throw mapError(error);
}
}
}
41 changes: 41 additions & 0 deletions fs/unstable_read_dir_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright 2018-2025 the Deno authors. MIT license.

import { assert, assertEquals, assertRejects } from "@std/assert";
import { fromFileUrl, join, resolve } from "@std/path";
import { readDir } from "./unstable_read_dir.ts";
import { NotFound } from "./unstable_errors.js";

const testdataDir = resolve(fromFileUrl(import.meta.url), "../testdata");

Deno.test("readDir() reads from the directory and its subdirectories", async () => {
const files = [];
for await (const e of readDir(testdataDir)) {
files.push(e);
}

let counter = 0;
for (const f of files) {
if (f.name === "walk") {
assert(f.isDirectory);
counter++;
}
}

assertEquals(counter, 1);
});

Deno.test("readDir() rejects when the path is not a directory", async () => {
await assertRejects(async () => {
const testFile = join(testdataDir, "0.ts");
await readDir(testFile)[Symbol.asyncIterator]().next();
}, Error);
});

Deno.test("readDir() rejects when the directory does not exist", async () => {
await assertRejects(
async () => {
await readDir("non_existent_dir")[Symbol.asyncIterator]().next();
},
NotFound,
);
});
19 changes: 19 additions & 0 deletions fs/unstable_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,22 @@ export interface FileInfo {
* _Linux/Mac OS only._ */
isSocket: boolean | null;
}

/**
* Information about a directory entry returned from {@linkcode readDir}
* and {@linkcode readDirSync}.
*/
export interface DirEntry {
kt3k marked this conversation as resolved.
Show resolved Hide resolved
/** The file name of the entry. It is just the entity name and does not
* include the full path. */
name: string;
/** True if this is info for a regular file. Mutually exclusive to
* `FileInfo.isDirectory` and `FileInfo.isSymlink`. */
isFile: boolean;
/** True if this is info for a regular directory. Mutually exclusive to
* `FileInfo.isFile` and `FileInfo.isSymlink`. */
isDirectory: boolean;
/** True if this is info for a symlink. Mutually exclusive to
* `FileInfo.isFile` and `FileInfo.isDirectory`. */
isSymlink: boolean;
}
Loading