UNPKG

xo

Version:

JavaScript/TypeScript linter (ESLint wrapper) with great defaults

159 lines (152 loc) • 8.22 kB
import path from 'node:path'; import micromatch from 'micromatch'; import arrify from 'arrify'; import { typescriptParser } from 'eslint-config-xo'; import { codeFilesGlob, jsExtensions, jsFilesGlob, } from './constants.js'; export { typescriptParser }; // eslint-disable-line unicorn/prefer-export-from -- Also used locally /** Convert a `xo` config item to an ESLint config item. In a flat structure these config items represent the config object items. XO-only options (`space`, `semicolon`, `prettier`) are stripped, `files` and `ignores` are normalized to arrays, and all other ESLint config properties are preserved. A non-empty item without `files` is scoped to code files. Empty items stay empty so the caller can drop them. @param xoConfig - The XO config item to convert. @returns The equivalent ESLint config item. */ export const xoToEslintConfigItem = (xoConfig) => { const { files, rules, space, prettier, ignores, semicolon: hasSemicolon, ..._xoConfig } = xoConfig; const eslintConfig = { ..._xoConfig, ...(ignores !== undefined && { ignores: arrify(ignores) }), ...(rules && { rules }), }; // Without `files`, ESLint applies a config item to every linted file, including HTML, Markdown, JSON, and CSS. Scoping it to code files keeps JavaScript-specific settings, such as the TypeScript parser and type-aware rules, away from those files. if (files !== undefined || Object.keys(eslintConfig).length > 0) { eslintConfig.files = arrify(files ?? codeFilesGlob); } return eslintConfig; }; /** Function used to match files which should be included in the `tsconfig.json` files. @param cwd - The current working directory to resolve relative filepaths. @param files - The _absolute_ file paths to match against the globs. @param globs - The globs to match the files against. @param ignores - The globs to ignore when matching the files. @returns An array of file paths that match the globs and do not match the ignores. */ export const matchFilesForTsConfig = (cwd, files, globs, ignores) => micromatch( // Relative file paths avoid the leading `./` mismatch described in https://github.com/micromatch/micromatch/issues/217 files?.map(file => path.relative(cwd, file)) ?? [], // Keep glob and ignore patterns unchanged because micromatch treats backslashes as escape characters, including on Windows. globs, { dot: true, ignore: ignores, cwd, }).map(file => path.resolve(cwd, file)); const legacyPropertyHints = { overrides: 'Use an array of config objects with `files` patterns instead.', extends: 'Spread the config directly into your XO config array.', env: 'Use `languageOptions.globals` instead.', globals: 'Move to `languageOptions.globals`.', parser: 'Move to `languageOptions.parser`.', parserOptions: 'Move to `languageOptions.parserOptions`.', root: 'Not needed in flat config.', ecmaVersion: 'Move to `languageOptions.ecmaVersion`.', sourceType: 'Move to `languageOptions.sourceType`.', noInlineConfig: 'Move to `linterOptions.noInlineConfig`.', reportUnusedDisableDirectives: 'Move to `linterOptions.reportUnusedDisableDirectives`.', ignorePatterns: 'Use `ignores` instead.', react: 'Install `eslint-config-xo-react` and spread it into your XO config instead.', }; /** Validate an XO config array for legacy ESLint config properties that are not supported in flat config. @param xoConfig - The flat XO config to validate. */ export const validateXoConfig = (xoConfig) => { // Skip the first item (internal base config prepended by XO) for (const config of xoConfig.values().drop(1)) { for (const key of Object.keys(config)) { const hint = legacyPropertyHints[key]; if (hint !== undefined) { throw new Error(`Invalid XO config property \`${key}\`. ${hint}`); } } } }; /** Once a config is resolved, it is pre-processed to ensure that all properties are set correctly. This includes ensuring that user-defined properties can override XO defaults, and that files are parsed correctly and performantly based on the user's XO config. @param xoConfig - The flat XO config to pre-process. @returns The pre-processed flat XO config. */ // eslint-disable-next-line complexity export const preProcessXoConfig = (xoConfig) => { validateXoConfig(xoConfig); const tsFilesGlob = []; const tsFilesIgnoresGlob = []; // The first config item is the internal base config; push it through unmodified. const processedConfig = xoConfig[0] ? [{ ...xoConfig[0] }] : []; for (const { ...config } of xoConfig.values().drop(1)) { const { languageOptions } = config; const parserOptions = languageOptions?.['parserOptions']; // Use TS parser/plugin for JS files if the config contains TypeScript rules which are applied to JS files. // typescript-eslint rules set to "off" are ignored and not applied to JS files. if (config.rules && languageOptions?.['parser'] === undefined && parserOptions?.project === undefined && parserOptions?.programs === undefined && !config.plugins?.['@typescript-eslint']) { const hasTsRules = Object.entries(config.rules).some(rulePair => { // If it's not a @typescript-eslint rule, we don't care if (!rulePair[0].startsWith('@typescript-eslint/')) { return false; } if (Array.isArray(rulePair[1])) { return rulePair[1]?.[0] !== 'off' && rulePair[1]?.[0] !== 0; } return rulePair[1] !== 'off' && rulePair[1] !== 0; }); if (hasTsRules) { let isAppliedToJsFiles = false; if (config.files === undefined) { isAppliedToJsFiles = true; } else { // Config files are glob patterns, so path normalization would corrupt them on Windows. const filePatterns = arrify(config.files).flat(); // Extract the glob portion from each pattern. const globPatterns = filePatterns.map(pattern => micromatch.scan(pattern, { dot: true }).glob).filter(Boolean); // Check whether the file globs match a test file with a JavaScript extension // If not, check literal file paths. The format callback removes the leading `./` that micromatch preserves on literal paths. isAppliedToJsFiles = micromatch.some(jsExtensions.map(ext => `test.${ext}`), globPatterns, { dot: true }) || micromatch.some(filePatterns, jsFilesGlob, { dot: true, format: filePattern => filePattern.startsWith('./') ? filePattern.slice(2) : filePattern, }); } if (isAppliedToJsFiles) { const updatedLanguageOptions = languageOptions ? { ...languageOptions, parser: typescriptParser } : { parser: typescriptParser }; config.languageOptions = updatedLanguageOptions; tsFilesGlob.push(...arrify(config.files ?? codeFilesGlob).flat()); tsFilesIgnoresGlob.push(...arrify(config.ignores)); } } } // If the config sets `parserOptions.project`, `projectService`, `tsconfigRootDir`, or `programs`, treat those files as opt-out for XO's automatic program wiring. if (parserOptions?.project !== undefined || parserOptions?.projectService !== undefined || parserOptions?.tsconfigRootDir !== undefined || parserOptions?.programs !== undefined) { // The glob itself should NOT be negated tsFilesIgnoresGlob.push(...arrify(config.files ?? codeFilesGlob).flat()); } processedConfig.push(config); } return { config: processedConfig, tsFilesGlob, tsFilesIgnoresGlob, }; }; //# sourceMappingURL=utils.js.map