Skip to content

Fix: unbuild Not Working — Build Output Empty, Stub Mode Failing, or Rollup Errors

FixDevs ·

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 files

Or 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 reads main, module, exports, and bin from package.json to determine what to build. If these fields are missing or misconfigured, unbuild doesn’t know what to generate.
  • Stub mode creates proxy files--stub generates lightweight files that jiti (a runtime TypeScript compiler) processes on the fly. This enables live development without rebuilding. But it requires jiti and only works in Node.js.
  • Rollup resolves imports differently from Node.js — relative imports in source files need extensions or proper resolution config. ./utils without 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.emitCJS and 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 source

Fix 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles