@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.
- package/dist/index.js +1425 -228
- package/dist/index.js.map +4 -4
- package/dist/rules/atom-naming/atom-naming.d.ts +2 -0
- package/dist/rules/atom-naming/index.d.ts +1 -0
- package/dist/rules/component-classname/component-classname.d.ts +2 -0
- package/dist/rules/component-classname/index.d.ts +1 -0
- package/dist/rules/component-name-matches-file/component-name-matches-file.d.ts +4 -0
- package/dist/rules/component-name-matches-file/index.d.ts +1 -0
- package/dist/rules/dumb-component-purity/dumb-component-purity.d.ts +3 -0
- package/dist/rules/dumb-component-purity/index.d.ts +1 -0
- package/dist/rules/feature-files/feature-files.d.ts +4 -0
- package/dist/rules/feature-files/index.d.ts +1 -0
- package/dist/rules/no-hooks-in-services/index.d.ts +1 -0
- package/dist/rules/no-hooks-in-services/no-hooks-in-services.d.ts +3 -0
- package/dist/rules/services-namespace-import/index.d.ts +1 -0
- package/dist/rules/services-namespace-import/services-namespace-import.d.ts +2 -0
- package/dist/utils/component-file-scope.d.ts +8 -0
- package/dist/utils/exported-components.d.ts +16 -0
- package/package.json +2 -2
- package/readme.md +179 -12
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { atomNaming } from './atom-naming';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { componentClassname } from './component-classname';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { componentNameMatchesFile } from './component-name-matches-file';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { dumbComponentPurity } from './dumb-component-purity';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { featureFiles } from './feature-files';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { noHooksInServices } from './no-hooks-in-services';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { servicesNamespaceImport } from './services-namespace-import';
|
|
@@ -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.
|
|
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.
|
|
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\`…\``
|
|
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
|
|
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`
|
|
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
|
|
1032
|
-
| ----------------- |
|
|
1033
|
-
| `maxLines` | `15`
|
|
1034
|
-
| `maxExampleLines` | `10`
|
|
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
|
|
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
|