vanilla-builder 0.0.0-stage → 0.1.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,367 @@
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 } from '../src/util.js';
12
+
13
+ const tty = process.stdout.isTTY && !process.env.NO_COLOR;
14
+ const color = (code) => (text) => (tty ? `\x1b[${code}m${text}\x1b[0m` : String(text));
15
+ const C = { red: color(31), yellow: color(33), green: color(32), blue: color(34), gray: color(90), bold: color(1), cyan: color(36) };
16
+
17
+ const HELP = `Builder ${VERSION} — empaqueta, minifica y ofusca proyectos web vanilla (HTML, CSS y JS)
18
+
19
+ Uso:
20
+ builder build [carpeta] [opciones] Compila el proyecto en dist/ (por defecto)
21
+ builder analyze [carpeta] [--json] Muestra dependencias detectadas y la salida prevista, sin escribir
22
+ builder init [carpeta] [--force] Crea builder.config.json con lo detectado
23
+ builder dev [carpeta] [--port N] Compila, vigila cambios y sirve dist/ con recarga automática
24
+ builder serve [carpeta] [--port N] Sirve la carpeta de salida para probarla
25
+
26
+ Opciones de build:
27
+ --out <carpeta> Carpeta de salida (por defecto dist)
28
+ --config <archivo> Archivo de configuración (por defecto builder.config.json)
29
+ --entry <archivo> Entrada explícita (repetible). Por defecto: todos los .html completos
30
+ --include <patrón> Fuerza incluir archivos (repetible), ej. "data/**"
31
+ --ignore <patrón> Excluye archivos (repetible), ej. "borradores/**"
32
+ --obfuscate [nivel] Ofusca tu JS: low | medium | high | max (por defecto medium)
33
+ Las librerías de terceros se detectan solas y NO se ofuscan.
34
+ --dep <patrón> Marca a mano archivos como dependencia (no se ofuscan), repetible
35
+ --not-dep <patrón> Corrige la detección: ese archivo es tuyo y sí se ofusca, repetible
36
+ --obfuscate-deps Ofusca también las dependencias
37
+ --minifier terser Compresión máxima del JS con una segunda pasada de terser
38
+ --compress [gzip|brotli] Genera además .gz y .br precomprimidos (por defecto ambos)
39
+ --hash Nombres con hash en JS/CSS (caché a largo plazo)
40
+ --sourcemap [modo] Genera source maps (linked | hidden | inline)
41
+ --target <target> Navegadores/ES de salida, ej. es2018 (por defecto: el del propio código)
42
+ --drop-console Quita console.* y debugger
43
+ --no-minify No minifica (útil para depurar)
44
+ --no-merge No une scripts clásicos consecutivos
45
+ --unreferenced <modo> Archivos no referenciados: process | copy | skip (por defecto process)
46
+ --base <ruta> Ruta base del sitio para URLs absolutas (por defecto /)
47
+ --no-verify Omite la verificación final (no recomendado)
48
+ --force Permite vaciar una carpeta de salida que no creó Builder
49
+ --keep-failed Conserva la carpeta temporal si la verificación falla
50
+ --json Resultado en JSON (para CI)
51
+ --verbose Muestra notas, archivos copiados y excluidos
52
+ --quiet Solo errores
53
+ `;
54
+
55
+ function parseArgs(argv) {
56
+ const args = { _: [], entry: [], include: [], ignore: [], dep: [], 'not-dep': [] };
57
+ const takesValue = new Set([
58
+ 'out',
59
+ 'config',
60
+ 'entry',
61
+ 'include',
62
+ 'ignore',
63
+ 'target',
64
+ 'unreferenced',
65
+ 'base',
66
+ 'port',
67
+ 'host',
68
+ 'dep',
69
+ 'not-dep',
70
+ 'minifier',
71
+ ]);
72
+ const optionalValue = { obfuscate: ['low', 'medium', 'high', 'max'], sourcemap: ['linked', 'hidden', 'inline'], compress: ['gzip', 'brotli'] };
73
+ for (let i = 0; i < argv.length; i++) {
74
+ const a = argv[i];
75
+ if (!a.startsWith('--')) {
76
+ args._.push(a);
77
+ continue;
78
+ }
79
+ let [key, val] = a.slice(2).split(/=(.*)/s);
80
+ if (takesValue.has(key)) {
81
+ if (val === undefined) val = argv[++i];
82
+ if (val === undefined) throw new BuildError(`Falta el valor de --${key}.`);
83
+ if (Array.isArray(args[key])) args[key].push(val);
84
+ else args[key] = val;
85
+ } else if (optionalValue[key]) {
86
+ if (val === undefined && optionalValue[key].includes(argv[i + 1])) val = argv[++i];
87
+ args[key] = val ?? true;
88
+ } else if (key.startsWith('no-')) args[key.slice(3)] = false;
89
+ else args[key] = val ?? true;
90
+ }
91
+ return args;
92
+ }
93
+
94
+ function overridesFrom(args) {
95
+ const o = {};
96
+ if (args.out) o.outDir = args.out;
97
+ if (args.entry.length) o.entries = args.entry;
98
+ if (args.include.length) o.include = args.include;
99
+ if (args.ignore.length) o.ignore = args.ignore;
100
+ if (args.obfuscate !== undefined) o.obfuscate = args.obfuscate === true ? 'medium' : args.obfuscate;
101
+ if (args.hash !== undefined) o.hash = !!args.hash;
102
+ if (args.sourcemap !== undefined) o.sourcemap = args.sourcemap === 'linked' ? true : args.sourcemap;
103
+ if (args.unreferenced) o.unreferenced = args.unreferenced;
104
+ if (args.base) o.base = args.base;
105
+ if (args.verify === false) o.verify = false;
106
+ if (args.compress !== undefined) o.compress = args.compress;
107
+ const deps = {};
108
+ if (args.dep.length) deps.include = args.dep;
109
+ if (args['not-dep'].length) deps.exclude = args['not-dep'];
110
+ if (args['obfuscate-deps']) deps.obfuscate = true;
111
+ if (args['detect-deps'] === false) deps.detect = false;
112
+ if (Object.keys(deps).length) o.dependencies = deps;
113
+ const js = {};
114
+ if (args.target) js.target = args.target;
115
+ if (args['drop-console']) js.drop = ['console', 'debugger'];
116
+ if (args.merge === false) js.mergeClassic = false;
117
+ if (args.minifier) js.minifier = args.minifier;
118
+ if (args.minify === false) {
119
+ js.minify = false;
120
+ o.css = { minify: false };
121
+ o.html = { minify: false };
122
+ }
123
+ if (Object.keys(js).length) o.js = js;
124
+ return o;
125
+ }
126
+
127
+ function printList(title, items, colorFn, limit = 30) {
128
+ if (!items.length) return;
129
+ console.log(`\n${colorFn(`${title} (${items.length})`)}`);
130
+ for (const i of items.slice(0, limit)) console.log(` • ${i}`);
131
+ if (items.length > limit) console.log(C.gray(` … y ${items.length - limit} más (usa --json para verlos todos)`));
132
+ }
133
+
134
+ function jsonResult(result) {
135
+ return {
136
+ ok: true,
137
+ version: result.version,
138
+ root: result.root,
139
+ outDir: result.outDir,
140
+ durationMs: result.durationMs,
141
+ pages: result.pages,
142
+ outputs: result.report.rows,
143
+ totals: result.report.totals,
144
+ coverage: Object.fromEntries(result.coverage),
145
+ ignored: result.ignored,
146
+ broken: result.broken,
147
+ workers: result.workers,
148
+ standalone: result.standalone,
149
+ dependencies: result.dependencies,
150
+ vendor: result.vendor,
151
+ warnings: result.warnings,
152
+ notes: result.notes,
153
+ verify: result.verify && { errors: result.verify.errors, stats: result.verify.stats },
154
+ };
155
+ }
156
+
157
+ function printSummary(result, args) {
158
+ const r = result.report;
159
+ const counts = Object.entries(r.byType)
160
+ .map(([t, v]) => `${v.files} ${t}`)
161
+ .join(' · ');
162
+ console.log(renderReport(r, C, { verbose: args.verbose }));
163
+ console.log(`\n${C.green('✔')} ${counts} ${C.gray(`(${(result.durationMs / 1000).toFixed(2)} s)`)}`);
164
+ if (result.verify) {
165
+ const s = result.verify.stats;
166
+ console.log(`${C.green('✔')} Verificación: ${s.files} archivos escritos, ${s.refs} referencias comprobadas, ${s.js} JS válidos, 0 errores`);
167
+ }
168
+ console.log(`${C.green('✔')} Cobertura: ${result.coverage.size}/${result.coverage.size} archivos de origen con destino conocido`);
169
+ if (r.compressed) console.log(`${C.green('✔')} ${r.compressed} archivos precomprimidos (.gz/.br)`);
170
+ if (result.obfuscated) {
171
+ const deps = [
172
+ ...result.dependencies.map((d) => `${d.rel} — ${d.reason}`),
173
+ ...result.vendor.map((v) => `${v} — paquete importado (archivo vendor aparte)`),
174
+ ];
175
+ if (deps.length) printList('Dependencias detectadas (no se ofuscan)', deps, C.cyan);
176
+ }
177
+ printList('Avisos', result.warnings, C.yellow);
178
+ if (args.verbose) {
179
+ printList('Notas', result.notes, C.cyan, 200);
180
+ printList(
181
+ 'Excluidos',
182
+ result.ignored.map((i) => `${i.rel} — ${i.reason}`),
183
+ C.gray,
184
+ 200,
185
+ );
186
+ } else if (result.notes.length) console.log(C.gray(`\n${result.notes.length} nota(s) informativas: usa --verbose para verlas.`));
187
+ }
188
+
189
+ async function cmdBuild(dir, args) {
190
+ const cfg = await loadConfig({ dir, config: args.config, overrides: overridesFrom(args) });
191
+ const log = args.quiet || args.json ? () => {} : (m) => console.log(C.gray(` ${m}`));
192
+ if (!args.quiet && !args.json) console.log(`${C.bold(`Builder ${VERSION}`)} ${cfg.root} ${C.gray('→')} ${cfg.outDir}`);
193
+ const result = await runPipeline(cfg, { mode: 'build', force: !!args.force, keepFailed: !!args['keep-failed'], log });
194
+ if (args.json) console.log(JSON.stringify(jsonResult(result), null, 2));
195
+ else if (!args.quiet) printSummary(result, args);
196
+ return result;
197
+ }
198
+
199
+ async function cmdAnalyze(dir, args) {
200
+ const cfg = await loadConfig({ dir, config: args.config, overrides: overridesFrom(args) });
201
+ const result = await runPipeline(cfg, { mode: 'analyze' });
202
+ if (args.json) {
203
+ console.log(JSON.stringify(jsonResult(result), null, 2));
204
+ return;
205
+ }
206
+ console.log(`${C.bold(`Builder ${VERSION} — análisis`)} ${cfg.root}\n`);
207
+ console.log(`${C.bold('Páginas completas')}: ${result.pages.full.join(', ') || '—'}`);
208
+ if (result.pages.conservative.length)
209
+ console.log(`${C.bold('Modo conservador')} (fragmentos u otras codificaciones): ${result.pages.conservative.join(', ')}`);
210
+ console.log(`\n${C.bold('Qué pasará con cada archivo del proyecto')}`);
211
+ const rows = [...result.coverage.entries()].sort((a, b) => a[0].localeCompare(b[0]));
212
+ const w = Math.min(50, Math.max(...rows.map(([k]) => k.length), 10));
213
+ for (const [rel, list] of rows) {
214
+ const shown = args.verbose ? list : list.filter((s) => s !== `copiado → ${rel}`);
215
+ if (!shown.length) continue;
216
+ console.log(` ${rel.padEnd(w)} ${C.gray(shown.join(' | '))}`);
217
+ }
218
+ const copied = rows.filter(([rel, list]) => list.length === 1 && list[0] === `copiado → ${rel}`).length;
219
+ if (copied && !args.verbose) console.log(C.gray(` + ${copied} recursos que se copian sin cambios (--verbose para listarlos)`));
220
+ const deps = [...result.dependencies.map((d) => `${d.rel} — ${d.reason}`)];
221
+ printList('Dependencias de terceros detectadas (no se ofuscan)', deps, C.cyan);
222
+ if (result.workers.length)
223
+ printList(
224
+ 'Workers detectados',
225
+ result.workers.map((wk) => `${wk.rel} (${wk.kind}${wk.module ? ', módulo' : ''}) desde ${wk.from}`),
226
+ C.cyan,
227
+ );
228
+ printList(
229
+ 'Referencias rotas en el código fuente',
230
+ result.broken.map((b) => `${b.from} → ${b.url} (${b.status})`),
231
+ C.yellow,
232
+ );
233
+ printList('Avisos', result.warnings, C.yellow);
234
+ printList(
235
+ 'Excluidos',
236
+ result.ignored.map((i) => `${i.rel} — ${i.reason}`),
237
+ C.gray,
238
+ args.verbose ? 500 : 15,
239
+ );
240
+ if (args.verbose) printList('Notas', result.notes, C.cyan, 500);
241
+ const t = result.report.totals;
242
+ console.log(`\nSalida prevista: ${t.files} archivos, ${formatBytes(t.before)} → ${formatBytes(t.size)} (sin ofuscar). Nada se escribió.`);
243
+ }
244
+
245
+ async function cmdInit(dir, args) {
246
+ const projectDir = path.resolve(dir);
247
+ const existing = findConfigFile(projectDir);
248
+ if (existing && !args.force) throw new BuildError(`Ya existe ${path.basename(existing)}. Usa --force para reemplazarlo.`);
249
+ const cfg = await loadConfig({ dir, config: args.config, overrides: overridesFrom(args) });
250
+ const result = await runPipeline(cfg, { mode: 'analyze' });
251
+ const config = {
252
+ outDir: relFrom(cfg.root, cfg.outDir) ?? cfg.outDir,
253
+ entries: 'auto',
254
+ include: [],
255
+ ignore: [],
256
+ unreferenced: 'process',
257
+ obfuscate: false,
258
+ dependencies: { detect: true, obfuscate: false, include: [], exclude: [] },
259
+ hash: false,
260
+ sourcemap: false,
261
+ compress: false,
262
+ js: { target: 'auto', minifier: 'esbuild', drop: [] },
263
+ };
264
+ const file = path.join(projectDir, 'builder.config.json');
265
+ fs.writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
266
+ console.log(`${C.green('✔')} Creado ${file}`);
267
+ console.log(` Páginas detectadas: ${result.pages.full.join(', ') || '— (ninguna; define "entries")'}`);
268
+ 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".`));
269
+ if (result.dependencies.length)
270
+ console.log(` Librerías de terceros detectadas (no se ofuscarán): ${result.dependencies.map((d) => d.rel).join(', ')}`);
271
+ console.log(C.gray(' Edita "include" para forzar archivos que solo se cargan desde JS, e "ignore" para excluir.'));
272
+ console.log(C.gray(' En "dependencies.include" marcas a mano librerías; en "dependencies.exclude" corriges una detección.'));
273
+ }
274
+
275
+ async function cmdServe(dir, args) {
276
+ const cfg = await loadConfig({ dir, config: args.config, overrides: overridesFrom(args) });
277
+ const target = fs.existsSync(cfg.outDir) ? cfg.outDir : null;
278
+ if (!target) throw new BuildError(`No existe ${cfg.outDir}. Ejecuta primero "builder build".`);
279
+ const srv = await startServer({ dir: target, port: Number(args.port ?? 5173), host: args.host ?? '127.0.0.1' });
280
+ console.log(`${C.green('✔')} Sirviendo ${target} en ${C.bold(srv.url)} ${C.gray('(Ctrl+C para salir)')}`);
281
+ if (args.open) openBrowser(srv.url);
282
+ }
283
+
284
+ async function cmdDev(dir, args) {
285
+ const cfg = await loadConfig({ dir, config: args.config, overrides: overridesFrom(args) });
286
+ let srv = null;
287
+ let running = false;
288
+ let pending = false;
289
+ const rebuild = async (reason) => {
290
+ if (running) {
291
+ pending = true;
292
+ return;
293
+ }
294
+ running = true;
295
+ const t = Date.now();
296
+ try {
297
+ const fresh = await loadConfig({ dir, config: args.config, overrides: overridesFrom(args) });
298
+ const result = await runPipeline(fresh, { mode: 'build', force: !!args.force });
299
+ console.log(
300
+ `${C.green('✔')} ${reason ? `${reason} → ` : ''}compilado en ${Date.now() - t} ms${result.warnings.length ? C.yellow(` (${result.warnings.length} avisos)`) : ''}`,
301
+ );
302
+ for (const w of result.warnings.slice(0, 5)) console.log(C.yellow(` • ${w}`));
303
+ srv?.buildOk();
304
+ srv?.reload();
305
+ } catch (err) {
306
+ console.error(C.red(`✖ ${err.message}`));
307
+ for (const d of err.details ?? []) console.error(C.red(` • ${d}`));
308
+ srv?.buildError([err.message, ...(err.details ?? [])].join('\n'));
309
+ } finally {
310
+ running = false;
311
+ if (pending) {
312
+ pending = false;
313
+ rebuild('cambios pendientes');
314
+ }
315
+ }
316
+ };
317
+ await rebuild();
318
+ fs.mkdirSync(cfg.outDir, { recursive: true });
319
+ srv = await startServer({ dir: cfg.outDir, port: Number(args.port ?? 5173), host: args.host ?? '127.0.0.1', liveReload: true });
320
+ console.log(`${C.green('✔')} Sirviendo ${cfg.outDir} en ${C.bold(srv.url)} con recarga automática ${C.gray('(Ctrl+C para salir)')}`);
321
+ if (args.open) openBrowser(srv.url);
322
+ let timer = null;
323
+ const changed = new Set();
324
+ const outDir = path.resolve(cfg.outDir);
325
+ const ignore = (rel) => {
326
+ const abs = path.join(cfg.root, rel);
327
+ return abs === outDir || isInside(outDir, abs) || /(^|\/)(node_modules|\.git)(\/|$)|\.builder-(tmp|old)-/.test(rel);
328
+ };
329
+ watchTree(cfg.root, ignore, (rel) => {
330
+ changed.add(rel);
331
+ clearTimeout(timer);
332
+ timer = setTimeout(() => {
333
+ const list = [...changed];
334
+ changed.clear();
335
+ rebuild(list.length === 1 ? list[0] : `${list.length} archivos`);
336
+ }, 150);
337
+ });
338
+ }
339
+
340
+ function openBrowser(url) {
341
+ const cmd = process.platform === 'win32' ? 'cmd' : process.platform === 'darwin' ? 'open' : 'xdg-open';
342
+ const argv = process.platform === 'win32' ? ['/c', 'start', '""', url] : [url];
343
+ spawn(cmd, argv, { detached: true, stdio: 'ignore' }).unref();
344
+ }
345
+
346
+ async function main() {
347
+ const argv = process.argv.slice(2);
348
+ const args = parseArgs(argv);
349
+ if (args.version || args.v) return console.log(VERSION);
350
+ const [command = 'help', dir = '.'] = args._;
351
+ if (args.help || args.h || command === 'help') return console.log(HELP);
352
+ const commands = { build: cmdBuild, analyze: cmdAnalyze, init: cmdInit, serve: cmdServe, dev: cmdDev };
353
+ if (!commands[command]) throw new BuildError(`Comando desconocido "${command}". Usa "builder help".`);
354
+ await commands[command](dir, args);
355
+ }
356
+
357
+ main().catch((err) => {
358
+ const json = process.argv.includes('--json');
359
+ if (json) {
360
+ console.log(JSON.stringify({ ok: false, error: err.message, details: err.details ?? [] }, null, 2));
361
+ } else {
362
+ console.error(C.red(`✖ ${err.message}`));
363
+ for (const d of err.details ?? []) console.error(C.red(` • ${d}`));
364
+ if (!(err instanceof BuildError)) console.error(C.gray(err.stack));
365
+ }
366
+ process.exitCode = err instanceof BuildError ? 1 : 2;
367
+ });
package/package.json CHANGED
@@ -1,6 +1,81 @@
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.1.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
+ },
29
+ "keywords": [
30
+ "bundler",
31
+ "minify",
32
+ "obfuscate",
33
+ "obfuscator",
34
+ "vanilla",
35
+ "html",
36
+ "css",
37
+ "javascript",
38
+ "build",
39
+ "esbuild"
40
+ ],
41
+ "author": "MarioACo",
42
+ "license": "MIT",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/MarioACo/vanilla-builder.git"
46
+ },
47
+ "homepage": "https://github.com/MarioACo/vanilla-builder#readme",
48
+ "bugs": {
49
+ "url": "https://github.com/MarioACo/vanilla-builder/issues"
50
+ },
51
+ "engines": {
52
+ "node": ">=16"
53
+ },
54
+ "dependencies": {
55
+ "acorn": "8.19.0",
56
+ "acorn-walk": "8.3.5",
57
+ "esbuild": "0.28.2",
58
+ "parse5": "8.0.1",
59
+ "picomatch": "4.0.7"
60
+ },
61
+ "peerDependencies": {
62
+ "javascript-obfuscator": ">=4.1.1 <6",
63
+ "terser": "^5.31.0"
64
+ },
65
+ "peerDependenciesMeta": {
66
+ "javascript-obfuscator": {
67
+ "optional": true
68
+ },
69
+ "terser": {
70
+ "optional": true
71
+ }
72
+ },
73
+ "devDependencies": {
74
+ "javascript-obfuscator": "5.9.0",
75
+ "playwright-core": "1.63.0",
76
+ "terser": "5.51.2"
77
+ },
78
+ "overrides": {
79
+ "zod": "4.6.7"
80
+ }
81
+ }