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/bin/builder.js
CHANGED
|
@@ -10,12 +10,15 @@ import { startServer } from '../src/server.js';
|
|
|
10
10
|
import { watchTree } from '../src/watch.js';
|
|
11
11
|
import { BuildError, formatBytes, relFrom, isInside, JS_EXT, extOf, toPosix } from '../src/util.js';
|
|
12
12
|
import { globMatcher } from '../src/files.js';
|
|
13
|
+
import { L, setLang } from '../src/i18n.js';
|
|
13
14
|
|
|
14
15
|
const tty = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
15
16
|
const color = (code) => (text) => (tty ? `\x1b[${code}m${text}\x1b[0m` : String(text));
|
|
16
17
|
const C = { red: color(31), yellow: color(33), green: color(32), blue: color(34), gray: color(90), bold: color(1), cyan: color(36) };
|
|
17
18
|
|
|
18
|
-
const
|
|
19
|
+
const help = () =>
|
|
20
|
+
L(
|
|
21
|
+
`Builder ${VERSION} — empaqueta, minifica y ofusca proyectos web vanilla (HTML, CSS y JS)
|
|
19
22
|
|
|
20
23
|
Uso:
|
|
21
24
|
builder build [carpeta] [opciones] Compila el proyecto en dist/ (por defecto)
|
|
@@ -60,10 +63,92 @@ Opciones de build:
|
|
|
60
63
|
--json Resultado en JSON (para CI)
|
|
61
64
|
--verbose Muestra notas, archivos copiados y excluidos
|
|
62
65
|
--quiet Solo errores
|
|
63
|
-
|
|
66
|
+
--budget <patrón=tamaño> Presupuesto de tamaño, ej. "total=1MB gzip" o "js/*.js=50KB" (repetible)
|
|
67
|
+
--images Optimiza PNG (sin pérdida) y JPEG/WebP (calidad 80) con sharp
|
|
68
|
+
--image-quality <1-100> Calidad de JPEG/WebP con --images (por defecto 80)
|
|
69
|
+
--browsers <consulta> Navegadores objetivo (browserslist), ej. "> 0.5%, not dead"; --no-browsers los ignora
|
|
70
|
+
--lang <en|es> Idioma de los mensajes (por defecto: el del sistema; o BUILDER_LANG)
|
|
71
|
+
`,
|
|
72
|
+
`Builder ${VERSION} — bundle, minify and obfuscate vanilla web projects (HTML, CSS and JS)
|
|
73
|
+
|
|
74
|
+
Usage:
|
|
75
|
+
builder build [folder] [options] Build the project into dist/ (default)
|
|
76
|
+
builder analyze [folder] [--json] Show detected dependencies and the planned output, without writing
|
|
77
|
+
builder init [folder] [--force] Create builder.config.json from what was detected
|
|
78
|
+
builder deps [folder] List which JS files are third-party libraries (not obfuscated) and which are yours
|
|
79
|
+
builder deps add|own|reset <pattern…> Mark libraries (add), your code (own) or back to automatic (reset)
|
|
80
|
+
builder dev [folder] [--port N] Build, watch for changes and serve dist/ with live reload
|
|
81
|
+
builder serve [folder] [--port N] Serve the output folder to try it
|
|
82
|
+
|
|
83
|
+
Mark dependencies without configuration:
|
|
84
|
+
In the HTML: <script src="js/lib.js" data-builder="dependency"></script> (or "own")
|
|
85
|
+
In the file: /* @builder-dependency */ at the top (or @builder-own)
|
|
86
|
+
With deps: builder deps add "js/libs/**" (another folder: --dir <folder>)
|
|
87
|
+
|
|
88
|
+
Build options:
|
|
89
|
+
--out <folder> Output folder (default dist)
|
|
90
|
+
--config <file> Configuration file (default builder.config.json)
|
|
91
|
+
--entry <file> Explicit entry (repeatable). Default: every complete .html
|
|
92
|
+
--include <pattern> Always include files (repeatable), e.g. "data/**"
|
|
93
|
+
--ignore <pattern> Exclude files (repeatable), e.g. "drafts/**"
|
|
94
|
+
--obfuscate [level] Obfuscate your JS: low | medium | high | max (default medium)
|
|
95
|
+
Third-party libraries are detected and NOT obfuscated.
|
|
96
|
+
--dep <pattern> Mark files as a dependency by hand (not obfuscated), repeatable
|
|
97
|
+
--not-dep <pattern> Correct the detection: that file is yours and is obfuscated, repeatable
|
|
98
|
+
--obfuscate-deps Obfuscate dependencies too
|
|
99
|
+
--minifier terser Maximum JS compression with a second terser pass
|
|
100
|
+
--compress [gzip|brotli] Also write precompressed .gz and .br files (both by default)
|
|
101
|
+
--hash Hashed JS/CSS file names (long-term caching)
|
|
102
|
+
--sourcemap [mode] Generate source maps (linked | hidden | inline)
|
|
103
|
+
--target <target> Output browsers/ES level, e.g. es2018 (default: your own code's level)
|
|
104
|
+
--drop-console Remove console.* and debugger
|
|
105
|
+
--no-minify Do not minify (useful for debugging)
|
|
106
|
+
--no-merge Do not merge consecutive classic scripts
|
|
107
|
+
--unreferenced <mode> Unreferenced files: process | copy | skip (default process)
|
|
108
|
+
--base <path> Site base path for absolute URLs (default /)
|
|
109
|
+
--workers <n|auto> Threads for obfuscation/compression (0 = none; default auto)
|
|
110
|
+
--no-cache Do not use the cache from previous builds
|
|
111
|
+
--no-verify Skip the final verification (not recommended)
|
|
112
|
+
--force Allow emptying an output folder Builder did not create
|
|
113
|
+
--keep-failed Keep the temporary folder if verification fails
|
|
114
|
+
--json JSON result (for CI)
|
|
115
|
+
--verbose Show notes, copied and excluded files
|
|
116
|
+
--quiet Errors only
|
|
117
|
+
--budget <pattern=size> Size budget, e.g. "total=1MB gzip" or "js/*.js=50KB" (repeatable)
|
|
118
|
+
--images Optimize PNG (lossless) and JPEG/WebP (quality 80) with sharp
|
|
119
|
+
--image-quality <1-100> JPEG/WebP quality with --images (default 80)
|
|
120
|
+
--browsers <query> Target browsers (browserslist), e.g. "> 0.5%, not dead"; --no-browsers ignores them
|
|
121
|
+
--lang <en|es> Message language (default: system language; or BUILDER_LANG)
|
|
122
|
+
`,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// Opciones sin valor, y las que admiten la forma --no-x.
|
|
126
|
+
const BOOLEAN = new Set([
|
|
127
|
+
'help',
|
|
128
|
+
'version',
|
|
129
|
+
'json',
|
|
130
|
+
'verbose',
|
|
131
|
+
'quiet',
|
|
132
|
+
'force',
|
|
133
|
+
'keep-failed',
|
|
134
|
+
'open',
|
|
135
|
+
'hash',
|
|
136
|
+
'obfuscate-deps',
|
|
137
|
+
'drop-console',
|
|
138
|
+
'images',
|
|
139
|
+
]);
|
|
140
|
+
const NEGATABLE = new Set(['verify', 'minify', 'merge', 'cache', 'detect-deps', 'browsers', 'hash', 'images']);
|
|
141
|
+
|
|
142
|
+
function distance(a, b) {
|
|
143
|
+
const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
144
|
+
for (let j = 1; j <= b.length; j++) d[0][j] = j;
|
|
145
|
+
for (let i = 1; i <= a.length; i++)
|
|
146
|
+
for (let j = 1; j <= b.length; j++) d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
147
|
+
return d[a.length][b.length];
|
|
148
|
+
}
|
|
64
149
|
|
|
65
150
|
function parseArgs(argv) {
|
|
66
|
-
const args = { _: [], entry: [], include: [], ignore: [], dep: [], 'not-dep': [] };
|
|
151
|
+
const args = { _: [], entry: [], include: [], ignore: [], dep: [], 'not-dep': [], budget: [] };
|
|
67
152
|
const takesValue = new Set([
|
|
68
153
|
'out',
|
|
69
154
|
'config',
|
|
@@ -80,6 +165,10 @@ function parseArgs(argv) {
|
|
|
80
165
|
'minifier',
|
|
81
166
|
'workers',
|
|
82
167
|
'dir',
|
|
168
|
+
'lang',
|
|
169
|
+
'browsers',
|
|
170
|
+
'image-quality',
|
|
171
|
+
'budget',
|
|
83
172
|
]);
|
|
84
173
|
const optionalValue = { obfuscate: ['low', 'medium', 'high', 'max'], sourcemap: ['linked', 'hidden', 'inline'], compress: ['gzip', 'brotli'] };
|
|
85
174
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -89,9 +178,20 @@ function parseArgs(argv) {
|
|
|
89
178
|
continue;
|
|
90
179
|
}
|
|
91
180
|
let [key, val] = a.slice(2).split(/=(.*)/s);
|
|
181
|
+
const bare = key.startsWith('no-') && NEGATABLE.has(key.slice(3)) ? key.slice(3) : key;
|
|
182
|
+
if (!takesValue.has(key) && !optionalValue[key] && !BOOLEAN.has(key) && !(bare !== key && NEGATABLE.has(bare))) {
|
|
183
|
+
const known = [...takesValue, ...Object.keys(optionalValue), ...BOOLEAN, ...[...NEGATABLE].map((k) => `no-${k}`)];
|
|
184
|
+
const near = known.map((k) => [k, distance(key, k)]).sort((x, y) => x[1] - y[1])[0];
|
|
185
|
+
throw new BuildError(
|
|
186
|
+
L(`Opción desconocida --${key}.`, `Unknown option --${key}.`) +
|
|
187
|
+
(near && near[1] <= 3
|
|
188
|
+
? L(` ¿Quisiste decir --${near[0]}?`, ` Did you mean --${near[0]}?`)
|
|
189
|
+
: L(' Usa "builder help".', ' Use "builder help".')),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
92
192
|
if (takesValue.has(key)) {
|
|
93
193
|
if (val === undefined) val = argv[++i];
|
|
94
|
-
if (val === undefined) throw new BuildError(`Falta el valor de --${key}.`);
|
|
194
|
+
if (val === undefined) throw new BuildError(L(`Falta el valor de --${key}.`, `Missing value for --${key}.`));
|
|
95
195
|
if (Array.isArray(args[key])) args[key].push(val);
|
|
96
196
|
else args[key] = val;
|
|
97
197
|
} else if (optionalValue[key]) {
|
|
@@ -117,9 +217,30 @@ function overridesFrom(args) {
|
|
|
117
217
|
if (args.verify === false) o.verify = false;
|
|
118
218
|
if (args.compress !== undefined) o.compress = args.compress;
|
|
119
219
|
if (args.cache === false) o.cache = false;
|
|
220
|
+
if (args['image-quality'] !== undefined) {
|
|
221
|
+
const q = Number(args['image-quality']);
|
|
222
|
+
if (!(Number.isInteger(q) && q >= 1 && q <= 100))
|
|
223
|
+
throw new BuildError(L('--image-quality debe ser un número entre 1 y 100.', '--image-quality must be a number between 1 and 100.'));
|
|
224
|
+
o.images = { quality: q };
|
|
225
|
+
} else if (args.images !== undefined) o.images = !!args.images;
|
|
226
|
+
if (args.budget.length) {
|
|
227
|
+
// --budget "patrón=tamaño" (repetible): sustituye a los presupuestos de la configuración.
|
|
228
|
+
o.budgets = {};
|
|
229
|
+
for (const b of args.budget) {
|
|
230
|
+
const at = b.lastIndexOf('=');
|
|
231
|
+
if (at < 1)
|
|
232
|
+
throw new BuildError(
|
|
233
|
+
L(`--budget necesita la forma patrón=tamaño, ej. "js/*.js=50KB": ${b}`, `--budget needs the form pattern=size, e.g. "js/*.js=50KB": ${b}`),
|
|
234
|
+
);
|
|
235
|
+
o.budgets[b.slice(0, at)] = b.slice(at + 1);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (args.browsers === false) o.browsers = false;
|
|
239
|
+
else if (typeof args.browsers === 'string') o.browsers = args.browsers;
|
|
120
240
|
if (args.workers !== undefined) {
|
|
121
241
|
const n = Number(args.workers);
|
|
122
|
-
if (args.workers !== 'auto' && !(Number.isInteger(n) && n >= 0))
|
|
242
|
+
if (args.workers !== 'auto' && !(Number.isInteger(n) && n >= 0))
|
|
243
|
+
throw new BuildError(L('--workers debe ser "auto" o un número (0 = sin hilos).', '--workers must be "auto" or a number (0 = no threads).'));
|
|
123
244
|
o.workers = args.workers === 'auto' ? 'auto' : n;
|
|
124
245
|
}
|
|
125
246
|
const deps = {};
|
|
@@ -146,7 +267,12 @@ function printList(title, items, colorFn, limit = 30) {
|
|
|
146
267
|
if (!items.length) return;
|
|
147
268
|
console.log(`\n${colorFn(`${title} (${items.length})`)}`);
|
|
148
269
|
for (const i of items.slice(0, limit)) console.log(` • ${i}`);
|
|
149
|
-
if (items.length > limit)
|
|
270
|
+
if (items.length > limit)
|
|
271
|
+
console.log(
|
|
272
|
+
C.gray(
|
|
273
|
+
L(` … y ${items.length - limit} más (usa --json para verlos todos)`, ` … and ${items.length - limit} more (use --json to see them all)`),
|
|
274
|
+
),
|
|
275
|
+
);
|
|
150
276
|
}
|
|
151
277
|
|
|
152
278
|
function jsonResult(result) {
|
|
@@ -170,6 +296,7 @@ function jsonResult(result) {
|
|
|
170
296
|
notes: result.notes,
|
|
171
297
|
verify: result.verify && { errors: result.verify.errors, stats: result.verify.stats },
|
|
172
298
|
perf: result.perf,
|
|
299
|
+
budgets: result.budgets,
|
|
173
300
|
};
|
|
174
301
|
}
|
|
175
302
|
|
|
@@ -182,39 +309,74 @@ function printSummary(result, args) {
|
|
|
182
309
|
console.log(`\n${C.green('✔')} ${counts} ${C.gray(`(${(result.durationMs / 1000).toFixed(2)} s)`)}`);
|
|
183
310
|
if (result.verify) {
|
|
184
311
|
const s = result.verify.stats;
|
|
185
|
-
console.log(
|
|
312
|
+
console.log(
|
|
313
|
+
L(
|
|
314
|
+
`${C.green('✔')} Verificación: ${s.files} archivos escritos, ${s.refs} referencias comprobadas, ${s.js} JS válidos, 0 errores`,
|
|
315
|
+
`${C.green('✔')} Verification: ${s.files} files written, ${s.refs} references checked, ${s.js} valid JS, 0 errors`,
|
|
316
|
+
),
|
|
317
|
+
);
|
|
186
318
|
}
|
|
187
|
-
console.log(
|
|
188
|
-
|
|
319
|
+
console.log(
|
|
320
|
+
L(
|
|
321
|
+
`${C.green('✔')} Cobertura: ${result.coverage.size}/${result.coverage.size} archivos de origen con destino conocido`,
|
|
322
|
+
`${C.green('✔')} Coverage: ${result.coverage.size}/${result.coverage.size} source files with a known destination`,
|
|
323
|
+
),
|
|
324
|
+
);
|
|
325
|
+
if (result.budgets?.checked)
|
|
326
|
+
console.log(
|
|
327
|
+
`${C.green('✔')} ${L(`Presupuestos: ${result.budgets.checked} comprobados, todos dentro del límite`, `Budgets: ${result.budgets.checked} checked, all within limits`)}`,
|
|
328
|
+
);
|
|
329
|
+
if (r.compressed)
|
|
330
|
+
console.log(
|
|
331
|
+
L(`${C.green('✔')} ${r.compressed} archivos precomprimidos (.gz/.br)`, `${C.green('✔')} ${r.compressed} precompressed files (.gz/.br)`),
|
|
332
|
+
);
|
|
189
333
|
const p = result.perf;
|
|
190
334
|
if (p?.tasks) {
|
|
191
335
|
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
192
|
-
const where = p.threads
|
|
336
|
+
const where = p.threads
|
|
337
|
+
? L(
|
|
338
|
+
`en el hilo principal y ${plural(p.threads, 'hilo', 'hilos')} más`,
|
|
339
|
+
`on the main thread and ${plural(p.threads, 'more thread', 'more threads')}`,
|
|
340
|
+
)
|
|
341
|
+
: L('en el hilo principal', 'on the main thread');
|
|
193
342
|
const all = p.cacheHits === p.tasks;
|
|
194
343
|
const detail = all
|
|
195
344
|
? p.tasks === 1
|
|
196
|
-
? 'reutilizado de la caché'
|
|
197
|
-
: 'todos reutilizados de la caché'
|
|
198
|
-
: `${where}${p.cacheHits ? `, ${p.cacheHits} de la caché` : ''}
|
|
199
|
-
console.log(
|
|
345
|
+
? L('reutilizado de la caché', 'reused from the cache')
|
|
346
|
+
: L('todos reutilizados de la caché', 'all reused from the cache')
|
|
347
|
+
: L(`${where}${p.cacheHits ? `, ${p.cacheHits} de la caché` : ''}`, `${where}${p.cacheHits ? `, ${p.cacheHits} from the cache` : ''}`);
|
|
348
|
+
console.log(
|
|
349
|
+
L(
|
|
350
|
+
`${C.green('✔')} Ofuscación/terser: ${plural(p.tasks, 'archivo', 'archivos')}, ${detail}`,
|
|
351
|
+
`${C.green('✔')} Obfuscation/terser: ${plural(p.tasks, 'file', 'files')}, ${detail}`,
|
|
352
|
+
),
|
|
353
|
+
);
|
|
200
354
|
}
|
|
201
355
|
if (result.obfuscated) {
|
|
202
356
|
const deps = [
|
|
203
357
|
...result.dependencies.map((d) => `${d.rel} — ${d.reason}`),
|
|
204
|
-
...result.vendor.map((v) => `${v} — paquete importado (archivo vendor aparte)`),
|
|
358
|
+
...result.vendor.map((v) => L(`${v} — paquete importado (archivo vendor aparte)`, `${v} — imported package (separate vendor file)`)),
|
|
205
359
|
];
|
|
206
|
-
if (deps.length) printList('Dependencias detectadas (no se ofuscan)', deps, C.cyan);
|
|
360
|
+
if (deps.length) printList(L('Dependencias detectadas (no se ofuscan)', 'Detected dependencies (not obfuscated)'), deps, C.cyan);
|
|
207
361
|
}
|
|
208
|
-
printList('Avisos', result.warnings, C.yellow);
|
|
362
|
+
printList(L('Avisos', 'Warnings'), result.warnings, C.yellow);
|
|
209
363
|
if (args.verbose) {
|
|
210
|
-
printList('Notas', result.notes, C.cyan, 200);
|
|
364
|
+
printList(L('Notas', 'Notes'), result.notes, C.cyan, 200);
|
|
211
365
|
printList(
|
|
212
|
-
'Excluidos',
|
|
366
|
+
L('Excluidos', 'Excluded'),
|
|
213
367
|
result.ignored.map((i) => `${i.rel} — ${i.reason}`),
|
|
214
368
|
C.gray,
|
|
215
369
|
200,
|
|
216
370
|
);
|
|
217
|
-
} else if (result.notes.length)
|
|
371
|
+
} else if (result.notes.length)
|
|
372
|
+
console.log(
|
|
373
|
+
C.gray(
|
|
374
|
+
L(
|
|
375
|
+
`\n${result.notes.length} nota(s) informativas: usa --verbose para verlas.`,
|
|
376
|
+
`\n${result.notes.length} informational note(s): use --verbose to see them.`,
|
|
377
|
+
),
|
|
378
|
+
),
|
|
379
|
+
);
|
|
218
380
|
}
|
|
219
381
|
|
|
220
382
|
async function cmdBuild(dir, args) {
|
|
@@ -234,49 +396,78 @@ async function cmdAnalyze(dir, args) {
|
|
|
234
396
|
console.log(JSON.stringify(jsonResult(result), null, 2));
|
|
235
397
|
return;
|
|
236
398
|
}
|
|
237
|
-
console.log(`${C.bold(`Builder ${VERSION} — análisis`)} ${cfg.root}\n`);
|
|
238
|
-
console.log(
|
|
399
|
+
console.log(L(`${C.bold(`Builder ${VERSION} — análisis`)} ${cfg.root}\n`, `${C.bold(`Builder ${VERSION} — analysis`)} ${cfg.root}\n`));
|
|
400
|
+
console.log(
|
|
401
|
+
L(
|
|
402
|
+
`${C.bold('Páginas completas')}: ${result.pages.full.join(', ') || '—'}`,
|
|
403
|
+
`${C.bold('Complete pages')}: ${result.pages.full.join(', ') || '—'}`,
|
|
404
|
+
),
|
|
405
|
+
);
|
|
239
406
|
if (result.pages.conservative.length)
|
|
240
407
|
console.log(`${C.bold('Modo conservador')} (fragmentos u otras codificaciones): ${result.pages.conservative.join(', ')}`);
|
|
241
|
-
console.log(`\n${C.bold('Qué pasará con cada archivo del proyecto')}`);
|
|
408
|
+
console.log(L(`\n${C.bold('Qué pasará con cada archivo del proyecto')}`, `\n${C.bold('What will happen to each project file')}`));
|
|
242
409
|
const rows = [...result.coverage.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
243
410
|
const w = Math.min(50, Math.max(...rows.map(([k]) => k.length), 10));
|
|
244
411
|
for (const [rel, list] of rows) {
|
|
245
|
-
const shown = args.verbose ? list : list.filter((s) => s !==
|
|
412
|
+
const shown = args.verbose ? list : list.filter((s) => s !== `${L('copiado', 'copied')} → ${rel}`);
|
|
246
413
|
if (!shown.length) continue;
|
|
247
414
|
console.log(` ${rel.padEnd(w)} ${C.gray(shown.join(' | '))}`);
|
|
248
415
|
}
|
|
249
|
-
const copied = rows.filter(([rel, list]) => list.length === 1 && list[0] ===
|
|
250
|
-
if (copied && !args.verbose)
|
|
416
|
+
const copied = rows.filter(([rel, list]) => list.length === 1 && list[0] === `${L('copiado', 'copied')} → ${rel}`).length;
|
|
417
|
+
if (copied && !args.verbose)
|
|
418
|
+
console.log(
|
|
419
|
+
C.gray(
|
|
420
|
+
L(
|
|
421
|
+
` + ${copied} recursos que se copian sin cambios (--verbose para listarlos)`,
|
|
422
|
+
` + ${copied} assets copied unchanged (--verbose to list them)`,
|
|
423
|
+
),
|
|
424
|
+
),
|
|
425
|
+
);
|
|
251
426
|
const deps = [...result.dependencies.map((d) => `${d.rel} — ${d.reason}`)];
|
|
252
|
-
printList('Dependencias de terceros detectadas (no se ofuscan)', deps, C.cyan);
|
|
427
|
+
printList(L('Dependencias de terceros detectadas (no se ofuscan)', 'Detected third-party dependencies (not obfuscated)'), deps, C.cyan);
|
|
253
428
|
if (result.workers.length)
|
|
254
429
|
printList(
|
|
255
|
-
'Workers detectados',
|
|
256
|
-
result.workers.map((wk) =>
|
|
430
|
+
L('Workers detectados', 'Detected workers'),
|
|
431
|
+
result.workers.map((wk) =>
|
|
432
|
+
L(
|
|
433
|
+
`${wk.rel} (${wk.kind}${wk.module ? ', módulo' : ''}) desde ${wk.from}`,
|
|
434
|
+
`${wk.rel} (${wk.kind}${wk.module ? ', module' : ''}) from ${wk.from}`,
|
|
435
|
+
),
|
|
436
|
+
),
|
|
257
437
|
C.cyan,
|
|
258
438
|
);
|
|
259
439
|
printList(
|
|
260
|
-
'Referencias rotas en el código fuente',
|
|
440
|
+
L('Referencias rotas en el código fuente', 'Broken references in the source code'),
|
|
261
441
|
result.broken.map((b) => `${b.from} → ${b.url} (${b.status})`),
|
|
262
442
|
C.yellow,
|
|
263
443
|
);
|
|
264
|
-
printList('Avisos', result.warnings, C.yellow);
|
|
444
|
+
printList(L('Avisos', 'Warnings'), result.warnings, C.yellow);
|
|
265
445
|
printList(
|
|
266
|
-
'Excluidos',
|
|
446
|
+
L('Excluidos', 'Excluded'),
|
|
267
447
|
result.ignored.map((i) => `${i.rel} — ${i.reason}`),
|
|
268
448
|
C.gray,
|
|
269
449
|
args.verbose ? 500 : 15,
|
|
270
450
|
);
|
|
271
|
-
if (args.verbose) printList('Notas', result.notes, C.cyan, 500);
|
|
451
|
+
if (args.verbose) printList(L('Notas', 'Notes'), result.notes, C.cyan, 500);
|
|
272
452
|
const t = result.report.totals;
|
|
273
|
-
console.log(
|
|
453
|
+
console.log(
|
|
454
|
+
L(
|
|
455
|
+
`\nSalida prevista: ${t.files} archivos, ${formatBytes(t.before)} → ${formatBytes(t.size)} (sin ofuscar). Nada se escribió.`,
|
|
456
|
+
`\nPlanned output: ${t.files} files, ${formatBytes(t.before)} → ${formatBytes(t.size)} (not obfuscated). Nothing was written.`,
|
|
457
|
+
),
|
|
458
|
+
);
|
|
274
459
|
}
|
|
275
460
|
|
|
276
461
|
async function cmdInit(dir, args) {
|
|
277
462
|
const projectDir = path.resolve(dir);
|
|
278
463
|
const existing = findConfigFile(projectDir);
|
|
279
|
-
if (existing && !args.force)
|
|
464
|
+
if (existing && !args.force)
|
|
465
|
+
throw new BuildError(
|
|
466
|
+
L(
|
|
467
|
+
`Ya existe ${path.basename(existing)}. Usa --force para reemplazarlo.`,
|
|
468
|
+
`${path.basename(existing)} already exists. Use --force to replace it.`,
|
|
469
|
+
),
|
|
470
|
+
);
|
|
280
471
|
const cfg = await loadConfig({ dir, config: args.config, overrides: overridesFrom(args) });
|
|
281
472
|
const result = await runPipeline(cfg, { mode: 'analyze' });
|
|
282
473
|
const config = {
|
|
@@ -295,12 +486,44 @@ async function cmdInit(dir, args) {
|
|
|
295
486
|
const file = path.join(projectDir, 'builder.config.json');
|
|
296
487
|
fs.writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
|
|
297
488
|
console.log(`${C.green('✔')} Creado ${file}`);
|
|
298
|
-
console.log(
|
|
299
|
-
|
|
489
|
+
console.log(
|
|
490
|
+
L(
|
|
491
|
+
` Páginas detectadas: ${result.pages.full.join(', ') || '— (ninguna; define "entries")'}`,
|
|
492
|
+
` Detected pages: ${result.pages.full.join(', ') || '— (none; set "entries")'}`,
|
|
493
|
+
),
|
|
494
|
+
);
|
|
495
|
+
if (result.broken.length)
|
|
496
|
+
console.log(
|
|
497
|
+
C.yellow(
|
|
498
|
+
L(
|
|
499
|
+
` Hay ${result.broken.length} referencia(s) rota(s) en el código; míralas con "builder analyze".`,
|
|
500
|
+
` There are ${result.broken.length} broken reference(s) in the code; see them with "builder analyze".`,
|
|
501
|
+
),
|
|
502
|
+
),
|
|
503
|
+
);
|
|
300
504
|
if (result.dependencies.length)
|
|
301
|
-
console.log(
|
|
302
|
-
|
|
303
|
-
|
|
505
|
+
console.log(
|
|
506
|
+
L(
|
|
507
|
+
` Librerías de terceros detectadas (no se ofuscarán): ${result.dependencies.map((d) => d.rel).join(', ')}`,
|
|
508
|
+
` Detected third-party libraries (will not be obfuscated): ${result.dependencies.map((d) => d.rel).join(', ')}`,
|
|
509
|
+
),
|
|
510
|
+
);
|
|
511
|
+
console.log(
|
|
512
|
+
C.gray(
|
|
513
|
+
L(
|
|
514
|
+
' Edita "include" para forzar archivos que solo se cargan desde JS, e "ignore" para excluir.',
|
|
515
|
+
' Edit "include" to force files that are only loaded from JS, and "ignore" to exclude.',
|
|
516
|
+
),
|
|
517
|
+
),
|
|
518
|
+
);
|
|
519
|
+
console.log(
|
|
520
|
+
C.gray(
|
|
521
|
+
L(
|
|
522
|
+
' En "dependencies.include" marcas a mano librerías; en "dependencies.exclude" corriges una detección.',
|
|
523
|
+
' In "dependencies.include" you mark libraries by hand; in "dependencies.exclude" you correct a detection.',
|
|
524
|
+
),
|
|
525
|
+
),
|
|
526
|
+
);
|
|
304
527
|
}
|
|
305
528
|
|
|
306
529
|
/** builder deps [carpeta] — lista; builder deps add|own|reset <patrón…> — edita builder.config.json. */
|
|
@@ -316,28 +539,52 @@ async function cmdDeps(_dir, args) {
|
|
|
316
539
|
console.log(JSON.stringify({ dependencies: result.dependencies, own }, null, 2));
|
|
317
540
|
return;
|
|
318
541
|
}
|
|
319
|
-
console.log(`${C.bold(`Builder ${VERSION} — dependencias`)} ${cfg.root}`);
|
|
542
|
+
console.log(`${C.bold(`Builder ${VERSION} — ${L('dependencias', 'dependencies')}`)} ${cfg.root}`);
|
|
320
543
|
printList(
|
|
321
|
-
'Librerías de terceros (no se ofuscan)',
|
|
544
|
+
L('Librerías de terceros (no se ofuscan)', 'Third-party libraries (not obfuscated)'),
|
|
322
545
|
[...deps].map(([rel, why]) => `${rel} ${C.gray(`— ${why}`)}`),
|
|
323
546
|
C.cyan,
|
|
324
547
|
200,
|
|
325
548
|
);
|
|
326
|
-
printList('Tu código (se ofusca con --obfuscate)', own, C.green, 200);
|
|
327
|
-
if (!deps.size && !own.length) console.log('\nNo hay archivos JavaScript en el proyecto.');
|
|
328
|
-
console.log(C.gray('\n¿Algo mal clasificado?'));
|
|
329
|
-
console.log(
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
console.log(
|
|
549
|
+
printList(L('Tu código (se ofusca con --obfuscate)', 'Your code (obfuscated with --obfuscate)'), own, C.green, 200);
|
|
550
|
+
if (!deps.size && !own.length) console.log(L('\nNo hay archivos JavaScript en el proyecto.', '\nThere are no JavaScript files in the project.'));
|
|
551
|
+
console.log(C.gray(L('\n¿Algo mal clasificado?', '\nSomething classified wrong?')));
|
|
552
|
+
console.log(
|
|
553
|
+
C.gray(L(' builder deps add <patrón> es una librería: no se ofusca', ' builder deps add <pattern> it is a library: not obfuscated')),
|
|
554
|
+
);
|
|
555
|
+
console.log(
|
|
556
|
+
C.gray(L(' builder deps own <patrón> es tu código: sí se ofusca', ' builder deps own <pattern> it is your code: obfuscated')),
|
|
557
|
+
);
|
|
558
|
+
console.log(
|
|
559
|
+
C.gray(L(' builder deps reset <patrón> vuelve a la detección automática', ' builder deps reset <pattern> back to automatic detection')),
|
|
560
|
+
);
|
|
561
|
+
console.log(
|
|
562
|
+
C.gray(
|
|
563
|
+
L(
|
|
564
|
+
' También: data-builder="dependency" en el <script>, o /* @builder-dependency */ al inicio del archivo.',
|
|
565
|
+
' Also: data-builder="dependency" on the <script>, or /* @builder-dependency */ at the top of the file.',
|
|
566
|
+
),
|
|
567
|
+
),
|
|
568
|
+
);
|
|
333
569
|
}
|
|
334
570
|
|
|
335
571
|
async function editDeps(sub, rawPatterns, args) {
|
|
336
|
-
if (!rawPatterns.length)
|
|
572
|
+
if (!rawPatterns.length)
|
|
573
|
+
throw new BuildError(
|
|
574
|
+
L(
|
|
575
|
+
`Indica uno o más archivos o patrones: builder deps ${sub} "js/vendor/**"`,
|
|
576
|
+
`Give one or more files or patterns: builder deps ${sub} "js/vendor/**"`,
|
|
577
|
+
),
|
|
578
|
+
);
|
|
337
579
|
const projectDir = path.resolve(args.dir ?? '.');
|
|
338
580
|
const file = args.config ? path.resolve(args.config) : (findConfigFile(projectDir) ?? path.join(projectDir, 'builder.config.json'));
|
|
339
581
|
if (!file.endsWith('.json'))
|
|
340
|
-
throw new BuildError(
|
|
582
|
+
throw new BuildError(
|
|
583
|
+
L(
|
|
584
|
+
`La configuración está en ${path.basename(file)} (JavaScript); añade los patrones a mano en "dependencies".`,
|
|
585
|
+
`The configuration is in ${path.basename(file)} (JavaScript); add the patterns to "dependencies" by hand.`,
|
|
586
|
+
),
|
|
587
|
+
);
|
|
341
588
|
const config = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8').replace(/^/, '')) : {};
|
|
342
589
|
if (config.dependencies === false || config.dependencies === true || config.dependencies == null) config.dependencies = {};
|
|
343
590
|
const d = config.dependencies;
|
|
@@ -369,19 +616,38 @@ async function editDeps(sub, rawPatterns, args) {
|
|
|
369
616
|
for (const p of patterns) {
|
|
370
617
|
const hits = jsFiles.filter(globMatcher([p]));
|
|
371
618
|
if (!hits.length) {
|
|
372
|
-
console.log(
|
|
619
|
+
console.log(
|
|
620
|
+
C.yellow(
|
|
621
|
+
L(
|
|
622
|
+
` ! "${p}" no coincide con ningún archivo JavaScript del proyecto (queda guardado igualmente)`,
|
|
623
|
+
` ! "${p}" does not match any JavaScript file in the project (saved anyway)`,
|
|
624
|
+
),
|
|
625
|
+
),
|
|
626
|
+
);
|
|
373
627
|
continue;
|
|
374
628
|
}
|
|
375
|
-
for (const rel of hits)
|
|
629
|
+
for (const rel of hits)
|
|
630
|
+
console.log(
|
|
631
|
+
L(
|
|
632
|
+
` ${rel} ${deps.has(rel) ? C.cyan(`librería — ${deps.get(rel)}`) : C.green('tu código — se ofusca')}`,
|
|
633
|
+
` ${rel} ${deps.has(rel) ? C.cyan(`library — ${deps.get(rel)}`) : C.green('your code — obfuscated')}`,
|
|
634
|
+
),
|
|
635
|
+
);
|
|
376
636
|
}
|
|
377
637
|
}
|
|
378
638
|
|
|
379
639
|
async function cmdServe(dir, args) {
|
|
380
640
|
const cfg = await loadConfig({ dir, config: args.config, overrides: overridesFrom(args) });
|
|
381
641
|
const target = fs.existsSync(cfg.outDir) ? cfg.outDir : null;
|
|
382
|
-
if (!target)
|
|
642
|
+
if (!target)
|
|
643
|
+
throw new BuildError(L(`No existe ${cfg.outDir}. Ejecuta primero "builder build".`, `${cfg.outDir} does not exist. Run "builder build" first.`));
|
|
383
644
|
const srv = await startServer({ dir: target, port: Number(args.port ?? 5173), host: args.host ?? '127.0.0.1' });
|
|
384
|
-
console.log(
|
|
645
|
+
console.log(
|
|
646
|
+
L(
|
|
647
|
+
`${C.green('✔')} Sirviendo ${target} en ${C.bold(srv.url)} ${C.gray('(Ctrl+C para salir)')}`,
|
|
648
|
+
`${C.green('✔')} Serving ${target} at ${C.bold(srv.url)} ${C.gray('(Ctrl+C to quit)')}`,
|
|
649
|
+
),
|
|
650
|
+
);
|
|
385
651
|
if (args.open) openBrowser(srv.url);
|
|
386
652
|
}
|
|
387
653
|
|
|
@@ -390,9 +656,12 @@ async function cmdDev(dir, args) {
|
|
|
390
656
|
let srv = null;
|
|
391
657
|
let running = false;
|
|
392
658
|
let pending = false;
|
|
393
|
-
|
|
659
|
+
// files: archivos que cambiaron; si todos son CSS (y sin hash), basta con recargar las hojas de estilos.
|
|
660
|
+
let pendingFiles = [];
|
|
661
|
+
const rebuild = async (reason, files = null) => {
|
|
394
662
|
if (running) {
|
|
395
663
|
pending = true;
|
|
664
|
+
pendingFiles = files && pendingFiles ? [...pendingFiles, ...files] : null;
|
|
396
665
|
return;
|
|
397
666
|
}
|
|
398
667
|
running = true;
|
|
@@ -401,11 +670,13 @@ async function cmdDev(dir, args) {
|
|
|
401
670
|
const fresh = await loadConfig({ dir, config: args.config, overrides: overridesFrom(args) });
|
|
402
671
|
const result = await runPipeline(fresh, { mode: 'build', force: !!args.force });
|
|
403
672
|
console.log(
|
|
404
|
-
`${C.green('✔')} ${reason ? `${reason} → ` : ''}compilado en ${Date.now() - t} ms${result.warnings.length ? C.yellow(` (${result.warnings.length} avisos)`) : ''}`,
|
|
673
|
+
`${C.green('✔')} ${reason ? `${reason} → ` : ''}${L('compilado en', 'built in')} ${Date.now() - t} ms${result.warnings.length ? C.yellow(L(` (${result.warnings.length} avisos)`, ` (${result.warnings.length} warnings)`)) : ''}`,
|
|
405
674
|
);
|
|
406
675
|
for (const w of result.warnings.slice(0, 5)) console.log(C.yellow(` • ${w}`));
|
|
407
676
|
srv?.buildOk();
|
|
408
|
-
|
|
677
|
+
const cssOnly = files?.length && !fresh.hash && files.every((f) => /\.css$/i.test(f));
|
|
678
|
+
if (cssOnly) srv?.reloadCss();
|
|
679
|
+
else srv?.reload();
|
|
409
680
|
} catch (err) {
|
|
410
681
|
console.error(C.red(`✖ ${err.message}`));
|
|
411
682
|
for (const d of err.details ?? []) console.error(C.red(` • ${d}`));
|
|
@@ -414,14 +685,21 @@ async function cmdDev(dir, args) {
|
|
|
414
685
|
running = false;
|
|
415
686
|
if (pending) {
|
|
416
687
|
pending = false;
|
|
417
|
-
|
|
688
|
+
const next = pendingFiles;
|
|
689
|
+
pendingFiles = [];
|
|
690
|
+
rebuild(L('cambios pendientes', 'pending changes'), next);
|
|
418
691
|
}
|
|
419
692
|
}
|
|
420
693
|
};
|
|
421
694
|
await rebuild();
|
|
422
695
|
fs.mkdirSync(cfg.outDir, { recursive: true });
|
|
423
696
|
srv = await startServer({ dir: cfg.outDir, port: Number(args.port ?? 5173), host: args.host ?? '127.0.0.1', liveReload: true });
|
|
424
|
-
console.log(
|
|
697
|
+
console.log(
|
|
698
|
+
L(
|
|
699
|
+
`${C.green('✔')} Sirviendo ${cfg.outDir} en ${C.bold(srv.url)} con recarga automática ${C.gray('(Ctrl+C para salir)')}`,
|
|
700
|
+
`${C.green('✔')} Serving ${cfg.outDir} at ${C.bold(srv.url)} with live reload ${C.gray('(Ctrl+C to quit)')}`,
|
|
701
|
+
),
|
|
702
|
+
);
|
|
425
703
|
if (args.open) openBrowser(srv.url);
|
|
426
704
|
let timer = null;
|
|
427
705
|
const changed = new Set();
|
|
@@ -436,7 +714,7 @@ async function cmdDev(dir, args) {
|
|
|
436
714
|
timer = setTimeout(() => {
|
|
437
715
|
const list = [...changed];
|
|
438
716
|
changed.clear();
|
|
439
|
-
rebuild(list.length === 1 ? list[0] : `${list.length} archivos`);
|
|
717
|
+
rebuild(list.length === 1 ? list[0] : L(`${list.length} archivos`, `${list.length} files`), list);
|
|
440
718
|
}, 150);
|
|
441
719
|
});
|
|
442
720
|
}
|
|
@@ -449,12 +727,16 @@ function openBrowser(url) {
|
|
|
449
727
|
|
|
450
728
|
async function main() {
|
|
451
729
|
const argv = process.argv.slice(2);
|
|
730
|
+
// El idioma se fija antes que nada para que todos los mensajes salgan en él.
|
|
731
|
+
const langAt = argv.findIndex((a) => a === '--lang' || a.startsWith('--lang='));
|
|
732
|
+
if (langAt > -1) setLang(argv[langAt].includes('=') ? argv[langAt].split('=')[1] : argv[langAt + 1]);
|
|
452
733
|
const args = parseArgs(argv);
|
|
453
734
|
if (args.version || args.v) return console.log(VERSION);
|
|
454
735
|
const [command = 'help', dir = '.'] = args._;
|
|
455
|
-
if (args.help || args.h || command === 'help') return console.log(
|
|
736
|
+
if (args.help || args.h || command === 'help') return console.log(help());
|
|
456
737
|
const commands = { build: cmdBuild, analyze: cmdAnalyze, init: cmdInit, serve: cmdServe, dev: cmdDev, deps: cmdDeps };
|
|
457
|
-
if (!commands[command])
|
|
738
|
+
if (!commands[command])
|
|
739
|
+
throw new BuildError(L(`Comando desconocido "${command}". Usa "builder help".`, `Unknown command "${command}". Use "builder help".`));
|
|
458
740
|
await commands[command](dir, args);
|
|
459
741
|
}
|
|
460
742
|
|