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.
- package/dist/doctor/components-scan.js +167 -15
- package/package.json +1 -1
|
@@ -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
|
-
*
|
|
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
|
|
11
|
-
/**
|
|
12
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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
|
|
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
|
-
/**
|
|
187
|
-
|
|
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(
|
|
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
|
-
|
|
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(([
|
|
243
|
-
|
|
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