roast-my-design-system 9.3.0 → 9.3.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/package.json +1 -1
- package/skills/roast-my-design-system/scripts/harvest/paint.mjs +6 -1
- package/skills/roast-my-design-system/scripts/harvest/tokens.mjs +3 -0
- package/skills/roast-my-design-system/scripts/harvest/walk.mjs +11 -7
- package/skills/roast-my-design-system/scripts/lib/exempt.mjs +68 -0
- package/skills/roast-my-design-system/scripts/lib/guard-api.mjs +5 -2
- package/skills/roast-my-design-system/scripts/lib/version.mjs +1 -1
- package/skills/roast-my-design-system/scripts/mcp/engine.mjs +5 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roast-my-design-system",
|
|
3
|
-
"version": "9.3.
|
|
3
|
+
"version": "9.3.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 112 public repos, reads React and web components (Stencil, Lit), hands you a copy-paste fix prompt for each top finding, writes rules for Claude, Cursor, Copilot and Windsurf with --apply, and runs as a local MCP server with --mcp.",
|
|
6
6
|
"keywords": [
|
|
@@ -50,7 +50,12 @@ const DOOR_TYPO_RE = /(?<![\w-])(?:[\w-]+:)*(?:font-(?:thin|extralight|light|nor
|
|
|
50
50
|
export const blankComments = (s) => s
|
|
51
51
|
.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '))
|
|
52
52
|
.replace(/(^|[^:'"`\\])(\/\/[^\n]*)/g, (m, pre, c) => pre + ' '.repeat(c.length));
|
|
53
|
-
|
|
53
|
+
// Not templates: in a product a templates folder is usually a screen
|
|
54
|
+
// (teable's admin templates, documenso's and formbricks' template pickers),
|
|
55
|
+
// and hiding it hid their colours from the report and the live checks. A
|
|
56
|
+
// project generator's templates are left out by the walk instead, and email
|
|
57
|
+
// templates by the email rule (2026-09-30).
|
|
58
|
+
export const DEMO_PATH_RE = /(^|\/)(stories|storybook|__stories__|examples?|demos?|playground|fixtures?|__tests__|__mocks__|e2e|cypress)\//i;
|
|
54
59
|
|
|
55
60
|
/**
|
|
56
61
|
* @param root repo root
|
|
@@ -573,6 +573,9 @@ export function harvestTokens(root, styleFiles, codeFiles, { email = null } = {}
|
|
|
573
573
|
// triggered the "every single one is hardcoded" banner on it.
|
|
574
574
|
const renderToImage = RENDER_TO_IMAGE_RE.test(src) || OG_ROUTE_RE.test(f);
|
|
575
575
|
if (renderToImage) { skip(f, src); continue; }
|
|
576
|
+
// a page a headless browser prints to a PDF (rybbit's reports), found by
|
|
577
|
+
// the walk from the file that prints it (lib/exempt.mjs printedFilesOf)
|
|
578
|
+
if (email?.printed?.includes(f)) { skip(f, src); continue; }
|
|
576
579
|
// an email written with an email kit, wherever it lives (react-email)
|
|
577
580
|
if (EMAIL_KIT_RE.test(src)) { skip(f, src); continue; }
|
|
578
581
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* stylesheets of any flavor, and config — not just a happy-path shadcn layout.
|
|
5
5
|
*/
|
|
6
6
|
import { CATALOGUE } from '../profiles/shadcn-data.mjs';
|
|
7
|
-
import { emailContextOf } from '../lib/exempt.mjs';
|
|
7
|
+
import { emailContextOf, printedFilesOf } from '../lib/exempt.mjs';
|
|
8
8
|
import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
|
|
9
9
|
import { join, relative, extname } from 'node:path';
|
|
10
10
|
|
|
@@ -80,12 +80,13 @@ const LEGACY_DIR_RE = /^(?:old|legacy|deprecated|archive|archived)$|[-_](?:old|l
|
|
|
80
80
|
|
|
81
81
|
// Scaffolding: the code a project generator copies into someone else's new
|
|
82
82
|
// repo, not the product. A starters/ folder, or a templates folder kept by a
|
|
83
|
-
// generator (create-app, a cli
|
|
84
|
-
// create-app/templates, the dashboard starter's
|
|
85
|
-
// BuilderIO's starters (133 duplicate copies)
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
|
|
83
|
+
// generator (create-app, a cli, a command-line tool's commands), a script or
|
|
84
|
+
// a dev tool: backstage's create-app/templates, the dashboard starter's
|
|
85
|
+
// scripts/cleanup-templates, BuilderIO's starters (133 duplicate copies),
|
|
86
|
+
// novu's commands/init/templates. The name alone is not enough: backstage's
|
|
87
|
+
// scaffolder form templates, OpenCTI's case templates and nocobase's block
|
|
88
|
+
// templates are screens of the product (2026-09-30).
|
|
89
|
+
const SCAFFOLD_DIR_RE = /(^|\/)starters$|(^|\/)(?:create-[\w-]+|cli|commands|scripts|dev|generators?)\/(?:[^/]+\/)*[\w-]*templates?$/i;
|
|
89
90
|
|
|
90
91
|
export function walkRepo(root, maxDepth = 14, exclusions = null, readAnyway = new Set()) {
|
|
91
92
|
const files = { code: [], styles: [], other: [] };
|
|
@@ -182,6 +183,9 @@ export function walkRepo(root, maxDepth = 14, exclusions = null, readAnyway = ne
|
|
|
182
183
|
// where the repo keeps its emails, read once here so every checker that
|
|
183
184
|
// is handed the walk judges an email the same way (lib/exempt.mjs)
|
|
184
185
|
files.email = emailContextOf(files, (f) => readSource(join(root, f)));
|
|
186
|
+
// and the files a headless browser prints to a PDF, which ride in the same
|
|
187
|
+
// context so every checker that honours the email rule honours this too
|
|
188
|
+
files.email.printed = printedFilesOf(files, (f) => readSource(join(root, f)));
|
|
185
189
|
return files;
|
|
186
190
|
}
|
|
187
191
|
|
|
@@ -116,6 +116,73 @@ export const ARTWORK_DIR_RE = /(^|\/)(icons?|logos?|illustrations?)\//i;
|
|
|
116
116
|
// medium: the tool reads the styling off the element (2026-09-17).
|
|
117
117
|
export const RENDER_TO_IMAGE_RE = /ImageResponse|from ['"]satori['"]|from ['"]@react-pdf|next\/og|from ['"](?:html-to-image|html2canvas(?:-pro)?|dom-to-image(?:-more)?|modern-screenshot)['"]/;
|
|
118
118
|
export const OG_ROUTE_RE = /(^|\/)api\/og\//;
|
|
119
|
+
|
|
120
|
+
// Printed by a headless browser: React turned into HTML (renderToStaticMarkup)
|
|
121
|
+
// and handed to puppeteer or playwright for a PDF or a screenshot. The page
|
|
122
|
+
// loads none of the app's stylesheets, so its styling is written inline, like
|
|
123
|
+
// an email's. The templates carry no marker of their own; the file that
|
|
124
|
+
// prints them does (rybbit's PDF reports: 31 of its 52 inline styles, score
|
|
125
|
+
// 40 against 50 without them). Found in 1 of 205 repos (2026-09-30), and
|
|
126
|
+
// nothing else has that shape: a file that renders React to HTML and drives
|
|
127
|
+
// a headless browser is always making a picture or a PDF.
|
|
128
|
+
const HEADLESS_DEP_RE = /^(?:puppeteer(?:-core)?|playwright(?:-core)?|@sparticuz\/chromium|chrome-aws-lambda)$/;
|
|
129
|
+
const HEADLESS_IMPORT_RE = /(?:from\s+|require\(\s*|import\(\s*)['"](?:puppeteer(?:-core)?|playwright(?:-core)?|@sparticuz\/chromium|chrome-aws-lambda)['"]/;
|
|
130
|
+
const SSR_RENDER_RE = /\brenderTo(?:StaticMarkup|String|StaticNodeStream)\s*\(/;
|
|
131
|
+
const REL_IMPORT_RE = /(?:from\s+|import\(\s*)['"](\.{1,2}\/[^'"]+)['"]/g;
|
|
132
|
+
|
|
133
|
+
/** A relative import, resolved against the walk's files, or null. */
|
|
134
|
+
function resolveRelative(fromDir, spec, code) {
|
|
135
|
+
const parts = fromDir ? fromDir.split('/') : [];
|
|
136
|
+
for (const s of spec.split('/')) {
|
|
137
|
+
if (s === '.' || s === '') continue;
|
|
138
|
+
if (s === '..') parts.pop(); else parts.push(s);
|
|
139
|
+
}
|
|
140
|
+
const p = parts.join('/');
|
|
141
|
+
const stem = p.replace(/\.[cm]?[jt]sx?$/, '');
|
|
142
|
+
const exts = ['.tsx', '.ts', '.jsx', '.js'];
|
|
143
|
+
for (const c of [p, ...exts.map((e) => stem + e), ...exts.map((e) => `${stem}/index${e}`)]) if (code.has(c)) return c;
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The component files a headless browser prints, for the exemption context.
|
|
149
|
+
* Only packages with a headless browser among their runtime dependencies are
|
|
150
|
+
* read (a test setup lists it as a devDependency), and only the printer's own
|
|
151
|
+
* folder is followed: a shared Button it imports from elsewhere stays judged.
|
|
152
|
+
* @param files the walk's { code, other } (repo-relative paths)
|
|
153
|
+
* @param read (file) => text or null
|
|
154
|
+
*/
|
|
155
|
+
export function printedFilesOf(files, read) {
|
|
156
|
+
const homes = [];
|
|
157
|
+
for (const f of (files.other ?? []).filter((x) => base(x) === 'package.json')) {
|
|
158
|
+
let pkg; try { pkg = JSON.parse(read(f) ?? ''); } catch { continue; }
|
|
159
|
+
if (Object.keys(pkg?.dependencies ?? {}).some((d) => HEADLESS_DEP_RE.test(d))) homes.push(dir(f));
|
|
160
|
+
}
|
|
161
|
+
if (!homes.length) return [];
|
|
162
|
+
const code = new Set(files.code ?? []);
|
|
163
|
+
const under = (f, h) => h === '' || f.startsWith(`${h}/`);
|
|
164
|
+
const printed = new Set();
|
|
165
|
+
for (const f of files.code ?? []) {
|
|
166
|
+
if (!/\.[cm]?[jt]sx?$/.test(f) || !homes.some((h) => under(f, h))) continue;
|
|
167
|
+
const text = read(f) ?? '';
|
|
168
|
+
if (!HEADLESS_IMPORT_RE.test(text) || !SSR_RENDER_RE.test(text)) continue;
|
|
169
|
+
if (/\.(tsx|jsx)$/.test(f)) printed.add(f);
|
|
170
|
+
const home = dir(f);
|
|
171
|
+
const seen = new Set([f]);
|
|
172
|
+
const queue = [[f, text]];
|
|
173
|
+
while (queue.length) {
|
|
174
|
+
const [from, src] = queue.shift();
|
|
175
|
+
for (const m of src.matchAll(REL_IMPORT_RE)) {
|
|
176
|
+
const target = resolveRelative(dir(from), m[1], code);
|
|
177
|
+
if (!target || seen.has(target) || !under(target, home)) continue;
|
|
178
|
+
seen.add(target);
|
|
179
|
+
if (/\.(tsx|jsx)$/.test(target)) printed.add(target);
|
|
180
|
+
queue.push([target, read(target) ?? '']);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return [...printed].sort();
|
|
185
|
+
}
|
|
119
186
|
export const RENDERER_PATH_RE = /(^|\/)(renderers?|scene|canvas)\/|renderElement|DebugCanvas/i;
|
|
120
187
|
|
|
121
188
|
/** Mostly drawing rather than styling, whatever the file is called. */
|
|
@@ -144,6 +211,7 @@ export function exemptReason(file, text = '', { email = null } = {}) {
|
|
|
144
211
|
if (CRASH_PAGE_RE.test(file)) return 'the crash page replaces the root layout, so the stylesheet never loads there and its styling has to be inline';
|
|
145
212
|
if (RENDERER_PATH_RE.test(file)) return 'a renderer draws pixels, so its colours are the picture rather than the interface';
|
|
146
213
|
if (OG_ROUTE_RE.test(file) || RENDER_TO_IMAGE_RE.test(text)) return 'a render-to-image surface accepts nothing but inline styling, so there is no on-system way to write one';
|
|
214
|
+
if (email?.printed?.includes(file)) return 'pages a headless browser prints to a PDF or an image load none of the app\'s stylesheets, so their styling has to be inline';
|
|
147
215
|
if (svgHeavy(text)) return 'the file is mostly drawing rather than styling';
|
|
148
216
|
if ((ARTWORK_NAME_RE.test(file) || ARTWORK_DIR_RE.test(file)) && SVG_MARKUP_RE.test(text)) return 'the colours belong to the artwork, not to the interface';
|
|
149
217
|
return null;
|
|
@@ -17,7 +17,7 @@ export { designGaps } from './gaps.mjs';
|
|
|
17
17
|
import { decideProfile, profileOf, installedDirs } from '../profiles/index.mjs';
|
|
18
18
|
import { KITS } from '../profiles/kit-common.mjs';
|
|
19
19
|
import { kitPaintFindings } from './kitpaint.mjs';
|
|
20
|
-
import { PALETTE_CLASS_RE, blankComments } from '../harvest/paint.mjs';
|
|
20
|
+
import { PALETTE_CLASS_RE, DEMO_PATH_RE, blankComments } from '../harvest/paint.mjs';
|
|
21
21
|
import { harvestTokens, extractStyling, normalizeHex, isGrey } from '../harvest/tokens.mjs';
|
|
22
22
|
import { harvestComponents, definedComponents, tsconfigAliases } from '../harvest/components.mjs';
|
|
23
23
|
import { findDuplicates } from '../harvest/duplicates.mjs';
|
|
@@ -63,7 +63,10 @@ export { WIDGET_CSS_RE, WIDGET_CONFIG_RE, LIBRARY_CLASS_RE, isLibraryClass } fro
|
|
|
63
63
|
// blankComments (8.4.5) is what the report and the live checks run on a file
|
|
64
64
|
// before matching, so a class named in a comment paints nothing; a guard that
|
|
65
65
|
// matches the raw text counts it, and disagrees with the report.
|
|
66
|
-
|
|
66
|
+
// DEMO_PATH_RE (9.3.2) is the folders the palette count leaves out, stories,
|
|
67
|
+
// examples and demos: a guard that skips them flags palette classes in the
|
|
68
|
+
// same files the report counts.
|
|
69
|
+
export { PALETTE_CLASS_RE, DEMO_PATH_RE, blankComments };
|
|
67
70
|
// A product built on a kit (MUI, Mantine, Chakra UI, Ant Design), 8.4.6: a
|
|
68
71
|
// colour or a pixel size written onto a kit component where the theme has a
|
|
69
72
|
// value. kitPaintFindings(fileText, system.profile.kit, { file }) returns the
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Single version constant for the engine — imported by diagnose (report
|
|
2
2
|
// footer) and rules (generated-by line). This is the bump spot that used to
|
|
3
3
|
// live as a const inside diagnose/index.mjs.
|
|
4
|
-
export const VERSION = '9.3.
|
|
4
|
+
export const VERSION = '9.3.2';
|
|
5
5
|
// The shape of harvest.json and summary.json. Bumped only when a field is
|
|
6
6
|
// renamed, removed or changes meaning; a new field is not a new schema. A
|
|
7
7
|
// tool comparing two scans compares like with like by this number, not by VERSION.
|
|
@@ -188,8 +188,11 @@ export function validateContent(content, k) {
|
|
|
188
188
|
// (editing them is the intended use) and a demo folder is not own code,
|
|
189
189
|
// exactly the files the tile leaves out.
|
|
190
190
|
const inShadcnDoors = (f) => !!f && k.shadcn.doors.some((d) => f === d || f.startsWith(`${d}/`));
|
|
191
|
-
|
|
192
|
-
|
|
191
|
+
// A demo folder is left out on a Tailwind theme too: the report's tile never
|
|
192
|
+
// counted one, and the live check flagged it (2026-09-30).
|
|
193
|
+
const demo = !!file && DEMO_PATH_RE.test(file);
|
|
194
|
+
const paletteRule = k.tailwind ? (demo ? null : 'tailwind')
|
|
195
|
+
: k.shadcn && !(file && (inShadcnDoors(file) || demo)) ? 'shadcn'
|
|
193
196
|
: null;
|
|
194
197
|
if (paletteRule && !css) {
|
|
195
198
|
const tw = k.tailwind ?? {};
|