├── .github ├── FUNDING.yml └── workflows │ ├── release.yml │ └── ci.yml ├── .npmrc ├── CONTRIBUTING.md ├── .gitignore ├── eslint.config.js ├── tsconfig.json ├── test ├── input │ ├── invalid-json-schema-plugin.ts │ └── plugin-with-schema-ids.ts ├── output │ ├── invalid-json-schema-plugin.d.ts │ ├── plugin-with-schema-ids.d.ts │ ├── no-rule-meta.d.ts │ ├── jsx-a11y.d.ts │ ├── plugins.d.ts │ └── flat-config-vue.d.ts └── index.test.ts ├── pnpm-workspace.yaml ├── LICENSE ├── .vscode └── settings.json ├── package.json ├── src ├── index.ts └── core.ts └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [antfu] 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check=true 2 | shell-emulator=true 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please refer to https://github.com/antfu/contribute 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .DS_Store 3 | .idea 4 | *.log 5 | *.tgz 6 | coverage 7 | dist 8 | lib-cov 9 | logs 10 | node_modules 11 | temp 12 | eslint-typegen.d.ts 13 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /// 3 | import antfu from '@antfu/eslint-config' 4 | // eslint-disable-next-line antfu/no-import-dist 5 | import typegen from './dist/index.mjs' 6 | 7 | export default typegen(await antfu({ 8 | vue: true, 9 | typescript: true, 10 | pnpm: true, 11 | }), 12 | ) 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["ESNext"], 5 | "module": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "resolveJsonModule": true, 8 | "strict": true, 9 | "strictNullChecks": true, 10 | "esModuleInterop": true, 11 | "skipDefaultLibCheck": true, 12 | "skipLibCheck": true 13 | }, 14 | "include": ["**/*.ts"], 15 | "exclude": ["node_modules", "dist"] 16 | 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'v*' 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Install pnpm 20 | uses: pnpm/action-setup@v2 21 | 22 | - name: Set node 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: lts/* 26 | 27 | - run: npx changelogithub 28 | env: 29 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 30 | -------------------------------------------------------------------------------- /test/input/invalid-json-schema-plugin.ts: -------------------------------------------------------------------------------- 1 | import type { ESLint, Rule } from 'eslint' 2 | 3 | export const invalidJsonSchemaPlugin: ESLint.Plugin = { 4 | rules: { 5 | 'invalid-json-schema-rule': { 6 | create: () => null as unknown as Rule.RuleListener, 7 | meta: { 8 | docs: { 9 | description: 'This rules points to a non-existing schema', 10 | }, 11 | schema: { 12 | allOf: [ 13 | { 14 | $ref: '#/definitions/some-definition-that-does-not-exist', 15 | }, 16 | ], 17 | type: 'object', 18 | }, 19 | }, 20 | }, 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /test/output/invalid-json-schema-plugin.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | import type { Linter } from 'eslint' 4 | 5 | declare module 'eslint' { 6 | namespace Linter { 7 | interface RulesRecord extends RuleOptions {} 8 | } 9 | } 10 | 11 | export interface RuleOptions { 12 | /** 13 | * This rules points to a non-existing schema 14 | */ 15 | 'invalid-json-schema-plugin/invalid-json-schema-rule'?: Linter.RuleEntry 16 | } 17 | 18 | /* ======= Declarations ======= */ 19 | // ----- invalid-json-schema-plugin/invalid-json-schema-rule ----- 20 | type InvalidJsonSchemaPluginInvalidJsonSchemaRule = unknown -------------------------------------------------------------------------------- /test/output/plugin-with-schema-ids.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | import type { Linter } from 'eslint' 4 | 5 | declare module 'eslint' { 6 | namespace Linter { 7 | interface RulesRecord extends RuleOptions {} 8 | } 9 | } 10 | 11 | export interface RuleOptions { 12 | 'plugin/schema-with-id-at-root'?: Linter.RuleEntry<_PluginSchemaWithIdAtRootSchemaId> 13 | } 14 | 15 | /* ======= Declarations ======= */ 16 | // ----- plugin/schema-with-id-at-root ----- 17 | type _PluginSchemaWithIdAtRootAId = string 18 | type _PluginSchemaWithIdAtRootC1Id = string 19 | interface _PluginSchemaWithIdAtRootSchemaId { 20 | a?: _PluginSchemaWithIdAtRootAId 21 | b?: _PluginSchemaWithIdAtRootBId 22 | c?: { 23 | c1?: _PluginSchemaWithIdAtRootC1Id 24 | } 25 | d?: { 26 | da?: string 27 | } 28 | } 29 | interface _PluginSchemaWithIdAtRootBId { 30 | b1?: string 31 | } -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - playground 3 | - docs 4 | - packages/* 5 | - examples/* 6 | catalogs: 7 | dev: 8 | '@antfu/eslint-config': ^4.19.0 9 | '@antfu/ni': ^25.0.0 10 | '@types/json-schema': ^7.0.15 11 | '@types/node': ^24.1.0 12 | bumpp: ^10.2.0 13 | eslint: ^9.31.0 14 | lint-staged: ^16.1.2 15 | pnpm: ^10.13.1 16 | simple-git-hooks: ^2.13.0 17 | tsx: ^4.20.3 18 | typescript: ^5.8.3 19 | unbuild: ^3.6.0 20 | vite: ^7.0.6 21 | prod: 22 | json-schema-to-typescript-lite: ^15.0.0 23 | ohash: ^2.0.11 24 | test: 25 | '@typescript-eslint/eslint-plugin': ^8.38.0 26 | eslint-plugin-antfu: ^3.1.1 27 | eslint-plugin-import-x: ^4.16.1 28 | eslint-plugin-jsx-a11y: ^6.10.2 29 | eslint-plugin-you-dont-need-lodash-underscore: ^6.14.0 30 | vitest: ^3.2.4 31 | onlyBuiltDependencies: 32 | - esbuild 33 | - simple-git-hooks 34 | - unrs-resolver 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Anthony Fu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Enable the ESlint flat config support 3 | "eslint.experimental.useFlatConfig": true, 4 | 5 | // Disable the default formatter, use eslint instead 6 | "prettier.enable": false, 7 | "editor.formatOnSave": false, 8 | 9 | // Auto fix 10 | "editor.codeActionsOnSave": { 11 | "source.fixAll": "explicit", 12 | "source.organizeImports": "never" 13 | }, 14 | 15 | // Silent the stylistic rules in you IDE, but still auto fix them 16 | "eslint.rules.customizations": [ 17 | { "rule": "style/*", "severity": "off" }, 18 | { "rule": "*-indent", "severity": "off" }, 19 | { "rule": "*-spacing", "severity": "off" }, 20 | { "rule": "*-spaces", "severity": "off" }, 21 | { "rule": "*-order", "severity": "off" }, 22 | { "rule": "*-dangle", "severity": "off" }, 23 | { "rule": "*-newline", "severity": "off" }, 24 | { "rule": "*quotes", "severity": "off" }, 25 | { "rule": "*semi", "severity": "off" } 26 | ], 27 | 28 | // Enable eslint for all supported languages 29 | "eslint.validate": [ 30 | "javascript", 31 | "javascriptreact", 32 | "typescript", 33 | "typescriptreact", 34 | "vue", 35 | "html", 36 | "markdown", 37 | "json", 38 | "jsonc", 39 | "yaml" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Install pnpm 19 | uses: pnpm/action-setup@v2 20 | 21 | - name: Set node 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: lts/* 25 | 26 | - name: Setup 27 | run: npm i -g @antfu/ni 28 | 29 | - name: Install 30 | run: nci 31 | 32 | - name: Lint 33 | run: nr lint 34 | 35 | - name: Typecheck 36 | run: nr typecheck 37 | 38 | test: 39 | runs-on: ${{ matrix.os }} 40 | 41 | strategy: 42 | matrix: 43 | node: [lts/*] 44 | os: [ubuntu-latest, windows-latest, macos-latest] 45 | fail-fast: false 46 | 47 | steps: 48 | - uses: actions/checkout@v3 49 | 50 | - name: Install pnpm 51 | uses: pnpm/action-setup@v2 52 | 53 | - name: Set node ${{ matrix.node }} 54 | uses: actions/setup-node@v3 55 | with: 56 | node-version: ${{ matrix.node }} 57 | 58 | - name: Setup 59 | run: npm i -g @antfu/ni 60 | 61 | - name: Install 62 | run: nci 63 | 64 | - name: Build 65 | run: nr build 66 | 67 | - name: Test 68 | run: nr test 69 | -------------------------------------------------------------------------------- /test/input/plugin-with-schema-ids.ts: -------------------------------------------------------------------------------- 1 | import type { ESLint, Rule } from 'eslint' 2 | 3 | export const pluginWithSchemaIds: ESLint.Plugin = { 4 | rules: { 5 | 'schema-with-id-at-root': { 6 | create: () => null as unknown as Rule.RuleListener, 7 | meta: { 8 | schema: { 9 | id: 'schemaId', 10 | type: 'object', 11 | additionalProperties: false, 12 | properties: { 13 | a: { 14 | id: 'aId', 15 | type: 'string', 16 | }, 17 | b: { 18 | id: 'bId', 19 | additionalProperties: false, 20 | type: 'object', 21 | properties: { 22 | b1: { 23 | type: 'string', 24 | }, 25 | }, 26 | }, 27 | c: { 28 | additionalProperties: false, 29 | type: 'object', 30 | properties: { 31 | c1: { 32 | type: 'string', 33 | id: 'c1Id', 34 | }, 35 | }, 36 | }, 37 | d: { 38 | additionalProperties: false, 39 | type: 'object', 40 | properties: { 41 | da: { 42 | type: 'string', 43 | }, 44 | }, 45 | }, 46 | }, 47 | }, 48 | }, 49 | }, 50 | }, 51 | } 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-typegen", 3 | "type": "module", 4 | "version": "2.3.0", 5 | "packageManager": "pnpm@10.13.1", 6 | "description": "Generate types from ESLint rule schemas automatically, with auto-completion and type-checking for rule options.", 7 | "author": "Anthony Fu ", 8 | "license": "MIT", 9 | "funding": "https://github.com/sponsors/antfu", 10 | "homepage": "https://github.com/antfu/eslint-typegen#readme", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/antfu/eslint-typegen.git" 14 | }, 15 | "bugs": "https://github.com/antfu/eslint-typegen/issues", 16 | "keywords": [ 17 | "eslint" 18 | ], 19 | "sideEffects": false, 20 | "exports": { 21 | ".": "./dist/index.mjs", 22 | "./core": "./dist/core.mjs" 23 | }, 24 | "main": "./dist/index.mjs", 25 | "module": "./dist/index.mjs", 26 | "types": "./dist/index.d.mts", 27 | "files": [ 28 | "dist" 29 | ], 30 | "scripts": { 31 | "build": "unbuild", 32 | "dev": "unbuild --stub", 33 | "lint": "nr build && eslint .", 34 | "prepublishOnly": "nr build", 35 | "release": "bumpp && pnpm publish", 36 | "start": "tsx src/index.ts", 37 | "test": "vitest", 38 | "typecheck": "tsc --noEmit", 39 | "prepare": "simple-git-hooks" 40 | }, 41 | "peerDependencies": { 42 | "eslint": "^9.0.0" 43 | }, 44 | "dependencies": { 45 | "json-schema-to-typescript-lite": "catalog:prod", 46 | "ohash": "catalog:prod" 47 | }, 48 | "devDependencies": { 49 | "@antfu/eslint-config": "catalog:dev", 50 | "@antfu/ni": "catalog:dev", 51 | "@types/json-schema": "catalog:dev", 52 | "@types/node": "catalog:dev", 53 | "@typescript-eslint/eslint-plugin": "catalog:test", 54 | "bumpp": "catalog:dev", 55 | "eslint": "catalog:dev", 56 | "eslint-plugin-antfu": "catalog:test", 57 | "eslint-plugin-import-x": "catalog:test", 58 | "eslint-plugin-jsx-a11y": "catalog:test", 59 | "eslint-plugin-you-dont-need-lodash-underscore": "catalog:test", 60 | "lint-staged": "catalog:dev", 61 | "pnpm": "catalog:dev", 62 | "simple-git-hooks": "catalog:dev", 63 | "tsx": "catalog:dev", 64 | "typescript": "catalog:dev", 65 | "unbuild": "catalog:dev", 66 | "vite": "catalog:dev", 67 | "vitest": "catalog:test" 68 | }, 69 | "simple-git-hooks": { 70 | "pre-commit": "pnpm lint-staged" 71 | }, 72 | "lint-staged": { 73 | "*": "eslint --fix" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { vue } from '@antfu/eslint-config' 2 | import { expect, it } from 'vitest' 3 | import { flatConfigsToRulesDTS, pluginsToRulesDTS } from '../src/core' 4 | import { invalidJsonSchemaPlugin } from './input/invalid-json-schema-plugin' 5 | import { pluginWithSchemaIds } from './input/plugin-with-schema-ids' 6 | 7 | it('pluginsToRuleOptions', async () => { 8 | await expect(await pluginsToRulesDTS({ 9 | // @ts-expect-error missing types 10 | import: await import('eslint-plugin-import-x').then(m => m.default), 11 | antfu: await import('eslint-plugin-antfu').then(m => m.default), 12 | })) 13 | .toMatchFileSnapshot('./output/plugins.d.ts') 14 | }) 15 | 16 | it('pluginsToRuleOptions ts expect no warnings', async () => { 17 | await pluginsToRulesDTS({ 18 | // @ts-expect-error missing types 19 | ts: await import('@typescript-eslint/eslint-plugin').then(m => m.default), 20 | }) 21 | }) 22 | 23 | it('core rules', async () => { 24 | const { builtinRules } = await import('eslint/use-at-your-own-risk') 25 | 26 | await expect(await pluginsToRulesDTS({ 27 | '': { rules: Object.fromEntries(builtinRules.entries()) }, 28 | })) 29 | .toMatchFileSnapshot('./output/core-rules.d.ts') 30 | }) 31 | 32 | it('invalid JSON schema plugin', async () => { 33 | await expect(await pluginsToRulesDTS({ 34 | 'invalid-json-schema-plugin': invalidJsonSchemaPlugin, 35 | })) 36 | .toMatchFileSnapshot('./output/invalid-json-schema-plugin.d.ts') 37 | }) 38 | 39 | it('json schema with ids', async () => { 40 | await expect(await pluginsToRulesDTS({ 41 | plugin: pluginWithSchemaIds, 42 | })) 43 | .toMatchFileSnapshot('./output/plugin-with-schema-ids.d.ts') 44 | }) 45 | 46 | it('flatConfigsToRuleOptions', async () => { 47 | await expect(await flatConfigsToRulesDTS(await vue() as any)) 48 | .toMatchFileSnapshot('./output/flat-config-vue.d.ts') 49 | }) 50 | 51 | it('jsx-a11y', async () => { 52 | await expect(await pluginsToRulesDTS({ 53 | // @ts-expect-error missing types 54 | 'jsx-a11y': await import('eslint-plugin-jsx-a11y').then(m => m.default), 55 | })) 56 | .toMatchFileSnapshot('./output/jsx-a11y.d.ts') 57 | }) 58 | 59 | it('no rule `meta`', async () => { 60 | await expect(await pluginsToRulesDTS({ 61 | // @ts-expect-error missing types 62 | 'you-dont-need-lodash-underscore': await import('eslint-plugin-you-dont-need-lodash-underscore').then(m => m.default), 63 | })) 64 | .toMatchFileSnapshot('./output/no-rule-meta.d.ts') 65 | }) 66 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Linter } from 'eslint' 2 | import type { FlatConfigsToRulesOptions } from './core' 3 | import { existsSync } from 'node:fs' 4 | import fs from 'node:fs/promises' 5 | import { hash as makeHash } from 'ohash' 6 | import { version } from '../package.json' 7 | import { flatConfigsToPlugins, pluginsToRulesDTS } from './core' 8 | 9 | export interface TypeGenOptions extends FlatConfigsToRulesOptions { 10 | /** 11 | * Include core rules in the generated types. 12 | * 13 | * @default true 14 | */ 15 | includeCoreRules?: boolean 16 | 17 | /** 18 | * Path to the generated types file. 19 | */ 20 | dtsPath?: string 21 | } 22 | 23 | /** 24 | * Wrap with resolved flat configs to generates types for rules. 25 | */ 26 | export default async function typegen( 27 | configs: Promise | Linter.Config[], 28 | options: TypeGenOptions = {}, 29 | ): Promise { 30 | const { 31 | includeCoreRules = true, 32 | dtsPath = 'eslint-typegen.d.ts', 33 | } = options 34 | 35 | const resolved = await configs 36 | let configsInput = resolved 37 | 38 | if (includeCoreRules) { 39 | const { builtinRules } = await import('eslint/use-at-your-own-risk') 40 | configsInput = [ 41 | { 42 | plugins: { 43 | '': { rules: Object.fromEntries(builtinRules.entries()) }, 44 | }, 45 | }, 46 | ...configsInput, 47 | ] 48 | } 49 | 50 | const plugins = await flatConfigsToPlugins(configsInput, options) 51 | const configNames = configsInput.flatMap(c => c.name).filter(Boolean) as string[] 52 | const hashSource = [ 53 | // version of eslint-typegen 54 | version, 55 | // plugins name and version 56 | ...Object.entries(plugins) 57 | .map(([n, p]) => [p.meta?.name, p.meta?.version].filter(Boolean).join('@') || p.name || n) 58 | .sort(), 59 | // config names 60 | ...configNames, 61 | ].join(' ') 62 | const hash = makeHash(hashSource) 63 | 64 | const previousHash = existsSync(dtsPath) 65 | ? (await fs.readFile(dtsPath, 'utf-8')).match(/\/\* eslint-typegen-hash: (\S*) \*\//)?.[1]?.trim() 66 | : undefined 67 | 68 | if (previousHash !== hash) { 69 | const dts = [ 70 | '/* This file is generated by eslint-typegen, for augmenting rules types in ESLint */', 71 | '/* You might want to include this file in tsconfig.json but excluded from git */', 72 | `/* eslint-typegen-hash: ${hash} */`, 73 | '', 74 | await pluginsToRulesDTS(plugins, { 75 | ...options, 76 | configNames, 77 | }), 78 | ].join('\n') 79 | 80 | fs.writeFile(dtsPath, dts, 'utf-8') 81 | } 82 | 83 | return resolved 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eslint-typegen 2 | 3 | [![npm version][npm-version-src]][npm-version-href] 4 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 5 | [![bundle][bundle-src]][bundle-href] 6 | [![JSDocs][jsdocs-src]][jsdocs-href] 7 | [![License][license-src]][license-href] 8 | 9 | Generate types from ESLint rule schemas automatically, with auto-completion and type-checking for rule options. 10 | 11 | ![](https://github.com/antfu/eslint-typegen/assets/11247099/642ffdc0-c662-4f3b-9237-16d776be1c3e) 12 | 13 | > Btw, if you are using [`@antfu/eslint-config`](https://github.com/antfu/eslint-config), you may NOT need to setup this, as the types for rules [are already shipped with the package](https://github.com/antfu/eslint-config/blob/95963ac345d27cd06a4eeb5ebc16e1848cb2fd81/scripts/typegen.ts#L29-L33). 14 | 15 | ## Usage 16 | 17 | ```bash 18 | npm i eslint-typegen 19 | ``` 20 | 21 | In your `eslint.config.mjs`, wrap the export with `typegen` function: 22 | 23 | ```ts 24 | // @ts-check 25 | /// 26 | import typegen from 'eslint-typegen' 27 | 28 | export default typegen( 29 | [ 30 | // ...your normal eslint flat config 31 | ] 32 | ) 33 | ``` 34 | 35 | Run ESLint once, an `eslint-typegen.d.ts` file will be generated to augment ESLint's `Linter.RulesRecord` types, to provide you with auto-completion and type-checking for your ESLint rules configuration based on the ESLint plugins you are using. 36 | 37 | > It will caluclate the hash of the plugins meta from your flat config, and only regenerate the types when they changes. If you want to force regenerate the types, you can delete the `eslint-typegen.d.ts` file and run ESLint again. 38 | 39 | ## Low-level API 40 | 41 | You can find low-level APIs in the `eslint-typegen/core` modules. 42 | 43 | ```ts 44 | import fs from 'node:fs/promises' 45 | import pluginTs from '@typescript-eslint/eslint-plugin' 46 | import pluginN from 'eslint-plugin-n' 47 | import { pluginsToRulesDTS } from 'eslint-typegen/core' 48 | 49 | const dts = await pluginsToRulesDTS({ 50 | '@typescript-eslint': pluginTs, 51 | 'n': pluginN, 52 | }) 53 | 54 | await fs.writeFile('eslint-typegen.d.ts', dts) 55 | ``` 56 | 57 | This can be useful if you want to have more control over the generation process, or even bundle the types with your config package. For example, [here](https://github.com/antfu/eslint-config/blob/95963ac345d27cd06a4eeb5ebc16e1848cb2fd81/scripts/typegen.ts#L29-L33) is how [`@antfu/eslint-config`](https://github.com/antfu/eslint-config) does. 58 | 59 | ## How it works 60 | 61 | Thanks to that [ESLint requires rules to provide the JSONSchema for options](https://eslint.org/docs/latest/extend/custom-rules#options-schemas), we could leverage that to generate types with [`json-schema-to-typescript`](https://github.com/bcherny/json-schema-to-typescript). With the new flat config allowing you to execute the code with fully resolved configs, we managed to sneak in the type generation process on the fly. 62 | 63 | ## Credits 64 | 65 | The initial idea comes from [@Shinigami92](https://github.com/Shinigami92) via his work on [`eslint-define-config`](https://github.com/eslint-types/eslint-define-config), thanks to him! 66 | 67 | ## Sponsors 68 | 69 |

70 | 71 | 72 | 73 |

74 | 75 | ## License 76 | 77 | [MIT](./LICENSE) License © 2023-PRESENT [Anthony Fu](https://github.com/antfu) 78 | 79 | 80 | 81 | [npm-version-src]: https://img.shields.io/npm/v/eslint-typegen?style=flat&colorA=080f12&colorB=1fa669 82 | [npm-version-href]: https://npmjs.com/package/eslint-typegen 83 | [npm-downloads-src]: https://img.shields.io/npm/dm/eslint-typegen?style=flat&colorA=080f12&colorB=1fa669 84 | [npm-downloads-href]: https://npmjs.com/package/eslint-typegen 85 | [bundle-src]: https://img.shields.io/bundlephobia/minzip/eslint-typegen?style=flat&colorA=080f12&colorB=1fa669&label=minzip 86 | [bundle-href]: https://bundlephobia.com/result?p=eslint-typegen 87 | [license-src]: https://img.shields.io/github/license/antfu/eslint-typegen.svg?style=flat&colorA=080f12&colorB=1fa669 88 | [license-href]: https://github.com/antfu/eslint-typegen/blob/main/LICENSE 89 | [jsdocs-src]: https://img.shields.io/badge/jsdocs-reference-080f12?style=flat&colorA=080f12&colorB=1fa669 90 | [jsdocs-href]: https://www.jsdocs.io/package/eslint-typegen 91 | -------------------------------------------------------------------------------- /test/output/no-rule-meta.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | import type { Linter } from 'eslint' 4 | 5 | declare module 'eslint' { 6 | namespace Linter { 7 | interface RulesRecord extends RuleOptions {} 8 | } 9 | } 10 | 11 | export interface RuleOptions { 12 | 'you-dont-need-lodash-underscore/all'?: Linter.RuleEntry<[]> 13 | 'you-dont-need-lodash-underscore/any'?: Linter.RuleEntry<[]> 14 | 'you-dont-need-lodash-underscore/assign'?: Linter.RuleEntry<[]> 15 | 'you-dont-need-lodash-underscore/bind'?: Linter.RuleEntry<[]> 16 | 'you-dont-need-lodash-underscore/capitalize'?: Linter.RuleEntry<[]> 17 | 'you-dont-need-lodash-underscore/cast-array'?: Linter.RuleEntry<[]> 18 | 'you-dont-need-lodash-underscore/clone-deep'?: Linter.RuleEntry<[]> 19 | 'you-dont-need-lodash-underscore/collect'?: Linter.RuleEntry<[]> 20 | 'you-dont-need-lodash-underscore/concat'?: Linter.RuleEntry<[]> 21 | 'you-dont-need-lodash-underscore/contains'?: Linter.RuleEntry<[]> 22 | 'you-dont-need-lodash-underscore/defaults'?: Linter.RuleEntry<[]> 23 | 'you-dont-need-lodash-underscore/detect'?: Linter.RuleEntry<[]> 24 | 'you-dont-need-lodash-underscore/drop'?: Linter.RuleEntry<[]> 25 | 'you-dont-need-lodash-underscore/drop-right'?: Linter.RuleEntry<[]> 26 | 'you-dont-need-lodash-underscore/each'?: Linter.RuleEntry<[]> 27 | 'you-dont-need-lodash-underscore/ends-with'?: Linter.RuleEntry<[]> 28 | 'you-dont-need-lodash-underscore/entries'?: Linter.RuleEntry<[]> 29 | 'you-dont-need-lodash-underscore/every'?: Linter.RuleEntry<[]> 30 | 'you-dont-need-lodash-underscore/extend-own'?: Linter.RuleEntry<[]> 31 | 'you-dont-need-lodash-underscore/fill'?: Linter.RuleEntry<[]> 32 | 'you-dont-need-lodash-underscore/filter'?: Linter.RuleEntry<[]> 33 | 'you-dont-need-lodash-underscore/find'?: Linter.RuleEntry<[]> 34 | 'you-dont-need-lodash-underscore/find-index'?: Linter.RuleEntry<[]> 35 | 'you-dont-need-lodash-underscore/first'?: Linter.RuleEntry<[]> 36 | 'you-dont-need-lodash-underscore/flatten'?: Linter.RuleEntry<[]> 37 | 'you-dont-need-lodash-underscore/foldl'?: Linter.RuleEntry<[]> 38 | 'you-dont-need-lodash-underscore/foldr'?: Linter.RuleEntry<[]> 39 | 'you-dont-need-lodash-underscore/for-each'?: Linter.RuleEntry<[]> 40 | 'you-dont-need-lodash-underscore/get'?: Linter.RuleEntry<[]> 41 | 'you-dont-need-lodash-underscore/head'?: Linter.RuleEntry<[]> 42 | 'you-dont-need-lodash-underscore/includes'?: Linter.RuleEntry<[]> 43 | 'you-dont-need-lodash-underscore/index-of'?: Linter.RuleEntry<[]> 44 | 'you-dont-need-lodash-underscore/inject'?: Linter.RuleEntry<[]> 45 | 'you-dont-need-lodash-underscore/is-array'?: Linter.RuleEntry<[]> 46 | 'you-dont-need-lodash-underscore/is-array-buffer'?: Linter.RuleEntry<[]> 47 | 'you-dont-need-lodash-underscore/is-date'?: Linter.RuleEntry<[]> 48 | 'you-dont-need-lodash-underscore/is-finite'?: Linter.RuleEntry<[]> 49 | 'you-dont-need-lodash-underscore/is-function'?: Linter.RuleEntry<[]> 50 | 'you-dont-need-lodash-underscore/is-integer'?: Linter.RuleEntry<[]> 51 | 'you-dont-need-lodash-underscore/is-nan'?: Linter.RuleEntry<[]> 52 | 'you-dont-need-lodash-underscore/is-nil'?: Linter.RuleEntry<[]> 53 | 'you-dont-need-lodash-underscore/is-null'?: Linter.RuleEntry<[]> 54 | 'you-dont-need-lodash-underscore/is-string'?: Linter.RuleEntry<[]> 55 | 'you-dont-need-lodash-underscore/is-undefined'?: Linter.RuleEntry<[]> 56 | 'you-dont-need-lodash-underscore/join'?: Linter.RuleEntry<[]> 57 | 'you-dont-need-lodash-underscore/keys'?: Linter.RuleEntry<[]> 58 | 'you-dont-need-lodash-underscore/last'?: Linter.RuleEntry<[]> 59 | 'you-dont-need-lodash-underscore/last-index-of'?: Linter.RuleEntry<[]> 60 | 'you-dont-need-lodash-underscore/map'?: Linter.RuleEntry<[]> 61 | 'you-dont-need-lodash-underscore/omit'?: Linter.RuleEntry<[]> 62 | 'you-dont-need-lodash-underscore/pad-end'?: Linter.RuleEntry<[]> 63 | 'you-dont-need-lodash-underscore/pad-start'?: Linter.RuleEntry<[]> 64 | 'you-dont-need-lodash-underscore/pairs'?: Linter.RuleEntry<[]> 65 | 'you-dont-need-lodash-underscore/reduce'?: Linter.RuleEntry<[]> 66 | 'you-dont-need-lodash-underscore/reduce-right'?: Linter.RuleEntry<[]> 67 | 'you-dont-need-lodash-underscore/repeat'?: Linter.RuleEntry<[]> 68 | 'you-dont-need-lodash-underscore/replace'?: Linter.RuleEntry<[]> 69 | 'you-dont-need-lodash-underscore/reverse'?: Linter.RuleEntry<[]> 70 | 'you-dont-need-lodash-underscore/select'?: Linter.RuleEntry<[]> 71 | 'you-dont-need-lodash-underscore/size'?: Linter.RuleEntry<[]> 72 | 'you-dont-need-lodash-underscore/slice'?: Linter.RuleEntry<[]> 73 | 'you-dont-need-lodash-underscore/some'?: Linter.RuleEntry<[]> 74 | 'you-dont-need-lodash-underscore/split'?: Linter.RuleEntry<[]> 75 | 'you-dont-need-lodash-underscore/starts-with'?: Linter.RuleEntry<[]> 76 | 'you-dont-need-lodash-underscore/take-right'?: Linter.RuleEntry<[]> 77 | 'you-dont-need-lodash-underscore/throttle'?: Linter.RuleEntry<[]> 78 | 'you-dont-need-lodash-underscore/to-lower'?: Linter.RuleEntry<[]> 79 | 'you-dont-need-lodash-underscore/to-pairs'?: Linter.RuleEntry<[]> 80 | 'you-dont-need-lodash-underscore/to-upper'?: Linter.RuleEntry<[]> 81 | 'you-dont-need-lodash-underscore/trim'?: Linter.RuleEntry<[]> 82 | 'you-dont-need-lodash-underscore/union-by'?: Linter.RuleEntry<[]> 83 | 'you-dont-need-lodash-underscore/uniq'?: Linter.RuleEntry<[]> 84 | 'you-dont-need-lodash-underscore/values'?: Linter.RuleEntry<[]> 85 | } 86 | 87 | /* ======= Declarations ======= */ 88 | -------------------------------------------------------------------------------- /src/core.ts: -------------------------------------------------------------------------------- 1 | import type { ESLint, Linter, Rule } from 'eslint' 2 | import type { JSONSchema4 } from 'json-schema' 3 | import type { Options as CompileOptions } from 'json-schema-to-typescript-lite' 4 | import { compile as compileSchema, normalizeIdentifier } from 'json-schema-to-typescript-lite' 5 | 6 | export interface RulesTypeGenOptions { 7 | /** 8 | * Insert type imports for the generated types. 9 | * 10 | * @default true 11 | */ 12 | includeTypeImports?: boolean 13 | 14 | /** 15 | * Include comments to disable ESLint and Prettier. 16 | * 17 | * @default true 18 | */ 19 | includeIgnoreComments?: boolean 20 | 21 | /** 22 | * Augment the interface to ESLint's `Linter.RulesRecord`. 23 | * 24 | * @default true 25 | */ 26 | includeAugmentation?: boolean 27 | 28 | /** 29 | * Augment the `DefaultConfigNamesMap` interface for `eslint-flat-config-utils` 30 | * For auto-completion of config names etc. 31 | * 32 | * @see https://github.com/antfu/eslint-flat-config-utils 33 | * @default false 34 | */ 35 | augmentFlatConfigUtils?: boolean 36 | 37 | /** 38 | * The name of the exported type. 39 | * 40 | * @default 'RuleOptions' 41 | */ 42 | exportTypeName?: string 43 | 44 | /** 45 | * Options for json-schema-to-typescript 46 | */ 47 | compileOptions?: Partial 48 | } 49 | 50 | export interface FlatConfigsToPluginsOptions { 51 | filterConfig?: (config: Linter.Config) => boolean 52 | filterPlugin?: (name: string, plugin: ESLint.Plugin) => boolean 53 | } 54 | 55 | export interface FlatConfigsToRulesOptions 56 | extends RulesTypeGenOptions, FlatConfigsToPluginsOptions { 57 | } 58 | 59 | export async function flatConfigsToPlugins( 60 | configs: Linter.Config[], 61 | options: FlatConfigsToPluginsOptions = {}, 62 | ) { 63 | const plugins: Record = {} 64 | for (const config of configs) { 65 | if (!config.plugins) 66 | continue 67 | if (options.filterConfig?.(config) === false) 68 | continue 69 | for (const [name, plugin] of Object.entries(config.plugins)) { 70 | if (options.filterPlugin?.(name, plugin) === false) 71 | continue 72 | plugins[name] = plugin 73 | } 74 | } 75 | return plugins 76 | } 77 | 78 | /** 79 | * Generate types for rules from an array of ESLint configurations. 80 | */ 81 | export async function flatConfigsToRulesDTS( 82 | configs: Linter.Config[], 83 | options: FlatConfigsToRulesOptions = {}, 84 | ) { 85 | return pluginsToRulesDTS( 86 | await flatConfigsToPlugins(configs, options), 87 | options, 88 | ) 89 | } 90 | 91 | /** 92 | * Generate types for rule from an object of ESLint plugins. 93 | */ 94 | export async function pluginsToRulesDTS( 95 | plugins: Record, 96 | options: RulesTypeGenOptions & { configNames?: string[] } = {}, 97 | ) { 98 | const { 99 | includeTypeImports = true, 100 | includeIgnoreComments = true, 101 | includeAugmentation = true, 102 | augmentFlatConfigUtils = false, 103 | exportTypeName = 'RuleOptions', 104 | compileOptions = {}, 105 | configNames = [], 106 | } = options 107 | 108 | const rules: [name: string, rule: Rule.RuleModule][] = [] 109 | 110 | for (const [pluginName, plugin] of Object.entries(plugins)) { 111 | for (const [ruleName, rule] of Object.entries(plugin.rules || {})) { 112 | if (rule?.meta == null || typeof rule.meta === 'object') { 113 | rules.push([ 114 | pluginName 115 | ? `${pluginName}/${ruleName}` 116 | : ruleName, 117 | rule, 118 | ]) 119 | } 120 | } 121 | } 122 | 123 | rules.sort(([a], [b]) => a.localeCompare(b)) 124 | const resolved = await Promise.all(rules 125 | .map(([name, rule]) => compileRule(name, rule, compileOptions))) 126 | 127 | const exports = [ 128 | ...(includeIgnoreComments 129 | ? [ 130 | '/* eslint-disable */', 131 | '/* prettier-ignore */', 132 | ] 133 | : []), 134 | ...(includeTypeImports 135 | ? [ 136 | 'import type { Linter } from \'eslint\'', 137 | ] 138 | : []), 139 | ...(includeAugmentation 140 | ? [ 141 | '', 142 | `declare module 'eslint' {`, 143 | ` namespace Linter {`, 144 | ` interface RulesRecord extends ${exportTypeName} {}`, 145 | ` }`, 146 | `}`, 147 | ] 148 | : []), 149 | ...(augmentFlatConfigUtils && configNames.length 150 | ? [ 151 | '', 152 | '// @ts-ignore - In case the package is not installed', 153 | `declare module 'eslint-flat-config-utils' {`, 154 | ` interface DefaultConfigNamesMap {`, 155 | ...configNames.map(name => ` '${name}'?: true,`), 156 | ` }`, 157 | `}`, 158 | ] 159 | : []), 160 | '', 161 | `export interface ${exportTypeName} {`, 162 | ...resolved.flatMap(({ typeName, name, jsdoc }) => [ 163 | jsdoc?.length 164 | ? ` /**\n${jsdoc.map(i => ` * ${i}`).join('\n').replaceAll(/\*\//g, '*\\/')}\n */` 165 | : undefined, 166 | ` '${name}'?: Linter.RuleEntry<${typeName}>`, 167 | ]).filter(Boolean), 168 | `}`, 169 | ] 170 | const typeDeclarations = resolved.flatMap(({ typeDeclarations }) => typeDeclarations).join('\n') 171 | 172 | return [ 173 | ...exports, 174 | '', 175 | '/* ======= Declarations ======= */', 176 | typeDeclarations, 177 | ].join('\n') 178 | } 179 | 180 | export async function compileRule( 181 | ruleName: string, 182 | rule: Rule.RuleModule, 183 | compileOptions: Partial = {}, 184 | ) { 185 | const meta = rule.meta ?? {} 186 | let schemas = meta.schema as JSONSchema4[] ?? [] 187 | if (!Array.isArray(schemas)) 188 | schemas = [schemas] 189 | 190 | const id = normalizeIdentifier(ruleName) 191 | 192 | const jsdoc: string[] = [] 193 | if (meta.docs?.description) 194 | jsdoc.push(meta.docs.description) 195 | if (meta.docs?.url) 196 | jsdoc.push(`@see ${meta.docs.url}`) 197 | if (meta.deprecated) 198 | jsdoc.push('@deprecated') 199 | 200 | if (!meta.schema || !schemas.length) { 201 | return { 202 | jsdoc, 203 | name: ruleName, 204 | typeName: '[]', 205 | typeDeclarations: [], 206 | } 207 | } 208 | 209 | let lines: string[] = [] 210 | 211 | const schema: JSONSchema4 = Array.isArray(meta.schema) 212 | ? { type: 'array', items: meta.schema, definitions: meta.schema?.[0]?.definitions } 213 | : meta.schema 214 | 215 | let overriddenRootSchemaId: string | undefined 216 | let isRootSchema = true 217 | 218 | try { 219 | const compiled = await compileSchema(schema, id, { 220 | unreachableDefinitions: false, 221 | strictIndexSignatures: true, 222 | customName(schema, keyName) { 223 | const canOverrideRootSchemaId = isRootSchema 224 | isRootSchema = false 225 | 226 | const resolved = schema.title || schema.$id || keyName 227 | if (resolved === id) 228 | return id 229 | if (!resolved) 230 | return undefined! 231 | 232 | const normalizedName = `_${normalizeIdentifier(`${id}_${resolved}`)}` 233 | if (canOverrideRootSchemaId) 234 | overriddenRootSchemaId = normalizedName 235 | 236 | return normalizedName 237 | }, 238 | ...compileOptions, 239 | }) 240 | lines.push( 241 | compiled 242 | .replace(/\/\*[\s\S]*?\*\//g, ''), 243 | ) 244 | } 245 | catch (error) { 246 | console.warn(`Failed to compile schema ${ruleName} for rule ${ruleName}. Falling back to unknown.`) 247 | console.error(error) 248 | lines.push(`export type ${id} = unknown\n`) 249 | } 250 | 251 | lines = lines 252 | .join('\n') 253 | .split('\n') 254 | .map(line => line.replace(/^(export )/, '')) 255 | .filter(Boolean) 256 | 257 | lines.unshift(`// ----- ${ruleName} -----`) 258 | 259 | return { 260 | name: ruleName, 261 | jsdoc, 262 | typeName: overriddenRootSchemaId ?? id, 263 | typeDeclarations: lines, 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /test/output/jsx-a11y.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | import type { Linter } from 'eslint' 4 | 5 | declare module 'eslint' { 6 | namespace Linter { 7 | interface RulesRecord extends RuleOptions {} 8 | } 9 | } 10 | 11 | export interface RuleOptions { 12 | /** 13 | * Enforce emojis are wrapped in `` and provide screen reader access. 14 | * @see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/accessible-emoji.md 15 | * @deprecated 16 | */ 17 | 'jsx-a11y/accessible-emoji'?: Linter.RuleEntry 18 | /** 19 | * Enforce all elements that require alternative text have meaningful information to relay back to end user. 20 | * @see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/alt-text.md 21 | */ 22 | 'jsx-a11y/alt-text'?: Linter.RuleEntry 23 | /** 24 | * Enforce `` text to not exactly match "click here", "here", "link", or "a link". 25 | * @see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/anchor-ambiguous-text.md 26 | */ 27 | 'jsx-a11y/anchor-ambiguous-text'?: Linter.RuleEntry 28 | /** 29 | * Enforce all anchors to contain accessible content. 30 | * @see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/anchor-has-content.md 31 | */ 32 | 'jsx-a11y/anchor-has-content'?: Linter.RuleEntry 33 | /** 34 | * Enforce all anchors are valid, navigable elements. 35 | * @see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/anchor-is-valid.md 36 | */ 37 | 'jsx-a11y/anchor-is-valid'?: Linter.RuleEntry 38 | /** 39 | * Enforce elements with aria-activedescendant are tabbable. 40 | * @see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/aria-activedescendant-has-tabindex.md 41 | */ 42 | 'jsx-a11y/aria-activedescendant-has-tabindex'?: Linter.RuleEntry 43 | /** 44 | * Enforce all `aria-*` props are valid. 45 | * @see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/aria-props.md 46 | */ 47 | 'jsx-a11y/aria-props'?: Linter.RuleEntry 48 | /** 49 | * Enforce ARIA state and property values are valid. 50 | * @see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/aria-proptypes.md 51 | */ 52 | 'jsx-a11y/aria-proptypes'?: Linter.RuleEntry 53 | /** 54 | * Enforce that elements with ARIA roles must use a valid, non-abstract ARIA role. 55 | * @see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/aria-role.md 56 | */ 57 | 'jsx-a11y/aria-role'?: Linter.RuleEntry 58 | /** 59 | * Enforce that elements that do not support ARIA roles, states, and properties do not have those attributes. 60 | * @see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/aria-unsupported-elements.md 61 | */ 62 | 'jsx-a11y/aria-unsupported-elements'?: Linter.RuleEntry 63 | /** 64 | * Enforce that autocomplete attributes are used correctly. 65 | * @see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/autocomplete-valid.md 66 | */ 67 | 'jsx-a11y/autocomplete-valid'?: Linter.RuleEntry 68 | /** 69 | * Enforce a clickable non-interactive element has at least one keyboard event listener. 70 | * @see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/click-events-have-key-events.md 71 | */ 72 | 'jsx-a11y/click-events-have-key-events'?: Linter.RuleEntry 73 | /** 74 | * Enforce that a control (an interactive element) has a text label. 75 | * @see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/control-has-associated-label.md 76 | */ 77 | 'jsx-a11y/control-has-associated-label'?: Linter.RuleEntry 78 | /** 79 | * Enforce heading (`h1`, `h2`, etc) elements contain accessible content. 80 | * @see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/heading-has-content.md 81 | */ 82 | 'jsx-a11y/heading-has-content'?: Linter.RuleEntry 83 | /** 84 | * Enforce `` element has `lang` prop. 85 | * @see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/html-has-lang.md 86 | */ 87 | 'jsx-a11y/html-has-lang'?: Linter.RuleEntry 88 | /** 89 | * Enforce iframe elements have a title attribute. 90 | * @see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/iframe-has-title.md 91 | */ 92 | 'jsx-a11y/iframe-has-title'?: Linter.RuleEntry 93 | /** 94 | * Enforce `` alt prop does not contain the word "image", "picture", or "photo". 95 | * @see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/img-redundant-alt.md 96 | */ 97 | 'jsx-a11y/img-redundant-alt'?: Linter.RuleEntry 98 | /** 99 | * Enforce that elements with interactive handlers like `onClick` must be focusable. 100 | * @see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/interactive-supports-focus.md 101 | */ 102 | 'jsx-a11y/interactive-supports-focus'?: Linter.RuleEntry 103 | /** 104 | * Enforce that a `label` tag has a text label and an associated control. 105 | * @see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/label-has-associated-control.md 106 | */ 107 | 'jsx-a11y/label-has-associated-control'?: Linter.RuleEntry 108 | /** 109 | * Enforce that `