Compare commits

..

48 Commits

Author SHA1 Message Date
c38449c759 Bump 2025-06-12 21:59:23 -04:00
d7e9b986c7 Bump 2025-06-07 20:32:51 -04:00
d06cd4937c Bump deps 2025-05-24 17:38:09 -04:00
a746e2fbc8 Bump version 2025-05-10 19:13:52 -04:00
9b4a6cb498 Bump deps 2025-04-27 01:01:23 -04:00
d7990e5cf1 Update 2025-02-26 14:00:41 -05:00
cddee19340 Bump 2025-02-05 18:55:12 -05:00
c2d582dea0 Update 2025-01-25 19:44:09 -05:00
8824c2166d Bump 2024-12-28 18:34:57 -05:00
51df8a9a2c Bump 2024-12-15 21:38:43 -05:00
1e56001e78 New version 2024-12-15 02:30:34 -05:00
2b4c3038b3 Update 2024-12-12 19:48:39 -05:00
eb5f72a049 Bump 2024-12-08 19:14:11 -05:00
952914699b Bump version and fix glob 2024-12-01 17:51:52 -05:00
78ed00fd21 Bump version 2024-11-28 23:27:52 -05:00
43d9cc294e add default 2024-11-23 17:04:14 -05:00
235cf4101b Bump 2024-11-23 16:50:52 -05:00
f36e47f144 Update 2024-11-17 19:58:37 -05:00
97eb90f6c6 Bump 2024-11-09 21:39:15 -05:00
a1ab2ad7f5 Update 2024-11-03 20:11:26 -05:00
511652dd48 read gitignore 2024-10-20 01:55:55 -04:00
553965243f Merge pull request 'Merge flat into main' (#1) from flat into main
Reviewed-on: #1
2024-10-19 22:51:03 +00:00
eb366f3b2e Update 2024-10-19 18:50:17 -04:00
00d0dfa107 Upgrade to ESLint 9 2024-10-16 00:29:26 -04:00
0138cabb27 Update rules 2024-08-25 16:28:34 -04:00
b0cc4a1525 Update rules 2024-08-13 03:37:00 -04:00
191848fdca Update 2024-08-11 17:32:54 -04:00
afb93c4971 Fix local rules 2024-08-08 02:59:28 -04:00
25695599aa Update 2024-08-04 21:59:18 -04:00
f3fbf99c0c Update 2024-08-03 23:20:22 -04:00
92e6e5081b chore 2024-08-03 21:00:40 -04:00
e5546e21ad Update 2024-08-02 23:04:37 -04:00
ba20685f2e Bump 2024-08-02 15:51:10 -04:00
3ac5f91988 chore 2024-07-30 23:00:25 -04:00
2853da4344 Update 2024-07-27 22:45:56 -04:00
1e6d679af3 Bump version 2024-06-27 01:19:33 -04:00
674eaf1811 Remove import 2024-06-16 04:50:37 -04:00
c175f87441 Update 2024-05-04 20:44:31 -04:00
003a43512c New rules 2024-04-19 21:54:02 -04:00
fb50ede688 Update 2024-04-17 00:43:45 -04:00
51455e3c21 Update 2024-04-16 22:29:31 -04:00
1d955d951d Bump ver 2024-04-06 19:15:05 -04:00
8ba9b0725a Update 2024-04-06 19:05:14 -04:00
4d2762de39 Update 2024-03-30 02:06:38 -04:00
19b07691fc bump 2024-03-14 22:31:04 -04:00
4c67de9f72 Update 2024-03-04 03:37:23 -05:00
9ea078f414 Bump 2024-02-26 03:29:42 -05:00
ba2671d760 New patch 2024-02-03 20:22:42 -05:00
87 changed files with 8127 additions and 6690 deletions

51
.eslint.ts Normal file
View File

@ -0,0 +1,51 @@
import type { FlatESLintConfig } from '@aet/eslint-define-config';
import js from '@eslint/js';
import * as tsParser from '@typescript-eslint/parser';
import importPlugin from 'eslint-plugin-import-x';
import unicorn from 'eslint-plugin-unicorn';
import tsEslint from 'typescript-eslint';
import { importRules } from './src/presets/typescript';
export default [
js.configs.recommended, //
...tsEslint.configs.recommendedTypeChecked,
unicorn.configs['flat/recommended'],
importPlugin.flatConfigs.recommended,
importPlugin.flatConfigs.react,
importPlugin.flatConfigs.typescript,
{
files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'],
languageOptions: {
parserOptions: {
parser: tsParser,
projectService: true,
ecmaVersion: 'latest',
// https://github.com/unjs/jiti/issues/167 import.meta.dirname
tsconfigRootDir: __dirname,
sourceType: 'module',
},
},
settings: {
'import-x/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx', '.mts', '.cts'],
},
'import-x/resolver': {
typescript: true,
node: true,
},
},
ignores: ['eslint.config.cjs'],
rules: {
...importRules,
},
},
{
rules: {
'unicorn/prevent-abbreviations': 'off',
'unicorn/import-style': 'off',
'unicorn/switch-case-braces': ['error', 'avoid'],
'unicorn/no-null': 'off',
},
},
] as FlatESLintConfig[];

View File

@ -1,62 +0,0 @@
{
"root": true,
"env": {
"node": true,
"browser": true,
"es6": true
},
"extends": ["eslint:recommended", "prettier"],
"parserOptions": {
"sourceType": "module",
"ecmaVersion": "latest"
},
"rules": {
"no-restricted-imports": [
"warn",
{
"paths": [
"array-includes",
"array.prototype.flat",
"array.prototype.flatmap",
"array.prototype.tosorted",
"object.entries",
"object.fromentries",
"object.hasown",
"object.values",
"string.prototype.matchall",
"has"
]
}
],
"arrow-body-style": ["error", "as-needed"],
"class-methods-use-this": [
"warn",
{ "exceptMethods": ["toString", "shouldComponentUpdate"] }
],
"complexity": ["warn", { "max": 100 }],
"curly": ["error", "multi-line", "consistent"],
"eqeqeq": ["error", "smart"],
"no-async-promise-executor": "off",
"no-case-declarations": "off",
"no-constant-condition": ["error", { "checkLoops": false }],
"no-debugger": "off",
"no-empty": ["error", { "allowEmptyCatch": true }],
"no-inner-declarations": "off",
"no-lonely-if": "error",
"no-template-curly-in-string": "error",
"no-var": "error",
"object-shorthand": ["error", "always", { "ignoreConstructors": true }],
"one-var": ["error", { "var": "never", "let": "never" }],
"prefer-const": ["error", { "destructuring": "all" }],
"prefer-destructuring": [
"warn",
{ "AssignmentExpression": { "array": false, "object": false } }
],
"prefer-rest-params": "warn",
"prefer-spread": "warn",
"quote-props": ["error", "as-needed"],
"spaced-comment": ["error", "always", { "markers": ["/"] }],
"sort-imports": ["warn", { "ignoreDeclarationSort": true }],
"yoda": ["error", "never", { "exceptRange": true }]
}
}

6
.gitignore vendored
View File

@ -1,8 +1,14 @@
drafts
!/packages/eslint-plugin-react-hooks !/packages/eslint-plugin-react-hooks
/packages/eslint-define-config
/react /react
/test
src/types/rules
dist2
dist/**/*.js dist/**/*.js
dist/**/*.js.map dist/**/*.js.map
!dist/default.js
# Logs # Logs
logs logs

1
.npmrc
View File

@ -1,2 +1,3 @@
registry http://raspberrypi.local:4873 registry http://raspberrypi.local:4873
always-auth=true always-auth=true
ignore-scripts=true

View File

@ -1,6 +1,7 @@
{ {
"editor.formatOnSave": true, "editor.formatOnSave": true,
"eslint.runtime": "node", "eslint.runtime": "node",
"eslint.useFlatConfig": true,
"search.exclude": { "search.exclude": {
"**/node_modules": true, "**/node_modules": true,
"**/bower_components": true, "**/bower_components": true,

View File

@ -4,6 +4,12 @@ Personal ESLint config. Guaranteed to have no useless polyfills.
## flat config support ## flat config support
- ❌ [import](https://github.com/import-js/eslint-plugin-import/issues/2556)
- ✅ [react](https://github.com/jsx-eslint/eslint-plugin-react/pull/3429) - ✅ [react](https://github.com/jsx-eslint/eslint-plugin-react/pull/3429)
- ⏱️ [a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/pull/891) - [unicorn](https://github.com/sindresorhus/eslint-plugin-unicorn/pull/1886)
- ❌ [import](https://github.com/un-ts/eslint-plugin-import-x/issues/29)
- ❌ [jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/issues/978, supports flat config)
| Name | Flat Config | Issue |
| ------- | ----------- | ---------------------------------------------------------------------------------------------------------- |
| react | ✅ | [jsx-eslint/eslint-plugin-react#3429](https://github.com/jsx-eslint/eslint-plugin-react/pull/3429) |
| unicorn | ✅ | [sindresorhus/eslint-plugin-unicorn#1886](https://github.com/sindresorhus/eslint-plugin-unicorn/pull/1886) |

64
dist/config/index.d.ts vendored Normal file
View File

@ -0,0 +1,64 @@
import type { FlatESLintConfig } from '@aet/eslint-define-config';
import type { Linter } from 'eslint';
type MiddlewareResult = Linter.Config | Linter.Config[];
export type Middleware =
| (() => Promise<MiddlewareResult>)
| (() => Promise<{ default: MiddlewareResult }>);
export type Environment =
| 'jsdoc'
| 'lingui'
| 'react'
| 'reactQuery'
| 'reactRefresh'
| 'storybook'
| 'tailwind'
| 'testingLibrary'
| 'vitest';
export interface NormalizedExtendConfigOptions {
auto?: boolean;
middlewares?: Middleware[];
configs: FlatESLintConfig[];
/**
* Use `.gitignore` file to exclude files from ESLint.
*/
gitignore?: boolean;
env?: {
[key in Environment]?: boolean;
};
}
export type ExtendConfigOptions =
| FlatESLintConfig
| FlatESLintConfig[]
| NormalizedExtendConfigOptions;
/**
* Returns a ESLint config object.
*
* By default, it includes `["@typescript-eslint", "import-x", "prettier", "unicorn"]` configs.
* Additional bundled plugins include:
*
* 1. [`react`](https://github.com/jsx-eslint/eslint-plugin-react#list-of-supported-rules)
* (automatically enables
* [`react-hooks`](https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks))
* 2. [`react-refresh`](https://github.com/ArnaudBarre/eslint-plugin-react-refresh)
* 3. [`jsx-a11y`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y#supported-rules)
* 4. [`unicorn`](https://github.com/sindresorhus/eslint-plugin-unicorn#rules)
* 5. [`n`](https://github.com/eslint-community/eslint-plugin-n#-rules) (Node.js specific,
* requires `minimatch`)
* 6. [`jsdoc`](https://github.com/gajus/eslint-plugin-jsdoc#rules)
*
* Non bundled:
* 1. [`graphql`](https://the-guild.dev/graphql/eslint/rules)
*
* @returns ESLint configuration object.
*/
export function extendConfig(options?: ExtendConfigOptions): Promise<FlatESLintConfig[]>;
export const error = 'error';
export const warn = 'warn';
export const off = 'off';

5
dist/default.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
import type { FlatESLintConfig } from '@aet/eslint-define-config';
declare const _default: FlatESLintConfig[];
export default _default;

3
dist/default.js vendored Normal file
View File

@ -0,0 +1,3 @@
import { extendConfig } from './config/index.js';
export default await extendConfig();

2
dist/eslint-init.sh vendored Normal file
View File

@ -0,0 +1,2 @@
#!/bin/bash
echo 'export { default } from "@aet/eslint-rules/default";' > eslint.config.js

View File

@ -1,14 +0,0 @@
import type { Linter } from 'eslint';
export const rules: Readonly<Linter.RulesRecord>;
export const configs: {
recommended: Linter.BaseConfig;
errors: Linter.BaseConfig;
warnings: Linter.BaseConfig;
'stage-0': Linter.BaseConfig;
react: Linter.BaseConfig;
'react-native': Linter.BaseConfig;
electron: Linter.BaseConfig;
typescript: Linter.BaseConfig;
};

View File

@ -1,8 +0,0 @@
import type { Linter } from 'eslint';
export const rules: Readonly<Linter.RulesRecord>;
export const configs: {
recommended: Linter.BaseConfig;
strict: Linter.BaseConfig;
};

View File

@ -1,12 +0,0 @@
import type { Linter, Rule } from 'eslint';
export const __EXPERIMENTAL__: false;
export const configs: {
recommended: Linter.BaseConfig;
};
export const rules: {
'rules-of-hooks': Rule.RuleModule;
'exhaustive-deps': Rule.RuleModule;
};

View File

@ -1,9 +0,0 @@
import type { Linter } from 'eslint';
export const deprecatedRules: Readonly<Linter.RulesRecord>;
export const configs: {
recommended: Linter.BaseConfig;
all: Linter.BaseConfig;
'jsx-runtime': Linter.BaseConfig;
};

74
dist/index.d.ts vendored
View File

@ -1,74 +0,0 @@
// Generated by dts-bundle-generator v9.2.4
import { ESLintUtils } from '@typescript-eslint/utils';
import { Rule } from 'eslint';
import { ESLintConfig, Rules } from 'eslint-define-config';
export declare const error = "error";
export declare const warn = "warn";
export declare const off = "off";
export type RuleLevel = "error" | "warn" | "off" | 0 | 1 | 2;
export type RuleEntry<Options> = RuleLevel | [
RuleLevel,
Partial<Options>
];
export interface LocalRuleOptions {
/** Bans import from the specifier '.' and '..' and replaces it with '.+/index' */
"rules/no-import-dot": RuleEntry<unknown>;
/**
* Enforce template literal expressions to be of `string` type
* @see [restrict-template-expressions](https://typescript-eslint.io/rules/restrict-template-expressions)
*/
"rules/restrict-template-expressions": RuleEntry<{
allow: string[];
}>;
/** Ban assignment of empty object literals `{}` and replace them with `Object.create(null)` */
"rules/no-empty-object-literal": RuleEntry<unknown>;
}
export type RuleOptions = Rules & Partial<LocalRuleOptions>;
/**
* ESLint Configuration.
* @see [ESLint Configuration](https://eslint.org/docs/latest/user-guide/configuring/)
*/
export type Config = Omit<ESLintConfig, "rules"> & {
/**
* Rules.
* @see [Rules](https://eslint.org/docs/latest/user-guide/configuring/rules)
*/
rules?: RuleOptions;
/**
*/
customRules?: {
rule: () => Promise<{
default: Rule.RuleModule | ESLintUtils.RuleModule<string, unknown[]>;
}>;
options?: RuleLevel;
}[];
};
export declare function defineCustomRule<Options extends readonly unknown[]>(rule: () => Promise<{
default: Rule.RuleModule | ESLintUtils.RuleModule<string, Options>;
}>, options?: Options): {
rule: () => Promise<{
default: Rule.RuleModule | ESLintUtils.RuleModule<string, Options>;
}>;
options: Options | undefined;
};
/**
* Returns a ESLint config object.
*
* By default, it includes `["@typescript-eslint", "import", "prettier"]` configs.
* Additional bundled plugins include:
*
* 1. [react](https://github.com/jsx-eslint/eslint-plugin-react#list-of-supported-rules)
* 2. [react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh)
* 3. [jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y#supported-rules)
* 4. [unicorn](https://github.com/sindresorhus/eslint-plugin-unicorn#rules)
* 5. [n](https://github.com/eslint-community/eslint-plugin-n#-rules) (Node.js specific)
* 6. [jsdoc](https://github.com/gajus/eslint-plugin-jsdoc#rules)
*
* Non bundled:
* 1. [graphql](https://the-guild.dev/graphql/eslint/rules)
*/
export declare function extendConfig({ plugins, settings, rules, extends: _extends, overrides, customRules, ...rest }?: Config): ESLintConfig;
export {};

97
dist/package.json vendored
View File

@ -1,45 +1,74 @@
{ {
"name": "@aet/eslint-rules", "name": "@aet/eslint-rules",
"version": "0.0.12", "version": "2.0.51",
"license": "UNLICENSED", "license": "UNLICENSED",
"type": "module",
"bin": {
"eslint-init": "eslint-init.sh",
"eslint-install": "install.js",
"eslint-print": "print-config.sh"
},
"peerDependencies": { "peerDependencies": {
"eslint": "^8.53.0", "eslint": "^9.15.0",
"typescript": "^5.2.2" "typescript": "^5.7.2"
},
"exports": {
".": "./config/index.js",
"./default": "./default.js",
"./prettier": "./prettier.js",
"./tsconfig": "./tsconfig.json",
"./types": "./types.js"
},
"optionalDependencies": {
"@tanstack/eslint-plugin-query": "^5.62.1"
}, },
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.4.0", "@aet/eslint-define-config": "^0.1.15",
"@types/eslint": "^8.56.2", "@antfu/install-pkg": "^1.1.0",
"@typescript-eslint/eslint-plugin": "^6.18.1", "@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/parser": "^6.18.1", "@eslint-react/eslint-plugin": "1.51.3",
"@typescript-eslint/type-utils": "^6.18.1", "@eslint/js": "^9.28.0",
"@typescript-eslint/utils": "^6.18.1", "@nolyfill/is-core-module": "^1.0.39",
"aria-query": "^5.3.0", "@stylistic/eslint-plugin": "^4.4.1",
"axe-core": "^4.8.3", "@typescript-eslint/eslint-plugin": "^8.34.0",
"axobject-query": "^4.0.0", "@typescript-eslint/parser": "^8.34.0",
"@typescript-eslint/type-utils": "^8.34.0",
"@typescript-eslint/utils": "^8.34.0",
"aria-query": "^5.3.2",
"axe-core": "^4.10.3",
"axobject-query": "4.1.0",
"damerau-levenshtein": "1.0.8", "damerau-levenshtein": "1.0.8",
"debug": "^4.3.4", "debug": "^4.4.1",
"doctrine": "^3.0.0", "doctrine": "^3.0.0",
"emoji-regex": "^10.3.0", "emoji-regex": "^10.4.0",
"enhanced-resolve": "^5.15.0", "enhanced-resolve": "^5.18.1",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^10.1.5",
"eslint-define-config": "^1.24.1",
"eslint-import-resolver-node": "^0.3.9", "eslint-import-resolver-node": "^0.3.9",
"eslint-module-utils": "^2.8.0", "eslint-import-resolver-typescript": "^4.4.3",
"eslint-plugin-es-x": "^7.5.0", "eslint-module-utils": "^2.12.0",
"eslint-plugin-jsdoc": "^48.0.2", "eslint-plugin-import-x": "^4.15.1",
"eslint-plugin-unicorn": "^50.0.1", "eslint-plugin-regexp": "^2.9.0",
"eslint-plugin-unicorn": "^59.0.1",
"esprima": "^4.0.1",
"esquery": "^1.6.0",
"estraverse": "^5.3.0", "estraverse": "^5.3.0",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.3",
"get-tsconfig": "^4.7.2", "get-tsconfig": "^4.10.1",
"ignore": "^5.3.0", "globals": "^16.2.0",
"is-builtin-module": "^3.2.1", "ignore": "^7.0.5",
"is-bun-module": "^2.0.0",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
"language-tags": "^1.0.9", "language-tags": "^2.1.0",
"lodash": "^4.17.21", "lodash-es": "^4.17.21",
"minimatch": "^9.0.3", "minimatch": "^10.0.1",
"resolve": "^2.0.0-next.5", "semver": "^7.7.2",
"semver": "^7.5.4", "typescript-eslint": "^8.34.0"
"tsconfig-paths": "^4.2.0" },
"pnpm": {
"overrides": {
"is-core-module": "file:./overrides/is-core-module",
"supports-preserve-symlinks-flag": "file:./overrides/supports-preserve-symlinks-flag"
}
}, },
"overrides": { "overrides": {
"is-core-module": "file:./overrides/is-core-module", "is-core-module": "file:./overrides/is-core-module",
@ -48,11 +77,5 @@
"resolutions": { "resolutions": {
"**/is-core-module": "file:./overrides/is-core-module", "**/is-core-module": "file:./overrides/is-core-module",
"**/supports-preserve-symlinks-flag": "file:./overrides/supports-preserve-symlinks-flag" "**/supports-preserve-symlinks-flag": "file:./overrides/supports-preserve-symlinks-flag"
},
"pnpm": {
"overrides": {
"is-core-module": "file:./overrides/is-core-module",
"supports-preserve-symlinks-flag": "file:./overrides/supports-preserve-symlinks-flag"
}
} }
} }

13
dist/prettier.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
// Generated by dts-bundle-generator v9.4.0
import { Config } from 'prettier';
declare function defineConfig({ tailwind, ...config }: Partial<Config> & {
tailwind?: boolean;
}): Config;
export {
defineConfig as default,
};
export {};

2
dist/print-config.sh vendored Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash
node -e "import('./eslint.config.mjs').then(config => console.dir(config, { depth: null }))"

19
dist/tsconfig.json vendored Normal file
View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"allowArbitraryExtensions": true,
"allowImportingTsExtensions": true,
"jsx": "react-jsx",
"module": "Preserve",
"moduleResolution": "Bundler",
"noEmit": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"resolveJsonModule": true,
"strict": true,
"stripInternal": true,
"target": "ESNext",
"verbatimModuleSyntax": true
}
}

16
dist/types.d.ts vendored Normal file
View File

@ -0,0 +1,16 @@
// Generated by dts-bundle-generator v9.4.0
import { ESLintUtils } from '@typescript-eslint/utils';
import { Rule } from 'eslint';
export declare function defineRules(rules: {
[ruleName: string]: Rule.RuleModule | ESLintUtils.RuleModule<string, unknown[]>;
}): {
[ruleName: string]: Rule.RuleModule | ESLintUtils.RuleModule<string, unknown[], unknown, ESLintUtils.RuleListener>;
};
export declare function defineRule({ name, create, ...meta }: Rule.RuleMetaData & {
name?: string;
create: (context: Rule.RuleContext) => Rule.RuleListener;
}): Rule.RuleModule;
export {};

1130
dist/yarn.lock vendored

File diff suppressed because it is too large Load Diff

3
eslint.config.cjs Normal file
View File

@ -0,0 +1,3 @@
/* eslint-disable */
require('@swc-node/register');
module.exports = require('./.eslint.ts').default;

View File

@ -1,44 +1,98 @@
{ {
"name": "@aet/eslint-configs", "name": "@aet/eslint-configs",
"type": "module",
"version": "0.0.0",
"scripts": { "scripts": {
"build": "./scripts/build.sh", "build": "./scripts/build.ts",
"check-import": "./scripts/check-imports.ts" "check-import": "./scripts/check-imports.ts",
"define": "/usr/local/bin/codium ./packages/eslint-define-config",
"sync": "./scripts/sync-deps.ts",
"do": "(cd dist && ver bump && npm publish && ver unpub)"
}, },
"private": true, "private": true,
"dependencies": {
"@aet/eslint-define-config": "^0.1.16",
"@antfu/install-pkg": "^1.1.0",
"@eslint-community/eslint-utils": "^4.7.0",
"@eslint-react/eslint-plugin": "1.52.2",
"@eslint/js": "^9.28.0",
"@nolyfill/is-core-module": "^1.0.39",
"@stylistic/eslint-plugin": "^4.4.1",
"@typescript-eslint/eslint-plugin": "^8.34.0",
"@typescript-eslint/parser": "^8.34.0",
"@typescript-eslint/type-utils": "^8.34.0",
"@typescript-eslint/utils": "^8.34.0",
"aria-query": "^5.3.2",
"axe-core": "^4.10.3",
"axobject-query": "4.1.0",
"damerau-levenshtein": "1.0.8",
"debug": "^4.4.1",
"doctrine": "^3.0.0",
"emoji-regex": "^10.4.0",
"enhanced-resolve": "^5.18.1",
"eslint-config-prettier": "^10.1.5",
"eslint-import-resolver-node": "^0.3.9",
"eslint-import-resolver-typescript": "^4.4.3",
"eslint-module-utils": "^2.12.0",
"eslint-plugin-import-x": "^4.15.2",
"eslint-plugin-regexp": "^2.9.0",
"eslint-plugin-unicorn": "^59.0.1",
"esprima": "^4.0.1",
"esquery": "^1.6.0",
"estraverse": "^5.3.0",
"fast-glob": "^3.3.3",
"get-tsconfig": "^4.10.1",
"globals": "^16.2.0",
"ignore": "^7.0.5",
"is-bun-module": "^2.0.0",
"is-glob": "^4.0.3",
"language-tags": "^2.1.0",
"lodash-es": "^4.17.21",
"minimatch": "^10.0.3",
"semver": "^7.7.2",
"typescript-eslint": "^8.34.0"
},
"devDependencies": { "devDependencies": {
"@babel/core": "^7.23.7", "@babel/core": "^7.27.4",
"@babel/plugin-transform-flow-strip-types": "^7.23.3", "@babel/plugin-transform-flow-strip-types": "^7.27.1",
"@babel/preset-env": "^7.23.8", "@babel/preset-env": "^7.27.2",
"@graphql-eslint/eslint-plugin": "^4.4.0",
"@swc-node/register": "^1.10.10",
"@tanstack/eslint-plugin-query": "^5.78.0",
"@types/babel-plugin-macros": "^3.1.3", "@types/babel-plugin-macros": "^3.1.3",
"@types/babel__core": "^7.20.5", "@types/babel__core": "^7.20.5",
"@types/eslint": "^8.56.2", "@types/eslint-config-prettier": "^6.11.3",
"@types/estree": "^1.0.5", "@types/eslint-plugin-tailwindcss": "^3.17.0",
"@types/estree-jsx": "^1.0.3", "@types/esprima": "^4.0.6",
"@types/lodash": "^4.14.202", "@types/esquery": "^1.5.4",
"@types/node": "^20.11.0", "@types/estree": "^1.0.8",
"@typescript-eslint/eslint-plugin": "6.18.1", "@types/estree-jsx": "^1.0.5",
"@typescript-eslint/type-utils": "^6.18.1", "@types/lodash-es": "^4.17.12",
"@typescript-eslint/types": "^6.18.1", "@types/node": "^22.15.30",
"@typescript-eslint/typescript-estree": "^6.18.1", "@types/react-refresh": "^0.14.6",
"@typescript-eslint/utils": "^6.18.1", "@typescript-eslint/types": "^8.34.0",
"@typescript-eslint/typescript-estree": "^8.34.0",
"@vitest/eslint-plugin": "^1.2.4",
"babel-plugin-macros": "^3.1.0", "babel-plugin-macros": "^3.1.0",
"dts-bundle-generator": "^9.2.4", "dts-bundle-generator": "9.5.1",
"esbin": "0.0.4", "esbuild": "0.25.5",
"esbuild": "0.19.11",
"esbuild-plugin-alias": "^0.2.1", "esbuild-plugin-alias": "^0.2.1",
"eslint": "8.56.0", "eslint": "^9.28.0",
"eslint-config-prettier": "^9.1.0", "eslint-plugin-jsdoc": "^50.7.1",
"eslint-define-config": "^1.24.1", "eslint-plugin-lingui": "^0.10.1",
"fast-glob": "^3.3.2", "eslint-plugin-react-refresh": "^0.4.20",
"find-cache-dir": "^5.0.0", "eslint-plugin-storybook": "^9.0.9",
"json-schema-to-ts": "^3.0.0", "eslint-plugin-testing-library": "^7.5.2",
"lodash": "^4.17.21", "graphql": "^16.11.0",
"minimatch": "^9.0.3", "json-schema-to-ts": "^3.1.1",
"nolyfill": "^1.0.44",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"picocolors": "^1.0.0", "picocolors": "^1.1.1",
"prettier": "^3.2.1", "prettier": "^3.5.3",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"typescript": "^5.3.3" "terser": "^5.42.0",
"type-fest": "^4.41.0",
"typescript": "^5.8.3"
}, },
"prettier": { "prettier": {
"arrowParens": "avoid", "arrowParens": "avoid",
@ -50,14 +104,22 @@
}, },
"pnpm": { "pnpm": {
"overrides": { "overrides": {
"function-bind": "npm:@nolyfill/function-bind@latest", "@typescript-eslint/utils": "8.0.0",
"has-proto": "npm:@nolyfill/has-proto@latest", "function-bind": "npm:@nolyfill/function-bind@^1",
"has-symbols": "npm:@nolyfill/has-symbols@latest", "has-proto": "npm:@nolyfill/has-proto@^1",
"hasown": "npm:@nolyfill/hasown@latest", "has-symbols": "npm:@nolyfill/has-symbols@^1",
"isarray": "npm:@nolyfill/isarray@latest", "hasown": "npm:@nolyfill/hasown@^1",
"jsonify": "npm:@nolyfill/jsonify@latest", "isarray": "npm:@nolyfill/isarray@^1",
"object-keys": "npm:@nolyfill/object-keys@latest", "jsonify": "npm:@nolyfill/jsonify@^1",
"set-function-length": "npm:@nolyfill/set-function-length@latest" "object-keys": "npm:@nolyfill/object-keys@^1",
"set-function-length": "npm:@nolyfill/set-function-length@^1",
"@babel/types": "7.25.2",
"is-core-module": "npm:@nolyfill/is-core-module@^1",
"json-stable-stringify": "npm:@nolyfill/json-stable-stringify@^1"
},
"patchedDependencies": {
"@typescript-eslint/typescript-estree@8.0.0": "patches/@typescript-eslint__typescript-estree@8.0.0.patch",
"dts-bundle-generator": "patches/dts-bundle-generator.patch"
} }
} }
} }

Submodule packages/eslint-import-resolver-typescript deleted from 7b6bfc3947

Submodule packages/eslint-plugin-import deleted from 6b95a02193

Submodule packages/eslint-plugin-jsx-a11y updated: fffb05b38c...a7d1a12a61

Submodule packages/eslint-plugin-n updated: 47cd9a6a0e...42464abe64

Submodule packages/eslint-plugin-react deleted from ecadb92609

View File

@ -22,8 +22,11 @@ import type {
ChainExpression, ChainExpression,
Pattern, Pattern,
OptionalMemberExpression, OptionalMemberExpression,
ArrayExpression,
VariableDeclaration,
} from 'estree'; } from 'estree';
import type { FromSchema } from 'json-schema-to-ts'; import type { FromSchema } from 'json-schema-to-ts';
import { __EXPERIMENTAL__ } from './index'; import { __EXPERIMENTAL__ } from './index';
const schema = { const schema = {
@ -81,7 +84,23 @@ const rule: Rule.RuleModule = {
context.report(problem); context.report(problem);
} }
const scopeManager = context.sourceCode.scopeManager; /**
* SourceCode#getText that also works down to ESLint 3.0.0
*/
const getSource =
typeof context.getSource === 'function'
? (node: Node) => context.getSource(node)
: (node: Node) => context.sourceCode.getText(node);
/**
* SourceCode#getScope that also works down to ESLint 3.0.0
*/
const getScope =
typeof context.getScope === 'function'
? () => context.getScope()
: (node: Node) => context.sourceCode.getScope(node);
const scopeManager = context.getSourceCode().scopeManager;
// Should be shared between visitors. // Should be shared between visitors.
const setStateCallSites = new WeakMap<Expression, Pattern>(); const setStateCallSites = new WeakMap<Expression, Pattern>();
@ -128,7 +147,7 @@ const rule: Rule.RuleModule = {
' }\n' + ' }\n' +
' fetchData();\n' + ' fetchData();\n' +
`}, [someId]); // Or [] if effect doesn't need props or state\n\n` + `}, [someId]); // Or [] if effect doesn't need props or state\n\n` +
'Learn more about data fetching with Hooks: https://reactjs.org/link/hooks-data-fetching', 'Learn more about data fetching with Hooks: https://react.dev/link/hooks-data-fetching',
}); });
} }
@ -173,6 +192,8 @@ const rule: Rule.RuleModule = {
// ^^^ true for this reference // ^^^ true for this reference
// const [state, dispatch] = useReducer() / React.useReducer() // const [state, dispatch] = useReducer() / React.useReducer()
// ^^^ true for this reference // ^^^ true for this reference
// const [state, dispatch] = useActionState() / React.useActionState()
// ^^^ true for this reference
// const ref = useRef() // const ref = useRef()
// ^^^ true for this reference // ^^^ true for this reference
// const onStuff = useEffectEvent(() => {}) // const onStuff = useEffectEvent(() => {})
@ -187,31 +208,32 @@ const rule: Rule.RuleModule = {
return false; return false;
} }
// Look for `let stuff = ...` // Look for `let stuff = ...`
if (def.node.type !== 'VariableDeclarator') { const node = def.node as Node;
if (node.type !== 'VariableDeclarator') {
return false; return false;
} }
let init = (def.node as VariableDeclarator).init; let init = node.init;
if (init == null) { if (init == null) {
return false; return false;
} }
while (init.type === 'TSAsExpression') { while (init.type === 'TSAsExpression' || init.type === 'AsExpression') {
init = init.expression; init = init.expression;
} }
// Detect primitive constants // Detect primitive constants
// const foo = 42 // const foo = 42
let declaration = def.node.parent; let declaration = node.parent;
if (declaration == null) { if (declaration == null) {
// This might happen if variable is declared after the callback. // This might happen if variable is declared after the callback.
// In that case ESLint won't set up .parent refs. // In that case ESLint won't set up .parent refs.
// So we'll set them up manually. // So we'll set them up manually.
fastFindReferenceWithParent(componentScope.block, def.node.id); fastFindReferenceWithParent(componentScope.block, node.id);
declaration = def.node.parent; declaration = node.parent;
if (declaration == null) { if (declaration == null) {
return false; return false;
} }
} }
if ( if (
declaration.kind === 'const' && (declaration as VariableDeclaration).kind === 'const' &&
init.type === 'Literal' && init.type === 'Literal' &&
(typeof init.value === 'string' || (typeof init.value === 'string' ||
typeof init.value === 'number' || typeof init.value === 'number' ||
@ -252,7 +274,11 @@ const rule: Rule.RuleModule = {
} }
// useEffectEvent() return value is always unstable. // useEffectEvent() return value is always unstable.
return true; return true;
} else if (name === 'useState' || name === 'useReducer') { } else if (
name === 'useState' ||
name === 'useReducer' ||
name === 'useActionState'
) {
// Only consider second value in initializing tuple stable. // Only consider second value in initializing tuple stable.
if ( if (
id.type === 'ArrayPattern' && id.type === 'ArrayPattern' &&
@ -264,14 +290,14 @@ const rule: Rule.RuleModule = {
if (name === 'useState') { if (name === 'useState') {
const references = resolved.references; const references = resolved.references;
let writeCount = 0; let writeCount = 0;
for (let i = 0; i < references.length; i++) { for (const reference of references) {
if (references[i].isWrite()) { if (reference.isWrite()) {
writeCount++; writeCount++;
} }
if (writeCount > 1) { if (writeCount > 1) {
return false; return false;
} }
setStateCallSites.set(references[i].identifier, id.elements[0]!); setStateCallSites.set(reference.identifier, id.elements[0]!);
} }
} }
// Setter is stable. // Setter is stable.
@ -279,28 +305,26 @@ const rule: Rule.RuleModule = {
} else if (id.elements[0] === resolved.identifiers[0]) { } else if (id.elements[0] === resolved.identifiers[0]) {
if (name === 'useState') { if (name === 'useState') {
const references = resolved.references; const references = resolved.references;
for (let i = 0; i < references.length; i++) { for (const reference of references) {
stateVariables.add(references[i].identifier); stateVariables.add(reference.identifier);
} }
} }
// State variable itself is dynamic. // State variable itself is dynamic.
return false; return false;
} }
} }
} else if (name === 'useTransition') { } else if (
// Only consider second value in initializing tuple stable. // Only consider second value in initializing tuple stable.
if ( name === 'useTransition' &&
id.type === 'ArrayPattern' && id.type === 'ArrayPattern' &&
id.elements.length === 2 && id.elements.length === 2 &&
Array.isArray(resolved.identifiers) Array.isArray(resolved.identifiers) &&
) {
// Is second tuple value the same reference we're checking? // Is second tuple value the same reference we're checking?
if (id.elements[1] === resolved.identifiers[0]) { id.elements[1] === resolved.identifiers[0]
) {
// Setter is stable. // Setter is stable.
return true; return true;
} }
}
}
// By default assume it's dynamic. // By default assume it's dynamic.
return false; return false;
} }
@ -319,7 +343,7 @@ const rule: Rule.RuleModule = {
} }
// Search the direct component subscopes for // Search the direct component subscopes for
// top-level function definitions matching this reference. // top-level function definitions matching this reference.
const fnNode = def.node; const fnNode = def.node as Node;
const childScopes = componentScope.childScopes; const childScopes = componentScope.childScopes;
let fnScope = null; let fnScope = null;
let i; let i;
@ -424,9 +448,9 @@ const rule: Rule.RuleModule = {
dependencyNode.type === 'Identifier' && dependencyNode.type === 'Identifier' &&
(dependencyNode.parent!.type === 'MemberExpression' || (dependencyNode.parent!.type === 'MemberExpression' ||
dependencyNode.parent!.type === 'OptionalMemberExpression') && dependencyNode.parent!.type === 'OptionalMemberExpression') &&
!dependencyNode.parent!.computed && !dependencyNode.parent.computed &&
dependencyNode.parent!.property.type === 'Identifier' && dependencyNode.parent.property.type === 'Identifier' &&
dependencyNode.parent!.property.name === 'current' && dependencyNode.parent.property.name === 'current' &&
// ...in a cleanup function or below... // ...in a cleanup function or below...
isInsideEffectCleanup(reference) isInsideEffectCleanup(reference)
) { ) {
@ -479,12 +503,11 @@ const rule: Rule.RuleModule = {
// Warn about accessing .current in cleanup effects. // Warn about accessing .current in cleanup effects.
currentRefsInEffectCleanup.forEach(({ reference, dependencyNode }, dependency) => { currentRefsInEffectCleanup.forEach(({ reference, dependencyNode }, dependency) => {
const references: Scope.Reference[] = reference.resolved!.references; const references: Scope.Reference[] = reference.resolved.references;
// Is React managing this ref or us? // Is React managing this ref or us?
// Let's see if we can find a .current assignment. // Let's see if we can find a .current assignment.
let foundCurrentAssignment = false; let foundCurrentAssignment = false;
for (let i = 0; i < references.length; i++) { for (const { identifier } of references) {
const { identifier } = references[i];
const { parent } = identifier; const { parent } = identifier;
if ( if (
parent != null && parent != null &&
@ -496,7 +519,7 @@ const rule: Rule.RuleModule = {
parent.property.name === 'current' && parent.property.name === 'current' &&
// ref.current = <something> // ref.current = <something>
parent.parent!.type === 'AssignmentExpression' && parent.parent!.type === 'AssignmentExpression' &&
parent.parent!.left === parent parent.parent.left === parent
) { ) {
foundCurrentAssignment = true; foundCurrentAssignment = true;
break; break;
@ -529,11 +552,11 @@ const rule: Rule.RuleModule = {
node: writeExpr, node: writeExpr,
message: message:
`Assignments to the '${key}' variable from inside React Hook ` + `Assignments to the '${key}' variable from inside React Hook ` +
`${context.getSource(reactiveHook)} will be lost after each ` + `${getSource(reactiveHook)} will be lost after each ` +
`render. To preserve the value over time, store it in a useRef ` + `render. To preserve the value over time, store it in a useRef ` +
`Hook and keep the mutable value in the '.current' property. ` + `Hook and keep the mutable value in the '.current' property. ` +
`Otherwise, you can move this variable directly inside ` + `Otherwise, you can move this variable directly inside ` +
`${context.getSource(reactiveHook)}.`, `${getSource(reactiveHook)}.`,
}); });
} }
@ -543,11 +566,11 @@ const rule: Rule.RuleModule = {
if (isStable) { if (isStable) {
stableDependencies.add(key); stableDependencies.add(key);
} }
references.forEach(reference => { for (const reference of references) {
if (reference.writeExpr) { if (reference.writeExpr) {
reportStaleAssignment(reference.writeExpr, key); reportStaleAssignment(reference.writeExpr, key);
} }
}); }
}); });
if (staleAssignments.size > 0) { if (staleAssignments.size > 0) {
@ -563,15 +586,15 @@ const rule: Rule.RuleModule = {
if (setStateInsideEffectWithoutDeps) { if (setStateInsideEffectWithoutDeps) {
return; return;
} }
references.forEach(reference => { for (const reference of references) {
if (setStateInsideEffectWithoutDeps) { if (setStateInsideEffectWithoutDeps) {
return; continue;
} }
const id = reference.identifier; const id = reference.identifier;
const isSetState: boolean = setStateCallSites.has(id); const isSetState: boolean = setStateCallSites.has(id);
if (!isSetState) { if (!isSetState) {
return; continue;
} }
let fnScope: Scope.Scope = reference.from; let fnScope: Scope.Scope = reference.from;
@ -583,9 +606,8 @@ const rule: Rule.RuleModule = {
// TODO: we could potentially ignore early returns. // TODO: we could potentially ignore early returns.
setStateInsideEffectWithoutDeps = key; setStateInsideEffectWithoutDeps = key;
} }
}
}); });
});
if (setStateInsideEffectWithoutDeps) { if (setStateInsideEffectWithoutDeps) {
const { suggestedDependencies } = collectRecommendations({ const { suggestedDependencies } = collectRecommendations({
dependencies, dependencies,
@ -620,49 +642,56 @@ const rule: Rule.RuleModule = {
const declaredDependencies: DeclaredDependency[] = []; const declaredDependencies: DeclaredDependency[] = [];
const externalDependencies = new Set<string>(); const externalDependencies = new Set<string>();
if (declaredDependenciesNode.type !== 'ArrayExpression') { const isArrayExpression = declaredDependenciesNode.type === 'ArrayExpression';
const isTSAsArrayExpression =
declaredDependenciesNode.type === 'TSAsExpression' &&
declaredDependenciesNode.expression.type === 'ArrayExpression';
if (!isArrayExpression && !isTSAsArrayExpression) {
// If the declared dependencies are not an array expression then we // If the declared dependencies are not an array expression then we
// can't verify that the user provided the correct dependencies. Tell // can't verify that the user provided the correct dependencies. Tell
// the user this in an error. // the user this in an error.
reportProblem({ reportProblem({
node: declaredDependenciesNode, node: declaredDependenciesNode,
message: message:
`React Hook ${context.getSource(reactiveHook)} was passed a ` + `React Hook ${getSource(reactiveHook)} was passed a ` +
'dependency list that is not an array literal. This means we ' + 'dependency list that is not an array literal. This means we ' +
"can't statically verify whether you've passed the correct " + "can't statically verify whether you've passed the correct " +
'dependencies.', 'dependencies.',
}); });
} else { } else {
declaredDependenciesNode.elements.forEach(declaredDependencyNode => { const arrayExpression = isTSAsArrayExpression
? declaredDependenciesNode.expression
: declaredDependenciesNode;
for (const declaredDependencyNode of (arrayExpression as ArrayExpression)
.elements) {
// Skip elided elements. // Skip elided elements.
if (declaredDependencyNode === null) { if (declaredDependencyNode === null) {
return; continue;
} }
// If we see a spread element then add a special warning. // If we see a spread element then add a special warning.
if (declaredDependencyNode.type === 'SpreadElement') { if (declaredDependencyNode.type === 'SpreadElement') {
reportProblem({ reportProblem({
node: declaredDependencyNode, node: declaredDependencyNode,
message: message:
`React Hook ${context.getSource(reactiveHook)} has a spread ` + `React Hook ${getSource(reactiveHook)} has a spread ` +
"element in its dependency array. This means we can't " + "element in its dependency array. This means we can't " +
"statically verify whether you've passed the " + "statically verify whether you've passed the " +
'correct dependencies.', 'correct dependencies.',
}); });
return; continue;
} }
if (useEffectEventVariables.has(declaredDependencyNode)) { if (useEffectEventVariables.has(declaredDependencyNode)) {
reportProblem({ reportProblem({
node: declaredDependencyNode, node: declaredDependencyNode,
message: message:
'Functions returned from `useEffectEvent` must not be included in the dependency array. ' + 'Functions returned from `useEffectEvent` must not be included in the dependency array. ' +
`Remove \`${context.getSource(declaredDependencyNode)}\` from the list.`, `Remove \`${getSource(declaredDependencyNode)}\` from the list.`,
suggest: [ suggest: [
{ {
desc: `Remove the dependency \`${context.getSource( desc: `Remove the dependency \`${getSource(declaredDependencyNode)}\``,
declaredDependencyNode,
)}\``,
fix(fixer) { fix(fixer) {
return fixer.removeRange(declaredDependencyNode.range!); return fixer.removeRange(declaredDependencyNode.range);
}, },
}, },
], ],
@ -696,13 +725,13 @@ const rule: Rule.RuleModule = {
reportProblem({ reportProblem({
node: declaredDependencyNode, node: declaredDependencyNode,
message: message:
`React Hook ${context.getSource(reactiveHook)} has a ` + `React Hook ${getSource(reactiveHook)} has a ` +
`complex expression in the dependency array. ` + `complex expression in the dependency array. ` +
'Extract it to a separate variable so it can be statically checked.', 'Extract it to a separate variable so it can be statically checked.',
}); });
} }
return; continue;
} else { } else {
throw error; throw error;
} }
@ -731,7 +760,7 @@ const rule: Rule.RuleModule = {
if (!isDeclaredInComponent) { if (!isDeclaredInComponent) {
externalDependencies.add(declaredDependency); externalDependencies.add(declaredDependency);
} }
}); }
} }
const { const {
@ -782,9 +811,7 @@ const rule: Rule.RuleModule = {
const message = const message =
`The '${construction.name.name}' ${depType} ${causation} the dependencies of ` + `The '${construction.name.name}' ${depType} ${causation} the dependencies of ` +
`${reactiveHookName} Hook (at line ${ `${reactiveHookName} Hook (at line ${declaredDependenciesNode.loc!.start.line}) ` +
declaredDependenciesNode.loc!.start.line
}) ` +
`change on every render. ${advice}`; `change on every render. ${advice}`;
let suggest: Rule.SuggestionReportDescriptor[] | undefined; let suggest: Rule.SuggestionReportDescriptor[] | undefined;
@ -838,7 +865,7 @@ const rule: Rule.RuleModule = {
// in some extra deduplication. We can't do this // in some extra deduplication. We can't do this
// for effects though because those have legit // for effects though because those have legit
// use cases for over-specifying deps. // use cases for over-specifying deps.
if (!isEffect && missingDependencies.size) { if (!isEffect && missingDependencies.size > 0) {
suggestedDeps = collectRecommendations({ suggestedDeps = collectRecommendations({
dependencies, dependencies,
declaredDependencies: [], // Pretend we don't know declaredDependencies: [], // Pretend we don't know
@ -854,7 +881,7 @@ const rule: Rule.RuleModule = {
return true; return true;
} }
const declaredDepKeys = declaredDependencies.map(dep => dep.key); const declaredDepKeys = declaredDependencies.map(dep => dep.key);
const sortedDeclaredDepKeys = declaredDepKeys.slice().sort(); const sortedDeclaredDepKeys = [...declaredDepKeys].sort();
return declaredDepKeys.join(',') === sortedDeclaredDepKeys.join(','); return declaredDepKeys.join(',') === sortedDeclaredDepKeys.join(',');
} }
@ -895,11 +922,7 @@ const rule: Rule.RuleModule = {
' ' + ' ' +
(deps.size > 1 ? 'dependencies' : 'dependency') + (deps.size > 1 ? 'dependencies' : 'dependency') +
': ' + ': ' +
joinEnglish( joinEnglish([...deps].sort().map(name => "'" + formatDependency(name) + "'")) +
Array.from(deps)
.sort()
.map(name => "'" + formatDependency(name) + "'"),
) +
`. Either ${fixVerb} ${ `. Either ${fixVerb} ${
deps.size > 1 ? 'them' : 'it' deps.size > 1 ? 'them' : 'it'
} or remove the dependency array.` } or remove the dependency array.`
@ -909,20 +932,20 @@ const rule: Rule.RuleModule = {
let extraWarning = ''; let extraWarning = '';
if (unnecessaryDependencies.size > 0) { if (unnecessaryDependencies.size > 0) {
let badRef: string | null = null; let badRef: string | null = null;
Array.from(unnecessaryDependencies.keys()).forEach(key => { for (const key of unnecessaryDependencies.keys()) {
if (badRef !== null) { if (badRef !== null) {
return; continue;
} }
if (key.endsWith('.current')) { if (key.endsWith('.current')) {
badRef = key; badRef = key;
} }
}); }
if (badRef !== null) { if (badRef !== null) {
extraWarning = extraWarning =
` Mutable values like '${badRef}' aren't valid dependencies ` + ` Mutable values like '${badRef}' aren't valid dependencies ` +
"because mutating them doesn't re-render the component."; "because mutating them doesn't re-render the component.";
} else if (externalDependencies.size > 0) { } else if (externalDependencies.size > 0) {
const dep = Array.from(externalDependencies)[0]; const dep = [...externalDependencies][0];
// Don't show this warning for things that likely just got moved *inside* the callback // Don't show this warning for things that likely just got moved *inside* the callback
// because in that case they're clearly not referring to globals. // because in that case they're clearly not referring to globals.
if (!scope.set.has(dep)) { if (!scope.set.has(dep)) {
@ -971,11 +994,11 @@ const rule: Rule.RuleModule = {
` However, 'props' will change when *any* prop changes, so the ` + ` However, 'props' will change when *any* prop changes, so the ` +
`preferred fix is to destructure the 'props' object outside of ` + `preferred fix is to destructure the 'props' object outside of ` +
`the ${reactiveHookName} call and refer to those specific props ` + `the ${reactiveHookName} call and refer to those specific props ` +
`inside ${context.getSource(reactiveHook)}.`; `inside ${getSource(reactiveHook)}.`;
} }
} }
if (!extraWarning && missingDependencies.size) { if (!extraWarning && missingDependencies.size > 0) {
// See if the user is trying to avoid specifying a callable prop. // See if the user is trying to avoid specifying a callable prop.
// This usually means they're unaware of useCallback. // This usually means they're unaware of useCallback.
let missingCallbackDep: string | null = null; let missingCallbackDep: string | null = null;
@ -1041,7 +1064,7 @@ const rule: Rule.RuleModule = {
let id: Identifier; let id: Identifier;
let maybeCall: Node | null; let maybeCall: Node | null;
for (let i = 0; i < references.length; i++) { for (let i = 0; i < references.length; i++) {
id = references[i].identifier as Identifier; id = references[i].identifier;
maybeCall = id.parent!; maybeCall = id.parent!;
// Try to see if we have setState(someExpr(missingDep)). // Try to see if we have setState(someExpr(missingDep)).
while (maybeCall != null && maybeCall !== componentScope.block) { while (maybeCall != null && maybeCall !== componentScope.block) {
@ -1125,7 +1148,7 @@ const rule: Rule.RuleModule = {
reportProblem({ reportProblem({
node: declaredDependenciesNode, node: declaredDependenciesNode,
message: message:
`React Hook ${context.getSource(reactiveHook)} has ` + `React Hook ${getSource(reactiveHook)} has ` +
// To avoid a long message, show the next actionable item. // To avoid a long message, show the next actionable item.
(getWarningMessage(missingDependencies, 'a', 'missing', 'include') || (getWarningMessage(missingDependencies, 'a', 'missing', 'include') ||
getWarningMessage(unnecessaryDependencies, 'an', 'unnecessary', 'exclude') || getWarningMessage(unnecessaryDependencies, 'an', 'unnecessary', 'exclude') ||
@ -1158,7 +1181,11 @@ const rule: Rule.RuleModule = {
const reactiveHook = node.callee as Identifier | MemberExpression; const reactiveHook = node.callee as Identifier | MemberExpression;
const reactiveHookName = (getNodeWithoutReactNamespace(reactiveHook) as Identifier) const reactiveHookName = (getNodeWithoutReactNamespace(reactiveHook) as Identifier)
.name; .name;
const declaredDependenciesNode = node.arguments[callbackIndex + 1]; const maybeNode = node.arguments[callbackIndex + 1];
const declaredDependenciesNode =
maybeNode && !(maybeNode.type === 'Identifier' && maybeNode.name === 'undefined')
? maybeNode
: undefined;
const isEffect = /Effect($|[^a-z])/g.test(reactiveHookName); const isEffect = /Effect($|[^a-z])/g.test(reactiveHookName);
// Check whether a callback is supplied. If there is no callback supplied // Check whether a callback is supplied. If there is no callback supplied
@ -1203,7 +1230,16 @@ const rule: Rule.RuleModule = {
isEffect, isEffect,
); );
return; // Handled return; // Handled
case 'Identifier': case 'TSAsExpression':
visitFunctionWithDependencies(
callback.expression,
declaredDependenciesNode,
reactiveHook,
reactiveHookName,
isEffect,
);
return; // Handled
case 'Identifier': {
if (!declaredDependenciesNode) { if (!declaredDependenciesNode) {
// No deps, no problems. // No deps, no problems.
return; // Handled return; // Handled
@ -1221,7 +1257,7 @@ const rule: Rule.RuleModule = {
return; // Handled return; // Handled
} }
// We'll do our best effort to find it, complain otherwise. // We'll do our best effort to find it, complain otherwise.
const variable = context.getScope().set.get(callback.name); const variable = getScope(callback).set.get(callback.name);
if (variable == null || variable.defs == null) { if (variable == null || variable.defs == null) {
// If it's not in scope, we don't care. // If it's not in scope, we don't care.
return; // Handled return; // Handled
@ -1271,6 +1307,7 @@ const rule: Rule.RuleModule = {
break; // Unhandled break; // Unhandled
} }
break; // Unhandled break; // Unhandled
}
default: default:
// useEffect(generateEffectBody(), []); // useEffect(generateEffectBody(), []);
reportProblem({ reportProblem({
@ -1358,33 +1395,33 @@ function collectRecommendations({
function createDepTree(): DepTree { function createDepTree(): DepTree {
return { return {
isUsed: false, isUsed: false, // True if used in code
isSatisfiedRecursively: false, isSatisfiedRecursively: false, // True if specified in deps
isSubtreeUsed: false, isSubtreeUsed: false, // True if something deeper is used by code
children: new Map<string, never>(), children: new Map(), // Nodes for properties
}; };
} }
// Mark all required nodes first. // Mark all required nodes first.
// Imagine exclamation marks next to each used deep property. // Imagine exclamation marks next to each used deep property.
dependencies.forEach((_, key) => { for (const key of dependencies.keys()) {
const node = getOrCreateNodeByPath(depTree, key); const node = getOrCreateNodeByPath(depTree, key);
node.isUsed = true; node.isUsed = true;
markAllParentsByPath(depTree, key, parent => { markAllParentsByPath(depTree, key, parent => {
parent.isSubtreeUsed = true; parent.isSubtreeUsed = true;
}); });
}); }
// Mark all satisfied nodes. // Mark all satisfied nodes.
// Imagine checkmarks next to each declared dependency. // Imagine checkmarks next to each declared dependency.
declaredDependencies.forEach(({ key }) => { for (const { key } of declaredDependencies) {
const node = getOrCreateNodeByPath(depTree, key); const node = getOrCreateNodeByPath(depTree, key);
node.isSatisfiedRecursively = true; node.isSatisfiedRecursively = true;
}); }
stableDependencies.forEach(key => { for (const key of stableDependencies) {
const node = getOrCreateNodeByPath(depTree, key); const node = getOrCreateNodeByPath(depTree, key);
node.isSatisfiedRecursively = true; node.isSatisfiedRecursively = true;
}); }
// Tree manipulation helpers. // Tree manipulation helpers.
function getOrCreateNodeByPath(rootNode: DepTree, path: string): DepTree { function getOrCreateNodeByPath(rootNode: DepTree, path: string): DepTree {
@ -1460,15 +1497,15 @@ function collectRecommendations({
const suggestedDependencies: string[] = []; const suggestedDependencies: string[] = [];
const unnecessaryDependencies = new Set<string>(); const unnecessaryDependencies = new Set<string>();
const duplicateDependencies = new Set<string>(); const duplicateDependencies = new Set<string>();
declaredDependencies.forEach(({ key }) => { for (const { key } of declaredDependencies) {
// Does this declared dep satisfy a real need? // Does this declared dep satisfy a real need?
if (satisfyingDependencies.has(key)) { if (satisfyingDependencies.has(key)) {
if (!suggestedDependencies.includes(key)) { if (suggestedDependencies.includes(key)) {
// Good one.
suggestedDependencies.push(key);
} else {
// Duplicate. // Duplicate.
duplicateDependencies.add(key); duplicateDependencies.add(key);
} else {
// Good one.
suggestedDependencies.push(key);
} }
} else { } else {
if (isEffect && !key.endsWith('.current') && !externalDependencies.has(key)) { if (isEffect && !key.endsWith('.current') && !externalDependencies.has(key)) {
@ -1476,7 +1513,7 @@ function collectRecommendations({
// Such as resetting scroll when ID changes. // Such as resetting scroll when ID changes.
// Consider them legit. // Consider them legit.
// The exception is ref.current which is always wrong. // The exception is ref.current which is always wrong.
if (suggestedDependencies.indexOf(key) === -1) { if (!suggestedDependencies.includes(key)) {
suggestedDependencies.push(key); suggestedDependencies.push(key);
} }
} else { } else {
@ -1484,12 +1521,12 @@ function collectRecommendations({
unnecessaryDependencies.add(key); unnecessaryDependencies.add(key);
} }
} }
}); }
// Then add the missing ones at the end. // Then add the missing ones at the end.
missingDependencies.forEach(key => { for (const key of missingDependencies) {
suggestedDependencies.push(key); suggestedDependencies.push(key);
}); }
return { return {
suggestedDependencies, suggestedDependencies,
@ -1545,7 +1582,7 @@ function getConstructionExpressionType(node: Node) {
} }
return null; return null;
case 'TypeCastExpression': case 'TypeCastExpression':
return getConstructionExpressionType(node.expression); case 'AsExpression':
case 'TSAsExpression': case 'TSAsExpression':
return getConstructionExpressionType(node.expression); return getConstructionExpressionType(node.expression);
} }
@ -1623,14 +1660,15 @@ function scanForConstructions({
while (currentScope !== scope && currentScope != null) { while (currentScope !== scope && currentScope != null) {
currentScope = currentScope.upper!; currentScope = currentScope.upper!;
} }
if (currentScope !== scope) { if (
currentScope !== scope &&
// This reference is outside the Hook callback. // This reference is outside the Hook callback.
// It can only be legit if it's the deps array. // It can only be legit if it's the deps array.
if (!isAncestorNodeOf(declaredDependenciesNode, reference.identifier)) { !isAncestorNodeOf(declaredDependenciesNode, reference.identifier)
) {
return true; return true;
} }
} }
}
return false; return false;
} }
@ -1653,7 +1691,6 @@ function getDependency(node: Node): Node {
if ( if (
(parent.type === 'MemberExpression' || parent.type === 'OptionalMemberExpression') && (parent.type === 'MemberExpression' || parent.type === 'OptionalMemberExpression') &&
parent.object === node && parent.object === node &&
parent.property.type === 'Identifier' &&
parent.property.name !== 'current' && parent.property.name !== 'current' &&
!parent.computed && !parent.computed &&
!( !(
@ -1796,7 +1833,7 @@ function getReactiveHookCallbackIndex(
try { try {
name = analyzePropertyChain(node, null); name = analyzePropertyChain(node, null);
} catch (error) { } catch (error) {
if (/Unsupported node type/.test(error.message)) { if (/Unsupported node type/.test((error as Error).message)) {
return 0; return 0;
} else { } else {
throw error; throw error;
@ -1842,12 +1879,12 @@ function fastFindReferenceWithParent(start: Node, target: Node): Node | null {
value.parent = item; value.parent = item;
queue.push(value); queue.push(value);
} else if (Array.isArray(value)) { } else if (Array.isArray(value)) {
value.forEach(val => { for (const val of value) {
if (isNodeLike(val)) { if (isNodeLike(val)) {
val.parent = item; val.parent = item;
queue.push(val); queue.push(val);
} }
}); }
} }
} }
} }
@ -1870,7 +1907,7 @@ function joinEnglish(arr: string[]): string {
return s; return s;
} }
function isNodeLike(val: any): boolean { function isNodeLike(val: unknown): val is Node {
return ( return (
typeof val === 'object' && typeof val === 'object' &&
val !== null && val !== null &&

View File

@ -6,7 +6,6 @@
*/ */
/* global BigInt */ /* global BigInt */
/* eslint-disable no-for-of-loops/no-for-of-loops */
import type { Rule, Scope } from 'eslint'; import type { Rule, Scope } from 'eslint';
import type { import type {
CallExpression, CallExpression,
@ -16,6 +15,7 @@ import type {
Identifier, Identifier,
BaseFunction, BaseFunction,
} from 'estree'; } from 'estree';
import { __EXPERIMENTAL__ } from './index'; import { __EXPERIMENTAL__ } from './index';
/** /**
@ -24,10 +24,7 @@ import { __EXPERIMENTAL__ } from './index';
*/ */
function isHookName(s: string) { function isHookName(s: string) {
if (__EXPERIMENTAL__) { return s === 'use' || /^use[\dA-Z]/.test(s);
return s === 'use' || /^use[A-Z0-9]/.test(s);
}
return /^use[A-Z0-9]/.test(s);
} }
/** /**
@ -94,11 +91,9 @@ function isMemoCallback(node: Rule.Node) {
function isInsideComponentOrHook(node: Rule.Node) { function isInsideComponentOrHook(node: Rule.Node) {
while (node) { while (node) {
const functionName = getFunctionName(node); const functionName = getFunctionName(node);
if (functionName) { if (functionName && (isComponentName(functionName) || isHook(functionName))) {
if (isComponentName(functionName) || isHook(functionName)) {
return true; return true;
} }
}
if (isForwardRefCallback(node) || isMemoCallback(node)) { if (isForwardRefCallback(node) || isMemoCallback(node)) {
return true; return true;
} }
@ -115,10 +110,7 @@ function isUseEffectEventIdentifier(node: Node) {
} }
function isUseIdentifier(node: Node) { function isUseIdentifier(node: Node) {
if (__EXPERIMENTAL__) { return isReactFunction(node as Expression, 'use');
return node.type === 'Identifier' && node.name === 'use';
}
return false;
} }
const rule: Rule.RuleModule = { const rule: Rule.RuleModule = {
@ -161,6 +153,22 @@ const rule: Rule.RuleModule = {
} }
} }
/**
* SourceCode#getText that also works down to ESLint 3.0.0
*/
const getSource =
typeof context.getSource === 'function'
? (node: Node) => context.getSource(node)
: (node: Node) => context.sourceCode.getText(node);
/**
* SourceCode#getScope that also works down to ESLint 3.0.0
*/
const getScope =
typeof context.getScope === 'function'
? () => context.getScope()
: (node: Node) => context.sourceCode.getScope(node);
return { return {
// Maintain code segment path stack as we traverse. // Maintain code segment path stack as we traverse.
onCodePathSegmentStart: segment => codePathSegmentStack.push(segment), onCodePathSegmentStart: segment => codePathSegmentStack.push(segment),
@ -479,7 +487,7 @@ const rule: Rule.RuleModule = {
context.report({ context.report({
node: hook, node: hook,
message: message:
`React Hook "${context.getSource(hook)}" may be executed ` + `React Hook "${getSource(hook)}" may be executed ` +
'more than once. Possibly because it is called in a loop. ' + 'more than once. Possibly because it is called in a loop. ' +
'React Hooks must be called in the exact same order in ' + 'React Hooks must be called in the exact same order in ' +
'every component render.', 'every component render.',
@ -498,7 +506,7 @@ const rule: Rule.RuleModule = {
context.report({ context.report({
node: hook, node: hook,
message: message:
`React Hook "${context.getSource(hook)}" cannot be ` + `React Hook "${getSource(hook)}" cannot be ` +
'called in an async function.', 'called in an async function.',
}); });
} }
@ -513,7 +521,7 @@ const rule: Rule.RuleModule = {
!isUseIdentifier(hook) // `use(...)` can be called conditionally. !isUseIdentifier(hook) // `use(...)` can be called conditionally.
) { ) {
const message = const message =
`React Hook "${context.getSource(hook)}" is called ` + `React Hook "${getSource(hook)}" is called ` +
'conditionally. React Hooks must be called in the exact ' + 'conditionally. React Hooks must be called in the exact ' +
'same order in every component render.' + 'same order in every component render.' +
(possiblyHasEarlyReturn (possiblyHasEarlyReturn
@ -530,15 +538,15 @@ const rule: Rule.RuleModule = {
) { ) {
// Custom message for hooks inside a class // Custom message for hooks inside a class
const message = const message =
`React Hook "${context.getSource(hook)}" cannot be called ` + `React Hook "${getSource(hook)}" cannot be called ` +
'in a class component. React Hooks must be called in a ' + 'in a class component. React Hooks must be called in a ' +
'React function component or a custom React Hook function.'; 'React function component or a custom React Hook function.';
context.report({ node: hook, message }); context.report({ node: hook, message });
} else if (codePathFunctionName) { } else if (codePathFunctionName) {
// Custom message if we found an invalid function name. // Custom message if we found an invalid function name.
const message = const message =
`React Hook "${context.getSource(hook)}" is called in ` + `React Hook "${getSource(hook)}" is called in ` +
`function "${context.getSource(codePathFunctionName)}" ` + `function "${getSource(codePathFunctionName)}" ` +
'that is neither a React function component nor a custom ' + 'that is neither a React function component nor a custom ' +
'React Hook function.' + 'React Hook function.' +
' React component names must start with an uppercase letter.' + ' React component names must start with an uppercase letter.' +
@ -547,7 +555,7 @@ const rule: Rule.RuleModule = {
} else if (codePathNode.type === 'Program') { } else if (codePathNode.type === 'Program') {
// These are dangerous if you have inline requires enabled. // These are dangerous if you have inline requires enabled.
const message = const message =
`React Hook "${context.getSource(hook)}" cannot be called ` + `React Hook "${getSource(hook)}" cannot be called ` +
'at the top level. React Hooks must be called in a ' + 'at the top level. React Hooks must be called in a ' +
'React function component or a custom React Hook function.'; 'React function component or a custom React Hook function.';
context.report({ node: hook, message }); context.report({ node: hook, message });
@ -560,7 +568,7 @@ const rule: Rule.RuleModule = {
// `use(...)` can be called in callbacks. // `use(...)` can be called in callbacks.
if (isSomewhereInsideComponentOrHook && !isUseIdentifier(hook)) { if (isSomewhereInsideComponentOrHook && !isUseIdentifier(hook)) {
const message = const message =
`React Hook "${context.getSource(hook)}" cannot be called ` + `React Hook "${getSource(hook)}" cannot be called ` +
'inside a callback. React Hooks must be called in a ' + 'inside a callback. React Hooks must be called in a ' +
'React function component or a custom React Hook function.'; 'React function component or a custom React Hook function.';
context.report({ node: hook, message }); context.report({ node: hook, message });
@ -612,7 +620,7 @@ const rule: Rule.RuleModule = {
context.report({ context.report({
node, node,
message: message:
`\`${context.getSource( `\`${getSource(
node, node,
)}\` is a function created with React Hook "useEffectEvent", and can only be called from ` + )}\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
'the same component. They cannot be assigned to variables or passed down.', 'the same component. They cannot be assigned to variables or passed down.',
@ -629,14 +637,14 @@ const rule: Rule.RuleModule = {
FunctionDeclaration(node) { FunctionDeclaration(node) {
// function MyComponent() { const onClick = useEffectEvent(...) } // function MyComponent() { const onClick = useEffectEvent(...) }
if (isInsideComponentOrHook(node)) { if (isInsideComponentOrHook(node)) {
recordAllUseEffectEventFunctions(context.getScope()); recordAllUseEffectEventFunctions(getScope(node));
} }
}, },
ArrowFunctionExpression(node) { ArrowFunctionExpression(node) {
// const MyComponent = () => { const onClick = useEffectEvent(...) } // const MyComponent = () => { const onClick = useEffectEvent(...) }
if (isInsideComponentOrHook(node)) { if (isInsideComponentOrHook(node)) {
recordAllUseEffectEventFunctions(context.getScope()); recordAllUseEffectEventFunctions(getScope(node));
} }
}, },
}; };

View File

@ -4,23 +4,30 @@
* This source code is licensed under the MIT license found in the * This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import type { Linter } from 'eslint';
import RulesOfHooks from './RulesOfHooks'; import { Linter } from 'eslint';
import ExhaustiveDeps from './ExhaustiveDeps'; import ExhaustiveDeps from './ExhaustiveDeps';
import { name, version } from './package.json';
import RulesOfHooks from './RulesOfHooks';
export const __EXPERIMENTAL__ = false; export const __EXPERIMENTAL__ = false;
export const configs = { export const flatConfigs = {
recommended: { recommended: {
plugins: ['react-hooks'], name: 'react-hooks/recommended',
plugins: {
'react-hooks': {
meta: { name, version },
rules: {
'rules-of-hooks': RulesOfHooks,
'exhaustive-deps': ExhaustiveDeps,
},
},
},
rules: { rules: {
'react-hooks/rules-of-hooks': 'error', 'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn', 'react-hooks/exhaustive-deps': 'warn',
}, },
} as Linter.BaseConfig, } satisfies Linter.Config,
};
export const rules = {
'rules-of-hooks': RulesOfHooks,
'exhaustive-deps': ExhaustiveDeps,
}; };

View File

@ -1,6 +1,9 @@
{ {
"name": "eslint-plugin-react-hooks",
"version": "4.2.0",
"upstream": { "upstream": {
"version": 1, "version": 1,
"comment": "https://github.com/facebook/react/pull/30774",
"sources": { "sources": {
"main": { "main": {
"repository": "git@github.com:facebook/react.git", "repository": "git@github.com:facebook/react.git",

Submodule packages/jsx-ast-utils updated: 5943318eaf...a8ca8f7033

View File

@ -1,28 +0,0 @@
diff --git a/package.json b/package.json
index 00e5f73..2fa025f 100644
--- a/package.json
+++ b/package.json
@@ -62,8 +62,7 @@
"typecov": "type-coverage"
},
"peerDependencies": {
- "eslint": "*",
- "eslint-plugin-import": "*"
+ "eslint": "*"
},
"dependencies": {
"debug": "^4.3.4",
diff --git a/tsconfig.json b/tsconfig.json
deleted file mode 100644
index a303861..0000000
--- a/tsconfig.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "extends": "@1stg/tsconfig/node16",
- "compilerOptions": {
- "module": "Node16",
- "outDir": "./lib"
- },
- "include": ["./src", "./shim.d.ts"]
-}

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,16 @@
diff --git a/src/index.js b/src/index.js diff --git a/src/index.js b/src/index.js
index 7b931fe..eaea267 100644 index 980081e..3cf8018 100644
--- a/src/index.js --- a/src/index.js
+++ b/src/index.js +++ b/src/index.js
@@ -1,296 +1,344 @@ @@ -1,48 +1,90 @@
/* eslint-disable global-require */ /* eslint-disable global-require */
+// @ts-check -const flatConfigBase = require('./configs/flat-config-base');
-const legacyConfigBase = require('./configs/legacy-config-base');
-const { name, version } = require('../package.json');
+import flatConfigBase from './configs/flat-config-base';
+import legacyConfigBase from './configs/legacy-config-base';
+import { name, version } from '../package.json';
+
+import accessibleEmoji from './rules/accessible-emoji'; +import accessibleEmoji from './rules/accessible-emoji';
+import altText from './rules/alt-text'; +import altText from './rules/alt-text';
+import anchorAmbiguousText from './rules/anchor-ambiguous-text'; +import anchorAmbiguousText from './rules/anchor-ambiguous-text';
@ -45,8 +51,7 @@ index 7b931fe..eaea267 100644
+import scope from './rules/scope'; +import scope from './rules/scope';
+import tabindexNoPositive from './rules/tabindex-no-positive'; +import tabindexNoPositive from './rules/tabindex-no-positive';
-module.exports = { const allRules = {
- rules: {
- 'accessible-emoji': require('./rules/accessible-emoji'), - 'accessible-emoji': require('./rules/accessible-emoji'),
- 'alt-text': require('./rules/alt-text'), - 'alt-text': require('./rules/alt-text'),
- 'anchor-ambiguous-text': require('./rules/anchor-ambiguous-text'), - 'anchor-ambiguous-text': require('./rules/anchor-ambiguous-text'),
@ -86,538 +91,135 @@ index 7b931fe..eaea267 100644
- 'role-supports-aria-props': require('./rules/role-supports-aria-props'), - 'role-supports-aria-props': require('./rules/role-supports-aria-props'),
- scope: require('./rules/scope'), - scope: require('./rules/scope'),
- 'tabindex-no-positive': require('./rules/tabindex-no-positive'), - 'tabindex-no-positive': require('./rules/tabindex-no-positive'),
- }, + 'accessible-emoji': accessibleEmoji,
- configs: { + 'alt-text': altText,
- recommended: { + 'anchor-ambiguous-text': anchorAmbiguousText,
- plugins: [ + 'anchor-has-content': anchorHasContent,
- 'jsx-a11y', + 'anchor-is-valid': anchorIsValid,
+export const rules = kebabCase({ + 'aria-activedescendant-has-tabindex': ariaActivedescendantHasTabindex,
+ accessibleEmoji, + 'aria-props': ariaProps,
+ altText, + 'aria-proptypes': ariaProptypes,
+ anchorAmbiguousText, + 'aria-role': ariaRole,
+ anchorHasContent, + 'aria-unsupported-elements': ariaUnsupportedElements,
+ anchorIsValid, + 'autocomplete-valid': autocompleteValid,
+ ariaActivedescendantHasTabindex, + 'click-events-have-key-events': clickEventsHaveKeyEvents,
+ ariaProps, + 'control-has-associated-label': controlHasAssociatedLabel,
+ ariaProptypes, + 'heading-has-content': headingHasContent,
+ ariaRole, + 'html-has-lang': htmlHasLang,
+ ariaUnsupportedElements, + 'iframe-has-title': iframeHasTitle,
+ autocompleteValid, + 'img-redundant-alt': imgRedundantAlt,
+ clickEventsHaveKeyEvents, + 'interactive-supports-focus': interactiveSupportsFocus,
+ controlHasAssociatedLabel, + 'label-has-associated-control': labelHasAssociatedControl,
+ headingHasContent, + 'label-has-for': labelHasFor,
+ htmlHasLang,
+ iframeHasTitle,
+ imgRedundantAlt,
+ interactiveSupportsFocus,
+ labelHasAssociatedControl,
+ labelHasFor,
+ lang, + lang,
+ mediaHasCaption, + 'media-has-caption': mediaHasCaption,
+ mouseEventsHaveKeyEvents, + 'mouse-events-have-key-events': mouseEventsHaveKeyEvents,
+ noAccessKey, + 'no-access-key': noAccessKey,
+ noAriaHiddenOnFocusable, + 'no-aria-hidden-on-focusable': noAriaHiddenOnFocusable,
+ noAutofocus, + 'no-autofocus': noAutofocus,
+ noDistractingElements, + 'no-distracting-elements': noDistractingElements,
+ 'no-interactive-element-to-noninteractive-role':
+ noInteractiveElementToNoninteractiveRole, + noInteractiveElementToNoninteractiveRole,
+ noNoninteractiveElementInteractions, + 'no-noninteractive-element-interactions': noNoninteractiveElementInteractions,
+ 'no-noninteractive-element-to-interactive-role':
+ noNoninteractiveElementToInteractiveRole, + noNoninteractiveElementToInteractiveRole,
+ noNoninteractiveTabindex, + 'no-noninteractive-tabindex': noNoninteractiveTabindex,
+ noOnChange, + 'no-onchange': noOnChange,
+ noRedundantRoles, + 'no-redundant-roles': noRedundantRoles,
+ noStaticElementInteractions, + 'no-static-element-interactions': noStaticElementInteractions,
+ preferTagOverRole, + 'prefer-tag-over-role': preferTagOverRole,
+ roleHasRequiredAriaProps, + 'role-has-required-aria-props': roleHasRequiredAriaProps,
+ roleSupportsAriaProps, + 'role-supports-aria-props': roleSupportsAriaProps,
+ scope, + scope,
+ tabindexNoPositive, + 'tabindex-no-positive': tabindexNoPositive,
+});
+export const configs = {
+ recommended: {
+ plugins: [
+ 'jsx-a11y',
+ ],
+ 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',
+ ],
+ },
],
- parserOptions: {
- ecmaFeatures: {
- jsx: true,
+ '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'],
},
- },
- 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',
+ ],
+ 'jsx-a11y/no-noninteractive-element-interactions': [
+ 'error',
+ {
+ handlers: [
+ 'onClick',
+ 'onError',
+ 'onLoad',
+ 'onMouseDown',
+ 'onMouseUp',
+ 'onKeyPress',
+ 'onKeyDown',
+ 'onKeyUp',
],
- ignoreRoles: [
- 'grid',
+ 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',
- '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', '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',
- ],
- 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',
+ ol: [
'listbox',
'menu',
'menubar',
'radiogroup',
- 'row',
'tablist',
- 'toolbar',
'tree',
'treegrid',
],
- includeRoles: [
- 'alert',
- 'dialog',
+ li: ['menuitem', '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/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/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',
+ ],
+ 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',
],
- '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'],
- },
+ ignoreRoles: [
+ 'grid',
+ 'listbox',
+ 'menu',
+ 'menubar',
+ 'radiogroup',
+ 'row',
+ 'tablist',
+ 'toolbar',
+ 'tree',
+ 'treegrid',
],
- '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',
},
},
}; };
+
+/** @param {object} obj */ const recommendedRules = {
+function kebabCase(obj) { @@ -294,15 +336,15 @@ const jsxA11y = {
+ return Object.fromEntries( * Given a ruleset and optionally a flat config name, generate a config.
+ Object.entries(obj).map(([key, value]) => [ * @param {object} rules - ruleset for this config
+ key.replace(/([A-Z])/g, '-$1').toLowerCase(), * @param {string} flatConfigName - name for the config if flat
+ value, - * @returns Config for this set of rules.
+ ]), + * @returns {import('eslint').Linter.Config} Config for this set of rules.
*/
const createConfig = (rules, flatConfigName) => ({
...(flatConfigName
? {
- ...flatConfigBase,
- name: `jsx-a11y/${flatConfigName}`,
- plugins: { 'jsx-a11y': jsxA11y },
- }
+ ...flatConfigBase,
+ name: `jsx-a11y/${flatConfigName}`,
+ plugins: { 'jsx-a11y': jsxA11y },
+ }
: { ...legacyConfigBase, plugins: ['jsx-a11y'] }),
rules: { ...rules },
});
@@ -317,4 +359,4 @@ const flatConfigs = {
strict: createConfig(strictRules, 'strict'),
};
-module.exports = Object.assign(jsxA11y, { configs, flatConfigs });
+export default { ...jsxA11y, configs, flatConfigs };
diff --git a/src/rules/autocomplete-valid.js b/src/rules/autocomplete-valid.js
index df7b6b8..c4d0da1 100644
--- a/src/rules/autocomplete-valid.js
+++ b/src/rules/autocomplete-valid.js
@@ -6,7 +6,7 @@
// ----------------------------------------------------------------------------
// Rule Definition
// ----------------------------------------------------------------------------
-import { runVirtualRule } from 'axe-core';
+import axe from 'axe-core';
import { getLiteralPropValue, getProp } from 'jsx-ast-utils';
import { generateObjSchema, arraySchema } from '../util/schemas';
import getElementType from '../util/getElementType';
@@ -24,23 +24,25 @@ export default {
schema: [schema],
},
- create: (context) => {
+ create: context => {
const elementType = getElementType(context);
return {
- JSXOpeningElement: (node) => {
+ JSXOpeningElement: node => {
const options = context.options[0] || {};
const { inputComponents = [] } = options;
const inputTypes = ['input'].concat(inputComponents);
const elType = elementType(node);
- const autocomplete = getLiteralPropValue(getProp(node.attributes, 'autocomplete'));
+ const autocomplete = getLiteralPropValue(
+ getProp(node.attributes, 'autocomplete'),
+ ); + );
+}
if (typeof autocomplete !== 'string' || !inputTypes.includes(elType)) {
return;
}
const type = getLiteralPropValue(getProp(node.attributes, 'type'));
- const { violations } = runVirtualRule('autocomplete-valid', {
+ const { violations } = axe.runVirtualRule('autocomplete-valid', {
nodeName: 'input',
attributes: {
autocomplete,
diff --git a/src/rules/label-has-associated-control.js b/src/rules/label-has-associated-control.js
index d65abe9..22ecee7 100644
--- a/src/rules/label-has-associated-control.js
+++ b/src/rules/label-has-associated-control.js
@@ -11,7 +11,7 @@
import { hasProp, getProp, getPropValue } from 'jsx-ast-utils';
import type { JSXElement } from 'ast-types-flow';
-import minimatch from 'minimatch';
+import { minimatch } from 'minimatch';
import { generateObjSchema, arraySchema } from '../util/schemas';
import type { ESLintConfig, ESLintContext, ESLintVisitorSelectorConfig } from '../../flow/eslint';
import getElementType from '../util/getElementType';
diff --git a/src/util/mayContainChildComponent.js b/src/util/mayContainChildComponent.js diff --git a/src/util/mayContainChildComponent.js b/src/util/mayContainChildComponent.js
index 43a03ef..5e1035e 100644 index 65000a0..09b199a 100644
--- a/src/util/mayContainChildComponent.js --- a/src/util/mayContainChildComponent.js
+++ b/src/util/mayContainChildComponent.js +++ b/src/util/mayContainChildComponent.js
@@ -9,7 +9,7 @@ @@ -9,7 +9,7 @@
@ -629,3 +231,16 @@ index 43a03ef..5e1035e 100644
export default function mayContainChildComponent( export default function mayContainChildComponent(
root: Node, root: Node,
diff --git a/src/util/mayHaveAccessibleLabel.js b/src/util/mayHaveAccessibleLabel.js
index 186ef5e..3dd7d4d 100644
--- a/src/util/mayHaveAccessibleLabel.js
+++ b/src/util/mayHaveAccessibleLabel.js
@@ -11,7 +11,7 @@
import includes from 'array-includes';
import { getPropValue, propName, elementType as rawElementType } from 'jsx-ast-utils';
import type { JSXOpeningElement, Node } from 'ast-types-flow';
-import minimatch from 'minimatch';
+import { minimatch } from 'minimatch';
function tryTrim(value: any) {
return typeof value === 'string' ? value.trim() : value;

View File

@ -1,8 +1,8 @@
diff --git a/lib/index.js b/lib/index.js diff --git a/lib/index.js b/lib/index.js
index def1bbf..6fdff14 100644 index de95218..e30a3df 100644
--- a/lib/index.js --- a/lib/index.js
+++ b/lib/index.js +++ b/lib/index.js
@@ -1,9 +1,9 @@ @@ -1,17 +1,17 @@
"use strict" "use strict"
-const pkg = require("../package.json") -const pkg = require("../package.json")
@ -14,19 +14,18 @@ index def1bbf..6fdff14 100644
+import cjsConfig from "./configs/recommended-script" +import cjsConfig from "./configs/recommended-script"
+import recommendedConfig from "./configs/recommended" +import recommendedConfig from "./configs/recommended"
const rules = { /** @import { ESLint, Linter } from 'eslint' */
"callback-return": require("./rules/callback-return"),
@@ -51,8 +51,8 @@ const rules = {
const mod = { /** @type {ESLint.Plugin} */
const base = {
meta: { meta: {
- name: pkg.name, - name: pkg.name,
- version: pkg.version, - version: pkg.version,
+ name, + name,
+ version, + version,
}, },
rules, rules: {
} "callback-return": require("./rules/callback-return"),
diff --git a/tests/fixtures/no-extraneous/dependencies/node_modules/@bbb/aaa.js b/tests/fixtures/no-extraneous/dependencies/node_modules/@bbb/aaa.js diff --git a/tests/fixtures/no-extraneous/dependencies/node_modules/@bbb/aaa.js b/tests/fixtures/no-extraneous/dependencies/node_modules/@bbb/aaa.js
deleted file mode 100644 deleted file mode 100644
index e69de29..0000000 index e69de29..0000000

View File

@ -1,352 +0,0 @@
diff --git a/.eslintrc b/.eslintrc
deleted file mode 100644
index 4991f200..00000000
--- a/.eslintrc
+++ /dev/null
@@ -1,82 +0,0 @@
-{
- "root": true,
- "extends": ["airbnb-base", "plugin:eslint-plugin/recommended"],
- "plugins": ["eslint-plugin"],
- "env": {
- "es6": true,
- "node": true
- },
- "parserOptions": {
- "ecmaVersion": 6,
- "ecmaFeatures": {
- "jsx": true
- },
- "sourceType": "script",
- },
- "ignorePatterns": [
- "coverage/",
- ".nyc_output/",
- ],
- "rules": {
- "comma-dangle": [2, "always-multiline"],
- "object-shorthand": [2, "always", {
- "ignoreConstructors": false,
- "avoidQuotes": false, // this is the override vs airbnb
- }],
- "max-len": [2, 120, {
- "ignoreStrings": true,
- "ignoreTemplateLiterals": true,
- "ignoreComments": true,
- }],
- "consistent-return": 0,
-
- "prefer-destructuring": [2, { "array": false, "object": false }, { "enforceForRenamedProperties": false }],
- "prefer-object-spread": 0, // until node 8 is required
- "prefer-rest-params": 0, // until node 6 is required
- "prefer-spread": 0, // until node 6 is required
- "function-call-argument-newline": 1, // TODO: enable
- "function-paren-newline": 0,
- "no-plusplus": [2, {"allowForLoopAfterthoughts": true}],
- "no-param-reassign": 1,
- "no-restricted-syntax": [2, {
- "selector": "ObjectPattern",
- "message": "Object destructuring is not compatible with Node v4"
- }],
- "strict": [2, "safe"],
- "valid-jsdoc": [2, {
- "requireReturn": false,
- "requireParamDescription": false,
- "requireReturnDescription": false,
- }],
-
- "eslint-plugin/consistent-output": 0,
- "eslint-plugin/require-meta-docs-description": [2, { "pattern": "^(Enforce|Require|Disallow)" }],
- "eslint-plugin/require-meta-schema": 0,
- "eslint-plugin/require-meta-type": 0
- },
- "overrides": [
- {
- "files": "tests/**",
- "rules": {
- "no-template-curly-in-string": 1,
- },
- },
- {
- "files": "markdown.config.js",
- "rules": {
- "no-console": 0,
- },
- },
- {
- "files": ".github/workflows/*.js",
- "parserOptions": {
- "ecmaVersion": 2019,
- },
- "rules": {
- "camelcase": 0,
- "no-console": 0,
- "no-restricted-syntax": 0,
- },
- },
- ],
-}
diff --git a/index.js b/index.js
index 4140c6c8..03e623af 100644
--- a/index.js
+++ b/index.js
@@ -1,31 +1,25 @@
-'use strict';
-
-const configAll = require('./configs/all');
-const configRecommended = require('./configs/recommended');
-const configRuntime = require('./configs/jsx-runtime');
-
-const allRules = require('./lib/rules');
+import configAll from './configs/all';
+import configRecommended from './configs/recommended';
+import configRuntime from './configs/jsx-runtime';
+import { name } from './package.json';
+export { default as rules } from './lib/rules';
// for legacy config system
-const plugins = [
- 'react',
-];
+const plugins = [name];
+
+export const deprecatedRules = configAll.plugins.react.deprecatedRules;
-module.exports = {
- deprecatedRules: configAll.plugins.react.deprecatedRules,
- rules: allRules,
- configs: {
- recommended: Object.assign({}, configRecommended, {
- parserOptions: configRecommended.languageOptions.parserOptions,
- plugins,
- }),
- all: Object.assign({}, configAll, {
- parserOptions: configAll.languageOptions.parserOptions,
- plugins,
- }),
- 'jsx-runtime': Object.assign({}, configRuntime, {
- parserOptions: configRuntime.languageOptions.parserOptions,
- plugins,
- }),
- },
+export const configs = {
+ recommended: Object.assign({}, configRecommended, {
+ parserOptions: configRecommended.languageOptions.parserOptions,
+ plugins,
+ }),
+ all: Object.assign({}, configAll, {
+ parserOptions: configAll.languageOptions.parserOptions,
+ plugins,
+ }),
+ 'jsx-runtime': Object.assign({}, configRuntime, {
+ parserOptions: configRuntime.languageOptions.parserOptions,
+ plugins,
+ }),
};
diff --git a/lib/rules/button-has-type.js b/lib/rules/button-has-type.js
index 204a33c4..01d992c2 100644
--- a/lib/rules/button-has-type.js
+++ b/lib/rules/button-has-type.js
@@ -5,8 +5,7 @@
'use strict';
-const getProp = require('jsx-ast-utils/getProp');
-const getLiteralPropValue = require('jsx-ast-utils/getLiteralPropValue');
+const { getProp, getLiteralPropValue } = require('jsx-ast-utils');
const docsUrl = require('../util/docsUrl');
const isCreateElement = require('../util/isCreateElement');
const report = require('../util/report');
diff --git a/lib/rules/jsx-fragments.js b/lib/rules/jsx-fragments.js
index 38b4dd8b..d0575572 100644
--- a/lib/rules/jsx-fragments.js
+++ b/lib/rules/jsx-fragments.js
@@ -5,7 +5,7 @@
'use strict';
-const elementType = require('jsx-ast-utils/elementType');
+import { elementType } from 'jsx-ast-utils';
const pragmaUtil = require('../util/pragma');
const variableUtil = require('../util/variable');
const testReactVersion = require('../util/version').testReactVersion;
diff --git a/lib/rules/jsx-key.js b/lib/rules/jsx-key.js
index c13a00cf..fb457e63 100644
--- a/lib/rules/jsx-key.js
+++ b/lib/rules/jsx-key.js
@@ -5,8 +5,7 @@
'use strict';
-const hasProp = require('jsx-ast-utils/hasProp');
-const propName = require('jsx-ast-utils/propName');
+import { hasProp, propName } from 'jsx-ast-utils';
const values = require('object.values');
const docsUrl = require('../util/docsUrl');
const pragmaUtil = require('../util/pragma');
diff --git a/lib/rules/jsx-no-bind.js b/lib/rules/jsx-no-bind.js
index 17e56e2e..cb6dec1a 100644
--- a/lib/rules/jsx-no-bind.js
+++ b/lib/rules/jsx-no-bind.js
@@ -7,7 +7,7 @@
'use strict';
-const propName = require('jsx-ast-utils/propName');
+import { propName } from 'jsx-ast-utils';
const docsUrl = require('../util/docsUrl');
const jsxUtil = require('../util/jsx');
const report = require('../util/report');
diff --git a/lib/rules/jsx-pascal-case.js b/lib/rules/jsx-pascal-case.js
index a1bb4811..db051356 100644
--- a/lib/rules/jsx-pascal-case.js
+++ b/lib/rules/jsx-pascal-case.js
@@ -5,7 +5,7 @@
'use strict';
-const elementType = require('jsx-ast-utils/elementType');
+import { elementType } from 'jsx-ast-utils';
const minimatch = require('minimatch');
const docsUrl = require('../util/docsUrl');
const jsxUtil = require('../util/jsx');
diff --git a/lib/rules/jsx-sort-props.js b/lib/rules/jsx-sort-props.js
index 6d19f201..4b1849cc 100644
--- a/lib/rules/jsx-sort-props.js
+++ b/lib/rules/jsx-sort-props.js
@@ -5,7 +5,7 @@
'use strict';
-const propName = require('jsx-ast-utils/propName');
+import { propName } from 'jsx-ast-utils';
const includes = require('array-includes');
const toSorted = require('array.prototype.tosorted');
diff --git a/lib/rules/no-namespace.js b/lib/rules/no-namespace.js
index 64bbc8d5..b5e9c803 100644
--- a/lib/rules/no-namespace.js
+++ b/lib/rules/no-namespace.js
@@ -5,7 +5,7 @@
'use strict';
-const elementType = require('jsx-ast-utils/elementType');
+import { elementType } from 'jsx-ast-utils';
const docsUrl = require('../util/docsUrl');
const isCreateElement = require('../util/isCreateElement');
const report = require('../util/report');
diff --git a/lib/rules/no-unknown-property.js b/lib/rules/no-unknown-property.js
index 2e77f289..cff14d45 100644
--- a/lib/rules/no-unknown-property.js
+++ b/lib/rules/no-unknown-property.js
@@ -541,7 +541,7 @@ module.exports = {
create(context) {
function getIgnoreConfig() {
- return (context.options[0] && context.options[0].ignore) || DEFAULTS.ignore;
+ return context.options[0]?.ignore || DEFAULTS.ignore;
}
function getRequireDataLowercase() {
@@ -554,7 +554,7 @@ module.exports = {
JSXAttribute(node) {
const ignoreNames = getIgnoreConfig();
const actualName = context.getSourceCode().getText(node.name);
- if (ignoreNames.indexOf(actualName) >= 0) {
+ if (ignoreNames.includes(actualName)) {
return;
}
const name = normalizeAttributeCase(actualName);
@@ -582,6 +582,15 @@ module.exports = {
const tagName = getTagName(node);
+ if (
+ (actualName === 'css' && hasEmotion) ||
+ (tagName === 'style' &&
+ (actualName === 'global' || actualName === 'jsx') &&
+ hasNext)
+ ) {
+ return;
+ }
+
if (tagName === 'fbt' || tagName === 'fbs') { return; } // fbt/fbs nodes are bonkers, let's not go there
if (!isValidHTMLTagInJSX(node)) { return; }
diff --git a/lib/util/annotations.js b/lib/util/annotations.js
index 60aaef8c..ad8dc0bf 100644
--- a/lib/util/annotations.js
+++ b/lib/util/annotations.js
@@ -27,6 +27,6 @@ function isAnnotatedFunctionPropsDeclaration(node, context) {
return (isAnnotated && (isDestructuredProps || isProps));
}
-module.exports = {
+export {
isAnnotatedFunctionPropsDeclaration,
};
diff --git a/lib/util/ast.js b/lib/util/ast.js
index fd6019a3..3cbc293e 100644
--- a/lib/util/ast.js
+++ b/lib/util/ast.js
@@ -4,7 +4,7 @@
'use strict';
-const estraverse = require('estraverse');
+import estraverse from 'estraverse';
// const pragmaUtil = require('./pragma');
/**
@@ -428,7 +428,7 @@ function isTSTypeParameterInstantiation(node) {
return nodeType === 'TSTypeParameterInstantiation';
}
-module.exports = {
+export {
traverse,
findReturnStatement,
getFirstNodeInLine,
diff --git a/lib/util/jsx.js b/lib/util/jsx.js
index 55073bfe..efc07af1 100644
--- a/lib/util/jsx.js
+++ b/lib/util/jsx.js
@@ -4,7 +4,7 @@
'use strict';
-const elementType = require('jsx-ast-utils/elementType');
+import { elementType } from 'jsx-ast-utils';
const astUtil = require('./ast');
const isCreateElement = require('./isCreateElement');
diff --git a/tsconfig.json b/tsconfig.json
deleted file mode 100644
index 39187b7f..00000000
--- a/tsconfig.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "compilerOptions": {
- "target": "ES2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
- "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
- // "lib": ["es2015"], /* Specify library files to be included in the compilation. */
- "allowJs": true, /* Allow javascript files to be compiled. */
- "checkJs": true, /* Report errors in .js files. */
- "noEmit": true, /* Do not emit outputs. */
- "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
-
- /* Strict Type-Checking Options */
- // "strict": true, /* Enable all strict type-checking options. */
- "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */
- // "strictNullChecks": true, /* Enable strict null checks. */
- // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
- "strictFunctionTypes": true, /* Enable strict checking of function types. */
- "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
- "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
- "alwaysStrict": false, /* Parse in strict mode and emit "use strict" for each source file. */
- "resolveJsonModule": true
- },
- "include": ["lib"],
-}

View File

@ -0,0 +1,25 @@
diff --git a/dist/parseSettings/createParseSettings.js b/dist/parseSettings/createParseSettings.js
index 4c8b40ae895d45bd7dfcf64c8e49e29ce48dd663..0a62880ff50b7341fa909155293cbdb77fa99c97 100644
--- a/dist/parseSettings/createParseSettings.js
+++ b/dist/parseSettings/createParseSettings.js
@@ -1,4 +1,5 @@
"use strict";
+var fs = require("node:fs");
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -89,10 +90,12 @@ function createParseSettings(code, tsestreeOptions = {}) {
tsestreeOptions.extraFileExtensions.every(ext => typeof ext === 'string')
? tsestreeOptions.extraFileExtensions
: [],
- filePath: (0, shared_1.ensureAbsolutePath)(typeof tsestreeOptions.filePath === 'string' &&
+ filePath: fs.realpathSync(
+ (0, shared_1.ensureAbsolutePath)(typeof tsestreeOptions.filePath === 'string' &&
tsestreeOptions.filePath !== '<input>'
? tsestreeOptions.filePath
- : getFileName(tsestreeOptions.jsx), tsconfigRootDir),
+ : getFileName(tsestreeOptions.jsx), tsconfigRootDir)
+ ),
jsDocParsingMode,
jsx: tsestreeOptions.jsx === true,
loc: tsestreeOptions.loc === true,

View File

@ -0,0 +1,13 @@
diff --git a/dist/eslint-utils/getParserServices.js b/dist/eslint-utils/getParserServices.js
index 3b3020f601ba9cc92fdaf643ee3a8bdc44d1291a..730fccd5838b388b496a8861705e0d9883fc2fcb 100644
--- a/dist/eslint-utils/getParserServices.js
+++ b/dist/eslint-utils/getParserServices.js
@@ -24,7 +24,7 @@ function getParserServices(context, allowWithoutFullTypeInformation = false) {
// this forces the user to supply parserOptions.project
if (context.sourceCode.parserServices.program == null &&
!allowWithoutFullTypeInformation) {
- throwError(parser);
+ // throwError(parser);
}
return context.sourceCode.parserServices;
}

View File

@ -0,0 +1,11 @@
diff --git a/dist/helpers/check-diagnostics-errors.js b/dist/helpers/check-diagnostics-errors.js
index 3ff0a59509fe381189764a253e6b668241e3b921..9b1eadf36278cea8dadc6cb5cfed4c4a89e91609 100644
--- a/dist/helpers/check-diagnostics-errors.js
+++ b/dist/helpers/check-diagnostics-errors.js
@@ -20,6 +20,5 @@ function checkDiagnosticsErrors(diagnostics, failMessage) {
return;
}
(0, logger_1.errorLog)(ts.formatDiagnostics(diagnostics, formatDiagnosticsHost).trim());
- throw new Error(failMessage);
}
exports.checkDiagnosticsErrors = checkDiagnosticsErrors;

8231
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,217 +1,23 @@
#!/usr/bin/env tsx #!/usr/bin/env tsx
import assert from 'node:assert'; import { promises as fs } from 'node:fs';
import { readFileSync, promises as fs } from 'node:fs';
import { resolve, extname, relative } from 'node:path';
import { isBuiltin } from 'node:module'; import { isBuiltin } from 'node:module';
import esbuild from 'esbuild'; import { relative, resolve } from 'node:path';
import type { Loader, Plugin } from 'esbuild';
import * as babel from '@babel/core';
import { memoize } from 'lodash';
import { gray, green } from 'picocolors';
import type { types as t, types } from '@babel/core';
import { dependencies } from '../dist/package.json';
import { createMacro, type MacroHandler } from 'babel-plugin-macros';
import * as polyfill from '../src/polyfill';
import { buildLocalRules } from '../src/build-local-rules';
import { execSync } from 'node:child_process';
const polyfills = Object.keys(polyfill); import esbuild from 'esbuild';
import type { Plugin } from 'esbuild';
import { memoize } from 'lodash-es';
import c from 'picocolors';
import { minify_sync } from 'terser';
import { dependencies } from '../dist/package.json';
import { dts } from './dts';
import { babelPlugin } from './modifier';
const ENV = (process.env.NODE_ENV ??= 'production'); const ENV = (process.env.NODE_ENV ??= 'production');
const PROD = ENV === 'production'; const PROD = ENV === 'production';
class HandlerMap { const { gray, green } = c;
map = new Map<string, MacroHandler>();
set(names: string | string[], handler: MacroHandler) {
names = Array.isArray(names) ? names : [names];
const macro = createMacro(handler);
for (const name of names) {
this.map.set(name, macro);
}
return this;
}
get keys() {
return Array.from(this.map.keys());
}
resolvePath = (module: string) => module;
require = (module: string) => this.map.get(module);
isMacrosName = (module: string) => this.map.has(module);
}
const map = new HandlerMap()
.set(
'object.assign',
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('assign'))),
)
.set(
['object-values', 'object.values'],
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('values'))),
)
.set(
'object.fromentries',
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('fromEntries'))),
)
.set(
'object.entries',
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('entries'))),
)
.set(
'hasown',
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('hasOwn'))),
)
.set(
'has',
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('hasOwn'))),
)
.set(
'array-includes',
proto(t => t.identifier('includes')),
)
.set(
'array.prototype.flatmap',
proto(t => t.identifier('flatMap')),
)
.set(
'array.prototype.flat',
proto(t => t.identifier('flat')),
)
.set(
'array.prototype.findlastindex',
proto(t => t.identifier('findLastIndex')),
)
.set(
'array.prototype.tosorted',
proto(t => t.identifier('toSorted')),
)
.set(
'array.prototype.toreversed',
proto(t => t.identifier('toReversed')),
)
.set(
'array.prototype.findlast',
proto(t => t.identifier('findLast')),
)
.set(
'string.prototype.matchall',
proto(t => t.identifier('matchAll')),
)
.set(
'string.prototype.includes',
proto(t => t.identifier('includes')),
)
.set(
'object.groupby',
replace(t =>
t.memberExpression(
t.callExpression(t.identifier('require'), [t.stringLiteral('lodash')]),
t.identifier('groupBy'),
),
),
);
// es-iterator-helpers/Iterator.prototype.*
const polyfillPath = resolve(__dirname, '../src/polyfill.ts');
const requirePolyfill = (t: typeof types, name: string) =>
t.memberExpression(
t.callExpression(t.identifier('require'), [t.stringLiteral(polyfillPath)]),
t.identifier(name),
);
map.set(
`es-iterator-helpers/Iterator.from`,
replace(t => requirePolyfill(t, 'from')),
);
for (const name of polyfills) {
map.set(
`es-iterator-helpers/Iterator.prototype.${name}`,
replace(t => requirePolyfill(t, name)),
);
}
map.set(
'safe-regex-test',
replace(t => requirePolyfill(t, 'safeRegexTest')),
);
function replace(getReplacement: (types: typeof t) => t.Expression): MacroHandler {
return ({ references, babel: { types: t } }) => {
references.default.forEach(referencePath => {
referencePath.replaceWith(getReplacement(t));
});
};
}
function proto(getProperty: (types: typeof t) => t.Expression): MacroHandler {
return ({ references, babel: { types: t } }) => {
references.default.forEach(referencePath => {
const { parent, parentPath } = referencePath;
assert(t.isCallExpression(parent));
const [callee, ...rest] = parent.arguments;
parentPath!.replaceWith(
t.callExpression(
t.memberExpression(callee as t.Expression, getProperty(t)),
rest,
),
);
});
};
}
export const babelPlugin: Plugin = {
name: 'babel',
setup(build) {
const { keys, ...macroOptions } = map;
build.onLoad({ filter: /\.[jt]sx?$/ }, args => {
const { path } = args;
if (path.includes('node_modules/')) {
return null;
}
let source = readFileSync(path, 'utf-8')
.replaceAll("require('object.hasown/polyfill')()", 'Object.hasOwn')
.replaceAll("require('object.fromentries/polyfill')()", 'Object.fromEntries')
.replaceAll(
"Object.keys(require('prop-types'))",
JSON.stringify(Object.keys(require('prop-types'))),
);
if (
path.includes('packages/eslint-plugin-import/src/rules/') ||
path.includes('packages/eslint-plugin-import/config/')
) {
source = source.replace('\nmodule.exports = {', '\nexport default {');
}
const isFlow = source.includes('@flow');
const loader = extname(path).slice(1) as Loader;
if (!isFlow && !keys.some(key => source.includes(key))) {
return { contents: source, loader };
}
const res = babel.transformSync(source, {
filename: path,
babelrc: false,
configFile: false,
parserOpts: {
plugins: [isFlow ? 'flow' : 'typescript'],
},
plugins: [
isFlow && '@babel/plugin-transform-flow-strip-types',
['babel-plugin-macros', macroOptions],
].filter(Boolean),
})!;
return {
contents: res.code!,
loader,
};
});
},
};
declare global { declare global {
interface Array<T> { interface Array<T> {
@ -266,12 +72,10 @@ if (process.env.DEBUG) {
async function bundle( async function bundle(
entry: string, entry: string,
outfile = entry outfile: string,
.replace('./packages/', './dist/') options?: esbuild.BuildOptions & { treeShaking?: boolean },
.replace('src/', '')
.replace('.ts', '.js'),
) { ) {
await esbuild.build({ const output = await esbuild.build({
entryPoints: [entry], entryPoints: [entry],
outfile, outfile,
bundle: true, bundle: true,
@ -281,14 +85,37 @@ async function bundle(
sourcemap: 'linked', sourcemap: 'linked',
plugins, plugins,
define: {}, define: {},
alias: { alias: {},
// esm modules format: 'esm',
},
external: ['find-cache-dir'],
banner: { banner: {
js: '/* eslint-disable */', js: '/* eslint-disable */',
}, },
...options,
}); });
if (options?.treeShaking) {
const [text, setText] = await useText(outfile);
const minified = minify_sync(text, {
module: true,
compress: {
conditionals: true,
dead_code: true,
defaults: false,
evaluate: true,
passes: 3,
pure_new: true,
side_effects: true,
unused: true,
},
mangle: false,
format: {
comments: true,
},
});
await setText(minified.code!);
}
return output;
} }
async function editPackageJson() { async function editPackageJson() {
@ -311,46 +138,58 @@ async function editPackageJson() {
} }
async function useText(path: string) { async function useText(path: string) {
const state = await fs.readFile(path, 'utf-8'); const state = await fs.readFile(path, 'utf8');
const setState = (text: string) => fs.writeFile(path, text); const setState = (text: string) => fs.writeFile(path, text);
return [state, setState] as const; return [state, setState] as const;
} }
function bundleType(source: string, output: string) {
try {
return dts({
source,
dist: output,
project: './tsconfig.build.json',
});
} catch {
// noop
}
}
async function main() { async function main() {
console.log('Building local rules...'); console.log('Building type definitions…');
await buildLocalRules(); try {
await fs.rm('dist/config', { recursive: true });
} catch {
// noop
}
console.log('Building type definitions...'); bundleType('./src/prettier.ts', './dist/prettier.d.ts');
execSync( bundleType('./src/types.ts', './dist/types.d.ts');
[
'npx',
'dts-bundle-generator',
'"./src/index.ts"',
'-o',
'"./dist/index.d.ts"',
'--project',
'"./tsconfig.build.json"',
'--no-check',
].join(' '),
);
console.log('Building packages...'); const unminify = { minify: false };
console.log('Building packages…');
await Promise.all([ await Promise.all([
bundle('./packages/eslint-plugin-react/index.js'), bundle('./src/index.ts', undefined!, {
bundle('./packages/eslint-plugin-import/src/index.js'), format: 'esm',
bundle('./packages/eslint-plugin-jsx-a11y/src/index.js'), splitting: true,
bundle('./packages/eslint-plugin-react-hooks/index.ts'), outdir: './dist/config',
bundle('./packages/eslint-plugin-n/lib/index.js', './dist/eslint-plugin-n/index.js'), ...unminify,
bundle('./packages/eslint-import-resolver-typescript/src/index.ts'), }),
bundle('./src/rules/index.ts', './dist/eslint-plugin-rules/index.js'), bundle('./src/types.ts', './dist/types.js', unminify),
bundle('./src/local/index.ts', './dist/eslint-plugin-local/index.js'), bundle('./src/prettier.ts', './dist/prettier.js', unminify),
bundle('./src/index.ts', './dist/index.js'), bundle('./src/install.ts', './dist/install.js', {
treeShaking: true,
minify: false,
banner: {
js: '#!/usr/bin/env node\n/* eslint-disable */',
},
}),
editPackageJson(), editPackageJson(),
]); ]);
console.log('Removing redirect...'); // bundleType('./src/index.ts', './dist/config/index.d.ts');
const [distIndex, setDistIndex] = await useText('./dist/index.js'); await fs.copyFile('./src/config.d.ts', './dist/config/index.d.ts');
await setDistIndex(distIndex.replace(/import.*redirect.*;/g, ''));
} }
void main(); void main();

View File

@ -1,14 +1,14 @@
#!/usr/bin/env bun #!/usr/bin/env bun
import fs from 'node:fs';
import { builtinModules } from 'node:module';
import glob from 'fast-glob'; import glob from 'fast-glob';
import fs from 'fs'; import { uniq } from 'lodash-es';
import { builtinModules } from 'module';
import { uniq } from 'lodash'; import { dependencies, peerDependencies } from '../dist/package.json';
import { dependencies, peerDependencies, overrides } from '../dist/package.json';
function checkImports() { function checkImports() {
const deps = Object.keys({ ...dependencies, ...peerDependencies, ...overrides }).concat( const deps = Object.keys({ ...dependencies, ...peerDependencies }).concat('eslint');
'eslint',
);
const builtIn = new Set(builtinModules.flatMap(module => [module, `node:${module}`])); const builtIn = new Set(builtinModules.flatMap(module => [module, `node:${module}`]));
function findRequires(text: string) { function findRequires(text: string) {

31
scripts/dts.ts Normal file
View File

@ -0,0 +1,31 @@
#!/usr/bin/env node
import {
type EntryPointConfig,
generateDtsBundle,
} from 'dts-bundle-generator/dist/bundle-generator';
import * as ts from 'typescript';
export function dts({
source,
dist,
project,
}: {
source: string;
dist: string;
project: string;
}): void {
const entry: EntryPointConfig = {
filePath: source,
failOnClass: false,
output: {
exportReferencedTypes: true,
},
};
const generatedDts = generateDtsBundle([entry], {
preferredConfigPath: project,
followSymlinks: true,
});
ts.sys.writeFile(dist, generatedDts[0]);
}

200
scripts/modifier.ts Normal file
View File

@ -0,0 +1,200 @@
#!/usr/bin/env tsx
import assert from 'node:assert';
import { readFileSync } from 'node:fs';
import { extname, resolve } from 'node:path';
import * as babel from '@babel/core';
import type { types as t, types } from '@babel/core';
import babelMacros, { type MacroHandler } from 'babel-plugin-macros';
import type { Loader, Plugin } from 'esbuild';
import PropTypes from 'prop-types';
import * as polyfill from '../src/polyfill';
const polyfills = Object.keys(polyfill);
class HandlerMap {
map = new Map<string, MacroHandler>();
set(names: string | string[], handler: MacroHandler) {
names = Array.isArray(names) ? names : [names];
const macro = babelMacros.createMacro(handler);
for (const name of names) {
this.map.set(name, macro);
}
return this;
}
get keys() {
return [...this.map.keys()];
}
resolvePath = (module: string) => module;
require = (module: string) => this.map.get(module);
isMacrosName = (module: string) => this.map.has(module);
}
const map = new HandlerMap()
.set(
'object.assign',
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('assign'))),
)
.set(
['object-values', 'object.values'],
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('values'))),
)
.set(
'object.fromentries',
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('fromEntries'))),
)
.set(
'object.entries',
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('entries'))),
)
.set(
'hasown',
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('hasOwn'))),
)
.set(
'has',
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('hasOwn'))),
)
.set(
'array-includes',
proto(t => t.identifier('includes')),
)
.set(
'array.prototype.flatmap',
proto(t => t.identifier('flatMap')),
)
.set(
'array.prototype.flat',
proto(t => t.identifier('flat')),
)
.set(
'array.prototype.findlastindex',
proto(t => t.identifier('findLastIndex')),
)
.set(
'array.prototype.tosorted',
proto(t => t.identifier('toSorted')),
)
.set(
'array.prototype.toreversed',
proto(t => t.identifier('toReversed')),
)
.set(
'array.prototype.findlast',
proto(t => t.identifier('findLast')),
)
.set(
'string.prototype.matchall',
proto(t => t.identifier('matchAll')),
)
.set(
'string.prototype.includes',
proto(t => t.identifier('includes')),
)
.set(
'object.groupby',
replace(t =>
t.memberExpression(
t.callExpression(t.identifier('require'), [t.stringLiteral('lodash-es')]),
t.identifier('groupBy'),
),
),
);
// es-iterator-helpers/Iterator.prototype.*
const polyfillPath = resolve(import.meta.dirname, '../src/polyfill.ts');
const requirePolyfill = (t: typeof types, name: string) =>
t.memberExpression(
t.callExpression(t.identifier('require'), [t.stringLiteral(polyfillPath)]),
t.identifier(name),
);
map.set(
`es-iterator-helpers/Iterator.from`,
replace(t => requirePolyfill(t, 'from')),
);
for (const name of polyfills) {
map.set(
`es-iterator-helpers/Iterator.prototype.${name}`,
replace(t => requirePolyfill(t, name)),
);
}
map.set(
'safe-regex-test',
replace(t => requirePolyfill(t, 'safeRegexTest')),
);
function replace(getReplacement: (types: typeof t) => t.Expression): MacroHandler {
return ({ references, babel: { types: t } }) => {
for (const referencePath of references.default) {
referencePath.replaceWith(getReplacement(t));
}
};
}
function proto(getProperty: (types: typeof t) => t.Expression): MacroHandler {
return ({ references, babel: { types: t } }) => {
for (const referencePath of references.default) {
const { parent, parentPath } = referencePath;
assert(t.isCallExpression(parent));
const [callee, ...rest] = parent.arguments;
parentPath!.replaceWith(
t.callExpression(
t.memberExpression(callee as t.Expression, getProperty(t)),
rest,
),
);
}
};
}
export const babelPlugin: Plugin = {
name: 'babel',
setup(build) {
const { keys, ...macroOptions } = map;
build.onLoad({ filter: /\.[jt]sx?$/ }, args => {
const { path } = args;
if (path.includes('node_modules/')) {
return null;
}
const source = readFileSync(path, 'utf8')
.replaceAll("require('object.hasown/polyfill')()", 'Object.hasOwn')
.replaceAll("require('object.fromentries/polyfill')()", 'Object.fromEntries')
.replaceAll(
"Object.keys(require('prop-types'))",
JSON.stringify(Object.keys(PropTypes)),
);
const isFlow = source.includes('@flow');
const loader = extname(path).slice(1) as Loader;
if (!isFlow && !keys.some(key => source.includes(key))) {
return { contents: source, loader };
}
const res = babel.transformSync(source, {
filename: path,
babelrc: false,
configFile: false,
parserOpts: {
plugins: [isFlow ? 'flow' : 'typescript'],
},
plugins: [
isFlow && '@babel/plugin-transform-flow-strip-types',
['babel-plugin-macros', macroOptions],
].filter(Boolean),
})!;
return {
contents: res.code!,
loader,
};
});
},
};

View File

@ -1,19 +1,25 @@
#!/bin/bash #!/bin/bash
set -e set -e
git-pull() {
name=$1
git config pull.rebase true
git config rebase.autoStash true
git pull --quiet
ref=$(git log -1 --pretty='{"hash":"%H","date":"%aI","committer":"%cn","subject":"%s"}')
yq -iP ".$name=$ref" ../../src/commits.json -o json
}
pull() { pull() {
echo "🚛 Pulling $2" echo "🚛 Pulling $2"
if [ ! -d "packages/$2" ]; then if [ ! -d "packages/$2" ]; then
echo "📦 Repository not found, cloning..." echo "📦 Repository not found, cloning..."
git clone "https://github.com/$1/$2.git" "packages/$2" git clone "https://github.com/$1/$2.git" "packages/$2"
fi fi
(cd "packages/$2" && git config pull.rebase true && git config rebase.autoStash true && git pull) (cd "packages/$2" && git-pull "$2")
echo echo
} }
pull import-js eslint-import-resolver-typescript
pull import-js eslint-plugin-import
pull jsx-eslint eslint-plugin-jsx-a11y pull jsx-eslint eslint-plugin-jsx-a11y
pull eslint-community eslint-plugin-n pull eslint-community eslint-plugin-n
pull jsx-eslint eslint-plugin-react
pull jsx-eslint jsx-ast-utils pull jsx-eslint jsx-ast-utils

View File

@ -3,9 +3,6 @@ sync() (
cd "packages/$1" && git diff HEAD > "../../patch/$1.patch" cd "packages/$1" && git diff HEAD > "../../patch/$1.patch"
) )
sync eslint-import-resolver-typescript
sync eslint-plugin-import
sync eslint-plugin-jsx-a11y sync eslint-plugin-jsx-a11y
sync eslint-plugin-n sync eslint-plugin-n
sync eslint-plugin-react
sync jsx-ast-utils sync jsx-ast-utils

14
scripts/sync-deps.ts Executable file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env node
import fs from 'node:fs';
const up = JSON.parse(
fs.readFileSync('package.json', 'utf8'),
) as typeof import('../package.json');
const down = JSON.parse(
fs.readFileSync('dist/package.json', 'utf8'),
) as typeof import('../dist/package.json');
down.dependencies = up.dependencies;
fs.writeFileSync('dist/package.json', JSON.stringify(down, null, 2));

View File

@ -1,30 +0,0 @@
#!/usr/bin/env tsx
import { promises as fs } from 'node:fs';
import { camelCase } from 'lodash';
export async function buildLocalRules() {
const files = (await fs.readdir('./src/rules'))
.filter(file => file.endsWith('.ts'))
.filter(file => file !== 'index.ts')
.map(file => file.slice(0, -3));
const entryFile = /* js */ `
import type { Rule } from 'eslint';
import type { ESLintUtils } from '@typescript-eslint/utils';
${files.map(file => `import ${camelCase(file)} from './${file}';`).join('\n')}
export const rules: Record<
string,
Rule.RuleModule | ESLintUtils.RuleModule<string, unknown[]>
> = {
${files.map(file => `'${file}': ${camelCase(file)},`).join('\n ')}
};
`.trim();
await fs.writeFile('./src/rules/index.ts', entryFile + '\n');
}
if (require.main === module) {
buildLocalRules();
}

32
src/commits.json Normal file
View File

@ -0,0 +1,32 @@
{
"eslint-plugin-import": {
"hash": "6554bd5c30976290024cecc44ef1e96746cf3cf7",
"date": "2024-05-23T12:47:41-07:00",
"committer": "Jordan Harband",
"subject": "[meta] add `repository.directory` field"
},
"eslint-plugin-jsx-a11y": {
"hash": "a7d1a12a6198d546c4a06477b385b4fde03b762e",
"date": "2025-06-05T12:28:53-07:00",
"committer": "Jordan Harband",
"subject": "[Tests] fix linting errors introduced in 2d9ad55"
},
"eslint-plugin-n": {
"hash": "42464abe64c5cefb709e8e0a9072b6bb1cd7fcdc",
"date": "2025-06-13T01:37:54+08:00",
"committer": "GitHub",
"subject": "chore(master): release 17.20.0 (#448)"
},
"eslint-plugin-react": {
"hash": "983b88dd3cb5e07919517d3fde4085f60883ded7",
"date": "2024-07-24T15:26:33-07:00",
"committer": "Jordan Harband",
"subject": "[Tests] `no-array-index-key`: actually run valid tests"
},
"jsx-ast-utils": {
"hash": "a8ca8f70331b02db537b0b5cf72ea10e3d6c9377",
"date": "2025-02-20T08:51:06-08:00",
"committer": "Jordan Harband",
"subject": "[Dev Deps] pin `psl` due to breaking change in a minor version"
}
}

64
src/config.d.ts vendored Normal file
View File

@ -0,0 +1,64 @@
import type { FlatESLintConfig } from '@aet/eslint-define-config';
import type { Linter } from 'eslint';
type MiddlewareResult = Linter.Config | Linter.Config[];
export type Middleware =
| (() => Promise<MiddlewareResult>)
| (() => Promise<{ default: MiddlewareResult }>);
export type Environment =
| 'jsdoc'
| 'lingui'
| 'react'
| 'reactQuery'
| 'reactRefresh'
| 'storybook'
| 'tailwind'
| 'testingLibrary'
| 'vitest';
export interface NormalizedExtendConfigOptions {
auto?: boolean;
middlewares?: Middleware[];
configs: FlatESLintConfig[];
/**
* Use `.gitignore` file to exclude files from ESLint.
*/
gitignore?: boolean;
env?: {
[key in Environment]?: boolean;
};
}
export type ExtendConfigOptions =
| FlatESLintConfig
| FlatESLintConfig[]
| NormalizedExtendConfigOptions;
/**
* Returns a ESLint config object.
*
* By default, it includes `["@typescript-eslint", "import-x", "prettier", "unicorn"]` configs.
* Additional bundled plugins include:
*
* 1. [`react`](https://github.com/jsx-eslint/eslint-plugin-react#list-of-supported-rules)
* (automatically enables
* [`react-hooks`](https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks))
* 2. [`react-refresh`](https://github.com/ArnaudBarre/eslint-plugin-react-refresh)
* 3. [`jsx-a11y`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y#supported-rules)
* 4. [`unicorn`](https://github.com/sindresorhus/eslint-plugin-unicorn#rules)
* 5. [`n`](https://github.com/eslint-community/eslint-plugin-n#-rules) (Node.js specific,
* requires `minimatch`)
* 6. [`jsdoc`](https://github.com/gajus/eslint-plugin-jsdoc#rules)
*
* Non bundled:
* 1. [`graphql`](https://the-guild.dev/graphql/eslint/rules)
*
* @returns ESLint configuration object.
*/
export function extendConfig(options?: ExtendConfigOptions): Promise<FlatESLintConfig[]>;
export const error = 'error';
export const warn = 'warn';
export const off = 'off';

44
src/custom/index.ts Normal file
View File

@ -0,0 +1,44 @@
import type { ESLint } from 'eslint';
import noEmptyObjectLiteral from './no-empty-object-literal';
import noImportDot from './no-import-dot';
import noUselessImportAlias from './no-useless-import-alias';
import requireImportAttribute from './require-import-attribute';
import restrictTemplateExpressions from './restrict-template-expressions';
type RuleLevel = 'error' | 'warn' | 'off' | 0 | 1 | 2;
type RuleEntry<Options> = RuleLevel | [RuleLevel, Partial<Options>];
export interface LocalRuleOptions {
/** Bans import from the specifier '.' and '..' and replaces it with '.+/index' */
'aet/no-import-dot': RuleEntry<unknown>;
/**
* Enforce template literal expressions to be of `string` type
* @see [restrict-template-expressions](https://typescript-eslint.io/rules/restrict-template-expressions)
*/
'typed-aet/restrict-template-expressions': RuleEntry<{ allow: string[] }>;
/** Ban assignment of empty object literals `{}` and replace them with `Object.create(null)` */
'aet/no-empty-object-literal': RuleEntry<unknown>;
/** Ban useless import alias */
'aet/no-useless-import-alias': RuleEntry<unknown>;
/** Require the use of `{ type: "json" }` for JSON imports. */
'aet/require-import-attribute': RuleEntry<unknown>;
}
export const plugin: ESLint.Plugin = {
name: 'aet',
rules: {
'no-empty-object-literal': noEmptyObjectLiteral,
'no-import-dot': noImportDot,
'no-useless-import-alias': noUselessImportAlias,
'require-import-attribute': requireImportAttribute,
},
};
export const typedPlugin: ESLint.Plugin = {
name: 'typed-aet',
rules: {
// @ts-expect-error type mismatch
'restrict-template-expressions': restrictTemplateExpressions,
},
};

View File

@ -1,7 +1,6 @@
import type { Rule } from 'eslint'; import { defineRule } from '../types';
const rule: Rule.RuleModule = { export default defineRule({
meta: {
type: 'problem', type: 'problem',
docs: { docs: {
description: description:
@ -10,7 +9,7 @@ const rule: Rule.RuleModule = {
recommended: true, recommended: true,
}, },
fixable: 'code', fixable: 'code',
},
create: context => ({ create: context => ({
AssignmentExpression(node) { AssignmentExpression(node) {
if ( if (
@ -27,6 +26,4 @@ const rule: Rule.RuleModule = {
} }
}, },
}), }),
}; });
export default rule;

View File

@ -0,0 +1,45 @@
import type { Rule } from 'eslint';
import type { Position } from 'estree';
const rule: Rule.RuleModule = {
meta: {
type: 'problem',
docs: {
description:
"Ban useless import aliasing like `import { abc as abc } from 'module'`",
category: 'Best Practices',
recommended: true,
},
fixable: 'code',
},
create(context) {
return {
ImportDeclaration(node) {
if (node.specifiers.length === 0) return;
for (const specifier of node.specifiers) {
if (specifier.type !== 'ImportSpecifier') continue;
const { imported, local } = specifier;
if (
imported.name === local.name &&
!arePositionsEqual(imported.loc!.start, local.loc!.start)
) {
context.report({
node: specifier,
message: `Useless aliasing of '${imported.name}'?`,
fix(fixer) {
return fixer.removeRange([imported.range![1], local.range![1]]);
},
});
}
}
},
};
},
};
const arePositionsEqual = (a: Position, b: Position) =>
a.line === b.line && a.column === b.column;
export default rule;

View File

@ -0,0 +1,61 @@
import type { Rule } from 'eslint';
import type { Identifier, SimpleLiteral } from 'estree';
interface ImportAttribute {
key: Identifier | SimpleLiteral;
value: SimpleLiteral;
}
const requireImportAttribute: Rule.RuleModule = {
meta: {
type: 'problem',
docs: {
description:
'Requires JSON imports to have a "with { type: \'json\' }" import attribute.',
category: 'Possible Errors',
recommended: false,
},
fixable: 'code',
schema: [],
messages: {
missingJsonTypeAttribute:
'JSON imports must have a "with { type: \'json\' }" import attribute.',
},
},
create(context) {
return {
ImportDeclaration(node) {
// Only check modules that end with ".json"
if (
typeof node.source.value === 'string' &&
node.source.value.endsWith('.json')
) {
// Cast the node to our extended interface to access the assertions.
const importNode = node;
const assertions = (
importNode as {
assertions?: ImportAttribute[];
}
).assertions;
const hasJsonType = assertions?.some(attr => {
const keyName =
(attr.key as Identifier).name ?? (attr.key as SimpleLiteral).value;
return keyName === 'type' && attr.value.value === 'json';
});
if (!hasJsonType) {
context.report({
node,
messageId: 'missingJsonTypeAttribute',
fix: fixer =>
fixer.insertTextAfterRange(node.source.range!, ' with { type: "json" }'),
});
}
}
},
};
},
};
export default requireImportAttribute;

View File

@ -1,14 +1,19 @@
// https://github.com/typescript-eslint/typescript-eslint/blob/75c128856b1ce05a4fec799bfa6de03b3dab03d0/packages/eslint-plugin/src/rules/restrict-template-expressions.ts // https://github.com/typescript-eslint/typescript-eslint/blob/75c128856b1ce05a4fec799bfa6de03b3dab03d0/packages/eslint-plugin/src/rules/restrict-template-expressions.ts
import * as ts from 'typescript';
import { ESLintUtils, type TSESTree, AST_NODE_TYPES } from '@typescript-eslint/utils';
import { import {
getConstrainedTypeAtLocation,
getTypeName, getTypeName,
isTypeAnyType, isTypeAnyType,
isTypeFlagSet, isTypeFlagSet,
isTypeNeverType, isTypeNeverType,
getConstrainedTypeAtLocation,
} from '@typescript-eslint/type-utils'; } from '@typescript-eslint/type-utils';
import {
AST_NODE_TYPES,
ESLintUtils,
ParserServicesWithTypeInformation,
type TSESTree,
} from '@typescript-eslint/utils';
import { getParserServices } from '@typescript-eslint/utils/eslint-utils'; import { getParserServices } from '@typescript-eslint/utils/eslint-utils';
import * as ts from 'typescript';
const createRule = ESLintUtils.RuleCreator( const createRule = ESLintUtils.RuleCreator(
name => `https://typescript-eslint.io/rules/${name}`, name => `https://typescript-eslint.io/rules/${name}`,
@ -29,8 +34,6 @@ export default createRule<Option[], MessageId>({
type: 'problem', type: 'problem',
docs: { docs: {
description: 'Enforce template literal expressions to be of `string` type', description: 'Enforce template literal expressions to be of `string` type',
recommended: 'recommended',
requiresTypeChecking: true,
}, },
messages: { messages: {
invalidType: 'Invalid type "{{type}}" of template literal expression.', invalidType: 'Invalid type "{{type}}" of template literal expression.',
@ -54,8 +57,16 @@ export default createRule<Option[], MessageId>({
}, },
defaultOptions: [defaultOption], defaultOptions: [defaultOption],
create(context, [options]) { create(context, [options]) {
const services = getParserServices(context); let services: ParserServicesWithTypeInformation | undefined;
const checker = services.program!.getTypeChecker(); try {
services = getParserServices(context);
} catch (error) {
console.error(error);
}
if (!services?.program) return {};
const checker = services.program.getTypeChecker();
const allowed = new Set(options.allow); const allowed = new Set(options.allow);
const { StringLike, NumberLike, BigIntLike, BooleanLike, Null, Undefined } = const { StringLike, NumberLike, BigIntLike, BooleanLike, Null, Undefined } =

118
src/environment.ts Normal file
View File

@ -0,0 +1,118 @@
import fs from 'node:fs';
import path from 'node:path';
import type { Environment, NormalizedExtendConfigOptions } from './config';
import type { Middleware } from './middleware';
import { reactQuery, storybook, vitest } from './presets/misc';
import { react, reactRefresh } from './presets/react';
type Pkg = typeof import('../package.json');
type Dependency = keyof Pkg['dependencies'] | RemoveType<keyof Pkg['devDependencies']>;
type RemoveType<T extends string> = T extends `@types/${infer U}__${infer V}`
? `@${U}/${V}`
: T extends `@types/${infer U}`
? U
: T;
export const middlewares = {
react,
reactRefresh,
tailwind: () => import('./presets/tailwind'),
storybook,
reactQuery,
testingLibrary: () => import('./presets/testing-library'),
jsdoc: () => import('./presets/jsdoc'),
vitest,
lingui: () => import('./presets/lingui'),
} satisfies {
[key in Environment]: Middleware;
};
export const envs: {
dependency: string[];
eslintPlugin?: Dependency;
middleware: keyof typeof middlewares;
}[] = [
{
dependency: ['react'],
middleware: 'react',
},
{
dependency: ['@vitejs/plugin-react', '@vitejs/plugin-react-swc'],
eslintPlugin: 'eslint-plugin-react-refresh',
middleware: 'reactRefresh',
},
{
dependency: ['tailwindcss'],
eslintPlugin: 'eslint-plugin-tailwindcss',
middleware: 'tailwind',
},
{
dependency: ['storybook'],
eslintPlugin: 'eslint-plugin-storybook',
middleware: 'storybook',
},
{
dependency: ['@lingui/core', '@lingui/react'],
eslintPlugin: 'eslint-plugin-lingui',
middleware: 'lingui',
},
{
dependency: ['@tanstack/react-query'],
eslintPlugin: '@tanstack/eslint-plugin-query',
middleware: 'reactQuery',
},
{
dependency: ['@testing-library/react'],
eslintPlugin: 'eslint-plugin-testing-library',
middleware: 'testingLibrary',
},
{
dependency: ['vitest'],
eslintPlugin: '@vitest/eslint-plugin',
middleware: 'vitest',
},
];
export function getProjectDependencies() {
const rootDir = process.cwd();
const pkgJsonPath = path.resolve(rootDir, 'package.json');
const pkgJson = fs.existsSync(pkgJsonPath)
? (JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')) as {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
})
: {};
return new Set(
Object.keys({
...pkgJson.dependencies,
...pkgJson.devDependencies,
...pkgJson.peerDependencies,
}),
);
}
export function* checkEnv(): Generator<Middleware> {
const deps = getProjectDependencies();
for (const { dependency, eslintPlugin, middleware } of envs) {
if (
dependency.some(dep => deps.has(dep)) &&
(!eslintPlugin || deps.has(eslintPlugin))
) {
yield middlewares[middleware];
}
}
}
export function* fromEnvironments(envs: NormalizedExtendConfigOptions['env']) {
for (const [env, enabled] of Object.entries(envs ?? {}) as [Environment, boolean][]) {
if (enabled) {
yield middlewares[env];
}
}
}

View File

@ -1,210 +1,168 @@
/// <reference path="./types.d.ts" /> /* eslint-disable import-x/no-named-as-default-member */
import './redirect';
import fs from 'node:fs'; import fs from 'node:fs';
import type { Rule } from 'eslint';
import type { ESLintUtils } from '@typescript-eslint/utils'; import type { FlatESLintConfig } from '@aet/eslint-define-config';
import type { ESLintConfig, Rules } from 'eslint-define-config'; import * as tsParser from '@typescript-eslint/parser';
// import findCacheDirectory from 'find-cache-dir'; import prettier from 'eslint-config-prettier';
import { typescriptRules } from './presets/typescript'; import importPlugin from 'eslint-plugin-import-x';
import { unicornRules } from './presets/unicorn'; import * as regexpPlugin from 'eslint-plugin-regexp';
import { uniq } from 'lodash-es';
import tseslint from 'typescript-eslint';
import type { ExtendConfigOptions, NormalizedExtendConfigOptions } from './config';
import { off } from './constants';
import { checkEnv, fromEnvironments } from './environment';
import type { Middleware } from './middleware';
import { eslintRules } from './presets/eslint'; import { eslintRules } from './presets/eslint';
import { reactRules } from './presets/react'; import stylistic from './presets/stylistic';
import { importRules } from './presets/import'; import { importRules, typescriptRules } from './presets/typescript';
import { jsDocRules } from './presets/jsdoc'; import unicorn from './presets/unicorn';
import { graphqlRules } from './presets/graphql';
import { localRules } from './presets/local';
import { error, warn, off } from './constants';
export { error, warn, off }; export { error, warn, off } from './constants';
declare global { function normalizeExtendConfig(
interface Array<T> { options: ExtendConfigOptions,
filter( ): NormalizedExtendConfigOptions {
predicate: BooleanConstructor, if (Array.isArray(options)) {
): Exclude<T, null | undefined | false | '' | 0>[]; options = { configs: options };
} } else if ('rules' in options) {
} options = { configs: [options] };
const unique = (...arr: (false | undefined | string | string[])[]): string[] => [
...new Set(arr.flat(1).filter(Boolean)),
];
const ensureArray = <T>(value?: T | T[]): T[] =>
value == null ? [] : Array.isArray(value) ? value : [value];
type RuleLevel = 'error' | 'warn' | 'off' | 0 | 1 | 2;
type RuleEntry<Options> = RuleLevel | [RuleLevel, Partial<Options>];
declare module 'eslint-define-config/src/rules/react/no-unknown-property.d.ts' {
export interface NoUnknownPropertyOption {
extends: ('next' | 'emotion')[];
}
}
export interface LocalRuleOptions {
/** Bans import from the specifier '.' and '..' and replaces it with '.+/index' */
'rules/no-import-dot': RuleEntry<unknown>;
/**
* Enforce template literal expressions to be of `string` type
* @see [restrict-template-expressions](https://typescript-eslint.io/rules/restrict-template-expressions)
*/
'rules/restrict-template-expressions': RuleEntry<{ allow: string[] }>;
/** Ban assignment of empty object literals `{}` and replace them with `Object.create(null)` */
'rules/no-empty-object-literal': RuleEntry<unknown>;
}
export type RuleOptions = Rules & Partial<LocalRuleOptions>;
/**
* ESLint Configuration.
* @see [ESLint Configuration](https://eslint.org/docs/latest/user-guide/configuring/)
*/
type Config = Omit<ESLintConfig, 'rules'> & {
/**
* Rules.
* @see [Rules](https://eslint.org/docs/latest/user-guide/configuring/rules)
*/
rules?: RuleOptions;
/**
*/
customRules?: {
rule: () => Promise<{
default: Rule.RuleModule | ESLintUtils.RuleModule<string, unknown[]>;
}>;
options?: RuleLevel;
}[];
};
export function defineCustomRule<Options extends readonly unknown[]>(
rule: () => Promise<{
default: Rule.RuleModule | ESLintUtils.RuleModule<string, Options>;
}>,
options?: Options,
) {
return { rule, options };
}
/**
* Returns a ESLint config object.
*
* By default, it includes `["@typescript-eslint", "import", "prettier"]` configs.
* Additional bundled plugins include:
*
* 1. [react](https://github.com/jsx-eslint/eslint-plugin-react#list-of-supported-rules)
* 2. [react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh)
* 3. [jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y#supported-rules)
* 4. [unicorn](https://github.com/sindresorhus/eslint-plugin-unicorn#rules)
* 5. [n](https://github.com/eslint-community/eslint-plugin-n#-rules) (Node.js specific)
* 6. [jsdoc](https://github.com/gajus/eslint-plugin-jsdoc#rules)
*
* Non bundled:
* 1. [graphql](https://the-guild.dev/graphql/eslint/rules)
*/
export function extendConfig({
plugins,
settings,
rules,
extends: _extends,
overrides,
customRules,
...rest
}: Config = {}): ESLintConfig {
const hasReact = plugins?.includes('react');
const hasReactRefresh = plugins?.includes('react-refresh');
const hasUnicorn = plugins?.includes('unicorn');
const hasJsDoc = plugins?.includes('jsdoc');
const hasGraphQL = plugins?.includes('@graphql-eslint');
const hasNext = ensureArray(_extends).some(name => name.includes(':@next/next'));
const ruleDir = false; // ?? findCacheDirectory({ name: '_eslint-rules' });
if (ruleDir) {
fs.rmSync(ruleDir, { recursive: true, force: true });
fs.mkdirSync(ruleDir, { recursive: true });
} }
const result: Config = { const {
root: true, auto = true,
parser: '@typescript-eslint/parser', middlewares = [],
plugins: unique('@typescript-eslint', 'import', 'rules', plugins), configs = [],
env: { node: true, browser: true, es2023: true }, gitignore = true,
reportUnusedDisableDirectives: true, env,
parserOptions: { } = options as NormalizedExtendConfigOptions;
project: true,
return {
auto,
middlewares,
configs,
gitignore,
env,
};
}
export async function extendConfig(
options: ExtendConfigOptions = [],
): Promise<FlatESLintConfig[]> {
const {
auto,
middlewares: addMiddlewares = [],
configs,
gitignore,
env,
} = normalizeExtendConfig(options);
const middlewares: Middleware[] = uniq([
() => import('./presets/custom'),
...(auto ? checkEnv() : []),
...fromEnvironments(env),
...addMiddlewares,
]);
const result: FlatESLintConfig[] = [
{
name: 'eslint-rules/eslint',
rules: eslintRules,
},
...tseslint.configs.recommendedTypeChecked,
importPlugin.flatConfigs.recommended,
importPlugin.flatConfigs.react,
importPlugin.flatConfigs.typescript,
...unicorn,
stylistic,
{
plugins: { regexp: regexpPlugin },
rules: {
...regexpPlugin.configs['flat/recommended'].rules,
// https://github.com/ota-meshi/eslint-plugin-regexp/issues/445
'regexp/strict': off,
'regexp/match-any': off,
// https://github.com/ota-meshi/eslint-plugin-regexp/issues/743
'regexp/letter-case': off,
},
},
{
name: 'eslint-rules/typescript-and-import-x',
files: ['**/*.{js,mjs,cjs,jsx,ts,tsx,mts,cts}'],
languageOptions: {
parserOptions: {
parser: tsParser,
projectService: true,
ecmaVersion: 'latest',
tsconfigRootDir: import.meta.dirname,
sourceType: 'module',
},
}, },
extends: unique(
'eslint:recommended',
'prettier',
'plugin:@typescript-eslint/recommended-type-checked',
'plugin:import/errors',
'plugin:import/typescript',
hasReact && [
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended',
],
hasJsDoc && 'plugin:jsdoc/recommended-typescript',
hasGraphQL && 'plugin:@graphql-eslint/recommended',
_extends,
),
settings: { settings: {
'import/parsers': { 'import-x/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx', '.mts', '.cts'], '@typescript-eslint/parser': ['.ts', '.tsx', '.mts', '.cts'],
}, },
'import/resolver': { 'import-x/resolver': {
typescript: { typescript: true,
alwaysTryTypes: true, node: true,
}, },
'import-x/core-modules': ['node:sqlite'],
}, },
react: { ignores: ['eslint.config.cjs'],
version: 'detect',
},
...settings,
},
overrides: [
{
files: [
'.eslintrc.js',
'*.config.js',
'index.js',
'babel.config.js',
'next.config.js',
],
extends: ['plugin:@typescript-eslint/disable-type-checked'],
rules: { rules: {
'rules/restrict-template-expressions': off, ...importRules,
...typescriptRules,
}, },
}, },
{ {
files: ['*.d.ts'], name: 'eslint-rules: Disable type checking',
files: ['**/*.js', '**/*.mjs', '**/*.cjs', '**/*.jsx'],
...tseslint.configs.disableTypeChecked,
rules: {
'import-x/no-commonjs': off,
'import-x/unambiguous': off,
'@typescript-eslint/no-require-imports': off,
'typed-custom/restrict-template-expressions': off,
...tseslint.configs.disableTypeChecked.rules,
},
},
{
name: 'eslint-rules/.d.ts-files',
files: ['**/*.d.ts'],
rules: { rules: {
'@typescript-eslint/consistent-type-imports': off, '@typescript-eslint/consistent-type-imports': off,
'import-x/unambiguous': off,
}, },
}, },
...(overrides ?? []), ] as FlatESLintConfig[];
],
rules: { for (const middleware of middlewares) {
...eslintRules, let fn = await middleware();
...typescriptRules, if ('default' in fn) {
...importRules, fn = fn.default;
...localRules, }
...(hasReact && { if (Array.isArray(fn)) {
...reactRules, result.push(...(fn as FlatESLintConfig[]).flat(Infinity));
'react/no-unknown-property': [ } else {
error, result.push(fn as unknown as FlatESLintConfig);
{ ignore: hasNext ? ['emotion', 'next'] : ['emotion'] }, }
], }
}),
...(hasReactRefresh && { if (configs?.length) {
'react-refresh/only-export-components': [warn, { allowConstantExport: true }], result.push(...configs);
}), }
...(hasUnicorn && unicornRules),
...(hasJsDoc && jsDocRules), result.push(prettier);
...(hasGraphQL && graphqlRules),
...rules, if (gitignore && fs.existsSync('.gitignore')) {
}, const ignores = fs
...rest, .readFileSync('.gitignore', 'utf8')
}; .trim()
.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#'));
result.push({ ignores });
}
return result; return result;
} }

22
src/install.ts Normal file
View File

@ -0,0 +1,22 @@
import { installPackage } from '@antfu/install-pkg';
import { uniq } from 'lodash-es';
import { envs, getProjectDependencies } from './environment';
const deps = getProjectDependencies();
const packages = uniq(
envs
.filter(
_ =>
_.dependency.some(dep => deps.has(dep)) &&
_.eslintPlugin &&
!deps.has(_.eslintPlugin),
)
.map(_ => _.eslintPlugin!),
);
console.log('Installing missing ESLint plugins.\n');
void installPackage(packages, {
silent: false,
});

View File

@ -1,38 +0,0 @@
import type { ESLint } from 'eslint';
import * as fs from 'node:fs';
import { resolve, basename, extname } from 'node:path';
function tryRequire(candidates: string[]) {
for (const candidate of candidates) {
try {
require(candidate);
return;
} catch {}
}
}
// https://github.com/gulpjs/interpret
tryRequire([
'esbin/register',
'esbuild-register',
'ts-node/register/transpile-only',
'@swc/register',
'sucrase/register',
'@babel/register',
'coffeescript/register',
]);
const folders = resolve(process.cwd(), 'eslint-local-rules');
const files = fs.readdirSync(folders);
const plugin: ESLint.Plugin = {
rules: {},
};
for (const file of files) {
const name = basename(file, extname(file));
const module = require(resolve(folders, file));
plugin.rules![name] = module.default ?? module;
}
export = plugin;

9
src/middleware.ts Normal file
View File

@ -0,0 +1,9 @@
import type { Linter } from 'eslint';
type MiddlewareResult = Linter.Config | Linter.Config[];
export type Middleware =
| (() => Promise<MiddlewareResult>)
| (() => Promise<{ default: MiddlewareResult }>);
export const def = <T>(module: { default: T }): T => module.default;

30
src/modules.d.ts vendored Normal file
View File

@ -0,0 +1,30 @@
// eslint-disable-next-line import-x/unambiguous
declare module 'module' {
export function _resolveFilename(
request: string,
parent: {
/**
* Can be null if the parent id is 'internal/preload' (e.g. via --require)
* which doesn't have a file path.
*/
filename: string | null;
},
isMain: boolean,
options?: Record<PropertyKey, unknown>,
): string;
}
declare module 'eslint-plugin-storybook' {
import type { Linter } from 'eslint';
export const configs: {
/** @deprecated */
csf: Linter.Config;
/** @deprecated */
recommended: Linter.Config;
'flat/csf': Linter.Config;
'flat/recommended': Linter.Config;
'flat/csf-strict': Linter.Config;
'flat/addon-interactions': Linter.Config;
};
}

View File

@ -0,0 +1,164 @@
[
"addEventListener",
"blur",
"caches",
"captureEvents",
"clientInformation",
"close",
"closed",
"crossOriginIsolated",
"devicePixelRatio",
"dispatchEvent",
"event",
"external",
"focus",
"innerHeight",
"innerWidth",
"length",
"locationbar",
"menubar",
"name",
"onabort",
"onafterprint",
"onanimationcancel",
"onanimationend",
"onanimationiteration",
"onanimationstart",
"onauxclick",
"onbeforeinput",
"onbeforeprint",
"onbeforetoggle",
"onbeforeunload",
"onblur",
"oncancel",
"oncanplay",
"oncanplaythrough",
"onchange",
"onclick",
"onclose",
"oncontextmenu",
"oncopy",
"oncuechange",
"oncut",
"ondblclick",
"ondevicemotion",
"ondeviceorientation",
"ondeviceorientationabsolute",
"ondrag",
"ondragend",
"ondragenter",
"ondragleave",
"ondragover",
"ondragstart",
"ondrop",
"ondurationchange",
"onemptied",
"onended",
"onerror",
"onfocus",
"onformdata",
"ongamepadconnected",
"ongamepaddisconnected",
"ongotpointercapture",
"onhashchange",
"oninput",
"oninvalid",
"onkeydown",
"onkeypress",
"onkeyup",
"onlanguagechange",
"onload",
"onloadeddata",
"onloadedmetadata",
"onloadstart",
"onlostpointercapture",
"onmessage",
"onmessageerror",
"onmousedown",
"onmouseenter",
"onmouseleave",
"onmousemove",
"onmouseout",
"onmouseover",
"onmouseup",
"onoffline",
"ononline",
"onorientationchange",
"onpagehide",
"onpageshow",
"onpaste",
"onpause",
"onplay",
"onplaying",
"onpointercancel",
"onpointerdown",
"onpointerenter",
"onpointerleave",
"onpointermove",
"onpointerout",
"onpointerover",
"onpointerup",
"onpopstate",
"onprogress",
"onratechange",
"onrejectionhandled",
"onreset",
"onresize",
"onscroll",
"onscrollend",
"onsecuritypolicyviolation",
"onseeked",
"onseeking",
"onselect",
"onselectionchange",
"onselectstart",
"onslotchange",
"onstalled",
"onstorage",
"onsubmit",
"onsuspend",
"ontimeupdate",
"ontoggle",
"ontouchcancel",
"ontouchend",
"ontouchmove",
"ontouchstart",
"ontransitioncancel",
"ontransitionend",
"ontransitionrun",
"ontransitionstart",
"onunhandledrejection",
"onunload",
"onvolumechange",
"onwaiting",
"onwebkitanimationend",
"onwebkitanimationiteration",
"onwebkitanimationstart",
"onwebkittransitionend",
"onwheel",
"orientation",
"origin",
"outerHeight",
"outerWidth",
"pageXOffset",
"pageYOffset",
"personalbar",
"releaseEvents",
"removeEventListener",
"reportError",
"screenLeft",
"screenTop",
"screenX",
"screenY",
"scroll",
"scrollbars",
"scrollBy",
"scrollTo",
"scrollX",
"scrollY",
"status",
"statusbar",
"stop",
"toolbar",
"top"
]

24
src/presets/custom.ts Normal file
View File

@ -0,0 +1,24 @@
import { error } from '../constants';
import { plugin, typedPlugin, LocalRuleOptions } from '../custom/index';
import { defineConfig } from '../types';
export default defineConfig([
{
name: 'eslint-rules/aet',
plugins: { aet: plugin },
rules: {
'aet/no-import-dot': error,
'aet/no-useless-import-alias': error,
'aet/require-import-attribute': error,
} satisfies Partial<LocalRuleOptions>,
},
{
name: 'eslint-rules/typed-aet',
plugins: { 'typed-aet': typedPlugin },
files: ['**/*.ts'],
ignores: ['**/*.d.ts'],
rules: {
'typed-aet/restrict-template-expressions': error,
} satisfies Partial<LocalRuleOptions>,
},
]);

View File

@ -1,9 +1,13 @@
import { error, warn, off } from '../constants'; import type { EslintRulesObject } from '@aet/eslint-define-config/src/rules/eslint';
import { EslintRules } from 'eslint-define-config/src/rules/eslint';
export const eslintRules: Partial<EslintRules> = { import { error, off, warn } from '../constants';
import restrictedGlobals from './_restrictedGlobals.json';
export const eslintRules: Partial<EslintRulesObject> = {
'arrow-body-style': [error, 'as-needed'], 'arrow-body-style': [error, 'as-needed'],
'class-methods-use-this': off, 'class-methods-use-this': warn,
'func-style': [error, 'declaration', { allowArrowFunctions: true }],
'no-async-promise-executor': off, 'no-async-promise-executor': off,
'no-case-declarations': off, 'no-case-declarations': off,
'no-console': warn, 'no-console': warn,
@ -13,7 +17,7 @@ export const eslintRules: Partial<EslintRules> = {
'no-empty': [error, { allowEmptyCatch: true }], 'no-empty': [error, { allowEmptyCatch: true }],
'no-inner-declarations': off, 'no-inner-declarations': off,
'no-lonely-if': error, 'no-lonely-if': error,
'no-restricted-globals': [error, 'event', 'name', 'length'], 'no-restricted-globals': [error, ...restrictedGlobals],
'no-restricted-imports': [ 'no-restricted-imports': [
error, error,
{ {
@ -29,7 +33,7 @@ export const eslintRules: Partial<EslintRules> = {
'no-template-curly-in-string': error, 'no-template-curly-in-string': error,
'no-var': error, 'no-var': error,
'object-shorthand': [error, 'always', { ignoreConstructors: true }], 'object-shorthand': [error, 'always', { ignoreConstructors: true }],
'one-var': [error, { var: 'never', let: 'never' }], 'one-var': [error, { var: 'never', let: 'never', const: 'never' }],
'prefer-arrow-callback': error, 'prefer-arrow-callback': error,
'prefer-const': [error, { destructuring: 'all' }], 'prefer-const': [error, { destructuring: 'all' }],
'prefer-destructuring': [ 'prefer-destructuring': [
@ -41,7 +45,6 @@ export const eslintRules: Partial<EslintRules> = {
'prefer-spread': warn, 'prefer-spread': warn,
'quote-props': [error, 'as-needed'], 'quote-props': [error, 'as-needed'],
'sort-imports': [warn, { ignoreDeclarationSort: true }], 'sort-imports': [warn, { ignoreDeclarationSort: true }],
'spaced-comment': [error, 'always', { markers: ['/', '#', '@'] }],
complexity: [warn, { max: 100 }], complexity: [warn, { max: 100 }],
curly: [error, 'multi-line', 'consistent'], curly: [error, 'multi-line', 'consistent'],
eqeqeq: [error, 'smart'], eqeqeq: [error, 'smart'],

View File

@ -1,4 +1,13 @@
import { GraphQLRules } from 'eslint-define-config/src/rules/graphql-eslint'; // Not usable. https://github.com/dimaMachina/graphql-eslint/issues/2178
import type { GraphQLRulesObject } from '@aet/eslint-define-config/src/rules/graphql-eslint';
import * as graphql from '@graphql-eslint/eslint-plugin';
import { defineConfig } from '../types';
// https://the-guild.dev/graphql/eslint/rules // https://the-guild.dev/graphql/eslint/rules
export const graphqlRules: Partial<GraphQLRules> = {}; const graphqlRules: Partial<GraphQLRulesObject> = {};
export default defineConfig({
processor: graphql.processors.graphql,
rules: graphqlRules,
});

View File

@ -1,8 +0,0 @@
import { error, off } from '../constants';
import { ImportRules } from 'eslint-define-config/src/rules/import';
export const importRules: Partial<ImportRules> = {
'import/export': off,
'import/no-duplicates': error,
'import/order': [error, { groups: ['builtin', 'external'] }],
};

View File

@ -1,3 +1,14 @@
import { JSDocRules } from 'eslint-define-config/src/rules/jsdoc'; import type { JSDocRulesObject } from '@aet/eslint-define-config/src/rules/jsdoc';
import module from 'eslint-plugin-jsdoc';
export const jsDocRules: Partial<JSDocRules> = {}; import { off } from '../constants';
import { defineConfig } from '../types';
const jsdocRules: Partial<JSDocRulesObject> = {
'jsdoc/require-jsdoc': off,
};
export default defineConfig([
module.configs['flat/recommended-typescript'],
{ name: 'eslint-rules/jsdoc', rules: jsdocRules },
]);

12
src/presets/lingui.ts Normal file
View File

@ -0,0 +1,12 @@
import type { LinguiRulesObject } from '@aet/eslint-define-config/src/rules/lingui';
import * as pluginLingui from 'eslint-plugin-lingui';
import { defineConfig } from '../types';
// https://the-guild.dev/graphql/eslint/rules
const linguiRules: Partial<LinguiRulesObject> = {};
export default defineConfig([
pluginLingui.configs['flat/recommended'],
{ rules: linguiRules },
]);

View File

@ -1,7 +0,0 @@
import type { LocalRuleOptions } from '../index';
import { error } from '../constants';
export const localRules: Partial<LocalRuleOptions> = {
'rules/no-import-dot': error,
'rules/restrict-template-expressions': error,
};

30
src/presets/misc.ts Normal file
View File

@ -0,0 +1,30 @@
import { def } from '../middleware';
import { defineConfig } from '../types';
export async function storybook() {
const { configs } = def(await import('eslint-plugin-storybook'));
return defineConfig([configs['flat/recommended']]);
}
export async function reactQuery() {
const { configs } = def(await import('@tanstack/eslint-plugin-query'));
return defineConfig(configs['flat/recommended']);
}
export async function vitest() {
const { configs } = def(await import('@vitest/eslint-plugin'));
return defineConfig([
configs.recommended,
{
rules: {
'vitest/expect-expect': [
'error',
{
assertFunctionNames: ['expect*', 'request.**.expect'],
additionalTestBlockFunctions: ['describe*'],
},
],
},
},
]);
}

View File

@ -1,9 +1,55 @@
import { error, off } from '../constants'; import type { ReactRulesObject } from '@aet/eslint-define-config/src/rules/react';
import { ReactRules } from 'eslint-define-config/src/rules/react'; import type { ReactRefreshRulesObject } from '@aet/eslint-define-config/src/rules/react-refresh';
import type { Linter, ESLint } from 'eslint';
export const reactRules: Partial<ReactRules> = { import { error, off, warn } from '../constants';
'react/display-name': off, import { def } from '../middleware';
'react/no-children-prop': error, import { defineConfig } from '../types';
'react/prop-types': off,
'react/react-in-jsx-scope': off, const reactRules: Partial<ReactRulesObject> = {
'@eslint-react/no-missing-component-display-name': off,
'@eslint-react/no-children-prop': error,
'@eslint-react/no-leaked-conditional-rendering': error,
'@eslint-react/prefer-read-only-props': off,
'@eslint-react/hooks-extra/no-direct-set-state-in-use-effect': off,
}; };
export async function react() {
const reactPlugin = def(await import('@eslint-react/eslint-plugin'));
const a11y = def(await import('../../packages/eslint-plugin-jsx-a11y/src/index'));
const hooks = await import('../../packages/eslint-plugin-react-hooks');
return defineConfig([
reactPlugin.configs['recommended-type-checked'] as unknown as Linter.Config,
hooks.flatConfigs.recommended,
a11y.flatConfigs.recommended,
{
name: 'eslint-rules/react',
files: ['**/*.tsx'],
rules: reactRules,
},
{
name: 'eslint-rules/react/test-files',
files: ['**/*.test.tsx'],
rules: {
'@eslint-react/no-clone-element': off,
'@eslint-react/no-create-ref': off,
},
},
]);
}
const refreshRules: Partial<ReactRefreshRulesObject> = {
'react-refresh/only-export-components': [warn, { allowConstantExport: true }],
};
export async function reactRefresh() {
const refreshPlugin = def(await import('eslint-plugin-react-refresh'));
return defineConfig({
name: 'eslint-rules/react-refresh',
plugins: {
'react-refresh': refreshPlugin as unknown as ESLint.Plugin,
},
rules: refreshRules,
});
}

25
src/presets/stylistic.ts Normal file
View File

@ -0,0 +1,25 @@
import type { StylisticRulesObject } from '@aet/eslint-define-config/src/rules/stylistic';
import stylistic from '@stylistic/eslint-plugin';
import { error } from '../constants';
import { defineConfig } from '../types';
const stylisticRules: Partial<StylisticRulesObject> = {
'stylistic/spaced-comment': [
error,
'always',
{ block: { exceptions: ['@__PURE__', '#__PURE__'] } },
],
'stylistic/jsx-sort-props': [
error,
{ callbacksLast: true, shorthandFirst: true, multiline: 'last' },
],
};
export default defineConfig({
name: 'eslint-rules/stylistic',
plugins: {
stylistic,
},
rules: stylisticRules,
});

23
src/presets/tailwind.ts Normal file
View File

@ -0,0 +1,23 @@
import type { TailwindRulesObject } from '@aet/eslint-define-config/src/rules/tailwind';
import tailwind from 'eslint-plugin-tailwindcss';
import { off } from '../constants';
import { defineConfig } from '../types';
const tailwindRules: Partial<TailwindRulesObject> = {
'tailwindcss/no-custom-classname': off,
} as const;
export default defineConfig([
...tailwind.configs['flat/recommended'],
{
name: 'eslint-rules/tailwind',
rules: tailwindRules,
settings: {
tailwindcss: {
callees: ['classnames', 'clsx', 'tw', 'twx'],
classRegex: /^(css|class(Name)?)$/.source,
},
},
},
]);

View File

@ -0,0 +1,16 @@
import type { TestingLibraryRulesObject } from '@aet/eslint-define-config/src/rules/testing-library';
import testingLibrary from 'eslint-plugin-testing-library';
import { defineConfig } from '../types';
const testingLibraryRules: Partial<TestingLibraryRulesObject> = {};
export default defineConfig({
name: 'eslint-rules/testing-library',
files: ['**/*.(spec|test).{ts,tsx}'],
...testingLibrary.configs['flat/react'],
rules: {
...testingLibrary.configs['flat/react'].rules,
...testingLibraryRules,
},
});

View File

@ -1,7 +1,25 @@
import { error, off, warn } from '../constants'; import type { ImportXRulesObject } from '@aet/eslint-define-config/src/rules/import-x';
import type { TypeScriptRules } from 'eslint-define-config/src/rules/typescript-eslint'; import type { TypeScriptRulesObject } from '@aet/eslint-define-config/src/rules/typescript-eslint';
export const typescriptRules: Partial<TypeScriptRules> = { import { error, off, warn } from '../constants';
export const importRules: Partial<ImportXRulesObject> = {
'import-x/first': error,
'import-x/no-absolute-path': error,
'import-x/no-duplicates': warn,
'import-x/no-useless-path-segments': error,
'import-x/order': [
warn,
{
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object'],
'newlines-between': 'always',
alphabetize: { order: 'asc', caseInsensitive: true },
},
],
'import-x/unambiguous': error,
};
export const typescriptRules: Partial<TypeScriptRulesObject> = {
'@typescript-eslint/ban-ts-comment': [ '@typescript-eslint/ban-ts-comment': [
error, error,
{ {
@ -11,7 +29,6 @@ export const typescriptRules: Partial<TypeScriptRules> = {
'ts-nocheck': 'allow-with-description', 'ts-nocheck': 'allow-with-description',
}, },
], ],
'@typescript-eslint/ban-types': [error, { extendDefaults: true }],
'@typescript-eslint/consistent-type-imports': [ '@typescript-eslint/consistent-type-imports': [
error, error,
{ disallowTypeAnnotations: false, fixStyle: 'inline-type-imports' }, { disallowTypeAnnotations: false, fixStyle: 'inline-type-imports' },
@ -20,8 +37,17 @@ export const typescriptRules: Partial<TypeScriptRules> = {
warn, warn,
{ accessibility: 'no-public' }, { accessibility: 'no-public' },
], ],
'@typescript-eslint/no-empty-object-type': off,
'@typescript-eslint/no-empty-interface': [error, { allowSingleExtends: true }], '@typescript-eslint/no-empty-interface': [error, { allowSingleExtends: true }],
'@typescript-eslint/no-explicit-any': off, '@typescript-eslint/no-explicit-any': off,
'@typescript-eslint/no-floating-promises': [
'warn',
{
allowForKnownSafeCalls: [
{ from: 'package', name: ['it', 'describe', 'test'], package: 'node:test' },
],
},
],
'@typescript-eslint/no-misused-promises': [error, { checksVoidReturn: false }], '@typescript-eslint/no-misused-promises': [error, { checksVoidReturn: false }],
'@typescript-eslint/no-namespace': off, '@typescript-eslint/no-namespace': off,
'@typescript-eslint/no-unnecessary-type-assertion': error, '@typescript-eslint/no-unnecessary-type-assertion': error,

View File

@ -1,31 +1,49 @@
import { error, warn } from '../constants'; import type { UnicornRulesObject } from '@aet/eslint-define-config/src/rules/unicorn';
import { UnicornRules } from 'eslint-define-config/src/rules/unicorn'; import unicorn from 'eslint-plugin-unicorn';
import globals from 'globals';
import { error, off, warn } from '../constants';
import { defineConfig } from '../types';
const suggest = (suggest: string) => ({ suggest, fix: false }); const suggest = (suggest: string) => ({ suggest, fix: false });
// https://github.com/sindresorhus/eslint-plugin-unicorn/tree/28e7498ad06679bb92343db53bb40a7b5ba2990a // https://github.com/sindresorhus/eslint-plugin-unicorn/tree/1774135a5ddbded2c89f82952e37a3e3bb01cdfa
export const unicornRules: Partial<UnicornRules> = { const unicornRules: Partial<UnicornRulesObject> = {
'unicorn/better-regex': error, 'unicorn/better-regex': error,
'unicorn/consistent-destructuring': warn,
'unicorn/consistent-empty-array-spread': error,
'unicorn/consistent-existence-index-check': error,
'unicorn/consistent-function-scoping': warn, 'unicorn/consistent-function-scoping': warn,
'unicorn/escape-case': error, 'unicorn/escape-case': error,
'unicorn/no-array-for-each': warn, 'unicorn/no-array-for-each': warn,
'unicorn/no-array-method-this-argument': error, 'unicorn/no-array-method-this-argument': error,
'unicorn/no-array-push-push': warn, 'unicorn/no-array-push-push': warn,
'unicorn/no-await-in-promise-methods': error,
'unicorn/no-console-spaces': warn, 'unicorn/no-console-spaces': warn,
'unicorn/no-for-loop': warn, 'unicorn/no-for-loop': warn,
'unicorn/no-instanceof-array': error, 'unicorn/no-instanceof-array': error,
'unicorn/no-invalid-fetch-options': error,
'unicorn/no-invalid-remove-event-listener': error,
'unicorn/no-length-as-slice-end': error,
'unicorn/no-lonely-if': warn, 'unicorn/no-lonely-if': warn,
'unicorn/no-negation-in-equality-check': error,
'unicorn/no-new-buffer': error,
'unicorn/no-single-promise-in-promise-methods': error,
'unicorn/no-static-only-class': error, 'unicorn/no-static-only-class': error,
'unicorn/no-typeof-undefined': error, 'unicorn/no-typeof-undefined': error,
// 'unicorn/no-unused-properties': warn, 'unicorn/no-unnecessary-await': error,
'unicorn/no-unnecessary-polyfills': error,
'unicorn/no-unreadable-array-destructuring': warn,
'unicorn/no-useless-fallback-in-spread': error, 'unicorn/no-useless-fallback-in-spread': error,
'unicorn/no-useless-promise-resolve-reject': error, 'unicorn/no-useless-promise-resolve-reject': error,
'unicorn/no-useless-spread': error, 'unicorn/no-useless-spread': error,
'unicorn/no-useless-switch-case': error, 'unicorn/no-useless-switch-case': error,
'unicorn/no-useless-undefined': error,
// https://github.com/prettier/eslint-config-prettier/issues/51 'unicorn/no-zero-fractions': error,
// 'unicorn/number-literal-case': error, 'unicorn/number-literal-case': error,
'unicorn/prefer-array-index-of': error,
'unicorn/prefer-array-find': error, 'unicorn/prefer-array-find': error,
'unicorn/prefer-array-flat': error,
'unicorn/prefer-array-flat-map': error, 'unicorn/prefer-array-flat-map': error,
'unicorn/prefer-array-some': error, 'unicorn/prefer-array-some': error,
'unicorn/prefer-at': error, 'unicorn/prefer-at': error,
@ -34,23 +52,35 @@ export const unicornRules: Partial<UnicornRules> = {
'unicorn/prefer-default-parameters': warn, 'unicorn/prefer-default-parameters': warn,
'unicorn/prefer-dom-node-dataset': error, 'unicorn/prefer-dom-node-dataset': error,
'unicorn/prefer-dom-node-remove': error, 'unicorn/prefer-dom-node-remove': error,
'unicorn/prefer-dom-node-text-content': warn,
'unicorn/prefer-export-from': [error, { ignoreUsedVariables: false }], 'unicorn/prefer-export-from': [error, { ignoreUsedVariables: false }],
'unicorn/prefer-includes': error, 'unicorn/prefer-includes': error,
'unicorn/prefer-json-parse-buffer': warn,
'unicorn/prefer-keyboard-event-key': warn, 'unicorn/prefer-keyboard-event-key': warn,
'unicorn/prefer-logical-operator-over-ternary': warn, 'unicorn/prefer-logical-operator-over-ternary': warn,
'unicorn/prefer-math-trunc': error, 'unicorn/prefer-math-min-max': error,
'unicorn/prefer-math-trunc': warn,
'unicorn/prefer-modern-dom-apis': error,
'unicorn/prefer-modern-math-apis': error, 'unicorn/prefer-modern-math-apis': error,
'unicorn/prefer-negative-index': error, 'unicorn/prefer-negative-index': error,
'unicorn/prefer-node-protocol': error, 'unicorn/prefer-node-protocol': error,
'unicorn/prefer-object-from-entries': error, 'unicorn/prefer-object-from-entries': error,
'unicorn/prefer-optional-catch-binding': error, 'unicorn/prefer-optional-catch-binding': error,
'unicorn/prefer-prototype-methods': error,
'unicorn/prefer-reflect-apply': error, 'unicorn/prefer-reflect-apply': error,
'unicorn/prefer-regexp-test': error, 'unicorn/prefer-regexp-test': error,
'unicorn/prefer-set-has': warn, 'unicorn/prefer-set-has': warn,
'unicorn/prefer-set-size': error,
'unicorn/prefer-string-raw': warn,
'unicorn/prefer-string-slice': error, 'unicorn/prefer-string-slice': error,
'unicorn/prefer-string-starts-ends-with': warn, 'unicorn/prefer-string-starts-ends-with': warn,
'unicorn/prefer-string-trim-start-end': error, 'unicorn/prefer-string-trim-start-end': error,
'unicorn/prefer-switch': warn,
'unicorn/prefer-ternary': warn, 'unicorn/prefer-ternary': warn,
'unicorn/prefer-type-error': warn,
'unicorn/relative-url-style': warn,
'unicorn/require-number-to-fixed-digits-argument': error,
'unicorn/require-post-message-target-origin': warn,
'unicorn/string-content': [ 'unicorn/string-content': [
warn, warn,
{ {
@ -64,8 +94,35 @@ export const unicornRules: Partial<UnicornRules> = {
'<=>': suggest('⇔'), '<=>': suggest('⇔'),
'\\.\\.\\.': suggest('…'), '\\.\\.\\.': suggest('…'),
"'s ": suggest('s '), "'s ": suggest('s '),
"'d ": suggest('d '),
"'t ": suggest('t '),
"l'": suggest('l'),
"d'": suggest('d'),
"qu'": suggest('qu'),
'\\?!': suggest('⁈'),
'!\\?': suggest('⁉'),
}, },
}, },
], ],
'unicorn/template-indent': warn, 'unicorn/template-indent': warn,
}; };
export default defineConfig([
{
name: 'eslint-rules/unicorn',
languageOptions: {
globals: globals.builtin,
},
plugins: {
unicorn,
},
rules: unicornRules,
},
{
name: 'eslint-rules/unicorn/tests',
files: ['**/*.test.ts', '**/*.test.tsx'],
rules: {
'unicorn/no-useless-undefined': off,
},
},
]);

24
src/prettier.ts Normal file
View File

@ -0,0 +1,24 @@
import type { Config } from 'prettier';
const prettier: Config = {
arrowParens: 'avoid',
tabWidth: 2,
printWidth: 90,
semi: true,
singleQuote: true,
trailingComma: 'all',
plugins: [],
};
export default function defineConfig(
config: Partial<Config> & {
tailwind?: boolean;
},
) {
const result: Config = {
...prettier,
...config,
};
return result;
}

View File

@ -1,21 +0,0 @@
import Module from 'module';
const { name } = [require][0]('./package.json');
const _resolveFilename = (Module as any)._resolveFilename;
const alias = new Set([
'eslint-import-resolver-typescript',
'eslint-plugin-import',
'eslint-plugin-jsx-a11y',
'eslint-plugin-local',
'eslint-plugin-n',
'eslint-plugin-react-hooks',
'eslint-plugin-react',
'eslint-plugin-rules',
]);
(Module as any)._resolveFilename = function (module: string, ...args: any[]) {
if (alias.has(module)) {
module = `${name}/${module}`;
}
return _resolveFilename(module, ...args);
};

View File

@ -1,15 +0,0 @@
import type { Rule } from 'eslint';
import type { ESLintUtils } from '@typescript-eslint/utils';
import noEmptyObjectLiteral from './no-empty-object-literal';
import noImportDot from './no-import-dot';
import restrictTemplateExpressions from './restrict-template-expressions';
export const rules: Record<
string,
Rule.RuleModule | ESLintUtils.RuleModule<string, unknown[]>
> = {
'no-empty-object-literal': noEmptyObjectLiteral,
'no-import-dot': noImportDot,
'restrict-template-expressions': restrictTemplateExpressions,
};

21
src/types.d.ts vendored
View File

@ -1,21 +0,0 @@
declare module '@typescript-eslint/utils' {
export * from '@typescript-eslint/utils/dist/index';
}
declare module '@typescript-eslint/typescript-estree' {
export * from '@typescript-eslint/typescript-estree/dist/index';
}
declare module '@typescript-eslint/type-utils' {
export * from '@typescript-eslint/type-utils/dist/index';
}
declare module '@typescript-eslint/utils/eslint-utils' {
export * from '@typescript-eslint/utils/dist/eslint-utils';
}
declare module '@typescript-eslint/utils/json-schema' {
export * from '@typescript-eslint/utils/dist/json-schema';
}
declare module '@typescript-eslint/scope-manager' {
export * from '@typescript-eslint/scope-manager/dist/index';
}
declare module '@typescript-eslint/types' {
export * from '@typescript-eslint/types/dist/index';
}

37
src/types.ts Normal file
View File

@ -0,0 +1,37 @@
import type { ESLintUtils } from '@typescript-eslint/utils';
import type { Rule, Linter } from 'eslint';
export function defineRules(rules: {
[ruleName: string]: Rule.RuleModule | ESLintUtils.RuleModule<string, unknown[]>;
}) {
return rules;
}
export function defineConfig(config: Linter.Config): Linter.Config;
export function defineConfig(config: Linter.Config[]): Linter.Config[];
export function defineConfig(config: Linter.Config | Linter.Config[]) {
if (!config || (Array.isArray(config) && config.some(c => !c))) {
console.trace();
throw new Error('Config cannot be empty');
}
return config;
}
export function defineRule({
name,
create,
...meta
}: Rule.RuleMetaData & {
name?: string;
create: (context: Rule.RuleContext) => Rule.RuleListener;
}): Rule.RuleModule {
const module: Rule.RuleModule = {
meta,
create,
};
if (name != null) {
Object.defineProperty(module, 'name', { value: name });
}
return module;
}

View File

@ -0,0 +1,18 @@
// eslint-disable-next-line import-x/unambiguous
declare module 'eslint-plugin-react-refresh' {
import type { TSESLint } from '@typescript-eslint/utils';
export const rules: {
'only-export-components': TSESLint.RuleModule<
'exportAll' | 'namedExport' | 'anonymousExport' | 'noExport' | 'localComponents',
| []
| [
{
allowConstantExport?: boolean;
checkJS?: boolean;
allowExportNames?: string[];
},
]
>;
};
}

View File

@ -0,0 +1,55 @@
// eslint-disable-next-line import-x/unambiguous
declare module 'eslint-plugin-testing-library' {
import type { Rule, Linter } from 'eslint';
// 6.3.0
const plugin: {
meta: {
name: 'eslint-plugin-testing-library';
version: '6.3.0';
};
configs: {
dom: Linter.BaseConfig;
angular: Linter.BaseConfig;
react: Linter.BaseConfig;
vue: Linter.BaseConfig;
marko: Linter.BaseConfig;
'flat/dom': Linter.Config;
'flat/angular': Linter.Config;
'flat/react': Linter.Config;
'flat/vue': Linter.Config;
'flat/marko': Linter.Config;
};
rules: {
'await-async-events': Rule.RuleModule;
'await-async-queries': Rule.RuleModule;
'await-async-utils': Rule.RuleModule;
'consistent-data-testid': Rule.RuleModule;
'no-await-sync-events': Rule.RuleModule;
'no-await-sync-queries': Rule.RuleModule;
'no-container': Rule.RuleModule;
'no-debugging-utils': Rule.RuleModule;
'no-dom-import': Rule.RuleModule;
'no-global-regexp-flag-in-query': Rule.RuleModule;
'no-manual-cleanup': Rule.RuleModule;
'no-node-access': Rule.RuleModule;
'no-promise-in-fire-event': Rule.RuleModule;
'no-render-in-lifecycle': Rule.RuleModule;
'no-unnecessary-act': Rule.RuleModule;
'no-wait-for-multiple-assertions': Rule.RuleModule;
'no-wait-for-side-effects': Rule.RuleModule;
'no-wait-for-snapshot': Rule.RuleModule;
'prefer-explicit-assert': Rule.RuleModule;
'prefer-find-by': Rule.RuleModule;
'prefer-implicit-assert': Rule.RuleModule;
'prefer-presence-queries': Rule.RuleModule;
'prefer-query-by-disappearance': Rule.RuleModule;
'prefer-query-matchers': Rule.RuleModule;
'prefer-screen-queries': Rule.RuleModule;
'prefer-user-event': Rule.RuleModule;
'render-result-naming-convention': Rule.RuleModule;
};
};
export = plugin;
}

View File

@ -6,8 +6,8 @@
"esModuleInterop": true, "esModuleInterop": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"module": "commonjs", "module": "ESNext",
"moduleResolution": "node", "moduleResolution": "Bundler",
"noEmit": true, "noEmit": true,
"noImplicitOverride": true, "noImplicitOverride": true,
"noUnusedLocals": true, "noUnusedLocals": true,