synthesisui 0.16.165 → 0.16.167
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 +47 -0
- package/dist/doctor/reachability.js +293 -0
- package/package.json +1 -1
package/dist/commands/import.js
CHANGED
|
@@ -21,6 +21,7 @@ import { describeConvention, describeRemainder, detectConventions, } from "../do
|
|
|
21
21
|
import { pageLaws } from "../doctor/page-laws.js";
|
|
22
22
|
import { callSiteNode, placeLayers, } from "../doctor/place-layers.js";
|
|
23
23
|
import { publicApi, requiredProps } from "../doctor/public-api.js";
|
|
24
|
+
import { aliasesOf, describeReachability, edgesIn, reachabilityOf, } from "../doctor/reachability.js";
|
|
24
25
|
import { diagnose, scanSource } from "../doctor/scan.js";
|
|
25
26
|
import { parseSchemeBlocks } from "../doctor/scheme-blocks.js";
|
|
26
27
|
import { describeSignals, emptySignals, finishSignals, readSignalsInto, } from "../doctor/signals.js";
|
|
@@ -973,12 +974,23 @@ export async function takeCensus(root, opts) {
|
|
|
973
974
|
.then((raw) => JSON.parse(raw).name ?? null)
|
|
974
975
|
.catch(() => null);
|
|
975
976
|
const shared = scopePkg ? [...internal, scopePkg] : internal;
|
|
977
|
+
/**
|
|
978
|
+
* O GRAFO DE IMPORTS DA EVIDÊNCIA, montado no laço que já abre cada arquivo.
|
|
979
|
+
*
|
|
980
|
+
* O `--usage` doa CONTAGEM e LEI, então código que nenhuma rota atinge vota nas leis do cliente -
|
|
981
|
+
* 180 de 966 arquivos no repo do dono (07/08). Refazer a varredura para montar o grafo seria
|
|
982
|
+
* percorrer o repo inteiro de novo por um dado que este laço já tem na mão. Ver `reachability.ts`.
|
|
983
|
+
*/
|
|
984
|
+
const edges = new Map();
|
|
985
|
+
const aliases = [];
|
|
976
986
|
for (const u of opts?.usage ?? []) {
|
|
977
987
|
const uInternal = await internalSpecifiers(u.path);
|
|
988
|
+
aliases.push(...(await aliasesOf(u.path)));
|
|
978
989
|
for await (const file of walk(u.path)) {
|
|
979
990
|
const src = await readFile(file, "utf8").catch(() => "");
|
|
980
991
|
if (!src)
|
|
981
992
|
continue;
|
|
993
|
+
edges.set(file, edgesIn(src));
|
|
982
994
|
const rel = join(u.label, relative(u.path, file));
|
|
983
995
|
if (/(\.(spec|test|stories)\.[a-z]+$|__tests__\/|(^|\/)\.storybook\/)/.test(rel)) {
|
|
984
996
|
continue;
|
|
@@ -1215,6 +1227,11 @@ export async function takeCensus(root, opts) {
|
|
|
1215
1227
|
*
|
|
1216
1228
|
* `componentFiles` segue existindo para as formas de estilo, que SÃO por arquivo.
|
|
1217
1229
|
*/
|
|
1230
|
+
/**
|
|
1231
|
+
* O GRAFO SÓ FECHA DEPOIS QUE TODOS OS ARQUIVOS FORAM VISTOS: um especificador só resolve para um
|
|
1232
|
+
* arquivo que já esteja na lista, então isto não pode rodar dentro do laço que a preenche.
|
|
1233
|
+
*/
|
|
1234
|
+
const reachability = edges.size > 0 ? await reachabilityOf({ edges, aliases }) : null;
|
|
1218
1235
|
const coverage = summarizeCoverage(shapes, defined.length || componentFiles, Object.keys(looks).length, Object.values(looks).reduce((n, look) => n +
|
|
1219
1236
|
Object.keys(look.base).length +
|
|
1220
1237
|
Object.keys(look.dark ?? {}).length +
|
|
@@ -1350,6 +1367,25 @@ export async function takeCensus(root, opts) {
|
|
|
1350
1367
|
for (const line of coverageLines)
|
|
1351
1368
|
console.log(body(line));
|
|
1352
1369
|
}
|
|
1370
|
+
/**
|
|
1371
|
+
* O QUE VOTA NAS LEIS DELE SEM ESTAR VIVO - dito, e nada excluído.
|
|
1372
|
+
*
|
|
1373
|
+
* Lei 7 do `CLAUDE.md`: o que a esteira não sabe tratar se DECLARA, com o número do repo dele, no
|
|
1374
|
+
* momento do censo. Silêncio sobre uma lacuna é o que faz um produto correto parecer quebrado - e
|
|
1375
|
+
* aqui o silêncio custava mais que aparência, porque as leis saíam distorcidas sem ninguém saber.
|
|
1376
|
+
*/
|
|
1377
|
+
const reachLines = reachability ? describeReachability(reachability) : [];
|
|
1378
|
+
if (reachLines.length > 0) {
|
|
1379
|
+
console.log("");
|
|
1380
|
+
console.log(section("What no route reaches"));
|
|
1381
|
+
for (const line of reachLines)
|
|
1382
|
+
console.log(body(line));
|
|
1383
|
+
if (reachability && reachability.unreached.length > 0)
|
|
1384
|
+
console.log(body(paint.faint(`${reachability.unreached
|
|
1385
|
+
.slice(0, 4)
|
|
1386
|
+
.map((f) => relative(root, f))
|
|
1387
|
+
.join(", ")}${reachability.unreached.length > 4 ? `, and ${reachability.unreached.length - 4} more` : ""} - the full list travels in the census, and nothing was left out because of it`)));
|
|
1388
|
+
}
|
|
1353
1389
|
/**
|
|
1354
1390
|
* WHAT THE GATE TURNED AWAY - said out loud, grouped, with the reason.
|
|
1355
1391
|
*
|
|
@@ -1622,6 +1658,17 @@ export async function takeCensus(root, opts) {
|
|
|
1622
1658
|
* ninguém ter que reportar (dono, 04/08).
|
|
1623
1659
|
*/
|
|
1624
1660
|
ledger,
|
|
1661
|
+
/**
|
|
1662
|
+
* O QUE NENHUMA ROTA ATINGE, entre os arquivos que doam evidência.
|
|
1663
|
+
*
|
|
1664
|
+
* O `--usage` não doa token nem componente - doa CONTAGEM e LEI, que é o que transforma uma
|
|
1665
|
+
* escolha assentada numa lei que governa. Então código morto ali VOTA nas leis do cliente, e
|
|
1666
|
+
* até 07/08 nada contava: 180 de 966 arquivos no repo do dono.
|
|
1667
|
+
*
|
|
1668
|
+
* Viaja com o censo e NÃO exclui nada. Ver `reachability.ts` para o motivo de ser um grafo e não
|
|
1669
|
+
* uma regra de nome - `_legacy` tinha 178 arquivos e 106 deles estavam vivos.
|
|
1670
|
+
*/
|
|
1671
|
+
...(reachability ? { reachability } : {}),
|
|
1625
1672
|
/**
|
|
1626
1673
|
* A ESCALA QUE O `sx` DELES USA, só quando eles declaram uma diferente do default do MUI.
|
|
1627
1674
|
* Medida no censo e consumida pela derivação - ver `readSxProps`.
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* OS NOMES QUE O ROTEADOR MONTA SOZINHO.
|
|
5
|
+
*
|
|
6
|
+
* Um arquivo com um destes nomes é uma porta de entrada mesmo que ninguém o importe - é o framework
|
|
7
|
+
* que o monta. Sem esta lista, um app inteiro sairia como inalcançável a partir do nada.
|
|
8
|
+
*/
|
|
9
|
+
const ENTRY = /\/(page|layout|route|template|default|loading|error|not-found|global-error|middleware|instrumentation)\.[jt]sx?$/;
|
|
10
|
+
const EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
11
|
+
/**
|
|
12
|
+
* Os especificadores que um arquivo importa - estáticos, re-exports e dinâmicos.
|
|
13
|
+
*
|
|
14
|
+
* Re-export conta: um barril (`export { X } from "./x"`) é o caminho pelo qual metade dos projetos
|
|
15
|
+
* chega nos próprios componentes, e ignorá-lo transformaria uma pasta inteira em morta.
|
|
16
|
+
*/
|
|
17
|
+
export function edgesIn(source) {
|
|
18
|
+
const out = [];
|
|
19
|
+
for (const re of [
|
|
20
|
+
/\bfrom\s+["']([^"']+)["']/g,
|
|
21
|
+
/\bimport\s*\(\s*["']([^"']+)["']\s*\)/g,
|
|
22
|
+
/\brequire\s*\(\s*["']([^"']+)["']\s*\)/g,
|
|
23
|
+
/\bimport\s+["']([^"']+)["']/g,
|
|
24
|
+
])
|
|
25
|
+
for (const m of source.matchAll(re))
|
|
26
|
+
out.push(m[1]);
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Os apelidos que o projeto declara - `paths` do tsconfig, que é onde `@/` mora.
|
|
31
|
+
*
|
|
32
|
+
* Sem eles, um projeto que importa tudo por `@/components/...` lê como um grafo sem aresta nenhuma,
|
|
33
|
+
* e a medição diria que o app inteiro está morto. Ler o `tsconfig` é a diferença entre uma medição e
|
|
34
|
+
* um alarme.
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* Tira comentários de um tsconfig SEM ATRAVESSAR STRING - e essa ressalva é o ponto inteiro.
|
|
38
|
+
*
|
|
39
|
+
* A primeira versão era um `replace` de duas regex, e ela comia o próprio alias que veio buscar:
|
|
40
|
+
* `"@/*"` - o apelido padrão de qualquer projeto Next - começa com `/*`, que lê como abertura de
|
|
41
|
+
* bloco. O parser engolia dali até o primeiro `*` `/` adiante, o JSON quebrava, e a função devolvia
|
|
42
|
+
* zero alias. Consequência: todo `@/…` deixava de resolver e o app inteiro saía como código morto
|
|
43
|
+
* (07/08, 1573 arestas perdidas no app do dono).
|
|
44
|
+
*
|
|
45
|
+
* Um erro de leitura NOSSO virando uma acusação sobre o código de alguém é o pior modo de falha que
|
|
46
|
+
* esta esteira tem, e é por isso que o scanner sabe onde uma string começa e acaba.
|
|
47
|
+
*/
|
|
48
|
+
function withoutComments(raw) {
|
|
49
|
+
let out = "";
|
|
50
|
+
let inString = false;
|
|
51
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
52
|
+
const c = raw[i];
|
|
53
|
+
const next = raw[i + 1];
|
|
54
|
+
if (inString) {
|
|
55
|
+
out += c;
|
|
56
|
+
if (c === "\\") {
|
|
57
|
+
out += next ?? "";
|
|
58
|
+
i += 1;
|
|
59
|
+
}
|
|
60
|
+
else if (c === '"')
|
|
61
|
+
inString = false;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (c === '"') {
|
|
65
|
+
inString = true;
|
|
66
|
+
out += c;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (c === "/" && next === "/") {
|
|
70
|
+
while (i < raw.length && raw[i] !== "\n")
|
|
71
|
+
i += 1;
|
|
72
|
+
out += "\n";
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (c === "/" && next === "*") {
|
|
76
|
+
i += 2;
|
|
77
|
+
while (i < raw.length && !(raw[i] === "*" && raw[i + 1] === "/"))
|
|
78
|
+
i += 1;
|
|
79
|
+
i += 1;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
out += c;
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
export async function aliasesOf(root) {
|
|
87
|
+
return readAliases(join(root, "tsconfig.json"), 0);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Lê um tsconfig POR CAMINHO DE ARQUIVO, e é isso que faz o `extends` funcionar.
|
|
91
|
+
*
|
|
92
|
+
* A primeira versão pegava o diretório do arquivo apontado e voltava a procurar `tsconfig.json` nele
|
|
93
|
+
* - então um `extends: "../../tsconfig.base.json"` lia o `tsconfig.json` da raiz, que não existe, e
|
|
94
|
+
* devolvia zero alias. No app do dono isso fazia todo `@/…` deixar de resolver: 1053 arestas
|
|
95
|
+
* perdidas, e o app inteiro lendo como morto (07/08).
|
|
96
|
+
*/
|
|
97
|
+
async function readAliases(path,
|
|
98
|
+
/** Guarda contra `extends` circular, e contra uma cadeia que ninguém pretendia seguir. */
|
|
99
|
+
depth) {
|
|
100
|
+
if (depth > 4)
|
|
101
|
+
return [];
|
|
102
|
+
const raw = await readFile(path, "utf8").catch(() => "");
|
|
103
|
+
if (!raw)
|
|
104
|
+
return [];
|
|
105
|
+
let parsed;
|
|
106
|
+
try {
|
|
107
|
+
parsed = JSON.parse(withoutComments(raw));
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
const here = dirname(path);
|
|
113
|
+
const inherited = parsed.extends?.startsWith(".")
|
|
114
|
+
? await readAliases(resolve(here, parsed.extends), depth + 1)
|
|
115
|
+
: [];
|
|
116
|
+
/**
|
|
117
|
+
* `paths` é relativo ao `baseUrl` DO ARQUIVO QUE O DECLARA - um base config na raiz aponta para as
|
|
118
|
+
* pastas a partir da raiz, e resolvê-lo a partir do app daria um caminho que não existe.
|
|
119
|
+
*/
|
|
120
|
+
const base = resolve(here, parsed.compilerOptions?.baseUrl ?? ".");
|
|
121
|
+
const own = Object.entries(parsed.compilerOptions?.paths ?? {}).map(([from, to]) => ({
|
|
122
|
+
prefix: from.replace(/\*$/, ""),
|
|
123
|
+
to: to.map((t) => resolve(base, t.replace(/\*$/, ""))),
|
|
124
|
+
}));
|
|
125
|
+
/** O do próprio projeto vence o herdado, que é a regra do TypeScript. */
|
|
126
|
+
return [...own, ...inherited];
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Parece um caminho DESTE repo, e não um pacote?
|
|
130
|
+
*
|
|
131
|
+
* `@/x`, `~/x` e `#x` são as três formas de apelido local que aparecem na prática, e nenhuma é um
|
|
132
|
+
* pacote npm válido - um escopo real é `@escopo/nome`. Existe para o contador de `unresolved` pegar
|
|
133
|
+
* o caso em que a configuração de alias falhou: sem isto, um `@/` sem alias casado era descartado em
|
|
134
|
+
* silêncio, e o número de mortos subia sem a ressalva que devia acompanhá-lo.
|
|
135
|
+
*/
|
|
136
|
+
export function looksLocal(spec) {
|
|
137
|
+
return /^(\.|~\/|#)/.test(spec) || /^@\//.test(spec);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Um import de ASSET não é uma aresta de código.
|
|
141
|
+
*
|
|
142
|
+
* `import "./globals.css"` e `import logo from "./logo.png"` são reais e não fazem componente nenhum
|
|
143
|
+
* ficar vivo. Contá-los como aresta perdida inflava a ressalva que qualifica todo o resto - e uma
|
|
144
|
+
* ressalva inflada é tão inútil quanto uma ausente.
|
|
145
|
+
*/
|
|
146
|
+
const ASSET = /\.(css|scss|sass|less|styl|png|jpe?g|gif|svg|webp|avif|ico|woff2?|ttf|otf|mp4|webm|json|md|txt|ya?ml|graphql|wasm)$/i;
|
|
147
|
+
/**
|
|
148
|
+
* Existe como arquivo? Com memória, porque a mesma resposta é pedida centenas de vezes.
|
|
149
|
+
*
|
|
150
|
+
* Cada especificador tenta até treze candidatos (o base, seis extensões, seis `index`), e um barril
|
|
151
|
+
* importado por cem arquivos repete os mesmos treves `stat` cem vezes. Sem isto, medir um app de 1446
|
|
152
|
+
* arquivos passava de três segundos - e um leitor que roda dentro de todo import não pode pagar isso
|
|
153
|
+
* por um dado que não muda durante a rodada.
|
|
154
|
+
*/
|
|
155
|
+
function fileChecker() {
|
|
156
|
+
const known = new Map();
|
|
157
|
+
return (path) => {
|
|
158
|
+
const seen = known.get(path);
|
|
159
|
+
if (seen)
|
|
160
|
+
return seen;
|
|
161
|
+
const asked = stat(path).then((s) => s.isFile(), () => false);
|
|
162
|
+
known.set(path, asked);
|
|
163
|
+
return asked;
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/** Um especificador vira um arquivo real, ou nada. `null` é contado, nunca engolido. */
|
|
167
|
+
async function toFile(known, from, spec, aliases, isFile) {
|
|
168
|
+
const bases = [];
|
|
169
|
+
if (spec.startsWith("."))
|
|
170
|
+
bases.push(resolve(dirname(from), spec));
|
|
171
|
+
else
|
|
172
|
+
for (const a of aliases)
|
|
173
|
+
if (spec.startsWith(a.prefix))
|
|
174
|
+
for (const t of a.to)
|
|
175
|
+
bases.push(join(t, spec.slice(a.prefix.length)));
|
|
176
|
+
if (bases.length === 0)
|
|
177
|
+
return null;
|
|
178
|
+
for (const base of bases)
|
|
179
|
+
for (const candidate of [
|
|
180
|
+
base,
|
|
181
|
+
...EXTS.map((e) => base + e),
|
|
182
|
+
...EXTS.map((e) => join(base, `index${e}`)),
|
|
183
|
+
]) {
|
|
184
|
+
if (known.has(candidate))
|
|
185
|
+
return candidate;
|
|
186
|
+
/**
|
|
187
|
+
* Fora da lista varrida ainda pode existir: um alias que aponta para outro pacote do monorepo,
|
|
188
|
+
* ou um `.js` que o walk deste escopo não coletou. O próprio `base` entra no teste - um alias
|
|
189
|
+
* que aponta DIRETO para um arquivo (`"@x/core": ["packages/core/src/index.ts"]`) não tem
|
|
190
|
+
* sufixo nenhum, e pular o base fazia esse caso nunca resolver.
|
|
191
|
+
*/
|
|
192
|
+
if (await isFile(candidate))
|
|
193
|
+
return candidate;
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Quem o roteador alcança, e quem não.
|
|
199
|
+
*
|
|
200
|
+
* `edges` chega já coletado pelo laço que já abriu cada arquivo - refazer a leitura para montar o
|
|
201
|
+
* grafo seria uma segunda varredura do repo inteiro por um dado que a primeira já tinha na mão.
|
|
202
|
+
*/
|
|
203
|
+
export async function reachabilityOf(input) {
|
|
204
|
+
/**
|
|
205
|
+
* O UNIVERSO É CÓDIGO-FONTE, e não tudo que tem extensão.
|
|
206
|
+
*
|
|
207
|
+
* `.d.ts` são declarações de tipo e `next.config.js`/`tailwind.config.ts` são configuração: nenhum
|
|
208
|
+
* é montado por uma rota e nenhum doa evidência sobre como o produto usa o sistema. Contá-los
|
|
209
|
+
* inflava o denominador com arquivos que são inalcançáveis por definição - 2090 no lugar de 966 no
|
|
210
|
+
* app do dono (07/08), e um número inflado no denominador é uma acusação inflada no numerador.
|
|
211
|
+
*/
|
|
212
|
+
const isFile = fileChecker();
|
|
213
|
+
const known = new Set([...input.edges.keys()].filter((f) => /\.[jt]sx?$/.test(f) &&
|
|
214
|
+
!/\.d\.ts$/.test(f) &&
|
|
215
|
+
!/\/[a-z.-]+\.config\.[jt]sx?$/.test(f)));
|
|
216
|
+
const graph = new Map();
|
|
217
|
+
let unresolved = 0;
|
|
218
|
+
for (const [file, specs] of input.edges) {
|
|
219
|
+
if (!known.has(file))
|
|
220
|
+
continue;
|
|
221
|
+
const out = [];
|
|
222
|
+
for (const spec of specs) {
|
|
223
|
+
const mine = spec.startsWith(".") ||
|
|
224
|
+
input.aliases.some((a) => spec.startsWith(a.prefix));
|
|
225
|
+
/**
|
|
226
|
+
* Pacote de terceiro não é aresta deste repo. Mas um apelido LOCAL que nenhum alias casa é uma
|
|
227
|
+
* aresta PERDIDA - e era descartado aqui em silêncio, o que subia o número de mortos sem a
|
|
228
|
+
* ressalva que devia vir junto (07/08).
|
|
229
|
+
*/
|
|
230
|
+
if (!mine) {
|
|
231
|
+
if (looksLocal(spec) && !ASSET.test(spec))
|
|
232
|
+
unresolved += 1;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const hit = await toFile(known, file, spec, input.aliases, isFile);
|
|
236
|
+
if (hit)
|
|
237
|
+
out.push(hit);
|
|
238
|
+
/**
|
|
239
|
+
* Asset que não resolve não é aresta perdida - ver `ASSET`. A tentativa acontece assim mesmo,
|
|
240
|
+
* porque um `./styles.css` que EXISTE é um nó legítimo do grafo, e recusá-lo antes de tentar
|
|
241
|
+
* derrubou 300 arquivos alcançados na primeira versão desta linha (07/08).
|
|
242
|
+
*/ else if (!ASSET.test(spec))
|
|
243
|
+
unresolved += 1;
|
|
244
|
+
}
|
|
245
|
+
graph.set(file, out);
|
|
246
|
+
}
|
|
247
|
+
const entries = [...known].filter((f) => ENTRY.test(f));
|
|
248
|
+
const seen = new Set();
|
|
249
|
+
const queue = [...entries];
|
|
250
|
+
while (queue.length > 0) {
|
|
251
|
+
const file = queue.pop();
|
|
252
|
+
if (!file || seen.has(file))
|
|
253
|
+
continue;
|
|
254
|
+
seen.add(file);
|
|
255
|
+
for (const next of graph.get(file) ?? [])
|
|
256
|
+
if (!seen.has(next))
|
|
257
|
+
queue.push(next);
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
entries: entries.length,
|
|
261
|
+
files: known.size,
|
|
262
|
+
reached: seen.size,
|
|
263
|
+
unreached: [...known].filter((f) => !seen.has(f)).sort(),
|
|
264
|
+
unresolved,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* A frase, e ela QUALIFICA o número antes de o dar.
|
|
269
|
+
*
|
|
270
|
+
* Um "19% do que vota nas suas leis é código morto" sem a ressalva do `unresolved` é uma afirmação
|
|
271
|
+
* que quem lê não tem como checar - e a primeira coisa que alguém faz com um número desses é apagar
|
|
272
|
+
* arquivo. A ordem aqui é deliberada: o que foi contado, depois o que a contagem não soube ver.
|
|
273
|
+
*/
|
|
274
|
+
export function describeReachability(r) {
|
|
275
|
+
if (r.files === 0 || r.entries === 0)
|
|
276
|
+
return [];
|
|
277
|
+
const dead = r.unreached.length;
|
|
278
|
+
if (dead === 0)
|
|
279
|
+
return [
|
|
280
|
+
`Every one of the ${r.files} files behind your evidence is reached from one of its ${r.entries} entry points - nothing dead is voting on your laws.`,
|
|
281
|
+
];
|
|
282
|
+
const pct = Math.round((dead / r.files) * 100);
|
|
283
|
+
const out = [
|
|
284
|
+
`${dead} of ${r.files} files (${pct}%) are not reached from any of the ${r.entries} entry points this app declares - they are counted as evidence, which means dead code is voting on which of your choices became laws.`,
|
|
285
|
+
];
|
|
286
|
+
/**
|
|
287
|
+
* A RESSALVA VIAJA COM O NÚMERO, sempre - e o corte é 5% dos especificadores. Acima disso o grafo
|
|
288
|
+
* perdeu arestas demais para a palavra "morto" significar alguma coisa.
|
|
289
|
+
*/
|
|
290
|
+
if (r.unresolved > 0)
|
|
291
|
+
out.push(`${r.unresolved} import${r.unresolved === 1 ? "" : "s"} could not be resolved to a file, so that count is a CEILING and not a measurement: an import this reader cannot follow turns living code into "dead". Nothing was excluded on account of it.`);
|
|
292
|
+
return out;
|
|
293
|
+
}
|
package/package.json
CHANGED