prettier-eslint
Version:
Formats your JavaScript using prettier followed by eslint --fix
210 lines (207 loc) • 8.01 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 getLogger from 'loglevel-colored-level-prefix';
import { extractFileExtensions, formatForLog, getESLint, getModulePath, getOptionsForFormatting, importModule, mergeConfigs, } from "./utils.js";
const logger = getLogger({ prefix: 'prettier-eslint' });
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);
logger.trace('called analyze with options:', formatForLog(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);
logger.debug('inferred options:', formatForLog({
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.overrideConfigFile = true;
eslintConfig.overrideConfig = {
languageOptions: eslintConfig.languageOptions,
rules: eslintConfig.rules,
ignores: eslintConfig.ignorePatterns ?? [],
plugins: eslintConfig.plugins ?? {},
settings: eslintConfig.settings ?? {},
...eslintConfig.overrideConfig,
};
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,
});
logger.trace('eslint.lintText returned the following report:', formatForLog(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,
baseConfig: eslintConfig.baseConfig,
overrideConfig: eslintConfig.overrideConfig,
overrideConfigFile: eslintConfig.overrideConfigFile,
plugins: eslintConfig.plugins,
};
}
async function getESLintConfig(filePath, eslintPath, eslintConfig) {
if (filePath) {
eslintConfig.cwd = path.dirname(filePath);
}
logger.trace(oneLine `
creating ESLint CLI Engine to get the config for
"${filePath || process.cwd()}"
`);
const eslint = await getESLint(eslintPath, getESLintApiOptions(eslintConfig));
try {
logger.debug(`getting eslint config for file at "${filePath}"`);
const config = (await eslint.calculateConfigForFile(filePath));
logger.trace(`eslint config for "${filePath}" received`, formatForLog(config));
return {
...eslintConfig,
...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