vanilla-builder 0.1.0 → 0.2.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 +20 -0
- package/README.es.md +76 -7
- package/README.md +76 -7
- package/bin/builder.js +106 -2
- package/package.json +7 -3
- package/src/builder.js +272 -89
- package/src/compress.js +44 -0
- package/src/config.js +197 -190
- package/src/deps.js +60 -1
- package/src/files.js +7 -0
- package/src/js.js +26 -34
- package/src/jsimports.js +54 -0
- package/src/jsinfo.js +19 -3
- package/src/pool.js +341 -0
- package/src/report.js +14 -19
- package/src/tersercore.js +33 -0
- package/src/urls.js +3 -0
- package/src/util.js +5 -1
- package/src/verify.js +13 -23
- package/src/worker.js +26 -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,10 @@ 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';
|
|
24
26
|
import { buildReport } from './report.js';
|
|
25
27
|
import {
|
|
26
28
|
BuildError,
|
|
@@ -29,6 +31,8 @@ import {
|
|
|
29
31
|
BOM,
|
|
30
32
|
extOf,
|
|
31
33
|
JS_EXT,
|
|
34
|
+
TS_EXT,
|
|
35
|
+
jsOutPath,
|
|
32
36
|
CSS_EXT,
|
|
33
37
|
HTML_EXT,
|
|
34
38
|
shortHash,
|
|
@@ -40,7 +44,7 @@ import {
|
|
|
40
44
|
|
|
41
45
|
export const VERSION = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
42
46
|
|
|
43
|
-
const MERGE_ATTRS = new Set(['src', 'type', 'defer', 'charset', 'language']);
|
|
47
|
+
const MERGE_ATTRS = new Set(['src', 'type', 'defer', 'charset', 'language', 'data-builder']);
|
|
44
48
|
const typeOfExt = (ext) =>
|
|
45
49
|
HTML_EXT.has(ext)
|
|
46
50
|
? 'html'
|
|
@@ -88,6 +92,9 @@ class Context {
|
|
|
88
92
|
this.depIncluded = globMatcher(cfg.dependencies.include);
|
|
89
93
|
this.depExcluded = globMatcher(cfg.dependencies.exclude);
|
|
90
94
|
this.depCache = new Map();
|
|
95
|
+
this.depMarks = new Map();
|
|
96
|
+
this.tsCode = new Map();
|
|
97
|
+
this.pkgNames = null;
|
|
91
98
|
this.vendorPackages = [];
|
|
92
99
|
this.chunkDir = cfg.js.chunkDir.replace(/\/+$/, '');
|
|
93
100
|
}
|
|
@@ -122,6 +129,8 @@ class Context {
|
|
|
122
129
|
}
|
|
123
130
|
|
|
124
131
|
readText(rel, { kind = 'text', charset } = {}) {
|
|
132
|
+
// TypeScript: como JS se lee la versión ya transpilada (sin tipos).
|
|
133
|
+
if (kind === 'js' && this.tsCode.has(rel)) return { text: this.tsCode.get(rel), bom: false, encoding: 'utf8' };
|
|
125
134
|
const key = `${rel}\0${kind}`;
|
|
126
135
|
if (this.textCache.has(key)) return this.textCache.get(key);
|
|
127
136
|
const buf = fs.readFileSync(this.abs(rel));
|
|
@@ -186,7 +195,7 @@ class Context {
|
|
|
186
195
|
if (JS_EXT.has(ext) || CSS_EXT.has(ext)) {
|
|
187
196
|
if (!this.workers.has(rel)) this.requireStandalone(rel, `new URL() en ${from}`);
|
|
188
197
|
} else this.registerAsset(rel, from);
|
|
189
|
-
return rel;
|
|
198
|
+
return jsOutPath(rel);
|
|
190
199
|
}
|
|
191
200
|
|
|
192
201
|
requireStandalone(rel, reason, extra = {}) {
|
|
@@ -235,23 +244,52 @@ class Context {
|
|
|
235
244
|
}
|
|
236
245
|
|
|
237
246
|
hasOwnOutput(rel) {
|
|
238
|
-
const o = this.outputs.get(rel);
|
|
247
|
+
const o = this.outputs.get(rel) ?? (TS_EXT.has(extOf(rel)) ? this.outputs.get(jsOutPath(rel)) : undefined);
|
|
239
248
|
return !!o && o.sources?.includes(rel);
|
|
240
249
|
}
|
|
241
250
|
|
|
242
251
|
/** ¿Es una librería de terceros? (a mano en dependencies.include/exclude, o detectada). */
|
|
252
|
+
// Prioridad: configuración (include/exclude) > marca en el HTML o en el archivo > detección automática.
|
|
243
253
|
dependencyReason(rel) {
|
|
244
254
|
if (this.depCache.has(rel)) return this.depCache.get(rel);
|
|
245
255
|
const deps = this.cfg.dependencies;
|
|
256
|
+
const isJs = JS_EXT.has(extOf(rel)) && this.files.has(rel);
|
|
257
|
+
const text = isJs ? this.readText(rel, { kind: 'js' }).text : '';
|
|
258
|
+
const pragma = isJs ? dependencyPragma(text) : null;
|
|
259
|
+
const mark = this.depMarks.get(rel);
|
|
246
260
|
let reason = null;
|
|
247
261
|
if (this.depExcluded(rel)) reason = null;
|
|
248
262
|
else if (this.depIncluded(rel)) reason = 'marcado a mano (dependencies.include)';
|
|
263
|
+
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)';
|
|
249
266
|
else if (/(^|\/)node_modules\//.test(rel)) reason = 'paquete de npm';
|
|
250
|
-
else if (deps.detect &&
|
|
267
|
+
else if (deps.detect && isJs) reason = detectDependency(rel, text, { packages: this.packageHints() });
|
|
251
268
|
this.depCache.set(rel, reason);
|
|
252
269
|
return reason;
|
|
253
270
|
}
|
|
254
271
|
|
|
272
|
+
/** Paquetes del package.json más cercano (para reconocer librerías copiadas a mano). */
|
|
273
|
+
packageHints() {
|
|
274
|
+
if (this.pkgNames) return this.pkgNames;
|
|
275
|
+
this.pkgNames = new Map();
|
|
276
|
+
for (let dir = this.root, i = 0; i < 10; i++) {
|
|
277
|
+
const file = path.join(dir, 'package.json');
|
|
278
|
+
if (fs.existsSync(file)) {
|
|
279
|
+
try {
|
|
280
|
+
this.pkgNames = packageNames(JSON.parse(fs.readFileSync(file, 'utf8')));
|
|
281
|
+
} catch {
|
|
282
|
+
// package.json ilegible: no aporta pistas
|
|
283
|
+
}
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
const parent = path.dirname(dir);
|
|
287
|
+
if (parent === dir) break;
|
|
288
|
+
dir = parent;
|
|
289
|
+
}
|
|
290
|
+
return this.pkgNames;
|
|
291
|
+
}
|
|
292
|
+
|
|
255
293
|
shouldObfuscate(rels) {
|
|
256
294
|
if (!this.obfuscatorOptions) return false;
|
|
257
295
|
const list = rels.filter(Boolean);
|
|
@@ -372,6 +410,70 @@ function loadPages(ctx) {
|
|
|
372
410
|
}
|
|
373
411
|
|
|
374
412
|
/** Decide qué páginas se procesan completas (entradas + enlazadas) y cuáles en modo conservador. */
|
|
413
|
+
/**
|
|
414
|
+
* Transpila cada archivo TypeScript a JavaScript (solo quita los tipos: no baja el nivel de sintaxis).
|
|
415
|
+
* Usa el tsconfig.json del proyecto si existe. Lo que falle se avisa y sigue el camino normal
|
|
416
|
+
* (se copia sin cambios), así que nunca se pierde nada.
|
|
417
|
+
*/
|
|
418
|
+
async function transpileTypeScript(ctx) {
|
|
419
|
+
const list = [...ctx.files.keys()].filter((rel) => TS_EXT.has(extOf(rel)));
|
|
420
|
+
if (!list.length) return;
|
|
421
|
+
let tsconfigRaw;
|
|
422
|
+
const tsconfig = path.join(ctx.root, 'tsconfig.json');
|
|
423
|
+
if (fs.existsSync(tsconfig)) tsconfigRaw = fs.readFileSync(tsconfig, 'utf8');
|
|
424
|
+
const esbuild = await import('esbuild');
|
|
425
|
+
await mapLimit(list, 32, async (rel) => {
|
|
426
|
+
const source = ctx.readText(rel, { kind: 'ts' }).text;
|
|
427
|
+
try {
|
|
428
|
+
const r = await esbuild.transform(source, {
|
|
429
|
+
loader: 'ts',
|
|
430
|
+
sourcefile: rel,
|
|
431
|
+
target: 'esnext',
|
|
432
|
+
tsconfigRaw,
|
|
433
|
+
logLevel: 'silent',
|
|
434
|
+
supported: { 'import-attributes': true },
|
|
435
|
+
});
|
|
436
|
+
const dir = path.posix.dirname(rel);
|
|
437
|
+
// "./x" → la ruta que existirá en la salida (x.ts → x.js, x/index.ts → x/index.js…).
|
|
438
|
+
const complete = (spec) => {
|
|
439
|
+
const base = path.posix.normalize(path.posix.join(dir, spec));
|
|
440
|
+
for (const cand of ['.ts', '.mts', '.js', '.mjs', '/index.ts', '/index.js'].map((s) => base + s)) {
|
|
441
|
+
if (ctx.files.has(cand)) return spec + jsOutPath(cand).slice(base.length);
|
|
442
|
+
}
|
|
443
|
+
return null;
|
|
444
|
+
};
|
|
445
|
+
ctx.tsCode.set(rel, rewriteTsImports(r.code, complete));
|
|
446
|
+
} catch (err) {
|
|
447
|
+
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);
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** <script src="..." data-builder="dependency|own">: el usuario marca librerías (o código propio) en el HTML. */
|
|
454
|
+
const MARK_VALUES = { dependency: 'dependency', dependencia: 'dependency', dep: 'dependency', own: 'own', propio: 'own' };
|
|
455
|
+
function markDependencies(ctx, pages) {
|
|
456
|
+
for (const page of pages.values()) {
|
|
457
|
+
for (const s of page.model.scripts) {
|
|
458
|
+
if (!s.src || !s.attrs.has('data-builder')) continue;
|
|
459
|
+
const raw = String(s.attrs.get('data-builder') ?? '')
|
|
460
|
+
.trim()
|
|
461
|
+
.toLowerCase();
|
|
462
|
+
const value = MARK_VALUES[raw];
|
|
463
|
+
if (!value) {
|
|
464
|
+
ctx.warn(`data-builder="${raw}" no se reconoce; usa "dependency" o "own"`, page.rel);
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
const res = ctx.resolve(s.src.value, page.baseDir);
|
|
468
|
+
if (res.status !== 'ok') continue;
|
|
469
|
+
const prev = ctx.depMarks.get(res.rel);
|
|
470
|
+
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);
|
|
472
|
+
else ctx.depMarks.set(res.rel, value);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
375
477
|
function selectEntries(ctx, pages) {
|
|
376
478
|
const cfg = ctx.cfg;
|
|
377
479
|
const extraEntries = { modules: [], classic: [], css: [] };
|
|
@@ -534,12 +636,35 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
534
636
|
const t0 = Date.now();
|
|
535
637
|
if (mode === 'build') assertSafeOutDir(cfg, { force: opts.force });
|
|
536
638
|
const ctx = new Context(cfg, { ...opts, mode });
|
|
639
|
+
const workers = cfg.workers === 'auto' ? autoWorkers() : cfg.workers;
|
|
640
|
+
ctx.heavy = createHeavy({ root: cfg.root, workers, cacheDir: cfg.cache && mode === 'build' ? defaultCacheDir(cfg.root) : null });
|
|
641
|
+
try {
|
|
642
|
+
return await pipeline(ctx, cfg, opts, mode, t0);
|
|
643
|
+
} finally {
|
|
644
|
+
await ctx.heavy.close();
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
async function pipeline(ctx, cfg, opts, mode, t0) {
|
|
537
649
|
const log = opts.log ?? (() => {});
|
|
650
|
+
// Tiempo de cada fase (perf.phases): para saber dónde se va el tiempo en proyectos reales.
|
|
651
|
+
const phases = [];
|
|
652
|
+
let phaseStart = Date.now();
|
|
653
|
+
let phaseName = 'preparación';
|
|
654
|
+
const mark = (next) => {
|
|
655
|
+
const now = Date.now();
|
|
656
|
+
phases.push([phaseName, now - phaseStart]);
|
|
657
|
+
phaseName = next;
|
|
658
|
+
phaseStart = now;
|
|
659
|
+
};
|
|
538
660
|
if (ctx.obfuscatorOptions) await ensureObfuscator(cfg.root);
|
|
539
661
|
if (cfg.js.minifier === 'terser' && cfg.js.minify) await ensureTerser(cfg.root);
|
|
540
662
|
|
|
663
|
+
mark('páginas');
|
|
541
664
|
// 1. Páginas y entradas
|
|
665
|
+
await transpileTypeScript(ctx);
|
|
542
666
|
const pages = loadPages(ctx);
|
|
667
|
+
markDependencies(ctx, pages);
|
|
543
668
|
const extra = selectEntries(ctx, pages);
|
|
544
669
|
const fullPages = [...pages.values()].filter((p) => p.full);
|
|
545
670
|
const partialPages = [...pages.values()].filter((p) => !p.full);
|
|
@@ -548,6 +673,7 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
548
673
|
for (const p of fullPages) collectRefs(ctx, p);
|
|
549
674
|
for (const p of partialPages) collectRefs(ctx, p);
|
|
550
675
|
|
|
676
|
+
mark('módulos');
|
|
551
677
|
// 2. Módulos ES agrupados por import map
|
|
552
678
|
const groups = new Map();
|
|
553
679
|
const groupFor = (page, importMap) => {
|
|
@@ -619,7 +745,7 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
619
745
|
if (res.mode === 'per-file') {
|
|
620
746
|
ctx.note(`módulos sin empaquetar (cada archivo minificado en su sitio): ${res.reason}`, [...g.pages].join(', ') || 'entries');
|
|
621
747
|
for (const rel of res.inputs) ctx.requireStandalone(rel, 'módulo (modo sin empaquetar)', { module: true });
|
|
622
|
-
for (const rel of g.entries) entryOut.set(`${g.key}\0${rel}`, rel);
|
|
748
|
+
for (const rel of g.entries) entryOut.set(`${g.key}\0${rel}`, jsOutPath(rel));
|
|
623
749
|
for (const [id, v] of g.virtual) {
|
|
624
750
|
const info = analyzeJs(v.code, { module: true });
|
|
625
751
|
if (!info.ok) continue;
|
|
@@ -632,25 +758,20 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
632
758
|
continue;
|
|
633
759
|
}
|
|
634
760
|
log(`Módulos: ${g.entries.size} entradas, ${res.inputs.length} archivos, target ${res.target}`);
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
let code = o.code;
|
|
638
|
-
let map = o.map;
|
|
761
|
+
// Ofuscar/comprimir cada salida en paralelo; registrar después en el orden original (salida determinista).
|
|
762
|
+
const finals = await mapLimit(res.outputs, 64, async (o) => {
|
|
639
763
|
const obfRels = o.virtualId ? [g.virtual.get(o.virtualId).page.rel] : o.isChunk ? o.inputs : [o.entry];
|
|
640
764
|
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
|
-
}
|
|
765
|
+
const r = await obfuscateModule(ctx, o.code, o.outRel);
|
|
766
|
+
return r.note ? { code: o.code, map: o.map, warn: r.note } : { code: r.code, map: null };
|
|
653
767
|
}
|
|
768
|
+
const t = await terserPass(ctx, o.code, { isModule: true, level: levelOf(res.target) });
|
|
769
|
+
return t !== o.code ? { code: t, map: null } : { code: o.code, map: o.map };
|
|
770
|
+
});
|
|
771
|
+
for (const [i, o] of res.outputs.entries()) {
|
|
772
|
+
const sources = o.inputs.length ? o.inputs : o.entry ? [o.entry] : [];
|
|
773
|
+
const { code, map, warn } = finals[i];
|
|
774
|
+
if (warn) ctx.warn(warn, o.outRel);
|
|
654
775
|
if (o.virtualId) {
|
|
655
776
|
const v = g.virtual.get(o.virtualId);
|
|
656
777
|
inlineModuleCode.set(o.virtualId, code.replace(/\n$/, ''));
|
|
@@ -669,6 +790,7 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
669
790
|
}
|
|
670
791
|
}
|
|
671
792
|
|
|
793
|
+
mark('css');
|
|
672
794
|
// 3. CSS enlazado
|
|
673
795
|
const cssOut = new Map();
|
|
674
796
|
const cssEntries = new Set(extra.css);
|
|
@@ -679,10 +801,14 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
679
801
|
if (res.status === 'ok') cssEntries.add(res.rel);
|
|
680
802
|
else if (res.status !== 'skip') ctx.reportBroken(page.rel, l.href.value, res);
|
|
681
803
|
}
|
|
682
|
-
|
|
804
|
+
const cssList = [...cssEntries];
|
|
805
|
+
const cssBuilt = await mapLimit(cssList, 16, (rel) => {
|
|
806
|
+
const hashed = cfg.hash && !extra.css.includes(rel);
|
|
807
|
+
return buildCss(ctx, { entry: rel, outRel: hashed ? `${ctx.chunkDir}/${path.posix.basename(rel)}` : rel });
|
|
808
|
+
});
|
|
809
|
+
for (const [ci, rel] of cssList.entries()) {
|
|
683
810
|
const hashed = cfg.hash && !extra.css.includes(rel);
|
|
684
|
-
const
|
|
685
|
-
const r = await buildCss(ctx, { entry: rel, outRel: provisional });
|
|
811
|
+
const r = cssBuilt[ci];
|
|
686
812
|
if (r.error) {
|
|
687
813
|
ctx.warn(`no se pudo procesar el CSS (${r.error}); se copia tal cual`, rel);
|
|
688
814
|
cssOut.set(rel, ctx.emitCopy(rel, rel, 'copy-fallback'));
|
|
@@ -695,6 +821,7 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
695
821
|
for (const i of r.inputs) ctx.cover(i, i === rel ? 'procesado' : 'empaquetado', outRel);
|
|
696
822
|
}
|
|
697
823
|
|
|
824
|
+
mark('white-space');
|
|
698
825
|
// 4. Detección de white-space: pre (para no colapsar espacios del HTML)
|
|
699
826
|
let collapse = cfg.html.collapseWhitespace;
|
|
700
827
|
if (collapse === 'auto') {
|
|
@@ -708,15 +835,28 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
708
835
|
);
|
|
709
836
|
}
|
|
710
837
|
|
|
838
|
+
mark('scripts clásicos');
|
|
711
839
|
// 5. Scripts clásicos por página
|
|
712
|
-
|
|
840
|
+
// Primero se planifican todas las páginas, luego se minifica/ofusca cada grupo distinto en paralelo
|
|
841
|
+
// y por último se registra todo en el orden original.
|
|
842
|
+
const pageRuns = [];
|
|
843
|
+
const uniqueRuns = new Map();
|
|
713
844
|
for (const page of fullPages) {
|
|
714
845
|
for (const run of classicRuns(ctx, page)) {
|
|
715
|
-
const
|
|
716
|
-
|
|
846
|
+
const key = `${run.items.map((c) => c.rel).join('|')}\0${run.items[0].key}`;
|
|
847
|
+
pageRuns.push({ page, run, key });
|
|
848
|
+
if (!uniqueRuns.has(key)) uniqueRuns.set(key, run);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
const runKeys = [...uniqueRuns.keys()];
|
|
852
|
+
const runResults = await mapLimit(runKeys, 64, (key) => computeClassicRun(ctx, uniqueRuns.get(key)));
|
|
853
|
+
const computedRuns = new Map(runKeys.map((k, i) => [k, runResults[i]]));
|
|
854
|
+
const classicCache = new Map();
|
|
855
|
+
{
|
|
856
|
+
for (const { page, run, key } of pageRuns) {
|
|
717
857
|
let out = classicCache.get(key);
|
|
718
858
|
if (!out) {
|
|
719
|
-
out =
|
|
859
|
+
out = commitClassicRun(ctx, run, page, computedRuns.get(key));
|
|
720
860
|
classicCache.set(key, out);
|
|
721
861
|
}
|
|
722
862
|
if (out.merged) {
|
|
@@ -736,6 +876,7 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
736
876
|
}
|
|
737
877
|
}
|
|
738
878
|
|
|
879
|
+
mark('edición html');
|
|
739
880
|
// 6. Edición de cada página: módulos, CSS, inline, preload
|
|
740
881
|
for (const page of fullPages) {
|
|
741
882
|
for (const m of moduleRefs.filter((x) => x.page === page)) {
|
|
@@ -761,9 +902,11 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
761
902
|
}
|
|
762
903
|
for (const page of pages.values()) await processInline(ctx, page, collapse);
|
|
763
904
|
|
|
905
|
+
mark('workers y sueltos');
|
|
764
906
|
// 7. Workers, archivos sueltos y rutas mencionadas (hasta que no aparezca nada nuevo)
|
|
765
907
|
await drainQueues(ctx, cssOut);
|
|
766
908
|
|
|
909
|
+
mark('no referenciados');
|
|
767
910
|
// 8. Archivos no referenciados
|
|
768
911
|
const unref = [];
|
|
769
912
|
for (const [rel, f] of ctx.files) {
|
|
@@ -781,6 +924,7 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
781
924
|
}
|
|
782
925
|
await drainQueues(ctx, cssOut);
|
|
783
926
|
|
|
927
|
+
mark('html final');
|
|
784
928
|
// 9. Preloads y HTML final
|
|
785
929
|
for (const { page, l } of preloadLinks) {
|
|
786
930
|
const res = ctx.resolve(l.href.value, page.baseDir);
|
|
@@ -811,6 +955,7 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
811
955
|
await finalizePage(ctx, page, collapse);
|
|
812
956
|
}
|
|
813
957
|
|
|
958
|
+
mark('recursos');
|
|
814
959
|
// 10. Recursos: todo lo demás se copia (sin perder nada)
|
|
815
960
|
for (const [rel, f] of ctx.files) {
|
|
816
961
|
if (ctx.hasOwnOutput(rel)) {
|
|
@@ -838,6 +983,7 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
838
983
|
ctx.cover(rel, 'copiado', rel);
|
|
839
984
|
}
|
|
840
985
|
|
|
986
|
+
mark('avisos');
|
|
841
987
|
// 11. Avisos de rutas mencionadas en el código que ya no existirán
|
|
842
988
|
for (const [rel, from] of ctx.mentions) {
|
|
843
989
|
if (!ctx.outputs.has(rel) && ctx.coverage.get(rel)?.every((s) => s.startsWith('empaquetado'))) {
|
|
@@ -851,9 +997,12 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
851
997
|
}
|
|
852
998
|
}
|
|
853
999
|
|
|
1000
|
+
mark('precompresión');
|
|
854
1001
|
// 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]);
|
|
1002
|
+
if (cfg.compress && mode === 'build') await precompress(ctx, cfg.compress === true ? ['gzip', 'brotli'] : [cfg.compress]);
|
|
856
1003
|
|
|
1004
|
+
mark('informe de tamaños');
|
|
1005
|
+
const report = await buildReport(ctx.outputs, ctx.heavy);
|
|
857
1006
|
const result = {
|
|
858
1007
|
version: VERSION,
|
|
859
1008
|
root: cfg.root,
|
|
@@ -873,15 +1022,18 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
873
1022
|
.map((rel) => ({ rel, reason: ctx.dependencyReason(rel) })),
|
|
874
1023
|
vendor: ctx.vendorPackages,
|
|
875
1024
|
obfuscated: !!ctx.obfuscatorOptions,
|
|
876
|
-
report
|
|
1025
|
+
report,
|
|
1026
|
+
perf: Object.assign(ctx.heavy.stats, { phases }),
|
|
877
1027
|
verify: null,
|
|
878
1028
|
durationMs: 0,
|
|
879
1029
|
};
|
|
880
1030
|
if (mode === 'analyze') {
|
|
1031
|
+
mark('fin');
|
|
881
1032
|
result.durationMs = Date.now() - t0;
|
|
882
1033
|
return result;
|
|
883
1034
|
}
|
|
884
1035
|
|
|
1036
|
+
mark('escritura y verificación');
|
|
885
1037
|
// 12. Escritura, verificación y publicación
|
|
886
1038
|
let staging;
|
|
887
1039
|
try {
|
|
@@ -900,6 +1052,7 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
900
1052
|
files: [...ctx.outputs.keys()].sort(),
|
|
901
1053
|
});
|
|
902
1054
|
await swapIntoPlace(cfg, staging);
|
|
1055
|
+
mark('fin');
|
|
903
1056
|
} catch (err) {
|
|
904
1057
|
if (staging && !opts.keepFailed) await discardStaging(staging);
|
|
905
1058
|
throw err;
|
|
@@ -910,26 +1063,21 @@ export async function runPipeline(cfg, opts = {}) {
|
|
|
910
1063
|
|
|
911
1064
|
const COMPRESS_TYPES = new Set(['html', 'css', 'js', 'json', 'svg', 'text', 'map']);
|
|
912
1065
|
|
|
913
|
-
function precompress(ctx, kinds) {
|
|
914
|
-
|
|
915
|
-
|
|
1066
|
+
async function precompress(ctx, kinds) {
|
|
1067
|
+
const items = [...ctx.outputs].filter(([, o]) => COMPRESS_TYPES.has(o.type));
|
|
1068
|
+
// En paralelo (pool de hilos de libuv); se registra en orden. El informe reutiliza estos mismos resultados.
|
|
1069
|
+
const done = await mapLimit(items, 64, async ([, o]) => {
|
|
916
1070
|
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
|
-
}
|
|
1071
|
+
if (buf.length < 1024) return null; // no compensa
|
|
1072
|
+
const [gz, br] = await Promise.all([kinds.includes('gzip') ? gzipOf(buf) : null, kinds.includes('brotli') ? brotliOf(buf) : null]);
|
|
1073
|
+
return { buf, gz, br };
|
|
1074
|
+
});
|
|
1075
|
+
items.forEach(([rel, o], i) => {
|
|
1076
|
+
const d = done[i];
|
|
1077
|
+
if (!d) return;
|
|
1078
|
+
if (d.gz && d.gz.length < d.buf.length) ctx.emit(`${rel}.gz`, d.gz, { type: 'compressed', action: 'gzip', sources: o.sources });
|
|
1079
|
+
if (d.br && d.br.length < d.buf.length) ctx.emit(`${rel}.br`, d.br, { type: 'compressed', action: 'brotli', sources: o.sources });
|
|
1080
|
+
});
|
|
933
1081
|
}
|
|
934
1082
|
|
|
935
1083
|
function sumSizes(ctx, rels) {
|
|
@@ -957,21 +1105,41 @@ function markModuleImport(ctx, spec, baseDir, from, importMap) {
|
|
|
957
1105
|
else ctx.reportBroken(from, spec, r);
|
|
958
1106
|
}
|
|
959
1107
|
|
|
960
|
-
|
|
1108
|
+
/** Parte costosa y sin efectos de un grupo de scripts clásicos: unir, minificar y ofuscar. */
|
|
1109
|
+
async function computeClassicRun(ctx, run) {
|
|
1110
|
+
const items = run.items;
|
|
1111
|
+
const first = items[0];
|
|
1112
|
+
if (items.length === 1 && !first.info.ok) return { copy: true };
|
|
1113
|
+
let mergeError = null;
|
|
1114
|
+
if (items.length > 1) {
|
|
1115
|
+
// Separadores con ; y salto de línea: evitan que un archivo sin ; final se "pegue" al siguiente.
|
|
1116
|
+
const code = items.map((c) => ctx.readText(c.rel, { kind: 'js', charset: c.s.attrs.get('charset') }).text.replace(/\s*$/, '\n')).join(';\n');
|
|
1117
|
+
const info = analyzeJs(code, { module: false });
|
|
1118
|
+
if (info.ok) return { merged: await processClassic(ctx, { code, id: items.map((c) => c.rel).join('+'), info, obfuscate: first.obf }) };
|
|
1119
|
+
mergeError = info.error;
|
|
1120
|
+
}
|
|
1121
|
+
const separate = await Promise.all(
|
|
1122
|
+
items.map((c) => {
|
|
1123
|
+
const { text } = ctx.readText(c.rel, { kind: 'js', charset: c.s.attrs.get('charset') });
|
|
1124
|
+
return processClassic(ctx, { code: text, id: c.rel, info: c.info, obfuscate: c.obf, sourcemap: !!ctx.cfg.sourcemap });
|
|
1125
|
+
}),
|
|
1126
|
+
);
|
|
1127
|
+
return { separate, mergeError };
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
/** Registra el resultado de computeClassicRun (salidas, cobertura y notas) en orden. */
|
|
1131
|
+
function commitClassicRun(ctx, run, page, comp) {
|
|
961
1132
|
const items = run.items;
|
|
962
1133
|
const first = items[0];
|
|
963
|
-
if (
|
|
1134
|
+
if (comp.copy) {
|
|
964
1135
|
ctx.warn(`no se pudo analizar (${first.info.error}); se copia sin cambios`, first.rel);
|
|
965
1136
|
const outRel = ctx.emitCopy(first.rel, first.rel, 'copy-fallback');
|
|
966
1137
|
ctx.cover(first.rel, 'copiado sin cambios (no se pudo analizar)', outRel);
|
|
967
1138
|
return { merged: false, outs: [{ outRel, changed: false }] };
|
|
968
1139
|
}
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
const info = analyzeJs(code, { module: false });
|
|
973
|
-
if (info.ok) {
|
|
974
|
-
const r = await processClassic(ctx, { code, id: items.map((c) => c.rel).join('+'), info, obfuscate: first.obf });
|
|
1140
|
+
{
|
|
1141
|
+
if (comp.merged) {
|
|
1142
|
+
const r = comp.merged;
|
|
975
1143
|
for (const n of r.notes) ctx.note(n, `scripts unidos (${items.map((c) => c.rel).join(', ')})`);
|
|
976
1144
|
const outRel = ctx.emitCode(`${ctx.chunkDir}/scripts-${shortHash(r.code)}.js`, r.code, null, {
|
|
977
1145
|
type: 'js',
|
|
@@ -985,16 +1153,15 @@ async function processClassicRun(ctx, run, page) {
|
|
|
985
1153
|
for (const c of items) ctx.cover(c.rel, 'unido', outRel);
|
|
986
1154
|
return { merged: true, outRel };
|
|
987
1155
|
}
|
|
988
|
-
ctx.warn(`no se pudieron unir (${
|
|
1156
|
+
if (comp.mergeError) ctx.warn(`no se pudieron unir (${comp.mergeError}); se procesan por separado`, page.rel);
|
|
989
1157
|
}
|
|
990
1158
|
// Un solo archivo, o una unión fallida: cada uno se procesa en su sitio.
|
|
991
1159
|
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 });
|
|
1160
|
+
for (const [i, c] of items.entries()) {
|
|
1161
|
+
const r = comp.separate[i];
|
|
995
1162
|
for (const n of r.notes) ctx.note(n, c.rel);
|
|
996
1163
|
if (c.reasons.length && c.reasons[0] !== 'unión desactivada') ctx.note(`no se une con otros scripts: ${c.reasons.join(', ')}`, c.rel);
|
|
997
|
-
const outRel = ctx.cfg.hash ? ctx.hashedPath(c.rel, r.code, '.js') : c.rel;
|
|
1164
|
+
const outRel = ctx.cfg.hash ? ctx.hashedPath(c.rel, r.code, '.js') : jsOutPath(c.rel);
|
|
998
1165
|
ctx.emitCode(outRel, r.code, r.map, { type: 'js', action: 'minify', sources: [c.rel], before: ctx.files.get(c.rel).size });
|
|
999
1166
|
ctx.cover(c.rel, 'procesado', outRel);
|
|
1000
1167
|
outs.push({ outRel, changed: true });
|
|
@@ -1125,52 +1292,65 @@ async function drainQueues(ctx, cssOut) {
|
|
|
1125
1292
|
}
|
|
1126
1293
|
let code = r.code;
|
|
1127
1294
|
if (ctx.shouldObfuscate([rel])) {
|
|
1128
|
-
const o = obfuscateModule(ctx, code, rel);
|
|
1295
|
+
const o = await obfuscateModule(ctx, code, rel);
|
|
1129
1296
|
if (o.note) ctx.warn(o.note, rel);
|
|
1130
1297
|
code = o.code;
|
|
1131
1298
|
} else code = await terserPass(ctx, code, { isModule: true, level: 2020 });
|
|
1132
1299
|
const sources = [...r.consumed];
|
|
1133
|
-
ctx.emitCode(rel, code, code === r.code ? r.map : null, {
|
|
1300
|
+
ctx.emitCode(jsOutPath(rel), code, code === r.code ? r.map : null, {
|
|
1134
1301
|
type: 'js',
|
|
1135
1302
|
module: true,
|
|
1136
1303
|
action: 'worker',
|
|
1137
1304
|
sources,
|
|
1138
1305
|
before: sumSizes(ctx, sources),
|
|
1139
1306
|
});
|
|
1140
|
-
for (const s of sources) ctx.cover(s, s === rel ? 'procesado (worker)' : 'empaquetado', rel);
|
|
1307
|
+
for (const s of sources) ctx.cover(s, s === rel ? 'procesado (worker)' : 'empaquetado', jsOutPath(rel));
|
|
1141
1308
|
} else ctx.requireStandalone(rel, `worker clásico (${w.kind})`, { classic: true });
|
|
1142
1309
|
}
|
|
1310
|
+
// Análisis en orden, trabajo pesado en paralelo, registro otra vez en orden.
|
|
1311
|
+
const jobs = [];
|
|
1143
1312
|
for (const [rel, s] of ctx.standalone) {
|
|
1144
1313
|
if (ctx.done.has(`s:${rel}`)) continue;
|
|
1145
1314
|
ctx.done.add(`s:${rel}`);
|
|
1146
1315
|
progressed = true;
|
|
1147
1316
|
if (ctx.hasOwnOutput(rel) || !ctx.files.has(rel)) continue;
|
|
1148
|
-
|
|
1317
|
+
const job = planStandalone(ctx, rel, s, cssOut);
|
|
1318
|
+
if (job) jobs.push(job);
|
|
1149
1319
|
}
|
|
1320
|
+
const results = await mapLimit(jobs, 64, (j) => j.run());
|
|
1321
|
+
jobs.forEach((j, i) => j.commit(results[i]));
|
|
1150
1322
|
if (!progressed) return;
|
|
1151
1323
|
}
|
|
1152
1324
|
throw new BuildError('Demasiadas rondas al resolver dependencias (¿ciclo inesperado?).');
|
|
1153
1325
|
}
|
|
1154
1326
|
|
|
1155
|
-
|
|
1327
|
+
/**
|
|
1328
|
+
* Prepara un archivo suelto: el análisis (que descubre más dependencias) se hace ya, en orden;
|
|
1329
|
+
* devuelve {run, commit}: run() es la parte costosa y sin efectos, commit() registra el resultado.
|
|
1330
|
+
*/
|
|
1331
|
+
function planStandalone(ctx, rel, s, cssOut) {
|
|
1156
1332
|
const ext = extOf(rel);
|
|
1157
1333
|
const dir = path.posix.dirname(rel) === '.' ? '' : path.posix.dirname(rel);
|
|
1158
1334
|
if (CSS_EXT.has(ext)) {
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1335
|
+
// buildCss registra los recursos de url() mientras compila: eso solo añade trabajo a la siguiente ronda.
|
|
1336
|
+
return {
|
|
1337
|
+
run: () => buildCss(ctx, { entry: rel, outRel: rel }),
|
|
1338
|
+
commit: (r) => {
|
|
1339
|
+
if (r.error) {
|
|
1340
|
+
ctx.warn(`no se pudo procesar el CSS (${r.error.split('\n')[0]}); se copia tal cual`, rel);
|
|
1341
|
+
ctx.emitCopy(rel, rel, 'copy-fallback');
|
|
1342
|
+
ctx.cover(rel, 'copiado sin cambios (error al procesar)', rel);
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
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);
|
|
1347
|
+
if (!cssOut.has(rel)) cssOut.set(rel, rel);
|
|
1348
|
+
},
|
|
1349
|
+
};
|
|
1170
1350
|
}
|
|
1171
1351
|
if (!JS_EXT.has(ext)) {
|
|
1172
1352
|
ctx.registerAsset(rel, s.reason);
|
|
1173
|
-
return;
|
|
1353
|
+
return null;
|
|
1174
1354
|
}
|
|
1175
1355
|
const auto = ctx.jsInfo(rel, 'auto');
|
|
1176
1356
|
const isModule = !s.classic && (s.module || (auto.ok && (auto.hasModuleSyntax || auto.hasImportMeta)) || (!auto.ok && auto.isModule));
|
|
@@ -1179,12 +1359,11 @@ async function processStandalone(ctx, rel, s, cssOut) {
|
|
|
1179
1359
|
ctx.warn(`no se pudo analizar (${info.error}); se copia sin cambios`, rel);
|
|
1180
1360
|
ctx.emitCopy(rel, rel, 'copy-fallback');
|
|
1181
1361
|
ctx.cover(rel, 'copiado sin cambios (no se pudo analizar)', rel);
|
|
1182
|
-
return;
|
|
1362
|
+
return null;
|
|
1183
1363
|
}
|
|
1184
1364
|
ctx.noteJsInfo(rel, info, dir);
|
|
1185
1365
|
const { text } = ctx.readText(rel, { kind: 'js' });
|
|
1186
1366
|
const obfuscate = ctx.shouldObfuscate([rel]);
|
|
1187
|
-
let r;
|
|
1188
1367
|
if (isModule) {
|
|
1189
1368
|
for (const imp of info.imports) if (imp.spec) markModuleImport(ctx, imp.spec, dir, rel, null);
|
|
1190
1369
|
for (const u of info.urls) {
|
|
@@ -1192,11 +1371,15 @@ async function processStandalone(ctx, rel, s, cssOut) {
|
|
|
1192
1371
|
if (res.status === 'ok') ctx.urlTarget(res.rel, rel);
|
|
1193
1372
|
else ctx.reportBroken(rel, u.spec, res);
|
|
1194
1373
|
}
|
|
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
1374
|
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1375
|
+
const job = { code: text, id: rel, info, obfuscate, sourcemap: !!ctx.cfg.sourcemap };
|
|
1376
|
+
return {
|
|
1377
|
+
run: () => (isModule ? processModuleStandalone(ctx, job) : processClassic(ctx, job)),
|
|
1378
|
+
commit: (r) => {
|
|
1379
|
+
for (const n of r.notes) ctx.note(n, rel);
|
|
1380
|
+
const out = jsOutPath(rel);
|
|
1381
|
+
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);
|
|
1383
|
+
},
|
|
1384
|
+
};
|
|
1202
1385
|
}
|