vanilla-builder 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/README.es.md +553 -107
- package/README.md +545 -101
- package/bin/builder.js +432 -46
- package/package.json +21 -4
- package/src/browsers.js +128 -0
- package/src/budgets.js +77 -0
- package/src/builder.js +612 -165
- package/src/compress.js +44 -0
- package/src/config.js +280 -190
- package/src/deps.js +70 -6
- package/src/files.js +19 -7
- package/src/html.js +10 -3
- package/src/i18n.js +33 -0
- package/src/images.js +61 -0
- package/src/js.js +123 -74
- package/src/jsimports.js +54 -0
- package/src/jsinfo.js +81 -6
- package/src/obfuscate.js +8 -4
- package/src/optional.js +7 -3
- package/src/output.js +37 -9
- package/src/plugins.js +126 -0
- package/src/pool.js +343 -0
- package/src/report.js +23 -22
- package/src/server.js +8 -4
- package/src/tersercore.js +33 -0
- package/src/urls.js +3 -0
- package/src/util.js +5 -1
- package/src/verify.js +37 -27
- package/src/worker.js +30 -0
package/bin/builder.js
CHANGED
|
@@ -8,21 +8,32 @@ import { runPipeline, VERSION } from '../src/builder.js';
|
|
|
8
8
|
import { renderReport } from '../src/report.js';
|
|
9
9
|
import { startServer } from '../src/server.js';
|
|
10
10
|
import { watchTree } from '../src/watch.js';
|
|
11
|
-
import { BuildError, formatBytes, relFrom, isInside } from '../src/util.js';
|
|
11
|
+
import { BuildError, formatBytes, relFrom, isInside, JS_EXT, extOf, toPosix } from '../src/util.js';
|
|
12
|
+
import { globMatcher } from '../src/files.js';
|
|
13
|
+
import { L, setLang } from '../src/i18n.js';
|
|
12
14
|
|
|
13
15
|
const tty = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
14
16
|
const color = (code) => (text) => (tty ? `\x1b[${code}m${text}\x1b[0m` : String(text));
|
|
15
17
|
const C = { red: color(31), yellow: color(33), green: color(32), blue: color(34), gray: color(90), bold: color(1), cyan: color(36) };
|
|
16
18
|
|
|
17
|
-
const
|
|
19
|
+
const help = () =>
|
|
20
|
+
L(
|
|
21
|
+
`Builder ${VERSION} — empaqueta, minifica y ofusca proyectos web vanilla (HTML, CSS y JS)
|
|
18
22
|
|
|
19
23
|
Uso:
|
|
20
24
|
builder build [carpeta] [opciones] Compila el proyecto en dist/ (por defecto)
|
|
21
25
|
builder analyze [carpeta] [--json] Muestra dependencias detectadas y la salida prevista, sin escribir
|
|
22
26
|
builder init [carpeta] [--force] Crea builder.config.json con lo detectado
|
|
27
|
+
builder deps [carpeta] Lista qué JS son librerías de terceros (no se ofuscan) y cuáles son tuyos
|
|
28
|
+
builder deps add|own|reset <patrón…> Marca librerías (add), tu código (own) o vuelve a lo automático (reset)
|
|
23
29
|
builder dev [carpeta] [--port N] Compila, vigila cambios y sirve dist/ con recarga automática
|
|
24
30
|
builder serve [carpeta] [--port N] Sirve la carpeta de salida para probarla
|
|
25
31
|
|
|
32
|
+
Marcar dependencias sin configuración:
|
|
33
|
+
En el HTML: <script src="js/lib.js" data-builder="dependency"></script> (o "own")
|
|
34
|
+
En el archivo: /* @builder-dependency */ al principio (o @builder-own)
|
|
35
|
+
Con deps: builder deps add "js/libs/**" (en otra carpeta: --dir <carpeta>)
|
|
36
|
+
|
|
26
37
|
Opciones de build:
|
|
27
38
|
--out <carpeta> Carpeta de salida (por defecto dist)
|
|
28
39
|
--config <archivo> Archivo de configuración (por defecto builder.config.json)
|
|
@@ -44,16 +55,100 @@ Opciones de build:
|
|
|
44
55
|
--no-merge No une scripts clásicos consecutivos
|
|
45
56
|
--unreferenced <modo> Archivos no referenciados: process | copy | skip (por defecto process)
|
|
46
57
|
--base <ruta> Ruta base del sitio para URLs absolutas (por defecto /)
|
|
58
|
+
--workers <n|auto> Hilos para ofuscar/comprimir en paralelo (0 = sin hilos; por defecto auto)
|
|
59
|
+
--no-cache No usa la caché de compilaciones anteriores
|
|
47
60
|
--no-verify Omite la verificación final (no recomendado)
|
|
48
61
|
--force Permite vaciar una carpeta de salida que no creó Builder
|
|
49
62
|
--keep-failed Conserva la carpeta temporal si la verificación falla
|
|
50
63
|
--json Resultado en JSON (para CI)
|
|
51
64
|
--verbose Muestra notas, archivos copiados y excluidos
|
|
52
65
|
--quiet Solo errores
|
|
53
|
-
|
|
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
|
+
}
|
|
54
149
|
|
|
55
150
|
function parseArgs(argv) {
|
|
56
|
-
const args = { _: [], entry: [], include: [], ignore: [], dep: [], 'not-dep': [] };
|
|
151
|
+
const args = { _: [], entry: [], include: [], ignore: [], dep: [], 'not-dep': [], budget: [] };
|
|
57
152
|
const takesValue = new Set([
|
|
58
153
|
'out',
|
|
59
154
|
'config',
|
|
@@ -68,6 +163,12 @@ function parseArgs(argv) {
|
|
|
68
163
|
'dep',
|
|
69
164
|
'not-dep',
|
|
70
165
|
'minifier',
|
|
166
|
+
'workers',
|
|
167
|
+
'dir',
|
|
168
|
+
'lang',
|
|
169
|
+
'browsers',
|
|
170
|
+
'image-quality',
|
|
171
|
+
'budget',
|
|
71
172
|
]);
|
|
72
173
|
const optionalValue = { obfuscate: ['low', 'medium', 'high', 'max'], sourcemap: ['linked', 'hidden', 'inline'], compress: ['gzip', 'brotli'] };
|
|
73
174
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -77,9 +178,20 @@ function parseArgs(argv) {
|
|
|
77
178
|
continue;
|
|
78
179
|
}
|
|
79
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
|
+
}
|
|
80
192
|
if (takesValue.has(key)) {
|
|
81
193
|
if (val === undefined) val = argv[++i];
|
|
82
|
-
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}.`));
|
|
83
195
|
if (Array.isArray(args[key])) args[key].push(val);
|
|
84
196
|
else args[key] = val;
|
|
85
197
|
} else if (optionalValue[key]) {
|
|
@@ -104,6 +216,33 @@ function overridesFrom(args) {
|
|
|
104
216
|
if (args.base) o.base = args.base;
|
|
105
217
|
if (args.verify === false) o.verify = false;
|
|
106
218
|
if (args.compress !== undefined) o.compress = args.compress;
|
|
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;
|
|
240
|
+
if (args.workers !== undefined) {
|
|
241
|
+
const n = Number(args.workers);
|
|
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).'));
|
|
244
|
+
o.workers = args.workers === 'auto' ? 'auto' : n;
|
|
245
|
+
}
|
|
107
246
|
const deps = {};
|
|
108
247
|
if (args.dep.length) deps.include = args.dep;
|
|
109
248
|
if (args['not-dep'].length) deps.exclude = args['not-dep'];
|
|
@@ -128,7 +267,12 @@ function printList(title, items, colorFn, limit = 30) {
|
|
|
128
267
|
if (!items.length) return;
|
|
129
268
|
console.log(`\n${colorFn(`${title} (${items.length})`)}`);
|
|
130
269
|
for (const i of items.slice(0, limit)) console.log(` • ${i}`);
|
|
131
|
-
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
|
+
);
|
|
132
276
|
}
|
|
133
277
|
|
|
134
278
|
function jsonResult(result) {
|
|
@@ -151,6 +295,8 @@ function jsonResult(result) {
|
|
|
151
295
|
warnings: result.warnings,
|
|
152
296
|
notes: result.notes,
|
|
153
297
|
verify: result.verify && { errors: result.verify.errors, stats: result.verify.stats },
|
|
298
|
+
perf: result.perf,
|
|
299
|
+
budgets: result.budgets,
|
|
154
300
|
};
|
|
155
301
|
}
|
|
156
302
|
|
|
@@ -163,27 +309,74 @@ function printSummary(result, args) {
|
|
|
163
309
|
console.log(`\n${C.green('✔')} ${counts} ${C.gray(`(${(result.durationMs / 1000).toFixed(2)} s)`)}`);
|
|
164
310
|
if (result.verify) {
|
|
165
311
|
const s = result.verify.stats;
|
|
166
|
-
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
|
+
);
|
|
318
|
+
}
|
|
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
|
+
);
|
|
333
|
+
const p = result.perf;
|
|
334
|
+
if (p?.tasks) {
|
|
335
|
+
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
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');
|
|
342
|
+
const all = p.cacheHits === p.tasks;
|
|
343
|
+
const detail = all
|
|
344
|
+
? p.tasks === 1
|
|
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
|
+
);
|
|
167
354
|
}
|
|
168
|
-
console.log(`${C.green('✔')} Cobertura: ${result.coverage.size}/${result.coverage.size} archivos de origen con destino conocido`);
|
|
169
|
-
if (r.compressed) console.log(`${C.green('✔')} ${r.compressed} archivos precomprimidos (.gz/.br)`);
|
|
170
355
|
if (result.obfuscated) {
|
|
171
356
|
const deps = [
|
|
172
357
|
...result.dependencies.map((d) => `${d.rel} — ${d.reason}`),
|
|
173
|
-
...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)`)),
|
|
174
359
|
];
|
|
175
|
-
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);
|
|
176
361
|
}
|
|
177
|
-
printList('Avisos', result.warnings, C.yellow);
|
|
362
|
+
printList(L('Avisos', 'Warnings'), result.warnings, C.yellow);
|
|
178
363
|
if (args.verbose) {
|
|
179
|
-
printList('Notas', result.notes, C.cyan, 200);
|
|
364
|
+
printList(L('Notas', 'Notes'), result.notes, C.cyan, 200);
|
|
180
365
|
printList(
|
|
181
|
-
'Excluidos',
|
|
366
|
+
L('Excluidos', 'Excluded'),
|
|
182
367
|
result.ignored.map((i) => `${i.rel} — ${i.reason}`),
|
|
183
368
|
C.gray,
|
|
184
369
|
200,
|
|
185
370
|
);
|
|
186
|
-
} 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
|
+
);
|
|
187
380
|
}
|
|
188
381
|
|
|
189
382
|
async function cmdBuild(dir, args) {
|
|
@@ -203,49 +396,78 @@ async function cmdAnalyze(dir, args) {
|
|
|
203
396
|
console.log(JSON.stringify(jsonResult(result), null, 2));
|
|
204
397
|
return;
|
|
205
398
|
}
|
|
206
|
-
console.log(`${C.bold(`Builder ${VERSION} — análisis`)} ${cfg.root}\n`);
|
|
207
|
-
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
|
+
);
|
|
208
406
|
if (result.pages.conservative.length)
|
|
209
407
|
console.log(`${C.bold('Modo conservador')} (fragmentos u otras codificaciones): ${result.pages.conservative.join(', ')}`);
|
|
210
|
-
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')}`));
|
|
211
409
|
const rows = [...result.coverage.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
212
410
|
const w = Math.min(50, Math.max(...rows.map(([k]) => k.length), 10));
|
|
213
411
|
for (const [rel, list] of rows) {
|
|
214
|
-
const shown = args.verbose ? list : list.filter((s) => s !==
|
|
412
|
+
const shown = args.verbose ? list : list.filter((s) => s !== `${L('copiado', 'copied')} → ${rel}`);
|
|
215
413
|
if (!shown.length) continue;
|
|
216
414
|
console.log(` ${rel.padEnd(w)} ${C.gray(shown.join(' | '))}`);
|
|
217
415
|
}
|
|
218
|
-
const copied = rows.filter(([rel, list]) => list.length === 1 && list[0] ===
|
|
219
|
-
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
|
+
);
|
|
220
426
|
const deps = [...result.dependencies.map((d) => `${d.rel} — ${d.reason}`)];
|
|
221
|
-
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);
|
|
222
428
|
if (result.workers.length)
|
|
223
429
|
printList(
|
|
224
|
-
'Workers detectados',
|
|
225
|
-
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
|
+
),
|
|
226
437
|
C.cyan,
|
|
227
438
|
);
|
|
228
439
|
printList(
|
|
229
|
-
'Referencias rotas en el código fuente',
|
|
440
|
+
L('Referencias rotas en el código fuente', 'Broken references in the source code'),
|
|
230
441
|
result.broken.map((b) => `${b.from} → ${b.url} (${b.status})`),
|
|
231
442
|
C.yellow,
|
|
232
443
|
);
|
|
233
|
-
printList('Avisos', result.warnings, C.yellow);
|
|
444
|
+
printList(L('Avisos', 'Warnings'), result.warnings, C.yellow);
|
|
234
445
|
printList(
|
|
235
|
-
'Excluidos',
|
|
446
|
+
L('Excluidos', 'Excluded'),
|
|
236
447
|
result.ignored.map((i) => `${i.rel} — ${i.reason}`),
|
|
237
448
|
C.gray,
|
|
238
449
|
args.verbose ? 500 : 15,
|
|
239
450
|
);
|
|
240
|
-
if (args.verbose) printList('Notas', result.notes, C.cyan, 500);
|
|
451
|
+
if (args.verbose) printList(L('Notas', 'Notes'), result.notes, C.cyan, 500);
|
|
241
452
|
const t = result.report.totals;
|
|
242
|
-
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
|
+
);
|
|
243
459
|
}
|
|
244
460
|
|
|
245
461
|
async function cmdInit(dir, args) {
|
|
246
462
|
const projectDir = path.resolve(dir);
|
|
247
463
|
const existing = findConfigFile(projectDir);
|
|
248
|
-
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
|
+
);
|
|
249
471
|
const cfg = await loadConfig({ dir, config: args.config, overrides: overridesFrom(args) });
|
|
250
472
|
const result = await runPipeline(cfg, { mode: 'analyze' });
|
|
251
473
|
const config = {
|
|
@@ -264,20 +486,168 @@ async function cmdInit(dir, args) {
|
|
|
264
486
|
const file = path.join(projectDir, 'builder.config.json');
|
|
265
487
|
fs.writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
|
|
266
488
|
console.log(`${C.green('✔')} Creado ${file}`);
|
|
267
|
-
console.log(
|
|
268
|
-
|
|
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
|
+
);
|
|
269
504
|
if (result.dependencies.length)
|
|
270
|
-
console.log(
|
|
271
|
-
|
|
272
|
-
|
|
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
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** builder deps [carpeta] — lista; builder deps add|own|reset <patrón…> — edita builder.config.json. */
|
|
530
|
+
async function cmdDeps(_dir, args) {
|
|
531
|
+
const sub = args._[1];
|
|
532
|
+
if (['add', 'own', 'reset'].includes(sub)) return editDeps(sub, args._.slice(2), args);
|
|
533
|
+
const dir = args.dir ?? sub ?? '.';
|
|
534
|
+
const cfg = await loadConfig({ dir, config: args.config, overrides: overridesFrom(args) });
|
|
535
|
+
const result = await runPipeline(cfg, { mode: 'analyze' });
|
|
536
|
+
const deps = new Map(result.dependencies.map((d) => [d.rel, d.reason]));
|
|
537
|
+
const own = [...result.coverage.keys()].filter((rel) => JS_EXT.has(extOf(rel)) && !deps.has(rel)).sort();
|
|
538
|
+
if (args.json) {
|
|
539
|
+
console.log(JSON.stringify({ dependencies: result.dependencies, own }, null, 2));
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
console.log(`${C.bold(`Builder ${VERSION} — ${L('dependencias', 'dependencies')}`)} ${cfg.root}`);
|
|
543
|
+
printList(
|
|
544
|
+
L('Librerías de terceros (no se ofuscan)', 'Third-party libraries (not obfuscated)'),
|
|
545
|
+
[...deps].map(([rel, why]) => `${rel} ${C.gray(`— ${why}`)}`),
|
|
546
|
+
C.cyan,
|
|
547
|
+
200,
|
|
548
|
+
);
|
|
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
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async function editDeps(sub, rawPatterns, args) {
|
|
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
|
+
);
|
|
579
|
+
const projectDir = path.resolve(args.dir ?? '.');
|
|
580
|
+
const file = args.config ? path.resolve(args.config) : (findConfigFile(projectDir) ?? path.join(projectDir, 'builder.config.json'));
|
|
581
|
+
if (!file.endsWith('.json'))
|
|
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
|
+
);
|
|
588
|
+
const config = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8').replace(/^/, '')) : {};
|
|
589
|
+
if (config.dependencies === false || config.dependencies === true || config.dependencies == null) config.dependencies = {};
|
|
590
|
+
const d = config.dependencies;
|
|
591
|
+
const include = new Set(d.include ?? []);
|
|
592
|
+
const exclude = new Set(d.exclude ?? []);
|
|
593
|
+
const patterns = rawPatterns.map((p) => toPosix(p).replace(/^\.\//, ''));
|
|
594
|
+
for (const p of patterns) {
|
|
595
|
+
if (sub === 'add') {
|
|
596
|
+
include.add(p);
|
|
597
|
+
exclude.delete(p);
|
|
598
|
+
} else if (sub === 'own') {
|
|
599
|
+
exclude.add(p);
|
|
600
|
+
include.delete(p);
|
|
601
|
+
} else {
|
|
602
|
+
include.delete(p);
|
|
603
|
+
exclude.delete(p);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
d.include = [...include];
|
|
607
|
+
d.exclude = [...exclude];
|
|
608
|
+
// Se valida antes de escribir: nunca deja un builder.config.json roto.
|
|
609
|
+
const cfg = await loadConfig({ dir: projectDir, config: args.config, overrides: { dependencies: { include: d.include, exclude: d.exclude } } });
|
|
610
|
+
const existed = fs.existsSync(file);
|
|
611
|
+
fs.writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
|
|
612
|
+
const result = await runPipeline(cfg, { mode: 'analyze' });
|
|
613
|
+
const deps = new Map(result.dependencies.map((x) => [x.rel, x.reason]));
|
|
614
|
+
const jsFiles = [...result.coverage.keys()].filter((rel) => JS_EXT.has(extOf(rel)));
|
|
615
|
+
console.log(`${C.green('✔')} ${existed ? 'Actualizado' : 'Creado'} ${file}`);
|
|
616
|
+
for (const p of patterns) {
|
|
617
|
+
const hits = jsFiles.filter(globMatcher([p]));
|
|
618
|
+
if (!hits.length) {
|
|
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
|
+
);
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
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
|
+
);
|
|
636
|
+
}
|
|
273
637
|
}
|
|
274
638
|
|
|
275
639
|
async function cmdServe(dir, args) {
|
|
276
640
|
const cfg = await loadConfig({ dir, config: args.config, overrides: overridesFrom(args) });
|
|
277
641
|
const target = fs.existsSync(cfg.outDir) ? cfg.outDir : null;
|
|
278
|
-
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.`));
|
|
279
644
|
const srv = await startServer({ dir: target, port: Number(args.port ?? 5173), host: args.host ?? '127.0.0.1' });
|
|
280
|
-
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
|
+
);
|
|
281
651
|
if (args.open) openBrowser(srv.url);
|
|
282
652
|
}
|
|
283
653
|
|
|
@@ -286,9 +656,12 @@ async function cmdDev(dir, args) {
|
|
|
286
656
|
let srv = null;
|
|
287
657
|
let running = false;
|
|
288
658
|
let pending = false;
|
|
289
|
-
|
|
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) => {
|
|
290
662
|
if (running) {
|
|
291
663
|
pending = true;
|
|
664
|
+
pendingFiles = files && pendingFiles ? [...pendingFiles, ...files] : null;
|
|
292
665
|
return;
|
|
293
666
|
}
|
|
294
667
|
running = true;
|
|
@@ -297,11 +670,13 @@ async function cmdDev(dir, args) {
|
|
|
297
670
|
const fresh = await loadConfig({ dir, config: args.config, overrides: overridesFrom(args) });
|
|
298
671
|
const result = await runPipeline(fresh, { mode: 'build', force: !!args.force });
|
|
299
672
|
console.log(
|
|
300
|
-
`${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)`)) : ''}`,
|
|
301
674
|
);
|
|
302
675
|
for (const w of result.warnings.slice(0, 5)) console.log(C.yellow(` • ${w}`));
|
|
303
676
|
srv?.buildOk();
|
|
304
|
-
|
|
677
|
+
const cssOnly = files?.length && !fresh.hash && files.every((f) => /\.css$/i.test(f));
|
|
678
|
+
if (cssOnly) srv?.reloadCss();
|
|
679
|
+
else srv?.reload();
|
|
305
680
|
} catch (err) {
|
|
306
681
|
console.error(C.red(`✖ ${err.message}`));
|
|
307
682
|
for (const d of err.details ?? []) console.error(C.red(` • ${d}`));
|
|
@@ -310,14 +685,21 @@ async function cmdDev(dir, args) {
|
|
|
310
685
|
running = false;
|
|
311
686
|
if (pending) {
|
|
312
687
|
pending = false;
|
|
313
|
-
|
|
688
|
+
const next = pendingFiles;
|
|
689
|
+
pendingFiles = [];
|
|
690
|
+
rebuild(L('cambios pendientes', 'pending changes'), next);
|
|
314
691
|
}
|
|
315
692
|
}
|
|
316
693
|
};
|
|
317
694
|
await rebuild();
|
|
318
695
|
fs.mkdirSync(cfg.outDir, { recursive: true });
|
|
319
696
|
srv = await startServer({ dir: cfg.outDir, port: Number(args.port ?? 5173), host: args.host ?? '127.0.0.1', liveReload: true });
|
|
320
|
-
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
|
+
);
|
|
321
703
|
if (args.open) openBrowser(srv.url);
|
|
322
704
|
let timer = null;
|
|
323
705
|
const changed = new Set();
|
|
@@ -332,7 +714,7 @@ async function cmdDev(dir, args) {
|
|
|
332
714
|
timer = setTimeout(() => {
|
|
333
715
|
const list = [...changed];
|
|
334
716
|
changed.clear();
|
|
335
|
-
rebuild(list.length === 1 ? list[0] : `${list.length} archivos`);
|
|
717
|
+
rebuild(list.length === 1 ? list[0] : L(`${list.length} archivos`, `${list.length} files`), list);
|
|
336
718
|
}, 150);
|
|
337
719
|
});
|
|
338
720
|
}
|
|
@@ -345,12 +727,16 @@ function openBrowser(url) {
|
|
|
345
727
|
|
|
346
728
|
async function main() {
|
|
347
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]);
|
|
348
733
|
const args = parseArgs(argv);
|
|
349
734
|
if (args.version || args.v) return console.log(VERSION);
|
|
350
735
|
const [command = 'help', dir = '.'] = args._;
|
|
351
|
-
if (args.help || args.h || command === 'help') return console.log(
|
|
352
|
-
const commands = { build: cmdBuild, analyze: cmdAnalyze, init: cmdInit, serve: cmdServe, dev: cmdDev };
|
|
353
|
-
if (!commands[command])
|
|
736
|
+
if (args.help || args.h || command === 'help') return console.log(help());
|
|
737
|
+
const commands = { build: cmdBuild, analyze: cmdAnalyze, init: cmdInit, serve: cmdServe, dev: cmdDev, deps: cmdDeps };
|
|
738
|
+
if (!commands[command])
|
|
739
|
+
throw new BuildError(L(`Comando desconocido "${command}". Usa "builder help".`, `Unknown command "${command}". Use "builder help".`));
|
|
354
740
|
await commands[command](dir, args);
|
|
355
741
|
}
|
|
356
742
|
|