@starci/eslint-canon-fe 6.0.1 → 7.0.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 +19 -0
- package/README.md +3 -5
- package/client-boundary.mjs +6 -8
- package/comments.mjs +38 -15
- package/docs/brand-values.md +2 -2
- package/docs/client-boundary.md +10 -10
- package/docs/env-owner.md +4 -4
- package/docs/formatting.md +2 -2
- package/docs/grammar-boundary.md +2 -2
- package/docs/hooks-folder.md +2 -2
- package/docs/hygiene.md +8 -8
- package/docs/lint-escape-hatch.md +2 -2
- package/docs/lists.md +6 -6
- package/docs/native-controls.md +6 -6
- package/docs/next-conventions.md +20 -20
- package/docs/size-and-state-budget.md +6 -6
- package/docs/size-growth.md +2 -2
- package/docs/status-colors.md +2 -2
- package/docs/translation.md +2 -2
- package/docs/transport.md +16 -16
- package/docs/type-safety.md +8 -8
- package/env-owner.mjs +2 -3
- package/file-layout.mjs +3 -43
- package/formatting.mjs +2 -2
- package/grammar-boundary.mjs +1 -2
- package/hooks-folder.mjs +6 -7
- package/index.mjs +3 -27
- package/lib/config.mjs +6 -22
- package/lib/scope.mjs +3 -12
- package/lib/why.mjs +0 -65
- package/lint-escape-hatch.mjs +3 -3
- package/lists.mjs +0 -4
- package/loading.mjs +2 -2
- package/native-controls.mjs +0 -4
- package/next-conventions.mjs +3 -7
- package/package.json +1 -1
- package/props-and-slots.mjs +4 -5
- package/runtime/knowledge/hfs/slots.yaml +148 -109
- package/runtime/scripts/lib/hfs-allows.mjs +15 -0
- package/runtime/scripts/lib/hfs-slots.mjs +13 -6
- package/runtime/scripts/lib/language.mjs +107 -0
- package/served-locale.mjs +2 -2
- package/shape-slot.mjs +2 -3
- package/size-and-state-budget.mjs +1 -4
- package/size-growth.mjs +1 -2
- package/status-colors.mjs +0 -2
- package/tokens.mjs +1 -1
- package/translation.mjs +1 -29
- package/transport.mjs +4 -8
- package/docs/e2e-shape.md +0 -161
- package/docs/spec-quality.md +0 -147
- package/e2e-shape.mjs +0 -345
- package/spec-quality.mjs +0 -318
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 7.0.0 - 2026-09-30
|
|
4
|
+
|
|
5
|
+
- **New: `no-vietnamese-in-source` (R91).** Identifiers, string literals, template text, JSX text, comments and test titles carry no Vietnamese letter, specs included. Detection is structural on characters (`scripts/lib/language.mjs` in `runtime/`, folded to NFC, so NFD is caught too). The only exemption is the i18n fixtures slot `fe.e2e-support.i18n` (`e2e/fixtures/i18n/`). Removed: the second-language branches of `no-hardcoded-copy` (its `second` and `comment` messages) and the `SECOND_LANGUAGE_LETTER` export; the one rule holds the law.
|
|
6
|
+
- **Breaking: the front end has no tests, so the front-end test rules are gone.** Removed rules: `no-class-string-in-spec`,
|
|
7
|
+
`spec-tests-its-neighbour`, `no-barrel-spec`, `no-double-cast-in-spec`, `connected-spec-has-axe`, `no-mocked-translations`
|
|
8
|
+
(spec quality), `e2e-spec-location`, `e2e-no-absolute-path`, `e2e-no-docker`, `e2e-no-cross-repo-write`, `e2e-no-skip`,
|
|
9
|
+
`e2e-typed-helpers`, `playwright-viewports` (e2e shape) and `unit-test-colocated`, with their why entries, docs and fixtures.
|
|
10
|
+
The factory no longer builds an e2e block (`e2e/**`, `playwright.config.ts`) and returns the ignore block and the typed source block
|
|
11
|
+
only; `sourceRecommended` and `e2eRecommended` are gone (`recommended` is the one set); the spec and e2e exemptions of the other
|
|
12
|
+
rules (`isSpecFile`, `isE2eSource`, the `e2e` tier) are deleted. A test file, a test-tool file or a test dependency in a
|
|
13
|
+
front-end repository is now refused by `hfs check` (FE_NO_TESTS, R97) instead.
|
|
14
|
+
|
|
15
|
+
## 6.0.2 - 2026-09-30
|
|
16
|
+
|
|
17
|
+
- **Fix: `use-client-only-at-boundary` refused the providers wrapper.** The route tree's `providers.tsx` (root or `[locale]`,
|
|
18
|
+
role `providers` of `fe.route` in the slot manifest) is the client component a server layout renders around its children to host
|
|
19
|
+
context providers; the directive is accepted there. A providers wrapper kept elsewhere (a layout feature, a module) is still a
|
|
20
|
+
finding, and the message names `providers.tsx` as its home.
|
|
21
|
+
|
|
3
22
|
## 6.0.1 - 2026-09-30
|
|
4
23
|
|
|
5
24
|
- **Fix: `no-hand-rolled-polling` refused a shared clock.** A timer is polling only when what it runs reads data - a call whose
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ import { loadHfs, starciFeConfig } from "@starci/eslint-canon-fe"
|
|
|
15
15
|
export default starciFeConfig({ hfs: loadHfs(import.meta.url) })
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
`loadHfs` reads the repository's `hfs.json` and the slot manifest shipped in `runtime/`. The factory returns
|
|
18
|
+
`loadHfs` reads the repository's `hfs.json` and the slot manifest shipped in `runtime/`. The factory returns two
|
|
19
19
|
flat-config blocks and owns everything about the law: which files are linted (from the HFS profile), which rules are on
|
|
20
20
|
(every one, at `error` - nothing is off and nothing is a warning), the React Hooks rules (`eslint-plugin-react-hooks` 7, its
|
|
21
21
|
recommended set lifted to `error`), typed linting, and the inline-directive fence (`noInlineConfig` plus
|
|
@@ -24,9 +24,8 @@ recommended set lifted to `error`), typed linting, and the inline-directive fenc
|
|
|
24
24
|
- The **ignore** block: `node_modules`, `.next`, `dist`, `coverage`, `__generated__` and `.starci/`.
|
|
25
25
|
- The **source** block governs `apps/*/src/**` and `packages/*/src/**` with types (`parserOptions.projectService`) and
|
|
26
26
|
`settings.starci.hfs`, the slot view path-scoped rules ask (`lib/hfs.mjs` `hfsOf`), never a path pattern.
|
|
27
|
-
- The **e2e** block governs `e2e/**` and `playwright.config.ts` with the e2e rules and the escape-hatch fence.
|
|
28
27
|
|
|
29
|
-
`recommended`, `
|
|
28
|
+
`recommended`, `why`, `loadHfs`, `linterOptions` and the plugin itself are also
|
|
30
29
|
exported; `why[rule]` is `{ code, vi, fixVi }`, the finding code and the Vietnamese why the harness quotes.
|
|
31
30
|
|
|
32
31
|
## Rules
|
|
@@ -36,8 +35,7 @@ its Vietnamese why. New in 5.0: [env-owner](docs/env-owner.md), [transport](docs
|
|
|
36
35
|
[client-boundary](docs/client-boundary.md), [hooks-folder](docs/hooks-folder.md),
|
|
37
36
|
[next-conventions](docs/next-conventions.md), [translation](docs/translation.md) (no literal copy at any
|
|
38
37
|
tier, no `vn-ok`), [brand-values](docs/brand-values.md), [native-controls](docs/native-controls.md),
|
|
39
|
-
[size-and-state-budget](docs/size-and-state-budget.md), [
|
|
40
|
-
[spec-quality](docs/spec-quality.md) and [lint-escape-hatch](docs/lint-escape-hatch.md). New in 5.1: [lists](docs/lists.md), [hygiene](docs/hygiene.md), [formatting](docs/formatting.md) and [type-safety](docs/type-safety.md), plus rules added to next-conventions, client-boundary, native-controls and transport. New in 6.0: `effect-subscription-needs-cleanup` (timers, frames, listeners, observers, sockets and subscriptions are released against the same handle), `server-module-marks-server-only` ([client-boundary](docs/client-boundary.md)), `no-data-fetch-in-effect` sees indirect loads, `no-hardcoded-copy` reads templates and sentence values, `no-inline-lint-config` refuses `NOSONAR` and the coverage/formatter switches. Also new in 6.0: [grammar-boundary](docs/grammar-boundary.md) (`no-raw-structural-element`), `one-outcome-union` ([transport](docs/transport.md)), `i18n-stack-in-one-module` ([next-conventions](docs/next-conventions.md)), [size-growth](docs/size-growth.md) (`file-size-growth`: a file over the manifest's line budget is not born large and does not grow; the budget is `ruleParams.fe.fileLines`, read from the manifest copy in `runtime/`).
|
|
38
|
+
[size-and-state-budget](docs/size-and-state-budget.md) and [lint-escape-hatch](docs/lint-escape-hatch.md). New in 5.1: [lists](docs/lists.md), [hygiene](docs/hygiene.md), [formatting](docs/formatting.md) and [type-safety](docs/type-safety.md), plus rules added to next-conventions, client-boundary, native-controls and transport. New in 6.1: `no-vietnamese-in-source` (English-only identifiers, strings, comments and test titles; the i18n fixtures slot `e2e/fixtures/i18n/` is the one exemption). New in 6.0: `effect-subscription-needs-cleanup` (timers, frames, listeners, observers, sockets and subscriptions are released against the same handle), `server-module-marks-server-only` ([client-boundary](docs/client-boundary.md)), `no-data-fetch-in-effect` sees indirect loads, `no-hardcoded-copy` reads templates and sentence values, `no-inline-lint-config` refuses `NOSONAR` and the coverage/formatter switches. Also new in 6.0: [grammar-boundary](docs/grammar-boundary.md) (`no-raw-structural-element`), `one-outcome-union` ([transport](docs/transport.md)), `i18n-stack-in-one-module` ([next-conventions](docs/next-conventions.md)), [size-growth](docs/size-growth.md) (`file-size-growth`: a file over the manifest's line budget is not born large and does not grow; the budget is `ruleParams.fe.fileLines`, read from the manifest copy in `runtime/`).
|
|
41
39
|
|
|
42
40
|
Older laws cover comments, file layout, icons and vendor ownership, landmarks, loading states,
|
|
43
41
|
naming, props, served locales, component splits, design tokens, type safety, typography, and
|
package/client-boundary.mjs
CHANGED
|
@@ -16,11 +16,12 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import ts from "typescript"
|
|
19
|
-
import { classifyImport, fileOf, isProductSource,
|
|
19
|
+
import { classifyImport, fileOf, isProductSource, kindOfFile, roleOfFile, slotOfFile, tierOfFile } from "./lib/scope.mjs"
|
|
20
20
|
import { typed } from "./lib/types.mjs"
|
|
21
21
|
|
|
22
22
|
/** The route roles that are client-only by the framework's own rule: `error` and `global-error`. */
|
|
23
|
-
|
|
23
|
+
/** Route-tree roles that are client components by construction: the error boundaries, and the providers wrapper a layout renders. */
|
|
24
|
+
const FRAMEWORK_CLIENT_ROLES = new Set(["error", "global-error", "providers"])
|
|
24
25
|
|
|
25
26
|
/** True when the program opens with the `"use client"` directive. */
|
|
26
27
|
const clientDirective = (program) =>
|
|
@@ -58,13 +59,13 @@ export const useClientOnlyAtBoundary = {
|
|
|
58
59
|
schema: [],
|
|
59
60
|
messages: {
|
|
60
61
|
slot:
|
|
61
|
-
"`\"use client\"` on a {{slot}}. Routes, pages and layouts are server components: making one a client component ships its whole subtree, including the message catalog, to every reader and stops it reading data on the server. Move the interaction into a block below it and put the directive on that block's `index.tsx`.",
|
|
62
|
+
"`\"use client\"` on a {{slot}}. Routes, pages and layouts are server components: making one a client component ships its whole subtree, including the message catalog, to every reader and stops it reading data on the server. Move the interaction into a block below it and put the directive on that block's `index.tsx`; context providers go in the route tree's `providers.tsx`.",
|
|
62
63
|
elsewhere:
|
|
63
|
-
"`\"use client\"` here is not at an interaction boundary. The directive belongs on the `index.tsx` of the interactive block (or overlay), on a leaf whose interaction is intrinsic,
|
|
64
|
+
"`\"use client\"` here is not at an interaction boundary. The directive belongs on the `index.tsx` of the interactive block (or overlay), on a leaf whose interaction is intrinsic, on `error.tsx`/`global-error.tsx`, or on the route tree's `providers.tsx` (the context providers a layout renders around its children). Below a client block the file is already client; above one it must stay a server component.",
|
|
64
65
|
},
|
|
65
66
|
},
|
|
66
67
|
create(context) {
|
|
67
|
-
if (
|
|
68
|
+
if (isBoundary(context)) return {}
|
|
68
69
|
const slot = routeFill(context)
|
|
69
70
|
return {
|
|
70
71
|
Program(program) {
|
|
@@ -113,7 +114,6 @@ export const clientNoServerImport = {
|
|
|
113
114
|
},
|
|
114
115
|
},
|
|
115
116
|
create(context) {
|
|
116
|
-
if (isSpecFile(fileOf(context))) return {}
|
|
117
117
|
let client = false
|
|
118
118
|
return {
|
|
119
119
|
Program(program) {
|
|
@@ -171,7 +171,6 @@ export const serverModuleMarksServerOnly = {
|
|
|
171
171
|
},
|
|
172
172
|
},
|
|
173
173
|
create(context) {
|
|
174
|
-
if (isSpecFile(fileOf(context))) return {}
|
|
175
174
|
const slot = slotOfFile(context)
|
|
176
175
|
if (!slot || ROUTE_FILE_SLOTS.has(slot)) return {}
|
|
177
176
|
// A block or a page is a server or client COMPONENT, not a server module: whether client code reaches the server through it is
|
|
@@ -262,7 +261,6 @@ export const noDangerousHtml = {
|
|
|
262
261
|
},
|
|
263
262
|
},
|
|
264
263
|
create(context) {
|
|
265
|
-
if (isSpecFile(context.filename || context.getFilename())) return {}
|
|
266
264
|
return {
|
|
267
265
|
JSXAttribute(node) {
|
|
268
266
|
if (node.name.type !== "JSXIdentifier" || node.name.name !== "dangerouslySetInnerHTML") return
|
package/comments.mjs
CHANGED
|
@@ -5,21 +5,18 @@
|
|
|
5
5
|
* would miss the one in a log message, so it walks comments, identifiers, string literals, template
|
|
6
6
|
* chunks and JSX text alike - everywhere prose can hide in a source file.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* a second language is content.
|
|
8
|
+
* `no-vietnamese-in-source` holds the English-only law: identifiers, string literals, template text, JSX text,
|
|
9
|
+
* comments and test titles carry no Vietnamese letter. The old `vn-ok: <reason>` pragma is gone (it was used 490
|
|
10
|
+
* times in one repository, and one escape is enough to turn a rule into a comment convention). Detection is
|
|
11
|
+
* structural on characters (`scripts/lib/language.mjs`, folded to NFC), never a word list.
|
|
13
12
|
*
|
|
14
13
|
* The exceptions that remain are placements, not judgements, and that is on purpose: a locale dictionary
|
|
15
14
|
* IS the other language, and a fixture reproducing a real string has to reproduce it exactly. A
|
|
16
|
-
* judgement-based exception would be argued per file forever.
|
|
15
|
+
* judgement-based exception would be argued per file forever. The Vietnamese rule has no placement exemption at all.
|
|
17
16
|
*/
|
|
18
17
|
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
/** The letters that mark the second language a source string must not carry outside a catalogue. */
|
|
22
|
-
export const SECOND_LANGUAGE_LETTER = /[À-ÃÈ-ÊÌÍÒ-ÕÙÚÝà-ãè-êìíò-õùúýĂăĐđĨĩŨũƠơƯưẠ-ỿ]/
|
|
18
|
+
import { hasSecondLanguage } from "./runtime/scripts/lib/language.mjs"
|
|
19
|
+
import { fileOf, inSlot } from "./lib/scope.mjs"
|
|
23
20
|
|
|
24
21
|
/**
|
|
25
22
|
* Extended pictographs, or a regional-indicator pair.
|
|
@@ -35,17 +32,16 @@ export const hasEmoji = (text) =>
|
|
|
35
32
|
/**
|
|
36
33
|
* Files whose second-language text or emoji is CONTENT rather than authoring, decided by what the file IS.
|
|
37
34
|
*
|
|
38
|
-
*
|
|
39
|
-
* which are the product's other language
|
|
40
|
-
*
|
|
41
|
-
* "copy module": a `resources/` folder of strings is copy that skipped the catalogue.
|
|
35
|
+
* One kind only: the locale dictionaries (a `.json` file of an i18n slot, `fe.modules.i18n` or `fe.package.i18n`),
|
|
36
|
+
* which are the product's other language. There is no "copy module": a `resources/` folder of strings is copy that
|
|
37
|
+
* skipped the catalogue.
|
|
42
38
|
*
|
|
43
39
|
* @param {object} context - The ESLint rule context.
|
|
44
40
|
* @returns {boolean} True when the linted file holds content rather than authoring.
|
|
45
41
|
*/
|
|
46
42
|
export const isContentFile = (context) => {
|
|
47
43
|
const file = fileOf(context)
|
|
48
|
-
return
|
|
44
|
+
return inSlot(context, "fe.modules.i18n", "fe.package.i18n") && file.endsWith(".json")
|
|
49
45
|
}
|
|
50
46
|
|
|
51
47
|
/** Walk every place prose can hide, and hand each to one check. */
|
|
@@ -58,6 +54,12 @@ const proseVisitors = (context, report) => {
|
|
|
58
54
|
Identifier(node) {
|
|
59
55
|
report(node, node.name)
|
|
60
56
|
},
|
|
57
|
+
JSXIdentifier(node) {
|
|
58
|
+
report(node, node.name)
|
|
59
|
+
},
|
|
60
|
+
PrivateIdentifier(node) {
|
|
61
|
+
report(node, node.name)
|
|
62
|
+
},
|
|
61
63
|
Literal(node) {
|
|
62
64
|
if (typeof node.value === "string") report(node, node.value)
|
|
63
65
|
},
|
|
@@ -121,10 +123,31 @@ export const noEmojiInSource = {
|
|
|
121
123
|
},
|
|
122
124
|
}
|
|
123
125
|
|
|
126
|
+
// -- COMMENTS-5 ------------------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
/** Source prose is English: no Vietnamese letter in an identifier, string, comment, JSX text or test title. */
|
|
129
|
+
export const noVietnameseInSource = {
|
|
130
|
+
meta: {
|
|
131
|
+
type: "problem",
|
|
132
|
+
docs: { description: "No Vietnamese in source authoring, specs included." },
|
|
133
|
+
schema: [],
|
|
134
|
+
messages: {
|
|
135
|
+
vietnamese:
|
|
136
|
+
"A Vietnamese letter in source. Every reader has to be able to read all of the code, its comments and its test titles, and half of a two-language file is unavailable to somebody. Write it in English; product copy lives in the `messages/<locale>.json` catalogue behind `t()`; there is no pragma.",
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
create(context) {
|
|
140
|
+
return proseVisitors(context, (node, text) => {
|
|
141
|
+
if (hasSecondLanguage(text)) context.report({ node, messageId: "vietnamese" })
|
|
142
|
+
})
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
|
|
124
146
|
/** The rules this law contributes to the plugin. */
|
|
125
147
|
export const rules = {
|
|
126
148
|
"require-export-jsdoc": requireExportJsdoc,
|
|
127
149
|
"no-emoji-in-source": noEmojiInSource,
|
|
150
|
+
"no-vietnamese-in-source": noVietnameseInSource,
|
|
128
151
|
}
|
|
129
152
|
|
|
130
153
|
/**
|
package/docs/brand-values.md
CHANGED
|
@@ -25,6 +25,6 @@ const c = "#ff0000"
|
|
|
25
25
|
|
|
26
26
|
**Finding code:** `FE_STYLE_TOKEN_ONLY`
|
|
27
27
|
|
|
28
|
-
**
|
|
28
|
+
**Why:** `<file>` uses a raw colour/length value (`<what>`). Colours and spacing come only from grammar tokens; brand colours live only in `brand.css`.
|
|
29
29
|
|
|
30
|
-
**
|
|
30
|
+
**Fix:** Use a grammar token class or variable; colour values are declared only in `modules/brand/brand.css`.
|
package/docs/client-boundary.md
CHANGED
|
@@ -26,9 +26,9 @@ export const Feed = () => null
|
|
|
26
26
|
|
|
27
27
|
**Finding code:** `FE_CLIENT_BOUNDARY`
|
|
28
28
|
|
|
29
|
-
**
|
|
29
|
+
**Why:** `"use client"` in `<file>` (`<slot>` must not be a client component).
|
|
30
30
|
|
|
31
|
-
**
|
|
31
|
+
**Fix:** Put `"use client"` in the `index.tsx` of an interactive block, a leaf, or `error.tsx`/`global-error.tsx`; keep layouts and pages as server components.
|
|
32
32
|
|
|
33
33
|
## `starci-fe/client-no-server-import`
|
|
34
34
|
|
|
@@ -50,9 +50,9 @@ import useSWR from "swr"
|
|
|
50
50
|
|
|
51
51
|
**Finding code:** `FE_CLIENT_SERVER_IMPORT`
|
|
52
52
|
|
|
53
|
-
**
|
|
53
|
+
**Why:** The client component at `<file>` imports code that exists only on the server (`server-only`, `next/headers`, Node modules, a server reader).
|
|
54
54
|
|
|
55
|
-
**
|
|
55
|
+
**Fix:** Read data in a server component or server reader and pass it down as props; or use SWR calling the app client.
|
|
56
56
|
|
|
57
57
|
## `starci-fe/server-module-marks-server-only`
|
|
58
58
|
|
|
@@ -75,9 +75,9 @@ export const readCourses = async () => (await headers()).get("x-locale")
|
|
|
75
75
|
|
|
76
76
|
**Finding code:** `FE_SERVER_ONLY_MARK`
|
|
77
77
|
|
|
78
|
-
**
|
|
78
|
+
**Why:** Module `<file>` imports server-only APIs (`next/headers`, `next/server`, `next-intl/server`, Node modules or a module that is already server-only) but does not begin with `import "server-only"`.
|
|
79
79
|
|
|
80
|
-
**
|
|
80
|
+
**Fix:** Make `import "server-only"` the first statement of the file; route files (`page`, `layout`, `route`, `proxy`) are already server components so they do not need it.
|
|
81
81
|
|
|
82
82
|
## `starci-fe/web-storage-only-in-modules`
|
|
83
83
|
|
|
@@ -97,9 +97,9 @@ const draft = useTopUpDraft()
|
|
|
97
97
|
|
|
98
98
|
**Finding code:** `FE_STORAGE_OUTSIDE_MODULES`
|
|
99
99
|
|
|
100
|
-
**
|
|
100
|
+
**Why:** `<file>` uses `localStorage`/`sessionStorage` outside `modules/`; storage does not exist on the server and throws when the quota is full.
|
|
101
101
|
|
|
102
|
-
**
|
|
102
|
+
**Fix:** Put reads/writes behind a hook or a guarded module in `modules/` (`typeof window`, try/catch) and call it from a block.
|
|
103
103
|
|
|
104
104
|
## `starci-fe/no-dangerous-html`
|
|
105
105
|
|
|
@@ -119,6 +119,6 @@ const draft = useTopUpDraft()
|
|
|
119
119
|
|
|
120
120
|
**Finding code:** `FE_DANGEROUS_HTML`
|
|
121
121
|
|
|
122
|
-
**
|
|
122
|
+
**Why:** `dangerouslySetInnerHTML` on `<tag>` in `<file>`: an arbitrary string becomes executable HTML.
|
|
123
123
|
|
|
124
|
-
**
|
|
124
|
+
**Fix:** Render content as elements; only a `<script>` carrying JSON-LD or theme code from an app constant may use it.
|
package/docs/env-owner.md
CHANGED
|
@@ -25,9 +25,9 @@ const url = config.apiUrl
|
|
|
25
25
|
|
|
26
26
|
**Finding code:** `FE_ENV_OWNER`
|
|
27
27
|
|
|
28
|
-
**
|
|
28
|
+
**Why:** `<file>` reads environment variables. Only `modules/config` may read env.
|
|
29
29
|
|
|
30
|
-
**
|
|
30
|
+
**Fix:** Read the value from a typed export of `modules/config` instead of `process.env`.
|
|
31
31
|
|
|
32
32
|
## `starci-fe/no-hardcoded-endpoint-fallback`
|
|
33
33
|
|
|
@@ -47,6 +47,6 @@ const url = process.env.API_URL
|
|
|
47
47
|
|
|
48
48
|
**Finding code:** `FE_ENV_OWNER`
|
|
49
49
|
|
|
50
|
-
**
|
|
50
|
+
**Why:** `<file>` has a hard fallback value for an address or secret (`?? "http://localhost…"`). A missing variable in production must throw when the configuration loads, never silently point at a dev machine.
|
|
51
51
|
|
|
52
|
-
**
|
|
52
|
+
**Fix:** Remove the fallback; let `modules/config` throw when a variable is missing in production.
|
package/docs/formatting.md
CHANGED
|
@@ -25,6 +25,6 @@ const format = useFormatter()
|
|
|
25
25
|
|
|
26
26
|
**Finding code:** `FE_I18N_FORMATTER`
|
|
27
27
|
|
|
28
|
-
**
|
|
28
|
+
**Why:** `<file>` formats a number, money or date with `toLocale*String`, `new Intl.*`, `toFixed`, a currency symbol pasted into a template, or a date library, instead of the next-intl formatter.
|
|
29
29
|
|
|
30
|
-
**
|
|
30
|
+
**Fix:** Use `useFormatter()` (or `getFormatter()` on the server): `number(...)`, `dateTime(...)`, `relativeTime(...)`.
|
package/docs/grammar-boundary.md
CHANGED
|
@@ -28,6 +28,6 @@ export const Home = () => <SurfaceCard><Text>{t("intro")}</Text></SurfaceCard>
|
|
|
28
28
|
|
|
29
29
|
**Finding code:** `FE_NATIVE_FORM_CONTROL`
|
|
30
30
|
|
|
31
|
-
**
|
|
31
|
+
**Why:** Raw `<tag>` in `<file>`. Page structure and text are composed from grammar components, not written as raw HTML.
|
|
32
32
|
|
|
33
|
-
**
|
|
33
|
+
**Fix:** Replace it with the matching grammar component (`Heading`, `Text`, `SurfaceCard`, ...); if the grammar lacks one, add it to the grammar or the ui package instead of drawing it in place.
|
package/docs/hooks-folder.md
CHANGED
|
@@ -24,6 +24,6 @@ export const useCourse = () => 1
|
|
|
24
24
|
|
|
25
25
|
**Finding code:** `FE_HOOKS_ARE_HOOKS`
|
|
26
26
|
|
|
27
|
-
**
|
|
27
|
+
**Why:** `<file>` in `hooks/` is not a React hook. Server readers belong in `modules/api`, helpers in `<domain>.shared.ts`.
|
|
28
28
|
|
|
29
|
-
**
|
|
29
|
+
**Fix:** One hook per `use*.ts` file; shared helpers go into `<domain>.shared.ts`; server readers go into `modules/api/<domain>/read-*.ts`.
|
package/docs/hygiene.md
CHANGED
|
@@ -51,9 +51,9 @@ const subscribe = (listener: () => void) => {
|
|
|
51
51
|
|
|
52
52
|
**Finding code:** `FE_EFFECT_CLEANUP`
|
|
53
53
|
|
|
54
|
-
**
|
|
54
|
+
**Why:** `<timer>` in `<file>` runs outside an effect, or inside an effect with no cleanup calling `clearTimeout`/`clearInterval`.
|
|
55
55
|
|
|
56
|
-
**
|
|
56
|
+
**Fix:** Put the timer in `useEffect` and return `() => clearTimeout(id)`; or move it into a hook that does so.
|
|
57
57
|
|
|
58
58
|
## `starci-fe/no-data-fetch-in-effect`
|
|
59
59
|
|
|
@@ -73,9 +73,9 @@ const { data } = useSWR(key, fetcher)
|
|
|
73
73
|
|
|
74
74
|
**Finding code:** `FE_EFFECT_FETCH`
|
|
75
75
|
|
|
76
|
-
**
|
|
76
|
+
**Why:** `useEffect` in `<file>` loads data (`await`, `fetch`, `.then`): no cache, no request merging, no loading/error state, no cancellation.
|
|
77
77
|
|
|
78
|
-
**
|
|
78
|
+
**Fix:** Read on the client with SWR calling the app client (`hooks/`), and on the route side with a server reader `modules/api/<domain>/read-*.ts`.
|
|
79
79
|
|
|
80
80
|
## `starci-fe/no-empty-catch`
|
|
81
81
|
|
|
@@ -95,9 +95,9 @@ try { await save() } catch (error) { return { kind: "unavailable", cause: error
|
|
|
95
95
|
|
|
96
96
|
**Finding code:** `FE_SWALLOWED_ERROR`
|
|
97
97
|
|
|
98
|
-
**
|
|
98
|
+
**Why:** `catch` or `.catch` in `<file>` does nothing: the error vanishes, with no notice and no trace.
|
|
99
99
|
|
|
100
|
-
**
|
|
100
|
+
**Fix:** Return a typed outcome carrying the cause, show an error state, or rethrow.
|
|
101
101
|
|
|
102
102
|
## `starci-fe/no-console`
|
|
103
103
|
|
|
@@ -117,6 +117,6 @@ return { kind: "unavailable", cause: error }
|
|
|
117
117
|
|
|
118
118
|
**Finding code:** `FE_CONSOLE_CALL`
|
|
119
119
|
|
|
120
|
-
**
|
|
120
|
+
**Why:** `console.<method>` in `<file>`: it enters no log pipeline and exposes internal details to the user.
|
|
121
121
|
|
|
122
|
-
**
|
|
122
|
+
**Fix:** Return a typed outcome, show an error state, or let the error reach `error.tsx`.
|
|
@@ -29,6 +29,6 @@ const s = 1
|
|
|
29
29
|
|
|
30
30
|
**Finding code:** `HFS_INLINE_SUPPRESSION`
|
|
31
31
|
|
|
32
|
-
**
|
|
32
|
+
**Why:** There is a rule-disabling comment at `<file>:<line>`. HFS does not allow disabling in place - fix the code, or propose changing the rule.
|
|
33
33
|
|
|
34
|
-
**
|
|
34
|
+
**Fix:** Remove `eslint-disable`, `@ts-ignore`, `@ts-expect-error` or `vn-ok` and fix the cause.
|
package/docs/lists.md
CHANGED
|
@@ -24,9 +24,9 @@ rows.map((row) => <Row key={row.id} row={row} />)
|
|
|
24
24
|
|
|
25
25
|
**Finding code:** `FE_LIST_KEY`
|
|
26
26
|
|
|
27
|
-
**
|
|
27
|
+
**Why:** An element returned from `.map` in `<file>` has no `key`, so React identifies rows by position and rows swap state when removed or reordered.
|
|
28
28
|
|
|
29
|
-
**
|
|
29
|
+
**Fix:** Add a `key` taken from the data id; for a fragment use `<Fragment key={...}>`.
|
|
30
30
|
|
|
31
31
|
## `starci-fe/no-index-key`
|
|
32
32
|
|
|
@@ -46,9 +46,9 @@ rows.map((row) => <Row key={row.id} row={row} />)
|
|
|
46
46
|
|
|
47
47
|
**Finding code:** `FE_LIST_KEY`
|
|
48
48
|
|
|
49
|
-
**
|
|
49
|
+
**Why:** The `key` in `<file>` comes from the `.map` index or a randomly generated value; an index is a position, not an identity.
|
|
50
50
|
|
|
51
|
-
**
|
|
51
|
+
**Fix:** Use the id the data carries (`key={row.id}`); do not use index, `Math.random`, `Date.now`.
|
|
52
52
|
|
|
53
53
|
## `starci-fe/no-inline-literal-prop-in-list`
|
|
54
54
|
|
|
@@ -69,6 +69,6 @@ rows.map((row) => <Avatar key={row.id} props={AVATAR_PROPS} />)
|
|
|
69
69
|
|
|
70
70
|
**Finding code:** `FE_LIST_KEY`
|
|
71
71
|
|
|
72
|
-
**
|
|
72
|
+
**Why:** A component inside `.map` in `<file>` receives an object or array literal as a prop: every row gets a new value on every render, so memo never hits.
|
|
73
73
|
|
|
74
|
-
**
|
|
74
|
+
**Fix:** Hoist the constant out of the component or build it once before `.map`.
|
package/docs/native-controls.md
CHANGED
|
@@ -24,9 +24,9 @@ Use the grammar's renderers; never a bare select, input, textarea or button.
|
|
|
24
24
|
|
|
25
25
|
**Finding code:** `FE_NATIVE_FORM_CONTROL`
|
|
26
26
|
|
|
27
|
-
**
|
|
27
|
+
**Why:** Raw `<tag>` in `<file>`. Use the grammar renderer.
|
|
28
28
|
|
|
29
|
-
**
|
|
29
|
+
**Fix:** Replace it with a grammar component; if none exists, add it to the grammar instead of drawing it in place.
|
|
30
30
|
|
|
31
31
|
## `starci-fe/no-native-img`
|
|
32
32
|
|
|
@@ -46,9 +46,9 @@ Use `next/image`, never a bare `<img>` (catalogue R62, sub-check `FE_NATIVE_IMAG
|
|
|
46
46
|
|
|
47
47
|
**Finding code:** `FE_NATIVE_IMAGE`
|
|
48
48
|
|
|
49
|
-
**
|
|
49
|
+
**Why:** Raw `<img>` in `<file>`: it loads the original image and reserves no space, so the page jumps when the image loads.
|
|
50
50
|
|
|
51
|
-
**
|
|
51
|
+
**Fix:** Use `Image` from `next/image` with `width` and `height` (or `fill` and `sizes`) and `alt` from the catalog.
|
|
52
52
|
|
|
53
53
|
## `starci-fe/image-has-size`
|
|
54
54
|
|
|
@@ -68,6 +68,6 @@ Use `next/image`, never a bare `<img>` (catalogue R62, sub-check `FE_NATIVE_IMAG
|
|
|
68
68
|
|
|
69
69
|
**Finding code:** `FE_NATIVE_IMAGE`
|
|
70
70
|
|
|
71
|
-
**
|
|
71
|
+
**Why:** `Image` in `<file>` lacks `width` and `height` (or has `fill` without `sizes`): the browser cannot reserve space.
|
|
72
72
|
|
|
73
|
-
**
|
|
73
|
+
**Fix:** Provide both `width` and `height`, or `fill` inside a sized frame together with `sizes`.
|
package/docs/next-conventions.md
CHANGED
|
@@ -24,9 +24,9 @@ export function proxy(request) { return request }
|
|
|
24
24
|
|
|
25
25
|
**Finding code:** `FE_NEXT_CONVENTIONS`
|
|
26
26
|
|
|
27
|
-
**
|
|
27
|
+
**Why:** `<file>` uses the name `middleware`. Since Next 16 the request interceptor is `proxy.ts` and exports `proxy`.
|
|
28
28
|
|
|
29
|
-
**
|
|
29
|
+
**Fix:** Rename the file to `proxy.ts` and rename the export to `proxy`.
|
|
30
30
|
|
|
31
31
|
## `starci-fe/locale-segment-is-locale`
|
|
32
32
|
|
|
@@ -46,9 +46,9 @@ export default function Page() {}
|
|
|
46
46
|
|
|
47
47
|
**Finding code:** `FE_I18N_PLACEMENT`
|
|
48
48
|
|
|
49
|
-
**
|
|
49
|
+
**Why:** The file `<file>` sits under a language segment that is not `[locale]`. There is only one pattern: `next-intl`, `[locale]`, default `vi`.
|
|
50
50
|
|
|
51
|
-
**
|
|
51
|
+
**Fix:** Rename the segment directory to `[locale]`.
|
|
52
52
|
|
|
53
53
|
## `starci-fe/no-second-i18n-stack`
|
|
54
54
|
|
|
@@ -68,9 +68,9 @@ import { useTranslations } from "next-intl"
|
|
|
68
68
|
|
|
69
69
|
**Finding code:** `FE_I18N_PLACEMENT`
|
|
70
70
|
|
|
71
|
-
**
|
|
71
|
+
**Why:** `<file>` imports a second i18n library. The app uses only `next-intl` through `modules/i18n`.
|
|
72
72
|
|
|
73
|
-
**
|
|
73
|
+
**Fix:** Switch to `next-intl`; remove the other library.
|
|
74
74
|
|
|
75
75
|
## `starci-fe/html-lang-from-locale`
|
|
76
76
|
|
|
@@ -90,9 +90,9 @@ import { useTranslations } from "next-intl"
|
|
|
90
90
|
|
|
91
91
|
**Finding code:** `FE_I18N_LITERAL`
|
|
92
92
|
|
|
93
|
-
**
|
|
93
|
+
**Why:** `<html lang>` in `<file>` is hardcoded. The `lang` attribute must come from the `[locale]` segment.
|
|
94
94
|
|
|
95
|
-
**
|
|
95
|
+
**Fix:** Use `lang={locale}` from the parameters of the `[locale]` layout.
|
|
96
96
|
|
|
97
97
|
## `starci-fe/page-exports-metadata`
|
|
98
98
|
|
|
@@ -113,9 +113,9 @@ export default function Page() { return <CoursesPage /> }
|
|
|
113
113
|
|
|
114
114
|
**Finding code:** `FE_PAGE_METADATA_MISSING`
|
|
115
115
|
|
|
116
|
-
**
|
|
116
|
+
**Why:** `<file>` is a `page.tsx` but does not export `metadata` or `generateMetadata`; every page in the branch carries the same title.
|
|
117
117
|
|
|
118
|
-
**
|
|
118
|
+
**Fix:** Export `metadata` (or `generateMetadata` when the title comes from data) with the title and description taken from the message catalog.
|
|
119
119
|
|
|
120
120
|
## `starci-fe/no-null-suspense-fallback`
|
|
121
121
|
|
|
@@ -135,9 +135,9 @@ export default function Page() { return <CoursesPage /> }
|
|
|
135
135
|
|
|
136
136
|
**Finding code:** `FE_SUSPENSE_NULL_FALLBACK`
|
|
137
137
|
|
|
138
|
-
**
|
|
138
|
+
**Why:** `<Suspense>` in `<file>` has an empty `fallback`: the user sees a blank space instead of a loading state.
|
|
139
139
|
|
|
140
|
-
**
|
|
140
|
+
**Fix:** Pass the skeleton (loading state) of exactly what is loading as the `fallback`.
|
|
141
141
|
|
|
142
142
|
## `starci-fe/navigation-from-intl`
|
|
143
143
|
|
|
@@ -157,9 +157,9 @@ import { useRouter } from "@/modules/i18n/navigation"
|
|
|
157
157
|
|
|
158
158
|
**Finding code:** `FE_I18N_NAVIGATION`
|
|
159
159
|
|
|
160
|
-
**
|
|
160
|
+
**Why:** `<file>` imports `Link`, `useRouter`, `usePathname` or `redirect` from Next, which is unaware of the locale: paths lose the `[locale]` prefix.
|
|
161
161
|
|
|
162
|
-
**
|
|
162
|
+
**Fix:** Import from `modules/i18n/navigation`, which next-intl builds from `routing.ts`.
|
|
163
163
|
|
|
164
164
|
## `starci-fe/no-native-anchor`
|
|
165
165
|
|
|
@@ -179,9 +179,9 @@ An internal link is `Link` from `modules/i18n/navigation`; an external `_blank`
|
|
|
179
179
|
|
|
180
180
|
**Finding code:** `FE_I18N_NAVIGATION`
|
|
181
181
|
|
|
182
|
-
**
|
|
182
|
+
**Why:** `<a>` in `<file>` points at an internal route (reloads the whole page, loses the locale, loses prefetch) or opens a new tab without `rel`.
|
|
183
183
|
|
|
184
|
-
**
|
|
184
|
+
**Fix:** Use `Link` from `modules/i18n/navigation` with an href built by `modules/routes`; for `_blank` links add `rel="noopener noreferrer"`.
|
|
185
185
|
|
|
186
186
|
## `starci-fe/no-hardcoded-route`
|
|
187
187
|
|
|
@@ -201,9 +201,9 @@ router.push(routes.newWorkspace())
|
|
|
201
201
|
|
|
202
202
|
**Finding code:** `FE_ROUTE_HARDCODED`
|
|
203
203
|
|
|
204
|
-
**
|
|
204
|
+
**Why:** `<file>` writes a route path directly at the call site; when the route moves, the copy points at a 404.
|
|
205
205
|
|
|
206
|
-
**
|
|
206
|
+
**Fix:** Build the href with a function from `modules/routes` and use that same function everywhere.
|
|
207
207
|
|
|
208
208
|
## `starci-fe/i18n-stack-in-one-module`
|
|
209
209
|
|
|
@@ -227,6 +227,6 @@ export const i18n = createAppI18n({ locales: ["vi"] })
|
|
|
227
227
|
|
|
228
228
|
**Finding code:** `FE_I18N_PLACEMENT`
|
|
229
229
|
|
|
230
|
-
**
|
|
230
|
+
**Why:** `<file>` builds a layer of the next-intl stack itself (`defineRouting`, `createNavigation`, `getRequestConfig`, `createMiddleware`). The stack may be written only once per repo.
|
|
231
231
|
|
|
232
|
-
**
|
|
232
|
+
**Fix:** Call the `createAppI18n` factory of the shared i18n package in `modules/i18n/index.ts` and import the result; in a single-app repo, write the stack only in that app's `modules/i18n`.
|