synthesisui 0.16.131 → 0.16.133
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 +126 -11
- package/dist/doctor/component-gate.js +25 -1
- package/dist/doctor/fragments.js +219 -0
- package/dist/doctor/page-laws.js +106 -0
- package/dist/doctor/style-ledger.js +119 -0
- package/dist/index.js +2 -0
- package/package.json +1 -1
package/dist/commands/import.js
CHANGED
|
@@ -16,7 +16,9 @@ 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";
|
|
21
|
+
import { pageLaws } from "../doctor/page-laws.js";
|
|
20
22
|
import { callSiteNode, placeLayers, } from "../doctor/place-layers.js";
|
|
21
23
|
import { publicApi, requiredProps } from "../doctor/public-api.js";
|
|
22
24
|
import { diagnose, scanSource } from "../doctor/scan.js";
|
|
@@ -24,6 +26,7 @@ import { parseSchemeBlocks } from "../doctor/scheme-blocks.js";
|
|
|
24
26
|
import { describeSignals, emptySignals, finishSignals, readSignalsInto, } from "../doctor/signals.js";
|
|
25
27
|
import { sketchOf } from "../doctor/sketch.js";
|
|
26
28
|
import { describeForeign, readStyleIslands, } from "../doctor/style-island.js";
|
|
29
|
+
import { buildLedger, describeLedger, } from "../doctor/style-ledger.js";
|
|
27
30
|
import { buildTable } from "../doctor/tokens.js";
|
|
28
31
|
import { definitionSpan, readInlineStyle, rootClasses, rootTag, transcribe, } from "../doctor/transcribe.js";
|
|
29
32
|
import { transcribeVariants } from "../doctor/variant-read.js";
|
|
@@ -125,12 +128,28 @@ async function siblingProjects(root) {
|
|
|
125
128
|
* this" - could not be said, and we ended up rediscovering by colour distance
|
|
126
129
|
* what their stylesheet had spelled out all along.
|
|
127
130
|
*/
|
|
128
|
-
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) {
|
|
129
142
|
let css = "";
|
|
130
143
|
for await (const file of walkAll([root])) {
|
|
131
144
|
if (!/\.(css|scss|sass|less)$/i.test(file))
|
|
132
145
|
continue;
|
|
133
|
-
|
|
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
|
+
});
|
|
134
153
|
}
|
|
135
154
|
return css;
|
|
136
155
|
}
|
|
@@ -254,7 +273,9 @@ export async function takeCensus(root, opts) {
|
|
|
254
273
|
*/
|
|
255
274
|
const fetched = await fetchCatalogue(opts?.registry);
|
|
256
275
|
useLiveCatalogue(fetched.ok ? asCatalogueTable(fetched.index) : null);
|
|
257
|
-
|
|
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);
|
|
258
279
|
const table = buildTable({ css, source: "yours" });
|
|
259
280
|
const schemes = parseSchemeBlocks(css);
|
|
260
281
|
const reports = [];
|
|
@@ -263,6 +284,14 @@ export async function takeCensus(root, opts) {
|
|
|
263
284
|
const defined = [];
|
|
264
285
|
/** Nome → arquivo e fonte, para toda leitura por componente - ver o preenchimento abaixo. */
|
|
265
286
|
const sourceOf = new Map();
|
|
287
|
+
/** Nome → o que a peça é, quando o portão soube dizer. Ver `CensusLook.kind`. */
|
|
288
|
+
const kindOf = new Map();
|
|
289
|
+
/** A raiz e a composição de cada página, para as leis - ver `pageLaws`. */
|
|
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();
|
|
266
295
|
/** What the gate turned away, with the reason. Never dropped in silence. */
|
|
267
296
|
const skips = [];
|
|
268
297
|
// Kept for the reference check below, which needs every file at once: a name
|
|
@@ -308,9 +337,6 @@ export async function takeCensus(root, opts) {
|
|
|
308
337
|
const projectsOf = new Map();
|
|
309
338
|
/** Component → the axes its own `cva` declares. See `variant-read.ts`. */
|
|
310
339
|
const variantAxes = new Map();
|
|
311
|
-
/** How much came out of CSS Modules, for the coverage report. */
|
|
312
|
-
let moduleFiles = 0;
|
|
313
|
-
let moduleDeclarations = 0;
|
|
314
340
|
/**
|
|
315
341
|
* THE KEYFRAMES THE READING NEEDS, by name - and this was collected and dropped.
|
|
316
342
|
*
|
|
@@ -391,9 +417,33 @@ export async function takeCensus(root, opts) {
|
|
|
391
417
|
internal,
|
|
392
418
|
});
|
|
393
419
|
if (verdict.ok) {
|
|
394
|
-
|
|
420
|
+
/**
|
|
421
|
+
* O QUE A PEÇA É viaja com ela - ver `GateVerdict.kind`. Um template decide arranjo e
|
|
422
|
+
* não aparência, e três superfícies precisam saber disso para não cobrar cor de quem
|
|
423
|
+
* nunca prometeu cor.
|
|
424
|
+
*/
|
|
425
|
+
found.push(verdict.kind ? { ...d, kind: verdict.kind } : d);
|
|
395
426
|
continue;
|
|
396
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* A RAIZ DE UMA PÁGINA E O QUE ELA COMPÕE, para as leis - ver `pageLaws`.
|
|
430
|
+
*
|
|
431
|
+
* Uma rota individualmente não vale receita: 5 elementos de média e a raiz repetindo o
|
|
432
|
+
* que outras oito já disseram. Juntas elas dizem como é a casca deste projeto, que é o
|
|
433
|
+
* que falta para alguém escrever a próxima (04/08).
|
|
434
|
+
*/
|
|
435
|
+
if (verdict.why === "route" || verdict.why === "screen") {
|
|
436
|
+
const sketch = sketchOf(src, d.name);
|
|
437
|
+
const root = sketch[0];
|
|
438
|
+
pages.push({
|
|
439
|
+
root: root?.classes
|
|
440
|
+
? transcribe(root.classes.split(/\s+/).filter(Boolean), declaredValues).base
|
|
441
|
+
: {},
|
|
442
|
+
composes: sketch
|
|
443
|
+
.filter((n) => /^[A-Z]/.test(n.tag))
|
|
444
|
+
.map((n) => n.tag.split(".")[0]),
|
|
445
|
+
});
|
|
446
|
+
}
|
|
397
447
|
skips.push({
|
|
398
448
|
name: d.name,
|
|
399
449
|
file: rel,
|
|
@@ -426,8 +476,17 @@ export async function takeCensus(root, opts) {
|
|
|
426
476
|
* numa lista feita para biblioteca. O look já nasce aqui, com o arquivo e a fonte na mão,
|
|
427
477
|
* e é aqui que a leitura tem que se pendurar.
|
|
428
478
|
*/
|
|
429
|
-
|
|
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));
|
|
484
|
+
for (const d of found) {
|
|
430
485
|
sourceOf.set(d.name, { file: rel, source: src });
|
|
486
|
+
/** O que a peça É, do portão - ver `GateVerdict.kind` e `CensusLook.kind`. */
|
|
487
|
+
if (d.kind)
|
|
488
|
+
kindOf.set(d.name, d.kind);
|
|
489
|
+
}
|
|
431
490
|
// THE LOOK, from their own class names. One transcription per file, keyed
|
|
432
491
|
// by the component it defines - the root element's classes are the
|
|
433
492
|
// component's own look, and everything below it is a part this slice does
|
|
@@ -511,12 +570,16 @@ export async function takeCensus(root, opts) {
|
|
|
511
570
|
const usage = readModuleUsage(src, mod.local);
|
|
512
571
|
if (usage.length === 0)
|
|
513
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));
|
|
514
579
|
const readModule = transcribeModule(readModuleCss(moduleCss), usage, declaredValues);
|
|
515
580
|
if (readModule.read === 0)
|
|
516
581
|
continue;
|
|
517
582
|
fromModule = readModule;
|
|
518
|
-
moduleFiles += 1;
|
|
519
|
-
moduleDeclarations += readModule.read;
|
|
520
583
|
for (const [name, frames] of Object.entries(readModule.keyframes)) {
|
|
521
584
|
const offsets = keyframeOffsets(frames);
|
|
522
585
|
if (!keyframes[name] && Object.keys(offsets).length > 0)
|
|
@@ -733,6 +796,18 @@ export async function takeCensus(root, opts) {
|
|
|
733
796
|
if (hasScreens) {
|
|
734
797
|
const keep = [];
|
|
735
798
|
for (const d of defined) {
|
|
799
|
+
/**
|
|
800
|
+
* UM TEMPLATE VIVE NUMA TELA SÓ POR DEFINIÇÃO - ele É a tela.
|
|
801
|
+
*
|
|
802
|
+
* A regra de duas telas existe para separar "decisão que o produto compartilha" de
|
|
803
|
+
* "markup daquela tela", e está certa para uma PEÇA. Aplicada a um template ela recusa
|
|
804
|
+
* exatamente o que o portão acabou de aceitar: as 16 telas do dashboard dele passavam o
|
|
805
|
+
* portão como `template` e caíam aqui, aparecendo no relatório como `feature` (04/08).
|
|
806
|
+
*/
|
|
807
|
+
if (d.kind === "template") {
|
|
808
|
+
keep.push(d);
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
736
811
|
const screens = screensOf.get(d.name)?.size ?? 0;
|
|
737
812
|
if (screens >= SCREENS_FOR_SYSTEM) {
|
|
738
813
|
keep.push(d);
|
|
@@ -1107,6 +1182,13 @@ export async function takeCensus(root, opts) {
|
|
|
1107
1182
|
Object.keys(l.parts ?? {}).length === 0 &&
|
|
1108
1183
|
(l.layers ?? []).length === 0);
|
|
1109
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));
|
|
1110
1192
|
const coverageLines = describeCoverage(coverage, composition);
|
|
1111
1193
|
if (coverageLines.length > 0) {
|
|
1112
1194
|
console.log("");
|
|
@@ -1126,6 +1208,26 @@ export async function takeCensus(root, opts) {
|
|
|
1126
1208
|
* The skill asks which has priority; the chosen one becomes a system-wide rule that
|
|
1127
1209
|
* reaches CLAUDE.md, so every prompt after this respects where a new component goes.
|
|
1128
1210
|
*/
|
|
1211
|
+
/**
|
|
1212
|
+
* AS LEIS DAS PÁGINAS, ditas onde a arquitetura já é dita - governam o sistema, não uma peça,
|
|
1213
|
+
* então `applies` é vazio e elas chegam a todo prompt. Ver `pageLaws`.
|
|
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
|
+
}
|
|
1222
|
+
const pageRules = pageLaws(pages);
|
|
1223
|
+
if (pageRules.length > 0) {
|
|
1224
|
+
console.log("");
|
|
1225
|
+
console.log(section("What your pages agree on"));
|
|
1226
|
+
for (const law of pageRules) {
|
|
1227
|
+
console.log(body(law.text));
|
|
1228
|
+
console.log(body(paint.faint(` ${law.evidence}`)));
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1129
1231
|
if (architectures.length > 0) {
|
|
1130
1232
|
console.log("");
|
|
1131
1233
|
console.log(section("How this project is organised"));
|
|
@@ -1332,8 +1434,12 @@ export async function takeCensus(root, opts) {
|
|
|
1332
1434
|
if (c)
|
|
1333
1435
|
c.api = stamped;
|
|
1334
1436
|
const look = looks[name];
|
|
1335
|
-
if (look)
|
|
1437
|
+
if (look) {
|
|
1336
1438
|
look.api = stamped;
|
|
1439
|
+
const kind = kindOf.get(name);
|
|
1440
|
+
if (kind)
|
|
1441
|
+
look.kind = kind;
|
|
1442
|
+
}
|
|
1337
1443
|
}
|
|
1338
1444
|
const brokenRefs = findBrokenRefs(sources, declaredNames);
|
|
1339
1445
|
const conventions = detectConventions(sources);
|
|
@@ -1354,6 +1460,14 @@ export async function takeCensus(root, opts) {
|
|
|
1354
1460
|
observed: distinctValues(d),
|
|
1355
1461
|
...(components.length > 0 ? { components } : {}),
|
|
1356
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,
|
|
1357
1471
|
/**
|
|
1358
1472
|
* THE SHAPE, AND THE RULE IT BECOMES.
|
|
1359
1473
|
*
|
|
@@ -2414,6 +2528,7 @@ export async function runImport(opts) {
|
|
|
2414
2528
|
census = await takeCensus(readFrom, {
|
|
2415
2529
|
...(usage.length > 0 ? { usage } : {}),
|
|
2416
2530
|
...(scope ? { scopeLabel: scope } : {}),
|
|
2531
|
+
...(opts.cli ? { cli: opts.cli } : {}),
|
|
2417
2532
|
});
|
|
2418
2533
|
if (scope)
|
|
2419
2534
|
census.scope = scope;
|
|
@@ -43,6 +43,19 @@ const RESERVED_FILE_OTHER = /(^|[/\\])(\+page|\+layout|\+server|_app|_document|_
|
|
|
43
43
|
* hundred libraries. It only counts when the file itself is a route.
|
|
44
44
|
*/
|
|
45
45
|
const SCREEN_SUFFIX = /(Page|Screen|Route|Template|Layout)$/;
|
|
46
|
+
/**
|
|
47
|
+
* Quantos elementos uma tela tem e quantos deles são peça de alguém - o que separa um template
|
|
48
|
+
* de uma casca. Contado no arquivo, sem sketch: este portão roda antes de qualquer leitura.
|
|
49
|
+
*/
|
|
50
|
+
function screenShape(source, name) {
|
|
51
|
+
const at = source.indexOf(name);
|
|
52
|
+
const body = at === -1 ? source : source.slice(at);
|
|
53
|
+
const tags = [...body.matchAll(/<([A-Za-z][\w.]*)/g)].map((m) => m[1]);
|
|
54
|
+
return {
|
|
55
|
+
nodes: tags.length,
|
|
56
|
+
composes: new Set(tags.filter((t) => /^[A-Z]/.test(t)).map((t) => t.split(".")[0])).size,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
46
59
|
/** Something that carries context down the tree rather than drawing. */
|
|
47
60
|
const PROVIDER_SUFFIX = /(Provider|Providers|Context|Boundary|Guard|Wrapper)$/;
|
|
48
61
|
/** Any JSX at all in the file. A capitalised export with none is a constant, a
|
|
@@ -174,11 +187,22 @@ export function gateComponent(input) {
|
|
|
174
187
|
because: `\`${file}\` is a file name the framework reserves - it is a route, not a component somebody can import`,
|
|
175
188
|
};
|
|
176
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* UMA TELA ENTRA COMO TEMPLATE, e o que decide é o que ela FAZ.
|
|
192
|
+
*
|
|
193
|
+
* O nome é a pista (`…Template`, `…Screen`, `…Page`) e não a prova: uma tela que compõe três
|
|
194
|
+
* ou mais peças numa árvore de dez ou mais nós é uma decisão de arranjo que vale receita -
|
|
195
|
+
* `CurateTemplate` tem 89 nós e 237 declarações. Uma que não alcança isso é casca, e casca é
|
|
196
|
+
* lei, não peça.
|
|
197
|
+
*/
|
|
177
198
|
if (SCREEN_SUFFIX.test(name)) {
|
|
199
|
+
const shape = screenShape(source, name);
|
|
200
|
+
if (shape.composes >= 3 && shape.nodes >= 10)
|
|
201
|
+
return { ok: true, kind: "template" };
|
|
178
202
|
return {
|
|
179
203
|
ok: false,
|
|
180
204
|
why: "screen",
|
|
181
|
-
because: `\`${name}\` names a
|
|
205
|
+
because: `\`${name}\` names a screen, and composes ${shape.composes} piece${shape.composes === 1 ? "" : "s"} across ${shape.nodes} element${shape.nodes === 1 ? "" : "s"} - too thin to be a template of yours. A design system is made of the pieces a screen is built FROM`,
|
|
182
206
|
};
|
|
183
207
|
}
|
|
184
208
|
if (PROVIDER_SUFFIX.test(name) || IS_CONTEXT.test(source)) {
|
|
@@ -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,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* O QUE AS PÁGINAS DE UM APP CONCORDAM, quando nenhuma delas vale uma receita.
|
|
3
|
+
*
|
|
4
|
+
* As 129 rotas do dashboard dele têm média de 5 elementos e 2 peças compostas, e a raiz de uma
|
|
5
|
+
* repete o que outras oito já disseram. Cada uma individualmente não tem o que uma receita
|
|
6
|
+
* carrega - virar 129 entradas na vitrine seria o problema de escala sem o benefício.
|
|
7
|
+
*
|
|
8
|
+
* Mas juntas elas dizem uma coisa que hoje não existe em lugar nenhum do sistema. Medido nas 129
|
|
9
|
+
* (04/08):
|
|
10
|
+
*
|
|
11
|
+
* width: 100% 12 páginas
|
|
12
|
+
* position: sticky 8 ┐
|
|
13
|
+
* top: 0 8 ├ juntos: o cabeçalho fixo de página
|
|
14
|
+
* z-index: 50 8 ┘
|
|
15
|
+
* padding: 1rem 8
|
|
16
|
+
* min-height: 100vh 6 a casca da página
|
|
17
|
+
* ContentWrapper 22 páginas compõem
|
|
18
|
+
* PageHeader 14
|
|
19
|
+
*
|
|
20
|
+
* Três ou mais arquivos concordando é hábito - a mesma escada que o resto da esteira usa. E uma
|
|
21
|
+
* lei com a contagem atrás vale mais que a receita de qualquer uma das 129, porque é isso que
|
|
22
|
+
* alguém precisa saber para escrever a 130ª.
|
|
23
|
+
*
|
|
24
|
+
* O QUE ELA NUNCA FAZ: propor. Uma lei aqui é a descrição do que eles já fazem, com o número, e
|
|
25
|
+
* o que NÃO se repete não vira lei - duas páginas concordando é coincidência, e chamar isso de
|
|
26
|
+
* convenção do projeto seria inventar uma decisão que ninguém tomou.
|
|
27
|
+
*/
|
|
28
|
+
/** O mínimo para um hábito, e é o mesmo número que o resto da esteira usa. */
|
|
29
|
+
const AGREE = 3;
|
|
30
|
+
/** Propriedades que descrevem a MOLDURA de uma página, e não a aparência de uma peça. */
|
|
31
|
+
const SHELL = /^(minHeight|height|width|maxWidth|padding|paddingTop|paddingBottom|margin|marginTop|display|gridTemplateColumns|gap|position|top|zIndex|overflow|justifyContent|alignItems)$/;
|
|
32
|
+
/**
|
|
33
|
+
* As leis que as páginas de um escopo concordam, ou nada quando não concordam.
|
|
34
|
+
*
|
|
35
|
+
* `pages` é uma entrada por página: o que a RAIZ dela declara e quais peças ela compõe. Quem
|
|
36
|
+
* chama já leu as duas coisas para o relatório do portão, então isto não abre nenhum arquivo.
|
|
37
|
+
*/
|
|
38
|
+
export function pageLaws(pages) {
|
|
39
|
+
if (pages.length < AGREE)
|
|
40
|
+
return [];
|
|
41
|
+
const values = new Map();
|
|
42
|
+
const composed = new Map();
|
|
43
|
+
for (const page of pages) {
|
|
44
|
+
for (const [prop, value] of Object.entries(page.root)) {
|
|
45
|
+
if (!SHELL.test(prop))
|
|
46
|
+
continue;
|
|
47
|
+
const key = `${prop}: ${value}`;
|
|
48
|
+
values.set(key, (values.get(key) ?? 0) + 1);
|
|
49
|
+
}
|
|
50
|
+
for (const name of new Set(page.composes))
|
|
51
|
+
composed.set(name, (composed.get(name) ?? 0) + 1);
|
|
52
|
+
}
|
|
53
|
+
const agreed = [...values.entries()]
|
|
54
|
+
.filter(([, n]) => n >= AGREE)
|
|
55
|
+
.sort((a, b) => b[1] - a[1]);
|
|
56
|
+
const laws = [];
|
|
57
|
+
/**
|
|
58
|
+
* A CASCA: o que a raiz de uma página deste projeto declara. Uma altura mínima de viewport e um
|
|
59
|
+
* padding de container não são a aparência de uma peça - são a moldura, e é o que falta a quem
|
|
60
|
+
* vai escrever a próxima página.
|
|
61
|
+
*/
|
|
62
|
+
const shell = agreed.filter(([key]) => !/^(position|top|zIndex):/.test(key));
|
|
63
|
+
if (shell.length > 0) {
|
|
64
|
+
laws.push({
|
|
65
|
+
text: `A page of this project is ${shell
|
|
66
|
+
.slice(0, 4)
|
|
67
|
+
.map(([key]) => `\`${key}\``)
|
|
68
|
+
.join(", ")}` +
|
|
69
|
+
`${composed.size > 0 && (composed.get(topOf(composed)) ?? 0) >= AGREE ? ` and puts its content inside \`${topOf(composed)}\`` : ""}` +
|
|
70
|
+
`. Write the next one the same way, or change all of them.`,
|
|
71
|
+
applies: [],
|
|
72
|
+
kind: "implementation",
|
|
73
|
+
evidence: shell
|
|
74
|
+
.slice(0, 4)
|
|
75
|
+
.map(([key, n]) => `${key} in ${n} of ${pages.length} pages`)
|
|
76
|
+
.join("; "),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* O CABEÇALHO FIXO: os três juntos são um padrão, e separados não dizem nada. Só vale como lei
|
|
81
|
+
* quando os três concordam, porque `position: sticky` sozinho pode ser qualquer coisa.
|
|
82
|
+
*/
|
|
83
|
+
const sticky = agreed.filter(([key]) => /^(position: sticky|top: 0|zIndex:)/.test(key));
|
|
84
|
+
if (sticky.length >= 3) {
|
|
85
|
+
const header = [...composed.entries()]
|
|
86
|
+
.filter(([name, n]) => n >= AGREE && /header/i.test(name))
|
|
87
|
+
.sort((a, b) => b[1] - a[1])[0];
|
|
88
|
+
laws.push({
|
|
89
|
+
text: `The header of a page is ${sticky
|
|
90
|
+
.map(([key]) => `\`${key}\``)
|
|
91
|
+
.join(", ")}` +
|
|
92
|
+
`${header ? ` and it is \`${header[0]}\`` : ""}` +
|
|
93
|
+
`. A second sticky element at another z-index is how two headers end up fighting.`,
|
|
94
|
+
applies: [],
|
|
95
|
+
kind: "implementation",
|
|
96
|
+
evidence: sticky
|
|
97
|
+
.map(([key, n]) => `${key} in ${n} of ${pages.length} pages`)
|
|
98
|
+
.join("; "),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return laws;
|
|
102
|
+
}
|
|
103
|
+
/** O nome mais composto, que é o candidato a "onde o conteúdo vai". */
|
|
104
|
+
function topOf(composed) {
|
|
105
|
+
return [...composed.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? "";
|
|
106
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
const BECAUSE = {
|
|
39
|
+
"shape-not-read": "no reader in this version understands this shape, so the styles it holds reach no recipe",
|
|
40
|
+
"value-not-read": "the shape is read and this value is not - a utility or property outside the scales this project declares",
|
|
41
|
+
computed: "the value is assembled at render time, so it is not in the source at any level and there is nothing to read",
|
|
42
|
+
"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",
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* O ledger de uma varredura, a partir dos fragmentos vistos.
|
|
46
|
+
*
|
|
47
|
+
* Puro: quem chama junta os fragmentos onde já abre os arquivos, e isto só conta e agrupa. O
|
|
48
|
+
* texto de um fragmento LIDO é descartado aqui de propósito - a interpretação dele já vive no
|
|
49
|
+
* look, e reter as duas cópias é o que dobraria o censo.
|
|
50
|
+
*/
|
|
51
|
+
export function buildLedger(cli, seen) {
|
|
52
|
+
const counted = blank();
|
|
53
|
+
const interpreted = blank();
|
|
54
|
+
const groups = new Map();
|
|
55
|
+
for (const f of seen) {
|
|
56
|
+
counted[f.shape] += 1;
|
|
57
|
+
if (f.read) {
|
|
58
|
+
interpreted[f.shape] += 1;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const reason = f.reason ?? "shape-not-read";
|
|
62
|
+
const key = `${f.shape}:${reason}`;
|
|
63
|
+
const group = groups.get(key) ?? {
|
|
64
|
+
shape: f.shape,
|
|
65
|
+
reason,
|
|
66
|
+
uses: 0,
|
|
67
|
+
files: 0,
|
|
68
|
+
because: BECAUSE[reason],
|
|
69
|
+
examples: [],
|
|
70
|
+
seenFiles: new Set(),
|
|
71
|
+
};
|
|
72
|
+
group.uses += 1;
|
|
73
|
+
group.seenFiles.add(f.file);
|
|
74
|
+
if (group.examples.length < 3 &&
|
|
75
|
+
!group.examples.some((e) => e.file === f.file))
|
|
76
|
+
group.examples.push({
|
|
77
|
+
file: f.file,
|
|
78
|
+
line: f.line,
|
|
79
|
+
text: f.text.replace(/\s+/g, " ").trim().slice(0, 160),
|
|
80
|
+
});
|
|
81
|
+
groups.set(key, group);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
cli,
|
|
85
|
+
counted,
|
|
86
|
+
interpreted,
|
|
87
|
+
unread: [...groups.values()]
|
|
88
|
+
.map(({ seenFiles, ...group }) => ({ ...group, files: seenFiles.size }))
|
|
89
|
+
.sort((a, b) => b.uses - a.uses),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function blank() {
|
|
93
|
+
return Object.fromEntries(SHAPES.map((s) => [s, 0]));
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* O relatório, na ordem em que uma pessoa decide se aquilo a incomoda.
|
|
97
|
+
*
|
|
98
|
+
* Lidera com o que foi CONTABILIZADO, porque é a única promessa que a esteira pode cumprir, e
|
|
99
|
+
* nomeia cada lacuna com o número do repo dela. Um percentual sem a contagem atrás é número de
|
|
100
|
+
* marketing, e um zero sem motivo lê como falha nossa.
|
|
101
|
+
*/
|
|
102
|
+
export function describeLedger(l) {
|
|
103
|
+
const total = sum(l.counted);
|
|
104
|
+
if (total === 0)
|
|
105
|
+
return [];
|
|
106
|
+
const read = sum(l.interpreted);
|
|
107
|
+
const lines = [
|
|
108
|
+
`${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.`,
|
|
109
|
+
];
|
|
110
|
+
for (const g of l.unread) {
|
|
111
|
+
lines.push(` ${g.uses} ${g.shape}${g.uses === 1 ? "" : "s"} in ${g.files} file${g.files === 1 ? "" : "s"} - ${g.because}`);
|
|
112
|
+
for (const e of g.examples)
|
|
113
|
+
lines.push(` ${e.file}:${e.line} ${e.text.slice(0, 80)}`);
|
|
114
|
+
}
|
|
115
|
+
return lines;
|
|
116
|
+
}
|
|
117
|
+
function sum(counts) {
|
|
118
|
+
return Object.values(counts).reduce((n, k) => n + k, 0);
|
|
119
|
+
}
|
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