From b0b8df3051080fcdbbd8e7eaf36bb49d0077c224 Mon Sep 17 00:00:00 2001 From: Robin van Wijngaarden Date: Tue, 9 Dec 2025 21:48:50 +0100 Subject: [PATCH] fs: detect dot files when using globstar Using globstar in glob pattern should not prevent dot (hidden) files from being matched. --- lib/internal/fs/glob.js | 40 ++++++++++++++++++++++++---------- test/parallel/test-fs-glob.mjs | 5 +++++ 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/lib/internal/fs/glob.js b/lib/internal/fs/glob.js index 3e9c83356ce1f3..1bfa39150e5196 100644 --- a/lib/internal/fs/glob.js +++ b/lib/internal/fs/glob.js @@ -442,7 +442,18 @@ class Glob { const fromSymlink = pattern.symlinks.has(index); if (current === lazyMinimatch().GLOBSTAR) { - if (entry.name[0] === '.' || (this.#exclude && this.#exclude(this.#withFileTypes ? entry : entry.name))) { + const isDot = entry.name[0] === '.'; + const nextMatches = pattern.test(nextIndex, entry.name); + + let nextNonGlobIndex = nextIndex; + while (pattern.at(nextNonGlobIndex) === lazyMinimatch().GLOBSTAR) { + nextNonGlobIndex++; + } + + const matchesDot = isDot && pattern.test(nextNonGlobIndex, entry.name); + + if ((isDot && !matchesDot) || + (this.#exclude && this.#exclude(this.#withFileTypes ? entry : entry.name))) { continue; } if (!fromSymlink && entry.isDirectory()) { @@ -455,7 +466,6 @@ class Glob { // Any pattern after ** is also a potential pattern // so we can already test it here - const nextMatches = pattern.test(nextIndex, entry.name); if (nextMatches && nextIndex === last && !isLast) { // If next pattern is the last one, add to results this.#results.add(entryPath); @@ -642,7 +652,18 @@ class Glob { const fromSymlink = pattern.symlinks.has(index); if (current === lazyMinimatch().GLOBSTAR) { - if (entry.name[0] === '.' || (this.#exclude && this.#exclude(this.#withFileTypes ? entry : entry.name))) { + const isDot = entry.name[0] === '.'; + const nextMatches = pattern.test(nextIndex, entry.name); + + let nextNonGlobIndex = nextIndex; + while (pattern.at(nextNonGlobIndex) === lazyMinimatch().GLOBSTAR) { + nextNonGlobIndex++; + } + + const matchesDot = isDot && pattern.test(nextNonGlobIndex, entry.name); + + if ((isDot && !matchesDot) || + (this.#exclude && this.#exclude(this.#withFileTypes ? entry : entry.name))) { continue; } if (!fromSymlink && entry.isDirectory()) { @@ -650,22 +671,17 @@ class Glob { subPatterns.add(index); } else if (!fromSymlink && index === last) { // If ** is last, add to results - if (!this.#results.has(entryPath)) { - if (this.#results.add(entryPath)) { - yield this.#withFileTypes ? entry : entryPath; - } + if (!this.#results.has(entryPath) && this.#results.add(entryPath)) { + yield this.#withFileTypes ? entry : entryPath; } } // Any pattern after ** is also a potential pattern // so we can already test it here - const nextMatches = pattern.test(nextIndex, entry.name); if (nextMatches && nextIndex === last && !isLast) { // If next pattern is the last one, add to results - if (!this.#results.has(entryPath)) { - if (this.#results.add(entryPath)) { - yield this.#withFileTypes ? entry : entryPath; - } + if (!this.#results.has(entryPath) && this.#results.add(entryPath)) { + yield this.#withFileTypes ? entry : entryPath; } } else if (nextMatches && entry.isDirectory()) { // Pattern matched, meaning two patterns forward diff --git a/test/parallel/test-fs-glob.mjs b/test/parallel/test-fs-glob.mjs index bb8e019a72051a..3f98cdf33d5294 100644 --- a/test/parallel/test-fs-glob.mjs +++ b/test/parallel/test-fs-glob.mjs @@ -30,6 +30,8 @@ async function setup() { 'a/cb/e/f', 'a/x/.y/b', 'a/z/.y/b', + 'a/.b', + 'a/b/.b', ].map((f) => resolve(fixtureDir, f)); const symlinkTo = resolve(fixtureDir, 'a/symlink/a/b/c'); @@ -188,6 +190,9 @@ const patterns = { ], '*/*/*/f': ['a/bc/e/f', 'a/cb/e/f'], './**/f': ['a/bc/e/f', 'a/cb/e/f'], + '**/.b': ['a/.b', 'a/b/.b'], + './**/.b': ['a/.b', 'a/b/.b'], + 'a/**/.b': ['a/.b', 'a/b/.b'], 'a/symlink/a/b/c/a/b/c/a/b/c//a/b/c////a/b/c/**/b/c/**': common.isWindows ? [] : [ 'a/symlink/a/b/c/a/b/c/a/b/c/a/b/c/a/b/c/a/b/c', 'a/symlink/a/b/c/a/b/c/a/b/c/a/b/c/a/b/c/a/b/c/a',