@starci/eslint-canon-fe 9.2.0 → 9.4.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/CHANGELOG.md +14 -0
- package/README.md +5 -1
- package/dist/brand-values.d.ts +28 -0
- package/dist/brand-values.js +172 -0
- package/dist/button-intent.d.ts +11 -0
- package/dist/button-intent.js +361 -0
- package/dist/callback-shape.d.ts +7 -0
- package/dist/callback-shape.js +396 -0
- package/dist/class-names.d.ts +19 -0
- package/dist/class-names.js +170 -0
- package/dist/client-boundary.d.ts +42 -0
- package/dist/client-boundary.js +404 -0
- package/dist/comments.d.ts +47 -0
- package/dist/comments.js +156 -0
- package/dist/control-height.d.ts +7 -0
- package/dist/control-height.js +83 -0
- package/dist/drawing.d.ts +31 -0
- package/dist/drawing.js +376 -0
- package/dist/env-owner.d.ts +22 -0
- package/dist/env-owner.js +291 -0
- package/dist/fe-stack.d.ts +10 -0
- package/dist/fe-stack.js +73 -0
- package/dist/file-layout.d.ts +71 -0
- package/dist/file-layout.js +465 -0
- package/dist/formatting.d.ts +25 -0
- package/dist/formatting.js +104 -0
- package/dist/function-budget.d.ts +9 -0
- package/dist/function-budget.js +48 -0
- package/dist/grammar-shim.d.ts +5 -0
- package/dist/grammar-shim.js +126 -0
- package/dist/hooks-folder.d.ts +34 -0
- package/dist/hooks-folder.js +320 -0
- package/dist/hygiene.d.ts +39 -0
- package/dist/hygiene.js +540 -0
- package/dist/index.d.ts +105 -0
- package/dist/index.js +310 -0
- package/{landmark.mjs → dist/landmark.d.ts} +4 -6
- package/dist/landmark.js +6 -0
- package/dist/lib/ast.d.ts +36 -0
- package/dist/lib/ast.js +94 -0
- package/dist/lib/bindings.d.ts +84 -0
- package/dist/lib/bindings.js +168 -0
- package/dist/lib/class-text.d.ts +3 -0
- package/dist/lib/class-text.js +30 -0
- package/dist/lib/config.d.ts +81 -0
- package/dist/lib/config.js +141 -0
- package/dist/lib/drawing-scope.d.ts +5 -0
- package/{lib/drawing-scope.mjs → dist/lib/drawing-scope.js} +6 -11
- package/dist/lib/eslint-boundary.d.ts +23 -0
- package/dist/lib/eslint-boundary.js +6 -0
- package/dist/lib/fe-stack.d.ts +49 -0
- package/dist/lib/fe-stack.js +98 -0
- package/dist/lib/hfs-view.d.ts +74 -0
- package/dist/lib/hfs-view.js +6 -0
- package/dist/lib/hfs.d.ts +34 -0
- package/dist/lib/hfs.js +72 -0
- package/dist/lib/next.d.ts +4 -0
- package/dist/lib/next.js +10 -0
- package/dist/lib/own-hook-origin.d.ts +17 -0
- package/dist/lib/own-hook-origin.js +52 -0
- package/dist/lib/package-root.d.ts +4 -0
- package/dist/lib/package-root.js +10 -0
- package/dist/lib/path.d.ts +2 -0
- package/{lib/path.mjs → dist/lib/path.js} +1 -1
- package/dist/lib/platform.d.ts +26 -0
- package/{lib/platform.mjs → dist/lib/platform.js} +38 -35
- package/dist/lib/program-for.d.ts +5 -0
- package/dist/lib/program-for.js +23 -0
- package/dist/lib/pure-owner-helper.d.ts +10 -0
- package/dist/lib/pure-owner-helper.js +156 -0
- package/dist/lib/react.d.ts +16 -0
- package/dist/lib/react.js +22 -0
- package/dist/lib/redux-hook-origin.d.ts +4 -0
- package/dist/lib/redux-hook-origin.js +50 -0
- package/dist/lib/rule.d.ts +28 -0
- package/dist/lib/rule.js +5 -0
- package/dist/lib/scope.d.ts +88 -0
- package/{lib/scope.mjs → dist/lib/scope.js} +83 -93
- package/dist/lib/typed-class-values.d.ts +13 -0
- package/dist/lib/typed-class-values.js +92 -0
- package/dist/lib/types.d.ts +44 -0
- package/dist/lib/types.js +64 -0
- package/dist/lib/why.d.ts +350 -0
- package/dist/lib/why.js +350 -0
- package/dist/lint-escape-hatch.d.ts +24 -0
- package/{lint-escape-hatch.mjs → dist/lint-escape-hatch.js} +38 -45
- package/dist/lists.d.ts +31 -0
- package/dist/lists.js +202 -0
- package/dist/loading.d.ts +34 -0
- package/dist/loading.js +157 -0
- package/dist/naming.d.ts +33 -0
- package/dist/naming.js +176 -0
- package/dist/native-controls.d.ts +24 -0
- package/dist/native-controls.js +105 -0
- package/dist/next-conventions.d.ts +48 -0
- package/dist/next-conventions.js +452 -0
- package/dist/project-graph.d.ts +5 -0
- package/{project-graph.mjs → dist/project-graph.js} +8 -12
- package/dist/props-and-slots.d.ts +19 -0
- package/dist/props-and-slots.js +320 -0
- package/dist/served-locale.d.ts +38 -0
- package/dist/served-locale.js +263 -0
- package/dist/shape-slot.d.ts +18 -0
- package/dist/shape-slot.js +324 -0
- package/dist/size-and-state-budget.d.ts +46 -0
- package/dist/size-and-state-budget.js +424 -0
- package/dist/size-growth.d.ts +24 -0
- package/dist/size-growth.js +64 -0
- package/dist/sonar-parity.d.ts +5 -0
- package/{sonar-parity.mjs → dist/sonar-parity.js} +11 -7
- package/dist/surface-rules.d.ts +18 -0
- package/dist/surface-rules.js +491 -0
- package/dist/swr-family.d.ts +15 -0
- package/dist/swr-family.js +419 -0
- package/dist/the-split.d.ts +27 -0
- package/dist/the-split.js +157 -0
- package/dist/tokens.d.ts +9 -0
- package/dist/tokens.js +35 -0
- package/dist/translation.d.ts +51 -0
- package/dist/translation.js +403 -0
- package/dist/transport.d.ts +73 -0
- package/dist/transport.js +885 -0
- package/dist/type-safety.d.ts +43 -0
- package/dist/type-safety.js +139 -0
- package/dist/ui-rulings.d.ts +11 -0
- package/dist/ui-rulings.js +121 -0
- package/package.json +31 -9
- package/runtime/knowledge/hfs/canon-pins.yaml +22 -14
- package/runtime/scripts/lib/package-at.mjs +5 -5
- package/brand-values.mjs +0 -172
- package/button-intent.mjs +0 -320
- package/callback-shape.mjs +0 -332
- package/class-names.mjs +0 -149
- package/client-boundary.mjs +0 -400
- package/comments.mjs +0 -159
- package/control-height.mjs +0 -77
- package/drawing.mjs +0 -315
- package/env-owner.mjs +0 -244
- package/fe-stack.mjs +0 -86
- package/file-layout.mjs +0 -472
- package/formatting.mjs +0 -110
- package/function-budget.mjs +0 -49
- package/grammar-shim.mjs +0 -123
- package/hooks-folder.mjs +0 -300
- package/hygiene.mjs +0 -513
- package/index.mjs +0 -317
- package/lib/ast.mjs +0 -97
- package/lib/bindings.mjs +0 -159
- package/lib/class-text.mjs +0 -29
- package/lib/config.mjs +0 -142
- package/lib/fe-stack.mjs +0 -77
- package/lib/hfs.mjs +0 -49
- package/lib/next.mjs +0 -6
- package/lib/own-hook-origin.mjs +0 -53
- package/lib/program-for.mjs +0 -22
- package/lib/pure-owner-helper.mjs +0 -148
- package/lib/react.mjs +0 -28
- package/lib/redux-hook-origin.mjs +0 -44
- package/lib/typed-class-values.mjs +0 -81
- package/lib/types.mjs +0 -63
- package/lib/why.mjs +0 -352
- package/lists.mjs +0 -193
- package/loading.mjs +0 -156
- package/naming.mjs +0 -185
- package/native-controls.mjs +0 -108
- package/next-conventions.mjs +0 -447
- package/props-and-slots.mjs +0 -276
- package/served-locale.mjs +0 -243
- package/size-and-state-budget.mjs +0 -376
- package/size-growth.mjs +0 -62
- package/surface-rules.mjs +0 -443
- package/swr-family.mjs +0 -326
- package/the-split.mjs +0 -155
- package/tokens.mjs +0 -39
- package/translation.mjs +0 -392
- package/transport.mjs +0 -830
- package/type-safety.mjs +0 -147
- package/ui-rulings.mjs +0 -134
package/dist/lib/why.js
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The "why" of each rule the HFS catalogue assigns to this canon.
|
|
3
|
+
*
|
|
4
|
+
* WHERE IT IS SURFACED. An ESLint message is written for the developer at the terminal, in English,
|
|
5
|
+
* and says what is wrong in the vocabulary of the rule. The agent that reads a failed land gate needs
|
|
6
|
+
* the catalogue's sentence instead: the finding code (the key in `modules/kernel/failure-codes.yaml`)
|
|
7
|
+
* and a headline and next step it can quote to the owner. This map is the canon's half of that
|
|
8
|
+
* contract: `code` is the finding code, `en` the English source of the headline with `<file>` /
|
|
9
|
+
* `<what>` placeholders the reader fills from the ESLint location, `fix` the one sentence "what do I
|
|
10
|
+
* do now". The owner reads both in Vietnamese through the declared message catalog: the `en` and
|
|
11
|
+
* `fix` strings are the keys of `modules/i18n/messages/canon.yaml`, so `translator('vi')(entry.en)` and
|
|
12
|
+
* `translator('vi')(entry.fix)` hand back the Vietnamese of the same sentence.
|
|
13
|
+
*
|
|
14
|
+
* Every rule that carries a catalogue id (R18, R22, R49-R59, R60-R62, R65, the fe-stack catalog) has an entry;
|
|
15
|
+
* the twin test refuses a rule of those laws with no entry and an entry for a rule that does not exist.
|
|
16
|
+
*/
|
|
17
|
+
/** @typedef {{ code: string, en: string, fix: string }} Why */
|
|
18
|
+
/** @type {Record<string, Why>} */
|
|
19
|
+
export const why = {
|
|
20
|
+
"selection-fill-no-check": {
|
|
21
|
+
code: "FE_NESTED_VARIANT",
|
|
22
|
+
en: "`<file>` adds a check indicator to a navigation-like single-select list.",
|
|
23
|
+
fix: 'Set selectionIndicator="none" and let the row fill carry selection.',
|
|
24
|
+
},
|
|
25
|
+
"no-grammar-internal-selector": {
|
|
26
|
+
code: "FE_GRAMMAR_LAYOUT_ONLY",
|
|
27
|
+
en: "`<file>` targets a Grammar internal class from application styles.",
|
|
28
|
+
fix: "Use the Grammar public props and slots; keep internal selectors inside Grammar.",
|
|
29
|
+
},
|
|
30
|
+
"divider-full-bleed": {
|
|
31
|
+
code: "FE_DIVIDER_FULL_BLEED",
|
|
32
|
+
en: "`<file>` insets a horizontal Grammar divider with wrapper padding.",
|
|
33
|
+
fix: "Move horizontal padding to the sibling content bands so the divider reaches both surface edges.",
|
|
34
|
+
},
|
|
35
|
+
"button-intent-required": {
|
|
36
|
+
code: "FE_BUTTON_INTENT_REQUIRED",
|
|
37
|
+
en: "`<file>` renders a Button or IconButton without the command's intent.",
|
|
38
|
+
fix: "Declare intent from the command's consequence and rank so Grammar supplies its variant.",
|
|
39
|
+
},
|
|
40
|
+
"control-height": {
|
|
41
|
+
code: "FE_CONTROL_HEIGHT",
|
|
42
|
+
en: "`<file>` picks a larger control size or mixes control sizes in one row.",
|
|
43
|
+
fix: "Omit size so Grammar draws the one 40px control height; use sm only for every control of an item or dense toolbar row, and lg only for a landing hero call to action.",
|
|
44
|
+
},
|
|
45
|
+
"no-grammar-shim": {
|
|
46
|
+
code: "FE_GRAMMAR_SHIM",
|
|
47
|
+
en: "`<file>` re-exports a Grammar component under the app's own name.",
|
|
48
|
+
fix: "Delete the shim and import the component from the Grammar where it is rendered; report the Grammar defect the shim worked around (the op files blocked) so the Grammar fixes it.",
|
|
49
|
+
},
|
|
50
|
+
"button-no-variant": {
|
|
51
|
+
code: "FE_BUTTON_NO_VARIANT",
|
|
52
|
+
en: "`<file>` chooses a Button or IconButton variant directly.",
|
|
53
|
+
fix: "Replace variant with intent and let Grammar resolve placement and visual treatment.",
|
|
54
|
+
},
|
|
55
|
+
"destructive-confirm-in-dialog": {
|
|
56
|
+
code: "FE_DESTRUCTIVE_CONFIRM_IN_DIALOG",
|
|
57
|
+
en: "`<file>` renders a final destructive press outside its confirmation dialog.",
|
|
58
|
+
fix: "Place the destructive confirm inside the same-file AlertDialog or ConfirmDialog; use entry on its trigger.",
|
|
59
|
+
},
|
|
60
|
+
"single-commit-per-render": {
|
|
61
|
+
code: "FE_SINGLE_COMMIT_PER_RENDER",
|
|
62
|
+
en: "`<file>` renders more than one commit in the same component render path.",
|
|
63
|
+
fix: "Keep the next action as commit and assign the other commands their consequence-based intents.",
|
|
64
|
+
},
|
|
65
|
+
"no-commit-in-collection-item": {
|
|
66
|
+
code: "FE_NO_COMMIT_IN_COLLECTION_ITEM",
|
|
67
|
+
en: "`<file>` repeats a commit inside a collection item render.",
|
|
68
|
+
fix: "Use alternative for the item's next action and keep the enclosing view's commit unique.",
|
|
69
|
+
},
|
|
70
|
+
"nested-control-variant": {
|
|
71
|
+
code: "FE_NESTED_VARIANT",
|
|
72
|
+
en: "`<file>` sets a static Grammar field variant, or forces primary Tabs inside a painted surface.",
|
|
73
|
+
fix: "Omit the field or nested Tabs variant so Grammar supplies the default from its surface context.",
|
|
74
|
+
},
|
|
75
|
+
"no-app-surface-paint": {
|
|
76
|
+
code: "FE_GRAMMAR_LAYOUT_ONLY",
|
|
77
|
+
en: "`<file>` sets paint or typography in app CSS instead of composing Grammar's published components and props.",
|
|
78
|
+
fix: "Keep app CSS layout-only and compose a painted Grammar surface such as SurfaceCard.",
|
|
79
|
+
},
|
|
80
|
+
"no-list-panel-padding": {
|
|
81
|
+
code: "FE_LIST_PANEL_PADDING",
|
|
82
|
+
en: "`<file>` adds horizontal wrapper padding to a Grammar list or its section caption, stacking the content inset.",
|
|
83
|
+
fix: "Remove the wrapper padding and use ListBox inset=\"panel\" so headers, captions and rows share the panel content edge.",
|
|
84
|
+
},
|
|
85
|
+
"no-env-outside-config": {
|
|
86
|
+
code: "FE_ENV_OWNER",
|
|
87
|
+
en: "`<file>` reads an environment variable. Only `modules/config` may read env.",
|
|
88
|
+
fix: "Read the value from a typed export of `modules/config` instead of `process.env`.",
|
|
89
|
+
},
|
|
90
|
+
"no-hardcoded-endpoint-fallback": {
|
|
91
|
+
code: "FE_ENV_OWNER",
|
|
92
|
+
en: "`<file>` carries a hardcoded fallback for an address or a secret (`?? \"http://localhost…\"`). A variable missing in production must throw at config load, not silently point at a dev machine.",
|
|
93
|
+
fix: "Delete the fallback; let `modules/config` throw when a variable is missing in production.",
|
|
94
|
+
},
|
|
95
|
+
"fetch-only-in-api-client": {
|
|
96
|
+
code: "FE_TRANSPORT_OWNER",
|
|
97
|
+
en: "`<file>` reaches the back end outside the API layer: `fetch` (or `Request`, `EventSource`, `sendBeacon`, another HTTP library) outside `modules/api`, or Apollo, axios or socket.io-client outside its home (`modules/api`, `modules/api/rest`, `hooks/socketio`).",
|
|
98
|
+
fix: "Call an operation of `modules/api` (Apollo GraphQL, or axios in `modules/api/rest`) through its SWR hook in `hooks/swr`; listen to realtime through `hooks/socketio`.",
|
|
99
|
+
},
|
|
100
|
+
"client-fetch-has-signal": {
|
|
101
|
+
code: "FE_TRANSPORT_OWNER",
|
|
102
|
+
en: "`fetch` in `<file>` has no `signal`. Every request of the API layer must carry a timeout and an `AbortSignal`.",
|
|
103
|
+
fix: "Pass `signal` (`AbortSignal.timeout(...)` combined with the caller's signal).",
|
|
104
|
+
},
|
|
105
|
+
"no-shared-transport-state": {
|
|
106
|
+
code: "FE_TRANSPORT_OWNER",
|
|
107
|
+
en: "`<file>` holds shared state as `let`/`var` in the API layer. Token and locale must not sit in a singleton.",
|
|
108
|
+
fix: "Pass credential and locale as parameters or through context.",
|
|
109
|
+
},
|
|
110
|
+
"client-maps-auth-to-refused": {
|
|
111
|
+
code: "FE_HTTP_STATUS_COLLAPSE",
|
|
112
|
+
en: "The client in `<file>` has no branch comparing `response.status` with 401 and 403 and returning `{ kind: \"refused\" }`, so the \"sign-in required\" state is never reached.",
|
|
113
|
+
fix: "Add a `response.status === 401 || response.status === 403` branch returning `{ ok: false, kind: \"refused\" }` right inside the client.",
|
|
114
|
+
},
|
|
115
|
+
"no-failure-collapse": {
|
|
116
|
+
code: "FE_HTTP_STATUS_COLLAPSE",
|
|
117
|
+
en: "`<file>` merges every failure (an error HTTP response or a failed `Outcome`) into one branch or one empty value, so the reason is lost.",
|
|
118
|
+
fix: "Keep the reason: for an `Outcome` branch on the reason code; for an HTTP response turn the status code into an `Outcome` (`refused` for 401/403, `not-found`, `invalid`, `unavailable`). Do not use the server's raw error as the reason.",
|
|
119
|
+
},
|
|
120
|
+
"no-hand-typed-wire": {
|
|
121
|
+
code: "FE_WIRE_GENERATED",
|
|
122
|
+
en: "`<file>` hand-types a wire type, casts a response, or writes a GraphQL document outside the API layer. Use the types generated from `be/contracts/`.",
|
|
123
|
+
fix: "Run codegen from the be contract snapshot (`be/contracts/`) and import the generated types; write each GraphQL document in its operation file under `modules/api/graphql/queries` or `modules/api/graphql/mutations`.",
|
|
124
|
+
},
|
|
125
|
+
"use-client-only-at-boundary": {
|
|
126
|
+
code: "FE_CLIENT_BOUNDARY",
|
|
127
|
+
en: "`\"use client\"` in `<file>` (`<slot>` may not be a client component).",
|
|
128
|
+
fix: "Put `\"use client\"` on the `index.tsx` of an interactive block, a leaf, or `error.tsx`/`global-error.tsx`; keep layout and page as server components.",
|
|
129
|
+
},
|
|
130
|
+
"hooks-folder-holds-hooks-only": {
|
|
131
|
+
code: "FE_HOOKS_ARE_HOOKS",
|
|
132
|
+
en: "`<file>` inside `hooks/` is not a React hook, or is not a file its hooks family names. A call to the back end goes to `modules/api`, a helper to `<domain>.shared.ts`.",
|
|
133
|
+
fix: "One hook per `use*.ts` file under the name its family gives it (`hooks/swr/api/graphql/queries/useQuery<X>Swr.ts`, `hooks/rhf/use<X>Form.ts`, ...); a shared helper goes in `<domain>.shared.ts`; a back-end call is an operation of `modules/api`.",
|
|
134
|
+
},
|
|
135
|
+
"no-middleware-file": {
|
|
136
|
+
code: "FE_NEXT_CONVENTIONS",
|
|
137
|
+
en: "`<file>` uses the name `middleware`. From Next 16 the request gate is `proxy.ts` exporting `proxy`.",
|
|
138
|
+
fix: "Rename the file to `proxy.ts` and rename the export to `proxy`.",
|
|
139
|
+
},
|
|
140
|
+
"locale-segment-is-locale": {
|
|
141
|
+
code: "FE_I18N_PLACEMENT",
|
|
142
|
+
en: "File `<file>` sits under a locale segment that is not `[locale]`. There is one pattern: `next-intl`, `[locale]`, default `vi`.",
|
|
143
|
+
fix: "Rename the segment directory to `[locale]`.",
|
|
144
|
+
},
|
|
145
|
+
"no-second-i18n-stack": {
|
|
146
|
+
code: "FE_I18N_PLACEMENT",
|
|
147
|
+
en: "`<file>` imports a second i18n library. The app uses only `next-intl` through `modules/i18n`.",
|
|
148
|
+
fix: "Switch to `next-intl`; delete the other library.",
|
|
149
|
+
},
|
|
150
|
+
"html-lang-from-locale": {
|
|
151
|
+
code: "FE_I18N_LITERAL",
|
|
152
|
+
en: "`<html lang>` in `<file>` is a hardcoded string. The `lang` attribute must come from the `[locale]` segment.",
|
|
153
|
+
fix: "Use `lang={locale}` from the parameter of the `[locale]` layout.",
|
|
154
|
+
},
|
|
155
|
+
"no-hardcoded-copy": {
|
|
156
|
+
code: "FE_I18N_LITERAL",
|
|
157
|
+
en: "The text `\"<text>\"` in `<file>:<line>` does not go through `t()`.",
|
|
158
|
+
fix: "Move the sentence into `modules/i18n/messages/<locale>.json` and read it with `t(\"key\")`; no exceptions.",
|
|
159
|
+
},
|
|
160
|
+
"no-raw-brand-value": {
|
|
161
|
+
code: "FE_STYLE_TOKEN_ONLY",
|
|
162
|
+
en: "`<file>` uses a raw colour/length value (`<what>`). Colour and spacing come only from grammar tokens; brand colour lives only in `brand.css`.",
|
|
163
|
+
fix: "Use a grammar token class or variable; a colour value is declared only in `modules/brand/brand.css`.",
|
|
164
|
+
},
|
|
165
|
+
"no-native-form-control": {
|
|
166
|
+
code: "FE_NATIVE_FORM_CONTROL",
|
|
167
|
+
en: "Raw `<tag>` in `<file>`. Use the grammar renderer.",
|
|
168
|
+
fix: "Replace it with the grammar component; if none exists yet, add it to grammar instead of drawing it in place.",
|
|
169
|
+
},
|
|
170
|
+
"component-line-budget": {
|
|
171
|
+
code: "FE_SIZE_AND_STATE_BUDGET",
|
|
172
|
+
en: "`<file>` is `<n>` lines long, over the 300-line budget of a component.",
|
|
173
|
+
fix: "Split the drawing part from the data part and each area into its own unit.",
|
|
174
|
+
},
|
|
175
|
+
"unit-hook-budget": {
|
|
176
|
+
code: "FE_SIZE_AND_STATE_BUDGET",
|
|
177
|
+
en: "`<unit>` has `<n>` data hooks / `<m>` useState, over the budget of 6.",
|
|
178
|
+
fix: "Fold state that changes together into a reducer or a dedicated hook; split each data area into its own connection block.",
|
|
179
|
+
},
|
|
180
|
+
"no-hand-rolled-polling": {
|
|
181
|
+
code: "FE_SIZE_AND_STATE_BUDGET",
|
|
182
|
+
en: "`<unit>` hand-writes a poll loop (`setInterval` or a self-rescheduling `setTimeout`).",
|
|
183
|
+
fix: "Refresh through a data hook (`refreshInterval`) or a socket, one mechanism per resource.",
|
|
184
|
+
},
|
|
185
|
+
"no-inline-lint-config": {
|
|
186
|
+
code: "HFS_INLINE_SUPPRESSION",
|
|
187
|
+
en: "A comment disabling a rule sits at `<file>:<line>`. HFS does not allow in-place suppression - fix the code, or propose a rule change.",
|
|
188
|
+
fix: "Remove `eslint-disable`, `@ts-ignore` or `@ts-expect-error` and fix the cause.",
|
|
189
|
+
},
|
|
190
|
+
"no-double-cast": {
|
|
191
|
+
code: "FE_TYPE_ESCAPE",
|
|
192
|
+
en: "`<file>` casts through `unknown` (`as unknown as T`): the compiler forgets everything it knows right where the data enters.",
|
|
193
|
+
fix: "Narrow from `unknown` with a type guard or a parser; do not cast.",
|
|
194
|
+
},
|
|
195
|
+
"no-type-assertion": {
|
|
196
|
+
code: "FE_TYPE_ESCAPE",
|
|
197
|
+
en: "`<file>` uses `as T` or `<T>x`: an assertion the compiler cannot check. If it is wrong, the error lands in the user's browser.",
|
|
198
|
+
fix: "Narrow with a type guard, `in`, a discriminant or a parser where the data enters; use `satisfies` when you only mean to check a literal.",
|
|
199
|
+
},
|
|
200
|
+
"no-non-null-assertion": {
|
|
201
|
+
code: "FE_TYPE_ESCAPE",
|
|
202
|
+
en: "`<file>` uses `x!`: asserting a value is present without proving it.",
|
|
203
|
+
fix: "Handle the absent branch (`if`, `??`, an early return) or fix the type so the value cannot be absent.",
|
|
204
|
+
},
|
|
205
|
+
"no-explicit-any": {
|
|
206
|
+
code: "FE_TYPE_ESCAPE",
|
|
207
|
+
en: "`<file>` uses `any`: it switches off type checking for that value and for everything inferred from it.",
|
|
208
|
+
fix: "Use a real type, a generic, or `unknown` narrowed at the use site.",
|
|
209
|
+
},
|
|
210
|
+
"list-item-has-key": {
|
|
211
|
+
code: "FE_LIST_KEY",
|
|
212
|
+
en: "An element returned from `.map` in `<file>` has no `key`, so React identifies the row by position and the row loses state on delete or reorder.",
|
|
213
|
+
fix: "Add a `key` taken from the data's id; for a fragment use `<Fragment key={...}>`.",
|
|
214
|
+
},
|
|
215
|
+
"no-index-key": {
|
|
216
|
+
code: "FE_LIST_KEY",
|
|
217
|
+
en: "A `key` in `<file>` comes from the `.map` index or a randomly generated value; an index is a position, not an identity.",
|
|
218
|
+
fix: "Use the id the data carries (`key={row.id}`); never the index, `Math.random`, `Date.now`.",
|
|
219
|
+
},
|
|
220
|
+
"no-inline-literal-prop-in-list": {
|
|
221
|
+
code: "FE_LIST_KEY",
|
|
222
|
+
en: "A component in a `.map` in `<file>` receives an object or array literal as a prop: every row gets a new value every render, so memo never hits.",
|
|
223
|
+
fix: "Hoist the constant outside the component or build it once before `.map`.",
|
|
224
|
+
},
|
|
225
|
+
"effect-subscription-needs-cleanup": {
|
|
226
|
+
code: "FE_EFFECT_CLEANUP",
|
|
227
|
+
en: "`<what>` in `<file>` (timer, frame, listener, observer, socket or subscription) is started by an effect or `subscribe` but the returned cleanup does not release that same handle/target/listener.",
|
|
228
|
+
fix: "Keep the handle and return a cleanup calling `clearTimeout(id)`, `cancelAnimationFrame(id)`, `removeEventListener` with the same type and listener, `.disconnect()`, `.close()` or `.unsubscribe()` on the right object.",
|
|
229
|
+
},
|
|
230
|
+
"no-data-fetch-in-effect": {
|
|
231
|
+
code: "FE_EFFECT_FETCH",
|
|
232
|
+
en: "`useEffect` in `<file>` starts a promise (`await`, `fetch`, `.then`, `void load()`, `mutate()`/`refresh()` called directly): no cache, no request merging, no loading/error state, no abort. Freshness of data comes from the SWR key, not from an effect.",
|
|
233
|
+
fix: "Read with an SWR hook of `hooks/swr` over an operation of `modules/api`, keyed by what changes; call `mutate()` from the event that changes the data (handler, socket message).",
|
|
234
|
+
},
|
|
235
|
+
"no-empty-catch": {
|
|
236
|
+
code: "FE_SWALLOWED_ERROR",
|
|
237
|
+
en: "A `catch` or `.catch` in `<file>` does nothing: the error vanishes, no notice, no trace.",
|
|
238
|
+
fix: "Return a typed outcome carrying the cause, show an error state, or rethrow.",
|
|
239
|
+
},
|
|
240
|
+
"no-console": {
|
|
241
|
+
code: "FE_CONSOLE_CALL",
|
|
242
|
+
en: "`console.<method>` in `<file>`: it reaches no log pipeline and leaks internal detail to the user.",
|
|
243
|
+
fix: "Return a typed outcome, show an error state, or let the error reach `error.tsx`.",
|
|
244
|
+
},
|
|
245
|
+
"page-exports-metadata": {
|
|
246
|
+
code: "FE_PAGE_METADATA_MISSING",
|
|
247
|
+
en: "`<file>` is `page.tsx` but exports no `metadata` or `generateMetadata`; every page in the branch carries the same title.",
|
|
248
|
+
fix: "Export `metadata` (or `generateMetadata` when the title comes from data) with a title and description taken from the message catalog.",
|
|
249
|
+
},
|
|
250
|
+
"no-null-suspense-fallback": {
|
|
251
|
+
code: "FE_SUSPENSE_NULL_FALLBACK",
|
|
252
|
+
en: "`<Suspense>` in `<file>` has an empty `fallback`: the user sees a blank instead of a loading state.",
|
|
253
|
+
fix: "Pass the skeleton (loading state) of the very thing loading as the `fallback`.",
|
|
254
|
+
},
|
|
255
|
+
"navigation-from-intl": {
|
|
256
|
+
code: "FE_I18N_NAVIGATION",
|
|
257
|
+
en: "`<file>` imports `Link`, `useRouter`, `usePathname` or `redirect` from Next, which does not know the locale: the path loses the `[locale]` prefix.",
|
|
258
|
+
fix: "Import from the locale-aware public routes entry or shared i18n package, built from the one routing configuration.",
|
|
259
|
+
},
|
|
260
|
+
"no-native-anchor": {
|
|
261
|
+
code: "FE_I18N_NAVIGATION",
|
|
262
|
+
en: "`<a>` in `<file>` points at an internal route (reloads the whole page, loses the locale, loses prefetch) or opens a new tab without `rel`.",
|
|
263
|
+
fix: "Use the locale-aware Link with an href built by modules/routes; a _blank link adds rel=\"noopener noreferrer\".",
|
|
264
|
+
},
|
|
265
|
+
"use-intl-formatter": {
|
|
266
|
+
code: "FE_I18N_FORMATTER",
|
|
267
|
+
en: "`<file>` formats a number, money or a date with `toLocale*String`, `new Intl.*`, `toFixed`, a currency sign pasted into a template or a date library, instead of the next-intl formatter.",
|
|
268
|
+
fix: "Use `useFormatter()` (or `getFormatter()` on the server): `number(...)`, `dateTime(...)`, `relativeTime(...)`.",
|
|
269
|
+
},
|
|
270
|
+
"no-hardcoded-route": {
|
|
271
|
+
code: "FE_ROUTE_HARDCODED",
|
|
272
|
+
en: "`<file>` writes a route path inline at the call site; when the route moves, the copy points at a 404.",
|
|
273
|
+
fix: "Build the href with a function of `modules/routes` and use that same function everywhere.",
|
|
274
|
+
},
|
|
275
|
+
"client-no-server-import": {
|
|
276
|
+
code: "FE_CLIENT_SERVER_IMPORT",
|
|
277
|
+
en: "A client component in `<file>` imports code that exists only on the server (`server-only`, `next/headers`, a Node module, a server-only module).",
|
|
278
|
+
fix: "Read the data through an SWR hook of `hooks/swr` over an operation of `modules/api`; keep request-scoped server APIs in server components.",
|
|
279
|
+
},
|
|
280
|
+
"server-module-marks-server-only": {
|
|
281
|
+
code: "FE_SERVER_ONLY_MARK",
|
|
282
|
+
en: "Module `<file>` imports a server-only API (`next/headers`, `next/server`, `next-intl/server`, a Node module or an already server-only module) but does not open with `import \"server-only\"`.",
|
|
283
|
+
fix: "Make `import \"server-only\"` the file's first statement; route files (`page`, `layout`, `route`, `proxy`) are server components already and need none.",
|
|
284
|
+
},
|
|
285
|
+
"no-server-action": {
|
|
286
|
+
code: "FE_SERVER_ACTION_FORBIDDEN",
|
|
287
|
+
en: "`<file>` carries a `\"use server\"` directive: a Server Action, an endpoint the front end serves. The front end serves no API.",
|
|
288
|
+
fix: "Write it as a GraphQL mutation of the back end and call it through its operation in `modules/api` and its SWR hook in `hooks/swr`.",
|
|
289
|
+
},
|
|
290
|
+
"web-storage-only-in-modules": {
|
|
291
|
+
code: "FE_STORAGE_OUTSIDE_MODULES",
|
|
292
|
+
en: "`<file>` uses `localStorage`/`sessionStorage` outside `modules/`; storage does not exist on the server and throws when over quota.",
|
|
293
|
+
fix: "Put the read/write behind a hook or a module in `modules/` with a guard (`typeof window`, try/catch) and call it from the block.",
|
|
294
|
+
},
|
|
295
|
+
"no-dangerous-html": {
|
|
296
|
+
code: "FE_DANGEROUS_HTML",
|
|
297
|
+
en: "`dangerouslySetInnerHTML` on `<tag>` in `<file>`: any string becomes runnable HTML.",
|
|
298
|
+
fix: "Render content as elements; only a `<script>` carrying JSON-LD or theme code from an app constant may use it.",
|
|
299
|
+
},
|
|
300
|
+
"response-cookie-attributes": {
|
|
301
|
+
code: "FE_COOKIE_ATTRIBUTES",
|
|
302
|
+
en: "`<file>` writes a cookie through the Next response cookie without stating fixed `httpOnly`, missing `secure` or a `sameSite` that is not `lax`/`strict`.",
|
|
303
|
+
fix: "Pass one `as const` options constant of the module owning the cookie: `httpOnly: true` (`false` only for a cookie an optional script must read), `secure` from `modules/config`, `sameSite: \"lax\"` or `\"strict\"`.",
|
|
304
|
+
},
|
|
305
|
+
"props-fields-readonly": {
|
|
306
|
+
code: "FE_PROPS_MUTABLE",
|
|
307
|
+
en: "A props type of a component in `<file>` has a field or collection that is not `readonly`, so the component could write into what it is passed.",
|
|
308
|
+
fix: "Mark every field and index signature `readonly`, and write collections as `readonly T[]`, `readonly [A, B]` or `ReadonlyArray<T>`.",
|
|
309
|
+
},
|
|
310
|
+
"no-native-img": {
|
|
311
|
+
code: "FE_NATIVE_IMAGE",
|
|
312
|
+
en: "Raw `<img>` in `<file>`: it loads the original image and holds no space, so the page jumps when the image arrives.",
|
|
313
|
+
fix: "Use `Image` of `next/image` with `width` and `height` (or `fill` and `sizes`) and an `alt` from the catalog.",
|
|
314
|
+
},
|
|
315
|
+
"image-has-size": {
|
|
316
|
+
code: "FE_NATIVE_IMAGE",
|
|
317
|
+
en: "`Image` in `<file>` lacks `width` and `height` (or has `fill` without `sizes`): the browser cannot hold the space.",
|
|
318
|
+
fix: "Give both `width` and `height`, or `fill` inside a sized frame with `sizes`.",
|
|
319
|
+
},
|
|
320
|
+
"outcome-kinds-exhaustive": {
|
|
321
|
+
code: "FE_OUTCOME_KIND_UNHANDLED",
|
|
322
|
+
en: "A `switch` on `kind` in `<file>` has an `ok` branch but misses one of refused, invalid, not-found, unavailable.",
|
|
323
|
+
fix: "Write all five branches of `Outcome<T>`, one screen each; do not lean on `default`.",
|
|
324
|
+
},
|
|
325
|
+
"one-outcome-union": {
|
|
326
|
+
code: "FE_HTTP_STATUS_COLLAPSE",
|
|
327
|
+
en: "`<file>` declares another result union (`success`, `ok` or `kind`) outside `modules/api/graphql/types.ts`. A repo has only one result vocabulary, `GraphQLResponse<T>`.",
|
|
328
|
+
fix: "Use `GraphQLResponse<T>` of `modules/api/graphql/types.ts` (or the shared api package); put the business detail in its `data` instead of declaring a new union.",
|
|
329
|
+
},
|
|
330
|
+
"i18n-stack-in-one-module": {
|
|
331
|
+
code: "FE_I18N_PLACEMENT",
|
|
332
|
+
en: "`<file>` builds a layer of the next-intl stack itself (`defineRouting`, `createNavigation`, `getRequestConfig`, `createMiddleware`). The stack is written once for the whole repo.",
|
|
333
|
+
fix: "Use the shared i18n factory; a single app keeps routing/request config in modules/i18n and navigation in modules/routes. The proxy imports routing without navigation.",
|
|
334
|
+
},
|
|
335
|
+
"no-app-relative-clock": {
|
|
336
|
+
code: "FE_SIZE_AND_STATE_BUDGET",
|
|
337
|
+
en: "`<file>` starts a recurring timer that reads the wall clock for live time labels.",
|
|
338
|
+
fix: "Render Grammar RelativeTime with the target dateTime, locale and mode; its shared ticker owns live relative time and countdown updates.",
|
|
339
|
+
},
|
|
340
|
+
"fe-stack-import": {
|
|
341
|
+
code: "FE_STACK_FORBIDDEN",
|
|
342
|
+
en: "`<file>` imports `<what>`, a package or scoped package family the front-end stack forbids, or a Supabase package outside `modules/auth`.",
|
|
343
|
+
fix: "Use the concern's canonical library from `knowledge/hfs/fe-stack.yaml` (the message names it). For @dicebear/*, pass the identity id as seed on Grammar Avatar, AvatarButton or AvatarGroup members. Supabase on the front end is the auth client in `modules/auth` only, data goes through `modules/api`.",
|
|
344
|
+
},
|
|
345
|
+
"file-size-growth": {
|
|
346
|
+
code: "HFS_SIZE_GROWTH",
|
|
347
|
+
en: "`<file>` exceeds the line budget: a new file already too long, or an existing file longer than at the parent commit. Every file must be small enough for one person to read through and one test to cover.",
|
|
348
|
+
fix: "Split the new part into its own file by responsibility; a file already over budget may only stay or shrink.",
|
|
349
|
+
},
|
|
350
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rule and flat-config fence that hold `lint-escape-hatch.md` (HFS R18, `HFS_INLINE_SUPPRESSION`).
|
|
3
|
+
*
|
|
4
|
+
* NO LOCAL EXCEPTION, OF ANY SPELLING. A rule that a comment can switch off is a convention, and a
|
|
5
|
+
* codebase learns which conventions are real by which ones it can suppress. The spellings this law
|
|
6
|
+
* refuses are the ones an author actually reaches for, and the three patterns below own them:
|
|
7
|
+
* ESLINT_DIRECTIVE (an ESLint switch or inline rule configuration), TS_DIRECTIVE (a TypeScript
|
|
8
|
+
* suppression) and TOOL_DIRECTIVE (Sonar's own switches, a line taken out of the coverage the gate
|
|
9
|
+
* reads, a region taken out of the formatter, a stylelint switch that reached a TS file).
|
|
10
|
+
*
|
|
11
|
+
* TWO LAYERS, BECAUSE ONE IS NOT ENOUGH. The flat-config fence (`noInlineConfig`) makes an ESLint
|
|
12
|
+
* directive INEFFECTIVE; this rule makes it a FINDING, so the attempt is named in the log instead of
|
|
13
|
+
* silently ignored. `reportUnusedDisableDirectives` closes the last gap: a directive that suppresses
|
|
14
|
+
* nothing is dead weight that reads as if it did.
|
|
15
|
+
*/
|
|
16
|
+
import { type Recommended } from "./lib/rule.js";
|
|
17
|
+
/** Inline lint configuration is repository policy, never a file-local choice. */
|
|
18
|
+
export declare const noInlineLintConfig: import("./lib/rule.js").Rule<"directive" | "typescript" | "tool", []>;
|
|
19
|
+
/** The rules this law contributes to the plugin. */
|
|
20
|
+
export declare const rules: {
|
|
21
|
+
"no-inline-lint-config": import("./lib/rule.js").Rule<"directive" | "typescript" | "tool", []>;
|
|
22
|
+
};
|
|
23
|
+
/** Every lint escape is a build error. */
|
|
24
|
+
export declare const recommended: Recommended;
|
|
@@ -13,12 +13,10 @@
|
|
|
13
13
|
* silently ignored. `reportUnusedDisableDirectives` closes the last gap: a directive that suppresses
|
|
14
14
|
* nothing is dead weight that reads as if it did.
|
|
15
15
|
*/
|
|
16
|
-
|
|
17
|
-
import { isProductSource } from "./lib/scope.
|
|
18
|
-
|
|
16
|
+
import { defineRule } from "./lib/rule.js";
|
|
17
|
+
import { isProductSource } from "./lib/scope.js";
|
|
19
18
|
/** Product source is governed; canon tests deliberately build forbidden directives. */
|
|
20
|
-
const isGoverned = (context) => isProductSource(context)
|
|
21
|
-
|
|
19
|
+
const isGoverned = (context) => isProductSource(context);
|
|
22
20
|
/**
|
|
23
21
|
* Any directive that changes ESLint's active rule set inside a source file.
|
|
24
22
|
*
|
|
@@ -29,54 +27,49 @@ const isGoverned = (context) => isProductSource(context)
|
|
|
29
27
|
* was to stop writing the explanation, which is the opposite of what this law wants. Under-catching
|
|
30
28
|
* is not the trade: a directive ESLint would obey always sits at the start.
|
|
31
29
|
*/
|
|
32
|
-
const ESLINT_DIRECTIVE = /^\s*eslint-(?:disable(?:-next-line|-line)?|enable|env)\b|^\s*eslint\s+[@\w/-]+\s
|
|
33
|
-
|
|
30
|
+
const ESLINT_DIRECTIVE = /^\s*eslint-(?:disable(?:-next-line|-line)?|enable|env)\b|^\s*eslint\s+[@\w/-]+\s*:/;
|
|
34
31
|
/** A TypeScript directive that turns the compiler's check off for the next line or the file. */
|
|
35
|
-
const TS_DIRECTIVE = /^\s*(?:\/\s*)?@ts-(?:ignore|expect-error|nocheck)\b
|
|
36
|
-
|
|
32
|
+
const TS_DIRECTIVE = /^\s*(?:\/\s*)?@ts-(?:ignore|expect-error|nocheck)\b/;
|
|
37
33
|
/**
|
|
38
34
|
* A quality-tool switch other than ESLint's: Sonar honours its no-sonar word anywhere in a comment (case-insensitively), so it is
|
|
39
35
|
* matched anywhere; the others are read by their tool from the start of the comment, so they are anchored there too.
|
|
40
36
|
*/
|
|
41
|
-
const TOOL_DIRECTIVE =
|
|
42
|
-
/\bNOSONAR\b|^\s*@sonar-ignore\b|^\s*(?:istanbul|c8|v8)\s+ignore\b|^\s*prettier-ignore(?:-start|-end)?\b|^\s*stylelint-(?:disable(?:-next-line|-line)?|enable)\b/i
|
|
43
|
-
|
|
37
|
+
const TOOL_DIRECTIVE = /\bNOSONAR\b|^\s*@sonar-ignore\b|^\s*(?:istanbul|c8|v8)\s+ignore\b|^\s*prettier-ignore(?:-start|-end)?\b|^\s*stylelint-(?:disable(?:-next-line|-line)?|enable)\b/i;
|
|
44
38
|
/** Inline lint configuration is repository policy, never a file-local choice. */
|
|
45
|
-
export const noInlineLintConfig = {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
tool:
|
|
56
|
-
"A Sonar, coverage, formatter or stylelint suppression (`NOSONAR`, `@sonar-ignore`, `istanbul ignore`, `c8 ignore`, `prettier-ignore`, `stylelint-disable`). It removes this code from a gate that is failing it for a reason, and the next reader cannot tell a measured exception from an avoided fix. Fix the code, or change the shared rule or the coverage threshold; there is no local exception path.",
|
|
39
|
+
export const noInlineLintConfig = defineRule({
|
|
40
|
+
meta: {
|
|
41
|
+
type: "problem",
|
|
42
|
+
docs: { description: "Source cannot change its own lint or type-check policy." },
|
|
43
|
+
schema: [],
|
|
44
|
+
messages: {
|
|
45
|
+
directive: "Inline ESLint configuration makes this file the author of whether repository law applies. Remove the directive and fix the code or the shared rule; there is no local exception path.",
|
|
46
|
+
typescript: "A TypeScript suppression directive. It turns the compiler off for code that is failing the check for a reason. Fix the type, or change the shared contract that produced it; there is no local exception path.",
|
|
47
|
+
tool: "A Sonar, coverage, formatter or stylelint suppression (`NOSONAR`, `@sonar-ignore`, `istanbul ignore`, `c8 ignore`, `prettier-ignore`, `stylelint-disable`). It removes this code from a gate that is failing it for a reason, and the next reader cannot tell a measured exception from an avoided fix. Fix the code, or change the shared rule or the coverage threshold; there is no local exception path.",
|
|
48
|
+
},
|
|
57
49
|
},
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
50
|
+
create(context) {
|
|
51
|
+
if (!isGoverned(context))
|
|
52
|
+
return {};
|
|
53
|
+
const source = context.sourceCode || context.getSourceCode();
|
|
54
|
+
return {
|
|
55
|
+
Program() {
|
|
56
|
+
for (const comment of source.getAllComments()) {
|
|
57
|
+
if (ESLINT_DIRECTIVE.test(comment.value))
|
|
58
|
+
context.report({ node: comment, messageId: "directive" });
|
|
59
|
+
else if (TS_DIRECTIVE.test(comment.value))
|
|
60
|
+
context.report({ node: comment, messageId: "typescript" });
|
|
61
|
+
else if (TOOL_DIRECTIVE.test(comment.value))
|
|
62
|
+
context.report({ node: comment, messageId: "tool" });
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
});
|
|
74
68
|
/** The rules this law contributes to the plugin. */
|
|
75
69
|
export const rules = {
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
70
|
+
"no-inline-lint-config": noInlineLintConfig,
|
|
71
|
+
};
|
|
79
72
|
/** Every lint escape is a build error. */
|
|
80
73
|
export const recommended = {
|
|
81
|
-
|
|
82
|
-
}
|
|
74
|
+
"starci-fe/no-inline-lint-config": "error",
|
|
75
|
+
};
|
package/dist/lists.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rules that hold `lists.md` (HFS R65 `FE_SIZE_AND_STATE_BUDGET`, sub-check `FE_LIST_KEY`).
|
|
3
|
+
*
|
|
4
|
+
* A LIST IS ITEMS WITH IDENTITY. React reconciles a rendered list by `key`: the key is how it knows
|
|
5
|
+
* that the third row is still the third row after the first was deleted. With no key React falls
|
|
6
|
+
* back to position, with an index key it says position is identity, and either way a deleted row
|
|
7
|
+
* hands its state (an open menu, a half-typed input, a focus ring) to its neighbour. Nothing fails a
|
|
8
|
+
* type check and the bug appears only on the second interaction, on a real list.
|
|
9
|
+
*
|
|
10
|
+
* WHAT COUNTS AS A LIST. A `.map` callback that returns JSX. A static array written in place
|
|
11
|
+
* (`[0, 1, 2].map(...)`) or a callback that ignores its item (`(_, index) => ...`, a skeleton) has no identity to lose, so an index key there is fine.
|
|
12
|
+
*
|
|
13
|
+
* THE MEMO TAX. A component prop written as `{{ ... }}` or `[...]` inside a `.map` callback is a new
|
|
14
|
+
* object per row per render, so `memo` on the row never hits and the whole list re-renders on every
|
|
15
|
+
* keystroke of its parent. Hoist the constant, or build it once above the map.
|
|
16
|
+
*/
|
|
17
|
+
import { type Recommended } from "./lib/rule.js";
|
|
18
|
+
/** Every element a `.map` callback returns carries a `key`. */
|
|
19
|
+
export declare const listItemHasKey: import("./lib/rule.js").Rule<"missing" | "fragment", []>;
|
|
20
|
+
/** The key is never the position, and never made up on every render. */
|
|
21
|
+
export declare const noIndexKey: import("./lib/rule.js").Rule<"index" | "random", []>;
|
|
22
|
+
/** A component in a mapped list gets no object or array literal for a prop. */
|
|
23
|
+
export declare const noInlineLiteralPropInList: import("./lib/rule.js").Rule<"literal", []>;
|
|
24
|
+
/** The rules this law contributes to the plugin. */
|
|
25
|
+
export declare const rules: {
|
|
26
|
+
"list-item-has-key": import("./lib/rule.js").Rule<"missing" | "fragment", []>;
|
|
27
|
+
"no-index-key": import("./lib/rule.js").Rule<"index" | "random", []>;
|
|
28
|
+
"no-inline-literal-prop-in-list": import("./lib/rule.js").Rule<"literal", []>;
|
|
29
|
+
};
|
|
30
|
+
/** Every rule is an error. */
|
|
31
|
+
export declare const recommended: Recommended;
|