Upgrade to ESLint 9
This commit is contained in:
parent
0138cabb27
commit
00d0dfa107
51
.eslint.ts
Normal file
51
.eslint.ts
Normal 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[];
|
195
.eslintrc.js
195
.eslintrc.js
@ -1,195 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
plugins: ['unicorn', 'jsdoc', 'import-x'],
|
||||
env: { node: true, browser: true, es2023: true },
|
||||
reportUnusedDisableDirectives: true,
|
||||
parserOptions: {
|
||||
project: true,
|
||||
},
|
||||
ignorePatterns: [],
|
||||
globals: {},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'prettier',
|
||||
'plugin:@typescript-eslint/recommended-type-checked',
|
||||
'plugin:jsdoc/recommended-typescript',
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
files: ['repl.ts', 'scripts/**/*.ts'],
|
||||
rules: { 'no-console': 'off' },
|
||||
},
|
||||
{
|
||||
files: ['.eslintrc.js', '.eslintrc.cjs', '*.config.js', 'index.js'],
|
||||
extends: ['plugin:@typescript-eslint/disable-type-checked'],
|
||||
rules: { 'rules/restrict-template-expressions': 'off' },
|
||||
},
|
||||
{
|
||||
files: ['*.d.ts'],
|
||||
rules: { '@typescript-eslint/consistent-type-imports': 'off' },
|
||||
},
|
||||
],
|
||||
rules: {
|
||||
'import-x/order': [
|
||||
'error',
|
||||
{
|
||||
groups: [
|
||||
'builtin',
|
||||
'external',
|
||||
'internal',
|
||||
'parent',
|
||||
'sibling',
|
||||
'index',
|
||||
'object',
|
||||
],
|
||||
'newlines-between': 'always-and-inside-groups',
|
||||
alphabetize: { order: 'asc', caseInsensitive: true },
|
||||
},
|
||||
],
|
||||
'arrow-body-style': ['error', 'as-needed'],
|
||||
'class-methods-use-this': 'warn',
|
||||
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
|
||||
'no-async-promise-executor': 'off',
|
||||
'no-case-declarations': 'off',
|
||||
'no-console': 'warn',
|
||||
'no-constant-condition': ['error', { checkLoops: false }],
|
||||
'no-debugger': 'warn',
|
||||
'no-duplicate-imports': 'off',
|
||||
'no-empty': ['error', { allowEmptyCatch: true }],
|
||||
'no-inner-declarations': 'off',
|
||||
'no-lonely-if': 'error',
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: 'crypto',
|
||||
importNames: ['webcrypto'],
|
||||
message: 'Use global `crypto` instead',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'no-template-curly-in-string': 'error',
|
||||
'no-var': 'error',
|
||||
'object-shorthand': ['error', 'always', { ignoreConstructors: true }],
|
||||
'one-var': ['error', { var: 'never', let: 'never', const: 'never' }],
|
||||
'prefer-arrow-callback': 'off',
|
||||
'prefer-const': ['error', { destructuring: 'all' }],
|
||||
'prefer-destructuring': [
|
||||
'warn',
|
||||
{ AssignmentExpression: { array: false, object: false } },
|
||||
],
|
||||
'prefer-object-spread': 'error',
|
||||
'prefer-rest-params': 'warn',
|
||||
'prefer-spread': 'warn',
|
||||
'quote-props': ['error', 'as-needed'],
|
||||
'sort-imports': ['warn', { ignoreDeclarationSort: true }],
|
||||
'spaced-comment': [
|
||||
'error',
|
||||
'always',
|
||||
{ markers: ['/', '#', '@'], block: { exceptions: ['@'] } },
|
||||
],
|
||||
complexity: ['warn', { max: 100 }],
|
||||
curly: ['error', 'multi-line', 'consistent'],
|
||||
eqeqeq: ['error', 'smart'],
|
||||
yoda: ['error', 'never', { exceptRange: true }],
|
||||
'jsdoc/require-jsdoc': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': [
|
||||
'error',
|
||||
{
|
||||
'ts-expect-error': 'allow-with-description',
|
||||
'ts-check': false,
|
||||
'ts-ignore': 'allow-with-description',
|
||||
'ts-nocheck': 'allow-with-description',
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'error',
|
||||
{
|
||||
disallowTypeAnnotations: false,
|
||||
fixStyle: 'inline-type-imports',
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/explicit-member-accessibility': [
|
||||
'warn',
|
||||
{ accessibility: 'no-public' },
|
||||
],
|
||||
'@typescript-eslint/no-empty-interface': ['error', { allowSingleExtends: true }],
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-misused-promises': ['error', { checksVoidReturn: false }],
|
||||
'@typescript-eslint/no-namespace': 'off',
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
|
||||
'@typescript-eslint/no-unsafe-argument': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-return': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-use-before-define': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'@typescript-eslint/restrict-template-expressions': 'off',
|
||||
'@typescript-eslint/triple-slash-reference': 'off',
|
||||
'@typescript-eslint/unbound-method': 'off',
|
||||
'unicorn/better-regex': 'error',
|
||||
'unicorn/consistent-function-scoping': 'warn',
|
||||
'unicorn/escape-case': 'error',
|
||||
'unicorn/no-array-for-each': 'warn',
|
||||
'unicorn/no-array-method-this-argument': 'error',
|
||||
'unicorn/no-array-push-push': 'warn',
|
||||
'unicorn/no-console-spaces': 'warn',
|
||||
'unicorn/no-for-loop': 'warn',
|
||||
'unicorn/no-instanceof-array': 'error',
|
||||
'unicorn/no-lonely-if': 'warn',
|
||||
'unicorn/no-static-only-class': 'error',
|
||||
'unicorn/no-typeof-undefined': 'error',
|
||||
'unicorn/no-useless-fallback-in-spread': 'error',
|
||||
'unicorn/no-useless-promise-resolve-reject': 'error',
|
||||
'unicorn/no-useless-spread': 'error',
|
||||
'unicorn/no-useless-switch-case': 'error',
|
||||
'unicorn/prefer-array-find': 'error',
|
||||
'unicorn/prefer-array-flat-map': 'error',
|
||||
'unicorn/prefer-array-some': 'error',
|
||||
'unicorn/prefer-at': 'error',
|
||||
'unicorn/prefer-blob-reading-methods': 'error',
|
||||
'unicorn/prefer-date-now': 'error',
|
||||
'unicorn/prefer-default-parameters': 'warn',
|
||||
'unicorn/prefer-dom-node-dataset': 'error',
|
||||
'unicorn/prefer-dom-node-remove': 'error',
|
||||
'unicorn/prefer-export-from': ['error', { ignoreUsedVariables: false }],
|
||||
'unicorn/prefer-includes': 'error',
|
||||
'unicorn/prefer-keyboard-event-key': 'warn',
|
||||
'unicorn/prefer-logical-operator-over-ternary': 'warn',
|
||||
'unicorn/prefer-math-trunc': 'error',
|
||||
'unicorn/prefer-modern-math-apis': 'error',
|
||||
'unicorn/prefer-negative-index': 'error',
|
||||
'unicorn/prefer-node-protocol': 'error',
|
||||
'unicorn/prefer-object-from-entries': 'error',
|
||||
'unicorn/prefer-optional-catch-binding': 'error',
|
||||
'unicorn/prefer-reflect-apply': 'error',
|
||||
'unicorn/prefer-regexp-test': 'error',
|
||||
'unicorn/prefer-set-has': 'warn',
|
||||
'unicorn/prefer-string-slice': 'error',
|
||||
'unicorn/prefer-string-starts-ends-with': 'warn',
|
||||
'unicorn/prefer-string-trim-start-end': 'error',
|
||||
'unicorn/prefer-ternary': 'warn',
|
||||
'unicorn/string-content': [
|
||||
'warn',
|
||||
{
|
||||
patterns: {
|
||||
'->': { suggest: '→', fix: false },
|
||||
'=>': { suggest: '⇒', fix: false },
|
||||
'<-': { suggest: '←', fix: false },
|
||||
'<=': { suggest: '≤', fix: false },
|
||||
'>=': { suggest: '≥', fix: false },
|
||||
'!=': { suggest: '≠', fix: false },
|
||||
'<=>': { suggest: '⇔', fix: false },
|
||||
'\\.\\.\\.': { suggest: '…', fix: false },
|
||||
"'s ": { suggest: '’s ', fix: false },
|
||||
},
|
||||
},
|
||||
],
|
||||
'unicorn/template-indent': 'warn',
|
||||
},
|
||||
parser: '@typescript-eslint/parser',
|
||||
};
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,8 +2,10 @@ drafts
|
||||
!/packages/eslint-plugin-react-hooks
|
||||
/packages/eslint-define-config
|
||||
/react
|
||||
/test
|
||||
src/types/rules
|
||||
|
||||
dist2
|
||||
dist/**/*.js
|
||||
dist/**/*.js.map
|
||||
|
||||
|
3
.npmrc
3
.npmrc
@ -1,2 +1,3 @@
|
||||
registry http://raspberrypi.local:4873
|
||||
always-auth=true
|
||||
always-auth=true
|
||||
ignore-scripts=true
|
||||
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -1,6 +1,7 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"eslint.runtime": "node",
|
||||
"eslint.useFlatConfig": true,
|
||||
"search.exclude": {
|
||||
"**/node_modules": true,
|
||||
"**/bower_components": true,
|
||||
|
44
dist/config/index.d.ts
vendored
Normal file
44
dist/config/index.d.ts
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
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 }>);
|
||||
|
||||
/**
|
||||
* 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)
|
||||
*
|
||||
* @param of Configuration options.
|
||||
* @returns ESLint configuration object.
|
||||
*/
|
||||
export function extendConfig({
|
||||
auto,
|
||||
middlewares: addMiddlewares,
|
||||
configs,
|
||||
}: {
|
||||
auto?: boolean;
|
||||
middlewares?: Middleware[];
|
||||
configs: FlatESLintConfig[];
|
||||
}): Promise<FlatESLintConfig[]>;
|
||||
|
||||
export const error = 'error';
|
||||
export const warn = 'warn';
|
||||
export const off = 'off';
|
8
dist/eslint-plugin-jsx-a11y/index.d.ts
vendored
8
dist/eslint-plugin-jsx-a11y/index.d.ts
vendored
@ -1,8 +0,0 @@
|
||||
import type { Linter } from 'eslint';
|
||||
|
||||
export const rules: Readonly<Linter.RulesRecord>;
|
||||
|
||||
export const configs: {
|
||||
recommended: Linter.BaseConfig;
|
||||
strict: Linter.BaseConfig;
|
||||
};
|
12
dist/eslint-plugin-react-hooks/index.d.ts
vendored
12
dist/eslint-plugin-react-hooks/index.d.ts
vendored
@ -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;
|
||||
};
|
87
dist/index.d.ts
vendored
87
dist/index.d.ts
vendored
@ -1,81 +1,12 @@
|
||||
// Generated by dts-bundle-generator v9.4.0
|
||||
|
||||
import { ESLintConfig, KnownExtends, Rules, Settings } from '@aet/eslint-define-config';
|
||||
import { ESLintUtils } from '@typescript-eslint/utils';
|
||||
import { Rule } from 'eslint';
|
||||
import { Merge, SetRequired } from 'type-fest';
|
||||
import { FlatESLintConfig } from '@aet/eslint-define-config';
|
||||
import { Linter } from 'eslint';
|
||||
|
||||
export type OptionalObjectKey<T> = Exclude<{
|
||||
[Key in keyof T]: undefined | any[] extends T[Key] ? Key : undefined | Record<any, any> extends T[Key] ? Key : never;
|
||||
}[keyof T], undefined>;
|
||||
export type MiddlewareConfig = Merge<SetRequired<ESLintConfig, OptionalObjectKey<ESLintConfig>>, {
|
||||
extends: KnownExtends[];
|
||||
}>;
|
||||
export interface MiddlewareFunctions {
|
||||
addRules(rules: Partial<RuleOptions>): void;
|
||||
addSettings(settings: Partial<Settings>): void;
|
||||
}
|
||||
export type Middleware = (config: MiddlewareConfig, helpers: MiddlewareFunctions) => void;
|
||||
export declare const graphql: Middleware;
|
||||
export declare const jsdoc: Middleware;
|
||||
export declare const storybook: Middleware;
|
||||
export declare const react: Middleware;
|
||||
export declare const reactRefresh: Middleware;
|
||||
export declare const tailwind: Middleware;
|
||||
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' */
|
||||
"custom/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)
|
||||
*/
|
||||
"custom/restrict-template-expressions": RuleEntry<{
|
||||
allow: string[];
|
||||
}>;
|
||||
/** Ban assignment of empty object literals `{}` and replace them with `Object.create(null)` */
|
||||
"custom/no-empty-object-literal": RuleEntry<unknown>;
|
||||
/** Ban useless import alias */
|
||||
"custom/no-useless-import-alias": RuleEntry<unknown>;
|
||||
}
|
||||
export type RuleOptions = Rules & Partial<LocalRuleOptions>;
|
||||
export interface CustomRule {
|
||||
rule: () => Promise<{
|
||||
default: Rule.RuleModule | ESLintUtils.RuleModule<string, unknown[]>;
|
||||
}>;
|
||||
options?: RuleLevel;
|
||||
}
|
||||
/**
|
||||
* ESLint Configuration.
|
||||
* @see [ESLint Configuration](https://eslint.org/docs/latest/user-guide/configuring/)
|
||||
*/
|
||||
export type InputConfig = Omit<ESLintConfig, "rules"> & {
|
||||
/**
|
||||
* Rules.
|
||||
* @see [Rules](https://eslint.org/docs/latest/user-guide/configuring/rules)
|
||||
*/
|
||||
rules?: Partial<RuleOptions>;
|
||||
/**
|
||||
* Glob pattern to find paths to custom rule files in JavaScript or TypeScript.
|
||||
* Note this must be a string literal or an array of string literals since
|
||||
* this is statically analyzed.
|
||||
*
|
||||
* Rules are prefixed with `custom/` and the file name is used as the rule name.
|
||||
*/
|
||||
customRuleFiles?: string | string[];
|
||||
/**
|
||||
* Automatically detect project types, dependencies and deduct the plugins.
|
||||
* @default true
|
||||
*/
|
||||
auto?: boolean;
|
||||
};
|
||||
export type MiddlewareResult = Linter.Config | Linter.Config[];
|
||||
export type Middleware = (() => Promise<MiddlewareResult>) | (() => Promise<{
|
||||
default: MiddlewareResult;
|
||||
}>);
|
||||
/**
|
||||
* Returns a ESLint config object.
|
||||
*
|
||||
@ -98,8 +29,10 @@ export type InputConfig = Omit<ESLintConfig, "rules"> & {
|
||||
* @param of Configuration options.
|
||||
* @returns ESLint configuration object.
|
||||
*/
|
||||
export declare function extendConfig(of?: InputConfig & {
|
||||
export declare function extendConfig({ auto, middlewares: addMiddlewares, configs, }: {
|
||||
auto?: boolean;
|
||||
middlewares?: Middleware[];
|
||||
}): ESLintConfig;
|
||||
configs: FlatESLintConfig[];
|
||||
}): Promise<FlatESLintConfig[]>;
|
||||
|
||||
export {};
|
||||
|
50
dist/package.json
vendored
50
dist/package.json
vendored
@ -1,56 +1,58 @@
|
||||
{
|
||||
"name": "@aet/eslint-rules",
|
||||
"version": "1.0.1-beta.42",
|
||||
"version": "2.0.1-beta.1",
|
||||
"license": "UNLICENSED",
|
||||
"bin": {
|
||||
"eslint-install": "install.js",
|
||||
"eslint-print": "print-config.sh"
|
||||
},
|
||||
"main": "./config/index.js",
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0",
|
||||
"eslint": "^9.12.0",
|
||||
"typescript": "^5.4.4"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tanstack/eslint-plugin-query": "^5.52.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@antfu/install-pkg": "^0.4.0",
|
||||
"@antfu/install-pkg": "^0.4.1",
|
||||
"@nolyfill/is-core-module": "^1.0.39",
|
||||
"@aet/eslint-define-config": "^0.1.0-beta.24",
|
||||
"@aet/eslint-define-config": "^0.1.0-beta.28",
|
||||
"@eslint/js": "^9.12.0",
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@types/eslint": "^9.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.2.0",
|
||||
"@typescript-eslint/parser": "^8.2.0",
|
||||
"@typescript-eslint/type-utils": "^8.2.0",
|
||||
"@typescript-eslint/utils": "^8.2.0",
|
||||
"@eslint-react/eslint-plugin": "1.12.1",
|
||||
"@stylistic/eslint-plugin": "^2.6.4",
|
||||
"aria-query": "^5.3.0",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.9.0",
|
||||
"@typescript-eslint/parser": "^8.9.0",
|
||||
"@eslint-react/eslint-plugin": "1.15.0",
|
||||
"@stylistic/eslint-plugin": "^2.9.0",
|
||||
"@typescript-eslint/type-utils": "^8.9.0",
|
||||
"@typescript-eslint/utils": "^8.9.0",
|
||||
"aria-query": "^5.3.2",
|
||||
"axe-core": "^4.10.0",
|
||||
"axobject-query": "4.1.0",
|
||||
"damerau-levenshtein": "1.0.8",
|
||||
"debug": "^4.3.6",
|
||||
"debug": "^4.3.7",
|
||||
"doctrine": "^3.0.0",
|
||||
"emoji-regex": "^10.3.0",
|
||||
"emoji-regex": "^10.4.0",
|
||||
"enhanced-resolve": "^5.17.1",
|
||||
"typescript-eslint": "^8.9.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-import-resolver-node": "^0.3.9",
|
||||
"eslint-module-utils": "^2.8.1",
|
||||
"eslint-module-utils": "^2.12.0",
|
||||
"eslint-plugin-es-x": "^8.0.0",
|
||||
"eslint-plugin-import-x": "^3.1.0",
|
||||
"eslint-plugin-jsdoc": "^50.2.2",
|
||||
"eslint-plugin-react-refresh": "^0.4.11",
|
||||
"eslint-plugin-unicorn": "^55.0.0",
|
||||
"eslint-plugin-import-x": "^4.3.1",
|
||||
"eslint-plugin-unicorn": "^56.0.0",
|
||||
"esprima": "^4.0.1",
|
||||
"esquery": "^1.6.0",
|
||||
"estraverse": "^5.3.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"get-tsconfig": "^4.7.6",
|
||||
"is-bun-module": "^1.1.0",
|
||||
"ignore": "^5.3.2",
|
||||
"get-tsconfig": "^4.8.1",
|
||||
"globals": "^15.11.0",
|
||||
"ignore": "^6.0.2",
|
||||
"is-bun-module": "^1.2.1",
|
||||
"is-glob": "^4.0.3",
|
||||
"language-tags": "^1.0.9",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"minimatch": "^10.0.1",
|
||||
"semver": "^7.6.3"
|
||||
},
|
||||
@ -68,4 +70,4 @@
|
||||
"**/is-core-module": "file:./overrides/is-core-module",
|
||||
"**/supports-preserve-symlinks-flag": "file:./overrides/supports-preserve-symlinks-flag"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
2
dist/print-config.sh
vendored
Normal file → Executable file
2
dist/print-config.sh
vendored
Normal file → Executable file
@ -1,2 +1,2 @@
|
||||
#!/bin/bash
|
||||
node -e "console.dir(require('./.eslintrc.js'), { depth: null })"
|
||||
node -e "import('./eslint.config.mjs').then(config => console.dir(config, { depth: null }))"
|
||||
|
3
eslint.config.cjs
Normal file
3
eslint.config.cjs
Normal file
@ -0,0 +1,3 @@
|
||||
/* eslint-disable */
|
||||
require('@swc-node/register');
|
||||
module.exports = require('./.eslint.ts').default;
|
71
package.json
71
package.json
@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "@aet/eslint-configs",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "./scripts/build.ts",
|
||||
"check-import": "./scripts/check-imports.ts",
|
||||
@ -8,48 +9,67 @@
|
||||
},
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@aet/eslint-define-config": "^0.1.0-beta.24",
|
||||
"@antfu/install-pkg": "^0.4.0",
|
||||
"@babel/core": "^7.25.2",
|
||||
"@aet/eslint-define-config": "^0.1.0-beta.28",
|
||||
"@antfu/install-pkg": "^0.4.1",
|
||||
"@babel/core": "^7.25.8",
|
||||
"@babel/plugin-transform-flow-strip-types": "^7.25.2",
|
||||
"@babel/preset-env": "^7.25.4",
|
||||
"@babel/preset-env": "^7.25.8",
|
||||
"@eslint-react/eslint-plugin": "^1.15.0",
|
||||
"@eslint/js": "^9.12.0",
|
||||
"@graphql-eslint/eslint-plugin": "^3.20.1",
|
||||
"@stylistic/eslint-plugin": "^2.9.0",
|
||||
"@swc-node/register": "^1.10.9",
|
||||
"@tanstack/eslint-plugin-query": "^5.59.7",
|
||||
"@types/babel-plugin-macros": "^3.1.3",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"@types/eslint": "^9.6.0",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
||||
"@types/eslint__js": "^8.42.3",
|
||||
"@types/esprima": "^4.0.6",
|
||||
"@types/esquery": "^1.5.4",
|
||||
"@types/estree": "^1.0.5",
|
||||
"@types/estree": "^1.0.6",
|
||||
"@types/estree-jsx": "^1.0.5",
|
||||
"@types/lodash": "^4.17.7",
|
||||
"@types/node": "^22.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.2.0",
|
||||
"@typescript-eslint/type-utils": "^8.2.0",
|
||||
"@typescript-eslint/types": "^8.2.0",
|
||||
"@typescript-eslint/typescript-estree": "^8.2.0",
|
||||
"@typescript-eslint/utils": "^8.2.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^22.7.5",
|
||||
"@types/react-refresh": "^0.14.6",
|
||||
"@typescript-eslint/eslint-plugin": "^8.9.0",
|
||||
"@typescript-eslint/parser": "^8.9.0",
|
||||
"@typescript-eslint/type-utils": "^8.9.0",
|
||||
"@typescript-eslint/types": "^8.9.0",
|
||||
"@typescript-eslint/typescript-estree": "^8.9.0",
|
||||
"@typescript-eslint/utils": "^8.9.0",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"dts-bundle-generator": "9.4.0",
|
||||
"esbuild": "0.23.1",
|
||||
"esbuild": "0.24.0",
|
||||
"esbuild-plugin-alias": "^0.2.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint": "9.12.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-import-x": "^3.1.0",
|
||||
"eslint-plugin-jsdoc": "^50.2.2",
|
||||
"eslint-plugin-unicorn": "^55.0.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.3",
|
||||
"eslint-plugin-import-x": "^4.3.1",
|
||||
"eslint-plugin-jsdoc": "^50.4.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.12",
|
||||
"eslint-plugin-storybook": "canary",
|
||||
"eslint-plugin-testing-library": "^6.3.0",
|
||||
"eslint-plugin-unicorn": "^56.0.0",
|
||||
"eslint-plugin-vitest": "^0.5.4",
|
||||
"esprima": "^4.0.1",
|
||||
"esquery": "^1.6.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"find-cache-dir": "^5.0.0",
|
||||
"json-schema-to-ts": "^3.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"globals": "^15.11.0",
|
||||
"graphql": "^16.9.0",
|
||||
"jiti": "^1.21.6",
|
||||
"json-schema-to-ts": "^3.1.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nolyfill": "^1.0.39",
|
||||
"patch-package": "^8.0.0",
|
||||
"picocolors": "^1.0.1",
|
||||
"picocolors": "^1.1.0",
|
||||
"prettier": "^3.3.3",
|
||||
"prop-types": "^15.8.1",
|
||||
"terser": "^5.31.6",
|
||||
"type-fest": "^4.25.0",
|
||||
"typescript": "^5.5.4"
|
||||
"terser": "^5.34.1",
|
||||
"type-fest": "^4.26.1",
|
||||
"typescript": "^5.6.3",
|
||||
"typescript-eslint": "^8.9.0"
|
||||
},
|
||||
"prettier": {
|
||||
"arrowParens": "avoid",
|
||||
@ -75,7 +95,8 @@
|
||||
"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"
|
||||
"@typescript-eslint/typescript-estree@8.0.0": "patches/@typescript-eslint__typescript-estree@8.0.0.patch",
|
||||
"dts-bundle-generator": "patches/dts-bundle-generator.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,8 +22,11 @@ import type {
|
||||
ChainExpression,
|
||||
Pattern,
|
||||
OptionalMemberExpression,
|
||||
ArrayExpression,
|
||||
VariableDeclaration,
|
||||
} from 'estree';
|
||||
import type { FromSchema } from 'json-schema-to-ts';
|
||||
|
||||
import { __EXPERIMENTAL__ } from './index';
|
||||
|
||||
const schema = {
|
||||
@ -81,7 +84,23 @@ const rule: Rule.RuleModule = {
|
||||
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.
|
||||
const setStateCallSites = new WeakMap<Expression, Pattern>();
|
||||
@ -128,7 +147,7 @@ const rule: Rule.RuleModule = {
|
||||
' }\n' +
|
||||
' fetchData();\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
|
||||
// const [state, dispatch] = useReducer() / React.useReducer()
|
||||
// ^^^ true for this reference
|
||||
// const [state, dispatch] = useActionState() / React.useActionState()
|
||||
// ^^^ true for this reference
|
||||
// const ref = useRef()
|
||||
// ^^^ true for this reference
|
||||
// const onStuff = useEffectEvent(() => {})
|
||||
@ -187,31 +208,32 @@ const rule: Rule.RuleModule = {
|
||||
return false;
|
||||
}
|
||||
// Look for `let stuff = ...`
|
||||
if (def.node.type !== 'VariableDeclarator') {
|
||||
const node = def.node as Node;
|
||||
if (node.type !== 'VariableDeclarator') {
|
||||
return false;
|
||||
}
|
||||
let init = (def.node as VariableDeclarator).init;
|
||||
let init = node.init;
|
||||
if (init == null) {
|
||||
return false;
|
||||
}
|
||||
while (init.type === 'TSAsExpression') {
|
||||
while (init.type === 'TSAsExpression' || init.type === 'AsExpression') {
|
||||
init = init.expression;
|
||||
}
|
||||
// Detect primitive constants
|
||||
// const foo = 42
|
||||
let declaration = def.node.parent;
|
||||
let declaration = node.parent;
|
||||
if (declaration == null) {
|
||||
// This might happen if variable is declared after the callback.
|
||||
// In that case ESLint won't set up .parent refs.
|
||||
// So we'll set them up manually.
|
||||
fastFindReferenceWithParent(componentScope.block, def.node.id);
|
||||
declaration = def.node.parent;
|
||||
fastFindReferenceWithParent(componentScope.block, node.id);
|
||||
declaration = node.parent;
|
||||
if (declaration == null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (
|
||||
declaration.kind === 'const' &&
|
||||
(declaration as VariableDeclaration).kind === 'const' &&
|
||||
init.type === 'Literal' &&
|
||||
(typeof init.value === 'string' ||
|
||||
typeof init.value === 'number' ||
|
||||
@ -252,7 +274,11 @@ const rule: Rule.RuleModule = {
|
||||
}
|
||||
// useEffectEvent() return value is always unstable.
|
||||
return true;
|
||||
} else if (name === 'useState' || name === 'useReducer') {
|
||||
} else if (
|
||||
name === 'useState' ||
|
||||
name === 'useReducer' ||
|
||||
name === 'useActionState'
|
||||
) {
|
||||
// Only consider second value in initializing tuple stable.
|
||||
if (
|
||||
id.type === 'ArrayPattern' &&
|
||||
@ -264,14 +290,14 @@ const rule: Rule.RuleModule = {
|
||||
if (name === 'useState') {
|
||||
const references = resolved.references;
|
||||
let writeCount = 0;
|
||||
for (let i = 0; i < references.length; i++) {
|
||||
if (references[i].isWrite()) {
|
||||
for (const reference of references) {
|
||||
if (reference.isWrite()) {
|
||||
writeCount++;
|
||||
}
|
||||
if (writeCount > 1) {
|
||||
return false;
|
||||
}
|
||||
setStateCallSites.set(references[i].identifier, id.elements[0]!);
|
||||
setStateCallSites.set(reference.identifier, id.elements[0]!);
|
||||
}
|
||||
}
|
||||
// Setter is stable.
|
||||
@ -279,27 +305,25 @@ const rule: Rule.RuleModule = {
|
||||
} else if (id.elements[0] === resolved.identifiers[0]) {
|
||||
if (name === 'useState') {
|
||||
const references = resolved.references;
|
||||
for (let i = 0; i < references.length; i++) {
|
||||
stateVariables.add(references[i].identifier);
|
||||
for (const reference of references) {
|
||||
stateVariables.add(reference.identifier);
|
||||
}
|
||||
}
|
||||
// State variable itself is dynamic.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if (name === 'useTransition') {
|
||||
} else if (
|
||||
// Only consider second value in initializing tuple stable.
|
||||
if (
|
||||
id.type === 'ArrayPattern' &&
|
||||
id.elements.length === 2 &&
|
||||
Array.isArray(resolved.identifiers)
|
||||
) {
|
||||
// Is second tuple value the same reference we're checking?
|
||||
if (id.elements[1] === resolved.identifiers[0]) {
|
||||
// Setter is stable.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
name === 'useTransition' &&
|
||||
id.type === 'ArrayPattern' &&
|
||||
id.elements.length === 2 &&
|
||||
Array.isArray(resolved.identifiers) &&
|
||||
// Is second tuple value the same reference we're checking?
|
||||
id.elements[1] === resolved.identifiers[0]
|
||||
) {
|
||||
// Setter is stable.
|
||||
return true;
|
||||
}
|
||||
// By default assume it's dynamic.
|
||||
return false;
|
||||
@ -319,7 +343,7 @@ const rule: Rule.RuleModule = {
|
||||
}
|
||||
// Search the direct component subscopes for
|
||||
// top-level function definitions matching this reference.
|
||||
const fnNode = def.node;
|
||||
const fnNode = def.node as Node;
|
||||
const childScopes = componentScope.childScopes;
|
||||
let fnScope = null;
|
||||
let i;
|
||||
@ -424,9 +448,9 @@ const rule: Rule.RuleModule = {
|
||||
dependencyNode.type === 'Identifier' &&
|
||||
(dependencyNode.parent!.type === 'MemberExpression' ||
|
||||
dependencyNode.parent!.type === 'OptionalMemberExpression') &&
|
||||
!dependencyNode.parent!.computed &&
|
||||
dependencyNode.parent!.property.type === 'Identifier' &&
|
||||
dependencyNode.parent!.property.name === 'current' &&
|
||||
!dependencyNode.parent.computed &&
|
||||
dependencyNode.parent.property.type === 'Identifier' &&
|
||||
dependencyNode.parent.property.name === 'current' &&
|
||||
// ...in a cleanup function or below...
|
||||
isInsideEffectCleanup(reference)
|
||||
) {
|
||||
@ -479,12 +503,11 @@ const rule: Rule.RuleModule = {
|
||||
|
||||
// Warn about accessing .current in cleanup effects.
|
||||
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?
|
||||
// Let's see if we can find a .current assignment.
|
||||
let foundCurrentAssignment = false;
|
||||
for (let i = 0; i < references.length; i++) {
|
||||
const { identifier } = references[i];
|
||||
for (const { identifier } of references) {
|
||||
const { parent } = identifier;
|
||||
if (
|
||||
parent != null &&
|
||||
@ -496,7 +519,7 @@ const rule: Rule.RuleModule = {
|
||||
parent.property.name === 'current' &&
|
||||
// ref.current = <something>
|
||||
parent.parent!.type === 'AssignmentExpression' &&
|
||||
parent.parent!.left === parent
|
||||
parent.parent.left === parent
|
||||
) {
|
||||
foundCurrentAssignment = true;
|
||||
break;
|
||||
@ -529,11 +552,11 @@ const rule: Rule.RuleModule = {
|
||||
node: writeExpr,
|
||||
message:
|
||||
`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 ` +
|
||||
`Hook and keep the mutable value in the '.current' property. ` +
|
||||
`Otherwise, you can move this variable directly inside ` +
|
||||
`${context.getSource(reactiveHook)}.`,
|
||||
`${getSource(reactiveHook)}.`,
|
||||
});
|
||||
}
|
||||
|
||||
@ -543,11 +566,11 @@ const rule: Rule.RuleModule = {
|
||||
if (isStable) {
|
||||
stableDependencies.add(key);
|
||||
}
|
||||
references.forEach(reference => {
|
||||
for (const reference of references) {
|
||||
if (reference.writeExpr) {
|
||||
reportStaleAssignment(reference.writeExpr, key);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (staleAssignments.size > 0) {
|
||||
@ -563,15 +586,15 @@ const rule: Rule.RuleModule = {
|
||||
if (setStateInsideEffectWithoutDeps) {
|
||||
return;
|
||||
}
|
||||
references.forEach(reference => {
|
||||
for (const reference of references) {
|
||||
if (setStateInsideEffectWithoutDeps) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
const id = reference.identifier;
|
||||
const isSetState: boolean = setStateCallSites.has(id);
|
||||
if (!isSetState) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
let fnScope: Scope.Scope = reference.from;
|
||||
@ -583,9 +606,8 @@ const rule: Rule.RuleModule = {
|
||||
// TODO: we could potentially ignore early returns.
|
||||
setStateInsideEffectWithoutDeps = key;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (setStateInsideEffectWithoutDeps) {
|
||||
const { suggestedDependencies } = collectRecommendations({
|
||||
dependencies,
|
||||
@ -620,49 +642,56 @@ const rule: Rule.RuleModule = {
|
||||
|
||||
const declaredDependencies: DeclaredDependency[] = [];
|
||||
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
|
||||
// can't verify that the user provided the correct dependencies. Tell
|
||||
// the user this in an error.
|
||||
reportProblem({
|
||||
node: declaredDependenciesNode,
|
||||
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 ' +
|
||||
"can't statically verify whether you've passed the correct " +
|
||||
'dependencies.',
|
||||
});
|
||||
} else {
|
||||
declaredDependenciesNode.elements.forEach(declaredDependencyNode => {
|
||||
const arrayExpression = isTSAsArrayExpression
|
||||
? declaredDependenciesNode.expression
|
||||
: declaredDependenciesNode;
|
||||
|
||||
for (const declaredDependencyNode of (arrayExpression as ArrayExpression)
|
||||
.elements) {
|
||||
// Skip elided elements.
|
||||
if (declaredDependencyNode === null) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
// If we see a spread element then add a special warning.
|
||||
if (declaredDependencyNode.type === 'SpreadElement') {
|
||||
reportProblem({
|
||||
node: declaredDependencyNode,
|
||||
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 " +
|
||||
"statically verify whether you've passed the " +
|
||||
'correct dependencies.',
|
||||
});
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
if (useEffectEventVariables.has(declaredDependencyNode)) {
|
||||
reportProblem({
|
||||
node: declaredDependencyNode,
|
||||
message:
|
||||
'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: [
|
||||
{
|
||||
desc: `Remove the dependency \`${context.getSource(
|
||||
declaredDependencyNode,
|
||||
)}\``,
|
||||
desc: `Remove the dependency \`${getSource(declaredDependencyNode)}\``,
|
||||
fix(fixer) {
|
||||
return fixer.removeRange(declaredDependencyNode.range!);
|
||||
return fixer.removeRange(declaredDependencyNode.range);
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -696,13 +725,13 @@ const rule: Rule.RuleModule = {
|
||||
reportProblem({
|
||||
node: declaredDependencyNode,
|
||||
message:
|
||||
`React Hook ${context.getSource(reactiveHook)} has a ` +
|
||||
`React Hook ${getSource(reactiveHook)} has a ` +
|
||||
`complex expression in the dependency array. ` +
|
||||
'Extract it to a separate variable so it can be statically checked.',
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
continue;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
@ -731,7 +760,7 @@ const rule: Rule.RuleModule = {
|
||||
if (!isDeclaredInComponent) {
|
||||
externalDependencies.add(declaredDependency);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
@ -782,9 +811,7 @@ const rule: Rule.RuleModule = {
|
||||
|
||||
const message =
|
||||
`The '${construction.name.name}' ${depType} ${causation} the dependencies of ` +
|
||||
`${reactiveHookName} Hook (at line ${
|
||||
declaredDependenciesNode.loc!.start.line
|
||||
}) ` +
|
||||
`${reactiveHookName} Hook (at line ${declaredDependenciesNode.loc!.start.line}) ` +
|
||||
`change on every render. ${advice}`;
|
||||
|
||||
let suggest: Rule.SuggestionReportDescriptor[] | undefined;
|
||||
@ -838,7 +865,7 @@ const rule: Rule.RuleModule = {
|
||||
// in some extra deduplication. We can't do this
|
||||
// for effects though because those have legit
|
||||
// use cases for over-specifying deps.
|
||||
if (!isEffect && missingDependencies.size) {
|
||||
if (!isEffect && missingDependencies.size > 0) {
|
||||
suggestedDeps = collectRecommendations({
|
||||
dependencies,
|
||||
declaredDependencies: [], // Pretend we don't know
|
||||
@ -854,7 +881,7 @@ const rule: Rule.RuleModule = {
|
||||
return true;
|
||||
}
|
||||
const declaredDepKeys = declaredDependencies.map(dep => dep.key);
|
||||
const sortedDeclaredDepKeys = declaredDepKeys.slice().sort();
|
||||
const sortedDeclaredDepKeys = [...declaredDepKeys].sort();
|
||||
return declaredDepKeys.join(',') === sortedDeclaredDepKeys.join(',');
|
||||
}
|
||||
|
||||
@ -895,11 +922,7 @@ const rule: Rule.RuleModule = {
|
||||
' ' +
|
||||
(deps.size > 1 ? 'dependencies' : 'dependency') +
|
||||
': ' +
|
||||
joinEnglish(
|
||||
Array.from(deps)
|
||||
.sort()
|
||||
.map(name => "'" + formatDependency(name) + "'"),
|
||||
) +
|
||||
joinEnglish([...deps].sort().map(name => "'" + formatDependency(name) + "'")) +
|
||||
`. Either ${fixVerb} ${
|
||||
deps.size > 1 ? 'them' : 'it'
|
||||
} or remove the dependency array.`
|
||||
@ -909,20 +932,20 @@ const rule: Rule.RuleModule = {
|
||||
let extraWarning = '';
|
||||
if (unnecessaryDependencies.size > 0) {
|
||||
let badRef: string | null = null;
|
||||
Array.from(unnecessaryDependencies.keys()).forEach(key => {
|
||||
for (const key of unnecessaryDependencies.keys()) {
|
||||
if (badRef !== null) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
if (key.endsWith('.current')) {
|
||||
badRef = key;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (badRef !== null) {
|
||||
extraWarning =
|
||||
` Mutable values like '${badRef}' aren't valid dependencies ` +
|
||||
"because mutating them doesn't re-render the component.";
|
||||
} 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
|
||||
// because in that case they're clearly not referring to globals.
|
||||
if (!scope.set.has(dep)) {
|
||||
@ -971,11 +994,11 @@ const rule: Rule.RuleModule = {
|
||||
` However, 'props' will change when *any* prop changes, so the ` +
|
||||
`preferred fix is to destructure the 'props' object outside of ` +
|
||||
`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.
|
||||
// This usually means they're unaware of useCallback.
|
||||
let missingCallbackDep: string | null = null;
|
||||
@ -1041,7 +1064,7 @@ const rule: Rule.RuleModule = {
|
||||
let id: Identifier;
|
||||
let maybeCall: Node | null;
|
||||
for (let i = 0; i < references.length; i++) {
|
||||
id = references[i].identifier as Identifier;
|
||||
id = references[i].identifier;
|
||||
maybeCall = id.parent!;
|
||||
// Try to see if we have setState(someExpr(missingDep)).
|
||||
while (maybeCall != null && maybeCall !== componentScope.block) {
|
||||
@ -1125,7 +1148,7 @@ const rule: Rule.RuleModule = {
|
||||
reportProblem({
|
||||
node: declaredDependenciesNode,
|
||||
message:
|
||||
`React Hook ${context.getSource(reactiveHook)} has ` +
|
||||
`React Hook ${getSource(reactiveHook)} has ` +
|
||||
// To avoid a long message, show the next actionable item.
|
||||
(getWarningMessage(missingDependencies, 'a', 'missing', 'include') ||
|
||||
getWarningMessage(unnecessaryDependencies, 'an', 'unnecessary', 'exclude') ||
|
||||
@ -1158,7 +1181,11 @@ const rule: Rule.RuleModule = {
|
||||
const reactiveHook = node.callee as Identifier | MemberExpression;
|
||||
const reactiveHookName = (getNodeWithoutReactNamespace(reactiveHook) as Identifier)
|
||||
.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);
|
||||
|
||||
// Check whether a callback is supplied. If there is no callback supplied
|
||||
@ -1203,7 +1230,16 @@ const rule: Rule.RuleModule = {
|
||||
isEffect,
|
||||
);
|
||||
return; // Handled
|
||||
case 'Identifier':
|
||||
case 'TSAsExpression':
|
||||
visitFunctionWithDependencies(
|
||||
callback.expression,
|
||||
declaredDependenciesNode,
|
||||
reactiveHook,
|
||||
reactiveHookName,
|
||||
isEffect,
|
||||
);
|
||||
return; // Handled
|
||||
case 'Identifier': {
|
||||
if (!declaredDependenciesNode) {
|
||||
// No deps, no problems.
|
||||
return; // Handled
|
||||
@ -1221,7 +1257,7 @@ const rule: Rule.RuleModule = {
|
||||
return; // Handled
|
||||
}
|
||||
// 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 it's not in scope, we don't care.
|
||||
return; // Handled
|
||||
@ -1271,6 +1307,7 @@ const rule: Rule.RuleModule = {
|
||||
break; // Unhandled
|
||||
}
|
||||
break; // Unhandled
|
||||
}
|
||||
default:
|
||||
// useEffect(generateEffectBody(), []);
|
||||
reportProblem({
|
||||
@ -1358,33 +1395,33 @@ function collectRecommendations({
|
||||
|
||||
function createDepTree(): DepTree {
|
||||
return {
|
||||
isUsed: false,
|
||||
isSatisfiedRecursively: false,
|
||||
isSubtreeUsed: false,
|
||||
children: new Map<string, never>(),
|
||||
isUsed: false, // True if used in code
|
||||
isSatisfiedRecursively: false, // True if specified in deps
|
||||
isSubtreeUsed: false, // True if something deeper is used by code
|
||||
children: new Map(), // Nodes for properties
|
||||
};
|
||||
}
|
||||
|
||||
// Mark all required nodes first.
|
||||
// Imagine exclamation marks next to each used deep property.
|
||||
dependencies.forEach((_, key) => {
|
||||
for (const key of dependencies.keys()) {
|
||||
const node = getOrCreateNodeByPath(depTree, key);
|
||||
node.isUsed = true;
|
||||
markAllParentsByPath(depTree, key, parent => {
|
||||
parent.isSubtreeUsed = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Mark all satisfied nodes.
|
||||
// Imagine checkmarks next to each declared dependency.
|
||||
declaredDependencies.forEach(({ key }) => {
|
||||
for (const { key } of declaredDependencies) {
|
||||
const node = getOrCreateNodeByPath(depTree, key);
|
||||
node.isSatisfiedRecursively = true;
|
||||
});
|
||||
stableDependencies.forEach(key => {
|
||||
}
|
||||
for (const key of stableDependencies) {
|
||||
const node = getOrCreateNodeByPath(depTree, key);
|
||||
node.isSatisfiedRecursively = true;
|
||||
});
|
||||
}
|
||||
|
||||
// Tree manipulation helpers.
|
||||
function getOrCreateNodeByPath(rootNode: DepTree, path: string): DepTree {
|
||||
@ -1460,15 +1497,15 @@ function collectRecommendations({
|
||||
const suggestedDependencies: string[] = [];
|
||||
const unnecessaryDependencies = new Set<string>();
|
||||
const duplicateDependencies = new Set<string>();
|
||||
declaredDependencies.forEach(({ key }) => {
|
||||
for (const { key } of declaredDependencies) {
|
||||
// Does this declared dep satisfy a real need?
|
||||
if (satisfyingDependencies.has(key)) {
|
||||
if (!suggestedDependencies.includes(key)) {
|
||||
// Good one.
|
||||
suggestedDependencies.push(key);
|
||||
} else {
|
||||
if (suggestedDependencies.includes(key)) {
|
||||
// Duplicate.
|
||||
duplicateDependencies.add(key);
|
||||
} else {
|
||||
// Good one.
|
||||
suggestedDependencies.push(key);
|
||||
}
|
||||
} else {
|
||||
if (isEffect && !key.endsWith('.current') && !externalDependencies.has(key)) {
|
||||
@ -1476,7 +1513,7 @@ function collectRecommendations({
|
||||
// Such as resetting scroll when ID changes.
|
||||
// Consider them legit.
|
||||
// The exception is ref.current which is always wrong.
|
||||
if (suggestedDependencies.indexOf(key) === -1) {
|
||||
if (!suggestedDependencies.includes(key)) {
|
||||
suggestedDependencies.push(key);
|
||||
}
|
||||
} else {
|
||||
@ -1484,12 +1521,12 @@ function collectRecommendations({
|
||||
unnecessaryDependencies.add(key);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Then add the missing ones at the end.
|
||||
missingDependencies.forEach(key => {
|
||||
for (const key of missingDependencies) {
|
||||
suggestedDependencies.push(key);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
suggestedDependencies,
|
||||
@ -1545,7 +1582,7 @@ function getConstructionExpressionType(node: Node) {
|
||||
}
|
||||
return null;
|
||||
case 'TypeCastExpression':
|
||||
return getConstructionExpressionType(node.expression);
|
||||
case 'AsExpression':
|
||||
case 'TSAsExpression':
|
||||
return getConstructionExpressionType(node.expression);
|
||||
}
|
||||
@ -1623,12 +1660,13 @@ function scanForConstructions({
|
||||
while (currentScope !== scope && currentScope != null) {
|
||||
currentScope = currentScope.upper!;
|
||||
}
|
||||
if (currentScope !== scope) {
|
||||
if (
|
||||
currentScope !== scope &&
|
||||
// This reference is outside the Hook callback.
|
||||
// It can only be legit if it's the deps array.
|
||||
if (!isAncestorNodeOf(declaredDependenciesNode, reference.identifier)) {
|
||||
return true;
|
||||
}
|
||||
!isAncestorNodeOf(declaredDependenciesNode, reference.identifier)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@ -1653,7 +1691,6 @@ function getDependency(node: Node): Node {
|
||||
if (
|
||||
(parent.type === 'MemberExpression' || parent.type === 'OptionalMemberExpression') &&
|
||||
parent.object === node &&
|
||||
parent.property.type === 'Identifier' &&
|
||||
parent.property.name !== 'current' &&
|
||||
!parent.computed &&
|
||||
!(
|
||||
@ -1796,7 +1833,7 @@ function getReactiveHookCallbackIndex(
|
||||
try {
|
||||
name = analyzePropertyChain(node, null);
|
||||
} catch (error) {
|
||||
if (/Unsupported node type/.test(error.message)) {
|
||||
if (/Unsupported node type/.test((error as Error).message)) {
|
||||
return 0;
|
||||
} else {
|
||||
throw error;
|
||||
@ -1842,12 +1879,12 @@ function fastFindReferenceWithParent(start: Node, target: Node): Node | null {
|
||||
value.parent = item;
|
||||
queue.push(value);
|
||||
} else if (Array.isArray(value)) {
|
||||
value.forEach(val => {
|
||||
for (const val of value) {
|
||||
if (isNodeLike(val)) {
|
||||
val.parent = item;
|
||||
queue.push(val);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1870,7 +1907,7 @@ function joinEnglish(arr: string[]): string {
|
||||
return s;
|
||||
}
|
||||
|
||||
function isNodeLike(val: any): boolean {
|
||||
function isNodeLike(val: unknown): val is Node {
|
||||
return (
|
||||
typeof val === 'object' &&
|
||||
val !== null &&
|
||||
|
@ -6,7 +6,6 @@
|
||||
*/
|
||||
|
||||
/* global BigInt */
|
||||
/* eslint-disable no-for-of-loops/no-for-of-loops */
|
||||
import type { Rule, Scope } from 'eslint';
|
||||
import type {
|
||||
CallExpression,
|
||||
@ -16,6 +15,7 @@ import type {
|
||||
Identifier,
|
||||
BaseFunction,
|
||||
} from 'estree';
|
||||
|
||||
import { __EXPERIMENTAL__ } from './index';
|
||||
|
||||
/**
|
||||
@ -24,10 +24,7 @@ import { __EXPERIMENTAL__ } from './index';
|
||||
*/
|
||||
|
||||
function isHookName(s: string) {
|
||||
if (__EXPERIMENTAL__) {
|
||||
return s === 'use' || /^use[A-Z0-9]/.test(s);
|
||||
}
|
||||
return /^use[A-Z0-9]/.test(s);
|
||||
return s === 'use' || /^use[\dA-Z]/.test(s);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -94,10 +91,8 @@ function isMemoCallback(node: Rule.Node) {
|
||||
function isInsideComponentOrHook(node: Rule.Node) {
|
||||
while (node) {
|
||||
const functionName = getFunctionName(node);
|
||||
if (functionName) {
|
||||
if (isComponentName(functionName) || isHook(functionName)) {
|
||||
return true;
|
||||
}
|
||||
if (functionName && (isComponentName(functionName) || isHook(functionName))) {
|
||||
return true;
|
||||
}
|
||||
if (isForwardRefCallback(node) || isMemoCallback(node)) {
|
||||
return true;
|
||||
@ -115,10 +110,7 @@ function isUseEffectEventIdentifier(node: Node) {
|
||||
}
|
||||
|
||||
function isUseIdentifier(node: Node) {
|
||||
if (__EXPERIMENTAL__) {
|
||||
return node.type === 'Identifier' && node.name === 'use';
|
||||
}
|
||||
return false;
|
||||
return isReactFunction(node as Expression, 'use');
|
||||
}
|
||||
|
||||
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 {
|
||||
// Maintain code segment path stack as we traverse.
|
||||
onCodePathSegmentStart: segment => codePathSegmentStack.push(segment),
|
||||
@ -479,7 +487,7 @@ const rule: Rule.RuleModule = {
|
||||
context.report({
|
||||
node: hook,
|
||||
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. ' +
|
||||
'React Hooks must be called in the exact same order in ' +
|
||||
'every component render.',
|
||||
@ -498,7 +506,7 @@ const rule: Rule.RuleModule = {
|
||||
context.report({
|
||||
node: hook,
|
||||
message:
|
||||
`React Hook "${context.getSource(hook)}" cannot be ` +
|
||||
`React Hook "${getSource(hook)}" cannot be ` +
|
||||
'called in an async function.',
|
||||
});
|
||||
}
|
||||
@ -513,7 +521,7 @@ const rule: Rule.RuleModule = {
|
||||
!isUseIdentifier(hook) // `use(...)` can be called conditionally.
|
||||
) {
|
||||
const message =
|
||||
`React Hook "${context.getSource(hook)}" is called ` +
|
||||
`React Hook "${getSource(hook)}" is called ` +
|
||||
'conditionally. React Hooks must be called in the exact ' +
|
||||
'same order in every component render.' +
|
||||
(possiblyHasEarlyReturn
|
||||
@ -530,15 +538,15 @@ const rule: Rule.RuleModule = {
|
||||
) {
|
||||
// Custom message for hooks inside a class
|
||||
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 ' +
|
||||
'React function component or a custom React Hook function.';
|
||||
context.report({ node: hook, message });
|
||||
} else if (codePathFunctionName) {
|
||||
// Custom message if we found an invalid function name.
|
||||
const message =
|
||||
`React Hook "${context.getSource(hook)}" is called in ` +
|
||||
`function "${context.getSource(codePathFunctionName)}" ` +
|
||||
`React Hook "${getSource(hook)}" is called in ` +
|
||||
`function "${getSource(codePathFunctionName)}" ` +
|
||||
'that is neither a React function component nor a custom ' +
|
||||
'React Hook function.' +
|
||||
' React component names must start with an uppercase letter.' +
|
||||
@ -547,7 +555,7 @@ const rule: Rule.RuleModule = {
|
||||
} else if (codePathNode.type === 'Program') {
|
||||
// These are dangerous if you have inline requires enabled.
|
||||
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 ' +
|
||||
'React function component or a custom React Hook function.';
|
||||
context.report({ node: hook, message });
|
||||
@ -560,7 +568,7 @@ const rule: Rule.RuleModule = {
|
||||
// `use(...)` can be called in callbacks.
|
||||
if (isSomewhereInsideComponentOrHook && !isUseIdentifier(hook)) {
|
||||
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 ' +
|
||||
'React function component or a custom React Hook function.';
|
||||
context.report({ node: hook, message });
|
||||
@ -612,7 +620,7 @@ const rule: Rule.RuleModule = {
|
||||
context.report({
|
||||
node,
|
||||
message:
|
||||
`\`${context.getSource(
|
||||
`\`${getSource(
|
||||
node,
|
||||
)}\` 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.',
|
||||
@ -629,14 +637,14 @@ const rule: Rule.RuleModule = {
|
||||
FunctionDeclaration(node) {
|
||||
// function MyComponent() { const onClick = useEffectEvent(...) }
|
||||
if (isInsideComponentOrHook(node)) {
|
||||
recordAllUseEffectEventFunctions(context.getScope());
|
||||
recordAllUseEffectEventFunctions(getScope(node));
|
||||
}
|
||||
},
|
||||
|
||||
ArrowFunctionExpression(node) {
|
||||
// const MyComponent = () => { const onClick = useEffectEvent(...) }
|
||||
if (isInsideComponentOrHook(node)) {
|
||||
recordAllUseEffectEventFunctions(context.getScope());
|
||||
recordAllUseEffectEventFunctions(getScope(node));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -4,23 +4,30 @@
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* 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 { name, version } from './package.json';
|
||||
import RulesOfHooks from './RulesOfHooks';
|
||||
|
||||
export const __EXPERIMENTAL__ = false;
|
||||
|
||||
export const configs = {
|
||||
export const flatConfigs = {
|
||||
recommended: {
|
||||
plugins: ['react-hooks'],
|
||||
name: 'react-hooks/recommended',
|
||||
plugins: {
|
||||
'react-hooks': {
|
||||
meta: { name, version },
|
||||
rules: {
|
||||
'rules-of-hooks': RulesOfHooks,
|
||||
'exhaustive-deps': ExhaustiveDeps,
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
},
|
||||
} as Linter.BaseConfig,
|
||||
};
|
||||
|
||||
export const rules = {
|
||||
'rules-of-hooks': RulesOfHooks,
|
||||
'exhaustive-deps': ExhaustiveDeps,
|
||||
} satisfies Linter.Config,
|
||||
};
|
||||
|
@ -1,6 +1,9 @@
|
||||
{
|
||||
"name": "eslint-plugin-react-hooks",
|
||||
"version": "4.2.0",
|
||||
"upstream": {
|
||||
"version": 1,
|
||||
"comment": "https://github.com/facebook/react/pull/30774",
|
||||
"sources": {
|
||||
"main": {
|
||||
"repository": "git@github.com:facebook/react.git",
|
||||
|
@ -1,10 +0,0 @@
|
||||
diff --git a/tsconfig.json b/tsconfig.json
|
||||
index 39c6900..6fd8822 100644
|
||||
--- a/tsconfig.json
|
||||
+++ b/tsconfig.json
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
- "extends": "@1stg/tsconfig/node16",
|
||||
"compilerOptions": {
|
||||
"module": "Node16",
|
||||
"outDir": "./lib",
|
@ -1,5 +1,5 @@
|
||||
diff --git a/src/index.js b/src/index.js
|
||||
index 2fa185f..29d65d0 100644
|
||||
index 2fa185f..3cf8018 100644
|
||||
--- a/src/index.js
|
||||
+++ b/src/index.js
|
||||
@@ -1,48 +1,90 @@
|
||||
@ -135,7 +135,13 @@ index 2fa185f..29d65d0 100644
|
||||
};
|
||||
|
||||
const recommendedRules = {
|
||||
@@ -299,10 +341,10 @@ const jsxA11y = {
|
||||
@@ -294,15 +336,15 @@ const jsxA11y = {
|
||||
* Given a ruleset and optionally a flat config name, generate a config.
|
||||
* @param {object} rules - ruleset for this config
|
||||
* @param {string} flatConfigName - name for the config if flat
|
||||
- * @returns Config for this set of rules.
|
||||
+ * @returns {import('eslint').Linter.Config} Config for this set of rules.
|
||||
*/
|
||||
const createConfig = (rules, flatConfigName) => ({
|
||||
...(flatConfigName
|
||||
? {
|
||||
@ -150,6 +156,55 @@ index 2fa185f..29d65d0 100644
|
||||
: { ...legacyConfigBase, plugins: ['jsx-a11y'] }),
|
||||
rules: { ...rules },
|
||||
});
|
||||
@@ -317,4 +359,4 @@ const flatConfigs = {
|
||||
strict: createConfig(strictRules, 'strict'),
|
||||
};
|
||||
|
||||
-module.exports = { ...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/util/mayContainChildComponent.js b/src/util/mayContainChildComponent.js
|
||||
index 43a03ef..5e1035e 100644
|
||||
--- a/src/util/mayContainChildComponent.js
|
||||
@ -163,3 +218,16 @@ index 43a03ef..5e1035e 100644
|
||||
|
||||
export default function mayContainChildComponent(
|
||||
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;
|
||||
|
13
patches/@typescript-eslint__utils@8.2.0.patch
Normal file
13
patches/@typescript-eslint__utils@8.2.0.patch
Normal 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;
|
||||
}
|
11
patches/dts-bundle-generator.patch
Normal file
11
patches/dts-bundle-generator.patch
Normal 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;
|
3993
pnpm-lock.yaml
generated
3993
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,31 +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/custom'))
|
||||
.filter(file => file.endsWith('.ts'))
|
||||
.filter(file => file !== 'index.ts')
|
||||
.map(file => file.slice(0, -3));
|
||||
|
||||
const entryFile = /* js */ `
|
||||
import type { ESLintUtils } from '@typescript-eslint/utils';
|
||||
import type { Rule } from 'eslint';
|
||||
|
||||
${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/custom/index.ts', entryFile + '\n');
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
void buildLocalRules();
|
||||
}
|
@ -5,19 +5,20 @@ import { relative, resolve } from 'node:path';
|
||||
|
||||
import esbuild from 'esbuild';
|
||||
import type { Plugin } from 'esbuild';
|
||||
import { memoize } from 'lodash';
|
||||
import { gray, green } from 'picocolors';
|
||||
import { memoize } from 'lodash-es';
|
||||
import c from 'picocolors';
|
||||
import { minify_sync } from 'terser';
|
||||
|
||||
import { dependencies } from '../dist/package.json';
|
||||
|
||||
import { buildLocalRules } from './build-local-rules';
|
||||
import { dts } from './dts';
|
||||
import { babelPlugin } from './modifier';
|
||||
|
||||
const ENV = (process.env.NODE_ENV ??= 'production');
|
||||
const PROD = ENV === 'production';
|
||||
|
||||
const { gray, green } = c;
|
||||
|
||||
declare global {
|
||||
interface Array<T> {
|
||||
filter(
|
||||
@ -71,10 +72,7 @@ if (process.env.DEBUG) {
|
||||
|
||||
async function bundle(
|
||||
entry: string,
|
||||
outfile = entry
|
||||
.replace('./packages/', './dist/')
|
||||
.replace('src/', '')
|
||||
.replace('.ts', '.js'),
|
||||
outfile: string,
|
||||
options?: esbuild.BuildOptions & { treeShaking?: boolean },
|
||||
) {
|
||||
const output = await esbuild.build({
|
||||
@ -140,25 +138,31 @@ async function editPackageJson() {
|
||||
}
|
||||
|
||||
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);
|
||||
return [state, setState] as const;
|
||||
}
|
||||
|
||||
function bundleType(source: string, output: string) {
|
||||
return dts({
|
||||
source,
|
||||
dist: output,
|
||||
project: './tsconfig.build.json',
|
||||
});
|
||||
try {
|
||||
return dts({
|
||||
source,
|
||||
dist: output,
|
||||
project: './tsconfig.build.json',
|
||||
});
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Building local rules…');
|
||||
await buildLocalRules();
|
||||
|
||||
console.log('Building type definitions…');
|
||||
bundleType('./src/index.ts', './dist/index.d.ts');
|
||||
try {
|
||||
await fs.rm('dist/config', { recursive: true });
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
|
||||
bundleType('./src/prettier.ts', './dist/prettier.d.ts');
|
||||
bundleType('./src/types.ts', './dist/types.d.ts');
|
||||
|
||||
@ -166,13 +170,12 @@ async function main() {
|
||||
|
||||
console.log('Building packages…');
|
||||
await Promise.all([
|
||||
bundle('./packages/eslint-plugin-jsx-a11y/src/index.js'),
|
||||
bundle('./packages/eslint-plugin-react-hooks/index.ts'),
|
||||
bundle('./packages/eslint-plugin-n/lib/index.js', './dist/eslint-plugin-n/index.js'),
|
||||
bundle('./packages/eslint-import-resolver-typescript/src/index.ts'),
|
||||
bundle('./src/custom/index.ts', './dist/eslint-plugin-custom/index.js'),
|
||||
bundle('./src/local/index.ts', './dist/eslint-plugin-local/index.js'),
|
||||
bundle('./src/index.ts', './dist/index.js', unminify),
|
||||
bundle('./src/index.ts', undefined!, {
|
||||
format: 'esm',
|
||||
splitting: true,
|
||||
outdir: './dist/config',
|
||||
...unminify,
|
||||
}),
|
||||
bundle('./src/types.ts', './dist/types.js', unminify),
|
||||
bundle('./src/prettier.ts', './dist/prettier.js', unminify),
|
||||
bundle('./src/install.ts', './dist/install.js', {
|
||||
@ -185,9 +188,8 @@ async function main() {
|
||||
editPackageJson(),
|
||||
]);
|
||||
|
||||
console.log('Removing redirect…');
|
||||
const [distIndex, setDistIndex] = await useText('./dist/index.js');
|
||||
await setDistIndex(distIndex.replace(/import.*redirect.*;/g, ''));
|
||||
// bundleType('./src/index.ts', './dist/config/index.d.ts');
|
||||
await fs.copyFile('./src/config.d.ts', './dist/config/index.d.ts');
|
||||
}
|
||||
|
||||
void main();
|
||||
|
@ -3,8 +3,7 @@ import fs from 'node:fs';
|
||||
import { builtinModules } from 'node:module';
|
||||
|
||||
import glob from 'fast-glob';
|
||||
|
||||
import { uniq } from 'lodash';
|
||||
import { uniq } from 'lodash-es';
|
||||
|
||||
import { dependencies, peerDependencies } from '../dist/package.json';
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
#!/usr/bin/env node
|
||||
import * as ts from 'typescript';
|
||||
import {
|
||||
type EntryPointConfig,
|
||||
generateDtsBundle,
|
||||
} from 'dts-bundle-generator/dist/bundle-generator';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
export function dts({
|
||||
source,
|
||||
|
@ -1,11 +1,14 @@
|
||||
#!/usr/bin/env tsx
|
||||
import assert from 'node:assert';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve, extname } from 'node:path';
|
||||
import type { Loader, Plugin } from 'esbuild';
|
||||
import { extname, resolve } from 'node:path';
|
||||
|
||||
import * as babel from '@babel/core';
|
||||
import type { types as t, types } from '@babel/core';
|
||||
import { createMacro, type MacroHandler } from 'babel-plugin-macros';
|
||||
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);
|
||||
@ -15,7 +18,7 @@ class HandlerMap {
|
||||
|
||||
set(names: string | string[], handler: MacroHandler) {
|
||||
names = Array.isArray(names) ? names : [names];
|
||||
const macro = createMacro(handler);
|
||||
const macro = babelMacros.createMacro(handler);
|
||||
for (const name of names) {
|
||||
this.map.set(name, macro);
|
||||
}
|
||||
@ -23,7 +26,7 @@ class HandlerMap {
|
||||
}
|
||||
|
||||
get keys() {
|
||||
return Array.from(this.map.keys());
|
||||
return [...this.map.keys()];
|
||||
}
|
||||
|
||||
resolvePath = (module: string) => module;
|
||||
@ -96,14 +99,14 @@ const map = new HandlerMap()
|
||||
'object.groupby',
|
||||
replace(t =>
|
||||
t.memberExpression(
|
||||
t.callExpression(t.identifier('require'), [t.stringLiteral('lodash')]),
|
||||
t.callExpression(t.identifier('require'), [t.stringLiteral('lodash-es')]),
|
||||
t.identifier('groupBy'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// es-iterator-helpers/Iterator.prototype.*
|
||||
const polyfillPath = resolve(__dirname, '../src/polyfill.ts');
|
||||
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)]),
|
||||
@ -127,15 +130,15 @@ map.set(
|
||||
|
||||
function replace(getReplacement: (types: typeof t) => t.Expression): MacroHandler {
|
||||
return ({ references, babel: { types: t } }) => {
|
||||
references.default.forEach(referencePath => {
|
||||
for (const referencePath of references.default) {
|
||||
referencePath.replaceWith(getReplacement(t));
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function proto(getProperty: (types: typeof t) => t.Expression): MacroHandler {
|
||||
return ({ references, babel: { types: t } }) => {
|
||||
references.default.forEach(referencePath => {
|
||||
for (const referencePath of references.default) {
|
||||
const { parent, parentPath } = referencePath;
|
||||
assert(t.isCallExpression(parent));
|
||||
const [callee, ...rest] = parent.arguments;
|
||||
@ -145,7 +148,7 @@ function proto(getProperty: (types: typeof t) => t.Expression): MacroHandler {
|
||||
rest,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -160,21 +163,14 @@ export const babelPlugin: Plugin = {
|
||||
return null;
|
||||
}
|
||||
|
||||
let source = readFileSync(path, 'utf-8')
|
||||
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(require('prop-types'))),
|
||||
JSON.stringify(Object.keys(PropTypes)),
|
||||
);
|
||||
|
||||
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;
|
||||
|
||||
|
@ -3,7 +3,6 @@ sync() (
|
||||
cd "packages/$1" && git diff HEAD > "../../patch/$1.patch"
|
||||
)
|
||||
|
||||
sync eslint-import-resolver-typescript
|
||||
sync eslint-plugin-jsx-a11y
|
||||
sync eslint-plugin-n
|
||||
sync jsx-ast-utils
|
||||
|
@ -6,22 +6,22 @@
|
||||
"subject": "[meta] add `repository.directory` field"
|
||||
},
|
||||
"eslint-import-resolver-typescript": {
|
||||
"hash": "3dfad602a05b4b3812a4d3fc681051932f86e838",
|
||||
"date": "2024-08-01T01:15:59+00:00",
|
||||
"hash": "5ee5879b4428f42edbc262d66e192c76202b7f47",
|
||||
"date": "2024-10-01T03:12:28+00:00",
|
||||
"committer": "GitHub",
|
||||
"subject": "chore(deps): update dependency node to v18.20.4 (#309)"
|
||||
"subject": "fix(deps): update dependency debug to ^4.3.7 (#316)"
|
||||
},
|
||||
"eslint-plugin-jsx-a11y": {
|
||||
"hash": "a08fbcc502d6a6fa7d01a48c5f0b895c61e8cdd5",
|
||||
"date": "2024-08-22T20:21:57+01:00",
|
||||
"hash": "4925ba8d0bf80a4b1d8e8645d310590bf1b40b64",
|
||||
"date": "2024-09-20T14:09:27-07:00",
|
||||
"committer": "Jordan Harband",
|
||||
"subject": "[Fix] `label-has-associated-control`: ignore undetermined label text"
|
||||
"subject": "[Fix] handle interactive/noninteractive changes from aria-query"
|
||||
},
|
||||
"eslint-plugin-n": {
|
||||
"hash": "e5e758ea0cd238220127ae7bcbd967f1d8920f28",
|
||||
"date": "2024-08-06T04:22:42+12:00",
|
||||
"hash": "23d0e846e9dbfb68ccf7f410a83457514d432263",
|
||||
"date": "2024-10-09T13:49:20+02:00",
|
||||
"committer": "GitHub",
|
||||
"subject": "docs(process-exit-as-throw): update wording (#323)"
|
||||
"subject": "chore(master): release 17.11.1 (#352)"
|
||||
},
|
||||
"eslint-plugin-react": {
|
||||
"hash": "983b88dd3cb5e07919517d3fde4085f60883ded7",
|
||||
|
44
src/config.d.ts
vendored
Normal file
44
src/config.d.ts
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
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 }>);
|
||||
|
||||
/**
|
||||
* 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)
|
||||
*
|
||||
* @param of Configuration options.
|
||||
* @returns ESLint configuration object.
|
||||
*/
|
||||
export function extendConfig({
|
||||
auto,
|
||||
middlewares: addMiddlewares,
|
||||
configs,
|
||||
}: {
|
||||
auto?: boolean;
|
||||
middlewares?: Middleware[];
|
||||
configs: FlatESLintConfig[];
|
||||
}): Promise<FlatESLintConfig[]>;
|
||||
|
||||
export const error = 'error';
|
||||
export const warn = 'warn';
|
||||
export const off = 'off';
|
@ -1,17 +1,40 @@
|
||||
import type { ESLintUtils } from '@typescript-eslint/utils';
|
||||
import type { Rule } from 'eslint';
|
||||
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 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,
|
||||
'no-useless-import-alias': noUselessImportAlias,
|
||||
'restrict-template-expressions': restrictTemplateExpressions,
|
||||
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' */
|
||||
'custom/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-custom/restrict-template-expressions': RuleEntry<{ allow: string[] }>;
|
||||
/** Ban assignment of empty object literals `{}` and replace them with `Object.create(null)` */
|
||||
'custom/no-empty-object-literal': RuleEntry<unknown>;
|
||||
/** Ban useless import alias */
|
||||
'custom/no-useless-import-alias': RuleEntry<unknown>;
|
||||
}
|
||||
|
||||
export const plugin: ESLint.Plugin = {
|
||||
name: 'custom',
|
||||
rules: {
|
||||
'no-empty-object-literal': noEmptyObjectLiteral,
|
||||
'no-import-dot': noImportDot,
|
||||
'no-useless-import-alias': noUselessImportAlias,
|
||||
},
|
||||
};
|
||||
|
||||
export const typedPlugin: ESLint.Plugin = {
|
||||
name: 'typed-custom',
|
||||
rules: {
|
||||
// @ts-expect-error type mismatch
|
||||
'restrict-template-expressions': restrictTemplateExpressions,
|
||||
},
|
||||
};
|
||||
|
@ -6,7 +6,12 @@ import {
|
||||
isTypeFlagSet,
|
||||
isTypeNeverType,
|
||||
} from '@typescript-eslint/type-utils';
|
||||
import { AST_NODE_TYPES, ESLintUtils, type TSESTree } from '@typescript-eslint/utils';
|
||||
import {
|
||||
AST_NODE_TYPES,
|
||||
ESLintUtils,
|
||||
ParserServicesWithTypeInformation,
|
||||
type TSESTree,
|
||||
} from '@typescript-eslint/utils';
|
||||
import { getParserServices } from '@typescript-eslint/utils/eslint-utils';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
@ -52,8 +57,14 @@ export default createRule<Option[], MessageId>({
|
||||
},
|
||||
defaultOptions: [defaultOption],
|
||||
create(context, [options]) {
|
||||
const services = getParserServices(context);
|
||||
if (!services.program) return {};
|
||||
let services: ParserServicesWithTypeInformation | undefined;
|
||||
try {
|
||||
services = getParserServices(context);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
if (!services?.program) return {};
|
||||
|
||||
const checker = services.program.getTypeChecker();
|
||||
const allowed = new Set(options.allow);
|
||||
|
@ -1,12 +1,13 @@
|
||||
import * as fs from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import type { Middleware } from './middleware';
|
||||
import { jsdoc } from './presets/jsdoc';
|
||||
import { reactQuery, storybook, vitest } from './presets/misc';
|
||||
import { react, reactRefresh } from './presets/react';
|
||||
import { tailwind } from './presets/tailwind';
|
||||
import { testingLibrary } from './presets/testing-library';
|
||||
|
||||
const jsdoc = () => import('./presets/jsdoc');
|
||||
const tailwind = () => import('./presets/tailwind');
|
||||
const testingLibrary = () => import('./presets/testing-library');
|
||||
|
||||
const middlewares = {
|
||||
react,
|
||||
@ -17,6 +18,8 @@ const middlewares = {
|
||||
testingLibrary,
|
||||
jsdoc,
|
||||
vitest,
|
||||
} satisfies {
|
||||
[key: string]: Middleware;
|
||||
};
|
||||
|
||||
export const envs: {
|
||||
@ -62,9 +65,13 @@ export const envs: {
|
||||
export function getProjectDependencies() {
|
||||
const rootDir = process.cwd();
|
||||
|
||||
const pkgJsonPath = resolve(rootDir, 'package.json');
|
||||
const pkgJsonPath = path.resolve(rootDir, 'package.json');
|
||||
const pkgJson = fs.existsSync(pkgJsonPath)
|
||||
? JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'))
|
||||
? (JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')) as {
|
||||
dependencies?: Record<string, string>;
|
||||
devDependencies?: Record<string, string>;
|
||||
peerDependencies?: Record<string, string>;
|
||||
})
|
||||
: {};
|
||||
|
||||
return new Set(
|
259
src/index.ts
259
src/index.ts
@ -1,191 +1,104 @@
|
||||
/// <reference path="./modules.d.ts" />
|
||||
import './redirect';
|
||||
import type { ESLintConfig, Extends, Plugin, Rules } from '@aet/eslint-define-config';
|
||||
import type { ESLintUtils } from '@typescript-eslint/utils';
|
||||
import type { Rule } from 'eslint';
|
||||
import { uniq } from 'lodash';
|
||||
import type { FlatESLintConfig } from '@aet/eslint-define-config';
|
||||
import * as tsParser from '@typescript-eslint/parser';
|
||||
import importPlugin from 'eslint-plugin-import-x';
|
||||
import { uniq } from 'lodash-es';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
import { off } from './constants';
|
||||
import { checkEnv } from './env';
|
||||
import type { Middleware, MiddlewareConfig, MiddlewareFunctions } from './middleware';
|
||||
import { custom } from './presets/custom';
|
||||
import { checkEnv } from './environment';
|
||||
import { Middleware } from './middleware';
|
||||
import { eslintRules } from './presets/eslint';
|
||||
import { stylistic } from './presets/stylistic';
|
||||
import { importTypeScript } from './presets/typescript';
|
||||
import { unicorn } from './presets/unicorn';
|
||||
|
||||
export { graphql } from './presets/graphql';
|
||||
export { jsdoc } from './presets/jsdoc';
|
||||
export { storybook } from './presets/misc';
|
||||
export { react, reactRefresh } from './presets/react';
|
||||
export { tailwind } from './presets/tailwind';
|
||||
import stylistic from './presets/stylistic';
|
||||
import { importRules, typescriptRules } from './presets/typescript';
|
||||
import unicorn from './presets/unicorn';
|
||||
|
||||
export { error, warn, off } from './constants';
|
||||
|
||||
declare global {
|
||||
interface Array<T> {
|
||||
filter(
|
||||
predicate: BooleanConstructor,
|
||||
): Exclude<T, null | undefined | false | '' | 0>[];
|
||||
}
|
||||
}
|
||||
|
||||
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>];
|
||||
|
||||
export interface LocalRuleOptions {
|
||||
/** Bans import from the specifier '.' and '..' and replaces it with '.+/index' */
|
||||
'custom/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)
|
||||
*/
|
||||
'custom/restrict-template-expressions': RuleEntry<{ allow: string[] }>;
|
||||
/** Ban assignment of empty object literals `{}` and replace them with `Object.create(null)` */
|
||||
'custom/no-empty-object-literal': RuleEntry<unknown>;
|
||||
/** Ban useless import alias */
|
||||
'custom/no-useless-import-alias': RuleEntry<unknown>;
|
||||
}
|
||||
|
||||
export type RuleOptions = Rules & Partial<LocalRuleOptions>;
|
||||
|
||||
export interface CustomRule {
|
||||
rule: () => Promise<{
|
||||
default: Rule.RuleModule | ESLintUtils.RuleModule<string, unknown[]>;
|
||||
}>;
|
||||
options?: RuleLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* ESLint Configuration.
|
||||
* @see [ESLint Configuration](https://eslint.org/docs/latest/user-guide/configuring/)
|
||||
*/
|
||||
export type InputConfig = Omit<ESLintConfig, 'rules'> & {
|
||||
/**
|
||||
* Rules.
|
||||
* @see [Rules](https://eslint.org/docs/latest/user-guide/configuring/rules)
|
||||
*/
|
||||
rules?: Partial<RuleOptions>;
|
||||
|
||||
/**
|
||||
* Glob pattern to find paths to custom rule files in JavaScript or TypeScript.
|
||||
* Note this must be a string literal or an array of string literals since
|
||||
* this is statically analyzed.
|
||||
*
|
||||
* Rules are prefixed with `custom/` and the file name is used as the rule name.
|
||||
*/
|
||||
customRuleFiles?: string | string[];
|
||||
|
||||
/**
|
||||
* Automatically detect project types, dependencies and deduct the plugins.
|
||||
* @default true
|
||||
*/
|
||||
export async function extendConfig({
|
||||
auto = true,
|
||||
middlewares: addMiddlewares = [],
|
||||
configs = [],
|
||||
}: {
|
||||
auto?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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)
|
||||
*
|
||||
* @param of Configuration options.
|
||||
* @returns ESLint configuration object.
|
||||
*/
|
||||
export function extendConfig(
|
||||
of: InputConfig & {
|
||||
middlewares?: Middleware[];
|
||||
} = {
|
||||
middlewares: [],
|
||||
},
|
||||
): ESLintConfig {
|
||||
const {
|
||||
auto = true,
|
||||
plugins: _plugins = [],
|
||||
settings = {},
|
||||
rules,
|
||||
extends: _extends,
|
||||
overrides,
|
||||
customRuleFiles,
|
||||
parserOptions,
|
||||
middlewares: _middlewares = [],
|
||||
...rest
|
||||
} = of;
|
||||
|
||||
const plugins: Plugin[] = [..._plugins];
|
||||
const extend: Extends[] = ensureArray(_extends);
|
||||
|
||||
if (customRuleFiles != null) {
|
||||
plugins.push('local');
|
||||
}
|
||||
|
||||
middlewares?: Middleware[];
|
||||
configs: FlatESLintConfig[];
|
||||
}): Promise<FlatESLintConfig[]> {
|
||||
const middlewares: Middleware[] = uniq([
|
||||
importTypeScript,
|
||||
unicorn,
|
||||
custom,
|
||||
stylistic,
|
||||
() => import('./presets/custom'),
|
||||
...(auto ? checkEnv() : []),
|
||||
..._middlewares,
|
||||
...addMiddlewares,
|
||||
]);
|
||||
|
||||
const result: MiddlewareConfig = {
|
||||
root: true,
|
||||
plugins: unique('custom', plugins),
|
||||
env: { node: true, browser: true, es2023: true },
|
||||
reportUnusedDisableDirectives: true,
|
||||
parserOptions: {
|
||||
project: true,
|
||||
...parserOptions,
|
||||
const result: FlatESLintConfig[] = [
|
||||
{ rules: eslintRules }, //
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
importPlugin.flatConfigs.recommended,
|
||||
importPlugin.flatConfigs.react,
|
||||
importPlugin.flatConfigs.typescript,
|
||||
...unicorn,
|
||||
stylistic,
|
||||
{
|
||||
files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: tsParser,
|
||||
projectService: true,
|
||||
ecmaVersion: 'latest',
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
'import-x/parsers': {
|
||||
'@typescript-eslint/parser': ['.ts', '.tsx', '.mts', '.cts'],
|
||||
},
|
||||
'import-x/resolver': {
|
||||
typescript: true,
|
||||
node: true,
|
||||
},
|
||||
'import-x/core-modules': ['node:sqlite'],
|
||||
},
|
||||
ignores: ['eslint.config.cjs'],
|
||||
rules: {
|
||||
...importRules,
|
||||
...typescriptRules,
|
||||
},
|
||||
},
|
||||
ignorePatterns: [],
|
||||
globals: {},
|
||||
extends: ['eslint:recommended', 'prettier', ...(extend as string[])],
|
||||
settings,
|
||||
overrides: [
|
||||
{ files: ['repl.ts', 'scripts/**/*.ts'], rules: { 'no-console': off } },
|
||||
...(overrides ?? []),
|
||||
],
|
||||
rules: { ...eslintRules },
|
||||
...rest,
|
||||
};
|
||||
{
|
||||
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,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.d.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/consistent-type-imports': off,
|
||||
'import-x/unambiguous': off,
|
||||
},
|
||||
},
|
||||
] as FlatESLintConfig[];
|
||||
|
||||
const functions: MiddlewareFunctions = {
|
||||
addRules(newRules) {
|
||||
Object.assign(result.rules, newRules);
|
||||
},
|
||||
addSettings(newSettings) {
|
||||
Object.assign(result.settings, newSettings);
|
||||
},
|
||||
};
|
||||
|
||||
for (const fn of middlewares) {
|
||||
fn(result, functions);
|
||||
for (const middleware of middlewares) {
|
||||
let fn = await middleware();
|
||||
if ('default' in fn) {
|
||||
fn = fn.default;
|
||||
}
|
||||
if (Array.isArray(fn)) {
|
||||
result.push(...(fn as FlatESLintConfig[]).flat(Infinity));
|
||||
} else {
|
||||
result.push(fn as unknown as FlatESLintConfig);
|
||||
}
|
||||
}
|
||||
|
||||
result.plugins = unique(result.plugins);
|
||||
result.extends = unique(result.extends);
|
||||
|
||||
Object.assign(result.rules, rules);
|
||||
if (configs) {
|
||||
result.push(...configs);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { installPackage } from '@antfu/install-pkg';
|
||||
import { uniq } from 'lodash';
|
||||
import { uniq } from 'lodash-es';
|
||||
|
||||
import { envs, getProjectDependencies } from './env';
|
||||
import { envs, getProjectDependencies } from './environment';
|
||||
|
||||
const deps = getProjectDependencies();
|
||||
const packages = uniq(
|
||||
|
@ -1,83 +0,0 @@
|
||||
import * as fs from 'node:fs';
|
||||
import { basename, extname, isAbsolute, resolve } from 'node:path';
|
||||
|
||||
import type { ESLint } from 'eslint';
|
||||
import { parseModule } from 'esprima';
|
||||
import query from 'esquery';
|
||||
import type { Node, Property } from 'estree';
|
||||
import { glob } from 'fast-glob';
|
||||
|
||||
// https://github.com/gulpjs/interpret
|
||||
const transpilers = [
|
||||
'esbuild-register',
|
||||
'tsx',
|
||||
'ts-node/register/transpile-only',
|
||||
'@swc/register',
|
||||
'sucrase/register',
|
||||
'@babel/register',
|
||||
'coffeescript/register',
|
||||
];
|
||||
|
||||
function tryRequire() {
|
||||
for (const candidate of transpilers) {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
require(candidate);
|
||||
return;
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
const unwrapDefault = <T = any>(module: any): T => module.default ?? module;
|
||||
|
||||
const plugin: ESLint.Plugin = {
|
||||
rules: {},
|
||||
};
|
||||
|
||||
function hydrateESTreeNode(n: Node): any {
|
||||
switch (n.type) {
|
||||
case 'Literal':
|
||||
return n.value;
|
||||
case 'ArrayExpression':
|
||||
return n.elements.filter(Boolean).map(hydrateESTreeNode);
|
||||
default:
|
||||
throw new Error(`Unsupported node type: ${n.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
function parseConfigFile(js: string) {
|
||||
const [node] = query(
|
||||
parseModule(js),
|
||||
'CallExpression[callee.name="extendConfig"] > ObjectExpression > Property[key.name="customRuleFiles"]',
|
||||
);
|
||||
return hydrateESTreeNode((node as Property).value);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const rootDir = process.cwd();
|
||||
|
||||
const eslintConfigFile = ['.eslintrc.js', '.eslintrc.cjs']
|
||||
.map(file => resolve(rootDir, file))
|
||||
.find(file => fs.existsSync(file));
|
||||
|
||||
if (!eslintConfigFile) return;
|
||||
|
||||
const eslintConfig = fs.readFileSync(eslintConfigFile, 'utf8');
|
||||
const customRuleFiles = parseConfigFile(eslintConfig);
|
||||
if (!customRuleFiles?.length) return;
|
||||
|
||||
tryRequire();
|
||||
for (let file of glob.sync(customRuleFiles)) {
|
||||
if (!isAbsolute(file)) {
|
||||
file = resolve(rootDir, file);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const module = unwrapDefault(require(file));
|
||||
const name = module.name ?? basename(file, extname(file));
|
||||
plugin.rules![name] = module;
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
export = plugin;
|
@ -1,31 +1,10 @@
|
||||
import type { ESLintConfig, KnownExtends, Settings } from '@aet/eslint-define-config';
|
||||
import type { Merge, SetRequired } from 'type-fest';
|
||||
import type { Linter } from 'eslint';
|
||||
|
||||
import type { RuleOptions } from './index';
|
||||
type MiddlewareResult = Linter.Config | Linter.Config[];
|
||||
|
||||
type OptionalObjectKey<T> = Exclude<
|
||||
{
|
||||
[Key in keyof T]: undefined | any[] extends T[Key]
|
||||
? Key
|
||||
: undefined | Record<any, any> extends T[Key]
|
||||
? Key
|
||||
: never;
|
||||
}[keyof T],
|
||||
undefined
|
||||
>;
|
||||
export type Middleware =
|
||||
| (() => Promise<MiddlewareResult>)
|
||||
| (() => Promise<{ default: MiddlewareResult }>);
|
||||
|
||||
export type MiddlewareConfig = Merge<
|
||||
SetRequired<ESLintConfig, OptionalObjectKey<ESLintConfig>>,
|
||||
{ extends: KnownExtends[] }
|
||||
>;
|
||||
|
||||
export interface MiddlewareFunctions {
|
||||
addRules(rules: Partial<RuleOptions>): void;
|
||||
addSettings(settings: Partial<Settings>): void;
|
||||
}
|
||||
|
||||
export type Middleware = (config: MiddlewareConfig, helpers: MiddlewareFunctions) => void;
|
||||
|
||||
export function defineMiddleware(middleware: Middleware): Middleware {
|
||||
return middleware;
|
||||
}
|
||||
// eslint-disable-next-line unicorn/prevent-abbreviations
|
||||
export const def = <T>(module: { default: T }): T => module.default;
|
||||
|
32
src/modules.d.ts
vendored
32
src/modules.d.ts
vendored
@ -1,25 +1,4 @@
|
||||
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';
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import-x/unambiguous
|
||||
declare module 'module' {
|
||||
export function _resolveFilename(
|
||||
request: string,
|
||||
@ -34,3 +13,12 @@ declare module 'module' {
|
||||
options?: Record<PropertyKey, unknown>,
|
||||
): string;
|
||||
}
|
||||
|
||||
declare module 'eslint-plugin-storybook' {
|
||||
import type { Linter } from 'eslint';
|
||||
|
||||
export const configs: {
|
||||
csf: Linter.Config;
|
||||
recommended: Linter.Config;
|
||||
};
|
||||
}
|
||||
|
@ -1,18 +1,20 @@
|
||||
import { error } from '../constants';
|
||||
import type { LocalRuleOptions } from '../index';
|
||||
import { defineMiddleware } from '../middleware';
|
||||
import { plugin, typedPlugin, LocalRuleOptions } from '../custom/index';
|
||||
import { defineConfig } from '../types';
|
||||
|
||||
const customRules: Partial<LocalRuleOptions> = {
|
||||
'custom/no-import-dot': error,
|
||||
'custom/no-useless-import-alias': error,
|
||||
};
|
||||
|
||||
export const custom = defineMiddleware((config, { addRules }) => {
|
||||
addRules(customRules);
|
||||
config.overrides.push({
|
||||
export default defineConfig([
|
||||
{
|
||||
plugins: { custom: plugin },
|
||||
rules: {
|
||||
'custom/no-import-dot': error,
|
||||
'custom/no-useless-import-alias': error,
|
||||
} satisfies Partial<LocalRuleOptions>,
|
||||
},
|
||||
{
|
||||
plugins: { 'typed-custom': typedPlugin },
|
||||
files: ['*.ts', '!*.d.ts'],
|
||||
rules: {
|
||||
'custom/restrict-template-expressions': error,
|
||||
},
|
||||
});
|
||||
});
|
||||
'typed-custom/restrict-template-expressions': error,
|
||||
} satisfies Partial<LocalRuleOptions>,
|
||||
},
|
||||
]);
|
||||
|
@ -1,12 +1,13 @@
|
||||
// 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 { defineMiddleware } from '../middleware';
|
||||
import { defineConfig } from '../types';
|
||||
|
||||
// https://the-guild.dev/graphql/eslint/rules
|
||||
const graphqlRules: Partial<GraphQLRulesObject> = {};
|
||||
|
||||
export const graphql = defineMiddleware((config, { addRules }) => {
|
||||
config.plugins.push('@graphql-eslint');
|
||||
config.extends.push('plugin:@graphql-eslint/recommended');
|
||||
addRules(graphqlRules);
|
||||
export default defineConfig({
|
||||
processor: graphql.processors.graphql,
|
||||
rules: graphqlRules,
|
||||
});
|
||||
|
@ -1,14 +1,14 @@
|
||||
import type { JSDocRulesObject } from '@aet/eslint-define-config/src/rules/jsdoc';
|
||||
import module from 'eslint-plugin-jsdoc';
|
||||
|
||||
import { off } from '../constants';
|
||||
import { defineMiddleware } from '../middleware';
|
||||
import { defineConfig } from '../types';
|
||||
|
||||
const jsdocRules: Partial<JSDocRulesObject> = {
|
||||
'jsdoc/require-jsdoc': off,
|
||||
};
|
||||
|
||||
export const jsdoc = defineMiddleware((config, { addRules }) => {
|
||||
config.plugins.push('jsdoc');
|
||||
config.extends.push('plugin:jsdoc/recommended-typescript');
|
||||
addRules(jsdocRules);
|
||||
});
|
||||
export default defineConfig([
|
||||
module.configs['flat/recommended-typescript'],
|
||||
{ rules: jsdocRules },
|
||||
]);
|
||||
|
@ -1,13 +1,17 @@
|
||||
import { defineMiddleware } from '../middleware';
|
||||
import { def } from '../middleware';
|
||||
import { defineConfig } from '../types';
|
||||
|
||||
export const storybook = defineMiddleware(config => {
|
||||
config.extends.push('plugin:storybook/recommended');
|
||||
});
|
||||
export async function storybook() {
|
||||
const { configs } = await import('eslint-plugin-storybook');
|
||||
return defineConfig([configs.recommended]);
|
||||
}
|
||||
|
||||
export const reactQuery = defineMiddleware(config => {
|
||||
config.extends.push('plugin:@tanstack/eslint-plugin-query/recommended');
|
||||
});
|
||||
export async function reactQuery() {
|
||||
const { configs } = def(await import('@tanstack/eslint-plugin-query'));
|
||||
return defineConfig(configs['flat/recommended']);
|
||||
}
|
||||
|
||||
export const vitest = defineMiddleware(config => {
|
||||
config.extends.push('plugin:vitest/recommended');
|
||||
});
|
||||
export async function vitest() {
|
||||
const { configs } = def(await import('eslint-plugin-vitest'));
|
||||
return defineConfig([configs.recommended]);
|
||||
}
|
||||
|
@ -2,35 +2,42 @@ import type { ReactRulesObject } from '@aet/eslint-define-config/src/rules/react
|
||||
import type { ReactRefreshRulesObject } from '@aet/eslint-define-config/src/rules/react-refresh';
|
||||
|
||||
import { error, off, warn } from '../constants';
|
||||
import { defineMiddleware } from '../middleware';
|
||||
import { def } from '../middleware';
|
||||
import { defineConfig } from '../types';
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
export const react = defineMiddleware((config, { addRules }) => {
|
||||
config.plugins.push('@eslint-react/eslint-plugin', 'react-hooks');
|
||||
config.extends.push(
|
||||
'plugin:@eslint-react/recommended-legacy',
|
||||
'plugin:@eslint-react/dom-legacy',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:jsx-a11y/recommended',
|
||||
);
|
||||
config.overrides.push({
|
||||
files: ['*.tsx'],
|
||||
rules: {
|
||||
'@eslint-react/no-leaked-conditional-rendering': error,
|
||||
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 any,
|
||||
hooks.flatConfigs.recommended,
|
||||
a11y.flatConfigs.recommended,
|
||||
{
|
||||
files: ['*.tsx'],
|
||||
rules: reactRules,
|
||||
},
|
||||
});
|
||||
addRules(reactRules);
|
||||
});
|
||||
]);
|
||||
}
|
||||
|
||||
const refreshRules: Partial<ReactRefreshRulesObject> = {
|
||||
'react-refresh/only-export-components': [warn, { allowConstantExport: true }],
|
||||
};
|
||||
|
||||
export const reactRefresh = defineMiddleware((config, { addRules }) => {
|
||||
config.plugins.push('react-refresh');
|
||||
addRules(refreshRules);
|
||||
});
|
||||
export async function reactRefresh() {
|
||||
const refreshPlugin = def(await import('eslint-plugin-react-refresh'));
|
||||
return defineConfig({
|
||||
plugins: {
|
||||
// @ts-expect-error no types
|
||||
'react-refresh': refreshPlugin,
|
||||
},
|
||||
rules: refreshRules,
|
||||
});
|
||||
}
|
||||
|
@ -1,21 +1,25 @@
|
||||
import type { StylisticRulesObject } from '@aet/eslint-define-config/src/rules/stylistic';
|
||||
import stylistic from '@stylistic/eslint-plugin';
|
||||
|
||||
import { error } from '../constants';
|
||||
import { defineMiddleware } from '../middleware';
|
||||
import { defineConfig } from '../types';
|
||||
|
||||
const stylisticRules: Partial<StylisticRulesObject> = {
|
||||
'@stylistic/spaced-comment': [
|
||||
'stylistic/spaced-comment': [
|
||||
error,
|
||||
'always',
|
||||
{
|
||||
markers: ['/', '#', '@'],
|
||||
// allow /*@__PURE__*/
|
||||
block: { exceptions: ['@'] },
|
||||
},
|
||||
// allow /*@__PURE__*/
|
||||
{ markers: ['/', '#', '@'], block: { exceptions: ['@'] } },
|
||||
],
|
||||
'stylistic/jsx-sort-props': [
|
||||
error,
|
||||
{ callbacksLast: true, shorthandFirst: true, multiline: 'last' },
|
||||
],
|
||||
};
|
||||
|
||||
export const stylistic = defineMiddleware((config, { addRules }) => {
|
||||
config.plugins.push('@stylistic');
|
||||
addRules(stylisticRules);
|
||||
export default defineConfig({
|
||||
plugins: {
|
||||
stylistic,
|
||||
},
|
||||
rules: stylisticRules,
|
||||
});
|
||||
|
@ -1,13 +1,14 @@
|
||||
import type { TailwindRulesObject } from '@aet/eslint-define-config/src/rules/tailwind';
|
||||
import tailwind from 'eslint-plugin-tailwindcss';
|
||||
|
||||
import { off } from '../constants';
|
||||
import { defineMiddleware } from '../middleware';
|
||||
import { defineConfig } from '../types';
|
||||
|
||||
const tailwindRules: Partial<TailwindRulesObject> = {
|
||||
'tailwindcss/no-custom-classname': off,
|
||||
} as const;
|
||||
|
||||
export const tailwind = defineMiddleware((config, { addRules }) => {
|
||||
config.extends.push('plugin:tailwindcss/recommended');
|
||||
addRules(tailwindRules);
|
||||
});
|
||||
export default defineConfig([
|
||||
...tailwind.configs['flat/recommended'],
|
||||
{ rules: tailwindRules },
|
||||
]);
|
||||
|
@ -1,13 +1,15 @@
|
||||
import type { TestingLibraryRulesObject } from '@aet/eslint-define-config/src/rules/testing-library';
|
||||
import testingLibrary from 'eslint-plugin-testing-library';
|
||||
|
||||
import { defineMiddleware } from '../middleware';
|
||||
import { defineConfig } from '../types';
|
||||
|
||||
const testingLibraryRules: Partial<TestingLibraryRulesObject> = {};
|
||||
|
||||
export const testingLibrary = defineMiddleware((config, { addRules }) => {
|
||||
config.overrides.push({
|
||||
files: ['**/*.(spec|test).{ts,tsx}'],
|
||||
plugins: ['plugin:testing-library/react'],
|
||||
});
|
||||
addRules(testingLibraryRules);
|
||||
export default defineConfig({
|
||||
files: ['**/*.(spec|test).{ts,tsx}'],
|
||||
...testingLibrary.configs['flat/react'],
|
||||
rules: {
|
||||
...testingLibrary.configs['flat/react'].rules,
|
||||
...testingLibraryRules,
|
||||
},
|
||||
});
|
||||
|
@ -2,9 +2,8 @@ import type { ImportXRulesObject } from '@aet/eslint-define-config/src/rules/imp
|
||||
import type { TypeScriptRulesObject } from '@aet/eslint-define-config/src/rules/typescript-eslint';
|
||||
|
||||
import { error, off, warn } from '../constants';
|
||||
import { defineMiddleware } from '../middleware';
|
||||
|
||||
const importRules: Partial<ImportXRulesObject> = {
|
||||
export const importRules: Partial<ImportXRulesObject> = {
|
||||
'import-x/first': error,
|
||||
'import-x/no-absolute-path': error,
|
||||
'import-x/no-duplicates': warn,
|
||||
@ -20,7 +19,7 @@ const importRules: Partial<ImportXRulesObject> = {
|
||||
'import-x/unambiguous': error,
|
||||
};
|
||||
|
||||
const typescriptRules: Partial<TypeScriptRulesObject> = {
|
||||
export const typescriptRules: Partial<TypeScriptRulesObject> = {
|
||||
'@typescript-eslint/ban-ts-comment': [
|
||||
error,
|
||||
{
|
||||
@ -38,6 +37,10 @@ const typescriptRules: Partial<TypeScriptRulesObject> = {
|
||||
warn,
|
||||
{ accessibility: 'no-public' },
|
||||
],
|
||||
'@typescript-eslint/no-empty-object-type': [
|
||||
error,
|
||||
{ allowInterfaces: 'with-single-extends' },
|
||||
],
|
||||
'@typescript-eslint/no-empty-interface': [error, { allowSingleExtends: true }],
|
||||
'@typescript-eslint/no-explicit-any': off,
|
||||
'@typescript-eslint/no-misused-promises': [error, { checksVoidReturn: false }],
|
||||
@ -55,42 +58,3 @@ const typescriptRules: Partial<TypeScriptRulesObject> = {
|
||||
'@typescript-eslint/triple-slash-reference': off,
|
||||
'@typescript-eslint/unbound-method': off,
|
||||
};
|
||||
|
||||
export const importTypeScript = defineMiddleware((config, { addRules, addSettings }) => {
|
||||
config.parser = '@typescript-eslint/parser';
|
||||
config.plugins.push('@typescript-eslint', 'import-x');
|
||||
config.extends.push(
|
||||
'plugin:@typescript-eslint/recommended-type-checked',
|
||||
'plugin:import-x/errors',
|
||||
'plugin:import-x/typescript',
|
||||
);
|
||||
addSettings({
|
||||
'import-x/parsers': {
|
||||
'@typescript-eslint/parser': ['.ts', '.tsx', '.mts', '.cts'],
|
||||
},
|
||||
'import-x/resolver': {
|
||||
typescript: true,
|
||||
},
|
||||
});
|
||||
config.overrides.push(
|
||||
{
|
||||
files: ['.eslintrc.js', '*.config.js', '*.cjs', '*.mjs'],
|
||||
extends: ['plugin:@typescript-eslint/disable-type-checked'],
|
||||
rules: {
|
||||
'import-x/no-commonjs': off,
|
||||
'import-x/unambiguous': off,
|
||||
'@typescript-eslint/no-require-imports': off,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.d.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/consistent-type-imports': off,
|
||||
'import-x/unambiguous': off,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
addRules(importRules);
|
||||
addRules(typescriptRules);
|
||||
});
|
||||
|
@ -1,8 +1,9 @@
|
||||
/* eslint-disable unicorn/string-content */
|
||||
import type { UnicornRulesObject } from '@aet/eslint-define-config/src/rules/unicorn';
|
||||
import unicorn from 'eslint-plugin-unicorn';
|
||||
import globals from 'globals';
|
||||
|
||||
import { error, off, warn } from '../constants';
|
||||
import { defineMiddleware } from '../middleware';
|
||||
import { defineConfig } from '../types';
|
||||
|
||||
const suggest = (suggest: string) => ({ suggest, fix: false });
|
||||
|
||||
@ -99,13 +100,31 @@ const unicornRules: Partial<UnicornRulesObject> = {
|
||||
'unicorn/template-indent': warn,
|
||||
};
|
||||
|
||||
export const unicorn = defineMiddleware((config, { addRules }) => {
|
||||
config.plugins.push('unicorn');
|
||||
addRules(unicornRules);
|
||||
config.overrides.push({
|
||||
// export const unicorn = defineMiddleware((config, { addRules }) => {
|
||||
// config.plugins.push('unicorn');
|
||||
// addRules(unicornRules);
|
||||
// config.overrides.push({
|
||||
// files: ['*.test.ts', '*.test.tsx'],
|
||||
// rules: {
|
||||
// 'unicorn/no-useless-undefined': off,
|
||||
// },
|
||||
// });
|
||||
// });
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
languageOptions: {
|
||||
globals: globals.builtin,
|
||||
},
|
||||
plugins: {
|
||||
unicorn,
|
||||
},
|
||||
rules: unicornRules,
|
||||
},
|
||||
{
|
||||
files: ['*.test.ts', '*.test.tsx'],
|
||||
rules: {
|
||||
'unicorn/no-useless-undefined': off,
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
]);
|
||||
|
@ -1,24 +0,0 @@
|
||||
import Module from 'node:module';
|
||||
const { name } = [require][0]('./package.json');
|
||||
|
||||
const _resolveFilename = Module._resolveFilename;
|
||||
const alias = new Set([
|
||||
'eslint-import-resolver-typescript',
|
||||
'eslint-plugin-jsx-a11y',
|
||||
'eslint-plugin-local',
|
||||
'eslint-plugin-n',
|
||||
'eslint-plugin-react-hooks',
|
||||
'eslint-plugin-custom',
|
||||
]);
|
||||
|
||||
type CDR<T> = T extends [any, ...infer R] ? R : [];
|
||||
|
||||
Module._resolveFilename = function (
|
||||
request: string,
|
||||
...args: CDR<Parameters<typeof _resolveFilename>>
|
||||
) {
|
||||
if (alias.has(request)) {
|
||||
request = `${name}/${request}`;
|
||||
}
|
||||
return _resolveFilename(request, ...args);
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
import type { Rule } from 'eslint';
|
||||
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[]>;
|
||||
@ -7,6 +7,13 @@ export function defineRules(rules: {
|
||||
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[]) {
|
||||
return config;
|
||||
}
|
||||
|
||||
export function defineRule({
|
||||
name,
|
||||
create,
|
||||
|
18
src/types/eslint-plugin-react-refresh.d.ts
vendored
Normal file
18
src/types/eslint-plugin-react-refresh.d.ts
vendored
Normal 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[];
|
||||
},
|
||||
]
|
||||
>;
|
||||
};
|
||||
}
|
55
src/types/eslint-plugin-testing-library.d.ts
vendored
Normal file
55
src/types/eslint-plugin-testing-library.d.ts
vendored
Normal 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;
|
||||
}
|
@ -6,8 +6,8 @@
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": true,
|
||||
"jsx": "react-jsx",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"noEmit": true,
|
||||
"noImplicitOverride": true,
|
||||
"noUnusedLocals": true,
|
||||
|
Loading…
x
Reference in New Issue
Block a user