Skip to content

Commit 5fa52a3

Browse files
committed
fix: support maxFiles without slicing
We currently exceed `maxFiles` and slice the result later. This doesn't work in an iterator since there is no end "result". This change instead makes the walker stop walking when `maxFiles` is met, so we already have the correctly sized result set.
1 parent d2089d3 commit 5fa52a3

8 files changed

Lines changed: 72 additions & 50 deletions

File tree

__tests__/fdir.test.ts

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
import { fdir, IterableOutput } from "../src/index";
1+
import { fdir } from "../src/index";
22
import fs from "fs";
33
import mock from "mock-fs";
4-
import { test, beforeEach, TestContext, vi } from "vitest";
4+
import { test, beforeEach, vi } from "vitest";
55
import path, { sep } from "path";
66
import { convertSlashes } from "../src/utils";
77
import picomatch from "picomatch";
8-
import { apiTypes, APITypes, cwd, restricted, root } from "./utils";
9-
import { APIBuilder } from "../src/builder/api-builder";
8+
import { apiTypes, APITypes, cwd, restricted, root, execute } from "./utils";
109

1110
beforeEach(() => {
1211
mock.restore();
@@ -26,22 +25,6 @@ test(`crawl single depth directory with callback`, (t) => {
2625
});
2726
});
2827

29-
async function execute<T extends IterableOutput>(
30-
api: APIBuilder<T>,
31-
type: APITypes
32-
): Promise<T> {
33-
let files: T[number][] = [];
34-
35-
if (type === "withIterator") {
36-
for await (const file of api[type]()) {
37-
files.push(file);
38-
}
39-
} else {
40-
files = await api[type]();
41-
}
42-
return files as T;
43-
}
44-
4528
async function crawl(type: APITypes, path: string) {
4629
const api = new fdir().crawl(path);
4730
return execute(api, type);
@@ -270,7 +253,7 @@ for (const type of apiTypes) {
270253
.onlyDirs()
271254
.filter((path) => path.includes("api"))
272255
.crawl("./src");
273-
const result = await api[type]();
256+
const result = await execute(api, type);
274257
t.expect(result).toHaveLength(2);
275258
});
276259

__tests__/symlinks.test.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { afterAll, beforeAll, beforeEach, describe, test } from "vitest";
2-
import { apiTypes, normalize, root } from "./utils";
1+
import { afterAll, beforeAll, describe, test } from "vitest";
2+
import { apiTypes, normalize, root, execute } from "./utils";
33
import mock from "mock-fs";
4-
import { fdir, Options } from "../src";
4+
import { fdir } from "../src";
55
import path from "path";
66

77
const fsWithRelativeSymlinks = {
@@ -154,7 +154,7 @@ for (const type of apiTypes) {
154154

155155
test(`resolve symlinks`, async (t) => {
156156
const api = new fdir().withSymlinks().crawl("/some/dir");
157-
const files = await api[type]();
157+
const files = await execute(api, type);
158158
t.expect(files.sort()).toStrictEqual(
159159
normalize([
160160
"/other/dir/file-2",
@@ -166,7 +166,7 @@ for (const type of apiTypes) {
166166

167167
test(`resolve recursive symlinks`, async (t) => {
168168
const api = new fdir().withSymlinks().crawl("/recursive");
169-
const files = await api[type]();
169+
const files = await execute(api, type);
170170
t.expect(files.sort()).toStrictEqual(
171171
normalize([
172172
"/double/recursive/another-file",
@@ -184,7 +184,7 @@ for (const type of apiTypes) {
184184
const api = new fdir()
185185
.withSymlinks({ resolvePaths: false })
186186
.crawl("/recursive");
187-
const files = await api[type]();
187+
const files = await execute(api, type);
188188
t.expect(files.sort()).toStrictEqual(
189189
normalize([
190190
"/recursive/dir/not-recursive/another-file",
@@ -234,7 +234,7 @@ for (const type of apiTypes) {
234234
.withRelativePaths()
235235
.withErrors()
236236
.crawl("./recursive");
237-
const files = await api[type]();
237+
const files = await execute(api, type);
238238
t.expect(files.sort()).toStrictEqual(
239239
normalize([
240240
"dir/not-recursive/another-file",
@@ -284,7 +284,7 @@ for (const type of apiTypes) {
284284
.withRelativePaths()
285285
.withErrors()
286286
.crawl("./recursive");
287-
const files = await api[type]();
287+
const files = await execute(api, type);
288288
t.expect(files.sort()).toStrictEqual(
289289
normalize([
290290
"..//double/recursive/another-file",
@@ -302,7 +302,7 @@ for (const type of apiTypes) {
302302
const api = new fdir()
303303
.withSymlinks({ resolvePaths: false })
304304
.crawl("/some/dir");
305-
const files = await api[type]();
305+
const files = await execute(api, type);
306306
t.expect(files.sort()).toStrictEqual(
307307
normalize([
308308
"/some/dir/dirSymlink/file-1",
@@ -317,7 +317,7 @@ for (const type of apiTypes) {
317317
.withSymlinks({ resolvePaths: false })
318318
.withRelativePaths()
319319
.crawl("/some/dir");
320-
const files = await api[type]();
320+
const files = await execute(api, type);
321321
t.expect(files.sort()).toStrictEqual(
322322
normalize([
323323
"dirSymlink/file-1",
@@ -332,7 +332,7 @@ for (const type of apiTypes) {
332332
.withSymlinks()
333333
.withRelativePaths()
334334
.crawl("./relative/dir");
335-
const files = await api[type]();
335+
const files = await execute(api, type);
336336
t.expect(files.sort()).toStrictEqual(
337337
normalize([
338338
"../../../../other-relative/dir/file-2",
@@ -347,7 +347,7 @@ for (const type of apiTypes) {
347347
.withSymlinks()
348348
.exclude((_name, path) => path === resolveSymlinkRoot("/sym/linked/"))
349349
.crawl("/some/dir");
350-
const files = await api[type]();
350+
const files = await execute(api, type);
351351
t.expect(files.sort()).toStrictEqual(normalize(["/other/dir/file-2"]));
352352
});
353353

@@ -358,31 +358,31 @@ for (const type of apiTypes) {
358358
(_name, path) => path === resolveSymlinkRoot("/some/dir/dirSymlink/")
359359
)
360360
.crawl("/some/dir");
361-
const files = await api[type]();
361+
const files = await execute(api, type);
362362
t.expect(files.sort()).toStrictEqual(
363363
normalize(["/some/dir/fileSymlink"])
364364
);
365365
});
366366

367367
test(`do not resolve symlinks`, async (t) => {
368368
const api = new fdir().crawl("/some/dir");
369-
const files = await api[type]();
369+
const files = await execute(api, type);
370370
t.expect(files.sort()).toStrictEqual(
371371
normalize(["dirSymlink", "fileSymlink", "fileSymlink2"])
372372
);
373373
});
374374

375375
test(`exclude symlinks`, async (t) => {
376376
const api = new fdir({ excludeSymlinks: true }).crawl("/some/dir");
377-
const files = await api[type]();
377+
const files = await execute(api, type);
378378
t.expect(files).toHaveLength(0);
379379
});
380380

381381
test(
382382
"doesn't hang when resolving symlinks in the root directory",
383383
async (t) => {
384384
const api = new fdir().withSymlinks({ resolvePaths: false }).crawl("/");
385-
const files = await api[type]();
385+
const files = await execute(api, type);
386386
const expectedFiles = normalize(["/lib/file-1", "/usr/lib/file-1"]);
387387
for (const expectedFile of expectedFiles) {
388388
t.expect(files).toContain(expectedFile);

__tests__/utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import path from "path";
2+
import type { APIBuilder } from "../src/builder/api-builder";
3+
import type { IterableOutput } from "../src/types";
24

35
export type APITypes = (typeof apiTypes)[number];
46
export const apiTypes = ["withPromise", "sync", "withIterator"] as const;
@@ -22,3 +24,19 @@ export function normalize(paths: string[]) {
2224
path.isAbsolute(p) ? path.resolve(p) : path.normalize(p)
2325
);
2426
}
27+
28+
export async function execute<T extends IterableOutput>(
29+
api: APIBuilder<T>,
30+
type: APITypes
31+
): Promise<T> {
32+
let files: T[number][] = [];
33+
34+
if (type === "withIterator") {
35+
for await (const file of api[type]()) {
36+
files.push(file);
37+
}
38+
} else {
39+
files = await api[type]();
40+
}
41+
return files as T;
42+
}

src/api/functions/push-directory.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,53 @@
1-
import { FilterPredicate, Options } from "../../types";
1+
import { FilterPredicate, Options, Counts } from "../../types";
22

33
export type PushDirectoryFunction = (
44
directoryPath: string,
55
paths: string[],
66
pushPath: (path: string, arr: string[]) => void,
7+
counts: Counts,
78
filters?: FilterPredicate[]
89
) => void;
910

1011
function pushDirectoryWithRelativePath(root: string): PushDirectoryFunction {
11-
return function (directoryPath, paths, pushPath) {
12+
return function (directoryPath, paths, pushPath, counts) {
1213
pushPath(directoryPath.substring(root.length) || ".", paths);
14+
counts.directories++;
1315
};
1416
}
1517

1618
function pushDirectoryFilterWithRelativePath(
1719
root: string
1820
): PushDirectoryFunction {
19-
return function (directoryPath, paths, pushPath, filters) {
21+
return function (directoryPath, paths, pushPath, counts, filters) {
2022
const relativePath = directoryPath.substring(root.length) || ".";
2123
if (filters!.every((filter) => filter(relativePath, true))) {
2224
pushPath(relativePath, paths);
25+
counts.directories++;
2326
}
2427
};
2528
}
2629

2730
const pushDirectory: PushDirectoryFunction = (
2831
directoryPath,
2932
paths,
30-
pushPath
33+
pushPath,
34+
counts
3135
) => {
3236
pushPath(directoryPath || ".", paths);
37+
counts.directories++;
3338
};
3439

3540
const pushDirectoryFilter: PushDirectoryFunction = (
3641
directoryPath,
3742
paths,
3843
pushPath,
44+
counts,
3945
filters
4046
) => {
4147
const path = directoryPath || ".";
4248
if (filters!.every((filter) => filter(path, true))) {
4349
pushPath(path, paths);
50+
counts.directories++;
4451
}
4552
};
4653

src/api/functions/push-file.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@ const pushFileFilter: PushFileFunction = (
2222
filename,
2323
paths,
2424
pushPath,
25-
_counts,
25+
counts,
2626
filters
2727
) => {
28-
if (filters!.every((filter) => filter(filename, false)))
28+
if (filters!.every((filter) => filter(filename, false))) {
2929
pushPath(filename, paths);
30+
counts.files++;
31+
}
3032
};
3133

3234
const pushFileCount: PushFileFunction = (
@@ -39,8 +41,9 @@ const pushFileCount: PushFileFunction = (
3941
counts.files++;
4042
};
4143

42-
const pushFile: PushFileFunction = (filename, paths, pushPath) => {
44+
const pushFile: PushFileFunction = (filename, paths, pushPath, counts) => {
4345
pushPath(filename, paths);
46+
counts.files++;
4447
};
4548

4649
const empty: PushFileFunction = () => {};

src/api/functions/walk-directory.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ const walkAsync: WalkDirectoryFunction = (
2323
if (currentDepth < 0) return state.queue.dequeue(null, state);
2424

2525
state.visited.push(crawlPath);
26-
state.counts.directories++;
2726

2827
// Perf: Node >= 10 introduced withFileTypes that helps us
2928
// skip an extra fs.stat call.
@@ -43,7 +42,6 @@ const walkSync: WalkDirectoryFunction = (
4342
) => {
4443
if (currentDepth < 0) return;
4544
state.visited.push(crawlPath);
46-
state.counts.directories++;
4745

4846
let entries: fs.Dirent[] = [];
4947
try {

src/api/iterator.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ class WalkerIterator<TOutput extends IterableOutput> {
66
#walker: Walker<TOutput>;
77
#currentGroup?: string[];
88
#queue: TOutput[number][] = [];
9+
#error?: unknown;
910

1011
public constructor(root: string, options: Options) {
1112
const pushPath = options.group ? this.#pushPath : this.#pushResult;
@@ -34,9 +35,12 @@ class WalkerIterator<TOutput extends IterableOutput> {
3435
}
3536
};
3637

37-
#onComplete = () => {
38+
#onComplete = (err: unknown) => {
3839
this.#currentGroup = undefined;
3940
this.#complete = true;
41+
if (err) {
42+
this.#error = err;
43+
}
4044
if (this.#resolver) {
4145
const resolver = this.#resolver;
4246
this.#resolver = undefined;
@@ -51,6 +55,10 @@ class WalkerIterator<TOutput extends IterableOutput> {
5155
yield* this.#queue;
5256
this.#queue = [];
5357

58+
if (this.#error) {
59+
throw this.#error;
60+
}
61+
5462
if (this.#complete) {
5563
return;
5664
}

src/api/walker.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export class Walker<TOutput extends Output> {
8484
this.root,
8585
this.state.paths,
8686
this.pushPath,
87+
this.state.counts,
8788
this.state.options.filters
8889
);
8990
this.walkDirectory(
@@ -98,7 +99,7 @@ export class Walker<TOutput extends Output> {
9899

99100
private walk = (entries: Dirent[], directoryPath: string, depth: number) => {
100101
const {
101-
paths,
102+
counts,
102103
options: {
103104
filters,
104105
resolveSymlinks,
@@ -115,12 +116,16 @@ export class Walker<TOutput extends Output> {
115116
if (
116117
controller.signal.aborted ||
117118
(signal && signal.aborted) ||
118-
(maxFiles && paths.length > maxFiles)
119+
(maxFiles && counts.directories + counts.files > maxFiles)
119120
)
120121
return;
121122

122123
const files = this.getArray(this.state.paths);
123124
for (let i = 0; i < entries.length; ++i) {
125+
if (maxFiles && counts.directories + counts.files >= maxFiles) {
126+
break;
127+
}
128+
124129
const entry = entries[i];
125130

126131
if (
@@ -142,7 +147,7 @@ export class Walker<TOutput extends Output> {
142147
this.state.options.pathSeparator
143148
);
144149
if (exclude && exclude(entry.name, path)) continue;
145-
this.pushDirectory(path, files, this.pushPath, filters);
150+
this.pushDirectory(path, files, this.pushPath, counts, filters);
146151
this.walkDirectory(this.state, path, path, depth - 1, this.walk);
147152
} else if (this.resolveSymlink && entry.isSymbolicLink()) {
148153
let path = joinPath.joinPathWithBasePath(entry.name, directoryPath);

0 commit comments

Comments
 (0)