UNPKG

prettier-eslint-cli

Version:
340 lines (339 loc) 12.7 kB
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) { if (typeof path === "string" && /^\.\.?\//.test(path)) { return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) { return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js"); }); } return path; }; import fs from 'node:fs/promises'; import path from 'node:path'; import { text } from 'node:stream/consumers'; import { pathToFileURL } from 'node:url'; import { ConfigArray } from '@eslint/config-array'; import { hfs } from '@humanfs/node'; import chalk from 'chalk'; import { ESLint } from 'eslint'; import { findUpSync } from 'find-up'; import globParent from 'glob-parent'; import nodeIgnore from 'ignore'; import indentString from 'indent-string'; import { Minimatch } from 'minimatch'; import { logger } from "./logger.js"; import * as messages from "./messages.js"; import { format } from "./prettier-eslint.js"; const INDENT_COUNT = 4; const DEFAULT_ESLINT_IGNORES = ['**/node_modules/', '.git/']; const LINE_SEPARATOR_REGEX = /\r|\r?\n/; const WINDOWS_PATH_SEPARATOR_REGEX = /\\/g; const configArrayCache = new Map(); const prettierignorePathCache = new Map(); const isIgnoredCache = new Map(); export function clearFormatFilesCaches() { configArrayCache.clear(); prettierignorePathCache.clear(); isIgnoredCache.clear(); } export async function formatFiles({ _: fileGlobs = [], $0: _$0, help: _help, h: _help_, version: _version, logLevel = logger.getLevel(), l: _logLevelAlias, config: _config, listDifferent, stdin, stdinFilepath, write, eslintPath, prettierPath, ignore: ignoreGlobs = [], eslintIgnore: applyEslintIgnore = true, prettierIgnore: applyPrettierIgnore = true, eslintConfigPath, prettierLast, includeDotFiles, ...prettierOptions }) { logger.setLevel(logLevel); const prettierESLintOptions = { logLevel, eslintPath, prettierPath, prettierLast, prettierOptions, }; if (eslintConfigPath) { prettierESLintOptions.eslintConfig = { overrideConfigFile: eslintConfigPath, }; } const cliOptions = { write, listDifferent, includeDotFiles, eslintConfigPath, ignoreGlobs: [...ignoreGlobs], }; if (stdin) { return formatStdin({ filePath: stdinFilepath, ...prettierESLintOptions }); } return formatFilesFromGlobs({ fileGlobs, cliOptions, prettierESLintOptions, applyEslintIgnore, applyPrettierIgnore, }); } async function formatStdin(prettierESLintOptions) { let stdinValue = ''; if (!process.stdin.isTTY) { const stdin = await text(process.stdin); stdinValue = stdin.trim(); } try { const formatted = await format({ text: stdinValue, ...prettierESLintOptions, }); process.stdout.write(formatted); return formatted; } catch (error) { logger.error('There was a problem trying to format the stdin text', `\n${indentString(error.stack, INDENT_COUNT)}`); process.exitCode = 1; return stdinValue; } } async function formatFilesFromGlobs({ fileGlobs, cliOptions, prettierESLintOptions, applyEslintIgnore, applyPrettierIgnore, }) { const concurrentGlobs = 3; const concurrentFormats = 10; const successes = []; const failures = []; const unchanged = []; try { const filePathGroups = await mapLimit(fileGlobs, concurrentGlobs, fileGlob => getFilesFromGlob(applyEslintIgnore, applyPrettierIgnore, fileGlob, cliOptions)); const filePaths = [...new Set(filePathGroups.flat())]; const formattedFiles = await mapLimit(filePaths, concurrentFormats, filePath => formatFile(path.resolve(filePath), prettierESLintOptions, cliOptions)); for (const info of formattedFiles) { if (info.error) { failures.push(info); } else if (info.unchanged) { unchanged.push(info); } else { successes.push(info); } } } catch (error) { logger.error('There was an unhandled error while formatting the files', `\n${indentString(error.stack, INDENT_COUNT)}`); process.exitCode = 1; return { error, successes, failures }; } const isSilent = logger.getLevel() === logger.levels.SILENT || cliOptions.listDifferent; if (!isSilent) { if (successes.length > 0) { console.error(messages.success({ success: chalk.green('success'), count: successes.length, countString: chalk.bold(successes.length), })); } if (failures.length > 0) { process.exitCode = 1; console.error(messages.failure({ failure: chalk.red('failure'), count: failures.length, countString: chalk.bold(failures.length), })); } if (unchanged.length > 0) { console.error(messages.unchanged({ unchanged: chalk.gray('unchanged'), count: unchanged.length, countString: chalk.bold(unchanged.length), })); } } return { successes, failures }; } async function getFilesFromGlob(applyEslintIgnore, applyPrettierIgnore, fileGlob, cliOptions) { const absoluteGlob = path.resolve(fileGlob); const basePath = globParent(absoluteGlob); const pattern = normalizePathForGlob(path.relative(basePath, absoluteGlob)); const matcher = new Minimatch(pattern, { dot: cliOptions.includeDotFiles }); const configArray = await getConfigArray(cliOptions.eslintConfigPath, cliOptions.ignoreGlobs, applyEslintIgnore); const filePaths = []; try { if (await hfs.isFile(absoluteGlob)) { if (!configArray.isFileIgnored(absoluteGlob)) { filePaths.push(absoluteGlob); } } else if (await hfs.isDirectory(basePath)) { for await (const entry of hfs.walk(basePath, { directoryFilter(dirEntry) { const absolutePath = path.resolve(basePath, dirEntry.path); return !configArray.isDirectoryIgnored(absolutePath); }, entryFilter(entry) { if (entry.isDirectory) { return false; } if (!matcher.match(entry.path)) { return false; } const absolutePath = path.resolve(basePath, entry.path); if (configArray.isFileIgnored(absolutePath)) { return false; } return true; }, })) { filePaths.push(path.resolve(basePath, entry.path)); } } } catch (error) { const code = error.code; if (code !== 'ENOENT' && code !== 'ENOTDIR') { throw error; } } if (!applyPrettierIgnore) { return filePaths; } const ignoredStatuses = await Promise.all(filePaths.map(isFilePathIgnored)); return filePaths.filter((_, index) => !ignoredStatuses[index]); } function normalizePathForGlob(filePath) { return path.posix.normalize(filePath.replaceAll(WINDOWS_PATH_SEPARATOR_REGEX, '/')); } async function formatFile(filePath, prettierESLintOptions, cliOptions) { const fileInfo = { filePath }; try { const text = await fs.readFile(filePath, 'utf8'); fileInfo.text = text; fileInfo.formatted = await format({ text, filePath, ...prettierESLintOptions, }); fileInfo.unchanged = fileInfo.text === fileInfo.formatted; if (cliOptions.write) { if (!fileInfo.unchanged) { await fs.writeFile(filePath, fileInfo.formatted); } } else if (cliOptions.listDifferent) { if (!fileInfo.unchanged) { process.exitCode = 1; console.log(fileInfo.filePath); } } else { process.stdout.write(fileInfo.formatted); } return fileInfo; } catch (error) { logger.error(`There was an error formatting "${fileInfo.filePath}":`, `\n${indentString(error.stack, INDENT_COUNT)}`); return { ...fileInfo, error }; } } async function mapLimit(items, limit, mapper) { const results = []; let index = 0; await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async () => { while (index < items.length) { const currentIndex = index; index += 1; results[currentIndex] = await mapper(items[currentIndex]); } })); return results; } async function getConfigArray(eslintConfigPath, ignoreGlobs, applyEslintIgnore) { const cacheKey = [ process.cwd(), applyEslintIgnore, eslintConfigPath ?? '', ...ignoreGlobs, ].join('\0'); const cached = configArrayCache.get(cacheKey); if (cached) { return cached; } const configArray = loadConfigArray(eslintConfigPath, ignoreGlobs, applyEslintIgnore); configArrayCache.set(cacheKey, configArray); return configArray; } async function loadConfigArray(eslintConfigPath, ignoreGlobs, applyEslintIgnore) { const configs = []; if (applyEslintIgnore) { configs.push({ ignores: DEFAULT_ESLINT_IGNORES }); const configPath = eslintConfigPath ? path.resolve(eslintConfigPath) : await new ESLint().findConfigFile(); if (configPath) { const configBasePath = path.dirname(configPath); const configModule = (await import(__rewriteRelativeImportExtension(pathToFileURL(configPath).href, true))); const rawConfig = Object.hasOwn(configModule, 'default') ? configModule.default : configModule; const configsToAdd = Array.isArray(rawConfig) ? rawConfig : [rawConfig]; configs.push(...configsToAdd.map(config => toIgnoreOnlyConfig(config, configBasePath))); } } if (ignoreGlobs.length > 0) { configs.push({ ignores: ignoreGlobs, }); } const array = new ConfigArray(configs, { basePath: process.cwd() }); await array.normalize(); return array; } function toIgnoreOnlyConfig(config, configBasePath) { if (!config || typeof config !== 'object' || Array.isArray(config)) { return config; } const { basePath, files, ignores } = config; const ignoreConfig = {}; if (files !== undefined) { ignoreConfig.files = files; } if (ignores !== undefined) { ignoreConfig.ignores = ignores; } if (Object.hasOwn(config, 'basePath')) { ignoreConfig.basePath = typeof basePath === 'string' && !path.isAbsolute(basePath) ? path.resolve(configBasePath, basePath) : basePath; } else { ignoreConfig.basePath = configBasePath; } return ignoreConfig; } function getNearestPrettierignorePath(filePath) { const { dir } = path.parse(filePath); if (!prettierignorePathCache.has(dir)) { prettierignorePathCache.set(dir, findUpSync('.prettierignore', { cwd: dir })); } return prettierignorePathCache.get(dir); } async function isFilePathIgnored(filePath) { const ignorePath = getNearestPrettierignorePath(filePath); if (!ignorePath) { return false; } const ignoreDir = path.parse(ignorePath).dir; const filePathRelativeToIgnoreDir = path.relative(ignoreDir, filePath); const isIgnored = await getIsIgnoredFromCache(ignorePath); return isIgnored(filePathRelativeToIgnoreDir); } function getIsIgnoredFromCache(filename) { const cached = isIgnoredCache.get(filename); if (cached) { return cached; } const isIgnored = getIsIgnored(filename); isIgnoredCache.set(filename, isIgnored); return isIgnored; } async function getIgnoreGlobs(filename) { const ignoreFile = await fs.readFile(filename, 'utf8'); return ignoreFile .split(LINE_SEPARATOR_REGEX) .filter(line => Boolean(line.trim())); } async function getIsIgnored(filename) { const instance = nodeIgnore(); instance.add(await getIgnoreGlobs(filename)); return instance.ignores.bind(instance); }