guard-my-design-system 2.1.0 → 2.2.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.
Files changed (3) hide show
  1. package/README.md +8 -3
  2. package/package.json +2 -2
  3. package/src/judge.mjs +51 -29
package/README.md CHANGED
@@ -62,7 +62,11 @@ it updates that same comment. It never adds more comments:
62
62
  gives the import line. Rows, tabs, close crosses and select triggers
63
63
  built on a button tag are left alone.
64
64
  - **A second definition of a component you already have.** The finding names
65
- the file that already defines it, and how many places use that one.
65
+ the file that already defines it, and how many places use that one. Web
66
+ components registered by tag count too. What a copy is, is the roast
67
+ report's answer: a framework's `Route` or `Layout`, a page, a story, an
68
+ email template and a wrapper built on the component it shares a name with
69
+ are not second copies.
66
70
  - **A new import of a duplicate component.** When a name is defined in more
67
71
  than one file and one copy is clearly the main one, importing another copy
68
72
  is flagged. The finding names the main copy, how often each is used, and
@@ -239,8 +243,9 @@ updating PR comment works on GitHub only, for now.
239
243
  picture drawn with code. A canvas renderer draws pixels. A file that draws
240
244
  SVG is artwork, not interface. The guard reads that list from the roast
241
245
  engine rather than keeping its own, so the two can never drift apart and
242
- give you different answers about the same file. Defining a new token is
243
- extending the system, not a problem.
246
+ give you different answers about the same file. The exemptions are about
247
+ styling: a second `Logo` is still a second `Logo`, and is flagged. Defining
248
+ a new token is extending the system, not a problem.
244
249
  - **Every finding comes with a fix.** The guard names the on-system value the
245
250
  author probably meant, so most fixes take under a minute and no meeting.
246
251
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guard-my-design-system",
3
- "version": "2.1.0",
3
+ "version": "2.2.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": "9.2.0"
14
+ "roast-my-design-system": "9.2.1"
15
15
  },
16
16
  "keywords": [
17
17
  "design-system",
package/src/judge.mjs CHANGED
@@ -10,7 +10,7 @@
10
10
  import {
11
11
  extractStyling, normalizeHex, nearestColor, nearestLength,
12
12
  isCodeFile, isStyleFile, typefaceOf, GENERIC_FONTS,
13
- definedComponents, exemptReason,
13
+ definedComponents, componentNamesIn, duplicateCopies, isPageFile, exemptReason,
14
14
  EXTRA_KINDS, extraValue, fontDeclarations,
15
15
  WIDGET_CSS_RE, isLibraryClass, PALETTE_CLASS_RE, blankComments, kitPaintFindings,
16
16
  tokenTwinFindings, avoidedImportFindings, isChartFile, chartFindings, handmadeButtonFindings,
@@ -181,12 +181,13 @@ export function judge(added, system, { readFile, readBase } = {}) {
181
181
  const knownFaces = new Set(
182
182
  [...faceCounts].filter(([face, n]) => n > (addedFaces.get(face) ?? 0)).map(([face]) => face)
183
183
  );
184
- // Components the repo already defines, by name. Pages are routes rather than
185
- // reusable parts, so two of a name there is not a second Button.
184
+ // Components the repo already defines, by name: the whole ledger. What
185
+ // counts as a second copy is the engine's call (duplicateCopies), so the
186
+ // guard and the report give one answer. A page, a framework's Route, a
187
+ // story, a wrapper and two icon libraries colliding are not second copies.
186
188
  const componentsByName = new Map();
187
189
  if (definedComponents && Array.isArray(system.components)) {
188
190
  for (const c of system.components) {
189
- if (c.isPage) continue;
190
191
  const list = componentsByName.get(c.name) ?? [];
191
192
  list.push(c);
192
193
  componentsByName.set(c.name, list);
@@ -236,10 +237,54 @@ export function judge(added, system, { readFile, readBase } = {}) {
236
237
  return isChartFile(file, w ?? '');
237
238
  };
238
239
 
240
+ // A hand-rolled second <Button> is the most expensive thing a pull request
241
+ // can add, and it was the one thing the guard could not see. The scan
242
+ // includes this change, so the new copy is in the ledger too: what counts
243
+ // is whether the name lives anywhere ELSE, and the engine answers that the
244
+ // way the report does (duplicateCopies).
245
+ const definedByFile = new Map();
246
+ const definedIn = (file) => {
247
+ if (!definedByFile.has(file)) definedByFile.set(file, new Set(componentNamesIn(wholeText(file) ?? '', file)));
248
+ return definedByFile.get(file);
249
+ };
250
+ const secondCopies = (file, line, text) => {
251
+ const out = [];
252
+ if (!componentsByName.size) return out;
253
+ // what the whole file defines for the report, kept to what this line declares
254
+ const onLine = new Set(definedComponents(text));
255
+ const names = wholeText(file) === null
256
+ ? [...onLine]
257
+ : [...definedIn(file)].filter((n) => onLine.has(n) || new RegExp(`\\bclass\\s+${n}\\b`).test(text));
258
+ for (const name of names) {
259
+ const variantOf = (f) => variants.find((v) => underAny(f, [v])) ?? null;
260
+ const counted = system.duplicates?.get?.(name)?.copies.map((c) => c.file) ?? null;
261
+ const elsewhere = duplicateCopies({ name, file, isPage: isPageFile(file) }, componentsByName.get(name), counted, samePath)
262
+ // a registry keeps the same component in sibling variants, and a
263
+ // block installs alone: neither is a second Button
264
+ .filter((c) => !(variantOf(file) && variantOf(c.file) && variantOf(c.file) !== variantOf(file)))
265
+ .filter((c) => !(underAny(file, blockDirs) && underAny(c.file, blockDirs)));
266
+ if (!elsewhere.length) continue;
267
+ const best = [...elsewhere].sort((a, b) => b.usageCount - a.usageCount)[0];
268
+ out.push({
269
+ file, line, kind: 'component', value: name,
270
+ // never open the advice with the path: the report capitalises the
271
+ // first letter, and a capitalised path is the wrong path
272
+ advice: elsewhere.length > 1
273
+ ? `${elsewhere.length} other files define it too; import ${best.file}, the one the codebase leans on`
274
+ : `import ${best.file} rather than starting a second one${best.usageCount ? `, which ${best.usageCount} place${best.usageCount === 1 ? '' : 's'} already do` : ''}`,
275
+ });
276
+ }
277
+ return out;
278
+ };
279
+
239
280
  for (const { file, line, text } of added) {
240
- if (exempt(file) || outOfScope(file)) continue;
281
+ if (outOfScope(file)) continue;
241
282
  const css = isStyleFile(file);
242
283
  if (!css && !isCodeFile(file)) continue;
284
+ // The exemptions are about styling: what an email, a drawing or a crash
285
+ // page cannot take from the system. A second copy of a component is a
286
+ // second copy in any medium, and the report counts it.
287
+ if (exempt(file)) { if (!css) findings.push(...secondCopies(file, line, text)); continue; }
243
288
 
244
289
  const seen = extractStyling(text, { css });
245
290
 
@@ -325,30 +370,7 @@ export function judge(added, system, { readFile, readBase } = {}) {
325
370
  });
326
371
  }
327
372
 
328
- // A hand-rolled second <Button> is the most expensive thing a pull request
329
- // can add, and it was the one thing the guard could not see. The scan
330
- // includes this change, so the new copy is in the ledger too: what counts
331
- // is whether the name lives anywhere ELSE.
332
- if (!css && componentsByName.size) {
333
- for (const name of definedComponents(text)) {
334
- const variantOf = (f) => variants.find((v) => underAny(f, [v])) ?? null;
335
- const elsewhere = (componentsByName.get(name) ?? []).filter((c) => !samePath(c.file, file))
336
- // a registry keeps the same component in sibling variants, and a
337
- // block installs alone: neither is a second Button
338
- .filter((c) => !(variantOf(file) && variantOf(c.file) && variantOf(c.file) !== variantOf(file)))
339
- .filter((c) => !(underAny(file, blockDirs) && underAny(c.file, blockDirs)));
340
- if (!elsewhere.length) continue;
341
- const best = [...elsewhere].sort((a, b) => b.usageCount - a.usageCount)[0];
342
- findings.push({
343
- file, line, kind: 'component', value: name,
344
- // never open the advice with the path: the report capitalises the
345
- // first letter, and a capitalised path is the wrong path
346
- advice: elsewhere.length > 1
347
- ? `${elsewhere.length} other files define it too; import ${best.file}, the one the codebase leans on`
348
- : `import ${best.file} rather than starting a second one${best.usageCount ? `, which ${best.usageCount} place${best.usageCount === 1 ? '' : 's'} already do` : ''}`,
349
- });
350
- }
351
- }
373
+ if (!css) findings.push(...secondCopies(file, line, text));
352
374
 
353
375
  for (const a of seen.arbitrary) {
354
376
  findings.push({