@joao.dadas/lume 0.0.0-stage → 0.4.0
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/README.md +20 -2
- package/bin/lume.mjs +78 -0
- package/package.json +28 -4
- package/src/check.mjs +593 -0
- package/src/init.mjs +308 -0
- package/src/projeto.mjs +188 -0
- package/src/registry.mjs +64 -0
- package/src/texto.mjs +244 -0
package/src/init.mjs
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
// lume init: conduz o agente do dev, uma etapa por chamada. Cada resposta é um
|
|
2
|
+
// JSON com `etapa`, `next`, `say`, `instruction`, `run`, `rules` e `details`.
|
|
3
|
+
// O estado vem do próprio projeto (components.json, lume.json, arquivos), então
|
|
4
|
+
// chamar de novo depois de cada passo sempre devolve o passo seguinte.
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { baseInstalada, conferir, KITS, REGRAS } from "./check.mjs";
|
|
9
|
+
import { LAYOUTS, lerProjeto } from "./projeto.mjs";
|
|
10
|
+
import { criarRegistry, REPO } from "./registry.mjs";
|
|
11
|
+
|
|
12
|
+
const PACOTE = "@joao.dadas/lume";
|
|
13
|
+
|
|
14
|
+
export const RULES = [
|
|
15
|
+
"Siga uma etapa por vez: rode o que está em `run`, faça o que está em `instruction` e chame o init de novo. Não pule etapa nem escreva código de tela antes de `next` ser `migrar`.",
|
|
16
|
+
"Fale com o usuário pelo texto de `say`. Quando `next` for `perguntar`, faça a pergunta e espere a resposta antes de seguir.",
|
|
17
|
+
"O lume substitui a interface atual de uma vez. Nunca crie pasta paralela de componentes (components/lume, components/ds) nem desvie os aliases do components.json: components/ui é do lume.",
|
|
18
|
+
"Nunca edite à mão um arquivo instalado da registry (components/ui e os blocos): variação vai por className na composição. O lume check compara cada arquivo com a versão da registry.",
|
|
19
|
+
"Nunca mantenha um componente antigo com a API antiga e a aparência do lume por dentro: reescreva a tela para a API do lume (Button variant default, Dialog no lugar de Modal, render no lugar de asChild).",
|
|
20
|
+
"Nunca troque a fonte, os tamanhos, o raio, a sombra, o movimento ou os ícones. Por produto só a cor muda, por tema instalado depois do base.",
|
|
21
|
+
"Antes de escrever qualquer tela, leia a skill lume-design-engineering (.agents/skills ou .claude/skills).",
|
|
22
|
+
"Não rode git clean, não apague arquivo fora do que a etapa pede e não faça commit nem push sem o usuário pedir.",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function comando(versao, args) {
|
|
26
|
+
return `npx -y ${PACOTE}@${versao} ${args}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resposta(versao, campos) {
|
|
30
|
+
return {
|
|
31
|
+
etapa: campos.etapa,
|
|
32
|
+
next: campos.next,
|
|
33
|
+
say: campos.say,
|
|
34
|
+
instruction: campos.instruction,
|
|
35
|
+
run: campos.run ?? [],
|
|
36
|
+
rules: RULES,
|
|
37
|
+
details: { versao, ...(campos.details ?? {}) },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** "0.4.0" > "0.3.1"? Compara X.Y.Z como números. */
|
|
42
|
+
export function versaoMaiorQue(a, b) {
|
|
43
|
+
const pa = String(a).split(".").map(Number);
|
|
44
|
+
const pb = String(b).split(".").map(Number);
|
|
45
|
+
for (let i = 0; i < 3; i++) {
|
|
46
|
+
if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) > (pb[i] ?? 0);
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const SEMVER = /^\d+\.\d+\.\d+$/;
|
|
52
|
+
|
|
53
|
+
/** Os comandos que instalam (ou reinstalam) tudo do lume numa versão. */
|
|
54
|
+
async function comandosDeInstalacao(projeto, registry, versao) {
|
|
55
|
+
const itens = await registry.itens();
|
|
56
|
+
const ui = itens.filter((i) => i.type === "registry:ui").map((i) => i.name);
|
|
57
|
+
const graficos = projeto.deps.recharts ? ["kit-graficos"] : [];
|
|
58
|
+
const doLumeJson = Array.isArray(projeto.lumeJson?.itens)
|
|
59
|
+
? projeto.lumeJson.itens
|
|
60
|
+
: [];
|
|
61
|
+
const nomes = [
|
|
62
|
+
...new Set([
|
|
63
|
+
...ui,
|
|
64
|
+
...KITS,
|
|
65
|
+
...graficos,
|
|
66
|
+
"login-form",
|
|
67
|
+
"guia",
|
|
68
|
+
...doLumeJson,
|
|
69
|
+
]),
|
|
70
|
+
].filter((n) => n !== "link-next");
|
|
71
|
+
const lista = nomes.map((n) => `${REPO}/${n}#v${versao}`).join(" ");
|
|
72
|
+
const run = [`npx shadcn@latest add ${lista} --overwrite --yes`];
|
|
73
|
+
if (projeto.framework === "next") {
|
|
74
|
+
run.push(
|
|
75
|
+
`npx shadcn@latest add ${REPO}/link-next#v${versao} --overwrite --yes`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return run;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function gravarLumeJson(raiz, lumeJson) {
|
|
82
|
+
writeFileSync(
|
|
83
|
+
path.join(raiz, "lume.json"),
|
|
84
|
+
`${JSON.stringify(lumeJson, null, 2)}\n`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function componentesFaltando(projeto, registry) {
|
|
89
|
+
const itens = await registry.itens();
|
|
90
|
+
const ui = itens.filter((i) => i.type === "registry:ui").map((i) => i.name);
|
|
91
|
+
const faltam = ui.filter(
|
|
92
|
+
(nome) =>
|
|
93
|
+
!existsSync(path.join(projeto.raiz, projeto.dirUi, `${nome}.tsx`)),
|
|
94
|
+
);
|
|
95
|
+
const shell = path.join(
|
|
96
|
+
projeto.raiz,
|
|
97
|
+
projeto.dirComponentes,
|
|
98
|
+
"app-shell",
|
|
99
|
+
"app-shell.tsx",
|
|
100
|
+
);
|
|
101
|
+
if (!existsSync(shell)) faltam.push("app-shell");
|
|
102
|
+
if (projeto.framework === "next") {
|
|
103
|
+
const link = path.join(projeto.raiz, projeto.dirComponentes, "link.tsx");
|
|
104
|
+
if (!existsSync(link) || !readFileSync(link, "utf8").includes("next/link"))
|
|
105
|
+
faltam.push("link-next");
|
|
106
|
+
}
|
|
107
|
+
return { faltam, ui };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function skillInstalada(raiz) {
|
|
111
|
+
return [".agents/skills", ".claude/skills"].some((d) =>
|
|
112
|
+
existsSync(path.join(raiz, d, "lume-design-engineering", "SKILL.md")),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function shellDoLayout(layout) {
|
|
117
|
+
if (layout === "topbar") return '<AppShell layout="topbar" ...>';
|
|
118
|
+
if (layout === "offcanvas")
|
|
119
|
+
return '<AppShell layout="sidebar" collapsible="offcanvas" ...>';
|
|
120
|
+
return '<AppShell layout="sidebar" collapsible="icon" ...>';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Uma etapa do init para o projeto em `raiz`. A versão que vale é a do
|
|
125
|
+
* lume.json; `opcoes.versao` (a do pacote) só entra num projeto sem lume.json
|
|
126
|
+
* ou quando o usuário aceita subir (`atualizar: "sim"`).
|
|
127
|
+
*/
|
|
128
|
+
export async function passo(raiz, opcoes) {
|
|
129
|
+
const {
|
|
130
|
+
versao: versaoPacote,
|
|
131
|
+
layout: layoutPedido,
|
|
132
|
+
atualizar,
|
|
133
|
+
buscar,
|
|
134
|
+
} = opcoes;
|
|
135
|
+
const projeto = lerProjeto(raiz);
|
|
136
|
+
let lumeJson =
|
|
137
|
+
projeto.lumeJson && !projeto.lumeJson.__invalido ? projeto.lumeJson : null;
|
|
138
|
+
let versao = SEMVER.test(String(lumeJson?.versao ?? ""))
|
|
139
|
+
? lumeJson.versao
|
|
140
|
+
: versaoPacote;
|
|
141
|
+
let reinit = comando(versao, "init");
|
|
142
|
+
|
|
143
|
+
if (!projeto.pkg) {
|
|
144
|
+
return resposta(versao, {
|
|
145
|
+
etapa: "ler",
|
|
146
|
+
next: "bloqueado",
|
|
147
|
+
say: "Não achei um package.json aqui. Rode o comando na raiz do projeto.",
|
|
148
|
+
instruction:
|
|
149
|
+
"Vá para a raiz do projeto (a pasta do package.json) e rode o init de novo.",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (!projeto.framework || projeto.tailwind !== 4) {
|
|
153
|
+
return resposta(versao, {
|
|
154
|
+
etapa: "ler",
|
|
155
|
+
next: "bloqueado",
|
|
156
|
+
say: `O lume precisa de Next ou Vite com Tailwind 4. Achei framework ${projeto.framework ?? "desconhecido"} e Tailwind ${projeto.tailwind ?? "ausente"}.`,
|
|
157
|
+
instruction:
|
|
158
|
+
"Mostre isso ao usuário e pare. Com Tailwind 3, a atualização é `npx @tailwindcss/upgrade`, que é uma tarefa própria e precisa do ok dele.",
|
|
159
|
+
details: { framework: projeto.framework, tailwind: projeto.tailwind },
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (layoutPedido) {
|
|
164
|
+
if (!LAYOUTS.includes(layoutPedido)) {
|
|
165
|
+
return resposta(versao, {
|
|
166
|
+
etapa: "ler",
|
|
167
|
+
next: "bloqueado",
|
|
168
|
+
say: `Layout "${layoutPedido}" não existe. As opções são ${LAYOUTS.join(", ")}.`,
|
|
169
|
+
instruction:
|
|
170
|
+
"Pergunte de novo ao usuário e rode o init com --layout=<escolha>.",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
lumeJson = { ...(lumeJson ?? {}), versao, layout: layoutPedido };
|
|
174
|
+
gravarLumeJson(raiz, lumeJson);
|
|
175
|
+
}
|
|
176
|
+
if (!lumeJson?.layout) {
|
|
177
|
+
const temInterface = existsSync(path.join(raiz, projeto.dirUi));
|
|
178
|
+
return resposta(versao, {
|
|
179
|
+
etapa: "ler",
|
|
180
|
+
next: "perguntar",
|
|
181
|
+
say: [
|
|
182
|
+
`Vou aplicar o lume v${versao} neste projeto (${projeto.framework}, Tailwind 4).`,
|
|
183
|
+
temInterface
|
|
184
|
+
? "Ele substitui a interface atual de uma vez: os componentes antigos saem e as telas são reescritas com os componentes do lume, numa mudança só."
|
|
185
|
+
: "Ele instala a base, os componentes e as regras de uma vez.",
|
|
186
|
+
"Qual layout de navegação você quer? sidebar (barra lateral que recolhe para ícones), offcanvas (barra lateral que some) ou topbar (barra no topo, até 7 zonas).",
|
|
187
|
+
].join(" "),
|
|
188
|
+
instruction: `Faça a pergunta de \`say\` ao usuário e espere a resposta. Depois rode \`${reinit} --layout=<sidebar|offcanvas|topbar>\`.`,
|
|
189
|
+
details: {
|
|
190
|
+
framework: projeto.framework,
|
|
191
|
+
alteracoesNaoCommitadas: projeto.alteracoesGit,
|
|
192
|
+
avisoGit:
|
|
193
|
+
projeto.alteracoesGit > 0
|
|
194
|
+
? "Há mudanças não commitadas. Sugira ao usuário commitar ou guardar antes, para a troca ficar numa mudança separada."
|
|
195
|
+
: undefined,
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
const layout = lumeJson.layout;
|
|
200
|
+
|
|
201
|
+
if (versaoMaiorQue(versaoPacote, versao) && atualizar !== "nao") {
|
|
202
|
+
const anterior = versao;
|
|
203
|
+
if (atualizar !== "sim") {
|
|
204
|
+
return resposta(versao, {
|
|
205
|
+
etapa: "atualizar",
|
|
206
|
+
next: "perguntar",
|
|
207
|
+
say: `O projeto está no lume v${anterior} e a v${versaoPacote} já saiu. Quer subir para a v${versaoPacote}? Os arquivos do lume são reinstalados com --overwrite.`,
|
|
208
|
+
instruction: `Faça a pergunta de \`say\` ao usuário e espere a resposta. Se ele quiser subir, rode \`${comando(versaoPacote, "init")} --atualizar=sim\`. Se não, rode \`${reinit}\`, que segue na v${anterior}.`,
|
|
209
|
+
details: { versaoDoProjeto: anterior, versaoNova: versaoPacote },
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
versao = versaoPacote;
|
|
213
|
+
reinit = comando(versao, "init");
|
|
214
|
+
lumeJson = { ...lumeJson, versao };
|
|
215
|
+
gravarLumeJson(raiz, lumeJson);
|
|
216
|
+
projeto.lumeJson = lumeJson;
|
|
217
|
+
if (baseInstalada(projeto)) {
|
|
218
|
+
const registry = criarRegistry(versao, buscar);
|
|
219
|
+
return resposta(versao, {
|
|
220
|
+
etapa: "atualizar",
|
|
221
|
+
next: "rodar",
|
|
222
|
+
say: `Subindo o lume de v${anterior} para v${versao}: os arquivos do lume são reinstalados na versão nova.`,
|
|
223
|
+
instruction: `O lume.json já está na v${versao}. Rode os comandos de \`run\` em ordem e depois \`${reinit}\`. O --overwrite troca os arquivos do lume pela versão nova: é o esperado. Não restaure versão antiga de nenhum deles.`,
|
|
224
|
+
run: await comandosDeInstalacao(projeto, registry, versao),
|
|
225
|
+
details: { versaoAnterior: anterior },
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const registry = criarRegistry(versao, buscar);
|
|
230
|
+
|
|
231
|
+
if (!baseInstalada(projeto)) {
|
|
232
|
+
const cj = projeto.componentsJson;
|
|
233
|
+
const run = [];
|
|
234
|
+
let instrucao = `Rode os comandos de \`run\` na raiz do projeto, em ordem, e depois \`${reinit}\`.`;
|
|
235
|
+
if (cj) {
|
|
236
|
+
run.push("rm components.json");
|
|
237
|
+
instrucao = `O components.json atual não é o do lume (style "${cj.style}", aliases.ui "${cj.aliases?.ui}"). ${instrucao}`;
|
|
238
|
+
}
|
|
239
|
+
run.push(`npx shadcn@latest init ${REPO}/base#v${versao} -y`);
|
|
240
|
+
return resposta(versao, {
|
|
241
|
+
etapa: "base",
|
|
242
|
+
next: "rodar",
|
|
243
|
+
say: "Instalando a base do lume: tokens, fontes e o components.json.",
|
|
244
|
+
instruction: `${instrucao} Nunca use -d no init do shadcn. Se o CSS global antigo tiver tokens próprios (outro @theme inline, outro @layer base, cores próprias), apague esses blocos: fica só o que a base escreveu.`,
|
|
245
|
+
run,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const { faltam } = await componentesFaltando(projeto, registry);
|
|
250
|
+
if (faltam.length > 0) {
|
|
251
|
+
const run = await comandosDeInstalacao(projeto, registry, versao);
|
|
252
|
+
return resposta(versao, {
|
|
253
|
+
etapa: "componentes",
|
|
254
|
+
next: "rodar",
|
|
255
|
+
say: "Instalando todos os componentes do lume no lugar dos atuais.",
|
|
256
|
+
instruction: `Rode os comandos de \`run\` em ordem e depois \`${reinit}\`. O --overwrite troca os arquivos de mesmo nome em components/ui: é o esperado. Não restaure versão antiga de nenhum deles.`,
|
|
257
|
+
run,
|
|
258
|
+
details: { faltam: faltam.slice(0, 20), totalFaltando: faltam.length },
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!skillInstalada(raiz)) {
|
|
263
|
+
return resposta(versao, {
|
|
264
|
+
etapa: "skill",
|
|
265
|
+
next: "rodar",
|
|
266
|
+
say: "Instalando a skill com as regras de design engineering do lume.",
|
|
267
|
+
instruction: `Rode o comando de \`run\` e depois \`${reinit}\`. Depois leia o SKILL.md instalado antes de seguir.`,
|
|
268
|
+
run: [`npx -y skills add ${REPO} --skill lume-design-engineering -y`],
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const resultado = await conferir(raiz, { versao, buscar });
|
|
273
|
+
if (!resultado.ok) {
|
|
274
|
+
const porRegra = {};
|
|
275
|
+
for (const v of resultado.violacoes)
|
|
276
|
+
porRegra[v.regra] = (porRegra[v.regra] ?? 0) + 1;
|
|
277
|
+
const arquivos = new Set(resultado.violacoes.map((v) => v.arquivo));
|
|
278
|
+
return resposta(versao, {
|
|
279
|
+
etapa: "migrar",
|
|
280
|
+
next: "migrar",
|
|
281
|
+
say: `Base, componentes e regras instalados. Faltam ${resultado.violacoes.length} ajustes em ${arquivos.size} arquivos para o projeto ficar no lume; vou reescrever as telas com os componentes dele.`,
|
|
282
|
+
instruction: [
|
|
283
|
+
`Rode \`${comando(versao, "check")}\` para ver a lista inteira e corrija tudo, regra por regra.`,
|
|
284
|
+
"Reescreva cada tela para os componentes de components/ui, na API do lume, e apague os componentes antigos depois de trocar os imports. O que é do produto e não existe no lume vai para components/app, composto com os primitivos do lume.",
|
|
285
|
+
`Monte o shell com ${shellDoLayout(layout)}, o layout que o usuário escolheu.`,
|
|
286
|
+
'Monte cada página como o guia manda: PageHeader, StatGrid com StatCard, Card size="sm", DataTable ou Table, Field com Input, EmptyState.',
|
|
287
|
+
`Quando o check passar, rode \`${reinit}\` de novo.`,
|
|
288
|
+
].join(" "),
|
|
289
|
+
run: [comando(versao, "check")],
|
|
290
|
+
details: {
|
|
291
|
+
layout,
|
|
292
|
+
violacoesPorRegra: porRegra,
|
|
293
|
+
dicas: Object.fromEntries(
|
|
294
|
+
Object.keys(porRegra).map((r) => [r, REGRAS[r]]),
|
|
295
|
+
),
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return resposta(versao, {
|
|
301
|
+
etapa: "pronto",
|
|
302
|
+
next: "pronto",
|
|
303
|
+
say: `O projeto está no lume v${versao}, com layout ${layout}, e o lume check passa.`,
|
|
304
|
+
instruction:
|
|
305
|
+
"Rode o typecheck, o lint, os testes e o build do projeto e mostre o resultado ao usuário. Sugira abrir o app e conferir as telas no claro e no escuro. Não faça commit nem push sem ele pedir.",
|
|
306
|
+
details: { layout },
|
|
307
|
+
});
|
|
308
|
+
}
|
package/src/projeto.mjs
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// Leitura do projeto que vai adotar (ou já adotou) o lume. Só lê: nada aqui
|
|
2
|
+
// grava arquivo.
|
|
3
|
+
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { lerJsonc } from "./texto.mjs";
|
|
8
|
+
|
|
9
|
+
export const LAYOUTS = ["sidebar", "offcanvas", "topbar"];
|
|
10
|
+
|
|
11
|
+
const PULAR = new Set([
|
|
12
|
+
"node_modules",
|
|
13
|
+
".next",
|
|
14
|
+
".git",
|
|
15
|
+
"dist",
|
|
16
|
+
"build",
|
|
17
|
+
"out",
|
|
18
|
+
"coverage",
|
|
19
|
+
".turbo",
|
|
20
|
+
".vercel",
|
|
21
|
+
".agents",
|
|
22
|
+
".claude",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
function lerArquivoJson(arquivo) {
|
|
26
|
+
if (!existsSync(arquivo)) return null;
|
|
27
|
+
try {
|
|
28
|
+
return lerJsonc(readFileSync(arquivo, "utf8"));
|
|
29
|
+
} catch {
|
|
30
|
+
return { __invalido: true };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Lista recursiva de arquivos (caminhos relativos à raiz, com /). */
|
|
35
|
+
export function listar(raiz, rel = "", acc = []) {
|
|
36
|
+
const dir = path.join(raiz, rel);
|
|
37
|
+
let entradas = [];
|
|
38
|
+
try {
|
|
39
|
+
entradas = readdirSync(dir, { withFileTypes: true });
|
|
40
|
+
} catch {
|
|
41
|
+
return acc;
|
|
42
|
+
}
|
|
43
|
+
for (const e of entradas) {
|
|
44
|
+
if (PULAR.has(e.name)) continue;
|
|
45
|
+
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
46
|
+
if (e.isDirectory()) listar(raiz, r, acc);
|
|
47
|
+
else if (e.isFile()) acc.push(r);
|
|
48
|
+
}
|
|
49
|
+
return acc;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function lerTsconfig(raiz) {
|
|
53
|
+
return (
|
|
54
|
+
lerArquivoJson(path.join(raiz, "tsconfig.json")) ??
|
|
55
|
+
lerArquivoJson(path.join(raiz, "jsconfig.json"))
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Os paths do tsconfig já resolvidos pelo baseUrl: { "@/": "src/" }. */
|
|
60
|
+
export function pathsDoTsconfig(raiz) {
|
|
61
|
+
const opcoes = lerTsconfig(raiz)?.compilerOptions ?? {};
|
|
62
|
+
const base = typeof opcoes.baseUrl === "string" ? opcoes.baseUrl : ".";
|
|
63
|
+
const saida = {};
|
|
64
|
+
for (const [chave, alvos] of Object.entries(opcoes.paths ?? {})) {
|
|
65
|
+
if (!chave.endsWith("/*") || !Array.isArray(alvos) || !alvos[0]) continue;
|
|
66
|
+
const alvo = String(alvos[0]).replace(/\*$/, "");
|
|
67
|
+
const junto = path.posix.normalize(path.posix.join(base, alvo || "."));
|
|
68
|
+
const rel =
|
|
69
|
+
junto === "." || junto === "./" ? "" : junto.replace(/^\.\//, "");
|
|
70
|
+
saida[chave.slice(0, -1)] = rel && !rel.endsWith("/") ? `${rel}/` : rel;
|
|
71
|
+
}
|
|
72
|
+
return saida;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Pasta do código do app: o alvo de `@/*` no tsconfig (com baseUrl). Só sem
|
|
77
|
+
* esse path cai em `src/` quando a pasta existe.
|
|
78
|
+
*/
|
|
79
|
+
export function prefixoDoApp(raiz) {
|
|
80
|
+
const paths = pathsDoTsconfig(raiz);
|
|
81
|
+
if ("@/" in paths) return paths["@/"];
|
|
82
|
+
const src = path.join(raiz, "src");
|
|
83
|
+
return existsSync(src) && statSync(src).isDirectory() ? "src/" : "";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** "@/components/ui" vira "src/components/ui" pelos paths do tsconfig. */
|
|
87
|
+
export function resolverAlias(alias, raiz, prefixoPadrao) {
|
|
88
|
+
if (!alias) return null;
|
|
89
|
+
const paths = pathsDoTsconfig(raiz);
|
|
90
|
+
const chaves = Object.keys(paths).sort((a, b) => b.length - a.length);
|
|
91
|
+
for (const de of chaves) {
|
|
92
|
+
if (!alias.startsWith(de)) continue;
|
|
93
|
+
return `${paths[de]}${alias.slice(de.length)}`.replace(/\/$/, "");
|
|
94
|
+
}
|
|
95
|
+
if (alias.startsWith("@/") || alias.startsWith("~/")) {
|
|
96
|
+
return `${prefixoPadrao}${alias.slice(2)}`;
|
|
97
|
+
}
|
|
98
|
+
return alias;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Prefixos de import que apontam para o próprio projeto: os dos aliases do
|
|
103
|
+
* components.json (`#/`, `@app/`) e as chaves de paths do tsconfig.
|
|
104
|
+
*/
|
|
105
|
+
function prefixosLocais(raiz, aliases) {
|
|
106
|
+
const saida = new Set(Object.keys(pathsDoTsconfig(raiz)));
|
|
107
|
+
for (const valor of Object.values(aliases ?? {})) {
|
|
108
|
+
if (typeof valor !== "string") continue;
|
|
109
|
+
const m = /^([^/]+\/)/.exec(valor);
|
|
110
|
+
if (m) saida.add(m[1]);
|
|
111
|
+
}
|
|
112
|
+
return [...saida].filter((p) => !p.startsWith("."));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function versaoMaior(spec) {
|
|
116
|
+
const m = /(\d+)/.exec(String(spec ?? ""));
|
|
117
|
+
return m ? Number(m[1]) : null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function gitSujo(raiz) {
|
|
121
|
+
try {
|
|
122
|
+
const saida = execFileSync("git", ["status", "--porcelain"], {
|
|
123
|
+
cwd: raiz,
|
|
124
|
+
encoding: "utf8",
|
|
125
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
126
|
+
});
|
|
127
|
+
return saida.split("\n").filter(Boolean).length;
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Tudo o que o init e o check precisam saber do projeto. */
|
|
134
|
+
export function lerProjeto(raiz) {
|
|
135
|
+
const pkg = lerArquivoJson(path.join(raiz, "package.json"));
|
|
136
|
+
const deps = {
|
|
137
|
+
...(pkg?.dependencies ?? {}),
|
|
138
|
+
...(pkg?.devDependencies ?? {}),
|
|
139
|
+
};
|
|
140
|
+
const framework = deps.next ? "next" : deps.vite ? "vite" : null;
|
|
141
|
+
const tailwind =
|
|
142
|
+
versaoMaior(deps.tailwindcss) ??
|
|
143
|
+
(deps["@tailwindcss/postcss"] || deps["@tailwindcss/vite"] ? 4 : null);
|
|
144
|
+
const prefixo = prefixoDoApp(raiz);
|
|
145
|
+
const componentsJson = lerArquivoJson(path.join(raiz, "components.json"));
|
|
146
|
+
const lumeJson = lerArquivoJson(path.join(raiz, "lume.json"));
|
|
147
|
+
const aliases = componentsJson?.aliases ?? {};
|
|
148
|
+
const dirUi = resolverAlias(aliases.ui ?? "@/components/ui", raiz, prefixo);
|
|
149
|
+
const dirComponentes = resolverAlias(
|
|
150
|
+
aliases.components ?? "@/components",
|
|
151
|
+
raiz,
|
|
152
|
+
prefixo,
|
|
153
|
+
);
|
|
154
|
+
const dirHooks = resolverAlias(aliases.hooks ?? "@/hooks", raiz, prefixo);
|
|
155
|
+
const dirLib = resolverAlias(aliases.lib ?? "@/lib", raiz, prefixo);
|
|
156
|
+
const candidatosCss = [
|
|
157
|
+
componentsJson?.tailwind?.css,
|
|
158
|
+
`${prefixo}app/globals.css`,
|
|
159
|
+
"app/globals.css",
|
|
160
|
+
`${prefixo}index.css`,
|
|
161
|
+
`${prefixo}styles/globals.css`,
|
|
162
|
+
].filter(Boolean);
|
|
163
|
+
const css = candidatosCss.find((c) => existsSync(path.join(raiz, c))) ?? null;
|
|
164
|
+
return {
|
|
165
|
+
raiz,
|
|
166
|
+
pkg,
|
|
167
|
+
deps,
|
|
168
|
+
framework,
|
|
169
|
+
tailwind,
|
|
170
|
+
prefixo,
|
|
171
|
+
componentsJson,
|
|
172
|
+
lumeJson,
|
|
173
|
+
dirUi,
|
|
174
|
+
dirComponentes,
|
|
175
|
+
dirHooks,
|
|
176
|
+
dirLib,
|
|
177
|
+
prefixosLocais: prefixosLocais(raiz, aliases),
|
|
178
|
+
css,
|
|
179
|
+
alteracoesGit: gitSujo(raiz),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Aponta para a registry do lume (adoção, mesmo que pela metade)? */
|
|
184
|
+
export function adotouLume(projeto) {
|
|
185
|
+
if (projeto.lumeJson) return true;
|
|
186
|
+
const texto = JSON.stringify(projeto.componentsJson ?? {});
|
|
187
|
+
return /joaodadas\/lume|lume-ashmont/.test(texto);
|
|
188
|
+
}
|
package/src/registry.mjs
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Leitura da registry do lume numa versão fixa: o registry.json da tag (lista
|
|
2
|
+
// de itens e caminhos) e o public/r/<item>.json (o conteúdo de cada arquivo).
|
|
3
|
+
// LUME_REGISTRY troca a origem: outra URL base ou uma pasta local do repo.
|
|
4
|
+
|
|
5
|
+
import { readFile } from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
export const REPO = "joaodadas/lume";
|
|
9
|
+
|
|
10
|
+
export function origem(versao) {
|
|
11
|
+
const env = process.env.LUME_REGISTRY;
|
|
12
|
+
if (env) return env.replace(/\/$/, "");
|
|
13
|
+
return `https://raw.githubusercontent.com/${REPO}/v${versao}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function buscarPadrao(url) {
|
|
17
|
+
if (!/^https?:\/\//.test(url)) {
|
|
18
|
+
return JSON.parse(await readFile(url.replace(/^file:\/\//, ""), "utf8"));
|
|
19
|
+
}
|
|
20
|
+
const r = await fetch(url, { headers: { "user-agent": "lume-cli" } });
|
|
21
|
+
if (!r.ok) throw new Error(`${url} respondeu ${r.status}`);
|
|
22
|
+
return r.json();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function criarRegistry(versao, buscar = buscarPadrao) {
|
|
26
|
+
const base = origem(versao);
|
|
27
|
+
const cache = new Map();
|
|
28
|
+
const pegar = (rel) => {
|
|
29
|
+
const url = /^https?:\/\//.test(base)
|
|
30
|
+
? `${base}/${rel}`
|
|
31
|
+
: path.join(base, rel);
|
|
32
|
+
if (!cache.has(url)) cache.set(url, buscar(url));
|
|
33
|
+
return cache.get(url);
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
base,
|
|
37
|
+
/** Itens da versão: { name, type, files: [{ path, target? }] }. */
|
|
38
|
+
async itens() {
|
|
39
|
+
const r = await pegar("registry.json");
|
|
40
|
+
return r.items ?? [];
|
|
41
|
+
},
|
|
42
|
+
/** O item buildado, com o conteúdo de cada arquivo. */
|
|
43
|
+
async item(nome) {
|
|
44
|
+
return pegar(`public/r/${nome}.json`);
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Executa tarefas assíncronas com no máximo `n` ao mesmo tempo. */
|
|
50
|
+
export async function emLotes(lista, n, fn) {
|
|
51
|
+
const saida = new Array(lista.length);
|
|
52
|
+
let proximo = 0;
|
|
53
|
+
const trabalhadores = Array.from(
|
|
54
|
+
{ length: Math.min(n, lista.length) },
|
|
55
|
+
async () => {
|
|
56
|
+
while (proximo < lista.length) {
|
|
57
|
+
const i = proximo++;
|
|
58
|
+
saida[i] = await fn(lista[i], i);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
await Promise.all(trabalhadores);
|
|
63
|
+
return saida;
|
|
64
|
+
}
|