guard-my-design-system 1.4.1 → 1.6.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 +11 -1
- package/package.json +2 -2
- package/src/judge.mjs +82 -8
- package/src/report.mjs +1 -0
package/README.md
CHANGED
|
@@ -50,12 +50,22 @@ it updates that same comment. It never adds more comments:
|
|
|
50
50
|
the nearest existing value named.
|
|
51
51
|
- **A typeface your system does not declare.**
|
|
52
52
|
- **`!important`.**
|
|
53
|
-
- **Arbitrary Tailwind values** such as `w-[137px]` and `mt-[37px]
|
|
53
|
+
- **Arbitrary Tailwind values** such as `w-[137px]` and `text-[10px]`. A bracket on a spacing utility, such as `mt-[37px]`, is reported as off-scale spacing instead, with the nearest scale step named.
|
|
54
54
|
- **An inline `style={{ }}` block.** Styling written there is invisible to the
|
|
55
55
|
system and to every agent that reads the file. Blocks built from variables
|
|
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 palette colour where a theme variable exists.** On a shadcn repo whose
|
|
60
|
+
theme file holds the variables, `text-slate-500` in the app's own code is
|
|
61
|
+
flagged and the theme file named. Off on utility-class installs.
|
|
62
|
+
|
|
63
|
+
It reads the repo the way the roast report does. On a shadcn repo the
|
|
64
|
+
installed catalogue, installed registries and kit blocks are not judged:
|
|
65
|
+
`shadcn add` is not a sin. On a repo that publishes a shadcn registry only
|
|
66
|
+
the published folders are judged. `!important` in an embedded widget's
|
|
67
|
+
stylesheet, or on a selector made of a library's own class names, is the
|
|
68
|
+
medium and passes.
|
|
59
69
|
|
|
60
70
|
It ignores everything that was already in the codebase. It asks one question
|
|
61
71
|
of a change: does it make things worse?
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "guard-my-design-system",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.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": "7.8.0"
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
17
17
|
"design-system",
|
package/src/judge.mjs
CHANGED
|
@@ -12,8 +12,39 @@ 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,
|
|
15
16
|
} from 'roast-my-design-system/engine';
|
|
16
17
|
|
|
18
|
+
// Folder membership, the way the engine's own splits do it.
|
|
19
|
+
const underAny = (file, dirs) => (dirs ?? []).some((d) => file === d || file.startsWith(`${d}/`) || file.endsWith(`/${d}`) || file.includes(`/${d}/`));
|
|
20
|
+
|
|
21
|
+
// The selector of the innermost block an added line sits in, read from the
|
|
22
|
+
// whole file: scan back from the line to the nearest unclosed "{" and take
|
|
23
|
+
// what precedes it. Null when the line is not inside a block.
|
|
24
|
+
function selectorAt(whole, lineNo) {
|
|
25
|
+
if (!whole) return null;
|
|
26
|
+
const lines = whole.split('\n');
|
|
27
|
+
// include the added line itself up to the declaration: the block often
|
|
28
|
+
// opens on the same line (`.cm-editor { font: x !important; }`)
|
|
29
|
+
const cur = lines[lineNo - 1] ?? '';
|
|
30
|
+
const cut = cur.search(/!\s*important/i);
|
|
31
|
+
const upto = lines.slice(0, Math.max(0, lineNo - 1)).join('\n') + '\n' + (cut >= 0 ? cur.slice(0, cut) : cur);
|
|
32
|
+
let depth = 0;
|
|
33
|
+
for (let i = upto.length - 1; i >= 0; i--) {
|
|
34
|
+
const ch = upto[i];
|
|
35
|
+
if (ch === '}') depth++;
|
|
36
|
+
else if (ch === '{') {
|
|
37
|
+
if (depth === 0) {
|
|
38
|
+
const before = upto.slice(0, i);
|
|
39
|
+
const start = Math.max(before.lastIndexOf('}'), before.lastIndexOf(';'), before.lastIndexOf('{'));
|
|
40
|
+
return before.slice(start + 1).trim().split('\n').pop().trim();
|
|
41
|
+
}
|
|
42
|
+
depth--;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
17
48
|
// git prints diff paths from the repository root; the engine lists them from
|
|
18
49
|
// the directory it scanned. When the guard runs in a subdirectory the two
|
|
19
50
|
// disagree by a prefix, so a suffix match stands in for equality. It errs
|
|
@@ -59,6 +90,22 @@ function exemptFiles(added, readFile) {
|
|
|
59
90
|
*/
|
|
60
91
|
export function judge(added, system, { readFile } = {}) {
|
|
61
92
|
const tokenSet = new Set(system.tokens);
|
|
93
|
+
// How the repo was read, from the engine's own profiles (roast 7.8):
|
|
94
|
+
// installed code is not the change's sin, a registry is judged on what it
|
|
95
|
+
// publishes, and a palette class counts only where a theme variable exists.
|
|
96
|
+
const prof = system.profile ?? {};
|
|
97
|
+
const installed = prof.installedDirs ?? [];
|
|
98
|
+
const counted = prof.registry?.countedDirs ?? [];
|
|
99
|
+
const variants = prof.registry?.variants ?? [];
|
|
100
|
+
const blockDirs = prof.registry?.blockDirs ?? [];
|
|
101
|
+
const outOfScope = (file) => underAny(file, installed) || (counted.length > 0 && !underAny(file, counted));
|
|
102
|
+
const whole = new Map();
|
|
103
|
+
const wholeText = (file) => {
|
|
104
|
+
if (!whole.has(file)) { let t = null; if (readFile) { try { t = readFile(file); } catch { t = null; } } whole.set(file, t); }
|
|
105
|
+
return whole.get(file);
|
|
106
|
+
};
|
|
107
|
+
const isWidgetFile = (file) => underAny(file, prof.widgetDirs) || WIDGET_CSS_RE.test(wholeText(file) ?? '');
|
|
108
|
+
const paletteRe = new RegExp(PALETTE_CLASS_RE.source, 'g');
|
|
62
109
|
|
|
63
110
|
// The system was learned from the tree that already CONTAINS these added
|
|
64
111
|
// lines, so a new value would vouch for itself. A value is only "known"
|
|
@@ -67,7 +114,7 @@ export function judge(added, system, { readFile } = {}) {
|
|
|
67
114
|
const addedLengths = new Map(), addedFaces = new Map();
|
|
68
115
|
const addedExtras = { radius: new Map(), fontsize: new Map(), shadow: new Map() };
|
|
69
116
|
for (const { file, line, text } of added) {
|
|
70
|
-
if (exempt(file)) continue;
|
|
117
|
+
if (exempt(file) || outOfScope(file)) continue;
|
|
71
118
|
const css = isStyleFile(file);
|
|
72
119
|
if (!css && !isCodeFile(file)) continue;
|
|
73
120
|
for (const s of extractStyling(text, { css }).spacing) {
|
|
@@ -127,7 +174,7 @@ export function judge(added, system, { readFile } = {}) {
|
|
|
127
174
|
const findings = [];
|
|
128
175
|
|
|
129
176
|
for (const { file, line, text } of added) {
|
|
130
|
-
if (exempt(file)) continue;
|
|
177
|
+
if (exempt(file) || outOfScope(file)) continue;
|
|
131
178
|
const css = isStyleFile(file);
|
|
132
179
|
if (!css && !isCodeFile(file)) continue;
|
|
133
180
|
|
|
@@ -188,7 +235,12 @@ export function judge(added, system, { readFile } = {}) {
|
|
|
188
235
|
// is whether the name lives anywhere ELSE.
|
|
189
236
|
if (!css && componentsByName.size) {
|
|
190
237
|
for (const name of definedComponents(text)) {
|
|
191
|
-
const
|
|
238
|
+
const variantOf = (f) => variants.find((v) => underAny(f, [v])) ?? null;
|
|
239
|
+
const elsewhere = (componentsByName.get(name) ?? []).filter((c) => !samePath(c.file, file))
|
|
240
|
+
// a registry keeps the same component in sibling variants, and a
|
|
241
|
+
// block installs alone: neither is a second Button
|
|
242
|
+
.filter((c) => !(variantOf(file) && variantOf(c.file) && variantOf(c.file) !== variantOf(file)))
|
|
243
|
+
.filter((c) => !(underAny(file, blockDirs) && underAny(c.file, blockDirs)));
|
|
192
244
|
if (!elsewhere.length) continue;
|
|
193
245
|
const best = [...elsewhere].sort((a, b) => b.usageCount - a.usageCount)[0];
|
|
194
246
|
findings.push({
|
|
@@ -209,11 +261,33 @@ export function judge(added, system, { readFile } = {}) {
|
|
|
209
261
|
});
|
|
210
262
|
}
|
|
211
263
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
264
|
+
// !important is the medium, not the mess, in two places the report also
|
|
265
|
+
// sets aside: a widget stylesheet that must beat its host page, and a
|
|
266
|
+
// selector aimed only at a library's own class names.
|
|
267
|
+
if (seen.important.length && !(css && isWidgetFile(file))) {
|
|
268
|
+
const sel = css ? selectorAt(wholeText(file), line) : null;
|
|
269
|
+
const classes = sel ? [...sel.matchAll(/\.([A-Za-z_][\w-]*)/g)].map((m) => m[1]) : [];
|
|
270
|
+
const libraryAimed = classes.length > 0 && classes.every(isLibraryClass);
|
|
271
|
+
if (!libraryAimed) {
|
|
272
|
+
for (const _ of seen.important) {
|
|
273
|
+
findings.push({
|
|
274
|
+
file, line, kind: 'important', value: '!important',
|
|
275
|
+
advice: 'the cascade admitting defeat; raise specificity or fix the source order',
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// A palette class where a theme variable exists (a shadcn kit in
|
|
282
|
+
// CSS-variable mode): paint from a tin. The same pattern the report
|
|
283
|
+
// counts per 100 files; here, per added line.
|
|
284
|
+
if (!css && prof.paletteReady) {
|
|
285
|
+
for (const m of text.matchAll(paletteRe)) {
|
|
286
|
+
findings.push({
|
|
287
|
+
file, line, kind: 'palette', value: m[0],
|
|
288
|
+
advice: `a theme variable covers this; use a semantic class such as bg-primary or text-muted-foreground, or add a variable${prof.sheetFile ? ` to ${prof.sheetFile}` : ''}`,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
217
291
|
}
|
|
218
292
|
|
|
219
293
|
// The engine's fontDeclarations decides what a judgeable font value is
|
package/src/report.mjs
CHANGED
|
@@ -15,6 +15,7 @@ const KIND_LABEL = {
|
|
|
15
15
|
font: 'new typeface',
|
|
16
16
|
inline: 'inline style block',
|
|
17
17
|
component: 'second definition of',
|
|
18
|
+
palette: 'palette colour where a theme variable exists',
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
// Kinds whose label already says everything; printing the value repeats it.
|