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/src/jsinfo.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Análisis estático de JavaScript con acorn: nivel de sintaxis, globales, workers y rutas.
|
|
2
2
|
import * as acorn from 'acorn';
|
|
3
3
|
import * as walk from 'acorn-walk';
|
|
4
|
+
import { L } from './i18n.js';
|
|
4
5
|
|
|
5
6
|
const VERSIONS = [5, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026];
|
|
6
7
|
const LATEST = 'latest';
|
|
@@ -124,7 +125,6 @@ export function analyzeJs(code, opts = {}) {
|
|
|
124
125
|
const info = {
|
|
125
126
|
ok: true,
|
|
126
127
|
isModule,
|
|
127
|
-
level: detectLevel(code, sourceType),
|
|
128
128
|
strict,
|
|
129
129
|
top: isModule ? new Map() : globalBindings(ast, strict),
|
|
130
130
|
imports: [],
|
|
@@ -139,6 +139,12 @@ export function analyzeJs(code, opts = {}) {
|
|
|
139
139
|
hasImportMeta: false,
|
|
140
140
|
hasModuleSyntax: false,
|
|
141
141
|
};
|
|
142
|
+
// El nivel ES cuesta varios parseos: solo se calcula si alguien lo pide (la mayoría de análisis no lo necesitan).
|
|
143
|
+
let level;
|
|
144
|
+
Object.defineProperty(info, 'level', {
|
|
145
|
+
enumerable: true,
|
|
146
|
+
get: () => (level === undefined ? (level = detectLevel(code, sourceType)) : level),
|
|
147
|
+
});
|
|
142
148
|
const addWorker = (arg, kind, optsNode) => {
|
|
143
149
|
if (!arg) return;
|
|
144
150
|
let spec = null;
|
|
@@ -244,20 +250,89 @@ export function checkSyntax(code, isModule) {
|
|
|
244
250
|
}
|
|
245
251
|
}
|
|
246
252
|
|
|
253
|
+
// Nombres que esbuild crea al adaptar sintaxis a navegadores antiguos: temporales (_a, _b2) y ayudantes (__async).
|
|
254
|
+
const ESBUILD_TEMP = /^(_[a-z]\d*|__[A-Za-z]+\d*)$/;
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Al transpilar un script clásico, esbuild puede declarar temporales en el ámbito global (var _a).
|
|
258
|
+
* Si TODAS las globales nuevas son de ese tipo, se renombran en todo el archivo a `${prefix}nombre`
|
|
259
|
+
* (único por archivo), así no pueden chocar con otros scripts. Devuelve el código nuevo, o null si no aplica.
|
|
260
|
+
* El renombrado es consistente en todos los ámbitos, así que no cambia el significado del código.
|
|
261
|
+
*/
|
|
262
|
+
export function isolateTempGlobals(code, before, prefix) {
|
|
263
|
+
let ast;
|
|
264
|
+
try {
|
|
265
|
+
ast = parse(code, 'script', LATEST);
|
|
266
|
+
} catch {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
const top = globalBindings(
|
|
270
|
+
ast,
|
|
271
|
+
ast.body.some((s) => s.directive === 'use strict'),
|
|
272
|
+
);
|
|
273
|
+
const fresh = [...top.keys()].filter((n) => !before.has(n));
|
|
274
|
+
if (!fresh.length || !fresh.every((n) => ESBUILD_TEMP.test(n))) return null;
|
|
275
|
+
const rename = new Map(fresh.map((n) => [n, `${prefix}${n}`]));
|
|
276
|
+
const edits = new Map();
|
|
277
|
+
walk.fullAncestor(ast, (node, _state, ancestors) => {
|
|
278
|
+
if (node.type !== 'Identifier' || !rename.has(node.name)) return;
|
|
279
|
+
const p = ancestors[ancestors.length - 2];
|
|
280
|
+
const to = rename.get(node.name);
|
|
281
|
+
if (p) {
|
|
282
|
+
if (p.type === 'MemberExpression' && p.property === node && !p.computed) return;
|
|
283
|
+
if (p.type === 'Property' && p.shorthand) {
|
|
284
|
+
// { _a } → { _a: nuevo }: la clave se conserva, el valor se renombra.
|
|
285
|
+
edits.set(node.start, { start: node.start, end: node.end, text: `${node.name}: ${to}` });
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if ((p.type === 'Property' || p.type === 'MethodDefinition' || p.type === 'PropertyDefinition') && p.key === node && !p.computed) return;
|
|
289
|
+
if (p.type === 'LabeledStatement' || p.type === 'BreakStatement' || p.type === 'ContinueStatement') return;
|
|
290
|
+
}
|
|
291
|
+
if (!edits.has(node.start)) edits.set(node.start, { start: node.start, end: node.end, text: to });
|
|
292
|
+
});
|
|
293
|
+
let out = code;
|
|
294
|
+
for (const e of [...edits.values()].sort((a, b) => b.start - a.start)) out = out.slice(0, e.start) + e.text + out.slice(e.end);
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
|
|
247
298
|
/**
|
|
248
299
|
* Compara las globales de un script antes y después de transformarlo.
|
|
249
300
|
* `allowPrefix` permite nombres nuevos con ese prefijo (los que añade el ofuscador).
|
|
250
301
|
*/
|
|
251
302
|
export function compareGlobals(before, afterCode, allowPrefix = null) {
|
|
252
|
-
|
|
253
|
-
|
|
303
|
+
// Solo hacen falta las globales: un parseo y un recorrido, sin el análisis completo.
|
|
304
|
+
let ast;
|
|
305
|
+
try {
|
|
306
|
+
ast = parse(afterCode, 'script', LATEST);
|
|
307
|
+
} catch (err) {
|
|
308
|
+
return { ok: false, reason: L(`el resultado no es JS válido (${err.message})`, `the result is not valid JS (${err.message})`) };
|
|
309
|
+
}
|
|
310
|
+
const after = {
|
|
311
|
+
top: globalBindings(
|
|
312
|
+
ast,
|
|
313
|
+
ast.body.some((s) => s.directive === 'use strict'),
|
|
314
|
+
),
|
|
315
|
+
};
|
|
254
316
|
for (const [name, kind] of before) {
|
|
255
|
-
if (!after.top.has(name)) return { ok: false, reason: `desaparece la global "${name}"` };
|
|
256
|
-
if (after.top.get(name) !== kind)
|
|
317
|
+
if (!after.top.has(name)) return { ok: false, reason: L(`desaparece la global "${name}"`, `the global "${name}" disappears`) };
|
|
318
|
+
if (after.top.get(name) !== kind)
|
|
319
|
+
return {
|
|
320
|
+
ok: false,
|
|
321
|
+
reason: L(
|
|
322
|
+
`la global "${name}" cambia de tipo (${kind} → ${after.top.get(name)})`,
|
|
323
|
+
`the global "${name}" changes kind (${kind} → ${after.top.get(name)})`,
|
|
324
|
+
),
|
|
325
|
+
};
|
|
257
326
|
}
|
|
258
327
|
for (const name of after.top.keys()) {
|
|
259
328
|
if (!before.has(name) && !(allowPrefix && name.startsWith(allowPrefix)))
|
|
260
|
-
return {
|
|
329
|
+
return {
|
|
330
|
+
ok: false,
|
|
331
|
+
reason: L(
|
|
332
|
+
`aparece una global nueva "${name}" que podría chocar con otros scripts`,
|
|
333
|
+
`a new global "${name}" appears that could clash with other scripts`,
|
|
334
|
+
),
|
|
335
|
+
};
|
|
261
336
|
}
|
|
262
337
|
return { ok: true };
|
|
263
338
|
}
|
package/src/obfuscate.js
CHANGED
|
@@ -3,6 +3,7 @@ import * as acorn from 'acorn';
|
|
|
3
3
|
import { shortHash } from './util.js';
|
|
4
4
|
import { compareGlobals, moduleShape, checkSyntax } from './jsinfo.js';
|
|
5
5
|
import { importOptional } from './optional.js';
|
|
6
|
+
import { L } from './i18n.js';
|
|
6
7
|
|
|
7
8
|
// Nunca se activan solos: cambian el comportamiento o rompen código que comparte globales.
|
|
8
9
|
const FORCED_SAFE = { renameGlobals: false, renameProperties: false, sourceMap: false, target: 'browser' };
|
|
@@ -101,7 +102,7 @@ let engine = null;
|
|
|
101
102
|
/** Carga javascript-obfuscator (instalado junto a Builder o en el proyecto). */
|
|
102
103
|
export async function ensureObfuscator(projectRoot) {
|
|
103
104
|
if (!engine) {
|
|
104
|
-
const mod = await importOptional('javascript-obfuscator', projectRoot, 'para ofuscar el JavaScript');
|
|
105
|
+
const mod = await importOptional('javascript-obfuscator', projectRoot, L('para ofuscar el JavaScript', 'to obfuscate JavaScript'));
|
|
105
106
|
engine = mod.default ?? mod;
|
|
106
107
|
}
|
|
107
108
|
return engine;
|
|
@@ -143,15 +144,18 @@ export function obfuscateSafe(code, { isModule, options, id, globals }) {
|
|
|
143
144
|
try {
|
|
144
145
|
out = engine.obfuscate(body, opts).getObfuscatedCode();
|
|
145
146
|
} catch (err) {
|
|
146
|
-
return {
|
|
147
|
+
return {
|
|
148
|
+
code: null,
|
|
149
|
+
reason: L(`el ofuscador falló (${String(err.message).split('\n')[0]})`, `the obfuscator failed (${String(err.message).split('\n')[0]})`),
|
|
150
|
+
};
|
|
147
151
|
}
|
|
148
152
|
if (isModule) {
|
|
149
153
|
const err = checkSyntax(out, true);
|
|
150
|
-
if (err) return { code: null, reason: `resultado inválido (${err})` };
|
|
154
|
+
if (err) return { code: null, reason: L(`resultado inválido (${err})`, `invalid result (${err})`) };
|
|
151
155
|
const a = moduleShape(body);
|
|
152
156
|
const b = moduleShape(out);
|
|
153
157
|
if (!a || !b || a.exports !== b.exports || a.imports !== b.imports)
|
|
154
|
-
return { code: null, reason: 'cambiaron las exportaciones o importaciones del módulo' };
|
|
158
|
+
return { code: null, reason: L('cambiaron las exportaciones o importaciones del módulo', 'the module exports or imports changed') };
|
|
155
159
|
} else {
|
|
156
160
|
const cmp = compareGlobals(globals, out, prefix);
|
|
157
161
|
if (!cmp.ok) return { code: null, reason: cmp.reason };
|
package/src/optional.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
5
|
import { BuildError } from './util.js';
|
|
6
|
+
import { L } from './i18n.js';
|
|
6
7
|
|
|
7
8
|
const cache = new Map();
|
|
8
9
|
|
|
@@ -25,9 +26,12 @@ export async function importOptional(name, projectRoot, purpose) {
|
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
if (!mod)
|
|
28
|
-
throw new BuildError(`Falta el paquete opcional "${name}" ${purpose}.`, [
|
|
29
|
-
`Instálalo una vez con: npm install --save-dev ${name}`,
|
|
30
|
-
|
|
29
|
+
throw new BuildError(L(`Falta el paquete opcional "${name}" ${purpose}.`, `Missing optional package "${name}" ${purpose}.`), [
|
|
30
|
+
L(`Instálalo una vez con: npm install --save-dev ${name}`, `Install it once with: npm install --save-dev ${name}`),
|
|
31
|
+
L(
|
|
32
|
+
'Builder lo carga solo cuando hace falta, por eso no viene incluido (así la instalación base es ligera).',
|
|
33
|
+
'Builder loads it only when needed, so it is not bundled (this keeps the base install small).',
|
|
34
|
+
),
|
|
31
35
|
]);
|
|
32
36
|
cache.set(name, mod);
|
|
33
37
|
return mod;
|
package/src/output.js
CHANGED
|
@@ -3,6 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { BuildError, isInside, retryFs, copyDir } from './util.js';
|
|
6
|
+
import { L } from './i18n.js';
|
|
6
7
|
|
|
7
8
|
export const MANIFEST = '.builder-manifest.json';
|
|
8
9
|
|
|
@@ -12,18 +13,40 @@ export function assertSafeOutDir(cfg, { force = false } = {}) {
|
|
|
12
13
|
const root = path.resolve(cfg.root);
|
|
13
14
|
const parsed = path.parse(out);
|
|
14
15
|
const same = (a, b) => path.relative(a, b) === '';
|
|
15
|
-
if (same(out, root))
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
if (same(out, root))
|
|
17
|
+
throw new BuildError(
|
|
18
|
+
L(
|
|
19
|
+
'"outDir" no puede ser la propia carpeta del proyecto: se borraría el código fuente.',
|
|
20
|
+
'"outDir" cannot be the project folder itself: the source code would be deleted.',
|
|
21
|
+
),
|
|
22
|
+
);
|
|
23
|
+
if (isInside(out, root))
|
|
24
|
+
throw new BuildError(
|
|
25
|
+
L(
|
|
26
|
+
`"outDir" (${out}) contiene al proyecto; se borraría el código fuente.`,
|
|
27
|
+
`"outDir" (${out}) contains the project; the source code would be deleted.`,
|
|
28
|
+
),
|
|
29
|
+
);
|
|
30
|
+
if (same(out, parsed.root)) throw new BuildError(L('"outDir" no puede ser la raíz de una unidad.', '"outDir" cannot be the root of a drive.'));
|
|
31
|
+
if (same(out, os.homedir())) throw new BuildError(L('"outDir" no puede ser tu carpeta de usuario.', '"outDir" cannot be your home folder.'));
|
|
19
32
|
if (!cfg.clean || force) return;
|
|
20
33
|
if (fs.existsSync(out)) {
|
|
21
|
-
if (!fs.statSync(out).isDirectory())
|
|
34
|
+
if (!fs.statSync(out).isDirectory())
|
|
35
|
+
throw new BuildError(L(`"outDir" existe y no es una carpeta: ${out}`, `"outDir" exists and is not a folder: ${out}`));
|
|
22
36
|
const entries = fs.readdirSync(out);
|
|
23
37
|
if (entries.length && !entries.includes(MANIFEST))
|
|
24
|
-
throw new BuildError(
|
|
25
|
-
|
|
26
|
-
|
|
38
|
+
throw new BuildError(
|
|
39
|
+
L(
|
|
40
|
+
`La carpeta de salida ${out} ya tiene archivos y no la creó Builder. No la borro para no perder datos.`,
|
|
41
|
+
`The output folder ${out} already has files and was not created by Builder. Not deleting it, to avoid losing data.`,
|
|
42
|
+
),
|
|
43
|
+
[
|
|
44
|
+
L(
|
|
45
|
+
'Elige otra carpeta con --out, vacíala tú mismo o usa --force si estás seguro.',
|
|
46
|
+
'Choose another folder with --out, empty it yourself, or use --force if you are sure.',
|
|
47
|
+
),
|
|
48
|
+
],
|
|
49
|
+
);
|
|
27
50
|
}
|
|
28
51
|
}
|
|
29
52
|
|
|
@@ -89,7 +112,12 @@ export async function swapIntoPlace(cfg, staging) {
|
|
|
89
112
|
await rmrf(staging);
|
|
90
113
|
} catch (copyErr) {
|
|
91
114
|
if (exists) await retryFs(() => fs.promises.rename(old, out)).catch(() => {});
|
|
92
|
-
throw new BuildError(
|
|
115
|
+
throw new BuildError(
|
|
116
|
+
L(
|
|
117
|
+
`No pude colocar la salida en ${out}: ${copyErr.message ?? err.message}`,
|
|
118
|
+
`Could not place the output in ${out}: ${copyErr.message ?? err.message}`,
|
|
119
|
+
),
|
|
120
|
+
);
|
|
93
121
|
}
|
|
94
122
|
}
|
|
95
123
|
if (exists) await rmrf(old).catch(() => {});
|
package/src/plugins.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Plugins: ganchos sencillos para casos que Builder no cubre.
|
|
2
|
+
//
|
|
3
|
+
// export default {
|
|
4
|
+
// plugins: [
|
|
5
|
+
// {
|
|
6
|
+
// name: 'version',
|
|
7
|
+
// filter: /\.(html|js)$/, // opcional: a qué archivos se aplica transform
|
|
8
|
+
// transform: ({ path, code }) => code.replaceAll('__VERSION__', '1.2.3'),
|
|
9
|
+
// buildStart: ({ config, mode }) => {},
|
|
10
|
+
// buildEnd: ({ result, outDir }) => {},
|
|
11
|
+
// },
|
|
12
|
+
// ],
|
|
13
|
+
// };
|
|
14
|
+
//
|
|
15
|
+
// En builder.config.json se nombran paquetes: "plugins": ["mi-plugin", ["otro-plugin", { "opcion": 1 }]].
|
|
16
|
+
// Un paquete exporta el objeto plugin o una función (opciones) => plugin.
|
|
17
|
+
//
|
|
18
|
+
// transform cambia el CÓDIGO FUENTE (HTML, CSS, JS, TS) antes de procesarlo: así el hash, el SRI
|
|
19
|
+
// y la verificación final se calculan sobre el resultado del plugin.
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
22
|
+
import { pathToFileURL } from 'node:url';
|
|
23
|
+
import { BuildError } from './util.js';
|
|
24
|
+
import { L } from './i18n.js';
|
|
25
|
+
|
|
26
|
+
export const TRANSFORM_EXT = new Set(['.html', '.htm', '.css', '.js', '.mjs', '.cjs', '.ts', '.mts', '.cts']);
|
|
27
|
+
const HOOKS = ['transform', 'buildStart', 'buildEnd'];
|
|
28
|
+
|
|
29
|
+
/** Errores de validación de la opción "plugins" (antes de cargar paquetes). */
|
|
30
|
+
export function validatePlugins(list) {
|
|
31
|
+
if (!Array.isArray(list)) return [L('"plugins" debe ser una lista.', '"plugins" must be a list.')];
|
|
32
|
+
const errors = [];
|
|
33
|
+
list.forEach((p, i) => {
|
|
34
|
+
const ok =
|
|
35
|
+
typeof p === 'string' ||
|
|
36
|
+
(Array.isArray(p) && typeof p[0] === 'string' && p.length <= 2) ||
|
|
37
|
+
(p && typeof p === 'object' && !Array.isArray(p) && typeof p.name === 'string');
|
|
38
|
+
if (!ok)
|
|
39
|
+
errors.push(
|
|
40
|
+
L(
|
|
41
|
+
`"plugins[${i}]" debe ser un nombre de paquete, [nombre, opciones] o un objeto con "name".`,
|
|
42
|
+
`"plugins[${i}]" must be a package name, [name, options] or an object with "name".`,
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
else if (p && typeof p === 'object' && !Array.isArray(p)) {
|
|
46
|
+
for (const k of Object.keys(p))
|
|
47
|
+
if (!['name', 'filter', ...HOOKS].includes(k))
|
|
48
|
+
errors.push(
|
|
49
|
+
L(
|
|
50
|
+
`"plugins[${i}].${k}" no es un gancho conocido (${HOOKS.join(', ')}).`,
|
|
51
|
+
`"plugins[${i}].${k}" is not a known hook (${HOOKS.join(', ')}).`,
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
return errors;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Carga los plugins nombrados como paquete (desde el proyecto) y devuelve objetos plugin. */
|
|
60
|
+
export async function loadPlugins(list, root) {
|
|
61
|
+
const out = [];
|
|
62
|
+
for (const entry of list) {
|
|
63
|
+
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
|
64
|
+
out.push(entry);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const [name, options] = Array.isArray(entry) ? entry : [entry, undefined];
|
|
68
|
+
let mod;
|
|
69
|
+
try {
|
|
70
|
+
const req = createRequire(path.join(root, 'package.json'));
|
|
71
|
+
mod = await import(pathToFileURL(req.resolve(name)).href);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
throw new BuildError(L(`No se pudo cargar el plugin "${name}": ${err.message}`, `Could not load the plugin "${name}": ${err.message}`), [
|
|
74
|
+
L(`Instálalo en el proyecto: npm install --save-dev ${name}`, `Install it in the project: npm install --save-dev ${name}`),
|
|
75
|
+
]);
|
|
76
|
+
}
|
|
77
|
+
const exported = mod.default ?? mod;
|
|
78
|
+
const plugin = typeof exported === 'function' ? await exported(options ?? {}) : exported;
|
|
79
|
+
if (!plugin || typeof plugin !== 'object' || typeof plugin.name !== 'string')
|
|
80
|
+
throw new BuildError(
|
|
81
|
+
L(
|
|
82
|
+
`El plugin "${name}" debe exportar un objeto con "name" o una función que lo devuelva.`,
|
|
83
|
+
`The plugin "${name}" must export an object with "name" or a function that returns one.`,
|
|
84
|
+
),
|
|
85
|
+
);
|
|
86
|
+
out.push(plugin);
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function call(plugin, hook, arg, where = '') {
|
|
92
|
+
try {
|
|
93
|
+
return await plugin[hook](arg);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
throw new BuildError(
|
|
96
|
+
L(
|
|
97
|
+
`El plugin "${plugin.name}" falló en ${hook}${where}: ${err.message}`,
|
|
98
|
+
`The plugin "${plugin.name}" failed in ${hook}${where}: ${err.message}`,
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Ejecuta un gancho sin valor de retorno (buildStart, buildEnd) en todos los plugins, en orden. */
|
|
105
|
+
export async function runHook(plugins, hook, arg) {
|
|
106
|
+
for (const p of plugins) if (typeof p[hook] === 'function') await call(p, hook, arg);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Pasa el código de un archivo por los transform de todos los plugins, en orden. */
|
|
110
|
+
export async function runTransform(plugins, rel, code) {
|
|
111
|
+
let current = code;
|
|
112
|
+
for (const p of plugins) {
|
|
113
|
+
if (typeof p.transform !== 'function') continue;
|
|
114
|
+
if (p.filter instanceof RegExp && !p.filter.test(rel)) continue;
|
|
115
|
+
const r = await call(p, 'transform', { path: rel, code: current }, ` (${rel})`);
|
|
116
|
+
if (typeof r === 'string') current = r;
|
|
117
|
+
else if (r != null)
|
|
118
|
+
throw new BuildError(
|
|
119
|
+
L(
|
|
120
|
+
`El plugin "${p.name}" devolvió algo que no es texto en transform (${rel}).`,
|
|
121
|
+
`The plugin "${p.name}" returned something that is not text from transform (${rel}).`,
|
|
122
|
+
),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return current;
|
|
126
|
+
}
|