guard-my-design-system 1.8.0 → 2.0.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 +25 -3
- package/index.mjs +8 -1
- package/package.json +2 -2
- package/src/judge.mjs +81 -4
- package/src/report.mjs +8 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# guard-my-design-system
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/guard-my-design-system) [](https://www.npmjs.com/package/guard-my-design-system) [](LICENSE) [](https://github.com/gregkozakiewicz/guard-my-design-system#what-makes-the-verdict-trustworthy) [](https://www.npmjs.com/package/guard-my-design-system) [](https://www.npmjs.com/package/guard-my-design-system) [](LICENSE) [](https://github.com/gregkozakiewicz/guard-my-design-system#what-makes-the-verdict-trustworthy) [](#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
|
|
|
@@ -56,9 +56,31 @@ it updates that same comment. It never adds more comments:
|
|
|
56
56
|
are decided elsewhere, so they are left alone.
|
|
57
57
|
- **A second definition of a component you already have.** The finding names
|
|
58
58
|
the file that already defines it, and how many places use that one.
|
|
59
|
+
- **A new import of a duplicate component.** When a name is defined in more
|
|
60
|
+
than one file and one copy is clearly the main one, importing another copy
|
|
61
|
+
is flagged. The finding names the main copy, how often each is used, and
|
|
62
|
+
the colours the other copy hard-codes. If two copies are used about
|
|
63
|
+
equally, nothing is flagged.
|
|
64
|
+
- **A new colour token that copies an existing one.** A token added to a
|
|
65
|
+
stylesheet whose value is almost the same as a token the system already
|
|
66
|
+
has, or whose dark value is exactly the same, is flagged with the existing
|
|
67
|
+
token named: `--color-overdue-soft (#fff4e5) is a twin of the existing
|
|
68
|
+
--color-warning-soft (#fdf5e6)`. Numbered steps such as `gray-100` and
|
|
69
|
+
shadcn's own theme variables are never compared.
|
|
59
70
|
- **A palette colour where a theme variable exists.** On a shadcn repo whose
|
|
60
71
|
theme file holds the variables, `text-slate-500` in the app's own code is
|
|
61
72
|
flagged and the theme file named. Off on utility-class installs.
|
|
73
|
+
- **A chart colour written by hand.** A chart needs several colours that
|
|
74
|
+
differ from each other, and most design systems never name them, so the
|
|
75
|
+
chart rule (roast 8.8) has three answers. Where the repo keeps a chart
|
|
76
|
+
palette (`--chart-*` or `--series-*` tokens, a `chartColors` entry in the
|
|
77
|
+
theme, or shadcn's `--chart-1` to `--chart-5` when a chart actually reads
|
|
78
|
+
them), a hex in a chart file is flagged and the palette named. Where the
|
|
79
|
+
repo has charts but no palette, a new chart painting by hand gets one line
|
|
80
|
+
that names the existing chart doing the same and asks for the palette
|
|
81
|
+
once. The first chart in a repo gets one line asking for a name. The
|
|
82
|
+
generic colour rule stays out of chart files, the same way it does in
|
|
83
|
+
`roast_validate`.
|
|
62
84
|
- **A colour or a pixel size written onto a kit component.** On a product
|
|
63
85
|
built on MUI, Mantine, Chakra UI or Ant Design, `color: '#667085'` in an
|
|
64
86
|
`sx` prop or a style object is flagged and the finding says whether the
|
|
@@ -123,7 +145,7 @@ jobs:
|
|
|
123
145
|
- uses: actions/checkout@v5
|
|
124
146
|
with:
|
|
125
147
|
fetch-depth: 0
|
|
126
|
-
- uses: gregkozakiewicz/guard-my-design-system@
|
|
148
|
+
- uses: gregkozakiewicz/guard-my-design-system@v2
|
|
127
149
|
```
|
|
128
150
|
|
|
129
151
|
After that it runs on every pull request and needs no attention from you.
|
|
@@ -132,7 +154,7 @@ If you want the check to fail instead of commenting, turn on strict mode.
|
|
|
132
154
|
It is the only setting:
|
|
133
155
|
|
|
134
156
|
```yaml
|
|
135
|
-
- uses: gregkozakiewicz/guard-my-design-system@
|
|
157
|
+
- uses: gregkozakiewicz/guard-my-design-system@v2
|
|
136
158
|
with:
|
|
137
159
|
strict: true
|
|
138
160
|
```
|
package/index.mjs
CHANGED
|
@@ -81,7 +81,14 @@ const readWhole = (file) => {
|
|
|
81
81
|
}
|
|
82
82
|
return wholeFile.get(file);
|
|
83
83
|
};
|
|
84
|
-
|
|
84
|
+
// The file at the base, so a token or an import already there before this
|
|
85
|
+
// change is not reported as new. null: the change creates the file.
|
|
86
|
+
const readBase = (file) => {
|
|
87
|
+
try {
|
|
88
|
+
return execFileSync('git', ['show', `${base}:${file}`], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 16 * 1024 * 1024 });
|
|
89
|
+
} catch { return null; }
|
|
90
|
+
};
|
|
91
|
+
let findings = judge(judged, system, { readFile: readWhole, readBase });
|
|
85
92
|
|
|
86
93
|
// The escape hatch: a `guard-ignore-next-line` comment silences every finding
|
|
87
94
|
// on the line below it. Checked against the file as it stands (not just the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "guard-my-design-system",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.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": "
|
|
14
|
+
"roast-my-design-system": "9.0.0"
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
17
17
|
"design-system",
|
package/src/judge.mjs
CHANGED
|
@@ -13,6 +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, isChartFile, chartFindings,
|
|
16
17
|
} from 'roast-my-design-system/engine';
|
|
17
18
|
|
|
18
19
|
// Folder membership, the way the engine's own splits do it.
|
|
@@ -86,9 +87,13 @@ function exemptFiles(added, readFile) {
|
|
|
86
87
|
* Judge added lines against the learned system.
|
|
87
88
|
* Returns [{ file, line, kind, value, advice }] sorted by file then line.
|
|
88
89
|
* kinds: color | spacing | radius | fontsize | shadow | arbitrary |
|
|
89
|
-
* important | font | inline | component
|
|
90
|
+
* important | font | inline | component | palette | kit-colour |
|
|
91
|
+
* kit-px | twin-token | avoided-copy | chart-colour | chart-palette
|
|
92
|
+
* readFile(file) gives the file as it stands; readBase(file) the file at the
|
|
93
|
+
* base (null when the change creates it), so a token or an import the change
|
|
94
|
+
* adds can be told from one that was already there.
|
|
90
95
|
*/
|
|
91
|
-
export function judge(added, system, { readFile } = {}) {
|
|
96
|
+
export function judge(added, system, { readFile, readBase } = {}) {
|
|
92
97
|
const tokenSet = new Set(system.tokens);
|
|
93
98
|
// How the repo was read, from the engine's own profiles (roast 7.8):
|
|
94
99
|
// installed code is not the change's sin, a registry is judged on what it
|
|
@@ -216,6 +221,18 @@ export function judge(added, system, { readFile } = {}) {
|
|
|
216
221
|
|
|
217
222
|
const findings = [];
|
|
218
223
|
|
|
224
|
+
// A chart's series colours are judged against the chart palette, or its
|
|
225
|
+
// absence, by the engine (roast 8.8.0, lib/charts): the report never
|
|
226
|
+
// counted them, the live checks counted every one, and the guard did too.
|
|
227
|
+
// On a chart file the chart rule owns colours, the same way it does in
|
|
228
|
+
// roast_validate: the kit judge and the generic colour rule stay out.
|
|
229
|
+
const chartColours = new Map(); // file → [{ value, index: line }]
|
|
230
|
+
const isChart = (file) => {
|
|
231
|
+
if (!system.charts) return false;
|
|
232
|
+
const w = wholeText(file);
|
|
233
|
+
return isChartFile(file, w ?? '');
|
|
234
|
+
};
|
|
235
|
+
|
|
219
236
|
for (const { file, line, text } of added) {
|
|
220
237
|
if (exempt(file) || outOfScope(file)) continue;
|
|
221
238
|
const css = isStyleFile(file);
|
|
@@ -223,7 +240,14 @@ export function judge(added, system, { readFile } = {}) {
|
|
|
223
240
|
|
|
224
241
|
const seen = extractStyling(text, { css });
|
|
225
242
|
|
|
226
|
-
const
|
|
243
|
+
const chart = !css && isChart(file);
|
|
244
|
+
if (chart) {
|
|
245
|
+
const list = chartColours.get(file) ?? [];
|
|
246
|
+
for (const c of seen.colors) if (!tokenSet.has(c.value)) list.push({ value: c.value, index: line });
|
|
247
|
+
chartColours.set(file, list);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const onKit = css || chart ? null : kitLines(file);
|
|
227
251
|
const kitPx = new Set();
|
|
228
252
|
for (const f of onKit?.get(line) ?? []) {
|
|
229
253
|
if (f.rule === 'kit-px') kitPx.add(f.value.split(': ')[1]);
|
|
@@ -234,7 +258,7 @@ export function judge(added, system, { readFile } = {}) {
|
|
|
234
258
|
}
|
|
235
259
|
|
|
236
260
|
for (const c of seen.colors) {
|
|
237
|
-
if (onKit) break; // the kit rule
|
|
261
|
+
if (onKit || chart) break; // the kit rule or the chart rule owns colours here
|
|
238
262
|
if (tokenSet.has(c.value)) continue; // disciplined token use
|
|
239
263
|
const near = c.value.startsWith('#') ? nearestColor(c.value, system.tokens) : null;
|
|
240
264
|
findings.push({
|
|
@@ -363,6 +387,59 @@ export function judge(added, system, { readFile } = {}) {
|
|
|
363
387
|
}
|
|
364
388
|
}
|
|
365
389
|
|
|
390
|
+
// Two checks that read the whole file against its base, roast 8.6: a new
|
|
391
|
+
// colour token that copies one the system already has, and a new import of
|
|
392
|
+
// the duplicate the canonical copy replaces. The engine words both, the
|
|
393
|
+
// same words roast_validate, roast_review and --check give; the guard keeps
|
|
394
|
+
// the hits on added lines.
|
|
395
|
+
const addedAt = new Map();
|
|
396
|
+
for (const { file, line } of added) {
|
|
397
|
+
if (!addedAt.has(file)) addedAt.set(file, new Set());
|
|
398
|
+
addedAt.get(file).add(line);
|
|
399
|
+
}
|
|
400
|
+
const baseText = (file) => {
|
|
401
|
+
if (!readBase) return undefined;
|
|
402
|
+
try { return readBase(file); } catch { return undefined; }
|
|
403
|
+
};
|
|
404
|
+
const lineAt = (text, index) => text.slice(0, index).split('\n').length;
|
|
405
|
+
for (const [file, lines] of addedAt) {
|
|
406
|
+
if (exempt(file) || outOfScope(file)) continue;
|
|
407
|
+
const css = isStyleFile(file);
|
|
408
|
+
if (!css && !isCodeFile(file)) continue;
|
|
409
|
+
const w = wholeText(file);
|
|
410
|
+
if (w == null) continue;
|
|
411
|
+
const hits = css
|
|
412
|
+
? (system.tokenDefs && w.includes('--') ? tokenTwinFindings(w, {
|
|
413
|
+
before: baseText(file),
|
|
414
|
+
others: system.tokenDefs.filter((d) => !samePath(d.file, file)),
|
|
415
|
+
tailwind: prof.kind === 'tailwind' || /@theme\b/.test(w),
|
|
416
|
+
}) : [])
|
|
417
|
+
: (system.duplicates ? avoidedImportFindings(w, { file, before: baseText(file), dupes: system.duplicates }) : []);
|
|
418
|
+
for (const f of hits) {
|
|
419
|
+
const line = lineAt(w, f.index);
|
|
420
|
+
if (!lines.has(line)) continue;
|
|
421
|
+
findings.push({
|
|
422
|
+
file, line, kind: f.rule, value: f.name,
|
|
423
|
+
advice: `${f.message} ${f.fix.replace(/\.$/, '')}`,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// The chart rule, once per chart file, in the engine's words: with a
|
|
429
|
+
// palette every hand-written colour is a finding that names it; without
|
|
430
|
+
// one the file gets a single line that names the precedent chart and asks
|
|
431
|
+
// for the palette once (roast 8.8.0). `index` carries the diff line.
|
|
432
|
+
for (const [file, colours] of chartColours) {
|
|
433
|
+
if (!colours.length) continue;
|
|
434
|
+
for (const f of chartFindings({ file, colours, charts: system.charts, tokenFile: system.tokenFile ?? null })) {
|
|
435
|
+
findings.push({
|
|
436
|
+
file, line: f.index, kind: f.rule,
|
|
437
|
+
value: f.rule === 'chart-colour' ? f.message.match(/Chart colour (\S+)/)?.[1] ?? 'colour' : `${colours.length} series colour${colours.length === 1 ? '' : 's'} by hand`,
|
|
438
|
+
advice: `${f.message} ${f.fix.replace(/\.$/, '')}`,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
366
443
|
// Two extractors can see the same value on the same line (a hex inside a
|
|
367
444
|
// Tailwind class is also a hex in the raw sweep). One sin, one line.
|
|
368
445
|
const seen = new Set();
|
package/src/report.mjs
CHANGED
|
@@ -19,11 +19,18 @@ const KIND_LABEL = {
|
|
|
19
19
|
// on a kit repo the engine words the label with the kit's name (f.label)
|
|
20
20
|
'kit-colour': 'colour written onto a kit component',
|
|
21
21
|
'kit-px': 'pixel size on a kit component',
|
|
22
|
+
// the engine's sentence names the token or the import itself (roast 8.6)
|
|
23
|
+
'twin-token': 'token that copies an existing one',
|
|
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',
|
|
22
29
|
};
|
|
23
30
|
const labelOf = (f) => f.label ?? KIND_LABEL[f.kind];
|
|
24
31
|
|
|
25
32
|
// Kinds whose label already says everything; printing the value repeats it.
|
|
26
|
-
const VALUELESS = new Set(['important', 'inline']);
|
|
33
|
+
const VALUELESS = new Set(['important', 'inline', 'twin-token', 'avoided-copy', 'chart-palette']);
|
|
27
34
|
|
|
28
35
|
const FOOTER = 'Full picture of the whole codebase: `npx roast-my-design-system`';
|
|
29
36
|
|