Skip to content

Commit 05b0c61

Browse files
Ensuring directory names that look like negated globs are special-cased
1 parent c373e50 commit 05b0c61

5 files changed

Lines changed: 117 additions & 28 deletions

File tree

package-lock.json

Lines changed: 37 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"specialist": "^1.4.5",
5353
"tiny-editorconfig": "^1.0.0",
5454
"tiny-jsonc": "^1.0.1",
55+
"tiny-readdir": "^2.7.4",
5556
"tiny-readdir-glob": "^1.23.1",
5657
"tiny-spinner": "^2.0.4",
5758
"worktank": "^2.7.3",

src/utils.ts

Lines changed: 58 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import { text as stream2text } from "node:stream/consumers";
1010
import url from "node:url";
1111
import resolveTimeout from "promise-resolve-timeout";
1212
import { exit } from "specialist";
13-
import readdir from "tiny-readdir-glob";
13+
import readdir from "tiny-readdir";
14+
import readdirGlob from "tiny-readdir-glob";
15+
import zeptomatch from "zeptomatch";
1416
import zeptomatchEscape from "zeptomatch-escape";
1517
import zeptomatchIsStatic from "zeptomatch-is-static";
1618
import type { ContextOptions, FormatOptions, FunctionMaybe, Key, LogLevel, Options, PrettierConfigWithOverrides, PrettierPlugin } from "./types.js";
@@ -54,6 +56,19 @@ function getCachePath(rootPath: string): string {
5456
return cachePath;
5557
}
5658

59+
function getDirectoryPaths(rootPath: string, withNodeModules: boolean) {
60+
const ignoreGlob = `**/{.git,.sl,.svn,.hg,.DS_Store,Thumbs.db${withNodeModules ? "" : ",node_modules"}}`;
61+
const ignoreRe = zeptomatch.compile(ignoreGlob);
62+
const ignore = (targetPath: string): boolean => {
63+
return ignoreRe.test(path.relative(rootPath, targetPath));
64+
};
65+
66+
return readdir(rootPath, {
67+
followSymlinks: false,
68+
ignore,
69+
});
70+
}
71+
5772
function getExpandedFoldersPaths(foldersPaths: string[], untilPath: string = "/"): [string[], string[]] {
5873
const knownPaths = new Set(foldersPaths);
5974
const expandedPaths = new Set<string>();
@@ -88,7 +103,7 @@ async function getFoldersChildrenPaths(foldersPaths: string[]): Promise<string[]
88103
}
89104

90105
function getGlobPaths(rootPath: string, globs: string[], withNodeModules: boolean) {
91-
return readdir(globs, {
106+
return readdirGlob(globs, {
92107
cwd: rootPath,
93108
followSymlinks: false,
94109
ignore: `**/{.git,.sl,.svn,.hg,.DS_Store,Thumbs.db${withNodeModules ? "" : ",node_modules"}}`,
@@ -179,6 +194,14 @@ function getProjectPath(rootPath: string): string {
179194
}
180195
}
181196

197+
function getStats(targetPath: string): fs.Stats | undefined {
198+
try {
199+
return fs.statSync(targetPath);
200+
} catch {
201+
return;
202+
}
203+
}
204+
182205
const getStdin = once(async (): Promise<string | undefined> => {
183206
// Without a TTY, the process is likely, but not certainly, being piped
184207
if (!process.stdin.isTTY) {
@@ -196,28 +219,39 @@ async function getTargetsPaths(
196219
const targetFiles: string[] = [];
197220
const targetFilesNames: string[] = [];
198221
const targetFilesNamesToPaths: Record<string, string[]> = {};
222+
const targetDirectories: string[] = [];
199223
const targetGlobs: string[] = [];
200224

201225
for (const glob of globs) {
202226
const filePath = path.resolve(rootPath, glob);
203-
if (isFile(filePath)) {
227+
const fileStats = getStats(filePath);
228+
if (fileStats?.isFile()) {
204229
const fileName = path.basename(filePath);
205230
targetFiles.push(filePath);
206231
targetFilesNames.push(fileName);
207232
targetFilesNamesToPaths.propertyIsEnumerable(fileName) || (targetFilesNamesToPaths[fileName] = []);
208233
targetFilesNamesToPaths[fileName].push(filePath);
234+
} else if (fileStats?.isDirectory()) {
235+
targetDirectories.push(filePath);
209236
} else {
210237
targetGlobs.push(glob);
211238
}
212239
}
213240

214-
const result = await getGlobPaths(rootPath, targetGlobs, withNodeModules);
215-
const resultFiles = result.files;
216-
const resultFilesFoundNames = [...result.filesFoundNames];
241+
const globResult = await getGlobPaths(rootPath, targetGlobs, withNodeModules);
242+
const globResultFiles = globResult.files;
243+
const globResultFilesFoundNames = [...globResult.filesFoundNames];
217244

218-
const filesPaths = [...without(targetFiles, resultFiles), ...resultFiles];
219-
const filesNames = [...without(targetFilesNames, resultFilesFoundNames), ...resultFilesFoundNames];
220-
const filesNamesToPaths = result.filesFoundNamesToPaths;
245+
const directoriesResults = await Promise.all(targetDirectories.map((targetPath) => getDirectoryPaths(targetPath, withNodeModules)));
246+
const directoriesResultsFiles = directoriesResults.map((result) => result.files);
247+
const directoriesResultsFilesFoundNames = directoriesResults.map((result) => [...result.filesNames]);
248+
249+
const foundFiles = uniqChunks(globResultFiles, ...directoriesResultsFiles);
250+
const foundFilesNames = uniqChunks(globResultFilesFoundNames, ...directoriesResultsFilesFoundNames);
251+
252+
const filesPaths = [...without(targetFiles, foundFiles), ...foundFiles];
253+
const filesNames = [...without(targetFilesNames, foundFilesNames), ...foundFilesNames];
254+
const filesNamesToPaths = globResult.filesFoundNamesToPaths;
221255

222256
for (const fileName in targetFilesNamesToPaths) {
223257
const prev = filesNamesToPaths[fileName];
@@ -226,8 +260,8 @@ async function getTargetsPaths(
226260
}
227261

228262
const filesExplicitPaths = targetFiles;
229-
const filesFoundPaths = result.filesFound;
230-
const foldersFoundPaths = [rootPath, ...result.directoriesFound];
263+
const filesFoundPaths = globResult.filesFound;
264+
const foldersFoundPaths = [rootPath, ...globResult.directoriesFound];
231265
return [filesPaths, filesNames, filesNamesToPaths, filesExplicitPaths, filesFoundPaths, foldersFoundPaths];
232266
}
233267

@@ -239,15 +273,6 @@ function isBoolean(value: unknown): value is boolean {
239273
return typeof value === "boolean";
240274
}
241275

242-
function isFile(targetPath: string): boolean {
243-
try {
244-
const stats = fs.statSync(targetPath);
245-
return stats.isFile();
246-
} catch {
247-
return false;
248-
}
249-
}
250-
251276
function isFunction(value: unknown): value is Function {
252277
return typeof value === "function";
253278
}
@@ -650,6 +675,17 @@ function uniq<T>(values: T[]): T[] {
650675
return Array.from(new Set(values));
651676
}
652677

678+
function uniqChunks<T>(...chunks: T[][]): T[] {
679+
const chunksNonEmpty = chunks.filter((chunk) => chunk.length);
680+
if (chunksNonEmpty.length === 0) {
681+
return [];
682+
} else if (chunksNonEmpty.length === 1) {
683+
return chunksNonEmpty[0];
684+
} else {
685+
return uniq(chunks.flat());
686+
}
687+
}
688+
653689
function without<T>(values: T[], exclude: T[]): T[] {
654690
if (!values.length) return values;
655691
if (!exclude.length) return values;
@@ -691,6 +727,7 @@ export {
691727
getPluginsPaths,
692728
getPluginsVersions,
693729
getProjectPath,
730+
getStats,
694731
getStdin,
695732
getTargetsPaths,
696733
isArray,
@@ -720,6 +757,7 @@ export {
720757
someOf,
721758
trimFinalNewline,
722759
uniq,
760+
uniqChunks,
723761
without,
724762
zipObjectUnless,
725763
};

test/__tests__/__snapshots__/patterns-glob.js.snap

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,17 @@ a.js"
2828

2929
exports[`fixtures-2: Should match \`a.js\` and \`!b.js\` (write) 1`] = `[]`;
3030

31+
exports[`fixtures-2: Should match all js files and all supported files in the '!dir.js' directory (stderr) 1`] = `""`;
32+
33+
exports[`fixtures-2: Should match all js files and all supported files in the '!dir.js' directory (stdout) 1`] = `
34+
"!b.js
35+
!dir.js/1.css
36+
!dir.js/2.css
37+
a.js"
38+
`;
39+
40+
exports[`fixtures-2: Should match all js files and all supported files in the '!dir.js' directory (write) 1`] = `[]`;
41+
3142
exports[`fixtures-2: Should only match \`!b.js\` (stderr) 1`] = `""`;
3243

3344
exports[`fixtures-2: Should only match \`!b.js\` (stdout) 1`] = `"!b.js"`;

test/__tests__/patterns-glob.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ describe("fixtures-1: Should match files except `a.js`", () => {
3636
└─ 2.css
3737
*/
3838

39+
describe("fixtures-2: Should match all js files and all supported files in the '!dir.js' directory", () => {
40+
runCli("patterns-glob/fixtures-2", [
41+
"*.js",
42+
"!dir.js",
43+
"-l",
44+
]).test({
45+
status: 1,
46+
});
47+
});
48+
3949
describe("fixtures-2: Should match `a.js` and `!b.js`", () => {
4050
runCli("patterns-glob/fixtures-2", [
4151
"*.js",

0 commit comments

Comments
 (0)