@slip-stream-kit/eslint-plugin 0.43.2 → 0.45.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 @@
1
+ export { servicesNamespaceImport } from './services-namespace-import';
@@ -0,0 +1,2 @@
1
+ import type { Rule } from 'eslint';
2
+ export declare const servicesNamespaceImport: 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.2",
4
+ "version": "0.45.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.2",
44
+ "@slip-stream-kit/config": "0.45.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,173 @@ 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
+ ### `services-namespace-import`
838
+
839
+ 🔧 Automatically fixable.
840
+
841
+ A relative import of a `services` module (`./services`, `../services`, `../services/index`) is a
842
+ namespace import named `service`, so every call site reads as `service.<member>` and a `vi.mock` of the
843
+ module replaces what the code actually reads. `import type` and inline `type` specifiers are allowed.
844
+
845
+ ```ts
846
+ // ❌ Incorrect
847
+ import { $isOpen, fetchItemsFx } from '../services'
848
+ import * as services from '../services'
849
+ // ✅ Correct
850
+ import * as service from '../services'
851
+ import type { Item } from '../services'
852
+ ```
853
+
854
+ ```
855
+ Import `../services` as a namespace — `import * as service from '../services'` — and read members as `service.<member>`.
856
+ Name the `../services` namespace `service`, not `services`.
857
+ ```
858
+
859
+ Autofix rewrites the import and prefixes every reference (`{ foo }` becomes `{ foo: service.foo }`). It
860
+ stands down — the report stays — when a reference is a JSX tag or a local re-export, when `service` is
861
+ already bound or shadowed where a reference sits, or when a file imports two different services modules.
862
+ Tests and stories are checked too; `index.*` barrels (which publish the services under a module-specific
863
+ name) and backend and e2e packages are skipped.
864
+
865
+ #### Options
866
+
867
+ | Option | Type | Default | Description |
868
+ | ---------------- | ---------- | ----------- | ----------------------------------- |
869
+ | `name` | `string` | `'service'` | The namespace binding. |
870
+ | `paths`/`ignore` | `string[]` | `[]` | Globs restricting / skipping files. |
871
+
872
+ ### `component-name-matches-file`
873
+
874
+ The exported component is named after its file in PascalCase: `abroad-slider-component.tsx` →
875
+ `AbroadSliderComponent`, `julyo-baggage-container.tsx` → `JulyoBaggageContainer`. A file under
876
+ `components/` ends with `-component.tsx`, one under `containers/` with `-container.tsx`.
877
+
878
+ ```tsx
879
+ // ❌ components/abroad-slider-component.tsx
880
+ export const AbroadSlider = (props: AbroadSliderProps) => …
881
+
882
+ // ✅
883
+ export const AbroadSliderComponent = (props: AbroadSliderComponentProps) => …
884
+ ```
885
+
886
+ A suggestion renames the component inside the file; importers are left to you. With several exported
887
+ components one matching name is enough. `index.tsx`, stories and tests are skipped.
888
+
889
+ It runs on `**/features/**`, `**/*-component.tsx` and `**/*-container.tsx` by default (`paths` replaces that).
890
+ `ignore` defaults to `**/pages/**`, `**/routes/**`, `**/renderer/**`, `**/primitives/**`; a value you
891
+ pass **replaces** that default.
892
+
893
+ ### `component-classname`
894
+
895
+ A dumb component (`features/*/components/**`, `*-component.tsx`) accepts `className` and merges it
896
+ into its root element.
897
+
898
+ ```tsx
899
+ // ❌
900
+ export interface BadgeComponentProps { label: string }
901
+ export const BadgeComponent = (props: BadgeComponentProps) => <span className="rounded">{props.label}</span>
902
+
903
+ // ✅
904
+ export interface BadgeComponentProps { label: string; className?: string }
905
+ export const BadgeComponent = (props: BadgeComponentProps) => {
906
+ const { label, className } = props
907
+
908
+ return <span className={cn('rounded', className)}>{label}</span>
909
+ }
910
+ ```
911
+
912
+ The fix adds `className?: string` to an in-file props type. The root is checked only for a single
913
+ non-Fragment JSX return; a spread attribute passes. `cn(styles.root, className)` passes, so CSS
914
+ modules need no option. Imported or generic props types are skipped. Options: `paths`, `ignore`.
915
+
916
+ ### `dumb-component-purity`
917
+
918
+ A dumb component renders props. In its scope (same as `component-classname`) only these hooks are
919
+ allowed: `useState`, `useRef`, `useMemo`, `useCallback`, `useId`, `useEffect`, `useLayoutEffect`,
920
+ `useImperativeHandle`, plus `allowHooks`. Any other hook — including a `#root/lib` wrapper around
921
+ state — is reported, and so is `fetch`, `httpClient.*` or an analytics call inside an effect.
922
+
923
+ ```
924
+ "useFeatureFlag" is not in the allowed hook list for dumb components. Call it in the container and pass the value as a prop.
925
+ ```
926
+
927
+ | Option | Type | Default | Description |
928
+ | ---------------- | ---------- | ------- | --------------------------------------------- |
929
+ | `allowHooks` | `string[]` | `[]` | Added to the defaults (e.g. `useMediaQuery`). |
930
+ | `paths`/`ignore` | `string[]` | scope | `paths` replaces the default scope. |
931
+
765
932
  ### `e2e-file-layout`
766
933
 
767
934
  Hold an **e2e package** to its file layout: specs are `*.spec.ts` at the root of a domain folder
@@ -1020,7 +1187,7 @@ Counting rules:
1020
1187
  block exactly. `@param`/`@returns`/`@throws` lines charge the prose budget.
1021
1188
  - **Both budgets can report on the same block** — they are checked independently.
1022
1189
 
1023
- **Why two budgets.** `@wl/require-jsdoc-example` *mandates* an `@example` on
1190
+ **Why two budgets.** `@wl/require-jsdoc-example` _mandates_ an `@example` on
1024
1191
  functions at cognitive complexity ≥ 12. If a rule-mandated example were charged
1025
1192
  against the prose budget, those functions could be pushed into a state with no
1026
1193
  compliant answer. Charging examples to their own budget is the contract that
@@ -1028,11 +1195,11 @@ keeps the two rules from deadlocking; it is pinned by an integration test.
1028
1195
 
1029
1196
  #### Options
1030
1197
 
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. |
1198
+ | Option | Default | Description |
1199
+ | ----------------- | ---------------------------------------------------- | -------------------------------------------------------- |
1200
+ | `maxLines` | `15` | Prose ceiling — total lines minus the `@example` bodies. |
1201
+ | `maxExampleLines` | `10` | Combined ceiling for the block's `@example` bodies. |
1202
+ | `exemptTags` | `['fileoverview', 'module', 'packageDocumentation']` | Tags that exempt a block entirely. |
1036
1203
 
1037
1204
  Only counts strictly greater than a ceiling report (`lines === max` is allowed).
1038
1205
 
@@ -1075,7 +1242,7 @@ documents).
1075
1242
 
1076
1243
  There is no autofix, and there will not be one: shortening a block means
1077
1244
  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 —
1245
+ valuable documentation. The part of this problem that _is_ mechanically fixable —
1079
1246
  blank-line padding and `{type}` annotations — is already handled by
1080
1247
  `jsdoc/tag-lines` and `jsdoc/no-types`, which do have fixers. Run those first;
1081
1248
  what remains is a judgement call for a human.
@@ -1141,9 +1308,9 @@ Counting rules:
1141
1308
 
1142
1309
  #### Options
1143
1310
 
1144
- | Option | Default | Description |
1145
- | ----------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------- |
1146
- | `maxSummaryLines` | `5` | Prose ceiling for the summary paragraph. |
1311
+ | Option | Default | Description |
1312
+ | ----------------- | ---------------------------------------------------- | ---------------------------------------------------------------------------------------- |
1313
+ | `maxSummaryLines` | `5` | Prose ceiling for the summary paragraph. |
1147
1314
  | `exemptTags` | `['fileoverview', 'module', 'packageDocumentation']` | Tags that exempt a block entirely. Same list as `max-jsdoc-lines`, so one tag does both. |
1148
1315
 
1149
1316
  ```js