vanilla-builder 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.es.md +231 -0
- package/README.md +232 -2
- package/bin/builder.js +471 -0
- package/package.json +83 -4
- package/src/builder.js +1385 -0
- package/src/compress.js +44 -0
- package/src/config.js +197 -0
- package/src/css.js +166 -0
- package/src/deps.js +199 -0
- package/src/files.js +159 -0
- package/src/html.js +446 -0
- package/src/index.js +22 -0
- package/src/js.js +584 -0
- package/src/jsimports.js +54 -0
- package/src/jsinfo.js +306 -0
- package/src/obfuscate.js +160 -0
- package/src/optional.js +38 -0
- package/src/output.js +100 -0
- package/src/pool.js +341 -0
- package/src/report.js +74 -0
- package/src/server.js +124 -0
- package/src/sourcemap.js +77 -0
- package/src/tersercore.js +33 -0
- package/src/urls.js +108 -0
- package/src/util.js +155 -0
- package/src/verify.js +138 -0
- package/src/watch.js +60 -0
- package/src/worker.js +26 -0
package/src/compress.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// gzip y brotli asíncronos (en el pool de hilos de libuv) y calculados una sola vez por contenido:
|
|
2
|
+
// el informe de tamaños y los archivos .gz/.br precomprimidos comparten el mismo resultado.
|
|
3
|
+
import zlib from 'node:zlib';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
|
|
6
|
+
const gzip = promisify(zlib.gzip);
|
|
7
|
+
const brotli = promisify(zlib.brotliCompress);
|
|
8
|
+
const cache = new WeakMap();
|
|
9
|
+
|
|
10
|
+
function entry(buf) {
|
|
11
|
+
let e = cache.get(buf);
|
|
12
|
+
if (!e) cache.set(buf, (e = {}));
|
|
13
|
+
return e;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function gzipOf(buf) {
|
|
17
|
+
const e = entry(buf);
|
|
18
|
+
return (e.gzip ??= gzip(buf, { level: 9 }));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function brotliOf(buf) {
|
|
22
|
+
const e = entry(buf);
|
|
23
|
+
return (e.brotli ??= brotli(buf, {
|
|
24
|
+
params: {
|
|
25
|
+
[zlib.constants.BROTLI_PARAM_QUALITY]: 11,
|
|
26
|
+
[zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT,
|
|
27
|
+
[zlib.constants.BROTLI_PARAM_SIZE_HINT]: buf.length,
|
|
28
|
+
},
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Ejecuta fn sobre cada elemento con como mucho `limit` tareas a la vez; conserva el orden del resultado. */
|
|
33
|
+
export async function mapLimit(items, limit, fn) {
|
|
34
|
+
const out = new Array(items.length);
|
|
35
|
+
let next = 0;
|
|
36
|
+
const lane = async () => {
|
|
37
|
+
while (next < items.length) {
|
|
38
|
+
const i = next++;
|
|
39
|
+
out[i] = await fn(items[i], i);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, lane));
|
|
43
|
+
return out;
|
|
44
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// Carga, combina y valida la configuración (builder.config.json / .mjs + opciones de la CLI).
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { BuildError } from './util.js';
|
|
6
|
+
|
|
7
|
+
export const CONFIG_FILES = ['builder.config.json', 'builder.config.mjs', 'builder.config.js'];
|
|
8
|
+
|
|
9
|
+
export const DEFAULTS = {
|
|
10
|
+
root: '.',
|
|
11
|
+
outDir: 'dist',
|
|
12
|
+
entries: 'auto',
|
|
13
|
+
include: [],
|
|
14
|
+
ignore: [],
|
|
15
|
+
defaultIgnore: true,
|
|
16
|
+
unreferenced: 'process',
|
|
17
|
+
base: '/',
|
|
18
|
+
hash: false,
|
|
19
|
+
sourcemap: false,
|
|
20
|
+
clean: true,
|
|
21
|
+
verify: true,
|
|
22
|
+
sri: 'auto',
|
|
23
|
+
html: { minify: true, removeComments: true, collapseWhitespace: 'auto' },
|
|
24
|
+
css: { minify: true, bundle: true, target: 'esnext' },
|
|
25
|
+
js: {
|
|
26
|
+
minify: true,
|
|
27
|
+
bundle: true,
|
|
28
|
+
target: 'auto',
|
|
29
|
+
mergeClassic: 'auto',
|
|
30
|
+
keepNames: false,
|
|
31
|
+
drop: [],
|
|
32
|
+
legalComments: 'eof',
|
|
33
|
+
define: {},
|
|
34
|
+
chunkDir: 'assets',
|
|
35
|
+
minifier: 'esbuild',
|
|
36
|
+
},
|
|
37
|
+
json: { minify: true },
|
|
38
|
+
obfuscate: false,
|
|
39
|
+
// Librerías de terceros: se detectan solas y no se ofuscan salvo que se pida.
|
|
40
|
+
dependencies: { detect: true, obfuscate: false, include: [], exclude: [] },
|
|
41
|
+
compress: false,
|
|
42
|
+
// Caché persistente de las transformaciones lentas (ofuscación, terser) entre compilaciones.
|
|
43
|
+
cache: true,
|
|
44
|
+
// Hilos para ofuscar/comprimir en paralelo: "auto" (núcleos - 1, máx. 8), un número, o 0 (sin hilos).
|
|
45
|
+
workers: 'auto',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const NESTED = ['html', 'css', 'js', 'json', 'dependencies'];
|
|
49
|
+
const ENUMS = {
|
|
50
|
+
unreferenced: ['process', 'copy', 'skip'],
|
|
51
|
+
sri: ['auto', true, false],
|
|
52
|
+
sourcemap: [false, true, 'hidden', 'inline'],
|
|
53
|
+
'js.mergeClassic': ['auto', true, false],
|
|
54
|
+
'js.legalComments': ['none', 'inline', 'eof'],
|
|
55
|
+
'html.collapseWhitespace': ['auto', true, false],
|
|
56
|
+
'js.minifier': ['esbuild', 'terser'],
|
|
57
|
+
compress: [false, true, 'gzip', 'brotli'],
|
|
58
|
+
};
|
|
59
|
+
const OBF_PRESETS = ['low', 'medium', 'high', 'max'];
|
|
60
|
+
|
|
61
|
+
function levenshtein(a, b) {
|
|
62
|
+
const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
63
|
+
for (let j = 1; j <= b.length; j++) d[0][j] = j;
|
|
64
|
+
for (let i = 1; i <= a.length; i++)
|
|
65
|
+
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));
|
|
66
|
+
return d[a.length][b.length];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function suggest(key, options) {
|
|
70
|
+
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]}"?` : '';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function checkKeys(obj, defaults, prefix, errors) {
|
|
75
|
+
for (const key of Object.keys(obj)) {
|
|
76
|
+
if (key === '$schema') continue;
|
|
77
|
+
if (!(key in defaults)) errors.push(`Opción desconocida "${prefix}${key}".${suggest(key, Object.keys(defaults))}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const asList = (v) => (v == null ? [] : Array.isArray(v) ? v : [v]);
|
|
82
|
+
|
|
83
|
+
export function normalizeConfig(raw, errors = []) {
|
|
84
|
+
const user = raw ?? {};
|
|
85
|
+
if (typeof user !== 'object' || Array.isArray(user)) throw new BuildError('La configuración debe ser un objeto JSON.');
|
|
86
|
+
checkKeys(user, DEFAULTS, '', errors);
|
|
87
|
+
const cfg = { ...DEFAULTS, ...user };
|
|
88
|
+
for (const k of NESTED) {
|
|
89
|
+
const v = user[k];
|
|
90
|
+
if (v === false) cfg[k] = k === 'dependencies' ? { ...DEFAULTS[k], detect: false } : { ...DEFAULTS[k], minify: false };
|
|
91
|
+
else if (v === true || v == null) cfg[k] = { ...DEFAULTS[k] };
|
|
92
|
+
else if (typeof v === 'object') {
|
|
93
|
+
checkKeys(v, DEFAULTS[k], `${k}.`, errors);
|
|
94
|
+
cfg[k] = { ...DEFAULTS[k], ...v };
|
|
95
|
+
} else errors.push(`"${k}" debe ser un objeto o true/false.`);
|
|
96
|
+
}
|
|
97
|
+
for (const k of ['include', 'ignore']) {
|
|
98
|
+
cfg[k] = asList(cfg[k]);
|
|
99
|
+
if (cfg[k].some((x) => typeof x !== 'string')) errors.push(`"${k}" debe ser una lista de rutas o patrones (texto).`);
|
|
100
|
+
}
|
|
101
|
+
for (const k of ['include', 'exclude']) {
|
|
102
|
+
cfg.dependencies[k] = asList(cfg.dependencies[k]);
|
|
103
|
+
if (cfg.dependencies[k].some((x) => typeof x !== 'string')) errors.push(`"dependencies.${k}" debe ser una lista de rutas o patrones (texto).`);
|
|
104
|
+
}
|
|
105
|
+
for (const k of ['detect', 'obfuscate']) if (typeof cfg.dependencies[k] !== 'boolean') errors.push(`"dependencies.${k}" debe ser true o false.`);
|
|
106
|
+
if (cfg.entries !== 'auto') {
|
|
107
|
+
cfg.entries = asList(cfg.entries);
|
|
108
|
+
if (!cfg.entries.length || cfg.entries.some((x) => typeof x !== 'string'))
|
|
109
|
+
errors.push('"entries" debe ser "auto" o una lista de archivos/patrones.');
|
|
110
|
+
}
|
|
111
|
+
for (const [key, allowed] of Object.entries(ENUMS)) {
|
|
112
|
+
const [a, b] = key.split('.');
|
|
113
|
+
const value = b ? cfg[a][b] : cfg[a];
|
|
114
|
+
if (!allowed.includes(value)) errors.push(`"${key}" debe ser uno de: ${allowed.map((x) => JSON.stringify(x)).join(', ')}.`);
|
|
115
|
+
}
|
|
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('/')) errors.push('"base" debe empezar con "/" (ej. "/" o "/mi-sitio/").');
|
|
118
|
+
else if (!cfg.base.endsWith('/')) cfg.base += '/';
|
|
119
|
+
if (typeof cfg.hash !== 'boolean') errors.push('"hash" debe ser true o false.');
|
|
120
|
+
if (typeof cfg.cache !== 'boolean') errors.push('"cache" debe ser true o false.');
|
|
121
|
+
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.');
|
|
123
|
+
cfg.js.drop = asList(cfg.js.drop);
|
|
124
|
+
if (cfg.js.drop.some((d) => !['console', 'debugger'].includes(d))) errors.push('"js.drop" solo admite "console" y "debugger".');
|
|
125
|
+
if (typeof cfg.js.chunkDir !== 'string' || /(^|\/)\.\.(\/|$)|^\//.test(cfg.js.chunkDir))
|
|
126
|
+
errors.push('"js.chunkDir" debe ser una carpeta relativa.');
|
|
127
|
+
for (const k of ['css', 'js']) {
|
|
128
|
+
const t = cfg[k].target;
|
|
129
|
+
if (!(typeof t === 'string' || (Array.isArray(t) && t.every((x) => typeof x === 'string'))))
|
|
130
|
+
errors.push(`"${k}.target" debe ser texto o lista (ej. "es2020" o ["chrome90","safari15"]).`);
|
|
131
|
+
}
|
|
132
|
+
const o = cfg.obfuscate;
|
|
133
|
+
if (o === true) cfg.obfuscate = 'medium';
|
|
134
|
+
else if (!(o === false || OBF_PRESETS.includes(o) || (o && typeof o === 'object' && !Array.isArray(o))))
|
|
135
|
+
errors.push(`"obfuscate" debe ser false, true, ${OBF_PRESETS.map((p) => `"${p}"`).join(', ')} o un objeto de opciones de javascript-obfuscator.`);
|
|
136
|
+
return cfg;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function readConfigFile(file) {
|
|
140
|
+
if (file.endsWith('.json')) {
|
|
141
|
+
const text = fs.readFileSync(file, 'utf8').replace(/^/, '');
|
|
142
|
+
try {
|
|
143
|
+
return JSON.parse(text);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
throw new BuildError(`No pude leer ${path.basename(file)}: JSON inválido (${err.message}).`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const mod = await import(`${pathToFileURL(file).href}?t=${fs.statSync(file).mtimeMs}`);
|
|
149
|
+
return typeof mod.default === 'function' ? await mod.default() : mod.default;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function findConfigFile(dir) {
|
|
153
|
+
for (const name of CONFIG_FILES) {
|
|
154
|
+
const file = path.join(dir, name);
|
|
155
|
+
if (fs.existsSync(file)) return file;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Resuelve la configuración final.
|
|
162
|
+
* @param {{dir?: string, config?: string, overrides?: object, cwd?: string}} opts
|
|
163
|
+
*/
|
|
164
|
+
export async function loadConfig({ dir = '.', config, overrides = {}, cwd = process.cwd() } = {}) {
|
|
165
|
+
const projectDir = path.resolve(cwd, dir);
|
|
166
|
+
if (!fs.existsSync(projectDir) || !fs.statSync(projectDir).isDirectory()) throw new BuildError(`La carpeta del proyecto no existe: ${projectDir}`);
|
|
167
|
+
const configFile = config ? path.resolve(cwd, config) : findConfigFile(projectDir);
|
|
168
|
+
if (config && !fs.existsSync(configFile)) throw new BuildError(`No existe el archivo de configuración: ${configFile}`);
|
|
169
|
+
const fileCfg = configFile ? await readConfigFile(configFile) : {};
|
|
170
|
+
const merged = mergeOverrides(fileCfg ?? {}, overrides);
|
|
171
|
+
const errors = [];
|
|
172
|
+
const cfg = normalizeConfig(merged, errors);
|
|
173
|
+
if (errors.length) throw new BuildError('La configuración tiene errores:', errors);
|
|
174
|
+
const baseDir = configFile ? path.dirname(configFile) : projectDir;
|
|
175
|
+
cfg.root = path.resolve(baseDir, cfg.root);
|
|
176
|
+
if (!fs.existsSync(cfg.root)) throw new BuildError(`"root" no existe: ${cfg.root}`);
|
|
177
|
+
// esbuild resuelve los enlaces simbólicos de lo que importa; si la raíz no es la ruta real, el mismo
|
|
178
|
+
// módulo aparece con dos rutas (pasa en macOS con /tmp → /private/tmp, o al abrir el proyecto por un enlace).
|
|
179
|
+
cfg.root = fs.realpathSync(cfg.root);
|
|
180
|
+
cfg.outDir = path.resolve(cfg.root, cfg.outDir);
|
|
181
|
+
cfg.configFile = configFile;
|
|
182
|
+
return cfg;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function mergeOverrides(base, over) {
|
|
186
|
+
const out = { ...base };
|
|
187
|
+
for (const [k, v] of Object.entries(over)) {
|
|
188
|
+
if (v === undefined) continue;
|
|
189
|
+
if (NESTED.includes(k) && v && typeof v === 'object' && !Array.isArray(v)) {
|
|
190
|
+
const prev = base[k];
|
|
191
|
+
out[k] = { ...(prev && typeof prev === 'object' ? prev : prev === false ? { minify: false } : {}), ...v };
|
|
192
|
+
} else if (['include', 'ignore'].includes(k)) {
|
|
193
|
+
out[k] = [...asList(base[k]), ...asList(v)];
|
|
194
|
+
} else out[k] = v;
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
package/src/css.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// CSS: empaqueta @import, reescribe url() según la ubicación final y minifica con esbuild.
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import * as esbuild from 'esbuild';
|
|
4
|
+
import { relFrom, toPosix } from './util.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* image-set("a.png" 1x) no lo reescribe esbuild al mover el CSS de carpeta.
|
|
8
|
+
* Se convierte a la forma equivalente image-set(url("a.png") 1x), que sí reescribe.
|
|
9
|
+
*/
|
|
10
|
+
export function imageSetStringsToUrl(css) {
|
|
11
|
+
const re = /(?:-webkit-)?image-set\(/gi;
|
|
12
|
+
let out = '';
|
|
13
|
+
let last = 0;
|
|
14
|
+
let m;
|
|
15
|
+
while ((m = re.exec(css))) {
|
|
16
|
+
let i = m.index + m[0].length;
|
|
17
|
+
let depth = 1;
|
|
18
|
+
let chunk = '';
|
|
19
|
+
let segStart = i;
|
|
20
|
+
const flush = (end) => {
|
|
21
|
+
chunk += css.slice(segStart, end);
|
|
22
|
+
};
|
|
23
|
+
while (i < css.length && depth > 0) {
|
|
24
|
+
const c = css[i];
|
|
25
|
+
if (c === '"' || c === "'") {
|
|
26
|
+
let j = i + 1;
|
|
27
|
+
while (j < css.length && css[j] !== c) j += css[j] === '\\' ? 2 : 1;
|
|
28
|
+
if (depth === 1) {
|
|
29
|
+
flush(i);
|
|
30
|
+
chunk += `url(${css.slice(i, j + 1)})`;
|
|
31
|
+
segStart = j + 1;
|
|
32
|
+
}
|
|
33
|
+
i = j + 1;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (c === '(') depth++;
|
|
37
|
+
else if (c === ')') depth--;
|
|
38
|
+
i++;
|
|
39
|
+
}
|
|
40
|
+
flush(i);
|
|
41
|
+
out += css.slice(last, m.index + m[0].length) + chunk;
|
|
42
|
+
last = i;
|
|
43
|
+
re.lastIndex = i;
|
|
44
|
+
}
|
|
45
|
+
return last === 0 ? css : out + css.slice(last);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const EXTERNAL = /^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i;
|
|
49
|
+
|
|
50
|
+
function cssPlugin(ctx, { outRel, consumed, label }) {
|
|
51
|
+
return {
|
|
52
|
+
name: 'vanilla-builder-css',
|
|
53
|
+
setup(b) {
|
|
54
|
+
b.onResolve({ filter: /.*/ }, (args) => {
|
|
55
|
+
if (args.kind === 'entry-point') return undefined;
|
|
56
|
+
const spec = args.path;
|
|
57
|
+
if (EXTERNAL.test(spec) || /\{\{|\$\{/.test(spec)) return { path: spec, external: true };
|
|
58
|
+
const isStdin = !args.importer || args.importer === '<stdin>';
|
|
59
|
+
const fromRel = isStdin ? label : (relFrom(ctx.root, args.importer) ?? label);
|
|
60
|
+
const baseDir = args.resolveDir ? (relFrom(ctx.root, args.resolveDir) ?? '') : path.posix.dirname(fromRel);
|
|
61
|
+
const r = ctx.resolve(spec, baseDir);
|
|
62
|
+
if (r.status !== 'ok') {
|
|
63
|
+
ctx.reportBroken(fromRel, spec, r);
|
|
64
|
+
return { path: spec, external: true };
|
|
65
|
+
}
|
|
66
|
+
if (args.kind === 'import-rule') return { path: ctx.abs(r.rel) };
|
|
67
|
+
// url(): el archivo se copia a su ruta final y la URL se recalcula desde el CSS de salida.
|
|
68
|
+
ctx.registerAsset(r.rel, fromRel);
|
|
69
|
+
const target = ctx.assetOut(r.rel);
|
|
70
|
+
let url = path.posix.relative(path.posix.dirname(outRel), target);
|
|
71
|
+
if (!url) url = path.posix.basename(target);
|
|
72
|
+
url = url
|
|
73
|
+
.split('/')
|
|
74
|
+
.map((s) => (s === '..' ? s : encodeURIComponent(s)))
|
|
75
|
+
.join('/');
|
|
76
|
+
return { path: url + r.info.suffix, external: true };
|
|
77
|
+
});
|
|
78
|
+
b.onLoad({ filter: /\.css$/i }, (args) => {
|
|
79
|
+
const rel = relFrom(ctx.root, args.path);
|
|
80
|
+
if (rel == null) return undefined;
|
|
81
|
+
consumed.add(rel);
|
|
82
|
+
const { text } = ctx.readText(rel, { kind: 'css' });
|
|
83
|
+
return { contents: imageSetStringsToUrl(text), loader: 'css' };
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Compila una hoja de estilos.
|
|
91
|
+
* @param {object} ctx contexto del builder
|
|
92
|
+
* @param {{entry?: string, inline?: {code: string, baseDir: string, label: string}, outRel: string}} job
|
|
93
|
+
*/
|
|
94
|
+
export async function buildCss(ctx, job) {
|
|
95
|
+
const cfg = ctx.cfg;
|
|
96
|
+
const consumed = new Set();
|
|
97
|
+
const common = {
|
|
98
|
+
bundle: cfg.css.bundle,
|
|
99
|
+
write: false,
|
|
100
|
+
minify: cfg.css.minify,
|
|
101
|
+
target: cfg.css.target,
|
|
102
|
+
logLevel: 'silent',
|
|
103
|
+
absWorkingDir: ctx.root,
|
|
104
|
+
outfile: path.join(ctx.root, '__vb_out__', job.outRel),
|
|
105
|
+
plugins: [cssPlugin(ctx, { outRel: job.outRel, consumed, label: job.entry ?? job.inline?.label })],
|
|
106
|
+
legalComments: cfg.css.minify ? cfg.js.legalComments : 'inline',
|
|
107
|
+
charset: 'ascii',
|
|
108
|
+
sourcemap: job.inline
|
|
109
|
+
? false
|
|
110
|
+
: cfg.sourcemap === 'hidden'
|
|
111
|
+
? 'external'
|
|
112
|
+
: cfg.sourcemap === 'inline'
|
|
113
|
+
? 'inline'
|
|
114
|
+
: cfg.sourcemap
|
|
115
|
+
? 'linked'
|
|
116
|
+
: false,
|
|
117
|
+
};
|
|
118
|
+
let result;
|
|
119
|
+
try {
|
|
120
|
+
result = job.inline
|
|
121
|
+
? await esbuild.build({
|
|
122
|
+
...common,
|
|
123
|
+
stdin: {
|
|
124
|
+
contents: imageSetStringsToUrl(job.inline.code),
|
|
125
|
+
resolveDir: ctx.abs(job.inline.baseDir),
|
|
126
|
+
loader: 'css',
|
|
127
|
+
sourcefile: job.inline.label,
|
|
128
|
+
},
|
|
129
|
+
})
|
|
130
|
+
: await esbuild.build({ ...common, entryPoints: [ctx.abs(job.entry)] });
|
|
131
|
+
} catch (err) {
|
|
132
|
+
return { error: esbuildErrorText(err, ctx.root) };
|
|
133
|
+
}
|
|
134
|
+
for (const w of result.warnings) ctx.warn(`CSS: ${formatMessage(w, ctx.root)}`, job.entry ?? job.inline?.label);
|
|
135
|
+
let code = '';
|
|
136
|
+
let map = null;
|
|
137
|
+
for (const f of result.outputFiles) {
|
|
138
|
+
if (f.path.endsWith('.map')) map = f.text;
|
|
139
|
+
else code = f.text;
|
|
140
|
+
}
|
|
141
|
+
if (job.inline) code = code.replace(/\n$/, '');
|
|
142
|
+
return { code, map, inputs: [...consumed] };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Minifica el contenido de un atributo style="" sin tocar sus URLs. */
|
|
146
|
+
export async function minifyStyleAttr(value, target) {
|
|
147
|
+
if (!value.trim()) return value;
|
|
148
|
+
try {
|
|
149
|
+
const r = await esbuild.transform(`*{${value}}`, { loader: 'css', minify: true, target, charset: 'ascii', logLevel: 'silent' });
|
|
150
|
+
const m = /^\*\{([\s\S]*)\}\s*$/.exec(r.code);
|
|
151
|
+
if (!m || r.warnings.length) return value;
|
|
152
|
+
return m[1];
|
|
153
|
+
} catch {
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function formatMessage(m, root) {
|
|
159
|
+
const loc = m.location ? `${toPosix(path.relative(root, path.resolve(root, m.location.file)))}:${m.location.line}:${m.location.column + 1} ` : '';
|
|
160
|
+
return `${loc}${m.text}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function esbuildErrorText(err, root) {
|
|
164
|
+
if (err?.errors?.length) return err.errors.map((e) => formatMessage(e, root)).join('\n');
|
|
165
|
+
return err?.message ?? String(err);
|
|
166
|
+
}
|
package/src/deps.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// Detección de librerías de terceros (jQuery, Bootstrap, paquetes de npm...).
|
|
2
|
+
// No se ofuscan: no aporta nada, las hace más lentas y puede romper su licencia.
|
|
3
|
+
|
|
4
|
+
const DEP_DIRS = /(^|\/)(node_modules|bower_components|jspm_packages|web_modules|vendors?|third[-_]?party|external|externals)\//i;
|
|
5
|
+
const MIN_NAME = /[.-](min|prod|production)\.m?js$|\.umd(\.min)?\.m?js$/i;
|
|
6
|
+
// Nombres de librerías conocidas, con o sin versión: jquery-3.7.1.js, bootstrap.bundle.js, chart.umd.js...
|
|
7
|
+
const KNOWN = new RegExp(
|
|
8
|
+
`^(${[
|
|
9
|
+
'jquery(?:[.-]ui)?',
|
|
10
|
+
'zepto',
|
|
11
|
+
'bootstrap',
|
|
12
|
+
'popper',
|
|
13
|
+
'@popperjs',
|
|
14
|
+
'lodash',
|
|
15
|
+
'underscore',
|
|
16
|
+
'moment(?:-with-locales)?',
|
|
17
|
+
'dayjs',
|
|
18
|
+
'luxon',
|
|
19
|
+
'd3',
|
|
20
|
+
'three',
|
|
21
|
+
'vue',
|
|
22
|
+
'react',
|
|
23
|
+
'react-dom',
|
|
24
|
+
'preact',
|
|
25
|
+
'angular',
|
|
26
|
+
'alpine',
|
|
27
|
+
'alpinejs',
|
|
28
|
+
'htmx',
|
|
29
|
+
'axios',
|
|
30
|
+
'gsap',
|
|
31
|
+
'swiper',
|
|
32
|
+
'leaflet',
|
|
33
|
+
'mapbox-gl',
|
|
34
|
+
'sweetalert2?',
|
|
35
|
+
'toastr',
|
|
36
|
+
'select2',
|
|
37
|
+
'datatables',
|
|
38
|
+
'jquery\\.dataTables',
|
|
39
|
+
'fullcalendar',
|
|
40
|
+
'prism',
|
|
41
|
+
'marked',
|
|
42
|
+
'dompurify',
|
|
43
|
+
'handlebars',
|
|
44
|
+
'mustache',
|
|
45
|
+
'backbone',
|
|
46
|
+
'knockout',
|
|
47
|
+
'socket\\.io',
|
|
48
|
+
'pdf(?:\\.worker)?',
|
|
49
|
+
'tinymce',
|
|
50
|
+
'ckeditor',
|
|
51
|
+
'quill',
|
|
52
|
+
'lottie',
|
|
53
|
+
'fontawesome',
|
|
54
|
+
'modernizr',
|
|
55
|
+
'core-js',
|
|
56
|
+
'regenerator-runtime',
|
|
57
|
+
'tailwind(?:css)?',
|
|
58
|
+
'bulma',
|
|
59
|
+
'uikit',
|
|
60
|
+
'materialize',
|
|
61
|
+
'fancybox',
|
|
62
|
+
'slick',
|
|
63
|
+
'owl\\.carousel',
|
|
64
|
+
'isotope',
|
|
65
|
+
'masonry',
|
|
66
|
+
'imagesloaded',
|
|
67
|
+
'inputmask',
|
|
68
|
+
'flatpickr',
|
|
69
|
+
'pikaday',
|
|
70
|
+
'nouislider',
|
|
71
|
+
'requirejs',
|
|
72
|
+
'es-module-shims',
|
|
73
|
+
'workbox-[\\w-]+',
|
|
74
|
+
'firebase(?:-[\\w-]+)?',
|
|
75
|
+
'plyr',
|
|
76
|
+
'howler',
|
|
77
|
+
'pixi',
|
|
78
|
+
'phaser',
|
|
79
|
+
'konva',
|
|
80
|
+
'echarts',
|
|
81
|
+
'apexcharts',
|
|
82
|
+
'highcharts',
|
|
83
|
+
'plotly',
|
|
84
|
+
'mathjax',
|
|
85
|
+
'katex',
|
|
86
|
+
'mermaid',
|
|
87
|
+
'codemirror',
|
|
88
|
+
'monaco',
|
|
89
|
+
'xlsx',
|
|
90
|
+
'jszip',
|
|
91
|
+
'filesaver',
|
|
92
|
+
'jspdf',
|
|
93
|
+
'html2canvas',
|
|
94
|
+
'qrcode',
|
|
95
|
+
'crypto-js',
|
|
96
|
+
'immutable',
|
|
97
|
+
'rxjs',
|
|
98
|
+
'mobx',
|
|
99
|
+
'redux',
|
|
100
|
+
'i18next',
|
|
101
|
+
'intl-tel-input',
|
|
102
|
+
'libphonenumber',
|
|
103
|
+
'cropper',
|
|
104
|
+
'dropzone',
|
|
105
|
+
'tippy',
|
|
106
|
+
'micromodal',
|
|
107
|
+
'glightbox',
|
|
108
|
+
'splide',
|
|
109
|
+
'flickity',
|
|
110
|
+
'scrollreveal',
|
|
111
|
+
'locomotive-scroll',
|
|
112
|
+
'lenis',
|
|
113
|
+
'barba',
|
|
114
|
+
'swup',
|
|
115
|
+
].join('|')})(?:[.@_-][\\w.-]*)?\\.m?js$`,
|
|
116
|
+
'i',
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// Marca escrita por el usuario en el primer comentario del archivo.
|
|
120
|
+
const PRAGMA = /@builder-(dependency|dependencia|own|propio)\b/i;
|
|
121
|
+
// Envoltorio UMD/AMD/CommonJS: el formato con el que se publican las librerías para usarlas con <script>.
|
|
122
|
+
const UMD = [
|
|
123
|
+
/typeof\s+define\s*===?\s*["']function["']\s*&&\s*define\.amd/,
|
|
124
|
+
/typeof\s+exports\s*===?\s*["']object["']\s*&&\s*typeof\s+module\s*!==?\s*["']undefined["']/,
|
|
125
|
+
/typeof\s+module\s*===?\s*["']object["']\s*&&\s*(?:typeof\s+)?module\.exports/,
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Marca explícita en el propio archivo: un comentario inicial con @builder-dependency o @builder-own.
|
|
130
|
+
* @returns {'dependency'|'own'|null}
|
|
131
|
+
*/
|
|
132
|
+
export function dependencyPragma(code = '') {
|
|
133
|
+
const head = code.slice(0, 2000);
|
|
134
|
+
const comment = /^\s*(?:\/\*[\s\S]*?\*\/|\/\/[^\n]*)/.exec(head)?.[0] ?? '';
|
|
135
|
+
const m = PRAGMA.exec(comment);
|
|
136
|
+
if (!m) return null;
|
|
137
|
+
return /^(dependency|dependencia)$/i.test(m[1]) ? 'dependency' : 'own';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Nombre "de paquete" de un archivo: jquery-3.7.1.min.js → jquery, chart.umd.js → chart. */
|
|
141
|
+
export function packageLikeName(fileName) {
|
|
142
|
+
return fileName
|
|
143
|
+
.toLowerCase()
|
|
144
|
+
.replace(/\.(m?js|cjs)$/, '')
|
|
145
|
+
.replace(/[.-](min|prod|production|umd|esm|es|module|cjs|iife|bundle|browser|global|dist)\b/g, '')
|
|
146
|
+
.replace(/[@.-]v?\d+(\.\d+){1,3}(-[\w.]+)?$/, '')
|
|
147
|
+
.replace(/[.-](min|prod|production|umd|esm|es|module|cjs|iife|bundle|browser|global|dist)$/g, '');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Nombres de los paquetes declarados en el package.json del proyecto, normalizados
|
|
152
|
+
* (chart.js → chart, @popperjs/core → popperjs), para reconocer copias a mano en js/.
|
|
153
|
+
* @returns {Map<string, string>} nombre normalizado → nombre en package.json
|
|
154
|
+
*/
|
|
155
|
+
export function packageNames(pkg) {
|
|
156
|
+
const names = new Map();
|
|
157
|
+
// devDependencies no: suelen ser herramientas (eslint, prettier…) y darían falsos positivos.
|
|
158
|
+
for (const field of ['dependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
159
|
+
for (const full of Object.keys(pkg?.[field] ?? {})) {
|
|
160
|
+
const lower = full.toLowerCase();
|
|
161
|
+
const [scope, bare] = lower.startsWith('@') ? lower.slice(1).split('/') : [null, lower];
|
|
162
|
+
for (const n of [bare, scope]) {
|
|
163
|
+
if (!n || n.length < 3) continue;
|
|
164
|
+
for (const key of [n, n.replace(/\.js$/, '')]) if (!names.has(key)) names.set(key, full);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Nombres demasiado genéricos para concluir nada solo por el nombre del archivo.
|
|
169
|
+
for (const generic of ['core', 'utils', 'util', 'app', 'main', 'index', 'common', 'types', 'lib', 'api']) names.delete(generic);
|
|
170
|
+
return names;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* ¿Parece una librería de terceros? Devuelve el motivo o null.
|
|
175
|
+
* @param {string} rel ruta relativa (POSIX)
|
|
176
|
+
* @param {string} code contenido
|
|
177
|
+
* @param {{packages?: Map<string, string>}} [hints] nombres de package.json del proyecto
|
|
178
|
+
*/
|
|
179
|
+
export function detectDependency(rel, code = '', hints = {}) {
|
|
180
|
+
if (DEP_DIRS.test(rel)) return `está en ${rel.match(DEP_DIRS)[2]}/`;
|
|
181
|
+
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)';
|
|
184
|
+
const pkgName = packageLikeName(name);
|
|
185
|
+
if (hints.packages?.has(pkgName)) return `se llama como el paquete "${hints.packages.get(pkgName)}" de tu package.json`;
|
|
186
|
+
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)';
|
|
188
|
+
// Cabecera de librería publicada: /*! o @license con número de versión (jQuery v3.7.1, Bootstrap v5.3.0...).
|
|
189
|
+
// Una cabecera propia sin versión ("/*! Mi app (c) 2026 */") no basta.
|
|
190
|
+
const header = /^\s*\/\*[\s\S]*?\*\//.exec(code.slice(0, 3000))?.[0] ?? '';
|
|
191
|
+
if ((/^\s*\/\*!/.test(header) || /@(license|preserve)\b/.test(header)) && /\bv?\d+\.\d+(\.\d+)?\b/.test(header))
|
|
192
|
+
return 'cabecera de licencia con versión';
|
|
193
|
+
// Código ya minificado: pocas líneas muy largas.
|
|
194
|
+
if (code.length > 4000) {
|
|
195
|
+
const lines = code.split('\n').length;
|
|
196
|
+
if (code.length / lines > 500) return 'código ya minificado';
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|