guard-my-design-system 1.9.0 → 2.1.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # guard-my-design-system
2
2
 
3
- [![npm](https://img.shields.io/npm/v/guard-my-design-system?color=2dd4bf&label=npm)](https://www.npmjs.com/package/guard-my-design-system) [![downloads](https://img.shields.io/npm/dm/guard-my-design-system?color=2dd4bf&label=downloads)](https://www.npmjs.com/package/guard-my-design-system) [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE) [![no telemetry](https://img.shields.io/badge/no-telemetry-2dd4bf)](https://github.com/gregkozakiewicz/guard-my-design-system#what-makes-the-verdict-trustworthy) [![GitHub Action](https://img.shields.io/badge/GitHub_Action-v1-2dd4bf)](#on-a-pull-request)
3
+ [![npm](https://img.shields.io/npm/v/guard-my-design-system?color=2dd4bf&label=npm)](https://www.npmjs.com/package/guard-my-design-system) [![downloads](https://img.shields.io/npm/dm/guard-my-design-system?color=2dd4bf&label=downloads)](https://www.npmjs.com/package/guard-my-design-system) [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE) [![no telemetry](https://img.shields.io/badge/no-telemetry-2dd4bf)](https://github.com/gregkozakiewicz/guard-my-design-system#what-makes-the-verdict-trustworthy) [![GitHub Action](https://img.shields.io/badge/GitHub_Action-v2-2dd4bf)](#on-a-pull-request)
4
4
 
5
5
  ## Your design system dies one pull request at a time. This makes sure it doesn't.
6
6
 
@@ -40,7 +40,9 @@ it updates that same comment. It never adds more comments:
40
40
  ## What it catches
41
41
 
42
42
  - **A hard-coded colour where a token exists.** The finding names the token:
43
- `var(--blue-500)`, not just a hex code. This works across colour notations:
43
+ `var(--blue-500)`, not just a hex code. A token's own value pasted into a
44
+ component or a stylesheet counts too: the finding says use the name, not
45
+ the value. This works across colour notations:
44
46
  a hex stray is matched to an hsl or oklch token, including shadcn's
45
47
  bare-triplet variables. Dark-theme values count as the system too, so a
46
48
  stray in a dark block snaps to the dark token, never its light twin.
@@ -54,6 +56,11 @@ it updates that same comment. It never adds more comments:
54
56
  - **An inline `style={{ }}` block.** Styling written there is invisible to the
55
57
  system and to every agent that reads the file. Blocks built from variables
56
58
  are decided elsewhere, so they are left alone.
59
+ - **A button built from scratch where the repo already has a Button.** A
60
+ styled button, or a button tag dressed as one, in a file whose package
61
+ can import a Button that at least 20 files already use. The finding
62
+ gives the import line. Rows, tabs, close crosses and select triggers
63
+ built on a button tag are left alone.
57
64
  - **A second definition of a component you already have.** The finding names
58
65
  the file that already defines it, and how many places use that one.
59
66
  - **A new import of a duplicate component.** When a name is defined in more
@@ -70,6 +77,17 @@ it updates that same comment. It never adds more comments:
70
77
  - **A palette colour where a theme variable exists.** On a shadcn repo whose
71
78
  theme file holds the variables, `text-slate-500` in the app's own code is
72
79
  flagged and the theme file named. Off on utility-class installs.
80
+ - **A chart colour written by hand.** A chart needs several colours that
81
+ differ from each other, and most design systems never name them, so the
82
+ chart rule (roast 8.8) has three answers. Where the repo keeps a chart
83
+ palette (`--chart-*` or `--series-*` tokens, a `chartColors` entry in the
84
+ theme, or shadcn's `--chart-1` to `--chart-5` when a chart actually reads
85
+ them), a hex in a chart file is flagged and the palette named. Where the
86
+ repo has charts but no palette, a new chart painting by hand gets one line
87
+ that names the existing chart doing the same and asks for the palette
88
+ once. The first chart in a repo gets one line asking for a name. The
89
+ generic colour rule stays out of chart files, the same way it does in
90
+ `roast_validate`.
73
91
  - **A colour or a pixel size written onto a kit component.** On a product
74
92
  built on MUI, Mantine, Chakra UI or Ant Design, `color: '#667085'` in an
75
93
  `sx` prop or a style object is flagged and the finding says whether the
@@ -134,7 +152,7 @@ jobs:
134
152
  - uses: actions/checkout@v5
135
153
  with:
136
154
  fetch-depth: 0
137
- - uses: gregkozakiewicz/guard-my-design-system@v1
155
+ - uses: gregkozakiewicz/guard-my-design-system@v2
138
156
  ```
139
157
 
140
158
  After that it runs on every pull request and needs no attention from you.
@@ -143,7 +161,7 @@ If you want the check to fail instead of commenting, turn on strict mode.
143
161
  It is the only setting:
144
162
 
145
163
  ```yaml
146
- - uses: gregkozakiewicz/guard-my-design-system@v1
164
+ - uses: gregkozakiewicz/guard-my-design-system@v2
147
165
  with:
148
166
  strict: true
149
167
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guard-my-design-system",
3
- "version": "1.9.0",
3
+ "version": "2.1.0",
4
4
  "description": "Your design system dies one pull request at a time. This makes sure it doesn't. A guard that judges only the lines a change adds, against the system the repo already has, and names the on-system value the author probably meant.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,7 +11,7 @@
11
11
  "src/"
12
12
  ],
13
13
  "dependencies": {
14
- "roast-my-design-system": "8.6.1"
14
+ "roast-my-design-system": "9.2.0"
15
15
  },
16
16
  "keywords": [
17
17
  "design-system",
package/src/judge.mjs CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  definedComponents, exemptReason,
14
14
  EXTRA_KINDS, extraValue, fontDeclarations,
15
15
  WIDGET_CSS_RE, isLibraryClass, PALETTE_CLASS_RE, blankComments, kitPaintFindings,
16
- tokenTwinFindings, avoidedImportFindings,
16
+ tokenTwinFindings, avoidedImportFindings, isChartFile, chartFindings, handmadeButtonFindings,
17
17
  } from 'roast-my-design-system/engine';
18
18
 
19
19
  // Folder membership, the way the engine's own splits do it.
@@ -88,7 +88,7 @@ function exemptFiles(added, readFile) {
88
88
  * Returns [{ file, line, kind, value, advice }] sorted by file then line.
89
89
  * kinds: color | spacing | radius | fontsize | shadow | arbitrary |
90
90
  * important | font | inline | component | palette | kit-colour |
91
- * kit-px | twin-token | avoided-copy
91
+ * kit-px | twin-token | avoided-copy | chart-colour | chart-palette
92
92
  * readFile(file) gives the file as it stands; readBase(file) the file at the
93
93
  * base (null when the change creates it), so a token or an import the change
94
94
  * adds can be told from one that was already there.
@@ -163,6 +163,9 @@ export function judge(added, system, { readFile, readBase } = {}) {
163
163
  }
164
164
  // "use var(--blue-500)", not "go hunt this hex": name a value when the
165
165
  // system defines it as a custom property.
166
+ const statesTokens = (file) => (system.tokenSources ?? []).some((t) => samePath(t, file));
167
+ // on a kit repo the theme is where a colour is decided (mcp/knowledge reads it the same way)
168
+ const themeFile = system.profile?.kit?.themeFiles?.[0] ?? system.tokenFile ?? null;
166
169
  const named = (value) => {
167
170
  // shadcn-style tokens are defined as bare triplets (--primary: 222.2 47.4%
168
171
  // 11.2%) but normalised to hsl(...); try the unwrapped form too.
@@ -221,6 +224,18 @@ export function judge(added, system, { readFile, readBase } = {}) {
221
224
 
222
225
  const findings = [];
223
226
 
227
+ // A chart's series colours are judged against the chart palette, or its
228
+ // absence, by the engine (roast 8.8.0, lib/charts): the report never
229
+ // counted them, the live checks counted every one, and the guard did too.
230
+ // On a chart file the chart rule owns colours, the same way it does in
231
+ // roast_validate: the kit judge and the generic colour rule stay out.
232
+ const chartColours = new Map(); // file → [{ value, index: line }]
233
+ const isChart = (file) => {
234
+ if (!system.charts) return false;
235
+ const w = wholeText(file);
236
+ return isChartFile(file, w ?? '');
237
+ };
238
+
224
239
  for (const { file, line, text } of added) {
225
240
  if (exempt(file) || outOfScope(file)) continue;
226
241
  const css = isStyleFile(file);
@@ -228,7 +243,14 @@ export function judge(added, system, { readFile, readBase } = {}) {
228
243
 
229
244
  const seen = extractStyling(text, { css });
230
245
 
231
- const onKit = css ? null : kitLines(file);
246
+ const chart = !css && isChart(file);
247
+ if (chart) {
248
+ const list = chartColours.get(file) ?? [];
249
+ for (const c of seen.colors) if (!tokenSet.has(c.value)) list.push({ value: c.value, index: line });
250
+ chartColours.set(file, list);
251
+ }
252
+
253
+ const onKit = css || chart ? null : kitLines(file);
232
254
  const kitPx = new Set();
233
255
  for (const f of onKit?.get(line) ?? []) {
234
256
  if (f.rule === 'kit-px') kitPx.add(f.value.split(': ')[1]);
@@ -239,8 +261,22 @@ export function judge(added, system, { readFile, readBase } = {}) {
239
261
  }
240
262
 
241
263
  for (const c of seen.colors) {
242
- if (onKit) break; // the kit rule owns colours on a kit file
243
- if (tokenSet.has(c.value)) continue; // disciplined token use
264
+ if (onKit || chart) break; // the kit rule or the chart rule owns colours here
265
+ if (tokenSet.has(c.value)) {
266
+ // A token's raw value is the definition only inside a file that
267
+ // states the palette (system.tokenSources, roast 9.1.3). Anywhere
268
+ // else it is the value pasted where the name belongs: the system
269
+ // cannot see it, and the next reader copies the hex.
270
+ if (!system.tokenSources || statesTokens(file)) continue;
271
+ const n = named(c.value);
272
+ findings.push({
273
+ file, line, kind: 'color', value: c.value,
274
+ advice: n !== c.value
275
+ ? `this is already the token ${n}; use the name, not the value`
276
+ : `the theme already holds this value${themeFile ? ` (${themeFile})` : ''}; read it from there rather than pasting it`,
277
+ });
278
+ continue;
279
+ }
244
280
  const near = c.value.startsWith('#') ? nearestColor(c.value, system.tokens) : null;
245
281
  findings.push({
246
282
  file, line, kind: 'color', value: c.value,
@@ -395,7 +431,13 @@ export function judge(added, system, { readFile, readBase } = {}) {
395
431
  others: system.tokenDefs.filter((d) => !samePath(d.file, file)),
396
432
  tailwind: prof.kind === 'tailwind' || /@theme\b/.test(w),
397
433
  }) : [])
398
- : (system.duplicates ? avoidedImportFindings(w, { file, before: baseText(file), dupes: system.duplicates }) : []);
434
+ : [
435
+ ...(system.duplicates ? avoidedImportFindings(w, { file, before: baseText(file), dupes: system.duplicates }) : []),
436
+ // a button built from scratch where the file's own package can
437
+ // import the repo's Button (roast 9.2.0); a warning in the engine,
438
+ // a finding here, on the line the button starts
439
+ ...(system.buttons?.length ? handmadeButtonFindings(w, { file }, system) : []),
440
+ ];
399
441
  for (const f of hits) {
400
442
  const line = lineAt(w, f.index);
401
443
  if (!lines.has(line)) continue;
@@ -406,6 +448,21 @@ export function judge(added, system, { readFile, readBase } = {}) {
406
448
  }
407
449
  }
408
450
 
451
+ // The chart rule, once per chart file, in the engine's words: with a
452
+ // palette every hand-written colour is a finding that names it; without
453
+ // one the file gets a single line that names the precedent chart and asks
454
+ // for the palette once (roast 8.8.0). `index` carries the diff line.
455
+ for (const [file, colours] of chartColours) {
456
+ if (!colours.length) continue;
457
+ for (const f of chartFindings({ file, colours, charts: system.charts, tokenFile: system.tokenFile ?? null })) {
458
+ findings.push({
459
+ file, line: f.index, kind: f.rule,
460
+ value: f.rule === 'chart-colour' ? f.message.match(/Chart colour (\S+)/)?.[1] ?? 'colour' : `${colours.length} series colour${colours.length === 1 ? '' : 's'} by hand`,
461
+ advice: `${f.message} ${f.fix.replace(/\.$/, '')}`,
462
+ });
463
+ }
464
+ }
465
+
409
466
  // Two extractors can see the same value on the same line (a hex inside a
410
467
  // Tailwind class is also a hex in the raw sweep). One sin, one line.
411
468
  const seen = new Set();
package/src/report.mjs CHANGED
@@ -22,11 +22,18 @@ const KIND_LABEL = {
22
22
  // the engine's sentence names the token or the import itself (roast 8.6)
23
23
  'twin-token': 'token that copies an existing one',
24
24
  'avoided-copy': 'import of a duplicate',
25
+ // the chart rule (roast 8.8): a colour beside a chart palette, or a chart
26
+ // painting by hand where the repo keeps none
27
+ 'chart-colour': 'chart colour written by hand',
28
+ 'chart-palette': 'chart painted by hand',
29
+ // a styled.button or a dressed button tag where the repo has a Button
30
+ // (roast 9.2.0); the engine's sentence names the Button and its import
31
+ 'handmade-button': 'button built from scratch',
25
32
  };
26
33
  const labelOf = (f) => f.label ?? KIND_LABEL[f.kind];
27
34
 
28
35
  // Kinds whose label already says everything; printing the value repeats it.
29
- const VALUELESS = new Set(['important', 'inline', 'twin-token', 'avoided-copy']);
36
+ const VALUELESS = new Set(['important', 'inline', 'twin-token', 'avoided-copy', 'chart-palette', 'handmade-button']);
30
37
 
31
38
  const FOOTER = 'Full picture of the whole codebase: `npx roast-my-design-system`';
32
39