Compare commits

..

1 Commits
main ... inline

Author SHA1 Message Date
Alex
179cf83891 Inline repo 2024-04-19 21:42:48 -04:00
141 changed files with 16228 additions and 7932 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 }]
}
}

5
.gitignore vendored
View File

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

3
.npmrc
View File

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

View File

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

View File

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

View File

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

5
dist/default.d.ts vendored
View File

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

3
dist/default.js vendored
View File

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

2
dist/eslint-init.sh vendored
View File

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

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 {};

107
dist/package.json vendored
View File

@ -1,81 +1,60 @@
{
"name": "@aet/eslint-rules",
"version": "2.0.51",
"version": "0.0.24-beta.1",
"license": "UNLICENSED",
"type": "module",
"bin": {
"eslint-init": "eslint-init.sh",
"eslint-install": "install.js",
"eslint-print": "print-config.sh"
},
"peerDependencies": {
"eslint": "^9.15.0",
"typescript": "^5.7.2"
},
"exports": {
".": "./config/index.js",
"./default": "./default.js",
"./prettier": "./prettier.js",
"./tsconfig": "./tsconfig.json",
"./types": "./types.js"
},
"optionalDependencies": {
"@tanstack/eslint-plugin-query": "^5.62.1"
"eslint": "^8.57.0",
"typescript": "^5.4.4"
},
"dependencies": {
"@aet/eslint-define-config": "^0.1.15",
"@antfu/install-pkg": "^1.1.0",
"@eslint-community/eslint-utils": "^4.7.0",
"@eslint-react/eslint-plugin": "1.51.3",
"@eslint/js": "^9.28.0",
"@nolyfill/is-core-module": "^1.0.39",
"@stylistic/eslint-plugin": "^4.4.1",
"@typescript-eslint/eslint-plugin": "^8.34.0",
"@typescript-eslint/parser": "^8.34.0",
"@typescript-eslint/type-utils": "^8.34.0",
"@typescript-eslint/utils": "^8.34.0",
"aria-query": "^5.3.2",
"axe-core": "^4.10.3",
"axobject-query": "4.1.0",
"@eslint-community/eslint-utils": "^4.4.0",
"@types/eslint": "^8.56.9",
"@typescript-eslint/eslint-plugin": "^7.7.0",
"@typescript-eslint/parser": "^7.7.0",
"@typescript-eslint/type-utils": "^7.7.0",
"@typescript-eslint/utils": "^7.7.0",
"aria-query": "^5.3.0",
"axe-core": "^4.9.0",
"axobject-query": "^4.0.0",
"damerau-levenshtein": "1.0.8",
"debug": "^4.4.1",
"debug": "^4.3.4",
"doctrine": "^3.0.0",
"emoji-regex": "^10.4.0",
"enhanced-resolve": "^5.18.1",
"eslint-config-prettier": "^10.1.5",
"emoji-regex": "^10.3.0",
"enhanced-resolve": "^5.16.0",
"eslint-config-prettier": "^9.1.0",
"eslint-define-config": "^1.24.1",
"eslint-import-resolver-node": "^0.3.9",
"eslint-import-resolver-typescript": "^4.4.3",
"eslint-module-utils": "^2.12.0",
"eslint-plugin-import-x": "^4.15.1",
"eslint-plugin-regexp": "^2.9.0",
"eslint-plugin-unicorn": "^59.0.1",
"eslint-module-utils": "^2.8.1",
"eslint-plugin-es-x": "^7.6.0",
"eslint-plugin-jsdoc": "^48.2.3",
"eslint-plugin-unicorn": "^52.0.0",
"esprima": "^4.0.1",
"esquery": "^1.6.0",
"esquery": "^1.5.0",
"estraverse": "^5.3.0",
"fast-glob": "^3.3.3",
"get-tsconfig": "^4.10.1",
"globals": "^16.2.0",
"ignore": "^7.0.5",
"is-bun-module": "^2.0.0",
"fast-glob": "^3.3.2",
"get-tsconfig": "^4.7.3",
"ignore": "^5.3.1",
"is-builtin-module": "^3.2.1",
"is-glob": "^4.0.3",
"language-tags": "^2.1.0",
"lodash-es": "^4.17.21",
"minimatch": "^10.0.1",
"semver": "^7.7.2",
"typescript-eslint": "^8.34.0"
"language-tags": "^1.0.9",
"lodash": "^4.17.21",
"minimatch": "^9.0.4",
"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": {
"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 }))"

19
dist/tsconfig.json vendored
View File

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

2
dist/types.d.ts vendored
View File

@ -6,7 +6,7 @@ import { Rule } from 'eslint';
export declare function defineRules(rules: {
[ruleName: string]: Rule.RuleModule | ESLintUtils.RuleModule<string, unknown[]>;
}): {
[ruleName: string]: Rule.RuleModule | ESLintUtils.RuleModule<string, unknown[], unknown, ESLintUtils.RuleListener>;
[ruleName: string]: Rule.RuleModule | ESLintUtils.RuleModule<string, unknown[], ESLintUtils.RuleListener>;
};
export declare function defineRule({ name, create, ...meta }: Rule.RuleMetaData & {
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,98 +1,49 @@
{
"name": "@aet/eslint-configs",
"type": "module",
"version": "0.0.0",
"scripts": {
"build": "./scripts/build.ts",
"check-import": "./scripts/check-imports.ts",
"define": "/usr/local/bin/codium ./packages/eslint-define-config",
"sync": "./scripts/sync-deps.ts",
"do": "(cd dist && ver bump && npm publish && ver unpub)"
"build": "./scripts/build-all.ts",
"build-types": "cd ./packages/eslint-define-config && ./scripts/index.ts",
"check-import": "./scripts/check-imports.ts"
},
"private": true,
"dependencies": {
"@aet/eslint-define-config": "^0.1.16",
"@antfu/install-pkg": "^1.1.0",
"@eslint-community/eslint-utils": "^4.7.0",
"@eslint-react/eslint-plugin": "1.52.2",
"@eslint/js": "^9.28.0",
"@nolyfill/is-core-module": "^1.0.39",
"@stylistic/eslint-plugin": "^4.4.1",
"@typescript-eslint/eslint-plugin": "^8.34.0",
"@typescript-eslint/parser": "^8.34.0",
"@typescript-eslint/type-utils": "^8.34.0",
"@typescript-eslint/utils": "^8.34.0",
"aria-query": "^5.3.2",
"axe-core": "^4.10.3",
"axobject-query": "4.1.0",
"damerau-levenshtein": "1.0.8",
"debug": "^4.4.1",
"doctrine": "^3.0.0",
"emoji-regex": "^10.4.0",
"enhanced-resolve": "^5.18.1",
"eslint-config-prettier": "^10.1.5",
"eslint-import-resolver-node": "^0.3.9",
"eslint-import-resolver-typescript": "^4.4.3",
"eslint-module-utils": "^2.12.0",
"eslint-plugin-import-x": "^4.15.2",
"eslint-plugin-regexp": "^2.9.0",
"eslint-plugin-unicorn": "^59.0.1",
"esprima": "^4.0.1",
"esquery": "^1.6.0",
"estraverse": "^5.3.0",
"fast-glob": "^3.3.3",
"get-tsconfig": "^4.10.1",
"globals": "^16.2.0",
"ignore": "^7.0.5",
"is-bun-module": "^2.0.0",
"is-glob": "^4.0.3",
"language-tags": "^2.1.0",
"lodash-es": "^4.17.21",
"minimatch": "^10.0.3",
"semver": "^7.7.2",
"typescript-eslint": "^8.34.0"
},
"devDependencies": {
"@babel/core": "^7.27.4",
"@babel/plugin-transform-flow-strip-types": "^7.27.1",
"@babel/preset-env": "^7.27.2",
"@graphql-eslint/eslint-plugin": "^4.4.0",
"@swc-node/register": "^1.10.10",
"@tanstack/eslint-plugin-query": "^5.78.0",
"@babel/core": "^7.24.4",
"@babel/plugin-transform-flow-strip-types": "^7.24.1",
"@babel/preset-env": "^7.24.4",
"@types/babel-plugin-macros": "^3.1.3",
"@types/babel__core": "^7.20.5",
"@types/eslint-config-prettier": "^6.11.3",
"@types/eslint-plugin-tailwindcss": "^3.17.0",
"@types/eslint": "^8.56.10",
"@types/esprima": "^4.0.6",
"@types/esquery": "^1.5.4",
"@types/estree": "^1.0.8",
"@types/esquery": "^1.5.3",
"@types/estree": "^1.0.5",
"@types/estree-jsx": "^1.0.5",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.15.30",
"@types/react-refresh": "^0.14.6",
"@typescript-eslint/types": "^8.34.0",
"@typescript-eslint/typescript-estree": "^8.34.0",
"@vitest/eslint-plugin": "^1.2.4",
"@types/lodash": "^4.17.0",
"@types/node": "^20.12.7",
"@typescript-eslint/eslint-plugin": "7.7.0",
"@typescript-eslint/type-utils": "^7.7.0",
"@typescript-eslint/types": "^7.7.0",
"@typescript-eslint/typescript-estree": "^7.7.0",
"@typescript-eslint/utils": "^7.7.0",
"babel-plugin-macros": "^3.1.0",
"dts-bundle-generator": "9.5.1",
"esbuild": "0.25.5",
"dts-bundle-generator": "^9.5.0",
"esbin": "0.0.4",
"esbuild": "0.20.2",
"esbuild-plugin-alias": "^0.2.1",
"eslint": "^9.28.0",
"eslint-plugin-jsdoc": "^50.7.1",
"eslint-plugin-lingui": "^0.10.1",
"eslint-plugin-react-refresh": "^0.4.20",
"eslint-plugin-storybook": "^9.0.9",
"eslint-plugin-testing-library": "^7.5.2",
"graphql": "^16.11.0",
"json-schema-to-ts": "^3.1.1",
"nolyfill": "^1.0.44",
"eslint": "8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-define-config": "file:./src/types",
"esprima": "^4.0.1",
"esquery": "^1.5.0",
"fast-glob": "^3.3.2",
"find-cache-dir": "^5.0.0",
"json-schema-to-ts": "^3.0.1",
"lodash": "^4.17.21",
"minimatch": "^9.0.4",
"patch-package": "^8.0.0",
"picocolors": "^1.1.1",
"prettier": "^3.5.3",
"picocolors": "^1.0.0",
"prettier": "^3.2.5",
"prop-types": "^15.8.1",
"terser": "^5.42.0",
"type-fest": "^4.41.0",
"typescript": "^5.8.3"
"typescript": "^5.4.5"
},
"prettier": {
"arrowParens": "avoid",
@ -104,22 +55,15 @@
},
"pnpm": {
"overrides": {
"@typescript-eslint/utils": "8.0.0",
"function-bind": "npm:@nolyfill/function-bind@^1",
"has-proto": "npm:@nolyfill/has-proto@^1",
"has-symbols": "npm:@nolyfill/has-symbols@^1",
"hasown": "npm:@nolyfill/hasown@^1",
"isarray": "npm:@nolyfill/isarray@^1",
"jsonify": "npm:@nolyfill/jsonify@^1",
"object-keys": "npm:@nolyfill/object-keys@^1",
"set-function-length": "npm:@nolyfill/set-function-length@^1",
"@babel/types": "7.25.2",
"is-core-module": "npm:@nolyfill/is-core-module@^1",
"json-stable-stringify": "npm:@nolyfill/json-stable-stringify@^1"
},
"patchedDependencies": {
"@typescript-eslint/typescript-estree@8.0.0": "patches/@typescript-eslint__typescript-estree@8.0.0.patch",
"dts-bundle-generator": "patches/dts-bundle-generator.patch"
"function-bind": "npm:@nolyfill/function-bind@latest",
"has-proto": "npm:@nolyfill/has-proto@latest",
"has-symbols": "npm:@nolyfill/has-symbols@latest",
"hasown": "npm:@nolyfill/hasown@latest",
"isarray": "npm:@nolyfill/isarray@latest",
"jsonify": "npm:@nolyfill/jsonify@latest",
"object-keys": "npm:@nolyfill/object-keys@latest",
"set-function-length": "npm:@nolyfill/set-function-length@latest",
"@babel/types": "7.24.0"
}
}
}

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;
}

@ -0,0 +1 @@
Subproject commit 7a02ac08b5aaac8c217f0e87142f97eafcc38fbc

@ -0,0 +1 @@
Subproject commit f77ceb679d59ced5d9a633123385470a9eea10d9

@ -1 +1 @@
Subproject commit a7d1a12a6198d546c4a06477b385b4fde03b762e
Subproject commit 0d5321a5457c5f0da0ca216053cc5b4f571b53ae

@ -1 +1 @@
Subproject commit 42464abe64c5cefb709e8e0a9072b6bb1cd7fcdc
Subproject commit eb11b5b35a6a797dc7fba6df53b1c4dada3a2a55

@ -0,0 +1 @@
Subproject commit 4467db503e38b9356517cf6926d11be544ccf4b1

View File

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

View File

@ -6,6 +6,7 @@
*/
/* global BigInt */
/* eslint-disable no-for-of-loops/no-for-of-loops */
import type { Rule, Scope } from 'eslint';
import type {
CallExpression,
@ -15,7 +16,6 @@ import type {
Identifier,
BaseFunction,
} from 'estree';
import { __EXPERIMENTAL__ } from './index';
/**
@ -24,7 +24,7 @@ import { __EXPERIMENTAL__ } from './index';
*/
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);
}
function isReactFunction(node: Expression | Super, functionName: string) {
function isReactFunction(node: Node, functionName: string) {
return (
(node as Identifier).name === functionName ||
(node.type === 'MemberExpression' &&
@ -91,8 +91,10 @@ function isMemoCallback(node: Rule.Node) {
function isInsideComponentOrHook(node: Rule.Node) {
while (node) {
const functionName = getFunctionName(node);
if (functionName && (isComponentName(functionName) || isHook(functionName))) {
return true;
if (functionName) {
if (isComponentName(functionName) || isHook(functionName)) {
return true;
}
}
if (isForwardRefCallback(node) || isMemoCallback(node)) {
return true;
@ -110,7 +112,7 @@ function isUseEffectEventIdentifier(node: Node) {
}
function isUseIdentifier(node: Node) {
return isReactFunction(node as Expression, 'use');
return isReactFunction(node, 'use');
}
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 {
// Maintain code segment path stack as we traverse.
onCodePathSegmentStart: segment => codePathSegmentStack.push(segment),
@ -487,7 +473,7 @@ const rule: Rule.RuleModule = {
context.report({
node: hook,
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. ' +
'React Hooks must be called in the exact same order in ' +
'every component render.',
@ -506,7 +492,7 @@ const rule: Rule.RuleModule = {
context.report({
node: hook,
message:
`React Hook "${getSource(hook)}" cannot be ` +
`React Hook "${context.getSource(hook)}" cannot be ` +
'called in an async function.',
});
}
@ -521,7 +507,7 @@ const rule: Rule.RuleModule = {
!isUseIdentifier(hook) // `use(...)` can be called conditionally.
) {
const message =
`React Hook "${getSource(hook)}" is called ` +
`React Hook "${context.getSource(hook)}" is called ` +
'conditionally. React Hooks must be called in the exact ' +
'same order in every component render.' +
(possiblyHasEarlyReturn
@ -538,15 +524,15 @@ const rule: Rule.RuleModule = {
) {
// Custom message for hooks inside a class
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 ' +
'React function component or a custom React Hook function.';
context.report({ node: hook, message });
} else if (codePathFunctionName) {
// Custom message if we found an invalid function name.
const message =
`React Hook "${getSource(hook)}" is called in ` +
`function "${getSource(codePathFunctionName)}" ` +
`React Hook "${context.getSource(hook)}" is called in ` +
`function "${context.getSource(codePathFunctionName)}" ` +
'that is neither a React function component nor a custom ' +
'React Hook function.' +
' React component names must start with an uppercase letter.' +
@ -555,7 +541,7 @@ const rule: Rule.RuleModule = {
} else if (codePathNode.type === 'Program') {
// These are dangerous if you have inline requires enabled.
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 ' +
'React function component or a custom React Hook function.';
context.report({ node: hook, message });
@ -568,7 +554,7 @@ const rule: Rule.RuleModule = {
// `use(...)` can be called in callbacks.
if (isSomewhereInsideComponentOrHook && !isUseIdentifier(hook)) {
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 ' +
'React function component or a custom React Hook function.';
context.report({ node: hook, message });
@ -620,7 +606,7 @@ const rule: Rule.RuleModule = {
context.report({
node,
message:
`\`${getSource(
`\`${context.getSource(
node,
)}\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
'the same component. They cannot be assigned to variables or passed down.',
@ -637,14 +623,14 @@ const rule: Rule.RuleModule = {
FunctionDeclaration(node) {
// function MyComponent() { const onClick = useEffectEvent(...) }
if (isInsideComponentOrHook(node)) {
recordAllUseEffectEventFunctions(getScope(node));
recordAllUseEffectEventFunctions(context.getScope());
}
},
ArrowFunctionExpression(node) {
// const MyComponent = () => { const onClick = useEffectEvent(...) }
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
* LICENSE file in the root directory of this source tree.
*/
import { Linter } from 'eslint';
import ExhaustiveDeps from './ExhaustiveDeps';
import { name, version } from './package.json';
import type { Linter } from 'eslint';
import RulesOfHooks from './RulesOfHooks';
import ExhaustiveDeps from './ExhaustiveDeps';
export const __EXPERIMENTAL__ = false;
export const flatConfigs = {
export const configs = {
recommended: {
name: 'react-hooks/recommended',
plugins: {
'react-hooks': {
meta: { name, version },
rules: {
'rules-of-hooks': RulesOfHooks,
'exhaustive-deps': ExhaustiveDeps,
},
},
},
plugins: ['react-hooks'],
rules: {
'react-hooks/rules-of-hooks': 'error',
'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": {
"version": 1,
"comment": "https://github.com/facebook/react/pull/30774",
"sources": {
"main": {
"repository": "git@github.com:facebook/react.git",
"commit": "899cb95f52cc83ab5ca1eb1e268c909d3f0961e7",
"repository": "https://github.com/facebook/react",
"commit": "0e0b69321a6fcfe8a3eaae3b1016beb110437b38",
"branch": "main"
}
}

@ -1 +1 @@
Subproject commit a8ca8f70331b02db537b0b5cf72ea10e3d6c9377
Subproject commit 5943318eaf23764eec3ff397ebb969613d728a95

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

View File

@ -1,8 +1,8 @@
diff --git a/lib/index.js b/lib/index.js
index de95218..e30a3df 100644
index 49fd4c7..a0fdd81 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -1,17 +1,17 @@
@@ -1,9 +1,9 @@
"use strict"
-const pkg = require("../package.json")
@ -14,110 +14,16 @@ index de95218..e30a3df 100644
+import cjsConfig from "./configs/recommended-script"
+import recommendedConfig from "./configs/recommended"
/** @import { ESLint, Linter } from 'eslint' */
/** @type {ESLint.Plugin} */
const base = {
/**
* @typedef {{
@@ -20,8 +20,8 @@ const recommendedConfig = require("./configs/recommended")
/** @type {import('eslint').ESLint.Plugin & { configs: Configs }} */
const plugin = {
meta: {
- name: pkg.name,
- version: pkg.version,
+ name,
+ version,
},
rules: {
rules: /** @type {Record<string, import('eslint').Rule.RuleModule>} */ ({
"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;

8533
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 { resolve, relative } from 'node:path';
import { isBuiltin } from 'node:module';
import { relative, resolve } from 'node:path';
import esbuild from 'esbuild';
import type { Plugin } from 'esbuild';
import { memoize } from 'lodash-es';
import c from 'picocolors';
import { minify_sync } from 'terser';
import { memoize } from 'lodash';
import { gray, green } from 'picocolors';
import { dependencies } from '../dist/package.json';
import { dts } from './dts';
import { babelPlugin } from './modifier';
const ENV = (process.env.NODE_ENV ??= 'production');
const PROD = ENV === 'production';
const { gray, green } = c;
declare global {
interface Array<T> {
filter(
@ -70,12 +65,14 @@ if (process.env.DEBUG) {
});
}
async function bundle(
function bundle(
entry: string,
outfile: string,
options?: esbuild.BuildOptions & { treeShaking?: boolean },
outfile = entry
.replace('./packages/', './dist/')
.replace('src/', '')
.replace('.ts', '.js'),
) {
const output = await esbuild.build({
return esbuild.build({
entryPoints: [entry],
outfile,
bundle: true,
@ -86,36 +83,11 @@ async function bundle(
plugins,
define: {},
alias: {},
format: 'esm',
external: ['find-cache-dir'],
banner: {
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() {
@ -138,58 +110,44 @@ async function editPackageJson() {
}
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);
return [state, setState] as const;
}
function bundleType(source: string, output: string) {
try {
return dts({
source,
dist: output,
project: './tsconfig.build.json',
});
} catch {
// noop
}
return dts({
source,
dist: output,
project: './tsconfig.build.json',
});
}
async function main() {
console.log('Building type definitions…');
try {
await fs.rm('dist/config', { recursive: true });
} catch {
// noop
}
console.log('Building type definitions...');
bundleType('./src/index.ts', './dist/index.d.ts');
bundleType('./src/prettier.ts', './dist/prettier.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([
bundle('./src/index.ts', undefined!, {
format: 'esm',
splitting: true,
outdir: './dist/config',
...unminify,
}),
bundle('./src/types.ts', './dist/types.js', unminify),
bundle('./src/prettier.ts', './dist/prettier.js', unminify),
bundle('./src/install.ts', './dist/install.js', {
treeShaking: true,
minify: false,
banner: {
js: '#!/usr/bin/env node\n/* eslint-disable */',
},
}),
bundle('./packages/eslint-plugin-react/index.js'),
bundle('./packages/eslint-plugin-import/src/index.js'),
bundle('./packages/eslint-plugin-jsx-a11y/src/index.js'),
bundle('./packages/eslint-plugin-react-hooks/index.ts'),
bundle('./packages/eslint-plugin-n/lib/index.js', './dist/eslint-plugin-n/index.js'),
bundle('./packages/eslint-import-resolver-typescript/src/index.ts'),
bundle('./src/rules/index.ts', './dist/eslint-plugin-rules/index.js'),
bundle('./src/local/index.ts', './dist/eslint-plugin-local/index.js'),
bundle('./src/index.ts', './dist/index.js'),
bundle('./src/types.ts', './dist/types.js'),
bundle('./src/prettier.ts', './dist/prettier.js'),
editPackageJson(),
]);
// bundleType('./src/index.ts', './dist/config/index.d.ts');
await fs.copyFile('./src/config.d.ts', './dist/config/index.d.ts');
console.log('Removing redirect...');
const [distIndex, setDistIndex] = await useText('./dist/index.js');
await setDistIndex(distIndex.replace(/import.*redirect.*;/g, ''));
}
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
import fs from 'node:fs';
import { builtinModules } from 'node:module';
import glob from 'fast-glob';
import { uniq } from 'lodash-es';
import { dependencies, peerDependencies } from '../dist/package.json';
import fs from 'fs';
import { builtinModules } from 'module';
import { uniq } from 'lodash';
import { dependencies, peerDependencies, overrides } from '../dist/package.json';
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}`]));
function findRequires(text: string) {

View File

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

View File

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

View File

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

View File

@ -3,6 +3,9 @@ sync() (
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-n
sync eslint-plugin-react
sync jsx-ast-utils

View File

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

View File

@ -1,32 +1,38 @@
{
"eslint-plugin-import": {
"hash": "6554bd5c30976290024cecc44ef1e96746cf3cf7",
"date": "2024-05-23T12:47:41-07:00",
"hash": "f77ceb679d59ced5d9a633123385470a9eea10d9",
"date": "2024-04-07T12:55:28+12:00",
"committer": "Jordan Harband",
"subject": "[meta] add `repository.directory` field"
"subject": "[actions] cancel in-progress runs on PR updates"
},
"eslint-import-resolver-typescript": {
"hash": "7a02ac08b5aaac8c217f0e87142f97eafcc38fbc",
"date": "2024-04-01T01:06:20+00:00",
"committer": "GitHub",
"subject": "chore(deps): update dependency npm-run-all2 to ^5.0.2 (#277)"
},
"eslint-plugin-jsx-a11y": {
"hash": "a7d1a12a6198d546c4a06477b385b4fde03b762e",
"date": "2025-06-05T12:28:53-07:00",
"hash": "0d5321a5457c5f0da0ca216053cc5b4f571b53ae",
"date": "2024-01-27T22:18:19-08:00",
"committer": "Jordan Harband",
"subject": "[Tests] fix linting errors introduced in 2d9ad55"
"subject": "[Deps] update `@babel/runtime`, `safe-regex-test`"
},
"eslint-plugin-n": {
"hash": "42464abe64c5cefb709e8e0a9072b6bb1cd7fcdc",
"date": "2025-06-13T01:37:54+08:00",
"hash": "eb11b5b35a6a797dc7fba6df53b1c4dada3a2a55",
"date": "2024-04-17T17:40:32+08:00",
"committer": "GitHub",
"subject": "chore(master): release 17.20.0 (#448)"
"subject": "chore: upgrade globals v15 (#241)"
},
"eslint-plugin-react": {
"hash": "983b88dd3cb5e07919517d3fde4085f60883ded7",
"date": "2024-07-24T15:26:33-07:00",
"hash": "4467db503e38b9356517cf6926d11be544ccf4b1",
"date": "2024-03-16T12:54:58+09:00",
"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": {
"hash": "a8ca8f70331b02db537b0b5cf72ea10e3d6c9377",
"date": "2025-02-20T08:51:06-08:00",
"hash": "5943318eaf23764eec3ff397ebb969613d728a95",
"date": "2023-07-28T18:34:04-07:00",
"committer": "Jordan Harband",
"subject": "[Dev Deps] pin `psl` due to breaking change in a minor version"
"subject": "v3.3.5"
}
}

64
src/config.d.ts vendored
View File

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

View File

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

View File

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

View File

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

View File

@ -1,168 +1,214 @@
/* eslint-disable import-x/no-named-as-default-member */
/// <reference path="./modules.d.ts" />
import './redirect';
import fs from 'node:fs';
import type { FlatESLintConfig } from '@aet/eslint-define-config';
import * as tsParser from '@typescript-eslint/parser';
import prettier from 'eslint-config-prettier';
import importPlugin from 'eslint-plugin-import-x';
import * as regexpPlugin from 'eslint-plugin-regexp';
import { uniq } from 'lodash-es';
import tseslint from 'typescript-eslint';
import type { ExtendConfigOptions, NormalizedExtendConfigOptions } from './config';
import { off } from './constants';
import { checkEnv, fromEnvironments } from './environment';
import type { Middleware } from './middleware';
import type { Rule } from 'eslint';
import type { ESLintUtils } from '@typescript-eslint/utils';
import type { ESLintConfig, Rules } from 'eslint-define-config';
import { typescriptRules } from './presets/typescript';
import { unicornRules } from './presets/unicorn';
import { eslintRules } from './presets/eslint';
import stylistic from './presets/stylistic';
import { importRules, typescriptRules } from './presets/typescript';
import unicorn from './presets/unicorn';
import { reactRules } from './presets/react';
import { importRules } from './presets/import';
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 };
function normalizeExtendConfig(
options: ExtendConfigOptions,
): NormalizedExtendConfigOptions {
if (Array.isArray(options)) {
options = { configs: options };
} else if ('rules' in options) {
options = { configs: [options] };
declare global {
interface Array<T> {
filter(
predicate: BooleanConstructor,
): Exclude<T, null | undefined | false | '' | 0>[];
}
const {
auto = true,
middlewares = [],
configs = [],
gitignore = true,
env,
} = options as NormalizedExtendConfigOptions;
return {
auto,
middlewares,
configs,
gitignore,
env,
};
}
export async function extendConfig(
options: ExtendConfigOptions = [],
): Promise<FlatESLintConfig[]> {
const unique = (...arr: (false | undefined | string | string[])[]): string[] => [
...new Set(arr.flat(1).filter(Boolean)),
];
const ensureArray = <T>(value?: T | T[]): T[] =>
value == null ? [] : Array.isArray(value) ? value : [value];
type RuleLevel = 'error' | 'warn' | 'off' | 0 | 1 | 2;
type RuleEntry<Options> = RuleLevel | [RuleLevel, Partial<Options>];
declare module 'eslint-define-config' {
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 {
auto,
middlewares: addMiddlewares = [],
configs,
gitignore,
env,
} = normalizeExtendConfig(options);
plugins = [],
settings,
rules,
extends: _extends,
overrides,
customRuleFiles,
...rest
} = of;
const middlewares: Middleware[] = uniq([
() => import('./presets/custom'),
...(auto ? checkEnv() : []),
...fromEnvironments(env),
...addMiddlewares,
]);
const result: FlatESLintConfig[] = [
{
name: 'eslint-rules/eslint',
rules: eslintRules,
},
...tseslint.configs.recommendedTypeChecked,
importPlugin.flatConfigs.recommended,
importPlugin.flatConfigs.react,
importPlugin.flatConfigs.typescript,
...unicorn,
stylistic,
{
plugins: { regexp: regexpPlugin },
rules: {
...regexpPlugin.configs['flat/recommended'].rules,
// https://github.com/ota-meshi/eslint-plugin-regexp/issues/445
'regexp/strict': off,
'regexp/match-any': off,
// https://github.com/ota-meshi/eslint-plugin-regexp/issues/743
'regexp/letter-case': off,
},
},
{
name: 'eslint-rules/typescript-and-import-x',
files: ['**/*.{js,mjs,cjs,jsx,ts,tsx,mts,cts}'],
languageOptions: {
parserOptions: {
parser: tsParser,
projectService: true,
ecmaVersion: 'latest',
tsconfigRootDir: import.meta.dirname,
sourceType: 'module',
},
},
settings: {
'import-x/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx', '.mts', '.cts'],
},
'import-x/resolver': {
typescript: true,
node: true,
},
'import-x/core-modules': ['node:sqlite'],
},
ignores: ['eslint.config.cjs'],
rules: {
...importRules,
...typescriptRules,
},
},
{
name: 'eslint-rules: Disable type checking',
files: ['**/*.js', '**/*.mjs', '**/*.cjs', '**/*.jsx'],
...tseslint.configs.disableTypeChecked,
rules: {
'import-x/no-commonjs': off,
'import-x/unambiguous': off,
'@typescript-eslint/no-require-imports': off,
'typed-custom/restrict-template-expressions': off,
...tseslint.configs.disableTypeChecked.rules,
},
},
{
name: 'eslint-rules/.d.ts-files',
files: ['**/*.d.ts'],
rules: {
'@typescript-eslint/consistent-type-imports': off,
'import-x/unambiguous': off,
},
},
] as FlatESLintConfig[];
const hasReact = plugins.includes('react');
const hasReactRefresh = plugins.includes('react-refresh');
const hasUnicorn = plugins.includes('unicorn');
const hasJsDoc = plugins.includes('jsdoc');
const hasGraphQL = plugins.includes('@graphql-eslint');
const hasNext = ensureArray(_extends).some(name => name.includes(':@next/next'));
const hasTailwind = ensureArray(_extends).some(name =>
name.includes('plugin:tailwindcss/'),
);
for (const middleware of middlewares) {
let fn = await middleware();
if ('default' in fn) {
fn = fn.default;
}
if (Array.isArray(fn)) {
result.push(...(fn as FlatESLintConfig[]).flat(Infinity));
} else {
result.push(fn as unknown as FlatESLintConfig);
}
const ruleDir = false; // ?? findCacheDirectory({ name: '_eslint-rules' });
if (ruleDir) {
fs.rmSync(ruleDir, { recursive: true, force: true });
fs.mkdirSync(ruleDir, { recursive: true });
}
if (configs?.length) {
result.push(...configs);
}
result.push(prettier);
if (gitignore && fs.existsSync('.gitignore')) {
const ignores = fs
.readFileSync('.gitignore', 'utf8')
.trim()
.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#'));
result.push({ ignores });
}
const result: InputConfig = {
root: true,
parser: '@typescript-eslint/parser',
plugins: unique('@typescript-eslint', 'import', 'rules', plugins),
env: { node: true, browser: true, es2023: true },
reportUnusedDisableDirectives: true,
parserOptions: {
project: true,
},
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: {
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx', '.mts', '.cts'],
},
'import/resolver': {
typescript: {
alwaysTryTypes: true,
},
},
react: {
version: 'detect',
},
...settings,
},
overrides: [
{
files: ['.eslintrc.js', '.eslintrc.cjs', '*.config.js', 'index.js'],
extends: ['plugin:@typescript-eslint/disable-type-checked'],
rules: {
'rules/restrict-template-expressions': off,
},
},
{
files: ['*.d.ts'],
rules: {
'@typescript-eslint/consistent-type-imports': off,
},
},
{
files: ['repl.ts', 'scripts/**/*.ts'],
rules: {
'no-console': off,
},
},
...(overrides ?? []),
],
rules: {
...eslintRules,
...typescriptRules,
...importRules,
...localRules,
...(hasReact && {
...reactRules,
'react/no-unknown-property': [
error,
{ ignore: hasNext ? ['css', 'next'] : ['css'] },
],
}),
...(hasReactRefresh && {
'react-refresh/only-export-components': [warn, { allowConstantExport: true }],
}),
...(hasUnicorn && unicornRules),
...(hasJsDoc && jsDocRules),
...(hasGraphQL && graphqlRules),
...(hasTailwind && tailwindRules),
...rules,
},
...rest,
};
return result;
}

View File

@ -1,22 +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(
_ =>
_.dependency.some(dep => deps.has(dep)) &&
_.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,9 +0,0 @@
import type { Linter } from 'eslint';
type MiddlewareResult = Linter.Config | Linter.Config[];
export type Middleware =
| (() => Promise<MiddlewareResult>)
| (() => Promise<{ default: MiddlewareResult }>);
export const def = <T>(module: { default: T }): T => module.default;

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

View File

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

View File

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

View File

@ -1,13 +1,4 @@
// Not usable. https://github.com/dimaMachina/graphql-eslint/issues/2178
import type { GraphQLRulesObject } from '@aet/eslint-define-config/src/rules/graphql-eslint';
import * as graphql from '@graphql-eslint/eslint-plugin';
import { defineConfig } from '../types';
import { GraphQLRules } from 'eslint-define-config/rules/graphql-eslint';
// https://the-guild.dev/graphql/eslint/rules
const graphqlRules: Partial<GraphQLRulesObject> = {};
export default defineConfig({
processor: graphql.processors.graphql,
rules: graphqlRules,
});
export const graphqlRules: Partial<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 module from 'eslint-plugin-jsdoc';
import { JSDocRules } from 'eslint-define-config/rules/jsdoc';
import { off } from '../constants';
import { defineConfig } from '../types';
const jsdocRules: Partial<JSDocRulesObject> = {
'jsdoc/require-jsdoc': off,
};
export default defineConfig([
module.configs['flat/recommended-typescript'],
{ name: 'eslint-rules/jsdoc', rules: jsdocRules },
]);
export const jsDocRules: Partial<JSDocRules> = {};

View File

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

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,30 +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('@vitest/eslint-plugin'));
return defineConfig([
configs.recommended,
{
rules: {
'vitest/expect-expect': [
'error',
{
assertFunctionNames: ['expect*', 'request.**.expect'],
additionalTestBlockFunctions: ['describe*'],
},
],
},
},
]);
}

View File

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

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 { defineConfig } from '../types';
const tailwindRules: Partial<TailwindRulesObject> = {
export const tailwindRules = {
'tailwindcss/no-custom-classname': off,
} as const;
export default defineConfig([
...tailwind.configs['flat/recommended'],
{
name: 'eslint-rules/tailwind',
rules: tailwindRules,
settings: {
tailwindcss: {
callees: ['classnames', 'clsx', 'tw', 'twx'],
classRegex: /^(css|class(Name)?)$/.source,
},
},
},
]);

View File

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

View File

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

View File

@ -10,15 +10,28 @@ const prettier: Config = {
plugins: [],
};
export default function defineConfig(
config: Partial<Config> & {
tailwind?: boolean;
},
) {
export default function defineConfig({
tailwind,
...config
}: Partial<Config> & {
tailwind?: boolean;
}) {
const result: Config = {
...prettier,
...config,
};
if (tailwind) {
ensureHas(result.plugins!, 'prettier-plugin-tailwindcss');
result.tailwindAttributes ??= ['css'];
result.tailwindFunctions ??= ['tw'];
}
return result;
}
function ensureHas<T>(list: T[], item: T) {
if (!list.includes(item)) {
list.push(item);
}
}

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
import * as ts from 'typescript';
import { ESLintUtils, type TSESTree, AST_NODE_TYPES } from '@typescript-eslint/utils';
import {
getConstrainedTypeAtLocation,
getTypeName,
isTypeAnyType,
isTypeFlagSet,
isTypeNeverType,
getConstrainedTypeAtLocation,
} 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 * as ts from 'typescript';
const createRule = ESLintUtils.RuleCreator(
name => `https://typescript-eslint.io/rules/${name}`,
@ -34,6 +29,8 @@ export default createRule<Option[], MessageId>({
type: 'problem',
docs: {
description: 'Enforce template literal expressions to be of `string` type',
recommended: 'recommended',
requiresTypeChecking: true,
},
messages: {
invalidType: 'Invalid type "{{type}}" of template literal expression.',
@ -57,16 +54,8 @@ export default createRule<Option[], MessageId>({
},
defaultOptions: [defaultOption],
create(context, [options]) {
let services: ParserServicesWithTypeInformation | undefined;
try {
services = getParserServices(context);
} catch (error) {
console.error(error);
}
if (!services?.program) return {};
const checker = services.program.getTypeChecker();
const services = getParserServices(context);
const checker = services.program!.getTypeChecker();
const allowed = new Set(options.allow);
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 { Rule, Linter } from 'eslint';
export function defineRules(rules: {
[ruleName: string]: Rule.RuleModule | ESLintUtils.RuleModule<string, unknown[]>;
@ -7,17 +7,6 @@ export function defineRules(rules: {
return rules;
}
export function defineConfig(config: Linter.Config): Linter.Config;
export function defineConfig(config: Linter.Config[]): Linter.Config[];
export function defineConfig(config: Linter.Config | Linter.Config[]) {
if (!config || (Array.isArray(config) && config.some(c => !c))) {
console.trace();
throw new Error('Config cannot be empty');
}
return config;
}
export function defineRule({
name,
create,

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';

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