synthesisui 0.16.302 → 0.16.306
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/agent-wiring.js +11 -9
- package/dist/anatomy-read.js +82 -4
- package/dist/commands/align.js +17 -1
- package/dist/commands/component.js +21 -6
- package/dist/commands/generate.js +4 -1
- package/dist/commands/import.js +31 -3
- package/dist/commands/init.js +76 -2
- package/dist/commands/refit.js +4 -1
- package/dist/commands/upgrade.js +6 -1
- package/dist/component-codegen.js +9 -1
- package/dist/config.js +9 -2
- package/dist/doctor/class-style.js +8 -2
- package/dist/doctor/coverage.js +28 -18
- package/dist/doctor/fragments.js +6 -2
- package/dist/doctor/transcribe.js +11 -1
- package/dist/doctor/variant-read.js +199 -2
- package/dist/index.js +3 -1
- package/dist/install-marks.js +37 -2
- package/dist/stack.js +56 -0
- package/dist/styles-flavour.js +121 -0
- package/package.json +1 -1
package/dist/agent-wiring.js
CHANGED
|
@@ -37,20 +37,22 @@ export async function hookCommand(root, version) {
|
|
|
37
37
|
: `npx synthesisui@${version} hook`;
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
|
-
* COMO A SESSÃO CHAMA O CLI,
|
|
40
|
+
* COMO A SESSÃO CHAMA O CLI - pinado, como o hook de escrita, e por um número.
|
|
41
41
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
42
|
+
* Este já foi `@latest`, com a tese de que um `align` pinado não conhece verificação escrita depois
|
|
43
|
+
* dele. A tese perdeu para a medição de 25/08: `@latest` NUNCA cacheia - o npx faz round-trip no
|
|
44
|
+
* registro em TODA abertura de sessão (2,92s, sempre), enquanto a versão fixada cacheia (0,32s
|
|
45
|
+
* morno). O trabalho do align em si custa 50ms; o resto era o preço de manter a tese.
|
|
46
46
|
*
|
|
47
|
-
*
|
|
47
|
+
* E a tese tinha resposta: dizer o que está atrasado - inclusive o próprio CLI - continua sendo o
|
|
48
|
+
* trabalho do align, só que agora o fato vem do SERVIDOR (`cli` no `?meta=1`, ver `versionBehind`),
|
|
49
|
+
* que sabe o que está publicado sem que este binário precise se reinstalar para perguntar.
|
|
48
50
|
*/
|
|
49
|
-
export async function alignCommand(root) {
|
|
51
|
+
export async function alignCommand(root, version) {
|
|
50
52
|
const local = join(root, "node_modules", ".bin", "synthesisui");
|
|
51
53
|
return (await exists(local))
|
|
52
54
|
? "npx --no-install synthesisui align"
|
|
53
|
-
:
|
|
55
|
+
: `npx synthesisui@${version} align`;
|
|
54
56
|
}
|
|
55
57
|
/**
|
|
56
58
|
* Returns the command that IS in the file, not the one we would have written.
|
|
@@ -234,7 +236,7 @@ export async function wireAgent(root, version, want) {
|
|
|
234
236
|
* logado" ou "este sistema não sabe de onde foi medido".
|
|
235
237
|
*/
|
|
236
238
|
session: want.hook
|
|
237
|
-
? await wireSessionStart(root, await alignCommand(root))
|
|
239
|
+
? await wireSessionStart(root, await alignCommand(root, version))
|
|
238
240
|
: "skipped",
|
|
239
241
|
mcp: want.mcp ? await wireMcp(root, version) : "skipped",
|
|
240
242
|
};
|
package/dist/anatomy-read.js
CHANGED
|
@@ -234,6 +234,49 @@ function measuredAt(node, sketch) {
|
|
|
234
234
|
out.text = String(measured.text).trim().slice(0, 120);
|
|
235
235
|
return out;
|
|
236
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* AS PALAVRAS DA CLASSE DELE QUE DESCREVEM UM NÓ - a matéria-prima do desempate.
|
|
239
|
+
*
|
|
240
|
+
* Uma lista de classes é meio vocabulário dele e meio gramática do framework. `animate-shimmer` diz
|
|
241
|
+
* o que aquele nó É; `absolute`, `inset-0` e `z-10` dizem onde ele está, e servem para qualquer nó
|
|
242
|
+
* da página.
|
|
243
|
+
*
|
|
244
|
+
* Então a régua é a INTENÇÃO, e a lista de descartes é do que é posicional, dimensional ou de
|
|
245
|
+
* estado. O que sobra é o que ele nomeou.
|
|
246
|
+
*
|
|
247
|
+
* MEDIDO no `Button` do dono: o `<span>` do brilho veste `animate-shimmer` entre seis classes de
|
|
248
|
+
* posição, e a palavra `shimmer` é a única que diz o que ele é.
|
|
249
|
+
*
|
|
250
|
+
* ORDEM ESTÁVEL, e ela importa: o mesmo componente lido duas vezes tem que produzir o mesmo nome,
|
|
251
|
+
* senão um re-import renomeia partes e o CSS dele para de casar.
|
|
252
|
+
*/
|
|
253
|
+
function describingWords(classes) {
|
|
254
|
+
if (!classes)
|
|
255
|
+
return [];
|
|
256
|
+
/** Posição, caixa, camada, estado: verdadeiro para qualquer nó, então não descreve nenhum. */
|
|
257
|
+
const NOISE = /^(absolute|relative|fixed|sticky|static|inset|top|right|bottom|left|z|flex|grid|block|inline|hidden|w|h|min|max|p[xytrbl]?|m[xytrbl]?|gap|items|justify|self|order|shrink|grow|basis|overflow|opacity|transition|duration|ease|delay|pointer|select|cursor|group|peer|translate|rotate|scale|transform|origin|will)$/;
|
|
258
|
+
const out = [];
|
|
259
|
+
for (const raw of classes.split(/\s+/)) {
|
|
260
|
+
if (!raw)
|
|
261
|
+
continue;
|
|
262
|
+
/** Variante e valor arbitrário fora: `hover:`, `md:`, `[&>svg]:` não nomeiam o nó. */
|
|
263
|
+
if (raw.includes(":") || raw.includes("["))
|
|
264
|
+
continue;
|
|
265
|
+
const head = raw.replace(/^-/, "").split("/")[0];
|
|
266
|
+
const segs = head.split("-").filter(Boolean);
|
|
267
|
+
if (segs.length === 0)
|
|
268
|
+
continue;
|
|
269
|
+
if (NOISE.test(segs[0]))
|
|
270
|
+
continue;
|
|
271
|
+
/** `animate-shimmer` -> `shimmer`; `overlay` -> `overlay`. A última palavra é a que nomeia. */
|
|
272
|
+
const word = segs[segs.length - 1];
|
|
273
|
+
if (!/^[a-z][a-z0-9]{2,}$/.test(word))
|
|
274
|
+
continue;
|
|
275
|
+
if (!out.includes(word))
|
|
276
|
+
out.push(word);
|
|
277
|
+
}
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
237
280
|
export function resolveAnatomy(read, declared, deps,
|
|
238
281
|
/** Their name → the slug it reaches in this system. See `RefResolver`. */
|
|
239
282
|
resolve,
|
|
@@ -297,10 +340,37 @@ globals) {
|
|
|
297
340
|
top = top[0].children;
|
|
298
341
|
notes.push("the outermost wrapper is the component itself, so its children are the anatomy");
|
|
299
342
|
}
|
|
300
|
-
/**
|
|
301
|
-
|
|
343
|
+
/**
|
|
344
|
+
* O NOME DE UMA PARTE, DESEMPATADO - e o desempate DESCREVE antes de contar.
|
|
345
|
+
*
|
|
346
|
+
* O QUE O CLIENTE PERDIA: um número nosso no vocabulário dele, e não só na tela. O nome da parte
|
|
347
|
+
* vira CLASSE no código gerado (`partClassName` em `component-codegen.ts`), então
|
|
348
|
+
* `ds-button-text-2` acaba num arquivo que ele mantém - e ele não tem como saber que aquele é o
|
|
349
|
+
* brilho e este é o conteúdo sem abrir o original.
|
|
350
|
+
*
|
|
351
|
+
* MEDIDO no `codelevel` (24/08): 9 partes numeradas em 6 de 62 componentes. No `Button` dele são
|
|
352
|
+
* três `<span>` irmãos - a máscara do hover, o brilho que desliza, e o conteúdo -, e o segundo
|
|
353
|
+
* carrega `animate-shimmer` na própria classe. O nome estava na frente o tempo todo.
|
|
354
|
+
*
|
|
355
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
356
|
+
* A ORDEM É: O QUE ELE ESCREVEU, DEPOIS O NÚMERO.
|
|
357
|
+
*
|
|
358
|
+
* A classe do nó é vocabulário DELE - `animate-shimmer`, `overlay`, `track`. Derivar dali é usar o
|
|
359
|
+
* nome que ele já deu àquele pedaço; numerar é o que sobra quando essa pergunta não é feita.
|
|
360
|
+
*
|
|
361
|
+
* E o número CONTINUA existindo como último recurso, porque duas partes não podem dividir uma
|
|
362
|
+
* chave. A diferença é que ele deixou de ser a primeira resposta.
|
|
363
|
+
*/
|
|
364
|
+
const claim = (wanted, classes) => {
|
|
302
365
|
if (parts[wanted] == null)
|
|
303
366
|
return wanted;
|
|
367
|
+
for (const hint of describingWords(classes)) {
|
|
368
|
+
const candidate = `${wanted}-${hint}`;
|
|
369
|
+
if (parts[candidate] == null)
|
|
370
|
+
return candidate;
|
|
371
|
+
if (parts[hint] == null)
|
|
372
|
+
return hint;
|
|
373
|
+
}
|
|
304
374
|
for (let n = 2; n < 100; n++) {
|
|
305
375
|
const candidate = `${wanted}-${n}`;
|
|
306
376
|
if (parts[candidate] == null)
|
|
@@ -472,9 +542,17 @@ globals) {
|
|
|
472
542
|
// value. A node with no name is pure structure and that is legitimate.
|
|
473
543
|
const wanted = safePartName(String(node.name ?? ""));
|
|
474
544
|
if (wanted) {
|
|
475
|
-
|
|
545
|
+
/**
|
|
546
|
+
* AS CLASSES DELE ENTRAM NO DESEMPATE - ver `claim` e `describingWords`.
|
|
547
|
+
*
|
|
548
|
+
* O nó pode carregar a lista verbatim (`classes`) ou apontar para o censo (`at`). As duas
|
|
549
|
+
* servem, e o censo é preferido pelo mesmo motivo de sempre: uma lista redigitada é uma
|
|
550
|
+
* cópia que pode perder um modificador.
|
|
551
|
+
*/
|
|
552
|
+
const key = claim(wanted, node.classes ??
|
|
553
|
+
(node.at != null ? sketch?.[node.at]?.classes : undefined));
|
|
476
554
|
if (key !== wanted) {
|
|
477
|
-
notes.push(`two parts named \`${wanted}\` - the second is \`${key}
|
|
555
|
+
notes.push(`two parts named \`${wanted}\` - the second is \`${key}\`, named from your own class`);
|
|
478
556
|
}
|
|
479
557
|
/**
|
|
480
558
|
* THE CENSUS'S OWN STRING WINS when the node points at it. A retyped
|
package/dist/commands/align.js
CHANGED
|
@@ -459,6 +459,20 @@ export async function versionBehind(root, opts = {}) {
|
|
|
459
459
|
says: `the rules that govern "${lock.slug}" changed since this repo materialized them - your agent reads the copy on disk, so it is still following the previous set.`,
|
|
460
460
|
run: "npx synthesisui upgrade",
|
|
461
461
|
};
|
|
462
|
+
/**
|
|
463
|
+
* ESTE BINÁRIO FICOU PARA TRÁS - a linha que devolve ao align o trabalho que o `@latest` fazia.
|
|
464
|
+
*
|
|
465
|
+
* A abertura de sessão roda uma versão PINADA desde 25/08 (o `@latest` custava 2,92s de registro
|
|
466
|
+
* em toda sessão, medido), então nada aqui descobre sozinho que existe CLI mais novo - o número
|
|
467
|
+
* vem do servidor, que consulta o npm e cala quando não sabe. Vem POR ÚLTIMO de propósito: um
|
|
468
|
+
* fato sobre o sistema dele vale mais que uma novidade sobre a nossa ferramenta, e esta linha só
|
|
469
|
+
* aparece quando todo o resto está em dia.
|
|
470
|
+
*/
|
|
471
|
+
if (opts.cli && body.cli && isOlderCli(opts.cli, body.cli))
|
|
472
|
+
return {
|
|
473
|
+
says: `this session's opening check runs CLI ${opts.cli} and ${body.cli} is what installs today - the check is pinned on purpose, so it will not move by itself.`,
|
|
474
|
+
run: "npx synthesisui@latest connect",
|
|
475
|
+
};
|
|
462
476
|
return null;
|
|
463
477
|
}
|
|
464
478
|
/**
|
|
@@ -550,7 +564,9 @@ export async function misalignments(root, opts = {}) {
|
|
|
550
564
|
...(opts.cli ? { cli: opts.cli } : {}),
|
|
551
565
|
}).catch(() => []);
|
|
552
566
|
/** A única linha que custa rede, e ela some inteira quando não há rede. */
|
|
553
|
-
const remote = await versionBehind(root
|
|
567
|
+
const remote = await versionBehind(root, {
|
|
568
|
+
...(opts.cli ? { cli: opts.cli } : {}),
|
|
569
|
+
}).catch(() => null);
|
|
554
570
|
if (remote)
|
|
555
571
|
items.push(remote);
|
|
556
572
|
return items;
|
|
@@ -8,6 +8,7 @@ import { hasInteractiveTemplate, interactiveTemplate, } from "../interactive-tem
|
|
|
8
8
|
import { body, section, snippet } from "../output.js";
|
|
9
9
|
import { findCollision, reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
|
|
10
10
|
import { fetchComponent, RegistryError } from "../registry.js";
|
|
11
|
+
import { flavourResolver } from "../styles-flavour.js";
|
|
11
12
|
import { inTheirTongue, tongueOf } from "../their-tongue.js";
|
|
12
13
|
import { readCensus, unreadComment, unreadForComponent, } from "../unread-for-component.js";
|
|
13
14
|
/**
|
|
@@ -59,7 +60,9 @@ export function localName(blueprint, asked) {
|
|
|
59
60
|
* 2. YOUR component (unless --artifacts-only, target "next") →
|
|
60
61
|
* `<componentsDir>/<name>/` from `_synthesisui/config.json`: a real
|
|
61
62
|
* `export function <Pascal>()` with variants as typed props, in the
|
|
62
|
-
*
|
|
63
|
+
* language THIS component is already written in - `flavourResolver` reads the
|
|
64
|
+
* measured shape off the census, and `styles: "css" | "tailwind"` in the config
|
|
65
|
+
* overrides it for the whole project when they are migrating to one of them.
|
|
63
66
|
*
|
|
64
67
|
* The component's styles reference the DS tokens, so the system itself must be
|
|
65
68
|
* installed (`synthesisui add <slug>`) for `tokens.css`/`theme.css` to resolve.
|
|
@@ -120,8 +123,20 @@ export async function component(slug, name, opts) {
|
|
|
120
123
|
console.log(`✓ ${res.name} → _synthesisui/ds/${slug}/components/${res.name}.{json,css} (${slug} v${res.version})`);
|
|
121
124
|
}
|
|
122
125
|
// 2. YOUR component - a real, importable `export function <Pascal>()` in the
|
|
123
|
-
//
|
|
126
|
+
// language it is already written in (config: styles match|css|tailwind), under
|
|
127
|
+
// componentsDir.
|
|
124
128
|
const wantInteractive = opts.interactive && hasInteractiveTemplate(res.name);
|
|
129
|
+
/**
|
|
130
|
+
* A LÍNGUA DESTE COMPONENTE - e ela pode não ser a do projeto inteiro.
|
|
131
|
+
*
|
|
132
|
+
* Com `styles: "match"` a resposta é por componente: o `Card` que ele escreve em CSS Modules
|
|
133
|
+
* recebe um `.css` ao lado, e o `Button` que ele escreve em utilitários recebe utilitários.
|
|
134
|
+
*
|
|
135
|
+
* Resolvido UMA vez, acima de tudo que a usa - a materialização e as instruções de instalação
|
|
136
|
+
* precisam falar do mesmo arquivo, e duas resoluções separadas seriam duas chances de a
|
|
137
|
+
* instrução mandar importar um `.css` que o componente não ganhou.
|
|
138
|
+
*/
|
|
139
|
+
const flavour = (await flavourResolver(root, config.styles))(res.name);
|
|
125
140
|
if (!opts.artifactsOnly && config.target === "next") {
|
|
126
141
|
/**
|
|
127
142
|
* WE DO NOT TAKE A NAME THEY ARE USING.
|
|
@@ -210,7 +225,7 @@ export async function component(slug, name, opts) {
|
|
|
210
225
|
filenames = [`${local}.tsx`, `${local}.css`, "index.ts"];
|
|
211
226
|
}
|
|
212
227
|
else {
|
|
213
|
-
const files = generateComponentFiles(slug, res.name, res.recipe, css, res.version,
|
|
228
|
+
const files = generateComponentFiles(slug, res.name, res.recipe, css, res.version, flavour, await reactMajorOf(root),
|
|
214
229
|
/**
|
|
215
230
|
* THE SPELLING, FROM THE VERSION WE JUST FETCHED.
|
|
216
231
|
*
|
|
@@ -246,18 +261,18 @@ export async function component(slug, name, opts) {
|
|
|
246
261
|
for (const file of files) {
|
|
247
262
|
await writeFile(join(compDir, file.filename), file.filename.endsWith(".tsx") ? `${nota}${file.code}` : file.code, "utf8");
|
|
248
263
|
}
|
|
249
|
-
if (
|
|
264
|
+
if (flavour === "tailwind")
|
|
250
265
|
await writeCn(root, compDir, slug);
|
|
251
266
|
filenames = files.map((f) => f.filename);
|
|
252
267
|
}
|
|
253
|
-
const flavor = wantInteractive ? "interactive" : `styles: ${
|
|
268
|
+
const flavor = wantInteractive ? "interactive" : `styles: ${flavour}`;
|
|
254
269
|
console.log(`✓ ${config.componentsDir}/${local}/ → ${filenames.join(", ")} (${flavor})${local === res.name ? "" : ` - the "${res.name}" blueprint, under your name`}`);
|
|
255
270
|
}
|
|
256
271
|
else if (opts.interactive && !hasInteractiveTemplate(res.name)) {
|
|
257
272
|
console.log(` note: no interactive template for "${res.name}" - materialized the standard shell.`);
|
|
258
273
|
}
|
|
259
274
|
// ── DX: concrete paths + copy-pasteable snippets, with breathing room ──
|
|
260
|
-
const tailwind =
|
|
275
|
+
const tailwind = flavour === "tailwind";
|
|
261
276
|
const imports = tailwind
|
|
262
277
|
? [
|
|
263
278
|
`@import "tailwindcss";`,
|
|
@@ -5,6 +5,7 @@ import { readProjectConfig, resolveRegistry } from "../config.js";
|
|
|
5
5
|
import { installedSlugs } from "../installed.js";
|
|
6
6
|
import { reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
|
|
7
7
|
import { postGenerate, RegistryError } from "../registry.js";
|
|
8
|
+
import { flavourResolver } from "../styles-flavour.js";
|
|
8
9
|
/** PascalCase para o hint de import (course-card → CourseCard). */
|
|
9
10
|
function pascalName(name) {
|
|
10
11
|
return name.replace(/(^|[-_])([a-z0-9])/g, (_, __, c) => c.toUpperCase());
|
|
@@ -65,7 +66,9 @@ export async function generate(description, opts) {
|
|
|
65
66
|
const version = await readActiveVersion(root, slug);
|
|
66
67
|
const compDir = join(root, config.componentsDir, res.name);
|
|
67
68
|
await mkdir(compDir, { recursive: true });
|
|
68
|
-
|
|
69
|
+
// A LÍNGUA DE CADA COMPONENTE, resolvida uma vez - ver `flavourResolver`.
|
|
70
|
+
const flavourOf = await flavourResolver(root, config.styles);
|
|
71
|
+
const files = generateComponentFiles(slug, res.name, res.recipe, res.css, version, flavourOf(res.name), await reactMajorOf(root),
|
|
69
72
|
// Read off the installed document: a generated component lands in the same
|
|
70
73
|
// project as the stylesheet it has to match.
|
|
71
74
|
await readInstalledConvention(root, slug), res.name, await readInstalledScheme(root, slug));
|
package/dist/commands/import.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
-
import { basename, dirname, join, relative } from "node:path";
|
|
2
|
+
import { basename, dirname, join, relative, sep } from "node:path";
|
|
3
3
|
import { anatomyFromSketch } from "../anatomy-from-sketch.js";
|
|
4
4
|
import { applyAnatomyPatch, hasEdits, } from "../anatomy-patch.js";
|
|
5
5
|
import { resolveAnatomy, resolveFlatParts, safePartName, } from "../anatomy-read.js";
|
|
@@ -402,8 +402,18 @@ export async function takeCensus(root, opts) {
|
|
|
402
402
|
* Os caminhos que o leitor REALMENTE olhou são o escopo mais os de uso: `--scope packages/ui
|
|
403
403
|
* --usage apps/web` lê os dois, então nenhum dos dois é "fora". Sem `scopeLabel` a função devolve
|
|
404
404
|
* `null` por conta, porque aí ele apontou para a raiz.
|
|
405
|
+
*
|
|
406
|
+
* A CAMINHADA PARTE DA RAIZ DO REPO, não da raiz escopada. `root` aqui JÁ é
|
|
407
|
+
* `<repo>/<scopeLabel>` (o chamador faz `join(root, one)`), então medir a partir dele compara
|
|
408
|
+
* `src/Button.tsx` com `packages/ui/…` - nada casa, e os 121 arquivos que a esteira acabou de LER
|
|
409
|
+
* eram reportados como "fora do que você apontou - nada foi lido lá". Disparava SEMPRE com
|
|
410
|
+
* `--scope`, e o primeiro relatório do cliente abria com uma acusação falsa sobre o repositório
|
|
411
|
+
* dele (medido em 25/08: 121 contra os 14 reais).
|
|
405
412
|
*/
|
|
406
|
-
const
|
|
413
|
+
const repoRoot = scopeLabel && root.endsWith(join(sep, ...scopeLabel.split("/")))
|
|
414
|
+
? root.slice(0, root.length - scopeLabel.length - 1)
|
|
415
|
+
: root;
|
|
416
|
+
const outside = await outsideScope(repoRoot, [
|
|
407
417
|
...(scopeLabel ? [scopeLabel] : []),
|
|
408
418
|
...(opts?.usage ?? []).map((u) => u.label),
|
|
409
419
|
]).catch(() => null);
|
|
@@ -558,6 +568,14 @@ export async function takeCensus(root, opts) {
|
|
|
558
568
|
const islands = new Map();
|
|
559
569
|
/** How each component expresses style, so the census can state its own coverage. */
|
|
560
570
|
const shapes = new Map();
|
|
571
|
+
/**
|
|
572
|
+
* A FORMA DE ESTILO DE CADA COMPONENTE - o dado que decide em que língua ele volta.
|
|
573
|
+
*
|
|
574
|
+
* Um repositório real não tem UMA forma: no `frontend-hub` são 214 arquivos em CSS Modules, 157
|
|
575
|
+
* em lista de classes e 138 em template, e nenhuma passa de 30%. Um sabor global obriga a maioria
|
|
576
|
+
* do código dele a receber componentes na forma errada - ver `countShape`.
|
|
577
|
+
*/
|
|
578
|
+
const shapeOf = new Map();
|
|
561
579
|
/** A escala de espaçamento que o projeto declara para o `sx` - ver o uso abaixo. */
|
|
562
580
|
let sxSpacing;
|
|
563
581
|
/** Component FILES the gate accepted - the honest denominator for coverage. A first
|
|
@@ -681,7 +699,7 @@ export async function takeCensus(root, opts) {
|
|
|
681
699
|
runtimeOf.set(d.name, runtime);
|
|
682
700
|
}
|
|
683
701
|
if (found.length > 0) {
|
|
684
|
-
countShape(src, found[0].name, shapes);
|
|
702
|
+
countShape(src, found[0].name, shapes, shapeOf);
|
|
685
703
|
componentFiles += 1;
|
|
686
704
|
}
|
|
687
705
|
defined.push(...found);
|
|
@@ -2224,6 +2242,16 @@ export async function takeCensus(root, opts) {
|
|
|
2224
2242
|
components: coverage.components,
|
|
2225
2243
|
read: coverage.read,
|
|
2226
2244
|
declarations: coverage.declarations,
|
|
2245
|
+
/**
|
|
2246
|
+
* A FORMA POR COMPONENTE, ao lado da soma - ver `shapeOf`.
|
|
2247
|
+
*
|
|
2248
|
+
* A soma responde *"como este projeto escreve estilo"*; esta responde *"como ESTE
|
|
2249
|
+
* componente escreve"*, e é a segunda que decide em que língua ele volta. Aditiva e
|
|
2250
|
+
* omitida quando vazia, então nenhum censo existente muda de forma.
|
|
2251
|
+
*/
|
|
2252
|
+
...(shapeOf.size > 0
|
|
2253
|
+
? { byComponent: Object.fromEntries([...shapeOf].sort()) }
|
|
2254
|
+
: {}),
|
|
2227
2255
|
shapes: coverage.counts.map((c) => ({
|
|
2228
2256
|
key: c.shape.key,
|
|
2229
2257
|
label: c.shape.label,
|
package/dist/commands/init.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { DEFAULT_CONFIG, writeProjectConfig } from "../config.js";
|
|
2
2
|
import { body, section, snippet } from "../output.js";
|
|
3
|
+
import { resolveDeps, stylesFor, tailwindMajor } from "../stack.js";
|
|
4
|
+
import { formMix, measuredStyle } from "../styles-flavour.js";
|
|
3
5
|
import { wiringPrompt } from "../wiring-prompt.js";
|
|
4
6
|
import { add } from "./add.js";
|
|
5
7
|
/**
|
|
@@ -16,7 +18,29 @@ export async function init(opts) {
|
|
|
16
18
|
target,
|
|
17
19
|
pagesDir: opts.pagesDir ?? (target === "next" ? "app" : DEFAULT_CONFIG.pagesDir),
|
|
18
20
|
componentsDir: opts.componentsDir ?? DEFAULT_CONFIG.componentsDir,
|
|
19
|
-
|
|
21
|
+
/**
|
|
22
|
+
* O SABOR SAI DO QUE O PROJETO DELE DECLARA - ver `stylesFor`.
|
|
23
|
+
*
|
|
24
|
+
* A flag continua vencendo: quem digitou `--styles` escolheu, e uma dedução nossa por cima de
|
|
25
|
+
* uma resposta dele seria a plataforma discordando de quem perguntou.
|
|
26
|
+
*
|
|
27
|
+
* O que mudou é o silêncio. Até 24/08, ausência de flag significava `"css"` - inclusive num
|
|
28
|
+
* projeto com `tailwindcss ^4.3.0` nas dependências, que é o caso do repositório do dono. Ele
|
|
29
|
+
* receberia componentes em CSS, com um `.css` ao lado de cada um, num repositório onde tudo o
|
|
30
|
+
* mais é Tailwind. E a informação para acertar já estava medida no mesmo segundo.
|
|
31
|
+
*
|
|
32
|
+
* E DESDE 25/08 A AUSÊNCIA DE FLAG É `match`, que é a resposta por componente. Um sabor único
|
|
33
|
+
* presume que o projeto tem um sabor único, e um repositório real não tem: medido no
|
|
34
|
+
* `frontend-hub`, 276 componentes importam um `.module.css` e 231 escrevem utilitários.
|
|
35
|
+
* Qualquer sabor único que se escolha ali manda mais de 200 arquivos de volta na língua errada.
|
|
36
|
+
*
|
|
37
|
+
* `match` não é uma dedução mais fraca, é uma mais fina: quando o projeto TEM uma forma só, ela
|
|
38
|
+
* responde exatamente o que `stylesFor` responderia, para todo componente. A linha impressa
|
|
39
|
+
* abaixo diz qual é o resultado neste projeto, com o número.
|
|
40
|
+
*/
|
|
41
|
+
styles: opts.styles === "tailwind" || opts.styles === "css"
|
|
42
|
+
? opts.styles
|
|
43
|
+
: "match",
|
|
20
44
|
/**
|
|
21
45
|
* Gravada COM A DATA, e só quando alguém respondeu. Migração é uma fase que termina, e uma fase sem
|
|
22
46
|
* data é a etiqueta que envelhece em silêncio - o doctor imprime as duas em toda rodada.
|
|
@@ -33,7 +57,57 @@ export async function init(opts) {
|
|
|
33
57
|
console.log(` target: ${config.target}`);
|
|
34
58
|
console.log(` pagesDir: ${config.pagesDir}`);
|
|
35
59
|
console.log(` componentsDir: ${config.componentsDir}`);
|
|
36
|
-
|
|
60
|
+
/**
|
|
61
|
+
* E A DEDUÇÃO É DITA - senão a plataforma decide certo e em silêncio, que é meio caminho.
|
|
62
|
+
*
|
|
63
|
+
* A linha diz de onde veio a escolha e o que ela produz, porque as duas coisas mudam o arquivo que
|
|
64
|
+
* ele vai manter: em `tailwind` o componente sai com utilitários inline; em `css` sai um TSX fino
|
|
65
|
+
* com um `.css` ao lado.
|
|
66
|
+
*
|
|
67
|
+
* E o MAIOR do Tailwind aparece quando existe: `@theme` e `@utility` são v4, e um projeto v3
|
|
68
|
+
* recebendo sintaxe de v4 tem um arquivo que o build dele não entende. Dizer a versão que a gente
|
|
69
|
+
* leu é o que permite ele nos contradizer antes de gerar o primeiro componente.
|
|
70
|
+
*/
|
|
71
|
+
const major = tailwindMajor(await resolveDeps(root).catch(() => ({})));
|
|
72
|
+
const { byComponent, classStyle: measured } = await measuredStyle(root);
|
|
73
|
+
const mix = formMix(byComponent);
|
|
74
|
+
const project = stylesFor(await resolveDeps(root).catch(() => ({})), measured);
|
|
75
|
+
/**
|
|
76
|
+
* A FRASE DIZ QUAL DAS DUAS COISAS DECIDIU - o que ele ESCREVE ou o que está INSTALADO.
|
|
77
|
+
*
|
|
78
|
+
* As duas podem discordar, e num projeto híbrido elas discordam: `tailwindcss` nas dependências e
|
|
79
|
+
* CSS Modules no código. Dizer só "tailwind" deixaria ele sem saber se a gente olhou o
|
|
80
|
+
* `package.json` ou os arquivos - e é essa diferença que ele precisa para nos contradizer.
|
|
81
|
+
*/
|
|
82
|
+
/**
|
|
83
|
+
* E QUANDO EXISTE MISTURA, A LINHA DIZ A MISTURA - com os dois números.
|
|
84
|
+
*
|
|
85
|
+
* `match` sem número é uma palavra: ele não sabe se vai receber CSS, utilitários, ou os dois, e
|
|
86
|
+
* não tem como nos contradizer. Com os números ele lê a mesma medida que decidiu, e se ela
|
|
87
|
+
* estiver errada - porque ele está MIGRANDO, e quer receber na língua de destino - ele sabe
|
|
88
|
+
* exatamente o que reescrever com `--styles`.
|
|
89
|
+
*/
|
|
90
|
+
const why = opts.styles === "tailwind" || opts.styles === "css"
|
|
91
|
+
? "you asked for it"
|
|
92
|
+
: mix.mixed
|
|
93
|
+
? `each component in the language it is already written in - ${mix.css} in CSS, ${mix.tailwind} in utilities${mix.undecided ? `, ${mix.undecided} following the project` : ""}. Run --styles css or --styles tailwind if you are migrating to one of them`
|
|
94
|
+
: measured === "utility"
|
|
95
|
+
? `every component here writes utilities${major ? `, on tailwind v${major}` : ""}, so that is what they come back in`
|
|
96
|
+
: measured === "modules"
|
|
97
|
+
? "every component here writes CSS Modules, so that is what they come back in"
|
|
98
|
+
: measured === "global"
|
|
99
|
+
? "every component here writes a global stylesheet, so that is what they come back in"
|
|
100
|
+
: /**
|
|
101
|
+
* E NA PRIMEIRA CORRIDA NÃO EXISTE MEDIDA - `init` roda ANTES do import, que é a
|
|
102
|
+
* ordem que o `sui-init` prescreve. Dizer só "match" aqui deixaria ele achando que
|
|
103
|
+
* a plataforma escolheu, quando ela ainda não olhou.
|
|
104
|
+
*
|
|
105
|
+
* Então a linha diz as duas coisas: o que `match` significa, e que o número chega
|
|
106
|
+
* depois. A dependência entra como a única pista que existe hoje, nomeada como
|
|
107
|
+
* pista - senão ela se passa por medida.
|
|
108
|
+
*/
|
|
109
|
+
`each component in the language it is already written in - measured at import. Nothing measured yet; ${project === "tailwind" ? `your dependencies declare tailwindcss${major ? ` v${major}` : ""}` : "no tailwind in your dependencies"}`;
|
|
110
|
+
console.log(` styles: ${config.styles} (${why})`);
|
|
37
111
|
if (config.absorb)
|
|
38
112
|
console.log(` absorb: ${config.absorb}${config.absorb === "code" ? " (we hand you the CSS block; your file stays yours)" : " (absorbed values become tokens of the system)"}`);
|
|
39
113
|
if (config.intent)
|
package/dist/commands/refit.js
CHANGED
|
@@ -6,6 +6,7 @@ import { installedSlugs } from "../installed.js";
|
|
|
6
6
|
import { body, section, snippet } from "../output.js";
|
|
7
7
|
import { reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
|
|
8
8
|
import { fetchComponent, postRefit, postSaveComponent, RegistryError, } from "../registry.js";
|
|
9
|
+
import { flavourResolver } from "../styles-flavour.js";
|
|
9
10
|
/** Slugs INSTALLED under `_synthesisui/ds/` (a `.lock` marks a real install -
|
|
10
11
|
* a folder holding only refit artifacts doesn't count). */
|
|
11
12
|
/** True when the system is actually installed (tokens.css present). */
|
|
@@ -114,7 +115,9 @@ export async function refit(file, opts) {
|
|
|
114
115
|
if (config.target === "next") {
|
|
115
116
|
const compDir = join(root, config.componentsDir, res.name);
|
|
116
117
|
await mkdir(compDir, { recursive: true });
|
|
117
|
-
|
|
118
|
+
// A LÍNGUA DE CADA COMPONENTE, resolvida uma vez - ver `flavourResolver`.
|
|
119
|
+
const flavourOf = await flavourResolver(root, config.styles);
|
|
120
|
+
const files = generateComponentFiles(slug, res.name, res.recipe, res.css, saved.version, flavourOf(res.name), await reactMajorOf(root), await readInstalledConvention(root, slug), res.name, await readInstalledScheme(root, slug));
|
|
118
121
|
for (const f of files) {
|
|
119
122
|
await writeFile(join(compDir, f.filename), f.code, "utf8");
|
|
120
123
|
}
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -9,6 +9,7 @@ import { installedBehind, MATERIALISER_SINCE } from "../install-marks.js";
|
|
|
9
9
|
import { body, section, snippet } from "../output.js";
|
|
10
10
|
import { reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
|
|
11
11
|
import { fetchChangelog, fetchComponent, fetchDesignSystem, RegistryError, } from "../registry.js";
|
|
12
|
+
import { flavourResolver } from "../styles-flavour.js";
|
|
12
13
|
import { readCensus, unreadComment, unreadForComponent, } from "../unread-for-component.js";
|
|
13
14
|
import { add } from "./add.js";
|
|
14
15
|
import { reportWhatIsLeft } from "./align.js";
|
|
@@ -284,6 +285,10 @@ export async function upgrade(asked, opts) {
|
|
|
284
285
|
catch {
|
|
285
286
|
// no componentsDir yet - nothing materialized
|
|
286
287
|
}
|
|
288
|
+
// A LÍNGUA DE CADA COMPONENTE, resolvida uma vez para a corrida inteira e não por
|
|
289
|
+
// componente: `upgrade` reescreve tudo que já está instalado, e reler o censo a cada
|
|
290
|
+
// arquivo seria a mesma resposta lida N vezes. Ver `flavourResolver`.
|
|
291
|
+
const flavourOf = await flavourResolver(root, config.styles);
|
|
287
292
|
for (const entry of entries) {
|
|
288
293
|
const tsxPath = join(componentsRoot, entry, `${entry}.tsx`);
|
|
289
294
|
let head = "";
|
|
@@ -297,7 +302,7 @@ export async function upgrade(asked, opts) {
|
|
|
297
302
|
continue;
|
|
298
303
|
try {
|
|
299
304
|
const res = await fetchComponent(base, slug, entry);
|
|
300
|
-
const files = generateComponentFiles(slug, res.name, res.recipe, res.css, res.version,
|
|
305
|
+
const files = generateComponentFiles(slug, res.name, res.recipe, res.css, res.version, flavourOf(res.name),
|
|
301
306
|
// Both were missing here, and `upgrade` is the command that REWRITES
|
|
302
307
|
// components somebody already has: without the convention it would have
|
|
303
308
|
// taken a working component and stripped its styles.
|
|
@@ -1338,7 +1338,15 @@ ${dataAttrLines(partAxes)}${partAxes.length ? "\n" : ""} {...props}
|
|
|
1338
1338
|
.join("\n")}`;
|
|
1339
1339
|
}
|
|
1340
1340
|
/** All files for one component, under `<componentsDir>/<name>/`. */
|
|
1341
|
-
export function generateComponentFiles(slug, name, recipe, css, version,
|
|
1341
|
+
export function generateComponentFiles(slug, name, recipe, css, version,
|
|
1342
|
+
/**
|
|
1343
|
+
* A LÍNGUA DESTE COMPONENTE, e não a do projeto - resolvida por `flavourResolver`.
|
|
1344
|
+
*
|
|
1345
|
+
* Já foi o sabor do projeto inteiro. Num repositório real isso devolvia mais de 200 arquivos na
|
|
1346
|
+
* língua errada: medido em 605 componentes, 276 importam um `.module.css` e 231 escrevem
|
|
1347
|
+
* utilitários, e nenhuma forma passa de metade.
|
|
1348
|
+
*/
|
|
1349
|
+
styles,
|
|
1342
1350
|
/** Consumer's React major, read from its package.json. Null = unknown, which
|
|
1343
1351
|
* keeps the ref-less type rather than guessing in the unsafe direction. */
|
|
1344
1352
|
reactMajor = null,
|
package/dist/config.js
CHANGED
|
@@ -105,7 +105,7 @@ export const DEFAULT_CONFIG = {
|
|
|
105
105
|
target: "next",
|
|
106
106
|
pagesDir: "app",
|
|
107
107
|
componentsDir: "components",
|
|
108
|
-
styles: "
|
|
108
|
+
styles: "match",
|
|
109
109
|
};
|
|
110
110
|
const projectConfigPath = (root) => join(root, "_synthesisui", "config.json");
|
|
111
111
|
/** Reads the project config, falling back to defaults when absent/invalid. */
|
|
@@ -121,7 +121,14 @@ export async function readProjectConfig(root) {
|
|
|
121
121
|
componentsDir: typeof parsed.componentsDir === "string" && parsed.componentsDir
|
|
122
122
|
? parsed.componentsDir
|
|
123
123
|
: DEFAULT_CONFIG.componentsDir,
|
|
124
|
-
|
|
124
|
+
// `match` é o padrão desde 24/08 e um config mais velho não o carrega. `"css"` continua
|
|
125
|
+
// sendo o que um config antigo pedia explicitamente, então ele é honrado como escrito - e a
|
|
126
|
+
// ausência do campo passa a valer `match`, que é a resposta medida.
|
|
127
|
+
styles: parsed.styles === "tailwind"
|
|
128
|
+
? "tailwind"
|
|
129
|
+
: parsed.styles === "css"
|
|
130
|
+
? "css"
|
|
131
|
+
: "match",
|
|
125
132
|
/**
|
|
126
133
|
* A INTENÇÃO NÃO TEM DEFAULT AQUI, de propósito.
|
|
127
134
|
*
|
|
@@ -26,8 +26,14 @@ const MODULE_FILE = /\.module\.(css|scss|sass|less)$/i;
|
|
|
26
26
|
const STYLESHEET = /\.(css|scss|sass|less)$/i;
|
|
27
27
|
/** A class selector at the start of a rule: `.metric-card__title {`. */
|
|
28
28
|
const CLASS_DECL = /^\s*\.([a-zA-Z][a-zA-Z0-9_-]*)\s*[,{:]/gm;
|
|
29
|
-
/**
|
|
30
|
-
|
|
29
|
+
/**
|
|
30
|
+
* A Tailwind-shaped utility in markup - enough to know they exist.
|
|
31
|
+
*
|
|
32
|
+
* EXPORTADO em 24/08 porque o censo passou a contar utilitários POR COMPONENTE, e não só por
|
|
33
|
+
* projeto: duas contagens que discordem sobre o que é um utilitário mandariam o mesmo arquivo para
|
|
34
|
+
* sabores diferentes na soma e no detalhe.
|
|
35
|
+
*/
|
|
36
|
+
export const UTILITY = /\b(?:bg|text|border|p|px|py|m|mx|my|gap|flex|grid|rounded|shadow)-[a-z0-9[\]./-]+/g;
|
|
31
37
|
/**
|
|
32
38
|
* Below this many global class declarations, a "convention" is a handful of
|
|
33
39
|
* one-off selectors rather than a house style - and adopting one from noise
|
package/dist/doctor/coverage.js
CHANGED
|
@@ -1,20 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* WHAT THIS VERSION CAN AND CANNOT READ, MEASURED ON THEIR OWN REPO.
|
|
3
|
-
*
|
|
4
|
-
* The most commercially important thing in the pipeline, and it took a person saying
|
|
5
|
-
* "nothing works" to see it (dono, 01/08).
|
|
6
|
-
*
|
|
7
|
-
* Before this, importing a CSS-Modules app produced 283 components with zero
|
|
8
|
-
* declarations and said nothing about why. A person seeing that concludes the product is
|
|
9
|
-
* broken, and they are reasoning correctly from what they were shown. The defect was
|
|
10
|
-
* never the gap - every reader has a boundary. The defect was the SILENCE about it.
|
|
11
|
-
*
|
|
12
|
-
* So the census measures its own coverage and says the number before anything is sent:
|
|
13
|
-
* how each component expresses style, how many of those this version reads, and which
|
|
14
|
-
* ones it does not - by name, with the count from their repo rather than a disclaimer.
|
|
15
|
-
*
|
|
16
|
-
* A governance product that cannot state what it does not govern is not trustworthy.
|
|
17
|
-
*/
|
|
1
|
+
import { UTILITY } from "./class-style.js";
|
|
18
2
|
/**
|
|
19
3
|
* Every shape, in the order the enumeration found them. Each entry survived being
|
|
20
4
|
* counted on a real repo: a shape with no evidence is a shape I assumed.
|
|
@@ -79,7 +63,27 @@ export const STYLE_SHAPES = [
|
|
|
79
63
|
test: /\bsx=\{|@chakra-ui|@pandacss/,
|
|
80
64
|
},
|
|
81
65
|
];
|
|
82
|
-
export function countShape(source, name, into
|
|
66
|
+
export function countShape(source, name, into,
|
|
67
|
+
/**
|
|
68
|
+
* A FORMA DE CADA COMPONENTE, e não só a soma - o dado que a esteira media e jogava fora.
|
|
69
|
+
*
|
|
70
|
+
* O QUE ELE DESTRAVA: componentes gerados na língua do arquivo onde eles nascem. Um repositório
|
|
71
|
+
* real não tem UMA forma - medido no `frontend-hub` (24/08): 214 arquivos em CSS Modules, 157 em
|
|
72
|
+
* lista de classes escrita, 138 em template com valor de runtime. Nenhuma passa de 30%.
|
|
73
|
+
*
|
|
74
|
+
* Escolher um sabor global obriga a maioria do código dele a receber componentes na forma errada.
|
|
75
|
+
* Com a forma por componente, o `Button` que ele escreve em utilitários recebe utilitários e o
|
|
76
|
+
* `Card` em CSS Modules recebe CSS Modules - cada um na língua que aquele arquivo fala.
|
|
77
|
+
*
|
|
78
|
+
* A PRIMEIRA FORMA VENCE, e a ordem é a de `STYLE_SHAPES`: um arquivo pode casar mais de um teste
|
|
79
|
+
* (um componente em CSS Modules que também usa `style={{ }}` numa linha), e a primeira da lista é
|
|
80
|
+
* a mais estrutural. Somar as duas diria que ele usa as duas igualmente, o que não é verdade.
|
|
81
|
+
*
|
|
82
|
+
* Opcional porque `countShape` tem outro chamador que só quer a soma - e um parâmetro obrigatório
|
|
83
|
+
* ali seria mudar uma pergunta que não mudou.
|
|
84
|
+
*/
|
|
85
|
+
perComponent) {
|
|
86
|
+
let first = null;
|
|
83
87
|
for (const shape of STYLE_SHAPES) {
|
|
84
88
|
if (!shape.test.test(source))
|
|
85
89
|
continue;
|
|
@@ -88,7 +92,13 @@ export function countShape(source, name, into) {
|
|
|
88
92
|
if (at.examples.length < 4)
|
|
89
93
|
at.examples.push(name);
|
|
90
94
|
into.set(shape.key, at);
|
|
95
|
+
first ??= shape.key;
|
|
91
96
|
}
|
|
97
|
+
if (first && perComponent && !perComponent.has(name))
|
|
98
|
+
perComponent.set(name, {
|
|
99
|
+
shape: first,
|
|
100
|
+
utilities: source.match(UTILITY)?.length ?? 0,
|
|
101
|
+
});
|
|
92
102
|
}
|
|
93
103
|
export function summarizeCoverage(counts, components, read, declarations) {
|
|
94
104
|
return {
|
package/dist/doctor/fragments.js
CHANGED
|
@@ -45,8 +45,12 @@ const STYLED_OPEN = /\bstyled(?:\.[a-zA-Z][\w]*|\([^)]{1,60}\))\s*(?:<[^>]{0,120
|
|
|
45
45
|
const PIECES = /([^{};]*)([{};]|$)/g;
|
|
46
46
|
/** Uma declaração `prop: value`, já sem o terminador. */
|
|
47
47
|
const DECLARATION = /^(-{0,2}[a-zA-Z][\w-]*)\s*:\s*([^;{}]{1,200})$/;
|
|
48
|
-
/**
|
|
49
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Uma classe que só um literal pode ter: começa com letra minúscula - ou com o `!` de important,
|
|
50
|
+
* que é como o Tailwind v3 escreve `!text-ink-900`. Sem o `!`, um className inteiro de important
|
|
51
|
+
* nem entrava no denominador: não era lido E não era "não lido" (25/08).
|
|
52
|
+
*/
|
|
53
|
+
const LOOKS_LIKE_CLASS = /^[!a-z[]/;
|
|
50
54
|
/**
|
|
51
55
|
* `${styles.card}` dentro de um template: uma classe de CSS module, cujo valor mora no arquivo de
|
|
52
56
|
* estilo - que a esteira lê. É a mesma regra que `CLASS_RUNTIME` já aplica a `className={styles.x}`.
|
|
@@ -669,7 +669,17 @@ function withAlpha(d, alpha) {
|
|
|
669
669
|
}
|
|
670
670
|
/** `w-1/2`, `basis-2/3` - a fraction is a width, and the `/` is not an alpha. */
|
|
671
671
|
const FRACTION = /^(w|h|basis|max-w|max-h|min-w|min-h|top|right|bottom|left|inset)-(\d+)\/(\d+)$/;
|
|
672
|
-
export function readUtility(
|
|
672
|
+
export function readUtility(raw, declared) {
|
|
673
|
+
/**
|
|
674
|
+
* O `!` DE IMPORTANT SAI ANTES DE TUDO - `!text-ink-900` (v3) e `text-ink-900!` (v4).
|
|
675
|
+
*
|
|
676
|
+
* O important é como ele força o cascade DENTRO do app dele; a decisão de design é o valor, e
|
|
677
|
+
* era ela que morria: `!text-ink-900` chegava aqui inteiro, nenhum prefixo casava, e a camada
|
|
678
|
+
* sumia sem uma linha - com `--color-ink-900` declarado no CSS dele. 48 valores num censo real,
|
|
679
|
+
* 20 e 4 nas duas populações congeladas (25/08). Numa receita não há cascade para forçar, então
|
|
680
|
+
* despir o `!` não perde decisão nenhuma.
|
|
681
|
+
*/
|
|
682
|
+
const utility = raw.replace(/^!/, "").replace(/!$/, "");
|
|
673
683
|
/**
|
|
674
684
|
* BEFORE THE ALPHA SPLIT, because the two share a slash and only one of them is a
|
|
675
685
|
* colour. `w-1/2` was reaching the size reader as `w-1` and coming back `0.25rem` -
|
|
@@ -270,6 +270,67 @@ export function literalClasses(fragment) {
|
|
|
270
270
|
}
|
|
271
271
|
return out;
|
|
272
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* OS TRECHOS DO ARQUIVO QUE CONSOMEM CLASSE - `className={…}` (e qualquer `*ClassName=`),
|
|
275
|
+
* e as chamadas que produzem classe (`cn`, `clsx`, `cva`, `tv`, …).
|
|
276
|
+
*
|
|
277
|
+
* Existe porque os três leitores de condição liam o arquivo INTEIRO como se toda string fosse
|
|
278
|
+
* classe: 531 de 1119 entradas de `rawLayers.classes` num censo real eram literais de ternário e
|
|
279
|
+
* de record que nunca foram classe - `gray`, `True`, `Find`, `✅`, `h1` - e o gradiente do Aurora
|
|
280
|
+
* deles chegava partido em `0%,rgba(…)` porque um VALOR de CSS foi partido no espaço como se
|
|
281
|
+
* fosse lista de classes (medido em 25/08). A forma sozinha não decide (`gray` e `hidden` têm a
|
|
282
|
+
* mesma forma); o consumo decide - a mesma doutrina que `looksLikeClassList` já escreveu.
|
|
283
|
+
*/
|
|
284
|
+
const CLASS_ATTR = /(?:^|[^A-Za-z])[A-Za-z]*[Cc]lassName=\{/g;
|
|
285
|
+
const CLASS_CALL = /\b(?:cn|clsx|classNames|cx|twMerge|twJoin|classcat|cva|tv)\(/g;
|
|
286
|
+
export function classSpans(source) {
|
|
287
|
+
const spans = [];
|
|
288
|
+
for (const re of [CLASS_ATTR, CLASS_CALL]) {
|
|
289
|
+
re.lastIndex = 0;
|
|
290
|
+
for (const m of source.matchAll(re)) {
|
|
291
|
+
const open = (m.index ?? 0) + m[0].length - 1;
|
|
292
|
+
const end = endOf(source, open);
|
|
293
|
+
if (end !== -1)
|
|
294
|
+
spans.push([open, end]);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return spans;
|
|
298
|
+
}
|
|
299
|
+
function inSpan(spans, index) {
|
|
300
|
+
return spans.some(([open, end]) => index > open && index < end);
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* O RECORD É CONSUMIDO COMO CLASSE? - direto (`styles[size]` dentro de um contexto de classe) ou
|
|
304
|
+
* com um pulo (`const config = statusConfig[status]` e depois `config.dotClass` num `cn()`),
|
|
305
|
+
* inclusive desestruturado (`const { fontSize } = sizeConfig[size]`).
|
|
306
|
+
*
|
|
307
|
+
* Um pulo só, de propósito: seguir a cadeia inteira seria reimplementar um resolvedor de escopo, e
|
|
308
|
+
* os 8 de 8 casos medidos no repo real (dono, 06/08) cabem em um.
|
|
309
|
+
*/
|
|
310
|
+
function consumedAsClass(source, spans, symbol) {
|
|
311
|
+
for (const m of source.matchAll(new RegExp(`\\b${symbol}\\s*\\[`, "g"))) {
|
|
312
|
+
const at = m.index ?? 0;
|
|
313
|
+
if (inSpan(spans, at))
|
|
314
|
+
return true;
|
|
315
|
+
const before = source.slice(Math.max(0, at - 120), at);
|
|
316
|
+
const alias = /([A-Za-z_$][\w$]*)\s*=\s*$/.exec(before)?.[1];
|
|
317
|
+
const destructured = /\{([^{}]*)\}\s*=\s*$/.exec(before)?.[1];
|
|
318
|
+
const names = destructured
|
|
319
|
+
? destructured.split(",").map((s) => s.split(":")[0].trim())
|
|
320
|
+
: alias
|
|
321
|
+
? [alias]
|
|
322
|
+
: [];
|
|
323
|
+
for (const name of names) {
|
|
324
|
+
if (!name || !/^[A-Za-z_$][\w$]*$/.test(name))
|
|
325
|
+
continue;
|
|
326
|
+
for (const use of source.matchAll(new RegExp(`\\b${name}\\b`, "g"))) {
|
|
327
|
+
if (inSpan(spans, use.index ?? 0))
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
273
334
|
/**
|
|
274
335
|
* Line and block comments removed, quotes respected.
|
|
275
336
|
*
|
|
@@ -600,8 +661,15 @@ function enclosingCallClasses(source, at) {
|
|
|
600
661
|
}
|
|
601
662
|
export function readInlineConditions(source) {
|
|
602
663
|
const layers = [];
|
|
664
|
+
const spans = classSpans(source);
|
|
603
665
|
INLINE_COND.lastIndex = 0;
|
|
604
666
|
for (const m of source.matchAll(INLINE_COND)) {
|
|
667
|
+
/**
|
|
668
|
+
* `cond && "string"` fora de um contexto de classe é quase sempre CONTEÚDO de JSX
|
|
669
|
+
* (`{loading && "Finding..."}`), e entrava como classe - o mesmo portão dos ternários.
|
|
670
|
+
*/
|
|
671
|
+
if (!inSpan(spans, m.index ?? 0) && !looksLikeClassList(m[5] ?? m[9] ?? ""))
|
|
672
|
+
continue;
|
|
605
673
|
const negated = (m[1] ?? m[6]) === "!";
|
|
606
674
|
const prop = (m[2] ?? m[7] ?? "").split(".").pop() ?? "";
|
|
607
675
|
const raw = m[5] ?? m[9] ?? "";
|
|
@@ -824,6 +892,7 @@ unattributed) {
|
|
|
824
892
|
axisOf.set(m[1], m[2]);
|
|
825
893
|
}
|
|
826
894
|
const layers = [];
|
|
895
|
+
const spans = classSpans(clean);
|
|
827
896
|
LOOKUP_RECORD.lastIndex = 0;
|
|
828
897
|
for (const m of clean.matchAll(LOOKUP_RECORD)) {
|
|
829
898
|
const symbol = m[1];
|
|
@@ -840,6 +909,16 @@ unattributed) {
|
|
|
840
909
|
const options = entries.filter((e) => literalClasses(e.value).length > 0);
|
|
841
910
|
if (options.length < 2)
|
|
842
911
|
continue;
|
|
912
|
+
/**
|
|
913
|
+
* SÓ É ESTILO O QUE UM CONTEXTO DE CLASSE CONSOME - ou o que tem a forma inequívoca de uma
|
|
914
|
+
* lista de classes. Um record indexado é a forma de MUITAS coisas: `TIER_LABELS[tier]` guarda
|
|
915
|
+
* conteúdo, `AURORA_PALETTES[palette]` guarda um gradiente que vai para `style=`, e ambos
|
|
916
|
+
* entravam aqui como classe - 531 de 1119 entradas do censo real eram isso (25/08). O consumo
|
|
917
|
+
* era a única prova, e a mesma regra já valia para os campos de uma tabela por elemento.
|
|
918
|
+
*/
|
|
919
|
+
if (!consumedAsClass(clean, spans, symbol) &&
|
|
920
|
+
!options.some((o) => looksLikeClassList(o.value)))
|
|
921
|
+
continue;
|
|
843
922
|
if (axesOut) {
|
|
844
923
|
const names = options.map((o) => o.key);
|
|
845
924
|
axesOut[axis] = [...new Set([...(axesOut[axis] ?? []), ...names])];
|
|
@@ -916,8 +995,26 @@ unattributed) {
|
|
|
916
995
|
const CLASS_TERNARY = /([!A-Za-z_$][\w$.!]*)\s*(?:===\s*["']([^"']+)["']\s*)?\?\s*["']([^"']*)["']\s*:\s*["']([^"']*)["']/g;
|
|
917
996
|
export function readClassTernaries(source) {
|
|
918
997
|
const layers = [];
|
|
998
|
+
const clean = stripComments(source);
|
|
999
|
+
const spans = classSpans(clean);
|
|
919
1000
|
CLASS_TERNARY.lastIndex = 0;
|
|
920
|
-
for (const m of
|
|
1001
|
+
for (const m of clean.matchAll(CLASS_TERNARY)) {
|
|
1002
|
+
/**
|
|
1003
|
+
* UM TERNÁRIO ENTRE DUAS STRINGS É A FORMA DE QUALQUER COISA - `loading ? "Finding..." :
|
|
1004
|
+
* "Find"` é o rótulo de um botão, `large ? "h1" : "h2"` é uma tag. Fora de um contexto de
|
|
1005
|
+
* classe, só a forma inequívoca de lista de classes prova estilo; dentro, o consumo prova.
|
|
1006
|
+
* Sem este portão, os dois exemplos acima entravam no censo como classe (25/08).
|
|
1007
|
+
*/
|
|
1008
|
+
const at = m.index ?? 0;
|
|
1009
|
+
if (!inSpan(spans, at)) {
|
|
1010
|
+
const before = clean.slice(Math.max(0, at - 120), at);
|
|
1011
|
+
const alias = /([A-Za-z_$][\w$]*)\s*=\s*$/.exec(before)?.[1];
|
|
1012
|
+
const aliased = alias &&
|
|
1013
|
+
/^[A-Za-z_$][\w$]*$/.test(alias) &&
|
|
1014
|
+
[...clean.matchAll(new RegExp(`\\b${alias}\\b`, "g"))].some((u) => inSpan(spans, u.index ?? 0));
|
|
1015
|
+
if (!aliased && !looksLikeClassList(m[3]) && !looksLikeClassList(m[4]))
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
921
1018
|
const negated = m[1].startsWith("!");
|
|
922
1019
|
const prop = m[1].replace(/^!/, "").split(".").pop() ?? "";
|
|
923
1020
|
if (!prop)
|
|
@@ -941,6 +1038,100 @@ export function readClassTernaries(source) {
|
|
|
941
1038
|
}
|
|
942
1039
|
return layers;
|
|
943
1040
|
}
|
|
1041
|
+
/** O valor quando ele é UMA string literal inteira - `null` para objeto, template com `${}`, etc. */
|
|
1042
|
+
function singleStringValue(value) {
|
|
1043
|
+
const trimmed = value.trim();
|
|
1044
|
+
if (!/^["'`]/.test(trimmed))
|
|
1045
|
+
return null;
|
|
1046
|
+
const closed = endOfString(trimmed);
|
|
1047
|
+
if (closed === -1 || trimmed.slice(closed).trim())
|
|
1048
|
+
return null;
|
|
1049
|
+
const inner = trimmed.slice(1, closed - 1);
|
|
1050
|
+
if (trimmed[0] === "`" && inner.includes("${"))
|
|
1051
|
+
return null;
|
|
1052
|
+
return inner;
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* UM RECORD CONSUMIDO POR `style=` É ESTILO POR PROVA, NÃO POR FORMA - e o valor viaja INTEIRO.
|
|
1056
|
+
*
|
|
1057
|
+
* const AURORA_PALETTES: Record<AuroraPalette, string> = {
|
|
1058
|
+
* default: "linear-gradient(var(--aurora-angle,125deg),rgba(79,70,229,0.18) 0%,…)",
|
|
1059
|
+
* cool: "linear-gradient(…)",
|
|
1060
|
+
* };
|
|
1061
|
+
* style={{ backgroundImage: AURORA_PALETTES[palette], … }} // ou style={mergedStyle}
|
|
1062
|
+
*
|
|
1063
|
+
* O leitor de classes partia esse gradiente no espaço e o censo carregava `0%,rgba(192,132,252,0.18)`
|
|
1064
|
+
* como se fosse classe - 46 valores num censo real (25/08). A propriedade é a que ELE escreveu no
|
|
1065
|
+
* objeto de style (`backgroundImage`), o valor é o que ELE declarou no record, e o eixo é a prop que
|
|
1066
|
+
* indexa - transcrever não é inventar. Um nível de indireção é seguido (`const mergedStyle = {…}` e
|
|
1067
|
+
* `style={mergedStyle}`), que é como o componente real escreve.
|
|
1068
|
+
*/
|
|
1069
|
+
export function readStyleRecords(source, axesOut) {
|
|
1070
|
+
const clean = stripComments(source);
|
|
1071
|
+
const axisOf = new Map();
|
|
1072
|
+
RECORD_INDEX.lastIndex = 0;
|
|
1073
|
+
for (const m of clean.matchAll(RECORD_INDEX)) {
|
|
1074
|
+
if (!axisOf.has(m[1]))
|
|
1075
|
+
axisOf.set(m[1], m[2]);
|
|
1076
|
+
}
|
|
1077
|
+
if (axisOf.size === 0)
|
|
1078
|
+
return [];
|
|
1079
|
+
// symbol → option → o valor inteiro, para os records cujos valores são strings.
|
|
1080
|
+
const records = new Map();
|
|
1081
|
+
LOOKUP_RECORD.lastIndex = 0;
|
|
1082
|
+
for (const m of clean.matchAll(LOOKUP_RECORD)) {
|
|
1083
|
+
if (!axisOf.has(m[1]))
|
|
1084
|
+
continue;
|
|
1085
|
+
const open = (m.index ?? 0) + m[0].length - 1;
|
|
1086
|
+
const end = endOf(clean, open);
|
|
1087
|
+
if (end === -1)
|
|
1088
|
+
continue;
|
|
1089
|
+
const options = topLevelEntries(clean.slice(open + 1, end - 1))
|
|
1090
|
+
.map((e) => ({ key: e.key, value: singleStringValue(e.value) }))
|
|
1091
|
+
.filter((e) => e.value != null);
|
|
1092
|
+
if (options.length >= 2)
|
|
1093
|
+
records.set(m[1], options);
|
|
1094
|
+
}
|
|
1095
|
+
if (records.size === 0)
|
|
1096
|
+
return [];
|
|
1097
|
+
// Os corpos que ALIMENTAM um style=: o objeto inline, e o const que um style={nome} aponta.
|
|
1098
|
+
const bodies = [];
|
|
1099
|
+
for (const m of clean.matchAll(/style=\{\{/g)) {
|
|
1100
|
+
const open = (m.index ?? 0) + m[0].length - 1;
|
|
1101
|
+
const end = endOf(clean, open);
|
|
1102
|
+
if (end !== -1)
|
|
1103
|
+
bodies.push(clean.slice(open + 1, end - 1));
|
|
1104
|
+
}
|
|
1105
|
+
for (const m of clean.matchAll(/style=\{\s*([A-Za-z_$][\w$]*)\s*\}/g)) {
|
|
1106
|
+
const named = new RegExp(`(?:const|let)\\s+${m[1]}\\b[^=]*=\\s*\\{`, "g").exec(clean);
|
|
1107
|
+
if (!named)
|
|
1108
|
+
continue;
|
|
1109
|
+
const open = named.index + named[0].length - 1;
|
|
1110
|
+
const end = endOf(clean, open);
|
|
1111
|
+
if (end !== -1)
|
|
1112
|
+
bodies.push(clean.slice(open + 1, end - 1));
|
|
1113
|
+
}
|
|
1114
|
+
const layers = [];
|
|
1115
|
+
for (const body of bodies) {
|
|
1116
|
+
for (const m of body.matchAll(/([A-Za-z_$][\w$]*)\s*:\s*([A-Za-z_$][\w$]*)\s*\[/g)) {
|
|
1117
|
+
const property = m[1];
|
|
1118
|
+
const options = records.get(m[2]);
|
|
1119
|
+
const axis = axisOf.get(m[2]);
|
|
1120
|
+
if (!options || !axis)
|
|
1121
|
+
continue;
|
|
1122
|
+
if (axesOut)
|
|
1123
|
+
axesOut[axis] = [
|
|
1124
|
+
...new Set([...(axesOut[axis] ?? []), ...options.map((o) => o.key)]),
|
|
1125
|
+
];
|
|
1126
|
+
for (const option of options)
|
|
1127
|
+
layers.push({
|
|
1128
|
+
when: { variant: { [axis]: option.key } },
|
|
1129
|
+
style: { [property]: option.value },
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
return layers;
|
|
1134
|
+
}
|
|
944
1135
|
/**
|
|
945
1136
|
* A LOOKUP INDEXED BY A TERNARY, which is how the same record serves a boolean.
|
|
946
1137
|
*
|
|
@@ -977,6 +1168,12 @@ globals) {
|
|
|
977
1168
|
...readLookupRecords(source, fromRecords, unslotted),
|
|
978
1169
|
...readClassTernaries(source),
|
|
979
1170
|
];
|
|
1171
|
+
/**
|
|
1172
|
+
* O record que um `style=` consome - valores prontos, sem tabela de escala no meio.
|
|
1173
|
+
* Entram direto nas camadas finais (e registram o eixo), nunca em `raw`: `raw` é
|
|
1174
|
+
* matéria-prima de reinterpretação por classes, e estes já são o valor final.
|
|
1175
|
+
*/
|
|
1176
|
+
const styleRecords = readStyleRecords(source, fromRecords);
|
|
980
1177
|
const axes = {};
|
|
981
1178
|
const defaults = {};
|
|
982
1179
|
const allLayers = [];
|
|
@@ -1087,7 +1284,7 @@ globals) {
|
|
|
1087
1284
|
reads,
|
|
1088
1285
|
axes,
|
|
1089
1286
|
defaults,
|
|
1090
|
-
layers,
|
|
1287
|
+
layers: [...layers, ...styleRecords],
|
|
1091
1288
|
raw: allLayers,
|
|
1092
1289
|
base: withGlobal(transcribe(baseClasses, declared), baseClasses, globals),
|
|
1093
1290
|
unslotted,
|
package/dist/index.js
CHANGED
|
@@ -104,7 +104,9 @@ Options:
|
|
|
104
104
|
--target <t> template/init target: next | general (default: next)
|
|
105
105
|
--pages-dir <dir> init: folder for generated pages (default: app)
|
|
106
106
|
--components-dir <dir> init: folder where components live (default: components)
|
|
107
|
-
--styles <s> init:
|
|
107
|
+
--styles <s> init: css | tailwind. Default: each component comes back in the
|
|
108
|
+
language it is already written in - pass one of these only if you
|
|
109
|
+
are migrating TO it
|
|
108
110
|
--artifacts-only component: skip the .tsx materialization (recipe + css only)
|
|
109
111
|
--interactive component: materialize the rich/behaving variant (join-field, streak, xp-bar)
|
|
110
112
|
--replace <name> refit: replace an existing DS component (keeps its name)
|
package/dist/install-marks.js
CHANGED
|
@@ -128,7 +128,16 @@
|
|
|
128
128
|
* é sempre o bump deste PR - nunca o número que o `package.json` já carrega, porque alguém pode
|
|
129
129
|
* publicar no meio.
|
|
130
130
|
*/
|
|
131
|
-
|
|
131
|
+
/**
|
|
132
|
+
* 0.16.305 -> 0.16.306 em 25/08, e o SIM é sobre a FIAÇÃO que o `upgrade` regrava: o hook de
|
|
133
|
+
* `SessionStart` passa a ser pinado (`npx synthesisui@<versão> align`) em vez de `@latest`. O
|
|
134
|
+
* `@latest` nunca cacheia - round-trip no registro em TODA abertura de sessão, 2,92s medidos, contra
|
|
135
|
+
* 0,32s da versão fixada - e o trabalho do align em si custa 50ms. Um `upgrade` anterior deixa o
|
|
136
|
+
* `.claude/settings.json` pagando 2,9s por sessão; quem roda o upgrade novo recebe bytes diferentes
|
|
137
|
+
* ali, que é exatamente o que esta marca existe para dizer. Quem avisa versão nova passa a ser o
|
|
138
|
+
* próprio align, com o `cli` que o servidor manda no `?meta=1`.
|
|
139
|
+
*/
|
|
140
|
+
export const MATERIALISER_SINCE = "0.16.306";
|
|
132
141
|
/**
|
|
133
142
|
* A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
|
|
134
143
|
*
|
|
@@ -253,7 +262,33 @@ export const CHECKER_SINCE = "0.16.250";
|
|
|
253
262
|
* publicado antes deste código existir, e uma marca nele calaria o aviso para quem o instalou. Mesma
|
|
254
263
|
* lição de algumas horas antes, na mesma sessão.
|
|
255
264
|
*/
|
|
256
|
-
|
|
265
|
+
/**
|
|
266
|
+
* 0.16.299 -> 0.16.305 em 25/08: o censo passou a carregar `coverage.byComponent` - como CADA
|
|
267
|
+
* componente escreve estilo, e quantos utilitários ele usa.
|
|
268
|
+
*
|
|
269
|
+
* Campo novo, então um censo medido antes não o tem: `styles: "match"` cai no sabor do projeto
|
|
270
|
+
* inteiro, que é o que a versão anterior já fazia. Ninguém regride - mas quem não remede também não
|
|
271
|
+
* ganha, e num projeto híbrido o que ele ganha são mais de 200 componentes voltando na língua em
|
|
272
|
+
* que já estão escritos, em vez de na língua da maioria.
|
|
273
|
+
*
|
|
274
|
+
* É exatamente o que esta marca existe para dizer: o `align` avisa que vale remedir.
|
|
275
|
+
*/
|
|
276
|
+
/**
|
|
277
|
+
* 0.16.305 -> 0.16.306 em 25/08: três leituras que devolvem estilo do cliente, medidas antes de
|
|
278
|
+
* escrever (itens 8, 9 e 10 do diagnóstico de 25/08):
|
|
279
|
+
*
|
|
280
|
+
* 1. O `!` de important deixa de matar o utilitário: `!text-ink-900` com `--color-ink-900`
|
|
281
|
+
* declarado agora resolve (48 valores no censo real; e o ledger passou a CONTAR um className
|
|
282
|
+
* de important, que antes nem entrava no denominador).
|
|
283
|
+
* 2. Um record consumido por `style=` viaja como o VALOR que ele é: o gradiente do Aurora chega
|
|
284
|
+
* inteiro em `layers`, em vez de partido em `0%,rgba(…)` como classe (46 valores).
|
|
285
|
+
* 3. Só é classe o que um contexto de classe consome: literais de ternário e de record de
|
|
286
|
+
* conteúdo (`Finding...`, `h1`, `✅`) saem de `rawLayers.classes` - eram 531 de 1119 entradas
|
|
287
|
+
* (47%) no censo real, poluindo numerador e denominador de toda métrica em cima.
|
|
288
|
+
*
|
|
289
|
+
* Um censo medido antes carrega a poluição e a perda; quem remede ganha os três de uma vez.
|
|
290
|
+
*/
|
|
291
|
+
export const READER_SINCE = "0.16.306";
|
|
257
292
|
/**
|
|
258
293
|
* O QUE ESTÁ INSTALADO AQUI FICOU PARA TRÁS - e as DUAS condições que fazem isso ser verdade.
|
|
259
294
|
*
|
package/dist/stack.js
CHANGED
|
@@ -187,3 +187,59 @@ export async function detectStack(root) {
|
|
|
187
187
|
stack.push("plain css");
|
|
188
188
|
return stack;
|
|
189
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* QUAL SABOR DE ESTILO O PROJETO DELE PEDE - derivado do que foi MEDIDO, nunca de um default nosso.
|
|
192
|
+
*
|
|
193
|
+
* O QUE ACONTECIA: `DEFAULT_CONFIG.styles` é `"css"`, e só virava `"tailwind"` se alguém passasse
|
|
194
|
+
* `--styles` no `init`. O import nunca pergunta e nunca grava esse campo. Resultado, no repositório
|
|
195
|
+
* do dono (24/08): um projeto com `tailwindcss ^4.3.0` receberia componentes em CSS, com um `.css`
|
|
196
|
+
* ao lado de cada um, num repositório onde todo o resto é Tailwind.
|
|
197
|
+
*
|
|
198
|
+
* E a informação para acertar já estava na mão: `detectStack` mede `tailwind` no segundo em que o
|
|
199
|
+
* censo é tirado. A plataforma media o fato e usava o oposto - a mesma família do prefixo `ds-` e
|
|
200
|
+
* das partes numeradas.
|
|
201
|
+
*
|
|
202
|
+
* A REGRA É A DELE, E O SILÊNCIO É NOSSO. Um projeto que declara Tailwind recebe Tailwind. Um que
|
|
203
|
+
* não declara recebe CSS, que é o que funciona em qualquer lugar - e aí o default é uma resposta
|
|
204
|
+
* honesta a uma pergunta sem dado, não um palpite sobre um dado que existe.
|
|
205
|
+
*/
|
|
206
|
+
export function stylesFor(deps,
|
|
207
|
+
/**
|
|
208
|
+
* A FORMA DOMINANTE MEDIDA NO CÓDIGO DELE - `utility`, `modules` ou `global`.
|
|
209
|
+
*
|
|
210
|
+
* ELA VENCE A DEPENDÊNCIA, e o caso híbrido é a razão: um projeto pode ter `tailwindcss`
|
|
211
|
+
* instalado e escrever quase tudo em CSS Modules. Medido no `frontend-hub` (24/08):
|
|
212
|
+
* `@tailwindcss/postcss` e `@tailwindcss/vite` nas dependências, `classStyle: "modules"`, e o
|
|
213
|
+
* ledger conta 17 976 fragmentos `css` contra 2 620 de classe. Perguntar ao `package.json`
|
|
214
|
+
* responderia "tailwind" sobre um repositório que escreve CSS Modules.
|
|
215
|
+
*
|
|
216
|
+
* A dependência diz o que está INSTALADO; `classStyle` diz o que ele ESCREVE. A segunda é a
|
|
217
|
+
* pergunta - e é a mesma lei do resto da esteira: medir o fato, não o proxy.
|
|
218
|
+
*
|
|
219
|
+
* Ausente quando o censo não mediu (uma chamada mais velha, um projeto sem componente), e aí a
|
|
220
|
+
* dependência volta a ser a melhor pista que existe.
|
|
221
|
+
*/
|
|
222
|
+
classStyle) {
|
|
223
|
+
if (classStyle === "utility")
|
|
224
|
+
return "tailwind";
|
|
225
|
+
if (classStyle === "modules" || classStyle === "global")
|
|
226
|
+
return "css";
|
|
227
|
+
return deps.tailwindcss ? "tailwind" : "css";
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* A VERSÃO MAIOR DO TAILWIND - e ela decide a sintaxe, não só a etiqueta.
|
|
231
|
+
*
|
|
232
|
+
* `@theme` e `@utility` existem na v4 e não na v3. Gerar a sintaxe de uma para um projeto da outra
|
|
233
|
+
* produz um arquivo que o build DELE não entende - e é o pior tipo de saída, porque parece certa
|
|
234
|
+
* até alguém compilar.
|
|
235
|
+
*
|
|
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
|
+
* resolve é ausência de resposta, e supor a v4 seria escolher o mais novo por conveniência nossa.
|
|
238
|
+
*/
|
|
239
|
+
export function tailwindMajor(deps) {
|
|
240
|
+
const raw = deps.tailwindcss;
|
|
241
|
+
if (!raw)
|
|
242
|
+
return null;
|
|
243
|
+
const m = /(\d+)\./.exec(raw.replace(/^[^0-9]*/, ""));
|
|
244
|
+
return m ? Number(m[1]) : null;
|
|
245
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { resolveDeps, stylesFor } from "./stack.js";
|
|
4
|
+
/**
|
|
5
|
+
* QUANTOS UTILITÁRIOS BASTAM PARA DIZER QUE UM COMPONENTE ESCREVE UTILITÁRIOS.
|
|
6
|
+
*
|
|
7
|
+
* Um `p-4` solto num arquivo de CSS Modules é a exceção de alguém, não a língua do arquivo. Três
|
|
8
|
+
* separa as duas populações com folga na medição de 24/08: os componentes em CSS Modules ficam em
|
|
9
|
+
* 1% acima desse corte e os de lista de classes em 78%.
|
|
10
|
+
*/
|
|
11
|
+
const ENOUGH_UTILITIES = 3;
|
|
12
|
+
/**
|
|
13
|
+
* O QUE A FORMA MEDIDA DECIDE SOZINHA - e `null` quando ela não decide.
|
|
14
|
+
*
|
|
15
|
+
* `module` decide: quem importa um `.module.css` escreve CSS, e não há segunda leitura. As outras
|
|
16
|
+
* formas não decidem pelo nome - `static` é `className="flex gap-2"` em 78% dos casos e
|
|
17
|
+
* `className="card-header"` nos outros 22% -, então quem responde é a contagem de utilitários.
|
|
18
|
+
*
|
|
19
|
+
* E quando nem ela responde, a resposta é `null`: o componente volta no sabor do PROJETO. Ausência
|
|
20
|
+
* de sinal devolve a decisão para quem tem mais dado, em vez de virar palpite aqui.
|
|
21
|
+
*/
|
|
22
|
+
export function flavourOfShape(measured) {
|
|
23
|
+
if (!measured)
|
|
24
|
+
return null;
|
|
25
|
+
if (measured.shape === "module")
|
|
26
|
+
return "css";
|
|
27
|
+
if (measured.utilities >= ENOUGH_UTILITIES)
|
|
28
|
+
return "tailwind";
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* O SABOR DESTE COMPONENTE, com o do projeto como piso.
|
|
33
|
+
*
|
|
34
|
+
* `match` pergunta ao que foi medido e cai no do projeto quando a medida não decide - inclusive num
|
|
35
|
+
* censo tirado antes de 24/08, que não carrega medida nenhuma. Um ajuste explícito (`css` ou
|
|
36
|
+
* `tailwind`) vence sempre: ele é o destino que ele escolheu, e medida nenhuma sabe disso.
|
|
37
|
+
*/
|
|
38
|
+
export function flavourFor(setting, project, component, byComponent) {
|
|
39
|
+
if (setting !== "match")
|
|
40
|
+
return setting;
|
|
41
|
+
return flavourOfShape(byComponent?.[component]) ?? project;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* O CORTE DA MINORIA - abaixo disso a segunda forma é exceção, não migração.
|
|
45
|
+
*
|
|
46
|
+
* 10% dos componentes é o menor grupo que ainda dói se voltar na língua errada: num repositório de
|
|
47
|
+
* 605, são 60 arquivos. Abaixo disso a plataforma decide sozinha e não gasta a atenção dele.
|
|
48
|
+
*/
|
|
49
|
+
const MINORITY = 0.1;
|
|
50
|
+
export function formMix(byComponent) {
|
|
51
|
+
const entries = Object.values(byComponent ?? {});
|
|
52
|
+
let css = 0;
|
|
53
|
+
let tailwind = 0;
|
|
54
|
+
for (const measured of entries) {
|
|
55
|
+
const flavour = flavourOfShape(measured);
|
|
56
|
+
if (flavour === "css")
|
|
57
|
+
css += 1;
|
|
58
|
+
else if (flavour === "tailwind")
|
|
59
|
+
tailwind += 1;
|
|
60
|
+
}
|
|
61
|
+
const decided = css + tailwind;
|
|
62
|
+
const smaller = Math.min(css, tailwind);
|
|
63
|
+
return {
|
|
64
|
+
css,
|
|
65
|
+
tailwind,
|
|
66
|
+
undecided: entries.length - decided,
|
|
67
|
+
mixed: decided > 0 && smaller / decided >= MINORITY,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* O SABOR DE CADA COMPONENTE, RESOLVIDO UMA VEZ PARA O COMANDO INTEIRO.
|
|
72
|
+
*
|
|
73
|
+
* O QUE ELE EVITA: quatro comandos escrevem componentes - `component`, `generate`, `refit` e
|
|
74
|
+
* `upgrade` -, e cada um teria que ler o censo, ler o `package.json`, casar as duas medidas e
|
|
75
|
+
* decidir. Quatro cópias da mesma regra são quatro chances de duas delas discordarem, e aí o mesmo
|
|
76
|
+
* componente volta em CSS por um comando e em utilitários pelo outro.
|
|
77
|
+
*
|
|
78
|
+
* Devolve uma FUNÇÃO e não um valor, porque a resposta é por componente: `flavourOf("Card")` pode
|
|
79
|
+
* ser `css` e `flavourOf("Button")` `tailwind` na mesma corrida, que é exatamente o ponto.
|
|
80
|
+
*
|
|
81
|
+
* TUDO GUARDADO: um censo ilegível, um `package.json` ausente ou um projeto que nunca importou
|
|
82
|
+
* caem no sabor do projeto, e um projeto sem medida nenhuma cai em `css` - que é o que funciona em
|
|
83
|
+
* qualquer lugar. Nenhuma leitura aqui pode impedir alguém de gerar um componente.
|
|
84
|
+
*/
|
|
85
|
+
export async function flavourResolver(root, setting) {
|
|
86
|
+
// Um ajuste explícito é o destino que ele escolheu: nada aqui precisa ser lido para honrá-lo.
|
|
87
|
+
if (setting !== "match")
|
|
88
|
+
return () => setting;
|
|
89
|
+
const { byComponent, classStyle } = await measuredStyle(root);
|
|
90
|
+
const project = stylesFor(await resolveDeps(root).catch(() => ({})), classStyle);
|
|
91
|
+
return (component) => flavourFor("match", project, component, byComponent);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* O QUE O CENSO MEDIU SOBRE ESTILO - as duas respostas, numa leitura só.
|
|
95
|
+
*
|
|
96
|
+
* `byComponent` diz como CADA componente escreve; `classStyle` diz como o projeto escreve. A
|
|
97
|
+
* primeira decide o sabor de um componente e a segunda é o piso quando ela não decide, então elas
|
|
98
|
+
* andam juntas - e lê-las em dois lugares diferentes é abrir espaço para duas respostas.
|
|
99
|
+
*
|
|
100
|
+
* VAZIO em vez de erro quando não há censo, quando ele é ilegível, ou quando foi tirado antes de
|
|
101
|
+
* 24/08 e não carrega medida por componente: `init` roda antes de qualquer medição, e um censo
|
|
102
|
+
* quebrado não pode impedir alguém de configurar o projeto.
|
|
103
|
+
*/
|
|
104
|
+
export async function measuredStyle(root) {
|
|
105
|
+
const raw = await readFile(join(root, "_synthesisui", "census.json"), "utf8").catch(() => "");
|
|
106
|
+
if (!raw)
|
|
107
|
+
return {};
|
|
108
|
+
try {
|
|
109
|
+
const census = JSON.parse(raw);
|
|
110
|
+
const kind = census.classStyle?.kind;
|
|
111
|
+
return {
|
|
112
|
+
byComponent: census.coverage?.byComponent,
|
|
113
|
+
classStyle: kind === "global" || kind === "modules" || kind === "utility"
|
|
114
|
+
? kind
|
|
115
|
+
: undefined,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return {};
|
|
120
|
+
}
|
|
121
|
+
}
|
package/package.json
CHANGED