@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.
Files changed (117) hide show
  1. package/CHANGELOG.md +18 -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 +18 -18
  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 +5 -27
  28. package/lib/config.mjs +6 -22
  29. package/lib/scope.mjs +3 -12
  30. package/lib/why.mjs +3 -68
  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/project-graph.mjs +23 -0
  38. package/props-and-slots.mjs +4 -5
  39. package/runtime/knowledge/hfs/slots.yaml +145 -107
  40. package/runtime/knowledge/patterns/fe/folder.yaml +309 -0
  41. package/runtime/scripts/checks/architecture/backend.mjs +350 -0
  42. package/runtime/scripts/checks/architecture/background-unowned.mjs +107 -0
  43. package/runtime/scripts/checks/architecture/client-reaches-server.mjs +94 -0
  44. package/runtime/scripts/checks/architecture/clones.mjs +200 -0
  45. package/runtime/scripts/checks/architecture/config-unread.mjs +35 -0
  46. package/runtime/scripts/checks/architecture/config.mjs +310 -0
  47. package/runtime/scripts/checks/architecture/connection-map.mjs +208 -0
  48. package/runtime/scripts/checks/architecture/constructor-deps.mjs +100 -0
  49. package/runtime/scripts/checks/architecture/contract-fixture-guard.mjs +128 -0
  50. package/runtime/scripts/checks/architecture/contracts.mjs +792 -0
  51. package/runtime/scripts/checks/architecture/cross-app-duplicate.mjs +73 -0
  52. package/runtime/scripts/checks/architecture/dead-exports.mjs +265 -0
  53. package/runtime/scripts/checks/architecture/default-deny.mjs +129 -0
  54. package/runtime/scripts/checks/architecture/doc-language.mjs +39 -0
  55. package/runtime/scripts/checks/architecture/entrypoint.mjs +57 -0
  56. package/runtime/scripts/checks/architecture/error-codes.mjs +45 -0
  57. package/runtime/scripts/checks/architecture/error-masked.mjs +60 -0
  58. package/runtime/scripts/checks/architecture/fe-slot-allows.mjs +38 -0
  59. package/runtime/scripts/checks/architecture/feature-shape.mjs +49 -0
  60. package/runtime/scripts/checks/architecture/framework-pinned.mjs +83 -0
  61. package/runtime/scripts/checks/architecture/frontend.mjs +995 -0
  62. package/runtime/scripts/checks/architecture/hfs-graph.mjs +61 -0
  63. package/runtime/scripts/checks/architecture/hfs.mjs +521 -0
  64. package/runtime/scripts/checks/architecture/hooks-are-hooks.mjs +88 -0
  65. package/runtime/scripts/checks/architecture/i18n-keys.mjs +146 -0
  66. package/runtime/scripts/checks/architecture/index.mjs +321 -0
  67. package/runtime/scripts/checks/architecture/injection-token-exported.mjs +62 -0
  68. package/runtime/scripts/checks/architecture/machine-ast.mjs +138 -0
  69. package/runtime/scripts/checks/architecture/module-per-transport.mjs +130 -0
  70. package/runtime/scripts/checks/architecture/next-data.mjs +775 -0
  71. package/runtime/scripts/checks/architecture/owners.mjs +95 -0
  72. package/runtime/scripts/checks/architecture/package-shape.mjs +63 -0
  73. package/runtime/scripts/checks/architecture/reachability.mjs +233 -0
  74. package/runtime/scripts/checks/architecture/register-once.mjs +141 -0
  75. package/runtime/scripts/checks/architecture/registration.mjs +319 -0
  76. package/runtime/scripts/checks/architecture/required-files.mjs +172 -0
  77. package/runtime/scripts/checks/architecture/route-files-thin.mjs +97 -0
  78. package/runtime/scripts/checks/architecture/schema-owner.mjs +261 -0
  79. package/runtime/scripts/checks/architecture/source-names.mjs +607 -0
  80. package/runtime/scripts/checks/architecture/sql-owner.mjs +142 -0
  81. package/runtime/scripts/checks/architecture/sql-tokens.mjs +327 -0
  82. package/runtime/scripts/checks/architecture/surface.mjs +91 -0
  83. package/runtime/scripts/checks/architecture/symbols.mjs +193 -0
  84. package/runtime/scripts/checks/architecture/test-world-files.mjs +163 -0
  85. package/runtime/scripts/checks/architecture/tiers.mjs +130 -0
  86. package/runtime/scripts/checks/architecture/transport-owner.mjs +112 -0
  87. package/runtime/scripts/checks/architecture/typescript.mjs +573 -0
  88. package/runtime/scripts/checks/architecture/unit-spec-providers.mjs +122 -0
  89. package/runtime/scripts/checks/common.mjs +44 -0
  90. package/runtime/scripts/checks/typescript-programs.mjs +82 -0
  91. package/runtime/scripts/lib/hfs-allows.mjs +15 -0
  92. package/runtime/scripts/lib/hfs-path-findings.mjs +84 -0
  93. package/runtime/scripts/lib/hfs-rules/fe-no-tests.mjs +47 -0
  94. package/runtime/scripts/lib/hfs-rules/read.mjs +28 -0
  95. package/runtime/scripts/lib/hfs-rules/secrets.mjs +54 -0
  96. package/runtime/scripts/lib/hfs-rules/spec-placement.mjs +29 -0
  97. package/runtime/scripts/lib/hfs-slots.mjs +13 -6
  98. package/runtime/scripts/lib/json.mjs +22 -0
  99. package/runtime/scripts/lib/language.mjs +107 -0
  100. package/runtime/scripts/lib/project-graph.mjs +0 -0
  101. package/runtime/scripts/lib/project-rule.mjs +39 -0
  102. package/runtime/scripts/lib/repo-identity.mjs +50 -0
  103. package/runtime/scripts/lib/secret-patterns.mjs +44 -0
  104. package/runtime/scripts/lib/stack-services.mjs +217 -0
  105. package/runtime/scripts/lib/test-secrets.mjs +120 -0
  106. package/served-locale.mjs +2 -2
  107. package/shape-slot.mjs +2 -3
  108. package/size-and-state-budget.mjs +1 -4
  109. package/size-growth.mjs +1 -2
  110. package/status-colors.mjs +0 -2
  111. package/tokens.mjs +1 -1
  112. package/translation.mjs +1 -29
  113. package/transport.mjs +13 -17
  114. package/docs/e2e-shape.md +0 -161
  115. package/docs/spec-quality.md +0 -147
  116. package/e2e-shape.mjs +0 -345
  117. 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-http-status-collapse": {
49
+ "no-failure-collapse": {
50
50
  code: "FE_HTTP_STATUS_COLLAPSE",
51
- vi: "`<file>` gộp mọi mã HTTP thành một nhánh (hoặc trả null khi phản hồi lỗi). 401/403 phải thành `refused`.",
52
- fixVi: "Trả `Outcome` theo mã: `refused` (401/403), `not-found`, `invalid`, `unavailable`; không trả nguyên văn lỗi của server làm lý do.",
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.",
@@ -19,10 +19,10 @@
19
19
  * nothing is dead weight that reads as if it did.
20
20
  */
21
21
 
22
- import { fileOf, isE2eSource, isProductSource, isSpecFile } from "./lib/scope.mjs"
22
+ import { isProductSource } from "./lib/scope.mjs"
23
23
 
24
- /** Product source, its specs and the e2e tree are governed; canon tests deliberately build forbidden directives. */
25
- const isGoverned = (context) => isProductSource(context) || isSpecFile(fileOf(context)) || isE2eSource(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, isSpecFile } from "./lib/scope.mjs"
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) && !isSpecFile(fileOf(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/
@@ -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) {
@@ -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, isSpecFile, roleOfFile, slotOfFile, stem } from "./lib/scope.mjs"
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 (isSpecFile(fileOf(context)) || isNavigationOwner(context)) return {}
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 (isSpecFile(fileOf(context)) || isRoutesModule(context)) return {}
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": "6.0.2",
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"]))
@@ -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, isSpecFile, kindOfFile } from "./lib/scope.mjs"
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) || isSpecFile(fileOf(context))) return {}
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) || isSpecFile(fileOf(context))) return {}
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 (isSpecFile(fileOf(context)) || !isComponentFile(context) || kindOfFile(context) === "leaves") return {}
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
  },