Skip to content

Commit

Permalink
feat: cleanup and options improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
spence-s committed Aug 18, 2024
1 parent e8e1b3e commit f346cee
Show file tree
Hide file tree
Showing 15 changed files with 332 additions and 265 deletions.
65 changes: 39 additions & 26 deletions lib/cli.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
#!/usr/bin/env node
#!/usr/bin/env -S node --no-warnings=ExperimentalWarning
// no-use-extend-native plugin creates an experimental warning so we silence it
// https://github.com/nodejs/node/issues/30810#issuecomment-1893682691

import path from 'node:path';
import process from 'node:process';
import {type Rule} from 'eslint';
import formatterPretty, {type LintResult} from 'eslint-formatter-pretty';
import meow from 'meow';
import {type Rule, type ESLint} from 'eslint';
import formatterPretty from 'eslint-formatter-pretty';
// eslint-disable-next-line import-x/no-named-default
import {default as meow} from 'meow';
import _debug from 'debug';
import type {LinterOptions, XoConfigOptions} from './types.js';
import {XO} from './xo.js';
Expand All @@ -17,12 +21,12 @@ const cli = meow(
Options
--fix Automagically fix issues
--space Use space indent instead of tabs [Default: 2]
--no-semicolon Prevent use of semicolons
--prettier Conform to Prettier code style
--space Use space indent instead of tabs [Default: 2]
--semicolon Use semicolons [Default: true]
--prettier Conform to Prettier code style [Default: false]
--print-config Print the effective ESLint config for the given file
--ignore Ignore pattern globs, can be set multiple times
--cwd=<dir> Working directory for files
--cwd=<dir> Working directory for files [Default: process.cwd()]
Examples
$ xo
Expand All @@ -46,6 +50,12 @@ const cli = meow(
space: {
type: 'string',
},
config: {
type: 'string',
},
quiet: {
type: 'boolean',
},
semicolon: {
type: 'boolean',
},
Expand Down Expand Up @@ -85,6 +95,7 @@ const baseXoConfigOptions: XoConfigOptions = {
const linterOptions: LinterOptions = {
fix: cliOptions.fix,
cwd: (cliOptions.cwd && path.resolve(cliOptions.cwd)) ?? process.cwd(),
quiet: cliOptions.quiet,
};

// Make data types for `options.space` match those of the API
Expand All @@ -107,27 +118,29 @@ if (typeof cliOptions.space === 'string') {
}
}

// if (
// process.env['GITHUB_ACTIONS'] &&
// !linterOptions.fix &&
// !linterOptions.reporter
// ) {
// linterOptions.quiet = true;
// }
if (
process.env['GITHUB_ACTIONS']
&& !linterOptions.fix
&& !cliOptions.reporter
) {
linterOptions.quiet = true;
}

const log = async (report: {
cwd: string;
results: Array<Readonly<LintResult>>;
rulesMeta: Record<string, Rule.RuleMetaData> & {cwd: string};
errorCount?: number;
errorCount: number;
warningCount: number;
fixableErrorCount: number;
fixableWarningCount: number;
results: ESLint.LintResult[];
rulesMeta: Record<string, Rule.RuleMetaData>;
}) => {
const reporter = formatterPretty;
// cliOptions.reporter
// ? await new XO(cliOptions).getFormatter(cliOptions.reporter ?? 'compact')
// :
const reporter
= cliOptions.reporter
? await new XO(linterOptions, baseXoConfigOptions).getFormatter(cliOptions.reporter)
: {format: formatterPretty};

// @ts-expect-error upgrade stuff
console.log(reporter(report.results, report));
// @ts-expect-error the types don't quite match up here
console.log(reporter.format(report.results, {cwd: linterOptions.cwd, ...report}));

process.exitCode = report.errorCount === 0 ? 0 : 1;
};
Expand Down Expand Up @@ -156,6 +169,6 @@ if (typeof cliOptions.printConfig === 'string') {
debug('xo.outputFixes success');
}

// @ts-expect-error idk man
// @ts-expect-error dues to the types in the formatter
await log(report);
}
5 changes: 2 additions & 3 deletions lib/create-eslint-config/handle-prettier-options.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
// eslint-disable-next-line import-x/no-named-default
import {default as prettier} from 'prettier';
import {type FlatESLintConfig} from 'eslint-define-config';
import {type Linter, type ESLint} from 'eslint';
import pluginPrettier from 'eslint-plugin-prettier';
import {type ESLint} from 'eslint';
import configPrettier from 'eslint-config-prettier';
import {type XoConfigItem} from '../types.js';

Expand All @@ -17,7 +16,7 @@ let cachedPrettierConfig: Record<string, unknown>;
* @param xoUserConfig
* @param eslintConfigItem
*/
export async function handlePrettierOptions(cwd: string, xoUserConfig: XoConfigItem, eslintConfigItem: FlatESLintConfig): Promise<void> {
export async function handlePrettierOptions(cwd: string, xoUserConfig: XoConfigItem, eslintConfigItem: Linter.Config): Promise<void> {
const prettierOptions = cachedPrettierConfig ?? (await prettier.resolveConfig(cwd, {editorconfig: true})) ?? {};

// Only look up prettier once per run
Expand Down
96 changes: 10 additions & 86 deletions lib/create-eslint-config/index.ts
Original file line number Diff line number Diff line change
@@ -1,92 +1,18 @@
/* eslint-disable complexity */
import pluginAva from 'eslint-plugin-ava';
import pluginUnicorn from 'eslint-plugin-unicorn';
import pluginImport from 'eslint-plugin-import-x';
import pluginN from 'eslint-plugin-n';
import pluginComments from '@eslint-community/eslint-plugin-eslint-comments';
import pluginPromise from 'eslint-plugin-promise';
import pluginNoUseExtendNative from 'eslint-plugin-no-use-extend-native';

import process from 'node:process';
import configXoTypescript from 'eslint-config-xo-typescript';
import stylisticPlugin from '@stylistic/eslint-plugin';
import arrify from 'arrify';
import globals from 'globals';
import {type FlatESLintConfig} from 'eslint-define-config';
import {
DEFAULT_IGNORES,
TS_EXTENSIONS,
TS_FILES_GLOB,
ALL_FILES_GLOB,
JS_EXTENSIONS,
ALL_EXTENSIONS,
} from '../constants.js';
import {type Linter} from 'eslint';
import {type XoConfigItem} from '../types.js';
import {jsRules, tsRules, baseRules} from '../rules.js';
import {xoPluginsConfig} from './xo-plugins-config.js';
import {xoToEslintConfigItem} from './xo-to-eslint-config-item.js';
import {handlePrettierOptions} from './handle-prettier-options.js';

/**
* Takes a xo flat config and returns an eslint flat config
*/
async function createConfig(userConfigs?: XoConfigItem[]): Promise<FlatESLintConfig[]> {
const cwd = '';

const baseConfig: FlatESLintConfig[] = [
{
ignores: DEFAULT_IGNORES,
},
{
files: [ALL_FILES_GLOB],
plugins: {
'no-use-extend-native': pluginNoUseExtendNative,
ava: pluginAva,
unicorn: pluginUnicorn,
'import-x': pluginImport,
n: pluginN,
'@eslint-community/eslint-comments': pluginComments,
promise: pluginPromise,
'@stylistic': stylisticPlugin,
},
languageOptions: {
globals: {
...globals.es2021,
...globals.node,
},
ecmaVersion: configXoTypescript[0]?.languageOptions?.ecmaVersion,
sourceType: configXoTypescript[0]?.languageOptions?.sourceType,
parserOptions: {
...configXoTypescript[0]?.languageOptions?.parserOptions,
},
},
settings: {
'import-x/extensions': ALL_EXTENSIONS,
'import-x/core-modules': ['electron', 'atom'],
'import-x/parsers': {
espree: JS_EXTENSIONS,
'@typescript-eslint/parser': TS_EXTENSIONS,
},
'import-x/external-module-folders': [
'node_modules',
'node_modules/@types',
],
'import-x/resolver': {
node: ALL_EXTENSIONS,
},
},
/**
* These are the base rules that are always applied to all js and ts file types
*/
rules: {...baseRules, ...jsRules},
},
{
plugins: configXoTypescript[1]?.plugins,
files: [TS_FILES_GLOB],
languageOptions: configXoTypescript[1]?.languageOptions,
/** This turns on rules in typescript-eslint and turns off rules from eslint that conflict */
rules: tsRules,
},
...configXoTypescript.slice(2),
];

export async function createConfig(userConfigs?: XoConfigItem[], cwd?: string): Promise<Linter.Config[]> {
const baseConfig: Linter.Config[] = [...xoPluginsConfig];
/**
* Since configs are merged and the last config takes precedence
* this means we need to handle both true AND false cases for each option.
Expand All @@ -99,10 +25,7 @@ async function createConfig(userConfigs?: XoConfigItem[]): Promise<FlatESLintCon
continue;
}

/**
* Special case
* global ignores
*/
/** Special case global ignores */
if (
keysOfXoConfig.length === 1
&& keysOfXoConfig[0] === 'ignores'
Expand All @@ -111,7 +34,7 @@ async function createConfig(userConfigs?: XoConfigItem[]): Promise<FlatESLintCon
continue;
}

// An eslint config item with rules and files initialized
/** An eslint config item derived from the xo config item with rules and files initialized */
const eslintConfigItem = xoToEslintConfigItem(xoUserConfig);

if (xoUserConfig.semicolon === false) {
Expand All @@ -135,8 +58,9 @@ async function createConfig(userConfigs?: XoConfigItem[]): Promise<FlatESLintCon
}

if (xoUserConfig.prettier) {
// TODO: how to handle prettier options if user just wants eslint config and not cli?
// eslint-disable-next-line no-await-in-loop
await handlePrettierOptions(cwd, xoUserConfig, eslintConfigItem);
await handlePrettierOptions(cwd ?? process.cwd(), xoUserConfig, eslintConfigItem);
} else if (xoUserConfig.prettier === false) {
// Turn prettier off for a subset of files
eslintConfigItem.rules['prettier/prettier'] = 'off';
Expand Down
78 changes: 78 additions & 0 deletions lib/create-eslint-config/xo-plugins-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@

import pluginAva from 'eslint-plugin-ava';
import pluginUnicorn from 'eslint-plugin-unicorn';
import pluginImport from 'eslint-plugin-import-x';
import pluginN from 'eslint-plugin-n';
import pluginComments from '@eslint-community/eslint-plugin-eslint-comments';
import pluginPromise from 'eslint-plugin-promise';
import pluginNoUseExtendNative from 'eslint-plugin-no-use-extend-native';
import configXoTypescript from 'eslint-config-xo-typescript';
import stylisticPlugin from '@stylistic/eslint-plugin';
import globals from 'globals';
import {type Linter} from 'eslint';
import {
DEFAULT_IGNORES,
TS_EXTENSIONS,
TS_FILES_GLOB,
ALL_FILES_GLOB,
JS_EXTENSIONS,
ALL_EXTENSIONS,
} from '../constants.js';
import {jsRules, tsRules, baseRules} from '../rules.js';

export const xoPluginsConfig: Linter.Config[] = [
{
ignores: DEFAULT_IGNORES,
},
{
files: [ALL_FILES_GLOB],
plugins: {
'no-use-extend-native': pluginNoUseExtendNative,
ava: pluginAva,
unicorn: pluginUnicorn,
'import-x': pluginImport,
n: pluginN,
'@eslint-community/eslint-comments': pluginComments,
promise: pluginPromise,
'@stylistic': stylisticPlugin,
},
languageOptions: {
globals: {
...globals.es2021,
...globals.node,
},
ecmaVersion: configXoTypescript[0]?.languageOptions?.ecmaVersion,
sourceType: configXoTypescript[0]?.languageOptions?.sourceType,
parserOptions: {
...configXoTypescript[0]?.languageOptions?.parserOptions,
},
},
settings: {
'import-x/extensions': ALL_EXTENSIONS,
'import-x/core-modules': ['electron', 'atom'],
'import-x/parsers': {
espree: JS_EXTENSIONS,
'@typescript-eslint/parser': TS_EXTENSIONS,
},
'import-x/external-module-folders': [
'node_modules',
'node_modules/@types',
],
'import-x/resolver': {
node: ALL_EXTENSIONS,
},
},
/**
* These are the base rules that are always applied to all js and ts file types
*/
rules: {...baseRules, ...jsRules},
},
{
plugins: configXoTypescript[1]?.plugins,
files: [TS_FILES_GLOB],
languageOptions: configXoTypescript[1]?.languageOptions,
/** This turns on rules in typescript-eslint and turns off rules from eslint that conflict */
rules: tsRules,
},
...configXoTypescript.slice(2),
];
6 changes: 3 additions & 3 deletions lib/create-eslint-config/xo-to-eslint-config-item.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

import arrify from 'arrify';
import {type SetRequired} from 'type-fest';
import {type FlatESLintConfig} from 'eslint-define-config';
import {type Linter} from 'eslint';
import {
ALL_FILES_GLOB,
} from '../constants.js';
Expand All @@ -16,10 +16,10 @@ import {type XoConfigItem} from '../types.js';
* @param xoConfig
* @returns eslintConfig
*/
export const xoToEslintConfigItem = (xoConfig: XoConfigItem): SetRequired<FlatESLintConfig, 'rules' | 'files'> => {
export const xoToEslintConfigItem = (xoConfig: XoConfigItem): SetRequired<Linter.Config, 'rules' | 'files'> => {
const {files, rules, space, prettier, ignores, semicolon, ..._xoConfig} = xoConfig;

const eslintConfig: SetRequired<FlatESLintConfig, 'rules' | 'files'> = {
const eslintConfig: SetRequired<Linter.Config, 'rules' | 'files'> = {
..._xoConfig,
files: arrify(xoConfig.files ?? ALL_FILES_GLOB),
rules: xoConfig.rules ?? {},
Expand Down
Loading

0 comments on commit f346cee

Please sign in to comment.