synthesisui 0.16.390 → 0.16.395
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/absorb-plan.js +77 -6
- package/dist/commands/absorb.js +44 -3
- package/dist/commands/doctor.js +57 -10
- package/dist/doctor/scan.js +23 -3
- package/dist/doctor/style-ledger.js +20 -0
- package/dist/doctor/their-compiled-names.js +121 -0
- package/dist/doctor/their-names.js +39 -4
- package/dist/doctor/tokens.js +37 -1
- package/dist/stack.js +30 -2
- package/dist/their-vars.js +38 -7
- package/package.json +1 -1
package/dist/absorb-plan.js
CHANGED
|
@@ -10,7 +10,11 @@ const HOME = {
|
|
|
10
10
|
};
|
|
11
11
|
/** Uma palavra de categoria no começo do nome deles não é família - é a categoria repetida. */
|
|
12
12
|
const CATEGORY = /^(color|colour|radius|rounded|spacing|space|gap|font|type|text|duration|motion|ease|easing)-/;
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* O nome sem o sigilo. `$` e `@` entram porque o vocabulário dele pode estar num pré-processador -
|
|
15
|
+
* um `$gray_dark` que mantivesse o `$` viraria o caminho `color.$gray.dark`, que não é um caminho.
|
|
16
|
+
*/
|
|
17
|
+
const clean = (name) => name.replace(/^(--|[$@])/, "").toLowerCase();
|
|
14
18
|
/**
|
|
15
19
|
* QUAL DOS NOMES DELE, quando mais de um segura o mesmo valor - e aqui a natureza do ACHADO decide.
|
|
16
20
|
*
|
|
@@ -105,7 +109,31 @@ function nearestOwn(theirs, literal, kind) {
|
|
|
105
109
|
* existente com outro valor - isso não é absorver, é repintar.
|
|
106
110
|
*/
|
|
107
111
|
export function absorbPlan(d, theirs, have, cap = 40) {
|
|
108
|
-
|
|
112
|
+
/**
|
|
113
|
+
* A REPETIÇÃO É UM FILTRO DE RUÍDO, E RUÍDO É UMA PROPRIEDADE DA ESCALA - não do valor.
|
|
114
|
+
*
|
|
115
|
+
* Ordenar por repetição está certo onde a deriva se concentra: no `~/projects/web-subscribe` são
|
|
116
|
+
* 858 valores distintos escritos à mão, e mostrar os 40 mais repetidos é o que transforma isso numa
|
|
117
|
+
* fila de trabalho. Num projeto que acabou de nascer, o mesmo corte apaga o projeto inteiro.
|
|
118
|
+
*
|
|
119
|
+
* Medido em 07/09 sobre três populações:
|
|
120
|
+
*
|
|
121
|
+
* ~/projects/web-subscribe 858 distintos 484 repetidos 362 apareciam uma vez só
|
|
122
|
+
* ~/projects/web-onboarding 52 distintos 21 repetidos 29 apareciam uma vez só
|
|
123
|
+
* create-next-app real 4 distintos 0 repetidos 4 apareciam uma vez só
|
|
124
|
+
*
|
|
125
|
+
* Naquele último o `absorb` respondia que não havia nada a absorver, três linhas depois de o
|
|
126
|
+
* `doctor` dizer *"0 of 4 have a name waiting"*.
|
|
127
|
+
*
|
|
128
|
+
* A REGRA SAI DO TETO QUE JÁ EXISTE, e por isso não é mais um número escolhido por nós: o corte
|
|
129
|
+
* serve para ESCOLHER quando não cabe tudo. Quando tudo cabe, não há o que escolher, e esconder
|
|
130
|
+
* metade é esconder por hábito. Num repositório grande nada muda - 484 já estouram o teto sozinhos.
|
|
131
|
+
*/
|
|
132
|
+
const repeated = d.repeats.filter((r) => !r.token);
|
|
133
|
+
const single = d.once.filter((r) => !r.token);
|
|
134
|
+
const unnamed = repeated.length + single.length <= cap
|
|
135
|
+
? [...repeated, ...single]
|
|
136
|
+
: repeated;
|
|
109
137
|
const entries = [];
|
|
110
138
|
for (const r of unnamed) {
|
|
111
139
|
if (entries.length >= cap)
|
|
@@ -133,7 +161,25 @@ export function absorbPlan(d, theirs, have, cap = 40) {
|
|
|
133
161
|
* `var(--dashboard-white-500)` e o `absorb`, no mesmo dia, pedia que ele batizasse `#fff`. Dois
|
|
134
162
|
* comandos com conselhos opostos sobre o mesmo valor.
|
|
135
163
|
*/
|
|
136
|
-
|
|
164
|
+
/**
|
|
165
|
+
* O NOME DELE VEM DAS DUAS FORMAS - e sem a segunda os dois comandos voltam a se contradizer.
|
|
166
|
+
*
|
|
167
|
+
* É o mesmo defeito que esta função já corrigiu uma vez, reaberto por outro caminho: o `doctor`
|
|
168
|
+
* passou a dizer *"#555 · your code calls it $gray_dark"* e o `absorb`, no mesmo repositório,
|
|
169
|
+
* pedia que ele batizasse o `#555`. Dois comandos com conselhos opostos sobre o mesmo valor.
|
|
170
|
+
*
|
|
171
|
+
* A ORDEM É A MESMA DO RESTO DA ESTEIRA: primeiro o nome que o navegador recebe, e só depois o
|
|
172
|
+
* que o pré-processador resolve. Onde ele nomeia nas duas formas, ganha a custom property.
|
|
173
|
+
*
|
|
174
|
+
* E aqui o nome compilado NÃO é um beco: o caminho que sai dele vira um token de verdade na
|
|
175
|
+
* fundação, que o navegador recebe - então as ocorrências em `.tsx`, que nunca poderiam receber
|
|
176
|
+
* um `$`, ficam trocáveis pelo caminho que já existe (`absorb` -> `upgrade` -> `doctor --fix`).
|
|
177
|
+
* Medido em 07/09 no `~/projects/web-subscribe`: 323 ocorrências têm nome dele, e só 52 delas
|
|
178
|
+
* estão num arquivo que poderia escrever o `$` diretamente.
|
|
179
|
+
*/
|
|
180
|
+
const value = normalizeValue(r.literal, theirs.rootPx);
|
|
181
|
+
const theirName = nameOf(r.kind, theirs.byValue.get(value)) ??
|
|
182
|
+
nameOf(r.kind, theirs.compiledAway.get(value)?.map((c) => c.name));
|
|
137
183
|
const path = pathFor(r.kind, theirName);
|
|
138
184
|
/** "≈ perto de um token seu" só interessa a quem não tem um EXATO - com o nome na mão, a dica
|
|
139
185
|
* vira ruído, e no arquivo de proposta ela vira uma segunda opção que não é opção. */
|
|
@@ -151,7 +197,11 @@ export function absorbPlan(d, theirs, have, cap = 40) {
|
|
|
151
197
|
...(near ? { near } : {}),
|
|
152
198
|
});
|
|
153
199
|
}
|
|
154
|
-
return {
|
|
200
|
+
return {
|
|
201
|
+
entries,
|
|
202
|
+
more: Math.max(0, unnamed.length - entries.length),
|
|
203
|
+
unnamed: d.repeats.length + d.once.length,
|
|
204
|
+
};
|
|
155
205
|
}
|
|
156
206
|
/** Quantas linhas da proposta ainda precisam de um nome humano. */
|
|
157
207
|
export const needingName = (plan) => plan.entries.filter((e) => !e.path);
|
|
@@ -166,9 +216,21 @@ export function describeAbsorb(plan) {
|
|
|
166
216
|
const ready = plan.entries.filter((e) => e.path);
|
|
167
217
|
const pending = needingName(plan);
|
|
168
218
|
const lines = [];
|
|
219
|
+
/**
|
|
220
|
+
* VAZIO POR DOIS MOTIVOS OPOSTOS, e a frase afirmava só um.
|
|
221
|
+
*
|
|
222
|
+
* "every repeated value already has a name" é notícia boa. Mas a lista também fica vazia quando não
|
|
223
|
+
* há valor NENHUM escrito à mão - e aí a frase de cima descreve um repositório que não é o dele.
|
|
224
|
+
* Medido em 07/09 numa cópia de `create-next-app`: 4 valores sem nome, e esta era a resposta.
|
|
225
|
+
*
|
|
226
|
+
* A distinção não é cosmética: um manda seguir em frente, o outro diz que o comando não tem
|
|
227
|
+
* trabalho porque o projeto não tem deriva. Duas conversas diferentes.
|
|
228
|
+
*/
|
|
169
229
|
if (plan.entries.length === 0)
|
|
170
230
|
return [
|
|
171
|
-
|
|
231
|
+
plan.unnamed > 0
|
|
232
|
+
? `Nothing to absorb here: the ${plan.unnamed} value${plan.unnamed === 1 ? "" : "s"} written by hand ${plan.unnamed === 1 ? "already has" : "already have"} a name in your system.`
|
|
233
|
+
: "Nothing to absorb: no design value in this project is written by hand.",
|
|
172
234
|
];
|
|
173
235
|
/**
|
|
174
236
|
* A SEÇÃO DOS QUE JÁ TÊM NOME SÓ EXISTE SE ALGUM TIVER - e no dia 1 nenhum tem.
|
|
@@ -187,9 +249,18 @@ export function describeAbsorb(plan) {
|
|
|
187
249
|
const close = pending.filter((e) => e.near);
|
|
188
250
|
if (ready.length > 0)
|
|
189
251
|
lines.push("");
|
|
252
|
+
/**
|
|
253
|
+
* "REPEATED" SÓ QUANDO ELES SE REPETEM - e desde que o corte por repetição deixou de valer para
|
|
254
|
+
* projeto pequeno, essa palavra passou a descrever um repositório que não é o dele.
|
|
255
|
+
*
|
|
256
|
+
* Medido em 07/09 numa cópia de `create-next-app`: a linha dizia *"4 repeated values"* sobre
|
|
257
|
+
* quatro valores que aparecem uma vez cada. Uma palavra errada na primeira linha do comando gasta
|
|
258
|
+
* a credibilidade das outras dez.
|
|
259
|
+
*/
|
|
260
|
+
const everyOneRepeats = pending.every((e) => e.count > 1);
|
|
190
261
|
lines.push(ready.length > 0
|
|
191
262
|
? `${pending.length} nobody names yet. Naming is a design decision, so those wait for a word from you:`
|
|
192
|
-
: `${pending.length} repeated values, and none of them has a name yet. Naming is a design decision, so each waits for a word from you:`);
|
|
263
|
+
: `${pending.length} ${everyOneRepeats ? "repeated values" : "values written by hand"}, and none of them has a name yet. Naming is a design decision, so each waits for a word from you:`);
|
|
193
264
|
for (const e of pending.slice(0, 8))
|
|
194
265
|
lines.push(` ${e.value.padEnd(24)} ${`<${e.kind}, ${e.files} file${e.files === 1 ? "" : "s"}>`.padEnd(28)} ${e.near ? `≈ your ${e.near.name} (${e.near.away})` : 'path: ""'}`);
|
|
195
266
|
if (pending.length > 8)
|
package/dist/commands/absorb.js
CHANGED
|
@@ -3,8 +3,10 @@ import { dirname, join, resolve } from "node:path";
|
|
|
3
3
|
import { absorbPlan, describeAbsorb, needingName, } from "../absorb-plan.js";
|
|
4
4
|
import { readProjectConfig, readToken, resolveRegistry } from "../config.js";
|
|
5
5
|
import { diagnose, scanSource } from "../doctor/scan.js";
|
|
6
|
+
import { withTheirNames } from "../doctor/their-names.js";
|
|
6
7
|
import { measuredScope, scopePaths } from "../measured-scope.js";
|
|
7
8
|
import { body, paint, section, snippet } from "../output.js";
|
|
9
|
+
import { resolveDeps, tailwindMajor } from "../stack.js";
|
|
8
10
|
import { loadSystem, walkAll } from "./doctor.js";
|
|
9
11
|
/**
|
|
10
12
|
* `synthesisui absorb` - O SISTEMA APRENDE O DESIGN QUE O CÓDIGO JÁ TEM.
|
|
@@ -66,12 +68,28 @@ export async function absorb(opts) {
|
|
|
66
68
|
* ideia primeiro e ficou com a implementação de todo mundo.
|
|
67
69
|
*/
|
|
68
70
|
const theirs = installed.theirs;
|
|
71
|
+
/**
|
|
72
|
+
* SEM SISTEMA NOSSO, A TABELA DA VARREDURA É A DELE - a mesma queda que o `doctor` já faz.
|
|
73
|
+
*
|
|
74
|
+
* O QUE ACONTECIA: este comando varria com `installed.table`, que sem sistema instalado é a tabela
|
|
75
|
+
* VAZIA. Então nada tinha nome, nem o que o próprio código dele nomeia - e a linha
|
|
76
|
+
* `--background: #ffffff`, que é onde o token dele NASCE, entrava na proposta como um valor solto
|
|
77
|
+
* a ser batizado. Medido em 07/09 numa cópia de `create-next-app`: 4 das 8 entradas eram as
|
|
78
|
+
* declarações dos tokens dele, e duas delas já traziam `theirName` preenchido - a proposta pedindo
|
|
79
|
+
* um nome para o que ela mesma acabara de dizer que já tem um.
|
|
80
|
+
*
|
|
81
|
+
* O `doctor` resolve isto há tempos, e a linha é literalmente a dele: quando não há nada nosso, o
|
|
82
|
+
* vocabulário dele é o único que existe, e é contra ele que a medição acontece.
|
|
83
|
+
*/
|
|
84
|
+
const table = installed.table.byName.size > 0
|
|
85
|
+
? installed.table
|
|
86
|
+
: withTheirNames(theirs, theirs);
|
|
69
87
|
const reports = [];
|
|
70
88
|
for await (const file of walkAll(roots)) {
|
|
71
89
|
const source = await readFile(file, "utf8").catch(() => null);
|
|
72
90
|
if (source === null)
|
|
73
91
|
continue;
|
|
74
|
-
reports.push(scanSource(file.slice(root.length + 1), source,
|
|
92
|
+
reports.push(scanSource(file.slice(root.length + 1), source, table));
|
|
75
93
|
}
|
|
76
94
|
const plan = absorbPlan(diagnose(reports), theirs, pathsInSystem(installed.documents), opts.cap ?? 40);
|
|
77
95
|
console.log(section(hasSystem
|
|
@@ -173,13 +191,36 @@ async function sendProposal(root, path, registry) {
|
|
|
173
191
|
console.log(body(paint.dim(proposal.slug
|
|
174
192
|
? "Your config says absorbed values belong in your CSS, so nothing was sent."
|
|
175
193
|
: "You have no system yet, so these belong in your own CSS - nothing was sent, and nothing needed an account.")));
|
|
194
|
+
/**
|
|
195
|
+
* NUM TAILWIND v4, `:root` DECLARA A VARIÁVEL E NÃO GERA A CLASSE.
|
|
196
|
+
*
|
|
197
|
+
* O QUE ELE VIVIA: nomeava `#383838` de `color.ink.700`, colava o bloco que este comando
|
|
198
|
+
* imprime, e `text-ink-700` continuava não existindo. A variável estava lá, o doctor a
|
|
199
|
+
* reconhecia - e o código dele não tinha como usá-la. A metade visível da promessa não chegava.
|
|
200
|
+
*
|
|
201
|
+
* É a mesma lei que a `INV-VOLTA-13` já enuncia do outro lado da esteira: *"no Tailwind v4 só
|
|
202
|
+
* `@theme` gera utilitário"*. A folha que a plataforma instala sabe disso desde 23/08; o bloco
|
|
203
|
+
* que a plataforma manda ELE colar não sabia.
|
|
204
|
+
*
|
|
205
|
+
* E A SINTAXE SAI DO PROJETO, nunca de um padrão nosso - `tailwindMajor` já existe e o
|
|
206
|
+
* comentário dele diz por quê: *"gerar a sintaxe de uma para um projeto da outra produz um
|
|
207
|
+
* arquivo que o build DELE não entende, e é o pior tipo de saída, porque parece certa até
|
|
208
|
+
* alguém compilar"*. Sem Tailwind, ou na v3, `:root` continua sendo a resposta certa: lá o
|
|
209
|
+
* utilitário nasce do `tailwind.config`, não do CSS.
|
|
210
|
+
*/
|
|
211
|
+
const major = tailwindMajor(await resolveDeps(root));
|
|
212
|
+
const block = major === 4 ? "@theme" : ":root";
|
|
176
213
|
console.log("");
|
|
177
|
-
console.log(
|
|
214
|
+
console.log(` ${block} {`);
|
|
178
215
|
for (const e of ready)
|
|
179
216
|
console.log(` --${e.path.replace(/\./g, "-")}: ${e.value};`);
|
|
180
217
|
console.log(" }");
|
|
181
218
|
console.log("");
|
|
182
|
-
console.log(body(
|
|
219
|
+
console.log(body(major === 4
|
|
220
|
+
? "In `@theme` and not `:root`: on Tailwind v4 only `@theme` generates the utility, so this is what makes `text-ink-700` exist in your code."
|
|
221
|
+
: "The next import or `sync` reads these back as tokens you declared, and from then on the doctor holds your code to them by your own names."));
|
|
222
|
+
if (major === 4)
|
|
223
|
+
console.log(body("The next import or `sync` reads these back as tokens you declared, and from then on the doctor holds your code to them by your own names."));
|
|
183
224
|
return;
|
|
184
225
|
}
|
|
185
226
|
const token = await readToken();
|
package/dist/commands/doctor.js
CHANGED
|
@@ -400,14 +400,20 @@ async function harvestOwnTokens(roots,
|
|
|
400
400
|
* e `4px` deixaria de casar com `0.25rem` de um lado só. */
|
|
401
401
|
measured) {
|
|
402
402
|
let css = "";
|
|
403
|
+
/** UMA A UMA, e não só concatenadas: a regra de escopo de `their-compiled-names.ts` decide pelo
|
|
404
|
+
* `@import` de um arquivo no outro, e a concatenação apaga de qual arquivo cada linha veio. */
|
|
405
|
+
const sheets = new Map();
|
|
403
406
|
for await (const file of walkAll(roots)) {
|
|
404
407
|
if (!/\.(css|scss|sass|less)$/i.test(file))
|
|
405
408
|
continue;
|
|
406
|
-
|
|
409
|
+
const source = await readFile(file, "utf8").catch(() => "");
|
|
410
|
+
sheets.set(file, source);
|
|
411
|
+
css += `\n${source}`;
|
|
407
412
|
}
|
|
408
413
|
return buildTable({
|
|
409
414
|
css,
|
|
410
415
|
source: "yours",
|
|
416
|
+
sheets,
|
|
411
417
|
rootPx: measured?.px,
|
|
412
418
|
rootFrom: measured?.from ?? null,
|
|
413
419
|
});
|
|
@@ -464,10 +470,27 @@ function verdict(d, hasSystem, overruled, conflicts) {
|
|
|
464
470
|
*/
|
|
465
471
|
if (!hasSystem) {
|
|
466
472
|
const distinct = new Set(d.findings.map((f) => f.literal.toLowerCase()));
|
|
473
|
+
/**
|
|
474
|
+
* O QUE O CÓDIGO DELE JÁ NOMEIA - e sem esta linha o relatório se contradiz.
|
|
475
|
+
*
|
|
476
|
+
* "none of them has a name yet" é o fecho de um projeto sem sistema, e ele era verdade enquanto
|
|
477
|
+
* a plataforma só lia custom properties. Agora o detalhe imprime `#555 · your code calls it
|
|
478
|
+
* $gray_dark` três linhas acima: o resumo dizendo que nada tem nome, sobre um valor que a linha
|
|
479
|
+
* de cima acabou de nomear, é o defeito que faz alguém desconfiar do relatório inteiro.
|
|
480
|
+
*
|
|
481
|
+
* E o número muda a decisão dele: quem já batizou metade dos valores no Sass não está no dia
|
|
482
|
+
* zero - ele está a um `absorb` de trazer esses nomes para um vocabulário que o navegador
|
|
483
|
+
* recebe, o que é uma conversa diferente de "escolha o sistema de alguém".
|
|
484
|
+
*/
|
|
485
|
+
const spoken = new Set(d.findings
|
|
486
|
+
.filter((f) => f.theirCompiledName)
|
|
487
|
+
.map((f) => f.literal.toLowerCase()));
|
|
467
488
|
return [
|
|
468
489
|
...head,
|
|
469
490
|
body(`${distinct.size} distinct design values are written by hand here.`),
|
|
470
|
-
|
|
491
|
+
spoken.size === 0
|
|
492
|
+
? body("No design system is installed, so none of them has a name yet.")
|
|
493
|
+
: body(`No design system is installed, but your own code already names ${spoken.size} of them - in Sass or Less, which the browser never receives.`),
|
|
471
494
|
"",
|
|
472
495
|
body("Name them yourself - this reads them and asks you for the words:"),
|
|
473
496
|
snippet(["npx synthesisui@latest absorb"]),
|
|
@@ -1534,9 +1557,19 @@ export async function doctor(opts) {
|
|
|
1534
1557
|
? `→ no ${x.kind} named for it · the value lives as ${x.token}`
|
|
1535
1558
|
: nameToWrite(x)
|
|
1536
1559
|
? `→ ${nameToWrite(x)}${alsoNamed(x)}`
|
|
1537
|
-
:
|
|
1538
|
-
|
|
1539
|
-
|
|
1560
|
+
: /**
|
|
1561
|
+
* O CÓDIGO DELE JÁ NOMEIA ISTO, e a linha dizia que nada nomeava.
|
|
1562
|
+
*
|
|
1563
|
+
* Vem antes de `near` porque um nome EXATO que ele escreveu vale mais que o degrau
|
|
1564
|
+
* mais próximo de outra coisa. Sem seta de troca: `$gray_dark` não existe num `.tsx`
|
|
1565
|
+
* e a troca segura depende de duas provas que esta camada não tem - ver
|
|
1566
|
+
* `TheirName.writable`.
|
|
1567
|
+
*/
|
|
1568
|
+
x.theirCompiledName
|
|
1569
|
+
? `· your code calls it ${x.theirCompiledName}`
|
|
1570
|
+
: near
|
|
1571
|
+
? `→ nearest is ${near.theirs ?? near.name} (${near.value})`
|
|
1572
|
+
: "→ no token holds this value yet";
|
|
1540
1573
|
say(` ${String(x.line).padStart(4)} ${x.literal} ${named}`);
|
|
1541
1574
|
}
|
|
1542
1575
|
if (f.findings.length > shown.length) {
|
|
@@ -1891,11 +1924,25 @@ export async function doctor(opts) {
|
|
|
1891
1924
|
* que NENHUM dos dois nomeia o valor. É isso que a linha passa a dizer, e é o que separa esta
|
|
1892
1925
|
* fila da anterior: aquela tem nome esperando, esta precisa de um.
|
|
1893
1926
|
*/
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1927
|
+
/**
|
|
1928
|
+
* O SEU CÓDIGO JÁ NOMEIA ISTO, e a linha dizia o contrário.
|
|
1929
|
+
*
|
|
1930
|
+
* "no name for it, here or in your CSS" é uma afirmação sobre o repositório DELE, e ela era
|
|
1931
|
+
* falsa sempre que o nome estava num pré-processador: medido em 07/09 no `web-subscribe`,
|
|
1932
|
+
* `#555` é `$gray_dark` em `css/colors.scss` e aparecia como "sem nome" em 27 arquivos.
|
|
1933
|
+
*
|
|
1934
|
+
* A linha DIZ o nome e não convida a trocar - `$gray_dark` não existe num `.tsx`, e a troca
|
|
1935
|
+
* segura depende de o arquivo de destino compilar aquele pré-processador e importar aquela
|
|
1936
|
+
* folha, que é o próximo passo desta frente. Dizer o que existe já muda a decisão dele; trocar
|
|
1937
|
+
* sem essas duas provas quebraria o build.
|
|
1938
|
+
*/
|
|
1939
|
+
what: r.theirCompiledName
|
|
1940
|
+
? `${r.literal} - your code calls it ${r.theirCompiledName} (no fix: it never reaches the browser)`
|
|
1941
|
+
: near
|
|
1942
|
+
? `${r.literal} - name it, or snap to ${near.theirs ?? near.name}`
|
|
1943
|
+
: r.crossFamily
|
|
1944
|
+
? `${r.literal} - no ${r.kind} named for it, and the value lives as ${r.token} in another family`
|
|
1945
|
+
: `${r.literal} - no name for it, here or in your CSS`,
|
|
1899
1946
|
size: `${r.files} file${r.files === 1 ? "" : "s"}`,
|
|
1900
1947
|
cheap: false,
|
|
1901
1948
|
});
|
package/dist/doctor/scan.js
CHANGED
|
@@ -540,7 +540,18 @@ function scanCore(file, source, table) {
|
|
|
540
540
|
...(/(?<!r)em$/i.test(literal.trim())
|
|
541
541
|
? { fontRelative: true }
|
|
542
542
|
: {}),
|
|
543
|
-
...(theirs ? { theirToken: theirs.name } : {}),
|
|
543
|
+
...(theirs?.writable ? { theirToken: theirs.name } : {}),
|
|
544
|
+
/**
|
|
545
|
+
* O NOME QUE SÓ SE DIZ - `$gray_dark` do Sass, `@brand` do Less.
|
|
546
|
+
*
|
|
547
|
+
* Separado de `theirToken` porque aquele campo é o que o `--fix` ESCREVE, e um nome de
|
|
548
|
+
* pré-processador escrito num `.tsx` é código quebrado. Aqui ele serve ao relatório, que
|
|
549
|
+
* deixa de afirmar "no name for it, here or in your CSS" sobre um valor que o código dele
|
|
550
|
+
* nomeia - a afirmação era sobre o repositório DELE, e era falsa.
|
|
551
|
+
*/
|
|
552
|
+
...(theirs && !theirs.writable
|
|
553
|
+
? { theirCompiledName: theirs.name }
|
|
554
|
+
: {}),
|
|
544
555
|
...(theirs?.also.length ? { theirAlso: theirs.also } : {}),
|
|
545
556
|
/**
|
|
546
557
|
* A COINCIDÊNCIA VIAJA COM O ACHADO - ver `tokenMatch`.
|
|
@@ -737,6 +748,9 @@ export function diagnose(files) {
|
|
|
737
748
|
kind: f.kind,
|
|
738
749
|
token: f.token,
|
|
739
750
|
...(f.theirToken ? { theirToken: f.theirToken } : {}),
|
|
751
|
+
...(f.theirCompiledName
|
|
752
|
+
? { theirCompiledName: f.theirCompiledName }
|
|
753
|
+
: {}),
|
|
740
754
|
...(f.theirAlso?.length ? { theirAlso: f.theirAlso } : {}),
|
|
741
755
|
...(f.fontRelative ? { fontRelative: true } : {}),
|
|
742
756
|
count: 1,
|
|
@@ -745,25 +759,31 @@ export function diagnose(files) {
|
|
|
745
759
|
});
|
|
746
760
|
}
|
|
747
761
|
}
|
|
748
|
-
const
|
|
762
|
+
const everyLiteral = [...byLiteral.entries()]
|
|
749
763
|
.map(([key, v]) => ({
|
|
750
764
|
literal: key.slice(key.indexOf(":") + 1),
|
|
751
765
|
kind: v.kind,
|
|
752
766
|
token: v.token,
|
|
753
767
|
...(v.theirToken ? { theirToken: v.theirToken } : {}),
|
|
768
|
+
...(v.theirCompiledName
|
|
769
|
+
? { theirCompiledName: v.theirCompiledName }
|
|
770
|
+
: {}),
|
|
754
771
|
...(v.theirAlso?.length ? { theirAlso: v.theirAlso } : {}),
|
|
755
772
|
...(v.fontRelative ? { fontRelative: true } : {}),
|
|
756
773
|
count: v.count,
|
|
757
774
|
files: v.files.size,
|
|
758
775
|
...(v.crossFamily ? { crossFamily: true } : {}),
|
|
759
776
|
}))
|
|
760
|
-
.filter((r) => r.count > 1)
|
|
761
777
|
.sort((a, b) => b.count - a.count);
|
|
778
|
+
const repeats = everyLiteral.filter((r) => r.count > 1);
|
|
779
|
+
/** Ver `Diagnosis.once` - o que o corte por repetição deixa de fora. */
|
|
780
|
+
const once = everyLiteral.filter((r) => r.count === 1);
|
|
762
781
|
return {
|
|
763
782
|
// A file with a phantom name and no drift has nothing to say by the old
|
|
764
783
|
// measure and the worst thing to say by the new one. Both keep it.
|
|
765
784
|
files: files.filter((f) => f.findings.length > 0 || (f.phantoms?.length ?? 0) > 0),
|
|
766
785
|
findings: flat,
|
|
786
|
+
once,
|
|
767
787
|
counts,
|
|
768
788
|
/**
|
|
769
789
|
* QUANTOS JÁ TÊM NOME NESTE REPOSITÓRIO - e o dele conta.
|
|
@@ -450,6 +450,26 @@ export function describeValueRuler(values) {
|
|
|
450
450
|
const decisions = values.seen - values.structure;
|
|
451
451
|
const closed = values.interpreted + values.answered;
|
|
452
452
|
const percent = decisions > 0 ? Math.round((closed / decisions) * 100) : 0;
|
|
453
|
+
/**
|
|
454
|
+
* NADA PARA MEDIR NÃO É ZERO POR CENTO - e a frase dizia zero três vezes.
|
|
455
|
+
*
|
|
456
|
+
* O QUE O CLIENTE VIA, caminhado em 07/09 num `create-next-app` recém-criado: *"Of the 0 class
|
|
457
|
+
* declarations on your components, 0 are structure. Of the 0 design decisions, 0 are interpreted -
|
|
458
|
+
* 0%."* Três zeros e um 0% sobre um projeto onde a régua não tinha o que medir, porque ele ainda
|
|
459
|
+
* não escreveu um componente - só páginas, e uma página não é componente de design system, o que o
|
|
460
|
+
* próprio relatório explica seis linhas acima.
|
|
461
|
+
*
|
|
462
|
+
* `0%` é um número de DESEMPENHO, e ali não houve desempenho nenhum. É a lei 14 do `CLAUDE.md`:
|
|
463
|
+
* *"um zero pelado lê como falha nossa; um zero com motivo lê como fato"* - e para quem acabou de
|
|
464
|
+
* apontar a plataforma para o próprio projeto, uma linha de 0% é a primeira impressão.
|
|
465
|
+
*
|
|
466
|
+
* A frase que substitui diz o FATO e o caminho, sem inventar percentual: não há componente para
|
|
467
|
+
* medir ainda.
|
|
468
|
+
*/
|
|
469
|
+
if (values.seen === 0)
|
|
470
|
+
return [
|
|
471
|
+
"No component of yours carries a class declaration yet - so there is nothing for this ruler to read. It starts answering once a component exists, and pages do not count as components.",
|
|
472
|
+
];
|
|
453
473
|
return [
|
|
454
474
|
`Of the ${values.seen} class declarations on your components, ${values.structure} are structure (layout plumbing, not design decisions). Of the ${decisions} design decisions, ${closed} are interpreted${values.answered > 0 ? ` (${values.answered} of them answered by you)` : ""} - ${percent}%.`,
|
|
455
475
|
];
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { dirname, join, resolve } from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* OS NOMES DELE QUE SOMEM NA COMPILAÇÃO - e que a plataforma afirmava não existir.
|
|
4
|
+
*
|
|
5
|
+
* O QUE O CLIENTE VIA. Um projeto que declara `$gray_dark: #555` em `css/colors.scss` e escreve
|
|
6
|
+
* `#555` em dezenas de arquivos recebia, do nosso relatório: *"#555 - no name for it, here or in
|
|
7
|
+
* your CSS"*. A frase é uma afirmação sobre o repositório DELE, e ela era falsa. Medido em 07/09 no
|
|
8
|
+
* `~/projects/web-subscribe`: `doctor` dizia `13 tokens found` num projeto com 27 nomes de cor
|
|
9
|
+
* declarados e partilhados, e 323 ocorrências ficavam sem o nome que o próprio código já lhes dá.
|
|
10
|
+
*
|
|
11
|
+
* POR QUE A PLATAFORMA NÃO OS VIA, e não era descuido: a colheita lê custom properties (`--x`), que
|
|
12
|
+
* é o que o navegador recebe. Uma variável de pré-processador - `$x` no Sass, `@x` no Less - resolve
|
|
13
|
+
* em tempo de compilação e não chega ao CSS final. Ela é vocabulário dele para LER, nunca endereço
|
|
14
|
+
* para APONTAR: escrever `var($gray_dark)` não existe.
|
|
15
|
+
*
|
|
16
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
17
|
+
* A REGRA QUE SEPARA A FEATURE DO DEFEITO: ESCOPO.
|
|
18
|
+
*
|
|
19
|
+
* Uma custom property em `:root` é global por construção - é isso que a torna vocabulário. Um `$x` no
|
|
20
|
+
* topo de um arquivo é uma constante DAQUELE arquivo, e tratar as duas como a mesma coisa inventa
|
|
21
|
+
* vocabulário em vez de ler o que existe.
|
|
22
|
+
*
|
|
23
|
+
* Medido no mesmo repositório, e a diferença é a feature inteira:
|
|
24
|
+
*
|
|
25
|
+
* 323 ocorrências ganham o nome certo ($ declarado em folha que outra folha importa)
|
|
26
|
+
* 1085 ganhariam um nome FALSO ($color e $primary-color declarados dentro de um
|
|
27
|
+
* styles.module.scss de um componente - `$color: #FFF`
|
|
28
|
+
* existe em dois módulos diferentes, e `#fff` aparece
|
|
29
|
+
* 596 vezes no projeto inteiro)
|
|
30
|
+
*
|
|
31
|
+
* Então só conta o que é PARTILHADO: um arquivo que outro arquivo importa foi escrito para ser
|
|
32
|
+
* vocabulário comum, e quem decide isso é o `@import`/`@use`/`@forward` dele - não uma convenção de
|
|
33
|
+
* nome de arquivo nossa, que seria a forma de fixar o hábito de um repositório dentro do produto.
|
|
34
|
+
*/
|
|
35
|
+
/** As extensões cujo vocabulário some antes do navegador. */
|
|
36
|
+
const PREPROCESSED = /\.(scss|sass|less)$/i;
|
|
37
|
+
/** `$nome: valor;` no Sass, `@nome: valor;` no Less - no topo do arquivo, fora de qualquer bloco. */
|
|
38
|
+
const DECLARATION = /^[ \t]*([$@][a-zA-Z0-9_-]+)[ \t]*:[ \t]*([^;{}]+);/gm;
|
|
39
|
+
/**
|
|
40
|
+
* `!default` É A MARCA DA PRÓPRIA LINGUAGEM PARA "ISTO É O PADRÃO DE UMA BIBLIOTECA".
|
|
41
|
+
*
|
|
42
|
+
* Em Sass ele significa literalmente *este valor vale se quem me usa não definiu outro* - a forma
|
|
43
|
+
* como um pacote publica vocabulário configurável. Um valor que ELE decidiu não carrega `!default`,
|
|
44
|
+
* e é essa diferença que separa a decisão dele do default de um pacote que alguém copiou para
|
|
45
|
+
* dentro do repositório.
|
|
46
|
+
*
|
|
47
|
+
* Medido em 07/09 no `~/projects/web-subscribe`, que tem `bootstrap-sass` e `ionicons` copiados para
|
|
48
|
+
* `css/`: dos 1185 nomes que a varredura encontra, 1125 são desses dois pacotes e TODOS trazem
|
|
49
|
+
* `!default`. Sem esta porta, o relatório responderia que o `#555` dele "se chama
|
|
50
|
+
* `$navbar-default-link-active-color`" - o vocabulário de uma biblioteca apresentado como o dele,
|
|
51
|
+
* que é o oposto exato do que este leitor existe para fazer.
|
|
52
|
+
*
|
|
53
|
+
* O LADO SEGURO DO ERRO, declarado: um projeto que use `!default` no PRÓPRIO vocabulário perde esses
|
|
54
|
+
* nomes e volta a ver "no name for it". Deixar de nomear o que existe custa uma linha de relatório;
|
|
55
|
+
* batizar o valor dele com a palavra de um pacote custa a confiança na próxima linha.
|
|
56
|
+
*/
|
|
57
|
+
const LIBRARY_DEFAULT = /!\s*default\b/i;
|
|
58
|
+
/** `@import "colors"`, `@use "./colors" as c`, `@forward "colors"`. */
|
|
59
|
+
const REFERENCE = /@(?:import|use|forward)\s+["']([^"']+)["']/g;
|
|
60
|
+
/**
|
|
61
|
+
* QUAIS FOLHAS OUTRA FOLHA IMPORTA - a prova de que aquele arquivo é vocabulário comum.
|
|
62
|
+
*
|
|
63
|
+
* A resolução segue o que o Sass faz: a extensão é opcional e o parcial pode ter o `_` na frente.
|
|
64
|
+
* Um especificador que não aponta para nenhum arquivo do projeto é um pacote (`@import "bootstrap"`),
|
|
65
|
+
* e pacote não é vocabulário dele.
|
|
66
|
+
*/
|
|
67
|
+
export function sharedSheets(sheets) {
|
|
68
|
+
const known = new Set(sheets.keys());
|
|
69
|
+
const shared = new Set();
|
|
70
|
+
for (const [path, source] of sheets) {
|
|
71
|
+
const dir = dirname(path);
|
|
72
|
+
for (const [, spec] of source.matchAll(REFERENCE)) {
|
|
73
|
+
const bare = spec.replace(/\.(scss|sass|less|css)$/i, "");
|
|
74
|
+
const base = bare.split("/").pop() ?? bare;
|
|
75
|
+
const parent = bare.slice(0, bare.length - base.length);
|
|
76
|
+
for (const candidate of [
|
|
77
|
+
`${bare}.scss`,
|
|
78
|
+
`${bare}.sass`,
|
|
79
|
+
`${bare}.less`,
|
|
80
|
+
`${bare}.css`,
|
|
81
|
+
join(parent, `_${base}.scss`),
|
|
82
|
+
join(parent, `_${base}.sass`),
|
|
83
|
+
join(parent, `_${base}.less`),
|
|
84
|
+
]) {
|
|
85
|
+
const abs = resolve(dir, candidate);
|
|
86
|
+
if (known.has(abs)) {
|
|
87
|
+
shared.add(abs);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return shared;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* O VOCABULÁRIO PARTILHADO QUE O NAVEGADOR NUNCA VÊ.
|
|
97
|
+
*
|
|
98
|
+
* A primeira declaração vence, a mesma regra que a colheita de custom properties usa: uma folha
|
|
99
|
+
* posterior sobrescrevendo uma anterior é cascata, e este leitor não vê cascata.
|
|
100
|
+
*/
|
|
101
|
+
export function compiledNames(sheets) {
|
|
102
|
+
const shared = sharedSheets(sheets);
|
|
103
|
+
const out = [];
|
|
104
|
+
const seen = new Set();
|
|
105
|
+
for (const [path, source] of sheets) {
|
|
106
|
+
if (!PREPROCESSED.test(path) || !shared.has(path))
|
|
107
|
+
continue;
|
|
108
|
+
for (const [, name, value] of source.matchAll(DECLARATION)) {
|
|
109
|
+
if (seen.has(name) || LIBRARY_DEFAULT.test(value))
|
|
110
|
+
continue;
|
|
111
|
+
seen.add(name);
|
|
112
|
+
/** `!global` diz onde a atribuição vale, não o que ela vale - sai do valor e não da lista. */
|
|
113
|
+
out.push({
|
|
114
|
+
name,
|
|
115
|
+
value: value.replace(/!\s*global\b/i, "").trim(),
|
|
116
|
+
file: path,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
@@ -47,7 +47,12 @@ import { FAMILY_PREFIX, familySays, formDecides, normalizeValue, UNAMBIGUOUS, }
|
|
|
47
47
|
* que o próprio css dele nomeia `--dashboard-font-family`. É pouco, e é o único grupo que sobrou do
|
|
48
48
|
* item que eu tinha nomeado - os `em` saíram na leva anterior.
|
|
49
49
|
*/
|
|
50
|
-
|
|
50
|
+
/**
|
|
51
|
+
* O nome sem o sigilo, em pedaços. `$` e `@` entram porque o vocabulário dele pode estar num
|
|
52
|
+
* pré-processador (ver `their-compiled-names.ts`), e um `$gray-dark` que não perdesse o `$` nunca
|
|
53
|
+
* casaria segmento nenhum no desempate - o critério existiria e nunca alcançaria esses nomes.
|
|
54
|
+
*/
|
|
55
|
+
const segments = (name) => name.replace(/^(--|[$@])/, "").split("-");
|
|
51
56
|
/**
|
|
52
57
|
* QUAL DOS NOMES DELE, quando mais de um segura o mesmo valor - e são 18 no repo real.
|
|
53
58
|
*
|
|
@@ -132,13 +137,20 @@ roles) {
|
|
|
132
137
|
* sistema do dono, `#f59e0b` é `--color-tier-gold` E `--color-feedback-warning` - dois conceitos
|
|
133
138
|
* dele, gamificação e estado -, e o relatório dizia `→ {color.tier-gold}` como se fosse fato.
|
|
134
139
|
*/
|
|
135
|
-
function escolha(candidates, ours, convention, roles
|
|
140
|
+
function escolha(candidates, ours, convention, roles,
|
|
141
|
+
/** `false` para nome de pré-processador - ver `TheirName.writable`. */
|
|
142
|
+
writable = true) {
|
|
136
143
|
const name = pick(candidates, ours, convention, roles);
|
|
137
|
-
return { name, also: candidates.filter((c) => c !== name) };
|
|
144
|
+
return { name, also: candidates.filter((c) => c !== name), writable };
|
|
138
145
|
}
|
|
139
146
|
export function theirNames(ours, theirs) {
|
|
140
147
|
const out = new Map();
|
|
141
|
-
|
|
148
|
+
/**
|
|
149
|
+
* AS DUAS FONTES, e não só a primeira: um projeto que declara o vocabulário INTEIRO em Sass tem
|
|
150
|
+
* `byName` vazio e mesmo assim tem nomes para dizer. Sair aqui devolvia mapa vazio para ele, e o
|
|
151
|
+
* relatório voltava a afirmar que o repositório não nomeia um valor que o repositório nomeia.
|
|
152
|
+
*/
|
|
153
|
+
if (theirs.byName.size === 0 && theirs.compiledAway.size === 0)
|
|
142
154
|
return out;
|
|
143
155
|
/**
|
|
144
156
|
* OS PAPÉIS QUE ESTE SISTEMA DECLARA, derivados dos NOSSOS nomes - ver o critério 2 de `pick`.
|
|
@@ -205,6 +217,29 @@ export function theirNames(ours, theirs) {
|
|
|
205
217
|
out.set(key, escolha(candidates, null, convention, roles));
|
|
206
218
|
}
|
|
207
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* POR ÚLTIMO, O QUE SÓ SE DIZ - e ser o último é a regra, não a ordem do arquivo.
|
|
222
|
+
*
|
|
223
|
+
* Onde ele nomeia o mesmo valor nas duas formas, quem ganha é a custom property: ela resolve no
|
|
224
|
+
* navegador, o `--fix` pode escrevê-la e ela sobrevive a qualquer mudança de pré-processador. Só
|
|
225
|
+
* quando NENHUM nome que o navegador recebe segura aquele valor é que o nome compilado entra - e
|
|
226
|
+
* entra marcado como não escrevível.
|
|
227
|
+
*
|
|
228
|
+
* A mesma porta da forma vale aqui: `UNAMBIGUOUS` deixa passar o que a FORMA já classifica (cor,
|
|
229
|
+
* duração, pilha de fontes) e deixa o comprimento cru de fora. Sem isso, um `$mobile_gutter: 10px`
|
|
230
|
+
* seria oferecido como o nome de todo `10px` do projeto - medido em 07/09 no `web-subscribe`, onde
|
|
231
|
+
* `10px` aparece em 102 arquivos e quase nenhum é um gutter.
|
|
232
|
+
*/
|
|
233
|
+
for (const [value, names] of theirs.compiledAway) {
|
|
234
|
+
for (const [kind, form] of Object.entries(UNAMBIGUOUS)) {
|
|
235
|
+
if (!form.test(value))
|
|
236
|
+
continue;
|
|
237
|
+
const key = `${kind}:${value}`;
|
|
238
|
+
if (out.has(key))
|
|
239
|
+
continue;
|
|
240
|
+
out.set(key, escolha(names.map((n) => n.name), null, convention, roles, false));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
208
243
|
return out;
|
|
209
244
|
}
|
|
210
245
|
/**
|
package/dist/doctor/tokens.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { DEFAULT_ROOT_PX } from "./root-size.js";
|
|
15
15
|
import { parseSchemeBlocks } from "./scheme-blocks.js";
|
|
16
|
+
import { compiledNames } from "./their-compiled-names.js";
|
|
16
17
|
export const EMPTY_TABLE = {
|
|
17
18
|
source: null,
|
|
18
19
|
name: null,
|
|
@@ -24,6 +25,7 @@ export const EMPTY_TABLE = {
|
|
|
24
25
|
rootFrom: null,
|
|
25
26
|
aliases: new Map(),
|
|
26
27
|
declared: new Set(),
|
|
28
|
+
compiledAway: new Map(),
|
|
27
29
|
keyframes: new Set(),
|
|
28
30
|
};
|
|
29
31
|
const hex2 = (n) => Math.max(0, Math.min(255, Math.round(n)))
|
|
@@ -318,6 +320,17 @@ export function buildTable(input) {
|
|
|
318
320
|
const byName = source === "installed"
|
|
319
321
|
? parseTokens(input.css)
|
|
320
322
|
: parseRootTokens(input.css);
|
|
323
|
+
/**
|
|
324
|
+
* O VOCABULÁRIO DELE QUE NÃO CHEGA AO NAVEGADOR, somado ao que chega.
|
|
325
|
+
*
|
|
326
|
+
* Entra DEPOIS das custom properties e sem sobrescrever nenhuma: onde as duas formas nomeiam o
|
|
327
|
+
* mesmo valor, o nome que o navegador recebe é o que resolve, e é ele que a folha pode apontar.
|
|
328
|
+
*/
|
|
329
|
+
const compiled = source === "installed" || !input.sheets ? [] : compiledNames(input.sheets);
|
|
330
|
+
const compiledAway = new Map();
|
|
331
|
+
for (const one of compiled)
|
|
332
|
+
if (!byName.has(one.name))
|
|
333
|
+
compiledAway.set(one.name, one);
|
|
321
334
|
/**
|
|
322
335
|
* Built from the RESOLVED map, not from `byName`. A semantic role that
|
|
323
336
|
* aliases a primitive has to be findable by the primitive's value, or the
|
|
@@ -337,6 +350,24 @@ export function buildTable(input) {
|
|
|
337
350
|
*/
|
|
338
351
|
const raw = source === "installed" ? parseTokensWithAliases(input.css) : new Map();
|
|
339
352
|
const isAlias = (name) => /^var\(/.test((raw.get(name) ?? "").trim());
|
|
353
|
+
/**
|
|
354
|
+
* OS NOMES QUE SÓ SE DIZEM, indexados pelo MESMO valor normalizado das outras tabelas.
|
|
355
|
+
*
|
|
356
|
+
* Fora de `byName` e de `byValue` de propósito: aquelas duas respondem "o meu sistema nomeia
|
|
357
|
+
* isto?", que é a pergunta da cobertura e do `--fix`. Um nome que o navegador nunca recebe não
|
|
358
|
+
* pode entrar em nenhuma das duas - inflaria a contagem de tokens dele (medido em 07/09 no
|
|
359
|
+
* `web-subscribe`: 13 viraria 1198, e 1125 dos acrescentados eram bootstrap e ionicons copiados
|
|
360
|
+
* para dentro do repositório) e ofereceria ao `--fix` um nome que quebra um `.tsx`.
|
|
361
|
+
*/
|
|
362
|
+
const byCompiledValue = new Map();
|
|
363
|
+
for (const one of compiledAway.values()) {
|
|
364
|
+
const key = normalizeValue(one.value, rootPx);
|
|
365
|
+
const list = byCompiledValue.get(key);
|
|
366
|
+
if (list)
|
|
367
|
+
list.push(one);
|
|
368
|
+
else
|
|
369
|
+
byCompiledValue.set(key, [one]);
|
|
370
|
+
}
|
|
340
371
|
const byValue = new Map();
|
|
341
372
|
for (const [name, value] of resolved) {
|
|
342
373
|
const key = normalizeValue(value, rootPx);
|
|
@@ -360,6 +391,7 @@ export function buildTable(input) {
|
|
|
360
391
|
/** Vazio até `withTheirNames` ler o vocabulário dele - ver `their-names.ts`. */
|
|
361
392
|
aliases: new Map(),
|
|
362
393
|
declared: parseDeclaredNames(input.css),
|
|
394
|
+
compiledAway: byCompiledValue,
|
|
363
395
|
keyframes: new Set([...input.css.matchAll(/@keyframes\s+([a-zA-Z0-9_-]+)/g)].map((m) => m[1])),
|
|
364
396
|
};
|
|
365
397
|
}
|
|
@@ -418,7 +450,11 @@ kind) {
|
|
|
418
450
|
const theirs = kind
|
|
419
451
|
? table.aliases.get(`${kind}:${normalizeValue(best.value, table.rootPx)}`)
|
|
420
452
|
: undefined;
|
|
421
|
-
|
|
453
|
+
/**
|
|
454
|
+
* SÓ UM NOME ESCREVÍVEL - esta linha convida a trocar (`snap to X`), e um nome de pré-processador
|
|
455
|
+
* não pode ir para o código dele. Ver `TheirName.writable`.
|
|
456
|
+
*/
|
|
457
|
+
return theirs?.writable ? { ...best, theirs: theirs.name } : best;
|
|
422
458
|
}
|
|
423
459
|
/**
|
|
424
460
|
* The family a token belongs to, from the drift it was found in.
|
package/dist/stack.js
CHANGED
|
@@ -235,11 +235,39 @@ classStyle) {
|
|
|
235
235
|
*
|
|
236
236
|
* `null` quando o pacote não está lá ou quando a faixa não diz um número - um intervalo que não se
|
|
237
237
|
* resolve é ausência de resposta, e supor a v4 seria escolher o mais novo por conveniência nossa.
|
|
238
|
+
*
|
|
239
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
240
|
+
* O MAIOR SOZINHO É UMA FAIXA, e exigir o ponto apagava metade dos projetos v4.
|
|
241
|
+
*
|
|
242
|
+
* A primeira versão desta função casava `(\d+)\.` - um número SEGUIDO DE PONTO. `^4.1.2` respondia
|
|
243
|
+
* 4; `^4`, que é exatamente o que o `create-next-app` e o instalador do Tailwind v4 escrevem,
|
|
244
|
+
* respondia `null`. E `null` aqui não é neutro: é a plataforma dizendo *não sei qual versão* sobre um
|
|
245
|
+
* projeto que declara a versão na primeira linha do `package.json` dele.
|
|
246
|
+
*
|
|
247
|
+
* Medido em 07/09 nos sete projetos com Tailwind desta máquina:
|
|
248
|
+
*
|
|
249
|
+
* "^4" 3 projetos -> null test_ds_01, web-subscribe, web-dashboard
|
|
250
|
+
* "^3.4.1" 1 projeto -> 3
|
|
251
|
+
* "^3.3.5" 1 projeto -> 3
|
|
252
|
+
* "3.4.3" 1 projeto -> 3
|
|
253
|
+
* "4.1.11" 1 projeto -> 4
|
|
254
|
+
*
|
|
255
|
+
* Os três invisíveis são os três v4 - e não por acaso: é a v4 que se instala como `^4`. Quem paga é
|
|
256
|
+
* quem começa um projeto HOJE.
|
|
257
|
+
*
|
|
258
|
+
* A AMBIGUIDADE CONTINUA VALENDO `null`, e agora ela é medida em vez de suposta: cada faixa da
|
|
259
|
+
* expressão diz um maior, e a resposta só existe quando todas dizem o MESMO. `>=3 <5` continua sendo
|
|
260
|
+
* ausência de resposta, porque é; `^4` deixa de ser, porque não é.
|
|
238
261
|
*/
|
|
239
262
|
export function tailwindMajor(deps) {
|
|
240
263
|
const raw = deps.tailwindcss;
|
|
241
264
|
if (!raw)
|
|
242
265
|
return null;
|
|
243
|
-
const
|
|
244
|
-
|
|
266
|
+
const majors = new Set();
|
|
267
|
+
for (const part of raw.split(/\s*\|\|\s*|\s+|,/).filter(Boolean)) {
|
|
268
|
+
const m = /(\d+)/.exec(part.replace(/^[^0-9]*/, ""));
|
|
269
|
+
if (m)
|
|
270
|
+
majors.add(Number(m[1]));
|
|
271
|
+
}
|
|
272
|
+
return majors.size === 1 ? [...majors][0] : null;
|
|
245
273
|
}
|
package/dist/their-vars.js
CHANGED
|
@@ -101,7 +101,14 @@ export function pointAtTheirNames(css, theirs, resolvable) {
|
|
|
101
101
|
* buildou não tem como provar o que resolve, e um par afirmado sem prova é pior que par nenhum.
|
|
102
102
|
*/
|
|
103
103
|
if (!resolvable || theirs.byName.size === 0)
|
|
104
|
-
return {
|
|
104
|
+
return {
|
|
105
|
+
css,
|
|
106
|
+
pointed: 0,
|
|
107
|
+
pruned: 0,
|
|
108
|
+
compiledAway: 0,
|
|
109
|
+
cycles: 0,
|
|
110
|
+
pairs: [],
|
|
111
|
+
};
|
|
105
112
|
const ours = buildTable({ css, source: "installed" });
|
|
106
113
|
const alias = theirNames(ours, theirs);
|
|
107
114
|
/**
|
|
@@ -116,6 +123,7 @@ export function pointAtTheirNames(css, theirs, resolvable) {
|
|
|
116
123
|
bridged.set(theirName, ourName);
|
|
117
124
|
let pointed = 0;
|
|
118
125
|
let pruned = 0;
|
|
126
|
+
let compiledAway = 0;
|
|
119
127
|
let cycles = 0;
|
|
120
128
|
/** Ver `PointedAt.pairs`: o mapa sai do mesmo casamento que reescreve a linha. */
|
|
121
129
|
const pairs = [];
|
|
@@ -126,9 +134,19 @@ export function pointAtTheirNames(css, theirs, resolvable) {
|
|
|
126
134
|
const kind = Object.entries(FAMILY_KIND).find(([prefix]) => name.startsWith(prefix))?.[1];
|
|
127
135
|
if (!kind)
|
|
128
136
|
return line;
|
|
129
|
-
const
|
|
137
|
+
const theirs_ = alias.get(`${kind}:${normalizeValue(value.trim(), ours.rootPx)}`);
|
|
138
|
+
const theirName = theirs_?.name;
|
|
130
139
|
if (!theirName || theirName === name)
|
|
131
140
|
return line;
|
|
141
|
+
/**
|
|
142
|
+
* UM NOME DE PRÉ-PROCESSADOR NÃO É UM ENDEREÇO. `var($gray_dark)` não existe em CSS nenhum,
|
|
143
|
+
* então a linha fica com o valor - e a recusa se conta pela SUA causa, não como se o build
|
|
144
|
+
* dele tivesse podado um token que ele pode voltar a usar.
|
|
145
|
+
*/
|
|
146
|
+
if (!theirs_.writable) {
|
|
147
|
+
compiledAway += 1;
|
|
148
|
+
return line;
|
|
149
|
+
}
|
|
132
150
|
/**
|
|
133
151
|
* A PONTE JÁ APONTA PARA CÁ - então apontar de volta fecha um ciclo, e um ciclo não deixa o
|
|
134
152
|
* valor errado: deixa a propriedade INVÁLIDA. A utility dele já resolve pelo nosso token, que é
|
|
@@ -146,7 +164,7 @@ export function pointAtTheirNames(css, theirs, resolvable) {
|
|
|
146
164
|
pairs.push({ ours: name, theirs: theirName, value: value.trim() });
|
|
147
165
|
return `${indent}${name}${sep}var(${theirName});`;
|
|
148
166
|
});
|
|
149
|
-
return { css: out, pointed, pruned, cycles, pairs };
|
|
167
|
+
return { css: out, pointed, pruned, compiledAway, cycles, pairs };
|
|
150
168
|
}
|
|
151
169
|
/**
|
|
152
170
|
* O PREFIXO DA NOSSA VARIÁVEL -> A FAMÍLIA, e a chave que `theirNames` devolve é `<família>:<valor>`.
|
|
@@ -171,7 +189,14 @@ const FAMILY_KIND = {
|
|
|
171
189
|
export async function pointTokensAtTheirNames(root, payload) {
|
|
172
190
|
const css = payload.artifacts["tokens.css"] ?? "";
|
|
173
191
|
if (!css)
|
|
174
|
-
return {
|
|
192
|
+
return {
|
|
193
|
+
css,
|
|
194
|
+
pointed: 0,
|
|
195
|
+
pruned: 0,
|
|
196
|
+
compiledAway: 0,
|
|
197
|
+
cycles: 0,
|
|
198
|
+
pairs: [],
|
|
199
|
+
};
|
|
175
200
|
const [theirs, resolvable] = await Promise.all([
|
|
176
201
|
harvestTheirCss(root),
|
|
177
202
|
resolvableVars(root),
|
|
@@ -190,6 +215,9 @@ async function harvestTheirCss(root) {
|
|
|
190
215
|
"_synthesisui",
|
|
191
216
|
]);
|
|
192
217
|
let css = "";
|
|
218
|
+
/** As folhas UMA A UMA, porque a regra de escopo de `their-compiled-names.ts` precisa saber qual
|
|
219
|
+
* arquivo importa qual - a concatenação apaga exatamente essa informação. */
|
|
220
|
+
const sheets = new Map();
|
|
193
221
|
const walk = async (dir) => {
|
|
194
222
|
for (const entry of await readdir(dir, { withFileTypes: true }).catch(() => [])) {
|
|
195
223
|
if (skip.has(entry.name) || entry.name.startsWith("."))
|
|
@@ -197,12 +225,15 @@ async function harvestTheirCss(root) {
|
|
|
197
225
|
const path = join(dir, entry.name);
|
|
198
226
|
if (entry.isDirectory())
|
|
199
227
|
await walk(path);
|
|
200
|
-
else if (/\.(css|scss|sass|less)$/i.test(entry.name))
|
|
201
|
-
|
|
228
|
+
else if (/\.(css|scss|sass|less)$/i.test(entry.name)) {
|
|
229
|
+
const source = await readFile(path, "utf8").catch(() => "");
|
|
230
|
+
sheets.set(path, source);
|
|
231
|
+
css += `\n${source}`;
|
|
232
|
+
}
|
|
202
233
|
}
|
|
203
234
|
};
|
|
204
235
|
await walk(root);
|
|
205
|
-
return buildTable({ css, source: "yours" });
|
|
236
|
+
return buildTable({ css, source: "yours", sheets });
|
|
206
237
|
}
|
|
207
238
|
/**
|
|
208
239
|
* AS LINHAS QUE APONTAM PARA UM NOME DELE QUE NÃO EXISTE MAIS.
|
package/package.json
CHANGED