roast-my-design-system 5.5.0 → 5.5.2
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
|
@@ -59,7 +59,7 @@ One scan powers all of it; the flags decide what lands on disk. Combine freely.
|
|
|
59
59
|
|
|
60
60
|
The full report for vercel/ai-chatbot, top to bottom — including "What the numbers mean", Claude's read of the scan, embedded right under the verdict:
|
|
61
61
|
|
|
62
|
-

|
|
63
63
|
|
|
64
64
|
The same report in light mode (one file, built-in toggle):
|
|
65
65
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roast-my-design-system",
|
|
3
|
-
"version": "5.5.
|
|
3
|
+
"version": "5.5.2",
|
|
4
4
|
"mcpName": "io.github.gregkozakiewicz/roast-my-design-system",
|
|
5
5
|
"description": "Your AI can write the UI. This makes sure it writes your UI. A deterministic scanner scores your design system 0-100 against 34 public repos, writes rules for Claude, Cursor, Copilot and Windsurf with --apply, and runs as a local MCP server with --mcp.",
|
|
6
6
|
"keywords": [
|
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
"license": "MIT",
|
|
31
31
|
"author": "Greg Kozakiewicz (https://gregkozakiewicz.com)",
|
|
32
32
|
"type": "module",
|
|
33
|
+
"exports": {
|
|
34
|
+
"./engine": "./skills/roast-my-design-system/scripts/lib/guard-api.mjs",
|
|
35
|
+
"./package.json": "./package.json"
|
|
36
|
+
},
|
|
33
37
|
"bin": {
|
|
34
38
|
"roast-my-design-system": "bin/roast.mjs"
|
|
35
39
|
},
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* guard-api — the one official doorway into the engine for OTHER packages.
|
|
3
|
+
*
|
|
4
|
+
* guard-my-design-system imports this file (through the roast npm package's
|
|
5
|
+
* "./engine" export). Everything else in the engine stays private and free to
|
|
6
|
+
* refactor; what is exported here is a public promise — rename or reshape it
|
|
7
|
+
* only with a deliberate version bump and a changelog line.
|
|
8
|
+
*
|
|
9
|
+
* Kept deliberately narrow: learn what the repo's system is, scan a piece of
|
|
10
|
+
* text for styling, and say which on-system value a stray most resembles.
|
|
11
|
+
*/
|
|
12
|
+
import { extname } from 'node:path';
|
|
13
|
+
import { walkRepo } from '../harvest/walk.mjs';
|
|
14
|
+
import { harvestTokens, extractStyling, normalizeHex, isGrey } from '../harvest/tokens.mjs';
|
|
15
|
+
import { loadExclusions } from './exclusions.mjs';
|
|
16
|
+
import { hexRgb } from './nearpairs.mjs';
|
|
17
|
+
import { typefaceOf, GENERIC_FONTS } from './typefaces.mjs';
|
|
18
|
+
|
|
19
|
+
export { extractStyling, normalizeHex, isGrey, hexRgb, typefaceOf, GENERIC_FONTS };
|
|
20
|
+
|
|
21
|
+
// Same classification walkRepo uses, exposed so a caller looking at one file
|
|
22
|
+
// (a diff hunk) treats it the way the full scan would.
|
|
23
|
+
const CODE_EXTS = new Set(['.tsx', '.jsx', '.ts', '.js', '.mjs', '.cjs']);
|
|
24
|
+
const STYLE_EXTS = new Set(['.css', '.scss', '.sass', '.less']);
|
|
25
|
+
export const isCodeFile = (p) => CODE_EXTS.has(extname(p));
|
|
26
|
+
export const isStyleFile = (p) => STYLE_EXTS.has(extname(p));
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Scan the whole repo once and return what its design system IS — the
|
|
30
|
+
* reference a guard judges new lines against. Respects .roastignore.
|
|
31
|
+
*
|
|
32
|
+
* Returns {
|
|
33
|
+
* tokenFile, // the file defining the most --vars, or null
|
|
34
|
+
* colors, // [{ value, count, isToken, files }] every colour seen
|
|
35
|
+
* tokens, // shorthand: just the token colour values
|
|
36
|
+
* spacing, // [{ value, count, files }] every length seen
|
|
37
|
+
* fontFamilies, // [{ value, count, files }] every family declared
|
|
38
|
+
* tailwind, // { colors, spacing, radii, textSizes, arbitrary }
|
|
39
|
+
* files, // { styles: n, code: n } — how much was read
|
|
40
|
+
* }
|
|
41
|
+
*/
|
|
42
|
+
export function learnSystem(repoRoot, { exclude = [] } = {}) {
|
|
43
|
+
const exclusions = loadExclusions(repoRoot, exclude);
|
|
44
|
+
const files = walkRepo(repoRoot, 14, exclusions);
|
|
45
|
+
const t = harvestTokens(repoRoot, files.styles, files.code);
|
|
46
|
+
return {
|
|
47
|
+
tokenFile: t.tokenFile,
|
|
48
|
+
colors: t.colors,
|
|
49
|
+
tokens: t.colors.filter((c) => c.isToken).map((c) => c.value),
|
|
50
|
+
spacing: t.spacing,
|
|
51
|
+
fontFamilies: t.fontFamilies,
|
|
52
|
+
tailwind: t.tailwind,
|
|
53
|
+
files: { styles: files.styles.length, code: files.code.length },
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The token a stray colour most resembles. Distance is the largest channel
|
|
59
|
+
* difference (the same yardstick nearColorPairs uses), so "8" here means what
|
|
60
|
+
* "near-identical" means everywhere else in the engine. Hex-parseable values
|
|
61
|
+
* only; returns { value, distance } or null.
|
|
62
|
+
*/
|
|
63
|
+
export function nearestColor(value, tokenValues) {
|
|
64
|
+
const target = hexRgb(normalizeHex(value));
|
|
65
|
+
if (!target) return null;
|
|
66
|
+
let best = null;
|
|
67
|
+
for (const t of tokenValues) {
|
|
68
|
+
const rgb = hexRgb(t);
|
|
69
|
+
if (!rgb || rgb.a !== target.a) continue;
|
|
70
|
+
const d = Math.max(Math.abs(rgb.r - target.r), Math.abs(rgb.g - target.g), Math.abs(rgb.b - target.b));
|
|
71
|
+
if (d === 0) continue; // identical to a token: not a stray at all
|
|
72
|
+
if (!best || d < best.distance) best = { value: t, distance: d };
|
|
73
|
+
}
|
|
74
|
+
return best;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The on-scale length a stray spacing value most resembles. Same-unit
|
|
79
|
+
* comparison only (12px is not "near" 0.75rem here; unit conversion is a
|
|
80
|
+
* judgement the guard should not fake). Returns { value, distance } or null.
|
|
81
|
+
*/
|
|
82
|
+
export function nearestLength(value, scaleValues) {
|
|
83
|
+
const parse = (v) => {
|
|
84
|
+
const m = /^(-?\d*\.?\d+)(px|rem|em|%|vh|vw|pt)$/.exec(String(v).trim());
|
|
85
|
+
return m ? { n: parseFloat(m[1]), unit: m[2] } : null;
|
|
86
|
+
};
|
|
87
|
+
const target = parse(value);
|
|
88
|
+
if (!target) return null;
|
|
89
|
+
let best = null;
|
|
90
|
+
for (const s of scaleValues) {
|
|
91
|
+
const p = parse(s);
|
|
92
|
+
if (!p || p.unit !== target.unit) continue;
|
|
93
|
+
const d = Math.abs(p.n - target.n);
|
|
94
|
+
if (d === 0) continue;
|
|
95
|
+
if (!best || d < best.distance) best = { value: s, distance: d };
|
|
96
|
+
}
|
|
97
|
+
return best;
|
|
98
|
+
}
|