Three of the four ways to ship a JavaScript library in 2026 break on the same one-line import — import './styles.css' — and the bundler that survives all four is the one most “library bundler” guides tell you not to reach for. The short answer to the “library bundler vite tsup” question is to ship with Vite’s library mode. tsup’s own README redirects users to tsdown, tsdown is Rolldown with library defaults, and the whole “library bundler” category is converging back to the same Rollup-family core that Vite library mode already exposes. The category is a pre-Vite historical accident, and every active option in the search results leads back to tooling Vite already ships.

Editorial illustration of the opening tension in The case for Vite as the only bundler library authors should ship
Opening hook: A one-line CSS import can break three common library shipping paths; the rest of the article explains why Vite survives that edge case.
  • tsup is no longer maintained — its GitHub README tells users to switch to tsdown.
  • tsdown is published by VoidZero (the team behind Vite, Vitest, Rolldown, and Oxc) and sits on Rolldown, per the tsdown migration guide.
  • Rolldown is the VoidZero-built Rust port of Rollup, sharing the same maintainers as Vite — so tsdown and Vite library mode are converging on a common engine.
  • Vite’s library mode wraps Rollup with dual ESM/CJS, externals, and a dedicated vite-plugin-dts for type emission.
  • “Just use tsc” works for pure-TypeScript libraries and fails the moment you export JSX, import CSS, or rely on directives like 'use client'.

The verdict in one paragraph: ship with Vite library mode

Use Vite’s library mode as your bundler. It is Rollup with the externals, format, and entry conventions a library needs already wired up, plus the same plugin surface you use in your app. You write one vite.config.ts, add vite-plugin-dts for type emission, and you get ESM, CJS, and .d.ts output that consumer bundlers actually tree-shake. The frequent comparison “library bundler vite tsup” assumes those tools are in different categories. They are not — Vite library mode is a superset.

Terminal output for The case for Vite as the only bundler library authors should ship
Output captured from a live run.

The terminal output captures the shape of the result: a dist/ with an ESM file, a CJS file, a sourcemap, a single index.d.ts rolled up by the dts plugin, and nothing else. That is the artifact a library publisher needs to put on npm. Notice what is not there — no tsup.config.ts, no separate build.config.ts for unbuild, no microbundle preset chain. One config, one toolchain.

the broader Vite story goes into the specifics of this.

Why “library bundler” is a category mistake

The dedicated-library-bundler category emerged because early Webpack and Parcel were optimised for application bundles — single-graph, minified, code-split, asset-hashed output — and that artifact shape is wrong for a library. Library authors needed per-file ESM, no minification, externalised dependencies, and emitted types. Rollup did this natively, but its config was sharp-edged enough that wrappers proliferated: microbundle, unbuild, pkgroll, tsup, and now tsdown.

Vite’s library mode collapses the category. Under the hood it runs Rollup. The same Rollup that microbundle wraps, that unbuild wraps, that pkgroll wraps. The difference is that Vite library mode applies defaults that match library conventions — entry-as-files, ESM-first, dependencies externalised on request — and gives you the same plugin ecosystem an app author already knows. There is no second mental model. The phrase “library bundler vite tsup” lumps these together because users intuit they should be comparable; they are, and one of them is the superset.

There is a longer treatment in Vite’s dependency graph internals.

tsup is deprecated — by its own author

The tsup GitHub README opens with a maintenance notice telling readers the project is no longer actively maintained and recommending tsdown. This is the load-bearing fact the rest of the discourse keeps burying. If you reach for tsup in 2026 because “it’s the standard library bundler,” you are reaching for software whose own author has pointed you elsewhere. That is enough on its own to remove tsup from the decision.

This matters for security and supply-chain reasons too. An unmaintained bundler in your build pipeline is an unpatched dependency in every consumer’s tree until you swap it out. If you adopted tsup in 2023 and have not revisited the choice, the choice has changed.

Official documentation for library bundler vite tsup
Official documentation.

The Vite documentation page for library mode is the official reference you would otherwise duplicate in your own config. It documents the entry/name/fileName triplet, the format list (Vite emits es and umd for a single entry, es and cjs for multiple), and the externals shape — exactly the API surface every library wrapper reimplements with its own naming.

tsdown is Rolldown with library defaults, and Rolldown shares Vite’s maintainers

The tsdown migration guide is explicit: tsdown is built on Rolldown, published by VoidZero, and intended as the spiritual successor to tsup. Rolldown itself is the VoidZero-led Rust port of Rollup, maintained by the same team that ships Vite, Vitest, and Oxc. So the chain is: tsdown wraps Rolldown; Rolldown is built by the Vite team; Vite library mode is the long-term home for library authors already inside that ecosystem. Picking tsdown today is making a partial bet on the VoidZero stack. Picking Vite library mode today is making the same bet without the intermediate config file.

The case for keeping tsdown as a separate tool weakens as the VoidZero toolchain matures around Vite. If your only objection to Vite library mode is “I don’t want a Vite config in a non-Vite repo,” tsdown is a fine waystation. If you have any other Vite — Vitest, a docs site on VitePress, a Storybook on Vite — the second config is friction without a benefit.

Why “just use tsc” breaks the moment you ship JSX, CSS, or “use client”

The minimalist counsel — emit per-file ESM with the TypeScript compiler, declare sideEffects: false, expose per-file entry points, done — is correct for the library shape it serves: pure-TypeScript utilities with no template syntax and no asset imports. The advice collapses immediately outside that shape.

The failure modes are concrete:

Background on this in server-side rendering constraints.

  • tsc with "jsx": "preserve" emits .jsx that consumers must transform; with "jsx": "react-jsx" it inlines the JSX runtime import but cannot run a JSX-aware plugin chain (think SVG-to-component or MDX).
  • CSS imports (import './button.css') are not part of the TypeScript language — tsc will refuse them outright without a custom module declaration, and even with one will emit the import unchanged, leaving the consumer to set up CSS handling.
  • The React Server Components directive 'use client' must survive to the chunk head. A naked tsc emit can preserve it because tsc does not strip strings — but the directive needs to be the first non-comment line of the emitted file, which collapses on barrel re-exports and any post-processing.
  • Asset imports — SVG-as-component, CSS Modules, MDX — require a plugin pipeline that tsc by design does not have.

This is where every “use tsc” recommendation hits its boundary. Vite library mode does not have this boundary because it ships the same plugin pipeline as Vite app mode: @vitejs/plugin-react for JSX and the React refresh transform, native CSS handling, asset URL resolution, and directive preservation through Rollup’s output.banner / chunk-head conventions.

The four library shapes and what survives each

The shape of the library decides the bundler. Most posts in the search results pick a shape implicitly and answer for that one. Here is the explicit version.

What each toolchain produces for the four common library shapes
Library shape tsc-only tsup (esbuild) tsdown (Rolldown) Vite library mode (Rollup)
Pure TypeScript utility Works. Ships per-file ESM and .d.ts. Smallest, simplest output. Works, but bundles by default — kills tree-shaking unless you set glob entries. Works. Per-file output with format: 'esm' and explicit entries. Works. Per-file output with multi-entry build.lib.entry.
React/JSX component library Fails on CSS imports; emits .js with embedded jsx-runtime import but no plugin chain for SVG/MDX. Works for plain JSX. CSS handling is opt-in via loader. Works. Inherits Rolldown’s plugin pipeline. Works. @vitejs/plugin-react plus native CSS handling.
CSS-shipping component library Fails. tsc cannot resolve import './x.css'. Partial. Bundles CSS as side-effect imports; CSS Modules need plugins. Works. Rolldown handles CSS imports and modules. Works. Vite’s native CSS handling, PostCSS, and CSS Modules ship out of the box.
RSC-compatible ('use client') Preserves the directive only if no barrel re-export is in the chain. Historically stripped directives; modern releases preserve them only with config flags. Preserves directives at the chunk head. Preserves directives at the chunk head with the standard preserveModules setup.

Read down the right column. Vite library mode is the only entry that wins every row. That is the engineering case in one table: there is no library shape where Vite library mode is wrong, and there are three shapes where the alternatives are wrong.

A related write-up: shipping under tight bundle budgets.

Topic diagram for The case for Vite as the only bundler library authors should ship
Purpose-built diagram for this article — The case for Vite as the only bundler library authors should ship.

The diagram makes the dependency chain explicit: tsup wraps esbuild, microbundle and pkgroll wrap Rollup, unbuild wraps Rollup, tsdown wraps Rolldown, and Vite library mode wraps Rollup. Every active branch leads back to a Rollup-family core, and Vite is the only consumer of that core you already have a dev server, a test runner, and a plugin ecosystem for.

Where the headline speedup actually comes from

The pitch for Rust-based bundlers is the build-time delta. The honest version of that pitch separates transpilation from type-generation. On a multi-package library monorepo, the bundler portion of the build is rarely the slow part — tsc --emitDeclarationOnly is. Modern toolchains skip that path by using TypeScript’s isolated declarations to emit .d.ts files without invoking the full type checker, which is where the headline speedups land.

If you adopt tsdown for a “switching from tsup made my build faster” reason, run the comparison with --isolated-declarations on both sides and you will usually find the bundler delta is a fraction of the total. The implication: most of the perf argument for switching from one library bundler to another is really an argument for switching how types are emitted, which is orthogonal to the bundler. Vite library mode plus vite-plugin-dts with rollupTypes: true participates in the same trick.

Related: how lifecycle design drives speed.

The vite.config.ts that replaces your tsup.config.ts

Here is the line-for-line replacement. The library is a React component package shipping Button and Dialog, with CSS and an RSC-compatible primitive.

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import dts from 'vite-plugin-dts'
import { resolve } from 'node:path'

export default defineConfig({
  plugins: [
    react(),
    dts({ rollupTypes: true, tsconfigPath: './tsconfig.build.json' }),
  ],
  build: {
    lib: {
      entry: {
        index: resolve(__dirname, 'src/index.ts'),
        button: resolve(__dirname, 'src/button.tsx'),
        dialog: resolve(__dirname, 'src/dialog.tsx'),
      },
      formats: ['es', 'cjs'],
      fileName: (format, name) =>
        `${name}.${format === 'es' ? 'mjs' : 'cjs'}`,
    },
    rollupOptions: {
      external: ['react', 'react/jsx-runtime', 'react-dom'],
      output: {
        preserveModules: false,
        exports: 'named',
      },
    },
    sourcemap: true,
    minify: false,
  },
})

The package.json side declares the dual entries and the sideEffects contract that lets consumers tree-shake:

See also Vite Environments in practice.

{
  "type": "module",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    },
    "./button": {
      "types": "./dist/button.d.ts",
      "import": "./dist/button.mjs",
      "require": "./dist/button.cjs"
    }
  },
  "sideEffects": ["**/*.css"]
}

Three details to call out. First, external covers react/jsx-runtime — miss that and you ship the runtime twice. The official Vite discussion on managing external dependencies walks through the reasoning. Second, sideEffects: ["**/*.css"] is the precise list — false would let consumer bundlers drop your stylesheet imports. Third, multi-entry mode is what gives you per-file tree-shaking; a single index.ts entry would defeat it.

HackerNews engagement for library bundler vite tsup stories
Live data: top HackerNews stories about “library bundler vite tsup” by community points.

Community discussions on this topic — Hacker News threads, package-author writeups — keep relitigating the same trade-off because the SERP buries the conclusion. The signal in the noise: every time someone posts a fresh “how I bundle my library in 2026,” the comments converge on Vite library mode plus vite-plugin-dts, with tsdown as the second choice for non-Vite repos. The discussion is mature; the article ecosystem has not caught up.

The strongest counter-argument

The strongest objection runs like this: Vite is a development-server-first tool, and conflating “app bundler” with “library bundler” produces output that subtly disagrees with library conventions — chunk hashing on by default, asset inlining heuristics, history-API rewrites baked into config. Authors who hit those edges blame Vite and reach for a “library-dedicated” tool that does less.

The rebuttal: Vite library mode is an explicit, documented build mode with different defaults from app mode. The build documentation spells out the divergences — no minification when build.lib is set, externals required for runtime deps, dual-format output for single-entry builds. The complaints above describe app-mode defaults that the library-mode flag turns off. The remaining ergonomic gap is the dts plugin, which is a one-line addition to plugins. If your concern is the size of the dependency tree, Vite plus vite-plugin-dts plus @vitejs/plugin-react is fewer transitive dependencies than tsup plus a separate dts pipeline in most current lockfiles.

Related: monorepo workspace tooling.

Terminal animation: The case for Vite as the only bundler library authors should ship
Here it is in action.

The terminal animation steps through the migration: rename tsup.config.ts to vite.config.ts, add the three plugins, run vite build, and check dist/. For most React component libraries the entire migration is a single afternoon, and the vite-plugin-dts rollup-types flag produces the same single bundled index.d.ts tsup users are accustomed to seeing.

The takeaway

If you are starting a library this year, use Vite library mode. If you are maintaining a tsup-based library, schedule the migration — tsup is unmaintained, and the next CVE in its transitive graph is your problem. If you are evaluating tsdown specifically because of speed claims, run the comparison with isolated declarations enabled on both sides; the bundler is rarely the bottleneck the headline number suggests. And if you are still emitting with bare tsc, the day you add a CSS import or a JSX file is the day that strategy stops working — Vite library mode is the path that does not break on that change.

References