Files
manuales-factorpesca/scripts/build-manual.js
T
2026-10-13 21:22:54 -06:00

219 lines
7.4 KiB
JavaScript

#!/usr/bin/env node
/* =========================================================
manuales-factorpesca / scripts/build-manual.js
Convierte templates/manual.html en un PDF Letter profesional.
Uso:
node scripts/build-manual.js
node scripts/build-manual.js --html ruta/al/manual.html
node scripts/build-manual.js --out dist/manual-v1.0.0.pdf
node scripts/build-manual.js --slug capturas-app
Por defecto busca el HTML en templates/manual.html y escribe
el PDF en dist/manual.pdf.
========================================================= */
'use strict';
const path = require('node:path');
const fs = require('node:fs');
const os = require('node:os');
const SKILL_DIR = path.resolve(__dirname, '..');
const DEFAULT_HTML = path.join(SKILL_DIR, 'templates', 'manual.html');
const DEFAULT_OUT_DIR = path.join(SKILL_DIR, 'dist');
const ARTIFACTS_DIR = path.join(SKILL_DIR, 'artifacts');
function parseArgs(argv) {
const out = {
html: DEFAULT_HTML,
outDir: DEFAULT_OUT_DIR,
outFile: null,
printBackground: true,
landscape: false,
marginTop: '0',
marginBottom: '0',
marginLeft: '0',
marginRight: '0',
preferCSSPageSize: true,
displayHeaderFooter: false,
};
for (let i = 2; i < argv.length; i++) {
const a = argv[i];
switch (a) {
case '--html': out.html = path.resolve(argv[++i]); break;
case '--out': out.outFile = path.resolve(argv[++i]); break;
case '--out-dir': out.outDir = path.resolve(argv[++i]); break;
case '--no-background': out.printBackground = false; break;
case '--landscape': out.landscape = true; break;
case '--margin-top': out.marginTop = argv[++i]; break;
case '--margin-bottom': out.marginBottom = argv[++i]; break;
case '--margin-left': out.marginLeft = argv[++i]; break;
case '--margin-right': out.marginRight = argv[++i]; break;
case '--no-css-page-size': out.preferCSSPageSize = false; break;
case '--show-header-footer': out.displayHeaderFooter = true; break;
case '--help':
case '-h':
printHelp();
process.exit(0);
break;
default:
console.warn(`[build-manual] argumento desconocido ignorado: ${a}`);
}
}
return out;
}
function printHelp() {
console.log(`Uso: node build-manual.js [opciones]
Opciones:
--html <ruta> Ruta al HTML fuente. Por defecto templates/manual.html
--out <archivo> Ruta completa del PDF de salida.
--out-dir <carpeta> Carpeta de salida. Por defecto dist/.
--no-background No imprimir colores de fondo.
--landscape Orientación horizontal.
--margin-top <valor> Margen superior CSS (p. ej. 22mm).
--margin-bottom <valor> Margen inferior CSS.
--margin-left <valor> Margen izquierdo CSS.
--margin-right <valor> Margen derecho CSS.
--no-css-page-size Ignorar @page del CSS y usar los márgenes dados.
--show-header-footer Habilita la cabecera/pie de Chromium (no usado).
-h, --help Muestra esta ayuda.
`);
}
function ensurePlaywright() {
try {
return require('playwright');
} catch (err) {
console.error(`[build-manual] No se encontró 'playwright' en el PATH.
Instálalo ejecutando:
npm install playwright
o usa la skill playwright-skill de lackeyjb para preparar el entorno:
npx skills add lackeyjb/playwright-skill --skill playwright-skill --yes
`);
process.exit(1);
}
}
function buildPdfUrl(htmlPath) {
const abs = path.resolve(htmlPath);
if (!fs.existsSync(abs)) {
console.error(`[build-manual] No se encuentra el HTML: ${abs}`);
process.exit(1);
}
return 'file://' + abs;
}
function copyArtifactsIntoTemp(htmlPath) {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'manual-build-'));
const htmlDst = path.join(tmp, path.basename(htmlPath));
fs.copyFileSync(htmlPath, htmlDst);
const cssSrc = path.join(path.dirname(htmlPath), 'manual.css');
const jsSrc = path.join(path.dirname(htmlPath), 'manual.js');
if (fs.existsSync(cssSrc)) fs.copyFileSync(cssSrc, path.join(tmp, 'manual.css'));
if (fs.existsSync(jsSrc)) fs.copyFileSync(jsSrc, path.join(tmp, 'manual.js'));
if (fs.existsSync(ARTIFACTS_DIR)) {
copyDir(ARTIFACTS_DIR, path.join(tmp, 'artifacts'));
console.log(`[build-manual] Capturas copiadas a ${path.join(tmp, 'artifacts')}`);
}
return tmp;
}
function copyDir(src, dst) {
if (!fs.existsSync(src)) return;
fs.mkdirSync(dst, { recursive: true });
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
const s = path.join(src, entry.name);
const d = path.join(dst, entry.name);
if (entry.isDirectory()) copyDir(s, d);
else fs.copyFileSync(s, d);
}
}
function defaultOutFile(htmlPath) {
const html = fs.readFileSync(htmlPath, 'utf8');
const base = path.basename(htmlPath, path.extname(htmlPath));
const versionMatch = html.match(/data-meta-version[^>]*>([^<]+)/);
const version = (versionMatch ? versionMatch[1].trim() : '0.0.0').replace(/^v/i, '').replace(/[^a-zA-Z0-9._-]/g, '') || '0.0.0';
const slug = base.replace(/^manual[-_]?/, '').replace(/[^a-zA-Z0-9._-]/g, '') || 'app';
return `manual-${slug}-v${version}.pdf`;
}
async function waitForFonts(page) {
try {
await page.evaluate(() => document.fonts && document.fonts.ready);
} catch (_) {}
}
async function main() {
const args = parseArgs(process.argv);
const playwright = ensurePlaywright();
const { chromium } = playwright;
if (!fs.existsSync(args.html)) {
console.error(`[build-manual] HTML no encontrado: ${args.html}`);
process.exit(1);
}
fs.mkdirSync(args.outDir, { recursive: true });
const outFile = args.outFile || path.join(args.outDir, defaultOutFile(args.html));
const assetsTmp = copyArtifactsIntoTemp(args.html);
const url = 'file://' + path.join(assetsTmp, path.basename(args.html));
console.log(`[build-manual] Renderizando: ${args.html}`);
console.log(`[build-manual] Salida: ${outFile}`);
const browser = await chromium.launch({
headless: true,
args: ['--no-sandbox', '--disable-dev-shm-usage'],
});
try {
const context = await browser.newContext({
viewport: { width: 1100, height: 1400 },
deviceScaleFactor: 2,
});
const page = await context.newPage();
page.on('pageerror', (e) => console.error('[page error]', e));
page.on('console', (msg) => {
if (msg.type() === 'error') console.error('[page console]', msg.text());
});
await page.goto(url, { waitUntil: 'networkidle', timeout: 60_000 });
await waitForFonts(page);
await page.waitForTimeout(400);
const pdfOptions = {
path: outFile,
printBackground: args.printBackground,
preferCSSPageSize: args.preferCSSPageSize,
displayHeaderFooter: args.displayHeaderFooter,
};
if (!args.preferCSSPageSize) {
pdfOptions.format = args.landscape ? { width: '11in', height: '8.5in' } : { width: '8.5in', height: '11in' };
pdfOptions.margin = {
top: args.marginTop,
bottom: args.marginBottom,
left: args.marginLeft,
right: args.marginRight,
};
}
await page.pdf(pdfOptions);
const stat = fs.statSync(outFile);
console.log(`[build-manual] PDF generado: ${outFile} (${(stat.size / 1024).toFixed(1)} KB)`);
} finally {
await browser.close();
if (assetsTmp) {
try { fs.rmSync(assetsTmp, { recursive: true, force: true }); } catch (_) {}
}
}
}
main().catch((err) => {
console.error('[build-manual] Error inesperado:', err);
process.exit(1);
});