@starci/eslint-canon-fe 3.0.2 → 5.1.2
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 +35 -0
- package/README.md +41 -16
- package/brand-values.mjs +174 -0
- package/class-names.mjs +1 -1
- package/client-boundary.mjs +199 -0
- package/comments.mjs +144 -190
- package/docs/brand-values.md +30 -0
- package/docs/client-boundary.md +99 -0
- package/docs/e2e-shape.md +161 -0
- package/docs/env-owner.md +52 -0
- package/docs/formatting.md +30 -0
- package/docs/hooks-folder.md +29 -0
- package/docs/hygiene.md +111 -0
- package/docs/lint-escape-hatch.md +32 -0
- package/docs/lists.md +74 -0
- package/docs/native-controls.md +73 -0
- package/docs/next-conventions.md +206 -0
- package/docs/size-and-state-budget.md +73 -0
- package/docs/spec-quality.md +139 -0
- package/docs/translation.md +50 -0
- package/docs/transport.md +168 -0
- package/docs/type-safety.md +95 -0
- package/e2e-shape.mjs +348 -0
- package/env-owner.mjs +134 -0
- package/file-layout.mjs +100 -7
- package/formatting.mjs +106 -0
- package/hooks-folder.mjs +119 -0
- package/hygiene.mjs +235 -0
- package/icon.mjs +210 -211
- package/index.mjs +292 -131
- package/lib/ast.mjs +94 -0
- package/lib/path.mjs +2 -0
- package/lib/scope.mjs +42 -0
- package/lib/why.mjs +304 -0
- package/lint-adoption.mjs +15 -52
- package/lint-escape-hatch.mjs +91 -56
- package/lists.mjs +197 -0
- package/loading.mjs +163 -164
- package/naming.mjs +143 -143
- package/native-controls.mjs +112 -0
- package/next-conventions.mjs +346 -0
- package/package.json +7 -3
- package/props-and-slots.mjs +4 -2
- package/served-locale.mjs +160 -160
- package/shape-slot.mjs +339 -0
- package/size-and-state-budget.mjs +196 -0
- package/spec-quality.mjs +239 -0
- package/the-split.mjs +133 -134
- package/tokens.mjs +267 -268
- package/translation.mjs +262 -122
- package/transport.mjs +404 -0
- package/type-safety.mjs +156 -74
- package/typography.mjs +82 -83
package/comments.mjs
CHANGED
|
@@ -1,190 +1,144 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The rules that hold `comments.md`.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
/** The
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
/\/
|
|
46
|
-
/\/
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
/\.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
if (!declaration) return
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
return proseVisitors(context, (node, text) => {
|
|
147
|
-
if (!text || !SECOND_LANGUAGE_LETTER.test(text)) return
|
|
148
|
-
if (ENDONYM.test(text) || OK_PRAGMA.test(text)) return
|
|
149
|
-
if (marked.has(node.loc.start.line)) return
|
|
150
|
-
context.report({ node, messageId: "second" })
|
|
151
|
-
})
|
|
152
|
-
},
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// -- COMMENTS-4 ------------------------------------------------------------------------------------
|
|
156
|
-
|
|
157
|
-
/** No Unicode emoji in source: stable product reactions use attributed checked-in SVG artwork. */
|
|
158
|
-
export const noEmojiInSource = {
|
|
159
|
-
meta: {
|
|
160
|
-
type: "problem",
|
|
161
|
-
docs: { description: "No emoji in source authoring." },
|
|
162
|
-
schema: [],
|
|
163
|
-
messages: {
|
|
164
|
-
emoji:
|
|
165
|
-
"Unicode emoji in source authoring. It renders differently on every platform, sorts unpredictably, breaks a terminal that is not expecting it, and does not mean the same thing in two countries. Generic marks belong to the icon vocabulary; product reactions use the attributed checked-in SVG artwork through the reaction leaf.",
|
|
166
|
-
},
|
|
167
|
-
},
|
|
168
|
-
create(context) {
|
|
169
|
-
if (isContentFile(context.filename || context.getFilename())) return {}
|
|
170
|
-
return proseVisitors(context, (node, text) => {
|
|
171
|
-
if (hasEmoji(text)) context.report({ node, messageId: "emoji" })
|
|
172
|
-
})
|
|
173
|
-
},
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/** The rules this law contributes to the plugin. */
|
|
177
|
-
export const rules = {
|
|
178
|
-
"require-export-jsdoc": requireExportJsdoc,
|
|
179
|
-
"no-second-language-in-source": noSecondLanguageInSource,
|
|
180
|
-
"no-emoji-in-source": noEmojiInSource,
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* The level this law asks for, as the plugin's own opinion.
|
|
185
|
-
*
|
|
186
|
-
* `require-export-jsdoc` is the one a repository adopting this with history should expect a count
|
|
187
|
-
* from - every undocumented export in the tree reports at once. The other two are usually already
|
|
188
|
-
* at zero, because a second language in source is noticed by readers long before a rule arrives.
|
|
189
|
-
*/
|
|
190
|
-
export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]))
|
|
1
|
+
/**
|
|
2
|
+
* The rules that hold `comments.md`.
|
|
3
|
+
*
|
|
4
|
+
* Both reach further than their names suggest, and deliberately. An emoji rule that read only JSX
|
|
5
|
+
* would miss the one in a log message, so it walks comments, identifiers, string literals, template
|
|
6
|
+
* chunks and JSX text alike - everywhere prose can hide in a source file.
|
|
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.
|
|
13
|
+
*
|
|
14
|
+
* The exceptions that remain are paths, not judgements, and that is on purpose: a locale dictionary
|
|
15
|
+
* 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.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { normalizePath } from "./lib/path.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 = /[À-ÃÈ-ÊÌÍÒ-ÕÙÚÝà-ãè-êìíò-õùúýĂăĐđĨĩŨũƠơƯưẠ-ỿ]/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Extended pictographs, or a regional-indicator pair.
|
|
26
|
+
*
|
|
27
|
+
* Two tests rather than one character class, so the class stays free of joiners and combining marks
|
|
28
|
+
* - a single class covering both trips the misleading-character-class rule, and a rule that has to
|
|
29
|
+
* be silenced to exist is one nobody trusts.
|
|
30
|
+
*/
|
|
31
|
+
export const hasEmoji = (text) =>
|
|
32
|
+
typeof text === "string" &&
|
|
33
|
+
(/\p{Extended_Pictographic}/u.test(text) || /[\u{1F1E6}-\u{1F1FF}]{2}/u.test(text))
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Paths whose second-language text or emoji is CONTENT rather than authoring.
|
|
37
|
+
*
|
|
38
|
+
* Two kinds only: the locale dictionaries (`messages/<locale>.json`), which are the product's other
|
|
39
|
+
* language, and fixtures and specs, which reproduce real strings and would be testing something else
|
|
40
|
+
* if they translated them. There is no "copy module" path: a `resources/` folder of strings is
|
|
41
|
+
* copy that skipped the catalogue.
|
|
42
|
+
*/
|
|
43
|
+
export const CONTENT_PATHS = [
|
|
44
|
+
/\/messages\/[a-z-]+\.json$/i,
|
|
45
|
+
/\/__fixtures?__\//,
|
|
46
|
+
/\/fixtures?\//,
|
|
47
|
+
/\.fixture\.(ts|tsx|js|mjs|cjs)$/i,
|
|
48
|
+
/\.test\.(ts|tsx|js|mjs|cjs)$/i,
|
|
49
|
+
/\.spec\.(ts|tsx|js|mjs|cjs)$/i,
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
/** True when a file holds content rather than authoring. */
|
|
53
|
+
export const isContentFile = (filename) => {
|
|
54
|
+
const file = normalizePath(filename)
|
|
55
|
+
return CONTENT_PATHS.some((pattern) => pattern.test(file))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Walk every place prose can hide, and hand each to one check. */
|
|
59
|
+
const proseVisitors = (context, report) => {
|
|
60
|
+
const source = context.sourceCode || context.getSourceCode()
|
|
61
|
+
return {
|
|
62
|
+
Program() {
|
|
63
|
+
for (const comment of source.getAllComments()) report(comment, comment.value)
|
|
64
|
+
},
|
|
65
|
+
Identifier(node) {
|
|
66
|
+
report(node, node.name)
|
|
67
|
+
},
|
|
68
|
+
Literal(node) {
|
|
69
|
+
if (typeof node.value === "string") report(node, node.value)
|
|
70
|
+
},
|
|
71
|
+
TemplateElement(node) {
|
|
72
|
+
report(node, node.value && node.value.cooked)
|
|
73
|
+
},
|
|
74
|
+
JSXText(node) {
|
|
75
|
+
report(node, node.value)
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// -- COMMENTS-1 ------------------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
/** Every export opens with a documentation block. */
|
|
83
|
+
export const requireExportJsdoc = {
|
|
84
|
+
meta: {
|
|
85
|
+
type: "suggestion",
|
|
86
|
+
docs: { description: "Exported declarations open with a documentation block." },
|
|
87
|
+
schema: [],
|
|
88
|
+
messages: {
|
|
89
|
+
jsdoc:
|
|
90
|
+
"`{{name}}` is exported with no documentation block. An export is read far more often than it is written, and by people who never open the body - name the ROLE it plays, not the signature, which the signature already states.",
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
create(context) {
|
|
94
|
+
const source = context.sourceCode || context.getSourceCode()
|
|
95
|
+
const hasBlock = (node) =>
|
|
96
|
+
source.getCommentsBefore(node).some((comment) => comment.type === "Block" && comment.value.startsWith("*"))
|
|
97
|
+
const check = (node) => {
|
|
98
|
+
const declaration = node.declaration
|
|
99
|
+
if (!declaration) return
|
|
100
|
+
const kinds = ["VariableDeclaration", "TSInterfaceDeclaration", "FunctionDeclaration", "TSTypeAliasDeclaration"]
|
|
101
|
+
if (!kinds.includes(declaration.type) || hasBlock(node)) return
|
|
102
|
+
const id =
|
|
103
|
+
declaration.id || (declaration.declarations && declaration.declarations[0] && declaration.declarations[0].id)
|
|
104
|
+
context.report({ node: id || declaration, messageId: "jsdoc", data: { name: (id && id.name) || "this export" } })
|
|
105
|
+
}
|
|
106
|
+
return { ExportNamedDeclaration: check, ExportDefaultDeclaration: check }
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// -- COMMENTS-4 ------------------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
/** No Unicode emoji in source: stable product reactions use attributed checked-in SVG artwork. */
|
|
113
|
+
export const noEmojiInSource = {
|
|
114
|
+
meta: {
|
|
115
|
+
type: "problem",
|
|
116
|
+
docs: { description: "No emoji in source authoring." },
|
|
117
|
+
schema: [],
|
|
118
|
+
messages: {
|
|
119
|
+
emoji:
|
|
120
|
+
"Unicode emoji in source authoring. It renders differently on every platform, sorts unpredictably, breaks a terminal that is not expecting it, and does not mean the same thing in two countries. Generic marks belong to the icon vocabulary; product reactions use the attributed checked-in SVG artwork through the reaction leaf.",
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
create(context) {
|
|
124
|
+
if (isContentFile(context.filename || context.getFilename())) return {}
|
|
125
|
+
return proseVisitors(context, (node, text) => {
|
|
126
|
+
if (hasEmoji(text)) context.report({ node, messageId: "emoji" })
|
|
127
|
+
})
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The rules this law contributes to the plugin. */
|
|
132
|
+
export const rules = {
|
|
133
|
+
"require-export-jsdoc": requireExportJsdoc,
|
|
134
|
+
"no-emoji-in-source": noEmojiInSource,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The level this law asks for, as the plugin's own opinion.
|
|
139
|
+
*
|
|
140
|
+
* `require-export-jsdoc` is the one a repository adopting this with history should expect a count
|
|
141
|
+
* from - every undocumented export in the tree reports at once. The other two are usually already
|
|
142
|
+
* at zero, because a second language in source is noticed by readers long before a rule arrives.
|
|
143
|
+
*/
|
|
144
|
+
export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]))
|
|
@@ -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,99 @@
|
|
|
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`, 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/web-storage-only-in-modules`
|
|
58
|
+
|
|
59
|
+
`localStorage` and `sessionStorage` are touched only inside `modules/` (sub-check `FE_STORAGE_OUTSIDE_MODULES`).
|
|
60
|
+
|
|
61
|
+
**Invalid** (`src/components/blocks/Wallet/index.tsx`)
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
const raw = sessionStorage.getItem(KEY)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Valid** (`src/components/blocks/Wallet/index.tsx`)
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
const draft = useTopUpDraft()
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Finding code:** `FE_STORAGE_OUTSIDE_MODULES`
|
|
74
|
+
|
|
75
|
+
**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.
|
|
76
|
+
|
|
77
|
+
**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.
|
|
78
|
+
|
|
79
|
+
## `starci-fe/no-dangerous-html`
|
|
80
|
+
|
|
81
|
+
`dangerouslySetInnerHTML` only on `<script>`; never on a visible element (sub-check `FE_DANGEROUS_HTML`).
|
|
82
|
+
|
|
83
|
+
**Invalid** (`src/components/blocks/Feed/component.tsx`)
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
<div dangerouslySetInnerHTML={{ __html: html }} />
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Valid** (`src/components/blocks/Feed/component.tsx`)
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON_LD }} />
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Finding code:** `FE_DANGEROUS_HTML`
|
|
96
|
+
|
|
97
|
+
**Vì sao (why):** `dangerouslySetInnerHTML` trên `<tag>` ở `<file>`: chuỗi bất kỳ trở thành HTML chạy được.
|
|
98
|
+
|
|
99
|
+
**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ư.
|