@ztd-me/eslint 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,117 @@
1
- # Temporary Holding Version
1
+ # @ztd-me/eslint
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Strict, typed ESLint flat configs built on `@antfu/eslint-config`. The standard is defined independently of existing projects. Every selected research rule is an error; existing antfu opinions keep their upstream severities.
4
+
5
+ ```sh
6
+ pnpm add -D @ztd-me/eslint eslint@^10.11.0 typescript@~6.0.3
7
+ ```
8
+
9
+ Requires Node >=22.14, ESLint 10.11 or later in major 10, and TypeScript >=5.4 <6.1. The tested versions and all 198 research entries are documented in [rule coverage](docs/rule-coverage.md) and the [machine-readable catalog](docs/rule-catalog.json).
10
+
11
+ ```js
12
+ // eslint.config.js (ESM)
13
+ import ztd from '@ztd-me/eslint'
14
+
15
+ export default ztd({
16
+ typescript: { tsconfigPath: 'tsconfig.json' },
17
+ react: true,
18
+ ignores: ['dist/**'],
19
+ })
20
+ ```
21
+
22
+ The default export and named `createConfig` are the same asynchronous function and return a Promise of flat configs. The declarations export `ConfigOptions`, `TypeScriptOptions`, `ReactOptions`, `VueOptions`, and `LocalConfig`. This API accepts explicit final overrides:
23
+
24
+ ```js
25
+ import { createConfig } from '@ztd-me/eslint'
26
+
27
+ export default createConfig({
28
+ vue: true,
29
+ typescript: {
30
+ tsconfigPath: 'tsconfig.json',
31
+ tsconfigRootDir: import.meta.dirname,
32
+ },
33
+ }, {
34
+ name: 'project/cli-output',
35
+ files: ['cli/**'],
36
+ rules: { 'no-console': 'off' },
37
+ })
38
+ ```
39
+
40
+ Framework dependencies used by this package are included: enabling a supported profile does not prompt to install a plugin. React and Vue default to `false`; enable only the frameworks used by the project. Both can be enabled with independent `files` globs in a mixed repository. Vue targets Vue 3; Vue 2 is outside this profile.
41
+
42
+ ## TypeScript
43
+
44
+ Type-aware checking is on by default, using `tsconfig.json`. Supply an existing project config with `strict: true` (including `strictNullChecks`) and `noUncheckedIndexedAccess: true`. Inherited compiler options are resolved; missing configs and unsafe compiler options fail explicitly. The selected TS config must include every linted TS/TSX file, including handwritten configuration and test files. For solution-style project references, point to the actual linted project's config.
45
+
46
+ The parser uses the explicit TS project, with `.vue` as an extra file extension and `vue-eslint-parser` as the SFC outer parser. Typed rules cover Vue scripts as well as TS and TSX. Vue template expressions use Vue rules and the custom array visitor; TypeScript rules do not type-check template expressions.
47
+
48
+ For a JavaScript-only scope, use `typescript: false`. This turns off TypeScript parsing and rules; it does not lower JS complexity, length, or correctness standards. There is no syntax-only TypeScript or `strictTypes: false` mode. Run `tsc --noEmit` as a separate compiler gate; `exactOptionalPropertyTypes`, `noPropertyAccessFromIndexSignature`, and `useUnknownInCatchVariables` are also recommended compiler settings.
49
+
50
+ ## Framework options
51
+
52
+ | Option | Default | Behavior |
53
+ | --- | --- | --- |
54
+ | `react` | `false` | `true`, or `{ files, compiler, experimental }`; includes hooks in `.ts` as well as JSX |
55
+ | `react.compiler` | `false` | Adds four official React Compiler checks |
56
+ | `react.experimental` | `false` | Adds experimental fetch cleanup checking |
57
+ | `vue` | `false` | `true`, or `{ files }`; Vue 3 correctness and accessibility are strict |
58
+ | `test` | `false` | Explicitly enable the researched Vitest profile; do not enable for Node test or Jest |
59
+ | `rules` | `{}` | Deliberate final overrides; subsequent flat configs run last |
60
+
61
+ JSX accessibility is disabled and `eslint-plugin-jsx-a11y` is not installed: its 6.10.2 peer range excludes ESLint 10. This is the user-authorized compatibility exception, not an accessibility certification. Vue accessibility remains enabled. JSX syntax and React correctness remain enabled when React is selected.
62
+
63
+ The selected React Hooks checks use one owner per behavior: `react/*` for hooks and effects, `react-hooks/*` only for globals, immutability, refs, and optional Compiler checks. The experimental fetch rule is off until explicitly selected. React version migration opinions are inherited from antfu rather than strengthened as correctness rules.
64
+
65
+ ## Limits and arrays
66
+
67
+ | Measure | Inclusive maximum / threshold |
68
+ | --- | --- |
69
+ | Classic cyclomatic complexity | 10 |
70
+ | Cognitive complexity | 15 |
71
+ | Control-flow nesting | 4 |
72
+ | Function length | 60 effective lines, including IIFEs and nested function text |
73
+ | File length | 300 effective lines; SFCs include script, template and style |
74
+ | Code and comments per line | 120 characters, tab width 2 |
75
+ | Function parameters | 4; TS `this: void` is not counted |
76
+ | Vue block lines | script 200, template 200, style 150; blank lines excluded, comments counted |
77
+ | Vue template nesting | 5 |
78
+ | Array literals | 3 or more elements expand, including exactly 3 |
79
+
80
+ These are researched defaults, not empirically optimal constants. Function/file counts exclude blank lines and comments. Line-width rules only exempt lines containing URLs; strings, regexes, templates and comments remain checked. Width rules report violations without unsafe automatic wrapping.
81
+
82
+ `ztd/array-layout` implements the complete array policy in JS, TS, JSX, Vue scripts and Vue templates:
83
+
84
+ - Empty arrays and simple arrays of 1–2 elements use one line when the complete surrounding line fits 120 characters.
85
+ - Arrays with at least 3 elements, complex/nested elements, multiline elements, or over-wide simple pairs expand: bracket boundaries and each element have separate lines. Antfu supplies indentation and multiline trailing commas.
86
+ - Simple elements are primitives, identifiers, unary operations on primitives, simple noncomputed property chains, and their TS/chain wrappers. Objects, arrays, calls, functions, spreads, and conditional expressions are complex.
87
+ - Comments retain their association. A fix that would cross a comment is withheld for manual editing; comments prevent short-array collapsing. CRLF is preserved.
88
+ - Spreads count as one syntax element; holes remain prohibited by sparse-array rules. Destructuring and tuple types use their existing consistency policy rather than the literal threshold. Tuple values and `as const` arrays use the literal policy.
89
+
90
+ ```ts
91
+ const pair = [first, second]
92
+ const ids = [
93
+ first,
94
+ second,
95
+ third,
96
+ ]
97
+ const users = [
98
+ { id: 1 },
99
+ { id: 2 },
100
+ ]
101
+ const [red, green, blue] = rgb
102
+ ```
103
+
104
+ The custom rule replaces the four transitional array rules from the research. Antfu's ArrayExpression consistency visitor is disabled; object/import/parameter consistency stays enabled. Stable stylistic formatting stays active. Disabling it or enabling its experimental list formatter is rejected; explicit final rule overrides remain available.
105
+
106
+ ## Validation and release
107
+
108
+ ```sh
109
+ pnpm install --frozen-lockfile
110
+ pnpm run check
111
+ ```
112
+
113
+ The checks build JS/declarations, type-check the public API, self-lint source/tests/scripts, exercise real TS/React/Vue fixtures, check all numeric boundaries and all selected catalog options, and install/import/type-check an independently packed artifact with pnpm. Negative fixtures are checked by the test harness, not included in ordinary self-lint. The test modules have reasoned local exceptions for Node's asynchronous ESM setup and imports of the distribution being tested; safety and size limits remain active.
114
+
115
+ [Publishing instructions](docs/publishing.md) describe the stage-only GitHub workflow and the user-controlled promotion. Staging is not a public release. After promotion, run `node scripts/registry-smoke.mjs 0.1.0` to install the exact public version in a fresh pnpm project and verify imports, declarations and lint behavior. Registry installation follows pnpm's supply-chain policies; a policy rejection is a blocker, not permission to disable the policy.
116
+
117
+ The repository has not granted an open-source license; package metadata is `UNLICENSED` pending that separate decision.
@@ -0,0 +1,2 @@
1
+ import type { Rule } from 'eslint';
2
+ export declare const arrayLayout: Rule.RuleModule;
@@ -0,0 +1,103 @@
1
+ import { isRecord, isSimple } from './array-simple.js';
2
+ function fitsLine(source, node, text, tokens) {
3
+ const start = tokens.getFirstToken(node);
4
+ const end = tokens.getLastToken(node);
5
+ if (start === null || end === null) {
6
+ return false;
7
+ }
8
+ const prefix = source.lines[start.loc.start.line - 1]?.slice(0, start.loc.start.column) ?? '';
9
+ const suffix = source.lines[end.loc.end.line - 1]?.slice(end.loc.end.column) ?? '';
10
+ return Array.from(`${prefix}${text}${suffix}`.replaceAll('\t', ' ')).length <= 120;
11
+ }
12
+ function hasTemplateVisitor(value) {
13
+ return isRecord(value) && typeof value.defineTemplateBodyVisitor === 'function'
14
+ && typeof value.getTemplateBodyTokenStore === 'function';
15
+ }
16
+ function gapBefore(source, token) {
17
+ if (token === null) {
18
+ return [];
19
+ }
20
+ const previous = source.getTokenBefore(token);
21
+ if (previous === null || previous.loc.end.line !== token.loc.start.line) {
22
+ return [];
23
+ }
24
+ return [
25
+ [
26
+ previous.range[1],
27
+ token.range[0],
28
+ ],
29
+ ];
30
+ }
31
+ function expand(context, node, elements, tokens) {
32
+ const source = context.sourceCode;
33
+ const open = tokens.getFirstToken(node);
34
+ const close = tokens.getLastToken(node);
35
+ if (open === null || close === null) {
36
+ return;
37
+ }
38
+ const ranges = elements.flatMap(element => gapBefore(tokens, tokens.getFirstToken(element)));
39
+ ranges.push(...gapBefore(tokens, close));
40
+ if (ranges.length === 0) {
41
+ return;
42
+ }
43
+ const indent = source.lines[open.loc.start.line - 1]?.match(/^\s*/u)?.[0] ?? '';
44
+ const newline = source.text.includes('\r\n') ? '\r\n' : '\n';
45
+ context.report({
46
+ node,
47
+ messageId: 'expand',
48
+ fix(fixer) {
49
+ // Comments remain in place; a non-whitespace gap requires a manual layout edit.
50
+ if (ranges.some(([start, end]) => /\S/u.test(source.text.slice(start, end)))) {
51
+ return null;
52
+ }
53
+ return ranges.map(range => fixer.replaceTextRange(range, `${newline}${indent}${range[1] === close.range[0] ? '' : ' '}`));
54
+ },
55
+ });
56
+ }
57
+ function createVisitor(context, tokens) {
58
+ return {
59
+ ArrayExpression(node) {
60
+ const elements = node.elements.filter(element => element !== null);
61
+ if (elements.length !== node.elements.length) {
62
+ return;
63
+ }
64
+ const source = context.sourceCode;
65
+ const complex = elements.some(element => !isSimple(element) || /[\r\n]/u.test(source.getText(element)));
66
+ if (elements.length >= 3 || complex) {
67
+ expand(context, node, elements, tokens);
68
+ return;
69
+ }
70
+ if (tokens.getCommentsInside(node).length > 0) {
71
+ return;
72
+ }
73
+ const compact = `[${elements.map(element => source.getText(element)).join(', ')}]`;
74
+ if (elements.length > 0 && !fitsLine(source, node, compact, tokens)) {
75
+ expand(context, node, elements, tokens);
76
+ return;
77
+ }
78
+ if (/[\r\n]/u.test(source.getText(node)) && fitsLine(source, node, compact, tokens)) {
79
+ context.report({ node, messageId: 'collapse', fix: fixer => fixer.replaceText(node, compact) });
80
+ }
81
+ },
82
+ };
83
+ }
84
+ export const arrayLayout = {
85
+ meta: {
86
+ type: 'layout',
87
+ docs: { description: 'Expand arrays of at least three items or complex items; collapse safe short arrays' },
88
+ fixable: 'whitespace',
89
+ schema: [],
90
+ messages: {
91
+ expand: 'Use separate bracket and element lines for arrays with at least 3 items or complex/multiline items.',
92
+ collapse: 'Keep a simple array with fewer than 3 items on one line when the complete line fits 120 characters.',
93
+ },
94
+ },
95
+ create(context) {
96
+ const script = createVisitor(context, context.sourceCode);
97
+ const services = context.sourceCode.parserServices;
98
+ if (hasTemplateVisitor(services)) {
99
+ return services.defineTemplateBodyVisitor(createVisitor(context, services.getTemplateBodyTokenStore()), script);
100
+ }
101
+ return script;
102
+ },
103
+ };
@@ -0,0 +1,2 @@
1
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
2
+ export declare function isSimple(node: unknown): boolean;
@@ -0,0 +1,42 @@
1
+ export function isRecord(value) {
2
+ return typeof value === 'object' && value !== null;
3
+ }
4
+ function isLiteral(node) {
5
+ return isRecord(node) && node.type === 'Literal';
6
+ }
7
+ function isAccess(node) {
8
+ if (node.computed !== false || !isRecord(node.property) || node.property.type !== 'Identifier') {
9
+ return false;
10
+ }
11
+ if (!isRecord(node.object)) {
12
+ return false;
13
+ }
14
+ return node.object.type === 'Identifier'
15
+ || (node.object.type === 'MemberExpression' && isAccess(node.object));
16
+ }
17
+ function isWrapper(node) {
18
+ const wrappers = [
19
+ 'TSAsExpression',
20
+ 'TSSatisfiesExpression',
21
+ 'TSNonNullExpression',
22
+ 'ChainExpression',
23
+ ];
24
+ return typeof node.type === 'string' && wrappers.includes(node.type) && isSimple(node.expression);
25
+ }
26
+ export function isSimple(node) {
27
+ if (!isRecord(node)) {
28
+ return false;
29
+ }
30
+ switch (node.type) {
31
+ case 'Identifier':
32
+ return true;
33
+ case 'Literal':
34
+ return node.value === null || typeof node.value !== 'object';
35
+ case 'UnaryExpression':
36
+ return isLiteral(node.argument) && isSimple(node.argument);
37
+ case 'MemberExpression':
38
+ return isAccess(node);
39
+ default:
40
+ return isWrapper(node);
41
+ }
42
+ }
@@ -0,0 +1,6 @@
1
+ import type { ConfigOptions, LocalConfig } from './options.js';
2
+ import antfu from '@antfu/eslint-config';
3
+ export type { ConfigOptions, LocalConfig, ReactOptions, TypeScriptOptions, VueOptions } from './options.js';
4
+ /** Strict flat configs. Project overrides are deliberately last and reviewable. */
5
+ export declare function createConfig(options?: ConfigOptions, ...localConfigs: LocalConfig[]): Promise<Awaited<ReturnType<typeof antfu>>>;
6
+ export default createConfig;
package/dist/index.js ADDED
@@ -0,0 +1,201 @@
1
+ import antfu, { GLOB_SRC, GLOB_TESTS, GLOB_TS, GLOB_TSX } from '@antfu/eslint-config';
2
+ import hooks from 'eslint-plugin-react-hooks';
3
+ import sonar from 'eslint-plugin-sonarjs';
4
+ import { arrayLayout } from './array-layout.js';
5
+ import { isRecord } from './array-simple.js';
6
+ import { reactRules } from './rules/react.js';
7
+ import { sharedRules } from './rules/shared.js';
8
+ import { testsRules } from './rules/tests.js';
9
+ import { tsSyntaxRules } from './rules/ts-syntax.js';
10
+ import { typedRules } from './rules/typed.js';
11
+ import { vueA11yRules } from './rules/vue-a11y.js';
12
+ import { vueRules } from './rules/vue.js';
13
+ import { resolveTypeScript } from './typescript.js';
14
+ function shared(sfcFiles) {
15
+ return {
16
+ name: 'ztd/shared',
17
+ files: [
18
+ GLOB_SRC,
19
+ ...sfcFiles,
20
+ ],
21
+ plugins: { sonarjs: sonar, ztd: { rules: { 'array-layout': arrayLayout } } },
22
+ linterOptions: { reportUnusedDisableDirectives: 'error' },
23
+ rules: {
24
+ ...sharedRules,
25
+ 'ztd/array-layout': 'error',
26
+ 'antfu/curly': 'off',
27
+ 'antfu/if-newline': 'off',
28
+ 'antfu/consistent-list-newline': [
29
+ 'error',
30
+ { ArrayExpression: false },
31
+ ],
32
+ 'style/array-bracket-newline': 'off',
33
+ 'style/array-element-newline': 'off',
34
+ 'no-unused-vars': 'off',
35
+ 'eslint-comments/no-unlimited-disable': 'error',
36
+ },
37
+ };
38
+ }
39
+ function typed(files) {
40
+ return [
41
+ {
42
+ name: 'ztd/typescript-syntax',
43
+ files,
44
+ rules: {
45
+ ...tsSyntaxRules,
46
+ 'max-params': 'off',
47
+ 'no-shadow': 'off',
48
+ 'no-unused-expressions': 'off',
49
+ 'no-useless-constructor': 'off',
50
+ 'no-setter-return': 'off',
51
+ 'ts/no-unused-vars': 'off',
52
+ 'ts/explicit-function-return-type': 'off',
53
+ },
54
+ },
55
+ {
56
+ name: 'ztd/typescript-typed',
57
+ files,
58
+ ignores: ['**/*.md/**', '**/*.astro/*.ts'],
59
+ rules: {
60
+ ...typedRules,
61
+ 'require-await': 'off',
62
+ 'no-throw-literal': 'off',
63
+ 'prefer-promise-reject-errors': 'off',
64
+ 'no-constant-binary-expression': 'off',
65
+ },
66
+ },
67
+ ];
68
+ }
69
+ function react(options) {
70
+ if (options === false || options === undefined) {
71
+ return [];
72
+ }
73
+ const settings = typeof options === 'object' ? options : {};
74
+ const { 'react/no-leaked-conditional-rendering': _typedRule, ...rules } = reactRules;
75
+ return [
76
+ {
77
+ name: 'ztd/react',
78
+ files: settings.files ?? [GLOB_SRC],
79
+ plugins: { 'react-hooks': hooks },
80
+ rules: {
81
+ ...rules,
82
+ 'react/web-api-no-leaked-fetch': settings.experimental === true ? 'error' : 'off',
83
+ ...Object.fromEntries([
84
+ 'config',
85
+ 'gating',
86
+ 'incompatible-library',
87
+ 'preserve-manual-memoization',
88
+ ].map(rule => [
89
+ `react-hooks/${rule}`,
90
+ settings.compiler === true ? 'error' : 'off',
91
+ ])),
92
+ },
93
+ },
94
+ ];
95
+ }
96
+ function vue(options) {
97
+ if (options === false || options === undefined) {
98
+ return [];
99
+ }
100
+ const files = typeof options === 'object' ? options.files ?? ['**/*.vue'] : ['**/*.vue'];
101
+ return [
102
+ {
103
+ name: 'ztd/vue',
104
+ files,
105
+ rules: {
106
+ ...vueRules,
107
+ ...vueA11yRules,
108
+ 'style/max-len': 'off',
109
+ 'vue/array-bracket-newline': 'off',
110
+ 'vue/array-element-newline': 'off',
111
+ },
112
+ },
113
+ ];
114
+ }
115
+ function vitest(enabled, tsFiles) {
116
+ if (!enabled) {
117
+ return [];
118
+ }
119
+ const { 'test/unbound-method': _unbound, ...syntaxTests } = testsRules;
120
+ const configs = [
121
+ {
122
+ name: 'ztd/vitest',
123
+ files: GLOB_TESTS,
124
+ rules: { ...syntaxTests, 'test/no-only-tests': 'error' },
125
+ },
126
+ ];
127
+ if (tsFiles.length > 0) {
128
+ configs.push({
129
+ name: 'ztd/vitest-typed',
130
+ files: tsFiles.flatMap(file => GLOB_TESTS.map(testFile => [file, testFile])),
131
+ rules: { 'ts/unbound-method': 'off', 'test/unbound-method': [
132
+ 'error',
133
+ { ignoreStatic: false },
134
+ ] },
135
+ });
136
+ }
137
+ return configs;
138
+ }
139
+ function vueFiles(options) {
140
+ if (options === false || options === undefined) {
141
+ return [];
142
+ }
143
+ return typeof options === 'object' ? options.files ?? ['**/*.vue'] : ['**/*.vue'];
144
+ }
145
+ function antfuReact(options, tsFiles) {
146
+ if (options === false || options === undefined) {
147
+ return false;
148
+ }
149
+ const files = typeof options === 'object' ? options.files ?? [GLOB_SRC] : [GLOB_SRC];
150
+ return { files, filesTypeAware: files.flatMap(file => tsFiles.map(tsFile => [file, tsFile])), overrides: {} };
151
+ }
152
+ function antfuVue(files) {
153
+ return files.length === 0 ? false : { a11y: true, vueVersion: 3, files };
154
+ }
155
+ function resolveDefaults(options) {
156
+ return {
157
+ ...options,
158
+ typescript: options.typescript ?? true,
159
+ react: options.react ?? false,
160
+ vue: options.vue ?? false,
161
+ test: options.test ?? false,
162
+ rules: options.rules ?? {},
163
+ };
164
+ }
165
+ function validateFormatting(settings) {
166
+ if (settings === false || (isRecord(settings) && settings.experimental === true)) {
167
+ throw new Error('@ztd-me/eslint: stable stylistic formatting is required; '
168
+ + 'disable competing experimental list rules');
169
+ }
170
+ }
171
+ /** Strict flat configs. Project overrides are deliberately last and reviewable. */
172
+ export async function createConfig(options = {}, ...localConfigs) {
173
+ validateFormatting(options.stylistic);
174
+ const { typescript, react: enableReact, vue: enableVue, test, rules, ...base } = resolveDefaults(options);
175
+ const tsOptions = typescript === false ? false : resolveTypeScript(typescript);
176
+ const sfcFiles = vueFiles(enableVue);
177
+ const tsFiles = [
178
+ GLOB_TS,
179
+ GLOB_TSX,
180
+ ...sfcFiles,
181
+ ];
182
+ const strict = [
183
+ shared(sfcFiles),
184
+ ...react(enableReact),
185
+ ...vue(enableVue),
186
+ ...(typescript === false ? [] : typed(tsFiles)),
187
+ ...vitest(test, typescript === false ? [] : tsFiles),
188
+ ];
189
+ return await antfu({
190
+ ...base,
191
+ typescript: tsOptions === false ? false : { ...tsOptions, filesTypeAware: tsFiles },
192
+ react: antfuReact(enableReact, tsFiles),
193
+ vue: antfuVue(sfcFiles),
194
+ test,
195
+ jsx: { a11y: false },
196
+ autoRenamePlugins: true,
197
+ isInEditor: false,
198
+ antislop: false,
199
+ }, ...strict, { name: 'ztd/project-rules', rules }, ...localConfigs);
200
+ }
201
+ export default createConfig;
@@ -0,0 +1,38 @@
1
+ import type { OptionsConfig, StylisticConfig, TypedFlatConfigItem } from '@antfu/eslint-config';
2
+ import type { Linter } from 'eslint';
3
+ export interface TypeScriptOptions {
4
+ /** Default: tsconfig.json. Its resolved compiler options must enable strict and noUncheckedIndexedAccess. */
5
+ tsconfigPath?: string;
6
+ /** Absolute project root; defaults to process.cwd(). */
7
+ tsconfigRootDir?: string;
8
+ }
9
+ export interface ReactOptions {
10
+ /** Scope both antfu and strict React rules; defaults to JS/TS source extensions. */
11
+ files?: string[];
12
+ /** Additional official React Compiler checks. */
13
+ compiler?: boolean;
14
+ /** Opt in to the experimental fetch cleanup check. */
15
+ experimental?: boolean;
16
+ }
17
+ export interface VueOptions {
18
+ /** Vue 3 SFCs. Default: **\/*.vue. */
19
+ files?: string[];
20
+ }
21
+ type RemovedOptions = 'typescript' | 'react' | 'vue' | 'jsx' | 'test' | 'autoRenamePlugins' | 'isInEditor' | 'antislop' | 'stylistic';
22
+ type BaseOptions = Omit<OptionsConfig, RemovedOptions>;
23
+ export interface ConfigOptions extends BaseOptions {
24
+ /** Stable formatting remains active because line limits and array policy require it. */
25
+ stylistic?: true | Omit<StylisticConfig, 'experimental'>;
26
+ /** Typed checking is enabled by default. Set false only for a JavaScript-only scope. */
27
+ typescript?: boolean | TypeScriptOptions;
28
+ /** Explicitly enable React. Default: false. */
29
+ react?: boolean | ReactOptions;
30
+ /** Explicitly enable Vue 3, including accessibility. Default: false. */
31
+ vue?: boolean | VueOptions;
32
+ /** Enable the researched Vitest checks only for Vitest projects. Default: false. */
33
+ test?: boolean;
34
+ /** Final, explicit project rule overrides. */
35
+ rules?: Linter.RulesRecord;
36
+ }
37
+ export type LocalConfig = TypedFlatConfigItem | Linter.Config;
38
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { Linter } from 'eslint';
2
+ export declare const reactRules: Linter.RulesRecord;
@@ -0,0 +1,55 @@
1
+ export const reactRules = {
2
+ 'react/exhaustive-deps': [
3
+ 'error',
4
+ {
5
+ enableDangerousAutofixThisMayCauseInfiniteLoops: false,
6
+ requireExplicitEffectDeps: false,
7
+ },
8
+ ],
9
+ 'react/no-array-index-key': 'error',
10
+ 'react/purity': 'error',
11
+ 'react/set-state-in-effect': 'error',
12
+ 'react/web-api-no-leaked-event-listener': 'error',
13
+ 'react/web-api-no-leaked-intersection-observer': 'error',
14
+ 'react/web-api-no-leaked-interval': 'error',
15
+ 'react/web-api-no-leaked-resize-observer': 'error',
16
+ 'react/web-api-no-leaked-timeout': 'error',
17
+ 'react/jsx-no-comment-textnodes': 'error',
18
+ 'react/jsx-no-leaked-dollar': 'error',
19
+ 'react/jsx-no-leaked-semicolon': 'error',
20
+ 'react/dom-no-dangerously-set-innerhtml': 'error',
21
+ 'react/dom-no-script-url': 'error',
22
+ 'react/dom-no-unsafe-iframe-sandbox': 'error',
23
+ 'react/use-state': [
24
+ 'error',
25
+ {
26
+ enforceAssignment: true,
27
+ enforceLazyInitialization: true,
28
+ enforceSetterName: true,
29
+ },
30
+ ],
31
+ 'react/no-unstable-context-value': 'error',
32
+ 'react/no-unstable-default-props': [
33
+ 'error',
34
+ {
35
+ safeDefaultProps: [],
36
+ },
37
+ ],
38
+ 'react/dom-no-missing-button-type': 'error',
39
+ 'react/dom-no-missing-iframe-sandbox': 'error',
40
+ 'react/dom-no-unsafe-target-blank': 'error',
41
+ 'react/rules-of-hooks': 'error',
42
+ 'react/error-boundaries': 'error',
43
+ 'react/no-missing-key': 'error',
44
+ 'react/no-nested-component-definitions': 'error',
45
+ 'react/no-nested-lazy-component-declarations': 'error',
46
+ 'react/set-state-in-render': 'error',
47
+ 'react/static-components': 'error',
48
+ 'react/use-memo': 'error',
49
+ 'react/dom-no-dangerously-set-innerhtml-with-children': 'error',
50
+ 'react/dom-no-void-elements-with-children': 'error',
51
+ 'react/no-leaked-conditional-rendering': 'error',
52
+ 'react-hooks/globals': 'error',
53
+ 'react-hooks/immutability': 'error',
54
+ 'react-hooks/refs': 'error',
55
+ };
@@ -0,0 +1,2 @@
1
+ import type { Linter } from 'eslint';
2
+ export declare const sharedRules: Linter.RulesRecord;