@codyswann/lisa 3.45.10 → 3.46.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/dist/configs/eslint/expo.d.ts.map +1 -1
- package/dist/configs/eslint/expo.js +5 -0
- package/dist/configs/eslint/expo.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +59 -7
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/eslint-plugin-ui-standards/README.md +90 -0
- package/eslint-plugin-ui-standards/index.js +4 -0
- package/eslint-plugin-ui-standards/rules/no-unbound-design-value.js +375 -0
- package/expo/copy-overwrite/eslint.expo.ts +5 -0
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-design-intake/SKILL.md +169 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-design-intake/agents/openai.yaml +4 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-implement/SKILL.md +2 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-quality-review/SKILL.md +2 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-review-local/SKILL.md +9 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-tdd-implementation/SKILL.md +1 -0
- package/plugins/lisa/commands/design/intake.md +7 -0
- package/plugins/lisa/rules/eager/design-value-binding.md +80 -0
- package/plugins/lisa/rules/reference/design-value-binding.md +204 -0
- package/plugins/lisa/scripts/design-bindings-probe.mjs +713 -0
- package/plugins/lisa/scripts/design-intake-gate.mjs +470 -0
- package/plugins/lisa/scripts/design-variable-ids.mjs +565 -0
- package/plugins/lisa/skills/lisa-design-intake/SKILL.md +169 -0
- package/plugins/lisa/skills/lisa-design-intake/agents/openai.yaml +4 -0
- package/plugins/lisa/skills/lisa-implement/SKILL.md +2 -0
- package/plugins/lisa/skills/lisa-quality-review/SKILL.md +2 -0
- package/plugins/lisa/skills/lisa-review-local/SKILL.md +9 -0
- package/plugins/lisa/skills/lisa-tdd-implementation/SKILL.md +1 -0
- package/plugins/lisa-agy/commands/lisa/design/intake.md +7 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/scripts/design-bindings-probe.mjs +713 -0
- package/plugins/lisa-agy/scripts/design-intake-gate.mjs +470 -0
- package/plugins/lisa-agy/scripts/design-variable-ids.mjs +565 -0
- package/plugins/lisa-agy/skills/lisa-design-intake/SKILL.md +169 -0
- package/plugins/lisa-agy/skills/lisa-implement/SKILL.md +2 -0
- package/plugins/lisa-agy/skills/lisa-quality-review/SKILL.md +2 -0
- package/plugins/lisa-agy/skills/lisa-review-local/SKILL.md +9 -0
- package/plugins/lisa-agy/skills/lisa-tdd-implementation/SKILL.md +1 -0
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/commands/lisa/design/intake.md +7 -0
- package/plugins/lisa-copilot/rules/eager/design-value-binding.md +80 -0
- package/plugins/lisa-copilot/rules/reference/design-value-binding.md +204 -0
- package/plugins/lisa-copilot/scripts/design-bindings-probe.mjs +713 -0
- package/plugins/lisa-copilot/scripts/design-intake-gate.mjs +470 -0
- package/plugins/lisa-copilot/scripts/design-variable-ids.mjs +565 -0
- package/plugins/lisa-copilot/skills/lisa-design-intake/SKILL.md +169 -0
- package/plugins/lisa-copilot/skills/lisa-implement/SKILL.md +2 -0
- package/plugins/lisa-copilot/skills/lisa-quality-review/SKILL.md +2 -0
- package/plugins/lisa-copilot/skills/lisa-review-local/SKILL.md +9 -0
- package/plugins/lisa-copilot/skills/lisa-tdd-implementation/SKILL.md +1 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/commands/lisa/design/intake.md +7 -0
- package/plugins/lisa-cursor/rules/design-value-binding-reference.mdc +209 -0
- package/plugins/lisa-cursor/rules/design-value-binding.mdc +85 -0
- package/plugins/lisa-cursor/scripts/design-bindings-probe.mjs +713 -0
- package/plugins/lisa-cursor/scripts/design-intake-gate.mjs +470 -0
- package/plugins/lisa-cursor/scripts/design-variable-ids.mjs +565 -0
- package/plugins/lisa-cursor/skills/lisa-design-intake/SKILL.md +169 -0
- package/plugins/lisa-cursor/skills/lisa-implement/SKILL.md +2 -0
- package/plugins/lisa-cursor/skills/lisa-quality-review/SKILL.md +2 -0
- package/plugins/lisa-cursor/skills/lisa-review-local/SKILL.md +9 -0
- package/plugins/lisa-cursor/skills/lisa-tdd-implementation/SKILL.md +1 -0
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/commands/design/intake.md +7 -0
- package/plugins/src/base/rules/eager/design-value-binding.md +80 -0
- package/plugins/src/base/rules/reference/design-value-binding.md +204 -0
- package/plugins/src/base/scripts/design-bindings-probe.mjs +713 -0
- package/plugins/src/base/scripts/design-intake-gate.mjs +470 -0
- package/plugins/src/base/scripts/design-variable-ids.mjs +565 -0
- package/plugins/src/base/skills/lisa-design-intake/SKILL.md +169 -0
- package/plugins/src/base/skills/lisa-implement/SKILL.md +2 -0
- package/plugins/src/base/skills/lisa-quality-review/SKILL.md +2 -0
- package/plugins/src/base/skills/lisa-review-local/SKILL.md +9 -0
- package/plugins/src/base/skills/lisa-tdd-implementation/SKILL.md +1 -0
|
@@ -146,6 +146,95 @@ const MyComponent = () => (
|
|
|
146
146
|
|
|
147
147
|
---
|
|
148
148
|
|
|
149
|
+
### no-unbound-design-value
|
|
150
|
+
|
|
151
|
+
Flags a hardcoded style value in an axis that has a published design-variable
|
|
152
|
+
collection behind it.
|
|
153
|
+
|
|
154
|
+
#### Rule Details
|
|
155
|
+
|
|
156
|
+
Design handoff has one rule: **values come from design variables where a variable
|
|
157
|
+
system exists.** Visual measurement is supplemental there, and legitimate as the
|
|
158
|
+
primary source where no variable system exists.
|
|
159
|
+
|
|
160
|
+
The regime is resolved **per axis, not per project**. A library with a mature
|
|
161
|
+
colour system and no spacing scale is the common case, and it has to work:
|
|
162
|
+
colour is bound and a literal is a defect, while spacing is measured and a
|
|
163
|
+
literal is correct — in the same file, on the same day.
|
|
164
|
+
|
|
165
|
+
**Regime-awareness is by declaration, not by live query.** ESLint cannot ask a
|
|
166
|
+
design tool which collections are published, so the typed axes arrive as the
|
|
167
|
+
`typedAxes` option, mirroring `design.tokens.axes` in `.lisa.config.json`. The
|
|
168
|
+
default is the empty list, so **an unconfigured project sees nothing**. That
|
|
169
|
+
silence is deliberate: a rule that fires on every project is indistinguishable
|
|
170
|
+
from no rule at all, because the first thing anyone does is switch it off.
|
|
171
|
+
|
|
172
|
+
The rule answers only the objective question — is a value in a typed axis
|
|
173
|
+
written as a literal? It never judges whether a design is ambiguous or ugly.
|
|
174
|
+
That is an opinion, and an agent asked for one blocks on everything or nothing.
|
|
175
|
+
|
|
176
|
+
**Axes:** `color`, `spacing`, `typography`, `radius`, `elevation`, `motion`.
|
|
177
|
+
|
|
178
|
+
**Surfaces covered:** style-object properties (`StyleSheet.create`, inline style
|
|
179
|
+
objects, theme objects), JSX attributes, and `styled`/`css` tagged-template
|
|
180
|
+
bodies — whose declarations live inside a string and are invisible to a
|
|
181
|
+
property-name visitor.
|
|
182
|
+
|
|
183
|
+
**Not covered:** stylesheet files (`.css`, `.scss`) — ESLint does not parse them.
|
|
184
|
+
|
|
185
|
+
#### Examples
|
|
186
|
+
|
|
187
|
+
Given `typedAxes: ["color", "radius"]` — colour and radius are published, spacing
|
|
188
|
+
is not:
|
|
189
|
+
|
|
190
|
+
**Incorrect** (a literal in a typed axis):
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
const styles = StyleSheet.create({
|
|
194
|
+
card: { backgroundColor: "#3A7BD5", borderRadius: 12 },
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Correct** (bound in the typed axes, measured in the untyped one):
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
const styles = StyleSheet.create({
|
|
202
|
+
card: {
|
|
203
|
+
backgroundColor: tokens.surface.raised,
|
|
204
|
+
borderRadius: tokens.radius.card,
|
|
205
|
+
padding: 12, // spacing has no variable collection — measuring is the source
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
#### Configuration
|
|
211
|
+
|
|
212
|
+
```javascript
|
|
213
|
+
// eslint.config.local.ts
|
|
214
|
+
{
|
|
215
|
+
rules: {
|
|
216
|
+
"ui-standards/no-unbound-design-value": ["error", {
|
|
217
|
+
typedAxes: ["color", "radius"],
|
|
218
|
+
}],
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**Options:**
|
|
224
|
+
|
|
225
|
+
| Option | Type | Default | Description |
|
|
226
|
+
|--------|------|---------|-------------|
|
|
227
|
+
| `typedAxes` | `string[]` | `[]` | Axes with a published variable collection. Empty means the rule reports nothing. |
|
|
228
|
+
| `ignoreValues` | `(string \| number)[]` | `[0, 1]` | Values never worth a token — a zero is the absence of a value, a one is a hairline. |
|
|
229
|
+
| `tokenSourcePaths` | `string[]` | `["/theme/", "/themes/", "/tokens/", "/design-system/", "/design-tokens/"]` | Path fragments where literals belong, because those files mirror the published variables. |
|
|
230
|
+
|
|
231
|
+
This rule is the executable rung of the design-handoff policy. The judgment rung
|
|
232
|
+
— regime detection against the live variable collections, the five block
|
|
233
|
+
conditions, and escalation of a blocked item — lives in the `lisa-design-intake`
|
|
234
|
+
skill (`/lisa:design:intake`).
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
149
238
|
## Installation
|
|
150
239
|
|
|
151
240
|
This plugin is installed locally as a file dependency:
|
|
@@ -172,6 +261,7 @@ export default [
|
|
|
172
261
|
rules: {
|
|
173
262
|
'ui-standards/no-classname-outside-ui': 'error',
|
|
174
263
|
'ui-standards/no-direct-rn-imports': 'error',
|
|
264
|
+
'ui-standards/no-unbound-design-value': ['error', { typedAxes: ['color'] }],
|
|
175
265
|
},
|
|
176
266
|
},
|
|
177
267
|
];
|
|
@@ -12,10 +12,13 @@
|
|
|
12
12
|
* Rules:
|
|
13
13
|
* - no-classname-outside-ui: Disallows className prop outside UI components
|
|
14
14
|
* - no-direct-rn-imports: Disallows direct React Native imports
|
|
15
|
+
* - no-unbound-design-value: Disallows a hardcoded style value in an axis that
|
|
16
|
+
* has a published design-variable collection (regime-aware; opt-in per axis)
|
|
15
17
|
* @module eslint-plugin-ui-standards
|
|
16
18
|
*/
|
|
17
19
|
const noClassnameOutsideUi = require("./rules/no-classname-outside-ui");
|
|
18
20
|
const noDirectRnImports = require("./rules/no-direct-rn-imports");
|
|
21
|
+
const noUnboundDesignValue = require("./rules/no-unbound-design-value");
|
|
19
22
|
|
|
20
23
|
const plugin = {
|
|
21
24
|
meta: {
|
|
@@ -25,6 +28,7 @@ const plugin = {
|
|
|
25
28
|
rules: {
|
|
26
29
|
"no-classname-outside-ui": noClassnameOutsideUi,
|
|
27
30
|
"no-direct-rn-imports": noDirectRnImports,
|
|
31
|
+
"no-unbound-design-value": noUnboundDesignValue,
|
|
28
32
|
},
|
|
29
33
|
};
|
|
30
34
|
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is managed by Lisa.
|
|
3
|
+
* Do not edit directly — changes will be overwritten on the next `lisa` run.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Flags a hardcoded style value in an axis the project has declared *typed* —
|
|
8
|
+
* an axis that has a published design-variable collection behind it.
|
|
9
|
+
*
|
|
10
|
+
* The policy this enforces is per-axis, not per-project: colour may have a
|
|
11
|
+
* mature variable system while spacing has none, in the same library, on the
|
|
12
|
+
* same day. Where a variable system exists the variable is the source of truth
|
|
13
|
+
* and a literal is a copy of a number that will change without warning. Where
|
|
14
|
+
* no variable system exists, measuring the design *is* the legitimate source
|
|
15
|
+
* and this rule must stay silent — a control that fires on every project is
|
|
16
|
+
* indistinguishable from no control at all, because it gets turned off.
|
|
17
|
+
*
|
|
18
|
+
* **Regime-awareness is by declaration, not by live query.** ESLint cannot ask
|
|
19
|
+
* a design tool which collections are published, so the typed axes arrive as a
|
|
20
|
+
* rule option (`typedAxes`), sourced from `design.tokens.axes` in
|
|
21
|
+
* `.lisa.config.json`. The default is the empty list, which means the rule
|
|
22
|
+
* reports nothing until a project states which axes it has a variable system
|
|
23
|
+
* for. Silence-by-default is the deliberate choice: the failure mode of an
|
|
24
|
+
* over-firing rule is a disabled rule.
|
|
25
|
+
*
|
|
26
|
+
* The judgment this rule deliberately does NOT make is "is this design
|
|
27
|
+
* ambiguous" — that is an opinion, not a fact, and an agent asked for it blocks
|
|
28
|
+
* on everything or nothing. This rule only answers the objective question: is
|
|
29
|
+
* a value in a typed axis written as a literal?
|
|
30
|
+
* @module eslint-plugin-ui-standards/rules/no-unbound-design-value
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** Every axis the policy recognises. Fixed vocabulary — projects pick a subset. */
|
|
34
|
+
const AXES = [
|
|
35
|
+
"color",
|
|
36
|
+
"spacing",
|
|
37
|
+
"typography",
|
|
38
|
+
"radius",
|
|
39
|
+
"elevation",
|
|
40
|
+
"motion",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Object-property names, in the JS style-object dialects (React Native
|
|
45
|
+
* `StyleSheet`, inline `style` objects, theme objects), mapped to their axis.
|
|
46
|
+
* Compared after camelCase/kebab-case normalization, so the same table serves
|
|
47
|
+
* `borderRadius`, `border-radius`, and `BORDER_RADIUS`.
|
|
48
|
+
*/
|
|
49
|
+
const PROPERTY_AXES = new Map(
|
|
50
|
+
Object.entries({
|
|
51
|
+
// color
|
|
52
|
+
color: "color",
|
|
53
|
+
backgroundcolor: "color",
|
|
54
|
+
background: "color",
|
|
55
|
+
bordercolor: "color",
|
|
56
|
+
bordertopcolor: "color",
|
|
57
|
+
borderbottomcolor: "color",
|
|
58
|
+
borderleftcolor: "color",
|
|
59
|
+
borderrightcolor: "color",
|
|
60
|
+
tintcolor: "color",
|
|
61
|
+
shadowcolor: "color",
|
|
62
|
+
placeholdertextcolor: "color",
|
|
63
|
+
fill: "color",
|
|
64
|
+
stroke: "color",
|
|
65
|
+
// spacing
|
|
66
|
+
padding: "spacing",
|
|
67
|
+
paddingtop: "spacing",
|
|
68
|
+
paddingbottom: "spacing",
|
|
69
|
+
paddingleft: "spacing",
|
|
70
|
+
paddingright: "spacing",
|
|
71
|
+
paddinghorizontal: "spacing",
|
|
72
|
+
paddingvertical: "spacing",
|
|
73
|
+
margin: "spacing",
|
|
74
|
+
margintop: "spacing",
|
|
75
|
+
marginbottom: "spacing",
|
|
76
|
+
marginleft: "spacing",
|
|
77
|
+
marginright: "spacing",
|
|
78
|
+
marginhorizontal: "spacing",
|
|
79
|
+
marginvertical: "spacing",
|
|
80
|
+
gap: "spacing",
|
|
81
|
+
rowgap: "spacing",
|
|
82
|
+
columngap: "spacing",
|
|
83
|
+
// typography
|
|
84
|
+
fontsize: "typography",
|
|
85
|
+
fontweight: "typography",
|
|
86
|
+
lineheight: "typography",
|
|
87
|
+
letterspacing: "typography",
|
|
88
|
+
fontfamily: "typography",
|
|
89
|
+
// radius
|
|
90
|
+
borderradius: "radius",
|
|
91
|
+
bordertopleftradius: "radius",
|
|
92
|
+
bordertoprightradius: "radius",
|
|
93
|
+
borderbottomleftradius: "radius",
|
|
94
|
+
borderbottomrightradius: "radius",
|
|
95
|
+
// elevation
|
|
96
|
+
elevation: "elevation",
|
|
97
|
+
shadowopacity: "elevation",
|
|
98
|
+
shadowradius: "elevation",
|
|
99
|
+
boxshadow: "elevation",
|
|
100
|
+
// motion
|
|
101
|
+
animationduration: "motion",
|
|
102
|
+
transitionduration: "motion",
|
|
103
|
+
transitiondelay: "motion",
|
|
104
|
+
})
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Literals that are unmistakably colours, wherever they appear. Colour is the
|
|
109
|
+
* one axis whose values are self-identifying, so it is caught by shape as well
|
|
110
|
+
* as by property name — a `#3A7BD5` handed to a prop this table never heard of
|
|
111
|
+
* is still a colour copied out of a design file.
|
|
112
|
+
*
|
|
113
|
+
* Kept as two simple patterns rather than one alternation: the combined form
|
|
114
|
+
* tripped the regex-complexity budget, and two named shapes read better anyway.
|
|
115
|
+
*/
|
|
116
|
+
const HEX_COLOR = /^#[0-9a-f]{3,8}$/iu;
|
|
117
|
+
|
|
118
|
+
/** The functional colour notations. */
|
|
119
|
+
const FUNCTIONAL_COLOR = /^(?:rgba?|hsla?)\([^)]*\)$/iu;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* One CSS declaration, already isolated to a single `property: value` slice.
|
|
123
|
+
* Splitting first and matching an anchored pattern second keeps the matcher
|
|
124
|
+
* linear — a single scanning regex over the whole body backtracks.
|
|
125
|
+
*/
|
|
126
|
+
const CSS_DECLARATION = /^([a-z][a-z-]*):(.+)$/iu;
|
|
127
|
+
|
|
128
|
+
/** A CSS value that is a variable reference rather than a bare literal. */
|
|
129
|
+
const CSS_VARIABLE_REFERENCE = /var\(|\$\{|\$[a-z_]/iu;
|
|
130
|
+
|
|
131
|
+
/** Tag callees that mean "the template body is CSS". */
|
|
132
|
+
const CSS_TAGS = new Set(["css", "styled", "createGlobalStyle", "keyframes"]);
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Normalize a property name so `borderRadius`, `border-radius`, and
|
|
136
|
+
* `BORDER_RADIUS` all reach the same table entry.
|
|
137
|
+
* @param {string} name - Raw property name.
|
|
138
|
+
* @returns {string} Lowercased name with separators removed.
|
|
139
|
+
*/
|
|
140
|
+
function normalizeProperty(name) {
|
|
141
|
+
return String(name).replaceAll(/[-_]/gu, "").toLowerCase();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Resolve the static name of an object-property or JSX-attribute key.
|
|
146
|
+
* @param {object} node - Property or JSXAttribute node.
|
|
147
|
+
* @returns {string | null} The key's name, or null when it is computed.
|
|
148
|
+
*/
|
|
149
|
+
function keyNameOf(node) {
|
|
150
|
+
const key = node?.key ?? node?.name;
|
|
151
|
+
if (!key) return null;
|
|
152
|
+
if (node?.computed === true) return null;
|
|
153
|
+
if (key.type === "Identifier" || key.type === "JSXIdentifier")
|
|
154
|
+
return key.name;
|
|
155
|
+
if (key.type === "Literal" && typeof key.value === "string") return key.value;
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Read a literal's value when the node is a plain literal (or a negated
|
|
161
|
+
* numeric one, which is a `UnaryExpression` in the AST rather than a Literal).
|
|
162
|
+
* @param {object} node - Value node.
|
|
163
|
+
* @returns {{ value: string | number } | null} The literal value, or null.
|
|
164
|
+
*/
|
|
165
|
+
function literalValueOf(node) {
|
|
166
|
+
if (node?.type === "Literal") {
|
|
167
|
+
const { value } = node;
|
|
168
|
+
return typeof value === "string" || typeof value === "number"
|
|
169
|
+
? { value }
|
|
170
|
+
: null;
|
|
171
|
+
}
|
|
172
|
+
if (
|
|
173
|
+
node?.type === "UnaryExpression" &&
|
|
174
|
+
node.operator === "-" &&
|
|
175
|
+
node.argument?.type === "Literal" &&
|
|
176
|
+
typeof node.argument.value === "number"
|
|
177
|
+
) {
|
|
178
|
+
return { value: -node.argument.value };
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Decide the axis a literal belongs to, or null when it belongs to none.
|
|
185
|
+
* @param {string | null} property - Normalized property name, if any.
|
|
186
|
+
* @param {string | number} value - The literal value.
|
|
187
|
+
* @returns {string | null} Axis name, or null.
|
|
188
|
+
*/
|
|
189
|
+
function axisFor(property, value) {
|
|
190
|
+
if (typeof value === "string") {
|
|
191
|
+
const trimmed = value.trim();
|
|
192
|
+
if (HEX_COLOR.test(trimmed) || FUNCTIONAL_COLOR.test(trimmed)) {
|
|
193
|
+
return "color";
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return property === null ? null : (PROPERTY_AXES.get(property) ?? null);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* True when the file is a place design values are *defined* rather than
|
|
201
|
+
* consumed — a theme or token module is exactly where the literals belong,
|
|
202
|
+
* because that is the file the variables are mirrored into.
|
|
203
|
+
* @param {string} filename - Absolute or relative file path.
|
|
204
|
+
* @param {readonly string[]} tokenSourcePaths - Path fragments to exempt.
|
|
205
|
+
* @returns {boolean} Whether the file defines tokens.
|
|
206
|
+
*/
|
|
207
|
+
function isTokenSource(filename, tokenSourcePaths) {
|
|
208
|
+
const normalized = String(filename).replaceAll("\\", "/").toLowerCase();
|
|
209
|
+
return tokenSourcePaths.some(fragment =>
|
|
210
|
+
normalized.includes(String(fragment).toLowerCase())
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Extract every axis-bearing CSS declaration from a tagged template body.
|
|
216
|
+
*
|
|
217
|
+
* Styled-component and `css` template bodies are opaque to a property-name
|
|
218
|
+
* visitor — the declarations live inside a string, not in the AST — so they are
|
|
219
|
+
* parsed here rather than left uncovered.
|
|
220
|
+
* @param {string} text - The concatenated template body.
|
|
221
|
+
* @returns {{ property: string, value: string, axis: string }[]} Findings.
|
|
222
|
+
*/
|
|
223
|
+
function cssDeclarations(text) {
|
|
224
|
+
const found = [];
|
|
225
|
+
for (const slice of text.split(/[;{}\n]/u)) {
|
|
226
|
+
const match = CSS_DECLARATION.exec(slice.trim());
|
|
227
|
+
if (match === null) continue;
|
|
228
|
+
const property = normalizeProperty(match[1] ?? "");
|
|
229
|
+
const value = (match[2] ?? "").trim();
|
|
230
|
+
if (value.length === 0 || CSS_VARIABLE_REFERENCE.test(value)) continue;
|
|
231
|
+
const axis = axisFor(property, value);
|
|
232
|
+
if (axis !== null) found.push({ property, value, axis });
|
|
233
|
+
}
|
|
234
|
+
return found;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* True when the tagged template is CSS (`styled.div`, `styled(X)`, `css`).
|
|
239
|
+
* @param {object} node - TaggedTemplateExpression node.
|
|
240
|
+
* @returns {boolean} Whether the body should be read as CSS.
|
|
241
|
+
*/
|
|
242
|
+
function isCssTag(node) {
|
|
243
|
+
const tag = node?.tag;
|
|
244
|
+
if (tag?.type === "Identifier") return CSS_TAGS.has(tag.name);
|
|
245
|
+
if (tag?.type === "MemberExpression" && tag.object?.type === "Identifier") {
|
|
246
|
+
return CSS_TAGS.has(tag.object.name);
|
|
247
|
+
}
|
|
248
|
+
if (tag?.type === "CallExpression" && tag.callee?.type === "Identifier") {
|
|
249
|
+
return CSS_TAGS.has(tag.callee.name);
|
|
250
|
+
}
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
module.exports = {
|
|
255
|
+
meta: {
|
|
256
|
+
type: "problem",
|
|
257
|
+
docs: {
|
|
258
|
+
description:
|
|
259
|
+
"Disallow hardcoded style values in an axis that has a published design-variable collection",
|
|
260
|
+
category: "Best Practices",
|
|
261
|
+
recommended: false,
|
|
262
|
+
},
|
|
263
|
+
fixable: null,
|
|
264
|
+
schema: [
|
|
265
|
+
{
|
|
266
|
+
type: "object",
|
|
267
|
+
properties: {
|
|
268
|
+
typedAxes: {
|
|
269
|
+
type: "array",
|
|
270
|
+
items: { type: "string", enum: AXES },
|
|
271
|
+
uniqueItems: true,
|
|
272
|
+
},
|
|
273
|
+
ignoreValues: {
|
|
274
|
+
type: "array",
|
|
275
|
+
items: { type: ["string", "number"] },
|
|
276
|
+
},
|
|
277
|
+
tokenSourcePaths: {
|
|
278
|
+
type: "array",
|
|
279
|
+
items: { type: "string" },
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
additionalProperties: false,
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
messages: {
|
|
286
|
+
unboundDesignValue:
|
|
287
|
+
"This project publishes {{axis}} as design variables, so `{{property}}: {{value}}` is a copied number that changes without warning. Use the published {{axis}} variable, or ask for one to be published for this value.",
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
create(context) {
|
|
292
|
+
const options = context.options[0] ?? {};
|
|
293
|
+
const typedAxes = new Set(options.typedAxes ?? []);
|
|
294
|
+
// `0` and `1` are ignored by default: a zero is the absence of a value and
|
|
295
|
+
// a one is almost always a hairline border, and neither is a number any
|
|
296
|
+
// design system publishes as a token. Reporting them is the noise that
|
|
297
|
+
// gets a rule switched off.
|
|
298
|
+
const ignoreValues = new Set(options.ignoreValues ?? [0, 1]);
|
|
299
|
+
const tokenSourcePaths = options.tokenSourcePaths ?? [
|
|
300
|
+
"/theme/",
|
|
301
|
+
"/themes/",
|
|
302
|
+
"/tokens/",
|
|
303
|
+
"/design-system/",
|
|
304
|
+
"/design-tokens/",
|
|
305
|
+
];
|
|
306
|
+
|
|
307
|
+
// Nothing is typed until the project says so. This is the regime gate: on a
|
|
308
|
+
// project with no declared variable collections the rule visits nothing.
|
|
309
|
+
if (typedAxes.size === 0) return {};
|
|
310
|
+
|
|
311
|
+
const filename = context.filename ?? context.getFilename();
|
|
312
|
+
if (isTokenSource(filename, tokenSourcePaths)) return {};
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Report one finding when its axis is typed and its value is not exempt.
|
|
316
|
+
* @param {object} node - Node to attach the report to.
|
|
317
|
+
* @param {string} property - Human-readable property name.
|
|
318
|
+
* @param {string | number} value - The literal value.
|
|
319
|
+
* @param {string | null} axis - Resolved axis.
|
|
320
|
+
* @returns {void}
|
|
321
|
+
*/
|
|
322
|
+
const reportIfTyped = (node, property, value, axis) => {
|
|
323
|
+
if (axis === null || !typedAxes.has(axis)) return;
|
|
324
|
+
if (ignoreValues.has(value)) return;
|
|
325
|
+
context.report({
|
|
326
|
+
node,
|
|
327
|
+
messageId: "unboundDesignValue",
|
|
328
|
+
data: { axis, property, value: String(value) },
|
|
329
|
+
});
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Shared handler for a keyed node carrying a single literal value.
|
|
334
|
+
* @param {object} node - Property or JSXAttribute node.
|
|
335
|
+
* @param {object} valueNode - The node holding the value.
|
|
336
|
+
* @returns {void}
|
|
337
|
+
*/
|
|
338
|
+
const checkKeyedLiteral = (node, valueNode) => {
|
|
339
|
+
const rawKey = keyNameOf(node);
|
|
340
|
+
const literal = literalValueOf(valueNode);
|
|
341
|
+
if (literal === null) return;
|
|
342
|
+
const property = rawKey === null ? null : normalizeProperty(rawKey);
|
|
343
|
+
reportIfTyped(
|
|
344
|
+
valueNode,
|
|
345
|
+
rawKey ?? "value",
|
|
346
|
+
literal.value,
|
|
347
|
+
axisFor(property, literal.value)
|
|
348
|
+
);
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
Property(node) {
|
|
353
|
+
checkKeyedLiteral(node, node.value);
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
JSXAttribute(node) {
|
|
357
|
+
const valueNode =
|
|
358
|
+
node.value?.type === "JSXExpressionContainer"
|
|
359
|
+
? node.value.expression
|
|
360
|
+
: node.value;
|
|
361
|
+
checkKeyedLiteral(node, valueNode);
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
TaggedTemplateExpression(node) {
|
|
365
|
+
if (!isCssTag(node)) return;
|
|
366
|
+
const body = (node.quasi?.quasis ?? [])
|
|
367
|
+
.map(quasi => quasi.value?.cooked ?? quasi.value?.raw ?? "")
|
|
368
|
+
.join("\n${...}\n");
|
|
369
|
+
for (const finding of cssDeclarations(body)) {
|
|
370
|
+
reportIfTyped(node, finding.property, finding.value, finding.axis);
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
},
|
|
375
|
+
};
|
|
@@ -124,6 +124,11 @@ export function getExpoConfig({
|
|
|
124
124
|
// UI standards (all off by default)
|
|
125
125
|
"ui-standards/no-classname-outside-ui": "off",
|
|
126
126
|
"ui-standards/no-direct-rn-imports": "off",
|
|
127
|
+
// Regime-aware by declaration: silent until the project names the axes
|
|
128
|
+
// it publishes design variables for. Activate per axis in
|
|
129
|
+
// eslint.config.local.ts, mirroring `design.tokens.axes` in
|
|
130
|
+
// .lisa.config.json — e.g. ["error", { typedAxes: ["color"] }].
|
|
131
|
+
"ui-standards/no-unbound-design-value": "off",
|
|
127
132
|
|
|
128
133
|
// Tailwind
|
|
129
134
|
"tailwindcss/classnames-order": [
|
package/package.json
CHANGED
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
"ws": ">=8.21.0"
|
|
133
133
|
},
|
|
134
134
|
"name": "@codyswann/lisa",
|
|
135
|
-
"version": "3.
|
|
135
|
+
"version": "3.46.0",
|
|
136
136
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
137
137
|
"main": "dist/index.js",
|
|
138
138
|
"exports": {
|