219 lines
7.4 KiB
JavaScript
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);
|
|
});
|