昨天我們完成了 Prisma 整合和資料庫設計,今天要解決一個看似簡單但影響深遠的問題:如何打包我們的共享套件?這不只是技術選擇,更關係到:
在工作經驗中,因為套件打包策略選擇不當,可能導致專案後期維護困難。
讓我們先分析 Kyo-System 的套件結構:
packages/
├── @kyong/kyo-core/ # 後端核心邏輯 (Node.js)
├── @kyong/kyo-types/ # 共享型別定義 (Universal)
├── @kyong/kyo-ui/ # UI 元件庫 (Browser)
└── @kyong/kyo-config/ # 配置工具 (Universal)
不同套件的需求差異:
適用場景:簡單的純 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"
}
}
優點:
缺點:
適用場景:需要優化和 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"
}
}
優點:
缺點:
適用場景:快速開發,需要多格式輸出
// 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"
}
}
優點:
缺點:
// 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:型別定義套件最簡單有效的方案。
// 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:需要精確控制外部依賴和多格式輸出。
// 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。
// 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.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 變更時,使用它的套件會立即收到型別更新
// 所有配置都啟用 sourcemap: true
// 確保在開發時能追踪到原始碼位置
# 啟動所有套件的監聽模式
pnpm run dev
# 或者只監聽特定套件
pnpm --filter @kyong/kyo-core run dev
// package.json 添加分析腳本
{
"scripts": {
"analyze": "rollup-plugin-analyzer --limit 10"
}
}
// 確保 package.json 正確配置 sideEffects
{
"sideEffects": false, // 或者 ["*.css", "*.scss"]
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.esm.js",
"require": "./dist/index.cjs.js"
}
}
}
# .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"
}
}
# .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
├─ 跨平台相容性
├─ 多格式輸出
└─ 快速開發體驗
明天我們將建立完整的測試策略,使用 Node.js 內建測試執行器搭配 Vitest,確保所有套件的品質和穩定性。
套件打包不只是技術選擇,更是對開發體驗和維護性的長期投資! 🚀