synthesisui 0.16.132 → 0.16.134
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 +75 -8
- package/dist/doctor/fragments.js +219 -0
- package/dist/doctor/style-ledger.js +146 -0
- package/dist/index.js +2 -0
- package/package.json +1 -1
package/dist/commands/import.js
CHANGED
|
@@ -16,6 +16,7 @@ import { crosswalk, floorSize, isLibrary, observedRules, useLiveCatalogue, } fro
|
|
|
16
16
|
import { keyframeOffsets, moduleImports, readModuleCss, readModuleUsage, transcribeModule, } from "../doctor/css-modules.js";
|
|
17
17
|
import { dataContract } from "../doctor/data-contract.js";
|
|
18
18
|
import { reconcile, scanDefinitions, } from "../doctor/definitions-scan.js";
|
|
19
|
+
import { fragmentsOfSource, fragmentsOfStylesheet, judgeFragments, } from "../doctor/fragments.js";
|
|
19
20
|
import { describeConvention, describeRemainder, detectConventions, } from "../doctor/idiom.js";
|
|
20
21
|
import { pageLaws } from "../doctor/page-laws.js";
|
|
21
22
|
import { callSiteNode, placeLayers, } from "../doctor/place-layers.js";
|
|
@@ -25,6 +26,7 @@ import { parseSchemeBlocks } from "../doctor/scheme-blocks.js";
|
|
|
25
26
|
import { describeSignals, emptySignals, finishSignals, readSignalsInto, } from "../doctor/signals.js";
|
|
26
27
|
import { sketchOf } from "../doctor/sketch.js";
|
|
27
28
|
import { describeForeign, readStyleIslands, } from "../doctor/style-island.js";
|
|
29
|
+
import { buildLedger, describeLedger, } from "../doctor/style-ledger.js";
|
|
28
30
|
import { buildTable } from "../doctor/tokens.js";
|
|
29
31
|
import { definitionSpan, readInlineStyle, rootClasses, rootTag, transcribe, } from "../doctor/transcribe.js";
|
|
30
32
|
import { transcribeVariants } from "../doctor/variant-read.js";
|
|
@@ -126,12 +128,28 @@ async function siblingProjects(root) {
|
|
|
126
128
|
* this" - could not be said, and we ended up rediscovering by colour distance
|
|
127
129
|
* what their stylesheet had spelled out all along.
|
|
128
130
|
*/
|
|
129
|
-
async function harvestOwnCss(root
|
|
131
|
+
async function harvestOwnCss(root,
|
|
132
|
+
/**
|
|
133
|
+
* O LEDGER TAMBÉM COME AQUI, e por um motivo de promessa: este é o único laço que vê TODO
|
|
134
|
+
* arquivo de estilo do escopo, inclusive o module de um componente que o portão não admite.
|
|
135
|
+
*
|
|
136
|
+
* O laço de componente lê o module do componente aceito, então sem isto 9 663 declarações
|
|
137
|
+
* continuariam invisíveis - e a promessa desta fatia é que nada é descartado em silêncio
|
|
138
|
+
* (dono, 04/08). O veredito de cada uma sai depois, em `judgeFragments`: pertencer a componente
|
|
139
|
+
* não admitido é um motivo, não um desaparecimento.
|
|
140
|
+
*/
|
|
141
|
+
into) {
|
|
130
142
|
let css = "";
|
|
131
143
|
for await (const file of walkAll([root])) {
|
|
132
144
|
if (!/\.(css|scss|sass|less)$/i.test(file))
|
|
133
145
|
continue;
|
|
134
|
-
|
|
146
|
+
const body = await readFile(file, "utf8").catch(() => "");
|
|
147
|
+
css += `\n${body}`;
|
|
148
|
+
if (into && body)
|
|
149
|
+
into.push({
|
|
150
|
+
file: relative(root, file),
|
|
151
|
+
fragments: fragmentsOfStylesheet(relative(root, file), body, /\.module\./.test(file)),
|
|
152
|
+
});
|
|
135
153
|
}
|
|
136
154
|
return css;
|
|
137
155
|
}
|
|
@@ -255,7 +273,9 @@ export async function takeCensus(root, opts) {
|
|
|
255
273
|
*/
|
|
256
274
|
const fetched = await fetchCatalogue(opts?.registry);
|
|
257
275
|
useLiveCatalogue(fetched.ok ? asCatalogueTable(fetched.index) : null);
|
|
258
|
-
|
|
276
|
+
/** Todo arquivo de estilo do escopo, para o ledger - ver o parâmetro `into`. */
|
|
277
|
+
const styleFiles = [];
|
|
278
|
+
const css = await harvestOwnCss(root, styleFiles);
|
|
259
279
|
const table = buildTable({ css, source: "yours" });
|
|
260
280
|
const schemes = parseSchemeBlocks(css);
|
|
261
281
|
const reports = [];
|
|
@@ -268,6 +288,10 @@ export async function takeCensus(root, opts) {
|
|
|
268
288
|
const kindOf = new Map();
|
|
269
289
|
/** A raiz e a composição de cada página, para as leis - ver `pageLaws`. */
|
|
270
290
|
const pages = [];
|
|
291
|
+
/** Todo fragmento de estilo visto, para o ledger - ver `buildLedger`. */
|
|
292
|
+
const fragments = [];
|
|
293
|
+
/** Arquivos de estilo que pertencem a componente admitido - o veredito do `css` sai daqui. */
|
|
294
|
+
const admittedStyles = new Set();
|
|
271
295
|
/** What the gate turned away, with the reason. Never dropped in silence. */
|
|
272
296
|
const skips = [];
|
|
273
297
|
// Kept for the reference check below, which needs every file at once: a name
|
|
@@ -313,9 +337,6 @@ export async function takeCensus(root, opts) {
|
|
|
313
337
|
const projectsOf = new Map();
|
|
314
338
|
/** Component → the axes its own `cva` declares. See `variant-read.ts`. */
|
|
315
339
|
const variantAxes = new Map();
|
|
316
|
-
/** How much came out of CSS Modules, for the coverage report. */
|
|
317
|
-
let moduleFiles = 0;
|
|
318
|
-
let moduleDeclarations = 0;
|
|
319
340
|
/**
|
|
320
341
|
* THE KEYFRAMES THE READING NEEDS, by name - and this was collected and dropped.
|
|
321
342
|
*
|
|
@@ -455,6 +476,11 @@ export async function takeCensus(root, opts) {
|
|
|
455
476
|
* numa lista feita para biblioteca. O look já nasce aqui, com o arquivo e a fonte na mão,
|
|
456
477
|
* e é aqui que a leitura tem que se pendurar.
|
|
457
478
|
*/
|
|
479
|
+
/**
|
|
480
|
+
* TODO FRAGMENTO DE ESTILO DESTE ARQUIVO, cego ao portão - ver `fragmentsOfSource`. É o que
|
|
481
|
+
* permite o relatório dizer o que existe e não foi lido, em vez de só o que foi.
|
|
482
|
+
*/
|
|
483
|
+
fragments.push(fragmentsOfSource(rel, src));
|
|
458
484
|
for (const d of found) {
|
|
459
485
|
sourceOf.set(d.name, { file: rel, source: src });
|
|
460
486
|
/** O que a peça É, do portão - ver `GateVerdict.kind` e `CensusLook.kind`. */
|
|
@@ -544,12 +570,16 @@ export async function takeCensus(root, opts) {
|
|
|
544
570
|
const usage = readModuleUsage(src, mod.local);
|
|
545
571
|
if (usage.length === 0)
|
|
546
572
|
continue;
|
|
573
|
+
/**
|
|
574
|
+
* ESTE MODULE PERTENCE A UM COMPONENTE ADMITIDO - o único fato que este laço tem e o
|
|
575
|
+
* ledger precisa. Os fragmentos dele vêm do `harvestOwnCss`, que vê todos os arquivos
|
|
576
|
+
* de estilo e não só os de componente aceito.
|
|
577
|
+
*/
|
|
578
|
+
admittedStyles.add(relative(root, cssPath));
|
|
547
579
|
const readModule = transcribeModule(readModuleCss(moduleCss), usage, declaredValues);
|
|
548
580
|
if (readModule.read === 0)
|
|
549
581
|
continue;
|
|
550
582
|
fromModule = readModule;
|
|
551
|
-
moduleFiles += 1;
|
|
552
|
-
moduleDeclarations += readModule.read;
|
|
553
583
|
for (const [name, frames] of Object.entries(readModule.keyframes)) {
|
|
554
584
|
const offsets = keyframeOffsets(frames);
|
|
555
585
|
if (!keyframes[name] && Object.keys(offsets).length > 0)
|
|
@@ -1152,6 +1182,13 @@ export async function takeCensus(root, opts) {
|
|
|
1152
1182
|
Object.keys(l.parts ?? {}).length === 0 &&
|
|
1153
1183
|
(l.layers ?? []).length === 0);
|
|
1154
1184
|
}).length;
|
|
1185
|
+
/**
|
|
1186
|
+
* O LEDGER, com o veredito dado pelos mesmos leitores da esteira - ver `judgeFragments`.
|
|
1187
|
+
*
|
|
1188
|
+
* Montado aqui porque é o primeiro momento em que as três coisas existem juntas: os fragmentos
|
|
1189
|
+
* vistos, os tokens declarados, e quais arquivos de estilo pertencem a componente admitido.
|
|
1190
|
+
*/
|
|
1191
|
+
const ledger = buildLedger(opts?.cli ?? "unknown", judgeFragments([...fragments.flat(), ...styleFiles.flatMap((s) => s.fragments)], declaredValues, admittedStyles));
|
|
1155
1192
|
const coverageLines = describeCoverage(coverage, composition);
|
|
1156
1193
|
if (coverageLines.length > 0) {
|
|
1157
1194
|
console.log("");
|
|
@@ -1175,6 +1212,13 @@ export async function takeCensus(root, opts) {
|
|
|
1175
1212
|
* AS LEIS DAS PÁGINAS, ditas onde a arquitetura já é dita - governam o sistema, não uma peça,
|
|
1176
1213
|
* então `applies` é vazio e elas chegam a todo prompt. Ver `pageLaws`.
|
|
1177
1214
|
*/
|
|
1215
|
+
const ledgerLines = describeLedger(ledger);
|
|
1216
|
+
if (ledgerLines.length > 0) {
|
|
1217
|
+
console.log("");
|
|
1218
|
+
console.log(section("Every style fragment, accounted for"));
|
|
1219
|
+
for (const line of ledgerLines.slice(0, 24))
|
|
1220
|
+
console.log(body(line));
|
|
1221
|
+
}
|
|
1178
1222
|
const pageRules = pageLaws(pages);
|
|
1179
1223
|
if (pageRules.length > 0) {
|
|
1180
1224
|
console.log("");
|
|
@@ -1416,6 +1460,14 @@ export async function takeCensus(root, opts) {
|
|
|
1416
1460
|
observed: distinctValues(d),
|
|
1417
1461
|
...(components.length > 0 ? { components } : {}),
|
|
1418
1462
|
...(defined.length > 0 ? { defined } : {}),
|
|
1463
|
+
/**
|
|
1464
|
+
* TODO FRAGMENTO DE ESTILO, CONTABILIZADO - ver `StyleLedger`.
|
|
1465
|
+
*
|
|
1466
|
+
* Viaja com o censo porque é a plataforma que mostra o número ao cliente, e é a partir daqui
|
|
1467
|
+
* que a skill deste repositório descobre qual forma ainda não é lida e onde ela mora, sem
|
|
1468
|
+
* ninguém ter que reportar (dono, 04/08).
|
|
1469
|
+
*/
|
|
1470
|
+
ledger,
|
|
1419
1471
|
/**
|
|
1420
1472
|
* THE SHAPE, AND THE RULE IT BECOMES.
|
|
1421
1473
|
*
|
|
@@ -1457,6 +1509,20 @@ export async function takeCensus(root, opts) {
|
|
|
1457
1509
|
skipped: skips
|
|
1458
1510
|
.slice(0, 400)
|
|
1459
1511
|
.map(({ name, why, because }) => ({ name, why, because })),
|
|
1512
|
+
/**
|
|
1513
|
+
* OS NÚMEROS VERDADEIROS DO PORTÃO, porque a lista acima é cortada em 400 e num
|
|
1514
|
+
* monorepo real o corte estoura (712 recusados, 400 gravados). Contar a lista daria
|
|
1515
|
+
* 400 e pareceria completo.
|
|
1516
|
+
*/
|
|
1517
|
+
gate: {
|
|
1518
|
+
admitted: scoped.length,
|
|
1519
|
+
refused: skips.length,
|
|
1520
|
+
groups: groupSkips(skips).map((g) => ({
|
|
1521
|
+
why: g.why,
|
|
1522
|
+
label: g.label,
|
|
1523
|
+
count: g.names.length,
|
|
1524
|
+
})),
|
|
1525
|
+
},
|
|
1460
1526
|
}
|
|
1461
1527
|
: {}),
|
|
1462
1528
|
...((opts?.usage ?? []).length > 0
|
|
@@ -2476,6 +2542,7 @@ export async function runImport(opts) {
|
|
|
2476
2542
|
census = await takeCensus(readFrom, {
|
|
2477
2543
|
...(usage.length > 0 ? { usage } : {}),
|
|
2478
2544
|
...(scope ? { scopeLabel: scope } : {}),
|
|
2545
|
+
...(opts.cli ? { cli: opts.cli } : {}),
|
|
2479
2546
|
});
|
|
2480
2547
|
if (scope)
|
|
2481
2548
|
census.scope = scope;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TODO FRAGMENTO QUE CARREGA ESTILO, com arquivo e linha.
|
|
3
|
+
*
|
|
4
|
+
* Esta é a metade que faltava para o ledger: a esteira lia estilo em cinco lugares diferentes e
|
|
5
|
+
* nenhum deles registrava o que tinha visto e não conseguido usar. O resultado é que "o que falta"
|
|
6
|
+
* era uma pergunta que só se respondia abrindo o repo do cliente à mão (dono, 04/08).
|
|
7
|
+
*
|
|
8
|
+
* A varredura aqui é DELIBERADAMENTE cega ao portão: ela vê o estilo de todo arquivo, inclusive de
|
|
9
|
+
* componente que não vai virar receita. O portão é uma decisão sobre a VITRINE, e usá-lo para
|
|
10
|
+
* decidir o que se mede é o que fez 9 663 declarações de module ficarem invisíveis.
|
|
11
|
+
*
|
|
12
|
+
* O VEREDITO NÃO É DADO AQUI. Esta função diz "vi este fragmento, desta forma, aqui"; quem sabe
|
|
13
|
+
* se ele foi lido é a interpretação, que roda depois. Juntar as duas coisas numa função seria
|
|
14
|
+
* repetir a mistura que a tabela de cobertura já paga: forma e veredito são fatos diferentes.
|
|
15
|
+
*/
|
|
16
|
+
import { readInlineStyle, transcribe } from "./transcribe.js";
|
|
17
|
+
/** `className="…"` e `className={"…"}` - o literal puro. */
|
|
18
|
+
const CLASS_LITERAL = /className=(?:"([^"]{0,400})"|\{\s*"([^"]{0,400})"\s*\})/g;
|
|
19
|
+
/** `className={`… ${x} …`}` - metade literal, metade runtime. */
|
|
20
|
+
const CLASS_TEMPLATE = /className=\{\s*`([^`]{0,400})`\s*\}/g;
|
|
21
|
+
/** `className={algo}` sem literal nenhum: não existe na fonte. */
|
|
22
|
+
const CLASS_RUNTIME = /className=\{\s*([A-Za-z_$][\w$.[\]]{0,60})\s*\}/g;
|
|
23
|
+
/** Um literal dentro de `cn("…", …)` e amigos. */
|
|
24
|
+
const CALL_LITERAL = /\b(?:cn|clsx|classNames|cx|twMerge|twJoin|classcat)\(([^)]{0,600})\)/g;
|
|
25
|
+
const STRING_IN_CALL = /"([^"]{0,300})"|'([^']{0,300})'/g;
|
|
26
|
+
/** `style={{ … }}`. */
|
|
27
|
+
const INLINE = /style=\{\{([^}]{0,400})\}\}/g;
|
|
28
|
+
/** `sx={{ … }}`. */
|
|
29
|
+
const SX = /sx=\{\{([^}]{0,400})\}\}/g;
|
|
30
|
+
/** `styled.div\`…\`` e `styled(X)\`…\``. */
|
|
31
|
+
const STYLED = /styled[.(][^`]{0,60}`([^`]{0,400})`/g;
|
|
32
|
+
/**
|
|
33
|
+
* Um pedaço de linha de estilo e o que o termina: `{` faz do pedaço um seletor, `;` e `}` fazem
|
|
34
|
+
* dele uma declaração. Ler a linha inteira de uma vez perde `.card { padding: 8px }` numa linha só,
|
|
35
|
+
* e um coletor que promete não deixar nada invisível não pode escolher a formatação do arquivo.
|
|
36
|
+
*/
|
|
37
|
+
const PIECES = /([^{};]*)([{};]|$)/g;
|
|
38
|
+
/** Uma declaração `prop: value`, já sem o terminador. */
|
|
39
|
+
const DECLARATION = /^(-{0,2}[a-zA-Z][\w-]*)\s*:\s*([^;{}]{1,200})$/;
|
|
40
|
+
/** Uma classe que só um literal pode ter: começa com letra minúscula. */
|
|
41
|
+
const LOOKS_LIKE_CLASS = /^[a-z[]/;
|
|
42
|
+
/**
|
|
43
|
+
* `${styles.card}` dentro de um template: uma classe de CSS module, cujo valor mora no arquivo de
|
|
44
|
+
* estilo - que a esteira lê. É a mesma regra que `CLASS_RUNTIME` já aplica a `className={styles.x}`.
|
|
45
|
+
*/
|
|
46
|
+
const MODULE_REF = /\$\{\s*[A-Za-z_$][\w$]*\.[A-Za-z_$][\w$]*\s*\}/g;
|
|
47
|
+
/**
|
|
48
|
+
* Os fragmentos de um arquivo `.tsx`/`.jsx`, com a linha de cada um.
|
|
49
|
+
*
|
|
50
|
+
* `read` sai FALSO em todos: quem preenche o veredito é quem tem a interpretação em mão. As formas
|
|
51
|
+
* que nenhum leitor cobre (`sx`, `styled`, `runtime`) já saem com o motivo, porque nesses casos o
|
|
52
|
+
* motivo é a própria forma e não depende de leitura.
|
|
53
|
+
*/
|
|
54
|
+
export function fragmentsOfSource(file, source) {
|
|
55
|
+
const out = [];
|
|
56
|
+
const at = (index) => source.slice(0, index).split("\n").length;
|
|
57
|
+
const push = (shape, index, text, reason) => {
|
|
58
|
+
const trimmed = text.trim();
|
|
59
|
+
if (!trimmed)
|
|
60
|
+
return;
|
|
61
|
+
out.push({
|
|
62
|
+
shape,
|
|
63
|
+
file,
|
|
64
|
+
line: at(index),
|
|
65
|
+
text: trimmed,
|
|
66
|
+
read: false,
|
|
67
|
+
...(reason ? { reason } : {}),
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
for (const m of source.matchAll(CLASS_LITERAL))
|
|
71
|
+
push("class", m.index ?? 0, m[1] ?? m[2] ?? "");
|
|
72
|
+
for (const m of source.matchAll(CLASS_TEMPLATE))
|
|
73
|
+
push("template", m.index ?? 0, m[1] ?? "");
|
|
74
|
+
for (const m of source.matchAll(CLASS_RUNTIME)) {
|
|
75
|
+
/**
|
|
76
|
+
* `className={styles.content}` é uma classe de CSS module, que a esteira LÊ - o valor está no
|
|
77
|
+
* arquivo de estilo, não aqui. Só um identificador solto é genuinamente runtime.
|
|
78
|
+
*/
|
|
79
|
+
const value = m[1] ?? "";
|
|
80
|
+
if (/^[A-Za-z_$][\w$]*\.[A-Za-z_$][\w$]*$/.test(value))
|
|
81
|
+
continue;
|
|
82
|
+
push("runtime", m.index ?? 0, `className={${value}}`, "computed");
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* OS LITERAIS DENTRO DE UMA CHAMADA. `cn("flex p-4", cond && "gap-2")` tem dois, e os dois são
|
|
86
|
+
* classe escrita à mão - a forma mais comum do repo dele (113 arquivos).
|
|
87
|
+
*/
|
|
88
|
+
for (const call of source.matchAll(CALL_LITERAL)) {
|
|
89
|
+
const body = call[1] ?? "";
|
|
90
|
+
const base = (call.index ?? 0) + (call[0].length - body.length - 1);
|
|
91
|
+
for (const s of body.matchAll(STRING_IN_CALL)) {
|
|
92
|
+
const text = s[1] ?? s[2] ?? "";
|
|
93
|
+
if (!LOOKS_LIKE_CLASS.test(text))
|
|
94
|
+
continue;
|
|
95
|
+
push("class", base + (s.index ?? 0), text);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const m of source.matchAll(INLINE))
|
|
99
|
+
push("inline", m.index ?? 0, m[1] ?? "");
|
|
100
|
+
for (const m of source.matchAll(SX))
|
|
101
|
+
push("sx", m.index ?? 0, m[1] ?? "", "shape-not-read");
|
|
102
|
+
for (const m of source.matchAll(STYLED))
|
|
103
|
+
push("styled", m.index ?? 0, m[1] ?? "", "shape-not-read");
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Os fragmentos de um arquivo de estilo: uma declaração é um fragmento.
|
|
108
|
+
*
|
|
109
|
+
* `scoped` diz se aquele arquivo é um CSS module. A distinção importa para o veredito e não para a
|
|
110
|
+
* contagem: um global com regra de CLASSE é forma que nenhum leitor cobre hoje - 399 declarações
|
|
111
|
+
* no repo dele, e a única lacuna que estava calada em vez de declarada (04/08).
|
|
112
|
+
*/
|
|
113
|
+
export function fragmentsOfStylesheet(file, css, scoped) {
|
|
114
|
+
const out = [];
|
|
115
|
+
let selector = "";
|
|
116
|
+
for (const [i, raw] of css.split("\n").entries()) {
|
|
117
|
+
const line = raw.replace(/\/\*.*?\*\//g, "");
|
|
118
|
+
for (const piece of line.matchAll(PIECES)) {
|
|
119
|
+
const text = (piece[1] ?? "").trim();
|
|
120
|
+
if (piece[2] === "{") {
|
|
121
|
+
selector = text;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Sem terminador não há declaração: `padding: 8px` no fim de uma linha pode ser a primeira
|
|
126
|
+
* metade de um valor que continua na próxima.
|
|
127
|
+
*/
|
|
128
|
+
if (!piece[2] || !text)
|
|
129
|
+
continue;
|
|
130
|
+
const m = DECLARATION.exec(text);
|
|
131
|
+
if (!m)
|
|
132
|
+
continue;
|
|
133
|
+
/**
|
|
134
|
+
* UM TOKEN NÃO É UM FRAGMENTO DE COMPONENTE. `--color-ocean-500: #…` é a paleta, e ela tem
|
|
135
|
+
* o seu próprio caminho e o seu próprio relatório; contá-la aqui inflaria o total com o que
|
|
136
|
+
* já está coberto em outro lugar.
|
|
137
|
+
*/
|
|
138
|
+
if (m[1].startsWith("--"))
|
|
139
|
+
continue;
|
|
140
|
+
out.push({
|
|
141
|
+
shape: "css",
|
|
142
|
+
file,
|
|
143
|
+
line: i + 1,
|
|
144
|
+
text: `${selector ? `${selector} ` : ""}{ ${m[1]}: ${m[2].trim()} }`,
|
|
145
|
+
read: false,
|
|
146
|
+
...(scoped ? {} : { reason: "shape-not-read" }),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* O VEREDITO DE CADA FRAGMENTO, dado pelos MESMOS leitores que a esteira usa.
|
|
154
|
+
*
|
|
155
|
+
* Não é aproximação: para uma class string, "foi lida" é literalmente `transcribe` devolver ao
|
|
156
|
+
* menos uma declaração - a mesma chamada que produz o look. Um segundo julgamento, escrito à parte,
|
|
157
|
+
* seria a maneira mais rápida de o relatório e a receita começarem a discordar.
|
|
158
|
+
*
|
|
159
|
+
* `admitted` é o conjunto de arquivos de estilo que pertencem a componente que o escopo admite. É
|
|
160
|
+
* o que separa "esta forma não é lida" de "esta forma é lida e este componente não entra" - dois
|
|
161
|
+
* fatos diferentes que, somados num número só, fizeram 36% parecer defeito de leitura quando era
|
|
162
|
+
* o portão funcionando (dono, 04/08).
|
|
163
|
+
*/
|
|
164
|
+
export function judgeFragments(seen, declared, admitted) {
|
|
165
|
+
return seen.map((f) => {
|
|
166
|
+
if (f.reason)
|
|
167
|
+
return f;
|
|
168
|
+
switch (f.shape) {
|
|
169
|
+
case "class":
|
|
170
|
+
case "template": {
|
|
171
|
+
/**
|
|
172
|
+
* UMA INTERPOLAÇÃO DE MODULE NÃO É UMA LACUNA. `` `${styles.card} ${styles.open}` `` é
|
|
173
|
+
* classe de CSS module, e o valor dela está no arquivo de estilo - que a esteira lê. São
|
|
174
|
+
* 262 dos 277 templates não lidos do dashboard dele (95%), e reportá-los como valor não
|
|
175
|
+
* lido inventa uma lacuna onde a captura está completa por outra forma. Um relatório com
|
|
176
|
+
* lacuna falsa custa o mesmo que um com lacuna escondida: manda alguém consertar o que
|
|
177
|
+
* não está quebrado (medido, 04/08).
|
|
178
|
+
*/
|
|
179
|
+
const modules = f.text.match(MODULE_REF) ?? [];
|
|
180
|
+
const literal = f.text
|
|
181
|
+
.replace(MODULE_REF, " ")
|
|
182
|
+
.replace(/\$\{[^}]*\}/g, " ");
|
|
183
|
+
/**
|
|
184
|
+
* Sem literal nenhum: ou o module responde por tudo, ou o que sobrou é `${className}` -
|
|
185
|
+
* um valor que só existe em tempo de execução, e o motivo disso é `computed`, nunca
|
|
186
|
+
* "valor não lido". A esteira não viu valor algum para não ler.
|
|
187
|
+
*/
|
|
188
|
+
if (f.shape === "template" && !/[a-z]/i.test(literal))
|
|
189
|
+
return modules.length > 0
|
|
190
|
+
? { ...f, read: true }
|
|
191
|
+
: { ...f, read: false, reason: "computed" };
|
|
192
|
+
const t = transcribe(literal.split(/\s+/).filter(Boolean), declared);
|
|
193
|
+
const read = Object.keys(t.base).length +
|
|
194
|
+
Object.keys(t.dark).length +
|
|
195
|
+
Object.values(t.states ?? {}).reduce((n, b) => n + Object.keys(b).length, 0) >
|
|
196
|
+
0;
|
|
197
|
+
return read
|
|
198
|
+
? { ...f, read: true }
|
|
199
|
+
: { ...f, read: false, reason: "value-not-read" };
|
|
200
|
+
}
|
|
201
|
+
case "inline": {
|
|
202
|
+
const read = Object.keys(readInlineStyle(`style={{${f.text}}}`)).length > 0;
|
|
203
|
+
return read
|
|
204
|
+
? { ...f, read: true }
|
|
205
|
+
: { ...f, read: false, reason: "computed" };
|
|
206
|
+
}
|
|
207
|
+
case "css":
|
|
208
|
+
return admitted.has(f.file)
|
|
209
|
+
? { ...f, read: true }
|
|
210
|
+
: {
|
|
211
|
+
...f,
|
|
212
|
+
read: false,
|
|
213
|
+
reason: "component-not-admitted",
|
|
214
|
+
};
|
|
215
|
+
default:
|
|
216
|
+
return { ...f, read: false, reason: "shape-not-read" };
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* O LEDGER DE ESTILO: todo fragmento que carrega aparência, contabilizado.
|
|
3
|
+
*
|
|
4
|
+
* A esteira sempre soube dizer o que ela LÊ. O que ela nunca soube dizer é o que ela viu e não
|
|
5
|
+
* leu, onde aquilo mora, e quanto disso existe - e sem isso não existe ciclo de conserto: alguém
|
|
6
|
+
* precisa reportar, e um cliente que importou uma vez e foi embora nunca reporta (dono, 04/08).
|
|
7
|
+
*
|
|
8
|
+
* Medido no dashboard dele: 20 541 fragmentos que carregam estilo, 3,0 MB se guardados verbatim -
|
|
9
|
+
* do mesmo tamanho da fonte inteira. Guardar tudo seria dobrar o censo para reter o que a
|
|
10
|
+
* interpretação já extraiu.
|
|
11
|
+
*
|
|
12
|
+
* ENTÃO A CONTABILIDADE É ESTA, e ela é o que "sem perda" significa aqui:
|
|
13
|
+
*
|
|
14
|
+
* contado todo fragmento entra num total, por forma. Nada é invisível.
|
|
15
|
+
* interpretado o valor dele já vive no look, que é de onde a receita lê
|
|
16
|
+
* não lido o TEXTO vem verbatim, com arquivo e linha, agrupado por forma e motivo
|
|
17
|
+
*
|
|
18
|
+
* Um fragmento interpretado não precisa do texto original, porque a gente tem a interpretação; um
|
|
19
|
+
* não lido só serve com o texto e o lugar. O resultado pesa quilobytes, é acionável linha por
|
|
20
|
+
* linha, e nunca guarda o arquivo inteiro de ninguém - que é a resposta que a gente quer poder
|
|
21
|
+
* dar, olhando o schema, quando um cliente perguntar se guardamos o código dele.
|
|
22
|
+
*
|
|
23
|
+
* O QUE ELE NÃO PROMETE: 100% de interpretação. `className={cx(getVariant(props))}` monta a
|
|
24
|
+
* classe em tempo de execução e ela não existe na fonte, em nenhum nível. A promessa é 100%
|
|
25
|
+
* CONTABILIZADO - todo fragmento ou interpretado ou listado com motivo, arquivo e linha - com a
|
|
26
|
+
* interpretação como um número que se move. Dizer 100% de cobertura existindo uma classe
|
|
27
|
+
* calculada em runtime seria exatamente a mentira que esta esteira existe para não contar.
|
|
28
|
+
*/
|
|
29
|
+
const SHAPES = [
|
|
30
|
+
"class",
|
|
31
|
+
"template",
|
|
32
|
+
"inline",
|
|
33
|
+
"css",
|
|
34
|
+
"sx",
|
|
35
|
+
"styled",
|
|
36
|
+
"runtime",
|
|
37
|
+
];
|
|
38
|
+
/**
|
|
39
|
+
* Como cada forma se chama no terminal. É cópia do RELATÓRIO, não do contrato: a tela da
|
|
40
|
+
* plataforma escreve a sua própria (o dado que viaja é a chave da forma), porque uma linha de
|
|
41
|
+
* terminal alinhada em coluna e um rótulo de painel não são o mesmo texto.
|
|
42
|
+
*/
|
|
43
|
+
const SHAPE_LABEL = {
|
|
44
|
+
class: "class lists",
|
|
45
|
+
template: "template strings",
|
|
46
|
+
inline: "style={{ }}",
|
|
47
|
+
css: "stylesheet declarations",
|
|
48
|
+
sx: "sx props",
|
|
49
|
+
styled: "styled-components",
|
|
50
|
+
runtime: "classes built at runtime",
|
|
51
|
+
};
|
|
52
|
+
const BECAUSE = {
|
|
53
|
+
"shape-not-read": "no reader in this version understands this shape, so the styles it holds reach no recipe",
|
|
54
|
+
"value-not-read": "the shape is read and this value is not - a utility or property outside the scales this project declares",
|
|
55
|
+
computed: "the value is assembled at render time, so it is not in the source at any level and there is nothing to read",
|
|
56
|
+
"component-not-admitted": "read, and it belongs to a component this scope does not admit into the system - the styles exist and no recipe claims them",
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* O ledger de uma varredura, a partir dos fragmentos vistos.
|
|
60
|
+
*
|
|
61
|
+
* Puro: quem chama junta os fragmentos onde já abre os arquivos, e isto só conta e agrupa. O
|
|
62
|
+
* texto de um fragmento LIDO é descartado aqui de propósito - a interpretação dele já vive no
|
|
63
|
+
* look, e reter as duas cópias é o que dobraria o censo.
|
|
64
|
+
*/
|
|
65
|
+
export function buildLedger(cli, seen) {
|
|
66
|
+
const counted = blank();
|
|
67
|
+
const interpreted = blank();
|
|
68
|
+
const groups = new Map();
|
|
69
|
+
for (const f of seen) {
|
|
70
|
+
counted[f.shape] += 1;
|
|
71
|
+
if (f.read) {
|
|
72
|
+
interpreted[f.shape] += 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const reason = f.reason ?? "shape-not-read";
|
|
76
|
+
const key = `${f.shape}:${reason}`;
|
|
77
|
+
const group = groups.get(key) ?? {
|
|
78
|
+
shape: f.shape,
|
|
79
|
+
reason,
|
|
80
|
+
uses: 0,
|
|
81
|
+
files: 0,
|
|
82
|
+
because: BECAUSE[reason],
|
|
83
|
+
examples: [],
|
|
84
|
+
seenFiles: new Set(),
|
|
85
|
+
};
|
|
86
|
+
group.uses += 1;
|
|
87
|
+
group.seenFiles.add(f.file);
|
|
88
|
+
if (group.examples.length < 3 &&
|
|
89
|
+
!group.examples.some((e) => e.file === f.file))
|
|
90
|
+
group.examples.push({
|
|
91
|
+
file: f.file,
|
|
92
|
+
line: f.line,
|
|
93
|
+
text: f.text.replace(/\s+/g, " ").trim().slice(0, 160),
|
|
94
|
+
});
|
|
95
|
+
groups.set(key, group);
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
cli,
|
|
99
|
+
counted,
|
|
100
|
+
interpreted,
|
|
101
|
+
unread: [...groups.values()]
|
|
102
|
+
.map(({ seenFiles, ...group }) => ({ ...group, files: seenFiles.size }))
|
|
103
|
+
.sort((a, b) => b.uses - a.uses),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function blank() {
|
|
107
|
+
return Object.fromEntries(SHAPES.map((s) => [s, 0]));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* O relatório, na ordem em que uma pessoa decide se aquilo a incomoda.
|
|
111
|
+
*
|
|
112
|
+
* Lidera com o que foi CONTABILIZADO, porque é a única promessa que a esteira pode cumprir, e
|
|
113
|
+
* nomeia cada lacuna com o número do repo dela. Um percentual sem a contagem atrás é número de
|
|
114
|
+
* marketing, e um zero sem motivo lê como falha nossa.
|
|
115
|
+
*/
|
|
116
|
+
export function describeLedger(l) {
|
|
117
|
+
const total = sum(l.counted);
|
|
118
|
+
if (total === 0)
|
|
119
|
+
return [];
|
|
120
|
+
const read = sum(l.interpreted);
|
|
121
|
+
const lines = [
|
|
122
|
+
`${total} style fragments in your files, and every one is accounted for: ${read} interpreted (${Math.round((read / total) * 100)}%), ${total - read} listed below with the file and line where each lives.`,
|
|
123
|
+
];
|
|
124
|
+
/**
|
|
125
|
+
* POR FORMA, ANTES DOS GRUPOS. O total responde "quanto"; esta tabela responde "de que jeito o
|
|
126
|
+
* projeto escreve estilo", que é a pergunta que decide qual leitor vale construir. Sem ela, 56%
|
|
127
|
+
* num app dominado por CSS Modules e 56% num app de Tailwind leem igual e pedem trabalhos
|
|
128
|
+
* opostos.
|
|
129
|
+
*/
|
|
130
|
+
for (const shape of SHAPES) {
|
|
131
|
+
const seen = l.counted[shape];
|
|
132
|
+
if (seen === 0)
|
|
133
|
+
continue;
|
|
134
|
+
const got = l.interpreted[shape];
|
|
135
|
+
lines.push(` ${SHAPE_LABEL[shape].padEnd(28)} ${String(seen).padStart(6)} seen ${String(got).padStart(6)} interpreted${got === seen ? "" : ` (${seen - got} listed below)`}`);
|
|
136
|
+
}
|
|
137
|
+
for (const g of l.unread) {
|
|
138
|
+
lines.push(` ${g.uses} ${g.shape}${g.uses === 1 ? "" : "s"} in ${g.files} file${g.files === 1 ? "" : "s"} - ${g.because}`);
|
|
139
|
+
for (const e of g.examples)
|
|
140
|
+
lines.push(` ${e.file}:${e.line} ${e.text.slice(0, 80)}`);
|
|
141
|
+
}
|
|
142
|
+
return lines;
|
|
143
|
+
}
|
|
144
|
+
function sum(counts) {
|
|
145
|
+
return Object.values(counts).reduce((n, k) => n + k, 0);
|
|
146
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -180,6 +180,8 @@ async function main() {
|
|
|
180
180
|
// The census is arithmetic over their files; --dry keeps it on disk.
|
|
181
181
|
await runImport({
|
|
182
182
|
root: dir,
|
|
183
|
+
/** A versão que mediu, para o ledger - ver `StyleLedger.cli`. */
|
|
184
|
+
cli: CLI_VERSION,
|
|
183
185
|
dry: flags.dry === true,
|
|
184
186
|
census: typeof flags.census === "string" ? flags.census : undefined,
|
|
185
187
|
name: typeof flags.name === "string" ? flags.name : undefined,
|
package/package.json
CHANGED