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/config.js
CHANGED
|
@@ -3,6 +3,10 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
5
|
import { BuildError } from './util.js';
|
|
6
|
+
import { L } from './i18n.js';
|
|
7
|
+
import { resolveBrowsers } from './browsers.js';
|
|
8
|
+
import { validateBudgets } from './budgets.js';
|
|
9
|
+
import { validatePlugins, loadPlugins } from './plugins.js';
|
|
6
10
|
|
|
7
11
|
export const CONFIG_FILES = ['builder.config.json', 'builder.config.mjs', 'builder.config.js'];
|
|
8
12
|
|
|
@@ -43,6 +47,14 @@ export const DEFAULTS = {
|
|
|
43
47
|
cache: true,
|
|
44
48
|
// Hilos para ofuscar/comprimir en paralelo: "auto" (núcleos - 1, máx. 8), un número, o 0 (sin hilos).
|
|
45
49
|
workers: 'auto',
|
|
50
|
+
// Navegadores objetivo: 'auto' (browserslist del proyecto si existe), false, o una consulta de browserslist.
|
|
51
|
+
browsers: 'auto',
|
|
52
|
+
// Optimizar PNG (sin pérdida) y JPEG/WebP (calidad indicada) con sharp: false, true o { quality: 80 }.
|
|
53
|
+
images: false,
|
|
54
|
+
// Presupuestos de tamaño por patrón de salida (o "total"): { "js/**/*.js": "50 KB", "total": "1 MB gzip" }.
|
|
55
|
+
budgets: false,
|
|
56
|
+
// Plugins: objetos con ganchos (builder.config.mjs) o nombres de paquete. Ver src/plugins.js.
|
|
57
|
+
plugins: [],
|
|
46
58
|
};
|
|
47
59
|
|
|
48
60
|
const NESTED = ['html', 'css', 'js', 'json', 'dependencies'];
|
|
@@ -68,13 +80,19 @@ function levenshtein(a, b) {
|
|
|
68
80
|
|
|
69
81
|
function suggest(key, options) {
|
|
70
82
|
const best = options.map((o) => [o, levenshtein(key, o)]).sort((x, y) => x[1] - y[1])[0];
|
|
71
|
-
return best && best[1] <= 3 ? ` ¿Quisiste decir "${best[0]}"?` : '';
|
|
83
|
+
return best && best[1] <= 3 ? L(` ¿Quisiste decir "${best[0]}"?`, ` Did you mean "${best[0]}"?`) : '';
|
|
72
84
|
}
|
|
73
85
|
|
|
74
86
|
function checkKeys(obj, defaults, prefix, errors) {
|
|
75
87
|
for (const key of Object.keys(obj)) {
|
|
76
88
|
if (key === '$schema') continue;
|
|
77
|
-
if (!(key in defaults))
|
|
89
|
+
if (!(key in defaults))
|
|
90
|
+
errors.push(
|
|
91
|
+
L(
|
|
92
|
+
`Opción desconocida "${prefix}${key}".${suggest(key, Object.keys(defaults))}`,
|
|
93
|
+
`Unknown option "${prefix}${key}".${suggest(key, Object.keys(defaults))}`,
|
|
94
|
+
),
|
|
95
|
+
);
|
|
78
96
|
}
|
|
79
97
|
}
|
|
80
98
|
|
|
@@ -82,7 +100,8 @@ const asList = (v) => (v == null ? [] : Array.isArray(v) ? v : [v]);
|
|
|
82
100
|
|
|
83
101
|
export function normalizeConfig(raw, errors = []) {
|
|
84
102
|
const user = raw ?? {};
|
|
85
|
-
if (typeof user !== 'object' || Array.isArray(user))
|
|
103
|
+
if (typeof user !== 'object' || Array.isArray(user))
|
|
104
|
+
throw new BuildError(L('La configuración debe ser un objeto JSON.', 'The configuration must be a JSON object.'));
|
|
86
105
|
checkKeys(user, DEFAULTS, '', errors);
|
|
87
106
|
const cfg = { ...DEFAULTS, ...user };
|
|
88
107
|
for (const k of NESTED) {
|
|
@@ -92,47 +111,96 @@ export function normalizeConfig(raw, errors = []) {
|
|
|
92
111
|
else if (typeof v === 'object') {
|
|
93
112
|
checkKeys(v, DEFAULTS[k], `${k}.`, errors);
|
|
94
113
|
cfg[k] = { ...DEFAULTS[k], ...v };
|
|
95
|
-
} else errors.push(`"${k}" debe ser un objeto o true/false.`);
|
|
114
|
+
} else errors.push(L(`"${k}" debe ser un objeto o true/false.`, `"${k}" must be an object or true/false.`));
|
|
96
115
|
}
|
|
97
116
|
for (const k of ['include', 'ignore']) {
|
|
98
117
|
cfg[k] = asList(cfg[k]);
|
|
99
|
-
if (cfg[k].some((x) => typeof x !== 'string'))
|
|
118
|
+
if (cfg[k].some((x) => typeof x !== 'string'))
|
|
119
|
+
errors.push(L(`"${k}" debe ser una lista de rutas o patrones (texto).`, `"${k}" must be a list of paths or patterns (text).`));
|
|
100
120
|
}
|
|
101
121
|
for (const k of ['include', 'exclude']) {
|
|
102
122
|
cfg.dependencies[k] = asList(cfg.dependencies[k]);
|
|
103
|
-
if (cfg.dependencies[k].some((x) => typeof x !== 'string'))
|
|
123
|
+
if (cfg.dependencies[k].some((x) => typeof x !== 'string'))
|
|
124
|
+
errors.push(
|
|
125
|
+
L(`"dependencies.${k}" debe ser una lista de rutas o patrones (texto).`, `"dependencies.${k}" must be a list of paths or patterns (text).`),
|
|
126
|
+
);
|
|
104
127
|
}
|
|
105
|
-
for (const k of ['detect', 'obfuscate'])
|
|
128
|
+
for (const k of ['detect', 'obfuscate'])
|
|
129
|
+
if (typeof cfg.dependencies[k] !== 'boolean')
|
|
130
|
+
errors.push(L(`"dependencies.${k}" debe ser true o false.`, `"dependencies.${k}" must be true or false.`));
|
|
106
131
|
if (cfg.entries !== 'auto') {
|
|
107
132
|
cfg.entries = asList(cfg.entries);
|
|
108
133
|
if (!cfg.entries.length || cfg.entries.some((x) => typeof x !== 'string'))
|
|
109
|
-
errors.push('"entries" debe ser "auto" o una lista de archivos/patrones.');
|
|
134
|
+
errors.push(L('"entries" debe ser "auto" o una lista de archivos/patrones.', '"entries" must be "auto" or a list of files/patterns.'));
|
|
110
135
|
}
|
|
111
136
|
for (const [key, allowed] of Object.entries(ENUMS)) {
|
|
112
137
|
const [a, b] = key.split('.');
|
|
113
138
|
const value = b ? cfg[a][b] : cfg[a];
|
|
114
|
-
if (!allowed.includes(value))
|
|
139
|
+
if (!allowed.includes(value))
|
|
140
|
+
errors.push(
|
|
141
|
+
L(
|
|
142
|
+
`"${key}" debe ser uno de: ${allowed.map((x) => JSON.stringify(x)).join(', ')}.`,
|
|
143
|
+
`"${key}" must be one of: ${allowed.map((x) => JSON.stringify(x)).join(', ')}.`,
|
|
144
|
+
),
|
|
145
|
+
);
|
|
115
146
|
}
|
|
116
|
-
if (typeof cfg.outDir !== 'string' || !cfg.outDir.trim()) errors.push('"outDir" debe ser una ruta.');
|
|
117
|
-
if (typeof cfg.base !== 'string' || !cfg.base.startsWith('/'))
|
|
147
|
+
if (typeof cfg.outDir !== 'string' || !cfg.outDir.trim()) errors.push(L('"outDir" debe ser una ruta.', '"outDir" must be a path.'));
|
|
148
|
+
if (typeof cfg.base !== 'string' || !cfg.base.startsWith('/'))
|
|
149
|
+
errors.push(L('"base" debe empezar con "/" (ej. "/" o "/mi-sitio/").', '"base" must start with "/" (e.g. "/" or "/my-site/").'));
|
|
118
150
|
else if (!cfg.base.endsWith('/')) cfg.base += '/';
|
|
119
|
-
if (typeof cfg.hash !== 'boolean') errors.push('"hash" debe ser true o false.');
|
|
120
|
-
|
|
151
|
+
if (typeof cfg.hash !== 'boolean') errors.push(L('"hash" debe ser true o false.', '"hash" must be true or false.'));
|
|
152
|
+
const b = cfg.browsers;
|
|
153
|
+
if (!(
|
|
154
|
+
b === 'auto' ||
|
|
155
|
+
b === false ||
|
|
156
|
+
(typeof b === 'string' && b.trim()) ||
|
|
157
|
+
(Array.isArray(b) && b.length && b.every((x) => typeof x === 'string'))
|
|
158
|
+
))
|
|
159
|
+
errors.push(
|
|
160
|
+
L(
|
|
161
|
+
'"browsers" debe ser "auto", false o una consulta de browserslist (texto o lista).',
|
|
162
|
+
'"browsers" must be "auto", false or a browserslist query (text or list).',
|
|
163
|
+
),
|
|
164
|
+
);
|
|
165
|
+
const im = cfg.images;
|
|
166
|
+
if (!(
|
|
167
|
+
typeof im === 'boolean' ||
|
|
168
|
+
(im &&
|
|
169
|
+
typeof im === 'object' &&
|
|
170
|
+
!Array.isArray(im) &&
|
|
171
|
+
Object.keys(im).every((k) => k === 'quality') &&
|
|
172
|
+
(im.quality === undefined || (Number.isInteger(im.quality) && im.quality >= 1 && im.quality <= 100)))
|
|
173
|
+
))
|
|
174
|
+
errors.push(L('"images" debe ser true, false o { "quality": 1-100 }.', '"images" must be true, false or { "quality": 1-100 }.'));
|
|
175
|
+
errors.push(...validateBudgets(cfg.budgets));
|
|
176
|
+
errors.push(...validatePlugins(cfg.plugins));
|
|
177
|
+
if (typeof cfg.cache !== 'boolean') errors.push(L('"cache" debe ser true o false.', '"cache" must be true or false.'));
|
|
121
178
|
if (!(cfg.workers === 'auto' || (Number.isInteger(cfg.workers) && cfg.workers >= 0 && cfg.workers <= 64)))
|
|
122
|
-
errors.push('"workers" debe ser "auto" o un número entre 0 y 64.');
|
|
179
|
+
errors.push(L('"workers" debe ser "auto" o un número entre 0 y 64.', '"workers" must be "auto" or a number between 0 and 64.'));
|
|
123
180
|
cfg.js.drop = asList(cfg.js.drop);
|
|
124
|
-
if (cfg.js.drop.some((d) => !['console', 'debugger'].includes(d)))
|
|
181
|
+
if (cfg.js.drop.some((d) => !['console', 'debugger'].includes(d)))
|
|
182
|
+
errors.push(L('"js.drop" solo admite "console" y "debugger".', '"js.drop" only accepts "console" and "debugger".'));
|
|
125
183
|
if (typeof cfg.js.chunkDir !== 'string' || /(^|\/)\.\.(\/|$)|^\//.test(cfg.js.chunkDir))
|
|
126
|
-
errors.push('"js.chunkDir" debe ser una carpeta relativa.');
|
|
184
|
+
errors.push(L('"js.chunkDir" debe ser una carpeta relativa.', '"js.chunkDir" must be a relative folder.'));
|
|
127
185
|
for (const k of ['css', 'js']) {
|
|
128
186
|
const t = cfg[k].target;
|
|
129
187
|
if (!(typeof t === 'string' || (Array.isArray(t) && t.every((x) => typeof x === 'string'))))
|
|
130
|
-
errors.push(
|
|
188
|
+
errors.push(
|
|
189
|
+
L(
|
|
190
|
+
`"${k}.target" debe ser texto o lista (ej. "es2020" o ["chrome90","safari15"]).`,
|
|
191
|
+
`"${k}.target" must be text or a list (e.g. "es2020" or ["chrome90","safari15"]).`,
|
|
192
|
+
),
|
|
193
|
+
);
|
|
131
194
|
}
|
|
132
195
|
const o = cfg.obfuscate;
|
|
133
196
|
if (o === true) cfg.obfuscate = 'medium';
|
|
134
197
|
else if (!(o === false || OBF_PRESETS.includes(o) || (o && typeof o === 'object' && !Array.isArray(o))))
|
|
135
|
-
errors.push(
|
|
198
|
+
errors.push(
|
|
199
|
+
L(
|
|
200
|
+
`"obfuscate" debe ser false, true, ${OBF_PRESETS.map((p) => `"${p}"`).join(', ')} o un objeto de opciones de javascript-obfuscator.`,
|
|
201
|
+
`"obfuscate" must be false, true, ${OBF_PRESETS.map((p) => `"${p}"`).join(', ')} or an object of javascript-obfuscator options.`,
|
|
202
|
+
),
|
|
203
|
+
);
|
|
136
204
|
return cfg;
|
|
137
205
|
}
|
|
138
206
|
|
|
@@ -142,7 +210,12 @@ async function readConfigFile(file) {
|
|
|
142
210
|
try {
|
|
143
211
|
return JSON.parse(text);
|
|
144
212
|
} catch (err) {
|
|
145
|
-
throw new BuildError(
|
|
213
|
+
throw new BuildError(
|
|
214
|
+
L(
|
|
215
|
+
`No pude leer ${path.basename(file)}: JSON inválido (${err.message}).`,
|
|
216
|
+
`Could not read ${path.basename(file)}: invalid JSON (${err.message}).`,
|
|
217
|
+
),
|
|
218
|
+
);
|
|
146
219
|
}
|
|
147
220
|
}
|
|
148
221
|
const mod = await import(`${pathToFileURL(file).href}?t=${fs.statSync(file).mtimeMs}`);
|
|
@@ -163,22 +236,32 @@ export function findConfigFile(dir) {
|
|
|
163
236
|
*/
|
|
164
237
|
export async function loadConfig({ dir = '.', config, overrides = {}, cwd = process.cwd() } = {}) {
|
|
165
238
|
const projectDir = path.resolve(cwd, dir);
|
|
166
|
-
if (!fs.existsSync(projectDir) || !fs.statSync(projectDir).isDirectory())
|
|
239
|
+
if (!fs.existsSync(projectDir) || !fs.statSync(projectDir).isDirectory())
|
|
240
|
+
throw new BuildError(L(`La carpeta del proyecto no existe: ${projectDir}`, `The project folder does not exist: ${projectDir}`));
|
|
167
241
|
const configFile = config ? path.resolve(cwd, config) : findConfigFile(projectDir);
|
|
168
|
-
if (config && !fs.existsSync(configFile))
|
|
242
|
+
if (config && !fs.existsSync(configFile))
|
|
243
|
+
throw new BuildError(L(`No existe el archivo de configuración: ${configFile}`, `The configuration file does not exist: ${configFile}`));
|
|
169
244
|
const fileCfg = configFile ? await readConfigFile(configFile) : {};
|
|
170
245
|
const merged = mergeOverrides(fileCfg ?? {}, overrides);
|
|
171
246
|
const errors = [];
|
|
172
247
|
const cfg = normalizeConfig(merged, errors);
|
|
173
|
-
if (errors.length) throw new BuildError('La configuración tiene errores:', errors);
|
|
248
|
+
if (errors.length) throw new BuildError(L('La configuración tiene errores:', 'The configuration has errors:'), errors);
|
|
174
249
|
const baseDir = configFile ? path.dirname(configFile) : projectDir;
|
|
175
250
|
cfg.root = path.resolve(baseDir, cfg.root);
|
|
176
|
-
if (!fs.existsSync(cfg.root)) throw new BuildError(`"root" no existe: ${cfg.root}`);
|
|
251
|
+
if (!fs.existsSync(cfg.root)) throw new BuildError(L(`"root" no existe: ${cfg.root}`, `"root" does not exist: ${cfg.root}`));
|
|
177
252
|
// esbuild resuelve los enlaces simbólicos de lo que importa; si la raíz no es la ruta real, el mismo
|
|
178
253
|
// módulo aparece con dos rutas (pasa en macOS con /tmp → /private/tmp, o al abrir el proyecto por un enlace).
|
|
179
254
|
cfg.root = fs.realpathSync(cfg.root);
|
|
180
255
|
cfg.outDir = path.resolve(cfg.root, cfg.outDir);
|
|
181
256
|
cfg.configFile = configFile;
|
|
257
|
+
// Navegadores objetivo (browserslist): solo cambian los targets que siguen con su valor por defecto.
|
|
258
|
+
cfg.plugins = await loadPlugins(cfg.plugins, cfg.root);
|
|
259
|
+
cfg.browserTargets = await resolveBrowsers(cfg.root, cfg.browsers);
|
|
260
|
+
const targets = cfg.browserTargets?.targets;
|
|
261
|
+
if (targets?.length) {
|
|
262
|
+
if (cfg.css.target === DEFAULTS.css.target) cfg.css.target = targets;
|
|
263
|
+
if (cfg.js.target === DEFAULTS.js.target) cfg.js.target = targets;
|
|
264
|
+
}
|
|
182
265
|
return cfg;
|
|
183
266
|
}
|
|
184
267
|
|
package/src/deps.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Detección de librerías de terceros (jQuery, Bootstrap, paquetes de npm...).
|
|
2
2
|
// No se ofuscan: no aporta nada, las hace más lentas y puede romper su licencia.
|
|
3
|
+
import { L } from './i18n.js';
|
|
3
4
|
|
|
4
5
|
const DEP_DIRS = /(^|\/)(node_modules|bower_components|jspm_packages|web_modules|vendors?|third[-_]?party|external|externals)\//i;
|
|
5
6
|
const MIN_NAME = /[.-](min|prod|production)\.m?js$|\.umd(\.min)?\.m?js$/i;
|
|
@@ -177,23 +178,27 @@ export function packageNames(pkg) {
|
|
|
177
178
|
* @param {{packages?: Map<string, string>}} [hints] nombres de package.json del proyecto
|
|
178
179
|
*/
|
|
179
180
|
export function detectDependency(rel, code = '', hints = {}) {
|
|
180
|
-
if (DEP_DIRS.test(rel)) return `está en ${rel.match(DEP_DIRS)[2]}
|
|
181
|
+
if (DEP_DIRS.test(rel)) return L(`está en ${rel.match(DEP_DIRS)[2]}/`, `in ${rel.match(DEP_DIRS)[2]}/`);
|
|
181
182
|
const name = rel.split('/').pop();
|
|
182
|
-
if (KNOWN.test(name)) return 'nombre de librería conocida';
|
|
183
|
-
if (MIN_NAME.test(name)) return 'versión ya compilada (.min.js / .umd.js)';
|
|
183
|
+
if (KNOWN.test(name)) return L('nombre de librería conocida', 'well-known library name');
|
|
184
|
+
if (MIN_NAME.test(name)) return L('versión ya compilada (.min.js / .umd.js)', 'already compiled build (.min.js / .umd.js)');
|
|
184
185
|
const pkgName = packageLikeName(name);
|
|
185
|
-
if (hints.packages?.has(pkgName))
|
|
186
|
+
if (hints.packages?.has(pkgName))
|
|
187
|
+
return L(
|
|
188
|
+
`se llama como el paquete "${hints.packages.get(pkgName)}" de tu package.json`,
|
|
189
|
+
`named like the "${hints.packages.get(pkgName)}" package in your package.json`,
|
|
190
|
+
);
|
|
186
191
|
const edges = code.length > 8000 ? code.slice(0, 5000) + code.slice(-3000) : code;
|
|
187
|
-
if (UMD.some((re) => re.test(edges))) return 'envoltorio UMD/AMD (formato de librería publicada)';
|
|
192
|
+
if (UMD.some((re) => re.test(edges))) return L('envoltorio UMD/AMD (formato de librería publicada)', 'UMD/AMD wrapper (published library format)');
|
|
188
193
|
// Cabecera de librería publicada: /*! o @license con número de versión (jQuery v3.7.1, Bootstrap v5.3.0...).
|
|
189
194
|
// Una cabecera propia sin versión ("/*! Mi app (c) 2026 */") no basta.
|
|
190
195
|
const header = /^\s*\/\*[\s\S]*?\*\//.exec(code.slice(0, 3000))?.[0] ?? '';
|
|
191
196
|
if ((/^\s*\/\*!/.test(header) || /@(license|preserve)\b/.test(header)) && /\bv?\d+\.\d+(\.\d+)?\b/.test(header))
|
|
192
|
-
return 'cabecera de licencia con versión';
|
|
197
|
+
return L('cabecera de licencia con versión', 'license header with a version');
|
|
193
198
|
// Código ya minificado: pocas líneas muy largas.
|
|
194
199
|
if (code.length > 4000) {
|
|
195
200
|
const lines = code.split('\n').length;
|
|
196
|
-
if (code.length / lines > 500) return 'código ya minificado';
|
|
201
|
+
if (code.length / lines > 500) return L('código ya minificado', 'already minified code');
|
|
197
202
|
}
|
|
198
203
|
return null;
|
|
199
204
|
}
|
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 = [
|
|
@@ -85,7 +86,7 @@ export function scanProject(cfg) {
|
|
|
85
86
|
try {
|
|
86
87
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
87
88
|
} catch (err) {
|
|
88
|
-
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})`) });
|
|
89
90
|
return;
|
|
90
91
|
}
|
|
91
92
|
entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
@@ -100,9 +101,10 @@ export function scanProject(cfg) {
|
|
|
100
101
|
const st = fs.statSync(abs);
|
|
101
102
|
isFile = st.isFile();
|
|
102
103
|
isDir = false;
|
|
103
|
-
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)') });
|
|
104
106
|
} catch {
|
|
105
|
-
ignored.push({ rel, reason: 'enlace simbólico roto' });
|
|
107
|
+
ignored.push({ rel, reason: L('enlace simbólico roto', 'broken symbolic link') });
|
|
106
108
|
continue;
|
|
107
109
|
}
|
|
108
110
|
}
|
|
@@ -113,7 +115,10 @@ export function scanProject(cfg) {
|
|
|
113
115
|
const byUser = isUser(rel) || isUser(probe);
|
|
114
116
|
const wanted = cfg.include.some((p) => normalizePattern(p).startsWith(`${rel}/`) || p.startsWith('**'));
|
|
115
117
|
if ((byUser || isDefault(probe)) && !wanted) {
|
|
116
|
-
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
|
+
});
|
|
117
122
|
continue;
|
|
118
123
|
}
|
|
119
124
|
walk(abs);
|
|
@@ -123,9 +128,9 @@ export function scanProject(cfg) {
|
|
|
123
128
|
const forced = isIncluded(rel);
|
|
124
129
|
let reason = null;
|
|
125
130
|
if (!forced) {
|
|
126
|
-
if (isSecret(rel)) reason = 'posible secreto (excluido por seguridad)';
|
|
127
|
-
else if (isUser(rel)) reason = 'ignore (configuración)';
|
|
128
|
-
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');
|
|
129
134
|
}
|
|
130
135
|
if (reason) {
|
|
131
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
|
+
}
|