From 3f46a6657ab2f5c42de86303be04f6826f5bbe45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Guti=C3=A9rrez?= <35082514+alezmad@users.noreply.github.com> Date: Thu, 9 Apr 2026 00:35:04 +0100 Subject: [PATCH] fix(web): add CSS stub loader for Payload CMS route collection in Docker Node ESM can't handle .css imports during Next.js route collection. This loader intercepts .css resolutions and returns empty modules, fixing the build for all Payload deps (richtext-lexical, react-image-crop, etc.) Co-Authored-By: Claude Sonnet 4.6 --- apps/web/Dockerfile | 6 +++--- apps/web/css-stub-loader.mjs | 31 +++++++++++++++++++++++++++++++ apps/web/next.config.ts | 2 ++ 3 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 apps/web/css-stub-loader.mjs diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index 8447937..4e475e8 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -25,9 +25,9 @@ ENV NEXT_PUBLIC_URL=$NEXT_PUBLIC_URL ENV NEXT_PUBLIC_PRODUCT_NAME=$NEXT_PUBLIC_PRODUCT_NAME ENV NEXT_PUBLIC_DEFAULT_LOCALE=$NEXT_PUBLIC_DEFAULT_LOCALE -# TURBOPACK=0 forces webpack for production build — Payload CMS's -# richtext-lexical CSS imports fail under Turbopack. -ENV TURBOPACK=0 +# Node ESM loader that stubs .css imports during route collection. +# Payload CMS deps import .css files that Node can't handle outside webpack. +ENV NODE_OPTIONS="--import ./apps/web/css-stub-loader.mjs" RUN npx turbo run build --filter=web... # Stage 2: runtime — standalone output only diff --git a/apps/web/css-stub-loader.mjs b/apps/web/css-stub-loader.mjs new file mode 100644 index 0000000..743c698 --- /dev/null +++ b/apps/web/css-stub-loader.mjs @@ -0,0 +1,31 @@ +/** + * Node.js ESM custom loader — stubs .css imports as empty modules. + * + * Next.js 16 does route collection in raw Node ESM (not webpack/turbopack). + * Payload CMS dependencies import .css files which Node can't handle. + * This loader intercepts .css resolutions and returns an empty module. + * + * Usage: NODE_OPTIONS="--import ./apps/web/css-stub-loader.mjs" + */ + +import { register } from "node:module"; + +register( + "data:text/javascript," + + encodeURIComponent(` +export function resolve(specifier, context, nextResolve) { + if (specifier.endsWith('.css')) { + return { url: 'data:text/javascript,export default {};', shortCircuit: true }; + } + return nextResolve(specifier, context); +} + +export function load(url, context, nextLoad) { + if (url.endsWith('.css')) { + return { format: 'module', source: 'export default {};', shortCircuit: true }; + } + return nextLoad(url, context); +} +`), + import.meta.url, +); diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index 7014e2e..291eeeb 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -89,6 +89,8 @@ const config: NextConfig = { "@payloadcms/db-sqlite", "@payloadcms/richtext-lexical", "@payloadcms/next", + "@payloadcms/ui", + "react-image-crop", "sharp", ], turbopack: {