From 6ae4e3028482f575adec82e252a304dba4b7cd9d Mon Sep 17 00:00:00 2001 From: David Date: Tue, 6 Oct 2026 10:49:10 +0200 Subject: [PATCH] Invoices: ship pdfjs-dist's worker into the standalone image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every PDF import failed in production with "Setting up fake worker failed: Cannot find module '/app/node_modules/pdfjs-dist/legacy/build/pdf.worker.mjs'". output: "standalone" ships only the files Next's output tracer can see. It traced pdf.mjs, because lib/invoice-parser.ts imports it by literal path, but the worker's sole reference is a createRequire(...).resolve() call, which the tracer does not follow. So the image contained pdf.mjs alone, pdfjs fell back to its default "./pdf.worker.mjs" relative to pdf.mjs, and the import hit a file that was never copied. Copy the whole package in the runner stage: the worker has to sit next to pdf.mjs, and so do the standard fonts, which the parser needs because it passes useSystemFonts. Verified against the reconstructed runner layout — the old layout reproduces the error verbatim, the new one parses and extracts text. Also in lib/invoice-parser.ts: workerPinned was set before the resolve, so a failed resolve was never retried; and the empty catch swallowed the MODULE_NOT_FOUND that named the real cause. Both fixed. --- Dockerfile | 9 +++++++++ lib/invoice-parser.ts | 19 ++++++++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5074a7e..f85c2b2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -78,6 +78,15 @@ COPY --from=builder /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +# Next's output tracer cannot see pdfjs-dist's worker. The only reference to it +# is a createRequire(...).resolve() call, which the tracer does not follow, so +# the standalone bundle ships pdf.mjs WITHOUT pdf.worker.mjs — and every parse +# then dies with "Setting up fake worker failed: Cannot find module +# '/app/node_modules/pdfjs-dist/legacy/build/pdf.worker.mjs'". Copy the package +# in whole: the worker has to sit next to pdf.mjs at runtime, and so do the +# standard fonts, which the parser needs because it passes useSystemFonts. +COPY --from=deps --chown=nextjs:nodejs /app/node_modules/pdfjs-dist ./node_modules/pdfjs-dist + USER nextjs EXPOSE 3000 diff --git a/lib/invoice-parser.ts b/lib/invoice-parser.ts index 1a8319a..88d4647 100644 --- a/lib/invoice-parser.ts +++ b/lib/invoice-parser.ts @@ -172,21 +172,30 @@ export function isoDate(s: string): string { // falls back to a dynamic import of the worker module, which the bundler rewrites to a // .next path that does not exist in the standalone image — every parse then dies with // "Setting up fake worker failed". Pinning the worker to the real installed file avoids -// that. pdfjs-dist is listed in serverExternalPackages so it is required (and traced) -// from node_modules rather than bundled. +// that, but only if the file is actually there: pdfjs-dist is in +// serverExternalPackages so it is required from node_modules rather than bundled, and +// Next's tracer does NOT copy pdf.worker.mjs — the standalone bundle contains pdf.mjs +// alone. The Dockerfile copies the whole package in for that reason. Do not assume this +// pin alone is sufficient. let workerPinned = false; async function loadPdfjs() { const pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs"); if (!workerPinned) { - workerPinned = true; try { const { createRequire } = await import("node:module"); const req = createRequire(import.meta.url); pdfjs.GlobalWorkerOptions.workerSrc = req.resolve( "pdfjs-dist/legacy/build/pdf.worker.mjs", ); - } catch { - // Leave pdfjs's own default in place; it is correct for unbundled Node. + workerPinned = true; + } catch (err) { + // Not fatal, but it must not be silent: pdfjs then falls back to a worker + // path relative to pdf.mjs, so the parse fails much later with an opaque + // "Setting up fake worker failed". Staying UNpinned lets the next parse + // retry, so the worker is picked up as soon as it is on disk. + console.error( + `[invoice-parser] could not resolve pdf.worker.mjs: ${(err as Error).message}`, + ); } } return pdfjs;