@maistik/nuxt-pdf
Version:
A Nuxt 3 & 4 module for server-side PDF generation using Handlebars templates
155 lines (154 loc) • 5.07 kB
JavaScript
import FormData from "form-data";
export function createPdfProvider(providerType, config) {
switch (providerType) {
case "gotenberg":
return new GotenbergProvider(config.gotenberg);
case "browserless":
return new BrowserlessProvider(config.browserless);
case "puppeteer":
return new PuppeteerProvider(config.puppeteer);
default:
throw new Error(`Unknown PDF provider: ${providerType}`);
}
}
class GotenbergProvider {
constructor(config) {
this.config = config;
}
async generatePdf(html, options) {
const form = new FormData();
const htmlBuffer = Buffer.from(html, "utf8");
form.append("files", htmlBuffer, {
filename: "index.html",
contentType: "text/html",
knownLength: htmlBuffer.length
});
if (options.format) {
const dimensions = this.getPageDimensions(options.format);
form.append("paperWidth", dimensions.width);
form.append("paperHeight", dimensions.height);
}
if (options.margin) {
form.append("marginTop", ((options.margin.top ?? 0) / 25.4).toString());
form.append("marginBottom", ((options.margin.bottom ?? 0) / 25.4).toString());
form.append("marginLeft", ((options.margin.left ?? 0) / 25.4).toString());
form.append("marginRight", ((options.margin.right ?? 0) / 25.4).toString());
}
if (options.landscape) {
form.append("landscape", "true");
}
if (options.printBackground) {
form.append("printBackground", "true");
}
form.append("waitForNetworkIdle", "true");
form.append("waitDelay", "1s");
const bodyBuffer = form.getBuffer();
const headers = {
...form.getHeaders(),
// multipart/form-data; boundary=XXX
"Content-Length": bodyBuffer.length.toString()
};
const response = await fetch(`${this.config.url}/forms/chromium/convert/html`, {
method: "POST",
body: bodyBuffer,
headers
});
if (!response.ok) {
throw new Error(`Gotenberg error: ${response.statusText}`);
}
return Buffer.from(await response.arrayBuffer());
}
getPageDimensions(format) {
const formats = {
A4: { width: "8.27in", height: "11.69in" },
Letter: { width: "8.5in", height: "11in" },
Legal: { width: "8.5in", height: "14in" }
};
return formats[format] || formats["A4"];
}
}
class BrowserlessProvider {
constructor(config) {
this.config = config;
}
async generatePdf(html, options) {
const response = await fetch(`${this.config.url}/pdf?token=${this.config.apiKey}`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
html,
options: {
format: options.format || "A4",
margin: options.margin,
landscape: options.landscape || false,
printBackground: options.printBackground !== false
}
})
});
if (!response.ok) {
throw new Error(`Browserless error: ${response.statusText}`);
}
return Buffer.from(await response.arrayBuffer());
}
}
class PuppeteerProvider {
constructor(config) {
this.config = config;
}
// Launching a full Chromium per request is slow and flaky (especially on CI),
// so we keep one browser alive and reuse it, relaunching if it disconnects.
static browser = null;
static launching = null;
async getBrowser() {
const existing = PuppeteerProvider.browser;
if (existing?.connected) {
return existing;
}
if (!PuppeteerProvider.launching) {
PuppeteerProvider.launching = (async () => {
const puppeteer = await import("puppeteer").then((m) => m.default);
const launchOptions = this.config.launchOptions;
const browser = await puppeteer.launch({
// `--disable-dev-shm-usage` avoids Chromium exhausting the small
// /dev/shm tmpfs in CI/Docker, a common cause of hangs and crashes.
...launchOptions,
args: ["--disable-dev-shm-usage", ...launchOptions?.args ?? []]
});
PuppeteerProvider.browser = browser;
browser.on("disconnected", () => {
PuppeteerProvider.browser = null;
});
return browser;
})();
try {
await PuppeteerProvider.launching;
} finally {
PuppeteerProvider.launching = null;
}
} else {
await PuppeteerProvider.launching;
}
return PuppeteerProvider.browser;
}
async generatePdf(html, options) {
const browser = await this.getBrowser();
const page = await browser.newPage();
try {
await page.setContent(html, { waitUntil: "domcontentloaded", timeout: 3e4 });
const pdfOptions = {
format: options.format || "A4",
margin: options.margin,
landscape: options.landscape || false,
printBackground: options.printBackground !== false,
timeout: 3e4
};
const pdfBuffer = await page.pdf(pdfOptions);
return Buffer.from(pdfBuffer);
} finally {
await page.close().catch(() => {
});
}
}
}