synthesisui 0.16.421 → 0.16.423

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.
@@ -470,8 +470,16 @@ const scaleKey = (v) => {
470
470
  const m = v.match(/^\{typography\.scale\.([a-zA-Z0-9-]+)\.fontSize\}$/);
471
471
  return m ? kebab(m[1]) : null;
472
472
  };
473
- /** "{color.semantic.primary}" → "var(--ds-color-semantic-primary)" - the raw
474
- * scoped vars always exist, so arbitrary-property fallbacks never dangle. */
473
+ /**
474
+ * "{color.semantic.primary}" → "var(--ds-color-semantic-primary)" - a grafia INTERNA, que a porta
475
+ * de tradução (`speak`) reescreve no nome dele ou no valor literal antes de qualquer byte ir a
476
+ * disco.
477
+ *
478
+ * ELA NUNCA SOBREVIVE ATÉ O ARQUIVO DELE (`INV-GERAL-13`): `speak` é a última coisa que roda em
479
+ * `generateComponentFiles`, e `nothing-of-ours-is-asked-for.spec.ts` reprova o texto que sair daqui
480
+ * com a nossa grafia dentro. Existe como forma intermediária porque é ela que carrega o caminho da
481
+ * referência (`color.semantic.primary`) até a folha que sabe o valor.
482
+ */
475
483
  const refToDsVar = (v) => v.replace(/\{([a-z0-9.-]+)\}/gi, (_, path) => {
476
484
  return `var(--ds-${path.split(".").map(kebab).join("-")})`;
477
485
  });
@@ -531,25 +539,28 @@ function remToTwScale(value) {
531
539
  return Number.isInteger(n) && n >= 0 && n <= 96 ? String(n) : null;
532
540
  }
533
541
  /**
534
- * O UTILITÁRIO NOMEADO SÓ VALE QUANDO A FOLHA INSTALADA O DECLARA - e é isso que decide o VALOR.
542
+ * O UTILITÁRIO NOMEADO SÓ VALE QUANDO O `@theme` **DELE** O DECLARA - e é isso que decide o VALOR.
535
543
  *
536
544
  * `rounded-lg` existe em qualquer projeto com Tailwind, então a classe nunca "falta". O que muda é
537
- * de quem é o valor: quando o `theme.css` que caiu na pasta dele declara `--radius-lg`, a classe
538
- * pinta o raio DO SISTEMA; quando não declara, ela pinta o default do Tailwind - e o componente
539
- * mente em silêncio, que é pior que uma classe morta, porque nada na tela parece faltar.
545
+ * de quem é o valor: quando o `@theme` do projeto dele declara `--radius-lg`, a classe pinta o raio
546
+ * DELE, e a decisão continua sendo dele; quando não declara, ela pinta o default do Tailwind - e o
547
+ * componente mente em silêncio, que é pior que uma classe morta, porque nada na tela parece faltar.
540
548
  *
541
- * A folha não declara por dois motivos, os dois legítimos: a redefinição roubaria uma classe que o
542
- * app dele já usa (`INV-VOLTA-13`, o alinhamento), ou o compilador não faz ponte para um nome que
543
- * não está em `meta.ownedNames` (o `--font-body` do codelevel). Nos dois casos a resposta certa é a
544
- * mesma: emitir o valor arbitrário com `var(--ds-*)`, que resolve sempre dentro de `[data-ds]`.
549
+ * A PERGUNTA ERA SOBRE A NOSSA FOLHA ATÉ 11/09, e era a última dependência de runtime que o código
550
+ * entregue carregava. `rounded-lg` escrito porque o NOSSO `theme.css` declara `--radius-lg` só pinta
551
+ * o sistema num app que importa aquele arquivo - e `INV-GERAL-13` diz que nenhum app dele importa. O
552
+ * componente saía com a aparência certa na nossa cabeça e o raio do Tailwind na tela dele.
545
553
  *
546
- * MEDIDO em 07/09 sobre TODOS os componentes das duas populações:
554
+ * QUANDO ELE NÃO DECLARA, a resposta é o valor arbitrário - e a porta de tradução o resolve no nome
555
+ * dele ou no literal, nunca numa variável nossa.
547
556
  *
548
- * codelevel, repo real 58 de 110 utilitários de token (53%) não carregavam o valor dele
557
+ * MEDIDO em 07/09 sobre TODOS os componentes das duas populações, com a folha NOSSA respondendo:
558
+ *
559
+ * codelevel, repo real 58 de 110 utilitários de token (53%) já caíam no arbitrário
549
560
  * ember, app novo 34 de 241 (14%)
550
561
  *
551
- * `null` é projeto sem folha instalada - `refit` e `generate` já dizem que nada pinta sem o `add`,
552
- * e ali o nome legível não custa nada.
562
+ * `null` é "ninguém mediu" - `refit` e `generate` já dizem que nada pinta sem o `add`, e ali o nome
563
+ * legível não custa nada.
553
564
  */
554
565
  const minted = (themeVars, cssVar) => themeVars === null || themeVars.has(cssVar);
555
566
  /** One declaration → Tailwind classes (pretty when mappable, arbitrary-property
@@ -890,15 +901,27 @@ function variantOwnedProps(variants, axis) {
890
901
  return [...props];
891
902
  }
892
903
  // ── Emission ─────────────────────────────────────────────────────────────────
904
+ /**
905
+ * O CABEÇALHO DO ARQUIVO DELE - e as duas linhas de setup saíram dele em 11/09.
906
+ *
907
+ * O QUE ELAS DIZIAM, dentro de TODO `.tsx` que a plataforma escreve no repositório dele:
908
+ * *"Global setup (once per app): import _synthesisui/ds/<slug>/tokens.css"* e *"put data-ds=<slug>
909
+ * on a root element"*. Era a instrução que o produto promete não dar, escrita no lugar onde ela
910
+ * sobrevive a qualquer conserto do terminal - e ela ficava lá, num arquivo dele, para todo agente
911
+ * e toda pessoa lerem depois (`INV-GERAL-13`).
912
+ *
913
+ * E ELA FAZIA UM SEGUNDO ESTRAGO, medido em 10/09: `readWiring` procurava essas mesmas strings no
914
+ * repositório para responder *"este projeto carrega o sistema?"*, então a partir do primeiro
915
+ * componente que NÓS escrevemos ela respondia `imported: true` sobre um projeto que não importa
916
+ * nada. Um comentário nosso nunca provou nada sobre o projeto dele.
917
+ *
918
+ * O que fica é a PROCEDÊNCIA - quem escreveu, de qual sistema, em que versão. Ela não pede nada; ela
919
+ * responde a pergunta de quem abre o arquivo daqui a seis meses.
920
+ */
893
921
  function header(slug, name, version, mode) {
894
- const setup = mode === "tailwind"
895
- ? `import _synthesisui/ds/${slug}/theme.css (Tailwind adapter) + tokens.css`
896
- : `import _synthesisui/ds/${slug}/tokens.css`;
897
922
  return [
898
923
  `// Generated by SynthesisUI - "${name}" from the "${slug}" design system (v${version}).`,
899
- `// On-system by construction: every style resolves to the DS tokens.`,
900
- `// Global setup (once per app): ${setup}`,
901
- `// and put data-ds="${slug}" on a root element (e.g. <body data-ds="${slug}">).`,
924
+ `// Every value here is a name YOUR code declares, or the value itself - nothing to import.`,
902
925
  ...(mode === "tailwind" ? [OVERRIDE_WARNING] : []),
903
926
  ].join("\n");
904
927
  }
@@ -1507,12 +1530,16 @@ scheme,
1507
1530
  */
1508
1531
  tongue,
1509
1532
  /**
1510
- * O QUE A FOLHA INSTALADA DECLARA NO `@theme` - `installedThemeVars(root, slug)`.
1533
+ * O QUE O `@theme` **DELE** DECLARA - `theirThemeVars(root)`.
1511
1534
  *
1512
1535
  * OBRIGATÓRIO, pelo mesmo argumento do `scheme` e do `tongue`: o utilitário nomeado que este
1513
- * módulo escreve só carrega o valor DO SISTEMA quando a folha que caiu na pasta dele declara a
1536
+ * módulo escreve só carrega um valor de design quando o `@theme` do PROJETO dele declara a
1514
1537
  * variável. Sem essa resposta o codegen escreve `rounded-lg` e a classe pinta o default do
1515
- * Tailwind - o componente mente sem nada faltar na tela. `null` é projeto sem folha instalada.
1538
+ * Tailwind - o componente mente sem nada faltar na tela.
1539
+ *
1540
+ * ERA A NOSSA FOLHA QUE RESPONDIA ISTO, e essa era a última dependência de runtime que o código
1541
+ * entregue ainda tinha (`INV-GERAL-13`, 11/09). `null` é "ninguém mediu", e continua significando
1542
+ * que o nome legível não custa nada - `refit` e `generate` já dizem que nada pinta sem o `add`.
1516
1543
  */
1517
1544
  themeVars) {
1518
1545
  const files = [];
@@ -66,6 +66,9 @@ register("pt-BR", {
66
66
  "already current": "já está em dia",
67
67
  "updated to this CLI's pipeline": "atualizada para esta versão",
68
68
  "removed - renamed to /sui-import-ds": "removida - virou /sui-import-ds",
69
+ /** A skill aposentada fica no repositório dele, nomeada - ver `RETIRED_SKILLS`. */
70
+ "retired - nothing of ours loads in your app any more": "aposentada - nada nosso carrega no seu app",
71
+ "it stays in your repo - yours to delete": "ela fica no seu repositório - sua para apagar",
69
72
  // ── onde o bloco de regras caiu ──
70
73
  "how to turn this repo into your system": "como transformar este repo no seu sistema",
71
74
  "rewritten for what is installed": "reescrito para o que está instalado",
@@ -437,7 +437,7 @@ export function describeFix(result, dry) {
437
437
  const lines = [];
438
438
  if (applied.length === 0) {
439
439
  lines.push(decisions > 0
440
- ? `Nothing to apply. All ${decisions} findings are values your system has no name for - those are design decisions, not fixes.`
440
+ ? `Nothing to apply. All ${decisions} findings are values your own code names nowhere - those are design decisions, not fixes. Name one and \`--fix\` picks it up.`
441
441
  : relative > 0
442
442
  ? `Nothing to apply. All ${relative} findings are lengths in \`em\`, which follow the element's font size - swapping them can move the layout, so that call is yours.`
443
443
  : /**
@@ -531,6 +531,6 @@ export function describeFix(result, dry) {
531
531
  const rewritten = new Set(applied.map((a) => `${a.file}:${a.line}`));
532
532
  const halfWritten = skipped.filter((s) => rewritten.has(`${s.file}:${s.line}`)).length;
533
533
  if (halfWritten > 0)
534
- lines.push(` ${halfWritten} of those ${halfWritten === 1 ? "sits" : "sit"} on a line this command just rewrote - a shorthand comes back with a token and a literal side by side (\`padding: 8px var(--ds-spacing-24)\`). See them by value: npx synthesisui doctor --migrate`);
534
+ lines.push(` ${halfWritten} of those ${halfWritten === 1 ? "sits" : "sit"} on a line this command just rewrote - a shorthand comes back with a token and a literal side by side (\`padding: 8px var(--spacing-24)\`). See them by value: npx synthesisui doctor --migrate`);
535
535
  return lines;
536
536
  }
@@ -12,14 +12,40 @@
12
12
  import { utilitiesOn } from "./idiom-names.js";
13
13
  import { normalizeValue, tokenMatch } from "./tokens.js";
14
14
  /**
15
- * O NOME QUE SE ESCREVE NO ARQUIVO DELE - e existe uma função só para isto por um motivo.
15
+ * O NOME QUE SE ESCREVE NO ARQUIVO DELE - e ele é SEMPRE do vocabulário dele (`INV-GERAL-13`).
16
16
  *
17
17
  * Seis lugares decidem o que a pessoa vê ou o que o `--fix` grava: o relatório, o plano, o hook, o
18
18
  * MCP, a aplicação e a contagem. Deixar cada um lembrar de preferir `theirToken` é o padrão do
19
19
  * argumento opcional que se esquece - e o sintoma seria o pior possível: o comando aconselhando um
20
20
  * nome e o hook aconselhando outro, sobre o mesmo arquivo.
21
+ *
22
+ * O NOSSO NOME SAIU DAQUI EM 11/09, e ele era a metade que contrariava a decisão de 07/09.
23
+ *
24
+ * A regra é literal: *"tem nome no sistema DELE? SIM -> a nossa receita usa o nome dele. NÃO ->
25
+ * mantém HARDCODED, e a plataforma só AVISA"*. Esta função devolvia `theirToken ?? token`, e o
26
+ * `token` é o nome COMPILADO do sistema - a grafia `--ds-*`, que é invenção nossa. Então o comando
27
+ * que promete adotar o sistema trocava um literal que funciona por uma variável que só resolve com
28
+ * uma folha nossa carregada no app dele.
29
+ *
30
+ * MEDIDO no `codelevel` em 04/09, sobre as 216 trocas com nome esperando: **60 (28%) escreviam a
31
+ * nossa grafia**. Eram exatamente as que exigiam o `@import` - e para protegê-las o comando recusava
32
+ * as outras 156 por inteiro, cobrando a folha em troca.
33
+ *
34
+ * O valor que só o nosso lado nomeia não desaparece do relatório: ele vira AVISO, com o valor e a
35
+ * linha, e a decisão de batizá-lo continua sendo dele. Ver `ourNameOnly`.
21
36
  */
22
- export const nameToWrite = (f) => f.theirToken ?? f.token;
37
+ export const nameToWrite = (f) => f.theirToken ?? (f.tokenIsTheirs ? f.token : null);
38
+ /**
39
+ * O VALOR QUE **SÓ** O NOSSO LADO NOMEIA - o que a plataforma AVISA em vez de escrever.
40
+ *
41
+ * É a outra metade de `nameToWrite`, e ela existe para o silêncio não acontecer: um achado sem nome
42
+ * dele e com nome nosso some das duas listas se ninguém perguntar por ele, e sumir é
43
+ * indistinguível de "está tudo nomeado" para quem lê (lei 8).
44
+ *
45
+ * Quem chama DIZ o valor e onde ele está - nunca propõe o nome. É a escolha dele de 10/09 entre as
46
+ * duas alternativas, e a mesma que o hook já segue.
47
+ */
48
+ export const ourNameOnly = (f) => !f.theirToken && !f.tokenIsTheirs && Boolean(f.token);
23
49
  /**
24
50
  * `next/og` renders JSX to a PNG on the server. There is no document, so there
25
51
  * is no `var(--ds-*)` to read: every colour in such a file MUST be a literal.
@@ -607,6 +633,8 @@ function scanCore(file, source, table) {
607
633
  * `--spacing` no vocabulário dele, e qual dos dois está certo depende de onde o literal está.
608
634
  */
609
635
  const theirs = table.aliases.get(`${kind}:${normalizeValue(literal, table.rootPx)}`);
636
+ /** A procedência do `token`, lida da tabela que o achou - ver `tokenIsTheirs`. */
637
+ const tableIsTheirs = table.source !== null && table.source !== "installed";
610
638
  findings.push({
611
639
  kind,
612
640
  line: at,
@@ -617,6 +645,9 @@ function scanCore(file, source, table) {
617
645
  ? { fontRelative: true }
618
646
  : {}),
619
647
  ...(theirs?.writable ? { theirToken: theirs.name } : {}),
648
+ ...(tableIsTheirs && match?.token
649
+ ? { tokenIsTheirs: true }
650
+ : {}),
620
651
  /**
621
652
  * O NOME QUE SÓ SE DIZ - `$gray_dark` do Sass, `@brand` do Less.
622
653
  *
@@ -196,8 +196,14 @@ export function theirNames(ours, theirs) {
196
196
  * 66 delas gravadas por um `--fix --write`, e a pior na PRIMEIRA página do relatório -
197
197
  * `0.25em → --radius-xs` em 116 arquivos. Ver `FAMILY_WORDS`.
198
198
  *
199
- * Recusar aqui não perde informação: sem alias, `nameToWrite` cai no NOSSO token, que é da
200
- * família certa porque foi o prefixo dela que o trouxe a este laço.
199
+ * E DESDE 11/09 RECUSAR AQUI SIGNIFICA SILÊNCIO, não um nome nosso no lugar.
200
+ *
201
+ * Esta linha dizia que recusar não perdia informação, *"porque sem alias `nameToWrite` cai no
202
+ * NOSSO token"*. Ele não cai mais: a nossa grafia deixou de ser escrita no código dele
203
+ * (`INV-GERAL-13`), então um valor cuja categoria não fecha sai da lista de trocas em vez de
204
+ * sair com o nosso nome. Isso é o desfecho certo - a troca errada era escrever um token de
205
+ * tipo num `border-radius` -, e o valor não desaparece: ele é DITO, com o valor e a linha,
206
+ * pelo caminho que `ourNameOnly` alimenta.
201
207
  */
202
208
  const usable = formDecides(value)
203
209
  ? candidates
package/dist/fonts.js CHANGED
@@ -89,7 +89,7 @@ export function googleFontsHref(families) {
89
89
  */
90
90
  export const FAMILY_SEAM_PREFIX = "--ds-typography-families-";
91
91
  export function nextFontSnippet(input) {
92
- const { families, slug, appDir = "app", seam, facts, declaredWeights, } = input;
92
+ const { families, appDir = "app", facts, declaredWeights } = input;
93
93
  /**
94
94
  * A ORDEM É DETERMINÍSTICA e os duplicados saem: isto vira uma linha de arquivo gerado, e um
95
95
  * conjunto que muda de ordem entre rodadas produz diff onde nada mudou.
@@ -219,15 +219,39 @@ export function nextFontSnippet(input) {
219
219
  * projeto dele deixaria de compilar por causa da linha que a gente mandou colar.
220
220
  */
221
221
  const emitted = roles.filter((role) => constOf(role) !== undefined);
222
+ /**
223
+ * O `data-ds` SAIU DO LAYOUT DELE EM 11/09 - `INV-GERAL-13`.
224
+ *
225
+ * O atributo existia para a NOSSA folha aplicar dentro dele, e nenhum app dele carrega a nossa
226
+ * folha. O que resta na linha é o que o `next/font` exige e é dele: a classe que carrega a
227
+ * variável de cada fonte.
228
+ */
222
229
  const layout = [
223
230
  `// ${appDir}/layout.tsx`,
224
231
  `import { ${[...new Set(emitted.map(constOf))].join(", ")} } from "./fonts";`,
225
- `<body data-ds="${slug}" className={\`${[...new Set(emitted.map((r) => `\${${constOf(r)}.variable}`))].join(" ")}\`}>`,
232
+ `<body className={\`${[...new Set(emitted.map((r) => `\${${constOf(r)}.variable}`))].join(" ")}\`}>`,
226
233
  ];
234
+ /**
235
+ * O BLOCO DE CSS PASSA A ESCREVER NO VOCABULÁRIO **DELE** - o conserto de 11/09.
236
+ *
237
+ * O QUE ELE ESCREVIA: um `[data-ds="<slug>"]` com `--ds-typography-families-<role>` apontando para
238
+ * a fonte. Nosso escopo, nossa variável, dentro da folha dele - e só resolvia com a nossa folha
239
+ * carregada, que é o que `INV-GERAL-13` proíbe.
240
+ *
241
+ * O QUE ELE ESCREVE AGORA: um `@theme` com `--font-<role>`, que é o namespace de fonte do Tailwind
242
+ * DELE. Duas coisas acontecem de uma vez, e as duas são dele: a utility `font-<role>` passa a
243
+ * existir no projeto dele carregando a fonte que o `next/font` baixou, e `theirThemeVars` -
244
+ * o leitor que decide o utilitário nomeado no codegen - passa a ver aquele nome. Daí em diante o
245
+ * componente que a plataforma escreve veste `font-<role>` em vez de um literal, porque o `@theme`
246
+ * DELE declara o nome.
247
+ *
248
+ * Nenhuma linha aqui menciona o sistema, o slug ou uma variável nossa: é a fonte dele, ligada ao
249
+ * Tailwind dele.
250
+ */
227
251
  const css = [
228
- `/* ${appDir}/globals.css - AFTER the tokens.css import */`,
229
- `[data-ds="${slug}"] {`,
230
- ...emitted.map((role) => ` ${seam?.[role] ?? `${FAMILY_SEAM_PREFIX}${role}`}: var(${roleVar(role)});`),
252
+ `/* ${appDir}/globals.css - in your own @theme */`,
253
+ `@theme {`,
254
+ ...emitted.map((role) => ` --font-${role}: var(${roleVar(role)});`),
231
255
  `}`,
232
256
  ];
233
257
  /**
@@ -1,134 +1,23 @@
1
- import { readdir, readFile } from "node:fs/promises";
2
- import { dirname, join, posix, relative, resolve } from "node:path";
1
+ import { readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
3
  import { exists } from "./agent-wiring.js";
4
4
  /**
5
- * A FOLHA GLOBAL QUE OS APPS DELE REALMENTE CARREGAM - e num monorepo ela quase nunca é a do app.
5
+ * AS PASTAS DE APP DESTE REPOSITÓRIO - e era um módulo sobre a folha global dele até 11/09.
6
6
  *
7
- * O QUE O CLIENTE PERCEBE SEM ISTO. O `add` manda importar os tokens em `apps/<x>/app/globals.css`.
8
- * MEDIDO em 01/09 no `codelevel`: os DOIS apps dele fazem `@import "@repo/ui/styles/globals.css"`, e
9
- * o `globals.css` do app diz, em comentário dele, *"Do not redeclare tokens or fonts here - extend in
10
- * styles/"*. A instrução apontava para o arquivo errado, e a convenção certa estava escrita ali do
11
- * lado.
7
+ * O QUE MORAVA AQUI: `globalSheetOf`, `sheetChainOf`, `prefixFrom` e o resolvedor de `exports` do
8
+ * workspace. Os quatro existiam para uma coisa - achar a folha que os apps dele realmente carregam,
9
+ * e contar o `../` até a raiz, para a instrução de `@import` apontar para o arquivo certo.
12
10
  *
13
- * Ele seguiu a instrução no arquivo CERTO - o do pacote - e o caminho relativo que a gente imprimiu
14
- * era o do app. Um monorepo com pacote de UI compartilhado não é exceção: é a forma comum, e a
15
- * instrução tem que derivar de onde o CSS dele mora em vez de assumir.
11
+ * Era a parte mais cuidadosa daquele texto. MEDIDO em 01/09 no `codelevel`: os dois apps dele fazem
12
+ * `@import "@repo/ui/styles/globals.css"`, e o `globals.css` do app diz, em comentário DELE, *"Do
13
+ * not redeclare tokens or fonts here"*. A instrução apontava para o arquivo errado, com o caminho
14
+ * relativo de outro, e o conserto foi seguir a cadeia em vez de assumir.
16
15
  *
17
- * ─────────────────────────────────────────────────────────────────────────
18
- * COMO A FOLHA REAL É ENCONTRADA, e nada aqui é palpite:
19
- *
20
- * 1. abre o `globals.css` do app
21
- * 2. procura `@import "<spec>"` que aponte para um CSS DENTRO deste repositório
22
- * 3. resolve o `<spec>`: caminho relativo, ou pacote do workspace pelo `exports`
23
- * 4. repete na folha encontrada, até ela não re-exportar mais
24
- *
25
- * O QUE NÃO É SEGUIDO: `tailwindcss` e qualquer coisa que não resolva para um arquivo daqui. Um
26
- * `@import "tailwindcss"` é a biblioteca, e mandar o cliente editá-la seria pior que a instrução que
27
- * isto conserta.
16
+ * O TEXTO INTEIRO DEIXOU DE EXISTIR (`INV-GERAL-13`): nada que a plataforma escreve no repositório
17
+ * dele depende de folha nossa, então não há `@import` para apontar. O que sobrevive é a única
18
+ * pergunta que não era sobre a nossa folha - *"quais pastas deste repositório são app?"* -, porque é
19
+ * onde o `fonts.ts` do `next/font` DELE entra, um por app.
28
20
  */
29
- /** Quantos saltos seguir antes de desistir - um ciclo de imports não pode travar um `add`. */
30
- const MAX_HOPS = 5;
31
- const IMPORT = /@import\s+["']([^"']+)["']/g;
32
- /** `@repo/ui/styles/globals.css` → `packages/ui/src/styles/globals.css`, pelo `exports` dele. */
33
- async function fromWorkspace(root, spec) {
34
- for (const group of ["packages", "apps", "libs"]) {
35
- let entries;
36
- try {
37
- const { readdir } = await import("node:fs/promises");
38
- entries = await readdir(join(root, group));
39
- }
40
- catch {
41
- continue;
42
- }
43
- for (const entry of entries) {
44
- const pkgPath = join(root, group, entry, "package.json");
45
- const raw = await readFile(pkgPath, "utf8").catch(() => null);
46
- if (!raw)
47
- continue;
48
- let pkg;
49
- try {
50
- pkg = JSON.parse(raw);
51
- }
52
- catch {
53
- continue;
54
- }
55
- if (!pkg.name || !spec.startsWith(`${pkg.name}/`))
56
- continue;
57
- const sub = `./${spec.slice(pkg.name.length + 1)}`;
58
- const target = pkg.exports?.[sub];
59
- if (typeof target !== "string")
60
- continue;
61
- return posix.join(group, entry, target.replace(/^\.\//, ""));
62
- }
63
- }
64
- return null;
65
- }
66
- /**
67
- * A folha onde os tokens dele devem entrar, relativa à raiz - `appSheet` quando nada a re-exporta.
68
- *
69
- * DELEGA, e não caminha: quem caminha é `sheetChainOf`. Dois caminhadores sobre a mesma cadeia é
70
- * como um deles para de seguir um salto que o outro segue, e ninguém descobre até um cliente
71
- * receber a instrução apontando para a folha errada - que é exatamente o defeito que
72
- * `INV-VOLTA-12` fechou.
73
- */
74
- export async function globalSheetOf(root, appSheet) {
75
- const chain = await sheetChainOf(root, appSheet);
76
- return chain[chain.length - 1] ?? appSheet;
77
- }
78
- /**
79
- * A CADEIA INTEIRA, da folha do app até a última que ninguém re-exporta - e por que ela é pública.
80
- *
81
- * O QUE O CLIENTE GANHA: a resposta "este app carrega o meu design system?" medida no app DELE, e
82
- * não no projeto. `readWiring` varria a raiz e devolvia "existe em algum lugar daqui": num monorepo
83
- * com dois apps servidos, um fiado e outro não, o fiado respondia pelo outro - e a pessoa recebia
84
- * "está tudo certo" sobre o app que não carrega nada.
85
- *
86
- * A pergunta dos IMPORTS não se responde varrendo pasta: ela se responde seguindo a cadeia de
87
- * `@import` a partir da folha daquele app, porque a folha que carrega os tokens quase nunca é a do
88
- * app - num monorepo é a do pacote compartilhado, e os dois apps chegam nela.
89
- *
90
- * A ordem é do app para fora, e o teto de saltos é o mesmo: um ciclo de imports não pode travar um
91
- * comando.
92
- */
93
- export async function sheetChainOf(root, appSheet) {
94
- const chain = [];
95
- let current = appSheet;
96
- for (let hop = 0; hop < MAX_HOPS; hop += 1) {
97
- chain.push(current);
98
- const raw = await readFile(join(root, current), "utf8").catch(() => null);
99
- if (!raw)
100
- return chain;
101
- let next = null;
102
- for (const m of raw.matchAll(IMPORT)) {
103
- const spec = m[1];
104
- if (!spec.endsWith(".css"))
105
- continue;
106
- const candidate = spec.startsWith(".")
107
- ? posix.normalize(posix.join(posix.dirname(current), spec))
108
- : await fromWorkspace(root, spec);
109
- if (!candidate)
110
- continue;
111
- /** Fora da raiz não é folha dele para editar - e um `..` demais sai do repositório. */
112
- if (candidate.startsWith(".."))
113
- continue;
114
- const readable = await readFile(join(root, candidate), "utf8").catch(() => null);
115
- if (readable === null)
116
- continue;
117
- next = candidate;
118
- break;
119
- }
120
- /** Uma folha que aponta para si mesma encerra a cadeia em vez de gastar o teto de saltos. */
121
- if (!next || next === current || chain.includes(next))
122
- return chain;
123
- current = next;
124
- }
125
- return chain;
126
- }
127
- /** O `../` que leva daquela folha até a raiz do repositório - o prefixo do `@import`. */
128
- export function prefixFrom(sheet) {
129
- const up = relative(dirname(resolve("/r", sheet)), "/r");
130
- return up === "" ? "./" : `${up.split(/[\\/]/).join("/")}/`;
131
- }
132
21
  /**
133
22
  * A RAIZ DE CADA APP DESTE REPOSITÓRIO - onde o escopo e a tipografia entram, um por app.
134
23
  *