@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/lists.mjs
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The rules that hold `lists.md` (HFS R65 `FE_SIZE_AND_STATE_BUDGET`, sub-check `FE_LIST_KEY`).
|
|
3
|
-
*
|
|
4
|
-
* A LIST IS ITEMS WITH IDENTITY. React reconciles a rendered list by `key`: the key is how it knows
|
|
5
|
-
* that the third row is still the third row after the first was deleted. With no key React falls
|
|
6
|
-
* back to position, with an index key it says position is identity, and either way a deleted row
|
|
7
|
-
* hands its state (an open menu, a half-typed input, a focus ring) to its neighbour. Nothing fails a
|
|
8
|
-
* type check and the bug appears only on the second interaction, on a real list.
|
|
9
|
-
*
|
|
10
|
-
* WHAT COUNTS AS A LIST. A `.map` callback that returns JSX. A static array written in place
|
|
11
|
-
* (`[0, 1, 2].map(...)`) or a callback that ignores its item (`(_, index) => ...`, a skeleton) has no identity to lose, so an index key there is fine.
|
|
12
|
-
*
|
|
13
|
-
* THE MEMO TAX. A component prop written as `{{ ... }}` or `[...]` inside a `.map` callback is a new
|
|
14
|
-
* object per row per render, so `memo` on the row never hits and the whole list re-renders on every
|
|
15
|
-
* keystroke of its parent. Hoist the constant, or build it once above the map.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { attribute, attributeValue, calleeName, isFunction, mentions } from "./lib/ast.mjs"
|
|
19
|
-
|
|
20
|
-
/** `.map(callback)` where the callback is written inline; the call node, else null. */
|
|
21
|
-
const mapCallback = (call) => {
|
|
22
|
-
if (call.type !== "CallExpression" || call.callee.type !== "MemberExpression" || call.callee.computed) return null
|
|
23
|
-
if (call.callee.property.type !== "Identifier" || call.callee.property.name !== "map") return null
|
|
24
|
-
const receiver = calleeName(call.callee.object)
|
|
25
|
-
if (receiver !== null && /(?:^|\.)Children$/.test(receiver)) return null
|
|
26
|
-
const callback = call.arguments[0]
|
|
27
|
-
return callback && isFunction(callback) ? callback : null
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** The JSX a callback returns: its expression body, or the value of each of its own `return` statements. */
|
|
31
|
-
const returnedJsx = (callback) => {
|
|
32
|
-
const found = []
|
|
33
|
-
const take = (node) => {
|
|
34
|
-
if (!node) return
|
|
35
|
-
if (node.type === "JSXElement" || node.type === "JSXFragment") found.push(node)
|
|
36
|
-
else if (node.type === "ConditionalExpression") {
|
|
37
|
-
take(node.consequent)
|
|
38
|
-
take(node.alternate)
|
|
39
|
-
} else if (node.type === "LogicalExpression") take(node.right)
|
|
40
|
-
}
|
|
41
|
-
if (callback.body.type !== "BlockStatement") {
|
|
42
|
-
take(callback.body)
|
|
43
|
-
return found
|
|
44
|
-
}
|
|
45
|
-
const scan = (node) => {
|
|
46
|
-
if (!node || typeof node.type !== "string") return
|
|
47
|
-
if (node.type === "ReturnStatement") return take(node.argument)
|
|
48
|
-
if (node.type === "BlockStatement") node.body.forEach(scan)
|
|
49
|
-
else if (node.type === "IfStatement") {
|
|
50
|
-
scan(node.consequent)
|
|
51
|
-
scan(node.alternate)
|
|
52
|
-
} else if (node.type === "SwitchStatement") node.cases.forEach((entry) => entry.consequent.forEach(scan))
|
|
53
|
-
else if (node.type === "TryStatement") {
|
|
54
|
-
scan(node.block)
|
|
55
|
-
scan(node.handler?.body)
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
scan(callback.body)
|
|
59
|
-
return found
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* True when the rows have no identity to lose: the receiver is an array written in place, or the callback
|
|
64
|
-
* ignores the item (`(_, index) => ...`), which is how a skeleton or a placeholder count is rendered.
|
|
65
|
-
*/
|
|
66
|
-
const isPositional = (call, callback) => {
|
|
67
|
-
if (call.callee.object.type === "ArrayExpression") return true
|
|
68
|
-
const item = callback.params[0]
|
|
69
|
-
return Boolean(item) && item.type === "Identifier" && item.name.startsWith("_")
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// -- LISTS-1 ---------------------------------------------------------------------------------------
|
|
73
|
-
|
|
74
|
-
/** Every element a `.map` callback returns carries a `key`. */
|
|
75
|
-
export const listItemHasKey = {
|
|
76
|
-
meta: {
|
|
77
|
-
type: "problem",
|
|
78
|
-
docs: { description: "An element returned from `.map` carries a `key`." },
|
|
79
|
-
schema: [],
|
|
80
|
-
messages: {
|
|
81
|
-
missing:
|
|
82
|
-
"This element is returned from `.map` with no `key`. React then identifies rows by position, so deleting or reordering one hands its state (an open menu, typed text, focus) to its neighbour. Give it a stable key from the data: `key={row.id}`.",
|
|
83
|
-
fragment:
|
|
84
|
-
"A `<>...</>` returned from `.map` cannot carry a key. Use `<Fragment key={...}>` (import `Fragment` from `react`) so each row has an identity.",
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
create(context) {
|
|
88
|
-
return {
|
|
89
|
-
CallExpression(call) {
|
|
90
|
-
const callback = mapCallback(call)
|
|
91
|
-
if (!callback) return
|
|
92
|
-
for (const element of returnedJsx(callback)) {
|
|
93
|
-
if (element.type === "JSXFragment") context.report({ node: element, messageId: "fragment" })
|
|
94
|
-
else if (!attribute(element.openingElement, "key")) context.report({ node: element.openingElement, messageId: "missing" })
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
}
|
|
98
|
-
},
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// -- LISTS-2 ---------------------------------------------------------------------------------------
|
|
102
|
-
|
|
103
|
-
/** The key is never the position, and never made up on every render. */
|
|
104
|
-
export const noIndexKey = {
|
|
105
|
-
meta: {
|
|
106
|
-
type: "problem",
|
|
107
|
-
docs: { description: "A list key is stable data identity, never the map index or a random value." },
|
|
108
|
-
schema: [],
|
|
109
|
-
messages: {
|
|
110
|
-
index:
|
|
111
|
-
"`key` is built from the `.map` index. The index is the row's position, not its identity: after a delete or a reorder the wrong row keeps the state. Use an id the data carries (`key={row.id}`).",
|
|
112
|
-
random:
|
|
113
|
-
"`key` is generated during render (`Math.random`, `Date.now`, `crypto.randomUUID`). A new key on every render remounts every row, throwing away its state and its DOM. Use an id the data carries.",
|
|
114
|
-
},
|
|
115
|
-
},
|
|
116
|
-
create(context) {
|
|
117
|
-
const source = context.sourceCode ?? context.getSourceCode()
|
|
118
|
-
return {
|
|
119
|
-
CallExpression(call) {
|
|
120
|
-
const callback = mapCallback(call)
|
|
121
|
-
if (!callback) return
|
|
122
|
-
const indexParam = callback.params[1]
|
|
123
|
-
const indexName = indexParam && indexParam.type === "Identifier" ? indexParam.name : null
|
|
124
|
-
for (const element of returnedJsx(callback)) {
|
|
125
|
-
if (element.type !== "JSXElement") continue
|
|
126
|
-
const key = attribute(element.openingElement, "key")
|
|
127
|
-
const value = attributeValue(key)
|
|
128
|
-
if (!value) continue
|
|
129
|
-
const text = source.getText(value)
|
|
130
|
-
if (/\b(?:Math\.random|Date\.now|randomUUID|nanoid|uuid)\b/.test(text)) {
|
|
131
|
-
context.report({ node: key, messageId: "random" })
|
|
132
|
-
} else if (indexName && !isPositional(call, callback) && mentions(source, value, indexName)) {
|
|
133
|
-
context.report({ node: key, messageId: "index" })
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
},
|
|
137
|
-
}
|
|
138
|
-
},
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// -- LISTS-3 ---------------------------------------------------------------------------------------
|
|
142
|
-
|
|
143
|
-
/** True for `<Component>` and `<a.B>`, false for a DOM tag: only a component can be memoised. */
|
|
144
|
-
const isComponentTag = (opening) => {
|
|
145
|
-
if (opening.name.type === "JSXMemberExpression") return true
|
|
146
|
-
return opening.name.type === "JSXIdentifier" && /^[A-Z]/.test(opening.name.name)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** True when the nearest function around `node` is a `.map` callback. */
|
|
150
|
-
const insideMapCallback = (node) => {
|
|
151
|
-
for (let current = node.parent; current; current = current.parent) {
|
|
152
|
-
if (!isFunction(current)) continue
|
|
153
|
-
return current.parent?.type === "CallExpression" && current.parent.arguments[0] === current && mapCallback(current.parent) === current
|
|
154
|
-
}
|
|
155
|
-
return false
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/** A component in a mapped list gets no object or array literal for a prop. */
|
|
159
|
-
export const noInlineLiteralPropInList = {
|
|
160
|
-
meta: {
|
|
161
|
-
type: "problem",
|
|
162
|
-
docs: { description: "No object or array literal as a prop of a component rendered inside `.map`." },
|
|
163
|
-
schema: [],
|
|
164
|
-
messages: {
|
|
165
|
-
literal:
|
|
166
|
-
"`{{prop}}` is an object or array literal written inside `.map`: a new value per row per render, so a memoised row never hits and the whole list re-renders on every parent update. Hoist it to a constant above the component, or build it once before the map.",
|
|
167
|
-
},
|
|
168
|
-
},
|
|
169
|
-
create(context) {
|
|
170
|
-
return {
|
|
171
|
-
JSXAttribute(node) {
|
|
172
|
-
const opening = node.parent
|
|
173
|
-
if (!opening || opening.type !== "JSXOpeningElement" || !isComponentTag(opening)) return
|
|
174
|
-
if (node.name.type !== "JSXIdentifier" || node.name.name === "key") return
|
|
175
|
-
const value = attributeValue(node)
|
|
176
|
-
if (!value || (value.type !== "ObjectExpression" && value.type !== "ArrayExpression")) return
|
|
177
|
-
if (value.type === "ArrayExpression" && value.elements.length === 0) return
|
|
178
|
-
if (!insideMapCallback(node)) return
|
|
179
|
-
context.report({ node, messageId: "literal", data: { prop: node.name.name } })
|
|
180
|
-
},
|
|
181
|
-
}
|
|
182
|
-
},
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** The rules this law contributes to the plugin. */
|
|
186
|
-
export const rules = {
|
|
187
|
-
"list-item-has-key": listItemHasKey,
|
|
188
|
-
"no-index-key": noIndexKey,
|
|
189
|
-
"no-inline-literal-prop-in-list": noInlineLiteralPropInList,
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/** Every rule is an error. */
|
|
193
|
-
export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]))
|
package/loading.mjs
DELETED
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The rules that hold `loading.md`.
|
|
3
|
-
*
|
|
4
|
-
* Three shapes of the same mistake: a second tree describing the first. A twin component, a
|
|
5
|
-
* ready-made placeholder handed in as a prop, and a ternary picking between two different
|
|
6
|
-
* components at a call site. Each is correct on the day it is written and wrong the first time the
|
|
7
|
-
* real shape changes.
|
|
8
|
-
*
|
|
9
|
-
* WHY THIS NEEDS RULES AT ALL, when so much of this canon is held by types. A resting shape has no
|
|
10
|
-
* assertion to fail: it is not wrong in a way a compiler or a test can see, it is wrong on screen,
|
|
11
|
-
* and only for the second somebody happens to be watching. That is exactly the class of defect a
|
|
12
|
-
* lint rule is for - the one nothing else will ever report.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { classOf, fileOf, isComponentFile } from "./lib/scope.mjs"
|
|
16
|
-
|
|
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)
|
|
19
|
-
|
|
20
|
-
/** A flag that means "waiting", in the spellings this codebase uses. */
|
|
21
|
-
const WAITING_FLAG = /\bis(?:Loading|Skeleton|Pending)\b/
|
|
22
|
-
|
|
23
|
-
/** The name of a component whose whole job is to mirror another one's shape. */
|
|
24
|
-
const TWIN_NAME = /^[A-Za-z0-9]*Skeleton$/
|
|
25
|
-
|
|
26
|
-
/** JSX element name, including `Foo.Bar`. */
|
|
27
|
-
const jsxElementName = (opening) => {
|
|
28
|
-
const name = opening && opening.name
|
|
29
|
-
if (!name) return null
|
|
30
|
-
if (name.type === "JSXIdentifier") return name.name
|
|
31
|
-
if (name.type === "JSXMemberExpression" && name.object && name.property) {
|
|
32
|
-
return `${name.object.name}.${name.property.name}`
|
|
33
|
-
}
|
|
34
|
-
return null
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// -- LOADING-1 -------------------------------------------------------------------------------------
|
|
38
|
-
|
|
39
|
-
/** No component exists whose job is to mirror another component's shape. */
|
|
40
|
-
export const noRestingTwinComponent = {
|
|
41
|
-
meta: {
|
|
42
|
-
type: "problem",
|
|
43
|
-
docs: { description: "No component whose whole job is to mirror another one's shape." },
|
|
44
|
-
schema: [],
|
|
45
|
-
messages: {
|
|
46
|
-
twin:
|
|
47
|
-
"`{{name}}` is a hand-mirrored twin: a second description of a shape that already has one. It cannot be kept in step - it can only be noticed after it has already drifted, and a resting shape has no assertion to fail, so nothing will report the drift. Give the component it mirrors a resting state and let it rest as ITSELF.",
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
create(context) {
|
|
51
|
-
if (!isGoverned(context)) return {}
|
|
52
|
-
const owner = classOf(context)
|
|
53
|
-
if (owner.role !== "entry" || !TWIN_NAME.test(owner.bindings?.name ?? "")) return {}
|
|
54
|
-
return {
|
|
55
|
-
Program(node) {
|
|
56
|
-
context.report({ node, messageId: "twin", data: { name: owner.bindings.name } })
|
|
57
|
-
},
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// -- LOADING-1, handed in from outside --------------------------------------------------------------
|
|
63
|
-
|
|
64
|
-
/** A ready-made placeholder is the same second tree, passed as a prop. */
|
|
65
|
-
export const noPlaceholderProp = {
|
|
66
|
-
meta: {
|
|
67
|
-
type: "problem",
|
|
68
|
-
docs: { description: "No prop hands a component a ready-made resting shape to render." },
|
|
69
|
-
schema: [],
|
|
70
|
-
messages: {
|
|
71
|
-
prop:
|
|
72
|
-
"A resting shape handed in as markup is a second description of a tree that already has one, and it sits further from the shape it copies than a twin does - the component it describes cannot even see it. Pass the waiting flag down instead, and let each part rest as itself.",
|
|
73
|
-
import:
|
|
74
|
-
"`{{name}}` is a hand-kept resting tree imported from beside its subject. It drifts the first time the real shape changes, and nothing reports it. Thread the waiting flag down instead.",
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
create(context) {
|
|
78
|
-
if (!isGoverned(context)) return {}
|
|
79
|
-
return {
|
|
80
|
-
JSXAttribute(node) {
|
|
81
|
-
const name = node.name && node.name.type === "JSXIdentifier" ? node.name.name : null
|
|
82
|
-
if (name !== "skeleton" && name !== "placeholder" && name !== "fallback") return
|
|
83
|
-
const value = node.value
|
|
84
|
-
if (!value || value.type !== "JSXExpressionContainer") return
|
|
85
|
-
const expression = value.expression
|
|
86
|
-
if (expression.type === "JSXElement" || expression.type === "JSXFragment") {
|
|
87
|
-
context.report({ node, messageId: "prop" })
|
|
88
|
-
}
|
|
89
|
-
},
|
|
90
|
-
ImportDeclaration(node) {
|
|
91
|
-
const source = node.source && node.source.value
|
|
92
|
-
if (typeof source !== "string" || !/^\.\.?\//.test(source)) return
|
|
93
|
-
for (const specifier of node.specifiers || []) {
|
|
94
|
-
const local = specifier.local && specifier.local.name
|
|
95
|
-
// A bare `Skeleton` is the primitive a component rests WITH, not a twin of one.
|
|
96
|
-
if (!local || local === "Skeleton" || !TWIN_NAME.test(local)) continue
|
|
97
|
-
context.report({ node: specifier, messageId: "import", data: { name: local } })
|
|
98
|
-
}
|
|
99
|
-
},
|
|
100
|
-
}
|
|
101
|
-
},
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// -- LOADING-2 -------------------------------------------------------------------------------------
|
|
105
|
-
|
|
106
|
-
/** A caller never picks between a resting shape and a real one. */
|
|
107
|
-
export const noRestingBranchAtCallSite = {
|
|
108
|
-
meta: {
|
|
109
|
-
type: "problem",
|
|
110
|
-
docs: { description: "A waiting flag must not choose between two different components." },
|
|
111
|
-
schema: [],
|
|
112
|
-
messages: {
|
|
113
|
-
branch:
|
|
114
|
-
"`{{flag}}` picks between two DIFFERENT elements, which is a resting shape written at the call site - and it drifts from the real one the first time the real one changes. Give the component below a waiting flag and pass it down; let it rest as ITSELF. The same component on both sides is the honest form: one description, two states.",
|
|
115
|
-
},
|
|
116
|
-
},
|
|
117
|
-
create(context) {
|
|
118
|
-
if (!isGoverned(context)) return {}
|
|
119
|
-
const source = context.sourceCode || context.getSourceCode()
|
|
120
|
-
/** Root element name of one arm, or null when the arm is not an element. */
|
|
121
|
-
const armName = (expression) => {
|
|
122
|
-
if (!expression) return null
|
|
123
|
-
if (expression.type === "JSXElement") return jsxElementName(expression.openingElement)
|
|
124
|
-
if (expression.type === "JSXFragment") return "<>"
|
|
125
|
-
return null
|
|
126
|
-
}
|
|
127
|
-
return {
|
|
128
|
-
ConditionalExpression(node) {
|
|
129
|
-
const test = source.getText(node.test)
|
|
130
|
-
if (!WAITING_FLAG.test(test)) return
|
|
131
|
-
const left = armName(node.consequent)
|
|
132
|
-
const right = armName(node.alternate)
|
|
133
|
-
// Both arms must be real elements AND differ. `null` on one side is LOADING-5 - a control
|
|
134
|
-
// that has nowhere to go yet - and that is correct rather than a second tree.
|
|
135
|
-
if (!left || !right || left === right) return
|
|
136
|
-
context.report({ node, messageId: "branch", data: { flag: test.trim().slice(0, 40) } })
|
|
137
|
-
},
|
|
138
|
-
}
|
|
139
|
-
},
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** The rules this law contributes to the plugin. */
|
|
143
|
-
export const rules = {
|
|
144
|
-
"no-resting-twin-component": noRestingTwinComponent,
|
|
145
|
-
"no-placeholder-prop": noPlaceholderProp,
|
|
146
|
-
"no-resting-branch-at-call-site": noRestingBranchAtCallSite,
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* The level this law asks for, as the plugin's own opinion.
|
|
151
|
-
*
|
|
152
|
-
* A repository adopting these with history should expect the twin rule to report a folder at a
|
|
153
|
-
* time: hand-kept placeholder trees arrive in families, one per screen, and each is a real shape
|
|
154
|
-
* that has to be folded back into the component it copies rather than deleted.
|
|
155
|
-
*/
|
|
156
|
-
export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]))
|
package/naming.mjs
DELETED
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The rules that hold `naming.md`.
|
|
3
|
-
*
|
|
4
|
-
* Two spellings, both of which work, which is exactly why they are rules: nothing corrects the
|
|
5
|
-
* second one, so a file written on a Tuesday reads differently from its neighbour and every diff
|
|
6
|
-
* afterwards carries noise that has nothing to do with the change.
|
|
7
|
-
*
|
|
8
|
-
* Neither rule can see intent, and both are deliberately narrow because of it. NAMING-1 fires on
|
|
9
|
-
* shape alone - a module-level `function` - which is exact. NAMING-2 fires on the `handle` prefix
|
|
10
|
-
* alone, which catches the rename-at-the-boundary case and nothing else; a name that merely
|
|
11
|
-
* describes an action is not its business.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { hfsOf } from "./lib/hfs.mjs"
|
|
15
|
-
import { NEXT_RESERVED_EXPORTS } from "./lib/next.mjs"
|
|
16
|
-
import { fileOf, inSlot, roleOfFile } from "./lib/scope.mjs"
|
|
17
|
-
|
|
18
|
-
/** Parents that make a function declaration module-level rather than nested. */
|
|
19
|
-
const MODULE_LEVEL_PARENTS = new Set(["Program", "ExportNamedDeclaration", "ExportDefaultDeclaration"])
|
|
20
|
-
|
|
21
|
-
// -- NAMING-1 --------------------------------------------------------------------------------------
|
|
22
|
-
|
|
23
|
-
/** A module-level function is an arrow const, so nothing exists before the line that declares it. */
|
|
24
|
-
export const preferArrowExport = {
|
|
25
|
-
meta: {
|
|
26
|
-
type: "suggestion",
|
|
27
|
-
docs: { description: "Module-level functions use an arrow const, not a `function` declaration." },
|
|
28
|
-
schema: [],
|
|
29
|
-
messages: {
|
|
30
|
-
fn: "`function {{name}}` is hoisted, so it exists before the line that declares it and the order of this file stops promising anything about what is defined when. Write `const {{name}} = (...) => {...}` - a const cannot be used before it exists, so the file reads in the order it runs.",
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
create(context) {
|
|
34
|
-
return {
|
|
35
|
-
FunctionDeclaration(node) {
|
|
36
|
-
const parent = node.parent && node.parent.type
|
|
37
|
-
if (!MODULE_LEVEL_PARENTS.has(parent)) return
|
|
38
|
-
context.report({
|
|
39
|
-
node: node.id || node,
|
|
40
|
-
messageId: "fn",
|
|
41
|
-
data: { name: (node.id && node.id.name) || "default" },
|
|
42
|
-
})
|
|
43
|
-
},
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// -- NAMING-2 --------------------------------------------------------------------------------------
|
|
49
|
-
|
|
50
|
-
/** Something a reader triggers is named `onX`, so the name survives being passed. */
|
|
51
|
-
export const handlerOnPrefix = {
|
|
52
|
-
meta: {
|
|
53
|
-
type: "suggestion",
|
|
54
|
-
docs: { description: "Handlers are named `onX`, not `handleX`." },
|
|
55
|
-
schema: [],
|
|
56
|
-
messages: {
|
|
57
|
-
handle: "`{{name}}` is renamed at every boundary it crosses - the slot is `on`, the DOM attribute is `onClick`, and the props type says `on{{rest}}`. Call it `on{{rest}}` at birth and the name is the same at the declaration, at the call site and in the type.",
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
create(context) {
|
|
61
|
-
const flag = (node, name) => {
|
|
62
|
-
if (!name || !/^handle[A-Z]/.test(name)) return
|
|
63
|
-
context.report({ node, messageId: "handle", data: { name, rest: name.slice("handle".length) } })
|
|
64
|
-
}
|
|
65
|
-
return {
|
|
66
|
-
VariableDeclarator(node) {
|
|
67
|
-
if (node.id && node.id.type === "Identifier") flag(node.id, node.id.name)
|
|
68
|
-
},
|
|
69
|
-
JSXAttribute(node) {
|
|
70
|
-
if (node.name) flag(node.name, node.name.name)
|
|
71
|
-
},
|
|
72
|
-
TSPropertySignature(node) {
|
|
73
|
-
if (node.key && node.key.type === "Identifier") flag(node.key, node.key.name)
|
|
74
|
-
},
|
|
75
|
-
}
|
|
76
|
-
},
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// -- machine-only identity -------------------------------------------------------------------------
|
|
80
|
-
|
|
81
|
-
/** A const introduces a value; it does not merely give an existing identifier a second name. */
|
|
82
|
-
export const noDirectConstAlias = {
|
|
83
|
-
meta: {
|
|
84
|
-
type: "problem",
|
|
85
|
-
docs: { description: "A const must not be a direct identifier-to-identifier alias." },
|
|
86
|
-
schema: [],
|
|
87
|
-
messages: {
|
|
88
|
-
alias:
|
|
89
|
-
"`const {{alias}} = {{original}}` gives one value two names without adding behavior. Use `{{original}}` directly, or make `{{alias}}` the result of the transformation it claims to represent.",
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
create(context) {
|
|
93
|
-
// A route segment file (slot `fe.route`, a role: page, layout, route, ...) is the one place Next dictates the exported name; `export const generateMetadata = x` is the contract.
|
|
94
|
-
const segmentFile = inSlot(context, "fe.route") && roleOfFile(context) !== null
|
|
95
|
-
return {
|
|
96
|
-
VariableDeclarator(node) {
|
|
97
|
-
if (node.parent?.kind !== "const") return
|
|
98
|
-
if (node.id?.type !== "Identifier" || node.init?.type !== "Identifier") return
|
|
99
|
-
if (segmentFile && NEXT_RESERVED_EXPORTS.has(node.id.name) && node.parent.parent?.type === "ExportNamedDeclaration") return
|
|
100
|
-
context.report({
|
|
101
|
-
node: node.id,
|
|
102
|
-
messageId: "alias",
|
|
103
|
-
data: { alias: node.id.name, original: node.init.name },
|
|
104
|
-
})
|
|
105
|
-
},
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** The rules this law contributes to the plugin. */
|
|
111
|
-
// -- NAMING-3 --------------------------------------------------------------------------------------
|
|
112
|
-
|
|
113
|
-
/** Letters that exist in Vietnamese and not in English, in the forms a path can carry. */
|
|
114
|
-
const SECOND_LANGUAGE_PATH = /[\u00e0\u00e1\u00e2\u00e3\u00e8\u00e9\u00ea\u00ec\u00ed\u00f2\u00f3\u00f4\u00f5\u00f9\u00fa\u00fd\u0103\u0111\u0129\u0169\u01a1\u01b0\u1ea1\u1ea3\u1ea5\u1ea7\u1ea9\u1eab\u1ead\u1eaf\u1eb1\u1eb3\u1eb5\u1eb7\u1eb9\u1ebb\u1ebd\u1ebf\u1ec1\u1ec3\u1ec5\u1ec7\u1ec9\u1ecb\u1ecd\u1ecf\u1ed1\u1ed3\u1ed5\u1ed7\u1ed9\u1edb\u1edd\u1edf\u1ee1\u1ee3\u1ee5\u1ee7\u1ee9\u1eeb\u1eed\u1eef\u1ef1\u1ef3\u1ef5\u1ef7\u1ef9]/i
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Second-language words spelled in ASCII, as a route or folder segment writes them.
|
|
118
|
-
*
|
|
119
|
-
* A path cannot carry diacritics, so a name written with them reaches the filesystem unaccented
|
|
120
|
-
* (`cap-phat` below) and every accent-based check passes it. The segments below are the ones this product actually produced.
|
|
121
|
-
* A list is the honest instrument here: guessing at Vietnamese-shaped ASCII would refuse `dang`
|
|
122
|
-
* in `dangerous` and `cap` in `capacity`, and a rule that fires on English words is one a
|
|
123
|
-
* repository turns off.
|
|
124
|
-
*/
|
|
125
|
-
const ROMANISED = [
|
|
126
|
-
"dang-nhap", "dang-ky", "dang-xuat", "cap-phat", "khoa-hoc", "hoc-vien", "gioi-thieu",
|
|
127
|
-
"lien-he", "tai-khoan", "thanh-toan", "gio-hang", "tin-tuc", "san-pham", "bang-gia",
|
|
128
|
-
"dieu-khoan", "chinh-sach", "trang-chu", "quan-ly", "cai-dat", "ho-so",
|
|
129
|
-
]
|
|
130
|
-
|
|
131
|
-
/** A route segment or folder name, lowercased, from a normalized path. */
|
|
132
|
-
const segmentsOf = (filename) =>
|
|
133
|
-
String(filename || "").replace(/\\/g, "/").toLowerCase().split("/").filter(Boolean)
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* A path names things in one language: the one every reader of this repository shares.
|
|
137
|
-
*
|
|
138
|
-
* WHY A PATH AND NOT ONLY THE SOURCE. `no-second-language-in-source` reads identifiers, comments
|
|
139
|
-
* and strings, and cannot see the file it is reading. So a route could be `app/cap-phat/page.tsx`
|
|
140
|
-
* with every identifier inside it in English, and nothing said a word - while the URL, the import
|
|
141
|
-
* specifier, the folder in every editor sidebar and the path in every stack trace stayed in a
|
|
142
|
-
* language half the readers do not have.
|
|
143
|
-
*
|
|
144
|
-
* A ROUTE SEGMENT IS ALSO A PUBLIC NAME. It is the address a customer sees and a support ticket
|
|
145
|
-
* quotes, so this is not only an authoring question: the product's own URLs stop being readable to
|
|
146
|
-
* anybody outside one language.
|
|
147
|
-
*/
|
|
148
|
-
const noSecondLanguageInPath = {
|
|
149
|
-
meta: {
|
|
150
|
-
type: "problem",
|
|
151
|
-
docs: { description: "File and route names are written in the repository's one shared language." },
|
|
152
|
-
schema: [],
|
|
153
|
-
messages: {
|
|
154
|
-
path:
|
|
155
|
-
"`{{segment}}` names this file in a second language. A path is read by more people than the code inside it - it is the URL a customer quotes, the folder every editor shows, and the specifier every import repeats - and unlike a comment it cannot be skimmed past. Rename the segment; the words a person READS belong in the locale catalogue, never in the address.",
|
|
156
|
-
},
|
|
157
|
-
},
|
|
158
|
-
create(context) {
|
|
159
|
-
const offending = segmentsOf(hfsOf(context).relative(fileOf(context))).find(
|
|
160
|
-
(segment) => SECOND_LANGUAGE_PATH.test(segment) || ROMANISED.includes(segment.replace(/[()[\]]/g, "")),
|
|
161
|
-
)
|
|
162
|
-
if (!offending) return {}
|
|
163
|
-
return {
|
|
164
|
-
Program(node) {
|
|
165
|
-
context.report({ node, messageId: "path", data: { segment: offending } })
|
|
166
|
-
},
|
|
167
|
-
}
|
|
168
|
-
},
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export const rules = {
|
|
172
|
-
"prefer-arrow-export": preferArrowExport,
|
|
173
|
-
"handler-on-prefix": handlerOnPrefix,
|
|
174
|
-
"no-second-language-in-path": noSecondLanguageInPath,
|
|
175
|
-
"no-direct-const-alias": noDirectConstAlias,
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* The level this law asks for, as the plugin's own opinion.
|
|
180
|
-
*
|
|
181
|
-
* A consuming repository's `eslint.config.mjs` stays the authority on what is actually switched on.
|
|
182
|
-
* Both rules here are mechanical and auto-correctable by hand in seconds, so a repository adopting
|
|
183
|
-
* them with existing debt should burn the count down rather than live at `warn`.
|
|
184
|
-
*/
|
|
185
|
-
export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]))
|
package/native-controls.mjs
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The rule that holds `native-controls.md` (HFS R62, `FE_NATIVE_FORM_CONTROL`).
|
|
3
|
-
*
|
|
4
|
-
* THE GRAMMAR OWNS THE CONTROL. A bare `<select>`, `<input>`, `<textarea>` or `<button>` in product
|
|
5
|
-
* source is a control with no focus ring, no dark value, no density, no disabled or invalid state
|
|
6
|
-
* from the design system - it looks native on one platform and is styled by hand on the next, and
|
|
7
|
-
* the hand styling is where the private colour and pixel values came from (see `brand-values`).
|
|
8
|
-
* The renderer the grammar publishes is the same element with the state the product needs.
|
|
9
|
-
*
|
|
10
|
-
* NO TIER IS EXEMPT. The grammar package itself is not linted by this canon (it is where the native
|
|
11
|
-
* element legitimately lives); everything the product owns - leaves included - renders the grammar's
|
|
12
|
-
* control. If the grammar lacks the control, the fix is to add it to the grammar, not to draw one here.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { attribute } from "./lib/ast.mjs"
|
|
16
|
-
|
|
17
|
-
/** Intrinsic elements a reader operates, that the grammar renders for the product. */
|
|
18
|
-
const NATIVE = new Set(["select", "input", "textarea", "button"])
|
|
19
|
-
|
|
20
|
-
/** No native form control in product source. */
|
|
21
|
-
export const noNativeFormControl = {
|
|
22
|
-
meta: {
|
|
23
|
-
type: "problem",
|
|
24
|
-
docs: { description: "Use the grammar's renderers; never a bare select, input, textarea or button." },
|
|
25
|
-
schema: [],
|
|
26
|
-
messages: {
|
|
27
|
-
native:
|
|
28
|
-
"A bare `<{{tag}}>`. The grammar owns this control: it brings the focus ring, disabled and invalid states, density and the dark value, and a hand-styled native element brings none of them. Use the grammar's renderer; if it does not exist yet, add it to the grammar rather than drawing one here.",
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
create(context) {
|
|
32
|
-
return {
|
|
33
|
-
JSXOpeningElement(node) {
|
|
34
|
-
if (node.name.type !== "JSXIdentifier" || !NATIVE.has(node.name.name)) return
|
|
35
|
-
context.report({ node, messageId: "native", data: { tag: node.name.name } })
|
|
36
|
-
},
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// -- NATIVE-2 --------------------------------------------------------------------------------------
|
|
42
|
-
|
|
43
|
-
/** No bare `<img>`; the framework's image owns size, format, lazy loading and layout shift. */
|
|
44
|
-
export const noNativeImg = {
|
|
45
|
-
meta: {
|
|
46
|
-
type: "problem",
|
|
47
|
-
docs: { description: "Use `next/image`, never a bare `<img>`." },
|
|
48
|
-
schema: [],
|
|
49
|
-
messages: {
|
|
50
|
-
img:
|
|
51
|
-
"A bare `<img>` ships the original file at its original size, reserves no space (the page jumps when it loads) and is never lazy by policy. Use `Image` from `next/image` with `width` and `height`, or `fill` and `sizes`, and an `alt` from the message catalogue.",
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
create(context) {
|
|
55
|
-
return {
|
|
56
|
-
JSXOpeningElement(node) {
|
|
57
|
-
if (node.name.type === "JSXIdentifier" && node.name.name === "img") context.report({ node, messageId: "img" })
|
|
58
|
-
},
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// -- NATIVE-3 --------------------------------------------------------------------------------------
|
|
64
|
-
|
|
65
|
-
/** Every `next/image` reserves its space: a size, or `fill` inside a sized parent with `sizes`. */
|
|
66
|
-
export const imageHasSize = {
|
|
67
|
-
meta: {
|
|
68
|
-
type: "problem",
|
|
69
|
-
docs: { description: "`next/image` carries `width` and `height`, or `fill` with `sizes`." },
|
|
70
|
-
schema: [],
|
|
71
|
-
messages: {
|
|
72
|
-
size:
|
|
73
|
-
"This `Image` has no `width` and `height` and is not `fill`. The browser cannot reserve its space, so the page shifts when it loads and the layout score falls. Give it both dimensions, or `fill` inside a sized parent.",
|
|
74
|
-
sizes:
|
|
75
|
-
"This `Image` is `fill` with no `sizes`. Next then assumes the image is as wide as the viewport and downloads the largest variant on every screen. State the rendered width: `sizes=\"(min-width: 768px) 33vw, 100vw\"`.",
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
create(context) {
|
|
79
|
-
const locals = new Set()
|
|
80
|
-
return {
|
|
81
|
-
ImportDeclaration(node) {
|
|
82
|
-
if (node.source.value !== "next/image") return
|
|
83
|
-
for (const specifier of node.specifiers) if (specifier.type === "ImportDefaultSpecifier") locals.add(specifier.local.name)
|
|
84
|
-
},
|
|
85
|
-
JSXOpeningElement(node) {
|
|
86
|
-
if (node.name.type !== "JSXIdentifier" || !locals.has(node.name.name)) return
|
|
87
|
-
// A spread may carry the size; the rule judges what it can read.
|
|
88
|
-
if (node.attributes.some((entry) => entry.type === "JSXSpreadAttribute")) return
|
|
89
|
-
const fill = attribute(node, "fill")
|
|
90
|
-
if (fill) {
|
|
91
|
-
if (!attribute(node, "sizes")) context.report({ node, messageId: "sizes" })
|
|
92
|
-
return
|
|
93
|
-
}
|
|
94
|
-
if (!attribute(node, "width") || !attribute(node, "height")) context.report({ node, messageId: "size" })
|
|
95
|
-
},
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** The rules this law contributes to the plugin. */
|
|
101
|
-
export const rules = {
|
|
102
|
-
"no-native-form-control": noNativeFormControl,
|
|
103
|
-
"no-native-img": noNativeImg,
|
|
104
|
-
"image-has-size": imageHasSize,
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** Every rule is an error. */
|
|
108
|
-
export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]))
|