From 72c98fe048d034389f56b386afc7b181c3637499 Mon Sep 17 00:00:00 2001 From: michael faith Date: Mon, 17 Jun 2024 12:17:50 -0500 Subject: [PATCH] feat: 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.configs['flat/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', }, }, ]; ``` --- .eslintignore | 1 + README.md | 94 +++- examples/flat-cjs/.gitignore | 24 + examples/flat-cjs/README.md | 8 + examples/flat-cjs/eslint.config.cjs | 22 + examples/flat-cjs/index.html | 13 + examples/flat-cjs/package.json | 27 ++ 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-cjs/vite.config.js | 7 + examples/flat-esm/.gitignore | 24 + examples/flat-esm/README.md | 8 + examples/flat-esm/eslint.config.js | 22 + examples/flat-esm/index.html | 13 + examples/flat-esm/package.json | 27 ++ 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/flat-esm/vite.config.js | 7 + examples/legacy/.eslintrc.cjs | 17 + examples/legacy/.gitignore | 24 + examples/legacy/README.md | 8 + examples/legacy/index.html | 13 + examples/legacy/package.json | 25 ++ 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 + examples/legacy/vite.config.js | 7 + package.json | 10 +- src/configs/flat-config-base.js | 9 + src/configs/legacy-config-base.js | 7 + src/index.js | 600 +++++++++++++------------ 42 files changed, 1198 insertions(+), 293 deletions(-) create mode 100644 .eslintignore create mode 100644 examples/flat-cjs/.gitignore create mode 100644 examples/flat-cjs/README.md 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-cjs/vite.config.js create mode 100644 examples/flat-esm/.gitignore create mode 100644 examples/flat-esm/README.md 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/flat-esm/vite.config.js create mode 100644 examples/legacy/.eslintrc.cjs create mode 100644 examples/legacy/.gitignore create mode 100644 examples/legacy/README.md 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 examples/legacy/vite.config.js create mode 100644 src/configs/flat-config-base.js create mode 100644 src/configs/legacy-config-base.js diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..56bef8cb3 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +examples/ \ No newline at end of file diff --git a/README.md b/README.md index 02f1ed5c0..fbfe5f494 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,9 @@ 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 +111,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. + +- `flat/strict` +- `flat/recommended` + +#### CJS + +```js +const jsxA11y = require('eslint-plugin-jsx-a11y'); + +export default [ + jsxA11y.configs['flat/recommended'], + { + // Your additional configs and overrides + }, +]; +``` + +#### ESM + +```js +import jsxA11y from 'eslint-plugin-jsx-a11y'; + +export default [ + jsxA11y.configs['flat/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.configs['flat/recommended'], + languageOptions: { + ...jsxA11y.configs['flat/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 +214,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/.gitignore b/examples/flat-cjs/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/examples/flat-cjs/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/flat-cjs/README.md b/examples/flat-cjs/README.md new file mode 100644 index 000000000..f768e33fc --- /dev/null +++ b/examples/flat-cjs/README.md @@ -0,0 +1,8 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh diff --git a/examples/flat-cjs/eslint.config.cjs b/examples/flat-cjs/eslint.config.cjs new file mode 100644 index 000000000..09024b622 --- /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.configs['flat/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': 'warn', + '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..715a023db --- /dev/null +++ b/examples/flat-cjs/package.json @@ -0,0 +1,27 @@ +{ + "name": "flat-cjs", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "cross-env ESLINT_USE_FLAT_CONFIG=true eslint . --report-unused-disable-directives", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.5.0", + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@vitejs/plugin-react": "^4.2.1", + "cross-env": "^7.0.3", + "eslint": "^8.57.0", + "eslint-plugin-jsx-a11y": "file:../..", + "globals": "^15.6.0", + "vite": "^5.2.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-cjs/vite.config.js b/examples/flat-cjs/vite.config.js new file mode 100644 index 000000000..5a33944a9 --- /dev/null +++ b/examples/flat-cjs/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/examples/flat-esm/.gitignore b/examples/flat-esm/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/examples/flat-esm/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/flat-esm/README.md b/examples/flat-esm/README.md new file mode 100644 index 000000000..f768e33fc --- /dev/null +++ b/examples/flat-esm/README.md @@ -0,0 +1,8 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh diff --git a/examples/flat-esm/eslint.config.js b/examples/flat-esm/eslint.config.js new file mode 100644 index 000000000..8da7824c8 --- /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.configs['flat/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', + }, + }, +]; 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..e5eee6407 --- /dev/null +++ b/examples/flat-esm/package.json @@ -0,0 +1,27 @@ +{ + "name": "flat-esm", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "cross-env ESLINT_USE_FLAT_CONFIG=true eslint . --report-unused-disable-directives", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.5.0", + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@vitejs/plugin-react": "^4.2.1", + "cross-env": "^7.0.3", + "eslint": "^8.57.0", + "eslint-plugin-jsx-a11y": "file:../..", + "globals": "^15.6.0", + "vite": "^5.2.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/flat-esm/vite.config.js b/examples/flat-esm/vite.config.js new file mode 100644 index 000000000..5a33944a9 --- /dev/null +++ b/examples/flat-esm/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +}) 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/.gitignore b/examples/legacy/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/examples/legacy/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/legacy/README.md b/examples/legacy/README.md new file mode 100644 index 000000000..f768e33fc --- /dev/null +++ b/examples/legacy/README.md @@ -0,0 +1,8 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 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..f4308fc75 --- /dev/null +++ b/examples/legacy/package.json @@ -0,0 +1,25 @@ +{ + "name": "legacy", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "cross-env ESLINT_USE_FLAT_CONFIG=false eslint . --ext js,jsx --report-unused-disable-directives", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@vitejs/plugin-react": "^4.2.1", + "cross-env": "^7.0.3", + "eslint": "^8.57.0", + "eslint-plugin-jsx-a11y": "file:../..", + "vite": "^5.2.0" + } +} 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/examples/legacy/vite.config.js b/examples/legacy/vite.config.js new file mode 100644 index 000000000..5a33944a9 --- /dev/null +++ b/examples/legacy/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/package.json b/package.json index 382b6828a..06ca9004d 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,13 @@ "test": "npm run jest", "posttest": "aud --production", "test:ci": "npm run jest -- --ci --runInBand", + "test:examples": "npm run build && concurrently -c \"auto\" \"npm:test-example:*\"", + "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)\")\"" @@ -47,6 +51,7 @@ "babel-jest": "^24.9.0", "babel-plugin-add-module-exports": "^1.0.4", "babel-preset-airbnb": "^5.0.0", + "concurrently": "^8.2.2", "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8", "eslint-config-airbnb-base": "^15.0.0", "eslint-doc-generator": "^1.7.1", @@ -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..5010ae6e3 100644 --- a/src/index.js +++ b/src/index.js @@ -1,296 +1,318 @@ /* 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', ], - 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', - }, + alert: ['onKeyUp', 'onKeyDown', 'onKeyPress'], + body: ['onError', 'onLoad'], + dialog: ['onKeyUp', 'onKeyDown', 'onKeyPress'], + iframe: ['onError', 'onLoad'], + img: ['onError', 'onLoad'], }, - strict: { - plugins: [ - 'jsx-a11y', + ], + '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', ], - 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', - }, + 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', }; + +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', + ], + 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 to plugin object +const configs = { + recommended: createConfig(recommendedRules), + strict: createConfig(strictRules), + 'flat/recommended': createConfig(recommendedRules, 'flat/recommended'), + 'flat/strict': createConfig(strictRules, 'flat/strict'), +}; + +module.exports = { ...jsxA11y, configs };