synthesisui 0.16.251 → 0.16.252
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 +157 -11
- package/dist/declared-elsewhere.js +77 -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";
|
|
@@ -37,6 +38,7 @@ import { withLibraryStructure } from "../library-structure.js";
|
|
|
37
38
|
import { body, paint, section } from "../output.js";
|
|
38
39
|
import { phase, startProgress } from "../progress.js";
|
|
39
40
|
import { detectStack, resolveDeps, stackVersions } from "../stack.js";
|
|
41
|
+
import { placeInWorkspace } from "../workspace-place.js";
|
|
40
42
|
import { walk, walkAll } from "./doctor.js";
|
|
41
43
|
/**
|
|
42
44
|
* How many distinct values travel, PER KIND.
|
|
@@ -1996,7 +1998,15 @@ export async function takeCensus(root, opts) {
|
|
|
1996
1998
|
},
|
|
1997
1999
|
};
|
|
1998
2000
|
}
|
|
1999
|
-
|
|
2001
|
+
/**
|
|
2002
|
+
* O RESUMO RECEBE O LUGAR JUNTO COM O CENSO - e não a metade dele.
|
|
2003
|
+
*
|
|
2004
|
+
* `sayBrokenRefs` precisa saber ONDE procurar a declaração que o escopo não viu,
|
|
2005
|
+
* e um parâmetro opcional aqui seria a mesma porta que o §6 do método manda
|
|
2006
|
+
* fechar: uma função que pode ser chamada pela metade vai ser, e o sintoma seria
|
|
2007
|
+
* o relatório voltando a dizer "your CSS never declares" sem que nada falhe.
|
|
2008
|
+
*/
|
|
2009
|
+
async function summarize(c, root, scope) {
|
|
2000
2010
|
const byKind = new Map();
|
|
2001
2011
|
for (const v of c.observed)
|
|
2002
2012
|
byKind.set(v.kind, (byKind.get(v.kind) ?? 0) + v.count);
|
|
@@ -2045,7 +2055,7 @@ function summarize(c) {
|
|
|
2045
2055
|
}
|
|
2046
2056
|
}
|
|
2047
2057
|
sayConventions(c);
|
|
2048
|
-
sayBrokenRefs(c);
|
|
2058
|
+
await sayBrokenRefs(c, root, scope);
|
|
2049
2059
|
}
|
|
2050
2060
|
/**
|
|
2051
2061
|
* THEIR VOCABULARY, MEASURED. This section exists so every later finding can be
|
|
@@ -2076,14 +2086,70 @@ function sayConventions(c) {
|
|
|
2076
2086
|
* finding that needs no agreement with us: the convention being broken is
|
|
2077
2087
|
* theirs.
|
|
2078
2088
|
*/
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2089
|
+
/**
|
|
2090
|
+
* A PASTA QUE CONTÉM TODOS ESTES CAMINHOS - o `--scope` que traria as
|
|
2091
|
+
* declarações junto, derivado dos endereços e não sugerido de cabeça.
|
|
2092
|
+
*
|
|
2093
|
+
* Devolve `null` quando os caminhos não compartilham pasta nenhuma: aí não existe
|
|
2094
|
+
* um escopo único que resolva, e inventar um seria pior que dizer a regra geral.
|
|
2095
|
+
*/
|
|
2096
|
+
function commonPrefix(paths) {
|
|
2097
|
+
if (paths.length === 0)
|
|
2098
|
+
return null;
|
|
2099
|
+
const out = [];
|
|
2100
|
+
for (let i = 0;; i++) {
|
|
2101
|
+
const seg = paths[0][i];
|
|
2102
|
+
// O último segmento é o arquivo, e um arquivo não é um escopo.
|
|
2103
|
+
if (seg == null || i >= paths[0].length - 1)
|
|
2104
|
+
break;
|
|
2105
|
+
if (!paths.every((p) => p[i] === seg && i < p.length - 1))
|
|
2106
|
+
break;
|
|
2107
|
+
out.push(seg);
|
|
2108
|
+
}
|
|
2109
|
+
return out.length > 0 ? out.join("/") : null;
|
|
2110
|
+
}
|
|
2111
|
+
async function sayBrokenRefs(c, root, scope) {
|
|
2112
|
+
const all = c.brokenRefs ?? [];
|
|
2113
|
+
if (all.length === 0)
|
|
2082
2114
|
return;
|
|
2115
|
+
/**
|
|
2116
|
+
* DUAS PERGUNTAS DIFERENTES, E O RELATÓRIO FAZIA UMA SÓ.
|
|
2117
|
+
*
|
|
2118
|
+
* "Não resolve no que eu medi" e "não existe no seu código" são coisas
|
|
2119
|
+
* distintas, e o texto afirmava a segunda. Com `--scope src/components` no
|
|
2120
|
+
* monorepo do dono são 1021 referências contra 2 declarações, enquanto o CSS
|
|
2121
|
+
* dele declara 118 em `src/app/**` e 764 em `packages/ui` (medido 19/08).
|
|
2122
|
+
*
|
|
2123
|
+
* Culpar o CSS de quem confiou o repositório por um recorte NOSSO é o pior
|
|
2124
|
+
* lugar para errar: o relatório é o que a pessoa lê depois, sozinha.
|
|
2125
|
+
*/
|
|
2126
|
+
const at = await declaredElsewhere(root, scope, all.map((b) => b.name)).catch(() => new Map());
|
|
2127
|
+
const outside = all.filter((b) => at.has(b.name));
|
|
2128
|
+
const broken = all.filter((b) => !at.has(b.name));
|
|
2129
|
+
if (outside.length > 0) {
|
|
2130
|
+
const uses = outside.reduce((n, b) => n + b.count, 0);
|
|
2131
|
+
console.log("");
|
|
2132
|
+
console.log(section("These are declared outside what was measured"));
|
|
2133
|
+
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.`));
|
|
2134
|
+
console.log("");
|
|
2135
|
+
for (const b of outside.slice(0, 6)) {
|
|
2136
|
+
console.log(body(`${paint.strong(`var(${b.name})`)} ${paint.faint(`${b.count}× here`)} - declared in ${paint.strong(at.get(b.name) ?? "")}`));
|
|
2137
|
+
}
|
|
2138
|
+
if (outside.length > 6)
|
|
2139
|
+
console.log(body(paint.faint(`(${outside.length - 6} more)`)));
|
|
2140
|
+
console.log("");
|
|
2141
|
+
/** O caminho comum dos endereços é o escopo que traria todos de uma vez. */
|
|
2142
|
+
const wider = commonPrefix([...at.values()].map((v) => v.split("/")));
|
|
2143
|
+
console.log(body(wider
|
|
2144
|
+
? `A wider ${paint.strong("--scope")} keeps your names: ${paint.strong(`--scope ${wider}`)} covers the declarations above.`
|
|
2145
|
+
: "A wider --scope keeps your names, because the declarations come along with the code that uses them."));
|
|
2146
|
+
if (broken.length === 0)
|
|
2147
|
+
return;
|
|
2148
|
+
}
|
|
2083
2149
|
const uses = broken.reduce((n, b) => n + b.count, 0);
|
|
2084
2150
|
console.log("");
|
|
2085
2151
|
console.log(section("These resolve to nothing"));
|
|
2086
|
-
console.log(body(`${uses} reference${uses === 1 ? "" : "s"} to ${broken.length} token${broken.length === 1 ? "" : "s"}
|
|
2152
|
+
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
2153
|
console.log("");
|
|
2088
2154
|
for (const b of broken.slice(0, 8)) {
|
|
2089
2155
|
const where = `${b.count}× in ${b.files} file${b.files === 1 ? "" : "s"}`;
|
|
@@ -2286,6 +2352,75 @@ function printAgentContract() {
|
|
|
2286
2352
|
* just stops letting a person believe the average of three apps is their design
|
|
2287
2353
|
* system.
|
|
2288
2354
|
*/
|
|
2355
|
+
/**
|
|
2356
|
+
* O LUGAR, DITO ANTES DO ESCOPO - e o comando que a resposta implica.
|
|
2357
|
+
*
|
|
2358
|
+
* A detecção de monorepo olhava só para BAIXO (`siblingProjects` lê `apps/`,
|
|
2359
|
+
* `packages/`, `libs/` dentro do root), então quem roda o import de dentro do app
|
|
2360
|
+
* que está migrando recebia silêncio: `apps/web-dashboard` não contém `apps/`.
|
|
2361
|
+
* Medido no monorepo do dono (19/08): 5 pacotes, `packages/ui` declarando 764
|
|
2362
|
+
* tokens em 3 arquivos contra 114 em 6 no app onde ele rodou, e o aviso que
|
|
2363
|
+
* ensina `--scope packages/ui --usage apps/web-dashboard` nunca apareceu.
|
|
2364
|
+
*
|
|
2365
|
+
* A ORDEM É A DA DÚVIDA DE QUEM LÊ: onde eu estou, onde mora o vocabulário, e só
|
|
2366
|
+
* então o comando. Um comando oferecido antes das duas primeiras respostas é uma
|
|
2367
|
+
* receita para copiar sem entender.
|
|
2368
|
+
*/
|
|
2369
|
+
async function sayAboutPlace(root, scope) {
|
|
2370
|
+
const place = await placeInWorkspace(root);
|
|
2371
|
+
/** Sem workspace acima, a pergunta antiga ainda vale: há projetos AQUI dentro? */
|
|
2372
|
+
if (!place) {
|
|
2373
|
+
if (!scope)
|
|
2374
|
+
await sayIfWorkspace(root);
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2377
|
+
/** Na raiz do workspace o aviso que já existia é o certo - ele lista os apps. */
|
|
2378
|
+
if (place.here === null) {
|
|
2379
|
+
if (!scope)
|
|
2380
|
+
await sayIfWorkspace(root);
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
const vocab = place.vocabulary;
|
|
2384
|
+
const mine = place.packages.find((p) => p.rel === place.here);
|
|
2385
|
+
/**
|
|
2386
|
+
* NADA A DIZER quando o vocabulário está aqui mesmo: a pessoa escolheu o
|
|
2387
|
+
* pacote que declara os tokens, e repetir o que ela acertou é ruído.
|
|
2388
|
+
*/
|
|
2389
|
+
if (!vocab || vocab.rel === place.here)
|
|
2390
|
+
return;
|
|
2391
|
+
/** Nem quando o escopo dado já aponta para lá - ela já sabe. */
|
|
2392
|
+
if (scope && join(place.here, scope) === vocab.rel)
|
|
2393
|
+
return;
|
|
2394
|
+
const up = relative(root, place.workspaceRoot).split("\\").join("/") || ".";
|
|
2395
|
+
/**
|
|
2396
|
+
* SÓ QUEM CONSOME A BIBLIOTECA doa evidência. Oferecer `--usage apps/web-admin`
|
|
2397
|
+
* para um app que nunca a importa ensina a medir escolhas de um lugar que não
|
|
2398
|
+
* vota - e `--usage` existe justamente para dizer quais escolhas já são lei.
|
|
2399
|
+
* Medido no monorepo do dono: dos dois outros apps, nenhum declara a
|
|
2400
|
+
* biblioteca, e a primeira versão desta mensagem oferecia os dois.
|
|
2401
|
+
*/
|
|
2402
|
+
const consumers = place.packages.filter((p) => p.rel !== vocab.rel &&
|
|
2403
|
+
p.rel !== place.here &&
|
|
2404
|
+
vocab.name != null &&
|
|
2405
|
+
p.dependsOn.includes(vocab.name));
|
|
2406
|
+
const usage = [place.here, ...consumers.map((c) => c.rel)]
|
|
2407
|
+
.slice(0, 3)
|
|
2408
|
+
.map((a) => `--usage ${a}`)
|
|
2409
|
+
.join(" ");
|
|
2410
|
+
console.log("");
|
|
2411
|
+
console.log(section("This folder is one package of a workspace"));
|
|
2412
|
+
console.log(body(`${paint.strong(place.here)} is one of ${place.packages.length} packages under ${paint.strong(place.workspaceRoot)}.`));
|
|
2413
|
+
console.log("");
|
|
2414
|
+
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}.`));
|
|
2415
|
+
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.")));
|
|
2416
|
+
console.log("");
|
|
2417
|
+
console.log(body("The system is one place, and the evidence is another:"));
|
|
2418
|
+
console.log("");
|
|
2419
|
+
console.log(body(` ${paint.strong(`synthesisui import --dir ${up} --scope ${vocab.rel} ${usage}`)}`));
|
|
2420
|
+
console.log(body(paint.faint(` --dir points at the workspace root, so --scope and --usage are read from there.`)));
|
|
2421
|
+
console.log("");
|
|
2422
|
+
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.")));
|
|
2423
|
+
}
|
|
2289
2424
|
async function sayIfWorkspace(root) {
|
|
2290
2425
|
const { apps, shared } = await siblingProjects(root);
|
|
2291
2426
|
if (apps.length < 2)
|
|
@@ -3058,11 +3193,22 @@ export async function runImport(opts) {
|
|
|
3058
3193
|
*/
|
|
3059
3194
|
if (!opts.census)
|
|
3060
3195
|
await resolveReadParts(census, root);
|
|
3061
|
-
summarize(census);
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3196
|
+
await summarize(census, root, scope);
|
|
3197
|
+
/**
|
|
3198
|
+
* ONDE ESTOU vem antes de O QUE EU LEIO, e o `!scope` calava exatamente quem
|
|
3199
|
+
* mais precisava ouvir.
|
|
3200
|
+
*
|
|
3201
|
+
* A condição era "só quando ninguém escopou ainda", pelo motivo certo: mandar
|
|
3202
|
+
* escopar para a pasta que a pessoa acabou de escopar lê como a ferramenta não
|
|
3203
|
+
* ter percebido. Só que ela também calava o caso em que o escopo escolhido não
|
|
3204
|
+
* é onde o vocabulário mora - e aí o silêncio não é discrição, é perder a
|
|
3205
|
+
* única chance de dizer que o sistema está em outro pacote.
|
|
3206
|
+
*
|
|
3207
|
+
* `sayAboutPlace` faz as duas perguntas na ordem: onde este comando está, e
|
|
3208
|
+
* onde as declarações estão. Ele só fala quando as respostas divergem.
|
|
3209
|
+
*/
|
|
3210
|
+
if (!opts.census)
|
|
3211
|
+
await sayAboutPlace(root, scope);
|
|
3066
3212
|
// A census handed to us is written BACK TO ITSELF; one we took lands at the
|
|
3067
3213
|
// ROOT, whatever it measured. Everything else in `_synthesisui/` is anchored
|
|
3068
3214
|
// 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
|
+
}
|
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