vanilla-builder 0.2.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
@@ -10,12 +10,15 @@ import { startServer } from '../src/server.js';
10
10
  import { watchTree } from '../src/watch.js';
11
11
  import { BuildError, formatBytes, relFrom, isInside, JS_EXT, extOf, toPosix } from '../src/util.js';
12
12
  import { globMatcher } from '../src/files.js';
13
+ import { L, setLang } from '../src/i18n.js';
13
14
 
14
15
  const tty = process.stdout.isTTY && !process.env.NO_COLOR;
15
16
  const color = (code) => (text) => (tty ? `\x1b[${code}m${text}\x1b[0m` : String(text));
16
17
  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
 
18
- 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)
19
22
 
20
23
  Uso:
21
24
  builder build [carpeta] [opciones] Compila el proyecto en dist/ (por defecto)
@@ -60,10 +63,92 @@ Opciones de build:
60
63
  --json Resultado en JSON (para CI)
61
64
  --verbose Muestra notas, archivos copiados y excluidos
62
65
  --quiet Solo errores
63
- `;
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
+ }
64
149
 
65
150
  function parseArgs(argv) {
66
- const args = { _: [], entry: [], include: [], ignore: [], dep: [], 'not-dep': [] };
151
+ const args = { _: [], entry: [], include: [], ignore: [], dep: [], 'not-dep': [], budget: [] };
67
152
  const takesValue = new Set([
68
153
  'out',
69
154
  'config',
@@ -80,6 +165,10 @@ function parseArgs(argv) {
80
165
  'minifier',
81
166
  'workers',
82
167
  'dir',
168
+ 'lang',
169
+ 'browsers',
170
+ 'image-quality',
171
+ 'budget',
83
172
  ]);
84
173
  const optionalValue = { obfuscate: ['low', 'medium', 'high', 'max'], sourcemap: ['linked', 'hidden', 'inline'], compress: ['gzip', 'brotli'] };
85
174
  for (let i = 0; i < argv.length; i++) {
@@ -89,9 +178,20 @@ function parseArgs(argv) {
89
178
  continue;
90
179
  }
91
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
+ }
92
192
  if (takesValue.has(key)) {
93
193
  if (val === undefined) val = argv[++i];
94
- 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}.`));
95
195
  if (Array.isArray(args[key])) args[key].push(val);
96
196
  else args[key] = val;
97
197
  } else if (optionalValue[key]) {
@@ -117,9 +217,30 @@ function overridesFrom(args) {
117
217
  if (args.verify === false) o.verify = false;
118
218
  if (args.compress !== undefined) o.compress = args.compress;
119
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;
120
240
  if (args.workers !== undefined) {
121
241
  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).');
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).'));
123
244
  o.workers = args.workers === 'auto' ? 'auto' : n;
124
245
  }
125
246
  const deps = {};
@@ -146,7 +267,12 @@ function printList(title, items, colorFn, limit = 30) {
146
267
  if (!items.length) return;
147
268
  console.log(`\n${colorFn(`${title} (${items.length})`)}`);
148
269
  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)`));
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
+ );
150
276
  }
151
277
 
152
278
  function jsonResult(result) {
@@ -170,6 +296,7 @@ function jsonResult(result) {
170
296
  notes: result.notes,
171
297
  verify: result.verify && { errors: result.verify.errors, stats: result.verify.stats },
172
298
  perf: result.perf,
299
+ budgets: result.budgets,
173
300
  };
174
301
  }
175
302
 
@@ -182,39 +309,74 @@ function printSummary(result, args) {
182
309
  console.log(`\n${C.green('✔')} ${counts} ${C.gray(`(${(result.durationMs / 1000).toFixed(2)} s)`)}`);
183
310
  if (result.verify) {
184
311
  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`);
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
+ );
186
318
  }
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)`);
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
+ );
189
333
  const p = result.perf;
190
334
  if (p?.tasks) {
191
335
  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';
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');
193
342
  const all = p.cacheHits === p.tasks;
194
343
  const detail = all
195
344
  ? 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}`);
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
+ );
200
354
  }
201
355
  if (result.obfuscated) {
202
356
  const deps = [
203
357
  ...result.dependencies.map((d) => `${d.rel} — ${d.reason}`),
204
- ...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)`)),
205
359
  ];
206
- 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);
207
361
  }
208
- printList('Avisos', result.warnings, C.yellow);
362
+ printList(L('Avisos', 'Warnings'), result.warnings, C.yellow);
209
363
  if (args.verbose) {
210
- printList('Notas', result.notes, C.cyan, 200);
364
+ printList(L('Notas', 'Notes'), result.notes, C.cyan, 200);
211
365
  printList(
212
- 'Excluidos',
366
+ L('Excluidos', 'Excluded'),
213
367
  result.ignored.map((i) => `${i.rel} — ${i.reason}`),
214
368
  C.gray,
215
369
  200,
216
370
  );
217
- } 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
+ );
218
380
  }
219
381
 
220
382
  async function cmdBuild(dir, args) {
@@ -234,49 +396,78 @@ async function cmdAnalyze(dir, args) {
234
396
  console.log(JSON.stringify(jsonResult(result), null, 2));
235
397
  return;
236
398
  }
237
- console.log(`${C.bold(`Builder ${VERSION} — análisis`)} ${cfg.root}\n`);
238
- 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
+ );
239
406
  if (result.pages.conservative.length)
240
407
  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')}`);
408
+ console.log(L(`\n${C.bold('Qué pasará con cada archivo del proyecto')}`, `\n${C.bold('What will happen to each project file')}`));
242
409
  const rows = [...result.coverage.entries()].sort((a, b) => a[0].localeCompare(b[0]));
243
410
  const w = Math.min(50, Math.max(...rows.map(([k]) => k.length), 10));
244
411
  for (const [rel, list] of rows) {
245
- 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}`);
246
413
  if (!shown.length) continue;
247
414
  console.log(` ${rel.padEnd(w)} ${C.gray(shown.join(' | '))}`);
248
415
  }
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)`));
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
+ );
251
426
  const deps = [...result.dependencies.map((d) => `${d.rel} — ${d.reason}`)];
252
- 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);
253
428
  if (result.workers.length)
254
429
  printList(
255
- 'Workers detectados',
256
- 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
+ ),
257
437
  C.cyan,
258
438
  );
259
439
  printList(
260
- 'Referencias rotas en el código fuente',
440
+ L('Referencias rotas en el código fuente', 'Broken references in the source code'),
261
441
  result.broken.map((b) => `${b.from} → ${b.url} (${b.status})`),
262
442
  C.yellow,
263
443
  );
264
- printList('Avisos', result.warnings, C.yellow);
444
+ printList(L('Avisos', 'Warnings'), result.warnings, C.yellow);
265
445
  printList(
266
- 'Excluidos',
446
+ L('Excluidos', 'Excluded'),
267
447
  result.ignored.map((i) => `${i.rel} — ${i.reason}`),
268
448
  C.gray,
269
449
  args.verbose ? 500 : 15,
270
450
  );
271
- if (args.verbose) printList('Notas', result.notes, C.cyan, 500);
451
+ if (args.verbose) printList(L('Notas', 'Notes'), result.notes, C.cyan, 500);
272
452
  const t = result.report.totals;
273
- 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
+ );
274
459
  }
275
460
 
276
461
  async function cmdInit(dir, args) {
277
462
  const projectDir = path.resolve(dir);
278
463
  const existing = findConfigFile(projectDir);
279
- 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
+ );
280
471
  const cfg = await loadConfig({ dir, config: args.config, overrides: overridesFrom(args) });
281
472
  const result = await runPipeline(cfg, { mode: 'analyze' });
282
473
  const config = {
@@ -295,12 +486,44 @@ async function cmdInit(dir, args) {
295
486
  const file = path.join(projectDir, 'builder.config.json');
296
487
  fs.writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
297
488
  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".`));
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
+ );
300
504
  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.'));
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
+ );
304
527
  }
305
528
 
306
529
  /** builder deps [carpeta] — lista; builder deps add|own|reset <patrón…> — edita builder.config.json. */
@@ -316,28 +539,52 @@ async function cmdDeps(_dir, args) {
316
539
  console.log(JSON.stringify({ dependencies: result.dependencies, own }, null, 2));
317
540
  return;
318
541
  }
319
- console.log(`${C.bold(`Builder ${VERSION} — dependencias`)} ${cfg.root}`);
542
+ console.log(`${C.bold(`Builder ${VERSION} — ${L('dependencias', 'dependencies')}`)} ${cfg.root}`);
320
543
  printList(
321
- 'Librerías de terceros (no se ofuscan)',
544
+ L('Librerías de terceros (no se ofuscan)', 'Third-party libraries (not obfuscated)'),
322
545
  [...deps].map(([rel, why]) => `${rel} ${C.gray(`— ${why}`)}`),
323
546
  C.cyan,
324
547
  200,
325
548
  );
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.'));
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
+ );
333
569
  }
334
570
 
335
571
  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/**"`);
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
+ );
337
579
  const projectDir = path.resolve(args.dir ?? '.');
338
580
  const file = args.config ? path.resolve(args.config) : (findConfigFile(projectDir) ?? path.join(projectDir, 'builder.config.json'));
339
581
  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".`);
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
+ );
341
588
  const config = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8').replace(/^/, '')) : {};
342
589
  if (config.dependencies === false || config.dependencies === true || config.dependencies == null) config.dependencies = {};
343
590
  const d = config.dependencies;
@@ -369,19 +616,38 @@ async function editDeps(sub, rawPatterns, args) {
369
616
  for (const p of patterns) {
370
617
  const hits = jsFiles.filter(globMatcher([p]));
371
618
  if (!hits.length) {
372
- console.log(C.yellow(` ! "${p}" no coincide con ningún archivo JavaScript del proyecto (queda guardado igualmente)`));
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
+ );
373
627
  continue;
374
628
  }
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')}`);
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
+ );
376
636
  }
377
637
  }
378
638
 
379
639
  async function cmdServe(dir, args) {
380
640
  const cfg = await loadConfig({ dir, config: args.config, overrides: overridesFrom(args) });
381
641
  const target = fs.existsSync(cfg.outDir) ? cfg.outDir : null;
382
- 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.`));
383
644
  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)')}`);
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
+ );
385
651
  if (args.open) openBrowser(srv.url);
386
652
  }
387
653
 
@@ -390,9 +656,12 @@ async function cmdDev(dir, args) {
390
656
  let srv = null;
391
657
  let running = false;
392
658
  let pending = false;
393
- 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) => {
394
662
  if (running) {
395
663
  pending = true;
664
+ pendingFiles = files && pendingFiles ? [...pendingFiles, ...files] : null;
396
665
  return;
397
666
  }
398
667
  running = true;
@@ -401,11 +670,13 @@ async function cmdDev(dir, args) {
401
670
  const fresh = await loadConfig({ dir, config: args.config, overrides: overridesFrom(args) });
402
671
  const result = await runPipeline(fresh, { mode: 'build', force: !!args.force });
403
672
  console.log(
404
- `${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)`)) : ''}`,
405
674
  );
406
675
  for (const w of result.warnings.slice(0, 5)) console.log(C.yellow(` • ${w}`));
407
676
  srv?.buildOk();
408
- 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();
409
680
  } catch (err) {
410
681
  console.error(C.red(`✖ ${err.message}`));
411
682
  for (const d of err.details ?? []) console.error(C.red(` • ${d}`));
@@ -414,14 +685,21 @@ async function cmdDev(dir, args) {
414
685
  running = false;
415
686
  if (pending) {
416
687
  pending = false;
417
- rebuild('cambios pendientes');
688
+ const next = pendingFiles;
689
+ pendingFiles = [];
690
+ rebuild(L('cambios pendientes', 'pending changes'), next);
418
691
  }
419
692
  }
420
693
  };
421
694
  await rebuild();
422
695
  fs.mkdirSync(cfg.outDir, { recursive: true });
423
696
  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)')}`);
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
+ );
425
703
  if (args.open) openBrowser(srv.url);
426
704
  let timer = null;
427
705
  const changed = new Set();
@@ -436,7 +714,7 @@ async function cmdDev(dir, args) {
436
714
  timer = setTimeout(() => {
437
715
  const list = [...changed];
438
716
  changed.clear();
439
- rebuild(list.length === 1 ? list[0] : `${list.length} archivos`);
717
+ rebuild(list.length === 1 ? list[0] : L(`${list.length} archivos`, `${list.length} files`), list);
440
718
  }, 150);
441
719
  });
442
720
  }
@@ -449,12 +727,16 @@ function openBrowser(url) {
449
727
 
450
728
  async function main() {
451
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]);
452
733
  const args = parseArgs(argv);
453
734
  if (args.version || args.v) return console.log(VERSION);
454
735
  const [command = 'help', dir = '.'] = args._;
455
- if (args.help || args.h || command === 'help') return console.log(HELP);
736
+ if (args.help || args.h || command === 'help') return console.log(help());
456
737
  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".`);
738
+ if (!commands[command])
739
+ throw new BuildError(L(`Comando desconocido "${command}". Usa "builder help".`, `Unknown command "${command}". Use "builder help".`));
458
740
  await commands[command](dir, args);
459
741
  }
460
742