synthesisui 0.16.497 → 0.16.498

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.
@@ -21,7 +21,7 @@ import { describeFiltered, ruleApplies, rulesForProject, } from "../rule-filter.
21
21
  import { detectStack, resolveDeps } from "../stack.js";
22
22
  import { onlyWhatMatched } from "../their-theme.js";
23
23
  import { tongueOf } from "../their-tongue.js";
24
- import { pointTokensAtTheirNames } from "../their-vars.js";
24
+ import { namesTheyDeclare, pointTokensAtTheirNames } from "../their-vars.js";
25
25
  import { tracksAnyOf } from "../tracked.js";
26
26
  /**
27
27
  * QUAL METADE DESTA PASTA UM TIME COMMITA.
@@ -252,6 +252,22 @@ export async function add(slug, opts) {
252
252
  const theirVars = await pointTokensAtTheirNames(projectRoot, payload,
253
253
  /** O que um build anterior provou - ver `provenBefore`. Sem build aqui, é a melhor prova que há. */
254
254
  (prev?.tokenMap ?? []).map((p) => p.theirs));
255
+ /**
256
+ * O PAR QUE O `init` DECLAROU CONTINUA (jornada "o pedido livre sai mastigado", 15/10).
257
+ *
258
+ * Num projeto novo o `init` escreve os nomes do sistema no `@theme` dele e grava cada par no `.lock`. O `connect`, logo
259
+ * em seguida, passa por aqui - e o cruzamento acima só reconhece o nome de mesmo formato (`--ds-color-sand-50` com
260
+ * `--color-sand-50`), não o papel (`--ds-color-semantic-primary` com `--color-primary`). O mapa caía de 91 para 27 pares,
261
+ * e o template voltava a 40 valores crus no hero. O par anterior fica enquanto o CSS dele ainda declara aquele nome: o
262
+ * nome sumiu, o par some junto.
263
+ */
264
+ const reproved = new Set(theirVars.pairs.map((p) => p.ours));
265
+ const before = (prev?.tokenMap ?? []).filter((p) => !reproved.has(p.ours));
266
+ const stillDeclared = await namesTheyDeclare(projectRoot, before.map((p) => p.theirs));
267
+ const tokenMap = [
268
+ ...theirVars.pairs,
269
+ ...before.filter((p) => stillDeclared.has(p.theirs)),
270
+ ];
255
271
  /**
256
272
  * O `theme.css` ALINHA ONDE HÁ O QUE ALINHAR - ver `onlyWhatMatched`.
257
273
  *
@@ -416,7 +432,7 @@ export async function add(slug, opts) {
416
432
  */
417
433
  ...(theirVars.pointed > 0 ? { theirVars: theirVars.pointed } : {}),
418
434
  /** Ver `RootLock.tokenMap`: o fato, ao lado do número que ele produz. */
419
- ...(theirVars.pairs.length > 0 ? { tokenMap: theirVars.pairs } : {}),
435
+ ...(tokenMap.length > 0 ? { tokenMap } : {}),
420
436
  ...(scope ? { scope } : {}),
421
437
  ...(systems.length > 0 ? { scopes: systems } : {}),
422
438
  ...(usage.length > 0 ? { usage } : {}),
@@ -1,13 +1,12 @@
1
- import { DEFAULT_CONFIG, writeProjectConfig } from "../config.js";
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { DEFAULT_CONFIG, resolveRegistry, writeProjectConfig, } from "../config.js";
4
+ import { readProjectTheme, writeBesideTheirs, writeNewProjectRoot, writeNewProjectTheme, } from "../new-project-theme.js";
2
5
  import { body, section, snippet } from "../output.js";
6
+ import { fetchDesignSystem } from "../registry.js";
3
7
  import { resolveDeps, stylesFor, tailwindMajor } from "../stack.js";
4
8
  import { formMix, measuredStyle } from "../styles-flavour.js";
5
- import { readFile } from "node:fs/promises";
6
- import { join } from "node:path";
7
- import { readProjectTheme, writeBesideTheirs, writeNewProjectRoot, writeNewProjectTheme } from "../new-project-theme.js";
8
9
  import { add } from "./add.js";
9
- import { resolveRegistry } from "../config.js";
10
- import { fetchDesignSystem } from "../registry.js";
11
10
  const whereOf = (answer) => answer === "file" || answer === "1"
12
11
  ? "file"
13
12
  : answer === "separate" || answer === "2"
@@ -195,9 +194,13 @@ export async function init(opts) {
195
194
  const tokens = version !== null
196
195
  ? await readFile(join(slugDir, `v${version}`, "tokens.css"), "utf8").catch(() => "")
197
196
  : "";
198
- const plan = tokens ? await readProjectTheme(root, tokens, opts.ds) : null;
197
+ const plan = tokens
198
+ ? await readProjectTheme(root, tokens, opts.ds)
199
+ : null;
199
200
  if (plan?.kind === "new" && version !== null) {
200
201
  const theme = await writeNewProjectTheme(root, tokens, opts.ds, version);
202
+ if (theme.written)
203
+ await recordDeclaredNames(slugDir, theme.pairs ?? []);
201
204
  if (theme.written) {
202
205
  console.log("");
203
206
  console.log(`✓ ${theme.written}: ${theme.count} names of ${opts.ds} declared in your own @theme, with the values written - your project had none of its own. They are yours: bg-accent, p-md and rounded-lg now paint the system. Fonts come from the GUIDE's next/font recipe; the alternate scheme is not wired.`);
@@ -235,8 +238,14 @@ export async function init(opts) {
235
238
  const slugDir = join(root, "_synthesisui", "ds", opts.ds);
236
239
  const lock = JSON.parse(await readFile(join(slugDir, ".lock"), "utf8").catch(() => "{}"));
237
240
  const version = typeof lock.version === "number" ? lock.version : null;
238
- const tokens = version !== null ? await readFile(join(slugDir, `v${version}`, "tokens.css"), "utf8").catch(() => "") : "";
239
- const theme = tokens && version !== null ? await writeNewProjectRoot(root, tokens, opts.ds, version) : null;
241
+ const tokens = version !== null
242
+ ? await readFile(join(slugDir, `v${version}`, "tokens.css"), "utf8").catch(() => "")
243
+ : "";
244
+ const theme = tokens && version !== null
245
+ ? await writeNewProjectRoot(root, tokens, opts.ds, version)
246
+ : null;
247
+ if (theme?.written)
248
+ await recordDeclaredNames(slugDir, theme.pairs ?? []);
240
249
  if (theme?.written) {
241
250
  console.log("");
242
251
  console.log(`✓ ${theme.written}: ${theme.count} names of ${opts.ds} declared in your own :root, with the values written - your project had none of its own. They are yours: var(--color-primary) and var(--spacing-md) now paint the system.`);
@@ -267,3 +276,24 @@ export async function init(opts) {
267
276
  console.log(" • synthesisui add <slug> bring a design system in");
268
277
  console.log(" • synthesisui template <slug> <name> materialize a full page");
269
278
  }
279
+ /**
280
+ * O QUE O `init` ACABOU DE DECLARAR VAI PARA O `.lock` (jornada "o pedido livre sai mastigado", 15/10).
281
+ *
282
+ * O `add` roda ANTES de o tema ser escrito, quando o projeto novo ainda não nomeia nada, e grava o `tokenMap` vazio. O
283
+ * `template` e o `component` traduzem pela mesma `tongue` que lê esse mapa - então a primeira landing de um projeto novo
284
+ * vinha com ~260 valores à mão (medido na rodada com agente de 15/10: "0 references now speak the name YOUR code gives
285
+ * the value, and 44 carry the value"), e o agente gastava rodadas convertendo-os de volta. Os pares aqui não são palpite:
286
+ * são os nomes que o próprio `init` escreveu, um a um, com o valor de cada um. O que o mapa já tinha continua.
287
+ */
288
+ async function recordDeclaredNames(slugDir, pairs) {
289
+ if (pairs.length === 0)
290
+ return;
291
+ const path = join(slugDir, ".lock");
292
+ const lock = JSON.parse(await readFile(path, "utf8").catch(() => "{}"));
293
+ const known = new Set((lock.tokenMap ?? []).map((p) => p.ours));
294
+ const tokenMap = [
295
+ ...(lock.tokenMap ?? []),
296
+ ...pairs.filter((p) => !known.has(p.ours)),
297
+ ];
298
+ await writeFile(path, `${JSON.stringify({ ...lock, tokenMap }, null, 2)}\n`, "utf8");
299
+ }
@@ -333,7 +333,12 @@
333
333
  * servidor diz que uma landing pedida em palavras livres passa primeiro pelo `landing`. Uma pasta escrita antes deixa o
334
334
  * agente decidir a página inteira sozinho.
335
335
  */
336
- export const MATERIALISER_SINCE = "0.16.497";
336
+ /**
337
+ * 0.16.497 -> 0.16.498 em 15/10, e o passo 1 dá **SIM** (o pedido livre sai mastigado, a otimização): o `init` de um projeto
338
+ * novo grava no `.lock` os nomes que acabou de declarar, e o `template` e o `component` passam a escrevê-los - 51 nomes e 3
339
+ * valores crus no hero, contra 0 e 54 antes. Uma pasta escrita antes traz a landing com os valores à mão.
340
+ */
341
+ export const MATERIALISER_SINCE = "0.16.498";
337
342
  /**
338
343
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
339
344
  *
@@ -12,7 +12,15 @@ const BOILERPLATE = new Set([
12
12
  "--font-geist-sans",
13
13
  "--font-geist-mono",
14
14
  ]);
15
- const SKIP = new Set(["node_modules", ".next", "dist", "build", "out", "_synthesisui", "coverage"]);
15
+ const SKIP = new Set([
16
+ "node_modules",
17
+ ".next",
18
+ "dist",
19
+ "build",
20
+ "out",
21
+ "_synthesisui",
22
+ "coverage",
23
+ ]);
16
24
  const TAILWIND_IMPORT = /@import\s+[\x22\x27]tailwindcss[\x22\x27]/;
17
25
  /**
18
26
  * O COMENTÁRIO DO CSS SE ESCREVE COM `\u002a` NO FONTE, e não com o asterisco - a guarda de
@@ -78,7 +86,9 @@ export async function writeNewProjectTheme(root, tokensCss, slug, version) {
78
86
  const withoutOurs = css.replace(END_RE(slug), "\n");
79
87
  for (const m of withoutOurs.matchAll(/(?:^|[{;\s])(--[a-zA-Z0-9_-]+)\s*:/g)) {
80
88
  const name = m[1];
81
- if (!name.startsWith("--ds-") && !name.startsWith("--tw-") && !BOILERPLATE.has(name))
89
+ if (!name.startsWith("--ds-") &&
90
+ !name.startsWith("--tw-") &&
91
+ !BOILERPLATE.has(name))
82
92
  own.add(name);
83
93
  }
84
94
  }
@@ -112,7 +122,11 @@ export async function writeNewProjectTheme(root, tokensCss, slug, version) {
112
122
  ].join("\n");
113
123
  const base = css.replace(END_RE(slug), "\n").replace(/\n*$/, "\n");
114
124
  await writeFile(path, `${base}\n${block}\n`, "utf8");
115
- return { written: relative(root, path).split("\\").join("/"), count: lines.length };
125
+ return {
126
+ written: relative(root, path).split("\\").join("/"),
127
+ count: lines.length,
128
+ pairs: systemPairs(tokensCss, (await readTheirs(root, slug)).every),
129
+ };
116
130
  }
117
131
  /**
118
132
  * O VOCABULÁRIO DELE E O CSS GLOBAL, para quem escreve nomes fora do `init` - o `colours` (etapa 05, 28/09). A mesma
@@ -176,11 +190,17 @@ export async function writeNewProjectRoot(root, tokensCss, slug, version) {
176
190
  ].join("\n");
177
191
  const base = css.replace(END_RE(slug), "\n").replace(/\n*$/, "\n");
178
192
  await writeFile(path, `${base}\n${block}\n`, "utf8");
179
- return { written: relative(root, path).split("\\").join("/"), count: lines.length };
193
+ return {
194
+ written: relative(root, path).split("\\").join("/"),
195
+ count: lines.length,
196
+ pairs: systemPairs(tokensCss, r.every),
197
+ };
180
198
  }
181
199
  const importLine = (slug) => `@import "./${slug}.css";`;
182
200
  /** O CSS dele sem nada nosso: sem o bloco e sem a linha que importa o arquivo à parte. */
183
- const withoutOurs = (css, slug) => css.replace(END_RE(slug), "\n").replace(new RegExp(`^${escape(importLine(slug))}\\n`, "m"), "");
201
+ const withoutOurs = (css, slug) => css
202
+ .replace(END_RE(slug), "\n")
203
+ .replace(new RegExp(`^${escape(importLine(slug))}\\n`, "m"), "");
184
204
  async function readTheirs(root, slug) {
185
205
  const sheets = await sheetsOf(root);
186
206
  const raw = new Map();
@@ -228,10 +248,13 @@ async function readTheirs(root, slug) {
228
248
  const everyRaw = new Map(raw);
229
249
  for (const full of sheets.values())
230
250
  for (const m of full.matchAll(/(?:^|[{;\s])(--[a-zA-Z0-9_-]+)\s*:\s*([^;}]+)/g))
231
- if (!everyRaw.has(m[1]) && !m[1].startsWith("--ds-"))
251
+ if (!everyRaw.has(m[1]) &&
252
+ !m[1].startsWith("--ds-"))
232
253
  everyRaw.set(m[1], m[2].trim());
233
254
  const every = resolved(everyRaw);
234
- const entries = [...sheets].filter(([, css]) => TAILWIND_IMPORT.test(css)).map(([p]) => p);
255
+ const entries = [...sheets]
256
+ .filter(([, css]) => TAILWIND_IMPORT.test(css))
257
+ .map(([p]) => p);
235
258
  const rel = (p) => relative(root, p).split("\\").join("/");
236
259
  const top = [...counts].sort((a, b) => b[1] - a[1])[0];
237
260
  const entry = entries.length === 1 ? entries[0] : null;
@@ -254,6 +277,21 @@ async function readTheirs(root, slug) {
254
277
  * criou (`--ds-color-amber-400: var(--color-amber-400)`). Pular essas linhas encolhia o bloco na segunda
255
278
  * rodada: medido na prova de 27/09, 82 nomes viraram 38.
256
279
  */
280
+ /** Os mesmos nomes de `systemNames`, com a variável nossa ao lado - o par que o `.lock` guarda. */
281
+ function systemPairs(tokensCss, lookup = new Map()) {
282
+ const { names, values } = tongueOf([], tokensCss);
283
+ const out = [];
284
+ for (const ours of new Set([...values.keys(), ...names.keys()])) {
285
+ const theirs = theirNameFor(ours);
286
+ if (!theirs)
287
+ continue;
288
+ const pointed = names.get(ours);
289
+ const value = pointed ? lookup.get(pointed) : values.get(ours);
290
+ if (value && !value.includes("var(--ds-"))
291
+ out.push({ ours, theirs, value });
292
+ }
293
+ return out;
294
+ }
257
295
  function systemNames(tokensCss, lookup = new Map()) {
258
296
  const { names, values } = tongueOf([], tokensCss);
259
297
  const out = [];
@@ -273,7 +311,9 @@ export async function readProjectTheme(root, tokensCss, slug) {
273
311
  const r = await readTheirs(root, slug);
274
312
  const base = { entry: r.entry, tokensFile: r.tokensFile };
275
313
  const colourValues = [
276
- ...new Set(systemNames(tokensCss, r.every).filter(([n]) => n.startsWith("--color-")).map(([, v]) => v.toLowerCase())),
314
+ ...new Set(systemNames(tokensCss, r.every)
315
+ .filter(([n]) => n.startsWith("--color-"))
316
+ .map(([, v]) => v.toLowerCase())),
277
317
  ];
278
318
  const held = new Set([...r.values.values()].map((v) => v.trim().toLowerCase()));
279
319
  const matched = colourValues.filter((v) => held.has(v)).length;
@@ -284,7 +324,11 @@ export async function readProjectTheme(root, tokensCss, slug) {
284
324
  return { kind: "many-tailwind-entries", ...counts, ...base };
285
325
  if (r.own.size === 0)
286
326
  return { kind: "new", ...counts, ...base };
287
- return { kind: matched * 2 >= colourValues.length && matched > 0 ? "matches" : "differs", ...counts, ...base };
327
+ return {
328
+ kind: matched * 2 >= colourValues.length && matched > 0 ? "matches" : "differs",
329
+ ...counts,
330
+ ...base,
331
+ };
288
332
  }
289
333
  /**
290
334
  * O BLOCO DO SISTEMA AO LADO DO DELE - o terceiro caso.
@@ -305,7 +349,9 @@ export async function readProjectTheme(root, tokensCss, slug) {
305
349
  export async function writeBesideTheirs(root, tokensCss, slug, version, where) {
306
350
  const r = await readTheirs(root, slug);
307
351
  const entryPath = r.entry ? join(root, r.entry) : null;
308
- const separatePath = entryPath ? join(dirname(entryPath), `${slug}.css`) : null;
352
+ const separatePath = entryPath
353
+ ? join(dirname(entryPath), `${slug}.css`)
354
+ : null;
309
355
  /** Tira de todo lugar o que é nosso, antes de escrever no lugar escolhido. */
310
356
  const cleaned = new Map();
311
357
  for (const [path, css] of r.sheets) {
@@ -317,7 +363,9 @@ export async function writeBesideTheirs(root, tokensCss, slug, version, where) {
317
363
  for (const [path, css] of cleaned)
318
364
  if (css !== r.sheets.get(path))
319
365
  await writeFile(path, css, "utf8");
320
- if (separatePath && r.sheets.get(separatePath)?.startsWith(begin(slug)) && where !== "separate")
366
+ if (separatePath &&
367
+ r.sheets.get(separatePath)?.startsWith(begin(slug)) &&
368
+ where !== "separate")
321
369
  await rm(separatePath, { force: true });
322
370
  };
323
371
  const empty = { written: null, collisions: [], throughInline: [], added: 0 };
@@ -332,7 +380,9 @@ export async function writeBesideTheirs(root, tokensCss, slug, version, where) {
332
380
  }
333
381
  const missing = system.filter(([n]) => !r.own.has(n) && !r.values.has(n));
334
382
  const collisions = system
335
- .filter(([n, v]) => r.values.has(n) && !r.inline.has(n) && !same(r.values.get(n), v))
383
+ .filter(([n, v]) => r.values.has(n) &&
384
+ !r.inline.has(n) &&
385
+ !same(r.values.get(n), v))
336
386
  .map(([n]) => n);
337
387
  const throughInline = [];
338
388
  const scoped = system.map(([n, v]) => {
@@ -346,7 +396,9 @@ export async function writeBesideTheirs(root, tokensCss, slug, version, where) {
346
396
  const block = [
347
397
  begin(slug),
348
398
  open(`${slug} v${version}, beside your own tokens. The @theme adds only the names your project did not have; inside the data-theme selector below every value is the system's, including the names you already use, which keep their value everywhere else. Written by \`init\`, where you chose.`),
349
- ...(missing.length > 0 ? ["@theme {", ...missing.map(([n, v]) => ` ${n}: ${v};`), "}", ""] : []),
399
+ ...(missing.length > 0
400
+ ? ["@theme {", ...missing.map(([n, v]) => ` ${n}: ${v};`), "}", ""]
401
+ : []),
350
402
  `[data-theme="${slug}"] {`,
351
403
  ...scoped,
352
404
  "}",
@@ -1,6 +1,6 @@
1
1
  import { readdir, readFile } from "node:fs/promises";
2
- import { readStyleSheet } from "./doctor/scss-lens.js";
3
2
  import { join } from "node:path";
3
+ import { readStyleSheet } from "./doctor/scss-lens.js";
4
4
  import { theirNames } from "./doctor/their-names.js";
5
5
  import { buildTable, normalizeValue, } from "./doctor/tokens.js";
6
6
  import { FAMILY_SEAM_PREFIX } from "./fonts.js";
@@ -268,3 +268,13 @@ export function danglingTheirVars(installedCss, theirs) {
268
268
  }
269
269
  return out;
270
270
  }
271
+ /**
272
+ * OS NOMES QUE O CSS DELE AINDA DECLARA, dentre os pedidos - para o `add` não apagar o par que o `init` gravou
273
+ * (jornada "o pedido livre sai mastigado", 15/10). Ver o uso em `commands/add.ts`.
274
+ */
275
+ export async function namesTheyDeclare(root, names) {
276
+ if (names.length === 0)
277
+ return new Set();
278
+ const theirs = await harvestTheirCss(root);
279
+ return new Set(names.filter((n) => theirs.byName.has(n)));
280
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.497",
3
+ "version": "0.16.498",
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": {