Upgrade to ESLint 9

This commit is contained in:
Alex
2024-10-16 00:29:26 -04:00
parent 0138cabb27
commit 00d0dfa107
53 changed files with 3813 additions and 2207 deletions

View File

@ -6,22 +6,22 @@
"subject": "[meta] add `repository.directory` field"
},
"eslint-import-resolver-typescript": {
"hash": "3dfad602a05b4b3812a4d3fc681051932f86e838",
"date": "2024-08-01T01:15:59+00:00",
"hash": "5ee5879b4428f42edbc262d66e192c76202b7f47",
"date": "2024-10-01T03:12:28+00:00",
"committer": "GitHub",
"subject": "chore(deps): update dependency node to v18.20.4 (#309)"
"subject": "fix(deps): update dependency debug to ^4.3.7 (#316)"
},
"eslint-plugin-jsx-a11y": {
"hash": "a08fbcc502d6a6fa7d01a48c5f0b895c61e8cdd5",
"date": "2024-08-22T20:21:57+01:00",
"hash": "4925ba8d0bf80a4b1d8e8645d310590bf1b40b64",
"date": "2024-09-20T14:09:27-07:00",
"committer": "Jordan Harband",
"subject": "[Fix] `label-has-associated-control`: ignore undetermined label text"
"subject": "[Fix] handle interactive/noninteractive changes from aria-query"
},
"eslint-plugin-n": {
"hash": "e5e758ea0cd238220127ae7bcbd967f1d8920f28",
"date": "2024-08-06T04:22:42+12:00",
"hash": "23d0e846e9dbfb68ccf7f410a83457514d432263",
"date": "2024-10-09T13:49:20+02:00",
"committer": "GitHub",
"subject": "docs(process-exit-as-throw): update wording (#323)"
"subject": "chore(master): release 17.11.1 (#352)"
},
"eslint-plugin-react": {
"hash": "983b88dd3cb5e07919517d3fde4085f60883ded7",

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

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

View File

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

View File

@ -6,7 +6,12 @@ import {
isTypeFlagSet,
isTypeNeverType,
} from '@typescript-eslint/type-utils';
import { AST_NODE_TYPES, ESLintUtils, type TSESTree } from '@typescript-eslint/utils';
import {
AST_NODE_TYPES,
ESLintUtils,
ParserServicesWithTypeInformation,
type TSESTree,
} from '@typescript-eslint/utils';
import { getParserServices } from '@typescript-eslint/utils/eslint-utils';
import * as ts from 'typescript';
@ -52,8 +57,14 @@ export default createRule<Option[], MessageId>({
},
defaultOptions: [defaultOption],
create(context, [options]) {
const services = getParserServices(context);
if (!services.program) return {};
let services: ParserServicesWithTypeInformation | undefined;
try {
services = getParserServices(context);
} catch (error) {
console.error(error);
}
if (!services?.program) return {};
const checker = services.program.getTypeChecker();
const allowed = new Set(options.allow);

View File

@ -1,12 +1,13 @@
import * as fs from 'node:fs';
import { resolve } from 'node:path';
import fs from 'node:fs';
import path from 'node:path';
import type { Middleware } from './middleware';
import { jsdoc } from './presets/jsdoc';
import { reactQuery, storybook, vitest } from './presets/misc';
import { react, reactRefresh } from './presets/react';
import { tailwind } from './presets/tailwind';
import { testingLibrary } from './presets/testing-library';
const jsdoc = () => import('./presets/jsdoc');
const tailwind = () => import('./presets/tailwind');
const testingLibrary = () => import('./presets/testing-library');
const middlewares = {
react,
@ -17,6 +18,8 @@ const middlewares = {
testingLibrary,
jsdoc,
vitest,
} satisfies {
[key: string]: Middleware;
};
export const envs: {
@ -62,9 +65,13 @@ export const envs: {
export function getProjectDependencies() {
const rootDir = process.cwd();
const pkgJsonPath = resolve(rootDir, 'package.json');
const pkgJsonPath = path.resolve(rootDir, 'package.json');
const pkgJson = fs.existsSync(pkgJsonPath)
? JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'))
? (JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')) as {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
})
: {};
return new Set(

View File

@ -1,191 +1,104 @@
/// <reference path="./modules.d.ts" />
import './redirect';
import type { ESLintConfig, Extends, Plugin, Rules } from '@aet/eslint-define-config';
import type { ESLintUtils } from '@typescript-eslint/utils';
import type { Rule } from 'eslint';
import { uniq } from 'lodash';
import type { FlatESLintConfig } from '@aet/eslint-define-config';
import * as tsParser from '@typescript-eslint/parser';
import importPlugin from 'eslint-plugin-import-x';
import { uniq } from 'lodash-es';
import tseslint from 'typescript-eslint';
import { off } from './constants';
import { checkEnv } from './env';
import type { Middleware, MiddlewareConfig, MiddlewareFunctions } from './middleware';
import { custom } from './presets/custom';
import { checkEnv } from './environment';
import { Middleware } from './middleware';
import { eslintRules } from './presets/eslint';
import { stylistic } from './presets/stylistic';
import { importTypeScript } from './presets/typescript';
import { unicorn } from './presets/unicorn';
export { graphql } from './presets/graphql';
export { jsdoc } from './presets/jsdoc';
export { storybook } from './presets/misc';
export { react, reactRefresh } from './presets/react';
export { tailwind } from './presets/tailwind';
import stylistic from './presets/stylistic';
import { importRules, typescriptRules } from './presets/typescript';
import unicorn from './presets/unicorn';
export { error, warn, off } from './constants';
declare global {
interface Array<T> {
filter(
predicate: BooleanConstructor,
): Exclude<T, null | undefined | false | '' | 0>[];
}
}
const unique = (...arr: (false | undefined | string | string[])[]): string[] => [
...new Set(arr.flat(1).filter(Boolean)),
];
const ensureArray = <T>(value?: T | T[]): T[] =>
value == null ? [] : Array.isArray(value) ? value : [value];
type RuleLevel = 'error' | 'warn' | 'off' | 0 | 1 | 2;
type RuleEntry<Options> = RuleLevel | [RuleLevel, Partial<Options>];
export interface LocalRuleOptions {
/** Bans import from the specifier '.' and '..' and replaces it with '.+/index' */
'custom/no-import-dot': RuleEntry<unknown>;
/**
* Enforce template literal expressions to be of `string` type
* @see [restrict-template-expressions](https://typescript-eslint.io/rules/restrict-template-expressions)
*/
'custom/restrict-template-expressions': RuleEntry<{ allow: string[] }>;
/** Ban assignment of empty object literals `{}` and replace them with `Object.create(null)` */
'custom/no-empty-object-literal': RuleEntry<unknown>;
/** Ban useless import alias */
'custom/no-useless-import-alias': RuleEntry<unknown>;
}
export type RuleOptions = Rules & Partial<LocalRuleOptions>;
export interface CustomRule {
rule: () => Promise<{
default: Rule.RuleModule | ESLintUtils.RuleModule<string, unknown[]>;
}>;
options?: RuleLevel;
}
/**
* ESLint Configuration.
* @see [ESLint Configuration](https://eslint.org/docs/latest/user-guide/configuring/)
*/
export type InputConfig = Omit<ESLintConfig, 'rules'> & {
/**
* Rules.
* @see [Rules](https://eslint.org/docs/latest/user-guide/configuring/rules)
*/
rules?: Partial<RuleOptions>;
/**
* Glob pattern to find paths to custom rule files in JavaScript or TypeScript.
* Note this must be a string literal or an array of string literals since
* this is statically analyzed.
*
* Rules are prefixed with `custom/` and the file name is used as the rule name.
*/
customRuleFiles?: string | string[];
/**
* Automatically detect project types, dependencies and deduct the plugins.
* @default true
*/
export async function extendConfig({
auto = true,
middlewares: addMiddlewares = [],
configs = [],
}: {
auto?: boolean;
};
/**
* Returns a ESLint config object.
*
* By default, it includes `["@typescript-eslint", "import-x", "prettier", "unicorn"]` configs.
* Additional bundled plugins include:
*
* 1. [`react`](https://github.com/jsx-eslint/eslint-plugin-react#list-of-supported-rules)
* (automatically enables
* [`react-hooks`](https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks))
* 2. [`react-refresh`](https://github.com/ArnaudBarre/eslint-plugin-react-refresh)
* 3. [`jsx-a11y`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y#supported-rules)
* 4. [`unicorn`](https://github.com/sindresorhus/eslint-plugin-unicorn#rules)
* 5. [`n`](https://github.com/eslint-community/eslint-plugin-n#-rules) (Node.js specific,
* requires `minimatch`)
* 6. [`jsdoc`](https://github.com/gajus/eslint-plugin-jsdoc#rules)
*
* Non bundled:
* 1. [`graphql`](https://the-guild.dev/graphql/eslint/rules)
*
* @param of Configuration options.
* @returns ESLint configuration object.
*/
export function extendConfig(
of: InputConfig & {
middlewares?: Middleware[];
} = {
middlewares: [],
},
): ESLintConfig {
const {
auto = true,
plugins: _plugins = [],
settings = {},
rules,
extends: _extends,
overrides,
customRuleFiles,
parserOptions,
middlewares: _middlewares = [],
...rest
} = of;
const plugins: Plugin[] = [..._plugins];
const extend: Extends[] = ensureArray(_extends);
if (customRuleFiles != null) {
plugins.push('local');
}
middlewares?: Middleware[];
configs: FlatESLintConfig[];
}): Promise<FlatESLintConfig[]> {
const middlewares: Middleware[] = uniq([
importTypeScript,
unicorn,
custom,
stylistic,
() => import('./presets/custom'),
...(auto ? checkEnv() : []),
..._middlewares,
...addMiddlewares,
]);
const result: MiddlewareConfig = {
root: true,
plugins: unique('custom', plugins),
env: { node: true, browser: true, es2023: true },
reportUnusedDisableDirectives: true,
parserOptions: {
project: true,
...parserOptions,
const result: FlatESLintConfig[] = [
{ rules: eslintRules }, //
...tseslint.configs.recommendedTypeChecked,
importPlugin.flatConfigs.recommended,
importPlugin.flatConfigs.react,
importPlugin.flatConfigs.typescript,
...unicorn,
stylistic,
{
files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'],
languageOptions: {
parserOptions: {
parser: tsParser,
projectService: true,
ecmaVersion: 'latest',
tsconfigRootDir: import.meta.dirname,
sourceType: 'module',
},
},
settings: {
'import-x/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx', '.mts', '.cts'],
},
'import-x/resolver': {
typescript: true,
node: true,
},
'import-x/core-modules': ['node:sqlite'],
},
ignores: ['eslint.config.cjs'],
rules: {
...importRules,
...typescriptRules,
},
},
ignorePatterns: [],
globals: {},
extends: ['eslint:recommended', 'prettier', ...(extend as string[])],
settings,
overrides: [
{ files: ['repl.ts', 'scripts/**/*.ts'], rules: { 'no-console': off } },
...(overrides ?? []),
],
rules: { ...eslintRules },
...rest,
};
{
files: ['*.js', '*.mjs', '*.cjs', '*.jsx'],
...tseslint.configs.disableTypeChecked,
rules: {
'import-x/no-commonjs': off,
'import-x/unambiguous': off,
'@typescript-eslint/no-require-imports': off,
'typed-custom/restrict-template-expressions': off,
...tseslint.configs.disableTypeChecked.rules,
},
},
{
files: ['*.d.ts'],
rules: {
'@typescript-eslint/consistent-type-imports': off,
'import-x/unambiguous': off,
},
},
] as FlatESLintConfig[];
const functions: MiddlewareFunctions = {
addRules(newRules) {
Object.assign(result.rules, newRules);
},
addSettings(newSettings) {
Object.assign(result.settings, newSettings);
},
};
for (const fn of middlewares) {
fn(result, functions);
for (const middleware of middlewares) {
let fn = await middleware();
if ('default' in fn) {
fn = fn.default;
}
if (Array.isArray(fn)) {
result.push(...(fn as FlatESLintConfig[]).flat(Infinity));
} else {
result.push(fn as unknown as FlatESLintConfig);
}
}
result.plugins = unique(result.plugins);
result.extends = unique(result.extends);
Object.assign(result.rules, rules);
if (configs) {
result.push(...configs);
}
return result;
}

View File

@ -1,7 +1,7 @@
import { installPackage } from '@antfu/install-pkg';
import { uniq } from 'lodash';
import { uniq } from 'lodash-es';
import { envs, getProjectDependencies } from './env';
import { envs, getProjectDependencies } from './environment';
const deps = getProjectDependencies();
const packages = uniq(

View File

@ -1,83 +0,0 @@
import * as fs from 'node:fs';
import { basename, extname, isAbsolute, resolve } from 'node:path';
import type { ESLint } from 'eslint';
import { parseModule } from 'esprima';
import query from 'esquery';
import type { Node, Property } from 'estree';
import { glob } from 'fast-glob';
// https://github.com/gulpjs/interpret
const transpilers = [
'esbuild-register',
'tsx',
'ts-node/register/transpile-only',
'@swc/register',
'sucrase/register',
'@babel/register',
'coffeescript/register',
];
function tryRequire() {
for (const candidate of transpilers) {
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
require(candidate);
return;
} catch {}
}
}
const unwrapDefault = <T = any>(module: any): T => module.default ?? module;
const plugin: ESLint.Plugin = {
rules: {},
};
function hydrateESTreeNode(n: Node): any {
switch (n.type) {
case 'Literal':
return n.value;
case 'ArrayExpression':
return n.elements.filter(Boolean).map(hydrateESTreeNode);
default:
throw new Error(`Unsupported node type: ${n.type}`);
}
}
function parseConfigFile(js: string) {
const [node] = query(
parseModule(js),
'CallExpression[callee.name="extendConfig"] > ObjectExpression > Property[key.name="customRuleFiles"]',
);
return hydrateESTreeNode((node as Property).value);
}
function main() {
const rootDir = process.cwd();
const eslintConfigFile = ['.eslintrc.js', '.eslintrc.cjs']
.map(file => resolve(rootDir, file))
.find(file => fs.existsSync(file));
if (!eslintConfigFile) return;
const eslintConfig = fs.readFileSync(eslintConfigFile, 'utf8');
const customRuleFiles = parseConfigFile(eslintConfig);
if (!customRuleFiles?.length) return;
tryRequire();
for (let file of glob.sync(customRuleFiles)) {
if (!isAbsolute(file)) {
file = resolve(rootDir, file);
}
// eslint-disable-next-line @typescript-eslint/no-require-imports
const module = unwrapDefault(require(file));
const name = module.name ?? basename(file, extname(file));
plugin.rules![name] = module;
}
}
main();
export = plugin;

View File

@ -1,31 +1,10 @@
import type { ESLintConfig, KnownExtends, Settings } from '@aet/eslint-define-config';
import type { Merge, SetRequired } from 'type-fest';
import type { Linter } from 'eslint';
import type { RuleOptions } from './index';
type MiddlewareResult = Linter.Config | Linter.Config[];
type OptionalObjectKey<T> = Exclude<
{
[Key in keyof T]: undefined | any[] extends T[Key]
? Key
: undefined | Record<any, any> extends T[Key]
? Key
: never;
}[keyof T],
undefined
>;
export type Middleware =
| (() => Promise<MiddlewareResult>)
| (() => Promise<{ default: MiddlewareResult }>);
export type MiddlewareConfig = Merge<
SetRequired<ESLintConfig, OptionalObjectKey<ESLintConfig>>,
{ extends: KnownExtends[] }
>;
export interface MiddlewareFunctions {
addRules(rules: Partial<RuleOptions>): void;
addSettings(settings: Partial<Settings>): void;
}
export type Middleware = (config: MiddlewareConfig, helpers: MiddlewareFunctions) => void;
export function defineMiddleware(middleware: Middleware): Middleware {
return middleware;
}
// eslint-disable-next-line unicorn/prevent-abbreviations
export const def = <T>(module: { default: T }): T => module.default;

32
src/modules.d.ts vendored
View File

@ -1,25 +1,4 @@
declare module '@typescript-eslint/utils' {
export * from '@typescript-eslint/utils/dist/index';
}
declare module '@typescript-eslint/typescript-estree' {
export * from '@typescript-eslint/typescript-estree/dist/index';
}
declare module '@typescript-eslint/type-utils' {
export * from '@typescript-eslint/type-utils/dist/index';
}
declare module '@typescript-eslint/utils/eslint-utils' {
export * from '@typescript-eslint/utils/dist/eslint-utils';
}
declare module '@typescript-eslint/utils/json-schema' {
export * from '@typescript-eslint/utils/dist/json-schema';
}
declare module '@typescript-eslint/scope-manager' {
export * from '@typescript-eslint/scope-manager/dist/index';
}
declare module '@typescript-eslint/types' {
export * from '@typescript-eslint/types/dist/index';
}
// eslint-disable-next-line import-x/unambiguous
declare module 'module' {
export function _resolveFilename(
request: string,
@ -34,3 +13,12 @@ declare module 'module' {
options?: Record<PropertyKey, unknown>,
): string;
}
declare module 'eslint-plugin-storybook' {
import type { Linter } from 'eslint';
export const configs: {
csf: Linter.Config;
recommended: Linter.Config;
};
}

View File

@ -1,18 +1,20 @@
import { error } from '../constants';
import type { LocalRuleOptions } from '../index';
import { defineMiddleware } from '../middleware';
import { plugin, typedPlugin, LocalRuleOptions } from '../custom/index';
import { defineConfig } from '../types';
const customRules: Partial<LocalRuleOptions> = {
'custom/no-import-dot': error,
'custom/no-useless-import-alias': error,
};
export const custom = defineMiddleware((config, { addRules }) => {
addRules(customRules);
config.overrides.push({
export default defineConfig([
{
plugins: { custom: plugin },
rules: {
'custom/no-import-dot': error,
'custom/no-useless-import-alias': error,
} satisfies Partial<LocalRuleOptions>,
},
{
plugins: { 'typed-custom': typedPlugin },
files: ['*.ts', '!*.d.ts'],
rules: {
'custom/restrict-template-expressions': error,
},
});
});
'typed-custom/restrict-template-expressions': error,
} satisfies Partial<LocalRuleOptions>,
},
]);

View File

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

View File

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

View File

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

View File

@ -2,35 +2,42 @@ import type { ReactRulesObject } from '@aet/eslint-define-config/src/rules/react
import type { ReactRefreshRulesObject } from '@aet/eslint-define-config/src/rules/react-refresh';
import { error, off, warn } from '../constants';
import { defineMiddleware } from '../middleware';
import { def } from '../middleware';
import { defineConfig } from '../types';
const reactRules: Partial<ReactRulesObject> = {
'@eslint-react/no-missing-component-display-name': off,
'@eslint-react/no-children-prop': error,
'@eslint-react/no-leaked-conditional-rendering': error,
};
export const react = defineMiddleware((config, { addRules }) => {
config.plugins.push('@eslint-react/eslint-plugin', 'react-hooks');
config.extends.push(
'plugin:@eslint-react/recommended-legacy',
'plugin:@eslint-react/dom-legacy',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended',
);
config.overrides.push({
files: ['*.tsx'],
rules: {
'@eslint-react/no-leaked-conditional-rendering': error,
export async function react() {
const reactPlugin = def(await import('@eslint-react/eslint-plugin'));
const a11y = def(await import('../../packages/eslint-plugin-jsx-a11y/src/index'));
const hooks = await import('../../packages/eslint-plugin-react-hooks');
return defineConfig([
reactPlugin.configs['recommended-type-checked'] as any,
hooks.flatConfigs.recommended,
a11y.flatConfigs.recommended,
{
files: ['*.tsx'],
rules: reactRules,
},
});
addRules(reactRules);
});
]);
}
const refreshRules: Partial<ReactRefreshRulesObject> = {
'react-refresh/only-export-components': [warn, { allowConstantExport: true }],
};
export const reactRefresh = defineMiddleware((config, { addRules }) => {
config.plugins.push('react-refresh');
addRules(refreshRules);
});
export async function reactRefresh() {
const refreshPlugin = def(await import('eslint-plugin-react-refresh'));
return defineConfig({
plugins: {
// @ts-expect-error no types
'react-refresh': refreshPlugin,
},
rules: refreshRules,
});
}

View File

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

View File

@ -1,13 +1,14 @@
import type { TailwindRulesObject } from '@aet/eslint-define-config/src/rules/tailwind';
import tailwind from 'eslint-plugin-tailwindcss';
import { off } from '../constants';
import { defineMiddleware } from '../middleware';
import { defineConfig } from '../types';
const tailwindRules: Partial<TailwindRulesObject> = {
'tailwindcss/no-custom-classname': off,
} as const;
export const tailwind = defineMiddleware((config, { addRules }) => {
config.extends.push('plugin:tailwindcss/recommended');
addRules(tailwindRules);
});
export default defineConfig([
...tailwind.configs['flat/recommended'],
{ rules: tailwindRules },
]);

View File

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

View File

@ -2,9 +2,8 @@ import type { ImportXRulesObject } from '@aet/eslint-define-config/src/rules/imp
import type { TypeScriptRulesObject } from '@aet/eslint-define-config/src/rules/typescript-eslint';
import { error, off, warn } from '../constants';
import { defineMiddleware } from '../middleware';
const importRules: Partial<ImportXRulesObject> = {
export const importRules: Partial<ImportXRulesObject> = {
'import-x/first': error,
'import-x/no-absolute-path': error,
'import-x/no-duplicates': warn,
@ -20,7 +19,7 @@ const importRules: Partial<ImportXRulesObject> = {
'import-x/unambiguous': error,
};
const typescriptRules: Partial<TypeScriptRulesObject> = {
export const typescriptRules: Partial<TypeScriptRulesObject> = {
'@typescript-eslint/ban-ts-comment': [
error,
{
@ -38,6 +37,10 @@ const typescriptRules: Partial<TypeScriptRulesObject> = {
warn,
{ accessibility: 'no-public' },
],
'@typescript-eslint/no-empty-object-type': [
error,
{ allowInterfaces: 'with-single-extends' },
],
'@typescript-eslint/no-empty-interface': [error, { allowSingleExtends: true }],
'@typescript-eslint/no-explicit-any': off,
'@typescript-eslint/no-misused-promises': [error, { checksVoidReturn: false }],
@ -55,42 +58,3 @@ const typescriptRules: Partial<TypeScriptRulesObject> = {
'@typescript-eslint/triple-slash-reference': off,
'@typescript-eslint/unbound-method': off,
};
export const importTypeScript = defineMiddleware((config, { addRules, addSettings }) => {
config.parser = '@typescript-eslint/parser';
config.plugins.push('@typescript-eslint', 'import-x');
config.extends.push(
'plugin:@typescript-eslint/recommended-type-checked',
'plugin:import-x/errors',
'plugin:import-x/typescript',
);
addSettings({
'import-x/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx', '.mts', '.cts'],
},
'import-x/resolver': {
typescript: true,
},
});
config.overrides.push(
{
files: ['.eslintrc.js', '*.config.js', '*.cjs', '*.mjs'],
extends: ['plugin:@typescript-eslint/disable-type-checked'],
rules: {
'import-x/no-commonjs': off,
'import-x/unambiguous': off,
'@typescript-eslint/no-require-imports': off,
},
},
{
files: ['*.d.ts'],
rules: {
'@typescript-eslint/consistent-type-imports': off,
'import-x/unambiguous': off,
},
},
);
addRules(importRules);
addRules(typescriptRules);
});

View File

@ -1,8 +1,9 @@
/* eslint-disable unicorn/string-content */
import type { UnicornRulesObject } from '@aet/eslint-define-config/src/rules/unicorn';
import unicorn from 'eslint-plugin-unicorn';
import globals from 'globals';
import { error, off, warn } from '../constants';
import { defineMiddleware } from '../middleware';
import { defineConfig } from '../types';
const suggest = (suggest: string) => ({ suggest, fix: false });
@ -99,13 +100,31 @@ const unicornRules: Partial<UnicornRulesObject> = {
'unicorn/template-indent': warn,
};
export const unicorn = defineMiddleware((config, { addRules }) => {
config.plugins.push('unicorn');
addRules(unicornRules);
config.overrides.push({
// export const unicorn = defineMiddleware((config, { addRules }) => {
// config.plugins.push('unicorn');
// addRules(unicornRules);
// config.overrides.push({
// files: ['*.test.ts', '*.test.tsx'],
// rules: {
// 'unicorn/no-useless-undefined': off,
// },
// });
// });
export default defineConfig([
{
languageOptions: {
globals: globals.builtin,
},
plugins: {
unicorn,
},
rules: unicornRules,
},
{
files: ['*.test.ts', '*.test.tsx'],
rules: {
'unicorn/no-useless-undefined': off,
},
});
});
},
]);

View File

@ -1,24 +0,0 @@
import Module from 'node:module';
const { name } = [require][0]('./package.json');
const _resolveFilename = Module._resolveFilename;
const alias = new Set([
'eslint-import-resolver-typescript',
'eslint-plugin-jsx-a11y',
'eslint-plugin-local',
'eslint-plugin-n',
'eslint-plugin-react-hooks',
'eslint-plugin-custom',
]);
type CDR<T> = T extends [any, ...infer R] ? R : [];
Module._resolveFilename = function (
request: string,
...args: CDR<Parameters<typeof _resolveFilename>>
) {
if (alias.has(request)) {
request = `${name}/${request}`;
}
return _resolveFilename(request, ...args);
};

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,6 +7,13 @@ export function defineRules(rules: {
return rules;
}
export function defineConfig(config: Linter.Config): Linter.Config;
export function defineConfig(config: Linter.Config[]): Linter.Config[];
export function defineConfig(config: Linter.Config | Linter.Config[]) {
return config;
}
export function defineRule({
name,
create,

View File

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

View File

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