synthesisui 0.16.135 → 0.16.136

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.
@@ -13,7 +13,7 @@ import { describeGate, gateComponent, groupSkips, SCREENS_FOR_SYSTEM, } from "..
13
13
  import { emptyTally, internalSpecifiers, scanComponentsInto, tallyToInventory, } from "../doctor/components-scan.js";
14
14
  import { countShape, describeCoverage, summarizeCoverage, } from "../doctor/coverage.js";
15
15
  import { crosswalk, floorSize, isLibrary, observedRules, useLiveCatalogue, } from "../doctor/crosswalk.js";
16
- import { keyframeOffsets, moduleImports, readModuleCss, readModuleUsage, transcribeModule, } from "../doctor/css-modules.js";
16
+ import { keyframeOffsets, moduleImports, partialCandidates, readModuleCss, readModuleUsage, sheetImports, transcribeModule, } from "../doctor/css-modules.js";
17
17
  import { dataContract } from "../doctor/data-contract.js";
18
18
  import { reconcile, scanDefinitions, } from "../doctor/definitions-scan.js";
19
19
  import { fragmentsOfSource, fragmentsOfStylesheet, judgeFragments, } from "../doctor/fragments.js";
@@ -128,6 +128,48 @@ async function siblingProjects(root) {
128
128
  * this" - could not be said, and we ended up rediscovering by colour distance
129
129
  * what their stylesheet had spelled out all along.
130
130
  */
131
+ /**
132
+ * UMA FOLHA E TUDO O QUE ELA TRAZ PARA DENTRO DE SI, na ordem em que o sass compila.
133
+ *
134
+ * Uma partial (`_darkMode.scss`) nunca é compilada sozinha: ela entra em quem a importa, e portanto
135
+ * herda o escopo de quem a importa. Ler o `.module.scss` sem seguir os `@import` dele deixava de
136
+ * fora 1 400 declarações (91% de tudo o que a esteira chamava de "CSS global sem leitor") - e elas
137
+ * são, no build dele, as faces light e dark de classes que o componente JÁ veste (medido, 05/08).
138
+ *
139
+ * Devolve o CSS concatenado e os arquivos que entraram, porque o ledger precisa saber que a partial
140
+ * pertence a um componente admitido - senão ela continua contabilizada como forma sem leitor, e o
141
+ * relatório passa a discordar da receita.
142
+ */
143
+ async function sheetWithImports(path, seen = new Set(), depth = 0) {
144
+ if (depth > 6 || seen.has(path))
145
+ return { css: "", files: [] };
146
+ seen.add(path);
147
+ const body = await readFile(path, "utf8").catch(() => "");
148
+ if (!body)
149
+ return { css: "", files: [] };
150
+ let css = "";
151
+ const files = [path];
152
+ /**
153
+ * O IMPORTADO VEM ANTES, que é a ordem do sass: quem importa sobrescreve o que importou. Inverter
154
+ * isto faria a face dark de uma partial vencer a base do próprio module.
155
+ */
156
+ for (const spec of sheetImports(body)) {
157
+ for (const candidate of partialCandidates(spec)) {
158
+ const child = join(dirname(path), candidate);
159
+ const nested = await sheetWithImports(child, seen, depth + 1);
160
+ if (nested.css || nested.files.length > 1 || (await exists(child))) {
161
+ css += nested.css;
162
+ files.push(...nested.files);
163
+ break;
164
+ }
165
+ }
166
+ }
167
+ return { css: `${css}\n${body}`, files };
168
+ }
169
+ /** Existe no disco - o candidato de partial que o sass acharia primeiro. */
170
+ async function exists(path) {
171
+ return await readFile(path, "utf8").then(() => true, () => false);
172
+ }
131
173
  async function harvestOwnCss(root,
132
174
  /**
133
175
  * O LEDGER TAMBÉM COME AQUI, e por um motivo de promessa: este é o único laço que vê TODO
@@ -139,17 +181,40 @@ async function harvestOwnCss(root,
139
181
  * não admitido é um motivo, não um desaparecimento.
140
182
  */
141
183
  into) {
142
- let css = "";
184
+ /**
185
+ * DOIS PASSES, porque o escopo de uma folha está no GRAFO e não no nome dela.
186
+ *
187
+ * O primeiro junta tudo; o segundo julga cada folha sabendo quem importa quem. Um pass só
188
+ * decidia por `/\.module\./` e chamava de "CSS global sem leitor" 1 400 declarações que o build
189
+ * deles compila dentro de um module (medido, 05/08).
190
+ */
191
+ const sheets = [];
143
192
  for await (const file of walkAll([root])) {
144
193
  if (!/\.(css|scss|sass|less)$/i.test(file))
145
194
  continue;
146
195
  const body = await readFile(file, "utf8").catch(() => "");
196
+ sheets.push({ file, body });
197
+ }
198
+ /** Todo caminho que alguma folha importa, resolvido como o sass resolveria. */
199
+ const imported = new Set();
200
+ for (const sheet of sheets)
201
+ for (const spec of sheetImports(sheet.body))
202
+ for (const candidate of partialCandidates(spec))
203
+ imported.add(join(dirname(sheet.file), candidate));
204
+ let css = "";
205
+ for (const { file, body } of sheets) {
147
206
  css += `\n${body}`;
148
- if (into && body)
149
- into.push({
150
- file: relative(root, file),
151
- fragments: fragmentsOfStylesheet(relative(root, file), body, /\.module\./.test(file)),
152
- });
207
+ if (!into || !body)
208
+ continue;
209
+ const partial = basename(file).startsWith("_");
210
+ into.push({
211
+ file: relative(root, file),
212
+ fragments: fragmentsOfStylesheet(relative(root, file), body, /\.module\./.test(file) || (partial && imported.has(file))
213
+ ? "scoped"
214
+ : partial
215
+ ? "dead"
216
+ : "global"),
217
+ });
153
218
  }
154
219
  return css;
155
220
  }
@@ -564,7 +629,13 @@ export async function takeCensus(root, opts) {
564
629
  let fromModule = null;
565
630
  for (const mod of modules) {
566
631
  const cssPath = join(dirname(file), mod.specifier);
567
- const moduleCss = await readFile(cssPath, "utf8").catch(() => "");
632
+ /**
633
+ * O MODULE E AS PARTIALS QUE ELE IMPORTA, que no build dele são um arquivo só. Ler apenas
634
+ * o `.module.scss` deixava 1 400 declarações fora - as faces light e dark de classes que
635
+ * o componente já veste, escritas em `_lightMode.scss`/`_darkMode.scss` ao lado.
636
+ */
637
+ const sheet = await sheetWithImports(cssPath);
638
+ const moduleCss = sheet.css.trim();
568
639
  if (!moduleCss)
569
640
  continue;
570
641
  const usage = readModuleUsage(src, mod.local);
@@ -574,8 +645,14 @@ export async function takeCensus(root, opts) {
574
645
  * ESTE MODULE PERTENCE A UM COMPONENTE ADMITIDO - o único fato que este laço tem e o
575
646
  * ledger precisa. Os fragmentos dele vêm do `harvestOwnCss`, que vê todos os arquivos
576
647
  * de estilo e não só os de componente aceito.
648
+ *
649
+ * A PARTIAL ENTRA JUNTO: ela é parte deste module, então o veredito dela é o mesmo. Sem
650
+ * isto o relatório continuaria chamando de "forma sem leitor" exatamente o que a receita
651
+ * acabou de ler - relatório e receita discordando, que é o defeito que o ledger existe
652
+ * para não ter.
577
653
  */
578
- admittedStyles.add(relative(root, cssPath));
654
+ for (const part of sheet.files)
655
+ admittedStyles.add(relative(root, part));
579
656
  const readModule = transcribeModule(readModuleCss(moduleCss), usage, declaredValues);
580
657
  if (readModule.read === 0)
581
658
  continue;
@@ -335,6 +335,50 @@ export function moduleImports(source) {
335
335
  specifier: m[2],
336
336
  }));
337
337
  }
338
+ /**
339
+ * `@import "./_darkMode"`, `@use "./_lightMode.scss"` - o que uma folha traz para dentro de si.
340
+ *
341
+ * UMA PARTIAL NÃO É UMA FOLHA. Um arquivo `_nome.scss` nunca é compilado sozinho - o sass o
342
+ * compila DENTRO de quem o importa, e por isso o escopo dele é o de quem importa. No app do dono,
343
+ * 122 das 138 folhas "globais" são partials e 120 delas são importadas por um `.module.scss` do
344
+ * mesmo diretório: 1 400 declarações (91%) que a esteira classificava como CSS global sem leitor
345
+ * são, no build dele, regras de module - hasheadas e escopadas ao componente (medido, 05/08).
346
+ *
347
+ * Decidir escopo por NOME DE ARQUIVO (`/\.module\./`) é a lei 14 um nível abaixo: a resposta está
348
+ * no grafo de imports, não na string do caminho.
349
+ */
350
+ const SHEET_IMPORT = /@(?:import|use|forward)\s+["']([^"']+)["']/g;
351
+ export function sheetImports(css) {
352
+ SHEET_IMPORT.lastIndex = 0;
353
+ return ([...stripCssComments(css).matchAll(SHEET_IMPORT)]
354
+ .map((m) => m[1].trim())
355
+ /**
356
+ * Só o que é relativo. `@use "sass:math"` e um pacote do node_modules não carregam decisão de
357
+ * aparência deles, e seguir esses caminhos é como um leitor de CSS vira um resolvedor de
358
+ * módulos pela metade.
359
+ */
360
+ .filter((spec) => spec.startsWith(".") || !spec.includes(":"))
361
+ .filter((spec) => !spec.startsWith("~") && !spec.startsWith("url(")));
362
+ }
363
+ /** `./_darkMode` → os nomes de arquivo que o sass tentaria, na ordem em que ele tenta. */
364
+ export function partialCandidates(specifier) {
365
+ const clean = specifier.replace(/^\.\//, "");
366
+ const slash = clean.lastIndexOf("/");
367
+ const dir = slash === -1 ? "" : `${clean.slice(0, slash)}/`;
368
+ const base = slash === -1 ? clean : clean.slice(slash + 1);
369
+ if (/\.(css|scss|sass|less)$/.test(base))
370
+ return base.startsWith("_")
371
+ ? [`${dir}${base}`]
372
+ : [`${dir}${base}`, `${dir}_${base}`];
373
+ const stem = base.startsWith("_") ? base : `_${base}`;
374
+ return [
375
+ `${dir}${base}.scss`,
376
+ `${dir}${stem}.scss`,
377
+ `${dir}${base}.sass`,
378
+ `${dir}${stem}.sass`,
379
+ `${dir}${base}.css`,
380
+ ];
381
+ }
338
382
  /**
339
383
  * A generic in type position, not an element.
340
384
  *
@@ -110,7 +110,20 @@ export function fragmentsOfSource(file, source) {
110
110
  * contagem: um global com regra de CLASSE é forma que nenhum leitor cobre hoje - 399 declarações
111
111
  * no repo dele, e a única lacuna que estava calada em vez de declarada (04/08).
112
112
  */
113
- export function fragmentsOfStylesheet(file, css, scoped) {
113
+ export function fragmentsOfStylesheet(file, css,
114
+ /**
115
+ * COMO ESTA FOLHA CHEGA NO BUILD DELES, e isso vem do GRAFO DE IMPORTS e nunca do nome do
116
+ * arquivo. Uma partial (`_darkMode.scss`) é compilada dentro de quem a importa e herda o escopo
117
+ * dele: 122 das 138 folhas "globais" do app dele são partials, e 120 entram num `.module.scss`
118
+ * ao lado (medido, 05/08).
119
+ *
120
+ * scoped module, ou partial de um module: o veredito é do componente dono, e
121
+ * `judgeFragments` o dá olhando se o escopo o admitiu
122
+ * global uma folha que o código importa: forma que nenhum leitor cobre hoje
123
+ * dead partial que ninguém importa: o build deles não a compila, e ela não
124
+ * pinta nada - aqui, nem lá
125
+ */
126
+ kind) {
114
127
  const out = [];
115
128
  let selector = "";
116
129
  for (const [i, raw] of css.split("\n").entries()) {
@@ -143,7 +156,11 @@ export function fragmentsOfStylesheet(file, css, scoped) {
143
156
  line: i + 1,
144
157
  text: `${selector ? `${selector} ` : ""}{ ${m[1]}: ${m[2].trim()} }`,
145
158
  read: false,
146
- ...(scoped ? {} : { reason: "shape-not-read" }),
159
+ ...(kind === "global"
160
+ ? { reason: "shape-not-read" }
161
+ : kind === "dead"
162
+ ? { reason: "sheet-not-imported" }
163
+ : {}),
147
164
  });
148
165
  }
149
166
  }
@@ -84,7 +84,14 @@ function whereOf(shape, reason) {
84
84
  module: null,
85
85
  work: reason === "component-not-admitted"
86
86
  ? "the gate refused the component: these styles exist and no recipe claims them. Changing that means moving the GATE - a decision about the vitrine, not a reader to write"
87
- : "the value is assembled at render time and is not in the source at any level - there is nothing to read",
87
+ : reason === "sheet-not-imported"
88
+ ? /**
89
+ * NÃO É LACUNA NOSSA NEM DELES-QUE-A-GENTE-CONSERTA: é um arquivo que o build deles
90
+ * não compila. Cair na frase de runtime dizia que o valor é montado em tempo de
91
+ * execução, o que é falso e mandaria alguém procurar o que não existe.
92
+ */
93
+ "a sass partial no stylesheet imports: their own build never compiles it, so it paints nothing today - here or in their app. Deleting it is their call, not a reader to write"
94
+ : "the value is assembled at render time and is not in the source at any level - there is nothing to read",
88
95
  };
89
96
  }
90
97
  }
@@ -54,6 +54,7 @@ const BECAUSE = {
54
54
  "value-not-read": "the shape is read and this value is not - a utility or property outside the scales this project declares",
55
55
  computed: "the value is assembled at render time, so it is not in the source at any level and there is nothing to read",
56
56
  "component-not-admitted": "read, and it belongs to a component this scope does not admit into the system - the styles exist and no recipe claims them",
57
+ "sheet-not-imported": "this is a sass partial and no stylesheet imports it, so your own build never compiles it - it paints nothing today, here or anywhere",
57
58
  };
58
59
  /**
59
60
  * O ledger de uma varredura, a partir dos fragmentos vistos.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.135",
3
+ "version": "0.16.136",
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": {