synthesisui 0.16.301 → 0.16.305
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/anatomy-read.js +82 -4
- package/dist/commands/component.js +21 -6
- package/dist/commands/generate.js +4 -1
- package/dist/commands/import.js +19 -1
- 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/index.js +3 -1
- package/dist/install-marks.js +13 -2
- package/dist/stack.js +56 -0
- package/dist/styles-flavour.js +121 -0
- package/package.json +1 -1
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
|
|
@@ -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
|
@@ -558,6 +558,14 @@ export async function takeCensus(root, opts) {
|
|
|
558
558
|
const islands = new Map();
|
|
559
559
|
/** How each component expresses style, so the census can state its own coverage. */
|
|
560
560
|
const shapes = new Map();
|
|
561
|
+
/**
|
|
562
|
+
* A FORMA DE ESTILO DE CADA COMPONENTE - o dado que decide em que língua ele volta.
|
|
563
|
+
*
|
|
564
|
+
* Um repositório real não tem UMA forma: no `frontend-hub` são 214 arquivos em CSS Modules, 157
|
|
565
|
+
* em lista de classes e 138 em template, e nenhuma passa de 30%. Um sabor global obriga a maioria
|
|
566
|
+
* do código dele a receber componentes na forma errada - ver `countShape`.
|
|
567
|
+
*/
|
|
568
|
+
const shapeOf = new Map();
|
|
561
569
|
/** A escala de espaçamento que o projeto declara para o `sx` - ver o uso abaixo. */
|
|
562
570
|
let sxSpacing;
|
|
563
571
|
/** Component FILES the gate accepted - the honest denominator for coverage. A first
|
|
@@ -681,7 +689,7 @@ export async function takeCensus(root, opts) {
|
|
|
681
689
|
runtimeOf.set(d.name, runtime);
|
|
682
690
|
}
|
|
683
691
|
if (found.length > 0) {
|
|
684
|
-
countShape(src, found[0].name, shapes);
|
|
692
|
+
countShape(src, found[0].name, shapes, shapeOf);
|
|
685
693
|
componentFiles += 1;
|
|
686
694
|
}
|
|
687
695
|
defined.push(...found);
|
|
@@ -2224,6 +2232,16 @@ export async function takeCensus(root, opts) {
|
|
|
2224
2232
|
components: coverage.components,
|
|
2225
2233
|
read: coverage.read,
|
|
2226
2234
|
declarations: coverage.declarations,
|
|
2235
|
+
/**
|
|
2236
|
+
* A FORMA POR COMPONENTE, ao lado da soma - ver `shapeOf`.
|
|
2237
|
+
*
|
|
2238
|
+
* A soma responde *"como este projeto escreve estilo"*; esta responde *"como ESTE
|
|
2239
|
+
* componente escreve"*, e é a segunda que decide em que língua ele volta. Aditiva e
|
|
2240
|
+
* omitida quando vazia, então nenhum censo existente muda de forma.
|
|
2241
|
+
*/
|
|
2242
|
+
...(shapeOf.size > 0
|
|
2243
|
+
? { byComponent: Object.fromEntries([...shapeOf].sort()) }
|
|
2244
|
+
: {}),
|
|
2227
2245
|
shapes: coverage.counts.map((c) => ({
|
|
2228
2246
|
key: c.shape.key,
|
|
2229
2247
|
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/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,7 @@
|
|
|
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
|
-
export const MATERIALISER_SINCE = "0.16.
|
|
131
|
+
export const MATERIALISER_SINCE = "0.16.305";
|
|
132
132
|
/**
|
|
133
133
|
* A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
|
|
134
134
|
*
|
|
@@ -253,7 +253,18 @@ export const CHECKER_SINCE = "0.16.250";
|
|
|
253
253
|
* publicado antes deste código existir, e uma marca nele calaria o aviso para quem o instalou. Mesma
|
|
254
254
|
* lição de algumas horas antes, na mesma sessão.
|
|
255
255
|
*/
|
|
256
|
-
|
|
256
|
+
/**
|
|
257
|
+
* 0.16.299 -> 0.16.305 em 25/08: o censo passou a carregar `coverage.byComponent` - como CADA
|
|
258
|
+
* componente escreve estilo, e quantos utilitários ele usa.
|
|
259
|
+
*
|
|
260
|
+
* Campo novo, então um censo medido antes não o tem: `styles: "match"` cai no sabor do projeto
|
|
261
|
+
* inteiro, que é o que a versão anterior já fazia. Ninguém regride - mas quem não remede também não
|
|
262
|
+
* ganha, e num projeto híbrido o que ele ganha são mais de 200 componentes voltando na língua em
|
|
263
|
+
* que já estão escritos, em vez de na língua da maioria.
|
|
264
|
+
*
|
|
265
|
+
* É exatamente o que esta marca existe para dizer: o `align` avisa que vale remedir.
|
|
266
|
+
*/
|
|
267
|
+
export const READER_SINCE = "0.16.305";
|
|
257
268
|
/**
|
|
258
269
|
* O QUE ESTÁ INSTALADO AQUI FICOU PARA TRÁS - e as DUAS condições que fazem isso ser verdade.
|
|
259
270
|
*
|
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