Last active
December 31, 2025 23:41
-
-
Save SippieCup/78edd78f8229882171061cc486755cce to your computer and use it in GitHub Desktop.
Handlebars pdf generation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import fs from 'fs'; | |
| import * as path from 'path'; | |
| import puppeteer from 'puppeteer'; | |
| import handlebars from 'handlebars'; | |
| import { CreatePdfRequest } from './types'; | |
| import { MILLISECONDS_IN_MINUTE } from '../../timeUtils'; | |
| import { PartialTemplate } from '../../../handlebars/types'; | |
| /** | |
| * @returns the created pdf as a buffer | |
| */ | |
| export const createPdf = async (createPdfRequest: CreatePdfRequest): Promise<Buffer> => { | |
| const { templatePath, cssPath, templateData, partials, margin, headerTemplate, footerTemplate, width, height, landscape } = createPdfRequest; | |
| // Resolve all paths; when running tests we may not have built assets in dist/, so fall back to src/ equivalents. | |
| const resolveCandidatePath = (p: string): string => { | |
| const normalized = path.normalize(p); | |
| if (fs.existsSync(normalized)) return normalized; | |
| // try swapping dist/handlebars -> src/handlebars | |
| const alt = normalized.replace(path.normalize('dist/handlebars'), path.normalize('src/handlebars')); | |
| return fs.existsSync(alt) ? alt : normalized; | |
| }; | |
| const resolvedTemplatePath = resolveCandidatePath(templatePath); | |
| const resolvedCssPath = cssPath ? resolveCandidatePath(cssPath) : null; | |
| const resolvedPartials = (partials ?? []).map((p) => ({ | |
| ...p, | |
| templatePath: resolveCandidatePath(p.templatePath), | |
| cssPath: p.cssPath ? resolveCandidatePath(p.cssPath) : undefined, | |
| })); | |
| const pathsToCheck: string[] = getFilePathsToCheck(resolvedTemplatePath, resolvedCssPath ?? null, resolvedPartials as any); | |
| const paths = pathsToCheck.map((filepath) => path.normalize(filepath)); | |
| console.log(JSON.stringify(pathsToCheck, null, 2)); | |
| // pwd | |
| console.log(`Current directory: ${process.cwd()}`); | |
| for (const filepath of paths) { | |
| if (!fs.existsSync(filepath)) { | |
| throw new Error(`File ${filepath} does not exist`); | |
| } | |
| } | |
| // register partials | |
| for (const partial of resolvedPartials ?? []) { | |
| const partialTemplate = fs.readFileSync(path.normalize(partial.templatePath), 'utf8'); | |
| handlebars.registerPartial(partial.partialName, partialTemplate); | |
| } | |
| const template = fs.readFileSync(path.normalize(resolvedTemplatePath), 'utf8'); | |
| const compiledTemplate = handlebars.compile(template); | |
| const htmlResult: string = compiledTemplate(templateData, { | |
| allowProtoPropertiesByDefault: true, | |
| }); | |
| const browser = await puppeteer.launch({ | |
| headless: true, | |
| executablePath: process.env.PUPPETEER_EXECUTABLE_PATH ?? undefined, | |
| args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu', '--disable-dev-shm-usage'], | |
| }); | |
| try { | |
| const page = await browser.newPage(); | |
| await page.setContent(htmlResult, { | |
| waitUntil: ['load'], | |
| }); | |
| // add css styles from given files | |
| if (resolvedCssPath) await page.addStyleTag({ path: resolvedCssPath }); | |
| await page.addStyleTag({ | |
| content: '@page { size: auto; }', | |
| }); | |
| for (const partial of resolvedPartials ?? []) { | |
| if (partial.cssPath) { | |
| await page.addStyleTag({ path: partial.cssPath }); | |
| } | |
| } | |
| const pdfBuffer = await page.pdf({ | |
| headerTemplate, | |
| footerTemplate, | |
| displayHeaderFooter: headerTemplate || footerTemplate ? true : false, | |
| printBackground: true, | |
| timeout: MILLISECONDS_IN_MINUTE * 5, // timeout after 5 minutes | |
| margin, | |
| width, | |
| height, | |
| landscape, | |
| }); | |
| return Buffer.from(pdfBuffer); | |
| } finally { | |
| await browser.close(); | |
| } | |
| }; | |
| const getFilePathsToCheck = (mainTemplatePath: string, mainCssPath: string | null, partials: PartialTemplate[]) => { | |
| const paths = [mainTemplatePath]; | |
| if (mainCssPath) paths.push(mainCssPath); | |
| for (const partial of partials ?? []) { | |
| paths.push(partial.templatePath); | |
| if (partial.cssPath) paths.push(partial.cssPath); | |
| } | |
| return paths; | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment