@starci/eslint-canon-fe 6.0.2 → 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 +12 -0
  2. package/README.md +3 -5
  3. package/client-boundary.mjs +2 -5
  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 +145 -107
  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
@@ -1,147 +0,0 @@
1
- # Spec quality
2
-
3
- Law module: `spec-quality.mjs`. Catalogue: R67 FE_SPEC_QUALITY, R60 (real catalogue in specs).
4
-
5
- A spec is a claim about one unit's behaviour: no class-string pinning, it imports the unit beside it, no spec for a barrel, no `as unknown as`, an axe assertion for every connected screen, and a real `next-intl` catalogue instead of a mock.
6
-
7
- Every rule below is an error in `starciFeConfig`; none can be switched off or suppressed inline.
8
-
9
- ## `starci-fe/no-class-string-in-spec`
10
-
11
- A component spec asserts behaviour, never a class string.
12
-
13
- **Invalid** (`src/components/leaves/Chip/Chip.test.tsx`)
14
-
15
- ```ts
16
- expect(el).toHaveClass("bg-primary")
17
- ```
18
-
19
- **Valid** (`src/components/leaves/Chip/Chip.test.tsx`)
20
-
21
- ```ts
22
- expect(screen.getByRole("button", { name: "Save" })).toBeEnabled()
23
- ```
24
-
25
- **Finding code:** `FE_SPEC_QUALITY`
26
-
27
- **Vì sao (why):** Spec `<file>` ghim chuỗi class. Spec kiểm hành vi, không kiểm stylesheet.
28
-
29
- **Cách sửa:** Assert theo role, tên, trạng thái hoặc chữ hiển thị.
30
-
31
- ## `starci-fe/spec-tests-its-neighbour`
32
-
33
- A spec imports its sibling subject: `X.test.tsx` imports `./X`.
34
-
35
- **Invalid** (`src/components/leaves/Chip/Chip.test.tsx`)
36
-
37
- ```ts
38
- import { Row } from "../Row"
39
- ```
40
-
41
- **Valid** (`src/components/leaves/Chip/Chip.test.tsx`)
42
-
43
- ```ts
44
- import { Chip } from "./Chip"
45
- ```
46
-
47
- **Finding code:** `FE_SPEC_QUALITY`
48
-
49
- **Vì sao (why):** Spec `<file>` không import chủ thể nằm cạnh nó.
50
-
51
- **Cách sửa:** Đưa spec về cạnh chủ thể, hoặc test đúng đơn vị nằm cạnh.
52
-
53
- ## `starci-fe/no-barrel-spec`
54
-
55
- No `index.spec.ts(x)` beside an `index.ts(x)` that holds only imports and re-exports. The sibling is read and parsed: an `index.ts` that declares a function, class or value (implementation) may have its spec; a missing sibling is no finding.
56
-
57
- **Invalid** (`src/hooks/index.spec.ts`, beside `src/hooks/index.ts` = `export * from "./useFeed"`)
58
-
59
- ```ts
60
- // file: src/hooks/index.spec.ts
61
- ```
62
-
63
- **Valid** (`src/modules/browser-storage/index.spec.ts`, beside an `index.ts` that declares `createStore` and exports two stores)
64
-
65
- ```ts
66
- // file: src/modules/browser-storage/index.spec.ts
67
- ```
68
-
69
- **Finding code:** `FE_SPEC_QUALITY`
70
-
71
- **Vì sao (why):** Spec `<file>` đặt cạnh một barrel. Barrel không có hành vi để test.
72
-
73
- **Cách sửa:** Xóa spec; test từng đơn vị mà barrel xuất lại, cạnh chính đơn vị đó.
74
-
75
- ## `starci-fe/no-double-cast-in-spec`
76
-
77
- A spec builds its values with the type's own shape, not `as unknown as`.
78
-
79
- **Invalid** (`any spec`)
80
-
81
- ```ts
82
- const x = { id: 1 } as unknown as Course
83
- ```
84
-
85
- **Valid** (`any spec`)
86
-
87
- ```ts
88
- const x = mock<Course>()
89
- ```
90
-
91
- **Finding code:** `FE_SPEC_QUALITY`
92
-
93
- **Vì sao (why):** Spec `<file>` ép kiểu hai lần (`as unknown as`).
94
-
95
- **Cách sửa:** Dựng giá trị bằng factory có kiểu (`mock<T>()` hoặc fixture builder).
96
-
97
- ## `starci-fe/connected-spec-has-axe`
98
-
99
- The spec of a connected block or feature runs an axe assertion.
100
-
101
- **Invalid** (`src/components/blocks/Feed/index.test.tsx`)
102
-
103
- ```ts
104
- it("renders", () => {})
105
- ```
106
-
107
- **Valid** (`src/components/blocks/Feed/index.test.tsx`)
108
-
109
- ```ts
110
- it("a11y", async () => { expect(await axe(container)).toHaveNoViolations() })
111
- ```
112
-
113
- **Finding code:** `FE_SPEC_QUALITY`
114
-
115
- **Vì sao (why):** Spec `<file>` của màn hình kết nối không có assert axe.
116
-
117
- **Cách sửa:** Thêm `expect(await axe(container)).toHaveNoViolations()`.
118
-
119
- ## `starci-fe/no-mocked-translations`
120
-
121
- A spec renders with the real catalogue; it never mocks `next-intl`. A `vi.mock("next-intl/server", factory)` in the spec of a server helper (no provider exists for it) is allowed when the factory - or a `vi.hoisted` block it reads - uses the app's real catalogue (an import that resolves into `modules/i18n/messages/`); a literal or key-echoing server mock, an automock, and any mock of the client `next-intl` stay findings.
122
-
123
- **Invalid** (`any spec`)
124
-
125
- ```ts
126
- vi.mock("next-intl", () => ({}))
127
- ```
128
-
129
- **Valid** (`any spec`)
130
-
131
- ```ts
132
- render(<NextIntlClientProvider messages={messages} locale="vi" />)
133
- ```
134
-
135
- ```ts
136
- import messages from "@/modules/i18n/messages/vi.json"
137
- vi.mock("next-intl/server", async () => {
138
- const { createTranslator } = await import("next-intl")
139
- return { getTranslations: async () => createTranslator({ locale: "vi", messages }) }
140
- })
141
- ```
142
-
143
- **Finding code:** `FE_I18N_CATALOG`
144
-
145
- **Vì sao (why):** Spec `<file>` giả lập `next-intl`, nên chỉ kiểm khóa chứ không kiểm catalog thật.
146
-
147
- **Cách sửa:** Component client: render trong `NextIntlClientProvider` với `messages/<locale>.json` thật. Helper server (`next-intl/server`): factory của mock dựng translator từ catalog thật (`createTranslator` với `modules/i18n/messages/<locale>.json`).
package/e2e-shape.mjs DELETED
@@ -1,345 +0,0 @@
1
- /**
2
- * The rules that hold `e2e-shape.md` (HFS R66, `FE_E2E_SHAPE`).
3
- *
4
- * THESE RULES GOVERN THE E2E TREE, NOT `src/`. `starciFeConfig` attaches them to `e2e/**` and
5
- * `playwright.config.*`; every rule here also scopes itself by HFS slot (tier e2e, the Playwright config role), so a config that reaches it by
6
- * mistake governs nothing it should not.
7
- *
8
- * WHY THE SHAPE IS FIXED. An e2e suite is the only proof that the whole app works, and it is run by
9
- * hand, so it rots quietly: absolute paths that exist on one machine, a Docker dependency that makes
10
- * it unrunnable in CI, a helper that writes into the OTHER repository's tree, a `test.skip` that
11
- * turns a missing environment into a green run, and helpers whose untyped parameters hide the fields
12
- * a test depends on. One layout - `e2e/<area>/<name>.e2e-spec.ts`, three viewports, typed helpers -
13
- * is what lets the land gate check it statically even though it cannot run it.
14
- *
15
- * WHAT IS NOT HERE. That `typecheck:e2e` runs before `test:e2e` is a `package.json` fact (the
16
- * architecture machine reads it); that the specs pass is the run itself.
17
- */
18
-
19
- import { baseName, classOf, fileOf, inSlot, isE2eSource, roleOfFile } from "./lib/scope.mjs"
20
-
21
- /** The three viewports every e2e project set covers, as `width x height`. */
22
- export const VIEWPORTS = Object.freeze(["1440x900", "768x1024", "390x844"])
23
-
24
- /** A file that reads like a spec, by any of the common spellings. */
25
- const SPEC_LIKE = /(?:\.|-)(?:e2e-)?(?:spec|test)\.[cm]?tsx?$/
26
-
27
- /** A helper file that is named as a spec. */
28
- const E2E_SPEC_NAME = /\.e2e-spec\./
29
-
30
- /** The slots of the e2e tree: the specs (`fe.e2e`) and their helpers (`fe.e2e-support`). */
31
- const E2E_SLOTS = ["fe.e2e", "fe.e2e-support"]
32
-
33
- /** An absolute filesystem path: a drive, a UNC share, or a well-known root. */
34
- const ABSOLUTE_PATH = /^(?:[A-Za-z]:[\\/]|\\\\|\/(?:Users|home|tmp|mnt|var|opt|etc|root|d|c)\/)/
35
-
36
- /** A `..` segment in a path string. */
37
- const PARENT_SEGMENT = /(?:^|[\\/])\.\.(?:[\\/]|$)/
38
-
39
- /** Static string of a literal or an expression-free template, else null. */
40
- const staticString = (node) => {
41
- if (!node) return null
42
- if (node.type === "Literal" && typeof node.value === "string") return node.value
43
- if (node.type === "TemplateLiteral") return node.quasis.map((quasi) => quasi.value.cooked ?? "").join("")
44
- return null
45
- }
46
-
47
- /** Every string in a subtree, so `path.join(__dirname, "..", "..")` is read as its parts. */
48
- const stringsIn = (node, source) => {
49
- const found = []
50
- const walk = (current) => {
51
- if (!current || typeof current.type !== "string") return
52
- const text = staticString(current)
53
- if (text !== null) found.push(text)
54
- for (const key of source.visitorKeys[current.type] ?? []) {
55
- const child = current[key]
56
- if (Array.isArray(child)) child.forEach(walk)
57
- else walk(child)
58
- }
59
- }
60
- walk(node)
61
- return found
62
- }
63
-
64
- // -- E2E-1 -----------------------------------------------------------------------------------------
65
-
66
- /** Specs live at `e2e/<area>/<name>.e2e-spec.ts`. */
67
- export const e2eSpecLocation = {
68
- meta: {
69
- type: "problem",
70
- docs: { description: "An e2e spec is `e2e/<area>/<name>.e2e-spec.ts`; helpers live under `support/` or `fixtures/`." },
71
- schema: [],
72
- messages: {
73
- location:
74
- "`{{name}}` is not at `e2e/<area>/<name>.e2e-spec.ts`. One shape - an area folder, the `.e2e-spec.ts` suffix - is what lets the gate find every spec without running them; helpers go in `e2e/support/` or `e2e/fixtures/` and are never named like a spec.",
75
- },
76
- },
77
- create(context) {
78
- const name = baseName(fileOf(context))
79
- const found = classOf(context)
80
- // A helper slot (`fe.e2e-support`) owns no spec: a helper named like one is a spec in the wrong place.
81
- const helperNamedSpec = inSlot(context, "fe.e2e-support") && E2E_SPEC_NAME.test(name)
82
- // A spec-looking file no slot owns but that sits inside the e2e tree (the nearest slot is an e2e slot, matched past the repository root)
83
- // is a spec off the one place `fe.e2e` gives it. A file outside the e2e tree is not this rule's business.
84
- const misplacedSpec = found.status === "no-slot" && E2E_SLOTS.includes(found.nearest?.slot) && found.nearest.matchedDepth >= 1 && SPEC_LIKE.test(name)
85
- if (helperNamedSpec || misplacedSpec) return { Program: (node) => context.report({ node, messageId: "location", data: { name } }) }
86
- return {}
87
- },
88
- }
89
-
90
- // -- E2E-2 -----------------------------------------------------------------------------------------
91
-
92
- /** No absolute path in e2e source. */
93
- export const e2eNoAbsolutePath = {
94
- meta: {
95
- type: "problem",
96
- docs: { description: "No absolute filesystem path in e2e source." },
97
- schema: [],
98
- messages: {
99
- absolute:
100
- "An absolute path (`{{path}}`). It exists on one machine and nowhere else, so the suite passes locally and cannot run in CI or on another machine. Build the path from the config or the test's own directory.",
101
- },
102
- },
103
- create(context) {
104
- if (!isE2eSource(context)) return {}
105
- const check = (node, text) => {
106
- if (typeof text === "string" && ABSOLUTE_PATH.test(text)) context.report({ node, messageId: "absolute", data: { path: text } })
107
- }
108
- return {
109
- Literal: (node) => check(node, node.value),
110
- TemplateElement: (node) => check(node, node.value && node.value.cooked),
111
- }
112
- },
113
- }
114
-
115
- // -- E2E-3 -----------------------------------------------------------------------------------------
116
-
117
- /** Container tooling that an e2e suite must not depend on. */
118
- const DOCKER_MODULE = /^(?:dockerode|testcontainers|@testcontainers\/.+|docker-compose)$/
119
-
120
- /** A command or string that starts Docker. */
121
- const DOCKER_TEXT = /\bdocker(?:-compose)?\b|\bdocker\s+compose\b/i
122
-
123
- /** No Docker in e2e. */
124
- export const e2eNoDocker = {
125
- meta: {
126
- type: "problem",
127
- docs: { description: "An e2e suite does not start or require Docker." },
128
- schema: [],
129
- messages: {
130
- docker:
131
- "Docker in an e2e suite. A suite that needs a container runtime cannot run where the app can, and it hides the real prerequisite (a running app and backend) behind a tool. Point the suite at a URL from the config and let the environment provide the stack.",
132
- },
133
- },
134
- create(context) {
135
- if (!isE2eSource(context)) return {}
136
- return {
137
- ImportDeclaration(node) {
138
- if (DOCKER_MODULE.test(String(node.source.value))) context.report({ node, messageId: "docker" })
139
- },
140
- Literal(node) {
141
- if (typeof node.value === "string" && DOCKER_TEXT.test(node.value)) context.report({ node, messageId: "docker" })
142
- },
143
- TemplateElement(node) {
144
- if (DOCKER_TEXT.test((node.value && node.value.cooked) || "")) context.report({ node, messageId: "docker" })
145
- },
146
- }
147
- },
148
- }
149
-
150
- // -- E2E-4 -----------------------------------------------------------------------------------------
151
-
152
- /** Filesystem calls that write, move or remove. */
153
- const FS_WRITE = new Set([
154
- "writeFile",
155
- "writeFileSync",
156
- "appendFile",
157
- "appendFileSync",
158
- "mkdir",
159
- "mkdirSync",
160
- "rm",
161
- "rmSync",
162
- "rmdir",
163
- "rmdirSync",
164
- "copyFile",
165
- "copyFileSync",
166
- "cp",
167
- "cpSync",
168
- "rename",
169
- "renameSync",
170
- "unlink",
171
- "unlinkSync",
172
- "createWriteStream",
173
- ])
174
-
175
- /** The name a call is made through: `writeFile` or `fs.writeFile` or `fs.promises.writeFile`. */
176
- const calleeName = (callee) => {
177
- if (callee.type === "Identifier") return callee.name
178
- if (callee.type === "MemberExpression" && !callee.computed) return callee.property.name
179
- return null
180
- }
181
-
182
- /** No e2e write outside the repository's own tree. */
183
- export const e2eNoCrossRepoWrite = {
184
- meta: {
185
- type: "problem",
186
- docs: { description: "An e2e file write never targets an absolute path or climbs out of the repository." },
187
- schema: [],
188
- messages: {
189
- write:
190
- "This file operation writes to a path that is absolute or climbs out with `..`. An e2e run that writes into another repository (the backend's tree, a shared folder) changes state it does not own, and its result depends on a checkout layout. Write under this repository's own output folder.",
191
- },
192
- },
193
- create(context) {
194
- if (!isE2eSource(context)) return {}
195
- const source = context.sourceCode || context.getSourceCode()
196
- return {
197
- CallExpression(node) {
198
- const name = calleeName(node.callee)
199
- if (!name || !FS_WRITE.has(name)) return
200
- const strings = node.arguments.flatMap((argument) => stringsIn(argument, source))
201
- if (strings.some((text) => ABSOLUTE_PATH.test(text) || PARENT_SEGMENT.test(text) || text === "..")) {
202
- context.report({ node, messageId: "write" })
203
- }
204
- },
205
- }
206
- },
207
- }
208
-
209
- // -- E2E-5 -----------------------------------------------------------------------------------------
210
-
211
- /** The root identifier of a member chain: `test` in `test.describe.skip`. */
212
- const chainRoot = (node) => {
213
- let current = node
214
- while (current && current.type === "MemberExpression") current = current.object
215
- return current && current.type === "Identifier" ? current.name : null
216
- }
217
-
218
- /** No skipped test: a missing environment is a failure. */
219
- export const e2eNoSkip = {
220
- meta: {
221
- type: "problem",
222
- docs: { description: "No `test.skip`, `test.fixme` or `describe.skip`: a missing environment fails the run." },
223
- schema: [],
224
- messages: {
225
- skip:
226
- "A skipped test. Skipping because the environment is missing turns \"the suite cannot run\" into a green run, which is the one result an e2e suite must never give. Fix the environment, or delete the test.",
227
- },
228
- },
229
- create(context) {
230
- if (!isE2eSource(context)) return {}
231
- return {
232
- CallExpression(node) {
233
- const callee = node.callee
234
- if (callee.type !== "MemberExpression" || callee.computed) return
235
- if (!["skip", "fixme"].includes(callee.property.name)) return
236
- if (["test", "it", "describe"].includes(chainRoot(callee.object))) context.report({ node, messageId: "skip" })
237
- },
238
- }
239
- },
240
- }
241
-
242
- // -- E2E-6 -----------------------------------------------------------------------------------------
243
-
244
- /** True when a function is a module-level helper: declared, or bound to a top-level `const`. */
245
- const isModuleLevel = (node) => {
246
- let current = node.parent
247
- if (node.type !== "FunctionDeclaration") {
248
- if (!current || current.type !== "VariableDeclarator") return false
249
- current = current.parent && current.parent.parent
250
- }
251
- return Boolean(current) && ["Program", "ExportNamedDeclaration", "ExportDefaultDeclaration"].includes(current.type)
252
- }
253
-
254
- /** Helpers carry types; nothing is `any`. */
255
- export const e2eTypedHelpers = {
256
- meta: {
257
- type: "problem",
258
- docs: { description: "e2e helper parameters are typed and nothing is `any`." },
259
- schema: [],
260
- messages: {
261
- untyped:
262
- "A helper parameter with no type. A test depends on the fields a helper takes and returns; untyped, a renamed field fails at run time in a test nobody touched. Annotate the parameter.",
263
- any: "`any` in e2e source. It turns the compiler off for the value a test asserts on. Type it, or narrow from `unknown`.",
264
- },
265
- },
266
- create(context) {
267
- if (!isE2eSource(context)) return {}
268
- const checkParams = (node) => {
269
- if (!isModuleLevel(node)) return
270
- for (const parameter of node.params) {
271
- const target = parameter.type === "AssignmentPattern" ? parameter.left : parameter
272
- // A default value gives the parameter a type the compiler infers.
273
- if (parameter.type === "AssignmentPattern" && parameter.right.type === "Literal") continue
274
- if (target.type === "RestElement") {
275
- if (!target.typeAnnotation) context.report({ node: target, messageId: "untyped" })
276
- continue
277
- }
278
- if (!target.typeAnnotation) context.report({ node: target, messageId: "untyped" })
279
- }
280
- }
281
- return {
282
- FunctionDeclaration: checkParams,
283
- ArrowFunctionExpression: checkParams,
284
- FunctionExpression: checkParams,
285
- TSAnyKeyword(node) {
286
- context.report({ node, messageId: "any" })
287
- },
288
- }
289
- },
290
- }
291
-
292
- // -- E2E-7 -----------------------------------------------------------------------------------------
293
-
294
- /** Every Playwright config declares exactly the three viewports. */
295
- export const playwrightViewports = {
296
- meta: {
297
- type: "problem",
298
- docs: { description: `playwright.config declares the viewports ${VIEWPORTS.join(", ")} and no others.` },
299
- schema: [],
300
- messages: {
301
- missing:
302
- "The Playwright config does not declare the viewport {{size}}. Every app is proved at 1440x900, 768x1024 and 390x844 so a layout that only works on a desktop screen fails here.",
303
- extra:
304
- "The Playwright config declares the viewport {{size}}, which is not one of 1440x900, 768x1024, 390x844. A fourth size is a fourth thing to keep passing that no design covers.",
305
- },
306
- },
307
- create(context) {
308
- if (roleOfFile(context) !== "playwright") return {}
309
- const seen = new Map()
310
- return {
311
- Property(node) {
312
- if (node.computed || node.key.type !== "Identifier" || node.key.name !== "viewport") return
313
- if (node.value.type !== "ObjectExpression") return
314
- const field = (name) => {
315
- const property = node.value.properties.find((p) => p.type === "Property" && p.key.type === "Identifier" && p.key.name === name)
316
- return property && property.value.type === "Literal" ? property.value.value : null
317
- }
318
- const width = field("width")
319
- const height = field("height")
320
- if (typeof width === "number" && typeof height === "number") seen.set(`${width}x${height}`, node)
321
- },
322
- "Program:exit"(program) {
323
- for (const size of VIEWPORTS) if (!seen.has(size)) context.report({ node: program, messageId: "missing", data: { size } })
324
- for (const [size, node] of seen) if (!VIEWPORTS.includes(size)) context.report({ node, messageId: "extra", data: { size } })
325
- },
326
- }
327
- },
328
- }
329
-
330
- /** The rules this law contributes to the plugin. */
331
- export const rules = {
332
- "e2e-spec-location": e2eSpecLocation,
333
- "e2e-no-absolute-path": e2eNoAbsolutePath,
334
- "e2e-no-docker": e2eNoDocker,
335
- "e2e-no-cross-repo-write": e2eNoCrossRepoWrite,
336
- "e2e-no-skip": e2eNoSkip,
337
- "e2e-typed-helpers": e2eTypedHelpers,
338
- "playwright-viewports": playwrightViewports,
339
- }
340
-
341
- /** Which tree these rules govern: the factory attaches them to `e2e/**`, not to `src/**`. */
342
- export const scope = "e2e"
343
-
344
- /** Every rule is an error. */
345
- export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]))