@starci/eslint-canon-fe 8.0.8 → 8.0.9
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 +1 -0
- package/README.md +5 -4
- package/client-boundary.mjs +4 -2
- package/comments.mjs +1 -1
- package/docs/next-conventions.md +1 -1
- package/docs/translation.md +1 -1
- package/docs/transport.md +2 -2
- package/{shape-slot.mjs → drawing.mjs} +5 -5
- package/file-layout.mjs +8 -6
- package/icon.mjs +1 -23
- package/index.mjs +2 -2
- package/lib/ast.mjs +14 -11
- package/lib/config.mjs +1 -1
- package/lib/scope.mjs +6 -2
- package/lib/why.mjs +113 -110
- package/naming.mjs +4 -4
- package/package.json +3 -1
- package/project-graph.mjs +1 -1
- package/runtime/engine/config.mjs +494 -0
- package/runtime/engine/invalid-config.mjs +15 -0
- package/runtime/engine/orca-config.mjs +27 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +160 -29
- package/runtime/knowledge/hfs/slots.yaml +912 -120
- package/runtime/knowledge/patterns/fe/folder.yaml +131 -25
- package/runtime/knowledge/sonar-gate.yaml +52 -18
- package/runtime/modules/models/registry.yaml +591 -0
- package/runtime/modules/models/runtimes.yaml +701 -0
- package/runtime/packages/hfs/templates/app/package-scripts/package.json +3 -10
- package/runtime/scripts/api/git/config-get.mjs +10 -0
- package/runtime/scripts/api/git/lib.mjs +16 -26
- package/runtime/scripts/api/git/ls-files.mjs +6 -0
- package/runtime/scripts/api/git/remote.mjs +5 -0
- package/runtime/scripts/api/git/rev-parse-query.mjs +5 -0
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +220 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +16 -23
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +16 -34
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +3 -1
- package/runtime/scripts/hfs/architecture/clones.mjs +4 -4
- package/runtime/scripts/hfs/architecture/config.mjs +9 -6
- package/runtime/scripts/hfs/architecture/connection-map.mjs +20 -8
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +88 -0
- package/runtime/scripts/hfs/architecture/context-map.mjs +237 -0
- package/runtime/scripts/hfs/architecture/context-owner.mjs +82 -0
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +47 -0
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +60 -0
- package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +4 -10
- package/runtime/scripts/hfs/architecture/contracts.mjs +33 -162
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +58 -29
- package/runtime/scripts/hfs/architecture/doc-language.mjs +1 -1
- package/runtime/scripts/hfs/architecture/entrypoint.mjs +2 -1
- package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +1 -1
- package/runtime/scripts/hfs/architecture/feature-shape.mjs +5 -2
- package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
- package/runtime/scripts/hfs/architecture/frontend.mjs +17 -43
- package/runtime/scripts/hfs/architecture/hfs.mjs +24 -39
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +1 -1
- package/runtime/scripts/hfs/architecture/index.mjs +17 -4
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +44 -11
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +27 -0
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +30 -6
- package/runtime/scripts/hfs/architecture/next-data.mjs +22 -63
- package/runtime/scripts/hfs/architecture/owners.mjs +35 -15
- package/runtime/scripts/hfs/architecture/register-once.mjs +2 -1
- package/runtime/scripts/hfs/architecture/registration.mjs +23 -81
- package/runtime/scripts/hfs/architecture/required-files.mjs +20 -12
- package/runtime/scripts/hfs/architecture/schema-owner.mjs +17 -114
- package/runtime/scripts/hfs/architecture/server-action.mjs +24 -0
- package/runtime/scripts/hfs/architecture/side-boundary.mjs +16 -0
- package/runtime/scripts/hfs/architecture/source-names.mjs +27 -147
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +59 -18
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +64 -0
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +111 -0
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +131 -0
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +42 -0
- package/runtime/scripts/hfs/architecture/supabase.mjs +434 -0
- package/runtime/scripts/hfs/architecture/surface.mjs +9 -3
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +9 -19
- package/runtime/scripts/hfs/architecture/tiers.mjs +47 -5
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +3 -3
- package/runtime/scripts/hfs/architecture/typescript.mjs +12 -26
- package/runtime/scripts/hfs/declaration-shape.mjs +62 -0
- package/runtime/scripts/hfs/declaration-slots.mjs +75 -0
- package/runtime/scripts/hfs/edition-slots.mjs +66 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +109 -32
- package/runtime/scripts/hfs/param-names.mjs +18 -0
- package/runtime/scripts/hfs/path-findings.mjs +1 -1
- package/runtime/scripts/hfs/project-graph.mjs +3 -3
- package/runtime/scripts/hfs/repo-identity.mjs +9 -6
- package/runtime/scripts/hfs/rule-params-shape.mjs +50 -0
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +6 -5
- package/runtime/scripts/hfs/rules/read.mjs +1 -1
- package/runtime/scripts/hfs/rules/secrets.mjs +3 -3
- package/runtime/scripts/hfs/rules/spec-placement.mjs +1 -1
- package/runtime/scripts/hfs/slots.mjs +78 -108
- package/runtime/scripts/hfs/test-world-slot.mjs +6 -0
- package/runtime/scripts/hfs/typescript-programs.mjs +0 -5
- package/runtime/scripts/hfs/view.mjs +3 -2
- package/runtime/scripts/lib/dockerfile.mjs +124 -0
- package/runtime/scripts/lib/git.mjs +36 -0
- package/runtime/scripts/lib/i18n.mjs +86 -0
- package/runtime/scripts/lib/json.mjs +13 -3
- package/runtime/scripts/lib/language.mjs +24 -5
- package/runtime/scripts/lib/list.mjs +20 -0
- package/runtime/scripts/lib/path-key.mjs +57 -2
- package/runtime/scripts/lib/read-text.mjs +34 -0
- package/runtime/scripts/lib/regex.mjs +4 -0
- package/runtime/scripts/lib/secret-patterns.mjs +4 -3
- package/runtime/scripts/lib/sops-envelope.mjs +58 -0
- package/runtime/scripts/lib/stack-services.mjs +7 -31
- package/runtime/scripts/lib/ts-ast.mjs +35 -0
- package/runtime/scripts/lib/walk.mjs +4 -3
- package/size-and-state-budget.mjs +1 -1
- package/tokens.mjs +10 -18
- package/translation.mjs +1 -1
- package/transport.mjs +2 -2
- package/runtime/scripts/api/sops/decrypt.mjs +0 -16
- package/runtime/scripts/lib/test-secrets.mjs +0 -116
package/lib/why.mjs
CHANGED
|
@@ -1,279 +1,282 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The "why" of each rule the HFS catalogue assigns to this canon.
|
|
3
3
|
*
|
|
4
4
|
* WHERE IT IS SURFACED. An ESLint message is written for the developer at the terminal, in English,
|
|
5
5
|
* and says what is wrong in the vocabulary of the rule. The agent that reads a failed land gate needs
|
|
6
6
|
* the catalogue's sentence instead: the finding code (the key in `modules/kernel/failure-codes.yaml`)
|
|
7
|
-
* and a
|
|
8
|
-
*
|
|
9
|
-
* the reader fills from the ESLint location, `
|
|
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.
|
|
10
13
|
*
|
|
11
|
-
* Every rule that carries a catalogue id (R18, R22, R49-R59, R60-R62, R65
|
|
14
|
+
* Every rule that carries a catalogue id (R18, R22, R49-R59, R60-R62, R65) has an entry;
|
|
12
15
|
* the twin test refuses a rule of those laws with no entry and an entry for a rule that does not exist.
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
|
-
/** @typedef {{ code: string,
|
|
18
|
+
/** @typedef {{ code: string, en: string, fix: string }} Why */
|
|
16
19
|
|
|
17
20
|
/** @type {Record<string, Why>} */
|
|
18
21
|
export const why = {
|
|
19
22
|
"no-env-outside-config": {
|
|
20
23
|
code: "FE_ENV_OWNER",
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
en: "`<file>` reads an environment variable. Only `modules/config` may read env.",
|
|
25
|
+
fix: "Read the value from a typed export of `modules/config` instead of `process.env`.",
|
|
23
26
|
},
|
|
24
27
|
"no-hardcoded-endpoint-fallback": {
|
|
25
28
|
code: "FE_ENV_OWNER",
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
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.",
|
|
30
|
+
fix: "Delete the fallback; let `modules/config` throw when a variable is missing in production.",
|
|
28
31
|
},
|
|
29
32
|
"fetch-only-in-api-client": {
|
|
30
33
|
code: "FE_TRANSPORT_OWNER",
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
en: "`fetch` (or `Request`, `EventSource`, `sendBeacon`, another HTTP library) in `<file>` sits outside the one client (`modules/api/client.ts`, or `src/client.ts` of the shared api package). A repo has exactly one transport.",
|
|
35
|
+
fix: "Call the repo's client and receive `Outcome<T>`; do not call `fetch` yourself.",
|
|
33
36
|
},
|
|
34
37
|
"client-fetch-has-signal": {
|
|
35
38
|
code: "FE_TRANSPORT_OWNER",
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
en: "`fetch` in `<file>` has no `signal`. The client must carry a timeout and an `AbortSignal`.",
|
|
40
|
+
fix: "Pass `signal` (`AbortSignal.timeout(...)` combined with the caller's signal).",
|
|
38
41
|
},
|
|
39
42
|
"no-shared-transport-state": {
|
|
40
43
|
code: "FE_TRANSPORT_OWNER",
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
en: "`<file>` holds shared state as `let`/`var` in the API layer. Token and locale must not sit in a singleton.",
|
|
45
|
+
fix: "Pass credential and locale as parameters or through context.",
|
|
43
46
|
},
|
|
44
47
|
"client-maps-auth-to-refused": {
|
|
45
48
|
code: "FE_HTTP_STATUS_COLLAPSE",
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
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.",
|
|
50
|
+
fix: "Add a `response.status === 401 || response.status === 403` branch returning `{ ok: false, kind: \"refused\" }` right inside the client.",
|
|
48
51
|
},
|
|
49
52
|
"no-failure-collapse": {
|
|
50
53
|
code: "FE_HTTP_STATUS_COLLAPSE",
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
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.",
|
|
55
|
+
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.",
|
|
53
56
|
},
|
|
54
57
|
"no-hand-typed-wire": {
|
|
55
58
|
code: "FE_WIRE_GENERATED",
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
en: "`<file>` hand-types a wire type or casts a response. Use the types generated from `be/contracts/`.",
|
|
60
|
+
fix: "Run codegen from the be contract snapshot (`be/contracts/`) and import the generated types; put GraphQL documents in `.graphql` files.",
|
|
58
61
|
},
|
|
59
62
|
"use-client-only-at-boundary": {
|
|
60
63
|
code: "FE_CLIENT_BOUNDARY",
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
en: "`\"use client\"` in `<file>` (`<slot>` may not be a client component).",
|
|
65
|
+
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.",
|
|
63
66
|
},
|
|
64
67
|
"hooks-folder-holds-hooks-only": {
|
|
65
68
|
code: "FE_HOOKS_ARE_HOOKS",
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
en: "`<file>` inside `hooks/` is not a React hook. A server reader goes to `modules/api`, a helper to `<domain>.shared.ts`.",
|
|
70
|
+
fix: "One hook per `use*.ts` file; a shared helper goes in `<domain>.shared.ts`; a server reader goes in `modules/api/<domain>/read-*.ts`.",
|
|
68
71
|
},
|
|
69
72
|
"no-middleware-file": {
|
|
70
73
|
code: "FE_NEXT_CONVENTIONS",
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
en: "`<file>` uses the name `middleware`. From Next 16 the request gate is `proxy.ts` exporting `proxy`.",
|
|
75
|
+
fix: "Rename the file to `proxy.ts` and rename the export to `proxy`.",
|
|
73
76
|
},
|
|
74
77
|
"locale-segment-is-locale": {
|
|
75
78
|
code: "FE_I18N_PLACEMENT",
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
en: "File `<file>` sits under a locale segment that is not `[locale]`. There is one pattern: `next-intl`, `[locale]`, default `vi`.",
|
|
80
|
+
fix: "Rename the segment directory to `[locale]`.",
|
|
78
81
|
},
|
|
79
82
|
"no-second-i18n-stack": {
|
|
80
83
|
code: "FE_I18N_PLACEMENT",
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
en: "`<file>` imports a second i18n library. The app uses only `next-intl` through `modules/i18n`.",
|
|
85
|
+
fix: "Switch to `next-intl`; delete the other library.",
|
|
83
86
|
},
|
|
84
87
|
"html-lang-from-locale": {
|
|
85
88
|
code: "FE_I18N_LITERAL",
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
en: "`<html lang>` in `<file>` is a hardcoded string. The `lang` attribute must come from the `[locale]` segment.",
|
|
90
|
+
fix: "Use `lang={locale}` from the parameter of the `[locale]` layout.",
|
|
88
91
|
},
|
|
89
92
|
"no-hardcoded-copy": {
|
|
90
93
|
code: "FE_I18N_LITERAL",
|
|
91
|
-
|
|
92
|
-
|
|
94
|
+
en: "The text `\"<text>\"` in `<file>:<line>` does not go through `t()`.",
|
|
95
|
+
fix: "Move the sentence into `modules/i18n/messages/<locale>.json` and read it with `t(\"key\")`; no exceptions and no `vn-ok`.",
|
|
93
96
|
},
|
|
94
97
|
"no-raw-brand-value": {
|
|
95
98
|
code: "FE_STYLE_TOKEN_ONLY",
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
en: "`<file>` uses a raw colour/length value (`<what>`). Colour and spacing come only from grammar tokens; brand colour lives only in `brand.css`.",
|
|
100
|
+
fix: "Use a grammar token class or variable; a colour value is declared only in `modules/brand/brand.css`.",
|
|
98
101
|
},
|
|
99
102
|
"status-text-uses-soft-foreground": {
|
|
100
103
|
code: "FE_STYLE_TOKEN_ONLY",
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
en: "`<file>` paints text or an icon with a solid status tone (`<what>`, e.g. `text-success`). A solid tone is a background colour; used as a text colour it reads below 4.5:1 on the page background.",
|
|
105
|
+
fix: "Switch to the soft pair: `text-<tone>-soft-foreground` (with `bg-<tone>-soft` when it sits on a light surface); a solid tone is used only with `bg-<tone>` and `text-<tone>-foreground`.",
|
|
103
106
|
},
|
|
104
107
|
"no-native-form-control": {
|
|
105
108
|
code: "FE_NATIVE_FORM_CONTROL",
|
|
106
|
-
|
|
107
|
-
|
|
109
|
+
en: "Raw `<tag>` in `<file>`. Use the grammar renderer.",
|
|
110
|
+
fix: "Replace it with the grammar component; if none exists yet, add it to grammar instead of drawing it in place.",
|
|
108
111
|
},
|
|
109
112
|
"component-line-budget": {
|
|
110
113
|
code: "FE_SIZE_AND_STATE_BUDGET",
|
|
111
|
-
|
|
112
|
-
|
|
114
|
+
en: "`<file>` is `<n>` lines long, over the 300-line budget of a component.",
|
|
115
|
+
fix: "Split the drawing part from the data part and each area into its own unit.",
|
|
113
116
|
},
|
|
114
117
|
"unit-hook-budget": {
|
|
115
118
|
code: "FE_SIZE_AND_STATE_BUDGET",
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
en: "`<unit>` has `<n>` data hooks / `<m>` useState, over the budget of 6.",
|
|
120
|
+
fix: "Fold state that changes together into a reducer or a dedicated hook; split each data area into its own connection block.",
|
|
118
121
|
},
|
|
119
122
|
"no-hand-rolled-polling": {
|
|
120
123
|
code: "FE_SIZE_AND_STATE_BUDGET",
|
|
121
|
-
|
|
122
|
-
|
|
124
|
+
en: "`<unit>` hand-writes a poll loop (`setInterval` or a self-rescheduling `setTimeout`).",
|
|
125
|
+
fix: "Refresh through a data hook (`refreshInterval`) or a socket, one mechanism per resource.",
|
|
123
126
|
},
|
|
124
127
|
"no-inline-lint-config": {
|
|
125
128
|
code: "HFS_INLINE_SUPPRESSION",
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
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.",
|
|
130
|
+
fix: "Remove `eslint-disable`, `@ts-ignore`, `@ts-expect-error` or `vn-ok` and fix the cause.",
|
|
128
131
|
},
|
|
129
132
|
"no-double-cast": {
|
|
130
133
|
code: "FE_TYPE_ESCAPE",
|
|
131
|
-
|
|
132
|
-
|
|
134
|
+
en: "`<file>` casts through `unknown` (`as unknown as T`): the compiler forgets everything it knows right where the data enters.",
|
|
135
|
+
fix: "Narrow from `unknown` with a type guard or a parser; do not cast.",
|
|
133
136
|
},
|
|
134
137
|
"no-type-assertion": {
|
|
135
138
|
code: "FE_TYPE_ESCAPE",
|
|
136
|
-
|
|
137
|
-
|
|
139
|
+
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.",
|
|
140
|
+
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.",
|
|
138
141
|
},
|
|
139
142
|
"no-non-null-assertion": {
|
|
140
143
|
code: "FE_TYPE_ESCAPE",
|
|
141
|
-
|
|
142
|
-
|
|
144
|
+
en: "`<file>` uses `x!`: asserting a value is present without proving it.",
|
|
145
|
+
fix: "Handle the absent branch (`if`, `??`, an early return) or fix the type so the value cannot be absent.",
|
|
143
146
|
},
|
|
144
147
|
"no-explicit-any": {
|
|
145
148
|
code: "FE_TYPE_ESCAPE",
|
|
146
|
-
|
|
147
|
-
|
|
149
|
+
en: "`<file>` uses `any`: it switches off type checking for that value and for everything inferred from it.",
|
|
150
|
+
fix: "Use a real type, a generic, or `unknown` narrowed at the use site.",
|
|
148
151
|
},
|
|
149
152
|
"list-item-has-key": {
|
|
150
153
|
code: "FE_LIST_KEY",
|
|
151
|
-
|
|
152
|
-
|
|
154
|
+
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.",
|
|
155
|
+
fix: "Add a `key` taken from the data's id; for a fragment use `<Fragment key={...}>`.",
|
|
153
156
|
},
|
|
154
157
|
"no-index-key": {
|
|
155
158
|
code: "FE_LIST_KEY",
|
|
156
|
-
|
|
157
|
-
|
|
159
|
+
en: "A `key` in `<file>` comes from the `.map` index or a randomly generated value; an index is a position, not an identity.",
|
|
160
|
+
fix: "Use the id the data carries (`key={row.id}`); never the index, `Math.random`, `Date.now`.",
|
|
158
161
|
},
|
|
159
162
|
"no-inline-literal-prop-in-list": {
|
|
160
163
|
code: "FE_LIST_KEY",
|
|
161
|
-
|
|
162
|
-
|
|
164
|
+
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.",
|
|
165
|
+
fix: "Hoist the constant outside the component or build it once before `.map`.",
|
|
163
166
|
},
|
|
164
167
|
"effect-subscription-needs-cleanup": {
|
|
165
168
|
code: "FE_EFFECT_CLEANUP",
|
|
166
|
-
|
|
167
|
-
|
|
169
|
+
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.",
|
|
170
|
+
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.",
|
|
168
171
|
},
|
|
169
172
|
"no-data-fetch-in-effect": {
|
|
170
173
|
code: "FE_EFFECT_FETCH",
|
|
171
|
-
|
|
172
|
-
|
|
174
|
+
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.",
|
|
175
|
+
fix: "Read on the client with SWR calling the app's client (`hooks/`) keyed by what changes; call `mutate()` from the event that changes the data (handler, socket message); on the route use the server reader `modules/api/<domain>/read-*.ts`.",
|
|
173
176
|
},
|
|
174
177
|
"no-empty-catch": {
|
|
175
178
|
code: "FE_SWALLOWED_ERROR",
|
|
176
|
-
|
|
177
|
-
|
|
179
|
+
en: "A `catch` or `.catch` in `<file>` does nothing: the error vanishes, no notice, no trace.",
|
|
180
|
+
fix: "Return a typed outcome carrying the cause, show an error state, or rethrow.",
|
|
178
181
|
},
|
|
179
182
|
"no-console": {
|
|
180
183
|
code: "FE_CONSOLE_CALL",
|
|
181
|
-
|
|
182
|
-
|
|
184
|
+
en: "`console.<method>` in `<file>`: it reaches no log pipeline and leaks internal detail to the user.",
|
|
185
|
+
fix: "Return a typed outcome, show an error state, or let the error reach `error.tsx`.",
|
|
183
186
|
},
|
|
184
187
|
"page-exports-metadata": {
|
|
185
188
|
code: "FE_PAGE_METADATA_MISSING",
|
|
186
|
-
|
|
187
|
-
|
|
189
|
+
en: "`<file>` is `page.tsx` but exports no `metadata` or `generateMetadata`; every page in the branch carries the same title.",
|
|
190
|
+
fix: "Export `metadata` (or `generateMetadata` when the title comes from data) with a title and description taken from the message catalog.",
|
|
188
191
|
},
|
|
189
192
|
"no-null-suspense-fallback": {
|
|
190
193
|
code: "FE_SUSPENSE_NULL_FALLBACK",
|
|
191
|
-
|
|
192
|
-
|
|
194
|
+
en: "`<Suspense>` in `<file>` has an empty `fallback`: the user sees a blank instead of a loading state.",
|
|
195
|
+
fix: "Pass the skeleton (loading state) of the very thing loading as the `fallback`.",
|
|
193
196
|
},
|
|
194
197
|
"navigation-from-intl": {
|
|
195
198
|
code: "FE_I18N_NAVIGATION",
|
|
196
|
-
|
|
197
|
-
|
|
199
|
+
en: "`<file>` imports `Link`, `useRouter`, `usePathname` or `redirect` from Next, which does not know the locale: the path loses the `[locale]` prefix.",
|
|
200
|
+
fix: "Import from `modules/i18n/navigation`, built by next-intl from `routing.ts`.",
|
|
198
201
|
},
|
|
199
202
|
"no-native-anchor": {
|
|
200
203
|
code: "FE_I18N_NAVIGATION",
|
|
201
|
-
|
|
202
|
-
|
|
204
|
+
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`.",
|
|
205
|
+
fix: "Use `Link` from `modules/i18n/navigation` with an href built by `modules/routes`; a `_blank` link adds `rel=\"noopener noreferrer\"`.",
|
|
203
206
|
},
|
|
204
207
|
"use-intl-formatter": {
|
|
205
208
|
code: "FE_I18N_FORMATTER",
|
|
206
|
-
|
|
207
|
-
|
|
209
|
+
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.",
|
|
210
|
+
fix: "Use `useFormatter()` (or `getFormatter()` on the server): `number(...)`, `dateTime(...)`, `relativeTime(...)`.",
|
|
208
211
|
},
|
|
209
212
|
"no-hardcoded-route": {
|
|
210
213
|
code: "FE_ROUTE_HARDCODED",
|
|
211
|
-
|
|
212
|
-
|
|
214
|
+
en: "`<file>` writes a route path inline at the call site; when the route moves, the copy points at a 404.",
|
|
215
|
+
fix: "Build the href with a function of `modules/routes` and use that same function everywhere.",
|
|
213
216
|
},
|
|
214
217
|
"client-no-server-import": {
|
|
215
218
|
code: "FE_CLIENT_SERVER_IMPORT",
|
|
216
|
-
|
|
217
|
-
|
|
219
|
+
en: "A client component in `<file>` imports code that exists only on the server (`server-only`, `next/headers`, a Node module, a server reader).",
|
|
220
|
+
fix: "Read the data in a server component or a server reader and pass it down as props; or use SWR calling the app's client.",
|
|
218
221
|
},
|
|
219
222
|
"server-module-marks-server-only": {
|
|
220
223
|
code: "FE_SERVER_ONLY_MARK",
|
|
221
|
-
|
|
222
|
-
|
|
224
|
+
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\"`.",
|
|
225
|
+
fix: "Make `import \"server-only\"` the file's first statement; route files (`page`, `layout`, `route`, `proxy`) are server components already and need none.",
|
|
223
226
|
},
|
|
224
227
|
"web-storage-only-in-modules": {
|
|
225
228
|
code: "FE_STORAGE_OUTSIDE_MODULES",
|
|
226
|
-
|
|
227
|
-
|
|
229
|
+
en: "`<file>` uses `localStorage`/`sessionStorage` outside `modules/`; storage does not exist on the server and throws when over quota.",
|
|
230
|
+
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.",
|
|
228
231
|
},
|
|
229
232
|
"no-dangerous-html": {
|
|
230
233
|
code: "FE_DANGEROUS_HTML",
|
|
231
|
-
|
|
232
|
-
|
|
234
|
+
en: "`dangerouslySetInnerHTML` on `<tag>` in `<file>`: any string becomes runnable HTML.",
|
|
235
|
+
fix: "Render content as elements; only a `<script>` carrying JSON-LD or theme code from an app constant may use it.",
|
|
233
236
|
},
|
|
234
237
|
"response-cookie-attributes": {
|
|
235
238
|
code: "FE_COOKIE_ATTRIBUTES",
|
|
236
|
-
|
|
237
|
-
|
|
239
|
+
en: "`<file>` writes a cookie through the Next response cookie without stating fixed `httpOnly`, missing `secure` or a `sameSite` that is not `lax`/`strict`.",
|
|
240
|
+
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\"`.",
|
|
238
241
|
},
|
|
239
242
|
"props-fields-readonly": {
|
|
240
243
|
code: "FE_PROPS_MUTABLE",
|
|
241
|
-
|
|
242
|
-
|
|
244
|
+
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.",
|
|
245
|
+
fix: "Mark every field and index signature `readonly`, and write collections as `readonly T[]`, `readonly [A, B]` or `ReadonlyArray<T>`.",
|
|
243
246
|
},
|
|
244
247
|
"no-native-img": {
|
|
245
248
|
code: "FE_NATIVE_IMAGE",
|
|
246
|
-
|
|
247
|
-
|
|
249
|
+
en: "Raw `<img>` in `<file>`: it loads the original image and holds no space, so the page jumps when the image arrives.",
|
|
250
|
+
fix: "Use `Image` of `next/image` with `width` and `height` (or `fill` and `sizes`) and an `alt` from the catalog.",
|
|
248
251
|
},
|
|
249
252
|
"image-has-size": {
|
|
250
253
|
code: "FE_NATIVE_IMAGE",
|
|
251
|
-
|
|
252
|
-
|
|
254
|
+
en: "`Image` in `<file>` lacks `width` and `height` (or has `fill` without `sizes`): the browser cannot hold the space.",
|
|
255
|
+
fix: "Give both `width` and `height`, or `fill` inside a sized frame with `sizes`.",
|
|
253
256
|
},
|
|
254
257
|
"outcome-kinds-exhaustive": {
|
|
255
258
|
code: "FE_OUTCOME_KIND_UNHANDLED",
|
|
256
|
-
|
|
257
|
-
|
|
259
|
+
en: "A `switch` on `kind` in `<file>` has an `ok` branch but misses one of refused, invalid, not-found, unavailable.",
|
|
260
|
+
fix: "Write all five branches of `Outcome<T>`, one screen each; do not lean on `default`.",
|
|
258
261
|
},
|
|
259
262
|
"one-outcome-union": {
|
|
260
263
|
code: "FE_HTTP_STATUS_COLLAPSE",
|
|
261
|
-
|
|
262
|
-
|
|
264
|
+
en: "`<file>` declares another result union (`ok` or `kind`) outside `outcome.ts`. A repo has only one `Outcome<T>`.",
|
|
265
|
+
fix: "Use `Outcome<T>` of `modules/api/outcome.ts` (or the shared api package); add business detail through the second parameter instead of declaring a new union.",
|
|
263
266
|
},
|
|
264
267
|
"i18n-stack-in-one-module": {
|
|
265
268
|
code: "FE_I18N_PLACEMENT",
|
|
266
|
-
|
|
267
|
-
|
|
269
|
+
en: "`<file>` builds a layer of the next-intl stack itself (`defineRouting`, `createNavigation`, `getRequestConfig`, `createMiddleware`). The stack is written once for the whole repo.",
|
|
270
|
+
fix: "Call the `createAppI18n` factory of the shared i18n package inside `modules/i18n/index.ts` and import the result; a single-app repo writes the stack only in that app's `modules/i18n`.",
|
|
268
271
|
},
|
|
269
272
|
"no-raw-structural-element": {
|
|
270
273
|
code: "FE_NATIVE_FORM_CONTROL",
|
|
271
|
-
|
|
272
|
-
|
|
274
|
+
en: "Raw `<tag>` in `<file>`. Page structure and text are composed from grammar components, not hand-written HTML.",
|
|
275
|
+
fix: "Replace it with the matching grammar component (`Heading`, `Text`, `SurfaceCard`, ...); if grammar lacks it, add it to grammar or the ui package instead of drawing it in place.",
|
|
273
276
|
},
|
|
274
277
|
"file-size-growth": {
|
|
275
278
|
code: "HFS_SIZE_GROWTH",
|
|
276
|
-
|
|
277
|
-
|
|
279
|
+
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.",
|
|
280
|
+
fix: "Split the new part into its own file by responsibility; a file already over budget may only stay or shrink.",
|
|
278
281
|
},
|
|
279
282
|
}
|
package/naming.mjs
CHANGED
|
@@ -111,13 +111,13 @@ export const noDirectConstAlias = {
|
|
|
111
111
|
// -- NAMING-3 --------------------------------------------------------------------------------------
|
|
112
112
|
|
|
113
113
|
/** Letters that exist in Vietnamese and not in English, in the forms a path can carry. */
|
|
114
|
-
const SECOND_LANGUAGE_PATH = /[
|
|
114
|
+
const SECOND_LANGUAGE_PATH = /[\u00e0\u00e1\u00e2\u00e3\u00e8\u00e9\u00ea\u00ec\u00ed\u00f2\u00f3\u00f4\u00f5\u00f9\u00fa\u00fd\u0103\u0111\u0129\u0169\u01a1\u01b0\u1ea1\u1ea3\u1ea5\u1ea7\u1ea9\u1eab\u1ead\u1eaf\u1eb1\u1eb3\u1eb5\u1eb7\u1eb9\u1ebb\u1ebd\u1ebf\u1ec1\u1ec3\u1ec5\u1ec7\u1ec9\u1ecb\u1ecd\u1ecf\u1ed1\u1ed3\u1ed5\u1ed7\u1ed9\u1edb\u1edd\u1edf\u1ee1\u1ee3\u1ee5\u1ee7\u1ee9\u1eeb\u1eed\u1eef\u1ef1\u1ef3\u1ef5\u1ef7\u1ef9]/i
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
117
|
* Second-language words spelled in ASCII, as a route or folder segment writes them.
|
|
118
118
|
*
|
|
119
|
-
* A path cannot carry diacritics, so
|
|
120
|
-
* accent-based check passes it. The segments below are the ones this product actually produced.
|
|
119
|
+
* A path cannot carry diacritics, so a name written with them reaches the filesystem unaccented
|
|
120
|
+
* (`cap-phat` below) and every accent-based check passes it. The segments below are the ones this product actually produced.
|
|
121
121
|
* A list is the honest instrument here: guessing at Vietnamese-shaped ASCII would refuse `dang`
|
|
122
122
|
* in `dangerous` and `cap` in `capacity`, and a rule that fires on English words is one a
|
|
123
123
|
* repository turns off.
|
|
@@ -145,7 +145,7 @@ const segmentsOf = (filename) =>
|
|
|
145
145
|
* quotes, so this is not only an authoring question: the product's own URLs stop being readable to
|
|
146
146
|
* anybody outside one language.
|
|
147
147
|
*/
|
|
148
|
-
|
|
148
|
+
const noSecondLanguageInPath = {
|
|
149
149
|
meta: {
|
|
150
150
|
type: "problem",
|
|
151
151
|
docs: { description: "File and route names are written in the repository's one shared language." },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starci/eslint-canon-fe",
|
|
3
|
-
"version": "8.0.
|
|
3
|
+
"version": "8.0.9",
|
|
4
4
|
"description": "The StarCi front-end lint canon: every rule its laws publish, gathered as one flat-config plugin and attached by one factory.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
"access": "public"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
|
+
"prepack": "node ../../hfs/prep-runtime.mjs",
|
|
35
|
+
"pretest": "node ../../hfs/prep-runtime.mjs",
|
|
34
36
|
"test": "node --test \"*.spec.mjs\""
|
|
35
37
|
},
|
|
36
38
|
"keywords": [
|
package/project-graph.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* One graph per lint run: `scripts/hfs/project-graph.mjs` (a byte copy in this package's runtime/) builds the machine's module
|
|
5
5
|
* graph and slot manifest once for the repository, caches it, and each rule below reports the findings of its own codes on
|
|
6
6
|
* the file ESLint is visiting, on the line the machine names. There is no second graph and no second judge: the list of
|
|
7
|
-
* rules and their codes is `LINT_ENFORCERS` of the machine (scripts/hfs/architecture/surface.mjs); `
|
|
7
|
+
* rules and their codes is `LINT_ENFORCERS` of the machine (scripts/hfs/architecture/surface.mjs); `starci app check` keeps the
|
|
8
8
|
* findings that have no TypeScript file to sit on. A graph that cannot be built stops the run with the machine's message.
|
|
9
9
|
*/
|
|
10
10
|
import { LINT_ENFORCERS } from "./runtime/scripts/hfs/architecture/surface.mjs"
|