@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.
Files changed (63) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +2 -0
  3. package/package.json +1 -1
  4. package/src/lib/config/fragments/react-rules.js +15 -0
  5. package/src/lib/config/index.d.ts +57 -0
  6. package/src/lib/config/plugins.d.ts +13 -0
  7. package/src/lib/config/presets/base.d.ts +26 -0
  8. package/src/lib/config/presets/react.d.ts +31 -0
  9. package/src/lib/config/presets/react.js +9 -0
  10. package/src/lib/rules/component-name-matches-filename/component-name-matches-filename.d.ts +34 -0
  11. package/src/lib/rules/component-name-matches-filename/component-name-matches-filename.js +108 -0
  12. package/src/lib/rules/one-component-per-file/one-component-per-file.d.ts +29 -0
  13. package/src/lib/rules/one-component-per-file/one-component-per-file.js +73 -0
  14. package/src/lib/rules-map.d.ts +13 -0
  15. package/src/lib/rules-map.js +4 -0
  16. package/src/lib/utils/component-utils.d.ts +36 -0
  17. package/src/lib/utils/component-utils.js +192 -0
  18. package/src/index.js.map +0 -1
  19. package/src/lib/config/fragments/ignores.js.map +0 -1
  20. package/src/lib/config/fragments/import-rules.js.map +0 -1
  21. package/src/lib/config/fragments/jest-overrides.js.map +0 -1
  22. package/src/lib/config/fragments/jsdoc-rules.js.map +0 -1
  23. package/src/lib/config/fragments/module-boundaries.js.map +0 -1
  24. package/src/lib/config/fragments/react-rules.js.map +0 -1
  25. package/src/lib/config/fragments/restricted-imports.js.map +0 -1
  26. package/src/lib/config/fragments/testing-library.js.map +0 -1
  27. package/src/lib/config/fragments/typescript-rules.js.map +0 -1
  28. package/src/lib/config/index.js.map +0 -1
  29. package/src/lib/config/plugins.js.map +0 -1
  30. package/src/lib/config/presets/base.js.map +0 -1
  31. package/src/lib/config/presets/e2e.js.map +0 -1
  32. package/src/lib/config/presets/react.js.map +0 -1
  33. package/src/lib/config/presets/server.js.map +0 -1
  34. package/src/lib/config/utils.js.map +0 -1
  35. package/src/lib/config-helpers/create-skip-when.js.map +0 -1
  36. package/src/lib/rules/cva-merge-base-classes-as-array/cva-merge-base-classes-as-array.js.map +0 -1
  37. package/src/lib/rules/design-guideline-button-icon-size-match/design-guideline-button-icon-size-match.js.map +0 -1
  38. package/src/lib/rules/no-internal-barrel-files/no-internal-barrel-files.js.map +0 -1
  39. package/src/lib/rules/no-internal-graphql-when-tagged-with-gql-public/no-internal-graphql-when-tagged-with-gql-public.js.map +0 -1
  40. package/src/lib/rules/no-jest-mock-trackunit-react-core-hooks/no-jest-mock-trackunit-react-core-hooks.js.map +0 -1
  41. package/src/lib/rules/no-template-strings-in-classname-prop/no-template-strings-in-classname-prop.js.map +0 -1
  42. package/src/lib/rules/no-typescript-assertion/no-typescript-assertion.js.map +0 -1
  43. package/src/lib/rules/prefer-destructured-imports/prefer-destructured-imports.js.map +0 -1
  44. package/src/lib/rules/prefer-event-specific-callback-naming/name-suggestion-strategies.js.map +0 -1
  45. package/src/lib/rules/prefer-event-specific-callback-naming/prefer-event-specific-callback-naming.js.map +0 -1
  46. package/src/lib/rules/prefer-event-specific-callback-naming/strategies/string-based.js.map +0 -1
  47. package/src/lib/rules/prefer-event-specific-callback-naming/strategies/type-based.js.map +0 -1
  48. package/src/lib/rules/prefer-event-specific-callback-naming/utils.js.map +0 -1
  49. package/src/lib/rules/prefer-field-components/prefer-field-components.js.map +0 -1
  50. package/src/lib/rules/prefer-mouse-event-handler-in-react-props/prefer-mouse-event-handler-in-react-props.js.map +0 -1
  51. package/src/lib/rules/require-classname-alternatives/require-classname-alternatives.js.map +0 -1
  52. package/src/lib/rules/require-component-prop-contracts/require-component-prop-contracts.js.map +0 -1
  53. package/src/lib/rules/require-list-item-virtualization-props/require-list-item-virtualization-props.js.map +0 -1
  54. package/src/lib/rules/require-optional-prop-initialization/require-optional-prop-initialization.js.map +0 -1
  55. package/src/lib/rules/require-optional-prop-initialization/suggestion-utils.js.map +0 -1
  56. package/src/lib/rules-map.js.map +0 -1
  57. package/src/lib/utils/ast-utils.js.map +0 -1
  58. package/src/lib/utils/classname-utils.js.map +0 -1
  59. package/src/lib/utils/file-utils.js.map +0 -1
  60. package/src/lib/utils/import-utils.js.map +0 -1
  61. package/src/lib/utils/nx-utils.js.map +0 -1
  62. package/src/lib/utils/package-utils.js.map +0 -1
  63. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/eslint-plugin-trackunit",
3
- "version": "0.4.67",
3
+ "version": "0.5.2",
4
4
  "license": "SEE LICENSE IN LICENSE.txt",
5
5
  "repository": "https://github.com/Trackunit/manager",
6
6
  "engines": {
@@ -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
@@ -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
  };
@@ -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
+ };