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/files.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import picomatch from 'picomatch';
|
|
5
5
|
import { CONFIG_FILES } from './config.js';
|
|
6
6
|
import { relFrom, toPosix, extOf } from './util.js';
|
|
7
|
+
import { L } from './i18n.js';
|
|
7
8
|
|
|
8
9
|
/** Archivos de desarrollo que nunca deberían publicarse. Se desactiva con defaultIgnore:false. */
|
|
9
10
|
export const DEFAULT_IGNORE = [
|
|
@@ -33,9 +34,16 @@ export const DEFAULT_IGNORE = [
|
|
|
33
34
|
'**/eslint.config.*',
|
|
34
35
|
'**/prettier.config.*',
|
|
35
36
|
'tsconfig.json',
|
|
37
|
+
'tsconfig.*.json',
|
|
36
38
|
'jsconfig.json',
|
|
39
|
+
// Declaraciones de tipos: no contienen código ejecutable.
|
|
40
|
+
'**/*.d.ts',
|
|
41
|
+
'**/*.d.mts',
|
|
42
|
+
'**/*.d.cts',
|
|
37
43
|
'**/*.test.js',
|
|
38
44
|
'**/*.spec.js',
|
|
45
|
+
'**/*.test.ts',
|
|
46
|
+
'**/*.spec.ts',
|
|
39
47
|
'**/__tests__/**',
|
|
40
48
|
'**/*.map',
|
|
41
49
|
];
|
|
@@ -78,7 +86,7 @@ export function scanProject(cfg) {
|
|
|
78
86
|
try {
|
|
79
87
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
80
88
|
} catch (err) {
|
|
81
|
-
ignored.push({ rel: relFrom(root, dir) || '.', reason: `no se pudo leer (${err.code})` });
|
|
89
|
+
ignored.push({ rel: relFrom(root, dir) || '.', reason: L(`no se pudo leer (${err.code})`, `could not be read (${err.code})`) });
|
|
82
90
|
return;
|
|
83
91
|
}
|
|
84
92
|
entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
@@ -93,9 +101,10 @@ export function scanProject(cfg) {
|
|
|
93
101
|
const st = fs.statSync(abs);
|
|
94
102
|
isFile = st.isFile();
|
|
95
103
|
isDir = false;
|
|
96
|
-
if (st.isDirectory())
|
|
104
|
+
if (st.isDirectory())
|
|
105
|
+
ignored.push({ rel, reason: L('enlace simbólico a carpeta (no se sigue)', 'symbolic link to a folder (not followed)') });
|
|
97
106
|
} catch {
|
|
98
|
-
ignored.push({ rel, reason: 'enlace simbólico roto' });
|
|
107
|
+
ignored.push({ rel, reason: L('enlace simbólico roto', 'broken symbolic link') });
|
|
99
108
|
continue;
|
|
100
109
|
}
|
|
101
110
|
}
|
|
@@ -106,7 +115,10 @@ export function scanProject(cfg) {
|
|
|
106
115
|
const byUser = isUser(rel) || isUser(probe);
|
|
107
116
|
const wanted = cfg.include.some((p) => normalizePattern(p).startsWith(`${rel}/`) || p.startsWith('**'));
|
|
108
117
|
if ((byUser || isDefault(probe)) && !wanted) {
|
|
109
|
-
ignored.push({
|
|
118
|
+
ignored.push({
|
|
119
|
+
rel: `${rel}/`,
|
|
120
|
+
reason: byUser ? L('ignore (configuración)', 'ignore (configuration)') : L('excluido por defecto', 'excluded by default'),
|
|
121
|
+
});
|
|
110
122
|
continue;
|
|
111
123
|
}
|
|
112
124
|
walk(abs);
|
|
@@ -116,9 +128,9 @@ export function scanProject(cfg) {
|
|
|
116
128
|
const forced = isIncluded(rel);
|
|
117
129
|
let reason = null;
|
|
118
130
|
if (!forced) {
|
|
119
|
-
if (isSecret(rel)) reason = 'posible secreto (excluido por seguridad)';
|
|
120
|
-
else if (isUser(rel)) reason = 'ignore (configuración)';
|
|
121
|
-
else if (isDefault(rel)) reason = 'excluido por defecto';
|
|
131
|
+
if (isSecret(rel)) reason = L('posible secreto (excluido por seguridad)', 'possible secret (excluded for safety)');
|
|
132
|
+
else if (isUser(rel)) reason = L('ignore (configuración)', 'ignore (configuration)');
|
|
133
|
+
else if (isDefault(rel)) reason = L('excluido por defecto', 'excluded by default');
|
|
122
134
|
}
|
|
123
135
|
if (reason) {
|
|
124
136
|
ignored.push({ rel, reason });
|
package/src/html.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// HTML: extrae referencias con su posición exacta, aplica ediciones puntuales y minifica con verificación.
|
|
2
2
|
import { parse } from 'parse5';
|
|
3
|
+
import { L } from './i18n.js';
|
|
3
4
|
|
|
4
5
|
const JS_TYPES = new Set([
|
|
5
6
|
'',
|
|
@@ -215,7 +216,7 @@ export function applyEdits(text, edits) {
|
|
|
215
216
|
let out = text;
|
|
216
217
|
let limit = Infinity;
|
|
217
218
|
for (const e of sorted) {
|
|
218
|
-
if (e.end > limit) throw new Error(`Ediciones superpuestas en la posición ${e.start}`);
|
|
219
|
+
if (e.end > limit) throw new Error(L(`Ediciones superpuestas en la posición ${e.start}`, `Overlapping edits at position ${e.start}`));
|
|
219
220
|
out = out.slice(0, e.start) + e.text + out.slice(e.end);
|
|
220
221
|
limit = e.start;
|
|
221
222
|
}
|
|
@@ -324,7 +325,7 @@ export async function minifyHtml(text, { removeComments, collapseWhitespace }) {
|
|
|
324
325
|
try {
|
|
325
326
|
out = applyEdits(text, minifyEdits(text, { removeComments, collapseWhitespace }));
|
|
326
327
|
} catch (err) {
|
|
327
|
-
return { code: text, note: `no se pudo minificar el HTML (${err.message})` };
|
|
328
|
+
return { code: text, note: L(`no se pudo minificar el HTML (${err.message})`, `could not minify the HTML (${err.message})`) };
|
|
328
329
|
}
|
|
329
330
|
const before = htmlSignature(text);
|
|
330
331
|
const after = htmlSignature(out);
|
|
@@ -332,7 +333,13 @@ export async function minifyHtml(text, { removeComments, collapseWhitespace }) {
|
|
|
332
333
|
let i = 0;
|
|
333
334
|
while (i < before.length && before[i] === after[i]) i++;
|
|
334
335
|
const near = before.slice(Math.max(0, i - 40), i + 40).replace(/\s+/g, ' ');
|
|
335
|
-
return {
|
|
336
|
+
return {
|
|
337
|
+
code: text,
|
|
338
|
+
note: L(
|
|
339
|
+
`la minificación cambiaba la estructura cerca de «${near}»; se deja sin minificar`,
|
|
340
|
+
`minification changed the structure near «${near}»; left unminified`,
|
|
341
|
+
),
|
|
342
|
+
};
|
|
336
343
|
}
|
|
337
344
|
return { code: out };
|
|
338
345
|
}
|
package/src/i18n.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Idioma de los mensajes: inglés o español.
|
|
2
|
+
// Prioridad: --lang / setLang() > BUILDER_LANG > LC_ALL / LC_MESSAGES / LANG > idioma del sistema > inglés.
|
|
3
|
+
// Los campos de --json, la configuración y los nombres de fase no se traducen nunca.
|
|
4
|
+
|
|
5
|
+
function detect() {
|
|
6
|
+
const env = process.env;
|
|
7
|
+
const explicit = env.BUILDER_LANG;
|
|
8
|
+
if (explicit) return /^es/i.test(explicit) ? 'es' : 'en';
|
|
9
|
+
const posix = env.LC_ALL || env.LC_MESSAGES || env.LANG;
|
|
10
|
+
// "C" y "POSIX" no dicen nada del idioma de la persona: se mira el del sistema.
|
|
11
|
+
if (posix && !/^(C|POSIX)(\.|$)/i.test(posix)) return /^es/i.test(posix) ? 'es' : 'en';
|
|
12
|
+
try {
|
|
13
|
+
return /^es\b/i.test(Intl.DateTimeFormat().resolvedOptions().locale) ? 'es' : 'en';
|
|
14
|
+
} catch {
|
|
15
|
+
return 'en';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let lang = detect();
|
|
20
|
+
|
|
21
|
+
/** Cambia el idioma ('en' o 'es'). */
|
|
22
|
+
export function setLang(value) {
|
|
23
|
+
lang = /^es/i.test(String(value)) ? 'es' : 'en';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getLang() {
|
|
27
|
+
return lang;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Devuelve el texto en el idioma activo. Ambos textos se escriben juntos en el código. */
|
|
31
|
+
export function L(es, en) {
|
|
32
|
+
return lang === 'es' ? es : en;
|
|
33
|
+
}
|
package/src/images.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Optimización de imágenes con sharp (dependencia opcional). Mismo formato y mismo nombre: no cambia ninguna referencia.
|
|
2
|
+
// - PNG: recompresión sin pérdida, comprobada píxel a píxel.
|
|
3
|
+
// - JPEG y WebP: se recodifican con la calidad indicada (con pérdida, visualmente equivalente).
|
|
4
|
+
// Se conservan los metadatos (orientación EXIF, perfil de color). Si el resultado no es más pequeño, se deja el original.
|
|
5
|
+
import { importOptional } from './optional.js';
|
|
6
|
+
import { L } from './i18n.js';
|
|
7
|
+
|
|
8
|
+
export const IMAGE_EXT = new Set(['.png', '.jpg', '.jpeg', '.webp']);
|
|
9
|
+
|
|
10
|
+
let sharpMod = null;
|
|
11
|
+
|
|
12
|
+
export async function ensureSharp(root) {
|
|
13
|
+
if (!sharpMod) {
|
|
14
|
+
const mod = await importOptional('sharp', root, L('para optimizar imágenes (--images)', 'to optimize images (--images)'));
|
|
15
|
+
sharpMod = mod.default ?? mod;
|
|
16
|
+
}
|
|
17
|
+
return sharpMod;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Opciones normalizadas de "images": false, true o { quality }. */
|
|
21
|
+
export function imageOptions(setting) {
|
|
22
|
+
if (!setting) return null;
|
|
23
|
+
const quality = typeof setting === 'object' && Number.isInteger(setting.quality) ? setting.quality : 80;
|
|
24
|
+
return { quality };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function samePixels(sharp, a, b) {
|
|
28
|
+
const [ra, rb] = await Promise.all([
|
|
29
|
+
sharp(a).ensureAlpha().raw().toBuffer({ resolveWithObject: true }),
|
|
30
|
+
sharp(b).ensureAlpha().raw().toBuffer({ resolveWithObject: true }),
|
|
31
|
+
]);
|
|
32
|
+
return ra.info.width === rb.info.width && ra.info.height === rb.info.height && ra.data.equals(rb.data);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @returns {Promise<{data: Buffer} | {skip: string}>} la imagen optimizada, o el motivo para dejar la original.
|
|
37
|
+
*/
|
|
38
|
+
export async function optimizeImage(sharp, buf, ext, { quality }) {
|
|
39
|
+
let meta;
|
|
40
|
+
try {
|
|
41
|
+
meta = await sharp(buf).metadata();
|
|
42
|
+
} catch (err) {
|
|
43
|
+
return { skip: L(`no se pudo leer la imagen (${err.message})`, `could not read the image (${err.message})`) };
|
|
44
|
+
}
|
|
45
|
+
if ((meta.pages ?? 1) > 1) return { skip: L('imagen animada', 'animated image') };
|
|
46
|
+
const base = () => sharp(buf).keepMetadata();
|
|
47
|
+
let out;
|
|
48
|
+
try {
|
|
49
|
+
if (ext === '.png') out = await base().png({ compressionLevel: 9, effort: 10, palette: false, adaptiveFiltering: true }).toBuffer();
|
|
50
|
+
else if (ext === '.webp') out = await base().webp({ quality, effort: 6 }).toBuffer();
|
|
51
|
+
else out = await base().jpeg({ quality, mozjpeg: true }).toBuffer();
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return { skip: L(`sharp falló (${err.message})`, `sharp failed (${err.message})`) };
|
|
54
|
+
}
|
|
55
|
+
if (out.length >= buf.length) return { skip: L('ya estaba optimizada', 'already optimized') };
|
|
56
|
+
const after = await sharp(out).metadata();
|
|
57
|
+
if (after.width !== meta.width || after.height !== meta.height || after.format !== meta.format)
|
|
58
|
+
return { skip: L('el resultado no conserva tamaño o formato', 'the result does not keep size or format') };
|
|
59
|
+
if (ext === '.png' && !(await samePixels(sharp, buf, out))) return { skip: L('los píxeles no coinciden', 'pixels do not match') };
|
|
60
|
+
return { data: out };
|
|
61
|
+
}
|
package/src/js.js
CHANGED
|
@@ -4,13 +4,13 @@ import fs from 'node:fs';
|
|
|
4
4
|
import * as esbuild from 'esbuild';
|
|
5
5
|
import * as acorn from 'acorn';
|
|
6
6
|
import * as walk from 'acorn-walk';
|
|
7
|
-
import { analyzeJs, compareGlobals,
|
|
8
|
-
import { obfuscateSafe } from './obfuscate.js';
|
|
7
|
+
import { analyzeJs, compareGlobals, isolateTempGlobals } from './jsinfo.js';
|
|
9
8
|
import { importOptional } from './optional.js';
|
|
10
9
|
import { replaceWithMap } from './sourcemap.js';
|
|
11
|
-
import { relFrom, toPosix, BuildError, rootPrefix, hasOwn, shortHash } from './util.js';
|
|
10
|
+
import { relFrom, toPosix, BuildError, rootPrefix, hasOwn, shortHash, jsOutPath } from './util.js';
|
|
12
11
|
import { classifyUrl } from './urls.js';
|
|
13
12
|
import { formatMessage, esbuildErrorText } from './css.js';
|
|
13
|
+
import { L } from './i18n.js';
|
|
14
14
|
|
|
15
15
|
/** Marcador de "raíz de salida" que se sustituye por la ruta relativa real de cada archivo generado. */
|
|
16
16
|
export const MARK = '__VB_ROOT__';
|
|
@@ -56,50 +56,75 @@ export async function processClassic(ctx, { code, id, info, obfuscate, sourcemap
|
|
|
56
56
|
const notes = [];
|
|
57
57
|
let out = code;
|
|
58
58
|
let map = null;
|
|
59
|
+
let allowPrefix = null;
|
|
59
60
|
if (js.minify) {
|
|
60
61
|
const auto = await targetForLevel(info.level);
|
|
61
|
-
const attempts = [{ target: js.target === 'auto' ? auto : js.target, keepNames: js.keepNames, label: 'completa' }];
|
|
62
|
-
if (js.target !== 'auto' || js.keepNames) attempts.push({ target: auto, keepNames: false, label: 'sin transpilar' });
|
|
63
|
-
attempts.push({ target: auto, keepNames: false, whitespace: true, label: 'solo espacios' });
|
|
62
|
+
const attempts = [{ target: js.target === 'auto' ? auto : js.target, keepNames: js.keepNames, label: L('completa', 'full') }];
|
|
63
|
+
if (js.target !== 'auto' || js.keepNames) attempts.push({ target: auto, keepNames: false, label: L('sin transpilar', 'without transpiling') });
|
|
64
|
+
attempts.push({ target: auto, keepNames: false, whitespace: true, label: L('solo espacios', 'whitespace only') });
|
|
64
65
|
let done = false;
|
|
66
|
+
// Mismo prefijo que usa el ofuscador para sus nombres: las comprobaciones posteriores ya lo aceptan.
|
|
67
|
+
const tempPrefix = `_${shortHash(id, 6)}`;
|
|
68
|
+
const transform = (input, a, extra = {}) =>
|
|
69
|
+
esbuild.transform(input, {
|
|
70
|
+
loader: 'js',
|
|
71
|
+
target: a.target,
|
|
72
|
+
charset: 'ascii',
|
|
73
|
+
supported: SUPPORTED,
|
|
74
|
+
legalComments: js.legalComments,
|
|
75
|
+
drop: js.drop,
|
|
76
|
+
define: js.define,
|
|
77
|
+
keepNames: a.keepNames,
|
|
78
|
+
sourcemap: sourcemap ? 'external' : false,
|
|
79
|
+
sourcefile: id,
|
|
80
|
+
...(a.whitespace ? { minifyWhitespace: true } : { minify: true }),
|
|
81
|
+
logLevel: 'silent',
|
|
82
|
+
...extra,
|
|
83
|
+
});
|
|
65
84
|
for (const a of attempts) {
|
|
66
85
|
try {
|
|
67
|
-
const r = await
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
86
|
+
const r = await transform(code, a);
|
|
87
|
+
let candidate = r.code;
|
|
88
|
+
let cmp = compareGlobals(info.top, candidate);
|
|
89
|
+
let isolated = false;
|
|
90
|
+
if (!cmp.ok && !a.whitespace && a.target !== auto) {
|
|
91
|
+
// Al adaptar la sintaxis, esbuild declara temporales globales (var _a). Se adapta sin minificar
|
|
92
|
+
// (nombres reconocibles), se aíslan con un prefijo propio del archivo y después se minifica:
|
|
93
|
+
// la minificación no renombra globales de un script clásico ni crea temporales nuevas.
|
|
94
|
+
const lowered = await transform(code, a, { minify: false, minifyWhitespace: false, sourcemap: false });
|
|
95
|
+
const iso = isolateTempGlobals(lowered.code, info.top, tempPrefix);
|
|
96
|
+
if (iso) {
|
|
97
|
+
const r2 = await transform(iso, a, { sourcemap: false });
|
|
98
|
+
const cmp2 = compareGlobals(info.top, r2.code, tempPrefix);
|
|
99
|
+
if (cmp2.ok) {
|
|
100
|
+
candidate = r2.code;
|
|
101
|
+
cmp = cmp2;
|
|
102
|
+
isolated = true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
82
106
|
if (cmp.ok) {
|
|
83
|
-
out =
|
|
84
|
-
map = r.map || null;
|
|
107
|
+
out = candidate;
|
|
108
|
+
map = isolated ? null : r.map || null;
|
|
109
|
+
if (isolated) allowPrefix = tempPrefix;
|
|
85
110
|
done = true;
|
|
86
111
|
break;
|
|
87
112
|
}
|
|
88
|
-
notes.push(`minificación ${a.label} descartada: ${cmp.reason}`);
|
|
113
|
+
notes.push(L(`minificación ${a.label} descartada: ${cmp.reason}`, `${a.label} minification discarded: ${cmp.reason}`));
|
|
89
114
|
} catch (err) {
|
|
90
|
-
notes.push(`minificación ${a.label} falló: ${firstLine(err)}`);
|
|
115
|
+
notes.push(L(`minificación ${a.label} falló: ${firstLine(err)}`, `${a.label} minification failed: ${firstLine(err)}`));
|
|
91
116
|
}
|
|
92
117
|
}
|
|
93
|
-
if (!done) notes.push('se deja sin minificar para no romperlo');
|
|
118
|
+
if (!done) notes.push(L('se deja sin minificar para no romperlo', 'left unminified so it does not break'));
|
|
94
119
|
}
|
|
95
120
|
if (obfuscate) {
|
|
96
|
-
const o =
|
|
121
|
+
const o = await ctx.heavy.obfuscate(out, { isModule: false, options: ctx.obfuscatorOptions, id, globals: info.top });
|
|
97
122
|
if (o.code) {
|
|
98
123
|
out = o.code;
|
|
99
124
|
map = null;
|
|
100
|
-
} else notes.push(`no se ofuscó: ${o.reason}`);
|
|
125
|
+
} else notes.push(L(`no se ofuscó: ${o.reason}`, `not obfuscated: ${o.reason}`));
|
|
101
126
|
} else if (out !== code) {
|
|
102
|
-
const t = await terserPass(ctx, out, { isModule: false, level: info.level, globals: info.top });
|
|
127
|
+
const t = await terserPass(ctx, out, { isModule: false, level: info.level, globals: info.top, allowPrefix });
|
|
103
128
|
if (t !== out) {
|
|
104
129
|
out = t;
|
|
105
130
|
map = null;
|
|
@@ -134,15 +159,15 @@ export async function processModuleStandalone(ctx, { code, id, info, obfuscate,
|
|
|
134
159
|
out = r.code;
|
|
135
160
|
map = r.map || null;
|
|
136
161
|
} catch (err) {
|
|
137
|
-
notes.push(`no se pudo minificar: ${firstLine(err)}`);
|
|
162
|
+
notes.push(L(`no se pudo minificar: ${firstLine(err)}`, `could not minify: ${firstLine(err)}`));
|
|
138
163
|
}
|
|
139
164
|
}
|
|
140
165
|
if (obfuscate) {
|
|
141
|
-
const o =
|
|
166
|
+
const o = await ctx.heavy.obfuscate(out, { isModule: true, options: ctx.obfuscatorOptions, id });
|
|
142
167
|
if (o.code) {
|
|
143
168
|
out = o.code;
|
|
144
169
|
map = null;
|
|
145
|
-
} else notes.push(`no se ofuscó: ${o.reason}`);
|
|
170
|
+
} else notes.push(L(`no se ofuscó: ${o.reason}`, `not obfuscated: ${o.reason}`));
|
|
146
171
|
} else if (out !== code) {
|
|
147
172
|
const t = await terserPass(ctx, out, { isModule: true, level: info.level });
|
|
148
173
|
if (t !== out) {
|
|
@@ -154,9 +179,9 @@ export async function processModuleStandalone(ctx, { code, id, info, obfuscate,
|
|
|
154
179
|
}
|
|
155
180
|
|
|
156
181
|
/** Ofusca un módulo ya generado (paquete, chunk o worker). */
|
|
157
|
-
export function obfuscateModule(ctx, code, id) {
|
|
158
|
-
const o =
|
|
159
|
-
return o.code ? { code: o.code } : { code, note: `no se ofuscó: ${o.reason}` };
|
|
182
|
+
export async function obfuscateModule(ctx, code, id) {
|
|
183
|
+
const o = await ctx.heavy.obfuscate(code, { isModule: true, options: ctx.obfuscatorOptions, id });
|
|
184
|
+
return o.code ? { code: o.code } : { code, note: L(`no se ofuscó: ${o.reason}`, `not obfuscated: ${o.reason}`) };
|
|
160
185
|
}
|
|
161
186
|
|
|
162
187
|
/** Aplica un import map (solo "imports"; con "scopes" se usa el modo sin empaquetar). */
|
|
@@ -196,6 +221,14 @@ export function mapSpecifier(ctx, importMap, spec, importerDir) {
|
|
|
196
221
|
|
|
197
222
|
const EXTERNAL_SPEC = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
|
|
198
223
|
|
|
224
|
+
function isFile(abs) {
|
|
225
|
+
try {
|
|
226
|
+
return fs.statSync(abs).isFile();
|
|
227
|
+
} catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
199
232
|
function modulePlugin(ctx, group, state) {
|
|
200
233
|
const isDepPath = (abs) => {
|
|
201
234
|
if (!abs) return false;
|
|
@@ -212,8 +245,10 @@ function modulePlugin(ctx, group, state) {
|
|
|
212
245
|
if (!info.ok) return code;
|
|
213
246
|
state.levels.push(info.level ?? 9999);
|
|
214
247
|
if (!local) return code;
|
|
215
|
-
for (const u of info.unsafeUrls)
|
|
216
|
-
|
|
248
|
+
for (const u of info.unsafeUrls)
|
|
249
|
+
state.unsafe.push(L(`new URL(variable, import.meta.url) en ${id}: ${u}`, `new URL(variable, import.meta.url) in ${id}: ${u}`));
|
|
250
|
+
for (const u of info.unsafeImports)
|
|
251
|
+
state.unsafe.push(L(`import() con una ruta calculada en ${id}: ${u}`, `import() with a computed path in ${id}: ${u}`));
|
|
217
252
|
if (state.mode === 'scan') ctx.noteJsInfo(id, info, baseDir, { module: true });
|
|
218
253
|
if (!info.urls.length) return code;
|
|
219
254
|
let out = '';
|
|
@@ -245,6 +280,11 @@ function modulePlugin(ctx, group, state) {
|
|
|
245
280
|
if (!state.vendor || a.pluginData?.vbInner || r?.external || r?.errors) return r;
|
|
246
281
|
if (a.namespace !== 'vb-inline' && isDepPath(a.importer)) return r;
|
|
247
282
|
let abs = r?.path;
|
|
283
|
+
// Ruta relativa a un archivo que existe: se resuelve aquí, sin otro viaje de ida y vuelta a esbuild.
|
|
284
|
+
if (!abs && /^\.\.?\//.test(a.path) && a.namespace !== 'vb-inline' && a.resolveDir) {
|
|
285
|
+
const direct = path.resolve(a.resolveDir, a.path);
|
|
286
|
+
if (path.extname(direct) && isFile(direct)) abs = direct;
|
|
287
|
+
}
|
|
248
288
|
if (!abs) {
|
|
249
289
|
const res = await b.resolve(a.path, {
|
|
250
290
|
kind: a.kind,
|
|
@@ -275,24 +315,39 @@ function modulePlugin(ctx, group, state) {
|
|
|
275
315
|
ctx.reportBroken(importer, spec, r);
|
|
276
316
|
return { path: spec, external: true };
|
|
277
317
|
}
|
|
278
|
-
ctx.requireStandalone(r.rel, `importado como CSS desde ${importer}`);
|
|
318
|
+
ctx.requireStandalone(r.rel, L(`importado como CSS desde ${importer}`, `imported as CSS from ${importer}`));
|
|
279
319
|
return { path: `${MARK}/${r.rel}`, external: true };
|
|
280
320
|
}
|
|
281
321
|
const mapped = mapSpecifier(ctx, group.importMap, spec, baseDir);
|
|
282
322
|
if (mapped?.external) return { path: spec, external: true };
|
|
283
323
|
if (mapped?.rel) return { path: ctx.abs(mapped.rel) };
|
|
284
324
|
if (mapped?.abs) return { path: mapped.abs };
|
|
285
|
-
if (mapped?.broken)
|
|
325
|
+
if (mapped?.broken)
|
|
326
|
+
return {
|
|
327
|
+
errors: [
|
|
328
|
+
{
|
|
329
|
+
text: L(
|
|
330
|
+
`El import map de la página apunta "${spec}" a "${mapped.target}", que no existe.`,
|
|
331
|
+
`The page import map points "${spec}" to "${mapped.target}", which does not exist.`,
|
|
332
|
+
),
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
};
|
|
286
336
|
if (spec.startsWith('/')) {
|
|
287
337
|
const r = ctx.resolve(spec, baseDir);
|
|
288
338
|
if (r.status === 'ok') return { path: ctx.abs(r.rel) };
|
|
289
|
-
return {
|
|
339
|
+
return {
|
|
340
|
+
errors: [{ text: L(`No existe "${spec}" (importado desde ${importer}).`, `"${spec}" does not exist (imported from ${importer}).`) }],
|
|
341
|
+
};
|
|
290
342
|
}
|
|
291
343
|
return undefined;
|
|
292
344
|
};
|
|
293
|
-
b.onLoad({ filter: /\.(?:m?js|cjs)$/i }, (a) => {
|
|
345
|
+
b.onLoad({ filter: /\.(?:m?js|cjs|m?ts|cts)$/i }, (a) => {
|
|
294
346
|
const rel = relFrom(ctx.root, a.path);
|
|
295
347
|
const local = rel != null && !/(^|\/)node_modules\//.test(rel) && ctx.files.has(rel);
|
|
348
|
+
// Los .ts propios llegan ya transpilados (readText); los de node_modules los transpila esbuild.
|
|
349
|
+
const isTs = /\.(?:m?ts|cts)$/i.test(a.path);
|
|
350
|
+
if (!local && isTs) return { contents: fs.readFileSync(a.path, 'utf8'), loader: 'ts' };
|
|
296
351
|
const code = local ? ctx.readText(rel, { kind: 'js' }).text : fs.readFileSync(a.path, 'utf8');
|
|
297
352
|
if (local) state.consumed.add(rel);
|
|
298
353
|
return { contents: prepare(code, local ? rel : toPosix(a.path), local ? path.posix.dirname(rel) : '', local), loader: 'js' };
|
|
@@ -309,40 +364,21 @@ let terser = null;
|
|
|
309
364
|
|
|
310
365
|
/** Carga terser (opcional) para la compresión máxima. */
|
|
311
366
|
export async function ensureTerser(projectRoot) {
|
|
312
|
-
terser = await importOptional(
|
|
367
|
+
terser = await importOptional(
|
|
368
|
+
'terser',
|
|
369
|
+
projectRoot,
|
|
370
|
+
L('para la compresión máxima (js.minifier: "terser")', 'for maximum compression (js.minifier: "terser")'),
|
|
371
|
+
);
|
|
313
372
|
}
|
|
314
373
|
|
|
315
374
|
/**
|
|
316
375
|
* Segunda pasada con terser (varias pasadas de compresión). Solo se acepta si el resultado
|
|
317
376
|
* es más pequeño y conserva globales (scripts) o exportaciones/importaciones (módulos).
|
|
318
377
|
*/
|
|
319
|
-
export async function terserPass(ctx, code, { isModule, level, globals }) {
|
|
378
|
+
export async function terserPass(ctx, code, { isModule, level, globals, allowPrefix = null }) {
|
|
320
379
|
const js = ctx.cfg.js;
|
|
321
380
|
if (!terser || js.minifier !== 'terser' || !js.minify || ctx.cfg.sourcemap) return code;
|
|
322
|
-
|
|
323
|
-
let out;
|
|
324
|
-
try {
|
|
325
|
-
const r = await terser.minify(code, {
|
|
326
|
-
module: isModule,
|
|
327
|
-
toplevel: isModule,
|
|
328
|
-
ecma,
|
|
329
|
-
compress: { passes: 3, ecma, module: isModule, toplevel: isModule },
|
|
330
|
-
mangle: { toplevel: isModule },
|
|
331
|
-
keep_fnames: js.keepNames,
|
|
332
|
-
keep_classnames: js.keepNames,
|
|
333
|
-
format: { comments: /^!|@license|@preserve/i, ascii_only: true, ecma },
|
|
334
|
-
});
|
|
335
|
-
out = r.code;
|
|
336
|
-
} catch {
|
|
337
|
-
return code;
|
|
338
|
-
}
|
|
339
|
-
if (!out || out.length >= code.trimEnd().length) return code;
|
|
340
|
-
if (isModule) {
|
|
341
|
-
const a = moduleShape(code);
|
|
342
|
-
const b = moduleShape(out);
|
|
343
|
-
if (!a || !b || a.exports !== b.exports || a.imports !== b.imports) return code;
|
|
344
|
-
} else if (!globals || !compareGlobals(globals, out).ok) return code;
|
|
345
|
-
return out;
|
|
381
|
+
return ctx.heavy.terser(code, { isModule, level, globals: isModule ? undefined : globals, allowPrefix, keepNames: js.keepNames });
|
|
346
382
|
}
|
|
347
383
|
|
|
348
384
|
/** Bytes de origen que aportaron código a una salida (incluye paquetes de node_modules). */
|
|
@@ -470,16 +506,23 @@ export async function buildModuleGroup(ctx, group) {
|
|
|
470
506
|
try {
|
|
471
507
|
scan = await esbuild.build({ ...common, target: 'esnext', plugins: [modulePlugin(ctx, group, scanState)] });
|
|
472
508
|
} catch (err) {
|
|
473
|
-
throw new BuildError(
|
|
509
|
+
throw new BuildError(
|
|
510
|
+
L('No se pudieron empaquetar los módulos JavaScript:', 'Could not bundle the JavaScript modules:'),
|
|
511
|
+
esbuildErrorText(err, ctx.root).split('\n'),
|
|
512
|
+
);
|
|
474
513
|
}
|
|
475
514
|
const inputs = Object.keys(scan.metafile.inputs)
|
|
476
515
|
.map((p) => relFrom(ctx.root, path.resolve(ctx.root, p)))
|
|
477
516
|
.filter((rel) => rel && ctx.files.has(rel));
|
|
478
517
|
const dynamicWarnings = scan.warnings.filter((w) => w.id === 'unsupported-dynamic-import');
|
|
479
518
|
let reason = null;
|
|
480
|
-
if (!cfg.js.bundle) reason = 'js.bundle está desactivado';
|
|
519
|
+
if (!cfg.js.bundle) reason = L('js.bundle está desactivado', 'js.bundle is disabled');
|
|
481
520
|
else if (group.forcePerFile) reason = group.forcePerFile;
|
|
482
|
-
else if (dynamicWarnings.length)
|
|
521
|
+
else if (dynamicWarnings.length)
|
|
522
|
+
reason = L(
|
|
523
|
+
`hay import() con rutas que no se pueden seguir (${formatMessage(dynamicWarnings[0], ctx.root)})`,
|
|
524
|
+
`there are import() calls with paths that cannot be followed (${formatMessage(dynamicWarnings[0], ctx.root)})`,
|
|
525
|
+
);
|
|
483
526
|
else if (scanState.unsafe.length) reason = scanState.unsafe[0];
|
|
484
527
|
if (reason) return { mode: 'per-file', reason, inputs, consumed: new Set() };
|
|
485
528
|
for (const w of scan.warnings) ctx.warn(`JS: ${formatMessage(w, ctx.root)}`);
|
|
@@ -494,7 +537,10 @@ export async function buildModuleGroup(ctx, group) {
|
|
|
494
537
|
emitState.vendorOut = vendor.outByAbs;
|
|
495
538
|
} catch (err) {
|
|
496
539
|
ctx.warn(
|
|
497
|
-
|
|
540
|
+
L(
|
|
541
|
+
`no se pudieron separar las dependencias en archivos vendor (${esbuildErrorText(err, ctx.root).split('\n')[0]}); van dentro del paquete y se ofuscarán con él`,
|
|
542
|
+
`could not split dependencies into vendor files (${esbuildErrorText(err, ctx.root).split('\n')[0]}); they stay in the bundle and will be obfuscated with it`,
|
|
543
|
+
),
|
|
498
544
|
);
|
|
499
545
|
}
|
|
500
546
|
}
|
|
@@ -510,7 +556,10 @@ export async function buildModuleGroup(ctx, group) {
|
|
|
510
556
|
plugins: [modulePlugin(ctx, group, emitState)],
|
|
511
557
|
});
|
|
512
558
|
} catch (err) {
|
|
513
|
-
throw new BuildError(
|
|
559
|
+
throw new BuildError(
|
|
560
|
+
L('No se pudieron empaquetar los módulos JavaScript:', 'Could not bundle the JavaScript modules:'),
|
|
561
|
+
esbuildErrorText(err, ctx.root).split('\n'),
|
|
562
|
+
);
|
|
514
563
|
}
|
|
515
564
|
const maps = new Map();
|
|
516
565
|
const files = [];
|
|
@@ -552,7 +601,7 @@ export async function buildModuleGroup(ctx, group) {
|
|
|
552
601
|
export async function buildModuleWorker(ctx, rel) {
|
|
553
602
|
const group = { importMap: null, virtual: new Map(), entries: new Set([rel]) };
|
|
554
603
|
const state = newState('scan');
|
|
555
|
-
const outfile = path.join(ctx.root, '__vb_out__', rel);
|
|
604
|
+
const outfile = path.join(ctx.root, '__vb_out__', jsOutPath(rel));
|
|
556
605
|
const run = (st, extra) =>
|
|
557
606
|
esbuild.build({
|
|
558
607
|
absWorkingDir: ctx.root,
|
|
@@ -584,7 +633,7 @@ export async function buildModuleWorker(ctx, rel) {
|
|
|
584
633
|
if (f.path.endsWith('.map')) map = f.text;
|
|
585
634
|
else code = f.text.replace(/\n\/\/# sourceMappingURL=.*\n?$/, '\n');
|
|
586
635
|
}
|
|
587
|
-
({ code, map } = finalizeMarkers(code, map, rel));
|
|
636
|
+
({ code, map } = finalizeMarkers(code, map, jsOutPath(rel)));
|
|
588
637
|
return { code, map, consumed: emit.consumed };
|
|
589
638
|
} catch (err) {
|
|
590
639
|
return { error: esbuildErrorText(err, ctx.root) };
|
package/src/jsimports.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Rutas importadas por un JS ya generado (para verificar que existen en la salida).
|
|
2
|
+
import * as acorn from 'acorn';
|
|
3
|
+
import * as walk from 'acorn-walk';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reescribe los imports relativos de un archivo TypeScript ya transpilado para que funcionen en el navegador:
|
|
7
|
+
* "./x.ts" → "./x.js" (tras compilar existe con extensión .js) y "./x" → "./x.js" o "./x/index.js"
|
|
8
|
+
* según `complete(spec)`, que devuelve la ruta completa o null. Si el código no se puede leer, se devuelve igual.
|
|
9
|
+
*/
|
|
10
|
+
export function rewriteTsImports(code, complete = () => null) {
|
|
11
|
+
let ast;
|
|
12
|
+
try {
|
|
13
|
+
ast = acorn.parse(code, { ecmaVersion: 'latest', sourceType: 'module', allowHashBang: true });
|
|
14
|
+
} catch {
|
|
15
|
+
return code;
|
|
16
|
+
}
|
|
17
|
+
const edits = [];
|
|
18
|
+
const consider = (src) => {
|
|
19
|
+
if (src?.type !== 'Literal' || typeof src.value !== 'string' || !/^\.{1,2}\//.test(src.value)) return;
|
|
20
|
+
let spec = src.value;
|
|
21
|
+
if (/\.(m|c)?ts$/i.test(spec)) spec = spec.replace(/\.(m|c)?ts$/i, (_, p) => `.${(p ?? '').toLowerCase()}js`);
|
|
22
|
+
else if (!/\.[a-z0-9]+$/i.test(spec.split('/').pop())) spec = complete(spec) ?? spec;
|
|
23
|
+
if (spec !== src.value) edits.push({ start: src.start, end: src.end, text: JSON.stringify(spec) });
|
|
24
|
+
};
|
|
25
|
+
const isImportMetaUrl = (m) =>
|
|
26
|
+
m?.type === 'MemberExpression' && m.object.type === 'MetaProperty' && m.object.meta.name === 'import' && m.property.name === 'url';
|
|
27
|
+
walk.full(ast, (n) => {
|
|
28
|
+
if (n.type === 'ImportDeclaration' || n.type === 'ExportAllDeclaration' || n.type === 'ExportNamedDeclaration') consider(n.source);
|
|
29
|
+
else if (n.type === 'ImportExpression') consider(n.source);
|
|
30
|
+
// new URL('./worker.ts', import.meta.url): workers y recursos cargados por ruta.
|
|
31
|
+
else if (n.type === 'NewExpression' && n.callee.type === 'Identifier' && n.callee.name === 'URL' && isImportMetaUrl(n.arguments[1]))
|
|
32
|
+
consider(n.arguments[0]);
|
|
33
|
+
});
|
|
34
|
+
let out = code;
|
|
35
|
+
for (const e of edits.sort((a, b) => b.start - a.start)) out = out.slice(0, e.start) + e.text + out.slice(e.end);
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Devuelve {imports} o {error} si el código no es JavaScript válido. */
|
|
40
|
+
export function jsImports(code, isModule) {
|
|
41
|
+
let ast;
|
|
42
|
+
try {
|
|
43
|
+
ast = acorn.parse(code, { ecmaVersion: 'latest', sourceType: isModule ? 'module' : 'script', allowHashBang: true });
|
|
44
|
+
} catch (err) {
|
|
45
|
+
return { error: err.message };
|
|
46
|
+
}
|
|
47
|
+
const imports = [];
|
|
48
|
+
walk.full(ast, (n) => {
|
|
49
|
+
if ((n.type === 'ImportDeclaration' || n.type === 'ExportAllDeclaration' || n.type === 'ExportNamedDeclaration') && n.source)
|
|
50
|
+
imports.push(n.source.value);
|
|
51
|
+
if (n.type === 'ImportExpression' && n.source.type === 'Literal' && typeof n.source.value === 'string') imports.push(n.source.value);
|
|
52
|
+
});
|
|
53
|
+
return { imports };
|
|
54
|
+
}
|