diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3b8aa86..6e98150 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,12 +10,12 @@ jobs: fail-fast: false matrix: node-version: - - 16 - - 14 - - 12 + - 22 + - 20 + - 18 steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm install diff --git a/index.js b/index.js index cd07b89..38a36a5 100644 --- a/index.js +++ b/index.js @@ -1,218 +1,209 @@ -'use strict'; -module.exports = { - parserOptions: { - ecmaFeatures: { - jsx: true - } - }, - plugins: [ - 'react', - 'react-hooks' - ], - settings: { - react: { - version: 'detect' - } - }, - rules: { - 'react/boolean-prop-naming': [ - 'error', - { - validateNested: true - } - ], - 'react/button-has-type': 'error', - 'react/jsx-child-element-spacing': 'error', - 'react/default-props-match-prop-types': 'error', - 'react/function-component-definition': [ - 'error', - { - namedComponents: 'function-declaration', - unnamedComponents: 'arrow-function' - } - ], - 'react/hook-use-state': 'error', - 'react/iframe-missing-sandbox': 'error', - 'react/no-access-state-in-setstate': 'error', - 'react/no-array-index-key': 'error', - 'react/no-arrow-function-lifecycle': 'error', - 'react/no-children-prop': 'error', - 'react/no-danger': 'error', - 'react/no-danger-with-children': 'error', - 'react/no-deprecated': 'error', - 'react/no-did-update-set-state': 'error', - 'react/no-direct-mutation-state': 'error', - 'react/no-find-dom-node': 'error', - 'react/no-invalid-html-attribute': 'error', - 'react/no-is-mounted': 'error', - 'react/no-namespace': 'error', - 'react/no-redundant-should-component-update': 'error', - 'react/no-render-return-value': 'error', - 'react/no-typos': 'error', - 'react/no-string-refs': [ - 'error', - { - noTemplateLiterals: true - } - ], - 'react/no-this-in-sfc': 'error', - 'react/no-unescaped-entities': 'error', - 'react/no-unknown-property': 'error', - 'react/no-unsafe': 'error', - 'react/no-unused-prop-types': 'error', - 'react/no-unused-state': 'error', - 'react/prefer-read-only-props': 'error', - 'react/prop-types': 'error', - 'react/react-in-jsx-scope': 'error', - 'react/require-default-props': [ - 'error', - { - forbidDefaultForRequired: true, - ignoreFunctionalComponents: true - } - ], - 'react/self-closing-comp': 'error', - 'react/state-in-constructor': [ - 'error', - 'never' - ], - 'react/static-property-placement': 'error', - 'react/style-prop-object': [ - 'error', - { - allow: [ - // This allows react-intl’s `<FormattedNumber value={0.42} style='percent'/>`. - 'FormattedNumber' - ] - } - ], - 'react/void-dom-elements-no-children': 'error', - 'react/jsx-boolean-value': 'error', - 'react/jsx-closing-bracket-location': [ - 'error', - { - nonEmpty: 'tag-aligned', - selfClosing: false - } - ], - 'react/jsx-closing-tag-location': 'error', - 'react/jsx-curly-newline': [ - 'error', - { - multiline: 'consistent', - singleline: 'forbid' - } - ], - 'react/jsx-curly-spacing': [ - 'error', - 'never' - ], - 'react/jsx-equals-spacing': [ - 'error', - 'never' - ], - 'react/jsx-first-prop-new-line': 'error', - 'react/jsx-indent': [ - 'error', - 'tab', - { - checkAttributes: true, - indentLogicalExpressions: true - } - ], - 'react/jsx-indent-props': [ - 'error', - 'tab' - ], - 'react/jsx-key': [ - 'error', - { - checkFragmentShorthand: true, - checkKeyMustBeforeSpread: true, - warnOnDuplicates: true - } - ], - 'react/jsx-max-props-per-line': [ - 'error', - { - maximum: 3, - when: 'multiline' - } - ], - 'react/jsx-no-bind': [ - 'error', - { - allowArrowFunctions: true - } - ], - 'react/jsx-no-comment-textnodes': 'error', - 'react/jsx-no-constructed-context-values': 'error', - 'react/jsx-no-duplicate-props': [ - 'error', - { - ignoreCase: true - } - ], - 'react/jsx-no-script-url': 'error', - 'react/jsx-no-target-blank': [ - 'error', - { - warnOnSpreadAttributes: true, - forms: true - } - ], - 'react/jsx-no-undef': 'error', - 'react/jsx-no-useless-fragment': 'error', - // Disabled for now as it produces too many errors - // 'react/jsx-one-expression-per-line': ['error', {allow: 'single-child'}], - 'react/jsx-curly-brace-presence': [ - 'error', - { - props: 'never', - children: 'never', - propElementValues: 'always' - } - ], - 'react/jsx-fragments': [ - 'error', - 'syntax' - ], - 'react/jsx-pascal-case': 'error', - 'react/jsx-props-no-multi-spaces': 'error', - 'react/jsx-sort-props': [ - 'error', - { - callbacksLast: true, - shorthandFirst: true, - noSortAlphabetically: true, - reservedFirst: true - } - ], - 'react/jsx-tag-spacing': [ - 'error', - { - closingSlash: 'never', - beforeSelfClosing: 'never', - afterOpening: 'never', - beforeClosing: 'never' - } - ], - 'react/jsx-uses-react': 'error', - 'react/jsx-uses-vars': 'error', - 'react/jsx-wrap-multilines': [ - 'error', - { - declaration: 'parens-new-line', - assignment: 'parens-new-line', - return: 'parens-new-line', - arrow: 'parens-new-line', - condition: 'ignore', - logical: 'ignore', - prop: 'ignore' - } - ], +import react from 'eslint-plugin-react'; +import reactHooks from 'eslint-plugin-react-hooks'; - 'react-hooks/rules-of-hooks': 'error', - 'react-hooks/exhaustive-deps': 'warn' - } -}; +export default [ + { + plugins: { + react, + 'react-hooks': reactHooks, + }, + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + settings: { + react: { + version: 'detect', + }, + }, + rules: { + 'react/boolean-prop-naming': [ + 'error', + { + validateNested: true, + }, + ], + 'react/button-has-type': 'error', + 'react/jsx-child-element-spacing': 'error', + 'react/default-props-match-prop-types': 'error', + 'react/function-component-definition': [ + 'error', + { + namedComponents: 'function-declaration', + unnamedComponents: 'arrow-function', + }, + ], + 'react/hook-use-state': 'error', + 'react/iframe-missing-sandbox': 'error', + 'react/no-access-state-in-setstate': 'error', + 'react/no-array-index-key': 'error', + 'react/no-arrow-function-lifecycle': 'error', + 'react/no-children-prop': 'error', + 'react/no-danger': 'error', + 'react/no-danger-with-children': 'error', + 'react/no-deprecated': 'error', + 'react/no-did-update-set-state': 'error', + 'react/no-direct-mutation-state': 'error', + 'react/no-find-dom-node': 'error', + 'react/no-invalid-html-attribute': 'error', + 'react/no-is-mounted': 'error', + 'react/no-namespace': 'error', + 'react/no-redundant-should-component-update': 'error', + 'react/no-render-return-value': 'error', + 'react/no-typos': 'error', + 'react/no-string-refs': [ + 'error', + { + noTemplateLiterals: true, + }, + ], + 'react/no-this-in-sfc': 'error', + 'react/no-unescaped-entities': 'error', + 'react/no-unknown-property': 'error', + 'react/no-unsafe': 'error', + 'react/no-unused-prop-types': 'error', + 'react/no-unused-state': 'error', + 'react/prefer-read-only-props': 'error', + 'react/prop-types': 'error', + 'react/react-in-jsx-scope': 'error', + 'react/require-default-props': [ + 'error', + { + forbidDefaultForRequired: true, + ignoreFunctionalComponents: true, + }, + ], + 'react/self-closing-comp': 'error', + 'react/state-in-constructor': ['error', 'never'], + 'react/static-property-placement': 'error', + 'react/style-prop-object': [ + 'error', + { + allow: [ + // This allows react-intl’s `<FormattedNumber value={0.42} style='percent'/>`. + 'FormattedNumber', + ], + }, + ], + 'react/void-dom-elements-no-children': 'error', + 'react/jsx-boolean-value': 'error', + 'react/jsx-closing-bracket-location': [ + 'error', + { + nonEmpty: 'tag-aligned', + selfClosing: false, + }, + ], + 'react/jsx-closing-tag-location': 'error', + 'react/jsx-curly-newline': [ + 'error', + { + multiline: 'consistent', + singleline: 'forbid', + }, + ], + 'react/jsx-curly-spacing': ['error', 'never'], + 'react/jsx-equals-spacing': ['error', 'never'], + 'react/jsx-first-prop-new-line': 'error', + 'react/jsx-indent': [ + 'error', + 'tab', + { + checkAttributes: true, + indentLogicalExpressions: true, + }, + ], + 'react/jsx-indent-props': ['error', 'tab'], + 'react/jsx-key': [ + 'error', + { + checkFragmentShorthand: true, + checkKeyMustBeforeSpread: true, + warnOnDuplicates: true, + }, + ], + 'react/jsx-max-props-per-line': [ + 'error', + { + maximum: 3, + when: 'multiline', + }, + ], + 'react/jsx-no-bind': [ + 'error', + { + allowArrowFunctions: true, + }, + ], + 'react/jsx-no-comment-textnodes': 'error', + 'react/jsx-no-constructed-context-values': 'error', + 'react/jsx-no-duplicate-props': [ + 'error', + { + ignoreCase: true, + }, + ], + 'react/jsx-no-script-url': 'error', + 'react/jsx-no-target-blank': [ + 'error', + { + warnOnSpreadAttributes: true, + forms: true, + }, + ], + 'react/jsx-no-undef': 'error', + 'react/jsx-no-useless-fragment': 'error', + // Disabled for now as it produces too many errors + // 'react/jsx-one-expression-per-line': ['error', {allow: 'single-child'}], + 'react/jsx-curly-brace-presence': [ + 'error', + { + props: 'never', + children: 'never', + propElementValues: 'always', + }, + ], + 'react/jsx-fragments': ['error', 'syntax'], + 'react/jsx-pascal-case': 'error', + 'react/jsx-props-no-multi-spaces': 'error', + 'react/jsx-sort-props': [ + 'error', + { + callbacksLast: true, + shorthandFirst: true, + noSortAlphabetically: true, + reservedFirst: true, + }, + ], + 'react/jsx-tag-spacing': [ + 'error', + { + closingSlash: 'never', + beforeSelfClosing: 'never', + afterOpening: 'never', + beforeClosing: 'never', + }, + ], + 'react/jsx-uses-react': 'error', + 'react/jsx-uses-vars': 'error', + 'react/jsx-wrap-multilines': [ + 'error', + { + declaration: 'parens-new-line', + assignment: 'parens-new-line', + return: 'parens-new-line', + arrow: 'parens-new-line', + condition: 'ignore', + logical: 'ignore', + prop: 'ignore', + }, + ], + + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + }, + }, +]; diff --git a/package.json b/package.json index 19d32c3..079d3dc 100644 --- a/package.json +++ b/package.json @@ -10,12 +10,17 @@ "email": "sindresorhus@gmail.com", "url": "https://sindresorhus.com" }, + "type": "module", "engines": { "node": ">=12" }, "scripts": { "test": "ava" }, + "exports": { + ".": "./index.js", + "./space": "./space.js" + }, "files": [ "index.js", "space.js" @@ -51,16 +56,15 @@ "simple" ], "devDependencies": { - "ava": "^2.4.0", - "eslint": "^8.6.0", + "ava": "^6.2.0", + "eslint": "^9.18.0", "eslint-plugin-react": "^7.29.0", - "eslint-plugin-react-hooks": "^4.3.0", - "is-plain-obj": "^3.0.0", + "eslint-plugin-react-hooks": "^5.1.0", "react": "^17.0.2" }, "peerDependencies": { - "eslint": ">=8.6.0", + "eslint": ">=9.18.0", "eslint-plugin-react": ">=7.29.0", - "eslint-plugin-react-hooks": ">=4.3.0" + "eslint-plugin-react-hooks": ">=5.1.0" } } diff --git a/readme.md b/readme.md index a86ba71..22df646 100644 --- a/readme.md +++ b/readme.md @@ -10,51 +10,37 @@ npm install --save-dev eslint-config-xo eslint-config-xo-react eslint-plugin-rea ## Usage -Add some ESLint config to your package.json: +Add some ESLint config to your `eslint.config.js`: -```json -{ - "name": "my-awesome-project", - "eslintConfig": { - "extends": [ - "xo", - "xo-react" - ] - } -} -``` - -Or to .eslintrc: +```js +// eslint.config.js +import xo from 'eslint-config-xo'; +import xoReact from 'eslint-config-xo-react'; -```json -{ - "extends": [ - "xo", - "xo-react" - ] -} +export default [...xo, ...xoReact]; ``` Use the `space` sub-config if you want 2 space indentation instead of tabs: -```json -{ - "extends": [ - "xo", - "xo-react/space" - ] -} +```js +// eslint.config.js +import xo from 'eslint-config-xo'; +import xoReactSpace from 'eslint-config-xo-react/space'; + +export default [...xo, ...xoReactSpace]; ``` -You can also mix it with a [XO](https://github.com/xojs/xo) sub-config: +You can also mix it with a [eslint-config-xo](https://github.com/xojs/eslint-config-xo) sub-config: -```json -{ - "extends": [ - "xo/esnext", - "xo-react" - ] -} +```js +// eslint.config.js +import xoSpace from 'eslint-config-xo/space'; +import xoReactSpace from 'eslint-config-xo-react/space'; + +export default [ + ...xoSpace, + ...xoReactSpace +]; ``` ## Tip diff --git a/space.js b/space.js index 8f9d4b6..31aa43e 100644 --- a/space.js +++ b/space.js @@ -1,16 +1,14 @@ -'use strict'; -const path = require('path'); +import configs from './index.js'; -module.exports = { - extends: path.join(__dirname, 'index.js'), - rules: { - 'react/jsx-indent-props': [ - 'error', - 2 - ], - 'react/jsx-indent': [ - 'error', - 2 - ] - } -}; +const [config] = configs; + +export default [ + { + ...config, + rules: { + ...config.rules, + 'react/jsx-indent-props': ['error', 2], + 'react/jsx-indent': ['error', 2], + }, + }, +]; diff --git a/test/test.js b/test/test.js index e7bbf05..85d8916 100644 --- a/test/test.js +++ b/test/test.js @@ -1,12 +1,13 @@ import test from 'ava'; -import isPlainObj from 'is-plain-obj'; +import eslintConfigXoReact from '../index.js'; +import eslintConfigXoReactSpace from '../space.js'; import {ESLint} from 'eslint'; const hasRule = (errors, ruleId) => errors.some(error => error.ruleId === ruleId); async function runEslint(string, config) { const eslint = new ESLint({ - useEslintrc: false, + overrideConfigFile: true, overrideConfig: config, }); @@ -16,28 +17,20 @@ async function runEslint(string, config) { } test('main', async t => { - const config = require('../space.js'); + t.true(Array.isArray(eslintConfigXoReact)); - t.true(isPlainObj(config)); - t.true(isPlainObj(config.rules)); - - const errors = await runEslint('var app = <div className="foo">Unicorn</div>', config); + const errors = await runEslint('var app = <div className="foo">Unicorn</div>', eslintConfigXoReact); t.true(hasRule(errors, 'react/react-in-jsx-scope')); }); test('space', async t => { - const config = require('../space.js'); - - t.true(isPlainObj(config)); - t.true(isPlainObj(config.rules)); + t.true(Array.isArray(eslintConfigXoReactSpace)); - const errors = await runEslint('<App>\n\t<Hello/>\n</App>', config); + const errors = await runEslint('<App>\n\t<Hello/>\n</App>', eslintConfigXoReactSpace); t.true(hasRule(errors, 'react/jsx-indent')); }); test('no errors', async t => { - const config = require('../index.js'); - - const errors = await runEslint('var React = require(\'react\');\nvar el = <div/>;', config); + const errors = await runEslint('var React = require(\'react\');\nvar el = <div/>;', eslintConfigXoReact); t.deepEqual(errors, []); });