synthesisui 0.16.484 → 0.16.487
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/claude-md.js +11 -4
- package/dist/commands/add.js +69 -45
- package/dist/commands/component.js +38 -0
- package/dist/commands/doctor.js +3 -3
- package/dist/commands/hook.js +60 -5
- package/dist/commands/import.js +95 -4
- package/dist/commands/init.js +10 -0
- package/dist/doctor/architecture.js +4 -1
- package/dist/doctor/component-gate.js +14 -0
- package/dist/doctor/component-of.js +68 -0
- package/dist/doctor/components-scan.js +9 -0
- package/dist/doctor/other-frameworks.js +26 -0
- package/dist/doctor/scan.js +82 -3
- package/dist/doctor/scheme-scope.js +39 -1
- package/dist/doctor/sheet-order.js +99 -0
- package/dist/doctor/ts-tokens-lens.js +143 -21
- package/dist/install-marks.js +26 -2
- package/package.json +1 -1
package/dist/claude-md.js
CHANGED
|
@@ -455,12 +455,19 @@ and task, call it once - an empty result means there is nothing more to retrieve
|
|
|
455
455
|
tells you what was decided and why it matters; **the file on disk is still the authority for what is
|
|
456
456
|
written now** - read the code before you edit it, never a remembered signature.`
|
|
457
457
|
: "";
|
|
458
|
+
/**
|
|
459
|
+
* O TEMA É DELE - bateria ampliada, item 22 (29/09). Em c01, c02, c05, c17 e c23 o agente mexeu no tema do projeto
|
|
460
|
+
* sem perguntar: `--text-xl` no globals.css para "casar com a escala", 17 valores no tailwind.config.js, três cores
|
|
461
|
+
* de status no custom.scss, um `app/tokens.css` com os nomes `--ds-*` da plataforma. Cada um parecia ajudar, e cada
|
|
462
|
+
* um é uma decisão da pessoa tomada por outra pessoa.
|
|
463
|
+
*/
|
|
464
|
+
const theirsLine = "\n\n**The project's theme is the person's.** Do not add, change or remove a token, a theme config value (tailwind.config, theme.ts, uno.config) or the stylesheet that declares them unless they ask. A value you need and the theme does not have is ONE question with your proposal - not an edit.";
|
|
458
465
|
const rule = recall.available
|
|
459
466
|
? onlyAdopted
|
|
460
467
|
? `**These are true without asking anyone:** use the project's OWN custom properties, exactly as
|
|
461
468
|
this system declares them. No raw colours, spacings or radii that a token already covers, and never a
|
|
462
469
|
new token invented in silence - say so instead, because a new token is a decision for a person. There
|
|
463
|
-
is no component index for an adopted system: the tokens ARE the contract
|
|
470
|
+
is no component index for an adopted system: the tokens ARE the contract.${theirsLine}
|
|
464
471
|
|
|
465
472
|
**What the vocabulary IS is not written here.** Fetch only the piece the task requires:
|
|
466
473
|
\`system_doctrine\` for the rules and the voice, \`find_token\` for a value you are about to write.${memoryLine}${selfCheck}`
|
|
@@ -472,7 +479,7 @@ is no component index for an adopted system: the tokens ARE the contract.
|
|
|
472
479
|
- To override a style a component already sets, use this system's semantic role class - never \`!\`.
|
|
473
480
|
If the override is ignored, regenerate that component: older ones predate the resolver.
|
|
474
481
|
- Motion is selection, not improvisation. The base is quiet: nothing moves until a person asks, and
|
|
475
|
-
what moves comes from this system's own vocabulary - never hand-rolled \`@keyframes\` or raw durations
|
|
482
|
+
what moves comes from this system's own vocabulary - never hand-rolled \`@keyframes\` or raw durations.${theirsLine}
|
|
476
483
|
|
|
477
484
|
**When you are writing UI that belongs to one of the systems below, check its index first.** If an
|
|
478
485
|
entry covers the purpose, do not write it from scratch - materialise it:
|
|
@@ -492,11 +499,11 @@ If nothing covers it, say which indexed entry you considered and why it did not
|
|
|
492
499
|
project's OWN custom properties, exactly as the guide lists them. Do not write raw colours,
|
|
493
500
|
spacings or radii that a token already covers, and do not invent a new token silently - say so
|
|
494
501
|
instead, because a new token is a decision for a person to make. There is no component
|
|
495
|
-
manifest for an adopted system - the tokens ARE the contract.${selfCheck}`
|
|
502
|
+
manifest for an adopted system - the tokens ARE the contract.${theirsLine}${selfCheck}`
|
|
496
503
|
: `**When creating or editing components, read the system's GUIDE.md and follow it:** use the
|
|
497
504
|
vocabulary it lists - every name in it is a name THIS project already declares, written exactly as
|
|
498
505
|
you would write it. Do not use raw values outside the system's scale, and never a name from a
|
|
499
|
-
stylesheet this project does not have
|
|
506
|
+
stylesheet this project does not have.${theirsLine}
|
|
500
507
|
|
|
501
508
|
**Before creating any UI element, look it up in the manifest below.** If something there
|
|
502
509
|
already covers the purpose, do not write it from scratch - run
|
package/dist/commands/add.js
CHANGED
|
@@ -870,56 +870,80 @@ export async function add(slug, opts) {
|
|
|
870
870
|
* `fonts.ts`. Um arquivo compartilhado atravessaria a fronteira do workspace.
|
|
871
871
|
*/
|
|
872
872
|
const wroteIn = [];
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
const snippet = plan;
|
|
889
|
-
const header = [
|
|
890
|
-
`// Self-hosted type for the "${payload.slug}" design system (via next/font -`,
|
|
891
|
-
`// preloaded, no font flash). Generated by \`synthesisui add\`; edit freely.`,
|
|
892
|
-
];
|
|
893
|
-
await writeFile(fontsPath, `${[...header, ...snippet.fontsFile.slice(1)].join("\n")}\n`, "utf8");
|
|
894
|
-
wroteIn.push(`${dir}/fonts.ts`);
|
|
895
|
-
}
|
|
896
|
-
const wroteFonts = wroteIn.length > 0;
|
|
897
|
-
console.log("");
|
|
898
|
-
if (wroteFonts) {
|
|
899
|
-
console.log(line(`3. ✓ wrote ${wroteIn.join(", ")} - self-hosted type via next/font (preloaded, no font flash).`));
|
|
900
|
-
console.log(line(" Finish the wiring with two small edits:"));
|
|
901
|
-
}
|
|
902
|
-
else {
|
|
903
|
-
console.log(line(appDirFound
|
|
904
|
-
? `3. Load the type via next/font (${appDir}/fonts.ts already exists - left untouched; it should export:)`
|
|
905
|
-
: /**
|
|
906
|
-
* A FRASE DIZIA "already exists" PARA DOIS ESTADOS DIFERENTES, e um deles é o contrário
|
|
907
|
-
* disso: não achei a pasta do app. No monorepo dele o arquivo não existia e o terminal
|
|
908
|
-
* dizia que existia - o cliente não tem como saber que os caminhos acima são chute.
|
|
909
|
-
*/
|
|
910
|
-
`3. Load the type via next/font (I could not find your app folder from here, so the paths above are examples - create ${appDir}/fonts.ts wherever your app router lives):`));
|
|
873
|
+
/**
|
|
874
|
+
* NO IMPORT, NENHUM ARQUIVO MORTO - bateria ampliada, item 20 (29/09). O c01 declara Inter e Space Grotesk e não as
|
|
875
|
+
* carrega; o import escrevia `app/fonts.ts` e deixava "two small edits" que ninguém fez - um arquivo que nada
|
|
876
|
+
* importa, e as fontes do sistema sem aparecer na tela. No projeto de origem o código é dele: o passo diz em uma
|
|
877
|
+
* linha o que acontece hoje e mostra a ligação inteira, para ele fazer (ou pedir ao agente) se quiser.
|
|
878
|
+
*/
|
|
879
|
+
const named = [
|
|
880
|
+
...new Set(Object.values(families)
|
|
881
|
+
.filter((f) => typeof f === "string")
|
|
882
|
+
.map((f) => f.split(",")[0]?.trim().replace(/^["']|["']$/g, "") ?? "")
|
|
883
|
+
.filter(Boolean)),
|
|
884
|
+
];
|
|
885
|
+
if (opts.fromImport) {
|
|
886
|
+
console.log("");
|
|
887
|
+
console.log(line(`3. Your code names ${named.join(" and ") || "its fonts"} and nothing here loads ${named.length === 1 ? "it" : "them"} - today the text falls back to the browser's font. Nothing was written. To load ${named.length === 1 ? "it" : "them"}, these three edits do it:`));
|
|
911
888
|
console.log("");
|
|
912
889
|
console.log(snippet(nextFonts.fontsFile));
|
|
913
890
|
console.log("");
|
|
914
|
-
console.log(
|
|
891
|
+
console.log(snippet(nextFonts.layout));
|
|
892
|
+
console.log("");
|
|
893
|
+
console.log(snippet(nextFonts.css));
|
|
915
894
|
}
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
895
|
+
else {
|
|
896
|
+
for (const dir of appDirs.length > 0 ? appDirs : [appDir]) {
|
|
897
|
+
const fontsPath = join(projectRoot, ...dir.split("/"), "fonts.ts");
|
|
898
|
+
/** Arquivo dele que já existe nunca é reescrito - o setup impresso diz o que ele deve exportar. */
|
|
899
|
+
if (await exists(fontsPath))
|
|
900
|
+
continue;
|
|
901
|
+
if (!(await exists(join(projectRoot, ...dir.split("/")))))
|
|
902
|
+
continue;
|
|
903
|
+
const plan = nextFontSnippet({
|
|
904
|
+
families,
|
|
905
|
+
appDir: dir,
|
|
906
|
+
facts: fontFacts,
|
|
907
|
+
declaredWeights: payload.document.foundations.typography.weights,
|
|
908
|
+
});
|
|
909
|
+
if (!plan.ok)
|
|
910
|
+
continue;
|
|
911
|
+
const snippet = plan;
|
|
912
|
+
const header = [
|
|
913
|
+
`// Self-hosted type for the "${payload.slug}" design system (via next/font -`,
|
|
914
|
+
`// preloaded, no font flash). Generated by \`synthesisui add\`; edit freely.`,
|
|
915
|
+
];
|
|
916
|
+
await writeFile(fontsPath, `${[...header, ...snippet.fontsFile.slice(1)].join("\n")}\n`, "utf8");
|
|
917
|
+
wroteIn.push(`${dir}/fonts.ts`);
|
|
918
|
+
}
|
|
919
|
+
const wroteFonts = wroteIn.length > 0;
|
|
920
|
+
console.log("");
|
|
921
|
+
if (wroteFonts) {
|
|
922
|
+
console.log(line(`3. ✓ wrote ${wroteIn.join(", ")} - self-hosted type via next/font (preloaded, no font flash).`));
|
|
923
|
+
console.log(line(" Finish the wiring with two small edits:"));
|
|
924
|
+
}
|
|
925
|
+
else {
|
|
926
|
+
console.log(line(appDirFound
|
|
927
|
+
? `3. Load the type via next/font (${appDir}/fonts.ts already exists - left untouched; it should export:)`
|
|
928
|
+
: /**
|
|
929
|
+
* A FRASE DIZIA "already exists" PARA DOIS ESTADOS DIFERENTES, e um deles é o contrário
|
|
930
|
+
* disso: não achei a pasta do app. No monorepo dele o arquivo não existia e o terminal
|
|
931
|
+
* dizia que existia - o cliente não tem como saber que os caminhos acima são chute.
|
|
932
|
+
*/
|
|
933
|
+
`3. Load the type via next/font (I could not find your app folder from here, so the paths above are examples - create ${appDir}/fonts.ts wherever your app router lives):`));
|
|
934
|
+
console.log("");
|
|
935
|
+
console.log(snippet(nextFonts.fontsFile));
|
|
936
|
+
console.log("");
|
|
937
|
+
console.log(line(" Then finish the wiring:"));
|
|
938
|
+
}
|
|
939
|
+
console.log("");
|
|
940
|
+
console.log(snippet(nextFonts.layout));
|
|
921
941
|
console.log("");
|
|
922
|
-
console.log(
|
|
942
|
+
console.log(snippet(nextFonts.css));
|
|
943
|
+
if (fontsHref) {
|
|
944
|
+
console.log("");
|
|
945
|
+
console.log(line(` Quick alternative (works anywhere, may flash on cold loads): <link rel="stylesheet" href="${fontsHref}" /> in the <head>.`));
|
|
946
|
+
}
|
|
923
947
|
}
|
|
924
948
|
}
|
|
925
949
|
else if (fontsHref) {
|
|
@@ -77,6 +77,17 @@ export async function component(slug, name, opts) {
|
|
|
77
77
|
if (!SAFE_NAME.test(slug)) {
|
|
78
78
|
throw new RegistryError(`Invalid slug "${slug}".`);
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* FORA DE REACT, O COMANDO DIZ E PARA - bateria ampliada, item 15 (29/09). Em Astro, Svelte, Vue, Angular e num site
|
|
82
|
+
* HTML (c11 a c14, c28) ele escrevia um `.tsx` React, e o agente traduzia à mão: no Vue sobraram `cn.ts` e
|
|
83
|
+
* `sidebar.tsx` esquecidos no projeto. A promessa é React e Next; o que não cabe nela se diz antes de escrever.
|
|
84
|
+
*/
|
|
85
|
+
const other = await notReact(root);
|
|
86
|
+
if (other) {
|
|
87
|
+
console.log(`This project is written in ${other}, and \`synthesisui component\` writes React components, so nothing was written. Build it in ${other === "plain HTML" ? "your own HTML and CSS" : other} with the system's values: \`synthesisui use ${slug} "<what you want>"\` prints a prompt your agent can follow, and the MCP tools answer every value.`);
|
|
88
|
+
process.exitCode = 1;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
80
91
|
console.log(`→ fetching "${name}" from "${slug}" …`);
|
|
81
92
|
const res = await fetchComponent(base, slug, name, opts.version);
|
|
82
93
|
// The server should only ever return a kebab-case name, but never trust a
|
|
@@ -632,3 +643,30 @@ export async function component(slug, name, opts) {
|
|
|
632
643
|
}
|
|
633
644
|
console.log("");
|
|
634
645
|
}
|
|
646
|
+
/** O framework que não é React, pelo `package.json` - ou "plain HTML" quando não há `package.json` e há um `.html`. */
|
|
647
|
+
async function notReact(root) {
|
|
648
|
+
const raw = await readFile(join(root, "package.json"), "utf8").catch(() => null);
|
|
649
|
+
if (raw == null) {
|
|
650
|
+
const { readdir } = await import("node:fs/promises");
|
|
651
|
+
const names = await readdir(root).catch(() => []);
|
|
652
|
+
return names.some((n) => /\.html?$/i.test(n)) ? "plain HTML" : null;
|
|
653
|
+
}
|
|
654
|
+
try {
|
|
655
|
+
const pkg = JSON.parse(raw);
|
|
656
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
657
|
+
if (deps.react || deps.next || deps["react-dom"])
|
|
658
|
+
return null;
|
|
659
|
+
if (deps.nuxt || deps.vue)
|
|
660
|
+
return "Vue";
|
|
661
|
+
if (deps["@sveltejs/kit"] || deps.svelte)
|
|
662
|
+
return "Svelte";
|
|
663
|
+
if (deps.astro)
|
|
664
|
+
return "Astro";
|
|
665
|
+
if (deps["@angular/core"])
|
|
666
|
+
return "Angular";
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
catch {
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { readStyleSheet, scssVariables } from "../doctor/scss-lens.js";
|
|
3
|
-
import { tsTokenObjects, tsTokensAsVars } from "../doctor/ts-tokens-lens.js";
|
|
3
|
+
import { THEME_SOURCE, tsTokenObjects, tsTokensAsVars } from "../doctor/ts-tokens-lens.js";
|
|
4
4
|
import { dirname, join, relative, resolve } from "node:path";
|
|
5
5
|
import { describeIntent, intentOf, readProjectConfig } from "../config.js";
|
|
6
6
|
import { describeFix, planFix, readerFor, writeFix, } from "../doctor/apply-fix.js";
|
|
@@ -472,7 +472,7 @@ measured) {
|
|
|
472
472
|
/** E o objeto de tokens em TypeScript - ver `TokenTable.tsNames` (27/09). */
|
|
473
473
|
const tsNames = new Map();
|
|
474
474
|
for await (const file of walkAll(roots))
|
|
475
|
-
if (
|
|
475
|
+
if (THEME_SOURCE.test(file) && !/\.d\.ts$/.test(file))
|
|
476
476
|
for (const o of tsTokenObjects(await readFile(file, "utf8").catch(() => "")))
|
|
477
477
|
for (const [n, v] of o.values)
|
|
478
478
|
if (!tsNames.has(n))
|
|
@@ -774,7 +774,7 @@ export async function doctor(opts) {
|
|
|
774
774
|
/** Os objetos de tokens em TypeScript, para a lente da varredura - lidos da RAIZ, como o vocabulário. */
|
|
775
775
|
const tsObjects = [];
|
|
776
776
|
for await (const file of walkAll([root]))
|
|
777
|
-
if (
|
|
777
|
+
if (THEME_SOURCE.test(file) && !/\.d\.ts$/.test(file))
|
|
778
778
|
tsObjects.push(...tsTokenObjects(await readFile(file, "utf8").catch(() => "")));
|
|
779
779
|
const tally = emptyTally();
|
|
780
780
|
const internalSpecs = await internalSpecifiers(root);
|
package/dist/commands/hook.js
CHANGED
|
@@ -5,6 +5,7 @@ import { readAutoSync, startSync } from "../auto-sync.js";
|
|
|
5
5
|
import { changedSince } from "../changed-files.js";
|
|
6
6
|
import { emptyTally, exportedNames, internalSpecifiers, scanComponentsInto, tallyToInventory, } from "../doctor/components-scan.js";
|
|
7
7
|
import { checkContracts } from "../doctor/contract-check.js";
|
|
8
|
+
import { componentOf } from "../doctor/component-of.js";
|
|
8
9
|
import { appendEvent, ledgerPath } from "../doctor/ledger.js";
|
|
9
10
|
import { describeOffScale, diagnose, nameToWrite, scanSource } from "../doctor/scan.js";
|
|
10
11
|
import { declarados, falaDaPrateleira, naPrateleira } from "../doctor/shelf.js";
|
|
@@ -317,12 +318,16 @@ async function report(root, filePath, mode) {
|
|
|
317
318
|
// just flagged is the evidence the agent read the report and acted. Without
|
|
318
319
|
// the clean lines the record could only ever say "found", never "fixed",
|
|
319
320
|
// which is the half we could not answer when asked (28/07).
|
|
321
|
+
const component = componentOf(rel, documents);
|
|
320
322
|
await appendEvent(root, {
|
|
321
323
|
kind: "hook",
|
|
322
324
|
at: new Date().toISOString(),
|
|
323
325
|
file: rel,
|
|
324
326
|
named: named.length,
|
|
325
327
|
phantoms: phantoms.length,
|
|
328
|
+
unnamed: unnamed.length,
|
|
329
|
+
offScale: d.files.reduce((n, f) => n + (f.offScale?.length ?? 0), 0),
|
|
330
|
+
...(component ? { component } : {}),
|
|
326
331
|
});
|
|
327
332
|
/** O bloqueio de CONSUMIR é a única saída que recusa, e ele acompanha o relatório seja qual for. */
|
|
328
333
|
const block = mode === "consume" && composed.length > 0;
|
|
@@ -425,7 +430,14 @@ async function report(root, filePath, mode) {
|
|
|
425
430
|
/** O QUE FOI CORTADO É DITO - medido num `.css` real: 20 de 252, e as 232 sumiam caladas. */
|
|
426
431
|
...(unnamed.length > 20
|
|
427
432
|
? [` … and ${unnamed.length - 20} more in this file`]
|
|
428
|
-
: []), "", "This system declares no name for them, so there is nothing to replace them with.",
|
|
433
|
+
: []), "", "This system declares no name for them, so there is nothing to replace them with.",
|
|
434
|
+
/**
|
|
435
|
+
* UMA PERGUNTA COM A PROPOSTA, NÃO UMA LISTA PARA NOMEAR - bateria ampliada, item 18 (29/09). Em 17 dos 29
|
|
436
|
+
* projetos o turno terminava em "tell me what to call them" com 5 a 43 valores: a tela pronta e o trabalho
|
|
437
|
+
* aberto, esperando a pessoa inventar nomes. Quem escolhe continua sendo ela; o que muda é que ela recebe a
|
|
438
|
+
* sugestão pronta, no estilo que o projeto já usa, e responde uma vez.
|
|
439
|
+
*/
|
|
440
|
+
"Do not add a name on your own. For the values that repeat, propose names in this project's own style (next to the names it already declares) as ONE question the person answers with a yes - then add them and swap the values only after that yes. Leave one-off values as they are, and do not list every value back to the person.");
|
|
429
441
|
}
|
|
430
442
|
if (phantoms.length > 0) {
|
|
431
443
|
lines.push("", "Names this system does not declare. These look tokenized and apply nothing at all:", ...phantoms.slice(0, 20).map((p) => ` line ${p.line} ${p.name}`), "", "Use a name the system has, or say which value you need and what you would call it. Do NOT invent a token.");
|
|
@@ -613,9 +625,17 @@ round) {
|
|
|
613
625
|
if (src == null || !table)
|
|
614
626
|
continue;
|
|
615
627
|
measured += 1;
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
628
|
+
/**
|
|
629
|
+
* O QUE SAI DA ESCALA CONTA AQUI TAMBÉM - bateria ampliada, item 14 (29/09). No c06 o turno mudou 18 arquivos, e
|
|
630
|
+
* três deles tinham `@media (max-width: 767px)`, fora da escala (o md é 768). A régua de 28/09 achava; esta
|
|
631
|
+
* linha só somava os valores à mão, e o turno terminava em "checked too - nothing to name".
|
|
632
|
+
*/
|
|
633
|
+
const report = scanSource(rel, src, table);
|
|
634
|
+
const n = diagnose([report]).findings.length;
|
|
635
|
+
const off = report.offScale?.length ?? 0;
|
|
636
|
+
const what = [n > 0 ? `${n} by hand` : "", off > 0 ? `${off} off your scale` : ""].filter(Boolean).join(", ");
|
|
637
|
+
if (what)
|
|
638
|
+
dirty.push(`${rel} (${what})`);
|
|
619
639
|
}
|
|
620
640
|
const unmeasured = rest.length - measured;
|
|
621
641
|
said.push([
|
|
@@ -634,6 +654,34 @@ round) {
|
|
|
634
654
|
: "Run `npx synthesisui doctor` to see them, or write one of them again and this will check it.",
|
|
635
655
|
].join("\n"));
|
|
636
656
|
}
|
|
657
|
+
/**
|
|
658
|
+
* O QUE PASSOU DE PROPÓSITO, DITO UMA VEZ POR TURNO - a régua de 28/09 ("a checagem diz isso uma vez"), bateria
|
|
659
|
+
* ampliada, item 14. Sem esta linha, "nothing to name" ao lado de um `1px` ou de um `100dvh` parecia que a checagem
|
|
660
|
+
* não tinha visto, e o agente do c06 e do c18 foi conferir à mão. Só no fim do turno: um comando de shell não repete.
|
|
661
|
+
*/
|
|
662
|
+
// Só pega carona: um turno limpo continua calado - bloquear o fim do turno para dizer que passou seria ruído.
|
|
663
|
+
if (round === "turn" && changed.length > 0 && said.length > 0) {
|
|
664
|
+
const { table } = await loadSystem(root).catch(() => ({ table: null }));
|
|
665
|
+
const passes = new Map();
|
|
666
|
+
if (table)
|
|
667
|
+
for (const { rel } of changed.slice(0, REFUSAL_CEILING)) {
|
|
668
|
+
const src = await readFile(join(root, rel), "utf8").catch(() => null);
|
|
669
|
+
if (src == null)
|
|
670
|
+
continue;
|
|
671
|
+
for (const a of scanSource(rel, src, table).setAside ?? []) {
|
|
672
|
+
if (!/passes$|no token holds$|on your system's scale$/.test(a.reason))
|
|
673
|
+
continue;
|
|
674
|
+
const files = passes.get(a.reason) ?? new Set();
|
|
675
|
+
files.add(rel);
|
|
676
|
+
passes.set(a.reason, files);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (passes.size > 0)
|
|
680
|
+
said.push(`Passed on purpose, not missed: ${[...passes]
|
|
681
|
+
.slice(0, 6)
|
|
682
|
+
.map(([why, files]) => `${why} (${files.size} file${files.size === 1 ? "" : "s"})`)
|
|
683
|
+
.join("; ")}.`);
|
|
684
|
+
}
|
|
637
685
|
return { said, block };
|
|
638
686
|
}
|
|
639
687
|
/**
|
|
@@ -686,5 +734,12 @@ async function endOfTurn(root, input) {
|
|
|
686
734
|
await moveTurnClock(root);
|
|
687
735
|
out(said.length === 0
|
|
688
736
|
? pass()
|
|
689
|
-
: { decision: "block", reason: said.join("\n\n") });
|
|
737
|
+
: { decision: "block", reason: [...said, CLOSE_THE_TURN].join("\n\n") });
|
|
690
738
|
}
|
|
739
|
+
/**
|
|
740
|
+
* A ÚLTIMA MENSAGEM É O RESUMO, NÃO O RODAPÉ - bateria ampliada, item 19 (29/09). Este bloqueio faz o agente responder
|
|
741
|
+
* mais uma vez, e essa resposta vira a última coisa que a pessoa lê: em quase todos os 30 projetos, um turno de três a
|
|
742
|
+
* cinco minutos terminava numa nota sobre a checagem ("One more note from the style checker…"), e o que foi feito
|
|
743
|
+
* ficava no meio da conversa.
|
|
744
|
+
*/
|
|
745
|
+
export const CLOSE_THE_TURN = "Answer this in a line or two, then END your reply with the summary of what this turn did for the person - that last message is the one they read.";
|
package/dist/commands/import.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { importMark, writeSyncMark } from "../last-sync.js";
|
|
3
3
|
import { readStyleSheet } from "../doctor/scss-lens.js";
|
|
4
|
-
import { tsTokenObjects, tsTokensAsVars } from "../doctor/ts-tokens-lens.js";
|
|
4
|
+
import { THEME_SOURCE, tsTokenObjects, tsTokensAsVars } from "../doctor/ts-tokens-lens.js";
|
|
5
5
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { anatomyFromSketch } from "../anatomy-from-sketch.js";
|
|
7
7
|
import { applyAnatomyPatch, hasEdits, } from "../anatomy-patch.js";
|
|
@@ -36,6 +36,9 @@ import { aliasesOf, describeReachability, edgesIn, reachabilityOf, } from "../do
|
|
|
36
36
|
import { rootGroundOf } from "../doctor/root-ground.js";
|
|
37
37
|
import { diagnose, scanSource } from "../doctor/scan.js";
|
|
38
38
|
import { parseSchemeBlocks } from "../doctor/scheme-blocks.js";
|
|
39
|
+
import { attributeFaces } from "../doctor/scheme-scope.js";
|
|
40
|
+
import { cssImportLists, resolveSheetConflicts, unloadedSheets } from "../doctor/sheet-order.js";
|
|
41
|
+
import { unreadComponentFiles } from "../doctor/other-frameworks.js";
|
|
39
42
|
import { describeSignals, emptySignals, finishSignals, readSignalsInto, } from "../doctor/signals.js";
|
|
40
43
|
import { sketchOf } from "../doctor/sketch.js";
|
|
41
44
|
import { describeForeign, readStyleIslands, } from "../doctor/style-island.js";
|
|
@@ -282,6 +285,34 @@ alsoGlobalsFrom) {
|
|
|
282
285
|
const body = await readStyleSheet(file);
|
|
283
286
|
sheets.push({ file, body });
|
|
284
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* O QUE UMA FOLHA DELE IMPORTA DE DENTRO DE UMA PASTA QUE A VARREDURA PULA - bateria ampliada, 29/09.
|
|
290
|
+
*
|
|
291
|
+
* O Style Dictionary gera `build/css/variables.css` e o `globals.css` o importa. A varredura pula `build/`
|
|
292
|
+
* (lá costuma morar saída de build, e ler aquilo seria ler o bundle), então as 14 variáveis dele nunca
|
|
293
|
+
* entravam, e o import dizia "your code names no token yet" a quem tinha o sistema mais organizado do kit.
|
|
294
|
+
*
|
|
295
|
+
* A pasta continua pulada; o que muda é que uma folha que ELE importa entra, porque o `@import` é a palavra
|
|
296
|
+
* dele dizendo que aquilo faz parte do CSS dele. Só dentro do escopo medido.
|
|
297
|
+
*/
|
|
298
|
+
const lidas = new Set(sheets.map((s) => s.file));
|
|
299
|
+
for (let i = 0; i < sheets.length; i++) {
|
|
300
|
+
for (const spec of sheetImports(sheets[i].body)) {
|
|
301
|
+
for (const candidate of partialCandidates(spec)) {
|
|
302
|
+
const alvo = join(dirname(sheets[i].file), candidate);
|
|
303
|
+
if (lidas.has(alvo))
|
|
304
|
+
break;
|
|
305
|
+
if (!alvo.startsWith(`${root}${sep}`))
|
|
306
|
+
break;
|
|
307
|
+
const body = await readStyleSheet(alvo);
|
|
308
|
+
if (!body)
|
|
309
|
+
continue;
|
|
310
|
+
lidas.add(alvo);
|
|
311
|
+
sheets.push({ file: alvo, body });
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
285
316
|
/**
|
|
286
317
|
* AS FOLHAS DO RESTO DO REPOSITÓRIO - lidas com a MESMA régua, e nunca com uma segunda.
|
|
287
318
|
* `deForaDoEscopo` marca quais são, porque elas só podem virar classe global: nem `css`, nem
|
|
@@ -564,13 +595,45 @@ export async function takeCensus(root, opts) {
|
|
|
564
595
|
*/
|
|
565
596
|
const tsObjects = [];
|
|
566
597
|
for await (const file of walkAll([root]))
|
|
567
|
-
if (
|
|
598
|
+
if (THEME_SOURCE.test(file) && !/\.d\.ts$/.test(file))
|
|
568
599
|
tsObjects.push(...tsTokenObjects(await readFile(file, "utf8").catch(() => "")));
|
|
569
600
|
const tsRoot = tsObjects
|
|
570
601
|
.flatMap((o) => [...o.values].map(([n, v]) => `${n}: ${v};`))
|
|
571
602
|
.join(" ");
|
|
572
|
-
|
|
573
|
-
|
|
603
|
+
/**
|
|
604
|
+
* O MESMO TOKEN EM DUAS FOLHAS: fica o que a TELA mostra - ver `sheet-order.ts` (bateria ampliada, c20). O leitor de
|
|
605
|
+
* raiz fica com a primeira declaração; aqui a folha que o código importa por último vem primeiro, só para os tokens.
|
|
606
|
+
* As classes continuam lendo `css` na ordem de sempre.
|
|
607
|
+
*/
|
|
608
|
+
const codeFiles = [];
|
|
609
|
+
const citingFiles = [];
|
|
610
|
+
for await (const file of walkAll([root])) {
|
|
611
|
+
if (/\.(tsx|ts|jsx|js|mjs)$/.test(file) && !/\.d\.ts$/.test(file))
|
|
612
|
+
codeFiles.push(file);
|
|
613
|
+
if (/\.(tsx|ts|jsx|js|mjs|cjs|vue|svelte|astro|html|json)$/.test(file) && !/\.d\.ts$/.test(file))
|
|
614
|
+
citingFiles.push(file);
|
|
615
|
+
}
|
|
616
|
+
// A varredura não entra em `.json` nem em `.html`, e é na RAIZ que eles dizem que folhas carregam: `angular.json`,
|
|
617
|
+
// `index.html` (Vite, site estático). Sem isto o `styles.scss` do Angular parecia não carregado.
|
|
618
|
+
for (const name of await readdir(root).catch(() => []))
|
|
619
|
+
if (/\.(json|html)$/.test(name) && name !== "package-lock.json")
|
|
620
|
+
citingFiles.push(join(root, name));
|
|
621
|
+
const sheetsInScope = globalSheets
|
|
622
|
+
.filter((g) => !g.outside)
|
|
623
|
+
.map((g) => ({ file: resolve(root, g.file), body: g.body }));
|
|
624
|
+
/** A folha que ninguém carrega não dá token - ver `unloadedSheets` (bateria ampliada, c17 e c18). */
|
|
625
|
+
const unloaded = new Set(await unloadedSheets(sheetsInScope, citingFiles));
|
|
626
|
+
const loaded = sheetsInScope.filter((g) => !unloaded.has(g.file));
|
|
627
|
+
const { ordered, conflicts: declaredConflicts } = resolveSheetConflicts(loaded, await cssImportLists(codeFiles));
|
|
628
|
+
const tokenCss = unloaded.size > 0 || declaredConflicts.some((c) => c.decidedBy) ? ordered.map((g) => g.body).join("\n") : css;
|
|
629
|
+
const declaredUnloaded = [...unloaded];
|
|
630
|
+
/** Componentes que esta versão não lê (Vue, Svelte, Astro, Angular) - ver `other-frameworks.ts`. */
|
|
631
|
+
const everyFile = (await readdir(root, { recursive: true }).catch(() => []))
|
|
632
|
+
.filter((f) => !/(^|[/\\])(node_modules|\.git|\.next|\.nuxt|\.svelte-kit|\.astro|\.angular|dist|build|_synthesisui)([/\\]|$)/.test(f))
|
|
633
|
+
.map((f) => join(root, f));
|
|
634
|
+
const unreadComponents = await unreadComponentFiles(everyFile);
|
|
635
|
+
const table = buildTable({ css: tsRoot ? `${tokenCss}\n:root { ${tsRoot} }` : tokenCss, source: "yours" });
|
|
636
|
+
const schemes = parseSchemeBlocks(tokenCss);
|
|
574
637
|
const reports = [];
|
|
575
638
|
const tally = emptyTally();
|
|
576
639
|
const internal = await internalSpecifiers(root);
|
|
@@ -2469,6 +2532,7 @@ export async function takeCensus(root, opts) {
|
|
|
2469
2532
|
globalSheets
|
|
2470
2533
|
.filter((g) => !g.outside)
|
|
2471
2534
|
.map((g) => ({ file: g.file, css: g.body })));
|
|
2535
|
+
const declaredBrands = attributeFaces(css, new Set(schemes.base.keys()));
|
|
2472
2536
|
const declaredSchemes = {
|
|
2473
2537
|
base: Object.fromEntries(schemes.base),
|
|
2474
2538
|
light: Object.fromEntries(schemes.light),
|
|
@@ -2761,6 +2825,10 @@ export async function takeCensus(root, opts) {
|
|
|
2761
2825
|
? { declaredAlt: Object.fromEntries(schemes.dark) }
|
|
2762
2826
|
: {}),
|
|
2763
2827
|
declaredSchemes,
|
|
2828
|
+
...(declaredBrands.length > 0 ? { declaredBrands } : {}),
|
|
2829
|
+
...(declaredConflicts.length > 0 ? { declaredConflicts } : {}),
|
|
2830
|
+
...(declaredUnloaded.length > 0 ? { declaredUnloaded } : {}),
|
|
2831
|
+
...(unreadComponents.length > 0 ? { unreadComponents } : {}),
|
|
2764
2832
|
observed: distinctValues(d),
|
|
2765
2833
|
...(components.length > 0 ? { components } : {}),
|
|
2766
2834
|
...(defined.length > 0 ? { defined } : {}),
|
|
@@ -3944,6 +4012,11 @@ export function defaultScheme(declared) {
|
|
|
3944
4012
|
* transcrito em vez de serem reescritas: a régua que as produziu é a mesma, então a tela nunca
|
|
3945
4013
|
* diverge do relatório.
|
|
3946
4014
|
*/
|
|
4015
|
+
/** O caminho a partir da pasta do projeto, que é como ele o reconhece. */
|
|
4016
|
+
function relativeish(file) {
|
|
4017
|
+
const cwd = process.cwd();
|
|
4018
|
+
return file.startsWith(`${cwd}${sep}`) ? file.slice(cwd.length + 1) : file;
|
|
4019
|
+
}
|
|
3947
4020
|
function mostrarLeitura(c, secoes, titulos = DECIDEM) {
|
|
3948
4021
|
const valores = c.values?.classes;
|
|
3949
4022
|
const decisoes = valores ? valores.seen - valores.structure : 0;
|
|
@@ -3956,6 +4029,22 @@ function mostrarLeitura(c, secoes, titulos = DECIDEM) {
|
|
|
3956
4029
|
console.log(body(`${paint.strong(`${lidos} of ${total}`)} components came out with a blueprint${pct === null
|
|
3957
4030
|
? ""
|
|
3958
4031
|
: `, and ${paint.strong(`${pct}%`)} of your ${decisoes} style decisions carry your own vocabulary`}.`));
|
|
4032
|
+
for (const u of c.unreadComponents ?? []) {
|
|
4033
|
+
const ext = { Vue: ".vue", Svelte: ".svelte", Astro: ".astro", Angular: "Angular" }[u.framework] ?? u.framework;
|
|
4034
|
+
console.log(body(`Your components are written in ${u.framework} (${u.files} ${ext === "Angular" ? "Angular component" : ext} file${u.files === 1 ? "" : "s"}), and this version reads components written in React only. Your colours, spacing and type came in; your components did not - the system starts without them.`));
|
|
4035
|
+
}
|
|
4036
|
+
for (const f of c.declaredUnloaded ?? [])
|
|
4037
|
+
console.log(body(`${relativeish(f)} declares variables, and nothing in this project loads it - no import, no link, no config. They were left out of this system.`));
|
|
4038
|
+
for (const k of c.declaredConflicts ?? []) {
|
|
4039
|
+
const onde = k.values.map((v) => `${v.value} in ${relativeish(v.file)}`).join(", ");
|
|
4040
|
+
console.log(body(k.decidedBy
|
|
4041
|
+
? `${k.name} is declared twice (${onde}). Your screen shows ${k.shown}, the one ${relativeish(k.decidedBy)} imports last, and that is the value this system keeps.`
|
|
4042
|
+
: `${k.name} is declared twice (${onde}), and nothing here says which one loads last. This system keeps ${k.shown} - check which one your screen shows.`));
|
|
4043
|
+
}
|
|
4044
|
+
if (c.declaredBrands?.length) {
|
|
4045
|
+
const outras = c.declaredBrands.map((b) => b.selector).join(", ");
|
|
4046
|
+
console.log(body(`Your code names ${c.declaredBrands.length === 1 ? "one more brand" : `${c.declaredBrands.length} more brands`} with the same names (${outras}). This system is the one in :root; ${c.declaredBrands.length === 1 ? "the other was" : "the others were"} read and left out, not mixed in.`));
|
|
4047
|
+
}
|
|
3959
4048
|
/**
|
|
3960
4049
|
* TODAS as ocorrências, e não a primeira: um import com dois escopos declara a organização de
|
|
3961
4050
|
* CADA um, e `find` mostraria `packages/ui` e engoliria `packages/charts` - a segunda lacuna
|
|
@@ -5364,6 +5453,8 @@ export async function runImport(opts) {
|
|
|
5364
5453
|
* `import-installs.spec.ts` provava que a chamada existe e não que ela leva o que precisa.
|
|
5365
5454
|
*/
|
|
5366
5455
|
cli: opts.cli,
|
|
5456
|
+
/** O projeto é o de origem: nenhum `fonts.ts` morto no código dele - ver `AddOptions.fromImport`. */
|
|
5457
|
+
fromImport: true,
|
|
5367
5458
|
});
|
|
5368
5459
|
}
|
|
5369
5460
|
catch (error) {
|
package/dist/commands/init.js
CHANGED
|
@@ -6,6 +6,8 @@ import { readFile } from "node:fs/promises";
|
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { readProjectTheme, writeBesideTheirs, writeNewProjectRoot, writeNewProjectTheme } from "../new-project-theme.js";
|
|
8
8
|
import { add } from "./add.js";
|
|
9
|
+
import { resolveRegistry } from "../config.js";
|
|
10
|
+
import { fetchDesignSystem } from "../registry.js";
|
|
9
11
|
const whereOf = (answer) => answer === "file" || answer === "1"
|
|
10
12
|
? "file"
|
|
11
13
|
: answer === "separate" || answer === "2"
|
|
@@ -48,6 +50,14 @@ async function askWhere(tokensFile, separate, entry, intro) {
|
|
|
48
50
|
*/
|
|
49
51
|
export async function init(opts) {
|
|
50
52
|
const root = opts.dir ?? process.cwd();
|
|
53
|
+
/**
|
|
54
|
+
* O SISTEMA EXISTE ANTES DE QUALQUER ARQUIVO - bateria ampliada, item 21 (29/09). `init --ds teste-nunca-existiu`
|
|
55
|
+
* recusava bem ("No design system ... that this machine can see"), mas só depois de escrever `_synthesisui/config.json`,
|
|
56
|
+
* o `ledger.jsonl` e a statusline: o projeto ficava com o rastro de uma instalação que não aconteceu. A pergunta
|
|
57
|
+
* vai ao registry primeiro; a recusa é a mesma, e agora nada fica para trás.
|
|
58
|
+
*/
|
|
59
|
+
if (opts.ds)
|
|
60
|
+
await fetchDesignSystem(resolveRegistry(opts.registry), opts.ds);
|
|
51
61
|
const target = opts.target === "general" ? "general" : "next";
|
|
52
62
|
const config = {
|
|
53
63
|
target,
|
|
@@ -72,8 +72,11 @@ const SHAPES = [
|
|
|
72
72
|
* Next (app e pages) são do framework: dizem onde a tela mora, não onde um componente nasce.
|
|
73
73
|
*/
|
|
74
74
|
const ROUTE_FILE = /(^|\/)(page|layout|loading|error|global-error|not-found|template|default|route|head|_app|_document|_error)\.(tsx|jsx|ts|js)$/i;
|
|
75
|
+
/** E todo arquivo de `pages/` do pages router - bateria ampliada, c30: `index.tsx` e `about.tsx` são rotas, não
|
|
76
|
+
* componentes "ao lado da rota". */
|
|
77
|
+
const PAGES_DIR = /^(src\/)?pages\//i;
|
|
75
78
|
export function countsAsOrganization(rel) {
|
|
76
|
-
return /\.(tsx|jsx|vue|svelte)$/i.test(rel) && !ROUTE_FILE.test(rel);
|
|
79
|
+
return /\.(tsx|jsx|vue|svelte)$/i.test(rel) && !ROUTE_FILE.test(rel) && !PAGES_DIR.test(rel.replace(/\\/g, "/"));
|
|
77
80
|
}
|
|
78
81
|
/**
|
|
79
82
|
* O MESMO NOME, MÓDULO CAIXA E SEPARADOR - a única normalização deste arquivo.
|
|
@@ -30,6 +30,8 @@ import { localOnlyNames } from "./components-scan.js";
|
|
|
30
30
|
* so a file called `page.tsx` IS a route in every Next codebase there is.
|
|
31
31
|
*/
|
|
32
32
|
const RESERVED_FILE = /(^|[/\\])(page|layout|template|default|error|global-error|not-found|loading|route|middleware|instrumentation|sitemap|robots|opengraph-image|icon|apple-icon|manifest)\.[jt]sx?$/;
|
|
33
|
+
/** `pages/…` na raiz ou em `src/` - o roteador por arquivo do Next antigo (e as telas de um app Vite). */
|
|
34
|
+
const PAGES_ROUTER = /^(src[/\\])?pages[/\\].+\.[jt]sx?$/;
|
|
33
35
|
/** Nuxt, SvelteKit and Remix equivalents, for the same exact reason. */
|
|
34
36
|
const RESERVED_FILE_OTHER = /(^|[/\\])(\+page|\+layout|\+server|_app|_document|_layout)\.[a-z]+$/;
|
|
35
37
|
/**
|
|
@@ -229,6 +231,18 @@ export function gateComponent(input) {
|
|
|
229
231
|
because: `\`${file}\` is the root the app mounts - the whole screen, not a component somebody can import`,
|
|
230
232
|
};
|
|
231
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* O PAGES ROUTER - bateria ampliada, c30 (29/09). No Next antigo cada arquivo de `pages/` É uma rota
|
|
236
|
+
* (`pages/index.tsx` é `/`), e o nome dele não é reservado: o census contava 0 páginas, e o import chamava
|
|
237
|
+
* `pages/` de "componentes ao lado da rota". Em um app Vite, `src/pages/Home.tsx` também é uma tela inteira.
|
|
238
|
+
*/
|
|
239
|
+
if (PAGES_ROUTER.test(file)) {
|
|
240
|
+
return {
|
|
241
|
+
ok: false,
|
|
242
|
+
why: "route",
|
|
243
|
+
because: `\`${file}\` lives in pages/ - each file there is a route, not a component somebody can import`,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
232
246
|
if (RESERVED_FILE.test(file) || RESERVED_FILE_OTHER.test(file)) {
|
|
233
247
|
return {
|
|
234
248
|
ok: false,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { basename, dirname } from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* DE QUAL COMPONENTE É ESTE ARQUIVO - a ponte que o documento já tem, e nenhuma outra (E01, 30/09).
|
|
4
|
+
*
|
|
5
|
+
* A receita diz onde o componente mora (`components.<Nome>.source.file`), escrito pela leitura do
|
|
6
|
+
* import. O arquivo que a checagem olha pode ser esse mesmo, ou o estilo ao lado dele:
|
|
7
|
+
* `components/Card.module.css` é do `Card` cujo arquivo é `components/Card.tsx` - mesma pasta, mesmo
|
|
8
|
+
* nome antes do primeiro ponto.
|
|
9
|
+
*
|
|
10
|
+
* E A PASTA DO COMPONENTE É DELE (medido no `codelevel` em 30/09, na E03): `packages/ui/src/CourseCover/motion.ts` é
|
|
11
|
+
* do `course-cover`, porque a pasta tem o nome do arquivo do componente. `components/`, que é de todos, não vira de
|
|
12
|
+
* ninguém: ali só o arquivo e o estilo com o mesmo nome.
|
|
13
|
+
*
|
|
14
|
+
* DUAS RECEITAS NO MESMO ARQUIVO: fica a que tem o nome dele. O `codelevel` tem `course-cover` e `covers` em
|
|
15
|
+
* `CourseCover.tsx`, e a regra antiga ("duas reclamam, nenhuma") deixava sem componente as 21 checagens do
|
|
16
|
+
* componente mais checado do sistema. Se nenhuma tem o nome, continua nenhuma.
|
|
17
|
+
*
|
|
18
|
+
* SEM RECEITA, SEM COMPONENTE. Um `app/page.module.css` que nenhuma receita reclama não vira "Page":
|
|
19
|
+
* o palpite pelo nome contaria checagens para um componente que não existe no sistema dele, e é
|
|
20
|
+
* dessa contagem que os degraus do jogo vão sair. E se duas receitas diferentes reclamam o mesmo
|
|
21
|
+
* arquivo, também não há resposta - escolher uma seria o mesmo palpite com outra roupa.
|
|
22
|
+
*/
|
|
23
|
+
export function componentOf(file, documents) {
|
|
24
|
+
const exact = new Map();
|
|
25
|
+
const sibling = new Map();
|
|
26
|
+
const folder = new Map();
|
|
27
|
+
const dir = dirname(file);
|
|
28
|
+
const stem = stemOf(file);
|
|
29
|
+
for (const doc of documents) {
|
|
30
|
+
const components = doc?.components;
|
|
31
|
+
if (!components || typeof components !== "object")
|
|
32
|
+
continue;
|
|
33
|
+
for (const [name, recipe] of Object.entries(components)) {
|
|
34
|
+
const at = recipe?.source
|
|
35
|
+
?.file;
|
|
36
|
+
if (typeof at !== "string" || at === "")
|
|
37
|
+
continue;
|
|
38
|
+
if (at === file)
|
|
39
|
+
exact.set(name, at);
|
|
40
|
+
else if (dirname(at) !== dir)
|
|
41
|
+
continue;
|
|
42
|
+
else if (stemOf(at) === stem)
|
|
43
|
+
sibling.set(name, at);
|
|
44
|
+
else if (basename(dir) === stemOf(at))
|
|
45
|
+
folder.set(name, at);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const found = [exact, sibling, folder].find((m) => m.size > 0);
|
|
49
|
+
if (!found)
|
|
50
|
+
return undefined;
|
|
51
|
+
if (found.size === 1)
|
|
52
|
+
return [...found.keys()][0];
|
|
53
|
+
const named = [...found].filter(([name, at]) => name === kebab(stemOf(at)));
|
|
54
|
+
return named.length === 1 ? named[0]?.[0] : undefined;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* `CourseCover` -> `course-cover`: a chave que a plataforma dá à receita - cópia de `contractName`
|
|
58
|
+
* (apps/web/src/lib/ds/exclusive-contract.ts), porque o CLI é publicado sozinho e não importa de lá.
|
|
59
|
+
*/
|
|
60
|
+
const kebab = (name) => name
|
|
61
|
+
.split(".")
|
|
62
|
+
.join("-")
|
|
63
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
64
|
+
.toLowerCase()
|
|
65
|
+
.replace(/[^a-z0-9-]/g, "-")
|
|
66
|
+
.replace(/-+/g, "-")
|
|
67
|
+
.replace(/^-|-$/g, "");
|
|
68
|
+
const stemOf = (path) => basename(path).split(".")[0] ?? "";
|
|
@@ -348,6 +348,15 @@ internal = []) {
|
|
|
348
348
|
return;
|
|
349
349
|
const owners = importedNames(source, internal);
|
|
350
350
|
const private_ = localOnlyNames(source);
|
|
351
|
+
/**
|
|
352
|
+
* `function App({ Component, pageProps })` - A TELA QUE CHEGA COMO PROP (bateria ampliada, c30, 29/09). O `_app.tsx`
|
|
353
|
+
* do pages router renderiza `<Component {...pageProps} />`, e `Component` entrava no sistema como um componente
|
|
354
|
+
* dele. Um nome com maiúscula que o arquivo recebe como parâmetro desestruturado é fiação, igual a um local privado.
|
|
355
|
+
*/
|
|
356
|
+
for (const m of source.matchAll(/\(\s*\{([^}]*)\}\s*(?::[^)]*)?\)/g))
|
|
357
|
+
for (const part of m[1].split(","))
|
|
358
|
+
for (const id of /^\s*(?:[\w$]+\s*:\s*)?([A-Z][\w$]*)\b/.exec(part)?.slice(1) ?? [])
|
|
359
|
+
private_.add(id);
|
|
351
360
|
/**
|
|
352
361
|
* Abertura por regex, corpo por SCANNER - ver `bodyOf`. A regex só encontra `<Nome`; onde a
|
|
353
362
|
* tag termina é uma pergunta que só um scanner com estado responde.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
export async function unreadComponentFiles(files) {
|
|
3
|
+
const count = { Vue: 0, Svelte: 0, Astro: 0, Angular: 0 };
|
|
4
|
+
for (const f of files) {
|
|
5
|
+
if (/\.vue$/i.test(f)) {
|
|
6
|
+
if (!/(^|\/)(pages|layouts)\//.test(f) && !/(^|\/)app\.vue$/i.test(f))
|
|
7
|
+
count.Vue += 1;
|
|
8
|
+
}
|
|
9
|
+
else if (/\.svelte$/i.test(f) &&
|
|
10
|
+
!/(^|\/)\+(page|layout|error)\.svelte$/.test(f))
|
|
11
|
+
count.Svelte += 1;
|
|
12
|
+
else if (/\.astro$/i.test(f) && !/(^|\/)pages\//.test(f))
|
|
13
|
+
count.Astro += 1;
|
|
14
|
+
else if (/\.ts$/i.test(f) && !/\.(spec|test)\.ts$/.test(f)) {
|
|
15
|
+
const src = await readFile(f, "utf8").catch(() => "");
|
|
16
|
+
const root = /(^|\/)app(\.component)?\.ts$/.test(f);
|
|
17
|
+
if (!root &&
|
|
18
|
+
/@Component\s*\(\s*\{/.test(src) &&
|
|
19
|
+
/from\s+["']@angular\/core["']/.test(src))
|
|
20
|
+
count.Angular += 1;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return Object.entries(count)
|
|
24
|
+
.filter(([, n]) => n > 0)
|
|
25
|
+
.map(([framework, n]) => ({ framework, files: n }));
|
|
26
|
+
}
|
package/dist/doctor/scan.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { propertyAt, roleFits } from "./their-names.js";
|
|
13
13
|
import { utilitiesOn } from "./idiom-names.js";
|
|
14
14
|
import { normalizeValue, tokenMatch } from "./tokens.js";
|
|
15
|
+
import { THEME_SOURCE, tsTokenObjects } from "./ts-tokens-lens.js";
|
|
15
16
|
/**
|
|
16
17
|
* O NOME QUE SE ESCREVE NO ARQUIVO DELE - e ele é SEMPRE do vocabulário dele (`INV-GERAL-13`).
|
|
17
18
|
*
|
|
@@ -488,7 +489,74 @@ export function scanSource(file, source, table) {
|
|
|
488
489
|
: null),
|
|
489
490
|
};
|
|
490
491
|
}
|
|
491
|
-
return scanCore(file, source, table);
|
|
492
|
+
return stretchedStack(file, source, themeDefinitionsAside(file, source, scanCore(file, source, table)));
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* A GRADE QUE DIVIDE A ALTURA DA TELA - bateria ampliada, item 17 (29/09).
|
|
496
|
+
*
|
|
497
|
+
* Em 11 dos 28 projetos com tela, o celular abria com meia tela vazia antes do conteúdo, e o agente dizia "works on
|
|
498
|
+
* mobile" nos 11. A causa é uma só, escrita pelo próprio agente na casca: `display: grid` com `min-height: 100dvh` e as
|
|
499
|
+
* colunas empilhando em `1fr` no celular. Sem `grid-template-rows`, as duas linhas automáticas dividem a altura - a da
|
|
500
|
+
* barra lateral cresce até a metade da tela. O conserto é uma linha (`grid-template-rows: auto 1fr`, ou
|
|
501
|
+
* `align-content: start`), e é dito aqui porque o agente não vê o celular.
|
|
502
|
+
*/
|
|
503
|
+
function stretchedStack(file, source, report) {
|
|
504
|
+
if (!/\.(css|scss|sass|less|tsx|jsx)$/i.test(file))
|
|
505
|
+
return report;
|
|
506
|
+
const stacks = /grid-template-columns\s*:\s*1fr\s*[;}]|gridTemplateColumns\s*:[^,}\n]*["']1fr["']/.test(source);
|
|
507
|
+
if (!stacks)
|
|
508
|
+
return report;
|
|
509
|
+
const found = [];
|
|
510
|
+
const ROWS = /grid-template-rows|gridTemplateRows|align-content|alignContent|place-content|placeContent/;
|
|
511
|
+
/** O seletor que recebe a regra de linhas em QUALQUER bloco (o `@media` do c06 põe `grid-template-rows` ali). */
|
|
512
|
+
const settled = new Set();
|
|
513
|
+
const blocks = /([^{};]*)\{([^{}]*)\}/g;
|
|
514
|
+
for (const m of source.matchAll(blocks))
|
|
515
|
+
if (ROWS.test(m[2]))
|
|
516
|
+
settled.add(m[1].trim());
|
|
517
|
+
for (const m of source.matchAll(blocks)) {
|
|
518
|
+
const body = m[2];
|
|
519
|
+
const selector = m[1].trim();
|
|
520
|
+
const grid = /display\s*:\s*["']?grid\b/.test(body);
|
|
521
|
+
const tall = /min-?[hH]eight\s*:\s*["']?100(dvh|svh|lvh|vh)\b/.exec(body);
|
|
522
|
+
if (!grid || !tall || ROWS.test(body) || (selector.startsWith(".") && settled.has(selector)))
|
|
523
|
+
continue;
|
|
524
|
+
const at = source.slice(0, (m.index ?? 0) + m[1].length + 1 + body.indexOf(tall[0])).split("\n").length;
|
|
525
|
+
found.push({ line: at, literal: `min-height: 100${tall[1]}`, why: "stretch" });
|
|
526
|
+
}
|
|
527
|
+
if (found.length === 0)
|
|
528
|
+
return report;
|
|
529
|
+
return { ...report, offScale: [...(report.offScale ?? []), ...found] };
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* O ARQUIVO ONDE O TEMA NASCE NÃO É DERIVA - bateria ampliada, item 13 (29/09).
|
|
533
|
+
*
|
|
534
|
+
* A checagem do fim do turno dizia "11 hand-written values in src/theme.ts" (c03), 18 no do Emotion (c04), 13 no
|
|
535
|
+
* `theme.css.ts` (c05), 18 no `uno.config.ts` (c15) e 7 no `custom.scss` do Bootstrap (c17). São as DEFINIÇÕES
|
|
536
|
+
* dele: o único lugar onde um valor pode e deve estar escrito. O agente gastava o fim de cada turno explicando isso.
|
|
537
|
+
*
|
|
538
|
+
* Postos de lado com o motivo, como os de fixture: não acusados e não creditados. Num `.ts`/`.js` com objeto de tema
|
|
539
|
+
* (o mesmo leitor de `ts-tokens-lens`), o arquivo todo; num `.scss`, as linhas `$nome: valor`.
|
|
540
|
+
*/
|
|
541
|
+
function themeDefinitionsAside(file, source, report) {
|
|
542
|
+
if (report.findings.length === 0)
|
|
543
|
+
return report;
|
|
544
|
+
let aside = [];
|
|
545
|
+
if (THEME_SOURCE.test(file) && !/\.(tsx|jsx)$/i.test(file) && tsTokenObjects(source).length > 0)
|
|
546
|
+
aside = report.findings;
|
|
547
|
+
else if (/\.(scss|sass)$/i.test(file)) {
|
|
548
|
+
const lines = source.split("\n");
|
|
549
|
+
// `$primary: #7c2d12;` - a definição simples. Um mapa (`$r: (radius: 6px, …)`) segue lido como antes.
|
|
550
|
+
aside = report.findings.filter((f) => /^\s*\$[\w-]+\s*:\s*[^(]*;?\s*$/.test(lines[f.line - 1] ?? ""));
|
|
551
|
+
}
|
|
552
|
+
if (aside.length === 0)
|
|
553
|
+
return report;
|
|
554
|
+
const kept = report.findings.filter((f) => !aside.includes(f));
|
|
555
|
+
return {
|
|
556
|
+
...report,
|
|
557
|
+
findings: kept,
|
|
558
|
+
setAside: [...(report.setAside ?? []), { reason: "your theme is defined here - these are its definitions", count: aside.length }],
|
|
559
|
+
};
|
|
492
560
|
}
|
|
493
561
|
/**
|
|
494
562
|
* OS SELETORES QUE DECLARAM VOCABULÁRIO - `@theme`, `:root` e a redefinição por tema (bateria, 28/09: b5 escreve
|
|
@@ -1072,7 +1140,9 @@ context = []) {
|
|
|
1072
1140
|
}
|
|
1073
1141
|
const MEDIA_WIDTH = /\b(?:(?:min|max)-)?width\s*(?::|[<>]=?)\s*([\d.]+)(px|rem|em)\b/g;
|
|
1074
1142
|
const TOKEN_ARITHMETIC = /(?:\$\{\s*|:\s*)([A-Za-z_$][\w$]*(?:\.[\w$]+)*\.(?:[\w$]*(?:space|spacing|gap|radius|size|font)[\w$]*)(?:\.[\w$]+)*)\s*([+\-*/])\s*(\d+(?:\.\d+)?)\b/gi;
|
|
1075
|
-
const BORDER_WIDTH = /\
|
|
1143
|
+
const BORDER_WIDTH = /\b(?:border(?:-(?:top|right|bottom|left|inline|block)(?:-(?:start|end))?)?|outline)(?:-width|-offset)?\s*:\s*(\d+(?:\.\d+)?px)\b/g;
|
|
1144
|
+
/** `100dvh`, `50vw` - uma medida da janela, que nenhum token guarda. */
|
|
1145
|
+
const VIEWPORT = /(?<![\w.-])(\d+(?:\.\d+)?(?:dvh|svh|lvh|vh|dvw|svw|lvw|vw))\b/g;
|
|
1076
1146
|
const BORDER_NAME = /border|line|hairline|stroke|rule|divider|thin/i;
|
|
1077
1147
|
/**
|
|
1078
1148
|
* FORA DA ESCALA, linha a linha - ver `OffScale`. `setAside` recebe o que foi medido e está certo: o ponto de quebra
|
|
@@ -1105,7 +1175,14 @@ function offScaleOf(file, line, table, setAside) {
|
|
|
1105
1175
|
const theirs = [table.aliases.get(`spacing:${v}`), table.aliases.get(`radius:${v}`)].find((a) => a && BORDER_NAME.test(a.name));
|
|
1106
1176
|
if (theirs)
|
|
1107
1177
|
out.push({ literal: m[1], why: "named", nearest: `var(${theirs.name})` });
|
|
1178
|
+
/**
|
|
1179
|
+
* SEM NOME DELE, PASSA - E É DITO (a régua de 28/09, "a checagem diz isso uma vez"; bateria ampliada, item 14). O
|
|
1180
|
+
* fim do turno do c06 dizia "nothing to name" com 1px e 2px nos arquivos, e o agente os achava à mão.
|
|
1181
|
+
*/ else if (/^[12]px$/.test(v))
|
|
1182
|
+
setAside(`a ${v} border or outline - nothing in this project names a border width, so it passes`);
|
|
1108
1183
|
}
|
|
1184
|
+
for (const m of line.matchAll(VIEWPORT))
|
|
1185
|
+
setAside(`${m[1]} - a size of the window, which no token holds`);
|
|
1109
1186
|
return out;
|
|
1110
1187
|
}
|
|
1111
1188
|
/** As linhas que o `check_file` e o fim do turno dizem sobre `OffScale` - uma frase por caso, a mesma nos dois. */
|
|
@@ -1114,6 +1191,8 @@ export function describeOffScale(files) {
|
|
|
1114
1191
|
? ` ${f.file}:${o.line} ${o.literal} - a breakpoint off your system's widths; the nearest is ${o.nearest} (a media query cannot use a token, so match the width)`
|
|
1115
1192
|
: o.why === "arithmetic"
|
|
1116
1193
|
? ` ${f.file}:${o.line} ${o.literal} - arithmetic on a token lands between the steps; use a step the system has, or ask for one`
|
|
1117
|
-
:
|
|
1194
|
+
: o.why === "stretch"
|
|
1195
|
+
? ` ${f.file}:${o.line} ${o.literal} on a grid that stacks to one column - on a phone its rows split the screen's height, so the bar above the content grows to half the screen; add grid-template-rows: auto 1fr (or align-content: start)`
|
|
1196
|
+
: ` ${f.file}:${o.line} ${o.literal} - this repo calls it ${o.nearest}`));
|
|
1118
1197
|
return rows.length > 0 ? ["", "Off this system's scale:", ...rows] : [];
|
|
1119
1198
|
}
|
|
@@ -33,6 +33,8 @@ const DARK_MARK = /(\.dark\b|\.theme-dark\b|\[data-(?:theme|scheme|mode)\s*[~^|*
|
|
|
33
33
|
const LIGHT_MARK = /(\.light\b|\.theme-light\b|\[data-(?:theme|scheme|mode)\s*[~^|*$]?=\s*["']?light["']?\s*\])/i;
|
|
34
34
|
/** As raízes do documento - os lugares onde alguém escreve o vocabulário do projeto. */
|
|
35
35
|
const ROOT_MARK = /(^|[\s>+~,])(:root|html|:host|body)\b/gi;
|
|
36
|
+
/** Um seletor que é SÓ atributo, sem elemento nem classe: `[data-brand="acme"]`. */
|
|
37
|
+
const DOCUMENT_ATTRIBUTE = /^(\[[\w-]+(?:\s*[~^|*$]?=\s*(?:"[^"]*"|'[^']*'|[\w-]+))?\s*\])+$/;
|
|
36
38
|
/** `@theme` do Tailwind v4 é raiz por definição: é onde a paleta inteira é declarada. */
|
|
37
39
|
const AT_THEME = /^@theme\b/i;
|
|
38
40
|
/**
|
|
@@ -71,7 +73,22 @@ function isDocumentScoped(selector) {
|
|
|
71
73
|
// real virava local, e com ele os dois tokens que o comentário de `tokens.ts` cita
|
|
72
74
|
// por nome (`--loader-color`, `--color-track`).
|
|
73
75
|
const flat = selector.replace(/:(?:where|is)\s*\(/gi, "").replace(/\)/g, "");
|
|
74
|
-
|
|
76
|
+
const parts = flat.split(",");
|
|
77
|
+
/**
|
|
78
|
+
* `:root, [data-brand="acme"] { … }` - A MARCA PADRÃO DITA DUAS VEZES (bateria ampliada, c07, 29/09).
|
|
79
|
+
*
|
|
80
|
+
* O projeto multimarca declara a marca padrão na raiz E sob o atributo que a troca, na mesma regra. O
|
|
81
|
+
* atributo sozinho, sem elemento, é o que ele põe no `<html>`; ao lado de uma raiz na MESMA lista, ele fala
|
|
82
|
+
* do documento, e não de um componente. Sem isto a regra inteira caía em `other`, as 5 variáveis dele não
|
|
83
|
+
* entravam, e o sistema nascia misturando as duas marcas pelo "the colour your code paints most".
|
|
84
|
+
*/
|
|
85
|
+
const rootInList = parts.some((one) => {
|
|
86
|
+
ROOT_MARK.lastIndex = 0;
|
|
87
|
+
return ROOT_MARK.test(one);
|
|
88
|
+
});
|
|
89
|
+
return parts.every((one) => {
|
|
90
|
+
if (rootInList && DOCUMENT_ATTRIBUTE.test(one.trim()))
|
|
91
|
+
return true;
|
|
75
92
|
SCHEME_MARK.lastIndex = 0;
|
|
76
93
|
const scoped = SCHEME_MARK.test(one);
|
|
77
94
|
ROOT_MARK.lastIndex = 0;
|
|
@@ -270,3 +287,24 @@ export function scopedBlocks(css) {
|
|
|
270
287
|
* consumo.
|
|
271
288
|
*/
|
|
272
289
|
const NAME = /^(--[a-z0-9_-]+|-{0,2}[a-z][a-z0-9-]*)$/i;
|
|
290
|
+
/**
|
|
291
|
+
* AS OUTRAS MARCAS - `[data-brand="globex"] { --brand: …; }` redeclarando os nomes da raiz (bateria ampliada, c07).
|
|
292
|
+
*
|
|
293
|
+
* Um bloco só de atributo que redeclara pelo menos dois nomes da raiz é outra marca (ou outro tema) do mesmo
|
|
294
|
+
* sistema, e não vocabulário novo. Ele não entra na base - a base é a marca da raiz -, mas a pessoa precisa saber
|
|
295
|
+
* que ele foi visto e deixado de fora, e não misturado nem esquecido.
|
|
296
|
+
*/
|
|
297
|
+
export function attributeFaces(css, baseNames) {
|
|
298
|
+
const out = new Map();
|
|
299
|
+
for (const block of scopedBlocks(css)) {
|
|
300
|
+
if (block.scope !== "other" || block.stack.length !== 1)
|
|
301
|
+
continue;
|
|
302
|
+
const selector = block.stack[0].trim();
|
|
303
|
+
if (!DOCUMENT_ATTRIBUTE.test(selector))
|
|
304
|
+
continue;
|
|
305
|
+
const same = block.declarations.filter((d) => baseNames.has(d.name)).length;
|
|
306
|
+
if (same >= 2)
|
|
307
|
+
out.set(selector, (out.get(selector) ?? 0) + same);
|
|
308
|
+
}
|
|
309
|
+
return [...out].map(([selector, names]) => ({ selector, names }));
|
|
310
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { parseRootTokens } from "./tokens.js";
|
|
4
|
+
const CSS_IMPORT = /import\s+(?:[\w*{}\s,]+\s+from\s+)?["']([^"']+\.(?:css|scss|sass|less))["']/g;
|
|
5
|
+
/** Os `import "…css"` de cada arquivo de código, resolvidos, na ordem em que aparecem. */
|
|
6
|
+
export async function cssImportLists(codeFiles) {
|
|
7
|
+
const out = new Map();
|
|
8
|
+
for (const file of codeFiles) {
|
|
9
|
+
const src = await readFile(file, "utf8").catch(() => "");
|
|
10
|
+
if (!src.includes(".css") && !src.includes(".scss"))
|
|
11
|
+
continue;
|
|
12
|
+
const list = [];
|
|
13
|
+
for (const m of src.matchAll(CSS_IMPORT)) {
|
|
14
|
+
const spec = m[1];
|
|
15
|
+
if (!spec.startsWith("."))
|
|
16
|
+
continue;
|
|
17
|
+
list.push(join(dirname(file), spec));
|
|
18
|
+
}
|
|
19
|
+
if (list.length > 1)
|
|
20
|
+
out.set(file, list);
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Os conflitos entre folhas, e a ORDEM das folhas para o leitor de raiz (que fica com a primeira): quem a tela mostra
|
|
26
|
+
* vem antes. Folha que não está em conflito nenhum mantém o lugar.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveSheetConflicts(sheets, importLists) {
|
|
29
|
+
const perSheet = sheets.map((s) => ({
|
|
30
|
+
...s,
|
|
31
|
+
tokens: parseRootTokens(s.body),
|
|
32
|
+
}));
|
|
33
|
+
const byName = new Map();
|
|
34
|
+
for (const s of perSheet)
|
|
35
|
+
for (const [name, value] of s.tokens) {
|
|
36
|
+
const list = byName.get(name) ?? [];
|
|
37
|
+
list.push({ file: s.file, value });
|
|
38
|
+
byName.set(name, list);
|
|
39
|
+
}
|
|
40
|
+
const conflicts = [];
|
|
41
|
+
/** Folha -> quantas folhas ela vence. A que vence mais sobe na ordem. */
|
|
42
|
+
const wins = new Map();
|
|
43
|
+
for (const [name, values] of byName) {
|
|
44
|
+
const distinct = new Set(values.map((v) => v.value.trim().toLowerCase()));
|
|
45
|
+
if (values.length < 2 || distinct.size < 2)
|
|
46
|
+
continue;
|
|
47
|
+
let decidedBy = null;
|
|
48
|
+
let winner = null;
|
|
49
|
+
for (const [importer, list] of importLists) {
|
|
50
|
+
const positions = values.map((v) => list.indexOf(v.file));
|
|
51
|
+
if (positions.some((p) => p === -1))
|
|
52
|
+
continue;
|
|
53
|
+
const last = positions.indexOf(Math.max(...positions));
|
|
54
|
+
winner = values[last] ?? null;
|
|
55
|
+
decidedBy = importer;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
if (winner)
|
|
59
|
+
wins.set(winner.file, (wins.get(winner.file) ?? 0) + 1);
|
|
60
|
+
conflicts.push({
|
|
61
|
+
name,
|
|
62
|
+
shown: (winner ?? values[0])?.value ?? "",
|
|
63
|
+
values,
|
|
64
|
+
decidedBy,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const ordered = [...sheets].sort((a, b) => (wins.get(b.file) ?? 0) - (wins.get(a.file) ?? 0));
|
|
68
|
+
return { ordered, conflicts };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* A FOLHA QUE NINGUÉM CARREGA - bateria ampliada, c17 e c18 (29/09).
|
|
72
|
+
*
|
|
73
|
+
* O create-next-app deixa um `app/globals.css` com `--foreground: #ededed`, e quem troca por outra folha (o
|
|
74
|
+
* `vendor.min.css` do c18, o `custom.scss` do Bootstrap no c17) costuma esquecer o arquivo. Nenhuma página o importa,
|
|
75
|
+
* ele não pinta nada, e a cor do texto do sistema saía dele.
|
|
76
|
+
*
|
|
77
|
+
* Uma folha só dá token quando algum arquivo a cita pelo nome: um `import` no código, um `<link>` no HTML, a lista de
|
|
78
|
+
* estilos de uma config (`angular.json`, `nuxt.config.ts`) ou um `@import` de outra folha. Quando nada no projeto cita
|
|
79
|
+
* folha nenhuma, a pergunta não tem resposta, e nada é tirado.
|
|
80
|
+
*/
|
|
81
|
+
export async function unloadedSheets(sheets, textFiles) {
|
|
82
|
+
if (sheets.length < 2)
|
|
83
|
+
return [];
|
|
84
|
+
const texts = await Promise.all(textFiles.map((f) => readFile(f, "utf8").catch(() => "")));
|
|
85
|
+
const all = [...texts, ...sheets.map((s) => s.body)].join("\n");
|
|
86
|
+
const base = (f) => f.slice(f.lastIndexOf("/") + 1);
|
|
87
|
+
const cited = (f) => {
|
|
88
|
+
const name = base(f);
|
|
89
|
+
const stem = name.replace(/\.(css|scss|sass|less)$/, "");
|
|
90
|
+
return all.includes(name) || new RegExp(`["'/]_?${stem.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']`).test(all);
|
|
91
|
+
};
|
|
92
|
+
const citedAny = sheets.some((s) => cited(s.file));
|
|
93
|
+
if (!citedAny)
|
|
94
|
+
return [];
|
|
95
|
+
return sheets
|
|
96
|
+
.filter((s) => !/\.module\./.test(s.file) && !base(s.file).startsWith("_"))
|
|
97
|
+
.filter((s) => parseRootTokens(s.body).size > 0 && !cited(s.file))
|
|
98
|
+
.map((s) => s.file);
|
|
99
|
+
}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
const kebab = (k) => k.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Um literal de objeto a partir de `{`, ou `null` quando a própria estrutura não fecha.
|
|
4
|
+
*
|
|
5
|
+
* UMA ENTRADA ILEGÍVEL CUSTA SÓ ELA - bateria ampliada, 29/09. O tema do styled-components tinha
|
|
6
|
+
* `space: [0, 4, 8, 16, 24, 32]` e o do Emotion `spacing: (n) => \`${n * 4}px\``, e o objeto inteiro caía
|
|
7
|
+
* por causa de uma chave: o import dizia "your code names no token yet" com seis cores nomeadas ao lado. Agora
|
|
8
|
+
* um array vira escala (números) ou pilha de fonte (textos), e o que não é literal (função, chamada, spread,
|
|
9
|
+
* referência) fica sem leitura SÓ naquela chave. Uma expressão sobre token continua sem leitura, como antes.
|
|
10
|
+
*/
|
|
3
11
|
function parseObject(s, start) {
|
|
4
12
|
let i = start + 1;
|
|
5
13
|
const tree = {};
|
|
@@ -26,61 +34,168 @@ function parseObject(s, start) {
|
|
|
26
34
|
i = end + 1;
|
|
27
35
|
return out;
|
|
28
36
|
};
|
|
37
|
+
/** Anda até a próxima `,` ou `}` do MESMO nível, atravessando parênteses, colchetes, chaves e textos. */
|
|
38
|
+
const skip = () => {
|
|
39
|
+
let depth = 0;
|
|
40
|
+
while (i < s.length) {
|
|
41
|
+
const c = s[i];
|
|
42
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
43
|
+
const end = s.indexOf(c, i + 1);
|
|
44
|
+
if (end === -1)
|
|
45
|
+
return false;
|
|
46
|
+
i = end + 1;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (c === "(" || c === "[" || c === "{")
|
|
50
|
+
depth += 1;
|
|
51
|
+
else if (c === ")" || c === "]" || c === "}") {
|
|
52
|
+
if (depth === 0)
|
|
53
|
+
return c === "}";
|
|
54
|
+
depth -= 1;
|
|
55
|
+
}
|
|
56
|
+
else if (c === "," && depth === 0)
|
|
57
|
+
return true;
|
|
58
|
+
i += 1;
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
};
|
|
62
|
+
/** `[0, 4, 8]` -> escala; `["Inter", "sans-serif"]` -> uma pilha de fonte. Outra coisa: `undefined`. */
|
|
63
|
+
const array = () => {
|
|
64
|
+
const close = s.indexOf("]", i);
|
|
65
|
+
if (close === -1)
|
|
66
|
+
return undefined;
|
|
67
|
+
const inner = s.slice(i + 1, close);
|
|
68
|
+
if (/[[{(]/.test(inner))
|
|
69
|
+
return undefined;
|
|
70
|
+
const items = inner
|
|
71
|
+
.split(",")
|
|
72
|
+
.map((x) => x.trim())
|
|
73
|
+
.filter((x) => x !== "");
|
|
74
|
+
if (items.length === 0)
|
|
75
|
+
return undefined;
|
|
76
|
+
if (items.every((x) => /^-?\d+(\.\d+)?$/.test(x))) {
|
|
77
|
+
i = close + 1;
|
|
78
|
+
return Object.fromEntries(items.map((x, n) => [String(n), Number(x)]));
|
|
79
|
+
}
|
|
80
|
+
if (items.every((x) => /^(["'])[^"']*\1$/.test(x))) {
|
|
81
|
+
i = close + 1;
|
|
82
|
+
return items.map((x) => x.slice(1, -1)).join(", ");
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
};
|
|
29
86
|
for (;;) {
|
|
30
87
|
ws();
|
|
31
88
|
if (s[i] === "}")
|
|
32
89
|
return { tree, end: i + 1 };
|
|
90
|
+
if (i >= s.length)
|
|
91
|
+
return null;
|
|
33
92
|
let key = str();
|
|
34
93
|
if (key === null) {
|
|
35
94
|
const m = /^[A-Za-z_$][\w$]*|^\d+/.exec(s.slice(i));
|
|
36
|
-
if (!m)
|
|
37
|
-
|
|
95
|
+
if (!m) {
|
|
96
|
+
// `...spread` ou chave computada: a entrada fica sem leitura, o resto do objeto segue
|
|
97
|
+
if (!skip())
|
|
98
|
+
return s[i] === "}" ? { tree, end: i + 1 } : null;
|
|
99
|
+
if (s[i] === ",")
|
|
100
|
+
i += 1;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
38
103
|
key = m[0];
|
|
39
104
|
i += key.length;
|
|
40
105
|
}
|
|
41
106
|
ws();
|
|
42
|
-
if (s[i] !== ":")
|
|
43
|
-
|
|
107
|
+
if (s[i] !== ":") {
|
|
108
|
+
// `{ primary, secondary }` (atalho) ou método: sem valor literal para ler
|
|
109
|
+
if (!skip())
|
|
110
|
+
return s[i] === "}" ? { tree, end: i + 1 } : null;
|
|
111
|
+
if (s[i] === ",")
|
|
112
|
+
i += 1;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
44
115
|
i += 1;
|
|
45
116
|
ws();
|
|
117
|
+
let value;
|
|
46
118
|
if (s[i] === "{") {
|
|
47
119
|
const inner = parseObject(s, i);
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
120
|
+
if (inner) {
|
|
121
|
+
value = inner.tree;
|
|
122
|
+
i = inner.end;
|
|
123
|
+
}
|
|
52
124
|
}
|
|
125
|
+
else if (s[i] === "[")
|
|
126
|
+
value = array();
|
|
53
127
|
else {
|
|
54
128
|
const text = str();
|
|
55
129
|
if (text !== null)
|
|
56
|
-
|
|
130
|
+
value = text;
|
|
57
131
|
else {
|
|
58
|
-
const m = /^-?\d+(\.\d+)
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
132
|
+
const m = /^-?\d+(\.\d+)?(?=\s*[,}\n])/.exec(s.slice(i));
|
|
133
|
+
if (m) {
|
|
134
|
+
value = Number(m[0]);
|
|
135
|
+
i += m[0].length;
|
|
136
|
+
}
|
|
63
137
|
}
|
|
64
138
|
}
|
|
139
|
+
if (value === undefined) {
|
|
140
|
+
if (!skip())
|
|
141
|
+
return s[i] === "}" ? { tree, end: i + 1 } : null;
|
|
142
|
+
}
|
|
143
|
+
else
|
|
144
|
+
tree[key] = value;
|
|
65
145
|
ws();
|
|
66
146
|
if (s[i] === ",")
|
|
67
147
|
i += 1;
|
|
68
148
|
}
|
|
69
149
|
}
|
|
70
150
|
const DESIGN = /^(#[0-9a-f]{3,8}|rgba?\(|hsla?\(|oklch\(|cubic-bezier\(|-?\d+(\.\d+)?(px|rem|em|%|ms|s)\b)|,|['"]/i;
|
|
151
|
+
/** Número que NÃO é medida: peso, camada, altura de linha, opacidade. Ganha `px` só o resto. */
|
|
152
|
+
const UNITLESS = /(font-?weight|weight|z-?index|line-?height|opacity|flex|order)$/i;
|
|
153
|
+
/**
|
|
154
|
+
* INVÓLUCROS DE CONFIG - `theme.extend.colors` do Tailwind 3, `theme.tokens.colors` do Chakra. Eles dizem ONDE o
|
|
155
|
+
* vocabulário mora na config, e não fazem parte do nome dele: `--theme-extend-colors-brand` seria um nome que ele
|
|
156
|
+
* nunca escreveu. Só saem no começo do caminho.
|
|
157
|
+
*/
|
|
158
|
+
const WRAPPER = new Set(["theme", "extend", "tokens"]);
|
|
71
159
|
function flatten(tree, prefix, out) {
|
|
72
160
|
for (const [k, v] of Object.entries(tree)) {
|
|
73
|
-
|
|
74
|
-
|
|
161
|
+
if (prefix.length === 0 && ["content", "plugins", "presets", "dark-mode", "darkMode", "safelist"].includes(k))
|
|
162
|
+
continue;
|
|
163
|
+
const seg = kebab(k);
|
|
164
|
+
if (prefix.every((p) => WRAPPER.has(p)) && WRAPPER.has(seg) && typeof v === "object") {
|
|
165
|
+
flatten(v, [...prefix, seg], out);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
// `brand: { DEFAULT: "#0f766e", soft: … }` - no Tailwind o DEFAULT É o `brand`, e não um tom chamado "default"
|
|
169
|
+
const path = [...prefix.filter((p) => !WRAPPER.has(p)), ...(k === "DEFAULT" ? [] : [seg])];
|
|
170
|
+
if (typeof v === "object") {
|
|
171
|
+
// `{ value: "#1e3a8a" }` - a forma de token do Chakra e do Style Dictionary: o nome é o de cima
|
|
172
|
+
const keys = Object.keys(v);
|
|
173
|
+
if (keys.length === 1 && keys[0] === "value" && typeof v.value !== "object") {
|
|
174
|
+
out.set(`--${path.join("-")}`, typeof v.value === "number" ? `${v.value}px` : v.value);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
75
177
|
flatten(v, path, out);
|
|
178
|
+
}
|
|
76
179
|
else
|
|
77
|
-
out.set(`--${path.join("-")}`, typeof v === "number" ? `${v}px` : v);
|
|
180
|
+
out.set(`--${path.join("-")}`, typeof v === "number" ? (UNITLESS.test(k) ? String(v) : `${v}px`) : v);
|
|
78
181
|
}
|
|
79
182
|
}
|
|
80
|
-
/**
|
|
183
|
+
/**
|
|
184
|
+
* ONDE UM OBJETO DE TOKENS COMEÇA - bateria ampliada, 29/09. Antes só `export const x = {`, e por isso nenhum dos
|
|
185
|
+
* temas escritos como a biblioteca manda entrava: `createTheme({...})` (MUI, vanilla-extract), `defineConfig({...})`
|
|
186
|
+
* (Chakra, UnoCSS), `module.exports = {...}` (tailwind.config.js), `export default {...}`. Uma const não exportada só
|
|
187
|
+
* conta quando a chamada é de tema, porque `const style = {` dentro de um componente não é o sistema dele.
|
|
188
|
+
*/
|
|
189
|
+
const THEME_CALL = /^(createTheme|defineConfig|createSystem|extendTheme|defineTokens|createGlobalTheme|defineTheme)$/;
|
|
190
|
+
const OPENERS = /(?:(export\s+)?(?:const|let)\s+(\[[^\]]*\]|[A-Za-z_$][\w$]*)\s*(?::[^=]+)?=|export\s+default|module\.exports\s*=)\s*(?:([A-Za-z_$][\w$.]*)\s*\(\s*(?:[A-Za-z_$][\w$]*\s*,\s*)?)?\{/g;
|
|
191
|
+
/** Os objetos que seguram valores de design - um objeto de rotas ou de textos não conta. */
|
|
81
192
|
export function tsTokenObjects(source) {
|
|
82
193
|
const out = [];
|
|
83
|
-
for (const m of source.matchAll(
|
|
194
|
+
for (const m of source.matchAll(OPENERS)) {
|
|
195
|
+
const exported = /^(export|module)/.test(m[0]) || m[1] !== undefined;
|
|
196
|
+
const call = (m[3] ?? "").split(".").pop() ?? "";
|
|
197
|
+
if (!exported && !THEME_CALL.test(call))
|
|
198
|
+
continue;
|
|
84
199
|
const parsed = parseObject(source, (m.index ?? 0) + m[0].length - 1);
|
|
85
200
|
if (!parsed)
|
|
86
201
|
continue;
|
|
@@ -88,8 +203,9 @@ export function tsTokenObjects(source) {
|
|
|
88
203
|
flatten(parsed.tree, [], values);
|
|
89
204
|
const leaves = [...values.values()];
|
|
90
205
|
const design = leaves.filter((v) => DESIGN.test(v) || /^\d+(\.\d+)?px$/.test(v)).length;
|
|
206
|
+
const name = (m[2] ?? "default").replace(/^\[\s*|\s*\]$/g, "").split(",").pop()?.trim() || "default";
|
|
91
207
|
if (leaves.length >= 4 && design * 10 >= leaves.length * 6)
|
|
92
|
-
out.push({ name
|
|
208
|
+
out.push({ name, values });
|
|
93
209
|
}
|
|
94
210
|
return out;
|
|
95
211
|
}
|
|
@@ -135,3 +251,9 @@ export function tsTokensAsVars(source, objects) {
|
|
|
135
251
|
});
|
|
136
252
|
return out;
|
|
137
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* OS ARQUIVOS ONDE UM TEMA PODE MORAR - TypeScript e também JavaScript, porque o `tailwind.config.js` do Tailwind 3 é
|
|
256
|
+
* o sistema inteiro de quem o usa (bateria ampliada, c02). O filtro de valor de design de `tsTokenObjects` é o que
|
|
257
|
+
* separa o tema de um `next.config.js`.
|
|
258
|
+
*/
|
|
259
|
+
export const THEME_SOURCE = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
|
package/dist/install-marks.js
CHANGED
|
@@ -509,7 +509,25 @@ export const COUNTED_DIFFERENTLY = "this run counts a value as named only when Y
|
|
|
509
509
|
* 0.16.475 -> 0.16.478 em 28/09, e o passo 1 dá **SIM** (etapa 02, "a nossa grafia conta como nossa"): o fim do turno diz
|
|
510
510
|
* quando o arquivo usa `var(--ds-*)` - o valor do sistema com o NOSSO nome. Um hook anterior cala sobre esse arquivo.
|
|
511
511
|
*/
|
|
512
|
-
|
|
512
|
+
/**
|
|
513
|
+
* 0.16.478 -> 0.16.485 em 29/09, e o passo 1 dá **SIM** (a bateria ampliada, itens 13, 14, 17, 18 e 19): o fim do turno
|
|
514
|
+
* deixa de acusar o arquivo onde o tema é definido, nomeia o que sai da escala depois do corte, diz uma vez o que passou
|
|
515
|
+
* de propósito, avisa a grade que divide a altura da tela no celular, propõe os nomes numa pergunta só e termina pedindo
|
|
516
|
+
* o resumo por último. Um hook anterior diz outra coisa sobre o mesmo turno.
|
|
517
|
+
*/
|
|
518
|
+
/**
|
|
519
|
+
* 0.16.485 -> 0.16.486 em 30/09, e o passo 1 dá **SIM** (o jogo dos envios, E01): a linha que o hook grava no ledger
|
|
520
|
+
* passa a dizer quantos valores sem nome e quantos fora da escala a checagem achou, e de qual componente é o arquivo. Um
|
|
521
|
+
* hook anterior grava "limpo" sobre o arquivo que acabou de acusar, e é dessas linhas que os degraus de cada componente
|
|
522
|
+
* vão ser contados - quem está atrás vê o componente amadurecer com checagens que não foram limpas.
|
|
523
|
+
*/
|
|
524
|
+
/**
|
|
525
|
+
* 0.16.486 -> 0.16.487 em 30/09, e o passo 1 dá **SIM** (o jogo dos envios, E03): a ponte arquivo -> componente passa a
|
|
526
|
+
* dar dono ao arquivo da pasta do componente (`CourseCover/motion.ts`) e, quando duas receitas reclamam o mesmo arquivo,
|
|
527
|
+
* à que tem o nome dele. Medido no `codelevel`: 33 checagens do componente mais checado chegavam sem componente. Um
|
|
528
|
+
* hook anterior continua mandando essas checagens sem dono, e elas não contam para o degrau.
|
|
529
|
+
*/
|
|
530
|
+
export const CHECKER_SINCE = "0.16.487";
|
|
513
531
|
/**
|
|
514
532
|
* A ÚLTIMA VERSÃO EM QUE OS LEITORES PASSARAM A PRODUZIR UM CENSO DIFERENTE.
|
|
515
533
|
*
|
|
@@ -979,7 +997,13 @@ export const CHECKER_SINCE = "0.16.478";
|
|
|
979
997
|
* mão o que um `:root { --a: x; }` ou um `[data-theme="dark"] { … }` de UMA linha atribui a uma variável - a definição do
|
|
980
998
|
* token. Quatro apps do corpus moveram. Um censo lido antes guardou essas declarações como drift.
|
|
981
999
|
*/
|
|
982
|
-
|
|
1000
|
+
/**
|
|
1001
|
+
* 0.16.473 -> 0.16.485 em 29/09, e o passo 1 dá **SIM** (a bateria ampliada, itens 1 a 11): o censo lê a folha que ele
|
|
1002
|
+
* importa de dentro de build/, a marca da raiz do multimarca, o tema em JS/TS de sete bibliotecas, o token que a tela
|
|
1003
|
+
* mostra quando duas folhas brigam, deixa de fora a folha que ninguém carrega, conta o pages router como rota e diz os
|
|
1004
|
+
* componentes em Vue, Svelte, Astro e Angular. Um censo lido antes guardou "names no token yet" onde havia tokens.
|
|
1005
|
+
*/
|
|
1006
|
+
export const READER_SINCE = "0.16.485";
|
|
983
1007
|
/**
|
|
984
1008
|
* O QUE ESTÁ INSTALADO AQUI FICOU PARA TRÁS - e as DUAS condições que fazem isso ser verdade.
|
|
985
1009
|
*
|
package/package.json
CHANGED