Skip to content

为什么不使用 rollup? ​

最开始的时候 rollup 就是用来打包 js libary 的, 随着社区的发展才逐渐支持 typescript 打包, 虽然支持但是打包配置比较复杂, 打包速度也并不理想(如果不使用 swc 的话), 而且不支持打包 .d.ts 文件

tsup 介绍 ​

tsup 对标 rollup 是一个基于 esbuild 实现的 TypeScript 的打包工具

快速开始 ​

初始化项目 ​

sh
mkdir -p ./tsup-demo/src && cd tsup-demo
pnpm init
pinpm i typescript tsup -D
tsc --init

目录结构 ​

现在假设已经写好了一个 typescript 项目, 并且开始打包

txt
.
├── dist
│   ├── index.d.mts
│   ├── index.d.ts
│   ├── index.js
│   ├── index.js.map
│   ├── index.mjs
│   └── index.mjs.map
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── src
│   ├── config
│   │   ├── default.ts
│   │   ├── development.ts
│   │   ├── production.ts
│   │   └── test.ts
│   ├── config.schema.ts
│   ├── config.ts
│   └── index.ts
├── tsconfig.json
└── tsup.config.ts

4 directories, 18 files

配置文件 ​

ts
import { defineConfig } from "tsup";

// 全部选项: https://www.jsdocs.io/package/tsup
// prettier-ignore
export default defineConfig({
  entry: ["src/index.ts"], // 打包的入口文件, 和 rollup 类似
  splitting: false,        // 是否开启代码分割
  sourcemap: true,         // 是否生成 sourcemap 文件
  clean: true,             // 打包之前清理上一次打包的结果
  dts: true,               // 打包时生成类型声明文件 .d.ts
  format: ["cjs", "esm"],  // 编译后的 js 使用的模块规范(多个就输出多份结果)
  minify: false,           // 是否压缩打包后的代码
  treeshake: true,         // 打包时清理没有用到的代码
});
json
{
  "name": "tsup-demo",
  "version": "0.0.1",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "tsup"
  },
  "packageManager": "pnpm@10.13.1",
  "devDependencies": {
    "tsup": "^8.5.0",
    "typescript": "^5.8.3"
  },
  "dependencies": {
    "@types/lodash-es": "^4.17.12",
    "lodash-es": "^4.17.21",
    "zod": "^4.0.5"
  }
}
jsonc
{
  "compilerOptions": {
    "forceConsistentCasingInFileNames": true,
    "target": "es2016",
    "module": "es6",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "noImplicitAny": true,
    "noEmit": true,
    "noUnusedLocals": true,
    "types": [],
    "baseUrl": ".",
    "paths": {
      "@/": ["./src/*"],
    },
  },
  "exclude": ["**/dist/**", "**/.nuxt/**", "**/nuxt.d.ts", "**/examples/**", "**/docs/**", "**/playground/**"],
}

源码实现 ​

ts
import { ConfigManager } from "./config";

export type { ConfigType, PartialConfigType } from "./config.schema";
export type { NodeEnv } from "./config";
export const config = new ConfigManager();
ts
import { configSchema, ConfigType, PartialConfigType } from "./config.schema";
import { merge, has, get } from "lodash-es";

// config files
import defaultConfig from "./config/default";
import testConfig from "./config/test";
import productionConfig from "./config/production";
import developmentConfig from "./config/development";

export type NodeEnv = "development" | "test" | "production";

export class ConfigManager {
  private config: ConfigType;

  constructor() {
    const baseConfig: ConfigType = defaultConfig;
    this.config = baseConfig;
  }

  init(nodeEnv: NodeEnv = "development") {
    const configMaps: Record<NodeEnv, PartialConfigType> = {
      development: developmentConfig,
      test: testConfig,
      production: productionConfig,
    };
    const currentConfig: PartialConfigType = configMaps[nodeEnv];
    this.config = merge(this.config, currentConfig); // merge defaultConfig and currentConfig
    this.config = configSchema.parse(this.config);
    console.log(`[config]initialized`);
    console.log(this.config);
  }

  get(keyPath: string) {
    if (keyPath === "*") {
      return this.config;
    }
    return get(this.config, keyPath);
  }

  has(keyPath: string): boolean {
    return has(this.config, keyPath);
  }
}
ts
import { z } from "zod"; // zod v4

export const configSchema = z.object({
  isDev: z.boolean(),
  timezone: z.string().default("Asia/Beijing").optional(),
  database: z.looseObject({
    dialect: z.string(),
    host: z.string(),
    port: z.number(),
    username: z.string(),
    password: z.string(),
    database: z.string(),
    // autoLoadModels: z.boolean().default(true).optional(),
  }),
});
export type ConfigType = z.infer<typeof configSchema>;
export type PartialConfigType = Partial<ConfigType>;
ts
import type { ConfigType } from "../config.schema";

const config: ConfigType = {
  isDev: true, // 是否开启调试模式
  timezone: "Asia/Shanghai",
  database: {
    dialect: "postgres",
    host: "127.0.0.1",
    port: 5432,
    username: "postgres",
    password: "postgres123456",
    database: "", // can be override it by other configure
    autoLoadModels: true,
  },
};

export default config;
ts
import { PartialConfigType } from "../config.schema";
export default {
  isDev: false,
  database: {
    database: "production",
  },
} as PartialConfigType;
ts
import { PartialConfigType } from "../config.schema";
export default {
  isDev: true,
  database: {
    database: "dev",
  },
} as PartialConfigType;
ts
import { PartialConfigType } from "../config.schema";
export default {
  isDev: true,
  database: {
    database: "test",
  },
} as PartialConfigType;

虽然 tsup 使用起来已经足够简单了, 但是开源社区的工具总是层出不穷, 一言不合就搞个新的, 最近又有个 tsdown 出来了, 而且与 rolldown 结合的非常好, 且有中文文档, 上手非常简单, 等rolldown发布 1.0 release 版本, 应该就可以直接无缝切换到 tsdown 了

tsdown 介绍 ​

tsdown 是对标 tsup 基于 rolldown 开发的 TypeScript 的打包工具

前置准备 ​

假设已经学习过 tsup, 现在要从 tsup 直接迁移到 tsdown

sh
# 复制 tsup-demo 项目的代码, 不用重新写了, 主要是学习打包
cp -r ./tsup-demo ./tsdown-demo

# 卸载 tsup
pnpm remove tsup

# 安装 tsdown
pnpm install tsdown -D

从 tsup 迁移 ​

sh
npx tsdown migrate

# 这个操作会自动读取 tsup.config.ts 配置文件
# 并且据此生成 tsdown.config.ts 配置文件
# 然后删除原来的 tsup.config.ts 的配置文件

修改 package.json ​

json
{
  "name": "tsdown-demo",
  "version": "0.0.1",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "tsdown"
  },
  "packageManager": "pnpm@10.13.1",
  "devDependencies": {
    "tsdown": "^0.12.9",
    "typescript": "^5.8.3"
  },
  "dependencies": {
    "@types/lodash-es": "^4.17.12",
    "lodash-es": "^4.17.21",
    "zod": "^4.0.5"
  }
}

再次执行打包 ​

发现没有任何问题, 使用 js 导入打包后的文件等都没有什么问题, 可能是我的代码比较简单

sh
pnpm run build

注意 ​

虽然 tsup/tsdown 都可以正常打包, 但是现阶段(2025-07-15) rolldown 毕竟还是 beta 版本, 所以我的建议是, 生产环境还是用 tsup, 个人项目/学习 可以使用 tsdown 尝鲜 后续等 tsdown 进入 release 状态, 就可以无缝直接切换过去

Released under the MIT License.