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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roast-my-design-system",
3
- "version": "9.3.0",
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
- export const DEMO_PATH_RE = /(^|\/)(stories|storybook|__stories__|examples?|demos?|templates?|playground|fixtures?|__tests__|__mocks__|e2e|cypress)\//i;
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), a script or a dev tool: backstage's
84
- // create-app/templates, the dashboard starter's scripts/cleanup-templates,
85
- // BuilderIO's starters (133 duplicate copies). The name alone is not enough:
86
- // backstage's scaffolder form templates, OpenCTI's case templates and
87
- // nocobase's block templates are screens of the product (2026-09-30).
88
- const SCAFFOLD_DIR_RE = /(^|\/)starters$|(^|\/)(?:create-[\w-]+|cli|scripts|dev|generators?)\/(?:[^/]+\/)*[\w-]*templates?$/i;
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
- export { PALETTE_CLASS_RE, blankComments };
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.0';
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
- const paletteRule = k.tailwind ? 'tailwind'
192
- : k.shadcn && !(file && (inShadcnDoors(file) || DEMO_PATH_RE.test(file))) ? 'shadcn'
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 ?? {};