Skip to content

Fix: tsup Not Working — Build Failing, Types Not Generated, or ESM/CJS Output Wrong

FixDevs ·

Quick Answer

How to fix tsup bundler issues — entry points, dual ESM/CJS output, TypeScript declaration files, external dependencies, tree shaking, and package.json exports configuration.

The Problem

tsup builds but the output doesn’t work when imported:

npx tsup src/index.ts
# Build succeeds but:
# require('./dist/index.js') → Error: Cannot use import statement
# import from './dist/index.mjs' → Error: Named export 'foo' not found

Or TypeScript declarations aren’t generated:

npx tsup src/index.ts --dts
# Error: Declaration generation is not supported for this file

Or dependencies are bundled when they shouldn’t be:

Output is 2MB — includes all of node_modules

Why This Happens

tsup is a zero-config TypeScript bundler powered by esbuild. It’s the go-to tool for building npm packages, but output format and module resolution need careful configuration:

  • ESM and CJS have different syntax — ESM uses import/export, CJS uses require/module.exports. If your package.json says "type": "module" but tsup outputs CJS, Node.js rejects require() calls. The output format must match what consumers expect.
  • --dts uses TypeScript’s compiler, not esbuild — esbuild strips types but can’t generate .d.ts files. tsup runs tsc separately for declarations, which can fail if your tsconfig.json has issues.
  • Dependencies are external by default for libraries — tsup doesn’t bundle node_modules by default (using --format esm or --format cjs). But some configurations accidentally include dependencies, bloating the output.
  • package.json exports must point to the right files — the exports field tells Node.js which file to load for ESM vs CJS. Misconfigured exports cause “module not found” errors even when files exist.

Fix 1: Basic Library Build

npm install -D tsup
// tsup.config.ts
import { defineConfig } from 'tsup';

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['esm', 'cjs'],     // Dual output
  dts: true,                    // Generate .d.ts files
  sourcemap: true,              // Source maps for debugging
  clean: true,                  // Clean dist/ before build
  splitting: false,             // Don't code-split (single entry)
  treeshake: true,              // Remove unused code
  outDir: 'dist',

  // External packages — don't bundle these
  external: [
    // By default, all node_modules are external
    // Add specific externals if needed
  ],

  // Banner/footer
  banner: {
    js: '/* MIT License - My Package */',
  },
});
// package.json — correct exports configuration
{
  "name": "my-package",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": {
        "types": "./dist/index.d.ts",
        "default": "./dist/index.js"
      },
      "require": {
        "types": "./dist/index.d.cts",
        "default": "./dist/index.cjs"
      }
    },
    "./utils": {
      "import": {
        "types": "./dist/utils.d.ts",
        "default": "./dist/utils.js"
      },
      "require": {
        "types": "./dist/utils.d.cts",
        "default": "./dist/utils.cjs"
      }
    }
  },
  "files": ["dist"],
  "scripts": {
    "build": "tsup",
    "dev": "tsup --watch",
    "prepublishOnly": "npm run build"
  }
}

Fix 2: Multiple Entry Points

// tsup.config.ts — multiple entries
import { defineConfig } from 'tsup';

export default defineConfig({
  entry: {
    index: 'src/index.ts',
    utils: 'src/utils/index.ts',
    cli: 'src/cli.ts',
    'react': 'src/react/index.ts',
  },
  format: ['esm', 'cjs'],
  dts: true,
  sourcemap: true,
  clean: true,
  // Split code between entries (shared chunks)
  splitting: true,

  // Separate config per entry
  // Or use multiple defineConfig entries:
});

// Alternative: array config for different settings per entry
export default defineConfig([
  {
    entry: ['src/index.ts'],
    format: ['esm', 'cjs'],
    dts: true,
  },
  {
    entry: ['src/cli.ts'],
    format: ['esm'],
    dts: false,
    banner: { js: '#!/usr/bin/env node' },
  },
]);

Fix 3: Fix Declaration File Generation

// tsup.config.ts
export default defineConfig({
  entry: ['src/index.ts'],
  format: ['esm', 'cjs'],

  // Option 1: Generate .d.ts with tsup (uses tsc internally)
  dts: true,

  // Option 2: Only generate declarations (no bundle)
  // dts: { only: true },

  // Option 3: Use a specific tsconfig for declarations
  // dts: { tsconfig: './tsconfig.build.json' },

  // Option 4: Generate .d.ts and .d.cts for dual package
  dts: true,
  // tsup auto-generates .d.cts when format includes 'cjs'
});
// tsconfig.build.json — dedicated config for declarations
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "declaration": true,
    "declarationMap": true,
    "emitDeclarationOnly": true,
    "outDir": "dist"
  },
  "include": ["src"],
  "exclude": ["src/**/*.test.ts", "src/**/*.spec.ts"]
}

Common dts errors and fixes:

// Error: "Cannot find module 'X' or its corresponding type declarations"
// Fix: Install @types/X or add to tsconfig paths

// Error: "Declaration generation not supported"
// Fix: Remove `isolatedModules: true` from tsconfig, or use a separate tsconfig for dts

// Error: "Referenced project 'X' must have setting composite"
// Fix: Add `"composite": true` to referenced tsconfig

Fix 4: Handle Dependencies Correctly

// tsup.config.ts
export default defineConfig({
  entry: ['src/index.ts'],
  format: ['esm', 'cjs'],
  dts: true,

  // External — don't bundle these (default for node_modules)
  external: [
    'react',
    'react-dom',
    /^@types\//,  // Regex pattern
  ],

  // noExternal — force bundle these into the output
  noExternal: [
    'tiny-utility-lib',  // Bundle this small dependency
  ],

  // For a CLI tool — bundle everything
  // noExternal: [/.*/],  // Bundle all dependencies

  // Environment variables
  env: {
    NODE_ENV: 'production',
  },

  // Replace values at build time
  define: {
    'process.env.VERSION': JSON.stringify(require('./package.json').version),
  },
});
// package.json — peer dependencies stay external
{
  "peerDependencies": {
    "react": ">=18.0.0",
    "react-dom": ">=18.0.0"
  },
  "peerDependenciesMeta": {
    "react-dom": { "optional": true }
  },
  "dependencies": {
    "tiny-utility": "^1.0.0"
  },
  "devDependencies": {
    "tsup": "^8.0.0",
    "typescript": "^5.0.0"
  }
}

Fix 5: React Component Library

// tsup.config.ts — for React component libraries
import { defineConfig } from 'tsup';

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['esm', 'cjs'],
  dts: true,
  sourcemap: true,
  clean: true,
  treeshake: true,

  // Don't bundle React — it's a peer dependency
  external: ['react', 'react-dom', 'react/jsx-runtime'],

  // Inject React JSX runtime
  esbuildOptions(options) {
    options.jsx = 'automatic';
  },

  // CSS handling
  // Option 1: Extract CSS to separate file
  // (CSS modules are supported automatically)

  // Option 2: Inject CSS into JS (not recommended for libraries)
  // injectStyle: true,
});

// src/index.ts — export components
export { Button } from './components/Button';
export { Input } from './components/Input';
export { Card } from './components/Card';

// Export types
export type { ButtonProps } from './components/Button';
export type { InputProps } from './components/Input';

Fix 6: Watch Mode and Development

# Watch for changes during development
npx tsup --watch

# Watch specific directories
npx tsup --watch src --watch lib

# Ignore patterns
npx tsup --watch --ignore-watch node_modules --ignore-watch dist
// package.json scripts
{
  "scripts": {
    "build": "tsup",
    "dev": "tsup --watch",
    "typecheck": "tsc --noEmit",
    "lint": "eslint src/",
    "test": "vitest",
    "prepublishOnly": "npm run build",
    "size": "size-limit",
    "clean": "rm -rf dist"
  }
}

Still Not Working?

“Cannot use import statement in a module” — the consumer is using require() on an ESM file. Add format: ['esm', 'cjs'] to generate both formats, and configure package.json exports to map require to the .cjs file and import to the .js file.

Output is huge (MBs) — dependencies are being bundled. Check noExternal isn’t set too broadly. By default, tsup externalizes node_modules. Add treeshake: true to remove unused code. For libraries, peer dependencies and regular dependencies should be external.

.d.ts files missing — add dts: true to the config. If it fails, check for TypeScript errors in your source (tsc --noEmit). tsup runs the TypeScript compiler for declarations — any TS error that prevents compilation also prevents declaration generation.

Exports field doesn’t resolve — Node.js’s exports is strict. The "." entry must exist for the main export. Paths must include the full file extension (.js, .cjs). The types condition must come first in each export block. Run npx publint to validate your package.json.

For related build tool issues, see Fix: esbuild Not Working and Fix: Rspack 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