primer commit
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
#!/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);
|
||||
});
|
||||
Reference in New Issue
Block a user