@starci/eslint-canon-fe 4.0.0 → 6.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 +111 -0
- package/README.md +33 -16
- package/brand-values.mjs +172 -0
- package/class-names.mjs +9 -9
- package/client-boundary.mjs +287 -0
- package/comments.mjs +137 -190
- package/docs/brand-values.md +30 -0
- package/docs/client-boundary.md +124 -0
- package/docs/e2e-shape.md +161 -0
- package/docs/env-owner.md +52 -0
- package/docs/formatting.md +30 -0
- package/docs/grammar-boundary.md +33 -0
- package/docs/hooks-folder.md +29 -0
- package/docs/hygiene.md +122 -0
- package/docs/lint-escape-hatch.md +34 -0
- package/docs/lists.md +74 -0
- package/docs/native-controls.md +73 -0
- package/docs/next-conventions.md +232 -0
- package/docs/size-and-state-budget.md +73 -0
- package/docs/size-growth.md +29 -0
- package/docs/spec-quality.md +147 -0
- package/docs/status-colors.md +32 -0
- package/docs/translation.md +54 -0
- package/docs/transport.md +195 -0
- package/docs/type-safety.md +95 -0
- package/e2e-shape.mjs +345 -0
- package/env-owner.mjs +134 -0
- package/file-layout.mjs +105 -104
- package/formatting.mjs +104 -0
- package/grammar-boundary.mjs +85 -9
- package/hooks-folder.mjs +120 -0
- package/hygiene.mjs +513 -0
- package/icon.mjs +212 -212
- package/index.mjs +243 -142
- package/landmark.mjs +0 -11
- package/lib/ast.mjs +94 -0
- package/lib/bindings.mjs +144 -0
- package/lib/config.mjs +79 -0
- package/lib/hfs.mjs +48 -0
- package/lib/next.mjs +6 -0
- package/lib/path.mjs +2 -0
- package/lib/platform.mjs +66 -0
- package/lib/react.mjs +28 -0
- package/lib/scope.mjs +142 -0
- package/lib/status-tones.generated.mjs +9 -0
- package/lib/types.mjs +63 -0
- package/lib/why.mjs +334 -0
- package/lint-escape-hatch.mjs +93 -56
- package/lists.mjs +197 -0
- package/loading.mjs +156 -164
- package/naming.mjs +149 -143
- package/native-controls.mjs +112 -0
- package/next-conventions.mjs +430 -0
- package/package.json +15 -6
- package/props-and-slots.mjs +19 -13
- package/runtime/engine/plain-object.mjs +5 -0
- package/runtime/engine/runtime-root.mjs +32 -0
- package/runtime/engine/yaml.mjs +161 -0
- package/runtime/knowledge/hfs/slots.yaml +1072 -0
- package/runtime/scripts/lib/git.mjs +53 -0
- package/runtime/scripts/lib/glob.mjs +23 -0
- package/runtime/scripts/lib/hfs-allows.mjs +42 -0
- package/runtime/scripts/lib/hfs-slots.mjs +722 -0
- package/runtime/scripts/lib/hfs-view.mjs +68 -0
- package/runtime/scripts/lib/next-contract.mjs +14 -0
- package/runtime/scripts/lib/path-key.mjs +17 -0
- package/runtime/scripts/lib/recorded-lines.mjs +27 -0
- package/served-locale.mjs +174 -160
- package/shape-slot.mjs +22 -21
- package/size-and-state-budget.mjs +214 -0
- package/size-growth.mjs +58 -0
- package/spec-quality.mjs +318 -0
- package/status-colors.mjs +139 -0
- package/the-split.mjs +133 -134
- package/tokens.mjs +259 -268
- package/translation.mjs +295 -122
- package/transport.mjs +752 -0
- package/type-safety.mjs +147 -74
- package/typography.mjs +78 -83
- package/vendor-boundary.mjs +5 -27
- package/lint-adoption.mjs +0 -116
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# No raw brand values in TypeScript
|
|
2
|
+
|
|
3
|
+
Law module: `brand-values.mjs`. Catalogue: R61 FE_STYLE_TOKEN_ONLY (TypeScript half; CSS belongs to @starci/stylelint-canon).
|
|
4
|
+
|
|
5
|
+
Colour values exist only in `modules/brand/brand.css`, on the tokens the grammar declares, with a light and a dark value. A hex, a colour function, a named paint colour or a pixel length in TypeScript skips the token scale.
|
|
6
|
+
|
|
7
|
+
Every rule below is an error in `starciFeConfig`; none can be switched off or suppressed inline.
|
|
8
|
+
|
|
9
|
+
## `starci-fe/no-raw-brand-value`
|
|
10
|
+
|
|
11
|
+
No hex, colour function, named paint colour or px length in TypeScript; tokens only.
|
|
12
|
+
|
|
13
|
+
**Invalid** (`src/components/blocks/Feed/index.tsx`)
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
const c = "#ff0000"
|
|
17
|
+
<div style={{ width: 12 }} />
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Valid** (`src/components/blocks/Feed/index.tsx`)
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
<div className="bg-surface p-4" />
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Finding code:** `FE_STYLE_TOKEN_ONLY`
|
|
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`.
|
|
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`.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Client boundary
|
|
2
|
+
|
|
3
|
+
Law module: `client-boundary.mjs`. Catalogue: R55 FE_CLIENT_BOUNDARY.
|
|
4
|
+
|
|
5
|
+
Server first. `"use client"` appears only on the `index.tsx` of an interactive block (or overlay), on a leaf whose interaction is intrinsic, on a branch or leaf of a workspace package (`packages/<pkg>/src/{branches,leaves}/`, where the grammar tiers sit directly under `src/`), and on `error.tsx` / `global-error.tsx`, which Next requires to be client components. Routes, pages and layouts are server components.
|
|
6
|
+
|
|
7
|
+
Every rule below is an error in `starciFeConfig`; none can be switched off or suppressed inline.
|
|
8
|
+
|
|
9
|
+
## `starci-fe/use-client-only-at-boundary`
|
|
10
|
+
|
|
11
|
+
`"use client"` appears only at a block index, a leaf, or a Next error boundary.
|
|
12
|
+
|
|
13
|
+
**Invalid** (`src/app/[locale]/layout.tsx`)
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
"use client"
|
|
17
|
+
export default function Layout() { return null }
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Valid** (`src/components/blocks/Feed/index.tsx`)
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
"use client"
|
|
24
|
+
export const Feed = () => null
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Finding code:** `FE_CLIENT_BOUNDARY`
|
|
28
|
+
|
|
29
|
+
**Vì sao (why):** `"use client"` ở `<file>` (`<slot>` không được là client component).
|
|
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.
|
|
32
|
+
|
|
33
|
+
## `starci-fe/client-no-server-import`
|
|
34
|
+
|
|
35
|
+
A `"use client"` file does not import `server-only`, `next/headers`, `next/server`, `next-intl/server`, a Node built-in or a server reader (sub-check `FE_CLIENT_SERVER_IMPORT`).
|
|
36
|
+
|
|
37
|
+
**Invalid** (`src/components/blocks/Feed/index.tsx`)
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
"use client"
|
|
41
|
+
import { cookies } from "next/headers"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Valid** (`src/components/blocks/Feed/index.tsx`)
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
"use client"
|
|
48
|
+
import useSWR from "swr"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Finding code:** `FE_CLIENT_SERVER_IMPORT`
|
|
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).
|
|
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.
|
|
56
|
+
|
|
57
|
+
## `starci-fe/server-module-marks-server-only`
|
|
58
|
+
|
|
59
|
+
A module that imports `next/headers`, `next/server` (a value import), `next-intl/server`, a Node built-in, or a module that is itself server-only starts with `import "server-only"` (sub-check `FE_SERVER_ONLY_MARK`). A module is server-only when the TypeScript module resolution of the import specifier reaches a source file whose first statement is `import "server-only"`; `import type` and `import { type X }` carry no runtime and are not judged. Route files are server components by construction and need no marker: they are recognized by their slot (`fe.route`, `fe.source-root-pinned`), not by their name. A `"use client"` file that imports the server is `client-no-server-import`'s finding. The multi-hop client reachability (a client block reaching a marked module through a hook) is the architecture machine's.
|
|
60
|
+
|
|
61
|
+
**Invalid** (`src/modules/api/courses/read-courses.ts`)
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { headers } from "next/headers"
|
|
65
|
+
export const readCourses = async () => (await headers()).get("x-locale")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Valid** (`src/modules/api/courses/read-courses.ts`)
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import "server-only"
|
|
72
|
+
import { headers } from "next/headers"
|
|
73
|
+
export const readCourses = async () => (await headers()).get("x-locale")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Finding code:** `FE_SERVER_ONLY_MARK`
|
|
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"`.
|
|
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.
|
|
81
|
+
|
|
82
|
+
## `starci-fe/web-storage-only-in-modules`
|
|
83
|
+
|
|
84
|
+
`localStorage` and `sessionStorage` are touched only inside `modules/` (sub-check `FE_STORAGE_OUTSIDE_MODULES`).
|
|
85
|
+
|
|
86
|
+
**Invalid** (`src/components/blocks/Wallet/index.tsx`)
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
const raw = sessionStorage.getItem(KEY)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Valid** (`src/components/blocks/Wallet/index.tsx`)
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
const draft = useTopUpDraft()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Finding code:** `FE_STORAGE_OUTSIDE_MODULES`
|
|
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.
|
|
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.
|
|
103
|
+
|
|
104
|
+
## `starci-fe/no-dangerous-html`
|
|
105
|
+
|
|
106
|
+
`dangerouslySetInnerHTML` only on `<script>`; never on a visible element (sub-check `FE_DANGEROUS_HTML`).
|
|
107
|
+
|
|
108
|
+
**Invalid** (`src/components/blocks/Feed/component.tsx`)
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
<div dangerouslySetInnerHTML={{ __html: html }} />
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Valid** (`src/components/blocks/Feed/component.tsx`)
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON_LD }} />
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Finding code:** `FE_DANGEROUS_HTML`
|
|
121
|
+
|
|
122
|
+
**Vì sao (why):** `dangerouslySetInnerHTML` trên `<tag>` ở `<file>`: chuỗi bất kỳ trở thành HTML chạy được.
|
|
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.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# e2e shape
|
|
2
|
+
|
|
3
|
+
Law module: `e2e-shape.mjs`. Catalogue: R66 FE_E2E_SHAPE.
|
|
4
|
+
|
|
5
|
+
These rules govern `e2e/**` and `playwright.config.*`, not `src/`; `starciFeConfig` attaches them through its second block. Specs are `e2e/<area>/<name>.e2e-spec.ts`; the Playwright projects cover 1440x900, 768x1024 and 390x844; no absolute path, no docker, no write outside the repository, no `test.skip`, and typed helpers.
|
|
6
|
+
|
|
7
|
+
Every rule below is an error in `starciFeConfig`; none can be switched off or suppressed inline.
|
|
8
|
+
|
|
9
|
+
## `starci-fe/e2e-spec-location`
|
|
10
|
+
|
|
11
|
+
An e2e spec is `e2e/<area>/<name>.e2e-spec.ts`; helpers live under `support/` or `fixtures/`.
|
|
12
|
+
|
|
13
|
+
**Invalid** (`e2e/play.spec.ts`)
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
// file: e2e/play.spec.ts
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Valid** (`e2e/course/play.e2e-spec.ts`)
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// file: e2e/course/play.e2e-spec.ts
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Finding code:** `FE_E2E_SHAPE`
|
|
26
|
+
|
|
27
|
+
**Vì sao (why):** Spec e2e `<file>` không nằm ở `e2e/<area>/<name>.e2e-spec.ts`.
|
|
28
|
+
|
|
29
|
+
**Cách sửa:** Đưa spec vào `e2e/<area>/` với hậu tố `.e2e-spec.ts`; helper vào `e2e/support/` hoặc `e2e/fixtures/`.
|
|
30
|
+
|
|
31
|
+
## `starci-fe/e2e-no-absolute-path`
|
|
32
|
+
|
|
33
|
+
No absolute filesystem path in e2e source.
|
|
34
|
+
|
|
35
|
+
**Invalid** (`e2e/support/session.ts`)
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
const p = "D:/repos/nivo-backend/out"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Valid** (`e2e/support/session.ts`)
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
const p = path.join(__dirname, "fixtures", "a.png")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Finding code:** `FE_E2E_SHAPE`
|
|
48
|
+
|
|
49
|
+
**Vì sao (why):** Spec e2e `<file>` dùng đường dẫn tuyệt đối.
|
|
50
|
+
|
|
51
|
+
**Cách sửa:** Dựng đường dẫn từ cấu hình hoặc thư mục của chính spec.
|
|
52
|
+
|
|
53
|
+
## `starci-fe/e2e-no-docker`
|
|
54
|
+
|
|
55
|
+
An e2e suite does not start or require Docker.
|
|
56
|
+
|
|
57
|
+
**Invalid** (`e2e/support/stack.ts`)
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
execSync("docker compose up -d")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Valid** (`e2e/course/play.e2e-spec.ts`)
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
await page.goto(baseUrl)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Finding code:** `FE_E2E_SHAPE`
|
|
70
|
+
|
|
71
|
+
**Vì sao (why):** Spec e2e `<file>` phụ thuộc vào docker.
|
|
72
|
+
|
|
73
|
+
**Cách sửa:** Trỏ suite tới một URL từ cấu hình; môi trường tự cung cấp stack.
|
|
74
|
+
|
|
75
|
+
## `starci-fe/e2e-no-cross-repo-write`
|
|
76
|
+
|
|
77
|
+
An e2e file write never targets an absolute path or climbs out of the repository.
|
|
78
|
+
|
|
79
|
+
**Invalid** (`e2e/support/seed.ts`)
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
await fs.writeFile("../backend/seed.json", data)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Valid** (`e2e/support/seed.ts`)
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
await fs.writeFile(path.join(outDir, "report.json"), data)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Finding code:** `FE_E2E_SHAPE`
|
|
92
|
+
|
|
93
|
+
**Vì sao (why):** Spec e2e `<file>` ghi ra ngoài repo của app (đường dẫn tuyệt đối hoặc `..`).
|
|
94
|
+
|
|
95
|
+
**Cách sửa:** Chỉ ghi vào thư mục kết quả của chính repo này.
|
|
96
|
+
|
|
97
|
+
## `starci-fe/e2e-no-skip`
|
|
98
|
+
|
|
99
|
+
No `test.skip`, `test.fixme` or `describe.skip`: a missing environment fails the run.
|
|
100
|
+
|
|
101
|
+
**Invalid** (`e2e/course/play.e2e-spec.ts`)
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
test.skip(!process.env.BASE_URL, "needs env")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Valid** (`e2e/course/play.e2e-spec.ts`)
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
test("plays", async ({ page }) => {})
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Finding code:** `FE_E2E_SHAPE`
|
|
114
|
+
|
|
115
|
+
**Vì sao (why):** Spec e2e `<file>` bỏ qua test (`test.skip`/`fixme`) — thiếu môi trường phải làm suite đỏ, không được xanh.
|
|
116
|
+
|
|
117
|
+
**Cách sửa:** Sửa môi trường hoặc xóa test; không bỏ qua có điều kiện.
|
|
118
|
+
|
|
119
|
+
## `starci-fe/e2e-typed-helpers`
|
|
120
|
+
|
|
121
|
+
e2e helper parameters are typed and nothing is `any`.
|
|
122
|
+
|
|
123
|
+
**Invalid** (`e2e/support/session.ts`)
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
export const login = async (page, role) => page
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Valid** (`e2e/support/session.ts`)
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
export const login = async (page: Page, role: Role) => page
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Finding code:** `FE_E2E_SHAPE`
|
|
136
|
+
|
|
137
|
+
**Vì sao (why):** Helper e2e ở `<file>` có tham số không kiểu hoặc dùng `any`.
|
|
138
|
+
|
|
139
|
+
**Cách sửa:** Khai kiểu cho mọi tham số của helper; thay `any` bằng kiểu thật hoặc `unknown` kèm kiểm tra.
|
|
140
|
+
|
|
141
|
+
## `starci-fe/playwright-viewports`
|
|
142
|
+
|
|
143
|
+
playwright.config declares the viewports 1440x900, 768x1024, 390x844 and no others.
|
|
144
|
+
|
|
145
|
+
**Invalid** (`playwright.config.ts`)
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
projects: [{ use: { viewport: { width: 1440, height: 900 } } }]
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Valid** (`playwright.config.ts`)
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
projects: three, each with one of 1440x900, 768x1024, 390x844
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Finding code:** `FE_E2E_SHAPE`
|
|
158
|
+
|
|
159
|
+
**Vì sao (why):** `playwright.config` không khai đúng ba viewport 1440x900, 768x1024, 390x844.
|
|
160
|
+
|
|
161
|
+
**Cách sửa:** Mỗi project khai một viewport; đủ ba, không thêm cỡ thứ tư.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Environment owner
|
|
2
|
+
|
|
3
|
+
Law module: `env-owner.mjs`. Catalogue: R49 FE_ENV_OWNER.
|
|
4
|
+
|
|
5
|
+
The environment is read in exactly one place, `modules/config`, and nothing there has a literal default that stands in for a real deployment value. A missing production variable must fail when the config module loads, not point production at a developer's machine.
|
|
6
|
+
|
|
7
|
+
Every rule below is an error in `starciFeConfig`; none can be switched off or suppressed inline.
|
|
8
|
+
|
|
9
|
+
## `starci-fe/no-env-outside-config`
|
|
10
|
+
|
|
11
|
+
`process.env` and `import.meta.env` are read only in `modules/config`.
|
|
12
|
+
|
|
13
|
+
**Invalid** (`src/hooks/session/useSession.ts`)
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
const url = process.env.API_URL
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Valid** (`src/hooks/session/useSession.ts`)
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { config } from "@/modules/config"
|
|
23
|
+
const url = config.apiUrl
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Finding code:** `FE_ENV_OWNER`
|
|
27
|
+
|
|
28
|
+
**Vì sao (why):** `<file>` đọc biến môi trường. Chỉ `modules/config` được đọc env.
|
|
29
|
+
|
|
30
|
+
**Cách sửa:** Đọc giá trị từ export có kiểu của `modules/config` thay vì `process.env`.
|
|
31
|
+
|
|
32
|
+
## `starci-fe/no-hardcoded-endpoint-fallback`
|
|
33
|
+
|
|
34
|
+
No URL literal as a fallback, and no literal default for an address or secret variable.
|
|
35
|
+
|
|
36
|
+
**Invalid** (`src/modules/config/env.ts`)
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const url = process.env.API_URL ?? "http://localhost:3068"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Valid** (`src/modules/config/env.ts`)
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const url = process.env.API_URL
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Finding code:** `FE_ENV_OWNER`
|
|
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.
|
|
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.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Formatting numbers, money and dates
|
|
2
|
+
|
|
3
|
+
Law module: `formatting.mjs`. Catalogue: R59 FE_I18N_PLACEMENT, sub-check `FE_I18N_FORMATTER`.
|
|
4
|
+
|
|
5
|
+
`next-intl` owns the reader's locale and time zone and hands them to `useFormatter()` / `getFormatter()`. `toLocale*String`, `new Intl.*`, displayed `toFixed`, a currency symbol glued to a template substitution and the date libraries either guess the locale (server and browser disagree, so a price flickers on hydration) or hard-code one. `modules/i18n/**` is exempt: it is where formats are configured.
|
|
6
|
+
|
|
7
|
+
Every rule below is an error in `starciFeConfig`; none can be switched off or suppressed inline.
|
|
8
|
+
|
|
9
|
+
## `starci-fe/use-intl-formatter`
|
|
10
|
+
|
|
11
|
+
Format numbers, money and dates with `useFormatter` / `getFormatter` from `next-intl`.
|
|
12
|
+
|
|
13
|
+
**Invalid** (`src/components/blocks/Feed/component.tsx`)
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
<p>{new Date(at).toLocaleString()}</p>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Valid** (`src/components/blocks/Feed/component.tsx`)
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
const format = useFormatter()
|
|
23
|
+
<p>{format.dateTime(new Date(at), { dateStyle: "medium" })}</p>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Finding code:** `FE_I18N_FORMATTER`
|
|
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.
|
|
29
|
+
|
|
30
|
+
**Cách sửa:** Dùng `useFormatter()` (hoặc `getFormatter()` ở server): `number(...)`, `dateTime(...)`, `relativeTime(...)`.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Grammar boundary
|
|
2
|
+
|
|
3
|
+
Law module: `grammar-boundary.mjs`. Catalogue: R62 FE_NATIVE_FORM_CONTROL (the grammar owns the element).
|
|
4
|
+
|
|
5
|
+
Page structure and text are composed from `@starci/grammar` (or the repository's `<family>-ui` package), not drawn as raw HTML.
|
|
6
|
+
|
|
7
|
+
Every rule below is an error in `starciFeConfig`; none can be switched off or suppressed inline.
|
|
8
|
+
|
|
9
|
+
## `starci-fe/no-raw-structural-element`
|
|
10
|
+
|
|
11
|
+
Page structure and text are grammar components, not raw `div`-family HTML; only `fe.package.ui` renderers draw raw tags.
|
|
12
|
+
|
|
13
|
+
Applies to app source that composes the grammar (slots `fe.feature`, `fe.components`, `fe.route`, `fe.hooks`) and to every workspace package except the `<family>-ui` package (`fe.package.ui`), whose components are the renderers. Spec files render raw HTML as a harness and are not judged; a file no slot owns is not judged.
|
|
14
|
+
|
|
15
|
+
The refused tags are exactly those `@starci/grammar` has a component for (the twin test reads the package source): `h1-h6` (`Heading`), `p` and `span` (`Text`), `hr` (`Divider`), `label` (`Label`), `form` (`Form`), `fieldset` (`Fieldset`), `dl` `dt` `dd` (`DescriptionList`), `table` `thead` `tbody` `tr` `td` `th` (`DataTable`), `ul` `ol` `li` (`SurfaceListCard`, `StaticStateRow`), `header` (`SectionHeader`, `TopBar`), `footer` (`Footer`), `nav` (`Subnav`, `Breadcrumbs`, `NavigationFeatureNav`), `section` (`SurfaceCard`), `main` (`WorkspaceShell`), `figure` and `figcaption` (`MediaFrame`). `div`, `br`, `aside` and `article` are not refused: the grammar has no component for them.
|
|
16
|
+
|
|
17
|
+
**Invalid** (`src/features/pages/home/component.tsx`)
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
export const Home = () => <section><p>{t("intro")}</p></section>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Valid** (`src/features/pages/home/component.tsx`)
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
export const Home = () => <SurfaceCard><Text>{t("intro")}</Text></SurfaceCard>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Finding code:** `FE_NATIVE_FORM_CONTROL`
|
|
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ô.
|
|
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ỗ.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# hooks/ holds hooks
|
|
2
|
+
|
|
3
|
+
Law module: `hooks-folder.mjs`. Catalogue: R56 FE_HOOKS_ARE_HOOKS (lint half).
|
|
4
|
+
|
|
5
|
+
`hooks/<domain>/use<Name>.ts` holds one hook per file; `hooks/<domain>/<domain>.shared.ts` holds the domain's non-hook helpers; server readers live in `modules/api/<domain>/read-*.ts`. Whether a folder has exactly one shared file and whether a helper name repeats across files belongs to the architecture machine.
|
|
6
|
+
|
|
7
|
+
Every rule below is an error in `starciFeConfig`; none can be switched off or suppressed inline.
|
|
8
|
+
|
|
9
|
+
## `starci-fe/hooks-folder-holds-hooks-only`
|
|
10
|
+
|
|
11
|
+
`hooks/<domain>/` holds `use*.ts` hooks and one `<domain>.shared.ts`; nothing else.
|
|
12
|
+
|
|
13
|
+
**Invalid** (`src/hooks/course/readCourse.ts`)
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
export const readCourse = () => 1
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Valid** (`src/hooks/course/useCourse.ts`)
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
export const useCourse = () => 1
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Finding code:** `FE_HOOKS_ARE_HOOKS`
|
|
26
|
+
|
|
27
|
+
**Vì sao (why):** `<file>` trong `hooks/` không phải React hook. Reader server về `modules/api`, helper về `<domain>.shared.ts`.
|
|
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`.
|
package/docs/hygiene.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Runtime hygiene
|
|
2
|
+
|
|
3
|
+
Law module: `hygiene.mjs`. Catalogue: R65 FE_SIZE_AND_STATE_BUDGET (sub-checks `FE_EFFECT_CLEANUP`, `FE_SWALLOWED_ERROR`, `FE_CONSOLE_CALL`) and R50 FE_TRANSPORT_OWNER (sub-check `FE_EFFECT_FETCH`).
|
|
4
|
+
|
|
5
|
+
Work started by a component that nobody owns the end of: a timer, listener, observer, socket or subscription that outlives the component, a fetch in an effect with no cache or cancel, an empty `catch` that hides the failure, a `console` call no pipeline reads. Every rule here decides by resolution: an effect is a call to React's own `useEffect` / `useLayoutEffect` export (the import is followed), the timer or listener is the platform's when its symbol is declared by the default library, "the same handle" is the same scope binding (or member path such as `timerRef.current`), and "a promise" is what the type checker says. There is no folder exemption: a `modules/` file is judged by its structure like any other (the API client starts its timeout timer and clears it in `finally`, in the same function).
|
|
6
|
+
|
|
7
|
+
Every rule below is an error in `starciFeConfig`; none can be switched off or suppressed inline.
|
|
8
|
+
|
|
9
|
+
## `starci-fe/effect-subscription-needs-cleanup`
|
|
10
|
+
|
|
11
|
+
Inside a `useEffect` / `useLayoutEffect` callback and inside the subscribe function of `useSyncExternalStore`, everything that keeps running after the callback returns is released by the cleanup the callback returns, against the same handle, target and listener:
|
|
12
|
+
|
|
13
|
+
| Started | Released by the returned cleanup |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| `setTimeout` / `setInterval` (the handle) | `clearTimeout(id)` / `clearInterval(id)` on the same handle (variable, ref member, module `let`, or a list drained with `forEach(clearTimeout)`) |
|
|
16
|
+
| `requestAnimationFrame` (the id) | `cancelAnimationFrame(id)` |
|
|
17
|
+
| `target.addEventListener(type, listener)` | `target.removeEventListener(type, listener)` with the same target, type and listener binding; an inline listener cannot be removed; or `{ signal }` with `controller.abort()` |
|
|
18
|
+
| `new ResizeObserver` / `IntersectionObserver` / `MutationObserver` that is `.observe`d | `observer.disconnect()` |
|
|
19
|
+
| `new WebSocket` / `EventSource` / `BroadcastChannel` | `connection.close()` |
|
|
20
|
+
| a call returning a subscription object (its type has `unsubscribe`, `off` or `close`) | that method on the stored object |
|
|
21
|
+
|
|
22
|
+
The cleanup may be a returned function, a named function that is returned, or may call a same-file function that releases. Outside an effect, a timer or frame is accepted only when the function that starts it also releases it (the API client's `try { ... } finally { clearTimeout(id) }`, the `subscribe` that returns its own unsubscribe); a listener started outside an effect is not judged. A function-returning unsubscribe (`const off = on(handler)`) is not decided here: the checker cannot tell it from any other callback.
|
|
23
|
+
|
|
24
|
+
**Invalid** (`src/components/blocks/Feed/component.tsx`)
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
const onClick = () => { setTimeout(() => setOpen(false), 300) }
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Valid** (`src/components/blocks/Feed/component.tsx`)
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const id = setTimeout(() => setOpen(false), 300)
|
|
35
|
+
return () => clearTimeout(id)
|
|
36
|
+
}, [])
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Valid** also: a function that starts the timer and directly returns a cleanup clearing the same handle (the `subscribe` of `useSyncExternalStore`)
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
const subscribe = (listener: () => void) => {
|
|
43
|
+
listeners.add(listener)
|
|
44
|
+
if (timer === undefined) timer = setInterval(tick, 60_000)
|
|
45
|
+
return () => {
|
|
46
|
+
listeners.delete(listener)
|
|
47
|
+
if (listeners.size === 0) { clearInterval(timer); timer = undefined }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Finding code:** `FE_EFFECT_CLEANUP`
|
|
53
|
+
|
|
54
|
+
**Vì sao (why):** `<timer>` ở `<file>` chạy ngoài effect hoặc trong effect không có cleanup gọi `clearTimeout`/`clearInterval`.
|
|
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 đó.
|
|
57
|
+
|
|
58
|
+
## `starci-fe/no-data-fetch-in-effect`
|
|
59
|
+
|
|
60
|
+
An effect body starts no promise: no `await`, `fetch`, `.then`, `void load()`, `mutate()` or `refresh()`; read through SWR or a server reader. The call is judged when it runs synchronously in the body (an immediately-invoked function counts); a promise started inside a listener or timer callback the effect registers, or in the cleanup, is that callback's work. A call is a load when its type is a promise, when it is `.then`-ed, or when it is `void`-ed and untyped; a platform promise other than `fetch` (`audio.play()`, `clipboard.writeText()`) is not a data load. Data freshness comes from the SWR key: an SWR `mutate()` or `refresh()` called directly in the effect body is a finding too; run it from the event that changed the data (a handler, a socket message).
|
|
61
|
+
|
|
62
|
+
**Invalid** (`src/components/blocks/Feed/component.tsx`)
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
useEffect(() => { fetch(url).then(setData) }, [url])
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Valid** (`src/components/blocks/Feed/component.tsx`)
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
const { data } = useSWR(key, fetcher)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Finding code:** `FE_EFFECT_FETCH`
|
|
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ỷ.
|
|
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`.
|
|
79
|
+
|
|
80
|
+
## `starci-fe/no-empty-catch`
|
|
81
|
+
|
|
82
|
+
A `catch` that does nothing hides the failure; handle it or return a typed outcome.
|
|
83
|
+
|
|
84
|
+
**Invalid** (`src/components/blocks/Feed/component.tsx`)
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
try { await save() } catch {}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Valid** (`src/components/blocks/Feed/component.tsx`)
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
try { await save() } catch (error) { return { kind: "unavailable", cause: error } }
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Finding code:** `FE_SWALLOWED_ERROR`
|
|
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.
|
|
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.
|
|
101
|
+
|
|
102
|
+
## `starci-fe/no-console`
|
|
103
|
+
|
|
104
|
+
No `console.*` call in product source.
|
|
105
|
+
|
|
106
|
+
**Invalid** (`src/components/blocks/Feed/component.tsx`)
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
console.error(error)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Valid** (`src/components/blocks/Feed/component.tsx`)
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
return { kind: "unavailable", cause: error }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Finding code:** `FE_CONSOLE_CALL`
|
|
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.
|
|
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`.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# No inline suppression of any spelling
|
|
2
|
+
|
|
3
|
+
Law module: `lint-escape-hatch.mjs`. Catalogue: R18 HFS_INLINE_SUPPRESSION.
|
|
4
|
+
|
|
5
|
+
No `eslint-disable` (line, next-line, block), `eslint-enable`, `eslint-env`, inline `eslint rule: ...` config, `@ts-ignore`, `@ts-expect-error`, `@ts-nocheck`, no retired `vn-ok:` pragma, and no `NOSONAR`, `@sonar-ignore`, `istanbul ignore`, `c8 ignore`, `v8 ignore`, `prettier-ignore` or `stylelint-disable` comment. `noInlineConfig` makes a directive ineffective, this rule makes it a finding, and `reportUnusedDisableDirectives` reports a leftover that suppresses nothing. The rule covers `src/**`, specs and the e2e tree.
|
|
6
|
+
|
|
7
|
+
Every rule below is an error in `starciFeConfig`; none can be switched off or suppressed inline.
|
|
8
|
+
|
|
9
|
+
## `starci-fe/no-inline-lint-config`
|
|
10
|
+
|
|
11
|
+
Source cannot change its own lint or type-check policy, and the vn-ok pragma is retired.
|
|
12
|
+
|
|
13
|
+
**Invalid** (`src/components/blocks/Feed/index.tsx`)
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
// eslint-disable-next-line no-console
|
|
17
|
+
// @ts-ignore
|
|
18
|
+
// NOSONAR
|
|
19
|
+
/* istanbul ignore next */
|
|
20
|
+
const s = "x" // vn-ok: reason
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Valid** (`src/components/blocks/Feed/index.tsx`)
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
// the rule is fixed centrally; fix the code instead
|
|
27
|
+
const s = 1
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Finding code:** `HFS_INLINE_SUPPRESSION`
|
|
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.
|
|
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.
|