@eslint-react/kit 2.1.2-next.2 → 4.0.0-beta.2

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.
Files changed (4) hide show
  1. package/README.md +418 -0
  2. package/dist/index.d.ts +120 -154
  3. package/dist/index.js +133 -99
  4. package/package.json +33 -11
package/README.md ADDED
@@ -0,0 +1,418 @@
1
+ # @eslint-react/kit
2
+
3
+ ESLint React's toolkit for building custom React lint rules right inside your `eslint.config.ts`.
4
+
5
+ ## Index
6
+
7
+ - [Installation](#installation)
8
+ - [Quick Start](#quick-start)
9
+ - [API Reference](#api-reference)
10
+ - [`defineConfig` (default export)](#defineconfig-default-export)
11
+ - [`merge`](#merge)
12
+ - [`Kit` — the toolkit object](#kit--the-toolkit-object)
13
+ - [`kit.collect`](#kitcollect) — Semantic collectors
14
+ - [`kit.is`](#kitis) — Predicates
15
+ - [`kit.hint`](#kithint) — Detection hint bit-flags
16
+ - [`kit.flag`](#kitflag) — Component characteristic flags
17
+ - [`kit.settings`](#kitsettings) — Normalized ESLint React settings
18
+ - [Examples](#examples)
19
+ - [Simple: Ban `forwardRef`](#simple-ban-forwardref)
20
+ - [Component: Destructure component props](#component-destructure-component-props)
21
+ - [Hooks: Warn on custom hooks that don't call other hooks](#hooks-warn-on-custom-hooks-that-dont-call-other-hooks)
22
+ - [Multiple Collectors: No component/hook factories](#multiple-collectors-no-componenthook-factories)
23
+ - [More Examples](#more-examples)
24
+
25
+ ## Installation
26
+
27
+ ```sh
28
+ npm install --save-dev @eslint-react/kit
29
+ ```
30
+
31
+ ## Quick Start
32
+
33
+ ```ts
34
+ import eslintReact from "@eslint-react/eslint-plugin";
35
+ import eslintReactKit, { merge } from "@eslint-react/kit";
36
+ import eslintJs from "@eslint/js";
37
+ import { defineConfig } from "eslint/config";
38
+ import tseslint from "typescript-eslint";
39
+
40
+ export default defineConfig(
41
+ {
42
+ files: ["**/*.{ts,tsx}"],
43
+ extends: [
44
+ eslintJs.configs.recommended,
45
+ tseslint.configs.recommended,
46
+ eslintReact.configs["recommended-typescript"],
47
+ eslintReactKit(
48
+ {
49
+ name: "function-component-definition",
50
+ make: (context, { collect }) => {
51
+ const { query, visitor } = collect.components(context);
52
+
53
+ return merge(
54
+ visitor,
55
+ {
56
+ "Program:exit"(program) {
57
+ for (const { node } of query.all(program)) {
58
+ if (node.type === "FunctionDeclaration") continue;
59
+ context.report({
60
+ node,
61
+ message: "Function components must be defined with function declarations.",
62
+ });
63
+ }
64
+ },
65
+ },
66
+ );
67
+ },
68
+ },
69
+ ),
70
+ ],
71
+ rules: {
72
+ "@eslint-react/kit/function-component-definition": "error",
73
+ },
74
+ },
75
+ );
76
+ ```
77
+
78
+ ## API Reference
79
+
80
+ ### `defineConfig` (default export)
81
+
82
+ ```ts
83
+ import defineReactConfig from "@eslint-react/kit";
84
+
85
+ defineReactConfig(...rules: RuleDefinition[]): Linter.Config
86
+ ```
87
+
88
+ Creates an ESLint flat-config object from one or more custom rule definitions. Rules are registered under the `@eslint-react/kit` plugin namespace and enabled at `"error"` severity by default.
89
+
90
+ **`RuleDefinition`:**
91
+
92
+ | Field | Type | Description |
93
+ | ------ | -------------------------------- | -------------------------------------------------------------------------------- |
94
+ | `name` | `string` | Unique rule name. Used as `@eslint-react/kit/<name>` in config. |
95
+ | `make` | `(context, kit) => RuleListener` | Rule factory. Receives the ESLint rule context and the structured `Kit` toolkit. |
96
+
97
+ ### `merge`
98
+
99
+ ```ts
100
+ import { merge } from "@eslint-react/kit";
101
+
102
+ merge(...listeners: RuleListener[]): RuleListener
103
+ ```
104
+
105
+ Merges multiple `RuleListener` (visitor) objects into a single listener. When two or more listeners define the same visitor key, the handlers are chained and execute in order.
106
+
107
+ This is essential for combining a collector's `visitor` with your own inspection logic.
108
+
109
+ ### Kit — the toolkit object
110
+
111
+ The second argument to `make` is a structured `Kit` object:
112
+
113
+ ```
114
+ kit
115
+ ├── collect -> Semantic collectors (components, hooks)
116
+ ├── is -> All predicates (component, hook, React API, import source)
117
+ ├── hint -> Detection hint bit-flags
118
+ ├── flag -> Component characteristic bit-flags
119
+ ├── settings -> Normalized ESLint React settings
120
+ ```
121
+
122
+ ---
123
+
124
+ #### `kit.collect`
125
+
126
+ Collector factories create a `{ query, visitor }` pair. The `visitor` must be merged into your rule listener via `merge()`. After traversal completes, `query.all(program)` yields all detected semantic nodes.
127
+
128
+ | Method | Returns | Description |
129
+ | ------------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------- |
130
+ | `components(context, options?)` | `CollectorWithContext<FunctionComponentSemanticNode>` | Detects function components. Options: `{ hint?: bigint, collectDisplayName?: boolean }` |
131
+ | `hooks(context)` | `CollectorWithContext<HookSemanticNode>` | Detects custom hook definitions. |
132
+
133
+ **`CollectorWithContext`** extends `Collector` with contextual queries:
134
+
135
+ | Query | Description |
136
+ | -------------------- | ----------------------------------------- |
137
+ | `query.all(program)` | All collected semantic nodes in the file. |
138
+
139
+ ---
140
+
141
+ #### `kit.is`
142
+
143
+ All predicates live under `kit.is` — organized into four sub-sections.
144
+
145
+ ##### Component
146
+
147
+ | Predicate | Signature | Description |
148
+ | --------------------------- | ------------------------- | --------------------------------------------------------------------------- |
149
+ | `componentDefinition` | `(node, hint) -> boolean` | Whether a function node is a component definition. (context pre-bound) |
150
+ | `componentName` | `(name) -> boolean` | Strict PascalCase component name check. |
151
+ | `componentNameLoose` | `(name) -> boolean` | Loose component name check. |
152
+ | `componentWrapperCall` | `(node) -> boolean` | Whether a node is a `memo(…)` or `forwardRef(…)` call. (context pre-bound) |
153
+ | `componentWrapperCallLoose` | `(node) -> boolean` | Like above, but also matches `useCallback(…)`. (context pre-bound) |
154
+ | `componentWrapperCallback` | `(node) -> boolean` | Whether a function is the callback passed to a wrapper. (context pre-bound) |
155
+
156
+ ##### Hook
157
+
158
+ General hook predicates:
159
+
160
+ | Predicate | Signature | Description |
161
+ | -------------------------- | ------------------------------------- | ------------------------------------------------------------ |
162
+ | `hook` | `(node) -> boolean` | Whether a function node is a hook (by name). |
163
+ | `hookCall` | `(node) -> boolean` | Whether a node is a hook call. |
164
+ | `hookName` | `(name) -> boolean` | Whether a string matches the `use[A-Z]` convention. |
165
+ | `useEffectLikeCall` | `(node, additionalHooks?) -> boolean` | Whether a node is a `useEffect`/`useLayoutEffect`-like call. |
166
+ | `useStateLikeCall` | `(node, additionalHooks?) -> boolean` | Whether a node is a `useState`-like call. |
167
+ | `useEffectSetupCallback` | `(node) -> boolean` | Whether a node is a useEffect setup function. |
168
+ | `useEffectCleanupCallback` | `(node) -> boolean` | Whether a node is a useEffect cleanup function. |
169
+
170
+ ##### React API
171
+
172
+ Factory functions (context pre-bound):
173
+
174
+ | Predicate | Signature | Description |
175
+ | -------------- | -------------------------------- | ---------------------------------------------------------------------------- |
176
+ | `reactAPI` | `(apiName) -> (node) -> boolean` | Factory: creates a predicate for a React API identifier. (context pre-bound) |
177
+ | `reactAPICall` | `(apiName) -> (node) -> boolean` | Factory: creates a predicate for a React API call. (context pre-bound) |
178
+
179
+ Pre-built identifier predicates (context pre-bound):
180
+
181
+ `captureOwnerStack`, `childrenCount`, `childrenForEach`, `childrenMap`, `childrenOnly`, `childrenToArray`, `cloneElement`, `createContext`, `createElement`, `forwardRef`, `memo`, `lazy`
182
+
183
+ Pre-built call predicates (context pre-bound):
184
+
185
+ `captureOwnerStackCall`, `childrenCountCall`, `childrenForEachCall`, `childrenMapCall`, `childrenOnlyCall`, `childrenToArrayCall`, `cloneElementCall`, `createContextCall`, `createElementCall`, `forwardRefCall`, `memoCall`, `lazyCall`
186
+
187
+ All React API predicates and factories have `context` pre-bound — no need to pass the rule context manually:
188
+
189
+ ```ts
190
+ // Direct check
191
+ is.memo(node);
192
+ is.memoCall(node);
193
+
194
+ // Useful in filter/find
195
+ nodes.filter(is.memoCall);
196
+
197
+ // Factory for any API name
198
+ const isCreateRefCall = is.reactAPICall("createRef");
199
+ isCreateRefCall(node);
200
+ ```
201
+
202
+ ##### Import source
203
+
204
+ | Predicate | Signature | Description |
205
+ | ---------------------------- | ----------------------------------------- | ---------------------------------------------------- |
206
+ | `initializedFromReact` | `(name, scope, importSource?) -> boolean` | Whether a variable comes from a React import. |
207
+ | `initializedFromReactNative` | `(name, scope, importSource?) -> boolean` | Whether a variable comes from a React Native import. |
208
+
209
+ ---
210
+
211
+ #### `kit.hint`
212
+
213
+ Bit-flags that control what the component collector considers a "component". Combine with bitwise OR (`|`) and remove with bitwise AND-NOT (`& ~`).
214
+
215
+ ```ts
216
+ // The default hint used when none is specified
217
+ hint.component.Default;
218
+
219
+ // All available flags
220
+ hint.component.DoNotIncludeFunctionDefinedAsObjectMethod;
221
+ hint.component.DoNotIncludeFunctionDefinedAsClassMethod;
222
+ hint.component.DoNotIncludeFunctionDefinedAsArrayMapCallback;
223
+ hint.component.DoNotIncludeFunctionDefinedAsArbitraryCallExpressionCallback;
224
+ // … and more (inherits all JsxDetectionHint flags)
225
+ ```
226
+
227
+ **Customization example:**
228
+
229
+ ```ts
230
+ const { query, visitor } = collect.components(context, {
231
+ // Also treat object methods as components (remove the exclusion flag)
232
+ hint: hint.component.Default & ~hint.component.DoNotIncludeFunctionDefinedAsObjectMethod,
233
+ });
234
+ ```
235
+
236
+ ---
237
+
238
+ #### `kit.flag`
239
+
240
+ Bit-flags indicating component characteristics. Check with bitwise AND (`&`).
241
+
242
+ ```ts
243
+ flag.component.None; // 0n — no flags
244
+ flag.component.Memo; // wrapped in React.memo
245
+ flag.component.ForwardRef; // wrapped in React.forwardRef
246
+ ```
247
+
248
+ **Usage:**
249
+
250
+ ```ts
251
+ for (const component of query.all(program)) {
252
+ if (component.flag & flag.component.Memo) {
253
+ // This component is memoized
254
+ }
255
+ }
256
+ ```
257
+
258
+ #### `kit.settings`
259
+
260
+ Exposes the normalized `react-x` settings from the ESLint shared configuration (`context.settings["react-x"]`). This lets your custom rules read and react to the same project-level settings used by the built-in rules.
261
+
262
+ | Property | Type | Default | Description |
263
+ | ----------------------- | ------------------------------------------------------- | ----------- | --------------------------------------------------------- |
264
+ | `version` | `string` | auto-detect | Resolved React version (e.g. `"19.2.4"`). |
265
+ | `importSource` | `string` | `"react"` | The module React is imported from (e.g. `"@pika/react"`). |
266
+ | `compilationMode` | `"infer" \| "annotation" \| "syntax" \| "all" \| "off"` | `"off"` | The React Compiler compilation mode the project uses. |
267
+ | `polymorphicPropName` | `string \| null` | `"as"` | Prop name used for polymorphic components. |
268
+ | `additionalStateHooks` | `RegExpLike` | — | Pattern matching custom hooks treated as state hooks. |
269
+ | `additionalEffectHooks` | `RegExpLike` | — | Pattern matching custom hooks treated as effect hooks. |
270
+
271
+ `RegExpLike` is an object with a `test(s: string) => boolean` method (same interface as `RegExp`).
272
+
273
+ **Usage:**
274
+
275
+ ```ts
276
+ defineReactConfig({
277
+ name: "require-react-19",
278
+ make: (context, { settings }) => ({
279
+ Program(program) {
280
+ if (!settings.version.startsWith("19.")) {
281
+ context.report({
282
+ node: program,
283
+ message: `This project requires React 19, but detected version ${settings.version}.`,
284
+ });
285
+ }
286
+ },
287
+ }),
288
+ });
289
+ ```
290
+
291
+ ---
292
+
293
+ ## Examples
294
+
295
+ ### Simple: Ban `forwardRef`
296
+
297
+ This is a simplified kit reimplementation of the built-in [`react-x/no-forwardRef`](https://eslint-react.xyz/docs/rules/no-forwardRef) rule.
298
+
299
+ ```ts
300
+ defineReactConfig({
301
+ name: "no-forward-ref",
302
+ make: (context, { is }) => ({
303
+ CallExpression(node) {
304
+ if (is.forwardRefCall(node)) {
305
+ context.report({ node, message: "forwardRef is deprecated in React 19. Pass ref as a prop instead." });
306
+ }
307
+ },
308
+ }),
309
+ });
310
+ ```
311
+
312
+ ### Component: Destructure component props
313
+
314
+ This is a simplified kit reimplementation of the built-in [`react-x/prefer-destructuring-assignment`](https://eslint-react.xyz/docs/rules/prefer-destructuring-assignment) rule.
315
+
316
+ ```ts
317
+ defineReactConfig({
318
+ name: "destructure-component-props",
319
+ make: (context, { collect }) => {
320
+ const { query, visitor } = collect.components(context);
321
+
322
+ return merge(visitor, {
323
+ "Program:exit"(program) {
324
+ for (const { node } of query.all(program)) {
325
+ const [props] = node.params;
326
+ if (props == null) continue;
327
+ if (props.type !== AST.Identifier) continue;
328
+ const propName = props.name;
329
+ const propVariable = context.sourceCode.getScope(node).variables.find((v) => v.name === propName);
330
+ const propReferences = propVariable?.references ?? [];
331
+ for (const ref of propReferences) {
332
+ const { parent } = ref.identifier;
333
+ if (parent.type !== AST.MemberExpression) continue;
334
+ context.report({
335
+ message: "Use destructuring assignment for component props.",
336
+ node: parent,
337
+ });
338
+ }
339
+ }
340
+ },
341
+ });
342
+ },
343
+ });
344
+ ```
345
+
346
+ ### Hooks: Warn on custom hooks that don't call other hooks
347
+
348
+ This is a simplified kit reimplementation of the built-in [`react-x/no-unnecessary-use-prefix`](https://eslint-react.xyz/docs/rules/no-unnecessary-use-prefix) rule.
349
+
350
+ ```ts
351
+ defineReactConfig({
352
+ name: "no-unnecessary-use-prefix",
353
+ make: (context, { collect }) => {
354
+ const { query, visitor } = collect.hooks(context);
355
+
356
+ return merge(visitor, {
357
+ "Program:exit"(program) {
358
+ for (const { node, hookCalls } of query.all(program)) {
359
+ if (hookCalls.length === 0) {
360
+ context.report({
361
+ node,
362
+ message: "A custom hook should use at least one hook, otherwise it's just a regular function.",
363
+ });
364
+ }
365
+ }
366
+ },
367
+ });
368
+ },
369
+ });
370
+ ```
371
+
372
+ ### Multiple Collectors: No component/hook factories
373
+
374
+ Disallow defining components or hooks inside other functions (factory pattern).
375
+ This is a simplified kit reimplementation of the built-in [`react-x/component-hook-factories`](https://eslint-react.xyz/docs/rules/component-hook-factories) rule.
376
+
377
+ ```ts
378
+ defineReactConfig({
379
+ name: "component-hook-factories",
380
+ make: (context, { collect }) => {
381
+ const fc = collect.components(context);
382
+ const hk = collect.hooks(context);
383
+ return merge(
384
+ fc.visitor,
385
+ hk.visitor,
386
+ {
387
+ "Program:exit"(program) {
388
+ const comps = fc.query.all(program);
389
+ const hooks = hk.query.all(program);
390
+ for (const { name, node, kind } of [...comps, ...hooks]) {
391
+ if (name == null) continue;
392
+ if (findParent(node, isFunction) == null) continue;
393
+ context.report({
394
+ node,
395
+ message: `Don't define ${kind} "${name}" inside a function. Move it to the module level.`,
396
+ });
397
+ }
398
+ },
399
+ },
400
+ );
401
+ },
402
+ });
403
+
404
+ function findParent({ parent }: TSESTree.Node, test: (n: TSESTree.Node) => boolean): TSESTree.Node | null {
405
+ if (parent == null) return null;
406
+ if (test(parent)) return parent;
407
+ if (parent.type === "Program") return null;
408
+ return findParent(parent, test);
409
+ }
410
+
411
+ function isFunction({ type }: TSESTree.Node) {
412
+ return type === "FunctionDeclaration" || type === "FunctionExpression" || type === "ArrowFunctionExpression";
413
+ }
414
+ ```
415
+
416
+ ## More Examples
417
+
418
+ Please check the [Rule Recipes](https://eslint-react.xyz/docs/configuration/configure-custom-rules#rule-recipes) in the documentation site.
package/dist/index.d.ts CHANGED
@@ -1,161 +1,127 @@
1
- import { unit } from "@eslint-react/eff";
2
- import * as tseslint from "@typescript-eslint/utils/ts-eslint";
3
- import { ReportDescriptor } from "@typescript-eslint/utils/ts-eslint";
1
+ import * as core from "@eslint-react/core";
2
+ import { ESLintReactSettingsNormalized } from "@eslint-react/shared";
3
+ import { TSESTreeFunction } from "@eslint-react/ast";
4
+ import { TSESTree } from "@typescript-eslint/utils";
5
+ import { RuleContext, RuleFix, RuleFixer, RuleListener } from "@typescript-eslint/utils/ts-eslint";
6
+ import { Linter } from "eslint";
4
7
 
5
- //#region src/constants.d.ts
6
- /**
7
- * Regular expressions for matching a HTML tag name
8
- */
9
- declare const RE_HTML_TAG: RegExp;
10
- /**
11
- * Regular expression for matching a TypeScript file extension.
12
- */
13
- declare const RE_TS_EXT: RegExp;
14
- /**
15
- * Regular expression for matching a JavaScript file extension.
16
- */
17
- declare const RE_JS_EXT: RegExp;
18
- /**
19
- * Regular expression for matching a PascalCase string.
20
- */
21
- declare const RE_PASCAL_CASE: RegExp;
22
- /**
23
- * Regular expression for matching a camelCase string.
24
- */
25
- declare const RE_CAMEL_CASE: RegExp;
26
- /**
27
- * Regular expression for matching a kebab-case string.
28
- */
29
- declare const RE_KEBAB_CASE: RegExp;
30
- /**
31
- * Regular expression for matching a snake_case string.
32
- */
33
- declare const RE_SNAKE_CASE: RegExp;
34
- /**
35
- * Regular expression for matching a CONSTANT_CASE string.
36
- */
37
- declare const RE_CONSTANT_CASE: RegExp;
38
- declare const RE_JAVASCRIPT_PROTOCOL: RegExp;
39
- /**
40
- * Regular expression for matching a valid JavaScript identifier.
41
- */
42
- declare const RE_JS_IDENTIFIER: RegExp;
43
- /**
44
- * Regular expression for matching a RegExp string.
45
- */
46
- declare const RE_REGEXP_STR: RegExp;
47
- /**
48
- * Regular expression for matching a `@jsx` annotation comment.
49
- */
50
- declare const RE_ANNOTATION_JSX: RegExp;
51
- /**
52
- * Regular expression for matching a `@jsxFrag` annotation comment.
53
- */
54
- declare const RE_ANNOTATION_JSX_FRAG: RegExp;
55
- /**
56
- * Regular expression for matching a `@jsxRuntime` annotation comment.
57
- */
58
- declare const RE_ANNOTATION_JSX_RUNTIME: RegExp;
59
- /**
60
- * Regular expression for matching a `@jsxImportSource` annotation comment.
61
- */
62
- declare const RE_ANNOTATION_JSX_IMPORT_SOURCE: RegExp;
63
- /**
64
- * Regular expression for matching a React component name.
65
- */
66
- declare const RE_COMPONENT_NAME: RegExp;
67
- /**
68
- * Regular expression for matching a React component name (loose).
69
- */
70
- declare const RE_COMPONENT_NAME_LOOSE: RegExp;
71
- /**
72
- * Regular expression for matching a React Hook name.
73
- */
74
- declare const RE_HOOK_NAME: RegExp;
75
- //#endregion
76
- //#region src/types/index.d.ts
77
- /**
78
- * Rule severity.
79
- * @since 0.0.1
80
- */
81
- type SeverityName = "off" | "warn" | "error";
82
- /**
83
- * The numeric severity level for a rule.
84
- *
85
- * - `0` means off.
86
- * - `1` means warn.
87
- * - `2` means error.
88
- */
89
- type SeverityLevel = 0 | 1 | 2;
90
- /**
91
- * The severity of a rule in a configuration.
92
- */
93
- type Severity = SeverityName | SeverityLevel;
94
- /**
95
- * Rule declaration.
96
- * @internal
97
- * @since 0.0.1
98
- */
99
- type RuleConfig<RuleOptions extends unknown[] = unknown[]> = Severity | [Severity, ...Partial<RuleOptions>];
100
- /**
101
- * Rule context.
102
- * @since 0.0.1
103
- */
104
- type RuleContext<MessageIds extends string = string, Options extends readonly unknown[] = readonly unknown[]> = tseslint.RuleContext<MessageIds, Options>;
105
- /**
106
- * Rule feature.
107
- * @since 1.20.0
108
- */
109
- type RuleFeature = "CFG" | "DBG" | "FIX" | "MOD" | "TSC" | "EXP";
110
- type RulePolicy = number;
111
- type RuleSuggest<MessageIds extends string = string> = {
112
- messageId: MessageIds;
113
- data?: Record<string, unknown>;
114
- fix: tseslint.ReportFixFunction;
115
- };
116
- /**
117
- * A collection of settings.
118
- */
119
- interface SettingsConfig {
120
- [key: string]: unknown;
8
+ //#region src/index.d.ts
9
+ interface Collector<T> {
10
+ query: {
11
+ all(program: TSESTree.Program): T[];
12
+ };
13
+ visitor: RuleListener;
121
14
  }
122
- interface CompatibleRule {
123
- meta: Record<string, any>;
124
- create: (...args: any[]) => any;
15
+ interface CollectorWithContext<T> extends Collector<T> {
16
+ query: {
17
+ all(program: TSESTree.Program): T[];
18
+ };
125
19
  }
126
- interface CompatiblePlugin {
127
- meta: {
128
- name: string;
129
- version: string;
20
+ declare function components(ctx: RuleContext<string, unknown[]>, options?: {
21
+ collectDisplayName?: boolean;
22
+ hint?: bigint;
23
+ }): CollectorWithContext<core.FunctionComponentSemanticNode>;
24
+ declare function hooks(ctx: RuleContext<string, unknown[]>): CollectorWithContext<core.HookSemanticNode>;
25
+ interface RuleToolkit {
26
+ is: {
27
+ componentDefinition: (node: TSESTreeFunction, hint: bigint) => boolean;
28
+ componentName: typeof core.isComponentName;
29
+ componentNameLoose: typeof core.isComponentNameLoose;
30
+ componentWrapperCall: (node: TSESTree.Node) => boolean;
31
+ componentWrapperCallLoose: (node: TSESTree.Node) => boolean;
32
+ componentWrapperCallback: (node: TSESTree.Node) => boolean;
33
+ hook: typeof core.isHook;
34
+ hookCall: typeof core.isHookCall;
35
+ hookName: typeof core.isHookName;
36
+ useEffectLikeCall: typeof core.isUseEffectLikeCall;
37
+ useStateLikeCall: typeof core.isUseStateLikeCall;
38
+ useEffectSetupCallback: typeof core.isUseEffectSetupCallback;
39
+ useEffectCleanupCallback: typeof core.isUseEffectCleanupCallback;
40
+ useCall: typeof core.isUseCall;
41
+ useActionStateCall: typeof core.isUseActionStateCall;
42
+ useCallbackCall: typeof core.isUseCallbackCall;
43
+ useContextCall: typeof core.isUseContextCall;
44
+ useDebugValueCall: typeof core.isUseDebugValueCall;
45
+ useDeferredValueCall: typeof core.isUseDeferredValueCall;
46
+ useEffectCall: typeof core.isUseEffectCall;
47
+ useFormStatusCall: typeof core.isUseFormStatusCall;
48
+ useIdCall: typeof core.isUseIdCall;
49
+ useImperativeHandleCall: typeof core.isUseImperativeHandleCall;
50
+ useInsertionEffectCall: typeof core.isUseInsertionEffectCall;
51
+ useLayoutEffectCall: typeof core.isUseLayoutEffectCall;
52
+ useMemoCall: typeof core.isUseMemoCall;
53
+ useOptimisticCall: typeof core.isUseOptimisticCall;
54
+ useReducerCall: typeof core.isUseReducerCall;
55
+ useRefCall: typeof core.isUseRefCall;
56
+ useStateCall: typeof core.isUseStateCall;
57
+ useSyncExternalStoreCall: typeof core.isUseSyncExternalStoreCall;
58
+ useTransitionCall: typeof core.isUseTransitionCall;
59
+ reactAPI: (api: string) => (node: null | TSESTree.Node) => boolean;
60
+ reactAPICall: (api: string) => (node: null | TSESTree.Node) => node is TSESTree.CallExpression;
61
+ captureOwnerStack: (node: null | TSESTree.Node) => boolean;
62
+ childrenCount: (node: null | TSESTree.Node) => boolean;
63
+ childrenForEach: (node: null | TSESTree.Node) => boolean;
64
+ childrenMap: (node: null | TSESTree.Node) => boolean;
65
+ childrenOnly: (node: null | TSESTree.Node) => boolean;
66
+ childrenToArray: (node: null | TSESTree.Node) => boolean;
67
+ cloneElement: (node: null | TSESTree.Node) => boolean;
68
+ createContext: (node: null | TSESTree.Node) => boolean;
69
+ createElement: (node: null | TSESTree.Node) => boolean;
70
+ createRef: (node: null | TSESTree.Node) => boolean;
71
+ forwardRef: (node: null | TSESTree.Node) => boolean;
72
+ memo: (node: null | TSESTree.Node) => boolean;
73
+ lazy: (node: null | TSESTree.Node) => boolean;
74
+ captureOwnerStackCall: (node: null | TSESTree.Node) => node is TSESTree.CallExpression;
75
+ childrenCountCall: (node: null | TSESTree.Node) => node is TSESTree.CallExpression;
76
+ childrenForEachCall: (node: null | TSESTree.Node) => node is TSESTree.CallExpression;
77
+ childrenMapCall: (node: null | TSESTree.Node) => node is TSESTree.CallExpression;
78
+ childrenOnlyCall: (node: null | TSESTree.Node) => node is TSESTree.CallExpression;
79
+ childrenToArrayCall: (node: null | TSESTree.Node) => node is TSESTree.CallExpression;
80
+ cloneElementCall: (node: null | TSESTree.Node) => node is TSESTree.CallExpression;
81
+ createContextCall: (node: null | TSESTree.Node) => node is TSESTree.CallExpression;
82
+ createElementCall: (node: null | TSESTree.Node) => node is TSESTree.CallExpression;
83
+ createRefCall: (node: null | TSESTree.Node) => node is TSESTree.CallExpression;
84
+ forwardRefCall: (node: null | TSESTree.Node) => node is TSESTree.CallExpression;
85
+ memoCall: (node: null | TSESTree.Node) => node is TSESTree.CallExpression;
86
+ lazyCall: (node: null | TSESTree.Node) => node is TSESTree.CallExpression;
87
+ initializedFromReact: typeof core.isInitializedFromReact;
88
+ initializedFromReactNative: typeof core.isInitializedFromReactNative;
89
+ };
90
+ hint: {
91
+ component: typeof core.ComponentDetectionHint & {
92
+ Default: bigint;
93
+ };
94
+ };
95
+ flag: {
96
+ component: typeof core.ComponentFlag;
130
97
  };
131
- rules: Record<string, CompatibleRule>;
98
+ collect: {
99
+ components: typeof components;
100
+ hooks: typeof hooks;
101
+ };
102
+ settings: ESLintReactSettingsNormalized;
132
103
  }
133
- interface CompatibleConfig {
134
- name?: string;
135
- rules?: Record<string, RuleConfig>;
136
- settings?: SettingsConfig;
104
+ interface RuleDefinition {
105
+ name: string;
106
+ make(ctx: RuleContext<string, unknown[]>, kit: RuleToolkit): RuleListener;
107
+ }
108
+ declare function defineConfig(...rules: RuleDefinition[]): Linter.Config;
109
+ declare function merge(...listeners: RuleListener[]): RuleListener;
110
+ declare module "@typescript-eslint/utils/ts-eslint" {
111
+ interface RuleContext<MessageIds extends string, Options extends readonly unknown[]> {
112
+ report(descriptor: {
113
+ readonly data?: Readonly<Record<string, unknown>>;
114
+ readonly fix?: ((fixer: RuleFixer) => IterableIterator<RuleFix> | readonly RuleFix[] | RuleFix | null) | null;
115
+ readonly loc?: Readonly<TSESTree.SourceLocation> | Readonly<TSESTree.Position>;
116
+ readonly message: string;
117
+ readonly node: TSESTree.Node;
118
+ readonly suggest?: readonly {
119
+ readonly data?: Readonly<Record<string, unknown>>;
120
+ readonly desc: string;
121
+ readonly fix: (fixer: RuleFixer) => IterableIterator<RuleFix> | readonly RuleFix[] | RuleFix | null;
122
+ }[] | null;
123
+ }): void;
124
+ }
137
125
  }
138
126
  //#endregion
139
- //#region src/utils/regexp.d.ts
140
- /**
141
- * Convert a string to the `RegExp`.
142
- * Normal strings (e.g. `"foo"`) is converted to `/^foo$/` of `RegExp`.
143
- * Strings like `"/^foo/i"` are converted to `/^foo/i` of `RegExp`.
144
- * @see https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/utils/regexp.ts
145
- * @param string The string to convert.
146
- * @returns Returns the `RegExp`.
147
- */
148
- declare function toRegExp(string: string): {
149
- test(s: string): boolean;
150
- };
151
- /**
152
- * Checks whether given string is regexp string
153
- * @param string The string to check
154
- * @returns boolean
155
- */
156
- declare function isRegExp(string: string): boolean;
157
- //#endregion
158
- //#region src/utils/reporting.d.ts
159
- declare function report(context: RuleContext): (descriptor: unit | null | ReportDescriptor<string>) => void;
160
- //#endregion
161
- export { CompatibleConfig, CompatiblePlugin, CompatibleRule, RE_ANNOTATION_JSX, RE_ANNOTATION_JSX_FRAG, RE_ANNOTATION_JSX_IMPORT_SOURCE, RE_ANNOTATION_JSX_RUNTIME, RE_CAMEL_CASE, RE_COMPONENT_NAME, RE_COMPONENT_NAME_LOOSE, RE_CONSTANT_CASE, RE_HOOK_NAME, RE_HTML_TAG, RE_JAVASCRIPT_PROTOCOL, RE_JS_EXT, RE_JS_IDENTIFIER, RE_KEBAB_CASE, RE_PASCAL_CASE, RE_REGEXP_STR, RE_SNAKE_CASE, RE_TS_EXT, RuleConfig, RuleContext, RuleFeature, RulePolicy, RuleSuggest, SettingsConfig, Severity, SeverityLevel, SeverityName, isRegExp, report, toRegExp };
127
+ export { Collector, CollectorWithContext, type RuleContext, RuleToolkit, defineConfig as default, defineConfig, merge };
package/dist/index.js CHANGED
@@ -1,108 +1,142 @@
1
- import "@eslint-react/eff";
1
+ import * as core from "@eslint-react/core";
2
+ import { getSettingsFromContext } from "@eslint-react/shared";
2
3
 
3
- //#region src/constants.ts
4
- /**
5
- * Regular expressions for matching a HTML tag name
6
- */
7
- const RE_HTML_TAG = /^[a-z][^-]*$/u;
8
- /**
9
- * Regular expression for matching a TypeScript file extension.
10
- */
11
- const RE_TS_EXT = /^[cm]?tsx?$/u;
12
- /**
13
- * Regular expression for matching a JavaScript file extension.
14
- */
15
- const RE_JS_EXT = /^[cm]?jsx?$/u;
16
- /**
17
- * Regular expression for matching a PascalCase string.
18
- */
19
- const RE_PASCAL_CASE = /^[A-Z][\dA-Za-z]*$/u;
20
- /**
21
- * Regular expression for matching a camelCase string.
22
- */
23
- const RE_CAMEL_CASE = /^[a-z][\dA-Za-z]*$/u;
24
- /**
25
- * Regular expression for matching a kebab-case string.
26
- */
27
- const RE_KEBAB_CASE = /^[a-z][\d\-a-z]*$/u;
28
- /**
29
- * Regular expression for matching a snake_case string.
30
- */
31
- const RE_SNAKE_CASE = /^[a-z][\d_a-z]*$/u;
32
- /**
33
- * Regular expression for matching a CONSTANT_CASE string.
34
- */
35
- const RE_CONSTANT_CASE = /^[A-Z][\d_A-Z]*$/u;
36
- const RE_JAVASCRIPT_PROTOCOL = /^[\u0000-\u001F ]*j[\t\n\r]*a[\t\n\r]*v[\t\n\r]*a[\t\n\r]*s[\t\n\r]*c[\t\n\r]*r[\t\n\r]*i[\t\n\r]*p[\t\n\r]*t[\t\n\r]*:/iu;
37
- /**
38
- * Regular expression for matching a valid JavaScript identifier.
39
- */
40
- const RE_JS_IDENTIFIER = /^[_$a-z][\w$]*$/i;
41
- /**
42
- * Regular expression for matching a RegExp string.
43
- */
44
- const RE_REGEXP_STR = /^\/(.+)\/([A-Za-z]*)$/u;
45
- /**
46
- * Regular expression for matching a `@jsx` annotation comment.
47
- */
48
- const RE_ANNOTATION_JSX = /@jsx\s+(\S+)/u;
49
- /**
50
- * Regular expression for matching a `@jsxFrag` annotation comment.
51
- */
52
- const RE_ANNOTATION_JSX_FRAG = /@jsxFrag\s+(\S+)/u;
53
- /**
54
- * Regular expression for matching a `@jsxRuntime` annotation comment.
55
- */
56
- const RE_ANNOTATION_JSX_RUNTIME = /@jsxRuntime\s+(\S+)/u;
57
- /**
58
- * Regular expression for matching a `@jsxImportSource` annotation comment.
59
- */
60
- const RE_ANNOTATION_JSX_IMPORT_SOURCE = /@jsxImportSource\s+(\S+)/u;
61
- /**
62
- * Regular expression for matching a React component name.
63
- */
64
- const RE_COMPONENT_NAME = /^[A-Z]/u;
65
- /**
66
- * Regular expression for matching a React component name (loose).
67
- */
68
- const RE_COMPONENT_NAME_LOOSE = /^_?[A-Z]/u;
69
- /**
70
- * Regular expression for matching a React Hook name.
71
- */
72
- const RE_HOOK_NAME = /^use/u;
4
+ //#region package.json
5
+ var name = "@eslint-react/kit";
6
+ var version = "4.0.0-beta.2";
73
7
 
74
8
  //#endregion
75
- //#region src/utils/regexp.ts
76
- /**
77
- * Convert a string to the `RegExp`.
78
- * Normal strings (e.g. `"foo"`) is converted to `/^foo$/` of `RegExp`.
79
- * Strings like `"/^foo/i"` are converted to `/^foo/i` of `RegExp`.
80
- * @see https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/utils/regexp.ts
81
- * @param string The string to convert.
82
- * @returns Returns the `RegExp`.
83
- */
84
- function toRegExp(string) {
85
- const [, pattern, flags = "u"] = RE_REGEXP_STR.exec(string) ?? [];
86
- if (pattern != null) return new RegExp(pattern, flags);
87
- return { test: (s) => s === string };
9
+ //#region src/index.ts
10
+ function components(ctx, options) {
11
+ const { api, visitor } = core.getComponentCollector(ctx, options);
12
+ return {
13
+ query: { all: (program) => api.getAllComponents(program) },
14
+ visitor
15
+ };
88
16
  }
89
- /**
90
- * Checks whether given string is regexp string
91
- * @param string The string to check
92
- * @returns boolean
93
- */
94
- function isRegExp(string) {
95
- return RE_REGEXP_STR.test(string);
17
+ function hooks(ctx) {
18
+ const { api, visitor } = core.getHookCollector(ctx);
19
+ return {
20
+ query: { all: (program) => api.getAllHooks(program) },
21
+ visitor
22
+ };
96
23
  }
97
-
98
- //#endregion
99
- //#region src/utils/reporting.ts
100
- function report(context) {
101
- return (descriptor) => {
102
- if (descriptor == null) return;
103
- return context.report(descriptor);
24
+ function createKit(ctx) {
25
+ return {
26
+ is: {
27
+ componentDefinition: (node, hint) => core.isComponentDefinition(ctx, node, hint),
28
+ componentName: core.isComponentName,
29
+ componentNameLoose: core.isComponentNameLoose,
30
+ componentWrapperCall: (node) => core.isComponentWrapperCall(ctx, node),
31
+ componentWrapperCallLoose: (node) => core.isComponentWrapperCallLoose(ctx, node),
32
+ componentWrapperCallback: (node) => core.isComponentWrapperCallback(ctx, node),
33
+ hook: core.isHook,
34
+ hookCall: core.isHookCall,
35
+ hookName: core.isHookName,
36
+ useEffectLikeCall: core.isUseEffectLikeCall,
37
+ useStateLikeCall: core.isUseStateLikeCall,
38
+ useEffectSetupCallback: core.isUseEffectSetupCallback,
39
+ useEffectCleanupCallback: core.isUseEffectCleanupCallback,
40
+ useCall: core.isUseCall,
41
+ useActionStateCall: core.isUseActionStateCall,
42
+ useCallbackCall: core.isUseCallbackCall,
43
+ useContextCall: core.isUseContextCall,
44
+ useDebugValueCall: core.isUseDebugValueCall,
45
+ useDeferredValueCall: core.isUseDeferredValueCall,
46
+ useEffectCall: core.isUseEffectCall,
47
+ useFormStatusCall: core.isUseFormStatusCall,
48
+ useIdCall: core.isUseIdCall,
49
+ useImperativeHandleCall: core.isUseImperativeHandleCall,
50
+ useInsertionEffectCall: core.isUseInsertionEffectCall,
51
+ useLayoutEffectCall: core.isUseLayoutEffectCall,
52
+ useMemoCall: core.isUseMemoCall,
53
+ useOptimisticCall: core.isUseOptimisticCall,
54
+ useReducerCall: core.isUseReducerCall,
55
+ useRefCall: core.isUseRefCall,
56
+ useStateCall: core.isUseStateCall,
57
+ useSyncExternalStoreCall: core.isUseSyncExternalStoreCall,
58
+ useTransitionCall: core.isUseTransitionCall,
59
+ reactAPI: (api) => core.isReactAPI(api)(ctx),
60
+ reactAPICall: (api) => core.isReactAPICall(api)(ctx),
61
+ captureOwnerStack: core.isCaptureOwnerStack(ctx),
62
+ childrenCount: core.isChildrenCount(ctx),
63
+ childrenForEach: core.isChildrenForEach(ctx),
64
+ childrenMap: core.isChildrenMap(ctx),
65
+ childrenOnly: core.isChildrenOnly(ctx),
66
+ childrenToArray: core.isChildrenToArray(ctx),
67
+ cloneElement: core.isCloneElement(ctx),
68
+ createContext: core.isCreateContext(ctx),
69
+ createElement: core.isCreateElement(ctx),
70
+ createRef: core.isCreateRef(ctx),
71
+ forwardRef: core.isForwardRef(ctx),
72
+ memo: core.isMemo(ctx),
73
+ lazy: core.isLazy(ctx),
74
+ captureOwnerStackCall: core.isCaptureOwnerStackCall(ctx),
75
+ childrenCountCall: core.isChildrenCountCall(ctx),
76
+ childrenForEachCall: core.isChildrenForEachCall(ctx),
77
+ childrenMapCall: core.isChildrenMapCall(ctx),
78
+ childrenOnlyCall: core.isChildrenOnlyCall(ctx),
79
+ childrenToArrayCall: core.isChildrenToArrayCall(ctx),
80
+ cloneElementCall: core.isCloneElementCall(ctx),
81
+ createContextCall: core.isCreateContextCall(ctx),
82
+ createElementCall: core.isCreateElementCall(ctx),
83
+ createRefCall: core.isCreateRefCall(ctx),
84
+ forwardRefCall: core.isForwardRefCall(ctx),
85
+ memoCall: core.isMemoCall(ctx),
86
+ lazyCall: core.isLazyCall(ctx),
87
+ initializedFromReact: core.isInitializedFromReact,
88
+ initializedFromReactNative: core.isInitializedFromReactNative
89
+ },
90
+ hint: { component: {
91
+ ...core.ComponentDetectionHint,
92
+ Default: core.DEFAULT_COMPONENT_DETECTION_HINT
93
+ } },
94
+ flag: { component: core.ComponentFlag },
95
+ collect: {
96
+ components,
97
+ hooks
98
+ },
99
+ settings: getSettingsFromContext(ctx)
104
100
  };
105
101
  }
102
+ function defineConfig(...rules) {
103
+ return {
104
+ files: ["**/*.ts", "**/*.tsx"],
105
+ plugins: { [name]: {
106
+ meta: {
107
+ name,
108
+ version
109
+ },
110
+ rules: rules.reduce((acc, { name, make }) => {
111
+ Reflect.set(acc, name, {
112
+ meta: {
113
+ fixable: "code",
114
+ hasSuggestions: true
115
+ },
116
+ create(ctx) {
117
+ return make(ctx, createKit(ctx));
118
+ }
119
+ });
120
+ return acc;
121
+ }, {})
122
+ } },
123
+ rules: rules.reduce((acc, { name: name$1 }) => {
124
+ acc[`${name}/${name$1}`] = "error";
125
+ return acc;
126
+ }, {})
127
+ };
128
+ }
129
+ function merge(...listeners) {
130
+ const [base = {}, ...rest] = listeners;
131
+ for (const r of rest) for (const key in r) {
132
+ const existing = base[key];
133
+ base[key] = existing ? (...args) => {
134
+ existing(...args);
135
+ r[key]?.(...args);
136
+ } : r[key];
137
+ }
138
+ return base;
139
+ }
106
140
 
107
141
  //#endregion
108
- export { RE_ANNOTATION_JSX, RE_ANNOTATION_JSX_FRAG, RE_ANNOTATION_JSX_IMPORT_SOURCE, RE_ANNOTATION_JSX_RUNTIME, RE_CAMEL_CASE, RE_COMPONENT_NAME, RE_COMPONENT_NAME_LOOSE, RE_CONSTANT_CASE, RE_HOOK_NAME, RE_HTML_TAG, RE_JAVASCRIPT_PROTOCOL, RE_JS_EXT, RE_JS_IDENTIFIER, RE_KEBAB_CASE, RE_PASCAL_CASE, RE_REGEXP_STR, RE_SNAKE_CASE, RE_TS_EXT, isRegExp, report, toRegExp };
142
+ export { defineConfig as default, defineConfig, merge };
package/package.json CHANGED
@@ -1,7 +1,13 @@
1
1
  {
2
2
  "name": "@eslint-react/kit",
3
- "version": "2.1.2-next.2",
4
- "description": "ESLint React's plugin kit for building plugins and rules.",
3
+ "version": "4.0.0-beta.2",
4
+ "description": "ESLint React's utility module for building custom rules.",
5
+ "keywords": [
6
+ "react",
7
+ "eslint",
8
+ "eslint-react",
9
+ "@eslint-react/kit"
10
+ ],
5
11
  "homepage": "https://github.com/Rel1cx/eslint-react",
6
12
  "bugs": {
7
13
  "url": "https://github.com/Rel1cx/eslint-react/issues"
@@ -12,7 +18,7 @@
12
18
  "directory": "packages/utilities/kit"
13
19
  },
14
20
  "license": "MIT",
15
- "author": "Rel1cx<rel1cx@proton.me>",
21
+ "author": "Rel1cx",
16
22
  "sideEffects": false,
17
23
  "type": "module",
18
24
  "exports": {
@@ -22,26 +28,42 @@
22
28
  },
23
29
  "./package.json": "./package.json"
24
30
  },
31
+ "main": "./dist/index.js",
32
+ "module": "./dist/index.js",
33
+ "types": "./dist/index.d.ts",
25
34
  "files": [
26
35
  "dist",
27
36
  "./package.json"
28
37
  ],
29
38
  "dependencies": {
30
- "@typescript-eslint/utils": "^8.46.0",
31
- "@eslint-react/eff": "2.1.2-next.2"
39
+ "@typescript-eslint/utils": "^8.57.2",
40
+ "@eslint-react/ast": "4.0.0-beta.2",
41
+ "@eslint-react/shared": "4.0.0-beta.2",
42
+ "@eslint-react/core": "4.0.0-beta.2"
32
43
  },
33
44
  "devDependencies": {
34
- "@tsconfig/node22": "^22.0.2",
35
- "tsdown": "^0.15.6",
36
- "@local/configs": "0.0.0"
45
+ "eslint": "^10.1.0",
46
+ "tsdown": "^0.21.5",
47
+ "@local/configs": "0.0.0",
48
+ "@local/eff": "3.0.0-beta.72"
49
+ },
50
+ "peerDependencies": {
51
+ "eslint": "^10.0.0",
52
+ "typescript": "*"
37
53
  },
38
54
  "engines": {
39
- "node": ">=20.19.0"
55
+ "node": ">=22.0.0"
56
+ },
57
+ "publishConfig": {
58
+ "access": "public"
59
+ },
60
+ "inlinedDependencies": {
61
+ "@local/eff": "workspace:*"
40
62
  },
41
63
  "scripts": {
42
- "build": "tsdown --dts-resolve",
64
+ "build": "tsdown",
43
65
  "build:docs": "typedoc",
44
66
  "lint:publish": "publint",
45
- "lint:ts": "tsc --noEmit"
67
+ "lint:ts": "tsl"
46
68
  }
47
69
  }