@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.
Files changed (53) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +41 -16
  3. package/brand-values.mjs +174 -0
  4. package/class-names.mjs +1 -1
  5. package/client-boundary.mjs +199 -0
  6. package/comments.mjs +144 -190
  7. package/docs/brand-values.md +30 -0
  8. package/docs/client-boundary.md +99 -0
  9. package/docs/e2e-shape.md +161 -0
  10. package/docs/env-owner.md +52 -0
  11. package/docs/formatting.md +30 -0
  12. package/docs/hooks-folder.md +29 -0
  13. package/docs/hygiene.md +111 -0
  14. package/docs/lint-escape-hatch.md +32 -0
  15. package/docs/lists.md +74 -0
  16. package/docs/native-controls.md +73 -0
  17. package/docs/next-conventions.md +206 -0
  18. package/docs/size-and-state-budget.md +73 -0
  19. package/docs/spec-quality.md +139 -0
  20. package/docs/translation.md +50 -0
  21. package/docs/transport.md +168 -0
  22. package/docs/type-safety.md +95 -0
  23. package/e2e-shape.mjs +348 -0
  24. package/env-owner.mjs +134 -0
  25. package/file-layout.mjs +100 -7
  26. package/formatting.mjs +106 -0
  27. package/hooks-folder.mjs +119 -0
  28. package/hygiene.mjs +235 -0
  29. package/icon.mjs +210 -211
  30. package/index.mjs +292 -131
  31. package/lib/ast.mjs +94 -0
  32. package/lib/path.mjs +2 -0
  33. package/lib/scope.mjs +42 -0
  34. package/lib/why.mjs +304 -0
  35. package/lint-adoption.mjs +15 -52
  36. package/lint-escape-hatch.mjs +91 -56
  37. package/lists.mjs +197 -0
  38. package/loading.mjs +163 -164
  39. package/naming.mjs +143 -143
  40. package/native-controls.mjs +112 -0
  41. package/next-conventions.mjs +346 -0
  42. package/package.json +7 -3
  43. package/props-and-slots.mjs +4 -2
  44. package/served-locale.mjs +160 -160
  45. package/shape-slot.mjs +339 -0
  46. package/size-and-state-budget.mjs +196 -0
  47. package/spec-quality.mjs +239 -0
  48. package/the-split.mjs +133 -134
  49. package/tokens.mjs +267 -268
  50. package/translation.mjs +262 -122
  51. package/transport.mjs +404 -0
  52. package/type-safety.mjs +156 -74
  53. package/typography.mjs +82 -83
package/the-split.mjs CHANGED
@@ -1,153 +1,152 @@
1
- /**
2
- * The rule that holds `the-split.md`.
3
- *
4
- * ONE RULE, AND IT WATCHES ONE HALF. The drawing half's purity is checkable: a file that fetches,
5
- * reads a store or resolves a word is visible in what it calls. The connected half's discipline is
6
- * not - a file that draws badly, or settles the wrong situation, looks exactly like one that does
7
- * neither. So this rule guards the boundary from the side where a machine can see it, and the other
8
- * side stays a matter for review.
9
- *
10
- * The scope is the filename. `component.tsx` is the drawing half by convention across every tier
11
- * that splits, so the convention does the scoping and the rule needs no configuration.
12
- */
13
-
14
- /** Forward-slash form of a filename, so Windows paths compare like every other path. */
15
- const normalizePath = (filename) => String(filename || "").replace(/\\/g, "/")
16
-
17
- /** The drawing half, named by the convention every split surface follows. */
18
- const isDrawingHalf = (filename) => /(?:^|\/)component\.tsx$/.test(normalizePath(filename))
19
-
20
- /** A connected block entrypoint and the public name its folder fixes. */
21
- const connectedBlock = (filename) => {
22
- const match = normalizePath(filename).match(
23
- /\/src\/components\/blocks\/(?:[^/]+\/)*([A-Z][A-Za-z0-9]*)\/index\.tsx$/,
24
- )
25
- return match ? match[1] : null
26
- }
27
-
28
- /**
29
- * Calls that reach for the world instead of receiving it.
30
- *
31
- * Four families: a request, a store, the translation runtime, and a direct query. Each is a
32
- * dependency the drawing half cannot be rendered from a fixture without.
33
- */
34
- const REACHES_FOR_THE_WORLD =
35
- /^(?:useSWR|useSWRMutation|use[A-Za-z0-9]*Swr|useAppSelector|useDispatch|use[A-Za-z0-9]*Store|useTranslations|useLocale|useFormatter|query[A-Z][A-Za-z0-9]*|mutation[A-Z][A-Za-z0-9]*)$/
36
-
37
- // -- SPLIT-1 ---------------------------------------------------------------------------------------
38
-
39
- /** The drawing half receives everything and asks for nothing. */
40
- export const presentationalPurity = {
41
- meta: {
42
- type: "problem",
43
- docs: { description: "The drawing half takes resolved props: no request, no store, no translation." },
44
- schema: [],
45
- messages: {
46
- reaches:
47
- "`{{name}}(...)` reaches for the world from the drawing half. This file must be renderable from a fixture - that is what makes it testable without standing the world up first, and it is the whole reason the split exists. Move the call to `index.tsx` and pass the result down as a settled situation.",
48
- },
49
- },
50
- create(context) {
51
- if (!isDrawingHalf(context.filename || context.getFilename())) return {}
52
- return {
53
- CallExpression(node) {
54
- const callee = node.callee
55
- if (!callee || callee.type !== "Identifier") return
56
- if (!REACHES_FOR_THE_WORLD.test(callee.name)) return
57
- context.report({ node, messageId: "reaches", data: { name: callee.name } })
58
- },
59
- }
60
- },
61
- }
62
-
63
- // -- SPLIT-5 ---------------------------------------------------------------------------------------
64
-
65
- /** A connected block delegates every render path to its exact pure twin. */
66
- export const connectedBlockHasPresentationalTwin = {
67
- meta: {
68
- type: "problem",
1
+ /**
2
+ * The rule that holds `the-split.md`.
3
+ *
4
+ * ONE RULE, AND IT WATCHES ONE HALF. The drawing half's purity is checkable: a file that fetches,
5
+ * reads a store or resolves a word is visible in what it calls. The connected half's discipline is
6
+ * not - a file that draws badly, or settles the wrong situation, looks exactly like one that does
7
+ * neither. So this rule guards the boundary from the side where a machine can see it, and the other
8
+ * side stays a matter for review.
9
+ *
10
+ * The scope is the filename. `component.tsx` is the drawing half by convention across every tier
11
+ * that splits, so the convention does the scoping and the rule needs no configuration.
12
+ */
13
+
14
+ import { normalizePath } from "./lib/path.mjs"
15
+
16
+ /** The drawing half, named by the convention every split surface follows. */
17
+ const isDrawingHalf = (filename) => /(?:^|\/)component\.tsx$/.test(normalizePath(filename))
18
+
19
+ /** A connected block entrypoint and the public name its folder fixes. */
20
+ const connectedBlock = (filename) => {
21
+ const match = normalizePath(filename).match(
22
+ /\/src\/components\/blocks\/(?:[^/]+\/)*([A-Z][A-Za-z0-9]*)\/index\.tsx$/,
23
+ )
24
+ return match ? match[1] : null
25
+ }
26
+
27
+ /**
28
+ * Calls that reach for the world instead of receiving it.
29
+ *
30
+ * Four families: a request, a store, the translation runtime, and a direct query. Each is a
31
+ * dependency the drawing half cannot be rendered from a fixture without.
32
+ */
33
+ const REACHES_FOR_THE_WORLD =
34
+ /^(?:useSWR|useSWRMutation|use[A-Za-z0-9]*Swr|useAppSelector|useDispatch|use[A-Za-z0-9]*Store|useTranslations|useLocale|useFormatter|query[A-Z][A-Za-z0-9]*|mutation[A-Z][A-Za-z0-9]*)$/
35
+
36
+ // -- SPLIT-1 ---------------------------------------------------------------------------------------
37
+
38
+ /** The drawing half receives everything and asks for nothing. */
39
+ export const presentationalPurity = {
40
+ meta: {
41
+ type: "problem",
42
+ docs: { description: "The drawing half takes resolved props: no request, no store, no translation." },
43
+ schema: [],
44
+ messages: {
45
+ reaches:
46
+ "`{{name}}(...)` reaches for the world from the drawing half. This file must be renderable from a fixture - that is what makes it testable without standing the world up first, and it is the whole reason the split exists. Move the call to `index.tsx` and pass the result down as a settled situation.",
47
+ },
48
+ },
49
+ create(context) {
50
+ if (!isDrawingHalf(context.filename || context.getFilename())) return {}
51
+ return {
52
+ CallExpression(node) {
53
+ const callee = node.callee
54
+ if (!callee || callee.type !== "Identifier") return
55
+ if (!REACHES_FOR_THE_WORLD.test(callee.name)) return
56
+ context.report({ node, messageId: "reaches", data: { name: callee.name } })
57
+ },
58
+ }
59
+ },
60
+ }
61
+
62
+ // -- SPLIT-5 ---------------------------------------------------------------------------------------
63
+
64
+ /** A connected block delegates every render path to its exact pure twin. */
65
+ export const connectedBlockHasPresentationalTwin = {
66
+ meta: {
67
+ type: "problem",
69
68
  docs: { description: "A connected block index renders only its exact `XBase` twin from component.tsx." },
70
- schema: [],
71
- messages: {
72
- missing:
73
- "`{{block}}` reads the world, so it must import exact `{{twin}}` from `./component`. A single leaf, one tree, or no local state is not an exception.",
74
- bypass:
75
- "Connected `{{block}}` renders `<{{rendered}}>` directly. Resolve the world here, then render only `<{{twin}}>`; component.tsx owns the complete presentation.",
69
+ schema: [],
70
+ messages: {
71
+ missing:
72
+ "`{{block}}` reads the world, so it must import exact `{{twin}}` from `./component`. A single leaf, one tree, or no local state is not an exception.",
73
+ bypass:
74
+ "Connected `{{block}}` renders `<{{rendered}}>` directly. Resolve the world here, then render only `<{{twin}}>`; component.tsx owns the complete presentation.",
76
75
  unused:
77
76
  "Connected `{{block}}` imports `{{twin}}` but never renders it. Every connected render path must cross the pure twin.",
78
77
  called:
79
78
  "Connected `{{block}}` calls `{{twin}}(...)` as an ordinary function. Render `<{{twin}} />` so React owns the twin's hook lifecycle and every render keeps one stable component boundary.",
80
- },
81
- },
82
- create(context) {
83
- const block = connectedBlock(context.filename || context.getFilename())
84
- if (!block) return {}
85
-
79
+ },
80
+ },
81
+ create(context) {
82
+ const block = connectedBlock(context.filename || context.getFilename())
83
+ if (!block) return {}
84
+
86
85
  const twin = `${block}Base`
87
86
  const worldBindings = new Set()
88
87
  const rendered = []
89
88
  const calledTwin = []
90
- let importsTwin = false
91
- let readsWorld = false
92
- let rendersTwin = false
93
-
94
- return {
95
- ImportDeclaration(node) {
96
- const source = typeof node.source?.value === "string" ? node.source.value : ""
97
- for (const specifier of node.specifiers || []) {
98
- const imported = specifier.imported?.name
99
- const local = specifier.local?.name
100
- if (source === "./component" && imported === twin && local === twin) importsTwin = true
101
- if (imported && local && REACHES_FOR_THE_WORLD.test(imported)) worldBindings.add(local)
102
- }
103
- },
89
+ let importsTwin = false
90
+ let readsWorld = false
91
+ let rendersTwin = false
92
+
93
+ return {
94
+ ImportDeclaration(node) {
95
+ const source = typeof node.source?.value === "string" ? node.source.value : ""
96
+ for (const specifier of node.specifiers || []) {
97
+ const imported = specifier.imported?.name
98
+ const local = specifier.local?.name
99
+ if (source === "./component" && imported === twin && local === twin) importsTwin = true
100
+ if (imported && local && REACHES_FOR_THE_WORLD.test(imported)) worldBindings.add(local)
101
+ }
102
+ },
104
103
  CallExpression(node) {
105
104
  if (node.callee?.type !== "Identifier") return
106
105
  if (node.callee.name === twin) calledTwin.push(node)
107
106
  if (worldBindings.has(node.callee.name) || REACHES_FOR_THE_WORLD.test(node.callee.name)) {
108
- readsWorld = true
109
- }
110
- },
111
- JSXOpeningElement(node) {
112
- if (node.name?.type !== "JSXIdentifier") return
113
- rendered.push({ name: node.name.name, node })
114
- if (node.name.name === twin) rendersTwin = true
115
- },
116
- "Program:exit"(node) {
117
- if (!readsWorld) return
118
- if (!importsTwin) {
119
- context.report({ node, messageId: "missing", data: { block, twin } })
120
- return
121
- }
107
+ readsWorld = true
108
+ }
109
+ },
110
+ JSXOpeningElement(node) {
111
+ if (node.name?.type !== "JSXIdentifier") return
112
+ rendered.push({ name: node.name.name, node })
113
+ if (node.name.name === twin) rendersTwin = true
114
+ },
115
+ "Program:exit"(node) {
116
+ if (!readsWorld) return
117
+ if (!importsTwin) {
118
+ context.report({ node, messageId: "missing", data: { block, twin } })
119
+ return
120
+ }
122
121
  for (const entry of rendered) {
123
- if (entry.name !== twin) {
124
- context.report({
125
- node: entry.node,
126
- messageId: "bypass",
127
- data: { block, rendered: entry.name, twin },
128
- })
129
- }
122
+ if (entry.name !== twin) {
123
+ context.report({
124
+ node: entry.node,
125
+ messageId: "bypass",
126
+ data: { block, rendered: entry.name, twin },
127
+ })
128
+ }
130
129
  }
131
130
  for (const call of calledTwin) {
132
131
  context.report({ node: call, messageId: "called", data: { block, twin } })
133
132
  }
134
133
  if (!rendersTwin) context.report({ node, messageId: "unused", data: { block, twin } })
135
- },
136
- }
137
- },
138
- }
139
-
140
- /** The rules this law contributes to the plugin. */
141
- export const rules = {
142
- "presentational-purity": presentationalPurity,
143
- "connected-block-has-presentational-twin": connectedBlockHasPresentationalTwin,
144
- }
145
-
146
- /**
147
- * The level this law asks for, as the plugin's own opinion.
148
- *
149
- * Exact: it fires on a call by name, in one file by name. A repository adopting it with history
150
- * should expect each report to be a real move rather than a deletion - the call has to go
151
- * somewhere, and where it goes is the other half.
152
- */
153
- export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]))
134
+ },
135
+ }
136
+ },
137
+ }
138
+
139
+ /** The rules this law contributes to the plugin. */
140
+ export const rules = {
141
+ "presentational-purity": presentationalPurity,
142
+ "connected-block-has-presentational-twin": connectedBlockHasPresentationalTwin,
143
+ }
144
+
145
+ /**
146
+ * The level this law asks for, as the plugin's own opinion.
147
+ *
148
+ * Exact: it fires on a call by name, in one file by name. A repository adopting it with history
149
+ * should expect each report to be a real move rather than a deletion - the call has to go
150
+ * somewhere, and where it goes is the other half.
151
+ */
152
+ export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]))