DEV Community

Cover image for Migrating a Real TypeScript OSS Library from tsup to tsdown 🔧

Migrating a Real TypeScript OSS Library from tsup to tsdown 🔧

nyaomaru on October 07, 2026

The config migration was small. Preserving the package contract was the interesting part. Hoi hoi! 👋 I'm @nyaomaru, a frontend engineer who enj...
Collapse
 
shubhradev profile image
Shubhra Pokhariya •

Really liked the way you approached this migration, especially testing the packed package rather than stopping at a successful build. A library can have a completely green build and still ship something that no longer matches its exports, runtime entry points, or declaration paths.

Testing the actual tarball from a temporary consumer makes that boundary much clearer than testing the repository build alone.

Collapse
 
nyaomaru profile image
nyaomaru •

Thanks! 😸
That was exactly the point I cared about looking at it from the consumer’s side and checking what actually gets packed and installed, not just whether the build is green.

Really glad you liked that approach! 😺

Collapse
 
koda2026 profile image
Harun - solo dev •

nyaomaru, this is a masterclass in library migration. the packed-package smoke test is exactly the kind of rigorous verification that prevents silent breaking changes. testing the actual tarball from a consumer's perspective is a pro move.

also, i have to ask: your cover images are consistently top-tier! 🔥 as someone trying to build a clean, lightweight visual brand for my own projects (and still remembering the "cozy cat" idea 😸), i’d love to know your workflow.

do you use figma, a specific ai tool, or a particular design template to get that clean, consistent look?

thanks for sharing both the technical deep dive and the visual inspiration! 🐯

Collapse
 
nyaomaru profile image
nyaomaru •

Thanks so much! 😸
I’m really glad you liked both the technical side and the visuals.

For the cover images, I actually make them by hand in Illustrator and Photoshop. So they’re not AI-generated, they’re completely original. 😼
It definitely takes more time, but honestly, that’s also one of the fun parts for me 😹
I usually try to keep the same general style, shapes, and mood so the articles feel like they belong together visually.

Cozy Cat is still alive in my head 😹

Collapse
 
technogamerz profile image
𝐓𝐡𝐞 𝐋𝐚𝐳𝐲 𝐆𝐢𝐫𝐥 •

Hey nyaomaru(sorry I don't know your real name), hope you are well!❤️
I have an idea to create an organization called Cozy Cat for creative people like you. Do you think it’s a good idea? There are so many creative people on DEV.to, and I think it could be a great place for creative minds

Thread Thread
 
nyaomaru profile image
nyaomaru •

nyaomaru may actually be my real name 😸
I think creating a Cozy Cat organization sounds really interesting! I’d be happy to join. 👍
And if you ever get an idea to build something together, creating a GitHub organization for Cozy Cat could be fun too 🐱✨

Thread Thread
 
alexcodebytes profile image
Oleksandr •

Hey! I am doing great, thanks for asking. An organization called Cozy Cat sounds absolutely amazing, honestly. DEV.to has a massive pool of talented people, but having a dedicated space focused on the creative, design-centric side of development is a killer idea. I would definitely support that runtime environment!

Collapse
 
koda2026 profile image
Harun - solo dev •

nyaomaru, hand-crafted in Illustrator and Photoshop?! That completely explains the incredible quality and consistency. 🎨

It’s so refreshing to hear that you actually enjoy the design process. That extra care really shows, and it makes the whole series feel like a premium, cohesive package rather than just another tech post.

I will absolutely keep the Cozy Cat dream alive on my end then! 😸 Thanks again for the inspiration, the technical deep dive, and for sharing such high-quality work with the community.

Happy coding (and designing)! 🐯

Collapse
 
onizuka profile image
Onizuka •

The part about tsdown silently changing files that were part of the public contract is exactly why I don't trust "just swap the bundler" migrations. Had a similar thing with a rollup update last year — build went green, output diffs were ugly, and I only caught it because I diffed the dist/ folder before publishing. That packed-package smoke test you mentioned is the real safety net here, not the config.

Collapse
 
nyaomaru profile image
nyaomaru •

Exactly 😸
A green build only tells you the bundler finished, not that the published package contract stayed intact.

Diffing dist/ is a great warning sign, but the packed-package smoke test is the real safety net because it tests what users actually install.

Your Rollup example is exactly why “just swap the bundler” migrations can be deceptively risky.

Collapse
 
johnnylemonny profile image
𝗝𝗼𝗵𝗻 •

Great write-up, @nyaomaru ! The point that a successful build doesn’t necessarily mean the package contract is preserved is especially important. I liked how you verified the actual packed artifact instead of stopping at a green build, and it was refreshing to see the honest trade-offs around output size and performance. As a contributor to another one of your projects, ChangelogBot it’s great to see the same careful approach to compatibility here too! 😸

Collapse
 
nyaomaru profile image
nyaomaru •

Thank you so much, John! 😸
That “green build doesn’t guarantee the package contract” point was exactly what I wanted to get across.
I also wanted to be transparent about the trade-offs instead of pretending the migration was simply better in every way. 😹

And it makes me especially happy that you noticed the same compatibility-first mindset in ChangelogBot, you’ve been helping make that project much stronger too. Thanks again for all your contributions there! 🙌

Collapse
 
johnnylemonny profile image
𝗝𝗼𝗵𝗻 •

Thank you, Nyaomaru! I’m really glad to be part of ChangelogBot and to contribute wherever I can. Keep up the great work! 😸

Collapse
 
nyaomaru profile image
nyaomaru •

Both 😸
The smoke test runs npm pack, installs the resulting tarball into a temporary consumer project, and then actually exercises both entry points:

  • ESM with import ... from 'is-kit'
  • CJS with require('is-kit')

It also compiles a strict NodeNext TypeScript consumer against the installed package.
So it’s not only inspecting the tarball, it verifies the package from the consumer side after installation.

Collapse
 
khalisollis profile image
Khali Sollis •

What I particularly appreciate about this migration is that it illustrates a broader principle of software engineering: internal implementation changes should be evaluated against externally observable behavior, not merely against whether the new implementation executes successfully.

Shubhra and Onizuka have already highlighted the importance of testing the packed package rather than trusting a successful build. I think there's another dimension worth exploring: the distinction between compatibility as a technical property and compatibility as a promise to users.

A published library creates expectations that extend beyond its source code. Export paths, module formats, declaration files, dependency requirements, and runtime behavior become part of an implicit agreement with downstream developers. A migration can be perfectly successful from the maintainer's perspective while quietly violating that agreement.

Your decision to test the actual npm tarball across ESM, CJS, and multiple TypeScript versions is particularly valuable because it shifts verification to the boundary where those expectations matter.

I also appreciated your restraint in interpreting the performance measurements. Reporting a faster wall-clock result without claiming a demonstrated performance improvement is an important distinction. Too often, engineering decisions are justified through benchmarks that lack enough repetitions or controlled conditions to support their conclusions.

One additional consideration might be testing the package against a small matrix of representative consumer environments, including older supported Node runtimes and different module-resolution configurations. That could help distinguish compatibility preserved in the tested environment from compatibility preserved across the full range of environments a package promises to support.

Ultimately, this article demonstrates something more important than how to migrate between two bundlers: a green build establishes that a process completed, while meaningful compatibility testing establishes whether the result still fulfills its obligations.

That mindset is valuable far beyond TypeScript libraries. It's applicable to dependency upgrades, infrastructure migrations, API changes, and virtually any modernization effort where the implementation changes but the promises to users are supposed to remain intact.

Collapse
 
nyaomaru profile image
nyaomaru •

Thank you for such a thoughtful comment! 😸
I completely agree that compatibility is also about preserving the contract — or mental model — users already have with a library or service. It’s not enough for something to simply “still work” technically if a change creates unnecessary friction or breaks expectations for existing users.

Of course, some breaking changes are sometimes unavoidable, but when we already have users depending on something, I think we should be very careful about introducing changes that disadvantage them.

I’ll also consider expanding the consumer test matrix with additional Node and module-resolution environments.

Thanks again for the great suggestion! 🙌

Collapse
 
mudassirworks profile image
Mudassir Khan •

the 'first build succeeded while silently changing the contract' scenario is the one that bites the most. ran into the same category of issue migrating a utility package — not tsup to tsdown, but a bundler config change that kept the build green while shifting the output declarations just enough to break downstream consumers on strict mode. the type system doesn't complain, the CI doesn't flag it, and you find out three weeks later when someone files an issue. what did you use to actually verify the consumer experience before publishing?

Collapse
 
nyaomaru profile image
nyaomaru •

Exactly, that was the part I wanted to avoid relying on CI for. 😸

Before publishing, I npm pack the actual package and install that tarball into a temporary consumer project. From there I check:

  • runtime ESM import
  • runtime CJS require
  • TypeScript ESM with strict NodeNext
  • TypeScript CJS with strict NodeNext

So the test is against the artifact users would actually install, not the source tree or bundler output in isolation.

I also keep separate TypeScript compatibility checks across supported TS versions.

That setup already caught a few assumptions that looked fine from the library side but were only obvious from the consumer side.

Collapse
 
dev_in_the_fog profile image
Jason Y. (dev_in_the_fog) •

High quality write-up! Breaking down complex engineering nuances into pragmatic, actionable takeaways is rare. Really enjoyed this! 🌟

Collapse
 
nyaomaru profile image
nyaomaru •

Thank you so much! 😸
I’m really glad you enjoyed it, and I’m especially happy that the practical side came through.
Really appreciate the kind words! 🌟

Collapse
 
technogamerz profile image
𝐓𝐡𝐞 𝐋𝐚𝐳𝐲 𝐆𝐢𝐫𝐥 •

Again cover image op 🔥

Collapse
 
nyaomaru profile image
nyaomaru •

Thx! 😸

Collapse
 
hemapriya_kanagala profile image
Hemapriya Kanagala •

Nyaomaru, that “build success ≠ package contract preserved” part, such an interesting thing to catch during a migration!

Collapse
 
nyaomaru profile image
nyaomaru •

Thx! 😸
That surprised me too, everything was green, but the package contract had still changed.
Definitely a good reminder to test what users actually install!

Collapse
 
ai_adam profile image
ai_adam •

Migration config giữa hai bundler hiện nay thường "dễ" vì API đều hội tụ về chuẩn exports + types trong package.json. Phần thú vị (và hay bị bỏ qua) là làm sao giữ contract thực sự: thứ tự declare trong exports, điều kiện import/require/types/default, và cách mỗi bundler emit .d.ts + .d.mts/.d.cts.

Khi chuyển sang tsdown (dùng Rolldown/Rust), có gặp case nào type re-export bị flatten không? tsup có option dts: { resolve: true } để preserve đường dẫn gốc, tsdown hiện tại handle thế nào? Cũng lo ngại về sideEffects: false — nếu library có CSS/asset import, Rolldown có tree-shake đúng không so với esbuild.

Nếu bài viết có chia sẻ thêm diff package.json trước/sau hoặc test matrix (Node 18/20/22, ESM/CJS, bundler downstream) thì sẽ cực hữu ích cho team đang cân nhắc switch PS: the tool I meant is on labagent .tech