synthesisui 0.16.407 → 0.16.409
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/doctor.js +66 -23
- package/dist/doctor/apply-fix.js +66 -3
- package/dist/doctor/ci-format.js +22 -1
- package/dist/doctor/ledger.js +18 -1
- package/dist/doctor/scan.js +100 -16
- package/dist/global-sheet.js +31 -4
- package/dist/install-marks.js +18 -2
- package/dist/wiring-read.js +77 -0
- package/package.json +1 -1
package/dist/commands/doctor.js
CHANGED
|
@@ -8,7 +8,7 @@ import { emptyTally, internalSpecifiers, scanComponentsInto, tallyToInventory, }
|
|
|
8
8
|
import { checkContracts } from "../doctor/contract-check.js";
|
|
9
9
|
import { describeMissing, missingDependencies, summarizeMissing, } from "../doctor/dependencies.js";
|
|
10
10
|
import { findFrozenBindings } from "../doctor/frozen.js";
|
|
11
|
-
import { appendEvent, COVERAGE_RULE, readEvents, suggestionsFrom, summarize, unrepresentedFrom, } from "../doctor/ledger.js";
|
|
11
|
+
import { appendEvent, COVERAGE_RULE, COVERAGE_RULE_REASON, readEvents, suggestionsFrom, summarize, unrepresentedFrom, } from "../doctor/ledger.js";
|
|
12
12
|
import { bindingsFromDocument, countComponents, findOverrides, } from "../doctor/overrides.js";
|
|
13
13
|
import { checkableName, readRequests, verifyAndCloseRequests, } from "../doctor/requests.js";
|
|
14
14
|
import { DEFAULT_ROOT_PX, rootSizeOf, saidOfRoot, } from "../doctor/root-size.js";
|
|
@@ -16,7 +16,11 @@ import { diagnose, nameToWrite, scanSource, siblingTokens, } from "../doctor/sca
|
|
|
16
16
|
import { findSelfConflicts, forbiddenProps, isReset, propMatchesLabel, } from "../doctor/self-conflict.js";
|
|
17
17
|
import { withTheirNames } from "../doctor/their-names.js";
|
|
18
18
|
import { buildTable, EMPTY_TABLE, nearestToken, } from "../doctor/tokens.js";
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* `detectAppDirs` SAIU DESTE IMPORT em 09/09: quem pergunta "quais pastas são app?" agora é
|
|
21
|
+
* `readWiringPerApp`, que responde a fiação de cada uma. O comando não escolhe mais um app.
|
|
22
|
+
*/
|
|
23
|
+
import { globalSheetOf, prefixFrom } from "../global-sheet.js";
|
|
20
24
|
import { groupRole } from "../group-role.js";
|
|
21
25
|
import { actingSlug, describeScope, measuredScope, scopePaths, } from "../measured-scope.js";
|
|
22
26
|
import { body, paint, section, snippet } from "../output.js";
|
|
@@ -25,7 +29,7 @@ import { installedThemeCss, whatOnlyTheSheetResolves, } from "../sheet-needed.js
|
|
|
25
29
|
import { resolveDeps } from "../stack.js";
|
|
26
30
|
import { projectTongue } from "../their-tongue.js";
|
|
27
31
|
import { danglingTheirVars } from "../their-vars.js";
|
|
28
|
-
import { readWiring } from "../wiring-read.js";
|
|
32
|
+
import { readWiring, readWiringPerApp } from "../wiring-read.js";
|
|
29
33
|
/**
|
|
30
34
|
* `synthesisui doctor` - the check nobody else ships.
|
|
31
35
|
*
|
|
@@ -1068,28 +1072,61 @@ export async function doctor(opts) {
|
|
|
1068
1072
|
* inventado.
|
|
1069
1073
|
*/
|
|
1070
1074
|
if (table.slug) {
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
+
/**
|
|
1076
|
+
* A INSTRUÇÃO É POR APP, e não do primeiro deles.
|
|
1077
|
+
*
|
|
1078
|
+
* O DEFEITO: esta linha era `appDirs[0]` - num monorepo com três apps servidos, dois recebiam
|
|
1079
|
+
* a instrução do PRIMEIRO, com a folha e o prefixo relativo de outro app. É o mesmo defeito
|
|
1080
|
+
* que `INV-VOLTA-12` fechou uma vez ("ele seguiu a instrução no arquivo CERTO e recebeu de nós
|
|
1081
|
+
* o prefixo do arquivo errado"), voltando pela porta do plural: a invariante fala de "os apps
|
|
1082
|
+
* dele", e o comando respondia por um.
|
|
1083
|
+
*
|
|
1084
|
+
* E A FIAÇÃO TAMBÉM É POR APP (`readWiringPerApp`): `readWiring` varre a raiz e responde
|
|
1085
|
+
* "existe em algum lugar daqui", então o app fiado respondia pelo que não estava - e a pessoa
|
|
1086
|
+
* abria o app sem nada e o comando dizia que estava tudo certo.
|
|
1087
|
+
*
|
|
1088
|
+
* COM UM APP, o texto não muda em byte nenhum - é a maioria dos projetos, e um cabeçalho
|
|
1089
|
+
* por app ali seria cerimônia sobre uma lista de um.
|
|
1090
|
+
*/
|
|
1091
|
+
const perApp = await readWiringPerApp(root, table.slug, config.pagesDir);
|
|
1092
|
+
const targets = perApp.length > 0 ? perApp : [{ app: null, wiring }];
|
|
1075
1093
|
console.log("");
|
|
1076
1094
|
console.log(body("Paste this into your agent - it does the setup for you:"));
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
/** Só é passo quando o projeto TEM o arquivo de fontes - ver `readWiring`. */
|
|
1082
|
-
type: wiring.fontsWritten && !wiring.fontsMapped,
|
|
1083
|
-
...(sheet
|
|
1084
|
-
? { sheet: { path: sheet, prefix: prefixFrom(sheet) } }
|
|
1085
|
-
: {}),
|
|
1095
|
+
for (const { app, wiring: w } of targets) {
|
|
1096
|
+
const sheet = app
|
|
1097
|
+
? await globalSheetOf(root, `${app}/globals.css`)
|
|
1098
|
+
: null;
|
|
1086
1099
|
/**
|
|
1087
|
-
*
|
|
1088
|
-
*
|
|
1089
|
-
*
|
|
1100
|
+
* O APP QUE JÁ ESTÁ FIADO SAI DA LISTA, nomeado. Repetir o setup para quem já o fez é o
|
|
1101
|
+
* defeito de 08/09 outra vez, e num monorepo ele apareceria em escala: dois blocos
|
|
1102
|
+
* idênticos, um deles cobrando o que já está colado.
|
|
1090
1103
|
*/
|
|
1091
|
-
|
|
1092
|
-
|
|
1104
|
+
const pending = !w.imported || !w.scoped || (w.fontsWritten && !w.fontsMapped);
|
|
1105
|
+
if (targets.length > 1) {
|
|
1106
|
+
console.log("");
|
|
1107
|
+
console.log(body(pending
|
|
1108
|
+
? ` ${app} - still needs it`
|
|
1109
|
+
: ` ${app} - already wired, nothing to paste`));
|
|
1110
|
+
}
|
|
1111
|
+
if (!pending)
|
|
1112
|
+
continue;
|
|
1113
|
+
console.log("");
|
|
1114
|
+
console.log(snippet(setupPrompt(table.slug, {
|
|
1115
|
+
tokens: !w.imported,
|
|
1116
|
+
scope: !w.scoped,
|
|
1117
|
+
/** Só é passo quando o projeto TEM o arquivo de fontes - ver `readWiring`. */
|
|
1118
|
+
type: w.fontsWritten && !w.fontsMapped,
|
|
1119
|
+
...(sheet
|
|
1120
|
+
? { sheet: { path: sheet, prefix: prefixFrom(sheet) } }
|
|
1121
|
+
: {}),
|
|
1122
|
+
/**
|
|
1123
|
+
* DUAS LINHAS COM TAILWIND, UMA SEM - a mesma decisão que o `add` já toma pelo
|
|
1124
|
+
* projeto. Dizer "as duas linhas" a um projeto que precisa de uma manda o agente
|
|
1125
|
+
* procurar o que não existe.
|
|
1126
|
+
*/
|
|
1127
|
+
imports: config.styles === "css" ? 1 : 2,
|
|
1128
|
+
}).split("\n")));
|
|
1129
|
+
}
|
|
1093
1130
|
}
|
|
1094
1131
|
}
|
|
1095
1132
|
/**
|
|
@@ -1419,9 +1456,15 @@ export async function doctor(opts) {
|
|
|
1419
1456
|
* para 37% sem uma linha de código mudada. Somar as duas fotos anunciaria um progresso
|
|
1420
1457
|
* inventado; calar a mudança deixaria alguém procurar o que fez o número pular.
|
|
1421
1458
|
*/
|
|
1422
|
-
/**
|
|
1459
|
+
/**
|
|
1460
|
+
* E SEM O NÚMERO DA VERSÃO: o que mudou a conta é a conta, não a nossa release.
|
|
1461
|
+
*
|
|
1462
|
+
* A RAZÃO VEM DA RÉGUA, nunca de uma frase cravada aqui: a de 13/08 (*"your own tokens count
|
|
1463
|
+
* now too"*) continuou impressa quando a régua subiu por outro motivo em 09/09, explicando a
|
|
1464
|
+
* causa errada para quem lê. Ver `COVERAGE_RULE_REASON`.
|
|
1465
|
+
*/
|
|
1423
1466
|
if (record.ruleChanged)
|
|
1424
|
-
console.log(body(
|
|
1467
|
+
console.log(body(` the way this is counted changed - ${COVERAGE_RULE_REASON[COVERAGE_RULE] ?? "the ruler moved"}, so the trend restarts here.`));
|
|
1425
1468
|
}
|
|
1426
1469
|
}
|
|
1427
1470
|
/**
|
package/dist/doctor/apply-fix.js
CHANGED
|
@@ -96,6 +96,30 @@ function wholeValue(line, at, literal) {
|
|
|
96
96
|
const after = line[at + literal.length] ?? "";
|
|
97
97
|
return !/[0-9a-z#]/i.test(before) && !/[0-9a-z]/i.test(after);
|
|
98
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* A troca nas colunas medidas, da DIREITA para a esquerda: escrever `var(--x)` é mais longo que o
|
|
101
|
+
* literal, e fazer isso da esquerda para a direita moveria as colunas seguintes.
|
|
102
|
+
*/
|
|
103
|
+
function swapAtCols(line, literal, name, cols) {
|
|
104
|
+
const guarded = selfRanges(line, name);
|
|
105
|
+
let out = line;
|
|
106
|
+
let count = 0;
|
|
107
|
+
let blocked = 0;
|
|
108
|
+
for (const at of [...cols].sort((a, b) => b - a)) {
|
|
109
|
+
/** A linha mudou desde a medição: o comando tem um motivo para isso, e ele não é este. */
|
|
110
|
+
if (!out.startsWith(literal, at))
|
|
111
|
+
continue;
|
|
112
|
+
if (!wholeValue(out, at, literal))
|
|
113
|
+
continue;
|
|
114
|
+
if (guarded.some(([start, end]) => at >= start && at + literal.length <= end)) {
|
|
115
|
+
blocked++;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
count++;
|
|
119
|
+
out = `${out.slice(0, at)}var(${name})${out.slice(at + literal.length)}`;
|
|
120
|
+
}
|
|
121
|
+
return { line: out, count, blocked };
|
|
122
|
+
}
|
|
99
123
|
/** Onde este valor está escrito INTEIRO nesta linha - a mesma pergunta que a troca faz. */
|
|
100
124
|
function wholeValuesIn(line, literal) {
|
|
101
125
|
const out = [];
|
|
@@ -106,7 +130,21 @@ function wholeValuesIn(line, literal) {
|
|
|
106
130
|
}
|
|
107
131
|
return out;
|
|
108
132
|
}
|
|
109
|
-
|
|
133
|
+
/**
|
|
134
|
+
* A TROCA ACONTECE NAS COLUNAS QUE A VARREDURA MEDIU - ver `Finding.cols`.
|
|
135
|
+
*
|
|
136
|
+
* Trocar TODA ocorrência do literal na linha era o defeito que o contador de shorthand destapou:
|
|
137
|
+
* com um achado de `8px` em `padding: 4px 8px`, o comando reescrevia também o `width: 8px`, o
|
|
138
|
+
* `top: 8px`, o `font-size: 8px` e os dois offsets de `box-shadow` da mesma linha - seis escritas
|
|
139
|
+
* onde havia um achado. A/B medido em 09/09 com os dois binários sobre o mesmo arquivo.
|
|
140
|
+
*
|
|
141
|
+
* SEM COLUNAS, O COMPORTAMENTO É O DE ANTES, e isso é deliberado: um literal DERIVADO (`duration-300`
|
|
142
|
+
* -> `300ms`) não está escrito na linha, então não há coluna para ele, e a resposta certa continua
|
|
143
|
+
* sendo a que o comando já dá (nada foi escrito, e ele diz por quê).
|
|
144
|
+
*/
|
|
145
|
+
function swapAll(line, literal, name, cols) {
|
|
146
|
+
if (cols && cols.length > 0)
|
|
147
|
+
return swapAtCols(line, literal, name, cols);
|
|
110
148
|
const guarded = selfRanges(line, name);
|
|
111
149
|
let out = "";
|
|
112
150
|
let from = 0;
|
|
@@ -168,8 +206,17 @@ export function planFix(d, read) {
|
|
|
168
206
|
* DE BAIXO PARA CIMA no arquivo não é necessário - a troca não muda a contagem de linhas - mas a
|
|
169
207
|
* ORDEM por arquivo é, para dois achados na mesma linha não se atropelarem: o segundo procura o seu
|
|
170
208
|
* literal no texto que o primeiro já deixou.
|
|
209
|
+
*
|
|
210
|
+
* E DA DIREITA PARA A ESQUERDA DENTRO DA LINHA, desde que a troca acontece nas COLUNAS medidas
|
|
211
|
+
* (ver `Finding.cols`): `var(--ds-spacing-8)` é mais longo que `8px`, então trocar o primeiro
|
|
212
|
+
* valor de um `padding: 8px 24px` empurra o segundo para a direita e a coluna dele deixa de
|
|
213
|
+
* casar - o `24px` ficava literal, e o comando dizia que a linha tinha mudado desde a medição.
|
|
214
|
+
* Medido no próprio caso desta rodada, com o teste ponta a ponta em `apply-fix.spec.ts`.
|
|
171
215
|
*/
|
|
172
|
-
|
|
216
|
+
const rightToLeft = [...d.findings].sort((a, b) => a.file === b.file && a.line === b.line
|
|
217
|
+
? Math.max(...(b.cols ?? [-1])) - Math.max(...(a.cols ?? [-1]))
|
|
218
|
+
: 0);
|
|
219
|
+
for (const f of rightToLeft) {
|
|
173
220
|
/**
|
|
174
221
|
* O NOME DELE GANHA - ver `nameToWrite` em `scan.ts`.
|
|
175
222
|
*
|
|
@@ -286,7 +333,7 @@ export function planFix(d, read) {
|
|
|
286
333
|
const already = `${f.file}:${f.line}:${f.literal}`;
|
|
287
334
|
if (done.has(already))
|
|
288
335
|
continue;
|
|
289
|
-
const swapped = current === undefined ? null : swapAll(current, f.literal, name);
|
|
336
|
+
const swapped = current === undefined ? null : swapAll(current, f.literal, name, f.cols);
|
|
290
337
|
/**
|
|
291
338
|
* TODA OCORRÊNCIA DA LINHA ERA A PRÓPRIA DECLARAÇÃO - o caso que o guard sempre recusou, dito
|
|
292
339
|
* com a mesma frase de antes. O que mudou é que uma linha que declara E usa deixou de cair aqui.
|
|
@@ -469,5 +516,21 @@ export function describeFix(result, dry) {
|
|
|
469
516
|
lines.push(`${unread} file${unread === 1 ? "" : "s"} could not be read.`);
|
|
470
517
|
if (decisions > 0)
|
|
471
518
|
lines.push(`${decisions} finding${decisions === 1 ? "" : "s"} left: values your system has no name for. Those are decisions - name them, or file a request: npx synthesisui request token`);
|
|
519
|
+
/**
|
|
520
|
+
* OS QUE SOBRARAM NA LINHA QUE O COMANDO ACABOU DE REESCREVER - e sem esta linha o recibo não os
|
|
521
|
+
* liga a nada.
|
|
522
|
+
*
|
|
523
|
+
* Desde que cada comprimento de um shorthand tem achado próprio, uma linha volta do `--fix` com
|
|
524
|
+
* token E literal lado a lado: `padding: 8px var(--ds-spacing-24)`. O número de "sobraram" já
|
|
525
|
+
* aparecia, mas não dizia que parte dele está em linhas que este mesmo comando tocou - e é
|
|
526
|
+
* justamente onde ele vai olhar primeiro quando abrir o diff.
|
|
527
|
+
*
|
|
528
|
+
* Mesmo formato das outras linhas por motivo (`selfRef`, `notWritten`, `relative`): o número, o
|
|
529
|
+
* porquê, e o comando para ver.
|
|
530
|
+
*/
|
|
531
|
+
const rewritten = new Set(applied.map((a) => `${a.file}:${a.line}`));
|
|
532
|
+
const halfWritten = skipped.filter((s) => rewritten.has(`${s.file}:${s.line}`)).length;
|
|
533
|
+
if (halfWritten > 0)
|
|
534
|
+
lines.push(` ${halfWritten} of those ${halfWritten === 1 ? "sits" : "sit"} on a line this command just rewrote - a shorthand comes back with a token and a literal side by side (\`padding: 8px var(--ds-spacing-24)\`). See them by value: npx synthesisui doctor --migrate`);
|
|
472
535
|
return lines;
|
|
473
536
|
}
|
package/dist/doctor/ci-format.js
CHANGED
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
* NADA AQUI FALA COM A REDE nem pede login. É a mesma decisão que mantém o hook instalado: uma
|
|
23
23
|
* checagem que telefona para casa é uma checagem que alguém desinstala.
|
|
24
24
|
*/
|
|
25
|
+
import { isOlderCli } from "../cli-version.js";
|
|
26
|
+
import { CHECKER_SINCE } from "../install-marks.js";
|
|
25
27
|
const byFileOf = (d) => {
|
|
26
28
|
const out = {};
|
|
27
29
|
for (const f of d.files)
|
|
@@ -63,8 +65,23 @@ export function compareToBaseline(d, base, now) {
|
|
|
63
65
|
const drift = { was: base.totals.drift, now: d.findings.length };
|
|
64
66
|
const phantoms = { was: base.totals.phantoms, now: d.phantomUses };
|
|
65
67
|
const scopeChanged = (base.scope ?? "") !== (now.scope ?? "");
|
|
68
|
+
/**
|
|
69
|
+
* O LEITOR MUDOU DESDE O BASELINE, e aí a subida é NOSSA - reprovar o PR dele por isso é o alarme
|
|
70
|
+
* falso que ensina um time a passar `--no-verify`.
|
|
71
|
+
*
|
|
72
|
+
* MEDIDO EM 09/09: um baseline escrito pela 0.16.407 dizia 8799, a rodada de hoje contou 9531 -
|
|
73
|
+
* cada comprimento de um shorthand conta agora -, e a catraca respondia `worse: true`, *"Fix those
|
|
74
|
+
* lines"*, saída 1, num repositório onde ninguém tocou uma linha. O tipo deste arquivo já
|
|
75
|
+
* prometia o contrário desde o primeiro dia (*"um número que sobe porque o leitor melhorou não é
|
|
76
|
+
* regressão"*), e `incomparable` existia para exatamente isto.
|
|
77
|
+
*
|
|
78
|
+
* A RÉGUA É A MARCA QUE JÁ EXISTE: `CHECKER_SINCE` é a última versão em que a leitura que o CI
|
|
79
|
+
* compara mudou de resposta, e ela sobe no mesmo PR que muda a leitura. Nada de limiar novo.
|
|
80
|
+
*/
|
|
81
|
+
const readerMoved = base.cli !== undefined && isOlderCli(base.cli, CHECKER_SINCE);
|
|
66
82
|
return {
|
|
67
83
|
worse: !scopeChanged &&
|
|
84
|
+
!readerMoved &&
|
|
68
85
|
(drift.now > drift.was ||
|
|
69
86
|
phantoms.now > phantoms.was ||
|
|
70
87
|
regressed.length > 0),
|
|
@@ -76,7 +93,11 @@ export function compareToBaseline(d, base, now) {
|
|
|
76
93
|
? {
|
|
77
94
|
incomparable: `the baseline was measured on ${base.scope ?? "the whole repo"} and this run read ${now.scope ?? "the whole repo"} - different inputs, so nothing is failed on the difference`,
|
|
78
95
|
}
|
|
79
|
-
:
|
|
96
|
+
: readerMoved
|
|
97
|
+
? {
|
|
98
|
+
incomparable: `the baseline was written by CLI ${base.cli}, and this run counts every length in a shorthand - that version counted only the first. The rise is in the reading, not in your code. Rewrite the line before comparing: npx synthesisui doctor --write-baseline`,
|
|
99
|
+
}
|
|
100
|
+
: {}),
|
|
80
101
|
};
|
|
81
102
|
}
|
|
82
103
|
/**
|
package/dist/doctor/ledger.js
CHANGED
|
@@ -215,8 +215,25 @@ export async function readEvents(root) {
|
|
|
215
215
|
*
|
|
216
216
|
* A 2 é a camada de token DELE entrando na conta (13/08): `var(--color-ocean-500)`, que ele declara
|
|
217
217
|
* no CSS global, deixou de ser invisível e passou a contar como valor governado.
|
|
218
|
+
*
|
|
219
|
+
* A 3 é cada comprimento de um shorthand entrando na conta (09/09): `padding: 8px 24px` contava um
|
|
220
|
+
* valor e agora conta dois. Sem subir a régua, a tendência anunciava uma QUEDA que ninguém fez -
|
|
221
|
+
* medido: `coverage 40% -> 0% since the first run` na cópia do `web-subscribe`, e `30% -> 28%` no
|
|
222
|
+
* `frontend-hub`. O denominador cresceu porque a leitura melhorou, e comparar as duas fotos é
|
|
223
|
+
* comparar réguas diferentes.
|
|
224
|
+
*/
|
|
225
|
+
export const COVERAGE_RULE = 3;
|
|
226
|
+
/**
|
|
227
|
+
* POR QUE A TENDÊNCIA RECOMEÇOU - uma frase por régua, e a saída lê ESTA, nunca uma fixa.
|
|
228
|
+
*
|
|
229
|
+
* A frase estava cravada no `doctor` desde 13/08 (*"your own tokens count now too"*), então na
|
|
230
|
+
* primeira vez que a régua subiu por outro motivo ela explicaria a causa ERRADA para quem lê. A
|
|
231
|
+
* razão viaja com a régua: quem sobe uma escreve a outra ao lado.
|
|
218
232
|
*/
|
|
219
|
-
export const
|
|
233
|
+
export const COVERAGE_RULE_REASON = {
|
|
234
|
+
2: "your own tokens count now too",
|
|
235
|
+
3: "every length in a shorthand counts now - `padding: 8px 24px` was one value and is two",
|
|
236
|
+
};
|
|
220
237
|
export function summarize(events) {
|
|
221
238
|
const hooks = events.filter((e) => e.kind === "hook" && e.file);
|
|
222
239
|
const byFile = new Map();
|
package/dist/doctor/scan.js
CHANGED
|
@@ -74,10 +74,43 @@ export const COLOR = /#[0-9a-fA-F]{8}\b|#[0-9a-fA-F]{6}\b|#[0-9a-fA-F]{3}\b|rgba
|
|
|
74
74
|
* JSX also camelCases, so `borderRadius` has to be as welcome as
|
|
75
75
|
* `border-radius`.
|
|
76
76
|
*/
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
*
|
|
80
|
-
|
|
77
|
+
/**
|
|
78
|
+
* CADA COMPRIMENTO DA DECLARAÇÃO, e não só o primeiro - a dívida que a `INV-VOC-09` declarou.
|
|
79
|
+
*
|
|
80
|
+
* O padrão exigia o número CONTÍGUO à propriedade, então `padding: 8px 24px` rendia `8px` e o
|
|
81
|
+
* `24px` não existia para a esteira: nem como achado, nem na fila do `absorb`. Medido em 09/09 nas
|
|
82
|
+
* duas populações:
|
|
83
|
+
*
|
|
84
|
+
* declarações de espaço/raio com comprimento 3694 (web-subscribe) 2970 (frontend-hub)
|
|
85
|
+
* com 2+ comprimentos (shorthand) 501 (14%) 482 (16%)
|
|
86
|
+
* ocorrências que a lista NÃO via 619 540
|
|
87
|
+
* na PRIMEIRA TELA (as 40 mais repetidas) 5 entram, 5 saem 4 entram, 4 saem
|
|
88
|
+
* maior subcontagem de um valor 16px: 287 -> 398 577 -> 773
|
|
89
|
+
*
|
|
90
|
+
* A promessa *"o arquivo tem os 40 mais repetidos"* era falsa, porque a contagem não era a real - e
|
|
91
|
+
* na MESMA linha impressa havia dois números discordando, o `because` do passe de função (que conta
|
|
92
|
+
* todos os comprimentos) e a coluna `<spacing, N files>` do scan (parcial).
|
|
93
|
+
*
|
|
94
|
+
* A IMUNIDADE À ÂNCORA É PRESERVADA, e é o que separa esta leitura da do passe de função: o valor é
|
|
95
|
+
* lido DENTRO da declaração (até `;`, `}`, `]`, aspa ou fim de linha), então um `width: 100px` na
|
|
96
|
+
* mesma linha nunca é lido como padding. Duas réguas, a mesma resposta.
|
|
97
|
+
*/
|
|
98
|
+
const DECL_VALUE = `\\s*:\\s*["'\`]?([^;}\\]"'\`\\n]*)`;
|
|
99
|
+
/**
|
|
100
|
+
* O comprimento dentro de um valor. O lookbehind impede `4px` dentro de `24px`; o guard de trás
|
|
101
|
+
* aceita o `_` que o Tailwind usa por espaço (`8px_16px`), como o literal de tempo já faz.
|
|
102
|
+
*/
|
|
103
|
+
const LENGTH_IN_VALUE = /(?<![\d.])(-?\d*\.?\d+)(px|rem|em)(?![a-zA-Z0-9%])/g;
|
|
104
|
+
/** `border-radius: 14px 8px`, `borderRadius: "14px"` - a declaração inteira. */
|
|
105
|
+
const RADIUS_DECL = new RegExp(`border-?[Rr]adius${DECL_VALUE}`, "g");
|
|
106
|
+
/**
|
|
107
|
+
* `rounded-[14px]`, `rounded-[4px_8px]` - o que está entre os colchetes é um VALOR, e o Tailwind
|
|
108
|
+
* escreve o espaço como `_`. A primeira versão desta rodada capturava um comprimento por par de
|
|
109
|
+
* colchetes, então `rounded-[4px_8px]` rendia só o `4px` - o mesmo defeito que o outro lado
|
|
110
|
+
* consertou, e o comentário de `LENGTH_IN_VALUE` já citava `8px_16px` como a razão de aceitar `_`.
|
|
111
|
+
* N=0 nas três populações medidas em 09/09: o defeito não estava na amostra, estava na promessa.
|
|
112
|
+
*/
|
|
113
|
+
const RADIUS_UTIL = /rounded(?:-[a-z]+)?-\[([^\]]*)/g;
|
|
81
114
|
/**
|
|
82
115
|
* Arbitrary spacing: `p-[18px]`, `gap-[7px]`, `margin: 18px`, `gap: "18px"`.
|
|
83
116
|
* The JSX side also writes `paddingLeft`, `marginTop` and friends.
|
|
@@ -106,7 +139,9 @@ const RADIUS = new RegExp(`(?:border-?[Rr]adius\\s*:\\s*${OPEN}|rounded(?:-[a-z]
|
|
|
106
139
|
* Duas gramáticas para a mesma propriedade é o tipo de coisa que só aparece quando alguém aponta
|
|
107
140
|
* para um arquivo e conta na mão.
|
|
108
141
|
*/
|
|
109
|
-
const
|
|
142
|
+
const SPACING_DECL = new RegExp(`(?:padding|margin|gap)(?:[A-Z][a-z]+|(?:-[a-z]+)+)?${DECL_VALUE}`, "g");
|
|
143
|
+
/** `p-[18px]`, `gap-[7px]`, `p-[8px_16px]` - o valor inteiro entre os colchetes. */
|
|
144
|
+
const SPACING_UTIL = /(?:\b[pmg](?:[trblxy])?-\[|gap-\[)([^\]]*)/g;
|
|
110
145
|
/** A font stack written by hand rather than taken from the type scale. */
|
|
111
146
|
const FONT = /font-family\s*:\s*([^;}\n]+)/g;
|
|
112
147
|
/**
|
|
@@ -438,6 +473,16 @@ function scanCore(file, source, table) {
|
|
|
438
473
|
let svgDepth = 0;
|
|
439
474
|
source.split("\n").forEach((raw, i) => {
|
|
440
475
|
const line = raw.trim();
|
|
476
|
+
/**
|
|
477
|
+
* O RECUO DA LINHA, para as COLUNAS do achado serem as do ARQUIVO.
|
|
478
|
+
*
|
|
479
|
+
* A varredura trabalha na linha aparada, e todos os guards de posição (fallback, markup,
|
|
480
|
+
* conteúdo) medem contra ela. `Finding.cols` sai para fora - é o `--fix` que a usa para escrever
|
|
481
|
+
* no arquivo DELE -, então ela precisa da coluna real: sem somar o recuo, um `border-radius: 4px`
|
|
482
|
+
* indentado com dois espaços apontava para dois caracteres antes do literal, e a troca não
|
|
483
|
+
* acontecia (o comando dizia que a linha havia mudado desde a medição).
|
|
484
|
+
*/
|
|
485
|
+
const indent = line ? raw.indexOf(line) : 0;
|
|
441
486
|
const at = i + 1;
|
|
442
487
|
tokenUses += countTokenUses(line, table);
|
|
443
488
|
/**
|
|
@@ -519,8 +564,20 @@ function scanCore(file, source, table) {
|
|
|
519
564
|
return;
|
|
520
565
|
}
|
|
521
566
|
const key = `${kind}:${literal}`;
|
|
522
|
-
|
|
567
|
+
/**
|
|
568
|
+
* A MESMA DECISÃO É UM ACHADO E VÁRIAS ESCRITAS: a segunda ocorrência não vira uma linha nova
|
|
569
|
+
* no relatório, mas a coluna dela entra no achado que já existe - senão o `--fix` deixaria
|
|
570
|
+
* metade da linha literal.
|
|
571
|
+
*/
|
|
572
|
+
const written = col >= 0 && line.startsWith(literal, col) ? col + indent : -1;
|
|
573
|
+
if (seen.has(key)) {
|
|
574
|
+
if (written >= 0) {
|
|
575
|
+
const first = findings.find((f) => f.kind === kind && f.literal === literal && f.line === at);
|
|
576
|
+
if (first && !first.cols?.includes(written))
|
|
577
|
+
first.cols = [...(first.cols ?? []), written];
|
|
578
|
+
}
|
|
523
579
|
return;
|
|
580
|
+
}
|
|
524
581
|
seen.add(key);
|
|
525
582
|
// The kind is already known here, and without passing it the lookup
|
|
526
583
|
// answers a `gap` with a radius token.
|
|
@@ -536,6 +593,7 @@ function scanCore(file, source, table) {
|
|
|
536
593
|
kind,
|
|
537
594
|
line: at,
|
|
538
595
|
literal,
|
|
596
|
+
...(written >= 0 ? { cols: [written] } : {}),
|
|
539
597
|
token: match?.token ?? null,
|
|
540
598
|
...(/(?<!r)em$/i.test(literal.trim())
|
|
541
599
|
? { fontRelative: true }
|
|
@@ -576,16 +634,42 @@ function scanCore(file, source, table) {
|
|
|
576
634
|
}
|
|
577
635
|
push("color", m[0], m.index ?? -1);
|
|
578
636
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
637
|
+
/**
|
|
638
|
+
* CADA COMPRIMENTO DA DECLARAÇÃO, com a COLUNA DELE - e a coluna importa: os três guards de
|
|
639
|
+
* posição (fallback de `var()`, markup como texto, conteúdo que a página mostra) decidem por
|
|
640
|
+
* ela. Passar a coluna da propriedade para todos faria o segundo valor de um shorthand ser
|
|
641
|
+
* julgado pelo lugar do primeiro.
|
|
642
|
+
*/
|
|
643
|
+
const pushLengths = (kind, m) => {
|
|
644
|
+
const whole = m[1] ?? "";
|
|
645
|
+
/**
|
|
646
|
+
* O VALOR PARA NA PRÓXIMA PROPRIEDADE ESCRITA - e sem isto a leitura vazava por duas formas
|
|
647
|
+
* que o QA reproduziu em 09/09 (N=0 nas três populações, 1335 arquivos Sass/Less varridos):
|
|
648
|
+
*
|
|
649
|
+
* @supports (padding: 8px) and (width: 100px) contava o `100px` como padding
|
|
650
|
+
* $r: (border-radius: 6px, font-size: 14px) contava o `14px` como raio
|
|
651
|
+
*
|
|
652
|
+
* Os terminadores de `DECL_VALUE` (`;`, `}`, `]`, aspa, fim de linha) não fecham nenhuma das
|
|
653
|
+
* duas: a declaração de verdade acabou onde a PRÓXIMA começou.
|
|
654
|
+
*/
|
|
655
|
+
const cut = /(?<![\w-])[a-zA-Z-]{2,}\s*:/.exec(whole);
|
|
656
|
+
const value = cut ? whole.slice(0, cut.index) : whole;
|
|
657
|
+
const base = (m.index ?? 0) + m[0].length - whole.length;
|
|
658
|
+
for (const l of value.matchAll(LENGTH_IN_VALUE)) {
|
|
659
|
+
const literal = `${l[1]}${l[2]}`;
|
|
660
|
+
/** `0`, `1px`, `9999px`: idiom em QUALQUER posição, não só na primeira. */
|
|
661
|
+
if (!IDIOM.has(literal))
|
|
662
|
+
push(kind, literal, base + (l.index ?? 0));
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
for (const m of line.matchAll(RADIUS_UTIL))
|
|
666
|
+
pushLengths("radius", m);
|
|
667
|
+
for (const m of line.matchAll(RADIUS_DECL))
|
|
668
|
+
pushLengths("radius", m);
|
|
669
|
+
for (const m of line.matchAll(SPACING_UTIL))
|
|
670
|
+
pushLengths("spacing", m);
|
|
671
|
+
for (const m of line.matchAll(SPACING_DECL))
|
|
672
|
+
pushLengths("spacing", m);
|
|
589
673
|
for (const m of line.matchAll(FONT)) {
|
|
590
674
|
const stack = m[1].trim();
|
|
591
675
|
// A stack already reading from the system is the point, not a problem.
|
package/dist/global-sheet.js
CHANGED
|
@@ -65,13 +65,39 @@ async function fromWorkspace(root, spec) {
|
|
|
65
65
|
}
|
|
66
66
|
/**
|
|
67
67
|
* A folha onde os tokens dele devem entrar, relativa à raiz - `appSheet` quando nada a re-exporta.
|
|
68
|
+
*
|
|
69
|
+
* DELEGA, e não caminha: quem caminha é `sheetChainOf`. Dois caminhadores sobre a mesma cadeia é
|
|
70
|
+
* como um deles para de seguir um salto que o outro segue, e ninguém descobre até um cliente
|
|
71
|
+
* receber a instrução apontando para a folha errada - que é exatamente o defeito que
|
|
72
|
+
* `INV-VOLTA-12` fechou.
|
|
68
73
|
*/
|
|
69
74
|
export async function globalSheetOf(root, appSheet) {
|
|
75
|
+
const chain = await sheetChainOf(root, appSheet);
|
|
76
|
+
return chain[chain.length - 1] ?? appSheet;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* A CADEIA INTEIRA, da folha do app até a última que ninguém re-exporta - e por que ela é pública.
|
|
80
|
+
*
|
|
81
|
+
* O QUE O CLIENTE GANHA: a resposta "este app carrega o meu design system?" medida no app DELE, e
|
|
82
|
+
* não no projeto. `readWiring` varria a raiz e devolvia "existe em algum lugar daqui": num monorepo
|
|
83
|
+
* com dois apps servidos, um fiado e outro não, o fiado respondia pelo outro - e a pessoa recebia
|
|
84
|
+
* "está tudo certo" sobre o app que não carrega nada.
|
|
85
|
+
*
|
|
86
|
+
* A pergunta dos IMPORTS não se responde varrendo pasta: ela se responde seguindo a cadeia de
|
|
87
|
+
* `@import` a partir da folha daquele app, porque a folha que carrega os tokens quase nunca é a do
|
|
88
|
+
* app - num monorepo é a do pacote compartilhado, e os dois apps chegam nela.
|
|
89
|
+
*
|
|
90
|
+
* A ordem é do app para fora, e o teto de saltos é o mesmo: um ciclo de imports não pode travar um
|
|
91
|
+
* comando.
|
|
92
|
+
*/
|
|
93
|
+
export async function sheetChainOf(root, appSheet) {
|
|
94
|
+
const chain = [];
|
|
70
95
|
let current = appSheet;
|
|
71
96
|
for (let hop = 0; hop < MAX_HOPS; hop += 1) {
|
|
97
|
+
chain.push(current);
|
|
72
98
|
const raw = await readFile(join(root, current), "utf8").catch(() => null);
|
|
73
99
|
if (!raw)
|
|
74
|
-
return
|
|
100
|
+
return chain;
|
|
75
101
|
let next = null;
|
|
76
102
|
for (const m of raw.matchAll(IMPORT)) {
|
|
77
103
|
const spec = m[1];
|
|
@@ -91,11 +117,12 @@ export async function globalSheetOf(root, appSheet) {
|
|
|
91
117
|
next = candidate;
|
|
92
118
|
break;
|
|
93
119
|
}
|
|
94
|
-
|
|
95
|
-
|
|
120
|
+
/** Uma folha que aponta para si mesma encerra a cadeia em vez de gastar o teto de saltos. */
|
|
121
|
+
if (!next || next === current || chain.includes(next))
|
|
122
|
+
return chain;
|
|
96
123
|
current = next;
|
|
97
124
|
}
|
|
98
|
-
return
|
|
125
|
+
return chain;
|
|
99
126
|
}
|
|
100
127
|
/** O `../` que leva daquela folha até a raiz do repositório - o prefixo do `@import`. */
|
|
101
128
|
export function prefixFrom(sheet) {
|
package/dist/install-marks.js
CHANGED
|
@@ -250,7 +250,15 @@ export const MATERIALISER_SINCE = "0.16.403";
|
|
|
250
250
|
* `#6b6b78 is already --color-ink-500` numa frase da landing e manda tokenizar TEXTO. Um hook
|
|
251
251
|
* pinado antes desta versão aconselha OUTRA coisa, que é a régua desta marca.
|
|
252
252
|
*/
|
|
253
|
-
|
|
253
|
+
/**
|
|
254
|
+
* 0.16.378 -> 0.16.408 em 09/09: o hook passa a ver CADA comprimento de uma declaração. `scanSource`
|
|
255
|
+
* exigia o número contíguo à propriedade, então num `padding: 8px 24px` ele aconselhava sobre o
|
|
256
|
+
* `8px` e ficava calado sobre o `24px` - mesmo com um `--spacing-24` do próprio repositório dele
|
|
257
|
+
* nomeando aquele valor. Medido em 09/09 nas duas populações: 619 e 540 ocorrências que a leitura
|
|
258
|
+
* não via, 14% e 16% das declarações de espaço/raio. Um hook pinado antes desta versão aconselha
|
|
259
|
+
* MENOS, que é a régua desta marca.
|
|
260
|
+
*/
|
|
261
|
+
export const CHECKER_SINCE = "0.16.408";
|
|
254
262
|
/**
|
|
255
263
|
* A ÚLTIMA VERSÃO EM QUE OS LEITORES PASSARAM A PRODUZIR UM CENSO DIFERENTE.
|
|
256
264
|
*
|
|
@@ -691,7 +699,15 @@ export const CHECKER_SINCE = "0.16.378";
|
|
|
691
699
|
* muda um byte. Medido em 06/09: `frontend-hub/apps/web-dashboard` tem UM grupo acima (6 393), e
|
|
692
700
|
* `packages/ui` e `apps/web-review` nenhum.
|
|
693
701
|
*/
|
|
694
|
-
|
|
702
|
+
/**
|
|
703
|
+
* 0.16.383 -> 0.16.408 em 09/09: o censo passou a carregar cada comprimento de um shorthand. Um
|
|
704
|
+
* `padding: 8px 24px` gravava só o `8px`, então o censo de quem já importou afirma menos valores à
|
|
705
|
+
* mão do que o repositório dele tem - 619 ocorrências no `web-subscribe` e 540 no `frontend-hub`, e
|
|
706
|
+
* na primeira tela do `absorb` (as 40 mais repetidas) 5 e 4 valores ocupavam o lugar de outros que
|
|
707
|
+
* deveriam estar lá. Quem tem censo gravado precisa de um `sync` para a fila de trabalho dele ser a
|
|
708
|
+
* real; ver o livro-razão de `corpus.spec.ts`.
|
|
709
|
+
*/
|
|
710
|
+
export const READER_SINCE = "0.16.408";
|
|
695
711
|
/**
|
|
696
712
|
* O QUE ESTÁ INSTALADO AQUI FICOU PARA TRÁS - e as DUAS condições que fazem isso ser verdade.
|
|
697
713
|
*
|
package/dist/wiring-read.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { walk } from "./commands/doctor.js";
|
|
4
4
|
import { FAMILY_SEAM_PREFIX } from "./fonts.js";
|
|
5
|
+
import { detectAppDirs, sheetChainOf } from "./global-sheet.js";
|
|
5
6
|
/**
|
|
6
7
|
* A FIAÇÃO DESTE PROJETO, MEDIDA - e a mesma medição para todos os comandos que a citam.
|
|
7
8
|
*
|
|
@@ -111,3 +112,79 @@ export async function readWiring(root, slug) {
|
|
|
111
112
|
}
|
|
112
113
|
return w;
|
|
113
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* A FIAÇÃO DE CADA APP, e num monorepo é ela que decide - `readWiring` acima não decide.
|
|
117
|
+
*
|
|
118
|
+
* O QUE O CLIENTE VIA: `readWiring` varre da RAIZ e responde "existe, em algum lugar daqui". Num
|
|
119
|
+
* monorepo com dois apps servidos, um fiado e outro não, o fiado respondia pelo outro: a pessoa
|
|
120
|
+
* abria o app que não carrega nada e o comando dizia que estava tudo certo. O contrato já declarava
|
|
121
|
+
* isso como lacuna (`INV-VOLTA-12`, "a resposta é do PROJETO, e num monorepo ela não decide") e o
|
|
122
|
+
* comando dizia o que mediu em vez de afirmar - honesto, e ainda a resposta errada.
|
|
123
|
+
*
|
|
124
|
+
* COMO A RESPOSTA CERTA É ENCONTRADA, e cada metade vem de onde ela mora:
|
|
125
|
+
*
|
|
126
|
+
* os `@import` seguindo a CADEIA de folhas a partir da folha daquele app (`sheetChainOf`).
|
|
127
|
+
* A folha que carrega os tokens quase nunca é a do app: num monorepo é a do
|
|
128
|
+
* pacote compartilhado, e os dois apps chegam nela - então varrer pasta daria
|
|
129
|
+
* "não" para os dois, e varrer a raiz dá "sim" para todos
|
|
130
|
+
* o `data-ds` na árvore DAQUELE app, porque o escopo mora no layout dele
|
|
131
|
+
* as fontes idem - `init` escreve o arquivo de fonte por app
|
|
132
|
+
*
|
|
133
|
+
* O QUE ESTA FUNÇÃO NÃO GENERALIZA, declarado com o N de hoje (medido em 09/09):
|
|
134
|
+
*
|
|
135
|
+
* `detectAppDirs` só procura em `apps/` e `packages/` cobre 4 de 4 monorepos medidos;
|
|
136
|
+
* `pnpm-workspace.yaml` aparece em 0 de 10
|
|
137
|
+
* e só reconhece app pelo `layout` do App Router cobre 7 de 7 raízes de app medidas;
|
|
138
|
+
* `vite` aparece só em PACOTE de
|
|
139
|
+
* biblioteca, corretamente ignorado
|
|
140
|
+
*
|
|
141
|
+
* As duas são estreitezas REAIS e as duas têm N=0 de evidência contrária hoje. Generalizá-las sem
|
|
142
|
+
* uma população que as exija seria dimensionar pela imaginação - o erro que `INV-GERAL-07` nomeia
|
|
143
|
+
* pelo avesso. Quando aparecer um repositório que caia fora, o número dele é que abre a frente.
|
|
144
|
+
*/
|
|
145
|
+
export async function readWiringPerApp(root, slug, pagesDir) {
|
|
146
|
+
if (!slug)
|
|
147
|
+
return [];
|
|
148
|
+
const apps = await detectAppDirs(root, pagesDir);
|
|
149
|
+
const out = [];
|
|
150
|
+
const hasShadcn = await readFile(join(root, "components.json"), "utf8").then(() => true, () => false);
|
|
151
|
+
for (const app of apps) {
|
|
152
|
+
const w = {
|
|
153
|
+
imported: false,
|
|
154
|
+
themed: false,
|
|
155
|
+
scoped: false,
|
|
156
|
+
fontsWritten: false,
|
|
157
|
+
fontsMapped: false,
|
|
158
|
+
hasShadcn,
|
|
159
|
+
bridged: false,
|
|
160
|
+
};
|
|
161
|
+
/** Os imports: a cadeia daquele app, e não a pasta dele. */
|
|
162
|
+
for (const sheet of await sheetChainOf(root, `${app}/globals.css`)) {
|
|
163
|
+
const css = await readFile(join(root, sheet), "utf8").catch(() => "");
|
|
164
|
+
if (!css)
|
|
165
|
+
continue;
|
|
166
|
+
if (css.includes(`_synthesisui/ds/${slug}/tokens.css`))
|
|
167
|
+
w.imported = true;
|
|
168
|
+
if (css.includes(`_synthesisui/ds/${slug}/theme.css`))
|
|
169
|
+
w.themed = true;
|
|
170
|
+
if (css.includes(`_synthesisui/ds/${slug}/shadcn.css`))
|
|
171
|
+
w.bridged = true;
|
|
172
|
+
}
|
|
173
|
+
/** O escopo e as fontes: a árvore DAQUELE app - é onde o layout dele mora. */
|
|
174
|
+
for await (const file of walk(join(root, app))) {
|
|
175
|
+
const src = await readFile(file, "utf8").catch(() => "");
|
|
176
|
+
if (!src)
|
|
177
|
+
continue;
|
|
178
|
+
if (src.includes(`data-ds="${slug}"`))
|
|
179
|
+
w.scoped = true;
|
|
180
|
+
if (src.includes("--font-ds-") && /next\/font/.test(src))
|
|
181
|
+
w.fontsWritten = true;
|
|
182
|
+
if (new RegExp(`${FAMILY_SEAM_PREFIX}\\w+\\s*:\\s*var\\(\\s*--font-ds-`).test(src))
|
|
183
|
+
w.fontsMapped = true;
|
|
184
|
+
if (w.scoped && w.fontsMapped)
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
out.push({ app, wiring: w });
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
}
|
package/package.json
CHANGED