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/CHANGELOG.md +41 -0
- package/README.es.md +553 -107
- package/README.md +545 -101
- package/bin/builder.js +432 -46
- package/package.json +21 -4
- package/src/browsers.js +128 -0
- package/src/budgets.js +77 -0
- package/src/builder.js +612 -165
- package/src/compress.js +44 -0
- package/src/config.js +280 -190
- package/src/deps.js +70 -6
- package/src/files.js +19 -7
- package/src/html.js +10 -3
- package/src/i18n.js +33 -0
- package/src/images.js +61 -0
- package/src/js.js +123 -74
- package/src/jsimports.js +54 -0
- package/src/jsinfo.js +81 -6
- package/src/obfuscate.js +8 -4
- package/src/optional.js +7 -3
- package/src/output.js +37 -9
- package/src/plugins.js +126 -0
- package/src/pool.js +343 -0
- package/src/report.js +23 -22
- package/src/server.js +8 -4
- package/src/tersercore.js +33 -0
- package/src/urls.js +3 -0
- package/src/util.js +5 -1
- package/src/verify.js +37 -27
- package/src/worker.js +30 -0
package/src/builder.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Orquestador: descubre dependencias, procesa cada tipo de archivo, verifica y publica la salida.
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import
|
|
4
|
+
import { createHeavy, defaultCacheDir, autoWorkers } from './pool.js';
|
|
5
|
+
import { gzipOf, brotliOf, mapLimit } from './compress.js';
|
|
5
6
|
import { scanProject, matchFiles, globMatcher } from './files.js';
|
|
6
7
|
import { resolveRef, classifyUrl, parseSrcset } from './urls.js';
|
|
7
8
|
import { analyzeJs } from './jsinfo.js';
|
|
@@ -18,9 +19,13 @@ import {
|
|
|
18
19
|
levelOf,
|
|
19
20
|
} from './js.js';
|
|
20
21
|
import { resolveObfuscatorOptions, ensureObfuscator } from './obfuscate.js';
|
|
21
|
-
import { detectDependency } from './deps.js';
|
|
22
|
+
import { detectDependency, dependencyPragma, packageNames } from './deps.js';
|
|
22
23
|
import { assertSafeOutDir, writeStaging, swapIntoPlace, discardStaging, writeManifest, MANIFEST } from './output.js';
|
|
23
24
|
import { verifyOutput } from './verify.js';
|
|
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';
|
|
24
29
|
import { buildReport } from './report.js';
|
|
25
30
|
import {
|
|
26
31
|
BuildError,
|
|
@@ -29,6 +34,8 @@ import {
|
|
|
29
34
|
BOM,
|
|
30
35
|
extOf,
|
|
31
36
|
JS_EXT,
|
|
37
|
+
TS_EXT,
|
|
38
|
+
jsOutPath,
|
|
32
39
|
CSS_EXT,
|
|
33
40
|
HTML_EXT,
|
|
34
41
|
shortHash,
|
|
@@ -37,10 +44,11 @@ import {
|
|
|
37
44
|
minifyJsonText,
|
|
38
45
|
hasOwn,
|
|
39
46
|
} from './util.js';
|
|
47
|
+
import { L } from './i18n.js';
|
|
40
48
|
|
|
41
49
|
export const VERSION = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
42
50
|
|
|
43
|
-
const MERGE_ATTRS = new Set(['src', 'type', 'defer', 'charset', 'language']);
|
|
51
|
+
const MERGE_ATTRS = new Set(['src', 'type', 'defer', 'charset', 'language', 'data-builder']);
|
|
44
52
|
const typeOfExt = (ext) =>
|
|
45
53
|
HTML_EXT.has(ext)
|
|
46
54
|
? 'html'
|
|
@@ -88,6 +96,10 @@ class Context {
|
|
|
88
96
|
this.depIncluded = globMatcher(cfg.dependencies.include);
|
|
89
97
|
this.depExcluded = globMatcher(cfg.dependencies.exclude);
|
|
90
98
|
this.depCache = new Map();
|
|
99
|
+
this.depMarks = new Map();
|
|
100
|
+
this.tsCode = new Map();
|
|
101
|
+
this.sourceOverrides = new Map();
|
|
102
|
+
this.pkgNames = null;
|
|
91
103
|
this.vendorPackages = [];
|
|
92
104
|
this.chunkDir = cfg.js.chunkDir.replace(/\/+$/, '');
|
|
93
105
|
}
|
|
@@ -122,6 +134,10 @@ class Context {
|
|
|
122
134
|
}
|
|
123
135
|
|
|
124
136
|
readText(rel, { kind = 'text', charset } = {}) {
|
|
137
|
+
// TypeScript: como JS se lee la versión ya transpilada (sin tipos; ya incluye los cambios de los plugins).
|
|
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' };
|
|
125
141
|
const key = `${rel}\0${kind}`;
|
|
126
142
|
if (this.textCache.has(key)) return this.textCache.get(key);
|
|
127
143
|
const buf = fs.readFileSync(this.abs(rel));
|
|
@@ -132,7 +148,13 @@ class Context {
|
|
|
132
148
|
// latin1 conserva cada byte tal cual: lo que no se edita sale idéntico.
|
|
133
149
|
text = buf.toString('latin1');
|
|
134
150
|
encoding = 'latin1';
|
|
135
|
-
this.warn(
|
|
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
|
+
);
|
|
136
158
|
} else {
|
|
137
159
|
let label = (charset || 'windows-1252').toLowerCase();
|
|
138
160
|
try {
|
|
@@ -142,7 +164,13 @@ class Context {
|
|
|
142
164
|
text = new TextDecoder(label).decode(buf);
|
|
143
165
|
}
|
|
144
166
|
encoding = label;
|
|
145
|
-
this.note(
|
|
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
|
+
);
|
|
146
174
|
}
|
|
147
175
|
}
|
|
148
176
|
const stripped = stripBom(text);
|
|
@@ -163,17 +191,36 @@ class Context {
|
|
|
163
191
|
if (this.brokenKeys.has(key)) return;
|
|
164
192
|
this.brokenKeys.add(key);
|
|
165
193
|
this.broken.push({ from, url, status: r.status, target: r.rel });
|
|
166
|
-
if (r.status === 'ignored')
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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);
|
|
170
217
|
}
|
|
171
218
|
|
|
172
219
|
registerAsset(rel, from) {
|
|
173
220
|
if (!this.assets.has(rel)) this.assets.set(rel, new Set());
|
|
174
221
|
if (from) this.assets.get(rel).add(from);
|
|
175
222
|
const ext = extOf(rel);
|
|
176
|
-
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}`));
|
|
177
224
|
}
|
|
178
225
|
|
|
179
226
|
assetOut(rel) {
|
|
@@ -184,9 +231,9 @@ class Context {
|
|
|
184
231
|
urlTarget(rel, from) {
|
|
185
232
|
const ext = extOf(rel);
|
|
186
233
|
if (JS_EXT.has(ext) || CSS_EXT.has(ext)) {
|
|
187
|
-
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}`));
|
|
188
235
|
} else this.registerAsset(rel, from);
|
|
189
|
-
return rel;
|
|
236
|
+
return jsOutPath(rel);
|
|
190
237
|
}
|
|
191
238
|
|
|
192
239
|
requireStandalone(rel, reason, extra = {}) {
|
|
@@ -197,7 +244,14 @@ class Context {
|
|
|
197
244
|
registerWorker(rel, info) {
|
|
198
245
|
const prev = this.workers.get(rel);
|
|
199
246
|
if (prev) {
|
|
200
|
-
if (prev.module !== info.module)
|
|
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
|
+
);
|
|
201
255
|
prev.module = prev.module || info.module;
|
|
202
256
|
return;
|
|
203
257
|
}
|
|
@@ -208,7 +262,13 @@ class Context {
|
|
|
208
262
|
noteJsInfo(id, info, baseDir) {
|
|
209
263
|
for (const w of info.workers) {
|
|
210
264
|
if (!w.spec) {
|
|
211
|
-
this.warn(
|
|
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
|
+
);
|
|
212
272
|
continue;
|
|
213
273
|
}
|
|
214
274
|
const c = classifyUrl(w.spec);
|
|
@@ -219,7 +279,7 @@ class Context {
|
|
|
219
279
|
this.reportBroken(id, w.spec, this.resolve(w.spec, baseDir));
|
|
220
280
|
continue;
|
|
221
281
|
}
|
|
222
|
-
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 });
|
|
223
283
|
else this.registerWorker(rel, { module: w.module, kind: w.kind, from: id });
|
|
224
284
|
}
|
|
225
285
|
for (const s of info.strings) {
|
|
@@ -235,23 +295,53 @@ class Context {
|
|
|
235
295
|
}
|
|
236
296
|
|
|
237
297
|
hasOwnOutput(rel) {
|
|
238
|
-
const o = this.outputs.get(rel);
|
|
298
|
+
const o = this.outputs.get(rel) ?? (TS_EXT.has(extOf(rel)) ? this.outputs.get(jsOutPath(rel)) : undefined);
|
|
239
299
|
return !!o && o.sources?.includes(rel);
|
|
240
300
|
}
|
|
241
301
|
|
|
242
302
|
/** ¿Es una librería de terceros? (a mano en dependencies.include/exclude, o detectada). */
|
|
303
|
+
// Prioridad: configuración (include/exclude) > marca en el HTML o en el archivo > detección automática.
|
|
243
304
|
dependencyReason(rel) {
|
|
244
305
|
if (this.depCache.has(rel)) return this.depCache.get(rel);
|
|
245
306
|
const deps = this.cfg.dependencies;
|
|
307
|
+
const isJs = JS_EXT.has(extOf(rel)) && this.files.has(rel);
|
|
308
|
+
const text = isJs ? this.readText(rel, { kind: 'js' }).text : '';
|
|
309
|
+
const pragma = isJs ? dependencyPragma(text) : null;
|
|
310
|
+
const mark = this.depMarks.get(rel);
|
|
246
311
|
let reason = null;
|
|
247
312
|
if (this.depExcluded(rel)) reason = null;
|
|
248
313
|
else if (this.depIncluded(rel)) reason = 'marcado a mano (dependencies.include)';
|
|
249
|
-
else if (
|
|
250
|
-
else if (
|
|
314
|
+
else if (mark === 'own' || pragma === 'own') reason = null;
|
|
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');
|
|
319
|
+
else if (deps.detect && isJs) reason = detectDependency(rel, text, { packages: this.packageHints() });
|
|
251
320
|
this.depCache.set(rel, reason);
|
|
252
321
|
return reason;
|
|
253
322
|
}
|
|
254
323
|
|
|
324
|
+
/** Paquetes del package.json más cercano (para reconocer librerías copiadas a mano). */
|
|
325
|
+
packageHints() {
|
|
326
|
+
if (this.pkgNames) return this.pkgNames;
|
|
327
|
+
this.pkgNames = new Map();
|
|
328
|
+
for (let dir = this.root, i = 0; i < 10; i++) {
|
|
329
|
+
const file = path.join(dir, 'package.json');
|
|
330
|
+
if (fs.existsSync(file)) {
|
|
331
|
+
try {
|
|
332
|
+
this.pkgNames = packageNames(JSON.parse(fs.readFileSync(file, 'utf8')));
|
|
333
|
+
} catch {
|
|
334
|
+
// package.json ilegible: no aporta pistas
|
|
335
|
+
}
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
const parent = path.dirname(dir);
|
|
339
|
+
if (parent === dir) break;
|
|
340
|
+
dir = parent;
|
|
341
|
+
}
|
|
342
|
+
return this.pkgNames;
|
|
343
|
+
}
|
|
344
|
+
|
|
255
345
|
shouldObfuscate(rels) {
|
|
256
346
|
if (!this.obfuscatorOptions) return false;
|
|
257
347
|
const list = rels.filter(Boolean);
|
|
@@ -263,23 +353,35 @@ class Context {
|
|
|
263
353
|
/** Registra un archivo de salida; nunca sobrescribe en silencio un contenido distinto. */
|
|
264
354
|
emit(outRel, data, meta) {
|
|
265
355
|
const norm = path.posix.normalize(outRel);
|
|
266
|
-
if (norm.startsWith('../') || path.posix.isAbsolute(norm) || norm === MANIFEST)
|
|
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}`));
|
|
267
358
|
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data, meta.encoding === 'latin1' ? 'latin1' : 'utf8');
|
|
268
359
|
const prev = this.outputs.get(norm);
|
|
269
360
|
if (prev) {
|
|
270
361
|
const same = prev.copyFrom ? fs.readFileSync(prev.copyFrom).equals(buf) : prev.data.equals(buf);
|
|
271
362
|
if (!same)
|
|
272
|
-
throw new BuildError(
|
|
273
|
-
`
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
+
);
|
|
277
374
|
prev.sources = [...new Set([...(prev.sources ?? []), ...(meta.sources ?? [])])];
|
|
278
375
|
return norm;
|
|
279
376
|
}
|
|
280
377
|
const lower = norm.toLowerCase();
|
|
281
378
|
if (this.outLower.has(lower) && this.outLower.get(lower) !== norm)
|
|
282
|
-
this.warn(
|
|
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
|
+
);
|
|
283
385
|
this.outLower.set(lower, norm);
|
|
284
386
|
this.outputs.set(norm, { ...meta, data: buf });
|
|
285
387
|
return norm;
|
|
@@ -287,6 +389,8 @@ class Context {
|
|
|
287
389
|
|
|
288
390
|
emitCopy(outRel, rel, action = 'copy') {
|
|
289
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] });
|
|
290
394
|
const prev = this.outputs.get(outRel);
|
|
291
395
|
if (prev) {
|
|
292
396
|
if (prev.sources?.includes(rel)) return outRel;
|
|
@@ -348,7 +452,7 @@ function loadPages(ctx) {
|
|
|
348
452
|
try {
|
|
349
453
|
model = parseHtml(text);
|
|
350
454
|
} catch (err) {
|
|
351
|
-
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);
|
|
352
456
|
continue;
|
|
353
457
|
}
|
|
354
458
|
const dir = path.posix.dirname(rel) === '.' ? '' : path.posix.dirname(rel);
|
|
@@ -357,7 +461,13 @@ function loadPages(ctx) {
|
|
|
357
461
|
const c = classifyUrl(model.base);
|
|
358
462
|
if (c.type === 'external') {
|
|
359
463
|
page.baseExternal = true;
|
|
360
|
-
ctx.warn(
|
|
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
|
+
);
|
|
361
471
|
} else if (c.type === 'root' || c.type === 'relative') {
|
|
362
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);
|
|
363
473
|
page.baseDir = path.posix
|
|
@@ -372,6 +482,93 @@ function loadPages(ctx) {
|
|
|
372
482
|
}
|
|
373
483
|
|
|
374
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
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Transpila cada archivo TypeScript a JavaScript (solo quita los tipos: no baja el nivel de sintaxis).
|
|
501
|
+
* Usa el tsconfig.json del proyecto si existe. Lo que falle se avisa y sigue el camino normal
|
|
502
|
+
* (se copia sin cambios), así que nunca se pierde nada.
|
|
503
|
+
*/
|
|
504
|
+
async function transpileTypeScript(ctx) {
|
|
505
|
+
const list = [...ctx.files.keys()].filter((rel) => TS_EXT.has(extOf(rel)));
|
|
506
|
+
if (!list.length) return;
|
|
507
|
+
let tsconfigRaw;
|
|
508
|
+
const tsconfig = path.join(ctx.root, 'tsconfig.json');
|
|
509
|
+
if (fs.existsSync(tsconfig)) tsconfigRaw = fs.readFileSync(tsconfig, 'utf8');
|
|
510
|
+
const esbuild = await import('esbuild');
|
|
511
|
+
await mapLimit(list, 32, async (rel) => {
|
|
512
|
+
const source = ctx.readText(rel, { kind: 'ts' }).text;
|
|
513
|
+
try {
|
|
514
|
+
const r = await esbuild.transform(source, {
|
|
515
|
+
loader: 'ts',
|
|
516
|
+
sourcefile: rel,
|
|
517
|
+
target: 'esnext',
|
|
518
|
+
tsconfigRaw,
|
|
519
|
+
logLevel: 'silent',
|
|
520
|
+
supported: { 'import-attributes': true },
|
|
521
|
+
});
|
|
522
|
+
const dir = path.posix.dirname(rel);
|
|
523
|
+
// "./x" → la ruta que existirá en la salida (x.ts → x.js, x/index.ts → x/index.js…).
|
|
524
|
+
const complete = (spec) => {
|
|
525
|
+
const base = path.posix.normalize(path.posix.join(dir, spec));
|
|
526
|
+
for (const cand of ['.ts', '.mts', '.js', '.mjs', '/index.ts', '/index.js'].map((s) => base + s)) {
|
|
527
|
+
if (ctx.files.has(cand)) return spec + jsOutPath(cand).slice(base.length);
|
|
528
|
+
}
|
|
529
|
+
return null;
|
|
530
|
+
};
|
|
531
|
+
ctx.tsCode.set(rel, rewriteTsImports(r.code, complete));
|
|
532
|
+
} catch (err) {
|
|
533
|
+
const msg = String(err.errors?.[0]?.text ?? err.message).split('\n')[0];
|
|
534
|
+
ctx.warn(L(`TypeScript no válido (${msg}); se copia sin transpilar`, `invalid TypeScript (${msg}); copied without transpiling`), rel);
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** <script src="..." data-builder="dependency|own">: el usuario marca librerías (o código propio) en el HTML. */
|
|
540
|
+
const MARK_VALUES = { dependency: 'dependency', dependencia: 'dependency', dep: 'dependency', own: 'own', propio: 'own' };
|
|
541
|
+
function markDependencies(ctx, pages) {
|
|
542
|
+
for (const page of pages.values()) {
|
|
543
|
+
for (const s of page.model.scripts) {
|
|
544
|
+
if (!s.src || !s.attrs.has('data-builder')) continue;
|
|
545
|
+
const raw = String(s.attrs.get('data-builder') ?? '')
|
|
546
|
+
.trim()
|
|
547
|
+
.toLowerCase();
|
|
548
|
+
const value = MARK_VALUES[raw];
|
|
549
|
+
if (!value) {
|
|
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
|
+
);
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
const res = ctx.resolve(s.src.value, page.baseDir);
|
|
557
|
+
if (res.status !== 'ok') continue;
|
|
558
|
+
const prev = ctx.depMarks.get(res.rel);
|
|
559
|
+
if (prev && prev !== value)
|
|
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
|
+
);
|
|
567
|
+
else ctx.depMarks.set(res.rel, value);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
375
572
|
function selectEntries(ctx, pages) {
|
|
376
573
|
const cfg = ctx.cfg;
|
|
377
574
|
const extraEntries = { modules: [], classic: [], css: [] };
|
|
@@ -380,7 +577,7 @@ function selectEntries(ctx, pages) {
|
|
|
380
577
|
queue = [...pages.values()].filter((p) => p.model.isDocument).map((p) => p.rel);
|
|
381
578
|
} else {
|
|
382
579
|
const { matched, missing } = matchFiles(ctx.files, cfg.entries);
|
|
383
|
-
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);
|
|
384
581
|
for (const rel of matched) {
|
|
385
582
|
const ext = extOf(rel);
|
|
386
583
|
if (HTML_EXT.has(ext)) queue.push(rel);
|
|
@@ -388,7 +585,11 @@ function selectEntries(ctx, pages) {
|
|
|
388
585
|
else if (JS_EXT.has(ext)) {
|
|
389
586
|
const info = ctx.jsInfo(rel);
|
|
390
587
|
(info.ok && (info.hasModuleSyntax || info.hasImportMeta) ? extraEntries.modules : extraEntries.classic).push(rel);
|
|
391
|
-
} else
|
|
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
|
+
);
|
|
392
593
|
}
|
|
393
594
|
}
|
|
394
595
|
const full = new Set();
|
|
@@ -447,7 +648,10 @@ function parseImportMap(ctx, page, item) {
|
|
|
447
648
|
const json = JSON.parse(item.inline.code);
|
|
448
649
|
return { json, imports: json.imports ?? {}, scopes: json.scopes, baseDir: page.baseDir };
|
|
449
650
|
} catch (err) {
|
|
450
|
-
ctx.warn(
|
|
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
|
+
);
|
|
451
655
|
return null;
|
|
452
656
|
}
|
|
453
657
|
}
|
|
@@ -478,16 +682,23 @@ function classicRuns(ctx, page) {
|
|
|
478
682
|
continue;
|
|
479
683
|
}
|
|
480
684
|
const rel = res.rel;
|
|
481
|
-
if (!JS_EXT.has(extOf(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
|
+
);
|
|
482
693
|
const info = ctx.jsInfo(rel, false);
|
|
483
694
|
const attrs = s.attrs;
|
|
484
695
|
const reasons = [];
|
|
485
|
-
if (!mergeOn) reasons.push('unión desactivada');
|
|
486
|
-
if (!info.ok) reasons.push('no se pudo analizar');
|
|
487
|
-
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>'));
|
|
488
699
|
if (attrs.has('async')) reasons.push('async');
|
|
489
|
-
if (info.usesCurrentScript) reasons.push('usa document.currentScript');
|
|
490
|
-
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'));
|
|
491
702
|
const extra = [...attrs.keys()].filter((a) => !MERGE_ATTRS.has(a));
|
|
492
703
|
if (extra.length) reasons.push(`atributos ${extra.join(', ')}`);
|
|
493
704
|
const obf = ctx.shouldObfuscate([rel]);
|
|
@@ -534,20 +745,77 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
534
745
|
const t0 = Date.now();
|
|
535
746
|
if (mode === 'build') assertSafeOutDir(cfg, { force: opts.force });
|
|
536
747
|
const ctx = new Context(cfg, { ...opts, mode });
|
|
748
|
+
const workers = cfg.workers === 'auto' ? autoWorkers() : cfg.workers;
|
|
749
|
+
ctx.heavy = createHeavy({ root: cfg.root, workers, cacheDir: cfg.cache && mode === 'build' ? defaultCacheDir(cfg.root) : null });
|
|
750
|
+
try {
|
|
751
|
+
return await pipeline(ctx, cfg, opts, mode, t0);
|
|
752
|
+
} finally {
|
|
753
|
+
await ctx.heavy.close();
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
async function pipeline(ctx, cfg, opts, mode, t0) {
|
|
537
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
|
+
);
|
|
783
|
+
// Tiempo de cada fase (perf.phases): para saber dónde se va el tiempo en proyectos reales.
|
|
784
|
+
const phases = [];
|
|
785
|
+
let phaseStart = Date.now();
|
|
786
|
+
let phaseName = 'setup';
|
|
787
|
+
const mark = (next) => {
|
|
788
|
+
const now = Date.now();
|
|
789
|
+
phases.push([phaseName, now - phaseStart]);
|
|
790
|
+
phaseName = next;
|
|
791
|
+
phaseStart = now;
|
|
792
|
+
};
|
|
538
793
|
if (ctx.obfuscatorOptions) await ensureObfuscator(cfg.root);
|
|
539
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;
|
|
540
797
|
|
|
798
|
+
mark('pages');
|
|
541
799
|
// 1. Páginas y entradas
|
|
800
|
+
await runHook(cfg.plugins, 'buildStart', { config: cfg, mode });
|
|
801
|
+
await applyTransforms(ctx);
|
|
802
|
+
await transpileTypeScript(ctx);
|
|
542
803
|
const pages = loadPages(ctx);
|
|
804
|
+
markDependencies(ctx, pages);
|
|
543
805
|
const extra = selectEntries(ctx, pages);
|
|
544
806
|
const fullPages = [...pages.values()].filter((p) => p.full);
|
|
545
807
|
const partialPages = [...pages.values()].filter((p) => !p.full);
|
|
546
|
-
log(
|
|
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
|
+
);
|
|
547
814
|
const htmlBlob = [];
|
|
548
815
|
for (const p of fullPages) collectRefs(ctx, p);
|
|
549
816
|
for (const p of partialPages) collectRefs(ctx, p);
|
|
550
817
|
|
|
818
|
+
mark('modules');
|
|
551
819
|
// 2. Módulos ES agrupados por import map
|
|
552
820
|
const groups = new Map();
|
|
553
821
|
const groupFor = (page, importMap) => {
|
|
@@ -560,7 +828,7 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
560
828
|
entries: new Set(),
|
|
561
829
|
virtual: new Map(),
|
|
562
830
|
pages: new Set(),
|
|
563
|
-
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,
|
|
564
832
|
vendorSplit: !!ctx.obfuscatorOptions && !cfg.dependencies.obfuscate,
|
|
565
833
|
});
|
|
566
834
|
return groups.get(key);
|
|
@@ -569,7 +837,11 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
569
837
|
const preloadLinks = [];
|
|
570
838
|
for (const page of fullPages) {
|
|
571
839
|
const map = page.model.importMaps[0] ? parseImportMap(ctx, page, page.model.importMaps[0]) : null;
|
|
572
|
-
if (page.model.importMaps.length > 1)
|
|
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
|
+
);
|
|
573
845
|
let vIndex = 0;
|
|
574
846
|
for (const s of page.model.scripts) {
|
|
575
847
|
if (s.kind !== 'module' || s.inTemplate) continue;
|
|
@@ -589,7 +861,7 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
589
861
|
const outDir = page.baseDir ? `${page.baseDir}/` : '';
|
|
590
862
|
g.virtual.set(id, {
|
|
591
863
|
code: s.inline.code,
|
|
592
|
-
label: `${page.rel} (módulo inline ${vIndex})`,
|
|
864
|
+
label: L(`${page.rel} (módulo inline ${vIndex})`, `${page.rel} (inline module ${vIndex})`),
|
|
593
865
|
baseDir: page.baseDir,
|
|
594
866
|
out: `${outDir}__vb_inline_${shortHash(id)}`,
|
|
595
867
|
page,
|
|
@@ -617,9 +889,15 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
617
889
|
if (!g.entries.size && !g.virtual.size) continue;
|
|
618
890
|
const res = await buildModuleGroup(ctx, { ...g, hash: g.hash });
|
|
619
891
|
if (res.mode === 'per-file') {
|
|
620
|
-
ctx.note(
|
|
621
|
-
|
|
622
|
-
|
|
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 });
|
|
900
|
+
for (const rel of g.entries) entryOut.set(`${g.key}\0${rel}`, jsOutPath(rel));
|
|
623
901
|
for (const [id, v] of g.virtual) {
|
|
624
902
|
const info = analyzeJs(v.code, { module: true });
|
|
625
903
|
if (!info.ok) continue;
|
|
@@ -631,44 +909,51 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
631
909
|
}
|
|
632
910
|
continue;
|
|
633
911
|
}
|
|
634
|
-
log(
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
+
);
|
|
918
|
+
// Ofuscar/comprimir cada salida en paralelo; registrar después en el orden original (salida determinista).
|
|
919
|
+
const finals = await mapLimit(res.outputs, 64, async (o) => {
|
|
639
920
|
const obfRels = o.virtualId ? [g.virtual.get(o.virtualId).page.rel] : o.isChunk ? o.inputs : [o.entry];
|
|
640
921
|
if (!o.isVendor && ctx.shouldObfuscate(obfRels)) {
|
|
641
|
-
const r = obfuscateModule(ctx, code, o.outRel);
|
|
642
|
-
|
|
643
|
-
else {
|
|
644
|
-
code = r.code;
|
|
645
|
-
map = null;
|
|
646
|
-
}
|
|
647
|
-
} else {
|
|
648
|
-
const t = await terserPass(ctx, code, { isModule: true, level: levelOf(res.target) });
|
|
649
|
-
if (t !== code) {
|
|
650
|
-
code = t;
|
|
651
|
-
map = null;
|
|
652
|
-
}
|
|
922
|
+
const r = await obfuscateModule(ctx, o.code, o.outRel);
|
|
923
|
+
return r.note ? { code: o.code, map: o.map, warn: r.note } : { code: r.code, map: null };
|
|
653
924
|
}
|
|
925
|
+
const t = await terserPass(ctx, o.code, { isModule: true, level: levelOf(res.target) });
|
|
926
|
+
return t !== o.code ? { code: t, map: null } : { code: o.code, map: o.map };
|
|
927
|
+
});
|
|
928
|
+
for (const [i, o] of res.outputs.entries()) {
|
|
929
|
+
const sources = o.inputs.length ? o.inputs : o.entry ? [o.entry] : [];
|
|
930
|
+
const { code, map, warn } = finals[i];
|
|
931
|
+
if (warn) ctx.warn(warn, o.outRel);
|
|
654
932
|
if (o.virtualId) {
|
|
655
933
|
const v = g.virtual.get(o.virtualId);
|
|
656
934
|
inlineModuleCode.set(o.virtualId, code.replace(/\n$/, ''));
|
|
657
|
-
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)`));
|
|
658
936
|
continue;
|
|
659
937
|
}
|
|
660
938
|
const action = o.isVendor ? 'vendor' : o.isChunk ? 'chunk' : 'bundle';
|
|
661
939
|
const outRel = ctx.emitCode(o.outRel, code, map, { type: 'js', module: true, action, sources, before: o.before || sumSizes(ctx, sources) });
|
|
662
940
|
if (o.entry) entryOut.set(`${g.key}\0${o.entry}`, outRel);
|
|
663
|
-
for (const rel of o.inputs)
|
|
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
|
+
);
|
|
664
947
|
}
|
|
665
948
|
for (const spec of res.vendor) if (!ctx.vendorPackages.includes(spec)) ctx.vendorPackages.push(spec);
|
|
666
949
|
// esbuild puede incrustar un módulo entero (p. ej. constantes) sin que figure en ninguna salida.
|
|
667
950
|
for (const rel of res.consumed) {
|
|
668
|
-
if (!ctx.coverage.get(rel)?.some((s) => s.startsWith('empaquetado'
|
|
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)'));
|
|
669
953
|
}
|
|
670
954
|
}
|
|
671
955
|
|
|
956
|
+
mark('css');
|
|
672
957
|
// 3. CSS enlazado
|
|
673
958
|
const cssOut = new Map();
|
|
674
959
|
const cssEntries = new Set(extra.css);
|
|
@@ -679,22 +964,27 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
679
964
|
if (res.status === 'ok') cssEntries.add(res.rel);
|
|
680
965
|
else if (res.status !== 'skip') ctx.reportBroken(page.rel, l.href.value, res);
|
|
681
966
|
}
|
|
682
|
-
|
|
967
|
+
const cssList = [...cssEntries];
|
|
968
|
+
const cssBuilt = await mapLimit(cssList, 16, (rel) => {
|
|
969
|
+
const hashed = cfg.hash && !extra.css.includes(rel);
|
|
970
|
+
return buildCss(ctx, { entry: rel, outRel: hashed ? `${ctx.chunkDir}/${path.posix.basename(rel)}` : rel });
|
|
971
|
+
});
|
|
972
|
+
for (const [ci, rel] of cssList.entries()) {
|
|
683
973
|
const hashed = cfg.hash && !extra.css.includes(rel);
|
|
684
|
-
const
|
|
685
|
-
const r = await buildCss(ctx, { entry: rel, outRel: provisional });
|
|
974
|
+
const r = cssBuilt[ci];
|
|
686
975
|
if (r.error) {
|
|
687
|
-
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);
|
|
688
977
|
cssOut.set(rel, ctx.emitCopy(rel, rel, 'copy-fallback'));
|
|
689
|
-
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);
|
|
690
979
|
continue;
|
|
691
980
|
}
|
|
692
981
|
const outRel = hashed ? ctx.hashedPath(rel, r.code, '.css') : rel;
|
|
693
982
|
ctx.emitCode(outRel, r.code, r.map, { type: 'css', action: 'bundle', sources: r.inputs, before: sumSizes(ctx, r.inputs) });
|
|
694
983
|
cssOut.set(rel, outRel);
|
|
695
|
-
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);
|
|
696
985
|
}
|
|
697
986
|
|
|
987
|
+
mark('white-space');
|
|
698
988
|
// 4. Detección de white-space: pre (para no colapsar espacios del HTML)
|
|
699
989
|
let collapse = cfg.html.collapseWhitespace;
|
|
700
990
|
if (collapse === 'auto') {
|
|
@@ -704,19 +994,35 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
704
994
|
collapse = !pre;
|
|
705
995
|
if (pre)
|
|
706
996
|
ctx.note(
|
|
707
|
-
|
|
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
|
+
),
|
|
708
1001
|
);
|
|
709
1002
|
}
|
|
710
1003
|
|
|
1004
|
+
mark('classic scripts');
|
|
711
1005
|
// 5. Scripts clásicos por página
|
|
712
|
-
|
|
1006
|
+
// Primero se planifican todas las páginas, luego se minifica/ofusca cada grupo distinto en paralelo
|
|
1007
|
+
// y por último se registra todo en el orden original.
|
|
1008
|
+
const pageRuns = [];
|
|
1009
|
+
const uniqueRuns = new Map();
|
|
713
1010
|
for (const page of fullPages) {
|
|
714
1011
|
for (const run of classicRuns(ctx, page)) {
|
|
715
|
-
const
|
|
716
|
-
|
|
1012
|
+
const key = `${run.items.map((c) => c.rel).join('|')}\0${run.items[0].key}`;
|
|
1013
|
+
pageRuns.push({ page, run, key });
|
|
1014
|
+
if (!uniqueRuns.has(key)) uniqueRuns.set(key, run);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
const runKeys = [...uniqueRuns.keys()];
|
|
1018
|
+
const runResults = await mapLimit(runKeys, 64, (key) => computeClassicRun(ctx, uniqueRuns.get(key)));
|
|
1019
|
+
const computedRuns = new Map(runKeys.map((k, i) => [k, runResults[i]]));
|
|
1020
|
+
const classicCache = new Map();
|
|
1021
|
+
{
|
|
1022
|
+
for (const { page, run, key } of pageRuns) {
|
|
717
1023
|
let out = classicCache.get(key);
|
|
718
1024
|
if (!out) {
|
|
719
|
-
out =
|
|
1025
|
+
out = commitClassicRun(ctx, run, page, computedRuns.get(key));
|
|
720
1026
|
classicCache.set(key, out);
|
|
721
1027
|
}
|
|
722
1028
|
if (out.merged) {
|
|
@@ -736,6 +1042,7 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
736
1042
|
}
|
|
737
1043
|
}
|
|
738
1044
|
|
|
1045
|
+
mark('html edits');
|
|
739
1046
|
// 6. Edición de cada página: módulos, CSS, inline, preload
|
|
740
1047
|
for (const page of fullPages) {
|
|
741
1048
|
for (const m of moduleRefs.filter((x) => x.page === page)) {
|
|
@@ -761,9 +1068,11 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
761
1068
|
}
|
|
762
1069
|
for (const page of pages.values()) await processInline(ctx, page, collapse);
|
|
763
1070
|
|
|
1071
|
+
mark('workers and standalone');
|
|
764
1072
|
// 7. Workers, archivos sueltos y rutas mencionadas (hasta que no aparezca nada nuevo)
|
|
765
1073
|
await drainQueues(ctx, cssOut);
|
|
766
1074
|
|
|
1075
|
+
mark('unreferenced');
|
|
767
1076
|
// 8. Archivos no referenciados
|
|
768
1077
|
const unref = [];
|
|
769
1078
|
for (const [rel, f] of ctx.files) {
|
|
@@ -773,14 +1082,16 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
773
1082
|
for (const rel of unref) {
|
|
774
1083
|
if (ctx.coverage.has(rel)) continue;
|
|
775
1084
|
const f = ctx.files.get(rel);
|
|
776
|
-
if (cfg.unreferenced === 'process' || f.forced)
|
|
1085
|
+
if (cfg.unreferenced === 'process' || f.forced)
|
|
1086
|
+
ctx.requireStandalone(rel, L('no referenciado (se procesa suelto)', 'unreferenced (processed on its own)'));
|
|
777
1087
|
else if (cfg.unreferenced === 'copy') {
|
|
778
1088
|
ctx.emitCopy(rel, rel);
|
|
779
|
-
ctx.cover(rel, 'no referenciado: copiado', rel);
|
|
780
|
-
} 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)'));
|
|
781
1091
|
}
|
|
782
1092
|
await drainQueues(ctx, cssOut);
|
|
783
1093
|
|
|
1094
|
+
mark('final html');
|
|
784
1095
|
// 9. Preloads y HTML final
|
|
785
1096
|
for (const { page, l } of preloadLinks) {
|
|
786
1097
|
const res = ctx.resolve(l.href.value, page.baseDir);
|
|
@@ -791,7 +1102,13 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
791
1102
|
(ctx.hasOwnOutput(res.rel) || ctx.assets.has(res.rel) ? res.rel : null);
|
|
792
1103
|
if (target == null) {
|
|
793
1104
|
page.edits.push({ start: l.start, end: l.end, text: '' });
|
|
794
|
-
ctx.note(
|
|
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
|
+
);
|
|
795
1112
|
} else if (target !== res.rel) {
|
|
796
1113
|
page.edits.push({ start: l.href.start, end: l.href.end, text: attrText('href', urlFor(ctx, page, l.href.value, target)) });
|
|
797
1114
|
}
|
|
@@ -800,29 +1117,35 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
800
1117
|
for (const page of pages.values()) {
|
|
801
1118
|
const referenced = page.full || ctx.pageRefs.has(page.rel) || ctx.mentions.has(page.rel) || ctx.files.get(page.rel).forced;
|
|
802
1119
|
if (!referenced && cfg.unreferenced === 'skip') {
|
|
803
|
-
ctx.cover(page.rel, 'no referenciado: omitido (unreferenced: skip)');
|
|
1120
|
+
ctx.cover(page.rel, L('no referenciado: omitido (unreferenced: skip)', 'unreferenced: skipped (unreferenced: skip)'));
|
|
804
1121
|
continue;
|
|
805
1122
|
}
|
|
806
1123
|
if (!referenced && cfg.unreferenced === 'copy') {
|
|
807
1124
|
ctx.emitCopy(page.rel, page.rel);
|
|
808
|
-
ctx.cover(page.rel, 'no referenciado: copiado', page.rel);
|
|
1125
|
+
ctx.cover(page.rel, L('no referenciado: copiado', 'unreferenced: copied'), page.rel);
|
|
809
1126
|
continue;
|
|
810
1127
|
}
|
|
811
1128
|
await finalizePage(ctx, page, collapse);
|
|
812
1129
|
}
|
|
813
1130
|
|
|
1131
|
+
mark('assets');
|
|
814
1132
|
// 10. Recursos: todo lo demás se copia (sin perder nada)
|
|
1133
|
+
const imageJobs = [];
|
|
815
1134
|
for (const [rel, f] of ctx.files) {
|
|
816
1135
|
if (ctx.hasOwnOutput(rel)) {
|
|
817
|
-
if (!ctx.coverage.has(rel)) ctx.cover(rel, 'procesado', rel);
|
|
1136
|
+
if (!ctx.coverage.has(rel)) ctx.cover(rel, L('procesado', 'processed'), rel);
|
|
818
1137
|
continue;
|
|
819
1138
|
}
|
|
820
1139
|
const cov = ctx.coverage.get(rel);
|
|
821
1140
|
// Empaquetar solo absorbe JS y CSS; un JSON importado se publica también por si se pide con fetch().
|
|
822
|
-
if (
|
|
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;
|
|
823
1146
|
const referenced = ctx.assets.has(rel) || ctx.mentions.has(rel);
|
|
824
1147
|
if (cfg.unreferenced === 'skip' && !referenced && !f.forced) {
|
|
825
|
-
ctx.cover(rel, 'no referenciado: omitido (unreferenced: skip)');
|
|
1148
|
+
ctx.cover(rel, L('no referenciado: omitido (unreferenced: skip)', 'unreferenced: skipped (unreferenced: skip)'));
|
|
826
1149
|
continue;
|
|
827
1150
|
}
|
|
828
1151
|
if ((f.ext === '.json' || f.ext === '.webmanifest') && cfg.json.minify) {
|
|
@@ -830,30 +1153,72 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
830
1153
|
const min = raw == null ? null : minifyJsonText(stripBom(raw).text);
|
|
831
1154
|
if (min != null && Buffer.byteLength(min) < f.size) {
|
|
832
1155
|
ctx.emit(rel, min, { type: 'json', action: 'minify', sources: [rel], before: f.size });
|
|
833
|
-
ctx.cover(rel, 'JSON minificado', rel);
|
|
1156
|
+
ctx.cover(rel, L('JSON minificado', 'JSON minified'), rel);
|
|
834
1157
|
continue;
|
|
835
1158
|
}
|
|
836
1159
|
}
|
|
1160
|
+
if (sharp && IMAGE_EXT.has(f.ext)) {
|
|
1161
|
+
imageJobs.push({ rel, f });
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
837
1164
|
ctx.emitCopy(rel, rel);
|
|
838
|
-
ctx.cover(rel, 'copiado', rel);
|
|
1165
|
+
ctx.cover(rel, L('copiado', 'copied'), rel);
|
|
839
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
|
+
});
|
|
840
1186
|
|
|
1187
|
+
mark('warnings');
|
|
841
1188
|
// 11. Avisos de rutas mencionadas en el código que ya no existirán
|
|
842
1189
|
for (const [rel, from] of ctx.mentions) {
|
|
843
|
-
if (!ctx.outputs.has(rel) && ctx.coverage.get(rel)?.every((s) => s.startsWith('empaquetado'))) {
|
|
844
|
-
ctx.warn(
|
|
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
|
+
);
|
|
845
1197
|
}
|
|
846
1198
|
}
|
|
847
1199
|
if (cfg.hash) {
|
|
848
1200
|
for (const [rel, from] of ctx.mentions) {
|
|
849
1201
|
if ((cssOut.get(rel) ?? rel) !== rel)
|
|
850
|
-
ctx.note(
|
|
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
|
+
);
|
|
851
1208
|
}
|
|
852
1209
|
}
|
|
853
1210
|
|
|
1211
|
+
mark('precompression');
|
|
854
1212
|
// 12. Precompresión .gz / .br para servidores que sirven archivos comprimidos (nginx gzip_static, etc.)
|
|
855
|
-
if (cfg.compress && mode === 'build') precompress(ctx, cfg.compress === true ? ['gzip', 'brotli'] : [cfg.compress]);
|
|
1213
|
+
if (cfg.compress && mode === 'build') await precompress(ctx, cfg.compress === true ? ['gzip', 'brotli'] : [cfg.compress]);
|
|
856
1214
|
|
|
1215
|
+
mark('size report');
|
|
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);
|
|
857
1222
|
const result = {
|
|
858
1223
|
version: VERSION,
|
|
859
1224
|
root: cfg.root,
|
|
@@ -873,16 +1238,28 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
873
1238
|
.map((rel) => ({ rel, reason: ctx.dependencyReason(rel) })),
|
|
874
1239
|
vendor: ctx.vendorPackages,
|
|
875
1240
|
obfuscated: !!ctx.obfuscatorOptions,
|
|
876
|
-
report
|
|
1241
|
+
report,
|
|
1242
|
+
budgets,
|
|
1243
|
+
perf: Object.assign(ctx.heavy.stats, { phases }),
|
|
877
1244
|
verify: null,
|
|
878
1245
|
durationMs: 0,
|
|
879
1246
|
};
|
|
880
1247
|
if (mode === 'analyze') {
|
|
1248
|
+
mark('fin');
|
|
881
1249
|
result.durationMs = Date.now() - t0;
|
|
882
1250
|
return result;
|
|
883
1251
|
}
|
|
884
1252
|
|
|
1253
|
+
mark('write and verify');
|
|
885
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
|
+
);
|
|
886
1263
|
let staging;
|
|
887
1264
|
try {
|
|
888
1265
|
staging = await writeStaging(cfg, ctx.outputs);
|
|
@@ -890,7 +1267,13 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
890
1267
|
result.verify = await verifyOutput(staging, ctx.outputs, ctx);
|
|
891
1268
|
if (result.verify.errors.length) {
|
|
892
1269
|
if (!opts.keepFailed) await discardStaging(staging);
|
|
893
|
-
throw new BuildError(
|
|
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
|
+
);
|
|
894
1277
|
}
|
|
895
1278
|
}
|
|
896
1279
|
await writeManifest(staging, {
|
|
@@ -900,6 +1283,8 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
900
1283
|
files: [...ctx.outputs.keys()].sort(),
|
|
901
1284
|
});
|
|
902
1285
|
await swapIntoPlace(cfg, staging);
|
|
1286
|
+
await runHook(cfg.plugins, 'buildEnd', { result, outDir: cfg.outDir });
|
|
1287
|
+
mark('fin');
|
|
903
1288
|
} catch (err) {
|
|
904
1289
|
if (staging && !opts.keepFailed) await discardStaging(staging);
|
|
905
1290
|
throw err;
|
|
@@ -910,26 +1295,21 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
910
1295
|
|
|
911
1296
|
const COMPRESS_TYPES = new Set(['html', 'css', 'js', 'json', 'svg', 'text', 'map']);
|
|
912
1297
|
|
|
913
|
-
function precompress(ctx, kinds) {
|
|
914
|
-
|
|
915
|
-
|
|
1298
|
+
async function precompress(ctx, kinds) {
|
|
1299
|
+
const items = [...ctx.outputs].filter(([, o]) => COMPRESS_TYPES.has(o.type));
|
|
1300
|
+
// En paralelo (pool de hilos de libuv); se registra en orden. El informe reutiliza estos mismos resultados.
|
|
1301
|
+
const done = await mapLimit(items, 64, async ([, o]) => {
|
|
916
1302
|
const buf = o.copyFrom ? fs.readFileSync(o.copyFrom) : o.data;
|
|
917
|
-
if (buf.length < 1024)
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
[zlib.constants.BROTLI_PARAM_SIZE_HINT]: buf.length,
|
|
928
|
-
},
|
|
929
|
-
});
|
|
930
|
-
if (br.length < buf.length) ctx.emit(`${rel}.br`, br, { type: 'compressed', action: 'brotli', sources: o.sources });
|
|
931
|
-
}
|
|
932
|
-
}
|
|
1303
|
+
if (buf.length < 1024) return null; // no compensa
|
|
1304
|
+
const [gz, br] = await Promise.all([kinds.includes('gzip') ? gzipOf(buf) : null, kinds.includes('brotli') ? brotliOf(buf) : null]);
|
|
1305
|
+
return { buf, gz, br };
|
|
1306
|
+
});
|
|
1307
|
+
items.forEach(([rel, o], i) => {
|
|
1308
|
+
const d = done[i];
|
|
1309
|
+
if (!d) return;
|
|
1310
|
+
if (d.gz && d.gz.length < d.buf.length) ctx.emit(`${rel}.gz`, d.gz, { type: 'compressed', action: 'gzip', sources: o.sources });
|
|
1311
|
+
if (d.br && d.br.length < d.buf.length) ctx.emit(`${rel}.br`, d.br, { type: 'compressed', action: 'brotli', sources: o.sources });
|
|
1312
|
+
});
|
|
933
1313
|
}
|
|
934
1314
|
|
|
935
1315
|
function sumSizes(ctx, rels) {
|
|
@@ -947,31 +1327,54 @@ function markModuleImport(ctx, spec, baseDir, from, importMap) {
|
|
|
947
1327
|
.map((k) => importMap.imports[k] + spec.slice(k.length))[0];
|
|
948
1328
|
if (target) {
|
|
949
1329
|
const r = ctx.resolve(target, importMap.baseDir);
|
|
950
|
-
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 });
|
|
951
1331
|
}
|
|
952
1332
|
}
|
|
953
1333
|
return;
|
|
954
1334
|
}
|
|
955
1335
|
const r = ctx.resolve(spec, baseDir);
|
|
956
|
-
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 });
|
|
957
1337
|
else ctx.reportBroken(from, spec, r);
|
|
958
1338
|
}
|
|
959
1339
|
|
|
960
|
-
|
|
1340
|
+
/** Parte costosa y sin efectos de un grupo de scripts clásicos: unir, minificar y ofuscar. */
|
|
1341
|
+
async function computeClassicRun(ctx, run) {
|
|
961
1342
|
const items = run.items;
|
|
962
1343
|
const first = items[0];
|
|
963
|
-
if (items.length === 1 && !first.info.ok) {
|
|
964
|
-
|
|
965
|
-
const outRel = ctx.emitCopy(first.rel, first.rel, 'copy-fallback');
|
|
966
|
-
ctx.cover(first.rel, 'copiado sin cambios (no se pudo analizar)', outRel);
|
|
967
|
-
return { merged: false, outs: [{ outRel, changed: false }] };
|
|
968
|
-
}
|
|
1344
|
+
if (items.length === 1 && !first.info.ok) return { copy: true };
|
|
1345
|
+
let mergeError = null;
|
|
969
1346
|
if (items.length > 1) {
|
|
970
1347
|
// Separadores con ; y salto de línea: evitan que un archivo sin ; final se "pegue" al siguiente.
|
|
971
1348
|
const code = items.map((c) => ctx.readText(c.rel, { kind: 'js', charset: c.s.attrs.get('charset') }).text.replace(/\s*$/, '\n')).join(';\n');
|
|
972
1349
|
const info = analyzeJs(code, { module: false });
|
|
973
|
-
if (info.ok) {
|
|
974
|
-
|
|
1350
|
+
if (info.ok) return { merged: await processClassic(ctx, { code, id: items.map((c) => c.rel).join('+'), info, obfuscate: first.obf }) };
|
|
1351
|
+
mergeError = info.error;
|
|
1352
|
+
}
|
|
1353
|
+
const separate = await Promise.all(
|
|
1354
|
+
items.map((c) => {
|
|
1355
|
+
const { text } = ctx.readText(c.rel, { kind: 'js', charset: c.s.attrs.get('charset') });
|
|
1356
|
+
return processClassic(ctx, { code: text, id: c.rel, info: c.info, obfuscate: c.obf, sourcemap: !!ctx.cfg.sourcemap });
|
|
1357
|
+
}),
|
|
1358
|
+
);
|
|
1359
|
+
return { separate, mergeError };
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/** Registra el resultado de computeClassicRun (salidas, cobertura y notas) en orden. */
|
|
1363
|
+
function commitClassicRun(ctx, run, page, comp) {
|
|
1364
|
+
const items = run.items;
|
|
1365
|
+
const first = items[0];
|
|
1366
|
+
if (comp.copy) {
|
|
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
|
+
);
|
|
1371
|
+
const outRel = ctx.emitCopy(first.rel, first.rel, 'copy-fallback');
|
|
1372
|
+
ctx.cover(first.rel, L('copiado sin cambios (no se pudo analizar)', 'copied unchanged (could not be parsed)'), outRel);
|
|
1373
|
+
return { merged: false, outs: [{ outRel, changed: false }] };
|
|
1374
|
+
}
|
|
1375
|
+
{
|
|
1376
|
+
if (comp.merged) {
|
|
1377
|
+
const r = comp.merged;
|
|
975
1378
|
for (const n of r.notes) ctx.note(n, `scripts unidos (${items.map((c) => c.rel).join(', ')})`);
|
|
976
1379
|
const outRel = ctx.emitCode(`${ctx.chunkDir}/scripts-${shortHash(r.code)}.js`, r.code, null, {
|
|
977
1380
|
type: 'js',
|
|
@@ -982,21 +1385,25 @@ async function processClassicRun(ctx, run, page) {
|
|
|
982
1385
|
items.map((c) => c.rel),
|
|
983
1386
|
),
|
|
984
1387
|
});
|
|
985
|
-
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);
|
|
986
1389
|
return { merged: true, outRel };
|
|
987
1390
|
}
|
|
988
|
-
|
|
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
|
+
);
|
|
989
1396
|
}
|
|
990
1397
|
// Un solo archivo, o una unión fallida: cada uno se procesa en su sitio.
|
|
991
1398
|
const outs = [];
|
|
992
|
-
for (const c of items) {
|
|
993
|
-
const
|
|
994
|
-
const r = await processClassic(ctx, { code: text, id: c.rel, info: c.info, obfuscate: c.obf, sourcemap: !!ctx.cfg.sourcemap });
|
|
1399
|
+
for (const [i, c] of items.entries()) {
|
|
1400
|
+
const r = comp.separate[i];
|
|
995
1401
|
for (const n of r.notes) ctx.note(n, c.rel);
|
|
996
|
-
if (c.reasons.length && c.reasons[0] !== 'unión desactivada'
|
|
997
|
-
|
|
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);
|
|
1404
|
+
const outRel = ctx.cfg.hash ? ctx.hashedPath(c.rel, r.code, '.js') : jsOutPath(c.rel);
|
|
998
1405
|
ctx.emitCode(outRel, r.code, r.map, { type: 'js', action: 'minify', sources: [c.rel], before: ctx.files.get(c.rel).size });
|
|
999
|
-
ctx.cover(c.rel, 'procesado', outRel);
|
|
1406
|
+
ctx.cover(c.rel, L('procesado', 'processed'), outRel);
|
|
1000
1407
|
outs.push({ outRel, changed: true });
|
|
1001
1408
|
}
|
|
1002
1409
|
return { merged: false, outs };
|
|
@@ -1009,7 +1416,7 @@ async function processInline(ctx, page) {
|
|
|
1009
1416
|
if (s.src) {
|
|
1010
1417
|
if (!page.full) {
|
|
1011
1418
|
const res = ctx.resolve(s.src.value, page.baseDir);
|
|
1012
|
-
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' });
|
|
1013
1420
|
else if (res.status !== 'skip') ctx.reportBroken(page.rel, s.src.value, res);
|
|
1014
1421
|
}
|
|
1015
1422
|
continue;
|
|
@@ -1018,7 +1425,10 @@ async function processInline(ctx, page) {
|
|
|
1018
1425
|
if (s.kind === 'classic') {
|
|
1019
1426
|
const info = analyzeJs(s.inline.code, { module: false });
|
|
1020
1427
|
if (!info.ok) {
|
|
1021
|
-
ctx.note(
|
|
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
|
+
);
|
|
1022
1432
|
continue;
|
|
1023
1433
|
}
|
|
1024
1434
|
ctx.noteJsInfo(page.rel, info, page.baseDir);
|
|
@@ -1048,7 +1458,7 @@ async function processInline(ctx, page) {
|
|
|
1048
1458
|
for (const l of page.model.links) {
|
|
1049
1459
|
if (l.type !== 'stylesheet') continue;
|
|
1050
1460
|
const res = ctx.resolve(l.href.value, page.baseDir);
|
|
1051
|
-
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}`));
|
|
1052
1462
|
else if (res.status !== 'skip') ctx.reportBroken(page.rel, l.href.value, res);
|
|
1053
1463
|
}
|
|
1054
1464
|
}
|
|
@@ -1060,7 +1470,13 @@ async function processInline(ctx, page) {
|
|
|
1060
1470
|
outRel: page.baseDir ? `${page.baseDir}/__page.html` : '__page.html',
|
|
1061
1471
|
});
|
|
1062
1472
|
if (r.error) {
|
|
1063
|
-
ctx.note(
|
|
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
|
+
);
|
|
1064
1480
|
continue;
|
|
1065
1481
|
}
|
|
1066
1482
|
if (r.code !== st.code) page.edits.push({ start: st.start, end: st.end, text: escapeInlineStyle(r.code) });
|
|
@@ -1097,7 +1513,7 @@ async function finalizePage(ctx, page, collapse) {
|
|
|
1097
1513
|
baseDir: page.baseDir,
|
|
1098
1514
|
baseExternal: page.baseExternal,
|
|
1099
1515
|
});
|
|
1100
|
-
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);
|
|
1101
1517
|
}
|
|
1102
1518
|
|
|
1103
1519
|
/** Procesa workers y archivos que deben existir sueltos en su ruta original, hasta agotar la cola. */
|
|
@@ -1108,7 +1524,7 @@ async function drainQueues(ctx, cssOut) {
|
|
|
1108
1524
|
for (const [rel] of ctx.mentions) {
|
|
1109
1525
|
const ext = extOf(rel);
|
|
1110
1526
|
if ((JS_EXT.has(ext) || CSS_EXT.has(ext)) && !ctx.hasOwnOutput(rel) && !ctx.standalone.has(rel))
|
|
1111
|
-
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)}`));
|
|
1112
1528
|
else if (!JS_EXT.has(ext) && !CSS_EXT.has(ext) && !HTML_EXT.has(ext)) ctx.registerAsset(rel, ctx.mentions.get(rel));
|
|
1113
1529
|
}
|
|
1114
1530
|
for (const [rel, w] of ctx.workers) {
|
|
@@ -1119,72 +1535,99 @@ async function drainQueues(ctx, cssOut) {
|
|
|
1119
1535
|
if (w.module) {
|
|
1120
1536
|
const r = await buildModuleWorker(ctx, rel);
|
|
1121
1537
|
if (r.error) {
|
|
1122
|
-
ctx.warn(
|
|
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
|
+
);
|
|
1123
1545
|
ctx.requireStandalone(rel, 'worker (respaldo)', { module: true });
|
|
1124
1546
|
continue;
|
|
1125
1547
|
}
|
|
1126
1548
|
let code = r.code;
|
|
1127
1549
|
if (ctx.shouldObfuscate([rel])) {
|
|
1128
|
-
const o = obfuscateModule(ctx, code, rel);
|
|
1550
|
+
const o = await obfuscateModule(ctx, code, rel);
|
|
1129
1551
|
if (o.note) ctx.warn(o.note, rel);
|
|
1130
1552
|
code = o.code;
|
|
1131
1553
|
} else code = await terserPass(ctx, code, { isModule: true, level: 2020 });
|
|
1132
1554
|
const sources = [...r.consumed];
|
|
1133
|
-
ctx.emitCode(rel, code, code === r.code ? r.map : null, {
|
|
1555
|
+
ctx.emitCode(jsOutPath(rel), code, code === r.code ? r.map : null, {
|
|
1134
1556
|
type: 'js',
|
|
1135
1557
|
module: true,
|
|
1136
1558
|
action: 'worker',
|
|
1137
1559
|
sources,
|
|
1138
1560
|
before: sumSizes(ctx, sources),
|
|
1139
1561
|
});
|
|
1140
|
-
for (const s of sources)
|
|
1141
|
-
|
|
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 });
|
|
1142
1565
|
}
|
|
1566
|
+
// Análisis en orden, trabajo pesado en paralelo, registro otra vez en orden.
|
|
1567
|
+
const jobs = [];
|
|
1143
1568
|
for (const [rel, s] of ctx.standalone) {
|
|
1144
1569
|
if (ctx.done.has(`s:${rel}`)) continue;
|
|
1145
1570
|
ctx.done.add(`s:${rel}`);
|
|
1146
1571
|
progressed = true;
|
|
1147
1572
|
if (ctx.hasOwnOutput(rel) || !ctx.files.has(rel)) continue;
|
|
1148
|
-
|
|
1573
|
+
const job = planStandalone(ctx, rel, s, cssOut);
|
|
1574
|
+
if (job) jobs.push(job);
|
|
1149
1575
|
}
|
|
1576
|
+
const results = await mapLimit(jobs, 64, (j) => j.run());
|
|
1577
|
+
jobs.forEach((j, i) => j.commit(results[i]));
|
|
1150
1578
|
if (!progressed) return;
|
|
1151
1579
|
}
|
|
1152
|
-
throw new BuildError(
|
|
1580
|
+
throw new BuildError(
|
|
1581
|
+
L('Demasiadas rondas al resolver dependencias (¿ciclo inesperado?).', 'Too many rounds resolving dependencies (unexpected cycle?).'),
|
|
1582
|
+
);
|
|
1153
1583
|
}
|
|
1154
1584
|
|
|
1155
|
-
|
|
1585
|
+
/**
|
|
1586
|
+
* Prepara un archivo suelto: el análisis (que descubre más dependencias) se hace ya, en orden;
|
|
1587
|
+
* devuelve {run, commit}: run() es la parte costosa y sin efectos, commit() registra el resultado.
|
|
1588
|
+
*/
|
|
1589
|
+
function planStandalone(ctx, rel, s, cssOut) {
|
|
1156
1590
|
const ext = extOf(rel);
|
|
1157
1591
|
const dir = path.posix.dirname(rel) === '.' ? '' : path.posix.dirname(rel);
|
|
1158
1592
|
if (CSS_EXT.has(ext)) {
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1593
|
+
// buildCss registra los recursos de url() mientras compila: eso solo añade trabajo a la siguiente ronda.
|
|
1594
|
+
return {
|
|
1595
|
+
run: () => buildCss(ctx, { entry: rel, outRel: rel }),
|
|
1596
|
+
commit: (r) => {
|
|
1597
|
+
if (r.error) {
|
|
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
|
+
);
|
|
1605
|
+
ctx.emitCopy(rel, rel, 'copy-fallback');
|
|
1606
|
+
ctx.cover(rel, L('copiado sin cambios (error al procesar)', 'copied unchanged (processing error)'), rel);
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
ctx.emitCode(rel, r.code, r.map, { type: 'css', action: 'standalone', sources: [rel], before: ctx.files.get(rel).size });
|
|
1610
|
+
for (const i of r.inputs) ctx.cover(i, i === rel ? L('procesado (suelto)', 'processed (standalone)') : L('empaquetado', 'bundled'), rel);
|
|
1611
|
+
if (!cssOut.has(rel)) cssOut.set(rel, rel);
|
|
1612
|
+
},
|
|
1613
|
+
};
|
|
1170
1614
|
}
|
|
1171
1615
|
if (!JS_EXT.has(ext)) {
|
|
1172
1616
|
ctx.registerAsset(rel, s.reason);
|
|
1173
|
-
return;
|
|
1617
|
+
return null;
|
|
1174
1618
|
}
|
|
1175
1619
|
const auto = ctx.jsInfo(rel, 'auto');
|
|
1176
1620
|
const isModule = !s.classic && (s.module || (auto.ok && (auto.hasModuleSyntax || auto.hasImportMeta)) || (!auto.ok && auto.isModule));
|
|
1177
1621
|
const info = isModule ? ctx.jsInfo(rel, true) : ctx.jsInfo(rel, false);
|
|
1178
1622
|
if (!info.ok) {
|
|
1179
|
-
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);
|
|
1180
1624
|
ctx.emitCopy(rel, rel, 'copy-fallback');
|
|
1181
|
-
ctx.cover(rel, 'copiado sin cambios (no se pudo analizar)', rel);
|
|
1182
|
-
return;
|
|
1625
|
+
ctx.cover(rel, L('copiado sin cambios (no se pudo analizar)', 'copied unchanged (could not be parsed)'), rel);
|
|
1626
|
+
return null;
|
|
1183
1627
|
}
|
|
1184
1628
|
ctx.noteJsInfo(rel, info, dir);
|
|
1185
1629
|
const { text } = ctx.readText(rel, { kind: 'js' });
|
|
1186
1630
|
const obfuscate = ctx.shouldObfuscate([rel]);
|
|
1187
|
-
let r;
|
|
1188
1631
|
if (isModule) {
|
|
1189
1632
|
for (const imp of info.imports) if (imp.spec) markModuleImport(ctx, imp.spec, dir, rel, null);
|
|
1190
1633
|
for (const u of info.urls) {
|
|
@@ -1192,11 +1635,15 @@ async function processStandalone(ctx, rel, s, cssOut) {
|
|
|
1192
1635
|
if (res.status === 'ok') ctx.urlTarget(res.rel, rel);
|
|
1193
1636
|
else ctx.reportBroken(rel, u.spec, res);
|
|
1194
1637
|
}
|
|
1195
|
-
r = await processModuleStandalone(ctx, { code: text, id: rel, info, obfuscate, sourcemap: !!ctx.cfg.sourcemap });
|
|
1196
|
-
} else {
|
|
1197
|
-
r = await processClassic(ctx, { code: text, id: rel, info, obfuscate, sourcemap: !!ctx.cfg.sourcemap });
|
|
1198
1638
|
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1639
|
+
const job = { code: text, id: rel, info, obfuscate, sourcemap: !!ctx.cfg.sourcemap };
|
|
1640
|
+
return {
|
|
1641
|
+
run: () => (isModule ? processModuleStandalone(ctx, job) : processClassic(ctx, job)),
|
|
1642
|
+
commit: (r) => {
|
|
1643
|
+
for (const n of r.notes) ctx.note(n, rel);
|
|
1644
|
+
const out = jsOutPath(rel);
|
|
1645
|
+
ctx.emitCode(out, r.code, r.map, { type: 'js', module: isModule, action: 'standalone', sources: [rel], before: ctx.files.get(rel).size });
|
|
1646
|
+
ctx.cover(rel, L(`procesado suelto (${s.reason})`, `processed standalone (${s.reason})`), out);
|
|
1647
|
+
},
|
|
1648
|
+
};
|
|
1202
1649
|
}
|