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/urls.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Clasificación y resolución de referencias (URLs) a archivos del proyecto.
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
5
|
+
const DYNAMIC = /\{\{|\$\{|<%|<\?|\{%/;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Clasifica una URL tal como aparece en el código fuente.
|
|
9
|
+
* type: empty | fragment | external | dynamic | root | relative
|
|
10
|
+
*/
|
|
11
|
+
export function classifyUrl(raw) {
|
|
12
|
+
const value = String(raw ?? '').trim();
|
|
13
|
+
if (!value) return { type: 'empty', value };
|
|
14
|
+
if (value.startsWith('#')) return { type: 'fragment', value };
|
|
15
|
+
if (DYNAMIC.test(value)) return { type: 'dynamic', value };
|
|
16
|
+
if (value.startsWith('//') || value.startsWith('\\\\') || SCHEME.test(value)) return { type: 'external', value };
|
|
17
|
+
const cut = value.search(/[?#]/);
|
|
18
|
+
const rawPath = (cut === -1 ? value : value.slice(0, cut)).replace(/\\/g, '/');
|
|
19
|
+
const suffix = cut === -1 ? '' : value.slice(cut);
|
|
20
|
+
let decoded;
|
|
21
|
+
try {
|
|
22
|
+
decoded = decodeURIComponent(rawPath);
|
|
23
|
+
} catch {
|
|
24
|
+
decoded = rawPath;
|
|
25
|
+
}
|
|
26
|
+
return { type: rawPath.startsWith('/') ? 'root' : 'relative', value, path: decoded, rawPath, suffix };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resuelve una referencia a un archivo del proyecto.
|
|
31
|
+
* @param {string} raw URL encontrada
|
|
32
|
+
* @param {string} baseDir carpeta (relativa a root, POSIX) contra la que se resuelven las rutas relativas
|
|
33
|
+
* @param {{files: Map, ignoredSet?: Set, base: string}} ctx
|
|
34
|
+
* @param {{page?: boolean}} [opts] page: intenta también carpeta/index.html y ruta.html
|
|
35
|
+
* @returns {{status: 'ok'|'missing'|'ignored'|'outside'|'skip', rel?: string, info: object}}
|
|
36
|
+
*/
|
|
37
|
+
export function resolveRef(raw, baseDir, ctx, opts = {}) {
|
|
38
|
+
const info = classifyUrl(raw);
|
|
39
|
+
if (info.type !== 'root' && info.type !== 'relative') return { status: 'skip', info };
|
|
40
|
+
let rel;
|
|
41
|
+
if (info.type === 'root') {
|
|
42
|
+
const base = ctx.base || '/';
|
|
43
|
+
if (info.path.startsWith(base)) rel = info.path.slice(base.length);
|
|
44
|
+
else if (`${info.path}/` === base) rel = '';
|
|
45
|
+
else return { status: 'outside', info };
|
|
46
|
+
} else {
|
|
47
|
+
rel = path.posix.join(baseDir || '.', info.path);
|
|
48
|
+
}
|
|
49
|
+
rel = path.posix.normalize(rel);
|
|
50
|
+
if (rel === '.' || rel === './') rel = '';
|
|
51
|
+
if (rel === '..' || rel.startsWith('../')) return { status: 'outside', info };
|
|
52
|
+
const isDirLike = rel === '' || rel.endsWith('/') || info.path.endsWith('/');
|
|
53
|
+
const candidates = [];
|
|
54
|
+
if (!isDirLike) candidates.push(rel);
|
|
55
|
+
if (opts.page || isDirLike) {
|
|
56
|
+
const dir = rel.replace(/\/$/, '');
|
|
57
|
+
candidates.push(dir ? `${dir}/index.html` : 'index.html');
|
|
58
|
+
if (!isDirLike && !path.posix.extname(rel)) candidates.push(`${rel}.html`);
|
|
59
|
+
}
|
|
60
|
+
const lowerMap = ctx.lowerMap;
|
|
61
|
+
for (const c of candidates) {
|
|
62
|
+
if (ctx.files.has(c)) return { status: 'ok', rel: c, info };
|
|
63
|
+
}
|
|
64
|
+
// Convención de TypeScript: se escribe "./x.js" y el archivo es x.ts (su salida será justo x.js).
|
|
65
|
+
const ts = !isDirLike && /\.(m|c)?js$/i.test(rel) ? rel.replace(/\.(m|c)?js$/i, (_, p) => `.${p ?? ''}ts`) : null;
|
|
66
|
+
if (ts && ctx.files.has(ts)) return { status: 'ok', rel: ts, info };
|
|
67
|
+
for (const c of candidates) {
|
|
68
|
+
if (ctx.ignoredSet?.has(c)) return { status: 'ignored', rel: c, info };
|
|
69
|
+
// Diferencia solo de mayúsculas: en Windows funciona, en un servidor Linux no.
|
|
70
|
+
const ci = lowerMap?.get(c.toLowerCase());
|
|
71
|
+
if (ci) return { status: 'case', rel: ci, info };
|
|
72
|
+
}
|
|
73
|
+
return { status: 'missing', rel: candidates[0] ?? rel, info };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Divide un atributo srcset en candidatos {url, start, end} (posiciones relativas al valor). */
|
|
77
|
+
export function parseSrcset(value) {
|
|
78
|
+
const out = [];
|
|
79
|
+
let i = 0;
|
|
80
|
+
const n = value.length;
|
|
81
|
+
const isWs = (c) => c === ' ' || c === '\t' || c === '\n' || c === '\r' || c === '\f';
|
|
82
|
+
while (i < n) {
|
|
83
|
+
while (i < n && (isWs(value[i]) || value[i] === ',')) i++;
|
|
84
|
+
if (i >= n) break;
|
|
85
|
+
const start = i;
|
|
86
|
+
while (i < n && !isWs(value[i])) i++;
|
|
87
|
+
let end = i;
|
|
88
|
+
let url = value.slice(start, end);
|
|
89
|
+
let hasDescriptor = true;
|
|
90
|
+
if (url.endsWith(',')) {
|
|
91
|
+
while (url.endsWith(',')) url = url.slice(0, -1);
|
|
92
|
+
end = start + url.length;
|
|
93
|
+
hasDescriptor = false;
|
|
94
|
+
}
|
|
95
|
+
if (url) out.push({ url, start, end });
|
|
96
|
+
if (hasDescriptor) {
|
|
97
|
+
let depth = 0;
|
|
98
|
+
while (i < n) {
|
|
99
|
+
const c = value[i];
|
|
100
|
+
if (c === '(') depth++;
|
|
101
|
+
else if (c === ')') depth = Math.max(0, depth - 1);
|
|
102
|
+
else if (c === ',' && depth === 0) break;
|
|
103
|
+
i++;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
package/src/util.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Utilidades compartidas: rutas, hashes, codificación y errores.
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
export class BuildError extends Error {
|
|
7
|
+
constructor(message, details = []) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'BuildError';
|
|
10
|
+
this.details = details;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const toPosix = (p) => p.split(path.sep).join('/');
|
|
15
|
+
|
|
16
|
+
/** Ruta relativa POSIX de `abs` respecto a `root`; null si queda fuera. */
|
|
17
|
+
export function relFrom(root, abs) {
|
|
18
|
+
const rel = path.relative(root, abs);
|
|
19
|
+
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return rel === '' ? '' : null;
|
|
20
|
+
return toPosix(rel);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const isInside = (parent, child) => {
|
|
24
|
+
const rel = path.relative(parent, child);
|
|
25
|
+
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function sha256(data) {
|
|
29
|
+
return crypto.createHash('sha256').update(data).digest();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Hash corto y estable (hexadecimal) para nombres de archivo. */
|
|
33
|
+
export const shortHash = (data, length = 8) => sha256(data).toString('hex').slice(0, length);
|
|
34
|
+
|
|
35
|
+
export function sri(data, algo = 'sha384') {
|
|
36
|
+
return `${algo}-${crypto.createHash(algo).update(data).digest('base64')}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ignoreBOM: true deja el BOM en el texto para poder detectarlo y conservarlo.
|
|
40
|
+
const strictDecoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
41
|
+
|
|
42
|
+
/** Devuelve el texto si el buffer es UTF-8 válido; null si no (se copiará tal cual). */
|
|
43
|
+
export function decodeUtf8(buf) {
|
|
44
|
+
try {
|
|
45
|
+
return strictDecoder.decode(buf);
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const BOM = '';
|
|
52
|
+
export function stripBom(text) {
|
|
53
|
+
return text.charCodeAt(0) === 0xfeff ? { text: text.slice(1), bom: true } : { text, bom: false };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const extOf = (p) => path.posix.extname(p).toLowerCase();
|
|
57
|
+
|
|
58
|
+
// TypeScript se transpila al empezar y desde ahí se trata como JavaScript (sale con extensión .js).
|
|
59
|
+
export const TS_EXT = new Set(['.ts', '.mts', '.cts']);
|
|
60
|
+
export const JS_EXT = new Set(['.js', '.mjs', '.cjs', ...TS_EXT]);
|
|
61
|
+
/** Ruta de salida de un script: app.ts → app.js, x.mts → x.mjs, x.cts → x.cjs. */
|
|
62
|
+
export const jsOutPath = (rel) => rel.replace(/\.(m|c)?ts$/i, (_, p) => `.${(p ?? '').toLowerCase()}js`);
|
|
63
|
+
export const CSS_EXT = new Set(['.css']);
|
|
64
|
+
export const HTML_EXT = new Set(['.html', '.htm']);
|
|
65
|
+
export const JSON_EXT = new Set(['.json', '.webmanifest', '.map']);
|
|
66
|
+
|
|
67
|
+
/** Ruta URL relativa de un archivo de salida a otro (ambos relativos a outDir, POSIX). */
|
|
68
|
+
export function relativeUrl(fromFileRel, toFileRel) {
|
|
69
|
+
const fromDir = path.posix.dirname(fromFileRel);
|
|
70
|
+
let rel = path.posix.relative(fromDir === '.' ? '' : fromDir, toFileRel);
|
|
71
|
+
if (!rel.startsWith('.')) rel = `./${rel}`;
|
|
72
|
+
return encodePath(rel);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Prefijo relativo desde el directorio de un archivo hasta la raíz de salida ("." o "../.."). */
|
|
76
|
+
export function rootPrefix(fromFileRel) {
|
|
77
|
+
const depth = path.posix.dirname(fromFileRel) === '.' ? 0 : path.posix.dirname(fromFileRel).split('/').length;
|
|
78
|
+
return depth === 0 ? '.' : Array(depth).fill('..').join('/');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const encodePath = (p) =>
|
|
82
|
+
p
|
|
83
|
+
.split('/')
|
|
84
|
+
.map((seg) => (seg === '.' || seg === '..' ? seg : encodeURIComponent(seg)))
|
|
85
|
+
.join('/');
|
|
86
|
+
|
|
87
|
+
export function formatBytes(n) {
|
|
88
|
+
if (n < 1024) return `${n} B`;
|
|
89
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
90
|
+
return `${(n / 1024 / 1024).toFixed(2)} MB`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
94
|
+
|
|
95
|
+
/** Reintenta operaciones de disco que en Windows/OneDrive fallan por bloqueos momentáneos. */
|
|
96
|
+
export async function retryFs(fn, attempts = 12) {
|
|
97
|
+
for (let i = 0; ; i++) {
|
|
98
|
+
try {
|
|
99
|
+
return await fn();
|
|
100
|
+
} catch (err) {
|
|
101
|
+
if (i >= attempts - 1 || !['EPERM', 'EBUSY', 'EACCES', 'ENOTEMPTY', 'EEXIST'].includes(err.code)) throw err;
|
|
102
|
+
await sleep(50 * (i + 1));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const unique = (arr) => [...new Set(arr)];
|
|
108
|
+
|
|
109
|
+
// Compatibilidad con Node 16 (Object.hasOwn llegó en 16.9; fs.cp en 16.7 como experimental).
|
|
110
|
+
export const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
|
|
111
|
+
|
|
112
|
+
/** Copia recursiva de carpetas. */
|
|
113
|
+
export async function copyDir(src, dest) {
|
|
114
|
+
await fs.promises.mkdir(dest, { recursive: true });
|
|
115
|
+
for (const entry of await fs.promises.readdir(src, { withFileTypes: true })) {
|
|
116
|
+
const from = path.join(src, entry.name);
|
|
117
|
+
const to = path.join(dest, entry.name);
|
|
118
|
+
if (entry.isDirectory()) await copyDir(from, to);
|
|
119
|
+
else await retryFs(() => fs.promises.copyFile(from, to));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Lista recursiva de archivos (rutas relativas POSIX). */
|
|
124
|
+
export function listFiles(dir, base = dir) {
|
|
125
|
+
const out = [];
|
|
126
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
127
|
+
const abs = path.join(dir, entry.name);
|
|
128
|
+
if (entry.isDirectory()) out.push(...listFiles(abs, base));
|
|
129
|
+
else out.push(toPosix(path.relative(base, abs)));
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Quita espacios fuera de las cadenas de un JSON válido. No toca ningún valor (sin pérdida). */
|
|
135
|
+
export function minifyJsonText(text) {
|
|
136
|
+
try {
|
|
137
|
+
JSON.parse(text);
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
let out = '';
|
|
142
|
+
let inStr = false;
|
|
143
|
+
for (let i = 0; i < text.length; i++) {
|
|
144
|
+
const c = text[i];
|
|
145
|
+
if (inStr) {
|
|
146
|
+
out += c;
|
|
147
|
+
if (c === '\\') out += text[++i];
|
|
148
|
+
else if (c === '"') inStr = false;
|
|
149
|
+
} else if (c === '"') {
|
|
150
|
+
inStr = true;
|
|
151
|
+
out += c;
|
|
152
|
+
} else if (c !== ' ' && c !== '\t' && c !== '\n' && c !== '\r') out += c;
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
package/src/verify.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Verificación de la salida antes de publicarla: referencias, sintaxis, escritura y cobertura.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { jsImports } from './jsimports.js';
|
|
5
|
+
import { mapLimit } from './compress.js';
|
|
6
|
+
import { parseHtml } from './html.js';
|
|
7
|
+
import { classifyUrl, parseSrcset } from './urls.js';
|
|
8
|
+
|
|
9
|
+
function unescapeCss(s) {
|
|
10
|
+
return s.replace(/\\([0-9a-f]{1,6}[ \t\n]?|[\s\S])/gi, (_, e) => (/^[0-9a-f]/i.test(e) ? String.fromCodePoint(parseInt(e, 16)) : e));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function stringEnd(css, i) {
|
|
14
|
+
const q = css[i];
|
|
15
|
+
let j = i + 1;
|
|
16
|
+
while (j < css.length && css[j] !== q) j += css[j] === '\\' ? 2 : 1;
|
|
17
|
+
return j;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** URLs de url() e @import de una hoja de estilos. */
|
|
21
|
+
export function cssRefs(css) {
|
|
22
|
+
const refs = [];
|
|
23
|
+
let i = 0;
|
|
24
|
+
while (i < css.length) {
|
|
25
|
+
const c = css[i];
|
|
26
|
+
if (c === '/' && css[i + 1] === '*') {
|
|
27
|
+
const e = css.indexOf('*/', i + 2);
|
|
28
|
+
i = e < 0 ? css.length : e + 2;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (c === '"' || c === "'") {
|
|
32
|
+
const end = stringEnd(css, i);
|
|
33
|
+
if (/@import\s*$/i.test(css.slice(Math.max(0, i - 12), i))) refs.push(unescapeCss(css.slice(i + 1, end)));
|
|
34
|
+
i = end + 1;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if ((c === 'u' || c === 'U') && /^url\(/i.test(css.slice(i, i + 4)) && !/[\w-]/.test(css[i - 1] ?? '')) {
|
|
38
|
+
let j = i + 4;
|
|
39
|
+
while (/\s/.test(css[j] ?? '')) j++;
|
|
40
|
+
if (css[j] === '"' || css[j] === "'") {
|
|
41
|
+
const end = stringEnd(css, j);
|
|
42
|
+
refs.push(unescapeCss(css.slice(j + 1, end)));
|
|
43
|
+
i = end + 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
let close = j;
|
|
47
|
+
while (close < css.length && css[close] !== ')') close += css[close] === '\\' ? 2 : 1;
|
|
48
|
+
refs.push(unescapeCss(css.slice(j, close).trim()));
|
|
49
|
+
i = close + 1;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
i++;
|
|
53
|
+
}
|
|
54
|
+
return refs;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {string} staging carpeta temporal escrita
|
|
59
|
+
* @param {Map<string, object>} outputs archivos generados (rel → {data|copyFrom, type, size})
|
|
60
|
+
* @param {object} ctx contexto del builder (base, referencias rotas en el origen, cobertura)
|
|
61
|
+
*/
|
|
62
|
+
export async function verifyOutput(staging, outputs, ctx) {
|
|
63
|
+
const errors = [];
|
|
64
|
+
const warnings = [];
|
|
65
|
+
const stats = { files: 0, html: 0, css: 0, js: 0, refs: 0 };
|
|
66
|
+
const lower = new Map([...outputs.keys()].map((k) => [k.toLowerCase(), k]));
|
|
67
|
+
const exists = (rel) => outputs.has(rel);
|
|
68
|
+
const check = (fromRel, url, baseDir, { page = false, srcset = false } = {}) => {
|
|
69
|
+
const urls = srcset ? parseSrcset(url).map((c) => c.url) : [url];
|
|
70
|
+
for (const u of urls) {
|
|
71
|
+
const info = classifyUrl(u);
|
|
72
|
+
if (info.type !== 'root' && info.type !== 'relative') continue;
|
|
73
|
+
if (ctx.brokenValues.has(u)) continue; // ya estaba rota en el código fuente
|
|
74
|
+
stats.refs++;
|
|
75
|
+
let rel;
|
|
76
|
+
if (info.type === 'root') {
|
|
77
|
+
if (!info.path.startsWith(ctx.cfg.base)) continue;
|
|
78
|
+
rel = info.path.slice(ctx.cfg.base.length);
|
|
79
|
+
} else rel = path.posix.normalize(path.posix.join(baseDir || '.', info.path));
|
|
80
|
+
if (rel === '.' || rel === './') rel = '';
|
|
81
|
+
if (rel === '..' || rel.startsWith('../')) continue; // fuera del sitio: no se puede comprobar
|
|
82
|
+
const candidates = rel === '' || rel.endsWith('/') ? [`${rel}index.html`] : page ? [rel, `${rel}/index.html`, `${rel}.html`] : [rel];
|
|
83
|
+
if (candidates.some(exists)) continue;
|
|
84
|
+
const ci = candidates.map((c) => lower.get(c.toLowerCase())).find(Boolean);
|
|
85
|
+
errors.push(`${fromRel}: la referencia "${u}" no existe en la salida${ci ? ` (existe "${ci}" con otras mayúsculas)` : ''}.`);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Parseo de los JS generados: en paralelo (hilos) y con caché por contenido; el resultado es el mismo.
|
|
90
|
+
const parse = (code, isModule) => (ctx.heavy ? ctx.heavy.imports(code, isModule) : jsImports(code, isModule));
|
|
91
|
+
const jsList = [...outputs].filter(([, o]) => o.type === 'js' && !o.copyFrom);
|
|
92
|
+
const parsed = new Map(await mapLimit(jsList, 64, async ([rel, o]) => [rel, await parse(o.data.toString('utf8'), !!o.module)]));
|
|
93
|
+
|
|
94
|
+
for (const [rel, o] of outputs) {
|
|
95
|
+
stats.files++;
|
|
96
|
+
const dest = path.join(staging, rel);
|
|
97
|
+
let st;
|
|
98
|
+
try {
|
|
99
|
+
st = fs.statSync(dest);
|
|
100
|
+
} catch {
|
|
101
|
+
errors.push(`${rel}: no se escribió en disco.`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const expected = o.copyFrom ? o.size : Buffer.byteLength(o.data);
|
|
105
|
+
if (st.size !== expected) errors.push(`${rel}: tamaño en disco ${st.size} ≠ esperado ${expected}.`);
|
|
106
|
+
if (o.copyFrom) continue;
|
|
107
|
+
const dir = path.posix.dirname(rel) === '.' ? '' : path.posix.dirname(rel);
|
|
108
|
+
if (o.type === 'html') {
|
|
109
|
+
stats.html++;
|
|
110
|
+
const text = o.encoding === 'latin1' ? o.data.toString('latin1') : o.data.toString('utf8');
|
|
111
|
+
const model = parseHtml(text);
|
|
112
|
+
const baseDir = o.baseDir ?? dir;
|
|
113
|
+
if (o.baseExternal) continue;
|
|
114
|
+
for (const s of model.scripts) {
|
|
115
|
+
if (s.src) check(rel, s.src.value, baseDir);
|
|
116
|
+
if (s.inline && s.kind === 'module') {
|
|
117
|
+
const r = jsImports(s.inline.code, true);
|
|
118
|
+
if (r.error) errors.push(`${rel}: un <script type="module"> inline no es JS válido (${r.error}).`);
|
|
119
|
+
else for (const spec of r.imports) if (/^\.{0,2}\//.test(spec)) check(rel, spec, baseDir);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for (const l of model.links) if (l.type === 'stylesheet' || l.type === 'asset' || l.type === 'preload') check(rel, l.href.value, baseDir);
|
|
123
|
+
for (const r of model.refs) if (!r.soft) check(rel, r.value, baseDir, { page: r.kind === 'page', srcset: r.kind === 'srcset' });
|
|
124
|
+
} else if (o.type === 'css') {
|
|
125
|
+
stats.css++;
|
|
126
|
+
for (const u of cssRefs(o.data.toString('utf8'))) check(rel, u, dir);
|
|
127
|
+
} else if (o.type === 'js') {
|
|
128
|
+
stats.js++;
|
|
129
|
+
const r = parsed.get(rel);
|
|
130
|
+
if (r.error) errors.push(`${rel}: el JavaScript generado no es válido (${r.error}).`);
|
|
131
|
+
else for (const spec of r.imports) if (/^\.{0,2}\//.test(spec)) check(rel, spec, dir);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const missing = [...ctx.files.keys()].filter((rel) => !ctx.coverage.has(rel));
|
|
136
|
+
for (const rel of missing) errors.push(`${rel}: Builder no registró qué hizo con este archivo (no se publicaría).`);
|
|
137
|
+
return { errors, warnings, stats };
|
|
138
|
+
}
|
package/src/watch.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Vigila una carpeta y sus subcarpetas. fs.watch recursivo no existe en Linux antes de Node 20:
|
|
2
|
+
// en ese caso se vigila cada carpeta por separado y se añaden las nuevas al vuelo.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} root
|
|
8
|
+
* @param {(rel: string) => boolean} ignore recibe la ruta relativa (POSIX) y devuelve true para ignorarla
|
|
9
|
+
* @param {(rel: string) => void} onChange
|
|
10
|
+
* @returns {() => void} función para dejar de vigilar
|
|
11
|
+
*/
|
|
12
|
+
export function watchTree(root, ignore, onChange) {
|
|
13
|
+
const emit = (abs) => {
|
|
14
|
+
const rel = path.relative(root, abs).split(path.sep).join('/');
|
|
15
|
+
if (rel && !rel.startsWith('..') && !ignore(rel)) onChange(rel);
|
|
16
|
+
};
|
|
17
|
+
try {
|
|
18
|
+
const w = fs.watch(root, { recursive: true }, (_e, name) => name && emit(path.join(root, name.toString())));
|
|
19
|
+
return () => w.close();
|
|
20
|
+
} catch (err) {
|
|
21
|
+
if (err.code !== 'ERR_FEATURE_UNAVAILABLE_ON_PLATFORM') throw err;
|
|
22
|
+
}
|
|
23
|
+
const watchers = new Map();
|
|
24
|
+
const add = (dir) => {
|
|
25
|
+
if (watchers.has(dir)) return;
|
|
26
|
+
const rel = path.relative(root, dir).split(path.sep).join('/');
|
|
27
|
+
if (rel && ignore(rel)) return;
|
|
28
|
+
try {
|
|
29
|
+
watchers.set(
|
|
30
|
+
dir,
|
|
31
|
+
fs.watch(dir, (_e, name) => {
|
|
32
|
+
if (!name) return;
|
|
33
|
+
const abs = path.join(dir, name.toString());
|
|
34
|
+
try {
|
|
35
|
+
if (fs.statSync(abs).isDirectory()) addTree(abs);
|
|
36
|
+
} catch {
|
|
37
|
+
/* borrado */
|
|
38
|
+
}
|
|
39
|
+
emit(abs);
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
} catch {
|
|
43
|
+
/* sin permiso o borrada */
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const addTree = (dir) => {
|
|
47
|
+
add(dir);
|
|
48
|
+
let entries = [];
|
|
49
|
+
try {
|
|
50
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
51
|
+
} catch {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
for (const e of entries) if (e.isDirectory()) addTree(path.join(dir, e.name));
|
|
55
|
+
};
|
|
56
|
+
addTree(root);
|
|
57
|
+
return () => {
|
|
58
|
+
for (const w of watchers.values()) w.close();
|
|
59
|
+
};
|
|
60
|
+
}
|
package/src/worker.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Hilo de trabajo: ofusca o comprime con terser fuera del hilo principal.
|
|
2
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
3
|
+
import { ensureObfuscator, obfuscateSafe } from './obfuscate.js';
|
|
4
|
+
import { importOptional } from './optional.js';
|
|
5
|
+
import { terserMinify } from './tersercore.js';
|
|
6
|
+
import { jsImports } from './jsimports.js';
|
|
7
|
+
|
|
8
|
+
let terser = null;
|
|
9
|
+
|
|
10
|
+
parentPort.on('message', async ({ id, kind, payload }) => {
|
|
11
|
+
try {
|
|
12
|
+
let result;
|
|
13
|
+
if (kind === 'obfuscate') {
|
|
14
|
+
await ensureObfuscator(workerData.root);
|
|
15
|
+
result = obfuscateSafe(payload.code, payload.opts);
|
|
16
|
+
} else if (kind === 'terser') {
|
|
17
|
+
terser ??= await importOptional('terser', workerData.root, 'para la compresión máxima');
|
|
18
|
+
result = await terserMinify(terser, payload.code, payload.opts);
|
|
19
|
+
} else if (kind === 'imports') {
|
|
20
|
+
result = jsImports(payload.code, payload.opts.isModule);
|
|
21
|
+
} else throw new Error(`tarea desconocida: ${kind}`);
|
|
22
|
+
parentPort.postMessage({ id, result });
|
|
23
|
+
} catch (err) {
|
|
24
|
+
parentPort.postMessage({ id, error: String(err?.message ?? err) });
|
|
25
|
+
}
|
|
26
|
+
});
|