@starci/eslint-canon-fe 9.2.0 → 9.4.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 +14 -0
- package/README.md +5 -1
- package/dist/brand-values.d.ts +28 -0
- package/dist/brand-values.js +172 -0
- package/dist/button-intent.d.ts +11 -0
- package/dist/button-intent.js +361 -0
- package/dist/callback-shape.d.ts +7 -0
- package/dist/callback-shape.js +396 -0
- package/dist/class-names.d.ts +19 -0
- package/dist/class-names.js +170 -0
- package/dist/client-boundary.d.ts +42 -0
- package/dist/client-boundary.js +404 -0
- package/dist/comments.d.ts +47 -0
- package/dist/comments.js +156 -0
- package/dist/control-height.d.ts +7 -0
- package/dist/control-height.js +83 -0
- package/dist/drawing.d.ts +31 -0
- package/dist/drawing.js +376 -0
- package/dist/env-owner.d.ts +22 -0
- package/dist/env-owner.js +291 -0
- package/dist/fe-stack.d.ts +10 -0
- package/dist/fe-stack.js +73 -0
- package/dist/file-layout.d.ts +71 -0
- package/dist/file-layout.js +465 -0
- package/dist/formatting.d.ts +25 -0
- package/dist/formatting.js +104 -0
- package/dist/function-budget.d.ts +9 -0
- package/dist/function-budget.js +48 -0
- package/dist/grammar-shim.d.ts +5 -0
- package/dist/grammar-shim.js +126 -0
- package/dist/hooks-folder.d.ts +34 -0
- package/dist/hooks-folder.js +320 -0
- package/dist/hygiene.d.ts +39 -0
- package/dist/hygiene.js +540 -0
- package/dist/index.d.ts +105 -0
- package/dist/index.js +310 -0
- package/{landmark.mjs → dist/landmark.d.ts} +4 -6
- package/dist/landmark.js +6 -0
- package/dist/lib/ast.d.ts +36 -0
- package/dist/lib/ast.js +94 -0
- package/dist/lib/bindings.d.ts +84 -0
- package/dist/lib/bindings.js +168 -0
- package/dist/lib/class-text.d.ts +3 -0
- package/dist/lib/class-text.js +30 -0
- package/dist/lib/config.d.ts +81 -0
- package/dist/lib/config.js +141 -0
- package/dist/lib/drawing-scope.d.ts +5 -0
- package/{lib/drawing-scope.mjs → dist/lib/drawing-scope.js} +6 -11
- package/dist/lib/eslint-boundary.d.ts +23 -0
- package/dist/lib/eslint-boundary.js +6 -0
- package/dist/lib/fe-stack.d.ts +49 -0
- package/dist/lib/fe-stack.js +98 -0
- package/dist/lib/hfs-view.d.ts +74 -0
- package/dist/lib/hfs-view.js +6 -0
- package/dist/lib/hfs.d.ts +34 -0
- package/dist/lib/hfs.js +72 -0
- package/dist/lib/next.d.ts +4 -0
- package/dist/lib/next.js +10 -0
- package/dist/lib/own-hook-origin.d.ts +17 -0
- package/dist/lib/own-hook-origin.js +52 -0
- package/dist/lib/package-root.d.ts +4 -0
- package/dist/lib/package-root.js +10 -0
- package/dist/lib/path.d.ts +2 -0
- package/{lib/path.mjs → dist/lib/path.js} +1 -1
- package/dist/lib/platform.d.ts +26 -0
- package/{lib/platform.mjs → dist/lib/platform.js} +38 -35
- package/dist/lib/program-for.d.ts +5 -0
- package/dist/lib/program-for.js +23 -0
- package/dist/lib/pure-owner-helper.d.ts +10 -0
- package/dist/lib/pure-owner-helper.js +156 -0
- package/dist/lib/react.d.ts +16 -0
- package/dist/lib/react.js +22 -0
- package/dist/lib/redux-hook-origin.d.ts +4 -0
- package/dist/lib/redux-hook-origin.js +50 -0
- package/dist/lib/rule.d.ts +28 -0
- package/dist/lib/rule.js +5 -0
- package/dist/lib/scope.d.ts +88 -0
- package/{lib/scope.mjs → dist/lib/scope.js} +83 -93
- package/dist/lib/typed-class-values.d.ts +13 -0
- package/dist/lib/typed-class-values.js +92 -0
- package/dist/lib/types.d.ts +44 -0
- package/dist/lib/types.js +64 -0
- package/dist/lib/why.d.ts +350 -0
- package/dist/lib/why.js +350 -0
- package/dist/lint-escape-hatch.d.ts +24 -0
- package/{lint-escape-hatch.mjs → dist/lint-escape-hatch.js} +38 -45
- package/dist/lists.d.ts +31 -0
- package/dist/lists.js +202 -0
- package/dist/loading.d.ts +34 -0
- package/dist/loading.js +157 -0
- package/dist/naming.d.ts +33 -0
- package/dist/naming.js +176 -0
- package/dist/native-controls.d.ts +24 -0
- package/dist/native-controls.js +105 -0
- package/dist/next-conventions.d.ts +48 -0
- package/dist/next-conventions.js +452 -0
- package/dist/project-graph.d.ts +5 -0
- package/{project-graph.mjs → dist/project-graph.js} +8 -12
- package/dist/props-and-slots.d.ts +19 -0
- package/dist/props-and-slots.js +320 -0
- package/dist/served-locale.d.ts +38 -0
- package/dist/served-locale.js +263 -0
- package/dist/shape-slot.d.ts +18 -0
- package/dist/shape-slot.js +324 -0
- package/dist/size-and-state-budget.d.ts +46 -0
- package/dist/size-and-state-budget.js +424 -0
- package/dist/size-growth.d.ts +24 -0
- package/dist/size-growth.js +64 -0
- package/dist/sonar-parity.d.ts +5 -0
- package/{sonar-parity.mjs → dist/sonar-parity.js} +11 -7
- package/dist/surface-rules.d.ts +18 -0
- package/dist/surface-rules.js +491 -0
- package/dist/swr-family.d.ts +15 -0
- package/dist/swr-family.js +419 -0
- package/dist/the-split.d.ts +27 -0
- package/dist/the-split.js +157 -0
- package/dist/tokens.d.ts +9 -0
- package/dist/tokens.js +35 -0
- package/dist/translation.d.ts +51 -0
- package/dist/translation.js +403 -0
- package/dist/transport.d.ts +73 -0
- package/dist/transport.js +885 -0
- package/dist/type-safety.d.ts +43 -0
- package/dist/type-safety.js +139 -0
- package/dist/ui-rulings.d.ts +11 -0
- package/dist/ui-rulings.js +121 -0
- package/package.json +31 -9
- package/runtime/knowledge/hfs/canon-pins.yaml +22 -14
- package/runtime/scripts/lib/package-at.mjs +5 -5
- package/brand-values.mjs +0 -172
- package/button-intent.mjs +0 -320
- package/callback-shape.mjs +0 -332
- package/class-names.mjs +0 -149
- package/client-boundary.mjs +0 -400
- package/comments.mjs +0 -159
- package/control-height.mjs +0 -77
- package/drawing.mjs +0 -315
- package/env-owner.mjs +0 -244
- package/fe-stack.mjs +0 -86
- package/file-layout.mjs +0 -472
- package/formatting.mjs +0 -110
- package/function-budget.mjs +0 -49
- package/grammar-shim.mjs +0 -123
- package/hooks-folder.mjs +0 -300
- package/hygiene.mjs +0 -513
- package/index.mjs +0 -317
- package/lib/ast.mjs +0 -97
- package/lib/bindings.mjs +0 -159
- package/lib/class-text.mjs +0 -29
- package/lib/config.mjs +0 -142
- package/lib/fe-stack.mjs +0 -77
- package/lib/hfs.mjs +0 -49
- package/lib/next.mjs +0 -6
- package/lib/own-hook-origin.mjs +0 -53
- package/lib/program-for.mjs +0 -22
- package/lib/pure-owner-helper.mjs +0 -148
- package/lib/react.mjs +0 -28
- package/lib/redux-hook-origin.mjs +0 -44
- package/lib/typed-class-values.mjs +0 -81
- package/lib/types.mjs +0 -63
- package/lib/why.mjs +0 -352
- package/lists.mjs +0 -193
- package/loading.mjs +0 -156
- package/naming.mjs +0 -185
- package/native-controls.mjs +0 -108
- package/next-conventions.mjs +0 -447
- package/props-and-slots.mjs +0 -276
- package/served-locale.mjs +0 -243
- package/size-and-state-budget.mjs +0 -376
- package/size-growth.mjs +0 -62
- package/surface-rules.mjs +0 -443
- package/swr-family.mjs +0 -326
- package/the-split.mjs +0 -155
- package/tokens.mjs +0 -39
- package/translation.mjs +0 -392
- package/transport.mjs +0 -830
- package/type-safety.mjs +0 -147
- package/ui-rulings.mjs +0 -134
package/surface-rules.mjs
DELETED
|
@@ -1,443 +0,0 @@
|
|
|
1
|
-
/** Grammar owns surface nesting and the single content edge of a panel list (ANATOMY-2, PADDING-0). */
|
|
2
|
-
import { attributeValue, stringOf, walk } from "./lib/ast.mjs"
|
|
3
|
-
import { functionOf, importOf, isFn, patternPath, unwrap, variableOf } from "./lib/bindings.mjs"
|
|
4
|
-
import { typedClassStrings, typedPropertyStrings } from "./lib/typed-class-values.mjs"
|
|
5
|
-
import { reactHookOf } from "./lib/react.mjs"
|
|
6
|
-
|
|
7
|
-
const GRAMMAR_ENTRY = /^@starci\/grammar(?:\/(?:common|core|offset-pop))?$/
|
|
8
|
-
const SURFACES = new Set([
|
|
9
|
-
"SurfaceCard", "SurfaceListCard", "SurfaceAccordionCard", "Sidebar",
|
|
10
|
-
"Dialog", "Drawer", "AlertDialog", "Popover",
|
|
11
|
-
"WorkspaceShell", "TopBar", "NavigationFeatureNav",
|
|
12
|
-
])
|
|
13
|
-
const FIELDS = new Set([
|
|
14
|
-
"Input", "TextField", "Textarea", "TextArea", "SearchField", "NumberField", "DateField", "TimeField",
|
|
15
|
-
"Select", "ComboBox", "DatePicker", "DateRangePicker", "Checkbox", "CheckboxGroup", "RadioGroup", "PressableField", "OtpInput",
|
|
16
|
-
])
|
|
17
|
-
const CLASS_BUILDERS = new Set(["cn", "clsx", "classNames", "cva", "tv"])
|
|
18
|
-
|
|
19
|
-
/** Resolve a JSX name to its Grammar export and optional compound parts, preserving lexical import identity. */
|
|
20
|
-
const grammarName = (context, name) => {
|
|
21
|
-
if (name.type === "JSXMemberExpression") {
|
|
22
|
-
const owner = grammarName(context, name.object)
|
|
23
|
-
return owner === null ? null : [...owner, name.property.name]
|
|
24
|
-
}
|
|
25
|
-
if (name.type !== "JSXIdentifier") return null
|
|
26
|
-
const imported = importOf(context, name)
|
|
27
|
-
if (!imported || !GRAMMAR_ENTRY.test(imported.source)) return null
|
|
28
|
-
if (imported.imported !== null) return [imported.imported]
|
|
29
|
-
const binding = variableOf(context, name)?.defs.find((definition) => definition.type === "ImportBinding")
|
|
30
|
-
return binding?.node.type === "ImportNamespaceSpecifier" ? [] : null
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Class and variant expressions expose their statically authored strings, including const aliases and cn branches. */
|
|
34
|
-
const authoredStrings = (context, input, seen = new Set(), mode = "classes") => {
|
|
35
|
-
const node = unwrap(input?.type === "JSXExpressionContainer" ? input.expression : input)
|
|
36
|
-
if (!node || seen.has(node)) return []
|
|
37
|
-
seen.add(node)
|
|
38
|
-
const text = stringOf(node)
|
|
39
|
-
if (text !== null) return [{ node, text }]
|
|
40
|
-
if (node.type === "TemplateElement") return [{ node, text: node.value.cooked ?? node.value.raw }]
|
|
41
|
-
if (node.type === "Identifier") {
|
|
42
|
-
const values = bindingValues(context, node)
|
|
43
|
-
return values.length ? values.flatMap((value) => authoredStrings(context, value, seen, mode)) : typedClassStrings(context, node, mode)
|
|
44
|
-
}
|
|
45
|
-
if (node.type === "MemberExpression") {
|
|
46
|
-
const propertyName = node.computed ? stringOf(node.property) : node.property.name
|
|
47
|
-
const values = propertyValues(context, node.object, propertyName)
|
|
48
|
-
return values.length ? values.flatMap((value) => authoredStrings(context, value, seen, mode)) : typedClassStrings(context, node, mode)
|
|
49
|
-
}
|
|
50
|
-
const children = stringChildren(node, mode)
|
|
51
|
-
return children.flatMap((child) => authoredStrings(context, child, seen, mode))
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Only value-bearing positions contribute classes; object keys support cn's conditional class dictionary. */
|
|
55
|
-
const stringChildren = (node, mode) => {
|
|
56
|
-
if (node.type === "TemplateLiteral") return mode === "classes" ? [...node.quasis, ...node.expressions] : []
|
|
57
|
-
if (node.type === "ArrayExpression") return node.elements
|
|
58
|
-
if (node.type === "CallExpression") return mode === "classes" ? node.arguments : []
|
|
59
|
-
if (node.type === "ConditionalExpression") return [node.consequent, node.alternate]
|
|
60
|
-
if (node.type === "LogicalExpression") return node.operator === "&&" ? [node.right] : [node.left, node.right]
|
|
61
|
-
if (node.type === "ObjectExpression") return node.properties.flatMap((property) => property.type === "Property" ? [property.key, property.value] : [property.argument])
|
|
62
|
-
return []
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** A declared React component is a separate render tree; inline collection/render callbacks keep the surrounding context. */
|
|
66
|
-
const componentBoundary = (context, node) => {
|
|
67
|
-
if (!isFn(node)) return false
|
|
68
|
-
if (node.parent?.type === "CallExpression" && ["memo", "forwardRef"].includes(reactHookOf(context, node.parent))) return true
|
|
69
|
-
if (node.parent?.type === "JSXExpressionContainer" || node.parent?.type === "CallExpression" && node.parent.arguments.includes(node)) return false
|
|
70
|
-
const name = node.id?.name ?? (node.parent?.type === "VariableDeclarator" ? node.parent.id.name : null)
|
|
71
|
-
return name !== null && /^[A-Z]/.test(name)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** The Grammar ancestors whose rendered content contains this node, excluding an overlay's trigger. */
|
|
75
|
-
function* surfaceAncestors(context, node) {
|
|
76
|
-
const triggers = new Set()
|
|
77
|
-
for (let current = node.parent; current; current = current.parent) {
|
|
78
|
-
if (componentBoundary(context, current)) return
|
|
79
|
-
if (current.type === "JSXAttribute" && current.name.name === "trigger") triggers.add(current.parent)
|
|
80
|
-
if (current.type === "JSXElement" && !triggers.has(current.openingElement)) yield current.openingElement
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const isSurface = (context, node, descendant) => {
|
|
85
|
-
const name = grammarName(context, node.name)
|
|
86
|
-
if (name?.length !== 1) return false
|
|
87
|
-
if (name[0] === "SurfaceAccordionCard") {
|
|
88
|
-
let frame = "painted"
|
|
89
|
-
for (const entry of node.attributes) {
|
|
90
|
-
const value = entry.type === "JSXSpreadAttribute" ? frameEffect(context, entry.argument, frame)
|
|
91
|
-
: entry.name.name === "frame" ? unpaintedFrame(context, attributeValue(entry)) ? "unpainted" : "painted" : "absent"
|
|
92
|
-
if (value !== "absent") frame = value
|
|
93
|
-
}
|
|
94
|
-
return frame !== "unpainted"
|
|
95
|
-
}
|
|
96
|
-
if (name[0] === "WorkspaceShell") {
|
|
97
|
-
for (let current = descendant; current && current !== node.parent; current = current.parent) {
|
|
98
|
-
if (current.type === "JSXAttribute" && current.parent === node && current.name.name === "header") return true
|
|
99
|
-
}
|
|
100
|
-
return false
|
|
101
|
-
}
|
|
102
|
-
return SURFACES.has(name[0])
|
|
103
|
-
}
|
|
104
|
-
const classTokens = (text) => text.split(/\s+/).map((token) => {
|
|
105
|
-
let depth = 0
|
|
106
|
-
let start = 0
|
|
107
|
-
for (let index = 0; index < token.length; index++) {
|
|
108
|
-
if (token[index] === "[") depth++
|
|
109
|
-
else if (token[index] === "]") depth--
|
|
110
|
-
else if (token[index] === ":" && depth === 0) start = index + 1
|
|
111
|
-
}
|
|
112
|
-
return token.slice(start).replace(/^!|!$/g, "")
|
|
113
|
-
})
|
|
114
|
-
/** Paint and typography belong to Grammar even when the app uses a token rather than a raw colour. */
|
|
115
|
-
const paintProperty = (name) => {
|
|
116
|
-
const property = String(name).replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`).replace(/^-(?:webkit|moz|ms)-/, "")
|
|
117
|
-
if (/^text-(?:align|overflow|wrap)(?:-|$)/.test(property)) return false
|
|
118
|
-
return /^(?:background|border|outline|font|text|fill|stroke|mask|color|accent-color|caret-color)(?:-|$)/.test(property)
|
|
119
|
-
|| /^(?:opacity|box-shadow|filter|backdrop-filter|mix-blend-mode|appearance|letter-spacing|line-height|clip-path)$/.test(property)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const rawSurface = (token) => {
|
|
123
|
-
const arbitrary = /^\[([^:]+):/.exec(token)
|
|
124
|
-
if (arbitrary) return paintProperty(arbitrary[1])
|
|
125
|
-
if (/^text-(?:left|right|center|justify|start|end|wrap|nowrap|balance|pretty|ellipsis|clip)$/.test(token)) return false
|
|
126
|
-
return /^-?(?:bg|from|via|to|shadow|inset-shadow|text-shadow|ring|inset-ring|outline|border|divide|rounded|text|font|leading|tracking|fill|stroke|mask|accent|caret|decoration|opacity|mix-blend|bg-blend|blur|brightness|contrast|drop-shadow|grayscale|hue-rotate|invert|saturate|sepia|backdrop|filter|appearance)(?:-|$)/.test(token)
|
|
127
|
-
|| /^(?:underline|overline|line-through|no-underline|italic|not-italic|antialiased|subpixel-antialiased)$/.test(token)
|
|
128
|
-
}
|
|
129
|
-
const horizontalPadding = (token) => /^(?:p|px|ps|pe|pl|pr)-.+$/.test(token) || /^\[padding-inline(?:-start|-end)?:.+\]$/.test(token)
|
|
130
|
-
|
|
131
|
-
/** Computed keys select their evaluated static name, rather than the identifier's spelling. */
|
|
132
|
-
const propertyName = (context, property) => {
|
|
133
|
-
if (!property.computed) return property.key.name ?? property.key.value
|
|
134
|
-
const values = authoredStrings(context, property.key, new Set(), "variant")
|
|
135
|
-
return values.length === 1 ? values[0].text : null
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/** A binding selects its own object property; sibling declarations do not become its value. */
|
|
139
|
-
const bindingValues = (context, node, seen = new Set()) => {
|
|
140
|
-
const definition = variableOf(context, node)?.defs.find((entry) => entry.type === "Variable")
|
|
141
|
-
if (!definition) return []
|
|
142
|
-
const path = patternPath(definition.node.id, node.name)
|
|
143
|
-
if (path === null) return []
|
|
144
|
-
return path.reduce((values, key) => values.flatMap((value) => propertyValues(context, value, key, seen)), [definition.node.init]).filter(Boolean)
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** Static object spreads retain the same published-prop obligations as ordinary JSX attributes. */
|
|
148
|
-
const propertyValues = (context, input, key, seen = new Set()) => {
|
|
149
|
-
const node = unwrap(input)
|
|
150
|
-
if (!node || seen.has(node)) return []
|
|
151
|
-
seen.add(node)
|
|
152
|
-
if (node.type === "Identifier") {
|
|
153
|
-
return bindingValues(context, node, seen).flatMap((value) => propertyValues(context, value, key, seen))
|
|
154
|
-
}
|
|
155
|
-
if (node.type === "MemberExpression") {
|
|
156
|
-
const member = node.computed ? stringOf(node.property) : node.property.name
|
|
157
|
-
return propertyValues(context, node.object, member, seen).flatMap((value) => propertyValues(context, value, key, seen))
|
|
158
|
-
}
|
|
159
|
-
if (node.type === "ConditionalExpression") return [node.consequent, node.alternate].flatMap((branch) => propertyValues(context, branch, key, seen))
|
|
160
|
-
if (node.type !== "ObjectExpression") return []
|
|
161
|
-
return node.properties.flatMap((property) => {
|
|
162
|
-
if (property.type === "SpreadElement") return propertyValues(context, property.argument, key, seen)
|
|
163
|
-
return propertyName(context, property) === key ? [property.value] : []
|
|
164
|
-
})
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const propStrings = (context, entry, name, mode = "classes") => {
|
|
168
|
-
if (entry.type === "JSXSpreadAttribute") {
|
|
169
|
-
const values = propertyValues(context, entry.argument, name)
|
|
170
|
-
return values.length ? values.flatMap((value) => authoredStrings(context, value, new Set(), mode)) : typedPropertyStrings(context, entry.argument, name, mode)
|
|
171
|
-
}
|
|
172
|
-
return entry.name.name === name ? authoredStrings(context, attributeValue(entry), new Set(), mode) : []
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** Only a fully known unpainted frame removes the accordion's bounded surface context. */
|
|
176
|
-
const unpaintedFrame = (context, input, seen = new Set()) => {
|
|
177
|
-
const node = unwrap(input)
|
|
178
|
-
if (!node || seen.has(node)) return false
|
|
179
|
-
seen.add(node)
|
|
180
|
-
const text = stringOf(node)
|
|
181
|
-
if (text !== null) return text === "unpainted"
|
|
182
|
-
if (node.type === "ConditionalExpression") return [node.consequent, node.alternate].every((branch) => unpaintedFrame(context, branch, new Set(seen)))
|
|
183
|
-
const values = node.type === "Identifier" ? bindingValues(context, node)
|
|
184
|
-
: node.type === "MemberExpression" ? propertyValues(context, node.object, node.computed ? stringOf(node.property) : node.property.name) : []
|
|
185
|
-
return values.length > 0 && values.every((value) => unpaintedFrame(context, value, new Set(seen)))
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/** A spread overrides preceding frame props; an unresolved spread may install a bounded frame. */
|
|
189
|
-
const frameEffect = (context, input, prior, seen = new Set()) => {
|
|
190
|
-
const node = unwrap(input)
|
|
191
|
-
if (!node || seen.has(node)) return "painted"
|
|
192
|
-
seen.add(node)
|
|
193
|
-
if (node.type === "ObjectExpression") {
|
|
194
|
-
let effect = prior
|
|
195
|
-
for (const property of node.properties) {
|
|
196
|
-
const key = property.type === "Property" ? propertyName(context, property) : null
|
|
197
|
-
const value = property.type === "SpreadElement" ? frameEffect(context, property.argument, effect, new Set(seen))
|
|
198
|
-
: key === "frame" ? unpaintedFrame(context, property.value) ? "unpainted" : "painted" : key === null ? "painted" : "absent"
|
|
199
|
-
if (value !== "absent") effect = value
|
|
200
|
-
}
|
|
201
|
-
return effect
|
|
202
|
-
}
|
|
203
|
-
const values = node.type === "Identifier" ? bindingValues(context, node)
|
|
204
|
-
: node.type === "MemberExpression" ? propertyValues(context, node.object, node.computed ? stringOf(node.property) : node.property.name)
|
|
205
|
-
: node.type === "ConditionalExpression" ? [node.consequent, node.alternate] : []
|
|
206
|
-
const effects = values.map((value) => frameEffect(context, value, prior, new Set(seen)))
|
|
207
|
-
return effects.length > 0 && effects.every((effect) => effect === effects[0]) ? effects[0] : "painted"
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/** Resolve authored style objects without treating unrelated object siblings or conditions as CSS. */
|
|
211
|
-
const stylePaint = (context, input, seen = new Set()) => {
|
|
212
|
-
const node = unwrap(input?.type === "JSXExpressionContainer" ? input.expression : input)
|
|
213
|
-
if (!node || seen.has(node)) return []
|
|
214
|
-
seen.add(node)
|
|
215
|
-
if (node.type === "Identifier") {
|
|
216
|
-
const values = bindingValues(context, node)
|
|
217
|
-
return values.length ? values.flatMap((value) => stylePaint(context, value, seen))
|
|
218
|
-
: typedClassStrings(context, node, "style-keys").filter((entry) => paintProperty(entry.text)).map((entry) => entry.node)
|
|
219
|
-
}
|
|
220
|
-
if (node.type === "MemberExpression") {
|
|
221
|
-
const member = node.computed ? stringOf(node.property) : node.property.name
|
|
222
|
-
const values = propertyValues(context, node.object, member)
|
|
223
|
-
return values.length ? values.flatMap((value) => stylePaint(context, value, seen))
|
|
224
|
-
: typedClassStrings(context, node, "style-keys").filter((entry) => paintProperty(entry.text)).map((entry) => entry.node)
|
|
225
|
-
}
|
|
226
|
-
if (node.type === "ConditionalExpression") return [node.consequent, node.alternate].flatMap((branch) => stylePaint(context, branch, seen))
|
|
227
|
-
if (node.type === "LogicalExpression") return (node.operator === "&&" ? [node.right] : [node.left, node.right]).flatMap((branch) => stylePaint(context, branch, seen))
|
|
228
|
-
if (node.type !== "ObjectExpression") return []
|
|
229
|
-
return node.properties.flatMap((property) => {
|
|
230
|
-
if (property.type === "SpreadElement") return stylePaint(context, property.argument, seen)
|
|
231
|
-
return paintProperty(propertyName(context, property)) ? [property] : []
|
|
232
|
-
})
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/** An explicit section caption excludes the row's own content subtree. */
|
|
236
|
-
const inSectionCaption = (context, node) => {
|
|
237
|
-
for (const ancestor of surfaceAncestors(context, node.parent)) {
|
|
238
|
-
if (isSurface(context, ancestor, node)) return false
|
|
239
|
-
const owner = grammarName(context, ancestor.name)
|
|
240
|
-
if (owner?.[0] !== "ListBox") continue
|
|
241
|
-
if (owner[1] === "Item") return false
|
|
242
|
-
if (["Section", "Header", "SectionHeader"].includes(owner[1])) return true
|
|
243
|
-
}
|
|
244
|
-
return false
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/** ANATOMY-2: field variants belong to Grammar's context, including across app component boundaries. */
|
|
248
|
-
export const nestedControlVariant = {
|
|
249
|
-
meta: {
|
|
250
|
-
type: "problem",
|
|
251
|
-
docs: { description: "ANATOMY-2: Grammar context supplies field variants; nested Tabs cannot force primary." },
|
|
252
|
-
schema: [],
|
|
253
|
-
messages: {
|
|
254
|
-
variant: "FE_NESTED_VARIANT: app fields cannot set a static variant. Omit variant so Grammar's surface context supplies the appropriate default.",
|
|
255
|
-
tabs: "FE_NESTED_VARIANT: Tabs inside a Grammar surface cannot force variant=\"primary\". Omit variant so Grammar supplies secondary.",
|
|
256
|
-
},
|
|
257
|
-
},
|
|
258
|
-
create(context) {
|
|
259
|
-
return {
|
|
260
|
-
JSXOpeningElement(node) {
|
|
261
|
-
const name = grammarName(context, node.name)
|
|
262
|
-
if (name?.length !== 1) return
|
|
263
|
-
if (FIELDS.has(name[0])) {
|
|
264
|
-
const variant = node.attributes.find((entry) => propStrings(context, entry, "variant", "variant").length > 0)
|
|
265
|
-
if (variant) context.report({ node: variant, messageId: "variant" })
|
|
266
|
-
} else if (name[0] === "Tabs") {
|
|
267
|
-
const primary = node.attributes.find((entry) => propStrings(context, entry, "variant", "variant").some((value) => value.text === "primary"))
|
|
268
|
-
if (primary && [...surfaceAncestors(context, node.parent)].some((ancestor) => isSurface(context, ancestor, node))) context.report({ node: primary, messageId: "tabs" })
|
|
269
|
-
}
|
|
270
|
-
},
|
|
271
|
-
}
|
|
272
|
-
},
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/** App classes carry layout; Grammar owns surface paint. */
|
|
276
|
-
export const noAppSurfacePaint = {
|
|
277
|
-
meta: {
|
|
278
|
-
type: "problem",
|
|
279
|
-
docs: { description: "App composition CSS is layout-only; Grammar paints surfaces." },
|
|
280
|
-
schema: [],
|
|
281
|
-
messages: { surface: "FE_GRAMMAR_LAYOUT_ONLY: app CSS paints or styles Grammar content. Keep composition CSS layout-only and use Grammar's published surfaces, typography and tone props." },
|
|
282
|
-
},
|
|
283
|
-
create(context) {
|
|
284
|
-
const reported = new Set()
|
|
285
|
-
const checkClassEntries = (entries) => {
|
|
286
|
-
for (const entry of entries) {
|
|
287
|
-
if (!classTokens(entry.text).some(rawSurface) || reported.has(entry.node)) continue
|
|
288
|
-
reported.add(entry.node)
|
|
289
|
-
context.report({ node: entry.node, messageId: "surface" })
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
const checkClasses = (value) => checkClassEntries(authoredStrings(context, value))
|
|
293
|
-
const reportStyle = (nodes) => {
|
|
294
|
-
for (const node of nodes) {
|
|
295
|
-
if (reported.has(node)) continue
|
|
296
|
-
reported.add(node)
|
|
297
|
-
context.report({ node, messageId: "surface" })
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
const checkStyle = (value) => {
|
|
301
|
-
reportStyle(stylePaint(context, value))
|
|
302
|
-
}
|
|
303
|
-
return {
|
|
304
|
-
JSXAttribute(node) {
|
|
305
|
-
if (["className", "class", "classNames"].includes(node.name.name)) checkClasses(node.value)
|
|
306
|
-
if (node.name.name === "style") checkStyle(node.value)
|
|
307
|
-
},
|
|
308
|
-
JSXSpreadAttribute(node) {
|
|
309
|
-
for (const name of ["className", "class", "classNames"]) checkClassEntries(propStrings(context, node, name))
|
|
310
|
-
const values = propertyValues(context, node.argument, "style")
|
|
311
|
-
if (values.length) values.forEach(checkStyle)
|
|
312
|
-
else reportStyle(typedPropertyStrings(context, node.argument, "style", "style-keys").filter((entry) => paintProperty(entry.text)).map((entry) => entry.node))
|
|
313
|
-
},
|
|
314
|
-
CallExpression(node) {
|
|
315
|
-
if (node.callee.type === "Identifier" && CLASS_BUILDERS.has(node.callee.name)) checkClasses(node)
|
|
316
|
-
},
|
|
317
|
-
VariableDeclarator(node) {
|
|
318
|
-
if (/[Cc]lass/.test(node.id.name ?? "") || /(?:^|[/\\])classNames\.[cm]?[jt]sx?$/.test(context.filename)) checkClasses(node.init)
|
|
319
|
-
},
|
|
320
|
-
Property(node) {
|
|
321
|
-
if (["className", "class", "classNames"].includes(propertyName(context, node))) checkClasses(node.value)
|
|
322
|
-
if (propertyName(context, node) === "style") checkStyle(node.value)
|
|
323
|
-
},
|
|
324
|
-
}
|
|
325
|
-
},
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/** PADDING-0: app padding wrappers cannot add a second horizontal inset to Grammar's list or section caption. */
|
|
329
|
-
export const noListPanelPadding = {
|
|
330
|
-
meta: {
|
|
331
|
-
type: "problem",
|
|
332
|
-
docs: { description: "PADDING-0: panel lists and section captions share the Grammar surface's content edge." },
|
|
333
|
-
schema: [],
|
|
334
|
-
messages: { padding: "FE_LIST_PANEL_PADDING: horizontal padding stacks with the Grammar list inset. Remove this wrapper padding and use ListBox inset=\"panel\" so rows and section captions share the panel content edge." },
|
|
335
|
-
},
|
|
336
|
-
create(context) {
|
|
337
|
-
const reported = new Set()
|
|
338
|
-
const check = (opening) => {
|
|
339
|
-
const className = opening.attributes.find((entry) => ["className", "class"].some((name) => propStrings(context, entry, name).some((value) => classTokens(value.text).some(horizontalPadding))))
|
|
340
|
-
if (!className || reported.has(className)) return
|
|
341
|
-
reported.add(className)
|
|
342
|
-
context.report({ node: className, messageId: "padding" })
|
|
343
|
-
}
|
|
344
|
-
return {
|
|
345
|
-
JSXOpeningElement(node) {
|
|
346
|
-
const ancestors = [...surfaceAncestors(context, node.parent)]
|
|
347
|
-
const surfaceIndex = ancestors.findIndex((ancestor) => isSurface(context, ancestor, node))
|
|
348
|
-
if (surfaceIndex < 0) return
|
|
349
|
-
const name = grammarName(context, node.name)
|
|
350
|
-
if (name?.[0] === "ListBox" && (name.length === 1 || ["Section", "Header", "SectionHeader"].includes(name[1]))) {
|
|
351
|
-
check(node)
|
|
352
|
-
for (const ancestor of ancestors.slice(0, surfaceIndex + 1)) check(ancestor)
|
|
353
|
-
} else if (inSectionCaption(context, node)) check(node)
|
|
354
|
-
},
|
|
355
|
-
}
|
|
356
|
-
},
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/** BOUNDARY-1 case-5: only content bands carry inset; horizontal separators reach the surface edges. */
|
|
360
|
-
export const dividerFullBleed = {
|
|
361
|
-
meta: {
|
|
362
|
-
type: "problem",
|
|
363
|
-
docs: {
|
|
364
|
-
description:
|
|
365
|
-
"BOUNDARY-1 case-5: horizontal Grammar dividers are full-bleed.",
|
|
366
|
-
},
|
|
367
|
-
schema: [],
|
|
368
|
-
messages: {
|
|
369
|
-
padding:
|
|
370
|
-
"FE_DIVIDER_FULL_BLEED: horizontal padding on this wrapper insets a Grammar Divider/Separator. Move padding to the sibling content bands so the divider reaches both surface edges.",
|
|
371
|
-
},
|
|
372
|
-
},
|
|
373
|
-
create(context) {
|
|
374
|
-
// Follow rendered same-file components, helper calls and JSX constants, with cycle protection.
|
|
375
|
-
const containsDivider = (root, seen = new Set()) => {
|
|
376
|
-
if (!root || seen.has(root)) return false
|
|
377
|
-
seen.add(root)
|
|
378
|
-
let found = false
|
|
379
|
-
const follow = (node) => {
|
|
380
|
-
found = containsDivider(node, seen) || found
|
|
381
|
-
}
|
|
382
|
-
walk(context.sourceCode, root, (node) => {
|
|
383
|
-
if (found) return false
|
|
384
|
-
if (isFn(node)) return false
|
|
385
|
-
if (node.type === "JSXOpeningElement") {
|
|
386
|
-
const name = grammarName(context, node.name)
|
|
387
|
-
if (
|
|
388
|
-
name?.length === 1 &&
|
|
389
|
-
["Divider", "Separator"].includes(name[0])
|
|
390
|
-
) {
|
|
391
|
-
const orientations = node.attributes.flatMap((entry) =>
|
|
392
|
-
propStrings(context, entry, "orientation", "variant"),
|
|
393
|
-
)
|
|
394
|
-
if (
|
|
395
|
-
!orientations.length ||
|
|
396
|
-
orientations.some((entry) => entry.text !== "vertical")
|
|
397
|
-
)
|
|
398
|
-
found = true
|
|
399
|
-
} else if (node.name.type === "JSXIdentifier") {
|
|
400
|
-
for (const definition of variableOf(context, node.name)?.defs ??
|
|
401
|
-
[]) {
|
|
402
|
-
if (definition.type === "FunctionName")
|
|
403
|
-
follow(definition.node.body)
|
|
404
|
-
if (definition.type === "Variable")
|
|
405
|
-
follow(functionOf(context, definition.node.init)?.body)
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
if (node.type === "CallExpression") {
|
|
410
|
-
follow(functionOf(context, node.callee)?.body)
|
|
411
|
-
for (const argument of node.arguments)
|
|
412
|
-
if (isFn(argument)) follow(argument.body)
|
|
413
|
-
}
|
|
414
|
-
if (
|
|
415
|
-
node.type === "Identifier" &&
|
|
416
|
-
node.parent?.type === "JSXExpressionContainer"
|
|
417
|
-
) {
|
|
418
|
-
for (const value of bindingValues(context, node)) follow(value)
|
|
419
|
-
}
|
|
420
|
-
return undefined
|
|
421
|
-
})
|
|
422
|
-
return found
|
|
423
|
-
}
|
|
424
|
-
return {
|
|
425
|
-
JSXOpeningElement(node) {
|
|
426
|
-
const padding = node.attributes.find((entry) =>
|
|
427
|
-
propStrings(context, entry, "className").some((value) =>
|
|
428
|
-
classTokens(value.text).some(horizontalPadding),
|
|
429
|
-
),
|
|
430
|
-
)
|
|
431
|
-
if (!padding) return
|
|
432
|
-
if (node.parent.children.some((child) => containsDivider(child)))
|
|
433
|
-
context.report({ node: padding, messageId: "padding" })
|
|
434
|
-
},
|
|
435
|
-
}
|
|
436
|
-
},
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
/** The surface laws ship at error in every composing source tree. */
|
|
440
|
-
export const rules = { "divider-full-bleed": dividerFullBleed, "nested-control-variant": nestedControlVariant, "no-list-panel-padding": noListPanelPadding, "no-app-surface-paint": noAppSurfacePaint }
|
|
441
|
-
|
|
442
|
-
/** No app can lower or omit a surface law. */
|
|
443
|
-
export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]))
|