vanilla-builder 0.1.0 → 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 +20 -0
- package/README.es.md +76 -7
- package/README.md +76 -7
- package/bin/builder.js +106 -2
- package/package.json +7 -3
- package/src/builder.js +272 -89
- package/src/compress.js +44 -0
- package/src/config.js +197 -190
- package/src/deps.js +60 -1
- package/src/files.js +7 -0
- package/src/js.js +26 -34
- package/src/jsimports.js +54 -0
- package/src/jsinfo.js +19 -3
- package/src/pool.js +341 -0
- package/src/report.js +14 -19
- package/src/tersercore.js +33 -0
- package/src/urls.js +3 -0
- package/src/util.js +5 -1
- package/src/verify.js +13 -23
- 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
CHANGED
|
@@ -1,190 +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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
for (const k of ['include', '
|
|
98
|
-
cfg
|
|
99
|
-
if (cfg
|
|
100
|
-
}
|
|
101
|
-
for (const k of ['
|
|
102
|
-
|
|
103
|
-
cfg.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
cfg.
|
|
117
|
-
if (cfg.
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
if (
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
cfg
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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/deps.js
CHANGED
|
@@ -116,16 +116,75 @@ const KNOWN = new RegExp(
|
|
|
116
116
|
'i',
|
|
117
117
|
);
|
|
118
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
|
+
|
|
119
173
|
/**
|
|
120
174
|
* ¿Parece una librería de terceros? Devuelve el motivo o null.
|
|
121
175
|
* @param {string} rel ruta relativa (POSIX)
|
|
122
176
|
* @param {string} code contenido
|
|
177
|
+
* @param {{packages?: Map<string, string>}} [hints] nombres de package.json del proyecto
|
|
123
178
|
*/
|
|
124
|
-
export function detectDependency(rel, code = '') {
|
|
179
|
+
export function detectDependency(rel, code = '', hints = {}) {
|
|
125
180
|
if (DEP_DIRS.test(rel)) return `está en ${rel.match(DEP_DIRS)[2]}/`;
|
|
126
181
|
const name = rel.split('/').pop();
|
|
127
182
|
if (KNOWN.test(name)) return 'nombre de librería conocida';
|
|
128
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)';
|
|
129
188
|
// Cabecera de librería publicada: /*! o @license con número de versión (jQuery v3.7.1, Bootstrap v5.3.0...).
|
|
130
189
|
// Una cabecera propia sin versión ("/*! Mi app (c) 2026 */") no basta.
|
|
131
190
|
const header = /^\s*\/\*[\s\S]*?\*\//.exec(code.slice(0, 3000))?.[0] ?? '';
|
package/src/files.js
CHANGED
|
@@ -33,9 +33,16 @@ export const DEFAULT_IGNORE = [
|
|
|
33
33
|
'**/eslint.config.*',
|
|
34
34
|
'**/prettier.config.*',
|
|
35
35
|
'tsconfig.json',
|
|
36
|
+
'tsconfig.*.json',
|
|
36
37
|
'jsconfig.json',
|
|
38
|
+
// Declaraciones de tipos: no contienen código ejecutable.
|
|
39
|
+
'**/*.d.ts',
|
|
40
|
+
'**/*.d.mts',
|
|
41
|
+
'**/*.d.cts',
|
|
37
42
|
'**/*.test.js',
|
|
38
43
|
'**/*.spec.js',
|
|
44
|
+
'**/*.test.ts',
|
|
45
|
+
'**/*.spec.ts',
|
|
39
46
|
'**/__tests__/**',
|
|
40
47
|
'**/*.map',
|
|
41
48
|
];
|