synthesisui 0.16.289 → 0.16.291

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
+ }
@@ -1,7 +1,8 @@
1
- import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { generateComponentFiles } from "../component-codegen.js";
4
4
  import { readProjectConfig, resolveRegistry } from "../config.js";
5
+ import { installedSlugs } from "../installed.js";
5
6
  import { reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
6
7
  import { postGenerate, RegistryError } from "../registry.js";
7
8
  /** PascalCase para o hint de import (course-card → CourseCard). */
@@ -20,17 +21,6 @@ async function readActiveVersion(root, slug) {
20
21
  }
21
22
  }
22
23
  /** Slugs materialized under `_synthesisui/ds/` in the project. */
23
- async function installedSlugs(root) {
24
- try {
25
- const entries = await readdir(join(root, "_synthesisui", "ds"), {
26
- withFileTypes: true,
27
- });
28
- return entries.filter((e) => e.isDirectory()).map((e) => e.name);
29
- }
30
- catch {
31
- return [];
32
- }
33
- }
34
24
  /**
35
25
  * Generates a token-only component recipe for the project's design system
36
26
  * (chat-gen PRO, hosted) and materializes it additively under
@@ -1,35 +1,13 @@
1
- import { access, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
1
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { basename, join } from "node:path";
3
3
  import { generateComponentFiles } from "../component-codegen.js";
4
4
  import { readProjectConfig, resolveRegistry } from "../config.js";
5
+ import { installedSlugs } from "../installed.js";
5
6
  import { body, section, snippet } from "../output.js";
6
7
  import { reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
7
8
  import { fetchComponent, postRefit, postSaveComponent, RegistryError, } from "../registry.js";
8
9
  /** Slugs INSTALLED under `_synthesisui/ds/` (a `.lock` marks a real install -
9
10
  * a folder holding only refit artifacts doesn't count). */
10
- async function installedSlugs(root) {
11
- try {
12
- const entries = await readdir(join(root, "_synthesisui", "ds"), {
13
- withFileTypes: true,
14
- });
15
- const slugs = [];
16
- for (const entry of entries) {
17
- if (!entry.isDirectory())
18
- continue;
19
- try {
20
- await access(join(root, "_synthesisui", "ds", entry.name, ".lock"));
21
- slugs.push(entry.name);
22
- }
23
- catch {
24
- // artifacts-only folder (e.g. a refit before `add`) - not installed
25
- }
26
- }
27
- return slugs;
28
- }
29
- catch {
30
- return [];
31
- }
32
- }
33
11
  /** True when the system is actually installed (tokens.css present). */
34
12
  async function isInstalled(root, slug) {
35
13
  try {
package/dist/index.js CHANGED
@@ -30,6 +30,7 @@ import { template } from "./commands/template.js";
30
30
  import { upgrade } from "./commands/upgrade.js";
31
31
  import { use } from "./commands/use.js";
32
32
  import { appendEvent } from "./doctor/ledger.js";
33
+ import { blueprintTarget, installedSlugs } from "./installed.js";
33
34
  import { RegistryError } from "./registry.js";
34
35
  /** Our own version, for pinning the hook and MCP commands we write into a
35
36
  * project. Read from the package we are running out of, so a pinned command
@@ -44,8 +45,10 @@ Usage - deterministic, FREE:
44
45
  synthesisui list [options] list the published design systems
45
46
  synthesisui list --mine your own systems, with their group
46
47
  synthesisui add <slug> [options] materialize a DS into _synthesisui/ds/<slug>/
47
- synthesisui component <slug> <name> bring one EXISTING component in as YOUR <Pascal>.tsx
48
- synthesisui bp <slug> <name> the same command, short for blueprint
48
+ synthesisui bp <name> bring one EXISTING blueprint in as YOUR <Pascal>.tsx
49
+ (the slug is optional - this repo's system answers for it)
50
+ synthesisui bp <slug> <name> the same, from another system
51
+ synthesisui blueprint | component the same command, spelled in full
49
52
  synthesisui template <slug> <name> materialize a whole page from a DS template
50
53
  (--as landing-home names the output - multi-page safe)
51
54
  synthesisui upgrade <slug> update an installed DS + regenerate your components + migration brief
@@ -468,14 +471,42 @@ async function main() {
468
471
  * que apelido nenhum.
469
472
  */
470
473
  case "bp":
474
+ case "blueprint":
471
475
  case "component": {
472
- const slug = args[0];
473
- const name = args[1];
474
- if (!slug || !name) {
475
- console.error("error: provide slug and component name - `synthesisui component <slug> <name>`");
476
+ /**
477
+ * O SLUG É OPCIONAL, e o repositório responde por ele.
478
+ *
479
+ * `synthesisui bp card` num repositório que tem UM sistema instalado é a forma que a pessoa
480
+ * escreve naturalmente - ela está dentro do projeto, o sistema dela é aquele, e repetir o nome
481
+ * dele é a plataforma pedindo uma informação que ela já tem no disco.
482
+ *
483
+ * COM DOIS ARGUMENTOS o primeiro é o slug, que é o comportamento de sempre e o que permite
484
+ * trazer um blueprint de OUTRO sistema para cá.
485
+ *
486
+ * E O CASO AMBÍGUO É RECUSADO EM VEZ DE ADIVINHADO: um argumento só que é exatamente o nome do
487
+ * sistema instalado - `synthesisui bp codelevel-ds` - é o erro que o comentário acima registra,
488
+ * e ele tem duas leituras opostas ("instale este sistema" / "materialize um blueprint chamado
489
+ * codelevel-ds"). Escolher uma em silêncio seria acertar metade das vezes.
490
+ */
491
+ const root = dir ?? process.cwd();
492
+ const installed = await installedSlugs(root);
493
+ const target = blueprintTarget({
494
+ args: [args[0], args[1]],
495
+ local: installed.length === 1 ? installed[0] : null,
496
+ installed,
497
+ });
498
+ if ("error" in target) {
499
+ console.error(target.error === "ambiguous"
500
+ ? `error: "${target.name}" is the system installed here, not a blueprint name - say which blueprint you want: \`synthesisui bp <name>\``
501
+ : target.error === "no-name"
502
+ ? "error: provide a blueprint name - `synthesisui bp <name>`, or `synthesisui bp <slug> <name>` for another system"
503
+ : target.error === "no-system"
504
+ ? "error: no design system installed here - run `synthesisui add <slug>` first, or name one: `synthesisui bp <slug> <name>`"
505
+ : `error: ${installed.length} systems installed here (${installed.join(", ")}) - say which: \`synthesisui bp <slug> <name>\``);
476
506
  process.exitCode = 1;
477
507
  return;
478
508
  }
509
+ const { slug, name } = target;
479
510
  let version;
480
511
  if (typeof flags.version === "string") {
481
512
  version = Number.parseInt(flags.version.replace(/^v/i, ""), 10);
@@ -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
  *
@@ -0,0 +1,68 @@
1
+ import { access, readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ /**
4
+ * QUAIS SISTEMAS ESTÃO INSTALADOS NESTE REPOSITÓRIO - um lugar só, e a resposta certa.
5
+ *
6
+ * ESTA FUNÇÃO EXISTIA DUAS VEZES, em `refit.ts` e `generate.ts`, e elas discordavam: a primeira
7
+ * exigia o `.lock` - o arquivo que prova que o `add` rodou - e a segunda listava a pasta. Uma pasta
8
+ * `ds/<slug>/` existe assim que qualquer comando escreve um artefato ali, então o `generate`
9
+ * enxergava como instalado um sistema que ainda não tinha nada materializado, e a mensagem que ele
10
+ * daria ao cliente seria sobre um sistema que ele não pode usar.
11
+ *
12
+ * O `.lock` é o portão porque ele é o que o `add` escreve por último: se ele está lá, os arquivos
13
+ * estão. Ver `RootLock` em `add.ts`.
14
+ *
15
+ * E COM ISTO O SLUG PASSA A SER OPCIONAL nos comandos que agem sobre um sistema. Quem está num
16
+ * repositório que tem um sistema instalado não deveria precisar repetir o nome dele - o repositório
17
+ * já sabe.
18
+ */
19
+ export async function installedSlugs(root) {
20
+ try {
21
+ const dsDir = join(root, "_synthesisui", "ds");
22
+ const entries = await readdir(dsDir, { withFileTypes: true });
23
+ const slugs = [];
24
+ for (const entry of entries) {
25
+ if (!entry.isDirectory())
26
+ continue;
27
+ try {
28
+ await access(join(dsDir, entry.name, ".lock"));
29
+ slugs.push(entry.name);
30
+ }
31
+ catch {
32
+ // pasta de artefato (um refit antes do `add`, por exemplo) - não é um install
33
+ }
34
+ }
35
+ return slugs.sort();
36
+ }
37
+ catch {
38
+ return [];
39
+ }
40
+ }
41
+ /**
42
+ * O SISTEMA DESTE REPOSITÓRIO, quando há exatamente um - e `null` quando a pergunta não tem resposta.
43
+ *
44
+ * Nenhum instalado e dois instalados são situações diferentes com a mesma consequência: a plataforma
45
+ * não pode escolher por ele. Escolher o primeiro em ordem alfabética seria a pior forma de errar -
46
+ * silenciosa, e correta na metade dos casos. Quem chama diz o que fazer com o `null`, e o que ele
47
+ * tem a dizer é diferente nos dois casos, então `installedSlugs` continua disponível para nomear os
48
+ * candidatos na mensagem.
49
+ */
50
+ export async function theInstalledSlug(root) {
51
+ const slugs = await installedSlugs(root);
52
+ return slugs.length === 1 ? slugs[0] : null;
53
+ }
54
+ export function blueprintTarget(input) {
55
+ const [first, second] = input.args;
56
+ if (first && second)
57
+ return { slug: first, name: second };
58
+ const name = first;
59
+ if (!name)
60
+ return { error: "no-name" };
61
+ if (input.local && name === input.local)
62
+ return { error: "ambiguous", name };
63
+ if (!input.local)
64
+ return input.installed.length > 1
65
+ ? { error: "many-systems" }
66
+ : { error: "no-system" };
67
+ return { slug: input.local, name };
68
+ }
@@ -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.291",
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": {