synthesisui 0.16.251 → 0.16.253
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/import.js +220 -11
- package/dist/declared-elsewhere.js +77 -0
- package/dist/name-claim.js +71 -0
- package/dist/stack.js +50 -0
- package/dist/workspace-place.js +140 -0
- package/package.json +1 -1
package/dist/commands/import.js
CHANGED
|
@@ -3,6 +3,7 @@ import { basename, dirname, join, relative } from "node:path";
|
|
|
3
3
|
import { anatomyFromSketch } from "../anatomy-from-sketch.js";
|
|
4
4
|
import { resolveAnatomy, resolveFlatParts, safePartName, } from "../anatomy-read.js";
|
|
5
5
|
import { readCredentials, readToken, resolveRegistry, sameRegistry, } from "../config.js";
|
|
6
|
+
import { declaredElsewhere } from "../declared-elsewhere.js";
|
|
6
7
|
import { architectureRule, describeArchitecture, describeChoice, detectArchitectures, } from "../doctor/architecture.js";
|
|
7
8
|
import { findBrokenRefs } from "../doctor/broken-refs.js";
|
|
8
9
|
import { nestingRules, propRules, readDefinitionProps, readNesting, readRuntime, } from "../doctor/call-sites.js";
|
|
@@ -34,9 +35,11 @@ import { definitionSpan, parseClass, readInlineStyle, rootClasses, rootTag, tran
|
|
|
34
35
|
import { transcribeVariants } from "../doctor/variant-read.js";
|
|
35
36
|
import { frontierKind, packageRoot } from "../frontier-kind.js";
|
|
36
37
|
import { withLibraryStructure } from "../library-structure.js";
|
|
38
|
+
import { claimName } from "../name-claim.js";
|
|
37
39
|
import { body, paint, section } from "../output.js";
|
|
38
40
|
import { phase, startProgress } from "../progress.js";
|
|
39
41
|
import { detectStack, resolveDeps, stackVersions } from "../stack.js";
|
|
42
|
+
import { placeInWorkspace } from "../workspace-place.js";
|
|
40
43
|
import { walk, walkAll } from "./doctor.js";
|
|
41
44
|
/**
|
|
42
45
|
* How many distinct values travel, PER KIND.
|
|
@@ -392,6 +395,16 @@ export async function takeCensus(root, opts) {
|
|
|
392
395
|
const internal = await internalSpecifiers(root);
|
|
393
396
|
const defined = [];
|
|
394
397
|
/** Nome → arquivo e fonte, para toda leitura por componente - ver o preenchimento abaixo. */
|
|
398
|
+
/**
|
|
399
|
+
* QUEM DETÉM CADA NOME - e o registro de quem pediu o mesmo.
|
|
400
|
+
*
|
|
401
|
+
* Os oito mapas deste laço são indexados por nome de componente, então dois
|
|
402
|
+
* `Card.tsx` disputam a mesma chave e o último do `walk` sobrescrevia os
|
|
403
|
+
* outros em silêncio. Medido no repo do dono: 20 nomes colidem no repo
|
|
404
|
+
* inteiro, 14 dentro de um app, e um dos quatro `Card` é o do design system
|
|
405
|
+
* dele. Ver `name-claim.ts` para o desempate e por que ele é por estrutura.
|
|
406
|
+
*/
|
|
407
|
+
const claims = { by: new Map(), collisions: [] };
|
|
395
408
|
const sourceOf = new Map();
|
|
396
409
|
/** Nome → o que a peça é, quando o portão soube dizer. Ver `CensusLook.kind`. */
|
|
397
410
|
const kindOf = new Map();
|
|
@@ -590,6 +603,21 @@ export async function takeCensus(root, opts) {
|
|
|
590
603
|
because: verdict.because,
|
|
591
604
|
});
|
|
592
605
|
}
|
|
606
|
+
/**
|
|
607
|
+
* O NOME É DISPUTADO AQUI, ANTES DE QUALQUER ESCRITA.
|
|
608
|
+
*
|
|
609
|
+
* Filtrar `found` fecha a porta de uma vez: os oito mapas deste laço
|
|
610
|
+
* derivam dele, então um componente que não detém o nome simplesmente não
|
|
611
|
+
* chega a nenhum deles - em vez de oito guardas que alguém esquece de
|
|
612
|
+
* repetir na nona escrita.
|
|
613
|
+
*
|
|
614
|
+
* O descarte NÃO é silencioso: `claims.collisions` guarda os dois caminhos
|
|
615
|
+
* e o censo os carrega. Trazer as duas receitas exige o contexto viajando
|
|
616
|
+
* na chave, que é a fatia dos grupos.
|
|
617
|
+
*/
|
|
618
|
+
const claimed = found.filter((d) => claimName(claims, d.name, rel));
|
|
619
|
+
found.length = 0;
|
|
620
|
+
found.push(...claimed);
|
|
593
621
|
for (const d of found) {
|
|
594
622
|
const props = readDefinitionProps(d.name, src);
|
|
595
623
|
propsOf.set(d.name, props);
|
|
@@ -1878,6 +1906,14 @@ export async function takeCensus(root, opts) {
|
|
|
1878
1906
|
...(Object.keys(keyframes).length > 0 ? { keyframes } : {}),
|
|
1879
1907
|
...(animations.size > 0 ? { animations: [...animations].sort() } : {}),
|
|
1880
1908
|
...(brokenRefs.length > 0 ? { brokenRefs } : {}),
|
|
1909
|
+
/**
|
|
1910
|
+
* OS NOMES DISPUTADOS - ver `name-claim.ts`.
|
|
1911
|
+
*
|
|
1912
|
+
* Aditivo e omitido quando vazio, que é o caso de toda biblioteca sã: no repo
|
|
1913
|
+
* do dono, `packages/ui` produz zero e o repo inteiro produz 20. Um campo que
|
|
1914
|
+
* só aparece quando há algo a dizer não muda nada para quem já lê o censo.
|
|
1915
|
+
*/
|
|
1916
|
+
...(claims.collisions.length > 0 ? { collisions: claims.collisions } : {}),
|
|
1881
1917
|
...(conventions.length > 0 ? { conventions } : {}),
|
|
1882
1918
|
classStyle,
|
|
1883
1919
|
...(Object.keys(looks).length > 0 ? { looks } : {}),
|
|
@@ -1996,7 +2032,15 @@ export async function takeCensus(root, opts) {
|
|
|
1996
2032
|
},
|
|
1997
2033
|
};
|
|
1998
2034
|
}
|
|
1999
|
-
|
|
2035
|
+
/**
|
|
2036
|
+
* O RESUMO RECEBE O LUGAR JUNTO COM O CENSO - e não a metade dele.
|
|
2037
|
+
*
|
|
2038
|
+
* `sayBrokenRefs` precisa saber ONDE procurar a declaração que o escopo não viu,
|
|
2039
|
+
* e um parâmetro opcional aqui seria a mesma porta que o §6 do método manda
|
|
2040
|
+
* fechar: uma função que pode ser chamada pela metade vai ser, e o sintoma seria
|
|
2041
|
+
* o relatório voltando a dizer "your CSS never declares" sem que nada falhe.
|
|
2042
|
+
*/
|
|
2043
|
+
async function summarize(c, root, scope) {
|
|
2000
2044
|
const byKind = new Map();
|
|
2001
2045
|
for (const v of c.observed)
|
|
2002
2046
|
byKind.set(v.kind, (byKind.get(v.kind) ?? 0) + v.count);
|
|
@@ -2045,7 +2089,36 @@ function summarize(c) {
|
|
|
2045
2089
|
}
|
|
2046
2090
|
}
|
|
2047
2091
|
sayConventions(c);
|
|
2048
|
-
|
|
2092
|
+
sayCollisions(c);
|
|
2093
|
+
await sayBrokenRefs(c, root, scope);
|
|
2094
|
+
}
|
|
2095
|
+
/**
|
|
2096
|
+
* DOIS ARQUIVOS, UM NOME - dito, com os dois caminhos.
|
|
2097
|
+
*
|
|
2098
|
+
* O silêncio aqui era o pior tipo: a pessoa recebe 36 componentes onde o repo
|
|
2099
|
+
* tem 39, e nada no relatório sugere que três existiram. Medido no monorepo do
|
|
2100
|
+
* dono: 20 nomes no repo inteiro, e um dos quatro `Card.tsx` é o do design system
|
|
2101
|
+
* dele competindo com dois que vivem sob rotas.
|
|
2102
|
+
*
|
|
2103
|
+
* A frase diz o que fazer, e são duas coisas diferentes: um escopo mais estreito
|
|
2104
|
+
* quando os homônimos são de outro projeto, ou um rename quando são do mesmo.
|
|
2105
|
+
*/
|
|
2106
|
+
function sayCollisions(c) {
|
|
2107
|
+
const all = c.collisions ?? [];
|
|
2108
|
+
if (all.length === 0)
|
|
2109
|
+
return;
|
|
2110
|
+
const names = new Set(all.map((x) => x.name));
|
|
2111
|
+
console.log("");
|
|
2112
|
+
console.log(section("Two files, one name"));
|
|
2113
|
+
console.log(body(`${names.size} name${names.size === 1 ? "" : "s"} ${names.size === 1 ? "is" : "are"} defined more than once in what was measured, so ${all.length} definition${all.length === 1 ? "" : "s"} did not make it into this system. The one closest to the root of the scope kept the name - never the order the disk happened to return.`));
|
|
2114
|
+
console.log("");
|
|
2115
|
+
for (const x of all.slice(0, 6)) {
|
|
2116
|
+
console.log(body(`${paint.strong(x.name)} ${paint.faint("kept")} ${x.kept}\n ${paint.faint("dropped")} ${x.dropped}`));
|
|
2117
|
+
}
|
|
2118
|
+
if (all.length > 6)
|
|
2119
|
+
console.log(body(paint.faint(`(${all.length - 6} more)`)));
|
|
2120
|
+
console.log("");
|
|
2121
|
+
console.log(body(paint.faint("If the homonyms belong to different projects, a narrower --scope is the answer and nothing is lost. If they belong to the same one, renaming in your code is - two components with one name is one name your agent cannot resolve either.")));
|
|
2049
2122
|
}
|
|
2050
2123
|
/**
|
|
2051
2124
|
* THEIR VOCABULARY, MEASURED. This section exists so every later finding can be
|
|
@@ -2076,14 +2149,70 @@ function sayConventions(c) {
|
|
|
2076
2149
|
* finding that needs no agreement with us: the convention being broken is
|
|
2077
2150
|
* theirs.
|
|
2078
2151
|
*/
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2152
|
+
/**
|
|
2153
|
+
* A PASTA QUE CONTÉM TODOS ESTES CAMINHOS - o `--scope` que traria as
|
|
2154
|
+
* declarações junto, derivado dos endereços e não sugerido de cabeça.
|
|
2155
|
+
*
|
|
2156
|
+
* Devolve `null` quando os caminhos não compartilham pasta nenhuma: aí não existe
|
|
2157
|
+
* um escopo único que resolva, e inventar um seria pior que dizer a regra geral.
|
|
2158
|
+
*/
|
|
2159
|
+
function commonPrefix(paths) {
|
|
2160
|
+
if (paths.length === 0)
|
|
2161
|
+
return null;
|
|
2162
|
+
const out = [];
|
|
2163
|
+
for (let i = 0;; i++) {
|
|
2164
|
+
const seg = paths[0][i];
|
|
2165
|
+
// O último segmento é o arquivo, e um arquivo não é um escopo.
|
|
2166
|
+
if (seg == null || i >= paths[0].length - 1)
|
|
2167
|
+
break;
|
|
2168
|
+
if (!paths.every((p) => p[i] === seg && i < p.length - 1))
|
|
2169
|
+
break;
|
|
2170
|
+
out.push(seg);
|
|
2171
|
+
}
|
|
2172
|
+
return out.length > 0 ? out.join("/") : null;
|
|
2173
|
+
}
|
|
2174
|
+
async function sayBrokenRefs(c, root, scope) {
|
|
2175
|
+
const all = c.brokenRefs ?? [];
|
|
2176
|
+
if (all.length === 0)
|
|
2082
2177
|
return;
|
|
2178
|
+
/**
|
|
2179
|
+
* DUAS PERGUNTAS DIFERENTES, E O RELATÓRIO FAZIA UMA SÓ.
|
|
2180
|
+
*
|
|
2181
|
+
* "Não resolve no que eu medi" e "não existe no seu código" são coisas
|
|
2182
|
+
* distintas, e o texto afirmava a segunda. Com `--scope src/components` no
|
|
2183
|
+
* monorepo do dono são 1021 referências contra 2 declarações, enquanto o CSS
|
|
2184
|
+
* dele declara 118 em `src/app/**` e 764 em `packages/ui` (medido 19/08).
|
|
2185
|
+
*
|
|
2186
|
+
* Culpar o CSS de quem confiou o repositório por um recorte NOSSO é o pior
|
|
2187
|
+
* lugar para errar: o relatório é o que a pessoa lê depois, sozinha.
|
|
2188
|
+
*/
|
|
2189
|
+
const at = await declaredElsewhere(root, scope, all.map((b) => b.name)).catch(() => new Map());
|
|
2190
|
+
const outside = all.filter((b) => at.has(b.name));
|
|
2191
|
+
const broken = all.filter((b) => !at.has(b.name));
|
|
2192
|
+
if (outside.length > 0) {
|
|
2193
|
+
const uses = outside.reduce((n, b) => n + b.count, 0);
|
|
2194
|
+
console.log("");
|
|
2195
|
+
console.log(section("These are declared outside what was measured"));
|
|
2196
|
+
console.log(body(`${uses} reference${uses === 1 ? "" : "s"} to ${outside.length} token${outside.length === 1 ? "" : "s"} your code DOES declare - just not inside ${paint.strong(scope ?? "this folder")}. Your CSS is fine; the recipes carry the value you typed instead of the name you gave it, because the name was not in what we read.`));
|
|
2197
|
+
console.log("");
|
|
2198
|
+
for (const b of outside.slice(0, 6)) {
|
|
2199
|
+
console.log(body(`${paint.strong(`var(${b.name})`)} ${paint.faint(`${b.count}× here`)} - declared in ${paint.strong(at.get(b.name) ?? "")}`));
|
|
2200
|
+
}
|
|
2201
|
+
if (outside.length > 6)
|
|
2202
|
+
console.log(body(paint.faint(`(${outside.length - 6} more)`)));
|
|
2203
|
+
console.log("");
|
|
2204
|
+
/** O caminho comum dos endereços é o escopo que traria todos de uma vez. */
|
|
2205
|
+
const wider = commonPrefix([...at.values()].map((v) => v.split("/")));
|
|
2206
|
+
console.log(body(wider
|
|
2207
|
+
? `A wider ${paint.strong("--scope")} keeps your names: ${paint.strong(`--scope ${wider}`)} covers the declarations above.`
|
|
2208
|
+
: "A wider --scope keeps your names, because the declarations come along with the code that uses them."));
|
|
2209
|
+
if (broken.length === 0)
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2083
2212
|
const uses = broken.reduce((n, b) => n + b.count, 0);
|
|
2084
2213
|
console.log("");
|
|
2085
2214
|
console.log(section("These resolve to nothing"));
|
|
2086
|
-
console.log(body(`${uses} reference${uses === 1 ? "" : "s"} to ${broken.length} token${broken.length === 1 ? "" : "s"}
|
|
2215
|
+
console.log(body(`${uses} reference${uses === 1 ? "" : "s"} to ${broken.length} token${broken.length === 1 ? "" : "s"} nothing in this repository declares. A ${paint.strong("var()")} with no declaration paints no colour - not a different colour. Where exactly one declared token differs by a namespace, the recipe now uses THAT one, so the look survives; the rest travel as the value you typed.`));
|
|
2087
2216
|
console.log("");
|
|
2088
2217
|
for (const b of broken.slice(0, 8)) {
|
|
2089
2218
|
const where = `${b.count}× in ${b.files} file${b.files === 1 ? "" : "s"}`;
|
|
@@ -2286,6 +2415,75 @@ function printAgentContract() {
|
|
|
2286
2415
|
* just stops letting a person believe the average of three apps is their design
|
|
2287
2416
|
* system.
|
|
2288
2417
|
*/
|
|
2418
|
+
/**
|
|
2419
|
+
* O LUGAR, DITO ANTES DO ESCOPO - e o comando que a resposta implica.
|
|
2420
|
+
*
|
|
2421
|
+
* A detecção de monorepo olhava só para BAIXO (`siblingProjects` lê `apps/`,
|
|
2422
|
+
* `packages/`, `libs/` dentro do root), então quem roda o import de dentro do app
|
|
2423
|
+
* que está migrando recebia silêncio: `apps/web-dashboard` não contém `apps/`.
|
|
2424
|
+
* Medido no monorepo do dono (19/08): 5 pacotes, `packages/ui` declarando 764
|
|
2425
|
+
* tokens em 3 arquivos contra 114 em 6 no app onde ele rodou, e o aviso que
|
|
2426
|
+
* ensina `--scope packages/ui --usage apps/web-dashboard` nunca apareceu.
|
|
2427
|
+
*
|
|
2428
|
+
* A ORDEM É A DA DÚVIDA DE QUEM LÊ: onde eu estou, onde mora o vocabulário, e só
|
|
2429
|
+
* então o comando. Um comando oferecido antes das duas primeiras respostas é uma
|
|
2430
|
+
* receita para copiar sem entender.
|
|
2431
|
+
*/
|
|
2432
|
+
async function sayAboutPlace(root, scope) {
|
|
2433
|
+
const place = await placeInWorkspace(root);
|
|
2434
|
+
/** Sem workspace acima, a pergunta antiga ainda vale: há projetos AQUI dentro? */
|
|
2435
|
+
if (!place) {
|
|
2436
|
+
if (!scope)
|
|
2437
|
+
await sayIfWorkspace(root);
|
|
2438
|
+
return;
|
|
2439
|
+
}
|
|
2440
|
+
/** Na raiz do workspace o aviso que já existia é o certo - ele lista os apps. */
|
|
2441
|
+
if (place.here === null) {
|
|
2442
|
+
if (!scope)
|
|
2443
|
+
await sayIfWorkspace(root);
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2446
|
+
const vocab = place.vocabulary;
|
|
2447
|
+
const mine = place.packages.find((p) => p.rel === place.here);
|
|
2448
|
+
/**
|
|
2449
|
+
* NADA A DIZER quando o vocabulário está aqui mesmo: a pessoa escolheu o
|
|
2450
|
+
* pacote que declara os tokens, e repetir o que ela acertou é ruído.
|
|
2451
|
+
*/
|
|
2452
|
+
if (!vocab || vocab.rel === place.here)
|
|
2453
|
+
return;
|
|
2454
|
+
/** Nem quando o escopo dado já aponta para lá - ela já sabe. */
|
|
2455
|
+
if (scope && join(place.here, scope) === vocab.rel)
|
|
2456
|
+
return;
|
|
2457
|
+
const up = relative(root, place.workspaceRoot).split("\\").join("/") || ".";
|
|
2458
|
+
/**
|
|
2459
|
+
* SÓ QUEM CONSOME A BIBLIOTECA doa evidência. Oferecer `--usage apps/web-admin`
|
|
2460
|
+
* para um app que nunca a importa ensina a medir escolhas de um lugar que não
|
|
2461
|
+
* vota - e `--usage` existe justamente para dizer quais escolhas já são lei.
|
|
2462
|
+
* Medido no monorepo do dono: dos dois outros apps, nenhum declara a
|
|
2463
|
+
* biblioteca, e a primeira versão desta mensagem oferecia os dois.
|
|
2464
|
+
*/
|
|
2465
|
+
const consumers = place.packages.filter((p) => p.rel !== vocab.rel &&
|
|
2466
|
+
p.rel !== place.here &&
|
|
2467
|
+
vocab.name != null &&
|
|
2468
|
+
p.dependsOn.includes(vocab.name));
|
|
2469
|
+
const usage = [place.here, ...consumers.map((c) => c.rel)]
|
|
2470
|
+
.slice(0, 3)
|
|
2471
|
+
.map((a) => `--usage ${a}`)
|
|
2472
|
+
.join(" ");
|
|
2473
|
+
console.log("");
|
|
2474
|
+
console.log(section("This folder is one package of a workspace"));
|
|
2475
|
+
console.log(body(`${paint.strong(place.here)} is one of ${place.packages.length} packages under ${paint.strong(place.workspaceRoot)}.`));
|
|
2476
|
+
console.log("");
|
|
2477
|
+
console.log(body(`And the vocabulary is not here: ${paint.strong(vocab.rel)} declares ${vocab.declarations} token${vocab.declarations === 1 ? "" : "s"} across ${vocab.files} file${vocab.files === 1 ? "" : "s"}, while this package declares ${mine?.declarations ?? 0} across ${mine?.files ?? 0}.`));
|
|
2478
|
+
console.log(body(paint.faint("A system built from the app is the average of one consumer; the tokens live where the library declares them.")));
|
|
2479
|
+
console.log("");
|
|
2480
|
+
console.log(body("The system is one place, and the evidence is another:"));
|
|
2481
|
+
console.log("");
|
|
2482
|
+
console.log(body(` ${paint.strong(`synthesisui import --dir ${up} --scope ${vocab.rel} ${usage}`)}`));
|
|
2483
|
+
console.log(body(paint.faint(` --dir points at the workspace root, so --scope and --usage are read from there.`)));
|
|
2484
|
+
console.log("");
|
|
2485
|
+
console.log(body(paint.dim("Nothing you ran is wasted - this census is a fine diagnosis of this app. It is a system for one consumer, which is the part worth knowing before you publish it.")));
|
|
2486
|
+
}
|
|
2289
2487
|
async function sayIfWorkspace(root) {
|
|
2290
2488
|
const { apps, shared } = await siblingProjects(root);
|
|
2291
2489
|
if (apps.length < 2)
|
|
@@ -3058,11 +3256,22 @@ export async function runImport(opts) {
|
|
|
3058
3256
|
*/
|
|
3059
3257
|
if (!opts.census)
|
|
3060
3258
|
await resolveReadParts(census, root);
|
|
3061
|
-
summarize(census);
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3259
|
+
await summarize(census, root, scope);
|
|
3260
|
+
/**
|
|
3261
|
+
* ONDE ESTOU vem antes de O QUE EU LEIO, e o `!scope` calava exatamente quem
|
|
3262
|
+
* mais precisava ouvir.
|
|
3263
|
+
*
|
|
3264
|
+
* A condição era "só quando ninguém escopou ainda", pelo motivo certo: mandar
|
|
3265
|
+
* escopar para a pasta que a pessoa acabou de escopar lê como a ferramenta não
|
|
3266
|
+
* ter percebido. Só que ela também calava o caso em que o escopo escolhido não
|
|
3267
|
+
* é onde o vocabulário mora - e aí o silêncio não é discrição, é perder a
|
|
3268
|
+
* única chance de dizer que o sistema está em outro pacote.
|
|
3269
|
+
*
|
|
3270
|
+
* `sayAboutPlace` faz as duas perguntas na ordem: onde este comando está, e
|
|
3271
|
+
* onde as declarações estão. Ele só fala quando as respostas divergem.
|
|
3272
|
+
*/
|
|
3273
|
+
if (!opts.census)
|
|
3274
|
+
await sayAboutPlace(root, scope);
|
|
3066
3275
|
// A census handed to us is written BACK TO ITSELF; one we took lands at the
|
|
3067
3276
|
// ROOT, whatever it measured. Everything else in `_synthesisui/` is anchored
|
|
3068
3277
|
// there - config, `ds/<slug>/`, the hook's marker - and the census was the
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import { placeInWorkspace } from "./workspace-place.js";
|
|
4
|
+
/**
|
|
5
|
+
* ONDE ESTE TOKEN É DECLARADO, QUANDO NÃO É NO QUE FOI MEDIDO.
|
|
6
|
+
*
|
|
7
|
+
* `findBrokenRefs` compara os `var()` do escopo contra as declarações do escopo,
|
|
8
|
+
* e o relatório chama o resto de *"tokens your CSS never declares"*. Com
|
|
9
|
+
* `--scope` isso pode ser falso, e no monorepo do dono era (19/08):
|
|
10
|
+
* `--scope src/components` deixa 1021 referências contra 2 declarações, e o CSS
|
|
11
|
+
* dele declara 118 em `src/app/**` e 764 em `packages/ui`.
|
|
12
|
+
*
|
|
13
|
+
* Afirmar um defeito que não existe no código de quem confiou o repositório é
|
|
14
|
+
* pior do que não dizer nada - o relatório é o documento que a pessoa lê depois,
|
|
15
|
+
* sozinha, e ele estava culpando o CSS dela por uma escolha de recorte NOSSA.
|
|
16
|
+
*
|
|
17
|
+
* Então a pergunta muda de "existe?" para "existe ONDE?", em duas camadas: fora
|
|
18
|
+
* do escopo mas dentro do que foi apontado, e nos outros pacotes do mesmo
|
|
19
|
+
* workspace - que é onde uma biblioteca de design system mora.
|
|
20
|
+
*/
|
|
21
|
+
const SHEET = /\.(css|scss|sass|less)$/i;
|
|
22
|
+
const DECL = /(^|[\s{;])(--[a-zA-Z0-9-]+)\s*:/g;
|
|
23
|
+
/** Onde cada nome aparece declarado, primeiro achado ganha - basta um endereço. */
|
|
24
|
+
async function harvest(dir, label, skip, into) {
|
|
25
|
+
const visit = async (at, depth) => {
|
|
26
|
+
if (depth > 8)
|
|
27
|
+
return;
|
|
28
|
+
for (const e of await readdir(at, { withFileTypes: true }).catch(() => [])) {
|
|
29
|
+
if (e.name.startsWith(".") || e.name === "node_modules")
|
|
30
|
+
continue;
|
|
31
|
+
const full = join(at, e.name);
|
|
32
|
+
if (skip && full === skip)
|
|
33
|
+
continue;
|
|
34
|
+
if (e.isDirectory()) {
|
|
35
|
+
await visit(full, depth + 1);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (!SHEET.test(e.name))
|
|
39
|
+
continue;
|
|
40
|
+
const body = await readFile(full, "utf8").catch(() => "");
|
|
41
|
+
for (const m of body.matchAll(DECL)) {
|
|
42
|
+
const name = m[2];
|
|
43
|
+
if (!into.has(name))
|
|
44
|
+
into.set(name, `${label}${relative(dir, full).split("\\").join("/")}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
await visit(dir, 0);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* NOME -> CAMINHO onde ele é declarado, fora do que foi medido.
|
|
52
|
+
*
|
|
53
|
+
* Só roda quando há algo a explicar: sem referências pendentes não há pergunta,
|
|
54
|
+
* e um walk a mais por nada é um walk a mais.
|
|
55
|
+
*/
|
|
56
|
+
export async function declaredElsewhere(root, scope, names) {
|
|
57
|
+
const out = new Map();
|
|
58
|
+
if (names.length === 0)
|
|
59
|
+
return out;
|
|
60
|
+
const wanted = new Set(names);
|
|
61
|
+
const found = new Map();
|
|
62
|
+
/** Camada 1: o resto do que a pessoa apontou, sem reler o escopo. */
|
|
63
|
+
await harvest(root, "", scope ? join(root, scope) : null, found);
|
|
64
|
+
/** Camada 2: os outros pacotes do workspace - onde uma biblioteca mora. */
|
|
65
|
+
const place = await placeInWorkspace(root);
|
|
66
|
+
if (place) {
|
|
67
|
+
for (const pkg of place.packages) {
|
|
68
|
+
if (pkg.rel === place.here)
|
|
69
|
+
continue;
|
|
70
|
+
await harvest(join(place.workspaceRoot, pkg.rel), `${pkg.rel}/`, null, found);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (const [name, where] of found)
|
|
74
|
+
if (wanted.has(name))
|
|
75
|
+
out.set(name, where);
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UM NOME, UM COMPONENTE - e o que fazer quando dois pedem o mesmo.
|
|
3
|
+
*
|
|
4
|
+
* O laço de leitura do import escreve em oito mapas indexados por NOME de
|
|
5
|
+
* componente (`looks`, `sourceOf`, `kindOf`, `propsOf`, `runtimeOf`,
|
|
6
|
+
* `variantAxes`, `islands`, `keyframes`). Dois `Card.tsx` em pastas diferentes
|
|
7
|
+
* disputam a mesma chave, e até aqui o último que o `walk` visitou sobrescrevia
|
|
8
|
+
* os outros - em silêncio, e por uma ordem que é do sistema de arquivos e não
|
|
9
|
+
* uma decisão de ninguém.
|
|
10
|
+
*
|
|
11
|
+
* Medido no monorepo do dono (19/08): 20 nomes colidem no repo inteiro, 14 dentro
|
|
12
|
+
* de `apps/web-dashboard/src`, e quatro arquivos se chamam `Card.tsx` - um deles
|
|
13
|
+
* é o `Card` do design system dele, e dois estão sob `_legacy`. `packages/ui`
|
|
14
|
+
* sozinho não tem colisão nenhuma: o problema mora em escopo largo, que é
|
|
15
|
+
* exatamente o default que a flexibilidade pede.
|
|
16
|
+
*
|
|
17
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
18
|
+
* O DESEMPATE É POR ESTRUTURA, E DETERMINÍSTICO
|
|
19
|
+
*
|
|
20
|
+
* A intuição primeira era "quem o pacote EXPORTA fica com o nome", e ela não
|
|
21
|
+
* responde: `publicApi().exports` é um conjunto de NOMES, e os dois candidatos
|
|
22
|
+
* têm o mesmo nome. Escolher pelo que o pacote realmente publica exige o grafo a
|
|
23
|
+
* partir da entrada do manifesto, e essa é a fatia que também entrega os grupos.
|
|
24
|
+
*
|
|
25
|
+
* Então: menos segmentos de caminho vence, e empate resolve alfabeticamente. É
|
|
26
|
+
* pouco, e é honesto - a diferença que importa é que a resposta deixa de mudar
|
|
27
|
+
* conforme a ordem em que o disco devolve os arquivos.
|
|
28
|
+
*
|
|
29
|
+
* E NUNCA POR NOME DE PASTA. Descartar `_legacy` por se chamar `_legacy` é a lei
|
|
30
|
+
* 14 sendo furada: no repo dele `_legacy` tem 178 arquivos e 106 estão VIVOS,
|
|
31
|
+
* montados por rota (medido 07/08). Classificação é por estrutura, nunca por nome.
|
|
32
|
+
*
|
|
33
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
34
|
+
* O PERDEDOR NÃO ENTRA, E ISSO É DITO
|
|
35
|
+
*
|
|
36
|
+
* Esta fatia não traz as duas receitas - trazer os dois `Card` exige o contexto
|
|
37
|
+
* viajando na chave, que é a fatia dos GRUPOS. O que ela conserta é a perda
|
|
38
|
+
* calada: cada descarte vira uma linha do censo com os dois caminhos, e o
|
|
39
|
+
* relatório os imprime. Uma lacuna declarada é confiança; calada é bug.
|
|
40
|
+
*/
|
|
41
|
+
/** Profundidade como o caminho a declara - o número de pastas até o arquivo. */
|
|
42
|
+
const depth = (file) => file.split("/").length;
|
|
43
|
+
/**
|
|
44
|
+
* ESTE ARQUIVO FICA COM ESTE NOME?
|
|
45
|
+
*
|
|
46
|
+
* `true` quando ele detém o nome depois desta chamada - e aí quem chamou pode
|
|
47
|
+
* escrever nos mapas. `false` quando outro arquivo o detém, e a disputa foi
|
|
48
|
+
* registrada em `collisions`.
|
|
49
|
+
*
|
|
50
|
+
* Idempotente para o mesmo arquivo: o laço lê um arquivo uma vez, mas um
|
|
51
|
+
* componente pode ser reivindicado por mais de um caminho de código, e cobrar
|
|
52
|
+
* uma colisão de um arquivo contra si mesmo seria inventar um conflito.
|
|
53
|
+
*/
|
|
54
|
+
export function claimName(claims, name, file) {
|
|
55
|
+
const held = claims.by.get(name);
|
|
56
|
+
if (held === undefined) {
|
|
57
|
+
claims.by.set(name, file);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
if (held === file)
|
|
61
|
+
return true;
|
|
62
|
+
const incumbentWins = depth(held) < depth(file) ||
|
|
63
|
+
(depth(held) === depth(file) && held.localeCompare(file) <= 0);
|
|
64
|
+
if (incumbentWins) {
|
|
65
|
+
claims.collisions.push({ name, kept: held, dropped: file });
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
claims.by.set(name, file);
|
|
69
|
+
claims.collisions.push({ name, kept: file, dropped: held });
|
|
70
|
+
return true;
|
|
71
|
+
}
|
package/dist/stack.js
CHANGED
|
@@ -27,6 +27,15 @@ import { join } from "node:path";
|
|
|
27
27
|
* app que usa o sistema, então o manifesto daquele app é evidência tanto quanto
|
|
28
28
|
* o da raiz.
|
|
29
29
|
*/
|
|
30
|
+
/**
|
|
31
|
+
* O RANGE QUE DIZ "ISTO É DESTE REPOSITÓRIO" - o mesmo prefixo que
|
|
32
|
+
* `frontier-kind.ts` já reconhece como local, escrito aqui uma vez.
|
|
33
|
+
*
|
|
34
|
+
* Sobrescrever o range com este valor é mais verdadeiro do que preservar o que o
|
|
35
|
+
* app digitou: um `*` num monorepo NÃO significa "qualquer versão do npm", e era
|
|
36
|
+
* assim que ele estava sendo lido.
|
|
37
|
+
*/
|
|
38
|
+
const LOCAL_RANGE = "workspace:*";
|
|
30
39
|
export async function resolveDeps(root) {
|
|
31
40
|
const deps = {};
|
|
32
41
|
const workspaceDirs = [];
|
|
@@ -84,6 +93,47 @@ export async function resolveDeps(root) {
|
|
|
84
93
|
}
|
|
85
94
|
for (const w of workspaceDirs)
|
|
86
95
|
await readManifest(w);
|
|
96
|
+
/**
|
|
97
|
+
* O NOME DE UM PACOTE DESTE WORKSPACE É LOCAL, QUALQUER QUE SEJA O RANGE - e
|
|
98
|
+
* é a lei 13 se aplicando a si mesma: a ORIGEM decide, não o nome.
|
|
99
|
+
*
|
|
100
|
+
* Medido no monorepo do dono (19/08): `apps/web-dashboard` declara
|
|
101
|
+
* `"@frontend-hub/ui": "*"`, que é como npm e yarn workspaces apontam para o
|
|
102
|
+
* pacote ao lado. `LOCAL_RANGE` em `frontier-kind.ts` reconhece
|
|
103
|
+
* `workspace:|file:|link:|portal:`, então `frontierOf` respondia `opaque` -
|
|
104
|
+
* "de terceiro, se desenha sozinha" - sobre a biblioteca que É o design
|
|
105
|
+
* system dele, em 52 arquivos daquele app. Um monorepo que versiona de
|
|
106
|
+
* verdade (`"^2.1.0"`) caía igual.
|
|
107
|
+
*
|
|
108
|
+
* Quem decidia era o FORMATO DO RANGE, que não é a origem nem o nome: é uma
|
|
109
|
+
* terceira coisa. A origem está aqui - a raiz declara `workspaces`, e um
|
|
110
|
+
* daqueles diretórios tem um `package.json` cujo `name` é aquele
|
|
111
|
+
* especificador. Este laço grava esse fato, e ele SOBRESCREVE o range que o
|
|
112
|
+
* app escreveu, porque um pacote deste repositório não deixa de ser deste
|
|
113
|
+
* repositório por causa de como alguém o apontou.
|
|
114
|
+
*
|
|
115
|
+
* Corrigido aqui e não em `frontier-kind.ts` por dois motivos: a informação já
|
|
116
|
+
* passa por esta função, e aquele arquivo é gêmeo byte-idêntico de
|
|
117
|
+
* `libs/ds-contracts/src/frontier-kind.ts` - mudar a pergunta lá custaria os
|
|
118
|
+
* dois, e a pergunta lá está certa.
|
|
119
|
+
*
|
|
120
|
+
* ENTRA MESMO SEM O APP DECLARAR: alias de tsconfig, import direto ou
|
|
121
|
+
* dependência esquecida - o pacote existe no repositório, e é isso que a lei
|
|
122
|
+
* pergunta.
|
|
123
|
+
*/
|
|
124
|
+
for (const w of workspaceDirs) {
|
|
125
|
+
const raw = await readFile(join(w, "package.json"), "utf8").catch(() => null);
|
|
126
|
+
if (!raw)
|
|
127
|
+
continue;
|
|
128
|
+
try {
|
|
129
|
+
const name = JSON.parse(raw).name;
|
|
130
|
+
if (typeof name === "string" && name)
|
|
131
|
+
deps[name] = LOCAL_RANGE;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// manifesto ilegível custa a detecção daquele pacote, nunca a corrida
|
|
135
|
+
}
|
|
136
|
+
}
|
|
87
137
|
return deps;
|
|
88
138
|
}
|
|
89
139
|
/**
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
const MANIFEST = "package.json";
|
|
4
|
+
const SHEET = /\.(css|scss|sass|less)$/i;
|
|
5
|
+
const DECL = /(^|[\s{;])--[a-zA-Z0-9-]+\s*:/g;
|
|
6
|
+
async function manifest(dir) {
|
|
7
|
+
const raw = await readFile(join(dir, MANIFEST), "utf8").catch(() => null);
|
|
8
|
+
if (!raw)
|
|
9
|
+
return null;
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(raw);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Os globs que um manifesto usa para declarar workspaces, nas duas formas reais. */
|
|
18
|
+
function workspaceGlobs(p) {
|
|
19
|
+
const w = p.workspaces;
|
|
20
|
+
const list = Array.isArray(w)
|
|
21
|
+
? w
|
|
22
|
+
: Array.isArray(w?.packages)
|
|
23
|
+
? w.packages
|
|
24
|
+
: [];
|
|
25
|
+
return list.filter((g) => typeof g === "string");
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Quantas custom properties um pacote declara - o mesmo peso que
|
|
29
|
+
* `siblingProjects` já usa para escolher entre `packages/core` e `packages/ui`.
|
|
30
|
+
*
|
|
31
|
+
* Diretório com ponto e `node_modules` ficam fora pelo mesmo motivo que o `walk`
|
|
32
|
+
* do doctor os ignora: `.next/` guarda CSS COMPILADO, e contá-lo faria o build
|
|
33
|
+
* output votar em onde mora o vocabulário.
|
|
34
|
+
*/
|
|
35
|
+
async function weigh(dir) {
|
|
36
|
+
let declarations = 0;
|
|
37
|
+
let files = 0;
|
|
38
|
+
const visit = async (at, depth) => {
|
|
39
|
+
if (depth > 8)
|
|
40
|
+
return;
|
|
41
|
+
for (const e of await readdir(at, { withFileTypes: true }).catch(() => [])) {
|
|
42
|
+
if (e.name.startsWith(".") || e.name === "node_modules")
|
|
43
|
+
continue;
|
|
44
|
+
const full = join(at, e.name);
|
|
45
|
+
if (e.isDirectory()) {
|
|
46
|
+
// Um pacote aninhado fala o próprio sistema - ver `walk`, mesma regra.
|
|
47
|
+
if (await manifest(full))
|
|
48
|
+
continue;
|
|
49
|
+
await visit(full, depth + 1);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (!SHEET.test(e.name))
|
|
53
|
+
continue;
|
|
54
|
+
const body = await readFile(full, "utf8").catch(() => "");
|
|
55
|
+
const n = (body.match(DECL) ?? []).length;
|
|
56
|
+
if (n > 0) {
|
|
57
|
+
declarations += n;
|
|
58
|
+
files += 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
await visit(dir, 0);
|
|
63
|
+
return { declarations, files };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* O LUGAR, ou `null` quando não há workspace nenhum acima - e aí não há nada a
|
|
67
|
+
* anunciar, que é o caso da maioria dos projetos.
|
|
68
|
+
*
|
|
69
|
+
* Sobe no máximo quatro níveis, o mesmo teto de `resolveDeps`: cobre todo layout
|
|
70
|
+
* pnpm/npm/yarn sem sair andando pela pasta pessoal de quem roda.
|
|
71
|
+
*/
|
|
72
|
+
export async function placeInWorkspace(root) {
|
|
73
|
+
let dir = root;
|
|
74
|
+
let workspaceRoot = null;
|
|
75
|
+
let globs = [];
|
|
76
|
+
for (let up = 0; up < 4; up++) {
|
|
77
|
+
const p = await manifest(dir);
|
|
78
|
+
if (p) {
|
|
79
|
+
const g = workspaceGlobs(p);
|
|
80
|
+
if (g.length > 0) {
|
|
81
|
+
workspaceRoot = dir;
|
|
82
|
+
globs = g;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const parent = join(dir, "..");
|
|
87
|
+
if (parent === dir)
|
|
88
|
+
break;
|
|
89
|
+
dir = parent;
|
|
90
|
+
}
|
|
91
|
+
if (!workspaceRoot)
|
|
92
|
+
return null;
|
|
93
|
+
const dirs = [];
|
|
94
|
+
for (const g of globs) {
|
|
95
|
+
if (g.endsWith("/*")) {
|
|
96
|
+
const parent = join(workspaceRoot, g.slice(0, -2));
|
|
97
|
+
for (const e of await readdir(parent, { withFileTypes: true }).catch(() => []))
|
|
98
|
+
if (e.isDirectory())
|
|
99
|
+
dirs.push(join(parent, e.name));
|
|
100
|
+
}
|
|
101
|
+
else if (!g.includes("*")) {
|
|
102
|
+
dirs.push(join(workspaceRoot, g));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const packages = [];
|
|
106
|
+
const local = new Set();
|
|
107
|
+
const read = [];
|
|
108
|
+
for (const d of dirs) {
|
|
109
|
+
const p = await manifest(d);
|
|
110
|
+
if (!p)
|
|
111
|
+
continue;
|
|
112
|
+
read.push({ p, d });
|
|
113
|
+
if (typeof p.name === "string")
|
|
114
|
+
local.add(p.name);
|
|
115
|
+
}
|
|
116
|
+
for (const { p, d } of read) {
|
|
117
|
+
const { declarations, files } = await weigh(d);
|
|
118
|
+
const deps = {
|
|
119
|
+
...(p.dependencies ?? {}),
|
|
120
|
+
...(p.devDependencies ?? {}),
|
|
121
|
+
};
|
|
122
|
+
packages.push({
|
|
123
|
+
rel: relative(workspaceRoot, d).split("\\").join("/"),
|
|
124
|
+
name: typeof p.name === "string" ? p.name : null,
|
|
125
|
+
declarations,
|
|
126
|
+
files,
|
|
127
|
+
dependsOn: Object.keys(deps).filter((k) => local.has(k)),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
if (packages.length === 0)
|
|
131
|
+
return null;
|
|
132
|
+
const rel = relative(workspaceRoot, root).split("\\").join("/");
|
|
133
|
+
const ranked = [...packages].sort((a, b) => b.declarations - a.declarations);
|
|
134
|
+
return {
|
|
135
|
+
workspaceRoot,
|
|
136
|
+
here: rel === "" ? null : rel,
|
|
137
|
+
packages,
|
|
138
|
+
vocabulary: ranked[0]?.declarations > 0 ? ranked[0] : null,
|
|
139
|
+
};
|
|
140
|
+
}
|
package/package.json
CHANGED