synthesisui 0.16.483 → 0.16.484
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/baseline-craft.js +13 -1
- package/dist/claude-md.js +8 -1
- package/dist/commands/import.js +67 -1
- package/dist/commands/sync.js +50 -2
- package/dist/doctor/broken-refs.js +61 -0
- package/dist/merge-census.js +7 -0
- package/dist/responsive-form.js +85 -0
- package/package.json +1 -1
package/dist/baseline-craft.js
CHANGED
|
@@ -21,6 +21,16 @@ import { body, bodyWrapped, paint } from "./output.js";
|
|
|
21
21
|
* Vazio e não uma linha vazia: quem chama decide se imprime o título, e um bloco com cabeçalho e
|
|
22
22
|
* nada embaixo é pior que bloco nenhum.
|
|
23
23
|
*/
|
|
24
|
+
/** "A and B", "A, B, C, D and 2 more" - quatro nomes cabem numa linha de terminal. */
|
|
25
|
+
function namesOf(names) {
|
|
26
|
+
const shown = names.slice(0, 4);
|
|
27
|
+
const rest = names.length - shown.length;
|
|
28
|
+
if (rest > 0)
|
|
29
|
+
return `${shown.join(", ")} and ${rest} more`;
|
|
30
|
+
return shown.length === 1
|
|
31
|
+
? shown[0]
|
|
32
|
+
: `${shown.slice(0, -1).join(", ")} and ${shown[shown.length - 1]}`;
|
|
33
|
+
}
|
|
24
34
|
export function craftLines(craft) {
|
|
25
35
|
if (!craft)
|
|
26
36
|
return [];
|
|
@@ -50,7 +60,9 @@ export function craftLines(craft) {
|
|
|
50
60
|
* a segunda pergunta, e ela merece a segunda linha.
|
|
51
61
|
*/
|
|
52
62
|
if (apart)
|
|
53
|
-
lines.push(...bodyWrapped(
|
|
63
|
+
lines.push(...bodyWrapped(craft.unwrittenNames?.length
|
|
64
|
+
? `${namesOf(craft.unwrittenNames)} have no recipe yet - nothing in their files paints them - so they are counted apart, never against you.`
|
|
65
|
+
: `${apart} have no recipe yet: counted apart, never against you.`).map((l) => body(paint.dim(l))));
|
|
54
66
|
/**
|
|
55
67
|
* OS RÓTULOS ALINHADOS PELO MAIS LONGO, e a largura é medida e não estimada: os nomes chegam do
|
|
56
68
|
* servidor, então um padding fixo desalinharia no dia em que uma lente for renomeada lá.
|
package/dist/claude-md.js
CHANGED
|
@@ -3,6 +3,7 @@ import { dirname, join } from "node:path";
|
|
|
3
3
|
import { hasHook } from "./agent-wiring.js";
|
|
4
4
|
import { declaredReference } from "./group-role.js";
|
|
5
5
|
import { recallAvailable } from "./memory/availability.js";
|
|
6
|
+
import { readResponsiveForm, responsiveLine } from "./responsive-form.js";
|
|
6
7
|
const START = "<!-- synthesisui:start -->";
|
|
7
8
|
const END = "<!-- synthesisui:end -->";
|
|
8
9
|
/** Reads the installed DSs from the .lock files in _synthesisui/ds/<slug>/. */
|
|
@@ -543,9 +544,15 @@ element. Write every user-facing string in that language - labels, empty states,
|
|
|
543
544
|
\`alt\`. A screen reader pronounces \`aria-label\` using \`lang\`, so a mixed-language interface is
|
|
544
545
|
worse than an untranslated one. If the attribute is wrong, change it rather than writing against
|
|
545
546
|
it.`;
|
|
547
|
+
/**
|
|
548
|
+
* E COMO ELE FAZ O RESPONSIVO, medido como o idioma (item 19 da bateria, 29/09): no b9, 100% inline, o agente criou
|
|
549
|
+
* CSS Modules só para o ponto de quebra, porque ninguém tinha dito como aquele projeto faz.
|
|
550
|
+
*/
|
|
551
|
+
const form = await readResponsiveForm(projectRoot).catch(() => null);
|
|
552
|
+
const responsive = form ? `\n\n${responsiveLine(form)}` : "";
|
|
546
553
|
const body = `## Design Systems (via SynthesisUI)
|
|
547
554
|
|
|
548
|
-
This project uses design system(s) tracked by the \`synthesisui\` CLI. ${rule}${language}
|
|
555
|
+
This project uses design system(s) tracked by the \`synthesisui\` CLI. ${rule}${language}${responsive}
|
|
549
556
|
|
|
550
557
|
${sections.join("\n")}
|
|
551
558
|
|
package/dist/commands/import.js
CHANGED
|
@@ -11,7 +11,7 @@ import { PAGES_MAX, pageCompositionOf } from "../census-pages.js";
|
|
|
11
11
|
import { readCredentials, readProjectConfig, readToken, resolveRegistry, sameRegistry, } from "../config.js";
|
|
12
12
|
import { declaredElsewhere } from "../declared-elsewhere.js";
|
|
13
13
|
import { architectureGap, architectureRule, componentHome, describeArchitecture, describeChoice, describeGap, detectArchitectures, homeLine, packagingOf, proposeNewHome, resolvesAs, countsAsOrganization, } from "../doctor/architecture.js";
|
|
14
|
-
import { findBrokenRefs } from "../doctor/broken-refs.js";
|
|
14
|
+
import { findBrokenRefs, findOrphanVars, findSynonyms, synonymLine } from "../doctor/broken-refs.js";
|
|
15
15
|
import { nestingRules, propRules, readDefinitionProps, readNesting, readRuntime, } from "../doctor/call-sites.js";
|
|
16
16
|
import { asCatalogueTable, describeFallback, fetchCatalogue, } from "../doctor/catalogue-fetch.js";
|
|
17
17
|
import { describeClassStyle, detectClassStyle, } from "../doctor/class-style.js";
|
|
@@ -2620,6 +2620,8 @@ export async function takeCensus(root, opts) {
|
|
|
2620
2620
|
}
|
|
2621
2621
|
}
|
|
2622
2622
|
const brokenRefs = findBrokenRefs(sources, declaredNames);
|
|
2623
|
+
const orphanVars = findOrphanVars(css, sources);
|
|
2624
|
+
const synonyms = findSynonyms(css, sources);
|
|
2623
2625
|
const conventions = detectConventions(sources);
|
|
2624
2626
|
const classStyle = detectClassStyle(sources);
|
|
2625
2627
|
/**
|
|
@@ -2696,6 +2698,8 @@ export async function takeCensus(root, opts) {
|
|
|
2696
2698
|
: {}),
|
|
2697
2699
|
...(animations.size > 0 ? { animations: [...animations].sort() } : {}),
|
|
2698
2700
|
...(brokenRefs.length > 0 ? { brokenRefs } : {}),
|
|
2701
|
+
...(orphanVars.length > 0 ? { orphanVars } : {}),
|
|
2702
|
+
...(synonyms.length > 0 ? { synonyms } : {}),
|
|
2699
2703
|
/**
|
|
2700
2704
|
* OS SISTEMAS QUE O CSS DELE JÁ CHAMA PELO NOME - ver `wiredSlugs`.
|
|
2701
2705
|
*
|
|
@@ -3051,6 +3055,38 @@ async function summarize(c, root, scope) {
|
|
|
3051
3055
|
sayAdoption(c);
|
|
3052
3056
|
sayCollisions(c);
|
|
3053
3057
|
await sayBrokenRefs(c, root, scope);
|
|
3058
|
+
sayOrphanVars(c);
|
|
3059
|
+
saySynonyms(c);
|
|
3060
|
+
}
|
|
3061
|
+
/**
|
|
3062
|
+
* A MESMA COR COM VÁRIOS NOMES - uma linha por cor, até três (item 16 da bateria, 29/09). Propõe o nome que o código
|
|
3063
|
+
* mais lê; quem decide se junta é ele.
|
|
3064
|
+
*/
|
|
3065
|
+
function saySynonyms(c) {
|
|
3066
|
+
const all = c.synonyms ?? [];
|
|
3067
|
+
if (all.length === 0)
|
|
3068
|
+
return;
|
|
3069
|
+
console.log("");
|
|
3070
|
+
console.log(section("One colour, several names"));
|
|
3071
|
+
for (const g of all.slice(0, 3))
|
|
3072
|
+
console.log(body(synonymLine(g)));
|
|
3073
|
+
if (all.length > 3)
|
|
3074
|
+
console.log(body(paint.faint(`(${all.length - 3} more colours with more than one name)`)));
|
|
3075
|
+
}
|
|
3076
|
+
/**
|
|
3077
|
+
* DECLARADAS E NUNCA USADAS - uma linha, nunca uma acusação (item 15 da bateria, 29/09).
|
|
3078
|
+
*
|
|
3079
|
+
* No b1 eram três sobras de um tema antigo, e o import passava calado. Quem lê decide se apaga: pode ser de propósito.
|
|
3080
|
+
*/
|
|
3081
|
+
function sayOrphanVars(c) {
|
|
3082
|
+
const all = c.orphanVars ?? [];
|
|
3083
|
+
if (all.length === 0)
|
|
3084
|
+
return;
|
|
3085
|
+
const names = all.slice(0, 6).map((o) => paint.strong(o.name));
|
|
3086
|
+
const more = all.length > 6 ? ` and ${all.length - 6} more` : "";
|
|
3087
|
+
console.log("");
|
|
3088
|
+
console.log(section("Declared and never used"));
|
|
3089
|
+
console.log(body(`${all.length} variable${all.length === 1 ? "" : "s"} your CSS declares and nothing reads: ${names.join(", ")}${more}. Usually left over from an old theme - yours to delete or keep.`));
|
|
3054
3090
|
}
|
|
3055
3091
|
/**
|
|
3056
3092
|
* OS ESCOPOS QUE VIRARAM UM SISTEMA, e o que cada um perdeu no empate.
|
|
@@ -4138,6 +4174,13 @@ const DECIDEM = [
|
|
|
4138
4174
|
const NA_TELA_DO_IMPORT = [
|
|
4139
4175
|
"How this project is organised",
|
|
4140
4176
|
...DECIDEM,
|
|
4177
|
+
/**
|
|
4178
|
+
* E AS ÓRFÃS, numa linha (item 15 da bateria, 29/09): no b1 o dev contava três sobras de tema e o import passava
|
|
4179
|
+
* calado. Cabe porque é uma linha só, e ela muda o que ele faz a seguir: apagar ou manter.
|
|
4180
|
+
*/
|
|
4181
|
+
"Declared and never used",
|
|
4182
|
+
/** E a mesma cor com vários nomes (item 16 da bateria, 29/09): até três linhas, e cada uma propõe um nome. */
|
|
4183
|
+
"One colour, several names",
|
|
4141
4184
|
];
|
|
4142
4185
|
function sayReach(c) {
|
|
4143
4186
|
const reach = ladderReach(c.declared);
|
|
@@ -4654,6 +4697,20 @@ quiet = false) {
|
|
|
4654
4697
|
* token nenhum o sistema inteiro é a semente, e "v1 is your tokens" levava ao contrário. Ver
|
|
4655
4698
|
* `nao-espelha-o-que-nao-existe.spec.ts`.
|
|
4656
4699
|
*/
|
|
4700
|
+
/**
|
|
4701
|
+
* AS CORES QUE O PROJETO NÃO TEM - item 18 da bateria (28/09), liberado por ele em 29/09. No b4 a borda, o sucesso e o
|
|
4702
|
+
* perigo vinham do nosso ponto de partida, e o dev achava que eram dele. O servidor manda quais (`fromSeed`).
|
|
4703
|
+
*/
|
|
4704
|
+
export function fromSeedLine(roles) {
|
|
4705
|
+
if (roles.length === 0)
|
|
4706
|
+
return null;
|
|
4707
|
+
const list = roles.length === 1
|
|
4708
|
+
? roles[0]
|
|
4709
|
+
: `${roles.slice(0, -1).join(", ")} or ${roles[roles.length - 1]}`;
|
|
4710
|
+
return roles.length === 1
|
|
4711
|
+
? `Your code names no ${list} colour, so it comes from our starting point - yours to change on the colours page.`
|
|
4712
|
+
: `Your code names no ${list} colour, so those come from our starting point - yours to change on the colours page.`;
|
|
4713
|
+
}
|
|
4657
4714
|
export function birthLine(name, declared) {
|
|
4658
4715
|
return declared === 0
|
|
4659
4716
|
? `${name} - your code names no token yet, so this system is our starting point, with your colours and sizes measured next to it (listed in the report).`
|
|
@@ -5160,6 +5217,15 @@ export async function runImport(opts) {
|
|
|
5160
5217
|
*/
|
|
5161
5218
|
if (payload?.primary)
|
|
5162
5219
|
console.log(body(payload.primary));
|
|
5220
|
+
/**
|
|
5221
|
+
* E AS QUE ELE NÃO TEM (item 18 da bateria, 29/09). Com a porta das cores aberta a frase dela já diz tudo, e com zero
|
|
5222
|
+
* token a frase do nascimento já diz que o sistema inteiro é o ponto de partida - aí esta linha seria repetição.
|
|
5223
|
+
*/
|
|
5224
|
+
const seedLine = !payload?.coloursDoor && Object.keys(census.declared ?? {}).length > 0
|
|
5225
|
+
? fromSeedLine(payload?.fromSeed ?? [])
|
|
5226
|
+
: null;
|
|
5227
|
+
if (seedLine)
|
|
5228
|
+
console.log(body(seedLine));
|
|
5163
5229
|
/**
|
|
5164
5230
|
* A PORTA - convite, nunca bloqueio (o ponto 1 que ele aprovou em 28/09). O servidor manda a frase pronta, a mesma
|
|
5165
5231
|
* que o quadro do sistema mostra, e o link da tela de escolher daquele sistema.
|
package/dist/commands/sync.js
CHANGED
|
@@ -405,6 +405,16 @@ export async function remeasure(args) {
|
|
|
405
405
|
* a frase diz que é o esperado, e o `.lock` guarda isso, para o `align` não pedir de novo.
|
|
406
406
|
*/
|
|
407
407
|
if (head?.status === 404) {
|
|
408
|
+
/**
|
|
409
|
+
* MAS O 404 TAMBÉM É A RESPOSTA A UMA SESSÃO RECUSADA (29/09): a rota não diz "não te conheço" para não revelar
|
|
410
|
+
* sistema de ninguém. O token de www.synthesisui.com num sync contra o localhost caía aqui, e o `teste-e09-fechamento`,
|
|
411
|
+
* que TEM leitura, foi marcado como nunca medido. Antes de concluir, pergunta à sessão.
|
|
412
|
+
*/
|
|
413
|
+
if (!(await sessionAccepted(base, token))) {
|
|
414
|
+
console.log(section("Sync"));
|
|
415
|
+
console.log(body(`${base} does not accept this machine's login, so nothing was re-measured - and nothing was marked. Sign in there and run this again: npx synthesisui login --registry ${base}`));
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
408
418
|
await markNoReading(root, slug);
|
|
409
419
|
console.log(section("Sync"));
|
|
410
420
|
console.log(body(`"${slug}" was not read from a repository - it comes from the gallery or was built on the platform - so there is nothing here to re-measure. That is expected, and this repo will stop asking.`));
|
|
@@ -422,10 +432,22 @@ export async function remeasure(args) {
|
|
|
422
432
|
* SEM CENSO GRAVADO NÃO HÁ O QUE REMEDIR CONTRA. Um sistema que nasceu da galeria não tem escopo
|
|
423
433
|
* nem leitura, e medir o repo dele produziria um censo de um projeto que ninguém mapeou para ele.
|
|
424
434
|
*/
|
|
435
|
+
/**
|
|
436
|
+
* A LEITURA EXISTE, então uma marca "nunca medido" no `.lock` está errada - a que uma sessão recusada deixou antes de
|
|
437
|
+
* 29/09 (ver o 404 acima). Sai aqui, para o `align` voltar a pedir o que precisa.
|
|
438
|
+
*/
|
|
439
|
+
if (stored.hasCensus)
|
|
440
|
+
await unmarkNoReading(root, slug);
|
|
441
|
+
/**
|
|
442
|
+
* O SISTEMA DA GALERIA APRENDE COM O QUE ELE CONSTRÓI - item 8 da bateria (28/09), liberado por ele em 29/09.
|
|
443
|
+
*
|
|
444
|
+
* Esta resposta era "did not come from an import (...) Import it first", e no b11 tudo o que o agente construiu em cima
|
|
445
|
+
* do sistema da galeria ficava fora da plataforma. Sem leitura gravada, a medição vai sobre o repositório inteiro (a
|
|
446
|
+
* mesma queda de escopo que `scopes` abaixo já faz), e a frase diz que é a primeira.
|
|
447
|
+
*/
|
|
425
448
|
if (!stored.hasCensus) {
|
|
426
449
|
console.log(section("Sync"));
|
|
427
|
-
console.log(body(`"${slug}"
|
|
428
|
-
return null;
|
|
450
|
+
console.log(body(`This is the first measurement of what this project built on "${slug}" - it came from the gallery, so the platform had not read this repo yet. What you created goes to your system's draft; the system's own colours and type stay as they are.`));
|
|
429
451
|
}
|
|
430
452
|
/**
|
|
431
453
|
* A DISTÂNCIA ATÉ O CLI PUBLICADO, dita onde ele já está.
|
|
@@ -1094,6 +1116,32 @@ async function askForScope(root) {
|
|
|
1094
1116
|
export async function installedSlug(root) {
|
|
1095
1117
|
return (await installedSlugs(root))[0] ?? null;
|
|
1096
1118
|
}
|
|
1119
|
+
/** A sessão desta máquina vale em `base`? - a mesma pergunta que o `status` faz. Sem resposta, diz que não. */
|
|
1120
|
+
async function sessionAccepted(base, token) {
|
|
1121
|
+
const res = await fetch(`${base}/api/auth/get-session`, {
|
|
1122
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
1123
|
+
}).catch(() => null);
|
|
1124
|
+
if (!res?.ok)
|
|
1125
|
+
return false;
|
|
1126
|
+
const who = (await res.json().catch(() => null));
|
|
1127
|
+
return Boolean(who?.user);
|
|
1128
|
+
}
|
|
1129
|
+
/** Tira do `.lock` a marca de "sem leitura", quando a plataforma diz que a leitura existe. */
|
|
1130
|
+
async function unmarkNoReading(root, slug) {
|
|
1131
|
+
const path = join(root, "_synthesisui", "ds", slug, ".lock");
|
|
1132
|
+
const raw = await readFile(path, "utf8").catch(() => "");
|
|
1133
|
+
if (!raw)
|
|
1134
|
+
return;
|
|
1135
|
+
try {
|
|
1136
|
+
const { reading, ...rest } = JSON.parse(raw);
|
|
1137
|
+
if (reading !== "none")
|
|
1138
|
+
return;
|
|
1139
|
+
await writeFile(path, `${JSON.stringify(rest, null, 2)}\n`, "utf8");
|
|
1140
|
+
}
|
|
1141
|
+
catch {
|
|
1142
|
+
/** Um `.lock` ilegível não é deste passo consertar. */
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1097
1145
|
/** Anota no `.lock` que a plataforma não tem leitura deste sistema - ver o 404 em `remeasure`. */
|
|
1098
1146
|
async function markNoReading(root, slug) {
|
|
1099
1147
|
const path = join(root, "_synthesisui", "ds", slug, ".lock");
|
|
@@ -174,3 +174,64 @@ export function findBrokenRefs(sources, declared) {
|
|
|
174
174
|
b.count - a.count ||
|
|
175
175
|
a.name.localeCompare(b.name));
|
|
176
176
|
}
|
|
177
|
+
const THEME_BLOCK = /@theme\b[^{]*\{[^}]*\}/g;
|
|
178
|
+
const DECLARES_VAR = /(--[a-zA-Z0-9_-]+)\s*:/g;
|
|
179
|
+
const READS_IN_JS = /getPropertyValue\(\s*['"`](--[a-zA-Z0-9_-]+)/g;
|
|
180
|
+
const TAILWIND_SHORT = /\((--[a-zA-Z0-9_-]+)\)/g;
|
|
181
|
+
export function findOrphanVars(css, sources) {
|
|
182
|
+
const declared = new Set();
|
|
183
|
+
for (const m of css.replace(THEME_BLOCK, "").matchAll(DECLARES_VAR))
|
|
184
|
+
declared.add(m[1]);
|
|
185
|
+
if (declared.size === 0)
|
|
186
|
+
return [];
|
|
187
|
+
const used = new Set();
|
|
188
|
+
for (const text of [css, ...sources.map((s) => s.source)]) {
|
|
189
|
+
for (const m of text.matchAll(VAR_REF))
|
|
190
|
+
used.add(m[1]);
|
|
191
|
+
for (const m of text.matchAll(READS_IN_JS))
|
|
192
|
+
used.add(m[1]);
|
|
193
|
+
for (const m of text.matchAll(TAILWIND_SHORT))
|
|
194
|
+
used.add(m[1]);
|
|
195
|
+
}
|
|
196
|
+
return [...declared].filter((n) => !used.has(n)).map((name) => ({ name }));
|
|
197
|
+
}
|
|
198
|
+
const DECLARES_COLOUR = /(--[a-zA-Z0-9_-]+)\s*:\s*(#[0-9a-fA-F]{3,8}|rgba?\([^)]*\)|hsla?\([^)]*\))\s*[;}]/g;
|
|
199
|
+
export function findSynonyms(css, sources) {
|
|
200
|
+
const byValue = new Map();
|
|
201
|
+
for (const m of css.replace(THEME_BLOCK, "").matchAll(DECLARES_COLOUR)) {
|
|
202
|
+
const value = m[2].replace(/\s+/g, "").toLowerCase();
|
|
203
|
+
const names = byValue.get(value) ?? [];
|
|
204
|
+
if (!names.includes(m[1]))
|
|
205
|
+
names.push(m[1]);
|
|
206
|
+
byValue.set(value, names);
|
|
207
|
+
}
|
|
208
|
+
const uses = new Map();
|
|
209
|
+
/** Só nas fontes: a folha `css` é a soma das folhas que também estão lá, e contar as duas dobraria cada leitura. */
|
|
210
|
+
for (const text of sources.length > 0 ? sources.map((s) => s.source) : [css])
|
|
211
|
+
for (const m of text.matchAll(VAR_REF))
|
|
212
|
+
uses.set(m[1], (uses.get(m[1]) ?? 0) + 1);
|
|
213
|
+
const groups = [];
|
|
214
|
+
for (const [value, names] of byValue) {
|
|
215
|
+
if (names.length < 2)
|
|
216
|
+
continue;
|
|
217
|
+
groups.push({
|
|
218
|
+
value,
|
|
219
|
+
names: names
|
|
220
|
+
.map((name) => ({ name, uses: uses.get(name) ?? 0 }))
|
|
221
|
+
.sort((a, b) => b.uses - a.uses),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
return groups.sort((a, b) => b.names.length - a.names.length);
|
|
225
|
+
}
|
|
226
|
+
/** "#2563eb has 4 names: ... Your code reads --brand the most - keeping one would make the rest aliases." */
|
|
227
|
+
export function synonymLine(g) {
|
|
228
|
+
const listed = g.names
|
|
229
|
+
.map((n) => `${n.name} (${n.uses > 0 ? `${n.uses}×` : "never read"})`)
|
|
230
|
+
.join(", ");
|
|
231
|
+
const top = g.names[0]?.uses ?? 0;
|
|
232
|
+
const most = g.names.filter((n) => n.uses === top && top > 0).map((n) => n.name);
|
|
233
|
+
const tail = most.length === 0
|
|
234
|
+
? "Nothing reads any of them yet - keeping one would make the rest aliases."
|
|
235
|
+
: `Your code reads ${most.length === 1 ? most[0] : `${most.slice(0, -1).join(", ")} and ${most[most.length - 1]}`} the most - keeping one would make the rest aliases.`;
|
|
236
|
+
return `${g.value} has ${g.names.length} names: ${listed}. ${tail}`;
|
|
237
|
+
}
|
package/dist/merge-census.js
CHANGED
|
@@ -261,6 +261,13 @@ export function mergeCensus(list) {
|
|
|
261
261
|
* dele continua lendo o censo como antes.
|
|
262
262
|
*/
|
|
263
263
|
scopes: list.map((c, i) => nameOf(c, i)),
|
|
264
|
+
/**
|
|
265
|
+
* SEM ÓRFÃS NUM CENSO DE VÁRIOS ESCOPOS: a declarada num escopo pode ser lida no outro, e cada escopo só viu o seu.
|
|
266
|
+
* Dizer "nunca usada" sem ter olhado tudo seria acusar errado - fica calado (item 15 da bateria, 29/09).
|
|
267
|
+
*/
|
|
268
|
+
orphanVars: undefined,
|
|
269
|
+
/** E pelo mesmo motivo os sinônimos: cada escopo contou as leituras só no que viu. */
|
|
270
|
+
synonyms: undefined,
|
|
264
271
|
declared,
|
|
265
272
|
observed,
|
|
266
273
|
totals,
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
const SKIP = new Set([
|
|
4
|
+
"node_modules",
|
|
5
|
+
"_synthesisui",
|
|
6
|
+
".next",
|
|
7
|
+
".git",
|
|
8
|
+
"dist",
|
|
9
|
+
"build",
|
|
10
|
+
"out",
|
|
11
|
+
".turbo",
|
|
12
|
+
]);
|
|
13
|
+
/** Um teto, para um monorepo grande não virar uma varredura longa na hora de escrever o bloco. */
|
|
14
|
+
const MAX_FILES = 600;
|
|
15
|
+
const TAILWIND_PREFIX = /\b(?:sm|md|lg|xl|2xl):[a-z-]+/;
|
|
16
|
+
const MEDIA = /@media\b/;
|
|
17
|
+
const MATCH_MEDIA = /\bmatchMedia\s*\(|\buseMediaQuery\s*\(/;
|
|
18
|
+
const INLINE = /\bstyle=\{\{/g;
|
|
19
|
+
const CLASSNAME = /\bclassName=/g;
|
|
20
|
+
async function walk(root) {
|
|
21
|
+
const out = [];
|
|
22
|
+
const queue = [root];
|
|
23
|
+
while (queue.length > 0 && out.length < MAX_FILES) {
|
|
24
|
+
const dir = queue.shift();
|
|
25
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
26
|
+
for (const e of entries) {
|
|
27
|
+
if (SKIP.has(e.name))
|
|
28
|
+
continue;
|
|
29
|
+
const full = join(dir, e.name);
|
|
30
|
+
if (e.isDirectory())
|
|
31
|
+
queue.push(full);
|
|
32
|
+
else if (/\.(tsx|jsx|ts|js|css|scss|sass|less|html|vue|svelte)$/.test(e.name))
|
|
33
|
+
out.push(full);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
export async function readResponsiveForm(root) {
|
|
39
|
+
const files = await walk(root);
|
|
40
|
+
if (files.length === 0)
|
|
41
|
+
return null;
|
|
42
|
+
let media = null;
|
|
43
|
+
let js = null;
|
|
44
|
+
let inline = 0;
|
|
45
|
+
let classes = 0;
|
|
46
|
+
let componentSheets = 0;
|
|
47
|
+
for (const file of files) {
|
|
48
|
+
const src = await readFile(file, "utf8").catch(() => "");
|
|
49
|
+
const rel = relative(root, file);
|
|
50
|
+
const markup = /\.(tsx|jsx|html|vue|svelte)$/.test(file);
|
|
51
|
+
const sheet = /\.(css|scss|sass|less)$/.test(file);
|
|
52
|
+
if (markup && TAILWIND_PREFIX.test(src))
|
|
53
|
+
return { kind: "tailwind" };
|
|
54
|
+
if (sheet && !/globals?\.(css|scss)$/.test(file))
|
|
55
|
+
componentSheets += 1;
|
|
56
|
+
if (!media && (sheet || markup) && MEDIA.test(src))
|
|
57
|
+
media = rel;
|
|
58
|
+
if (!js && /\.(tsx|jsx|ts|js)$/.test(file) && MATCH_MEDIA.test(src))
|
|
59
|
+
js = rel;
|
|
60
|
+
if (markup) {
|
|
61
|
+
inline += src.match(INLINE)?.length ?? 0;
|
|
62
|
+
classes += src.match(CLASSNAME)?.length ?? 0;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (media)
|
|
66
|
+
return { kind: "media", file: media };
|
|
67
|
+
if (js)
|
|
68
|
+
return { kind: "js", file: js };
|
|
69
|
+
return { kind: "none", inline: inline > classes && componentSheets === 0 };
|
|
70
|
+
}
|
|
71
|
+
/** A frase para o bloco do agente - uma, curta, com o fato medido e o que fazer. */
|
|
72
|
+
export function responsiveLine(form) {
|
|
73
|
+
switch (form.kind) {
|
|
74
|
+
case "tailwind":
|
|
75
|
+
return "**This project makes screens responsive with Tailwind prefixes** (`md:`, `lg:`). Use the same prefixes for a breakpoint - never a new stylesheet or a `matchMedia` hook for it.";
|
|
76
|
+
case "media":
|
|
77
|
+
return `**This project makes screens responsive with \`@media\` in its stylesheets** - see \`${form.file}\`. Put a new breakpoint in the stylesheet of the component that needs it, the same way.`;
|
|
78
|
+
case "js":
|
|
79
|
+
return `**This project makes screens responsive in code**, with \`matchMedia\` - see \`${form.file}\`. Reuse that for a new breakpoint instead of adding a stylesheet.`;
|
|
80
|
+
case "none":
|
|
81
|
+
return form.inline
|
|
82
|
+
? "**This project has no breakpoint yet, and it styles inline** (`style={{}}`). If a screen needs one, keep the form: a small `matchMedia` hook next to the component, not a new stylesheet - and say that you added it."
|
|
83
|
+
: "**This project has no breakpoint yet.** If a screen needs one, add it in the form this project already styles with, and say that you added it.";
|
|
84
|
+
}
|
|
85
|
+
}
|
package/package.json
CHANGED