@trackunit/eslint-plugin-trackunit 0.4.67 → 0.5.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.
- package/CHANGELOG.md +27 -0
- package/README.md +2 -0
- package/package.json +1 -1
- package/src/lib/config/fragments/react-rules.js +15 -0
- package/src/lib/config/index.d.ts +57 -0
- package/src/lib/config/plugins.d.ts +13 -0
- package/src/lib/config/presets/base.d.ts +26 -0
- package/src/lib/config/presets/react.d.ts +31 -0
- package/src/lib/config/presets/react.js +9 -0
- package/src/lib/rules/component-name-matches-filename/component-name-matches-filename.d.ts +34 -0
- package/src/lib/rules/component-name-matches-filename/component-name-matches-filename.js +108 -0
- package/src/lib/rules/one-component-per-file/one-component-per-file.d.ts +29 -0
- package/src/lib/rules/one-component-per-file/one-component-per-file.js +73 -0
- package/src/lib/rules-map.d.ts +13 -0
- package/src/lib/rules-map.js +4 -0
- package/src/lib/utils/component-utils.d.ts +36 -0
- package/src/lib/utils/component-utils.js +192 -0
- package/src/index.js.map +0 -1
- package/src/lib/config/fragments/ignores.js.map +0 -1
- package/src/lib/config/fragments/import-rules.js.map +0 -1
- package/src/lib/config/fragments/jest-overrides.js.map +0 -1
- package/src/lib/config/fragments/jsdoc-rules.js.map +0 -1
- package/src/lib/config/fragments/module-boundaries.js.map +0 -1
- package/src/lib/config/fragments/react-rules.js.map +0 -1
- package/src/lib/config/fragments/restricted-imports.js.map +0 -1
- package/src/lib/config/fragments/testing-library.js.map +0 -1
- package/src/lib/config/fragments/typescript-rules.js.map +0 -1
- package/src/lib/config/index.js.map +0 -1
- package/src/lib/config/plugins.js.map +0 -1
- package/src/lib/config/presets/base.js.map +0 -1
- package/src/lib/config/presets/e2e.js.map +0 -1
- package/src/lib/config/presets/react.js.map +0 -1
- package/src/lib/config/presets/server.js.map +0 -1
- package/src/lib/config/utils.js.map +0 -1
- package/src/lib/config-helpers/create-skip-when.js.map +0 -1
- package/src/lib/rules/cva-merge-base-classes-as-array/cva-merge-base-classes-as-array.js.map +0 -1
- package/src/lib/rules/design-guideline-button-icon-size-match/design-guideline-button-icon-size-match.js.map +0 -1
- package/src/lib/rules/no-internal-barrel-files/no-internal-barrel-files.js.map +0 -1
- package/src/lib/rules/no-internal-graphql-when-tagged-with-gql-public/no-internal-graphql-when-tagged-with-gql-public.js.map +0 -1
- package/src/lib/rules/no-jest-mock-trackunit-react-core-hooks/no-jest-mock-trackunit-react-core-hooks.js.map +0 -1
- package/src/lib/rules/no-template-strings-in-classname-prop/no-template-strings-in-classname-prop.js.map +0 -1
- package/src/lib/rules/no-typescript-assertion/no-typescript-assertion.js.map +0 -1
- package/src/lib/rules/prefer-destructured-imports/prefer-destructured-imports.js.map +0 -1
- package/src/lib/rules/prefer-event-specific-callback-naming/name-suggestion-strategies.js.map +0 -1
- package/src/lib/rules/prefer-event-specific-callback-naming/prefer-event-specific-callback-naming.js.map +0 -1
- package/src/lib/rules/prefer-event-specific-callback-naming/strategies/string-based.js.map +0 -1
- package/src/lib/rules/prefer-event-specific-callback-naming/strategies/type-based.js.map +0 -1
- package/src/lib/rules/prefer-event-specific-callback-naming/utils.js.map +0 -1
- package/src/lib/rules/prefer-field-components/prefer-field-components.js.map +0 -1
- package/src/lib/rules/prefer-mouse-event-handler-in-react-props/prefer-mouse-event-handler-in-react-props.js.map +0 -1
- package/src/lib/rules/require-classname-alternatives/require-classname-alternatives.js.map +0 -1
- package/src/lib/rules/require-component-prop-contracts/require-component-prop-contracts.js.map +0 -1
- package/src/lib/rules/require-list-item-virtualization-props/require-list-item-virtualization-props.js.map +0 -1
- package/src/lib/rules/require-optional-prop-initialization/require-optional-prop-initialization.js.map +0 -1
- package/src/lib/rules/require-optional-prop-initialization/suggestion-utils.js.map +0 -1
- package/src/lib/rules-map.js.map +0 -1
- package/src/lib/utils/ast-utils.js.map +0 -1
- package/src/lib/utils/classname-utils.js.map +0 -1
- package/src/lib/utils/file-utils.js.map +0 -1
- package/src/lib/utils/import-utils.js.map +0 -1
- package/src/lib/utils/nx-utils.js.map +0 -1
- package/src/lib/utils/package-utils.js.map +0 -1
- package/src/lib/utils/typescript-utils.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
## 0.5.2 (2026-05-06)
|
|
2
|
+
|
|
3
|
+
### 🧱 Updated Dependencies
|
|
4
|
+
|
|
5
|
+
- Updated shared-utils to 1.14.2
|
|
6
|
+
|
|
7
|
+
## 0.5.1 (2026-05-06)
|
|
8
|
+
|
|
9
|
+
### 🚀 Features
|
|
10
|
+
|
|
11
|
+
- **eslint:** one-component-per-file + component-name-matches-filename rules ([#23077](https://github.com/Trackunit/manager/pull/23077))
|
|
12
|
+
|
|
13
|
+
### 🧱 Updated Dependencies
|
|
14
|
+
|
|
15
|
+
- Updated shared-utils to 1.14.1
|
|
16
|
+
|
|
17
|
+
### ❤️ Thank You
|
|
18
|
+
|
|
19
|
+
- Cursor @cursoragent
|
|
20
|
+
- Michael Buss Rønne @man-trackunit
|
|
21
|
+
|
|
22
|
+
## 0.5.0 (2026-05-04)
|
|
23
|
+
|
|
24
|
+
### 🧱 Updated Dependencies
|
|
25
|
+
|
|
26
|
+
- Updated shared-utils to 1.14.0
|
|
27
|
+
|
|
1
28
|
## 0.4.67 (2026-05-01)
|
|
2
29
|
|
|
3
30
|
This was a version bump only for eslint-plugin-trackunit to align it with other projects, there were no code changes.
|
package/README.md
CHANGED
|
@@ -64,6 +64,8 @@ All custom rules are enabled automatically through the presets under the `@track
|
|
|
64
64
|
| `@trackunit/require-optional-prop-initialization` | — | — | Requires optional component props to be initialized inside the component. |
|
|
65
65
|
| `@trackunit/require-component-prop-contracts` | — | — | Requires public component props to satisfy configured prop/interface contracts. |
|
|
66
66
|
| `@trackunit/require-list-item-virtualization-props` | — | — | Requires `VirtualizationListItemProps` on list items inside `<List>`. |
|
|
67
|
+
| `@trackunit/one-component-per-file` | warn | — | Enforces at most one exported React component per file. `exportedOnly: false` restricts to one total. |
|
|
68
|
+
| `@trackunit/component-name-matches-filename` | warn | — | Enforces that exported React component names match the filename (e.g. `Foo` in `Foo.tsx`). |
|
|
67
69
|
|
|
68
70
|
### Testing
|
|
69
71
|
|
package/package.json
CHANGED
|
@@ -118,6 +118,21 @@ exports.reactCustomRules = {
|
|
|
118
118
|
},
|
|
119
119
|
],
|
|
120
120
|
"@trackunit/prefer-field-components": "error",
|
|
121
|
+
"@trackunit/one-component-per-file": "warn",
|
|
122
|
+
"@trackunit/component-name-matches-filename": [
|
|
123
|
+
"warn",
|
|
124
|
+
{
|
|
125
|
+
allowedMismatches: [
|
|
126
|
+
// i18next convention: every library's translation.tsx exports a `Trans` wrapper
|
|
127
|
+
// named after the library component, not the file.
|
|
128
|
+
{ file: "translation", component: "Trans" },
|
|
129
|
+
// React context pattern: *Context files always contain both the context object and
|
|
130
|
+
// the provider component. The provider can't be renamed to match the file because
|
|
131
|
+
// the context object already uses that name.
|
|
132
|
+
{ file: "*Context", component: "*Provider" },
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
],
|
|
121
136
|
};
|
|
122
137
|
exports.reactTestingLibraryOverrides = {
|
|
123
138
|
files: ["**/*.spec.ts", "**/*.spec.tsx"],
|
|
@@ -4,6 +4,19 @@ export declare const configs: {
|
|
|
4
4
|
"@nx": typeof import("@nx/eslint-plugin");
|
|
5
5
|
"@trackunit": {
|
|
6
6
|
rules: {
|
|
7
|
+
"component-name-matches-filename": import("@typescript-eslint/utils/ts-eslint").RuleModule<"componentNameMismatch", [{
|
|
8
|
+
allowedMismatches?: ReadonlyArray<{
|
|
9
|
+
file: string;
|
|
10
|
+
component: string;
|
|
11
|
+
}>;
|
|
12
|
+
}], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
13
|
+
name: string;
|
|
14
|
+
};
|
|
15
|
+
"one-component-per-file": import("@typescript-eslint/utils/ts-eslint").RuleModule<"tooManyExported" | "tooManyComponents", [{
|
|
16
|
+
exportedOnly?: boolean;
|
|
17
|
+
}], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
18
|
+
name: string;
|
|
19
|
+
};
|
|
7
20
|
"cva-merge-base-classes-as-array": import("@typescript-eslint/utils/ts-eslint").RuleModule<"stringNeedsArray" | "arrayNeedsSplit", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
8
21
|
name: string;
|
|
9
22
|
};
|
|
@@ -176,6 +189,19 @@ export declare const configs: {
|
|
|
176
189
|
plugins: {
|
|
177
190
|
"@trackunit": {
|
|
178
191
|
rules: {
|
|
192
|
+
"component-name-matches-filename": import("@typescript-eslint/utils/ts-eslint").RuleModule<"componentNameMismatch", [{
|
|
193
|
+
allowedMismatches?: ReadonlyArray<{
|
|
194
|
+
file: string;
|
|
195
|
+
component: string;
|
|
196
|
+
}>;
|
|
197
|
+
}], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
198
|
+
name: string;
|
|
199
|
+
};
|
|
200
|
+
"one-component-per-file": import("@typescript-eslint/utils/ts-eslint").RuleModule<"tooManyExported" | "tooManyComponents", [{
|
|
201
|
+
exportedOnly?: boolean;
|
|
202
|
+
}], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
203
|
+
name: string;
|
|
204
|
+
};
|
|
179
205
|
"cva-merge-base-classes-as-array": import("@typescript-eslint/utils/ts-eslint").RuleModule<"stringNeedsArray" | "arrayNeedsSplit", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
180
206
|
name: string;
|
|
181
207
|
};
|
|
@@ -281,6 +307,19 @@ export declare const configs: {
|
|
|
281
307
|
plugins: {
|
|
282
308
|
"@trackunit": {
|
|
283
309
|
rules: {
|
|
310
|
+
"component-name-matches-filename": import("@typescript-eslint/utils/ts-eslint").RuleModule<"componentNameMismatch", [{
|
|
311
|
+
allowedMismatches?: ReadonlyArray<{
|
|
312
|
+
file: string;
|
|
313
|
+
component: string;
|
|
314
|
+
}>;
|
|
315
|
+
}], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
316
|
+
name: string;
|
|
317
|
+
};
|
|
318
|
+
"one-component-per-file": import("@typescript-eslint/utils/ts-eslint").RuleModule<"tooManyExported" | "tooManyComponents", [{
|
|
319
|
+
exportedOnly?: boolean;
|
|
320
|
+
}], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
321
|
+
name: string;
|
|
322
|
+
};
|
|
284
323
|
"cva-merge-base-classes-as-array": import("@typescript-eslint/utils/ts-eslint").RuleModule<"stringNeedsArray" | "arrayNeedsSplit", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
285
324
|
name: string;
|
|
286
325
|
};
|
|
@@ -789,6 +828,8 @@ export declare const configs: {
|
|
|
789
828
|
"jsdoc/require-jsdoc"?: undefined;
|
|
790
829
|
"no-console"?: undefined;
|
|
791
830
|
"@typescript-eslint/no-empty-function"?: undefined;
|
|
831
|
+
"@trackunit/one-component-per-file"?: undefined;
|
|
832
|
+
"@trackunit/component-name-matches-filename"?: undefined;
|
|
792
833
|
};
|
|
793
834
|
plugins?: undefined;
|
|
794
835
|
settings?: undefined;
|
|
@@ -799,6 +840,20 @@ export declare const configs: {
|
|
|
799
840
|
"jsdoc/require-jsdoc": string;
|
|
800
841
|
"no-console": string;
|
|
801
842
|
"@typescript-eslint/no-empty-function": string;
|
|
843
|
+
"@trackunit/one-component-per-file": string;
|
|
844
|
+
"@trackunit/component-name-matches-filename": string;
|
|
845
|
+
};
|
|
846
|
+
plugins?: undefined;
|
|
847
|
+
settings?: undefined;
|
|
848
|
+
languageOptions?: undefined;
|
|
849
|
+
} | {
|
|
850
|
+
files: string[];
|
|
851
|
+
rules: {
|
|
852
|
+
"@trackunit/one-component-per-file": string;
|
|
853
|
+
"@trackunit/component-name-matches-filename": string;
|
|
854
|
+
"jsdoc/require-jsdoc"?: undefined;
|
|
855
|
+
"no-console"?: undefined;
|
|
856
|
+
"@typescript-eslint/no-empty-function"?: undefined;
|
|
802
857
|
};
|
|
803
858
|
plugins?: undefined;
|
|
804
859
|
settings?: undefined;
|
|
@@ -874,6 +929,8 @@ export declare const configs: {
|
|
|
874
929
|
"jsdoc/require-jsdoc"?: undefined;
|
|
875
930
|
"no-console"?: undefined;
|
|
876
931
|
"@typescript-eslint/no-empty-function"?: undefined;
|
|
932
|
+
"@trackunit/one-component-per-file"?: undefined;
|
|
933
|
+
"@trackunit/component-name-matches-filename"?: undefined;
|
|
877
934
|
};
|
|
878
935
|
settings?: undefined;
|
|
879
936
|
})[];
|
|
@@ -22,6 +22,19 @@ import * as globals from "globals";
|
|
|
22
22
|
import * as jsoncParser from "jsonc-eslint-parser";
|
|
23
23
|
export declare const localRulesPlugin: {
|
|
24
24
|
rules: {
|
|
25
|
+
"component-name-matches-filename": import("@typescript-eslint/utils/ts-eslint").RuleModule<"componentNameMismatch", [{
|
|
26
|
+
allowedMismatches?: ReadonlyArray<{
|
|
27
|
+
file: string;
|
|
28
|
+
component: string;
|
|
29
|
+
}>;
|
|
30
|
+
}], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
31
|
+
name: string;
|
|
32
|
+
};
|
|
33
|
+
"one-component-per-file": import("@typescript-eslint/utils/ts-eslint").RuleModule<"tooManyExported" | "tooManyComponents", [{
|
|
34
|
+
exportedOnly?: boolean;
|
|
35
|
+
}], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
36
|
+
name: string;
|
|
37
|
+
};
|
|
25
38
|
"cva-merge-base-classes-as-array": import("@typescript-eslint/utils/ts-eslint").RuleModule<"stringNeedsArray" | "arrayNeedsSplit", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
26
39
|
name: string;
|
|
27
40
|
};
|
|
@@ -4,6 +4,19 @@ export declare const base: (import("eslint").Linter.FlatConfig<import("eslint").
|
|
|
4
4
|
"@nx": typeof nx;
|
|
5
5
|
"@trackunit": {
|
|
6
6
|
rules: {
|
|
7
|
+
"component-name-matches-filename": import("@typescript-eslint/utils/ts-eslint").RuleModule<"componentNameMismatch", [{
|
|
8
|
+
allowedMismatches?: ReadonlyArray<{
|
|
9
|
+
file: string;
|
|
10
|
+
component: string;
|
|
11
|
+
}>;
|
|
12
|
+
}], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
13
|
+
name: string;
|
|
14
|
+
};
|
|
15
|
+
"one-component-per-file": import("@typescript-eslint/utils/ts-eslint").RuleModule<"tooManyExported" | "tooManyComponents", [{
|
|
16
|
+
exportedOnly?: boolean;
|
|
17
|
+
}], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
18
|
+
name: string;
|
|
19
|
+
};
|
|
7
20
|
"cva-merge-base-classes-as-array": import("@typescript-eslint/utils/ts-eslint").RuleModule<"stringNeedsArray" | "arrayNeedsSplit", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
8
21
|
name: string;
|
|
9
22
|
};
|
|
@@ -176,6 +189,19 @@ export declare const base: (import("eslint").Linter.FlatConfig<import("eslint").
|
|
|
176
189
|
plugins: {
|
|
177
190
|
"@trackunit": {
|
|
178
191
|
rules: {
|
|
192
|
+
"component-name-matches-filename": import("@typescript-eslint/utils/ts-eslint").RuleModule<"componentNameMismatch", [{
|
|
193
|
+
allowedMismatches?: ReadonlyArray<{
|
|
194
|
+
file: string;
|
|
195
|
+
component: string;
|
|
196
|
+
}>;
|
|
197
|
+
}], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
198
|
+
name: string;
|
|
199
|
+
};
|
|
200
|
+
"one-component-per-file": import("@typescript-eslint/utils/ts-eslint").RuleModule<"tooManyExported" | "tooManyComponents", [{
|
|
201
|
+
exportedOnly?: boolean;
|
|
202
|
+
}], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
203
|
+
name: string;
|
|
204
|
+
};
|
|
179
205
|
"cva-merge-base-classes-as-array": import("@typescript-eslint/utils/ts-eslint").RuleModule<"stringNeedsArray" | "arrayNeedsSplit", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
180
206
|
name: string;
|
|
181
207
|
};
|
|
@@ -3,6 +3,19 @@ export declare const reactPreset: ({
|
|
|
3
3
|
plugins: {
|
|
4
4
|
"@trackunit": {
|
|
5
5
|
rules: {
|
|
6
|
+
"component-name-matches-filename": import("@typescript-eslint/utils/ts-eslint").RuleModule<"componentNameMismatch", [{
|
|
7
|
+
allowedMismatches?: ReadonlyArray<{
|
|
8
|
+
file: string;
|
|
9
|
+
component: string;
|
|
10
|
+
}>;
|
|
11
|
+
}], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
12
|
+
name: string;
|
|
13
|
+
};
|
|
14
|
+
"one-component-per-file": import("@typescript-eslint/utils/ts-eslint").RuleModule<"tooManyExported" | "tooManyComponents", [{
|
|
15
|
+
exportedOnly?: boolean;
|
|
16
|
+
}], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
17
|
+
name: string;
|
|
18
|
+
};
|
|
6
19
|
"cva-merge-base-classes-as-array": import("@typescript-eslint/utils/ts-eslint").RuleModule<"stringNeedsArray" | "arrayNeedsSplit", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
7
20
|
name: string;
|
|
8
21
|
};
|
|
@@ -511,6 +524,8 @@ export declare const reactPreset: ({
|
|
|
511
524
|
"jsdoc/require-jsdoc"?: undefined;
|
|
512
525
|
"no-console"?: undefined;
|
|
513
526
|
"@typescript-eslint/no-empty-function"?: undefined;
|
|
527
|
+
"@trackunit/one-component-per-file"?: undefined;
|
|
528
|
+
"@trackunit/component-name-matches-filename"?: undefined;
|
|
514
529
|
};
|
|
515
530
|
plugins?: undefined;
|
|
516
531
|
settings?: undefined;
|
|
@@ -521,6 +536,20 @@ export declare const reactPreset: ({
|
|
|
521
536
|
"jsdoc/require-jsdoc": string;
|
|
522
537
|
"no-console": string;
|
|
523
538
|
"@typescript-eslint/no-empty-function": string;
|
|
539
|
+
"@trackunit/one-component-per-file": string;
|
|
540
|
+
"@trackunit/component-name-matches-filename": string;
|
|
541
|
+
};
|
|
542
|
+
plugins?: undefined;
|
|
543
|
+
settings?: undefined;
|
|
544
|
+
languageOptions?: undefined;
|
|
545
|
+
} | {
|
|
546
|
+
files: string[];
|
|
547
|
+
rules: {
|
|
548
|
+
"@trackunit/one-component-per-file": string;
|
|
549
|
+
"@trackunit/component-name-matches-filename": string;
|
|
550
|
+
"jsdoc/require-jsdoc"?: undefined;
|
|
551
|
+
"no-console"?: undefined;
|
|
552
|
+
"@typescript-eslint/no-empty-function"?: undefined;
|
|
524
553
|
};
|
|
525
554
|
plugins?: undefined;
|
|
526
555
|
settings?: undefined;
|
|
@@ -596,6 +625,8 @@ export declare const reactPreset: ({
|
|
|
596
625
|
"jsdoc/require-jsdoc"?: undefined;
|
|
597
626
|
"no-console"?: undefined;
|
|
598
627
|
"@typescript-eslint/no-empty-function"?: undefined;
|
|
628
|
+
"@trackunit/one-component-per-file"?: undefined;
|
|
629
|
+
"@trackunit/component-name-matches-filename"?: undefined;
|
|
599
630
|
};
|
|
600
631
|
settings?: undefined;
|
|
601
632
|
})[];
|
|
@@ -66,6 +66,15 @@ exports.reactPreset = [
|
|
|
66
66
|
"jsdoc/require-jsdoc": "off",
|
|
67
67
|
"no-console": "off",
|
|
68
68
|
"@typescript-eslint/no-empty-function": "off",
|
|
69
|
+
"@trackunit/one-component-per-file": "off",
|
|
70
|
+
"@trackunit/component-name-matches-filename": "off",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
files: ["**/*.spec.ts", "**/*.spec.tsx", "**/*.test.ts", "**/*.test.tsx"],
|
|
75
|
+
rules: {
|
|
76
|
+
"@trackunit/one-component-per-file": "off",
|
|
77
|
+
"@trackunit/component-name-matches-filename": "off",
|
|
69
78
|
},
|
|
70
79
|
},
|
|
71
80
|
{
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: component-name-matches-filename
|
|
3
|
+
*
|
|
4
|
+
* Enforces that every exported React component is named after the file it lives in.
|
|
5
|
+
* For a file named `Foo.tsx`, the exported component must be named `Foo`.
|
|
6
|
+
*
|
|
7
|
+
* Anonymous default exports are skipped (no name to compare).
|
|
8
|
+
* `index.*` files are skipped (they are entry-point re-export files, not component files).
|
|
9
|
+
*
|
|
10
|
+
* When combined with `one-component-per-file`, these two rules together fully enforce
|
|
11
|
+
* the "one named component per file" convention.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* // File: Foo.tsx
|
|
15
|
+
* // Bad — exported name doesn't match filename
|
|
16
|
+
* export function Bar() { return <div />; }
|
|
17
|
+
*
|
|
18
|
+
* // Good
|
|
19
|
+
* export function Foo() { return <div />; }
|
|
20
|
+
*/
|
|
21
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
22
|
+
type AllowedMismatch = {
|
|
23
|
+
/** Basename of the file (without extension) where the mismatch is permitted. Supports glob patterns (`*`, `?`). */
|
|
24
|
+
file: string;
|
|
25
|
+
/** Exported component name that is permitted despite not matching the filename. Supports glob patterns (`*`, `?`). */
|
|
26
|
+
component: string;
|
|
27
|
+
};
|
|
28
|
+
type Options = [{
|
|
29
|
+
allowedMismatches?: ReadonlyArray<AllowedMismatch>;
|
|
30
|
+
}];
|
|
31
|
+
export declare const componentNameMatchesFilename: ESLintUtils.RuleModule<"componentNameMismatch", Options, unknown, ESLintUtils.RuleListener> & {
|
|
32
|
+
name: string;
|
|
33
|
+
};
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ESLint rule: component-name-matches-filename
|
|
4
|
+
*
|
|
5
|
+
* Enforces that every exported React component is named after the file it lives in.
|
|
6
|
+
* For a file named `Foo.tsx`, the exported component must be named `Foo`.
|
|
7
|
+
*
|
|
8
|
+
* Anonymous default exports are skipped (no name to compare).
|
|
9
|
+
* `index.*` files are skipped (they are entry-point re-export files, not component files).
|
|
10
|
+
*
|
|
11
|
+
* When combined with `one-component-per-file`, these two rules together fully enforce
|
|
12
|
+
* the "one named component per file" convention.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // File: Foo.tsx
|
|
16
|
+
* // Bad — exported name doesn't match filename
|
|
17
|
+
* export function Bar() { return <div />; }
|
|
18
|
+
*
|
|
19
|
+
* // Good
|
|
20
|
+
* export function Foo() { return <div />; }
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.componentNameMatchesFilename = void 0;
|
|
24
|
+
const tslib_1 = require("tslib");
|
|
25
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
26
|
+
const minimatch_1 = require("minimatch");
|
|
27
|
+
const path_1 = tslib_1.__importDefault(require("path"));
|
|
28
|
+
const component_utils_1 = require("../../utils/component-utils");
|
|
29
|
+
const createRule = utils_1.ESLintUtils.RuleCreator(name => `https://github.com/trackunit/manager/blob/main/libs/eslint/plugin-trackunit/src/lib/rules/${name}/${name}.ts`);
|
|
30
|
+
exports.componentNameMatchesFilename = createRule({
|
|
31
|
+
name: "component-name-matches-filename",
|
|
32
|
+
meta: {
|
|
33
|
+
type: "suggestion",
|
|
34
|
+
docs: {
|
|
35
|
+
description: "Enforce that exported React component names match the filename they live in. " +
|
|
36
|
+
"index.* files and anonymous default exports are exempt.",
|
|
37
|
+
},
|
|
38
|
+
messages: {
|
|
39
|
+
componentNameMismatch: "Component '{{name}}' should match the filename. " + "Rename it to '{{expected}}' or move it to its own file.",
|
|
40
|
+
},
|
|
41
|
+
schema: [
|
|
42
|
+
{
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: {
|
|
45
|
+
allowedMismatches: {
|
|
46
|
+
type: "array",
|
|
47
|
+
description: "Pairs that are exempt from the name-matching rule. Both `file` and `component` support glob patterns " +
|
|
48
|
+
"(`*`, `?`), so one entry can cover a whole family of files (e.g. { file: '*Context', component: '*Provider' }).",
|
|
49
|
+
items: {
|
|
50
|
+
type: "object",
|
|
51
|
+
properties: {
|
|
52
|
+
file: {
|
|
53
|
+
type: "string",
|
|
54
|
+
description: "Basename of the file without extension. Supports glob patterns (`*`, `?`). " +
|
|
55
|
+
"e.g. 'translation' or '*Context'.",
|
|
56
|
+
},
|
|
57
|
+
component: {
|
|
58
|
+
type: "string",
|
|
59
|
+
description: "Exported component name. Supports glob patterns (`*`, `?`). " + "e.g. 'Trans' or '*Provider'.",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
required: ["file", "component"],
|
|
63
|
+
additionalProperties: false,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
additionalProperties: false,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
defaultOptions: [{}],
|
|
72
|
+
create(context, [{ allowedMismatches = [] }]) {
|
|
73
|
+
const filename = context.filename;
|
|
74
|
+
const basename = path_1.default.basename(filename, path_1.default.extname(filename));
|
|
75
|
+
if (basename === "index" || basename === "<input>")
|
|
76
|
+
return {};
|
|
77
|
+
// Normalize kebab-case and snake_case filenames to PascalCase.
|
|
78
|
+
// e.g. `current-weather-status` → `CurrentWeatherStatus`, `app` → `App`.
|
|
79
|
+
const expectedName = basename
|
|
80
|
+
.split(/[-_]/)
|
|
81
|
+
.map(segment => segment.charAt(0).toUpperCase() + segment.slice(1))
|
|
82
|
+
.join("");
|
|
83
|
+
const tracker = (0, component_utils_1.createComponentFileTracker)();
|
|
84
|
+
return {
|
|
85
|
+
FunctionDeclaration: tracker.onFunctionDeclaration,
|
|
86
|
+
VariableDeclarator: tracker.onVariableDeclarator,
|
|
87
|
+
ExportNamedDeclaration: tracker.onExportNamedDeclaration,
|
|
88
|
+
ExportDefaultDeclaration: tracker.onExportDefaultDeclaration,
|
|
89
|
+
"Program:exit"() {
|
|
90
|
+
for (const { name, node } of tracker.getExportedComponents()) {
|
|
91
|
+
if (name === null)
|
|
92
|
+
continue; // anonymous default exports are exempt
|
|
93
|
+
if (name === expectedName)
|
|
94
|
+
continue;
|
|
95
|
+
const isAllowed = allowedMismatches.some(entry => (0, minimatch_1.minimatch)(basename, entry.file) && (0, minimatch_1.minimatch)(name, entry.component));
|
|
96
|
+
if (isAllowed)
|
|
97
|
+
continue;
|
|
98
|
+
context.report({
|
|
99
|
+
node,
|
|
100
|
+
messageId: "componentNameMismatch",
|
|
101
|
+
data: { name, expected: expectedName },
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
//# sourceMappingURL=component-name-matches-filename.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: one-component-per-file
|
|
3
|
+
*
|
|
4
|
+
* Enforces that each file contains at most one React component.
|
|
5
|
+
*
|
|
6
|
+
* Default mode (`exportedOnly: true`): only one exported component is allowed.
|
|
7
|
+
* Internal/private components are fine.
|
|
8
|
+
*
|
|
9
|
+
* Strict mode (`exportedOnly: false`): only one component total, regardless of
|
|
10
|
+
* whether it is exported.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // Bad (default) — two exported components
|
|
14
|
+
* export function Foo() { return <div />; }
|
|
15
|
+
* export function Bar() { return <span />; }
|
|
16
|
+
*
|
|
17
|
+
* // Good (default) — one exported, one internal
|
|
18
|
+
* function InternalHelper() { return <span />; }
|
|
19
|
+
* export function Foo() { return <div><InternalHelper /></div>; }
|
|
20
|
+
*/
|
|
21
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
22
|
+
type MessageIds = "tooManyExported" | "tooManyComponents";
|
|
23
|
+
type Options = [{
|
|
24
|
+
exportedOnly?: boolean;
|
|
25
|
+
}];
|
|
26
|
+
export declare const oneComponentPerFile: ESLintUtils.RuleModule<MessageIds, Options, unknown, ESLintUtils.RuleListener> & {
|
|
27
|
+
name: string;
|
|
28
|
+
};
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ESLint rule: one-component-per-file
|
|
4
|
+
*
|
|
5
|
+
* Enforces that each file contains at most one React component.
|
|
6
|
+
*
|
|
7
|
+
* Default mode (`exportedOnly: true`): only one exported component is allowed.
|
|
8
|
+
* Internal/private components are fine.
|
|
9
|
+
*
|
|
10
|
+
* Strict mode (`exportedOnly: false`): only one component total, regardless of
|
|
11
|
+
* whether it is exported.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* // Bad (default) — two exported components
|
|
15
|
+
* export function Foo() { return <div />; }
|
|
16
|
+
* export function Bar() { return <span />; }
|
|
17
|
+
*
|
|
18
|
+
* // Good (default) — one exported, one internal
|
|
19
|
+
* function InternalHelper() { return <span />; }
|
|
20
|
+
* export function Foo() { return <div><InternalHelper /></div>; }
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.oneComponentPerFile = void 0;
|
|
24
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
25
|
+
const component_utils_1 = require("../../utils/component-utils");
|
|
26
|
+
const createRule = utils_1.ESLintUtils.RuleCreator(name => `https://github.com/trackunit/manager/blob/main/libs/eslint/plugin-trackunit/src/lib/rules/${name}/${name}.ts`);
|
|
27
|
+
exports.oneComponentPerFile = createRule({
|
|
28
|
+
name: "one-component-per-file",
|
|
29
|
+
meta: {
|
|
30
|
+
type: "suggestion",
|
|
31
|
+
docs: {
|
|
32
|
+
description: "Enforce that each file contains at most one React component. " +
|
|
33
|
+
"Use exportedOnly: false to also disallow multiple non-exported components.",
|
|
34
|
+
},
|
|
35
|
+
messages: {
|
|
36
|
+
tooManyExported: "Only one component should be exported per file. Move this component to its own file.",
|
|
37
|
+
tooManyComponents: "Only one component is allowed per file. Move this component to its own file.",
|
|
38
|
+
},
|
|
39
|
+
schema: [
|
|
40
|
+
{
|
|
41
|
+
type: "object",
|
|
42
|
+
properties: {
|
|
43
|
+
exportedOnly: {
|
|
44
|
+
type: "boolean",
|
|
45
|
+
description: "When true (default), only exported components are counted. " +
|
|
46
|
+
"When false, all components in the file count toward the limit.",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
additionalProperties: false,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
defaultOptions: [{ exportedOnly: true }],
|
|
54
|
+
create(context, [{ exportedOnly = true }]) {
|
|
55
|
+
const tracker = (0, component_utils_1.createComponentFileTracker)();
|
|
56
|
+
return {
|
|
57
|
+
FunctionDeclaration: tracker.onFunctionDeclaration,
|
|
58
|
+
VariableDeclarator: tracker.onVariableDeclarator,
|
|
59
|
+
ExportNamedDeclaration: tracker.onExportNamedDeclaration,
|
|
60
|
+
ExportDefaultDeclaration: tracker.onExportDefaultDeclaration,
|
|
61
|
+
"Program:exit"() {
|
|
62
|
+
const components = exportedOnly ? tracker.getExportedComponents() : tracker.getAllComponents();
|
|
63
|
+
if (components.length <= 1)
|
|
64
|
+
return;
|
|
65
|
+
const messageId = exportedOnly ? "tooManyExported" : "tooManyComponents";
|
|
66
|
+
for (const { node } of components.slice(1)) {
|
|
67
|
+
context.report({ node, messageId });
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
//# sourceMappingURL=one-component-per-file.js.map
|
package/src/lib/rules-map.d.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
2
2
|
export declare const rulesMap: {
|
|
3
|
+
"component-name-matches-filename": ESLintUtils.RuleModule<"componentNameMismatch", [{
|
|
4
|
+
allowedMismatches?: ReadonlyArray<{
|
|
5
|
+
file: string;
|
|
6
|
+
component: string;
|
|
7
|
+
}>;
|
|
8
|
+
}], unknown, ESLintUtils.RuleListener> & {
|
|
9
|
+
name: string;
|
|
10
|
+
};
|
|
11
|
+
"one-component-per-file": ESLintUtils.RuleModule<"tooManyExported" | "tooManyComponents", [{
|
|
12
|
+
exportedOnly?: boolean;
|
|
13
|
+
}], unknown, ESLintUtils.RuleListener> & {
|
|
14
|
+
name: string;
|
|
15
|
+
};
|
|
3
16
|
"cva-merge-base-classes-as-array": ESLintUtils.RuleModule<"stringNeedsArray" | "arrayNeedsSplit", [], unknown, ESLintUtils.RuleListener> & {
|
|
4
17
|
name: string;
|
|
5
18
|
};
|
package/src/lib/rules-map.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.rulesMap = void 0;
|
|
4
|
+
const component_name_matches_filename_1 = require("./rules/component-name-matches-filename/component-name-matches-filename");
|
|
4
5
|
const cva_merge_base_classes_as_array_1 = require("./rules/cva-merge-base-classes-as-array/cva-merge-base-classes-as-array");
|
|
5
6
|
const design_guideline_button_icon_size_match_1 = require("./rules/design-guideline-button-icon-size-match/design-guideline-button-icon-size-match");
|
|
6
7
|
const no_internal_barrel_files_1 = require("./rules/no-internal-barrel-files/no-internal-barrel-files");
|
|
@@ -8,6 +9,7 @@ const no_internal_graphql_when_tagged_with_gql_public_1 = require("./rules/no-in
|
|
|
8
9
|
const no_jest_mock_trackunit_react_core_hooks_1 = require("./rules/no-jest-mock-trackunit-react-core-hooks/no-jest-mock-trackunit-react-core-hooks");
|
|
9
10
|
const no_template_strings_in_classname_prop_1 = require("./rules/no-template-strings-in-classname-prop/no-template-strings-in-classname-prop");
|
|
10
11
|
const no_typescript_assertion_1 = require("./rules/no-typescript-assertion/no-typescript-assertion");
|
|
12
|
+
const one_component_per_file_1 = require("./rules/one-component-per-file/one-component-per-file");
|
|
11
13
|
const prefer_destructured_imports_1 = require("./rules/prefer-destructured-imports/prefer-destructured-imports");
|
|
12
14
|
const prefer_event_specific_callback_naming_1 = require("./rules/prefer-event-specific-callback-naming/prefer-event-specific-callback-naming");
|
|
13
15
|
const prefer_field_components_1 = require("./rules/prefer-field-components/prefer-field-components");
|
|
@@ -17,6 +19,8 @@ const require_component_prop_contracts_1 = require("./rules/require-component-pr
|
|
|
17
19
|
const require_list_item_virtualization_props_1 = require("./rules/require-list-item-virtualization-props/require-list-item-virtualization-props");
|
|
18
20
|
const require_optional_prop_initialization_1 = require("./rules/require-optional-prop-initialization/require-optional-prop-initialization");
|
|
19
21
|
exports.rulesMap = {
|
|
22
|
+
"component-name-matches-filename": component_name_matches_filename_1.componentNameMatchesFilename,
|
|
23
|
+
"one-component-per-file": one_component_per_file_1.oneComponentPerFile,
|
|
20
24
|
"cva-merge-base-classes-as-array": cva_merge_base_classes_as_array_1.cvaMergeBaseClassesAsArray,
|
|
21
25
|
"no-internal-barrel-files": no_internal_barrel_files_1.noInternalBarrelFiles,
|
|
22
26
|
"no-typescript-assertion": no_typescript_assertion_1.noTypescriptAssertion,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for detecting React components and their export status in a file.
|
|
3
|
+
*
|
|
4
|
+
* Used by `one-component-per-file` and `component-name-matches-filename` rules.
|
|
5
|
+
*
|
|
6
|
+
* ## Handled patterns
|
|
7
|
+
* - `export function Foo() {}`
|
|
8
|
+
* - `export const Foo = () => {}`
|
|
9
|
+
* - `export default function Foo() {}`
|
|
10
|
+
* - `export default () => {}`
|
|
11
|
+
* - `const Foo = () => {}; export { Foo }`
|
|
12
|
+
* - `const Foo = () => {}; export { Foo as Bar }`
|
|
13
|
+
* - `export const Foo = forwardRef(...)` / `memo(...)` (React wrapper calls)
|
|
14
|
+
*/
|
|
15
|
+
import { TSESTree } from "@typescript-eslint/utils";
|
|
16
|
+
export type ComponentEntry = {
|
|
17
|
+
/** The name as it appears in the export (null for anonymous default exports). */
|
|
18
|
+
name: string | null;
|
|
19
|
+
/** The declaration node to report on (FunctionDeclaration or VariableDeclarator). */
|
|
20
|
+
node: TSESTree.Node;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Creates a stateful tracker that collects React component declarations and export
|
|
24
|
+
* information as ESLint visits AST nodes. Call `getExportedComponents()` or
|
|
25
|
+
* `getAllComponents()` inside a `Program:exit` handler once traversal is complete.
|
|
26
|
+
*/
|
|
27
|
+
export declare const createComponentFileTracker: () => {
|
|
28
|
+
onFunctionDeclaration(node: TSESTree.FunctionDeclaration): void;
|
|
29
|
+
onVariableDeclarator(node: TSESTree.VariableDeclarator): void;
|
|
30
|
+
onExportNamedDeclaration(node: TSESTree.ExportNamedDeclaration): void;
|
|
31
|
+
onExportDefaultDeclaration(node: TSESTree.ExportDefaultDeclaration): void;
|
|
32
|
+
/** Returns all exported React component entries. Call from `Program:exit`. */
|
|
33
|
+
getExportedComponents(): Array<ComponentEntry>;
|
|
34
|
+
/** Returns all top-level React component entries, exported or not. Call from `Program:exit`. */
|
|
35
|
+
getAllComponents(): Array<ComponentEntry>;
|
|
36
|
+
};
|