From 6b5f096f10b47326d68e2893152a48a79c8555b4 Mon Sep 17 00:00:00 2001 From: michael faith Date: Mon, 17 Jun 2024 12:17:50 -0500 Subject: [PATCH] [New] add support for Flat Config This change adds support for ESLint's new Flat config system. It maintains backwards compatibility with eslintrc style configs as well. To achieve this, we're now dynamically creating four configs: two are the original `recommended` and `strict`, and the other two are the new `flat/recommended` and `flat/strict`. The two `flat` ones are setup with the new config format, while the original two have the same options as before. Usage Legacy ```json { "extends": ["plugin:jsx-a11y/recommended"] } ``` Flat ```js import globals from 'globals'; import js from '@eslint/js'; import jsxA11y from 'eslint-plugin-jsx-a11y'; export default [ js.configs.recommended, jsxA11y.flatConfigs.recommended, { files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'], languageOptions: { ecmaVersion: 'latest', sourceType: 'module', globals: globals.browser, }, ignores: ['dist', 'eslint.config.js'], rules: { 'no-unused-vars': 'warn', 'jsx-a11y/anchor-ambiguous-text': 'warn', 'jsx-a11y/anchor-is-valid': 'warn', }, }, ]; ``` --- .eslintrc | 1 + .github/workflows/node-pretest.yml | 12 + README.md | 93 +++- examples/flat-cjs/eslint.config.cjs | 22 + examples/flat-cjs/index.html | 13 + examples/flat-cjs/package.json | 20 + examples/flat-cjs/public/vite.svg | 1 + examples/flat-cjs/src/App.css | 42 ++ examples/flat-cjs/src/App.jsx | 36 ++ examples/flat-cjs/src/assets/react.svg | 1 + examples/flat-cjs/src/index.css | 68 +++ examples/flat-cjs/src/main.jsx | 10 + examples/flat-esm/eslint.config.js | 22 + examples/flat-esm/index.html | 13 + examples/flat-esm/package.json | 20 + examples/flat-esm/public/vite.svg | 1 + examples/flat-esm/src/App.css | 42 ++ examples/flat-esm/src/App.jsx | 36 ++ examples/flat-esm/src/assets/react.svg | 1 + examples/flat-esm/src/index.css | 68 +++ examples/flat-esm/src/main.jsx | 10 + examples/legacy/.eslintrc.cjs | 17 + examples/legacy/index.html | 13 + examples/legacy/package.json | 18 + examples/legacy/public/vite.svg | 1 + examples/legacy/src/App.css | 42 ++ examples/legacy/src/App.jsx | 36 ++ examples/legacy/src/assets/react.svg | 1 + examples/legacy/src/index.css | 68 +++ examples/legacy/src/main.jsx | 10 + package.json | 10 +- src/configs/flat-config-base.js | 9 + src/configs/legacy-config-base.js | 7 + src/index.js | 602 +++++++++++++------------ 34 files changed, 1073 insertions(+), 293 deletions(-) create mode 100644 examples/flat-cjs/eslint.config.cjs create mode 100644 examples/flat-cjs/index.html create mode 100644 examples/flat-cjs/package.json create mode 100644 examples/flat-cjs/public/vite.svg create mode 100644 examples/flat-cjs/src/App.css create mode 100644 examples/flat-cjs/src/App.jsx create mode 100644 examples/flat-cjs/src/assets/react.svg create mode 100644 examples/flat-cjs/src/index.css create mode 100644 examples/flat-cjs/src/main.jsx create mode 100644 examples/flat-esm/eslint.config.js create mode 100644 examples/flat-esm/index.html create mode 100644 examples/flat-esm/package.json create mode 100644 examples/flat-esm/public/vite.svg create mode 100644 examples/flat-esm/src/App.css create mode 100644 examples/flat-esm/src/App.jsx create mode 100644 examples/flat-esm/src/assets/react.svg create mode 100644 examples/flat-esm/src/index.css create mode 100644 examples/flat-esm/src/main.jsx create mode 100644 examples/legacy/.eslintrc.cjs create mode 100644 examples/legacy/index.html create mode 100644 examples/legacy/package.json create mode 100644 examples/legacy/public/vite.svg create mode 100644 examples/legacy/src/App.css create mode 100644 examples/legacy/src/App.jsx create mode 100644 examples/legacy/src/assets/react.svg create mode 100644 examples/legacy/src/index.css create mode 100644 examples/legacy/src/main.jsx create mode 100644 src/configs/flat-config-base.js create mode 100644 src/configs/legacy-config-base.js diff --git a/.eslintrc b/.eslintrc index 7024a688d..67859870f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -7,6 +7,7 @@ "ignorePatterns": [ "lib/", "reports/", + "examples/", ], "parser": "@babel/eslint-parser", "plugins": [ diff --git a/.github/workflows/node-pretest.yml b/.github/workflows/node-pretest.yml index e501df6be..3a10687fa 100644 --- a/.github/workflows/node-pretest.yml +++ b/.github/workflows/node-pretest.yml @@ -38,3 +38,15 @@ jobs: node-version: 'lts/*' skip-ls-check: true - run: npm run posttest + + examples: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: ljharb/actions/node/install@main + name: 'nvm install lts/* && npm install' + with: + node-version: 'lts/*' + skip-ls-check: true + - run: npm run test:examples \ No newline at end of file diff --git a/README.md b/README.md index 02f1ed5c0..5f66a744c 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,8 @@ yarn add eslint-plugin-jsx-a11y --dev **Note:** If you installed ESLint globally (using the `-g` flag in npm, or the `global` prefix in yarn) then you must also install `eslint-plugin-jsx-a11y` globally. -## Usage + +## Usage - Legacy Config (`.eslintrc`) Add `jsx-a11y` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix: @@ -109,6 +110,94 @@ Add `plugin:jsx-a11y/recommended` or `plugin:jsx-a11y/strict` in `extends`: } ``` +## Usage - Flat Config (`eslint.config.js`) + +The default export of `eslint-plugin-jsx-a11y` is a plugin object. + +```js +const jsxA11y = require('eslint-plugin-jsx-a11y'); + +module.exports = [ + … + { + files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'], + plugins: { + 'jsx-a11y': jsxA11y, + }, + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + rules: { + // ... any rules you want + 'jsx-a11y/alt-text': 'error', + }, + // ... others are omitted for brevity + }, + … +]; +``` + +### Shareable Configs + +There are two shareable configs, provided by the plugin. + +- `flatConfigs.recommended` +- `flatConfigs.strict` + +#### CJS + +```js +const jsxA11y = require('eslint-plugin-jsx-a11y'); + +export default [ + jsxA11y.flatConfigs.recommended, + { + // Your additional configs and overrides + }, +]; +``` + +#### ESM + +```js +import jsxA11y from 'eslint-plugin-jsx-a11y'; + +export default [ + jsxA11y.flatConfigs.recommended, + { + // Your additional configs and overrides + }, +]; +``` + +**Note**: Our shareable config do configure `files` or [`languageOptions.globals`](https://eslint.org/docs/latest/user-guide/configuring/configuration-files-new#configuration-objects). +For most of the cases, you probably want to configure some of these properties yourself. + +```js +const jsxA11yRecommended = require('eslint-plugin-jsx-a11y'); +const globals = require('globals'); + +module.exports = [ + … + { + files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'], + ...jsxA11y.flatConfigs.recommended, + languageOptions: { + ...jsxA11y.flatConfigs.recommended.languageOptions, + globals: { + ...globals.serviceworker, + ...globals.browser, + }, + }, + }, + … +]; +``` + #### Component Mapping To enable your custom components to be checked as DOM elements, you can set global settings in your configuration file by mapping each custom component name to a DOM element type. @@ -124,7 +213,7 @@ For example, if you set the `polymorphicPropName` setting to `as` then this elem will be evaluated as an `h3`. If no `polymorphicPropName` is set, then the component will be evaluated as `Box`. -⚠️ Polymorphic components can make code harder to maintain; please use this feature with caution. +⚠️ Polymorphic components can make code harder to maintain; please use this feature with caution. ## Supported Rules diff --git a/examples/flat-cjs/eslint.config.cjs b/examples/flat-cjs/eslint.config.cjs new file mode 100644 index 000000000..fb9c07153 --- /dev/null +++ b/examples/flat-cjs/eslint.config.cjs @@ -0,0 +1,22 @@ +const globals = require('globals'); +const js = require('@eslint/js'); +const jsxA11y = require('eslint-plugin-jsx-a11y'); + +module.exports = [ + js.configs.recommended, + jsxA11y.flatConfigs.recommended, + { + files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'], + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + globals: globals.browser, + }, + ignores: ['dist', 'eslint.config.cjs'], + rules: { + 'no-unused-vars': 'off', + 'jsx-a11y/anchor-ambiguous-text': 'warn', + 'jsx-a11y/anchor-is-valid': 'warn', + }, + }, +]; diff --git a/examples/flat-cjs/index.html b/examples/flat-cjs/index.html new file mode 100644 index 000000000..0c589eccd --- /dev/null +++ b/examples/flat-cjs/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + + +
+ + + diff --git a/examples/flat-cjs/package.json b/examples/flat-cjs/package.json new file mode 100644 index 000000000..d89c7f081 --- /dev/null +++ b/examples/flat-cjs/package.json @@ -0,0 +1,20 @@ +{ + "name": "flat-cjs", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "lint": "cross-env ESLINT_USE_FLAT_CONFIG=true eslint . --report-unused-disable-directives" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.5.0", + "cross-env": "^7.0.3", + "eslint": "^8.57.0", + "eslint-plugin-jsx-a11y": "file:../..", + "globals": "^15.6.0" + } +} diff --git a/examples/flat-cjs/public/vite.svg b/examples/flat-cjs/public/vite.svg new file mode 100644 index 000000000..e7b8dfb1b --- /dev/null +++ b/examples/flat-cjs/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/flat-cjs/src/App.css b/examples/flat-cjs/src/App.css new file mode 100644 index 000000000..b9d355df2 --- /dev/null +++ b/examples/flat-cjs/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/examples/flat-cjs/src/App.jsx b/examples/flat-cjs/src/App.jsx new file mode 100644 index 000000000..b2d55881e --- /dev/null +++ b/examples/flat-cjs/src/App.jsx @@ -0,0 +1,36 @@ +import { useState } from 'react'; +import reactLogo from './assets/react.svg'; +import viteLogo from '/vite.svg'; +import './App.css'; + +function App() { + const [count, setCount] = useState(0); + + return ( + <> +
+ + Vite logo + + + React logo + + click here +
+

Vite + React

+
+ +

+ Edit src/App.jsx and save to test HMR +

+
+

+ Click on the Vite and React logos to learn more +

+ + ); +} + +export default App; diff --git a/examples/flat-cjs/src/assets/react.svg b/examples/flat-cjs/src/assets/react.svg new file mode 100644 index 000000000..6c87de9bb --- /dev/null +++ b/examples/flat-cjs/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/flat-cjs/src/index.css b/examples/flat-cjs/src/index.css new file mode 100644 index 000000000..6119ad9a8 --- /dev/null +++ b/examples/flat-cjs/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/examples/flat-cjs/src/main.jsx b/examples/flat-cjs/src/main.jsx new file mode 100644 index 000000000..54b39dd1d --- /dev/null +++ b/examples/flat-cjs/src/main.jsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.jsx' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + , +) diff --git a/examples/flat-esm/eslint.config.js b/examples/flat-esm/eslint.config.js new file mode 100644 index 000000000..ae5afaa73 --- /dev/null +++ b/examples/flat-esm/eslint.config.js @@ -0,0 +1,22 @@ +import globals from 'globals'; +import js from '@eslint/js'; +import jsxA11y from 'eslint-plugin-jsx-a11y'; + +export default [ + js.configs.recommended, + jsxA11y.flatConfigs.recommended, + { + files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'], + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + globals: globals.browser, + }, + ignores: ['dist', 'eslint.config.js'], + rules: { + 'no-unused-vars': 'off', + 'jsx-a11y/anchor-ambiguous-text': 'warn', + 'jsx-a11y/anchor-is-valid': 'warn', + }, + }, +]; diff --git a/examples/flat-esm/index.html b/examples/flat-esm/index.html new file mode 100644 index 000000000..0c589eccd --- /dev/null +++ b/examples/flat-esm/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + + +
+ + + diff --git a/examples/flat-esm/package.json b/examples/flat-esm/package.json new file mode 100644 index 000000000..cf10c8170 --- /dev/null +++ b/examples/flat-esm/package.json @@ -0,0 +1,20 @@ +{ + "name": "flat-esm", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "lint": "cross-env ESLINT_USE_FLAT_CONFIG=true eslint . --report-unused-disable-directives" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.5.0", + "cross-env": "^7.0.3", + "eslint": "^8.57.0", + "eslint-plugin-jsx-a11y": "file:../..", + "globals": "^15.6.0" + } +} diff --git a/examples/flat-esm/public/vite.svg b/examples/flat-esm/public/vite.svg new file mode 100644 index 000000000..e7b8dfb1b --- /dev/null +++ b/examples/flat-esm/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/flat-esm/src/App.css b/examples/flat-esm/src/App.css new file mode 100644 index 000000000..b9d355df2 --- /dev/null +++ b/examples/flat-esm/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/examples/flat-esm/src/App.jsx b/examples/flat-esm/src/App.jsx new file mode 100644 index 000000000..b2d55881e --- /dev/null +++ b/examples/flat-esm/src/App.jsx @@ -0,0 +1,36 @@ +import { useState } from 'react'; +import reactLogo from './assets/react.svg'; +import viteLogo from '/vite.svg'; +import './App.css'; + +function App() { + const [count, setCount] = useState(0); + + return ( + <> +
+ + Vite logo + + + React logo + + click here +
+

Vite + React

+
+ +

+ Edit src/App.jsx and save to test HMR +

+
+

+ Click on the Vite and React logos to learn more +

+ + ); +} + +export default App; diff --git a/examples/flat-esm/src/assets/react.svg b/examples/flat-esm/src/assets/react.svg new file mode 100644 index 000000000..6c87de9bb --- /dev/null +++ b/examples/flat-esm/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/flat-esm/src/index.css b/examples/flat-esm/src/index.css new file mode 100644 index 000000000..6119ad9a8 --- /dev/null +++ b/examples/flat-esm/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/examples/flat-esm/src/main.jsx b/examples/flat-esm/src/main.jsx new file mode 100644 index 000000000..54b39dd1d --- /dev/null +++ b/examples/flat-esm/src/main.jsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.jsx' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + , +) diff --git a/examples/legacy/.eslintrc.cjs b/examples/legacy/.eslintrc.cjs new file mode 100644 index 000000000..1b743411e --- /dev/null +++ b/examples/legacy/.eslintrc.cjs @@ -0,0 +1,17 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: ['eslint:recommended', 'plugin:jsx-a11y/recommended'], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + settings: { react: { version: '18.2' } }, + plugins: ['jsx-a11y'], + rules: { + 'no-unused-vars': 'off', + 'jsx-a11y/anchor-ambiguous-text': 'warn', + 'jsx-a11y/anchor-is-valid': 'warn', + }, +}; diff --git a/examples/legacy/index.html b/examples/legacy/index.html new file mode 100644 index 000000000..0c589eccd --- /dev/null +++ b/examples/legacy/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + + +
+ + + diff --git a/examples/legacy/package.json b/examples/legacy/package.json new file mode 100644 index 000000000..35be81484 --- /dev/null +++ b/examples/legacy/package.json @@ -0,0 +1,18 @@ +{ + "name": "legacy", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "lint": "cross-env ESLINT_USE_FLAT_CONFIG=false eslint . --ext js,jsx --report-unused-disable-directives" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "cross-env": "^7.0.3", + "eslint": "^8.57.0", + "eslint-plugin-jsx-a11y": "file:../.." + } +} diff --git a/examples/legacy/public/vite.svg b/examples/legacy/public/vite.svg new file mode 100644 index 000000000..e7b8dfb1b --- /dev/null +++ b/examples/legacy/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/legacy/src/App.css b/examples/legacy/src/App.css new file mode 100644 index 000000000..b9d355df2 --- /dev/null +++ b/examples/legacy/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/examples/legacy/src/App.jsx b/examples/legacy/src/App.jsx new file mode 100644 index 000000000..b2d55881e --- /dev/null +++ b/examples/legacy/src/App.jsx @@ -0,0 +1,36 @@ +import { useState } from 'react'; +import reactLogo from './assets/react.svg'; +import viteLogo from '/vite.svg'; +import './App.css'; + +function App() { + const [count, setCount] = useState(0); + + return ( + <> +
+ + Vite logo + + + React logo + + click here +
+

Vite + React

+
+ +

+ Edit src/App.jsx and save to test HMR +

+
+

+ Click on the Vite and React logos to learn more +

+ + ); +} + +export default App; diff --git a/examples/legacy/src/assets/react.svg b/examples/legacy/src/assets/react.svg new file mode 100644 index 000000000..6c87de9bb --- /dev/null +++ b/examples/legacy/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/legacy/src/index.css b/examples/legacy/src/index.css new file mode 100644 index 000000000..6119ad9a8 --- /dev/null +++ b/examples/legacy/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/examples/legacy/src/main.jsx b/examples/legacy/src/main.jsx new file mode 100644 index 000000000..54b39dd1d --- /dev/null +++ b/examples/legacy/src/main.jsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.jsx' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + , +) diff --git a/package.json b/package.json index 382b6828a..f14e0b68b 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,14 @@ "test": "npm run jest", "posttest": "aud --production", "test:ci": "npm run jest -- --ci --runInBand", + "pretest:examples": "npm run build", + "test:examples": "npm run test-example:legacy && npm run test-example:flat-esm && npm run test-example:flat-cjs", + "test-example:legacy": "cd examples/legacy && npm install && npm run lint", + "test-example:flat-esm": "cd examples/flat-esm && npm install && npm run lint", + "test-example:flat-cjs": "cd examples/flat-cjs && npm install && npm run lint", "jest": "jest --coverage __tests__/**/*", "pregenerate-list-of-rules": "npm run build", - "generate-list-of-rules": "eslint-doc-generator --rule-doc-title-format prefix-name --rule-doc-section-options false --config-emoji recommended,☑️", + "generate-list-of-rules": "eslint-doc-generator --rule-doc-title-format prefix-name --rule-doc-section-options false --config-emoji recommended,☑️ --ignore-config flat/recommended --ignore-config flat/strict", "generate-list-of-rules:check": "npm run generate-list-of-rules -- --check", "version": "auto-changelog && git add CHANGELOG.md", "postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\"" @@ -128,7 +133,8 @@ "/reports", "/flow", "scripts/", - "CONTRIBUTING.md" + "CONTRIBUTING.md", + "/examples" ] } } diff --git a/src/configs/flat-config-base.js b/src/configs/flat-config-base.js new file mode 100644 index 000000000..54b84e5fe --- /dev/null +++ b/src/configs/flat-config-base.js @@ -0,0 +1,9 @@ +module.exports = { + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, +}; diff --git a/src/configs/legacy-config-base.js b/src/configs/legacy-config-base.js new file mode 100644 index 000000000..2d177961e --- /dev/null +++ b/src/configs/legacy-config-base.js @@ -0,0 +1,7 @@ +module.exports = { + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, +}; diff --git a/src/index.js b/src/index.js index 752ff6121..2fa185fa5 100644 --- a/src/index.js +++ b/src/index.js @@ -1,296 +1,320 @@ /* eslint-disable global-require */ +const flatConfigBase = require('./configs/flat-config-base'); +const legacyConfigBase = require('./configs/legacy-config-base'); +const { name, version } = require('../package.json'); -module.exports = { - rules: { - 'accessible-emoji': require('./rules/accessible-emoji'), - 'alt-text': require('./rules/alt-text'), - 'anchor-ambiguous-text': require('./rules/anchor-ambiguous-text'), - 'anchor-has-content': require('./rules/anchor-has-content'), - 'anchor-is-valid': require('./rules/anchor-is-valid'), - 'aria-activedescendant-has-tabindex': require('./rules/aria-activedescendant-has-tabindex'), - 'aria-props': require('./rules/aria-props'), - 'aria-proptypes': require('./rules/aria-proptypes'), - 'aria-role': require('./rules/aria-role'), - 'aria-unsupported-elements': require('./rules/aria-unsupported-elements'), - 'autocomplete-valid': require('./rules/autocomplete-valid'), - 'click-events-have-key-events': require('./rules/click-events-have-key-events'), - 'control-has-associated-label': require('./rules/control-has-associated-label'), - 'heading-has-content': require('./rules/heading-has-content'), - 'html-has-lang': require('./rules/html-has-lang'), - 'iframe-has-title': require('./rules/iframe-has-title'), - 'img-redundant-alt': require('./rules/img-redundant-alt'), - 'interactive-supports-focus': require('./rules/interactive-supports-focus'), - 'label-has-associated-control': require('./rules/label-has-associated-control'), - 'label-has-for': require('./rules/label-has-for'), - lang: require('./rules/lang'), - 'media-has-caption': require('./rules/media-has-caption'), - 'mouse-events-have-key-events': require('./rules/mouse-events-have-key-events'), - 'no-access-key': require('./rules/no-access-key'), - 'no-aria-hidden-on-focusable': require('./rules/no-aria-hidden-on-focusable'), - 'no-autofocus': require('./rules/no-autofocus'), - 'no-distracting-elements': require('./rules/no-distracting-elements'), - 'no-interactive-element-to-noninteractive-role': require('./rules/no-interactive-element-to-noninteractive-role'), - 'no-noninteractive-element-interactions': require('./rules/no-noninteractive-element-interactions'), - 'no-noninteractive-element-to-interactive-role': require('./rules/no-noninteractive-element-to-interactive-role'), - 'no-noninteractive-tabindex': require('./rules/no-noninteractive-tabindex'), - 'no-onchange': require('./rules/no-onchange'), - 'no-redundant-roles': require('./rules/no-redundant-roles'), - 'no-static-element-interactions': require('./rules/no-static-element-interactions'), - 'prefer-tag-over-role': require('./rules/prefer-tag-over-role'), - 'role-has-required-aria-props': require('./rules/role-has-required-aria-props'), - 'role-supports-aria-props': require('./rules/role-supports-aria-props'), - scope: require('./rules/scope'), - 'tabindex-no-positive': require('./rules/tabindex-no-positive'), - }, - configs: { - recommended: { - plugins: [ - 'jsx-a11y', +const allRules = { + 'accessible-emoji': require('./rules/accessible-emoji'), + 'alt-text': require('./rules/alt-text'), + 'anchor-ambiguous-text': require('./rules/anchor-ambiguous-text'), + 'anchor-has-content': require('./rules/anchor-has-content'), + 'anchor-is-valid': require('./rules/anchor-is-valid'), + 'aria-activedescendant-has-tabindex': require('./rules/aria-activedescendant-has-tabindex'), + 'aria-props': require('./rules/aria-props'), + 'aria-proptypes': require('./rules/aria-proptypes'), + 'aria-role': require('./rules/aria-role'), + 'aria-unsupported-elements': require('./rules/aria-unsupported-elements'), + 'autocomplete-valid': require('./rules/autocomplete-valid'), + 'click-events-have-key-events': require('./rules/click-events-have-key-events'), + 'control-has-associated-label': require('./rules/control-has-associated-label'), + 'heading-has-content': require('./rules/heading-has-content'), + 'html-has-lang': require('./rules/html-has-lang'), + 'iframe-has-title': require('./rules/iframe-has-title'), + 'img-redundant-alt': require('./rules/img-redundant-alt'), + 'interactive-supports-focus': require('./rules/interactive-supports-focus'), + 'label-has-associated-control': require('./rules/label-has-associated-control'), + 'label-has-for': require('./rules/label-has-for'), + lang: require('./rules/lang'), + 'media-has-caption': require('./rules/media-has-caption'), + 'mouse-events-have-key-events': require('./rules/mouse-events-have-key-events'), + 'no-access-key': require('./rules/no-access-key'), + 'no-aria-hidden-on-focusable': require('./rules/no-aria-hidden-on-focusable'), + 'no-autofocus': require('./rules/no-autofocus'), + 'no-distracting-elements': require('./rules/no-distracting-elements'), + 'no-interactive-element-to-noninteractive-role': require('./rules/no-interactive-element-to-noninteractive-role'), + 'no-noninteractive-element-interactions': require('./rules/no-noninteractive-element-interactions'), + 'no-noninteractive-element-to-interactive-role': require('./rules/no-noninteractive-element-to-interactive-role'), + 'no-noninteractive-tabindex': require('./rules/no-noninteractive-tabindex'), + 'no-onchange': require('./rules/no-onchange'), + 'no-redundant-roles': require('./rules/no-redundant-roles'), + 'no-static-element-interactions': require('./rules/no-static-element-interactions'), + 'prefer-tag-over-role': require('./rules/prefer-tag-over-role'), + 'role-has-required-aria-props': require('./rules/role-has-required-aria-props'), + 'role-supports-aria-props': require('./rules/role-supports-aria-props'), + scope: require('./rules/scope'), + 'tabindex-no-positive': require('./rules/tabindex-no-positive'), +}; + +const recommendedRules = { + 'jsx-a11y/alt-text': 'error', + 'jsx-a11y/anchor-ambiguous-text': 'off', // TODO: error + 'jsx-a11y/anchor-has-content': 'error', + 'jsx-a11y/anchor-is-valid': 'error', + 'jsx-a11y/aria-activedescendant-has-tabindex': 'error', + 'jsx-a11y/aria-props': 'error', + 'jsx-a11y/aria-proptypes': 'error', + 'jsx-a11y/aria-role': 'error', + 'jsx-a11y/aria-unsupported-elements': 'error', + 'jsx-a11y/autocomplete-valid': 'error', + 'jsx-a11y/click-events-have-key-events': 'error', + 'jsx-a11y/control-has-associated-label': [ + 'off', + { + ignoreElements: [ + 'audio', + 'canvas', + 'embed', + 'input', + 'textarea', + 'tr', + 'video', + ], + ignoreRoles: [ + 'grid', + 'listbox', + 'menu', + 'menubar', + 'radiogroup', + 'row', + 'tablist', + 'toolbar', + 'tree', + 'treegrid', + ], + includeRoles: ['alert', 'dialog'], + }, + ], + 'jsx-a11y/heading-has-content': 'error', + 'jsx-a11y/html-has-lang': 'error', + 'jsx-a11y/iframe-has-title': 'error', + 'jsx-a11y/img-redundant-alt': 'error', + 'jsx-a11y/interactive-supports-focus': [ + 'error', + { + tabbable: [ + 'button', + 'checkbox', + 'link', + 'searchbox', + 'spinbutton', + 'switch', + 'textbox', + ], + }, + ], + 'jsx-a11y/label-has-associated-control': 'error', + 'jsx-a11y/label-has-for': 'off', + 'jsx-a11y/media-has-caption': 'error', + 'jsx-a11y/mouse-events-have-key-events': 'error', + 'jsx-a11y/no-access-key': 'error', + 'jsx-a11y/no-autofocus': 'error', + 'jsx-a11y/no-distracting-elements': 'error', + 'jsx-a11y/no-interactive-element-to-noninteractive-role': [ + 'error', + { + tr: ['none', 'presentation'], + canvas: ['img'], + }, + ], + 'jsx-a11y/no-noninteractive-element-interactions': [ + 'error', + { + handlers: [ + 'onClick', + 'onError', + 'onLoad', + 'onMouseDown', + 'onMouseUp', + 'onKeyPress', + 'onKeyDown', + 'onKeyUp', + ], + alert: ['onKeyUp', 'onKeyDown', 'onKeyPress'], + body: ['onError', 'onLoad'], + dialog: ['onKeyUp', 'onKeyDown', 'onKeyPress'], + iframe: ['onError', 'onLoad'], + img: ['onError', 'onLoad'], + }, + ], + 'jsx-a11y/no-noninteractive-element-to-interactive-role': [ + 'error', + { + ul: [ + 'listbox', + 'menu', + 'menubar', + 'radiogroup', + 'tablist', + 'tree', + 'treegrid', + ], + ol: [ + 'listbox', + 'menu', + 'menubar', + 'radiogroup', + 'tablist', + 'tree', + 'treegrid', + ], + li: [ + 'menuitem', + 'menuitemradio', + 'menuitemcheckbox', + 'option', + 'row', + 'tab', + 'treeitem', + ], + table: ['grid'], + td: ['gridcell'], + fieldset: ['radiogroup', 'presentation'], + }, + ], + 'jsx-a11y/no-noninteractive-tabindex': [ + 'error', + { + tags: [], + roles: ['tabpanel'], + allowExpressionValues: true, + }, + ], + 'jsx-a11y/no-redundant-roles': 'error', + 'jsx-a11y/no-static-element-interactions': [ + 'error', + { + allowExpressionValues: true, + handlers: [ + 'onClick', + 'onMouseDown', + 'onMouseUp', + 'onKeyPress', + 'onKeyDown', + 'onKeyUp', ], - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - }, - rules: { - 'jsx-a11y/alt-text': 'error', - 'jsx-a11y/anchor-ambiguous-text': 'off', // TODO: error - 'jsx-a11y/anchor-has-content': 'error', - 'jsx-a11y/anchor-is-valid': 'error', - 'jsx-a11y/aria-activedescendant-has-tabindex': 'error', - 'jsx-a11y/aria-props': 'error', - 'jsx-a11y/aria-proptypes': 'error', - 'jsx-a11y/aria-role': 'error', - 'jsx-a11y/aria-unsupported-elements': 'error', - 'jsx-a11y/autocomplete-valid': 'error', - 'jsx-a11y/click-events-have-key-events': 'error', - 'jsx-a11y/control-has-associated-label': ['off', { - ignoreElements: [ - 'audio', - 'canvas', - 'embed', - 'input', - 'textarea', - 'tr', - 'video', - ], - ignoreRoles: [ - 'grid', - 'listbox', - 'menu', - 'menubar', - 'radiogroup', - 'row', - 'tablist', - 'toolbar', - 'tree', - 'treegrid', - ], - includeRoles: [ - 'alert', - 'dialog', - ], - }], - 'jsx-a11y/heading-has-content': 'error', - 'jsx-a11y/html-has-lang': 'error', - 'jsx-a11y/iframe-has-title': 'error', - 'jsx-a11y/img-redundant-alt': 'error', - 'jsx-a11y/interactive-supports-focus': [ - 'error', - { - tabbable: [ - 'button', - 'checkbox', - 'link', - 'searchbox', - 'spinbutton', - 'switch', - 'textbox', - ], - }, - ], - 'jsx-a11y/label-has-associated-control': 'error', - 'jsx-a11y/label-has-for': 'off', - 'jsx-a11y/media-has-caption': 'error', - 'jsx-a11y/mouse-events-have-key-events': 'error', - 'jsx-a11y/no-access-key': 'error', - 'jsx-a11y/no-autofocus': 'error', - 'jsx-a11y/no-distracting-elements': 'error', - 'jsx-a11y/no-interactive-element-to-noninteractive-role': [ - 'error', - { - tr: ['none', 'presentation'], - canvas: ['img'], - }, - ], - 'jsx-a11y/no-noninteractive-element-interactions': [ - 'error', - { - handlers: [ - 'onClick', - 'onError', - 'onLoad', - 'onMouseDown', - 'onMouseUp', - 'onKeyPress', - 'onKeyDown', - 'onKeyUp', - ], - alert: ['onKeyUp', 'onKeyDown', 'onKeyPress'], - body: ['onError', 'onLoad'], - dialog: ['onKeyUp', 'onKeyDown', 'onKeyPress'], - iframe: ['onError', 'onLoad'], - img: ['onError', 'onLoad'], - }, - ], - 'jsx-a11y/no-noninteractive-element-to-interactive-role': [ - 'error', - { - ul: [ - 'listbox', - 'menu', - 'menubar', - 'radiogroup', - 'tablist', - 'tree', - 'treegrid', - ], - ol: [ - 'listbox', - 'menu', - 'menubar', - 'radiogroup', - 'tablist', - 'tree', - 'treegrid', - ], - li: ['menuitem', 'menuitemradio', 'menuitemcheckbox', 'option', 'row', 'tab', 'treeitem'], - table: ['grid'], - td: ['gridcell'], - fieldset: ['radiogroup', 'presentation'], - }, - ], - 'jsx-a11y/no-noninteractive-tabindex': [ - 'error', - { - tags: [], - roles: ['tabpanel'], - allowExpressionValues: true, - }, - ], - 'jsx-a11y/no-redundant-roles': 'error', - 'jsx-a11y/no-static-element-interactions': [ - 'error', - { - allowExpressionValues: true, - handlers: [ - 'onClick', - 'onMouseDown', - 'onMouseUp', - 'onKeyPress', - 'onKeyDown', - 'onKeyUp', - ], - }, - ], - 'jsx-a11y/role-has-required-aria-props': 'error', - 'jsx-a11y/role-supports-aria-props': 'error', - 'jsx-a11y/scope': 'error', - 'jsx-a11y/tabindex-no-positive': 'error', - }, }, - strict: { - plugins: [ - 'jsx-a11y', + ], + 'jsx-a11y/role-has-required-aria-props': 'error', + 'jsx-a11y/role-supports-aria-props': 'error', + 'jsx-a11y/scope': 'error', + 'jsx-a11y/tabindex-no-positive': 'error', +}; + +const strictRules = { + 'jsx-a11y/alt-text': 'error', + 'jsx-a11y/anchor-has-content': 'error', + 'jsx-a11y/anchor-is-valid': 'error', + 'jsx-a11y/aria-activedescendant-has-tabindex': 'error', + 'jsx-a11y/aria-props': 'error', + 'jsx-a11y/aria-proptypes': 'error', + 'jsx-a11y/aria-role': 'error', + 'jsx-a11y/aria-unsupported-elements': 'error', + 'jsx-a11y/autocomplete-valid': 'error', + 'jsx-a11y/click-events-have-key-events': 'error', + 'jsx-a11y/control-has-associated-label': [ + 'off', + { + ignoreElements: [ + 'audio', + 'canvas', + 'embed', + 'input', + 'textarea', + 'tr', + 'video', + ], + ignoreRoles: [ + 'grid', + 'listbox', + 'menu', + 'menubar', + 'radiogroup', + 'row', + 'tablist', + 'toolbar', + 'tree', + 'treegrid', ], - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - }, - rules: { - 'jsx-a11y/alt-text': 'error', - 'jsx-a11y/anchor-has-content': 'error', - 'jsx-a11y/anchor-is-valid': 'error', - 'jsx-a11y/aria-activedescendant-has-tabindex': 'error', - 'jsx-a11y/aria-props': 'error', - 'jsx-a11y/aria-proptypes': 'error', - 'jsx-a11y/aria-role': 'error', - 'jsx-a11y/aria-unsupported-elements': 'error', - 'jsx-a11y/autocomplete-valid': 'error', - 'jsx-a11y/click-events-have-key-events': 'error', - 'jsx-a11y/control-has-associated-label': ['off', { - ignoreElements: [ - 'audio', - 'canvas', - 'embed', - 'input', - 'textarea', - 'tr', - 'video', - ], - ignoreRoles: [ - 'grid', - 'listbox', - 'menu', - 'menubar', - 'radiogroup', - 'row', - 'tablist', - 'toolbar', - 'tree', - 'treegrid', - ], - includeRoles: [ - 'alert', - 'dialog', - ], - }], - 'jsx-a11y/heading-has-content': 'error', - 'jsx-a11y/html-has-lang': 'error', - 'jsx-a11y/iframe-has-title': 'error', - 'jsx-a11y/img-redundant-alt': 'error', - 'jsx-a11y/interactive-supports-focus': [ - 'error', - { - tabbable: [ - 'button', - 'checkbox', - 'link', - 'progressbar', - 'searchbox', - 'slider', - 'spinbutton', - 'switch', - 'textbox', - ], - }, - ], - 'jsx-a11y/label-has-for': 'off', - 'jsx-a11y/label-has-associated-control': 'error', - 'jsx-a11y/media-has-caption': 'error', - 'jsx-a11y/mouse-events-have-key-events': 'error', - 'jsx-a11y/no-access-key': 'error', - 'jsx-a11y/no-autofocus': 'error', - 'jsx-a11y/no-distracting-elements': 'error', - 'jsx-a11y/no-interactive-element-to-noninteractive-role': 'error', - 'jsx-a11y/no-noninteractive-element-interactions': [ - 'error', - { - body: ['onError', 'onLoad'], - iframe: ['onError', 'onLoad'], - img: ['onError', 'onLoad'], - }, - ], - 'jsx-a11y/no-noninteractive-element-to-interactive-role': 'error', - 'jsx-a11y/no-noninteractive-tabindex': 'error', - 'jsx-a11y/no-redundant-roles': 'error', - 'jsx-a11y/no-static-element-interactions': 'error', - 'jsx-a11y/role-has-required-aria-props': 'error', - 'jsx-a11y/role-supports-aria-props': 'error', - 'jsx-a11y/scope': 'error', - 'jsx-a11y/tabindex-no-positive': 'error', - }, + includeRoles: ['alert', 'dialog'], + }, + ], + 'jsx-a11y/heading-has-content': 'error', + 'jsx-a11y/html-has-lang': 'error', + 'jsx-a11y/iframe-has-title': 'error', + 'jsx-a11y/img-redundant-alt': 'error', + 'jsx-a11y/interactive-supports-focus': [ + 'error', + { + tabbable: [ + 'button', + 'checkbox', + 'link', + 'progressbar', + 'searchbox', + 'slider', + 'spinbutton', + 'switch', + 'textbox', + ], + }, + ], + 'jsx-a11y/label-has-for': 'off', + 'jsx-a11y/label-has-associated-control': 'error', + 'jsx-a11y/media-has-caption': 'error', + 'jsx-a11y/mouse-events-have-key-events': 'error', + 'jsx-a11y/no-access-key': 'error', + 'jsx-a11y/no-autofocus': 'error', + 'jsx-a11y/no-distracting-elements': 'error', + 'jsx-a11y/no-interactive-element-to-noninteractive-role': 'error', + 'jsx-a11y/no-noninteractive-element-interactions': [ + 'error', + { + body: ['onError', 'onLoad'], + iframe: ['onError', 'onLoad'], + img: ['onError', 'onLoad'], }, - }, + ], + 'jsx-a11y/no-noninteractive-element-to-interactive-role': 'error', + 'jsx-a11y/no-noninteractive-tabindex': 'error', + 'jsx-a11y/no-redundant-roles': 'error', + 'jsx-a11y/no-static-element-interactions': 'error', + 'jsx-a11y/role-has-required-aria-props': 'error', + 'jsx-a11y/role-supports-aria-props': 'error', + 'jsx-a11y/scope': 'error', + 'jsx-a11y/tabindex-no-positive': 'error', }; + +/** Base plugin object */ +const jsxA11y = { + meta: { name, version }, + rules: { ...allRules }, +}; + +/** + * Given a ruleset and optionally a flat config name, generate a config. + * @param {object} rules - ruleset for this config + * @param {string} flatConfigName - name for the config if flat + * @returns Config for this set of rules. + */ +const createConfig = (rules, flatConfigName) => ({ + ...(flatConfigName + ? { + ...flatConfigBase, + name: `jsx-a11y/${flatConfigName}`, + plugins: { 'jsx-a11y': jsxA11y }, + } + : { ...legacyConfigBase, plugins: ['jsx-a11y'] }), + rules: { ...rules }, +}); + +// Create configs for the plugin object +const configs = { + recommended: createConfig(recommendedRules), + strict: createConfig(strictRules), +}; +const flatConfigs = { + recommended: createConfig(recommendedRules, 'recommended'), + strict: createConfig(strictRules, 'strict'), +}; + +module.exports = { ...jsxA11y, configs, flatConfigs };