UNPKG

xo

Version:

JavaScript/TypeScript linter (ESLint wrapper) with great defaults

272 lines (270 loc) 10.1 kB
#!/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