@slip-stream-kit/eslint-plugin 0.1.2 → 0.1.5
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/dist/index.js +277 -42
- package/dist/index.js.map +4 -4
- package/dist/rules/require-component-stories.d.ts +3 -0
- package/dist/utils/component.d.ts +13 -0
- package/dist/utils/story-path.d.ts +44 -0
- package/package.json +4 -4
- package/readme.md +59 -0
package/dist/index.js
CHANGED
|
@@ -113,6 +113,52 @@ var getComponentFunction = (node) => {
|
|
|
113
113
|
}
|
|
114
114
|
return null;
|
|
115
115
|
};
|
|
116
|
+
var unwrapExport = (statement) => {
|
|
117
|
+
if (statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration") {
|
|
118
|
+
return statement.declaration ?? null;
|
|
119
|
+
}
|
|
120
|
+
return statement;
|
|
121
|
+
};
|
|
122
|
+
var declaresComponent = (node) => {
|
|
123
|
+
if (!node) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
if (node.type === "FunctionDeclaration") {
|
|
127
|
+
return isComponent(node);
|
|
128
|
+
}
|
|
129
|
+
if (node.type === "VariableDeclaration") {
|
|
130
|
+
return node.declarations.some((declaration) => {
|
|
131
|
+
const fn2 = getComponentFunction(declaration.init);
|
|
132
|
+
return fn2 ? isComponent(fn2) : false;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
const fn = getComponentFunction(node);
|
|
136
|
+
return fn ? isComponent(fn) : false;
|
|
137
|
+
};
|
|
138
|
+
var getDeclaredComponentName = (node) => {
|
|
139
|
+
if (!node) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
if (node.type === "FunctionDeclaration") {
|
|
143
|
+
return isComponent(node) ? getComponentName(node) : null;
|
|
144
|
+
}
|
|
145
|
+
if (node.type === "VariableDeclaration") {
|
|
146
|
+
for (const declaration of node.declarations) {
|
|
147
|
+
const fn2 = getComponentFunction(declaration.init);
|
|
148
|
+
if (fn2 && isComponent(fn2)) {
|
|
149
|
+
return getComponentName(fn2);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
const fn = getComponentFunction(node);
|
|
155
|
+
return fn && isComponent(fn) ? getComponentName(fn) : null;
|
|
156
|
+
};
|
|
157
|
+
var bodyDeclaresComponent = (body) => {
|
|
158
|
+
return body.some((statement) => {
|
|
159
|
+
return declaresComponent(unwrapExport(statement));
|
|
160
|
+
});
|
|
161
|
+
};
|
|
116
162
|
|
|
117
163
|
// src/utils/path-match.ts
|
|
118
164
|
var REGEX_METACHARS = /* @__PURE__ */ new Set(["\\", "^", "$", ".", "|", "+", "(", ")", "[", "]", "{", "}"]);
|
|
@@ -149,43 +195,22 @@ var matchesAnyGlob = (filename, patterns) => {
|
|
|
149
195
|
|
|
150
196
|
// src/rules/component-file-order.ts
|
|
151
197
|
var PROPS_SUFFIX = "Props";
|
|
152
|
-
var
|
|
153
|
-
if (statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration") {
|
|
154
|
-
return statement.declaration ?? null;
|
|
155
|
-
}
|
|
156
|
-
return statement;
|
|
157
|
-
};
|
|
158
|
-
var isPropsTypeDeclaration = (node) => {
|
|
198
|
+
var getPropsTypeName = (node) => {
|
|
159
199
|
if (!node) {
|
|
160
|
-
return
|
|
200
|
+
return null;
|
|
161
201
|
}
|
|
162
202
|
const named = node;
|
|
163
203
|
if (named.type !== "TSInterfaceDeclaration" && named.type !== "TSTypeAliasDeclaration") {
|
|
164
|
-
return
|
|
165
|
-
}
|
|
166
|
-
return named.id?.name?.endsWith(PROPS_SUFFIX) ?? false;
|
|
167
|
-
};
|
|
168
|
-
var declaresComponent = (node) => {
|
|
169
|
-
if (!node) {
|
|
170
|
-
return false;
|
|
171
|
-
}
|
|
172
|
-
if (node.type === "FunctionDeclaration") {
|
|
173
|
-
return isComponent(node);
|
|
174
|
-
}
|
|
175
|
-
if (node.type === "VariableDeclaration") {
|
|
176
|
-
return node.declarations.some((declaration) => {
|
|
177
|
-
const fn2 = getComponentFunction(declaration.init);
|
|
178
|
-
return fn2 ? isComponent(fn2) : false;
|
|
179
|
-
});
|
|
204
|
+
return null;
|
|
180
205
|
}
|
|
181
|
-
const
|
|
182
|
-
return
|
|
206
|
+
const name = named.id?.name;
|
|
207
|
+
return name?.endsWith(PROPS_SUFFIX) ? name : null;
|
|
183
208
|
};
|
|
184
209
|
var componentFileOrder = {
|
|
185
210
|
meta: {
|
|
186
211
|
type: "suggestion",
|
|
187
212
|
docs: {
|
|
188
|
-
description: "Enforce a strict top-level order in React component files: imports first, then
|
|
213
|
+
description: "Enforce a strict top-level order in React component files: imports first, then \u2014 for each component \u2014 its props interface/type declared immediately before the component.",
|
|
189
214
|
recommended: true,
|
|
190
215
|
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin"
|
|
191
216
|
},
|
|
@@ -209,7 +234,7 @@ var componentFileOrder = {
|
|
|
209
234
|
],
|
|
210
235
|
messages: {
|
|
211
236
|
importsFirst: "Imports must come before the component interface and declaration.",
|
|
212
|
-
|
|
237
|
+
interfaceImmediatelyBeforeComponent: "The component props interface must be declared immediately before the component."
|
|
213
238
|
}
|
|
214
239
|
},
|
|
215
240
|
create(context) {
|
|
@@ -226,33 +251,40 @@ var componentFileOrder = {
|
|
|
226
251
|
Program(program) {
|
|
227
252
|
const body = program.body;
|
|
228
253
|
const importIndices = [];
|
|
229
|
-
const
|
|
230
|
-
|
|
254
|
+
const components = [];
|
|
255
|
+
const propsIndexByName = /* @__PURE__ */ new Map();
|
|
231
256
|
body.forEach((statement, index) => {
|
|
232
257
|
if (statement.type === "ImportDeclaration") {
|
|
233
258
|
importIndices.push(index);
|
|
234
259
|
return;
|
|
235
260
|
}
|
|
236
261
|
const declaration = unwrapExport(statement);
|
|
237
|
-
|
|
238
|
-
|
|
262
|
+
const propsName = getPropsTypeName(declaration);
|
|
263
|
+
if (propsName !== null && !propsIndexByName.has(propsName)) {
|
|
264
|
+
propsIndexByName.set(propsName, index);
|
|
239
265
|
}
|
|
240
|
-
if (
|
|
241
|
-
|
|
266
|
+
if (declaresComponent(declaration)) {
|
|
267
|
+
components.push({ index, name: getDeclaredComponentName(declaration) });
|
|
242
268
|
}
|
|
243
269
|
});
|
|
244
|
-
if (
|
|
270
|
+
if (components.length === 0) {
|
|
245
271
|
return;
|
|
246
272
|
}
|
|
247
|
-
const
|
|
273
|
+
const first = components[0];
|
|
274
|
+
const firstPropsIndex = first.name === null ? void 0 : propsIndexByName.get(`${first.name}${PROPS_SUFFIX}`);
|
|
275
|
+
const importBoundary = Math.min(first.index, firstPropsIndex ?? first.index);
|
|
248
276
|
for (const importIndex of importIndices) {
|
|
249
277
|
if (importIndex > importBoundary) {
|
|
250
278
|
context.report({ node: body[importIndex], messageId: "importsFirst" });
|
|
251
279
|
}
|
|
252
280
|
}
|
|
253
|
-
for (const
|
|
254
|
-
if (
|
|
255
|
-
|
|
281
|
+
for (const component of components) {
|
|
282
|
+
if (component.name === null) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const propsIndex = propsIndexByName.get(`${component.name}${PROPS_SUFFIX}`);
|
|
286
|
+
if (propsIndex !== void 0 && propsIndex !== component.index - 1) {
|
|
287
|
+
context.report({ node: body[propsIndex], messageId: "interfaceImmediatelyBeforeComponent" });
|
|
256
288
|
}
|
|
257
289
|
}
|
|
258
290
|
}
|
|
@@ -436,11 +468,213 @@ ${baseIndent}}`
|
|
|
436
468
|
}
|
|
437
469
|
};
|
|
438
470
|
|
|
471
|
+
// src/rules/require-component-stories.ts
|
|
472
|
+
import { existsSync } from "node:fs";
|
|
473
|
+
import path2 from "node:path";
|
|
474
|
+
|
|
475
|
+
// src/utils/story-path.ts
|
|
476
|
+
import path from "node:path";
|
|
477
|
+
var DEFAULT_STORY_PATH_OPTIONS = {
|
|
478
|
+
storiesDir: "__stories__",
|
|
479
|
+
storySuffix: ".stories",
|
|
480
|
+
storyExtensions: [".tsx", ".jsx", ".ts", ".js"],
|
|
481
|
+
componentExtensions: [".tsx", ".jsx"],
|
|
482
|
+
componentSuffix: "-component",
|
|
483
|
+
extraTargets: []
|
|
484
|
+
};
|
|
485
|
+
var resolveOptions = (opts) => {
|
|
486
|
+
return { ...DEFAULT_STORY_PATH_OPTIONS, ...opts };
|
|
487
|
+
};
|
|
488
|
+
var toPosix = (filePath) => {
|
|
489
|
+
return filePath.split("\\").join("/");
|
|
490
|
+
};
|
|
491
|
+
var parse = (filePath) => {
|
|
492
|
+
const normalized = toPosix(filePath);
|
|
493
|
+
const segments = normalized.split("/");
|
|
494
|
+
const basename = segments[segments.length - 1] ?? "";
|
|
495
|
+
const ext = path.posix.extname(basename);
|
|
496
|
+
const base = ext ? basename.slice(0, -ext.length) : basename;
|
|
497
|
+
return {
|
|
498
|
+
dir: path.posix.dirname(normalized),
|
|
499
|
+
base,
|
|
500
|
+
ext,
|
|
501
|
+
parent: segments[segments.length - 2] ?? "",
|
|
502
|
+
grandparent: segments[segments.length - 3] ?? "",
|
|
503
|
+
greatGrandparent: segments[segments.length - 4] ?? ""
|
|
504
|
+
};
|
|
505
|
+
};
|
|
506
|
+
var passesComponentPreconditions = (parsed, options) => {
|
|
507
|
+
if (!options.componentExtensions.includes(parsed.ext)) {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
return options.componentSuffix === "" || parsed.base.endsWith(options.componentSuffix);
|
|
511
|
+
};
|
|
512
|
+
var classifyComponent = (filePath, opts) => {
|
|
513
|
+
const options = resolveOptions(opts);
|
|
514
|
+
const parsed = parse(filePath);
|
|
515
|
+
if (!passesComponentPreconditions(parsed, options)) {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
if (parsed.parent === "components" && parsed.greatGrandparent === "features") {
|
|
519
|
+
return { mode: "feature-root" };
|
|
520
|
+
}
|
|
521
|
+
if (parsed.parent === "default" && parsed.grandparent === "components") {
|
|
522
|
+
return { mode: "sibling" };
|
|
523
|
+
}
|
|
524
|
+
for (const target of options.extraTargets) {
|
|
525
|
+
const parentMatches = parsed.parent === target.componentsDir;
|
|
526
|
+
const anchorMatches = target.anchorParentDir == null || parsed.grandparent === target.anchorParentDir;
|
|
527
|
+
if (parentMatches && anchorMatches) {
|
|
528
|
+
return { mode: target.storyMode };
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return null;
|
|
532
|
+
};
|
|
533
|
+
var deriveExpectedStoryPaths = (filePath, opts) => {
|
|
534
|
+
const options = resolveOptions(opts);
|
|
535
|
+
const classification = classifyComponent(filePath, options);
|
|
536
|
+
if (!classification) {
|
|
537
|
+
return [];
|
|
538
|
+
}
|
|
539
|
+
const parsed = parse(filePath);
|
|
540
|
+
const storyBaseDir = classification.mode === "feature-root" ? path.posix.dirname(parsed.dir) : parsed.dir;
|
|
541
|
+
const storyDir = path.posix.join(storyBaseDir, options.storiesDir);
|
|
542
|
+
return options.storyExtensions.map((extension) => {
|
|
543
|
+
return path.posix.join(storyDir, `${parsed.base}${options.storySuffix}${extension}`);
|
|
544
|
+
});
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
// src/rules/require-component-stories.ts
|
|
548
|
+
var toStoryPathOptions = (options) => {
|
|
549
|
+
const picked = {};
|
|
550
|
+
if (options.storiesDir !== void 0) {
|
|
551
|
+
picked.storiesDir = options.storiesDir;
|
|
552
|
+
}
|
|
553
|
+
if (options.storySuffix !== void 0) {
|
|
554
|
+
picked.storySuffix = options.storySuffix;
|
|
555
|
+
}
|
|
556
|
+
if (options.storyExtensions !== void 0) {
|
|
557
|
+
picked.storyExtensions = options.storyExtensions;
|
|
558
|
+
}
|
|
559
|
+
if (options.componentSuffix !== void 0) {
|
|
560
|
+
picked.componentSuffix = options.componentSuffix;
|
|
561
|
+
}
|
|
562
|
+
if (options.extraTargets !== void 0) {
|
|
563
|
+
picked.extraTargets = options.extraTargets;
|
|
564
|
+
}
|
|
565
|
+
return picked;
|
|
566
|
+
};
|
|
567
|
+
var requireComponentStories = {
|
|
568
|
+
meta: {
|
|
569
|
+
type: "problem",
|
|
570
|
+
docs: {
|
|
571
|
+
description: "Require a co-located Storybook story for every dumb component.",
|
|
572
|
+
recommended: true,
|
|
573
|
+
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin"
|
|
574
|
+
},
|
|
575
|
+
schema: [
|
|
576
|
+
{
|
|
577
|
+
type: "object",
|
|
578
|
+
properties: {
|
|
579
|
+
paths: {
|
|
580
|
+
type: "array",
|
|
581
|
+
items: { type: "string" },
|
|
582
|
+
description: "Optional globs; when provided the rule only runs for files whose path matches one."
|
|
583
|
+
},
|
|
584
|
+
ignore: {
|
|
585
|
+
type: "array",
|
|
586
|
+
items: { type: "string" },
|
|
587
|
+
description: "Optional globs; the rule is skipped for matching files, even if they also match `paths`."
|
|
588
|
+
},
|
|
589
|
+
storiesDir: {
|
|
590
|
+
type: "string",
|
|
591
|
+
description: `Story directory name (default '${DEFAULT_STORY_PATH_OPTIONS.storiesDir}').`
|
|
592
|
+
},
|
|
593
|
+
storySuffix: {
|
|
594
|
+
type: "string",
|
|
595
|
+
description: `Suffix inserted before the extension (default '${DEFAULT_STORY_PATH_OPTIONS.storySuffix}').`
|
|
596
|
+
},
|
|
597
|
+
storyExtensions: {
|
|
598
|
+
type: "array",
|
|
599
|
+
items: { type: "string" },
|
|
600
|
+
description: "Extensions a satisfying story file may have, in priority order."
|
|
601
|
+
},
|
|
602
|
+
componentSuffix: {
|
|
603
|
+
type: "string",
|
|
604
|
+
description: `Basename suffix a component file must end with (default '${DEFAULT_STORY_PATH_OPTIONS.componentSuffix}'; '' disables).`
|
|
605
|
+
},
|
|
606
|
+
requireComponentAst: {
|
|
607
|
+
type: "boolean",
|
|
608
|
+
description: "When true (default), only require a story for files that actually declare a component."
|
|
609
|
+
},
|
|
610
|
+
extraTargets: {
|
|
611
|
+
type: "array",
|
|
612
|
+
items: {
|
|
613
|
+
type: "object",
|
|
614
|
+
properties: {
|
|
615
|
+
componentsDir: { type: "string" },
|
|
616
|
+
anchorParentDir: { type: "string" },
|
|
617
|
+
storyMode: { enum: ["feature-root", "sibling"] }
|
|
618
|
+
},
|
|
619
|
+
required: ["componentsDir", "storyMode"],
|
|
620
|
+
additionalProperties: false
|
|
621
|
+
},
|
|
622
|
+
description: "Extra structured component layouts (componentsDir + optional anchorParentDir + storyMode)."
|
|
623
|
+
}
|
|
624
|
+
},
|
|
625
|
+
additionalProperties: false
|
|
626
|
+
}
|
|
627
|
+
],
|
|
628
|
+
messages: {
|
|
629
|
+
missingStory: "Dumb component '{{component}}' is missing a Storybook story (expected at '{{expected}}')."
|
|
630
|
+
}
|
|
631
|
+
},
|
|
632
|
+
create(context) {
|
|
633
|
+
const options = context.options[0] ?? {};
|
|
634
|
+
const paths = options.paths ?? [];
|
|
635
|
+
const ignore = options.ignore ?? [];
|
|
636
|
+
const requireComponentAst = options.requireComponentAst ?? true;
|
|
637
|
+
const storyPathOptions = toStoryPathOptions(options);
|
|
638
|
+
if (ignore.length > 0 && matchesAnyGlob(context.filename, ignore)) {
|
|
639
|
+
return {};
|
|
640
|
+
}
|
|
641
|
+
if (paths.length > 0 && !matchesAnyGlob(context.filename, paths)) {
|
|
642
|
+
return {};
|
|
643
|
+
}
|
|
644
|
+
return {
|
|
645
|
+
Program(program) {
|
|
646
|
+
if (!classifyComponent(context.filename, storyPathOptions)) {
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if (requireComponentAst && !bodyDeclaresComponent(program.body)) {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
const candidates = deriveExpectedStoryPaths(context.filename, storyPathOptions);
|
|
653
|
+
const hasStory = candidates.some((candidate) => {
|
|
654
|
+
return existsSync(candidate);
|
|
655
|
+
});
|
|
656
|
+
if (hasStory) {
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
context.report({
|
|
660
|
+
node: program,
|
|
661
|
+
messageId: "missingStory",
|
|
662
|
+
data: {
|
|
663
|
+
component: path2.posix.basename(context.filename.split("\\").join("/")),
|
|
664
|
+
expected: candidates[0] ?? ""
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
|
|
439
672
|
// src/rules/index.ts
|
|
440
673
|
var rules = {
|
|
441
674
|
"props-destructuring-newline": propsDestructuringNewline,
|
|
442
675
|
"props-destructuring-blank-line": propsDestructuringBlankLine,
|
|
443
|
-
"component-file-order": componentFileOrder
|
|
676
|
+
"component-file-order": componentFileOrder,
|
|
677
|
+
"require-component-stories": requireComponentStories
|
|
444
678
|
};
|
|
445
679
|
|
|
446
680
|
// src/index.ts
|
|
@@ -448,7 +682,7 @@ var PLUGIN_NAME = "@wl";
|
|
|
448
682
|
var plugin = {
|
|
449
683
|
meta: {
|
|
450
684
|
name: "@wl/eslint-plugin",
|
|
451
|
-
version: "0.1.
|
|
685
|
+
version: "0.1.3"
|
|
452
686
|
},
|
|
453
687
|
rules,
|
|
454
688
|
configs: {}
|
|
@@ -460,7 +694,8 @@ plugin.configs.recommended = {
|
|
|
460
694
|
rules: {
|
|
461
695
|
[`${PLUGIN_NAME}/props-destructuring-newline`]: "error",
|
|
462
696
|
[`${PLUGIN_NAME}/props-destructuring-blank-line`]: "error",
|
|
463
|
-
[`${PLUGIN_NAME}/component-file-order`]: "error"
|
|
697
|
+
[`${PLUGIN_NAME}/component-file-order`]: "error",
|
|
698
|
+
[`${PLUGIN_NAME}/require-component-stories`]: "error"
|
|
464
699
|
}
|
|
465
700
|
};
|
|
466
701
|
var meta = plugin.meta;
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/utils/component.ts", "../src/utils/path-match.ts", "../src/rules/component-file-order.ts", "../src/rules/props-destructuring-blank-line.ts", "../src/rules/props-destructuring-newline.ts", "../src/rules/index.ts", "../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["import type * as ESTree from 'estree'\n\nexport type ComponentFunction = ESTree.ArrowFunctionExpression | ESTree.FunctionDeclaration | ESTree.FunctionExpression\n\n// Minimal structural view over the `parent` back-reference ESLint adds to every node.\ninterface WithParent {\n parent?: ESTree.Node\n}\n\n// Calls that wrap a component while preserving its identity (memo, forwardRef, observer, ...).\nconst COMPONENT_WRAPPER_CALLEES = new Set(['memo', 'forwardRef', 'observer', 'React.memo', 'React.forwardRef'])\n\n// Node types that introduce a new function scope \u2014 their returns are not the outer component's.\nconst NESTED_SCOPES = new Set(['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression'])\n\nconst isPascalCase = (name: string): boolean => {\n return /^[A-Z]/.test(name)\n}\n\nconst isJsxNode = (node: ESTree.Node | null | undefined): boolean => {\n if (!node) {\n return false\n }\n\n const type = node.type as string\n\n return type === 'JSXElement' || type === 'JSXFragment'\n}\n\nconst getParent = (node: ESTree.Node | undefined): ESTree.Node | undefined => {\n return (node as (WithParent & ESTree.Node) | undefined)?.parent\n}\n\n/** Best-effort name of a call's callee: `memo` for `memo(...)`, `React.memo` for `React.memo(...)`. */\nconst getCalleeName = (callee: ESTree.CallExpression['callee']): string | null => {\n if (callee.type === 'Identifier') {\n return callee.name\n }\n\n if (\n callee.type === 'MemberExpression' &&\n callee.object.type === 'Identifier' &&\n callee.property.type === 'Identifier'\n ) {\n return `${callee.object.name}.${callee.property.name}`\n }\n\n return null\n}\n\n/**\n * Resolve the declared name of a function, looking through component wrappers\n * such as `memo`/`forwardRef` so that `const Comp = memo(({ a }) => ...)` is\n * still recognised by its PascalCase variable name.\n */\nconst getComponentName = (node: ComponentFunction): string | null => {\n if (node.type === 'FunctionDeclaration') {\n return node.id?.name ?? null\n }\n\n let current = getParent(node)\n\n // Walk through wrapping call expressions (memo, forwardRef, React.memo, ...).\n while (current?.type === 'CallExpression') {\n const calleeName = getCalleeName(current.callee)\n\n if (!calleeName || !COMPONENT_WRAPPER_CALLEES.has(calleeName)) {\n break\n }\n\n current = getParent(current)\n }\n\n if (current?.type === 'VariableDeclarator' && current.id.type === 'Identifier') {\n return current.id.name\n }\n\n return null\n}\n\n/** Whether a function returns JSX, scanning its own body without descending into nested functions. */\nconst returnsJsx = (node: ComponentFunction): boolean => {\n if (node.body.type !== 'BlockStatement') {\n return isJsxNode(node.body)\n }\n\n let found = false\n\n const visit = (current: ESTree.Node | null | undefined): void => {\n if (found || !current || NESTED_SCOPES.has(current.type)) {\n return\n }\n\n if (current.type === 'ReturnStatement') {\n if (isJsxNode(current.argument)) {\n found = true\n }\n\n return\n }\n\n if (current.type === 'IfStatement') {\n visit(current.consequent)\n visit(current.alternate)\n\n return\n }\n\n if (current.type === 'BlockStatement') {\n current.body.forEach(visit)\n\n return\n }\n\n if (current.type === 'SwitchStatement') {\n for (const switchCase of current.cases) {\n switchCase.consequent.forEach(visit)\n }\n\n return\n }\n\n if (current.type === 'TryStatement') {\n visit(current.block)\n visit(current.handler?.body)\n visit(current.finalizer)\n\n return\n }\n\n if (\n current.type === 'ForStatement' ||\n current.type === 'ForInStatement' ||\n current.type === 'ForOfStatement' ||\n current.type === 'WhileStatement' ||\n current.type === 'DoWhileStatement'\n ) {\n visit(current.body)\n }\n }\n\n node.body.body.forEach(visit)\n\n return found\n}\n\n/** A function is treated as a React component when it is PascalCase-named or returns JSX. */\nexport const isComponent = (node: ComponentFunction): boolean => {\n const name = getComponentName(node)\n\n if (name && isPascalCase(name)) {\n return true\n }\n\n return returnsJsx(node)\n}\n\nconst isComponentFunctionNode = (node: ESTree.Node): node is ComponentFunction => {\n return (\n node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration'\n )\n}\n\n/**\n * Extract the component function from an expression, unwrapping a single layer of\n * component wrappers (`memo(fn)`, `forwardRef(fn)`, `React.memo(fn)`, ...). Returns\n * null when no function is found.\n */\nexport const getComponentFunction = (node: ESTree.Node | null | undefined): ComponentFunction | null => {\n if (!node) {\n return null\n }\n\n if (isComponentFunctionNode(node)) {\n return node\n }\n\n if (node.type === 'CallExpression') {\n for (const argument of node.arguments) {\n if (argument.type === 'SpreadElement') {\n continue\n }\n\n const found = getComponentFunction(argument)\n\n if (found) {\n return found\n }\n }\n }\n\n return null\n}\n", "// Characters that must be escaped when embedded literally into a RegExp source.\nconst REGEX_METACHARS = new Set(['\\\\', '^', '$', '.', '|', '+', '(', ')', '[', ']', '{', '}'])\n\n/**\n * Convert a glob pattern to an (unanchored) RegExp.\n *\n * - `**` matches any characters, including path separators.\n * - `*` matches any characters except a path separator.\n * - `?` matches a single non-separator character.\n *\n * The result is intentionally unanchored so a pattern matches anywhere in the\n * path (e.g. `features/**` matches `/repo/src/features/x/comp.tsx`).\n */\nconst globToRegExp = (glob: string): RegExp => {\n let source = ''\n\n for (let index = 0; index < glob.length; index++) {\n const char = glob[index]!\n\n if (char === '*') {\n if (glob[index + 1] === '*') {\n source += '.*'\n index++\n\n // Consume a trailing slash so `**/foo` also matches a bare `foo`.\n if (glob[index + 1] === '/') {\n index++\n }\n } else {\n source += '[^/]*'\n }\n } else if (char === '?') {\n source += '[^/]'\n } else if (REGEX_METACHARS.has(char)) {\n source += `\\\\${char}`\n } else {\n source += char\n }\n }\n\n return new RegExp(source)\n}\n\n/** Whether `filename` matches at least one of the provided glob `patterns`. */\nexport const matchesAnyGlob = (filename: string, patterns: readonly string[]): boolean => {\n const normalized = filename.split('\\\\').join('/')\n\n return patterns.some((pattern) => {\n return globToRegExp(pattern).test(normalized)\n })\n}\n", "import type { Rule } from 'eslint'\nimport type * as ESTree from 'estree'\n\nimport { getComponentFunction, isComponent } from '../utils/component'\nimport { matchesAnyGlob } from '../utils/path-match'\n\ninterface Options {\n paths?: string[]\n ignore?: string[]\n}\n\n// TS-only nodes are not modelled by estree; access their identifier structurally.\ninterface NamedDeclaration {\n type: string\n id?: { name?: string } | null\n}\n\nconst PROPS_SUFFIX = 'Props'\n\n/** Unwrap an `export ...` statement to the declaration it wraps (or the statement itself). */\nconst unwrapExport = (statement: ESTree.Statement | ESTree.ModuleDeclaration): ESTree.Node | null => {\n if (statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration') {\n return (statement.declaration as ESTree.Node | null) ?? null\n }\n\n return statement\n}\n\n/** Whether a declaration is a props interface/type alias (`SomethingProps`). */\nconst isPropsTypeDeclaration = (node: ESTree.Node | null): boolean => {\n if (!node) {\n return false\n }\n\n const named = node as NamedDeclaration\n\n if (named.type !== 'TSInterfaceDeclaration' && named.type !== 'TSTypeAliasDeclaration') {\n return false\n }\n\n return named.id?.name?.endsWith(PROPS_SUFFIX) ?? false\n}\n\n/** Whether a top-level declaration declares a React component. */\nconst declaresComponent = (node: ESTree.Node | null): boolean => {\n if (!node) {\n return false\n }\n\n if (node.type === 'FunctionDeclaration') {\n return isComponent(node)\n }\n\n if (node.type === 'VariableDeclaration') {\n return node.declarations.some((declaration) => {\n const fn = getComponentFunction(declaration.init)\n\n return fn ? isComponent(fn) : false\n })\n }\n\n const fn = getComponentFunction(node)\n\n return fn ? isComponent(fn) : false\n}\n\nexport const componentFileOrder: Rule.RuleModule = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Enforce a strict top-level order in React component files: imports first, then the component props interface/type, then the component declaration.',\n recommended: true,\n url: 'https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin',\n },\n schema: [\n {\n type: 'object',\n properties: {\n paths: {\n type: 'array',\n items: { type: 'string' },\n description:\n 'Optional glob patterns. When provided, the rule only runs for files whose path matches one of them.',\n },\n ignore: {\n type: 'array',\n items: { type: 'string' },\n description:\n 'Optional glob patterns. The rule is skipped for files whose path matches one of them, even if it also matches `paths`.',\n },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n importsFirst: 'Imports must come before the component interface and declaration.',\n interfaceBeforeComponent: 'The component props interface must be declared before the component.',\n },\n },\n\n create(context) {\n const options = (context.options[0] ?? {}) as Options\n const paths = options.paths ?? []\n const ignore = options.ignore ?? []\n\n // `ignore` takes precedence: skip excluded files even when they also match `paths`.\n if (ignore.length > 0 && matchesAnyGlob(context.filename, ignore)) {\n return {}\n }\n\n if (paths.length > 0 && !matchesAnyGlob(context.filename, paths)) {\n return {}\n }\n\n return {\n Program(program) {\n const body = program.body\n\n const importIndices: number[] = []\n const propsIndices: number[] = []\n let componentIndex = -1\n\n body.forEach((statement, index) => {\n if (statement.type === 'ImportDeclaration') {\n importIndices.push(index)\n\n return\n }\n\n const declaration = unwrapExport(statement)\n\n if (isPropsTypeDeclaration(declaration)) {\n propsIndices.push(index)\n }\n\n if (componentIndex === -1 && declaresComponent(declaration)) {\n componentIndex = index\n }\n })\n\n // The rule only governs files that actually contain a component.\n if (componentIndex === -1) {\n return\n }\n\n // Imports must precede the first props interface and the component.\n const importBoundary = Math.min(componentIndex, ...propsIndices)\n\n for (const importIndex of importIndices) {\n if (importIndex > importBoundary) {\n context.report({ node: body[importIndex]!, messageId: 'importsFirst' })\n }\n }\n\n // The props interface/type must precede the component declaration.\n for (const propsIndex of propsIndices) {\n if (propsIndex > componentIndex) {\n context.report({ node: body[propsIndex]!, messageId: 'interfaceBeforeComponent' })\n }\n }\n },\n }\n },\n}\n\nexport default componentFileOrder\n", "import type { Rule } from 'eslint'\nimport type * as ESTree from 'estree'\n\nimport type { ComponentFunction } from '../utils/component'\nimport { isComponent } from '../utils/component'\n\n/** Whether a statement is `const { ... } = props` (destructuring the `props` identifier). */\nconst isPropsDestructuring = (statement: ESTree.Statement): boolean => {\n if (statement.type !== 'VariableDeclaration') {\n return false\n }\n\n return statement.declarations.some((declaration) => {\n return (\n declaration.id.type === 'ObjectPattern' &&\n declaration.init?.type === 'Identifier' &&\n declaration.init.name === 'props'\n )\n })\n}\n\nexport const propsDestructuringBlankLine: Rule.RuleModule = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Require a blank line after the `const { ... } = props` destructuring statement at the top of a React component body.',\n recommended: true,\n url: 'https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin',\n },\n fixable: 'whitespace',\n schema: [],\n messages: {\n blankLineAfterProps: 'Add a blank line after destructuring props.',\n },\n },\n\n create(context) {\n const sourceCode = context.sourceCode\n\n const check = (node: ComponentFunction): void => {\n if (node.body.type !== 'BlockStatement') {\n return\n }\n\n if (!isComponent(node)) {\n return\n }\n\n const statements = node.body.body\n const index = statements.findIndex(isPropsDestructuring)\n\n if (index === -1) {\n return\n }\n\n const propsStatement = statements[index]\n const nextStatement = statements[index + 1]\n\n // `propsStatement` is defined because `index !== -1`; the guard also narrows the type.\n // Nothing follows the destructuring \u2014 no separation needed.\n if (!propsStatement || !nextStatement) {\n return\n }\n\n // The token/comment that follows the destructuring statement; a comment on the\n // next line still counts as \"no blank line\" until it is pushed down.\n const tokenAfter = sourceCode.getTokenAfter(propsStatement, { includeComments: true })\n const referenceLine = (tokenAfter ?? nextStatement).loc!.start.line\n\n if (referenceLine - propsStatement.loc!.end.line >= 2) {\n return\n }\n\n context.report({\n node: propsStatement,\n messageId: 'blankLineAfterProps',\n fix(fixer) {\n return fixer.insertTextAfter(propsStatement, '\\n')\n },\n })\n }\n\n return {\n ArrowFunctionExpression: check,\n FunctionDeclaration: check,\n FunctionExpression: check,\n }\n },\n}\n\nexport default propsDestructuringBlankLine\n", "import type { Rule } from 'eslint'\nimport type * as ESTree from 'estree'\n\nimport type { ComponentFunction } from '../utils/component'\nimport { isComponent } from '../utils/component'\n\n// Minimal structural views over nodes that estree's types do not fully model:\n// the optional TS type annotation and `range` that the parser attaches to params.\ninterface WithRange {\n range?: [number, number]\n}\ntype AnnotatedPattern = ESTree.ObjectPattern & { typeAnnotation?: ESTree.Node & WithRange } & WithRange\n\n// Recursively collect every identifier a destructuring pattern binds, so we can detect\n// whether it already introduces a `props` binding (e.g. a `...props` rest).\nconst collectBoundNames = (node: ESTree.Node | null, names: Set<string>): void => {\n if (!node) {\n return\n }\n\n switch (node.type) {\n case 'Identifier':\n names.add(node.name)\n break\n case 'ObjectPattern':\n for (const property of node.properties) collectBoundNames(property, names)\n break\n case 'ArrayPattern':\n for (const element of node.elements) collectBoundNames(element, names)\n break\n case 'Property':\n collectBoundNames(node.value, names)\n break\n case 'RestElement':\n collectBoundNames(node.argument, names)\n break\n case 'AssignmentPattern':\n collectBoundNames(node.left, names)\n break\n default:\n break\n }\n}\n\nexport const propsDestructuringNewline: Rule.RuleModule = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Require React components to accept a single props parameter and destructure it on its own line in the body, rather than destructuring inline in the parameter list.',\n recommended: true,\n url: 'https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin',\n },\n fixable: 'code',\n schema: [],\n messages: {\n destructureOnNewLine:\n 'Accept a single `props` parameter and destructure it on its own line in the component body instead of destructuring in the parameter list.',\n },\n },\n\n create(context) {\n const sourceCode = context.sourceCode\n\n const check = (node: ComponentFunction): void => {\n const firstParam = node.params[0]\n\n if (!firstParam || firstParam.type !== 'ObjectPattern') {\n return\n }\n\n if (!isComponent(node)) {\n return\n }\n\n // The fix renames the parameter to `props` and re-destructures it in the body\n // (`const <pattern> = props`). If the pattern already binds `props` (e.g. a\n // `...props` rest), that body binding collides with the new parameter and yields\n // an invalid \"Duplicate declaration props\". There is no safe rename that keeps the\n // `props` name the rule mandates, so skip these patterns entirely.\n const boundNames = new Set<string>()\n\n collectBoundNames(firstParam, boundNames)\n\n if (boundNames.has('props')) {\n return\n }\n\n const objectPattern = firstParam as AnnotatedPattern\n\n context.report({\n node: firstParam,\n messageId: 'destructureOnNewLine',\n fix(fixer) {\n const text = sourceCode.getText()\n const annotation = objectPattern.typeAnnotation\n\n const patternStart = objectPattern.range![0]\n const patternEnd = annotation ? annotation.range![0] : objectPattern.range![1]\n const fullEnd = annotation ? annotation.range![1] : objectPattern.range![1]\n\n const patternText = text.slice(patternStart, patternEnd).trim()\n const annotationText = annotation ? sourceCode.getText(annotation) : ''\n\n const fixes = [fixer.replaceTextRange([patternStart, fullEnd], `props${annotationText}`)]\n\n const destructureStatement = `const ${patternText} = props`\n\n // Indentation of the line the component is declared on, used as the base for inserted code.\n const lines = sourceCode.getLines()\n const declarationLine = lines[node.loc!.start.line - 1] ?? ''\n const baseIndent = declarationLine.slice(0, declarationLine.length - declarationLine.trimStart().length)\n const innerIndent = `${baseIndent} `\n\n if (node.body.type === 'BlockStatement') {\n const [firstStatement] = node.body.body\n\n if (firstStatement) {\n const indent = ' '.repeat(firstStatement.loc!.start.column)\n\n fixes.push(fixer.insertTextBefore(firstStatement, `${destructureStatement}\\n\\n${indent}`))\n } else {\n const openBrace = sourceCode.getFirstToken(node.body)!\n\n fixes.push(fixer.insertTextAfter(openBrace, `\\n${innerIndent}${destructureStatement}\\n${baseIndent}`))\n }\n\n return fixes\n }\n\n // Expression-bodied arrow (implicit return) \u2014 wrap it in a block.\n const bodyText = sourceCode.getText(node.body)\n\n fixes.push(\n fixer.replaceText(\n node.body,\n `{\\n${innerIndent}${destructureStatement}\\n\\n${innerIndent}return ${bodyText}\\n${baseIndent}}`,\n ),\n )\n\n return fixes\n },\n })\n }\n\n return {\n ArrowFunctionExpression: check,\n FunctionDeclaration: check,\n FunctionExpression: check,\n }\n },\n}\n\nexport default propsDestructuringNewline\n", "import type { Rule } from 'eslint'\n\nimport { componentFileOrder } from './component-file-order'\nimport { propsDestructuringBlankLine } from './props-destructuring-blank-line'\nimport { propsDestructuringNewline } from './props-destructuring-newline'\n\nexport const rules: Record<string, Rule.RuleModule> = {\n 'props-destructuring-newline': propsDestructuringNewline,\n 'props-destructuring-blank-line': propsDestructuringBlankLine,\n 'component-file-order': componentFileOrder,\n}\n", "import type { ESLint, Linter } from 'eslint'\n\nimport { rules } from './rules'\n\nconst PLUGIN_NAME = '@wl'\n\nconst plugin: ESLint.Plugin & { configs: Record<string, Linter.Config> } = {\n meta: {\n name: '@wl/eslint-plugin',\n version: '0.1.1',\n },\n rules,\n configs: {},\n}\n\n/**\n * Flat-config preset that registers the plugin and turns every rule on.\n *\n * @example\n * import wl from '@wl/eslint-plugin'\n *\n * export default [wl.configs.recommended]\n */\nplugin.configs.recommended = {\n plugins: {\n [PLUGIN_NAME]: plugin,\n },\n rules: {\n [`${PLUGIN_NAME}/props-destructuring-newline`]: 'error',\n [`${PLUGIN_NAME}/props-destructuring-blank-line`]: 'error',\n [`${PLUGIN_NAME}/component-file-order`]: 'error',\n },\n}\n\nexport const meta: ESLint.Plugin['meta'] = plugin.meta\nexport const configs: Record<string, Linter.Config> = plugin.configs\nexport { rules }\n\nexport default plugin\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": ["fn"]
|
|
3
|
+
"sources": ["../src/utils/component.ts", "../src/utils/path-match.ts", "../src/rules/component-file-order.ts", "../src/rules/props-destructuring-blank-line.ts", "../src/rules/props-destructuring-newline.ts", "../src/rules/require-component-stories.ts", "../src/utils/story-path.ts", "../src/rules/index.ts", "../src/index.ts"],
|
|
4
|
+
"sourcesContent": ["import type * as ESTree from 'estree'\n\nexport type ComponentFunction = ESTree.ArrowFunctionExpression | ESTree.FunctionDeclaration | ESTree.FunctionExpression\n\n// Minimal structural view over the `parent` back-reference ESLint adds to every node.\ninterface WithParent {\n parent?: ESTree.Node\n}\n\n// Calls that wrap a component while preserving its identity (memo, forwardRef, observer, ...).\nconst COMPONENT_WRAPPER_CALLEES = new Set(['memo', 'forwardRef', 'observer', 'React.memo', 'React.forwardRef'])\n\n// Node types that introduce a new function scope \u2014 their returns are not the outer component's.\nconst NESTED_SCOPES = new Set(['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression'])\n\nconst isPascalCase = (name: string): boolean => {\n return /^[A-Z]/.test(name)\n}\n\nconst isJsxNode = (node: ESTree.Node | null | undefined): boolean => {\n if (!node) {\n return false\n }\n\n const type = node.type as string\n\n return type === 'JSXElement' || type === 'JSXFragment'\n}\n\nconst getParent = (node: ESTree.Node | undefined): ESTree.Node | undefined => {\n return (node as (WithParent & ESTree.Node) | undefined)?.parent\n}\n\n/** Best-effort name of a call's callee: `memo` for `memo(...)`, `React.memo` for `React.memo(...)`. */\nconst getCalleeName = (callee: ESTree.CallExpression['callee']): string | null => {\n if (callee.type === 'Identifier') {\n return callee.name\n }\n\n if (\n callee.type === 'MemberExpression' &&\n callee.object.type === 'Identifier' &&\n callee.property.type === 'Identifier'\n ) {\n return `${callee.object.name}.${callee.property.name}`\n }\n\n return null\n}\n\n/**\n * Resolve the declared name of a function, looking through component wrappers\n * such as `memo`/`forwardRef` so that `const Comp = memo(({ a }) => ...)` is\n * still recognised by its PascalCase variable name.\n */\nconst getComponentName = (node: ComponentFunction): string | null => {\n if (node.type === 'FunctionDeclaration') {\n return node.id?.name ?? null\n }\n\n let current = getParent(node)\n\n // Walk through wrapping call expressions (memo, forwardRef, React.memo, ...).\n while (current?.type === 'CallExpression') {\n const calleeName = getCalleeName(current.callee)\n\n if (!calleeName || !COMPONENT_WRAPPER_CALLEES.has(calleeName)) {\n break\n }\n\n current = getParent(current)\n }\n\n if (current?.type === 'VariableDeclarator' && current.id.type === 'Identifier') {\n return current.id.name\n }\n\n return null\n}\n\n/** Whether a function returns JSX, scanning its own body without descending into nested functions. */\nconst returnsJsx = (node: ComponentFunction): boolean => {\n if (node.body.type !== 'BlockStatement') {\n return isJsxNode(node.body)\n }\n\n let found = false\n\n const visit = (current: ESTree.Node | null | undefined): void => {\n if (found || !current || NESTED_SCOPES.has(current.type)) {\n return\n }\n\n if (current.type === 'ReturnStatement') {\n if (isJsxNode(current.argument)) {\n found = true\n }\n\n return\n }\n\n if (current.type === 'IfStatement') {\n visit(current.consequent)\n visit(current.alternate)\n\n return\n }\n\n if (current.type === 'BlockStatement') {\n current.body.forEach(visit)\n\n return\n }\n\n if (current.type === 'SwitchStatement') {\n for (const switchCase of current.cases) {\n switchCase.consequent.forEach(visit)\n }\n\n return\n }\n\n if (current.type === 'TryStatement') {\n visit(current.block)\n visit(current.handler?.body)\n visit(current.finalizer)\n\n return\n }\n\n if (\n current.type === 'ForStatement' ||\n current.type === 'ForInStatement' ||\n current.type === 'ForOfStatement' ||\n current.type === 'WhileStatement' ||\n current.type === 'DoWhileStatement'\n ) {\n visit(current.body)\n }\n }\n\n node.body.body.forEach(visit)\n\n return found\n}\n\n/** A function is treated as a React component when it is PascalCase-named or returns JSX. */\nexport const isComponent = (node: ComponentFunction): boolean => {\n const name = getComponentName(node)\n\n if (name && isPascalCase(name)) {\n return true\n }\n\n return returnsJsx(node)\n}\n\nconst isComponentFunctionNode = (node: ESTree.Node): node is ComponentFunction => {\n return (\n node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration'\n )\n}\n\n/**\n * Extract the component function from an expression, unwrapping a single layer of\n * component wrappers (`memo(fn)`, `forwardRef(fn)`, `React.memo(fn)`, ...). Returns\n * null when no function is found.\n */\nexport const getComponentFunction = (node: ESTree.Node | null | undefined): ComponentFunction | null => {\n if (!node) {\n return null\n }\n\n if (isComponentFunctionNode(node)) {\n return node\n }\n\n if (node.type === 'CallExpression') {\n for (const argument of node.arguments) {\n if (argument.type === 'SpreadElement') {\n continue\n }\n\n const found = getComponentFunction(argument)\n\n if (found) {\n return found\n }\n }\n }\n\n return null\n}\n\n/** Unwrap an `export ...` statement to the declaration it wraps (or the statement itself). */\nexport const unwrapExport = (statement: ESTree.Statement | ESTree.ModuleDeclaration): ESTree.Node | null => {\n if (statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration') {\n return (statement.declaration as ESTree.Node | null) ?? null\n }\n\n return statement\n}\n\n/** Whether a single top-level declaration declares a React component. */\nexport const declaresComponent = (node: ESTree.Node | null): boolean => {\n if (!node) {\n return false\n }\n\n if (node.type === 'FunctionDeclaration') {\n return isComponent(node)\n }\n\n if (node.type === 'VariableDeclaration') {\n return node.declarations.some((declaration) => {\n const fn = getComponentFunction(declaration.init)\n\n return fn ? isComponent(fn) : false\n })\n }\n\n const fn = getComponentFunction(node)\n\n return fn ? isComponent(fn) : false\n}\n\n/**\n * Resolve the name of the React component declared by a single top-level declaration,\n * or null when the declaration is not a component or the component is anonymous\n * (e.g. `export default () => <div />`). Mirrors `declaresComponent`'s node dispatch and\n * resolves the name through component wrappers (`memo`/`forwardRef`).\n */\nexport const getDeclaredComponentName = (node: ESTree.Node | null): string | null => {\n if (!node) {\n return null\n }\n\n if (node.type === 'FunctionDeclaration') {\n return isComponent(node) ? getComponentName(node) : null\n }\n\n if (node.type === 'VariableDeclaration') {\n for (const declaration of node.declarations) {\n const fn = getComponentFunction(declaration.init)\n\n if (fn && isComponent(fn)) {\n return getComponentName(fn)\n }\n }\n\n return null\n }\n\n const fn = getComponentFunction(node)\n\n return fn && isComponent(fn) ? getComponentName(fn) : null\n}\n\n/** Whether any top-level statement in a program body declares a React component. */\nexport const bodyDeclaresComponent = (body: Array<ESTree.Statement | ESTree.ModuleDeclaration>): boolean => {\n return body.some((statement) => {\n return declaresComponent(unwrapExport(statement))\n })\n}\n", "// Characters that must be escaped when embedded literally into a RegExp source.\nconst REGEX_METACHARS = new Set(['\\\\', '^', '$', '.', '|', '+', '(', ')', '[', ']', '{', '}'])\n\n/**\n * Convert a glob pattern to an (unanchored) RegExp.\n *\n * - `**` matches any characters, including path separators.\n * - `*` matches any characters except a path separator.\n * - `?` matches a single non-separator character.\n *\n * The result is intentionally unanchored so a pattern matches anywhere in the\n * path (e.g. `features/**` matches `/repo/src/features/x/comp.tsx`).\n */\nconst globToRegExp = (glob: string): RegExp => {\n let source = ''\n\n for (let index = 0; index < glob.length; index++) {\n const char = glob[index]!\n\n if (char === '*') {\n if (glob[index + 1] === '*') {\n source += '.*'\n index++\n\n // Consume a trailing slash so `**/foo` also matches a bare `foo`.\n if (glob[index + 1] === '/') {\n index++\n }\n } else {\n source += '[^/]*'\n }\n } else if (char === '?') {\n source += '[^/]'\n } else if (REGEX_METACHARS.has(char)) {\n source += `\\\\${char}`\n } else {\n source += char\n }\n }\n\n return new RegExp(source)\n}\n\n/** Whether `filename` matches at least one of the provided glob `patterns`. */\nexport const matchesAnyGlob = (filename: string, patterns: readonly string[]): boolean => {\n const normalized = filename.split('\\\\').join('/')\n\n return patterns.some((pattern) => {\n return globToRegExp(pattern).test(normalized)\n })\n}\n", "import type { Rule } from 'eslint'\nimport type * as ESTree from 'estree'\n\nimport { declaresComponent, getDeclaredComponentName, unwrapExport } from '../utils/component'\nimport { matchesAnyGlob } from '../utils/path-match'\n\ninterface Options {\n paths?: string[]\n ignore?: string[]\n}\n\n// TS-only nodes are not modelled by estree; access their identifier structurally.\ninterface NamedDeclaration {\n type: string\n id?: { name?: string } | null\n}\n\nconst PROPS_SUFFIX = 'Props'\n\n/** The name of a props interface/type alias (`SomethingProps`), or null when it is neither. */\nconst getPropsTypeName = (node: ESTree.Node | null): string | null => {\n if (!node) {\n return null\n }\n\n const named = node as NamedDeclaration\n\n if (named.type !== 'TSInterfaceDeclaration' && named.type !== 'TSTypeAliasDeclaration') {\n return null\n }\n\n const name = named.id?.name\n\n return name?.endsWith(PROPS_SUFFIX) ? name : null\n}\n\nexport const componentFileOrder: Rule.RuleModule = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Enforce a strict top-level order in React component files: imports first, then \u2014 for each component \u2014 its props interface/type declared immediately before the component.',\n recommended: true,\n url: 'https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin',\n },\n schema: [\n {\n type: 'object',\n properties: {\n paths: {\n type: 'array',\n items: { type: 'string' },\n description:\n 'Optional glob patterns. When provided, the rule only runs for files whose path matches one of them.',\n },\n ignore: {\n type: 'array',\n items: { type: 'string' },\n description:\n 'Optional glob patterns. The rule is skipped for files whose path matches one of them, even if it also matches `paths`.',\n },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n importsFirst: 'Imports must come before the component interface and declaration.',\n interfaceImmediatelyBeforeComponent:\n 'The component props interface must be declared immediately before the component.',\n },\n },\n\n create(context) {\n const options = (context.options[0] ?? {}) as Options\n const paths = options.paths ?? []\n const ignore = options.ignore ?? []\n\n // `ignore` takes precedence: skip excluded files even when they also match `paths`.\n if (ignore.length > 0 && matchesAnyGlob(context.filename, ignore)) {\n return {}\n }\n\n if (paths.length > 0 && !matchesAnyGlob(context.filename, paths)) {\n return {}\n }\n\n return {\n Program(program) {\n const body = program.body\n\n const importIndices: number[] = []\n const components: { index: number; name: string | null }[] = []\n // First index of each `*Props` declaration, keyed by its name.\n const propsIndexByName = new Map<string, number>()\n\n body.forEach((statement, index) => {\n if (statement.type === 'ImportDeclaration') {\n importIndices.push(index)\n\n return\n }\n\n const declaration = unwrapExport(statement)\n\n const propsName = getPropsTypeName(declaration)\n\n if (propsName !== null && !propsIndexByName.has(propsName)) {\n propsIndexByName.set(propsName, index)\n }\n\n if (declaresComponent(declaration)) {\n components.push({ index, name: getDeclaredComponentName(declaration) })\n }\n })\n\n // The rule only governs files that actually contain a component.\n if (components.length === 0) {\n return\n }\n\n // Imports must precede the first component and its matching props interface.\n const first = components[0]!\n const firstPropsIndex = first.name === null ? undefined : propsIndexByName.get(`${first.name}${PROPS_SUFFIX}`)\n const importBoundary = Math.min(first.index, firstPropsIndex ?? first.index)\n\n for (const importIndex of importIndices) {\n if (importIndex > importBoundary) {\n context.report({ node: body[importIndex]!, messageId: 'importsFirst' })\n }\n }\n\n // Each component's own `<Name>Props` interface, when present, must sit immediately\n // before the component. Matching by name keeps a sibling component's props from\n // being judged against this component.\n for (const component of components) {\n if (component.name === null) {\n continue\n }\n\n const propsIndex = propsIndexByName.get(`${component.name}${PROPS_SUFFIX}`)\n\n if (propsIndex !== undefined && propsIndex !== component.index - 1) {\n context.report({ node: body[propsIndex]!, messageId: 'interfaceImmediatelyBeforeComponent' })\n }\n }\n },\n }\n },\n}\n\nexport default componentFileOrder\n", "import type { Rule } from 'eslint'\nimport type * as ESTree from 'estree'\n\nimport type { ComponentFunction } from '../utils/component'\nimport { isComponent } from '../utils/component'\n\n/** Whether a statement is `const { ... } = props` (destructuring the `props` identifier). */\nconst isPropsDestructuring = (statement: ESTree.Statement): boolean => {\n if (statement.type !== 'VariableDeclaration') {\n return false\n }\n\n return statement.declarations.some((declaration) => {\n return (\n declaration.id.type === 'ObjectPattern' &&\n declaration.init?.type === 'Identifier' &&\n declaration.init.name === 'props'\n )\n })\n}\n\nexport const propsDestructuringBlankLine: Rule.RuleModule = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Require a blank line after the `const { ... } = props` destructuring statement at the top of a React component body.',\n recommended: true,\n url: 'https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin',\n },\n fixable: 'whitespace',\n schema: [],\n messages: {\n blankLineAfterProps: 'Add a blank line after destructuring props.',\n },\n },\n\n create(context) {\n const sourceCode = context.sourceCode\n\n const check = (node: ComponentFunction): void => {\n if (node.body.type !== 'BlockStatement') {\n return\n }\n\n if (!isComponent(node)) {\n return\n }\n\n const statements = node.body.body\n const index = statements.findIndex(isPropsDestructuring)\n\n if (index === -1) {\n return\n }\n\n const propsStatement = statements[index]\n const nextStatement = statements[index + 1]\n\n // `propsStatement` is defined because `index !== -1`; the guard also narrows the type.\n // Nothing follows the destructuring \u2014 no separation needed.\n if (!propsStatement || !nextStatement) {\n return\n }\n\n // The token/comment that follows the destructuring statement; a comment on the\n // next line still counts as \"no blank line\" until it is pushed down.\n const tokenAfter = sourceCode.getTokenAfter(propsStatement, { includeComments: true })\n const referenceLine = (tokenAfter ?? nextStatement).loc!.start.line\n\n if (referenceLine - propsStatement.loc!.end.line >= 2) {\n return\n }\n\n context.report({\n node: propsStatement,\n messageId: 'blankLineAfterProps',\n fix(fixer) {\n return fixer.insertTextAfter(propsStatement, '\\n')\n },\n })\n }\n\n return {\n ArrowFunctionExpression: check,\n FunctionDeclaration: check,\n FunctionExpression: check,\n }\n },\n}\n\nexport default propsDestructuringBlankLine\n", "import type { Rule } from 'eslint'\nimport type * as ESTree from 'estree'\n\nimport type { ComponentFunction } from '../utils/component'\nimport { isComponent } from '../utils/component'\n\n// Minimal structural views over nodes that estree's types do not fully model:\n// the optional TS type annotation and `range` that the parser attaches to params.\ninterface WithRange {\n range?: [number, number]\n}\ntype AnnotatedPattern = ESTree.ObjectPattern & { typeAnnotation?: ESTree.Node & WithRange } & WithRange\n\n// Recursively collect every identifier a destructuring pattern binds, so we can detect\n// whether it already introduces a `props` binding (e.g. a `...props` rest).\nconst collectBoundNames = (node: ESTree.Node | null, names: Set<string>): void => {\n if (!node) {\n return\n }\n\n switch (node.type) {\n case 'Identifier':\n names.add(node.name)\n break\n case 'ObjectPattern':\n for (const property of node.properties) collectBoundNames(property, names)\n break\n case 'ArrayPattern':\n for (const element of node.elements) collectBoundNames(element, names)\n break\n case 'Property':\n collectBoundNames(node.value, names)\n break\n case 'RestElement':\n collectBoundNames(node.argument, names)\n break\n case 'AssignmentPattern':\n collectBoundNames(node.left, names)\n break\n default:\n break\n }\n}\n\nexport const propsDestructuringNewline: Rule.RuleModule = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Require React components to accept a single props parameter and destructure it on its own line in the body, rather than destructuring inline in the parameter list.',\n recommended: true,\n url: 'https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin',\n },\n fixable: 'code',\n schema: [],\n messages: {\n destructureOnNewLine:\n 'Accept a single `props` parameter and destructure it on its own line in the component body instead of destructuring in the parameter list.',\n },\n },\n\n create(context) {\n const sourceCode = context.sourceCode\n\n const check = (node: ComponentFunction): void => {\n const firstParam = node.params[0]\n\n if (!firstParam || firstParam.type !== 'ObjectPattern') {\n return\n }\n\n if (!isComponent(node)) {\n return\n }\n\n // The fix renames the parameter to `props` and re-destructures it in the body\n // (`const <pattern> = props`). If the pattern already binds `props` (e.g. a\n // `...props` rest), that body binding collides with the new parameter and yields\n // an invalid \"Duplicate declaration props\". There is no safe rename that keeps the\n // `props` name the rule mandates, so skip these patterns entirely.\n const boundNames = new Set<string>()\n\n collectBoundNames(firstParam, boundNames)\n\n if (boundNames.has('props')) {\n return\n }\n\n const objectPattern = firstParam as AnnotatedPattern\n\n context.report({\n node: firstParam,\n messageId: 'destructureOnNewLine',\n fix(fixer) {\n const text = sourceCode.getText()\n const annotation = objectPattern.typeAnnotation\n\n const patternStart = objectPattern.range![0]\n const patternEnd = annotation ? annotation.range![0] : objectPattern.range![1]\n const fullEnd = annotation ? annotation.range![1] : objectPattern.range![1]\n\n const patternText = text.slice(patternStart, patternEnd).trim()\n const annotationText = annotation ? sourceCode.getText(annotation) : ''\n\n const fixes = [fixer.replaceTextRange([patternStart, fullEnd], `props${annotationText}`)]\n\n const destructureStatement = `const ${patternText} = props`\n\n // Indentation of the line the component is declared on, used as the base for inserted code.\n const lines = sourceCode.getLines()\n const declarationLine = lines[node.loc!.start.line - 1] ?? ''\n const baseIndent = declarationLine.slice(0, declarationLine.length - declarationLine.trimStart().length)\n const innerIndent = `${baseIndent} `\n\n if (node.body.type === 'BlockStatement') {\n const [firstStatement] = node.body.body\n\n if (firstStatement) {\n const indent = ' '.repeat(firstStatement.loc!.start.column)\n\n fixes.push(fixer.insertTextBefore(firstStatement, `${destructureStatement}\\n\\n${indent}`))\n } else {\n const openBrace = sourceCode.getFirstToken(node.body)!\n\n fixes.push(fixer.insertTextAfter(openBrace, `\\n${innerIndent}${destructureStatement}\\n${baseIndent}`))\n }\n\n return fixes\n }\n\n // Expression-bodied arrow (implicit return) \u2014 wrap it in a block.\n const bodyText = sourceCode.getText(node.body)\n\n fixes.push(\n fixer.replaceText(\n node.body,\n `{\\n${innerIndent}${destructureStatement}\\n\\n${innerIndent}return ${bodyText}\\n${baseIndent}}`,\n ),\n )\n\n return fixes\n },\n })\n }\n\n return {\n ArrowFunctionExpression: check,\n FunctionDeclaration: check,\n FunctionExpression: check,\n }\n },\n}\n\nexport default propsDestructuringNewline\n", "import type { Rule } from 'eslint'\nimport { existsSync } from 'node:fs'\nimport path from 'node:path'\n\nimport { bodyDeclaresComponent } from '../utils/component'\nimport { matchesAnyGlob } from '../utils/path-match'\nimport type { ExtraTarget, StoryPathOptions } from '../utils/story-path'\nimport { DEFAULT_STORY_PATH_OPTIONS, classifyComponent, deriveExpectedStoryPaths } from '../utils/story-path'\n\ninterface Options {\n paths?: string[]\n ignore?: string[]\n storiesDir?: string\n storySuffix?: string\n storyExtensions?: string[]\n componentSuffix?: string\n requireComponentAst?: boolean\n extraTargets?: ExtraTarget[]\n}\n\n/** Pick the story-path knobs out of the rule options, leaving defaults to the helper. */\nconst toStoryPathOptions = (options: Options): Partial<StoryPathOptions> => {\n const picked: Partial<StoryPathOptions> = {}\n\n if (options.storiesDir !== undefined) {\n picked.storiesDir = options.storiesDir\n }\n\n if (options.storySuffix !== undefined) {\n picked.storySuffix = options.storySuffix\n }\n\n if (options.storyExtensions !== undefined) {\n picked.storyExtensions = options.storyExtensions\n }\n\n if (options.componentSuffix !== undefined) {\n picked.componentSuffix = options.componentSuffix\n }\n\n if (options.extraTargets !== undefined) {\n picked.extraTargets = options.extraTargets\n }\n\n return picked\n}\n\nexport const requireComponentStories: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Require a co-located Storybook story for every dumb component.',\n recommended: true,\n url: 'https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin',\n },\n schema: [\n {\n type: 'object',\n properties: {\n paths: {\n type: 'array',\n items: { type: 'string' },\n description: 'Optional globs; when provided the rule only runs for files whose path matches one.',\n },\n ignore: {\n type: 'array',\n items: { type: 'string' },\n description: 'Optional globs; the rule is skipped for matching files, even if they also match `paths`.',\n },\n storiesDir: {\n type: 'string',\n description: `Story directory name (default '${DEFAULT_STORY_PATH_OPTIONS.storiesDir}').`,\n },\n storySuffix: {\n type: 'string',\n description: `Suffix inserted before the extension (default '${DEFAULT_STORY_PATH_OPTIONS.storySuffix}').`,\n },\n storyExtensions: {\n type: 'array',\n items: { type: 'string' },\n description: 'Extensions a satisfying story file may have, in priority order.',\n },\n componentSuffix: {\n type: 'string',\n description: `Basename suffix a component file must end with (default '${DEFAULT_STORY_PATH_OPTIONS.componentSuffix}'; '' disables).`,\n },\n requireComponentAst: {\n type: 'boolean',\n description: 'When true (default), only require a story for files that actually declare a component.',\n },\n extraTargets: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n componentsDir: { type: 'string' },\n anchorParentDir: { type: 'string' },\n storyMode: { enum: ['feature-root', 'sibling'] },\n },\n required: ['componentsDir', 'storyMode'],\n additionalProperties: false,\n },\n description: 'Extra structured component layouts (componentsDir + optional anchorParentDir + storyMode).',\n },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n missingStory: \"Dumb component '{{component}}' is missing a Storybook story (expected at '{{expected}}').\",\n },\n },\n\n create(context) {\n const options = (context.options[0] ?? {}) as Options\n const paths = options.paths ?? []\n const ignore = options.ignore ?? []\n const requireComponentAst = options.requireComponentAst ?? true\n const storyPathOptions = toStoryPathOptions(options)\n\n // `ignore` takes precedence: skip excluded files even when they also match `paths`.\n if (ignore.length > 0 && matchesAnyGlob(context.filename, ignore)) {\n return {}\n }\n\n if (paths.length > 0 && !matchesAnyGlob(context.filename, paths)) {\n return {}\n }\n\n return {\n Program(program) {\n // Is this file a dumb component that requires a story, and where would the story live?\n if (!classifyComponent(context.filename, storyPathOptions)) {\n return\n }\n\n // Only enforce on files that actually declare a component (avoids flagging stray\n // non-component files placed under a components/ directory).\n if (requireComponentAst && !bodyDeclaresComponent(program.body)) {\n return\n }\n\n const candidates = deriveExpectedStoryPaths(context.filename, storyPathOptions)\n\n // The story is satisfied if ANY candidate (by extension) exists on disk.\n const hasStory = candidates.some((candidate) => {\n return existsSync(candidate)\n })\n\n if (hasStory) {\n return\n }\n\n context.report({\n node: program,\n messageId: 'missingStory',\n data: {\n component: path.posix.basename(context.filename.split('\\\\').join('/')),\n expected: candidates[0] ?? '',\n },\n })\n },\n }\n },\n}\n\nexport default requireComponentStories\n", "import path from 'node:path'\n\n/** Where a story file lives relative to its component. */\nexport type StoryMode = 'feature-root' | 'sibling'\n\n/** A consumer-defined extra component layout (structured \u2014 never a glob). */\nexport interface ExtraTarget {\n /** Immediate parent directory name a component file must sit directly inside. */\n componentsDir: string\n /** Optional grandparent directory name gate (e.g. `features`). */\n anchorParentDir?: string\n /** Where the story is expected for files matched by this target. */\n storyMode: StoryMode\n}\n\nexport interface StoryPathOptions {\n /** Story directory name (default `__stories__`). */\n storiesDir: string\n /** Suffix inserted before the extension (default `.stories`). */\n storySuffix: string\n /** Extensions a satisfying story file may have, in priority order. */\n storyExtensions: string[]\n /** Extensions a file must have to be considered a component. */\n componentExtensions: string[]\n /** Basename suffix a component file must end with (default `-component`; `''` disables). */\n componentSuffix: string\n /** Additional structured component layouts beyond the two built-ins. */\n extraTargets: ExtraTarget[]\n}\n\nexport const DEFAULT_STORY_PATH_OPTIONS: StoryPathOptions = {\n storiesDir: '__stories__',\n storySuffix: '.stories',\n storyExtensions: ['.tsx', '.jsx', '.ts', '.js'],\n componentExtensions: ['.tsx', '.jsx'],\n componentSuffix: '-component',\n extraTargets: [],\n}\n\nconst resolveOptions = (opts?: Partial<StoryPathOptions>): StoryPathOptions => {\n return { ...DEFAULT_STORY_PATH_OPTIONS, ...opts }\n}\n\n/** Normalize OS-native separators to posix so all downstream path logic is deterministic. */\nconst toPosix = (filePath: string): string => {\n return filePath.split('\\\\').join('/')\n}\n\ninterface ParsedComponent {\n /** Posix directory of the file. */\n dir: string\n /** Basename without its extension. */\n base: string\n /** Original extension (e.g. `.tsx`). */\n ext: string\n /** Immediate parent directory name. */\n parent: string\n /** Grandparent directory name. */\n grandparent: string\n /** Great-grandparent directory name. */\n greatGrandparent: string\n}\n\n/** Parse a file path into the segment view the gate and derivation both need. */\nconst parse = (filePath: string): ParsedComponent => {\n const normalized = toPosix(filePath)\n const segments = normalized.split('/')\n const basename = segments[segments.length - 1] ?? ''\n const ext = path.posix.extname(basename)\n const base = ext ? basename.slice(0, -ext.length) : basename\n\n return {\n dir: path.posix.dirname(normalized),\n base,\n ext,\n parent: segments[segments.length - 2] ?? '',\n grandparent: segments[segments.length - 3] ?? '',\n greatGrandparent: segments[segments.length - 4] ?? '',\n }\n}\n\n/** Whether the file passes the component preconditions (extension + name suffix). */\nconst passesComponentPreconditions = (parsed: ParsedComponent, options: StoryPathOptions): boolean => {\n if (!options.componentExtensions.includes(parsed.ext)) {\n return false\n }\n\n return options.componentSuffix === '' || parsed.base.endsWith(options.componentSuffix)\n}\n\n/**\n * Classify a file as a dumb component requiring a story, resolving WHERE its story should live.\n * Returns null when the file is not a component-requiring-a-story under any branch.\n *\n * Exactly one admit-condition may hold:\n * - feature-root: direct child of `components/` whose chain is `features/<f>/components`.\n * - sibling: a `components/default/<name>-component` file (parent `default`, grandparent `components`).\n * - extraTargets: a structured consumer-defined layout.\n */\nexport const classifyComponent = (filePath: string, opts?: Partial<StoryPathOptions>): { mode: StoryMode } | null => {\n const options = resolveOptions(opts)\n const parsed = parse(filePath)\n\n if (!passesComponentPreconditions(parsed, options)) {\n return null\n }\n\n // (a) feature-root \u2014 immediate parent is `components` AND the chain is `features/<feature>/components`.\n if (parsed.parent === 'components' && parsed.greatGrandparent === 'features') {\n return { mode: 'feature-root' }\n }\n\n // (b) sibling \u2014 `components/default/<name>-component.*` (NOT a direct child of `components/`).\n if (parsed.parent === 'default' && parsed.grandparent === 'components') {\n return { mode: 'sibling' }\n }\n\n // (c) extraTargets \u2014 structured layouts (componentsDir + optional anchorParentDir).\n for (const target of options.extraTargets) {\n const parentMatches = parsed.parent === target.componentsDir\n const anchorMatches = target.anchorParentDir == null || parsed.grandparent === target.anchorParentDir\n\n if (parentMatches && anchorMatches) {\n return { mode: target.storyMode }\n }\n }\n\n return null\n}\n\n/**\n * Ordered candidate story paths for a component file. Empty when the file is not a\n * component-requiring-a-story (see {@link classifyComponent}). The story is considered present\n * when ANY candidate exists on disk.\n */\nexport const deriveExpectedStoryPaths = (filePath: string, opts?: Partial<StoryPathOptions>): string[] => {\n const options = resolveOptions(opts)\n const classification = classifyComponent(filePath, options)\n\n if (!classification) {\n return []\n }\n\n const parsed = parse(filePath)\n\n // feature-root: dirname(file) is `.../components`, so its parent is the feature root.\n // sibling: the story dir sits next to the component file itself.\n const storyBaseDir = classification.mode === 'feature-root' ? path.posix.dirname(parsed.dir) : parsed.dir\n const storyDir = path.posix.join(storyBaseDir, options.storiesDir)\n\n return options.storyExtensions.map((extension) => {\n return path.posix.join(storyDir, `${parsed.base}${options.storySuffix}${extension}`)\n })\n}\n", "import type { Rule } from 'eslint'\n\nimport { componentFileOrder } from './component-file-order'\nimport { propsDestructuringBlankLine } from './props-destructuring-blank-line'\nimport { propsDestructuringNewline } from './props-destructuring-newline'\nimport { requireComponentStories } from './require-component-stories'\n\nexport const rules: Record<string, Rule.RuleModule> = {\n 'props-destructuring-newline': propsDestructuringNewline,\n 'props-destructuring-blank-line': propsDestructuringBlankLine,\n 'component-file-order': componentFileOrder,\n 'require-component-stories': requireComponentStories,\n}\n", "import type { ESLint, Linter } from 'eslint'\n\nimport { rules } from './rules'\n\nconst PLUGIN_NAME = '@wl'\n\nconst plugin: ESLint.Plugin & { configs: Record<string, Linter.Config> } = {\n meta: {\n name: '@wl/eslint-plugin',\n version: '0.1.3',\n },\n rules,\n configs: {},\n}\n\n/**\n * Flat-config preset that registers the plugin and turns every rule on.\n *\n * @example\n * import wl from '@wl/eslint-plugin'\n *\n * export default [wl.configs.recommended]\n */\nplugin.configs.recommended = {\n plugins: {\n [PLUGIN_NAME]: plugin,\n },\n rules: {\n [`${PLUGIN_NAME}/props-destructuring-newline`]: 'error',\n [`${PLUGIN_NAME}/props-destructuring-blank-line`]: 'error',\n [`${PLUGIN_NAME}/component-file-order`]: 'error',\n [`${PLUGIN_NAME}/require-component-stories`]: 'error',\n },\n}\n\nexport const meta: ESLint.Plugin['meta'] = plugin.meta\nexport const configs: Record<string, Linter.Config> = plugin.configs\nexport { rules }\n\nexport default plugin\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["fn", "path", "path"]
|
|
7
7
|
}
|
|
@@ -8,3 +8,16 @@ export declare const isComponent: (node: ComponentFunction) => boolean;
|
|
|
8
8
|
* null when no function is found.
|
|
9
9
|
*/
|
|
10
10
|
export declare const getComponentFunction: (node: ESTree.Node | null | undefined) => ComponentFunction | null;
|
|
11
|
+
/** Unwrap an `export ...` statement to the declaration it wraps (or the statement itself). */
|
|
12
|
+
export declare const unwrapExport: (statement: ESTree.Statement | ESTree.ModuleDeclaration) => ESTree.Node | null;
|
|
13
|
+
/** Whether a single top-level declaration declares a React component. */
|
|
14
|
+
export declare const declaresComponent: (node: ESTree.Node | null) => boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Resolve the name of the React component declared by a single top-level declaration,
|
|
17
|
+
* or null when the declaration is not a component or the component is anonymous
|
|
18
|
+
* (e.g. `export default () => <div />`). Mirrors `declaresComponent`'s node dispatch and
|
|
19
|
+
* resolves the name through component wrappers (`memo`/`forwardRef`).
|
|
20
|
+
*/
|
|
21
|
+
export declare const getDeclaredComponentName: (node: ESTree.Node | null) => string | null;
|
|
22
|
+
/** Whether any top-level statement in a program body declares a React component. */
|
|
23
|
+
export declare const bodyDeclaresComponent: (body: Array<ESTree.Statement | ESTree.ModuleDeclaration>) => boolean;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Where a story file lives relative to its component. */
|
|
2
|
+
export type StoryMode = 'feature-root' | 'sibling';
|
|
3
|
+
/** A consumer-defined extra component layout (structured — never a glob). */
|
|
4
|
+
export interface ExtraTarget {
|
|
5
|
+
/** Immediate parent directory name a component file must sit directly inside. */
|
|
6
|
+
componentsDir: string;
|
|
7
|
+
/** Optional grandparent directory name gate (e.g. `features`). */
|
|
8
|
+
anchorParentDir?: string;
|
|
9
|
+
/** Where the story is expected for files matched by this target. */
|
|
10
|
+
storyMode: StoryMode;
|
|
11
|
+
}
|
|
12
|
+
export interface StoryPathOptions {
|
|
13
|
+
/** Story directory name (default `__stories__`). */
|
|
14
|
+
storiesDir: string;
|
|
15
|
+
/** Suffix inserted before the extension (default `.stories`). */
|
|
16
|
+
storySuffix: string;
|
|
17
|
+
/** Extensions a satisfying story file may have, in priority order. */
|
|
18
|
+
storyExtensions: string[];
|
|
19
|
+
/** Extensions a file must have to be considered a component. */
|
|
20
|
+
componentExtensions: string[];
|
|
21
|
+
/** Basename suffix a component file must end with (default `-component`; `''` disables). */
|
|
22
|
+
componentSuffix: string;
|
|
23
|
+
/** Additional structured component layouts beyond the two built-ins. */
|
|
24
|
+
extraTargets: ExtraTarget[];
|
|
25
|
+
}
|
|
26
|
+
export declare const DEFAULT_STORY_PATH_OPTIONS: StoryPathOptions;
|
|
27
|
+
/**
|
|
28
|
+
* Classify a file as a dumb component requiring a story, resolving WHERE its story should live.
|
|
29
|
+
* Returns null when the file is not a component-requiring-a-story under any branch.
|
|
30
|
+
*
|
|
31
|
+
* Exactly one admit-condition may hold:
|
|
32
|
+
* - feature-root: direct child of `components/` whose chain is `features/<f>/components`.
|
|
33
|
+
* - sibling: a `components/default/<name>-component` file (parent `default`, grandparent `components`).
|
|
34
|
+
* - extraTargets: a structured consumer-defined layout.
|
|
35
|
+
*/
|
|
36
|
+
export declare const classifyComponent: (filePath: string, opts?: Partial<StoryPathOptions>) => {
|
|
37
|
+
mode: StoryMode;
|
|
38
|
+
} | null;
|
|
39
|
+
/**
|
|
40
|
+
* Ordered candidate story paths for a component file. Empty when the file is not a
|
|
41
|
+
* component-requiring-a-story (see {@link classifyComponent}). The story is considered present
|
|
42
|
+
* when ANY candidate exists on disk.
|
|
43
|
+
*/
|
|
44
|
+
export declare const deriveExpectedStoryPaths: (filePath: string, opts?: Partial<StoryPathOptions>) => string[];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slip-stream-kit/eslint-plugin",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.5",
|
|
5
5
|
"description": "Custom ESLint rules enforcing the white-label frontend architecture conventions",
|
|
6
6
|
"author": "Arthur Saenko <arthur.saenz7@gmail.com> (https://github.com/ArthurSaenz)",
|
|
7
7
|
"license": "MIT",
|
|
@@ -52,11 +52,11 @@
|
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@types/node": "catalog:",
|
|
55
|
-
"@typescript-eslint/parser": "^8.
|
|
55
|
+
"@typescript-eslint/parser": "^8.61.1",
|
|
56
56
|
"@wl/eslint-config": "workspace:*",
|
|
57
57
|
"esbuild": "^0.28.0",
|
|
58
|
-
"eslint": "^10.
|
|
58
|
+
"eslint": "^10.5.0",
|
|
59
59
|
"typescript": "^6.0.3",
|
|
60
|
-
"vitest": "^4.1.
|
|
60
|
+
"vitest": "^4.1.9"
|
|
61
61
|
}
|
|
62
62
|
}
|
package/readme.md
CHANGED
|
@@ -133,3 +133,62 @@ every file (you can also scope it the usual way with flat-config `files`).
|
|
|
133
133
|
|
|
134
134
|
Glob support: `*` matches within a path segment, `**` matches across segments,
|
|
135
135
|
`?` matches a single character. A file matches if any pattern matches its path.
|
|
136
|
+
|
|
137
|
+
### `require-component-stories`
|
|
138
|
+
|
|
139
|
+
Require a co-located Storybook story for every dumb component. By default it enforces two layouts,
|
|
140
|
+
mirroring the white-label `fe-architect` convention:
|
|
141
|
+
|
|
142
|
+
| Layout | Component file | Required story |
|
|
143
|
+
| --- | --- | --- |
|
|
144
|
+
| Feature component | `features/<feature>/components/<name>-component.tsx` | `features/<feature>/__stories__/<name>-component.stories.tsx` (feature root) |
|
|
145
|
+
| Shared default component | `components/default/<name>-component.tsx` | `components/default/__stories__/<name>-component.stories.tsx` (sibling) |
|
|
146
|
+
|
|
147
|
+
A file is treated as a dumb component when it ends with the `-component` suffix, has a `.tsx`/`.jsx`
|
|
148
|
+
extension, sits directly inside a `components/` directory (feature layout) or `components/default/`
|
|
149
|
+
(shared layout), and — by default — actually declares a React component. Containers, nested
|
|
150
|
+
`components/sub/*` files, barrels (`index.*`), and type files are never required to have stories. The
|
|
151
|
+
story is satisfied when any candidate (`.tsx`, `.jsx`, `.ts`, `.js`) exists on disk.
|
|
152
|
+
|
|
153
|
+
```js
|
|
154
|
+
{
|
|
155
|
+
rules: {
|
|
156
|
+
'@wl/require-component-stories': 'error',
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
#### Options (all optional)
|
|
162
|
+
|
|
163
|
+
| Option | Default | Description |
|
|
164
|
+
| --- | --- | --- |
|
|
165
|
+
| `paths` | `[]` | Restrict the rule to files matching these globs. |
|
|
166
|
+
| `ignore` | `[]` | Skip files matching these globs (takes precedence over `paths`). |
|
|
167
|
+
| `storiesDir` | `'__stories__'` | Directory name a story must live in. |
|
|
168
|
+
| `storySuffix` | `'.stories'` | Suffix inserted before the extension. |
|
|
169
|
+
| `storyExtensions` | `['.tsx', '.jsx', '.ts', '.js']` | Accepted story extensions, in priority order. |
|
|
170
|
+
| `componentSuffix` | `'-component'` | Basename suffix a component must end with (`''` disables the check). |
|
|
171
|
+
| `requireComponentAst` | `true` | When true, only require a story for files that actually declare a component. |
|
|
172
|
+
| `extraTargets` | `[]` | Extra structured layouts: `{ componentsDir, anchorParentDir?, storyMode: 'feature-root' \| 'sibling' }`. |
|
|
173
|
+
|
|
174
|
+
```js
|
|
175
|
+
{
|
|
176
|
+
rules: {
|
|
177
|
+
'@wl/require-component-stories': ['error', {
|
|
178
|
+
extraTargets: [{ componentsDir: 'widgets', anchorParentDir: 'ui', storyMode: 'sibling' }],
|
|
179
|
+
}],
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
#### Caveats
|
|
185
|
+
|
|
186
|
+
- **Filesystem-coupled.** Unlike pure AST rules, this one checks the disk for a sibling story file,
|
|
187
|
+
so results depend on the working-tree state.
|
|
188
|
+
- **`--cache`.** Adding or removing a story file does not change the component file, so a cached
|
|
189
|
+
ESLint result can go stale. Run without `--cache` in CI (or invalidate the cache) if you rely on
|
|
190
|
+
this rule as a gate.
|
|
191
|
+
- **Case sensitivity.** The existence check is exact-case; a casing mismatch may pass on a
|
|
192
|
+
case-insensitive filesystem (macOS) and fail on a case-sensitive one (Linux CI).
|
|
193
|
+
- **Unanchored globs.** `paths`/`ignore` patterns match anywhere in the path, so anchor them
|
|
194
|
+
(e.g. start with `**/`) when you need precision.
|