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/src/builder.js CHANGED
@@ -23,6 +23,9 @@ import { detectDependency, dependencyPragma, packageNames } from './deps.js';
23
23
  import { assertSafeOutDir, writeStaging, swapIntoPlace, discardStaging, writeManifest, MANIFEST } from './output.js';
24
24
  import { verifyOutput } from './verify.js';
25
25
  import { rewriteTsImports } from './jsimports.js';
26
+ import { IMAGE_EXT, ensureSharp, imageOptions, optimizeImage } from './images.js';
27
+ import { checkBudgets } from './budgets.js';
28
+ import { TRANSFORM_EXT, runHook, runTransform } from './plugins.js';
26
29
  import { buildReport } from './report.js';
27
30
  import {
28
31
  BuildError,
@@ -41,6 +44,7 @@ import {
41
44
  minifyJsonText,
42
45
  hasOwn,
43
46
  } from './util.js';
47
+ import { L } from './i18n.js';
44
48
 
45
49
  export const VERSION = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
46
50
 
@@ -94,6 +98,7 @@ class Context {
94
98
  this.depCache = new Map();
95
99
  this.depMarks = new Map();
96
100
  this.tsCode = new Map();
101
+ this.sourceOverrides = new Map();
97
102
  this.pkgNames = null;
98
103
  this.vendorPackages = [];
99
104
  this.chunkDir = cfg.js.chunkDir.replace(/\/+$/, '');
@@ -129,8 +134,10 @@ class Context {
129
134
  }
130
135
 
131
136
  readText(rel, { kind = 'text', charset } = {}) {
132
- // TypeScript: como JS se lee la versión ya transpilada (sin tipos).
137
+ // TypeScript: como JS se lee la versión ya transpilada (sin tipos; ya incluye los cambios de los plugins).
133
138
  if (kind === 'js' && this.tsCode.has(rel)) return { text: this.tsCode.get(rel), bom: false, encoding: 'utf8' };
139
+ // Código fuente cambiado por un plugin (transform): es el que se procesa a partir de aquí.
140
+ if (this.sourceOverrides.has(rel)) return { ...this.sourceOverrides.get(rel), encoding: 'utf8' };
134
141
  const key = `${rel}\0${kind}`;
135
142
  if (this.textCache.has(key)) return this.textCache.get(key);
136
143
  const buf = fs.readFileSync(this.abs(rel));
@@ -141,7 +148,13 @@ class Context {
141
148
  // latin1 conserva cada byte tal cual: lo que no se edita sale idéntico.
142
149
  text = buf.toString('latin1');
143
150
  encoding = 'latin1';
144
- this.warn('no es UTF-8; se conserva su codificación original y no se minifica', rel);
151
+ this.warn(
152
+ L(
153
+ 'no es UTF-8; se conserva su codificación original y no se minifica',
154
+ 'is not UTF-8; its original encoding is kept and it is not minified',
155
+ ),
156
+ rel,
157
+ );
145
158
  } else {
146
159
  let label = (charset || 'windows-1252').toLowerCase();
147
160
  try {
@@ -151,7 +164,13 @@ class Context {
151
164
  text = new TextDecoder(label).decode(buf);
152
165
  }
153
166
  encoding = label;
154
- this.note(`no es UTF-8; se leyó como ${label} y la salida usa escapes ASCII (no depende de la codificación)`, rel);
167
+ this.note(
168
+ L(
169
+ `no es UTF-8; se leyó como ${label} y la salida usa escapes ASCII (no depende de la codificación)`,
170
+ `is not UTF-8; it was read as ${label} and the output uses ASCII escapes (independent of the encoding)`,
171
+ ),
172
+ rel,
173
+ );
155
174
  }
156
175
  }
157
176
  const stripped = stripBom(text);
@@ -172,17 +191,36 @@ class Context {
172
191
  if (this.brokenKeys.has(key)) return;
173
192
  this.brokenKeys.add(key);
174
193
  this.broken.push({ from, url, status: r.status, target: r.rel });
175
- if (r.status === 'ignored') this.warn(`referencia a "${url}", pero ese archivo está excluido (ignore). No se publicará.`, from);
176
- else if (r.status === 'case') this.warn(`"${url}" solo existe como "${r.rel}" (otras mayúsculas): en un servidor Linux daría 404.`, from);
177
- else if (r.status === 'missing') this.warn(`"${url}" no existe (ya estaba rota en el código fuente).`, from);
178
- else if (r.status === 'outside') this.note(`"${url}" apunta fuera del proyecto; se deja igual.`, from);
194
+ if (r.status === 'ignored')
195
+ this.warn(
196
+ L(
197
+ `referencia a "${url}", pero ese archivo está excluido (ignore). No se publicará.`,
198
+ `references "${url}", but that file is excluded (ignore). It will not be published.`,
199
+ ),
200
+ from,
201
+ );
202
+ else if (r.status === 'case')
203
+ this.warn(
204
+ L(
205
+ `"${url}" solo existe como "${r.rel}" (otras mayúsculas): en un servidor Linux daría 404.`,
206
+ `"${url}" only exists as "${r.rel}" (different capitalization): it would be a 404 on a Linux server.`,
207
+ ),
208
+ from,
209
+ );
210
+ else if (r.status === 'missing')
211
+ this.warn(
212
+ L(`"${url}" no existe (ya estaba rota en el código fuente).`, `"${url}" does not exist (it was already broken in the source code).`),
213
+ from,
214
+ );
215
+ else if (r.status === 'outside')
216
+ this.note(L(`"${url}" apunta fuera del proyecto; se deja igual.`, `"${url}" points outside the project; left as is.`), from);
179
217
  }
180
218
 
181
219
  registerAsset(rel, from) {
182
220
  if (!this.assets.has(rel)) this.assets.set(rel, new Set());
183
221
  if (from) this.assets.get(rel).add(from);
184
222
  const ext = extOf(rel);
185
- if (JS_EXT.has(ext) || CSS_EXT.has(ext)) this.requireStandalone(rel, `referenciado desde ${from}`);
223
+ if (JS_EXT.has(ext) || CSS_EXT.has(ext)) this.requireStandalone(rel, L(`referenciado desde ${from}`, `referenced from ${from}`));
186
224
  }
187
225
 
188
226
  assetOut(rel) {
@@ -193,7 +231,7 @@ class Context {
193
231
  urlTarget(rel, from) {
194
232
  const ext = extOf(rel);
195
233
  if (JS_EXT.has(ext) || CSS_EXT.has(ext)) {
196
- if (!this.workers.has(rel)) this.requireStandalone(rel, `new URL() en ${from}`);
234
+ if (!this.workers.has(rel)) this.requireStandalone(rel, L(`new URL() en ${from}`, `new URL() in ${from}`));
197
235
  } else this.registerAsset(rel, from);
198
236
  return jsOutPath(rel);
199
237
  }
@@ -206,7 +244,14 @@ class Context {
206
244
  registerWorker(rel, info) {
207
245
  const prev = this.workers.get(rel);
208
246
  if (prev) {
209
- if (prev.module !== info.module) this.warn(`se usa como worker clásico y de módulo a la vez; se trata como módulo`, rel);
247
+ if (prev.module !== info.module)
248
+ this.warn(
249
+ L(
250
+ `se usa como worker clásico y de módulo a la vez; se trata como módulo`,
251
+ `used as both a classic and a module worker; treated as a module`,
252
+ ),
253
+ rel,
254
+ );
210
255
  prev.module = prev.module || info.module;
211
256
  return;
212
257
  }
@@ -217,7 +262,13 @@ class Context {
217
262
  noteJsInfo(id, info, baseDir) {
218
263
  for (const w of info.workers) {
219
264
  if (!w.spec) {
220
- this.warn(`${w.kind} con una ruta que no es texto fijo; Builder no puede seguirla (el archivo se procesará suelto si existe).`, id);
265
+ this.warn(
266
+ L(
267
+ `${w.kind} con una ruta que no es texto fijo; Builder no puede seguirla (el archivo se procesará suelto si existe).`,
268
+ `${w.kind} with a path that is not fixed text; Builder cannot follow it (the file will be processed on its own if it exists).`,
269
+ ),
270
+ id,
271
+ );
221
272
  continue;
222
273
  }
223
274
  const c = classifyUrl(w.spec);
@@ -228,7 +279,7 @@ class Context {
228
279
  this.reportBroken(id, w.spec, this.resolve(w.spec, baseDir));
229
280
  continue;
230
281
  }
231
- if (w.kind === 'importScripts') this.requireStandalone(rel, `importScripts en ${id}`, { classic: true });
282
+ if (w.kind === 'importScripts') this.requireStandalone(rel, L(`importScripts en ${id}`, `importScripts in ${id}`), { classic: true });
232
283
  else this.registerWorker(rel, { module: w.module, kind: w.kind, from: id });
233
284
  }
234
285
  for (const s of info.strings) {
@@ -261,9 +312,10 @@ class Context {
261
312
  if (this.depExcluded(rel)) reason = null;
262
313
  else if (this.depIncluded(rel)) reason = 'marcado a mano (dependencies.include)';
263
314
  else if (mark === 'own' || pragma === 'own') reason = null;
264
- else if (mark === 'dependency') reason = 'marcado en el HTML (data-builder="dependency")';
265
- else if (pragma === 'dependency') reason = 'marcado en el propio archivo (@builder-dependency)';
266
- else if (/(^|\/)node_modules\//.test(rel)) reason = 'paquete de npm';
315
+ else if (mark === 'dependency') reason = L('marcado en el HTML (data-builder="dependency")', 'marked in the HTML (data-builder="dependency")');
316
+ else if (pragma === 'dependency')
317
+ reason = L('marcado en el propio archivo (@builder-dependency)', 'marked in the file itself (@builder-dependency)');
318
+ else if (/(^|\/)node_modules\//.test(rel)) reason = L('paquete de npm', 'npm package');
267
319
  else if (deps.detect && isJs) reason = detectDependency(rel, text, { packages: this.packageHints() });
268
320
  this.depCache.set(rel, reason);
269
321
  return reason;
@@ -301,23 +353,35 @@ class Context {
301
353
  /** Registra un archivo de salida; nunca sobrescribe en silencio un contenido distinto. */
302
354
  emit(outRel, data, meta) {
303
355
  const norm = path.posix.normalize(outRel);
304
- if (norm.startsWith('../') || path.posix.isAbsolute(norm) || norm === MANIFEST) throw new BuildError(`Ruta de salida inválida: ${outRel}`);
356
+ if (norm.startsWith('../') || path.posix.isAbsolute(norm) || norm === MANIFEST)
357
+ throw new BuildError(L(`Ruta de salida inválida: ${outRel}`, `Invalid output path: ${outRel}`));
305
358
  const buf = Buffer.isBuffer(data) ? data : Buffer.from(data, meta.encoding === 'latin1' ? 'latin1' : 'utf8');
306
359
  const prev = this.outputs.get(norm);
307
360
  if (prev) {
308
361
  const same = prev.copyFrom ? fs.readFileSync(prev.copyFrom).equals(buf) : prev.data.equals(buf);
309
362
  if (!same)
310
- throw new BuildError(`Dos resultados distintos quieren escribirse en "${norm}".`, [
311
- `Primero: ${(prev.sources ?? []).join(', ') || prev.action}`,
312
- `Después: ${(meta.sources ?? []).join(', ') || meta.action}`,
313
- 'Suele pasar si el mismo archivo se usa con import maps distintos o si una carpeta del proyecto se llama igual que js.chunkDir.',
314
- ]);
363
+ throw new BuildError(
364
+ L(`Dos resultados distintos quieren escribirse en "${norm}".`, `Two different results want to be written to "${norm}".`),
365
+ [
366
+ L(`Primero: ${(prev.sources ?? []).join(', ') || prev.action}`, `First: ${(prev.sources ?? []).join(', ') || prev.action}`),
367
+ L(`Después: ${(meta.sources ?? []).join(', ') || meta.action}`, `Second: ${(meta.sources ?? []).join(', ') || meta.action}`),
368
+ L(
369
+ 'Suele pasar si el mismo archivo se usa con import maps distintos o si una carpeta del proyecto se llama igual que js.chunkDir.',
370
+ 'This usually happens when the same file is used with different import maps, or when a project folder has the same name as js.chunkDir.',
371
+ ),
372
+ ],
373
+ );
315
374
  prev.sources = [...new Set([...(prev.sources ?? []), ...(meta.sources ?? [])])];
316
375
  return norm;
317
376
  }
318
377
  const lower = norm.toLowerCase();
319
378
  if (this.outLower.has(lower) && this.outLower.get(lower) !== norm)
320
- this.warn(`"${norm}" y "${this.outLower.get(lower)}" solo se diferencian en mayúsculas; en Windows/macOS se pisarían.`);
379
+ this.warn(
380
+ L(
381
+ `"${norm}" y "${this.outLower.get(lower)}" solo se diferencian en mayúsculas; en Windows/macOS se pisarían.`,
382
+ `"${norm}" and "${this.outLower.get(lower)}" differ only in capitalization; on Windows/macOS they would overwrite each other.`,
383
+ ),
384
+ );
321
385
  this.outLower.set(lower, norm);
322
386
  this.outputs.set(norm, { ...meta, data: buf });
323
387
  return norm;
@@ -325,6 +389,8 @@ class Context {
325
389
 
326
390
  emitCopy(outRel, rel, action = 'copy') {
327
391
  const f = this.files.get(rel);
392
+ const override = this.sourceOverrides.get(rel);
393
+ if (override) return this.emit(outRel, (override.bom ? BOM : '') + override.text, { type: typeOfExt(f.ext), action, sources: [rel] });
328
394
  const prev = this.outputs.get(outRel);
329
395
  if (prev) {
330
396
  if (prev.sources?.includes(rel)) return outRel;
@@ -386,7 +452,7 @@ function loadPages(ctx) {
386
452
  try {
387
453
  model = parseHtml(text);
388
454
  } catch (err) {
389
- ctx.warn(`no se pudo analizar el HTML (${err.message}); se copia tal cual`, rel);
455
+ ctx.warn(L(`no se pudo analizar el HTML (${err.message}); se copia tal cual`, `could not parse the HTML (${err.message}); copied as is`), rel);
390
456
  continue;
391
457
  }
392
458
  const dir = path.posix.dirname(rel) === '.' ? '' : path.posix.dirname(rel);
@@ -395,7 +461,13 @@ function loadPages(ctx) {
395
461
  const c = classifyUrl(model.base);
396
462
  if (c.type === 'external') {
397
463
  page.baseExternal = true;
398
- ctx.warn(`<base href="${model.base}"> apunta fuera del sitio; sus rutas relativas no se pueden comprobar y se dejan igual.`, rel);
464
+ ctx.warn(
465
+ L(
466
+ `<base href="${model.base}"> apunta fuera del sitio; sus rutas relativas no se pueden comprobar y se dejan igual.`,
467
+ `<base href="${model.base}"> points outside the site; its relative paths cannot be checked and are left as they are.`,
468
+ ),
469
+ rel,
470
+ );
399
471
  } else if (c.type === 'root' || c.type === 'relative') {
400
472
  const p = c.type === 'root' ? (c.path.startsWith(ctx.base) ? c.path.slice(ctx.base.length) : c.path.slice(1)) : path.posix.join(dir, c.path);
401
473
  page.baseDir = path.posix
@@ -410,6 +482,20 @@ function loadPages(ctx) {
410
482
  }
411
483
 
412
484
  /** Decide qué páginas se procesan completas (entradas + enlazadas) y cuáles en modo conservador. */
485
+ /** Pasa el código fuente por los plugins con transform (en paralelo entre archivos, en orden entre plugins). */
486
+ async function applyTransforms(ctx) {
487
+ const plugins = ctx.cfg.plugins.filter((p) => typeof p.transform === 'function');
488
+ if (!plugins.length) return;
489
+ const list = [...ctx.files.keys()].filter((rel) => TRANSFORM_EXT.has(extOf(rel)));
490
+ await mapLimit(list, 16, async (rel) => {
491
+ const text = decodeUtf8(fs.readFileSync(ctx.abs(rel)));
492
+ if (text == null) return; // otras codificaciones: se respetan los bytes originales
493
+ const src = stripBom(text);
494
+ const out = await runTransform(plugins, rel, src.text);
495
+ if (out !== src.text) ctx.sourceOverrides.set(rel, { text: out, bom: src.bom });
496
+ });
497
+ }
498
+
413
499
  /**
414
500
  * Transpila cada archivo TypeScript a JavaScript (solo quita los tipos: no baja el nivel de sintaxis).
415
501
  * Usa el tsconfig.json del proyecto si existe. Lo que falle se avisa y sigue el camino normal
@@ -445,7 +531,7 @@ async function transpileTypeScript(ctx) {
445
531
  ctx.tsCode.set(rel, rewriteTsImports(r.code, complete));
446
532
  } catch (err) {
447
533
  const msg = String(err.errors?.[0]?.text ?? err.message).split('\n')[0];
448
- ctx.warn(`TypeScript no válido (${msg}); se copia sin transpilar`, rel);
534
+ ctx.warn(L(`TypeScript no válido (${msg}); se copia sin transpilar`, `invalid TypeScript (${msg}); copied without transpiling`), rel);
449
535
  }
450
536
  });
451
537
  }
@@ -461,14 +547,23 @@ function markDependencies(ctx, pages) {
461
547
  .toLowerCase();
462
548
  const value = MARK_VALUES[raw];
463
549
  if (!value) {
464
- ctx.warn(`data-builder="${raw}" no se reconoce; usa "dependency" o "own"`, page.rel);
550
+ ctx.warn(
551
+ L(`data-builder="${raw}" no se reconoce; usa "dependency" o "own"`, `data-builder="${raw}" is not recognized; use "dependency" or "own"`),
552
+ page.rel,
553
+ );
465
554
  continue;
466
555
  }
467
556
  const res = ctx.resolve(s.src.value, page.baseDir);
468
557
  if (res.status !== 'ok') continue;
469
558
  const prev = ctx.depMarks.get(res.rel);
470
559
  if (prev && prev !== value)
471
- ctx.warn(`"${res.rel}" está marcado como "${prev}" y como "${value}" en distintas páginas; se usa "${prev}"`, page.rel);
560
+ ctx.warn(
561
+ L(
562
+ `"${res.rel}" está marcado como "${prev}" y como "${value}" en distintas páginas; se usa "${prev}"`,
563
+ `"${res.rel}" is marked as "${prev}" and as "${value}" on different pages; using "${prev}"`,
564
+ ),
565
+ page.rel,
566
+ );
472
567
  else ctx.depMarks.set(res.rel, value);
473
568
  }
474
569
  }
@@ -482,7 +577,7 @@ function selectEntries(ctx, pages) {
482
577
  queue = [...pages.values()].filter((p) => p.model.isDocument).map((p) => p.rel);
483
578
  } else {
484
579
  const { matched, missing } = matchFiles(ctx.files, cfg.entries);
485
- if (missing.length) throw new BuildError('Algunas entradas de "entries" no existen:', missing);
580
+ if (missing.length) throw new BuildError(L('Algunas entradas de "entries" no existen:', 'Some "entries" do not exist:'), missing);
486
581
  for (const rel of matched) {
487
582
  const ext = extOf(rel);
488
583
  if (HTML_EXT.has(ext)) queue.push(rel);
@@ -490,7 +585,11 @@ function selectEntries(ctx, pages) {
490
585
  else if (JS_EXT.has(ext)) {
491
586
  const info = ctx.jsInfo(rel);
492
587
  (info.ok && (info.hasModuleSyntax || info.hasImportMeta) ? extraEntries.modules : extraEntries.classic).push(rel);
493
- } else ctx.warn(`"${rel}" no es HTML, JS ni CSS; se copiará como recurso`, 'entries');
588
+ } else
589
+ ctx.warn(
590
+ L(`"${rel}" no es HTML, JS ni CSS; se copiará como recurso`, `"${rel}" is not HTML, JS or CSS; it will be copied as an asset`),
591
+ 'entries',
592
+ );
494
593
  }
495
594
  }
496
595
  const full = new Set();
@@ -549,7 +648,10 @@ function parseImportMap(ctx, page, item) {
549
648
  const json = JSON.parse(item.inline.code);
550
649
  return { json, imports: json.imports ?? {}, scopes: json.scopes, baseDir: page.baseDir };
551
650
  } catch (err) {
552
- ctx.warn(`el import map no es JSON válido (${err.message}); se ignora`, page.rel);
651
+ ctx.warn(
652
+ L(`el import map no es JSON válido (${err.message}); se ignora`, `the import map is not valid JSON (${err.message}); ignored`),
653
+ page.rel,
654
+ );
553
655
  return null;
554
656
  }
555
657
  }
@@ -580,16 +682,23 @@ function classicRuns(ctx, page) {
580
682
  continue;
581
683
  }
582
684
  const rel = res.rel;
583
- if (!JS_EXT.has(extOf(rel))) ctx.note(`<script src> apunta a "${rel}" (extensión no habitual); se trata como JavaScript`, page.rel);
685
+ if (!JS_EXT.has(extOf(rel)))
686
+ ctx.note(
687
+ L(
688
+ `<script src> apunta a "${rel}" (extensión no habitual); se trata como JavaScript`,
689
+ `<script src> points to "${rel}" (unusual extension); treated as JavaScript`,
690
+ ),
691
+ page.rel,
692
+ );
584
693
  const info = ctx.jsInfo(rel, false);
585
694
  const attrs = s.attrs;
586
695
  const reasons = [];
587
- if (!mergeOn) reasons.push('unión desactivada');
588
- if (!info.ok) reasons.push('no se pudo analizar');
589
- if (s.inTemplate) reasons.push('dentro de <template>');
696
+ if (!mergeOn) reasons.push(L('unión desactivada', 'merging disabled'));
697
+ if (!info.ok) reasons.push(L('no se pudo analizar', 'could not be parsed'));
698
+ if (s.inTemplate) reasons.push(L('dentro de <template>', 'inside <template>'));
590
699
  if (attrs.has('async')) reasons.push('async');
591
- if (info.usesCurrentScript) reasons.push('usa document.currentScript');
592
- if (info.usesDocumentWrite) reasons.push('usa document.write');
700
+ if (info.usesCurrentScript) reasons.push(L('usa document.currentScript', 'uses document.currentScript'));
701
+ if (info.usesDocumentWrite) reasons.push(L('usa document.write', 'uses document.write'));
593
702
  const extra = [...attrs.keys()].filter((a) => !MERGE_ATTRS.has(a));
594
703
  if (extra.length) reasons.push(`atributos ${extra.join(', ')}`);
595
704
  const obf = ctx.shouldObfuscate([rel]);
@@ -647,10 +756,34 @@ export async function runPipeline(cfg, opts = {}) {
647
756
 
648
757
  async function pipeline(ctx, cfg, opts, mode, t0) {
649
758
  const log = opts.log ?? (() => {});
759
+ const bt = cfg.browserTargets;
760
+ if (bt?.missing)
761
+ ctx.warn(
762
+ L(
763
+ `hay navegadores objetivo en ${bt.from}, pero falta el paquete "browserslist"; se ignoran (instálalo con: npm install --save-dev browserslist)`,
764
+ `target browsers are set in ${bt.from}, but the "browserslist" package is missing; they are ignored (install it with: npm install --save-dev browserslist)`,
765
+ ),
766
+ );
767
+ else if (bt?.targets?.length) {
768
+ log(L(`Navegadores (${bt.from}): ${bt.targets.join(', ')}`, `Browsers (${bt.from}): ${bt.targets.join(', ')}`));
769
+ if (bt.ignored.length)
770
+ ctx.note(
771
+ L(
772
+ `navegadores sin motor equivalente en esbuild (se usan los demás): ${bt.ignored.join(', ')}`,
773
+ `browsers without an equivalent esbuild engine (the others are used): ${bt.ignored.join(', ')}`,
774
+ ),
775
+ );
776
+ } else if (bt && !bt.targets?.length)
777
+ ctx.warn(
778
+ L(
779
+ `la consulta de navegadores de ${bt.from} no da ningún navegador conocido; se ignora`,
780
+ `the browsers query in ${bt.from} matches no known browser; ignored`,
781
+ ),
782
+ );
650
783
  // Tiempo de cada fase (perf.phases): para saber dónde se va el tiempo en proyectos reales.
651
784
  const phases = [];
652
785
  let phaseStart = Date.now();
653
- let phaseName = 'preparación';
786
+ let phaseName = 'setup';
654
787
  const mark = (next) => {
655
788
  const now = Date.now();
656
789
  phases.push([phaseName, now - phaseStart]);
@@ -659,21 +792,30 @@ async function pipeline(ctx, cfg, opts, mode, t0) {
659
792
  };
660
793
  if (ctx.obfuscatorOptions) await ensureObfuscator(cfg.root);
661
794
  if (cfg.js.minifier === 'terser' && cfg.js.minify) await ensureTerser(cfg.root);
795
+ const images = mode === 'build' ? imageOptions(cfg.images) : null;
796
+ const sharp = images ? await ensureSharp(cfg.root) : null;
662
797
 
663
- mark('páginas');
798
+ mark('pages');
664
799
  // 1. Páginas y entradas
800
+ await runHook(cfg.plugins, 'buildStart', { config: cfg, mode });
801
+ await applyTransforms(ctx);
665
802
  await transpileTypeScript(ctx);
666
803
  const pages = loadPages(ctx);
667
804
  markDependencies(ctx, pages);
668
805
  const extra = selectEntries(ctx, pages);
669
806
  const fullPages = [...pages.values()].filter((p) => p.full);
670
807
  const partialPages = [...pages.values()].filter((p) => !p.full);
671
- log(`Páginas: ${fullPages.length} completas, ${partialPages.length} en modo conservador`);
808
+ log(
809
+ L(
810
+ `Páginas: ${fullPages.length} completas, ${partialPages.length} en modo conservador`,
811
+ `Pages: ${fullPages.length} complete, ${partialPages.length} in conservative mode`,
812
+ ),
813
+ );
672
814
  const htmlBlob = [];
673
815
  for (const p of fullPages) collectRefs(ctx, p);
674
816
  for (const p of partialPages) collectRefs(ctx, p);
675
817
 
676
- mark('módulos');
818
+ mark('modules');
677
819
  // 2. Módulos ES agrupados por import map
678
820
  const groups = new Map();
679
821
  const groupFor = (page, importMap) => {
@@ -686,7 +828,7 @@ async function pipeline(ctx, cfg, opts, mode, t0) {
686
828
  entries: new Set(),
687
829
  virtual: new Map(),
688
830
  pages: new Set(),
689
- forcePerFile: importMap?.scopes ? 'el import map usa "scopes"' : null,
831
+ forcePerFile: importMap?.scopes ? L('el import map usa "scopes"', 'the import map uses "scopes"') : null,
690
832
  vendorSplit: !!ctx.obfuscatorOptions && !cfg.dependencies.obfuscate,
691
833
  });
692
834
  return groups.get(key);
@@ -695,7 +837,11 @@ async function pipeline(ctx, cfg, opts, mode, t0) {
695
837
  const preloadLinks = [];
696
838
  for (const page of fullPages) {
697
839
  const map = page.model.importMaps[0] ? parseImportMap(ctx, page, page.model.importMaps[0]) : null;
698
- if (page.model.importMaps.length > 1) ctx.warn('tiene varios import maps; solo se usa el primero para empaquetar', page.rel);
840
+ if (page.model.importMaps.length > 1)
841
+ ctx.warn(
842
+ L('tiene varios import maps; solo se usa el primero para empaquetar', 'has several import maps; only the first one is used for bundling'),
843
+ page.rel,
844
+ );
699
845
  let vIndex = 0;
700
846
  for (const s of page.model.scripts) {
701
847
  if (s.kind !== 'module' || s.inTemplate) continue;
@@ -715,7 +861,7 @@ async function pipeline(ctx, cfg, opts, mode, t0) {
715
861
  const outDir = page.baseDir ? `${page.baseDir}/` : '';
716
862
  g.virtual.set(id, {
717
863
  code: s.inline.code,
718
- label: `${page.rel} (módulo inline ${vIndex})`,
864
+ label: L(`${page.rel} (módulo inline ${vIndex})`, `${page.rel} (inline module ${vIndex})`),
719
865
  baseDir: page.baseDir,
720
866
  out: `${outDir}__vb_inline_${shortHash(id)}`,
721
867
  page,
@@ -743,8 +889,14 @@ async function pipeline(ctx, cfg, opts, mode, t0) {
743
889
  if (!g.entries.size && !g.virtual.size) continue;
744
890
  const res = await buildModuleGroup(ctx, { ...g, hash: g.hash });
745
891
  if (res.mode === 'per-file') {
746
- ctx.note(`módulos sin empaquetar (cada archivo minificado en su sitio): ${res.reason}`, [...g.pages].join(', ') || 'entries');
747
- for (const rel of res.inputs) ctx.requireStandalone(rel, 'módulo (modo sin empaquetar)', { module: true });
892
+ ctx.note(
893
+ L(
894
+ `módulos sin empaquetar (cada archivo minificado en su sitio): ${res.reason}`,
895
+ `modules not bundled (each file minified in place): ${res.reason}`,
896
+ ),
897
+ [...g.pages].join(', ') || 'entries',
898
+ );
899
+ for (const rel of res.inputs) ctx.requireStandalone(rel, L('módulo (modo sin empaquetar)', 'module (unbundled mode)'), { module: true });
748
900
  for (const rel of g.entries) entryOut.set(`${g.key}\0${rel}`, jsOutPath(rel));
749
901
  for (const [id, v] of g.virtual) {
750
902
  const info = analyzeJs(v.code, { module: true });
@@ -757,7 +909,12 @@ async function pipeline(ctx, cfg, opts, mode, t0) {
757
909
  }
758
910
  continue;
759
911
  }
760
- log(`Módulos: ${g.entries.size} entradas, ${res.inputs.length} archivos, target ${res.target}`);
912
+ log(
913
+ L(
914
+ `Módulos: ${g.entries.size} entradas, ${res.inputs.length} archivos, target ${res.target}`,
915
+ `Modules: ${g.entries.size} entries, ${res.inputs.length} files, target ${res.target}`,
916
+ ),
917
+ );
761
918
  // Ofuscar/comprimir cada salida en paralelo; registrar después en el orden original (salida determinista).
762
919
  const finals = await mapLimit(res.outputs, 64, async (o) => {
763
920
  const obfRels = o.virtualId ? [g.virtual.get(o.virtualId).page.rel] : o.isChunk ? o.inputs : [o.entry];
@@ -775,18 +932,24 @@ async function pipeline(ctx, cfg, opts, mode, t0) {
775
932
  if (o.virtualId) {
776
933
  const v = g.virtual.get(o.virtualId);
777
934
  inlineModuleCode.set(o.virtualId, code.replace(/\n$/, ''));
778
- for (const rel of o.inputs) ctx.cover(rel, 'empaquetado', `${v.page.rel} (módulo inline)`);
935
+ for (const rel of o.inputs) ctx.cover(rel, L('empaquetado', 'bundled'), L(`${v.page.rel} (módulo inline)`, `${v.page.rel} (inline module)`));
779
936
  continue;
780
937
  }
781
938
  const action = o.isVendor ? 'vendor' : o.isChunk ? 'chunk' : 'bundle';
782
939
  const outRel = ctx.emitCode(o.outRel, code, map, { type: 'js', module: true, action, sources, before: o.before || sumSizes(ctx, sources) });
783
940
  if (o.entry) entryOut.set(`${g.key}\0${o.entry}`, outRel);
784
- for (const rel of o.inputs) ctx.cover(rel, o.isVendor ? 'empaquetado (dependencia, sin ofuscar)' : 'empaquetado', outRel);
941
+ for (const rel of o.inputs)
942
+ ctx.cover(
943
+ rel,
944
+ o.isVendor ? L('empaquetado (dependencia, sin ofuscar)', 'bundled (dependency, not obfuscated)') : L('empaquetado', 'bundled'),
945
+ outRel,
946
+ );
785
947
  }
786
948
  for (const spec of res.vendor) if (!ctx.vendorPackages.includes(spec)) ctx.vendorPackages.push(spec);
787
949
  // esbuild puede incrustar un módulo entero (p. ej. constantes) sin que figure en ninguna salida.
788
950
  for (const rel of res.consumed) {
789
- if (!ctx.coverage.get(rel)?.some((s) => s.startsWith('empaquetado'))) ctx.cover(rel, 'empaquetado (incrustado por completo en el paquete)');
951
+ if (!ctx.coverage.get(rel)?.some((s) => s.startsWith(L('empaquetado', 'bundled'))))
952
+ ctx.cover(rel, L('empaquetado (incrustado por completo en el paquete)', 'bundled (fully inlined into the bundle)'));
790
953
  }
791
954
  }
792
955
 
@@ -810,15 +973,15 @@ async function pipeline(ctx, cfg, opts, mode, t0) {
810
973
  const hashed = cfg.hash && !extra.css.includes(rel);
811
974
  const r = cssBuilt[ci];
812
975
  if (r.error) {
813
- ctx.warn(`no se pudo procesar el CSS (${r.error}); se copia tal cual`, rel);
976
+ ctx.warn(L(`no se pudo procesar el CSS (${r.error}); se copia tal cual`, `could not process the CSS (${r.error}); copied as is`), rel);
814
977
  cssOut.set(rel, ctx.emitCopy(rel, rel, 'copy-fallback'));
815
- ctx.cover(rel, 'copiado sin cambios (error al procesar)', rel);
978
+ ctx.cover(rel, L('copiado sin cambios (error al procesar)', 'copied unchanged (processing error)'), rel);
816
979
  continue;
817
980
  }
818
981
  const outRel = hashed ? ctx.hashedPath(rel, r.code, '.css') : rel;
819
982
  ctx.emitCode(outRel, r.code, r.map, { type: 'css', action: 'bundle', sources: r.inputs, before: sumSizes(ctx, r.inputs) });
820
983
  cssOut.set(rel, outRel);
821
- for (const i of r.inputs) ctx.cover(i, i === rel ? 'procesado' : 'empaquetado', outRel);
984
+ for (const i of r.inputs) ctx.cover(i, i === rel ? L('procesado', 'processed') : L('empaquetado', 'bundled'), outRel);
822
985
  }
823
986
 
824
987
  mark('white-space');
@@ -831,11 +994,14 @@ async function pipeline(ctx, cfg, opts, mode, t0) {
831
994
  collapse = !pre;
832
995
  if (pre)
833
996
  ctx.note(
834
- 'se usa white-space: pre/pre-wrap/pre-line en el CSS; el HTML no colapsa espacios para no alterar ningún texto (html.collapseWhitespace: true lo fuerza)',
997
+ L(
998
+ 'se usa white-space: pre/pre-wrap/pre-line en el CSS; el HTML no colapsa espacios para no alterar ningún texto (html.collapseWhitespace: true lo fuerza)',
999
+ 'white-space: pre/pre-wrap/pre-line is used in the CSS; HTML whitespace is not collapsed so no text changes (html.collapseWhitespace: true forces it)',
1000
+ ),
835
1001
  );
836
1002
  }
837
1003
 
838
- mark('scripts clásicos');
1004
+ mark('classic scripts');
839
1005
  // 5. Scripts clásicos por página
840
1006
  // Primero se planifican todas las páginas, luego se minifica/ofusca cada grupo distinto en paralelo
841
1007
  // y por último se registra todo en el orden original.
@@ -876,7 +1042,7 @@ async function pipeline(ctx, cfg, opts, mode, t0) {
876
1042
  }
877
1043
  }
878
1044
 
879
- mark('edición html');
1045
+ mark('html edits');
880
1046
  // 6. Edición de cada página: módulos, CSS, inline, preload
881
1047
  for (const page of fullPages) {
882
1048
  for (const m of moduleRefs.filter((x) => x.page === page)) {
@@ -902,11 +1068,11 @@ async function pipeline(ctx, cfg, opts, mode, t0) {
902
1068
  }
903
1069
  for (const page of pages.values()) await processInline(ctx, page, collapse);
904
1070
 
905
- mark('workers y sueltos');
1071
+ mark('workers and standalone');
906
1072
  // 7. Workers, archivos sueltos y rutas mencionadas (hasta que no aparezca nada nuevo)
907
1073
  await drainQueues(ctx, cssOut);
908
1074
 
909
- mark('no referenciados');
1075
+ mark('unreferenced');
910
1076
  // 8. Archivos no referenciados
911
1077
  const unref = [];
912
1078
  for (const [rel, f] of ctx.files) {
@@ -916,15 +1082,16 @@ async function pipeline(ctx, cfg, opts, mode, t0) {
916
1082
  for (const rel of unref) {
917
1083
  if (ctx.coverage.has(rel)) continue;
918
1084
  const f = ctx.files.get(rel);
919
- if (cfg.unreferenced === 'process' || f.forced) ctx.requireStandalone(rel, 'no referenciado (se procesa suelto)');
1085
+ if (cfg.unreferenced === 'process' || f.forced)
1086
+ ctx.requireStandalone(rel, L('no referenciado (se procesa suelto)', 'unreferenced (processed on its own)'));
920
1087
  else if (cfg.unreferenced === 'copy') {
921
1088
  ctx.emitCopy(rel, rel);
922
- ctx.cover(rel, 'no referenciado: copiado', rel);
923
- } else ctx.cover(rel, 'no referenciado: omitido (unreferenced: skip)');
1089
+ ctx.cover(rel, L('no referenciado: copiado', 'unreferenced: copied'), rel);
1090
+ } else ctx.cover(rel, L('no referenciado: omitido (unreferenced: skip)', 'unreferenced: skipped (unreferenced: skip)'));
924
1091
  }
925
1092
  await drainQueues(ctx, cssOut);
926
1093
 
927
- mark('html final');
1094
+ mark('final html');
928
1095
  // 9. Preloads y HTML final
929
1096
  for (const { page, l } of preloadLinks) {
930
1097
  const res = ctx.resolve(l.href.value, page.baseDir);
@@ -935,7 +1102,13 @@ async function pipeline(ctx, cfg, opts, mode, t0) {
935
1102
  (ctx.hasOwnOutput(res.rel) || ctx.assets.has(res.rel) ? res.rel : null);
936
1103
  if (target == null) {
937
1104
  page.edits.push({ start: l.start, end: l.end, text: '' });
938
- ctx.note(`se quita <link rel="${l.rels.join(' ')}" href="${l.href.value}">: ese archivo ahora va dentro de un paquete`, page.rel);
1105
+ ctx.note(
1106
+ L(
1107
+ `se quita <link rel="${l.rels.join(' ')}" href="${l.href.value}">: ese archivo ahora va dentro de un paquete`,
1108
+ `removed <link rel="${l.rels.join(' ')}" href="${l.href.value}">: that file is now inside a bundle`,
1109
+ ),
1110
+ page.rel,
1111
+ );
939
1112
  } else if (target !== res.rel) {
940
1113
  page.edits.push({ start: l.href.start, end: l.href.end, text: attrText('href', urlFor(ctx, page, l.href.value, target)) });
941
1114
  }
@@ -944,30 +1117,35 @@ async function pipeline(ctx, cfg, opts, mode, t0) {
944
1117
  for (const page of pages.values()) {
945
1118
  const referenced = page.full || ctx.pageRefs.has(page.rel) || ctx.mentions.has(page.rel) || ctx.files.get(page.rel).forced;
946
1119
  if (!referenced && cfg.unreferenced === 'skip') {
947
- ctx.cover(page.rel, 'no referenciado: omitido (unreferenced: skip)');
1120
+ ctx.cover(page.rel, L('no referenciado: omitido (unreferenced: skip)', 'unreferenced: skipped (unreferenced: skip)'));
948
1121
  continue;
949
1122
  }
950
1123
  if (!referenced && cfg.unreferenced === 'copy') {
951
1124
  ctx.emitCopy(page.rel, page.rel);
952
- ctx.cover(page.rel, 'no referenciado: copiado', page.rel);
1125
+ ctx.cover(page.rel, L('no referenciado: copiado', 'unreferenced: copied'), page.rel);
953
1126
  continue;
954
1127
  }
955
1128
  await finalizePage(ctx, page, collapse);
956
1129
  }
957
1130
 
958
- mark('recursos');
1131
+ mark('assets');
959
1132
  // 10. Recursos: todo lo demás se copia (sin perder nada)
1133
+ const imageJobs = [];
960
1134
  for (const [rel, f] of ctx.files) {
961
1135
  if (ctx.hasOwnOutput(rel)) {
962
- if (!ctx.coverage.has(rel)) ctx.cover(rel, 'procesado', rel);
1136
+ if (!ctx.coverage.has(rel)) ctx.cover(rel, L('procesado', 'processed'), rel);
963
1137
  continue;
964
1138
  }
965
1139
  const cov = ctx.coverage.get(rel);
966
1140
  // Empaquetar solo absorbe JS y CSS; un JSON importado se publica también por si se pide con fetch().
967
- if (cov && (JS_EXT.has(f.ext) || CSS_EXT.has(f.ext) || HTML_EXT.has(f.ext) || cov.some((s) => s.startsWith('no referenciado')))) continue;
1141
+ if (
1142
+ cov &&
1143
+ (JS_EXT.has(f.ext) || CSS_EXT.has(f.ext) || HTML_EXT.has(f.ext) || cov.some((s) => s.startsWith(L('no referenciado', 'unreferenced'))))
1144
+ )
1145
+ continue;
968
1146
  const referenced = ctx.assets.has(rel) || ctx.mentions.has(rel);
969
1147
  if (cfg.unreferenced === 'skip' && !referenced && !f.forced) {
970
- ctx.cover(rel, 'no referenciado: omitido (unreferenced: skip)');
1148
+ ctx.cover(rel, L('no referenciado: omitido (unreferenced: skip)', 'unreferenced: skipped (unreferenced: skip)'));
971
1149
  continue;
972
1150
  }
973
1151
  if ((f.ext === '.json' || f.ext === '.webmanifest') && cfg.json.minify) {
@@ -975,34 +1153,72 @@ async function pipeline(ctx, cfg, opts, mode, t0) {
975
1153
  const min = raw == null ? null : minifyJsonText(stripBom(raw).text);
976
1154
  if (min != null && Buffer.byteLength(min) < f.size) {
977
1155
  ctx.emit(rel, min, { type: 'json', action: 'minify', sources: [rel], before: f.size });
978
- ctx.cover(rel, 'JSON minificado', rel);
1156
+ ctx.cover(rel, L('JSON minificado', 'JSON minified'), rel);
979
1157
  continue;
980
1158
  }
981
1159
  }
1160
+ if (sharp && IMAGE_EXT.has(f.ext)) {
1161
+ imageJobs.push({ rel, f });
1162
+ continue;
1163
+ }
982
1164
  ctx.emitCopy(rel, rel);
983
- ctx.cover(rel, 'copiado', rel);
984
- }
1165
+ ctx.cover(rel, L('copiado', 'copied'), rel);
1166
+ }
1167
+ // Imágenes: en paralelo (sharp usa sus propios hilos) y con caché por contenido; se registran en orden.
1168
+ const optimized = await mapLimit(imageJobs, 8, async ({ rel, f }) => {
1169
+ const buf = fs.readFileSync(f.abs);
1170
+ const r = await ctx.heavy.memo(`image:${f.ext}:q${images.quality}:sharp${sharp.versions?.sharp ?? '?'}`, buf, async () => {
1171
+ const o = await optimizeImage(sharp, buf, f.ext, images);
1172
+ return o.data ? { b64: o.data.toString('base64') } : { skip: o.skip };
1173
+ });
1174
+ return r.b64 ? { data: Buffer.from(r.b64, 'base64') } : r;
1175
+ });
1176
+ imageJobs.forEach(({ rel, f }, i) => {
1177
+ const o = optimized[i];
1178
+ if (o.data) {
1179
+ ctx.emit(rel, o.data, { type: 'asset', action: 'image', sources: [rel], before: f.size });
1180
+ ctx.cover(rel, L('imagen optimizada', 'image optimized'), rel);
1181
+ } else {
1182
+ ctx.emitCopy(rel, rel);
1183
+ ctx.cover(rel, L(`copiado (${o.skip})`, `copied (${o.skip})`), rel);
1184
+ }
1185
+ });
985
1186
 
986
- mark('avisos');
1187
+ mark('warnings');
987
1188
  // 11. Avisos de rutas mencionadas en el código que ya no existirán
988
1189
  for (const [rel, from] of ctx.mentions) {
989
- if (!ctx.outputs.has(rel) && ctx.coverage.get(rel)?.every((s) => s.startsWith('empaquetado'))) {
990
- ctx.warn(`"${rel}" se menciona como texto en ${from} pero ya no existe por separado (va dentro de un paquete).`);
1190
+ if (!ctx.outputs.has(rel) && ctx.coverage.get(rel)?.every((s) => s.startsWith(L('empaquetado', 'bundled')))) {
1191
+ ctx.warn(
1192
+ L(
1193
+ `"${rel}" se menciona como texto en ${from} pero ya no existe por separado (va dentro de un paquete).`,
1194
+ `"${rel}" is mentioned as text in ${from} but no longer exists on its own (it is inside a bundle).`,
1195
+ ),
1196
+ );
991
1197
  }
992
1198
  }
993
1199
  if (cfg.hash) {
994
1200
  for (const [rel, from] of ctx.mentions) {
995
1201
  if ((cssOut.get(rel) ?? rel) !== rel)
996
- ctx.note(`"${rel}" se menciona como texto en ${from}: además de la versión con hash se publica en su ruta original.`);
1202
+ ctx.note(
1203
+ L(
1204
+ `"${rel}" se menciona como texto en ${from}: además de la versión con hash se publica en su ruta original.`,
1205
+ `"${rel}" is mentioned as text in ${from}: besides the hashed version, it is published at its original path.`,
1206
+ ),
1207
+ );
997
1208
  }
998
1209
  }
999
1210
 
1000
- mark('precompresión');
1211
+ mark('precompression');
1001
1212
  // 12. Precompresión .gz / .br para servidores que sirven archivos comprimidos (nginx gzip_static, etc.)
1002
1213
  if (cfg.compress && mode === 'build') await precompress(ctx, cfg.compress === true ? ['gzip', 'brotli'] : [cfg.compress]);
1003
1214
 
1004
- mark('informe de tamaños');
1215
+ mark('size report');
1005
1216
  const report = await buildReport(ctx.outputs, ctx.heavy);
1217
+ const budgets = checkBudgets(cfg.budgets, report);
1218
+ for (const p of budgets.unmatched)
1219
+ ctx.warn(L(`el presupuesto "${p}" no coincide con ningún archivo de salida`, `the budget "${p}" does not match any output file`));
1220
+ // En analyze los excesos son avisos; en build detienen la compilación antes de escribir nada.
1221
+ if (mode === 'analyze') for (const v of budgets.violations) ctx.warn(v);
1006
1222
  const result = {
1007
1223
  version: VERSION,
1008
1224
  root: cfg.root,
@@ -1023,6 +1239,7 @@ async function pipeline(ctx, cfg, opts, mode, t0) {
1023
1239
  vendor: ctx.vendorPackages,
1024
1240
  obfuscated: !!ctx.obfuscatorOptions,
1025
1241
  report,
1242
+ budgets,
1026
1243
  perf: Object.assign(ctx.heavy.stats, { phases }),
1027
1244
  verify: null,
1028
1245
  durationMs: 0,
@@ -1033,8 +1250,16 @@ async function pipeline(ctx, cfg, opts, mode, t0) {
1033
1250
  return result;
1034
1251
  }
1035
1252
 
1036
- mark('escritura y verificación');
1253
+ mark('write and verify');
1037
1254
  // 12. Escritura, verificación y publicación
1255
+ if (budgets.violations.length)
1256
+ throw new BuildError(
1257
+ L(
1258
+ `Se superaron ${budgets.violations.length} presupuesto(s) de tamaño; no se tocó ${cfg.outDir}.`,
1259
+ `${budgets.violations.length} size budget(s) exceeded; ${cfg.outDir} was not touched.`,
1260
+ ),
1261
+ budgets.violations,
1262
+ );
1038
1263
  let staging;
1039
1264
  try {
1040
1265
  staging = await writeStaging(cfg, ctx.outputs);
@@ -1042,7 +1267,13 @@ async function pipeline(ctx, cfg, opts, mode, t0) {
1042
1267
  result.verify = await verifyOutput(staging, ctx.outputs, ctx);
1043
1268
  if (result.verify.errors.length) {
1044
1269
  if (!opts.keepFailed) await discardStaging(staging);
1045
- throw new BuildError(`La verificación encontró ${result.verify.errors.length} problema(s); no se tocó ${cfg.outDir}.`, result.verify.errors);
1270
+ throw new BuildError(
1271
+ L(
1272
+ `La verificación encontró ${result.verify.errors.length} problema(s); no se tocó ${cfg.outDir}.`,
1273
+ `Verification found ${result.verify.errors.length} problem(s); ${cfg.outDir} was not touched.`,
1274
+ ),
1275
+ result.verify.errors,
1276
+ );
1046
1277
  }
1047
1278
  }
1048
1279
  await writeManifest(staging, {
@@ -1052,6 +1283,7 @@ async function pipeline(ctx, cfg, opts, mode, t0) {
1052
1283
  files: [...ctx.outputs.keys()].sort(),
1053
1284
  });
1054
1285
  await swapIntoPlace(cfg, staging);
1286
+ await runHook(cfg.plugins, 'buildEnd', { result, outDir: cfg.outDir });
1055
1287
  mark('fin');
1056
1288
  } catch (err) {
1057
1289
  if (staging && !opts.keepFailed) await discardStaging(staging);
@@ -1095,13 +1327,13 @@ function markModuleImport(ctx, spec, baseDir, from, importMap) {
1095
1327
  .map((k) => importMap.imports[k] + spec.slice(k.length))[0];
1096
1328
  if (target) {
1097
1329
  const r = ctx.resolve(target, importMap.baseDir);
1098
- if (r.status === 'ok') ctx.requireStandalone(r.rel, `importado desde ${from}`, { module: true });
1330
+ if (r.status === 'ok') ctx.requireStandalone(r.rel, L(`importado desde ${from}`, `imported from ${from}`), { module: true });
1099
1331
  }
1100
1332
  }
1101
1333
  return;
1102
1334
  }
1103
1335
  const r = ctx.resolve(spec, baseDir);
1104
- if (r.status === 'ok') ctx.requireStandalone(r.rel, `importado desde ${from}`, { module: true });
1336
+ if (r.status === 'ok') ctx.requireStandalone(r.rel, L(`importado desde ${from}`, `imported from ${from}`), { module: true });
1105
1337
  else ctx.reportBroken(from, spec, r);
1106
1338
  }
1107
1339
 
@@ -1132,9 +1364,12 @@ function commitClassicRun(ctx, run, page, comp) {
1132
1364
  const items = run.items;
1133
1365
  const first = items[0];
1134
1366
  if (comp.copy) {
1135
- ctx.warn(`no se pudo analizar (${first.info.error}); se copia sin cambios`, first.rel);
1367
+ ctx.warn(
1368
+ L(`no se pudo analizar (${first.info.error}); se copia sin cambios`, `could not be parsed (${first.info.error}); copied unchanged`),
1369
+ first.rel,
1370
+ );
1136
1371
  const outRel = ctx.emitCopy(first.rel, first.rel, 'copy-fallback');
1137
- ctx.cover(first.rel, 'copiado sin cambios (no se pudo analizar)', outRel);
1372
+ ctx.cover(first.rel, L('copiado sin cambios (no se pudo analizar)', 'copied unchanged (could not be parsed)'), outRel);
1138
1373
  return { merged: false, outs: [{ outRel, changed: false }] };
1139
1374
  }
1140
1375
  {
@@ -1150,20 +1385,25 @@ function commitClassicRun(ctx, run, page, comp) {
1150
1385
  items.map((c) => c.rel),
1151
1386
  ),
1152
1387
  });
1153
- for (const c of items) ctx.cover(c.rel, 'unido', outRel);
1388
+ for (const c of items) ctx.cover(c.rel, L('unido', 'merged'), outRel);
1154
1389
  return { merged: true, outRel };
1155
1390
  }
1156
- if (comp.mergeError) ctx.warn(`no se pudieron unir (${comp.mergeError}); se procesan por separado`, page.rel);
1391
+ if (comp.mergeError)
1392
+ ctx.warn(
1393
+ L(`no se pudieron unir (${comp.mergeError}); se procesan por separado`, `could not be merged (${comp.mergeError}); processed separately`),
1394
+ page.rel,
1395
+ );
1157
1396
  }
1158
1397
  // Un solo archivo, o una unión fallida: cada uno se procesa en su sitio.
1159
1398
  const outs = [];
1160
1399
  for (const [i, c] of items.entries()) {
1161
1400
  const r = comp.separate[i];
1162
1401
  for (const n of r.notes) ctx.note(n, c.rel);
1163
- if (c.reasons.length && c.reasons[0] !== 'unión desactivada') ctx.note(`no se une con otros scripts: ${c.reasons.join(', ')}`, c.rel);
1402
+ if (c.reasons.length && c.reasons[0] !== L('unión desactivada', 'merging disabled'))
1403
+ ctx.note(L(`no se une con otros scripts: ${c.reasons.join(', ')}`, `not merged with other scripts: ${c.reasons.join(', ')}`), c.rel);
1164
1404
  const outRel = ctx.cfg.hash ? ctx.hashedPath(c.rel, r.code, '.js') : jsOutPath(c.rel);
1165
1405
  ctx.emitCode(outRel, r.code, r.map, { type: 'js', action: 'minify', sources: [c.rel], before: ctx.files.get(c.rel).size });
1166
- ctx.cover(c.rel, 'procesado', outRel);
1406
+ ctx.cover(c.rel, L('procesado', 'processed'), outRel);
1167
1407
  outs.push({ outRel, changed: true });
1168
1408
  }
1169
1409
  return { merged: false, outs };
@@ -1176,7 +1416,7 @@ async function processInline(ctx, page) {
1176
1416
  if (s.src) {
1177
1417
  if (!page.full) {
1178
1418
  const res = ctx.resolve(s.src.value, page.baseDir);
1179
- if (res.status === 'ok') ctx.requireStandalone(res.rel, `script en ${page.rel}`, { module: s.kind === 'module' });
1419
+ if (res.status === 'ok') ctx.requireStandalone(res.rel, L(`script en ${page.rel}`, `script in ${page.rel}`), { module: s.kind === 'module' });
1180
1420
  else if (res.status !== 'skip') ctx.reportBroken(page.rel, s.src.value, res);
1181
1421
  }
1182
1422
  continue;
@@ -1185,7 +1425,10 @@ async function processInline(ctx, page) {
1185
1425
  if (s.kind === 'classic') {
1186
1426
  const info = analyzeJs(s.inline.code, { module: false });
1187
1427
  if (!info.ok) {
1188
- ctx.note(`un <script> inline no es JS estándar (¿plantilla?); se deja igual`, page.rel);
1428
+ ctx.note(
1429
+ L(`un <script> inline no es JS estándar (¿plantilla?); se deja igual`, `an inline <script> is not standard JS (a template?); left as is`),
1430
+ page.rel,
1431
+ );
1189
1432
  continue;
1190
1433
  }
1191
1434
  ctx.noteJsInfo(page.rel, info, page.baseDir);
@@ -1215,7 +1458,7 @@ async function processInline(ctx, page) {
1215
1458
  for (const l of page.model.links) {
1216
1459
  if (l.type !== 'stylesheet') continue;
1217
1460
  const res = ctx.resolve(l.href.value, page.baseDir);
1218
- if (res.status === 'ok') ctx.requireStandalone(res.rel, `hoja de estilos en ${page.rel}`);
1461
+ if (res.status === 'ok') ctx.requireStandalone(res.rel, L(`hoja de estilos en ${page.rel}`, `stylesheet in ${page.rel}`));
1219
1462
  else if (res.status !== 'skip') ctx.reportBroken(page.rel, l.href.value, res);
1220
1463
  }
1221
1464
  }
@@ -1227,7 +1470,13 @@ async function processInline(ctx, page) {
1227
1470
  outRel: page.baseDir ? `${page.baseDir}/__page.html` : '__page.html',
1228
1471
  });
1229
1472
  if (r.error) {
1230
- ctx.note(`un <style> no se pudo minificar (${r.error.split('\n')[0]}); se deja igual`, page.rel);
1473
+ ctx.note(
1474
+ L(
1475
+ `un <style> no se pudo minificar (${r.error.split('\n')[0]}); se deja igual`,
1476
+ `a <style> could not be minified (${r.error.split('\n')[0]}); left as is`,
1477
+ ),
1478
+ page.rel,
1479
+ );
1231
1480
  continue;
1232
1481
  }
1233
1482
  if (r.code !== st.code) page.edits.push({ start: st.start, end: st.end, text: escapeInlineStyle(r.code) });
@@ -1264,7 +1513,7 @@ async function finalizePage(ctx, page, collapse) {
1264
1513
  baseDir: page.baseDir,
1265
1514
  baseExternal: page.baseExternal,
1266
1515
  });
1267
- ctx.cover(page.rel, page.full ? 'procesado' : 'procesado (modo conservador)', page.rel);
1516
+ ctx.cover(page.rel, page.full ? L('procesado', 'processed') : L('procesado (modo conservador)', 'processed (conservative mode)'), page.rel);
1268
1517
  }
1269
1518
 
1270
1519
  /** Procesa workers y archivos que deben existir sueltos en su ruta original, hasta agotar la cola. */
@@ -1275,7 +1524,7 @@ async function drainQueues(ctx, cssOut) {
1275
1524
  for (const [rel] of ctx.mentions) {
1276
1525
  const ext = extOf(rel);
1277
1526
  if ((JS_EXT.has(ext) || CSS_EXT.has(ext)) && !ctx.hasOwnOutput(rel) && !ctx.standalone.has(rel))
1278
- ctx.requireStandalone(rel, `mencionado como texto en ${ctx.mentions.get(rel)}`);
1527
+ ctx.requireStandalone(rel, L(`mencionado como texto en ${ctx.mentions.get(rel)}`, `mentioned as text in ${ctx.mentions.get(rel)}`));
1279
1528
  else if (!JS_EXT.has(ext) && !CSS_EXT.has(ext) && !HTML_EXT.has(ext)) ctx.registerAsset(rel, ctx.mentions.get(rel));
1280
1529
  }
1281
1530
  for (const [rel, w] of ctx.workers) {
@@ -1286,7 +1535,13 @@ async function drainQueues(ctx, cssOut) {
1286
1535
  if (w.module) {
1287
1536
  const r = await buildModuleWorker(ctx, rel);
1288
1537
  if (r.error) {
1289
- ctx.warn(`no se pudo empaquetar el worker (${r.error.split('\n')[0]}); se procesa suelto`, rel);
1538
+ ctx.warn(
1539
+ L(
1540
+ `no se pudo empaquetar el worker (${r.error.split('\n')[0]}); se procesa suelto`,
1541
+ `could not bundle the worker (${r.error.split('\n')[0]}); processed on its own`,
1542
+ ),
1543
+ rel,
1544
+ );
1290
1545
  ctx.requireStandalone(rel, 'worker (respaldo)', { module: true });
1291
1546
  continue;
1292
1547
  }
@@ -1304,8 +1559,9 @@ async function drainQueues(ctx, cssOut) {
1304
1559
  sources,
1305
1560
  before: sumSizes(ctx, sources),
1306
1561
  });
1307
- for (const s of sources) ctx.cover(s, s === rel ? 'procesado (worker)' : 'empaquetado', jsOutPath(rel));
1308
- } else ctx.requireStandalone(rel, `worker clásico (${w.kind})`, { classic: true });
1562
+ for (const s of sources)
1563
+ ctx.cover(s, s === rel ? L('procesado (worker)', 'processed (worker)') : L('empaquetado', 'bundled'), jsOutPath(rel));
1564
+ } else ctx.requireStandalone(rel, L(`worker clásico (${w.kind})`, `classic worker (${w.kind})`), { classic: true });
1309
1565
  }
1310
1566
  // Análisis en orden, trabajo pesado en paralelo, registro otra vez en orden.
1311
1567
  const jobs = [];
@@ -1321,7 +1577,9 @@ async function drainQueues(ctx, cssOut) {
1321
1577
  jobs.forEach((j, i) => j.commit(results[i]));
1322
1578
  if (!progressed) return;
1323
1579
  }
1324
- throw new BuildError('Demasiadas rondas al resolver dependencias (¿ciclo inesperado?).');
1580
+ throw new BuildError(
1581
+ L('Demasiadas rondas al resolver dependencias (¿ciclo inesperado?).', 'Too many rounds resolving dependencies (unexpected cycle?).'),
1582
+ );
1325
1583
  }
1326
1584
 
1327
1585
  /**
@@ -1337,13 +1595,19 @@ function planStandalone(ctx, rel, s, cssOut) {
1337
1595
  run: () => buildCss(ctx, { entry: rel, outRel: rel }),
1338
1596
  commit: (r) => {
1339
1597
  if (r.error) {
1340
- ctx.warn(`no se pudo procesar el CSS (${r.error.split('\n')[0]}); se copia tal cual`, rel);
1598
+ ctx.warn(
1599
+ L(
1600
+ `no se pudo procesar el CSS (${r.error.split('\n')[0]}); se copia tal cual`,
1601
+ `could not process the CSS (${r.error.split('\n')[0]}); copied as is`,
1602
+ ),
1603
+ rel,
1604
+ );
1341
1605
  ctx.emitCopy(rel, rel, 'copy-fallback');
1342
- ctx.cover(rel, 'copiado sin cambios (error al procesar)', rel);
1606
+ ctx.cover(rel, L('copiado sin cambios (error al procesar)', 'copied unchanged (processing error)'), rel);
1343
1607
  return;
1344
1608
  }
1345
1609
  ctx.emitCode(rel, r.code, r.map, { type: 'css', action: 'standalone', sources: [rel], before: ctx.files.get(rel).size });
1346
- for (const i of r.inputs) ctx.cover(i, i === rel ? 'procesado (suelto)' : 'empaquetado', rel);
1610
+ for (const i of r.inputs) ctx.cover(i, i === rel ? L('procesado (suelto)', 'processed (standalone)') : L('empaquetado', 'bundled'), rel);
1347
1611
  if (!cssOut.has(rel)) cssOut.set(rel, rel);
1348
1612
  },
1349
1613
  };
@@ -1356,9 +1620,9 @@ function planStandalone(ctx, rel, s, cssOut) {
1356
1620
  const isModule = !s.classic && (s.module || (auto.ok && (auto.hasModuleSyntax || auto.hasImportMeta)) || (!auto.ok && auto.isModule));
1357
1621
  const info = isModule ? ctx.jsInfo(rel, true) : ctx.jsInfo(rel, false);
1358
1622
  if (!info.ok) {
1359
- ctx.warn(`no se pudo analizar (${info.error}); se copia sin cambios`, rel);
1623
+ ctx.warn(L(`no se pudo analizar (${info.error}); se copia sin cambios`, `could not be parsed (${info.error}); copied unchanged`), rel);
1360
1624
  ctx.emitCopy(rel, rel, 'copy-fallback');
1361
- ctx.cover(rel, 'copiado sin cambios (no se pudo analizar)', rel);
1625
+ ctx.cover(rel, L('copiado sin cambios (no se pudo analizar)', 'copied unchanged (could not be parsed)'), rel);
1362
1626
  return null;
1363
1627
  }
1364
1628
  ctx.noteJsInfo(rel, info, dir);
@@ -1379,7 +1643,7 @@ function planStandalone(ctx, rel, s, cssOut) {
1379
1643
  for (const n of r.notes) ctx.note(n, rel);
1380
1644
  const out = jsOutPath(rel);
1381
1645
  ctx.emitCode(out, r.code, r.map, { type: 'js', module: isModule, action: 'standalone', sources: [rel], before: ctx.files.get(rel).size });
1382
- ctx.cover(rel, `procesado suelto (${s.reason})`, out);
1646
+ ctx.cover(rel, L(`procesado suelto (${s.reason})`, `processed standalone (${s.reason})`), out);
1383
1647
  },
1384
1648
  };
1385
1649
  }