@code-fixer-23/nx-tsup
v1.1.0
Published
An **NX plugin** for building TypeScript libraries using **[Tsup](https://tsup.egoist.sh/)** - the fastest way to bundle your TypeScript libraries with zero config.
Readme
@code-fixer-23/nx-tsup
An NX plugin for building TypeScript libraries using Tsup - the fastest way to bundle your TypeScript libraries with zero config.
⚠️ Breaking Changes in v0.1.0
Important: The outputPath option has been renamed to outDir to align with tsup's native naming conventions.
If you're upgrading from v0.0.x, run the automatic migration:
nx migrate @code-fixer-23/nx-tsup@latest
nx migrate --run-migrationsThis will automatically update all your project.json files.
Features
- 🚀 Fast Bundling - Powered by esbuild through Tsup
- 📦 Zero Configuration - Sensible defaults, works out of the box
- 🎯 Type-Safe - Automatic TypeScript declaration file generation
- 🔧 Config Merging - Smart merging of
tsup.config.tsandproject.jsonoptions - 🧪 Test Integration - Optional Vitest or Jest setup with auto-detection
- 🎨 Linter Support - Optional ESLint or Biome integration
- ✨ Formatter Support - Optional Prettier, Biome, or ESLint Stylistic integration
- ⚙️ Advanced Options - Full tsup feature support (splitting, treeshake, external, etc.)
- 📚 Monorepo Ready - Works with both integrated and package-based monorepos
Installation
Prerequisites
Ensure you have an NX workspace set up. If not, create one:
npx create-nx-workspace@latestInstall the Plugin
# Using pnpm (recommended)
pnpm add -D @code-fixer-23/nx-tsup tsup
# Or using npm
npm install --save-dev @code-fixer-23/nx-tsup tsupUsage
Generate a New Library
Create a new Tsup-powered library:
# Interactive mode (recommended)
nx generate @code-fixer-23/nx-tsup:library
# With options
nx generate @code-fixer-23/nx-tsup:library my-lib \\
--importPath=@my-scope/my-lib \\
--description="My awesome library" \\
--testRunner=vitest \\
--linter=eslint \\
--formatter=prettierGenerator Options
| Option | Type | Default | Description |
| ------------- | ----------------------------------------------------- | -------------------------------- | ----------------------------------------- |
| name | string | required | Library name (kebab-case) |
| importPath | string | required | Import path (e.g., @scope/package-name) |
| directory | string | packages | Directory where library will be created |
| description | string | "" | Package description |
| testRunner | vitest | jest | none | auto-detect (fallback: jest) | Test framework to use |
| linter | eslint | biome | none | auto-detect (fallback: eslint) | Linter to configure |
| formatter | prettier | biome | eslint-stylistic | none | auto-detect | Code formatter to use |
| skipFormat | boolean | false | Skip formatting generated files |
Build Your Library
# Build a specific library
nx build my-lib
# Build all libraries
nx run-many -t build
# Build with watch mode
nx build my-lib --watchExecutor Options
The build executor supports the following options:
Core Options
| Option | Type | Default | Description |
| ---------- | -------------------------- | ---------- | --------------------------------- |
| outDir | string | required | Output directory for built files |
| main | string | required | Entry point file |
| tsConfig | string | required | Path to tsconfig file |
| format | ('esm'\|'cjs'\|'iife')[] | ['esm'] | Output formats (CLI-only) |
| watch | boolean | false | Enable watch mode (CLI-only) |
| assets | string[] | [] | Additional assets to copy to dist |
Build Options
| Option | Type | Default | Description |
| ----------- | ---------------------------------------- | --------- | ------------------------------------- |
| dts | boolean | true | Generate TypeScript declaration files |
| clean | boolean | true | Clean output directory before build |
| minify | boolean | false | Minify output |
| sourcemap | boolean \| 'inline' | false | Generate sourcemaps |
| splitting | boolean | false | Enable code splitting (ESM only) |
| treeshake | boolean \| 'smallest' \| 'recommended' | false | Enable tree shaking |
| target | string | es2022 | ECMAScript target (e.g., 'esnext') |
| platform | 'node' \| 'browser' \| 'neutral' | neutral | Target platform |
Dependency Options
| Option | Type | Default | Description |
| ------------ | ---------- | ------- | --------------------------------------- |
| external | string[] | [] | External dependencies to exclude |
| noExternal | string[] | [] | Dependencies to force include in bundle |
Advanced Options
| Option | Type | Default | Description |
| ---------------- | ----------------------- | ------- | ---------------------------------------- |
| banner | object | {} | Code to prepend (e.g., {js: '// ...'}) |
| footer | object | {} | Code to append |
| env | Record<string,string> | {} | Environment variables to define |
| define | Record<string,string> | {} | Global constants to define |
| inject | string[] | [] | Files to automatically inject |
| esbuildOptions | object | {} | Additional esbuild options |
| esbuildPlugins | string[] | [] | Paths to esbuild plugin modules |
Note: watch and format are CLI-only options and should not be defined in project.json. All other options can be configured in both project.json and tsup.config.ts, with project.json taking precedence.
Generated Project Structure
When you generate a library, the following structure is created:
packages/my-lib/
├── src/
│ ├── index.ts # Entry point
│ └── index.spec.ts # Example test (if test runner selected)
├── dist/ # Build output (after running build)
│ ├── index.js # ESM bundle
│ └── index.d.ts # Type declarations
├── tsup.config.ts # Tsup configuration
├── tsconfig.json # TypeScript config (project references)
├── tsconfig.lib.json # TypeScript config (lib-specific)
├── package.json # Package manifest with exports
├── README.md # Package documentation
├── vitest.config.ts # Vitest config (if selected)
├── jest.config.ts # Jest config (if selected)
├── eslint.config.mjs # ESLint config (if selected)
├── .prettierrc.json # Prettier config (if selected)
├── .prettierignore # Prettier ignore file (if selected)
└── biome.json # Biome config (if linter/formatter selected)Auto-detection behavior
When you omit --testRunner, --linter, and/or --formatter, the generator inspects your workspace root package.json to detect installed tools by their official package names:
- Test runners:
jest,vitest - Linters:
eslint,@biomejs/biome - Formatters:
prettier,@biomejs/biome,@stylistic/eslint-plugin
Selection rules:
- If exactly one candidate is present, it is selected automatically.
- If multiple candidates are present, you will be prompted to choose in interactive mode. In non-interactive/CI environments:
- Test runner fallback:
jest - Linter fallback:
eslint - Formatter fallback:
prettier(for ESLint),biome(for Biome linter), ornoneotherwise
- Test runner fallback:
- If none are present, the defaults are
jest,eslint, andprettier. - Special rule: ESLint Stylistic requires ESLint as the linter. If you select
eslint-stylisticwith a different linter, it will fall back toprettier. - Special rule: If Biome is selected as the linter and no formatter is specified, Biome will be used as the formatter (since Biome handles both linting and formatting).
To override detection, pass explicit flags, e.g. --testRunner=vitest --linter=eslint --formatter=prettier.
Configuration
Tsup Configuration
Each generated library includes a tsup.config.ts file at its root. You can customize the build process by modifying this file:
// packages/my-lib/tsup.config.ts
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'], // Add CJS format
dts: true,
clean: true,
sourcemap: true, // Enable sourcemaps
minify: true, // Enable minification
target: 'es2022',
splitting: false,
treeshake: true,
});Configuration Merging Strategy
🆕 New in v0.1.0: The plugin now intelligently merges options from tsup.config.ts and project.json, giving you the best of both worlds.
How It Works
- Config File Discovery: The executor searches for
tsup.config.{ts,mts,cts,js,mjs,cjs}in your project root - Smart Merging: Options from
project.jsonoverride those intsup.config.ts - TypeScript Support:
.tsconfig files are compiled on-the-fly using esbuild - Function Configs: Supports function-based configs with environment parameters
Supported Config Files (in order of precedence)
tsup.config.tstsup.config.mtstsup.config.ctstsup.config.jstsup.config.mjstsup.config.cjs
Merge Rules
- Primitives (boolean, string, number):
project.jsonvalue replaces config file value - Arrays:
project.jsonarray replaces config file array (no concatenation) - Objects (
banner,footer,env,define): Deep merge withproject.jsonwinning esbuildOptions: Composed as a function chain,project.jsonapplied last- CLI-only flags (
watch,format): Applied at runtime, not merged
Example: Basic Merging
tsup.config.ts:
import { defineConfig } from 'tsup';
export default defineConfig({
target: 'es2020',
splitting: false,
banner: { js: '// Copyright 2025' },
});project.json:
{
"targets": {
"build": {
"executor": "@code-fixer-23/nx-tsup:build",
"options": {
"outDir": "packages/my-lib/dist",
"main": "packages/my-lib/src/index.ts",
"tsConfig": "packages/my-lib/tsconfig.lib.json",
"target": "esnext",
"minify": true
}
}
}
}Result: Target is esnext (from project.json), splitting is false (from config), minify is true (from project.json), and banner is preserved.
Example: Function-based Config
import { defineConfig } from 'tsup';
export default defineConfig((options) => ({
target: options.watch ? 'es2022' : 'esnext',
minify: !options.watch,
dts: true,
}));The function receives { watch, format, mode } parameters.
Example: Advanced - esbuildOptions & Plugins
project.json:
{
"options": {
"outDir": "dist",
"main": "src/index.ts",
"tsConfig": "tsconfig.lib.json",
"external": ["react", "react-dom"],
"esbuildOptions": {
"keepNames": true
},
"esbuildPlugins": ["./esbuild-plugins/my-plugin.js"]
}
}Plugins are loaded from paths relative to the project root.
Package.json Exports
Generated libraries automatically include proper exports configuration:
{
"name": "@my-scope/my-lib",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
}
}Monorepo Support
Integrated Monorepo (Default)
In an integrated monorepo, tsup is installed at the workspace root and shared across all packages. This is the default configuration.
Package-based Monorepo
If your package.json includes a workspaces field, the generator automatically:
- Detects the package-based setup
- Adds
tsupto each generated package'sdevDependencies - Ensures each package can build independently
Advanced Usage
Multiple Entry Points
Edit your tsup.config.ts to support multiple entry points:
export default defineConfig({
entry: {
index: 'src/index.ts',
cli: 'src/cli.ts',
},
format: ['esm'],
dts: true,
});Custom Build Target
Override build options in your project.json:
{
"targets": {
"build": {
"executor": "@code-fixer-23/nx-tsup:build",
"options": {
"outDir": "packages/my-lib/dist",
"main": "packages/my-lib/src/index.ts",
"tsConfig": "packages/my-lib/tsconfig.lib.json",
"minify": true,
"sourcemap": true,
"splitting": true,
"treeshake": "smallest",
"external": ["react"]
}
}
}
}Note: Use --format and --watch as CLI flags: nx build my-lib --format=esm,cjs --watch
Testing
Run Tests
# Run tests for a specific library
nx test my-lib
# Run all tests
nx run-many -t test
# Run tests in watch mode
cd packages/my-lib && pnpm vitestLinting
Run Linter
# Lint a specific library
nx lint my-lib
# Lint all libraries
nx run-many -t lintType Checking
# Type-check a specific library
nx typecheck my-lib
# Type-check all libraries
nx run-many -t typecheckFormatting
If you selected a formatter during generation, you can format your code:
# Format a specific library
nx format my-lib
# Format all libraries
nx run-many -t formatFormatter Options
The generator supports three formatter options:
Prettier
Creates .prettierrc.json and .prettierignore files with sensible defaults:
- Semi-colons enabled
- Single quotes
- Tab width: 2
- Trailing commas: ES5
- Print width: 80
- Arrow parens: always
Biome
Uses Biome for both linting and formatting. Creates biome.json with formatter enabled.
ESLint Stylistic
Requires ESLint as linter. Uses @stylistic/eslint-plugin for formatting through ESLint.
Configures eslint.config.mjs with stylistic rules:
- Indent: 2 spaces
- Quotes: single
- Semi-colons: always
None
No formatter configuration or dependencies will be added.
Troubleshoots
Tsup not found
Ensure tsup is installed at the workspace root:
pnpm add -D -w tsupBuild fails with module resolution errors
Check your tsconfig.lib.json compiler options. Ensure moduleResolution is set correctly for your target environment.
Contributing
Contributions are welcome! Please open an issue or submit a pull request.
License
MIT
