synthesisui 0.16.198 → 0.16.199

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.
@@ -2,14 +2,146 @@ import { readdir, readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  /**
4
4
  * Opens a JSX element whose name is capitalised - which is React's own rule for
5
- * "this is a component, not an html tag".
5
+ * "this is a component, not an html tag". The name allows dots (`Card.Header`).
6
6
  *
7
- * The name allows dots (`Card.Header`) and the body is captured lazily up to
8
- * the first `>` that is not inside a quoted value.
7
+ * SÓ A ABERTURA. O corpo NÃO vem daqui - ver `bodyOf`.
9
8
  */
10
- const ELEMENT = /<([A-Z][A-Za-z0-9_]*(?:\.[A-Z][A-Za-z0-9_]*)*)(\s[^>]*?)?\/?>/gs;
11
- /** `variant="primary"` or `variant={"primary"}` - a literal a person typed. */
12
- const LITERAL_PROP = /([a-zA-Z][a-zA-Z0-9_-]*)\s*=\s*\{?\s*["']([^"']*)["']\s*\}?/g;
9
+ const OPENS = /<([A-Z][A-Za-z0-9_]*(?:\.[A-Z][A-Za-z0-9_]*)*)/g;
10
+ /**
11
+ * ONDE A TAG TERMINA DE VERDADE - e isto era uma varredura de caractere.
12
+ *
13
+ * O corpo era `(\s[^>]*?)?`: para no primeiro `>` que aparecer, sem saber o que é aspa e
14
+ * sem saber o que é aninhamento. Medido no repositório do dono em 10/08, isso produzia
15
+ * DOIS defeitos que pareciam não ter relação:
16
+ *
17
+ * <WidgetCard rightTitleSlot={ <Button variant="neutral" onClick={() => …
18
+ * os atributos do FILHO caem no corpo do PAI - o censo registrou `variant: neutral`
19
+ * no WidgetCard, que declara `default | left-border | full-border`. A tela reportou
20
+ * "uso fora do contrato" sobre um uso que nunca existiu.
21
+ *
22
+ * value="… Domain 'sg.futurescope.com' … the 'Sending' tab …"
23
+ * a aspa simples dentro da dupla fechava o valor cedo, o resto da frase ficava no
24
+ * corpo, e o passe de booleano nu colhia palavra por palavra: `the`, `following`,
25
+ * `registrar`, `domains`. Dezenove palavras viraram props do Textarea.
26
+ *
27
+ * Um defeito INVENTADO é pior que um não achado: o não achado custa uma lacuna, o
28
+ * inventado custa a confiança na tela inteira. Por isso a resposta não é acrescentar
29
+ * casos à regex - é um scanner que sabe o que está lendo, e que fecha por construção.
30
+ *
31
+ * Devolve o corpo e onde continuar a varredura. Uma tag sem fechamento (arquivo cortado,
32
+ * TypeScript genérico que não era elemento) devolve `null` e não vira uso.
33
+ */
34
+ function bodyOf(source, from) {
35
+ let quote = null;
36
+ let depth = 0;
37
+ for (let i = from; i < source.length; i++) {
38
+ const ch = source[i];
39
+ if (quote) {
40
+ /** Escape dentro de string: `\"` não fecha nada. */
41
+ if (ch === "\\")
42
+ i += 1;
43
+ else if (ch === quote)
44
+ quote = null;
45
+ continue;
46
+ }
47
+ if (ch === '"' || ch === "'" || ch === "`") {
48
+ quote = ch;
49
+ continue;
50
+ }
51
+ if (ch === "{")
52
+ depth += 1;
53
+ else if (ch === "}")
54
+ depth -= 1;
55
+ /** Em profundidade zero e fora de aspas: aqui a tag realmente fecha. */ else if (ch === ">" &&
56
+ depth <= 0)
57
+ return {
58
+ body: withoutExpressions(source.slice(from, i)).replace(/\/$/, ""),
59
+ end: i + 1,
60
+ };
61
+ }
62
+ return null;
63
+ }
64
+ /**
65
+ * O QUE ESTÁ DENTRO DE CHAVES NÃO É PROP DESTA TAG - e sem isto o corpo termina no lugar certo
66
+ * e ainda entrega o filho errado.
67
+ *
68
+ * `<WidgetCard rightTitleSlot={<Button variant="neutral" />} />` fecha corretamente no `/>` do
69
+ * WidgetCard: o `>` do Button está em profundidade 1. Mas o corpo continua carregando
70
+ * `variant="neutral"`, porque a atribuição do filho mora dentro da expressão do slot.
71
+ *
72
+ * Então a expressão vira espaço - e o que ela contém não se perde: o `<Button` dentro dela é
73
+ * encontrado pela própria varredura, no lugar dele, com as props dele.
74
+ *
75
+ * COM UMA EXCEÇÃO, E ELA É MEDIDA: `variant={"info"}` é o MESMO literal que `variant="info"`, e um
76
+ * spec já dizia isso. A primeira versão deste passe apagava os dois e a suíte reprovou - eu tinha
77
+ * escrito no comentário que o `LITERAL_PROP` via o corpo cru antes, o que o código não fazia. Uma
78
+ * chave contendo só um literal volta como valor; qualquer outra coisa dentro é expressão.
79
+ */
80
+ function withoutExpressions(body) {
81
+ let out = "";
82
+ let inner = "";
83
+ let depth = 0;
84
+ let quote = null;
85
+ /** Fecha uma expressão: um literal puro é VALOR e volta; o resto é do que estiver dentro. */
86
+ const close = () => {
87
+ out += /^\s*(["'`])(?:\\.|(?!\1)[^\\])*\1\s*$/.test(inner)
88
+ ? `{${inner}}`
89
+ : " ";
90
+ inner = "";
91
+ };
92
+ for (let i = 0; i < body.length; i++) {
93
+ const ch = body[i];
94
+ const keep = (text) => {
95
+ if (depth === 0)
96
+ out += text;
97
+ else
98
+ inner += text;
99
+ };
100
+ if (quote) {
101
+ if (ch === "\\") {
102
+ keep(body.slice(i, i + 2));
103
+ i += 1;
104
+ }
105
+ else {
106
+ keep(ch);
107
+ if (ch === quote)
108
+ quote = null;
109
+ }
110
+ continue;
111
+ }
112
+ if (ch === '"' || ch === "'" || ch === "`") {
113
+ quote = ch;
114
+ keep(ch);
115
+ continue;
116
+ }
117
+ if (ch === "{") {
118
+ depth += 1;
119
+ if (depth > 1)
120
+ inner += ch;
121
+ continue;
122
+ }
123
+ if (ch === "}") {
124
+ depth -= 1;
125
+ if (depth === 0)
126
+ close();
127
+ else if (depth > 0)
128
+ inner += ch;
129
+ else
130
+ depth = 0;
131
+ continue;
132
+ }
133
+ keep(ch);
134
+ }
135
+ return out;
136
+ }
137
+ /**
138
+ * `variant="primary"` ou `variant={'primary'}` - um literal que alguém digitou.
139
+ *
140
+ * A ASPA CASA COM ELA MESMA (`(["'])…\2`), e não com qualquer uma. `["']([^"']*)["']`
141
+ * tratava as duas como intercambiáveis, então um valor com apóstrofo dentro truncava - e o
142
+ * que sobrava virava prop. Ver `bodyOf`.
143
+ */
144
+ const LITERAL_PROP = /([a-zA-Z][a-zA-Z0-9_-]*)\s*=\s*\{?\s*(["'])((?:\\.|(?!\2)[^\\])*)\2\s*\}?/g;
13
145
  /** `dense` with no value is a boolean prop set to true, and that IS a literal
14
146
  * decision worth counting. */
15
147
  const BARE_PROP = /(^|\s)([a-z][a-zA-Z0-9_]*)(?=\s|$)/g;
@@ -171,8 +303,12 @@ internal = []) {
171
303
  return;
172
304
  const owners = importedNames(source, internal);
173
305
  const private_ = localOnlyNames(source);
174
- ELEMENT.lastIndex = 0;
175
- for (const m of source.matchAll(ELEMENT)) {
306
+ /**
307
+ * Abertura por regex, corpo por SCANNER - ver `bodyOf`. A regex só encontra `<Nome`; onde a
308
+ * tag termina é uma pergunta que só um scanner com estado responde.
309
+ */
310
+ OPENS.lastIndex = 0;
311
+ for (const m of source.matchAll(OPENS)) {
176
312
  if (m.index != null && looksLikeType(source, m.index))
177
313
  continue;
178
314
  const name = m[1];
@@ -180,11 +316,25 @@ internal = []) {
180
316
  // shadow nothing and still bind a name it also imports.
181
317
  if (!owners.has(name) && private_.has(name.split(".")[0]))
182
318
  continue;
183
- const body = m[2] ?? "";
319
+ const read = bodyOf(source, (m.index ?? 0) + m[0].length);
320
+ /** Tag sem fechamento não é uso: arquivo cortado, ou um genérico que não era elemento. */
321
+ if (!read)
322
+ continue;
323
+ const body = read.body;
184
324
  // A dotted name belongs to whoever exported its root (`Popover.Root`).
185
325
  const owner = owners.get(name) ?? owners.get(name.split(".")[0]);
186
- /** Ver `ComponentTally`: dois `Button` de pacotes diferentes são duas linhas, não uma. */
187
- let hit = tally.get(name);
326
+ /**
327
+ * A ORIGEM ENTRA NA CHAVE - a lei 13 do CLAUDE.md, que este comentário já afirmava e o código
328
+ * não cumpria: a chave era só o nome.
329
+ *
330
+ * Medido no repositório do dono em 10/08: os apps usam o `Button` DELE e o do MUI, e as
331
+ * variantes do MUI (`contained`, `outlined`, `text`) caíam na conta do Button dele, que declara
332
+ * `ocean`, `royal`, `neutral`. O mesmo com `CircularProgress` e `determinate`. E era a mesma
333
+ * causa do `own: true` valer em 7 de 128: uma linha que mistura duas origens nunca tem TODAS
334
+ * as origens de dentro.
335
+ */
336
+ const key = `${name}\u0000${owner ?? ""}`;
337
+ let hit = tally.get(key);
188
338
  if (!hit) {
189
339
  hit = {
190
340
  count: 0,
@@ -194,7 +344,7 @@ internal = []) {
194
344
  ...(owner && owner !== OWN ? { from: owner } : {}),
195
345
  origins: new Set(),
196
346
  };
197
- tally.set(name, hit);
347
+ tally.set(key, hit);
198
348
  }
199
349
  /** TODA origem vista, e não só a primeira - ver `origins` no tipo. */
200
350
  if (owner)
@@ -204,7 +354,8 @@ internal = []) {
204
354
  LITERAL_PROP.lastIndex = 0;
205
355
  const named = new Set();
206
356
  for (const p of body.matchAll(LITERAL_PROP)) {
207
- const [, prop, value] = p;
357
+ /** O grupo 2 é a ASPA (a backreference); o valor é o 3 - ver `LITERAL_PROP`. */
358
+ const [, prop, , value] = p;
208
359
  named.add(prop);
209
360
  if (NOT_A_DECISION.test(prop))
210
361
  continue;
@@ -239,8 +390,9 @@ internal = []) {
239
390
  /** The inventory, commonest first. */
240
391
  export function tallyToInventory(tally, max = 80) {
241
392
  return [...tally.entries()]
242
- .map(([name, v]) => ({
243
- name,
393
+ .map(([key, v]) => ({
394
+ /** A chave carrega a origem para separar as linhas - o nome é a primeira metade dela. */
395
+ name: key.split("\u0000")[0],
244
396
  ...(v.from ? { from: v.from } : {}),
245
397
  /** Só dele quando TODA aparição veio de dentro - ver `origins` e `OWN`. */
246
398
  ...(v.origins.size > 0 && [...v.origins].every((o) => o === OWN)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.198",
3
+ "version": "0.16.199",
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": {