Fix: tsup Not Working — Build Failing, Types Not Generated, or ESM/CJS Output Wrong
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 foundOr TypeScript declarations aren’t generated:
npx tsup src/index.ts --dts
# Error: Declaration generation is not supported for this fileOr dependencies are bundled when they shouldn’t be:
Output is 2MB — includes all of node_modulesWhy 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 usesrequire/module.exports. If yourpackage.jsonsays"type": "module"but tsup outputs CJS, Node.js rejectsrequire()calls. The output format must match what consumers expect. --dtsuses TypeScript’s compiler, not esbuild — esbuild strips types but can’t generate.d.tsfiles. tsup runstscseparately for declarations, which can fail if yourtsconfig.jsonhas issues.- Dependencies are external by default for libraries — tsup doesn’t bundle
node_modulesby default (using--format esmor--format cjs). But some configurations accidentally include dependencies, bloating the output. package.jsonexports must point to the right files — theexportsfield 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 tsconfigFix 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.
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: unbuild Not Working — Build Output Empty, Stub Mode Failing, or Rollup Errors
How to fix unbuild issues — build configuration, stub mode for development, ESM and CJS output, TypeScript declarations, external dependencies, and monorepo workspace builds.
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.