This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
tsdown is a blazing-fast bundler for TypeScript libraries powered by Rolldown and Oxc. It's designed as a seamless migration path from tsup with enhanced performance and features.
Key technologies:
- Rolldown: Core bundler (Rust-based Rollup alternative)
- pnpm: Package manager (v10.33.0)
- Vitest: Testing framework
- TypeScript: Strict mode with isolated declarations enabled
- ESM: Pure ESM package (
"type": "module")
# Build tsdown using itself
pnpm build
# Development mode (runs tsdown directly via tsx)
pnpm dev# Run all tests in watch mode
pnpm test
# Run tests without watch
pnpm test run
# Run a specific test file
pnpm test <file-pattern>
# Example: pnpm test src/config/file.test.ts
# Run with UI
pnpm test --ui
# Generate coverage
pnpm test --coverage# Lint (ESLint with cache)
pnpm lint
# Fix lint issues
pnpm lint:fix
# Type check
pnpm typecheck
# Format code (Prettier)
pnpm format# Run docs dev server
pnpm docs:dev
# Build docs
pnpm docs:build
# Preview built docs
pnpm docs:previewCLI (src/cli.ts)
→ build() (src/build.ts)
→ resolveConfig() (src/config/index.ts)
→ buildWithConfigs()
→ buildSingle() for each config
→ Hook: build:prepare
→ cleanOutDir()
→ getBuildOptions() → constructs Rolldown config
→ Hook: build:before
→ rolldownBuild() / rolldownWatch()
→ postBuild() → copy files, bundle processing
→ Hook: build:done
→ executeOnSuccess()
Multi-stage resolution pipeline:
-
Load config file (
src/config/file.ts)- Searches for
tsdown.config.{ts,js,json}orpackage.json(tsdown field) - Supports multiple loaders:
native(Node.js native TS),unrun(transpiler),auto(intelligent selection) - Can load from Vite/Vitest configs via
fromViteoption
- Searches for
-
Resolve workspace (
src/config/workspace.ts)- Auto-detects monorepo packages via
package.jsonfiles - Supports glob patterns for workspace filtering
- Root config inherited by workspace packages
- Auto-detects monorepo packages via
-
Resolve user config (
src/config/options.ts)- Merges CLI overrides with user config
- Resolves entry points (supports globs and negation)
- Normalizes format arrays and package-based settings
Config multiplier: Final configs = (inline) × (root configs) × (workspace packages) × (sub-configs per package)
Three-phase lifecycle using hookable library (src/features/hooks.ts):
-
build:prepare- Before any build starts- Context:
{ options: ResolvedConfig, hooks: Hookable }
- Context:
-
build:before- Before Rolldown builds (per format)- Extended context:
{ buildOptions: BuildOptions }
- Extended context:
-
build:done- After build completes- Extended context:
{ chunks: RolldownChunk[] }
- Extended context:
Each feature is self-contained and modular:
Core:
rolldown.ts- Rolldown build options constructionhooks.ts- Hook system implementation usinghookable
Rolldown Plugins:
deps.ts- Dependency management, external/inline validationnode-protocol.ts- Handlesnode:protocol additions/strippingshebang.ts- Preserves shebang lines in outputreport.ts- Bundle size reportingwatch.ts- Watch mode change tracking
Transformations:
entry.ts- Entry point resolution with glob support (including negation!pattern)target.ts- Compilation targets from package.json or configtsconfig.ts- TypeScript configuration resolutioncjs.ts- CommonJS deprecation warnings
Output Processing:
output.ts- Chunk filename and extension resolutioncopy.ts- Copy static files to distclean.ts- Output directory cleanup
Advanced Features:
pkg/index.ts- Package bundling orchestrationpkg/exports.ts- Auto-generate package.json exports fieldpkg/publint.ts- Package lintingpkg/attw.ts- "Are the types wrong" integrationdebug.ts- Debug namespace managementdevtools.ts- Vite DevTools integrationexe.ts- Executable bundling (SEA support)shims.ts- ESM/CJS shim injectionshortcuts.ts- Watch mode keyboard shortcuts
Plugins follow Rolldown's interface. Internal plugins are added based on config, user plugins append last. The build supports dual-format output (ESM + CJS) with a second pass for CJS type declarations (cjsDts).
Public plugin exports in src/plugins.ts: DepsPlugin, NodeProtocolPlugin, ReportPlugin, ShebangPlugin, WatchPlugin
-
Multi-format builds: Single config produces ESM + CJS + types. ES format handles types via dts plugin; CJS format has separate dts pass with
emitDtsOnly: true -
Package-aware building: Detects package.json, auto-generates exports field, validates bundled dependencies, runs package linters
-
Lazy feature loading: Optional peer dependencies loaded on-demand (
@tsdown/css, unplugin-unused, etc.) -
Watch mode coordination: Config file changes trigger full rebuild restart; file changes tracked per bundle; keyboard shortcuts for manual rebuild/exit
-
Workspace monorepo support: Root config inherited by workspace packages; each package gets own resolved config
Global setup: tests/setup.ts
- Auto-cleanup of temp directories before each test
- Mocks
console.warnto track warnings - Custom matcher:
expect(message).toHaveBeenWarned() - Throws error for unexpected warnings after each test
Test utilities: tests/utils.ts
testBuild()- Main helper for testing builds- Writes fixtures to temp directory
- Runs build with provided config
- Captures warnings and output
- Generates snapshot comparison
writeFixtures()- Write test files or load fromtests/fixtures/getTestDir()- Get temp directory for testchdir()- Temporarily change working directory
Test files are co-located with source files:
src/config/file.ts
src/config/file.test.ts
Example test pattern:
import { describe, expect, it } from 'vitest'
import { testBuild } from '../tests/utils.ts'
describe('feature name', () => {
it('should do something', async (context) => {
const { snapshot, warnings } = await testBuild({
context,
files: {
'index.ts': 'export const foo = "bar"',
},
options: {
format: 'esm',
dts: true,
},
})
expect(snapshot).toMatchFileSnapshot()
expect(warnings).toHaveLength(0)
})
})Snapshot testing: Uses expectFilesSnapshot from @sxzz/test-utils to compare output files against snapshots in tests/__snapshots__/
vitest.config.tssets 20s timeout, ignorestemp/directories- Setup file runs before each test
- Coverage includes
src/**only - Inline deps:
tinyglobby,fdir(for fs mocking)
Entry points support:
- Single file:
'index.ts' - Array:
['index.ts', 'cli.ts'] - Globs:
'src/*.ts' - Negation:
['src/*.ts', '!src/*.test.ts']
native- Use Node.js native TypeScript support (Node 23+, Bun, Deno)unrun- Use TypeScript transpiler (compatible with all Node versions)auto- Automatically choose based on environment (default)
When format: ['esm', 'cjs']:
- First pass builds both formats with shared types from ESM build
- If CJS needs separate types, second pass runs with
emitDtsOnly: true
tsdown detects package.json in the working directory to:
- Infer
type(ESM vs CJS) - Auto-generate
exportsfield whenexports: true - Validate external dependencies
- Run package validators (publint, attw)
- Strict mode enabled with
isolatedDeclarations: true - All exports must have explicit types
verbatimModuleSyntax: trueenforces explicit import types
fs.ts- File system utilities (fsExists(),fsStat(),fsRemove()); use instead of Node.js fs directlylogger.ts- Structured logging (logger.error(),.warn(),.info(),.debug()) with colours viaansis; respectslogLevelconfigchunks.ts- Chunk manipulation utilitiesformat.ts- Formatting utilities (byte sizes, etc.)general.ts- General utilities (glob resolution, type checking, etc.)package.ts- Package.json reading and manipulationtypes.ts- Shared TypeScript type definitions
Watch mode has special behaviors:
- Config file changes trigger full restart (clears module cache)
- Keyboard shortcuts:
r(manual rebuild),q(quit) - Build errors don't stop watch mode
- Resources cleaned via
AsyncDisposablepattern