synthesisui 0.16.274 → 0.16.276
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/anatomy-read.js +10 -1
- package/dist/attr-shape.js +150 -0
- package/dist/commands/import.js +37 -5
- package/dist/component-codegen.js +56 -5
- package/dist/doctor/sketch.js +36 -4
- package/dist/doctor/transcribe.js +72 -4
- package/dist/install-marks.js +1 -1
- package/dist/populations.spec-input.js +53 -0
- package/package.json +1 -1
package/dist/anatomy-read.js
CHANGED
|
@@ -211,8 +211,17 @@ function measuredAt(node, sketch) {
|
|
|
211
211
|
if (measured?.behaviourProps &&
|
|
212
212
|
Object.keys(measured.behaviourProps).length > 0)
|
|
213
213
|
out.behaviourProps = measured.behaviourProps;
|
|
214
|
-
if (measured?.passes && Object.keys(measured.passes).length > 0)
|
|
214
|
+
if (measured?.passes && Object.keys(measured.passes).length > 0) {
|
|
215
215
|
out.passes = measured.passes;
|
|
216
|
+
/**
|
|
217
|
+
* A FORMA VIAJA COM O VALOR, SEMPRE - inclusive vazia.
|
|
218
|
+
*
|
|
219
|
+
* Um `passes` sem `passesExpr` obriga quem emite a adivinhar entre aspas e chaves, e as duas
|
|
220
|
+
* adivinhações produzem código errado no arquivo dele. A lista vazia responde "nenhuma é
|
|
221
|
+
* expressão"; a AUSÊNCIA responde "este censo é velho", e são coisas diferentes.
|
|
222
|
+
*/
|
|
223
|
+
out.passesExpr = measured.passesExpr ?? [];
|
|
224
|
+
}
|
|
216
225
|
if (measured?.repeats)
|
|
217
226
|
out.repeats = measured.repeats;
|
|
218
227
|
/**
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* O QUE UM ATRIBUTO É, pela FORMA da chave e pela ORIGEM da tag - nunca por uma lista de nomes.
|
|
3
|
+
*
|
|
4
|
+
* O QUÊ. `passes` carrega tudo que um elemento recebe, e "tudo" são quatro coisas com destinos
|
|
5
|
+
* opostos: a acessibilidade que ele escreveu (`role`, `aria-valuenow`), o atributo específico do
|
|
6
|
+
* elemento (`type`, `viewBox`), o handler (`onMouseMove`) e a prop de OUTRO componente
|
|
7
|
+
* (`variant="premium"`). Emitir as quatro igual entrega código que não compila; calar as quatro
|
|
8
|
+
* entrega uma `<div>` muda.
|
|
9
|
+
*
|
|
10
|
+
* POR QUÊ IMPORTA. Um agente que reconstrói o componente a partir da receita escreve o que a receita
|
|
11
|
+
* disser. Medido nos dois clientes: `role` em 9 raízes de um e em 4 do outro, `aria-*` em 46 e 23
|
|
12
|
+
* chaves. Sem esta classificação a barra de progresso dele chega sem `role="progressbar"` e sem
|
|
13
|
+
* `aria-valuenow` - a lei 10 proíbe acessibilidade SUPOSTA, e calar a que ele ESCREVEU é o inverso.
|
|
14
|
+
*
|
|
15
|
+
* COMO. Duas perguntas, e nenhuma delas é "como se chama?":
|
|
16
|
+
*
|
|
17
|
+
* tem prefixo (`aria-`, `data-`, `xlink:`) -> global, válido em QUALQUER elemento
|
|
18
|
+
* a tag é minúscula -> é elemento HTML/SVG: tudo nele é atributo
|
|
19
|
+
* a tag é Capitalizada -> é componente: tudo nele é prop dele
|
|
20
|
+
*
|
|
21
|
+
* A lista `GLOBAL_ATTRS` é o único literal, e ela é um fato da especificação do HTML - esses
|
|
22
|
+
* atributos valem em todo elemento -, não um palpite sobre o código de ninguém. Medida em duas
|
|
23
|
+
* populações: cobre 100% dos casos de acessibilidade de raiz das duas.
|
|
24
|
+
*
|
|
25
|
+
* GÊMEO BYTE-IDÊNTICO de `packages/cli/src/attr-shape.ts`, provado por spec. O CLI é publicado
|
|
26
|
+
* standalone e não pode importar os contratos.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* OS ATRIBUTOS QUE VALEM EM QUALQUER ELEMENTO, pela especificação do HTML.
|
|
30
|
+
*
|
|
31
|
+
* Não é vocabulário do cliente e não é heurística: `role` e `tabindex` são globais, e um
|
|
32
|
+
* `aria-*` qualquer também. Por isso eles podem ser escritos no elemento que a gente emite mesmo
|
|
33
|
+
* quando a tag emitida difere da que ele escreveu - o que `element` não pode.
|
|
34
|
+
*/
|
|
35
|
+
const GLOBAL_ATTRS = new Set([
|
|
36
|
+
"role",
|
|
37
|
+
"title",
|
|
38
|
+
"id",
|
|
39
|
+
"lang",
|
|
40
|
+
"dir",
|
|
41
|
+
"tabIndex",
|
|
42
|
+
"tabindex",
|
|
43
|
+
"hidden",
|
|
44
|
+
"slot",
|
|
45
|
+
"translate",
|
|
46
|
+
"inert",
|
|
47
|
+
"popover",
|
|
48
|
+
"autoFocus",
|
|
49
|
+
"autofocus",
|
|
50
|
+
"spellCheck",
|
|
51
|
+
"spellcheck",
|
|
52
|
+
"contentEditable",
|
|
53
|
+
"contenteditable",
|
|
54
|
+
"draggable",
|
|
55
|
+
"accessKey",
|
|
56
|
+
"accesskey",
|
|
57
|
+
]);
|
|
58
|
+
/**
|
|
59
|
+
* A CLASSE DE UMA CHAVE, dada a tag que a carrega.
|
|
60
|
+
*
|
|
61
|
+
* `tag` ausente significa que a origem não é conhecida, e aí a resposta conservadora é
|
|
62
|
+
* `composition`: guardar não quebra nada, emitir pode.
|
|
63
|
+
*/
|
|
64
|
+
export function attrKind(key, tag) {
|
|
65
|
+
if (/^on[A-Z]/.test(key))
|
|
66
|
+
return "handler";
|
|
67
|
+
/**
|
|
68
|
+
* O PREFIXO É A PROVA. `aria-`, `data-` e `xlink:` são espaços de nome do próprio HTML, então uma
|
|
69
|
+
* chave com um deles é atributo em qualquer elemento - e é exatamente a chave que o coletor
|
|
70
|
+
* destruía até 22/08, quando `\w` engolia o hífen e `aria-hidden` virava `hidden`.
|
|
71
|
+
*/
|
|
72
|
+
if (/^(aria|data)-/.test(key) || key.includes(":"))
|
|
73
|
+
return "global";
|
|
74
|
+
if (GLOBAL_ATTRS.has(key))
|
|
75
|
+
return "global";
|
|
76
|
+
const spelled = String(tag ?? "").trim();
|
|
77
|
+
if (!spelled)
|
|
78
|
+
return "composition";
|
|
79
|
+
/**
|
|
80
|
+
* A ORIGEM DECIDE, NÃO O NOME - a lei 13, um nível abaixo. Uma tag minúscula é um elemento do
|
|
81
|
+
* HTML ou do SVG: tudo que ela recebe é atributo, por construção. Uma tag Capitalizada é um
|
|
82
|
+
* componente, e `variant` nela é a API dele, não marcação.
|
|
83
|
+
*/
|
|
84
|
+
return /^[a-z]/.test(spelled) ? "element" : "composition";
|
|
85
|
+
}
|
|
86
|
+
/** O texto JSX de um atributo, com a forma que a leitura mediu - ver `passesExpr`. */
|
|
87
|
+
export function attrText(key, value, isExpression) {
|
|
88
|
+
return isExpression ? ` ${key}={${value}}` : ` ${key}="${value}"`;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* OS ATRIBUTOS QUE PODEM SER ESCRITOS, e os que ficam guardados - numa decisão só.
|
|
92
|
+
*
|
|
93
|
+
* A função recebe o NÓ inteiro de propósito. A versão que recebia `passes` e `passesExpr` soltos
|
|
94
|
+
* podia ser chamada pela metade, e um `passes` sem a forma faz quem emite adivinhar entre aspas e
|
|
95
|
+
* chaves: as duas adivinhações produzem JSX que não compila. Fechar a porta é mais barato que
|
|
96
|
+
* vigiá-la (CLAUDE.md §6).
|
|
97
|
+
*
|
|
98
|
+
* `sameTag` diz se o elemento que a gente EMITE é o mesmo que ele escreveu. Só aí um atributo
|
|
99
|
+
* específico de elemento pode viajar: `viewBox` num `<span>` nosso é marcação inválida no arquivo
|
|
100
|
+
* dele.
|
|
101
|
+
*
|
|
102
|
+
* `requires` é o conjunto de props que a receita ALVO marca como obrigatória. Uma prop de
|
|
103
|
+
* composição obrigatória entra agora, porque sem ela o código não roda; o resto fica guardado na
|
|
104
|
+
* receita e só aparece quando alguém perguntar quais opções aquele componente tem (dono, 22/08).
|
|
105
|
+
*
|
|
106
|
+
* `expressions: "hold"` para quem GERA CÓDIGO, e não é preciosismo. `aria-valuenow={filled}` é
|
|
107
|
+
* verdadeiro no arquivo dele, onde `filled` existe; escrito dentro de um componente que a gente
|
|
108
|
+
* gera, `filled` não existe em lugar nenhum e o build DELE quebra. Um snippet é para uma pessoa
|
|
109
|
+
* adaptar e pode citar o nome dela; um arquivo gerado é para compilar.
|
|
110
|
+
*/
|
|
111
|
+
export function emittableAttrs(node, opts) {
|
|
112
|
+
/**
|
|
113
|
+
* FORMA DESCONHECIDA SEGURA TUDO - e este é o caso do censo tirado antes de 22/08.
|
|
114
|
+
*
|
|
115
|
+
* `passesExpr` ausente não quer dizer "nenhuma é expressão": quer dizer que o leitor daquele dia
|
|
116
|
+
* não sabia responder. Tratar como literal escreveria `title="metric.label"` no arquivo dele onde
|
|
117
|
+
* ele escreveu `title={metric.label}` - uma string no lugar de uma variável, que é leitura errada
|
|
118
|
+
* virando código errado, exatamente o defeito que esta família de consertos existe para não
|
|
119
|
+
* cometer.
|
|
120
|
+
*
|
|
121
|
+
* A omissão, ao contrário, é reparável: um `sync` traz a forma e o atributo aparece.
|
|
122
|
+
*/
|
|
123
|
+
if (node.passes && Object.keys(node.passes).length > 0 && !node.passesExpr)
|
|
124
|
+
return { write: "", held: Object.keys(node.passes) };
|
|
125
|
+
const expr = new Set(node.passesExpr ?? []);
|
|
126
|
+
const write = [];
|
|
127
|
+
const held = [];
|
|
128
|
+
for (const [key, value] of Object.entries(node.passes ?? {})) {
|
|
129
|
+
const kind = attrKind(key, null);
|
|
130
|
+
/**
|
|
131
|
+
* A CLASSE SEM TAG SÓ SEPARA GLOBAL E HANDLER. Para as outras duas a pergunta é o que a gente
|
|
132
|
+
* emite, e quem sabe isso é quem chama - por isso `sameTag` e `requires` chegam de fora.
|
|
133
|
+
*/
|
|
134
|
+
if (kind === "handler")
|
|
135
|
+
continue;
|
|
136
|
+
const isExpression = expr.has(key);
|
|
137
|
+
if (isExpression && opts?.expressions === "hold") {
|
|
138
|
+
held.push(key);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const allowed = kind === "global" ||
|
|
142
|
+
Boolean(opts?.sameTag) ||
|
|
143
|
+
Boolean(opts?.requires?.has(key));
|
|
144
|
+
if (allowed)
|
|
145
|
+
write.push(attrText(key, value, isExpression));
|
|
146
|
+
else
|
|
147
|
+
held.push(key);
|
|
148
|
+
}
|
|
149
|
+
return { write: write.join(""), held };
|
|
150
|
+
}
|
package/dist/commands/import.js
CHANGED
|
@@ -3084,13 +3084,45 @@ function rootPackage(tag, sketch) {
|
|
|
3084
3084
|
* Off the FIRST sketch node, which is the returned element - the same lookup `rootPackage`
|
|
3085
3085
|
* makes one line up, guarded the same way.
|
|
3086
3086
|
*/
|
|
3087
|
-
function rootBehaviour(tag, sketch) {
|
|
3088
|
-
|
|
3089
|
-
|
|
3087
|
+
export function rootBehaviour(tag, sketch) {
|
|
3088
|
+
/**
|
|
3089
|
+
* PELO NÓ QUE O COMPONENTE RETORNA, e não pelo primeiro do arquivo.
|
|
3090
|
+
*
|
|
3091
|
+
* `sketch[0]` é uma suposição que a segunda população desmente: um arquivo declara com frequência
|
|
3092
|
+
* um closure ANTES do render que o usa, e é exatamente por isso que `SketchNode.root` existe.
|
|
3093
|
+
* Medido: em 62 dos 323 componentes do dashboard os dois divergem, e em 4 deles o `rootProps`
|
|
3094
|
+
* já se perdia hoje por causa disso. No primeiro cliente o número é 0 - uma população só nunca
|
|
3095
|
+
* teria mostrado.
|
|
3096
|
+
*/
|
|
3097
|
+
const root = sketch.find((n) => n.root) ?? sketch[0];
|
|
3098
|
+
if (!tag || !root || root.tag !== tag)
|
|
3090
3099
|
return {};
|
|
3091
|
-
|
|
3092
|
-
? { rootProps:
|
|
3100
|
+
const props = root.behaviourProps && Object.keys(root.behaviourProps).length > 0
|
|
3101
|
+
? { rootProps: root.behaviourProps }
|
|
3102
|
+
: {};
|
|
3103
|
+
/**
|
|
3104
|
+
* E O QUE ELE ESCREVE NA RAIZ - `role`, `aria-*`, `type`. Este é o campo que faltava.
|
|
3105
|
+
*
|
|
3106
|
+
* `passes` só sobrevive num nó da árvore, e a raiz de um componente é CONSUMIDA: ela É o
|
|
3107
|
+
* componente, então não existe nó onde o fato dela caiba. Medido: 26 dos 59 componentes do
|
|
3108
|
+
* primeiro cliente e 113 dos 323 do segundo têm `passes` numa raiz sem slot, e um agente
|
|
3109
|
+
* reconstruindo o `Progress` a partir da receita escrevia uma `<div>` muda - sem
|
|
3110
|
+
* `role="progressbar"`, sem `aria-valuenow`. A lei 10 proíbe acessibilidade SUPOSTA; isto era o
|
|
3111
|
+
* inverso, que é pior: ele escreveu, o censo colheu, e a receita calou.
|
|
3112
|
+
*
|
|
3113
|
+
* SEPARADO DE `rootProps` de propósito. `rootProps` significa *prop cujo valor é uma condição*, e
|
|
3114
|
+
* `component-behaviour.ts` extrai nomes de estado dos valores dele. Jogar `role="progressbar"`
|
|
3115
|
+
* ali faria "progressbar" virar um nome de estado que o componente segura.
|
|
3116
|
+
*/
|
|
3117
|
+
const given = root.passes && Object.keys(root.passes).length > 0
|
|
3118
|
+
? {
|
|
3119
|
+
rootPasses: root.passes,
|
|
3120
|
+
...(root.passesExpr && root.passesExpr.length > 0
|
|
3121
|
+
? { rootPassesExpr: root.passesExpr }
|
|
3122
|
+
: {}),
|
|
3123
|
+
}
|
|
3093
3124
|
: {};
|
|
3125
|
+
return { ...props, ...given };
|
|
3094
3126
|
}
|
|
3095
3127
|
export async function resolveReadParts(census, root,
|
|
3096
3128
|
/** `true` numa re-medição: o resumo desta etapa fica para o `sync --full` - ver `say`. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { emittableAttrs } from "./attr-shape.js";
|
|
1
2
|
import { PHRASING_FORMS, } from "./types.js";
|
|
2
3
|
export const DEFAULT_CONVENTION = {
|
|
3
4
|
prefix: "ds-",
|
|
@@ -71,7 +72,27 @@ function elementFor(name, recipe) {
|
|
|
71
72
|
? { tag: "button", attrs: ' type="button"' }
|
|
72
73
|
: undefined);
|
|
73
74
|
const tag = hit?.tag ?? "div";
|
|
74
|
-
|
|
75
|
+
/**
|
|
76
|
+
* O QUE ELE ESCREVEU NA RAIZ VENCE O QUE A GENTE SUPÕE - a lei 10, aplicada onde ela nasceu.
|
|
77
|
+
*
|
|
78
|
+
* A tabela acima inventa `role="switch"` a partir do NOME da receita. Isso é uma suposição
|
|
79
|
+
* razoável e continua sendo uma suposição; `role="progressbar"` medido no `<div>` que ELE escreveu
|
|
80
|
+
* é um fato. Quando os dois falam da mesma chave, o fato ganha - e até 22/08 o fato nem chegava
|
|
81
|
+
* aqui: 26 dos 59 componentes de um cliente e 113 dos 323 do outro tinham a raiz com atributo e a
|
|
82
|
+
* receita muda.
|
|
83
|
+
*
|
|
84
|
+
* SÓ LITERAL. `aria-valuenow={filled}` é verdade no arquivo dele, onde `filled` existe; escrito
|
|
85
|
+
* dentro de um componente que a gente GERA, `filled` não existe e o build dele quebra. Ver
|
|
86
|
+
* `expressions: "hold"` em `attr-shape.ts`.
|
|
87
|
+
*/
|
|
88
|
+
const theirs = emittableAttrs({
|
|
89
|
+
passes: recipe.preview?.rootPasses,
|
|
90
|
+
passesExpr: recipe.preview?.rootPassesExpr,
|
|
91
|
+
}, {
|
|
92
|
+
sameTag: tag === recipe.preview?.rootTag,
|
|
93
|
+
expressions: "hold",
|
|
94
|
+
}).write;
|
|
95
|
+
const attrs = mergeAttrs(hit?.attrs ?? "", theirs);
|
|
75
96
|
const isVoid = tag === "input" || tag === "hr";
|
|
76
97
|
/**
|
|
77
98
|
* A VOID ELEMENT CANNOT HOST THE PARTS WE TELL PEOPLE TO PUT INSIDE IT.
|
|
@@ -338,6 +359,24 @@ function asElement(tag, attrs, voidEl) {
|
|
|
338
359
|
offersAs: true,
|
|
339
360
|
};
|
|
340
361
|
}
|
|
362
|
+
/**
|
|
363
|
+
* DUAS LISTAS DE ATRIBUTOS NUMA, e a DELE ganha a chave repetida.
|
|
364
|
+
*
|
|
365
|
+
* Sem isto o elemento sairia com `role="switch" role="progressbar"` - JSX que não compila e, se
|
|
366
|
+
* compilasse, resolveria pelo último. A chave é extraída do texto porque é assim que as duas metades
|
|
367
|
+
* já viajam; nada aqui inventa atributo.
|
|
368
|
+
*/
|
|
369
|
+
function mergeAttrs(ours, theirs) {
|
|
370
|
+
const taken = new Set([...theirs.matchAll(/\s([A-Za-z][\w:-]*)=/g)].map((m) => m[1]));
|
|
371
|
+
const kept = [...ours.matchAll(/\s[A-Za-z][\w:-]*=(?:"[^"]*"|\{[^}]*\})/g)]
|
|
372
|
+
.map((m) => m[0])
|
|
373
|
+
.filter((piece) => {
|
|
374
|
+
const key = /\s([A-Za-z][\w:-]*)=/.exec(piece)?.[1] ?? "";
|
|
375
|
+
return !taken.has(key);
|
|
376
|
+
})
|
|
377
|
+
.join("");
|
|
378
|
+
return `${kept}${theirs}`;
|
|
379
|
+
}
|
|
341
380
|
function dataAttrLines(axes) {
|
|
342
381
|
return axes
|
|
343
382
|
.map((a) => a.boolean
|
|
@@ -891,8 +930,20 @@ function treeParts(nodes, out = []) {
|
|
|
891
930
|
function emitTree(nodes, comp, indent) {
|
|
892
931
|
const lines = [];
|
|
893
932
|
for (const node of nodes) {
|
|
933
|
+
/**
|
|
934
|
+
* O QUE ELE ESCREVEU NESTE NÓ - `role`, `aria-*`, `data-*`, e a prop que o alvo EXIGE.
|
|
935
|
+
*
|
|
936
|
+
* Só literal: uma expressão nomeia uma variável do arquivo DELE que não existe dentro do
|
|
937
|
+
* componente que a gente gera, e o build dele quebra. `sameTag` é falso porque a tag emitida sai
|
|
938
|
+
* do formulário do nó (`FORM_TAG`), que é escolha nossa - então atravessa o que o HTML define
|
|
939
|
+
* como global, que é justamente onde a acessibilidade mora.
|
|
940
|
+
*/
|
|
941
|
+
const attrs = emittableAttrs(node, {
|
|
942
|
+
requires: new Set(node.passesRequired ?? []),
|
|
943
|
+
expressions: "hold",
|
|
944
|
+
}).write;
|
|
894
945
|
if (node.as === "component") {
|
|
895
|
-
lines.push(`${indent}{/* your <${pascal(node.ref ?? "")} /> goes here - it has a recipe of its own, so it is not inlined */}`);
|
|
946
|
+
lines.push(`${indent}{/* your <${pascal(node.ref ?? "")}${attrs} /> goes here - it has a recipe of its own, so it is not inlined */}`);
|
|
896
947
|
continue;
|
|
897
948
|
}
|
|
898
949
|
if (node.as === "external") {
|
|
@@ -918,17 +969,17 @@ function emitTree(nodes, comp, indent) {
|
|
|
918
969
|
}
|
|
919
970
|
const partComp = `${comp}${pascal(node.part)}`;
|
|
920
971
|
if (ARRANGES.has(node.as) && node.children && node.children.length > 0) {
|
|
921
|
-
lines.push(`${indent}<${partComp}>`);
|
|
972
|
+
lines.push(`${indent}<${partComp}${attrs}>`);
|
|
922
973
|
lines.push(emitTree(node.children, comp, `${indent} `));
|
|
923
974
|
lines.push(`${indent}</${partComp}>`);
|
|
924
975
|
continue;
|
|
925
976
|
}
|
|
926
977
|
const { voidEl } = FORM_TAG[node.as] ?? {};
|
|
927
978
|
if (voidEl) {
|
|
928
|
-
lines.push(`${indent}<${partComp} />`);
|
|
979
|
+
lines.push(`${indent}<${partComp}${attrs} />`);
|
|
929
980
|
continue;
|
|
930
981
|
}
|
|
931
|
-
lines.push(`${indent}<${partComp}>${node.text ?? `{/* ${node.part} */}`}</${partComp}>`);
|
|
982
|
+
lines.push(`${indent}<${partComp}${attrs}>${node.text ?? `{/* ${node.part} */}`}</${partComp}>`);
|
|
932
983
|
}
|
|
933
984
|
return lines.filter(Boolean).join("\n");
|
|
934
985
|
}
|
package/dist/doctor/sketch.js
CHANGED
|
@@ -362,8 +362,9 @@ export function passedProps(body,
|
|
|
362
362
|
/** O que `behaviourProps` já levou, para cada fato viver num lugar só. */
|
|
363
363
|
taken) {
|
|
364
364
|
const out = {};
|
|
365
|
+
const expression = [];
|
|
365
366
|
const SKIP = /^(className|class|key|ref|style|children)$/;
|
|
366
|
-
for (const m of body.matchAll(
|
|
367
|
+
for (const m of body.matchAll(PROP_ATTR)) {
|
|
367
368
|
const prop = m[1];
|
|
368
369
|
if (SKIP.test(prop) || taken?.[prop] || out[prop])
|
|
369
370
|
continue;
|
|
@@ -377,9 +378,27 @@ taken) {
|
|
|
377
378
|
if (!value || /=>|\bfunction\b/.test(value) || value.startsWith("{"))
|
|
378
379
|
continue;
|
|
379
380
|
out[prop] = value;
|
|
381
|
+
expression.push(prop);
|
|
380
382
|
}
|
|
381
|
-
return Object.keys(out).length > 0 ? out : null;
|
|
383
|
+
return Object.keys(out).length > 0 ? { props: out, expression } : null;
|
|
382
384
|
}
|
|
385
|
+
/**
|
|
386
|
+
* UM NOME DE PROP INCLUI O HÍFEN, e ele era o pedaço que a acessibilidade dele inteira carregava.
|
|
387
|
+
*
|
|
388
|
+
* O padrão anterior era `(\w+)=`, e `\w` não casa `-`. Num `aria-hidden="true"` o motor tentava
|
|
389
|
+
* `aria`, batia no hífen, voltava, desistia, e recomeçava em `label`/`hidden` - então o censo
|
|
390
|
+
* gravava a chave SEM o prefixo. Medido no fonte dos dois clientes: 83 `aria-*` + 4 `data-*` no
|
|
391
|
+
* `packages/ui` do primeiro, 174 + 623 no dashboard do segundo.
|
|
392
|
+
*
|
|
393
|
+
* O custo não era só de perda. `aria-hidden` gravado como `hidden` e emitido de volta escreve o
|
|
394
|
+
* atributo HTML `hidden`, que APAGA o elemento da tela: a leitura errada viraria código errado no
|
|
395
|
+
* arquivo dele. Por isso o conserto mora na COLETA e não na emissão - depois daqui a informação já
|
|
396
|
+
* não existe, e nenhum `reinterpret` a recupera.
|
|
397
|
+
*
|
|
398
|
+
* `[\w:-]` cobre as três grafias que o JSX aceita num nome de atributo: `data-x`, `aria-x` e
|
|
399
|
+
* `xlink:href`. A âncora `[A-Za-z]` na primeira posição impede que um hífen solto vire nome.
|
|
400
|
+
*/
|
|
401
|
+
const PROP_ATTR = /\b([A-Za-z][\w:-]*)=(?:"([^"]{0,80})"|'([^']{0,80})'|\{([^{}]{1,80})\})/g;
|
|
383
402
|
/**
|
|
384
403
|
* A expressão que um `map` percorre, quando este elemento está dentro de um - ver `repeats`.
|
|
385
404
|
*
|
|
@@ -542,8 +561,21 @@ export function sketchOf(source, name) {
|
|
|
542
561
|
* para a receita de uma TELA carregar a fiação, e não só a casca.
|
|
543
562
|
*/
|
|
544
563
|
const given = passedProps(event.body, conditional);
|
|
545
|
-
if (given)
|
|
546
|
-
node.passes = given;
|
|
564
|
+
if (given) {
|
|
565
|
+
node.passes = given.props;
|
|
566
|
+
/**
|
|
567
|
+
* A FORMA VIAJA SEMPRE QUE O VALOR VIAJA, inclusive VAZIA - e a lista vazia é o ponto.
|
|
568
|
+
*
|
|
569
|
+
* Se `passesExpr` só aparecesse quando há expressão, "ausente" significaria duas coisas ao
|
|
570
|
+
* mesmo tempo: *nenhuma delas é expressão* e *este censo é velho e não sabe responder*. Quem
|
|
571
|
+
* emite não tem como distinguir, e as duas exigem comportamentos OPOSTOS - escrever tudo como
|
|
572
|
+
* literal, ou não escrever nada.
|
|
573
|
+
*
|
|
574
|
+
* Com o array sempre presente, ausência quer dizer uma coisa só: censo tirado antes de
|
|
575
|
+
* 0.16.275. Ver `emittableAttrs`, que segura o atributo nesse caso em vez de adivinhar.
|
|
576
|
+
*/
|
|
577
|
+
node.passesExpr = given.expression;
|
|
578
|
+
}
|
|
547
579
|
const list = repeatOf(source, event.at);
|
|
548
580
|
if (list)
|
|
549
581
|
node.repeats = list;
|
|
@@ -2014,11 +2014,60 @@ function bodyBrace(source, declAt) {
|
|
|
2014
2014
|
* function to alias to either: the body IS the argument.
|
|
2015
2015
|
*/
|
|
2016
2016
|
const head = source.slice(declAt, declAt + 400);
|
|
2017
|
-
|
|
2017
|
+
/**
|
|
2018
|
+
* O GENÉRICO PODE SER ANINHADO, e `<[^>(]*>` proibia isso.
|
|
2019
|
+
*
|
|
2020
|
+
* `forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(` tem um `>` dentro do argumento de
|
|
2021
|
+
* tipo, então o padrão parava no primeiro e o wrapper inteiro deixava de casar - o componente caía
|
|
2022
|
+
* na varredura simples, que conta parêntese do `forwardRef` como se fosse do corpo. `ModalBody` e
|
|
2023
|
+
* `ModalFooter` do cliente ficavam sem raiz por isso.
|
|
2024
|
+
*
|
|
2025
|
+
* `[^(]*` deixa o `>` passar e para no parêntese, que é o delimitador de verdade aqui.
|
|
2026
|
+
*/
|
|
2027
|
+
const wrapper = /=\s*(?:React\.)?(?:forwardRef|memo|styled\([^)]*\))\s*(?:<[^(]*>)?\s*\(/.exec(head);
|
|
2018
2028
|
if (wrapper) {
|
|
2019
|
-
const
|
|
2029
|
+
const from = declAt + wrapper.index;
|
|
2030
|
+
const rest = source.slice(from);
|
|
2031
|
+
/**
|
|
2032
|
+
* A LISTA DE PARÂMETROS PODE TER UM PARÊNTESE DENTRO, e o padrão anterior não podia saber disso.
|
|
2033
|
+
*
|
|
2034
|
+
* Ele exigia `\([^)]*\)`, que para no PRIMEIRO `)`. O `NoiseOverlay` do cliente escreve
|
|
2035
|
+
* `color = "rgba(0,0,0,0.025)"` como default de prop, então a varredura terminava dentro da
|
|
2036
|
+
* lista, não achava a chave do corpo, e o componente inteiro ficava sem raiz: sem `rootTag`, sem
|
|
2037
|
+
* nó marcado `root`, sem `rootClasses` e sem `rootArrange`.
|
|
2038
|
+
*
|
|
2039
|
+
* Medido nas duas populações: 7 de 59 componentes de um cliente (12%) e 13 de 320 do outro
|
|
2040
|
+
* ficavam assim. É a forma de perda que o mapa cataloga - uma enumeração mais estreita que a
|
|
2041
|
+
* própria entrada -, e o conserto é contar parêntese em vez de proibi-lo.
|
|
2042
|
+
*/
|
|
2043
|
+
const named = /function\s*[A-Za-z0-9_$]*\s*\(/.exec(rest);
|
|
2044
|
+
const arrow = /=>\s*\{/.exec(rest);
|
|
2045
|
+
/**
|
|
2046
|
+
* O QUE VIER PRIMEIRO, como a alternância anterior já decidia. Procurar `function` no arquivo
|
|
2047
|
+
* inteiro casaria a PRÓXIMA função declarada quando o wrapper carrega um arrow - `forwardRef((
|
|
2048
|
+
* props, ref) => { … })` -, e o corpo lido seria o de outro componente.
|
|
2049
|
+
*/
|
|
2050
|
+
const namedFirst = named != null && (arrow == null || named.index < arrow.index);
|
|
2051
|
+
if (namedFirst && named) {
|
|
2052
|
+
const open = named.index + named[0].length - 1;
|
|
2053
|
+
let depth = 0;
|
|
2054
|
+
for (let j = open; j < rest.length; j++) {
|
|
2055
|
+
const ch = rest[j];
|
|
2056
|
+
if (ch === "(")
|
|
2057
|
+
depth += 1;
|
|
2058
|
+
else if (ch === ")") {
|
|
2059
|
+
depth -= 1;
|
|
2060
|
+
if (depth === 0) {
|
|
2061
|
+
const body = /^\s*\{/.exec(rest.slice(j + 1));
|
|
2062
|
+
if (body)
|
|
2063
|
+
return from + j + body[0].length;
|
|
2064
|
+
break;
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2020
2069
|
if (arrow) {
|
|
2021
|
-
const at =
|
|
2070
|
+
const at = from + arrow.index + arrow[0].length - 1;
|
|
2022
2071
|
if (source[at] === "{")
|
|
2023
2072
|
return at;
|
|
2024
2073
|
}
|
|
@@ -2088,7 +2137,26 @@ function ownReturn(source, declAt) {
|
|
|
2088
2137
|
continue;
|
|
2089
2138
|
}
|
|
2090
2139
|
if (ch === '"' || ch === "'" || ch === "`") {
|
|
2091
|
-
|
|
2140
|
+
/**
|
|
2141
|
+
* UM APÓSTROFO DENTRO DE UMA PALAVRA NÃO ABRE STRING - `Don't`, `we'll`, `it's`.
|
|
2142
|
+
*
|
|
2143
|
+
* Em texto JSX o apóstrofo é uma letra como outra qualquer, e tratá-lo como delimitador abria
|
|
2144
|
+
* uma "string" que só fechava no próximo apóstrofo do arquivo. Tudo depois disso ficava fora
|
|
2145
|
+
* de sincronia: a contagem de chaves perdia o passo e o `return` da raiz nunca era visto no
|
|
2146
|
+
* nível certo.
|
|
2147
|
+
*
|
|
2148
|
+
* Medido no dashboard do segundo cliente: 6 componentes ficavam sem raiz nenhuma por causa
|
|
2149
|
+
* disto - `CurateTemplate`, `CustomCardList`, `TunePreferences` e mais três -, e com a regra
|
|
2150
|
+
* nova nenhuma das 376 respostas que já estavam certas nas duas populações muda.
|
|
2151
|
+
*
|
|
2152
|
+
* A regra é estreita de propósito: só o apóstrofo com letra dos dois lados. `'a'` continua
|
|
2153
|
+
* string, e `it's` continua texto.
|
|
2154
|
+
*/
|
|
2155
|
+
const dentroDePalavra = ch === "'" &&
|
|
2156
|
+
/[A-Za-z]/.test(source[j - 1] ?? "") &&
|
|
2157
|
+
/[A-Za-z]/.test(source[j + 1] ?? "");
|
|
2158
|
+
if (!dentroDePalavra)
|
|
2159
|
+
quote = ch;
|
|
2092
2160
|
continue;
|
|
2093
2161
|
}
|
|
2094
2162
|
if (ch === "{")
|
package/dist/install-marks.js
CHANGED
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
* é sempre o bump deste PR - nunca o número que o `package.json` já carrega, porque alguém pode
|
|
129
129
|
* publicar no meio.
|
|
130
130
|
*/
|
|
131
|
-
export const MATERIALISER_SINCE = "0.16.
|
|
131
|
+
export const MATERIALISER_SINCE = "0.16.276";
|
|
132
132
|
/**
|
|
133
133
|
* A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
|
|
134
134
|
*
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* ONDE ESTÃO AS POPULAÇÕES MEDIDAS - e por que elas não moram mais na máquina de ninguém.
|
|
5
|
+
*
|
|
6
|
+
* O QUÊ. 23 specs medem contra o censo de dois repositórios reais. Até 22/08 cada um apontava para
|
|
7
|
+
* `~/projects/frontend-hub` ou `~/personal/codelevel-monorepo` na home do dono.
|
|
8
|
+
*
|
|
9
|
+
* POR QUÊ ISSO ERA UM DEFEITO. Não porque quebrava - 35 dos 36 pulavam educadamente quando o
|
|
10
|
+
* arquivo sumia. Porque ficava VERDE. Medido em 22/08: três artefatos do `signalui` já não estavam
|
|
11
|
+
* no disco e 12 testes pulavam em silêncio havia sabe-se lá quanto tempo. Uma suíte que fica verde
|
|
12
|
+
* quando parou de medir é pior que uma vermelha, e essa é a única classe de defeito que esta
|
|
13
|
+
* esteira não pode aceitar num instrumento.
|
|
14
|
+
*
|
|
15
|
+
* E O SEGUNDO CUSTO, que é o do produto: sem a segunda população, toda régua nova passa a ser
|
|
16
|
+
* medida num repositório só. Três réguas escritas em 22/08 estariam erradas por isso.
|
|
17
|
+
*
|
|
18
|
+
* COMO. `fixtures/populations/`, versionado, compacto. `npm run populations:refresh` atualiza a
|
|
19
|
+
* partir dos repositórios vivos quando eles existem - e o que mudou aparece no `git diff`, que é
|
|
20
|
+
* onde uma mudança de insumo pode ser revisada em vez de sentida.
|
|
21
|
+
*
|
|
22
|
+
* GÊMEO de `apps/web/src/lib/ds/populations.ts`: o CLI é publicado standalone e não importa de lá. As
|
|
23
|
+
* duas cópias resolvem o mesmo diretório por caminho relativo, então elas não podem divergir de
|
|
24
|
+
* conteúdo - só de posição.
|
|
25
|
+
*/
|
|
26
|
+
/** A raiz do monorepo, a partir deste arquivo: `packages/cli/src` → 3 níveis acima. */
|
|
27
|
+
const ROOT = join(import.meta.dirname, "..", "..", "..");
|
|
28
|
+
const POPULATIONS = join(ROOT, "fixtures", "populations");
|
|
29
|
+
/**
|
|
30
|
+
* 323 componentes, escopo `apps/web-dashboard`. A população que derruba régua feita em cima da
|
|
31
|
+
* outra: ela não declara um `--animate-*` sequer, escreve o relógio literal na receita, e tem 62
|
|
32
|
+
* componentes cujo primeiro nó do sketch não é a raiz.
|
|
33
|
+
*/
|
|
34
|
+
export const FRONTEND_HUB_CENSUS = join(POPULATIONS, "frontend-hub.census.json");
|
|
35
|
+
/**
|
|
36
|
+
* 59 componentes, escopo `packages/ui`. O caso de teste vivo do dia a dia - é dele que saem os
|
|
37
|
+
* números do `triage` e é nele que a jornada é conferida na tela.
|
|
38
|
+
*/
|
|
39
|
+
export const CODELEVEL_CENSUS = join(POPULATIONS, "codelevel-ui.census.json");
|
|
40
|
+
/**
|
|
41
|
+
* OS REPOSITÓRIOS VIVOS - e estes NÃO estão congelados, de propósito.
|
|
42
|
+
*
|
|
43
|
+
* Um punhado de specs não mede o censo: eles varrem os `.tsx` de ORIGEM para exercitar o COLETOR -
|
|
44
|
+
* `sketchOf`, `rootTag`, o leitor de `cn()`. Congelar 69 arquivos de código de cliente dentro deste
|
|
45
|
+
* repositório é uma decisão diferente de congelar uma medição, e ela não foi tomada.
|
|
46
|
+
*
|
|
47
|
+
* Então estes caminhos continuam dependendo da máquina, e é aqui que isso está DITO. Todo spec que
|
|
48
|
+
* os usa tem que guardar com `existsSync` - sem o guard, a ausência derruba a suíte em vez de pular.
|
|
49
|
+
*
|
|
50
|
+
* Medido em 22/08: 5 specs no `apps/web` e no `packages/cli` estão nesta situação.
|
|
51
|
+
*/
|
|
52
|
+
export const CODELEVEL_REPO = join(homedir(), "personal", "codelevel-monorepo");
|
|
53
|
+
export const FRONTEND_HUB_REPO = join(homedir(), "projects", "frontend-hub");
|
package/package.json
CHANGED