@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.
Files changed (53) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +3 -5
  3. package/client-boundary.mjs +6 -8
  4. package/comments.mjs +38 -15
  5. package/docs/brand-values.md +2 -2
  6. package/docs/client-boundary.md +10 -10
  7. package/docs/env-owner.md +4 -4
  8. package/docs/formatting.md +2 -2
  9. package/docs/grammar-boundary.md +2 -2
  10. package/docs/hooks-folder.md +2 -2
  11. package/docs/hygiene.md +8 -8
  12. package/docs/lint-escape-hatch.md +2 -2
  13. package/docs/lists.md +6 -6
  14. package/docs/native-controls.md +6 -6
  15. package/docs/next-conventions.md +20 -20
  16. package/docs/size-and-state-budget.md +6 -6
  17. package/docs/size-growth.md +2 -2
  18. package/docs/status-colors.md +2 -2
  19. package/docs/translation.md +2 -2
  20. package/docs/transport.md +16 -16
  21. package/docs/type-safety.md +8 -8
  22. package/env-owner.mjs +2 -3
  23. package/file-layout.mjs +3 -43
  24. package/formatting.mjs +2 -2
  25. package/grammar-boundary.mjs +1 -2
  26. package/hooks-folder.mjs +6 -7
  27. package/index.mjs +3 -27
  28. package/lib/config.mjs +6 -22
  29. package/lib/scope.mjs +3 -12
  30. package/lib/why.mjs +0 -65
  31. package/lint-escape-hatch.mjs +3 -3
  32. package/lists.mjs +0 -4
  33. package/loading.mjs +2 -2
  34. package/native-controls.mjs +0 -4
  35. package/next-conventions.mjs +3 -7
  36. package/package.json +1 -1
  37. package/props-and-slots.mjs +4 -5
  38. package/runtime/knowledge/hfs/slots.yaml +148 -109
  39. package/runtime/scripts/lib/hfs-allows.mjs +15 -0
  40. package/runtime/scripts/lib/hfs-slots.mjs +13 -6
  41. package/runtime/scripts/lib/language.mjs +107 -0
  42. package/served-locale.mjs +2 -2
  43. package/shape-slot.mjs +2 -3
  44. package/size-and-state-budget.mjs +1 -4
  45. package/size-growth.mjs +1 -2
  46. package/status-colors.mjs +0 -2
  47. package/tokens.mjs +1 -1
  48. package/translation.mjs +1 -29
  49. package/transport.mjs +4 -8
  50. package/docs/e2e-shape.md +0 -161
  51. package/docs/spec-quality.md +0 -147
  52. package/e2e-shape.mjs +0 -345
  53. 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 three
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`, `sourceRecommended`, `e2eRecommended`, `why`, `loadHfs`, `linterOptions` and the plugin itself are also
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), [e2e-shape](docs/e2e-shape.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
@@ -16,11 +16,12 @@
16
16
  */
17
17
 
18
18
  import ts from "typescript"
19
- import { classifyImport, fileOf, isProductSource, isSpecFile, kindOfFile, roleOfFile, slotOfFile, tierOfFile } from "./lib/scope.mjs"
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
- const FRAMEWORK_CLIENT_ROLES = new Set(["error", "global-error"])
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, or on `error.tsx`/`global-error.tsx`. Below a client block the file is already client; above one it must stay a server component.",
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 (isSpecFile(fileOf(context)) || isBoundary(context)) return {}
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
- * THE SECOND-LANGUAGE RULE IS GONE. It carried a `vn-ok: <reason>` pragma, an escape hatch that was
9
- * used 490 times in one repository, and one escape is enough to turn a rule into a comment
10
- * convention. Its job is now held by `no-hardcoded-copy` in `translation.mjs`, which has no
11
- * pragma: user-facing text comes from a `next-intl` catalogue, and the catalogue is the only place
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 { fileOf, inSlot, isSpecFile } from "./lib/scope.mjs"
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
- * Three kinds only: the locale dictionaries (a `.json` file of an i18n slot, `fe.modules.i18n` or `fe.package.i18n`),
39
- * which are the product's other language; the e2e support tree (slot `fe.e2e-support`: fixtures and support),
40
- * and specs, which reproduce real strings and would be testing something else if they translated them. There is no
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 isSpecFile(file) || inSlot(context, "fe.e2e-support") || (inSlot(context, "fe.modules.i18n", "fe.package.i18n") && file.endsWith(".json"))
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
  /**
@@ -25,6 +25,6 @@ const c = "#ff0000"
25
25
 
26
26
  **Finding code:** `FE_STYLE_TOKEN_ONLY`
27
27
 
28
- **Vì sao (why):** `<file>` dùng giá trị màu/độ dài thô (`<what>`). Màu và khoảng cách chỉ đến từ token grammar; màu thương hiệu chỉ ở `brand.css`.
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
- **Cách sửa:** Dùng lớp hoặc biến token của grammar; giá trị màu chỉ khai trong `modules/brand/brand.css`.
30
+ **Fix:** Use a grammar token class or variable; colour values are declared only in `modules/brand/brand.css`.
@@ -26,9 +26,9 @@ export const Feed = () => null
26
26
 
27
27
  **Finding code:** `FE_CLIENT_BOUNDARY`
28
28
 
29
- **Vì sao (why):** `"use client"` ở `<file>` (`<slot>` không được là client component).
29
+ **Why:** `"use client"` in `<file>` (`<slot>` must not be a client component).
30
30
 
31
- **Cách sửa:** Đặt `"use client"` ở `index.tsx` của block có tương tác, leaf, hoặc `error.tsx`/`global-error.tsx`; giữ layout và page là server component.
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
- **Vì sao (why):** Client component ở `<file>` nhập mã chỉ tồn tại ở server (`server-only`, `next/headers`, module Node, server reader).
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
- **Cách sửa:** Đọc dữ liệu ở server component hoặc server reader rồi truyền xuống bằng props; hoặc dùng SWR gọi client của app.
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
- **Vì sao (why):** Module `<file>` nhập API chỉ có ở server (`next/headers`, `next/server`, `next-intl/server`, module Node hoặc module đã là server-only) nhưng không mở đầu bằng `import "server-only"`.
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
- **Cách sửa:** Đặt `import "server-only"` làm câu lệnh đầu tiên của tệp; tệp route (`page`, `layout`, `route`, `proxy`) là server component sẵn nên không cần.
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
- **Vì sao (why):** `<file>` dùng `localStorage`/`sessionStorage` ngoài `modules/`; storage không có ở server và ném lỗi khi đầy hạn mức.
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
- **Cách sửa:** Đặt đọc/ghi sau một hook hoặc module trong `modules/` có bảo vệ (`typeof window`, try/catch) rồi gọi từ block.
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
- **Vì sao (why):** `dangerouslySetInnerHTML` trên `<tag>` ở `<file>`: chuỗi bất kỳ trở thành HTML chạy được.
122
+ **Why:** `dangerouslySetInnerHTML` on `<tag>` in `<file>`: an arbitrary string becomes executable HTML.
123
123
 
124
- **Cách sửa:** Render nội dung thành phần tử; chỉ `<script>` mang JSON-LD hoặc mã theme từ hằng số của app mới được dùng.
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
- **Vì sao (why):** `<file>` đọc biến môi trường. Chỉ `modules/config` được đọc env.
28
+ **Why:** `<file>` reads environment variables. Only `modules/config` may read env.
29
29
 
30
- **Cách sửa:** Đọc giá trị từ export có kiểu của `modules/config` thay vì `process.env`.
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
- **Vì sao (why):** `<file>` có giá trị dự phòng cứng cho địa chỉ hoặc bí mật (`?? "http://localhost…"`). Thiếu biến ở production phải ném lỗi khi nạp cấu hình, không được lặng lẽ trỏ về máy dev.
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
- **Cách sửa:** Xóa giá trị dự phòng; để `modules/config` ném lỗi khi thiếu biến ở production.
52
+ **Fix:** Remove the fallback; let `modules/config` throw when a variable is missing in production.
@@ -25,6 +25,6 @@ const format = useFormatter()
25
25
 
26
26
  **Finding code:** `FE_I18N_FORMATTER`
27
27
 
28
- **Vì sao (why):** `<file>` định dạng số, tiền hoặc ngày bằng `toLocale*String`, `new Intl.*`, `toFixed`, ký hiệu tiền dán vào template hoặc thư viện ngày, thay vì formatter của next-intl.
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
- **Cách sửa:** Dùng `useFormatter()` (hoặc `getFormatter()` ở server): `number(...)`, `dateTime(...)`, `relativeTime(...)`.
30
+ **Fix:** Use `useFormatter()` (or `getFormatter()` on the server): `number(...)`, `dateTime(...)`, `relativeTime(...)`.
@@ -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
- **Vì sao (why):** `<tag>` thô ở `<file>`. Cấu trúc trang và chữ được ghép từ component của grammar, không viết HTML thô.
31
+ **Why:** Raw `<tag>` in `<file>`. Page structure and text are composed from grammar components, not written as raw HTML.
32
32
 
33
- **Cách sửa:** Thay bằng component grammar tương ứng (`Heading`, `Text`, `SurfaceCard`, ...); nếu grammar chưa có, thêm vào grammar hoặc gói ui thay vì vẽ tại chỗ.
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.
@@ -24,6 +24,6 @@ export const useCourse = () => 1
24
24
 
25
25
  **Finding code:** `FE_HOOKS_ARE_HOOKS`
26
26
 
27
- **Vì sao (why):** `<file>` trong `hooks/` không phải React hook. Reader server về `modules/api`, helper về `<domain>.shared.ts`.
27
+ **Why:** `<file>` in `hooks/` is not a React hook. Server readers belong in `modules/api`, helpers in `<domain>.shared.ts`.
28
28
 
29
- **Cách sửa:** Mỗi tệp `use*.ts` một hook; helper dùng chung vào `<domain>.shared.ts`; reader server vào `modules/api/<domain>/read-*.ts`.
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
- **Vì sao (why):** `<timer>` ở `<file>` chạy ngoài effect hoặc trong effect không có cleanup gọi `clearTimeout`/`clearInterval`.
54
+ **Why:** `<timer>` in `<file>` runs outside an effect, or inside an effect with no cleanup calling `clearTimeout`/`clearInterval`.
55
55
 
56
- **Cách sửa:** Đặt timer trong `useEffect` và trả `() => clearTimeout(id)`; hoặc chuyển vào một hook làm việc đó.
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
- **Vì sao (why):** `useEffect` ở `<file>` tải dữ liệu (`await`, `fetch`, `.then`): không cache, không gộp request, không trạng thái loading/lỗi, không huỷ.
76
+ **Why:** `useEffect` in `<file>` loads data (`await`, `fetch`, `.then`): no cache, no request merging, no loading/error state, no cancellation.
77
77
 
78
- **Cách sửa:** Đọc phía client bằng SWR gọi client của app (`hooks/`), phía route bằng server reader `modules/api/<domain>/read-*.ts`.
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
- **Vì sao (why):** `catch` hoặc `.catch` ở `<file>` không làm gì: lỗi biến mất, không thông báo, không dấu vết.
98
+ **Why:** `catch` or `.catch` in `<file>` does nothing: the error vanishes, with no notice and no trace.
99
99
 
100
- **Cách sửa:** Trả một outcome có kiểu mang nguyên nhân, hiển thị trạng thái lỗi, hoặc ném lại.
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
- **Vì sao (why):** `console.<method>` ở `<file>`: không vào pipeline log nào và lộ chi tiết nội bộ cho người dùng.
120
+ **Why:** `console.<method>` in `<file>`: it enters no log pipeline and exposes internal details to the user.
121
121
 
122
- **Cách sửa:** Trả outcome có kiểu, hiển thị trạng thái lỗi, hoặc để lỗi tới `error.tsx`.
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
- **Vì sao (why):** Có chú thích tắt luật ở `<file>:<line>`. HFS không cho tắt tại chỗ — sửa code, hoặc đề xuất đổi luật.
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
- **Cách sửa:** Xóa `eslint-disable`, `@ts-ignore`, `@ts-expect-error` hoặc `vn-ok` và sửa nguyên nhân.
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
- **Vì sao (why):** Phần tử trả từ `.map` ở `<file>` không có `key`, nên React nhận diện hàng theo vị trí và hàng bị đổi state khi xoá hoặc sắp xếp lại.
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
- **Cách sửa:** Thêm `key` lấy từ id của dữ liệu; với fragment dùng `<Fragment key={...}>`.
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
- **Vì sao (why):** `key` ở `<file>` lấy từ chỉ số của `.map` hoặc giá trị sinh ngẫu nhiên; chỉ số là vị trí, không phải danh tính.
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
- **Cách sửa:** Dùng id mà dữ liệu mang (`key={row.id}`); không dùng index, `Math.random`, `Date.now`.
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
- **Vì sao (why):** Component trong `.map` ở `<file>` nhận object hoặc array literal làm prop: mỗi hàng mỗi lần render một giá trị mới, memo không bao giờ trúng.
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
- **Cách sửa:** Nâng hằng số ra ngoài component hoặc dựng một lần trước `.map`.
74
+ **Fix:** Hoist the constant out of the component or build it once before `.map`.
@@ -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
- **Vì sao (why):** `<tag>` thô ở `<file>`. Dùng renderer của grammar.
27
+ **Why:** Raw `<tag>` in `<file>`. Use the grammar renderer.
28
28
 
29
- **Cách sửa:** Thay bằng thành phần của grammar; nếu chưa có, thêm vào grammar thay vì vẽ tại chỗ.
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
- **Vì sao (why):** `<img>` thô ở `<file>`: tải ảnh gốc, không giữ chỗ nên trang nhảy khi ảnh tải xong.
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
- **Cách sửa:** Dùng `Image` của `next/image` với `width` và `height` (hoặc `fill` và `sizes`) và `alt` từ catalog.
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
- **Vì sao (why):** `Image` ở `<file>` thiếu `width` và `height` (hoặc `fill` mà thiếu `sizes`): trình duyệt không giữ chỗ được.
71
+ **Why:** `Image` in `<file>` lacks `width` and `height` (or has `fill` without `sizes`): the browser cannot reserve space.
72
72
 
73
- **Cách sửa:** Cho đủ `width` và `height`, hoặc `fill` trong khung có kích thước kèm `sizes`.
73
+ **Fix:** Provide both `width` and `height`, or `fill` inside a sized frame together with `sizes`.
@@ -24,9 +24,9 @@ export function proxy(request) { return request }
24
24
 
25
25
  **Finding code:** `FE_NEXT_CONVENTIONS`
26
26
 
27
- **Vì sao (why):** `<file>` dùng tên `middleware`. Từ Next 16 bộ chặn request là `proxy.ts` và xuất `proxy`.
27
+ **Why:** `<file>` uses the name `middleware`. Since Next 16 the request interceptor is `proxy.ts` and exports `proxy`.
28
28
 
29
- **Cách sửa:** Đổi tên tệp thành `proxy.ts` và đổi export thành `proxy`.
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
- **Vì sao (why):** Tệp `<file>` nằm dưới segment ngôn ngữ không phải `[locale]`. Chỉ có một mẫu: `next-intl`, `[locale]`, mặc định `vi`.
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
- **Cách sửa:** Đổi tên thư mục segment thành `[locale]`.
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
- **Vì sao (why):** `<file>` nhập một thư viện i18n thứ hai. App chỉ dùng `next-intl` qua `modules/i18n`.
71
+ **Why:** `<file>` imports a second i18n library. The app uses only `next-intl` through `modules/i18n`.
72
72
 
73
- **Cách sửa:** Chuyển sang `next-intl`; xóa thư viện còn lại.
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
- **Vì sao (why):** `<html lang>` ở `<file>` là chữ cứng. Thuộc tính `lang` phải lấy từ segment `[locale]`.
93
+ **Why:** `<html lang>` in `<file>` is hardcoded. The `lang` attribute must come from the `[locale]` segment.
94
94
 
95
- **Cách sửa:** Dùng `lang={locale}` từ tham số của layout `[locale]`.
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
- **Vì sao (why):** `<file>` là `page.tsx` nhưng không export `metadata` hoặc `generateMetadata`; mọi trang trong nhánh mang cùng một tiêu đề.
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
- **Cách sửa:** Export `metadata` (hoặc `generateMetadata` khi tiêu đề lấy từ dữ liệu) với tiêu đề và mô tả lấy từ catalog thông điệp.
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
- **Vì sao (why):** `<Suspense>` ở `<file>` có `fallback` rỗng: người dùng thấy khoảng trống thay vì trạng thái đang tải.
138
+ **Why:** `<Suspense>` in `<file>` has an empty `fallback`: the user sees a blank space instead of a loading state.
139
139
 
140
- **Cách sửa:** Truyền skeleton (trạng thái loading) của đúng thứ đang tải làm `fallback`.
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
- **Vì sao (why):** `<file>` nhập `Link`, `useRouter`, `usePathname` hoặc `redirect` từ Next, không biết locale: đường dẫn mất tiền tố `[locale]`.
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
- **Cách sửa:** Nhập từ `modules/i18n/navigation`, được next-intl dựng từ `routing.ts`.
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
- **Vì sao (why):** `<a>` ở `<file>` trỏ route nội bộ (tải lại cả trang, mất locale, mất prefetch) hoặc mở tab mới mà không có `rel`.
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
- **Cách sửa:** Dùng `Link` từ `modules/i18n/navigation` với href dựng bởi `modules/routes`; liên kết `_blank` thêm `rel="noopener noreferrer"`.
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
- **Vì sao (why):** `<file>` viết thẳng đường dẫn route ở nơi gọi; route đổi chỗ thì bản sao trỏ vào 404.
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
- **Cách sửa:** Dựng href bằng hàm của `modules/routes` và dùng đúng hàm đó ở mọi nơi.
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
- **Vì sao (why):** `<file>` tự dựng một tầng của stack next-intl (`defineRouting`, `createNavigation`, `getRequestConfig`, `createMiddleware`). Stack chỉ được viết một lần cho cả repo.
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
- **Cách sửa:** Gọi factory `createAppI18n` của gói i18n dùng chung trong `modules/i18n/index.ts` và nhập kết quả; repo một app thì chỉ viết stack trong `modules/i18n` của app đó.
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`.