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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vanilla-builder",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Zero-config bundler, minifier and obfuscator for vanilla HTML, CSS and JavaScript sites. Detects every dependency, verifies the output and never loses a file.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"test:browser": "node --test test/browser.test.js",
|
|
25
25
|
"test:node16": "node test/unit.test.js && node test/build.test.js",
|
|
26
26
|
"check": "node --test test/unit.test.js test/build.test.js test/browser.test.js",
|
|
27
|
-
"prepublishOnly": "npm test"
|
|
27
|
+
"prepublishOnly": "npm test",
|
|
28
|
+
"bench": "node bench/run.mjs"
|
|
28
29
|
},
|
|
29
30
|
"keywords": [
|
|
30
31
|
"bundler",
|
|
@@ -36,7 +37,13 @@
|
|
|
36
37
|
"css",
|
|
37
38
|
"javascript",
|
|
38
39
|
"build",
|
|
39
|
-
"esbuild"
|
|
40
|
+
"esbuild",
|
|
41
|
+
"typescript",
|
|
42
|
+
"obfuscation",
|
|
43
|
+
"minifier",
|
|
44
|
+
"browserslist",
|
|
45
|
+
"image-optimization",
|
|
46
|
+
"github-action"
|
|
40
47
|
],
|
|
41
48
|
"author": "MarioACo",
|
|
42
49
|
"license": "MIT",
|
|
@@ -59,8 +66,10 @@
|
|
|
59
66
|
"picomatch": "4.0.7"
|
|
60
67
|
},
|
|
61
68
|
"peerDependencies": {
|
|
69
|
+
"browserslist": "^4.21.0",
|
|
62
70
|
"javascript-obfuscator": ">=4.1.1 <6",
|
|
63
|
-
"terser": "^5.31.0"
|
|
71
|
+
"terser": "^5.31.0",
|
|
72
|
+
"sharp": ">=0.33.0"
|
|
64
73
|
},
|
|
65
74
|
"peerDependenciesMeta": {
|
|
66
75
|
"javascript-obfuscator": {
|
|
@@ -68,11 +77,19 @@
|
|
|
68
77
|
},
|
|
69
78
|
"terser": {
|
|
70
79
|
"optional": true
|
|
80
|
+
},
|
|
81
|
+
"browserslist": {
|
|
82
|
+
"optional": true
|
|
83
|
+
},
|
|
84
|
+
"sharp": {
|
|
85
|
+
"optional": true
|
|
71
86
|
}
|
|
72
87
|
},
|
|
73
88
|
"devDependencies": {
|
|
89
|
+
"browserslist": "4.29.4",
|
|
74
90
|
"javascript-obfuscator": "5.9.0",
|
|
75
91
|
"playwright-core": "1.63.0",
|
|
92
|
+
"sharp": "0.35.5",
|
|
76
93
|
"terser": "5.51.2"
|
|
77
94
|
},
|
|
78
95
|
"overrides": {
|
package/src/browsers.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Navegadores objetivo: lee la consulta de browserslist del proyecto y la convierte en "targets" de esbuild.
|
|
2
|
+
// Con ellos esbuild añade los prefijos CSS necesarios (-webkit-…) y adapta la sintaxis moderna de CSS y JS.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { importOptional } from './optional.js';
|
|
6
|
+
import { BuildError } from './util.js';
|
|
7
|
+
import { L } from './i18n.js';
|
|
8
|
+
|
|
9
|
+
// Nombres de browserslist → motores de esbuild. Los navegadores móviles usan el motor de su versión de escritorio.
|
|
10
|
+
const ENGINES = {
|
|
11
|
+
chrome: 'chrome',
|
|
12
|
+
and_chr: 'chrome',
|
|
13
|
+
edge: 'edge',
|
|
14
|
+
firefox: 'firefox',
|
|
15
|
+
and_ff: 'firefox',
|
|
16
|
+
safari: 'safari',
|
|
17
|
+
ios_saf: 'ios',
|
|
18
|
+
opera: 'opera',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** .browserslistrc: las líneas sin sección, o la sección [production] si existe (una compilación es para producción). */
|
|
22
|
+
function readRc(file) {
|
|
23
|
+
const sections = new Map([['', []]]);
|
|
24
|
+
let current = '';
|
|
25
|
+
for (const raw of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
|
|
26
|
+
const line = raw.replace(/#.*/, '').trim();
|
|
27
|
+
if (!line) continue;
|
|
28
|
+
const header = /^\[(.+)\]$/.exec(line);
|
|
29
|
+
if (header) {
|
|
30
|
+
// Una cabecera puede nombrar varios entornos: [production staging]
|
|
31
|
+
current = header[1].split(/\s+/).includes('production') ? 'production' : header[1];
|
|
32
|
+
if (!sections.has(current)) sections.set(current, []);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
sections.get(current).push(line);
|
|
36
|
+
}
|
|
37
|
+
const chosen = sections.get('production')?.length ? sections.get('production') : sections.get('');
|
|
38
|
+
return chosen.join(', ');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function fromPackageField(value) {
|
|
42
|
+
if (typeof value === 'string' || Array.isArray(value)) return value;
|
|
43
|
+
if (value && typeof value === 'object') return value.production ?? value.defaults ?? null;
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Busca la consulta de navegadores: la opción "browsers", un .browserslistrc o el campo "browserslist"
|
|
49
|
+
* del package.json más cercano (sin salir del proyecto que contiene ese package.json).
|
|
50
|
+
* @returns {{query: string|string[], from: string}|null}
|
|
51
|
+
*/
|
|
52
|
+
export function findBrowsersQuery(root, setting) {
|
|
53
|
+
if (setting === false) return null;
|
|
54
|
+
if ((typeof setting === 'string' && setting !== 'auto') || Array.isArray(setting)) return { query: setting, from: 'browsers' };
|
|
55
|
+
for (let dir = root, i = 0; i < 20; i++) {
|
|
56
|
+
const rc = path.join(dir, '.browserslistrc');
|
|
57
|
+
if (fs.existsSync(rc)) return { query: readRc(rc), from: path.relative(root, rc) || '.browserslistrc' };
|
|
58
|
+
const pkgFile = path.join(dir, 'package.json');
|
|
59
|
+
if (fs.existsSync(pkgFile)) {
|
|
60
|
+
try {
|
|
61
|
+
const query = fromPackageField(JSON.parse(fs.readFileSync(pkgFile, 'utf8')).browserslist);
|
|
62
|
+
if (query) return { query, from: `${path.relative(root, pkgFile) || 'package.json'} (browserslist)` };
|
|
63
|
+
} catch {
|
|
64
|
+
// package.json ilegible: sin pistas
|
|
65
|
+
}
|
|
66
|
+
return null; // el package.json marca el límite del proyecto
|
|
67
|
+
}
|
|
68
|
+
const parent = path.dirname(dir);
|
|
69
|
+
if (parent === dir) break;
|
|
70
|
+
dir = parent;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const cmpVersion = (a, b) => {
|
|
76
|
+
const pa = a.split('.').map(Number);
|
|
77
|
+
const pb = b.split('.').map(Number);
|
|
78
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
79
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
80
|
+
if (d) return d;
|
|
81
|
+
}
|
|
82
|
+
return 0;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** Convierte la lista de browserslist ("chrome 109", "ios_saf 18.5-18.7"…) en targets de esbuild (versión mínima por motor). */
|
|
86
|
+
export function toEsbuildTargets(list) {
|
|
87
|
+
const min = new Map();
|
|
88
|
+
const ignored = new Set();
|
|
89
|
+
for (const item of list) {
|
|
90
|
+
const [name, version = ''] = item.split(' ');
|
|
91
|
+
const engine = ENGINES[name];
|
|
92
|
+
const v = version.split('-')[0];
|
|
93
|
+
if (!engine || !/^\d+(\.\d+)*$/.test(v)) {
|
|
94
|
+
ignored.add(name);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (!min.has(engine) || cmpVersion(v, min.get(engine)) < 0) min.set(engine, v);
|
|
98
|
+
}
|
|
99
|
+
return { targets: [...min].map(([engine, v]) => `${engine}${v}`).sort(), ignored: [...ignored].sort() };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Resuelve los navegadores del proyecto.
|
|
104
|
+
* @returns {Promise<null | {targets: string[], from: string, ignored: string[]} | {missing: true, from: string}>}
|
|
105
|
+
*/
|
|
106
|
+
export async function resolveBrowsers(root, setting) {
|
|
107
|
+
const found = findBrowsersQuery(root, setting);
|
|
108
|
+
if (!found) return null;
|
|
109
|
+
let browserslist;
|
|
110
|
+
try {
|
|
111
|
+
const mod = await importOptional('browserslist', root, L('para usar los navegadores objetivo', 'to use target browsers'));
|
|
112
|
+
browserslist = mod.default ?? mod;
|
|
113
|
+
} catch (err) {
|
|
114
|
+
// Pedido a propósito en la configuración: es un error. Encontrado en el proyecto: se avisa y se sigue como antes.
|
|
115
|
+
if (found.from === 'browsers') throw err;
|
|
116
|
+
return { missing: true, from: found.from };
|
|
117
|
+
}
|
|
118
|
+
let list;
|
|
119
|
+
try {
|
|
120
|
+
list = browserslist(found.query, { path: root });
|
|
121
|
+
} catch (err) {
|
|
122
|
+
throw new BuildError(
|
|
123
|
+
L(`Consulta de navegadores no válida en ${found.from}: ${err.message}`, `Invalid browsers query in ${found.from}: ${err.message}`),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const { targets, ignored } = toEsbuildTargets(list);
|
|
127
|
+
return { targets, from: found.from, ignored };
|
|
128
|
+
}
|
package/src/budgets.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Presupuestos de tamaño: límites por archivo de salida (por patrón) o para el total.
|
|
2
|
+
// "budgets": { "js/**/*.js": "50 KB", "css/app.css": "20 KB gzip", "total": "1 MB brotli" }
|
|
3
|
+
// Si algo se pasa, la compilación falla antes de tocar outDir (como la verificación).
|
|
4
|
+
import picomatch from 'picomatch';
|
|
5
|
+
import { formatBytes } from './util.js';
|
|
6
|
+
import { L } from './i18n.js';
|
|
7
|
+
|
|
8
|
+
const UNITS = { b: 1, kb: 1024, mb: 1024 * 1024 };
|
|
9
|
+
const SIZE = /^\s*(\d+(?:\.\d+)?)\s*(b|kb|mb)?\s*(gzip|brotli)?\s*$/i;
|
|
10
|
+
|
|
11
|
+
/** "50 KB gzip" → { bytes: 51200, measure: 'gzip' }; un número se toma en bytes. Devuelve null si no es válido. */
|
|
12
|
+
export function parseBudget(value) {
|
|
13
|
+
if (typeof value === 'number' && value > 0) return { bytes: Math.floor(value), measure: 'size' };
|
|
14
|
+
const m = typeof value === 'string' ? SIZE.exec(value) : null;
|
|
15
|
+
if (!m) return null;
|
|
16
|
+
return { bytes: Math.round(Number(m[1]) * UNITS[(m[2] ?? 'b').toLowerCase()]), measure: (m[3] ?? 'size').toLowerCase() };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Errores de validación de la opción "budgets" (lista vacía si es correcta). */
|
|
20
|
+
export function validateBudgets(budgets) {
|
|
21
|
+
if (budgets === false || budgets == null) return [];
|
|
22
|
+
if (typeof budgets !== 'object' || Array.isArray(budgets))
|
|
23
|
+
return [L('"budgets" debe ser un objeto { "patrón": "50 KB" }.', '"budgets" must be an object { "pattern": "50 KB" }.')];
|
|
24
|
+
return Object.entries(budgets)
|
|
25
|
+
.filter(([, v]) => !parseBudget(v))
|
|
26
|
+
.map(([k, v]) =>
|
|
27
|
+
L(
|
|
28
|
+
`"budgets.${k}": ${JSON.stringify(v)} no es un tamaño válido (ej. "50 KB", "20 KB gzip", "1 MB brotli").`,
|
|
29
|
+
`"budgets.${k}": ${JSON.stringify(v)} is not a valid size (e.g. "50 KB", "20 KB gzip", "1 MB brotli").`,
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const label = (m) => (m === 'size' ? '' : ` ${m}`);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Comprueba los presupuestos contra las filas del informe de tamaños.
|
|
38
|
+
* @returns {{violations: string[], unmatched: string[], checked: number}}
|
|
39
|
+
*/
|
|
40
|
+
export function checkBudgets(budgets, report) {
|
|
41
|
+
const violations = [];
|
|
42
|
+
const unmatched = [];
|
|
43
|
+
let checked = 0;
|
|
44
|
+
if (!budgets) return { violations, unmatched, checked };
|
|
45
|
+
const rows = report.rows;
|
|
46
|
+
const measureOf = (row, measure) => (measure === 'size' ? row.size : (row[measure] ?? row.size));
|
|
47
|
+
for (const [pattern, value] of Object.entries(budgets)) {
|
|
48
|
+
const b = parseBudget(value);
|
|
49
|
+
if (pattern === 'total') {
|
|
50
|
+
checked++;
|
|
51
|
+
const total = rows.reduce((sum, r) => sum + measureOf(r, b.measure), 0);
|
|
52
|
+
if (total > b.bytes)
|
|
53
|
+
violations.push(
|
|
54
|
+
L(
|
|
55
|
+
`total${label(b.measure)}: ${formatBytes(total)} supera el presupuesto de ${formatBytes(b.bytes)}`,
|
|
56
|
+
`total${label(b.measure)}: ${formatBytes(total)} exceeds the budget of ${formatBytes(b.bytes)}`,
|
|
57
|
+
),
|
|
58
|
+
);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const isMatch = picomatch(pattern.replace(/^\.\//, ''), { dot: true, nocase: true });
|
|
62
|
+
const matched = rows.filter((r) => isMatch(r.file));
|
|
63
|
+
if (!matched.length) unmatched.push(pattern);
|
|
64
|
+
for (const r of matched) {
|
|
65
|
+
checked++;
|
|
66
|
+
const size = measureOf(r, b.measure);
|
|
67
|
+
if (size > b.bytes)
|
|
68
|
+
violations.push(
|
|
69
|
+
L(
|
|
70
|
+
`${r.file}${label(b.measure)}: ${formatBytes(size)} supera el presupuesto de ${formatBytes(b.bytes)} ("${pattern}")`,
|
|
71
|
+
`${r.file}${label(b.measure)}: ${formatBytes(size)} exceeds the budget of ${formatBytes(b.bytes)} ("${pattern}")`,
|
|
72
|
+
),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { violations, unmatched, checked };
|
|
77
|
+
}
|