@slip-stream-kit/eslint-plugin 0.43.0 → 0.44.0

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.
@@ -0,0 +1,2 @@
1
+ import type { Rule } from 'eslint';
2
+ export declare const atomNaming: Rule.RuleModule;
@@ -0,0 +1 @@
1
+ export { atomNaming } from './atom-naming';
@@ -0,0 +1,2 @@
1
+ import type { Rule } from 'eslint';
2
+ export declare const componentClassname: Rule.RuleModule;
@@ -0,0 +1 @@
1
+ export { componentClassname } from './component-classname';
@@ -0,0 +1,4 @@
1
+ import type { Rule } from 'eslint';
2
+ export declare const DEFAULT_PATHS: string[];
3
+ export declare const DEFAULT_IGNORE: string[];
4
+ export declare const componentNameMatchesFile: Rule.RuleModule;
@@ -0,0 +1 @@
1
+ export { componentNameMatchesFile } from './component-name-matches-file';
@@ -0,0 +1,3 @@
1
+ import type { Rule } from 'eslint';
2
+ export declare const DEFAULT_ALLOWED_HOOKS: string[];
3
+ export declare const dumbComponentPurity: Rule.RuleModule;
@@ -0,0 +1 @@
1
+ export { dumbComponentPurity } from './dumb-component-purity';
@@ -0,0 +1,4 @@
1
+ import type { Rule } from 'eslint';
2
+ export declare const ROOT_FILES: string[];
3
+ export declare const SERVICE_FILES: string[];
4
+ export declare const featureFiles: Rule.RuleModule;
@@ -0,0 +1 @@
1
+ export { featureFiles } from './feature-files';
@@ -0,0 +1 @@
1
+ export { noHooksInServices } from './no-hooks-in-services';
@@ -0,0 +1,3 @@
1
+ import type { Rule } from 'eslint';
2
+ export declare const DEFAULT_PATHS: string[];
3
+ export declare const noHooksInServices: Rule.RuleModule;
@@ -0,0 +1,8 @@
1
+ /** Where a feature's dumb components live — the default scope of the dumb-component rules. */
2
+ export declare const DUMB_COMPONENT_PATHS: string[];
3
+ /** Whether `filename` is a story or test file, which renders components without being one. */
4
+ export declare const isStoryOrTestFile: (filename: string) => boolean;
5
+ /** Whether `filename` sits in a package that can hold UI components (virtual filenames count). */
6
+ export declare const isUiPackageFile: (filename: string) => boolean;
7
+ /** The shared `paths`/`ignore` gate: ignore wins, an empty `paths` matches everything. */
8
+ export declare const isInScope: (filename: string, paths: readonly string[], ignore: readonly string[]) => boolean;
@@ -0,0 +1,16 @@
1
+ import type * as ESTree from 'estree';
2
+ import type { ComponentFunction } from './component';
3
+ export interface ExportedComponent {
4
+ /** Local (declared) name, or null for an anonymous default export. */
5
+ name: string | null;
6
+ /** The identifier that declares the name, or null for an anonymous default export. */
7
+ id: ESTree.Identifier | null;
8
+ /** The node a variable lookup resolves the declaration from (`getDeclaredVariables`). */
9
+ declaration: ESTree.FunctionDeclaration | ESTree.VariableDeclarator | null;
10
+ fn: ComponentFunction;
11
+ }
12
+ /**
13
+ * The React components a module exports: `export const X = ...`, `export function X`,
14
+ * `export default function X`, and `export { X }` / `export default X` of a local component.
15
+ */
16
+ export declare const collectExportedComponents: (program: ESTree.Program) => ExportedComponent[];
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.43.0",
4
+ "version": "0.44.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ArthurSaenz/infra-kit.git",
@@ -41,7 +41,7 @@
41
41
  "eslint": ">=9"
42
42
  },
43
43
  "devDependencies": {
44
- "@slip-stream-kit/config": "0.43.0",
44
+ "@slip-stream-kit/config": "0.44.0",
45
45
  "@types/node": "^26.5.1",
46
46
  "@typescript-eslint/parser": "^8.70.0",
47
47
  "@wl/eslint-config": "0.1.0",
package/readme.md CHANGED
@@ -413,10 +413,10 @@ const E = () => <div /> // reported here: "This file declares 5 components (max
413
413
  Only **top-level** declarations are counted. A multi-declarator statement
414
414
  (`const A = () => …, B = () => …`) counts each component separately. Re-exports
415
415
  (`export { X } from './x'`) declare nothing and are not counted, and
416
- `styled.div\`…\`` tagged templates are not component functions, so they are not
416
+ `styled.div\`…\``tagged templates are not component functions, so they are not
417
417
  counted either. Nested / in-render components are intentionally out of scope —
418
418
  that is a different concern (component identity / re-render stability), better
419
- served by `react/no-unstable-nested-components`.
419
+ served by`react/no-unstable-nested-components`.
420
420
 
421
421
  Detection uses the same heuristic as the other rules (PascalCase name through
422
422
  `memo`/`forwardRef`/`observer` wrappers, or a JSX return). A consequence worth
@@ -762,6 +762,138 @@ outside, so the public API is the right entry for them.
762
762
  | `rootAlias` | `string` | `'#root'` | The alias that maps to `src/`. |
763
763
  | `ignore` | `string[]` | `[]` | Globs; the rule is skipped for matching files. |
764
764
 
765
+ ### `atom-naming`
766
+
767
+ Jotai atoms declared at module top level follow the fe-architect naming rules:
768
+
769
+ - a readable atom (primitive, derived, read-write, `atomFamily`) starts with **`$`**;
770
+ - a write-only atom (`atom(null, write)`) has no `$` and ends with **`Fx`** when `write` is `async`, **`Atom`** otherwise;
771
+ - the write function takes at most one argument after `get, set`, typed as a named **`*Args`** type.
772
+
773
+ ```ts
774
+ // ❌ Incorrect
775
+ export const isOpen = atom(false)
776
+ export const toggleOpenedCard = atom(null, (get, set, currentId: string) => {})
777
+ export const startSearch = atom(null, async (_get, set, args: Omit<SearchArg, 'navigate'>) => {})
778
+
779
+ // ✅ Correct
780
+ export const $isOpen = atom(false)
781
+ export const toggleOpenedCardAtom = atom(null, (get, set, args: ToggleOpenedCardArgs) => {})
782
+ export const startSearchFx = atom(null, async (_get, set, args: StartSearchArgs) => {})
783
+ ```
784
+
785
+ No autofix: renaming an exported atom touches every importer. A write function passed by reference
786
+ (`atom(null, fn)`) accepts either suffix, and an untyped argument passes (tsc already types it).
787
+
788
+ #### Options
789
+
790
+ | Option | Type | Default |
791
+ | ---------------- | ---------- | -------- |
792
+ | `readablePrefix` | `string` | `'$'` |
793
+ | `asyncSuffix` | `string` | `'Fx'` |
794
+ | `syncSuffix` | `string` | `'Atom'` |
795
+ | `argsSuffix` | `string` | `'Args'` |
796
+ | `paths`/`ignore` | `string[]` | `[]` |
797
+
798
+ ### `no-hooks-in-services`
799
+
800
+ A service is atoms and pure functions. A `use*` function declared in `services/**`, `services.ts`,
801
+ `service.ts` or `entities/**/model.ts` is reported; the hook belongs in a container or `lib/`.
802
+ Backend and e2e packages are skipped (backends have `src/services/` too).
803
+
804
+ ```ts
805
+ // ❌ services/main.ts
806
+ export const useCatalogComments = (module: ECategory) => { … }
807
+
808
+ // ✅ services/main.ts
809
+ export const loadCatalogDataFx = atom(null, async (_get, set, args: LoadCatalogDataArgs) => { … })
810
+ ```
811
+
812
+ `paths` replaces the default locations; `ignore` skips files.
813
+
814
+ ### `feature-files`
815
+
816
+ `package-structure` judges a feature's folders; this rule judges its files.
817
+
818
+ - Feature root: `index`, `types`, `services`, `analytics`, `constants`, `translates` (`.ts`/`.tsx`), plus `extraRootFiles`.
819
+ - `services/` direct children: `main`, `api`, `libs`, `index`, `types`, `constants`, plus `extraServiceFiles`.
820
+ - A feature has `services.ts` **or** `services/`, not both.
821
+ - A service file renders nothing: JSX in it is reported.
822
+
823
+ Tests, stories and `.d.ts` files are skipped, as are backend and e2e packages.
824
+
825
+ ```
826
+ "service.ts" is not an allowed feature-root file. Allowed: index.ts, types.ts, services.ts, analytics.ts, constants.ts, translates.ts.
827
+ ```
828
+
829
+ #### Options
830
+
831
+ | Option | Type | Default | Description |
832
+ | ------------------- | ---------- | ------- | ------------------------------------ |
833
+ | `extraRootFiles` | `string[]` | `[]` | Extra root names, without extension. |
834
+ | `extraServiceFiles` | `string[]` | `[]` | Extra `services/` names, likewise. |
835
+ | `paths`/`ignore` | `string[]` | `[]` | Globs restricting / skipping files. |
836
+
837
+ ### `component-name-matches-file`
838
+
839
+ The exported component is named after its file in PascalCase: `abroad-slider-component.tsx` →
840
+ `AbroadSliderComponent`, `julyo-baggage-container.tsx` → `JulyoBaggageContainer`. A file under
841
+ `components/` ends with `-component.tsx`, one under `containers/` with `-container.tsx`.
842
+
843
+ ```tsx
844
+ // ❌ components/abroad-slider-component.tsx
845
+ export const AbroadSlider = (props: AbroadSliderProps) => …
846
+
847
+ // ✅
848
+ export const AbroadSliderComponent = (props: AbroadSliderComponentProps) => …
849
+ ```
850
+
851
+ A suggestion renames the component inside the file; importers are left to you. With several exported
852
+ components one matching name is enough. `index.tsx`, stories and tests are skipped.
853
+
854
+ It runs on `**/features/**`, `**/*-component.tsx` and `**/*-container.tsx` by default (`paths` replaces that).
855
+ `ignore` defaults to `**/pages/**`, `**/routes/**`, `**/renderer/**`, `**/primitives/**`; a value you
856
+ pass **replaces** that default.
857
+
858
+ ### `component-classname`
859
+
860
+ A dumb component (`features/*/components/**`, `*-component.tsx`) accepts `className` and merges it
861
+ into its root element.
862
+
863
+ ```tsx
864
+ // ❌
865
+ export interface BadgeComponentProps { label: string }
866
+ export const BadgeComponent = (props: BadgeComponentProps) => <span className="rounded">{props.label}</span>
867
+
868
+ // ✅
869
+ export interface BadgeComponentProps { label: string; className?: string }
870
+ export const BadgeComponent = (props: BadgeComponentProps) => {
871
+ const { label, className } = props
872
+
873
+ return <span className={cn('rounded', className)}>{label}</span>
874
+ }
875
+ ```
876
+
877
+ The fix adds `className?: string` to an in-file props type. The root is checked only for a single
878
+ non-Fragment JSX return; a spread attribute passes. `cn(styles.root, className)` passes, so CSS
879
+ modules need no option. Imported or generic props types are skipped. Options: `paths`, `ignore`.
880
+
881
+ ### `dumb-component-purity`
882
+
883
+ A dumb component renders props. In its scope (same as `component-classname`) only these hooks are
884
+ allowed: `useState`, `useRef`, `useMemo`, `useCallback`, `useId`, `useEffect`, `useLayoutEffect`,
885
+ `useImperativeHandle`, plus `allowHooks`. Any other hook — including a `#root/lib` wrapper around
886
+ state — is reported, and so is `fetch`, `httpClient.*` or an analytics call inside an effect.
887
+
888
+ ```
889
+ "useFeatureFlag" is not in the allowed hook list for dumb components. Call it in the container and pass the value as a prop.
890
+ ```
891
+
892
+ | Option | Type | Default | Description |
893
+ | ---------------- | ---------- | ------- | --------------------------------------------- |
894
+ | `allowHooks` | `string[]` | `[]` | Added to the defaults (e.g. `useMediaQuery`). |
895
+ | `paths`/`ignore` | `string[]` | scope | `paths` replaces the default scope. |
896
+
765
897
  ### `e2e-file-layout`
766
898
 
767
899
  Hold an **e2e package** to its file layout: specs are `*.spec.ts` at the root of a domain folder
@@ -1020,7 +1152,7 @@ Counting rules:
1020
1152
  block exactly. `@param`/`@returns`/`@throws` lines charge the prose budget.
1021
1153
  - **Both budgets can report on the same block** — they are checked independently.
1022
1154
 
1023
- **Why two budgets.** `@wl/require-jsdoc-example` *mandates* an `@example` on
1155
+ **Why two budgets.** `@wl/require-jsdoc-example` _mandates_ an `@example` on
1024
1156
  functions at cognitive complexity ≥ 12. If a rule-mandated example were charged
1025
1157
  against the prose budget, those functions could be pushed into a state with no
1026
1158
  compliant answer. Charging examples to their own budget is the contract that
@@ -1028,11 +1160,11 @@ keeps the two rules from deadlocking; it is pinned by an integration test.
1028
1160
 
1029
1161
  #### Options
1030
1162
 
1031
- | Option | Default | Description |
1032
- | ----------------- | -------------------------------------------------- | --------------------------------------------------------------------------- |
1033
- | `maxLines` | `15` | Prose ceiling — total lines minus the `@example` bodies. |
1034
- | `maxExampleLines` | `10` | Combined ceiling for the block's `@example` bodies. |
1035
- | `exemptTags` | `['fileoverview', 'module', 'packageDocumentation']` | Tags that exempt a block entirely. |
1163
+ | Option | Default | Description |
1164
+ | ----------------- | ---------------------------------------------------- | -------------------------------------------------------- |
1165
+ | `maxLines` | `15` | Prose ceiling — total lines minus the `@example` bodies. |
1166
+ | `maxExampleLines` | `10` | Combined ceiling for the block's `@example` bodies. |
1167
+ | `exemptTags` | `['fileoverview', 'module', 'packageDocumentation']` | Tags that exempt a block entirely. |
1036
1168
 
1037
1169
  Only counts strictly greater than a ceiling report (`lines === max` is allowed).
1038
1170
 
@@ -1075,7 +1207,7 @@ documents).
1075
1207
 
1076
1208
  There is no autofix, and there will not be one: shortening a block means
1077
1209
  deleting sentences, and the longest blocks in a codebase are usually its most
1078
- valuable documentation. The part of this problem that *is* mechanically fixable —
1210
+ valuable documentation. The part of this problem that _is_ mechanically fixable —
1079
1211
  blank-line padding and `{type}` annotations — is already handled by
1080
1212
  `jsdoc/tag-lines` and `jsdoc/no-types`, which do have fixers. Run those first;
1081
1213
  what remains is a judgement call for a human.
@@ -1141,9 +1273,9 @@ Counting rules:
1141
1273
 
1142
1274
  #### Options
1143
1275
 
1144
- | Option | Default | Description |
1145
- | ----------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------- |
1146
- | `maxSummaryLines` | `5` | Prose ceiling for the summary paragraph. |
1276
+ | Option | Default | Description |
1277
+ | ----------------- | ---------------------------------------------------- | ---------------------------------------------------------------------------------------- |
1278
+ | `maxSummaryLines` | `5` | Prose ceiling for the summary paragraph. |
1147
1279
  | `exemptTags` | `['fileoverview', 'module', 'packageDocumentation']` | Tags that exempt a block entirely. Same list as `max-jsdoc-lines`, so one tag does both. |
1148
1280
 
1149
1281
  ```js