@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/icon.mjs CHANGED
@@ -1,48 +1,47 @@
1
- /**
2
- * The rules that hold `icon.md`.
3
- *
4
- * Both exist because of the same escape, found by hand rather than by a gate: one leaf imported a
5
- * caret straight from the glyph library, at a size off both steps and a cut outside the two the
6
- * icon leaf offers. Nothing reported it. The vendor rule that guards the component library names
7
- * that library only, and the token rules that police the scale read SPACING utilities - gaps,
8
- * insets, offsets - so a glyph size is outside every pattern they match.
9
- *
10
- * The lesson is worth stating where the next rule gets written: a rule that names one vendor
11
- * protects one vendor, and a rule that scans one family of utilities protects that family. Neither
12
- * generalises on its own, and the gap between them is exactly where a third step gets invented.
13
- */
14
-
15
- /** Forward-slash form of a filename, so Windows paths compare like every other path. */
16
- const normalizePath = (filename) => String(filename || "").replace(/\\/g, "/")
17
-
18
- /** The one module allowed to name a glyph from a library. */
19
- const ICON_MODULE_RELATIVE = "leaves/Icon/index.tsx"
20
-
21
- /** Repeated goal / metric cells are text-led in the legacy reference. */
22
- const LABELLED_PROGRESS_ROW_RELATIVE = "composites/LabelledProgressRow/index.tsx"
23
-
24
- /**
25
- * Package roots that ship glyphs.
26
- *
27
- * Matched as a PREFIX so a subpath cannot walk around it - the escape that started this was
28
- * `@phosphor-icons/react/dist/ssr`, which an equality check on the package name does not see.
29
- */
30
- const GLYPH_PACKAGES = [
31
- "@phosphor-icons/",
32
- "lucide-react",
33
- "react-icons",
34
- "@heroicons/",
35
- "@tabler/icons",
36
- "@fortawesome/",
37
- "@mui/icons-material",
38
- "@fluentui/react-icons",
39
- "iconsax-react",
40
- "react-feather",
41
- ]
42
-
43
- /** Package-name signal for a glyph catalogue not yet listed explicitly. */
44
- const GLYPH_PACKAGE_NAME = /(?:icon|glyph|lucide|feather|tabler|fortawesome)/i
45
-
1
+ /**
2
+ * The rules that hold `icon.md`.
3
+ *
4
+ * Both exist because of the same escape, found by hand rather than by a gate: one leaf imported a
5
+ * caret straight from the glyph library, at a size off both steps and a cut outside the two the
6
+ * icon leaf offers. Nothing reported it. The vendor rule that guards the component library names
7
+ * that library only, and the token rules that police the scale read SPACING utilities - gaps,
8
+ * insets, offsets - so a glyph size is outside every pattern they match.
9
+ *
10
+ * The lesson is worth stating where the next rule gets written: a rule that names one vendor
11
+ * protects one vendor, and a rule that scans one family of utilities protects that family. Neither
12
+ * generalises on its own, and the gap between them is exactly where a third step gets invented.
13
+ */
14
+
15
+ import { normalizePath } from "./lib/path.mjs"
16
+
17
+ /** The one module allowed to name a glyph from a library. */
18
+ const ICON_MODULE_RELATIVE = "leaves/Icon/index.tsx"
19
+
20
+ /** Repeated goal / metric cells are text-led in the legacy reference. */
21
+ const LABELLED_PROGRESS_ROW_RELATIVE = "composites/LabelledProgressRow/index.tsx"
22
+
23
+ /**
24
+ * Package roots that ship glyphs.
25
+ *
26
+ * Matched as a PREFIX so a subpath cannot walk around it - the escape that started this was
27
+ * `@phosphor-icons/react/dist/ssr`, which an equality check on the package name does not see.
28
+ */
29
+ const GLYPH_PACKAGES = [
30
+ "@phosphor-icons/",
31
+ "lucide-react",
32
+ "react-icons",
33
+ "@heroicons/",
34
+ "@tabler/icons",
35
+ "@fortawesome/",
36
+ "@mui/icons-material",
37
+ "@fluentui/react-icons",
38
+ "iconsax-react",
39
+ "react-feather",
40
+ ]
41
+
42
+ /** Package-name signal for a glyph catalogue not yet listed explicitly. */
43
+ const GLYPH_PACKAGE_NAME = /(?:icon|glyph|lucide|feather|tabler|fortawesome)/i
44
+
46
45
  /** Upstream Heroicons plus StarCi's custom-only extension, each on the same two optical families. */
47
46
  const HEROICON_PACKAGES = new Set([
48
47
  "@heroicons/react/24/outline",
@@ -50,175 +49,175 @@ const HEROICON_PACKAGES = new Set([
50
49
  "@starci/heroicons/24/outline",
51
50
  "@starci/heroicons/16/solid",
52
51
  ])
53
-
54
- /**
55
- * Product reactions are attributed artwork rather than glyph-library imports.
56
- *
57
- * Their leaf owns this closed vocabulary and maps it to checked-in Fluent Emoji Flat SVGs under
58
- * `public/reactions/`; callers pass identities, never Unicode pictographs or asset paths.
59
- */
60
- export const REACTION_ASSET_NAMES = new Set(["like", "love", "haha", "wow", "sad", "angry"])
61
-
62
- /** True when this file is the icon leaf, which owns the meaning-to-glyph map. */
63
- const isIconLeafFile = (filename) => normalizePath(filename).endsWith(`/${ICON_MODULE_RELATIVE}`)
64
-
65
- /** True for the reusable repeated metric cell whose reference contains no decorative glyph. */
66
- const isLabelledProgressRowFile = (filename) =>
67
- normalizePath(filename).endsWith(`/${LABELLED_PROGRESS_ROW_RELATIVE}`)
68
-
69
- /** Product source under `src/`; tooling and config are out of scope. */
70
- const isSourceFile = (filename) => normalizePath(filename).includes("/src/")
71
-
72
- /** True when an import source resolves into a glyph library, subpaths included. */
73
- const isGlyphImport = (source) => {
74
- const value = typeof source === "string" ? source : ""
75
- const isExternalPackage = value !== "" && !value.startsWith(".") && !value.startsWith("@/")
76
- return GLYPH_PACKAGES.some((pkg) => value === pkg || value.startsWith(pkg)) ||
77
- (isExternalPackage && GLYPH_PACKAGE_NAME.test(value))
78
- }
79
-
80
- /** A glyph size written as a fraction of a step, or as an arbitrary value. */
81
- const OFF_SCALE_GLYPH = /\bsize-(?:\d+\.\d+|\[[^\]]+\])/
82
-
83
- /** True for a `className` / `class` JSX attribute. */
84
- const isClassAttribute = (node) =>
85
- node.type === "JSXAttribute" && node.name && (node.name.name === "className" || node.name.name === "class")
86
-
87
- /** Static string carried by a JSX attribute, or by a module constant holding a class string. */
88
- const staticText = (value) => {
89
- if (!value) return null
90
- if (value.type === "Literal" && typeof value.value === "string") return value.value
91
- if (value.type === "TemplateLiteral" && value.expressions.length === 0) {
92
- return value.quasis.map((quasi) => quasi.value.cooked).join(" ")
93
- }
94
- if (value.type === "JSXExpressionContainer") return staticText(value.expression)
95
- return null
96
- }
97
-
98
- // -- ICON-6 ----------------------------------------------------------------------------------------
99
-
100
- /** A glyph library is named in one file, and a caller asks for a MEANING instead. */
101
- export const noVendorIconOutsideIconLeaf = {
102
- meta: {
103
- type: "problem",
104
- docs: { description: "Glyph libraries are imported only by the icon leaf." },
105
- schema: [],
106
- messages: {
107
- vendor:
108
- "`{{source}}` is a glyph library, imported outside `{{leaf}}`. Reaching for it here decides three things at the call site - which library, which glyph, how big - and the next screen answers all three differently. Ask for the MEANING instead; if no meaning fits, add one to the icon leaf, which is where the answer is kept once.",
109
- },
110
- },
111
- create(context) {
112
- const file = normalizePath(context.filename || context.getFilename())
113
- if (!isSourceFile(file) || isIconLeafFile(file)) return {}
114
- return {
115
- ImportDeclaration(node) {
116
- const source = node.source && node.source.value
117
- if (!isGlyphImport(source)) return
52
+
53
+ /**
54
+ * Product reactions are attributed artwork rather than glyph-library imports.
55
+ *
56
+ * Their leaf owns this closed vocabulary and maps it to checked-in Fluent Emoji Flat SVGs under
57
+ * `public/reactions/`; callers pass identities, never Unicode pictographs or asset paths.
58
+ */
59
+ export const REACTION_ASSET_NAMES = new Set(["like", "love", "haha", "wow", "sad", "angry"])
60
+
61
+ /** True when this file is the icon leaf, which owns the meaning-to-glyph map. */
62
+ const isIconLeafFile = (filename) => normalizePath(filename).endsWith(`/${ICON_MODULE_RELATIVE}`)
63
+
64
+ /** True for the reusable repeated metric cell whose reference contains no decorative glyph. */
65
+ const isLabelledProgressRowFile = (filename) =>
66
+ normalizePath(filename).endsWith(`/${LABELLED_PROGRESS_ROW_RELATIVE}`)
67
+
68
+ /** Product source under `src/`; tooling and config are out of scope. */
69
+ const isSourceFile = (filename) => normalizePath(filename).includes("/src/")
70
+
71
+ /** True when an import source resolves into a glyph library, subpaths included. */
72
+ const isGlyphImport = (source) => {
73
+ const value = typeof source === "string" ? source : ""
74
+ const isExternalPackage = value !== "" && !value.startsWith(".") && !value.startsWith("@/")
75
+ return GLYPH_PACKAGES.some((pkg) => value === pkg || value.startsWith(pkg)) ||
76
+ (isExternalPackage && GLYPH_PACKAGE_NAME.test(value))
77
+ }
78
+
79
+ /** A glyph size written as a fraction of a step, or as an arbitrary value. */
80
+ const OFF_SCALE_GLYPH = /\bsize-(?:\d+\.\d+|\[[^\]]+\])/
81
+
82
+ /** True for a `className` / `class` JSX attribute. */
83
+ const isClassAttribute = (node) =>
84
+ node.type === "JSXAttribute" && node.name && (node.name.name === "className" || node.name.name === "class")
85
+
86
+ /** Static string carried by a JSX attribute, or by a module constant holding a class string. */
87
+ const staticText = (value) => {
88
+ if (!value) return null
89
+ if (value.type === "Literal" && typeof value.value === "string") return value.value
90
+ if (value.type === "TemplateLiteral" && value.expressions.length === 0) {
91
+ return value.quasis.map((quasi) => quasi.value.cooked).join(" ")
92
+ }
93
+ if (value.type === "JSXExpressionContainer") return staticText(value.expression)
94
+ return null
95
+ }
96
+
97
+ // -- ICON-6 ----------------------------------------------------------------------------------------
98
+
99
+ /** A glyph library is named in one file, and a caller asks for a MEANING instead. */
100
+ export const noVendorIconOutsideIconLeaf = {
101
+ meta: {
102
+ type: "problem",
103
+ docs: { description: "Glyph libraries are imported only by the icon leaf." },
104
+ schema: [],
105
+ messages: {
106
+ vendor:
107
+ "`{{source}}` is a glyph library, imported outside `{{leaf}}`. Reaching for it here decides three things at the call site - which library, which glyph, how big - and the next screen answers all three differently. Ask for the MEANING instead; if no meaning fits, add one to the icon leaf, which is where the answer is kept once.",
108
+ },
109
+ },
110
+ create(context) {
111
+ const file = normalizePath(context.filename || context.getFilename())
112
+ if (!isSourceFile(file) || isIconLeafFile(file)) return {}
113
+ return {
114
+ ImportDeclaration(node) {
115
+ const source = node.source && node.source.value
116
+ if (!isGlyphImport(source)) return
118
117
  context.report({ node, messageId: "vendor", data: { source, leaf: ICON_MODULE_RELATIVE } })
119
- },
120
- }
121
- },
122
- }
123
-
124
- // -- ICON-7 ----------------------------------------------------------------------------------------
125
-
126
- /** The icon leaf owns the map, but that ownership does not license a second glyph vocabulary. */
127
- export const heroiconsIsTheGlyphVendor = {
128
- meta: {
129
- type: "problem",
130
- docs: { description: "Only the Heroicons heading and micro families may supply product glyphs." },
131
- schema: [],
118
+ },
119
+ }
120
+ },
121
+ }
122
+
123
+ // -- ICON-7 ----------------------------------------------------------------------------------------
124
+
125
+ /** The icon leaf owns the map, but that ownership does not license a second glyph vocabulary. */
126
+ export const heroiconsIsTheGlyphVendor = {
127
+ meta: {
128
+ type: "problem",
129
+ docs: { description: "Only the Heroicons heading and micro families may supply product glyphs." },
130
+ schema: [],
132
131
  messages: {
133
132
  vendor:
134
133
  "`{{source}}` is outside StarCi's closed glyph surface. Use upstream `@heroicons/react/24/outline` or `16/solid` when the icon exists; use the matching `@starci/heroicons` subpath only for a custom cut missing upstream. The StarCi package never re-exports upstream icons. Iconify, Phosphor and every other glyph catalogue are refused.",
135
- },
136
- },
137
- create(context) {
138
- const file = normalizePath(context.filename || context.getFilename())
139
- if (!isSourceFile(file)) return {}
140
- return {
141
- ImportDeclaration(node) {
142
- const source = node.source && node.source.value
143
- if (!isGlyphImport(source) || HEROICON_PACKAGES.has(String(source))) return
134
+ },
135
+ },
136
+ create(context) {
137
+ const file = normalizePath(context.filename || context.getFilename())
138
+ if (!isSourceFile(file)) return {}
139
+ return {
140
+ ImportDeclaration(node) {
141
+ const source = node.source && node.source.value
142
+ if (!isGlyphImport(source) || HEROICON_PACKAGES.has(String(source))) return
144
143
  context.report({ node, messageId: "vendor", data: { source } })
145
- },
146
- }
147
- },
148
- }
149
-
150
- // -- ICON-1 ----------------------------------------------------------------------------------------
151
-
152
- /** A glyph takes one of the two steps; a fraction or an arbitrary value is a third. */
153
- export const noOffScaleGlyphSize = {
154
- meta: {
155
- type: "problem",
156
- docs: { description: "A glyph is sized by one of the two steps, never by a fraction or an arbitrary value." },
157
- schema: [],
158
- messages: {
159
- offScale:
160
- "`{{cls}}` is a glyph size off both steps. A third step is one nobody applies consistently - the author picks it for a reason true on their screen, and everyone after copies the nearest of three. Use the step the icon leaf offers, and if the case genuinely needs a size neither step gives, that is a change to the two steps rather than an exception here.",
161
- },
162
- },
163
- create(context) {
164
- const file = normalizePath(context.filename || context.getFilename())
165
- if (!isSourceFile(file)) return {}
166
- const scan = (node, text) => {
167
- if (!text) return
168
- const hit = text.match(OFF_SCALE_GLYPH)
169
- if (hit) context.report({ node, messageId: "offScale", data: { cls: hit[0] } })
170
- }
171
- return {
172
- JSXAttribute(node) {
173
- if (!isClassAttribute(node)) return
174
- scan(node, staticText(node.value))
175
- },
176
- VariableDeclarator(node) {
177
- scan(node, staticText(node.init))
178
- },
179
- }
180
- },
181
- }
182
-
183
- // -- ICON-10 ---------------------------------------------------------------------------------------
184
-
185
- /** A repeated metric cell does not invent a feature glyph absent from the reference. */
186
- export const noDecorativeIconInMetricCell = {
187
- meta: {
188
- type: "problem",
189
- docs: { description: "Repeated metric cells stay text-led unless the reference explicitly contains an icon." },
190
- schema: [],
191
- messages: {
192
- decorative:
193
- "`LabelledProgressRow` is a repeated metric cell, not a feature entry. Its legacy reference is text-led, so an `Icon` here invents emphasis and repeats glyph meanings across the grid. Keep the label and figure; reserve tiny glyphs for generic state or action semantics actually present in the reference.",
194
- },
195
- },
196
- create(context) {
197
- const file = normalizePath(context.filename || context.getFilename())
198
- if (!isLabelledProgressRowFile(file)) return {}
199
- return {
200
- JSXOpeningElement(node) {
201
- if (node.name?.type === "JSXIdentifier" && node.name.name === "Icon") {
202
- context.report({ node, messageId: "decorative" })
203
- }
204
- },
205
- }
206
- },
207
- }
208
-
209
- /** The rules this law contributes to the plugin. */
210
- export const rules = {
211
- "no-vendor-icon-outside-icon-leaf": noVendorIconOutsideIconLeaf,
212
- "heroicons-is-the-glyph-vendor": heroiconsIsTheGlyphVendor,
213
- "no-off-scale-glyph-size": noOffScaleGlyphSize,
214
- "no-decorative-icon-in-metric-cell": noDecorativeIconInMetricCell,
144
+ },
145
+ }
146
+ },
147
+ }
148
+
149
+ // -- ICON-1 ----------------------------------------------------------------------------------------
150
+
151
+ /** A glyph takes one of the two steps; a fraction or an arbitrary value is a third. */
152
+ export const noOffScaleGlyphSize = {
153
+ meta: {
154
+ type: "problem",
155
+ docs: { description: "A glyph is sized by one of the two steps, never by a fraction or an arbitrary value." },
156
+ schema: [],
157
+ messages: {
158
+ offScale:
159
+ "`{{cls}}` is a glyph size off both steps. A third step is one nobody applies consistently - the author picks it for a reason true on their screen, and everyone after copies the nearest of three. Use the step the icon leaf offers, and if the case genuinely needs a size neither step gives, that is a change to the two steps rather than an exception here.",
160
+ },
161
+ },
162
+ create(context) {
163
+ const file = normalizePath(context.filename || context.getFilename())
164
+ if (!isSourceFile(file)) return {}
165
+ const scan = (node, text) => {
166
+ if (!text) return
167
+ const hit = text.match(OFF_SCALE_GLYPH)
168
+ if (hit) context.report({ node, messageId: "offScale", data: { cls: hit[0] } })
169
+ }
170
+ return {
171
+ JSXAttribute(node) {
172
+ if (!isClassAttribute(node)) return
173
+ scan(node, staticText(node.value))
174
+ },
175
+ VariableDeclarator(node) {
176
+ scan(node, staticText(node.init))
177
+ },
178
+ }
179
+ },
180
+ }
181
+
182
+ // -- ICON-10 ---------------------------------------------------------------------------------------
183
+
184
+ /** A repeated metric cell does not invent a feature glyph absent from the reference. */
185
+ export const noDecorativeIconInMetricCell = {
186
+ meta: {
187
+ type: "problem",
188
+ docs: { description: "Repeated metric cells stay text-led unless the reference explicitly contains an icon." },
189
+ schema: [],
190
+ messages: {
191
+ decorative:
192
+ "`LabelledProgressRow` is a repeated metric cell, not a feature entry. Its legacy reference is text-led, so an `Icon` here invents emphasis and repeats glyph meanings across the grid. Keep the label and figure; reserve tiny glyphs for generic state or action semantics actually present in the reference.",
193
+ },
194
+ },
195
+ create(context) {
196
+ const file = normalizePath(context.filename || context.getFilename())
197
+ if (!isLabelledProgressRowFile(file)) return {}
198
+ return {
199
+ JSXOpeningElement(node) {
200
+ if (node.name?.type === "JSXIdentifier" && node.name.name === "Icon") {
201
+ context.report({ node, messageId: "decorative" })
202
+ }
203
+ },
204
+ }
205
+ },
206
+ }
207
+
208
+ /** The rules this law contributes to the plugin. */
209
+ export const rules = {
210
+ "no-vendor-icon-outside-icon-leaf": noVendorIconOutsideIconLeaf,
211
+ "heroicons-is-the-glyph-vendor": heroiconsIsTheGlyphVendor,
212
+ "no-off-scale-glyph-size": noOffScaleGlyphSize,
213
+ "no-decorative-icon-in-metric-cell": noDecorativeIconInMetricCell,
215
214
  }
216
-
217
- /**
218
- * The level this law asks for, as the plugin's own opinion.
219
- *
220
- * A consuming repository's `eslint.config.mjs` stays the authority on what is actually switched on.
221
- * Both rules are exact - a package prefix and a size pattern - so neither carries the false-positive
222
- * risk that would justify adopting it at `warn`.
223
- */
224
- export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]))
215
+
216
+ /**
217
+ * The level this law asks for, as the plugin's own opinion.
218
+ *
219
+ * A consuming repository's `eslint.config.mjs` stays the authority on what is actually switched on.
220
+ * Both rules are exact - a package prefix and a size pattern - so neither carries the false-positive
221
+ * risk that would justify adopting it at `warn`.
222
+ */
223
+ export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]))