How to Fix: Build error with Next.js 15 in monorepo
The build fails because Next.js 15 is stricter about how it resolves packages inside a monorepo, and this repo mixes shared package consumption with app-level build settings in a way that works during development but breaks during production compilation. In this setup, the app tries to consume code from workspace packages that are not being transpiled consistently for the Next.js build pipeline, which causes module resolution and compilation errors when building the repository.
Understanding the Root Cause
This issue usually appears in Next.js 15 monorepos when one or both apps import components, utilities, or configs from internal workspace packages, but the consuming app is not fully configured to let Next transform that external source code.
There are three common technical reasons this happens:
- Workspace packages are imported as source instead of prebuilt output. If a package exposes TypeScript, JSX, or modern ESM directly, the Next app must explicitly transpile it.
- Package export metadata is incomplete. Missing or incorrect main, module, types, or exports fields can cause build-time resolution failures even if local development seems fine.
- Monorepo dependency boundaries changed with newer bundling behavior. Next.js 15 relies on a stricter toolchain path, so code outside the app directory may need transpilePackages or compatible package builds.
In repos like this one, the biggest clue is that multiple apps fail during next build, not just at runtime. That strongly suggests a shared package configuration problem rather than an isolated page bug.
The practical root cause is: shared workspace packages are not being consumed in a build-safe way for Next.js 15.
Step-by-Step Solution
The most reliable fix is to make each shared package monorepo-safe for Next.js 15 and tell the Next apps to transpile those packages.
1. Identify the shared internal packages used by the apps
Open the app package files and look for imports from internal workspace packages such as a UI package, config package, or utility package.
// Example imports inside apps/template or apps/playground
import { Button } from "@doomui/ui"
import { cn } from "@doomui/utils"
Make a list of every internal package consumed by the failing apps.
2. Add transpilePackages to each Next.js app
In apps/template/next.config.js or next.config.ts, add the internal packages so Next.js compiles them correctly.
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ["@doomui/ui", "@doomui/utils"]
}
module.exports = nextConfig
Do the same for apps/playground.
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ["@doomui/ui", "@doomui/utils"]
}
module.exports = nextConfig
If the repo uses more internal packages, include all of them.
3. Fix package entry points in shared packages
Each shared package should expose valid entry files. Open the package.json for the internal packages and verify the fields.
{
"name": "@doomui/ui",
"version": "0.0.0",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts"
}
}
If you prefer source consumption, the config above can work when paired with transpilePackages. If you prefer prebuilt package output, build the package to dist and point exports there instead.
{
"name": "@doomui/ui",
"version": "0.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
}
}
Important: do not leave package metadata ambiguous. Next.js 15 is less forgiving when package boundaries are unclear.
4. Ensure shared packages declare their dependencies correctly
If @doomui/ui uses React, class-variance-authority, tailwind-merge, or other libraries, those packages must be declared correctly.
{
"peerDependencies": {
"react": "^18 || ^19",
"react-dom": "^18 || ^19"
},
"dependencies": {
"class-variance-authority": "^0.7.0",
"tailwind-merge": "^2.0.0"
}
}
Using the wrong dependency type can produce duplicate React instances or unresolved module errors during the build.
5. Align TypeScript path aliases with workspace resolution
If the monorepo uses tsconfig base paths, make sure they match actual package names and exports. Avoid pointing apps directly at deep source paths when the package name should be used instead.
{
"compilerOptions": {
"paths": {
"@doomui/ui": ["packages/ui/src/index.ts"],
"@doomui/utils": ["packages/utils/src/index.ts"]
}
}
}
This is acceptable for editor tooling, but the app should still import from the real package name and let Next resolve it through the workspace.
6. Clean install and rebuild the workspace
After config changes, clear all cached artifacts and reinstall dependencies.
rm -rf node_modules
rm -rf apps/template/.next
rm -rf apps/playground/.next
rm -rf packages/*/dist
pnpm install
pnpm --filter ./apps/template build
pnpm --filter ./apps/playground build
If the repo uses npm or yarn instead of pnpm, use the equivalent commands.
7. If a package ships uncompiled TypeScript, add a build step as a fallback
Some monorepos work better when shared packages are compiled before the apps build.
{
"scripts": {
"build": "tsup src/index.ts --format esm,cjs --dts"
}
}
Then update the app build order so internal packages are built first.
pnpm --filter @doomui/ui build
pnpm --filter ./apps/template build
This is not always required, but it is a strong fallback if direct source consumption remains unstable.
8. Example final Next.js app config
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ["@doomui/ui", "@doomui/utils"],
experimental: {
externalDir: true
}
}
module.exports = nextConfig
Use externalDir only if your setup needs external workspace source access. In many monorepos, transpilePackages is the key fix.
Common Edge Cases
- Duplicate React versions: If an internal package installs its own React version instead of using peer dependencies, builds may fail or hydration may break.
- Invalid exports map: A package.json exports field can silently block valid imports if subpaths are not declared.
- ESM/CJS mismatch: If a shared package outputs CommonJS but the app expects ESM, production build errors can appear even when dev mode works.
- Direct source imports: Importing from paths like @doomui/ui/src/button bypasses stable package boundaries and often breaks bundling.
- Tailwind content scanning: If UI components live in packages, Tailwind may not scan them unless the app includes workspace paths in its content config.
// tailwind.config.ts example
export default {
content: [
"./app/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"../../packages/ui/src/**/*.{ts,tsx}"
]
}
That does not usually cause the build crash itself, but it can create confusing missing-style symptoms after the main error is fixed.
FAQ
Why does next dev work but next build fails?
Development mode is often more tolerant because it resolves modules incrementally and can hide packaging mistakes. Production build performs stricter static analysis, tree-shaking, and bundling across the full dependency graph.
Do I need to prebuild every internal package in a Next.js 15 monorepo?
No. Many setups work fine with source imports if you use transpilePackages and proper package metadata. Prebuilding is mainly a fallback for packages with more complex output requirements.
Should I import shared code through tsconfig paths or package names?
Use the real workspace package name in app code. Path aliases are helpful for tooling, but package-name imports are more stable for Next.js production bundling.
The durable fix for this issue is to treat every internal shared module as a real package: give it correct exports, correct dependencies, and make the Next.js apps explicitly transpile it. Once those package boundaries are clean, both apps/template and apps/playground should build consistently in the monorepo.