vanilla-builder 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.es.md +531 -154
- package/README.md +530 -155
- package/bin/builder.js +345 -63
- package/package.json +16 -3
- package/src/browsers.js +128 -0
- package/src/budgets.js +77 -0
- package/src/builder.js +367 -103
- package/src/config.js +106 -23
- package/src/deps.js +12 -7
- package/src/files.js +12 -7
- package/src/html.js +10 -3
- package/src/i18n.js +33 -0
- package/src/images.js +61 -0
- package/src/js.js +99 -42
- package/src/jsinfo.js +63 -4
- 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 +6 -4
- package/src/report.js +9 -3
- package/src/server.js +8 -4
- package/src/tersercore.js +2 -2
- package/src/verify.js +26 -6
- package/src/worker.js +5 -1
package/src/pool.js
CHANGED
|
@@ -11,6 +11,7 @@ import { ensureObfuscator, obfuscateSafe } from './obfuscate.js';
|
|
|
11
11
|
import { importOptional } from './optional.js';
|
|
12
12
|
import { terserMinify } from './tersercore.js';
|
|
13
13
|
import { jsImports } from './jsimports.js';
|
|
14
|
+
import { L, getLang } from './i18n.js';
|
|
14
15
|
|
|
15
16
|
const WORKER_URL = new URL('./worker.js', import.meta.url);
|
|
16
17
|
const OWN_VERSION = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
@@ -84,7 +85,8 @@ export function createHeavy({ root, workers = autoWorkers(), cacheDir = null })
|
|
|
84
85
|
|
|
85
86
|
function cacheKey(kind, payload) {
|
|
86
87
|
const engine = kind === 'obfuscate' ? 'javascript-obfuscator' : kind === 'terser' ? 'terser' : 'acorn';
|
|
87
|
-
|
|
88
|
+
// El idioma entra en la clave: los motivos de rechazo ("no se ofuscó: …") se guardan ya traducidos.
|
|
89
|
+
const meta = JSON.stringify([CACHE_FORMAT, OWN_VERSION, kind, versionOf(engine), getLang(), payload.opts, plain(payload.opts.globals)]);
|
|
88
90
|
return crypto.createHash('sha256').update(meta).update('\0').update(payload.code).digest('hex');
|
|
89
91
|
}
|
|
90
92
|
|
|
@@ -175,12 +177,12 @@ export function createHeavy({ root, workers = autoWorkers(), cacheDir = null })
|
|
|
175
177
|
await ensureObfuscator(root);
|
|
176
178
|
return obfuscateSafe(payload.code, payload.opts);
|
|
177
179
|
}
|
|
178
|
-
inProcessTerser ??= await importOptional('terser', root, 'para la compresión máxima');
|
|
180
|
+
inProcessTerser ??= await importOptional('terser', root, L('para la compresión máxima', 'for maximum compression'));
|
|
179
181
|
return terserMinify(inProcessTerser, payload.code, payload.opts);
|
|
180
182
|
}
|
|
181
183
|
|
|
182
184
|
function spawn() {
|
|
183
|
-
const w = new Worker(WORKER_URL, { workerData: { root } });
|
|
185
|
+
const w = new Worker(WORKER_URL, { workerData: { root, lang: getLang() } });
|
|
184
186
|
w.on('message', ({ id, result, error }) => {
|
|
185
187
|
const p = pending.get(id);
|
|
186
188
|
pending.delete(id);
|
|
@@ -283,7 +285,7 @@ export function createHeavy({ root, workers = autoWorkers(), cacheDir = null })
|
|
|
283
285
|
const key = cacheDir
|
|
284
286
|
? crypto
|
|
285
287
|
.createHash('sha256')
|
|
286
|
-
.update(JSON.stringify([CACHE_FORMAT, kind]))
|
|
288
|
+
.update(JSON.stringify([CACHE_FORMAT, OWN_VERSION, getLang(), kind]))
|
|
287
289
|
.update('\0')
|
|
288
290
|
.update(keyData)
|
|
289
291
|
.digest('hex')
|
package/src/report.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Informe de tamaños (original, final, gzip y brotli) por archivo y totales.
|
|
2
2
|
import { formatBytes } from './util.js';
|
|
3
3
|
import { gzipOf, brotliOf, mapLimit } from './compress.js';
|
|
4
|
+
import { L } from './i18n.js';
|
|
4
5
|
|
|
5
6
|
const COMPRESSIBLE = new Set(['html', 'css', 'js', 'json', 'svg', 'text']);
|
|
6
7
|
|
|
@@ -51,15 +52,20 @@ export function renderReport(report, C, { verbose = false } = {}) {
|
|
|
51
52
|
const lines = [];
|
|
52
53
|
const shown = report.rows.filter((r) => verbose || r.action !== 'copy');
|
|
53
54
|
const hidden = report.rows.filter((r) => !verbose && r.action === 'copy');
|
|
54
|
-
const hiddenLabel = `+ ${hidden.length} recursos copiados sin cambios
|
|
55
|
+
const hiddenLabel = L(`+ ${hidden.length} recursos copiados sin cambios`, `+ ${hidden.length} assets copied unchanged`);
|
|
55
56
|
const w = Math.min(60, Math.max(20, ...shown.map((r) => r.file.length), hidden.length ? hiddenLabel.length : 0));
|
|
56
57
|
const pad = (s, n) => (s.length > n ? `…${s.slice(-(n - 1))}` : s.padEnd(n));
|
|
57
58
|
const num = (n) => (n == null ? '—' : formatBytes(n)).padStart(10);
|
|
58
59
|
lines.push(
|
|
59
|
-
C.gray(
|
|
60
|
+
C.gray(
|
|
61
|
+
L(
|
|
62
|
+
`${'Archivo'.padEnd(w)} ${'Antes'.padStart(10)} ${'Después'.padStart(10)} ${'gzip'.padStart(10)} ${'brotli'.padStart(10)} Ahorro`,
|
|
63
|
+
`${'File'.padEnd(w)} ${'Before'.padStart(10)} ${'After'.padStart(10)} ${'gzip'.padStart(10)} ${'brotli'.padStart(10)} Saved`,
|
|
64
|
+
),
|
|
65
|
+
),
|
|
60
66
|
);
|
|
61
67
|
for (const r of shown) {
|
|
62
|
-
const src = r.sources && r.sources.length > 1 ? C.gray(` (${r.sources.length} archivos)`) : '';
|
|
68
|
+
const src = r.sources && r.sources.length > 1 ? C.gray(L(` (${r.sources.length} archivos)`, ` (${r.sources.length} files)`)) : '';
|
|
63
69
|
lines.push(`${pad(r.file, w)} ${num(r.before)} ${num(r.size)} ${num(r.gzip)} ${num(r.brotli)} ${pct(r.before, r.size).padStart(6)}${src}`);
|
|
64
70
|
}
|
|
65
71
|
if (hidden.length) {
|
package/src/server.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import http from 'node:http';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { L } from './i18n.js';
|
|
5
6
|
|
|
6
7
|
const MIME = {
|
|
7
8
|
'.html': 'text/html; charset=utf-8',
|
|
@@ -42,7 +43,8 @@ const MIME = {
|
|
|
42
43
|
|
|
43
44
|
const RELOAD_SCRIPT = `<script>(()=>{const s=new EventSource('/__builder/events');let box;
|
|
44
45
|
s.addEventListener('reload',()=>location.reload());
|
|
45
|
-
s.addEventListener('
|
|
46
|
+
s.addEventListener('css',()=>{for(const l of document.querySelectorAll('link[rel~="stylesheet"][href]')){const u=new URL(l.href,location.href);if(u.origin!==location.origin)continue;u.searchParams.set('vb',Date.now());const n=l.cloneNode();n.href=u.href;n.onload=n.onerror=()=>l.remove();l.after(n)}});
|
|
47
|
+
s.addEventListener('error-build',e=>{box??=document.body.appendChild(Object.assign(document.createElement('pre'),{style:'position:fixed;inset:auto 0 0 0;max-height:50vh;overflow:auto;margin:0;padding:12px;background:#300;color:#fdd;font:12px/1.4 monospace;z-index:2147483647;white-space:pre-wrap'}));box.textContent=JSON.parse(e.data)});
|
|
46
48
|
s.addEventListener('ok',()=>{box?.remove();box=null});})();</script>`;
|
|
47
49
|
|
|
48
50
|
/**
|
|
@@ -56,7 +58,7 @@ export function startServer({ dir, port = 5173, host = '127.0.0.1', liveReload =
|
|
|
56
58
|
try {
|
|
57
59
|
pathname = decodeURIComponent(new URL(req.url, 'http://x').pathname);
|
|
58
60
|
} catch {
|
|
59
|
-
res.writeHead(400).end('URL inválida');
|
|
61
|
+
res.writeHead(400).end(L('URL inválida', 'Invalid URL'));
|
|
60
62
|
return;
|
|
61
63
|
}
|
|
62
64
|
if (liveReload && pathname === '/__builder/events') {
|
|
@@ -84,7 +86,7 @@ export function startServer({ dir, port = 5173, host = '127.0.0.1', liveReload =
|
|
|
84
86
|
}
|
|
85
87
|
fs.readFile(file, (err, data) => {
|
|
86
88
|
if (err) {
|
|
87
|
-
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }).end(`No encontrado: ${pathname}`);
|
|
89
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }).end(L(`No encontrado: ${pathname}`, `Not found: ${pathname}`));
|
|
88
90
|
return;
|
|
89
91
|
}
|
|
90
92
|
const ext = path.extname(file).toLowerCase();
|
|
@@ -111,7 +113,9 @@ export function startServer({ dir, port = 5173, host = '127.0.0.1', liveReload =
|
|
|
111
113
|
url: `http://${host === '0.0.0.0' ? 'localhost' : host}:${addr.port}/`,
|
|
112
114
|
port: addr.port,
|
|
113
115
|
reload: () => send('reload'),
|
|
114
|
-
|
|
116
|
+
// Solo cambió CSS: se recargan las hojas de estilos sin perder el estado de la página.
|
|
117
|
+
reloadCss: () => send('css'),
|
|
118
|
+
buildError: (msg) => send('error-build', `${L('Builder: error al compilar', 'Builder: build failed')}\n\n${msg}`),
|
|
115
119
|
buildOk: () => send('ok'),
|
|
116
120
|
close: () =>
|
|
117
121
|
new Promise((r) => {
|
package/src/tersercore.js
CHANGED
|
@@ -5,7 +5,7 @@ import { compareGlobals, moduleShape } from './jsinfo.js';
|
|
|
5
5
|
* Comprime con terser y solo acepta el resultado si es más pequeño y conserva las globales (scripts)
|
|
6
6
|
* o las exportaciones/importaciones (módulos). Devuelve el código original si no compensa.
|
|
7
7
|
*/
|
|
8
|
-
export async function terserMinify(terser, code, { isModule, level, globals, keepNames = false }) {
|
|
8
|
+
export async function terserMinify(terser, code, { isModule, level, globals, allowPrefix = null, keepNames = false }) {
|
|
9
9
|
const ecma = Math.min(2020, Math.max(5, level ?? 2020));
|
|
10
10
|
let out;
|
|
11
11
|
try {
|
|
@@ -28,6 +28,6 @@ export async function terserMinify(terser, code, { isModule, level, globals, kee
|
|
|
28
28
|
const a = moduleShape(code);
|
|
29
29
|
const b = moduleShape(out);
|
|
30
30
|
if (!a || !b || a.exports !== b.exports || a.imports !== b.imports) return code;
|
|
31
|
-
} else if (!globals || !compareGlobals(globals, out).ok) return code;
|
|
31
|
+
} else if (!globals || !compareGlobals(globals, out, allowPrefix).ok) return code;
|
|
32
32
|
return out;
|
|
33
33
|
}
|
package/src/verify.js
CHANGED
|
@@ -5,6 +5,7 @@ import { jsImports } from './jsimports.js';
|
|
|
5
5
|
import { mapLimit } from './compress.js';
|
|
6
6
|
import { parseHtml } from './html.js';
|
|
7
7
|
import { classifyUrl, parseSrcset } from './urls.js';
|
|
8
|
+
import { L } from './i18n.js';
|
|
8
9
|
|
|
9
10
|
function unescapeCss(s) {
|
|
10
11
|
return s.replace(/\\([0-9a-f]{1,6}[ \t\n]?|[\s\S])/gi, (_, e) => (/^[0-9a-f]/i.test(e) ? String.fromCodePoint(parseInt(e, 16)) : e));
|
|
@@ -82,7 +83,12 @@ export async function verifyOutput(staging, outputs, ctx) {
|
|
|
82
83
|
const candidates = rel === '' || rel.endsWith('/') ? [`${rel}index.html`] : page ? [rel, `${rel}/index.html`, `${rel}.html`] : [rel];
|
|
83
84
|
if (candidates.some(exists)) continue;
|
|
84
85
|
const ci = candidates.map((c) => lower.get(c.toLowerCase())).find(Boolean);
|
|
85
|
-
errors.push(
|
|
86
|
+
errors.push(
|
|
87
|
+
L(
|
|
88
|
+
`${fromRel}: la referencia "${u}" no existe en la salida${ci ? ` (existe "${ci}" con otras mayúsculas)` : ''}.`,
|
|
89
|
+
`${fromRel}: the reference "${u}" does not exist in the output${ci ? ` ("${ci}" exists with different capitalization)` : ''}.`,
|
|
90
|
+
),
|
|
91
|
+
);
|
|
86
92
|
}
|
|
87
93
|
};
|
|
88
94
|
|
|
@@ -98,11 +104,12 @@ export async function verifyOutput(staging, outputs, ctx) {
|
|
|
98
104
|
try {
|
|
99
105
|
st = fs.statSync(dest);
|
|
100
106
|
} catch {
|
|
101
|
-
errors.push(`${rel}: no se escribió en disco.`);
|
|
107
|
+
errors.push(L(`${rel}: no se escribió en disco.`, `${rel}: was not written to disk.`));
|
|
102
108
|
continue;
|
|
103
109
|
}
|
|
104
110
|
const expected = o.copyFrom ? o.size : Buffer.byteLength(o.data);
|
|
105
|
-
if (st.size !== expected)
|
|
111
|
+
if (st.size !== expected)
|
|
112
|
+
errors.push(L(`${rel}: tamaño en disco ${st.size} ≠ esperado ${expected}.`, `${rel}: size on disk ${st.size} ≠ expected ${expected}.`));
|
|
106
113
|
if (o.copyFrom) continue;
|
|
107
114
|
const dir = path.posix.dirname(rel) === '.' ? '' : path.posix.dirname(rel);
|
|
108
115
|
if (o.type === 'html') {
|
|
@@ -115,7 +122,13 @@ export async function verifyOutput(staging, outputs, ctx) {
|
|
|
115
122
|
if (s.src) check(rel, s.src.value, baseDir);
|
|
116
123
|
if (s.inline && s.kind === 'module') {
|
|
117
124
|
const r = jsImports(s.inline.code, true);
|
|
118
|
-
if (r.error)
|
|
125
|
+
if (r.error)
|
|
126
|
+
errors.push(
|
|
127
|
+
L(
|
|
128
|
+
`${rel}: un <script type="module"> inline no es JS válido (${r.error}).`,
|
|
129
|
+
`${rel}: an inline <script type="module"> is not valid JS (${r.error}).`,
|
|
130
|
+
),
|
|
131
|
+
);
|
|
119
132
|
else for (const spec of r.imports) if (/^\.{0,2}\//.test(spec)) check(rel, spec, baseDir);
|
|
120
133
|
}
|
|
121
134
|
}
|
|
@@ -127,12 +140,19 @@ export async function verifyOutput(staging, outputs, ctx) {
|
|
|
127
140
|
} else if (o.type === 'js') {
|
|
128
141
|
stats.js++;
|
|
129
142
|
const r = parsed.get(rel);
|
|
130
|
-
if (r.error)
|
|
143
|
+
if (r.error)
|
|
144
|
+
errors.push(L(`${rel}: el JavaScript generado no es válido (${r.error}).`, `${rel}: the generated JavaScript is not valid (${r.error}).`));
|
|
131
145
|
else for (const spec of r.imports) if (/^\.{0,2}\//.test(spec)) check(rel, spec, dir);
|
|
132
146
|
}
|
|
133
147
|
}
|
|
134
148
|
|
|
135
149
|
const missing = [...ctx.files.keys()].filter((rel) => !ctx.coverage.has(rel));
|
|
136
|
-
for (const rel of missing)
|
|
150
|
+
for (const rel of missing)
|
|
151
|
+
errors.push(
|
|
152
|
+
L(
|
|
153
|
+
`${rel}: Builder no registró qué hizo con este archivo (no se publicaría).`,
|
|
154
|
+
`${rel}: Builder did not record what it did with this file (it would not be published).`,
|
|
155
|
+
),
|
|
156
|
+
);
|
|
137
157
|
return { errors, warnings, stats };
|
|
138
158
|
}
|
package/src/worker.js
CHANGED
|
@@ -4,9 +4,13 @@ import { ensureObfuscator, obfuscateSafe } from './obfuscate.js';
|
|
|
4
4
|
import { importOptional } from './optional.js';
|
|
5
5
|
import { terserMinify } from './tersercore.js';
|
|
6
6
|
import { jsImports } from './jsimports.js';
|
|
7
|
+
import { L, setLang } from './i18n.js';
|
|
7
8
|
|
|
8
9
|
let terser = null;
|
|
9
10
|
|
|
11
|
+
// Mismo idioma que el proceso principal (puede venir de --lang).
|
|
12
|
+
setLang(workerData.lang);
|
|
13
|
+
|
|
10
14
|
parentPort.on('message', async ({ id, kind, payload }) => {
|
|
11
15
|
try {
|
|
12
16
|
let result;
|
|
@@ -14,7 +18,7 @@ parentPort.on('message', async ({ id, kind, payload }) => {
|
|
|
14
18
|
await ensureObfuscator(workerData.root);
|
|
15
19
|
result = obfuscateSafe(payload.code, payload.opts);
|
|
16
20
|
} else if (kind === 'terser') {
|
|
17
|
-
terser ??= await importOptional('terser', workerData.root, 'para la compresión máxima');
|
|
21
|
+
terser ??= await importOptional('terser', workerData.root, L('para la compresión máxima', 'for maximum compression'));
|
|
18
22
|
result = await terserMinify(terser, payload.code, payload.opts);
|
|
19
23
|
} else if (kind === 'imports') {
|
|
20
24
|
result = jsImports(payload.code, payload.opts.isModule);
|