@starci/eslint-canon-fe 6.0.2 → 7.1.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 +18 -0
- package/README.md +3 -5
- package/client-boundary.mjs +2 -5
- package/comments.mjs +38 -15
- package/docs/brand-values.md +2 -2
- package/docs/client-boundary.md +10 -10
- package/docs/env-owner.md +4 -4
- package/docs/formatting.md +2 -2
- package/docs/grammar-boundary.md +2 -2
- package/docs/hooks-folder.md +2 -2
- package/docs/hygiene.md +8 -8
- package/docs/lint-escape-hatch.md +2 -2
- package/docs/lists.md +6 -6
- package/docs/native-controls.md +6 -6
- package/docs/next-conventions.md +20 -20
- package/docs/size-and-state-budget.md +6 -6
- package/docs/size-growth.md +2 -2
- package/docs/status-colors.md +2 -2
- package/docs/translation.md +2 -2
- package/docs/transport.md +18 -18
- package/docs/type-safety.md +8 -8
- package/env-owner.mjs +2 -3
- package/file-layout.mjs +3 -43
- package/formatting.mjs +2 -2
- package/grammar-boundary.mjs +1 -2
- package/hooks-folder.mjs +6 -7
- package/index.mjs +5 -27
- package/lib/config.mjs +6 -22
- package/lib/scope.mjs +3 -12
- package/lib/why.mjs +3 -68
- package/lint-escape-hatch.mjs +3 -3
- package/lists.mjs +0 -4
- package/loading.mjs +2 -2
- package/native-controls.mjs +0 -4
- package/next-conventions.mjs +3 -7
- package/package.json +1 -1
- package/project-graph.mjs +23 -0
- package/props-and-slots.mjs +4 -5
- package/runtime/knowledge/hfs/slots.yaml +145 -107
- package/runtime/knowledge/patterns/fe/folder.yaml +309 -0
- package/runtime/scripts/checks/architecture/backend.mjs +350 -0
- package/runtime/scripts/checks/architecture/background-unowned.mjs +107 -0
- package/runtime/scripts/checks/architecture/client-reaches-server.mjs +94 -0
- package/runtime/scripts/checks/architecture/clones.mjs +200 -0
- package/runtime/scripts/checks/architecture/config-unread.mjs +35 -0
- package/runtime/scripts/checks/architecture/config.mjs +310 -0
- package/runtime/scripts/checks/architecture/connection-map.mjs +208 -0
- package/runtime/scripts/checks/architecture/constructor-deps.mjs +100 -0
- package/runtime/scripts/checks/architecture/contract-fixture-guard.mjs +128 -0
- package/runtime/scripts/checks/architecture/contracts.mjs +792 -0
- package/runtime/scripts/checks/architecture/cross-app-duplicate.mjs +73 -0
- package/runtime/scripts/checks/architecture/dead-exports.mjs +265 -0
- package/runtime/scripts/checks/architecture/default-deny.mjs +129 -0
- package/runtime/scripts/checks/architecture/doc-language.mjs +39 -0
- package/runtime/scripts/checks/architecture/entrypoint.mjs +57 -0
- package/runtime/scripts/checks/architecture/error-codes.mjs +45 -0
- package/runtime/scripts/checks/architecture/error-masked.mjs +60 -0
- package/runtime/scripts/checks/architecture/fe-slot-allows.mjs +38 -0
- package/runtime/scripts/checks/architecture/feature-shape.mjs +49 -0
- package/runtime/scripts/checks/architecture/framework-pinned.mjs +83 -0
- package/runtime/scripts/checks/architecture/frontend.mjs +995 -0
- package/runtime/scripts/checks/architecture/hfs-graph.mjs +61 -0
- package/runtime/scripts/checks/architecture/hfs.mjs +521 -0
- package/runtime/scripts/checks/architecture/hooks-are-hooks.mjs +88 -0
- package/runtime/scripts/checks/architecture/i18n-keys.mjs +146 -0
- package/runtime/scripts/checks/architecture/index.mjs +321 -0
- package/runtime/scripts/checks/architecture/injection-token-exported.mjs +62 -0
- package/runtime/scripts/checks/architecture/machine-ast.mjs +138 -0
- package/runtime/scripts/checks/architecture/module-per-transport.mjs +130 -0
- package/runtime/scripts/checks/architecture/next-data.mjs +775 -0
- package/runtime/scripts/checks/architecture/owners.mjs +95 -0
- package/runtime/scripts/checks/architecture/package-shape.mjs +63 -0
- package/runtime/scripts/checks/architecture/reachability.mjs +233 -0
- package/runtime/scripts/checks/architecture/register-once.mjs +141 -0
- package/runtime/scripts/checks/architecture/registration.mjs +319 -0
- package/runtime/scripts/checks/architecture/required-files.mjs +172 -0
- package/runtime/scripts/checks/architecture/route-files-thin.mjs +97 -0
- package/runtime/scripts/checks/architecture/schema-owner.mjs +261 -0
- package/runtime/scripts/checks/architecture/source-names.mjs +607 -0
- package/runtime/scripts/checks/architecture/sql-owner.mjs +142 -0
- package/runtime/scripts/checks/architecture/sql-tokens.mjs +327 -0
- package/runtime/scripts/checks/architecture/surface.mjs +91 -0
- package/runtime/scripts/checks/architecture/symbols.mjs +193 -0
- package/runtime/scripts/checks/architecture/test-world-files.mjs +163 -0
- package/runtime/scripts/checks/architecture/tiers.mjs +130 -0
- package/runtime/scripts/checks/architecture/transport-owner.mjs +112 -0
- package/runtime/scripts/checks/architecture/typescript.mjs +573 -0
- package/runtime/scripts/checks/architecture/unit-spec-providers.mjs +122 -0
- package/runtime/scripts/checks/common.mjs +44 -0
- package/runtime/scripts/checks/typescript-programs.mjs +82 -0
- package/runtime/scripts/lib/hfs-allows.mjs +15 -0
- package/runtime/scripts/lib/hfs-path-findings.mjs +84 -0
- package/runtime/scripts/lib/hfs-rules/fe-no-tests.mjs +47 -0
- package/runtime/scripts/lib/hfs-rules/read.mjs +28 -0
- package/runtime/scripts/lib/hfs-rules/secrets.mjs +54 -0
- package/runtime/scripts/lib/hfs-rules/spec-placement.mjs +29 -0
- package/runtime/scripts/lib/hfs-slots.mjs +13 -6
- package/runtime/scripts/lib/json.mjs +22 -0
- package/runtime/scripts/lib/language.mjs +107 -0
- package/runtime/scripts/lib/project-graph.mjs +0 -0
- package/runtime/scripts/lib/project-rule.mjs +39 -0
- package/runtime/scripts/lib/repo-identity.mjs +50 -0
- package/runtime/scripts/lib/secret-patterns.mjs +44 -0
- package/runtime/scripts/lib/stack-services.mjs +217 -0
- package/runtime/scripts/lib/test-secrets.mjs +120 -0
- package/served-locale.mjs +2 -2
- package/shape-slot.mjs +2 -3
- package/size-and-state-budget.mjs +1 -4
- package/size-growth.mjs +1 -2
- package/status-colors.mjs +0 -2
- package/tokens.mjs +1 -1
- package/translation.mjs +1 -29
- package/transport.mjs +13 -17
- package/docs/e2e-shape.md +0 -161
- package/docs/spec-quality.md +0 -147
- package/e2e-shape.mjs +0 -345
- package/spec-quality.mjs +0 -318
package/lib/why.mjs
CHANGED
|
@@ -46,10 +46,10 @@ export const why = {
|
|
|
46
46
|
vi: "Client ở `<file>` không có nhánh so sánh `response.status` với 401 và 403 rồi trả `{ kind: \"refused\" }`, nên trạng thái \"cần đăng nhập\" không bao giờ đạt được.",
|
|
47
47
|
fixVi: "Thêm nhánh `response.status === 401 || response.status === 403` trả `{ ok: false, kind: \"refused\" }` ngay trong client.",
|
|
48
48
|
},
|
|
49
|
-
"no-
|
|
49
|
+
"no-failure-collapse": {
|
|
50
50
|
code: "FE_HTTP_STATUS_COLLAPSE",
|
|
51
|
-
vi: "`<file>` gộp mọi
|
|
52
|
-
fixVi: "
|
|
51
|
+
vi: "`<file>` gộp mọi thất bại (phản hồi HTTP lỗi hoặc `Outcome` thất bại) thành một nhánh hoặc một giá trị rỗng, nên lý do bị mất.",
|
|
52
|
+
fixVi: "Giữ lý do: với `Outcome` thì rẽ nhánh theo mã lý do; với phản hồi HTTP thì đổi mã trạng thái thành `Outcome` (`refused` cho 401/403, `not-found`, `invalid`, `unavailable`). Không dùng nguyên văn lỗi của server làm lý do.",
|
|
53
53
|
},
|
|
54
54
|
"no-hand-typed-wire": {
|
|
55
55
|
code: "FE_WIRE_GENERATED",
|
|
@@ -121,71 +121,6 @@ export const why = {
|
|
|
121
121
|
vi: "`<unit>` tự viết vòng poll (`setInterval` hoặc `setTimeout` tự gọi lại).",
|
|
122
122
|
fixVi: "Làm tươi qua hook dữ liệu (`refreshInterval`) hoặc socket, mỗi resource đúng một cơ chế.",
|
|
123
123
|
},
|
|
124
|
-
"e2e-spec-location": {
|
|
125
|
-
code: "FE_E2E_SHAPE",
|
|
126
|
-
vi: "Spec e2e `<file>` không nằm ở `e2e/<area>/<name>.e2e-spec.ts`.",
|
|
127
|
-
fixVi: "Đưa spec vào `e2e/<area>/` với hậu tố `.e2e-spec.ts`; helper vào `e2e/support/` hoặc `e2e/fixtures/`.",
|
|
128
|
-
},
|
|
129
|
-
"e2e-no-absolute-path": {
|
|
130
|
-
code: "FE_E2E_SHAPE",
|
|
131
|
-
vi: "Spec e2e `<file>` dùng đường dẫn tuyệt đối.",
|
|
132
|
-
fixVi: "Dựng đường dẫn từ cấu hình hoặc thư mục của chính spec.",
|
|
133
|
-
},
|
|
134
|
-
"e2e-no-docker": {
|
|
135
|
-
code: "FE_E2E_SHAPE",
|
|
136
|
-
vi: "Spec e2e `<file>` phụ thuộc vào docker.",
|
|
137
|
-
fixVi: "Trỏ suite tới một URL từ cấu hình; môi trường tự cung cấp stack.",
|
|
138
|
-
},
|
|
139
|
-
"e2e-no-cross-repo-write": {
|
|
140
|
-
code: "FE_E2E_SHAPE",
|
|
141
|
-
vi: "Spec e2e `<file>` ghi ra ngoài repo của app (đường dẫn tuyệt đối hoặc `..`).",
|
|
142
|
-
fixVi: "Chỉ ghi vào thư mục kết quả của chính repo này.",
|
|
143
|
-
},
|
|
144
|
-
"e2e-no-skip": {
|
|
145
|
-
code: "FE_E2E_SHAPE",
|
|
146
|
-
vi: "Spec e2e `<file>` bỏ qua test (`test.skip`/`fixme`) — thiếu môi trường phải làm suite đỏ, không được xanh.",
|
|
147
|
-
fixVi: "Sửa môi trường hoặc xóa test; không bỏ qua có điều kiện.",
|
|
148
|
-
},
|
|
149
|
-
"e2e-typed-helpers": {
|
|
150
|
-
code: "FE_E2E_SHAPE",
|
|
151
|
-
vi: "Helper e2e ở `<file>` có tham số không kiểu hoặc dùng `any`.",
|
|
152
|
-
fixVi: "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.",
|
|
153
|
-
},
|
|
154
|
-
"playwright-viewports": {
|
|
155
|
-
code: "FE_E2E_SHAPE",
|
|
156
|
-
vi: "`playwright.config` không khai đúng ba viewport 1440x900, 768x1024, 390x844.",
|
|
157
|
-
fixVi: "Mỗi project khai một viewport; đủ ba, không thêm cỡ thứ tư.",
|
|
158
|
-
},
|
|
159
|
-
"no-class-string-in-spec": {
|
|
160
|
-
code: "FE_SPEC_QUALITY",
|
|
161
|
-
vi: "Spec `<file>` ghim chuỗi class. Spec kiểm hành vi, không kiểm stylesheet.",
|
|
162
|
-
fixVi: "Assert theo role, tên, trạng thái hoặc chữ hiển thị.",
|
|
163
|
-
},
|
|
164
|
-
"spec-tests-its-neighbour": {
|
|
165
|
-
code: "FE_SPEC_QUALITY",
|
|
166
|
-
vi: "Spec `<file>` không import chủ thể nằm cạnh nó.",
|
|
167
|
-
fixVi: "Đưa spec về cạnh chủ thể, hoặc test đúng đơn vị nằm cạnh.",
|
|
168
|
-
},
|
|
169
|
-
"no-barrel-spec": {
|
|
170
|
-
code: "FE_SPEC_QUALITY",
|
|
171
|
-
vi: "Spec `<file>` đặt cạnh một `index` chỉ có import và re-export (barrel). Barrel không có hành vi để test.",
|
|
172
|
-
fixVi: "Xóa spec; test từng đơn vị mà barrel xuất lại, cạnh chính đơn vị đó.",
|
|
173
|
-
},
|
|
174
|
-
"no-double-cast-in-spec": {
|
|
175
|
-
code: "FE_SPEC_QUALITY",
|
|
176
|
-
vi: "Spec `<file>` ép kiểu hai lần (`as unknown as`).",
|
|
177
|
-
fixVi: "Dựng giá trị bằng factory có kiểu (`mock<T>()` hoặc fixture builder).",
|
|
178
|
-
},
|
|
179
|
-
"connected-spec-has-axe": {
|
|
180
|
-
code: "FE_SPEC_QUALITY",
|
|
181
|
-
vi: "Spec `<file>` của màn hình kết nối không có assert axe.",
|
|
182
|
-
fixVi: "Thêm `expect(await axe(container)).toHaveNoViolations()`.",
|
|
183
|
-
},
|
|
184
|
-
"no-mocked-translations": {
|
|
185
|
-
code: "FE_I18N_CATALOG",
|
|
186
|
-
vi: "Spec `<file>` giả lập `next-intl`, nên chỉ kiểm khóa chứ không kiểm catalog thật.",
|
|
187
|
-
fixVi: "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`).",
|
|
188
|
-
},
|
|
189
124
|
"no-inline-lint-config": {
|
|
190
125
|
code: "HFS_INLINE_SUPPRESSION",
|
|
191
126
|
vi: "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.",
|
package/lint-escape-hatch.mjs
CHANGED
|
@@ -19,10 +19,10 @@
|
|
|
19
19
|
* nothing is dead weight that reads as if it did.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import {
|
|
22
|
+
import { isProductSource } from "./lib/scope.mjs"
|
|
23
23
|
|
|
24
|
-
/** Product source
|
|
25
|
-
const isGoverned = (context) => isProductSource(context)
|
|
24
|
+
/** Product source is governed; canon tests deliberately build forbidden directives. */
|
|
25
|
+
const isGoverned = (context) => isProductSource(context)
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Any directive that changes ESLint's active rule set inside a source file.
|
package/lists.mjs
CHANGED
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { attribute, attributeValue, calleeName, isFunction, mentions } from "./lib/ast.mjs"
|
|
19
|
-
import { isSpecFile } from "./lib/scope.mjs"
|
|
20
19
|
|
|
21
20
|
/** `.map(callback)` where the callback is written inline; the call node, else null. */
|
|
22
21
|
const mapCallback = (call) => {
|
|
@@ -86,7 +85,6 @@ export const listItemHasKey = {
|
|
|
86
85
|
},
|
|
87
86
|
},
|
|
88
87
|
create(context) {
|
|
89
|
-
if (isSpecFile(context.filename || context.getFilename())) return {}
|
|
90
88
|
return {
|
|
91
89
|
CallExpression(call) {
|
|
92
90
|
const callback = mapCallback(call)
|
|
@@ -116,7 +114,6 @@ export const noIndexKey = {
|
|
|
116
114
|
},
|
|
117
115
|
},
|
|
118
116
|
create(context) {
|
|
119
|
-
if (isSpecFile(context.filename || context.getFilename())) return {}
|
|
120
117
|
const source = context.sourceCode ?? context.getSourceCode()
|
|
121
118
|
return {
|
|
122
119
|
CallExpression(call) {
|
|
@@ -170,7 +167,6 @@ export const noInlineLiteralPropInList = {
|
|
|
170
167
|
},
|
|
171
168
|
},
|
|
172
169
|
create(context) {
|
|
173
|
-
if (isSpecFile(context.filename || context.getFilename())) return {}
|
|
174
170
|
return {
|
|
175
171
|
JSXAttribute(node) {
|
|
176
172
|
const opening = node.parent
|
package/loading.mjs
CHANGED
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
* lint rule is for - the one nothing else will ever report.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { classOf, fileOf, isComponentFile
|
|
15
|
+
import { classOf, fileOf, isComponentFile } from "./lib/scope.mjs"
|
|
16
16
|
|
|
17
17
|
/** Product component source these rules govern: a component owner's file, and not a spec (a placeholder in a test fixture is a fixture, not a second tree). */
|
|
18
|
-
const isGoverned = (context) => isComponentFile(context)
|
|
18
|
+
const isGoverned = (context) => isComponentFile(context)
|
|
19
19
|
|
|
20
20
|
/** A flag that means "waiting", in the spellings this codebase uses. */
|
|
21
21
|
const WAITING_FLAG = /\bis(?:Loading|Skeleton|Pending)\b/
|
package/native-controls.mjs
CHANGED
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { attribute } from "./lib/ast.mjs"
|
|
16
|
-
import { isSpecFile } from "./lib/scope.mjs"
|
|
17
16
|
|
|
18
17
|
/** Intrinsic elements a reader operates, that the grammar renders for the product. */
|
|
19
18
|
const NATIVE = new Set(["select", "input", "textarea", "button"])
|
|
@@ -30,7 +29,6 @@ export const noNativeFormControl = {
|
|
|
30
29
|
},
|
|
31
30
|
},
|
|
32
31
|
create(context) {
|
|
33
|
-
if (isSpecFile(context.filename || context.getFilename())) return {}
|
|
34
32
|
return {
|
|
35
33
|
JSXOpeningElement(node) {
|
|
36
34
|
if (node.name.type !== "JSXIdentifier" || !NATIVE.has(node.name.name)) return
|
|
@@ -54,7 +52,6 @@ export const noNativeImg = {
|
|
|
54
52
|
},
|
|
55
53
|
},
|
|
56
54
|
create(context) {
|
|
57
|
-
if (isSpecFile(context.filename || context.getFilename())) return {}
|
|
58
55
|
return {
|
|
59
56
|
JSXOpeningElement(node) {
|
|
60
57
|
if (node.name.type === "JSXIdentifier" && node.name.name === "img") context.report({ node, messageId: "img" })
|
|
@@ -79,7 +76,6 @@ export const imageHasSize = {
|
|
|
79
76
|
},
|
|
80
77
|
},
|
|
81
78
|
create(context) {
|
|
82
|
-
if (isSpecFile(context.filename || context.getFilename())) return {}
|
|
83
79
|
const locals = new Set()
|
|
84
80
|
return {
|
|
85
81
|
ImportDeclaration(node) {
|
package/next-conventions.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { attribute, attributeValue, calleeName, leadingTextOf, stringOf } from "./lib/ast.mjs"
|
|
15
15
|
import { hfsOf } from "./lib/hfs.mjs"
|
|
16
|
-
import { baseName, classOf, fileOf, inSlot,
|
|
16
|
+
import { baseName, classOf, fileOf, inSlot, roleOfFile, slotOfFile, stem } from "./lib/scope.mjs"
|
|
17
17
|
|
|
18
18
|
/** Another i18n stack: a second place that decides which language a reader sees. */
|
|
19
19
|
const OTHER_I18N = /^(?:react-i18next|i18next|next-i18next|react-intl|next-translate|@lingui\/.+|typesafe-i18n|rosetta)$/
|
|
@@ -35,7 +35,6 @@ export const noMiddlewareFile = {
|
|
|
35
35
|
},
|
|
36
36
|
create(context) {
|
|
37
37
|
const file = fileOf(context)
|
|
38
|
-
if (isSpecFile(file)) return {}
|
|
39
38
|
// The retired name has a slot of its own (`fe.source-root-retired`, forbidden); the interceptor is the `proxy` role of `fe.source-root-pinned`.
|
|
40
39
|
if (inSlot(context, "fe.source-root-retired")) {
|
|
41
40
|
return { Program: (node) => context.report({ node, messageId: "file", data: { name: baseName(file) } }) }
|
|
@@ -189,7 +188,6 @@ export const noNullSuspenseFallback = {
|
|
|
189
188
|
},
|
|
190
189
|
},
|
|
191
190
|
create(context) {
|
|
192
|
-
if (isSpecFile(context.filename || context.getFilename())) return {}
|
|
193
191
|
return {
|
|
194
192
|
JSXOpeningElement(node) {
|
|
195
193
|
const name = node.name
|
|
@@ -235,7 +233,7 @@ export const navigationFromIntl = {
|
|
|
235
233
|
},
|
|
236
234
|
},
|
|
237
235
|
create(context) {
|
|
238
|
-
if (
|
|
236
|
+
if (isNavigationOwner(context)) return {}
|
|
239
237
|
return {
|
|
240
238
|
ImportDeclaration(node) {
|
|
241
239
|
if (node.importKind === "type") return
|
|
@@ -271,7 +269,6 @@ export const noNativeAnchor = {
|
|
|
271
269
|
},
|
|
272
270
|
},
|
|
273
271
|
create(context) {
|
|
274
|
-
if (isSpecFile(context.filename || context.getFilename())) return {}
|
|
275
272
|
return {
|
|
276
273
|
JSXOpeningElement(node) {
|
|
277
274
|
if (node.name.type !== "JSXIdentifier" || node.name.name !== "a") return
|
|
@@ -309,7 +306,7 @@ export const noHardcodedRoute = {
|
|
|
309
306
|
},
|
|
310
307
|
},
|
|
311
308
|
create(context) {
|
|
312
|
-
if (
|
|
309
|
+
if (isRoutesModule(context)) return {}
|
|
313
310
|
const source = context.sourceCode ?? context.getSourceCode()
|
|
314
311
|
const report = (node, text) => context.report({ node, messageId: "route", data: { path: text } })
|
|
315
312
|
return {
|
|
@@ -393,7 +390,6 @@ export const i18nStackInOneModule = {
|
|
|
393
390
|
},
|
|
394
391
|
},
|
|
395
392
|
create(context) {
|
|
396
|
-
if (isSpecFile(context.filename || context.getFilename())) return {}
|
|
397
393
|
const hfs = hfsOf(context)
|
|
398
394
|
const slot = slotOfFile(context)
|
|
399
395
|
const single = hfs.apps.length === 1
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starci/eslint-canon-fe",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.1.0",
|
|
4
4
|
"description": "The StarCi front-end lint canon: every rule its laws publish, gathered as one flat-config plugin and attached by one factory.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rules that read the project graph: the findings of the architecture machine that sit on a TypeScript file.
|
|
3
|
+
*
|
|
4
|
+
* One graph per lint run: `scripts/lib/project-graph.mjs` (a byte copy in this package's runtime/) builds the machine's module
|
|
5
|
+
* graph and slot manifest once for the repository, caches it, and each rule below reports the findings of its own codes on
|
|
6
|
+
* the file ESLint is visiting, on the line the machine names. There is no second graph and no second judge: the list of
|
|
7
|
+
* rules and their codes is `LINT_ENFORCERS` of the machine (scripts/checks/architecture/surface.mjs); `hfs check` keeps the
|
|
8
|
+
* findings that have no TypeScript file to sit on. A graph that cannot be built stops the run with the machine's message.
|
|
9
|
+
*/
|
|
10
|
+
import { LINT_ENFORCERS } from "./runtime/scripts/checks/architecture/surface.mjs"
|
|
11
|
+
import { projectRule } from "./runtime/scripts/lib/project-rule.mjs"
|
|
12
|
+
import { hfsOf } from "./lib/hfs.mjs"
|
|
13
|
+
import { dirname, join } from "node:path"
|
|
14
|
+
import { fileURLToPath } from "node:url"
|
|
15
|
+
|
|
16
|
+
const RUNTIME = join(dirname(fileURLToPath(import.meta.url)), "runtime")
|
|
17
|
+
const mine = LINT_ENFORCERS.filter((enforcer) => enforcer.plugins.includes("fe"))
|
|
18
|
+
|
|
19
|
+
/** The rules this law contributes to the plugin: one per graph enforcer of this profile. */
|
|
20
|
+
export const rules = Object.fromEntries(mine.map((e) => [e.id, projectRule({ enforcer: e, runtimeRoot: RUNTIME, hfsOf })]))
|
|
21
|
+
|
|
22
|
+
/** Every one at error. */
|
|
23
|
+
export const recommended = Object.fromEntries(mine.map((e) => [`starci-fe/${e.id}`, "error"]))
|
package/props-and-slots.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Rules for named React props, ordinary children, and local visual ownership. */
|
|
2
2
|
|
|
3
3
|
import { declarationsOf, fileOf as declarationFile } from "./lib/types.mjs"
|
|
4
|
-
import { fileOf, isComponentFile, isComponentPath, isProductSource,
|
|
4
|
+
import { fileOf, isComponentFile, isComponentPath, isProductSource, kindOfFile } from "./lib/scope.mjs"
|
|
5
5
|
|
|
6
6
|
const propertyName = (node) => {
|
|
7
7
|
const key = node?.key ?? node?.property
|
|
@@ -70,7 +70,7 @@ export const publicComponentSignature = {
|
|
|
70
70
|
const actual = type?.type === "TSTypeReference" && type.typeName?.type === "Identifier" ? type.typeName.name : null
|
|
71
71
|
if (!actual || !expected.includes(actual)) context.report({ node: params[0], messageId: "type", data: { name, expected: expected.join(" or ") } })
|
|
72
72
|
}
|
|
73
|
-
if (!isComponentFile(context)
|
|
73
|
+
if (!isComponentFile(context)) return {}
|
|
74
74
|
return {
|
|
75
75
|
VariableDeclarator(node) {
|
|
76
76
|
const name = componentNameOf(node)
|
|
@@ -93,7 +93,7 @@ export const noPerPartClassNameProp = {
|
|
|
93
93
|
messages: { perPart: "Keep internal part styling owned by the component." },
|
|
94
94
|
},
|
|
95
95
|
create(context) {
|
|
96
|
-
if (!isComponentFile(context)
|
|
96
|
+
if (!isComponentFile(context)) return {}
|
|
97
97
|
return { TSPropertySignature(node) { const name = propertyName(node); if (name && name !== "className" && /^[a-z][A-Za-z0-9]*ClassName$/.test(name)) context.report({ node, messageId: "perPart" }) } }
|
|
98
98
|
},
|
|
99
99
|
}
|
|
@@ -107,7 +107,6 @@ export const noPublicClassNameProp = {
|
|
|
107
107
|
messages: { declaration: "Component props must not expose {{prop}}.", usage: "Do not pass {{prop}} to house component {{component}}." },
|
|
108
108
|
},
|
|
109
109
|
create(context) {
|
|
110
|
-
if (isSpecFile(fileOf(context))) return {}
|
|
111
110
|
const declared = isComponentFile(context)
|
|
112
111
|
/** A house component: the identifier resolves (through its import) to a file of a component layer. */
|
|
113
112
|
const isHouseComponent = (identifier) => declarationsOf(context, identifier).some((declaration) => isComponentPath(context, declarationFile(declaration)))
|
|
@@ -128,7 +127,7 @@ export const noPublicClassNameProp = {
|
|
|
128
127
|
export const noPublicFrameCssProps = {
|
|
129
128
|
meta: { type: "problem", docs: { description: "Non-leaf component props do not expose CSS-shaped frame decisions." }, schema: [], messages: { css: "Move {{prop}} into component-owned layout behavior." } },
|
|
130
129
|
create(context) {
|
|
131
|
-
if (
|
|
130
|
+
if (!isComponentFile(context) || kindOfFile(context) === "leaves") return {}
|
|
132
131
|
const cssProps = new Set(["gap", "padding", "align", "justify", "className", "classNames", "style", "inline", "nested"])
|
|
133
132
|
return { TSPropertySignature(node) { const name = propertyName(node); if (cssProps.has(name)) context.report({ node, messageId: "css", data: { prop: name } }) } }
|
|
134
133
|
},
|