vanilla-builder 0.1.0 → 0.3.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 CHANGED
@@ -8,21 +8,32 @@ import { runPipeline, VERSION } from '../src/builder.js';
8
8
  import { renderReport } from '../src/report.js';
9
9
  import { startServer } from '../src/server.js';
10
10
  import { watchTree } from '../src/watch.js';
11
- import { BuildError, formatBytes, relFrom, isInside } from '../src/util.js';
11
+ import { BuildError, formatBytes, relFrom, isInside, JS_EXT, extOf, toPosix } from '../src/util.js';
12
+ import { globMatcher } from '../src/files.js';
13
+ import { L, setLang } from '../src/i18n.js';
12
14
 
13
15
  const tty = process.stdout.isTTY && !process.env.NO_COLOR;
14
16
  const color = (code) => (text) => (tty ? `\x1b[${code}m${text}\x1b[0m` : String(text));
15
17
  const C = { red: color(31), yellow: color(33), green: color(32), blue: color(34), gray: color(90), bold: color(1), cyan: color(36) };
16
18
 
17
- const HELP = `Builder ${VERSION} — empaqueta, minifica y ofusca proyectos web vanilla (HTML, CSS y JS)
19
+ const help = () =>
20
+ L(
21
+ `Builder ${VERSION} — empaqueta, minifica y ofusca proyectos web vanilla (HTML, CSS y JS)
18
22
 
19
23
  Uso:
20
24
  builder build [carpeta] [opciones] Compila el proyecto en dist/ (por defecto)
21
25
  builder analyze [carpeta] [--json] Muestra dependencias detectadas y la salida prevista, sin escribir
22
26
  builder init [carpeta] [--force] Crea builder.config.json con lo detectado
27
+ builder deps [carpeta] Lista qué JS son librerías de terceros (no se ofuscan) y cuáles son tuyos
28
+ builder deps add|own|reset <patrón…> Marca librerías (add), tu código (own) o vuelve a lo automático (reset)
23
29
  builder dev [carpeta] [--port N] Compila, vigila cambios y sirve dist/ con recarga automática
24
30
  builder serve [carpeta] [--port N] Sirve la carpeta de salida para probarla
25
31
 
32
+ Marcar dependencias sin configuración:
33
+ En el HTML: <script src="js/lib.js" data-builder="dependency"></script> (o "own")
34
+ En el archivo: /* @builder-dependency */ al principio (o @builder-own)
35
+ Con deps: builder deps add "js/libs/**" (en otra carpeta: --dir <carpeta>)
36
+
26
37
  Opciones de build:
27
38
  --out <carpeta> Carpeta de salida (por defecto dist)
28
39
  --config <archivo> Archivo de configuración (por defecto builder.config.json)
@@ -44,16 +55,100 @@ Opciones de build:
44
55
  --no-merge No une scripts clásicos consecutivos
45
56
  --unreferenced <modo> Archivos no referenciados: process | copy | skip (por defecto process)
46
57
  --base <ruta> Ruta base del sitio para URLs absolutas (por defecto /)
58
+ --workers <n|auto> Hilos para ofuscar/comprimir en paralelo (0 = sin hilos; por defecto auto)
59
+ --no-cache No usa la caché de compilaciones anteriores
47
60
  --no-verify Omite la verificación final (no recomendado)
48
61
  --force Permite vaciar una carpeta de salida que no creó Builder
49
62
  --keep-failed Conserva la carpeta temporal si la verificación falla
50
63
  --json Resultado en JSON (para CI)
51
64
  --verbose Muestra notas, archivos copiados y excluidos
52
65
  --quiet Solo errores
53
- `;
66
+ --budget <patrón=tamaño> Presupuesto de tamaño, ej. "total=1MB gzip" o "js/*.js=50KB" (repetible)
67
+ --images Optimiza PNG (sin pérdida) y JPEG/WebP (calidad 80) con sharp
68
+ --image-quality <1-100> Calidad de JPEG/WebP con --images (por defecto 80)
69
+ --browsers <consulta> Navegadores objetivo (browserslist), ej. "> 0.5%, not dead"; --no-browsers los ignora
70
+ --lang <en|es> Idioma de los mensajes (por defecto: el del sistema; o BUILDER_LANG)
71
+ `,
72
+ `Builder ${VERSION} — bundle, minify and obfuscate vanilla web projects (HTML, CSS and JS)
73
+
74
+ Usage:
75
+ builder build [folder] [options] Build the project into dist/ (default)
76
+ builder analyze [folder] [--json] Show detected dependencies and the planned output, without writing
77
+ builder init [folder] [--force] Create builder.config.json from what was detected
78
+ builder deps [folder] List which JS files are third-party libraries (not obfuscated) and which are yours
79
+ builder deps add|own|reset <pattern…> Mark libraries (add), your code (own) or back to automatic (reset)
80
+ builder dev [folder] [--port N] Build, watch for changes and serve dist/ with live reload
81
+ builder serve [folder] [--port N] Serve the output folder to try it
82
+
83
+ Mark dependencies without configuration:
84
+ In the HTML: <script src="js/lib.js" data-builder="dependency"></script> (or "own")
85
+ In the file: /* @builder-dependency */ at the top (or @builder-own)
86
+ With deps: builder deps add "js/libs/**" (another folder: --dir <folder>)
87
+
88
+ Build options:
89
+ --out <folder> Output folder (default dist)
90
+ --config <file> Configuration file (default builder.config.json)
91
+ --entry <file> Explicit entry (repeatable). Default: every complete .html
92
+ --include <pattern> Always include files (repeatable), e.g. "data/**"
93
+ --ignore <pattern> Exclude files (repeatable), e.g. "drafts/**"
94
+ --obfuscate [level] Obfuscate your JS: low | medium | high | max (default medium)
95
+ Third-party libraries are detected and NOT obfuscated.
96
+ --dep <pattern> Mark files as a dependency by hand (not obfuscated), repeatable
97
+ --not-dep <pattern> Correct the detection: that file is yours and is obfuscated, repeatable
98
+ --obfuscate-deps Obfuscate dependencies too
99
+ --minifier terser Maximum JS compression with a second terser pass
100
+ --compress [gzip|brotli] Also write precompressed .gz and .br files (both by default)
101
+ --hash Hashed JS/CSS file names (long-term caching)
102
+ --sourcemap [mode] Generate source maps (linked | hidden | inline)
103
+ --target <target> Output browsers/ES level, e.g. es2018 (default: your own code's level)
104
+ --drop-console Remove console.* and debugger
105
+ --no-minify Do not minify (useful for debugging)
106
+ --no-merge Do not merge consecutive classic scripts
107
+ --unreferenced <mode> Unreferenced files: process | copy | skip (default process)
108
+ --base <path> Site base path for absolute URLs (default /)
109
+ --workers <n|auto> Threads for obfuscation/compression (0 = none; default auto)
110
+ --no-cache Do not use the cache from previous builds
111
+ --no-verify Skip the final verification (not recommended)
112
+ --force Allow emptying an output folder Builder did not create
113
+ --keep-failed Keep the temporary folder if verification fails
114
+ --json JSON result (for CI)
115
+ --verbose Show notes, copied and excluded files
116
+ --quiet Errors only
117
+ --budget <pattern=size> Size budget, e.g. "total=1MB gzip" or "js/*.js=50KB" (repeatable)
118
+ --images Optimize PNG (lossless) and JPEG/WebP (quality 80) with sharp
119
+ --image-quality <1-100> JPEG/WebP quality with --images (default 80)
120
+ --browsers <query> Target browsers (browserslist), e.g. "> 0.5%, not dead"; --no-browsers ignores them
121
+ --lang <en|es> Message language (default: system language; or BUILDER_LANG)
122
+ `,
123
+ );
124
+
125
+ // Opciones sin valor, y las que admiten la forma --no-x.
126
+ const BOOLEAN = new Set([
127
+ 'help',
128
+ 'version',
129
+ 'json',
130
+ 'verbose',
131
+ 'quiet',
132
+ 'force',
133
+ 'keep-failed',
134
+ 'open',
135
+ 'hash',
136
+ 'obfuscate-deps',
137
+ 'drop-console',
138
+ 'images',
139
+ ]);
140
+ const NEGATABLE = new Set(['verify', 'minify', 'merge', 'cache', 'detect-deps', 'browsers', 'hash', 'images']);
141
+
142
+ function distance(a, b) {
143
+ const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
144
+ for (let j = 1; j <= b.length; j++) d[0][j] = j;
145
+ for (let i = 1; i <= a.length; i++)
146
+ for (let j = 1; j <= b.length; j++) d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
147
+ return d[a.length][b.length];
148
+ }
54
149
 
55
150
  function parseArgs(argv) {
56
- const args = { _: [], entry: [], include: [], ignore: [], dep: [], 'not-dep': [] };
151
+ const args = { _: [], entry: [], include: [], ignore: [], dep: [], 'not-dep': [], budget: [] };
57
152
  const takesValue = new Set([
58
153
  'out',
59
154
  'config',
@@ -68,6 +163,12 @@ function parseArgs(argv) {
68
163
  'dep',
69
164
  'not-dep',
70
165
  'minifier',
166
+ 'workers',
167
+ 'dir',
168
+ 'lang',
169
+ 'browsers',
170
+ 'image-quality',
171
+ 'budget',
71
172
  ]);
72
173
  const optionalValue = { obfuscate: ['low', 'medium', 'high', 'max'], sourcemap: ['linked', 'hidden', 'inline'], compress: ['gzip', 'brotli'] };
73
174
  for (let i = 0; i < argv.length; i++) {
@@ -77,9 +178,20 @@ function parseArgs(argv) {
77
178
  continue;
78
179
  }
79
180
  let [key, val] = a.slice(2).split(/=(.*)/s);
181
+ const bare = key.startsWith('no-') && NEGATABLE.has(key.slice(3)) ? key.slice(3) : key;
182
+ if (!takesValue.has(key) && !optionalValue[key] && !BOOLEAN.has(key) && !(bare !== key && NEGATABLE.has(bare))) {
183
+ const known = [...takesValue, ...Object.keys(optionalValue), ...BOOLEAN, ...[...NEGATABLE].map((k) => `no-${k}`)];
184
+ const near = known.map((k) => [k, distance(key, k)]).sort((x, y) => x[1] - y[1])[0];
185
+ throw new BuildError(
186
+ L(`Opción desconocida --${key}.`, `Unknown option --${key}.`) +
187
+ (near && near[1] <= 3
188
+ ? L(` ¿Quisiste decir --${near[0]}?`, ` Did you mean --${near[0]}?`)
189
+ : L(' Usa "builder help".', ' Use "builder help".')),
190
+ );
191
+ }
80
192
  if (takesValue.has(key)) {
81
193
  if (val === undefined) val = argv[++i];
82
- if (val === undefined) throw new BuildError(`Falta el valor de --${key}.`);
194
+ if (val === undefined) throw new BuildError(L(`Falta el valor de --${key}.`, `Missing value for --${key}.`));
83
195
  if (Array.isArray(args[key])) args[key].push(val);
84
196
  else args[key] = val;
85
197
  } else if (optionalValue[key]) {
@@ -104,6 +216,33 @@ function overridesFrom(args) {
104
216
  if (args.base) o.base = args.base;
105
217
  if (args.verify === false) o.verify = false;
106
218
  if (args.compress !== undefined) o.compress = args.compress;
219
+ if (args.cache === false) o.cache = false;
220
+ if (args['image-quality'] !== undefined) {
221
+ const q = Number(args['image-quality']);
222
+ if (!(Number.isInteger(q) && q >= 1 && q <= 100))
223
+ throw new BuildError(L('--image-quality debe ser un número entre 1 y 100.', '--image-quality must be a number between 1 and 100.'));
224
+ o.images = { quality: q };
225
+ } else if (args.images !== undefined) o.images = !!args.images;
226
+ if (args.budget.length) {
227
+ // --budget "patrón=tamaño" (repetible): sustituye a los presupuestos de la configuración.
228
+ o.budgets = {};
229
+ for (const b of args.budget) {
230
+ const at = b.lastIndexOf('=');
231
+ if (at < 1)
232
+ throw new BuildError(
233
+ L(`--budget necesita la forma patrón=tamaño, ej. "js/*.js=50KB": ${b}`, `--budget needs the form pattern=size, e.g. "js/*.js=50KB": ${b}`),
234
+ );
235
+ o.budgets[b.slice(0, at)] = b.slice(at + 1);
236
+ }
237
+ }
238
+ if (args.browsers === false) o.browsers = false;
239
+ else if (typeof args.browsers === 'string') o.browsers = args.browsers;
240
+ if (args.workers !== undefined) {
241
+ const n = Number(args.workers);
242
+ if (args.workers !== 'auto' && !(Number.isInteger(n) && n >= 0))
243
+ throw new BuildError(L('--workers debe ser "auto" o un número (0 = sin hilos).', '--workers must be "auto" or a number (0 = no threads).'));
244
+ o.workers = args.workers === 'auto' ? 'auto' : n;
245
+ }
107
246
  const deps = {};
108
247
  if (args.dep.length) deps.include = args.dep;
109
248
  if (args['not-dep'].length) deps.exclude = args['not-dep'];
@@ -128,7 +267,12 @@ function printList(title, items, colorFn, limit = 30) {
128
267
  if (!items.length) return;
129
268
  console.log(`\n${colorFn(`${title} (${items.length})`)}`);
130
269
  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)`));
270
+ if (items.length > limit)
271
+ console.log(
272
+ C.gray(
273
+ L(` … y ${items.length - limit} más (usa --json para verlos todos)`, ` … and ${items.length - limit} more (use --json to see them all)`),
274
+ ),
275
+ );
132
276
  }
133
277
 
134
278
  function jsonResult(result) {
@@ -151,6 +295,8 @@ function jsonResult(result) {
151
295
  warnings: result.warnings,
152
296
  notes: result.notes,
153
297
  verify: result.verify && { errors: result.verify.errors, stats: result.verify.stats },
298
+ perf: result.perf,
299
+ budgets: result.budgets,
154
300
  };
155
301
  }
156
302
 
@@ -163,27 +309,74 @@ function printSummary(result, args) {
163
309
  console.log(`\n${C.green('✔')} ${counts} ${C.gray(`(${(result.durationMs / 1000).toFixed(2)} s)`)}`);
164
310
  if (result.verify) {
165
311
  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`);
312
+ console.log(
313
+ L(
314
+ `${C.green('✔')} Verificación: ${s.files} archivos escritos, ${s.refs} referencias comprobadas, ${s.js} JS válidos, 0 errores`,
315
+ `${C.green('✔')} Verification: ${s.files} files written, ${s.refs} references checked, ${s.js} valid JS, 0 errors`,
316
+ ),
317
+ );
318
+ }
319
+ console.log(
320
+ L(
321
+ `${C.green('✔')} Cobertura: ${result.coverage.size}/${result.coverage.size} archivos de origen con destino conocido`,
322
+ `${C.green('✔')} Coverage: ${result.coverage.size}/${result.coverage.size} source files with a known destination`,
323
+ ),
324
+ );
325
+ if (result.budgets?.checked)
326
+ console.log(
327
+ `${C.green('✔')} ${L(`Presupuestos: ${result.budgets.checked} comprobados, todos dentro del límite`, `Budgets: ${result.budgets.checked} checked, all within limits`)}`,
328
+ );
329
+ if (r.compressed)
330
+ console.log(
331
+ L(`${C.green('✔')} ${r.compressed} archivos precomprimidos (.gz/.br)`, `${C.green('✔')} ${r.compressed} precompressed files (.gz/.br)`),
332
+ );
333
+ const p = result.perf;
334
+ if (p?.tasks) {
335
+ const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
336
+ const where = p.threads
337
+ ? L(
338
+ `en el hilo principal y ${plural(p.threads, 'hilo', 'hilos')} más`,
339
+ `on the main thread and ${plural(p.threads, 'more thread', 'more threads')}`,
340
+ )
341
+ : L('en el hilo principal', 'on the main thread');
342
+ const all = p.cacheHits === p.tasks;
343
+ const detail = all
344
+ ? p.tasks === 1
345
+ ? L('reutilizado de la caché', 'reused from the cache')
346
+ : L('todos reutilizados de la caché', 'all reused from the cache')
347
+ : L(`${where}${p.cacheHits ? `, ${p.cacheHits} de la caché` : ''}`, `${where}${p.cacheHits ? `, ${p.cacheHits} from the cache` : ''}`);
348
+ console.log(
349
+ L(
350
+ `${C.green('✔')} Ofuscación/terser: ${plural(p.tasks, 'archivo', 'archivos')}, ${detail}`,
351
+ `${C.green('✔')} Obfuscation/terser: ${plural(p.tasks, 'file', 'files')}, ${detail}`,
352
+ ),
353
+ );
167
354
  }
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
355
  if (result.obfuscated) {
171
356
  const deps = [
172
357
  ...result.dependencies.map((d) => `${d.rel} — ${d.reason}`),
173
- ...result.vendor.map((v) => `${v} — paquete importado (archivo vendor aparte)`),
358
+ ...result.vendor.map((v) => L(`${v} — paquete importado (archivo vendor aparte)`, `${v} — imported package (separate vendor file)`)),
174
359
  ];
175
- if (deps.length) printList('Dependencias detectadas (no se ofuscan)', deps, C.cyan);
360
+ if (deps.length) printList(L('Dependencias detectadas (no se ofuscan)', 'Detected dependencies (not obfuscated)'), deps, C.cyan);
176
361
  }
177
- printList('Avisos', result.warnings, C.yellow);
362
+ printList(L('Avisos', 'Warnings'), result.warnings, C.yellow);
178
363
  if (args.verbose) {
179
- printList('Notas', result.notes, C.cyan, 200);
364
+ printList(L('Notas', 'Notes'), result.notes, C.cyan, 200);
180
365
  printList(
181
- 'Excluidos',
366
+ L('Excluidos', 'Excluded'),
182
367
  result.ignored.map((i) => `${i.rel} — ${i.reason}`),
183
368
  C.gray,
184
369
  200,
185
370
  );
186
- } else if (result.notes.length) console.log(C.gray(`\n${result.notes.length} nota(s) informativas: usa --verbose para verlas.`));
371
+ } else if (result.notes.length)
372
+ console.log(
373
+ C.gray(
374
+ L(
375
+ `\n${result.notes.length} nota(s) informativas: usa --verbose para verlas.`,
376
+ `\n${result.notes.length} informational note(s): use --verbose to see them.`,
377
+ ),
378
+ ),
379
+ );
187
380
  }
188
381
 
189
382
  async function cmdBuild(dir, args) {
@@ -203,49 +396,78 @@ async function cmdAnalyze(dir, args) {
203
396
  console.log(JSON.stringify(jsonResult(result), null, 2));
204
397
  return;
205
398
  }
206
- console.log(`${C.bold(`Builder ${VERSION} — análisis`)} ${cfg.root}\n`);
207
- console.log(`${C.bold('Páginas completas')}: ${result.pages.full.join(', ') || '—'}`);
399
+ console.log(L(`${C.bold(`Builder ${VERSION} — análisis`)} ${cfg.root}\n`, `${C.bold(`Builder ${VERSION} — analysis`)} ${cfg.root}\n`));
400
+ console.log(
401
+ L(
402
+ `${C.bold('Páginas completas')}: ${result.pages.full.join(', ') || '—'}`,
403
+ `${C.bold('Complete pages')}: ${result.pages.full.join(', ') || '—'}`,
404
+ ),
405
+ );
208
406
  if (result.pages.conservative.length)
209
407
  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')}`);
408
+ console.log(L(`\n${C.bold('Qué pasará con cada archivo del proyecto')}`, `\n${C.bold('What will happen to each project file')}`));
211
409
  const rows = [...result.coverage.entries()].sort((a, b) => a[0].localeCompare(b[0]));
212
410
  const w = Math.min(50, Math.max(...rows.map(([k]) => k.length), 10));
213
411
  for (const [rel, list] of rows) {
214
- const shown = args.verbose ? list : list.filter((s) => s !== `copiado → ${rel}`);
412
+ const shown = args.verbose ? list : list.filter((s) => s !== `${L('copiado', 'copied')} → ${rel}`);
215
413
  if (!shown.length) continue;
216
414
  console.log(` ${rel.padEnd(w)} ${C.gray(shown.join(' | '))}`);
217
415
  }
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)`));
416
+ const copied = rows.filter(([rel, list]) => list.length === 1 && list[0] === `${L('copiado', 'copied')} → ${rel}`).length;
417
+ if (copied && !args.verbose)
418
+ console.log(
419
+ C.gray(
420
+ L(
421
+ ` + ${copied} recursos que se copian sin cambios (--verbose para listarlos)`,
422
+ ` + ${copied} assets copied unchanged (--verbose to list them)`,
423
+ ),
424
+ ),
425
+ );
220
426
  const deps = [...result.dependencies.map((d) => `${d.rel} — ${d.reason}`)];
221
- printList('Dependencias de terceros detectadas (no se ofuscan)', deps, C.cyan);
427
+ printList(L('Dependencias de terceros detectadas (no se ofuscan)', 'Detected third-party dependencies (not obfuscated)'), deps, C.cyan);
222
428
  if (result.workers.length)
223
429
  printList(
224
- 'Workers detectados',
225
- result.workers.map((wk) => `${wk.rel} (${wk.kind}${wk.module ? ', módulo' : ''}) desde ${wk.from}`),
430
+ L('Workers detectados', 'Detected workers'),
431
+ result.workers.map((wk) =>
432
+ L(
433
+ `${wk.rel} (${wk.kind}${wk.module ? ', módulo' : ''}) desde ${wk.from}`,
434
+ `${wk.rel} (${wk.kind}${wk.module ? ', module' : ''}) from ${wk.from}`,
435
+ ),
436
+ ),
226
437
  C.cyan,
227
438
  );
228
439
  printList(
229
- 'Referencias rotas en el código fuente',
440
+ L('Referencias rotas en el código fuente', 'Broken references in the source code'),
230
441
  result.broken.map((b) => `${b.from} → ${b.url} (${b.status})`),
231
442
  C.yellow,
232
443
  );
233
- printList('Avisos', result.warnings, C.yellow);
444
+ printList(L('Avisos', 'Warnings'), result.warnings, C.yellow);
234
445
  printList(
235
- 'Excluidos',
446
+ L('Excluidos', 'Excluded'),
236
447
  result.ignored.map((i) => `${i.rel} — ${i.reason}`),
237
448
  C.gray,
238
449
  args.verbose ? 500 : 15,
239
450
  );
240
- if (args.verbose) printList('Notas', result.notes, C.cyan, 500);
451
+ if (args.verbose) printList(L('Notas', 'Notes'), result.notes, C.cyan, 500);
241
452
  const t = result.report.totals;
242
- console.log(`\nSalida prevista: ${t.files} archivos, ${formatBytes(t.before)} → ${formatBytes(t.size)} (sin ofuscar). Nada se escribió.`);
453
+ console.log(
454
+ L(
455
+ `\nSalida prevista: ${t.files} archivos, ${formatBytes(t.before)} → ${formatBytes(t.size)} (sin ofuscar). Nada se escribió.`,
456
+ `\nPlanned output: ${t.files} files, ${formatBytes(t.before)} → ${formatBytes(t.size)} (not obfuscated). Nothing was written.`,
457
+ ),
458
+ );
243
459
  }
244
460
 
245
461
  async function cmdInit(dir, args) {
246
462
  const projectDir = path.resolve(dir);
247
463
  const existing = findConfigFile(projectDir);
248
- if (existing && !args.force) throw new BuildError(`Ya existe ${path.basename(existing)}. Usa --force para reemplazarlo.`);
464
+ if (existing && !args.force)
465
+ throw new BuildError(
466
+ L(
467
+ `Ya existe ${path.basename(existing)}. Usa --force para reemplazarlo.`,
468
+ `${path.basename(existing)} already exists. Use --force to replace it.`,
469
+ ),
470
+ );
249
471
  const cfg = await loadConfig({ dir, config: args.config, overrides: overridesFrom(args) });
250
472
  const result = await runPipeline(cfg, { mode: 'analyze' });
251
473
  const config = {
@@ -264,20 +486,168 @@ async function cmdInit(dir, args) {
264
486
  const file = path.join(projectDir, 'builder.config.json');
265
487
  fs.writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
266
488
  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".`));
489
+ console.log(
490
+ L(
491
+ ` Páginas detectadas: ${result.pages.full.join(', ') || '— (ninguna; define "entries")'}`,
492
+ ` Detected pages: ${result.pages.full.join(', ') || '— (none; set "entries")'}`,
493
+ ),
494
+ );
495
+ if (result.broken.length)
496
+ console.log(
497
+ C.yellow(
498
+ L(
499
+ ` Hay ${result.broken.length} referencia(s) rota(s) en el código; míralas con "builder analyze".`,
500
+ ` There are ${result.broken.length} broken reference(s) in the code; see them with "builder analyze".`,
501
+ ),
502
+ ),
503
+ );
269
504
  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.'));
505
+ console.log(
506
+ L(
507
+ ` Librerías de terceros detectadas (no se ofuscarán): ${result.dependencies.map((d) => d.rel).join(', ')}`,
508
+ ` Detected third-party libraries (will not be obfuscated): ${result.dependencies.map((d) => d.rel).join(', ')}`,
509
+ ),
510
+ );
511
+ console.log(
512
+ C.gray(
513
+ L(
514
+ ' Edita "include" para forzar archivos que solo se cargan desde JS, e "ignore" para excluir.',
515
+ ' Edit "include" to force files that are only loaded from JS, and "ignore" to exclude.',
516
+ ),
517
+ ),
518
+ );
519
+ console.log(
520
+ C.gray(
521
+ L(
522
+ ' En "dependencies.include" marcas a mano librerías; en "dependencies.exclude" corriges una detección.',
523
+ ' In "dependencies.include" you mark libraries by hand; in "dependencies.exclude" you correct a detection.',
524
+ ),
525
+ ),
526
+ );
527
+ }
528
+
529
+ /** builder deps [carpeta] — lista; builder deps add|own|reset <patrón…> — edita builder.config.json. */
530
+ async function cmdDeps(_dir, args) {
531
+ const sub = args._[1];
532
+ if (['add', 'own', 'reset'].includes(sub)) return editDeps(sub, args._.slice(2), args);
533
+ const dir = args.dir ?? sub ?? '.';
534
+ const cfg = await loadConfig({ dir, config: args.config, overrides: overridesFrom(args) });
535
+ const result = await runPipeline(cfg, { mode: 'analyze' });
536
+ const deps = new Map(result.dependencies.map((d) => [d.rel, d.reason]));
537
+ const own = [...result.coverage.keys()].filter((rel) => JS_EXT.has(extOf(rel)) && !deps.has(rel)).sort();
538
+ if (args.json) {
539
+ console.log(JSON.stringify({ dependencies: result.dependencies, own }, null, 2));
540
+ return;
541
+ }
542
+ console.log(`${C.bold(`Builder ${VERSION} — ${L('dependencias', 'dependencies')}`)} ${cfg.root}`);
543
+ printList(
544
+ L('Librerías de terceros (no se ofuscan)', 'Third-party libraries (not obfuscated)'),
545
+ [...deps].map(([rel, why]) => `${rel} ${C.gray(`— ${why}`)}`),
546
+ C.cyan,
547
+ 200,
548
+ );
549
+ printList(L('Tu código (se ofusca con --obfuscate)', 'Your code (obfuscated with --obfuscate)'), own, C.green, 200);
550
+ if (!deps.size && !own.length) console.log(L('\nNo hay archivos JavaScript en el proyecto.', '\nThere are no JavaScript files in the project.'));
551
+ console.log(C.gray(L('\n¿Algo mal clasificado?', '\nSomething classified wrong?')));
552
+ console.log(
553
+ C.gray(L(' builder deps add <patrón> es una librería: no se ofusca', ' builder deps add <pattern> it is a library: not obfuscated')),
554
+ );
555
+ console.log(
556
+ C.gray(L(' builder deps own <patrón> es tu código: sí se ofusca', ' builder deps own <pattern> it is your code: obfuscated')),
557
+ );
558
+ console.log(
559
+ C.gray(L(' builder deps reset <patrón> vuelve a la detección automática', ' builder deps reset <pattern> back to automatic detection')),
560
+ );
561
+ console.log(
562
+ C.gray(
563
+ L(
564
+ ' También: data-builder="dependency" en el <script>, o /* @builder-dependency */ al inicio del archivo.',
565
+ ' Also: data-builder="dependency" on the <script>, or /* @builder-dependency */ at the top of the file.',
566
+ ),
567
+ ),
568
+ );
569
+ }
570
+
571
+ async function editDeps(sub, rawPatterns, args) {
572
+ if (!rawPatterns.length)
573
+ throw new BuildError(
574
+ L(
575
+ `Indica uno o más archivos o patrones: builder deps ${sub} "js/vendor/**"`,
576
+ `Give one or more files or patterns: builder deps ${sub} "js/vendor/**"`,
577
+ ),
578
+ );
579
+ const projectDir = path.resolve(args.dir ?? '.');
580
+ const file = args.config ? path.resolve(args.config) : (findConfigFile(projectDir) ?? path.join(projectDir, 'builder.config.json'));
581
+ if (!file.endsWith('.json'))
582
+ throw new BuildError(
583
+ L(
584
+ `La configuración está en ${path.basename(file)} (JavaScript); añade los patrones a mano en "dependencies".`,
585
+ `The configuration is in ${path.basename(file)} (JavaScript); add the patterns to "dependencies" by hand.`,
586
+ ),
587
+ );
588
+ const config = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8').replace(/^/, '')) : {};
589
+ if (config.dependencies === false || config.dependencies === true || config.dependencies == null) config.dependencies = {};
590
+ const d = config.dependencies;
591
+ const include = new Set(d.include ?? []);
592
+ const exclude = new Set(d.exclude ?? []);
593
+ const patterns = rawPatterns.map((p) => toPosix(p).replace(/^\.\//, ''));
594
+ for (const p of patterns) {
595
+ if (sub === 'add') {
596
+ include.add(p);
597
+ exclude.delete(p);
598
+ } else if (sub === 'own') {
599
+ exclude.add(p);
600
+ include.delete(p);
601
+ } else {
602
+ include.delete(p);
603
+ exclude.delete(p);
604
+ }
605
+ }
606
+ d.include = [...include];
607
+ d.exclude = [...exclude];
608
+ // Se valida antes de escribir: nunca deja un builder.config.json roto.
609
+ const cfg = await loadConfig({ dir: projectDir, config: args.config, overrides: { dependencies: { include: d.include, exclude: d.exclude } } });
610
+ const existed = fs.existsSync(file);
611
+ fs.writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
612
+ const result = await runPipeline(cfg, { mode: 'analyze' });
613
+ const deps = new Map(result.dependencies.map((x) => [x.rel, x.reason]));
614
+ const jsFiles = [...result.coverage.keys()].filter((rel) => JS_EXT.has(extOf(rel)));
615
+ console.log(`${C.green('✔')} ${existed ? 'Actualizado' : 'Creado'} ${file}`);
616
+ for (const p of patterns) {
617
+ const hits = jsFiles.filter(globMatcher([p]));
618
+ if (!hits.length) {
619
+ console.log(
620
+ C.yellow(
621
+ L(
622
+ ` ! "${p}" no coincide con ningún archivo JavaScript del proyecto (queda guardado igualmente)`,
623
+ ` ! "${p}" does not match any JavaScript file in the project (saved anyway)`,
624
+ ),
625
+ ),
626
+ );
627
+ continue;
628
+ }
629
+ for (const rel of hits)
630
+ console.log(
631
+ L(
632
+ ` ${rel} ${deps.has(rel) ? C.cyan(`librería — ${deps.get(rel)}`) : C.green('tu código — se ofusca')}`,
633
+ ` ${rel} ${deps.has(rel) ? C.cyan(`library — ${deps.get(rel)}`) : C.green('your code — obfuscated')}`,
634
+ ),
635
+ );
636
+ }
273
637
  }
274
638
 
275
639
  async function cmdServe(dir, args) {
276
640
  const cfg = await loadConfig({ dir, config: args.config, overrides: overridesFrom(args) });
277
641
  const target = fs.existsSync(cfg.outDir) ? cfg.outDir : null;
278
- if (!target) throw new BuildError(`No existe ${cfg.outDir}. Ejecuta primero "builder build".`);
642
+ if (!target)
643
+ throw new BuildError(L(`No existe ${cfg.outDir}. Ejecuta primero "builder build".`, `${cfg.outDir} does not exist. Run "builder build" first.`));
279
644
  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)')}`);
645
+ console.log(
646
+ L(
647
+ `${C.green('✔')} Sirviendo ${target} en ${C.bold(srv.url)} ${C.gray('(Ctrl+C para salir)')}`,
648
+ `${C.green('✔')} Serving ${target} at ${C.bold(srv.url)} ${C.gray('(Ctrl+C to quit)')}`,
649
+ ),
650
+ );
281
651
  if (args.open) openBrowser(srv.url);
282
652
  }
283
653
 
@@ -286,9 +656,12 @@ async function cmdDev(dir, args) {
286
656
  let srv = null;
287
657
  let running = false;
288
658
  let pending = false;
289
- const rebuild = async (reason) => {
659
+ // files: archivos que cambiaron; si todos son CSS (y sin hash), basta con recargar las hojas de estilos.
660
+ let pendingFiles = [];
661
+ const rebuild = async (reason, files = null) => {
290
662
  if (running) {
291
663
  pending = true;
664
+ pendingFiles = files && pendingFiles ? [...pendingFiles, ...files] : null;
292
665
  return;
293
666
  }
294
667
  running = true;
@@ -297,11 +670,13 @@ async function cmdDev(dir, args) {
297
670
  const fresh = await loadConfig({ dir, config: args.config, overrides: overridesFrom(args) });
298
671
  const result = await runPipeline(fresh, { mode: 'build', force: !!args.force });
299
672
  console.log(
300
- `${C.green('✔')} ${reason ? `${reason} → ` : ''}compilado en ${Date.now() - t} ms${result.warnings.length ? C.yellow(` (${result.warnings.length} avisos)`) : ''}`,
673
+ `${C.green('✔')} ${reason ? `${reason} → ` : ''}${L('compilado en', 'built in')} ${Date.now() - t} ms${result.warnings.length ? C.yellow(L(` (${result.warnings.length} avisos)`, ` (${result.warnings.length} warnings)`)) : ''}`,
301
674
  );
302
675
  for (const w of result.warnings.slice(0, 5)) console.log(C.yellow(` • ${w}`));
303
676
  srv?.buildOk();
304
- srv?.reload();
677
+ const cssOnly = files?.length && !fresh.hash && files.every((f) => /\.css$/i.test(f));
678
+ if (cssOnly) srv?.reloadCss();
679
+ else srv?.reload();
305
680
  } catch (err) {
306
681
  console.error(C.red(`✖ ${err.message}`));
307
682
  for (const d of err.details ?? []) console.error(C.red(` • ${d}`));
@@ -310,14 +685,21 @@ async function cmdDev(dir, args) {
310
685
  running = false;
311
686
  if (pending) {
312
687
  pending = false;
313
- rebuild('cambios pendientes');
688
+ const next = pendingFiles;
689
+ pendingFiles = [];
690
+ rebuild(L('cambios pendientes', 'pending changes'), next);
314
691
  }
315
692
  }
316
693
  };
317
694
  await rebuild();
318
695
  fs.mkdirSync(cfg.outDir, { recursive: true });
319
696
  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)')}`);
697
+ console.log(
698
+ L(
699
+ `${C.green('✔')} Sirviendo ${cfg.outDir} en ${C.bold(srv.url)} con recarga automática ${C.gray('(Ctrl+C para salir)')}`,
700
+ `${C.green('✔')} Serving ${cfg.outDir} at ${C.bold(srv.url)} with live reload ${C.gray('(Ctrl+C to quit)')}`,
701
+ ),
702
+ );
321
703
  if (args.open) openBrowser(srv.url);
322
704
  let timer = null;
323
705
  const changed = new Set();
@@ -332,7 +714,7 @@ async function cmdDev(dir, args) {
332
714
  timer = setTimeout(() => {
333
715
  const list = [...changed];
334
716
  changed.clear();
335
- rebuild(list.length === 1 ? list[0] : `${list.length} archivos`);
717
+ rebuild(list.length === 1 ? list[0] : L(`${list.length} archivos`, `${list.length} files`), list);
336
718
  }, 150);
337
719
  });
338
720
  }
@@ -345,12 +727,16 @@ function openBrowser(url) {
345
727
 
346
728
  async function main() {
347
729
  const argv = process.argv.slice(2);
730
+ // El idioma se fija antes que nada para que todos los mensajes salgan en él.
731
+ const langAt = argv.findIndex((a) => a === '--lang' || a.startsWith('--lang='));
732
+ if (langAt > -1) setLang(argv[langAt].includes('=') ? argv[langAt].split('=')[1] : argv[langAt + 1]);
348
733
  const args = parseArgs(argv);
349
734
  if (args.version || args.v) return console.log(VERSION);
350
735
  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".`);
736
+ if (args.help || args.h || command === 'help') return console.log(help());
737
+ const commands = { build: cmdBuild, analyze: cmdAnalyze, init: cmdInit, serve: cmdServe, dev: cmdDev, deps: cmdDeps };
738
+ if (!commands[command])
739
+ throw new BuildError(L(`Comando desconocido "${command}". Usa "builder help".`, `Unknown command "${command}". Use "builder help".`));
354
740
  await commands[command](dir, args);
355
741
  }
356
742