synthesisui 0.16.47 → 0.16.50
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/dist/commands/doctor.js +43 -0
- package/dist/commands/import.js +27 -53
- package/dist/doctor/components-scan.js +66 -22
- package/dist/doctor/contract-check.js +94 -0
- package/dist/doctor/crosswalk.js +32 -0
- package/package.json +1 -1
package/dist/commands/doctor.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { readdir, readFile } from "node:fs/promises";
|
|
2
2
|
import { join, relative, resolve } from "node:path";
|
|
3
3
|
import { findDivergences } from "../doctor/coherence.js";
|
|
4
|
+
import { emptyTally, internalSpecifiers, scanComponentsInto, tallyToInventory, } from "../doctor/components-scan.js";
|
|
5
|
+
import { checkContracts } from "../doctor/contract-check.js";
|
|
4
6
|
import { findFrozenBindings } from "../doctor/frozen.js";
|
|
5
7
|
import { appendEvent, readEvents, summarize } from "../doctor/ledger.js";
|
|
6
8
|
import { bindingsFromDocument, countComponents, findOverrides, } from "../doctor/overrides.js";
|
|
@@ -29,6 +31,8 @@ import { body, paint, section, snippet } from "../output.js";
|
|
|
29
31
|
* Runs offline, needs no account, and touches nothing. A tool that asks for a
|
|
30
32
|
* signup before it tells you anything is a tool nobody runs twice.
|
|
31
33
|
*/
|
|
34
|
+
/** Files that compose components to SHOW them, not to ship them. */
|
|
35
|
+
const IS_ASIDE = /(\.(spec|test|stories)\.[a-z]+$|__tests__\/|(^|\/)\.storybook\/)/;
|
|
32
36
|
const EXTS = [".tsx", ".ts", ".jsx", ".js", ".css", ".scss", ".vue", ".svelte"];
|
|
33
37
|
const SKIP = new Set([
|
|
34
38
|
"node_modules",
|
|
@@ -404,6 +408,8 @@ export async function doctor(opts) {
|
|
|
404
408
|
*/
|
|
405
409
|
const wiring = await readWiring(root, table.slug);
|
|
406
410
|
const skippedProjects = [];
|
|
411
|
+
const tally = emptyTally();
|
|
412
|
+
const internalSpecs = await internalSpecifiers(root);
|
|
407
413
|
for await (const file of scopes.length > 0
|
|
408
414
|
? walkAll(scopes)
|
|
409
415
|
: walk(root, skippedProjects)) {
|
|
@@ -412,6 +418,13 @@ export async function doctor(opts) {
|
|
|
412
418
|
continue;
|
|
413
419
|
const rel = relative(root, file);
|
|
414
420
|
reports.push(scanSource(rel, src, table));
|
|
421
|
+
// Composition, alongside values: the contract check needs to know which
|
|
422
|
+
// elements this file writes and with what options. Stories and tests are
|
|
423
|
+
// excluded for the reason they are excluded everywhere else - they compose
|
|
424
|
+
// components to SHOW them.
|
|
425
|
+
if (documents.length > 0 && !IS_ASIDE.test(rel)) {
|
|
426
|
+
scanComponentsInto(tally, rel, src, internalSpecs);
|
|
427
|
+
}
|
|
415
428
|
if (recipes.size > 0) {
|
|
416
429
|
for (const o of findOverrides(src, recipes))
|
|
417
430
|
overrides.push({ ...o, file: rel });
|
|
@@ -433,6 +446,7 @@ export async function doctor(opts) {
|
|
|
433
446
|
for (const r of reports) {
|
|
434
447
|
r.findings = r.findings.filter((f) => !explained.has(`${r.file}:${f.line}:${f.literal.toLowerCase()}`));
|
|
435
448
|
}
|
|
449
|
+
const breaches = checkContracts(tallyToInventory(tally, 400), documents);
|
|
436
450
|
const d = diagnose(reports);
|
|
437
451
|
// Nine releases in one evening added a section each, every one justified on
|
|
438
452
|
// its own, and nobody read the whole. The result was 151 lines carrying about
|
|
@@ -1081,6 +1095,35 @@ export async function doctor(opts) {
|
|
|
1081
1095
|
: "")));
|
|
1082
1096
|
}
|
|
1083
1097
|
}
|
|
1098
|
+
/**
|
|
1099
|
+
* A CONTRACT BROKEN, which is the first thing this command has ever said
|
|
1100
|
+
* about composition rather than about values.
|
|
1101
|
+
*
|
|
1102
|
+
* A colour outside the palette has always been drift. This is the same
|
|
1103
|
+
* sentence about a component: `<WidgetCard variant="danger">` on a
|
|
1104
|
+
* component whose contract offers `neutral` and `ocean`. No linter and no
|
|
1105
|
+
* type can say it, because the answer lives in the design system.
|
|
1106
|
+
*/
|
|
1107
|
+
if (breaches.length > 0) {
|
|
1108
|
+
// Grouped by element AND axis: grouping by element alone printed
|
|
1109
|
+
// `variant="danger" | "huge"` and then offered the options for variant,
|
|
1110
|
+
// when `huge` was a size. One line per axis or the line lies.
|
|
1111
|
+
const byAxis = new Map();
|
|
1112
|
+
for (const b of breaches) {
|
|
1113
|
+
const k = `${b.element}\u0000${b.axis}`;
|
|
1114
|
+
byAxis.set(k, [...(byAxis.get(k) ?? []), b]);
|
|
1115
|
+
}
|
|
1116
|
+
console.log("");
|
|
1117
|
+
console.log(body(`${paint.strong(String(breaches.length))} use${breaches.length === 1 ? "" : "s"} outside the contract`));
|
|
1118
|
+
for (const list of [...byAxis.values()].slice(0, 5)) {
|
|
1119
|
+
const first = list[0];
|
|
1120
|
+
const values = [...new Set(list.map((b) => b.used))];
|
|
1121
|
+
console.log(body(` <${first.element} ${first.axis}="${values.join('" | "')}"> ${paint.faint(`- your ds-${first.recipe} offers ${first.offered.join(", ")}`)}`));
|
|
1122
|
+
}
|
|
1123
|
+
if (byAxis.size > 5) {
|
|
1124
|
+
console.log(body(paint.faint(` (${byAxis.size - 5} more)`)));
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1084
1127
|
if (plan.length > 0) {
|
|
1085
1128
|
console.log("");
|
|
1086
1129
|
console.log(body("Where to start"));
|
package/dist/commands/import.js
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { basename, join, relative } from "node:path";
|
|
3
3
|
import { readToken, resolveRegistry } from "../config.js";
|
|
4
4
|
import { isNearDuplicate } from "../doctor/color-distance.js";
|
|
5
|
-
import { emptyTally, scanComponentsInto, tallyToInventory, } from "../doctor/components-scan.js";
|
|
5
|
+
import { emptyTally, internalSpecifiers, scanComponentsInto, tallyToInventory, } from "../doctor/components-scan.js";
|
|
6
6
|
import { crosswalk } from "../doctor/crosswalk.js";
|
|
7
7
|
import { diagnose, scanSource } from "../doctor/scan.js";
|
|
8
8
|
import { parseSchemeBlocks } from "../doctor/scheme-blocks.js";
|
|
@@ -126,57 +126,6 @@ async function resolveDeps(root) {
|
|
|
126
126
|
}
|
|
127
127
|
return deps;
|
|
128
128
|
}
|
|
129
|
-
/**
|
|
130
|
-
* Specifier prefixes this workspace owns: the aliases its tsconfig declares and
|
|
131
|
-
* the names of its sibling packages. Everything else that is not relative comes
|
|
132
|
-
* from node_modules.
|
|
133
|
-
*/
|
|
134
|
-
async function internalSpecifiers(root) {
|
|
135
|
-
const out = new Set();
|
|
136
|
-
for (let up = 0, dir = root; up < 4; up++) {
|
|
137
|
-
for (const f of ["tsconfig.json", "tsconfig.base.json"]) {
|
|
138
|
-
const raw = await readFile(join(dir, f), "utf8").catch(() => null);
|
|
139
|
-
if (!raw)
|
|
140
|
-
continue;
|
|
141
|
-
try {
|
|
142
|
-
// tsconfig allows comments and trailing commas; a census must not die
|
|
143
|
-
// on either, and the keys are all we want.
|
|
144
|
-
for (const m of raw.matchAll(/"([^"]+)\/?\*?"\s*:\s*\[/g)) {
|
|
145
|
-
const key = m[1].replace(/\/\*$/, "").replace(/\*$/, "");
|
|
146
|
-
if (key && !key.startsWith("."))
|
|
147
|
-
out.add(key.replace(/\/$/, ""));
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
catch {
|
|
151
|
-
// unreadable config costs the aliases, not the run
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
for (const group of ["packages", "libs", "apps"]) {
|
|
155
|
-
for (const e of await readdir(join(dir, group), {
|
|
156
|
-
withFileTypes: true,
|
|
157
|
-
}).catch(() => [])) {
|
|
158
|
-
if (!e.isDirectory())
|
|
159
|
-
continue;
|
|
160
|
-
const raw = await readFile(join(dir, group, e.name, "package.json"), "utf8").catch(() => null);
|
|
161
|
-
if (!raw)
|
|
162
|
-
continue;
|
|
163
|
-
try {
|
|
164
|
-
const name = JSON.parse(raw).name;
|
|
165
|
-
if (typeof name === "string" && name)
|
|
166
|
-
out.add(name);
|
|
167
|
-
}
|
|
168
|
-
catch {
|
|
169
|
-
// same
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
const parent = join(dir, "..");
|
|
174
|
-
if (parent === dir)
|
|
175
|
-
break;
|
|
176
|
-
dir = parent;
|
|
177
|
-
}
|
|
178
|
-
return [...out];
|
|
179
|
-
}
|
|
180
129
|
async function detectStack(root) {
|
|
181
130
|
const stack = [];
|
|
182
131
|
const has = async (f) => (await readFile(join(root, f), "utf8").catch(() => null)) !== null;
|
|
@@ -336,7 +285,18 @@ export async function takeCensus(root) {
|
|
|
336
285
|
}
|
|
337
286
|
}
|
|
338
287
|
const d = diagnose(reports);
|
|
339
|
-
const
|
|
288
|
+
const inventory = tallyToInventory(tally);
|
|
289
|
+
// The verdict travels WITH the payload: the platform reads one reading rather
|
|
290
|
+
// than computing a second opinion from the same numbers, which is how two
|
|
291
|
+
// implementations of the same judgement start disagreeing.
|
|
292
|
+
const verdicts = new Map(crosswalk(inventory).map((r) => [
|
|
293
|
+
r.component.name,
|
|
294
|
+
{ bucket: r.bucket, canonical: r.canonical, because: r.because },
|
|
295
|
+
]));
|
|
296
|
+
const components = inventory.map((c) => ({
|
|
297
|
+
...c,
|
|
298
|
+
...(verdicts.get(c.name) ?? {}),
|
|
299
|
+
}));
|
|
340
300
|
const pkgRaw = await readFile(join(root, "package.json"), "utf8").catch(() => null);
|
|
341
301
|
let name = null;
|
|
342
302
|
if (pkgRaw) {
|
|
@@ -477,6 +437,20 @@ function printCrosswalk(mine) {
|
|
|
477
437
|
show(`Already in the catalogue (${exists.length})`, exists, 6);
|
|
478
438
|
show(`The same decision, twice (${nearly.length})`, nearly, 6);
|
|
479
439
|
show(`Only yours (${exclusive.length})`, exclusive, 8);
|
|
440
|
+
// Counted, never listed: four icons and a provider under "only yours" pad the
|
|
441
|
+
// one list worth reading with the least interesting thing in it.
|
|
442
|
+
const icons = of("icon").length;
|
|
443
|
+
const providers = of("provider").length;
|
|
444
|
+
if (icons > 0 || providers > 0) {
|
|
445
|
+
const bits = [
|
|
446
|
+
icons > 0 &&
|
|
447
|
+
`${icons} icon${icons === 1 ? "" : "s"} (a library, not recipes)`,
|
|
448
|
+
providers > 0 &&
|
|
449
|
+
`${providers} provider${providers === 1 ? "" : "s"} (no UI of their own)`,
|
|
450
|
+
].filter(Boolean);
|
|
451
|
+
console.log(body(paint.faint(`Set aside: ${bits.join(" · ")}`)));
|
|
452
|
+
console.log("");
|
|
453
|
+
}
|
|
480
454
|
console.log(body(paint.dim("Nothing was mapped. This is a reading - the adopting is a decision you make.")));
|
|
481
455
|
}
|
|
482
456
|
function printAgentContract() {
|
|
@@ -1,24 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* The census reads vocabulary - colours, radii, spacing, fonts - and is blind
|
|
5
|
-
* to components. So a component library imported cleanly (dono, 30/07) arrived
|
|
6
|
-
* with thirteen colour ramps and not one of its eight atoms, and the system
|
|
7
|
-
* came out looking like ours instead of theirs.
|
|
8
|
-
*
|
|
9
|
-
* This is the same kind of arithmetic pointed at a different question: which
|
|
10
|
-
* elements does this code compose, how often, in how many files, and which
|
|
11
|
-
* literal values do their props take. No semantics, no judgement, no guessing
|
|
12
|
-
* what a component MEANS - that comparison happens later and with a person in
|
|
13
|
-
* the loop.
|
|
14
|
-
*
|
|
15
|
-
* A SCANNER, NOT A PARSER, and honest about it. The CLI ships no AST, so this
|
|
16
|
-
* reads JSX the way the drift scanner reads colours: by shape. It therefore
|
|
17
|
-
* sees what is written literally and misses what is computed
|
|
18
|
-
* (`<Comp {...props} />` contributes a use and no prop values), which is the
|
|
19
|
-
* right failure - a census that guessed at spread props would be inventing
|
|
20
|
-
* usage nobody wrote.
|
|
21
|
-
*/
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
22
3
|
/**
|
|
23
4
|
* Opens a JSX element whose name is capitalised - which is React's own rule for
|
|
24
5
|
* "this is a component, not an html tag".
|
|
@@ -59,7 +40,19 @@ function looksLikeType(source, at) {
|
|
|
59
40
|
* of copy wearing the shape of a variant axis. An axis is a closed set somebody
|
|
60
41
|
* designed; a title is whatever that screen happens to say.
|
|
61
42
|
*/
|
|
62
|
-
const
|
|
43
|
+
const CONTENT_WORD = "class|className|style|key|ref|id|href|src|alt|type|name|value|placeholder|children|title|label|text|heading|subtitle|caption|description|message|content|tooltip|hint|error|helper";
|
|
44
|
+
/**
|
|
45
|
+
* Whole name, or the TAIL of a camelCase compound.
|
|
46
|
+
*
|
|
47
|
+
* `bodyClassName` and `infoContent` walked straight past a whole-name match
|
|
48
|
+
* (dono, 30/07), and `infoContent` brought four sentences of screen copy into
|
|
49
|
+
* what is supposed to be a list of variant axes. The tail is what says what a
|
|
50
|
+
* prop IS: `infoContent` is content, `bodyClassName` is a class name.
|
|
51
|
+
*
|
|
52
|
+
* Matching the tail rather than anywhere keeps `titleTone` - an axis whose name
|
|
53
|
+
* merely starts with a content word.
|
|
54
|
+
*/
|
|
55
|
+
const NOT_A_DECISION = new RegExp(`^(?:${CONTENT_WORD})$|[a-z](?:${CONTENT_WORD.replace(/\b([a-z])/g, (_, c) => `[${c}${c.toUpperCase()}]`)})$|^(?:on[A-Z]|data-|aria-)`);
|
|
63
56
|
/** `import X, { A, B as C } from "spec"` - who owns each local name. */
|
|
64
57
|
const IMPORT = /import\s+(?:type\s+)?([\s\S]*?)\s+from\s+["']([^"']+)["']/g;
|
|
65
58
|
function importedNames(source, internal = []) {
|
|
@@ -166,3 +159,54 @@ export function tallyToInventory(tally, max = 80) {
|
|
|
166
159
|
.sort((a, b) => b.files - a.files || b.count - a.count)
|
|
167
160
|
.slice(0, max);
|
|
168
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Specifier prefixes this workspace owns: the aliases its tsconfig declares and
|
|
164
|
+
* the names of its sibling packages. Everything else that is not relative comes
|
|
165
|
+
* from node_modules.
|
|
166
|
+
*/
|
|
167
|
+
export async function internalSpecifiers(root) {
|
|
168
|
+
const out = new Set();
|
|
169
|
+
for (let up = 0, dir = root; up < 4; up++) {
|
|
170
|
+
for (const f of ["tsconfig.json", "tsconfig.base.json"]) {
|
|
171
|
+
const raw = await readFile(join(dir, f), "utf8").catch(() => null);
|
|
172
|
+
if (!raw)
|
|
173
|
+
continue;
|
|
174
|
+
try {
|
|
175
|
+
// tsconfig allows comments and trailing commas; a census must not die
|
|
176
|
+
// on either, and the keys are all we want.
|
|
177
|
+
for (const m of raw.matchAll(/"([^"]+)\/?\*?"\s*:\s*\[/g)) {
|
|
178
|
+
const key = m[1].replace(/\/\*$/, "").replace(/\*$/, "");
|
|
179
|
+
if (key && !key.startsWith("."))
|
|
180
|
+
out.add(key.replace(/\/$/, ""));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// unreadable config costs the aliases, not the run
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
for (const group of ["packages", "libs", "apps"]) {
|
|
188
|
+
for (const e of await readdir(join(dir, group), {
|
|
189
|
+
withFileTypes: true,
|
|
190
|
+
}).catch(() => [])) {
|
|
191
|
+
if (!e.isDirectory())
|
|
192
|
+
continue;
|
|
193
|
+
const raw = await readFile(join(dir, group, e.name, "package.json"), "utf8").catch(() => null);
|
|
194
|
+
if (!raw)
|
|
195
|
+
continue;
|
|
196
|
+
try {
|
|
197
|
+
const name = JSON.parse(raw).name;
|
|
198
|
+
if (typeof name === "string" && name)
|
|
199
|
+
out.add(name);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// same
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const parent = join(dir, "..");
|
|
207
|
+
if (parent === dir)
|
|
208
|
+
break;
|
|
209
|
+
dir = parent;
|
|
210
|
+
}
|
|
211
|
+
return [...out];
|
|
212
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOES THE CODE STAY INSIDE THE CONTRACT IT SIGNED?
|
|
3
|
+
*
|
|
4
|
+
* A system that arrived from `import` carries its exclusive components as
|
|
5
|
+
* CONTRACTS: the axes declared, the styles empty. That was worth doing only if
|
|
6
|
+
* something checks them afterwards - otherwise the declaration is a comment.
|
|
7
|
+
*
|
|
8
|
+
* This is that check, and it is the first one the product makes about
|
|
9
|
+
* COMPOSITION rather than about values. A colour outside the palette has always
|
|
10
|
+
* been drift; now `<WidgetCard variant="danger">` is drift too, on a component
|
|
11
|
+
* whose contract offers `neutral` and `ocean` - and nothing else in the
|
|
12
|
+
* toolchain can say that, because the answer lives in the design system rather
|
|
13
|
+
* than in the types.
|
|
14
|
+
*
|
|
15
|
+
* Silent by construction. An axis the contract does not declare is not checked
|
|
16
|
+
* (the system has no opinion about it), a component the system does not carry is
|
|
17
|
+
* not checked, and a value that cannot be read as a literal is not guessed at.
|
|
18
|
+
* A check that fires on what it does not know teaches people to ignore it.
|
|
19
|
+
*/
|
|
20
|
+
/** `WidgetCard` → `widget-card`, the same shape the contract was written under. */
|
|
21
|
+
export function recipeNameOf(element) {
|
|
22
|
+
return element
|
|
23
|
+
.split(".")
|
|
24
|
+
.join("-")
|
|
25
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.replace(/[^a-z0-9-]/g, "-")
|
|
28
|
+
.replace(/-+/g, "-")
|
|
29
|
+
.replace(/^-|-$/g, "");
|
|
30
|
+
}
|
|
31
|
+
/** axis → the options a recipe declares, read off the document. */
|
|
32
|
+
export function axesOfDocument(doc) {
|
|
33
|
+
const out = new Map();
|
|
34
|
+
const comps = doc?.components;
|
|
35
|
+
if (!comps)
|
|
36
|
+
return out;
|
|
37
|
+
for (const [name, recipe] of Object.entries(comps)) {
|
|
38
|
+
const variants = recipe
|
|
39
|
+
?.variants;
|
|
40
|
+
if (!variants)
|
|
41
|
+
continue;
|
|
42
|
+
const axes = new Map();
|
|
43
|
+
for (const [axis, options] of Object.entries(variants)) {
|
|
44
|
+
const keys = Object.keys((options ?? {})).filter(Boolean);
|
|
45
|
+
// One option is not a closed set; the contract writer refuses to declare
|
|
46
|
+
// those, and this refuses to enforce them for the same reason.
|
|
47
|
+
if (keys.length >= 2)
|
|
48
|
+
axes.set(axis.toLowerCase(), keys);
|
|
49
|
+
}
|
|
50
|
+
if (axes.size > 0)
|
|
51
|
+
out.set(name.toLowerCase(), axes);
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
export function checkContracts(used, documents) {
|
|
56
|
+
const axes = new Map();
|
|
57
|
+
for (const doc of documents) {
|
|
58
|
+
for (const [name, a] of axesOfDocument(doc))
|
|
59
|
+
axes.set(name, a);
|
|
60
|
+
}
|
|
61
|
+
if (axes.size === 0)
|
|
62
|
+
return [];
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const c of used) {
|
|
65
|
+
// A third party's component answers to its own package, not to this system.
|
|
66
|
+
if (c.from)
|
|
67
|
+
continue;
|
|
68
|
+
const recipe = recipeNameOf(c.name);
|
|
69
|
+
const contract = axes.get(recipe);
|
|
70
|
+
if (!contract)
|
|
71
|
+
continue;
|
|
72
|
+
for (const [prop, values] of Object.entries(c.props)) {
|
|
73
|
+
const offered = contract.get(prop.toLowerCase());
|
|
74
|
+
if (!offered)
|
|
75
|
+
continue;
|
|
76
|
+
const allowed = new Set(offered.map((o) => o.toLowerCase()));
|
|
77
|
+
for (const value of values) {
|
|
78
|
+
const v = value.trim().toLowerCase();
|
|
79
|
+
// `true` is what a bare boolean records; a contract about options has
|
|
80
|
+
// nothing to say about a flag.
|
|
81
|
+
if (!v || v === "true" || allowed.has(v))
|
|
82
|
+
continue;
|
|
83
|
+
out.push({
|
|
84
|
+
element: c.name,
|
|
85
|
+
recipe,
|
|
86
|
+
axis: prop,
|
|
87
|
+
used: value,
|
|
88
|
+
offered,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
package/dist/doctor/crosswalk.js
CHANGED
|
@@ -207,6 +207,26 @@ export function axisOverlap(a, b) {
|
|
|
207
207
|
shared += 1;
|
|
208
208
|
return shared / new Set([...va, ...vb]).size;
|
|
209
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Names that are not components in the sense this crosswalk means.
|
|
212
|
+
*
|
|
213
|
+
* An ICON is a glyph in a set, and a design system models it as a library
|
|
214
|
+
* rather than as a recipe with variant axes - so listing four of them under
|
|
215
|
+
* "only yours" pads the interesting list with the least interesting thing in
|
|
216
|
+
* it (dono, 30/07). A PROVIDER renders no UI at all.
|
|
217
|
+
*
|
|
218
|
+
* Both are reported as counts instead, which says more in one line than four
|
|
219
|
+
* lines of "nothing in the catalogue does this".
|
|
220
|
+
*/
|
|
221
|
+
const IS_ICON = /(^Icon|Icon$)/;
|
|
222
|
+
const IS_PROVIDER = /(Provider|Context)$/;
|
|
223
|
+
export function classifyAside(name) {
|
|
224
|
+
if (IS_ICON.test(name))
|
|
225
|
+
return "icon";
|
|
226
|
+
if (IS_PROVIDER.test(name))
|
|
227
|
+
return "provider";
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
210
230
|
/** Below this, two components share a value or two by accident. */
|
|
211
231
|
export const TWIN_OVERLAP = 0.5;
|
|
212
232
|
/**
|
|
@@ -254,6 +274,18 @@ export function crosswalk(components) {
|
|
|
254
274
|
}
|
|
255
275
|
return mine
|
|
256
276
|
.map((component) => {
|
|
277
|
+
const aside = classifyAside(component.name);
|
|
278
|
+
if (aside) {
|
|
279
|
+
return {
|
|
280
|
+
component,
|
|
281
|
+
canonical: null,
|
|
282
|
+
twins: [],
|
|
283
|
+
bucket: aside,
|
|
284
|
+
because: aside === "icon"
|
|
285
|
+
? "a glyph, which a system models as a library rather than a recipe"
|
|
286
|
+
: "renders no UI - infrastructure, not a component",
|
|
287
|
+
};
|
|
288
|
+
}
|
|
257
289
|
const canonical = canonicalName(component.name);
|
|
258
290
|
const twins = mine
|
|
259
291
|
.filter((o) => o.name !== component.name)
|
package/package.json
CHANGED