1. Decouple exe from format
Currently exe is a format (--format exe) that's internally mapped to cjs. Instead, exe should be a standalone boolean option:
tsdown ./src/index.ts --format esm --exe
tsdown ./src/index.ts --format cjs --exe
This way the user controls the actual module format, and --exe just enables the SEA post-build step. With Node.js landing ESM SEA support (nodejs/node#61813), tsdown would pass "mainFormat": "module" or "commonjs" to sea-config.json based on the chosen format. No special mapping needed.
Note: ESM mode currently cannot be used with useCodeCache or useSnapshot — tsdown should warn when these conflict.
2. Native addon / WASM support
It's Rolldown's responsibility to handle .node and .wasm files during bundling. What tsdown needs to do is map the non-JS output chunks into sea-config assets and inject a runtime shim so they're loaded via sea.getAsset() + process.dlopen() instead of filesystem reads.
3. Cross-platform builds
Build for linux/mac/windows from a single host by downloading the target platform's Node.js binary. For best startup performance, compiling on the target platform is still recommended. When cross-compiling, tsdown should warn and force-disable useCodeCache and useSnapshot since they are platform-specific and would crash on a different platform.
4. Code signing
Currently only ad-hoc macOS codesign. Industry-standard signing for distributable binaries:
- macOS: Developer ID signing + Apple notarization (
codesign --sign "Developer ID" + notarytool submit). Without notarization, Gatekeeper blocks the binary for end users.
- Windows: Authenticode signing via
signtool.exe with an EV/OV code signing certificate. Unsigned exes trigger SmartScreen "unknown publisher" warnings.
- Linux: No mandatory signing, but GPG signatures are common for package distribution.
5. Watch mode
Currently disabled. Could rebuild the exe on file changes, though SEA build time (seconds) makes this less practical. Might still be useful for CLI tool iteration.
6. Copy-package — automatic handling of unbundleable packages
Some packages can't be bundled — they contain native .node addons, .wasm modules, or use patterns like dynamic require() that break during bundling. These packages also have their own dependencies, so at runtime the entire dependency branch needs to be present in node_modules.
Rolldown's copy module type works at the single file level — it copies one file and rewrites one import path. It can't handle this case because:
- A package is a directory with
package.json, multiple entry points, and internal require() calls between its own files
- The package's dependencies also need to be present, forming a tree that must be resolved from
node_modules
- The
node_modules layout matters — packages resolve their own deps relative to their location
This requires package-manager-level dependency resolution, which is beyond what a bundler plugin should do. copy-package automates this at the tsdown layer:
- User marks a package for copy (API/naming TBD)
- tsdown externalizes the package from the bundle
- tsdown walks its dependency graph from
node_modules, following the full branch
- The entire branch (pkg-a → pkg-b → pkg-c → ...) is copied into the output's
node_modules
- The bundled code's
require('pkg-a') resolves to the copied location at runtime
In exe mode, these copied packages would additionally be mapped into sea-config.json assets.
tsdown handles the complexity here — dependency graph resolution, node_modules layout, and ensuring the copied branch is self-contained.
Low priority. The complexity of walking and copying dependency trees may not justify the use case. Worth revisiting once the more common exe scenarios (sections 1–5) are solid.
exefrom format — make it a boolean option (--exe) layered on top of--formatassets1. Decouple
exefrom formatCurrently
exeis a format (--format exe) that's internally mapped tocjs. Instead,exeshould be a standalone boolean option:This way the user controls the actual module format, and
--exejust enables the SEA post-build step. With Node.js landing ESM SEA support (nodejs/node#61813), tsdown would pass"mainFormat": "module"or"commonjs"to sea-config.json based on the chosen format. No special mapping needed.Note: ESM mode currently cannot be used with
useCodeCacheoruseSnapshot— tsdown should warn when these conflict.2. Native addon / WASM support
It's Rolldown's responsibility to handle
.nodeand.wasmfiles during bundling. What tsdown needs to do is map the non-JS output chunks into sea-configassetsand inject a runtime shim so they're loaded viasea.getAsset()+process.dlopen()instead of filesystem reads.3. Cross-platform builds
Build for linux/mac/windows from a single host by downloading the target platform's Node.js binary. For best startup performance, compiling on the target platform is still recommended. When cross-compiling, tsdown should warn and force-disable
useCodeCacheanduseSnapshotsince they are platform-specific and would crash on a different platform.4. Code signing
Currently only ad-hoc macOS codesign. Industry-standard signing for distributable binaries:
codesign --sign "Developer ID"+notarytool submit). Without notarization, Gatekeeper blocks the binary for end users.signtool.exewith an EV/OV code signing certificate. Unsigned exes trigger SmartScreen "unknown publisher" warnings.5. Watch mode
Currently disabled. Could rebuild the exe on file changes, though SEA build time (seconds) makes this less practical. Might still be useful for CLI tool iteration.
6. Copy-package — automatic handling of unbundleable packages
Some packages can't be bundled — they contain native
.nodeaddons,.wasmmodules, or use patterns like dynamicrequire()that break during bundling. These packages also have their own dependencies, so at runtime the entire dependency branch needs to be present innode_modules.Rolldown's
copymodule type works at the single file level — it copies one file and rewrites one import path. It can't handle this case because:package.json, multiple entry points, and internalrequire()calls between its own filesnode_modulesnode_moduleslayout matters — packages resolve their own deps relative to their locationThis requires package-manager-level dependency resolution, which is beyond what a bundler plugin should do.
copy-packageautomates this at the tsdown layer:node_modules, following the full branchnode_modulesrequire('pkg-a')resolves to the copied location at runtimeIn exe mode, these copied packages would additionally be mapped into
sea-config.jsonassets.tsdown handles the complexity here — dependency graph resolution,
node_moduleslayout, and ensuring the copied branch is self-contained.