Skip to content

Commit 7d7a762

Browse files
avivkellertargos
authored andcommitted
fs: allow 'withFileTypes' to be used with globs
PR-URL: #52837 Reviewed-By: Moshe Atlow <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Mohammed Keyvanzadeh <[email protected]>
1 parent 0bbc62c commit 7d7a762

File tree

4 files changed

+92
-16
lines changed

4 files changed

+92
-16
lines changed

doc/api/fs.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1073,6 +1073,10 @@ behavior is similar to `cp dir1/ dir2/`.
10731073
10741074
<!-- YAML
10751075
added: v22.0.0
1076+
changes:
1077+
- version: REPLACEME
1078+
pr-url: https://github.com/nodejs/node/pull/52837
1079+
description: Add support for `withFileTypes` as an option.
10761080
-->
10771081
10781082
> Stability: 1 - Experimental
@@ -1082,6 +1086,8 @@ added: v22.0.0
10821086
* `cwd` {string} current working directory. **Default:** `process.cwd()`
10831087
* `exclude` {Function} Function to filter out files/directories. Return
10841088
`true` to exclude the item, `false` to include it. **Default:** `undefined`.
1089+
* `withFileTypes` {boolean} `true` if the glob should return paths as Dirents,
1090+
`false` otherwise. **Default:** `false`.
10851091
* Returns: {AsyncIterator} An AsyncIterator that yields the paths of files
10861092
that match the pattern.
10871093
@@ -3109,6 +3115,10 @@ descriptor. See [`fs.utimes()`][].
31093115
31103116
<!-- YAML
31113117
added: v22.0.0
3118+
changes:
3119+
- version: REPLACEME
3120+
pr-url: https://github.com/nodejs/node/pull/52837
3121+
description: Add support for `withFileTypes` as an option.
31123122
-->
31133123
31143124
> Stability: 1 - Experimental
@@ -3119,6 +3129,8 @@ added: v22.0.0
31193129
* `cwd` {string} current working directory. **Default:** `process.cwd()`
31203130
* `exclude` {Function} Function to filter out files/directories. Return
31213131
`true` to exclude the item, `false` to include it. **Default:** `undefined`.
3132+
* `withFileTypes` {boolean} `true` if the glob should return paths as Dirents,
3133+
`false` otherwise. **Default:** `false`.
31223134
31233135
* `callback` {Function}
31243136
* `err` {Error}
@@ -5603,6 +5615,10 @@ Synchronous version of [`fs.futimes()`][]. Returns `undefined`.
56035615
56045616
<!-- YAML
56055617
added: v22.0.0
5618+
changes:
5619+
- version: REPLACEME
5620+
pr-url: https://github.com/nodejs/node/pull/52837
5621+
description: Add support for `withFileTypes` as an option.
56065622
-->
56075623
56085624
> Stability: 1 - Experimental
@@ -5612,6 +5628,8 @@ added: v22.0.0
56125628
* `cwd` {string} current working directory. **Default:** `process.cwd()`
56135629
* `exclude` {Function} Function to filter out files/directories. Return
56145630
`true` to exclude the item, `false` to include it. **Default:** `undefined`.
5631+
* `withFileTypes` {boolean} `true` if the glob should return paths as Dirents,
5632+
`false` otherwise. **Default:** `false`.
56155633
* Returns: {string\[]} paths of files that match the pattern.
56165634
56175635
```mjs

lib/internal/fs/glob.js

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const {
1616

1717
const { lstatSync, readdirSync } = require('fs');
1818
const { lstat, readdir } = require('fs/promises');
19-
const { join, resolve } = require('path');
19+
const { join, resolve, basename, isAbsolute } = require('path');
2020

2121
const {
2222
kEmptyObject,
@@ -27,6 +27,7 @@ const {
2727
validateString,
2828
validateStringArray,
2929
} = require('internal/validators');
30+
const { DirentFromStats } = require('internal/fs/utils');
3031

3132
let minimatch;
3233
function lazyMinimatch() {
@@ -37,6 +38,14 @@ function lazyMinimatch() {
3738
const isWindows = process.platform === 'win32';
3839
const isOSX = process.platform === 'darwin';
3940

41+
async function getDirent(path) {
42+
return new DirentFromStats(basename(path), await lstat(path), path);
43+
}
44+
45+
function getDirentSync(path) {
46+
return new DirentFromStats(basename(path), lstatSync(path), path);
47+
}
48+
4049
class Cache {
4150
#cache = new SafeMap();
4251
#statsCache = new SafeMap();
@@ -47,7 +56,7 @@ class Cache {
4756
if (cached) {
4857
return cached;
4958
}
50-
const promise = PromisePrototypeThen(lstat(path), null, () => null);
59+
const promise = PromisePrototypeThen(getDirent(path), null, () => null);
5160
this.#statsCache.set(path, promise);
5261
return promise;
5362
}
@@ -58,7 +67,7 @@ class Cache {
5867
}
5968
let val;
6069
try {
61-
val = lstatSync(path);
70+
val = getDirentSync(path);
6271
} catch {
6372
val = null;
6473
}
@@ -175,14 +184,16 @@ class Glob {
175184
#queue = [];
176185
#subpatterns = new SafeMap();
177186
#patterns;
187+
#withFileTypes;
178188
constructor(pattern, options = kEmptyObject) {
179189
validateObject(options, 'options');
180-
const { exclude, cwd } = options;
190+
const { exclude, cwd, withFileTypes } = options;
181191
if (exclude != null) {
182192
validateFunction(exclude, 'options.exclude');
183193
}
184194
this.#root = cwd ?? '.';
185195
this.#exclude = exclude;
196+
this.#withFileTypes = !!withFileTypes;
186197
let patterns;
187198
if (typeof pattern === 'object') {
188199
validateStringArray(pattern, 'patterns');
@@ -222,7 +233,14 @@ class Glob {
222233
.forEach((patterns, path) => ArrayPrototypePush(this.#queue, { __proto__: null, path, patterns }));
223234
this.#subpatterns.clear();
224235
}
225-
return ArrayFrom(this.#results);
236+
return ArrayFrom(
237+
this.#results,
238+
this.#withFileTypes ? (path) => this.#cache.statSync(
239+
isAbsolute(path) ?
240+
path :
241+
join(this.#root, path),
242+
) : undefined,
243+
);
226244
}
227245
#addSubpattern(path, pattern) {
228246
if (!this.#subpatterns.has(path)) {
@@ -317,7 +335,7 @@ class Glob {
317335
const fromSymlink = pattern.symlinks.has(index);
318336

319337
if (current === lazyMinimatch().GLOBSTAR) {
320-
if (entry.name[0] === '.' || (this.#exclude && this.#exclude(entry.name))) {
338+
if (entry.name[0] === '.' || (this.#exclude && this.#exclude(this.#withFileTypes ? entry : entry.name))) {
321339
continue;
322340
}
323341
if (!fromSymlink && entry.isDirectory()) {
@@ -460,7 +478,7 @@ class Glob {
460478
const result = join(path, p);
461479
if (!this.#results.has(result)) {
462480
this.#results.add(result);
463-
yield result;
481+
yield this.#withFileTypes ? stat : result;
464482
}
465483
}
466484
if (pattern.indexes.size === 1 && pattern.indexes.has(last)) {
@@ -472,7 +490,7 @@ class Glob {
472490
// if path is ".", add it only if pattern starts with "." or pattern is exactly "**"
473491
if (!this.#results.has(path)) {
474492
this.#results.add(path);
475-
yield path;
493+
yield this.#withFileTypes ? stat : path;
476494
}
477495
}
478496

@@ -522,7 +540,7 @@ class Glob {
522540
// If ** is last, add to results
523541
if (!this.#results.has(entryPath)) {
524542
this.#results.add(entryPath);
525-
yield entryPath;
543+
yield this.#withFileTypes ? entry : entryPath;
526544
}
527545
}
528546

@@ -533,7 +551,7 @@ class Glob {
533551
// If next pattern is the last one, add to results
534552
if (!this.#results.has(entryPath)) {
535553
this.#results.add(entryPath);
536-
yield entryPath;
554+
yield this.#withFileTypes ? entry : entryPath;
537555
}
538556
} else if (nextMatches && entry.isDirectory()) {
539557
// Pattern mached, meaning two patterns forward
@@ -569,14 +587,14 @@ class Glob {
569587
this.#cache.add(path, pattern.child(new SafeSet().add(nextIndex)));
570588
if (!this.#results.has(path)) {
571589
this.#results.add(path);
572-
yield path;
590+
yield this.#withFileTypes ? this.#cache.statSync(fullpath) : path;
573591
}
574592
}
575593
if (!this.#cache.seen(path, pattern, nextIndex) || !this.#cache.seen(parent, pattern, nextIndex)) {
576594
this.#cache.add(parent, pattern.child(new SafeSet().add(nextIndex)));
577595
if (!this.#results.has(parent)) {
578596
this.#results.add(parent);
579-
yield parent;
597+
yield this.#withFileTypes ? this.#cache.statSync(join(this.#root, parent)) : parent;
580598
}
581599
}
582600
}
@@ -592,7 +610,7 @@ class Glob {
592610
if (nextIndex === last) {
593611
if (!this.#results.has(entryPath)) {
594612
this.#results.add(entryPath);
595-
yield entryPath;
613+
yield this.#withFileTypes ? entry : entryPath;
596614
}
597615
} else {
598616
subPatterns.add(nextIndex + 1);
@@ -605,7 +623,7 @@ class Glob {
605623
if (index === last) {
606624
if (!this.#results.has(entryPath)) {
607625
this.#results.add(entryPath);
608-
yield entryPath;
626+
yield this.#withFileTypes ? entry : entryPath;
609627
}
610628
} else if (entry.isDirectory()) {
611629
subPatterns.add(nextIndex);

lib/internal/fs/utils.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,6 +961,7 @@ module.exports = {
961961
BigIntStats, // for testing
962962
copyObject,
963963
Dirent,
964+
DirentFromStats,
964965
emitRecursiveRmdirWarning,
965966
getDirent,
966967
getDirents,

test/parallel/test-fs-glob.mjs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import * as common from '../common/index.mjs';
22
import tmpdir from '../common/tmpdir.js';
3-
import { resolve, dirname, sep } from 'node:path';
3+
import { resolve, dirname, sep, basename } from 'node:path';
44
import { mkdir, writeFile, symlink, glob as asyncGlob } from 'node:fs/promises';
5-
import { glob, globSync } from 'node:fs';
5+
import { glob, globSync, Dirent } from 'node:fs';
66
import { test, describe } from 'node:test';
77
import { promisify } from 'node:util';
88
import assert from 'node:assert';
99

10+
function assertDirents(dirents) {
11+
assert.ok(dirents.every((dirent) => dirent instanceof Dirent));
12+
}
13+
1014
tmpdir.refresh();
1115

1216
const fixtureDir = tmpdir.resolve('fixtures');
@@ -333,3 +337,38 @@ describe('fsPromises glob', function() {
333337
});
334338
}
335339
});
340+
341+
describe('glob - withFileTypes', function() {
342+
const promisified = promisify(glob);
343+
for (const [pattern, expected] of Object.entries(patterns)) {
344+
test(pattern, async () => {
345+
const actual = await promisified(pattern, { cwd: fixtureDir, withFileTypes: true });
346+
assertDirents(actual);
347+
const normalized = expected.filter(Boolean).map((item) => basename(item)).sort();
348+
assert.deepStrictEqual(actual.map((dirent) => dirent.name).sort(), normalized.sort());
349+
});
350+
}
351+
});
352+
353+
describe('globSync - withFileTypes', function() {
354+
for (const [pattern, expected] of Object.entries(patterns)) {
355+
test(pattern, () => {
356+
const actual = globSync(pattern, { cwd: fixtureDir, withFileTypes: true });
357+
assertDirents(actual);
358+
const normalized = expected.filter(Boolean).map((item) => basename(item)).sort();
359+
assert.deepStrictEqual(actual.map((dirent) => dirent.name).sort(), normalized.sort());
360+
});
361+
}
362+
});
363+
364+
describe('fsPromises glob - withFileTypes', function() {
365+
for (const [pattern, expected] of Object.entries(patterns)) {
366+
test(pattern, async () => {
367+
const actual = [];
368+
for await (const item of asyncGlob(pattern, { cwd: fixtureDir, withFileTypes: true })) actual.push(item);
369+
assertDirents(actual);
370+
const normalized = expected.filter(Boolean).map((item) => basename(item)).sort();
371+
assert.deepStrictEqual(actual.map((dirent) => dirent.name).sort(), normalized.sort());
372+
});
373+
}
374+
});

0 commit comments

Comments
 (0)