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;