synthesisui 0.16.408 → 0.16.410
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/doctor.js +107 -22
- package/dist/global-sheet.js +31 -4
- package/dist/wiring-read.js +77 -0
- package/package.json +1 -1
package/dist/commands/doctor.js
CHANGED
|
@@ -16,7 +16,11 @@ 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
|
-
|
|
19
|
+
/**
|
|
20
|
+
* `detectAppDirs` SAIU DESTE IMPORT em 09/09: quem pergunta "quais pastas são app?" agora é
|
|
21
|
+
* `readWiringPerApp`, que responde a fiação de cada uma. O comando não escolhe mais um app.
|
|
22
|
+
*/
|
|
23
|
+
import { globalSheetOf, prefixFrom } from "../global-sheet.js";
|
|
20
24
|
import { groupRole } from "../group-role.js";
|
|
21
25
|
import { actingSlug, describeScope, measuredScope, scopePaths, } from "../measured-scope.js";
|
|
22
26
|
import { body, paint, section, snippet } from "../output.js";
|
|
@@ -25,7 +29,7 @@ import { installedThemeCss, whatOnlyTheSheetResolves, } from "../sheet-needed.js
|
|
|
25
29
|
import { resolveDeps } from "../stack.js";
|
|
26
30
|
import { projectTongue } from "../their-tongue.js";
|
|
27
31
|
import { danglingTheirVars } from "../their-vars.js";
|
|
28
|
-
import { readWiring } from "../wiring-read.js";
|
|
32
|
+
import { readWiring, readWiringPerApp } from "../wiring-read.js";
|
|
29
33
|
/**
|
|
30
34
|
* `synthesisui doctor` - the check nobody else ships.
|
|
31
35
|
*
|
|
@@ -1068,28 +1072,109 @@ export async function doctor(opts) {
|
|
|
1068
1072
|
* inventado.
|
|
1069
1073
|
*/
|
|
1070
1074
|
if (table.slug) {
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1075
|
+
/**
|
|
1076
|
+
* A INSTRUÇÃO É POR APP, e não do primeiro deles.
|
|
1077
|
+
*
|
|
1078
|
+
* O DEFEITO: esta linha era `appDirs[0]` - num monorepo com três apps servidos, dois recebiam
|
|
1079
|
+
* a instrução do PRIMEIRO, com a folha e o prefixo relativo de outro app. É o mesmo defeito
|
|
1080
|
+
* que `INV-VOLTA-12` fechou uma vez ("ele seguiu a instrução no arquivo CERTO e recebeu de nós
|
|
1081
|
+
* o prefixo do arquivo errado"), voltando pela porta do plural: a invariante fala de "os apps
|
|
1082
|
+
* dele", e o comando respondia por um.
|
|
1083
|
+
*
|
|
1084
|
+
* E A FIAÇÃO TAMBÉM É POR APP (`readWiringPerApp`): `readWiring` varre a raiz e responde
|
|
1085
|
+
* "existe em algum lugar daqui", então o app fiado respondia pelo que não estava - e a pessoa
|
|
1086
|
+
* abria o app sem nada e o comando dizia que estava tudo certo.
|
|
1087
|
+
*
|
|
1088
|
+
* COM UM APP, o texto não muda em byte nenhum - é a maioria dos projetos, e um cabeçalho
|
|
1089
|
+
* por app ali seria cerimônia sobre uma lista de um.
|
|
1090
|
+
*/
|
|
1091
|
+
const perApp = await readWiringPerApp(root, table.slug, config.pagesDir);
|
|
1092
|
+
const targets = perApp.length > 0 ? perApp : [{ app: null, wiring }];
|
|
1093
|
+
/**
|
|
1094
|
+
* OS APPS QUE PEDEM A MESMA COISA VIRAM UM BLOCO SÓ, nomeando os dois.
|
|
1095
|
+
*
|
|
1096
|
+
* O DEFEITO, e ele é meu, do PR de algumas horas atrás: no `codelevel-monorepo` os DOIS apps
|
|
1097
|
+
* resolvem para a MESMA folha (`packages/ui/src/styles/globals.css`, o pacote compartilhado),
|
|
1098
|
+
* então o comando imprimiu a mesma instrução de 40 linhas DUAS VEZES, com o mesmo caminho e o
|
|
1099
|
+
* mesmo prefixo. O dono leu a parede duas vezes (medido no repositório dele, 09/09).
|
|
1100
|
+
*
|
|
1101
|
+
* Eu tinha escrito o aviso no comentário abaixo - *"num monorepo ele apareceria em escala:
|
|
1102
|
+
* dois blocos idênticos"* - sobre o caso do app JÁ FIADO, e entreguei a duplicata no caso do
|
|
1103
|
+
* que ainda falta. Nomear cada app foi o conserto certo; repetir a instrução não.
|
|
1104
|
+
*
|
|
1105
|
+
* A CHAVE É A INSTRUÇÃO, não o app: dois apps com a mesma folha, o mesmo prefixo e os mesmos
|
|
1106
|
+
* passos pendentes recebem uma instrução só. Um monorepo onde cada app tem folha própria volta
|
|
1107
|
+
* a receber uma por app, porque ali as instruções DIFEREM - é a resposta derivada, e não uma
|
|
1108
|
+
* regra sobre quantos apps existem.
|
|
1109
|
+
*/
|
|
1110
|
+
const blocks = new Map();
|
|
1111
|
+
for (const { app, wiring: w } of targets) {
|
|
1112
|
+
const sheet = app
|
|
1113
|
+
? await globalSheetOf(root, `${app}/globals.css`)
|
|
1114
|
+
: null;
|
|
1115
|
+
/**
|
|
1116
|
+
* O APP QUE JÁ ESTÁ FIADO SAI DA LISTA, nomeado. Repetir o setup para quem já o fez é o
|
|
1117
|
+
* defeito de 08/09 outra vez.
|
|
1118
|
+
*/
|
|
1119
|
+
const pending = !w.imported || !w.scoped || (w.fontsWritten && !w.fontsMapped);
|
|
1120
|
+
const text = pending
|
|
1121
|
+
? setupPrompt(table.slug, {
|
|
1122
|
+
tokens: !w.imported,
|
|
1123
|
+
scope: !w.scoped,
|
|
1124
|
+
/** Só é passo quando o projeto TEM o arquivo de fontes - ver `readWiring`. */
|
|
1125
|
+
type: w.fontsWritten && !w.fontsMapped,
|
|
1126
|
+
...(sheet
|
|
1127
|
+
? { sheet: { path: sheet, prefix: prefixFrom(sheet) } }
|
|
1128
|
+
: {}),
|
|
1129
|
+
/**
|
|
1130
|
+
* DUAS LINHAS COM TAILWIND, UMA SEM - a mesma decisão que o `add` já toma pelo
|
|
1131
|
+
* projeto. Dizer "as duas linhas" a um projeto que precisa de uma manda o agente
|
|
1132
|
+
* procurar o que não existe.
|
|
1133
|
+
*/
|
|
1134
|
+
imports: config.styles === "css" ? 1 : 2,
|
|
1135
|
+
}).split("\n")
|
|
1136
|
+
: [];
|
|
1137
|
+
const key = `${pending ? "p" : "ok"}|${text.join("\n")}`;
|
|
1138
|
+
const found = blocks.get(key);
|
|
1139
|
+
if (found)
|
|
1140
|
+
found.apps.push(app ?? "");
|
|
1141
|
+
else
|
|
1142
|
+
blocks.set(key, { apps: [app ?? ""], text, pending });
|
|
1143
|
+
}
|
|
1144
|
+
const anyPending = [...blocks.values()].some((b) => b.pending);
|
|
1145
|
+
if (anyPending) {
|
|
1146
|
+
console.log("");
|
|
1147
|
+
console.log(body("Paste this into your agent - it does the setup for you:"));
|
|
1148
|
+
}
|
|
1149
|
+
for (const { apps, text, pending } of blocks.values()) {
|
|
1150
|
+
const named = apps.filter(Boolean);
|
|
1086
1151
|
/**
|
|
1087
|
-
*
|
|
1088
|
-
*
|
|
1089
|
-
*
|
|
1152
|
+
* O CABEÇALHO SÓ APARECE QUANDO HÁ MAIS DE UM APP no projeto - com um só ele seria cerimônia
|
|
1153
|
+
* sobre uma lista de um, e a maioria dos projetos é assim. Com vários, ele diz de QUEM é a
|
|
1154
|
+
* instrução, e o plural sai naturalmente: "apps/landing/app and apps/web/app".
|
|
1090
1155
|
*/
|
|
1091
|
-
|
|
1092
|
-
|
|
1156
|
+
if (targets.length > 1 && named.length > 0) {
|
|
1157
|
+
/**
|
|
1158
|
+
* A LISTA SE LÊ EM VOZ ALTA, e o número dela não é fixo: "A and B" com dois,
|
|
1159
|
+
* "A, B and C" com três. `join(" and ")` daria "A and B and C", e `"both"` mentiria
|
|
1160
|
+
* no dia em que o monorepo tiver três apps na mesma folha - o mesmo erro de
|
|
1161
|
+
* dimensionar pela amostra, na escala de uma palavra.
|
|
1162
|
+
*/
|
|
1163
|
+
const lista = named.length > 1
|
|
1164
|
+
? `${named.slice(0, -1).join(", ")} and ${named[named.length - 1]}`
|
|
1165
|
+
: named[0];
|
|
1166
|
+
console.log("");
|
|
1167
|
+
console.log(body(` ${lista} - ${pending
|
|
1168
|
+
? named.length > 1
|
|
1169
|
+
? "need it, and the steps are the same"
|
|
1170
|
+
: "still needs it"
|
|
1171
|
+
: "already wired, nothing to paste"}`));
|
|
1172
|
+
}
|
|
1173
|
+
if (!pending)
|
|
1174
|
+
continue;
|
|
1175
|
+
console.log("");
|
|
1176
|
+
console.log(snippet(text));
|
|
1177
|
+
}
|
|
1093
1178
|
}
|
|
1094
1179
|
}
|
|
1095
1180
|
/**
|
package/dist/global-sheet.js
CHANGED
|
@@ -65,13 +65,39 @@ async function fromWorkspace(root, spec) {
|
|
|
65
65
|
}
|
|
66
66
|
/**
|
|
67
67
|
* A folha onde os tokens dele devem entrar, relativa à raiz - `appSheet` quando nada a re-exporta.
|
|
68
|
+
*
|
|
69
|
+
* DELEGA, e não caminha: quem caminha é `sheetChainOf`. Dois caminhadores sobre a mesma cadeia é
|
|
70
|
+
* como um deles para de seguir um salto que o outro segue, e ninguém descobre até um cliente
|
|
71
|
+
* receber a instrução apontando para a folha errada - que é exatamente o defeito que
|
|
72
|
+
* `INV-VOLTA-12` fechou.
|
|
68
73
|
*/
|
|
69
74
|
export async function globalSheetOf(root, appSheet) {
|
|
75
|
+
const chain = await sheetChainOf(root, appSheet);
|
|
76
|
+
return chain[chain.length - 1] ?? appSheet;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* A CADEIA INTEIRA, da folha do app até a última que ninguém re-exporta - e por que ela é pública.
|
|
80
|
+
*
|
|
81
|
+
* O QUE O CLIENTE GANHA: a resposta "este app carrega o meu design system?" medida no app DELE, e
|
|
82
|
+
* não no projeto. `readWiring` varria a raiz e devolvia "existe em algum lugar daqui": num monorepo
|
|
83
|
+
* com dois apps servidos, um fiado e outro não, o fiado respondia pelo outro - e a pessoa recebia
|
|
84
|
+
* "está tudo certo" sobre o app que não carrega nada.
|
|
85
|
+
*
|
|
86
|
+
* A pergunta dos IMPORTS não se responde varrendo pasta: ela se responde seguindo a cadeia de
|
|
87
|
+
* `@import` a partir da folha daquele app, porque a folha que carrega os tokens quase nunca é a do
|
|
88
|
+
* app - num monorepo é a do pacote compartilhado, e os dois apps chegam nela.
|
|
89
|
+
*
|
|
90
|
+
* A ordem é do app para fora, e o teto de saltos é o mesmo: um ciclo de imports não pode travar um
|
|
91
|
+
* comando.
|
|
92
|
+
*/
|
|
93
|
+
export async function sheetChainOf(root, appSheet) {
|
|
94
|
+
const chain = [];
|
|
70
95
|
let current = appSheet;
|
|
71
96
|
for (let hop = 0; hop < MAX_HOPS; hop += 1) {
|
|
97
|
+
chain.push(current);
|
|
72
98
|
const raw = await readFile(join(root, current), "utf8").catch(() => null);
|
|
73
99
|
if (!raw)
|
|
74
|
-
return
|
|
100
|
+
return chain;
|
|
75
101
|
let next = null;
|
|
76
102
|
for (const m of raw.matchAll(IMPORT)) {
|
|
77
103
|
const spec = m[1];
|
|
@@ -91,11 +117,12 @@ export async function globalSheetOf(root, appSheet) {
|
|
|
91
117
|
next = candidate;
|
|
92
118
|
break;
|
|
93
119
|
}
|
|
94
|
-
|
|
95
|
-
|
|
120
|
+
/** Uma folha que aponta para si mesma encerra a cadeia em vez de gastar o teto de saltos. */
|
|
121
|
+
if (!next || next === current || chain.includes(next))
|
|
122
|
+
return chain;
|
|
96
123
|
current = next;
|
|
97
124
|
}
|
|
98
|
-
return
|
|
125
|
+
return chain;
|
|
99
126
|
}
|
|
100
127
|
/** O `../` que leva daquela folha até a raiz do repositório - o prefixo do `@import`. */
|
|
101
128
|
export function prefixFrom(sheet) {
|
package/dist/wiring-read.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { walk } from "./commands/doctor.js";
|
|
4
4
|
import { FAMILY_SEAM_PREFIX } from "./fonts.js";
|
|
5
|
+
import { detectAppDirs, sheetChainOf } from "./global-sheet.js";
|
|
5
6
|
/**
|
|
6
7
|
* A FIAÇÃO DESTE PROJETO, MEDIDA - e a mesma medição para todos os comandos que a citam.
|
|
7
8
|
*
|
|
@@ -111,3 +112,79 @@ export async function readWiring(root, slug) {
|
|
|
111
112
|
}
|
|
112
113
|
return w;
|
|
113
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* A FIAÇÃO DE CADA APP, e num monorepo é ela que decide - `readWiring` acima não decide.
|
|
117
|
+
*
|
|
118
|
+
* O QUE O CLIENTE VIA: `readWiring` varre da RAIZ e responde "existe, em algum lugar daqui". Num
|
|
119
|
+
* monorepo com dois apps servidos, um fiado e outro não, o fiado respondia pelo outro: a pessoa
|
|
120
|
+
* abria o app que não carrega nada e o comando dizia que estava tudo certo. O contrato já declarava
|
|
121
|
+
* isso como lacuna (`INV-VOLTA-12`, "a resposta é do PROJETO, e num monorepo ela não decide") e o
|
|
122
|
+
* comando dizia o que mediu em vez de afirmar - honesto, e ainda a resposta errada.
|
|
123
|
+
*
|
|
124
|
+
* COMO A RESPOSTA CERTA É ENCONTRADA, e cada metade vem de onde ela mora:
|
|
125
|
+
*
|
|
126
|
+
* os `@import` seguindo a CADEIA de folhas a partir da folha daquele app (`sheetChainOf`).
|
|
127
|
+
* A folha que carrega os tokens quase nunca é a do app: num monorepo é a do
|
|
128
|
+
* pacote compartilhado, e os dois apps chegam nela - então varrer pasta daria
|
|
129
|
+
* "não" para os dois, e varrer a raiz dá "sim" para todos
|
|
130
|
+
* o `data-ds` na árvore DAQUELE app, porque o escopo mora no layout dele
|
|
131
|
+
* as fontes idem - `init` escreve o arquivo de fonte por app
|
|
132
|
+
*
|
|
133
|
+
* O QUE ESTA FUNÇÃO NÃO GENERALIZA, declarado com o N de hoje (medido em 09/09):
|
|
134
|
+
*
|
|
135
|
+
* `detectAppDirs` só procura em `apps/` e `packages/` cobre 4 de 4 monorepos medidos;
|
|
136
|
+
* `pnpm-workspace.yaml` aparece em 0 de 10
|
|
137
|
+
* e só reconhece app pelo `layout` do App Router cobre 7 de 7 raízes de app medidas;
|
|
138
|
+
* `vite` aparece só em PACOTE de
|
|
139
|
+
* biblioteca, corretamente ignorado
|
|
140
|
+
*
|
|
141
|
+
* As duas são estreitezas REAIS e as duas têm N=0 de evidência contrária hoje. Generalizá-las sem
|
|
142
|
+
* uma população que as exija seria dimensionar pela imaginação - o erro que `INV-GERAL-07` nomeia
|
|
143
|
+
* pelo avesso. Quando aparecer um repositório que caia fora, o número dele é que abre a frente.
|
|
144
|
+
*/
|
|
145
|
+
export async function readWiringPerApp(root, slug, pagesDir) {
|
|
146
|
+
if (!slug)
|
|
147
|
+
return [];
|
|
148
|
+
const apps = await detectAppDirs(root, pagesDir);
|
|
149
|
+
const out = [];
|
|
150
|
+
const hasShadcn = await readFile(join(root, "components.json"), "utf8").then(() => true, () => false);
|
|
151
|
+
for (const app of apps) {
|
|
152
|
+
const w = {
|
|
153
|
+
imported: false,
|
|
154
|
+
themed: false,
|
|
155
|
+
scoped: false,
|
|
156
|
+
fontsWritten: false,
|
|
157
|
+
fontsMapped: false,
|
|
158
|
+
hasShadcn,
|
|
159
|
+
bridged: false,
|
|
160
|
+
};
|
|
161
|
+
/** Os imports: a cadeia daquele app, e não a pasta dele. */
|
|
162
|
+
for (const sheet of await sheetChainOf(root, `${app}/globals.css`)) {
|
|
163
|
+
const css = await readFile(join(root, sheet), "utf8").catch(() => "");
|
|
164
|
+
if (!css)
|
|
165
|
+
continue;
|
|
166
|
+
if (css.includes(`_synthesisui/ds/${slug}/tokens.css`))
|
|
167
|
+
w.imported = true;
|
|
168
|
+
if (css.includes(`_synthesisui/ds/${slug}/theme.css`))
|
|
169
|
+
w.themed = true;
|
|
170
|
+
if (css.includes(`_synthesisui/ds/${slug}/shadcn.css`))
|
|
171
|
+
w.bridged = true;
|
|
172
|
+
}
|
|
173
|
+
/** O escopo e as fontes: a árvore DAQUELE app - é onde o layout dele mora. */
|
|
174
|
+
for await (const file of walk(join(root, app))) {
|
|
175
|
+
const src = await readFile(file, "utf8").catch(() => "");
|
|
176
|
+
if (!src)
|
|
177
|
+
continue;
|
|
178
|
+
if (src.includes(`data-ds="${slug}"`))
|
|
179
|
+
w.scoped = true;
|
|
180
|
+
if (src.includes("--font-ds-") && /next\/font/.test(src))
|
|
181
|
+
w.fontsWritten = true;
|
|
182
|
+
if (new RegExp(`${FAMILY_SEAM_PREFIX}\\w+\\s*:\\s*var\\(\\s*--font-ds-`).test(src))
|
|
183
|
+
w.fontsMapped = true;
|
|
184
|
+
if (w.scoped && w.fontsMapped)
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
out.push({ app, wiring: w });
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
}
|
package/package.json
CHANGED