prettierx
Version:
prettierX - a less opinionated fork of the Prettier code formatter
224 lines (197 loc) • 6.1 kB
JavaScript
;
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,
};