@trackunit/eslint-plugin-trackunit 0.6.49 → 0.6.52

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,21 @@
1
+ ## 0.6.52 (2026-07-01)
2
+
3
+ ### 🧱 Updated Dependencies
4
+
5
+ - Updated shared-utils to 1.15.52
6
+
7
+ ## 0.6.51 (2026-07-01)
8
+
9
+ ### 🧱 Updated Dependencies
10
+
11
+ - Updated shared-utils to 1.15.51
12
+
13
+ ## 0.6.50 (2026-06-30)
14
+
15
+ ### 🧱 Updated Dependencies
16
+
17
+ - Updated shared-utils to 1.15.50
18
+
1
19
  ## 0.6.49 (2026-06-30)
2
20
 
3
21
  ### 🧱 Updated Dependencies
package/README.md CHANGED
@@ -83,6 +83,12 @@ All custom rules are enabled automatically through the presets under the `@track
83
83
  | `@trackunit/no-internal-graphql-when-tagged-with-gql-public` | error | — | Prevents internal GraphQL usage in projects tagged with `gql-public`. Applied to `project.json` files. |
84
84
 
85
85
 
86
+ ### Storybook
87
+
88
+ | Rule | Severity | Auto-fix | Description |
89
+ | ----------------------------------------------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------- |
90
+ | `@trackunit/require-storybook-folder-landing-page` | — | no | Requires a `*.docs.mdx` landing page (`<Meta title>`) for every ancestor folder implied by a story's title. Report-only; not yet enabled in a preset. |
91
+
86
92
  ## Utilities
87
93
 
88
94
  ### `createSkipWhen`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/eslint-plugin-trackunit",
3
- "version": "0.6.49",
3
+ "version": "0.6.52",
4
4
  "license": "SEE LICENSE IN LICENSE.txt",
5
5
  "repository": "https://github.com/Trackunit/manager",
6
6
  "engines": {
@@ -8,7 +8,7 @@ exports.typescriptRules = {
8
8
  "@typescript-eslint/no-unnecessary-condition": "error",
9
9
  "no-unused-vars": "off",
10
10
  "@typescript-eslint/no-unused-vars": [
11
- "warn",
11
+ "error",
12
12
  {
13
13
  args: "all",
14
14
  argsIgnorePattern: "^_",
@@ -86,6 +86,9 @@ export declare const configs: {
86
86
  }], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
87
87
  name: string;
88
88
  };
89
+ "require-storybook-folder-landing-page": import("@typescript-eslint/utils/ts-eslint").RuleModule<"missingLandingPage", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
90
+ name: string;
91
+ };
89
92
  };
90
93
  };
91
94
  "@typescript-eslint": {
@@ -272,6 +275,9 @@ export declare const configs: {
272
275
  }], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
273
276
  name: string;
274
277
  };
278
+ "require-storybook-folder-landing-page": import("@typescript-eslint/utils/ts-eslint").RuleModule<"missingLandingPage", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
279
+ name: string;
280
+ };
275
281
  };
276
282
  };
277
283
  "@nx"?: undefined;
@@ -390,6 +396,9 @@ export declare const configs: {
390
396
  }], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
391
397
  name: string;
392
398
  };
399
+ "require-storybook-folder-landing-page": import("@typescript-eslint/utils/ts-eslint").RuleModule<"missingLandingPage", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
400
+ name: string;
401
+ };
393
402
  };
394
403
  };
395
404
  "@typescript-eslint": {
@@ -104,6 +104,9 @@ export declare const localRulesPlugin: {
104
104
  }], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
105
105
  name: string;
106
106
  };
107
+ "require-storybook-folder-landing-page": import("@typescript-eslint/utils/ts-eslint").RuleModule<"missingLandingPage", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
108
+ name: string;
109
+ };
107
110
  };
108
111
  };
109
112
  export { eslintConfigPrettier, globals, importPlugin, jest, jsdoc, jsoncParser, noNull, nx, react, reactHooks, testingLibrary, tsParser, typescriptEslint, };
@@ -86,6 +86,9 @@ export declare const base: (import("eslint").Linter.FlatConfig<import("eslint").
86
86
  }], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
87
87
  name: string;
88
88
  };
89
+ "require-storybook-folder-landing-page": import("@typescript-eslint/utils/ts-eslint").RuleModule<"missingLandingPage", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
90
+ name: string;
91
+ };
89
92
  };
90
93
  };
91
94
  "@typescript-eslint": {
@@ -272,6 +275,9 @@ export declare const base: (import("eslint").Linter.FlatConfig<import("eslint").
272
275
  }], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
273
276
  name: string;
274
277
  };
278
+ "require-storybook-folder-landing-page": import("@typescript-eslint/utils/ts-eslint").RuleModule<"missingLandingPage", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
279
+ name: string;
280
+ };
275
281
  };
276
282
  };
277
283
  "@nx"?: undefined;
@@ -85,6 +85,9 @@ export declare const reactPreset: (import("eslint").Linter.Config<import("eslint
85
85
  }], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
86
86
  name: string;
87
87
  };
88
+ "require-storybook-folder-landing-page": import("@typescript-eslint/utils/ts-eslint").RuleModule<"missingLandingPage", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
89
+ name: string;
90
+ };
88
91
  };
89
92
  };
90
93
  "@typescript-eslint": {
@@ -4,19 +4,6 @@
4
4
  * used by both string-based and type-based detection strategies.
5
5
  */
6
6
  import { TSESLint, TSESTree } from "@typescript-eslint/utils";
7
- /**
8
- * Result of extracting callback information from an expression.
9
- * Does not include messageId - each strategy adds its own.
10
- */
11
- export type CallbackExtractionResult = {
12
- callbackName: string;
13
- suggestedName: string;
14
- node: TSESTree.Node;
15
- /** The identifier node for the callback - used for fix ranges */
16
- identifierNode: TSESTree.Identifier;
17
- /** Whether the callback is wrapped in an arrow function */
18
- isWrapped: boolean;
19
- } | null;
20
7
  /**
21
8
  * Information about a locally defined variable, including its definition
22
9
  * and all references within the same file.
@@ -40,8 +27,7 @@ export type LocalDefinitionInfo = {
40
27
  export declare const findLocalDefinitionInfo: (identifier: TSESTree.Identifier, context: TSESLint.RuleContext<string, ReadonlyArray<unknown>>) => LocalDefinitionInfo | null;
41
28
  /**
42
29
  * Result of extracting a callback identifier from an expression.
43
- * Unlike CallbackExtractionResult, this doesn't include suggested names
44
- * since those depend on the specific event handler being used.
30
+ * Does not include suggested names since those depend on the specific event handler being used.
45
31
  */
46
32
  export type CallbackIdentifierResult = {
47
33
  callbackName: string;
@@ -105,87 +105,6 @@ const findLocalDefinitionInfo = (identifier, context) => {
105
105
  };
106
106
  };
107
107
  exports.findLocalDefinitionInfo = findLocalDefinitionInfo;
108
- /**
109
- * Checks if a callback name is problematic when used with click handlers.
110
- * A name is problematic if it starts with "on" but not "onClick".
111
- *
112
- * @example
113
- * isProblematicName("onClose") // true - should be onClickClose
114
- * isProblematicName("onClickClose") // false - already correct
115
- * isProblematicName("handleClose") // false - different convention
116
- * isProblematicName("close") // false - doesn't start with "on"
117
- */
118
- const isProblematicName = (name) => {
119
- return name.startsWith("on") && !name.startsWith("onClick");
120
- };
121
- /**
122
- * Converts a problematic callback name to the suggested onClick* format.
123
- *
124
- * @example
125
- * getSuggestedName("onClose") // "onClickClose"
126
- * getSuggestedName("onCancel") // "onClickCancel"
127
- */
128
- const getSuggestedName = (name) => {
129
- // Remove "on" prefix and add "onClick" prefix
130
- return `onClick${name.slice(2)}`;
131
- };
132
- /**
133
- * Extracts callback info from a direct identifier pattern: onClick={onClose}
134
- */
135
- const extractFromDirectIdentifier = (expression) => {
136
- if (expression.type !== utils_1.AST_NODE_TYPES.Identifier) {
137
- return null;
138
- }
139
- const callbackName = expression.name;
140
- if (!isProblematicName(callbackName)) {
141
- return null;
142
- }
143
- return {
144
- callbackName,
145
- suggestedName: getSuggestedName(callbackName),
146
- node: expression,
147
- identifierNode: expression,
148
- isWrapped: false,
149
- };
150
- };
151
- /**
152
- * Extracts callback info from a single-call arrow function: onClick={() => onClose()}
153
- */
154
- const extractFromSingleCallArrow = (expression) => {
155
- if (expression.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
156
- return null;
157
- }
158
- const { body } = expression;
159
- // Only check if body is a single call expression (not a block statement)
160
- if (body.type !== utils_1.AST_NODE_TYPES.CallExpression) {
161
- return null;
162
- }
163
- // Only check if the callee is a simple identifier
164
- if (body.callee.type !== utils_1.AST_NODE_TYPES.Identifier) {
165
- return null;
166
- }
167
- const callbackName = body.callee.name;
168
- if (!isProblematicName(callbackName)) {
169
- return null;
170
- }
171
- return {
172
- callbackName,
173
- suggestedName: getSuggestedName(callbackName),
174
- node: body.callee,
175
- identifierNode: body.callee,
176
- isWrapped: true,
177
- };
178
- };
179
- /**
180
- * Extracts callback information from an expression.
181
- * Handles both direct identifiers (onClick={onClose}) and
182
- * single-call arrow functions (onClick={() => onClose()}).
183
- *
184
- * @returns Callback extraction result without messageId, or null if no problematic callback found
185
- */
186
- const extractCallbackInfo = (expression) => {
187
- return extractFromDirectIdentifier(expression) ?? extractFromSingleCallArrow(expression);
188
- };
189
108
  /**
190
109
  * Extracts the callback identifier from an expression without checking naming patterns.
191
110
  * Used by strict prop events check which has its own naming criteria.
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Require a Storybook landing page (`*.docs.mdx` with `<Meta title="...">`) for every ancestor
3
+ * FOLDER implied by a story's title.
4
+ *
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.
10
+ *
11
+ * The rule is REPORT-ONLY: it does not scaffold files. Creating a separate `*.docs.mdx` cannot be
12
+ * expressed as a pure ESLint fix, and ESLint evaluates fix/suggestion callbacks on every lint pass
13
+ * (not only under `--fix`), so a file-writing fix would write stubs during ordinary editor/CI lint.
14
+ * Stub scaffolding is therefore delivered as a separate, explicit command (tracked as a follow-up).
15
+ *
16
+ * See README.md for incorrect/correct examples.
17
+ */
18
+ import { ESLintUtils } from "@typescript-eslint/utils";
19
+ /** Canonical TODO marker. A landing page still containing this marker does NOT satisfy the rule. */
20
+ export declare const STORYBOOK_LANDING_PAGE_TODO_MARKER = "{/* TODO: storybook-folder-landing-page */}";
21
+ /**
22
+ * Canonical permalink to the how-to-author doc. Single source of truth, embedded in the rule's
23
+ * error message so developers tripping the rule get a direct link. The `master` permalink stays
24
+ * stable across file moves within the (internal) repo.
25
+ */
26
+ export declare const STORYBOOK_LANDING_PAGE_DOCS_URL = "https://github.com/Trackunit/manager/blob/master/docs/STORYBOOK-LANDING-PAGES.md";
27
+ /** Clear the module-level scan cache (primarily for tests). */
28
+ export declare const clearLandingPageCache: () => void;
29
+ export declare const requireStorybookFolderLandingPage: ESLintUtils.RuleModule<"missingLandingPage", [], unknown, ESLintUtils.RuleListener> & {
30
+ name: string;
31
+ };
@@ -0,0 +1,238 @@
1
+ "use strict";
2
+ /**
3
+ * Require a Storybook landing page (`*.docs.mdx` with `<Meta title="...">`) for every ancestor
4
+ * FOLDER implied by a story's title.
5
+ *
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.
11
+ *
12
+ * The rule is REPORT-ONLY: it does not scaffold files. Creating a separate `*.docs.mdx` cannot be
13
+ * expressed as a pure ESLint fix, and ESLint evaluates fix/suggestion callbacks on every lint pass
14
+ * (not only under `--fix`), so a file-writing fix would write stubs during ordinary editor/CI lint.
15
+ * Stub scaffolding is therefore delivered as a separate, explicit command (tracked as a follow-up).
16
+ *
17
+ * See README.md for incorrect/correct examples.
18
+ */
19
+ 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;
21
+ const tslib_1 = require("tslib");
22
+ const utils_1 = require("@typescript-eslint/utils");
23
+ const crypto = tslib_1.__importStar(require("crypto"));
24
+ const fs = tslib_1.__importStar(require("fs"));
25
+ const path = tslib_1.__importStar(require("path"));
26
+ const file_utils_1 = require("../../utils/file-utils");
27
+ 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
+ /** Canonical TODO marker. A landing page still containing this marker does NOT satisfy the rule. */
29
+ exports.STORYBOOK_LANDING_PAGE_TODO_MARKER = "{/* TODO: storybook-folder-landing-page */}";
30
+ /**
31
+ * Canonical permalink to the how-to-author doc. Single source of truth, embedded in the rule's
32
+ * error message so developers tripping the rule get a direct link. The `master` permalink stays
33
+ * stable across file moves within the (internal) repo.
34
+ */
35
+ exports.STORYBOOK_LANDING_PAGE_DOCS_URL = "https://github.com/Trackunit/manager/blob/master/docs/STORYBOOK-LANDING-PAGES.md";
36
+ /**
37
+ * Cache of landing-page scans, keyed by workspace root. The cached value is reused only while the
38
+ * current docs-file fingerprint matches, so long-lived ESLint processes pick up newly added or
39
+ * edited landing pages without requiring an ESLint server restart.
40
+ */
41
+ const landingPageCache = new Map();
42
+ /** Clear the module-level scan cache (primarily for tests). */
43
+ const clearLandingPageCache = () => {
44
+ landingPageCache.clear();
45
+ };
46
+ exports.clearLandingPageCache = clearLandingPageCache;
47
+ const SKIP_DIRECTORIES = new Set(["node_modules", "dist", ".git", ".nx"]);
48
+ // Match `title="..."` anywhere within an opening `<Meta ...>` tag (attribute order independent).
49
+ const META_TITLE_REGEX = /<Meta\b[^>]*?\btitle\s*=\s*("|')(.*?)\1/;
50
+ const STORY_FILE_REGEX = /\.stories\.(js|jsx|ts|tsx)$/;
51
+ const DOCS_MDX_REGEX = /\.docs\.mdx$/;
52
+ /** Recursively collect absolute paths of every `*.docs.mdx` file under `dir`. */
53
+ const collectDocsMdxFiles = (dir) => {
54
+ const result = [];
55
+ let entries;
56
+ try {
57
+ entries = fs.readdirSync(dir, { withFileTypes: true });
58
+ }
59
+ catch {
60
+ return result;
61
+ }
62
+ for (const entry of entries) {
63
+ const fullPath = path.join(dir, entry.name);
64
+ if (entry.isDirectory()) {
65
+ if (!SKIP_DIRECTORIES.has(entry.name)) {
66
+ result.push(...collectDocsMdxFiles(fullPath));
67
+ }
68
+ }
69
+ else if (DOCS_MDX_REGEX.test(entry.name)) {
70
+ result.push(fullPath);
71
+ }
72
+ }
73
+ return result;
74
+ };
75
+ /** Strip import lines and the `<Meta .../>` tag, leaving the documentation body. */
76
+ const extractBody = (contents) => {
77
+ return contents
78
+ .split("\n")
79
+ .filter(line => !/^\s*import\s/.test(line))
80
+ .join("\n")
81
+ .replace(/<Meta\b[^>]*\/>/, "")
82
+ .trim();
83
+ };
84
+ /** Scan the workspace for `*.docs.mdx` landing pages, building a title -> entry map. */
85
+ const scanLandingPages = (workspaceRoot) => {
86
+ const landingPages = new Map();
87
+ const hash = crypto.createHash("sha256");
88
+ for (const filePath of collectDocsMdxFiles(workspaceRoot).toSorted()) {
89
+ hash.update(filePath);
90
+ hash.update("\0");
91
+ let contents;
92
+ try {
93
+ contents = fs.readFileSync(filePath, "utf-8");
94
+ }
95
+ catch {
96
+ hash.update("unreadable");
97
+ hash.update("\0");
98
+ continue;
99
+ }
100
+ hash.update(contents);
101
+ hash.update("\0");
102
+ const match = META_TITLE_REGEX.exec(contents);
103
+ if (match && typeof match[2] === "string") {
104
+ landingPages.set(match[2], { filePath, body: extractBody(contents) });
105
+ }
106
+ }
107
+ return { fingerprint: hash.digest("hex"), landingPages };
108
+ };
109
+ /** Get landing pages from cache when the current docs-file fingerprint is unchanged. */
110
+ const getLandingPages = (workspaceRoot) => {
111
+ const scan = scanLandingPages(workspaceRoot);
112
+ const cached = landingPageCache.get(workspaceRoot);
113
+ if (cached?.fingerprint === scan.fingerprint) {
114
+ return cached.landingPages;
115
+ }
116
+ landingPageCache.set(workspaceRoot, scan);
117
+ return scan.landingPages;
118
+ };
119
+ /** Derive ancestor folder paths from a title, dropping the story leaf. */
120
+ const deriveFolders = (title) => {
121
+ const segments = title.split("/");
122
+ const folders = [];
123
+ // Drop the last segment (story leaf); accumulate prefixes for the rest.
124
+ for (let i = 1; i < segments.length; i++) {
125
+ folders.push(segments.slice(0, i).join("/"));
126
+ }
127
+ return folders;
128
+ };
129
+ /** A folder is satisfied by a landing page entry with non-empty body and no TODO marker. */
130
+ const isSatisfied = (entry) => {
131
+ if (!entry) {
132
+ return false;
133
+ }
134
+ if (entry.body.length === 0) {
135
+ return false;
136
+ }
137
+ return !entry.body.includes(exports.STORYBOOK_LANDING_PAGE_TODO_MARKER);
138
+ };
139
+ /** Unwrap satisfies/as wrappers around an expression. */
140
+ const unwrapExpression = (node) => {
141
+ let current = node;
142
+ while (current.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression || current.type === utils_1.AST_NODE_TYPES.TSAsExpression) {
143
+ current = current.expression;
144
+ }
145
+ return current;
146
+ };
147
+ /** Extract a string-literal `title` property from an object expression, if present. */
148
+ const getTitleFromObject = (object) => {
149
+ for (const property of object.properties) {
150
+ if (property.type === utils_1.AST_NODE_TYPES.Property &&
151
+ property.key.type === utils_1.AST_NODE_TYPES.Identifier &&
152
+ property.key.name === "title" &&
153
+ property.value.type === utils_1.AST_NODE_TYPES.Literal &&
154
+ typeof property.value.value === "string") {
155
+ return property.value;
156
+ }
157
+ }
158
+ return null;
159
+ };
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 {};
179
+ }
180
+ const nxJsonPath = (0, file_utils_1.findNearestFile)(currentFile, "nx.json");
181
+ if (nxJsonPath === null) {
182
+ return {};
183
+ }
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;
193
+ }
194
+ const landingPages = getLandingPages(workspaceRoot);
195
+ for (const folder of folders) {
196
+ const entry = landingPages.get(folder);
197
+ if (isSatisfied(entry)) {
198
+ continue;
199
+ }
200
+ context.report({
201
+ node: reportNode,
202
+ messageId: "missingLandingPage",
203
+ data: { folder },
204
+ });
205
+ }
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
+ }
231
+ }
232
+ }
233
+ }
234
+ },
235
+ };
236
+ },
237
+ });
238
+ //# sourceMappingURL=require-storybook-folder-landing-page.js.map
@@ -82,4 +82,7 @@ export declare const rulesMap: {
82
82
  }], unknown, ESLintUtils.RuleListener> & {
83
83
  name: string;
84
84
  };
85
+ "require-storybook-folder-landing-page": ESLintUtils.RuleModule<"missingLandingPage", [], unknown, ESLintUtils.RuleListener> & {
86
+ name: string;
87
+ };
85
88
  };
@@ -18,6 +18,7 @@ const require_classname_alternatives_1 = require("./rules/require-classname-alte
18
18
  const require_component_prop_contracts_1 = require("./rules/require-component-prop-contracts/require-component-prop-contracts");
19
19
  const require_list_item_virtualization_props_1 = require("./rules/require-list-item-virtualization-props/require-list-item-virtualization-props");
20
20
  const require_optional_prop_initialization_1 = require("./rules/require-optional-prop-initialization/require-optional-prop-initialization");
21
+ const require_storybook_folder_landing_page_1 = require("./rules/require-storybook-folder-landing-page/require-storybook-folder-landing-page");
21
22
  exports.rulesMap = {
22
23
  "component-name-matches-filename": component_name_matches_filename_1.componentNameMatchesFilename,
23
24
  "one-component-per-file": one_component_per_file_1.oneComponentPerFile,
@@ -36,5 +37,6 @@ exports.rulesMap = {
36
37
  "require-component-prop-contracts": require_component_prop_contracts_1.requireComponentPropContracts,
37
38
  "prefer-field-components": prefer_field_components_1.preferFieldComponents,
38
39
  "require-list-item-virtualization-props": require_list_item_virtualization_props_1.requireListItemVirtualizationProps,
40
+ "require-storybook-folder-landing-page": require_storybook_folder_landing_page_1.requireStorybookFolderLandingPage,
39
41
  };
40
42
  //# sourceMappingURL=rules-map.js.map
@@ -12,7 +12,6 @@ const utils_1 = require("@typescript-eslint/utils");
12
12
  * - `isParameterExplicitlyDestructured()` - Check if parameter is explicitly destructured
13
13
  * - `isParameterInitialized()` - Check if parameter has a default value
14
14
  * - `isParameterUsedInFunction()` - Check if parameter is used in function body
15
- * - `getParameterRestUsage()` - Get info about parameter accessed via rest params
16
15
  * - `hasExistingBodyInitializations()` - Check for initialization patterns in function body
17
16
  *
18
17
  * ## Identifier Origin Analysis (Usage → Origin)
@@ -194,44 +193,6 @@ const isParameterUsedInFunction = (context, functionNode, paramName) => {
194
193
  return false;
195
194
  };
196
195
  exports.isParameterUsedInFunction = isParameterUsedInFunction;
197
- /**
198
- * Check if a parameter is accessed via rest parameters (e.g., rest.paramName)
199
- * and return information about the rest parameter usage
200
- */
201
- const getParameterRestUsage = (context, functionNode, paramName) => {
202
- // Look for rest elements in both direct params and inside object patterns
203
- const restParameters = [];
204
- functionNode.params.forEach(param => {
205
- if (param.type === utils_1.AST_NODE_TYPES.RestElement && param.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
206
- // Direct rest parameter: (...rest: Params)
207
- restParameters.push({ name: param.argument.name, param });
208
- }
209
- else if (param.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
210
- // Look for rest elements inside object patterns: ({ param1, ...rest }: Params)
211
- param.properties.forEach(property => {
212
- if (property.type === utils_1.AST_NODE_TYPES.RestElement && property.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
213
- restParameters.push({ name: property.argument.name, param });
214
- }
215
- });
216
- }
217
- });
218
- if (restParameters.length === 0) {
219
- return { isUsedViaRest: false };
220
- }
221
- // Check if the function body contains rest.paramName patterns
222
- const functionText = context.sourceCode.getText(functionNode);
223
- for (const restParam of restParameters) {
224
- const pattern = new RegExp(`\\b${restParam.name}\\.${paramName}\\b`);
225
- if (pattern.test(functionText)) {
226
- return {
227
- isUsedViaRest: true,
228
- restParamName: restParam.name,
229
- restParam: restParam.param,
230
- };
231
- }
232
- }
233
- return { isUsedViaRest: false };
234
- };
235
196
  const jsxTagNameIsIntrinsic = (name) => name.type === utils_1.AST_NODE_TYPES.JSXIdentifier && /^[a-z]/.test(name.name);
236
197
  const nodeContainsJSX = (node) => {
237
198
  if (node.type === utils_1.AST_NODE_TYPES.JSXElement) {
@@ -337,49 +337,6 @@ const extractFromJsxAttribute = (node) => {
337
337
  return [];
338
338
  };
339
339
  // =============================================================================
340
- // Variable Declaration Extraction
341
- // =============================================================================
342
- /**
343
- * Extract classname locations from a variable declaration with a classname-like name.
344
- *
345
- * Example: const buttonClass = "flex items-center";
346
- */
347
- const extractFromVariableDeclarator = (node) => {
348
- if (node.id.type !== utils_1.AST_NODE_TYPES.Identifier) {
349
- return [];
350
- }
351
- const varName = node.id.name;
352
- if (!(0, exports.isClassnameVariable)(varName)) {
353
- return [];
354
- }
355
- if (!node.init) {
356
- return [];
357
- }
358
- const context = {
359
- type: "variable",
360
- variableName: varName,
361
- };
362
- // Handle string literal
363
- const extracted = extractStringValue(node.init);
364
- if (extracted) {
365
- return [
366
- {
367
- value: extracted.value,
368
- reportNode: node.init,
369
- fixNode: extracted.node,
370
- context,
371
- },
372
- ];
373
- }
374
- // Handle array of strings
375
- if (node.init.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
376
- return extractFromArray(node.init, context);
377
- }
378
- // Note: Function calls in variable initializers are handled separately
379
- // by the CallExpression handler
380
- return [];
381
- };
382
- // =============================================================================
383
340
  // Unified Entry Points
384
341
  // =============================================================================
385
342
  /**
@@ -426,27 +383,6 @@ const findClassnameStringsInAttribute = (node) => {
426
383
  return extractFromJsxAttribute(node);
427
384
  };
428
385
  exports.findClassnameStringsInAttribute = findClassnameStringsInAttribute;
429
- /**
430
- * Find all classname string locations in a VariableDeclarator node.
431
- *
432
- * Use this in your ESLint rule's VariableDeclarator handler to process
433
- * variables with classname-like names.
434
- *
435
- * @example
436
- * ```typescript
437
- * VariableDeclarator(node) {
438
- * const locations = findClassnameStringsInVariable(node);
439
- * for (const location of locations) {
440
- * if (hasBannedPattern(location.value)) {
441
- * context.report({ node: location.reportNode, ... });
442
- * }
443
- * }
444
- * }
445
- * ```
446
- */
447
- const findClassnameStringsInVariable = (node) => {
448
- return extractFromVariableDeclarator(node);
449
- };
450
386
  // =============================================================================
451
387
  // Utility Helpers for Fixing
452
388
  // =============================================================================
@@ -473,20 +409,4 @@ const formatAsArray = (classes) => {
473
409
  return JSON.stringify(classes);
474
410
  };
475
411
  exports.formatAsArray = formatAsArray;
476
- /**
477
- * Get the quote character used in a string literal.
478
- */
479
- const getQuoteChar = (node) => {
480
- if (typeof node.value === "string" && node.raw) {
481
- return node.raw.charAt(0);
482
- }
483
- return '"';
484
- };
485
- /**
486
- * Create a quoted string with the same quote style as the original.
487
- */
488
- const quoteString = (value, originalNode) => {
489
- const quote = getQuoteChar(originalNode);
490
- return `${quote}${value}${quote}`;
491
- };
492
412
  //# sourceMappingURL=classname-utils.js.map
@@ -12,11 +12,8 @@ const utils_1 = require("@typescript-eslint/utils");
12
12
  * ## Main Functions
13
13
  * - `addImportSpecifiers()` - Add new imports or merge with existing ones
14
14
  * - `replaceNamespaceWithDestructured()` - Replace namespace imports (e.g., `import React from "react"`)
15
- * - `addSingleImportSpecifier()` - Convenience for adding a single import
16
- *
17
15
  * ## Helper Functions
18
16
  * - `findImportDeclaration()` - Find existing import for a package
19
- * - `hasImportSpecifier()` - Check if specific import already exists
20
17
  * - `getImportSpecifiers()` - Extract specifiers from an import
21
18
  */
22
19
  /**
@@ -39,21 +36,6 @@ const findImportDeclaration = (sourceCode, packageName) => {
39
36
  return found;
40
37
  };
41
38
  exports.findImportDeclaration = findImportDeclaration;
42
- /**
43
- * Check if a specific specifier is already imported from a package.
44
- *
45
- * @example
46
- * // File has: import { useState, useEffect } from "react";
47
- * hasImportSpecifier(sourceCode, "react", "useState") // → true
48
- * hasImportSpecifier(sourceCode, "react", "useRef") // → false
49
- */
50
- const hasImportSpecifier = (sourceCode, packageName, specifierName) => {
51
- return sourceCode.ast.body.some(node => node.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
52
- node.source.value === packageName &&
53
- node.specifiers.some(specifier => specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
54
- specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
55
- specifier.imported.name === specifierName));
56
- };
57
39
  /**
58
40
  * Get existing import specifiers from an import declaration.
59
41
  *
@@ -119,24 +101,6 @@ const addImportSpecifiers = (options) => {
119
101
  return fixes;
120
102
  };
121
103
  exports.addImportSpecifiers = addImportSpecifiers;
122
- /**
123
- * Add a single import specifier - convenience wrapper around addImportSpecifiers.
124
- * Automatically finds and merges with existing imports from the same package.
125
- *
126
- * @example
127
- * // Before: import { useState } from "react";
128
- * // After: import { useEffect, useState } from "react";
129
- * addSingleImportSpecifier({ sourceCode, fixer, packageName: "react", specifier: "useEffect" })
130
- */
131
- const addSingleImportSpecifier = (options) => {
132
- const { sourceCode, packageName, specifier } = options;
133
- const existingImport = (0, exports.findImportDeclaration)(sourceCode, packageName);
134
- return (0, exports.addImportSpecifiers)({
135
- ...options,
136
- specifiers: [specifier],
137
- existingImport,
138
- });
139
- };
140
104
  /**
141
105
  * Replace a namespace/default import with destructured imports.
142
106
  * Automatically handles merging, semicolons, sorting, and formatting.