xo
Version:
JavaScript/TypeScript linter (ESLint wrapper) with great defaults
272 lines (270 loc) • 10.1 kB
JavaScript
#!/usr/bin/env node
import path from 'node:path';
import fs from 'node:fs/promises';
import process from 'node:process';
import { ESLint } from 'eslint';
import formatterPretty from 'eslint-formatter-pretty';
import getStdin from 'get-stdin';
import meow from 'meow';
import { pathExists } from 'path-exists';
import findCacheDirectory from 'find-cache-directory';
import { cacheDirName, tsExtensions } from './lib/constants.js';
import { Xo } from './lib/xo.js';
import openReport from './lib/open-report.js';
const isErrorWithExitCode = (error) => error instanceof Error && 'exitCode' in error && typeof error.exitCode === 'number';
const cli = meow(`
Usage
$ xo [<file|glob> ...]
Options
--fix Automagically fix issues
--fix-dry-run Automagically fix issues without saving the changes to the file system
--reporter Reporter to use
--space Use space indent instead of tabs [Default: 2]
--config Path to a XO configuration file
--semicolon Use semicolons [Default: true]
--prettier Format with prettier or turn off Prettier-conflicted rules when set to 'compat' [Default: false]
--print-config Print the effective ESLint config for the given file
--version Print XO version
--open Open files with issues in your editor
--quiet Show only errors and no warnings
--max-warnings Number of warnings to trigger nonzero exit code [Default: -1]
--stdin Validate/fix code from stdin
--stdin-filename Specify a filename for the --stdin option
--ignore Ignore pattern globs, can be set multiple times
--suppressions-location Path to a custom ESLint suppressions file
--cwd=<dir> Working directory for files [Default: process.cwd()]
Examples
$ xo
$ xo index.js
$ xo *.js !foo.js
$ xo --space
$ xo --print-config=index.js
`, {
importMeta: import.meta,
autoVersion: false,
booleanDefault: undefined,
flags: {
fix: {
type: 'boolean',
default: false,
},
fixDryRun: {
type: 'boolean',
default: false,
},
reporter: {
type: 'string',
},
space: {
type: 'string',
},
configPath: {
type: 'string',
aliases: ['config'],
},
quiet: {
type: 'boolean',
},
semicolon: {
type: 'boolean',
},
prettier: {
type: 'boolean',
},
cwd: {
type: 'string',
default: process.cwd(),
},
printConfig: {
type: 'string',
},
version: {
type: 'boolean',
},
stdin: {
type: 'boolean',
},
stdinFilename: {
type: 'string',
default: 'stdin.js',
},
open: {
type: 'boolean',
},
ignore: {
type: 'string',
isMultiple: true,
aliases: ['ignores'],
},
suppressionsLocation: {
type: 'string',
},
maxWarnings: {
type: 'number',
default: -1,
},
},
});
const { input, flags: cliOptions, showVersion } = cli;
const baseXoConfigOptions = {
space: cliOptions.space,
semicolon: cliOptions.semicolon,
prettier: cliOptions.prettier,
ignores: cliOptions.ignore,
};
const linterOptions = {
fix: cliOptions.fix || cliOptions.fixDryRun,
cwd: cliOptions.cwd === '' ? process.cwd() : path.resolve(cliOptions.cwd),
quiet: cliOptions.quiet,
ts: true,
configPath: cliOptions.configPath,
suppressionsLocation: cliOptions.suppressionsLocation,
};
// Make data types for `options.space` match those of the API
if (typeof cliOptions.space === 'string') {
cliOptions.space = cliOptions.space.trim();
if (/^\d+$/v.test(cliOptions.space)) {
baseXoConfigOptions.space = Number(cliOptions.space);
}
else if (cliOptions.space === 'true') {
baseXoConfigOptions.space = true;
}
else if (cliOptions.space === 'false') {
baseXoConfigOptions.space = false;
}
else {
if (cliOptions.space !== '') {
// Assume `options.space` was set to a filename when run as `xo --space file.js`
input.push(cliOptions.space);
}
baseXoConfigOptions.space = true;
}
}
const isGitHubActions = Boolean(process.env['GITHUB_ACTIONS']);
if (isGitHubActions
&& !linterOptions.fix
&& cliOptions.reporter === undefined) {
linterOptions.quiet = true;
}
// --max-warnings needs warning counts, which --quiet filters out
if (cliOptions.maxWarnings >= 0) {
linterOptions.quiet = false;
}
const log = async (report, xo) => {
const reporterName = cliOptions.reporter;
const shouldUsePrettyReporter = reporterName === undefined;
// Hide warnings when there are errors to reduce noise in the default human-readable output.
const results = shouldUsePrettyReporter && report.errorCount > 0 && cliOptions.maxWarnings < 0
? ESLint.getErrorResults(report.results)
: report.results;
if (shouldUsePrettyReporter) {
const counts = {
errorCount: 0,
warningCount: 0,
fixableErrorCount: 0,
fixableWarningCount: 0,
};
for (const result of results) {
counts.errorCount += result.errorCount;
counts.warningCount += result.warningCount;
counts.fixableErrorCount += result.fixableErrorCount;
counts.fixableWarningCount += result.fixableWarningCount;
}
const formatterMetadata = {
cwd: linterOptions.cwd,
...report,
...counts,
results,
};
console.log(formatterPretty(results, formatterMetadata));
}
else {
const reporter = await xo.getFormatter(reporterName);
console.log(await reporter.format(results));
}
process.exitCode = report.errorCount === 0 ? 0 : 1;
if (cliOptions.maxWarnings >= 0 && report.warningCount > cliOptions.maxWarnings) {
console.error(`XO found too many warnings (maximum: ${cliOptions.maxWarnings}).`);
process.exitCode = 1;
}
};
try {
if (cliOptions.version) {
showVersion();
}
if (cliOptions.stdin) {
const stdin = await getStdin();
let shouldRemoveStdInFile = false;
// For TypeScript, we need a file on the filesystem to lint it or else @typescript-eslint will blow up.
// We create a temporary file in the node_modules/.cache/xo-linter directory to avoid conflicts with the user's files and lint that file as if it were the stdin input as a work around.
// We clean up the file after linting.
if (cliOptions.stdinFilename !== '' && tsExtensions.includes(path.extname(cliOptions.stdinFilename).slice(1))) {
const absoluteFilePath = path.resolve(cliOptions.cwd, cliOptions.stdinFilename);
if (!await pathExists(absoluteFilePath)) {
const cacheDir = findCacheDirectory({ name: cacheDirName, cwd: linterOptions.cwd }) ?? path.join(cliOptions.cwd, 'node_modules', '.cache', cacheDirName);
cliOptions.stdinFilename = path.join(cacheDir, path.basename(absoluteFilePath));
shouldRemoveStdInFile = true;
baseXoConfigOptions.ignores = [
'!**/node_modules/**',
'!node_modules/**',
'!node_modules/',
`!${path.relative(cliOptions.cwd, cliOptions.stdinFilename)}`,
];
if (!await pathExists(path.dirname(cliOptions.stdinFilename))) {
await fs.mkdir(path.dirname(cliOptions.stdinFilename), { recursive: true });
}
await fs.writeFile(cliOptions.stdinFilename, stdin);
}
}
if (linterOptions.fix) {
const xo = new Xo(linterOptions, baseXoConfigOptions);
const { results } = await xo.lintText(stdin, {
filePath: cliOptions.stdinFilename,
});
const [result] = results;
process.stdout.write((result?.output) ?? stdin);
process.exit(0);
}
if (cliOptions.open) {
console.error('The `--open` flag is not supported on stdin');
if (shouldRemoveStdInFile) {
await fs.rm(cliOptions.stdinFilename);
}
process.exit(1);
}
const xo = new Xo(linterOptions, baseXoConfigOptions);
await log(await xo.lintText(stdin, { filePath: cliOptions.stdinFilename, warnIgnored: false }), xo);
if (shouldRemoveStdInFile) {
await fs.rm(cliOptions.stdinFilename);
}
process.exit(0);
}
if (typeof cliOptions.printConfig === 'string') {
if (input.length > 0 || cliOptions.printConfig === '') {
console.error('The `--print-config` flag must be used with exactly one filename');
process.exit(1);
}
const absoluteFilePath = path.resolve(cliOptions.cwd, cliOptions.printConfig);
const config = await new Xo(linterOptions, baseXoConfigOptions).calculateConfigForFile(absoluteFilePath);
console.log(JSON.stringify(config, undefined, '\t'));
}
else {
const xo = new Xo(linterOptions, baseXoConfigOptions);
const report = await xo.lintFiles(input);
if (cliOptions.fix) {
await Xo.outputFixes(report);
}
if (cliOptions.open) {
await openReport(report);
}
await log(report, xo);
}
}
catch (error) {
if (isErrorWithExitCode(error)) {
console.error(error.message);
process.exit(error.exitCode);
}
throw error;
}
//# sourceMappingURL=cli.js.map