diff --git a/.commitlintrc.json b/.commitlintrc.json new file mode 100644 index 00000000..d3d7f0cd --- /dev/null +++ b/.commitlintrc.json @@ -0,0 +1 @@ +{ "extends": ["@commitlint/config-conventional"] } diff --git a/.editorconfig b/.editorconfig index 1c6314a3..71b1e4bc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,6 +7,7 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true + [*.yml] indent_style = space indent_size = 2 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index dd9469f9..9cbbbceb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,23 +1,29 @@ -name: CI +# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs + +name: Node.js CI + on: - push - pull_request + jobs: test: - name: Node.js ${{ matrix.node-version }} runs-on: ubuntu-latest strategy: - fail-fast: false matrix: node-version: - 20 - 18 + - 22 + - 23 steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - name: Setup Node + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - - run: npm install --force + - run: npm install - run: npm test # - uses: codecov/codecov-action@v1 # if: matrix.node-version == 14 diff --git a/.gitignore b/.gitignore index ee3e1c15..8055d30f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,15 @@ +.DS_Store +.idea +Thumbs.db +tmp/ +temp/ node_modules -yarn.lock -!test/fixtures/project/node_modules -test/fixtures/project/node_modules/.cache -!test/fixtures/typescript/extends-module/node_modules -test/fixtures/typescript/extends-module/node_modules/.cache -!test/fixtures/typescript/extends-tsconfig-bases/node_modules -test/fixtures/typescript/extends-tsconfig-bases/node_modules/.cache -!test/fixtures/typescript/extends-array/node_modules -test/fixtures/typescript/extends-array/node_modules/.cache -.nyc_output +!test/fixtures/node_modules coverage +*.lcov +.nyc_output +*.log +.env +dist +.tsimp +*. diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 00000000..4974c35b --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx commitlint --edit $1 diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 00000000..d24fdfc6 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx lint-staged diff --git a/.lintstagedrc.cjs b/.lintstagedrc.cjs new file mode 100644 index 00000000..8680b321 --- /dev/null +++ b/.lintstagedrc.cjs @@ -0,0 +1,5 @@ +module.exports = { + '*.md,!test/**/*.md': 'prettier --check', + './package.json': ['npmPkgJsonLint ./package.json', 'prettier --check --plugin=prettier-plugin-packagejson ./package.json'], + '*.{js,ts}': 'node . --fix', +}; diff --git a/.npmpackagejsonlintrc b/.npmpackagejsonlintrc new file mode 100644 index 00000000..2d67897e --- /dev/null +++ b/.npmpackagejsonlintrc @@ -0,0 +1 @@ +{ "extends": "npm-package-json-lint-config-default" } diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..7e3df45e --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +./*.ts +./*.js +!./prettier-test.ts diff --git a/cli.js b/cli.js deleted file mode 100755 index dca59e12..00000000 --- a/cli.js +++ /dev/null @@ -1,224 +0,0 @@ -#!/usr/bin/env node -/* eslint-disable unicorn/prefer-top-level-await -- TODO: Use top-level await */ -import process from 'node:process'; -import getStdin from 'get-stdin'; -import meow from 'meow'; -import formatterPretty from 'eslint-formatter-pretty'; -import semver from 'semver'; -import openReport from './lib/open-report.js'; -import xo from './index.js'; - -const cli = meow(` - Usage - $ xo [ ...] - - Options - --fix Automagically fix issues - --reporter Reporter to use - --env Environment preset [Can be set multiple times] - --global Global variable [Can be set multiple times] - --ignore Additional paths to ignore [Can be set multiple times] - --space Use space indent instead of tabs [Default: 2] - --no-semicolon Prevent use of semicolons - --prettier Conform to Prettier code style - --node-version Range of Node.js version to support - --plugin Include third-party plugins [Can be set multiple times] - --extend Extend defaults with a custom config [Can be set multiple times] - --open Open files with issues in your editor - --quiet Show only errors and no warnings - --extension Additional extension to lint [Can be set multiple times] - --cwd= Working directory for files - --stdin Validate/fix code from stdin - --stdin-filename Specify a filename for the --stdin option - --print-config Print the effective ESLint config for the given file - - Examples - $ xo - $ xo index.js - $ xo *.js !foo.js - $ xo --space - $ xo --env=node --env=mocha - $ xo --plugin=react - $ xo --plugin=html --extension=html - $ echo 'const x=true' | xo --stdin --fix - $ xo --print-config=index.js - - Tips - - Add XO to your project with \`npm init xo\`. - - Put options in package.json instead of using flags so other tools can read it. -`, { - importMeta: import.meta, - autoVersion: false, - booleanDefault: undefined, - flags: { - fix: { - type: 'boolean', - }, - reporter: { - type: 'string', - }, - env: { - type: 'string', - isMultiple: true, - }, - global: { - type: 'string', - isMultiple: true, - }, - ignore: { - type: 'string', - isMultiple: true, - }, - space: { - type: 'string', - }, - semicolon: { - type: 'boolean', - }, - prettier: { - type: 'boolean', - }, - nodeVersion: { - type: 'string', - }, - plugin: { - type: 'string', - isMultiple: true, - }, - extend: { - type: 'string', - isMultiple: true, - }, - open: { - type: 'boolean', - }, - quiet: { - type: 'boolean', - }, - extension: { - type: 'string', - isMultiple: true, - }, - cwd: { - type: 'string', - }, - printConfig: { - type: 'string', - }, - stdin: { - type: 'boolean', - }, - stdinFilename: { - type: 'string', - }, - }, -}); - -const {input, flags: options, showVersion} = cli; - -// TODO: Fix this properly instead of the below workaround. -// Revert behavior of meow >8 to pre-8 (7.1.1) for flags using `isMultiple: true`. -// Otherwise, options defined in package.json can't be merged by lib/options-manager.js `mergeOptions()`. -for (const key in options) { - if (Array.isArray(options[key]) && options[key].length === 0) { - delete options[key]; - } -} - -// Make data types for `options.space` match those of the API -// Check for string type because `xo --no-space` sets `options.space` to `false` -if (typeof options.space === 'string') { - if (/^\d+$/u.test(options.space)) { - options.space = Number.parseInt(options.space, 10); - } else if (options.space === 'true') { - options.space = true; - } else if (options.space === 'false') { - options.space = false; - } else { - if (options.space !== '') { - // Assume `options.space` was set to a filename when run as `xo --space file.js` - input.push(options.space); - } - - options.space = true; - } -} - -if (process.env.GITHUB_ACTIONS && !options.fix && !options.reporter) { - options.quiet = true; -} - -const log = async report => { - const reporter = options.reporter || process.env.GITHUB_ACTIONS ? await xo.getFormatter(options.reporter || 'compact') : formatterPretty; - process.stdout.write(reporter(report.results, {rulesMeta: report.rulesMeta})); - process.exitCode = report.errorCount === 0 ? 0 : 1; -}; - -// `xo -` => `xo --stdin` -if (input[0] === '-') { - options.stdin = true; - input.shift(); -} - -if (options.version) { - showVersion(); -} - -if (options.nodeVersion) { - if (options.nodeVersion === 'false') { - options.nodeVersion = false; - } else if (!semver.validRange(options.nodeVersion)) { - console.error('The `--node-engine` flag must be a valid semver range (for example `>=6`)'); - process.exit(1); - } -} - -(async () => { - if (typeof options.printConfig === 'string') { - if (input.length > 0 || options.printConfig === '') { - console.error('The `--print-config` flag must be used with exactly one filename'); - process.exit(1); - } - - if (options.stdin) { - console.error('The `--print-config` flag is not supported on stdin'); - process.exit(1); - } - - options.filePath = options.printConfig; - const config = await xo.getConfig(options); - console.log(JSON.stringify(config, undefined, '\t')); - } else if (options.stdin) { - const stdin = await getStdin(); - - if (options.stdinFilename) { - options.filePath = options.stdinFilename; - } - - if (options.fix) { - const {results: [result]} = await xo.lintText(stdin, options); - // If there is no output, pass the stdin back out - process.stdout.write((result && result.output) || stdin); - return; - } - - if (options.open) { - console.error('The `--open` flag is not supported on stdin'); - process.exit(1); - } - - await log(await xo.lintText(stdin, options)); - } else { - const report = await xo.lintFiles(input, options); - - if (options.fix) { - await xo.outputFixes(report); - } - - if (options.open) { - openReport(report); - } - - await log(report); - } -})(); diff --git a/config/overrides.cjs b/config/overrides.cjs deleted file mode 100644 index f181757c..00000000 --- a/config/overrides.cjs +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports = { - // Put rule overrides here -}; diff --git a/config/plugins.cjs b/config/plugins.cjs deleted file mode 100644 index 87933d55..00000000 --- a/config/plugins.cjs +++ /dev/null @@ -1,404 +0,0 @@ -'use strict'; - -module.exports = { - // Repeated here from eslint-config-xo in case some plugins set something different - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - ecmaFeatures: { - jsx: true, - }, - }, - // -- End repeat - plugins: [ - 'no-use-extend-native', - 'ava', - 'unicorn', - 'promise', - 'import', - 'n', // eslint-plugin-node's successor - 'eslint-comments', - ], - extends: [ - 'plugin:ava/recommended', - 'plugin:unicorn/recommended', - ], - settings: { - 'import/core-modules': [ - 'electron', - 'atom', - ], - }, - rules: { - 'no-use-extend-native/no-use-extend-native': 'error', - - // TODO: Remove this override at some point. - // It's just here to ease users into readable variable names. - 'unicorn/prevent-abbreviations': [ - 'error', - { - checkFilenames: false, - checkDefaultAndNamespaceImports: false, - checkShorthandImports: false, - extendDefaultReplacements: true, - replacements: { - // https://thenextweb.com/dd/2020/07/13/linux-kernel-will-no-longer-use-terms-blacklist-and-slave/ - whitelist: { - include: true, - }, - blacklist: { - exclude: true, - }, - master: { - main: true, - }, - slave: { - secondary: true, - }, - - // Reverse. - application: { - app: true, - }, - applications: { - apps: true, - }, - - // Disable some that may be too annoying. - env: false, - i: false, // Do it at some point, but not ready for it yet. Maybe 2025. - - // Not part of `eslint-plugin-unicorn`. Upstream them at some point. - bin: { - binary: true, - }, - eof: { - endOfFile: true, - }, - impl: { - implement: true, - implementation: true, - }, - anim: { - animation: true, - }, - calc: { - calculate: true, - }, - dict: { - dictionary: true, - }, - dup: { - duplicate: true, - }, - enc: { - encode: true, - encryption: true, - }, - gen: { - generate: true, - general: true, - }, - gfx: { - graphics: true, - }, - inc: { - increment: true, - }, - iter: { - iterate: true, - iterator: true, - }, - nav: { - navigate: true, - navigation: true, - }, - norm: { - normalize: true, - }, - notif: { - notification: true, - }, - perf: { - performance: true, - }, - proc: { - process: true, - }, - rand: { - random: true, - }, - sys: { - system: true, - }, - temp: { - temporary: true, - }, - }, - }, - ], - - // TODO: Restore when it becomes safer: https://github.com/sindresorhus/eslint-plugin-unicorn/issues/681 - // 'unicorn/string-content': [ - // 'error', - // { - // patterns: { - // '': '’', - // [/\.\.\./.source]: '…', - // '->': '→', - // [/^http:\/\//.source]: 'http://' - // } - // } - // ], - - // The character class sorting is a bit buggy at the moment. - 'unicorn/better-regex': [ - 'error', - { - sortCharacterClasses: false, - }, - ], - - // Temporarily disabled because of https://github.com/sindresorhus/eslint-plugin-unicorn/issues/2218 - 'unicorn/no-empty-file': 'off', - - // TODO: Disabled for now as I don't have time to deal with the backslash that might come from this. Try to enable this rule in 2025. - 'unicorn/no-null': 'off', - - // We only enforce it for single-line statements to not be too opinionated. - 'unicorn/prefer-ternary': [ - 'error', - 'only-single-line', - ], - - // It will be disabled in the next version of eslint-plugin-unicorn. - 'unicorn/prefer-json-parse-buffer': 'off', - - // TODO: Remove this override when the rule is more stable. - 'unicorn/consistent-function-scoping': 'off', - - // TODO: Temporarily disabled until it becomes more mature. - 'unicorn/no-useless-undefined': 'off', - - // TODO: Enable it when I have tried it more in practice. - 'unicorn/prefer-string-raw': 'off', - - // TODO: Temporarily disabled as the rule is buggy. - 'function-call-argument-newline': 'off', - - 'promise/param-names': 'error', - 'promise/no-return-wrap': [ - 'error', - { - allowReject: true, - }, - ], - 'promise/no-new-statics': 'error', - 'promise/no-return-in-finally': 'error', - 'promise/valid-params': 'error', - 'promise/prefer-await-to-then': 'error', - - 'import/default': 'error', - 'import/export': 'error', - 'import/extensions': [ - 'error', - 'always', - { - ignorePackages: true, - }, - ], - 'import/first': 'error', - - // Enabled, but disabled on TypeScript (https://github.com/xojs/xo/issues/576) - 'import/named': 'error', - - 'import/namespace': [ - 'error', - { - allowComputed: true, - }, - ], - 'import/no-absolute-path': 'error', - 'import/no-anonymous-default-export': 'error', - 'import/no-named-default': 'error', - 'import/no-webpack-loader-syntax': 'error', - 'import/no-self-import': 'error', - 'import/no-cycle': [ - 'error', - { - ignoreExternal: true, - }, - ], - 'import/no-useless-path-segments': 'error', - 'import/newline-after-import': [ - 'error', - { - // TODO: Buggy. - // considerComments: true, - }, - ], - 'import/no-amd': 'error', - 'import/no-duplicates': [ - 'error', - { - 'prefer-inline': true, - }, - ], - - // We use `unicorn/prefer-module` instead. - // 'import/no-commonjs': 'error', - - // Looks useful, but too unstable at the moment - // 'import/no-deprecated': 'error', - - 'import/no-empty-named-blocks': 'error', - 'import/no-extraneous-dependencies': [ - 'error', - { - includeTypes: true, - }, - ], - 'import/no-mutable-exports': 'error', - 'import/no-named-as-default-member': 'error', - 'import/no-named-as-default': 'error', - - // Disabled because it's buggy and it also doesn't work with TypeScript - // 'import/no-unresolved': [ - // 'error', - // { - // commonjs: false - // } - // ], - - 'import/order': [ - 'error', - { - groups: [ - 'builtin', - 'external', - 'parent', - 'sibling', - 'index', - ], - 'newlines-between': 'never', - warnOnUnassignedImports: true, - }, - ], - 'import/no-unassigned-import': [ - 'error', - { - allow: [ - '@babel/polyfill', - '**/register', - '**/register.*', - '**/register/**', - '**/register/**.*', - '**/*.css', - '**/*.scss', - '**/*.sass', - '**/*.less', - ], - }, - ], - - // Redundant with `import/no-extraneous-dependencies`. - // 'n/no-extraneous-import': 'error', - // 'n/no-extraneous-require': 'error', - - // Redundant with `import/no-unresolved`. - // 'n/no-missing-import': 'error', // This rule is also buggy and doesn't support `node:`. - // 'n/no-missing-require': 'error', - - 'n/no-unpublished-bin': 'error', - - // We have this enabled in addition to `import/extensions` as this one has an auto-fix. - 'n/file-extension-in-import': [ - 'error', - 'always', - ], - 'n/no-mixed-requires': [ - 'error', - { - grouping: true, - allowCall: true, - }, - ], - 'n/no-new-require': 'error', - 'n/no-path-concat': 'error', - - // Disabled because they're too annoying, see: - // https://github.com/mysticatea/eslint-plugin-node/issues/105 - // 'n/no-unpublished-import': [ - // 'error', - // { - // allowModules: [ - // 'electron', - // 'atom' - // ] - // } - // ], - // 'n/no-unpublished-require': [ - // 'error', - // { - // allowModules: [ - // 'electron', - // 'atom' - // ] - // } - // ], - - 'n/process-exit-as-throw': 'error', - - // Disabled as the rule doesn't exclude scripts executed with `node` but not referenced in 'bin'. See https://github.com/mysticatea/eslint-plugin-node/issues/96 - // 'n/hashbang': 'error', - - 'n/no-deprecated-api': 'error', - - // We no longer enforce this as we don't want to use Buffer at all, but sometimes we need to conditionally use the `Buffer` global, but we then don't want the import so the module works cross-platform. - // 'n/prefer-global/buffer': [ - // 'error', - // 'never', - // ], - - 'n/prefer-global/console': [ - 'error', - 'always', - ], - 'n/prefer-global/process': [ - 'error', - 'never', - ], - 'n/prefer-global/text-decoder': [ - 'error', - 'always', - ], - 'n/prefer-global/text-encoder': [ - 'error', - 'always', - ], - 'n/prefer-global/url-search-params': [ - 'error', - 'always', - ], - 'n/prefer-global/url': [ - 'error', - 'always', - ], - 'n/prefer-promises/dns': 'error', - 'n/prefer-promises/fs': 'error', - 'eslint-comments/disable-enable-pair': [ - 'error', - { - allowWholeFile: true, - }, - ], - 'eslint-comments/no-aggregating-enable': 'error', - 'eslint-comments/no-duplicate-disable': 'error', - - // Disabled as it's already covered by the `unicorn/no-abusive-eslint-disable` rule. - // 'eslint-comments/no-unlimited-disable': 'error', - - 'eslint-comments/no-unused-disable': 'error', - 'eslint-comments/no-unused-enable': 'error', - }, -}; diff --git a/index.js b/index.js deleted file mode 100644 index 3ebb91f7..00000000 --- a/index.js +++ /dev/null @@ -1,129 +0,0 @@ -import path from 'node:path'; -import {ESLint} from 'eslint'; -import {globby, isGitIgnoredSync} from 'globby'; -import {isEqual} from 'lodash-es'; -import micromatch from 'micromatch'; -import arrify from 'arrify'; -import slash from 'slash'; -import { - parseOptions, - getIgnores, - mergeWithFileConfig, - getOptionGroups, -} from './lib/options-manager.js'; -import {mergeReports, processReport, getIgnoredReport} from './lib/report.js'; - -const globFiles = async (patterns, options) => { - const { - options: { - ignores, - extensions, - cwd, - }, - } = await mergeWithFileConfig(options); - - patterns = patterns.length === 0 - ? [`**/*.{${extensions.join(',')}}`] - : arrify(patterns).map(pattern => slash(pattern)); - - const files = await globby( - patterns, - { - ignore: ignores, gitignore: true, absolute: true, cwd, - }, - ); - - return files.filter(file => extensions.includes(path.extname(file).slice(1))); -}; - -const getConfig = async options => { - const {filePath, eslintOptions} = await parseOptions(options); - const engine = new ESLint(eslintOptions); - return engine.calculateConfigForFile(filePath); -}; - -const lintText = async (string, options) => { - const [[options_]] = Object.values(await getOptionGroups([options && options.filePath], options)); - const {filePath, warnIgnored, eslintOptions, isQuiet} = options_; - const {cwd, baseConfig: {ignorePatterns}} = eslintOptions; - - if (typeof filePath !== 'string' && !isEqual(getIgnores({}), ignorePatterns)) { - throw new Error('The `ignores` option requires the `filePath` option to be defined.'); - } - - if ( - filePath - && ( - micromatch.isMatch(path.relative(cwd, filePath), ignorePatterns) - || isGitIgnoredSync({cwd})(filePath) - ) - ) { - return getIgnoredReport(filePath); - } - - const eslint = new ESLint(eslintOptions); - - if (filePath && await eslint.isPathIgnored(filePath)) { - return getIgnoredReport(filePath); - } - - const report = await eslint.lintText(string, {filePath, warnIgnored}); - - const rulesMeta = eslint.getRulesMetaForResults(report); - - return processReport(report, {isQuiet, rulesMeta}); -}; - -const lintFiles = async (patterns, options) => { - const files = await globFiles(patterns, options); - - const groups = await getOptionGroups(files, options); - - const reports = await Promise.all( - Object.values(groups) - .map(async filesWithOptions => { - const options = filesWithOptions[0]; - const eslint = new ESLint(options.eslintOptions); - const files = []; - - for (const options of filesWithOptions) { - const {filePath, eslintOptions} = options; - const {cwd, baseConfig: {ignorePatterns}} = eslintOptions; - if ( - micromatch.isMatch(path.relative(cwd, filePath), ignorePatterns) - // eslint-disable-next-line no-await-in-loop -- Not worth refactoring - || await eslint.isPathIgnored(filePath) - ) { - continue; - } - - files.push(filePath); - } - - const report = await eslint.lintFiles(files); - - const rulesMeta = eslint.getRulesMetaForResults(report); - - return processReport(report, {isQuiet: options.isQuiet, rulesMeta}); - })); - - const report = mergeReports(reports); - - return report; -}; - -const getFormatter = async name => { - const {format} = await new ESLint().loadFormatter(name); - return format; -}; - -const xo = { - getFormatter, - getErrorResults: ESLint.getErrorResults, - outputFixes: async ({results}) => ESLint.outputFixes(results), - getConfig, - lintText, - lintFiles, -}; - -export default xo; diff --git a/index.ts b/index.ts new file mode 100644 index 00000000..cd81f393 --- /dev/null +++ b/index.ts @@ -0,0 +1,3 @@ +export * from './lib/xo.js'; + +export {default} from './lib/xo.js'; diff --git a/lib/cli.ts b/lib/cli.ts new file mode 100644 index 00000000..68e7df45 --- /dev/null +++ b/lib/cli.ts @@ -0,0 +1,180 @@ +#!/usr/bin/env node +// 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, 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'; + +const debug = _debug('xo:cli'); + +const cli = meow( + ` + Usage + $ xo [ ...] + + Options + --fix Automagically fix issues + --space Use space indent instead of tabs [Default: 2] + --semicolon Use semicolons [Default: true] + --prettier Conform to Prettier code style [Default: false] + --ts Auto configure type aware linting on unincluded ts files [Default: true] + --print-config Print the effective ESLint config for the given file + --ignore Ignore pattern globs, can be set multiple times + --cwd= 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, + }, + reporter: { + type: 'string', + }, + space: { + type: 'string', + }, + config: { + type: 'string', + }, + quiet: { + type: 'boolean', + }, + semicolon: { + type: 'boolean', + }, + prettier: { + type: 'boolean', + }, + ts: { + type: 'boolean', + default: true, + }, + cwd: { + type: 'string', + default: process.cwd(), + }, + printConfig: { + type: 'string', + }, + version: { + type: 'boolean', + }, + ignore: { + type: 'string', + isMultiple: true, + aliases: ['ignores'], + }, + }, + }, +); + +export type CliOptions = typeof cli; + +const {input, flags: cliOptions, showVersion} = cli; + +const baseXoConfigOptions: XoConfigOptions = { + space: cliOptions.space, + semicolon: cliOptions.semicolon, + prettier: cliOptions.prettier, + ignores: cliOptions.ignore, +}; + +const linterOptions: LinterOptions = { + fix: cliOptions.fix, + cwd: (cliOptions.cwd && path.resolve(cliOptions.cwd)) ?? process.cwd(), + quiet: cliOptions.quiet, + ts: cliOptions.ts, +}; + +// Make data types for `options.space` match those of the API +if (typeof cliOptions.space === 'string') { + cliOptions.space = cliOptions.space.trim(); + + if (/^\d+$/u.test(cliOptions.space)) { + baseXoConfigOptions.space = Number.parseInt(cliOptions.space, 10); + } 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; + } +} + +if ( + process.env['GITHUB_ACTIONS'] + && !linterOptions.fix + && !cliOptions.reporter +) { + linterOptions.quiet = true; +} + +const log = async (report: { + errorCount: number; + warningCount: number; + fixableErrorCount: number; + fixableWarningCount: number; + results: ESLint.LintResult[]; + rulesMeta: Record; +}) => { + const reporter + = cliOptions.reporter + ? await new XO(linterOptions, baseXoConfigOptions).getFormatter(cliOptions.reporter) + : {format: formatterPretty}; + + // @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; +}; + +if (cliOptions.version) { + showVersion(); +} + +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 config = await new XO(linterOptions, baseXoConfigOptions).calculateConfigForFile(cliOptions.printConfig); + console.log(JSON.stringify(config, undefined, '\t')); +} else { + debug('linterOptions %O', linterOptions); + const xo = new XO(linterOptions, baseXoConfigOptions); + + const report = await xo.lintFiles(input); + debug('xo.lintFiles success'); + + if (cliOptions.fix) { + await XO.outputFixes(report); + debug('xo.outputFixes success'); + } + + // @ts-expect-error dues to the types in the formatter + await log(report); +} diff --git a/lib/constants.js b/lib/constants.js deleted file mode 100644 index 889ec02e..00000000 --- a/lib/constants.js +++ /dev/null @@ -1,148 +0,0 @@ -const DEFAULT_IGNORES = [ - '**/node_modules/**', - '**/bower_components/**', - 'flow-typed/**', - 'coverage/**', - '{tmp,temp}/**', - '**/*.min.js', - 'vendor/**', - 'dist/**', - 'distribution/**', - 'tap-snapshots/*.{cjs,js}', -]; - -/** -List of options that values will be concatenanted during option merge. -Only applies to options defined as an Array. -*/ -const MERGE_OPTIONS_CONCAT = [ - 'extends', - 'envs', - 'globals', - 'plugins', -]; - -const TYPESCRIPT_EXTENSION = [ - 'ts', - 'tsx', - 'mts', - 'cts', -]; - -const DEFAULT_EXTENSION = [ - 'js', - 'jsx', - 'mjs', - 'cjs', - ...TYPESCRIPT_EXTENSION, -]; - -/** -Define the rules config that are overwritten only for specific version of Node.js based on `engines.node` in package.json or the `nodeVersion` option. - -The keys are rule names and the values are an Object with a valid semver (`4.0.0` is valid `4` is not) as keys and the rule configuration as values. - -Each entry define the rule config and the maximum Node.js version for which to set it. -The entry with the lowest version that is compliant with the `engines.node`/`nodeVersion` range will be used. - -@type {Object} - -@example -``` -{ - 'plugin/rule': { - '6.0.0': ['error', {prop: 'node-6-conf'}], - '8.0.0': ['error', {prop: 'node-8-conf'}] - } -} -``` - -With `engines.node` set to `>=4` the rule `plugin/rule` will not be used. -With `engines.node` set to `>=6` the rule `plugin/rule` will be used with the config `{prop: 'node-6-conf'}`. -With `engines.node` set to `>=8` the rule `plugin/rule` will be used with the config `{prop: 'node-8-conf'}`. -*/ -const ENGINE_RULES = { - 'unicorn/prefer-spread': { - '5.0.0': 'off', - }, - 'unicorn/no-new-buffer': { - '5.10.0': 'off', - }, - 'prefer-rest-params': { - '6.0.0': 'off', - }, - 'prefer-destructuring': { - '6.0.0': 'off', - }, - 'promise/prefer-await-to-then': { - '7.6.0': 'off', - }, - 'prefer-object-spread': { - '8.3.0': 'off', - }, - 'n/prefer-global/url-search-params': { - '10.0.0': 'off', - }, - 'n/prefer-global/url': { - '10.0.0': 'off', - }, - 'no-useless-catch': { - '10.0.0': 'off', - }, - 'prefer-named-capture-group': { - '10.0.0': 'off', - }, - 'n/prefer-global/text-encoder': { - '11.0.0': 'off', - }, - 'n/prefer-global/text-decoder': { - '11.0.0': 'off', - }, - 'unicorn/prefer-flat-map': { - '11.0.0': 'off', - }, - 'n/prefer-promises/dns': { - '11.14.0': 'off', - }, - 'n/prefer-promises/fs': { - '11.14.0': 'off', - }, -}; - -const MODULE_NAME = 'xo'; - -const CONFIG_FILES = [ - 'package.json', - `.${MODULE_NAME}-config`, - `.${MODULE_NAME}-config.json`, - `.${MODULE_NAME}-config.js`, - `.${MODULE_NAME}-config.cjs`, - `${MODULE_NAME}.config.js`, - `${MODULE_NAME}.config.cjs`, -]; - -const TSCONFIG_DEFAULTS = { - compilerOptions: { - target: 'es2018', - newLine: 'lf', - strict: true, - noImplicitReturns: true, - noUnusedLocals: true, - noUnusedParameters: true, - noFallthroughCasesInSwitch: true, - }, -}; - -const CACHE_DIR_NAME = 'xo-linter'; - -export { - DEFAULT_IGNORES, - DEFAULT_EXTENSION, - TYPESCRIPT_EXTENSION, - ENGINE_RULES, - MODULE_NAME, - CONFIG_FILES, - MERGE_OPTIONS_CONCAT, - TSCONFIG_DEFAULTS, - CACHE_DIR_NAME, -}; diff --git a/lib/constants.ts b/lib/constants.ts new file mode 100644 index 00000000..54fe6b5e --- /dev/null +++ b/lib/constants.ts @@ -0,0 +1,56 @@ +import {type TsConfigJsonResolved} from 'get-tsconfig'; + +export const DEFAULT_IGNORES = [ + '**/node_modules/**', + '**/bower_components/**', + 'flow-typed/**', + 'coverage/**', + '{tmp,temp}/**', + '**/*.min.js', + 'vendor/**', + 'dist/**', + 'tap-snapshots/*.{cjs,js}', +]; + +/** +List of options that values will be concatenanted during option merge. +Only applies to options defined as an Array. +*/ +export const MERGE_OPTIONS_CONCAT = ['extends', 'envs', 'globals', 'plugins']; + +export const TS_EXTENSIONS = ['ts', 'tsx', 'cts', 'mts']; + +export const JS_EXTENSIONS = ['js', 'jsx', 'mjs', 'cjs']; + +export const JS_FILES_GLOB = `**/*.{${JS_EXTENSIONS.join(',')}}`; + +export const TS_FILES_GLOB = `**/*.{${TS_EXTENSIONS.join(',')}}`; + +export const ALL_EXTENSIONS = [...JS_EXTENSIONS, ...TS_EXTENSIONS]; + +export const ALL_FILES_GLOB = `**/*.{${ALL_EXTENSIONS.join(',')}}`; + +export const MODULE_NAME = 'xo'; + +export const CONFIG_FILES = [ + 'package.json', + `.${MODULE_NAME}-config`, + `.${MODULE_NAME}-config.json`, + `.${MODULE_NAME}-config.js`, + `.${MODULE_NAME}-config.cjs`, + `${MODULE_NAME}.config.js`, + `${MODULE_NAME}.config.cjs`, +]; + +export const TSCONFIG_DEFAULTS: TsConfigJsonResolved = { + compilerOptions: { + target: 'es2018', + strict: true, + noImplicitReturns: true, + noUnusedLocals: true, + noUnusedParameters: true, + noFallthroughCasesInSwitch: true, + }, +}; + +export const CACHE_DIR_NAME = 'xo-linter'; diff --git a/lib/create-eslint-config/config.ts b/lib/create-eslint-config/config.ts new file mode 100644 index 00000000..3b2e041f --- /dev/null +++ b/lib/create-eslint-config/config.ts @@ -0,0 +1,398 @@ + +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'; + +if (Array.isArray(pluginAva?.configs?.['recommended'])) { + throw new TypeError('Invalid eslint-plugin-ava'); +} + +if (!configXoTypescript[1]) { + throw new Error('Invalid eslint-config-xo-typescript'); +} + +/** + * The base config that xo builds on top of from user options + */ +export const config: Linter.Config[] = [ + { + ignores: DEFAULT_IGNORES, + }, + { + name: 'XO', + 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, + // @ts-expect-error: This is a private plugin + '@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: { + ...pluginAva?.configs?.['recommended']?.rules, + ...pluginUnicorn.configs?.recommended?.rules, + 'no-use-extend-native/no-use-extend-native': 'error', + // TODO: Remove this override at some point. + // It's just here to ease users into readable variable names. + 'unicorn/prevent-abbreviations': [ + 'error', + { + checkFilenames: false, + checkDefaultAndNamespaceImports: false, + checkShorthandImports: false, + extendDefaultReplacements: false, + replacements: { + // https://thenextweb.com/dd/2020/07/13/linux-kernel-will-no-longer-use-terms-blacklist-and-slave/ + whitelist: { + include: true, + }, + blacklist: { + exclude: true, + }, + master: { + main: true, + }, + slave: { + secondary: true, + }, + + // Not part of `eslint-plugin-unicorn` + application: { + app: true, + }, + applications: { + apps: true, + }, + + // Part of `eslint-plugin-unicorn` + arr: { + array: true, + }, + e: { + error: true, + event: true, + }, + el: { + element: true, + }, + elem: { + element: true, + }, + len: { + length: true, + }, + msg: { + message: true, + }, + num: { + number: true, + }, + obj: { + object: true, + }, + opts: { + options: true, + }, + param: { + parameter: true, + }, + params: { + parameters: true, + }, + prev: { + previous: true, + }, + req: { + request: true, + }, + res: { + response: true, + result: true, + }, + ret: { + returnValue: true, + }, + str: { + string: true, + }, + temp: { + temporary: true, + }, + tmp: { + temporary: true, + }, + val: { + value: true, + }, + err: { + error: true, + }, + }, + }, + ], + // TODO: Restore when it becomes safer: https://github.com/sindresorhus/eslint-plugin-unicorn/issues/681 + // 'unicorn/string-content': [ + // 'error', + // { + // patterns: { + // '': '’', + // [/\.\.\./.source]: '…', + // '->': '→', + // [/^http:\/\//.source]: 'http://' + // } + // } + // ], + // The character class sorting is a bit buggy at the moment. + 'unicorn/better-regex': [ + 'error', + { + sortCharacterClasses: false, + }, + ], + // TODO: Disabled for now until it becomes more stable: https://github.com/sindresorhus/eslint-plugin-unicorn/search?q=consistent-destructuring+is:issue&state=open&type=issues + 'unicorn/consistent-destructuring': 'off', + // TODO: Disabled for now as I don't have time to deal with the backslash that might come from this. Try to enable this rule in 2021. + 'unicorn/no-null': 'off', + // We only enforce it for single-line statements to not be too opinionated. + 'unicorn/prefer-ternary': ['error', 'only-single-line'], + // It will be disabled in the next version of eslint-plugin-unicorn. + 'unicorn/prefer-json-parse-buffer': 'off', + // TODO: Remove this override when the rule is more stable. + 'unicorn/consistent-function-scoping': 'off', + // TODO: Temporarily disabled until it becomes more mature. + 'unicorn/no-useless-undefined': 'off', + // TODO: Temporarily disabled as the rule is buggy. + 'function-call-argument-newline': 'off', + + // Disabled as the plugin doesn't support ESLint 8 yet. + 'promise/param-names': 'error', + 'promise/no-return-wrap': [ + 'error', + { + allowReject: true, + }, + ], + 'promise/no-new-statics': 'error', + 'promise/no-return-in-finally': 'error', + 'promise/valid-params': 'error', + 'promise/prefer-await-to-then': 'error', + 'import-x/default': 'error', + 'import-x/export': 'error', + 'import-x/extensions': [ + 'error', + 'always', + { + ignorePackages: true, + }, + ], + 'import-x/first': 'error', + + // Enabled, but disabled on TypeScript (https://github.com/xojs/xo/issues/576) + 'import-x/named': 'error', + 'import-x/namespace': [ + 'error', + { + allowComputed: true, + }, + ], + 'import-x/no-absolute-path': 'error', + 'import-x/no-anonymous-default-export': 'error', + 'import-x/no-named-default': 'error', + 'import-x/no-webpack-loader-syntax': 'error', + 'import-x/no-self-import': 'error', + 'import-x/no-cycle': [ + 'error', + { + ignoreExternal: true, + }, + ], + 'import-x/no-useless-path-segments': 'error', + 'import-x/newline-after-import': [ + 'error', + { + // TODO: Buggy. + // considerComments: true, + }, + ], + 'import-x/no-amd': 'error', + 'import-x/no-duplicates': [ + 'error', + { + 'prefer-inline': true, + }, + ], + // We use `unicorn/prefer-module` instead. + // 'import-x/no-commonjs': 'error', + // Looks useful, but too unstable at the moment + // 'import-x/no-deprecated': 'error', + 'import-x/no-empty-named-blocks': 'error', + 'import-x/no-extraneous-dependencies': [ + 'error', + { + includeTypes: true, + }, + ], + 'import-x/no-mutable-exports': 'error', + 'import-x/no-named-as-default-member': 'error', + 'import-x/no-named-as-default': 'error', + // Disabled because it's buggy and it also doesn't work with TypeScript + // 'import-x/no-unresolved': [ + // 'error', + // { + // commonjs: false + // } + // ], + 'import-x/order': [ + 'error', + { + groups: ['builtin', 'external', 'parent', 'sibling', 'index'], + 'newlines-between': 'never', + warnOnUnassignedImports: true, + }, + ], + 'import-x/no-unassigned-import': [ + 'error', + { + allow: [ + '@babel/polyfill', + '**/register', + '**/register.*', + '**/register/**', + '**/register/**.*', + '**/*.css', + '**/*.scss', + '**/*.sass', + '**/*.less', + ], + }, + ], + // Redundant with `import-x/no-extraneous-dependencies`. + 'n/no-extraneous-import': 'error', + // 'n/no-extraneous-require': 'error', + // Redundant with `import-x/no-unresolved`. + // 'n/no-missing-import': 'error', // This rule is also buggy and doesn't support `node:`. + // 'n/no-missing-require': 'error', + 'n/no-unpublished-bin': 'error', + // We have this enabled in addition to `import-x/extensions` as this one has an auto-fix. + 'n/file-extension-in-import': [ + 'error', + 'always', + { + // TypeScript doesn't yet support using extensions and fails with error TS2691. + '.ts': 'never', + '.tsx': 'never', + }, + ], + 'n/no-mixed-requires': [ + 'error', + { + grouping: true, + allowCall: true, + }, + ], + 'n/no-new-require': 'error', + 'n/no-path-concat': 'error', + 'n/process-exit-as-throw': 'error', + 'n/no-deprecated-api': 'error', + 'n/prefer-global/buffer': ['error', 'never'], + 'n/prefer-global/console': ['error', 'always'], + 'n/prefer-global/process': ['error', 'never'], + 'n/prefer-global/text-decoder': ['error', 'always'], + 'n/prefer-global/text-encoder': ['error', 'always'], + 'n/prefer-global/url-search-params': ['error', 'always'], + 'n/prefer-global/url': ['error', 'always'], + 'n/prefer-promises/dns': 'error', + 'n/prefer-promises/fs': 'error', + '@eslint-community/eslint-comments/disable-enable-pair': [ + 'error', + { + allowWholeFile: true, + }, + ], + '@eslint-community/eslint-comments/no-aggregating-enable': 'error', + '@eslint-community/eslint-comments/no-duplicate-disable': 'error', + // Disabled as it's already covered by the `unicorn/no-abusive-eslint-disable` rule. + // 'eslint-comments/no-unlimited-disable': 'error', + '@eslint-community/eslint-comments/no-unused-disable': 'error', + '@eslint-community/eslint-comments/no-unused-enable': 'error', + ...configXoTypescript[0]?.rules, + }, + }, + { + name: 'XO TypeScript', + 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: { + ...configXoTypescript[1]?.rules, + 'unicorn/import-style': 'off', + 'n/file-extension-in-import': 'off', + // Disabled because of https://github.com/benmosher/eslint-plugin-import-x/issues/1590 + 'import-x/export': 'off', + // Does not work when the TS definition exports a default const. + 'import-x/default': 'off', + // Disabled as it doesn't work with TypeScript. + // This issue and some others: https://github.com/benmosher/eslint-plugin-import-x/issues/1341 + 'import-x/named': 'off', + }, + }, + ...configXoTypescript.slice(2), + { + files: ['xo.config.{js,ts}'], + rules: { + 'import-x/no-anonymous-default-export': 'off', + }, + }, +]; diff --git a/lib/create-eslint-config/index.ts b/lib/create-eslint-config/index.ts new file mode 100644 index 00000000..287cbf0a --- /dev/null +++ b/lib/create-eslint-config/index.ts @@ -0,0 +1,80 @@ +import process from 'node:process'; +import configXoTypescript from 'eslint-config-xo-typescript'; +import arrify from 'arrify'; +import {type Linter} from 'eslint'; +import {type XoConfigItem} from '../types.js'; +import {config} from './config.js'; +import {xoToEslintConfigItem} from './xo-to-eslint.js'; +import {handlePrettierOptions} from './prettier.js'; + +/** + * Takes a xo flat config and returns an eslint flat config + */ +export async function createConfig( + userConfigs?: XoConfigItem[], + cwd?: string, +): Promise { + const baseConfig = [...config]; + /** + * Since configs are merged and the last config takes precedence + * this means we need to handle both true AND false cases for each option. + * ie... we need to turn prettier,space,semi,etc... on or off for a specific file + */ + for (const xoUserConfig of userConfigs ?? []) { + const keysOfXoConfig = Object.keys(xoUserConfig); + + if (keysOfXoConfig.length === 0) { + continue; + } + + /** Special case global ignores */ + if (keysOfXoConfig.length === 1 && keysOfXoConfig[0] === 'ignores') { + baseConfig.push({ignores: arrify(xoUserConfig.ignores)}); + continue; + } + + /** An eslint config item derived from the xo config item with rules and files initialized */ + const eslintConfigItem = xoToEslintConfigItem(xoUserConfig); + + if (xoUserConfig.semicolon === false) { + eslintConfigItem.rules['@stylistic/semi'] = ['error', 'never']; + eslintConfigItem.rules['@stylistic/semi-spacing'] = [ + 'error', + {before: false, after: true}, + ]; + } + + if (xoUserConfig.space) { + const spaces + = typeof xoUserConfig.space === 'number' ? xoUserConfig.space : 2; + eslintConfigItem.rules['@stylistic/indent'] = [ + 'error', + spaces, + {SwitchCase: 1}, + ]; + } else if (xoUserConfig.space === false) { + // If a user set this false for a small subset of files for some reason, + // then we need to set them back to their original values + eslintConfigItem.rules['@stylistic/indent'] + = configXoTypescript[1]?.rules?.['@stylistic/indent']; + } + + if (xoUserConfig.prettier) { + // eslint-disable-next-line no-await-in-loop + 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'; + } + + baseConfig.push(eslintConfigItem); + } + + return baseConfig; +} + +export default createConfig; diff --git a/lib/create-eslint-config/prettier.ts b/lib/create-eslint-config/prettier.ts new file mode 100644 index 00000000..9e540dc0 --- /dev/null +++ b/lib/create-eslint-config/prettier.ts @@ -0,0 +1,62 @@ +import path from 'node:path'; +// eslint-disable-next-line import-x/no-named-default +import {default as prettier, type Options} from 'prettier'; +import {type Linter, type ESLint} from 'eslint'; +import pluginPrettier from 'eslint-plugin-prettier'; +import configPrettier from 'eslint-config-prettier'; +import {type XoConfigItem} from '../types.js'; + +/** + * Looks up prettier options and adds them to the eslint config, if they conflict with the xo config, throws an error. + * Does not handle prettier overrides but will not fail for them in case they are setup to handle non js/ts files. + * + * Mutates the eslintConfigItem + * + * @param cwd + * @param xoUserConfig + * @param eslintConfigItem + */ +export async function handlePrettierOptions(cwd: string, xoUserConfig: XoConfigItem, eslintConfigItem: Linter.Config): Promise { + const prettierOptions: Options = await prettier.resolveConfig(path.join(cwd, 'xo.config.js'), {editorconfig: true}) ?? {}; + + // validate that prettier options match other xoConfig options + if ((xoUserConfig.semicolon && prettierOptions.semi === false) ?? (!xoUserConfig.semicolon && prettierOptions.semi === true)) { + throw new Error(`The Prettier config \`semi\` is ${prettierOptions.semi} while XO \`semicolon\` is ${xoUserConfig.semicolon}, also check your .editorconfig for inconsistencies.`); + } + + if (((xoUserConfig.space ?? typeof xoUserConfig.space === 'number') && prettierOptions.useTabs === true) || (!xoUserConfig.space && prettierOptions.useTabs === false)) { + throw new Error(`The Prettier config \`useTabs\` is ${prettierOptions.useTabs} while XO \`space\` is ${xoUserConfig.space}, also check your .editorconfig for inconsistencies.`); + } + + if (typeof xoUserConfig.space === 'number' && typeof prettierOptions.tabWidth === 'number' && xoUserConfig.space !== prettierOptions.tabWidth) { + throw new Error(`The Prettier config \`tabWidth\` is ${prettierOptions.tabWidth} while XO \`space\` is ${xoUserConfig.space}, also check your .editorconfig for inconsistencies.`); + } + + // Add prettier plugin + eslintConfigItem.plugins = { + ...eslintConfigItem.plugins, + prettier: pluginPrettier, + }; + + // configure prettier rules + eslintConfigItem.rules = { + ...eslintConfigItem.rules, + ...(pluginPrettier.configs?.['recommended'] as ESLint.ConfigData) + ?.rules, + 'prettier/prettier': [ + 'error', + { + singleQuote: true, + bracketSpacing: false, + bracketSameLine: false, + trailingComma: 'all', + tabWidth: + typeof xoUserConfig.space === 'number' ? xoUserConfig.space : 2, + useTabs: !xoUserConfig.space, + semi: xoUserConfig.semicolon, + ...prettierOptions, + }, + ], + ...configPrettier.rules, + }; +} diff --git a/lib/create-eslint-config/xo-to-eslint.ts b/lib/create-eslint-config/xo-to-eslint.ts new file mode 100644 index 00000000..a11af518 --- /dev/null +++ b/lib/create-eslint-config/xo-to-eslint.ts @@ -0,0 +1,31 @@ + +import arrify from 'arrify'; +import {type SetRequired} from 'type-fest'; +import {type Linter} from 'eslint'; +import { + ALL_FILES_GLOB, +} from '../constants.js'; +import {type XoConfigItem} from '../types.js'; + +/** + * Convert a `xo` config item to an ESLint config item. + * In a flat structure these config items represent the config object items. + * + * Files and rules will always be defined and all other eslint config properties are preserved. + * + * @param xoConfig + * @returns eslintConfig + */ +export const xoToEslintConfigItem = (xoConfig: XoConfigItem): SetRequired => { + const {files, rules, space, prettier, ignores, semicolon, ..._xoConfig} = xoConfig; + + const eslintConfig: SetRequired = { + ..._xoConfig, + files: arrify(xoConfig.files ?? ALL_FILES_GLOB), + rules: xoConfig.rules ?? {}, + }; + + eslintConfig.ignores &&= arrify(xoConfig.ignores); + + return eslintConfig; +}; diff --git a/lib/open-report.js b/lib/open-report.js deleted file mode 100644 index 02870b0e..00000000 --- a/lib/open-report.js +++ /dev/null @@ -1,45 +0,0 @@ -import openEditor from 'open-editor'; - -const sortResults = (a, b) => a.errorCount + b.errorCount > 0 ? (a.errorCount - b.errorCount) : (a.warningCount - b.warningCount); - -const resultToFile = result => { - const [message] = result.messages - .sort((a, b) => { - if (a.severity < b.severity) { - return 1; - } - - if (a.severity > b.severity) { - return -1; - } - - if (a.line < b.line) { - return -1; - } - - if (a.line > b.line) { - return 1; - } - - return 0; - }); - - return { - file: result.filePath, - line: message.line, - column: message.column, - }; -}; - -const getFiles = (report, predicate) => report.results - .filter(result => predicate(result)) - .sort(sortResults) - .map(result => resultToFile(result)); - -const openReport = report => { - const count = report.errorCount > 0 ? 'errorCount' : 'warningCount'; - const files = getFiles(report, result => result[count] > 0); - openEditor(files); -}; - -export default openReport; diff --git a/lib/options-manager.js b/lib/options-manager.js deleted file mode 100644 index 8cd1e032..00000000 --- a/lib/options-manager.js +++ /dev/null @@ -1,677 +0,0 @@ -import {existsSync, promises as fs} from 'node:fs'; -import process from 'node:process'; -import os from 'node:os'; -import path from 'node:path'; -import arrify from 'arrify'; -import {mergeWith, flow, pick} from 'lodash-es'; -import {findUpSync} from 'find-up-simple'; -import findCacheDir from 'find-cache-dir'; -import prettier from 'prettier'; -import semver from 'semver'; -import {cosmiconfig, defaultLoaders} from 'cosmiconfig'; -import micromatch from 'micromatch'; -import stringify from 'json-stable-stringify-without-jsonify'; -import {Legacy} from '@eslint/eslintrc'; -import createEsmUtils from 'esm-utils'; -import MurmurHash3 from 'imurmurhash'; -import slash from 'slash'; -import {getTsconfig} from 'get-tsconfig'; -import { - DEFAULT_IGNORES, - DEFAULT_EXTENSION, - TYPESCRIPT_EXTENSION, - ENGINE_RULES, - MODULE_NAME, - CONFIG_FILES, - MERGE_OPTIONS_CONCAT, - TSCONFIG_DEFAULTS, - CACHE_DIR_NAME, -} from './constants.js'; - -const {__dirname, require} = createEsmUtils(import.meta); -const {normalizePackageName} = Legacy.naming; -const resolveModule = Legacy.ModuleResolver.resolve; - -const resolveFrom = (moduleId, fromDirectory = process.cwd()) => resolveModule(moduleId, path.join(fromDirectory, '__placeholder__.js')); - -resolveFrom.silent = (moduleId, fromDirectory) => { - try { - return resolveFrom(moduleId, fromDirectory); - } catch {} -}; - -const resolveLocalConfig = name => resolveModule(normalizePackageName(name, 'eslint-config'), import.meta.url); - -const cacheLocation = cwd => findCacheDir({name: CACHE_DIR_NAME, cwd}) || path.join(os.homedir() || os.tmpdir(), '.xo-cache/'); - -const DEFAULT_CONFIG = { - useEslintrc: false, - cache: true, - cacheLocation: path.join(cacheLocation(), 'xo-cache.json'), - globInputPaths: false, - resolvePluginsRelativeTo: __dirname, - baseConfig: { - extends: [ - resolveLocalConfig('xo'), - path.join(__dirname, '../config/overrides.cjs'), - path.join(__dirname, '../config/plugins.cjs'), - ], - }, -}; - -/** -Define the shape of deep properties for `mergeWith`. -*/ -const getEmptyConfig = () => ({ - baseConfig: { - rules: {}, - settings: {}, - globals: {}, - ignorePatterns: [], - env: {}, - plugins: [], - extends: [], - }, -}); - -const getEmptyXOConfig = () => ({ - rules: {}, - settings: {}, - globals: [], - envs: [], - plugins: [], - extends: [], -}); - -const mergeFunction = (previousValue, value, key) => { - if (Array.isArray(previousValue)) { - if (MERGE_OPTIONS_CONCAT.includes(key)) { - return [...previousValue, ...value]; - } - - return value; - } -}; - -const isTypescript = file => TYPESCRIPT_EXTENSION.includes(path.extname(file).slice(1)); - -/** -Find config for `lintText`. -The config files are searched starting from `options.filePath` if defined or `options.cwd` otherwise. -*/ -const mergeWithFileConfig = async options => { - options.cwd = path.resolve(options.cwd || process.cwd()); - - const configExplorer = cosmiconfig(MODULE_NAME, { - searchPlaces: CONFIG_FILES, - loaders: {noExt: defaultLoaders['.json']}, - stopDir: options.cwd, - }); - - const packageConfigExplorer = cosmiconfig('engines', {searchPlaces: ['package.json'], stopDir: options.cwd}); - options.filePath &&= path.resolve(options.cwd, options.filePath); - - const searchPath = options.filePath || options.cwd; - const {config: xoOptions, filepath: xoConfigPath} = (await configExplorer.search(searchPath)) || {}; - const {config: enginesOptions} = (await packageConfigExplorer.search(searchPath)) || {}; - - options = normalizeOptions({ - ...(enginesOptions && enginesOptions.node && semver.validRange(enginesOptions.node) ? {nodeVersion: enginesOptions.node} : {}), - ...xoOptions, - ...options, - }); - options.extensions = [...DEFAULT_EXTENSION, ...(options.extensions || [])]; - options.ignores = getIgnores(options); - options.cwd = xoConfigPath && path.dirname(xoConfigPath) !== options.cwd ? path.resolve(options.cwd, path.dirname(xoConfigPath)) : options.cwd; - - // Ensure eslint is ran minimal times across all linted files, once for each unique configuration - // incremental hash of: xo config path + override hash + tsconfig path - // ensures unique configurations - options.eslintConfigId = new MurmurHash3(xoConfigPath); - if (options.filePath) { - const overrides = applyOverrides(options.filePath, options); - options = overrides.options; - - if (overrides.hash) { - options.eslintConfigId = options.eslintConfigId.hash(`${overrides.hash}`); - } - } - - const prettierOptions = options.prettier ? await prettier.resolveConfig(searchPath, {editorconfig: true}) || {} : {}; - - if (options.filePath && isTypescript(options.filePath)) { - options = await handleTSConfig(options); - } - - // Ensure this field ends up as a string - options.eslintConfigId = options.eslintConfigId.result(); - - return {options, prettierOptions}; -}; - -/** - * Find the tsconfig or create a default config - * If a config is found but it doesn't cover the file as needed by parserOptions.project - * we create a temp config for that file that extends the found config. If no config is found - * for a file we apply a default config. - */ -const handleTSConfig = async options => { - // We can skip looking up the tsconfig if we have it defined - // in our parser options already. Otherwise we can look it up and create it as normal - options.ts = true; - options.tsConfig = {}; - options.tsConfigPath = ''; - - const {project: tsConfigProjectPath} = options.parserOptions || {}; - - if (tsConfigProjectPath) { - options.tsConfigPath = path.resolve(options.cwd, tsConfigProjectPath); - options.tsConfig = tsConfigResolvePaths(getTsconfig(options.tsConfigPath).config, options.tsConfigPath); - } else { - const {config: tsConfig, path: filepath} = getTsconfig(options.filePath) || {}; - options.tsConfigPath = filepath; - options.tsConfig = tsConfig; - if (options.tsConfigPath) { - options.tsConfig = tsConfigResolvePaths(tsConfig, options.tsConfigPath); - } else { - delete options.tsConfig; - } - } - - let hasMatch; - - // If there is no files or include property - ts uses **/* as default so all TS files are matched - // in tsconfig, excludes override includes - so we need to prioritize that matching logic - if ( - options.tsConfig - && !options.tsConfig.include - && !options.tsConfig.files - ) { - // If we have an excludes property, we need to check it - // If we match on excluded, then we definitively know that there is no tsconfig match - if (Array.isArray(options.tsConfig.exclude)) { - const exclude = options.tsConfig && Array.isArray(options.tsConfig.exclude) ? options.tsConfig.exclude : []; - hasMatch = !micromatch.contains(options.filePath, exclude); - } else { - // Not explicitly excluded and included by tsconfig defaults - hasMatch = true; - } - } else { - // We have either and include or a files property in tsconfig - const include = options.tsConfig && Array.isArray(options.tsConfig.include) ? options.tsConfig.include : []; - const files = options.tsConfig && Array.isArray(options.tsConfig.files) ? options.tsConfig.files : []; - const exclude = options.tsConfig && Array.isArray(options.tsConfig.exclude) ? options.tsConfig.exclude : []; - // If we also have an exlcude we need to check all the arrays, (files, include, exclude) - // this check not excluded and included in one of the file/include array - hasMatch = !micromatch.contains(options.filePath, exclude) - && micromatch.contains(options.filePath, [...include, ...files]); - } - - if (!hasMatch) { - // Only use our default tsconfig if no other tsconfig is found - otherwise extend the found config for linting - options.tsConfig = options.tsConfigPath ? {extends: options.tsConfigPath} : TSCONFIG_DEFAULTS; - options.tsConfigHash = new MurmurHash3(stringify(options.tsConfig)).result(); - options.tsConfigPath = path.join( - cacheLocation(options.cwd), - `tsconfig.${options.tsConfigHash}.json`, - ); - } - - options.eslintConfigId = options.eslintConfigId.hash(options.tsConfigPath); - - return options; -}; - -const normalizeOptions = options => { - options = {...options}; - - // Aliases for humans - const aliases = [ - 'env', - 'global', - 'ignore', - 'plugin', - 'rule', - 'setting', - 'extend', - 'extension', - ]; - - for (const singular of aliases) { - const plural = singular + 's'; - let value = options[plural] || options[singular]; - - delete options[singular]; - - if (value === undefined) { - continue; - } - - if (singular !== 'rule' && singular !== 'setting') { - value = arrify(value); - } - - options[plural] = value; - } - - return options; -}; - -const normalizeSpaces = options => typeof options.space === 'number' ? options.space : 2; - -/** -Transform an XO options into ESLint compatible options: -- Apply rules based on XO options (e.g `spaces` => `indent` rules or `semicolon` => `semi` rule). -- Resolve the extended configurations. -- Apply rules based on Prettier config if `prettier` option is `true`. -*/ -const buildConfig = (options, prettierOptions) => { - options = normalizeOptions(options); - - if (options.useEslintrc) { - throw new Error('The `useEslintrc` option is not supported'); - } - - return flow( - buildESLintConfig(options), - buildXOConfig(options), - buildTSConfig(options), - buildExtendsConfig(options), - buildPrettierConfig(options, prettierOptions), - )(mergeWith(getEmptyConfig(), DEFAULT_CONFIG, mergeFunction)); -}; - -const toValueMap = (array, value = true) => Object.fromEntries(array.map(item => [item, value])); - -const buildESLintConfig = options => config => { - if (options.rules) { - config.baseConfig.rules = { - ...config.baseConfig.rules, - ...options.rules, - }; - } - - if (options.parser) { - config.baseConfig.parser = options.parser; - } - - if (options.processor) { - config.baseConfig.processor = options.processor; - } - - config.baseConfig.settings = options.settings || {}; - - if (options.envs) { - config.baseConfig.env = { - ...config.baseConfig.env, - ...toValueMap(options.envs), - }; - } - - if (options.globals) { - config.baseConfig.globals = { - ...config.baseConfig.globals, - ...toValueMap(options.globals, 'readonly'), - }; - } - - if (options.plugins) { - config.baseConfig.plugins = [ - ...config.baseConfig.plugins, - ...options.plugins, - ]; - } - - if (options.ignores) { - config.baseConfig.ignorePatterns = [ - ...config.baseConfig.ignorePatterns, - ...options.ignores, - ]; - } - - if (options.parserOptions) { - config.baseConfig.parserOptions = { - ...config.baseConfig.parserOptions, - ...options.parserOptions, - }; - } - - return { - ...config, - ...pick(options, ['cwd', 'filePath', 'fix']), - }; -}; - -const buildXOConfig = options => config => { - const spaces = normalizeSpaces(options); - - for (const [rule, ruleConfig] of Object.entries(ENGINE_RULES)) { - for (const minVersion of Object.keys(ruleConfig).sort(semver.rcompare)) { - if (!options.nodeVersion || semver.intersects(options.nodeVersion, `<${minVersion}`)) { - config.baseConfig.rules[rule] ??= ruleConfig[minVersion]; - } - } - } - - if (options.nodeVersion) { - config.baseConfig.rules['n/no-unsupported-features/es-builtins'] ??= ['error', {version: options.nodeVersion}]; - config.baseConfig.rules['n/no-unsupported-features/es-syntax'] ??= ['error', {version: options.nodeVersion, ignores: ['modules']}]; - config.baseConfig.rules['n/no-unsupported-features/node-builtins'] ??= ['error', {version: options.nodeVersion, allowExperimental: true}]; - } - - if (options.space && !options.prettier) { - if (options.ts) { - config.baseConfig.rules['@typescript-eslint/indent'] ??= ['error', spaces, {SwitchCase: 1}]; - } else { - config.baseConfig.rules.indent ??= ['error', spaces, {SwitchCase: 1}]; - } - - // Only apply if the user has the React plugin - if (options.cwd && resolveFrom.silent('eslint-plugin-react', options.cwd)) { - config.baseConfig.plugins.push('react'); - config.baseConfig.rules['react/jsx-indent-props'] ??= ['error', spaces]; - config.baseConfig.rules['react/jsx-indent'] ??= ['error', spaces]; - } - } - - if (options.semicolon === false && !options.prettier) { - if (options.ts) { - config.baseConfig.rules['@typescript-eslint/semi'] ??= ['error', 'never']; - } else { - config.baseConfig.rules.semi ??= ['error', 'never']; - } - - config.baseConfig.rules['semi-spacing'] ??= ['error', { - before: false, - after: true, - }]; - } - - if (options.ts) { - config.baseConfig.rules['unicorn/import-style'] ??= 'off'; - config.baseConfig.rules['node/file-extension-in-import'] ??= 'off'; - - // Disabled because of https://github.com/benmosher/eslint-plugin-import/issues/1590 - config.baseConfig.rules['import/export'] ??= 'off'; - - // Does not work when the TS definition exports a default const. - config.baseConfig.rules['import/default'] ??= 'off'; - - // Disabled as it doesn't work with TypeScript. - // This issue and some others: https://github.com/benmosher/eslint-plugin-import/issues/1341 - config.baseConfig.rules['import/named'] ??= 'off'; - } - - config.baseConfig.settings['import/resolver'] = gatherImportResolvers(options); - - return config; -}; - -const buildExtendsConfig = options => config => { - if (options.extends && options.extends.length > 0) { - const configs = options.extends.map(name => { - // Don't do anything if it's a filepath - if (existsSync(path.resolve(options.cwd || process.cwd(), name))) { - return name; - } - - // Don't do anything if it's a config from a plugin or an internal eslint config - if (name.startsWith('eslint:') || name.startsWith('plugin:')) { - return name; - } - - const returnValue = resolveFrom(normalizePackageName(name, 'eslint-config'), options.cwd); - - if (!returnValue) { - throw new Error(`Couldn't find ESLint config: ${name}`); - } - - return returnValue; - }); - - config.baseConfig.extends = [...config.baseConfig.extends, ...configs]; - } - - return config; -}; - -const buildPrettierConfig = (options, prettierConfig) => config => { - if (options.prettier) { - // The prettier plugin uses Prettier to format the code with `--fix` - config.baseConfig.plugins.push('prettier'); - - // The prettier plugin overrides ESLint stylistic rules that are handled by Prettier - config.baseConfig.extends.push('plugin:prettier/recommended'); - - // The `prettier/prettier` rule reports errors if the code is not formatted in accordance to Prettier - config.baseConfig.rules['prettier/prettier'] ??= ['error', mergeWithPrettierConfig(options, prettierConfig)]; - } - - return config; -}; - -const mergeWithPrettierConfig = (options, prettierOptions) => { - if ((options.semicolon === true && prettierOptions.semi === false) - || (options.semicolon === false && prettierOptions.semi === true)) { - throw new Error(`The Prettier config \`semi\` is ${prettierOptions.semi} while XO \`semicolon\` is ${options.semicolon}`); - } - - if (((options.space === true || typeof options.space === 'number') && prettierOptions.useTabs === true) - || ((options.space === false) && prettierOptions.useTabs === false)) { - throw new Error(`The Prettier config \`useTabs\` is ${prettierOptions.useTabs} while XO \`space\` is ${options.space}`); - } - - if (typeof options.space === 'number' && typeof prettierOptions.tabWidth === 'number' && options.space !== prettierOptions.tabWidth) { - throw new Error(`The Prettier config \`tabWidth\` is ${prettierOptions.tabWidth} while XO \`space\` is ${options.space}`); - } - - return mergeWith( - {}, - { - singleQuote: true, - bracketSpacing: false, - bracketSameLine: false, - trailingComma: 'all', - tabWidth: normalizeSpaces(options), - useTabs: !options.space, - semi: options.semicolon !== false, - }, - prettierOptions, - mergeFunction, - ); -}; - -const buildTSConfig = options => config => { - if (options.ts) { - config.baseConfig.extends.push(require.resolve('eslint-config-xo-typescript')); - config.baseConfig.parser = require.resolve('@typescript-eslint/parser'); - config.baseConfig.parserOptions = { - ...config.baseConfig.parserOptions, - warnOnUnsupportedTypeScriptVersion: false, - ecmaFeatures: {jsx: true}, - project: options.tsConfigPath, - projectFolderIgnoreList: - options.parserOptions && options.parserOptions.projectFolderIgnoreList - ? options.parserOptions.projectFolderIgnoreList - : [new RegExp(`/node_modules/(?!.*\\.cache/${CACHE_DIR_NAME})`)], - }; - } - - return config; -}; - -const applyOverrides = (file, options) => { - if (options.overrides && options.overrides.length > 0) { - const {overrides} = options; - delete options.overrides; - - const {applicable, hash} = findApplicableOverrides(path.relative(options.cwd, file), overrides); - - options = mergeWith(getEmptyXOConfig(), options, ...applicable.map(override => normalizeOptions(override)), mergeFunction); - delete options.files; - return {options, hash}; - } - - return {options}; -}; - -/** -Builds a list of overrides for a particular path, and a hash value. -The hash value is a binary representation of which elements in the `overrides` array apply to the path. - -If `overrides.length === 4`, and only the first and third elements apply, then our hash is: 1010 (in binary) -*/ -const findApplicableOverrides = (path, overrides) => { - let hash = 0; - const applicable = []; - - for (const override of overrides) { - hash <<= 1; // eslint-disable-line no-bitwise -- Intentional bitwise usage - - if (micromatch.isMatch(path, override.files)) { - applicable.push(override); - hash |= 1; // eslint-disable-line no-bitwise -- Intentional bitwise usage - } - } - - return { - hash, - applicable, - }; -}; - -const getIgnores = ({ignores}) => [...DEFAULT_IGNORES, ...(ignores || [])]; - -const gatherImportResolvers = options => { - let resolvers = {}; - - const resolverSettings = options.settings && options.settings['import/resolver']; - if (resolverSettings) { - if (typeof resolverSettings === 'string') { - resolvers[resolverSettings] = {}; - } else { - resolvers = {...resolverSettings}; - } - } - - let webpackResolverSettings; - - if (options.webpack) { - webpackResolverSettings = options.webpack === true ? {} : options.webpack; - } else if (!(options.webpack === false || resolvers.webpack)) { - // If a webpack config file exists, add the import resolver automatically - const webpackConfigPath = findUpSync('webpack.config.js', {cwd: options.cwd}); - if (webpackConfigPath) { - webpackResolverSettings = {config: webpackConfigPath}; - } - } - - if (webpackResolverSettings) { - resolvers = { - ...resolvers, - webpack: { - ...resolvers.webpack, - ...webpackResolverSettings, - }, - }; - } - - return resolvers; -}; - -const parseOptions = async options => { - options = normalizeOptions(options); - const {options: foundOptions, prettierOptions} = await mergeWithFileConfig(options); - const {eslintConfigId, tsConfigHash, tsConfig, tsConfigPath} = foundOptions; - const {filePath, warnIgnored, ...eslintOptions} = buildConfig(foundOptions, prettierOptions); - return { - filePath, - warnIgnored, - isQuiet: options.quiet, - eslintOptions, - eslintConfigId, - tsConfigHash, - tsConfigPath, - tsConfig, - }; -}; - -const getOptionGroups = async (files, options) => { - const allOptions = await Promise.all( - arrify(files).map(filePath => parseOptions({...options, filePath})), - ); - - const tsGroups = {}; - const optionGroups = {}; - for (const options of allOptions) { - if (Array.isArray(optionGroups[options.eslintConfigId])) { - optionGroups[options.eslintConfigId].push(options); - } else { - optionGroups[options.eslintConfigId] = [options]; - } - - if (options.tsConfigHash) { - if (Array.isArray(tsGroups[options.tsConfigHash])) { - tsGroups[options.tsConfigHash].push(options); - } else { - tsGroups[options.tsConfigHash] = [options]; - } - } - } - - await Promise.all(Object.values(tsGroups).map(async tsGroup => { - await fs.mkdir(path.dirname(tsGroup[0].tsConfigPath), {recursive: true}); - await fs.writeFile(tsGroup[0].tsConfigPath, JSON.stringify({ - ...tsGroup[0].tsConfig, - files: tsGroup.map(o => o.filePath), - include: [], - exclude: [], - })); - })); - - // Files with same `xoConfigPath` can lint together - // https://github.com/xojs/xo/issues/599 - return optionGroups; -}; - -// Convert all include, files, and exclude to absolute paths -// and or globs. This works because ts only allows simple glob subset -const tsConfigResolvePaths = (tsConfig, tsConfigPath) => { - const tsConfigDirectory = path.dirname(tsConfigPath); - - if (Array.isArray(tsConfig.files)) { - tsConfig.files = tsConfig.files.map( - filePath => slash(path.resolve(tsConfigDirectory, filePath)), - ); - } - - if (Array.isArray(tsConfig.include)) { - tsConfig.include = tsConfig.include.map( - globPath => slash(path.resolve(tsConfigDirectory, globPath)), - ); - } - - if (Array.isArray(tsConfig.exclude)) { - tsConfig.exclude = tsConfig.exclude.map( - globPath => slash(path.resolve(tsConfigDirectory, globPath)), - ); - } - - return tsConfig; -}; - -export { - parseOptions, - getIgnores, - mergeWithFileConfig, - - // For tests - applyOverrides, - findApplicableOverrides, - mergeWithPrettierConfig, - normalizeOptions, - buildConfig, - getOptionGroups, - handleTSConfig, - tsConfigResolvePaths, -}; diff --git a/lib/report.js b/lib/report.js deleted file mode 100644 index a6faf102..00000000 --- a/lib/report.js +++ /dev/null @@ -1,86 +0,0 @@ -import defineLazyProperty from 'define-lazy-prop'; -import {ESLint} from 'eslint'; - -/** Merge multiple reports into a single report */ -const mergeReports = reports => { - const report = { - results: [], - errorCount: 0, - warningCount: 0, - }; - - for (const currentReport of reports) { - report.results.push(...currentReport.results); - report.errorCount += currentReport.errorCount; - report.warningCount += currentReport.warningCount; - report.rulesMeta = {...report.rulesMeta, ...currentReport.rulesMeta}; - } - - return report; -}; - -const processReport = (report, {isQuiet = false, rulesMeta} = {}) => { - if (isQuiet) { - report = ESLint.getErrorResults(report); - } - - const result = { - results: report, - rulesMeta, - ...getReportStatistics(report), - }; - - defineLazyProperty(result, 'usedDeprecatedRules', () => { - const seenRules = new Set(); - const rules = []; - - for (const {usedDeprecatedRules} of report) { - for (const rule of usedDeprecatedRules) { - if (seenRules.has(rule.ruleId)) { - continue; - } - - seenRules.add(rule.ruleId); - rules.push(rule); - } - } - - return rules; - }); - - return result; -}; - -const getReportStatistics = results => { - const statistics = { - errorCount: 0, - warningCount: 0, - fixableErrorCount: 0, - fixableWarningCount: 0, - }; - - for (const result of results) { - statistics.errorCount += result.errorCount; - statistics.warningCount += result.warningCount; - statistics.fixableErrorCount += result.fixableErrorCount; - statistics.fixableWarningCount += result.fixableWarningCount; - } - - return statistics; -}; - -const getIgnoredReport = filePath => ({ - errorCount: 0, - warningCount: 0, - results: [ - { - errorCount: 0, - warningCount: 0, - filePath, - messages: [], - }, - ], - isIgnored: true, -}); - -export {mergeReports, processReport, getIgnoredReport}; diff --git a/lib/resolve-config.ts b/lib/resolve-config.ts new file mode 100644 index 00000000..51ef7171 --- /dev/null +++ b/lib/resolve-config.ts @@ -0,0 +1,88 @@ +import path from 'node:path'; +import process from 'node:process'; +import {cosmiconfig, defaultLoaders} from 'cosmiconfig'; +import pick from 'lodash.pick'; +import {type LinterOptions, type FlatXoConfig} from './types.js'; +import {MODULE_NAME} from './constants.js'; + +/** + * Finds the xo config file + */ +export async function resolveXoConfig(options: LinterOptions): Promise<{ + flatOptions: FlatXoConfig; + flatConfigPath: string; + enginesOptions: {engines?: string}; +}> { + options.cwd ||= process.cwd(); + + if (!path.isAbsolute(options.cwd)) { + options.cwd = path.resolve(process.cwd(), options.cwd); + } + + const stopDirectory = path.dirname(options.cwd); + + const packageConfigExplorer = cosmiconfig('engines', { + searchPlaces: ['package.json'], + stopDir: options.cwd, + }); + + const flatConfigExplorer = cosmiconfig(MODULE_NAME, { + searchPlaces: [ + `${MODULE_NAME}.config.js`, + `${MODULE_NAME}.config.cjs`, + `${MODULE_NAME}.config.mjs`, + `${MODULE_NAME}.config.ts`, + `${MODULE_NAME}.config.cts`, + `${MODULE_NAME}.config.mts`, + ], + loaders: { + '.cts': defaultLoaders['.ts'], + '.mts': defaultLoaders['.ts'], + }, + stopDir: stopDirectory, + cache: true, + }); + + options.filePath &&= path.resolve(options.cwd, options.filePath); + + const searchPath = options.filePath ?? options.cwd; + + let [ + {config: flatOptions = [], filepath: flatConfigPath = ''}, + {config: enginesOptions = {}}, + ] = await Promise.all([ + (async () => + (await flatConfigExplorer.search(searchPath)) ?? {})() as Promise<{ + config: FlatXoConfig | undefined; + filepath: string; + }>, + (async () => + (await packageConfigExplorer.search(searchPath)) ?? {})() as Promise<{ + config: {engines: string} | undefined; + }>, + ]); + + const globalKeys = [ + 'ignores', + 'settings', + 'parserOptions', + 'prettier', + 'semicolon', + 'space', + 'rules', + 'env', + 'extension', + 'files', + 'plugins', + ]; + + flatOptions = flatOptions.map(config => pick(config, globalKeys)); + + return { + enginesOptions, + flatOptions, + flatConfigPath, + }; +} + +export default resolveXoConfig; diff --git a/lib/tsconfig.ts b/lib/tsconfig.ts new file mode 100644 index 00000000..42a26787 --- /dev/null +++ b/lib/tsconfig.ts @@ -0,0 +1,72 @@ + +import path from 'node:path'; +import fs from 'node:fs/promises'; +import {getTsconfig} from 'get-tsconfig'; +import micromatch from 'micromatch'; +import {TSCONFIG_DEFAULTS, CACHE_DIR_NAME} from './constants.js'; + +/** + * This function checks if the files are matched by the tsconfig include, exclude, and it returns the unmatched files. + * If no tsconfig is found, it will create a fallback tsconfig file in the node_modules/.cache/xo directory. + * + * @param options + * @returns The unmatched files. + */ +export async function tsconfig({cwd, files}: {cwd: string; files: string[]}) { + const {config: tsConfig = TSCONFIG_DEFAULTS, path: tsConfigPath} = getTsconfig(cwd) ?? {}; + + const unmatchedFiles: string[] = []; + + for (const filePath of files) { + let hasMatch = false; + + if (!tsConfigPath) { + unmatchedFiles.push(filePath); + continue; + } + + // If there is no files or include property - ts uses **/* as default so all TS files are matched + // in tsconfig, excludes override includes - so we need to prioritize that matching logic + if ( + tsConfig + && !tsConfig.include + && !tsConfig.files + ) { + // If we have an excludes property, we need to check it + // If we match on excluded, then we definitively know that there is no tsconfig match + if (Array.isArray(tsConfig.exclude)) { + const exclude = tsConfig && Array.isArray(tsConfig.exclude) ? tsConfig.exclude : []; + hasMatch = !micromatch.contains(filePath, exclude); + } else { + // Not explicitly excluded and included by tsconfig defaults + hasMatch = true; + } + } else { + // We have either and include or a files property in tsconfig + const include = tsConfig && Array.isArray(tsConfig.include) ? tsConfig.include : []; + const files = tsConfig && Array.isArray(tsConfig.files) ? tsConfig.files : []; + const exclude = tsConfig && Array.isArray(tsConfig.exclude) ? tsConfig.exclude : []; + // If we also have an exlcude we need to check all the arrays, (files, include, exclude) + // this check not excluded and included in one of the file/include array + hasMatch = !micromatch.contains(filePath, exclude) && micromatch.contains(filePath, [...include, ...files]); + } + + if (!hasMatch) { + unmatchedFiles.push(filePath); + } + } + + const fallbackTsConfigPath = path.join(cwd, 'node_modules', '.cache', CACHE_DIR_NAME, 'tsconfig.xo.json'); + + await fs.mkdir(path.dirname(fallbackTsConfigPath), {recursive: true}); + await fs.writeFile(fallbackTsConfigPath, JSON.stringify(tsConfig, null, 2)); + + delete tsConfig.include; + delete tsConfig.exclude; + delete tsConfig.files; + tsConfig.files = files; + await fs.mkdir(path.dirname(fallbackTsConfigPath), {recursive: true}); + await fs.writeFile(fallbackTsConfigPath, JSON.stringify(tsConfig, null, 2)); + + return {unmatchedFiles: unmatchedFiles.map(fp => path.relative(cwd, fp)), defaultProject: fallbackTsConfigPath}; +} diff --git a/lib/types.ts b/lib/types.ts new file mode 100644 index 00000000..e79dbbf1 --- /dev/null +++ b/lib/types.ts @@ -0,0 +1,84 @@ +import type {Simplify} from 'type-fest'; +import {type ESLint, type Rule, type Linter} from 'eslint'; + +export type Space = boolean | number | string | undefined; + +export type XoConfigOptions = { + /** + * Use spaces for indentation. + * Tabs are used if the value is `false`, otherwise the value is the number of spaces to use or true, the default number of spaces is 2. + */ + space?: Space; + /** + * Use semicolons at the end of statements or error for semi-colon usage. + */ + semicolon?: boolean; + /** + * Use Prettier to format code. + */ + prettier?: boolean; + /** + * Files to ignore, can be a glob or array of globs. + */ + ignores?: string | string[]; +}; + +export type LinterOptions = { + /** + * The current working directory to use for relative paths. + */ + cwd: string; + /** + * Write fixes to the files. + */ + fix?: boolean; + /** + * The path to the file being linted. + */ + filePath?: string; + /** + * If true,show only errors and NOT warnings. false by default. + */ + quiet?: boolean; + /** + * Auto configure type aware linting on unincluded ts files.Ensures that TypeScript files are linted with the type aware parser even if they are not explicitly included in the tsconfig. + */ + ts?: boolean; +}; + +export type LintTextOptions = { + /** + * The path to the file being linted. + */ + filePath: string; + /** + * Warn if the file is ignored. + */ + warnIgnored?: boolean; +}; + +export type XoConfigItem = Simplify & { + /** + * An array of glob patterns indicating the files that the configuration object should apply to. If not specified, the configuration object applies to all files. + * + * @see [Ignore Patterns](https://eslint.org/docs/latest/user-guide/configuring/configuration-files-new#excluding-files-with-ignores) + */ + files?: string | string[] | undefined; + /** + * An array of glob patterns indicating the files that the configuration object should not apply to. If not specified, the configuration object applies to all files matched by files. + * + * @see [Ignore Patterns](https://eslint.org/docs/latest/user-guide/configuring/configuration-files-new#excluding-files-with-ignores) + */ + ignores?: string | string[] | undefined; +}>; + +export type FlatXoConfig = XoConfigItem[]; + +export type XoLintResult = { + errorCount: number; + warningCount: number; + fixableErrorCount: number; + fixableWarningCount: number; + results: ESLint.LintResult[]; + rulesMeta: Rule.RuleMetaData; +}; diff --git a/lib/xo.ts b/lib/xo.ts new file mode 100644 index 00000000..780f0785 --- /dev/null +++ b/lib/xo.ts @@ -0,0 +1,383 @@ +import path from 'node:path'; +import os from 'node:os'; +import process from 'node:process'; +import {ESLint, type Linter} from 'eslint'; +import findCacheDir from 'find-cache-dir'; +import {globby} from 'globby'; +import arrify from 'arrify'; +import defineLazyProperty from 'define-lazy-prop'; +import _debug from 'debug'; +import { + type XoLintResult, + type LinterOptions, + type LintTextOptions, + type FlatXoConfig, + type XoConfigOptions, + type XoConfigItem, +} from './types.js'; +import {DEFAULT_IGNORES, CACHE_DIR_NAME, ALL_EXTENSIONS} from './constants.js'; +import createConfig from './create-eslint-config/index.js'; +import resolveXoConfig from './resolve-config.js'; +import {tsconfig} from './tsconfig.js'; + +const debug = _debug('xo'); +const initDebug = debug.extend('initEslint'); +export class XO { + /** + * Static lintText helper for backwards compat and use in editor extensions and other tools + */ + static async lintText(code: string, options: LintTextOptions & LinterOptions & XoConfigOptions) { + const xo = new XO( + { + cwd: options.cwd, + fix: options.fix, + filePath: options.filePath, + quiet: options.quiet, + ts: options.ts, + }, + { + space: options.space, + semicolon: options.semicolon, + prettier: options.prettier, + ignores: options.ignores, + }, + ); + return xo.lintText(code, {filePath: options.filePath, warnIgnored: options.warnIgnored}); + } + + /** + * Static lintFiles helper for backwards compat and use in editor extensions and other tools + */ + static async lintFiles(globs: string | undefined, options: LinterOptions & XoConfigOptions) { + const xo = new XO( + { + cwd: options.cwd, + fix: options.fix, + filePath: options.filePath, + quiet: options.quiet, + ts: options.ts, + }, + { + space: options.space, + semicolon: options.semicolon, + prettier: options.prettier, + ignores: options.ignores, + }, + ); + return xo.lintFiles(globs); + } + + /** + * Write the fixes to disk + */ + static async outputFixes(results: XoLintResult) { + await ESLint.outputFixes(results?.results ?? []); + } + + /** + * Required linter options,cwd, fix, and filePath (in case of lintText) + */ + linterOptions: LinterOptions; + /** + * Base XO config options that allow configuration from cli or other sources + * not to be confused with the xoConfig property which is the resolved XO config from the flat config AND base config + */ + baseXoConfig: XoConfigOptions; + /** + * file path to the eslint cache + */ + cacheLocation: string; + /** + * A re-usable ESLint instance configured with options calculated from the XO config + */ + eslint?: ESLint; + /** + * XO config derived from both the base config and the resolved flat config + */ + xoConfig?: FlatXoConfig; + /** + * The ESLint config calculated from the resolved XO config + */ + eslintConfig?: Linter.Config[]; + /** + * The flat xo config path, if there is one + */ + flatConfigPath?: string | undefined; + + constructor(_linterOptions: LinterOptions, _baseXoConfig: XoConfigOptions = {}) { + this.linterOptions = _linterOptions; + this.baseXoConfig = _baseXoConfig; + + // fix relative cwd paths + if (!path.isAbsolute(this.linterOptions.cwd)) { + this.linterOptions.cwd = path.resolve(process.cwd(), this.linterOptions.cwd); + } + + const backupCacheLocation = path.join(os.tmpdir(), CACHE_DIR_NAME); + + this.cacheLocation = findCacheDir({name: CACHE_DIR_NAME, cwd: this.linterOptions.cwd}) ?? backupCacheLocation; + } + + /** + * setXoConfig sets the xo config on the XO instance + * @private + */ + async setXoConfig() { + if (!this.xoConfig) { + const {flatOptions, flatConfigPath} = await resolveXoConfig({ + ...this.linterOptions, + }); + this.xoConfig = [this.baseXoConfig, ...flatOptions]; + this.flatConfigPath = flatConfigPath; + } + } + + /** + * setEslintConfig sets the eslint config on the XO instance + * @private + */ + async setEslintConfig() { + if (!this.xoConfig) { + throw new Error('"XO.setEslintConfig" failed'); + } + + this.eslintConfig ??= await createConfig([...this.xoConfig], this.linterOptions.cwd); + } + + /** + * setIgnores sets the ignores on the XO instance + * @private + */ + setIgnores() { + if (!this.baseXoConfig.ignores) { + let ignores: string[] = []; + + if (typeof this.baseXoConfig.ignores === 'string') { + ignores = arrify(this.baseXoConfig.ignores); + } else if (Array.isArray(this.baseXoConfig.ignores)) { + ignores = this.baseXoConfig.ignores; + } + + if (!this.xoConfig) { + throw new Error('"XO.setIgnores" failed'); + } + + if (ignores.length > 0) { + this.xoConfig.push({ignores}); + } + } + } + + async handleUnincludedTsFiles(files?: string[]) { + if (this.linterOptions.ts && files && files.length > 0) { + const tsFiles = files.filter(file => file.endsWith('.ts') || file.endsWith('.mts') || file.endsWith('.cts')); + + if (tsFiles.length > 0) { + const {defaultProject, unmatchedFiles} = await tsconfig({ + cwd: this.linterOptions.cwd, + files: tsFiles, + }); + + if (this.xoConfig && unmatchedFiles.length > 0) { + const config: XoConfigItem = {}; + config.files = unmatchedFiles; + config.languageOptions ??= {}; + config.languageOptions.parserOptions ??= {}; + config.languageOptions.parserOptions['projectService'] = false; + config.languageOptions.parserOptions['project'] = defaultProject; + this.xoConfig.push(config); + } + } + } + } + + /** + * initEslint initializes the ESLint instance on the XO instance + */ + public async initEslint(files?: string[]) { + await this.setXoConfig(); + initDebug('setXoConfig complete'); + + this.setIgnores(); + initDebug('setIgnores complete'); + + await this.handleUnincludedTsFiles(files); + initDebug('handleUnincludedTsFiles complete'); + + await this.setEslintConfig(); + initDebug('setEslintConfig complete'); + + if (!this.xoConfig) { + throw new Error('"XO.initEslint" failed'); + } + + const eslintOptions = { + cwd: this.linterOptions.cwd, + overrideConfig: this.eslintConfig, + overrideConfigFile: true, + globInputPaths: false, + warnIgnored: false, + cache: true, + cacheLocation: this.cacheLocation, + fix: this.linterOptions.fix, + } as const; + + this.eslint ??= new ESLint(eslintOptions); + initDebug('ESLint class created with options %O', eslintOptions); + } + + /** + * lintFiles lints the files on the XO instance + * @param globs glob pattern to pass to globby + * @returns XoLintResult + * @throws Error + */ + async lintFiles(globs?: string | string[]): Promise { + const lintFilesDebug = debug.extend('lintFiles'); + lintFilesDebug('lintFiles called with globs %O'); + + if (!globs || (Array.isArray(globs) && globs.length === 0)) { + globs = `**/*.{${ALL_EXTENSIONS.join(',')}}`; + } + + globs = arrify(globs); + + let files: string | string[] = await globby(globs, { + ignore: DEFAULT_IGNORES, + onlyFiles: true, + gitignore: true, + absolute: true, + cwd: this.linterOptions.cwd, + }); + + await this.initEslint(files); + lintFilesDebug('initEslint complete'); + + if (!this.eslint) { + throw new Error('Failed to initialize ESLint'); + } + + lintFilesDebug('globby success %O', files); + + if (files.length === 0) { + files = '!**/*'; + } + + const results = await this.eslint.lintFiles(files); + + lintFilesDebug('linting files success'); + + const rulesMeta = this.eslint.getRulesMetaForResults(results); + + lintFilesDebug('get rulesMeta success'); + + return this.processReport(results, {rulesMeta}); + } + + /** + * lintText lints the text on the XO instance + * @param code + * @param lintTextOptions + * @returns XoLintResult + * @throws Error + */ + async lintText( + code: string, + lintTextOptions: LintTextOptions, + ): Promise { + const {filePath, warnIgnored} = lintTextOptions; + + await this.initEslint([filePath]); + + if (!this.eslint) { + throw new Error('Failed to initialize ESLint'); + } + + const results = await this.eslint?.lintText(code, { + filePath, + warnIgnored, + + }); + + const rulesMeta = this.eslint.getRulesMetaForResults(results ?? []); + + return this.processReport(results ?? [], {rulesMeta}); + } + + async calculateConfigForFile(filePath: string): Promise { + await this.initEslint(); + + if (!this.eslint) { + throw new Error('Failed to initialize ESLint'); + } + + return this.eslint.calculateConfigForFile(filePath) as Promise; + } + + async getFormatter(name: string) { + await this.initEslint(); + + if (!this.eslint) { + throw new Error('Failed to initialize ESLint'); + } + + return this.eslint.loadFormatter(name); + } + + private processReport( + report: ESLint.LintResult[], + {rulesMeta = {}} = {}, + ): XoLintResult { + if (this.linterOptions.quiet) { + report = ESLint.getErrorResults(report); + } + + const result = { + results: report, + rulesMeta, + ...this.getReportStatistics(report), + }; + + defineLazyProperty(result, 'usedDeprecatedRules', () => { + const seenRules = new Set(); + const rules = []; + + for (const {usedDeprecatedRules} of report) { + for (const rule of usedDeprecatedRules) { + if (seenRules.has(rule.ruleId)) { + continue; + } + + seenRules.add(rule.ruleId); + rules.push(rule); + } + } + + return rules; + }); + + return result; + } + + private getReportStatistics(results: ESLint.LintResult[]) { + const statistics = { + errorCount: 0, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + }; + + for (const result of results) { + statistics.errorCount += result.errorCount; + statistics.warningCount += result.warningCount; + statistics.fixableErrorCount += result.fixableErrorCount; + statistics.fixableWarningCount += result.fixableWarningCount; + } + + return statistics; + } +} + +export type * from './types.js'; +export * from './create-eslint-config/index.js'; +export default XO; diff --git a/package.json b/package.json index be6fa4b2..8a38a94b 100644 --- a/package.json +++ b/package.json @@ -1,126 +1,114 @@ { - "name": "xo", - "version": "0.60.0", - "description": "JavaScript/TypeScript linter (ESLint wrapper) with great defaults", - "license": "MIT", - "repository": "xojs/xo", - "funding": "https://github.com/sponsors/sindresorhus", - "author": { - "name": "Sindre Sorhus", - "email": "sindresorhus@gmail.com", - "url": "https://sindresorhus.com" - }, - "type": "module", - "bin": "./cli.js", - "sideEffects": false, - "engines": { - "node": ">=18.18" - }, - "scripts": { - "test:clean": "find ./test -type d -name 'node_modules' -prune -not -path ./test/fixtures/project/node_modules -exec rm -rf '{}' +", - "test": "node cli.js && nyc ava" - }, - "files": [ - "config", - "lib", - "*.js" - ], - "keywords": [ - "cli-app", - "cli", - "xo", - "xoxo", - "happy", - "happiness", - "code", - "quality", - "style", - "lint", - "linter", - "jshint", - "jslint", - "eslint", - "validate", - "code style", - "standard", - "strict", - "check", - "checker", - "verify", - "enforce", - "hint", - "simple", - "javascript", - "typescript" - ], - "dependencies": { - "@eslint/eslintrc": "^3.2.0", - "@typescript-eslint/eslint-plugin": "^7.16.1", - "@typescript-eslint/parser": "^7.16.1", - "arrify": "^3.0.0", - "cosmiconfig": "^9.0.0", - "define-lazy-prop": "^3.0.0", - "eslint": "^8.57.0", - "eslint-config-prettier": "^9.1.0", - "eslint-config-xo": "^0.45.0", - "eslint-config-xo-typescript": "^5.0.0", - "eslint-formatter-pretty": "^6.0.1", - "eslint-import-resolver-webpack": "^0.13.9", - "eslint-plugin-ava": "^14.0.0", - "eslint-plugin-eslint-comments": "^3.2.0", - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-n": "^17.14.0", - "eslint-plugin-no-use-extend-native": "^0.5.0", - "eslint-plugin-prettier": "^5.2.1", - "eslint-plugin-promise": "^6.4.0", - "eslint-plugin-unicorn": "^56.0.1", - "esm-utils": "^4.3.0", - "find-cache-dir": "^5.0.0", - "find-up-simple": "^1.0.0", - "get-stdin": "^9.0.0", - "get-tsconfig": "^4.8.1", - "globby": "^14.0.2", - "imurmurhash": "^0.1.4", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash-es": "^4.17.21", - "meow": "^13.2.0", - "micromatch": "^4.0.8", - "open-editor": "^5.0.0", - "prettier": "^3.4.2", - "semver": "^7.6.3", - "slash": "^5.1.0", - "to-absolute-glob": "^3.0.0", - "typescript": "^5.7.2" - }, - "devDependencies": { - "ava": "^6.2.0", - "eslint-config-xo-react": "^0.27.0", - "eslint-plugin-react": "^7.37.2", - "eslint-plugin-react-hooks": "^5.1.0", - "execa": "^9.5.1", - "nyc": "^17.1.0", - "proxyquire": "^2.1.3", - "temp-write": "^6.0.0", - "webpack": "^5.97.1" - }, - "xo": { - "ignores": [ - "test/fixtures", - "test/temp", - "coverage" - ] - }, - "ava": { - "files": [ - "!test/temp" - ], - "timeout": "1m", - "workerThreads": false - }, - "nyc": { - "reporter": [ - "text", - "lcov" - ] - } + "name": "@spence-s/flat-xo", + "version": "0.0.5", + "description": "xo with eslint flat config", + "type": "module", + "exports": { + ".": "./dist/index.js", + "./create-eslint-config.js": "./dist/lib/create-eslint-config/index.js" + }, + "main": "dist/index.js", + "bin": { + "xo": "dist/lib/cli.js" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "npm run clean && tsc", + "build:watch": "tsc --watch", + "clean": "rm -rf dist", + "lint": "node dist/lib/cli.js", + "release": "np", + "test": "npm run build && npm run lint && npm run test:setup && ava", + "test:setup": "node scripts/setup-tests" + }, + "prettier": { + "plugins": [ + "prettier-plugin-packagejson" + ] + }, + "ava": { + "environmentVariables": { + "NODE_NO_WARNINGS": "1" + }, + "files": [ + "dist/test/**/*.js", + "!dist/test/fixtures/**", + "!dist/test/helpers/**", + "!dist/test/scripts/**" + ], + "nodeArguments": [ + "--enable-source-maps" + ], + "verbose": true + }, + "dependencies": { + "@eslint-community/eslint-plugin-eslint-comments": "^4.4.1", + "@eslint/eslintrc": "^3.2.0", + "@sindresorhus/tsconfig": "^7.0.0", + "@stylistic/eslint-plugin": "^2.13.0", + "@typescript-eslint/parser": "^8.20.0", + "arrify": "^3.0.0", + "cosmiconfig": "^9.0.0", + "debug": "^4.4.0", + "dedent": "^1.5.3", + "define-lazy-prop": "^3.0.0", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "eslint-config-xo-typescript": "^7.0.0", + "eslint-formatter-pretty": "^6.0.1", + "eslint-plugin-ava": "^15.0.1", + "eslint-plugin-import-x": "^4.6.1", + "eslint-plugin-n": "^17.15.1", + "eslint-plugin-no-use-extend-native": "^0.7.2", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-promise": "^7.2.1", + "eslint-plugin-unicorn": "^56.0.1", + "esm-utils": "^4.3.0", + "find-cache-dir": "^5.0.0", + "find-up": "^7.0.0", + "get-stdin": "^9.0.0", + "get-tsconfig": "^4.8.1", + "glob": "^11.0.1", + "globals": "^15.14.0", + "globby": "^14.0.2", + "imurmurhash": "^0.1.4", + "json-stable-stringify-without-jsonify": "^1.0.1", + "json5": "^2.2.3", + "lodash.isempty": "^4.4.0", + "lodash.pick": "^4.4.0", + "meow": "^13.2.0", + "micromatch": "^4.0.8", + "open-editor": "^5.1.0", + "prettier": "^3.4.2", + "semver": "^7.6.3", + "slash": "^5.1.0", + "to-absolute-glob": "^3.0.0", + "type-fest": "^4.32.0", + "typescript": "^5.7.3", + "typescript-eslint": "^8.20.0", + "webpack": "^5.97.1" + }, + "devDependencies": { + "@commitlint/cli": "^19.6.1", + "@commitlint/config-conventional": "^19.6.0", + "@types/debug": "^4.1.12", + "@types/eslint": "9.6.1", + "@types/lodash.isempty": "^4.4.9", + "@types/lodash.pick": "^4.4.9", + "@types/micromatch": "^4.0.9", + "@types/prettier": "^3.0.0", + "ava": "^6.2.0", + "execa": "^9.5.2", + "husky": "^9.1.7", + "lint-staged": "^15.3.0", + "np": "^10.1.0", + "npm-package-json-lint": "^8.0.0", + "npm-package-json-lint-config-default": "^7.0.1", + "path-exists": "^5.0.0", + "prettier-plugin-packagejson": "^2.5.6", + "temp-dir": "^3.0.0", + "xo": "file:." + } } diff --git a/readme.md b/readme.md index 157e50a2..992a7be9 100644 --- a/readme.md +++ b/readme.md @@ -43,9 +43,9 @@ It uses [ESLint](https://eslint.org) underneath, so issues regarding built-in ru npm install xo --save-dev ``` -*You must install XO locally. You can run it directly with `$ npx xo`.* +_You must install XO locally. You can run it directly with `$ npx xo`._ -*JSX is supported by default, but you'll need [eslint-config-xo-react](https://github.com/xojs/eslint-config-xo-react#use-with-xo) for React specific linting. Vue components are not supported by default. You'll need [eslint-config-xo-vue](https://github.com/ChocPanda/eslint-config-xo-vue#use-with-xo) for specific linting in a Vue app.* +_JSX is supported by default, but you'll need [eslint-config-xo-react](https://github.com/xojs/eslint-config-xo-react#use-with-xo) for React specific linting. Vue components are not supported by default. You'll need [eslint-config-xo-vue](https://github.com/ChocPanda/eslint-config-xo-vue#use-with-xo) for specific linting in a Vue app._ ## Usage @@ -57,22 +57,15 @@ $ xo --help Options --fix Automagically fix issues - --reporter Reporter to use --env Environment preset [Can be set multiple times] - --global Global variable [Can be set multiple times] --ignore Additional paths to ignore [Can be set multiple times] --space Use space indent instead of tabs [Default: 2] --no-semicolon Prevent use of semicolons --prettier Conform to Prettier code style - --node-version Range of Node.js version to support --plugin Include third-party plugins [Can be set multiple times] --extend Extend defaults with a custom config [Can be set multiple times] - --open Open files with issues in your editor --quiet Show only errors and no warnings - --extension Additional extension to lint [Can be set multiple times] --cwd= Working directory for files - --stdin Validate/fix code from stdin - --stdin-filename Specify a filename for the --stdin option --print-config Print the ESLint configuration for the given file Examples @@ -80,23 +73,15 @@ $ xo --help $ xo index.js $ xo *.js !foo.js $ xo --space - $ xo --env=node --env=mocha - $ xo --plugin=react - $ xo --plugin=html --extension=html - $ echo 'const x=true' | xo --stdin --fix $ xo --print-config=index.js - - Tips - - Add XO to your project with `npm init xo`. - - Put options in package.json instead of using flags so other tools can read it. ``` ## Default code style -*Any of these can be [overridden](#rules) if necessary.* +_Any of these can be [overridden](#rules) if necessary._ -- Tab indentation *[(or space)](#space)* -- Semicolons *[(or not)](#semicolon)* +- Tab indentation _[(or space)](#space)_ +- Semicolons _[(or not)](#semicolon)_ - Single-quotes - [Trailing comma](https://medium.com/@nikgraf/why-you-should-enforce-dangling-commas-for-multiline-statements-d034c98e36f8) for multiline statements - No unused variables @@ -111,122 +96,36 @@ The recommended workflow is to add XO locally to your project and run it with th Simply run `$ npm init xo` (with any options) to add XO to your package.json or create one. -### Before/after - -```diff - { - "name": "awesome-package", - "scripts": { -- "test": "ava", -+ "test": "xo && ava" - }, - "devDependencies": { -- "ava": "^3.0.0" -+ "ava": "^3.0.0", -+ "xo": "^0.41.0" - } - } -``` - -Then just run `$ npm test` and XO will be run before your tests. - ## Config -You can configure XO options with one of the following files: - -1. As JSON in the `xo` property in `package.json`: - -```json -{ - "name": "awesome-package", - "xo": { - "space": true - } -} -``` - -2. As JSON in `.xo-config` or `.xo-config.json`: - -```json -{ - "space": true -} -``` - -3. As a JavaScript module in `.xo-config.js` or `xo.config.js`: - -```js -module.exports = { - space: true -}; -``` - -4. For [ECMAScript module (ESM)](https://nodejs.org/api/esm.html) packages with [`"type": "module"`](https://nodejs.org/api/packages.html#packages_type), as a JavaScript module in `.xo-config.cjs` or `xo.config.cjs`: - -```js -module.exports = { - space: true -}; -``` - -[Globals](https://eslint.org/docs/user-guide/configuring/language-options#specifying-globals) and [rules](https://eslint.org/docs/user-guide/configuring/rules#configuring-rules) can be configured inline in files. - -### envs +You can configure XO options by creating an `xo.config.js` or an `xo.config.ts` file in the root directory of your project. XO's config is an extension of ESLints Flat Config. Like ESLint, an XO config exports an array of XO config objects. XO config objects extend [ESLint Configuration Objects](https://eslint.org/docs/latest/use/configure/configuration-files#configuration-objects). This means all the available configuration params for ESLint also work for `XO`. However, `XO` enhances and adds extra params to the configuration objects. -Type: `string[]`\ -Default: `['es2021', 'node']` +### files -Which [environments](https://eslint.org/docs/user-guide/configuring/language-options#specifying-environments) your code is designed to run in. Each environment brings with it a certain set of predefined global variables. +type: `string | string[] | undefined`, +default: `**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}`; -### globals - -Type: `string[]` - -Additional global variables your code accesses during execution. +A glob or array of glob strings which the config object will apply. By default `XO` will apply the configuration to [all files](lib/constants.ts). ### ignores Type: `string[]` -Some [paths](lib/options-manager.js) are ignored by default, including paths in `.gitignore` and [.eslintignore](https://eslint.org/docs/user-guide/configuring/ignoring-code#the-eslintignore-file). Additional ignores can be added here. +Some [paths](lib/constants.ts) are ignored by default, including paths in `.gitignore` and [.eslintignore](https://eslint.org/docs/user-guide/configuring/ignoring-code#the-eslintignore-file). Additional ignores can be added here. ### space Type: `boolean | number`\ -Default: `false` *(tab indentation)* +Default: `false` _(tab indentation)_ Set it to `true` to get 2-space indentation or specify the number of spaces. This option exists for pragmatic reasons, but I would strongly recommend you read ["Why tabs are superior"](http://lea.verou.me/2012/01/why-tabs-are-clearly-superior/). -### rules - -Type: `object` - -Override any of the [default rules](https://github.com/xojs/eslint-config-xo/blob/main/index.js). See the [ESLint docs](https://eslint.org/docs/rules/) for more info on each rule. - -Disable a rule in your XO config to turn it off globally in your project. - -Example using `package.json`: - -```json -{ - "xo": { - "rules": { - "unicorn/no-array-for-each": "off" - } - } -} -``` - -You could also use `.xo-config.json` or one of the other config file formats supported by XO. - -Please take a moment to consider if you really need to use this option. - ### semicolon Type: `boolean`\ -Default: `true` *(Semicolons required)* +Default: `true` _(Semicolons required)_ Set it to `false` to enforce no-semicolon style. @@ -238,6 +137,7 @@ Default: `false` Format code with [Prettier](https://github.com/prettier/prettier). [Prettier options](https://prettier.io/docs/en/options.html) will be based on your [Prettier config](https://prettier.io/docs/en/configuration.html). XO will then **merge** your options with its own defaults: + - [semi](https://prettier.io/docs/en/options.html#semicolons): based on [semicolon](#semicolon) option - [useTabs](https://prettier.io/docs/en/options.html#tabs): based on [space](#space) option - [tabWidth](https://prettier.io/docs/en/options.html#tab-width): based on [space](#space) option @@ -248,170 +148,24 @@ Format code with [Prettier](https://github.com/prettier/prettier). To stick with Prettier's defaults, add this to your Prettier config: ```js -module.exports = { - trailingComma: 'es5', - singleQuote: false, - bracketSpacing: true, +export default { + trailingComma: "es5", + singleQuote: false, + bracketSpacing: true, }; ``` If contradicting options are set for both Prettier and XO, an error will be thrown. -### nodeVersion - -Type: `string | boolean`\ -Default: Value of the `engines.node` key in the project `package.json` - -Enable rules specific to the Node.js versions within the configured range. - -If set to `false`, no rules specific to a Node.js version will be enabled. - -### plugins - -Type: `string[]` - -Include third-party [plugins](https://eslint.org/docs/user-guide/configuring/plugins#configuring-plugins). - -### extends - -Type: `string | string[]` - -Use one or more [shareable configs](https://eslint.org/docs/developer-guide/shareable-configs) or [plugin configs](https://eslint.org/docs/user-guide/configuring/configuration-files#using-a-configuration-from-a-plugin) to override any of the default rules (like `rules` above). - -### extensions - -Type: `string[]` - -Allow more extensions to be linted besides `.js`, `.jsx`, `.mjs`, and `.cjs` as well as their TypeScript equivalents `.ts`, `.tsx`, `.mts` and `.cts`. Make sure they're supported by ESLint or an ESLint plugin. - -### settings - -Type: `object` - -[Shared ESLint settings](https://eslint.org/docs/user-guide/configuring/configuration-files#adding-shared-settings) exposed to rules. - -### parser - -Type: `string` - -ESLint parser. For example, [`@babel/eslint-parser`](https://github.com/babel/babel/tree/main/eslint/babel-eslint-parser) if you're using language features that ESLint doesn't yet support. - -### processor - -Type: `string` - -[ESLint processor.](https://eslint.org/docs/user-guide/configuring/plugins#specifying-processor) - -### webpack - -Type: `boolean | object` -Default: `false` - -Use [eslint-import-resolver-webpack](https://github.com/benmosher/eslint-plugin-import/tree/master/resolvers/webpack) to resolve import search paths. This is enabled automatically if a `webpack.config.js` file is found. - -Set this to a boolean to explicitly enable or disable the resolver. - -Setting this to an object enables the resolver and passes the object as configuration. See the [resolver readme](https://github.com/benmosher/eslint-plugin-import/blob/master/resolvers/webpack/README.md) along with the [webpack documentation](https://webpack.js.org/configuration/resolve/) for more information. - -## TypeScript - -XO will automatically lint TypeScript files (`.ts`, `.mts`, `.cts`, `.d.ts` and `.tsx`) with the rules defined in [eslint-config-xo-typescript#use-with-xo](https://github.com/xojs/eslint-config-xo-typescript#use-with-xo). - -XO will handle the [@typescript-eslint/parser `project` option](https://typescript-eslint.io/packages/parser/#project) automatically even if you don't have a `tsconfig.json` in your project. - -## GitHub Actions - -XO uses a different formatter when running in a GitHub Actions workflow to be able to get [inline annotations](https://developer.github.com/changes/2019-09-06-more-check-annotations-shown-in-files-changed-tab/). XO also disables warnings here. - -**Note**: For this to work, the [setup-node](https://github.com/actions/setup-node) action must be run before XO. - -## Config Overrides - -XO makes it easy to override configs for specific files. The `overrides` property must be an array of override objects. Each override object must contain a `files` property which is a glob string, or an array of glob strings, relative to the config file. The remaining properties are identical to those described above, and will override the settings of the base config. If multiple override configs match the same file, each matching override is applied in the order it appears in the array. This means the last override in the array takes precedence over earlier ones. Consider the following example: - -```json -{ - "xo": { - "semicolon": false, - "space": 2, - "overrides": [ - { - "files": "test/*.js", - "space": 3 - }, - { - "files": "test/foo.js", - "semicolon": true - } - ] - } -} -``` - -- The base configuration is simply `space: 2`, `semicolon: false`. These settings are used for every file unless otherwise noted below. - -- For every file in `test/*.js`, the base config is used, but `space` is overridden with `3`. The resulting config is: - -```json -{ - "semicolon": false, - "space": 3 -} -``` - -- For `test/foo.js`, the base config is first applied, followed the first overrides config (its glob pattern also matches `test/foo.js`), finally the second override config is applied. The resulting config is: - -```json -{ - "semicolon": true, - "space": 3 -} -``` - ## Tips -### Using a parent's config +### The --ts option -If you have a directory structure with nested `package.json` files and you want one of the child manifests to be skipped, you can do so by ommiting the `xo` property in the child's `package.json`. For example, when you have separate app and dev `package.json` files with `electron-builder`. +By default, `XO` will handle all aspects of [type aware linting](https://typescript-eslint.io/getting-started/typed-linting/), even when a file is not included in a tsconfig, which would normally error when using ESLint directly. However, this incurs a small performance penalty of having to look up the tsconfig each time in order to calculate and write an appropriate default tscfonfig to use for the file. In situations where you are linting often, you may want to configure your project correctly for type aware linting. This can help performance in editor plugins. ### Monorepo -Put a `package.json` with your config at the root and omit the `xo` property in the `package.json` of your bundled packages. - -### Transpilation - -If some files in your project are transpiled in order to support an older Node.js version, you can use the [config overrides](#config-overrides) option to set a specific [`nodeVersion`](#nodeversion) to target your sources files. - -For example, if your project targets Node.js 8 but you want to use the latest JavaScript syntax as supported in Node.js 12: -1. Set the `engines.node` property of your `package.json` to `>=8` -2. Configure [Babel](https://babeljs.io) to transpile your source files (in `source` directory in this example) -3. Make sure to include the transpiled files in your published package with the [`files`](https://docs.npmjs.com/files/package.json#files) and [`main`](https://docs.npmjs.com/files/package.json#main) properties of your `package.json` -4. Configure the XO `overrides` option to set `nodeVersion` to `>=12` for your source files directory - -```json -{ - "engines": { - "node": ">=12" - }, - "scripts": { - "build": "babel source --out-dir distribution" - }, - "main": "distribution/index.js", - "files": [ - "distribution/**/*.js" - ], - "xo": { - "overrides": [ - { - "files": "source/**/*.js", - "nodeVersion": ">=16" - } - ] - } -} -``` - -This way your `package.json` will contain the actual minimum Node.js version supported by your published code, but XO will lint your source code as if it targets Node.js 16. +Put a `xo.config.js` with your config at the root and do not add a config to any of your bundled packages. ### Including files ignored by default @@ -419,11 +173,9 @@ To include files that XO [ignores by default](lib/constants.js#L1), add them as ```json { - "xo": { - "ignores": [ - "!vendor/**" - ] - } + "xo": { + "ignores": ["!vendor/**"] + } } ``` @@ -435,7 +187,7 @@ It means [hugs and kisses](https://en.wiktionary.org/wiki/xoxo). #### Why not Standard? -The [Standard style](https://standardjs.com) is a really cool idea. I too wish we could have one style to rule them all! But the reality is that the JS community is just too diverse and opinionated to create *one* code style. They also made the mistake of pushing their own style instead of the most popular one. In contrast, XO is more pragmatic and has no aspiration of being *the* style. My goal with XO is to make it simple to enforce consistent code style with close to no config. XO comes with my code style preference by default, as I mainly made it for myself, but everything is configurable. +The [Standard style](https://standardjs.com) is a really cool idea. I too wish we could have one style to rule them all! But the reality is that the JS community is just too diverse and opinionated to create _one_ code style. They also made the mistake of pushing their own style instead of the most popular one. In contrast, XO is more pragmatic and has no aspiration of being _the_ style. My goal with XO is to make it simple to enforce consistent code style with close to no config. XO comes with my code style preference by default, as I mainly made it for myself, but everything is configurable. #### Why not ESLint? @@ -476,7 +228,7 @@ XO is based on ESLint. This project started out as just a shareable ESLint confi ## Related -- [eslint-plugin-unicorn](https://github.com/sindresorhus/eslint-plugin-unicorn) - Various awesome ESLint rules *(Bundled in XO)* +- [eslint-plugin-unicorn](https://github.com/sindresorhus/eslint-plugin-unicorn) - Various awesome ESLint rules _(Bundled in XO)_ - [xo-summary](https://github.com/LitoMore/xo-summary) - Display output from `xo` as a list of style errors, ordered by count ## Badge diff --git a/scripts/setup-tests.js b/scripts/setup-tests.js new file mode 100644 index 00000000..ea302bc8 --- /dev/null +++ b/scripts/setup-tests.js @@ -0,0 +1,48 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import tempDir from 'temp-dir'; +import {$} from 'execa'; +import {pathExists} from 'path-exists'; + +/** + * Creates a test project with a package.json and tsconfig.json + * and installs the dependencies. + * + * @returns {string} The path to the test project. + */ +const cwd = path.join(tempDir, 'test-project'); + +if (await pathExists(cwd)) { + await fs.rm(cwd, {recursive: true, force: true}); +} + +// create the test project directory +await fs.mkdir(cwd, {recursive: true}); + +// create a package.json file +await fs.writeFile( + path.join(cwd, 'package.json'), + JSON.stringify({ + type: 'module', + name: 'test-project', + }), +); + +// create a tsconfig.json file +await fs.writeFile( + path.join(cwd, 'tsconfig.json'), + JSON.stringify({ + compilerOptions: { + module: 'node16', + target: 'ES2022', + strictNullChecks: true, + lib: ['DOM', 'DOM.Iterable', 'ES2022'], + }, + files: [path.join(cwd, 'test.ts')], + exclude: ['node_modules'], + }), +); + +// npm install in the test project directory +// which we will repeatedly copy in the temp dir to test the project against +await $({cwd, stdio: 'inherit'})`npm install --save-dev typescript @types/node @sindresorhus/tsconfig`; diff --git a/test/cli.js b/test/cli.js deleted file mode 100644 index 0cbbedf1..00000000 --- a/test/cli.js +++ /dev/null @@ -1,225 +0,0 @@ -import process from 'node:process'; -import fs from 'node:fs'; -import path from 'node:path'; -import test from 'ava'; -import {execa} from 'execa'; -import slash from 'slash'; -import createEsmUtils from 'esm-utils'; - -const {__dirname} = createEsmUtils(import.meta); -process.chdir(__dirname); - -const main = (arguments_, options) => execa(path.join(__dirname, '../cli.js'), arguments_, options); - -test('fix option', async t => { - const cwd = await fs.promises.mkdtemp(path.join(__dirname, './temp/')); - const filepath = path.join(cwd, 'x.js'); - await fs.promises.writeFile(filepath, 'console.log()\n'); - await main(['--fix', filepath], {cwd}); - t.is(fs.readFileSync(filepath, 'utf8').trim(), 'console.log();'); -}); - -test('fix option with stdin', async t => { - const {stdout} = await main(['--fix', '--stdin'], { - input: 'console.log()', - }); - t.is(stdout, 'console.log();'); -}); - -test('stdin-filename option with stdin', async t => { - const {stdout} = await main(['--stdin', '--stdin-filename=unicorn-file'], { - input: 'console.log()\n', - reject: false, - }); - t.regex(stdout, /unicorn-file:/u); -}); - -test('reporter option', async t => { - const cwd = await fs.promises.mkdtemp(path.join(__dirname, './temp/')); - const filepath = path.join(cwd, 'x.js'); - await fs.promises.writeFile(filepath, 'console.log()\n'); - - const error = await t.throwsAsync(() => - main(['--reporter=compact', filepath], {cwd}), - ); - t.true(error.stdout.includes('Error - ')); -}); - -test('overrides fixture', async t => { - const cwd = path.join(__dirname, 'fixtures/overrides'); - await t.notThrowsAsync(main([], {cwd})); -}); - -test('overrides work with relative path', async t => { - const cwd = path.join(__dirname, 'fixtures/overrides'); - const file = path.join('test', 'bar.js'); - await t.notThrowsAsync(main([file], {cwd})); -}); - -test('overrides work with relative path starting with `./`', async t => { - const cwd = path.join(__dirname, 'fixtures/overrides'); - const file = '.' + path.sep + path.join('test', 'bar.js'); - await t.notThrowsAsync(main([file], {cwd})); -}); - -test('overrides work with absolute path', async t => { - const cwd = path.join(__dirname, 'fixtures/overrides'); - const file = path.join(cwd, 'test', 'bar.js'); - await t.notThrowsAsync(main([file], {cwd})); -}); - -test.failing('override default ignore', async t => { - const cwd = path.join(__dirname, 'fixtures/ignores'); - await t.throwsAsync(main([], {cwd})); -}); - -test('ignore files in .gitignore', async t => { - const cwd = path.join(__dirname, 'fixtures/gitignore'); - const error = await t.throwsAsync(main(['--reporter=json'], {cwd})); - const reports = JSON.parse(error.stdout); - const files = reports - .map(report => path.relative(cwd, report.filePath)) - .map(report => slash(report)); - t.deepEqual(files.sort(), ['index.js', 'test/bar.js'].sort()); -}); - -test('ignore explicit files when in .gitgnore', async t => { - const cwd = path.join(__dirname, 'fixtures/gitignore'); - await t.notThrowsAsync(main(['test/foo.js', '--reporter=json'], {cwd})); -}); - -test('negative gitignores', async t => { - const cwd = path.join(__dirname, 'fixtures/negative-gitignore'); - const error = await t.throwsAsync(main(['--reporter=json'], {cwd})); - const reports = JSON.parse(error.stdout); - const files = reports.map(report => path.relative(cwd, report.filePath)); - t.deepEqual(files, ['foo.js']); -}); - -test('supports being extended with a shareable config', async t => { - const cwd = path.join(__dirname, 'fixtures/project'); - await t.notThrowsAsync(main([], {cwd})); -}); - -test('quiet option', async t => { - const cwd = await fs.promises.mkdtemp(path.join(__dirname, './temp/')); - const filepath = path.join(cwd, 'x.js'); - await fs.promises.writeFile(filepath, '// TODO: quiet\nconsole.log()\n'); - const error = await t.throwsAsync(main(['--quiet', '--reporter=json', filepath], {cwd})); - const [report] = JSON.parse(error.stdout); - t.is(report.warningCount, 0); -}); - -test('invalid node-engine option', async t => { - const cwd = await fs.promises.mkdtemp(path.join(__dirname, './temp/')); - const filepath = path.join(cwd, 'x.js'); - await fs.promises.writeFile(filepath, 'console.log()\n'); - const error = await t.throwsAsync(main(['--node-version', 'v', filepath], {cwd})); - t.is(error.exitCode, 1); -}); - -test('cli option takes precedence over config', async t => { - const cwd = path.join(__dirname, 'fixtures/default-options'); - const input = 'console.log()\n'; - - // Use config from package.json - await t.notThrowsAsync(main(['--stdin'], {cwd, input})); - - // Override package.json config with cli flag - await t.throwsAsync(main(['--semicolon=true', '--stdin'], {cwd, input})); - - // Use XO default (`true`) even if option is not set in package.json nor cli arg - // i.e make sure absent cli flags are not parsed as `false` - await t.throwsAsync(main(['--stdin'], {input})); -}); - -test('space option with number value', async t => { - const cwd = path.join(__dirname, 'fixtures/space'); - const {stdout} = await t.throwsAsync(main(['--space=4', 'one-space.js'], {cwd})); - t.true(stdout.includes('Expected indentation of 4 spaces')); -}); - -test('space option as boolean', async t => { - const cwd = path.join(__dirname, 'fixtures/space'); - const {stdout} = await t.throwsAsync(main(['--space'], {cwd})); - t.true(stdout.includes('Expected indentation of 2 spaces')); -}); - -test('space option as boolean with filename', async t => { - const cwd = path.join(__dirname, 'fixtures/space'); - const {stdout} = await main(['--reporter=json', '--space', 'two-spaces.js'], { - cwd, - reject: false, - }); - const reports = JSON.parse(stdout); - - // Only the specified file was checked (filename was not the value of `space`) - t.is(reports.length, 1); - - // The default space value of 2 was expected - t.is(reports[0].errorCount, 0); -}); - -test('space option with boolean strings', async t => { - const cwd = path.join(__dirname, 'fixtures/space'); - const trueResult = await t.throwsAsync(main(['--space=true'], {cwd})); - const falseResult = await t.throwsAsync(main(['--space=false'], {cwd})); - t.true(trueResult.stdout.includes('Expected indentation of 2 spaces')); - t.true(falseResult.stdout.includes('Expected indentation of 1 tab')); -}); - -test('extension option', async t => { - const cwd = path.join(__dirname, 'fixtures/custom-extension'); - const {stdout} = await t.throwsAsync(main(['--reporter=json', '--extension=unknown'], {cwd})); - const reports = JSON.parse(stdout); - - t.is(reports.length, 1); - t.true(reports[0].filePath.endsWith('.unknown')); -}); - -test('invalid print-config flag with stdin', async t => { - const error = await t.throwsAsync(() => - main(['--print-config', 'x.js', '--stdin'], {input: 'console.log()\n'}), - ); - t.is(error.stderr.trim(), 'The `--print-config` flag is not supported on stdin'); -}); - -test('print-config flag requires a single filename', async t => { - const error = await t.throwsAsync(() => - main(['--print-config', 'x.js', 'y.js']), - ); - t.is(error.stderr.trim(), 'The `--print-config` flag must be used with exactly one filename'); -}); - -test('print-config flag without filename', async t => { - const error = await t.throwsAsync(() => - main(['--print-config']), - ); - t.is(error.stderr.trim(), 'The `--print-config` flag must be used with exactly one filename'); -}); - -test('Do not override user-config', async t => { - const cwd = path.join(__dirname, 'fixtures/no-override-user-config'); - const {stdout} = await main(['--print-config', 'index.js'], {cwd}); - const config = JSON.parse(stdout); - t.like(config, { - rules: { - 'n/no-unsupported-features/es-builtins': ['off'], - 'n/no-unsupported-features/es-syntax': ['off'], - 'n/no-unsupported-features/node-builtins': ['off'], - }, - }); - - const {stdout: stdoutOverrides} = await main(['--print-config', 'overrides.js'], {cwd}); - const configOverrides = JSON.parse(stdoutOverrides); - t.like(configOverrides, { - rules: { - 'n/no-unsupported-features/es-builtins': ['off'], - 'n/no-unsupported-features/es-syntax': ['off'], - 'n/no-unsupported-features/node-builtins': ['error'], - }, - }); - - await t.notThrowsAsync(() => main(['index.js'], {cwd})); - await t.throwsAsync(() => main(['overrides.js'], {cwd})); -}); diff --git a/test/cli.test.ts b/test/cli.test.ts new file mode 100644 index 00000000..008d046c --- /dev/null +++ b/test/cli.test.ts @@ -0,0 +1,102 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import _test, {type TestFn} from 'ava'; // eslint-disable-line ava/use-test +import dedent from 'dedent'; +import {$} from 'execa'; +import {copyTestProject} from './helpers/copy-test-project.js'; + +const test = _test as TestFn<{cwd: string}>; + +test.beforeEach(async t => { + t.context.cwd = await copyTestProject(); +}); + +test.afterEach.always(async t => { + await fs.rm(t.context.cwd, {recursive: true, force: true}); +}); + +test('xo --cwd', async t => { + const filePath = path.join(t.context.cwd, 'test.js'); + await fs.writeFile(filePath, dedent`console.log('hello');\n`, 'utf8'); + + await t.notThrowsAsync($`node ./dist/lib/cli --cwd ${t.context.cwd}`); +}); + +test('xo --fix', async t => { + const filePath = path.join(t.context.cwd, 'test.js'); + await fs.writeFile(filePath, dedent`console.log('hello')\n`, 'utf8'); + await t.notThrowsAsync($`node ./dist/lib/cli --cwd ${t.context.cwd} --fix`); + const fileContent = await fs.readFile(filePath, 'utf8'); + t.is(fileContent, dedent`console.log('hello');\n`); +}); + +test('xo --space', async t => { + const filePath = path.join(t.context.cwd, 'test.js'); + await fs.writeFile(filePath, dedent`function test() {\n return true;\n}\n`, 'utf8'); + await t.throwsAsync($`node ./dist/lib/cli --cwd ${t.context.cwd} --fix --space=2`); + const fileContent = await fs.readFile(filePath, 'utf8'); + t.is(fileContent, 'function test() {\n return true;\n}\n'); +}); + +test('xo --no-semicolon', async t => { + const filePath = path.join(t.context.cwd, 'test.js'); + await fs.writeFile(filePath, dedent`console.log('hello');\n`, 'utf8'); + await t.notThrowsAsync($`node ./dist/lib/cli --cwd ${t.context.cwd} --fix --semicolon=false`); + const fileContent = await fs.readFile(filePath, 'utf8'); + t.is(fileContent, dedent`console.log('hello')\n`); +}); + +test('xo --prettier --fix', async t => { + const filePath = path.join(t.context.cwd, 'test.js'); + await fs.writeFile(filePath, dedent`function test(){return true}\n`, 'utf8'); + await t.throwsAsync($`node ./dist/lib/cli --cwd ${t.context.cwd} --fix --prettier`); + const fileContent = await fs.readFile(filePath, 'utf8'); + t.is(fileContent, 'function test() {\n\treturn true;\n}\n'); +}); + +test('xo --print-config', async t => { + const filePath = path.join(t.context.cwd, 'test.js'); + await fs.writeFile(filePath, dedent`console.log('hello');\n`, 'utf8'); + const {stdout} = await $`node ./dist/lib/cli --cwd ${t.context.cwd} --print-config=${filePath}`; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const config = JSON.parse(stdout); + t.true(typeof config === 'object'); + t.true('rules' in config); +}); + +test('xo --ignore', async t => { + const testFile = path.join(t.context.cwd, 'test.js'); + const ignoredFile = path.join(t.context.cwd, 'ignored.js'); + + await fs.writeFile(testFile, dedent`console.log('test');\n`, 'utf8'); + await fs.writeFile(ignoredFile, dedent`console.log('ignored');\n`, 'utf8'); + + const {stdout} = await $`node ./dist/lib/cli --cwd ${t.context.cwd} --ignore="ignored.js"`; + t.false(stdout.includes('ignored.js')); +}); + +test('xo lints ts files not found in tsconfig.json', async t => { + const filePath = path.join(t.context.cwd, 'test.ts'); + const tsConfigPath = path.join(t.context.cwd, 'tsconfig.json'); + const xoTsConfigPath = path.join(t.context.cwd, 'tsconfig.xo.json'); + const tsConfig = await fs.readFile(tsConfigPath, 'utf8'); + await fs.writeFile(xoTsConfigPath, tsConfig); + await fs.rm(tsConfigPath); + await fs.writeFile(filePath, dedent`console.log('hello');\n`, 'utf8'); + await t.notThrowsAsync($`node ./dist/lib/cli --cwd ${t.context.cwd}`); + await fs.writeFile(tsConfigPath, tsConfig); + await fs.rm(xoTsConfigPath); +}); + +test('xo does not lint ts files not found in tsconfig.json when --ts=false', async t => { + const filePath = path.join(t.context.cwd, 'test.ts'); + const tsConfigPath = path.join(t.context.cwd, 'tsconfig.json'); + const xoTsConfigPath = path.join(t.context.cwd, 'tsconfig.xo.json'); + const tsConfig = await fs.readFile(tsConfigPath, 'utf8'); + await fs.writeFile(xoTsConfigPath, tsConfig); + await fs.rm(tsConfigPath); + await fs.writeFile(filePath, dedent`console.log('hello');\n`, 'utf8'); + await t.throwsAsync($`node ./dist/lib/cli --cwd ${t.context.cwd} --ts=false`); + await fs.writeFile(tsConfigPath, tsConfig); + await fs.rm(xoTsConfigPath); +}); diff --git a/test/create-eslint-config.test.ts b/test/create-eslint-config.test.ts new file mode 100644 index 00000000..3e6d89d2 --- /dev/null +++ b/test/create-eslint-config.test.ts @@ -0,0 +1,145 @@ +import fs from 'node:fs/promises'; +import _test, {type TestFn} from 'ava'; // eslint-disable-line ava/use-test +import createConfig from '../lib/create-eslint-config/index.js'; +import {copyTestProject} from './helpers/copy-test-project.js'; +import {getJsRule} from './helpers/get-rule.js'; + +const test = _test as TestFn<{cwd: string}>; + +test.beforeEach(async t => { + t.context.cwd = await copyTestProject(); +}); + +test.afterEach.always(async t => { + await fs.rm(t.context.cwd, {recursive: true, force: true}); +}); + +test('base config rules', async t => { + const flatConfig = await createConfig(undefined, t.context.cwd); + + t.deepEqual(getJsRule(flatConfig, '@stylistic/indent'), [ + 'error', + 'tab', + {SwitchCase: 1}, + ]); + t.deepEqual(getJsRule(flatConfig, '@stylistic/semi'), ['error', 'always']); + t.deepEqual(getJsRule(flatConfig, '@stylistic/quotes'), ['error', 'single']); +}); + +test('empty config rules', async t => { + const flatConfig = await createConfig([], t.context.cwd); + + t.deepEqual(getJsRule(flatConfig, '@stylistic/indent'), [ + 'error', + 'tab', + {SwitchCase: 1}, + ]); + t.deepEqual(getJsRule(flatConfig, '@stylistic/semi'), ['error', 'always']); + t.deepEqual(getJsRule(flatConfig, '@stylistic/quotes'), ['error', 'single']); +}); + +test('config with space option', async t => { + const flatConfig = await createConfig([{space: true}], t.context.cwd); + + t.deepEqual(getJsRule(flatConfig, '@stylistic/indent'), [ + 'error', + 2, + {SwitchCase: 1}, + ]); +}); + +test('config with semi false option', async t => { + const flatConfig = await createConfig([{semicolon: false}], t.context.cwd); + + t.deepEqual(getJsRule(flatConfig, '@stylistic/semi'), ['error', 'never']); +}); + +test('config with rules', async t => { + const flatConfig = await createConfig([{rules: {'no-console': 'error'}}], t.context.cwd); + + t.is(getJsRule(flatConfig, 'no-console'), 'error'); +}); + +test('with prettier option', async t => { + const flatConfig = await createConfig([{prettier: true}], t.context.cwd); + + const prettierConfigTs = flatConfig.find(config => + typeof config?.plugins?.['prettier'] === 'object' + && config?.files?.[0]?.includes('ts')); + + t.truthy(prettierConfigTs); + + const prettierConfigJs = flatConfig.find(config => + typeof config?.plugins?.['prettier'] === 'object' + && config?.files?.[0]?.includes('js')); + + t.truthy(prettierConfigJs); + + t.deepEqual(prettierConfigJs?.rules?.['prettier/prettier'], [ + 'error', + { + bracketSameLine: false, + bracketSpacing: false, + semi: undefined, + singleQuote: true, + tabWidth: 2, + trailingComma: 'all', + useTabs: true, + }, + ]); + + t.deepEqual(prettierConfigTs?.rules?.['prettier/prettier'], [ + 'error', + { + bracketSameLine: false, + bracketSpacing: false, + semi: undefined, + singleQuote: true, + tabWidth: 2, + trailingComma: 'all', + useTabs: true, + }, + ]); +}); + +test('with prettier option and space', async t => { + const flatConfig = await createConfig([{prettier: true, space: true}], t.context.cwd); + + const prettierConfigTs = flatConfig.find(config => + typeof config?.plugins?.['prettier'] === 'object' + && config?.files?.[0]?.includes('ts')); + + t.truthy(prettierConfigTs); + + const prettierConfigJs = flatConfig.find(config => + typeof config?.plugins?.['prettier'] === 'object' + && config?.files?.[0]?.includes('js')); + + t.truthy(prettierConfigJs); + + t.deepEqual(prettierConfigJs?.rules?.['prettier/prettier'], [ + 'error', + { + bracketSameLine: false, + bracketSpacing: false, + semi: undefined, + singleQuote: true, + tabWidth: 2, + trailingComma: 'all', + useTabs: false, + }, + ]); + + t.deepEqual(prettierConfigTs?.rules?.['prettier/prettier'], [ + 'error', + { + bracketSameLine: false, + bracketSpacing: false, + semi: undefined, + singleQuote: true, + tabWidth: 2, + trailingComma: 'all', + useTabs: false, + }, + ]); +}); diff --git a/test/fixtures/config-files/extends-relative/.xo-config.json b/test/fixtures/config-files/extends-relative/.xo-config.json deleted file mode 100644 index 29bc67f9..00000000 --- a/test/fixtures/config-files/extends-relative/.xo-config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../extends.js" -} diff --git a/test/fixtures/config-files/extends-relative/file.js b/test/fixtures/config-files/extends-relative/file.js deleted file mode 100644 index eb542496..00000000 --- a/test/fixtures/config-files/extends-relative/file.js +++ /dev/null @@ -1,5 +0,0 @@ -const object = { - a: 1 -}; - -console.log(object.a); diff --git a/test/fixtures/config-files/xo-config/.xo-config b/test/fixtures/config-files/xo-config/.xo-config deleted file mode 100644 index 7b8b0edb..00000000 --- a/test/fixtures/config-files/xo-config/.xo-config +++ /dev/null @@ -1,3 +0,0 @@ -{ - "space": true -} diff --git a/test/fixtures/config-files/xo-config/file.js b/test/fixtures/config-files/xo-config/file.js deleted file mode 100644 index eb542496..00000000 --- a/test/fixtures/config-files/xo-config/file.js +++ /dev/null @@ -1,5 +0,0 @@ -const object = { - a: 1 -}; - -console.log(object.a); diff --git a/test/fixtures/config-files/xo-config_cjs/.xo-config.cjs b/test/fixtures/config-files/xo-config_cjs/.xo-config.cjs deleted file mode 100644 index 3c3e28a5..00000000 --- a/test/fixtures/config-files/xo-config_cjs/.xo-config.cjs +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - space: true -}; diff --git a/test/fixtures/config-files/xo-config_cjs/file.js b/test/fixtures/config-files/xo-config_cjs/file.js deleted file mode 100644 index eb542496..00000000 --- a/test/fixtures/config-files/xo-config_cjs/file.js +++ /dev/null @@ -1,5 +0,0 @@ -const object = { - a: 1 -}; - -console.log(object.a); diff --git a/test/fixtures/config-files/xo-config_js/.xo-config.js b/test/fixtures/config-files/xo-config_js/.xo-config.js deleted file mode 100644 index 3c3e28a5..00000000 --- a/test/fixtures/config-files/xo-config_js/.xo-config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - space: true -}; diff --git a/test/fixtures/config-files/xo-config_js/file.js b/test/fixtures/config-files/xo-config_js/file.js deleted file mode 100644 index eb542496..00000000 --- a/test/fixtures/config-files/xo-config_js/file.js +++ /dev/null @@ -1,5 +0,0 @@ -const object = { - a: 1 -}; - -console.log(object.a); diff --git a/test/fixtures/config-files/xo-config_json/.xo-config.json b/test/fixtures/config-files/xo-config_json/.xo-config.json deleted file mode 100644 index 368f2916..00000000 --- a/test/fixtures/config-files/xo-config_json/.xo-config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "space": true -} diff --git a/test/fixtures/config-files/xo-config_json/file.js b/test/fixtures/config-files/xo-config_json/file.js deleted file mode 100644 index eb542496..00000000 --- a/test/fixtures/config-files/xo-config_json/file.js +++ /dev/null @@ -1,5 +0,0 @@ -const object = { - a: 1 -}; - -console.log(object.a); diff --git a/test/fixtures/config-files/xo_config_cjs/file.js b/test/fixtures/config-files/xo_config_cjs/file.js deleted file mode 100644 index eb542496..00000000 --- a/test/fixtures/config-files/xo_config_cjs/file.js +++ /dev/null @@ -1,5 +0,0 @@ -const object = { - a: 1 -}; - -console.log(object.a); diff --git a/test/fixtures/config-files/xo_config_cjs/xo.config.cjs b/test/fixtures/config-files/xo_config_cjs/xo.config.cjs deleted file mode 100644 index 3c3e28a5..00000000 --- a/test/fixtures/config-files/xo_config_cjs/xo.config.cjs +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - space: true -}; diff --git a/test/fixtures/config-files/xo_config_js/file.js b/test/fixtures/config-files/xo_config_js/file.js deleted file mode 100644 index eb542496..00000000 --- a/test/fixtures/config-files/xo_config_js/file.js +++ /dev/null @@ -1,5 +0,0 @@ -const object = { - a: 1 -}; - -console.log(object.a); diff --git a/test/fixtures/config-files/xo_config_js/xo.config.js b/test/fixtures/config-files/xo_config_js/xo.config.js deleted file mode 100644 index 3c3e28a5..00000000 --- a/test/fixtures/config-files/xo_config_js/xo.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - space: true -}; diff --git a/test/fixtures/custom-extension/one-space.unknown b/test/fixtures/custom-extension/one-space.unknown deleted file mode 100644 index 0b1b5e7d..00000000 --- a/test/fixtures/custom-extension/one-space.unknown +++ /dev/null @@ -1,3 +0,0 @@ -console.log([ - 1 -]); diff --git a/test/fixtures/custom-extension/readme.md b/test/fixtures/custom-extension/readme.md deleted file mode 120000 index 2f969b9f..00000000 --- a/test/fixtures/custom-extension/readme.md +++ /dev/null @@ -1 +0,0 @@ -../../../readme.md \ No newline at end of file diff --git a/test/fixtures/cwd/unicorn.js b/test/fixtures/cwd/unicorn.js deleted file mode 100644 index 4728aef2..00000000 --- a/test/fixtures/cwd/unicorn.js +++ /dev/null @@ -1 +0,0 @@ -console.log('no semicolon') diff --git a/test/fixtures/default-options/package.json b/test/fixtures/default-options/package.json deleted file mode 100644 index 24ef9768..00000000 --- a/test/fixtures/default-options/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "xo": { - "semicolon": false - } -} diff --git a/test/fixtures/engines-overrides/package.json b/test/fixtures/engines-overrides/package.json deleted file mode 100644 index 3a598f61..00000000 --- a/test/fixtures/engines-overrides/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "application-name", - "version": "0.0.1", - "engines": { - "node": ">=6.0.0" - }, - "xo": { - "overrides": [ - { - "files": "*-transpile.js", - "nodeVersion": ">=8.0.0" - } - ] - } -} diff --git a/test/fixtures/engines-overrides/promise-then-transpile.js b/test/fixtures/engines-overrides/promise-then-transpile.js deleted file mode 100644 index bef7d5fc..00000000 --- a/test/fixtures/engines-overrides/promise-then-transpile.js +++ /dev/null @@ -1,7 +0,0 @@ -const promise = new Promise(resolve => { - resolve('test'); -}); - -function example() { - return promise.then(console.log); -} diff --git a/test/fixtures/engines-overrides/promise-then.js b/test/fixtures/engines-overrides/promise-then.js deleted file mode 100644 index bef7d5fc..00000000 --- a/test/fixtures/engines-overrides/promise-then.js +++ /dev/null @@ -1,7 +0,0 @@ -const promise = new Promise(resolve => { - resolve('test'); -}); - -function example() { - return promise.then(console.log); -} diff --git a/test/fixtures/engines/package.json b/test/fixtures/engines/package.json deleted file mode 100644 index 294b35b8..00000000 --- a/test/fixtures/engines/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "application-name", - "version": "0.0.1", - "engines": { - "node": ">=6.0.0" - } -} diff --git a/test/fixtures/eslintignore/.eslintignore b/test/fixtures/eslintignore/.eslintignore deleted file mode 100644 index 80c887a8..00000000 --- a/test/fixtures/eslintignore/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -bar.js diff --git a/test/fixtures/eslintignore/bar.js b/test/fixtures/eslintignore/bar.js deleted file mode 100644 index 4728aef2..00000000 --- a/test/fixtures/eslintignore/bar.js +++ /dev/null @@ -1 +0,0 @@ -console.log('no semicolon') diff --git a/test/fixtures/eslintignore/foo.js b/test/fixtures/eslintignore/foo.js deleted file mode 100644 index 4728aef2..00000000 --- a/test/fixtures/eslintignore/foo.js +++ /dev/null @@ -1 +0,0 @@ -console.log('no semicolon') diff --git a/test/fixtures/extends.js b/test/fixtures/extends.js deleted file mode 100644 index 4c294cb5..00000000 --- a/test/fixtures/extends.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; -module.exports = { - rules: { - 'object-curly-spacing': 0 - } -}; diff --git a/test/fixtures/gitignore-multiple-negation/!!!unicorn.js b/test/fixtures/gitignore-multiple-negation/!!!unicorn.js deleted file mode 100644 index 4728aef2..00000000 --- a/test/fixtures/gitignore-multiple-negation/!!!unicorn.js +++ /dev/null @@ -1 +0,0 @@ -console.log('no semicolon') diff --git a/test/fixtures/gitignore-multiple-negation/!!unicorn.js b/test/fixtures/gitignore-multiple-negation/!!unicorn.js deleted file mode 100644 index 4728aef2..00000000 --- a/test/fixtures/gitignore-multiple-negation/!!unicorn.js +++ /dev/null @@ -1 +0,0 @@ -console.log('no semicolon') diff --git a/test/fixtures/gitignore-multiple-negation/!unicorn.js b/test/fixtures/gitignore-multiple-negation/!unicorn.js deleted file mode 100644 index 4728aef2..00000000 --- a/test/fixtures/gitignore-multiple-negation/!unicorn.js +++ /dev/null @@ -1 +0,0 @@ -console.log('no semicolon') diff --git a/test/fixtures/gitignore-multiple-negation/.gitignore b/test/fixtures/gitignore-multiple-negation/.gitignore deleted file mode 100644 index 8595419f..00000000 --- a/test/fixtures/gitignore-multiple-negation/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -!!unicorn.js -!!!unicorn.js diff --git a/test/fixtures/gitignore-multiple-negation/unicorn.js b/test/fixtures/gitignore-multiple-negation/unicorn.js deleted file mode 100644 index 4728aef2..00000000 --- a/test/fixtures/gitignore-multiple-negation/unicorn.js +++ /dev/null @@ -1 +0,0 @@ -console.log('no semicolon') diff --git a/test/fixtures/gitignore/index.js b/test/fixtures/gitignore/index.js deleted file mode 100644 index 0a9af888..00000000 --- a/test/fixtures/gitignore/index.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict' - -module.exports = function (foo) { - return foo + 'bar' -} diff --git a/test/fixtures/gitignore/test/.gitignore b/test/fixtures/gitignore/test/.gitignore deleted file mode 100644 index 6a72320c..00000000 --- a/test/fixtures/gitignore/test/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -foo.js -!bar.js diff --git a/test/fixtures/gitignore/test/bar.js b/test/fixtures/gitignore/test/bar.js deleted file mode 100644 index aa059fbf..00000000 --- a/test/fixtures/gitignore/test/bar.js +++ /dev/null @@ -1,6 +0,0 @@ -import test from 'ava' -import fn from '../index.js' - -test('main', t => { - t.is(fn('foo'), fn('foobar')) -}) diff --git a/test/fixtures/gitignore/test/foo.js b/test/fixtures/gitignore/test/foo.js deleted file mode 100644 index f45bac36..00000000 --- a/test/fixtures/gitignore/test/foo.js +++ /dev/null @@ -1,6 +0,0 @@ -import test from 'ava' -import fn from '../' - -test('main', t => { - t.is(fn('foo'), fn('foobar')) -}) diff --git a/test/fixtures/ignores/dist/linter-error.js b/test/fixtures/ignores/dist/linter-error.js deleted file mode 100644 index 3d077d1f..00000000 --- a/test/fixtures/ignores/dist/linter-error.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - - -module.exports = function () { - - - return "all kinds of errors in this file" - -}; diff --git a/test/fixtures/ignores/index.js b/test/fixtures/ignores/index.js deleted file mode 100644 index e94f2472..00000000 --- a/test/fixtures/ignores/index.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports = function () { - return 'this module should not have any errors'; -}; diff --git a/test/fixtures/ignores/package.json b/test/fixtures/ignores/package.json deleted file mode 100644 index ec5b7c03..00000000 --- a/test/fixtures/ignores/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "application-name", - "version": "0.0.1", - "xo": { - "ignore": [ - "!dist/**" - ], - "rules": { - "unicorn/prefer-module": "off", - "unicorn/prefer-node-protocol": "off" - } - } -} diff --git a/test/fixtures/negative-gitignore/.gitignore b/test/fixtures/negative-gitignore/.gitignore deleted file mode 100644 index 9bef56ea..00000000 --- a/test/fixtures/negative-gitignore/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.js -!foo.js diff --git a/test/fixtures/negative-gitignore/bar.js b/test/fixtures/negative-gitignore/bar.js deleted file mode 100644 index 4728aef2..00000000 --- a/test/fixtures/negative-gitignore/bar.js +++ /dev/null @@ -1 +0,0 @@ -console.log('no semicolon') diff --git a/test/fixtures/negative-gitignore/foo.js b/test/fixtures/negative-gitignore/foo.js deleted file mode 100644 index 4728aef2..00000000 --- a/test/fixtures/negative-gitignore/foo.js +++ /dev/null @@ -1 +0,0 @@ -console.log('no semicolon') diff --git a/test/fixtures/nested-configs/child-override/.prettierrc b/test/fixtures/nested-configs/child-override/.prettierrc deleted file mode 100644 index 86c23c72..00000000 --- a/test/fixtures/nested-configs/child-override/.prettierrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "semi": false -} diff --git a/test/fixtures/nested-configs/child-override/child-prettier-override/package.json b/test/fixtures/nested-configs/child-override/child-prettier-override/package.json deleted file mode 100644 index cdceb53b..00000000 --- a/test/fixtures/nested-configs/child-override/child-prettier-override/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "xo": { - "overrides": [ - { - "files": "semicolon.js", - "prettier": true - } - ] - } -} diff --git a/test/fixtures/nested-configs/child-override/child-prettier-override/semicolon.js b/test/fixtures/nested-configs/child-override/child-prettier-override/semicolon.js deleted file mode 100644 index 00b248dc..00000000 --- a/test/fixtures/nested-configs/child-override/child-prettier-override/semicolon.js +++ /dev/null @@ -1 +0,0 @@ -console.log('semicolon'); diff --git a/test/fixtures/nested-configs/child-override/package.json b/test/fixtures/nested-configs/child-override/package.json deleted file mode 100644 index a6dbd700..00000000 --- a/test/fixtures/nested-configs/child-override/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "xo": { - "overrides": [ - { - "files": "two-spaces.js", - "space": 4 - } - ] - } -} diff --git a/test/fixtures/nested-configs/child-override/two-spaces.js b/test/fixtures/nested-configs/child-override/two-spaces.js deleted file mode 100644 index 70a5ead6..00000000 --- a/test/fixtures/nested-configs/child-override/two-spaces.js +++ /dev/null @@ -1,3 +0,0 @@ -console.log([ - 2 -]); diff --git a/test/fixtures/nested-configs/child/package.json b/test/fixtures/nested-configs/child/package.json deleted file mode 100644 index 24ef9768..00000000 --- a/test/fixtures/nested-configs/child/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "xo": { - "semicolon": false - } -} diff --git a/test/fixtures/nested-configs/child/semicolon.js b/test/fixtures/nested-configs/child/semicolon.js deleted file mode 100644 index 00b248dc..00000000 --- a/test/fixtures/nested-configs/child/semicolon.js +++ /dev/null @@ -1 +0,0 @@ -console.log('semicolon'); diff --git a/test/fixtures/nested-configs/no-semicolon.js b/test/fixtures/nested-configs/no-semicolon.js deleted file mode 100644 index 1008a4fc..00000000 --- a/test/fixtures/nested-configs/no-semicolon.js +++ /dev/null @@ -1 +0,0 @@ -console.log('no-semicolon') diff --git a/test/fixtures/nested-configs/package.json b/test/fixtures/nested-configs/package.json deleted file mode 100644 index c9789c52..00000000 --- a/test/fixtures/nested-configs/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "xo": { - "semicolon": true - } -} diff --git a/test/fixtures/nested-ignores/a.js b/test/fixtures/nested-ignores/a.js deleted file mode 100644 index b00bfec5..00000000 --- a/test/fixtures/nested-ignores/a.js +++ /dev/null @@ -1 +0,0 @@ -console.log('semicolon') diff --git a/test/fixtures/nested-ignores/b.js b/test/fixtures/nested-ignores/b.js deleted file mode 100644 index b00bfec5..00000000 --- a/test/fixtures/nested-ignores/b.js +++ /dev/null @@ -1 +0,0 @@ -console.log('semicolon') diff --git a/test/fixtures/nested-ignores/child/a.js b/test/fixtures/nested-ignores/child/a.js deleted file mode 100644 index b00bfec5..00000000 --- a/test/fixtures/nested-ignores/child/a.js +++ /dev/null @@ -1 +0,0 @@ -console.log('semicolon') diff --git a/test/fixtures/nested-ignores/child/b.js b/test/fixtures/nested-ignores/child/b.js deleted file mode 100644 index b00bfec5..00000000 --- a/test/fixtures/nested-ignores/child/b.js +++ /dev/null @@ -1 +0,0 @@ -console.log('semicolon') diff --git a/test/fixtures/nested-ignores/child/package.json b/test/fixtures/nested-ignores/child/package.json deleted file mode 100644 index f0a313fe..00000000 --- a/test/fixtures/nested-ignores/child/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "xo": { - "ignores": [ - "b.js", - "**/b.js" - ] - } -} diff --git a/test/fixtures/nested-ignores/package.json b/test/fixtures/nested-ignores/package.json deleted file mode 100644 index f852068c..00000000 --- a/test/fixtures/nested-ignores/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "xo": { - "ignores": [ - "a.js", - "**/a.js" - ] - } -} diff --git a/test/fixtures/nested/child-empty/package.json b/test/fixtures/nested/child-empty/package.json deleted file mode 100644 index 90bd27c7..00000000 --- a/test/fixtures/nested/child-empty/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "xo": {} -} diff --git a/test/fixtures/nested/child-ignore/package.json b/test/fixtures/nested/child-ignore/package.json deleted file mode 100644 index 0967ef42..00000000 --- a/test/fixtures/nested/child-ignore/package.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/test/fixtures/nested/child/package.json b/test/fixtures/nested/child/package.json deleted file mode 100644 index 9cc34ec6..00000000 --- a/test/fixtures/nested/child/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "xo": { - "space": true - } -} diff --git a/test/fixtures/nested/file.js b/test/fixtures/nested/file.js deleted file mode 100644 index 56756b16..00000000 --- a/test/fixtures/nested/file.js +++ /dev/null @@ -1,2 +0,0 @@ -const object = {a: 1}; -console.log(object.a); diff --git a/test/fixtures/nested/package.json b/test/fixtures/nested/package.json deleted file mode 100644 index 9cc34ec6..00000000 --- a/test/fixtures/nested/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "xo": { - "space": true - } -} diff --git a/test/fixtures/no-override-user-config/index.js b/test/fixtures/no-override-user-config/index.js deleted file mode 100644 index 9ff9f503..00000000 --- a/test/fixtures/no-override-user-config/index.js +++ /dev/null @@ -1 +0,0 @@ -const _ = new FormData(); diff --git a/test/fixtures/no-override-user-config/overrides.js b/test/fixtures/no-override-user-config/overrides.js deleted file mode 100644 index 9ff9f503..00000000 --- a/test/fixtures/no-override-user-config/overrides.js +++ /dev/null @@ -1 +0,0 @@ -const _ = new FormData(); diff --git a/test/fixtures/no-override-user-config/package.json b/test/fixtures/no-override-user-config/package.json deleted file mode 100644 index 264624c8..00000000 --- a/test/fixtures/no-override-user-config/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "xo": { - "rules": { - "n/no-unsupported-features/es-builtins": "off", - "n/no-unsupported-features/es-syntax": "off", - "n/no-unsupported-features/node-builtins": "off" - }, - "overrides": [ - { - "files": "overrides.js", - "rules": { - "n/no-unsupported-features/node-builtins": "error" - } - } - ] - }, - "engines": { - "node": ">=18" - } -} diff --git a/test/fixtures/nodir/nested/index.js b/test/fixtures/nodir/nested/index.js deleted file mode 100644 index 13203519..00000000 --- a/test/fixtures/nodir/nested/index.js +++ /dev/null @@ -1 +0,0 @@ -console.log("should not report"); diff --git a/test/fixtures/nodir/noextension b/test/fixtures/nodir/noextension deleted file mode 100644 index 4e9851b8..00000000 --- a/test/fixtures/nodir/noextension +++ /dev/null @@ -1 +0,0 @@ -console.log("should report"); diff --git a/test/fixtures/open-report/errors/one.js b/test/fixtures/open-report/errors/one.js deleted file mode 100644 index 25b43319..00000000 --- a/test/fixtures/open-report/errors/one.js +++ /dev/null @@ -1 +0,0 @@ -const a = 'a'; diff --git a/test/fixtures/open-report/errors/three.js b/test/fixtures/open-report/errors/three.js deleted file mode 100644 index b7aba66f..00000000 --- a/test/fixtures/open-report/errors/three.js +++ /dev/null @@ -1 +0,0 @@ -const a = "a"; \ No newline at end of file diff --git a/test/fixtures/open-report/errors/two-with-warnings.js b/test/fixtures/open-report/errors/two-with-warnings.js deleted file mode 100644 index f840f160..00000000 --- a/test/fixtures/open-report/errors/two-with-warnings.js +++ /dev/null @@ -1,2 +0,0 @@ -// error -// todo: this is a warning \ No newline at end of file diff --git a/test/fixtures/open-report/successes/success.js b/test/fixtures/open-report/successes/success.js deleted file mode 100644 index 30a5cf32..00000000 --- a/test/fixtures/open-report/successes/success.js +++ /dev/null @@ -1 +0,0 @@ -console.log('i am good'); diff --git a/test/fixtures/open-report/warnings/one.js b/test/fixtures/open-report/warnings/one.js deleted file mode 100644 index eec5a198..00000000 --- a/test/fixtures/open-report/warnings/one.js +++ /dev/null @@ -1 +0,0 @@ -// Todo: warning diff --git a/test/fixtures/open-report/warnings/three.js b/test/fixtures/open-report/warnings/three.js deleted file mode 100644 index 7b30a621..00000000 --- a/test/fixtures/open-report/warnings/three.js +++ /dev/null @@ -1,3 +0,0 @@ -// Todo: warning -// Todo: warning -// Todo: warning diff --git a/test/fixtures/overrides/index.js b/test/fixtures/overrides/index.js deleted file mode 100644 index 0a9af888..00000000 --- a/test/fixtures/overrides/index.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict' - -module.exports = function (foo) { - return foo + 'bar' -} diff --git a/test/fixtures/overrides/package.json b/test/fixtures/overrides/package.json deleted file mode 100644 index 57d549c9..00000000 --- a/test/fixtures/overrides/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "xo": { - "space": 2, - "semicolon": false, - "overrides": [ - { - "files": "test/*.js", - "space": 3 - }, - { - "files": "test/foo.js", - "space": 4 - } - ], - "rules": { - "import/no-extraneous-dependencies": "off", - "ava/no-ignored-test-files": "off", - "unicorn/prefer-module": "off", - "unicorn/prefer-node-protocol": "off", - "unicorn/no-anonymous-default-export": "off" - } - } -} diff --git a/test/fixtures/overrides/test/bar.js b/test/fixtures/overrides/test/bar.js deleted file mode 100644 index aa059fbf..00000000 --- a/test/fixtures/overrides/test/bar.js +++ /dev/null @@ -1,6 +0,0 @@ -import test from 'ava' -import fn from '../index.js' - -test('main', t => { - t.is(fn('foo'), fn('foobar')) -}) diff --git a/test/fixtures/overrides/test/foo.js b/test/fixtures/overrides/test/foo.js deleted file mode 100644 index 19efccc0..00000000 --- a/test/fixtures/overrides/test/foo.js +++ /dev/null @@ -1,6 +0,0 @@ -import test from 'ava' -import fn from '../index.js' - -test('main', t => { - t.is(fn('foo'), fn('foobar')) -}) diff --git a/test/fixtures/package.json b/test/fixtures/package.json deleted file mode 100644 index a0df0c86..00000000 --- a/test/fixtures/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "type": "commonjs" -} diff --git a/test/fixtures/prettier/package.json b/test/fixtures/prettier/package.json deleted file mode 100644 index efc2c8f6..00000000 --- a/test/fixtures/prettier/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "prettier": { - "useTabs": false, - "semi": false, - "tabWidth": 4, - "bracketSpacing": false, - "bracketSameLine": false, - "singleQuote": false, - "trailingComma": "all" - } -} diff --git a/test/fixtures/project/file.js b/test/fixtures/project/file.js deleted file mode 100644 index baaa239a..00000000 --- a/test/fixtures/project/file.js +++ /dev/null @@ -1,2 +0,0 @@ -const object = { a: 1 }; -console.log(object.a); diff --git a/test/fixtures/project/node_modules/eslint-config-custom/config.json b/test/fixtures/project/node_modules/eslint-config-custom/config.json deleted file mode 100644 index 0c6692d8..00000000 --- a/test/fixtures/project/node_modules/eslint-config-custom/config.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "rules": { - "object-curly-spacing": ["error", "always"] - } -} diff --git a/test/fixtures/project/node_modules/eslint-config-custom/package.json b/test/fixtures/project/node_modules/eslint-config-custom/package.json deleted file mode 100644 index 0d6691be..00000000 --- a/test/fixtures/project/node_modules/eslint-config-custom/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "eslint-config-custom", - "main": "config.json" -} diff --git a/test/fixtures/project/package.json b/test/fixtures/project/package.json deleted file mode 100644 index 824188f1..00000000 --- a/test/fixtures/project/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "xo": { - "extends": ["custom"] - } -} diff --git a/test/fixtures/space/one-space.js b/test/fixtures/space/one-space.js deleted file mode 100644 index cf203662..00000000 --- a/test/fixtures/space/one-space.js +++ /dev/null @@ -1,3 +0,0 @@ -console.log([ - 1, -]); diff --git a/test/fixtures/space/two-spaces.js b/test/fixtures/space/two-spaces.js deleted file mode 100644 index 6b010f41..00000000 --- a/test/fixtures/space/two-spaces.js +++ /dev/null @@ -1,3 +0,0 @@ -console.log([ - 1, -]); diff --git a/test/fixtures/typescript/child/extra-semicolon.cts b/test/fixtures/typescript/child/extra-semicolon.cts deleted file mode 100644 index 786cdaa6..00000000 --- a/test/fixtures/typescript/child/extra-semicolon.cts +++ /dev/null @@ -1 +0,0 @@ -console.log('extra-semicolon');; diff --git a/test/fixtures/typescript/child/extra-semicolon.mts b/test/fixtures/typescript/child/extra-semicolon.mts deleted file mode 100644 index 786cdaa6..00000000 --- a/test/fixtures/typescript/child/extra-semicolon.mts +++ /dev/null @@ -1 +0,0 @@ -console.log('extra-semicolon');; diff --git a/test/fixtures/typescript/child/extra-semicolon.ts b/test/fixtures/typescript/child/extra-semicolon.ts deleted file mode 100644 index 786cdaa6..00000000 --- a/test/fixtures/typescript/child/extra-semicolon.ts +++ /dev/null @@ -1 +0,0 @@ -console.log('extra-semicolon');; diff --git a/test/fixtures/typescript/child/no-semicolon.ts b/test/fixtures/typescript/child/no-semicolon.ts deleted file mode 100644 index 1008a4fc..00000000 --- a/test/fixtures/typescript/child/no-semicolon.ts +++ /dev/null @@ -1 +0,0 @@ -console.log('no-semicolon') diff --git a/test/fixtures/typescript/child/package.json b/test/fixtures/typescript/child/package.json deleted file mode 100644 index 24ef9768..00000000 --- a/test/fixtures/typescript/child/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "xo": { - "semicolon": false - } -} diff --git a/test/fixtures/typescript/child/sub-child/four-spaces.ts b/test/fixtures/typescript/child/sub-child/four-spaces.ts deleted file mode 100644 index 9b5220b1..00000000 --- a/test/fixtures/typescript/child/sub-child/four-spaces.ts +++ /dev/null @@ -1,3 +0,0 @@ -console.log([ - 4, -]); diff --git a/test/fixtures/typescript/child/sub-child/package.json b/test/fixtures/typescript/child/sub-child/package.json deleted file mode 100644 index 494121dc..00000000 --- a/test/fixtures/typescript/child/sub-child/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "xo": { - "space": 2 - } -} diff --git a/test/fixtures/typescript/child/sub-child/tsconfig.json b/test/fixtures/typescript/child/sub-child/tsconfig.json deleted file mode 100644 index 142e0af1..00000000 --- a/test/fixtures/typescript/child/sub-child/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "compilerOptions": { - "strictNullChecks": true - } -} diff --git a/test/fixtures/typescript/child/tsconfig.json b/test/fixtures/typescript/child/tsconfig.json deleted file mode 100644 index b766c5d4..00000000 --- a/test/fixtures/typescript/child/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "compilerOptions": { - "strictNullChecks": true - }, - "include": [ - "**/*.ts", - "**/*.tsx" - ] -} diff --git a/test/fixtures/typescript/deep-extends/config/tsconfig.json b/test/fixtures/typescript/deep-extends/config/tsconfig.json deleted file mode 100644 index 4712350c..00000000 --- a/test/fixtures/typescript/deep-extends/config/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "include": ["../included-file.ts"], - "exclude": ["../excluded-file.ts"] -} diff --git a/test/fixtures/typescript/deep-extends/package.json b/test/fixtures/typescript/deep-extends/package.json deleted file mode 100644 index 90bd27c7..00000000 --- a/test/fixtures/typescript/deep-extends/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "xo": {} -} diff --git a/test/fixtures/typescript/excludes/package.json b/test/fixtures/typescript/excludes/package.json deleted file mode 100644 index 90bd27c7..00000000 --- a/test/fixtures/typescript/excludes/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "xo": {} -} diff --git a/test/fixtures/typescript/excludes/tsconfig.json b/test/fixtures/typescript/excludes/tsconfig.json deleted file mode 100644 index b3ec6cf5..00000000 --- a/test/fixtures/typescript/excludes/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "exclude": ["excluded-file.ts"] -} diff --git a/test/fixtures/typescript/extends-array/node_modules/@sindresorhus/tsconfig/package.json b/test/fixtures/typescript/extends-array/node_modules/@sindresorhus/tsconfig/package.json deleted file mode 100644 index 918d2924..00000000 --- a/test/fixtures/typescript/extends-array/node_modules/@sindresorhus/tsconfig/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "@sindresorhus/tsconfig", - "version": "3.0.1", - "description": "Shared TypeScript config for my projects", - "license": "MIT", - "repository": "sindresorhus/tsconfig", - "author": { - "name": "Sindre Sorhus", - "email": "sindresorhus@gmail.com", - "url": "https://sindresorhus.com" - }, - "main": "tsconfig.json", - "engines": { - "node": ">=14" - }, - "files": [ - "tsconfig.json" - ], - "keywords": [ - "tsconfig", - "typescript", - "ts", - "config", - "configuration" - ] -} diff --git a/test/fixtures/typescript/extends-array/node_modules/@sindresorhus/tsconfig/tsconfig.json b/test/fixtures/typescript/extends-array/node_modules/@sindresorhus/tsconfig/tsconfig.json deleted file mode 100644 index 0612e961..00000000 --- a/test/fixtures/typescript/extends-array/node_modules/@sindresorhus/tsconfig/tsconfig.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "compilerOptions": { - // Disabled because of https://github.com/Microsoft/TypeScript/issues/29172 - // "outDir": "dist", - - "module": "node16", - "moduleResolution": "node16", - "moduleDetection": "force", - "target": "ES2020", // Node.js 14 - "lib": [ - "DOM", - "DOM.Iterable", - "ES2020" - ], - "allowSyntheticDefaultImports": true, // To provide backwards compatibility, Node.js allows you to import most CommonJS packages with a default import. This flag tells TypeScript that it's okay to use import on CommonJS modules. - "resolveJsonModule": false, // ESM doesn't yet support JSON modules. - "jsx": "react", - "declaration": true, - "pretty": true, - "newLine": "lf", - "stripInternal": true, - "strict": true, - "noImplicitReturns": true, - "noImplicitOverride": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "noPropertyAccessFromIndexSignature": true, - "noEmitOnError": true, - "useDefineForClassFields": true, - "forceConsistentCasingInFileNames": true, - "skipLibCheck": true - } -} diff --git a/test/fixtures/typescript/extends-array/node_modules/@tsconfig/node16/package.json b/test/fixtures/typescript/extends-array/node_modules/@tsconfig/node16/package.json deleted file mode 100644 index c0e6dfa8..00000000 --- a/test/fixtures/typescript/extends-array/node_modules/@tsconfig/node16/package.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"@tsconfig/node16","repository":{"type":"git","url":"https://github.com/tsconfig/bases.git","directory":"bases"},"license":"MIT","description":"A base TSConfig for working with Node 16.","keywords":["tsconfig","node16"],"version":"1.0.3"} \ No newline at end of file diff --git a/test/fixtures/typescript/extends-array/node_modules/@tsconfig/node16/tsconfig.json b/test/fixtures/typescript/extends-array/node_modules/@tsconfig/node16/tsconfig.json deleted file mode 100644 index 262ff50b..00000000 --- a/test/fixtures/typescript/extends-array/node_modules/@tsconfig/node16/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "display": "Node 16", - - "compilerOptions": { - "lib": ["es2021"], - "module": "commonjs", - "target": "es2021", - - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "moduleResolution": "node" - } -} diff --git a/test/fixtures/typescript/extends-array/package.json b/test/fixtures/typescript/extends-array/package.json deleted file mode 100644 index 90bd27c7..00000000 --- a/test/fixtures/typescript/extends-array/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "xo": {} -} diff --git a/test/fixtures/typescript/extends-array/tsconfig.json b/test/fixtures/typescript/extends-array/tsconfig.json deleted file mode 100644 index a0ad755a..00000000 --- a/test/fixtures/typescript/extends-array/tsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": [ - "@sindresorhus/tsconfig", - "@tsconfig/node16" - ], -} diff --git a/test/fixtures/typescript/extends-config/package.json b/test/fixtures/typescript/extends-config/package.json deleted file mode 100644 index 90bd27c7..00000000 --- a/test/fixtures/typescript/extends-config/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "xo": {} -} diff --git a/test/fixtures/typescript/extends-config/tsconfig.json b/test/fixtures/typescript/extends-config/tsconfig.json deleted file mode 100644 index 3cff3fa9..00000000 --- a/test/fixtures/typescript/extends-config/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "include": ["foo.ts"] -} diff --git a/test/fixtures/typescript/extends-module/node_modules/@sindresorhus/tsconfig/package.json b/test/fixtures/typescript/extends-module/node_modules/@sindresorhus/tsconfig/package.json deleted file mode 100644 index 918d2924..00000000 --- a/test/fixtures/typescript/extends-module/node_modules/@sindresorhus/tsconfig/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "@sindresorhus/tsconfig", - "version": "3.0.1", - "description": "Shared TypeScript config for my projects", - "license": "MIT", - "repository": "sindresorhus/tsconfig", - "author": { - "name": "Sindre Sorhus", - "email": "sindresorhus@gmail.com", - "url": "https://sindresorhus.com" - }, - "main": "tsconfig.json", - "engines": { - "node": ">=14" - }, - "files": [ - "tsconfig.json" - ], - "keywords": [ - "tsconfig", - "typescript", - "ts", - "config", - "configuration" - ] -} diff --git a/test/fixtures/typescript/extends-module/node_modules/@sindresorhus/tsconfig/tsconfig.json b/test/fixtures/typescript/extends-module/node_modules/@sindresorhus/tsconfig/tsconfig.json deleted file mode 100644 index 0612e961..00000000 --- a/test/fixtures/typescript/extends-module/node_modules/@sindresorhus/tsconfig/tsconfig.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "compilerOptions": { - // Disabled because of https://github.com/Microsoft/TypeScript/issues/29172 - // "outDir": "dist", - - "module": "node16", - "moduleResolution": "node16", - "moduleDetection": "force", - "target": "ES2020", // Node.js 14 - "lib": [ - "DOM", - "DOM.Iterable", - "ES2020" - ], - "allowSyntheticDefaultImports": true, // To provide backwards compatibility, Node.js allows you to import most CommonJS packages with a default import. This flag tells TypeScript that it's okay to use import on CommonJS modules. - "resolveJsonModule": false, // ESM doesn't yet support JSON modules. - "jsx": "react", - "declaration": true, - "pretty": true, - "newLine": "lf", - "stripInternal": true, - "strict": true, - "noImplicitReturns": true, - "noImplicitOverride": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "noPropertyAccessFromIndexSignature": true, - "noEmitOnError": true, - "useDefineForClassFields": true, - "forceConsistentCasingInFileNames": true, - "skipLibCheck": true - } -} diff --git a/test/fixtures/typescript/extends-module/package.json b/test/fixtures/typescript/extends-module/package.json deleted file mode 100644 index 90bd27c7..00000000 --- a/test/fixtures/typescript/extends-module/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "xo": {} -} diff --git a/test/fixtures/typescript/extends-module/tsconfig.json b/test/fixtures/typescript/extends-module/tsconfig.json deleted file mode 100644 index e4928276..00000000 --- a/test/fixtures/typescript/extends-module/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "@sindresorhus/tsconfig" -} diff --git a/test/fixtures/typescript/extends-tsconfig-bases/node_modules/@tsconfig/node16/package.json b/test/fixtures/typescript/extends-tsconfig-bases/node_modules/@tsconfig/node16/package.json deleted file mode 100644 index c0e6dfa8..00000000 --- a/test/fixtures/typescript/extends-tsconfig-bases/node_modules/@tsconfig/node16/package.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"@tsconfig/node16","repository":{"type":"git","url":"https://github.com/tsconfig/bases.git","directory":"bases"},"license":"MIT","description":"A base TSConfig for working with Node 16.","keywords":["tsconfig","node16"],"version":"1.0.3"} \ No newline at end of file diff --git a/test/fixtures/typescript/extends-tsconfig-bases/node_modules/@tsconfig/node16/tsconfig.json b/test/fixtures/typescript/extends-tsconfig-bases/node_modules/@tsconfig/node16/tsconfig.json deleted file mode 100644 index 262ff50b..00000000 --- a/test/fixtures/typescript/extends-tsconfig-bases/node_modules/@tsconfig/node16/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "display": "Node 16", - - "compilerOptions": { - "lib": ["es2021"], - "module": "commonjs", - "target": "es2021", - - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "moduleResolution": "node" - } -} diff --git a/test/fixtures/typescript/extends-tsconfig-bases/package.json b/test/fixtures/typescript/extends-tsconfig-bases/package.json deleted file mode 100644 index 90bd27c7..00000000 --- a/test/fixtures/typescript/extends-tsconfig-bases/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "xo": {} -} diff --git a/test/fixtures/typescript/extends-tsconfig-bases/tsconfig.json b/test/fixtures/typescript/extends-tsconfig-bases/tsconfig.json deleted file mode 100644 index c8f95207..00000000 --- a/test/fixtures/typescript/extends-tsconfig-bases/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "@tsconfig/node16" -} diff --git a/test/fixtures/typescript/package.json b/test/fixtures/typescript/package.json deleted file mode 100644 index 296f1a7e..00000000 --- a/test/fixtures/typescript/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "xo": { - "space": 4 - } -} diff --git a/test/fixtures/typescript/parseroptions-project/package.json b/test/fixtures/typescript/parseroptions-project/package.json deleted file mode 100644 index 174a84ca..00000000 --- a/test/fixtures/typescript/parseroptions-project/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "xo": { - "parserOptions": { - "project": "./projectconfig.json" - } - } -} diff --git a/test/fixtures/typescript/parseroptions-project/projectconfig.json b/test/fixtures/typescript/parseroptions-project/projectconfig.json deleted file mode 100644 index ea6be8e9..00000000 --- a/test/fixtures/typescript/parseroptions-project/projectconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "./tsconfig.json" -} diff --git a/test/fixtures/typescript/parseroptions-project/tsconfig.json b/test/fixtures/typescript/parseroptions-project/tsconfig.json deleted file mode 100644 index c203c4be..00000000 --- a/test/fixtures/typescript/parseroptions-project/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "include": ["included-file.ts"], - "exclude": ["excluded-file.ts"] -} diff --git a/test/fixtures/typescript/relative-configs/config/tsconfig.json b/test/fixtures/typescript/relative-configs/config/tsconfig.json deleted file mode 100644 index 4712350c..00000000 --- a/test/fixtures/typescript/relative-configs/config/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "include": ["../included-file.ts"], - "exclude": ["../excluded-file.ts"] -} diff --git a/test/fixtures/typescript/relative-configs/package.json b/test/fixtures/typescript/relative-configs/package.json deleted file mode 100644 index da497d78..00000000 --- a/test/fixtures/typescript/relative-configs/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "xo": { - "parserOptions": { - "project": "./config/tsconfig.json" - } - } -} diff --git a/test/fixtures/typescript/two-spaces.tsx b/test/fixtures/typescript/two-spaces.tsx deleted file mode 100644 index 5984f43a..00000000 --- a/test/fixtures/typescript/two-spaces.tsx +++ /dev/null @@ -1,3 +0,0 @@ -console.log([ - 2, -]); diff --git a/test/fixtures/webpack/no-config/file1.js b/test/fixtures/webpack/no-config/file1.js deleted file mode 100644 index 87c3a28f..00000000 --- a/test/fixtures/webpack/no-config/file1.js +++ /dev/null @@ -1,2 +0,0 @@ -import __ from 'inexistent'; -import _ from 'file2alias'; diff --git a/test/fixtures/webpack/no-config/file2.js b/test/fixtures/webpack/no-config/file2.js deleted file mode 100644 index db3c91a4..00000000 --- a/test/fixtures/webpack/no-config/file2.js +++ /dev/null @@ -1,3 +0,0 @@ -const foo = 1; - -export default foo; diff --git a/test/fixtures/webpack/no-config/file3.js b/test/fixtures/webpack/no-config/file3.js deleted file mode 100644 index 97ec6943..00000000 --- a/test/fixtures/webpack/no-config/file3.js +++ /dev/null @@ -1 +0,0 @@ -import _ from '!./file2.js'; diff --git a/test/fixtures/webpack/with-config/file1.js b/test/fixtures/webpack/with-config/file1.js deleted file mode 100644 index 87c3a28f..00000000 --- a/test/fixtures/webpack/with-config/file1.js +++ /dev/null @@ -1,2 +0,0 @@ -import __ from 'inexistent'; -import _ from 'file2alias'; diff --git a/test/fixtures/webpack/with-config/file2.js b/test/fixtures/webpack/with-config/file2.js deleted file mode 100644 index db3c91a4..00000000 --- a/test/fixtures/webpack/with-config/file2.js +++ /dev/null @@ -1,3 +0,0 @@ -const foo = 1; - -export default foo; diff --git a/test/fixtures/webpack/with-config/webpack.config.js b/test/fixtures/webpack/with-config/webpack.config.js deleted file mode 100644 index 12e90935..00000000 --- a/test/fixtures/webpack/with-config/webpack.config.js +++ /dev/null @@ -1,9 +0,0 @@ -const path = require('path'); - -module.exports = { - resolve: { - alias: { - file2alias: path.resolve(__dirname, 'file2.js') - } - } -}; diff --git a/test/helpers/copy-test-project.ts b/test/helpers/copy-test-project.ts new file mode 100644 index 00000000..9781705d --- /dev/null +++ b/test/helpers/copy-test-project.ts @@ -0,0 +1,58 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import {randomUUID} from 'node:crypto'; +import tempDir from 'temp-dir'; +// import {$} from 'execa'; +import {pathExists} from 'path-exists'; +import {type XoConfigItem} from '../../lib/types.js'; + +/** + * Copies the test project in the temp dir to a new directory. + * @returns {string} The path to the copied test project. + */ +export const copyTestProject = async () => { + if (!(await pathExists(tempDir))) { + throw new Error('temp-dir/test-project does not exist'); + } + + const testCwd = path.join(tempDir, 'test-project'); + const newCwd = path.join(tempDir, randomUUID()); + + await fs.cp(testCwd, newCwd, {recursive: true}); + + // create a tsconfig.json file + await fs.writeFile( + path.join(newCwd, 'tsconfig.json'), + JSON.stringify({ + compilerOptions: { + module: 'node16', + target: 'ES2022', + strictNullChecks: true, + lib: ['DOM', 'DOM.Iterable', 'ES2022'], + }, + files: [path.join(newCwd, 'test.ts')], + exclude: ['node_modules'], + }), + ); + return newCwd; +}; + +/** + * Adds a flag to the xo.config.js file in the test project in the temp dir. + * Cleans up any previous xo.config.js file. + * + * @param cwd - the test project directory + * @param config - contents of a xo.config.js file as a string + */ +export const addFlatConfigToProject = async ( + cwd: string, + config: XoConfigItem[], +) => { + const filePath = path.join(cwd, 'xo.config.js'); + + if (await pathExists(filePath)) { + await fs.rm(filePath, {force: true}); + } + + await fs.writeFile(filePath, `export default ${JSON.stringify(config)};`); +}; diff --git a/test/helpers/get-rule.ts b/test/helpers/get-rule.ts new file mode 100644 index 00000000..d9b79afb --- /dev/null +++ b/test/helpers/get-rule.ts @@ -0,0 +1,48 @@ +/* eslint-disable @stylistic/indent-binary-ops */ +import {type Linter} from 'eslint'; +import { + ALL_FILES_GLOB, + JS_FILES_GLOB, + TS_FILES_GLOB, +} from '../../lib/constants.js'; + +/** + * find the rule applied to js files + * + * @param flatConfig + * @param ruleId + */ +export const getJsRule = (flatConfig: Linter.Config[], ruleId: string) => { + const config = [...flatConfig].reverse().find(config => + (typeof config !== 'string' + && config?.rules?.[ruleId] + && config.files?.includes(ALL_FILES_GLOB)) + ?? config.files?.includes(JS_FILES_GLOB)); + + if (typeof config === 'string') { + return undefined; + } + + return config?.rules?.[ruleId]; +}; + +/** + * find the rule applied to ts files + * + * @param flatConfig + * @param ruleId + */ +export const getTsRule = (flatConfig: Linter.Config[], ruleId: string) => { + const config = [...flatConfig] + .reverse() + .find(config => + typeof config !== 'string' + && config?.rules?.[ruleId] + && config.files?.includes(TS_FILES_GLOB)); + + if (typeof config === 'string') { + return undefined; + } + + return config?.rules?.[ruleId]; +}; diff --git a/test/ignores.js b/test/ignores.js deleted file mode 100644 index d6f98bb9..00000000 --- a/test/ignores.js +++ /dev/null @@ -1,40 +0,0 @@ -import path from 'node:path'; -import test from 'ava'; -import createEsmUtils from 'esm-utils'; -import {globby} from 'globby'; -import xo from '../index.js'; - -const {__dirname} = createEsmUtils(import.meta); - -test('Should pickup "ignores" config', async t => { - const cwd = path.join(__dirname, 'fixtures/nested-ignores'); - - t.deepEqual( - await globby(['**/*.js'], {cwd}), - ['a.js', 'b.js', 'child/a.js', 'child/b.js'], - 'Should has 4 js files.', - ); - - // Should not match - // `a.js` (ignored by config in current directory) - // `child/a.js` (ignored by config in current directory) - // `child/b.js` (ignored by config in child directory) - const result = await xo.lintFiles('.', {cwd}); - const files = result.results.map(({filePath}) => filePath); - t.deepEqual(files, [path.join(cwd, 'b.js')], 'Should only report on `b.js`.'); -}); - -test('Should ignore "ignores" config in parent', async t => { - const cwd = path.join(__dirname, 'fixtures/nested-ignores/child'); - - t.deepEqual( - await globby(['**/*.js'], {cwd}), - ['a.js', 'b.js'], - 'Should has 2 js files.', - ); - - // Should only match `a.js` even it's ignored in parent - const result = await xo.lintFiles('.', {cwd}); - const files = result.results.map(({filePath}) => filePath); - t.deepEqual(files, [path.join(cwd, 'a.js')], 'Should only report on `a.js`.'); -}); diff --git a/test/lint-files.js b/test/lint-files.js deleted file mode 100644 index df3087d9..00000000 --- a/test/lint-files.js +++ /dev/null @@ -1,357 +0,0 @@ -import process from 'node:process'; -import path from 'node:path'; -import test from 'ava'; -import createEsmUtils from 'esm-utils'; -import xo from '../index.js'; - -const {__dirname} = createEsmUtils(import.meta); -process.chdir(__dirname); - -const hasRule = (results, filePath, ruleId, rulesMeta) => { - const result = results.find(x => x.filePath === filePath); - const hasRuleInResults = result ? result.messages.some(x => x.ruleId === ruleId) : false; - const hasRuleInResultsMeta = rulesMeta ? typeof rulesMeta[ruleId] === 'object' : true; - return hasRuleInResults && hasRuleInResultsMeta; -}; - -test('only accepts allowed extensions', async t => { - // Markdown files will always produce linter errors and will not be going away - const cwd = path.join(__dirname, 'fixtures/custom-extension'); - const mdGlob = '*.md'; - - // No files should be linted = no errors - const noOptionsResults = await xo.lintFiles(mdGlob, {cwd}); - t.is(noOptionsResults.errorCount, 0); - - // Markdown files linted (with no plugin for it) = errors - const moreExtensionsResults = await xo.lintFiles(mdGlob, {extensions: ['md'], cwd}); - t.true(moreExtensionsResults.errorCount > 0); -}); - -test('ignores dirs for empty extensions', async t => { - { - const cwd = path.join(__dirname, 'fixtures/nodir'); - const glob = '*'; - const results = await xo.lintFiles(glob, {extensions: ['', 'js'], cwd}); - const {results: [fileResult]} = results; - - // Only `fixtures/nodir/noextension` should be linted - const expected = 'fixtures/nodir/noextension'.split('/').join(path.sep); - const actual = path.relative(__dirname, fileResult.filePath); - t.is(actual, expected); - t.is(results.errorCount, 1); - } - - { - const cwd = path.join(__dirname, 'fixtures/nodir'); - const glob = 'nested/*'; - const results = await xo.lintFiles(glob, {cwd}); - const {results: [fileResult]} = results; - - // Ensure `nodir/nested` **would** report if globbed - const expected = 'fixtures/nodir/nested/index.js'.split('/').join(path.sep); - const actual = path.relative(__dirname, fileResult.filePath); - t.is(actual, expected); - t.is(results.errorCount, 1); - } - - { - const cwd = path.join(__dirname, 'fixtures/nodir'); - // Check Windows-style paths are working - const glob = 'nested\\*'; - const results = await xo.lintFiles(glob, {cwd}); - const {results: [fileResult]} = results; - - const expected = 'fixtures/nodir/nested/index.js'.split('/').join(path.sep); - const actual = path.relative(__dirname, fileResult.filePath); - t.is(actual, expected); - t.is(results.errorCount, 1); - } -}); - -test.serial('cwd option', async t => { - const {results} = await xo.lintFiles('**/*', {cwd: 'fixtures/cwd'}); - const paths = results.map(r => path.relative(__dirname, r.filePath)); - paths.sort(); - t.deepEqual(paths, [path.join('fixtures', 'cwd', 'unicorn.js')]); -}); - -test('do not lint gitignored files', async t => { - const cwd = path.join(__dirname, 'fixtures/gitignore'); - const glob = '**/*'; - const ignored = path.resolve('fixtures/gitignore/test/foo.js'); - const {results} = await xo.lintFiles(glob, {cwd}); - - t.is(results.some(r => r.filePath === ignored), false); -}); - -test('do not lint gitignored files in file with negative gitignores', async t => { - const cwd = path.join(__dirname, 'fixtures/negative-gitignore'); - const glob = '*'; - const ignored = path.resolve('fixtures/negative-gitignore/bar.js'); - const {results} = await xo.lintFiles(glob, {cwd}); - - t.is(results.some(r => r.filePath === ignored), false); -}); - -test('lint negatively gitignored files', async t => { - const cwd = path.join(__dirname, 'fixtures/negative-gitignore'); - const glob = '*'; - const negative = path.resolve('fixtures/negative-gitignore/foo.js'); - const {results} = await xo.lintFiles(glob, {cwd}); - - t.is(results.some(r => r.filePath === negative), true); -}); - -test('do not lint inapplicable negatively gitignored files', async t => { - const cwd = path.join(__dirname, 'fixtures/negative-gitignore'); - const glob = 'bar.js'; - const negative = path.resolve('fixtures/negative-gitignore/foo.js'); - const {results} = await xo.lintFiles(glob, {cwd}); - - t.is(results.some(r => r.filePath === negative), false); -}); - -test('multiple negative patterns should act as positive patterns', async t => { - const cwd = path.join(__dirname, 'fixtures', 'gitignore-multiple-negation'); - const {results} = await xo.lintFiles('**/*', {cwd}); - const paths = results.map(r => path.basename(r.filePath)); - paths.sort(); - - t.deepEqual(paths, ['!!unicorn.js', '!unicorn.js']); -}); - -test('enable rules based on nodeVersion', async t => { - const {results, rulesMeta} = await xo.lintFiles('**/*', {cwd: 'fixtures/engines-overrides'}); - - // The transpiled file (as specified in `overrides`) should use `await` - t.true( - hasRule( - results, - path.resolve('fixtures/engines-overrides/promise-then-transpile.js'), - 'promise/prefer-await-to-then', - rulesMeta, - ), - ); - // The non transpiled files can use `.then` - t.false( - hasRule( - results, - path.resolve('fixtures/engines-overrides/promise-then.js'), - 'promise/prefer-await-to-then', - rulesMeta, - ), - ); -}); - -test('do not lint eslintignored files', async t => { - const cwd = path.join(__dirname, 'fixtures/eslintignore'); - const glob = '*'; - const positive = path.resolve('fixtures/eslintignore/foo.js'); - const negative = path.resolve('fixtures/eslintignore/bar.js'); - const {results} = await xo.lintFiles(glob, {cwd}); - - t.is(results.some(r => r.filePath === positive), true); - t.is(results.some(r => r.filePath === negative), false); -}); - -test('find configurations close to linted file', async t => { - const {results, rulesMeta} = await xo.lintFiles('**/*', {cwd: 'fixtures/nested-configs'}); - - t.true( - hasRule( - results, - path.resolve('fixtures/nested-configs/child/semicolon.js'), - 'semi', - rulesMeta, - ), - ); - - t.true( - hasRule( - results, - path.resolve('fixtures/nested-configs/child-override/child-prettier-override/semicolon.js'), - 'prettier/prettier', - rulesMeta, - ), - ); - - t.true( - hasRule( - results, - path.resolve('fixtures/nested-configs/no-semicolon.js'), - 'semi', - rulesMeta, - ), - ); - - t.true( - hasRule( - results, - path.resolve('fixtures/nested-configs/child-override/two-spaces.js'), - 'indent', - rulesMeta, - ), - ); -}); - -test.serial('typescript files', async t => { - const {results, rulesMeta} = await xo.lintFiles('**/*', {cwd: 'fixtures/typescript'}); - - t.true( - hasRule( - results, - path.resolve('fixtures/typescript/two-spaces.tsx'), - '@typescript-eslint/indent', - rulesMeta, - ), - ); - - t.true( - hasRule( - results, - path.resolve('fixtures/typescript/child/extra-semicolon.ts'), - '@typescript-eslint/no-extra-semi', - rulesMeta, - ), - ); - - t.true( - hasRule( - results, - path.resolve('fixtures/typescript/child/extra-semicolon.mts'), - '@typescript-eslint/no-extra-semi', - rulesMeta, - ), - ); - - t.true( - hasRule( - results, - path.resolve('fixtures/typescript/child/extra-semicolon.cts'), - '@typescript-eslint/no-extra-semi', - rulesMeta, - ), - ); - - t.true( - hasRule( - results, - path.resolve('fixtures/typescript/child/sub-child/four-spaces.ts'), - '@typescript-eslint/indent', - rulesMeta, - ), - ); -}); - -test.serial('typescript 2 space option', async t => { - const {errorCount, results} = await xo.lintFiles('two-spaces.tsx', {cwd: 'fixtures/typescript', space: 2}); - // eslint-disable-next-line ava/assertion-arguments -- Type issue - t.is(errorCount, 0, JSON.stringify(results[0].messages)); -}); - -test.serial('typescript 4 space option', async t => { - const {errorCount, results} = await xo.lintFiles('child/sub-child/four-spaces.ts', {cwd: 'fixtures/typescript', space: 4}); - // eslint-disable-next-line ava/assertion-arguments -- Type issue - t.is(errorCount, 0, JSON.stringify(results[0].messages)); -}); - -test.serial('typescript no semicolon option', async t => { - const {errorCount, results} = await xo.lintFiles('child/no-semicolon.ts', {cwd: 'fixtures/typescript', semicolon: false}); - // eslint-disable-next-line ava/assertion-arguments -- Type issue - t.is(errorCount, 0, JSON.stringify(results[0].messages)); -}); - -test('webpack import resolver is used if webpack.config.js is found', async t => { - const cwd = 'fixtures/webpack/with-config/'; - const {results} = await xo.lintFiles('file1.js', { - cwd, - rules: { - 'import/no-unresolved': 2, - }, - }); - - // eslint-disable-next-line ava/assertion-arguments -- Type issue - t.is(results[0].errorCount, 1, JSON.stringify(results[0].messages)); - - const errorMessage = results[0].messages[0].message; - t.truthy(/Unable to resolve path to module 'inexistent'/.exec(errorMessage)); -}); - -test('webpack import resolver config can be passed through webpack option', async t => { - const cwd = 'fixtures/webpack/no-config/'; - - const {results} = await xo.lintFiles('file1.js', { - cwd, - webpack: { - config: { - resolve: { - alias: { - file2alias: path.resolve(__dirname, cwd, './file2.js'), - }, - }, - }, - }, - rules: { - 'import/no-unresolved': 2, - }, - }); - - // eslint-disable-next-line ava/assertion-arguments -- Type issue - t.is(results[0].errorCount, 1, JSON.stringify(results[0].messages)); -}); - -test('webpack import resolver is used if {webpack: true}', async t => { - const cwd = 'fixtures/webpack/no-config/'; - - const {results} = await xo.lintFiles('file3.js', { - cwd, - webpack: true, - rules: { - 'import/no-unresolved': 2, - 'import/no-webpack-loader-syntax': 0, - }, - }); - - // eslint-disable-next-line ava/assertion-arguments -- Type issue - t.is(results[0].errorCount, 0, JSON.stringify(results[0])); -}); - -async function configType(t, {dir}) { - const {results, rulesMeta} = await xo.lintFiles('**/*', {cwd: path.resolve('fixtures', 'config-files', dir)}); - - t.true( - hasRule( - results, - path.resolve('fixtures', 'config-files', dir, 'file.js'), - 'indent', - rulesMeta, - ), - ); -} - -configType.title = (_, {type}) => `load config from ${type}`.trim(); - -test(configType, {type: 'xo.config.js', dir: 'xo-config_js'}); -test(configType, {type: 'xo.config.cjs', dir: 'xo-config_cjs'}); -test(configType, {type: '.xo-config.js', dir: 'xo-config_js'}); -test(configType, {type: '.xo-config.cjs', dir: 'xo-config_cjs'}); -test(configType, {type: '.xo-config.json', dir: 'xo-config_json'}); -test(configType, {type: '.xo-config', dir: 'xo-config'}); - -test('load config file with relative extends from different cwd', async t => { - const directory = 'extends-relative'; - - const {results, rulesMeta} = await xo.lintFiles('**/*', { - cwd: path.resolve('fixtures', 'config-files', directory), - }); - - t.true( - hasRule( - results, - path.resolve('fixtures', 'config-files', directory, 'file.js'), - 'comma-dangle', - rulesMeta, - ), - ); -}); diff --git a/test/lint-files/config.test.ts b/test/lint-files/config.test.ts new file mode 100644 index 00000000..e85eb25e --- /dev/null +++ b/test/lint-files/config.test.ts @@ -0,0 +1,154 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import _test, {type TestFn} from 'ava'; // eslint-disable-line ava/use-test +import dedent from 'dedent'; +import {XO} from '../../lib/xo.js'; +import {copyTestProject} from '../helpers/copy-test-project.js'; + +const test = _test as TestFn<{cwd: string}>; + +test.beforeEach(async t => { + t.context.cwd = await copyTestProject(); +}); + +test.afterEach.always(async t => { + await fs.rm(t.context.cwd, {recursive: true, force: true}); +}); + +test('no config > js > semi', async t => { + const filePath = path.join(t.context.cwd, 'test.js'); + await fs.writeFile(filePath, dedent`console.log('hello')\n`, 'utf8'); + const {results} = await new XO({cwd: t.context.cwd}).lintFiles('**/*'); + t.is(results?.[0]?.messages.length, 1); + t.is(results?.[0]?.messages?.[0]?.ruleId, '@stylistic/semi'); +}); + +test('no config > ts > semi', async t => { + const filePath = path.join(t.context.cwd, 'test.ts'); + await fs.writeFile(filePath, dedent`console.log('hello')\n`, 'utf8'); + const {results} = await new XO({cwd: t.context.cwd}).lintFiles('**/*'); + t.is(results?.[0]?.messages?.length, 1); + t.is(results?.[0]?.messages?.[0]?.ruleId, '@stylistic/semi'); +}); + +test('flat config > semi > js', async t => { + const filePath = path.join(t.context.cwd, 'test.js'); + await fs.writeFile( + path.join(t.context.cwd, 'xo.config.js'), + dedent` + export default [ + { + semicolon: false + } + ]\n + `, + 'utf8', + ); + await fs.writeFile(filePath, dedent`console.log('hello');\n`, 'utf8'); + const xo = new XO({cwd: t.context.cwd}); + const {results} = await xo.lintFiles(); + t.is(results?.[0]?.messages?.length, 1); + t.is(results?.[0]?.messages?.[0]?.ruleId, '@stylistic/semi'); +}); + +test('typescript file with flat config - semicolon', async t => { + const filePath = path.join(t.context.cwd, 'test.ts'); + await fs.writeFile( + path.join(t.context.cwd, 'xo.config.js'), + dedent` + export default [ + { + semicolon: false + } + ];\n + `, + 'utf8', + ); + await fs.writeFile(filePath, dedent`console.log('hello');\n`, 'utf8'); + const xo = new XO({cwd: t.context.cwd}); + const {results} = await xo.lintFiles(); + t.is(results?.[0]?.messages?.length, 1); + t.is(results?.[0]?.messages?.[0]?.ruleId, '@stylistic/semi'); +}); + +test('typescript file with no tsconfig - semicolon', async t => { + const filePath = path.join(t.context.cwd, 'test.ts'); + await fs.rm(path.join(t.context.cwd, 'tsconfig.json')); + await fs.writeFile( + path.join(t.context.cwd, 'xo.config.js'), + dedent` + export default [ + { + semicolon: false + } + ];\n + `, + 'utf8', + ); + await fs.writeFile(filePath, dedent`console.log('hello');\n`, 'utf8'); + const xo = new XO({cwd: t.context.cwd, ts: true}); + const {results} = await xo.lintFiles(); + t.is(results?.[0]?.messages?.length, 1); + t.is(results?.[0]?.messages?.[0]?.ruleId, '@stylistic/semi'); +}); + +test('flat config > space > js', async t => { + const filePath = path.join(t.context.cwd, 'test.js'); + + await fs.writeFile( + path.join(t.context.cwd, 'xo.config.js'), + dedent` + export default [ + { + space: true + } + ];\n + `, + 'utf8', + ); + + const xo = new XO({cwd: t.context.cwd}); + await fs.writeFile( + filePath, + + dedent` + export function foo() { + console.log('hello'); + }\n + `, + ); + const {results} = await xo.lintFiles(); + t.is(results?.[0]?.messages.length, 1); + t.is(results?.[0]?.messages?.[0]?.messageId, 'wrongIndentation'); + t.is(results?.[0]?.messages?.[0]?.ruleId, '@stylistic/indent'); +}); + +test('flat config > space > ts', async t => { + const filePath = path.join(t.context.cwd, 'test.ts'); + + await fs.writeFile( + path.join(t.context.cwd, 'xo.config.js'), + dedent` + export default [ + { + space: true + } + ];\n + `, + 'utf8', + ); + + const xo = new XO({cwd: t.context.cwd}); + await fs.writeFile( + filePath, + dedent` + export function foo() { + console.log('hello'); + }\n + `, + ); + const {results} = await xo.lintFiles(); + t.is(results?.[0]?.messages.length, 1); + t.is(results?.[0]?.messages?.[0]?.messageId, 'wrongIndentation'); + t.is(results?.[0]?.messages?.[0]?.ruleId, '@stylistic/indent'); +}); diff --git a/test/lint-text.js b/test/lint-text.js deleted file mode 100644 index a6de9421..00000000 --- a/test/lint-text.js +++ /dev/null @@ -1,374 +0,0 @@ -import process from 'node:process'; -import {promises as fs} from 'node:fs'; -import path from 'node:path'; -import test from 'ava'; -import createEsmUtils from 'esm-utils'; -import xo from '../index.js'; - -const {__dirname} = createEsmUtils(import.meta); -process.chdir(__dirname); - -const hasRule = (results, expectedRuleId, rulesMeta) => { - const hasRuleInResults = results[0].messages.some(({ruleId}) => ruleId === expectedRuleId); - const hasRuleInMeta = rulesMeta ? typeof rulesMeta[expectedRuleId] === 'object' : true; - return hasRuleInResults && hasRuleInMeta; -}; - -test('.lintText()', async t => { - const {results, rulesMeta} = await xo.lintText('\'use strict\'\nconsole.log(\'unicorn\');\n'); - t.true(hasRule(results, 'semi', rulesMeta)); -}); - -test('default `ignores`', async t => { - const result = await xo.lintText('\'use strict\'\nconsole.log(\'unicorn\');\n', { - filePath: 'node_modules/ignored/index.js', - }); - t.is(result.errorCount, 0); - t.is(result.warningCount, 0); -}); - -test('`ignores` option', async t => { - const result = await xo.lintText('\'use strict\'\nconsole.log(\'unicorn\');\n', { - filePath: 'ignored/index.js', - ignores: ['ignored/**/*.js'], - }); - t.is(result.errorCount, 0); - t.is(result.warningCount, 0); -}); - -test('`ignores` option without cwd', async t => { - const result = await xo.lintText('\'use strict\'\nconsole.log(\'unicorn\');\n', { - filePath: 'ignored/index.js', - ignores: ['ignored/**/*.js'], - }); - t.is(result.errorCount, 0); - t.is(result.warningCount, 0); -}); - -test('respect overrides', async t => { - const result = await xo.lintText('\'use strict\'\nconsole.log(\'unicorn\');\n', { - filePath: 'ignored/index.js', - ignores: ['ignored/**/*.js'], - overrides: [ - { - files: ['ignored/**/*.js'], - ignores: [], - }, - ], - rules: { - 'unicorn/prefer-module': 'off', - 'unicorn/prefer-node-protocol': 'off', - }, - }); - t.is(result.errorCount, 1); - t.is(result.warningCount, 0); -}); - -test('overriden ignore', async t => { - const result = await xo.lintText('\'use strict\'\nconsole.log(\'unicorn\');\n', { - filePath: 'unignored.js', - overrides: [ - { - files: ['unignored.js'], - ignores: ['unignored.js'], - }, - ], - }); - t.is(result.errorCount, 0); - t.is(result.warningCount, 0); -}); - -test('`ignores` option without filename', async t => { - await t.throwsAsync(async () => { - await xo.lintText('\'use strict\'\nconsole.log(\'unicorn\');\n', { - ignores: ['ignored/**/*.js'], - }); - }, {message: /The `ignores` option requires the `filePath` option to be defined./u}); -}); - -test('JSX support', async t => { - const {results, rulesMeta} = await xo.lintText('const app =
Hello, React!
;\n'); - t.true(hasRule(results, 'no-unused-vars', rulesMeta)); -}); - -test('plugin support', async t => { - const {results, rulesMeta} = await xo.lintText('var React;\nReact.render();\n', { - plugins: ['react'], - rules: {'react/jsx-no-undef': 'error'}, - }); - t.true(hasRule(results, 'react/jsx-no-undef', rulesMeta)); -}); - -test('prevent use of extended native objects', async t => { - const {results, rulesMeta} = await xo.lintText('[].unicorn();\n'); - t.true(hasRule(results, 'no-use-extend-native/no-use-extend-native', rulesMeta)); -}); - -test('extends support', async t => { - const {results, rulesMeta} = await xo.lintText('var React;\nReact.render();\n', { - extends: 'xo-react', - }); - t.true(hasRule(results, 'react/jsx-no-undef', rulesMeta)); -}); - -test('disable style rules when `prettier` option is enabled', async t => { - const {results: withoutPrettier, rulesMeta} = await xo.lintText('(a) => {}\n', {filePath: 'test.js'}); - // `arrow-parens` is enabled - t.true(hasRule(withoutPrettier, 'arrow-parens', rulesMeta)); - // `prettier/prettier` is disabled - t.false(hasRule(withoutPrettier, 'prettier/prettier', rulesMeta)); - - const {results: withPrettier} = await xo.lintText('(a) => {}\n', {prettier: true, filePath: 'test.js'}); - // `arrow-parens` is disabled by `eslint-config-prettier` - t.false(hasRule(withPrettier, 'arrow-parens', rulesMeta)); - // `prettier/prettier` is enabled - this is a special case for rulesMeta - so we remove it from this test - t.true(hasRule(withPrettier, 'prettier/prettier')); -}); - -test('extends `react` support with `prettier` option', async t => { - const {results, rulesMeta} = await xo.lintText(';\n', {extends: 'xo-react', prettier: true, filePath: 'test.jsx'}); - // `react/jsx-curly-spacing` is disabled by `eslint-config-prettier` - t.false(hasRule(results, 'react/jsx-curly-spacing', rulesMeta)); - // `prettier/prettier` is enabled - t.true(hasRule(results, 'prettier/prettier', rulesMeta)); -}); - -test('regression test for #71', async t => { - const {results} = await xo.lintText('const foo = { key: \'value\' };\nconsole.log(foo);\n', { - extends: './fixtures/extends.js', - }); - t.is(results[0].errorCount, 0); -}); - -test('lintText() - overrides support', async t => { - const cwd = path.join(__dirname, 'fixtures/overrides'); - const bar = path.join(cwd, 'test/bar.js'); - const {results: barResults} = await xo.lintText(await fs.readFile(bar, 'utf8'), {filePath: bar, cwd}); - t.is(barResults[0].errorCount, 0); - - const foo = path.join(cwd, 'test/foo.js'); - const {results: fooResults} = await xo.lintText(await fs.readFile(foo, 'utf8'), {filePath: foo, cwd}); - t.is(fooResults[0].errorCount, 0); - - const index = path.join(cwd, 'test/index.js'); - const {results: indexResults} = await xo.lintText(await fs.readFile(bar, 'utf8'), {filePath: index, cwd}); - t.is(indexResults[0].errorCount, 0); -}); - -test('do not lint gitignored files if filename is given', async t => { - const cwd = path.join(__dirname, 'fixtures/gitignore'); - const ignoredPath = path.resolve('fixtures/gitignore/test/foo.js'); - const ignored = await fs.readFile(ignoredPath, 'utf8'); - const {results} = await xo.lintText(ignored, {filePath: ignoredPath, cwd}); - t.is(results[0].errorCount, 0); -}); - -test('lint gitignored files if filename is not given', async t => { - const ignoredPath = path.resolve('fixtures/gitignore/test/foo.js'); - const ignored = await fs.readFile(ignoredPath, 'utf8'); - const {results} = await xo.lintText(ignored); - t.true(results[0].errorCount > 0); -}); - -test('do not lint gitignored files in file with negative gitignores', async t => { - const cwd = path.join(__dirname, 'fixtures/negative-gitignore'); - const ignoredPath = path.resolve('fixtures/negative-gitignore/bar.js'); - const ignored = await fs.readFile(ignoredPath, 'utf8'); - const {results} = await xo.lintText(ignored, {filePath: ignoredPath, cwd}); - t.is(results[0].errorCount, 0); -}); - -test('multiple negative patterns should act as positive patterns', async t => { - const cwd = path.join(__dirname, 'fixtures', 'gitignore-multiple-negation'); - const filePath = path.join(cwd, '!!!unicorn.js'); - const text = await fs.readFile(filePath, 'utf8'); - const {results} = await xo.lintText(text, {filePath, cwd}); - t.is(results[0].errorCount, 0); -}); - -test('lint negatively gitignored files', async t => { - const cwd = path.join(__dirname, 'fixtures/negative-gitignore'); - const glob = path.posix.join(cwd, '*'); - const {results} = await xo.lintFiles(glob, {cwd}); - - t.true(results[0].errorCount > 0); -}); - -test('do not lint eslintignored files if filename is given', async t => { - const cwd = path.join(__dirname, 'fixtures/eslintignore'); - const ignoredPath = path.resolve('fixtures/eslintignore/bar.js'); - const ignored = await fs.readFile(ignoredPath, 'utf8'); - const {results} = await xo.lintText(ignored, {filePath: ignoredPath, cwd}); - t.is(results[0].errorCount, 0); -}); - -test('lint eslintignored files if filename is not given', async t => { - const ignoredPath = path.resolve('fixtures/eslintignore/bar.js'); - const ignored = await fs.readFile(ignoredPath, 'utf8'); - const {results} = await xo.lintText(ignored); - t.true(results[0].errorCount > 0); -}); - -test('enable rules based on nodeVersion', async t => { - const cwd = path.join(__dirname, 'fixtures', 'engines-overrides'); - const filePath = path.join(cwd, 'promise-then.js'); - const text = await fs.readFile(filePath, 'utf8'); - - let {results, rulesMeta} = await xo.lintText(text, {nodeVersion: '>=8.0.0'}); - t.true(hasRule(results, 'promise/prefer-await-to-then', rulesMeta)); - - ({results, rulesMeta} = await xo.lintText(text, {nodeVersion: '>=6.0.0'})); - t.false(hasRule(results, 'promise/prefer-await-to-then', rulesMeta)); -}); - -test('enable rules based on nodeVersion in override', async t => { - const cwd = path.join(__dirname, 'fixtures', 'engines-overrides'); - const filePath = path.join(cwd, 'promise-then.js'); - const text = await fs.readFile(filePath, 'utf8'); - - let {results, rulesMeta} = await xo.lintText(text, { - nodeVersion: '>=8.0.0', - filePath: 'promise-then.js', - overrides: [ - { - files: 'promise-*.js', - nodeVersion: '>=6.0.0', - }, - ], - }); - t.false(hasRule(results, 'promise/prefer-await-to-then', rulesMeta)); - - ({results, rulesMeta} = await xo.lintText(text, { - nodeVersion: '>=6.0.0', - filePath: 'promise-then.js', - overrides: [ - { - files: 'promise-*.js', - nodeVersion: '>=8.0.0', - }, - ], - })); - t.true(hasRule(results, 'promise/prefer-await-to-then', rulesMeta)); -}); - -test('allow unassigned stylesheet imports', async t => { - let {results, rulesMeta} = await xo.lintText('import \'stylesheet.css\''); - t.false(hasRule(results, 'import/no-unassigned-import', rulesMeta)); - - ({results, rulesMeta} = await xo.lintText('import \'stylesheet.scss\'')); - t.false(hasRule(results, 'import/no-unassigned-import', rulesMeta)); - - ({results, rulesMeta} = await xo.lintText('import \'stylesheet.sass\'')); - t.false(hasRule(results, 'import/no-unassigned-import', rulesMeta)); - - ({results, rulesMeta} = await xo.lintText('import \'stylesheet.less\'')); - t.false(hasRule(results, 'import/no-unassigned-import', rulesMeta)); -}); - -test('find configurations close to linted file', async t => { - let {results, rulesMeta} = await xo.lintText('console.log(\'semicolon\');\n', {filePath: 'fixtures/nested-configs/child/semicolon.js'}); - t.true(hasRule(results, 'semi', rulesMeta)); - - ({results, rulesMeta} = await xo.lintText('console.log(\'semicolon\');\n', {filePath: 'fixtures/nested-configs/child-override/child-prettier-override/semicolon.js'})); - t.true(hasRule(results, 'prettier/prettier', rulesMeta)); - - ({results, rulesMeta} = await xo.lintText('console.log(\'no-semicolon\')\n', {filePath: 'fixtures/nested-configs/no-semicolon.js'})); - t.true(hasRule(results, 'semi', rulesMeta)); - - ({results, rulesMeta} = await xo.lintText(`console.log([ - 2 -]);\n`, {filePath: 'fixtures/nested-configs/child-override/two-spaces.js'})); - t.true(hasRule(results, 'indent', rulesMeta)); -}); - -test('rulesMeta is added to the report by default', async t => { - const report = await xo.lintText('\'use strict\'\nconsole.log(\'unicorn\');\n'); - t.is(typeof report.rulesMeta, 'object'); -}); - -test('typescript files: two spaces fails', async t => { - const twoSpacesCwd = path.resolve('fixtures', 'typescript'); - const twoSpacesfilePath = path.resolve(twoSpacesCwd, 'two-spaces.tsx'); - const twoSpacesText = await fs.readFile(twoSpacesfilePath, 'utf8'); - const {results, rulesMeta} = await xo.lintText(twoSpacesText, { - filePath: twoSpacesfilePath, - }); - t.true(hasRule(results, '@typescript-eslint/indent', rulesMeta)); -}); - -test('typescript files: two spaces pass', async t => { - const twoSpacesCwd = path.resolve('fixtures', 'typescript'); - const twoSpacesfilePath = path.resolve(twoSpacesCwd, 'two-spaces.tsx'); - const twoSpacesText = await fs.readFile(twoSpacesfilePath, 'utf8'); - const {results} = await xo.lintText(twoSpacesText, { - filePath: twoSpacesfilePath, - space: 2, - }); - t.is(results[0].errorCount, 0); -}); - -test('typescript files: extra semi fail', async t => { - const extraSemiCwd = path.resolve('fixtures', 'typescript', 'child'); - const extraSemiFilePath = path.resolve(extraSemiCwd, 'extra-semicolon.ts'); - const extraSemiText = await fs.readFile(extraSemiFilePath, 'utf8'); - const {results, rulesMeta} = await xo.lintText(extraSemiText, { - filePath: extraSemiFilePath, - }); - t.true(hasRule(results, '@typescript-eslint/no-extra-semi', rulesMeta)); -}); - -test('typescript files: extra semi pass', async t => { - const noSemiCwd = path.resolve('fixtures', 'typescript', 'child'); - const noSemiFilePath = path.resolve(noSemiCwd, 'no-semicolon.ts'); - const noSemiText = await fs.readFile(noSemiFilePath, 'utf8'); - const {results} = await xo.lintText(noSemiText, { - filePath: noSemiFilePath, - semicolon: false, - }); - t.is(results[0].errorCount, 0); -}); - -test('typescript files: four space fail', async t => { - const fourSpacesCwd = path.resolve('fixtures', 'typescript', 'child', 'sub-child'); - const fourSpacesFilePath = path.resolve(fourSpacesCwd, 'four-spaces.ts'); - const fourSpacesText = await fs.readFile(fourSpacesFilePath, 'utf8'); - const {results, rulesMeta} = await xo.lintText(fourSpacesText, { - filePath: fourSpacesFilePath, - }); - t.true(hasRule(results, '@typescript-eslint/indent', rulesMeta)); -}); - -test('typescript files: four space pass', async t => { - const fourSpacesCwd = path.resolve('fixtures', 'typescript', 'child', 'sub-child'); - const fourSpacesFilePath = path.resolve(fourSpacesCwd, 'four-spaces.ts'); - const fourSpacesText = await fs.readFile(fourSpacesFilePath, 'utf8'); - const {results} = await xo.lintText(fourSpacesText, { - filePath: fourSpacesFilePath, - space: 4, - }); - // eslint-disable-next-line ava/assertion-arguments -- Type issue - t.is(results[0].errorCount, 0, JSON.stringify(results[0].messages)); -}); - -test('deprecated rules', async t => { - const {usedDeprecatedRules} = await xo.lintText('\'use strict\'\nconsole.log(\'unicorn\');\n'); - - for (const {ruleId, replacedBy} of usedDeprecatedRules) { - t.is(typeof ruleId, 'string'); - t.true(Array.isArray(replacedBy)); - } -}); - -async function configType(t, {dir}) { - const {results, rulesMeta} = await xo.lintText('var obj = { a: 1 };\n', {cwd: path.resolve('fixtures', 'config-files', dir), filePath: 'file.js'}); - t.true(hasRule(results, 'no-var', rulesMeta)); -} - -configType.title = (_, {type}) => `load config from ${type}`.trim(); - -test(configType, {type: 'xo.config.js', dir: 'xo-config_js'}); -test(configType, {type: 'xo.config.cjs', dir: 'xo-config_cjs'}); -test(configType, {type: '.xo-config.js', dir: 'xo-config_js'}); -test(configType, {type: '.xo-config.cjs', dir: 'xo-config_cjs'}); -test(configType, {type: '.xo-config.json', dir: 'xo-config_json'}); -test(configType, {type: '.xo-config', dir: 'xo-config'}); diff --git a/test/lint-text/config.test.ts b/test/lint-text/config.test.ts new file mode 100644 index 00000000..5662b7b9 --- /dev/null +++ b/test/lint-text/config.test.ts @@ -0,0 +1,164 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import _test, {type TestFn} from 'ava'; // eslint-disable-line ava/use-test +import dedent from 'dedent'; +import {XO} from '../../lib/xo.js'; +import {copyTestProject} from '../helpers/copy-test-project.js'; + +const test = _test as TestFn<{cwd: string}>; + +test.beforeEach(async t => { + t.context.cwd = await copyTestProject(); +}); + +test.afterEach.always(async t => { + await fs.rm(t.context.cwd, {recursive: true, force: true}); +}); + +test('no config > js > semi', async t => { + const filePath = path.join(t.context.cwd, 'test.js'); + const {results} = await new XO({cwd: t.context.cwd}).lintText( + dedent`console.log('hello')\n`, + {filePath}, + ); + t.is(results.length, 1); + t.is(results?.[0]?.messages?.[0]?.ruleId, '@stylistic/semi'); +}); + +test('no config > ts > semi', async t => { + const filePath = path.join(t.context.cwd, 'test.ts'); + const {results} = await new XO({cwd: t.context.cwd}).lintText( + dedent`console.log('hello')\n`, + {filePath}, + ); + + t.is(results?.[0]?.messages?.length, 1); + t.is(results?.[0]?.messages?.[0]?.ruleId, '@stylistic/semi'); +}); + +test('flat config > semi > js', async t => { + const filePath = path.join(t.context.cwd, 'test.js'); + await fs.writeFile( + path.join(t.context.cwd, 'xo.config.js'), + dedent` + export default [ + { + semicolon: false + } + ]\n + `, + 'utf8', + ); + const xo = new XO({cwd: t.context.cwd}); + const {results} = await xo.lintText(dedent`console.log('hello');\n`, { + filePath, + }); + t.is(results?.[0]?.messages?.length, 1); + t.is(results?.[0]?.messages?.[0]?.ruleId, '@stylistic/semi'); +}); + +test('typescript file with flat config - semicolon', async t => { + const filePath = path.join(t.context.cwd, 'test.ts'); + await fs.writeFile( + path.join(t.context.cwd, 'xo.config.js'), + dedent` + export default [ + { + semicolon: false + } + ];\n + `, + 'utf8', + ); + const xo = new XO({cwd: t.context.cwd}); + const {results} = await xo.lintText(dedent`console.log('hello');\n`, { + filePath, + }); + t.is(results?.[0]?.messages?.length, 1); + t.is(results?.[0]?.messages?.[0]?.ruleId, '@stylistic/semi'); +}); + +// for some reason, this test is failing on CI but works on mac, lint files is passing the same test. Not sure why. +test.skip('typescript file with no tsconfig - semicolon', async t => { + const filePath = path.join(t.context.cwd, 'test.ts'); + await fs.rm(path.join(t.context.cwd, 'tsconfig.json')); + await fs.writeFile( + path.join(t.context.cwd, 'xo.config.js'), + dedent` + export default [ + { + semicolon: false + } + ];\n + `, + 'utf8', + ); + const xo = new XO({cwd: t.context.cwd, ts: true}); + const {results} = await xo.lintText(dedent`console.log('hello');\n`, { + filePath, + }); + t.is(results?.[0]?.messages?.length, 1); + t.is(results?.[0]?.messages?.[0]?.ruleId, '@stylistic/semi'); +}); + +test('flat config > space > js', async t => { + const filePath = path.join(t.context.cwd, 'test.js'); + + await fs.writeFile( + path.join(t.context.cwd, 'xo.config.js'), + dedent` + export default [ + { + space: true + } + ];\n + `, + 'utf8', + ); + + const xo = new XO({cwd: t.context.cwd}); + const {results} = await xo.lintText( + dedent` + export function foo() { + console.log('hello'); + }\n + `, + { + filePath, + }, + ); + t.is(results?.[0]?.messages.length, 1); + t.is(results?.[0]?.messages?.[0]?.messageId, 'wrongIndentation'); + t.is(results?.[0]?.messages?.[0]?.ruleId, '@stylistic/indent'); +}); + +test('flat config > space > ts', async t => { + const filePath = path.join(t.context.cwd, 'test.ts'); + + await fs.writeFile( + path.join(t.context.cwd, 'xo.config.js'), + dedent` + export default [ + { + space: true + } + ];\n + `, + 'utf8', + ); + + const xo = new XO({cwd: t.context.cwd}); + const {results} = await xo.lintText( + dedent` + export function foo() { + console.log('hello'); + }\n + `, + { + filePath, + }, + ); + t.is(results?.[0]?.messages.length, 1); + t.is(results?.[0]?.messages?.[0]?.messageId, 'wrongIndentation'); + t.is(results?.[0]?.messages?.[0]?.ruleId, '@stylistic/indent'); +}); diff --git a/test/lint-text/plugins.test.ts b/test/lint-text/plugins.test.ts new file mode 100644 index 00000000..b2a2bc5a --- /dev/null +++ b/test/lint-text/plugins.test.ts @@ -0,0 +1,193 @@ +import path from 'node:path'; +import fs from 'node:fs/promises'; +import test from 'ava'; +import dedent from 'dedent'; +import {XO} from '../../lib/xo.js'; +import {copyTestProject} from '../helpers/copy-test-project.js'; + +let cwd: string; +let filePath: string; +let tsFilePath: string; + +test.before(async () => { + cwd = await copyTestProject(); + filePath = path.join(cwd, 'test.js'); + tsFilePath = path.join(cwd, 'test.ts'); +}); + +test.after.always(async () => { + await fs.rm(cwd, {recursive: true, force: true}); +}); + +test('no-use-extend-native', async t => { + const {results} = await new XO({cwd}).lintText( + dedent` + import {util} from 'node:util'; + + util.isBoolean('50bda47b09923e045759db8e8dd01a0bacd97370'.shortHash() === '50bdcs47');\n + `, + {filePath}, + ); + t.true(results[0]?.messages?.length === 1); + t.truthy(results[0]?.messages?.[0]); + t.is( + results[0]?.messages?.[0]?.ruleId, + 'no-use-extend-native/no-use-extend-native', + ); +}); + +test('no-use-extend-native ts', async t => { + const {results} = await new XO({cwd}).lintText( + dedent` + import {util} from 'node:util'; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + util.isBoolean('50bda47b09923e045759db8e8dd01a0bacd97370'.shortHash() === '50bdcs47');\n + `, + {filePath: tsFilePath}, + ); + t.true(results[0]?.messages?.length === 1); + t.truthy(results[0]?.messages?.[0]); + t.is( + results[0]?.messages?.[0]?.ruleId, + 'no-use-extend-native/no-use-extend-native', + ); +}); + +test('eslint-plugin-import import-x/order', async t => { + const {results} = await new XO({cwd}).lintText( + dedent` + import foo from 'foo'; + import {util} from 'node:util'; + + util.inspect(foo);\n + `, + {filePath}, + ); + + t.true(results[0]?.messages?.length === 1); + t.truthy(results[0]?.messages?.[0]); + t.is(results[0]?.messages?.[0]?.ruleId, 'import-x/order'); +}); + +test('eslint-plugin-import import-x/order ts', async t => { + const {results} = await new XO({cwd}).lintText( + dedent` + import foo from 'foo'; + import util from 'node:util'; + + util.inspect(foo);\n + `, + {filePath: tsFilePath}, + ); + t.true(results[0]?.messages?.length === 1); + t.truthy(results[0]?.messages?.[0]); + t.is(results[0]?.messages?.[0]?.ruleId, 'import-x/order'); +}); + +test('eslint-plugin-import import-x/extensions', async t => { + const {results} = await new XO({cwd}).lintText( + dedent` + import foo from './foo'; + + console.log(foo);\n + `, + {filePath}, + ); + t.true(results[0]?.messages?.length === 1); + t.truthy(results[0]?.messages?.[0]); + t.is(results[0]?.messages?.[0]?.ruleId, 'import-x/extensions'); +}); + +test('eslint-plugin-import import-x/extensions ts', async t => { + const {results} = await new XO({cwd}).lintText( + dedent` + import foo from './foo'; + + console.log(foo);\n + `, + {filePath: tsFilePath}, + ); + t.true(results[0]?.messages?.length === 1); + t.truthy(results[0]?.messages?.[0]); + t.is(results[0]?.messages?.[0]?.ruleId, 'import-x/extensions'); +}); + +test('eslint-plugin-import import-x/no-absolute-path ts', async t => { + const {results} = await new XO({cwd}).lintText( + dedent` + import foo from '/foo'; + + console.log(foo);\n + `, + {filePath: tsFilePath}, + ); + t.true(results[0]?.messages?.some(({ruleId}) => ruleId === 'import-x/no-absolute-path')); +}); + +test('eslint-plugin-import import-x/no-anonymous-default-export', async t => { + const {results} = await new XO({cwd}).lintText( + dedent` + export default () => {};\n + `, + {filePath}, + ); + + t.true(results[0]?.messages?.some(({ruleId}) => ruleId === 'import-x/no-anonymous-default-export')); +}); + +test('eslint-plugin-n n/prefer-global/process', async t => { + const {results} = await new XO({cwd}).lintText( + dedent` + process.cwd();\n + `, + {filePath}, + ); + t.true(results[0]?.messages?.length === 1); + t.truthy(results[0]?.messages?.[0]); + t.is(results[0]?.messages?.[0]?.ruleId, 'n/prefer-global/process'); +}); + +test('eslint-plugin-n n/prefer-global/process ts', async t => { + const {results} = await new XO({cwd}).lintText( + dedent` + process.cwd();\n + `, + {filePath: tsFilePath}, + ); + t.true(results[0]?.messages?.length === 1); + t.truthy(results[0]?.messages?.[0]); + t.is(results[0]?.messages?.[0]?.ruleId, 'n/prefer-global/process'); +}); + +test('eslint-plugin-eslint-comments enable-duplicate-disable', async t => { + const {results} = await new XO({ + cwd, + }).lintText( + dedent` + /* eslint-disable no-undef */ + export const foo = bar(); // eslint-disable-line no-undef + \n + `, + {filePath}, + ); + t.true(results[0]?.errorCount === 1); + t.true(results[0]?.messages.some(({ruleId}) => + ruleId === '@eslint-community/eslint-comments/no-duplicate-disable')); +}); + +test('eslint-plugin-eslint-comments no-duplicate-disable ts', async t => { + const {results} = await new XO({ + cwd, + }).lintText( + dedent` + /* eslint-disable no-undef */ + export const foo = 10; // eslint-disable-line no-undef + \n + `, + {filePath: tsFilePath}, + ); + t.true(results[0]?.errorCount === 1); + t.true(results[0]?.messages.some(({ruleId}) => + ruleId === '@eslint-community/eslint-comments/no-duplicate-disable')); +}); diff --git a/test/open-report.js b/test/open-report.js deleted file mode 100644 index 2c7bc0e2..00000000 --- a/test/open-report.js +++ /dev/null @@ -1,96 +0,0 @@ -import process from 'node:process'; -import path from 'node:path'; -import test from 'ava'; -import proxyquire from 'proxyquire'; -import createEsmUtils from 'esm-utils'; -import xo from '../index.js'; - -const {__dirname} = createEsmUtils(import.meta); -process.chdir(__dirname); - -test.failing('opens nothing when there are no errors nor warnings', async t => { - const glob = path.join(__dirname, 'fixtures/open-report/successes/*'); - const results = await xo.lintFiles(glob); - - const openReport = proxyquire('../lib/open-report', { - 'open-editor'(files) { - if (files.length > 0) { - t.fail(); - } - }, - }); - - openReport(results); - t.pass(); -}); - -test.failing('only opens errors if there are errors and warnings', async t => { - const glob = path.join(__dirname, 'fixtures/open-report/**'); - const results = await xo.lintFiles(glob); - - const expected = [ - { - file: path.join(__dirname, 'fixtures/open-report/errors/one.js'), - line: 1, - column: 7, - }, - { - file: path.join(__dirname, 'fixtures/open-report/errors/two-with-warnings.js'), - line: 1, - column: 1, - }, - { - file: path.join(__dirname, 'fixtures/open-report/errors/three.js'), - line: 1, - column: 7, - }, - ]; - - const openReport = proxyquire('../lib/open-report', { - 'open-editor'(files) { - t.deepEqual(files, expected); - }, - }); - openReport(results); -}); - -test.failing('if a file has errors and warnings, it opens the first error', async t => { - const glob = path.join(__dirname, 'fixtures/open-report/errors/two-with-warnings.js'); - const results = await xo.lintFiles(glob); - - const expected = [ - { - file: path.join(__dirname, 'fixtures/open-report/errors/two-with-warnings.js'), - line: 1, - column: 1, - }, - ]; - - const openReport = proxyquire('../lib/open-report', { - 'open-editor': files => t.deepEqual(files, expected), - }); - openReport(results); -}); - -test.failing('only opens warnings if there are no errors', async t => { - const glob = path.join(__dirname, 'fixtures/open-report/warnings/*'); - const results = await xo.lintFiles(glob); - - const expected = [ - { - file: path.join(__dirname, 'fixtures/open-report/warnings/one.js'), - line: 1, - column: 1, - }, - { - file: path.join(__dirname, 'fixtures/open-report/warnings/three.js'), - line: 1, - column: 1, - }, - ]; - - const openReport = proxyquire('../lib/open-report', { - 'open-editor': files => t.deepEqual(files, expected), - }); - openReport(results); -}); diff --git a/test/options-manager.js b/test/options-manager.js deleted file mode 100644 index 082c6f97..00000000 --- a/test/options-manager.js +++ /dev/null @@ -1,750 +0,0 @@ -import process from 'node:process'; -import path from 'node:path'; -import test from 'ava'; -import slash from 'slash'; -import createEsmUtils from 'esm-utils'; -import MurmurHash3 from 'imurmurhash'; -import {DEFAULT_EXTENSION, DEFAULT_IGNORES, TSCONFIG_DEFAULTS} from '../lib/constants.js'; -import * as manager from '../lib/options-manager.js'; - -const {__dirname, require, readJson, readJsonSync} = createEsmUtils(import.meta); -const parentConfig = readJsonSync('./fixtures/nested/package.json'); -const childConfig = readJsonSync('./fixtures/nested/child/package.json'); -const prettierConfig = readJsonSync('./fixtures/prettier/package.json'); -const enginesConfig = readJsonSync('./fixtures/engines/package.json'); - -process.chdir(__dirname); - -test('normalizeOptions: makes all the options plural and arrays', t => { - const options = manager.normalizeOptions({ - env: 'node', - global: 'foo', - ignore: 'test.js', - plugin: 'my-plugin', - rule: {'my-rule': 'foo'}, - setting: {'my-rule': 'bar'}, - extend: 'foo', - extension: 'html', - }); - - t.deepEqual(options, { - envs: [ - 'node', - ], - extends: [ - 'foo', - ], - extensions: [ - 'html', - ], - globals: [ - 'foo', - ], - ignores: [ - 'test.js', - ], - plugins: [ - 'my-plugin', - ], - rules: { - 'my-rule': 'foo', - }, - settings: { - 'my-rule': 'bar', - }, - }); -}); - -test('normalizeOptions: falsie values stay falsie', t => { - t.deepEqual(manager.normalizeOptions({}), {}); -}); - -test('buildConfig: defaults', t => { - const config = manager.buildConfig({}); - t.regex(slash(config.cacheLocation), /[\\/]\.cache\/xo-linter\/xo-cache.json[\\/]?$/u); - t.is(config.useEslintrc, false); - t.is(config.cache, true); - t.true(config.baseConfig.extends[0].includes('eslint-config-xo')); -}); - -test('buildConfig: space: true', t => { - const config = manager.buildConfig({space: true}); - t.deepEqual(config.baseConfig.rules.indent, ['error', 2, {SwitchCase: 1}]); -}); - -test('buildConfig: space: 4', t => { - const config = manager.buildConfig({space: 4}); - t.deepEqual(config.baseConfig.rules.indent, ['error', 4, {SwitchCase: 1}]); -}); - -test('buildConfig: semicolon', t => { - const config = manager.buildConfig({semicolon: false, nodeVersion: '12'}); - t.deepEqual(config.baseConfig.rules.semi, ['error', 'never']); - t.deepEqual(config.baseConfig.rules['semi-spacing'], ['error', {before: false, after: true}]); -}); - -test('buildConfig: prettier: true', t => { - const config = manager.buildConfig({prettier: true, extends: ['xo-react']}, {}); - - t.deepEqual(config.baseConfig.plugins, ['prettier']); - // Sets the `semi`, `useTabs` and `tabWidth` options in `prettier/prettier` based on the XO `space` and `semicolon` options - // Sets `singleQuote`, `trailingComma`, `bracketSpacing` and `bracketSameLine` with XO defaults - t.deepEqual(config.baseConfig.rules['prettier/prettier'], ['error', { - useTabs: true, - bracketSpacing: false, - bracketSameLine: false, - semi: true, - singleQuote: true, - tabWidth: 2, - trailingComma: 'all', - }]); - // eslint-prettier-config must always be last - t.is(config.baseConfig.extends.at(-1), 'plugin:prettier/recommended'); - // Indent rule is not enabled - t.is(config.baseConfig.rules.indent, undefined); - // Semi rule is not enabled - t.is(config.baseConfig.rules.semi, undefined); - // Semi-spacing is not enabled - t.is(config.baseConfig.rules['semi-spacing'], undefined); -}); - -test('buildConfig: prettier: true, typescript file', t => { - const config = manager.buildConfig({prettier: true, ts: true}, {}); - - t.deepEqual(config.baseConfig.plugins, ['prettier']); - // Sets the `semi`, `useTabs` and `tabWidth` options in `prettier/prettier` based on the XO `space` and `semicolon` options - // Sets `singleQuote`, `trailingComma`, `bracketSpacing` and `bracketSameLine` with XO defaults - t.deepEqual(config.baseConfig.rules['prettier/prettier'], ['error', { - useTabs: true, - bracketSpacing: false, - bracketSameLine: false, - semi: true, - singleQuote: true, - tabWidth: 2, - trailingComma: 'all', - }]); - - // eslint-prettier-config must always be last - t.is(config.baseConfig.extends.at(-1), 'plugin:prettier/recommended'); - t.regex(config.baseConfig.extends.at(-2), /xo-typescript/); - - // Indent rule is not enabled - t.is(config.baseConfig.rules.indent, undefined); - // Semi rule is not enabled - t.is(config.baseConfig.rules.semi, undefined); - // Semi-spacing is not enabled - t.is(config.baseConfig.rules['semi-spacing'], undefined); -}); - -test('buildConfig: prettier: true, semicolon: false', t => { - const config = manager.buildConfig({prettier: true, semicolon: false}, {}); - - // Sets the `semi` options in `prettier/prettier` based on the XO `semicolon` option - t.deepEqual(config.baseConfig.rules['prettier/prettier'], ['error', { - useTabs: true, - bracketSpacing: false, - bracketSameLine: false, - semi: false, - singleQuote: true, - tabWidth: 2, - trailingComma: 'all', - }]); - // Indent rule is not enabled - t.is(config.baseConfig.rules.indent, undefined); - // Semi rule is not enabled - t.is(config.baseConfig.rules.semi, undefined); - // Semi-spacing is not enabled - t.is(config.baseConfig.rules['semi-spacing'], undefined); -}); - -test('buildConfig: prettier: true, space: 4', t => { - const config = manager.buildConfig({prettier: true, space: 4}, {}); - - // Sets `useTabs` and `tabWidth` options in `prettier/prettier` rule based on the XO `space` options - t.deepEqual(config.baseConfig.rules['prettier/prettier'], ['error', { - useTabs: false, - bracketSpacing: false, - bracketSameLine: false, - semi: true, - singleQuote: true, - tabWidth: 4, - trailingComma: 'all', - }]); - // Indent rule is not enabled - t.is(config.baseConfig.rules.indent, undefined); - // Semi rule is not enabled - t.is(config.baseConfig.rules.semi, undefined); - // Semi-spacing is not enabled - t.is(config.baseConfig.rules['semi-spacing'], undefined); -}); - -test('buildConfig: prettier: true, space: true', t => { - const config = manager.buildConfig({prettier: true, space: true}, {}); - - // Sets `useTabs` and `tabWidth` options in `prettier/prettier` rule based on the XO `space` options - t.deepEqual(config.baseConfig.rules['prettier/prettier'], ['error', { - useTabs: false, - bracketSpacing: false, - bracketSameLine: false, - semi: true, - singleQuote: true, - tabWidth: 2, - trailingComma: 'all', - }]); - // Indent rule is not enabled - t.is(config.baseConfig.rules.indent, undefined); - // Semi rule is not enabled - t.is(config.baseConfig.rules.semi, undefined); - // Semi-spacing is not enabled - t.is(config.baseConfig.rules['semi-spacing'], undefined); -}); - -test('buildConfig: merge with prettier config', t => { - const cwd = path.resolve('fixtures', 'prettier'); - const config = manager.buildConfig({cwd, prettier: true}, prettierConfig.prettier); - - // Sets the `semi` options in `prettier/prettier` based on the XO `semicolon` option - t.deepEqual(config.baseConfig.rules['prettier/prettier'], ['error', prettierConfig.prettier]); - // Indent rule is not enabled - t.is(config.baseConfig.rules.indent, undefined); - // Semi rule is not enabled - t.is(config.baseConfig.rules.semi, undefined); - // Semi-spacing is not enabled - t.is(config.baseConfig.rules['semi-spacing'], undefined); -}); - -test('buildConfig: engines: undefined', t => { - const config = manager.buildConfig({}); - - // Do not include any Node.js version specific rules - t.is(config.baseConfig.rules['prefer-object-spread'], 'off'); - t.is(config.baseConfig.rules['prefer-rest-params'], 'off'); - t.is(config.baseConfig.rules['prefer-destructuring'], 'off'); - t.is(config.baseConfig.rules['promise/prefer-await-to-then'], 'off'); - t.is(config.baseConfig.rules['unicorn/prefer-flat-map'], 'off'); - t.is(config.baseConfig.rules['n/prefer-promises/dns'], 'off'); - t.is(config.baseConfig.rules['n/prefer-promises/fs'], 'off'); - t.is(config.baseConfig.rules['n/no-unsupported-features/es-builtins'], undefined); - t.is(config.baseConfig.rules['n/no-unsupported-features/es-syntax'], undefined); - t.is(config.baseConfig.rules['n/no-unsupported-features/node-builtins'], undefined); -}); - -test('buildConfig: nodeVersion: false', t => { - const config = manager.buildConfig({nodeVersion: false}); - - // Override all the rules specific to Node.js version - t.is(config.baseConfig.rules['prefer-object-spread'], 'off'); - t.is(config.baseConfig.rules['prefer-rest-params'], 'off'); - t.is(config.baseConfig.rules['prefer-destructuring'], 'off'); - t.is(config.baseConfig.rules['promise/prefer-await-to-then'], 'off'); - t.is(config.baseConfig.rules['unicorn/prefer-flat-map'], 'off'); - t.is(config.baseConfig.rules['n/prefer-promises/dns'], 'off'); - t.is(config.baseConfig.rules['n/prefer-promises/fs'], 'off'); -}); - -test('buildConfig: nodeVersion: >=6', t => { - const config = manager.buildConfig({nodeVersion: '>=6'}); - - // Turn off rule if we support Node.js below 7.6.0 - t.is(config.baseConfig.rules['promise/prefer-await-to-then'], 'off'); - // Set n/no-unsupported-features rules with the nodeVersion - t.deepEqual(config.baseConfig.rules['n/no-unsupported-features/es-builtins'], ['error', {version: '>=6'}]); - t.deepEqual( - config.baseConfig.rules['n/no-unsupported-features/es-syntax'], - ['error', {version: '>=6', ignores: ['modules']}], - ); - t.deepEqual(config.baseConfig.rules['n/no-unsupported-features/node-builtins'], ['error', {version: '>=6', allowExperimental: true}]); -}); - -test('buildConfig: nodeVersion: >=8', t => { - const config = manager.buildConfig({nodeVersion: '>=8'}); - - // Do not turn off rule if we support only Node.js above 7.6.0 - t.is(config.baseConfig.rules['promise/prefer-await-to-then'], undefined); - // Set n/no-unsupported-features rules with the nodeVersion - t.deepEqual(config.baseConfig.rules['n/no-unsupported-features/es-builtins'], ['error', {version: '>=8'}]); - t.deepEqual( - config.baseConfig.rules['n/no-unsupported-features/es-syntax'], - ['error', {version: '>=8', ignores: ['modules']}], - ); - t.deepEqual(config.baseConfig.rules['n/no-unsupported-features/node-builtins'], ['error', {version: '>=8', allowExperimental: true}]); -}); - -test('mergeWithPrettierConfig: use `singleQuote`, `trailingComma`, `bracketSpacing` and `bracketSameLine` from `prettier` config if defined', t => { - const prettierOptions = { - singleQuote: false, - trailingComma: 'none', - bracketSpacing: false, - bracketSameLine: false, - }; - const result = manager.mergeWithPrettierConfig({}, prettierOptions); - const expected = { - - ...prettierOptions, - tabWidth: 2, - useTabs: true, - semi: true, - }; - t.deepEqual(result, expected); -}); - -test('mergeWithPrettierConfig: determine `tabWidth`, `useTabs`, `semi` from xo config', t => { - const prettierOptions = { - tabWidth: 4, - useTabs: false, - semi: false, - }; - const result = manager.mergeWithPrettierConfig({space: 4, semicolon: false}, {}); - const expected = { - bracketSpacing: false, - bracketSameLine: false, - singleQuote: true, - trailingComma: 'all', - ...prettierOptions, - }; - t.deepEqual(result, expected); -}); - -test('mergeWithPrettierConfig: determine `tabWidth`, `useTabs`, `semi` from prettier config', t => { - const prettierOptions = { - useTabs: false, - semi: false, - tabWidth: 4, - }; - const result = manager.mergeWithPrettierConfig({}, prettierOptions); - const expected = { - bracketSpacing: false, - bracketSameLine: false, - singleQuote: true, - trailingComma: 'all', - ...prettierOptions, - }; - t.deepEqual(result, expected); -}); - -test('mergeWithPrettierConfig: throw error is `semi`/`semicolon` conflicts', t => { - t.throws(() => manager.mergeWithPrettierConfig( - {semicolon: true}, - {semi: false}, - )); - t.throws(() => manager.mergeWithPrettierConfig( - {semicolon: false}, - {semi: true}, - )); - - t.notThrows(() => manager.mergeWithPrettierConfig( - {semicolon: true}, - {semi: true}, - )); - t.notThrows(() => manager.mergeWithPrettierConfig({semicolon: false}, {semi: false})); -}); - -test('mergeWithPrettierConfig: throw error is `space`/`useTabs` conflicts', t => { - t.throws(() => manager.mergeWithPrettierConfig({space: false}, {useTabs: false})); - t.throws(() => manager.mergeWithPrettierConfig({space: true}, {useTabs: true})); - - t.notThrows(() => manager.mergeWithPrettierConfig({space: 4}, {useTabs: false})); - t.notThrows(() => manager.mergeWithPrettierConfig({space: true}, {useTabs: false})); - t.notThrows(() => manager.mergeWithPrettierConfig({space: false}, {useTabs: true})); -}); - -test('mergeWithPrettierConfig: throw error is `space`/`tabWidth` conflicts', t => { - t.throws(() => manager.mergeWithPrettierConfig({space: 4}, {tabWidth: 2})); - t.throws(() => manager.mergeWithPrettierConfig({space: 0}, {tabWidth: 2})); - t.throws(() => manager.mergeWithPrettierConfig({space: 2}, {tabWidth: 0})); - - t.notThrows(() => manager.mergeWithPrettierConfig({space: 4}, {tabWidth: 4})); - t.notThrows(() => manager.mergeWithPrettierConfig({space: false}, {tabWidth: 4})); - t.notThrows(() => manager.mergeWithPrettierConfig({space: true}, {tabWidth: 4})); -}); - -test('buildConfig: rules', t => { - const rules = {'object-curly-spacing': ['error', 'always']}; - const config = manager.buildConfig({rules, nodeVersion: '12'}); - t.deepEqual(config.baseConfig.rules['object-curly-spacing'], rules['object-curly-spacing']); -}); - -test('buildConfig: parser', t => { - const parser = 'babel-eslint'; - const config = manager.buildConfig({parser}); - t.deepEqual(config.baseConfig.parser, parser); -}); - -test('buildConfig: processor', t => { - const processor = 'svelte3/svelte3'; - const config = manager.buildConfig({processor}); - t.deepEqual(config.baseConfig.processor, processor); -}); - -test('buildConfig: settings', t => { - const settings = {'import/resolver': {webpack: {}}}; - const config = manager.buildConfig({settings}); - t.deepEqual(config.baseConfig.settings, settings); -}); - -test('buildConfig: finds webpack config file', t => { - const cwd = path.resolve('fixtures', 'webpack', 'with-config'); - const config = manager.buildConfig({cwd}); - const expected = {webpack: {config: path.resolve(cwd, 'webpack.config.js')}}; - t.deepEqual(config.baseConfig.settings['import/resolver'], expected); -}); - -test('buildConfig: webpack option sets resolver', t => { - const config = manager.buildConfig({webpack: true, settings: {'import/resolver': 'node'}}); - t.deepEqual(config.baseConfig.settings['import/resolver'], {webpack: {}, node: {}}); -}); - -test('buildConfig: webpack option handles object values', t => { - const config = manager.buildConfig({webpack: {foo: 1}, settings: {'import/resolver': 'node'}}); - t.deepEqual(config.baseConfig.settings['import/resolver'], {webpack: {foo: 1}, node: {}}); -}); - -test('buildConfig: webpack resolver is not added automatically if webpack option is set to false', t => { - const cwd = path.resolve('fixtures', 'webpack', 'with-config'); - const config = manager.buildConfig({cwd, webpack: false, settings: {}}); - t.deepEqual(config.baseConfig.settings['import/resolver'], {}); -}); - -test('buildConfig: webpack option is merged with import/resolver', t => { - const settings = {'import/resolver': {webpack: {bar: 1}}}; - const config = manager.buildConfig({settings, webpack: {foo: 1}}); - t.deepEqual(config.baseConfig.settings['import/resolver'], {webpack: {foo: 1, bar: 1}}); -}); - -test('buildConfig: extends', t => { - const config = manager.buildConfig({ - extends: [ - 'plugin:foo/bar', - 'eslint-config-prettier', - './fixtures/config-files/xo_config_js/xo.config.js', - ], - }); - - t.deepEqual(config.baseConfig.extends.slice(-3), [ - 'plugin:foo/bar', - path.resolve('../node_modules/eslint-config-prettier/index.js'), - './fixtures/config-files/xo_config_js/xo.config.js', - ]); -}); - -test('buildConfig: typescript', t => { - const config = manager.buildConfig({ts: true, tsConfigPath: './tsconfig.json'}); - - t.regex(config.baseConfig.extends.at(-1), /xo-typescript/); - t.is(config.baseConfig.parser, require.resolve('@typescript-eslint/parser')); - t.deepEqual(config.baseConfig.parserOptions, { - warnOnUnsupportedTypeScriptVersion: false, - ecmaFeatures: {jsx: true}, - project: './tsconfig.json', - projectFolderIgnoreList: [/\/node_modules\/(?!.*\.cache\/xo-linter)/], - }); - t.is(config.baseConfig.rules['import/named'], 'off'); -}); - -test('buildConfig: typescript with parserOption', t => { - const config = manager.buildConfig({ - ts: true, - parserOptions: {projectFolderIgnoreList: [], sourceType: 'script'}, - tsConfigPath: 'path/to/tmp-tsconfig.json', - }, {}); - - t.is(config.baseConfig.parser, require.resolve('@typescript-eslint/parser')); - t.deepEqual(config.baseConfig.parserOptions, { - warnOnUnsupportedTypeScriptVersion: false, - ecmaFeatures: {jsx: true}, - projectFolderIgnoreList: [], - project: 'path/to/tmp-tsconfig.json', - sourceType: 'script', - }); -}); - -test('buildConfig: parserOptions', t => { - const config = manager.buildConfig({ - parserOptions: { - sourceType: 'script', - }, - }); - - t.is(config.baseConfig.parserOptions.sourceType, 'script'); -}); - -test('buildConfig: prevents useEslintrc option', t => { - t.throws(() => { - manager.buildConfig({ - useEslintrc: true, - }); - }, { - instanceOf: Error, - message: 'The `useEslintrc` option is not supported', - }); -}); - -test('findApplicableOverrides', t => { - const result = manager.findApplicableOverrides('/user/dir/foo.js', [ - {files: '**/f*.js'}, - {files: '**/bar.js'}, - {files: '**/*oo.js'}, - {files: '**/*.txt'}, - ]); - - t.is(result.hash, 0b1010); - t.deepEqual(result.applicable, [ - {files: '**/f*.js'}, - {files: '**/*oo.js'}, - ]); -}); - -test('mergeWithFileConfig: use child if closest', async t => { - const cwd = path.resolve('fixtures', 'nested', 'child'); - const {options} = await manager.mergeWithFileConfig({cwd}); - const eslintConfigId = new MurmurHash3(path.join(cwd, 'package.json')).result(); - const expected = { - ...childConfig.xo, extensions: DEFAULT_EXTENSION, ignores: DEFAULT_IGNORES, cwd, eslintConfigId, - }; - t.deepEqual(options, expected); -}); - -test('mergeWithFileConfig: use parent if closest', async t => { - const cwd = path.resolve('fixtures', 'nested'); - const {options} = await manager.mergeWithFileConfig({cwd}); - const eslintConfigId = new MurmurHash3(path.join(cwd, 'package.json')).result(); - const expected = { - ...parentConfig.xo, extensions: DEFAULT_EXTENSION, ignores: DEFAULT_IGNORES, cwd, eslintConfigId, - }; - t.deepEqual(options, expected); -}); - -test('mergeWithFileConfig: use parent if child is ignored', async t => { - const cwd = path.resolve('fixtures', 'nested'); - const filePath = path.resolve(cwd, 'child-ignore', 'file.js'); - const {options} = await manager.mergeWithFileConfig({cwd, filePath}); - const eslintConfigId = new MurmurHash3(path.join(cwd, 'package.json')).result(); - const expected = { - ...parentConfig.xo, extensions: DEFAULT_EXTENSION, ignores: DEFAULT_IGNORES, cwd, filePath, eslintConfigId, - }; - t.deepEqual(options, expected); -}); - -test('mergeWithFileConfig: use child if child is empty', async t => { - const cwd = path.resolve('fixtures', 'nested', 'child-empty'); - const {options} = await manager.mergeWithFileConfig({cwd}); - const eslintConfigId = new MurmurHash3(path.join(cwd, 'package.json')).result(); - t.deepEqual(options, { - extensions: DEFAULT_EXTENSION, ignores: DEFAULT_IGNORES, cwd, eslintConfigId, - }); -}); - -test('mergeWithFileConfig: read engines from package.json', async t => { - const cwd = path.resolve('fixtures', 'engines'); - const {options} = await manager.mergeWithFileConfig({cwd}); - const eslintConfigId = new MurmurHash3().result(); - const expected = { - nodeVersion: enginesConfig.engines.node, extensions: DEFAULT_EXTENSION, ignores: DEFAULT_IGNORES, cwd, eslintConfigId, - }; - t.deepEqual(options, expected); -}); - -test('mergeWithFileConfig: XO engine options supersede package.json\'s', async t => { - const cwd = path.resolve('fixtures', 'engines'); - const {options} = await manager.mergeWithFileConfig({cwd, nodeVersion: '>=8'}); - const eslintConfigId = new MurmurHash3().result(); - const expected = { - nodeVersion: '>=8', extensions: DEFAULT_EXTENSION, ignores: DEFAULT_IGNORES, cwd, eslintConfigId, - }; - t.deepEqual(options, expected); -}); - -test('mergeWithFileConfig: XO engine options false supersede package.json\'s', async t => { - const cwd = path.resolve('fixtures', 'engines'); - const {options} = await manager.mergeWithFileConfig({cwd, nodeVersion: false}); - const eslintConfigId = new MurmurHash3().result(); - const expected = { - nodeVersion: false, extensions: DEFAULT_EXTENSION, ignores: DEFAULT_IGNORES, cwd, eslintConfigId, - }; - t.deepEqual(options, expected); -}); - -test('mergeWithFileConfig: resolves expected typescript file options', async t => { - const cwd = path.resolve('fixtures', 'typescript', 'child'); - const filePath = path.resolve(cwd, 'file.ts'); - const tsConfigPath = path.resolve(cwd, 'tsconfig.json'); - const tsConfig = await readJson(tsConfigPath); - const {options} = await manager.mergeWithFileConfig({cwd, filePath}); - const eslintConfigId = new MurmurHash3(path.resolve(cwd, 'package.json')).hash(tsConfigPath).result(); - const expected = { - filePath, - extensions: DEFAULT_EXTENSION, - ignores: DEFAULT_IGNORES, - cwd, - semicolon: false, - ts: true, - tsConfigPath, - eslintConfigId, - tsConfig: manager.tsConfigResolvePaths(tsConfig, tsConfigPath), - }; - t.deepEqual(options, expected); -}); - -test('mergeWithFileConfig: resolves expected tsx file options', async t => { - const cwd = path.resolve('fixtures', 'typescript', 'child'); - const filePath = path.resolve(cwd, 'file.tsx'); - const {options} = await manager.mergeWithFileConfig({cwd, filePath}); - const tsConfigPath = path.resolve(cwd, 'tsconfig.json'); - const tsConfig = await readJson(tsConfigPath); - const eslintConfigId = new MurmurHash3(path.join(cwd, 'package.json')).hash(tsConfigPath).result(); - const expected = { - filePath, - extensions: DEFAULT_EXTENSION, - ignores: DEFAULT_IGNORES, - cwd, - semicolon: false, - ts: true, - tsConfigPath, - eslintConfigId, - tsConfig: manager.tsConfigResolvePaths(tsConfig, tsConfigPath), - }; - t.deepEqual(options, expected); -}); - -test('mergeWithFileConfig: uses specified parserOptions.project as tsconfig', async t => { - const cwd = path.resolve('fixtures', 'typescript', 'parseroptions-project'); - const filePath = path.resolve(cwd, 'included-file.ts'); - const expectedTsConfigPath = path.resolve(cwd, 'projectconfig.json'); - const {options} = await manager.mergeWithFileConfig({cwd, filePath}); - t.is(options.tsConfigPath, expectedTsConfigPath); -}); - -test('mergeWithFileConfig: correctly resolves relative tsconfigs excluded file', async t => { - const cwd = path.resolve('fixtures', 'typescript', 'relative-configs'); - const excludedFilePath = path.resolve(cwd, 'excluded-file.ts'); - const excludeTsConfigPath = new RegExp(`${slash(cwd)}/node_modules/.cache/xo-linter/tsconfig\\..*\\.json[\\/]?$`, 'u'); - const {options} = await manager.mergeWithFileConfig({cwd, filePath: excludedFilePath}); - t.regex(slash(options.tsConfigPath), excludeTsConfigPath); -}); - -test('mergeWithFileConfig: correctly resolves relative tsconfigs included file', async t => { - const cwd = path.resolve('fixtures', 'typescript', 'relative-configs'); - const includedFilePath = path.resolve(cwd, 'included-file.ts'); - const includeTsConfigPath = path.resolve(cwd, 'config/tsconfig.json'); - const {options} = await manager.mergeWithFileConfig({cwd, filePath: includedFilePath}); - t.is(options.tsConfigPath, includeTsConfigPath); -}); - -test('mergeWithFileConfig: uses generated tsconfig if specified parserOptions.project excludes file', async t => { - const cwd = path.resolve('fixtures', 'typescript', 'parseroptions-project'); - const filePath = path.resolve(cwd, 'excluded-file.ts'); - const expectedTsConfigPath = new RegExp(`${slash(cwd)}/node_modules/.cache/xo-linter/tsconfig\\..*\\.json[\\/]?$`, 'u'); - const {options} = await manager.mergeWithFileConfig({cwd, filePath}); - t.regex(slash(options.tsConfigPath), expectedTsConfigPath); -}); - -test('mergeWithFileConfig: uses generated tsconfig if specified parserOptions.project misses file', async t => { - const cwd = path.resolve('fixtures', 'typescript', 'parseroptions-project'); - const filePath = path.resolve(cwd, 'missed-by-options-file.ts'); - const expectedTsConfigPath = new RegExp(`${slash(cwd)}/node_modules/.cache/xo-linter/tsconfig\\..*\\.json[\\/]?$`, 'u'); - const {options} = await manager.mergeWithFileConfig({cwd, filePath}); - t.regex(slash(options.tsConfigPath), expectedTsConfigPath); -}); - -test('mergeWithFileConfig: auto generated ts config extends found ts config if file is not covered', async t => { - const cwd = path.resolve('fixtures', 'typescript', 'extends-config'); - const filePath = path.resolve(cwd, 'does-not-matter.ts'); - const expectedConfigPath = new RegExp(`${slash(cwd)}/node_modules/.cache/xo-linter/tsconfig\\..*\\.json[\\/]?$`, 'u'); - const expected = { - extends: path.resolve(cwd, 'tsconfig.json'), - }; - const {options} = await manager.mergeWithFileConfig({cwd, filePath}); - t.regex(slash(options.tsConfigPath), expectedConfigPath); - t.deepEqual(expected, options.tsConfig); -}); - -test('mergeWithFileConfig: used found ts config if file is covered', async t => { - const cwd = path.resolve('fixtures', 'typescript', 'extends-config'); - const filePath = path.resolve(cwd, 'foo.ts'); - const expectedConfigPath = path.resolve(cwd, 'tsconfig.json'); - const {options} = await manager.mergeWithFileConfig({cwd, filePath}); - t.is(options.tsConfigPath, expectedConfigPath); -}); - -test('mergeWithFileConfig: auto generated ts config extends found ts config if file is explicitly excluded', async t => { - const cwd = path.resolve('fixtures', 'typescript', 'excludes'); - const filePath = path.resolve(cwd, 'excluded-file.ts'); - const expectedConfigPath = new RegExp(`${slash(cwd)}/node_modules/.cache/xo-linter/tsconfig\\..*\\.json[\\/]?$`, 'u'); - const expected = { - extends: path.resolve(cwd, 'tsconfig.json'), - }; - const {options} = await manager.mergeWithFileConfig({cwd, filePath}); - t.regex(slash(options.tsConfigPath), expectedConfigPath); - t.deepEqual(expected, options.tsConfig); -}); - -test('mergeWithFileConfig: creates temp tsconfig if none present', async t => { - const cwd = path.resolve('fixtures', 'typescript'); - const expectedConfigPath = new RegExp(`${slash(cwd)}/node_modules/.cache/xo-linter/tsconfig\\..*\\.json[\\/]?$`, 'u'); - const filePath = path.resolve(cwd, 'does-not-matter.ts'); - const {options} = await manager.mergeWithFileConfig({cwd, filePath}); - t.regex(slash(options.tsConfigPath), expectedConfigPath); - t.deepEqual(options.tsConfig, TSCONFIG_DEFAULTS); -}); - -test('mergeWithFileConfig: tsconfig can properly extend configs in node_modules', async t => { - const cwd = path.resolve('fixtures', 'typescript', 'extends-module'); - const expectedConfigPath = path.join(cwd, 'tsconfig.json'); - const filePath = path.resolve(cwd, 'does-not-matter.ts'); - await t.notThrowsAsync(manager.mergeWithFileConfig({cwd, filePath})); - const {options} = await manager.mergeWithFileConfig({cwd, filePath}); - t.is(options.tsConfigPath, expectedConfigPath); -}); - -test('mergeWithFileConfig: tsconfig can properly extend tsconfig base node_modules', async t => { - const cwd = path.resolve('fixtures', 'typescript', 'extends-tsconfig-bases'); - const expectedConfigPath = path.join(cwd, 'tsconfig.json'); - const filePath = path.resolve(cwd, 'does-not-matter.ts'); - await t.notThrowsAsync(manager.mergeWithFileConfig({cwd, filePath})); - const {options} = await manager.mergeWithFileConfig({cwd, filePath}); - t.is(options.tsConfigPath, expectedConfigPath); -}); - -test('mergeWithFileConfig: tsconfig can properly resolve extends arrays introduced in ts 5', async t => { - const cwd = path.resolve('fixtures', 'typescript', 'extends-array'); - const expectedConfigPath = path.join(cwd, 'tsconfig.json'); - const filePath = path.resolve(cwd, 'does-not-matter.ts'); - await t.notThrowsAsync(manager.mergeWithFileConfig({cwd, filePath})); - const {options} = await manager.mergeWithFileConfig({cwd, filePath}); - t.is(options.tsConfigPath, expectedConfigPath); -}); - -test('applyOverrides', t => { - t.deepEqual( - manager.applyOverrides( - 'file.js', - { - overrides: [ - { - files: 'file.js', - rules: {'rule-2': 'c'}, - extends: ['overrride-extend'], - globals: ['override'], - plugins: ['override-plugin'], - }, - ], - rules: {'rule-1': 'a', 'rule-2': 'b'}, - extends: ['base-extend'], - globals: ['base'], - plugins: ['base-plugin'], - cwd: '.', - }), - { - options: { - rules: {'rule-1': 'a', 'rule-2': 'c'}, - extends: ['base-extend', 'overrride-extend'], - globals: ['base', 'override'], - plugins: ['base-plugin', 'override-plugin'], - envs: [], - settings: {}, - cwd: '.', - }, - hash: 1, - }, - ); -}); diff --git a/test/options.js b/test/options.js deleted file mode 100644 index c6eeccb0..00000000 --- a/test/options.js +++ /dev/null @@ -1,24 +0,0 @@ -import test from 'ava'; -import xo from '../index.js'; - -test('prettier', async t => { - const config = await xo.getConfig({prettier: true, filePath: 'example.js'}); - - t.is( - config.rules['arrow-body-style'][0], - 'off', - 'Should extends `eslint-plugin-prettier`, not `eslint-config-prettier`.', - ); - - t.is( - config.rules['unicorn/empty-brace-spaces'][0], - 'off', - '`unicorn/empty-brace-spaces` should be turned off by prettier.', - ); - - t.is( - config.rules['@typescript-eslint/brace-style'][0], - 'off', - '`@typescript-eslint/brace-style` should be turned off even we are not using TypeScript.', - ); -}); diff --git a/test/print-config.js b/test/print-config.js deleted file mode 100644 index 70fd1b7a..00000000 --- a/test/print-config.js +++ /dev/null @@ -1,29 +0,0 @@ -import process from 'node:process'; -import path from 'node:path'; -import test from 'ava'; -import {execa} from 'execa'; -import tempWrite from 'temp-write'; -import createEsmUtils from 'esm-utils'; -import xo from '../index.js'; - -const {__dirname} = createEsmUtils(import.meta); -process.chdir(__dirname); - -const main = (arguments_, options) => execa(path.join(__dirname, '../cli.js'), arguments_, options); - -const hasUnicornPlugin = config => config.plugins.includes('unicorn'); -const hasPrintConfigGlobal = config => Object.keys(config.globals).includes('printConfig'); - -test('getConfig', async t => { - const filepath = await tempWrite('console.log()\n', 'x.js'); - const options = {filePath: filepath, globals: ['printConfig']}; - const result = await xo.getConfig(options); - t.true(hasUnicornPlugin(result) && hasPrintConfigGlobal(result)); -}); - -test('print-config option', async t => { - const filepath = await tempWrite('console.log()\n', 'x.js'); - const {stdout} = await main(['--global=printConfig', '--print-config', filepath]); - const result = JSON.parse(stdout); - t.true(hasUnicornPlugin(result) && hasPrintConfigGlobal(result)); -}); diff --git a/test/resolve-xo-config.test.ts b/test/resolve-xo-config.test.ts new file mode 100644 index 00000000..6bb3a845 --- /dev/null +++ b/test/resolve-xo-config.test.ts @@ -0,0 +1,74 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import _test, {type TestFn} from 'ava'; // eslint-disable-line ava/use-test +import {resolveXoConfig} from '../lib/resolve-config.js'; +import {copyTestProject} from './helpers/copy-test-project.js'; + +const test = _test as TestFn<{cwd: string}>; + +test.beforeEach(async t => { + t.context.cwd = await copyTestProject(); +}); + +test.afterEach.always(async t => { + await fs.rm(t.context.cwd, {recursive: true, force: true}); +}); + +test('resolveXoConfig > no config', async t => { + const {enginesOptions, flatOptions, flatConfigPath} = await resolveXoConfig({ + cwd: t.context.cwd, + }); + t.deepEqual(enginesOptions, {}); + t.deepEqual(flatOptions, []); + t.is(flatConfigPath, ''); +}); + +test('resolveXoConfig > resolves xo config', async t => { + const testConfig = `export default [ + { + space: true, + }, + ];`; + await fs.writeFile( + path.join(t.context.cwd, 'xo.config.js'), + testConfig, + 'utf8', + ); + const {enginesOptions, flatOptions, flatConfigPath} = await resolveXoConfig({ + cwd: t.context.cwd, + }); + + t.deepEqual(enginesOptions, {}); + t.deepEqual(flatConfigPath, path.join(t.context.cwd, 'xo.config.js')); + t.deepEqual(flatOptions, [{space: true}]); +}); + +test('resolveXoConfig > resolves xo config with engines', async t => { + const testConfig = `export default [ + { + space: true, + }, + ];`; + await fs.writeFile( + path.join(t.context.cwd, 'xo.config.js'), + testConfig, + 'utf8', + ); + await fs.writeFile( + path.join(t.context.cwd, 'package.json'), + JSON.stringify({ + engines: {node: '>=16'}, + ...JSON.parse(await fs.readFile(path.join(t.context.cwd, 'package.json'), 'utf8')), + }), + 'utf8', + ); + const {enginesOptions, flatOptions, flatConfigPath} = await resolveXoConfig({ + cwd: t.context.cwd, + }); + + t.is(flatConfigPath, path.join(t.context.cwd, 'xo.config.js')); + t.deepEqual(flatOptions, [{space: true}]); + t.deepEqual(enginesOptions, {node: '>=16'}); + + t.pass(); +}); diff --git a/test/temp/.gitignore b/test/temp/.gitignore deleted file mode 100644 index 355164c1..00000000 --- a/test/temp/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*/ diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..73e53085 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@sindresorhus/tsconfig", + "compilerOptions": { + "outDir": "dist", + "resolveJsonModule": true, + "sourceMap": true + }, + "include": ["**/*.ts", "scripts/setup-tests.js", "package.json"], + "exclude": ["node_modules", "dist", "./test.ts"] +} diff --git a/types.d.ts b/types.d.ts new file mode 100644 index 00000000..77c3cb3c --- /dev/null +++ b/types.d.ts @@ -0,0 +1,76 @@ +declare module 'eslint-plugin-ava' { + import {type ESLint} from 'eslint'; + + const plugin: ESLint.Plugin; + export default plugin; +} + +declare module 'eslint-plugin-import-x' { + import {type ESLint} from 'eslint'; + + const plugin: ESLint.Plugin; + export default plugin; +} + +declare module 'eslint-plugin-n' { + import {type ESLint} from 'eslint'; + + const plugin: ESLint.Plugin; + export default plugin; +} + +declare module 'eslint-plugin-eslint-comments' { + import {type ESLint} from 'eslint'; + + const plugin: ESLint.Plugin; + export default plugin; +} + +declare module '@eslint-community/eslint-plugin-eslint-comments' { + import {type ESLint} from 'eslint'; + + const plugin: ESLint.Plugin; + export default plugin; +} + +declare module 'eslint-plugin-no-use-extend-native' { + import {type ESLint} from 'eslint'; + + const plugin: ESLint.Plugin; + export default plugin; +} + +declare module 'eslint-plugin-prettier' { + import {type ESLint} from 'eslint'; + + const plugin: ESLint.Plugin; + export default plugin; +} + +declare module 'eslint-plugin-promise' { + import {type ESLint} from 'eslint'; + + const plugin: ESLint.Plugin; + export default plugin; +} + +declare module 'eslint-config-xo-typescript' { + import {type Linter} from 'eslint'; + + const config: Linter.Config[]; + export default config; +} + +declare module 'eslint-config-xo' { + import {type Linter} from 'eslint'; + + const config: Linter.Config[]; + export default config; +} + +declare module 'eslint-config-prettier' { + import {type Linter} from 'eslint'; + + const config: Linter.Config; + export default config; +} diff --git a/xo.config.ts b/xo.config.ts new file mode 100644 index 00000000..67813ea1 --- /dev/null +++ b/xo.config.ts @@ -0,0 +1,15 @@ +import type {FlatXoConfig} from './lib/types.js'; + +const xoConfig: FlatXoConfig = [ + {ignores: ['test/fixtures/**/*']}, + { + space: true, + rules: { + '@typescript-eslint/naming-convention': 'off', + 'ava/no-ignored-test-files': 'off', + 'capitalized-comments': 'off', + }, + }, +]; + +export default xoConfig;