fix(web): resolve Payload CMS build error with Node.js ESM loader

Payload CMS imports .css/.scss/.svg files that Node.js ESM can't handle
during page data collection. Added a custom ESM loader that stubs these
asset imports, fixing the build that has been broken since the upgrade.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-13 09:24:32 +01:00
parent 465ff9a10e
commit 80a6b8b50f
3 changed files with 7 additions and 31 deletions

View File

@@ -1,33 +1,10 @@
/** // Node.js ESM loader that stubs non-JS asset imports during Next.js page data collection.
* Node.js ESM custom loader — stubs static asset imports as empty modules. // Payload CMS and its deps import .css/.scss/.svg files that Node.js can't handle.
* const STUB_EXTENSIONS = ['.css', '.scss', '.sass', '.svg', '.png', '.jpg', '.jpeg', '.gif', '.ico', '.woff', '.woff2', '.ttf', '.eot'];
* Next.js 16 does route collection in raw Node ESM (not webpack/turbopack).
* Payload CMS deps import .css, .scss, .svg, and other assets that Node
* can't handle. This loader intercepts those and returns empty modules.
*
* Usage: NODE_OPTIONS="--import ./apps/web/css-stub-loader.mjs"
*/
import { register } from "node:module";
register(
"data:text/javascript," +
encodeURIComponent(`
const STYLE_RE = /\\.(css|scss|sass|less|svg|png|jpg|jpeg|gif|webp|ico|woff|woff2|ttf|eot|otf)$/;
export function resolve(specifier, context, nextResolve) { export function resolve(specifier, context, nextResolve) {
if (STYLE_RE.test(specifier)) { if (STUB_EXTENSIONS.some(ext => specifier.endsWith(ext))) {
return { url: 'data:text/javascript,export default {};', shortCircuit: true }; return { url: 'data:text/javascript,export default ""', shortCircuit: true };
} }
return nextResolve(specifier, context); return nextResolve(specifier, context);
} }
export function load(url, context, nextLoad) {
if (STYLE_RE.test(url)) {
return { format: 'module', source: 'export default {};', shortCircuit: true };
}
return nextLoad(url, context);
}
`),
import.meta.url,
);

View File

@@ -90,7 +90,6 @@ const config: NextConfig = {
"@payloadcms/richtext-lexical", "@payloadcms/richtext-lexical",
"@payloadcms/next", "@payloadcms/next",
"@payloadcms/ui", "@payloadcms/ui",
"react-image-crop",
"sharp", "sharp",
"libsodium-wrappers", "libsodium-wrappers",
], ],
@@ -130,7 +129,7 @@ const config: NextConfig = {
}, },
/** Enables hot reloading for local packages without a build step */ /** Enables hot reloading for local packages without a build step */
transpilePackages: INTERNAL_PACKAGES, transpilePackages: [...INTERNAL_PACKAGES, "react-image-crop"],
experimental: { experimental: {
optimizePackageImports: INTERNAL_PACKAGES, optimizePackageImports: INTERNAL_PACKAGES,
}, },

View File

@@ -4,7 +4,7 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "next build --webpack", "build": "NODE_OPTIONS='--experimental-loader ./css-stub-loader.mjs' next build --webpack",
"clean": "git clean -xdf .cache .next .turbo node_modules", "clean": "git clean -xdf .cache .next .turbo node_modules",
"dev": "next dev", "dev": "next dev",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "prettier --check . --ignore-path ../../.gitignore",