diff --git a/package-lock.json b/package-lock.json index b0546bc..d8506ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,8 +27,8 @@ "smol-toml": "^1.8.0", "specialist": "^2.0.0", "tiny-editorconfig": "^1.0.2", - "tiny-readdir": "^2.7.4", - "tiny-readdir-glob": "^1.23.2", + "tiny-readdir": "^3.1.1", + "tiny-readdir-glob": "^2.0.0", "tiny-spinner": "^2.0.5", "worktank": "^3.0.2", "zeptomatch": "^2.1.0", @@ -4559,6 +4559,16 @@ "tiny-readdir": "^2.2.0" } }, + "node_modules/json-archive/node_modules/tiny-readdir": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/tiny-readdir/-/tiny-readdir-2.7.4.tgz", + "integrity": "sha512-721U+zsYwDirjr8IM6jqpesD/McpZooeFi3Zc6mcjy1pse2C+v19eHPFRqz4chGXZFw7C3KITDjAtHETc2wj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "promise-make-counter": "^1.0.2" + } + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -6338,23 +6348,22 @@ "integrity": "sha512-RnIDHQ+r9zMuslQWVoRxfKVOumteeheQqbwNYJyQxzM2vzx/vdN5xAeL64F3rQOpfbVdxFkhM4zPDyfq7SxsBQ==" }, "node_modules/tiny-readdir": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/tiny-readdir/-/tiny-readdir-2.7.4.tgz", - "integrity": "sha512-721U+zsYwDirjr8IM6jqpesD/McpZooeFi3Zc6mcjy1pse2C+v19eHPFRqz4chGXZFw7C3KITDjAtHETc2wj7Q==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tiny-readdir/-/tiny-readdir-3.1.1.tgz", + "integrity": "sha512-SiNW3Kp5xshlA3McA2C/tjU7K8+VJc9ZNe09foB8ut8lMY7bR9NKPo5ARHDOj24mV69/utNy8gqDjF93FcFlFg==", + "license": "MIT", "dependencies": { "promise-make-counter": "^1.0.2" } }, "node_modules/tiny-readdir-glob": { - "version": "1.23.2", - "resolved": "https://registry.npmjs.org/tiny-readdir-glob/-/tiny-readdir-glob-1.23.2.tgz", - "integrity": "sha512-+47FIdgzEtZj03mOyq9iAljlZZNleqSEwe3i6Uzkzec5axbMg32Vp78U2fLo4TiCMv9gzjnno7yJn34z5pXECw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tiny-readdir-glob/-/tiny-readdir-glob-2.0.0.tgz", + "integrity": "sha512-1zAGi76CspOE4MjMrsx2OeLpvjiHyprFTuJ1O3k5C1qU09GpltOL+zx5rw9CaVQ/qkpkmCfc09TS2fV52GJrrw==", + "license": "MIT", "dependencies": { - "tiny-readdir": "^2.7.0", - "zeptomatch": "^2.0.1", - "zeptomatch-explode": "^1.0.1", - "zeptomatch-is-static": "^1.0.1", - "zeptomatch-unescape": "^1.0.1" + "tiny-readdir": "^3.1.1", + "zeptomatch": "^2.1.0" } }, "node_modules/tiny-spinner": { @@ -6952,20 +6961,10 @@ "resolved": "https://registry.npmjs.org/zeptomatch-escape/-/zeptomatch-escape-1.0.1.tgz", "integrity": "sha512-kAc5HzvnF66djCYDqpsS46Y/FKi+4pe/KJRmTmm/hwmoaNYzmm6bBY07cdkxmJCdY018S5UeQn4yP+9X2x1MbQ==" }, - "node_modules/zeptomatch-explode": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/zeptomatch-explode/-/zeptomatch-explode-1.0.1.tgz", - "integrity": "sha512-7cUQASLLRGZ20+zEQcEgQ9z/gH1+jSfrNg4KfRJSxF1QU2fpymAwWvnAxl69GD5pr3IV0V9vo3ke2np//Nh4tQ==" - }, "node_modules/zeptomatch-is-static": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/zeptomatch-is-static/-/zeptomatch-is-static-1.0.1.tgz", "integrity": "sha512-bN9q7H/UdXhkub01WE7b7Grg07jLldNnIWG2T1IpBq5NtvcQ4DwFbNiGGapnbKHUdWiCNjg/bIvixV88nj9gog==" - }, - "node_modules/zeptomatch-unescape": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/zeptomatch-unescape/-/zeptomatch-unescape-1.0.1.tgz", - "integrity": "sha512-xhSFkKV0aQ03e/eiN4VhOTwJhcqfH7SMiGHrWKw9gXi+0EVJAxJ8Gt4ehozYsYLhUXL1JFbP1g3EE6ZmkStB0g==" } }, "dependencies": { @@ -10170,6 +10169,17 @@ "dev": true, "requires": { "tiny-readdir": "^2.2.0" + }, + "dependencies": { + "tiny-readdir": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/tiny-readdir/-/tiny-readdir-2.7.4.tgz", + "integrity": "sha512-721U+zsYwDirjr8IM6jqpesD/McpZooeFi3Zc6mcjy1pse2C+v19eHPFRqz4chGXZFw7C3KITDjAtHETc2wj7Q==", + "dev": true, + "requires": { + "promise-make-counter": "^1.0.2" + } + } } }, "json-parse-even-better-errors": { @@ -11471,23 +11481,20 @@ "integrity": "sha512-RnIDHQ+r9zMuslQWVoRxfKVOumteeheQqbwNYJyQxzM2vzx/vdN5xAeL64F3rQOpfbVdxFkhM4zPDyfq7SxsBQ==" }, "tiny-readdir": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/tiny-readdir/-/tiny-readdir-2.7.4.tgz", - "integrity": "sha512-721U+zsYwDirjr8IM6jqpesD/McpZooeFi3Zc6mcjy1pse2C+v19eHPFRqz4chGXZFw7C3KITDjAtHETc2wj7Q==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tiny-readdir/-/tiny-readdir-3.1.1.tgz", + "integrity": "sha512-SiNW3Kp5xshlA3McA2C/tjU7K8+VJc9ZNe09foB8ut8lMY7bR9NKPo5ARHDOj24mV69/utNy8gqDjF93FcFlFg==", "requires": { "promise-make-counter": "^1.0.2" } }, "tiny-readdir-glob": { - "version": "1.23.2", - "resolved": "https://registry.npmjs.org/tiny-readdir-glob/-/tiny-readdir-glob-1.23.2.tgz", - "integrity": "sha512-+47FIdgzEtZj03mOyq9iAljlZZNleqSEwe3i6Uzkzec5axbMg32Vp78U2fLo4TiCMv9gzjnno7yJn34z5pXECw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tiny-readdir-glob/-/tiny-readdir-glob-2.0.0.tgz", + "integrity": "sha512-1zAGi76CspOE4MjMrsx2OeLpvjiHyprFTuJ1O3k5C1qU09GpltOL+zx5rw9CaVQ/qkpkmCfc09TS2fV52GJrrw==", "requires": { - "tiny-readdir": "^2.7.0", - "zeptomatch": "^2.0.1", - "zeptomatch-explode": "^1.0.1", - "zeptomatch-is-static": "^1.0.1", - "zeptomatch-unescape": "^1.0.1" + "tiny-readdir": "^3.1.1", + "zeptomatch": "^2.1.0" } }, "tiny-spinner": { @@ -11933,20 +11940,10 @@ "resolved": "https://registry.npmjs.org/zeptomatch-escape/-/zeptomatch-escape-1.0.1.tgz", "integrity": "sha512-kAc5HzvnF66djCYDqpsS46Y/FKi+4pe/KJRmTmm/hwmoaNYzmm6bBY07cdkxmJCdY018S5UeQn4yP+9X2x1MbQ==" }, - "zeptomatch-explode": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/zeptomatch-explode/-/zeptomatch-explode-1.0.1.tgz", - "integrity": "sha512-7cUQASLLRGZ20+zEQcEgQ9z/gH1+jSfrNg4KfRJSxF1QU2fpymAwWvnAxl69GD5pr3IV0V9vo3ke2np//Nh4tQ==" - }, "zeptomatch-is-static": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/zeptomatch-is-static/-/zeptomatch-is-static-1.0.1.tgz", "integrity": "sha512-bN9q7H/UdXhkub01WE7b7Grg07jLldNnIWG2T1IpBq5NtvcQ4DwFbNiGGapnbKHUdWiCNjg/bIvixV88nj9gog==" - }, - "zeptomatch-unescape": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/zeptomatch-unescape/-/zeptomatch-unescape-1.0.1.tgz", - "integrity": "sha512-xhSFkKV0aQ03e/eiN4VhOTwJhcqfH7SMiGHrWKw9gXi+0EVJAxJ8Gt4ehozYsYLhUXL1JFbP1g3EE6ZmkStB0g==" } } } diff --git a/package.json b/package.json index 59d27d9..dc3782c 100644 --- a/package.json +++ b/package.json @@ -54,8 +54,8 @@ "smol-toml": "^1.8.0", "specialist": "^2.0.0", "tiny-editorconfig": "^1.0.2", - "tiny-readdir": "^2.7.4", - "tiny-readdir-glob": "^1.23.2", + "tiny-readdir": "^3.1.1", + "tiny-readdir-glob": "^2.0.0", "tiny-spinner": "^2.0.5", "worktank": "^3.0.2", "zeptomatch": "^2.1.0", diff --git a/src/index.ts b/src/index.ts index bebb6d5..688e14e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -181,7 +181,11 @@ async function runGlobs(options: Options, pluginsDefaultOptions: PluginsOptions, const cache = shouldCache ? new Cache(cacheVersion, projectPath, getCacheRootPath(rootPath), options, stdout) : undefined; const prettier = await makePrettier(options, cache); - const concurrency = options.parallel ? (options.parallelWorkers ?? Math.max(1, (os.availableParallelism?.() ?? os.cpus().length) - 1)) : 1; + const concurrency = options.parallel + ? options.parallelWorkers > 0 + ? options.parallelWorkers + : Math.max(1, (os.availableParallelism?.() ?? os.cpus().length) - 1) + : 1; const filesResults = await mapSettledWithConcurrency(filesPathsTargets, concurrency, async (filePath) => { const isIgnored = () => (ignoreManual ? ignoreManual(filePath) : getIgnoreResolved(filePath, ignoreNames)); const isCacheable = () => cache?.has(filePath, isIgnored); diff --git a/src/utils.ts b/src/utils.ts index 14f6cad..b3679cd 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -11,6 +11,7 @@ import url from "node:url"; import resolveTimeout from "promise-resolve-timeout"; import { exit } from "specialist"; import readdir from "tiny-readdir"; +import type { Dirent } from "tiny-readdir"; import readdirGlob from "tiny-readdir-glob"; import zeptomatch from "zeptomatch"; import zeptomatchEscape from "zeptomatch-escape"; @@ -18,6 +19,8 @@ import zeptomatchIsStatic from "zeptomatch-is-static"; import type { ContextOptions, FormatOptions, FunctionMaybe, Key, LogLevel, Options, PrettierConfigWithOverrides, PrettierPlugin } from "./types.js"; import type { PluginsOptions } from "./types.js"; +type DirentWithParentPath = Dirent & { parentPath: string }; + function castArray(value: T | T[]): T[] { return isArray(value) ? value : [value]; } @@ -106,12 +109,54 @@ async function getFoldersChildrenPaths(foldersPaths: string[]): Promise { + return ignoreRe.test(path.relative(rootPath, targetPath)); + }; + + // These are the files and directories that were found during glob traversal. + // They haven't yet been filtered by the user globs so may not + // equal the result files. + const filesFound: string[] = []; + const filesFoundNamesToPaths: Record = Object.create(null); + const directoriesFound: string[] = []; + + const onDirents = (dirents: Dirent[]): undefined => { + for (const dirent of dirents) { + const direntName = dirent.name; + // TODO (jg): remove this cast once tiny-readdir knows about + // the `parentPath` property on Dirent objects + const direntPath = fastJoinedPath((dirent as DirentWithParentPath).parentPath, direntName); + if (ignore(direntPath)) continue; + if (dirent.isFile()) { + filesFound.push(direntPath); + if (!Object.hasOwn(filesFoundNamesToPaths, direntName)) { + filesFoundNamesToPaths[direntName] = []; + } + filesFoundNamesToPaths[direntName].push(direntPath); + } else if (dirent.isDirectory()) { + directoriesFound.push(direntPath); + } + } + }; + + // Globs are matched against paths relative to the root, which are never prefixed with "./" + // TODO (jg): handle '../' in globs + const globsNormalized = globs.map((glob) => glob.replace(/^(!*)(?:\.\/)+/, "$1")); + + const result = await readdirGlob(globsNormalized, { cwd: rootPath, followSymlinks: false, - ignore: `**/{.git,.sl,.svn,.hg,.DS_Store,Thumbs.db${withNodeModules ? "" : ",node_modules"}}`, + ignore: ignoreGlob, + onDirents, }); + + return { files: result.files, filesFound, filesFoundNamesToPaths, directoriesFound }; } async function getModule(modulePath: string): Promise { @@ -248,7 +293,9 @@ async function getTargetsPaths( const fileName = path.basename(filePath); targetFiles.push(filePath); targetFilesNames.push(fileName); - targetFilesNamesToPaths.propertyIsEnumerable(fileName) || (targetFilesNamesToPaths[fileName] = []); + if (!Object.hasOwn(targetFilesNamesToPaths, fileName)) { + targetFilesNamesToPaths[fileName] = []; + } targetFilesNamesToPaths[fileName].push(filePath); } else if (fileStats?.isDirectory()) { targetDirectories.push(filePath); @@ -259,11 +306,11 @@ async function getTargetsPaths( const globResult = await getGlobPaths(rootPath, targetGlobs, withNodeModules); const globResultFiles = globResult.files; - const globResultFilesFoundNames = [...globResult.filesFoundNames]; + const globResultFilesFoundNames = Object.keys(globResult.filesFoundNamesToPaths); const directoriesResults = await Promise.all(targetDirectories.map((targetPath) => getDirectoryPaths(targetPath, withNodeModules))); const directoriesResultsFiles = directoriesResults.map((result) => result.files); - const directoriesResultsFilesFoundNames = directoriesResults.map((result) => [...result.filesNames]); + const directoriesResultsFilesFoundNames = directoriesResults.map((result) => uniq(result.files.map((filePath) => path.basename(filePath)))); const foundFiles = uniqChunks(globResultFiles, ...directoriesResultsFiles); const foundFilesNames = uniqChunks(globResultFilesFoundNames, ...directoriesResultsFilesFoundNames); diff --git a/test/__tests__/parallel.js b/test/__tests__/parallel.js new file mode 100644 index 0000000..6caf213 --- /dev/null +++ b/test/__tests__/parallel.js @@ -0,0 +1,35 @@ +import { runCli } from "../utils"; + +describe("files are processed when parallelism is left at its defaults", () => { + runCli("write", [ + "--list-different", + "formatted.js", + "unformatted.js", + "unformatted2.js", + ], { + parallel: true, + }).test({ + stdout: "unformatted.js\nunformatted2.js", + stderr: "", + status: 1, + write: [], + }); +}); + +describe("files are processed when an explicit worker count is given", () => { + runCli("write", [ + "--parallel-workers", + "2", + "--list-different", + "formatted.js", + "unformatted.js", + "unformatted2.js", + ], { + parallel: true, + }).test({ + stdout: "unformatted.js\nunformatted2.js", + stderr: "", + status: 1, + write: [], + }); +}); diff --git a/test/__tests__/patterns.js b/test/__tests__/patterns.js index db1243e..31f1420 100644 --- a/test/__tests__/patterns.js +++ b/test/__tests__/patterns.js @@ -40,8 +40,7 @@ describe("multiple patterns with a negated pattern, ignores node_modules by defa }); }); -// TODO: Handle leading `./` and `../` in patterns. -describe.skip("multiple patterns with a negated pattern and leading `./`, ignores node_modules by default", () => { +describe("multiple patterns with a negated pattern and leading `./`, ignores node_modules by default", () => { runCli("patterns", [ "./**/*.js", "!./directory/**", diff --git a/test/utils.js b/test/utils.js index 9ee7e30..81116e8 100644 --- a/test/utils.js +++ b/test/utils.js @@ -122,7 +122,7 @@ async function runCommand(dir, args, options) { const fixtures = dir ? await getIsolatedFixtures(dir) : undefined; const archive = fixtures ? await getArchive(fixtures.path) : undefined; const cwd = fixtures ? fixtures.path : TESTS_PATH; - const argsForTesting = ["--no-parallel"]; + const argsForTesting = options.parallel ? [] : ["--no-parallel"]; const argsWithReplacements = args.map((arg) => arg.replaceAll("$CWD", cwd)); const result = exec("node", [BIN_PATH, ...argsForTesting, ...argsWithReplacements], { cwd, stdio: "pipe" });