Showing preview only (253K chars total). Download the full file or copy to clipboard to get everything.
Repository: egoist/tsup
Branch: main
Commit: b906f86102c0
Files: 75
Total size: 236.0 KB
Directory structure:
gitextract_11at8itm/
├── .devcontainer/
│ ├── Dockerfile
│ └── devcontainer.json
├── .editorconfig
├── .gitattributes
├── .github/
│ ├── renovate.json5
│ └── workflows/
│ ├── ci.yml
│ ├── format.yml
│ ├── release-continuous.yml
│ └── release.yml
├── .gitignore
├── .prettierignore
├── .prettierrc
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── assets/
│ ├── cjs_shims.js
│ ├── esm_shims.js
│ └── package.json
├── docs/
│ ├── README.md
│ └── index.html
├── package.json
├── schema.json
├── src/
│ ├── api-extractor.ts
│ ├── cli-default.ts
│ ├── cli-main.ts
│ ├── cli-node.ts
│ ├── errors.ts
│ ├── esbuild/
│ │ ├── external.ts
│ │ ├── index.ts
│ │ ├── native-node-module.ts
│ │ ├── node-protocol.ts
│ │ ├── postcss.ts
│ │ ├── svelte.ts
│ │ ├── swc.test.ts
│ │ └── swc.ts
│ ├── exports.ts
│ ├── fs.ts
│ ├── index.ts
│ ├── lib/
│ │ ├── public-dir.ts
│ │ └── report-size.ts
│ ├── load.ts
│ ├── log.ts
│ ├── options.ts
│ ├── plugin.ts
│ ├── plugins/
│ │ ├── cjs-interop.ts
│ │ ├── cjs-splitting.ts
│ │ ├── shebang.ts
│ │ ├── size-reporter.ts
│ │ ├── swc-target.ts
│ │ ├── terser.ts
│ │ └── tree-shaking.ts
│ ├── rollup/
│ │ └── ts-resolve.ts
│ ├── rollup.ts
│ ├── run.ts
│ ├── tsc.ts
│ └── utils.ts
├── test/
│ ├── __snapshots__/
│ │ ├── css.test.ts.snap
│ │ ├── dts.test.ts.snap
│ │ ├── index.test.ts.snap
│ │ └── tsconfig.test.ts.snap
│ ├── css.test.ts
│ ├── dts.test.ts
│ ├── example.test.ts
│ ├── experimental-dts.test.ts
│ ├── graphql.test.ts
│ ├── index.test.ts
│ ├── package.json
│ ├── shims.test.ts
│ ├── svelte.test.ts
│ ├── tsconfig.test.ts
│ └── utils.ts
├── tsconfig.json
├── tsup.config.ts
├── vitest-global.ts
└── vitest.config.mts
================================================
FILE CONTENTS
================================================
================================================
FILE: .devcontainer/Dockerfile
================================================
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.222.0/containers/javascript-node/.devcontainer/base.Dockerfile
# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 16, 14, 12, 16-bullseye, 14-bullseye, 12-bullseye, 16-buster, 14-buster, 12-buster
ARG VARIANT="16-bullseye"
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>
# [Optional] Uncomment if you want to install an additional version of node using nvm
# ARG EXTRA_NODE_VERSION=10
# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
# [Optional] Uncomment if you want to install more global node modules
RUN su node -c "npm install -g pnpm"
================================================
FILE: .devcontainer/devcontainer.json
================================================
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.222.0/containers/javascript-node
{
"name": "Node.js",
"build": {
"dockerfile": "Dockerfile",
// Update 'VARIANT' to pick a Node version: 16, 14, 12.
// Append -bullseye or -buster to pin to an OS version.
// Use -bullseye variants on local arm64/Apple Silicon.
"args": { "VARIANT": "16-bullseye" }
},
// Set *default* container specific settings.json values on container create.
"settings": {},
// Add the IDs of extensions you want installed when the container is created.
"extensions": ["dbaeumer.vscode-eslint"],
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "pnpm install",
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "node"
}
================================================
FILE: .editorconfig
================================================
root = true
[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false
================================================
FILE: .gitattributes
================================================
* text=auto eol=lf
================================================
FILE: .github/renovate.json5
================================================
{
extends: ['config:recommended', 'schedule:weekly', 'group:allNonMajor'],
labels: ['dependencies'],
rangeStrategy: 'bump',
packageRules: [
{
matchDepTypes: ['peerDependencies'],
enabled: false,
},
],
ignoreDeps: ['node'],
postUpdateOptions: ['pnpmDedupe'],
}
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node-version: [18, 20, 22]
runs-on: ${{ matrix.os }}
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4.1.0
name: Install pnpm
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: pnpm
- name: Install deps
run: pnpm i
# Runs a set of commands using the runners shell
- name: Build and Test
run: pnpm test
================================================
FILE: .github/workflows/format.yml
================================================
name: Fix
on:
push:
branches-ignore:
- main
- dev
jobs:
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4.1.0
name: Install pnpm
- uses: actions/setup-node@v4
with:
node-version: lts/*
cache: pnpm
- run: pnpm i
- name: Format
run: pnpm run format
- name: Commit files and push
continue-on-error: true
if: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/dev' }}
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add --all
git commit -m "chore(ci): [bot] format code"
git push
================================================
FILE: .github/workflows/release-continuous.yml
================================================
name: Publish Any Commit
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- uses: pnpm/action-setup@v4.1.0
name: Install pnpm
- uses: actions/setup-node@v4
with:
node-version: lts/*
cache: pnpm
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm build
- run: pnpx pkg-pr-new publish
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
uses: sxzz/workflows/.github/workflows/release.yml@v1
with:
publish: true
permissions:
contents: write
id-token: write
================================================
FILE: .gitignore
================================================
node_modules
*.log
dist
.cache
playground
.idea
.DS_Store
.eslintcache
================================================
FILE: .prettierignore
================================================
pnpm-lock.yaml
.cache
dist
================================================
FILE: .prettierrc
================================================
{
"singleQuote": true,
"semi": false
}
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing
## Making changes
1. Fork the repository.
2. Make changes.
3. Add tests in `test/`.
4. Run tests with `pnpm test`.
## Release changes
1. Merge PRs into dev branch.
2. Merge dev branch into main branch with `git checkout main && git merge dev`
3. Push main branch to remote with `git push`
4. GitHub action will create a release and publish it to npm.
Feel free to improve this process by creating an issue or PR.
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2021 EGOIST
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
> [!WARNING]
> This project is not actively maintained anymore. Please consider using [tsdown](https://github.com/rolldown/tsdown/) instead. Read more in [the migration guide](https://tsdown.dev/guide/migrate-from-tsup).
# tsup
[](https://npm.im/tsup) [](https://npm.im/tsup)
Bundle your TypeScript library with no config, powered by [esbuild](https://github.com/evanw/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](https://tsup.egoist.dev/#css-support).
## ⚙️ Install
Install it locally in your project folder:
```bash
npm i tsup -D
# Or Yarn
yarn add tsup --dev
# Or pnpm
pnpm add tsup -D
```
You can also install it globally but it's not recommended.
## 📖 Usage
### Bundle files
```bash
tsup [...files]
```
Files are written into `./dist`.
You can bundle multiple files in one go:
```bash
tsup src/index.ts src/cli.ts
```
This will output `dist/index.js` and `dist/cli.js`.
## 📚 Documentation
For complete usages, please dive into the [docs](https://tsup.egoist.dev).
For all configuration options, please see [the API docs](https://jsdocs.io/package/tsup).
## 💬 Discussions
Head over to the [discussions](https://github.com/egoist/tsup/discussions) to share your ideas.
## Sponsors
<p align="center">
<a href="https://chromatic.com" target="_blank"><picture>
<source media="(prefers-color-scheme: dark)" width="500" srcset="https://fastly.jsdelivr.net/gh/egoist-bot/images@main/uPic/Group 2 (2).png">
<img alt="Ship UIs faster with automated workflows for Storybook" width="500" src="https://fastly.jsdelivr.net/gh/egoist-bot/images@main/uPic/Group%202%20(1).png">
</picture></a>
</p>
<p align="center">
<a href="https://github.com/sponsors/egoist" target="_blank"><img src="https://sponsors-images.egoist.dev/sponsors.svg" alt="sponsors"></a>
</p>
## Project Stats

## License
MIT © [EGOIST](https://github.com/sponsors/egoist)
================================================
FILE: assets/cjs_shims.js
================================================
// Shim globals in cjs bundle
// There's a weird bug that esbuild will always inject importMetaUrl
// if we export it as `const importMetaUrl = ... __filename ...`
// But using a function will not cause this issue
const getImportMetaUrl = () =>
typeof document === "undefined"
? new URL(`file:${__filename}`).href
: (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT')
? document.currentScript.src
: new URL("main.js", document.baseURI).href;
export const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()
================================================
FILE: assets/esm_shims.js
================================================
// Shim globals in esm bundle
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const getFilename = () => fileURLToPath(import.meta.url)
const getDirname = () => path.dirname(getFilename())
export const __dirname = /* @__PURE__ */ getDirname()
export const __filename = /* @__PURE__ */ getFilename()
================================================
FILE: assets/package.json
================================================
{
"sideEffects": false
}
================================================
FILE: docs/README.md
================================================
```js preact
import { html } from 'docup'
export default () => {
const isPreview = location.hostname !== 'tsup.egoist.dev'
if (!isPreview) return null
return html`
<div class="message message_type__warning">
This is a preview version of the docs.
</div>
`
}
```
Bundle your TypeScript library with no config, powered by [esbuild](https://github.com/evanw/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](#css-support).
## Install
Install it locally in your project folder:
```bash
npm i tsup -D
# Or Yarn
yarn add tsup --dev
# Or pnpm
pnpm add tsup -D
```
You can also install it globally but it's not recommended.
## Usage
### Bundle files
```bash
tsup [...files]
```
Files are written into `./dist`.
You can bundle multiple files in one go:
```bash
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](https://nodejs.org/api/esm.html).
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.
```bash
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.
> INFO: Most of these options can be overwritten using the CLI options
You can use any of these files:
- `tsup.config.ts`
- `tsup.config.js`
- `tsup.config.cjs`
- `tsup.config.json`
- `tsup` property in your `package.json`
> INFO: In all the custom files you can export the options either as `tsup`, `default` or `module.exports =`
You can also specify a custom filename using the `--config` flag, or passing `--no-config` to disable config files.
[Check out all available options](https://jsdocs.io/package/tsup).
#### TypeScript / JavaScript
```ts
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:
```ts
import { defineConfig } from 'tsup'
export default defineConfig((options) => {
return {
minify: !options.watch,
}
})
```
The `options` here is derived from CLI flags.
#### package.json
```json
{
"tsup": {
"entry": ["src/index.ts"],
"splitting": false,
"sourcemap": true,
"clean": true
},
"scripts": {
"build": "tsup"
}
}
```
#### JSON Schema Store
Developers who are using [vscode](https://code.visualstudio.com/) or text editor which supports the JSON Language Server can leverage the [tsup schema store](https://cdn.jsdelivr.net/npm/tsup/schema.json) 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
{
"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`:
```bash
# 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:
```bash
# 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`:
```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
```bash
tsup index.ts --dts
```
This will emit `./dist/index.js` and `./dist/index.d.ts`. When emitting multiple [bundle formats](#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](https://www.npmjs.com/package/@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.
Since tsup version 8.0.0, you can also use `--experimental-dts` flag to generate declaration files. This flag use [@microsoft/api-extractor](https://www.npmjs.com/package/@microsoft/api-extractor) to generate declaration files, which is more reliable than the previous `--dts` flag. It's still experimental and we are looking for feedbacks.
To use `--experimental-dts`, you would need to install `@microsoft/api-extractor`, as it's a peer dependency of tsup:
```bash
npm i @microsoft/api-extractor -D
# Or Yarn
yarn add @microsoft/api-extractor --dev
```
#### 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](https://github.com/Microsoft/TypeScript/issues/14479) and [official documentation](https://www.typescriptlang.org/tsconfig/#declarationMap)).
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](https://github.com/egoist/tsup/issues/564). 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`](https://tsup.egoist.dev/#onsuccess) callback.
### Generate sourcemap file
```bash
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:
```bash
tsup index.ts --sourcemap inline
```
> Warning: Note that inline sourcemap is solely used for development, e.g. when developing a browser extension and the access to `.map` file is not allowed, and it's not recommended for production.
> Warning: Source map is not supported in `--dts` build.
### Bundle formats
Supported format: `esm`, `cjs`, (default) and `iife`.
You can bundle in multiple formats in one go:
```bash
tsup src/index.ts --format esm,cjs,iife
```
That will output files in following folder structure:
```bash
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:
```bash
dist
├── index.js # esm
├── index.global.js # iife
└── index.cjs # cjs
```
Read more about [`esm` support in Node.js](https://nodejs.org/api/esm.html#esm_enabling).
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:
```bash
tsup src/index.ts --format esm,cjs,iife --legacy-output
```
..which outputs to:
```bash
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:
```ts
export default defineConfig({
outExtension({ format }) {
return {
js: `.${format}.js`,
}
},
})
```
This will generate your files to `[name].[format].js`.
The signature of `outExtension` is:
```ts
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](https://esbuild.github.io/api/#splitting).
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](https://esbuild.github.io/api/#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](https://swc.rs).
### Compile-time environment variables
You can use `--env` flag to define compile-time environment variables:
```bash
tsup src/index.ts --env.NODE_ENV production
```
Note that `--env.VAR_NAME` only recognizes `process.env.VAR_NAME` and `import.meta.env.VAR_NAME`. If you use `process.env`, it will only take effect when it is used as a built-in global variable. Therefore, do not import `process` from `node:process`.
### 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`.
### Interop with CommonJS
By default, esbuild will transform `export default x` to `module.exports.default = x` in CommonJS, but you can change this behavior by using the `--cjsInterop` flag: If there are only default exports and no named exports, it will be transformed to `module.exports = x` instead.
```bash
tsup src/index.ts --cjsInterop
```
### Watch mode
```bash
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.
> INFO: By default it always ignores `dist`, `node_modules` & `.git`
```bash
tsup src/index.ts --watch --ignore-watch ignore-this-folder-too
```
> INFO: You can specify more than a folder repeating "--ignore-watch", for example: `tsup src src/index.ts --watch --ignore-watch folder1 --ignore-watch folder2`
### onSuccess
You can specify command to be executed after a successful build, specially useful for **Watch mode**
```bash
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:
```ts
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`:
```ts
import { defineConfig } from 'tsup'
export default defineConfig({
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.
```bash
tsup src/index.ts --minify
```
To use [Terser](https://github.com/terser/terser) instead of esbuild for minification, pass terser as argument value
```bash
tsup src/index.ts --minify terser
```
> NOTE: You must have terser installed. Install it with `npm install -D terser`
In `tsup.config.js`, you can pass `terserOptions` which will be passed to `terser.minify` as it is.
### Custom loader
Esbuild loader list:
```ts
type Loader =
| 'js'
| 'jsx'
| 'ts'
| 'tsx'
| 'css'
| 'json'
| 'text'
| 'base64'
| 'file'
| 'dataurl'
| 'binary'
| 'copy'
| 'default'
```
To use a custom loader via CLI flag:
```bash
tsup --loader ".jpg=base64" --loader ".webp=file"
```
Or via `tsup.config.ts`:
```ts
import { defineConfig } from 'tsup'
export default defineConfig({
loader: {
'.jpg': 'base64',
'.webp': 'file',
},
})
```
### Tree shaking
esbuild has [tree shaking](https://esbuild.github.io/api/#tree-shaking) enabled by default, but sometimes it's not working very well, see [#1794](https://github.com/evanw/esbuild/issues/1794) [#1435](https://github.com/evanw/esbuild/issues/1435), so tsup offers an additional option to let you use Rollup for tree shaking instead:
```bash
tsup src/index.ts --treeshake
```
This flag above will enable Rollup for tree shaking, and it's equivalent to the following `tsup.config.ts`:
```ts
import { defineConfig } from 'tsup'
export default defineConfig({
treeshake: true,
})
```
This option has the same type as the `treeshake` option in Rollup, [see more](https://rollupjs.org/guide/en/#treeshake).
### 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](https://esbuild.github.io/content-types/#css), and tsup allows you to use PostCSS plugins on top of native CSS support.
To use PostCSS, you need to install PostCSS:
```bash
npm i postcss -D
# Or Yarn
yarn add postcss --dev
```
..and populate a `postcss.config.js` in your project
```js
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](https://www.bundle-buddy.com/esbuild) 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`:
```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](https://esbuild.github.io/api/#build-api), and [how to write an esbuild plugin](https://esbuild.github.io/plugins/#using-plugins).
---
For more details:
```bash
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
```ts
import { defineConfig } from 'tsup'
export default defineConfig({
shims: true,
})
```
- When building the cjs bundle, it will compile `import.meta.url` as `typeof 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` as `path.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`.
### JavaScript API
If you want to use `tsup` in your Node.js program, you can use the JavaScript API:
```js
import { build } from 'tsup'
await build({
entry: ['src/index.ts'],
sourcemap: true,
dts: true,
})
```
For all available options for the `build` function, please see [the API docs](https://jsdocs.io/package/tsup).
### Using custom tsconfig.json
You can also use custom tsconfig.json file configurations by using the `--tsconfig` flag:
```bash
tsup --tsconfig tsconfig.prod.json
```
By default, tsup try to find the `tsconfig.json` file in the current directory, if it's not found, it will use the default tsup config.
### Using custom Swc configuration
When you use legacy TypeScript decorator by enabling `emitDecoratorMetadata` in your tsconfig, tsup will automatically use [SWC](https://swc.rs) to transpile
decorators. In this case, you can give extra swc configuration in the `tsup.config.ts` file.
For example, if you have to define `useDefineForClassFields`, you can do that as follows:
```ts
import { defineConfig } from 'tsup'
export default defineConfig({
entry: ['src/index.ts'],
splitting: false,
sourcemap: true,
clean: true,
swc: {
jsc: {
transform: {
useDefineForClassFields: true
}
}
}
})
```
Note: some SWC options cannot be configured:
```json
{
"parser": {
"syntax": "typescript",
"decorators": true
},
"transform": {
"legacyDecorator": true,
"decoratorMetadata": true
},
"keepClassNames": true,
"target": "es2022"
}
```
You can also define a custom `.swcrc` configuration file. Just set `swcrc` to `true`
in `tsup.config.ts` to allow SWC plugin to discover automatically your custom swc config file.
```ts
import { defineConfig } from 'tsup'
export default defineConfig({
entry: ['src/index.ts'],
splitting: false,
sourcemap: true,
clean: true,
swc: {
swcrc: true
}
})
```
## 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](https://swc.rs) 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](https://github.com/sponsors/egoist)
================================================
FILE: docs/index.html
================================================
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"
/>
<title>tsup</title>
<meta name="description" content="bundle typescript library with ease" />
<meta property="og:title" content="tsup" />
<meta
property="og:description"
content="bundle typescript library with ease"
/>
<meta name="twitter:card" content="summary" />
<!-- Stylesheet -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@egoist/docup@2/dist/docup.min.css"
/>
<style>
.logo-dark {
display: none;
}
@media (prefers-color-scheme: dark) {
.logo {
display: none;
}
.logo-dark {
display: block;
}
}
</style>
</head>
<body>
<script type="module">
import * as docup from 'https://cdn.jsdelivr.net/npm/@egoist/docup@2/dist/docup.min.js'
docup.init({
// ..options
navLinks: [
{ text: `Type Doc`, link: `https://jsdocs.io/package/tsup` },
{
text: `GitHub`,
link: `https://github.com/egoist/tsup`,
},
{
text: `❤️️ Sponsor`,
link: `https://github.com/sponsors/egoist`,
},
],
beforeSidebar: `<div style="padding: 20px;">
<div style="margin-bottom:10px">
<strong style="font-size: 0.8rem;color:#ccc;">SPONSOR</strong>
</div>
<a href="https://chromatic.com" rel="nofollow noopener" target="_blank"
><img
title="Ship UIs faster with automated workflows for Storybook"
alt="chromatic logo"
class="logo"
src="https://fastly.jsdelivr.net/gh/egoist-bot/images@main/uPic/Group 2 (1).png"
/><img
title="Ship UIs faster with automated workflows for Storybook"
alt="chromatic logo"
class="logo logo-dark"
src="https://fastly.jsdelivr.net/gh/egoist-bot/images@main/uPic/Group 2 (2).png"
/></a>
</div>`,
})
</script>
<script
async
defer
data-website-id="24610d03-48c4-4ad3-8557-4d424349d2ba"
src="https://umami.egoist.dev/mami.js"
></script>
</body>
</html>
================================================
FILE: package.json
================================================
{
"name": "tsup",
"version": "8.5.1",
"packageManager": "pnpm@10.22.0",
"description": "Bundle your TypeScript library with no config, powered by esbuild",
"license": "MIT",
"homepage": "https://tsup.egoist.dev/",
"repository": {
"type": "git",
"url": "git+https://github.com/egoist/tsup.git"
},
"author": "EGOIST",
"files": [
"/assets",
"/dist",
"/schema.json"
],
"main": "dist/index.js",
"types": "dist/index.d.ts",
"bin": {
"tsup": "dist/cli-default.js",
"tsup-node": "dist/cli-node.js"
},
"scripts": {
"dev": "pnpm run build-fast --watch",
"build": "tsup src/cli-*.ts src/index.ts src/rollup.ts --clean --splitting",
"prepublishOnly": "pnpm run build",
"test": "pnpm run build && pnpm run test-only",
"format": "prettier --write .",
"test-only": "vitest run",
"build-fast": "pnpm run build --no-dts",
"release": "bumpp"
},
"peerDependencies": {
"@microsoft/api-extractor": "^7.36.0",
"@swc/core": "^1",
"postcss": "^8.4.12",
"typescript": ">=4.5.0"
},
"peerDependenciesMeta": {
"@microsoft/api-extractor": {
"optional": true
},
"@swc/core": {
"optional": true
},
"postcss": {
"optional": true
},
"typescript": {
"optional": true
}
},
"dependencies": {
"bundle-require": "^5.1.0",
"cac": "^6.7.14",
"chokidar": "^4.0.3",
"consola": "^3.4.0",
"debug": "^4.4.0",
"esbuild": "^0.27.0",
"fix-dts-default-cjs-exports": "^1.0.0",
"joycon": "^3.1.1",
"picocolors": "^1.1.1",
"postcss-load-config": "^6.0.1",
"resolve-from": "^5.0.0",
"rollup": "^4.34.8",
"source-map": "^0.7.6",
"sucrase": "^3.35.0",
"tinyexec": "^0.3.2",
"tinyglobby": "^0.2.11",
"tree-kill": "^1.2.2"
},
"devDependencies": {
"@microsoft/api-extractor": "^7.50.0",
"@rollup/plugin-json": "6.1.0",
"@swc/core": "1.10.18",
"@types/debug": "4.1.12",
"@types/node": "22.13.4",
"@types/resolve": "1.20.6",
"bumpp": "^10.0.3",
"flat": "6.0.1",
"postcss": "8.5.2",
"postcss-simple-vars": "7.0.1",
"prettier": "3.5.1",
"resolve": "1.22.10",
"rollup-plugin-dts": "6.1.1",
"sass": "1.85.0",
"strip-json-comments": "5.0.1",
"svelte": "5.19.9",
"svelte-preprocess": "6.0.3",
"terser": "^5.39.0",
"ts-essentials": "10.0.4",
"tsup": "8.3.6",
"typescript": "5.7.3",
"vitest": "3.0.6",
"wait-for-expect": "3.0.2"
},
"engines": {
"node": ">=18"
},
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",
"@swc/core",
"esbuild",
"svelte-preprocess"
]
}
}
================================================
FILE: schema.json
================================================
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "tsup",
"version": 1.1,
"anyOf": [
{
"type": "object",
"required": ["tsup"],
"additionalProperties": true,
"properties": {
"tsup": {
"type": ["object", "array"],
"oneOf": [
{
"type": "object",
"additionalProperties": false,
"$ref": "#/definitions/options"
},
{
"type": "array",
"items": {
"additionalProperties": false,
"$ref": "#/definitions/options"
}
}
]
}
}
},
{
"type": ["object", "array"],
"oneOf": [
{
"type": "object",
"$ref": "#/definitions/options"
},
{
"type": "array",
"items": {
"$ref": "#/definitions/options"
}
}
]
}
],
"definitions": {
"options": {
"type": "object",
"markdownDescription": "Configuration options for [tsup](https://tsup.egoist.dev)",
"properties": {
"entry": {
"markdownDescription": "Files that each serve as an input to the bundling algorithm.\n\n---\nReferences:\n- [Entry Points](https://esbuild.github.io/api/#entry-points) - esbuild\n - [Multiple Entrypoints](https://tsup.egoist.dev/#multiple-entrypoints) - tsup",
"oneOf": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "object"
}
]
},
"treeshake": {
"markdownDescription": "By default esbuild already does treeshaking but this option allow you to perform additional treeshaking with Rollup and result in smaller bundle size.",
"oneOf": [
{
"type": "boolean"
},
{
"type": "string",
"enum": ["smallest", "safest", "recommended"]
}
]
},
"name": {
"type": "string",
"description": "Optional config name to show in CLI output"
},
"legacyOutput": {
"type": "boolean",
"description": "Output different formats to different folder instead of using different extension"
},
"target": {
"markdownDescription": "This sets the target environment for the generated code\n\n---\nReferences:\n- [Target](https://esbuild.github.io/api/#target) - esbuild",
"default": "node14",
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
},
"minify": {
"description": "When enabled, the generated code will be minified instead of pretty-printed.",
"oneOf": [
{
"type": "boolean"
},
{
"type": "string",
"enum": ["terser"]
}
]
},
"minifyWhitespace": {
"type": "boolean"
},
"minifyIdentifiers": {
"type": "boolean"
},
"minifySyntax": {
"type": "boolean"
},
"keepNames": {
"type": "boolean"
},
"watch": {
"oneOf": [
{
"type": "boolean"
},
{
"type": "string",
"items": {
"type": "string"
}
},
{
"type": "array",
"items": {
"type": ["string", "boolean"]
}
}
]
},
"ignoreWatch": {
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
},
"onSuccess": {
"type": "string"
},
"jsxFactory": {
"type": "string"
},
"jsxFragment": {
"type": "string"
},
"outDir": {
"type": "string"
},
"format": {
"oneOf": [
{
"enum": ["cjs", "iife", "esm"],
"type": "string"
},
{
"type": "array",
"uniqueItems": true,
"items": {
"type": "string",
"enum": ["cjs", "iife", "esm"]
}
}
]
},
"swc": {
"type": "object"
},
"globalName": {
"type": "string"
},
"env": {
"type": "object"
},
"define": {
"type": "object"
},
"dts": {
"markdownDescription": "This will emit `./dist/index.js` and `./dist/index.d.ts`.\n\nIf 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`.\n\n**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.",
"oneOf": [
{
"type": "boolean"
},
{
"type": "string"
},
{
"type": "object",
"properties": {
"entry": {
"oneOf": [
{
"type": "string"
},
{
"type": "object"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
}
}
}
]
},
"sourcemap": {
"oneOf": [
{
"type": "boolean"
},
{
"enum": ["inline"]
}
]
},
"noExternal": {
"type": "array",
"items": {
"type": "string"
},
"description": "Always bundle modules matching given patterns"
},
"external": {
"description": "Don't bundle these modules",
"type": "array",
"items": {
"type": "string"
}
},
"replaceNodeEnv": {
"type": "boolean",
"markdownDescription": "Replace `process.env.NODE_ENV` with `production` or `development` `production` when the bundled is minified, `development` otherwise"
},
"splitting": {
"type": "boolean",
"default": true,
"markdownDescription": "You may want to disable code splitting sometimes: [`#255`](https://github.com/egoist/tsup/issues/255)"
},
"clean": {
"description": "Clean output directory before each buil",
"oneOf": [
{
"type": "boolean"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
},
"silent": {
"type": "boolean",
"description": "Suppress non-error logs (excluding \"onSuccess\" process output)"
},
"skipNodeModulesBundle": {
"type": "boolean",
"description": "Skip node_modules bundling"
},
"pure": {
"markdownDescription": "See:\n- [Pure](https://esbuild.github.io/api/#pure) - esbuild",
"oneOf": [
{
"type": "boolean"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
},
"bundle": {
"default": true,
"type": "boolean",
"description": "Disable bundling, default to true"
},
"inject": {
"markdownDescription": "This option allows you to automatically replace a global variable with an import from another file.\n\n---\nSee:\n- [Inject](https://esbuild.github.io/api/#inject) - esbuild",
"type": "array",
"items": {
"type": "string"
}
},
"metafile": {
"type": "boolean",
"markdownDescription": "Emit esbuild metafile.\n\n---\nSee:\n- [Metafile](https://esbuild.github.io/api/#metafile) - esbuild"
},
"footer": {
"type": "object",
"properties": {
"js": {
"type": "string"
},
"css": {
"type": "string"
}
}
},
"banner": {
"type": "object",
"properties": {
"js": {
"type": "string"
},
"css": {
"type": "string"
}
}
},
"platform": {
"description": "Target platform",
"type": "string",
"default": "node",
"enum": ["node", "browser", "neutral"]
},
"config": {
"markdownDescription": "Disable config file with `false` or pass a custom config filename",
"type": ["boolean", "string"]
},
"tsconfig": {
"type": "string",
"description": " Use a custom tsconfig"
},
"injectStyle": {
"type": "boolean",
"default": false,
"description": "Inject CSS as style tags to document head"
},
"shims": {
"type": "boolean",
"default": false,
"description": "Inject cjs and esm shims if needed"
}
}
}
}
}
================================================
FILE: src/api-extractor.ts
================================================
import path from 'node:path'
import { handleError } from './errors'
import {
type ExportDeclaration,
formatAggregationExports,
formatDistributionExports,
} from './exports'
import { loadPkg } from './load'
import { createLogger } from './log'
import {
defaultOutExtension,
ensureTempDeclarationDir,
getApiExtractor,
removeFiles,
toAbsolutePath,
writeFileSync,
} from './utils'
import type { Format, NormalizedOptions } from './options'
import type {
ExtractorResult,
IConfigFile,
IExtractorConfigPrepareOptions,
} from '@microsoft/api-extractor'
const logger = createLogger()
function rollupDtsFile(
inputFilePath: string,
outputFilePath: string,
tsconfigFilePath: string,
) {
const cwd = process.cwd()
const packageJsonFullPath = path.join(cwd, 'package.json')
const configObject: IConfigFile = {
mainEntryPointFilePath: inputFilePath,
apiReport: {
enabled: false,
// `reportFileName` is not been used. It's just to fit the requirement of API Extractor.
reportFileName: 'tsup-report.api.md',
},
docModel: { enabled: false },
dtsRollup: {
enabled: true,
untrimmedFilePath: outputFilePath,
},
tsdocMetadata: { enabled: false },
compiler: {
tsconfigFilePath,
},
projectFolder: cwd,
newlineKind: 'lf',
}
const prepareOptions: IExtractorConfigPrepareOptions = {
configObject,
configObjectFullPath: undefined,
packageJsonFullPath,
}
const imported = getApiExtractor()
if (!imported) {
throw new Error(
`@microsoft/api-extractor is not installed. Please install it first.`,
)
}
const { ExtractorConfig, Extractor } = imported
const extractorConfig = ExtractorConfig.prepare(prepareOptions)
// Invoke API Extractor
const extractorResult: ExtractorResult = Extractor.invoke(extractorConfig, {
// Equivalent to the "--local" command-line parameter
localBuild: true,
// Equivalent to the "--verbose" command-line parameter
showVerboseMessages: true,
})
if (!extractorResult.succeeded) {
throw new Error(
`API Extractor completed with ${extractorResult.errorCount} errors and ${extractorResult.warningCount} warnings when processing ${inputFilePath}`,
)
}
}
async function rollupDtsFiles(
options: NormalizedOptions,
exports: ExportDeclaration[],
format: Format,
) {
if (!options.experimentalDts || !options.experimentalDts?.entry) {
return
}
/**
* `.tsup/declaration` directory
*/
const declarationDir = ensureTempDeclarationDir()
const outDir = options.outDir || 'dist'
const pkg = await loadPkg(process.cwd())
const dtsExtension = defaultOutExtension({ format, pkgType: pkg.type }).dts
const tsconfig = options.tsconfig || 'tsconfig.json'
let dtsInputFilePath = path.join(
declarationDir,
`_tsup-dts-aggregation${dtsExtension}`,
)
// @microsoft/api-extractor doesn't support `.d.mts` and `.d.cts` file as a
// entrypoint yet. So we replace the extension here as a temporary workaround.
//
// See the issue for more details:
// https://github.com/microsoft/rushstack/pull/4196
dtsInputFilePath = dtsInputFilePath
.replace(/\.d\.mts$/, '.dmts.d.ts')
.replace(/\.d\.cts$/, '.dcts.d.ts')
const dtsOutputFilePath = path.join(outDir, `_tsup-dts-rollup${dtsExtension}`)
writeFileSync(
dtsInputFilePath,
formatAggregationExports(exports, declarationDir),
)
rollupDtsFile(dtsInputFilePath, dtsOutputFilePath, tsconfig)
for (let [out, sourceFileName] of Object.entries(
options.experimentalDts.entry,
)) {
/**
* Source file name (`src/index.ts`)
*
* @example
*
* ```ts
* import { defineConfig } from 'tsup'
*
* export default defineConfig({
* entry: { index: 'src/index.ts' },
* // Here `src/index.ts` is our `sourceFileName`.
* })
* ```
*/
sourceFileName = toAbsolutePath(sourceFileName)
/**
* Output file name (`dist/index.d.ts`)
*
* @example
*
* ```ts
* import { defineConfig } from 'tsup'
*
* export default defineConfig({
* entry: { index: 'src/index.ts' },
* // Here `dist/index.d.ts` is our `outFileName`.
* })
* ```
*/
const outFileName = path.join(outDir, out + dtsExtension)
// Find all declarations that are exported from the current source file
const currentExports = exports.filter(
(declaration) => declaration.sourceFileName === sourceFileName,
)
writeFileSync(
outFileName,
formatDistributionExports(currentExports, outFileName, dtsOutputFilePath),
)
}
}
async function cleanDtsFiles(options: NormalizedOptions) {
if (options.clean) {
await removeFiles(['**/*.d.{ts,mts,cts}'], options.outDir)
}
}
export async function runDtsRollup(
options: NormalizedOptions,
exports?: ExportDeclaration[],
) {
try {
const start = Date.now()
const getDuration = () => {
return `${Math.floor(Date.now() - start)}ms`
}
logger.info('dts', 'Build start')
if (!exports) {
throw new Error('Unexpected internal error: dts exports is not define')
}
await cleanDtsFiles(options)
for (const format of options.format) {
await rollupDtsFiles(options, exports, format)
}
logger.success('dts', `⚡️ Build success in ${getDuration()}`)
} catch (error) {
handleError(error)
logger.error('dts', 'Build error')
}
}
================================================
FILE: src/cli-default.ts
================================================
#!/usr/bin/env node
import { handleError } from './errors'
import { main } from './cli-main'
main().catch(handleError)
================================================
FILE: src/cli-main.ts
================================================
import { cac } from 'cac'
import { flatten } from 'flat'
import { version } from '../package.json'
import { slash } from './utils'
import type { Format, Options } from '.'
function ensureArray(input: string): string[] {
return Array.isArray(input) ? input : input.split(',')
}
export async function main(options: Options = {}) {
const cli = cac('tsup')
cli
.command('[...files]', 'Bundle files', {
ignoreOptionDefaultValue: true,
})
.option('--entry.* <file>', 'Use a key-value pair as entry files')
.option('-d, --out-dir <dir>', 'Output directory', { default: 'dist' })
.option('--format <format>', 'Bundle format, "cjs", "iife", "esm"', {
default: 'cjs',
})
.option('--minify [terser]', 'Minify bundle')
.option('--minify-whitespace', 'Minify whitespace')
.option('--minify-identifiers', 'Minify identifiers')
.option('--minify-syntax', 'Minify syntax')
.option(
'--keep-names',
'Keep original function and class names in minified code',
)
.option('--target <target>', 'Bundle target, "es20XX" or "esnext"', {
default: 'es2017',
})
.option(
'--legacy-output',
'Output different formats to different folder instead of using different extensions',
)
.option('--dts [entry]', 'Generate declaration file')
.option('--dts-resolve', 'Resolve externals types used for d.ts files')
.option('--dts-only', 'Emit declaration files only')
.option(
'--experimental-dts [entry]',
'Generate declaration file (experimental)',
)
.option(
'--sourcemap [inline]',
'Generate external sourcemap, or inline source: --sourcemap inline',
)
.option(
'--watch [path]',
'Watch mode, if path is not specified, it watches the current folder ".". Repeat "--watch" for more than one path',
)
.option('--ignore-watch <path>', 'Ignore custom paths in watch mode')
.option(
'--onSuccess <command>',
'Execute command after successful build, specially useful for watch mode',
)
.option('--env.* <value>', 'Define compile-time env variables')
.option(
'--inject <file>',
'Replace a global variable with an import from another file',
)
.option('--define.* <value>', 'Define compile-time constants')
.option(
'--external <name>',
'Mark specific packages / package.json (dependencies and peerDependencies) as external',
)
.option('--global-name <name>', 'Global variable name for iife format')
.option('--jsxFactory <jsxFactory>', 'Name of JSX factory function', {
default: 'React.createElement',
})
.option('--jsxFragment <jsxFragment>', 'Name of JSX fragment function', {
default: 'React.Fragment',
})
.option('--replaceNodeEnv', 'Replace process.env.NODE_ENV')
.option('--no-splitting', 'Disable code splitting')
.option('--clean', 'Clean output directory')
.option(
'--silent',
'Suppress non-error logs (excluding "onSuccess" process output)',
)
.option('--pure <express>', 'Mark specific expressions as pure')
.option('--metafile', 'Emit esbuild metafile (a JSON file)')
.option('--platform <platform>', 'Target platform', {
default: 'node',
})
.option('--loader <ext=loader>', 'Specify the loader for a file extension')
.option('--tsconfig <filename>', 'Use a custom tsconfig')
.option('--config <filename>', 'Use a custom config file')
.option('--no-config', 'Disable config file')
.option('--shims', 'Enable cjs and esm shims')
.option('--inject-style', 'Inject style tag to document head')
.option(
'--treeshake [strategy]',
'Using Rollup for treeshaking instead, "recommended" or "smallest" or "safest"',
)
.option('--publicDir [dir]', 'Copy public directory to output directory')
.option(
'--killSignal <signal>',
'Signal to kill child process, "SIGTERM" or "SIGKILL"',
)
.option('--cjsInterop', 'Enable cjs interop')
.action(async (files: string[], flags) => {
const { build } = await import('.')
Object.assign(options, {
...flags,
})
if (!options.entry && files.length > 0) {
options.entry = files.map(slash)
}
if (flags.format) {
const format = ensureArray(flags.format) as Format[]
options.format = format
}
if (flags.external) {
const external = ensureArray(flags.external)
options.external = external
}
if (flags.target) {
options.target = flags.target.includes(',')
? flags.target.split(',')
: flags.target
}
if (flags.dts || flags.dtsResolve || flags.dtsOnly) {
options.dts = {}
if (typeof flags.dts === 'string') {
options.dts.entry = flags.dts
}
if (flags.dtsResolve) {
options.dts.resolve = flags.dtsResolve
}
if (flags.dtsOnly) {
options.dts.only = true
}
}
if (flags.inject) {
const inject = ensureArray(flags.inject)
options.inject = inject
}
if (flags.define) {
const define: Record<string, string> = flatten(flags.define)
options.define = define
}
if (flags.loader) {
const loader = ensureArray(flags.loader)
options.loader = loader.reduce((result, item) => {
const parts = item.split('=')
return {
...result,
[parts[0]]: parts[1],
}
}, {})
}
await build(options)
})
cli.help()
cli.version(version)
cli.parse(process.argv, { run: false })
await cli.runMatchedCommand()
}
================================================
FILE: src/cli-node.ts
================================================
#!/usr/bin/env node
import { handleError } from './errors'
import { main } from './cli-main'
main({
skipNodeModulesBundle: true,
}).catch(handleError)
================================================
FILE: src/errors.ts
================================================
import { isMainThread, parentPort } from 'node:worker_threads'
import colors from 'picocolors'
export class PrettyError extends Error {
constructor(message: string) {
super(message)
this.name = this.constructor.name
if (typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, this.constructor)
} else {
this.stack = new Error(message).stack
}
}
}
export function handleError(error: any) {
if (error.loc) {
console.error(
colors.bold(
colors.red(
`Error parsing: ${error.loc.file}:${error.loc.line}:${error.loc.column}`,
),
),
)
}
if (error.frame) {
console.error(colors.red(error.message))
console.error(colors.dim(error.frame))
} else if (error instanceof PrettyError) {
console.error(colors.red(error.message))
} else {
console.error(colors.red(error.stack))
}
process.exitCode = 1
if (!isMainThread && parentPort) {
parentPort.postMessage('error')
}
}
================================================
FILE: src/esbuild/external.ts
================================================
import { match, tsconfigPathsToRegExp } from 'bundle-require'
import type { Plugin } from 'esbuild'
// Must not start with "/" or "./" or "../" or "C:\" or be the exact strings ".." or "."
const NON_NODE_MODULE_RE = /^[A-Z]:[/\\]|^\.{0,2}\/|^\.{1,2}$/
export const externalPlugin = ({
external,
noExternal,
skipNodeModulesBundle,
tsconfigResolvePaths,
}: {
external?: (string | RegExp)[]
noExternal?: (string | RegExp)[]
skipNodeModulesBundle?: boolean
tsconfigResolvePaths?: Record<string, string[]>
}): Plugin => {
return {
name: `external`,
setup(build) {
if (skipNodeModulesBundle) {
const resolvePatterns = tsconfigPathsToRegExp(
tsconfigResolvePaths || {},
)
build.onResolve({ filter: /.*/ }, (args) => {
// Resolve `paths` from tsconfig
if (match(args.path, resolvePatterns)) {
return
}
// Respect explicit external/noExternal conditions
if (match(args.path, noExternal)) {
return
}
if (match(args.path, external)) {
return { external: true }
}
// Exclude any other import that looks like a Node module
if (!NON_NODE_MODULE_RE.test(args.path)) {
return {
path: args.path,
external: true,
}
}
})
} else {
build.onResolve({ filter: /.*/ }, (args) => {
// Respect explicit external/noExternal conditions
if (match(args.path, noExternal)) {
return
}
if (match(args.path, external)) {
return { external: true }
}
})
}
},
}
}
================================================
FILE: src/esbuild/index.ts
================================================
import fs from 'node:fs'
import path from 'node:path'
import {
type BuildResult,
type Plugin as EsbuildPlugin,
build as esbuild,
formatMessages,
} from 'esbuild'
import consola from 'consola'
import { getProductionDeps, loadPkg } from '../load'
import { type Logger, getSilent } from '../log'
import { defaultOutExtension, truthy } from '../utils'
import { nodeProtocolPlugin } from './node-protocol'
import { externalPlugin } from './external'
import { postcssPlugin } from './postcss'
import { sveltePlugin } from './svelte'
import { swcPlugin } from './swc'
import { nativeNodeModulesPlugin } from './native-node-module'
import type { PluginContainer } from '../plugin'
import type { Format, NormalizedOptions } from '..'
import type { OutExtensionFactory } from '../options'
const getOutputExtensionMap = (
options: NormalizedOptions,
format: Format,
pkgType: string | undefined,
) => {
const outExtension: OutExtensionFactory =
options.outExtension || defaultOutExtension
const defaultExtension = defaultOutExtension({ format, pkgType })
const extension = outExtension({ options, format, pkgType })
return {
'.js': extension.js || defaultExtension.js,
}
}
/**
* Support to exclude special package.json
*/
const generateExternal = async (external: (string | RegExp)[]) => {
const result: (string | RegExp)[] = []
for (const item of external) {
if (typeof item !== 'string' || !item.endsWith('package.json')) {
result.push(item)
continue
}
const pkgPath: string = path.isAbsolute(item)
? path.dirname(item)
: path.dirname(path.resolve(process.cwd(), item))
const deps = await getProductionDeps(pkgPath)
result.push(...deps)
}
return result
}
export async function runEsbuild(
options: NormalizedOptions,
{
format,
css,
logger,
buildDependencies,
pluginContainer,
}: {
format: Format
css?: Map<string, string>
buildDependencies: Set<string>
logger: Logger
pluginContainer: PluginContainer
},
) {
const pkg = await loadPkg(process.cwd())
const deps = await getProductionDeps(process.cwd())
const external = [
// Exclude dependencies, e.g. `lodash`, `lodash/get`
...deps.map((dep) => new RegExp(`^${dep}($|\\/|\\\\)`)),
...(await generateExternal(options.external || [])),
]
const outDir = options.outDir
const outExtension = getOutputExtensionMap(options, format, pkg.type)
const env: { [k: string]: string } = {
...options.env,
}
if (options.replaceNodeEnv) {
env.NODE_ENV =
options.minify || options.minifyWhitespace ? 'production' : 'development'
}
logger.info(format, 'Build start')
const startTime = Date.now()
let result: BuildResult | undefined
const splitting =
format === 'iife'
? false
: typeof options.splitting === 'boolean'
? options.splitting
: format === 'esm'
const platform = options.platform || 'node'
const loader = options.loader || {}
const injectShims = options.shims
pluginContainer.setContext({
format,
splitting,
options,
logger,
})
await pluginContainer.buildStarted()
const esbuildPlugins: Array<EsbuildPlugin | false | undefined> = [
options.removeNodeProtocol && nodeProtocolPlugin(),
{
name: 'modify-options',
setup(build) {
pluginContainer.modifyEsbuildOptions(build.initialOptions)
if (options.esbuildOptions) {
options.esbuildOptions(build.initialOptions, { format })
}
},
},
// esbuild's `external` option doesn't support RegExp
// So here we use a custom plugin to implement it
format !== 'iife' &&
externalPlugin({
external,
noExternal: options.noExternal,
skipNodeModulesBundle: options.skipNodeModulesBundle,
tsconfigResolvePaths: options.tsconfigResolvePaths,
}),
options.tsconfigDecoratorMetadata && swcPlugin({ ...options.swc, logger }),
nativeNodeModulesPlugin(),
postcssPlugin({
css,
inject: options.injectStyle,
cssLoader: loader['.css'],
}),
sveltePlugin({ css }),
...(options.esbuildPlugins || []),
]
const banner =
typeof options.banner === 'function'
? options.banner({ format })
: options.banner
const footer =
typeof options.footer === 'function'
? options.footer({ format })
: options.footer
try {
result = await esbuild({
entryPoints: options.entry,
format:
(format === 'cjs' && splitting) || options.treeshake ? 'esm' : format,
bundle: typeof options.bundle === 'undefined' ? true : options.bundle,
platform,
globalName: options.globalName,
jsxFactory: options.jsxFactory,
jsxFragment: options.jsxFragment,
sourcemap: options.sourcemap ? 'external' : false,
target: options.target,
banner,
footer,
tsconfig: options.tsconfig,
loader: {
'.aac': 'file',
'.css': 'file',
'.eot': 'file',
'.flac': 'file',
'.gif': 'file',
'.jpeg': 'file',
'.jpg': 'file',
'.mp3': 'file',
'.mp4': 'file',
'.ogg': 'file',
'.otf': 'file',
'.png': 'file',
'.svg': 'file',
'.ttf': 'file',
'.wav': 'file',
'.webm': 'file',
'.webp': 'file',
'.woff': 'file',
'.woff2': 'file',
...loader,
},
mainFields:
platform === 'node'
? ['module', 'main']
: ['browser', 'module', 'main'],
plugins: esbuildPlugins.filter(truthy),
define: {
TSUP_FORMAT: JSON.stringify(format),
...(format === 'cjs' && injectShims
? {
'import.meta.url': 'importMetaUrl',
}
: {}),
...options.define,
...Object.keys(env).reduce((res, key) => {
const value = JSON.stringify(env[key])
return {
...res,
[`process.env.${key}`]: value,
[`import.meta.env.${key}`]: value,
}
}, {}),
},
inject: [
format === 'cjs' && injectShims
? path.join(__dirname, '../assets/cjs_shims.js')
: '',
format === 'esm' && injectShims && platform === 'node'
? path.join(__dirname, '../assets/esm_shims.js')
: '',
...(options.inject || []),
].filter(Boolean),
outdir:
options.legacyOutput && format !== 'cjs'
? path.join(outDir, format)
: outDir,
outExtension: options.legacyOutput ? undefined : outExtension,
write: false,
splitting,
logLevel: 'error',
minify: options.minify === 'terser' ? false : options.minify,
minifyWhitespace: options.minifyWhitespace,
minifyIdentifiers: options.minifyIdentifiers,
minifySyntax: options.minifySyntax,
keepNames: options.keepNames,
pure: typeof options.pure === 'string' ? [options.pure] : options.pure,
metafile: true,
})
} catch (error) {
logger.error(format, 'Build failed')
throw error
}
if (result && result.warnings && !getSilent()) {
const messages = result.warnings.filter((warning) => {
if (
warning.text.includes(
`This call to "require" will not be bundled because`,
) ||
warning.text.includes(`Indirect calls to "require" will not be bundled`)
)
return false
return true
})
const formatted = await formatMessages(messages, {
kind: 'warning',
color: true,
})
formatted.forEach((message) => {
consola.warn(message)
})
}
// Manually write files
if (result && result.outputFiles) {
await pluginContainer.buildFinished({
outputFiles: result.outputFiles,
metafile: result.metafile,
})
const timeInMs = Date.now() - startTime
logger.success(format, `⚡️ Build success in ${Math.floor(timeInMs)}ms`)
}
if (result.metafile) {
for (const file of Object.keys(result.metafile.inputs)) {
buildDependencies.add(file)
}
if (options.metafile) {
const outPath = path.resolve(outDir, `metafile-${format}.json`)
await fs.promises.mkdir(path.dirname(outPath), { recursive: true })
await fs.promises.writeFile(
outPath,
JSON.stringify(result.metafile),
'utf8',
)
}
}
}
================================================
FILE: src/esbuild/native-node-module.ts
================================================
import path from 'node:path'
import type { Plugin } from 'esbuild'
// Copied from https://github.com/evanw/esbuild/issues/1051#issuecomment-806325487
export const nativeNodeModulesPlugin = (): Plugin => {
return {
name: 'native-node-modules',
setup(build) {
// If a ".node" file is imported within a module in the "file" namespace, resolve
// it to an absolute path and put it into the "node-file" virtual namespace.
build.onResolve({ filter: /\.node$/, namespace: 'file' }, (args) => {
const resolvedId = require.resolve(args.path, {
paths: [args.resolveDir],
})
if (resolvedId.endsWith('.node')) {
return {
path: resolvedId,
namespace: 'node-file',
}
}
return {
path: resolvedId,
}
})
// Files in the "node-file" virtual namespace call "require()" on the
// path from esbuild of the ".node" file in the output directory.
build.onLoad({ filter: /.*/, namespace: 'node-file' }, (args) => {
return {
contents: `
import path from ${JSON.stringify(args.path)}
try { module.exports = require(path) }
catch {}
`,
resolveDir: path.dirname(args.path),
}
})
// If a ".node" file is imported within a module in the "node-file" namespace, put
// it in the "file" namespace where esbuild's default loading behavior will handle
// it. It is already an absolute path since we resolved it to one above.
build.onResolve(
{ filter: /\.node$/, namespace: 'node-file' },
(args) => ({
path: args.path,
namespace: 'file',
}),
)
// Tell esbuild's default loading behavior to use the "file" loader for
// these ".node" files.
const opts = build.initialOptions
opts.loader = opts.loader || {}
opts.loader['.node'] = 'file'
},
}
}
================================================
FILE: src/esbuild/node-protocol.ts
================================================
import type { Plugin } from 'esbuild'
/**
* The node: protocol was added to require in Node v14.18.0
* https://nodejs.org/api/esm.html#node-imports
*/
export const nodeProtocolPlugin = (): Plugin => {
const nodeProtocol = 'node:'
return {
name: 'node-protocol-plugin',
setup({ onResolve }) {
onResolve(
{
filter: /^node:/,
},
({ path }) => ({
path: path.slice(nodeProtocol.length),
external: true,
}),
)
},
}
}
================================================
FILE: src/esbuild/postcss.ts
================================================
import fs from 'node:fs'
import { type Loader, type Plugin, transform } from 'esbuild'
import { getPostcss } from '../utils'
import type { Result } from 'postcss-load-config'
export const postcssPlugin = ({
css,
inject,
cssLoader,
}: {
css?: Map<string, string>
inject?: boolean | ((css: string, fileId: string) => string | Promise<string>)
cssLoader?: Loader
}): Plugin => {
return {
name: 'postcss',
setup(build) {
let configCache: Result
const getPostcssConfig = async () => {
const loadConfig = require('postcss-load-config')
if (configCache) {
return configCache
}
try {
const result = await loadConfig({}, process.cwd())
configCache = result
return result
} catch (error: any) {
if (error.message.includes('No PostCSS Config found in')) {
const result = { plugins: [], options: {} }
return result
}
throw error
}
}
build.onResolve({ filter: /^#style-inject$/ }, () => {
return { path: '#style-inject', namespace: '#style-inject' }
})
build.onLoad(
{ filter: /^#style-inject$/, namespace: '#style-inject' },
() => {
return {
// Taken from https://github.com/egoist/style-inject/blob/master/src/index.js (MIT)
contents: `
export default function styleInject(css, { insertAt } = {}) {
if (!css || typeof document === 'undefined') return
const head = document.head || document.getElementsByTagName('head')[0]
const style = document.createElement('style')
style.type = 'text/css'
if (insertAt === 'top') {
if (head.firstChild) {
head.insertBefore(style, head.firstChild)
} else {
head.appendChild(style)
}
} else {
head.appendChild(style)
}
if (style.styleSheet) {
style.styleSheet.cssText = css
} else {
style.appendChild(document.createTextNode(css))
}
}
`,
loader: 'js',
}
},
)
build.onLoad({ filter: /\.css$/ }, async (args) => {
let contents: string
if (css && args.path.endsWith('.svelte.css')) {
contents = css.get(args.path)!
} else {
contents = await fs.promises.readFile(args.path, 'utf8')
}
// Load postcss config
const { plugins, options } = await getPostcssConfig()
if (plugins && plugins.length > 0) {
// Load postcss
const postcss = getPostcss()
if (!postcss) {
return {
errors: [
{
text: `postcss is not installed`,
},
],
}
}
// Transform CSS
const result = await postcss
?.default(plugins)
.process(contents, { ...options, from: args.path })
contents = result.css
}
if (inject) {
contents = (
await transform(contents, {
minify: build.initialOptions.minify,
minifyIdentifiers: build.initialOptions.minifyIdentifiers,
minifySyntax: build.initialOptions.minifySyntax,
minifyWhitespace: build.initialOptions.minifyWhitespace,
logLevel: build.initialOptions.logLevel,
loader: 'css',
})
).code
contents =
typeof inject === 'function'
? await inject(JSON.stringify(contents), args.path)
: `import styleInject from '#style-inject';styleInject(${JSON.stringify(
contents,
)})`
return {
contents,
loader: 'js',
}
}
return {
contents,
loader: cssLoader ?? 'css',
}
})
},
}
}
================================================
FILE: src/esbuild/svelte.ts
================================================
import fs from 'node:fs'
import path from 'node:path'
import { type Plugin, transform } from 'esbuild'
import { localRequire } from '../utils'
const useSvelteCssExtension = (p: string) =>
p.replace(/\.svelte$/, '.svelte.css')
export const sveltePlugin = ({
css,
}: {
css?: Map<string, string>
}): Plugin => {
return {
name: 'svelte',
setup(build) {
let svelte: typeof import('svelte/compiler')
let sveltePreprocessor: typeof import('svelte-preprocess').default
build.onResolve({ filter: /\.svelte\.css$/ }, (args) => {
return {
path: path.relative(
process.cwd(),
path.join(args.resolveDir, args.path),
),
namespace: 'svelte-css',
}
})
build.onLoad({ filter: /\.svelte$/ }, async (args) => {
svelte = svelte || localRequire('svelte/compiler')
sveltePreprocessor =
sveltePreprocessor || localRequire('svelte-preprocess')
if (!svelte) {
return {
errors: [{ text: `You need to install "svelte" in your project` }],
}
}
// This converts a message in Svelte's format to esbuild's format
const convertMessage = ({ message, start, end }: any) => {
let location
if (start && end) {
const lineText = source.split(/\r\n|\r|\n/g)[start.line - 1]
const lineEnd =
start.line === end.line ? end.column : lineText.length
location = {
file: filename,
line: start.line,
column: start.column,
length: lineEnd - start.column,
lineText,
}
}
return { text: message, location }
}
// Load the file from the file system
const source = await fs.promises.readFile(args.path, 'utf8')
const filename = path.relative(process.cwd(), args.path)
// Convert Svelte syntax to JavaScript
try {
const preprocess = await svelte.preprocess(
source,
sveltePreprocessor
? sveltePreprocessor({
sourceMap: true,
typescript: {
compilerOptions: {
verbatimModuleSyntax: true,
},
},
})
: {
async script({ content, attributes }) {
if (attributes.lang !== 'ts') return { code: content }
const { code, map } = await transform(content, {
sourcefile: args.path,
loader: 'ts',
sourcemap: true,
tsconfigRaw: {
compilerOptions: {
verbatimModuleSyntax: true,
},
},
logLevel: build.initialOptions.logLevel,
})
return {
code,
map,
}
},
},
{
filename: args.path,
},
)
const result = svelte.compile(preprocess.code, {
filename,
css: 'external',
})
let contents = result.js.code
if (css && result.css && result.css.code) {
const cssPath = useSvelteCssExtension(filename)
css.set(cssPath, result.css.code)
// Directly prepend the `import` statement as sourcemap doesn't matter for now
// If that's need we should use `magic-string`
contents = `import '${useSvelteCssExtension(path.basename(args.path))}';${
contents
}`
}
return { contents, warnings: result.warnings.map(convertMessage) }
} catch (error) {
return { errors: [convertMessage(error)] }
}
})
},
}
}
================================================
FILE: src/esbuild/swc.test.ts
================================================
import { describe, expect, test, vi } from 'vitest'
import { swcPlugin, type SwcPluginConfig } from './swc'
import { localRequire } from '../utils'
vi.mock('../utils')
const getFixture = async (opts: Partial<SwcPluginConfig> = {}) => {
const swc = {
transformFile: vi.fn().mockResolvedValue({
code: 'source-code',
map: JSON.stringify({
sources: ['file:///path/to/file.ts'],
}),
}),
}
const logger = {
warn: vi.fn(),
error: vi.fn(),
info: vi.fn(),
}
const build = {
initialOptions: {
keepNames: true,
},
onLoad: vi.fn(),
}
vi.mocked(localRequire).mockReturnValue(swc)
const plugin = swcPlugin({
...opts,
logger: logger as never,
})
await plugin.setup(build as never)
const onLoad = build.onLoad.mock.calls[0][1] as Function
return { swc, onLoad, logger, build }
}
describe('swcPlugin', () => {
test('swcPlugin transforms TypeScript code with decorators and default plugin swc option', async () => {
const { swc, onLoad } = await getFixture()
await onLoad({
path: 'file.ts',
})
expect(swc.transformFile).toHaveBeenCalledWith('file.ts', {
configFile: false,
jsc: {
keepClassNames: true,
parser: {
decorators: true,
syntax: 'typescript',
},
target: 'es2022',
transform: {
decoratorMetadata: true,
legacyDecorator: true,
},
},
sourceMaps: true,
swcrc: false,
})
})
test('swcPlugin transforms TypeScript code and use given plugin swc option', async () => {
const { swc, onLoad } = await getFixture({
jsc: {
transform: {
useDefineForClassFields: true,
},
},
})
await onLoad({
path: 'file.ts',
})
expect(swc.transformFile).toHaveBeenCalledWith('file.ts', {
configFile: false,
jsc: {
keepClassNames: true,
parser: {
decorators: true,
syntax: 'typescript',
},
target: 'es2022',
transform: {
decoratorMetadata: true,
legacyDecorator: true,
useDefineForClassFields: true,
},
},
sourceMaps: true,
swcrc: false,
})
})
})
================================================
FILE: src/esbuild/swc.ts
================================================
/**
* Use SWC to emit decorator metadata
*/
import path from 'node:path'
import { localRequire } from '../utils'
import type { JscConfig, Options } from '@swc/core'
import type { Plugin } from 'esbuild'
import type { Logger } from '../log'
export type SwcPluginConfig = { logger: Logger } & Options
export const swcPlugin = ({ logger, ...swcOptions }: SwcPluginConfig): Plugin => {
return {
name: 'swc',
setup(build) {
const swc: typeof import('@swc/core') = localRequire('@swc/core')
if (!swc) {
logger.warn(
build.initialOptions.format!,
`You have emitDecoratorMetadata enabled but @swc/core was not installed, skipping swc plugin`,
)
return
}
// Force esbuild to keep class names as well
build.initialOptions.keepNames = true
build.onLoad({ filter: /\.[jt]sx?$/ }, async (args) => {
const isTs = /\.tsx?$/.test(args.path)
const jsc: JscConfig = {
...swcOptions.jsc,
parser: {
...swcOptions.jsc?.parser,
syntax: isTs ? 'typescript' : 'ecmascript',
decorators: true,
},
transform: {
...swcOptions.jsc?.transform,
legacyDecorator: true,
decoratorMetadata: true,
},
keepClassNames: true,
target: 'es2022',
}
const result = await swc.transformFile(args.path, {
...swcOptions,
jsc,
sourceMaps: true,
configFile: false,
swcrc: swcOptions.swcrc ?? false,
})
let code = result.code
if (result.map) {
const map: { sources: string[] } = JSON.parse(result.map)
// Make sure sources are relative path
map.sources = map.sources.map((source) => {
return path.isAbsolute(source)
? path.relative(path.dirname(args.path), source)
: source
})
code += `//# sourceMappingURL=data:application/json;base64,${Buffer.from(
JSON.stringify(map),
).toString('base64')}`
}
return {
contents: code,
}
})
},
}
}
================================================
FILE: src/exports.ts
================================================
import path from 'node:path'
import { replaceDtsWithJsExtensions, slash, truthy } from './utils'
export type ExportDeclaration = ModuleExport | NamedExport
interface ModuleExport {
kind: 'module'
sourceFileName: string
destFileName: string
moduleName: string
isTypeOnly: boolean
}
interface NamedExport {
kind: 'named'
sourceFileName: string
destFileName: string
alias: string
name: string
isTypeOnly: boolean
}
export function formatAggregationExports(
exports: ExportDeclaration[],
declarationDirPath: string,
): string {
const lines = exports
.map((declaration) =>
formatAggregationExport(declaration, declarationDirPath),
)
.filter(truthy)
if (lines.length === 0) {
lines.push('export {};')
}
return `${lines.join('\n')}\n`
}
function formatAggregationExport(
declaration: ExportDeclaration,
declarationDirPath: string,
): string {
const dest = replaceDtsWithJsExtensions(
`./${path.posix.normalize(
slash(path.relative(declarationDirPath, declaration.destFileName)),
)}`,
)
if (declaration.kind === 'module') {
// Not implemented
return ''
} else if (declaration.kind === 'named') {
return [
'export',
declaration.isTypeOnly ? 'type' : '',
'{',
declaration.name,
declaration.name === declaration.alias ? '' : `as ${declaration.alias}`,
'} from',
`'${dest}';`,
]
.filter(truthy)
.join(' ')
} else {
throw new Error('Unknown declaration')
}
}
export function formatDistributionExports(
exports: ExportDeclaration[],
fromFilePath: string,
toFilePath: string,
) {
let importPath = replaceDtsWithJsExtensions(
path.posix.relative(
path.posix.dirname(path.posix.normalize(slash(fromFilePath))),
path.posix.normalize(slash(toFilePath)),
),
)
if (!/^\.+\//.test(importPath)) {
importPath = `./${importPath}`
}
const seen = {
named: new Set<string>(),
module: new Set<string>(),
}
const lines = exports
.filter((declaration) => {
if (declaration.kind === 'module') {
if (seen.module.has(declaration.moduleName)) {
return false
}
seen.module.add(declaration.moduleName)
return true
} else if (declaration.kind === 'named') {
if (seen.named.has(declaration.name)) {
return false
}
seen.named.add(declaration.name)
return true
} else {
return false
}
})
.map((declaration) => formatDistributionExport(declaration, importPath))
.filter(truthy)
if (lines.length === 0) {
lines.push('export {};')
}
return `${lines.join('\n')}\n`
}
function formatDistributionExport(
declaration: ExportDeclaration,
dest: string,
): string {
if (declaration.kind === 'named') {
return [
'export',
declaration.isTypeOnly ? 'type' : '',
'{',
declaration.alias,
declaration.name === declaration.alias ? '' : `as ${declaration.name}`,
'} from',
`'${dest}';`,
]
.filter(truthy)
.join(' ')
} else if (declaration.kind === 'module') {
return `export * from '${declaration.moduleName}';`
}
return ''
}
================================================
FILE: src/fs.ts
================================================
import path from 'node:path'
import fs from 'node:fs'
export const outputFile = async (
filepath: string,
data: any,
options?: { mode?: fs.Mode },
) => {
await fs.promises.mkdir(path.dirname(filepath), { recursive: true })
await fs.promises.writeFile(filepath, data, options)
}
export function copyDirSync(srcDir: string, destDir: string): void {
if (!fs.existsSync(srcDir)) return
fs.mkdirSync(destDir, { recursive: true })
for (const file of fs.readdirSync(srcDir)) {
const srcFile = path.resolve(srcDir, file)
if (srcFile === destDir) {
continue
}
const destFile = path.resolve(destDir, file)
const stat = fs.statSync(srcFile)
if (stat.isDirectory()) {
copyDirSync(srcFile, destFile)
} else {
fs.copyFileSync(srcFile, destFile)
}
}
}
================================================
FILE: src/index.ts
================================================
import path from 'node:path'
import fs from 'node:fs'
import { Worker } from 'node:worker_threads'
import { loadTsConfig } from 'bundle-require'
import { exec, type Result as ExecChild } from 'tinyexec'
import { glob, globSync } from 'tinyglobby'
import kill from 'tree-kill'
import { version } from '../package.json'
import { PrettyError, handleError } from './errors'
import { getAllDepsHash, loadTsupConfig } from './load'
import {
type MaybePromise,
debouncePromise,
removeFiles,
resolveExperimentalDtsConfig,
resolveInitialExperimentalDtsConfig,
slash,
} from './utils'
import { createLogger, setSilent } from './log'
import { runEsbuild } from './esbuild'
import { shebang } from './plugins/shebang'
import { cjsSplitting } from './plugins/cjs-splitting'
import { PluginContainer } from './plugin'
import { swcTarget } from './plugins/swc-target'
import { sizeReporter } from './plugins/size-reporter'
import { treeShakingPlugin } from './plugins/tree-shaking'
import { copyPublicDir, isInPublicDir } from './lib/public-dir'
import { terserPlugin } from './plugins/terser'
import { runTypeScriptCompiler } from './tsc'
import { runDtsRollup } from './api-extractor'
import { cjsInterop } from './plugins/cjs-interop'
import type { Format, KILL_SIGNAL, NormalizedOptions, Options } from './options'
export type { Format, Options, NormalizedOptions }
export const defineConfig = (
options:
| Options
| Options[]
| ((
/** The options derived from CLI flags */
overrideOptions: Options,
) => MaybePromise<Options | Options[]>),
) => options
/**
* tree-kill use `taskkill` command on Windows to kill the process,
* it may return 128 as exit code when the process has already exited.
* @see https://github.com/egoist/tsup/issues/976
*/
const isTaskkillCmdProcessNotFoundError = (err: Error) => {
return (
process.platform === 'win32' &&
'cmd' in err &&
'code' in err &&
typeof err.cmd === 'string' &&
err.cmd.startsWith('taskkill') &&
err.code === 128
)
}
const killProcess = ({ pid, signal }: { pid: number; signal: KILL_SIGNAL }) =>
new Promise<void>((resolve, reject) => {
kill(pid, signal, (err) => {
if (err && !isTaskkillCmdProcessNotFoundError(err)) return reject(err)
resolve()
})
})
const normalizeOptions = async (
logger: ReturnType<typeof createLogger>,
optionsFromConfigFile: Options | undefined,
optionsOverride: Options,
) => {
const _options = {
...optionsFromConfigFile,
...optionsOverride,
}
const options: Partial<NormalizedOptions> = {
outDir: 'dist',
removeNodeProtocol: true,
..._options,
format:
typeof _options.format === 'string'
? [_options.format as Format]
: _options.format || ['cjs'],
dts:
typeof _options.dts === 'boolean'
? _options.dts
? {}
: undefined
: typeof _options.dts === 'string'
? { entry: _options.dts }
: _options.dts,
experimentalDts: await resolveInitialExperimentalDtsConfig(
_options.experimentalDts,
),
}
setSilent(options.silent)
const entry = options.entry || options.entryPoints
if (!entry || Object.keys(entry).length === 0) {
throw new PrettyError(`No input files, try "tsup <your-file>" instead`)
}
if (Array.isArray(entry)) {
options.entry = await glob(entry)
// Ensure entry exists
if (!options.entry || options.entry.length === 0) {
throw new PrettyError(`Cannot find ${entry}`)
} else {
logger.info('CLI', `Building entry: ${options.entry.join(', ')}`)
}
} else {
Object.keys(entry).forEach((alias) => {
const filename = entry[alias]!
if (!fs.existsSync(filename)) {
throw new PrettyError(`Cannot find ${alias}: ${filename}`)
}
})
options.entry = entry
logger.info('CLI', `Building entry: ${JSON.stringify(entry)}`)
}
const tsconfig = loadTsConfig(process.cwd(), options.tsconfig)
if (tsconfig) {
logger.info(
'CLI',
`Using tsconfig: ${path.relative(process.cwd(), tsconfig.path)}`,
)
options.tsconfig = tsconfig.path
options.tsconfigResolvePaths = tsconfig.data?.compilerOptions?.paths || {}
options.tsconfigDecoratorMetadata =
tsconfig.data?.compilerOptions?.emitDecoratorMetadata
if (options.dts) {
options.dts.compilerOptions = {
...(tsconfig.data.compilerOptions || {}),
...(options.dts.compilerOptions || {}),
}
}
if (options.experimentalDts) {
options.experimentalDts = await resolveExperimentalDtsConfig(
options as NormalizedOptions,
tsconfig,
)
}
if (!options.target) {
options.target = tsconfig.data?.compilerOptions?.target?.toLowerCase()
}
} else if (options.tsconfig) {
throw new PrettyError(`Cannot find tsconfig: ${options.tsconfig}`)
}
if (!options.target) {
options.target = 'node16'
}
return options as NormalizedOptions
}
export async function build(_options: Options) {
const config =
_options.config === false
? {}
: await loadTsupConfig(
process.cwd(),
_options.config === true ? undefined : _options.config,
)
const configData =
typeof config.data === 'function'
? await config.data(_options)
: config.data
await Promise.all(
[...(Array.isArray(configData) ? configData : [configData])].map(
async (item) => {
const logger = createLogger(item?.name)
const options = await normalizeOptions(logger, item, _options)
logger.info('CLI', `tsup v${version}`)
if (config.path) {
logger.info('CLI', `Using tsup config: ${config.path}`)
}
if (options.watch) {
logger.info('CLI', 'Running in watch mode')
}
const experimentalDtsTask = async () => {
if (!options.dts && options.experimentalDts) {
const exports = runTypeScriptCompiler(options)
await runDtsRollup(options, exports)
}
}
const dtsTask = async () => {
if (options.dts && options.experimentalDts) {
throw new Error(
"You can't use both `dts` and `experimentalDts` at the same time",
)
}
await experimentalDtsTask()
if (options.dts) {
await new Promise<void>((resolve, reject) => {
const worker = new Worker(path.join(__dirname, './rollup.js'))
const terminateWorker = () => {
if (options.watch) return
worker.terminate()
}
worker.postMessage({
configName: item?.name,
options: {
...options, // functions cannot be cloned
injectStyle:
typeof options.injectStyle === 'function'
? undefined
: options.injectStyle,
banner: undefined,
footer: undefined,
esbuildPlugins: undefined,
esbuildOptions: undefined,
plugins: undefined,
treeshake: undefined,
onSuccess: undefined,
outExtension: undefined,
},
})
worker.on('message', (data) => {
if (data === 'error') {
terminateWorker()
reject(new Error('error occurred in dts build'))
} else if (data === 'success') {
terminateWorker()
resolve()
} else {
const { type, text } = data
if (type === 'log') {
console.log(text)
} else if (type === 'error') {
console.error(text)
}
}
})
})
}
}
const mainTasks = async () => {
if (!options.dts?.only) {
let onSuccessProcess: ExecChild | undefined
let onSuccessCleanup: (() => any) | undefined | void
/** Files imported by the entry */
const buildDependencies: Set<string> = new Set()
let depsHash = await getAllDepsHash(process.cwd())
const doOnSuccessCleanup = async () => {
if (onSuccessProcess) {
await killProcess({
pid: onSuccessProcess.pid!,
signal: options.killSignal || 'SIGTERM',
})
} else if (onSuccessCleanup) {
await onSuccessCleanup()
}
// reset them in all occasions anyway
onSuccessProcess = undefined
onSuccessCleanup = undefined
}
const debouncedBuildAll = debouncePromise(
() => {
return buildAll()
},
100,
handleError,
)
const buildAll = async () => {
await doOnSuccessCleanup()
// Store previous build dependencies in case the build failed
// So we can restore it
const previousBuildDependencies = new Set(buildDependencies)
buildDependencies.clear()
if (options.clean) {
const extraPatterns = Array.isArray(options.clean)
? options.clean
: []
// .d.ts files are removed in the `dtsTask` instead
// `dtsTask` is a separate process, which might start before `mainTasks`
if (options.dts || options.experimentalDts) {
extraPatterns.unshift('!**/*.d.{ts,cts,mts}')
}
await removeFiles(['**/*', ...extraPatterns], options.outDir)
logger.info('CLI', 'Cleaning output folder')
}
const css: Map<string, string> = new Map()
await Promise.all([
...options.format.map(async (format, index) => {
const pluginContainer = new PluginContainer([
shebang(),
...(options.plugins || []),
treeShakingPlugin({
treeshake: options.treeshake,
name: options.globalName,
silent: options.silent,
}),
cjsSplitting(),
cjsInterop(),
swcTarget(),
sizeReporter(),
terserPlugin({
minifyOptions: options.minify,
format,
terserOptions: options.terserOptions,
globalName: options.globalName,
logger,
}),
])
await runEsbuild(options, {
pluginContainer,
format,
css: index === 0 || options.injectStyle ? css : undefined,
logger,
buildDependencies,
}).catch((error) => {
previousBuildDependencies.forEach((v) =>
buildDependencies.add(v),
)
throw error
})
}),
])
copyPublicDir(options.publicDir, options.outDir)
if (options.onSuccess) {
if (typeof options.onSuccess === 'function') {
onSuccessCleanup = await options.onSuccess()
} else {
onSuccessProcess = exec(options.onSuccess, [], {
nodeOptions: { shell: true, stdio: 'inherit' },
})
onSuccessProcess.process?.on('exit', (code) => {
if (code && code !== 0) {
process.exitCode = code
}
})
}
}
}
const startWatcher = async () => {
if (!options.watch) return
const { watch } = await import('chokidar')
const customIgnores = options.ignoreWatch
? Array.isArray(options.ignoreWatch)
? options.ignoreWatch
: [options.ignoreWatch]
: []
const ignored = [
'**/{.git,node_modules}/**',
options.outDir,
...customIgnores,
]
const watchPaths =
typeof options.watch === 'boolean'
? '.'
: Array.isArray(options.watch)
? options.watch.filter((path) => typeof path === 'string')
: options.watch
logger.info(
'CLI',
`Watching for changes in ${
Array.isArray(watchPaths)
? watchPaths.map((v) => `"${v}"`).join(' | ')
: `"${watchPaths}"`
}`,
)
logger.info(
'CLI',
`Ignoring changes in ${ignored
.map((v) => `"${v}"`)
.join(' | ')}`,
)
const watcher = watch(await glob(watchPaths), {
ignoreInitial: true,
ignorePermissionErrors: true,
ignored: (p) => globSync(p, { ignore: ignored }).length === 0,
})
watcher.on('all', async (type, file) => {
file = slash(file)
if (
options.publicDir &&
isInPublicDir(options.publicDir, file)
) {
logger.info('CLI', `Change in public dir: ${file}`)
copyPublicDir(options.publicDir, options.outDir)
return
}
// By default we only rebuild when imported files change
// If you specify custom `watch`, a string or multiple strings
// We rebuild when those files change
let shouldSkipChange = false
if (options.watch === true) {
if (file === 'package.json' && !buildDependencies.has(file)) {
const currentHash = await getAllDepsHash(process.cwd())
shouldSkipChange = currentHash === depsHash
depsHash = currentHash
} else if (!buildDependencies.has(file)) {
shouldSkipChange = true
}
}
if (shouldSkipChange) {
return
}
logger.info('CLI', `Change detected: ${type} ${file}`)
debouncedBuildAll()
})
}
logger.info('CLI', `Target: ${options.target}`)
await buildAll()
startWatcher()
}
}
await Promise.all([dtsTask(), mainTasks()])
},
),
)
}
================================================
FILE: src/lib/public-dir.ts
================================================
import path from 'node:path'
import { copyDirSync } from '../fs'
import { slash } from '../utils'
export const copyPublicDir = (
publicDir: string | boolean | undefined,
outDir: string,
) => {
if (!publicDir) return
copyDirSync(path.resolve(publicDir === true ? 'public' : publicDir), outDir)
}
export const isInPublicDir = (
publicDir: string | boolean | undefined,
filePath: string,
) => {
if (!publicDir) return false
const publicPath = slash(
path.resolve(publicDir === true ? 'public' : publicDir),
)
return slash(path.resolve(filePath)).startsWith(`${publicPath}/`)
}
================================================
FILE: src/lib/report-size.ts
================================================
import colors from 'picocolors'
import type { Logger } from '../log'
const prettyBytes = (bytes: number) => {
if (bytes === 0) return '0 B'
const unit = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const exp = Math.floor(Math.log(bytes) / Math.log(1024))
return `${(bytes / 1024 ** exp).toFixed(2)} ${unit[exp]}`
}
const getLengthOfLongestString = (strings: string[]) => {
return strings.reduce((max, str) => {
return Math.max(max, str.length)
}, 0)
}
const padRight = (str: string, maxLength: number) => {
return str + ' '.repeat(maxLength - str.length)
}
export const reportSize = (
logger: Logger,
format: string,
files: { [name: string]: number },
) => {
const filenames = Object.keys(files)
const maxLength = getLengthOfLongestString(filenames) + 1
for (const name of filenames) {
logger.success(
format,
`${colors.bold(padRight(name, maxLength))}${colors.green(
prettyBytes(files[name]),
)}`,
)
}
}
================================================
FILE: src/load.ts
================================================
import fs from 'node:fs'
import path from 'node:path'
import JoyCon from 'joycon'
import { bundleRequire } from 'bundle-require'
import { jsoncParse } from './utils'
import type { defineConfig } from './'
const joycon = new JoyCon()
const loadJson = async (filepath: string) => {
try {
return jsoncParse(await fs.promises.readFile(filepath, 'utf8'))
} catch (error) {
if (error instanceof Error) {
throw new Error(
`Failed to parse ${path.relative(process.cwd(), filepath)}: ${
error.message
}`,
)
} else {
throw error
}
}
}
const jsonLoader = {
test: /\.json$/,
load(filepath: string) {
return loadJson(filepath)
},
}
joycon.addLoader(jsonLoader)
export async function loadTsupConfig(
cwd: string,
configFile?: string,
): Promise<{ path?: string; data?: ReturnType<typeof defineConfig> }> {
const configJoycon = new JoyCon()
const configPath = await configJoycon.resolve({
files: configFile
? [configFile]
: [
'tsup.config.ts',
'tsup.config.cts',
'tsup.config.mts',
'tsup.config.js',
'tsup.config.cjs',
'tsup.config.mjs',
'tsup.config.json',
'package.json',
],
cwd,
stopDir: path.parse(cwd).root,
packageKey: 'tsup',
})
if (configPath) {
if (configPath.endsWith('.json')) {
let data = await loadJson(configPath)
if (configPath.endsWith('package.json')) {
data = data.tsup
}
if (data) {
return { path: configPath, data }
}
return {}
}
const config = await bundleRequire({
filepath: configPath,
})
return {
path: configPath,
data: config.mod.tsup || config.mod.default || config.mod,
}
}
return {}
}
export async function loadPkg(cwd: string, clearCache: boolean = false) {
if (clearCache) {
joycon.clearCache()
}
const { data } = await joycon.load(['package.json'], cwd, path.dirname(cwd))
return data || {}
}
/*
* Production deps should be excluded from the bundle
*/
export async function getProductionDeps(
cwd: string,
clearCache: boolean = false,
) {
const data = await loadPkg(cwd, clearCache)
const deps = Array.from(
new Set([
...Object.keys(data.dependencies || {}),
...Object.keys(data.peerDependencies || {}),
]),
)
return deps
}
/**
* Use this to determine if we should rebuild when package.json changes
*/
export async function getAllDepsHash(cwd: string) {
const data = await loadPkg(cwd, true)
return JSON.stringify({
...data.dependencies,
...data.peerDependencies,
...data.devDependencies,
})
}
================================================
FILE: src/log.ts
================================================
import util from 'node:util'
import { isMainThread, parentPort } from 'node:worker_threads'
import colors from 'picocolors'
type LOG_TYPE = 'info' | 'success' | 'error' | 'warn'
export const colorize = (type: LOG_TYPE, data: any, onlyImportant = false) => {
if (onlyImportant && (type === 'info' || type === 'success')) return data
const color =
type === 'info'
? 'blue'
: type === 'error'
? 'red'
: type === 'warn'
? 'yellow'
: 'green'
return colors[color](data)
}
export const makeLabel = (
name: string | undefined,
input: string,
type: LOG_TYPE,
) => {
return [
name && `${colors.dim('[')}${name.toUpperCase()}${colors.dim(']')}`,
colorize(type, input.toUpperCase()),
]
.filter(Boolean)
.join(' ')
}
let silent = false
export function setSilent(isSilent?: boolean) {
silent = !!isSilent
}
export function getSilent() {
return silent
}
export type Logger = ReturnType<typeof createLogger>
export const createLogger = (name?: string) => {
return {
setName(_name: string) {
name = _name
},
success(label: string, ...args: any[]) {
return this.log(label, 'success', ...args)
},
info(label: string, ...args: any[]) {
return this.log(label, 'info', ...args)
},
error(label: string, ...args: any[]) {
return this.log(label, 'error', ...args)
},
warn(label: string, ...args: any[]) {
return this.log(label, 'warn', ...args)
},
log(
label: string,
type: 'info' | 'success' | 'error' | 'warn',
...data: unknown[]
) {
const args = [
makeLabel(name, label, type),
...data.map((item) => colorize(type, item, true)),
]
switch (type) {
case 'error': {
if (!isMainThread) {
parentPort?.postMessage({
type: 'error',
text: util.format(...args),
})
return
}
return console.error(...args)
}
default:
if (silent) return
if (!isMainThread) {
parentPort?.postMessage({
type: 'log',
text: util.format(...args),
})
return
}
console.log(...args)
}
},
}
}
================================================
FILE: src/options.ts
================================================
import type { BuildOptions, Plugin as EsbuildPlugin, Loader } from 'esbuild'
import type { InputOption } from 'rollup'
import type { MinifyOptions } from 'terser'
import type { MarkRequired } from 'ts-essentials'
import type { Plugin } from './plugin'
import type { TreeshakingStrategy } from './plugins/tree-shaking'
import type { SwcPluginConfig } from './esbuild/swc.js'
export type KILL_SIGNAL = 'SIGKILL' | 'SIGTERM'
export type Format = 'cjs' | 'esm' | 'iife'
export type ContextForOutPathGeneration = {
options: NormalizedOptions
format: Format
/** "type" field in project's package.json */
pkgType?: string
}
export type OutExtensionObject = { js?: string; dts?: string }
export type OutExtensionFactory = (
ctx: ContextForOutPathGeneration,
) => OutExtensionObject
export type DtsConfig = {
entry?: InputOption
/** Resolve external types used in dts files from node_modules */
resolve?: boolean | (string | RegExp)[]
/** Emit declaration files only */
only?: boolean
/** Insert at the top of each output .d.ts file */
banner?: string
/** Insert at the bottom */
footer?: string
/**
* Overrides `compilerOptions`
* This option takes higher priority than `compilerOptions` in tsconfig.json
*/
compilerOptions?: any
}
export type ExperimentalDtsConfig = {
entry?: InputOption
/**
* Overrides `compilerOptions`
* This option takes higher priority than `compilerOptions` in tsconfig.json
*/
compilerOptions?: any
}
export type BannerOrFooter =
| {
js?: string
css?: string
}
| ((ctx: { format: Format }) => { js?: string; css?: string } | undefined)
export type BrowserTarget =
| 'chrome'
| 'deno'
| 'edge'
| 'firefox'
| 'hermes'
| 'ie'
| 'ios'
| 'node'
| 'opera'
| 'rhino'
| 'safari'
export type BrowserTargetWithVersion =
| `${BrowserTarget}${number}`
| `${BrowserTarget}${number}.${number}`
| `${BrowserTarget}${number}.${number}.${number}`
export type EsTarget =
| 'es3'
| 'es5'
| 'es6'
| 'es2015'
| 'es2016'
| 'es2017'
| 'es2018'
| 'es2019'
| 'es2020'
| 'es2021'
| 'es2022'
| 'es2023'
| 'es2024'
| 'esnext'
export type Target =
| BrowserTarget
| BrowserTargetWithVersion
| EsTarget
| (string & {})
export type Entry = string[] | Record<string, string>
/**
* The options available in tsup.config.ts
* Not all of them are available from CLI flags
*/
export type Options = {
/** Optional config name to show in CLI output */
name?: string
/**
* @deprecated Use `entry` instead
*/
entryPoints?: Entry
entry?: Entry
/**
* Output different formats to different folder instead of using different extensions
*/
legacyOutput?: boolean
/**
* Compile target
*
* default to `node16`
*/
target?: Target | Target[]
minify?: boolean | 'terser'
terserOptions?: MinifyOptions
minifyWhitespace?: boolean
minifyIdentifiers?: boolean
minifySyntax?: boolean
keepNames?: boolean
watch?: boolean | string | (string | boolean)[]
ignoreWatch?: string[] | string
onSuccess?:
| string
| (() => Promise<void | undefined | (() => void | Promise<void>)>)
jsxFactory?: string
jsxFragment?: string
outDir?: string
outExtension?: OutExtensionFactory
format?: Format[] | Format
globalName?: string
env?: {
[k: string]: string
}
define?: {
[k: string]: string
}
dts?: boolean | string | DtsConfig
experimentalDts?: boolean | string | ExperimentalDtsConfig
sourcemap?: boolean | 'inline'
/** Always bundle modules matching given patterns */
noExternal?: (string | RegExp)[]
/** Don't bundle these modules */
external?: (string | RegExp)[]
/**
* Replace `process.env.NODE_ENV` with `production` or `development`
* `production` when the bundled is minified, `development` otherwise
*/
replaceNodeEnv?: boolean
/**
* Code splitting
* Default to `true` for ESM, `false` for CJS.
*
* You can set it to `true` explicitly, and may want to disable code splitting sometimes: [`#255`](https://github.com/egoist/tsup/issues/255)
*/
splitting?: boolean
/**
* Clean output directory before each build
*/
clean?: boolean | string[]
esbuildPlugins?: EsbuildPlugin[]
esbuildOptions?: (options: BuildOptions, context: { format: Format }) => void
/**
* Suppress non-error logs (excluding "onSuccess" process output)
*/
silent?: boolean
/**
* Skip node_modules bundling
* Will still bundle modules matching the `noExternal` option
*/
skipNodeModulesBundle?: boolean
/**
* @see https://esbuild.github.io/api/#pure
*/
pure?: string | string[]
/**
* Disable bundling, default to true
*/
bundle?: boolean
/**
* This option allows you to automatically replace a global variable with an import from another file.
* @see https://esbuild.github.io/api/#inject
*/
inject?: string[]
/**
* Emit esbuild metafile
* @see https://esbuild.github.io/api/#metafile
*/
metafile?: boolean
footer?: BannerOrFooter
banner?: BannerOrFooter
/**
* Target platform
* @default `node`
*/
platform?: 'node' | 'browser' | 'neutral'
/**
* Esbuild loader option
*/
loader?: Record<string, Loader>
/**
* Disable config file with `false`
* Or pass a custom config filename
*/
config?: boolean | string
/**
* Use a custom tsconfig
*/
tsconfig?: string
/**
* Inject CSS as style tags to document head
* @default {false}
*/
injectStyle?:
| boolean
| ((css: string, fileId: string) => string | Promise<string>)
/**
* Inject cjs and esm shims if needed
* @default false
*/
shims?: boolean
/**
* TSUP plugins
* @experimental
* @alpha
*/
plugins?: Plugin[]
/**
* By default esbuild already does treeshaking
*
* But this option allow you to perform additional treeshaking with Rollup
*
* This can result in smaller bundle size
*/
treeshake?: TreeshakingStrategy
/**
* Copy the files inside `publicDir` to output directory
*/
publicDir?: string | boolean
killSignal?: KILL_SIGNAL
/**
* Interop default within `module.exports` in cjs
* @default false
*/
cjsInterop?: boolean
/**
* Remove `node:` protocol from imports
*
* The default value will be flipped to `false` in the next major release
* @default true
*/
removeNodeProtocol?: boolean
swc?: SwcPluginConfig;
}
export interface NormalizedExperimentalDtsConfig {
entry: { [entryAlias: string]: string }
compilerOptions?: any
}
export type NormalizedOptions = Omit<
MarkRequired<Options, 'entry' | 'outDir'>,
'dts' | 'experimentalDts' | 'format'
> & {
dts?: DtsConfig
experimentalDts?: NormalizedExperimentalDtsConfig
tsconfigResolvePaths: Record<string, string[]>
tsconfigDecoratorMetadata?: boolean
format: Format[]
swc?: SwcPluginConfig
}
================================================
FILE: src/plugin.ts
================================================
import path from 'node:path'
import {
type RawSourceMap,
SourceMapConsumer,
SourceMapGenerator,
} from 'source-map'
import { outputFile } from './fs'
import type {
BuildOptions as EsbuildOptions,
Metafile,
OutputFile,
} from 'esbuild'
import type { Format, NormalizedOptions } from '.'
import type { Logger } from './log'
import { slash, type MaybePromise } from './utils'
import type { SourceMap } from 'rollup'
export type ChunkInfo = {
type: 'chunk'
code: string
map?: string | RawSourceMap | null
path: string
/**
* Sets the file mode
*/
mode?: number
entryPoint?: string
exports?: string[]
imports?: Metafile['outputs'][string]['imports']
}
export type AssetInfo = {
type: 'asset'
path: string
contents: Uint8Array
}
export type RenderChunk = (
this: PluginContext,
code: string,
chunkInfo: ChunkInfo,
) => MaybePromise<
| {
code: string
map?: object | string | SourceMap | null
}
| undefined
| null
| void
>
export type BuildStart = (this: PluginContext) => MaybePromise<void>
export type BuildEnd = (
this: PluginContext,
ctx: { writtenFiles: WrittenFile[] },
) => MaybePromise<void>
export type ModifyEsbuildOptions = (
this: PluginContext,
options: EsbuildOptions,
) => void
export type Plugin = {
name: string
esbuildOptions?: ModifyEsbuildOptions
buildStart?: BuildStart
renderChunk?: RenderChunk
buildEnd?: BuildEnd
}
export type PluginContext = {
format: Format
splitting?: boolean
options: NormalizedOptions
logger: Logger
}
export type WrittenFile = { readonly name: string; readonly size: number }
const parseSourceMap = (map?: string | object | null) => {
return typeof map === 'string' ? JSON.parse(map) : map
}
const isJS = (path: string) => /\.(js|mjs|cjs)$/.test(path)
const isCSS = (path: string) => /\.css$/.test(path)
export class PluginContainer {
plugins: Plugin[]
context?: PluginContext
constructor(plugins: Plugin[]) {
this.plugins = plugins
}
setContext(context: PluginContext) {
this.context = context
}
getContext() {
if (!this.context) throw new Error(`Plugin context is not set`)
return this.context
}
modifyEsbuildOptions(options: EsbuildOptions) {
for (const plugin of this.plugins) {
if (plugin.esbuildOptions) {
plugin.esbuildOptions.call(this.getContext(), options)
}
}
}
async buildStarted() {
for (const plugin of this.plugins) {
if (plugin.buildStart) {
await plugin.buildStart.call(this.getContext())
}
}
}
async buildFinished({
outputFiles,
metafile,
}: {
outputFiles: OutputFile[]
metafile?: Metafile
}) {
const files: Array<ChunkInfo | AssetInfo> = outputFiles
.filter((file) => !file.path.endsWith('.map'))
.map((file): ChunkInfo | AssetInfo => {
if (isJS(file.path) || isCSS(file.path)) {
const relativePath = slash(path.relative(process.cwd(), file.path))
const meta = metafile?.outputs[relativePath]
return {
type: 'chunk',
path: file.path,
code: file.text,
map: outputFiles.find((f) => f.path === `${file.path}.map`)?.text,
entryPoint: meta?.entryPoint,
exports: meta?.exports,
imports: meta?.imports,
}
} else {
return { type: 'asset', path: file.path, contents: file.contents }
}
})
const writtenFiles: WrittenFile[] = []
await Promise.all(
files.map(async (info) => {
for (const plugin of this.plugins) {
if (info.type === 'chunk' && plugin.renderChunk) {
const result = await plugin.renderChunk.call(
this.getContext(),
info.code,
info,
)
if (result) {
info.code = result.code
if (result.map) {
const originalConsumer = await new SourceMapConsumer(
parseSourceMap(info.map),
)
const newConsumer = await new SourceMapConsumer(
parseSourceMap(result.map),
)
const generator = SourceMapGenerator.fromSourceMap(newConsumer)
generator.applySourceMap(originalConsumer, info.path)
info.map = generator.toJSON()
originalConsumer.destroy()
newConsumer.destroy()
}
}
}
}
const inlineSourceMap = this.context!.options.sourcemap === 'inline'
const contents =
info.type === 'chunk'
? info.code +
getSourcemapComment(
inlineSourceMap,
info.map,
info.path,
isCSS(info.path),
)
: info.contents
await outputFile(info.path, contents, {
mode: info.type === 'chunk' ? info.mode : undefined,
})
writtenFiles.push({
get name() {
return path.relative(process.cwd(), info.path)
},
get size() {
return contents.length
},
})
if (info.type === 'chunk' && info.map && !inlineSourceMap) {
const map =
typeof info.map === 'string' ? JSON.parse(info.map) : info.map
const outPath = `${info.path}.map`
const contents = JSON.stringify(map)
await outputFile(outPath, contents)
writtenFiles.push({
get name() {
return path.relative(process.cwd(), outPath)
},
get size() {
return contents.length
},
})
}
}),
)
for (const plugin of this.plugins) {
if (plugin.buildEnd) {
await plugin.buildEnd.call(this.getContext(), { writtenFiles })
}
}
}
}
const getSourcemapComment = (
inline: boolean,
map: RawSourceMap | string | null | undefined,
filepath: string,
isCssFile: boolean,
) => {
if (!map) return ''
const prefix = isCssFile ? '/*' : '//'
const suffix = isCssFile ? ' */' : ''
const url = inline
? `data:application/json;base64,${Buffer.from(
typeof map === 'string' ? map : JSON.stringify(map),
).toString('base64')}`
: `${path.basename(filepath)}.map`
return `${prefix}# sourceMappingURL=${url}${suffix}`
}
================================================
FILE: src/plugins/cjs-interop.ts
================================================
import type { Plugin } from '../plugin'
export const cjsInterop = (): Plugin => {
return {
name: 'cjs-interop',
renderChunk(code, info) {
if (
!this.options.cjsInterop ||
this.format !== 'cjs' ||
info.type !== 'chunk' ||
!/\.(js|cjs)$/.test(info.path) ||
!info.entryPoint ||
info.exports?.length !== 1 ||
info.exports[0] !== 'default'
) {
return
}
return {
code: `${code}\nmodule.exports = exports.default;\n`,
map: info.map,
}
},
}
}
================================================
FILE: src/plugins/cjs-splitting.ts
================================================
// Workaround to enable code splitting for cjs format
// Manually transform esm to cjs
// TODO: remove this once esbuild supports code splitting for cjs natively
import type { Plugin } from '../plugin'
export const cjsSplitting = (): Plugin => {
return {
name: 'cjs-splitting',
async renderChunk(code, info) {
if (
!this.splitting ||
this.options.treeshake || // <-- handled by rollup
this.format !== 'cjs' ||
info.type !== 'chunk' ||
!/\.(js|cjs)$/.test(info.path)
) {
return
}
const { transform } = await import('sucrase')
const result = transform(code, {
filePath: info.path,
transforms: ['imports'],
sourceMapOptions: this.options.sourcemap
? {
compiledFilename: info.path,
}
: undefined,
})
return {
code: result.code,
map: result.sourceMap,
}
},
}
}
================================================
FILE: src/plugins/shebang.ts
================================================
import type { Plugin } from '../plugin'
export const shebang = (): Plugin => {
return {
name: 'shebang',
renderChunk(_, info) {
if (
info.type === 'chunk' &&
/\.(cjs|js|mjs)$/.test(info.path) &&
info.code.startsWith('#!')
) {
info.mode = 0o755
}
},
}
}
================================================
FILE: src/plugins/size-reporter.ts
================================================
import { reportSize } from '../lib/report-size'
import type { Plugin } from '../plugin'
export const sizeReporter = (): Plugin => {
return {
name: 'size-reporter',
buildEnd({ writtenFiles }) {
reportSize(
this.logger,
this.format,
writtenFiles.reduce((res, file) => {
return {
...res,
[file.name]: file.size,
}
}, {}),
)
},
}
}
================================================
FILE: src/plugins/swc-target.ts
================================================
import { PrettyError } from '../errors'
import { localRequire } from '../utils'
import type { ModuleConfig } from '@swc/core'
import type { Plugin } from '../plugin'
const TARGETS = ['es5', 'es3'] as const
export const swcTarget = (): Plugin => {
let enabled = false
let target: (typeof TARGETS)[number]
return {
name: 'swc-target',
esbuildOptions(options) {
if (
typeof options.target === 'string' &&
TARGETS.includes(options.target as any)
) {
target = options.target as any
options.target = 'es2020'
enabled = true
}
},
async renderChunk(code, info) {
if (!enabled || !/\.(cjs|mjs|js)$/.test(info.path)) {
return
}
const swc: typeof import('@swc/core') = localRequire('@swc/core')
if (!swc) {
throw new PrettyError(
`@swc/core is required for ${target} target. Please install it with \`npm install @swc/core -D\``,
)
}
const result = await swc.transform(code, {
filename: info.path,
sourceMaps: this.options.sourcemap,
minify: Boolean(this.options.minify),
jsc: {
target,
parser: {
syntax: 'ecmascript',
},
minify:
this.options.minify === true
? {
compress: false,
mangle: {
reserved: this.options.globalName
? [this.options.globalName]
: [],
},
}
: undefined,
},
module: {
type: this.format === 'cjs' ? 'commonjs' : 'es6',
} satisfies ModuleConfig,
})
return {
code: result.code,
map: result.map,
}
},
}
}
================================================
FILE: src/plugins/terser.ts
================================================
import { PrettyError } from '../errors'
import { localRequire } from '../utils'
import type { MinifyOptions } from 'terser'
import type { Logger } from '../log'
import type { Format, Options } from '../options'
import type { Plugin } from '../plugin'
export const terserPlugin = ({
minifyOptions,
format,
terserOptions = {},
globalName,
logger,
}: {
minifyOptions: Options['minify']
format: Format
terserOptions?: MinifyOptions
globalName?: string
logger: Logger
}): Plugin => {
return {
name: 'terser',
async renderChunk(code, info) {
if (minifyOptions !== 'terser' || !/\.(cjs|js|mjs)$/.test(info.path))
return
const terser: typeof import('terser') | undefined = localRequire('terser')
if (!terser) {
throw new PrettyError(
'terser is required for terser minification. Please install it with `npm install terser -D`',
)
}
const { minify } = terser
const defaultOptions: MinifyOptions = {}
if (format === 'esm') {
defaultOptions.module = true
} else if (!(format === 'iife' && globalName !== undefined)) {
defaultOptions.toplevel = true
}
try {
const minifiedOutput = await minify(
{ [info.path]: code },
{ ...defaultOptions, ...terserOptions },
)
logger.info('TERSER', 'Minifying with Terser')
if (!minifiedOutput.code) {
logger.error('TERSER', 'Failed to minify with terser')
}
logger.success('TERSER', 'Terser Minification success')
return { code: minifiedOutput.code!, map: minifiedOutput.map }
} catch (error) {
logger.error('TERSER', 'Failed to minify with terser')
logger.error('TERSER', error)
}
return { code, map: info.map }
},
}
}
================================================
FILE: src/plugins/tree-shaking.ts
================================================
import path from 'node:path'
import { type TreeshakingOptions, type TreeshakingPreset, rollup } from 'rollup'
import type { Plugin } from '../plugin'
export type TreeshakingStrategy =
| boolean
| TreeshakingOptions
| TreeshakingPreset
export const treeShakingPlugin = ({
treeshake,
name,
silent,
}: {
treeshake?: TreeshakingStrategy
name?: string
silent?: boolean
}): Plugin => {
return {
name: 'tree-shaking',
async renderChunk(code, info) {
if (!treeshake || !/\.(cjs|js|mjs)$/.test(info.path)) return
const bundle = await rollup({
input: [info.path],
plugins: [
{
name: 'tsup',
resolveId(source) {
if (source === info.path) return source
return false
},
load(id) {
if (id === info.path) return { code, map: info.map }
},
},
],
treeshake,
makeAbsoluteExternalsRelative: false,
preserveEntrySignatures: 'exports-only',
onwarn: silent ? () => {} : undefined,
})
const result = await bundle.generate({
interop: 'auto',
format: this.format,
file: info.path,
sourcemap: !!this.options.sourcemap,
compact: !!this.options.minify,
name,
})
for (const file of result.output) {
if (
file.type === 'chunk' &&
file.fileName === path.basename(info.path)
) {
return {
code: file.code,
map: file.map,
}
}
}
},
}
}
================================================
FILE: src/rollup/ts-resolve.ts
================================================
import fs from 'node:fs'
import path from 'node:path'
import { builtinModules } from 'node:module'
import _resolve from 'resolve'
import createDebug from 'debug'
import type { PluginImpl } from 'rollup'
const debug = createDebug('tsup:ts-resolve')
const resolveModule = (
id: string,
opts: _resolve.AsyncOpts,
): Promise<string | null> =>
new Promise((resolve, reject) => {
_resolve(id, opts, (err, res) => {
// @ts-expect-error error code is not typed
if (err?.code === 'MODULE_NOT_FOUND') return resolve(null)
if (err) return reject(err)
resolve(res || null)
})
})
export type TsResolveOptions = {
resolveOnly?: Array<string | RegExp>
ignore?: (source: string, importer?: string) => boolean
}
export const tsResolvePlugin: PluginImpl<TsResolveOptions> = ({
resolveOnly,
ignore,
} = {}) => {
const resolveExtensions = ['.d.ts', '.ts']
return {
name: `ts-resolve`,
async resolveId(source, importer) {
debug('resolveId source: %s', source)
debug('resolveId importer: %s ', importer)
if (!importer) return null
// ignore IDs with null character, these belong to other plugins
if (/\0/.test(source)) return null
if (builtinModules.includes(source)) return false
if (ignore && ignore(source, importer)) {
debug('ignored %s', source)
return null
}
if (resolveOnly) {
const shouldResolve = resolveOnly.some((v) => {
if (typeof v === 'string') return v === source
return v.test(source)
})
if (!shouldResolve) {
debug('skipped by matching resolveOnly: %s', source)
return null
}
}
// Skip absolute path
if (path.isAbsolute(source)) {
debug(`skipped absolute path: %s`, source)
return null
}
const basedir = importer
? await fs.promises.realpath(path.dirname(importer))
: process.cwd()
// A relative path
if (source[0] === '.') {
return resolveModule(source, {
basedir,
extensions: resolveExtensions,
})
}
let id: string | null = null
// Try resolving as relative path if `importer` is not present
if (!importer) {
id = await resolveModule(`./${source}`, {
basedir,
extensions: resolveExtensions,
})
}
// Try resolving in node_modules
if (!id) {
id = await resolveModule(source, {
basedir,
extensions: resolveExtensions,
packageFilter(pkg) {
pkg.main = pkg.types || pkg.typings
return pkg
},
paths: ['node_modules', 'node_modules/@types'],
})
}
if (id) {
debug('resolved %s to %s', source, id)
return id
}
debug('mark %s as external', source)
// Just make it external if can't be resolved, i.e. tsconfig path alias
return false
},
}
}
================================================
FILE: src/rollup.ts
================================================
import { parentPort } from 'node:worker_threads'
import path from 'node:path'
import ts from 'typescript'
import jsonPlugin from '@rollup/plugin-json'
import resolveFrom from 'resolve-from'
import { handleError } from './errors'
import { defaultOutExtension, removeFiles, toObjectEntry } from './utils'
import { type TsResolveOptions, tsResolvePlugin } from './rollup/ts-resolve'
import { createLogger, setSilent } from './log'
import { getProductionDeps, loadPkg } from './load'
import { reportSize } from './lib/report-size'
import type { NormalizedOptions } from './'
import type { InputOptions, OutputOptions, Plugin } from 'rollup'
import { FixDtsDefaultCjsExportsPlugin } from 'fix-dts-default-cjs-exports/rollup'
const logger = createLogger()
const parseCompilerOptions = (compilerOptions?: any) => {
if (!compilerOptions) return {}
const { options } = ts.parseJsonConfigFileContent(
{ compilerOptions },
ts.sys,
'./',
)
return options
}
// Use `require` to esbuild use the cjs build of rollup-plugin-dts
// the mjs build of rollup-plugin-dts uses `import.meta.url` which makes Node throws syntax error
// since tsup is published as a commonjs module for now
const dtsPlugin: typeof import('rollup-plugin-dts') = require('rollup-plugin-dts')
type RollupConfig = {
inputConfig: InputOptions
outputConfig: OutputOptions[]
}
const getRollupConfig = async (
options: NormalizedOptions,
): Promise<RollupConfig> => {
setSilent(options.silent)
const compilerOptions = parseCompilerOptions(options.dts?.compilerOptions)
const dtsOptions = options.dts || {}
dtsOptions.entry = dtsOptions.entry || options.entry
if (Array.isArray(dtsOptions.entry) && dtsOptions.entry.length > 1) {
dtsOptions.entry = toObjectEntry(dtsOptions.entry)
}
let tsResolveOptions: TsResolveOptions | undefined
if (dtsOptions.resolve) {
tsResolveOptions = {}
// Only resolve specific types when `dts.resolve` is an array
if (Array.isArray(dtsOptions.resolve)) {
tsResolveOptions.resolveOnly = dtsOptions.resolve
}
// `paths` should be handled by rollup-plugin-dts
if (compilerOptions.paths) {
const res = Object.keys(compilerOptions.paths).map(
(p) => new RegExp(`^${p.replace('*', '.+')}$`),
)
tsResolveOptions.ignore = (source) => {
return res.some((re) => re.test(source))
}
}
}
const pkg = await loadPkg(process.cwd())
const deps = await getProductionDeps(process.cwd())
const tsupCleanPlugin: Plugin = {
name: 'tsup:clean',
async buildStart() {
if (options.clean) {
await removeFiles(['**/*.d.{ts,mts,cts}'], options.outDir)
}
},
}
const ignoreFiles: Plugin = {
name: 'tsup:ignore-files',
load(id) {
if (!/\.(js|cjs|mjs|jsx|ts|tsx|mts|json)$/.test(id)) {
return ''
}
},
}
return {
inputConfig: {
input: dtsOptions.entry,
onwarn(warning, handler) {
if (
warning.code === 'UNRESOLVED_IMPORT' ||
warning.code === 'CIRCULAR_DEPENDENCY' ||
warning.code === 'EMPTY_BUNDLE'
) {
return
}
return handler(warning)
},
plugins: [
tsupCleanPlugin,
tsResolveOptions && tsResolvePlugin(tsResolveOptions),
jsonPlugin(),
ignoreFiles,
dtsPlugin.default({
tsconfig: options.tsconfig,
compilerOptions: {
...compilerOptions,
baseUrl: compilerOptions.baseUrl || '.',
// Ensure ".d.ts" modules are generated
declaration: true,
// Skip ".js" generation
noEmit: false,
emitDeclarationOnly: true,
// Skip code generation when error occurs
noEmitOnError: true,
// Avoid extra work
checkJs: false,
declarationMap: false,
skipLibCheck: true,
preserveSymlinks: false,
// Ensure we can parse the latest code
target: ts.ScriptTarget.ESNext,
},
}),
].filter(Boolean),
external: [
// Exclude dependencies, e.g. `lodash`, `lodash/get`
...deps.map((dep) => new RegExp(`^${dep}($|\\/|\\\\)`)),
...(options.external || []),
],
},
outputConfig: options.format.map((format): OutputOptions => {
const outputExtension =
options.outExtension?.({ format, options, pkgType: pkg.type }).dts ||
defaultOutExtension({ format, pkgType: pkg.type }).dts
return {
dir: options.outDir || 'dist',
format: 'esm',
exports: 'named',
banner: dtsOptions.banner,
footer: dtsOptions.footer,
entryFileNames: `[name]${outputExtension}`,
chunkFileNames: `[name]-[hash]${outputExtension}`,
plugins: [
format === 'cjs' &&
options.cjsInterop &&
FixDtsDefaultCjsExportsPlugin(),
].filter(Boolean),
}
}),
}
}
async function runRollup(options: RollupConfig) {
const { rollup } = await import('rollup')
try {
const start = Date.now()
const getDuration = () => {
return `${Math.floor(Date.now() - start)}ms`
}
logger.info('dts', 'Build start')
const bundle = await rollup(options.inputConfig)
const results = await Promise.all(options.outputConfig.map(bundle.write))
const outputs = results.flatMap((result) => result.output)
logger.success('dts', `⚡️ Build success in ${getDuration()}`)
reportSize(
logger,
'dts',
outputs.reduce((res, info) => {
const name = path.relative(
process.cwd(),
path.join(options.outputConfig[0].dir || '.', info.fileName),
)
return {
...res,
[name]: info.type === 'chunk' ? info.code.length : info.source.length,
}
}, {}),
)
} catch (error) {
handleError(error)
logger.error('dts', 'Build error')
}
}
async function watchRollup(options: {
inputConfig: InputOptions
outputConfig: OutputOptions[]
}) {
const { watch } = await import('rollup')
watch({
...options.inputConfig,
plugins: options.inputConfig.plugins,
output: options.outputConfig,
}).on('event', (event) => {
if (event.code === 'START') {
logger.info('dts', 'Build start')
} else if (event.code === 'BUNDLE_END') {
logger.success('dts', `⚡️ Build success in ${event.duration}ms`)
parentPort?.postMessage('success')
} else if (event.code === 'ERROR') {
logger.error('dts', 'Build failed')
handleError(event.error)
}
})
}
const startRollup = async (options: NormalizedOptions) => {
const config = await getRollupConfig(options)
if (options.watch) {
watchRollup(config)
} else {
try {
await runRollup(config)
parentPort?.postMessage('success')
} catch {
parentPort?.postMessage('error')
}
}
}
parentPort?.on('message', (data) => {
logger.setName(data.configName)
const hasTypescript = resolveFrom.silent(process.cwd(), 'typescript')
if (!hasTypescript) {
logger.error('dts', `You need to install "typescript" in your project`)
parentPort?.postMessage('error')
return
}
startRollup(data.options)
})
================================================
FILE: src/run.ts
================================================
import { spawn } from 'node:child_process'
export function runCode(filename: string, { args }: { args: string[] }) {
const cmd = spawn('node', [filename, ...args], {
stdio: 'inherit',
})
cmd.on('exit', (code) => {
process.exitCode = code || 0
})
}
================================================
FILE: src/tsc.ts
================================================
import { dirname } from 'node:path'
import { loadTsConfig } from 'bundle-require'
import ts from 'typescript'
import { handleError } from './errors'
import { createLogger } from './log'
import { ensureTempDeclarationDir, toAbsolutePath } from './utils'
import type { ExportDeclaration } from './exports'
import type { NormalizedOptions } from './options'
const logger = createLogger()
class AliasPool {
private seen = new Set<string>()
assign(name: string): string {
let suffix = 0
let alias = name === 'default' ? 'default_alias' : name
while (this.seen.has(alias)) {
alias = `${name}_alias_${++suffix}`
if (suffix >= 1000) {
throw new Error(
'Alias generation exceeded limit. Possible infinite loop detected.',
)
}
}
this.seen.add(alias)
return alias
}
}
/**
* Get all export declarations from root files.
*/
function getExports(
program: ts.Program,
fileMapping: Map<string, string>,
): ExportDeclaration[] {
const checker = program.getTypeChecker()
const aliasPool = new AliasPool()
const assignAlias = aliasPool.assign.bind(aliasPool)
function extractExports(sourceFileName: string): ExportDeclaration[] {
const cwd = program.getCurrentDirectory()
sourceFileName = toAbsolutePath(sourceFileName, cwd)
const sourceFile = program.getSourceFile(sourceFileName)
if (!sourceFile) {
return []
}
const destFileName = fileMapping.get(sourceFileName)
if (!destFileName) {
return []
}
const moduleSymbol = checker.getSymbolAtLocation(sourceFile)
if (!moduleSymbol) {
return []
}
const exports: ExportDeclaration[] = []
const exportSymbols = checker.getExportsOfModule(moduleSymbol)
exportSymbols.forEach((symbol) => {
const name = symbol.getName()
exports.push({
kind: 'named',
sourceFileName,
destFileName,
name,
alias: assignAlias(name),
isTypeOnly: false,
})
})
return exports
}
return program.getRootFileNames().flatMap(extractExports)
}
/**
* Use TypeScript compiler to emit declaration files.
*
* @returns The mapping from source TS file paths to output declaration file paths
*/
function emitDtsFiles(program: ts.Program, host: ts.CompilerHost) {
const fileMapping = new Map<string, string>()
const writeFile: ts.WriteFileCallback = (
fileName,
text,
writeByteOrderMark,
onError,
sourceFiles,
data,
) => {
const sourceFile = sourceFiles?.[0]
const sourceFileName = sourceFile?.fileName
if (sourceFileName && !fileName.endsWith('.map')) {
const cwd = program.getCurrentDirectory()
fileMapping.set(
toAbsolutePath(sourceFileName, cwd),
toAbsolutePath(fileName, cwd),
)
}
return host.writeFile(
fileName,
text,
writeByteOrderMark,
onError,
sourceFiles,
data,
)
}
const emitResult = program.emit(undefined, writeFile, undefined, true)
const diagnostics = ts
.getPreEmitDiagnostics(program)
.concat(emitResult.diagnostics)
const diagnosticMessages: string[] = []
diagnostics.forEach((diagnostic) => {
if (diagnostic.file) {
const { line, character } = ts.getLineAndCharacterOfPosition(
diagnostic.file,
diagnostic.start!,
)
const message = ts.flattenDiagnosticMessageText(
diagnostic.messageText,
'\n',
)
diagnosticMessages.push(
`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`,
)
} else {
const message = ts.flattenDiagnosticMessageText(
diagnostic.messageText,
'\n',
)
diagnosticMessages.push(message)
}
})
const diagnosticMessage = diagnosticMessages.join('\n')
if (diagnosticMessage) {
logger.error(
'TSC',
`Failed to emit declaration files.\n\n${diagnosticMessage}`,
)
throw new Error('TypeScript compilation failed')
}
return fileMapping
}
function emit(compilerOptions?: any, tsconfig?: string) {
const cwd = process.cwd()
const rawTsconfig = loadTsConfig(cwd, tsconfig)
if (!rawTsconfig) {
throw new Error(`Unable to find ${tsconfig || 'tsconfig.json'} in ${cwd}`)
}
const declarationDir = ensureTempDeclarationDir()
const parsedTsconfig = ts.parseJsonConfigFileContent(
{
...rawTsconfig.data,
compilerOptions: {
...rawTsconfig.data?.compilerOptions,
...compilerOptions,
// Enable declaration emit and disable javascript emit
noEmit: false,
declaration: true,
declarationMap: true,
declarationDir,
emitDeclarationOnly: true,
},
},
ts.sys,
tsconfig ? dirname(tsconfig) : './',
)
const options: ts.CompilerOptions = parsedTsconfig.options
const host: ts.CompilerHost = ts.createCompilerHost(options)
const program: ts.Program = ts.createProgram(
parsedTsconfig.fileNames,
options,
host,
)
const fileMapping = emitDtsFiles(program, host)
return getExports(program, fileMapping)
}
export function runTypeScriptCompiler(options: NormalizedOptions) {
try {
const start = Date.now()
const getDuration = () => {
return `${Math.floor(Date.now() - start)}ms`
}
logger.info('tsc', 'Build start')
const dtsOptions = options.experimentalDts!
const exports = emit(dtsOptions.compilerOptions, options.tsconfig)
logger.success('tsc', `⚡️ Build success in ${getDuration()}`)
return exports
} catch (error) {
handleError(error)
logger.error('tsc', 'Build error')
}
}
================================================
FILE: src/utils.ts
================================================
import fs from 'node:fs'
import path from 'node:path'
import resolveFrom from 'resolve-from'
import type { InputOption } from 'rollup'
import strip from 'strip-json-comments'
import { glob } from 'tinyglobby'
import type {
Entry,
Format,
NormalizedExperimentalDtsConfig,
NormalizedOptions,
Options,
} from './options'
export type MaybePromise<T> = T | Promise<T>
export type External =
| string
| RegExp
| ((id: string, parentId?: string) => boolean)
export function isExternal(
externals: External | External[],
id: string,
parentId?: string,
) {
id = slash(id)
if (!Array.isArray(externals)) {
externals = [externals]
}
for (const external of externals) {
if (
typeof external === 'string' &&
(id === external || id.includes(`/node_modules/${external}/`))
) {
return true
}
if (external instanceof RegExp && external.test(id)) {
return true
}
if (typeof external === 'function' && external(id, parentId)) {
return true
}
}
return false
}
export function getPostcss(): null | Awaited<typeof import('postcss')> {
return localRequire('postcss')
}
export function getApiExtractor(): null | Awaited<
typeof import('@microsoft/api-extractor')
> {
return localRequire('@microsoft/api-extractor')
}
export function localRequire(moduleName: string) {
const p = resolveFrom.silent(process.cwd(), moduleName)
return p && require(p)
}
export function pathExists(p: string) {
return new Promise((resolve) => {
fs.access(p, (err) => {
resolve(!err)
})
})
}
export async function removeFiles(patterns: string[], dir: string) {
const files = await glob(patterns, {
cwd: dir,
absolute: true,
})
files.forEach((file) => fs.existsSync(file) && fs.unlinkSync(file))
}
export function debouncePromise<T extends unknown[]>(
fn: (...args: T) => Promise<void>,
delay: number,
onError: (err: unknown) => void,
) {
let timeout: ReturnType<typeof setTimeout> | undefined
let promiseInFly: Promise<void> | undefined
let callbackPending: (() => void) | undefined
return function debounced(...args: Parameters<typeof fn>) {
if (promiseInFly) {
callbackPending = () => {
debounced(...args)
callbackPending = undefined
}
} else {
if (timeout != null) clearTimeout(timeout)
timeout = setTimeout(() => {
timeout = undefined
promiseInFly = fn(...args)
.catch(onError)
.finally(() => {
promiseInFly = undefined
if (callbackPending) callbackPending()
})
}, delay)
}
}
}
// Taken from https://github.com/sindresorhus/slash/blob/main/index.js (MIT)
export function slash(path: string) {
const isExtendedLengthPath = path.startsWith('\\\\?\\')
if (isExtendedLengthPath) {
return path
}
return path.replace(/\\/g, '/')
}
type Truthy<T> = T extends false | '' | 0 | null | undefined ? never : T // from lodash
export function truthy<T>(value: T): value is Truthy<T> {
return Boolean(value)
}
export function jsoncParse(data: string) {
try {
return new Function(`return ${strip(data).trim()}`)()
} catch {
// Silently ignore any error
// That's what tsc/jsonc-parser did after all
return {}
}
}
export function defaultOutExtension({
format,
pkgType,
}: {
format: Format
pkgType?: string
}): { js: string; dts: string } {
let jsExtension = '.js'
let dtsExtension = '.d.ts'
const isModule = pkgType === 'module'
if (isModule && format === 'cjs') {
jsExtension = '.cjs'
dtsExtension = '.d.cts'
}
if (!isModule && format === 'esm') {
jsExtension = '.mjs'
dtsExtension = '.d.mts'
}
if (format === 'iife') {
jsExtension = '.global.js'
}
return {
js: jsExtension,
dts: dtsExtension,
}
}
export function ensureTempDeclarationDir(): string {
const cwd = process.cwd()
const dirPath = path.join(cwd, '.tsup', 'declaration')
if (fs.existsSync(dirPath)) {
return dirPath
}
fs.mkdirSync(dirPath, { recursive: true })
const gitIgnorePath = path.join(cwd, '.tsup', '.gitignore')
writeFileSync(gitIgnorePath, '**/*\n')
return dirPath
}
// Make sure the entry is an object
// We use the base path (without extension) as the entry name
// To make declaration files work with multiple entrypoints
// See #316
export const toObjectEntry = (entry: string | Entry) => {
if (typeof entry === 'string') {
entry = [entry]
}
if (!Array.isArray(entry)) {
return entry
}
entry = entry.map((e) => e.replace(/\\/g, '/'))
const ancestor = findLowestCommonAncestor(entry)
return entry.reduce(
(result, item) => {
const key = item
.replace(ancestor, '')
.replace(/^\//, '')
.replace(/\.[a-z]+$/, '')
return {
...result,
[key]: item,
}
},
{} as Record<string, string>,
)
}
const findLowestCommonAncestor = (filepaths: string[]) => {
if (filepaths.length <= 1) return ''
const [first, ...rest] = filepaths
let ancestor = first.split('/')
for (const filepath of rest) {
const directories = filepath.split('/', ancestor.length)
let index = 0
for (const directory of directories) {
if (directory === ancestor[index]) {
index += 1
} else {
ancestor = ancestor.slice(0, index)
break
}
}
ancestor = ancestor.slice(0, index)
}
return ancestor.length <= 1 && ancestor[0] === ''
? `/${ancestor[0]}`
: ancestor.join('/')
}
export function toAbsolutePath(p: string, cwd?: string): string {
if (path.isAbsolute(p)) {
return p
}
return slash(path.normalize(path.join(cwd || process.cwd(), p)))
}
export function trimDtsExtension(fileName: string) {
return fileName.replace(/\.d\.(ts|mts|cts)x?$/, '')
}
export function writeFileSync(filePath: string, content: string) {
fs.mkdirSync(path.dirname(filePath), { recursive: true })
fs.writeFileSync(filePath, content)
}
/**
* Replaces TypeScript declaration file
* extensions (`.d.ts`, `.d.mts`, `.d.cts`)
* with their corresponding JavaScript variants (`.js`, `.mjs`, `.cjs`).
*
* @param dtsFilePath - The file path to be transformed.
* @returns The updated file path with the JavaScript extension.
*
* @internal
*/
export function replaceDtsWithJsExtensions(dtsFilePath: string) {
return dtsFilePath.replace(
/\.d\.(ts|mts|cts)$/,
(_, fileExtension: string) => {
switch (fileExtension) {
case 'ts':
return '.js'
case 'mts':
return '.mjs'
case 'cts':
return '.cjs'
default:
return ''
}
},
)
}
/**
* Converts an array of {@link NormalizedOptions.entry | entry paths}
* into an object where the keys represent the output
* file names (without extensions) and the values
* represent the corresponding input file paths.
*
* @param arrayOfEntries - An array of file path entries as strings.
* @returns An object where the keys are the output file name and the values are the input file name.
*
* @example
*
* ```ts
* import { defineConfig } from 'tsup'
*
* export default defineConfig({
* entry: ['src/index.ts', 'src/types.ts'],
* // Becomes `{ index: 'src/index.ts', types: 'src/types.ts' }`
* })
* ```
*
* @internal
*/
const convertArrayEntriesToObjectEntries = (arrayOfEntries: string[]) => {
const objectEntries = Object.fromEntries(
arrayOfEntries.map(
(entry) =>
[
path.posix.join(
...entry
.split(path.posix.sep)
.slice(1, -1)
.concat(path.parse(entry).name),
),
entry,
] as const,
),
)
return objectEntries
}
/**
* Resolves and standardizes entry paths into an object format. If the provided
* entry is a string or an array of strings, it resolves any potential glob
* patterns and converts the result into an entry object. If the input is
* already an object, it is returned as-is.
*
* @example
*
* ```ts
* import { defineConfig } from 'tsup'
*
* export default defineConfig({
* entry: { index: 'src/index.ts' },
* format: ['esm', 'cjs'],
* experimentalDts: { entry: 'src/**\/*.ts' },
* // becomes experimentalDts: { entry: { index: 'src/index.ts', types: 'src/types.ts } }
* })
* ```
*
* @internal
*/
const resolveEntryPaths = async (entryPaths: InputOption) => {
const resolvedEntryPaths =
typeof entryPaths === 'string' || Array.isArray(entryPaths)
? convertArrayEntriesToObjectEntries(await glob(entryPaths))
: entryPaths
return resolvedEntryPaths
}
/**
* Resolves the
* {@link NormalizedExperimentalDtsConfig | experimental DTS config} by
* resolving entry paths and merging the provided TypeScript configuration
* options.
*
* @param options - The options containing entry points and experimental DTS
* configuration.
* @param tsconfig - The loaded TypeScript configuration data.
*
* @internal
*/
export const resolveExperimentalDtsConfig = async (
options: NormalizedOptions,
tsconfig: any,
): Promise<NormalizedExperimentalDtsConfig> => {
const resolvedEntryPaths = await resolveEntryPaths(
options.experimentalDts?.entry || options.entry,
)
// Fallback to `options.entry` if we end up with an empty object.
const experimentalDtsObjectEntry =
Object.keys(resolvedEntryPaths).length === 0
? Array.isArray(options.entry)
? convertArrayEntriesToObjectEntries(options.entry)
: options.entry
: resolvedEntryPaths
const normalizedExperimentalDtsConfig: NormalizedExperimentalDtsConfig = {
compilerOptions: {
...(tsconfig.data.compilerOptions || {}),
...(options.experimentalDts?.compilerOptions || {}),
},
entry: experimentalDtsObjectEntry,
}
return normalizedExperimentalDtsConfig
}
/**
* Resolves the initial experimental DTS configuration into a consistent
* {@link NormalizedExperimentalDtsConfig} object.
*
* @internal
*/
export const resolveInitialExperimentalDtsConfig = async (
experimentalDts: Options['experimentalDts'],
): Promise<NormalizedExperimentalDtsConfig | undefined> => {
if (experimentalDts == null) {
return
}
if (typeof experimentalDts === 'boolean')
return experimentalDts ? { entry: {} } : undefined
if (typeof experimentalDts === 'string') {
// Treats the string as a glob pattern, resolving it to entry paths and
// returning an object with the `entry` property.
return {
entry: convertArrayEntriesToObjectEntries(await glob(experimentalDts)),
}
}
return {
...experimentalDts,
entry:
experimentalDts?.entry == null
? {}
: await resolveEntryPaths(experimentalDts.entry),
}
}
================================================
FILE: test/__snapshots__/css.test.ts.snap
================================================
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`import css 1`] = `
""use strict";
"
`;
exports[`import css in --dts 1`] = `
""use strict";
"
`;
exports[`support tailwindcss postcss plugin 1`] = `
""use strict";
"
`;
================================================
FILE: test/__snapshots__/dts.test.ts.snap
================================================
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`declaration files with multiple entrypoints #316 1`] = `
"declare const foo = 1;
export { foo };
"
`;
exports[`declaration files with multiple entrypoints #316 2`] = `
"declare const bar = "bar";
export { bar };
"
`;
exports[`enable --dts-resolve for specific module 1`] = `
"export * from 'vue';
type MarkRequired<T, RK extends keyof T> = Exclude<T, RK> & Required<Pick<T, RK>>
export type { MarkRequired };
"
`;
exports[`not bundle \`package/subpath\` in dts (resolve) 1`] = `
"import * as foo_bar from 'foo/bar';
declare const stuff: foo_bar.Foobar;
export { stuff };
"
`;
exports[`should emit declaration files with experimentalDts 1`] = `
"
//////////////////////////////////////////////////////////////////////
// dist/_tsup-dts-rollup.d.mts
//////////////////////////////////////////////////////////////////////
import { PipeableStream } from 'react-dom/server';
import { ReactDOMServerReadableStream } from 'react-dom/server';
import { renderToNodeStream } from 'react-dom/server';
import { renderToPipeableStream } from 'react-dom/server';
import { RenderToPipeableStreamOptions } from 'react-dom/server';
import { renderToReadableStream } from 'react-dom/server';
import { RenderToReadableStreamOptions } from 'react-dom/server';
import { renderToStaticMarkup } from 'react-dom/server';
import { renderToStaticNodeStream } from 'react-dom/server';
import { renderToString } from 'react-dom/server';
import { ServerOptions } from 'react-dom/server';
import * as ServerThirdPartyNamespace from 'react-dom';
import { version } from 'react-dom/server';
declare interface ClientRenderOptions {
document: boolean;
}
export { ClientRenderOptions }
export { ClientRenderOptions as ClientRenderOptions_alias_1 }
export declare function default_alias(options: ServerRenderOptions): void;
export { PipeableStream }
export { ReactDOMServerReadableStream }
declare function render(options: ClientRenderOptions): string;
export { render }
export { render as render_alias_1 }
/**
* Comment for server render function
*/
export declare function render_alias_2(options: ServerRenderOptions): string;
export { renderToNodeStream }
export { renderToPipeableStream }
export { RenderToPipeableStreamOptions }
export { renderToReadableStream }
export { RenderToReadableStreamOptions }
export { renderToStaticMarkup }
export { renderToStaticNodeStream }
export { renderToString }
export declare class ServerClass {
}
declare const serverConstant = 1;
export { serverConstant }
export { serverConstant as serverConstantAlias }
export { ServerOptions }
export declare interface ServerRenderOptions {
/**
* Comment for ServerRenderOptions.stream
*
* @public
*
* @my_custom_tag
*/
stream: boolean;
}
export { ServerThirdPartyNamespace }
declare function sharedFunction<T>(value: T): T | null;
export { sharedFunction }
export { sharedFunction as sharedFunction_alias_1 }
export { sharedFunction as sharedFunction_alias_2 }
export { sharedFunction as sharedFunction_alias_3 }
declare type sharedType = {
shared: boolean;
};
export { sharedType }
export { sharedType as sharedType_alias_1 }
export { sharedType as sharedType_alias_2 }
export { sharedType as sharedType_alias_3 }
export declare const VERSION: "0.0.0";
export { version }
export { }
//////////////////////////////////////////////////////////////////////
// dist/_tsup-dts-rollup.d.ts
//////////////////////////////////////////////////////////////////////
import { PipeableStream } from 'react-dom/server';
import { ReactDOMServerReadableStream } from 'react-dom/server';
import { renderToNodeStream } from 'react-dom/server';
import { renderToPipeableStream } from 'react-dom/server';
import { RenderToPipeableStreamOptions } from 'react-dom/server';
import { renderToReadableStream } from 'react-dom/server';
import { RenderToReadableStreamOptions } from 'react-dom/server';
import { renderToStaticMarkup } from 'react-dom/server';
import { renderToStaticNodeStream } from 'react-dom/server';
import { renderToString } from 'react-dom/server';
import { ServerOptions } from 'react-dom/server';
import * as ServerThirdPartyNamespace from 'react-dom';
import { version } from 'react-dom/server';
declare interface ClientRenderOptions {
document: boolean;
}
export { ClientRenderOptions }
export { ClientRenderOptions as ClientRenderOptions_alias_1 }
export declare function default_alias(options: ServerRenderOptions): void;
export { PipeableStream }
export { ReactDOMServerReadableStream }
declare function render(options: ClientRenderOptions): string;
export { render }
export { render as render_alias_1 }
/**
* Comment for server render function
*/
export declare function render_alias_2(options: ServerRenderOptions): string;
export { renderToNodeStream }
export { renderToPipeableStream }
export { RenderToPipeableStreamOptions }
export { renderToReadableStream }
export { RenderToReadableStreamOptions }
export { renderToStaticMarkup }
export { renderToStaticNodeStream }
export { renderToString }
export declare class ServerClass {
}
declare const serverConstant = 1;
export { serverConstant }
export { serverConstant as serverConstantAlias }
export { ServerOptions }
export declare interface ServerRenderOptions {
/**
* Comment for ServerRenderOptions.stream
*
* @public
*
* @my_custom_tag
*/
stream: boolean;
}
export { ServerThirdPartyNamespace }
declare function sharedFunction<T>(value: T): T | null;
export { sharedFunction }
export { sharedFunction as sharedFunction_alias_1 }
export { sharedFunction as sharedFunction_alias_2 }
export { sharedFunction as sharedFunction_alias_3 }
declare type sharedType = {
shared: boolean;
};
export { sharedType }
export { sharedType as sharedType_alias_1 }
export { sharedType as sharedType_alias_2 }
export { sharedType as sharedType_alias_3 }
export declare const VERSION: "0.0.0";
export { version }
export { }
//////////////////////////////////////////////////////////////////////
// dist/index.d.mts
//////////////////////////////////////////////////////////////////////
export { VERSION } from './_tsup-dts-rollup.mjs';
export { render_alias_1 as render } from './_tsup-dts-rollup.mjs';
export { ClientRenderOptions_alias_1 as ClientRenderOptions } from './_tsup-dts-rollup.mjs';
export { sharedFunction_alias_1 as sharedFunction } from './_tsup-dts-rollup.mjs';
export { sharedType_alias_1 as sharedType } from './_tsup-dts-rollup.mjs';
//////////////////////////////////////////////////////////////////////
// dist/index.d.ts
//////////////////////////////////////////////////////////////////////
export { VERSION } from './_tsup-dts-rollup.js';
export { render_alias_1 as render } from './_tsup-dts-rollup.js';
export { ClientRenderOptions_alias_1 as ClientRenderOptions } from './_tsup-dts-rollup.js';
export { sharedFunction_alias_1 as sharedFunction } from './_tsup-dts-rollup.js';
export { sharedType_alias_1 as sharedType } from './_tsup-dts-rollup.js';
//////////////////////////////////////////////////////////////////////
// dist/my-lib-client.d.mts
//////////////////////////////////////////////////////////////////////
export { render } from './_tsup-dts-rollup.mjs';
export { ClientRenderOptions } from './_tsup-dts-rollup.mjs';
export { sharedFunction } from './_tsup-dts-rollup.mjs';
export { sharedType } from './_tsup-dts-rollup.mjs';
//////////////////////////////////////////////////////////////////////
// dist/my-lib-client.d.ts
//////////////////////////////////////////////////////////////////////
export { render } from './_tsup-dts-rollup.js';
export { ClientRenderOptions } from './_tsup-dts-rollup.js';
export { sharedFunction } from './_tsup-dts-rollup.js';
export { sharedType } from './_tsup-dts-rollup.js';
//////////////////////////////////////////////////////////////////////
// dist/server/index.d.mts
//////////////////////////////////////////////////////////////////////
export { render_alias_2 as render } from '../_tsup-dts-rollup.mjs';
export { default_alias as default } from '../_tsup-dts-rollup.mjs';
export { ServerRenderOptions } from '../_tsup-dts-rollup.mjs';
export { serverConstant } from '../_tsup-dts-rollup.mjs';
export { serverConstantAlias } from '../_tsup-dts-rollup.mjs';
export { ServerClass } from '../_tsup-dts-rollup.mjs';
export { ServerThirdPartyNamespace } from '../_tsup-dts-rollup.mjs';
export { sharedFunction_alias_2 as sharedFunction } from '../_tsup-dts-rollup.mjs';
export { sharedType_alias_2 as sharedType } from '../_tsup-dts-rollup.mjs';
export { renderToPipeableStream } from '../_tsup-dts-rollup.mjs';
export { renderToString } from '../_tsup-dts-rollup.mjs';
export { renderToNodeStream } from '../_tsup-dts-rollup.mjs';
export { renderToStaticMarkup } from '../_tsup-dts-rollup.mjs';
export { renderToStaticNodeStream } from '../_tsup-dts-rollup.mjs';
export { renderToReadableStream } from '../_tsup-dts-rollup.mjs';
export { RenderToPipeableStreamOptions } from '../_tsup-dts-rollup.mjs';
export { PipeableStream } from '../_tsup-dts-rollup.mjs';
export { ServerOptions } from '../_tsup-dts-rollup.mjs';
export { RenderToReadableStreamOptions } from '../_tsup-dts-rollup.mjs';
export { ReactDOMServerReadableStream } from '../_tsup-dts-rollup.mjs';
export { version } from '../_tsup-dts-rollup.mjs';
//////////////////////////////////////////////////////////////////////
// dist/server/index.d.ts
//////////////////////////////////////////////////////////////////////
export { render_alias_2 as render } from '../_tsup-dts-rollup.js';
export { default_alias as default } from '../_tsup-dts-rollup.js';
export { ServerRenderOptions } from '../_tsup-dts-rollup.js';
export { serverConstant } from '../_tsup-dts-rollup.js';
export { serverConstantAlias } from '../_tsup-dts-rollup.js';
export { ServerClass } from '../_tsup-dts-rollup.js';
export { ServerThirdPartyNamespace } from '../_tsup-dts-rollup.js';
export { sharedFunction_alias_2 as sharedFunction } from '../_tsup-dts-rollup.js';
export { sharedType_alias_2 as sharedType } from '../_tsup-dts-rollup.js';
export { renderToPipeableStream } from '../_tsup-dts-rollup.js';
export { renderToString } from '../_tsup-dts-rollup.js';
export { renderToNodeStream } from '../_tsup-dts-rollup.js';
export { renderToStaticMarkup } from '../_tsup-dts-rollup.js';
export { renderToStaticNodeStream } from '../_tsup-dts-rollup.js';
export { renderToReadableStream } from '../_tsup-dts-rollup.js';
export { RenderToPipeableStreamOptions } from '../_tsup-dts-rollup.js';
export { PipeableStream } from '../_tsup-dts-rollup.js';
export { ServerOptions } from '../_tsup-dts-rollup.js';
export { RenderToReadableStreamOptions } from '../_tsup-dts-rollup.js';
export { ReactDOMServerReadableStream } from '../_tsup-dts-rollup.js';
export { version } from '../_tsup-dts-rollup.js';
"
`;
================================================
FILE: test/__snapshots__/index.test.ts.snap
================================================
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`code splitting in cjs format 1`] = `
""use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } }// input.ts
var foo = () => Promise.resolve().then(() => _interopRequireWildcard(require("./foo-D62QZYUQ.js")));
exports.foo = foo;
"
`;
exports[`code splitting in cjs format 2`] = `
""use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } }// another-input.ts
var foo = () => Promise.resolve().then(() => _interopRequireWildcard(require("./foo-D62QZYUQ.js")));
exports.foo = foo;
"
`;
exports[`disable code splitting to get proper module.exports = 1`] = `
""use strict";
// input.ts
module.exports = 123;
"
`;
exports[`don't remove node protocol 1`] = `
""use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
// input.ts
var import_node_fs = __toESM(require("node:fs"));
console.log(import_node_fs.default);
"
`;
exports[`external 1`] = `
""use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// input.ts
var input_exports = {};
__export(input_exports, {
bar: () => import_bar.bar,
baz: () => baz,
foo: () => import_foo.foo,
qux: () => import_qux.qux
});
module.exports = __toCommonJS(input_exports);
var import_foo = require("foo");
var import_bar = require("bar");
// node_modules/baz/index.ts
var baz = "baz";
// input.ts
var import_qux = require("qux");
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
bar,
baz,
foo,
qux
});
"
`;
exports[`multiple targets 1`] = `
""use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// input.ts
var input_exports = {};
__export(input_exports, {
answer: () => answer
});
module.exports = __toCommonJS(input_exports);
var answer = 42;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
answer
});
"
`;
exports[`node protocol 1`] = `
""use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
// input.ts
var import_node_fs = __toESM(require("fs"));
console.log(import_node_fs.default);
"
`;
exports[`simple 1`] = `
""use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// input.ts
var input_exports = {};
__export(input_exports, {
default: () => input_default
});
module.exports = __toCommonJS(input_exports);
// foo.ts
var foo_default = "foo";
// input.ts
var input_default = foo_default;
"
`;
================================================
FILE: test/__snapshots__/tsconfig.test.ts.snap
================================================
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`support baseUrl and paths in tsconfig.json 1`] = `
"var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// input.ts
var input_exports = {};
__export(input_exports, {
foo: () => foo
});
module.exports = __toCommonJS(input_exports);
// foo.ts
var foo = "foo";
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
foo
});
"
`;
exports[`support baseUrl and paths in tsconfig.json in --dts build 1`] = `
"declare const foo = "foo";
export { foo };
"
`;
exports[`support baseUrl and paths in tsconfig.json in --dts-resolve build 1`] = `
"declare const foo = "foo";
export { foo };
"
`;
================================================
FILE: test/css.test.ts
================================================
import { expect, test } from 'vitest'
import { getTestName, run } from './utils'
test('import css', async () => {
const { output, outFiles } = await run(getTestName(), {
'input.ts': `
import './foo.css'
`,
'postcss.config.js': `
module.exports = {
plugins: [require('postcss-simple-vars')()]
}
`,
'foo.css': `
$color: blue;
.foo {
color: $color;
}
`,
})
expect(output, `""`).toMatchSnapshot()
expect(outFiles).toEqual(['input.css', 'input.js'])
})
test('support tailwindcss postcss plugin', async () => {
const { output, outFiles } = await run(getTestName(), {
'input.ts': `
import './foo.css'
`,
'postcss.config.js': `
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
}
}
`,
'foo.css': `
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
})
expect(output).toMatchSnapshot()
expect(outFiles).toEqual(['input.css', 'input.js'])
})
test('import css in --dts', async () => {
const { output, outFiles } = await run(
getTestName(),
{
'input.ts': `
import './foo.css'
`,
'foo.css': `
.foo {
color: blue
}
`,
},
{ flags: ['--dts'] },
)
expect(output).toMatchSnapshot()
expect(outFiles).toEqual(['input.css', 'input.d.ts', 'input.js'])
})
================================================
FILE: test/dts.test.ts
================================================
import path from 'node:path'
import { expect, test } from 'vitest'
import { slash } from '../src/utils'
import { getTestName, run } from './utils'
test('not bundle `package/subpath` in dts (resolve)', async () => {
const { getFileContent } = await run(
getTestName(),
{
'package.json': `{ "dependencies": { "foo": "*" } }`,
'input.ts': `export const stuff: import('foo/bar').Foobar = { foo: 'foo', bar: 'bar' };`,
'node_modules/foo/bar.d.ts': `export type Foobar = { foo: 'foo', bar: 'bar' }`,
'node_modules/foo/package.json': `{ "name": "foo", "version": "0.0.0" }`,
},
{
flags: ['--dts', '--dts-resolve'],
},
)
const content = await getFileContent('dist/input.d.ts')
expect(content).toMatchSnapshot()
})
test('enable --dts-resolve for specific module', async () => {
const { getFileContent } = await run(getTestName(), {
'input.ts': `export * from 'vue'
export type {MarkRequired} from 'foo'
`,
'node_modules/foo/index.d.ts': `
export type MarkRequired<T, RK extends keyof T> = Exclude<T, RK> & Required<Pick<T, RK>>
`,
'node_modules/foo/package.json': `{ "name": "foo", "version": "0.0.0" }`,
'tsup.config.ts': `
export default {
dts: {
resolve: ['foo']
},
}
`,
})
const content = await getFileContent('dist/input.d.ts')
expect(content).toMatchSnapshot()
})
test(`custom tsconfig should pass to dts plugin`, async () => {
const { outFiles } = await run(getTestName(), {
'input.ts': `export const foo = { name: 'foo'}`,
'tsconfig.json': `{
"compilerOptions": {
"baseUrl":".",
"target": "esnext",
"incremental": true
}
}`,
'tsconfig.build.json': `{
"compilerOptions": {
"baseUrl":".",
"target": "esnext"
}
}`,
'tsup.config.ts': `
export default {
entry: ['src/input.ts'],
format: 'esm',
tsconfig: './tsconfig.build.json',
dts: {
only: true
}
}
`,
})
expect(outFiles).toEqual(['input.d.mts'])
})
test('should emit a declaration file per format', async () => {
const { outFiles } = await run(getTestName(), {
'input.ts': `export default 'foo'`,
'tsup.config.ts': `
export default {
entry: ['src/input.ts'],
format: ['esm', 'cjs'],
dts: true
}`,
})
expect(outFiles).toEqual([
'input.d.mts',
'input.d.ts',
'input.js',
'input.mjs',
])
})
test('should emit a declaration file per format (type: module)', async () => {
const { outFiles } = await run(getTestName(), {
'input.ts': `export default 'foo'`,
'package.json': `{
"type": "module"
}`,
'tsup.config.ts': `
export default {
entry: ['src/input.ts'],
format: ['esm', 'cjs'],
dts: true
}`,
})
expect(outFiles).toEqual([
'input.cjs',
'input.d.cts',
'input.d.ts',
'input.js',
])
})
test('should emit dts chunks per format', async () => {
const { outFiles } = await run(
getTestName(),
{
'src/input1.ts': `
import type { InternalType } from './shared.js'
export function getValue(value: InternalType) {
return value;
}
`,
'src/input2.ts': `
import type { InternalType } from './shared.js'
export function getValue(value: InternalType) {
return value;
}
`,
'src/shared.ts': `export type InternalType = 'foo'`,
'tsup.config.ts': `
export default {
entry: ['./src/input1.ts', './src/input2.ts'],
format: ['esm', 'cjs'],
dts: true
}`,
},
{ entry: [] },
)
expect(outFiles).toEqual([
'input1.d.mts',
'input1.d.ts',
'input1.js',
'input1.mjs',
'input2.d.mts',
'input2.d.ts',
'input2.js',
'input2.mjs',
'shared-jWa9aNVo.d.mts',
'shared-jWa9aNVo.d.ts',
])
})
test('should emit dts chunks per format (type: module)', async () => {
const { outFiles } = await run(
getTestName(),
{
'src/input1.ts': `
import type { InternalType } from './shared.js'
export function getValue(value: InternalType) {
return value;
}
`,
'src/input2.ts': `
import type { InternalType } from './shared.js'
export function getValue(value: InternalType) {
return value;
}
`,
'src/shared.ts': `export type InternalType = 'foo'`,
'tsup.config.ts': `
export default {
entry: ['./src/input1.ts', './src/input2.ts'],
format: ['esm', 'cjs'],
dts: true
}`,
'package.json': `{
"type": "module"
}`,
},
{ entry: [] },
)
expect(outFiles).toEqual([
'input1.cjs',
'input1.d.cts',
'input1.d.ts',
'input1.js',
'input2.cjs',
'input2.d.cts',
'input2.d.ts',
'input2.js',
'shared-jWa9aNVo.d.cts',
'shared-jWa9aNVo.d.ts',
])
})
test('should emit declaration files with experimentalDts', async () => {
const files = {
'package.json': `
{
"name": "tsup-playground",
"private": true,
"version": "0.0.0",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"require": "./dist/index.js",
"import": "./dist/index.mjs",
"default": "./dist/index.js"
},
"./client": {
"types": "./dist/my-lib-client.d.ts",
"require": "./dist/my-lib-client.js",
"import": "./dist/my-lib-client.mjs",
"default": "./dist/my-lib-client.js"
},
"./server": {
"types": "./dist/server/index.d.ts",
"require": "./dist/server/index.js",
"import": "./dist/server/index.mjs",
"default": "./dist/server/index.js"
}
}
}
`,
'tsconfig.json': `
{
"compilerOptions": {
"target": "ES2020",
"skipLibCheck": true,
"noEmit": true
},
"include": ["./src"]
}
`,
'tsup.config.ts': `
export default {
name: 'tsup',
target: 'es2022',
format: [
'esm',
'cjs'
],
entry: {
index: './src/index.ts',
'my-lib-client': './src/client.ts',
'server/index': './src/server.ts',
},
}
`,
'src/shared.ts': `
export function sharedFunction<T>(value: T): T | null {
return value || null
}
type sharedType = {
shared: boolean
}
export type { sharedType }
`,
'src/server.ts': `
export * from './shared'
/**
* Comment for server render function
*/
export function render(options: ServerRenderOptions): string {
return JSON.stringify(options)
}
export interface ServerRenderOptions {
/**
* Comment for ServerRenderOptions.stream
*
* @public
*
* @my_custom_tag
*/
stream: boolean
}
export const serverConstant = 1
export { serverConstant as serverConstantAlias }
export class ServerClass {};
export default function serverDefaultExport(options: ServerRenderOptions): void {};
// Export a third party module as a namespace
import * as ServerThirdPartyNamespace from 'react-dom';
export { ServerThirdPartyNamespace }
// Export a third party module
export * from 'react-dom/server';
`,
'src/client.ts': `
export * from './shared'
export function render(options: ClientRenderOptions): string {
return JSON.stringify(options)
}
export interface ClientRenderOptions {
document: boolean
}
`,
'src/index.ts': `
export * from './client'
export * from './shared'
export const VERSION = '0.0.0' as const
`,
}
const { outFiles, getFileContent } = await run(getTestName(), files, {
entry: [],
flags: ['--experimental-dts'],
})
const snapshots: string[] = []
await Promise.all(
outFiles
.filter((outFile) => outFile.includes('.d.'))
.map(async (outFile) => {
const filePath = path.join('dist', outFile)
const content = await getFileContent(filePath)
snapshots.push(
[
'',
'/'.repeat(70),
`// ${path.posix.normalize(slash(filePath))}`,
'/'.repeat(70),
'',
content,
].join('\n'),
)
}),
)
expect(snapshots.sort().join('\n')).toMatchSnapshot()
})
test('should only include exported declarations with experimentalDts', async () => {
const files = {
'package.json': `{ "name": "tsup-playground", "private": true }`,
'tsconfig.json': `{ "compilerOptions": { "skipLibCheck": true } }`,
'tsup.config.ts': `
export default {
entry: ['./src/entry1.ts', './src/entry2.ts']
}
`,
'src/shared.ts': `
export const declare1 = 'declare1'
export const declare2 = 'declare2'
`,
'src/entry1.ts': `
export { declare1 } from './shared'
`,
'src/entry2.ts': `
export { declare2 } from './shared'
`,
}
const { getFileContent } = await run(getTestName(), files, {
entry: [],
flags: ['--experimental-dts'],
})
const entry1dts = await getFileContent('dist/entry1.d.ts')
const entry2dts = await getFileContent('dist/entry2.d.ts')
expect(entry1dts).toContain('declare1')
expect(entry1dts).not.toContain('declare2')
expect(entry2dts).toContain('declare2')
expect(entry2dts).not.toContain('declare1')
})
test('.d.ts files should be cleaned when --clean and --experimental-dts are provided', async () => {
const filesFoo = {
'package.json': `{ "name": "tsup-playground", "private": true }`,
'foo.ts': `export const foo = 1`,
'tsconfig.json': JSON.stringify(
{
compilerOptions: { skipLibCheck: true },
},
null,
2,
),
}
const filesFooBar = {
...filesFoo,
'bar.ts': `export const bar = 2`,
}
// First run with both foo and bar
const result1 = await run(getTestName(), filesFooBar, {
entry: ['foo.ts', 'bar.ts'],
flags: ['--experimental-dts'],
})
expect(result1.outFiles).toContain('foo.d.ts')
expect(result1.outFiles).toContain('foo.js')
expect(result1.outFiles).toContain('bar.d.ts')
expect(result1.outFiles).toContain('bar.js')
// Second run with only foo
const result2 = await run(getTestName(), filesFoo, {
entry: ['foo.ts'],
flags: ['--experimental-dts'],
})
// When --clean is not provided, the previous bar.* files should still exist
expect(result2.outFiles).toContain('foo.d.ts')
expect(result2.outFiles).toContain('foo.js')
expect(result2.outFiles).toContain('bar.d.ts')
expect(result2.outFiles).toContain('bar.js')
// Third run with only foo and --clean
const result3 = await run(getTestName(), filesFoo, {
entry: ['foo.ts'],
flags: ['--experimental-dts', '--clean'],
})
// When --clean is provided, the previous bar.* files should be deleted
expect(result3.outFiles).toContain('foo.d.ts')
expect(result3.outFiles).toContain('foo.js')
expect(result3.outFiles).not.toContain('bar.d.ts')
expect(result3.outFiles).not.toContain('bar.js')
})
test('dts only: ignore files', async () => {
const { outFiles } = await run(
getTestName(),
{
'input.ts': `
import './style.scss'
export const a = 1
`,
'style.scss': `
@keyframes gallery-loading-spinner {
0% {}
}
`,
},
{
entry: ['input.ts'],
flags: ['--dts-only'],
},
)
expect(outFiles).toMatchInlineSnapshot(`
[
"input.d.ts",
]
`)
})
test('declaration files with multiple entrypoints #316', async () => {
const { getFileContent } = await run(
getTestName(),
{
'src/index.ts': `export const foo = 1`,
'src/bar/index.ts': `export const bar = 'bar'`,
},
{ flags: ['--dts'], entry: ['src/index.ts', 'src/bar/index.ts'] },
)
expect(
await getFileContent('dist/index.d.ts'),
'dist/index.d.ts',
).toMatchSnapshot()
expect(
await getFileContent('dist/bar/index.d.ts'),
'dist/bar/index.d.ts',
).toMatchSnapshot()
})
================================================
FILE: test/example.test.ts
================================================
import { test } from 'vitest'
import { getTestName, run } from './utils'
test('bundle vue and ts-essentials with --dts --dts-resolve flag', async () => {
await run(
getTestName(),
{
'input.ts': `export * from 'vue'
export type { MarkRequired } from 'ts-essentials'
`,
},
{
flags: ['--dts', '--dts-resolve'],
},
)
})
test('bundle @egoist/path-parser with --dts --dts-resolve flag', async () => {
await run(
getTestName(),
{
'input.ts': `import type { PathParser } from '@egoist/path-parser'
export type Opts = {
parser: PathParser
route: string
}
`,
},
{
flags: ['--dts', '--dts-resolve'],
},
)
})
================================================
FILE: test/experimental-dts.test.ts
================================================
import { test } from 'vitest'
import type { Options } from '../src/index.js'
import { getTestName, run } from './utils.js'
test.for([
{ moduleResolution: 'NodeNext', moduleKind: 'NodeNext' },
{ moduleResolution: 'Node16', moduleKind: 'Node16' },
{ moduleResolution: 'Bundler', moduleKind: 'ESNext' },
{ moduleResolution: 'Bundler', moduleKind: 'Preserve' },
{ moduleResolution: 'Node10', moduleKind: 'ESNext' },
{ moduleResolution: 'Node10', moduleKind: 'CommonJS' },
{ moduleResolution: 'Node', moduleKind: 'ESNext' },
{ moduleResolution: 'Node', moduleKind: 'CommonJS' },
] as const)(
"experimentalDts works with TypeScript's $moduleResolution module resolution and module set to $moduleKind",
async ({ moduleResolution, moduleKind }, { expect, task }) => {
const { getFileContent, outFiles } = await run(
getTestName(),
{
'src/types.ts': `export type Person = { name: string }`,
'src/index.ts': `expo
gitextract_11at8itm/ ├── .devcontainer/ │ ├── Dockerfile │ └── devcontainer.json ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── renovate.json5 │ └── workflows/ │ ├── ci.yml │ ├── format.yml │ ├── release-continuous.yml │ └── release.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets/ │ ├── cjs_shims.js │ ├── esm_shims.js │ └── package.json ├── docs/ │ ├── README.md │ └── index.html ├── package.json ├── schema.json ├── src/ │ ├── api-extractor.ts │ ├── cli-default.ts │ ├── cli-main.ts │ ├── cli-node.ts │ ├── errors.ts │ ├── esbuild/ │ │ ├── external.ts │ │ ├── index.ts │ │ ├── native-node-module.ts │ │ ├── node-protocol.ts │ │ ├── postcss.ts │ │ ├── svelte.ts │ │ ├── swc.test.ts │ │ └── swc.ts │ ├── exports.ts │ ├── fs.ts │ ├── index.ts │ ├── lib/ │ │ ├── public-dir.ts │ │ └── report-size.ts │ ├── load.ts │ ├── log.ts │ ├── options.ts │ ├── plugin.ts │ ├── plugins/ │ │ ├── cjs-interop.ts │ │ ├── cjs-splitting.ts │ │ ├── shebang.ts │ │ ├── size-reporter.ts │ │ ├── swc-target.ts │ │ ├── terser.ts │ │ └── tree-shaking.ts │ ├── rollup/ │ │ └── ts-resolve.ts │ ├── rollup.ts │ ├── run.ts │ ├── tsc.ts │ └── utils.ts ├── test/ │ ├── __snapshots__/ │ │ ├── css.test.ts.snap │ │ ├── dts.test.ts.snap │ │ ├── index.test.ts.snap │ │ └── tsconfig.test.ts.snap │ ├── css.test.ts │ ├── dts.test.ts │ ├── example.test.ts │ ├── experimental-dts.test.ts │ ├── graphql.test.ts │ ├── index.test.ts │ ├── package.json │ ├── shims.test.ts │ ├── svelte.test.ts │ ├── tsconfig.test.ts │ └── utils.ts ├── tsconfig.json ├── tsup.config.ts ├── vitest-global.ts └── vitest.config.mts
SYMBOL INDEX (122 symbols across 31 files)
FILE: src/api-extractor.ts
function rollupDtsFile (line 27) | function rollupDtsFile(
function rollupDtsFiles (line 86) | async function rollupDtsFiles(
function cleanDtsFiles (line 172) | async function cleanDtsFiles(options: NormalizedOptions) {
function runDtsRollup (line 178) | async function runDtsRollup(
FILE: src/cli-main.ts
function ensureArray (line 7) | function ensureArray(input: string): string[] {
function main (line 11) | async function main(options: Options = {}) {
FILE: src/errors.ts
class PrettyError (line 4) | class PrettyError extends Error {
method constructor (line 5) | constructor(message: string) {
function handleError (line 16) | function handleError(error: any) {
FILE: src/esbuild/external.ts
constant NON_NODE_MODULE_RE (line 5) | const NON_NODE_MODULE_RE = /^[A-Z]:[/\\]|^\.{0,2}\/|^\.{1,2}$/
method setup (line 21) | setup(build) {
FILE: src/esbuild/index.ts
function runEsbuild (line 61) | async function runEsbuild(
FILE: src/esbuild/native-node-module.ts
method setup (line 8) | setup(build) {
FILE: src/esbuild/node-protocol.ts
method setup (line 12) | setup({ onResolve }) {
FILE: src/esbuild/postcss.ts
method setup (line 18) | setup(build) {
FILE: src/esbuild/svelte.ts
method setup (line 17) | setup(build) {
FILE: src/esbuild/swc.ts
type SwcPluginConfig (line 10) | type SwcPluginConfig = { logger: Logger } & Options
method setup (line 16) | setup(build) {
FILE: src/exports.ts
type ExportDeclaration (line 4) | type ExportDeclaration = ModuleExport | NamedExport
type ModuleExport (line 6) | interface ModuleExport {
type NamedExport (line 14) | interface NamedExport {
function formatAggregationExports (line 23) | function formatAggregationExports(
function formatAggregationExport (line 40) | function formatAggregationExport(
function formatDistributionExports (line 70) | function formatDistributionExports(
function formatDistributionExport (line 118) | function formatDistributionExport(
FILE: src/fs.ts
function copyDirSync (line 13) | function copyDirSync(srcDir: string, destDir: string): void {
FILE: src/index.ts
function build (line 167) | async function build(_options: Options) {
FILE: src/load.ts
method load (line 28) | load(filepath: string) {
function loadTsupConfig (line 35) | async function loadTsupConfig(
function loadPkg (line 82) | async function loadPkg(cwd: string, clearCache: boolean = false) {
function getProductionDeps (line 93) | async function getProductionDeps(
function getAllDepsHash (line 112) | async function getAllDepsHash(cwd: string) {
FILE: src/log.ts
type LOG_TYPE (line 5) | type LOG_TYPE = 'info' | 'success' | 'error' | 'warn'
function setSilent (line 36) | function setSilent(isSilent?: boolean) {
function getSilent (line 40) | function getSilent() {
type Logger (line 44) | type Logger = ReturnType<typeof createLogger>
method setName (line 48) | setName(_name: string) {
method success (line 52) | success(label: string, ...args: any[]) {
method info (line 56) | info(label: string, ...args: any[]) {
method error (line 60) | error(label: string, ...args: any[]) {
method warn (line 64) | warn(label: string, ...args: any[]) {
method log (line 68) | log(
FILE: src/options.ts
type KILL_SIGNAL (line 9) | type KILL_SIGNAL = 'SIGKILL' | 'SIGTERM'
type Format (line 11) | type Format = 'cjs' | 'esm' | 'iife'
type ContextForOutPathGeneration (line 13) | type ContextForOutPathGeneration = {
type OutExtensionObject (line 20) | type OutExtensionObject = { js?: string; dts?: string }
type OutExtensionFactory (line 22) | type OutExtensionFactory = (
type DtsConfig (line 26) | type DtsConfig = {
type ExperimentalDtsConfig (line 43) | type ExperimentalDtsConfig = {
type BannerOrFooter (line 52) | type BannerOrFooter =
type BrowserTarget (line 59) | type BrowserTarget =
type BrowserTargetWithVersion (line 71) | type BrowserTargetWithVersion =
type EsTarget (line 75) | type EsTarget =
type Target (line 91) | type Target =
type Entry (line 97) | type Entry = string[] | Record<string, string>
type Options (line 103) | type Options = {
type NormalizedExperimentalDtsConfig (line 264) | interface NormalizedExperimentalDtsConfig {
type NormalizedOptions (line 269) | type NormalizedOptions = Omit<
FILE: src/plugin.ts
type ChunkInfo (line 18) | type ChunkInfo = {
type AssetInfo (line 32) | type AssetInfo = {
type RenderChunk (line 38) | type RenderChunk = (
type BuildStart (line 52) | type BuildStart = (this: PluginContext) => MaybePromise<void>
type BuildEnd (line 53) | type BuildEnd = (
type ModifyEsbuildOptions (line 58) | type ModifyEsbuildOptions = (
type Plugin (line 63) | type Plugin = {
type PluginContext (line 75) | type PluginContext = {
type WrittenFile (line 82) | type WrittenFile = { readonly name: string; readonly size: number }
class PluginContainer (line 91) | class PluginContainer {
method constructor (line 95) | constructor(plugins: Plugin[]) {
method setContext (line 99) | setContext(context: PluginContext) {
method getContext (line 103) | getContext() {
method modifyEsbuildOptions (line 108) | modifyEsbuildOptions(options: EsbuildOptions) {
method buildStarted (line 116) | async buildStarted() {
method buildFinished (line 124) | async buildFinished({
FILE: src/plugins/cjs-interop.ts
method renderChunk (line 7) | renderChunk(code, info) {
FILE: src/plugins/cjs-splitting.ts
method renderChunk (line 10) | async renderChunk(code, info) {
FILE: src/plugins/shebang.ts
method renderChunk (line 7) | renderChunk(_, info) {
FILE: src/plugins/size-reporter.ts
method buildEnd (line 8) | buildEnd({ writtenFiles }) {
FILE: src/plugins/swc-target.ts
constant TARGETS (line 6) | const TARGETS = ['es5', 'es3'] as const
method esbuildOptions (line 15) | esbuildOptions(options) {
method renderChunk (line 26) | async renderChunk(code, info) {
FILE: src/plugins/terser.ts
method renderChunk (line 24) | async renderChunk(code, info) {
FILE: src/plugins/tree-shaking.ts
type TreeshakingStrategy (line 5) | type TreeshakingStrategy =
method renderChunk (line 22) | async renderChunk(code, info) {
FILE: src/rollup.ts
type RollupConfig (line 33) | type RollupConfig = {
method buildStart (line 77) | async buildStart() {
method load (line 86) | load(id) {
method onwarn (line 96) | onwarn(warning, handler) {
function runRollup (line 161) | async function runRollup(options: RollupConfig) {
function watchRollup (line 193) | async function watchRollup(options: {
FILE: src/rollup/ts-resolve.ts
type TsResolveOptions (line 23) | type TsResolveOptions = {
method resolveId (line 37) | async resolveId(source, importer) {
FILE: src/run.ts
function runCode (line 3) | function runCode(filename: string, { args }: { args: string[] }) {
FILE: src/tsc.ts
class AliasPool (line 12) | class AliasPool {
method assign (line 15) | assign(name: string): string {
function getExports (line 36) | function getExports(
function emitDtsFiles (line 89) | function emitDtsFiles(program: ts.Program, host: ts.CompilerHost) {
function emit (line 163) | function emit(compilerOptions?: any, tsconfig?: string) {
function runTypeScriptCompiler (line 204) | function runTypeScriptCompiler(options: NormalizedOptions) {
FILE: src/utils.ts
type MaybePromise (line 15) | type MaybePromise<T> = T | Promise<T>
type External (line 17) | type External =
function isExternal (line 22) | function isExternal(
function getPostcss (line 51) | function getPostcss(): null | Awaited<typeof import('postcss')> {
function getApiExtractor (line 55) | function getApiExtractor(): null | Awaited<
function localRequire (line 61) | function localRequire(moduleName: string) {
function pathExists (line 66) | function pathExists(p: string) {
function removeFiles (line 74) | async function removeFiles(patterns: string[], dir: string) {
function debouncePromise (line 82) | function debouncePromise<T extends unknown[]>(
function slash (line 116) | function slash(path: string) {
type Truthy (line 126) | type Truthy<T> = T extends false | '' | 0 | null | undefined ? never : T...
function truthy (line 128) | function truthy<T>(value: T): value is Truthy<T> {
function jsoncParse (line 132) | function jsoncParse(data: string) {
function defaultOutExtension (line 142) | function defaultOutExtension({
function ensureTempDeclarationDir (line 169) | function ensureTempDeclarationDir(): string {
function toAbsolutePath (line 236) | function toAbsolutePath(p: string, cwd?: string): string {
function trimDtsExtension (line 244) | function trimDtsExtension(fileName: string) {
function writeFileSync (line 248) | function writeFileSync(filePath: string, content: string) {
function replaceDtsWithJsExtensions (line 263) | function replaceDtsWithJsExtensions(dtsFilePath: string) {
FILE: test/utils.ts
function getTestName (line 13) | function getTestName() {
function run (line 26) | async function run(
function filenamify (line 82) | function filenamify(input: string) {
FILE: vitest-global.ts
function setup (line 5) | async function setup() {
Condensed preview — 75 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (256K chars).
[
{
"path": ".devcontainer/Dockerfile",
"chars": 946,
"preview": "# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.222.0/containers/javascript-no"
},
{
"path": ".devcontainer/devcontainer.json",
"chars": 1077,
"preview": "// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:\n// https://github.co"
},
{
"path": ".editorconfig",
"chars": 131,
"preview": "root = true\n\n[*]\nindent_style = space\nindent_size = 2\ncharset = utf-8\ntrim_trailing_whitespace = false\ninsert_final_newl"
},
{
"path": ".gitattributes",
"chars": 18,
"preview": "* text=auto eol=lf"
},
{
"path": ".github/renovate.json5",
"chars": 294,
"preview": "{\n extends: ['config:recommended', 'schedule:weekly', 'group:allNonMajor'],\n labels: ['dependencies'],\n rangeStrategy"
},
{
"path": ".github/workflows/ci.yml",
"chars": 808,
"preview": "name: CI\n\non:\n push:\n branches:\n - main\n\n pull_request:\n branches:\n - main\n\njobs:\n test:\n strategy"
},
{
"path": ".github/workflows/format.yml",
"chars": 816,
"preview": "name: Fix\n\non:\n push:\n branches-ignore:\n - main\n - dev\n\njobs:\n format:\n runs-on: ubuntu-latest\n\n st"
},
{
"path": ".github/workflows/release-continuous.yml",
"chars": 482,
"preview": "name: Publish Any Commit\non: [push, pull_request]\n\njobs:\n build:\n runs-on: ubuntu-latest\n\n steps:\n - name: C"
},
{
"path": ".github/workflows/release.yml",
"chars": 217,
"preview": "name: Release\n\non:\n push:\n tags:\n - 'v*'\n\njobs:\n release:\n uses: sxzz/workflows/.github/workflows/release.y"
},
{
"path": ".gitignore",
"chars": 71,
"preview": "node_modules\n*.log\ndist\n.cache\nplayground\n.idea\n.DS_Store\n.eslintcache\n"
},
{
"path": ".prettierignore",
"chars": 27,
"preview": "pnpm-lock.yaml\n.cache\ndist\n"
},
{
"path": ".prettierrc",
"chars": 43,
"preview": "{\n \"singleQuote\": true,\n \"semi\": false\n}\n"
},
{
"path": "CONTRIBUTING.md",
"chars": 433,
"preview": "# Contributing\n\n## Making changes\n\n1. Fork the repository.\n2. Make changes.\n3. Add tests in `test/`.\n4. Run tests with `"
},
{
"path": "LICENSE",
"chars": 1063,
"preview": "MIT License\n\nCopyright (c) 2021 EGOIST\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof "
},
{
"path": "README.md",
"chars": 2209,
"preview": "> [!WARNING]\n> This project is not actively maintained anymore. Please consider using [tsdown](https://github.com/rolldo"
},
{
"path": "assets/cjs_shims.js",
"chars": 569,
"preview": "// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as"
},
{
"path": "assets/esm_shims.js",
"chars": 322,
"preview": "// Shim globals in esm bundle\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst getFilename ="
},
{
"path": "assets/package.json",
"chars": 27,
"preview": "{\n \"sideEffects\": false\n}\n"
},
{
"path": "docs/README.md",
"chars": 20097,
"preview": "```js preact\nimport { html } from 'docup'\n\nexport default () => {\n const isPreview = location.hostname !== 'tsup.egoist"
},
{
"path": "docs/index.html",
"chars": 2415,
"preview": "<!doctype html>\n<html>\n <head>\n <meta charset=\"utf-8\" />\n <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />"
},
{
"path": "package.json",
"chars": 2687,
"preview": "{\n \"name\": \"tsup\",\n \"version\": \"8.5.1\",\n \"packageManager\": \"pnpm@10.22.0\",\n \"description\": \"Bundle your TypeScript l"
},
{
"path": "schema.json",
"chars": 10133,
"preview": "{\n \"$schema\": \"http://json-schema.org/draft-07/schema\",\n \"$id\": \"tsup\",\n \"version\": 1.1,\n \"anyOf\": [\n {\n \"ty"
},
{
"path": "src/api-extractor.ts",
"chars": 5461,
"preview": "import path from 'node:path'\nimport { handleError } from './errors'\nimport {\n type ExportDeclaration,\n formatAggregati"
},
{
"path": "src/cli-default.ts",
"chars": 120,
"preview": "#!/usr/bin/env node\nimport { handleError } from './errors'\nimport { main } from './cli-main'\n\nmain().catch(handleError)\n"
},
{
"path": "src/cli-main.ts",
"chars": 5689,
"preview": "import { cac } from 'cac'\nimport { flatten } from 'flat'\nimport { version } from '../package.json'\nimport { slash } from"
},
{
"path": "src/cli-node.ts",
"chars": 154,
"preview": "#!/usr/bin/env node\nimport { handleError } from './errors'\nimport { main } from './cli-main'\n\nmain({\n skipNodeModulesBu"
},
{
"path": "src/errors.ts",
"chars": 1000,
"preview": "import { isMainThread, parentPort } from 'node:worker_threads'\nimport colors from 'picocolors'\n\nexport class PrettyError"
},
{
"path": "src/esbuild/external.ts",
"chars": 1710,
"preview": "import { match, tsconfigPathsToRegExp } from 'bundle-require'\nimport type { Plugin } from 'esbuild'\n\n// Must not start w"
},
{
"path": "src/esbuild/index.ts",
"chars": 8405,
"preview": "import fs from 'node:fs'\nimport path from 'node:path'\nimport {\n type BuildResult,\n type Plugin as EsbuildPlugin,\n bui"
},
{
"path": "src/esbuild/native-node-module.ts",
"chars": 1973,
"preview": "import path from 'node:path'\nimport type { Plugin } from 'esbuild'\n\n// Copied from https://github.com/evanw/esbuild/issu"
},
{
"path": "src/esbuild/node-protocol.ts",
"chars": 508,
"preview": "import type { Plugin } from 'esbuild'\n\n/**\n * The node: protocol was added to require in Node v14.18.0\n * https://nodejs"
},
{
"path": "src/esbuild/postcss.ts",
"chars": 4099,
"preview": "import fs from 'node:fs'\nimport { type Loader, type Plugin, transform } from 'esbuild'\nimport { getPostcss } from '../ut"
},
{
"path": "src/esbuild/svelte.ts",
"chars": 3989,
"preview": "import fs from 'node:fs'\nimport path from 'node:path'\nimport { type Plugin, transform } from 'esbuild'\nimport { localReq"
},
{
"path": "src/esbuild/swc.test.ts",
"chars": 2264,
"preview": "import { describe, expect, test, vi } from 'vitest'\nimport { swcPlugin, type SwcPluginConfig } from './swc'\nimport { loc"
},
{
"path": "src/esbuild/swc.ts",
"chars": 2191,
"preview": "/**\n * Use SWC to emit decorator metadata\n */\nimport path from 'node:path'\nimport { localRequire } from '../utils'\nimpor"
},
{
"path": "src/exports.ts",
"chars": 3213,
"preview": "import path from 'node:path'\nimport { replaceDtsWithJsExtensions, slash, truthy } from './utils'\n\nexport type ExportDecl"
},
{
"path": "src/fs.ts",
"chars": 808,
"preview": "import path from 'node:path'\nimport fs from 'node:fs'\n\nexport const outputFile = async (\n filepath: string,\n data: any"
},
{
"path": "src/index.ts",
"chars": 15165,
"preview": "import path from 'node:path'\nimport fs from 'node:fs'\nimport { Worker } from 'node:worker_threads'\nimport { loadTsConfig"
},
{
"path": "src/lib/public-dir.ts",
"chars": 600,
"preview": "import path from 'node:path'\nimport { copyDirSync } from '../fs'\nimport { slash } from '../utils'\n\nexport const copyPubl"
},
{
"path": "src/lib/report-size.ts",
"chars": 984,
"preview": "import colors from 'picocolors'\nimport type { Logger } from '../log'\n\nconst prettyBytes = (bytes: number) => {\n if (byt"
},
{
"path": "src/load.ts",
"chars": 2693,
"preview": "import fs from 'node:fs'\nimport path from 'node:path'\nimport JoyCon from 'joycon'\nimport { bundleRequire } from 'bundle-"
},
{
"path": "src/log.ts",
"chars": 2304,
"preview": "import util from 'node:util'\nimport { isMainThread, parentPort } from 'node:worker_threads'\nimport colors from 'picocolo"
},
{
"path": "src/options.ts",
"chars": 6881,
"preview": "import type { BuildOptions, Plugin as EsbuildPlugin, Loader } from 'esbuild'\nimport type { InputOption } from 'rollup'\ni"
},
{
"path": "src/plugin.ts",
"chars": 6407,
"preview": "import path from 'node:path'\nimport {\n type RawSourceMap,\n SourceMapConsumer,\n SourceMapGenerator,\n} from 'source-map"
},
{
"path": "src/plugins/cjs-interop.ts",
"chars": 564,
"preview": "import type { Plugin } from '../plugin'\n\nexport const cjsInterop = (): Plugin => {\n return {\n name: 'cjs-interop',\n\n"
},
{
"path": "src/plugins/cjs-splitting.ts",
"chars": 959,
"preview": "// Workaround to enable code splitting for cjs format\n// Manually transform esm to cjs\n// TODO: remove this once esbuild"
},
{
"path": "src/plugins/shebang.ts",
"chars": 321,
"preview": "import type { Plugin } from '../plugin'\n\nexport const shebang = (): Plugin => {\n return {\n name: 'shebang',\n\n ren"
},
{
"path": "src/plugins/size-reporter.ts",
"chars": 434,
"preview": "import { reportSize } from '../lib/report-size'\nimport type { Plugin } from '../plugin'\n\nexport const sizeReporter = ():"
},
{
"path": "src/plugins/swc-target.ts",
"chars": 1799,
"preview": "import { PrettyError } from '../errors'\nimport { localRequire } from '../utils'\nimport type { ModuleConfig } from '@swc/"
},
{
"path": "src/plugins/terser.ts",
"chars": 1822,
"preview": "import { PrettyError } from '../errors'\nimport { localRequire } from '../utils'\nimport type { MinifyOptions } from 'ters"
},
{
"path": "src/plugins/tree-shaking.ts",
"chars": 1597,
"preview": "import path from 'node:path'\nimport { type TreeshakingOptions, type TreeshakingPreset, rollup } from 'rollup'\nimport typ"
},
{
"path": "src/rollup/ts-resolve.ts",
"chars": 2983,
"preview": "import fs from 'node:fs'\nimport path from 'node:path'\nimport { builtinModules } from 'node:module'\nimport _resolve from "
},
{
"path": "src/rollup.ts",
"chars": 7268,
"preview": "import { parentPort } from 'node:worker_threads'\nimport path from 'node:path'\nimport ts from 'typescript'\nimport jsonPlu"
},
{
"path": "src/run.ts",
"chars": 265,
"preview": "import { spawn } from 'node:child_process'\n\nexport function runCode(filename: string, { args }: { args: string[] }) {\n "
},
{
"path": "src/tsc.ts",
"chars": 5645,
"preview": "import { dirname } from 'node:path'\nimport { loadTsConfig } from 'bundle-require'\nimport ts from 'typescript'\nimport { h"
},
{
"path": "src/utils.ts",
"chars": 10776,
"preview": "import fs from 'node:fs'\nimport path from 'node:path'\nimport resolveFrom from 'resolve-from'\nimport type { InputOption }"
},
{
"path": "test/__snapshots__/css.test.ts.snap",
"chars": 242,
"preview": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`import css 1`] = `\n\"\"use strict\";\n\"\n`;\n\nexports["
},
{
"path": "test/__snapshots__/dts.test.ts.snap",
"chars": 10955,
"preview": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`declaration files with multiple entrypoints #316"
},
{
"path": "test/__snapshots__/index.test.ts.snap",
"chars": 7376,
"preview": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`code splitting in cjs format 1`] = `\n\"\"use stric"
},
{
"path": "test/__snapshots__/tsconfig.test.ts.snap",
"chars": 1429,
"preview": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`support baseUrl and paths in tsconfig.json 1`] ="
},
{
"path": "test/css.test.ts",
"chars": 1385,
"preview": "import { expect, test } from 'vitest'\nimport { getTestName, run } from './utils'\n\ntest('import css', async () => {\n con"
},
{
"path": "test/dts.test.ts",
"chars": 12888,
"preview": "import path from 'node:path'\nimport { expect, test } from 'vitest'\nimport { slash } from '../src/utils'\nimport { getTest"
},
{
"path": "test/example.test.ts",
"chars": 717,
"preview": "import { test } from 'vitest'\nimport { getTestName, run } from './utils'\n\ntest('bundle vue and ts-essentials with --dts "
},
{
"path": "test/experimental-dts.test.ts",
"chars": 15330,
"preview": "import { test } from 'vitest'\nimport type { Options } from '../src/index.js'\nimport { getTestName, run } from './utils.j"
},
{
"path": "test/graphql.test.ts",
"chars": 1259,
"preview": "import { expect, test } from 'vitest'\nimport { getTestName, run } from './utils'\n\ntest('bundle graphql-tools with --dts "
},
{
"path": "test/index.test.ts",
"chars": 22930,
"preview": "import path from 'node:path'\nimport fs from 'node:fs'\nimport { expect, test } from 'vitest'\nimport waitForExpect from 'w"
},
{
"path": "test/package.json",
"chars": 406,
"preview": "{\n \"private\": true,\n \"devDependencies\": {\n \"@egoist/path-parser\": \"1.0.6\",\n \"@types/react\": \"18.3.6\",\n \"@type"
},
{
"path": "test/shims.test.ts",
"chars": 3235,
"preview": "import { test } from 'vitest'\nimport type { Options } from '../src/index.js'\nimport { getTestName, run } from './utils.j"
},
{
"path": "test/svelte.test.ts",
"chars": 2184,
"preview": "import { expect, test } from 'vitest'\nimport { getTestName, run } from './utils'\n\ntest('bundle svelte', async () => {\n "
},
{
"path": "test/tsconfig.test.ts",
"chars": 1735,
"preview": "import { expect, test } from 'vitest'\nimport { getTestName, run } from './utils'\n\ntest('custom tsconfig', async () => {\n"
},
{
"path": "test/utils.ts",
"chars": 2184,
"preview": "import fs from 'node:fs'\nimport fsp from 'node:fs/promises'\nimport path from 'node:path'\nimport { fileURLToPath } from '"
},
{
"path": "tsconfig.json",
"chars": 464,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"esnext\",\n \"lib\": [\"es2022\"],\n \"moduleDetection\": \"force\",\n \"module\": \"e"
},
{
"path": "tsup.config.ts",
"chars": 333,
"preview": "import { defineConfig } from 'tsup'\n\nexport default defineConfig({\n name: 'tsup',\n target: 'node18',\n dts: {\n reso"
},
{
"path": "vitest-global.ts",
"chars": 459,
"preview": "import path from 'node:path'\nimport fs from 'node:fs/promises'\nimport { exec } from 'tinyexec'\n\nexport default async fun"
},
{
"path": "vitest.config.mts",
"chars": 207,
"preview": "import { defineConfig } from 'vitest/config'\n\nexport default defineConfig({\n test: {\n testTimeout: 50000,\n global"
}
]
About this extraction
This page contains the full source code of the egoist/tsup GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 75 files (236.0 KB), approximately 62.4k tokens, and a symbol index with 122 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.