synthesisui 0.16.396 → 0.16.398

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.
@@ -8,6 +8,7 @@ import { hasInteractiveTemplate, interactiveTemplate, } from "../interactive-tem
8
8
  import { body, section, snippet } from "../output.js";
9
9
  import { findCollision, installedThemeVars, reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
10
10
  import { fetchComponent, RegistryError } from "../registry.js";
11
+ import { installedThemeCss, whatOnlyTheSheetResolves, } from "../sheet-needed.js";
11
12
  import { flavourResolver } from "../styles-flavour.js";
12
13
  import { inTheirTongue, projectTongue, sumSpoken, } from "../their-tongue.js";
13
14
  import { readCensus, unreadComment, unreadForComponent, } from "../unread-for-component.js";
@@ -110,6 +111,13 @@ export async function component(slug, name, opts) {
110
111
  * sai no fim - depois da linha que nomeia o que foi escrito.
111
112
  */
112
113
  let spoken = null;
114
+ /**
115
+ * OS BYTES QUE FORAM A DISCO - é sobre ELES que a pergunta "precisa da folha?" se responde.
116
+ *
117
+ * Pela mesma razão que `spoken` é preenchido em cada caminho de escrita: perguntar sobre o
118
+ * recipe, ou sobre o css antes da tradução, seria responder sobre um arquivo que ninguém abriu.
119
+ */
120
+ let writtenSource = "";
113
121
  const artifactsAreTheProduct = opts.artifactsOnly === true || config.target !== "next";
114
122
  if (artifactsAreTheProduct) {
115
123
  const dir = join(root, "_synthesisui", "ds", slug, "components");
@@ -119,6 +127,7 @@ export async function component(slug, name, opts) {
119
127
  const sheet = tongue ? inTheirTongue(res.css, tongue) : null;
120
128
  if (sheet)
121
129
  spoken = sheet;
130
+ writtenSource = sheet ? sheet.css : res.css;
122
131
  await writeFile(join(dir, `${res.name}.css`), `${sheet ? sheet.css : res.css}\n`, "utf8");
123
132
  console.log(`✓ ${res.name} → _synthesisui/ds/${slug}/components/${res.name}.{json,css} (${slug} v${res.version})`);
124
133
  }
@@ -287,6 +296,7 @@ export async function component(slug, name, opts) {
287
296
  ];
288
297
  for (const f of written)
289
298
  await writeFile(join(compDir, f.filename), f.content, "utf8");
299
+ writtenSource = written.map((f) => f.content).join("\n");
290
300
  /** O fingerprint do que escrevemos - é o que protege a edição dele no upgrade (T7). */
291
301
  await recordWritten(join(root, "_synthesisui", "ds", slug), local, written);
292
302
  filenames = written.map((f) => f.filename);
@@ -335,6 +345,7 @@ export async function component(slug, name, opts) {
335
345
  }));
336
346
  for (const f of written)
337
347
  await writeFile(join(compDir, f.filename), f.content, "utf8");
348
+ writtenSource = written.map((f) => f.content).join("\n");
338
349
  await recordWritten(join(root, "_synthesisui", "ds", slug), local, written);
339
350
  if (flavour === "tailwind")
340
351
  await writeCn(root, compDir, slug);
@@ -368,34 +379,63 @@ export async function component(slug, name, opts) {
368
379
  `@import "../_synthesisui/ds/${slug}/theme.css";`,
369
380
  ]
370
381
  : [`@import "../_synthesisui/ds/${slug}/tokens.css";`];
371
- console.log(section(`One-time setup (once per app, for "${slug}")`));
372
382
  /**
373
- * POR QUE O SETUP CONTINUA AQUI QUANDO NENHUMA VARIÁVEL É NOSSA - e dizer isto é o conserto.
383
+ * O SETUP SÓ É COBRADO QUANDO ESTE ARQUIVO PRECISA DELE - ver `sheet-needed.ts`.
384
+ *
385
+ * O DEFEITO, apontado pelo dono olhando a própria tela (07/09): o comando terminava dizendo
386
+ * *"No variable in what was just written points at our stylesheet"* e, três linhas abaixo,
387
+ * imprimia **One-time setup** mandando colar dois `@import`. As duas frases não podem ser
388
+ * verdadeiras ao mesmo tempo, e a segunda pedia uma dependência de runtime para um arquivo que
389
+ * não a usa.
374
390
  *
375
- * A tentação era suprimir o bloco quando `left` é vazio, e ele leria como a promessa "seu
376
- * repositório dispensa a folha". Medido no codelevel (07/09): as duas coisas são independentes.
377
- * As VARIÁVEIS podem ser todas dele e o arquivo continua vestindo utilitários que só o adaptador
378
- * do sistema gera (`rounded-3`, `gap-2xs`) e movimento que o `theme.css` declara referenciando
379
- * `--ds-*`. Suprimir entregaria um componente sem raio e sem gap.
391
+ * A VERSÃO ANTERIOR DESTE BLOCO já tinha visto metade do problema e parou na metade: manteve o
392
+ * setup e acrescentou *"o que estes imports ainda trazem são os utilitários do sistema e o
393
+ * movimento"*. A razão é real - `INV-VOLTA-13` deixa no `@theme` os nomes que só o sistema tem -
394
+ * mas ela era AFIRMADA, nunca medida. No `codelevel` aquele `@theme` declara 50 variáveis, todas
395
+ * `--animate-*` e `--max-width-*`, e o componente gerado não veste nenhuma delas: as duas
396
+ * animações que ele usa são declaradas pelo `globals.css` DELE.
380
397
  *
381
- * Então o bloco não muda de tamanho: ele muda de MOTIVO. A linha abaixo diz qual das duas coisas
382
- * a importação ainda serve, e ela é derivada, nunca fixa.
398
+ * Então a pergunta passa a ser feita: sobrou variável nossa, ou classe que só o nosso `@theme`
399
+ * gera? Se nenhuma das duas, o arquivo está isolado e o bloco diz isso - com o que o import ainda
400
+ * daria, para a escolha continuar sendo dele.
383
401
  */
384
- if (spoken && spoken.left.length === 0) {
385
- console.log(body(`(the variables are already yours - what these imports still bring is the`));
386
- console.log(body(` system's own utilities and its motion, which the app has to generate)`));
402
+ const need = whatOnlyTheSheetResolves({
403
+ source: writtenSource,
404
+ themeCss: await installedThemeCss(root, slug),
405
+ /** O vocabulário DELE sai da conta - ver `theirNames`. O mapa do `.lock` é a via mais barata. */
406
+ theirNames: tongue?.names ? [...tongue.names.values()] : [],
407
+ });
408
+ if (!need.needed) {
409
+ console.log(section("This file needs nothing else"));
410
+ console.log(body("Every variable and class in it is declared by your own code - no import, no scope"));
411
+ console.log(body("attribute, nothing at runtime. It is yours."));
387
412
  console.log("");
413
+ console.log(body(`The system stays what it is here: the reference your agent reads to build ON it.`));
414
+ console.log(body(`If you later want the utilities only its own @theme generates, that is when`));
415
+ console.log(body(`importing earns its place - and \`doctor\` says so.`));
416
+ }
417
+ /**
418
+ * E O "USE IT" SAI NOS DOIS CAMINHOS - a primeira versão disto era um `return`, e ela levava
419
+ * junto a única linha que diz COMO importar o componente. Dizer "está isolado" e sonegar o
420
+ * `import { ButtonSample }` troca um defeito por outro.
421
+ */
422
+ if (need.needed) {
423
+ console.log(section(`One-time setup (once per app, for "${slug}")`));
424
+ console.log(body(need.variables.length > 0
425
+ ? `(what still needs the sheet here: ${need.variables.slice(0, 3).join(", ")}${need.variables.length > 3 ? `, +${need.variables.length - 3}` : ""} - your code names no value for ${need.variables.length === 1 ? "it" : "them"})`
426
+ : `(what still needs the sheet here: the ${need.classes.slice(0, 3).join(", ")}${need.classes.length > 3 ? `, +${need.classes.length - 3}` : ""} ${need.classes.length === 1 ? "utility" : "utilities"}, which only this system's @theme generates)`));
427
+ console.log("");
428
+ console.log(body(`1. Import the design system in your GLOBAL stylesheet, e.g. app/globals.css`));
429
+ console.log(body(` (the path is relative to that file - hence the leading ../):`));
430
+ console.log("");
431
+ console.log(snippet(imports));
432
+ console.log("");
433
+ console.log(body(`2. Scope your app: add data-ds="${slug}" to a ROOT element, e.g. app/layout.tsx:`));
434
+ console.log("");
435
+ console.log(snippet([`<body data-ds="${slug}">{children}</body>`]));
436
+ console.log("");
437
+ console.log(body(`(If you haven't installed the system yet, run: synthesisui add ${slug})`));
388
438
  }
389
- console.log(body(`1. Import the design system in your GLOBAL stylesheet, e.g. app/globals.css`));
390
- console.log(body(` (the path is relative to that file - hence the leading ../):`));
391
- console.log("");
392
- console.log(snippet(imports));
393
- console.log("");
394
- console.log(body(`2. Scope your app: add data-ds="${slug}" to a ROOT element, e.g. app/layout.tsx:`));
395
- console.log("");
396
- console.log(snippet([`<body data-ds="${slug}">{children}</body>`]));
397
- console.log("");
398
- console.log(body(`(If you haven't installed the system yet, run: synthesisui add ${slug})`));
399
439
  console.log(section("Use it"));
400
440
  if (!opts.artifactsOnly && config.target === "next") {
401
441
  /**
@@ -22,7 +22,9 @@ import { groupRole } from "../group-role.js";
22
22
  import { actingSlug, describeScope, measuredScope, scopePaths, } from "../measured-scope.js";
23
23
  import { body, paint, section, snippet } from "../output.js";
24
24
  import { setupPrompt } from "../setup-prompt.js";
25
+ import { installedThemeCss, whatOnlyTheSheetResolves, } from "../sheet-needed.js";
25
26
  import { resolveDeps } from "../stack.js";
27
+ import { projectTongue } from "../their-tongue.js";
26
28
  import { danglingTheirVars } from "../their-vars.js";
27
29
  /**
28
30
  * `synthesisui doctor` - the check nobody else ships.
@@ -782,6 +784,14 @@ export async function doctor(opts) {
782
784
  * Nobody debugs from 0%. They conclude the product does not work.
783
785
  */
784
786
  const wiring = await readWiring(root, table.slug);
787
+ /**
788
+ * O QUE, NESTE REPOSITÓRIO, SÓ A NOSSA FOLHA RESOLVE - acumulado na varredura QUE JÁ ACONTECE.
789
+ *
790
+ * Uma segunda passada pelo disco para responder isto custaria o dobro num repositório de 178
791
+ * arquivos, e a resposta está no mesmo texto que o scan já tem na mão. Ver `sheet-needed.ts`.
792
+ */
793
+ const themeCss = table.slug ? await installedThemeCss(root, table.slug) : "";
794
+ let scannedSource = "";
785
795
  const skippedProjects = [];
786
796
  const tally = emptyTally();
787
797
  const internalSpecs = await internalSpecifiers(root);
@@ -793,6 +803,9 @@ export async function doctor(opts) {
793
803
  continue;
794
804
  const rel = relative(root, file);
795
805
  reports.push(scanSource(rel, src, table));
806
+ /** A pasta que a plataforma escreve não conta: ela É a folha, não um consumidor dela. */
807
+ if (!rel.startsWith("_synthesisui"))
808
+ scannedSource += `\n${src}`;
796
809
  // Composition, alongside values: the contract check needs to know which
797
810
  // elements this file writes and with what options. Stories and tests are
798
811
  // excluded for the reason they are excluded everywhere else - they compose
@@ -967,7 +980,22 @@ export async function doctor(opts) {
967
980
  * `table.source` é a distinção certa e ela já estava aqui, uma linha abaixo, em `unwired`:
968
981
  * `"yours"` e `"adopted"` são o vocabulário dela, e não há duas linhas para ligar.
969
982
  */
970
- const blocked = table.source === "installed" && (!wiring.imported || !wiring.scoped);
983
+ const sheetNeed = whatOnlyTheSheetResolves({
984
+ source: scannedSource,
985
+ themeCss,
986
+ theirNames: installed.theirs.byName.keys(),
987
+ });
988
+ /**
989
+ * BLOQUEADO SÓ QUANDO A FOLHA É NECESSÁRIA - a mesma pergunta que decide `unwired`.
990
+ *
991
+ * Sem esta condição, o comando parava de cobrar a fiação lá em cima e continuava recusando o
992
+ * `--fix` aqui embaixo, fechando com *"esse número fica acionável no momento em que as duas forem
993
+ * verdade"* - duas condições que ele mesmo tinha acabado de deixar de pedir. Um relatório que
994
+ * retira a exigência numa seção e a mantém na outra ensina que nenhuma das duas é para valer.
995
+ */
996
+ const blocked = table.source === "installed" &&
997
+ sheetNeed.needed &&
998
+ (!wiring.imported || !wiring.scoped);
971
999
  /**
972
1000
  * QUANTAS DAS DUAS FALTAM - e sem este número três frases mandavam consertar DUAS coisas quando
973
1001
  * faltava UMA.
@@ -983,12 +1011,71 @@ export async function doctor(opts) {
983
1011
  */
984
1012
  const missingWiring = (wiring.imported ? 0 : 1) + (wiring.scoped ? 0 : 1);
985
1013
  const bothMissing = missingWiring === 2;
1014
+ /**
1015
+ * A FIAÇÃO SÓ É COBRADA QUANDO ALGUMA COISA NESTE REPOSITÓRIO PRECISA DELA - ver `sheet-needed.ts`.
1016
+ *
1017
+ * O DEFEITO, apontado pelo dono em 07/09 sobre o próprio repositório: *"por que a gente precisa
1018
+ * desses imports, visto que o design do projeto dele já funciona? criar esse import gera
1019
+ * dependência ao design system, que é coisa que a gente não quer - a gente quer isolar os dois
1020
+ * pontos, sendo que o nosso deve ser apenas uma REFERÊNCIA para que qualquer agente que gera
1021
+ * código saiba como utilizar o design system"*.
1022
+ *
1023
+ * MEDIDO NO `codelevel` no mesmo dia: no repositório inteiro, apenas dois arquivos mencionam
1024
+ * `--ds-*` - um componente que a PLATAFORMA gerou com uma versão anterior à tradução, e a costura
1025
+ * de tipografia que o nosso próprio setup pediu. Zero linhas do código que ELE escreveu. Regerado
1026
+ * com o CLI de hoje, o componente sai com 0 referências à nossa folha, e as duas animações que ele
1027
+ * veste são declaradas pelo `globals.css` dele.
1028
+ *
1029
+ * Ou seja: o comando cobrava uma dependência de runtime que nada naquele repositório usava - e o
1030
+ * produto já promete o contrário em `their-tongue.ts` (*"nenhuma folha nossa precisa existir no
1031
+ * repositório dele"*). As duas metades discordavam porque nenhuma perguntava.
1032
+ *
1033
+ * O ESCOPO (`data-ds`) SEGUE A MESMA REGRA e pelo mesmo motivo: ele existe para a folha aplicar
1034
+ * dentro dele. Sem folha necessária, um atributo na árvore dele é a mesma dependência com outro
1035
+ * nome.
1036
+ *
1037
+ * E QUANDO ALGO PRECISA, tudo continua exatamente como era - inclusive o roteiro para o agente. A
1038
+ * mudança não é deixar de cobrar: é parar de cobrar sem ter perguntado.
1039
+ */
986
1040
  const unwired = table.source === "installed" &&
1041
+ sheetNeed.needed &&
987
1042
  (!wiring.imported || !wiring.scoped || fontsPending);
988
1043
  if (unwired) {
989
1044
  console.log("");
990
1045
  console.log(body(`${table.name ?? table.slug} is installed - but this project does not load it yet.`));
991
1046
  console.log("");
1047
+ /**
1048
+ * O QUE ESTÁ COBRANDO A FOLHA, POR NOME - senão a exigência lê como uma regra nossa.
1049
+ *
1050
+ * Ela não é: é um efeito do que ESTE repositório escreveu. Medido no `codelevel` (07/09), as
1051
+ * duas coisas que a exigiam eram um componente gerado por uma versão anterior à tradução do CLI
1052
+ * e a costura de tipografia que o nosso próprio setup pediu - nenhuma linha do código dele.
1053
+ * Regerado com o CLI de hoje, o mesmo componente sai com zero referências e a exigência some.
1054
+ *
1055
+ * Sem esta linha, quem lê não tem como saber que a dependência é REMOVÍVEL, e a única saída
1056
+ * visível é aceitar o import.
1057
+ */
1058
+ /**
1059
+ * E A SAÍDA SÓ É OFERECIDA QUANDO ELA EXISTE - senão o comando manda consertar o que não conserta.
1060
+ *
1061
+ * "re-run `component` and it speaks your own names instead" só é verdade onde há MAPA: onde algum
1062
+ * valor do sistema coincide com um nome que o repositório dele já declara. Sem mapa, a tradução
1063
+ * roda e não tem o que traduzir, regerar devolve o mesmo arquivo, e a folha é o caminho legítimo.
1064
+ *
1065
+ * MEDIDO EM 07/09, validando o binário publicado num `create-next-app` que instalou o
1066
+ * `codelevel`: `.lock` sem `tokenMap`, o componente saiu com 14 linhas `--ds-*`, e esta frase
1067
+ * mandava regerar. No `codelevel-monorepo`, onde o `.lock` tem 33 pares, regerar É a saída - e o
1068
+ * mesmo texto servia os dois. É o defeito que o `CLAUDE.md` registra em primeira pessoa: afirmar
1069
+ * que um comando conserta o que ele não conserta.
1070
+ */
1071
+ const translatable = (await projectTongue(root, table.slug ?? "")) !== null;
1072
+ const asks = sheetNeed.variables.length > 0
1073
+ ? `${sheetNeed.variables.slice(0, 3).join(", ")}${sheetNeed.variables.length > 3 ? `, +${sheetNeed.variables.length - 3} more` : ""}${translatable
1074
+ ? ` - re-run \`component\` on whatever writes ${sheetNeed.variables.length === 1 ? "it" : "them"} and it speaks your own names instead`
1075
+ : ` - your code names no value this system also holds, so there is nothing to translate ${sheetNeed.variables.length === 1 ? "it" : "them"} into, and the sheet is the way`}`
1076
+ : `the ${sheetNeed.classes.slice(0, 3).join(", ")}${sheetNeed.classes.length > 3 ? `, +${sheetNeed.classes.length - 3} more` : ""} ${sheetNeed.classes.length === 1 ? "utility" : "utilities"}, which only this system's @theme generates`;
1077
+ console.log(body(` what asks for it: ${asks}`));
1078
+ console.log("");
992
1079
  console.log(body(wiring.imported
993
1080
  ? ` ✓ some stylesheet imports _synthesisui/ds/${table.slug}/tokens.css`
994
1081
  : ` ✗ no stylesheet imports _synthesisui/ds/${table.slug}/tokens.css`));
@@ -0,0 +1,76 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { TAILWIND_THEME_VARS } from "./tailwind-theme-vars.js";
4
+ /** `--animate-shimmer` no `@theme` -> o nome `shimmer`, que é o que uma classe carrega. */
5
+ function themeNames(themeCss) {
6
+ const out = new Set();
7
+ for (const block of themeCss.matchAll(/@theme[^{]*\{([\s\S]*?)\n\}/g))
8
+ for (const m of block[1].matchAll(/^\s*--([a-z]+)-([a-zA-Z0-9_-]+)\s*:/gm))
9
+ out.add(m[2]);
10
+ return [...out];
11
+ }
12
+ /** As classes escritas no código - `hover:animate-shimmer` conta como `animate-shimmer`. */
13
+ function classesIn(source) {
14
+ const out = new Set();
15
+ for (const m of source.matchAll(/class(?:Name)?=(?:"([^"]*)"|'([^']*)'|\{`([^`]*)`\}|\{"([^"]*)"\})/g)) {
16
+ const body = m[1] ?? m[2] ?? m[3] ?? m[4] ?? "";
17
+ for (const raw of body.split(/\s+/)) {
18
+ if (!raw)
19
+ continue;
20
+ const bare = raw.split(":").pop() ?? raw;
21
+ out.add(bare.replace(/^[!-]/, ""));
22
+ }
23
+ }
24
+ return out;
25
+ }
26
+ /**
27
+ * O QUE, NESTE TEXTO, SÓ A FOLHA RESOLVE.
28
+ *
29
+ * Puro de propósito: quem varre o disco é quem chama, e um spec precisa poder afirmar a regra sem
30
+ * um diretório temporário.
31
+ */
32
+ export function whatOnlyTheSheetResolves(input) {
33
+ const variables = [
34
+ ...new Set([...input.source.matchAll(/var\(\s*(--ds-[a-zA-Z0-9_-]+)/g)].map((m) => m[1])),
35
+ ];
36
+ /**
37
+ * `--animate-shimmer` declarado por ele -> o nome `shimmer` sai da conta.
38
+ *
39
+ * E O QUE O PRÓPRIO TAILWIND DECLARA sai junto, pela mesma razão e pela lista que já existe:
40
+ * `animate-pulse` existe em qualquer projeto que importe o Tailwind, e acusá-la de exigir a nossa
41
+ * folha é o mesmo aviso falso que `TAILWIND_THEME_VARS` foi escrito para acabar. Medido no
42
+ * `codelevel` (07/09): sem esta linha, `animate-pulse` era a ÚNICA coisa sustentando a cobrança
43
+ * do import naquele repositório.
44
+ */
45
+ const theirs = new Set();
46
+ for (const name of [...TAILWIND_THEME_VARS, ...(input.theirNames ?? [])]) {
47
+ const m = /^--[a-z]+-(.+)$/.exec(name.toLowerCase());
48
+ if (m)
49
+ theirs.add(m[1]);
50
+ }
51
+ const names = themeNames(input.themeCss).filter((n) => !theirs.has(n));
52
+ const written = classesIn(input.source);
53
+ const classes = [
54
+ ...new Set([...written].filter((c) => names.some((n) => c === n || c.endsWith(`-${n}`)))),
55
+ ];
56
+ return {
57
+ variables,
58
+ classes,
59
+ needed: variables.length > 0 || classes.length > 0,
60
+ };
61
+ }
62
+ /** O `theme.css` da versão instalada - a folha da raiz é um re-export de uma linha. */
63
+ export async function installedThemeCss(root, slug) {
64
+ const dir = join(root, "_synthesisui", "ds", slug);
65
+ const lock = await readFile(join(dir, ".lock"), "utf8").catch(() => null);
66
+ let version = 0;
67
+ if (lock) {
68
+ try {
69
+ version = JSON.parse(lock).version ?? 0;
70
+ }
71
+ catch {
72
+ version = 0;
73
+ }
74
+ }
75
+ return readFile(join(dir, `v${version}`, "theme.css"), "utf8").catch(() => "");
76
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.396",
3
+ "version": "0.16.398",
4
4
  "description": "Bring SynthesisUI design systems into any project - tokens, typed components, whole pages and an agent-ready CLAUDE.md manifest.",
5
5
  "type": "module",
6
6
  "bin": {