@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
@@ -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.0.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",
@@ -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
  },