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 # [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 [![npm version](https://badgen.net/npm/v/tsup)](https://npm.im/tsup) [![npm downloads](https://badgen.net/npm/dm/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

Ship UIs faster with automated workflows for Storybook

sponsors

## Project Stats ![Alt](https://repobeats.axiom.co/api/embed/4ef361ec8445b33c2dab451e1d23784015834c72.svg 'Repobeats analytics image') ## 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`
This is a preview version of the docs.
` } ``` 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 ` 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 ` 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 ================================================ tsup ================================================ 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 ` 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.* ', 'Use a key-value pair as entry files') .option('-d, --out-dir ', 'Output directory', { default: 'dist' }) .option('--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 ', '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 ', 'Ignore custom paths in watch mode') .option( '--onSuccess ', 'Execute command after successful build, specially useful for watch mode', ) .option('--env.* ', 'Define compile-time env variables') .option( '--inject ', 'Replace a global variable with an import from another file', ) .option('--define.* ', 'Define compile-time constants') .option( '--external ', 'Mark specific packages / package.json (dependencies and peerDependencies) as external', ) .option('--global-name ', 'Global variable name for iife format') .option('--jsxFactory ', 'Name of JSX factory function', { default: 'React.createElement', }) .option('--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 ', 'Mark specific expressions as pure') .option('--metafile', 'Emit esbuild metafile (a JSON file)') .option('--platform ', 'Target platform', { default: 'node', }) .option('--loader ', 'Specify the loader for a file extension') .option('--tsconfig ', 'Use a custom tsconfig') .option('--config ', '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 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 = 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 }): 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 buildDependencies: Set 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 = [ 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 inject?: boolean | ((css: string, fileId: string) => string | Promise) 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 }): 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 = {}) => { 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(), module: new Set(), } 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 /** * 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((resolve, reject) => { kill(pid, signal, (err) => { if (err && !isTaskkillCmdProcessNotFoundError(err)) return reject(err) resolve() }) }) const normalizeOptions = async ( logger: ReturnType, optionsFromConfigFile: Options | undefined, optionsOverride: Options, ) => { const _options = { ...optionsFromConfigFile, ...optionsOverride, } const options: Partial = { 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 " 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((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 = 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 = 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 }> { 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 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 /** * 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 | Promise)>) 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 /** * 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) /** * 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, 'dts' | 'experimentalDts' | 'format' > & { dts?: DtsConfig experimentalDts?: NormalizedExperimentalDtsConfig tsconfigResolvePaths: Record 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 export type BuildEnd = ( this: PluginContext, ctx: { writtenFiles: WrittenFile[] }, ) => MaybePromise 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 = 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 => 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 ignore?: (source: string, importer?: string) => boolean } export const tsResolvePlugin: PluginImpl = ({ 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 => { 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() 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, ): 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() 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 | Promise 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 { 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( fn: (...args: T) => Promise, delay: number, onError: (err: unknown) => void, ) { let timeout: ReturnType | undefined let promiseInFly: Promise | undefined let callbackPending: (() => void) | undefined return function debounced(...args: Parameters) { 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 extends false | '' | 0 | null | undefined ? never : T // from lodash export function truthy(value: T): value is Truthy { 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, ) } 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 => { 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 => { 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 = Exclude & Required> 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(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(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 = Exclude & Required> `, '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(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': `export const foo = [1, 2, 3]\nexport type { Person } from './types.js'`, 'tsup.config.ts': `export default ${JSON.stringify( { name: task.name, entry: { index: 'src/index.ts' }, format: ['esm', 'cjs'], experimentalDts: true, } satisfies Options, null, 2, )}`, 'package.json': JSON.stringify( { name: 'testing-experimental-dts', description: task.name, type: 'module', }, null, 2, ), 'tsconfig.json': JSON.stringify( { compilerOptions: { module: moduleKind, moduleResolution, outDir: './dist', rootDir: './src', skipLibCheck: true, strict: true, }, include: ['src'], }, null, 2, ), }, { entry: [], }, ) expect(outFiles).toStrictEqual([ '_tsup-dts-rollup.d.cts', '_tsup-dts-rollup.d.ts', 'index.cjs', 'index.d.cts', 'index.d.ts', 'index.js', ]) const indexDtsContent = [ `export { foo } from './_tsup-dts-rollup.js';`, `export { Person } from './_tsup-dts-rollup.js';\n`, ].join('\n') expect(await getFileContent('dist/index.d.ts')).toStrictEqual( indexDtsContent, ) expect(await getFileContent('dist/index.d.cts')).toStrictEqual( indexDtsContent.replaceAll( `'./_tsup-dts-rollup.js'`, `'./_tsup-dts-rollup.cjs'`, ), ) }, ) test('experimentalDts works when `entry` is set to an array', async ({ expect, task, }) => { const { getFileContent, outFiles } = await run( getTestName(), { 'src/types.ts': `export type Person = { name: string }`, 'src/index.ts': `export const foo = [1, 2, 3]\nexport type { Person } from './types.js'`, 'tsup.config.ts': `export default ${JSON.stringify( { name: task.name, entry: ['src/index.ts'], format: ['esm', 'cjs'], experimentalDts: true, } satisfies Options, null, 2, )}`, 'package.json': JSON.stringify( { name: 'testing-experimental-dts-entry-array', description: task.name, type: 'module', }, null, 2, ), 'tsconfig.json': JSON.stringify( { compilerOptions: { outDir: './dist', rootDir: './src', skipLibCheck: true, strict: true, }, include: ['src'], }, null, 2, ), }, { entry: [], }, ) expect(outFiles).toStrictEqual([ '_tsup-dts-rollup.d.cts', '_tsup-dts-rollup.d.ts', 'index.cjs', 'index.d.cts', 'index.d.ts', 'index.js', ]) const indexDtsContent = [ `export { foo } from './_tsup-dts-rollup.js';`, `export { Person } from './_tsup-dts-rollup.js';\n`, ].join('\n') expect(await getFileContent('dist/index.d.ts')).toStrictEqual(indexDtsContent) expect(await getFileContent('dist/index.d.cts')).toStrictEqual( indexDtsContent.replaceAll( `'./_tsup-dts-rollup.js'`, `'./_tsup-dts-rollup.cjs'`, ), ) }) test('experimentalDts works when `entry` is set to an array of globs', async ({ expect, task, }) => { const { getFileContent, outFiles } = await run( getTestName(), { 'src/types.ts': `export type Person = { name: string }`, 'src/index.ts': `export const foo = [1, 2, 3]\nexport type { Person } from './types.js'`, 'tsup.config.ts': `export default ${JSON.stringify( { name: task.name, entry: ['src/**/*.ts'], format: ['esm', 'cjs'], experimentalDts: true, } satisfies Options, null, 2, )}`, 'package.json': JSON.stringify( { name: 'entry-array-of-globs', description: task.name, type: 'module', }, null, 2, ), 'tsconfig.json': JSON.stringify( { compilerOptions: { outDir: './dist', rootDir: './src', skipLibCheck: true, strict: true, }, include: ['src'], }, null, 2, ), }, { entry: [], }, ) expect(outFiles).toStrictEqual([ '_tsup-dts-rollup.d.cts', '_tsup-dts-rollup.d.ts', 'index.cjs', 'index.d.cts', 'index.d.ts', 'index.js', 'types.cjs', 'types.d.cts', 'types.d.ts', 'types.js', ]) const indexDtsContent = [ `export { foo } from './_tsup-dts-rollup.js';`, `export { Person } from './_tsup-dts-rollup.js';\n`, ].join('\n') const typesDtsContent = `export { Person_alias_1 as Person } from './_tsup-dts-rollup.js';\n` expect(await getFileContent('dist/index.d.ts')).toStrictEqual(indexDtsContent) expect(await getFileContent('dist/index.d.cts')).toStrictEqual( indexDtsContent.replaceAll( `'./_tsup-dts-rollup.js'`, `'./_tsup-dts-rollup.cjs'`, ), ) expect(await getFileContent('dist/types.d.ts')).toStrictEqual(typesDtsContent) expect(await getFileContent('dist/types.d.cts')).toStrictEqual( typesDtsContent.replaceAll( `'./_tsup-dts-rollup.js'`, `'./_tsup-dts-rollup.cjs'`, ), ) }) test('experimentalDts.entry can work independent from `options.entry`', async ({ expect, task, }) => { const { getFileContent, outFiles } = await run( getTestName(), { 'src/types.ts': `export type Person = { name: string }`, 'src/index.ts': `export const foo = [1, 2, 3]\nexport type { Person } from './types.js'`, 'tsup.config.ts': `export default ${JSON.stringify( { name: task.name, entry: ['src/**/*.ts'], format: ['esm', 'cjs'], experimentalDts: { entry: { index: 'src/index.ts' } }, } satisfies Options, null, 2, )}`, 'package.json': JSON.stringify( { name: 'testing-experimental-dts-entry-can-work-independent', description: task.name, type: 'module', }, null, 2, ), 'tsconfig.json': JSON.stringify( { compilerOptions: { outDir: './dist', rootDir: './src', skipLibCheck: true, strict: true, }, include: ['src'], }, null, 2, ), }, { entry: [], }, ) expect(outFiles).toStrictEqual([ '_tsup-dts-rollup.d.cts', '_tsup-dts-rollup.d.ts', 'index.cjs', 'index.d.cts', 'index.d.ts', 'index.js', 'types.cjs', 'types.js', ]) const indexDtsContent = [ `export { foo } from './_tsup-dts-rollup.js';`, `export { Person } from './_tsup-dts-rollup.js';\n`, ].join('\n') expect(await getFileContent('dist/index.d.ts')).toStrictEqual(indexDtsContent) expect(await getFileContent('dist/index.d.cts')).toStrictEqual( indexDtsContent.replaceAll( `'./_tsup-dts-rollup.js'`, `'./_tsup-dts-rollup.cjs'`, ), ) }) test('experimentalDts.entry can be an array of globs', async ({ expect, task, }) => { const { getFileContent, outFiles } = await run( getTestName(), { 'src/types.ts': `export type Person = { name: string }`, 'src/index.ts': `export const foo = [1, 2, 3]\nexport type { Person } from './types.js'`, 'tsup.config.ts': `export default ${JSON.stringify( { name: task.name, entry: { index: 'src/index.ts' }, format: ['esm', 'cjs'], experimentalDts: { entry: ['src/**/*.ts'] }, } satisfies Options, null, 2, )}`, 'package.json': JSON.stringify( { name: 'testing-experimental-dts-entry-array-of-globs', description: task.name, type: 'module', }, null, 2, ), 'tsconfig.json': JSON.stringify( { compilerOptions: { outDir: './dist', rootDir: './src', skipLibCheck: true, strict: true, }, include: ['src'], }, null, 2, ), }, { entry: [], }, ) expect(outFiles).toStrictEqual([ '_tsup-dts-rollup.d.cts', '_tsup-dts-rollup.d.ts', 'index.cjs', 'index.d.cts', 'index.d.ts', 'index.js', 'types.d.cts', 'types.d.ts', ]) const indexDtsContent = [ `export { foo } from './_tsup-dts-rollup.js';`, `export { Person } from './_tsup-dts-rollup.js';\n`, ].join('\n') expect(await getFileContent('dist/index.d.ts')).toStrictEqual(indexDtsContent) expect(await getFileContent('dist/index.d.cts')).toStrictEqual( indexDtsContent.replaceAll( `'./_tsup-dts-rollup.js'`, `'./_tsup-dts-rollup.cjs'`, ), ) }) test('experimentalDts can be a string', async ({ expect, task }) => { const { getFileContent, outFiles } = await run( getTestName(), { 'src/types.ts': `export type Person = { name: string }`, 'src/index.ts': `export const foo = [1, 2, 3]\nexport type { Person } from './types.js'`, 'tsup.config.ts': `export default ${JSON.stringify( { name: task.name, entry: ['src/**/*.ts'], format: ['esm', 'cjs'], experimentalDts: 'src/index.ts', } satisfies Options, null, 2, )}`, 'package.json': JSON.stringify( { name: 'testing-experimental-dts-can-be-a-string', description: task.name, type: 'module', }, null, 2, ), 'tsconfig.json': JSON.stringify( { compilerOptions: { outDir: './dist', rootDir: './src', skipLibCheck: true, strict: true, }, include: ['src'], }, null, 2, ), }, { entry: [], }, ) expect(outFiles).toStrictEqual([ '_tsup-dts-rollup.d.cts', '_tsup-dts-rollup.d.ts', 'index.cjs', 'index.d.cts', 'index.d.ts', 'index.js', 'types.cjs', 'types.js', ]) const indexDtsContent = [ `export { foo } from './_tsup-dts-rollup.js';`, `export { Person } from './_tsup-dts-rollup.js';\n`, ].join('\n') expect(await getFileContent('dist/index.d.ts')).toStrictEqual(indexDtsContent) expect(await getFileContent('dist/index.d.cts')).toStrictEqual( indexDtsContent.replaceAll( `'./_tsup-dts-rollup.js'`, `'./_tsup-dts-rollup.cjs'`, ), ) }) test('experimentalDts can be a string of glob pattern', async ({ expect, task, }) => { const { getFileContent, outFiles } = await run( getTestName(), { 'src/types.ts': `export type Person = { name: string }`, 'src/index.ts': `export const foo = [1, 2, 3]\nexport type { Person } from './types.js'`, 'tsup.config.ts': `export default ${JSON.stringify( { name: task.name, entry: { index: 'src/index.ts' }, format: ['esm', 'cjs'], experimentalDts: 'src/**/*.ts', } satisfies Options, null, 2, )}`, 'package.json': JSON.stringify( { name: 'testing-experimental-dts-can-be-a-string-of-glob-pattern', description: task.name, type: 'module', }, null, 2, ), 'tsconfig.json': JSON.stringify( { compilerOptions: { outDir: './dist', rootDir: './src', skipLibCheck: true, strict: true, }, include: ['src'], }, null, 2, ), }, { entry: [], }, ) expect(outFiles).toStrictEqual([ '_tsup-dts-rollup.d.cts', '_tsup-dts-rollup.d.ts', 'index.cjs', 'index.d.cts', 'index.d.ts', 'index.js', 'types.d.cts', 'types.d.ts', ]) const indexDtsContent = [ `export { foo } from './_tsup-dts-rollup.js';`, `export { Person } from './_tsup-dts-rollup.js';\n`, ].join('\n') expect(await getFileContent('dist/index.d.ts')).toStrictEqual(indexDtsContent) expect(await getFileContent('dist/index.d.cts')).toStrictEqual( indexDtsContent.replaceAll( `'./_tsup-dts-rollup.js'`, `'./_tsup-dts-rollup.cjs'`, ), ) }) test('experimentalDts.entry can be a string of glob pattern', async ({ expect, task, }) => { const { getFileContent, outFiles } = await run( getTestName(), { 'src/types.ts': `export type Person = { name: string }`, 'src/index.ts': `export const foo = [1, 2, 3]\nexport type { Person } from './types.js'`, 'tsup.config.ts': `export default ${JSON.stringify( { name: task.name, entry: { index: 'src/index.ts' }, format: ['esm', 'cjs'], experimentalDts: { entry: 'src/**/*.ts' }, } satisfies Options, null, 2, )}`, 'package.json': JSON.stringify( { name: 'testing-experimental-dts-entry-can-be-a-string-of-glob-pattern', description: task.name, type: 'module', }, null, 2, ), 'tsconfig.json': JSON.stringify( { compilerOptions: { outDir: './dist', rootDir: './src', skipLibCheck: true, strict: true, }, include: ['src'], }, null, 2, ), }, { entry: [], }, ) expect(outFiles).toStrictEqual([ '_tsup-dts-rollup.d.cts', '_tsup-dts-rollup.d.ts', 'index.cjs', 'index.d.cts', 'index.d.ts', 'index.js', 'types.d.cts', 'types.d.ts', ]) const indexDtsContent = [ `export { foo } from './_tsup-dts-rollup.js';`, `export { Person } from './_tsup-dts-rollup.js';\n`, ].join('\n') expect(await getFileContent('dist/index.d.ts')).toStrictEqual(indexDtsContent) expect(await getFileContent('dist/index.d.cts')).toStrictEqual( indexDtsContent.replaceAll( `'./_tsup-dts-rollup.js'`, `'./_tsup-dts-rollup.cjs'`, ), ) }) ================================================ FILE: test/graphql.test.ts ================================================ import { expect, test } from 'vitest' import { getTestName, run } from './utils' test('bundle graphql-tools with --dts flag', async () => { await run( getTestName(), { 'input.ts': `export { makeExecutableSchema } from 'graphql-tools'`, }, { flags: ['--dts'], }, ) }) test('bundle graphql-tools with --dts-resolve flag', async () => { await run( getTestName(), { 'input.ts': `export { makeExecutableSchema } from 'graphql-tools'`, }, { flags: ['--dts-resolve'], }, ) }) test('bundle graphql-tools with --sourcemap flag', async () => { const { outFiles } = await run( getTestName(), { 'input.ts': `export { makeExecutableSchema } from 'graphql-tools'`, }, { flags: ['--sourcemap'], }, ) expect(outFiles).toEqual(['input.js', 'input.js.map']) }) test('bundle graphql-tools with --sourcemap inline flag', async () => { const { output, outFiles } = await run( getTestName(), { 'input.ts': `export { makeExecutableSchema } from 'graphql-tools'`, }, { flags: ['--sourcemap', 'inline'], }, ) expect(output).toContain('//# sourceMappingURL=data:application/json;base64') expect(outFiles).toEqual(['input.js']) }) ================================================ FILE: test/index.test.ts ================================================ import path from 'node:path' import fs from 'node:fs' import { expect, test } from 'vitest' import waitForExpect from 'wait-for-expect' import { debouncePromise } from '../src/utils' import { getTestName, run } from './utils' test('simple', async () => { const { output, outFiles } = await run(getTestName(), { 'input.ts': `import foo from './foo';export default foo`, 'foo.ts': `export default 'foo'`, }) expect(output).toMatchSnapshot() expect(outFiles).toEqual(['input.js']) }) test('should not filter unknown directives during bundle', async () => { const { output, outFiles } = await run(getTestName(), { 'input.ts': `'use client'\nexport default 'foo'`, }) expect(output).toContain('use client') expect(outFiles).toEqual(['input.js']) }) test('multiple formats', async () => { const { outFiles } = await run( getTestName(), { 'input.ts': ` export const a = 1 `, }, { flags: ['--format', 'esm,cjs,iife'], }, ) expect(outFiles).toEqual(['input.global.js', 'input.js', 'input.mjs']) }) test('multiple formats and pkg.type is module', async () => { const { outFiles } = await run( getTestName(), { 'input.ts': ` export const a = 1 `, 'package.json': JSON.stringify({ type: 'module' }), }, { flags: ['--format', 'esm,cjs,iife'], }, ) expect(outFiles).toEqual(['input.cjs', 'input.global.js', 'input.js']) }) test('minify', async () => { const { output, outFiles } = await run( getTestName(), { 'input.ts': ` export function foo() { return 'foo' } `, }, { flags: ['--minify'], }, ) expect(output).toContain(`return"foo"`) expect(outFiles).toEqual(['input.js']) }) test('minify with es5 target', async () => { const { output, outFiles } = await run( getTestName(), { 'input.ts': ` export function foo() { return 'foo' } `, }, { flags: ['--minify', '--target', 'es5'], }, ) expect(output).toContain(`return"foo"`) expect(outFiles).toEqual(['input.js']) }) test('env flag', async () => { const { output, outFiles } = await run( getTestName(), { 'input.ts': ` export const env = process.env.NODE_ENV `, }, { flags: ['--env.NODE_ENV', 'production'], }, ) expect(output).toContain('var env = "production"') expect(outFiles).toEqual(['input.js']) }) test('node protocol', async () => { const { output } = await run(getTestName(), { 'input.ts': `import fs from 'node:fs'; console.log(fs)`, }) expect(output).toMatchSnapshot() expect(output).not.contain('node:fs') }) test("don't remove node protocol", async () => { const { output } = await run(getTestName(), { 'input.ts': `import fs from 'node:fs'; console.log(fs)`, 'tsup.config.ts': ` export default { removeNodeProtocol: false, }`, }) expect(output).toMatchSnapshot() expect(output).contain('node:fs') }) test('external', async () => { const { output } = await run(getTestName(), { 'input.ts': `export {foo} from 'foo' export {bar} from 'bar' export {baz} from 'baz' export {qux} from 'qux' `, 'node_modules/foo/index.ts': `export const foo = 'foo'`, 'node_modules/foo/package.json': `{"name":"foo","version":"0.0.0"}`, 'node_modules/bar/index.ts': `export const bar = 'bar'`, 'node_modules/bar/package.json': `{"name":"bar","version":"0.0.0"}`, 'node_modules/baz/index.ts': `export const baz = 'baz'`, 'node_modules/baz/package.json': `{"name":"baz","version":"0.0.0"}`, 'node_modules/qux/index.ts': `export const qux = 'qux'`, 'node_modules/qux/package.json': `{"name":"qux","version":"0.0.0"}`, 'another/package.json': `{"name":"another-pkg","dependencies":{"qux":"0.0.0"}}`, 'tsup.config.ts': ` export default { external: [/f/, 'bar', 'another/package.json'] } `, }) expect(output).toMatchSnapshot() }) test('noExternal are respected when skipNodeModulesBundle is true', async () => { const { output } = await run(getTestName(), { 'input.ts': `export {foo} from 'foo' export {bar} from 'bar' export {baz} from 'baz' `, 'node_modules/foo/index.ts': `export const foo = 'foo'`, 'node_modules/foo/package.json': `{"name":"foo","version":"0.0.0"}`, 'node_modules/bar/index.ts': `export const bar = 'bar'`, 'node_modules/bar/package.json': `{"name":"bar","version":"0.0.0"}`, 'node_modules/baz/index.ts': `export const baz = 'baz'`, 'node_modules/baz/package.json': `{"name":"baz","version":"0.0.0"}`, 'tsup.config.ts': ` export default { skipNodeModulesBundle: true, noExternal: [/foo/] } `, }) expect(output).toContain(`var foo = "foo"`) expect(output).not.toContain(`var bar = "bar"`) expect(output).not.toContain(`var baz = "baz"`) }) test('disable code splitting to get proper module.exports =', async () => { const { output } = await run( getTestName(), { 'input.ts': `export = 123`, }, { flags: ['--no-splitting'], }, ) expect(output).toMatchSnapshot() }) test('onSuccess', async () => { const { logs } = await run( getTestName(), { 'input.ts': "console.log('test');", }, { flags: ['--onSuccess', 'echo hello && echo world'], }, ) expect(logs.includes('hello')).toEqual(true) expect(logs.includes('world')).toEqual(true) }) test('onSuccess: use a function from config file', async () => { const { logs } = await run(getTestName(), { 'input.ts': "console.log('test');", 'tsup.config.ts': ` export default { onSuccess: async () => { console.log('hello') await new Promise((resolve) => { setTimeout(() => { console.log('world') resolve('') }, 1_000) }) } }`, }) expect(logs.includes('hello')).toEqual(true) expect(logs.includes('world')).toEqual(true) }) test(`transform import.meta.url in cjs format`, async () => { const { getFileContent } = await run( getTestName(), { 'input.ts': `export default import.meta.url`, }, { flags: ['--shims'], }, ) expect(await getFileContent('dist/input.js')).toContain('getImportMetaUrl') }) test(`transform __dirname and __filename in esm format`, async () => { const { getFileContent } = await run( getTestName(), { 'input.ts': `export const a = __dirname export const b = __filename `, }, { flags: ['--format', 'esm', '--shims'], }, ) const code = await getFileContent('dist/input.mjs') expect(code).toContain('getFilename') expect(code).toContain('getDirname') }) test('debounce promise', async () => { try { const equal = (a: T, b: T) => { const result = a === b if (!result) throw new Error(`${a} !== ${b}`) } const sleep = (n: number = Math.trunc(Math.random() * 50) + 20) => new Promise((resolve) => setTimeout(resolve, n)) let n = 0 const debounceFunction = debouncePromise( async () => { await sleep() ++n }, 100, (err: any) => { expect.fail(err.message) }, ) expect(n).toEqual(0) debounceFunction() debounceFunction() debounceFunction() debounceFunction() await waitForExpect(() => { equal(n, 1) }) await sleep(100) expect(n).toEqual(1) debounceFunction() await waitForExpect(() => { equal(n, 2) }) } catch (error: any) { return expect.fail(error.message) } }) test('exclude dependencies', async () => { const { getFileContent } = await run(getTestName(), { 'input.ts': `export {foo} from 'foo';export {nested} from 'foo/nested'`, 'package.json': `{"dependencies":{"foo":"0.0.0"}}`, 'node_modules/foo/index.js': `export const foo = 'foo'`, 'node_modules/foo/package.json': `{"name":"foo"}`, }) const contents = await getFileContent('dist/input.js') expect(contents).toContain('require("foo")') expect(contents).toContain('require("foo/nested")') }) test('code splitting in cjs format', async () => { const { getFileContent } = await run( getTestName(), { 'input.ts': `const foo = () => import('./foo');export {foo}`, 'another-input.ts': `const foo = () => import('./foo');export {foo}`, 'foo.ts': `export const foo = 'bar'`, }, { flags: ['another-input.ts', '--splitting'] }, ) expect(await getFileContent('dist/input.js')).toMatchSnapshot() expect(await getFileContent('dist/another-input.js')).toMatchSnapshot() }) test('esbuild metafile', async () => { const { outFiles } = await run( getTestName(), { 'input.ts': `export const foo = 1` }, { flags: ['--metafile'], }, ) expect(outFiles).toEqual(['input.js', 'metafile-cjs.json']) }) test('multiple entry with the same base name', async () => { const { outFiles } = await run( getTestName(), { 'src/input.ts': `export const foo = 1`, 'src/bar/input.ts': `export const bar = 2`, }, { entry: ['src/input.ts', 'src/bar/input.ts'], }, ) expect(outFiles).toEqual(['bar/input.js', 'input.js']) }) test('windows: backslash in entry', async () => { const { outFiles } = await run( getTestName(), { 'src/input.ts': `export const foo = 1` }, { entry: [String.raw`src\input.ts`], }, ) expect(outFiles).toEqual(['input.js']) }) test('emit declaration files only', async () => { const { outFiles } = await run( getTestName(), { 'input.ts': `export const foo = 1`, }, { flags: ['--dts-only'], }, ) expect(outFiles).toEqual(['input.d.ts']) }) test('decorator metadata', async () => { const { getFileContent } = await run(getTestName(), { 'input.ts': ` function Injectable() {} @Injectable() export class Foo { @Field() bar() {} } `, 'tsconfig.json': `{ "compilerOptions": { "emitDecoratorMetadata": true, } }`, }) const contents = await getFileContent('dist/input.js') expect(contents).toContain(`_ts_metadata("design:type", Function)`) }) test('inject style', async () => { const { outFiles, output } = await run( getTestName(), { 'input.ts': `import './style.css'`, 'style.css': `.hello { color: red }`, }, { flags: ['--inject-style', '--minify'], }, ) expect(outFiles).toEqual(['input.js']) expect(output).toContain('.hello{color:red}') }) test('inject style in multi formats', async () => { const { outFiles, getFileContent } = await run( getTestName(), { 'input.ts': `export * from './App.svelte'`, 'App.svelte': ` {msg} `, }, { flags: ['--inject-style', '--minify', '--format', 'esm,cjs,iife'], }, ) expect(outFiles).toEqual(['input.global.js', 'input.js', 'input.mjs']) for (const file of outFiles) { expect(await getFileContent(`dist/${file}`)).toContain('{color:red}') } }) test('shebang', async () => { const { outDir } = await run( getTestName(), { 'a.ts': `#!/usr/bin/env node\bconsole.log('a')`, 'b.ts': `console.log('b')`, }, { entry: ['a.ts', 'b.ts'], }, ) if (process.platform === 'win32') { return } expect(() => { fs.accessSync(path.join(outDir, 'a.js'), fs.constants.X_OK) }).not.toThrow() expect(() => { fs.accessSync(path.join(outDir, 'b.js'), fs.constants.X_OK) }).toThrow() }) test('es5 target', async () => { const { output, outFiles } = await run( getTestName(), { 'input.ts': ` export class Foo { hi (): void { let a = () => 'foo' console.log(a()) } } `, }, { flags: ['--target', 'es5'], }, ) expect(output).toMatch(/_create_class/) expect(outFiles).toEqual(['input.js']) }) test('es5 minify', async () => { const { getFileContent, outFiles } = await run( getTestName(), { 'input.ts': ` export class Foo { hi (): void { let a = () => 'foo' console.log(a()) } } `, }, { flags: [ '--target', 'es5', '--format', 'iife', '--globalName', 'FooAPI', '--minify', ], }, ) expect(outFiles).toEqual(['input.global.js']) const iifeBundle = await getFileContent('dist/input.global.js') expect(iifeBundle).toMatch(/var FooAPI/) expect(iifeBundle).not.toMatch(/createClass/) }) test('multiple targets', async () => { const { output, outFiles } = await run( getTestName(), { 'input.ts': ` export const answer = 42 `, }, { entry: ['input.ts'], flags: ['--target', 'es2020,chrome58,firefox57,safari11,edge16'], }, ) expect(output).toMatchSnapshot() expect(outFiles).toEqual(['input.js']) }) test('native-node-module plugin should handle *.node(.js) import properly', async () => { await run( getTestName(), { 'input.tsx': `export * from './hi.node'`, 'hi.node.js': `export const hi = 'hi'`, }, { entry: ['input.tsx'], }, ) }) test('proper sourcemap sources path when swc is enabled', async () => { const { getFileContent } = await run( getTestName(), { 'input.ts': `export const hi = 'hi'`, 'tsconfig.json': JSON.stringify({ compilerOptions: { emitDecoratorMetadata: true, }, }), }, { entry: ['input.ts'], flags: ['--sourcemap'], }, ) const map = await getFileContent('dist/input.js.map') expect(map).toContain(`["../input.ts"]`) }) // Fixing https://github.com/evanw/esbuild/issues/1794 test('use rollup for treeshaking', async () => { const { getFileContent } = await run( getTestName(), { 'input.ts': ` export { useRoute } from 'vue-router' `, }, { entry: ['input.ts'], flags: ['--treeshake', '--external', 'vue', '--format', 'esm'], }, ) expect(await getFileContent('dist/input.mjs')).toContain( `function useRoute(_name) { return inject(routeLocationKey); }`, ) }) test('use rollup for treeshaking --format cjs', async () => { const { getFileContent } = await run( getTestName(), { 'package.json': `{ "dependencies": { "react-select": "5.7.0", "react": "17.0.2", "react-dom": "17.0.2" } }`, 'input.tsx': ` import ReactSelect from 'react-select' export const Component = (props: {}) => { return }; `, 'tsconfig.json': `{ "compilerOptions": { "baseUrl": ".", "esModuleInterop": true, "isolatedModules": true, "jsx": "react-jsx", "lib": ["dom", "dom.iterable", "esnext"], "module": "esnext", "moduleResolution": "node", "noEmit": true, "rootDir": ".", "skipLibCheck": true, "sourceMap": true, "strict": true, "target": "es6", "importHelpers": true, "outDir": "dist" } }`, }, { entry: ['input.tsx'], flags: ['--treeshake', '--target', 'es2022', '--format', 'cjs'], }, ) expect(await getFileContent('dist/input.js')).toContain( `jsxRuntime.jsx(ReactSelect__default.default`, ) }) test('custom output extension', async () => { const { outFiles } = await run( getTestName(), { 'input.ts': `export const foo = [1,2,3]`, 'tsup.config.ts': `export default { outExtension({ format }) { return { js: '.' + format + '.js' } } }`, }, { entry: ['input.ts'], flags: ['--format', 'esm,cjs'], }, ) expect(outFiles).toMatchInlineSnapshot(` [ "input.cjs.js", "input.esm.js", ] `) }) test('custom config file', async () => { const { outFiles } = await run( getTestName(), { 'input.ts': `export const foo = [1,2,3]`, 'custom.config.ts': `export default { format: ['esm'] }`, }, { entry: ['input.ts'], flags: ['--config', 'custom.config.ts'], }, ) expect(outFiles).toMatchInlineSnapshot(` [ "input.mjs", ] `) }) test('use an object as entry from cli flag', async () => { const { outFiles } = await run( getTestName(), { 'input.ts': `export const foo = [1,2,3]`, }, { flags: ['--entry.foo', 'input.ts'], }, ) expect(outFiles).toMatchInlineSnapshot(` [ "foo.js", ] `) }) test('remove unused code', async () => { const { getFileContent } = await run( getTestName(), { 'input.ts': `if (import.meta.foo) { console.log(1) } else { console.log(2) }`, 'tsup.config.ts': `export default { define: { 'import.meta.foo': 'false' }, treeshake: true }`, }, {}, ) expect(await getFileContent('dist/input.js')).not.toContain('console.log(1)') }) test('treeshake should work with hashbang', async () => { const { getFileContent } = await run( getTestName(), { 'input.ts': '#!/usr/bin/node\nconsole.log(123)', }, { flags: ['--treeshake'], }, ) expect(await getFileContent('dist/input.js')).toMatchInlineSnapshot(` "#!/usr/bin/node 'use strict'; // input.ts console.log(123); " `) }) test('support target in tsconfig.json', async () => { const { getFileContent } = await run( getTestName(), { 'input.ts': `await import('./foo')`, 'foo.ts': `export default 'foo'`, 'tsconfig.json': `{ "compilerOptions": { "baseUrl":".", "target": "esnext" } }`, }, { flags: ['--format', 'esm'], }, ) expect(await getFileContent('dist/input.mjs')).contains('await import(') }) test('override target in tsconfig.json', async () => { await expect( run( getTestName(), { 'input.ts': `await import('./foo')`, 'foo.ts': `export default 'foo'`, 'tsconfig.json': `{ "compilerOptions": { "baseUrl":".", "target": "esnext" } }`, }, { flags: ['--format', 'esm', '--target', 'es2018'], }, ), ).rejects.toThrowError( `Top-level await is not available in the configured target environment ("es2018")`, ) }) test(`should generate export {} when there are no exports in source file`, async () => { const { outFiles, getFileContent } = await run(getTestName(), { 'input.ts': `const a = 'a'`, 'tsconfig.json': `{ "compilerOptions": { "baseUrl":".", "target": "esnext", } }`, 'tsup.config.ts': ` export default { entry: ['src/input.ts'], format: 'esm', dts: true } `, }) expect(outFiles).toEqual(['input.d.mts', 'input.mjs']) expect(await getFileContent('dist/input.d.mts')).toMatch(/export {\s*}/) }) test('custom inject style function - sync', async () => { const { outFiles, getFileContent } = await run(getTestName(), { 'input.ts': `import './style.css'`, 'style.css': `.hello { color: red }`, 'tsup.config.ts': ` export default { entry: ['src/input.ts'], minify: true, format: ['esm', 'cjs'], injectStyle: (css) => { return "__custom_inject_style__(" + css +")"; } }`, }) expect(outFiles).toEqual(['input.js', 'input.mjs']) expect(await getFileContent('dist/input.mjs')).toContain( '__custom_inject_style__(`.hello{color:red}\n`)', ) expect(await getFileContent('dist/input.js')).toContain( '__custom_inject_style__(`.hello{color:red}\n`)', ) }) test('custom inject style function - async', async () => { const { outFiles, getFileContent } = await run(getTestName(), { 'input.ts': `import './style.css'`, 'style.css': `.hello { color: red }`, 'tsup.config.ts': ` export default { entry: ['src/input.ts'], minify: true, format: ['esm', 'cjs'], injectStyle: async (css) => { await new Promise(resolve => setTimeout(resolve, 100)); return "__custom_async_inject_style__(" + css +")"; } }`, }) expect(outFiles).toEqual(['input.js', 'input.mjs']) expect(await getFileContent('dist/input.mjs')).toContain( '__custom_async_inject_style__(`.hello{color:red}\n`)', ) expect(await getFileContent('dist/input.js')).toContain( '__custom_async_inject_style__(`.hello{color:red}\n`)', ) }) test('preserve top-level variable for IIFE format', async () => { const { outFiles, getFileContent } = await run(getTestName(), { 'input.ts': `export default 'foo'`, 'tsup.config.ts': ` export default { entry: ['src/input.ts'], globalName: 'globalFoo', minify: 'terser', format: ['iife'] }`, }) expect(outFiles).toEqual(['input.global.js']) expect(await getFileContent('dist/input.global.js')).toMatch(/globalFoo\s*=/) }) test('should load postcss esm config', async () => { const { outFiles, getFileContent } = await run(getTestName(), { 'input.ts': ` import './foo.css' `, 'package.json': `{ "type": "module" }`, 'postcss.config.js': ` export default { plugins: {'postcss-simple-vars': {}} } `, 'foo.css': ` $color: blue; .foo { color: $color; } `, }) expect(outFiles).toEqual(['input.cjs', 'input.css']) expect(await getFileContent('dist/input.css')).toContain('color: blue;') }) test('generate sourcemap with --treeshake', async () => { const sourceCode = 'export function getValue(val: any){ return val; }' const { outFiles, getFileContent } = await run( getTestName(), { 'src/input.ts': sourceCode, }, { entry: ['src/input.ts'], flags: ['--treeshake', '--sourcemap', '--format=cjs,esm,iife'], }, ) expect(outFiles.length).toBe(6) await Promise.all( outFiles .filter((fileName) => fileName.endsWith('.map')) .map(async (sourceMapFile) => { const sourceMap = await getFileContent(`dist/${sourceMapFile}`).then( (rawContent) => JSON.parse(rawContent), ) expect(sourceMap.sources[0]).toBe('../src/input.ts') expect(sourceMap.sourcesContent[0]).toBe(sourceCode) const outputFileName = sourceMapFile.replace('.map', '') expect(sourceMap.file).toBe(outputFileName) }), ) }) ================================================ FILE: test/package.json ================================================ { "private": true, "devDependencies": { "@egoist/path-parser": "1.0.6", "@types/react": "18.3.6", "@types/react-dom": "18.3.0", "autoprefixer": "10.4.20", "graphql": "^16.9.0", "graphql-tools": "^9.0.1", "react": "18.3.1", "react-dom": "18.3.1", "react-select": "5.8.0", "tailwindcss": "3.4.11", "vue": "3.5.6", "vue-router": "4.4.5" }, "tsup": {} } ================================================ FILE: test/shims.test.ts ================================================ import { test } from 'vitest' import type { Options } from '../src/index.js' import { getTestName, run } from './utils.js' test('removeNodeProtocol works on shims', async ({ expect, task }) => { const { getFileContent, outFiles } = await run( getTestName(), { 'src/index.ts': 'export const foo = __dirname', 'tsup.config.ts': `export default ${JSON.stringify( { name: task.name, entry: { index: 'src/index.ts' }, format: ['esm'], shims: true, removeNodeProtocol: true, } satisfies Options, null, 2, )}`, 'package.json': JSON.stringify( { name: 'remove-node-protocol-works-on-shims', description: task.name, type: 'commonjs', sideEffects: false, }, null, 2, ), 'tsconfig.json': JSON.stringify( { compilerOptions: { outDir: './dist', rootDir: './src', skipLibCheck: true, strict: true, }, include: ['src'], }, null, 2, ), }, { entry: [], }, ) expect(outFiles).toStrictEqual(['index.mjs']) const indexMjsContent = `// ../../../assets/esm_shims.js import path from "path"; import { fileURLToPath } from "url"; var getFilename = () => fileURLToPath(import.meta.url); var getDirname = () => path.dirname(getFilename()); var __dirname = /* @__PURE__ */ getDirname(); // src/index.ts var foo = __dirname; export { foo }; ` expect(await getFileContent('dist/index.mjs')).toStrictEqual(indexMjsContent) }) test('disabling removeNodeProtocol retains node protocol in shims', async ({ expect, task, }) => { const { getFileContent, outFiles } = await run( getTestName(), { 'src/index.ts': `export const foo = __dirname`, 'tsup.config.ts': `export default ${JSON.stringify( { name: task.name, entry: { index: 'src/index.ts' }, format: ['esm'], shims: true, removeNodeProtocol: false, } satisfies Options, null, 2, )}`, 'package.json': JSON.stringify( { name: 'disabling-remove-node-protocol-retains-node-protocol-in-shims', description: task.name, type: 'commonjs', sideEffects: false, }, null, 2, ), 'tsconfig.json': JSON.stringify( { compilerOptions: { outDir: './dist', rootDir: './src', skipLibCheck: true, strict: true, }, include: ['src'], }, null, 2, ), }, { entry: [], }, ) expect(outFiles).toStrictEqual(['index.mjs']) const indexMjsContent = `// ../../../assets/esm_shims.js import path from "node:path"; import { fileURLToPath } from "node:url"; var getFilename = () => fileURLToPath(import.meta.url); var getDirname = () => path.dirname(getFilename()); var __dirname = /* @__PURE__ */ getDirname(); // src/index.ts var foo = __dirname; export { foo }; ` expect(await getFileContent('dist/index.mjs')).toStrictEqual(indexMjsContent) }) ================================================ FILE: test/svelte.test.ts ================================================ import { expect, test } from 'vitest' import { getTestName, run } from './utils' test('bundle svelte', async () => { const { output, getFileContent } = await run( getTestName(), { 'input.ts': `import App from './App.svelte' export { App } `, 'App.svelte': ` {msg} `, }, { // To make the snapshot leaner flags: ['--external', 'svelte/internal'], }, ) expect(output).not.toContain(' {msg} `, }) expect(outFiles).toEqual(['input.js']) }) test('svelte: typescript support', async () => { const { outFiles, output } = await run(getTestName(), { 'input.ts': `import App from './App.svelte' export { App } `, 'App.svelte': ` {say} `, 'Component.svelte': ` {name} `, }) expect(outFiles).toEqual(['input.js']) expect(output).toContain('// Component.svelte') }) test('svelte: sass support', async () => { const { outFiles, getFileContent } = await run(getTestName(), { 'input.ts': `import App from './App.svelte' export { App } `, 'App.svelte': `
Hello
`, }) expect(outFiles).toEqual(['input.css', 'input.js']) const outputCss = await getFileContent('dist/input.css') expect(outputCss).toMatch(/\.svelte-\w+:hover/) }) ================================================ FILE: test/tsconfig.test.ts ================================================ import { expect, test } from 'vitest' import { getTestName, run } from './utils' test('custom tsconfig', async () => { await run( getTestName(), { 'input.ts': `export const foo = 'foo'`, 'tsconfig.build.json': `{ "compilerOptions": { "baseUrl":"." } }`, }, { flags: ['--tsconfig', 'tsconfig.build.json'] }, ) }) test('support baseUrl and paths in tsconfig.json', async () => { const { getFileContent } = await run(getTestName(), { 'input.ts': `export * from '@/foo'`, 'foo.ts': `export const foo = 'foo'`, 'tsconfig.json': `{ "compilerOptions": { "baseUrl":".", "paths":{"@/*": ["./*"]} } }`, }) expect(await getFileContent('dist/input.js')).toMatchSnapshot() }) test('support baseUrl and paths in tsconfig.json in --dts build', async () => { const { getFileContent } = await run( getTestName(), { 'input.ts': `export * from '@/foo'`, 'src/foo.ts': `export const foo = 'foo'`, 'tsconfig.json': `{ "compilerOptions": { "baseUrl":".", "paths":{"@/*": ["./src/*"]} } }`, }, { flags: ['--dts'] }, ) expect(await getFileContent('dist/input.d.ts')).toMatchSnapshot() }) test('support baseUrl and paths in tsconfig.json in --dts-resolve build', async () => { const { getFileContent } = await run( getTestName(), { 'input.ts': `export * from '@/foo'`, 'src/foo.ts': `export const foo = 'foo'`, 'tsconfig.json': `{ "compilerOptions": { "baseUrl":".", "paths":{"@/*": ["./src/*"]} } }`, }, { flags: ['--dts-resolve'] }, ) expect(await getFileContent('dist/input.d.ts')).toMatchSnapshot() }) ================================================ FILE: test/utils.ts ================================================ import fs from 'node:fs' import fsp from 'node:fs/promises' import path from 'node:path' import { fileURLToPath } from 'node:url' import { expect } from 'vitest' import { exec } from 'tinyexec' import { glob } from 'tinyglobby' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const cacheDir = path.resolve(__dirname, '.cache') const bin = path.resolve(__dirname, '../dist/cli-default.js') export function getTestName() { const name = expect .getState() .currentTestName?.replace(/^[a-z]+/g, '_') .replace(/-/g, '_') if (!name) { throw new Error('No test name') } return name } export async function run( title: string, files: { [name: string]: string }, options: { entry?: string[] flags?: string[] env?: Record } = {}, ) { const testDir = path.resolve(cacheDir, filenamify(title)) // Write entry files on disk await Promise.all( Object.keys(files).map(async (name) => { const filePath = path.resolve(testDir, name) const parentDir = path.dirname(filePath) // Thanks to `recursive: true`, this doesn't fail even if the directory already exists. await fsp.mkdir(parentDir, { recursive: true }) return fsp.writeFile(filePath, files[name], 'utf8') }), ) const entry = options.entry || ['input.ts'] // Run tsup cli const processPromise = exec(bin, [...entry, ...(options.flags || [])], { nodeOptions: { cwd: testDir, env: { ...process.env, ...options.env }, }, }) const { stdout, stderr } = await processPromise const logs = stdout + stderr if (processPromise.exitCode !== 0) { throw new Error(logs) } // Get output const outFiles = await glob(['**/*'], { cwd: path.resolve(testDir, 'dist'), }).then((res) => res.sort()) return { get output() { return fs.readFileSync(path.resolve(testDir, 'dist/input.js'), 'utf8') }, outFiles, logs, outDir: path.resolve(testDir, 'dist'), getFileContent(filename: string) { return fsp.readFile(path.resolve(testDir, filename), 'utf8') }, } } function filenamify(input: string) { return input.replace(/[^a-zA-Z0-9]/g, '-') } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "esnext", "lib": ["es2022"], "moduleDetection": "force", "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "types": ["node"], "strict": true, "noUnusedLocals": true, "declaration": true, "declarationDir": "dist", "outDir": "./dist", "esModuleInterop": true, "isolatedModules": true, "verbatimModuleSyntax": true, "skipLibCheck": true } } ================================================ FILE: tsup.config.ts ================================================ import { defineConfig } from 'tsup' export default defineConfig({ name: 'tsup', target: 'node18', dts: { resolve: true, // build types for `src/index.ts` only // otherwise `Options` will not be exported by `tsup`, not sure how this happens, probably a bug in rollup-plugin-dts entry: './src/index.ts', }, }) ================================================ FILE: vitest-global.ts ================================================ import path from 'node:path' import fs from 'node:fs/promises' import { exec } from 'tinyexec' export default async function setup() { const testDir = path.resolve(__dirname, 'test') const cacheDir = path.resolve(testDir, '.cache') await fs.rm(cacheDir, { recursive: true, force: true }) console.log(`Installing dependencies in ./test folder`) await exec('pnpm', ['i'], { nodeOptions: { cwd: testDir } }) console.log(`Done... start testing..`) } ================================================ FILE: vitest.config.mts ================================================ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { testTimeout: 50000, globalSetup: 'vitest-global.ts', include: ["test/*.test.ts", "src/**/*.test.ts"] }, })