Fix: unbuild Not Working — Build Output Empty, Stub Mode Failing, or Rollup Errors
Quick Answer
How to fix unbuild issues — build configuration, stub mode for development, ESM and CJS output, TypeScript declarations, external dependencies, and monorepo workspace builds.
The Problem
unbuild produces an empty dist/ directory:
npx unbuild
# Build succeeded but dist/ is empty or missing expected filesOr stub mode doesn’t work:
npx unbuild --stub
# Error: Cannot find module './dist/index.mjs'Or the build fails with a rollup error:
[rollup] Error: Could not resolve './utils' from 'src/index.ts'Why This Happens
unbuild is a unified build system from the UnJS ecosystem. It uses Rollup under the hood and supports both bundled and stub (JIT) modes:
- Entry points are inferred from
package.json— unbuild readsmain,module,exports, andbinfrompackage.jsonto determine what to build. If these fields are missing or misconfigured, unbuild doesn’t know what to generate. - Stub mode creates proxy files —
--stubgenerates lightweight files thatjiti(a runtime TypeScript compiler) processes on the fly. This enables live development without rebuilding. But it requiresjitiand only works in Node.js. - Rollup resolves imports differently from Node.js — relative imports in source files need extensions or proper resolution config.
./utilswithout an extension may fail during the rollup bundling phase. - unbuild inlines dependencies by default — unlike tsup which externalizes node_modules, unbuild’s behavior depends on the
rollup.emitCJSand bundling configuration.
Fix 1: Basic Configuration
npm install -D unbuild// build.config.ts
import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
// Entry points — auto-detected from package.json if not specified
entries: ['src/index'],
// Generate TypeScript declarations
declaration: true,
// Output formats
rollup: {
emitCJS: true, // Generate CommonJS output alongside ESM
inlineDependencies: false, // Don't bundle node_modules
},
// Clean dist/ before building
clean: true,
// Externals — don't bundle these
externals: [
'react',
'react-dom',
/^@types\//,
],
});// package.json — unbuild reads these fields
{
"name": "my-lib",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
},
"files": ["dist"],
"scripts": {
"build": "unbuild",
"dev": "unbuild --stub",
"prepublishOnly": "npm run build"
}
}Fix 2: Multiple Entry Points
// build.config.ts
import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
entries: [
'src/index', // Main entry
'src/utils', // ./dist/utils.mjs
'src/cli', // CLI entry
{
input: 'src/runtime/', // Directory entry — builds all files
outDir: 'dist/runtime',
},
],
declaration: true,
rollup: { emitCJS: true },
clean: true,
});// package.json — exports for multiple entries
{
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./utils": {
"import": "./dist/utils.mjs",
"require": "./dist/utils.cjs"
}
},
"bin": {
"my-cli": "./dist/cli.mjs"
}
}Fix 3: Stub Mode for Development
# Stub mode — creates proxy files for live development
npx unbuild --stub# dist/ after --stub:
# dist/index.mjs → proxy that imports src/index.ts via jiti
# dist/index.cjs → proxy that requires src/index.ts via jiti// The stub proxy looks roughly like:
// dist/index.mjs
import jiti from 'jiti';
const _jiti = jiti(import.meta.url);
export default _jiti('/absolute/path/to/src/index.ts');
// This means:
// 1. Importing from dist/ actually runs src/ through jiti
// 2. Changes to src/ are reflected immediately (no rebuild)
// 3. TypeScript is compiled on-the-fly// For monorepo development — stub all packages
{
"scripts": {
"dev": "unbuild --stub",
"build": "unbuild"
}
}# In monorepo root
npx turbo dev # Runs unbuild --stub in all packages
# Now packages can import each other's latest sourceFix 4: Handle TypeScript Paths and Aliases
// build.config.ts
import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
entries: ['src/index'],
declaration: true,
rollup: {
emitCJS: true,
},
// Resolve aliases — match tsconfig paths
alias: {
'@': './src',
'~': './src',
},
// Hook into rollup config
hooks: {
'rollup:options': (ctx, options) => {
// Customize rollup options
},
},
});// tsconfig.json — paths must match alias
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}Fix 5: Monorepo Workspace Builds
// packages/core/build.config.ts
import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
entries: ['src/index'],
declaration: true,
rollup: { emitCJS: true },
clean: true,
// External workspace packages
externals: [
'@myorg/utils', // Don't bundle workspace siblings
'@myorg/shared',
],
});
// packages/react/build.config.ts
import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
entries: ['src/index'],
declaration: true,
rollup: { emitCJS: true },
clean: true,
externals: [
'react',
'react-dom',
'@myorg/core', // Peer dependency
],
});Fix 6: Advanced Rollup Configuration
// build.config.ts — fine-tune rollup
import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
entries: ['src/index'],
declaration: true,
rollup: {
emitCJS: true,
// Inline specific dependencies (bundle them)
inlineDependencies: false,
// Rollup output options
output: {
exports: 'named',
preserveModules: false, // Bundle into single file
},
// Replace values
replace: {
'process.env.NODE_ENV': JSON.stringify('production'),
__VERSION__: JSON.stringify(require('./package.json').version),
},
// Resolve options
resolve: {
preferBuiltins: true,
},
// CommonJS interop
commonjs: {
requireReturnsDefault: 'auto',
},
// JSON support
json: {
preferConst: true,
},
// esbuild options (used for TS compilation)
esbuild: {
target: 'node18',
minify: false,
},
},
});Still Not Working?
dist/ is empty after build — unbuild determines entry points from package.json fields (main, module, exports). If these point to files that don’t have corresponding source files, nothing is built. Add explicit entries in build.config.ts to override auto-detection.
Stub mode throws “Cannot find module” — jiti must be installed (it’s a dependency of unbuild). Also, the consuming code must import from the package name (not a relative path to dist). In a monorepo, the workspace package resolution must point to the stubbed dist/.
Rollup can’t resolve imports — relative imports like ./utils need to match actual files. If the source file is utils.ts, unbuild’s rollup config should resolve it. Add file extensions in imports (./utils.js) or configure the alias option in build.config.ts.
Declaration files not generated — set declaration: true in build.config.ts. unbuild uses mkdist or rollup-plugin-dts for declarations. If TypeScript errors exist in your source, declarations may fail silently. Run tsc --noEmit first to check for errors.
For related build tool issues, see Fix: tsup Not Working and Fix: esbuild Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Changesets Not Working — Version Not Bumping, Changelog Empty, or GitHub Action Failing
How to fix @changesets/cli issues — changeset creation, version bumping, changelog generation, monorepo support, npm publishing, and GitHub Actions automation.
Fix: publint Not Working — Package Exports Invalid, Types Not Found, or Dual Package Errors
How to fix publint package validation issues — exports field configuration, dual ESM/CJS packaging, type resolution, main/module/types fields, files array, and common packaging mistakes.
Fix: tsup Not Working — Build Failing, Types Not Generated, or ESM/CJS Output Wrong
How to fix tsup bundler issues — entry points, dual ESM/CJS output, TypeScript declaration files, external dependencies, tree shaking, and package.json exports configuration.
Fix: Clack Not Working — Prompts Not Displaying, Spinners Stuck, or Cancel Not Handled
How to fix @clack/prompts issues — interactive CLI prompts, spinners, multi-select, confirm dialogs, grouped tasks, cancellation handling, and building CLI tools with beautiful output.