guard-my-design-system 1.8.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 CHANGED
@@ -56,6 +56,17 @@ 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.
package/index.mjs CHANGED
@@ -81,7 +81,14 @@ const readWhole = (file) => {
81
81
  }
82
82
  return wholeFile.get(file);
83
83
  };
84
- let findings = judge(judged, system, { readFile: readWhole });
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.8.0",
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.4.6"
14
+ "roast-my-design-system": "8.6.1"
15
15
  },
16
16
  "keywords": [
17
17
  "design-system",
package/src/judge.mjs CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  definedComponents, exemptReason,
14
14
  EXTRA_KINDS, extraValue, fontDeclarations,
15
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
@@ -363,6 +368,44 @@ export function judge(added, system, { readFile } = {}) {
363
368
  }
364
369
  }
365
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
+
366
409
  // Two extractors can see the same value on the same line (a hex inside a
367
410
  // Tailwind class is also a hex in the raw sweep). One sin, one line.
368
411
  const seen = new Set();
package/src/report.mjs CHANGED
@@ -19,11 +19,14 @@ const KIND_LABEL = {
19
19
  // on a kit repo the engine words the label with the kit's name (f.label)
20
20
  'kit-colour': 'colour written onto a kit component',
21
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',
22
25
  };
23
26
  const labelOf = (f) => f.label ?? KIND_LABEL[f.kind];
24
27
 
25
28
  // Kinds whose label already says everything; printing the value repeats it.
26
- const VALUELESS = new Set(['important', 'inline']);
29
+ const VALUELESS = new Set(['important', 'inline', 'twin-token', 'avoided-copy']);
27
30
 
28
31
  const FOOTER = 'Full picture of the whole codebase: `npx roast-my-design-system`';
29
32