prettier-eslint
Version:
Formats your JavaScript using prettier followed by eslint --fix
225 lines (222 loc) • 8.54 kB
JavaScript
import fs from 'node:fs/promises';
import path from 'node:path';
import { oneLine, stripIndent } from 'common-tags';
import indentString from 'indent-string';
import { format as prettyFormat } from 'pretty-format';
import { hash } from 'stable-hash-x';
import { extractFileExtensions, getESLint, getModulePath, getOptionsForFormatting, importModule, isDebugEnabled, isTraceEnabled, logger, mergeConfigs, } from "./utils.js";
const eslintConfigCache = new Map();
export async function format(options) {
const { output } = await analyze(options);
return output;
}
export const DEFAULT_ESLINT_EXTENSIONS = [
'.cjs',
'.cts',
'.js',
'.jsx',
'.ts',
'.tsx',
'.mjs',
'.mts',
'.vue',
'.svelte',
];
export const DEFAULT_ESLINT_FILES = DEFAULT_ESLINT_EXTENSIONS.map(ext => `**/*${ext}`);
export async function analyze(options) {
const { logLevel = getDefaultLogLevel() } = options;
logger.setLevel(logLevel);
if (isTraceEnabled()) {
logger.trace('called analyze with options:', prettyFormat(options));
}
const { filePath, text = await getTextFromFilePath(filePath), eslintPath = getModulePath(filePath, 'eslint'), prettierPath = getModulePath(filePath, 'prettier'), prettierLast, fallbackPrettierOptions, } = options;
const eslintConfig = mergeConfigs(options.eslintConfig, await getESLintConfig(filePath, eslintPath, options.eslintConfig || {}));
const prettierOptions = mergeConfigs(filePath ? { filepath: filePath } : { parser: 'babel' }, await getPrettierConfig(filePath, prettierPath), options.prettierOptions);
const formattingOptions = getOptionsForFormatting(eslintConfig, prettierOptions, fallbackPrettierOptions);
if (isDebugEnabled()) {
logger.debug('inferred options:', prettyFormat({
filePath,
text,
eslintPath,
prettierPath,
eslintConfig: formattingOptions.eslint,
prettierOptions: formattingOptions.prettier,
logLevel,
prettierLast,
}));
}
const fileExtension = path.extname(filePath || '');
const eslintFiles = eslintConfig.files?.length
? eslintConfig.files
: DEFAULT_ESLINT_FILES;
const eslintExtensions = extractFileExtensions(eslintFiles.flat());
const onlyPrettier = filePath
? !eslintExtensions.includes(fileExtension)
: false;
const prettify = createPrettify(formattingOptions.prettier, prettierPath);
if (onlyPrettier) {
return prettify(text);
}
formattingOptions.eslint.languageOptions ??= {};
if (!formattingOptions.eslint.languageOptions.parser) {
if (['.ts', '.tsx'].includes(fileExtension)) {
formattingOptions.eslint.languageOptions.parser = await importModule('@typescript-eslint/parser');
}
else if (['.vue'].includes(fileExtension)) {
formattingOptions.eslint.languageOptions.parser =
await importModule('vue-eslint-parser');
}
else if (['.svelte'].includes(fileExtension)) {
formattingOptions.eslint.languageOptions.parser = await importModule('svelte-eslint-parser');
}
}
const eslintFix = createEslintFix(formattingOptions.eslint, eslintPath);
if (prettierLast) {
const eslintFixed = await eslintFix(text, filePath);
return prettify(eslintFixed);
}
const { output } = await prettify(text);
return eslintFix(output, filePath);
}
function createPrettify(formatOptions, prettierPath) {
return async function prettify(param) {
let text = param;
let messages = [];
if (typeof text !== 'string') {
messages = text.messages;
text = text.output;
}
logger.debug('calling prettier on text');
logger.trace(stripIndent `
prettier input:
${indentString(text, 2)}
`);
const prettier = await importModule(prettierPath, 'prettier');
try {
logger.trace('calling prettier.format with the text and prettierOptions');
const output = await prettier.format(text, formatOptions);
logger.trace('prettier: output === input', output === text);
logger.trace(stripIndent `
prettier output:
${indentString(output, 2)}
`);
return { output, messages };
}
catch (error) {
logger.error('prettier formatting failed due to a prettier error');
throw error;
}
};
}
function createEslintFix(eslintConfig, eslintPath) {
return async function eslintFix(text, filePath) {
eslintConfig = {
...eslintConfig,
overrideConfig: {
languageOptions: eslintConfig.languageOptions,
rules: eslintConfig.rules,
ignores: eslintConfig.ignorePatterns ?? [],
plugins: eslintConfig.plugins ?? {},
settings: eslintConfig.settings ?? {},
...eslintConfig.overrideConfig,
},
overrideConfigFile: true,
};
delete eslintConfig.baseConfig;
delete eslintConfig.language;
delete eslintConfig.languageOptions;
delete eslintConfig.linterOptions;
delete eslintConfig.rules;
delete eslintConfig.ignorePatterns;
delete eslintConfig.plugins;
delete eslintConfig.settings;
delete eslintConfig.overrideConfig.plugins['@'];
const eslint = await getESLint(eslintPath, eslintConfig);
try {
logger.trace('calling eslint.lintText with the text');
const report = await eslint.lintText(text, {
filePath,
warnIgnored: true,
});
if (isTraceEnabled()) {
logger.trace('eslint.lintText returned the following report:', prettyFormat(report));
}
const [{ output = text, messages }] = report;
logger.trace('eslint --fix: output === input', output === text);
logger.trace(stripIndent `
eslint --fix output:
${indentString(output, 2)}
`);
return { output, messages };
}
catch (error) {
logger.error('eslint --fix failed due to an eslint error');
throw error;
}
};
}
async function getTextFromFilePath(filePath) {
try {
logger.trace(oneLine `
attempting fs.readFile to get
the text for file at "${filePath}"
`);
return await fs.readFile(filePath, 'utf8');
}
catch (error) {
logger.error(oneLine `
failed to get the text to format
from the given filePath: "${filePath}"
`);
throw error;
}
}
function getESLintApiOptions(eslintConfig) {
return {
ignore: eslintConfig.ignore ?? true,
allowInlineConfig: eslintConfig.allowInlineConfig ?? true,
cwd: eslintConfig.cwd,
baseConfig: eslintConfig.baseConfig,
overrideConfig: eslintConfig.overrideConfig,
overrideConfigFile: eslintConfig.overrideConfigFile,
plugins: eslintConfig.plugins,
};
}
async function getESLintConfig(filePath, eslintPath, eslintConfig) {
const configPath = filePath || process.cwd();
const configOptions = getESLintApiOptions(eslintConfig);
const cacheKey = hash({ filePath, eslintPath, configOptions });
const cachedConfig = eslintConfigCache.get(cacheKey);
if (cachedConfig) {
return cachedConfig;
}
logger.trace(oneLine `
creating ESLint CLI Engine to get the config for
"${configPath}"
`);
const eslint = await getESLint(eslintPath, configOptions);
try {
logger.debug(`getting eslint config for file at "${filePath}"`);
const config = (await eslint.calculateConfigForFile(filePath));
if (isTraceEnabled()) {
logger.trace(`eslint config for "${filePath}" received`, prettyFormat(config));
}
eslintConfigCache.set(cacheKey, config);
return config;
}
catch {
logger.debug('Unable to find config');
return { rules: {} };
}
}
async function getPrettierConfig(filePath, prettierPath) {
const prettier = await importModule(prettierPath, 'prettier');
const { resolveConfig } = prettier;
return resolveConfig?.(filePath);
}
function getDefaultLogLevel() {
return process.env.LOG_LEVEL || 'warn';
}
export * from "./utils.js";
export default format;
//# sourceMappingURL=index.js.map