Skip to content

Use tsdown to build CLI binaries#258

Closed
bkeepers wants to merge 6 commits intomainfrom
tsdown-0.21-sea
Closed

Use tsdown to build CLI binaries#258
bkeepers wants to merge 6 commits intomainfrom
tsdown-0.21-sea

Conversation

@bkeepers
Copy link
Contributor

@bkeepers bkeepers commented Mar 7, 2026

tsdown 0.21 added support for SEA, so this switches from esbuilt to the latest tsdown.

Closes #254

@codecov
Copy link

codecov bot commented Mar 7, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

bkeepers and others added 5 commits March 7, 2026 18:47
tsdown's DepPlugin externalizes any package listed in package.json
dependencies by default. For a SEA binary every dependency must be
bundled into the single output file, so override this with
deps.alwaysBundle: () => true.

The previous deps.skipNodeModulesBundle: false was a no-op — that
flag only controls automatic externalization of node_modules that are
NOT production deps, and was already false by default.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ESM SEA binaries load ~7x slower than CJS due to async module graph
initialization (compileSourceTextModule). The old esbuild script used
format: cjs explicitly; tsdown defaults to esm.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The original esbuild script used minify: true. Smaller bundle means
less to parse and load at startup.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@bkeepers
Copy link
Contributor Author

bkeepers commented Mar 8, 2026

Startup Performance Analysis

Benchmarked against the current Homebrew release (built with esbuild):

user time
main (esbuild) ~0.65s
this branch (rolldown/tsdown) ~1.50s

Root cause: rolldown does not implement scope hoisting for CJS output.

esbuild merges all modules into a single flat scope — the old bundle has 4 module wrappers. rolldown wraps every module in a lazy closure, giving our bundle 568 closures that must be allocated and invoked at startup.

Both bundles use CJS format and minification, so the gap is entirely due to rolldown's module wrapping overhead.

Options considered

  1. useCodeCache: true — embeds pre-compiled V8 bytecode, skipping JS parse/compile. Would likely close the gap but produces a non-reproducible build. Ruled out since reproducibility is non-negotiable.

  2. Use esbuild only for the SEA build — tsdown handles the library build; esbuild handles the SEA bundle. Restores esbuild's scope hoisting. Adds esbuild back as a dev dependency.

  3. Wait for rolldown scope hoisting — this is on rolldown's roadmap. The gap will close, but no timeline.

The migration to tsdown's exe is otherwise clean and functional. The startup regression is a rolldown limitation, not a tsdown configuration issue.

@bkeepers
Copy link
Contributor Author

bkeepers commented Mar 8, 2026

The build in main is working fine, so there's no reason to switch to tsdown for the SEA build right now. Closing this for now.

@bkeepers bkeepers closed this Mar 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant