tsup is a fast and efficient, zero-configuration TypeScript bundler designed to streamline the process of compiling, optimizing, and outputting different module formats. Unlike older bundlers, tsup leverages esbuild under the hood for high-speed performance, supports modern ECMAScript modules and CommonJS(CJS), and provides built-in features like tree shaking, minification, and code splitting.
This guide walks through setting up tsup, configuring the output, and using the outExtension
option to customize file extensions.
tsup is a modern, fast, and zero-configuration bundler for TypeScript and JavaScript projects. It simplifies the process of bundling libraries or applications written in TypeScript or JavaScript, making it easier to produce optimized and production-ready code. tsup uses esbuild under the hood for rapid build times.
tsup is primarily used to bundle TypeScript and JavaScript projects into distributable formats. It automatically handles TypeScript compilation, tree shaking, and bundling without requiring complex configuration. It supports multiple output formats like ESM, CJS, and IIFE, making it versatile for various environments.
It’s ideal for building libraries, applications, or any project that needs to be packaged for deployment. tsup optimizes code by removing unused sections (tree shaking) and minifying output for production. It has native TypeScript support, allowing you to bundle your code directly without precompilation. Additionally, tsup can generate development builds with source maps for debugging and production-ready builds with minification.
tsup minimizes setup complexity by offering a zero-configuration approach, allowing developers to bundle TypeScript and JavaScript projects without extensive configuration files. Unlike traditional bundlers that require complex setups with multiple plugins and custom build scripts, tsup works out of the box by automatically detecting entry points, handling TypeScript compilation, and optimizing output formats. This streamlined workflow significantly reduces the time spent configuring a build system, making it easier to focus on development rather than setup.
If you’re considering alternative bundlers, check out our article “Using Rollup to package a library for TypeScript and JavaScript” for a detailed comparison.
Before bundling with tsup, start by creating a new TypeScript package. Initialize a project directory and set up TypeScript:
mkdir my-ts-package && cd my-ts-package npm init -y npm install typescript --save-dev npx tsc --init
This initializes a TypeScript package with a default tsconfig.json
.
To integrate tsup
into a TypeScript project, install it via npm:
npm install tsup --save-dev
Then, update the package.json
file to add a build script:
{ "scripts": { "build": "tsup" } }
By default, tsup looks for an index.ts
or src/index.ts
entry point. To specify an entry file manually, pass it as an argument. For example, if you have a main.ts
file inside src/
, you can define a simple function:
export function greet() { return "Hello from tsup!"; }
Run the following tsup
command:
npx tsup src/main.ts --format esm,cjs --dts
This command instructs tsup to generate both ESM and CJS outputs and includes TypeScript declaration files (.d.ts
). These files are essential for TypeScript libraries because they provide type definitions that enable editors and compilers to understand the package’s API without needing access to the original TypeScript source. Manually generating these files with tsc
can be cumbersome, requiring additional configurations, but tsup simplifies this by handling it automatically with the --dts
flag.
For those exploring other modern bundling options, our post “Migrating a TypeScript app from Node.js to Bun” offers valuable insights on an emerging alternative.
outExtension
By default, tsup outputs .js
files for both ESM and CJS formats. However, certain environments and packaging requirements may require different extensions. The outExtension
option allows renaming output files.
In a tsup.config.ts
file, define:
import { defineConfig } from 'tsup'; export default defineConfig({ entry: ['src/index.ts'], format: ['esm', 'cjs'], dts: true, outExtension({ format }) { return format === 'esm' ? { js: '.mjs' } : { js: '.cjs' }; }, });
This configuration ensures that ESM outputs use .mjs
, while CJS outputs use .cjs
, making module resolution more explicit in Node.js environments.
Supporting both ESM and CJS in a TypeScript package is crucial for module format compatibility across different environments. ESM (ECMAScript Modules) is the modern standard, optimized for tree shaking and better performance in bundlers like Webpack and Vite. CJS, on the other hand, is still widely used in Node.js projects and older toolchains. By generating both formats, the package is flexible, allowing users to consume it regardless of their module system.
tsup simplifies this dual support by allowing both formats to be defined in a single command:
npx tsup src/index.ts --format esm,cjs --dts
This approach ensures that both modern and legacy projects can import the package without issues.
For more on the differences between ESM and CommonJS — and why these distinctions matter — see our guide on CommonJS vs. ES modules in Node.js.
tsup
plays a crucial role in efficiently bundling TypeScript code. The configuration in Mappersmith’s tsup.config.ts
provides an excellent example of setting up bundling for different environments, target versions, and output formats. It showcases how to define entry points, handle different build scenarios like Node.js and browser environments, and manage sourcemaps, type declarations, and minification.
The package.json
script in Mappersmith integrates tsup
as part of a larger build process. It begins by copying version files, running tsup
to bundle the code, and finally generating type declarations. This modular approach keeps the workflow clean and focused on different aspects of the build process. The build script ties together multiple tasks, demonstrating how tsup
fits into a broader toolchain.
tsup.config.ts
fileFor Mappersmith’s tsup
configuration, the following setup is used:
import { defineConfig, Options } from 'tsup' import { esbuildPluginFilePathExtensions } from 'esbuild-plugin-file-path-extensions' // Inspired by https://github.com/immerjs/immer/pull/1032/files export default defineConfig((options) => { const commonOptions: Partial = { entry: ['src/**/*.[jt]s', '!./src/**/*.d.ts', '!./src/**/*.spec.[jt]s'], platform: 'node', target: 'node16', // `splitting` should be false, it ensures we are not getting any `chunk-*` files in the output. splitting: false, // `bundle` should be false, it ensures we are not getting the entire bundle in EVERY file of the output. bundle: false, // `sourcemap` should be true, we want to be able to point users back to the original source. sourcemap: true, clean: true, ...options, } const productionOptions = { minify: true, define: { 'process.env.NODE_ENV': JSON.stringify('production'), }, } return [ // ESM, standard bundler dev, embedded `process` references. // (this is consumed by ["exports" > "." > "import"] and ["exports > "." > "types"] in package.json) { ...commonOptions, format: ['esm'], clean: true, outDir: './dist/esm/', esbuildPlugins: [esbuildPluginFilePathExtensions({ filter: /^\./ })], // Yes, bundle: true => https://github.com/favware/esbuild-plugin-file-path-extensions?tab=readme-ov-file#usage bundle: true, dts: { compilerOptions: { resolveJsonModule: false, outDir: './dist', }, }, }, // ESM for use in browsers. Minified, with `process` compiled away { ...commonOptions, ...productionOptions, // `splitting` should be true (revert to the default) splitting: true, // `bundle` should be true, so we get everything in one file. bundle: true, entry: { 'mappersmith.production.min': 'src/index.ts', }, platform: 'browser', format: ['esm'], outDir: './dist/browser/', }, // CJS { ...commonOptions, clean: true, format: ['cjs'], outDir: './dist/', }, ] })
In the above setup:
commonOptions
contains settings that apply to all builds, such as defining entry files, targeting Node.js version 16, disabling code splitting, and enabling sourcemaps.productionOptions
applies specifically to production builds, enabling minification and defining the NODE_ENV
variable as production
.esbuildPluginFilePathExtensions
plugin and handling TypeScript declarations.outExtension
When generating production builds, it is often useful to append .min.js
to minified files for better clarity and organization. The outExtension
option in tsup allows you to modify output file extensions dynamically. Update your configuration as follows:
import { defineConfig } from 'tsup'; export default defineConfig((options) => ({ entry: ['src/index.ts'], format: ['esm', 'cjs'], dts: true, minify: true, outExtension({ format }) { return format === 'esm' ? { js: '.min.mjs' } : { js: '.min.cjs' }; }, }));
This setup ensures:
*.min.mjs
*.min.cjs
This improves clarity when distributing both development and production builds. Explicitly defining file extensions prevents ambiguity in module resolution, particularly in environments requiring strict format handling.
tsup supports multiple entry points, making it ideal for bundling libraries with several exports. To configure multiple entry points, update your tsup.config.ts
as follows:
import { defineConfig } from 'tsup'; export default defineConfig({ entry: { index: 'src/index.ts', utils: 'src/utils.ts', }, format: ['esm', 'cjs'], dts: true, splitting: true, sourcemap: true, clean: true, });
This configuration compiles src/index.ts
and src/utils.ts
separately, enabling better modularity and maintainability in larger projects.
If your project includes static assets (such as CSS or JSON files), tsup allows you to exclude external dependencies to keep the final bundle lightweight. Use the external
option to specify dependencies that should not be bundled:
import { defineConfig } from 'tsup'; export default defineConfig({ entry: ['src/index.ts'], format: ['esm', 'cjs'], dts: true, external: ['react', 'lodash'], });
This ensures that dependencies like react
and lodash
are referenced externally rather than bundled within the output, reducing the final file size and improving efficiency.
By leveraging these configurations, tsup provides a streamlined approach to managing multiple entry points, minified outputs, and external dependencies, making it a powerful tool for modern TypeScript project bundling.
When using tsup
to bundle your TypeScript package, following best practices ensures a smooth and efficient workflow while avoiding common pitfalls. Below are key strategies to use tsup
effectively:
To ensure your package works across different environments, always specify both ESM (ECMAScript Modules) and CJS (CommonJS) formats. Modern frameworks and tools often prefer ESM, while older systems or Node.js environments may still rely on CJS. Set the format
option in your tsup
configuration:
{ "format": ["esm", "cjs"] }
Failing to support both formats can limit your package’s usability, so this step is essential.
.d.ts
)TypeScript declaration files provide type information for users of your package. Without them, users lose type safety and IntelliSense support. Enable dts
in your tsup
configuration to generate these files:
{ "dts": true }
Skipping this step can hinder the developer experience for TypeScript users.
outExtension
for Node.js compatibilityNode.js has strict rules for resolving module files, requiring .mjs
for ESM and .cjs
for CJS. To avoid runtime errors, define the outExtension
option in your tsup
configuration:
{ "outExtension": ({ format }) => ({ ".js": format === "cjs" ? ".cjs" : ".mjs" }) }
This ensures Node.js correctly resolves your module files, preventing import issues.
Hardcoding paths in your tsup
configuration reduces flexibility and makes setup less reusable. Instead, use dynamic options to adapt to different formats or environments. Set the outDir
dynamically:
{ "outDir": ({ format }) => `dist/${format}` }
This approach keeps your configuration flexible and easier to maintain.
Minification reduces bundle size but makes debugging difficult. Enable source maps when minifying to simplify debugging:
{ "minify": true, "sourcemap": true }
Without source maps, debugging minified code can be nearly impossible.
By default, tsup treats certain dependencies as external and does not include them in the bundle. If you find missing dependencies in the final output, configure the external
option:
{ "external": ["react", "lodash"] }
This ensures dependencies like react
and lodash
are referenced externally rather than bundled, reducing file size.
While tsup
is optimized for speed, enabling certain features like source maps can impact build times. Monitor your build performance and adjust configurations as needed to balance speed and debugging capabilities.
tsup
is a powerful bundler that simplifies the process of bundling TypeScript projects. Its support for ESM and CJS formats, along with features like outExtension
for customized file extensions, makes it an essential tool for modern JavaScript development. By following these best practices, you can effectively integrate tsup
into your workflow, ensuring efficient and production-ready builds. Whether you are focusing on tree shaking, module format compatibility, or streamlined builds, tsup
provides the necessary tools for success.
LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowAdd to your JavaScript knowledge of shortcuts by mastering the ternary operator, so you can write cleaner code that your fellow developers will love.
Learn the fundamentals of React’s high-order components and play with some code samples to help you understand how it works.
Learn about the dependency inversion principle (DIP), its importance, and how to implement it across multiple programming languages.
Build a Telegram bot with Node.js and grammY to automate text, audio, and image responses using the Telegram API and Google Gemini.