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/js.js
CHANGED
|
@@ -4,12 +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 } from './jsinfo.js';
|
|
7
|
+
import { analyzeJs, compareGlobals, isolateTempGlobals } from './jsinfo.js';
|
|
8
8
|
import { importOptional } from './optional.js';
|
|
9
9
|
import { replaceWithMap } from './sourcemap.js';
|
|
10
10
|
import { relFrom, toPosix, BuildError, rootPrefix, hasOwn, shortHash, jsOutPath } from './util.js';
|
|
11
11
|
import { classifyUrl } from './urls.js';
|
|
12
12
|
import { formatMessage, esbuildErrorText } from './css.js';
|
|
13
|
+
import { L } from './i18n.js';
|
|
13
14
|
|
|
14
15
|
/** Marcador de "raíz de salida" que se sustituye por la ruta relativa real de cada archivo generado. */
|
|
15
16
|
export const MARK = '__VB_ROOT__';
|
|
@@ -55,50 +56,75 @@ export async function processClassic(ctx, { code, id, info, obfuscate, sourcemap
|
|
|
55
56
|
const notes = [];
|
|
56
57
|
let out = code;
|
|
57
58
|
let map = null;
|
|
59
|
+
let allowPrefix = null;
|
|
58
60
|
if (js.minify) {
|
|
59
61
|
const auto = await targetForLevel(info.level);
|
|
60
|
-
const attempts = [{ target: js.target === 'auto' ? auto : js.target, keepNames: js.keepNames, label: 'completa' }];
|
|
61
|
-
if (js.target !== 'auto' || js.keepNames) attempts.push({ target: auto, keepNames: false, label: 'sin transpilar' });
|
|
62
|
-
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') });
|
|
63
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
|
+
});
|
|
64
84
|
for (const a of attempts) {
|
|
65
85
|
try {
|
|
66
|
-
const r = await
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
+
}
|
|
81
106
|
if (cmp.ok) {
|
|
82
|
-
out =
|
|
83
|
-
map = r.map || null;
|
|
107
|
+
out = candidate;
|
|
108
|
+
map = isolated ? null : r.map || null;
|
|
109
|
+
if (isolated) allowPrefix = tempPrefix;
|
|
84
110
|
done = true;
|
|
85
111
|
break;
|
|
86
112
|
}
|
|
87
|
-
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}`));
|
|
88
114
|
} catch (err) {
|
|
89
|
-
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)}`));
|
|
90
116
|
}
|
|
91
117
|
}
|
|
92
|
-
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'));
|
|
93
119
|
}
|
|
94
120
|
if (obfuscate) {
|
|
95
121
|
const o = await ctx.heavy.obfuscate(out, { isModule: false, options: ctx.obfuscatorOptions, id, globals: info.top });
|
|
96
122
|
if (o.code) {
|
|
97
123
|
out = o.code;
|
|
98
124
|
map = null;
|
|
99
|
-
} else notes.push(`no se ofuscó: ${o.reason}`);
|
|
125
|
+
} else notes.push(L(`no se ofuscó: ${o.reason}`, `not obfuscated: ${o.reason}`));
|
|
100
126
|
} else if (out !== code) {
|
|
101
|
-
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 });
|
|
102
128
|
if (t !== out) {
|
|
103
129
|
out = t;
|
|
104
130
|
map = null;
|
|
@@ -133,7 +159,7 @@ export async function processModuleStandalone(ctx, { code, id, info, obfuscate,
|
|
|
133
159
|
out = r.code;
|
|
134
160
|
map = r.map || null;
|
|
135
161
|
} catch (err) {
|
|
136
|
-
notes.push(`no se pudo minificar: ${firstLine(err)}`);
|
|
162
|
+
notes.push(L(`no se pudo minificar: ${firstLine(err)}`, `could not minify: ${firstLine(err)}`));
|
|
137
163
|
}
|
|
138
164
|
}
|
|
139
165
|
if (obfuscate) {
|
|
@@ -141,7 +167,7 @@ export async function processModuleStandalone(ctx, { code, id, info, obfuscate,
|
|
|
141
167
|
if (o.code) {
|
|
142
168
|
out = o.code;
|
|
143
169
|
map = null;
|
|
144
|
-
} else notes.push(`no se ofuscó: ${o.reason}`);
|
|
170
|
+
} else notes.push(L(`no se ofuscó: ${o.reason}`, `not obfuscated: ${o.reason}`));
|
|
145
171
|
} else if (out !== code) {
|
|
146
172
|
const t = await terserPass(ctx, out, { isModule: true, level: info.level });
|
|
147
173
|
if (t !== out) {
|
|
@@ -155,7 +181,7 @@ export async function processModuleStandalone(ctx, { code, id, info, obfuscate,
|
|
|
155
181
|
/** Ofusca un módulo ya generado (paquete, chunk o worker). */
|
|
156
182
|
export async function obfuscateModule(ctx, code, id) {
|
|
157
183
|
const o = await ctx.heavy.obfuscate(code, { isModule: true, options: ctx.obfuscatorOptions, id });
|
|
158
|
-
return o.code ? { code: o.code } : { code, note: `no se ofuscó: ${o.reason}` };
|
|
184
|
+
return o.code ? { code: o.code } : { code, note: L(`no se ofuscó: ${o.reason}`, `not obfuscated: ${o.reason}`) };
|
|
159
185
|
}
|
|
160
186
|
|
|
161
187
|
/** Aplica un import map (solo "imports"; con "scopes" se usa el modo sin empaquetar). */
|
|
@@ -219,8 +245,10 @@ function modulePlugin(ctx, group, state) {
|
|
|
219
245
|
if (!info.ok) return code;
|
|
220
246
|
state.levels.push(info.level ?? 9999);
|
|
221
247
|
if (!local) return code;
|
|
222
|
-
for (const u of info.unsafeUrls)
|
|
223
|
-
|
|
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}`));
|
|
224
252
|
if (state.mode === 'scan') ctx.noteJsInfo(id, info, baseDir, { module: true });
|
|
225
253
|
if (!info.urls.length) return code;
|
|
226
254
|
let out = '';
|
|
@@ -287,18 +315,30 @@ function modulePlugin(ctx, group, state) {
|
|
|
287
315
|
ctx.reportBroken(importer, spec, r);
|
|
288
316
|
return { path: spec, external: true };
|
|
289
317
|
}
|
|
290
|
-
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}`));
|
|
291
319
|
return { path: `${MARK}/${r.rel}`, external: true };
|
|
292
320
|
}
|
|
293
321
|
const mapped = mapSpecifier(ctx, group.importMap, spec, baseDir);
|
|
294
322
|
if (mapped?.external) return { path: spec, external: true };
|
|
295
323
|
if (mapped?.rel) return { path: ctx.abs(mapped.rel) };
|
|
296
324
|
if (mapped?.abs) return { path: mapped.abs };
|
|
297
|
-
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
|
+
};
|
|
298
336
|
if (spec.startsWith('/')) {
|
|
299
337
|
const r = ctx.resolve(spec, baseDir);
|
|
300
338
|
if (r.status === 'ok') return { path: ctx.abs(r.rel) };
|
|
301
|
-
return {
|
|
339
|
+
return {
|
|
340
|
+
errors: [{ text: L(`No existe "${spec}" (importado desde ${importer}).`, `"${spec}" does not exist (imported from ${importer}).`) }],
|
|
341
|
+
};
|
|
302
342
|
}
|
|
303
343
|
return undefined;
|
|
304
344
|
};
|
|
@@ -324,17 +364,21 @@ let terser = null;
|
|
|
324
364
|
|
|
325
365
|
/** Carga terser (opcional) para la compresión máxima. */
|
|
326
366
|
export async function ensureTerser(projectRoot) {
|
|
327
|
-
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
|
+
);
|
|
328
372
|
}
|
|
329
373
|
|
|
330
374
|
/**
|
|
331
375
|
* Segunda pasada con terser (varias pasadas de compresión). Solo se acepta si el resultado
|
|
332
376
|
* es más pequeño y conserva globales (scripts) o exportaciones/importaciones (módulos).
|
|
333
377
|
*/
|
|
334
|
-
export async function terserPass(ctx, code, { isModule, level, globals }) {
|
|
378
|
+
export async function terserPass(ctx, code, { isModule, level, globals, allowPrefix = null }) {
|
|
335
379
|
const js = ctx.cfg.js;
|
|
336
380
|
if (!terser || js.minifier !== 'terser' || !js.minify || ctx.cfg.sourcemap) return code;
|
|
337
|
-
return ctx.heavy.terser(code, { isModule, level, globals: isModule ? undefined : globals, keepNames: js.keepNames });
|
|
381
|
+
return ctx.heavy.terser(code, { isModule, level, globals: isModule ? undefined : globals, allowPrefix, keepNames: js.keepNames });
|
|
338
382
|
}
|
|
339
383
|
|
|
340
384
|
/** Bytes de origen que aportaron código a una salida (incluye paquetes de node_modules). */
|
|
@@ -462,16 +506,23 @@ export async function buildModuleGroup(ctx, group) {
|
|
|
462
506
|
try {
|
|
463
507
|
scan = await esbuild.build({ ...common, target: 'esnext', plugins: [modulePlugin(ctx, group, scanState)] });
|
|
464
508
|
} catch (err) {
|
|
465
|
-
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
|
+
);
|
|
466
513
|
}
|
|
467
514
|
const inputs = Object.keys(scan.metafile.inputs)
|
|
468
515
|
.map((p) => relFrom(ctx.root, path.resolve(ctx.root, p)))
|
|
469
516
|
.filter((rel) => rel && ctx.files.has(rel));
|
|
470
517
|
const dynamicWarnings = scan.warnings.filter((w) => w.id === 'unsupported-dynamic-import');
|
|
471
518
|
let reason = null;
|
|
472
|
-
if (!cfg.js.bundle) reason = 'js.bundle está desactivado';
|
|
519
|
+
if (!cfg.js.bundle) reason = L('js.bundle está desactivado', 'js.bundle is disabled');
|
|
473
520
|
else if (group.forcePerFile) reason = group.forcePerFile;
|
|
474
|
-
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
|
+
);
|
|
475
526
|
else if (scanState.unsafe.length) reason = scanState.unsafe[0];
|
|
476
527
|
if (reason) return { mode: 'per-file', reason, inputs, consumed: new Set() };
|
|
477
528
|
for (const w of scan.warnings) ctx.warn(`JS: ${formatMessage(w, ctx.root)}`);
|
|
@@ -486,7 +537,10 @@ export async function buildModuleGroup(ctx, group) {
|
|
|
486
537
|
emitState.vendorOut = vendor.outByAbs;
|
|
487
538
|
} catch (err) {
|
|
488
539
|
ctx.warn(
|
|
489
|
-
|
|
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
|
+
),
|
|
490
544
|
);
|
|
491
545
|
}
|
|
492
546
|
}
|
|
@@ -502,7 +556,10 @@ export async function buildModuleGroup(ctx, group) {
|
|
|
502
556
|
plugins: [modulePlugin(ctx, group, emitState)],
|
|
503
557
|
});
|
|
504
558
|
} catch (err) {
|
|
505
|
-
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
|
+
);
|
|
506
563
|
}
|
|
507
564
|
const maps = new Map();
|
|
508
565
|
const files = [];
|
package/src/jsinfo.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Análisis estático de JavaScript con acorn: nivel de sintaxis, globales, workers y rutas.
|
|
2
2
|
import * as acorn from 'acorn';
|
|
3
3
|
import * as walk from 'acorn-walk';
|
|
4
|
+
import { L } from './i18n.js';
|
|
4
5
|
|
|
5
6
|
const VERSIONS = [5, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026];
|
|
6
7
|
const LATEST = 'latest';
|
|
@@ -249,6 +250,51 @@ export function checkSyntax(code, isModule) {
|
|
|
249
250
|
}
|
|
250
251
|
}
|
|
251
252
|
|
|
253
|
+
// Nombres que esbuild crea al adaptar sintaxis a navegadores antiguos: temporales (_a, _b2) y ayudantes (__async).
|
|
254
|
+
const ESBUILD_TEMP = /^(_[a-z]\d*|__[A-Za-z]+\d*)$/;
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Al transpilar un script clásico, esbuild puede declarar temporales en el ámbito global (var _a).
|
|
258
|
+
* Si TODAS las globales nuevas son de ese tipo, se renombran en todo el archivo a `${prefix}nombre`
|
|
259
|
+
* (único por archivo), así no pueden chocar con otros scripts. Devuelve el código nuevo, o null si no aplica.
|
|
260
|
+
* El renombrado es consistente en todos los ámbitos, así que no cambia el significado del código.
|
|
261
|
+
*/
|
|
262
|
+
export function isolateTempGlobals(code, before, prefix) {
|
|
263
|
+
let ast;
|
|
264
|
+
try {
|
|
265
|
+
ast = parse(code, 'script', LATEST);
|
|
266
|
+
} catch {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
const top = globalBindings(
|
|
270
|
+
ast,
|
|
271
|
+
ast.body.some((s) => s.directive === 'use strict'),
|
|
272
|
+
);
|
|
273
|
+
const fresh = [...top.keys()].filter((n) => !before.has(n));
|
|
274
|
+
if (!fresh.length || !fresh.every((n) => ESBUILD_TEMP.test(n))) return null;
|
|
275
|
+
const rename = new Map(fresh.map((n) => [n, `${prefix}${n}`]));
|
|
276
|
+
const edits = new Map();
|
|
277
|
+
walk.fullAncestor(ast, (node, _state, ancestors) => {
|
|
278
|
+
if (node.type !== 'Identifier' || !rename.has(node.name)) return;
|
|
279
|
+
const p = ancestors[ancestors.length - 2];
|
|
280
|
+
const to = rename.get(node.name);
|
|
281
|
+
if (p) {
|
|
282
|
+
if (p.type === 'MemberExpression' && p.property === node && !p.computed) return;
|
|
283
|
+
if (p.type === 'Property' && p.shorthand) {
|
|
284
|
+
// { _a } → { _a: nuevo }: la clave se conserva, el valor se renombra.
|
|
285
|
+
edits.set(node.start, { start: node.start, end: node.end, text: `${node.name}: ${to}` });
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if ((p.type === 'Property' || p.type === 'MethodDefinition' || p.type === 'PropertyDefinition') && p.key === node && !p.computed) return;
|
|
289
|
+
if (p.type === 'LabeledStatement' || p.type === 'BreakStatement' || p.type === 'ContinueStatement') return;
|
|
290
|
+
}
|
|
291
|
+
if (!edits.has(node.start)) edits.set(node.start, { start: node.start, end: node.end, text: to });
|
|
292
|
+
});
|
|
293
|
+
let out = code;
|
|
294
|
+
for (const e of [...edits.values()].sort((a, b) => b.start - a.start)) out = out.slice(0, e.start) + e.text + out.slice(e.end);
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
|
|
252
298
|
/**
|
|
253
299
|
* Compara las globales de un script antes y después de transformarlo.
|
|
254
300
|
* `allowPrefix` permite nombres nuevos con ese prefijo (los que añade el ofuscador).
|
|
@@ -259,7 +305,7 @@ export function compareGlobals(before, afterCode, allowPrefix = null) {
|
|
|
259
305
|
try {
|
|
260
306
|
ast = parse(afterCode, 'script', LATEST);
|
|
261
307
|
} catch (err) {
|
|
262
|
-
return { ok: false, reason: `el resultado no es JS válido (${err.message})` };
|
|
308
|
+
return { ok: false, reason: L(`el resultado no es JS válido (${err.message})`, `the result is not valid JS (${err.message})`) };
|
|
263
309
|
}
|
|
264
310
|
const after = {
|
|
265
311
|
top: globalBindings(
|
|
@@ -268,12 +314,25 @@ export function compareGlobals(before, afterCode, allowPrefix = null) {
|
|
|
268
314
|
),
|
|
269
315
|
};
|
|
270
316
|
for (const [name, kind] of before) {
|
|
271
|
-
if (!after.top.has(name)) return { ok: false, reason: `desaparece la global "${name}"` };
|
|
272
|
-
if (after.top.get(name) !== kind)
|
|
317
|
+
if (!after.top.has(name)) return { ok: false, reason: L(`desaparece la global "${name}"`, `the global "${name}" disappears`) };
|
|
318
|
+
if (after.top.get(name) !== kind)
|
|
319
|
+
return {
|
|
320
|
+
ok: false,
|
|
321
|
+
reason: L(
|
|
322
|
+
`la global "${name}" cambia de tipo (${kind} → ${after.top.get(name)})`,
|
|
323
|
+
`the global "${name}" changes kind (${kind} → ${after.top.get(name)})`,
|
|
324
|
+
),
|
|
325
|
+
};
|
|
273
326
|
}
|
|
274
327
|
for (const name of after.top.keys()) {
|
|
275
328
|
if (!before.has(name) && !(allowPrefix && name.startsWith(allowPrefix)))
|
|
276
|
-
return {
|
|
329
|
+
return {
|
|
330
|
+
ok: false,
|
|
331
|
+
reason: L(
|
|
332
|
+
`aparece una global nueva "${name}" que podría chocar con otros scripts`,
|
|
333
|
+
`a new global "${name}" appears that could clash with other scripts`,
|
|
334
|
+
),
|
|
335
|
+
};
|
|
277
336
|
}
|
|
278
337
|
return { ok: true };
|
|
279
338
|
}
|
package/src/obfuscate.js
CHANGED
|
@@ -3,6 +3,7 @@ import * as acorn from 'acorn';
|
|
|
3
3
|
import { shortHash } from './util.js';
|
|
4
4
|
import { compareGlobals, moduleShape, checkSyntax } from './jsinfo.js';
|
|
5
5
|
import { importOptional } from './optional.js';
|
|
6
|
+
import { L } from './i18n.js';
|
|
6
7
|
|
|
7
8
|
// Nunca se activan solos: cambian el comportamiento o rompen código que comparte globales.
|
|
8
9
|
const FORCED_SAFE = { renameGlobals: false, renameProperties: false, sourceMap: false, target: 'browser' };
|
|
@@ -101,7 +102,7 @@ let engine = null;
|
|
|
101
102
|
/** Carga javascript-obfuscator (instalado junto a Builder o en el proyecto). */
|
|
102
103
|
export async function ensureObfuscator(projectRoot) {
|
|
103
104
|
if (!engine) {
|
|
104
|
-
const mod = await importOptional('javascript-obfuscator', projectRoot, 'para ofuscar el JavaScript');
|
|
105
|
+
const mod = await importOptional('javascript-obfuscator', projectRoot, L('para ofuscar el JavaScript', 'to obfuscate JavaScript'));
|
|
105
106
|
engine = mod.default ?? mod;
|
|
106
107
|
}
|
|
107
108
|
return engine;
|
|
@@ -143,15 +144,18 @@ export function obfuscateSafe(code, { isModule, options, id, globals }) {
|
|
|
143
144
|
try {
|
|
144
145
|
out = engine.obfuscate(body, opts).getObfuscatedCode();
|
|
145
146
|
} catch (err) {
|
|
146
|
-
return {
|
|
147
|
+
return {
|
|
148
|
+
code: null,
|
|
149
|
+
reason: L(`el ofuscador falló (${String(err.message).split('\n')[0]})`, `the obfuscator failed (${String(err.message).split('\n')[0]})`),
|
|
150
|
+
};
|
|
147
151
|
}
|
|
148
152
|
if (isModule) {
|
|
149
153
|
const err = checkSyntax(out, true);
|
|
150
|
-
if (err) return { code: null, reason: `resultado inválido (${err})` };
|
|
154
|
+
if (err) return { code: null, reason: L(`resultado inválido (${err})`, `invalid result (${err})`) };
|
|
151
155
|
const a = moduleShape(body);
|
|
152
156
|
const b = moduleShape(out);
|
|
153
157
|
if (!a || !b || a.exports !== b.exports || a.imports !== b.imports)
|
|
154
|
-
return { code: null, reason: 'cambiaron las exportaciones o importaciones del módulo' };
|
|
158
|
+
return { code: null, reason: L('cambiaron las exportaciones o importaciones del módulo', 'the module exports or imports changed') };
|
|
155
159
|
} else {
|
|
156
160
|
const cmp = compareGlobals(globals, out, prefix);
|
|
157
161
|
if (!cmp.ok) return { code: null, reason: cmp.reason };
|
package/src/optional.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
5
|
import { BuildError } from './util.js';
|
|
6
|
+
import { L } from './i18n.js';
|
|
6
7
|
|
|
7
8
|
const cache = new Map();
|
|
8
9
|
|
|
@@ -25,9 +26,12 @@ export async function importOptional(name, projectRoot, purpose) {
|
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
if (!mod)
|
|
28
|
-
throw new BuildError(`Falta el paquete opcional "${name}" ${purpose}.`, [
|
|
29
|
-
`Instálalo una vez con: npm install --save-dev ${name}`,
|
|
30
|
-
|
|
29
|
+
throw new BuildError(L(`Falta el paquete opcional "${name}" ${purpose}.`, `Missing optional package "${name}" ${purpose}.`), [
|
|
30
|
+
L(`Instálalo una vez con: npm install --save-dev ${name}`, `Install it once with: npm install --save-dev ${name}`),
|
|
31
|
+
L(
|
|
32
|
+
'Builder lo carga solo cuando hace falta, por eso no viene incluido (así la instalación base es ligera).',
|
|
33
|
+
'Builder loads it only when needed, so it is not bundled (this keeps the base install small).',
|
|
34
|
+
),
|
|
31
35
|
]);
|
|
32
36
|
cache.set(name, mod);
|
|
33
37
|
return mod;
|
package/src/output.js
CHANGED
|
@@ -3,6 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { BuildError, isInside, retryFs, copyDir } from './util.js';
|
|
6
|
+
import { L } from './i18n.js';
|
|
6
7
|
|
|
7
8
|
export const MANIFEST = '.builder-manifest.json';
|
|
8
9
|
|
|
@@ -12,18 +13,40 @@ export function assertSafeOutDir(cfg, { force = false } = {}) {
|
|
|
12
13
|
const root = path.resolve(cfg.root);
|
|
13
14
|
const parsed = path.parse(out);
|
|
14
15
|
const same = (a, b) => path.relative(a, b) === '';
|
|
15
|
-
if (same(out, root))
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
if (same(out, root))
|
|
17
|
+
throw new BuildError(
|
|
18
|
+
L(
|
|
19
|
+
'"outDir" no puede ser la propia carpeta del proyecto: se borraría el código fuente.',
|
|
20
|
+
'"outDir" cannot be the project folder itself: the source code would be deleted.',
|
|
21
|
+
),
|
|
22
|
+
);
|
|
23
|
+
if (isInside(out, root))
|
|
24
|
+
throw new BuildError(
|
|
25
|
+
L(
|
|
26
|
+
`"outDir" (${out}) contiene al proyecto; se borraría el código fuente.`,
|
|
27
|
+
`"outDir" (${out}) contains the project; the source code would be deleted.`,
|
|
28
|
+
),
|
|
29
|
+
);
|
|
30
|
+
if (same(out, parsed.root)) throw new BuildError(L('"outDir" no puede ser la raíz de una unidad.', '"outDir" cannot be the root of a drive.'));
|
|
31
|
+
if (same(out, os.homedir())) throw new BuildError(L('"outDir" no puede ser tu carpeta de usuario.', '"outDir" cannot be your home folder.'));
|
|
19
32
|
if (!cfg.clean || force) return;
|
|
20
33
|
if (fs.existsSync(out)) {
|
|
21
|
-
if (!fs.statSync(out).isDirectory())
|
|
34
|
+
if (!fs.statSync(out).isDirectory())
|
|
35
|
+
throw new BuildError(L(`"outDir" existe y no es una carpeta: ${out}`, `"outDir" exists and is not a folder: ${out}`));
|
|
22
36
|
const entries = fs.readdirSync(out);
|
|
23
37
|
if (entries.length && !entries.includes(MANIFEST))
|
|
24
|
-
throw new BuildError(
|
|
25
|
-
|
|
26
|
-
|
|
38
|
+
throw new BuildError(
|
|
39
|
+
L(
|
|
40
|
+
`La carpeta de salida ${out} ya tiene archivos y no la creó Builder. No la borro para no perder datos.`,
|
|
41
|
+
`The output folder ${out} already has files and was not created by Builder. Not deleting it, to avoid losing data.`,
|
|
42
|
+
),
|
|
43
|
+
[
|
|
44
|
+
L(
|
|
45
|
+
'Elige otra carpeta con --out, vacíala tú mismo o usa --force si estás seguro.',
|
|
46
|
+
'Choose another folder with --out, empty it yourself, or use --force if you are sure.',
|
|
47
|
+
),
|
|
48
|
+
],
|
|
49
|
+
);
|
|
27
50
|
}
|
|
28
51
|
}
|
|
29
52
|
|
|
@@ -89,7 +112,12 @@ export async function swapIntoPlace(cfg, staging) {
|
|
|
89
112
|
await rmrf(staging);
|
|
90
113
|
} catch (copyErr) {
|
|
91
114
|
if (exists) await retryFs(() => fs.promises.rename(old, out)).catch(() => {});
|
|
92
|
-
throw new BuildError(
|
|
115
|
+
throw new BuildError(
|
|
116
|
+
L(
|
|
117
|
+
`No pude colocar la salida en ${out}: ${copyErr.message ?? err.message}`,
|
|
118
|
+
`Could not place the output in ${out}: ${copyErr.message ?? err.message}`,
|
|
119
|
+
),
|
|
120
|
+
);
|
|
93
121
|
}
|
|
94
122
|
}
|
|
95
123
|
if (exists) await rmrf(old).catch(() => {});
|
package/src/plugins.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Plugins: ganchos sencillos para casos que Builder no cubre.
|
|
2
|
+
//
|
|
3
|
+
// export default {
|
|
4
|
+
// plugins: [
|
|
5
|
+
// {
|
|
6
|
+
// name: 'version',
|
|
7
|
+
// filter: /\.(html|js)$/, // opcional: a qué archivos se aplica transform
|
|
8
|
+
// transform: ({ path, code }) => code.replaceAll('__VERSION__', '1.2.3'),
|
|
9
|
+
// buildStart: ({ config, mode }) => {},
|
|
10
|
+
// buildEnd: ({ result, outDir }) => {},
|
|
11
|
+
// },
|
|
12
|
+
// ],
|
|
13
|
+
// };
|
|
14
|
+
//
|
|
15
|
+
// En builder.config.json se nombran paquetes: "plugins": ["mi-plugin", ["otro-plugin", { "opcion": 1 }]].
|
|
16
|
+
// Un paquete exporta el objeto plugin o una función (opciones) => plugin.
|
|
17
|
+
//
|
|
18
|
+
// transform cambia el CÓDIGO FUENTE (HTML, CSS, JS, TS) antes de procesarlo: así el hash, el SRI
|
|
19
|
+
// y la verificación final se calculan sobre el resultado del plugin.
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
22
|
+
import { pathToFileURL } from 'node:url';
|
|
23
|
+
import { BuildError } from './util.js';
|
|
24
|
+
import { L } from './i18n.js';
|
|
25
|
+
|
|
26
|
+
export const TRANSFORM_EXT = new Set(['.html', '.htm', '.css', '.js', '.mjs', '.cjs', '.ts', '.mts', '.cts']);
|
|
27
|
+
const HOOKS = ['transform', 'buildStart', 'buildEnd'];
|
|
28
|
+
|
|
29
|
+
/** Errores de validación de la opción "plugins" (antes de cargar paquetes). */
|
|
30
|
+
export function validatePlugins(list) {
|
|
31
|
+
if (!Array.isArray(list)) return [L('"plugins" debe ser una lista.', '"plugins" must be a list.')];
|
|
32
|
+
const errors = [];
|
|
33
|
+
list.forEach((p, i) => {
|
|
34
|
+
const ok =
|
|
35
|
+
typeof p === 'string' ||
|
|
36
|
+
(Array.isArray(p) && typeof p[0] === 'string' && p.length <= 2) ||
|
|
37
|
+
(p && typeof p === 'object' && !Array.isArray(p) && typeof p.name === 'string');
|
|
38
|
+
if (!ok)
|
|
39
|
+
errors.push(
|
|
40
|
+
L(
|
|
41
|
+
`"plugins[${i}]" debe ser un nombre de paquete, [nombre, opciones] o un objeto con "name".`,
|
|
42
|
+
`"plugins[${i}]" must be a package name, [name, options] or an object with "name".`,
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
else if (p && typeof p === 'object' && !Array.isArray(p)) {
|
|
46
|
+
for (const k of Object.keys(p))
|
|
47
|
+
if (!['name', 'filter', ...HOOKS].includes(k))
|
|
48
|
+
errors.push(
|
|
49
|
+
L(
|
|
50
|
+
`"plugins[${i}].${k}" no es un gancho conocido (${HOOKS.join(', ')}).`,
|
|
51
|
+
`"plugins[${i}].${k}" is not a known hook (${HOOKS.join(', ')}).`,
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
return errors;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Carga los plugins nombrados como paquete (desde el proyecto) y devuelve objetos plugin. */
|
|
60
|
+
export async function loadPlugins(list, root) {
|
|
61
|
+
const out = [];
|
|
62
|
+
for (const entry of list) {
|
|
63
|
+
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
|
64
|
+
out.push(entry);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const [name, options] = Array.isArray(entry) ? entry : [entry, undefined];
|
|
68
|
+
let mod;
|
|
69
|
+
try {
|
|
70
|
+
const req = createRequire(path.join(root, 'package.json'));
|
|
71
|
+
mod = await import(pathToFileURL(req.resolve(name)).href);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
throw new BuildError(L(`No se pudo cargar el plugin "${name}": ${err.message}`, `Could not load the plugin "${name}": ${err.message}`), [
|
|
74
|
+
L(`Instálalo en el proyecto: npm install --save-dev ${name}`, `Install it in the project: npm install --save-dev ${name}`),
|
|
75
|
+
]);
|
|
76
|
+
}
|
|
77
|
+
const exported = mod.default ?? mod;
|
|
78
|
+
const plugin = typeof exported === 'function' ? await exported(options ?? {}) : exported;
|
|
79
|
+
if (!plugin || typeof plugin !== 'object' || typeof plugin.name !== 'string')
|
|
80
|
+
throw new BuildError(
|
|
81
|
+
L(
|
|
82
|
+
`El plugin "${name}" debe exportar un objeto con "name" o una función que lo devuelva.`,
|
|
83
|
+
`The plugin "${name}" must export an object with "name" or a function that returns one.`,
|
|
84
|
+
),
|
|
85
|
+
);
|
|
86
|
+
out.push(plugin);
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function call(plugin, hook, arg, where = '') {
|
|
92
|
+
try {
|
|
93
|
+
return await plugin[hook](arg);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
throw new BuildError(
|
|
96
|
+
L(
|
|
97
|
+
`El plugin "${plugin.name}" falló en ${hook}${where}: ${err.message}`,
|
|
98
|
+
`The plugin "${plugin.name}" failed in ${hook}${where}: ${err.message}`,
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Ejecuta un gancho sin valor de retorno (buildStart, buildEnd) en todos los plugins, en orden. */
|
|
105
|
+
export async function runHook(plugins, hook, arg) {
|
|
106
|
+
for (const p of plugins) if (typeof p[hook] === 'function') await call(p, hook, arg);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Pasa el código de un archivo por los transform de todos los plugins, en orden. */
|
|
110
|
+
export async function runTransform(plugins, rel, code) {
|
|
111
|
+
let current = code;
|
|
112
|
+
for (const p of plugins) {
|
|
113
|
+
if (typeof p.transform !== 'function') continue;
|
|
114
|
+
if (p.filter instanceof RegExp && !p.filter.test(rel)) continue;
|
|
115
|
+
const r = await call(p, 'transform', { path: rel, code: current }, ` (${rel})`);
|
|
116
|
+
if (typeof r === 'string') current = r;
|
|
117
|
+
else if (r != null)
|
|
118
|
+
throw new BuildError(
|
|
119
|
+
L(
|
|
120
|
+
`El plugin "${p.name}" devolvió algo que no es texto en transform (${rel}).`,
|
|
121
|
+
`The plugin "${p.name}" returned something that is not text from transform (${rel}).`,
|
|
122
|
+
),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return current;
|
|
126
|
+
}
|