Replace Puppeteer with FormePDF for PDF generation (fixes #630)#1086
Replace Puppeteer with FormePDF for PDF generation (fixes #630)#1086danmolitor wants to merge 6 commits intoal1abb:masterfrom
Conversation
Replaces the Puppeteer/headless Chrome PDF pipeline with FormePDF, a Rust engine compiled to WebAssembly. Both invoice templates converted to FormePDF JSX components. - No Chrome dependency, no cold starts - Renders in ~20ms vs 2-3s with Puppeteer - Same output — pixel diff is ~5.4% driven by Outfit→Helvetica font substitution, layout is identical Closes al1abb#630
These were accidentally created during the Puppeteer-to-FormePDF migration and are not imported anywhere.
These were only used to generate before/after comparison images for the PR.
- Upgrade @formepdf packages to 0.8.3 - Register Outfit Regular/Medium/SemiBold/Bold via Document fonts prop - Add font files to fonts/ directory - Set fontFamily: "Outfit" on Page for both templates - Add loadFont fallback for VS Code preview compatibility - Regenerate comparison images (diff down to ~5.2%)
|
@danmolitor is attempting to deploy a commit to the al1abb-team Team on Vercel. A member of the Team first needs to authorize it. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (3)
💤 Files with no reviewable changes (1)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughMigrates PDF generation from a Puppeteer/browser HTML pipeline to native PDF rendering with Changes
Sequence DiagramsequenceDiagram
participant Client
participant APIRoute as API Route
participant PDFService as PDF Service
participant TemplateModule as Template Module
participant FormePDF as `@formepdf`
participant PDFOutput as PDF Output
Client->>APIRoute: POST /api/invoice/generate
APIRoute->>PDFService: generatePdfService(request)
PDFService->>TemplateModule: dynamic import InvoiceTemplate{id}
TemplateModule-->>PDFService: Template component
PDFService->>FormePDF: renderDocument(InvoiceTemplate(data))
FormePDF->>FormePDF: Load fonts (Outfit via fs)
FormePDF->>FormePDF: Render PDF to bytes
FormePDF-->>PDFService: PDF bytes
PDFService-->>APIRoute: Response with PDF blob
APIRoute-->>Client: 200 OK (application/pdf)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (5)
services/invoice/server/generatePdfService.ts (1)
17-18: Consider wrappingreq.json()in try-catch for malformed request handling.If the request body is malformed JSON,
req.json()will throw, but this happens outside the try-catch block. The error will propagate as an unhandled exception rather than returning a structured 400 error.Wrap JSON parsing in error handling
export async function generatePdfService(req: NextRequest) { - const body: InvoiceType = await req.json(); - try { + const body: InvoiceType = await req.json(); const { renderDocument } = await import("@formepdf/core");Or add a dedicated parse block:
export async function generatePdfService(req: NextRequest) { + let body: InvoiceType; + try { + body = await req.json(); + } catch { + return new NextResponse( + JSON.stringify({ error: "Invalid JSON body" }), + { status: 400, headers: { "Content-Type": "application/json" } } + ); + } + try { - const body: InvoiceType = await req.json(); const { renderDocument } = await import("@formepdf/core");🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@services/invoice/server/generatePdfService.ts` around lines 17 - 18, The JSON parse of the request body in generatePdfService (const body: InvoiceType = await req.json()) can throw on malformed payloads; wrap the req.json() call in its own try-catch (or add a small parse block) to catch JSON parsing errors and return a structured 400 response (including a clear error message) instead of letting the exception propagate; ensure you still validate/cast to InvoiceType and only proceed to the existing logic if parsing succeeds.app/components/templates/invoice-pdf/InvoiceTemplate1.tsx (2)
143-143: Use strict equality (!== undefined) for consistency.Lines 143, 157, and 171 use loose equality (
!= undefined). While this works (catching bothnullandundefined), using strict equality is more explicit and aligns with TypeScript best practices.-{details.discountDetails?.amount != undefined && details.discountDetails?.amount > 0 && ( +{details.discountDetails?.amount !== undefined && details.discountDetails?.amount > 0 && (Also applies to: 157-157, 171-171
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/templates/invoice-pdf/InvoiceTemplate1.tsx` at line 143, Replace the loose null/undefined checks for discount amount with strict comparisons: locate the conditional expressions that use details.discountDetails?.amount != undefined (appearing in InvoiceTemplate1.tsx around the rendering conditionals) and change them to details.discountDetails?.amount !== undefined (also update the two other occurrences noted) so the code uses strict equality for clarity and TypeScript best practices.
22-29: Font loading error handling silently falls back without logging.When font loading fails, the error is silently caught and a fallback path is used. In production, if font files are missing or fs access fails unexpectedly, this could result in PDF rendering issues that are hard to diagnose.
Consider adding a warning log for debugging
function loadFont(file: string): string | Buffer { try { return readFileSync(join(fontsDir, file)); - } catch { + } catch (err) { // VS Code preview: fs unavailable or wrong cwd — use relative path + if (process.env.NODE_ENV !== 'development') { + console.warn(`Font loading fallback for ${file}:`, err); + } return join("../../../../fonts", file); } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/templates/invoice-pdf/InvoiceTemplate1.tsx` around lines 22 - 29, The loadFont function swallows errors and silently returns a fallback path; modify loadFont(file: string) to catch the error into a variable (from readFileSync(join(fontsDir, file))) and call a logger/warn (e.g., console.warn or the existing logger) including the file name, fontsDir, and the caught error before returning the current relative fallback path; keep the existing fallback behavior but ensure you reference the same symbols (loadFont, fontsDir, readFileSync, join) so missing fonts or fs issues are logged for debugging.app/components/templates/invoice-pdf/InvoiceTemplate2.tsx (2)
48-53: Minor: Redundant empty string check.The condition
details.invoiceLogo && details.invoiceLogo !== ""has a redundant check — an empty string is falsy, so the first part already excludes it.Simplify condition
- {details.invoiceLogo && details.invoiceLogo !== "" && isDataUrl(details.invoiceLogo) ? ( + {details.invoiceLogo && isDataUrl(details.invoiceLogo) ? (🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/templates/invoice-pdf/InvoiceTemplate2.tsx` around lines 48 - 53, Remove the redundant empty-string check in the conditional that renders the logo: replace the combined check using details.invoiceLogo && details.invoiceLogo !== "" with a single truthy check plus the data-url validator (i.e., use details.invoiceLogo && isDataUrl(details.invoiceLogo)) in the JSX within InvoiceTemplate2 (the block that renders <Image src={details.invoiceLogo} .../>), ensuring the same rendering behavior is preserved.
143-156: Consider strict equality for undefined checks.Using
!= undefined(loose equality) matches bothundefinedandnull. If this is intentional to handle nullable fields, it's fine. If the intent is only to check forundefined, prefer!== undefinedfor clarity. This pattern repeats for tax (line 157) and shipping (line 171).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/templates/invoice-pdf/InvoiceTemplate2.tsx` around lines 143 - 156, Replace loose undefined checks using != undefined with strict !== undefined for the discount, tax, and shipping amount checks in InvoiceTemplate2.tsx: update the conditional that reads details.discountDetails?.amount != undefined to details.discountDetails?.amount !== undefined and do the same for the similar checks for tax and shipping amounts so the conditions only match actual undefined (not null); keep the existing null-safe optional chaining (details.discountDetails?.amount) and preserve the surrounding logic in the render blocks (refer to the discount block, the tax block, and the shipping block).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/components/templates/invoice-pdf/DynamicInvoiceTemplate.tsx`:
- Around line 15-24: The dynamic import in DynamicInvoice (created inside
useMemo, using previewName = `InvoicePreview${props.details.pdfTemplate}`)
points to non-existent files (InvoicePreview1/2); change the import target to
match existing components (e.g., use
`InvoiceTemplate${props.details.pdfTemplate}`) or create corresponding
InvoicePreview<id>.tsx preview components if previews must differ from
server-side templates; update the previewName and the dynamic import call
accordingly so they align with lib/helpers.ts which uses
InvoiceTemplate${templateId}.
In `@app/components/templates/invoice-pdf/InvoiceTemplate2.data.json`:
- Line 73: The pdfTemplate field in InvoiceTemplate2.data.json is wrongly set to
1 causing the sample data to load Template1; update the pdfTemplate value to 2
in InvoiceTemplate2.data.json so the file correctly targets Template2 and verify
any code that reads pdfTemplate (e.g., template selection logic) will now pick
the intended template.
In `@app/components/templates/invoice-pdf/InvoiceTemplate2.tsx`:
- Around line 216-221: The payment info block in InvoiceTemplate2 is rendering
literal "undefined" when details.paymentInformation or its fields are missing;
update the JSX so the View/Text group is only rendered when
details.paymentInformation is truthy (conditional rendering around the View), or
alternatively replace each interpolation (e.g.,
details.paymentInformation?.bankName, accountName, accountNumber) with
nullish-coalescing fallbacks like an empty string or "N/A"; locate the block
using the View/Text elements in the InvoiceTemplate2 component and apply one of
these fixes to avoid showing "undefined".
In `@package.json`:
- Line 50: The package.json currently lists the unused dependency "puppeteer"
which should be removed; open package.json and delete the "puppeteer":
"^24.40.0" entry, then run your package manager (npm install or yarn install) to
update the lockfile (package-lock.json or yarn.lock) and node_modules, verify
there are no imports/usages of "puppeteer" in the repo, and commit the updated
package.json and lockfile to complete the removal.
---
Nitpick comments:
In `@app/components/templates/invoice-pdf/InvoiceTemplate1.tsx`:
- Line 143: Replace the loose null/undefined checks for discount amount with
strict comparisons: locate the conditional expressions that use
details.discountDetails?.amount != undefined (appearing in InvoiceTemplate1.tsx
around the rendering conditionals) and change them to
details.discountDetails?.amount !== undefined (also update the two other
occurrences noted) so the code uses strict equality for clarity and TypeScript
best practices.
- Around line 22-29: The loadFont function swallows errors and silently returns
a fallback path; modify loadFont(file: string) to catch the error into a
variable (from readFileSync(join(fontsDir, file))) and call a logger/warn (e.g.,
console.warn or the existing logger) including the file name, fontsDir, and the
caught error before returning the current relative fallback path; keep the
existing fallback behavior but ensure you reference the same symbols (loadFont,
fontsDir, readFileSync, join) so missing fonts or fs issues are logged for
debugging.
In `@app/components/templates/invoice-pdf/InvoiceTemplate2.tsx`:
- Around line 48-53: Remove the redundant empty-string check in the conditional
that renders the logo: replace the combined check using details.invoiceLogo &&
details.invoiceLogo !== "" with a single truthy check plus the data-url
validator (i.e., use details.invoiceLogo && isDataUrl(details.invoiceLogo)) in
the JSX within InvoiceTemplate2 (the block that renders <Image
src={details.invoiceLogo} .../>), ensuring the same rendering behavior is
preserved.
- Around line 143-156: Replace loose undefined checks using != undefined with
strict !== undefined for the discount, tax, and shipping amount checks in
InvoiceTemplate2.tsx: update the conditional that reads
details.discountDetails?.amount != undefined to details.discountDetails?.amount
!== undefined and do the same for the similar checks for tax and shipping
amounts so the conditions only match actual undefined (not null); keep the
existing null-safe optional chaining (details.discountDetails?.amount) and
preserve the surrounding logic in the render blocks (refer to the discount
block, the tax block, and the shipping block).
In `@services/invoice/server/generatePdfService.ts`:
- Around line 17-18: The JSON parse of the request body in generatePdfService
(const body: InvoiceType = await req.json()) can throw on malformed payloads;
wrap the req.json() call in its own try-catch (or add a small parse block) to
catch JSON parsing errors and return a structured 400 response (including a
clear error message) instead of letting the exception propagate; ensure you
still validate/cast to InvoiceType and only proceed to the existing logic if
parsing succeeds.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 66b7a0f4-9eb7-4948-b3c6-5cc6c694c9eb
⛔ Files ignored due to path filters (17)
.github/template-1-after.pdfis excluded by!**/*.pdf.github/template-1-after.pngis excluded by!**/*.png.github/template-1-before.pdfis excluded by!**/*.pdf.github/template-1-before.pngis excluded by!**/*.png.github/template-1-comparison.pngis excluded by!**/*.png.github/template-1-diff.pngis excluded by!**/*.png.github/template-2-after.pdfis excluded by!**/*.pdf.github/template-2-after.pngis excluded by!**/*.png.github/template-2-before.pdfis excluded by!**/*.pdf.github/template-2-before.pngis excluded by!**/*.png.github/template-2-comparison.pngis excluded by!**/*.png.github/template-2-diff.pngis excluded by!**/*.pngfonts/Outfit-Bold.ttfis excluded by!**/*.ttffonts/Outfit-Medium.ttfis excluded by!**/*.ttffonts/Outfit-Regular.ttfis excluded by!**/*.ttffonts/Outfit-SemiBold.ttfis excluded by!**/*.ttfpackage-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (12)
app/api/invoice/generate/route.tsapp/components/templates/invoice-pdf/DynamicInvoiceTemplate.tsxapp/components/templates/invoice-pdf/InvoiceTemplate1.data.jsonapp/components/templates/invoice-pdf/InvoiceTemplate1.tsxapp/components/templates/invoice-pdf/InvoiceTemplate2.data.jsonapp/components/templates/invoice-pdf/InvoiceTemplate2.tsxlib/pdf-helpers.tslib/variables.tsnext.config.jspackage.jsonpuppeteer.config.cjsservices/invoice/server/generatePdfService.ts
💤 Files with no reviewable changes (2)
- lib/variables.ts
- puppeteer.config.cjs
…remove puppeteer - Fix DynamicInvoiceTemplate to import InvoiceTemplate (not deleted InvoicePreview) - Fix InvoiceTemplate2.data.json pdfTemplate from 1 to 2 - Remove unused puppeteer dependency
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Fixes #630
Replaces the Puppeteer/headless Chrome PDF pipeline with FormePDF —
a Rust engine compiled to WebAssembly. Both invoice templates converted
to FormePDF JSX components.
Why
Puppeteer requires a full Chrome installation, has cold start issues,
and is notoriously painful to run in serverless or containerized
environments. FormePDF runs anywhere JavaScript runs — Node, Cloudflare
Workers, the browser — with no native dependencies.
What changed
InvoiceTemplate1.tsx— converted to FormePDF JSXInvoiceTemplate2.tsx— converted to FormePDF JSXlib/pdf-helpers.ts— shared formatting helpers extractedpackage.json—@formepdf/reactand@formepdf/corereplace PuppeteerPerformance
Output comparison
Pixel diff is ~5.4% — driven entirely by font substitution
(Outfit → Helvetica). Layout, spacing, and data rendering are identical.
Template 1
Template 2
Summary by CodeRabbit
New Features
Performance
Updates