UNPKG

xo

Version:

JavaScript/TypeScript linter (ESLint wrapper) with great defaults

125 lines 5.67 kB
/* eslint-disable complexity -- Translating every XO config option into ESLint rules is inherently branchy. */ import arrify from 'arrify'; import { getPrettierConfig } from 'eslint-config-xo'; import { config } from './config.js'; import { xoToEslintConfigItem } from './utils.js'; /** Merge all plugins from every config into a single config entry at the start of the array, ensuring user-provided plugins take precedence. This avoids ESLint's flat config rejecting duplicate plugin names. */ const hoistPlugins = (configs, userPluginOverrides) => { const plugins = {}; const configsWithoutPlugins = []; for (const configItem of configs) { const { plugins: configPlugins } = configItem; if (!configPlugins) { configsWithoutPlugins.push(configItem); continue; } // ESLint flat config rejects duplicate plugin names, so merge all plugins into one config. Object.assign(plugins, configPlugins); const { plugins: _ignored, ...configWithoutPlugins } = configItem; if (Object.keys(configWithoutPlugins).length > 0) { configsWithoutPlugins.push(configWithoutPlugins); } } for (const [pluginName, plugin] of userPluginOverrides) { plugins[pluginName] = plugin; } if (Object.keys(plugins).length === 0) { return configsWithoutPlugins; } return [ { name: 'xo/plugins', plugins, }, ...configsWithoutPlugins, ]; }; /** Takes an XO flat config and returns an ESLint flat config. */ export function xoToEslintConfig(flatXoConfig) { const baseConfig = [...config]; const userPluginOverrides = new Map(); for (const xoConfigItem of flatXoConfig ?? []) { const { plugins } = xoConfigItem; if (!plugins) { continue; } for (const [pluginName, plugin] of Object.entries(plugins)) { userPluginOverrides.set(pluginName, plugin); } } /** Since configs are merged and the last config takes precedence this means we need to handle both true AND false cases for each option. For example, we need to turn `prettier`, `space`, `semi`, etc. on or off for a specific file. */ for (const xoConfigItem of flatXoConfig ?? []) { const keysOfXoConfig = Object.keys(xoConfigItem); if (keysOfXoConfig.length === 0) { continue; } /** Special case global ignores */ if (xoConfigItem.ignores !== undefined) { if (keysOfXoConfig.length === 1) { baseConfig.push({ ignores: arrify(xoConfigItem.ignores) }); continue; } if (keysOfXoConfig.length === 2 && xoConfigItem.name !== undefined) { baseConfig.push({ name: xoConfigItem.name, ignores: arrify(xoConfigItem.ignores) }); continue; } } /** An ESLint config item derived from the XO config item with rules and files initialized. */ const eslintConfigItem = xoToEslintConfigItem(xoConfigItem); const isUsingSpaces = Boolean(xoConfigItem.space); if (xoConfigItem.semicolon === false) { eslintConfigItem.rules ??= {}; eslintConfigItem.rules['@stylistic/semi'] = ['error', 'never']; eslintConfigItem.rules['@stylistic/semi-spacing'] = ['error', { before: false, after: true }]; eslintConfigItem.rules['@stylistic/member-delimiter-style'] = [ 'error', { multiline: { delimiter: 'none' }, singleline: { delimiter: 'comma', requireLast: false }, }, ]; } if (isUsingSpaces) { const spaces = typeof xoConfigItem.space === 'number' ? xoConfigItem.space : 2; eslintConfigItem.rules ??= {}; eslintConfigItem.rules['@stylistic/indent'] = ['error', spaces, { SwitchCase: 1 }]; // eslint-disable-line @typescript-eslint/naming-convention eslintConfigItem.rules['@stylistic/indent-binary-ops'] = ['error', spaces]; } else if (xoConfigItem.space === false) { eslintConfigItem.rules ??= {}; eslintConfigItem.rules['@stylistic/indent'] = ['error', 'tab', { SwitchCase: 1 }]; // eslint-disable-line @typescript-eslint/naming-convention eslintConfigItem.rules['@stylistic/indent-binary-ops'] = ['error', 'tab']; } // Delegate Prettier integration to `eslint-config-xo`. Its config is pushed after `eslintConfigItem` below so it comes last, disabling the conflicting stylistic rules set above. const prettierConfig = getPrettierConfig({ prettier: xoConfigItem.prettier, // `Space` allows `string` for legacy reasons, but Prettier only needs `boolean | number`. space: xoConfigItem.space, semicolon: xoConfigItem.semicolon, files: eslintConfigItem.files, }); if (xoConfigItem.prettier === false) { // Turn Prettier off for a subset of files eslintConfigItem.rules ??= {}; eslintConfigItem.rules['prettier/prettier'] = 'off'; } if (Object.keys(eslintConfigItem).length > 0) { baseConfig.push(eslintConfigItem); } if (prettierConfig) { baseConfig.push(prettierConfig); } } // User plugins should always win, even if XO injects plugins later in the config list. return hoistPlugins(baseConfig, userPluginOverrides); } export default xoToEslintConfig; //# sourceMappingURL=xo-to-eslint.js.map