iT邦幫忙

2025 iThome 鐵人賽

DAY 6
0
Software Development

30 天打造工作室 SaaS 產品 (後端篇)系列 第 6

Day 6: Monorepo 套件打包策略實戰 - Rollup vs. tsup vs. tsc

  • 分享至 

  • xImage
  •  

套件打包的關鍵抉擇

昨天我們完成了 Prisma 整合和資料庫設計,今天要解決一個看似簡單但影響深遠的問題:如何打包我們的共享套件?這不只是技術選擇,更關係到:

  • 開發體驗:建構速度和除錯便利性
  • 相容性:不同環境(Node.js、瀏覽器)的支援度(最近被Node和Flutter app環境搞到
  • 效能:最終打包的大小和載入速度
  • 維護性:配置複雜度和長期維護成本

在工作經驗中,因為套件打包策略選擇不當,可能導致專案後期維護困難。

現有套件架構分析

讓我們先分析 Kyo-System 的套件結構:

packages/
├── @kyong/kyo-core/      # 後端核心邏輯 (Node.js)
├── @kyong/kyo-types/     # 共享型別定義 (Universal)
├── @kyong/kyo-ui/        # UI 元件庫 (Browser)
└── @kyong/kyo-config/    # 配置工具 (Universal)

不同套件的需求差異

  • kyo-core: 純 Node.js 環境,需要高效能打包
  • kyo-types: 型別定義,需要在前後端都能使用
  • kyo-ui: React 元件,需要 tree-shaking 和 ESM 支援
  • kyo-config: 配置工具,需要跨環境相容性

打包工具比較

1. TypeScript Compiler (tsc)

適用場景:簡單的純 TypeScript 專案

// packages/@kyong/kyo-types/tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "node",
    "declaration": true,
    "outDir": "dist",
    "rootDir": "src",
    "strict": true
  },
  "include": ["src/**/*"],
  "exclude": ["dist", "node_modules", "**/*.test.ts"]
}
// package.json
{
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch"
  }
}

優點

  • ✅ 配置簡單,官方工具
  • ✅ 完美的型別生成
  • ✅ 穩定性最高

缺點

  • ❌ 無法合併檔案,產生多個 .js 檔案
  • ❌ 沒有 tree-shaking 優化
  • ❌ 無法處理非 TS 檔案(CSS、圖片等)

2. Rollup

適用場景:需要優化和 tree-shaking 的函式庫

// packages/@kyong/kyo-core/rollup.config.mjs
import typescript from '@rollup/plugin-typescript';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import { readFileSync } from 'fs';

const pkg = JSON.parse(readFileSync('./package.json', 'utf8'));

export default {
  input: 'src/index.ts',
  external: [
    ...Object.keys(pkg.dependencies || {}),
    ...Object.keys(pkg.peerDependencies || {}),
    'fastify',
    'redis',
    '@prisma/client'
  ],
  output: [
    {
      file: pkg.main,
      format: 'cjs',
      sourcemap: true
    },
    {
      file: pkg.module,
      format: 'esm',
      sourcemap: true
    }
  ],
  plugins: [
    resolve({
      preferBuiltins: true
    }),
    commonjs(),
    typescript({
      tsconfig: './tsconfig.json',
      declaration: true,
      outDir: 'dist'
    })
  ]
};
// package.json
{
  "main": "dist/index.cjs.js",
  "module": "dist/index.esm.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "rollup -c",
    "dev": "rollup -c --watch"
  }
}

優點

  • ✅ 優秀的 tree-shaking
  • ✅ 可產生多種格式(ESM, CJS, UMD)
  • ✅ 豐富的插件生態系統
  • ✅ 產生單一檔案,便於分發

缺點

  • ❌ 配置複雜
  • ❌ 建構速度較慢
  • ❌ 需要手動處理外部依賴

3. tsup

適用場景:快速開發,需要多格式輸出

// packages/@kyong/kyo-ui/tsup.config.ts
import { defineConfig } from 'tsup';

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['cjs', 'esm'],
  dts: true,
  splitting: false,
  sourcemap: true,
  clean: true,
  external: ['react', 'react-dom'],
  esbuildOptions(options) {
    options.banner = {
      js: '"use client"'  // Next.js client components
    };
  }
});
// package.json
{
  "main": "dist/index.js",
  "module": "dist/index.mjs",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "tsup",
    "dev": "tsup --watch"
  }
}

優點

  • ✅ 零配置開箱即用
  • ✅ 基於 esbuild,建構極快
  • ✅ 內建多格式支援
  • ✅ 內建 code splitting

缺點

  • ❌ 客製化選項較少
  • ❌ esbuild 某些邊緣案例處理不如 Rollup
  • ❌ 對複雜專案的控制力不足

實際套件配置實作

kyo-types (純型別定義)

// packages/@kyong/kyo-types/src/index.ts
export * from './otp';
export * from './templates';
export * from './errors';
export * from './api';

// packages/@kyong/kyo-types/src/otp.ts
import { z } from 'zod';

export const SendOtpRequest = z.object({
  phone: z.string().regex(/^09\d{8}$/, '請輸入有效的台灣手機號碼'),
  templateId: z.number().optional()
});

export const SendOtpResponse = z.object({
  msgId: z.string(),
  status: z.string(),
  success: z.boolean()
});

export type SendOtpRequest = z.infer<typeof SendOtpRequest>;
export type SendOtpResponse = z.infer<typeof SendOtpResponse>;
// packages/@kyong/kyo-types/package.json
{
  "name": "@kyong/kyo-types",
  "version": "1.0.0",
  "type": "module",
  "main": "dist/index.js",
  "module": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "zod": "^3.22.0"
  }
}

選擇 tsc:型別定義套件最簡單有效的方案。

kyo-core (Node.js 核心邏輯)

// packages/@kyong/kyo-core/rollup.config.mjs
import typescript from '@rollup/plugin-typescript';
import resolve from '@rollup/plugin-node-resolve';
import json from '@rollup/plugin-json';

export default {
  input: 'src/index.ts',
  external: [
    'fastify',
    'redis',
    '@prisma/client',
    'ioredis',
    'zod',
    '@kyong/kyo-types'
  ],
  output: [
    {
      file: 'dist/index.cjs.js',
      format: 'cjs',
      sourcemap: true
    },
    {
      file: 'dist/index.esm.js',
      format: 'esm',
      sourcemap: true
    }
  ],
  plugins: [
    resolve({
      preferBuiltins: true
    }),
    json(),
    typescript({
      tsconfig: './tsconfig.json',
      declaration: true,
      outDir: 'dist'
    })
  ]
};

選擇 Rollup:需要精確控制外部依賴和多格式輸出。

kyo-ui (React 元件庫)

// packages/@kyong/kyo-ui/tsup.config.ts
import { defineConfig } from 'tsup';

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['esm'],
  dts: true,
  splitting: true,
  sourcemap: true,
  clean: true,
  external: [
    'react',
    'react-dom',
    '@mantine/core',
    '@mantine/hooks'
  ],
  esbuildOptions(options) {
    options.banner = {
      js: '"use client"'
    };
  }
});

選擇 tsup:React 元件需要快速建構和 code splitting。

kyo-config (通用配置工具)

// packages/@kyong/kyo-config/tsup.config.ts
import { defineConfig } from 'tsup';

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['cjs', 'esm'],
  dts: true,
  splitting: false,
  sourcemap: true,
  clean: true,
  platform: 'neutral'
});

選擇 tsup:需要跨平台相容性和多格式輸出。

建構工作流程整合

根目錄 Turbo 配置

// turbo.json
{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "dev": {
      "dependsOn": ["^build"],
      "cache": false,
      "persistent": true
    },
    "typecheck": {
      "dependsOn": ["^build"]
    }
  }
}

統一的建構腳本

// package.json (根目錄)
{
  "scripts": {
    "build": "turbo run build",
    "build:packages": "turbo run build --filter='@kyong/*'",
    "dev": "turbo run dev --parallel",
    "typecheck": "turbo run typecheck"
  }
}

開發體驗優化

即時型別更新

// packages/@kyong/kyo-types/src/otp.ts
export interface OtpValidationResult {
  isValid: boolean;
  attemptsLeft: number;
  nextRetryAt?: Date;
}

// 當 kyo-types 變更時,使用它的套件會立即收到型別更新

Source Map 偵錯

// 所有配置都啟用 sourcemap: true
// 確保在開發時能追踪到原始碼位置

監聽模式開發

# 啟動所有套件的監聽模式
pnpm run dev

# 或者只監聽特定套件
pnpm --filter @kyong/kyo-core run dev

效能與檔案大小分析

Bundle 分析

// package.json 添加分析腳本
{
  "scripts": {
    "analyze": "rollup-plugin-analyzer --limit 10"
  }
}

Tree-shaking 驗證

// 確保 package.json 正確配置 sideEffects
{
  "sideEffects": false,  // 或者 ["*.css", "*.scss"]
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.esm.js",
      "require": "./dist/index.cjs.js"
    }
  }
}

CI/CD 整合

GitHub Actions 工作流程

# .github/workflows/packages.yml
name: Packages CI

on:
  push:
    paths: ['packages/**']

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: pnpm/action-setup@v2
      - uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: 'pnpm'

      - run: pnpm install
      - run: pnpm run build:packages
      - run: pnpm run typecheck

      # 快取建構產物
      - uses: actions/cache@v3
        with:
          path: packages/*/dist
          key: packages-${{ github.sha }}

套件發布策略

版本管理

// 使用 Changeset 管理版本
{
  "devDependencies": {
    "@changesets/cli": "^2.26.0"
  },
  "scripts": {
    "changeset": "changeset",
    "version": "changeset version",
    "release": "pnpm run build:packages && changeset publish"
  }
}

私有 NPM Registry

# .npmrc (如果使用私有 registry)
@kyong:registry=https://npm.your-company.com/
//npm.your-company.com/:_authToken=${NPM_TOKEN}

最佳實踐總結

基於實作經驗,我歸納出這些原則:

工具選擇決策樹

純型別定義套件 → tsc
├─ 簡單、穩定
└─ 不需要額外優化

Node.js 後端套件 → Rollup
├─ 需要精確控制依賴
├─ 多格式輸出需求
└─ 追求最小包大小

前端 UI 元件庫 → tsup
├─ 快速建構需求
├─ 支援 React 特殊需求
└─ 內建 code splitting

通用工具套件 → tsup
├─ 跨平台相容性
├─ 多格式輸出
└─ 快速開發體驗

配置統一性

  1. 所有套件都啟用 sourcemap
  2. 統一的 TypeScript 配置繼承
  3. 一致的 lint 和格式化規則
  4. 標準化的 package.json exports 欄位

今日成果

  • 分析不同套件的打包需求 - 針對性選擇工具
  • 實作 Rollup、tsup、tsc 配置 - 涵蓋各種使用情境
  • 整合 Turbo 建構工作流程 - 高效的 monorepo 建構
  • 優化開發體驗 - 監聽模式和 source map
  • 建立發布流程 - CI/CD 與版本管理

明天我們將建立完整的測試策略,使用 Node.js 內建測試執行器搭配 Vitest,確保所有套件的品質和穩定性。

套件打包不只是技術選擇,更是對開發體驗和維護性的長期投資! 🚀


上一篇
Day5:Prisma ORM 與資料持久化層設計
下一篇
Day 7: 測試策略與品質保證架構
系列文
30 天打造工作室 SaaS 產品 (後端篇)7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言