The config migration was small. Preserving the package contract was the interesting part.
I'm @nyaomaru, a frontend engineer who enjoyed looking for fungus this season. πΈπ
Just remove tsup. Just install tsdown. Just change the config. Run the build. Done! π
The first tsdown build succeeded while silently changing files that were already part of the package's public contract.
Instead, I want to show what happened when I migrated a real published TypeScript library from tsup to tsdown, what actually changed, and how I verified that consumers would still receive the same package.
The package had: one entry point ESM and CJS outputs bundled declaration files explicit package.json exports a declaration banner a packed-package smoke test
| Environment | Value | | ------- | -------------------------------- | | Package | is-kit@1.14.2 | | Bundler | tsup@8.5.1 | | Entry | src/index.ts | | Output | ESM + CJS + bundled declarations | | Target | esnext |
tsdown is built around Rolldown, has an active ecosystem, and is explicitly designed as a migration path for projects currently using tsup.
Can I move the build setup to tsdown without changing what existing consumers receive?
along with the existing ESM/CJS runtime behavior, declaration compatibility, export set, and declaration banner.
The package itself is not "type": "module", and using an ESM-specific config extension avoids having Node reinterpret the config file and emit related warnings.
| Build | Main generated files | | -------------------------- | ------------------------------------------------------ | | tsup | index.mjs, index.js, index.d.ts | | default tsdown migration | index.mjs, index.cjs, index.d.mts, index.d.cts |
It doesn't automatically mean that output still matches every promise your already-published package makes.
The tsdown migration guide explicitly calls out the rename from outExtension to outExtensions.
tsdown chooses extensions based on the package type and output format to avoid ambiguous module interpretation.
If consumers already depend on your filenames, those filenames are part of your compatibility surface.
I already had a pnpm test:package command that builds the package, runs npm pack, installs the generated tarball into a temporary project, and verifies it from a real consumer's perspective.
A library can work perfectly inside its own repository while still publishing a broken package, so this smoke test exercises the artifact that users would actually install.
