Version
v25.8.2
Platform
Subsystem
fs
What steps will reproduce the bug?
const { mkdtempSync, mkdirSync, writeFileSync, globSync } = require('node:fs');
const { tmpdir } = require('node:os');
const { join } = require('node:path');
const { chdir, cwd } = require('node:process');
const base = mkdtempSync(join(tmpdir(), 'glob-root-'));
const ambient = join(base, 'ambient');
const root = join(base, 'root');
mkdirSync(ambient, { recursive: true });
mkdirSync(join(root, 'a'), { recursive: true });
writeFileSync(join(ambient, 'a'), 'shadow-file');
writeFileSync(join(root, 'a', 'real.txt'), 'real');
chdir(ambient);
const seen = [];
const result = globSync('a/**', {
cwd: root,
withFileTypes: true,
exclude: (dirent) => {
seen.push({
name: dirent.name,
parentPath: dirent.parentPath,
isDirectory: dirent.isDirectory(),
isFile: dirent.isFile(),
});
return dirent.isDirectory();
},
});
console.log(JSON.stringify({
processCwd: cwd(),
globCwd: root,
seen,
result: result.map((dirent) => ({
name: dirent.name,
parentPath: dirent.parentPath,
isDirectory: dirent.isDirectory(),
isFile: dirent.isFile(),
})),
}, null, 2));
How often does it reproduce? Is there a required condition?
It reproduces consistently in globSync() when all of the following are true:
withFileTypes: true
cwd !== process.cwd()
- the pattern goes through the root-entry
#addSubpattern() path (for example a/**)
If the ambient process.cwd() also contains the same relative path, the callback receives a Dirent for the ambient path instead of the glob cwd path. If the ambient cwd does not contain that path, the root entry can skip the callback entirely because statSync(path) returns null.
The async glob path does not seem affected.
What is the expected behavior? Why is that the expected behavior?
The exclude callback should receive a Dirent describing the candidate entry under options.cwd.
For the repro above, the callback should receive a directory dirent for <globCwd>/a, so dirent.isDirectory() should be true and the result should be an empty array because the callback returns true for directories.
What do you see instead?
The callback receives a Dirent for the ambient process.cwd() path instead:
{
"processCwd": "/tmp/.../ambient",
"globCwd": "/tmp/.../root",
"seen": [
{
"name": "a",
"parentPath": ".",
"isDirectory": false,
"isFile": true
},
{
"name": "real.txt",
"parentPath": "/tmp/.../root/a",
"isDirectory": false,
"isFile": true
}
],
"result": [
{
"name": "a",
"parentPath": "/tmp/.../root",
"isDirectory": true,
"isFile": false
},
{
"name": "real.txt",
"parentPath": "/tmp/.../root/a",
"isDirectory": false,
"isFile": true
}
]
}
So the root a entry is not excluded even though the callback logic is meant to exclude directories.
Additional information
This looks like a sync-only regression in the root-path exclude handling added for #56260 / #57420.
In lib/internal/fs/glob.js, #addSubpattern() computes const fullpath = resolve(this.#root, path), but in the withFileTypes + exclude branch it does:
const stat = this.#cache.statSync(path);
That appears to stat a path relative to process.cwd() instead of options.cwd. The async path already uses await this.#cache.stat(fullpath).
Version
v25.8.2
Platform
Subsystem
fs
What steps will reproduce the bug?
How often does it reproduce? Is there a required condition?
It reproduces consistently in
globSync()when all of the following are true:withFileTypes: truecwd !== process.cwd()#addSubpattern()path (for examplea/**)If the ambient
process.cwd()also contains the same relative path, the callback receives aDirentfor the ambient path instead of the globcwdpath. If the ambient cwd does not contain that path, the root entry can skip the callback entirely becausestatSync(path)returnsnull.The async glob path does not seem affected.
What is the expected behavior? Why is that the expected behavior?
The
excludecallback should receive aDirentdescribing the candidate entry underoptions.cwd.For the repro above, the callback should receive a directory dirent for
<globCwd>/a, sodirent.isDirectory()should betrueand the result should be an empty array because the callback returnstruefor directories.What do you see instead?
The callback receives a
Direntfor the ambientprocess.cwd()path instead:{ "processCwd": "/tmp/.../ambient", "globCwd": "/tmp/.../root", "seen": [ { "name": "a", "parentPath": ".", "isDirectory": false, "isFile": true }, { "name": "real.txt", "parentPath": "/tmp/.../root/a", "isDirectory": false, "isFile": true } ], "result": [ { "name": "a", "parentPath": "/tmp/.../root", "isDirectory": true, "isFile": false }, { "name": "real.txt", "parentPath": "/tmp/.../root/a", "isDirectory": false, "isFile": true } ] }So the root
aentry is not excluded even though the callback logic is meant to exclude directories.Additional information
This looks like a sync-only regression in the root-path exclude handling added for #56260 / #57420.
In
lib/internal/fs/glob.js,#addSubpattern()computesconst fullpath = resolve(this.#root, path), but in thewithFileTypes+excludebranch it does:That appears to stat a path relative to
process.cwd()instead ofoptions.cwd. The async path already usesawait this.#cache.stat(fullpath).