vanilla-builder 0.0.0-stage → 0.2.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/bin/builder.js ADDED
@@ -0,0 +1,471 @@
1
+ #!/usr/bin/env node
2
+ // Línea de comandos de Builder: build, analyze, init, dev y serve.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { spawn } from 'node:child_process';
6
+ import { loadConfig, findConfigFile } from '../src/config.js';
7
+ import { runPipeline, VERSION } from '../src/builder.js';
8
+ import { renderReport } from '../src/report.js';
9
+ import { startServer } from '../src/server.js';
10
+ import { watchTree } from '../src/watch.js';
11
+ import { BuildError, formatBytes, relFrom, isInside, JS_EXT, extOf, toPosix } from '../src/util.js';
12
+ import { globMatcher } from '../src/files.js';
13
+
14
+ const tty = process.stdout.isTTY && !process.env.NO_COLOR;
15
+ const color = (code) => (text) => (tty ? `\x1b[${code}m${text}\x1b[0m` : String(text));
16
+ const C = { red: color(31), yellow: color(33), green: color(32), blue: color(34), gray: color(90), bold: color(1), cyan: color(36) };
17
+
18
+ const HELP = `Builder ${VERSION} — empaqueta, minifica y ofusca proyectos web vanilla (HTML, CSS y JS)
19
+
20
+ Uso:
21
+ builder build [carpeta] [opciones] Compila el proyecto en dist/ (por defecto)
22
+ builder analyze [carpeta] [--json] Muestra dependencias detectadas y la salida prevista, sin escribir
23
+ builder init [carpeta] [--force] Crea builder.config.json con lo detectado
24
+ builder deps [carpeta] Lista qué JS son librerías de terceros (no se ofuscan) y cuáles son tuyos
25
+ builder deps add|own|reset <patrón…> Marca librerías (add), tu código (own) o vuelve a lo automático (reset)
26
+ builder dev [carpeta] [--port N] Compila, vigila cambios y sirve dist/ con recarga automática
27
+ builder serve [carpeta] [--port N] Sirve la carpeta de salida para probarla
28
+
29
+ Marcar dependencias sin configuración:
30
+ En el HTML: <script src="js/lib.js" data-builder="dependency"></script> (o "own")
31
+ En el archivo: /* @builder-dependency */ al principio (o @builder-own)
32
+ Con deps: builder deps add "js/libs/**" (en otra carpeta: --dir <carpeta>)
33
+
34
+ Opciones de build:
35
+ --out <carpeta> Carpeta de salida (por defecto dist)
36
+ --config <archivo> Archivo de configuración (por defecto builder.config.json)
37
+ --entry <archivo> Entrada explícita (repetible). Por defecto: todos los .html completos
38
+ --include <patrón> Fuerza incluir archivos (repetible), ej. "data/**"
39
+ --ignore <patrón> Excluye archivos (repetible), ej. "borradores/**"
40
+ --obfuscate [nivel] Ofusca tu JS: low | medium | high | max (por defecto medium)
41
+ Las librerías de terceros se detectan solas y NO se ofuscan.
42
+ --dep <patrón> Marca a mano archivos como dependencia (no se ofuscan), repetible
43
+ --not-dep <patrón> Corrige la detección: ese archivo es tuyo y sí se ofusca, repetible
44
+ --obfuscate-deps Ofusca también las dependencias
45
+ --minifier terser Compresión máxima del JS con una segunda pasada de terser
46
+ --compress [gzip|brotli] Genera además .gz y .br precomprimidos (por defecto ambos)
47
+ --hash Nombres con hash en JS/CSS (caché a largo plazo)
48
+ --sourcemap [modo] Genera source maps (linked | hidden | inline)
49
+ --target <target> Navegadores/ES de salida, ej. es2018 (por defecto: el del propio código)
50
+ --drop-console Quita console.* y debugger
51
+ --no-minify No minifica (útil para depurar)
52
+ --no-merge No une scripts clásicos consecutivos
53
+ --unreferenced <modo> Archivos no referenciados: process | copy | skip (por defecto process)
54
+ --base <ruta> Ruta base del sitio para URLs absolutas (por defecto /)
55
+ --workers <n|auto> Hilos para ofuscar/comprimir en paralelo (0 = sin hilos; por defecto auto)
56
+ --no-cache No usa la caché de compilaciones anteriores
57
+ --no-verify Omite la verificación final (no recomendado)
58
+ --force Permite vaciar una carpeta de salida que no creó Builder
59
+ --keep-failed Conserva la carpeta temporal si la verificación falla
60
+ --json Resultado en JSON (para CI)
61
+ --verbose Muestra notas, archivos copiados y excluidos
62
+ --quiet Solo errores
63
+ `;
64
+
65
+ function parseArgs(argv) {
66
+ const args = { _: [], entry: [], include: [], ignore: [], dep: [], 'not-dep': [] };
67
+ const takesValue = new Set([
68
+ 'out',
69
+ 'config',
70
+ 'entry',
71
+ 'include',
72
+ 'ignore',
73
+ 'target',
74
+ 'unreferenced',
75
+ 'base',
76
+ 'port',
77
+ 'host',
78
+ 'dep',
79
+ 'not-dep',
80
+ 'minifier',
81
+ 'workers',
82
+ 'dir',
83
+ ]);
84
+ const optionalValue = { obfuscate: ['low', 'medium', 'high', 'max'], sourcemap: ['linked', 'hidden', 'inline'], compress: ['gzip', 'brotli'] };
85
+ for (let i = 0; i < argv.length; i++) {
86
+ const a = argv[i];
87
+ if (!a.startsWith('--')) {
88
+ args._.push(a);
89
+ continue;
90
+ }
91
+ let [key, val] = a.slice(2).split(/=(.*)/s);
92
+ if (takesValue.has(key)) {
93
+ if (val === undefined) val = argv[++i];
94
+ if (val === undefined) throw new BuildError(`Falta el valor de --${key}.`);
95
+ if (Array.isArray(args[key])) args[key].push(val);
96
+ else args[key] = val;
97
+ } else if (optionalValue[key]) {
98
+ if (val === undefined && optionalValue[key].includes(argv[i + 1])) val = argv[++i];
99
+ args[key] = val ?? true;
100
+ } else if (key.startsWith('no-')) args[key.slice(3)] = false;
101
+ else args[key] = val ?? true;
102
+ }
103
+ return args;
104
+ }
105
+
106
+ function overridesFrom(args) {
107
+ const o = {};
108
+ if (args.out) o.outDir = args.out;
109
+ if (args.entry.length) o.entries = args.entry;
110
+ if (args.include.length) o.include = args.include;
111
+ if (args.ignore.length) o.ignore = args.ignore;
112
+ if (args.obfuscate !== undefined) o.obfuscate = args.obfuscate === true ? 'medium' : args.obfuscate;
113
+ if (args.hash !== undefined) o.hash = !!args.hash;
114
+ if (args.sourcemap !== undefined) o.sourcemap = args.sourcemap === 'linked' ? true : args.sourcemap;
115
+ if (args.unreferenced) o.unreferenced = args.unreferenced;
116
+ if (args.base) o.base = args.base;
117
+ if (args.verify === false) o.verify = false;
118
+ if (args.compress !== undefined) o.compress = args.compress;
119
+ if (args.cache === false) o.cache = false;
120
+ if (args.workers !== undefined) {
121
+ const n = Number(args.workers);
122
+ if (args.workers !== 'auto' && !(Number.isInteger(n) && n >= 0)) throw new BuildError('--workers debe ser "auto" o un número (0 = sin hilos).');
123
+ o.workers = args.workers === 'auto' ? 'auto' : n;
124
+ }
125
+ const deps = {};
126
+ if (args.dep.length) deps.include = args.dep;
127
+ if (args['not-dep'].length) deps.exclude = args['not-dep'];
128
+ if (args['obfuscate-deps']) deps.obfuscate = true;
129
+ if (args['detect-deps'] === false) deps.detect = false;
130
+ if (Object.keys(deps).length) o.dependencies = deps;
131
+ const js = {};
132
+ if (args.target) js.target = args.target;
133
+ if (args['drop-console']) js.drop = ['console', 'debugger'];
134
+ if (args.merge === false) js.mergeClassic = false;
135
+ if (args.minifier) js.minifier = args.minifier;
136
+ if (args.minify === false) {
137
+ js.minify = false;
138
+ o.css = { minify: false };
139
+ o.html = { minify: false };
140
+ }
141
+ if (Object.keys(js).length) o.js = js;
142
+ return o;
143
+ }
144
+
145
+ function printList(title, items, colorFn, limit = 30) {
146
+ if (!items.length) return;
147
+ console.log(`\n${colorFn(`${title} (${items.length})`)}`);
148
+ for (const i of items.slice(0, limit)) console.log(` • ${i}`);
149
+ if (items.length > limit) console.log(C.gray(` … y ${items.length - limit} más (usa --json para verlos todos)`));
150
+ }
151
+
152
+ function jsonResult(result) {
153
+ return {
154
+ ok: true,
155
+ version: result.version,
156
+ root: result.root,
157
+ outDir: result.outDir,
158
+ durationMs: result.durationMs,
159
+ pages: result.pages,
160
+ outputs: result.report.rows,
161
+ totals: result.report.totals,
162
+ coverage: Object.fromEntries(result.coverage),
163
+ ignored: result.ignored,
164
+ broken: result.broken,
165
+ workers: result.workers,
166
+ standalone: result.standalone,
167
+ dependencies: result.dependencies,
168
+ vendor: result.vendor,
169
+ warnings: result.warnings,
170
+ notes: result.notes,
171
+ verify: result.verify && { errors: result.verify.errors, stats: result.verify.stats },
172
+ perf: result.perf,
173
+ };
174
+ }
175
+
176
+ function printSummary(result, args) {
177
+ const r = result.report;
178
+ const counts = Object.entries(r.byType)
179
+ .map(([t, v]) => `${v.files} ${t}`)
180
+ .join(' · ');
181
+ console.log(renderReport(r, C, { verbose: args.verbose }));
182
+ console.log(`\n${C.green('✔')} ${counts} ${C.gray(`(${(result.durationMs / 1000).toFixed(2)} s)`)}`);
183
+ if (result.verify) {
184
+ const s = result.verify.stats;
185
+ console.log(`${C.green('✔')} Verificación: ${s.files} archivos escritos, ${s.refs} referencias comprobadas, ${s.js} JS válidos, 0 errores`);
186
+ }
187
+ console.log(`${C.green('✔')} Cobertura: ${result.coverage.size}/${result.coverage.size} archivos de origen con destino conocido`);
188
+ if (r.compressed) console.log(`${C.green('✔')} ${r.compressed} archivos precomprimidos (.gz/.br)`);
189
+ const p = result.perf;
190
+ if (p?.tasks) {
191
+ const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
192
+ const where = p.threads ? `en el hilo principal y ${plural(p.threads, 'hilo', 'hilos')} más` : 'en el hilo principal';
193
+ const all = p.cacheHits === p.tasks;
194
+ const detail = all
195
+ ? p.tasks === 1
196
+ ? 'reutilizado de la caché'
197
+ : 'todos reutilizados de la caché'
198
+ : `${where}${p.cacheHits ? `, ${p.cacheHits} de la caché` : ''}`;
199
+ console.log(`${C.green('✔')} Ofuscación/terser: ${plural(p.tasks, 'archivo', 'archivos')}, ${detail}`);
200
+ }
201
+ if (result.obfuscated) {
202
+ const deps = [
203
+ ...result.dependencies.map((d) => `${d.rel} — ${d.reason}`),
204
+ ...result.vendor.map((v) => `${v} — paquete importado (archivo vendor aparte)`),
205
+ ];
206
+ if (deps.length) printList('Dependencias detectadas (no se ofuscan)', deps, C.cyan);
207
+ }
208
+ printList('Avisos', result.warnings, C.yellow);
209
+ if (args.verbose) {
210
+ printList('Notas', result.notes, C.cyan, 200);
211
+ printList(
212
+ 'Excluidos',
213
+ result.ignored.map((i) => `${i.rel} — ${i.reason}`),
214
+ C.gray,
215
+ 200,
216
+ );
217
+ } else if (result.notes.length) console.log(C.gray(`\n${result.notes.length} nota(s) informativas: usa --verbose para verlas.`));
218
+ }
219
+
220
+ async function cmdBuild(dir, args) {
221
+ const cfg = await loadConfig({ dir, config: args.config, overrides: overridesFrom(args) });
222
+ const log = args.quiet || args.json ? () => {} : (m) => console.log(C.gray(` ${m}`));
223
+ if (!args.quiet && !args.json) console.log(`${C.bold(`Builder ${VERSION}`)} ${cfg.root} ${C.gray('→')} ${cfg.outDir}`);
224
+ const result = await runPipeline(cfg, { mode: 'build', force: !!args.force, keepFailed: !!args['keep-failed'], log });
225
+ if (args.json) console.log(JSON.stringify(jsonResult(result), null, 2));
226
+ else if (!args.quiet) printSummary(result, args);
227
+ return result;
228
+ }
229
+
230
+ async function cmdAnalyze(dir, args) {
231
+ const cfg = await loadConfig({ dir, config: args.config, overrides: overridesFrom(args) });
232
+ const result = await runPipeline(cfg, { mode: 'analyze' });
233
+ if (args.json) {
234
+ console.log(JSON.stringify(jsonResult(result), null, 2));
235
+ return;
236
+ }
237
+ console.log(`${C.bold(`Builder ${VERSION} — análisis`)} ${cfg.root}\n`);
238
+ console.log(`${C.bold('Páginas completas')}: ${result.pages.full.join(', ') || '—'}`);
239
+ if (result.pages.conservative.length)
240
+ console.log(`${C.bold('Modo conservador')} (fragmentos u otras codificaciones): ${result.pages.conservative.join(', ')}`);
241
+ console.log(`\n${C.bold('Qué pasará con cada archivo del proyecto')}`);
242
+ const rows = [...result.coverage.entries()].sort((a, b) => a[0].localeCompare(b[0]));
243
+ const w = Math.min(50, Math.max(...rows.map(([k]) => k.length), 10));
244
+ for (const [rel, list] of rows) {
245
+ const shown = args.verbose ? list : list.filter((s) => s !== `copiado → ${rel}`);
246
+ if (!shown.length) continue;
247
+ console.log(` ${rel.padEnd(w)} ${C.gray(shown.join(' | '))}`);
248
+ }
249
+ const copied = rows.filter(([rel, list]) => list.length === 1 && list[0] === `copiado → ${rel}`).length;
250
+ if (copied && !args.verbose) console.log(C.gray(` + ${copied} recursos que se copian sin cambios (--verbose para listarlos)`));
251
+ const deps = [...result.dependencies.map((d) => `${d.rel} — ${d.reason}`)];
252
+ printList('Dependencias de terceros detectadas (no se ofuscan)', deps, C.cyan);
253
+ if (result.workers.length)
254
+ printList(
255
+ 'Workers detectados',
256
+ result.workers.map((wk) => `${wk.rel} (${wk.kind}${wk.module ? ', módulo' : ''}) desde ${wk.from}`),
257
+ C.cyan,
258
+ );
259
+ printList(
260
+ 'Referencias rotas en el código fuente',
261
+ result.broken.map((b) => `${b.from} → ${b.url} (${b.status})`),
262
+ C.yellow,
263
+ );
264
+ printList('Avisos', result.warnings, C.yellow);
265
+ printList(
266
+ 'Excluidos',
267
+ result.ignored.map((i) => `${i.rel} — ${i.reason}`),
268
+ C.gray,
269
+ args.verbose ? 500 : 15,
270
+ );
271
+ if (args.verbose) printList('Notas', result.notes, C.cyan, 500);
272
+ const t = result.report.totals;
273
+ console.log(`\nSalida prevista: ${t.files} archivos, ${formatBytes(t.before)} → ${formatBytes(t.size)} (sin ofuscar). Nada se escribió.`);
274
+ }
275
+
276
+ async function cmdInit(dir, args) {
277
+ const projectDir = path.resolve(dir);
278
+ const existing = findConfigFile(projectDir);
279
+ if (existing && !args.force) throw new BuildError(`Ya existe ${path.basename(existing)}. Usa --force para reemplazarlo.`);
280
+ const cfg = await loadConfig({ dir, config: args.config, overrides: overridesFrom(args) });
281
+ const result = await runPipeline(cfg, { mode: 'analyze' });
282
+ const config = {
283
+ outDir: relFrom(cfg.root, cfg.outDir) ?? cfg.outDir,
284
+ entries: 'auto',
285
+ include: [],
286
+ ignore: [],
287
+ unreferenced: 'process',
288
+ obfuscate: false,
289
+ dependencies: { detect: true, obfuscate: false, include: [], exclude: [] },
290
+ hash: false,
291
+ sourcemap: false,
292
+ compress: false,
293
+ js: { target: 'auto', minifier: 'esbuild', drop: [] },
294
+ };
295
+ const file = path.join(projectDir, 'builder.config.json');
296
+ fs.writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
297
+ console.log(`${C.green('✔')} Creado ${file}`);
298
+ console.log(` Páginas detectadas: ${result.pages.full.join(', ') || '— (ninguna; define "entries")'}`);
299
+ if (result.broken.length) console.log(C.yellow(` Hay ${result.broken.length} referencia(s) rota(s) en el código; míralas con "builder analyze".`));
300
+ if (result.dependencies.length)
301
+ console.log(` Librerías de terceros detectadas (no se ofuscarán): ${result.dependencies.map((d) => d.rel).join(', ')}`);
302
+ console.log(C.gray(' Edita "include" para forzar archivos que solo se cargan desde JS, e "ignore" para excluir.'));
303
+ console.log(C.gray(' En "dependencies.include" marcas a mano librerías; en "dependencies.exclude" corriges una detección.'));
304
+ }
305
+
306
+ /** builder deps [carpeta] — lista; builder deps add|own|reset <patrón…> — edita builder.config.json. */
307
+ async function cmdDeps(_dir, args) {
308
+ const sub = args._[1];
309
+ if (['add', 'own', 'reset'].includes(sub)) return editDeps(sub, args._.slice(2), args);
310
+ const dir = args.dir ?? sub ?? '.';
311
+ const cfg = await loadConfig({ dir, config: args.config, overrides: overridesFrom(args) });
312
+ const result = await runPipeline(cfg, { mode: 'analyze' });
313
+ const deps = new Map(result.dependencies.map((d) => [d.rel, d.reason]));
314
+ const own = [...result.coverage.keys()].filter((rel) => JS_EXT.has(extOf(rel)) && !deps.has(rel)).sort();
315
+ if (args.json) {
316
+ console.log(JSON.stringify({ dependencies: result.dependencies, own }, null, 2));
317
+ return;
318
+ }
319
+ console.log(`${C.bold(`Builder ${VERSION} — dependencias`)} ${cfg.root}`);
320
+ printList(
321
+ 'Librerías de terceros (no se ofuscan)',
322
+ [...deps].map(([rel, why]) => `${rel} ${C.gray(`— ${why}`)}`),
323
+ C.cyan,
324
+ 200,
325
+ );
326
+ printList('Tu código (se ofusca con --obfuscate)', own, C.green, 200);
327
+ if (!deps.size && !own.length) console.log('\nNo hay archivos JavaScript en el proyecto.');
328
+ console.log(C.gray('\n¿Algo mal clasificado?'));
329
+ console.log(C.gray(' builder deps add <patrón> es una librería: no se ofusca'));
330
+ console.log(C.gray(' builder deps own <patrón> es tu código: sí se ofusca'));
331
+ console.log(C.gray(' builder deps reset <patrón> vuelve a la detección automática'));
332
+ console.log(C.gray(' También: data-builder="dependency" en el <script>, o /* @builder-dependency */ al inicio del archivo.'));
333
+ }
334
+
335
+ async function editDeps(sub, rawPatterns, args) {
336
+ if (!rawPatterns.length) throw new BuildError(`Indica uno o más archivos o patrones: builder deps ${sub} "js/vendor/**"`);
337
+ const projectDir = path.resolve(args.dir ?? '.');
338
+ const file = args.config ? path.resolve(args.config) : (findConfigFile(projectDir) ?? path.join(projectDir, 'builder.config.json'));
339
+ if (!file.endsWith('.json'))
340
+ throw new BuildError(`La configuración está en ${path.basename(file)} (JavaScript); añade los patrones a mano en "dependencies".`);
341
+ const config = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8').replace(/^/, '')) : {};
342
+ if (config.dependencies === false || config.dependencies === true || config.dependencies == null) config.dependencies = {};
343
+ const d = config.dependencies;
344
+ const include = new Set(d.include ?? []);
345
+ const exclude = new Set(d.exclude ?? []);
346
+ const patterns = rawPatterns.map((p) => toPosix(p).replace(/^\.\//, ''));
347
+ for (const p of patterns) {
348
+ if (sub === 'add') {
349
+ include.add(p);
350
+ exclude.delete(p);
351
+ } else if (sub === 'own') {
352
+ exclude.add(p);
353
+ include.delete(p);
354
+ } else {
355
+ include.delete(p);
356
+ exclude.delete(p);
357
+ }
358
+ }
359
+ d.include = [...include];
360
+ d.exclude = [...exclude];
361
+ // Se valida antes de escribir: nunca deja un builder.config.json roto.
362
+ const cfg = await loadConfig({ dir: projectDir, config: args.config, overrides: { dependencies: { include: d.include, exclude: d.exclude } } });
363
+ const existed = fs.existsSync(file);
364
+ fs.writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
365
+ const result = await runPipeline(cfg, { mode: 'analyze' });
366
+ const deps = new Map(result.dependencies.map((x) => [x.rel, x.reason]));
367
+ const jsFiles = [...result.coverage.keys()].filter((rel) => JS_EXT.has(extOf(rel)));
368
+ console.log(`${C.green('✔')} ${existed ? 'Actualizado' : 'Creado'} ${file}`);
369
+ for (const p of patterns) {
370
+ const hits = jsFiles.filter(globMatcher([p]));
371
+ if (!hits.length) {
372
+ console.log(C.yellow(` ! "${p}" no coincide con ningún archivo JavaScript del proyecto (queda guardado igualmente)`));
373
+ continue;
374
+ }
375
+ for (const rel of hits) console.log(` ${rel} ${deps.has(rel) ? C.cyan(`librería — ${deps.get(rel)}`) : C.green('tu código — se ofusca')}`);
376
+ }
377
+ }
378
+
379
+ async function cmdServe(dir, args) {
380
+ const cfg = await loadConfig({ dir, config: args.config, overrides: overridesFrom(args) });
381
+ const target = fs.existsSync(cfg.outDir) ? cfg.outDir : null;
382
+ if (!target) throw new BuildError(`No existe ${cfg.outDir}. Ejecuta primero "builder build".`);
383
+ const srv = await startServer({ dir: target, port: Number(args.port ?? 5173), host: args.host ?? '127.0.0.1' });
384
+ console.log(`${C.green('✔')} Sirviendo ${target} en ${C.bold(srv.url)} ${C.gray('(Ctrl+C para salir)')}`);
385
+ if (args.open) openBrowser(srv.url);
386
+ }
387
+
388
+ async function cmdDev(dir, args) {
389
+ const cfg = await loadConfig({ dir, config: args.config, overrides: overridesFrom(args) });
390
+ let srv = null;
391
+ let running = false;
392
+ let pending = false;
393
+ const rebuild = async (reason) => {
394
+ if (running) {
395
+ pending = true;
396
+ return;
397
+ }
398
+ running = true;
399
+ const t = Date.now();
400
+ try {
401
+ const fresh = await loadConfig({ dir, config: args.config, overrides: overridesFrom(args) });
402
+ const result = await runPipeline(fresh, { mode: 'build', force: !!args.force });
403
+ console.log(
404
+ `${C.green('✔')} ${reason ? `${reason} → ` : ''}compilado en ${Date.now() - t} ms${result.warnings.length ? C.yellow(` (${result.warnings.length} avisos)`) : ''}`,
405
+ );
406
+ for (const w of result.warnings.slice(0, 5)) console.log(C.yellow(` • ${w}`));
407
+ srv?.buildOk();
408
+ srv?.reload();
409
+ } catch (err) {
410
+ console.error(C.red(`✖ ${err.message}`));
411
+ for (const d of err.details ?? []) console.error(C.red(` • ${d}`));
412
+ srv?.buildError([err.message, ...(err.details ?? [])].join('\n'));
413
+ } finally {
414
+ running = false;
415
+ if (pending) {
416
+ pending = false;
417
+ rebuild('cambios pendientes');
418
+ }
419
+ }
420
+ };
421
+ await rebuild();
422
+ fs.mkdirSync(cfg.outDir, { recursive: true });
423
+ srv = await startServer({ dir: cfg.outDir, port: Number(args.port ?? 5173), host: args.host ?? '127.0.0.1', liveReload: true });
424
+ console.log(`${C.green('✔')} Sirviendo ${cfg.outDir} en ${C.bold(srv.url)} con recarga automática ${C.gray('(Ctrl+C para salir)')}`);
425
+ if (args.open) openBrowser(srv.url);
426
+ let timer = null;
427
+ const changed = new Set();
428
+ const outDir = path.resolve(cfg.outDir);
429
+ const ignore = (rel) => {
430
+ const abs = path.join(cfg.root, rel);
431
+ return abs === outDir || isInside(outDir, abs) || /(^|\/)(node_modules|\.git)(\/|$)|\.builder-(tmp|old)-/.test(rel);
432
+ };
433
+ watchTree(cfg.root, ignore, (rel) => {
434
+ changed.add(rel);
435
+ clearTimeout(timer);
436
+ timer = setTimeout(() => {
437
+ const list = [...changed];
438
+ changed.clear();
439
+ rebuild(list.length === 1 ? list[0] : `${list.length} archivos`);
440
+ }, 150);
441
+ });
442
+ }
443
+
444
+ function openBrowser(url) {
445
+ const cmd = process.platform === 'win32' ? 'cmd' : process.platform === 'darwin' ? 'open' : 'xdg-open';
446
+ const argv = process.platform === 'win32' ? ['/c', 'start', '""', url] : [url];
447
+ spawn(cmd, argv, { detached: true, stdio: 'ignore' }).unref();
448
+ }
449
+
450
+ async function main() {
451
+ const argv = process.argv.slice(2);
452
+ const args = parseArgs(argv);
453
+ if (args.version || args.v) return console.log(VERSION);
454
+ const [command = 'help', dir = '.'] = args._;
455
+ if (args.help || args.h || command === 'help') return console.log(HELP);
456
+ const commands = { build: cmdBuild, analyze: cmdAnalyze, init: cmdInit, serve: cmdServe, dev: cmdDev, deps: cmdDeps };
457
+ if (!commands[command]) throw new BuildError(`Comando desconocido "${command}". Usa "builder help".`);
458
+ await commands[command](dir, args);
459
+ }
460
+
461
+ main().catch((err) => {
462
+ const json = process.argv.includes('--json');
463
+ if (json) {
464
+ console.log(JSON.stringify({ ok: false, error: err.message, details: err.details ?? [] }, null, 2));
465
+ } else {
466
+ console.error(C.red(`✖ ${err.message}`));
467
+ for (const d of err.details ?? []) console.error(C.red(` • ${d}`));
468
+ if (!(err instanceof BuildError)) console.error(C.gray(err.stack));
469
+ }
470
+ process.exitCode = err instanceof BuildError ? 1 : 2;
471
+ });
package/package.json CHANGED
@@ -1,6 +1,85 @@
1
1
  {
2
2
  "name": "vanilla-builder",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Zero-config bundler, minifier and obfuscator for vanilla HTML, CSS and JavaScript sites. Detects every dependency, verifies the output and never loses a file.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./package.json": "./package.json"
10
+ },
11
+ "bin": {
12
+ "builder": "bin/builder.js"
13
+ },
14
+ "files": [
15
+ "bin/",
16
+ "src/",
17
+ "README.md",
18
+ "README.es.md",
19
+ "CHANGELOG.md",
20
+ "LICENSE"
21
+ ],
22
+ "scripts": {
23
+ "test": "node --test test/unit.test.js test/build.test.js",
24
+ "test:browser": "node --test test/browser.test.js",
25
+ "test:node16": "node test/unit.test.js && node test/build.test.js",
26
+ "check": "node --test test/unit.test.js test/build.test.js test/browser.test.js",
27
+ "prepublishOnly": "npm test",
28
+ "bench": "node bench/run.mjs"
29
+ },
30
+ "keywords": [
31
+ "bundler",
32
+ "minify",
33
+ "obfuscate",
34
+ "obfuscator",
35
+ "vanilla",
36
+ "html",
37
+ "css",
38
+ "javascript",
39
+ "build",
40
+ "esbuild",
41
+ "typescript",
42
+ "obfuscation",
43
+ "minifier"
44
+ ],
45
+ "author": "MarioACo",
46
+ "license": "MIT",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/MarioACo/vanilla-builder.git"
50
+ },
51
+ "homepage": "https://github.com/MarioACo/vanilla-builder#readme",
52
+ "bugs": {
53
+ "url": "https://github.com/MarioACo/vanilla-builder/issues"
54
+ },
55
+ "engines": {
56
+ "node": ">=16"
57
+ },
58
+ "dependencies": {
59
+ "acorn": "8.19.0",
60
+ "acorn-walk": "8.3.5",
61
+ "esbuild": "0.28.2",
62
+ "parse5": "8.0.1",
63
+ "picomatch": "4.0.7"
64
+ },
65
+ "peerDependencies": {
66
+ "javascript-obfuscator": ">=4.1.1 <6",
67
+ "terser": "^5.31.0"
68
+ },
69
+ "peerDependenciesMeta": {
70
+ "javascript-obfuscator": {
71
+ "optional": true
72
+ },
73
+ "terser": {
74
+ "optional": true
75
+ }
76
+ },
77
+ "devDependencies": {
78
+ "javascript-obfuscator": "5.9.0",
79
+ "playwright-core": "1.63.0",
80
+ "terser": "5.51.2"
81
+ },
82
+ "overrides": {
83
+ "zod": "4.6.7"
84
+ }
85
+ }