guard-my-design-system 1.7.0 → 1.9.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 +23 -2
- package/index.mjs +8 -1
- package/package.json +2 -2
- package/src/judge.mjs +87 -3
- package/src/report.mjs +10 -3
package/README.md
CHANGED
|
@@ -56,11 +56,32 @@ 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.
|
|
62
|
-
|
|
63
|
-
|
|
73
|
+
- **A colour or a pixel size written onto a kit component.** On a product
|
|
74
|
+
built on MUI, Mantine, Chakra UI or Ant Design, `color: '#667085'` in an
|
|
75
|
+
`sx` prop or a style object is flagged and the finding says whether the
|
|
76
|
+
theme already holds that colour, or tells you to add it there once. A
|
|
77
|
+
pixel size such as `p: '12px'` is turned into the theme's spacing step. The
|
|
78
|
+
advice is in the kit's own words: `sx` paths on MUI, props on Mantine,
|
|
79
|
+
style props on Chakra, the theme config on Ant Design.
|
|
80
|
+
|
|
81
|
+
It reads the repo the way the roast report does. On a kit repo the theme's
|
|
82
|
+
colours are the token set, and a colour the theme already holds is flagged
|
|
83
|
+
on a kit component all the same: writing it by hand is the exact mistake the
|
|
84
|
+
check exists for. On a shadcn repo the
|
|
64
85
|
installed catalogue, installed registries and kit blocks are not judged:
|
|
65
86
|
`shadcn add` is not a sin. On a repo that publishes a shadcn registry only
|
|
66
87
|
the published folders are judged. `!important` in an embedded widget's
|
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": "1.
|
|
3
|
+
"version": "1.9.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.
|
|
14
|
+
"roast-my-design-system": "8.6.1"
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
17
17
|
"design-system",
|
package/src/judge.mjs
CHANGED
|
@@ -12,7 +12,8 @@ import {
|
|
|
12
12
|
isCodeFile, isStyleFile, typefaceOf, GENERIC_FONTS,
|
|
13
13
|
definedComponents, exemptReason,
|
|
14
14
|
EXTRA_KINDS, extraValue, fontDeclarations,
|
|
15
|
-
WIDGET_CSS_RE, isLibraryClass, PALETTE_CLASS_RE, blankComments,
|
|
15
|
+
WIDGET_CSS_RE, isLibraryClass, PALETTE_CLASS_RE, blankComments, kitPaintFindings,
|
|
16
|
+
tokenTwinFindings, avoidedImportFindings,
|
|
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
|
|
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
|
|
@@ -185,6 +190,35 @@ export function judge(added, system, { readFile } = {}) {
|
|
|
185
190
|
}
|
|
186
191
|
}
|
|
187
192
|
|
|
193
|
+
// A product built on a kit (MUI, Mantine, Chakra UI, Ant Design), roast
|
|
194
|
+
// 8.4.6: a colour or a pixel size written onto a kit component where the
|
|
195
|
+
// theme has a value. The engine judges the whole file (the import that makes
|
|
196
|
+
// it a kit file sits at the top, where the diff never looks) and the guard
|
|
197
|
+
// keeps the hits on added lines. On a kit file the kit rule owns colours
|
|
198
|
+
// and the pixel sizes it named, so the generic rules stay quiet about the
|
|
199
|
+
// same value: one line, one finding, the same words as the roast report's
|
|
200
|
+
// live checks.
|
|
201
|
+
const kit = prof.kit ?? null;
|
|
202
|
+
const kitJudged = new Map();
|
|
203
|
+
const kitLines = (file) => {
|
|
204
|
+
if (!kit) return null;
|
|
205
|
+
if (!kitJudged.has(file)) {
|
|
206
|
+
const w = wholeText(file);
|
|
207
|
+
const j = w == null ? null : kitPaintFindings(w, kit, { file });
|
|
208
|
+
if (!j || j.exempt) kitJudged.set(file, null);
|
|
209
|
+
else {
|
|
210
|
+
const byLine = new Map();
|
|
211
|
+
for (const f of j.findings) {
|
|
212
|
+
const ln = w.slice(0, f.index).split('\n').length;
|
|
213
|
+
if (!byLine.has(ln)) byLine.set(ln, []);
|
|
214
|
+
byLine.get(ln).push(f);
|
|
215
|
+
}
|
|
216
|
+
kitJudged.set(file, byLine);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return kitJudged.get(file);
|
|
220
|
+
};
|
|
221
|
+
|
|
188
222
|
const findings = [];
|
|
189
223
|
|
|
190
224
|
for (const { file, line, text } of added) {
|
|
@@ -194,7 +228,18 @@ export function judge(added, system, { readFile } = {}) {
|
|
|
194
228
|
|
|
195
229
|
const seen = extractStyling(text, { css });
|
|
196
230
|
|
|
231
|
+
const onKit = css ? null : kitLines(file);
|
|
232
|
+
const kitPx = new Set();
|
|
233
|
+
for (const f of onKit?.get(line) ?? []) {
|
|
234
|
+
if (f.rule === 'kit-px') kitPx.add(f.value.split(': ')[1]);
|
|
235
|
+
findings.push({
|
|
236
|
+
file, line, kind: f.rule, value: f.value, label: f.label,
|
|
237
|
+
advice: f.note ? `${f.note}. ${f.fix.replace(/\.$/, '')}` : f.fix.replace(/\.$/, ''),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
197
241
|
for (const c of seen.colors) {
|
|
242
|
+
if (onKit) break; // the kit rule owns colours on a kit file
|
|
198
243
|
if (tokenSet.has(c.value)) continue; // disciplined token use
|
|
199
244
|
const near = c.value.startsWith('#') ? nearestColor(c.value, system.tokens) : null;
|
|
200
245
|
findings.push({
|
|
@@ -208,6 +253,7 @@ export function judge(added, system, { readFile } = {}) {
|
|
|
208
253
|
}
|
|
209
254
|
|
|
210
255
|
for (const s of seen.spacing) {
|
|
256
|
+
if (kitPx.has(s.value)) continue; // the kit rule said it
|
|
211
257
|
if (knownLengths.has(s.value)) continue; // the codebase already uses it
|
|
212
258
|
const near = nearestLength(s.value, [...knownLengths]);
|
|
213
259
|
findings.push({
|
|
@@ -322,6 +368,44 @@ export function judge(added, system, { readFile } = {}) {
|
|
|
322
368
|
}
|
|
323
369
|
}
|
|
324
370
|
|
|
371
|
+
// Two checks that read the whole file against its base, roast 8.6: a new
|
|
372
|
+
// colour token that copies one the system already has, and a new import of
|
|
373
|
+
// the duplicate the canonical copy replaces. The engine words both, the
|
|
374
|
+
// same words roast_validate, roast_review and --check give; the guard keeps
|
|
375
|
+
// the hits on added lines.
|
|
376
|
+
const addedAt = new Map();
|
|
377
|
+
for (const { file, line } of added) {
|
|
378
|
+
if (!addedAt.has(file)) addedAt.set(file, new Set());
|
|
379
|
+
addedAt.get(file).add(line);
|
|
380
|
+
}
|
|
381
|
+
const baseText = (file) => {
|
|
382
|
+
if (!readBase) return undefined;
|
|
383
|
+
try { return readBase(file); } catch { return undefined; }
|
|
384
|
+
};
|
|
385
|
+
const lineAt = (text, index) => text.slice(0, index).split('\n').length;
|
|
386
|
+
for (const [file, lines] of addedAt) {
|
|
387
|
+
if (exempt(file) || outOfScope(file)) continue;
|
|
388
|
+
const css = isStyleFile(file);
|
|
389
|
+
if (!css && !isCodeFile(file)) continue;
|
|
390
|
+
const w = wholeText(file);
|
|
391
|
+
if (w == null) continue;
|
|
392
|
+
const hits = css
|
|
393
|
+
? (system.tokenDefs && w.includes('--') ? tokenTwinFindings(w, {
|
|
394
|
+
before: baseText(file),
|
|
395
|
+
others: system.tokenDefs.filter((d) => !samePath(d.file, file)),
|
|
396
|
+
tailwind: prof.kind === 'tailwind' || /@theme\b/.test(w),
|
|
397
|
+
}) : [])
|
|
398
|
+
: (system.duplicates ? avoidedImportFindings(w, { file, before: baseText(file), dupes: system.duplicates }) : []);
|
|
399
|
+
for (const f of hits) {
|
|
400
|
+
const line = lineAt(w, f.index);
|
|
401
|
+
if (!lines.has(line)) continue;
|
|
402
|
+
findings.push({
|
|
403
|
+
file, line, kind: f.rule, value: f.name,
|
|
404
|
+
advice: `${f.message} ${f.fix.replace(/\.$/, '')}`,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
325
409
|
// Two extractors can see the same value on the same line (a hex inside a
|
|
326
410
|
// Tailwind class is also a hex in the raw sweep). One sin, one line.
|
|
327
411
|
const seen = new Set();
|
package/src/report.mjs
CHANGED
|
@@ -16,10 +16,17 @@ const KIND_LABEL = {
|
|
|
16
16
|
inline: 'inline style block',
|
|
17
17
|
component: 'second definition of',
|
|
18
18
|
palette: 'palette colour where a theme variable exists',
|
|
19
|
+
// on a kit repo the engine words the label with the kit's name (f.label)
|
|
20
|
+
'kit-colour': 'colour written onto a kit component',
|
|
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',
|
|
19
25
|
};
|
|
26
|
+
const labelOf = (f) => f.label ?? KIND_LABEL[f.kind];
|
|
20
27
|
|
|
21
28
|
// Kinds whose label already says everything; printing the value repeats it.
|
|
22
|
-
const VALUELESS = new Set(['important', 'inline']);
|
|
29
|
+
const VALUELESS = new Set(['important', 'inline', 'twin-token', 'avoided-copy']);
|
|
23
30
|
|
|
24
31
|
const FOOTER = 'Full picture of the whole codebase: `npx roast-my-design-system`';
|
|
25
32
|
|
|
@@ -29,7 +36,7 @@ export function terminalReport(findings) {
|
|
|
29
36
|
}
|
|
30
37
|
const lines = [`guard-my-design-system: ${findings.length} new issue${findings.length === 1 ? '' : 's'} in this change\n`];
|
|
31
38
|
for (const f of findings) {
|
|
32
|
-
lines.push(` ${f.file}:${f.line} · ${
|
|
39
|
+
lines.push(` ${f.file}:${f.line} · ${labelOf(f)} ${VALUELESS.has(f.kind) ? '' : f.value}`.trimEnd() + `. ${capitalise(f.advice)}.`);
|
|
33
40
|
}
|
|
34
41
|
lines.push('');
|
|
35
42
|
lines.push(' Only lines added in this change were counted. The existing codebase was not judged.');
|
|
@@ -49,7 +56,7 @@ export function markdownReport(findings) {
|
|
|
49
56
|
}
|
|
50
57
|
const out = [`**🛡 guard-my-design-system: ${findings.length} new issue${findings.length === 1 ? '' : 's'} in this pull request**`, ''];
|
|
51
58
|
for (const f of findings) {
|
|
52
|
-
out.push(`- \`${f.file}:${f.line}\` · ${
|
|
59
|
+
out.push(`- \`${f.file}:${f.line}\` · ${labelOf(f)} ${VALUELESS.has(f.kind) ? '' : `\`${f.value}\``}`.trimEnd() + `. ${capitalise(f.advice)}.`);
|
|
53
60
|
}
|
|
54
61
|
out.push('');
|
|
55
62
|
out.push(`<sub>Only added lines are checked; the existing codebase is never judged. ${FOOTER}</sub>`);
|