Compare commits

..

1 Commits

Author SHA1 Message Date
179cf83891 Inline repo 2024-04-19 21:42:48 -04:00
132 changed files with 16174 additions and 7003 deletions

View File

@ -1,51 +0,0 @@
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[];

62
.eslintrc Normal file
View File

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

4
.gitignore vendored
View File

@ -1,11 +1,7 @@
drafts
!/packages/eslint-plugin-react-hooks !/packages/eslint-plugin-react-hooks
/packages/eslint-define-config
/react /react
/test
src/types/rules src/types/rules
dist2
dist/**/*.js dist/**/*.js
dist/**/*.js.map dist/**/*.js.map

1
.npmrc
View File

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

View File

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

View File

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

View File

@ -1,47 +0,0 @@
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)
*
* @returns ESLint configuration object.
*/
export function extendConfig(
options:
| FlatESLintConfig[]
| {
auto?: boolean;
middlewares?: Middleware[];
configs: FlatESLintConfig[];
/**
* Use `.gitignore` file to exclude files from ESLint.
*/
gitignore?: boolean;
},
): Promise<FlatESLintConfig[]>;
export const error = 'error';
export const warn = 'warn';
export const off = 'off';

14
dist/eslint-plugin-import/index.d.ts vendored Normal file
View File

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

View File

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

View File

@ -0,0 +1,12 @@
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;
};

9
dist/eslint-plugin-react/index.d.ts vendored Normal file
View File

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

72
dist/index.d.ts vendored Normal file
View File

@ -0,0 +1,72 @@
// Generated by dts-bundle-generator v9.4.0
import { ESLintUtils } from '@typescript-eslint/utils';
import { Rule } from 'eslint';
import { ESLintConfig, Rules } from 'eslint-define-config';
export declare const error = "error";
export declare const warn = "warn";
export declare const off = "off";
export type RuleLevel = "error" | "warn" | "off" | 0 | 1 | 2;
export type RuleEntry<Options> = RuleLevel | [
RuleLevel,
Partial<Options>
];
export interface LocalRuleOptions {
/** Bans import from the specifier '.' and '..' and replaces it with '.+/index' */
"rules/no-import-dot": RuleEntry<unknown>;
/**
* Enforce template literal expressions to be of `string` type
* @see [restrict-template-expressions](https://typescript-eslint.io/rules/restrict-template-expressions)
*/
"rules/restrict-template-expressions": RuleEntry<{
allow: string[];
}>;
/** Ban assignment of empty object literals `{}` and replace them with `Object.create(null)` */
"rules/no-empty-object-literal": RuleEntry<unknown>;
}
export type RuleOptions = Rules & Partial<LocalRuleOptions>;
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?: 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.
*/
customRuleFiles?: string | string[];
};
/**
* Returns a ESLint config object.
*
* By default, it includes `["@typescript-eslint", "import", "prettier"]` configs.
* Additional bundled plugins include:
*
* 1. [`react`](https://github.com/jsx-eslint/eslint-plugin-react#list-of-supported-rules)
* (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)
* 6. [`jsdoc`](https://github.com/gajus/eslint-plugin-jsdoc#rules)
*
* Non bundled:
* 1. [`graphql`](https://the-guild.dev/graphql/eslint/rules)
*/
export declare function extendConfig(of?: InputConfig): ESLintConfig;
export {};

92
dist/package.json vendored
View File

@ -1,74 +1,60 @@
{ {
"name": "@aet/eslint-rules", "name": "@aet/eslint-rules",
"version": "2.0.6", "version": "0.0.24-beta.1",
"license": "UNLICENSED", "license": "UNLICENSED",
"type": "module",
"bin": {
"eslint-install": "install.js",
"eslint-print": "print-config.sh"
},
"main": "./config/index.js",
"peerDependencies": { "peerDependencies": {
"eslint": "^9.14.0", "eslint": "^8.57.0",
"typescript": "^5.6.3" "typescript": "^5.4.4"
},
"optionalDependencies": {
"@tanstack/eslint-plugin-query": "^5.59.7"
}, },
"dependencies": { "dependencies": {
"@antfu/install-pkg": "^0.4.1", "@eslint-community/eslint-utils": "^4.4.0",
"@nolyfill/is-core-module": "^1.0.39", "@types/eslint": "^8.56.9",
"@aet/eslint-define-config": "^0.1.0-beta.33", "@typescript-eslint/eslint-plugin": "^7.7.0",
"@eslint/js": "^9.14.0", "@typescript-eslint/parser": "^7.7.0",
"@eslint-community/eslint-utils": "^4.4.1", "@typescript-eslint/type-utils": "^7.7.0",
"@types/eslint": "^9.6.1", "@typescript-eslint/utils": "^7.7.0",
"@typescript-eslint/eslint-plugin": "^8.13.0", "aria-query": "^5.3.0",
"@typescript-eslint/parser": "^8.13.0", "axe-core": "^4.9.0",
"@eslint-react/eslint-plugin": "1.15.2", "axobject-query": "^4.0.0",
"@stylistic/eslint-plugin": "^2.10.1",
"@typescript-eslint/type-utils": "^8.13.0",
"@typescript-eslint/utils": "^8.13.0",
"aria-query": "^5.3.2",
"axe-core": "^4.10.2",
"axobject-query": "4.1.0",
"damerau-levenshtein": "1.0.8", "damerau-levenshtein": "1.0.8",
"debug": "^4.3.7", "debug": "^4.3.4",
"doctrine": "^3.0.0", "doctrine": "^3.0.0",
"emoji-regex": "^10.4.0", "emoji-regex": "^10.3.0",
"enhanced-resolve": "^5.17.1", "enhanced-resolve": "^5.16.0",
"typescript-eslint": "^8.13.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-define-config": "^1.24.1",
"eslint-import-resolver-node": "^0.3.9", "eslint-import-resolver-node": "^0.3.9",
"eslint-import-resolver-typescript": "^3.6.3", "eslint-module-utils": "^2.8.1",
"eslint-module-utils": "^2.12.0", "eslint-plugin-es-x": "^7.6.0",
"eslint-plugin-import-x": "^4.4.0", "eslint-plugin-jsdoc": "^48.2.3",
"eslint-plugin-unicorn": "^56.0.0", "eslint-plugin-unicorn": "^52.0.0",
"esprima": "^4.0.1", "esprima": "^4.0.1",
"esquery": "^1.6.0", "esquery": "^1.5.0",
"estraverse": "^5.3.0", "estraverse": "^5.3.0",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"get-tsconfig": "^4.8.1", "get-tsconfig": "^4.7.3",
"globals": "^15.12.0", "ignore": "^5.3.1",
"ignore": "^6.0.2", "is-builtin-module": "^3.2.1",
"is-bun-module": "^1.2.1",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
"language-tags": "^1.0.9", "language-tags": "^1.0.9",
"lodash-es": "^4.17.21", "lodash": "^4.17.21",
"minimatch": "^10.0.1", "minimatch": "^9.0.4",
"semver": "^7.6.3" "resolve": "^2.0.0-next.5",
"semver": "^7.6.0",
"tsconfig-paths": "^4.2.0"
},
"overrides": {
"supports-preserve-symlinks-flag": "file:./overrides/supports-preserve-symlinks-flag",
"is-core-module": "file:./overrides/is-core-module"
},
"resolutions": {
"**/supports-preserve-symlinks-flag": "file:./overrides/supports-preserve-symlinks-flag",
"**/is-core-module": "file:./overrides/is-core-module"
}, },
"pnpm": { "pnpm": {
"overrides": { "overrides": {
"is-core-module": "file:./overrides/is-core-module", "supports-preserve-symlinks-flag": "file:./overrides/supports-preserve-symlinks-flag",
"supports-preserve-symlinks-flag": "file:./overrides/supports-preserve-symlinks-flag" "is-core-module": "file:./overrides/is-core-module"
} }
},
"overrides": {
"is-core-module": "file:./overrides/is-core-module",
"supports-preserve-symlinks-flag": "file:./overrides/supports-preserve-symlinks-flag"
},
"resolutions": {
"**/is-core-module": "file:./overrides/is-core-module",
"**/supports-preserve-symlinks-flag": "file:./overrides/supports-preserve-symlinks-flag"
} }
} }

View File

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

2
dist/types.d.ts vendored
View File

@ -6,7 +6,7 @@ import { Rule } from 'eslint';
export declare function defineRules(rules: { export declare function defineRules(rules: {
[ruleName: string]: Rule.RuleModule | ESLintUtils.RuleModule<string, unknown[]>; [ruleName: string]: Rule.RuleModule | ESLintUtils.RuleModule<string, unknown[]>;
}): { }): {
[ruleName: string]: Rule.RuleModule | ESLintUtils.RuleModule<string, unknown[], unknown, ESLintUtils.RuleListener>; [ruleName: string]: Rule.RuleModule | ESLintUtils.RuleModule<string, unknown[], ESLintUtils.RuleListener>;
}; };
export declare function defineRule({ name, create, ...meta }: Rule.RuleMetaData & { export declare function defineRule({ name, create, ...meta }: Rule.RuleMetaData & {
name?: string; name?: string;

1130
dist/yarn.lock vendored Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,76 +1,49 @@
{ {
"name": "@aet/eslint-configs", "name": "@aet/eslint-configs",
"type": "module",
"version": "0.0.0",
"scripts": { "scripts": {
"build": "./scripts/build.ts", "build": "./scripts/build-all.ts",
"check-import": "./scripts/check-imports.ts", "build-types": "cd ./packages/eslint-define-config && ./scripts/index.ts",
"define": "/usr/local/bin/codium ./packages/eslint-define-config", "check-import": "./scripts/check-imports.ts"
"do": "yarn build; (cd dist && ver bump && npm publish && ver unpub)"
}, },
"private": true, "private": true,
"devDependencies": { "devDependencies": {
"@aet/eslint-define-config": "^0.1.0-beta.33", "@babel/core": "^7.24.4",
"@antfu/install-pkg": "^0.4.1", "@babel/plugin-transform-flow-strip-types": "^7.24.1",
"@babel/core": "^7.26.0", "@babel/preset-env": "^7.24.4",
"@babel/plugin-transform-flow-strip-types": "^7.25.9",
"@babel/preset-env": "^7.26.0",
"@eslint-react/eslint-plugin": "^1.15.2",
"@eslint/js": "^9.14.0",
"@graphql-eslint/eslint-plugin": "^3.20.1",
"@stylistic/eslint-plugin": "^2.10.1",
"@swc-node/register": "^1.10.9",
"@tanstack/eslint-plugin-query": "^5.60.1",
"@types/babel-plugin-macros": "^3.1.3", "@types/babel-plugin-macros": "^3.1.3",
"@types/babel__core": "^7.20.5", "@types/babel__core": "^7.20.5",
"@types/eslint": "^9.6.1", "@types/eslint": "^8.56.10",
"@types/eslint-config-prettier": "^6.11.3",
"@types/eslint-plugin-tailwindcss": "^3.17.0",
"@types/eslint__js": "^8.42.3",
"@types/esprima": "^4.0.6", "@types/esprima": "^4.0.6",
"@types/esquery": "^1.5.4", "@types/esquery": "^1.5.3",
"@types/estree": "^1.0.6", "@types/estree": "^1.0.5",
"@types/estree-jsx": "^1.0.5", "@types/estree-jsx": "^1.0.5",
"@types/lodash-es": "^4.17.12", "@types/lodash": "^4.17.0",
"@types/node": "^22.9.0", "@types/node": "^20.12.7",
"@types/react-refresh": "^0.14.6", "@typescript-eslint/eslint-plugin": "7.7.0",
"@typescript-eslint/eslint-plugin": "^8.13.0", "@typescript-eslint/type-utils": "^7.7.0",
"@typescript-eslint/parser": "^8.13.0", "@typescript-eslint/types": "^7.7.0",
"@typescript-eslint/type-utils": "^8.13.0", "@typescript-eslint/typescript-estree": "^7.7.0",
"@typescript-eslint/types": "^8.13.0", "@typescript-eslint/utils": "^7.7.0",
"@typescript-eslint/typescript-estree": "^8.13.0",
"@typescript-eslint/utils": "^8.13.0",
"babel-plugin-macros": "^3.1.0", "babel-plugin-macros": "^3.1.0",
"dts-bundle-generator": "9.5.1", "dts-bundle-generator": "^9.5.0",
"esbuild": "0.24.0", "esbin": "0.0.4",
"esbuild": "0.20.2",
"esbuild-plugin-alias": "^0.2.1", "esbuild-plugin-alias": "^0.2.1",
"eslint": "9.14.0", "eslint": "8.57.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.3", "eslint-define-config": "file:./src/types",
"eslint-plugin-import-x": "^4.4.0",
"eslint-plugin-jsdoc": "^50.4.3",
"eslint-plugin-react-refresh": "^0.4.14",
"eslint-plugin-storybook": "canary",
"eslint-plugin-testing-library": "^6.4.0",
"eslint-plugin-unicorn": "^56.0.0",
"eslint-plugin-vitest": "^0.5.4",
"esprima": "^4.0.1", "esprima": "^4.0.1",
"esquery": "^1.6.0", "esquery": "^1.5.0",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"find-cache-dir": "^5.0.0", "find-cache-dir": "^5.0.0",
"globals": "^15.12.0", "json-schema-to-ts": "^3.0.1",
"graphql": "^16.9.0", "lodash": "^4.17.21",
"json-schema-to-ts": "^3.1.1", "minimatch": "^9.0.4",
"lodash-es": "^4.17.21",
"nolyfill": "^1.0.42",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"picocolors": "^1.1.1", "picocolors": "^1.0.0",
"prettier": "^3.3.3", "prettier": "^3.2.5",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"terser": "^5.36.0", "typescript": "^5.4.5"
"type-fest": "^4.26.1",
"typescript": "^5.6.3",
"typescript-eslint": "^8.13.0"
}, },
"prettier": { "prettier": {
"arrowParens": "avoid", "arrowParens": "avoid",
@ -82,22 +55,15 @@
}, },
"pnpm": { "pnpm": {
"overrides": { "overrides": {
"@typescript-eslint/utils": "8.0.0", "function-bind": "npm:@nolyfill/function-bind@latest",
"function-bind": "npm:@nolyfill/function-bind@^1", "has-proto": "npm:@nolyfill/has-proto@latest",
"has-proto": "npm:@nolyfill/has-proto@^1", "has-symbols": "npm:@nolyfill/has-symbols@latest",
"has-symbols": "npm:@nolyfill/has-symbols@^1", "hasown": "npm:@nolyfill/hasown@latest",
"hasown": "npm:@nolyfill/hasown@^1", "isarray": "npm:@nolyfill/isarray@latest",
"isarray": "npm:@nolyfill/isarray@^1", "jsonify": "npm:@nolyfill/jsonify@latest",
"jsonify": "npm:@nolyfill/jsonify@^1", "object-keys": "npm:@nolyfill/object-keys@latest",
"object-keys": "npm:@nolyfill/object-keys@^1", "set-function-length": "npm:@nolyfill/set-function-length@latest",
"set-function-length": "npm:@nolyfill/set-function-length@^1", "@babel/types": "7.24.0"
"@babel/types": "7.25.2",
"is-core-module": "npm:@nolyfill/is-core-module@^1",
"json-stable-stringify": "npm:@nolyfill/json-stable-stringify@^1"
},
"patchedDependencies": {
"@typescript-eslint/typescript-estree@8.0.0": "patches/@typescript-eslint__typescript-estree@8.0.0.patch",
"dts-bundle-generator": "patches/dts-bundle-generator.patch"
} }
} }
} }

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021-2023 Christopher Quadflieg
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,144 @@
<p>
<a href="https://www.npmjs.com/package/eslint-define-config" target="_blank">
<img alt="NPM package" src="https://img.shields.io/npm/v/eslint-define-config.svg">
</a>
<a href="https://www.npmjs.com/package/eslint-define-config" target="_blank">
<img alt="Downloads" src="https://img.shields.io/npm/dt/eslint-define-config.svg">
</a>
<a href="https://github.com/eslint-types/eslint-define-config/actions/workflows/ci.yml">
<img alt="Build Status" src="https://github.com/eslint-types/eslint-define-config/actions/workflows/ci.yml/badge.svg?branch=main">
</a>
<a href="https://github.com/eslint-types/eslint-define-config/blob/main/LICENSE">
<img alt="License: MIT" src="https://img.shields.io/github/license/eslint-types/eslint-define-config.svg">
</a>
<a href="https://prettier.io" target="_blank">
<img alt="Code Style: Prettier" src="https://img.shields.io/badge/code_style-prettier-ff69b4.svg">
</a>
<a href="https://www.paypal.com/donate?hosted_button_id=L7GY729FBKTZY" target="_blank">
<img alt="Donate: PayPal" src="https://img.shields.io/badge/Donate-PayPal-blue.svg">
</a>
</p>
# eslint-define-config
Provide a `defineConfig` function for `.eslintrc.js`, and a `defineFlatConfig` function for `eslint.config.js` files.
> This project is written by a human and only partially automatically generated!
> Some rules are even enhanced by hand!
> Unfortunately, this has the disadvantage that not everything is immediately defined. For example, if a rule is not defined, it falls back to a basic definition.
> However, the advantage is that you get documentation for pretty much everything in the code and usually get a direct link to the respective plugin or eslint rule. The types are also strictly typed.
>
> So if you are missing something like a rule or a plugin that should also be supported or a rule definition is e.g. out of date, feel free to open an issue or PR for it.
# Installation
```bash
# add eslint and eslint-define-config to projects dev dependencies
npm add --save-dev eslint eslint-define-config
# or
yarn add --dev eslint eslint-define-config
# or
pnpm add --save-dev eslint eslint-define-config
```
# Usage
`.eslintrc.js`
```ts
// @ts-check
// To activate auto-suggestions for Rules of specific plugins, you need to add a `/// <reference types="eslint-plugin-PLUGIN_NAME/define-config-support" />` comment.
// ⚠️ This feature is very new and requires the support of the respective plugin owners.
/// <reference types="@typescript-eslint/eslint-plugin/define-config-support" />
const { defineConfig } = require('eslint-define-config');
module.exports = defineConfig({
root: true,
rules: {
// rules...
},
});
```
## Flat Config
`eslint.config.js`
```ts
// @ts-check
const { defineFlatConfig } = require('eslint-define-config');
module.exports = defineFlatConfig([
'eslint:recommended',
{
plugins: {
// plugins...
},
rules: {
// rules...
},
},
]);
```
# Why?
Improve your eslint configuration experience with:
- auto-suggestions
- type checking (Use `// @ts-check` at the first line in your `.eslintrc.js` or `eslint.config.js`)
- documentation
- deprecation warnings
<img src="https://user-images.githubusercontent.com/7195563/112484789-8a416480-8d7a-11eb-9337-d8b5bc16de17.png" alt="Image" width="600px"/>
## Video
_Click on the thumbnail to play the video_
<a href="https://user-images.githubusercontent.com/7195563/112726158-4a19e780-8f1c-11eb-8cc6-4ea6c100137f.mp4" target="_blank">
<img src="https://user-images.githubusercontent.com/7195563/112726343-30c56b00-8f1d-11eb-9b92-260c530caf1b.png" alt="Video" width="600px"/>
</a>
## Want to support your own plugin?
:warning: **This feature is very new and requires the support of the respective plugin owners**
Add a `declare module` to your plugin package like this:
```ts
declare module 'eslint-define-config' {
export interface CustomRuleOptions {
/**
* Require consistently using either `T[]` or `Array<T>` for arrays.
*
* @see [array-type](https://typescript-eslint.io/rules/array-type)
*/
'@typescript-eslint/array-type': [
{
default?: 'array' | 'generic' | 'array-simple';
readonly?: 'array' | 'generic' | 'array-simple';
},
];
// ... more Rules
}
}
```
There are other interfaces that can be extended.
- `CustomExtends`
- `CustomParserOptions`
- `CustomParsers`
- `CustomPlugins`
- `CustomSettings`
# Credits
- [Proposal Idea](https://github.com/eslint/eslint/issues/14249)
- [Vite](https://github.com/vitejs/vite) and [Evan You](https://github.com/yyx990803) for the idea
- [@antfu](https://github.com/antfu) and his [tweet](https://twitter.com/antfu7/status/1365907188338753536)

View File

@ -0,0 +1,120 @@
{
"name": "eslint-define-config",
"version": "1.24.1",
"description": "Provide a defineConfig function for .eslintrc.js files",
"scripts": {
"clean": "rimraf coverage .eslintcache dist pnpm-lock.yaml node_modules",
"format": "prettier --cache --write .",
"lint:run": "eslint --cache --cache-strategy content --report-unused-disable-directives .",
"lint": "run-s build lint:run",
"typecheck": "vitest typecheck",
"ts-check": "tsc",
"test": "vitest",
"coverage": "vitest run --coverage",
"generate:rules": "tsx ./scripts/generate-rule-files/cli.ts",
"prepublishOnly": "pnpm run clean && pnpm install && pnpm run build",
"preflight": "pnpm install && run-s format lint ts-check test typecheck"
},
"type": "module",
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"default": "./dist/index.js"
}
},
"keywords": [
"config",
"configuration",
"define-config",
"eslint-config",
"eslint",
"eslintconfig",
"typed",
"typescript"
],
"author": {
"name": "Christopher Quadflieg",
"email": "chrissi92@hotmail.de",
"url": "https://github.com/Shinigami92"
},
"repository": {
"type": "git",
"url": "https://github.com/eslint-types/eslint-define-config.git"
},
"funding": [
{
"type": "github",
"url": "https://github.com/Shinigami92"
},
{
"type": "paypal",
"url": "https://www.paypal.com/donate/?hosted_button_id=L7GY729FBKTZY"
}
],
"bugs": "https://github.com/eslint-types/eslint-define-config/issues",
"license": "MIT",
"files": [
"dist",
"src",
"tsconfig.json"
],
"devDependencies": {
"@graphql-eslint/eslint-plugin": "~3.20.1",
"@intlify/eslint-plugin-vue-i18n": "~2.0.0",
"@poppinss/cliui": "~6.4.1",
"@types/dedent": "^0.7.2",
"@types/eslint": "~8.44.3",
"@types/json-schema": "~7.0.13",
"@types/node": "~20.8.3",
"@typescript-eslint/eslint-plugin": "~6.7.4",
"@typescript-eslint/parser": "~6.7.4",
"@vitest/coverage-v8": "~0.34.6",
"change-case": "~5.4.4",
"dedent": "^1.5.3",
"eslint-config-prettier": "~9.0.0",
"eslint-gitignore": "~0.1.0",
"eslint-plugin-deprecation": "~2.0.0",
"eslint-plugin-eslint-comments": "~3.2.0",
"eslint-plugin-import": "~2.28.1",
"eslint-plugin-inclusive-language": "~2.2.1",
"eslint-plugin-jsdoc": "~46.8.2",
"eslint-plugin-jsonc": "~2.9.0",
"eslint-plugin-jsx-a11y": "~6.7.1",
"eslint-plugin-mdx": "~3.1.5",
"eslint-plugin-n": "~16.1.0",
"eslint-plugin-node": "~11.1.0",
"eslint-plugin-prettier": "~5.0.0",
"eslint-plugin-promise": "~6.1.1",
"eslint-plugin-react": "~7.33.2",
"eslint-plugin-react-hooks": "~4.6.0",
"eslint-plugin-sonarjs": "~0.21.0",
"eslint-plugin-spellcheck": "~0.0.20",
"eslint-plugin-testing-library": "~6.0.2",
"eslint-plugin-unicorn": "~48.0.1",
"eslint-plugin-vitest": "~0.3.2",
"eslint-plugin-vue": "~9.17.0",
"eslint-plugin-vue-pug": "~0.6.0",
"eslint-plugin-yml": "~1.10.0",
"expect-type": "~0.17.3",
"graphql": "~16.8.1",
"json-schema": "~0.4.0",
"json-schema-to-ts": "~2.9.2",
"json-schema-to-typescript": "~13.1.1",
"npm-run-all": "~4.1.5",
"prettier-plugin-organize-imports": "^3.2.4",
"rimraf": "~5.0.5",
"tsup": "~7.2.0",
"vue-eslint-parser": "~9.3.2"
},
"packageManager": "pnpm@8.8.0",
"engines": {
"node": ">=18.0.0",
"npm": ">=9.0.0",
"pnpm": ">=8.6.0"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
diff --git b/lib/rules/eslint/no-constructor-return.d.ts a/lib/rules/eslint/no-constructor-return.d.ts
index fedeca4..3e1fd03 100644
--- b/lib/rules/eslint/no-constructor-return.d.ts
+++ a/lib/rules/eslint/no-constructor-return.d.ts
@@ -1,16 +1,9 @@
import type { RuleConfig } from '../rule-config';
-/**
- * Option.
- */
-export interface NoConstructorReturnOption {
- [k: string]: any;
-}
-
/**
* Options.
*/
-export type NoConstructorReturnOptions = NoConstructorReturnOption;
+export type NoConstructorReturnOptions = [];
/**
* Disallow returning value from constructor.

View File

@ -0,0 +1,14 @@
diff --git b/lib/rules/graphql-eslint/naming-convention.d.ts a/lib/rules/graphql-eslint/naming-convention.d.ts
index 60b228b..5e01373 100644
--- b/lib/rules/graphql-eslint/naming-convention.d.ts
+++ a/lib/rules/graphql-eslint/naming-convention.d.ts
@@ -78,8 +78,7 @@ export type NamingConventionOption =
VariableDefinition?: AsString | AsObject;
allowLeadingUnderscore?: boolean;
allowTrailingUnderscore?: boolean;
- /**
- */
+ } & {
[k: string]: AsString | AsObject;
},
];

View File

@ -0,0 +1,13 @@
diff --git b/lib/rules/node/file-extension-in-import.d.ts a/lib/rules/node/file-extension-in-import.d.ts
index 652b18d..7261252 100644
--- b/lib/rules/node/file-extension-in-import.d.ts
+++ a/lib/rules/node/file-extension-in-import.d.ts
@@ -5,7 +5,7 @@ import type { RuleConfig } from '../rule-config';
*/
export interface FileExtensionInImportConfig {
tryExtensions?: string[];
- [k: string]: 'always' | 'never';
+ [ext: `.${string}`]: 'always' | 'never';
}
/**

View File

@ -0,0 +1,23 @@
diff --git a/lib/rules/react/jsx-no-constructed-context-values.d.ts b/lib/rules/react/jsx-no-constructed-context-values.d.ts
index 410f060..e356693 100644
--- a/lib/rules/react/jsx-no-constructed-context-values.d.ts
+++ b/lib/rules/react/jsx-no-constructed-context-values.d.ts
@@ -1,17 +1,9 @@
import type { RuleConfig } from '../rule-config';
-/**
- * Option.
- */
-export interface JsxNoConstructedContextValuesOption {
- [k: string]: any;
-}
-
/**
* Options.
*/
-export type JsxNoConstructedContextValuesOptions =
- JsxNoConstructedContextValuesOption;
+export type JsxNoConstructedContextValuesOptions = [];
/**
* Disallows JSX context provider values from taking values that will cause needless rerenders.

View File

@ -0,0 +1,13 @@
diff --git a/lib/rules/react/jsx-props-no-spreading.d.ts b/lib/rules/react/jsx-props-no-spreading.d.ts
index c1e0069..ba1e1bc 100644
--- a/lib/rules/react/jsx-props-no-spreading.d.ts
+++ b/lib/rules/react/jsx-props-no-spreading.d.ts
@@ -8,8 +8,6 @@ export type JsxPropsNoSpreadingOption = {
custom?: 'enforce' | 'ignore';
exceptions?: string[];
[k: string]: any;
-} & {
- [k: string]: any;
};
/**

View File

@ -0,0 +1,21 @@
diff --git a/lib/rules/vitest/valid-title.d.ts b/lib/rules/vitest/valid-title.d.ts
index 160be76..834ac9b 100644
--- a/lib/rules/vitest/valid-title.d.ts
+++ b/lib/rules/vitest/valid-title.d.ts
@@ -7,15 +7,7 @@ export interface ValidTitleOption {
ignoreTypeOfDescribeName?: boolean;
allowArguments?: boolean;
disallowedWords?: string[];
- /**
- */
- [k: string]:
- | string
- | [string]
- | [string, string]
- | {
- [k: string]: string | [string] | [string, string];
- };
+ [k: string]: any;
}
/**

View File

@ -0,0 +1,178 @@
#!/usr/bin/env bun
import { Logger, colors as cliColors } from '@poppinss/cliui';
import { pascalCase } from 'change-case';
import type { Rule } from 'eslint';
import { existsSync, promises as fs } from 'node:fs';
import { join, resolve } from 'node:path';
import { format } from 'prettier';
import { buildJSDoc, prettierConfig, type Plugin } from './utils';
import { PLUGIN_REGISTRY, loadPlugin } from './plugins-map';
import { RuleFile } from './rule-file';
interface FailedRule {
ruleName: string;
err: unknown;
}
const logger = new Logger();
const colors = cliColors.ansi();
const getRuleName = (name: string) =>
name === 'ESLint' ? 'ESLintRule' : `${pascalCase(name)}Rule`;
const index: [string, string][] = [];
/**
* Generate the `index.d.ts` file for the plugin's rules that will re-export all rules.
*/
async function generateRuleIndexFile(
pluginDirectory: string,
{ rules, name, id }: Plugin,
failedRules: FailedRule[],
): Promise<void> {
const generatedRules = Object.keys(rules!).filter(
ruleName => !failedRules.some(failedRule => failedRule.ruleName === ruleName),
);
/**
* Build all the import statements for the rules.
*/
const rulesImports = generatedRules
.map(name => `import type { ${getRuleName(name)} } from './${name}';`)
.join('\n');
/**
* Build the exported type that is an intersection of all the rules.
*/
const rulesFinalIntersection = generatedRules
.map(name => `${getRuleName(name)}`)
.sort()
.join(' & ');
const pluginRulesType = `
${buildJSDoc([`All ${name} rules.`])}
export type ${name}Rules = ${rulesFinalIntersection};
`;
/**
* Write the final `index.d.ts` file.
*/
const fileContent = `
${rulesImports}
${pluginRulesType}
`;
const indexPath = join(pluginDirectory, 'index.d.ts');
await fs.writeFile(indexPath, await format(fileContent, prettierConfig));
index.push([`${name}Rules`, `./${id}/index`]);
}
/**
* Print a report after having generated rules files for a plugin.
*/
function printGenerationReport(
rules: Array<[string, Rule.RuleModule]>,
failedRules: FailedRule[],
): void {
const msg: string = ` ✅ Generated ${rules.length - failedRules.length} rules`;
logger.logUpdate(colors.green(msg));
logger.logUpdatePersist();
if (failedRules.length) {
logger.log(colors.red(` ❌ Failed ${failedRules.length} rules`));
failedRules.forEach(({ ruleName, err }) => {
logger.log(colors.red(` - ${ruleName}: ${String(err)}`));
});
}
logger.log('');
}
/**
* Generate a `.d.ts` file for each rule in the given plugin.
*/
async function generateRulesFiles(
plugin: Plugin,
pluginDirectory: string,
): Promise<{ failedRules: FailedRule[] }> {
const failedRules: FailedRule[] = [];
const pluginRules = plugin.rules;
if (!pluginRules) {
throw new Error(
`Plugin ${plugin.name} doesn't have any rules. Did you forget to load them?`,
);
}
const rules = Object.entries(pluginRules);
for (const [ruleName, rule] of rules) {
logger.logUpdate(colors.yellow(` Generating > ${ruleName}`));
try {
await RuleFile(plugin, pluginDirectory, ruleName, rule);
} catch (err) {
failedRules.push({ ruleName, err });
}
}
printGenerationReport(rules, failedRules);
return { failedRules };
}
const rulesDirectory = resolve(__dirname, '../../../src/types/rules');
/**
* If it doesn't exist, create the directory that will contain the plugin's rule files.
*/
async function createPluginDirectory(pluginName: string): Promise<string> {
const pluginDirectory = join(rulesDirectory, pluginName);
if (existsSync(pluginDirectory)) {
await fs.rm(pluginDirectory, { recursive: true, force: true });
}
await fs.mkdir(pluginDirectory, { mode: 0o755, recursive: true });
return pluginDirectory;
}
export interface RunOptions {
plugins?: string[];
targetDirectory?: string;
}
for (const plugin of PLUGIN_REGISTRY) {
logger.info(`Generating ${plugin.name} rules.`);
logger.logUpdate(colors.yellow(` Loading plugin > ${plugin.name}`));
const loadedPlugin = await loadPlugin(plugin);
const pluginDir = await createPluginDirectory(plugin.id);
const { failedRules } = await generateRulesFiles(loadedPlugin, pluginDir);
await generateRuleIndexFile(pluginDir, loadedPlugin, failedRules);
}
await fs.writeFile(
resolve(rulesDirectory, 'index.d.ts'),
await format(
[
'import type { RuleConfig } from "./rule-config";',
...index.map(([name, path]) => `import { ${name} } from ${JSON.stringify(path)};`),
'',
'/**',
' * Rules.',
' *',
' * @see [Rules](https://eslint.org/docs/user-guide/configuring/rules)',
' */',
'',
'export type Rules = Partial<',
...index.map(([name]) => ` ${name} &`),
' Record<string, RuleConfig>',
'>;',
].join('\n'),
prettierConfig,
),
);

View File

@ -0,0 +1,58 @@
import type { JSONSchema4 } from 'json-schema';
import { compile } from 'json-schema-to-typescript';
/**
* Remove unnecessary comments that are generated by `json-schema-to-typescript`.
*/
function cleanJsDoc(content: string): string {
const patterns = [
/\* This interface was referenced by .+ JSON-Schema definition/,
/\* via the `.+` "/,
];
return content
.split('\n')
.filter(line => !patterns.some(ignoredLine => ignoredLine.test(line)))
.join('\n');
}
/**
* Replace some types that are generated by `json-schema-to-typescript`.
*/
export function patchTypes(content: string): string {
const replacements: [RegExp, string][] = [
[
/\(string & \{\s*\[k: string\]: any\s*\} & \{\s*\[k: string\]: any\s*\}\)\[\]/,
'string[]',
],
];
for (const [pattern, replacement] of replacements) {
content = content.replace(pattern, replacement);
}
return content;
}
/**
* Generate a type from the given JSON schema.
*/
export async function generateTypeFromSchema(
schema: JSONSchema4,
typeName: string,
): Promise<string> {
schema = JSON.parse(
JSON.stringify(schema).replace(/#\/items\/0\/\$defs\//g, '#/$defs/'),
);
const result = await compile(schema, typeName, {
format: false,
bannerComment: '',
style: {
singleQuote: true,
trailingComma: 'all',
},
unknownAny: false,
});
return patchTypes(cleanJsDoc(result));
}

View File

@ -0,0 +1,145 @@
import type { Plugin, PluginRules } from './utils';
/**
* Map of plugins for which the script will generate rule files.
*/
export const PLUGIN_REGISTRY: Plugin[] = [
{
id: 'deprecation',
name: 'Deprecation',
module: () => import('eslint-plugin-deprecation'),
},
{
id: 'eslint',
name: 'ESLint',
module: () => import('eslint'),
},
{
id: 'typescript-eslint',
name: 'TypeScript',
prefix: '@typescript-eslint',
module: () => import('@typescript-eslint/eslint-plugin'),
},
{
id: 'import',
name: 'Import',
module: () => import('eslint-plugin-import'),
},
{
id: 'eslint-comments',
name: 'EslintComments',
module: () => import('eslint-plugin-eslint-comments'),
},
{
id: 'graphql-eslint',
name: 'GraphQL',
prefix: '@graphql-eslint',
module: () => import('@graphql-eslint/eslint-plugin'),
},
{
id: 'jsdoc',
name: 'JSDoc',
prefix: 'jsdoc',
module: () => import('eslint-plugin-jsdoc'),
},
{
id: 'jsonc',
name: 'Jsonc',
module: () => import('eslint-plugin-jsonc'),
},
{
id: 'jsx-a11y',
name: 'JsxA11y',
module: () => import('eslint-plugin-jsx-a11y'),
},
{
id: 'mdx',
name: 'Mdx',
module: () => import('eslint-plugin-mdx'),
},
{
id: 'n',
name: 'N',
module: () => import('eslint-plugin-n'),
},
{
id: 'node',
name: 'Node',
module: () => import('eslint-plugin-node'),
},
{
id: 'promise',
name: 'Promise',
module: () => import('eslint-plugin-promise'),
},
{
id: 'react',
name: 'React',
module: () => import('eslint-plugin-react'),
},
{
id: 'react-hooks',
name: 'ReactHooks',
module: () => import('eslint-plugin-react-hooks'),
},
{
id: 'sonarjs',
name: 'SonarJS',
prefix: 'sonarjs',
module: () => import('eslint-plugin-sonarjs'),
},
{
id: 'spellcheck',
name: 'Spellcheck',
module: () => import('eslint-plugin-spellcheck'),
},
{
id: 'testing-library',
name: 'TestingLibrary',
module: () => import('eslint-plugin-testing-library'),
},
{
id: 'unicorn',
name: 'Unicorn',
module: () => import('eslint-plugin-unicorn'),
},
{
id: 'vitest',
name: 'Vitest',
module: () => import('eslint-plugin-vitest'),
},
{
id: 'vue',
name: 'Vue',
module: () => import('eslint-plugin-vue'),
},
{
id: 'vue-i18n',
name: 'VueI18n',
prefix: '@intlify/vue-i18n',
module: () => import('@intlify/eslint-plugin-vue-i18n'),
},
{
id: 'vue-pug',
name: 'VuePug',
module: () => import('eslint-plugin-vue-pug'),
},
{
id: 'yml',
name: 'Yml',
module: () => import('eslint-plugin-yml'),
},
] as const;
export async function loadPlugin(plugin: Plugin): Promise<Plugin> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mod: any = await plugin.module();
const rules: PluginRules =
plugin.name === 'ESLint'
? Object.fromEntries(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
new mod.Linter().getRules().entries(),
)
: mod.rules ?? mod.default.rules;
return { ...plugin, rules };
}

View File

@ -0,0 +1,168 @@
import { Logger, colors as cliColors } from '@poppinss/cliui';
import { pascalCase, kebabCase } from 'change-case';
import type { Rule } from 'eslint';
import type { JSONSchema4 } from 'json-schema';
import { execSync } from 'node:child_process';
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { format } from 'prettier';
import { generateTypeFromSchema } from './json-schema-to-ts';
import { buildJSDoc, prettierConfig, type Plugin } from './utils';
const logger = new Logger();
const colors = cliColors.ansi();
/**
* Build the rule description to append to the JSDoc.
*/
function buildDescription(description = ''): string {
description = description.charAt(0).toUpperCase() + description.slice(1);
if (description.length > 0 && !description.endsWith('.')) {
description += '.';
}
return description.replace('*/', '');
}
export async function RuleFile(
plugin: Plugin,
pluginDirectory: string,
ruleName: string,
rule: Rule.RuleModule,
) {
let content = '';
const rulePath = resolve(pluginDirectory, `${ruleName}.d.ts`);
const ruleNamePascalCase = pascalCase(ruleName);
const schema: JSONSchema4 | JSONSchema4[] | undefined = rule.meta?.schema;
const isSchemaArray = Array.isArray(schema);
const mainSchema = isSchemaArray ? schema[0] : schema;
const sideSchema = isSchemaArray && schema.length > 1 ? schema[1] : undefined;
const thirdSchema = isSchemaArray && schema.length > 2 ? schema[2] : undefined;
/**
* Generate a JSDoc with the rule description and `@see` url.
*/
function generateTypeJsDoc(): string {
const { meta } = rule;
const seeDocLink: string = meta?.docs?.url
? `@see [${ruleName}](${meta.docs.url})`
: '';
return buildJSDoc([
buildDescription(rule.meta?.docs?.description),
' ',
rule.meta?.deprecated ? '@deprecated' : '',
rule.meta?.deprecated ? ' ' : '',
seeDocLink,
]);
}
/**
* Generate a type from a JSON schema and append it to the file content.
*/
async function appendJsonSchemaType(
schema: JSONSchema4,
comment: string,
): Promise<void> {
const type = await generateTypeFromSchema(schema, ruleNamePascalCase + comment);
const jsdoc = buildJSDoc([`${comment}.`]);
content += `\n${jsdoc}`;
content += `\n${type}\n`;
}
/**
* Scoped rule name ESLint config uses.
*/
function prefixedRuleName(): string {
const { prefix, name } = plugin;
const rulePrefix = (prefix ?? kebabCase(name)) + '/';
return name === 'ESLint' ? ruleName : `${rulePrefix}${ruleName}`;
}
/**
* Append the `import type { RuleConfig } from '../rule-config'` at the top of the file.
*/
const nestedDepth = ruleName.split('/').length;
content += `import type { RuleConfig } from '${'../'.repeat(nestedDepth)}rule-config'\n\n`;
/**
* Generate and append types for the rule schemas.
*/
if (thirdSchema) {
await appendJsonSchemaType(thirdSchema, 'Setting');
}
if (sideSchema) {
await appendJsonSchemaType(sideSchema, 'Config');
}
if (mainSchema) {
await appendJsonSchemaType(mainSchema, 'Option');
}
if (mainSchema) {
/**
* Append the rule type options to the file content.
*/
let type: string = '';
if (!isSchemaArray) {
type = `${ruleNamePascalCase}Option`;
} else if (thirdSchema) {
type = `[${ruleNamePascalCase}Option?, ${ruleNamePascalCase}Config?, ${ruleNamePascalCase}Setting?]`;
} else if (sideSchema) {
type = `[${ruleNamePascalCase}Option?, ${ruleNamePascalCase}Config?]`;
} else if (mainSchema) {
type = `[${ruleNamePascalCase}Option?]`;
}
content += buildJSDoc(['Options.']) + '\n';
content += `export type ${ruleNamePascalCase}Options = ${type}\n\n`;
}
/**
* Append the rule config type to the file content.
*/
content += generateTypeJsDoc() + '\n';
content += `export type ${ruleNamePascalCase}RuleConfig = RuleConfig<${mainSchema ? `${ruleNamePascalCase}Options` : '[]'}>;\n\n`;
/**
* Append the final rule interface to the file content.
*/
content += generateTypeJsDoc() + '\n';
content += `export interface ${ruleNamePascalCase}Rule {`;
content += `${generateTypeJsDoc()}\n`;
content += `'${prefixedRuleName()}': ${ruleNamePascalCase}RuleConfig;`;
content += '}';
content = await format(content, prettierConfig);
/**
* Create the directory of the rule file if it doesn't exist.
*/
const subPath = dirname(rulePath);
if (!existsSync(subPath)) {
mkdirSync(subPath, { recursive: true });
}
writeFileSync(rulePath, content);
/**
* Apply a patch to the generated content if a diff file exists for it.
*
* Must be called after `generate()`.
*/
const pathParts = rulePath.split('/');
const diffFile = resolve(
__dirname,
'./diffs/rules',
pathParts.at(-2) ?? '',
`${pathParts.at(-1) ?? ''}.diff`,
);
if (existsSync(diffFile)) {
logger.logUpdate(colors.yellow(` 🧹 Adjusting ${prefixedRuleName()}`));
logger.logUpdatePersist();
execSync(`git apply ${diffFile}`);
}
}

View File

@ -0,0 +1,24 @@
import type { Rule } from 'eslint';
import type { Config } from 'prettier';
export function buildJSDoc(content: string[]) {
return ['/**', ...content.filter(Boolean).map(line => ` * ${line}`), ' */'].join('\n');
}
export const prettierConfig: Config = {
plugins: ['prettier-plugin-organize-imports'],
parser: 'typescript',
singleQuote: true,
trailingComma: 'all',
};
export type MaybeArray<T> = T | T[];
export type PluginRules = Record<string, Rule.RuleModule>;
export interface Plugin {
id: string;
name: string;
prefix?: string;
module: () => Promise<any>;
rules?: PluginRules;
}

Submodule packages/eslint-import-resolver-typescript updated: 3dfad602a0...7a02ac08b5

Submodule packages/eslint-plugin-import added at f77ceb679d

Submodule packages/eslint-plugin-jsx-a11y updated: cca288b73a...0d5321a545

Submodule packages/eslint-plugin-n updated: 6744257b43...eb11b5b35a

Submodule packages/eslint-plugin-react added at 4467db503e

View File

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

View File

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

View File

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

View File

@ -1,13 +1,10 @@
{ {
"name": "eslint-plugin-react-hooks",
"version": "4.2.0",
"upstream": { "upstream": {
"version": 1, "version": 1,
"comment": "https://github.com/facebook/react/pull/30774",
"sources": { "sources": {
"main": { "main": {
"repository": "git@github.com:facebook/react.git", "repository": "https://github.com/facebook/react",
"commit": "899cb95f52cc83ab5ca1eb1e268c909d3f0961e7", "commit": "0e0b69321a6fcfe8a3eaae3b1016beb110437b38",
"branch": "main" "branch": "main"
} }
} }

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,8 +1,8 @@
diff --git a/lib/index.js b/lib/index.js diff --git a/lib/index.js b/lib/index.js
index de95218..e30a3df 100644 index 49fd4c7..a0fdd81 100644
--- a/lib/index.js --- a/lib/index.js
+++ b/lib/index.js +++ b/lib/index.js
@@ -1,17 +1,17 @@ @@ -1,9 +1,9 @@
"use strict" "use strict"
-const pkg = require("../package.json") -const pkg = require("../package.json")
@ -14,110 +14,16 @@ index de95218..e30a3df 100644
+import cjsConfig from "./configs/recommended-script" +import cjsConfig from "./configs/recommended-script"
+import recommendedConfig from "./configs/recommended" +import recommendedConfig from "./configs/recommended"
/** @import { ESLint, Linter } from 'eslint' */ /**
* @typedef {{
/** @type {ESLint.Plugin} */ @@ -20,8 +20,8 @@ const recommendedConfig = require("./configs/recommended")
const base = { /** @type {import('eslint').ESLint.Plugin & { configs: Configs }} */
const plugin = {
meta: { meta: {
- name: pkg.name, - name: pkg.name,
- version: pkg.version, - version: pkg.version,
+ name, + name,
+ version, + version,
}, },
rules: { rules: /** @type {Record<string, import('eslint').Rule.RuleModule>} */ ({
"callback-return": require("./rules/callback-return"), "callback-return": require("./rules/callback-return"),
diff --git a/tests/fixtures/no-extraneous/dependencies/node_modules/@bbb/aaa.js b/tests/fixtures/no-extraneous/dependencies/node_modules/@bbb/aaa.js
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/fixtures/no-extraneous/dependencies/node_modules/aaa.js b/tests/fixtures/no-extraneous/dependencies/node_modules/aaa.js
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/fixtures/no-extraneous/dependencies/node_modules/bbb/index.js b/tests/fixtures/no-extraneous/dependencies/node_modules/bbb/index.js
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/fixtures/no-extraneous/dependencies/node_modules/bbb/package.json b/tests/fixtures/no-extraneous/dependencies/node_modules/bbb/package.json
deleted file mode 100644
index b7d25e2..0000000
--- a/tests/fixtures/no-extraneous/dependencies/node_modules/bbb/package.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "name": "bbb",
- "main": "index.js"
-}
\ No newline at end of file
diff --git a/tests/fixtures/no-extraneous/devDependencies/node_modules/@bbb/aaa.js b/tests/fixtures/no-extraneous/devDependencies/node_modules/@bbb/aaa.js
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/fixtures/no-extraneous/devDependencies/node_modules/aaa.js b/tests/fixtures/no-extraneous/devDependencies/node_modules/aaa.js
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/fixtures/no-extraneous/devDependencies/node_modules/bbb/index.js b/tests/fixtures/no-extraneous/devDependencies/node_modules/bbb/index.js
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/fixtures/no-extraneous/devDependencies/node_modules/bbb/package.json b/tests/fixtures/no-extraneous/devDependencies/node_modules/bbb/package.json
deleted file mode 100644
index b7d25e2..0000000
--- a/tests/fixtures/no-extraneous/devDependencies/node_modules/bbb/package.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "name": "bbb",
- "main": "index.js"
-}
\ No newline at end of file
diff --git a/tests/fixtures/no-extraneous/noDependencies/node_modules/@bbb/aaa.js b/tests/fixtures/no-extraneous/noDependencies/node_modules/@bbb/aaa.js
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/fixtures/no-extraneous/noDependencies/node_modules/aaa.js b/tests/fixtures/no-extraneous/noDependencies/node_modules/aaa.js
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/fixtures/no-extraneous/noDependencies/node_modules/bbb.js b/tests/fixtures/no-extraneous/noDependencies/node_modules/bbb.js
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/fixtures/no-extraneous/optionalDependencies/node_modules/@bbb/aaa.js b/tests/fixtures/no-extraneous/optionalDependencies/node_modules/@bbb/aaa.js
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/fixtures/no-extraneous/optionalDependencies/node_modules/aaa.js b/tests/fixtures/no-extraneous/optionalDependencies/node_modules/aaa.js
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/fixtures/no-extraneous/optionalDependencies/node_modules/bbb/index.js b/tests/fixtures/no-extraneous/optionalDependencies/node_modules/bbb/index.js
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/fixtures/no-extraneous/optionalDependencies/node_modules/bbb/package.json b/tests/fixtures/no-extraneous/optionalDependencies/node_modules/bbb/package.json
deleted file mode 100644
index b7d25e2..0000000
--- a/tests/fixtures/no-extraneous/optionalDependencies/node_modules/bbb/package.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "name": "bbb",
- "main": "index.js"
-}
\ No newline at end of file
diff --git a/tests/fixtures/no-extraneous/peerDependencies/node_modules/@bbb/aaa.js b/tests/fixtures/no-extraneous/peerDependencies/node_modules/@bbb/aaa.js
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/fixtures/no-extraneous/peerDependencies/node_modules/aaa.js b/tests/fixtures/no-extraneous/peerDependencies/node_modules/aaa.js
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/fixtures/no-extraneous/peerDependencies/node_modules/bbb/index.js b/tests/fixtures/no-extraneous/peerDependencies/node_modules/bbb/index.js
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/fixtures/no-extraneous/peerDependencies/node_modules/bbb/package.json b/tests/fixtures/no-extraneous/peerDependencies/node_modules/bbb/package.json
deleted file mode 100644
index b7d25e2..0000000
--- a/tests/fixtures/no-extraneous/peerDependencies/node_modules/bbb/package.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "name": "bbb",
- "main": "index.js"
-}
\ No newline at end of file
diff --git a/tests/fixtures/no-hide-core-modules/indirect-thirdparty/node_modules/util/index.js b/tests/fixtures/no-hide-core-modules/indirect-thirdparty/node_modules/util/index.js
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/fixtures/no-hide-core-modules/thirdparty/node_modules/util/index.js b/tests/fixtures/no-hide-core-modules/thirdparty/node_modules/util/index.js
deleted file mode 100644
index e69de29..0000000

View File

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

View File

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

View File

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

View File

@ -1,11 +0,0 @@
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;

7746
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,24 +1,19 @@
#!/usr/bin/env tsx #!/usr/bin/env bun
import './build-local-rules';
import { promises as fs } from 'node:fs'; import { promises as fs } from 'node:fs';
import { resolve, relative } from 'node:path';
import { isBuiltin } from 'node:module'; import { isBuiltin } from 'node:module';
import { relative, resolve } from 'node:path';
import esbuild from 'esbuild'; import esbuild from 'esbuild';
import type { Plugin } from 'esbuild'; import type { Plugin } from 'esbuild';
import { memoize } from 'lodash-es'; import { memoize } from 'lodash';
import c from 'picocolors'; import { gray, green } from 'picocolors';
import { minify_sync } from 'terser';
import { dependencies } from '../dist/package.json'; import { dependencies } from '../dist/package.json';
import { dts } from './dts'; import { dts } from './dts';
import { babelPlugin } from './modifier'; import { babelPlugin } from './modifier';
const ENV = (process.env.NODE_ENV ??= 'production'); const ENV = (process.env.NODE_ENV ??= 'production');
const PROD = ENV === 'production'; const PROD = ENV === 'production';
const { gray, green } = c;
declare global { declare global {
interface Array<T> { interface Array<T> {
filter( filter(
@ -70,12 +65,14 @@ if (process.env.DEBUG) {
}); });
} }
async function bundle( function bundle(
entry: string, entry: string,
outfile: string, outfile = entry
options?: esbuild.BuildOptions & { treeShaking?: boolean }, .replace('./packages/', './dist/')
.replace('src/', '')
.replace('.ts', '.js'),
) { ) {
const output = await esbuild.build({ return esbuild.build({
entryPoints: [entry], entryPoints: [entry],
outfile, outfile,
bundle: true, bundle: true,
@ -87,36 +84,10 @@ async function bundle(
define: {}, define: {},
alias: {}, alias: {},
external: ['find-cache-dir'], external: ['find-cache-dir'],
format: 'esm',
banner: { banner: {
js: '/* eslint-disable */', js: '/* eslint-disable */',
}, },
...options,
}); });
if (options?.treeShaking) {
const [text, setText] = await useText(outfile);
const minified = minify_sync(text, {
module: true,
compress: {
conditionals: true,
dead_code: true,
defaults: false,
evaluate: true,
passes: 3,
pure_new: true,
side_effects: true,
unused: true,
},
mangle: false,
format: {
comments: true,
},
});
await setText(minified.code!);
}
return output;
} }
async function editPackageJson() { async function editPackageJson() {
@ -139,58 +110,44 @@ async function editPackageJson() {
} }
async function useText(path: string) { async function useText(path: string) {
const state = await fs.readFile(path, 'utf8'); const state = await fs.readFile(path, 'utf-8');
const setState = (text: string) => fs.writeFile(path, text); const setState = (text: string) => fs.writeFile(path, text);
return [state, setState] as const; return [state, setState] as const;
} }
function bundleType(source: string, output: string) { function bundleType(source: string, output: string) {
try {
return dts({ return dts({
source, source,
dist: output, dist: output,
project: './tsconfig.build.json', project: './tsconfig.build.json',
}); });
} catch {
// noop
}
} }
async function main() { async function main() {
console.log('Building type definitions'); console.log('Building type definitions...');
try { bundleType('./src/index.ts', './dist/index.d.ts');
await fs.rm('dist/config', { recursive: true });
} catch {
// noop
}
bundleType('./src/prettier.ts', './dist/prettier.d.ts'); bundleType('./src/prettier.ts', './dist/prettier.d.ts');
bundleType('./src/types.ts', './dist/types.d.ts'); bundleType('./src/types.ts', './dist/types.d.ts');
const unminify = { minify: false }; console.log('Building packages...');
console.log('Building packages…');
await Promise.all([ await Promise.all([
bundle('./src/index.ts', undefined!, { bundle('./packages/eslint-plugin-react/index.js'),
format: 'esm', bundle('./packages/eslint-plugin-import/src/index.js'),
splitting: true, bundle('./packages/eslint-plugin-jsx-a11y/src/index.js'),
outdir: './dist/config', bundle('./packages/eslint-plugin-react-hooks/index.ts'),
...unminify, 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/types.ts', './dist/types.js', unminify), bundle('./src/rules/index.ts', './dist/eslint-plugin-rules/index.js'),
bundle('./src/prettier.ts', './dist/prettier.js', unminify), bundle('./src/local/index.ts', './dist/eslint-plugin-local/index.js'),
bundle('./src/install.ts', './dist/install.js', { bundle('./src/index.ts', './dist/index.js'),
treeShaking: true, bundle('./src/types.ts', './dist/types.js'),
minify: false, bundle('./src/prettier.ts', './dist/prettier.js'),
banner: {
js: '#!/usr/bin/env node\n/* eslint-disable */',
},
}),
editPackageJson(), editPackageJson(),
]); ]);
// bundleType('./src/index.ts', './dist/config/index.d.ts'); console.log('Removing redirect...');
await fs.copyFile('./src/config.d.ts', './dist/config/index.d.ts'); const [distIndex, setDistIndex] = await useText('./dist/index.js');
await setDistIndex(distIndex.replace(/import.*redirect.*;/g, ''));
} }
void main(); void main();

24
scripts/build-local-rules.ts Executable file
View File

@ -0,0 +1,24 @@
import { promises as fs } from 'node:fs';
import { camelCase } from 'lodash';
const files = (await fs.readdir('./src/rules'))
.filter(file => file.endsWith('.ts'))
.filter(file => file !== 'index.ts')
.map(file => file.slice(0, -3));
const entryFile = /* js */ `
import type { Rule } from 'eslint';
import type { ESLintUtils } from '@typescript-eslint/utils';
${files.map(file => `import ${camelCase(file)} from './${file}';`).join('\n')}
export const rules: Record<
string,
Rule.RuleModule | ESLintUtils.RuleModule<string, unknown[]>
> = {
${files.map(file => `'${file}': ${camelCase(file)},`).join('\n ')}
};
`.trim();
console.log('Building local rules...');
await fs.writeFile('./src/rules/index.ts', entryFile + '\n');

View File

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

View File

@ -1,9 +1,9 @@
#!/usr/bin/env node #!/usr/bin/env node
import {
type EntryPointConfig,
generateDtsBundle,
} from 'dts-bundle-generator/dist/bundle-generator';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {
generateDtsBundle,
type EntryPointConfig,
} from 'dts-bundle-generator/dist/bundle-generator';
export function dts({ export function dts({
source, source,
@ -17,14 +17,22 @@ export function dts({
const entry: EntryPointConfig = { const entry: EntryPointConfig = {
filePath: source, filePath: source,
failOnClass: false, failOnClass: false,
libraries: {
importedLibraries: ['eslint-define-config'],
},
output: { output: {
inlineDeclareExternals: false,
inlineDeclareGlobals: false,
sortNodes: false,
noBanner: false,
respectPreserveConstEnum: false,
exportReferencedTypes: true, exportReferencedTypes: true,
}, },
}; };
const generatedDts = generateDtsBundle([entry], { const generatedDts = generateDtsBundle([entry], {
preferredConfigPath: project, preferredConfigPath: project,
followSymlinks: true, followSymlinks: false,
}); });
ts.sys.writeFile(dist, generatedDts[0]); ts.sys.writeFile(dist, generatedDts[0]);

View File

@ -1,24 +1,23 @@
#!/usr/bin/env tsx #!/usr/bin/env tsx
import assert from 'node:assert'; import assert from 'node:assert';
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
import { extname, resolve } from 'node:path'; import { resolve, extname } from 'node:path';
import type { Loader, Plugin } from 'esbuild';
import * as babel from '@babel/core'; import * as babel from '@babel/core';
import type { types as t, types } from '@babel/core'; import type { types as t, types } from '@babel/core';
import babelMacros, { type MacroHandler } from 'babel-plugin-macros'; import { createMacro, type MacroHandler } from 'babel-plugin-macros';
import type { Loader, Plugin } from 'esbuild';
import PropTypes from 'prop-types';
import * as polyfill from '../src/polyfill'; import * as polyfill from '../src/polyfill';
const polyfills = Object.keys(polyfill); const polyfills = Object.keys(polyfill);
const ENV = (process.env.NODE_ENV ??= 'production');
class HandlerMap { class HandlerMap {
map = new Map<string, MacroHandler>(); map = new Map<string, MacroHandler>();
set(names: string | string[], handler: MacroHandler) { set(names: string | string[], handler: MacroHandler) {
names = Array.isArray(names) ? names : [names]; names = Array.isArray(names) ? names : [names];
const macro = babelMacros.createMacro(handler); const macro = createMacro(handler);
for (const name of names) { for (const name of names) {
this.map.set(name, macro); this.map.set(name, macro);
} }
@ -26,7 +25,7 @@ class HandlerMap {
} }
get keys() { get keys() {
return [...this.map.keys()]; return Array.from(this.map.keys());
} }
resolvePath = (module: string) => module; resolvePath = (module: string) => module;
@ -99,14 +98,14 @@ const map = new HandlerMap()
'object.groupby', 'object.groupby',
replace(t => replace(t =>
t.memberExpression( t.memberExpression(
t.callExpression(t.identifier('require'), [t.stringLiteral('lodash-es')]), t.callExpression(t.identifier('require'), [t.stringLiteral('lodash')]),
t.identifier('groupBy'), t.identifier('groupBy'),
), ),
), ),
); );
// es-iterator-helpers/Iterator.prototype.* // es-iterator-helpers/Iterator.prototype.*
const polyfillPath = resolve(import.meta.dirname, '../src/polyfill.ts'); const polyfillPath = resolve(__dirname, '../src/polyfill.ts');
const requirePolyfill = (t: typeof types, name: string) => const requirePolyfill = (t: typeof types, name: string) =>
t.memberExpression( t.memberExpression(
t.callExpression(t.identifier('require'), [t.stringLiteral(polyfillPath)]), t.callExpression(t.identifier('require'), [t.stringLiteral(polyfillPath)]),
@ -130,15 +129,15 @@ map.set(
function replace(getReplacement: (types: typeof t) => t.Expression): MacroHandler { function replace(getReplacement: (types: typeof t) => t.Expression): MacroHandler {
return ({ references, babel: { types: t } }) => { return ({ references, babel: { types: t } }) => {
for (const referencePath of references.default) { references.default.forEach(referencePath => {
referencePath.replaceWith(getReplacement(t)); referencePath.replaceWith(getReplacement(t));
} });
}; };
} }
function proto(getProperty: (types: typeof t) => t.Expression): MacroHandler { function proto(getProperty: (types: typeof t) => t.Expression): MacroHandler {
return ({ references, babel: { types: t } }) => { return ({ references, babel: { types: t } }) => {
for (const referencePath of references.default) { references.default.forEach(referencePath => {
const { parent, parentPath } = referencePath; const { parent, parentPath } = referencePath;
assert(t.isCallExpression(parent)); assert(t.isCallExpression(parent));
const [callee, ...rest] = parent.arguments; const [callee, ...rest] = parent.arguments;
@ -148,7 +147,7 @@ function proto(getProperty: (types: typeof t) => t.Expression): MacroHandler {
rest, rest,
), ),
); );
} });
}; };
} }
@ -163,14 +162,21 @@ export const babelPlugin: Plugin = {
return null; return null;
} }
const source = readFileSync(path, 'utf8') let source = readFileSync(path, 'utf-8')
.replaceAll("require('object.hasown/polyfill')()", 'Object.hasOwn') .replaceAll("require('object.hasown/polyfill')()", 'Object.hasOwn')
.replaceAll("require('object.fromentries/polyfill')()", 'Object.fromEntries') .replaceAll("require('object.fromentries/polyfill')()", 'Object.fromEntries')
.replaceAll( .replaceAll(
"Object.keys(require('prop-types'))", "Object.keys(require('prop-types'))",
JSON.stringify(Object.keys(PropTypes)), JSON.stringify(Object.keys(require('prop-types'))),
); );
if (
path.includes('packages/eslint-plugin-import/src/rules/') ||
path.includes('packages/eslint-plugin-import/config/')
) {
source = source.replace('\nmodule.exports = {', '\nexport default {');
}
const isFlow = source.includes('@flow'); const isFlow = source.includes('@flow');
const loader = extname(path).slice(1) as Loader; const loader = extname(path).slice(1) as Loader;

View File

@ -21,6 +21,8 @@ pull() {
} }
pull import-js eslint-import-resolver-typescript pull import-js eslint-import-resolver-typescript
pull import-js eslint-plugin-import
pull jsx-eslint eslint-plugin-jsx-a11y pull jsx-eslint eslint-plugin-jsx-a11y
pull eslint-community eslint-plugin-n pull eslint-community eslint-plugin-n
pull jsx-eslint eslint-plugin-react
pull jsx-eslint jsx-ast-utils pull jsx-eslint jsx-ast-utils

View File

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

View File

@ -1,33 +1,33 @@
{ {
"eslint-plugin-import": { "eslint-plugin-import": {
"hash": "6554bd5c30976290024cecc44ef1e96746cf3cf7", "hash": "f77ceb679d59ced5d9a633123385470a9eea10d9",
"date": "2024-05-23T12:47:41-07:00", "date": "2024-04-07T12:55:28+12:00",
"committer": "Jordan Harband", "committer": "Jordan Harband",
"subject": "[meta] add `repository.directory` field" "subject": "[actions] cancel in-progress runs on PR updates"
}, },
"eslint-import-resolver-typescript": { "eslint-import-resolver-typescript": {
"hash": "42e7cc3eb413dda56683c1b2b2483e4756e0bd62", "hash": "7a02ac08b5aaac8c217f0e87142f97eafcc38fbc",
"date": "2024-11-01T01:52:08+00:00", "date": "2024-04-01T01:06:20+00:00",
"committer": "GitHub", "committer": "GitHub",
"subject": "chore(deps): update dependency @types/node to ^18.19.63 (#320)" "subject": "chore(deps): update dependency npm-run-all2 to ^5.0.2 (#277)"
}, },
"eslint-plugin-jsx-a11y": { "eslint-plugin-jsx-a11y": {
"hash": "743168b1ba15196ec7001c7c1f368f5efbe78f0d", "hash": "0d5321a5457c5f0da0ca216053cc5b4f571b53ae",
"date": "2024-10-23T13:27:41+10:00", "date": "2024-01-27T22:18:19-08:00",
"committer": "Jordan Harband", "committer": "Jordan Harband",
"subject": "[New] `label-has-associated-control`: allow `labelComponents` to contain globs" "subject": "[Deps] update `@babel/runtime`, `safe-regex-test`"
}, },
"eslint-plugin-n": { "eslint-plugin-n": {
"hash": "c4d15512b24a8c7c3ba4bf8b598e66eafd1baeec", "hash": "eb11b5b35a6a797dc7fba6df53b1c4dada3a2a55",
"date": "2024-11-07T20:14:17+08:00", "date": "2024-04-17T17:40:32+08:00",
"committer": "GitHub", "committer": "GitHub",
"subject": "chore(master): release 17.13.1 (#381)" "subject": "chore: upgrade globals v15 (#241)"
}, },
"eslint-plugin-react": { "eslint-plugin-react": {
"hash": "983b88dd3cb5e07919517d3fde4085f60883ded7", "hash": "4467db503e38b9356517cf6926d11be544ccf4b1",
"date": "2024-07-24T15:26:33-07:00", "date": "2024-03-16T12:54:58+09:00",
"committer": "Jordan Harband", "committer": "Jordan Harband",
"subject": "[Tests] `no-array-index-key`: actually run valid tests" "subject": "[Fix] `boolean-prop-naming`: avoid a crash with a non-TSTypeReference type"
}, },
"jsx-ast-utils": { "jsx-ast-utils": {
"hash": "5943318eaf23764eec3ff397ebb969613d728a95", "hash": "5943318eaf23764eec3ff397ebb969613d728a95",

47
src/config.d.ts vendored
View File

@ -1,47 +0,0 @@
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)
*
* @returns ESLint configuration object.
*/
export function extendConfig(
options:
| FlatESLintConfig[]
| {
auto?: boolean;
middlewares?: Middleware[];
configs: FlatESLintConfig[];
/**
* Use `.gitignore` file to exclude files from ESLint.
*/
gitignore?: boolean;
},
): Promise<FlatESLintConfig[]>;
export const error = 'error';
export const warn = 'warn';
export const off = 'off';

View File

@ -1,40 +0,0 @@
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';
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,
},
};

View File

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

View File

@ -1,94 +0,0 @@
import fs from 'node:fs';
import path from 'node:path';
import type { Middleware } from './middleware';
import { reactQuery, storybook, vitest } from './presets/misc';
import { react, reactRefresh } from './presets/react';
const jsdoc = () => import('./presets/jsdoc');
const tailwind = () => import('./presets/tailwind');
const testingLibrary = () => import('./presets/testing-library');
const middlewares = {
react,
reactRefresh,
tailwind,
storybook,
reactQuery,
testingLibrary,
jsdoc,
vitest,
} satisfies {
[key: string]: Middleware;
};
export const envs: {
dependency: string;
eslintPlugin?: string;
middleware: keyof typeof middlewares;
}[] = [
{
dependency: 'react',
middleware: 'react',
},
{
dependency: '@vitejs/plugin-react',
middleware: 'reactRefresh',
},
{
dependency: 'tailwindcss',
eslintPlugin: 'eslint-plugin-tailwindcss',
middleware: 'tailwind',
},
{
dependency: 'storybook',
eslintPlugin: 'eslint-plugin-storybook',
middleware: 'storybook',
},
{
dependency: '@tanstack/react-query',
eslintPlugin: '@tanstack/eslint-plugin-query',
middleware: 'reactQuery',
},
{
dependency: '@testing-library/react',
eslintPlugin: 'eslint-plugin-testing-library',
middleware: 'testingLibrary',
},
{
dependency: 'vitest',
eslintPlugin: 'eslint-plugin-vitest',
middleware: 'vitest',
},
];
export function getProjectDependencies() {
const rootDir = process.cwd();
const pkgJsonPath = path.resolve(rootDir, 'package.json');
const pkgJson = fs.existsSync(pkgJsonPath)
? (JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')) as {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
})
: {};
return new Set(
Object.keys({
...pkgJson.dependencies,
...pkgJson.devDependencies,
...pkgJson.peerDependencies,
}),
);
}
export function* checkEnv(): Generator<Middleware> {
const deps = getProjectDependencies();
for (const { dependency, eslintPlugin, middleware } of envs) {
if (deps.has(dependency) && (!eslintPlugin || deps.has(eslintPlugin))) {
yield middlewares[middleware];
}
}
}

View File

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

View File

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

77
src/local/index.ts Normal file
View File

@ -0,0 +1,77 @@
import type { ESLint } from 'eslint';
import * as fs from 'node:fs';
import { resolve, basename, extname } from 'node:path';
import { glob } from 'fast-glob';
import { parseModule } from 'esprima';
import query from 'esquery';
import type { Node, Property } from 'estree';
// https://github.com/gulpjs/interpret
const transpilers = [
'esbin/register',
'esbuild-register',
'ts-node/register/transpile-only',
'@swc/register',
'sucrase/register',
'@babel/register',
'coffeescript/register',
];
function tryRequire() {
for (const candidate of transpilers) {
try {
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 (const file of glob.sync(customRuleFiles)) {
const module = unwrapDefault(require(file));
const name = module.name ?? basename(file, extname(file));
plugin.rules![name] = module;
}
}
main();
export = plugin;

View File

@ -1,10 +0,0 @@
import type { Linter } from 'eslint';
type MiddlewareResult = Linter.Config | Linter.Config[];
export type Middleware =
| (() => Promise<MiddlewareResult>)
| (() => Promise<{ default: MiddlewareResult }>);
// eslint-disable-next-line unicorn/prevent-abbreviations
export const def = <T>(module: { default: T }): T => module.default;

38
src/modules.d.ts vendored
View File

@ -1,4 +1,25 @@
// eslint-disable-next-line import-x/unambiguous 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';
}
declare module 'module' { declare module 'module' {
export function _resolveFilename( export function _resolveFilename(
request: string, request: string,
@ -13,18 +34,3 @@ declare module 'module' {
options?: Record<PropertyKey, unknown>, options?: Record<PropertyKey, unknown>,
): string; ): string;
} }
declare module 'eslint-plugin-storybook' {
import type { Linter } from 'eslint';
export const configs: {
/** @deprecated */
csf: Linter.Config;
/** @deprecated */
recommended: Linter.Config;
'flat/csf': Linter.Config;
'flat/recommended': Linter.Config;
'flat/csf-strict': Linter.Config;
'flat/addon-interactions': Linter.Config;
};
}

View File

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

View File

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

View File

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

View File

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

8
src/presets/import.ts Normal file
View File

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

View File

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

7
src/presets/local.ts Normal file
View File

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

View File

@ -1,17 +0,0 @@
import { def } from '../middleware';
import { defineConfig } from '../types';
export async function storybook() {
const { configs } = def(await import('eslint-plugin-storybook'));
return defineConfig([configs['flat/recommended']]);
}
export async function reactQuery() {
const { configs } = def(await import('@tanstack/eslint-plugin-query'));
return defineConfig(configs['flat/recommended']);
}
export async function vitest() {
const { configs } = def(await import('eslint-plugin-vitest'));
return defineConfig([configs.recommended]);
}

View File

@ -1,53 +1,9 @@
import type { ReactRulesObject } from '@aet/eslint-define-config/src/rules/react'; import { error, off } from '../constants';
import type { ReactRefreshRulesObject } from '@aet/eslint-define-config/src/rules/react-refresh'; import { ReactRules } from 'eslint-define-config/rules/react';
import type { Linter, ESLint } from 'eslint';
import { error, off, warn } from '../constants'; export const reactRules: Partial<ReactRules> = {
import { def } from '../middleware'; 'react/display-name': off,
import { defineConfig } from '../types'; 'react/no-children-prop': error,
'react/prop-types': off,
const reactRules: Partial<ReactRulesObject> = { 'react/react-in-jsx-scope': off,
'@eslint-react/no-missing-component-display-name': off,
'@eslint-react/no-children-prop': error,
'@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 unknown as Linter.Config,
hooks.flatConfigs.recommended,
a11y.flatConfigs.recommended,
{
name: 'eslint-rules/react',
files: ['*.tsx'],
rules: reactRules,
},
{
name: 'eslint-rules/react/test-files',
files: ['*.test.tsx'],
rules: {
'@eslint-react/no-clone-element': off,
'@eslint-react/no-create-ref': off,
},
},
]);
}
const refreshRules: Partial<ReactRefreshRulesObject> = {
'react-refresh/only-export-components': [warn, { allowConstantExport: true }],
};
export async function reactRefresh() {
const refreshPlugin = def(await import('eslint-plugin-react-refresh'));
return defineConfig({
name: 'eslint-rules/react-refresh',
plugins: {
'react-refresh': refreshPlugin as unknown as ESLint.Plugin,
},
rules: refreshRules,
});
}

View File

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

View File

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

View File

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

View File

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

View File

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

26
src/redirect.ts Normal file
View File

@ -0,0 +1,26 @@
import Module from 'module';
const { name } = [require][0]('./package.json');
const _resolveFilename = Module._resolveFilename;
const alias = new Set([
'eslint-import-resolver-typescript',
'eslint-plugin-import',
'eslint-plugin-jsx-a11y',
'eslint-plugin-local',
'eslint-plugin-n',
'eslint-plugin-react-hooks',
'eslint-plugin-react',
'eslint-plugin-rules',
]);
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);
};

15
src/rules/index.ts Normal file
View File

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

View File

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

View File

@ -1,5 +1,5 @@
import type { Rule } from 'eslint';
import type { ESLintUtils } from '@typescript-eslint/utils'; import type { ESLintUtils } from '@typescript-eslint/utils';
import type { Rule, Linter } from 'eslint';
export function defineRules(rules: { export function defineRules(rules: {
[ruleName: string]: Rule.RuleModule | ESLintUtils.RuleModule<string, unknown[]>; [ruleName: string]: Rule.RuleModule | ESLintUtils.RuleModule<string, unknown[]>;
@ -7,17 +7,6 @@ export function defineRules(rules: {
return 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[]) {
if (!config || (Array.isArray(config) && config.some(c => !c))) {
console.trace();
throw new Error('Config cannot be empty');
}
return config;
}
export function defineRule({ export function defineRule({
name, name,
create, create,

171
src/types/config/env.d.ts vendored Normal file
View File

@ -0,0 +1,171 @@
/**
* An environment provides predefined global variables.
*
* @see [Environments](https://eslint.org/docs/user-guide/configuring/language-options#specifying-environments)
*/
export interface Environments extends Partial<Record<string, boolean>> {
/**
* Browser global variables.
*/
browser?: boolean;
/**
* Node.js global variables and Node.js scoping.
*/
node?: boolean;
/**
* CommonJS global variables and CommonJS scoping (use this for browser-only code that uses Browserify/WebPack).
*/
commonjs?: boolean;
/**
* Globals common to both Node.js and Browser.
*/
'shared-node-browser'?: boolean;
/**
* Enable all ECMAScript 6 features except for modules (this automatically sets the `ecmaVersion` parser option to 6).
*/
es6?: boolean;
/**
* Adds all ECMAScript 2016 globals and automatically sets the `ecmaVersion` parser option to 7.
*/
es2016?: boolean;
/**
* Adds all ECMAScript 2017 globals and automatically sets the `ecmaVersion` parser option to 8.
*/
es2017?: boolean;
/**
* Adds all ECMAScript 2018 globals and automatically sets the `ecmaVersion` parser option to 9.
*/
es2018?: boolean;
/**
* Adds all ECMAScript 2019 globals and automatically sets the `ecmaVersion` parser option to 10.
*/
es2019?: boolean;
/**
* Adds all ECMAScript 2020 globals and automatically sets the `ecmaVersion` parser option to 11.
*/
es2020?: boolean;
/**
* Adds all ECMAScript 2021 globals and automatically sets the `ecmaVersion` parser option to 12.
*/
es2021?: boolean;
/**
* Adds all ECMAScript 2022 globals and automatically sets the `ecmaVersion` parser option to 13.
*/
es2022?: boolean;
/**
* Adds all ECMAScript 2023 globals and automatically sets the `ecmaVersion` parser option to 14.
*/
es2023?: boolean;
/**
* Web workers global variables.
*/
worker?: boolean;
/**
* Defines `require()` and `define()` as global variables as per the amd spec.
*/
amd?: boolean;
/**
* Adds all of the Mocha testing global variables.
*/
mocha?: boolean;
/**
* Adds all of the Jasmine testing global variables for version 1.3 and 2.0.
*/
jasmine?: boolean;
/**
* Jest global variables.
*/
jest?: boolean;
/**
* PhantomJS global variables.
*/
phantomjs?: boolean;
/**
* Protractor global variables.
*/
protractor?: boolean;
/**
* QUnit global variables.
*/
qunit?: boolean;
/**
* jQuery global variables.
*/
jquery?: boolean;
/**
* Prototype.js global variables.
*/
prototypejs?: boolean;
/**
* ShellJS global variables.
*/
shelljs?: boolean;
/**
* Meteor global variables.
*/
meteor?: boolean;
/**
* MongoDB global variables.
*/
mongo?: boolean;
/**
* AppleScript global variables.
*/
applescript?: boolean;
/**
* Java 8 Nashorn global variables.
*/
nashorn?: boolean;
/**
* Service Worker global variables.
*/
serviceworker?: boolean;
/**
* Atom test helper globals.
*/
atomtest?: boolean;
/**
* Ember test helper globals.
*/
embertest?: boolean;
/**
* WebExtensions globals.
*/
webextensions?: boolean;
/**
* GreaseMonkey globals.
*/
greasemonkey?: boolean;
}

View File

@ -0,0 +1,6 @@
/**
* Eslint EslintComments extends.
*
* @see [Eslint EslintComments extends](https://mysticatea.github.io/eslint-plugin-eslint-comments/#%F0%9F%93%96-usage)
*/
export type EslintCommentsExtends = 'plugin:eslint-comments/recommended';

View File

@ -0,0 +1,11 @@
/**
* Eslint GraphQL extends.
*
* @see [Eslint GraphQL extends](https://the-guild.dev/graphql/eslint/docs/configs)
*/
export type GraphqlExtends =
| 'plugin:@graphql-eslint/operations-all'
| 'plugin:@graphql-eslint/operations-recommended'
| 'plugin:@graphql-eslint/relay'
| 'plugin:@graphql-eslint/schema-all'
| 'plugin:@graphql-eslint/schema-recommended';

View File

@ -0,0 +1,9 @@
/**
* Eslint import extends.
*
* @see [Eslint import extends](https://github.com/benmosher/eslint-plugin-import#installation)
*/
export type ImportExtends =
| 'plugin:import/errors'
| 'plugin:import/warnings'
| 'plugin:import/typescript';

View File

@ -0,0 +1,6 @@
/**
* Eslint JSDoc extends.
*
* @see [Eslint JSDoc extends](https://github.com/gajus/eslint-plugin-jsdoc#configuration)
*/
export type JsdocExtends = 'plugin:jsdoc/recommended';

View File

@ -0,0 +1,13 @@
/**
* Eslint Jsonc extends.
*
* @see [Eslint Jsonc extends](https://github.com/ota-meshi/eslint-plugin-jsonc#configuration)
*/
export type JsoncExtends =
| 'plugin:jsdoc/base'
| 'plugin:jsdoc/recommended'
| 'plugin:jsonc/recommended-with-json'
| 'plugin:jsonc/recommended-with-jsonc'
| 'plugin:jsonc/recommended-with-json5'
| 'plugin:jsonc/prettier'
| 'plugin:jsonc/all';

View File

@ -0,0 +1,8 @@
/**
* Eslint JSX A11y extends.
*
* @see [Eslint JSX A11y extends](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y)
*/
export type JsxA11yExtends =
| 'plugin:jsx-a11y/strict'
| 'plugin:jsx-a11y/recommended';

View File

@ -0,0 +1,10 @@
/**
* Eslint MDX extends.
*
* @see [Eslint MDX extends](https://github.com/mdx-js/eslint-mdx/tree/master/packages/eslint-plugin-mdx)
*/
export type MdxExtends =
| 'plugin:mdx/base'
| 'plugin:mdx/code-blocks'
| 'plugin:mdx/overrides'
| 'plugin:mdx/recommended';

View File

@ -0,0 +1,9 @@
/**
* Eslint N (Node) extends.
*
* @see [Eslint N extends](https://github.com/eslint-community/eslint-plugin-n#-configs)
*/
export type NExtends =
| 'plugin:n/recommended'
| 'plugin:n/recommended-module'
| 'plugin:n/recommended-script';

View File

@ -0,0 +1,9 @@
/**
* Eslint Node extends.
*
* @see [Eslint Node extends](https://github.com/mysticatea/eslint-plugin-node#-configs)
*/
export type NodeExtends =
| 'plugin:node/recommended'
| 'plugin:node/recommended-module'
| 'plugin:node/recommended-script';

View File

@ -0,0 +1,6 @@
/**
* Eslint Prettier extends.
*
* @see [Eslint Prettier extends](https://github.com/prettier/eslint-plugin-prettier#recommended-configuration)
*/
export type PrettierExtends = 'plugin:prettier/recommended' | 'prettier';

View File

@ -0,0 +1,6 @@
/**
* Eslint promise extends.
*
* @see [Eslint promise extends](https://github.com/eslint-community/eslint-plugin-promise#usage)
*/
export type PromiseExtends = 'plugin:promise/recommended';

Some files were not shown because too many files have changed in this diff Show More