synthesisui 0.16.252 → 0.16.254
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/import.js +226 -9
- package/dist/index.js +12 -2
- package/dist/merge-census.js +231 -0
- package/dist/name-claim.js +71 -0
- package/package.json +1 -1
package/dist/commands/import.js
CHANGED
|
@@ -35,6 +35,8 @@ import { definitionSpan, parseClass, readInlineStyle, rootClasses, rootTag, tran
|
|
|
35
35
|
import { transcribeVariants } from "../doctor/variant-read.js";
|
|
36
36
|
import { frontierKind, packageRoot } from "../frontier-kind.js";
|
|
37
37
|
import { withLibraryStructure } from "../library-structure.js";
|
|
38
|
+
import { mergeCensus } from "../merge-census.js";
|
|
39
|
+
import { claimName } from "../name-claim.js";
|
|
38
40
|
import { body, paint, section } from "../output.js";
|
|
39
41
|
import { phase, startProgress } from "../progress.js";
|
|
40
42
|
import { detectStack, resolveDeps, stackVersions } from "../stack.js";
|
|
@@ -394,6 +396,16 @@ export async function takeCensus(root, opts) {
|
|
|
394
396
|
const internal = await internalSpecifiers(root);
|
|
395
397
|
const defined = [];
|
|
396
398
|
/** Nome → arquivo e fonte, para toda leitura por componente - ver o preenchimento abaixo. */
|
|
399
|
+
/**
|
|
400
|
+
* QUEM DETÉM CADA NOME - e o registro de quem pediu o mesmo.
|
|
401
|
+
*
|
|
402
|
+
* Os oito mapas deste laço são indexados por nome de componente, então dois
|
|
403
|
+
* `Card.tsx` disputam a mesma chave e o último do `walk` sobrescrevia os
|
|
404
|
+
* outros em silêncio. Medido no repo do dono: 20 nomes colidem no repo
|
|
405
|
+
* inteiro, 14 dentro de um app, e um dos quatro `Card` é o do design system
|
|
406
|
+
* dele. Ver `name-claim.ts` para o desempate e por que ele é por estrutura.
|
|
407
|
+
*/
|
|
408
|
+
const claims = { by: new Map(), collisions: [] };
|
|
397
409
|
const sourceOf = new Map();
|
|
398
410
|
/** Nome → o que a peça é, quando o portão soube dizer. Ver `CensusLook.kind`. */
|
|
399
411
|
const kindOf = new Map();
|
|
@@ -592,6 +604,21 @@ export async function takeCensus(root, opts) {
|
|
|
592
604
|
because: verdict.because,
|
|
593
605
|
});
|
|
594
606
|
}
|
|
607
|
+
/**
|
|
608
|
+
* O NOME É DISPUTADO AQUI, ANTES DE QUALQUER ESCRITA.
|
|
609
|
+
*
|
|
610
|
+
* Filtrar `found` fecha a porta de uma vez: os oito mapas deste laço
|
|
611
|
+
* derivam dele, então um componente que não detém o nome simplesmente não
|
|
612
|
+
* chega a nenhum deles - em vez de oito guardas que alguém esquece de
|
|
613
|
+
* repetir na nona escrita.
|
|
614
|
+
*
|
|
615
|
+
* O descarte NÃO é silencioso: `claims.collisions` guarda os dois caminhos
|
|
616
|
+
* e o censo os carrega. Trazer as duas receitas exige o contexto viajando
|
|
617
|
+
* na chave, que é a fatia dos grupos.
|
|
618
|
+
*/
|
|
619
|
+
const claimed = found.filter((d) => claimName(claims, d.name, rel));
|
|
620
|
+
found.length = 0;
|
|
621
|
+
found.push(...claimed);
|
|
595
622
|
for (const d of found) {
|
|
596
623
|
const props = readDefinitionProps(d.name, src);
|
|
597
624
|
propsOf.set(d.name, props);
|
|
@@ -1121,13 +1148,39 @@ export async function takeCensus(root, opts) {
|
|
|
1121
1148
|
const edges = new Map();
|
|
1122
1149
|
/** Por raiz: `@/` é uma pasta diferente em cada app - ver `reachabilityOf`. */
|
|
1123
1150
|
const graphRoots = [];
|
|
1151
|
+
/**
|
|
1152
|
+
* A DISTÂNCIA, contada onde os arquivos já estão abertos - ver `Census.adoption`.
|
|
1153
|
+
*
|
|
1154
|
+
* `scopePkg` é o nome do pacote do escopo, que é como um consumidor o importa.
|
|
1155
|
+
* Um arquivo conta quando ele NOMEIA esse especificador, e a régua é o texto do
|
|
1156
|
+
* import: um alias de tsconfig que aponta para a mesma pasta contaria como
|
|
1157
|
+
* "não usa", e essa é a diferença entre um número menor e um número inventado.
|
|
1158
|
+
*/
|
|
1159
|
+
const adoption = new Map();
|
|
1124
1160
|
for (const u of opts?.usage ?? []) {
|
|
1125
1161
|
const uInternal = await internalSpecifiers(u.path);
|
|
1126
1162
|
graphRoots.push({ path: u.path, aliases: await aliasesOf(u.path) });
|
|
1163
|
+
const tally = adoption.get(u.label) ?? { files: 0, using: 0 };
|
|
1164
|
+
adoption.set(u.label, tally);
|
|
1127
1165
|
for await (const file of walk(u.path)) {
|
|
1128
1166
|
const src = await readFile(file, "utf8").catch(() => "");
|
|
1129
1167
|
if (!src)
|
|
1130
1168
|
continue;
|
|
1169
|
+
/**
|
|
1170
|
+
* O DENOMINADOR SÓ CONTA QUEM PINTA.
|
|
1171
|
+
*
|
|
1172
|
+
* A primeira versão contava todo `.ts` - hooks, tipos, clientes http - e o
|
|
1173
|
+
* app do dono saiu como 105 de 1578 (7%), quando 105 de 356 arquivos de
|
|
1174
|
+
* marcação é 29%. Um arquivo de tipos não deixa de usar o design system:
|
|
1175
|
+
* ele nunca teve como. Diluir o denominador com ele faz a migração parecer
|
|
1176
|
+
* mais atrasada do que é, e um número pessimista é tão falso quanto um
|
|
1177
|
+
* otimista.
|
|
1178
|
+
*/
|
|
1179
|
+
if (/\.(tsx|jsx|vue|svelte)$/i.test(file)) {
|
|
1180
|
+
tally.files += 1;
|
|
1181
|
+
if (scopePkg && src.includes(scopePkg))
|
|
1182
|
+
tally.using += 1;
|
|
1183
|
+
}
|
|
1131
1184
|
edges.set(file, edgesIn(src));
|
|
1132
1185
|
const rel = join(u.label, relative(u.path, file));
|
|
1133
1186
|
if (/(\.(spec|test|stories)\.[a-z]+$|__tests__\/|(^|\/)\.storybook\/)/.test(rel)) {
|
|
@@ -1880,6 +1933,26 @@ export async function takeCensus(root, opts) {
|
|
|
1880
1933
|
...(Object.keys(keyframes).length > 0 ? { keyframes } : {}),
|
|
1881
1934
|
...(animations.size > 0 ? { animations: [...animations].sort() } : {}),
|
|
1882
1935
|
...(brokenRefs.length > 0 ? { brokenRefs } : {}),
|
|
1936
|
+
/**
|
|
1937
|
+
* OS NOMES DISPUTADOS - ver `name-claim.ts`.
|
|
1938
|
+
*
|
|
1939
|
+
* Aditivo e omitido quando vazio, que é o caso de toda biblioteca sã: no repo
|
|
1940
|
+
* do dono, `packages/ui` produz zero e o repo inteiro produz 20. Um campo que
|
|
1941
|
+
* só aparece quando há algo a dizer não muda nada para quem já lê o censo.
|
|
1942
|
+
*/
|
|
1943
|
+
...(claims.collisions.length > 0 ? { collisions: claims.collisions } : {}),
|
|
1944
|
+
...(scopePkg && adoption.size > 0
|
|
1945
|
+
? {
|
|
1946
|
+
adoption: {
|
|
1947
|
+
specifier: scopePkg,
|
|
1948
|
+
consumers: [...adoption.entries()].map(([label, t]) => ({
|
|
1949
|
+
label,
|
|
1950
|
+
files: t.files,
|
|
1951
|
+
using: t.using,
|
|
1952
|
+
})),
|
|
1953
|
+
},
|
|
1954
|
+
}
|
|
1955
|
+
: {}),
|
|
1883
1956
|
...(conventions.length > 0 ? { conventions } : {}),
|
|
1884
1957
|
classStyle,
|
|
1885
1958
|
...(Object.keys(looks).length > 0 ? { looks } : {}),
|
|
@@ -1995,6 +2068,8 @@ export async function takeCensus(root, opts) {
|
|
|
1995
2068
|
named: d.named,
|
|
1996
2069
|
tokenUses: d.tokenUses,
|
|
1997
2070
|
coverage: d.coverage,
|
|
2071
|
+
ownUses: d.ownUses,
|
|
2072
|
+
phantomUses: d.phantomUses,
|
|
1998
2073
|
},
|
|
1999
2074
|
};
|
|
2000
2075
|
}
|
|
@@ -2055,8 +2130,120 @@ async function summarize(c, root, scope) {
|
|
|
2055
2130
|
}
|
|
2056
2131
|
}
|
|
2057
2132
|
sayConventions(c);
|
|
2133
|
+
sayScopes(c);
|
|
2134
|
+
sayAdoption(c);
|
|
2135
|
+
sayCollisions(c);
|
|
2058
2136
|
await sayBrokenRefs(c, root, scope);
|
|
2059
2137
|
}
|
|
2138
|
+
/**
|
|
2139
|
+
* OS ESCOPOS QUE VIRARAM UM SISTEMA, e o que cada um perdeu no empate.
|
|
2140
|
+
*
|
|
2141
|
+
* `--scope` aceita mais de um, e a ordem é uma declaração: o primeiro vence. Um
|
|
2142
|
+
* token que os dois declaram diferente não é erro de ninguém - é divergência, e
|
|
2143
|
+
* a lei do dono (§8.6) é que a plataforma não decide sozinha. Ela mostra os
|
|
2144
|
+
* dois, diz quem é a fonte declarada, e propõe alinhar o outro lado.
|
|
2145
|
+
*
|
|
2146
|
+
* Medido no monorepo dele: `packages/ui` e `apps/web-dashboard/src` declaram os
|
|
2147
|
+
* mesmos três `--gradient-gray-*` com valores opostos - claros na biblioteca,
|
|
2148
|
+
* escuros no app. Nenhum dos dois está errado, e um deles ia desaparecer calado.
|
|
2149
|
+
*/
|
|
2150
|
+
function sayScopes(c) {
|
|
2151
|
+
const scopes = c.scopes ?? [];
|
|
2152
|
+
if (scopes.length < 2)
|
|
2153
|
+
return;
|
|
2154
|
+
const conflicts = c.scopeConflicts ?? [];
|
|
2155
|
+
console.log("");
|
|
2156
|
+
console.log(section("Two folders, one system"));
|
|
2157
|
+
console.log(body(`Measured ${scopes.length} scopes and fused them: ${scopes.map((x, i) => (i === 0 ? paint.strong(x) : x)).join(" · ")}. The first one is the authority - it wins a tie, and nothing from the others is dropped without a line.`));
|
|
2158
|
+
if (conflicts.length === 0)
|
|
2159
|
+
return;
|
|
2160
|
+
console.log("");
|
|
2161
|
+
console.log(body(`${conflicts.length} token${conflicts.length === 1 ? "" : "s"} ${conflicts.length === 1 ? "is" : "are"} declared differently in two of them:`));
|
|
2162
|
+
console.log("");
|
|
2163
|
+
for (const x of conflicts.slice(0, 5)) {
|
|
2164
|
+
console.log(body(`${paint.strong(x.name)}\n ${paint.faint("kept")} ${x.kept.value} ${paint.faint(`(${x.kept.scope})`)}\n ${paint.faint("also")} ${x.lost.value} ${paint.faint(`(${x.lost.scope})`)}`));
|
|
2165
|
+
}
|
|
2166
|
+
if (conflicts.length > 5)
|
|
2167
|
+
console.log(body(paint.faint(`(${conflicts.length - 5} more)`)));
|
|
2168
|
+
console.log("");
|
|
2169
|
+
console.log(body(paint.faint("Neither value is wrong - one of them is a different scheme, or a fork that drifted. The system carries the first; the screen can show both.")));
|
|
2170
|
+
}
|
|
2171
|
+
/**
|
|
2172
|
+
* QUANTO DA MIGRAÇÃO JÁ ACONTECEU - o número que faltava.
|
|
2173
|
+
*
|
|
2174
|
+
* O `intent` já dizia que alguém está migrando design para o pacote
|
|
2175
|
+
* compartilhado (`init --intent migrate`, versionado em `_synthesisui/config.json`
|
|
2176
|
+
* com a data). O que nenhuma superfície dizia é QUANTO já chegou. Medido no
|
|
2177
|
+
* monorepo do dono: 103 de 356 arquivos do `web-dashboard` importam
|
|
2178
|
+
* `@frontend-hub/ui`, e o `web-review` não o conhece.
|
|
2179
|
+
*
|
|
2180
|
+
* É a diferença entre medir estado e medir progresso: um relatório de estado é
|
|
2181
|
+
* igual em janeiro e em junho; este muda a cada versão.
|
|
2182
|
+
*/
|
|
2183
|
+
function sayAdoption(c) {
|
|
2184
|
+
const a = c.adoption;
|
|
2185
|
+
if (!a || a.consumers.length === 0)
|
|
2186
|
+
return;
|
|
2187
|
+
const total = a.consumers.reduce((n, x) => n + x.files, 0);
|
|
2188
|
+
const using = a.consumers.reduce((n, x) => n + x.using, 0);
|
|
2189
|
+
console.log("");
|
|
2190
|
+
console.log(section("How far the system already reaches"));
|
|
2191
|
+
console.log(body(`${using} of ${total} source files across the folders you named import ${paint.strong(a.specifier)}. Everything else in them is still painting on its own.`));
|
|
2192
|
+
console.log("");
|
|
2193
|
+
for (const x of a.consumers) {
|
|
2194
|
+
const pct = x.files > 0 ? Math.round((x.using / x.files) * 100) : 0;
|
|
2195
|
+
console.log(body(`${paint.strong(x.label.padEnd(24))} ${String(x.using).padStart(4)} of ${String(x.files).padStart(4)} files ${paint.faint(`· ${pct}%`)}${x.using === 0 ? paint.faint(" - does not know the system yet") : ""}`));
|
|
2196
|
+
}
|
|
2197
|
+
console.log("");
|
|
2198
|
+
console.log(body(paint.faint("This is the number a migration is measured by, and it is the one that moves. Run the import again after a batch and the difference is the progress.")));
|
|
2199
|
+
}
|
|
2200
|
+
/**
|
|
2201
|
+
* DOIS ARQUIVOS, UM NOME - dito, com os dois caminhos.
|
|
2202
|
+
*
|
|
2203
|
+
* O silêncio aqui era o pior tipo: a pessoa recebe 36 componentes onde o repo
|
|
2204
|
+
* tem 39, e nada no relatório sugere que três existiram. Medido no monorepo do
|
|
2205
|
+
* dono: 20 nomes no repo inteiro, e um dos quatro `Card.tsx` é o do design system
|
|
2206
|
+
* dele competindo com dois que vivem sob rotas.
|
|
2207
|
+
*
|
|
2208
|
+
* A frase diz o que fazer, e são duas coisas diferentes: um escopo mais estreito
|
|
2209
|
+
* quando os homônimos são de outro projeto, ou um rename quando são do mesmo.
|
|
2210
|
+
*/
|
|
2211
|
+
function sayCollisions(c) {
|
|
2212
|
+
const all = c.collisions ?? [];
|
|
2213
|
+
if (all.length === 0)
|
|
2214
|
+
return;
|
|
2215
|
+
/**
|
|
2216
|
+
* DUAS REGRAS, DUAS FRASES. Dentro de um escopo vence quem está mais perto da
|
|
2217
|
+
* raiz; entre escopos vence o primeiro `--scope`. Explicar um empate com a
|
|
2218
|
+
* regra do outro é descrever algo que não aconteceu.
|
|
2219
|
+
*/
|
|
2220
|
+
const crossScope = all.filter((x) => x.by === "scope");
|
|
2221
|
+
const inScope = all.filter((x) => x.by !== "scope");
|
|
2222
|
+
const names = new Set(all.map((x) => x.name));
|
|
2223
|
+
console.log("");
|
|
2224
|
+
console.log(section("Two places, one name"));
|
|
2225
|
+
console.log(body(`${names.size} name${names.size === 1 ? "" : "s"} ${names.size === 1 ? "is" : "are"} defined more than once in what was measured, so ${all.length} definition${all.length === 1 ? "" : "s"} did not make it into this system.`));
|
|
2226
|
+
if (crossScope.length > 0) {
|
|
2227
|
+
console.log("");
|
|
2228
|
+
console.log(body(`${crossScope.length} of them in two different scopes - the FIRST --scope kept the name:`));
|
|
2229
|
+
console.log("");
|
|
2230
|
+
for (const x of crossScope.slice(0, 6))
|
|
2231
|
+
console.log(body(`${paint.strong(x.name.padEnd(22))} ${paint.faint("kept in")} ${x.kept} ${paint.faint("· also in")} ${x.dropped}`));
|
|
2232
|
+
if (crossScope.length > 6)
|
|
2233
|
+
console.log(body(paint.faint(`(${crossScope.length - 6} more)`)));
|
|
2234
|
+
}
|
|
2235
|
+
if (inScope.length > 0) {
|
|
2236
|
+
console.log("");
|
|
2237
|
+
console.log(body(`${inScope.length} inside a single scope - the file closest to its root kept the name, never the order the disk happened to return:`));
|
|
2238
|
+
console.log("");
|
|
2239
|
+
for (const x of inScope.slice(0, 6))
|
|
2240
|
+
console.log(body(`${paint.strong(x.name)}\n ${paint.faint("kept")} ${x.kept}\n ${paint.faint("dropped")} ${x.dropped}`));
|
|
2241
|
+
if (inScope.length > 6)
|
|
2242
|
+
console.log(body(paint.faint(`(${inScope.length - 6} more)`)));
|
|
2243
|
+
}
|
|
2244
|
+
console.log("");
|
|
2245
|
+
console.log(body(paint.faint("If the homonyms belong to different projects, a narrower --scope is the answer and nothing is lost. If they belong to the same one, renaming in your code is - two components with one name is one name your agent cannot resolve either.")));
|
|
2246
|
+
}
|
|
2060
2247
|
/**
|
|
2061
2248
|
* THEIR VOCABULARY, MEASURED. This section exists so every later finding can be
|
|
2062
2249
|
* phrased as "you already do X, and here you did not" instead of "adopt ours".
|
|
@@ -3095,8 +3282,18 @@ export async function runImport(opts) {
|
|
|
3095
3282
|
.trim()
|
|
3096
3283
|
.replace(/^\.\/+/, "")
|
|
3097
3284
|
.replace(/\/+$/, "");
|
|
3098
|
-
|
|
3099
|
-
|
|
3285
|
+
/**
|
|
3286
|
+
* OS ESCOPOS, na ordem em que a pessoa os escreveu - e `scope` é o primeiro.
|
|
3287
|
+
*
|
|
3288
|
+
* O parser de flags já acumula uma flag repetida (`--usage` usa isso desde
|
|
3289
|
+
* 01/08), então `--scope packages/ui --scope packages/charts` chega aqui como
|
|
3290
|
+
* array sem nenhuma mudança de gramática.
|
|
3291
|
+
*/
|
|
3292
|
+
const scopes = (Array.isArray(opts.scope) ? opts.scope : opts.scope ? [opts.scope] : [])
|
|
3293
|
+
.map(tidy)
|
|
3294
|
+
.filter(Boolean)
|
|
3295
|
+
.filter((v, i, all) => all.indexOf(v) === i);
|
|
3296
|
+
const scope = scopes[0];
|
|
3100
3297
|
/**
|
|
3101
3298
|
* THE SECOND ROLE. `--scope` is the SYSTEM - one place, and the only source of
|
|
3102
3299
|
* the palette, the scales and the convention. `--usage` is the EVIDENCE - as
|
|
@@ -3171,13 +3368,33 @@ export async function runImport(opts) {
|
|
|
3171
3368
|
*/
|
|
3172
3369
|
phase(1, 3, "Measuring the repository");
|
|
3173
3370
|
console.log(section("Reading your project"));
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3371
|
+
/**
|
|
3372
|
+
* UMA MEDIÇÃO POR ESCOPO, E DEPOIS A FUSÃO.
|
|
3373
|
+
*
|
|
3374
|
+
* `takeCensus` recebe UM `root` em cinco pontos estruturais que são por
|
|
3375
|
+
* PACOTE e não por sistema (`harvestOwnCss`, `internalSpecifiers`,
|
|
3376
|
+
* `publicApi`, o `walk`, `aliasesOf`), então ensiná-la a medir N raízes
|
|
3377
|
+
* seria refatorar a função de 1855 linhas que o corpus dourado protege.
|
|
3378
|
+
* Medir N vezes e fundir troca esse risco por uma função pura - e as regras
|
|
3379
|
+
* de empate ficam num arquivo em vez de espalhadas por um laço.
|
|
3380
|
+
*
|
|
3381
|
+
* A EVIDÊNCIA VIAJA COM O PRIMEIRO: `--usage` doa contagem e lei, e somar a
|
|
3382
|
+
* mesma evidência N vezes multiplicaria os números que decidem o que é
|
|
3383
|
+
* hábito. Ela entra uma vez, na medição do escopo que declara.
|
|
3384
|
+
*/
|
|
3385
|
+
const measured = [];
|
|
3386
|
+
for (const [i, one] of (scopes.length > 0 ? scopes : [""]).entries()) {
|
|
3387
|
+
const at = one ? join(root, one) : root;
|
|
3388
|
+
const c = await takeCensus(at, {
|
|
3389
|
+
...(i === 0 && usage.length > 0 ? { usage } : {}),
|
|
3390
|
+
...(one ? { scopeLabel: one } : {}),
|
|
3391
|
+
...(opts.cli ? { cli: opts.cli } : {}),
|
|
3392
|
+
});
|
|
3393
|
+
if (one)
|
|
3394
|
+
c.scope = one;
|
|
3395
|
+
measured.push(c);
|
|
3396
|
+
}
|
|
3397
|
+
census = mergeCensus(measured);
|
|
3181
3398
|
}
|
|
3182
3399
|
/**
|
|
3183
3400
|
* DERIVE THE ANATOMY ON THE MEASURING PATH TOO.
|
package/dist/index.js
CHANGED
|
@@ -89,7 +89,8 @@ Options:
|
|
|
89
89
|
--version <n> install a specific version (default: latest)
|
|
90
90
|
--ds <slug> init: bring this DS in right away · generate: target DS (default: installed)
|
|
91
91
|
--name <name> preferred component name for generate
|
|
92
|
-
--scope <path> import: the SYSTEM - tokens, components, convention.
|
|
92
|
+
--scope <path> import: the SYSTEM - tokens, components, convention. Repeatable;
|
|
93
|
+
the first one wins a tie.
|
|
93
94
|
--usage <path> import: the EVIDENCE - counts, chosen values, laws. Repeatable.
|
|
94
95
|
--scheme <s> dark|light - which end of your ladder is the default
|
|
95
96
|
--target <t> template/init target: next | general (default: next)
|
|
@@ -160,7 +161,16 @@ async function main() {
|
|
|
160
161
|
// Only these two words. Anything else is a typo that would silently
|
|
161
162
|
// invert a system, so it falls through to being measured and asked.
|
|
162
163
|
// WHAT to read. `--dir` stays WHERE the project is, in every command.
|
|
163
|
-
|
|
164
|
+
/**
|
|
165
|
+
* `--scope` REPETÍVEL: um sistema pode viver em dois pacotes, e o parser
|
|
166
|
+
* já acumula flag repetida (`--usage` usa isso). A ordem é a que a pessoa
|
|
167
|
+
* escreveu, e ela decide os empates - ver `merge-census.ts`.
|
|
168
|
+
*/
|
|
169
|
+
scope: Array.isArray(flags.scope)
|
|
170
|
+
? flags.scope
|
|
171
|
+
: typeof flags.scope === "string"
|
|
172
|
+
? flags.scope
|
|
173
|
+
: undefined,
|
|
164
174
|
// WHERE THE EVIDENCE IS. Repeatable, and comma-separated works too - a
|
|
165
175
|
// person typing one path with a comma in it means two paths.
|
|
166
176
|
usage: [flags.usage]
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
const asRecord = (v) => (v ?? {});
|
|
2
|
+
/** Nome do escopo para as mensagens - `.` quando a medição não escopou nada. */
|
|
3
|
+
const nameOf = (c, i) => c.scope ?? (i === 0 ? "." : `#${i + 1}`);
|
|
4
|
+
/**
|
|
5
|
+
* OS TOKENS, com o primeiro escopo mandando - e o conflito registrado.
|
|
6
|
+
*
|
|
7
|
+
* `declared` é o coração: é dele que saem os nomes que viajam para a tela. Um
|
|
8
|
+
* `--brand-500` que vale `#1394dc` na biblioteca e `#0f6fa8` no pacote de
|
|
9
|
+
* gráficos não é um erro de ninguém, é uma divergência - e a lei do dono
|
|
10
|
+
* (§8.6) é que a plataforma não decide sozinha: ela mostra os dois e diz quem é
|
|
11
|
+
* a fonte declarada.
|
|
12
|
+
*/
|
|
13
|
+
function mergeDeclared(into, from, keptScope, fromScope, conflicts) {
|
|
14
|
+
for (const [name, value] of Object.entries(from)) {
|
|
15
|
+
const held = into[name];
|
|
16
|
+
if (held === undefined) {
|
|
17
|
+
into[name] = value;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (held === value)
|
|
21
|
+
continue;
|
|
22
|
+
conflicts.push({
|
|
23
|
+
kind: "token",
|
|
24
|
+
name,
|
|
25
|
+
kept: { scope: keptScope, value: held },
|
|
26
|
+
lost: { scope: fromScope, value },
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Os valores medidos, somados por (natureza, valor) - e o nome vem de quem o tem. */
|
|
31
|
+
function mergeObserved(a, b) {
|
|
32
|
+
const out = a.map((v) => ({ ...v }));
|
|
33
|
+
const at = new Map(out.map((v, i) => [`${v.kind}:${v.value}`, i]));
|
|
34
|
+
for (const v of b) {
|
|
35
|
+
const key = `${v.kind}:${v.value}`;
|
|
36
|
+
const i = at.get(key);
|
|
37
|
+
if (i === undefined) {
|
|
38
|
+
at.set(key, out.length);
|
|
39
|
+
out.push({ ...v });
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
out[i].count += v.count;
|
|
43
|
+
out[i].files += v.files;
|
|
44
|
+
/** Um nome não se apaga por o outro escopo não ter um. */
|
|
45
|
+
if (!out[i].token && v.token)
|
|
46
|
+
out[i].token = v.token;
|
|
47
|
+
}
|
|
48
|
+
return out.sort((x, y) => y.count - x.count);
|
|
49
|
+
}
|
|
50
|
+
/** União preservando a ordem de chegada - a do primeiro escopo primeiro. */
|
|
51
|
+
const union = (a, b) => [
|
|
52
|
+
...new Set([...(a ?? []), ...(b ?? [])]),
|
|
53
|
+
];
|
|
54
|
+
const concat = (a, b) => [
|
|
55
|
+
...(a ?? []),
|
|
56
|
+
...(b ?? []),
|
|
57
|
+
];
|
|
58
|
+
/** Soma campo a campo dois objetos de contagem, mantendo o que só um tem. */
|
|
59
|
+
function sumInto(a, b) {
|
|
60
|
+
if (!a)
|
|
61
|
+
return b;
|
|
62
|
+
if (!b)
|
|
63
|
+
return a;
|
|
64
|
+
const out = { ...a };
|
|
65
|
+
for (const [k, v] of Object.entries(b)) {
|
|
66
|
+
const mine = out[k];
|
|
67
|
+
if (typeof v === "number" && typeof mine === "number")
|
|
68
|
+
out[k] = mine + v;
|
|
69
|
+
else if (mine === undefined)
|
|
70
|
+
out[k] = v;
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* OS CENSOS, FUNDIDOS NA ORDEM EM QUE A PESSOA OS PEDIU.
|
|
76
|
+
*
|
|
77
|
+
* Devolve o próprio objeto quando há um só: o caminho comum - um escopo - não
|
|
78
|
+
* paga nada por esta função existir.
|
|
79
|
+
*/
|
|
80
|
+
export function mergeCensus(list) {
|
|
81
|
+
if (list.length === 0)
|
|
82
|
+
throw new Error("mergeCensus needs at least one census");
|
|
83
|
+
if (list.length === 1)
|
|
84
|
+
return list[0];
|
|
85
|
+
const first = list[0];
|
|
86
|
+
const conflicts = [];
|
|
87
|
+
const collisions = [
|
|
88
|
+
...(first.collisions ?? []),
|
|
89
|
+
];
|
|
90
|
+
const declared = { ...asRecord(first.declared) };
|
|
91
|
+
const declaredAlt = { ...asRecord(first.declaredAlt) };
|
|
92
|
+
const keyframes = { ...(first.keyframes ?? {}) };
|
|
93
|
+
const looks = { ...(first.looks ?? {}) };
|
|
94
|
+
let observed = first.observed.map((v) => ({ ...v }));
|
|
95
|
+
let totals = { ...first.totals };
|
|
96
|
+
let coverage = first.coverage;
|
|
97
|
+
let gate = first.gate;
|
|
98
|
+
let reachability = first.reachability;
|
|
99
|
+
let usage = first.usage ?? undefined;
|
|
100
|
+
let animations = first.animations ?? undefined;
|
|
101
|
+
let skipped = first.skipped ?? undefined;
|
|
102
|
+
let brokenRefs = first.brokenRefs ?? undefined;
|
|
103
|
+
let conventions = first.conventions ?? undefined;
|
|
104
|
+
let architectures = first.architectures ?? undefined;
|
|
105
|
+
let components = first.components ?? undefined;
|
|
106
|
+
let defined = first.defined ?? undefined;
|
|
107
|
+
const stack = [...(first.project.stack ?? [])];
|
|
108
|
+
for (let i = 1; i < list.length; i++) {
|
|
109
|
+
const c = list[i];
|
|
110
|
+
const scopeName = nameOf(c, i);
|
|
111
|
+
const firstName = nameOf(first, 0);
|
|
112
|
+
mergeDeclared(declared, asRecord(c.declared), firstName, scopeName, conflicts);
|
|
113
|
+
mergeDeclared(declaredAlt, asRecord(c.declaredAlt), firstName, scopeName, conflicts);
|
|
114
|
+
/**
|
|
115
|
+
* O COMPONENTE QUE OS DOIS DEFINEM fica com a receita do primeiro escopo, e
|
|
116
|
+
* o descarte entra em `collisions` - a mesma lista que `name-claim.ts`
|
|
117
|
+
* alimenta dentro de UM escopo. Uma lista, dois níveis: o arquivo e o
|
|
118
|
+
* pacote, e a tela não precisa saber a diferença para dizer a verdade.
|
|
119
|
+
*/
|
|
120
|
+
for (const [name, look] of Object.entries(c.looks ?? {})) {
|
|
121
|
+
if (looks[name] === undefined) {
|
|
122
|
+
looks[name] = look;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
/** `by` diz que este empate foi resolvido pela ORDEM DOS ESCOPOS. */
|
|
126
|
+
collisions.push({
|
|
127
|
+
name,
|
|
128
|
+
kept: firstName,
|
|
129
|
+
dropped: scopeName,
|
|
130
|
+
by: "scope",
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
for (const [name, frames] of Object.entries(c.keyframes ?? {}))
|
|
134
|
+
if (keyframes[name] === undefined)
|
|
135
|
+
keyframes[name] = frames;
|
|
136
|
+
observed = mergeObserved(observed, c.observed);
|
|
137
|
+
totals = {
|
|
138
|
+
scanned: totals.scanned + c.totals.scanned,
|
|
139
|
+
values: totals.values + c.totals.values,
|
|
140
|
+
named: totals.named + c.totals.named,
|
|
141
|
+
tokenUses: totals.tokenUses + c.totals.tokenUses,
|
|
142
|
+
ownUses: (totals.ownUses ?? 0) + (c.totals.ownUses ?? 0),
|
|
143
|
+
phantomUses: (totals.phantomUses ?? 0) + (c.totals.phantomUses ?? 0),
|
|
144
|
+
/** Recalculado abaixo - somar percentuais não significa nada. */
|
|
145
|
+
coverage: 0,
|
|
146
|
+
};
|
|
147
|
+
coverage = sumInto(coverage, c.coverage);
|
|
148
|
+
gate = sumInto(gate, c.gate);
|
|
149
|
+
reachability = sumInto(reachability, c.reachability);
|
|
150
|
+
usage = union(usage, c.usage);
|
|
151
|
+
animations = union(animations, c.animations);
|
|
152
|
+
skipped = concat(skipped, c.skipped);
|
|
153
|
+
brokenRefs = concat(brokenRefs, c.brokenRefs);
|
|
154
|
+
conventions = concat(conventions, c.conventions);
|
|
155
|
+
architectures = concat(architectures, c.architectures);
|
|
156
|
+
components = concat(components, c.components);
|
|
157
|
+
defined = concat(defined, c.defined);
|
|
158
|
+
for (const s of c.project.stack ?? [])
|
|
159
|
+
if (!stack.includes(s))
|
|
160
|
+
stack.push(s);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* A COBERTURA, RECALCULADA COM A FÓRMULA DE VERDADE.
|
|
164
|
+
*
|
|
165
|
+
* `(tokenUses + ownUses) / (tokenUses + ownUses + values + phantomUses)` - a
|
|
166
|
+
* mesma de `doctor/scan.ts:796`. A primeira versão desta função usou
|
|
167
|
+
* `named/values`, que parece razoável e não é a fórmula: fundir 48% e 30%
|
|
168
|
+
* devolvia 10% no monorepo do dono. Um número que não descreve nenhuma das
|
|
169
|
+
* medições é pior que não ter o número.
|
|
170
|
+
*
|
|
171
|
+
* Quando os censos vêm de um CLI que não gravava `ownUses`, a soma perde esses
|
|
172
|
+
* termos e o resultado seria menor que a verdade - então neste caso a
|
|
173
|
+
* cobertura do PRIMEIRO escopo é preservada, que é uma medição real de uma
|
|
174
|
+
* parte real, em vez de uma conta feita com metade dos termos.
|
|
175
|
+
*/
|
|
176
|
+
const named = totals.tokenUses + (totals.ownUses ?? 0);
|
|
177
|
+
const denominator = named + totals.values + (totals.phantomUses ?? 0);
|
|
178
|
+
const complete = list.every((c) => c.totals.ownUses !== undefined);
|
|
179
|
+
totals.coverage = !complete
|
|
180
|
+
? first.totals.coverage
|
|
181
|
+
: denominator > 0
|
|
182
|
+
? Math.round((named / denominator) * 100)
|
|
183
|
+
: 100;
|
|
184
|
+
const out = {
|
|
185
|
+
...first,
|
|
186
|
+
project: { ...first.project, stack },
|
|
187
|
+
/**
|
|
188
|
+
* `scope` SEGUE SENDO O PRIMEIRO, e não uma lista, porque há quem compare por
|
|
189
|
+
* igualdade: `live-census.ts:44` faz `census.scope === LIBRARY_SCOPE`, e um
|
|
190
|
+
* array ali seria um falso silencioso. `scopes` é aditivo - quem não sabe
|
|
191
|
+
* dele continua lendo o censo como antes.
|
|
192
|
+
*/
|
|
193
|
+
scopes: list.map((c, i) => nameOf(c, i)),
|
|
194
|
+
declared,
|
|
195
|
+
observed,
|
|
196
|
+
totals,
|
|
197
|
+
};
|
|
198
|
+
if (Object.keys(declaredAlt).length > 0)
|
|
199
|
+
out.declaredAlt = declaredAlt;
|
|
200
|
+
if (Object.keys(keyframes).length > 0)
|
|
201
|
+
out.keyframes = keyframes;
|
|
202
|
+
if (Object.keys(looks).length > 0)
|
|
203
|
+
out.looks = looks;
|
|
204
|
+
if (coverage)
|
|
205
|
+
out.coverage = coverage;
|
|
206
|
+
if (gate)
|
|
207
|
+
out.gate = gate;
|
|
208
|
+
if (reachability)
|
|
209
|
+
out.reachability = reachability;
|
|
210
|
+
if (usage && usage.length > 0)
|
|
211
|
+
out.usage = usage;
|
|
212
|
+
if (animations && animations.length > 0)
|
|
213
|
+
out.animations = animations;
|
|
214
|
+
if (skipped && skipped.length > 0)
|
|
215
|
+
out.skipped = skipped;
|
|
216
|
+
if (brokenRefs && brokenRefs.length > 0)
|
|
217
|
+
out.brokenRefs = brokenRefs;
|
|
218
|
+
if (conventions && conventions.length > 0)
|
|
219
|
+
out.conventions = conventions;
|
|
220
|
+
if (architectures && architectures.length > 0)
|
|
221
|
+
out.architectures = architectures;
|
|
222
|
+
if (components && components.length > 0)
|
|
223
|
+
out.components = components;
|
|
224
|
+
if (defined && defined.length > 0)
|
|
225
|
+
out.defined = defined;
|
|
226
|
+
if (collisions.length > 0)
|
|
227
|
+
out.collisions = collisions;
|
|
228
|
+
if (conflicts.length > 0)
|
|
229
|
+
out.scopeConflicts = conflicts;
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UM NOME, UM COMPONENTE - e o que fazer quando dois pedem o mesmo.
|
|
3
|
+
*
|
|
4
|
+
* O laço de leitura do import escreve em oito mapas indexados por NOME de
|
|
5
|
+
* componente (`looks`, `sourceOf`, `kindOf`, `propsOf`, `runtimeOf`,
|
|
6
|
+
* `variantAxes`, `islands`, `keyframes`). Dois `Card.tsx` em pastas diferentes
|
|
7
|
+
* disputam a mesma chave, e até aqui o último que o `walk` visitou sobrescrevia
|
|
8
|
+
* os outros - em silêncio, e por uma ordem que é do sistema de arquivos e não
|
|
9
|
+
* uma decisão de ninguém.
|
|
10
|
+
*
|
|
11
|
+
* Medido no monorepo do dono (19/08): 20 nomes colidem no repo inteiro, 14 dentro
|
|
12
|
+
* de `apps/web-dashboard/src`, e quatro arquivos se chamam `Card.tsx` - um deles
|
|
13
|
+
* é o `Card` do design system dele, e dois estão sob `_legacy`. `packages/ui`
|
|
14
|
+
* sozinho não tem colisão nenhuma: o problema mora em escopo largo, que é
|
|
15
|
+
* exatamente o default que a flexibilidade pede.
|
|
16
|
+
*
|
|
17
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
18
|
+
* O DESEMPATE É POR ESTRUTURA, E DETERMINÍSTICO
|
|
19
|
+
*
|
|
20
|
+
* A intuição primeira era "quem o pacote EXPORTA fica com o nome", e ela não
|
|
21
|
+
* responde: `publicApi().exports` é um conjunto de NOMES, e os dois candidatos
|
|
22
|
+
* têm o mesmo nome. Escolher pelo que o pacote realmente publica exige o grafo a
|
|
23
|
+
* partir da entrada do manifesto, e essa é a fatia que também entrega os grupos.
|
|
24
|
+
*
|
|
25
|
+
* Então: menos segmentos de caminho vence, e empate resolve alfabeticamente. É
|
|
26
|
+
* pouco, e é honesto - a diferença que importa é que a resposta deixa de mudar
|
|
27
|
+
* conforme a ordem em que o disco devolve os arquivos.
|
|
28
|
+
*
|
|
29
|
+
* E NUNCA POR NOME DE PASTA. Descartar `_legacy` por se chamar `_legacy` é a lei
|
|
30
|
+
* 14 sendo furada: no repo dele `_legacy` tem 178 arquivos e 106 estão VIVOS,
|
|
31
|
+
* montados por rota (medido 07/08). Classificação é por estrutura, nunca por nome.
|
|
32
|
+
*
|
|
33
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
34
|
+
* O PERDEDOR NÃO ENTRA, E ISSO É DITO
|
|
35
|
+
*
|
|
36
|
+
* Esta fatia não traz as duas receitas - trazer os dois `Card` exige o contexto
|
|
37
|
+
* viajando na chave, que é a fatia dos GRUPOS. O que ela conserta é a perda
|
|
38
|
+
* calada: cada descarte vira uma linha do censo com os dois caminhos, e o
|
|
39
|
+
* relatório os imprime. Uma lacuna declarada é confiança; calada é bug.
|
|
40
|
+
*/
|
|
41
|
+
/** Profundidade como o caminho a declara - o número de pastas até o arquivo. */
|
|
42
|
+
const depth = (file) => file.split("/").length;
|
|
43
|
+
/**
|
|
44
|
+
* ESTE ARQUIVO FICA COM ESTE NOME?
|
|
45
|
+
*
|
|
46
|
+
* `true` quando ele detém o nome depois desta chamada - e aí quem chamou pode
|
|
47
|
+
* escrever nos mapas. `false` quando outro arquivo o detém, e a disputa foi
|
|
48
|
+
* registrada em `collisions`.
|
|
49
|
+
*
|
|
50
|
+
* Idempotente para o mesmo arquivo: o laço lê um arquivo uma vez, mas um
|
|
51
|
+
* componente pode ser reivindicado por mais de um caminho de código, e cobrar
|
|
52
|
+
* uma colisão de um arquivo contra si mesmo seria inventar um conflito.
|
|
53
|
+
*/
|
|
54
|
+
export function claimName(claims, name, file) {
|
|
55
|
+
const held = claims.by.get(name);
|
|
56
|
+
if (held === undefined) {
|
|
57
|
+
claims.by.set(name, file);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
if (held === file)
|
|
61
|
+
return true;
|
|
62
|
+
const incumbentWins = depth(held) < depth(file) ||
|
|
63
|
+
(depth(held) === depth(file) && held.localeCompare(file) <= 0);
|
|
64
|
+
if (incumbentWins) {
|
|
65
|
+
claims.collisions.push({ name, kept: held, dropped: file, by: "path" });
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
claims.by.set(name, file);
|
|
69
|
+
claims.collisions.push({ name, kept: file, dropped: held, by: "path" });
|
|
70
|
+
return true;
|
|
71
|
+
}
|
package/package.json
CHANGED