@trackunit/eslint-plugin-trackunit 0.6.152 → 0.6.154

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 CHANGED
@@ -1,3 +1,17 @@
1
+ ## 0.6.154 (2026-09-28)
2
+
3
+ ### 🧱 Updated Dependencies
4
+
5
+ - Updated css-classname-utils to 0.0.55
6
+ - Updated shared-utils to 1.16.57
7
+
8
+ ## 0.6.153 (2026-09-25)
9
+
10
+ ### 🧱 Updated Dependencies
11
+
12
+ - Updated css-classname-utils to 0.0.54
13
+ - Updated shared-utils to 1.16.56
14
+
1
15
  ## 0.6.152 (2026-09-25)
2
16
 
3
17
  ### 🧱 Updated Dependencies
package/README.md CHANGED
@@ -86,9 +86,40 @@ All custom rules are enabled automatically through the presets under the `@track
86
86
 
87
87
  ### Storybook
88
88
 
89
- | Rule | Severity | Auto-fix | Description |
90
- | ----------------------------------------------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------- |
91
- | `@trackunit/require-storybook-folder-landing-page` | error | no | Requires a `*.docs.mdx` landing page (`<Meta title>`) for every ancestor folder implied by a story's title. Enabled for `*.stories.{ts,tsx}` files in the `react` preset. |
89
+ This package owns the `@trackunit/storybook/*` rule implementations and registers the
90
+ `@trackunit/storybook` plugin from the published `react` preset, but the published `react` preset
91
+ does **not enable any rule under that namespace itself**. Inside the `manager` monorepo, a
92
+ separate internal preset (`@trackunit/eslint-plugin-trackunit-internal`'s `storybook` preset,
93
+ consumed via `internalConfigs.storybook`) is what turns these rules on for every story-bearing
94
+ project.
95
+
96
+ | Rule | Severity in `manager` | Auto-fix | Enforces |
97
+ | ----------------------------------------------------- | ---------------------- | -------- | ------------------------------------------------------------------------------------------------------------------- |
98
+ | `@trackunit/storybook/require-folder-landing-page` | error | no | Requires a `*.docs.mdx` landing page (`<Meta title="...">`) for every ancestor folder implied by a story's `title` — including titles read from CSF factory `<binding>.meta({...})` calls. Without a landing page, Storybook renders an empty grouping node in the sidebar. |
99
+ | `@trackunit/require-storybook-folder-landing-page` | not enabled by default | no | Deprecated alias of `@trackunit/storybook/require-folder-landing-page` — same rule implementation, kept only so configs still referencing the pre-namespace id keep working. Neither the published `react` preset nor the internal `storybook` preset enables it; a consumer must re-enable it explicitly. |
100
+ | `@trackunit/storybook/require-csf-factory` | warn | no | Detection-only: flags story files in the legacy `export default {...}` shape (`oldFormat`), files mixing that shape with a factory `.meta({...})` call (`mixedFormat`), and files with neither signal (`missingMeta`). Stays silent on files already in the CSF **factory** format (`<binding>.meta({...})` + `<meta>.story({...})`). |
101
+ | `storybook/*` (upstream `eslint-plugin-storybook@^10.3.5` `flat/recommended`) | per-rule (upstream defaults), with `storybook/default-exports` forced `off` | per-rule | Upstream Storybook lint rules, spread only by the internal `storybook` preset — never by the published `react` preset. `storybook/default-exports` is turned back off immediately after the spread because it contradicts the CSF factory format's implicit default export via the `meta` object; `require-csf-factory` above already reports a story file with no meta/default export at all, so no detection guarantee is lost by disabling it. |
102
+
103
+ **External consumers opt in themselves.** The published `react` preset registers the
104
+ `@trackunit/storybook` plugin (so its rule ids resolve without extra config) but enables zero
105
+ rules under that namespace. If you consume `@trackunit/eslint-plugin-trackunit` outside the
106
+ `manager` monorepo and want this coverage, turn the rules on yourself:
107
+
108
+ ```js
109
+ module.exports = [
110
+ ...configs.base,
111
+ ...configs.react,
112
+ {
113
+ rules: {
114
+ "@trackunit/storybook/require-folder-landing-page": "error",
115
+ "@trackunit/storybook/require-csf-factory": "warn",
116
+ },
117
+ },
118
+ ];
119
+ ```
120
+
121
+ > **Planned:** A `no-local-meta-component` rule is expected to join the
122
+ > `@trackunit/storybook/*` namespace once it's written; it does not exist yet.
92
123
 
93
124
  ### Localization
94
125
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/eslint-plugin-trackunit",
3
- "version": "0.6.152",
3
+ "version": "0.6.154",
4
4
  "license": "SEE LICENSE IN LICENSE.txt",
5
5
  "repository": "https://github.com/Trackunit/manager",
6
6
  "engines": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "dependencies": {
10
10
  "@nx/eslint-plugin": "23.1.0",
11
- "@trackunit/css-classname-utils": "0.0.53",
11
+ "@trackunit/css-classname-utils": "0.0.55",
12
12
  "@typescript-eslint/eslint-plugin": "8.58.1",
13
13
  "@typescript-eslint/utils": "8.58.1",
14
14
  "eslint-config-prettier": "^10.1.8",
package/src/index.d.ts CHANGED
@@ -6,4 +6,5 @@ export { findMonorepoRoot } from "./lib/config/utils";
6
6
  export type { MatchCriteria, ProjectMetadata } from "./lib/utils/nx-utils";
7
7
  export { strictJsdocRules } from "./lib/config/fragments/jsdoc-rules";
8
8
  export { restrictedImportRules } from "./lib/config/fragments/restricted-imports";
9
- export { globals, jsdoc, localRulesPlugin, nx, reactHooks, tsParser } from "./lib/config/plugins";
9
+ export { storyFileGlobs, storyRelaxations } from "./lib/config/fragments/story-relaxations";
10
+ export { globals, jsdoc, localRulesPlugin, nx, reactHooks, storybookPlugin, tsParser } from "./lib/config/plugins";
package/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.tsParser = exports.reactHooks = exports.nx = exports.localRulesPlugin = exports.jsdoc = exports.globals = exports.restrictedImportRules = exports.strictJsdocRules = exports.findMonorepoRoot = exports.createSkipWhen = exports.configs = void 0;
3
+ exports.tsParser = exports.storybookPlugin = exports.reactHooks = exports.nx = exports.localRulesPlugin = exports.jsdoc = exports.globals = exports.storyRelaxations = exports.storyFileGlobs = exports.restrictedImportRules = exports.strictJsdocRules = exports.findMonorepoRoot = exports.createSkipWhen = exports.configs = void 0;
4
4
  const rules_map_1 = require("./lib/rules-map");
5
5
  exports.default = rules_map_1.rulesMap;
6
6
  var config_1 = require("./lib/config");
@@ -13,11 +13,15 @@ var jsdoc_rules_1 = require("./lib/config/fragments/jsdoc-rules");
13
13
  Object.defineProperty(exports, "strictJsdocRules", { enumerable: true, get: function () { return jsdoc_rules_1.strictJsdocRules; } });
14
14
  var restricted_imports_1 = require("./lib/config/fragments/restricted-imports");
15
15
  Object.defineProperty(exports, "restrictedImportRules", { enumerable: true, get: function () { return restricted_imports_1.restrictedImportRules; } });
16
+ var story_relaxations_1 = require("./lib/config/fragments/story-relaxations");
17
+ Object.defineProperty(exports, "storyFileGlobs", { enumerable: true, get: function () { return story_relaxations_1.storyFileGlobs; } });
18
+ Object.defineProperty(exports, "storyRelaxations", { enumerable: true, get: function () { return story_relaxations_1.storyRelaxations; } });
16
19
  var plugins_1 = require("./lib/config/plugins");
17
20
  Object.defineProperty(exports, "globals", { enumerable: true, get: function () { return plugins_1.globals; } });
18
21
  Object.defineProperty(exports, "jsdoc", { enumerable: true, get: function () { return plugins_1.jsdoc; } });
19
22
  Object.defineProperty(exports, "localRulesPlugin", { enumerable: true, get: function () { return plugins_1.localRulesPlugin; } });
20
23
  Object.defineProperty(exports, "nx", { enumerable: true, get: function () { return plugins_1.nx; } });
21
24
  Object.defineProperty(exports, "reactHooks", { enumerable: true, get: function () { return plugins_1.reactHooks; } });
25
+ Object.defineProperty(exports, "storybookPlugin", { enumerable: true, get: function () { return plugins_1.storybookPlugin; } });
22
26
  Object.defineProperty(exports, "tsParser", { enumerable: true, get: function () { return plugins_1.tsParser; } });
23
27
  //# sourceMappingURL=index.js.map
@@ -2,4 +2,3 @@ import type { Linter } from "eslint";
2
2
  export declare const reactRules: Linter.RulesRecord;
3
3
  export declare const reactCustomRules: Linter.RulesRecord;
4
4
  export declare const reactTestingLibraryOverrides: Linter.Config;
5
- export declare const reactStoryOverrides: Linter.Config;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.reactStoryOverrides = exports.reactTestingLibraryOverrides = exports.reactCustomRules = exports.reactRules = void 0;
3
+ exports.reactTestingLibraryOverrides = exports.reactCustomRules = exports.reactRules = void 0;
4
4
  exports.reactRules = {
5
5
  "react-hooks/rules-of-hooks": "error",
6
6
  "react-hooks/exhaustive-deps": "error",
@@ -141,12 +141,4 @@ exports.reactTestingLibraryOverrides = {
141
141
  "jest/expect-expect": "error",
142
142
  },
143
143
  };
144
- exports.reactStoryOverrides = {
145
- files: ["**/*.stories.ts", "**/*.stories.tsx"],
146
- rules: {
147
- "jsdoc/require-jsdoc": "off",
148
- "no-console": "off",
149
- "@typescript-eslint/no-empty-function": "off",
150
- },
151
- };
152
144
  //# sourceMappingURL=react-rules.js.map
@@ -0,0 +1,23 @@
1
+ import type { Linter } from "eslint";
2
+ /**
3
+ * Story-file glob patterns shared by every preset that needs to scope config to Storybook CSF
4
+ * files. Kept as its own export (rather than only living inside `storyRelaxations.files`) so
5
+ * consumers that need the globs for something other than the rule relaxations below (e.g. an
6
+ * `@nx/dependency-checks` `ignoredFiles` entry) can reuse the same list without re-typing it.
7
+ */
8
+ export declare const storyFileGlobs: ReadonlyArray<string>;
9
+ /**
10
+ * The single source of truth for the rule relaxations every story-bearing preset needs. Story
11
+ * files (CSF `meta`/story objects) legitimately:
12
+ * - skip JSDoc (`jsdoc/require-jsdoc`) — they're fixtures, not public API surface.
13
+ * - use `console.*` in play/interaction functions (`no-console`).
14
+ * - declare empty handlers as story args (`@typescript-eslint/no-empty-function`).
15
+ * - export multiple story objects per file and name them after the story, not the file
16
+ * (`@trackunit/one-component-per-file`, `@trackunit/component-name-matches-filename`).
17
+ * - render components without satisfying the prop-contract shape the rule expects from real
18
+ * components (`@trackunit/require-component-prop-contracts`).
19
+ *
20
+ * Intentionally does NOT touch `@trackunit/storybook/*` or `storybook/*` rules — those are
21
+ * registered and enabled by separate presets, not by this rule-relaxation fragment.
22
+ */
23
+ export declare const storyRelaxations: Linter.Config;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.storyRelaxations = exports.storyFileGlobs = void 0;
4
+ /**
5
+ * Story-file glob patterns shared by every preset that needs to scope config to Storybook CSF
6
+ * files. Kept as its own export (rather than only living inside `storyRelaxations.files`) so
7
+ * consumers that need the globs for something other than the rule relaxations below (e.g. an
8
+ * `@nx/dependency-checks` `ignoredFiles` entry) can reuse the same list without re-typing it.
9
+ */
10
+ exports.storyFileGlobs = ["**/*.stories.ts", "**/*.stories.tsx"];
11
+ /**
12
+ * The single source of truth for the rule relaxations every story-bearing preset needs. Story
13
+ * files (CSF `meta`/story objects) legitimately:
14
+ * - skip JSDoc (`jsdoc/require-jsdoc`) — they're fixtures, not public API surface.
15
+ * - use `console.*` in play/interaction functions (`no-console`).
16
+ * - declare empty handlers as story args (`@typescript-eslint/no-empty-function`).
17
+ * - export multiple story objects per file and name them after the story, not the file
18
+ * (`@trackunit/one-component-per-file`, `@trackunit/component-name-matches-filename`).
19
+ * - render components without satisfying the prop-contract shape the rule expects from real
20
+ * components (`@trackunit/require-component-prop-contracts`).
21
+ *
22
+ * Intentionally does NOT touch `@trackunit/storybook/*` or `storybook/*` rules — those are
23
+ * registered and enabled by separate presets, not by this rule-relaxation fragment.
24
+ */
25
+ exports.storyRelaxations = {
26
+ files: [...exports.storyFileGlobs],
27
+ rules: {
28
+ "jsdoc/require-jsdoc": "off",
29
+ "no-console": "off",
30
+ "@typescript-eslint/no-empty-function": "off",
31
+ "@trackunit/one-component-per-file": "off",
32
+ "@trackunit/component-name-matches-filename": "off",
33
+ "@trackunit/require-component-prop-contracts": "off",
34
+ },
35
+ };
36
+ //# sourceMappingURL=story-relaxations.js.map
@@ -603,6 +603,16 @@ export declare const configs: {
603
603
  };
604
604
  };
605
605
  };
606
+ "@trackunit/storybook": {
607
+ rules: {
608
+ "require-folder-landing-page": import("@typescript-eslint/utils/ts-eslint").RuleModule<"missingLandingPage", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
609
+ name: string;
610
+ };
611
+ "require-csf-factory": import("@typescript-eslint/utils/ts-eslint").RuleModule<"oldFormat" | "mixedFormat" | "missingMeta", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
612
+ name: string;
613
+ };
614
+ };
615
+ };
606
616
  "@typescript-eslint": {
607
617
  configs: Record<string, import("@typescript-eslint/utils/ts-eslint").ClassicConfig.Config>;
608
618
  meta: import("typescript-eslint").FlatConfig.PluginMeta;
@@ -1038,26 +1048,8 @@ export declare const configs: {
1038
1048
  })[];
1039
1049
  "no-unreachable": string;
1040
1050
  "prefer-spread": string;
1041
- "jsdoc/require-jsdoc"?: undefined;
1042
- "no-console"?: undefined;
1043
- "@typescript-eslint/no-empty-function"?: undefined;
1044
1051
  "@trackunit/one-component-per-file"?: undefined;
1045
1052
  "@trackunit/component-name-matches-filename"?: undefined;
1046
- "@trackunit/require-storybook-folder-landing-page"?: undefined;
1047
- };
1048
- plugins?: undefined;
1049
- settings?: undefined;
1050
- languageOptions?: undefined;
1051
- } | {
1052
- files: string[];
1053
- rules: {
1054
- "jsdoc/require-jsdoc": string;
1055
- "no-console": string;
1056
- "@typescript-eslint/no-empty-function": string;
1057
- "@trackunit/one-component-per-file": string;
1058
- "@trackunit/component-name-matches-filename": string;
1059
- "@trackunit/require-storybook-folder-landing-page": string;
1060
- "no-restricted-properties"?: undefined;
1061
1053
  };
1062
1054
  plugins?: undefined;
1063
1055
  settings?: undefined;
@@ -1068,10 +1060,6 @@ export declare const configs: {
1068
1060
  "@trackunit/one-component-per-file": string;
1069
1061
  "@trackunit/component-name-matches-filename": string;
1070
1062
  "no-restricted-properties": string;
1071
- "jsdoc/require-jsdoc"?: undefined;
1072
- "no-console"?: undefined;
1073
- "@typescript-eslint/no-empty-function"?: undefined;
1074
- "@trackunit/require-storybook-folder-landing-page"?: undefined;
1075
1063
  };
1076
1064
  plugins?: undefined;
1077
1065
  settings?: undefined;
@@ -1125,6 +1113,7 @@ export declare const configs: {
1125
1113
  };
1126
1114
  };
1127
1115
  "@trackunit"?: undefined;
1116
+ "@trackunit/storybook"?: undefined;
1128
1117
  "@typescript-eslint"?: undefined;
1129
1118
  jsdoc?: undefined;
1130
1119
  react?: undefined;
@@ -1144,12 +1133,8 @@ export declare const configs: {
1144
1133
  rules: {
1145
1134
  "jest/no-commented-out-tests": string;
1146
1135
  "jest/expect-expect": string;
1147
- "jsdoc/require-jsdoc"?: undefined;
1148
- "no-console"?: undefined;
1149
- "@typescript-eslint/no-empty-function"?: undefined;
1150
1136
  "@trackunit/one-component-per-file"?: undefined;
1151
1137
  "@trackunit/component-name-matches-filename"?: undefined;
1152
- "@trackunit/require-storybook-folder-landing-page"?: undefined;
1153
1138
  "no-restricted-properties"?: undefined;
1154
1139
  };
1155
1140
  settings?: undefined;
@@ -121,4 +121,23 @@ export declare const localRulesPlugin: {
121
121
  };
122
122
  };
123
123
  };
124
+ /**
125
+ * Shared plugin object for the `@trackunit/storybook` namespace. Rule code stays in this
126
+ * package; this object is what presets (and external consumers who opt in directly) register
127
+ * under the `@trackunit/storybook` plugin key so ESLint resolves e.g.
128
+ * `@trackunit/storybook/require-folder-landing-page` to plugin `@trackunit/storybook` + rule
129
+ * `require-folder-landing-page`. Must stay a single object reference — flat config compares
130
+ * plugin references by identity — so import this export everywhere the namespace is registered,
131
+ * never construct a new `{ rules: {...} }` object elsewhere.
132
+ */
133
+ export declare const storybookPlugin: {
134
+ rules: {
135
+ "require-folder-landing-page": import("@typescript-eslint/utils/ts-eslint").RuleModule<"missingLandingPage", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
136
+ name: string;
137
+ };
138
+ "require-csf-factory": import("@typescript-eslint/utils/ts-eslint").RuleModule<"oldFormat" | "mixedFormat" | "missingMeta", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
139
+ name: string;
140
+ };
141
+ };
142
+ };
124
143
  export { eslintConfigPrettier, globals, importPlugin, jest, jsdoc, jsoncParser, noNull, nx, react, reactHooks, testingLibrary, tsParser, typescriptEslint, };
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.typescriptEslint = exports.tsParser = exports.testingLibrary = exports.reactHooks = exports.react = exports.nx = exports.noNull = exports.jsoncParser = exports.jsdoc = exports.jest = exports.importPlugin = exports.globals = exports.eslintConfigPrettier = exports.localRulesPlugin = void 0;
3
+ exports.typescriptEslint = exports.tsParser = exports.testingLibrary = exports.reactHooks = exports.react = exports.nx = exports.noNull = exports.jsoncParser = exports.jsdoc = exports.jest = exports.importPlugin = exports.globals = exports.eslintConfigPrettier = exports.storybookPlugin = exports.localRulesPlugin = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  /**
6
6
  * Shared plugin instances for ESLint flat config.
@@ -37,6 +37,23 @@ const globals = tslib_1.__importStar(require("globals"));
37
37
  exports.globals = globals;
38
38
  const jsoncParser = tslib_1.__importStar(require("jsonc-eslint-parser"));
39
39
  exports.jsoncParser = jsoncParser;
40
+ const require_csf_factory_1 = require("../rules/require-csf-factory/require-csf-factory");
41
+ const require_storybook_folder_landing_page_1 = require("../rules/require-storybook-folder-landing-page/require-storybook-folder-landing-page");
40
42
  const rules_map_1 = require("../rules-map");
41
43
  exports.localRulesPlugin = { rules: rules_map_1.rulesMap };
44
+ /**
45
+ * Shared plugin object for the `@trackunit/storybook` namespace. Rule code stays in this
46
+ * package; this object is what presets (and external consumers who opt in directly) register
47
+ * under the `@trackunit/storybook` plugin key so ESLint resolves e.g.
48
+ * `@trackunit/storybook/require-folder-landing-page` to plugin `@trackunit/storybook` + rule
49
+ * `require-folder-landing-page`. Must stay a single object reference — flat config compares
50
+ * plugin references by identity — so import this export everywhere the namespace is registered,
51
+ * never construct a new `{ rules: {...} }` object elsewhere.
52
+ */
53
+ exports.storybookPlugin = {
54
+ rules: {
55
+ "require-folder-landing-page": require_storybook_folder_landing_page_1.requireFolderLandingPage,
56
+ "require-csf-factory": require_csf_factory_1.requireCsfFactory,
57
+ },
58
+ };
42
59
  //# sourceMappingURL=plugins.js.map
@@ -9,6 +9,7 @@ const jsdoc_rules_1 = require("../fragments/jsdoc-rules");
9
9
  const module_boundaries_1 = require("../fragments/module-boundaries");
10
10
  const no_direct_storage_access_1 = require("../fragments/no-direct-storage-access");
11
11
  const restricted_imports_1 = require("../fragments/restricted-imports");
12
+ const story_relaxations_1 = require("../fragments/story-relaxations");
12
13
  const typescript_rules_1 = require("../fragments/typescript-rules");
13
14
  const plugins_1 = require("../plugins");
14
15
  const utils_1 = require("../utils");
@@ -187,6 +188,9 @@ exports.base = [
187
188
  },
188
189
  },
189
190
  plugins_1.eslintConfigPrettier,
191
+ // Shared with the `react` preset and the internal `publicApi` preset. Consumers that only
192
+ // spread `base` (without `react`) still get the six rule relaxations for story files.
193
+ story_relaxations_1.storyRelaxations,
190
194
  {
191
195
  files: ["**/*.stories.{ts,tsx,js,jsx}", "**/*.demo.{ts,tsx,js,jsx}"],
192
196
  rules: {
@@ -102,6 +102,16 @@ export declare const reactPreset: (import("eslint").Linter.Config<import("eslint
102
102
  };
103
103
  };
104
104
  };
105
+ "@trackunit/storybook": {
106
+ rules: {
107
+ "require-folder-landing-page": import("@typescript-eslint/utils/ts-eslint").RuleModule<"missingLandingPage", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
108
+ name: string;
109
+ };
110
+ "require-csf-factory": import("@typescript-eslint/utils/ts-eslint").RuleModule<"oldFormat" | "mixedFormat" | "missingMeta", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
111
+ name: string;
112
+ };
113
+ };
114
+ };
105
115
  "@typescript-eslint": {
106
116
  configs: Record<string, import("@typescript-eslint/utils/ts-eslint").ClassicConfig.Config>;
107
117
  meta: import("typescript-eslint").FlatConfig.PluginMeta;
@@ -537,26 +547,8 @@ export declare const reactPreset: (import("eslint").Linter.Config<import("eslint
537
547
  })[];
538
548
  "no-unreachable": string;
539
549
  "prefer-spread": string;
540
- "jsdoc/require-jsdoc"?: undefined;
541
- "no-console"?: undefined;
542
- "@typescript-eslint/no-empty-function"?: undefined;
543
550
  "@trackunit/one-component-per-file"?: undefined;
544
551
  "@trackunit/component-name-matches-filename"?: undefined;
545
- "@trackunit/require-storybook-folder-landing-page"?: undefined;
546
- };
547
- plugins?: undefined;
548
- settings?: undefined;
549
- languageOptions?: undefined;
550
- } | {
551
- files: string[];
552
- rules: {
553
- "jsdoc/require-jsdoc": string;
554
- "no-console": string;
555
- "@typescript-eslint/no-empty-function": string;
556
- "@trackunit/one-component-per-file": string;
557
- "@trackunit/component-name-matches-filename": string;
558
- "@trackunit/require-storybook-folder-landing-page": string;
559
- "no-restricted-properties"?: undefined;
560
552
  };
561
553
  plugins?: undefined;
562
554
  settings?: undefined;
@@ -567,10 +559,6 @@ export declare const reactPreset: (import("eslint").Linter.Config<import("eslint
567
559
  "@trackunit/one-component-per-file": string;
568
560
  "@trackunit/component-name-matches-filename": string;
569
561
  "no-restricted-properties": string;
570
- "jsdoc/require-jsdoc"?: undefined;
571
- "no-console"?: undefined;
572
- "@typescript-eslint/no-empty-function"?: undefined;
573
- "@trackunit/require-storybook-folder-landing-page"?: undefined;
574
562
  };
575
563
  plugins?: undefined;
576
564
  settings?: undefined;
@@ -624,6 +612,7 @@ export declare const reactPreset: (import("eslint").Linter.Config<import("eslint
624
612
  };
625
613
  };
626
614
  "@trackunit"?: undefined;
615
+ "@trackunit/storybook"?: undefined;
627
616
  "@typescript-eslint"?: undefined;
628
617
  jsdoc?: undefined;
629
618
  react?: undefined;
@@ -643,12 +632,8 @@ export declare const reactPreset: (import("eslint").Linter.Config<import("eslint
643
632
  rules: {
644
633
  "jest/no-commented-out-tests": string;
645
634
  "jest/expect-expect": string;
646
- "jsdoc/require-jsdoc"?: undefined;
647
- "no-console"?: undefined;
648
- "@typescript-eslint/no-empty-function"?: undefined;
649
635
  "@trackunit/one-component-per-file"?: undefined;
650
636
  "@trackunit/component-name-matches-filename"?: undefined;
651
- "@trackunit/require-storybook-folder-landing-page"?: undefined;
652
637
  "no-restricted-properties"?: undefined;
653
638
  };
654
639
  settings?: undefined;
@@ -3,12 +3,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.reactPreset = void 0;
4
4
  const no_direct_storage_access_1 = require("../fragments/no-direct-storage-access");
5
5
  const react_rules_1 = require("../fragments/react-rules");
6
+ const story_relaxations_1 = require("../fragments/story-relaxations");
6
7
  const testing_library_1 = require("../fragments/testing-library");
7
8
  const plugins_1 = require("../plugins");
8
9
  exports.reactPreset = [
9
10
  {
10
11
  plugins: {
11
12
  "@trackunit": plugins_1.localRulesPlugin,
13
+ // Registers the `@trackunit/storybook` namespace so opt-in consumers can reference
14
+ // `@trackunit/storybook/require-folder-landing-page` without extra config. This preset
15
+ // intentionally does not enable any rule under that key itself.
16
+ "@trackunit/storybook": plugins_1.storybookPlugin,
12
17
  "@typescript-eslint": plugins_1.typescriptEslint,
13
18
  jsdoc: plugins_1.jsdoc,
14
19
  react: plugins_1.react,
@@ -63,19 +68,12 @@ exports.reactPreset = [
63
68
  },
64
69
  },
65
70
  no_direct_storage_access_1.noDirectStoragePropertyExceptionOverrides,
66
- {
67
- files: ["**/*.stories.ts", "**/*.stories.tsx"],
68
- rules: {
69
- "jsdoc/require-jsdoc": "off",
70
- "no-console": "off",
71
- "@typescript-eslint/no-empty-function": "off",
72
- "@trackunit/one-component-per-file": "off",
73
- "@trackunit/component-name-matches-filename": "off",
74
- // Every Storybook folder implied by a story title must have a landing page
75
- // (*.docs.mdx with a matching <Meta title>). See docs/STORYBOOK-LANDING-PAGES.md.
76
- "@trackunit/require-storybook-folder-landing-page": "error",
77
- },
78
- },
71
+ // The published preset registers `@trackunit/storybook` (above) for opt-in use but does NOT
72
+ // enable `@trackunit/storybook/require-folder-landing-page` (nor its deprecated
73
+ // `@trackunit/require-storybook-folder-landing-page` alias) here. External consumers who want
74
+ // the landing-page check opt in explicitly. The rule relaxations below are unrelated to that
75
+ // namespace and shared with `base` and the internal `publicApi` preset.
76
+ story_relaxations_1.storyRelaxations,
79
77
  {
80
78
  files: ["**/*.spec.ts", "**/*.spec.tsx", "**/*.test.ts", "**/*.test.tsx"],
81
79
  rules: {
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Warn when a `*.stories.*` file is written in the legacy CSF (Component Story Format)
3
+ * default-export shape instead of Storybook's CSF **factory** format.
4
+ *
5
+ * Architecture: per-story-file rule, syntax-only (no TypeScript type information — see
6
+ * `isFactoryMetaCall`'s doc comment for why type-checking `component:` is unnecessary).
7
+ *
8
+ * Recognized shapes, per file:
9
+ * - **Old format** — any `export default ...` in the file (the legacy `export default { title,
10
+ * component, ... }` meta object, whether inline or via a `const meta = {...}; export default
11
+ * meta;` variable), and no factory `.meta({...})` call anywhere in the file. Reported with
12
+ * `oldFormat`, pointing at the `export default` node.
13
+ * - **Factory format** (silent) — a call `<importedBinding>.meta({...})` (e.g.
14
+ * `const meta = preview.meta({ component, title });`), where `<importedBinding>` resolves to
15
+ * an imported identifier, and NEITHER an `export default` NOR an old-style named
16
+ * object-literal story export (`export const Foo = { args: {...} };`, including a `satisfies` or
17
+ * `as` wrapper around that object) is present. Individual
18
+ * stories are expected to be exported as `<meta>.story({...})`, but this rule does not require
19
+ * at least one such export to exist — it only needs the absence of old-format signals.
20
+ * A `component:` property that is a generic/instantiation expression (e.g.
21
+ * `MultiSelectField<Option>`) still matches this shape: the rule only inspects the *call*
22
+ * shape, never the properties inside the `.meta({...})` object, so it never needs to
23
+ * type-check `component`.
24
+ * - **Mixed format** — a factory `.meta({...})` call together with an
25
+ * `export default` OR an old-style object-literal named story export. Reported with
26
+ * `mixedFormat`, pointing at the `.meta({...})` call.
27
+ * - **Missing meta** — neither an `export default` nor a factory
28
+ * `.meta({...})` call anywhere in the file. Reported with `missingMeta`, pointing at the
29
+ * file's `Program` node (there is no more specific node to point at — the file declares no
30
+ * meta object at all).
31
+ *
32
+ * Never visits `.storybook/` config files, `*.demo.*` files, or `*.mdx` files, even if their
33
+ * name would otherwise match the story-file pattern.
34
+ *
35
+ * This is a **detection-only** rule: no autofix (`meta.fixable` unset) and no suggestions
36
+ * (`meta.hasSuggestions` unset) — migrating a story file to the factory format requires
37
+ * rewriting the whole file's shape (imports, meta declaration, every story export), which is far
38
+ * beyond what a syntax-preserving ESLint fixer can safely produce. Storybook ships an official
39
+ * codemod for this (`npx storybook@latest automigrate csf-factories`) — see
40
+ * `REQUIRE_CSF_FACTORY_DOCS_URL`.
41
+ *
42
+ * See README.md for incorrect/correct examples.
43
+ */
44
+ import { ESLintUtils } from "@typescript-eslint/utils";
45
+ type MessageIds = "oldFormat" | "mixedFormat" | "missingMeta";
46
+ /**
47
+ * Canonical permalink to the how-to-migrate doc. Single source of truth, embedded in every
48
+ * message so developers tripping the rule get a direct link. Mirrors the
49
+ * `STORYBOOK_LANDING_PAGE_DOCS_URL` idiom in `../require-storybook-folder-landing-page/require-storybook-folder-landing-page.ts`.
50
+ */
51
+ export declare const REQUIRE_CSF_FACTORY_DOCS_URL = "https://github.com/Trackunit/manager/blob/master/docs/STORYBOOK-CSF-FACTORY.md";
52
+ export declare const requireCsfFactory: ESLintUtils.RuleModule<MessageIds, [], unknown, ESLintUtils.RuleListener> & {
53
+ name: string;
54
+ };
55
+ export {};
@@ -0,0 +1,185 @@
1
+ "use strict";
2
+ /**
3
+ * Warn when a `*.stories.*` file is written in the legacy CSF (Component Story Format)
4
+ * default-export shape instead of Storybook's CSF **factory** format.
5
+ *
6
+ * Architecture: per-story-file rule, syntax-only (no TypeScript type information — see
7
+ * `isFactoryMetaCall`'s doc comment for why type-checking `component:` is unnecessary).
8
+ *
9
+ * Recognized shapes, per file:
10
+ * - **Old format** — any `export default ...` in the file (the legacy `export default { title,
11
+ * component, ... }` meta object, whether inline or via a `const meta = {...}; export default
12
+ * meta;` variable), and no factory `.meta({...})` call anywhere in the file. Reported with
13
+ * `oldFormat`, pointing at the `export default` node.
14
+ * - **Factory format** (silent) — a call `<importedBinding>.meta({...})` (e.g.
15
+ * `const meta = preview.meta({ component, title });`), where `<importedBinding>` resolves to
16
+ * an imported identifier, and NEITHER an `export default` NOR an old-style named
17
+ * object-literal story export (`export const Foo = { args: {...} };`, including a `satisfies` or
18
+ * `as` wrapper around that object) is present. Individual
19
+ * stories are expected to be exported as `<meta>.story({...})`, but this rule does not require
20
+ * at least one such export to exist — it only needs the absence of old-format signals.
21
+ * A `component:` property that is a generic/instantiation expression (e.g.
22
+ * `MultiSelectField<Option>`) still matches this shape: the rule only inspects the *call*
23
+ * shape, never the properties inside the `.meta({...})` object, so it never needs to
24
+ * type-check `component`.
25
+ * - **Mixed format** — a factory `.meta({...})` call together with an
26
+ * `export default` OR an old-style object-literal named story export. Reported with
27
+ * `mixedFormat`, pointing at the `.meta({...})` call.
28
+ * - **Missing meta** — neither an `export default` nor a factory
29
+ * `.meta({...})` call anywhere in the file. Reported with `missingMeta`, pointing at the
30
+ * file's `Program` node (there is no more specific node to point at — the file declares no
31
+ * meta object at all).
32
+ *
33
+ * Never visits `.storybook/` config files, `*.demo.*` files, or `*.mdx` files, even if their
34
+ * name would otherwise match the story-file pattern.
35
+ *
36
+ * This is a **detection-only** rule: no autofix (`meta.fixable` unset) and no suggestions
37
+ * (`meta.hasSuggestions` unset) — migrating a story file to the factory format requires
38
+ * rewriting the whole file's shape (imports, meta declaration, every story export), which is far
39
+ * beyond what a syntax-preserving ESLint fixer can safely produce. Storybook ships an official
40
+ * codemod for this (`npx storybook@latest automigrate csf-factories`) — see
41
+ * `REQUIRE_CSF_FACTORY_DOCS_URL`.
42
+ *
43
+ * See README.md for incorrect/correct examples.
44
+ */
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.requireCsfFactory = exports.REQUIRE_CSF_FACTORY_DOCS_URL = void 0;
47
+ const utils_1 = require("@typescript-eslint/utils");
48
+ const createRule = utils_1.ESLintUtils.RuleCreator(name => `https://github.com/trackunit/manager/blob/master/libs/eslint/plugin-trackunit/src/lib/rules/${name}/${name}.ts`);
49
+ /**
50
+ * Canonical permalink to the how-to-migrate doc. Single source of truth, embedded in every
51
+ * message so developers tripping the rule get a direct link. Mirrors the
52
+ * `STORYBOOK_LANDING_PAGE_DOCS_URL` idiom in `../require-storybook-folder-landing-page/require-storybook-folder-landing-page.ts`.
53
+ */
54
+ exports.REQUIRE_CSF_FACTORY_DOCS_URL = "https://github.com/Trackunit/manager/blob/master/docs/STORYBOOK-CSF-FACTORY.md";
55
+ // Only analyze story files — mirrors `STORY_FILE_REGEX` in the landing-page rule.
56
+ const STORY_FILE_REGEX = /\.stories\.(js|jsx|ts|tsx)$/;
57
+ // Excluded even when the name would otherwise match a story file.
58
+ const STORYBOOK_CONFIG_DIR_REGEX = /(^|\/)\.storybook\//;
59
+ // Not anchored to the end: a real demo file is named `Foo.demo.tsx` (never also `.stories.`,
60
+ // so it's already excluded by `STORY_FILE_REGEX` below), but this stays non-anchored so it also
61
+ // excludes a hypothetical combined name like `Foo.demo.stories.tsx` rather than relying on that
62
+ // combination never existing.
63
+ const DEMO_FILE_REGEX = /\.demo\./;
64
+ const MDX_REGEX = /\.mdx$/;
65
+ const isAnalyzableStoryFile = (filename) => {
66
+ if (MDX_REGEX.test(filename)) {
67
+ return false;
68
+ }
69
+ if (STORYBOOK_CONFIG_DIR_REGEX.test(filename)) {
70
+ return false;
71
+ }
72
+ if (DEMO_FILE_REGEX.test(filename)) {
73
+ return false;
74
+ }
75
+ return STORY_FILE_REGEX.test(filename);
76
+ };
77
+ /**
78
+ * True for a call `<identifier>.meta({...})` where `<identifier>` resolves to an imported
79
+ * binding. Only the call *shape* is inspected — the object argument's properties (e.g. a
80
+ * generic/instantiation `component:` value) are never type-checked or destructured, so this
81
+ * rule stays syntax-only.
82
+ */
83
+ const isFactoryMetaCall = (node, context) => {
84
+ const { callee } = node;
85
+ if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
86
+ return false;
87
+ }
88
+ if (callee.property.type !== utils_1.AST_NODE_TYPES.Identifier || callee.property.name !== "meta") {
89
+ return false;
90
+ }
91
+ if (callee.object.type !== utils_1.AST_NODE_TYPES.Identifier) {
92
+ return false;
93
+ }
94
+ const [firstArgument] = node.arguments;
95
+ if (firstArgument === undefined || firstArgument.type !== utils_1.AST_NODE_TYPES.ObjectExpression) {
96
+ return false;
97
+ }
98
+ const scope = context.sourceCode.getScope(callee.object);
99
+ const variable = scope.references.find(ref => ref.identifier === callee.object)?.resolved;
100
+ if (!variable) {
101
+ return false;
102
+ }
103
+ return variable.defs.some(def => def.type === "ImportBinding");
104
+ };
105
+ /** Unwrap `satisfies` / `as` wrappers, same as the landing-page rule's `unwrapExpression`. */
106
+ const unwrapExpression = (node) => {
107
+ let current = node;
108
+ while (current.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression || current.type === utils_1.AST_NODE_TYPES.TSAsExpression) {
109
+ current = current.expression;
110
+ }
111
+ return current;
112
+ };
113
+ /**
114
+ * An old-style named story export: `export const Foo = { ... };` (object literal, not a call).
115
+ * `satisfies` and `as` wrappers still count (`export const Legacy = { args: {} } satisfies StoryObj`),
116
+ * matching how current stories write `export const Default: Story = { ... }` with a bare object.
117
+ */
118
+ const isObjectLiteralStoryExport = (node) => {
119
+ const { declaration } = node;
120
+ if (declaration === null || declaration.type !== utils_1.AST_NODE_TYPES.VariableDeclaration) {
121
+ return false;
122
+ }
123
+ return declaration.declarations.some(declarator => {
124
+ if (declarator.init === null) {
125
+ return false;
126
+ }
127
+ return unwrapExpression(declarator.init).type === utils_1.AST_NODE_TYPES.ObjectExpression;
128
+ });
129
+ };
130
+ const create = context => {
131
+ if (!isAnalyzableStoryFile(context.filename)) {
132
+ return {};
133
+ }
134
+ let defaultExportNode = null;
135
+ let metaCallNode = null;
136
+ let hasObjectLiteralStoryExport = false;
137
+ return {
138
+ ExportDefaultDeclaration(node) {
139
+ defaultExportNode = node;
140
+ },
141
+ ExportNamedDeclaration(node) {
142
+ if (isObjectLiteralStoryExport(node)) {
143
+ hasObjectLiteralStoryExport = true;
144
+ }
145
+ },
146
+ CallExpression(node) {
147
+ if (metaCallNode === null && isFactoryMetaCall(node, context)) {
148
+ metaCallNode = node;
149
+ }
150
+ },
151
+ "Program:exit"(node) {
152
+ const hasDefaultExport = defaultExportNode !== null;
153
+ if (!hasDefaultExport && metaCallNode === null) {
154
+ context.report({ node, messageId: "missingMeta" });
155
+ return;
156
+ }
157
+ if (metaCallNode !== null && (hasDefaultExport || hasObjectLiteralStoryExport)) {
158
+ context.report({ node: metaCallNode, messageId: "mixedFormat" });
159
+ return;
160
+ }
161
+ if (defaultExportNode !== null) {
162
+ context.report({ node: defaultExportNode, messageId: "oldFormat" });
163
+ }
164
+ // Otherwise: factory format (a `.meta({...})` call with no old-format signals) — silent.
165
+ },
166
+ };
167
+ };
168
+ exports.requireCsfFactory = createRule({
169
+ name: "require-csf-factory",
170
+ meta: {
171
+ type: "problem",
172
+ docs: {
173
+ description: "Warn on Storybook story files that are not written in CSF factory format",
174
+ },
175
+ schema: [],
176
+ messages: {
177
+ oldFormat: `[require-csf-factory] This story file uses the legacy \`export default { ... }\` CSF format instead of Storybook's CSF factory format (\`preview.meta({...})\` + \`meta.story({...})\`). See ${exports.REQUIRE_CSF_FACTORY_DOCS_URL} for how to migrate.`,
178
+ mixedFormat: `[require-csf-factory] This story file mixes a factory \`.meta({...})\` call with legacy \`export default\`/object-literal story exports. Migrate the whole file to the CSF factory format. See ${exports.REQUIRE_CSF_FACTORY_DOCS_URL} for how to migrate.`,
179
+ missingMeta: `[require-csf-factory] This story file declares no meta object (neither \`export default\` nor a factory \`.meta({...})\` call). See ${exports.REQUIRE_CSF_FACTORY_DOCS_URL} for how to migrate.`,
180
+ },
181
+ },
182
+ defaultOptions: [],
183
+ create,
184
+ });
185
+ //# sourceMappingURL=require-csf-factory.js.map
@@ -3,10 +3,13 @@
3
3
  * FOLDER implied by a story's title.
4
4
  *
5
5
  * Architecture: per-story-file rule. Each `*.stories.@(js|jsx|ts|tsx)` file declares a Storybook
6
- * `title` (e.g. "Hooks/react-core-hooks/useDebounce"). The folders implied by that title are the
7
- * accumulated path prefixes EXCLUDING the story leaf (here: "Hooks" and "Hooks/react-core-hooks").
8
- * Every such folder must have a landing page: a `*.docs.mdx` somewhere in the workspace whose
9
- * `<Meta title="FOLDER" />` EXACTLY equals the folder path, with non-empty body and no TODO marker.
6
+ * `title` (e.g. "Hooks/react-core-hooks/useDebounce"), read from either the legacy default-export
7
+ * meta object (`export default { title, ... }`, including via an intermediate variable) or a
8
+ * Storybook CSF factory `.meta({...})` call (`const meta = preview.meta({ title, ... });`).
9
+ * The folders implied by that title are the accumulated path prefixes
10
+ * EXCLUDING the story leaf (here: "Hooks" and "Hooks/react-core-hooks"). Every such folder must
11
+ * have a landing page: a `*.docs.mdx` somewhere in the workspace whose `<Meta title="FOLDER" />`
12
+ * EXACTLY equals the folder path, with non-empty body and no TODO marker.
10
13
  *
11
14
  * The rule is REPORT-ONLY: it does not scaffold files. Creating a separate `*.docs.mdx` cannot be
12
15
  * expressed as a pure ESLint fix, and ESLint evaluates fix/suggestion callbacks on every lint pass
@@ -18,6 +21,13 @@
18
21
  import { ESLintUtils } from "@typescript-eslint/utils";
19
22
  /** Canonical TODO marker. A landing page still containing this marker does NOT satisfy the rule. */
20
23
  export declare const STORYBOOK_LANDING_PAGE_TODO_MARKER = "{/* TODO: storybook-folder-landing-page */}";
24
+ /**
25
+ * The namespaced rule id (plugin `@trackunit/storybook`, rule `require-folder-landing-page`).
26
+ * Registered as the `require-folder-landing-page` entry of the shared `storybookPlugin` object
27
+ * (see `../../config/plugins.ts`). This is the id the deprecated alias below points at via
28
+ * `meta.deprecated.replacedBy`.
29
+ */
30
+ export declare const STORYBOOK_NAMESPACED_RULE_ID = "@trackunit/storybook/require-folder-landing-page";
21
31
  /**
22
32
  * Canonical permalink to the how-to-author doc. Single source of truth, embedded in the rule's
23
33
  * error message so developers tripping the rule get a direct link. The `master` permalink stays
@@ -26,6 +36,20 @@ export declare const STORYBOOK_LANDING_PAGE_TODO_MARKER = "{/* TODO: storybook-f
26
36
  export declare const STORYBOOK_LANDING_PAGE_DOCS_URL = "https://github.com/Trackunit/manager/blob/master/docs/STORYBOOK-LANDING-PAGES.md";
27
37
  /** Clear the module-level scan cache (primarily for tests). */
28
38
  export declare const clearLandingPageCache: () => void;
39
+ /**
40
+ * The namespaced rule: `@trackunit/storybook/require-folder-landing-page` (registered under that
41
+ * key in the shared `storybookPlugin` object exported from `../../config/plugins.ts`). This is
42
+ * the current, non-deprecated rule id.
43
+ */
44
+ export declare const requireFolderLandingPage: ESLintUtils.RuleModule<"missingLandingPage", [], unknown, ESLintUtils.RuleListener> & {
45
+ name: string;
46
+ };
47
+ /**
48
+ * Deprecated alias of {@link requireFolderLandingPage}, kept registered under the original
49
+ * `require-storybook-folder-landing-page` key in `rulesMap` (id `@trackunit/require-storybook-folder-landing-page`)
50
+ * so existing configs keep working. Delegates to the identical `create` implementation — only the
51
+ * `meta` differs (marks itself deprecated and points at the namespaced replacement).
52
+ */
29
53
  export declare const requireStorybookFolderLandingPage: ESLintUtils.RuleModule<"missingLandingPage", [], unknown, ESLintUtils.RuleListener> & {
30
54
  name: string;
31
55
  };
@@ -4,10 +4,13 @@
4
4
  * FOLDER implied by a story's title.
5
5
  *
6
6
  * Architecture: per-story-file rule. Each `*.stories.@(js|jsx|ts|tsx)` file declares a Storybook
7
- * `title` (e.g. "Hooks/react-core-hooks/useDebounce"). The folders implied by that title are the
8
- * accumulated path prefixes EXCLUDING the story leaf (here: "Hooks" and "Hooks/react-core-hooks").
9
- * Every such folder must have a landing page: a `*.docs.mdx` somewhere in the workspace whose
10
- * `<Meta title="FOLDER" />` EXACTLY equals the folder path, with non-empty body and no TODO marker.
7
+ * `title` (e.g. "Hooks/react-core-hooks/useDebounce"), read from either the legacy default-export
8
+ * meta object (`export default { title, ... }`, including via an intermediate variable) or a
9
+ * Storybook CSF factory `.meta({...})` call (`const meta = preview.meta({ title, ... });`).
10
+ * The folders implied by that title are the accumulated path prefixes
11
+ * EXCLUDING the story leaf (here: "Hooks" and "Hooks/react-core-hooks"). Every such folder must
12
+ * have a landing page: a `*.docs.mdx` somewhere in the workspace whose `<Meta title="FOLDER" />`
13
+ * EXACTLY equals the folder path, with non-empty body and no TODO marker.
11
14
  *
12
15
  * The rule is REPORT-ONLY: it does not scaffold files. Creating a separate `*.docs.mdx` cannot be
13
16
  * expressed as a pure ESLint fix, and ESLint evaluates fix/suggestion callbacks on every lint pass
@@ -17,7 +20,7 @@
17
20
  * See README.md for incorrect/correct examples.
18
21
  */
19
22
  Object.defineProperty(exports, "__esModule", { value: true });
20
- exports.requireStorybookFolderLandingPage = exports.clearLandingPageCache = exports.STORYBOOK_LANDING_PAGE_DOCS_URL = exports.STORYBOOK_LANDING_PAGE_TODO_MARKER = void 0;
23
+ exports.requireStorybookFolderLandingPage = exports.requireFolderLandingPage = exports.clearLandingPageCache = exports.STORYBOOK_LANDING_PAGE_DOCS_URL = exports.STORYBOOK_NAMESPACED_RULE_ID = exports.STORYBOOK_LANDING_PAGE_TODO_MARKER = void 0;
21
24
  const tslib_1 = require("tslib");
22
25
  const utils_1 = require("@typescript-eslint/utils");
23
26
  const crypto = tslib_1.__importStar(require("crypto"));
@@ -27,6 +30,13 @@ const file_utils_1 = require("../../utils/file-utils");
27
30
  const createRule = utils_1.ESLintUtils.RuleCreator(name => `https://github.com/trackunit/manager/blob/master/libs/eslint/plugin-trackunit/src/lib/rules/${name}/${name}.ts`);
28
31
  /** Canonical TODO marker. A landing page still containing this marker does NOT satisfy the rule. */
29
32
  exports.STORYBOOK_LANDING_PAGE_TODO_MARKER = "{/* TODO: storybook-folder-landing-page */}";
33
+ /**
34
+ * The namespaced rule id (plugin `@trackunit/storybook`, rule `require-folder-landing-page`).
35
+ * Registered as the `require-folder-landing-page` entry of the shared `storybookPlugin` object
36
+ * (see `../../config/plugins.ts`). This is the id the deprecated alias below points at via
37
+ * `meta.deprecated.replacedBy`.
38
+ */
39
+ exports.STORYBOOK_NAMESPACED_RULE_ID = "@trackunit/storybook/require-folder-landing-page";
30
40
  /**
31
41
  * Canonical permalink to the how-to-author doc. Single source of truth, embedded in the rule's
32
42
  * error message so developers tripping the rule get a direct link. The `master` permalink stays
@@ -157,82 +167,136 @@ const getTitleFromObject = (object) => {
157
167
  }
158
168
  return null;
159
169
  };
160
- exports.requireStorybookFolderLandingPage = createRule({
161
- name: "require-storybook-folder-landing-page",
162
- meta: {
163
- type: "problem",
164
- docs: {
165
- description: "Require a Storybook landing page (*.docs.mdx) for every ancestor folder implied by a story's title",
166
- },
167
- schema: [],
168
- messages: {
169
- // URL embedded from the single-source-of-truth const (see STORYBOOK_LANDING_PAGE_DOCS_URL).
170
- missingLandingPage: `[require-storybook-folder-landing-page] Missing Storybook landing page for folder "{{folder}}". The <Meta title> must equal "{{folder}}" exactly (no extra leaf). Add a *.docs.mdx with <Meta title="{{folder}}" /> and real content. See ${exports.STORYBOOK_LANDING_PAGE_DOCS_URL} for how to author one.`,
171
- },
172
- },
173
- defaultOptions: [],
174
- create(context) {
175
- const currentFile = context.filename;
176
- // Only analyze story files (never .docs.* or .mdx).
177
- if (!STORY_FILE_REGEX.test(currentFile)) {
178
- return {};
170
+ /**
171
+ * The rule's `create` implementation, shared by every rule object below (the namespaced rule and
172
+ * the deprecated alias). Extracted to a standalone function so both `createRule(...)` calls
173
+ * delegate to the identical implementation instead of duplicating the detection/reporting logic.
174
+ */
175
+ const create = context => {
176
+ const currentFile = context.filename;
177
+ // Only analyze story files (never .docs.* or .mdx).
178
+ if (!STORY_FILE_REGEX.test(currentFile)) {
179
+ return {};
180
+ }
181
+ const nxJsonPath = (0, file_utils_1.findNearestFile)(currentFile, "nx.json");
182
+ if (nxJsonPath === null) {
183
+ return {};
184
+ }
185
+ const workspaceRoot = path.dirname(nxJsonPath);
186
+ const reportForObject = (titleNode, reportNode) => {
187
+ const title = titleNode.value;
188
+ if (typeof title !== "string") {
189
+ return;
179
190
  }
180
- const nxJsonPath = (0, file_utils_1.findNearestFile)(currentFile, "nx.json");
181
- if (nxJsonPath === null) {
182
- return {};
191
+ const folders = deriveFolders(title);
192
+ if (folders.length === 0) {
193
+ return;
183
194
  }
184
- const workspaceRoot = path.dirname(nxJsonPath);
185
- const reportForObject = (titleNode, reportNode) => {
186
- const title = titleNode.value;
187
- if (typeof title !== "string") {
188
- return;
189
- }
190
- const folders = deriveFolders(title);
191
- if (folders.length === 0) {
192
- return;
195
+ const landingPages = getLandingPages(workspaceRoot);
196
+ for (const folder of folders) {
197
+ const entry = landingPages.get(folder);
198
+ if (isSatisfied(entry)) {
199
+ continue;
193
200
  }
194
- const landingPages = getLandingPages(workspaceRoot);
195
- for (const folder of folders) {
196
- const entry = landingPages.get(folder);
197
- if (isSatisfied(entry)) {
198
- continue;
201
+ context.report({
202
+ node: reportNode,
203
+ messageId: "missingLandingPage",
204
+ data: { folder },
205
+ });
206
+ }
207
+ };
208
+ return {
209
+ ExportDefaultDeclaration(node) {
210
+ const declaration = node.declaration;
211
+ // Shape 1: export default { title: "..." }
212
+ if (declaration.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
213
+ const titleNode = getTitleFromObject(declaration);
214
+ if (titleNode) {
215
+ reportForObject(titleNode, node);
199
216
  }
200
- context.report({
201
- node: reportNode,
202
- messageId: "missingLandingPage",
203
- data: { folder },
204
- });
217
+ return;
205
218
  }
206
- };
207
- return {
208
- ExportDefaultDeclaration(node) {
209
- const declaration = node.declaration;
210
- // Shape 1: export default { title: "..." }
211
- if (declaration.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
212
- const titleNode = getTitleFromObject(declaration);
213
- if (titleNode) {
214
- reportForObject(titleNode, node);
215
- }
216
- return;
217
- }
218
- // Shape 2: const meta = { title: "..." } satisfies Meta; export default meta;
219
- if (declaration.type === utils_1.AST_NODE_TYPES.Identifier) {
220
- const variable = context.sourceCode
221
- .getScope(node)
222
- .references.find(ref => ref.identifier === declaration)?.resolved;
223
- const definition = variable?.defs[0];
224
- if (definition && definition.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator && definition.node.init) {
225
- const initializer = unwrapExpression(definition.node.init);
226
- if (initializer.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
227
- const titleNode = getTitleFromObject(initializer);
228
- if (titleNode) {
229
- reportForObject(titleNode, node);
230
- }
219
+ // Shape 2: const meta = { title: "..." } satisfies Meta; export default meta;
220
+ if (declaration.type === utils_1.AST_NODE_TYPES.Identifier) {
221
+ const variable = context.sourceCode
222
+ .getScope(node)
223
+ .references.find(ref => ref.identifier === declaration)?.resolved;
224
+ const definition = variable?.defs[0];
225
+ if (definition && definition.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator && definition.node.init) {
226
+ const initializer = unwrapExpression(definition.node.init);
227
+ if (initializer.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
228
+ const titleNode = getTitleFromObject(initializer);
229
+ if (titleNode) {
230
+ reportForObject(titleNode, node);
231
231
  }
232
232
  }
233
233
  }
234
- },
235
- };
234
+ }
235
+ },
236
+ // Shape 3 (CSF factory format): `<binding>.meta({ title: "..." })`, e.g.
237
+ // `const meta = preview.meta({ title: "Foo/Widget", component: Widget });`. Only the call
238
+ // shape is checked (a `.meta(...)` member call with an object-literal first argument) — this
239
+ // is purely additive: it does not change how the default-export shapes above are detected.
240
+ CallExpression(node) {
241
+ const { callee } = node;
242
+ if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
243
+ return;
244
+ }
245
+ if (callee.property.type !== utils_1.AST_NODE_TYPES.Identifier || callee.property.name !== "meta") {
246
+ return;
247
+ }
248
+ const [firstArgument] = node.arguments;
249
+ if (firstArgument === undefined || firstArgument.type !== utils_1.AST_NODE_TYPES.ObjectExpression) {
250
+ return;
251
+ }
252
+ const titleNode = getTitleFromObject(firstArgument);
253
+ if (titleNode) {
254
+ reportForObject(titleNode, node);
255
+ }
256
+ },
257
+ };
258
+ };
259
+ /**
260
+ * Builds a fresh `meta` object for a landing-page rule object. Called once per rule object (never
261
+ * shared by reference) so the namespaced rule and the deprecated alias can carry different
262
+ * `deprecated` values without one mutating the other.
263
+ */
264
+ const buildMeta = (deprecated) => ({
265
+ type: "problem",
266
+ docs: {
267
+ description: "Require a Storybook landing page (*.docs.mdx) for every ancestor folder implied by a story's title",
268
+ },
269
+ schema: [],
270
+ messages: {
271
+ // URL embedded from the single-source-of-truth const (see STORYBOOK_LANDING_PAGE_DOCS_URL).
272
+ missingLandingPage: `[require-storybook-folder-landing-page] Missing Storybook landing page for folder "{{folder}}". The <Meta title> must equal "{{folder}}" exactly (no extra leaf). Add a *.docs.mdx with <Meta title="{{folder}}" /> and real content. See ${exports.STORYBOOK_LANDING_PAGE_DOCS_URL} for how to author one.`,
236
273
  },
274
+ ...(deprecated === undefined ? {} : { deprecated }),
275
+ });
276
+ /**
277
+ * The namespaced rule: `@trackunit/storybook/require-folder-landing-page` (registered under that
278
+ * key in the shared `storybookPlugin` object exported from `../../config/plugins.ts`). This is
279
+ * the current, non-deprecated rule id.
280
+ */
281
+ exports.requireFolderLandingPage = createRule({
282
+ name: "require-storybook-folder-landing-page",
283
+ meta: buildMeta(),
284
+ defaultOptions: [],
285
+ create,
286
+ });
287
+ /**
288
+ * Deprecated alias of {@link requireFolderLandingPage}, kept registered under the original
289
+ * `require-storybook-folder-landing-page` key in `rulesMap` (id `@trackunit/require-storybook-folder-landing-page`)
290
+ * so existing configs keep working. Delegates to the identical `create` implementation — only the
291
+ * `meta` differs (marks itself deprecated and points at the namespaced replacement).
292
+ */
293
+ exports.requireStorybookFolderLandingPage = createRule({
294
+ name: "require-storybook-folder-landing-page",
295
+ meta: buildMeta({
296
+ message: `Use \`${exports.STORYBOOK_NAMESPACED_RULE_ID}\` instead.`,
297
+ replacedBy: [{ rule: { name: exports.STORYBOOK_NAMESPACED_RULE_ID } }],
298
+ }),
299
+ defaultOptions: [],
300
+ create,
237
301
  });
238
302
  //# sourceMappingURL=require-storybook-folder-landing-page.js.map