Skip to content

tsdown

npm version License: MIT Node.js tsdown

> Base tsdown configuration for jmlweb projects. The successor to @jmlweb/tsup-config-base (deprecated), with the same helper API. Recommended build config.

  • 🎯 Sensible defaults: Entry, dual CJS/ESM format, declarations, clean, dist
  • 🔁 Plain tsdown options: Every tsdown option goes at the top level; the presets only add defaults
  • 🧭 Stable file names: Keeps .js/.cjs/.d.ts/.d.cts so existing exports maps keep working
  • ⚡ Rolldown + Oxc: Fast builds, and declarations without the TypeScript compiler API when you opt in
  • 🖥️ CLI preset: Shebang injection, including per-entry shebangs in one config
Terminal window
npm install --save-dev @jmlweb/tsdown-config-base tsdown typescript

> typescript is needed for declarations unless you enable isolatedDeclarations (see Declaration Files).

Create tsdown.config.ts in your project root:

import { createTsdownConfig } from '@jmlweb/tsdown-config-base';
export default createTsdownConfig();
import { createTsdownConfig } from '@jmlweb/tsdown-config-base';
export default createTsdownConfig({
deps: {
neverBundle: ['@jmlweb/eslint-config-base-js', '@eslint/js', 'eslint'],
},
});
import { createTsdownConfig } from '@jmlweb/tsdown-config-base';
export default createTsdownConfig({
entry: { index: 'src/index.ts', utils: 'src/utils/index.ts' },
deps: { neverBundle: ['vitest'] },
minify: true,
sourcemap: true,
});
import { createTsdownCliConfig } from '@jmlweb/tsdown-config-base';
export default createTsdownCliConfig();
// Output: dist/cli.js with a #!/usr/bin/env node shebang
import { createTsdownCliConfig } from '@jmlweb/tsdown-config-base';
export default createTsdownCliConfig({
entry: { cli: 'src/cli.ts', index: 'src/index.ts' },
shebang: 'cli', // Only the cli entry gets a shebang
deps: { neverBundle: ['commander'] },
});
Setting Default Description
entry ['src/index.ts'] Entry point(s)
format ['cjs', 'esm'] Dual publishing
dts true Generate declarations
clean true Clean output before build
outDir 'dist' Output directory
fixedExtension false .js/.cjs by package.json type, not .mjs
checks { legacyCjs: false } Silences tsdown’s CommonJS warning (dual is on purpose)
target From engines.node Inferred by tsdown; without engines.node there is no syntax lowering
Option Type Default Description
entry string[] | Record<string, string> ['src/index.ts'] Entry point files
format ('cjs' | 'esm' | 'iife' | 'umd')[] ['cjs', 'esm'] Output formats
dts boolean | DtsOptions true Declaration files (or tsdown dts options)
clean boolean true Clean output before build
outDir string 'dist' Output directory
external (string | RegExp)[] [] Deprecated: use deps.neverBundle
options AdditionalOptions {} Deprecated: pass tsdown options at top level

Any other tsdown option is accepted at the top level.

createTsdownCliConfig(options?): UserConfig

Section titled “createTsdownCliConfig(options?): UserConfig”

Same options, with format defaulting to ['esm'] and entry to { cli: 'src/cli.ts' }, plus:

Option Type Default Description
target NodeTarget From engines.node Node.js target version
shebang boolean | string | string[] true Shebang on all entries (true) or the named ones

Shebang names are output names without extension (cli for dist/cli.js). An entry whose source already starts with a shebang keeps its own; no second one is added.

  • external maps to deps.neverBundle; passing both throws TsdownConfigError
  • Setting the same key at the top level and in options throws TsdownConfigError
  • checks: { legacyCjs: true } brings back the CommonJS warning; fixedExtension: true emits .mjs/.cjs (update your exports map)
  • createTsdownConfig, createTsdownCliConfig - Helpers
  • BASE_DEFAULTS, CLI_DEFAULTS - Default values
  • TsdownConfigError - Thrown on conflicting options
  • TsdownConfigOptions, TsdownCliConfigOptions, EntryConfig, NodeTarget, OutputFormat, DepsOptions - Types
  • AdditionalOptions, AdditionalCliOptions - Types of the deprecated options field
  • UserConfig - Re-exported from tsdown, also as Options
Area tsup-config-base tsdown-config-base
Helpers createTsupConfig, createTsupCliConfig createTsdownConfig, createTsdownCliConfig
Config file tsup.config.ts tsdown.config.ts
Return type Options (Options[] for selective shebang) Always one UserConfig
external tsup’s external Deprecated alias of deps.neverBundle
Selective shebang Split configs One config with a per-file banner
format cjs, esm, iife Adds umd
splitting, bundle In options Not available; use unbundle: true instead of bundle: false
noExternal In options deps.alwaysBundle
Declarations rollup-plugin-dts tsc, tsgo or Oxc
Node.js >= 22.12.0 ^22.18.0 || ^24.11.0 || >=26.0.0 (tsdown’s requirement)

tsdown picks the declaration generator automatically (verified with tsdown 0.23.0):

Project setup Generator Needs the TypeScript compiler API?
typescript 5.x / 6.x installed tsc Yes
typescript 7.x installed tsgo No: native binary (experimental in tsdown)
isolatedDeclarations: true in tsconfig.json oxc No: works without typescript (needs explicit export types)
Neither - Build fails: TypeScript is not installed

Force one with dts: { generator: 'oxc' } (or 'tsc' / 'tsgo').

> ⚠️ Much of the toolchain does not support TypeScript 7 yet (typescript-eslint <6.1.0, ts-jest <7, @astrojs/check ^5 || ^6). Stay on TypeScript 6 if you use them.

  • ✅ Building a TypeScript library with dual CJS/ESM output and declarations
  • ✅ Migrating from @jmlweb/tsup-config-base without changing published file names
  • ✅ Declaration generation ready for TypeScript 7
  • ✅ CLI packages with shebang injection

Staying on tsup? @jmlweb/tsup-config-base still works but is deprecated and unmaintained.

import { createTsdownConfig } from '@jmlweb/tsdown-config-base';
export default createTsdownConfig({
format: ['esm'], // ESM only
outDir: 'build',
fixedExtension: true, // Emit .mjs/.d.mts
unbundle: true, // Mirror src/ instead of bundling
deps: {
neverBundle: ['eslint'],
alwaysBundle: ['tiny-utils'],
},
});

tsdown already externalizes dependencies, peerDependencies and optionalDependencies; deps.neverBundle covers anything else.

{
"scripts": {
"build": "tsdown",
"clean": "rm -rf dist",
"prepublishOnly": "npm run build"
}
}
Terminal window
npm run build # Build the package
npm run clean # Clean build output
  • Node.js ^22.18.0, ^24.11.0 or >=26.0.0
  • tsdown ~0.23.0
  • TypeScript 5.x, 6.x or 7.x (optional with isolatedDeclarations)
  • tsdown (~0.23.0)

tsdown is pre-1.0 and ships breaking changes in minor releases, so the range allows patch updates only. It is widened as tsdown releases are verified.

  1. Install @jmlweb/tsdown-config-base and tsdown; remove @jmlweb/tsup-config-base and tsup

  2. Rename tsup.config.ts to tsdown.config.ts and change the build script from tsup to tsdown

  3. Change the import and helper name, and move external to deps.neverBundle (external still works, deprecated):

    // Before
    import { createTsupConfig } from '@jmlweb/tsup-config-base';
    export default createTsupConfig({ external: ['eslint'] });
    // After
    import { createTsdownConfig } from '@jmlweb/tsdown-config-base';
    export default createTsdownConfig({
    deps: { neverBundle: ['eslint'] },
    });
  4. Move everything from options to the top level, and replace tsup-only options (splitting, bundle, noExternal, esbuildPlugins, …) using Differences

  5. Build and compare dist/ with the previous output

See the Changelog for version history.

MIT