98 lines
3.3 KiB
JavaScript
98 lines
3.3 KiB
JavaScript
import puppeteer from 'puppeteer';
|
|
import { resolve, basename, dirname } from 'path';
|
|
import { existsSync, mkdirSync } from 'fs';
|
|
import { fileURLToPath } from 'url';
|
|
import { createServer } from 'http';
|
|
import { readFile } from 'fs/promises';
|
|
import { extname } from 'path';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const PROJECT_ROOT = resolve(__dirname, '../..');
|
|
const OUTPUT_DIR = resolve(PROJECT_ROOT, 'output/pdf');
|
|
|
|
const MIME_TYPES = {
|
|
'.html': 'text/html', '.css': 'text/css',
|
|
'.js': 'application/javascript', '.mjs': 'application/javascript',
|
|
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
'.webp': 'image/webp', '.svg': 'image/svg+xml', '.json': 'application/json',
|
|
};
|
|
|
|
async function generatePdf(htmlPath) {
|
|
const absolutePath = resolve(htmlPath);
|
|
if (!existsSync(absolutePath)) {
|
|
console.error(`File not found: ${absolutePath}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
|
|
// Strip .output and .template from PDF filename
|
|
let pdfName = basename(absolutePath, '.html');
|
|
pdfName = pdfName.replace(/\.(output|template)$/, '') + '.pdf';
|
|
const pdfPath = resolve(OUTPUT_DIR, pdfName);
|
|
|
|
// Start temp HTTP server to resolve /assets/ paths correctly
|
|
const server = createServer(async (req, res) => {
|
|
const filePath = resolve(PROJECT_ROOT, decodeURIComponent(req.url).replace(/^\//, ''));
|
|
try {
|
|
const data = await readFile(filePath);
|
|
const ext = extname(filePath);
|
|
res.writeHead(200, { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream' });
|
|
res.end(data);
|
|
} catch {
|
|
res.writeHead(404);
|
|
res.end('Not found');
|
|
}
|
|
});
|
|
|
|
await new Promise(r => server.listen(0, '127.0.0.1', r));
|
|
const port = server.address().port;
|
|
const relPath = absolutePath.replace(PROJECT_ROOT + '/', '');
|
|
|
|
const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox'] });
|
|
const page = await browser.newPage();
|
|
|
|
await page.goto(`http://127.0.0.1:${port}/${relPath}`, { waitUntil: 'networkidle0', timeout: 30000 });
|
|
|
|
// Theme custom elements load via dynamic import inside the per-theme loader.
|
|
// networkidle0 doesn't wait for the post-import upgrade + shadow-DOM image fetch,
|
|
// so we explicitly wait for any used custom elements to be defined and their
|
|
// shadow-DOM images to finish loading.
|
|
const hasCustomTags = await page.evaluate(() =>
|
|
[...document.querySelectorAll('*')].some(el => el.tagName.includes('-'))
|
|
);
|
|
if (hasCustomTags) {
|
|
await page.waitForFunction(() => {
|
|
const customs = [...document.querySelectorAll('*')].filter(el => el.tagName.includes('-'));
|
|
return customs.every(el => {
|
|
const root = el.shadowRoot;
|
|
if (!root) return false;
|
|
const imgs = root.querySelectorAll('img');
|
|
if (imgs.length === 0) return false;
|
|
return [...imgs].every(img => img.complete && img.naturalWidth > 0);
|
|
});
|
|
}, { timeout: 15000, polling: 100 });
|
|
}
|
|
|
|
await page.pdf({
|
|
path: pdfPath,
|
|
format: 'A4',
|
|
printBackground: true,
|
|
margin: { top: 0, right: 0, bottom: 0, left: 0 },
|
|
preferCSSPageSize: true,
|
|
});
|
|
|
|
await browser.close();
|
|
server.close();
|
|
console.log(`PDF generated: ${pdfPath}`);
|
|
return pdfPath;
|
|
}
|
|
|
|
const htmlFile = process.argv[2];
|
|
if (!htmlFile) {
|
|
console.error('Usage: node generate-pdf.mjs <html-file>');
|
|
process.exit(1);
|
|
}
|
|
|
|
generatePdf(htmlFile);
|