synthesisui 0.16.401 → 0.16.403
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/commands/add.js +85 -5
- package/dist/commands/component.js +103 -3
- package/dist/commands/doctor.js +1 -73
- package/dist/commands/list.js +23 -1
- package/dist/component-codegen.js +93 -7
- package/dist/font-facts.spec-input.js +24 -0
- package/dist/fonts.js +126 -30
- package/dist/guide.js +31 -2
- package/dist/install-marks.js +1 -1
- package/dist/next-font-weights.js +140 -0
- package/dist/wiring-read.js +113 -0
- package/package.json +1 -1
package/dist/commands/add.js
CHANGED
|
@@ -8,6 +8,7 @@ import { lockReference } from "../group-role.js";
|
|
|
8
8
|
import { buildGuide } from "../guide.js";
|
|
9
9
|
import { censusScope } from "../measured-scope.js";
|
|
10
10
|
import { recallAvailable } from "../memory/availability.js";
|
|
11
|
+
import { nextFontFacts } from "../next-font-weights.js";
|
|
11
12
|
import { body as line, section, snippet } from "../output.js";
|
|
12
13
|
import { fetchDesignSystem } from "../registry.js";
|
|
13
14
|
import { repoStateOf } from "../repo-state.js";
|
|
@@ -233,7 +234,16 @@ export async function add(slug, opts) {
|
|
|
233
234
|
* função responde: cortar sem caminho de volta trocaria contexto por ignorância.
|
|
234
235
|
*/
|
|
235
236
|
const tools = await recallAvailable(projectRoot);
|
|
236
|
-
|
|
237
|
+
/**
|
|
238
|
+
* OS PESOS DAS FAMÍLIAS, LIDOS UMA VEZ - ver `next-font-weights.ts`.
|
|
239
|
+
*
|
|
240
|
+
* Aqui, e não junto do passo 3 do setup, porque o GUIA também imprime o bloco do `next/font`
|
|
241
|
+
* para o cliente colar: dois leitores do mesmo manifesto poderiam discordar, e o guia é o
|
|
242
|
+
* arquivo que o agente dele lê antes de escrever. `null` em projeto que não é Next é a resposta
|
|
243
|
+
* certa, não uma falha - lá não existe `next/font` para ter peso nenhum.
|
|
244
|
+
*/
|
|
245
|
+
const fontFacts = await nextFontFacts(projectRoot);
|
|
246
|
+
await writeFile(join(versionDir, "GUIDE.md"), buildGuide(payload, tools.available, fontFacts), "utf8");
|
|
237
247
|
// 4. stable root re-exports for each CSS artifact → always the active version,
|
|
238
248
|
// so the consumer's @import path never changes across updates
|
|
239
249
|
const cssArtifacts = Object.keys(payload.artifacts).filter((f) => f.endsWith(".css") && !(f === "shadcn.css" && !wantsShadcn));
|
|
@@ -671,9 +681,71 @@ export async function add(slug, opts) {
|
|
|
671
681
|
// "blink". The Google Fonts <link> stays as the framework-agnostic path.
|
|
672
682
|
const families = payload.document.foundations.typography.families;
|
|
673
683
|
const fontsHref = googleFontsHref(families);
|
|
674
|
-
|
|
675
|
-
|
|
684
|
+
/**
|
|
685
|
+
* OS PESOS QUE AS FAMÍLIAS TÊM NO PROJETO DELE - ver `next-font-weights.ts`.
|
|
686
|
+
*
|
|
687
|
+
* Lido UMA vez e passado adiante: o manifesto tem 1942 famílias, e reabri-lo por app de um
|
|
688
|
+
* monorepo seria a mesma leitura repetida com liberdade de discordar de si mesma.
|
|
689
|
+
*/
|
|
690
|
+
const fontPlan = projectConfig.target === "next"
|
|
691
|
+
? nextFontSnippet({
|
|
692
|
+
families,
|
|
693
|
+
slug: payload.slug,
|
|
694
|
+
appDir,
|
|
695
|
+
seam: payload.fontSeam,
|
|
696
|
+
facts: fontFacts,
|
|
697
|
+
declaredWeights: payload.document.foundations.typography.weights,
|
|
698
|
+
})
|
|
676
699
|
: null;
|
|
700
|
+
const nextFonts = fontPlan?.ok ? fontPlan : null;
|
|
701
|
+
/**
|
|
702
|
+
* A LACUNA DE PESO, DITA EM VOZ ALTA (lei 8) - e ela é o oposto de um arquivo escrito errado.
|
|
703
|
+
*
|
|
704
|
+
* Sem o manifesto do `next/font` ao alcance, emitir `weight` seria adivinhar, e a adivinhação
|
|
705
|
+
* derrubou o `next build` de um projeto real em 08/09. Então o arquivo não sai, e o cliente
|
|
706
|
+
* recebe o motivo mais o caminho que funciona.
|
|
707
|
+
*/
|
|
708
|
+
/**
|
|
709
|
+
* A LACUNA DE FONTE, DITA COM A CAUSA CERTA (lei 8) - e as duas causas pedem frases diferentes.
|
|
710
|
+
*
|
|
711
|
+
* Sem manifesto, não sabemos os pesos de NENHUMA família e emiti-los seria adivinhar - a
|
|
712
|
+
* adivinhação derrubou o `next build` de um projeto real em 08/09. Com manifesto legível e
|
|
713
|
+
* família desconhecida, o diagnóstico é outro: aquela fonte não é do Google Fonts, e nem o
|
|
714
|
+
* `next/font/google` nem o `<link>` a servem. Dizer a primeira frase no segundo caso é um
|
|
715
|
+
* diagnóstico falso, e manda a pessoa para um caminho que não existe para ela.
|
|
716
|
+
*
|
|
717
|
+
* SEM NÚMERO, de propósito: o passo 3 é impresso logo abaixo, e dois blocos numerados "3." na
|
|
718
|
+
* mesma tela é a família de defeito que este PR conserta.
|
|
719
|
+
*/
|
|
720
|
+
if (fontPlan && !fontPlan.ok && fontPlan.why === "manifest-unreadable") {
|
|
721
|
+
console.log("");
|
|
722
|
+
console.log(line(`next/font: ${appDir}/fonts.ts was NOT written - this project's next/font manifest`));
|
|
723
|
+
console.log(line(`is not readable from here, so the weights ${fontPlan.families.join(", ")} actually`));
|
|
724
|
+
console.log(line(`${fontPlan.families.length === 1 ? "ships" : "ship"} cannot be read - and asking for a weight a family does not ship fails`));
|
|
725
|
+
console.log(line(`\`next build\`. Step 3 below works everywhere; add next/font yourself if you`));
|
|
726
|
+
console.log(line(`want it self-hosted.`));
|
|
727
|
+
}
|
|
728
|
+
if (fontPlan && !fontPlan.ok && fontPlan.why === "family-unknown") {
|
|
729
|
+
console.log("");
|
|
730
|
+
console.log(line(`next/font: ${appDir}/fonts.ts was NOT written - Google Fonts has no ${fontPlan.families.join(", ")},`));
|
|
731
|
+
console.log(line(`so neither next/font/google nor the link below can serve ${fontPlan.families.length === 1 ? "it" : "them"}. ${fontPlan.families.length === 1 ? "This is" : "These are"} yours to`));
|
|
732
|
+
console.log(line(`load - a licensed or self-hosted face, most likely - and the token names are`));
|
|
733
|
+
console.log(line(`already in tokens.css waiting for ${fontPlan.families.length === 1 ? "it" : "them"}.`));
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* E A FAMÍLIA QUE FICOU DE FORA DO ARQUIVO É NOMEADA - ver `unknown` em `fonts.ts`.
|
|
737
|
+
*
|
|
738
|
+
* Com duas famílias e só uma no manifesto, a segunda saía do `fonts.ts`, do `layout` e do mapa de
|
|
739
|
+
* CSS sem uma palavra: o token dela continua no `tokens.css` e o texto sai no fallback do
|
|
740
|
+
* navegador. Este módulo já dizia isso sobre o caso vizinho - *"um token que aponta para uma
|
|
741
|
+
* fonte que ninguém carregou é pior que um token ausente"*.
|
|
742
|
+
*/
|
|
743
|
+
if (nextFonts && nextFonts.unknown.length > 0) {
|
|
744
|
+
console.log("");
|
|
745
|
+
console.log(line(`next/font: ${nextFonts.unknown.join(", ")} ${nextFonts.unknown.length === 1 ? "is" : "are"} NOT in ${appDir}/fonts.ts - Google Fonts`));
|
|
746
|
+
console.log(line(`has no such family, so ${nextFonts.unknown.length === 1 ? "it is" : "they are"} yours to load. The token${nextFonts.unknown.length === 1 ? "" : "s"} for ${nextFonts.unknown.length === 1 ? "it" : "them"} ${nextFonts.unknown.length === 1 ? "is" : "are"}`));
|
|
747
|
+
console.log(line(`already in tokens.css, waiting.`));
|
|
748
|
+
}
|
|
677
749
|
if (nextFonts) {
|
|
678
750
|
/**
|
|
679
751
|
* UM `fonts.ts` POR APP, e não só no primeiro - lacuna do próprio conserto de 22/08.
|
|
@@ -694,9 +766,17 @@ export async function add(slug, opts) {
|
|
|
694
766
|
continue;
|
|
695
767
|
if (!(await exists(join(projectRoot, ...dir.split("/")))))
|
|
696
768
|
continue;
|
|
697
|
-
const
|
|
698
|
-
|
|
769
|
+
const plan = nextFontSnippet({
|
|
770
|
+
families,
|
|
771
|
+
slug: payload.slug,
|
|
772
|
+
appDir: dir,
|
|
773
|
+
seam: payload.fontSeam,
|
|
774
|
+
facts: fontFacts,
|
|
775
|
+
declaredWeights: payload.document.foundations.typography.weights,
|
|
776
|
+
});
|
|
777
|
+
if (!plan.ok)
|
|
699
778
|
continue;
|
|
779
|
+
const snippet = plan;
|
|
700
780
|
const header = [
|
|
701
781
|
`// Self-hosted type for the "${payload.slug}" design system (via next/font -`,
|
|
702
782
|
`// preloaded, no font flash). Generated by \`synthesisui add\`; edit freely.`,
|
|
@@ -2,8 +2,9 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { emitCn, readVocabulary } from "../cn-codegen.js";
|
|
5
|
-
import { generateComponentFiles } from "../component-codegen.js";
|
|
5
|
+
import { generateComponentFiles, } from "../component-codegen.js";
|
|
6
6
|
import { readProjectConfig, resolveRegistry } from "../config.js";
|
|
7
|
+
import { detectAppDirs } from "../global-sheet.js";
|
|
7
8
|
import { hasInteractiveTemplate, interactiveTemplate, } from "../interactive-templates.js";
|
|
8
9
|
import { body, section, snippet } from "../output.js";
|
|
9
10
|
import { findCollision, installedThemeVars, reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
|
|
@@ -12,6 +13,7 @@ import { installedThemeCss, whatOnlyTheSheetResolves, } from "../sheet-needed.js
|
|
|
12
13
|
import { flavourResolver } from "../styles-flavour.js";
|
|
13
14
|
import { inTheirTongue, projectTongue, sumSpoken, } from "../their-tongue.js";
|
|
14
15
|
import { readCensus, unreadComment, unreadForComponent, } from "../unread-for-component.js";
|
|
16
|
+
import { readWiring } from "../wiring-read.js";
|
|
15
17
|
import { editedSinceWritten, readWritten, recordWritten } from "../written.js";
|
|
16
18
|
/**
|
|
17
19
|
* Writes the shared `cn.ts` next to the components, built from THIS project's
|
|
@@ -69,6 +71,18 @@ export function localName(blueprint, asked) {
|
|
|
69
71
|
* The component's styles reference the DS tokens, so the system itself must be
|
|
70
72
|
* installed (`synthesisui add <slug>`) for `tokens.css`/`theme.css` to resolve.
|
|
71
73
|
*/
|
|
74
|
+
/**
|
|
75
|
+
* O QUE NESTE ARQUIVO PRECISA DA FOLHA, dito uma vez.
|
|
76
|
+
*
|
|
77
|
+
* A frase aparece nos três desfechos do bloco de setup (está aí · está no repo · falta), e três
|
|
78
|
+
* redações da mesma medição são três respostas no dia em que uma for editada.
|
|
79
|
+
*/
|
|
80
|
+
function whatNeedsIt(need) {
|
|
81
|
+
const some = (list) => `${list.slice(0, 3).join(", ")}${list.length > 3 ? `, +${list.length - 3}` : ""}`;
|
|
82
|
+
return need.variables.length > 0
|
|
83
|
+
? some(need.variables)
|
|
84
|
+
: `the ${some(need.classes)} ${need.classes.length === 1 ? "utility" : "utilities"}`;
|
|
85
|
+
}
|
|
72
86
|
export async function component(slug, name, opts) {
|
|
73
87
|
const base = resolveRegistry(opts.registry);
|
|
74
88
|
const root = opts.dir ?? process.cwd();
|
|
@@ -118,6 +132,14 @@ export async function component(slug, name, opts) {
|
|
|
118
132
|
* recipe, ou sobre o css antes da tradução, seria responder sobre um arquivo que ninguém abriu.
|
|
119
133
|
*/
|
|
120
134
|
let writtenSource = "";
|
|
135
|
+
/**
|
|
136
|
+
* O VEREDITO DA RAIZ - ver `GeneratedRoot` em `component-codegen.ts`.
|
|
137
|
+
*
|
|
138
|
+
* `null` no caminho do template interativo, e isso é uma resposta: ali o `.tsx` é curado à mão e
|
|
139
|
+
* já nasce com o elemento operável certo, então não há nada a avisar. Um `false` inventado aqui
|
|
140
|
+
* faria o comando calar por engano no caminho que ele deveria cobrir.
|
|
141
|
+
*/
|
|
142
|
+
let root_ = null;
|
|
121
143
|
const artifactsAreTheProduct = opts.artifactsOnly === true || config.target !== "next";
|
|
122
144
|
if (artifactsAreTheProduct) {
|
|
123
145
|
const dir = join(root, "_synthesisui", "ds", slug, "components");
|
|
@@ -332,7 +354,7 @@ export async function component(slug, name, opts) {
|
|
|
332
354
|
filenames = written.map((f) => f.filename);
|
|
333
355
|
}
|
|
334
356
|
else {
|
|
335
|
-
const { files, spoken: said } = generateComponentFiles(slug, res.name, res.recipe, res.css, res.version, flavour, await reactMajorOf(root),
|
|
357
|
+
const { files, spoken: said, root: generatedRoot, } = generateComponentFiles(slug, res.name, res.recipe, res.css, res.version, flavour, await reactMajorOf(root),
|
|
336
358
|
/**
|
|
337
359
|
* THE SPELLING, FROM THE VERSION WE JUST FETCHED.
|
|
338
360
|
*
|
|
@@ -353,6 +375,7 @@ export async function component(slug, name, opts) {
|
|
|
353
375
|
*/
|
|
354
376
|
local, await readInstalledScheme(root, slug), tongue, await installedThemeVars(root, slug));
|
|
355
377
|
spoken = said;
|
|
378
|
+
root_ = generatedRoot;
|
|
356
379
|
/**
|
|
357
380
|
* AS DECLARAÇÕES DO ARQUIVO DELE QUE O INTERPRETADOR NÃO LEU - ver `unread-for-component.ts`.
|
|
358
381
|
*
|
|
@@ -449,7 +472,50 @@ export async function component(slug, name, opts) {
|
|
|
449
472
|
* junto a única linha que diz COMO importar o componente. Dizer "está isolado" e sonegar o
|
|
450
473
|
* `import { ButtonSample }` troca um defeito por outro.
|
|
451
474
|
*/
|
|
452
|
-
|
|
475
|
+
/**
|
|
476
|
+
* A FIAÇÃO JÁ ESTÁ AQUI? - ver `wiring-read.ts`, a MESMA leitura que o `doctor` usa.
|
|
477
|
+
*
|
|
478
|
+
* MEDIDO em 08/09, nos dois apps do ato 2B: com os dois `@import` colados e o
|
|
479
|
+
* `data-ds="<slug>"` no `<body>`, este comando ainda imprimia os passos 1 e 2 para colar de
|
|
480
|
+
* novo. Ele perguntava se o arquivo PRECISA da folha e nunca se a folha já estava lá.
|
|
481
|
+
*
|
|
482
|
+
* As duas perguntas continuam sendo feitas, e nesta ordem: `need` decide se o setup é
|
|
483
|
+
* necessário, `wiring` decide se ele ainda está pendente.
|
|
484
|
+
*/
|
|
485
|
+
const wiring = need.needed ? await readWiring(root, slug) : null;
|
|
486
|
+
/**
|
|
487
|
+
* O ADAPTADOR CONTA quando o que precisa da folha são UTILITÁRIOS - eles só existem pelo
|
|
488
|
+
* `@theme` do `theme.css`. Dizer "está tudo aí" com o `tokens.css` sozinho declararia resolvido
|
|
489
|
+
* exatamente o que continua faltando.
|
|
490
|
+
*/
|
|
491
|
+
const sheetsThere = wiring?.imported === true && (!tailwind || wiring?.themed === true);
|
|
492
|
+
/**
|
|
493
|
+
* E A RESPOSTA É DO PROJETO, NÃO DO APP - então num repositório com mais de um app ela não
|
|
494
|
+
* decide, e o comando diz isso em vez de afirmar (`INV-VOLTA-12`: monorepo é a forma comum).
|
|
495
|
+
*
|
|
496
|
+
* `readWiring` varre a partir da raiz e responde "existe, em algum lugar daqui". Com dois apps
|
|
497
|
+
* servidos, um fiado e o outro não, um responderia pelo outro - e o arquivo que este comando
|
|
498
|
+
* acabou de escrever vai para uma pasta só de componentes, que não desambigua qual app o usa.
|
|
499
|
+
*/
|
|
500
|
+
const appDirs = await detectAppDirs(root, config.pagesDir ?? "app");
|
|
501
|
+
const oneApp = appDirs.length <= 1;
|
|
502
|
+
const alreadyWired = sheetsThere && wiring?.scoped === true;
|
|
503
|
+
if (need.needed && alreadyWired && !oneApp) {
|
|
504
|
+
console.log(section(`This file needs the sheet - and this repo has it`));
|
|
505
|
+
console.log(body(`What needs it: ${whatNeedsIt(need)}.`));
|
|
506
|
+
console.log(body(`Measured across this repository, not per app: ${slug}'s sheet is imported and`));
|
|
507
|
+
console.log(body(`data-ds="${slug}" is set somewhere in it. There ${appDirs.length === 2 ? "are two apps" : `are ${appDirs.length} apps`} here (${appDirs.slice(0, 2).join(", ")}${appDirs.length > 2 ? ", …" : ""}),`));
|
|
508
|
+
console.log(body(`so if this file lands in one that does not carry them, run \`doctor\` inside it.`));
|
|
509
|
+
}
|
|
510
|
+
if (need.needed && alreadyWired && oneApp) {
|
|
511
|
+
console.log(section(`This file needs the sheet - and it is already there`));
|
|
512
|
+
console.log(body(`What needs it: ${whatNeedsIt(need)}.`));
|
|
513
|
+
console.log(body(`This project already imports ${slug}'s ${tailwind ? "tokens.css and theme.css" : "tokens.css"} and carries data-ds="${slug}",`));
|
|
514
|
+
console.log(body(wiring?.fontsMapped
|
|
515
|
+
? `and its type is mapped onto the system's family tokens. Nothing to set up.`
|
|
516
|
+
: `so nothing to set up here. \`doctor\` says whether the type is mapped too.`));
|
|
517
|
+
}
|
|
518
|
+
if (need.needed && !alreadyWired) {
|
|
453
519
|
console.log(section(`One-time setup (once per app, for "${slug}")`));
|
|
454
520
|
console.log(body(need.variables.length > 0
|
|
455
521
|
? `(what still needs the sheet here: ${need.variables.slice(0, 3).join(", ")}${need.variables.length > 3 ? `, +${need.variables.length - 3}` : ""} - your code names no value for ${need.variables.length === 1 ? "it" : "them"})`
|
|
@@ -466,6 +532,40 @@ export async function component(slug, name, opts) {
|
|
|
466
532
|
console.log("");
|
|
467
533
|
console.log(body(`(If you haven't installed the system yet, run: synthesisui add ${slug})`));
|
|
468
534
|
}
|
|
535
|
+
/**
|
|
536
|
+
* O ELEMENTO QUE SAIU NÃO ATIVA O QUE A RECEITA PEDE - e isso se diz, em vez de sumir (lei 8).
|
|
537
|
+
*
|
|
538
|
+
* O QUE O CLIENTE VIA (medido em 08/09): `<Button>Ship it</Button>` renderizou um `<div>`. Sem
|
|
539
|
+
* foco, sem teclado, sem `role` - e as declarações de `:focus-visible` e `:disabled` da própria
|
|
540
|
+
* receita inertes, porque um `div` não recebe foco nem fica desabilitado.
|
|
541
|
+
*
|
|
542
|
+
* A RAIZ NÃO-OPERÁVEL ESTÁ CORRETA: a receita não declara papel de ação, e a lei 10 manda não
|
|
543
|
+
* inventar semântica que o contrato dele não tem - um `role="button"` + `tabIndex` supostos
|
|
544
|
+
* seriam acessibilidade que a gente adivinhou, o que é pior que não fazer. O que faltava era a
|
|
545
|
+
* segunda metade: DIZER.
|
|
546
|
+
*
|
|
547
|
+
* E A MEDIÇÃO VEM DE QUEM DECIDIU - ver `GeneratedRoot`. A primeira versão deste bloco lia o
|
|
548
|
+
* texto emitido com um regex (`const Tag = (as ?? "div|span")` + `focus-visible:` na fonte), e a
|
|
549
|
+
* revisão mediu as duas cegueiras: a grafia de variante do Tailwind não existe numa folha, então
|
|
550
|
+
* o aviso morria em `styles: css`; e `(div|span)` não alcançava o `article` que o guard da lei 10
|
|
551
|
+
* devolve quando o topo tem heading - o caso mais citado neste repositório.
|
|
552
|
+
*/
|
|
553
|
+
const inertRoot = root_ != null && !root_.operable;
|
|
554
|
+
/**
|
|
555
|
+
* OS ESTADOS COM O NOME QUE O CSS DELE USA - o contrato os guarda em camelCase (`focusVisible`),
|
|
556
|
+
* e é `:focus-visible` que ele vai procurar no arquivo. A conversão é de LEITURA, para o texto
|
|
557
|
+
* falar a língua da folha e não a do nosso schema.
|
|
558
|
+
*/
|
|
559
|
+
const interactiveStates = (root_?.interactiveStates ?? []).map((state) => state.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`));
|
|
560
|
+
if (interactiveStates.length > 0 && inertRoot) {
|
|
561
|
+
console.log(section("It renders a container, not a control"));
|
|
562
|
+
console.log(body(`This recipe declares no action role, so the root is a <${root_?.tag}> - we do not`));
|
|
563
|
+
console.log(body(`invent semantics your contract does not have. But it styles ${interactiveStates.join(", ")},`));
|
|
564
|
+
console.log(body(`and a <${root_?.tag}> reaches none of them: no focus, no keyboard, nothing announced.`));
|
|
565
|
+
console.log("");
|
|
566
|
+
console.log(body(`If it is interactive, the file takes the element as a prop - \`as="button"\`,`));
|
|
567
|
+
console.log(body(`\`as="a"\`, whatever it really is. Which one it is, is yours to say.`));
|
|
568
|
+
}
|
|
469
569
|
console.log(section("Use it"));
|
|
470
570
|
if (!opts.artifactsOnly && config.target === "next") {
|
|
471
571
|
/**
|
package/dist/commands/doctor.js
CHANGED
|
@@ -16,7 +16,6 @@ import { diagnose, nameToWrite, scanSource, siblingTokens, } from "../doctor/sca
|
|
|
16
16
|
import { findSelfConflicts, forbiddenProps, isReset, propMatchesLabel, } from "../doctor/self-conflict.js";
|
|
17
17
|
import { withTheirNames } from "../doctor/their-names.js";
|
|
18
18
|
import { buildTable, EMPTY_TABLE, nearestToken, } from "../doctor/tokens.js";
|
|
19
|
-
import { FAMILY_SEAM_PREFIX } from "../fonts.js";
|
|
20
19
|
import { detectAppDirs, globalSheetOf, prefixFrom } from "../global-sheet.js";
|
|
21
20
|
import { groupRole } from "../group-role.js";
|
|
22
21
|
import { actingSlug, describeScope, measuredScope, scopePaths, } from "../measured-scope.js";
|
|
@@ -26,6 +25,7 @@ import { installedThemeCss, whatOnlyTheSheetResolves, } from "../sheet-needed.js
|
|
|
26
25
|
import { resolveDeps } from "../stack.js";
|
|
27
26
|
import { projectTongue } from "../their-tongue.js";
|
|
28
27
|
import { danglingTheirVars } from "../their-vars.js";
|
|
28
|
+
import { readWiring } from "../wiring-read.js";
|
|
29
29
|
/**
|
|
30
30
|
* `synthesisui doctor` - the check nobody else ships.
|
|
31
31
|
*
|
|
@@ -542,78 +542,6 @@ function verdict(d, hasSystem, overruled, conflicts) {
|
|
|
542
542
|
}
|
|
543
543
|
return lines;
|
|
544
544
|
}
|
|
545
|
-
/**
|
|
546
|
-
* Wiring is a property of the PROJECT, never of the scope being read.
|
|
547
|
-
*
|
|
548
|
-
* This ran inside the scoped walk, so `doctor app/login` read only that folder
|
|
549
|
-
* - and the import lives in `app/globals.css`. It concluded "installed, but not
|
|
550
|
-
* wired up yet" about a project that was perfectly wired (my-test4, 27/07).
|
|
551
|
-
*
|
|
552
|
-
* Which would be a mild annoyance, except the managed block now tells the agent
|
|
553
|
-
* to run `doctor <the file you just wrote>` after every edit. The false alarm
|
|
554
|
-
* fires on every loop, and an agent that believes the system is unwired starts
|
|
555
|
-
* repairing wiring that was already correct - editing the one file we most need
|
|
556
|
-
* it to leave alone.
|
|
557
|
-
*
|
|
558
|
-
* The system itself was always resolved from the root. So is this now.
|
|
559
|
-
*/
|
|
560
|
-
async function readWiring(root, slug) {
|
|
561
|
-
const w = {
|
|
562
|
-
imported: false,
|
|
563
|
-
scoped: false,
|
|
564
|
-
/** `init` wrote a fonts file for this project (Next targets only). */
|
|
565
|
-
fontsWritten: false,
|
|
566
|
-
/** …and the stylesheet actually maps it onto the system's type tokens. */
|
|
567
|
-
fontsMapped: false,
|
|
568
|
-
/**
|
|
569
|
-
* THE FEATURE NOBODY KNEW WE SHIPPED.
|
|
570
|
-
*
|
|
571
|
-
* Every system compiles a `shadcn.css` mapping shadcn's whole variable
|
|
572
|
-
* contract onto its own tokens, so shadcn components wear the system instead
|
|
573
|
-
* of shadcn's defaults. It has been generated into every project since it
|
|
574
|
-
* was built, and until 28/07 nothing mentioned it: not `add`, not the
|
|
575
|
-
* managed block, not the GUIDE except as a filename in a list. The only
|
|
576
|
-
* documentation was a comment inside the file, which is the same as none.
|
|
577
|
-
*
|
|
578
|
-
* The person it mattered most to is the one who asked "I don't get the use
|
|
579
|
-
* case, I can just build a design system with shadcn" - and the answer was
|
|
580
|
-
* sitting unimported in his own repo.
|
|
581
|
-
*
|
|
582
|
-
* Reported only when the project HAS shadcn. Telling everybody about a
|
|
583
|
-
* bridge they will never use is how a report earns the skim.
|
|
584
|
-
*/
|
|
585
|
-
hasShadcn: false,
|
|
586
|
-
bridged: false,
|
|
587
|
-
};
|
|
588
|
-
if (!slug)
|
|
589
|
-
return w;
|
|
590
|
-
w.hasShadcn = await readFile(join(root, "components.json"), "utf8").then(() => true, () => false);
|
|
591
|
-
for await (const file of walk(root)) {
|
|
592
|
-
const src = await readFile(file, "utf8").catch(() => "");
|
|
593
|
-
if (!src)
|
|
594
|
-
continue;
|
|
595
|
-
if (src.includes(`_synthesisui/ds/${slug}/tokens.css`))
|
|
596
|
-
w.imported = true;
|
|
597
|
-
if (src.includes(`_synthesisui/ds/${slug}/shadcn.css`))
|
|
598
|
-
w.bridged = true;
|
|
599
|
-
if (src.includes(`data-ds="${slug}"`))
|
|
600
|
-
w.scoped = true;
|
|
601
|
-
// The requirement that stayed invisible. `init` writes a fonts file and
|
|
602
|
-
// asks for two more edits; an agent told to check only the first two did
|
|
603
|
-
// exactly that, stopped, and left the project rendering in the framework's
|
|
604
|
-
// default face (my-test2, 27/07). What the checker checks is what gets done.
|
|
605
|
-
if (src.includes("--font-ds-") && /next\/font/.test(src))
|
|
606
|
-
w.fontsWritten = true;
|
|
607
|
-
/** O prefixo vem da const única da costura - um literal aqui divergiria em silêncio (26/08). */
|
|
608
|
-
if (new RegExp(`${FAMILY_SEAM_PREFIX}\\w+\\s*:\\s*var\\(\\s*--font-ds-`).test(src))
|
|
609
|
-
w.fontsMapped = true;
|
|
610
|
-
// The bridge joins the early exit, otherwise the walk can stop before the
|
|
611
|
-
// stylesheet that imports it and report a wired project as unbridged.
|
|
612
|
-
if (w.imported && w.scoped && w.fontsMapped && (!w.hasShadcn || w.bridged))
|
|
613
|
-
break;
|
|
614
|
-
}
|
|
615
|
-
return w;
|
|
616
|
-
}
|
|
617
545
|
/** Onde o retrato mora por default: dentro do que a esteira já escreve, e commitável. */
|
|
618
546
|
const DEFAULT_BASELINE = "_synthesisui/drift-baseline.json";
|
|
619
547
|
/**
|
package/dist/commands/list.js
CHANGED
|
@@ -22,6 +22,8 @@ export async function list(opts) {
|
|
|
22
22
|
}
|
|
23
23
|
console.log("Your design systems:\n");
|
|
24
24
|
const width = Math.max(...mine.map((s) => s.slug.length));
|
|
25
|
+
/** Algum sistema tem rascunho à frente do que o `add` entrega? Só então a legenda tem serventia. */
|
|
26
|
+
let anyDraftAhead = false;
|
|
25
27
|
for (const s of mine) {
|
|
26
28
|
/**
|
|
27
29
|
* O GRUPO ENTRE PARÊNTESES, e "invited" quando ele não é dela - as duas
|
|
@@ -30,7 +32,27 @@ export async function list(opts) {
|
|
|
30
32
|
const group = s.group
|
|
31
33
|
? `${s.group}${s.mine ? "" : ", invited"}`
|
|
32
34
|
: "no group";
|
|
33
|
-
|
|
35
|
+
/**
|
|
36
|
+
* O NÚMERO QUE ELE RECEBE VEM PRIMEIRO, e o rascunho aparece como rascunho.
|
|
37
|
+
*
|
|
38
|
+
* O DEFEITO (medido em 08/09, num projeto novo): esta linha imprimia `codelevel (v3)` e o
|
|
39
|
+
* `add` instalava `CodeLevel v2`. A listagem mostrava a linha mais nova, que é o rascunho do
|
|
40
|
+
* Studio; o `add` serve a última publicada. Duas verdades sobre coisas diferentes, e nenhuma
|
|
41
|
+
* palavra separando as duas - então o cliente lia um número e recebia outro.
|
|
42
|
+
*
|
|
43
|
+
* Sem `installable` (registry antigo) nada muda: um número só, como antes.
|
|
44
|
+
*/
|
|
45
|
+
const installable = s.installable ?? s.version;
|
|
46
|
+
const draftAhead = s.version > installable;
|
|
47
|
+
if (draftAhead)
|
|
48
|
+
anyDraftAhead = true;
|
|
49
|
+
const versions = draftAhead
|
|
50
|
+
? `v${installable} · v${s.version} draft`
|
|
51
|
+
: `v${installable}`;
|
|
52
|
+
console.log(` ${s.slug.padEnd(width)} ${s.name} (${versions}) · ${group}`);
|
|
53
|
+
}
|
|
54
|
+
if (anyDraftAhead) {
|
|
55
|
+
console.log("\n`add` installs the published version; the draft is what the Studio is editing.");
|
|
34
56
|
}
|
|
35
57
|
console.log('\nWork in one with: synthesisui use <slug> "<what you are building>"');
|
|
36
58
|
return;
|
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
import { emittableAttrs } from "./attr-shape.js";
|
|
2
2
|
import { inTheirTongue, sumSpoken, } from "./their-tongue.js";
|
|
3
3
|
import { PHRASING_FORMS, } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* OS ELEMENTOS QUE SÃO OPERÁVEIS POR NATUREZA - a lista vem do HTML, e é por isso que ela é uma
|
|
6
|
+
* regra e não uma foto: nenhum repositório de cliente a muda.
|
|
7
|
+
*
|
|
8
|
+
* A pergunta que ela responde é a do avesso: uma tag FORA desta lista não recebe foco de teclado
|
|
9
|
+
* nem fica desabilitada, então toda declaração de `:focus-visible` e `:disabled` que a receita
|
|
10
|
+
* carrega fica inerte nela.
|
|
11
|
+
*/
|
|
12
|
+
const OPERABLE_ELEMENTS = new Set([
|
|
13
|
+
"button",
|
|
14
|
+
"a",
|
|
15
|
+
"input",
|
|
16
|
+
"select",
|
|
17
|
+
"textarea",
|
|
18
|
+
"summary",
|
|
19
|
+
"details",
|
|
20
|
+
"label",
|
|
21
|
+
"option",
|
|
22
|
+
]);
|
|
4
23
|
export const DEFAULT_CONVENTION = {
|
|
5
24
|
prefix: "ds-",
|
|
6
25
|
partSeparator: "-",
|
|
@@ -49,6 +68,32 @@ const camel = (name) => {
|
|
|
49
68
|
export function partIsInteractive(part) {
|
|
50
69
|
return Object.keys(part.states ?? {}).some((s) => /^(focus|focusVisible|focus-visible|disabled)$/i.test(s));
|
|
51
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* OS ESTADOS OPERÁVEIS QUE A RECEITA DECLARA - nas DUAS formas em que uma receita os declara.
|
|
73
|
+
*
|
|
74
|
+
* O DEFEITO QUE ISTO CONSERTA, medido no `button` do `codelevel` em 08/09: a primeira versão lia
|
|
75
|
+
* `recipe.states`, que é a forma das receitas que a PLATAFORMA GERA. Numa receita LIDA do
|
|
76
|
+
* repositório do cliente o mesmo fato mora em `layers[].when.state` - `recipe.states` vem vazio -,
|
|
77
|
+
* então o aviso deixou de sair exatamente no caso que o originou.
|
|
78
|
+
*
|
|
79
|
+
* É a mesma lição que `rootAxes` já carrega: *"axesOf alone keeps an axis only when the option's
|
|
80
|
+
* own block carries declarations - true for a recipe we generated, and false for every recipe we
|
|
81
|
+
* READ"*. Um leitor que conhece uma forma só atende metade da população, e a metade que ele perde é
|
|
82
|
+
* justamente a do cliente.
|
|
83
|
+
*/
|
|
84
|
+
export function interactiveStatesOf(recipe) {
|
|
85
|
+
const OPERABLE_STATE = /^(focus|focusVisible|focus-visible|disabled)$/i;
|
|
86
|
+
const found = new Set();
|
|
87
|
+
for (const state of Object.keys(recipe.states ?? {}))
|
|
88
|
+
if (OPERABLE_STATE.test(state))
|
|
89
|
+
found.add(state);
|
|
90
|
+
for (const layer of recipe.layers ?? []) {
|
|
91
|
+
const state = layer.when?.state;
|
|
92
|
+
if (state && OPERABLE_STATE.test(state))
|
|
93
|
+
found.add(state);
|
|
94
|
+
}
|
|
95
|
+
return [...found];
|
|
96
|
+
}
|
|
52
97
|
/** Intrinsic element + extra attrs per component, chosen like the platform
|
|
53
98
|
* renderer does (name first, then preview.kind). Fallback: div + children. */
|
|
54
99
|
function elementFor(name, recipe) {
|
|
@@ -941,6 +986,30 @@ function treeParts(nodes, out = []) {
|
|
|
941
986
|
}
|
|
942
987
|
return out;
|
|
943
988
|
}
|
|
989
|
+
/**
|
|
990
|
+
* A ÁRVORE JÁ TEM ONDE O CONTEÚDO DELE ENTRA?
|
|
991
|
+
*
|
|
992
|
+
* O QUE O CLIENTE VIA (medido em 08/09, num `create-next-app` com o `codelevel`): ele escreveu
|
|
993
|
+
* `<Button>Ship it</Button>` e a tela mostrou **"Ship it" duas vezes** - duas ocorrências dentro do
|
|
994
|
+
* `<main>` servido, não uma.
|
|
995
|
+
*
|
|
996
|
+
* A CAUSA: a árvore do recipe tem um nó `as: "slot"`, e `emitTree` emite `{children}` nele - é o
|
|
997
|
+
* lugar do conteúdo, dentro da parte que o desenha. A raiz emitia `{children}` OUTRA VEZ, logo
|
|
998
|
+
* depois da árvore. Não é um caso do `button`: vale para toda receita cuja árvore tenha slot.
|
|
999
|
+
*
|
|
1000
|
+
* ENTÃO A PERGUNTA PASSA A SER FEITA. Com slot na árvore, o conteúdo já tem endereço e a raiz não
|
|
1001
|
+
* o repete; sem slot, a raiz continua sendo o único lugar onde ele pode entrar - e sonegá-lo ali
|
|
1002
|
+
* seria trocar um defeito por outro, um componente que ignora o que o cliente escreveu dentro.
|
|
1003
|
+
*/
|
|
1004
|
+
function treeHasSlot(nodes) {
|
|
1005
|
+
for (const node of nodes ?? []) {
|
|
1006
|
+
if (node.as === "slot")
|
|
1007
|
+
return true;
|
|
1008
|
+
if (node.children && treeHasSlot(node.children))
|
|
1009
|
+
return true;
|
|
1010
|
+
}
|
|
1011
|
+
return false;
|
|
1012
|
+
}
|
|
944
1013
|
/**
|
|
945
1014
|
* THE COMPOSITION, EMITTED AS JSX RATHER THAN DESCRIBED IN A COMMENT.
|
|
946
1015
|
*
|
|
@@ -1114,8 +1183,13 @@ localName = name) {
|
|
|
1114
1183
|
"...props",
|
|
1115
1184
|
].join(", ");
|
|
1116
1185
|
const rootOpen = ` <${el.jsxTag}${el.jsxAttrs}\n className={${joinCls([`"${elementClass(name, convention)}"`, "className"])}}\n${dataAttrLines(axes)}${axes.length ? "\n" : ""} {...props}\n `;
|
|
1186
|
+
/**
|
|
1187
|
+
* O CONTEÚDO DELE ENTRA UMA VEZ - ver `treeHasSlot`. Com slot na árvore, o `{children}` da raiz
|
|
1188
|
+
* duplicava na tela o que o cliente escreveu.
|
|
1189
|
+
*/
|
|
1190
|
+
const rootTakesChildren = hasShape && !treeHasSlot(tree);
|
|
1117
1191
|
const rootJsx = hasShape
|
|
1118
|
-
? `${rootOpen}>\n${emitTree(tree, comp, " ")}\n {children}\n </${el.jsxTag}>`
|
|
1192
|
+
? `${rootOpen}>\n${emitTree(tree, comp, " ")}${rootTakesChildren ? "\n {children}" : ""}\n </${el.jsxTag}>`
|
|
1119
1193
|
: `${rootOpen}/>`;
|
|
1120
1194
|
// Tree order first, because that is the order somebody reads them in the file;
|
|
1121
1195
|
// anything the tree does not mention still gets its component.
|
|
@@ -1296,8 +1370,7 @@ ${el.setup} return (
|
|
|
1296
1370
|
${hasShape
|
|
1297
1371
|
? `${rootOpen} {...props}
|
|
1298
1372
|
>
|
|
1299
|
-
${emitTree(tree, comp, " ")}
|
|
1300
|
-
{children}
|
|
1373
|
+
${emitTree(tree, comp, " ")}${treeHasSlot(tree) ? "" : "\n {children}"}
|
|
1301
1374
|
</${el.jsxTag}>`
|
|
1302
1375
|
: `${rootOpen} {...props}
|
|
1303
1376
|
/>`}
|
|
@@ -1461,7 +1534,20 @@ themeVars) {
|
|
|
1461
1534
|
filename: "index.ts",
|
|
1462
1535
|
code: `export * from "./${localName}";\n`,
|
|
1463
1536
|
});
|
|
1464
|
-
|
|
1537
|
+
/**
|
|
1538
|
+
* O VEREDITO DA RAIZ SAI DAQUI, de `elementFor` e do contrato - ver `GeneratedRoot`.
|
|
1539
|
+
*
|
|
1540
|
+
* `interactiveStates` lê `recipe.states`, que é o mesmo dado nas duas línguas; a alternativa que
|
|
1541
|
+
* a revisão derrubou lia a grafia de variante do Tailwind na fonte emitida e ficava cega em
|
|
1542
|
+
* `styles: css`.
|
|
1543
|
+
*/
|
|
1544
|
+
const rootEl = elementFor(name, recipe);
|
|
1545
|
+
const root = {
|
|
1546
|
+
tag: rootEl.tag,
|
|
1547
|
+
interactiveStates: interactiveStatesOf(recipe),
|
|
1548
|
+
operable: OPERABLE_ELEMENTS.has(rootEl.tag),
|
|
1549
|
+
};
|
|
1550
|
+
return speak(files, tongue, styles === "tailwind" ? "tailwind-class" : "stylesheet", root);
|
|
1465
1551
|
}
|
|
1466
1552
|
/**
|
|
1467
1553
|
* A TRADUÇÃO, SOBRE OS BYTES, UMA VEZ - e o destino é de quem GEROU o texto, não da extensão.
|
|
@@ -1475,14 +1561,14 @@ themeVars) {
|
|
|
1475
1561
|
* `index.ts` é um re-export e não carrega estilo, mas passa pela mesma porta: um arquivo isento por
|
|
1476
1562
|
* lista é a lista que alguém esquece de atualizar.
|
|
1477
1563
|
*/
|
|
1478
|
-
function speak(files, tongue, tsxDestination) {
|
|
1564
|
+
function speak(files, tongue, tsxDestination, root) {
|
|
1479
1565
|
if (!tongue)
|
|
1480
|
-
return { files, spoken: null };
|
|
1566
|
+
return { files, spoken: null, root };
|
|
1481
1567
|
const reports = [];
|
|
1482
1568
|
const said = files.map((file) => {
|
|
1483
1569
|
const spoken = inTheirTongue(file.code, tongue, file.filename.endsWith(".css") ? "stylesheet" : tsxDestination);
|
|
1484
1570
|
reports.push(spoken);
|
|
1485
1571
|
return { filename: file.filename, code: spoken.css };
|
|
1486
1572
|
});
|
|
1487
|
-
return { files: said, spoken: sumSpoken(reports) };
|
|
1573
|
+
return { files: said, spoken: sumSpoken(reports), root };
|
|
1488
1574
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UM `FontFacts` COMPLETO A PARTIR DO QUE VOCÊ QUER DIZER - a porta que faltava fechar.
|
|
3
|
+
*
|
|
4
|
+
* O SUFIXO `.spec-input` DECLARA A INTENÇÃO NO PONTO DE IMPORTAÇÃO - ver `reachable.spec.ts`: este
|
|
5
|
+
* módulo existe só para alimentar spec, e é uma CATEGORIA, não uma órfã que alguém esqueceu de
|
|
6
|
+
* ligar. Ele nunca entra em código de produto.
|
|
7
|
+
*
|
|
8
|
+
* `buildGuide` e `nextFontSnippet` fecharam "esquecer o ARGUMENTO"; ficou aberto "esquecer um CAMPO
|
|
9
|
+
* dele", e isso custou 35 testes: quando `subsetsOf` nasceu, as fixtures escritas à mão em dez
|
|
10
|
+
* lugares não a tinham, o `guide.spec.ts` parou de CARREGAR, e a suíte seguiu verde com o arquivo
|
|
11
|
+
* inteiro fora da contagem. Só o `tsc` pegava, e o `tsc` dos specs não é o portão de cada rodada.
|
|
12
|
+
*
|
|
13
|
+
* Quem escreve um teste diz só o que importa para ele; os outros campos vêm completos por
|
|
14
|
+
* construção, e um campo novo neste tipo passa a chegar em toda fixture sozinho.
|
|
15
|
+
*/
|
|
16
|
+
export function factsOf(over = {}) {
|
|
17
|
+
const weights = over.weights ?? ["400", "500", "600"];
|
|
18
|
+
const subsets = over.subsets ?? ["latin"];
|
|
19
|
+
return {
|
|
20
|
+
weightsOf: () => weights,
|
|
21
|
+
nameOf: (family) => over.names?.[family] ?? family,
|
|
22
|
+
subsetsOf: () => subsets,
|
|
23
|
+
};
|
|
24
|
+
}
|
package/dist/fonts.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { subsetsToEmit, weightsToEmit, } from "./next-font-weights.js";
|
|
1
2
|
// Fallbacks genéricos do CSS - não são webfonts.
|
|
2
3
|
const GENERIC_FAMILIES = new Set([
|
|
3
4
|
"sans-serif",
|
|
@@ -87,61 +88,156 @@ export function googleFontsHref(families) {
|
|
|
87
88
|
* batiza em `fontSeamOf`. Payload novo dispensa a convenção: o dado vence.
|
|
88
89
|
*/
|
|
89
90
|
export const FAMILY_SEAM_PREFIX = "--ds-typography-families-";
|
|
90
|
-
export function nextFontSnippet(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
91
|
+
export function nextFontSnippet(input) {
|
|
92
|
+
const { families, slug, appDir = "app", seam, facts, declaredWeights, } = input;
|
|
93
|
+
/**
|
|
94
|
+
* A ORDEM É DETERMINÍSTICA e os duplicados saem: isto vira uma linha de arquivo gerado, e um
|
|
95
|
+
* conjunto que muda de ordem entre rodadas produz diff onde nada mudou.
|
|
96
|
+
*/
|
|
97
|
+
const wanted = declaredWeights
|
|
98
|
+
? [...new Set(Object.values(declaredWeights).map(String))].sort()
|
|
99
|
+
: ["400", "500", "600"];
|
|
97
100
|
/** Toda vaga que o documento tem, não só as nossas três - ver `customFontFamilies`. */
|
|
98
101
|
const roles = familySlots(families).filter((role) => {
|
|
99
102
|
const name = firstFamily(families[role] ?? "");
|
|
100
103
|
return name && !GENERIC_FAMILIES.has(name.toLowerCase());
|
|
101
104
|
});
|
|
105
|
+
const named = [
|
|
106
|
+
...new Set(roles.map((role) => firstFamily(families[role] ?? ""))),
|
|
107
|
+
];
|
|
102
108
|
if (roles.length === 0)
|
|
103
|
-
return
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
return { ok: false, why: "generic-only", families: [] };
|
|
110
|
+
/**
|
|
111
|
+
* SEM MANIFESTO, NADA É EMITIDO - e o motivo viaja para quem chama.
|
|
112
|
+
*
|
|
113
|
+
* A alternativa seria emitir os três degraus "porque quase sempre existem", que é exatamente a
|
|
114
|
+
* suposição que derrubou o build de um projeto real em 08/09. Um arquivo não escrito custa uma
|
|
115
|
+
* frase ao cliente; um arquivo escrito errado custa o build dele.
|
|
116
|
+
*/
|
|
117
|
+
if (!facts)
|
|
118
|
+
return { ok: false, why: "manifest-unreadable", families: named };
|
|
119
|
+
/**
|
|
120
|
+
* O IDENTIFICADOR SAI DA GRAFIA DO MANIFESTO, nunca de uma regra nossa - ver `FontFacts.nameOf`.
|
|
121
|
+
*
|
|
122
|
+
* A versão anterior fazia `replace(/ /g, "_")` sobre o nome como o documento DELE o escreve, e um
|
|
123
|
+
* `font-family: instrument serif` produzia `import { instrument_serif }`, que não existe. Uma
|
|
124
|
+
* capitalização "por palavra" trocaria esse defeito por outro: `IBM Plex Mono` sairia
|
|
125
|
+
* `Ibm_Plex_Mono`.
|
|
126
|
+
*
|
|
127
|
+
* E NÃO HÁ FALLBACK, de propósito. Uma família sem grafia conhecida é uma família que o manifesto
|
|
128
|
+
* não conhece, e ela segue o mesmo caminho de sempre: sai em `unknown`, sem linha escrita. Manter
|
|
129
|
+
* a heurística como último recurso deixaria a porta aberta para reescrever exatamente o import
|
|
130
|
+
* que quebra o build dele - vigiar em vez de fechar.
|
|
131
|
+
*/
|
|
132
|
+
const importName = (canonical) => canonical.replace(/ /g, "_");
|
|
133
|
+
/**
|
|
134
|
+
* A DEDUPLICAÇÃO É PELA GRAFIA CANÔNICA, e não pelo texto dele - a porta que a canonização
|
|
135
|
+
* ABRIU, achada pela revisão deste PR.
|
|
136
|
+
*
|
|
137
|
+
* Medido: um documento com `display: "Geist"` e `body: "geist, sans-serif"` - dois papéis
|
|
138
|
+
* chegando à mesma família com caixa diferente, que é o normal quando o CSS dele foi escrito por
|
|
139
|
+
* mãos e épocas distintas - produzia
|
|
140
|
+
*
|
|
141
|
+
* import { Geist, Geist } from "next/font/google";
|
|
142
|
+
* SyntaxError: Identifier 'Geist' has already been declared
|
|
143
|
+
*
|
|
144
|
+
* Enquanto o identificador era composto do texto dele, as duas grafias davam dois nomes
|
|
145
|
+
* diferentes (`geist` e `Geist`) - inválidos, mas distintos. Canonizar o nome sem canonizar a
|
|
146
|
+
* CHAVE trocou "dois imports errados" por "um arquivo que não parseia": o mesmo EXIT=1 da
|
|
147
|
+
* `INV-VOLTA-16`, pela porta que este próprio conserto criou.
|
|
148
|
+
*
|
|
149
|
+
* A chave é a grafia do manifesto - a mesma resposta para as duas escritas dele.
|
|
150
|
+
*/
|
|
151
|
+
const seen = new Map(); // canonical family -> const name
|
|
106
152
|
const importNames = [];
|
|
107
153
|
const consts = [];
|
|
154
|
+
/** Família cujos pesos o manifesto não conhece: não se inventa peso para ela. */
|
|
155
|
+
const unknown = [];
|
|
108
156
|
for (const role of roles) {
|
|
109
157
|
const name = firstFamily(families[role] ?? "");
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
158
|
+
const canonical = facts.nameOf(name);
|
|
159
|
+
if (!canonical || !seen.has(canonical)) {
|
|
160
|
+
const available = facts.weightsOf(name);
|
|
161
|
+
const emit = available ? weightsToEmit(available, wanted) : [];
|
|
162
|
+
if (emit.length === 0 || !canonical) {
|
|
163
|
+
if (!unknown.includes(name))
|
|
164
|
+
unknown.push(name);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* OS SUBSETS TAMBÉM SÃO LIDOS - ver `subsetsToEmit`. Era `["latin"]` fixo, e o `next` reprova
|
|
169
|
+
* subset inexistente na MESMA função em que reprova peso inexistente: 121 das 1911 famílias
|
|
170
|
+
* do manifesto não têm `latin`, então a terceira porta deste módulo levava ao mesmo EXIT=1.
|
|
171
|
+
*
|
|
172
|
+
* Lista vazia significa que a família não declara subset nenhum, e aí a chave não é escrita:
|
|
173
|
+
* o `next` desliga o preload sozinho, e escrever `["latin"]` seria afirmar o que ela não tem.
|
|
174
|
+
*/
|
|
175
|
+
const subsets = subsetsToEmit(facts.subsetsOf(name) ?? []);
|
|
176
|
+
seen.set(canonical, role);
|
|
177
|
+
importNames.push(importName(canonical));
|
|
178
|
+
consts.push(`export const ${role} = ${importName(canonical)}({`, ...(subsets.length > 0
|
|
179
|
+
? [` subsets: [${subsets.map((x) => `"${x}"`).join(", ")}],`]
|
|
180
|
+
: []),
|
|
181
|
+
/**
|
|
182
|
+
* OS PESOS SÃO OS QUE A FAMÍLIA TEM - ver `next-font-weights.ts`.
|
|
183
|
+
*
|
|
184
|
+
* Escrevê-los é obrigatório: `next/font` só aceita omissão para fonte VARIÁVEL, e muita
|
|
185
|
+
* família do Google é entregue em cortes estáticos (IBM Plex Mono, entre outras) - o
|
|
186
|
+
* arquivo gerado assumia variável e quebrava o primeiro `next build` do cliente (29/07).
|
|
187
|
+
*
|
|
188
|
+
* E escrevê-los FIXOS quebra o outro lado: `Instrument Serif` tem um único peso, e o
|
|
189
|
+
* `["400","500","600"]` de antes derrubou um `create-next-app` inteiro em 08/09. Os dois
|
|
190
|
+
* defeitos são o mesmo erro - afirmar sobre a fonte dele em vez de ler.
|
|
191
|
+
*/
|
|
192
|
+
` weight: [${emit.map((w) => `"${w}"`).join(", ")}],`, ` variable: "--font-ds-${role}",`, `});`);
|
|
124
193
|
}
|
|
125
194
|
}
|
|
195
|
+
/** Nenhuma família sobreviveu: o manifesto existe e não conhece nenhuma delas. */
|
|
196
|
+
if (importNames.length === 0)
|
|
197
|
+
return { ok: false, why: "family-unknown", families: unknown };
|
|
126
198
|
const fontsFile = [
|
|
127
199
|
`// ${appDir}/fonts.ts`,
|
|
128
200
|
`import { ${importNames.join(", ")} } from "next/font/google";`,
|
|
129
201
|
...consts,
|
|
130
202
|
];
|
|
131
|
-
|
|
203
|
+
/**
|
|
204
|
+
* A CONST QUE ESTE PAPEL USA - uma resposta só, e é o que impede as três leituras de divergirem.
|
|
205
|
+
*
|
|
206
|
+
* O `layout`, o mapa de CSS e o `roleVar` recompunham a chave cada um do seu jeito a partir do
|
|
207
|
+
* texto dele; com a deduplicação passando a ser canônica, três recomposições são três chances de
|
|
208
|
+
* uma delas citar uma const que o arquivo não exporta.
|
|
209
|
+
*/
|
|
210
|
+
const constOf = (role) => {
|
|
132
211
|
const name = firstFamily(families[role] ?? "");
|
|
133
|
-
|
|
212
|
+
const canonical = facts.nameOf(name);
|
|
213
|
+
return canonical ? seen.get(canonical) : undefined;
|
|
134
214
|
};
|
|
215
|
+
const roleVar = (role) => `--font-ds-${constOf(role)}`;
|
|
216
|
+
/**
|
|
217
|
+
* SÓ AS VAGAS QUE VIRARAM CÓDIGO - uma família descartada por peso desconhecido não pode aparecer
|
|
218
|
+
* no `layout` nem no mapa de CSS: o import citaria uma const que o `fonts.ts` não exporta, e o
|
|
219
|
+
* projeto dele deixaria de compilar por causa da linha que a gente mandou colar.
|
|
220
|
+
*/
|
|
221
|
+
const emitted = roles.filter((role) => constOf(role) !== undefined);
|
|
135
222
|
const layout = [
|
|
136
223
|
`// ${appDir}/layout.tsx`,
|
|
137
|
-
`import { ${[...new Set(
|
|
138
|
-
`<body data-ds="${slug}" className={\`${[...new Set(
|
|
224
|
+
`import { ${[...new Set(emitted.map(constOf))].join(", ")} } from "./fonts";`,
|
|
225
|
+
`<body data-ds="${slug}" className={\`${[...new Set(emitted.map((r) => `\${${constOf(r)}.variable}`))].join(" ")}\`}>`,
|
|
139
226
|
];
|
|
140
227
|
const css = [
|
|
141
228
|
`/* ${appDir}/globals.css - AFTER the tokens.css import */`,
|
|
142
229
|
`[data-ds="${slug}"] {`,
|
|
143
|
-
...
|
|
230
|
+
...emitted.map((role) => ` ${seam?.[role] ?? `${FAMILY_SEAM_PREFIX}${role}`}: var(${roleVar(role)});`),
|
|
144
231
|
`}`,
|
|
145
232
|
];
|
|
146
|
-
|
|
233
|
+
/**
|
|
234
|
+
* E A FAMÍLIA QUE FICOU DE FORA VIAJA - lei 8, e este módulo já dizia isto sobre o caso vizinho:
|
|
235
|
+
* *"um token que aponta para uma fonte que ninguém carregou é pior que um token ausente - o
|
|
236
|
+
* ausente pelo menos aparece na conta"*.
|
|
237
|
+
*
|
|
238
|
+
* Com duas famílias e só uma no manifesto, a outra saía do `fonts.ts`, do `layout` e do mapa de
|
|
239
|
+
* CSS sem uma palavra: o token dela continua no `tokens.css`, e o texto sai no fallback do
|
|
240
|
+
* navegador sem ninguém saber por quê.
|
|
241
|
+
*/
|
|
242
|
+
return { ok: true, fontsFile, layout, css, unknown };
|
|
147
243
|
}
|
package/dist/guide.js
CHANGED
|
@@ -279,7 +279,21 @@ export function buildGuide(payload,
|
|
|
279
279
|
* `false` mantém o guia inteiro, que é o comportamento de todo repositório sem o servidor
|
|
280
280
|
* registrado.
|
|
281
281
|
*/
|
|
282
|
-
toolsReachable = false
|
|
282
|
+
toolsReachable = false,
|
|
283
|
+
/**
|
|
284
|
+
* OS PESOS QUE AS FAMÍLIAS TÊM NO PROJETO DELE - ver `next-font-weights.ts`.
|
|
285
|
+
*
|
|
286
|
+
* Vem de fora pela mesma razão que `toolsReachable`: quem sabe é quem leu o disco dele. `null`
|
|
287
|
+
* significa "não deu para ler", e o guia então OMITE o bloco do `next/font` em vez de imprimir
|
|
288
|
+
* pesos supostos - um trecho colado com peso que a família não tem faz o `next build` falhar.
|
|
289
|
+
*
|
|
290
|
+
* OBRIGATÓRIO, e a razão é o `fixReach`: um argumento opcional aqui é um argumento que um
|
|
291
|
+
* chamador novo esquece, e o esquecimento faz o bloco do `next/font` DESAPARECER do único
|
|
292
|
+
* GUIDE.md do produto - em silêncio, com tsc, vitest e build passando. `nextFontSnippet` fechou
|
|
293
|
+
* essa porta; deixá-la entreaberta um nível acima seria vigiá-la em vez de fechá-la
|
|
294
|
+
* (CLAUDE.md, Parte I, seção 6).
|
|
295
|
+
*/
|
|
296
|
+
fontFacts) {
|
|
283
297
|
const { document: doc, slug, name, version } = payload;
|
|
284
298
|
const { meta, foundations, motion, components } = doc;
|
|
285
299
|
const semanticRoles = Object.keys(foundations.color.semantic);
|
|
@@ -316,7 +330,22 @@ toolsReachable = false) {
|
|
|
316
330
|
.join("&")}&display=swap`
|
|
317
331
|
: null;
|
|
318
332
|
/** A costura vem do payload quando o servidor a fala - o dado vence a convenção (T18-par). */
|
|
319
|
-
|
|
333
|
+
/**
|
|
334
|
+
* OS PESOS VÊM DE FORA (ver `next-font-weights.ts`), porque quem sabe é quem leu o projeto dele.
|
|
335
|
+
*
|
|
336
|
+
* `null` aqui NÃO é um esquecimento: o guia é escrito no `add`, que já leu o manifesto. Sem ele,
|
|
337
|
+
* o bloco do `next/font` não entra - um trecho para colar com um peso que a família não tem
|
|
338
|
+
* derruba o `next build` de quem colar, e o `<link>` abaixo funciona em qualquer framework.
|
|
339
|
+
*/
|
|
340
|
+
const fontPlan = nextFontSnippet({
|
|
341
|
+
families: foundations.typography.families,
|
|
342
|
+
slug,
|
|
343
|
+
seam: payload.fontSeam,
|
|
344
|
+
facts: fontFacts,
|
|
345
|
+
/** Os pesos que ELE declara - ver `declaredWeights`. O guia é o arquivo que o agente dele cola. */
|
|
346
|
+
declaredWeights: foundations.typography.weights,
|
|
347
|
+
});
|
|
348
|
+
const nextFonts = fontPlan.ok ? fontPlan : null;
|
|
320
349
|
const fontsSection = fontsHref
|
|
321
350
|
? `
|
|
322
351
|
## Fonts
|
package/dist/install-marks.js
CHANGED
|
@@ -170,7 +170,7 @@
|
|
|
170
170
|
* O que o cliente ganha ao rodar `upgrade`: o agente dele no Codex passa a poder PERGUNTAR ao
|
|
171
171
|
* sistema, em vez de só receber as regras e adivinhar o resto.
|
|
172
172
|
*/
|
|
173
|
-
export const MATERIALISER_SINCE = "0.16.
|
|
173
|
+
export const MATERIALISER_SINCE = "0.16.403";
|
|
174
174
|
/**
|
|
175
175
|
* A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
|
|
176
176
|
*
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OS PESOS QUE CADA FAMÍLIA REALMENTE TEM - lidos do projeto DELE, nunca de uma tabela nossa.
|
|
3
|
+
*
|
|
4
|
+
* O QUE O CLIENTE GANHA: o `npm run build` dele para de falhar por causa de um arquivo que a
|
|
5
|
+
* plataforma escreveu.
|
|
6
|
+
*
|
|
7
|
+
* O DEFEITO, medido em 08/09 num `create-next-app` recém-criado com o `codelevel` instalado pelo
|
|
8
|
+
* binário 0.16.401:
|
|
9
|
+
*
|
|
10
|
+
* Unknown weight 500 for font Instrument Serif.
|
|
11
|
+
* Available weights: 400
|
|
12
|
+
* Import trace: ./app/fonts.ts -> ./app/layout.tsx
|
|
13
|
+
*
|
|
14
|
+
* `next build` terminou em **EXIT=1**. O app não compila, e o arquivo que o derruba foi escrito
|
|
15
|
+
* pelo `add`. É o mesmo desfecho de `wired-slugs.ts`: a esteira existe para devolver o código dele
|
|
16
|
+
* funcionando, e entregou um repositório que não constrói.
|
|
17
|
+
*
|
|
18
|
+
* A CAUSA ERA UMA SUPOSIÇÃO ESCRITA EM COMENTÁRIO. O gerador fixava `weight: ["400","500","600"]`
|
|
19
|
+
* para TODA família, e o comentário afirmava que emitir sempre *"can never break"*. A afirmação
|
|
20
|
+
* nasceu do conserto de 29/07 - antes disso o arquivo omitia os pesos e quebrava para as fontes
|
|
21
|
+
* estáticas - e ela cobre metade do problema: omitir quebra para a fonte estática, e afirmar
|
|
22
|
+
* quebra para a fonte que não tem aquele corte. `Instrument Serif` tem exatamente um peso.
|
|
23
|
+
*
|
|
24
|
+
* ONDE MORA A RESPOSTA, e por isso ela não é nossa: o próprio `next` que o projeto dele instalou
|
|
25
|
+
* carrega o manifesto que o `next/font` usa para VALIDAR. *MEDIÇÃO, e a população é a versão do
|
|
26
|
+
* `next` que o projeto instalou: **1942 famílias** no `create-next-app` de 08/09, **1911** no
|
|
27
|
+
* `next` 16.2.0 deste repositório.* Cada uma com os pesos, estilos e subsets que existem. Ler dali significa que o que a gente
|
|
28
|
+
* emite é exatamente o que o compilador dele aceita - e que uma família nova do Google chega junto
|
|
29
|
+
* com o `npm update` dele, sem release nosso.
|
|
30
|
+
*
|
|
31
|
+
* `null` QUANDO NÃO DÁ PARA SABER. Sem manifesto legível, este módulo não adivinha: quem chama
|
|
32
|
+
* decide, e a decisão do produto é não escrever o arquivo e dizer o motivo (lei 8). O `<link>` do
|
|
33
|
+
* Google Fonts continua sendo o caminho que funciona - medido em 08/09, ele responde HTTP 200
|
|
34
|
+
* mesmo quando a URL pede um peso que a família não tem, porque aquela API ignora o que não existe.
|
|
35
|
+
*/
|
|
36
|
+
import { readFile } from "node:fs/promises";
|
|
37
|
+
import { createRequire } from "node:module";
|
|
38
|
+
import { dirname, join } from "node:path";
|
|
39
|
+
/**
|
|
40
|
+
* Os lugares onde o manifesto do `next/font` já morou. Mais de um de propósito: o caminho é interno
|
|
41
|
+
* ao Next, então uma versão que o mova não pode virar um build quebrado - vira `null`, e o produto
|
|
42
|
+
* diz que não conseguiu ler.
|
|
43
|
+
*/
|
|
44
|
+
const MANIFEST_PATHS = [
|
|
45
|
+
"dist/compiled/@next/font/dist/google/font-data.json",
|
|
46
|
+
"dist/compiled/@next/font/google/font-data.json",
|
|
47
|
+
"font/dist/google/font-data.json",
|
|
48
|
+
];
|
|
49
|
+
/**
|
|
50
|
+
* O manifesto do projeto DELE, resolvido pela mesma regra que o `import` dele usa.
|
|
51
|
+
*
|
|
52
|
+
* `createRequire` a partir da raiz do projeto respeita o hoisting do monorepo: o `next` de um app
|
|
53
|
+
* pode estar na raiz do workspace, e uma busca por caminho fixo em `node_modules/` acharia nada
|
|
54
|
+
* exatamente nos repositórios grandes, que são os que mais precisam disto.
|
|
55
|
+
*/
|
|
56
|
+
async function loadManifest(projectRoot) {
|
|
57
|
+
for (const pkg of ["next/package.json", "@next/font/package.json"]) {
|
|
58
|
+
let base;
|
|
59
|
+
try {
|
|
60
|
+
const require = createRequire(join(projectRoot, "package.json"));
|
|
61
|
+
base = dirname(require.resolve(pkg));
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
for (const rel of MANIFEST_PATHS) {
|
|
67
|
+
try {
|
|
68
|
+
const raw = await readFile(join(base, ...rel.split("/")), "utf8");
|
|
69
|
+
const parsed = JSON.parse(raw);
|
|
70
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
71
|
+
return parsed;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// próximo caminho - um manifesto ausente não é erro, é uma resposta
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* O leitor de pesos deste projeto, ou `null` quando o manifesto não está ao alcance.
|
|
82
|
+
*
|
|
83
|
+
* A busca do nome é case-insensitive porque o documento guarda o nome como ele aparece no CSS dele
|
|
84
|
+
* (`"instrument serif"` de um `font-family` em minúsculas é a mesma família), e o manifesto guarda
|
|
85
|
+
* a grafia do Google.
|
|
86
|
+
*/
|
|
87
|
+
export async function nextFontFacts(projectRoot) {
|
|
88
|
+
const manifest = await loadManifest(projectRoot);
|
|
89
|
+
if (!manifest)
|
|
90
|
+
return null;
|
|
91
|
+
const weights = new Map();
|
|
92
|
+
const names = new Map();
|
|
93
|
+
const subsets = new Map();
|
|
94
|
+
for (const [family, entry] of Object.entries(manifest)) {
|
|
95
|
+
const key = family.toLowerCase();
|
|
96
|
+
weights.set(key, (entry?.weights ?? []).filter((w) => /^\d+$/.test(w)));
|
|
97
|
+
names.set(key, family);
|
|
98
|
+
subsets.set(key, entry?.subsets ?? []);
|
|
99
|
+
}
|
|
100
|
+
const key = (family) => family.trim().toLowerCase();
|
|
101
|
+
return {
|
|
102
|
+
weightsOf: (family) => weights.get(key(family)) ?? null,
|
|
103
|
+
nameOf: (family) => names.get(key(family)) ?? null,
|
|
104
|
+
subsetsOf: (family) => subsets.get(key(family)) ?? null,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* OS PESOS A EMITIR para uma família, dados os que o gerador quer e os que existem.
|
|
109
|
+
*
|
|
110
|
+
* `wanted` são os três degraus que todo sistema gerado usa de fato - regular, medium, semibold. A
|
|
111
|
+
* saída é a interseção, na ordem dos disponíveis, e nunca o conjunto vazio: uma família que não tem
|
|
112
|
+
* nenhum dos três (só 200/300, por exemplo) recebe o peso mais próximo de 400 que ela tem, porque
|
|
113
|
+
* emitir lista vazia é `weight: []`, que o `next/font` recusa tanto quanto o peso inexistente.
|
|
114
|
+
*/
|
|
115
|
+
export function weightsToEmit(available, wanted = ["400", "500", "600"]) {
|
|
116
|
+
const have = available.filter((w) => /^\d+$/.test(w));
|
|
117
|
+
if (have.length === 0)
|
|
118
|
+
return [];
|
|
119
|
+
const kept = have.filter((w) => wanted.includes(w));
|
|
120
|
+
if (kept.length > 0)
|
|
121
|
+
return kept;
|
|
122
|
+
const nearest = [...have].sort((a, b) => Math.abs(Number(a) - 400) - Math.abs(Number(b) - 400))[0];
|
|
123
|
+
return [nearest];
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* OS SUBSETS A PEDIR para uma família, dados os que ela tem.
|
|
127
|
+
*
|
|
128
|
+
* `latin` quando ela o tem - é o que o texto dele usa, e pedir mais baixaria arquivo para nada.
|
|
129
|
+
* Sem `latin` mas com outros, os que ela tem: as 11 famílias nessa situação têm exatamente um
|
|
130
|
+
* (`Chenla` e `Content` são `["khmer"]`), então pedir a lista dela é pedir o único que existe.
|
|
131
|
+
*
|
|
132
|
+
* `[]` quando ela não tem subset nenhum, e aí a chave NÃO é escrita: o `next` desliga o preload
|
|
133
|
+
* sozinho nesse caso, e escrever `["latin"]` seria afirmar sobre a fonte dele um subset que ela não
|
|
134
|
+
* declara - a afirmação que este módulo existe para não fazer.
|
|
135
|
+
*/
|
|
136
|
+
export function subsetsToEmit(available) {
|
|
137
|
+
if (available.includes("latin"))
|
|
138
|
+
return ["latin"];
|
|
139
|
+
return [...available];
|
|
140
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { walk } from "./commands/doctor.js";
|
|
4
|
+
import { FAMILY_SEAM_PREFIX } from "./fonts.js";
|
|
5
|
+
/**
|
|
6
|
+
* A FIAÇÃO DESTE PROJETO, MEDIDA - e a mesma medição para todos os comandos que a citam.
|
|
7
|
+
*
|
|
8
|
+
* O QUE O CLIENTE VIA (medido em 08/09, nos dois apps do ato 2B): ele colou os dois `@import` no
|
|
9
|
+
* `globals.css`, pôs `data-ds="codelevel"` no `<body>`, mapeou as quatro famílias - e o
|
|
10
|
+
* `component` terminou imprimindo **One-time setup**, com os passos 1 e 2 para colar de novo. O
|
|
11
|
+
* comando media "este arquivo PRECISA da folha?" (`sheet-needed.ts`) e nunca "a folha JÁ está
|
|
12
|
+
* aqui?".
|
|
13
|
+
*
|
|
14
|
+
* As duas perguntas são diferentes e as duas importam: a primeira decide se o setup é necessário, e
|
|
15
|
+
* a segunda decide se ele ainda está PENDENTE. Cobrar o que já foi feito gasta a confiança de quem
|
|
16
|
+
* fez - ele fica sem saber se errou algo, e o texto que deveria orientar passa a ser ruído.
|
|
17
|
+
*
|
|
18
|
+
* POR QUE ESTE MÓDULO EXISTE, em vez de o `component` medir por conta: esta leitura já existia
|
|
19
|
+
* dentro do `doctor`, com o histórico inteiro dela. Uma segunda medição da mesma coisa é uma
|
|
20
|
+
* segunda medição livre de discordar da primeira - dois comandos dizendo coisas opostas sobre o
|
|
21
|
+
* mesmo repositório é exatamente o defeito que este PR conserta.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Wiring is a property of the PROJECT, never of the scope being read.
|
|
25
|
+
*
|
|
26
|
+
* This ran inside the scoped walk, so `doctor app/login` read only that folder
|
|
27
|
+
* - and the import lives in `app/globals.css`. It concluded "installed, but not
|
|
28
|
+
* wired up yet" about a project that was perfectly wired (my-test4, 27/07).
|
|
29
|
+
*
|
|
30
|
+
* Which would be a mild annoyance, except the managed block now tells the agent
|
|
31
|
+
* to run `doctor <the file you just wrote>` after every edit. The false alarm
|
|
32
|
+
* fires on every loop, and an agent that believes the system is unwired starts
|
|
33
|
+
* repairing wiring that was already correct - editing the one file we most need
|
|
34
|
+
* it to leave alone.
|
|
35
|
+
*
|
|
36
|
+
* The system itself was always resolved from the root. So is this now.
|
|
37
|
+
*/
|
|
38
|
+
export async function readWiring(root, slug) {
|
|
39
|
+
const w = {
|
|
40
|
+
imported: false,
|
|
41
|
+
/**
|
|
42
|
+
* O ADAPTADOR DO TAILWIND, e ele é uma pergunta SEPARADA de `imported`.
|
|
43
|
+
*
|
|
44
|
+
* O DEFEITO QUE ISTO CONSERTA, apontado na revisão deste PR: `imported` olha só o
|
|
45
|
+
* `tokens.css`, e o bloco que consulta esta leitura fala dos UTILITÁRIOS - que só o `@theme`
|
|
46
|
+
* do `theme.css` gera. Num projeto com um import e não o outro, o comando nomeava a falta
|
|
47
|
+
* (`what needs it: the bg-canvas utility`) e na linha seguinte declarava que não faltava nada.
|
|
48
|
+
*
|
|
49
|
+
* Um falso silêncio é pior que o falso alarme que este PR veio consertar: o cliente cola o
|
|
50
|
+
* componente, vê um bloco sem tipografia e sem sombra, e o comando acabou de dizer que estava
|
|
51
|
+
* tudo certo.
|
|
52
|
+
*/
|
|
53
|
+
themed: false,
|
|
54
|
+
scoped: false,
|
|
55
|
+
/** `init` wrote a fonts file for this project (Next targets only). */
|
|
56
|
+
fontsWritten: false,
|
|
57
|
+
/** …and the stylesheet actually maps it onto the system's type tokens. */
|
|
58
|
+
fontsMapped: false,
|
|
59
|
+
/**
|
|
60
|
+
* THE FEATURE NOBODY KNEW WE SHIPPED.
|
|
61
|
+
*
|
|
62
|
+
* Every system compiles a `shadcn.css` mapping shadcn's whole variable
|
|
63
|
+
* contract onto its own tokens, so shadcn components wear the system instead
|
|
64
|
+
* of shadcn's defaults. It has been generated into every project since it
|
|
65
|
+
* was built, and until 28/07 nothing mentioned it: not `add`, not the
|
|
66
|
+
* managed block, not the GUIDE except as a filename in a list. The only
|
|
67
|
+
* documentation was a comment inside the file, which is the same as none.
|
|
68
|
+
*
|
|
69
|
+
* The person it mattered most to is the one who asked "I don't get the use
|
|
70
|
+
* case, I can just build a design system with shadcn" - and the answer was
|
|
71
|
+
* sitting unimported in his own repo.
|
|
72
|
+
*
|
|
73
|
+
* Reported only when the project HAS shadcn. Telling everybody about a
|
|
74
|
+
* bridge they will never use is how a report earns the skim.
|
|
75
|
+
*/
|
|
76
|
+
hasShadcn: false,
|
|
77
|
+
bridged: false,
|
|
78
|
+
};
|
|
79
|
+
if (!slug)
|
|
80
|
+
return w;
|
|
81
|
+
w.hasShadcn = await readFile(join(root, "components.json"), "utf8").then(() => true, () => false);
|
|
82
|
+
for await (const file of walk(root)) {
|
|
83
|
+
const src = await readFile(file, "utf8").catch(() => "");
|
|
84
|
+
if (!src)
|
|
85
|
+
continue;
|
|
86
|
+
if (src.includes(`_synthesisui/ds/${slug}/tokens.css`))
|
|
87
|
+
w.imported = true;
|
|
88
|
+
if (src.includes(`_synthesisui/ds/${slug}/theme.css`))
|
|
89
|
+
w.themed = true;
|
|
90
|
+
if (src.includes(`_synthesisui/ds/${slug}/shadcn.css`))
|
|
91
|
+
w.bridged = true;
|
|
92
|
+
if (src.includes(`data-ds="${slug}"`))
|
|
93
|
+
w.scoped = true;
|
|
94
|
+
// The requirement that stayed invisible. `init` writes a fonts file and
|
|
95
|
+
// asks for two more edits; an agent told to check only the first two did
|
|
96
|
+
// exactly that, stopped, and left the project rendering in the framework's
|
|
97
|
+
// default face (my-test2, 27/07). What the checker checks is what gets done.
|
|
98
|
+
if (src.includes("--font-ds-") && /next\/font/.test(src))
|
|
99
|
+
w.fontsWritten = true;
|
|
100
|
+
/** O prefixo vem da const única da costura - um literal aqui divergiria em silêncio (26/08). */
|
|
101
|
+
if (new RegExp(`${FAMILY_SEAM_PREFIX}\\w+\\s*:\\s*var\\(\\s*--font-ds-`).test(src))
|
|
102
|
+
w.fontsMapped = true;
|
|
103
|
+
// The bridge joins the early exit, otherwise the walk can stop before the
|
|
104
|
+
// stylesheet that imports it and report a wired project as unbridged.
|
|
105
|
+
if (w.imported &&
|
|
106
|
+
w.themed &&
|
|
107
|
+
w.scoped &&
|
|
108
|
+
w.fontsMapped &&
|
|
109
|
+
(!w.hasShadcn || w.bridged))
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
return w;
|
|
113
|
+
}
|
package/package.json
CHANGED