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.
- package/README.md +8 -3
- package/package.json +2 -2
- 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.
|
|
243
|
-
|
|
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.
|
|
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.
|
|
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
|
|
185
|
-
//
|
|
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 (
|
|
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
|
-
|
|
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({
|