UNPKG

prettierx

Version:

prettierX - a less opinionated fork of the Prettier code formatter

224 lines (197 loc) 6.1 kB
"use strict"; const path = require("path"); const { promises: fs } = require("fs"); const fastGlob = require("fast-glob"); /** @typedef {import('./context').Context} Context */ /** * @param {Context} context */ async function* expandPatterns(context) { const cwd = process.cwd(); const seen = new Set(); let noResults = true; for await (const pathOrError of expandPatternsInternal(context)) { noResults = false; if (typeof pathOrError !== "string") { yield pathOrError; continue; } const relativePath = path.relative(cwd, pathOrError); // filter out duplicates if (seen.has(relativePath)) { continue; } seen.add(relativePath); yield relativePath; } if (noResults && context.argv["error-on-unmatched-pattern"] !== false) { // If there was no files and no other errors, let's yield a general error. yield { error: `No matching files. Patterns: ${context.filePatterns.join(" ")}`, }; } } /** * @param {Context} context */ async function* expandPatternsInternal(context) { // Ignores files in version control systems directories and `node_modules` const silentlyIgnoredDirs = [".git", ".svn", ".hg"]; if (context.argv["with-node-modules"] !== true) { silentlyIgnoredDirs.push("node_modules"); } const globOptions = { dot: true, ignore: silentlyIgnoredDirs.map((dir) => "**/" + dir), }; let supportedFilesGlob; const cwd = process.cwd(); /** @type {Array<{ type: 'file' | 'dir' | 'glob'; glob: string; input: string; }>} */ const entries = []; for (const pattern of context.filePatterns) { const absolutePath = path.resolve(cwd, pattern); if (containsIgnoredPathSegment(absolutePath, cwd, silentlyIgnoredDirs)) { continue; } const stat = await statSafe(absolutePath); if (stat) { if (stat.isFile()) { entries.push({ type: "file", glob: escapePathForGlob(fixWindowsSlashes(pattern)), input: pattern, }); } else if (stat.isDirectory()) { /* 1. Remove trailing `/`, `fast-glob` can't find files for `src//*.js` pattern 2. Cleanup dirname, when glob `src/../*.js` pattern with `fast-glob`, it returns files like 'src/../index.js' */ const relativePath = path.relative(cwd, absolutePath) || "."; entries.push({ type: "dir", glob: escapePathForGlob(fixWindowsSlashes(relativePath)) + "/" + getSupportedFilesGlob(), input: pattern, }); } } else if (pattern[0] === "!") { // convert negative patterns to `ignore` entries globOptions.ignore.push(fixWindowsSlashes(pattern.slice(1))); } else { entries.push({ type: "glob", glob: fixWindowsSlashes(pattern), input: pattern, }); } } for (const { type, glob, input } of entries) { let result; try { result = await fastGlob(glob, globOptions); } catch ({ message }) { /* istanbul ignore next */ yield { error: `${errorMessages.globError[type]}: ${input}\n${message}` }; /* istanbul ignore next */ continue; } if (result.length === 0) { if (context.argv["error-on-unmatched-pattern"] !== false) { yield { error: `${errorMessages.emptyResults[type]}: "${input}".` }; } } else { yield* sortPaths(result); } } function getSupportedFilesGlob() { if (!supportedFilesGlob) { const extensions = context.languages.flatMap( (lang) => lang.extensions || [] ); const filenames = context.languages.flatMap( (lang) => lang.filenames || [] ); supportedFilesGlob = `**/{${[ ...extensions.map((ext) => "*" + (ext[0] === "." ? ext : "." + ext)), ...filenames, ]}}`; } return supportedFilesGlob; } } const errorMessages = { globError: { file: "Unable to resolve file", dir: "Unable to expand directory", glob: "Unable to expand glob pattern", }, emptyResults: { file: "Explicitly specified file was ignored due to negative glob patterns", dir: "No supported files were found in the directory", glob: "No files matching the pattern were found", }, }; /** * @param {string} absolutePath * @param {string} cwd * @param {string[]} ignoredDirectories */ function containsIgnoredPathSegment(absolutePath, cwd, ignoredDirectories) { return path .relative(cwd, absolutePath) .split(path.sep) .some((dir) => ignoredDirectories.includes(dir)); } /** * @param {string[]} paths */ function sortPaths(paths) { return paths.sort((a, b) => a.localeCompare(b)); } /** * Get stats of a given path. * @param {string} filePath The path to target file. * @returns {Promise<import('fs').Stats | undefined>} The stats. */ async function statSafe(filePath) { try { return await fs.stat(filePath); } catch (error) { /* istanbul ignore next */ if (error.code !== "ENOENT") { throw error; } } } /** * This function should be replaced with `fastGlob.escapePath` when these issues are fixed: * - https://github.com/mrmlnc/fast-glob/issues/261 * - https://github.com/mrmlnc/fast-glob/issues/262 * @param {string} path */ function escapePathForGlob(path) { return fastGlob .escapePath( path.replace(/\\/g, "\0") // Workaround for fast-glob#262 (part 1) ) .replace(/\\!/g, "@(!)") // Workaround for fast-glob#261 .replace(/\0/g, "@(\\\\)"); // Workaround for fast-glob#262 (part 2) } const isWindows = path.sep === "\\"; /** * Using backslashes in globs is probably not okay, but not accepting * backslashes as path separators on Windows is even more not okay. * https://github.com/prettier/prettier/pull/6776#discussion_r380723717 * https://github.com/mrmlnc/fast-glob#how-to-write-patterns-on-windows * @param {string} pattern */ function fixWindowsSlashes(pattern) { return isWindows ? pattern.replace(/\\/g, "/") : pattern; } module.exports = { expandPatterns, fixWindowsSlashes, };