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: support custom fs #136

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions __tests__/fdir.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,30 @@ for (const type of apiTypes) {
.crawl("node_modules");
t.expect(globFunction).toHaveBeenCalledWith(["**/*.js"], "bleep");
});

test(`[${type}] using custom fs implementation`, async (t) => {
const readdirStub = vi.fn<Parameters<typeof fs.readdir>>((_path, _opts, cb) => {
cb(null, []);
});
const readdirSyncStub = vi.fn();
readdirSyncStub.mockReturnValue([]);
const fakeFs = {
...fs,
readdir: readdirStub,
readdirSync: readdirSyncStub
} as unknown as typeof fs;

const api = new fdir({
fs: fakeFs
})
.crawl("node_modules");
await api[type]();
if (type === 'withPromise') {
t.expect(readdirStub).toHaveBeenCalled();
} else {
t.expect(readdirSyncStub).toHaveBeenCalled();
}
});
}

test(`[async] crawl directory & use abort signal to abort`, async (t) => {
Expand Down
6 changes: 4 additions & 2 deletions src/api/functions/resolve-symlink.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import fs from "fs";
import type {Stats} from "fs";
import { WalkerState, Options } from "../../types";
import { dirname } from "path";

export type ResolveSymlinkFunction = (
path: string,
state: WalkerState,
callback: (stat: fs.Stats, path: string) => void
callback: (stat: Stats, path: string) => void
) => void;

const resolveSymlinksAsync: ResolveSymlinkFunction = function (
Expand All @@ -15,6 +15,7 @@ const resolveSymlinksAsync: ResolveSymlinkFunction = function (
) {
const {
queue,
fs,
options: { suppressErrors },
} = state;
queue.enqueue();
Expand All @@ -41,6 +42,7 @@ const resolveSymlinks: ResolveSymlinkFunction = function (
) {
const {
queue,
fs,
options: { suppressErrors },
} = state;
queue.enqueue();
Expand Down
8 changes: 5 additions & 3 deletions src/api/functions/walk-directory.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { WalkerState } from "../../types";
import fs from "fs";
import type { Dirent } from "fs";

export type WalkDirectoryFunction = (
state: WalkerState,
crawlPath: string,
directoryPath: string,
depth: number,
callback: (entries: fs.Dirent[], directoryPath: string, depth: number) => void
callback: (entries: Dirent[], directoryPath: string, depth: number) => void
) => void;

const readdirOpts = { withFileTypes: true } as const;
Expand All @@ -18,6 +18,7 @@ const walkAsync: WalkDirectoryFunction = (
currentDepth,
callback
) => {
const { fs } = state;
if (currentDepth < 0) return state.queue.dequeue(null, state);

state.visited.push(crawlPath);
Expand All @@ -40,11 +41,12 @@ const walkSync: WalkDirectoryFunction = (
currentDepth,
callback
) => {
const { fs } = state;
if (currentDepth < 0) return;
state.visited.push(crawlPath);
state.counts.directories++;

let entries: fs.Dirent[] = [];
let entries: Dirent[] = [];
try {
entries = fs.readdirSync(crawlPath || ".", readdirOpts);
} catch (e) {
Expand Down
4 changes: 3 additions & 1 deletion src/api/walker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import * as resolveSymlink from "./functions/resolve-symlink";
import * as invokeCallback from "./functions/invoke-callback";
import * as walkDirectory from "./functions/walk-directory";
import { Queue } from "./queue";
import { Dirent } from "fs";
import type { Dirent } from "fs";
import * as nativeFs from "fs";
import { Output } from "../types";
import { Counter } from "./counter";

Expand Down Expand Up @@ -48,6 +49,7 @@ export class Walker<TOutput extends Output> {
),
symlinks: new Map(),
visited: [""].slice(0, 0),
fs: options.fs ?? nativeFs
};

/*
Expand Down
12 changes: 12 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Queue } from "./api/queue";
import type * as nativeFs from "fs";

export type Counts = {
files: number;
Expand All @@ -25,13 +26,23 @@ export type PathsOutput = string[];

export type Output = OnlyCountsOutput | PathsOutput | GroupOutput;

export type FSLike = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is purposely explicit rather than just FSLike = typeof nativeFs since the fs module has all sorts of exports (both types and actual exports) which we don't want to have to stub out to satisfy the type when bringing our own fs-like object

readdir: typeof nativeFs.readdir,
readdirSync: typeof nativeFs.readdirSync,
realpath: typeof nativeFs.realpath,
realpathSync: typeof nativeFs.realpathSync,
stat: typeof nativeFs.stat,
statSync: typeof nativeFs.statSync,
};

export type WalkerState = {
root: string;
paths: string[];
groups: Group[];
counts: Counts;
options: Options;
queue: Queue;
fs: FSLike;

symlinks: Map<string, string>;
visited: string[];
Expand Down Expand Up @@ -65,6 +76,7 @@ export type Options<TGlobFunction = unknown> = {
pathSeparator: PathSeparator;
signal?: AbortSignal;
globFunction?: TGlobFunction;
fs?: FSLike
};

export type GlobMatcher = (test: string) => boolean;
Expand Down
Loading