synthesisui 0.16.289 → 0.16.290

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.
@@ -268,6 +268,8 @@ export async function add(slug, opts) {
268
268
  * repositório, e é ele que avisa quando uma delas deixa de resolver.
269
269
  */
270
270
  ...(theirVars.pointed > 0 ? { theirVars: theirVars.pointed } : {}),
271
+ /** Ver `RootLock.tokenMap`: o fato, ao lado do número que ele produz. */
272
+ ...(theirVars.pairs.length > 0 ? { tokenMap: theirVars.pairs } : {}),
271
273
  ...(scope ? { scope } : {}),
272
274
  ...(systems.length > 0 ? { scopes: systems } : {}),
273
275
  ...(usage.length > 0 ? { usage } : {}),
@@ -394,6 +396,29 @@ export async function add(slug, opts) {
394
396
  * `var(--color-ink-500)` num arquivo "gerenciado pelo synthesisui" e não tem como saber de onde
395
397
  * veio. Dito assim, ele lê a linha como o que ela é - o sistema seguindo o vocabulário dele.
396
398
  */
399
+ /**
400
+ * ELE RENOMEOU UM TOKEN, E A GENTE DIZ - antes era um silêncio que o sistema pagava.
401
+ *
402
+ * `their-vars.ts` decidiu NÃO emitir `var(--x, #valor)` justamente para não pintar o valor velho
403
+ * por cima de um rename dele: *"se ele renomear, quebra, e o `doctor` diz qual"* (dono, 22/08). A
404
+ * quebra é deliberada e continua. O que faltava era ele saber ANTES de abrir a tela e ver a cor
405
+ * sumida - o mapa anterior está no `.lock`, o novo acabou de ser calculado, e a diferença é uma
406
+ * comparação.
407
+ *
408
+ * SÓ O RENAME, e não a troca de valor: trocar o valor não quebra nada - a folha aponta para o nome
409
+ * dele e a cor nova passa a valer, que é o comportamento que a gente promete. Renomear é o que
410
+ * deixa a referência apontando para o vazio.
411
+ */
412
+ const renamed = (prev?.tokenMap ?? []).filter((before) => {
413
+ const now = theirVars.pairs.find((p) => p.ours === before.ours);
414
+ return now && now.theirs !== before.theirs;
415
+ });
416
+ if (renamed.length > 0) {
417
+ console.log(` ${renamed.length} token${renamed.length === 1 ? "" : "s"} you renamed since the last install: ${renamed
418
+ .slice(0, 3)
419
+ .map((r) => `${r.theirs} → ${theirVars.pairs.find((p) => p.ours === r.ours)?.theirs ?? "?"}`)
420
+ .join(", ")}${renamed.length > 3 ? ", …" : ""}. The system follows the new name from here.`);
421
+ }
397
422
  if (theirVars.pointed > 0)
398
423
  console.log(` ${theirVars.pointed} value${theirVars.pointed === 1 ? "" : "s"} in tokens.css now point at the name YOUR code already gives ${theirVars.pointed === 1 ? "it" : "them"} - change yours and the system follows${theirVars.pruned > 0 ? `; ${theirVars.pruned} matched but your build does not emit ${theirVars.pruned === 1 ? "that name" : "those names"}, so ${theirVars.pruned === 1 ? "it keeps" : "they keep"} the value` : ""}`);
399
424
  /**
@@ -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 { inTheirTongue, tongueOf } from "../their-tongue.js";
11
12
  import { readCensus, unreadComment, unreadForComponent, } from "../unread-for-component.js";
12
13
  /**
13
14
  * Writes the shared `cn.ts` next to the components, built from THIS project's
@@ -65,12 +66,41 @@ export async function component(slug, name, opts) {
65
66
  // o .tsx é materializado logo abaixo, ninguém as lê depois (medido na
66
67
  // auditoria de 16/08), e cada arquivo a mais no repo dele é superfície.
67
68
  const config = await readProjectConfig(root);
69
+ /**
70
+ * O CSS PASSA A FALAR A LÍNGUA DELE - ver `inTheirTongue`.
71
+ *
72
+ * O QUE MUDA NO ARQUIVO QUE ELE ABRE: `color: var(--ds-color-ink-900)` vira
73
+ * `color: var(--color-ink-900)`, o nome que o código dele já dá àquele valor. É isso que faz a
74
+ * nossa folha de 3945 linhas deixar de ser necessária no repositório de origem - o `globals.css`
75
+ * dele já declara o token, e a decisão continua sendo dele.
76
+ *
77
+ * AQUI E NÃO NO SERVIDOR, que é onde o CSS é compilado: o mapa descreve ESTE repositório. O mesmo
78
+ * sistema instalado em outro projeto dele não tem os mesmos nomes declarados, e traduzir lá
79
+ * apagaria a cor. Um projeto de destino chega sem mapa no `.lock`, nada é traduzido, e a folha
80
+ * continua sendo o caminho - o comando DIZ qual dos dois aconteceu.
81
+ */
82
+ const tongue = await tongueFor(root, slug);
83
+ const spoken = tongue ? inTheirTongue(res.css, tongue) : null;
84
+ const css = spoken ? spoken.css : res.css;
85
+ /**
86
+ * E ELE FICA SABENDO - traduzir em silêncio é a outra metade do mesmo erro.
87
+ *
88
+ * A última linha é a que decide se a nossa folha ainda é necessária aqui, e ela é a diferença
89
+ * entre "seu repositório dispensa a folha" e "quase" - dizer a primeira quando a verdade é a
90
+ * segunda é a promessa que quebra na tela dele.
91
+ */
92
+ if (spoken && (spoken.named > 0 || spoken.inlined > 0)) {
93
+ console.log(` ${spoken.named} reference${spoken.named === 1 ? "" : "s"} now speak${spoken.named === 1 ? "s" : ""} the name YOUR code gives the value${spoken.inlined > 0 ? `, and ${spoken.inlined} carr${spoken.inlined === 1 ? "ies" : "y"} the value because your code names no token for it` : ""}.`);
94
+ console.log(spoken.left.length === 0
95
+ ? ` Nothing in this file points at our stylesheet - it renders on your own tokens alone.`
96
+ : ` ${spoken.left.length} still point${spoken.left.length === 1 ? "s" : ""} at our stylesheet (${spoken.left.slice(0, 3).join(", ")}${spoken.left.length > 3 ? ", …" : ""}), so tokens.css is still needed here.`);
97
+ }
68
98
  const artifactsAreTheProduct = opts.artifactsOnly === true || config.target !== "next";
69
99
  if (artifactsAreTheProduct) {
70
100
  const dir = join(root, "_synthesisui", "ds", slug, "components");
71
101
  await mkdir(dir, { recursive: true });
72
102
  await writeFile(join(dir, `${res.name}.json`), `${JSON.stringify(res.recipe, null, 2)}\n`, "utf8");
73
- await writeFile(join(dir, `${res.name}.css`), `${res.css}\n`, "utf8");
103
+ await writeFile(join(dir, `${res.name}.css`), `${css}\n`, "utf8");
74
104
  console.log(`✓ ${res.name} → _synthesisui/ds/${slug}/components/${res.name}.{json,css} (${slug} v${res.version})`);
75
105
  }
76
106
  // 2. YOUR component - a real, importable `export function <Pascal>()` in the
@@ -151,12 +181,12 @@ export async function component(slug, name, opts) {
151
181
  // template drives itself off the .ds-* classes.
152
182
  const tsx = interactiveTemplate(res.name);
153
183
  await writeFile(join(compDir, `${res.name}.tsx`), tsx, "utf8");
154
- await writeFile(join(compDir, `${res.name}.css`), `${res.css}\n`, "utf8");
184
+ await writeFile(join(compDir, `${res.name}.css`), `${css}\n`, "utf8");
155
185
  await writeFile(join(compDir, "index.ts"), `export * from "./${res.name}";\n`, "utf8");
156
186
  filenames = [`${res.name}.tsx`, `${res.name}.css`, "index.ts"];
157
187
  }
158
188
  else {
159
- const files = generateComponentFiles(slug, res.name, res.recipe, res.css, res.version, config.styles, await reactMajorOf(root),
189
+ const files = generateComponentFiles(slug, res.name, res.recipe, css, res.version, config.styles, await reactMajorOf(root),
160
190
  /**
161
191
  * THE SPELLING, FROM THE VERSION WE JUST FETCHED.
162
192
  *
@@ -246,3 +276,35 @@ export async function component(slug, name, opts) {
246
276
  }
247
277
  console.log("");
248
278
  }
279
+ /**
280
+ * O VOCABULÁRIO DESTE REPOSITÓRIO, do que o `add` já deixou na pasta - ver `tongueOf`.
281
+ *
282
+ * `null` quando não há mapa: é a resposta de um projeto de destino, de um repositório que nunca
283
+ * buildou, ou de um install feito por um CLI anterior a 0.16.290. Nos três casos nada é traduzido e
284
+ * a folha instalada continua sendo o caminho, que é o comportamento de sempre.
285
+ */
286
+ async function tongueFor(root, slug) {
287
+ const dir = join(root, "_synthesisui", "ds", slug);
288
+ const lock = await readFile(join(dir, ".lock"), "utf8").catch(() => null);
289
+ if (!lock)
290
+ return null;
291
+ let map = [];
292
+ let version = 0;
293
+ try {
294
+ const parsed = JSON.parse(lock);
295
+ map = parsed.tokenMap ?? [];
296
+ version = parsed.version ?? 0;
297
+ }
298
+ catch {
299
+ return null;
300
+ }
301
+ if (map.length === 0)
302
+ return null;
303
+ /**
304
+ * O VALOR COMPILADO MORA NA PASTA DA VERSÃO - a folha da raiz é um re-export de uma linha, escrito
305
+ * assim de propósito para que o `@import` dele nunca mude entre updates (ver `add.ts`). É de lá
306
+ * que sai o literal para as variáveis que não têm nome dele.
307
+ */
308
+ const installed = await readFile(join(dir, `v${version}`, "tokens.css"), "utf8").catch(() => "");
309
+ return tongueOf(map, installed);
310
+ }
@@ -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.285";
131
+ export const MATERIALISER_SINCE = "0.16.290";
132
132
  /**
133
133
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
134
134
  *
@@ -63,6 +63,15 @@ export async function repoStateOf(projectRoot, slug, cli) {
63
63
  const payload = {
64
64
  ...(typeof lock?.version === "number" ? { installed: lock.version } : {}),
65
65
  ...(lock?.adopted === true ? { adopted: true } : {}),
66
+ /**
67
+ * O MAPA, do `.lock` - ver `RepoStatePayload.tokenMap`.
68
+ *
69
+ * Lido e não recalculado: o cruzamento é caro (varre as folhas dele e o build) e já foi feito
70
+ * pelo `add`, que é quem tinha o payload em mãos. Aqui ele só viaja.
71
+ */
72
+ ...(lock?.tokenMap && lock.tokenMap.length > 0
73
+ ? { tokenMap: lock.tokenMap }
74
+ : {}),
66
75
  cli,
67
76
  ...(doctor?.at ? { doctorAt: doctor.at } : {}),
68
77
  ...(typeof doctor?.phantoms === "number"
@@ -0,0 +1,47 @@
1
+ /** `var(--ds-x)` e `var(--ds-x, fallback)` - o que a folha compilada escreve. */
2
+ const OUR_VAR = /var\(\s*(--ds-[a-zA-Z0-9_-]+)\s*(?:,([^()]*))?\)/g;
3
+ export function inTheirTongue(css, tongue) {
4
+ let named = 0;
5
+ let inlined = 0;
6
+ const left = new Set();
7
+ const out = css.replace(OUR_VAR, (whole, name, fallback) => {
8
+ const theirName = tongue.names.get(name);
9
+ if (theirName) {
10
+ named += 1;
11
+ /**
12
+ * O FALLBACK NÃO VIAJA. `var(--x, #valor)` sobreviveria a um rename DELE pintando o valor
13
+ * velho - a plataforma decidindo por cima da decisão dele. Se ele renomear, quebra, e o
14
+ * `doctor` diz qual (dono, 22/08).
15
+ */
16
+ return `var(${theirName})`;
17
+ }
18
+ const value = tongue.values.get(name);
19
+ if (value) {
20
+ inlined += 1;
21
+ return value;
22
+ }
23
+ left.add(name);
24
+ return fallback ? whole : whole;
25
+ });
26
+ return { css: out, named, inlined, left: [...left].sort() };
27
+ }
28
+ /**
29
+ * O VOCABULÁRIO DESTE REPOSITÓRIO, montado do que o `add` já deixou na pasta.
30
+ *
31
+ * `map` vem do `.lock` (`tokenMap`) e é o que a máquina dele descobriu; `declared` vem do
32
+ * `tokens.css` instalado, que é onde cada variável nossa tem o valor compilado. Nenhuma medição
33
+ * nova: as duas coisas já estão no disco.
34
+ */
35
+ export function tongueOf(map, installedCss) {
36
+ const names = new Map(map.map((p) => [p.ours, p.theirs]));
37
+ const values = new Map();
38
+ for (const m of installedCss.matchAll(/^\s*(--ds-[a-zA-Z0-9_-]+)\s*:\s*([^;]+);/gm)) {
39
+ const value = m[2].trim();
40
+ /** Uma linha que aponta para outra variável não é um valor - seguir a cadeia é do navegador. */
41
+ if (value.includes("var("))
42
+ continue;
43
+ if (!values.has(m[1]))
44
+ values.set(m[1], value);
45
+ }
46
+ return { names, values };
47
+ }
@@ -95,12 +95,18 @@ export async function resolvableVars(root) {
95
95
  * ou `null` quando não há build - e aí nada é reescrito.
96
96
  */
97
97
  export function pointAtTheirNames(css, theirs, resolvable) {
98
+ /**
99
+ * SEM BUILD, SEM MAPA - a mesma regra da reescrita, e pelo mesmo motivo: um repositório que nunca
100
+ * buildou não tem como provar o que resolve, e um par afirmado sem prova é pior que par nenhum.
101
+ */
98
102
  if (!resolvable || theirs.byName.size === 0)
99
- return { css, pointed: 0, pruned: 0 };
103
+ return { css, pointed: 0, pruned: 0, pairs: [] };
100
104
  const ours = buildTable({ css, source: "installed" });
101
105
  const alias = theirNames(ours, theirs);
102
106
  let pointed = 0;
103
107
  let pruned = 0;
108
+ /** Ver `PointedAt.pairs`: o mapa sai do mesmo casamento que reescreve a linha. */
109
+ const pairs = [];
104
110
  const out = css.replace(/^(\s*)(--ds-[a-zA-Z0-9_-]+)(\s*:\s*)([^;]+);/gm, (line, indent, name, sep, value) => {
105
111
  /** Uma linha que já aponta para outra variável não é uma cópia de valor. */
106
112
  if (value.includes("var("))
@@ -116,9 +122,10 @@ export function pointAtTheirNames(css, theirs, resolvable) {
116
122
  return line;
117
123
  }
118
124
  pointed += 1;
125
+ pairs.push({ ours: name, theirs: theirName, value: value.trim() });
119
126
  return `${indent}${name}${sep}var(${theirName});`;
120
127
  });
121
- return { css: out, pointed, pruned };
128
+ return { css: out, pointed, pruned, pairs };
122
129
  }
123
130
  /**
124
131
  * O PREFIXO DA NOSSA VARIÁVEL -> A FAMÍLIA, e a chave que `theirNames` devolve é `<família>:<valor>`.
@@ -142,7 +149,7 @@ const FAMILY_KIND = {
142
149
  export async function pointTokensAtTheirNames(root, payload) {
143
150
  const css = payload.artifacts["tokens.css"] ?? "";
144
151
  if (!css)
145
- return { css, pointed: 0, pruned: 0 };
152
+ return { css, pointed: 0, pruned: 0, pairs: [] };
146
153
  const [theirs, resolvable] = await Promise.all([
147
154
  harvestTheirCss(root),
148
155
  resolvableVars(root),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.289",
3
+ "version": "0.16.290",
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": {