Bundle your TypeScript library with no config, powered by esbuild.
What can it bundle?
Anything that's supported by Node.js natively, namely .js
, .json
, .mjs
. And TypeScript .ts
, .tsx
. CSS support is experimental.
Install
Install it locally in your project folder:
npm i tsup -D
# Or Yarn
yarn add tsup --dev
You can also install it globally but it's not recommended.
Usage
Bundle files
tsup [...files]
Files are written into ./dist
.
You can bundle multiple files in one go:
tsup src/index.ts src/cli.ts
This will output dist/index.js
and dist/cli.js
.
Excluding packages
By default tsup bundles all import
-ed modules but dependencies
and peerDependencies
in your package.json
are always excluded, you can also use --external <module|pkgJson>
flag to mark other packages or other special package.json
's dependencies
and peerDependencies
as external.
Excluding all packages
If you are using tsup to build for Node.js applications/APIs, usually bundling dependencies is not needed, and it can even break things, for instance, while outputting to ESM.
tsup automatically excludes packages specified in the dependencies
and peerDependencies
fields in the package.json
, but if it somehow doesn't exclude some packages, this library also has a special executable tsup-node
that automatically skips bundling any Node.js package.
tsup-node src/index.ts
All other CLI flags still apply to this command. You can still use the noExternal
option to reinclude packages in the bundle,
for example packages that belong to a local monorepository.
If the regular tsup
command doesn't work for you, please submit an issue with a link to your repo so we can make the default command better.
Using custom configuration
You can also use tsup
using file configurations or in a property inside your package.json
, and you can even use TypeScript
and have type-safety while you are using it.
You can use any of these files:
tsup.config.ts
tsup.config.js
tsup.config.cjs
tsup.config.json
tsup
property in yourpackage.json
You can also specify a custom filename using the --config
flag, or passing --no-config
to disable config files.
Check out all available options.
TypeScript / JavaScript
import { defineConfig } from 'tsup'
export default defineConfig({
entry: ['src/index.ts'],
splitting: false,
sourcemap: true,
clean: true,
})
Conditional config
If the config needs to be conditionally determined based on CLI flags, it can export a function instead:
import { defineConfig } from 'tsup'
export default defineConfig((options) => {
return {
minify: !options.watch,
}
})
The options
here is derived from CLI flags.
package.json
{
"tsup": {
"entry": ["src/index.ts"],
"splitting": false,
"sourcemap": true,
"clean": true
},
"scripts": {
"build": "tsup"
}
}
JSON Schema Store
Developers who are using vscode or text editor which supports the JSON Language Server can leverage the tsup schema store via CDN. This schema store will provide intellisense capabilities such as completions, validations and descriptions within JSON file configurations like the tsup.config.json
and package.json
(tsup) property.
Provide the following configuration in your .vscode/settings.json
(or global) settings file:
{
"json.schemas": [
{
"url": "https://cdn.jsdelivr.net/npm/tsup/schema.json",
"fileMatch": [
"package.json",
"tsup.config.json"
]
}
]
}
Multiple entrypoints
Beside using positional arguments tsup [...files]
to specify multiple entrypoints, you can also use the cli flag --entry
:
# Outputs `dist/a.js` and `dist/b.js`.
tsup --entry src/a.ts --entry src/b.ts
The associated output file names can be defined as follows:
# Outputs `dist/foo.js` and `dist/bar.js`.
tsup --entry.foo src/a.ts --entry.bar src/b.ts
It's equivalent to the following tsup.config.ts
:
export default defineConfig({
// Outputs `dist/a.js` and `dist/b.js`.
entry: ['src/a.ts', 'src/b.ts'],
// Outputs `dist/foo.js` and `dist/bar.js`
entry: {
foo: 'src/a.ts',
bar: 'src/b.ts',
},
})
Generate declaration file
tsup index.ts --dts
This will emit ./dist/index.js
and ./dist/index.d.ts
. When emitting multiple bundle formats, one declaration file per bundle format is generated. This is required for consumers to get accurate type checking with TypeScript. Note that declaration files generated by any tool other than tsc
are not guaranteed to be error-free, so it's a good idea to test the output with tsc
or a tool like @arethetypeswrong/cli before publishing.
If you have multiple entry files, each entry will get a corresponding .d.ts
file. So when you only want to generate declaration file for a single entry, use --dts <entry>
format, e.g. --dts src/index.ts
.
Note that --dts
does not resolve external (aka in node_modules
) types used in the .d.ts
file, if that's somehow a requirement, try the experimental --dts-resolve
flag instead.
Emit declaration file only
The --dts-only
flag is the equivalent of the emitDeclarationOnly
option in tsc
. Using this flag will only emit the declaration file, without the JavaScript files.
Generate TypeScript declaration maps (.d.ts.map)
TypeScript declaration maps are mainly used to quickly jump to type definitions in the context of a monorepo (see source issue and official documentation).
They should not be included in a published NPM package and should not be confused with sourcemaps.
Tsup is not able to generate those files. Instead, you should use the TypeScript compiler directly, by running the following command after the build is done: tsc --emitDeclarationOnly --declaration
.
You can combine this command with Tsup onSuccess
callback.
Generate sourcemap file
tsup index.ts --sourcemap
This will emit ./dist/index.js
and ./dist/index.js.map
.
If you set multiple entry files, each entry will get a corresponding .map
file.
If you want to inline sourcemap, you can try:
tsup index.ts --sourcemap inline
Bundle formats
Supported format: esm
, cjs
, (default) and iife
.
You can bundle in multiple formats in one go:
tsup src/index.ts --format esm,cjs,iife
That will output files in following folder structure:
dist
├── index.mjs # esm
├── index.global.js # iife
└── index.js # cjs
If the type
field in your package.json
is set to module
, the filenames will be slightly different:
dist
├── index.js # esm
├── index.global.js # iife
└── index.cjs # cjs
Read more about esm
support in Node.js.
If you don't want extensions like .mjs
or .cjs
, e.g. you want your library to be used in a bundler (or environment) that doesn't support those, you can enable --legacy-output
flag:
tsup src/index.ts --format esm,cjs,iife --legacy-output
..which outputs to:
dist
├── esm
│ └── index.js
├── iife
│ └── index.js
└── index.js
Output extension
You can also change the output extension of the files by using outExtension
option:
export default defineConfig({
outExtension({ format }) {
return {
js: `.${format}.js`,
}
},
})
This will generate your files to [name].[format].js
.
The signature of outExtension
is:
type OutExtension = (ctx: Context) => Result
type Context = {
options: NormalizedOptions
format: Format
/** "type" field in project's package.json */
pkgType?: string
}
type Result = { js?: string }
Code Splitting
Code splitting currently only works with the esm
output format, and it's enabled by default. If you want code splitting for cjs
output format as well, try using --splitting
flag which is an experimental feature to get rid of the limitation in esbuild.
To disable code splitting altogether, try the --no-splitting
flag instead.
Target environment
You can use the target
option in tsup.config.ts
or the --target
flag to set the target environment for the generated JavaScript and/or CSS code. Each target environment is an environment name followed by a version number. The following environment names are currently supported:
- chrome
- edge
- firefox
- hermes
- ie
- ios
- node
- opera
- rhino
- safari
In addition, you can also specify JavaScript language versions such as es2020
.
The value for target
defaults to compilerOptions.target
in your tsconfig.json
, or node14
if unspecified. For more information check out esbuild's target option.
ES5 support
You can use --target es5
to compile the code down to es5, in this target your code will be transpiled by esbuild to es2020 first, and then transpiled to es5 by SWC.
Compile-time environment variables
You can use --env
flag to define compile-time environment variables:
tsup src/index.ts --env.NODE_ENV production
Building CLI app
When an entry file like src/cli.ts
contains hashbang like #!/bin/env node
tsup will automatically make the output file executable, so you don't have to run chmod +x dist/cli.js
.
Watch mode
tsup src/index.ts --watch
Turn on watch mode. This means that after the initial build, tsup will continue to watch for changes in any of the resolved files.
tsup src/index.ts --watch --ignore-watch ignore-this-folder-too
onSuccess
You can specify command to be executed after a successful build, specially useful for Watch mode
tsup src/index.ts --watch --onSuccess "node dist/index.js"
onSuccess
can also be a function
that returns Promise
. For this to work, you need to use tsup.config.ts
instead of the cli flag:
import { defineConfig } from 'tsup'
export default defineConfig({
async onSuccess() {
// Start some long running task
// Like a server
},
})
You can return a cleanup function in onSuccess
:
import { defineConfig } from 'tsup'
export default defineConfig({
async onSuccess() {
const server = http.createServer((req, res) => {
res.end('Hello World!')
})
server.listen(3000)
return () => {
server.close()
}
},
})
Minify output
You can also minify the output, resulting into lower bundle sizes by using the --minify
flag.
tsup src/index.ts --minify
To use Terser instead of esbuild for minification, pass terser as argument value
tsup src/index.ts --minify terser
In tsup.config.js
, you can pass terserOptions
which will be passed to terser.minify
as it is.
Custom loader
Esbuild loader list:
type Loader =
| 'js'
| 'jsx'
| 'ts'
| 'tsx'
| 'css'
| 'json'
| 'text'
| 'base64'
| 'file'
| 'dataurl'
| 'binary'
| 'copy'
| 'default'
To use a custom loader via CLI flag:
tsup --loader ".jpg=base64" --loader ".webp=file"
Or via tsup.config.ts
:
import { defineConfig } from 'tsup'
export default defineConfig({
loader: {
'.jpg': 'base64',
'.webp': 'file',
},
})
Tree shaking
esbuild has tree shaking enabled by default, but sometimes it's not working very well, see #1794 #1435, so tsup offers an additional option to let you use Rollup for tree shaking instead:
tsup src/index.ts --treeshake
This flag above will enable Rollup for tree shaking, and it's equivalent to the following tsup.config.ts
:
import { defineConfig } from 'tsup'
export default defineConfig({
treeshake: true,
})
This option has the same type as the treeshake
option in Rollup, see more.
What about type checking?
esbuild is fast because it doesn't perform any type checking, you already get type checking from your IDE like VS Code or WebStorm.
Additionally, if you want type checking at build time, you can enable --dts
, which will run a real TypeScript compiler to generate declaration file so you get type checking as well.
CSS support
esbuild has experimental CSS support, and tsup allows you to use PostCSS plugins on top of native CSS support.
To use PostCSS, you need to install PostCSS:
yarn add postcss --dev
..and populate a postcss.config.js
in your project
module.exports = {
plugins: [require('tailwindcss')(), require('autoprefixer')()],
}
Metafile
Passing --metafile
flag to tell esbuild to produce some metadata about the build in JSON format. You can feed the output file to analysis tools like bundle buddy to visualize the modules in your bundle and how much space each one takes up.
The file outputs as metafile-{format}.json
, e.g. tsup --format cjs,esm
will generate metafile-cjs.json
and metafile-esm.json
.
Custom esbuild plugin and options
Use esbuildPlugins
and esbuildOptions
respectively in tsup.config.ts
:
import { defineConfig } from 'tsup'
export default defineConfig({
esbuildPlugins: [YourPlugin],
esbuildOptions(options, context) {
options.define.foo = '"bar"'
},
})
The context
argument for esbuildOptions
:
context.format
:cjs
,esm
,iife
See all options here, and how to write an esbuild plugin.
For more details:
tsup --help
Inject cjs and esm shims
Enabling this option will fill in some code when building esm/cjs to make it work, such as __dirname
which is only available in the cjs module and import.meta.url
which is only available in the esm module
import { defineConfig } from 'tsup'
export default defineConfig({
shims: true,
})
- When building the cjs bundle, it will compile
import.meta.url
astypeof document === "undefined" ? new URL("file:" + __filename).href : document.currentScript && document.currentScript.src || new URL("main.js", document.baseURI).href
- When building the esm bundle, it will compile
__dirname
aspath.dirname(fileURLToPath(import.meta.url))
Copy files to output directory
Use --publicDir
flag to copy files inside ./public
folder to the output directory.
You can also specify a custom directory using --publicDir another-directory
.
Troubleshooting
error: No matching export in "xxx.ts" for import "xxx"
This usually happens when you have emitDecoratorMetadata
enabled in your tsconfig.json, in this mode we use SWC to transpile decorators to JavaScript so exported types will be eliminated, that's why esbuild won't be able to find corresponding exports. You can fix this by changing your import statement from import { SomeType }
to import { type SomeType }
or import type { SomeType }
.
License
MIT © EGOIST