nextforge:用插件與生命週期設計一個 Next.js 專案產生器
2026年5月4日

為什麼要做一個 CLI
每次開新的 Next.js 專案,前半小時幾乎都在做同樣的事:跑 create-next-app、裝狀態管理、設定登入、引入 UI 元件庫……步驟不難,但每次都要重來一遍。
nextforge 是想把這段流程自動化的實驗:輸入 nextforge my-app,回答幾個問題,就得到一個依照選擇配置好的專案。
? 請選擇您的專案模板: Next.js / Admin Dashboard / Landing Page
? 要啟用哪些核心功能? Auth (NextAuth / JWT)、Redux Toolkit
? 請選擇 UI 解決方案: shadcn/ui / Tailwind CSS Only / None
? 使用的套件管理工具: pnpm / npm / yarn
這篇不是使用說明,而是記錄它的架構怎麼一路演變,以及目前停在哪裡。
演進:從一行指令到插件引擎
整個專案在兩天內密集迭代,每個版本都是一個獨立的 PR:
| 版本 | 做了什麼 |
|---|---|
| v0.3 | 包一層 create-next-app,固定帶入 TypeScript、ESLint 等參數 |
| v0.5 | 安全層:寫入檔案前的檢查(safeInject)、發版失敗時自動回滾、dry-run |
| v0.6 | 插件系統:註冊表、自動載入、生命週期執行器 |
| v0.7 | 互動式問答(prompts + chalk) |
| v0.7.1 | 引擎重構:把功能全部拆成插件 |
| v0.8 | 讓插件真正可用:Auth 改寫、安全寫檔接回、錯誤分級、安裝指令統一 |
v0.5 的安全層
v0.5 時開始往專案裡寫入範本檔案,於是先做了一個 safeInject:寫檔前確認目標路徑沒有跑出專案資料夾、檔案已存在時不覆蓋(除非加 --force)、範本不存在就略過。
另一個是發版指令:升版號、commit、打 tag 這三步中任何一步失敗,都會依照已完成的步驟反向復原,把 repo 恢復到發版前的狀態。
v0.6 之後:為什麼改成插件
v0.5 的寫法是在一個 create 函式裡用 if 判斷要不要加 Auth、要不要加 Redux。功能一多,這個函式就會越來越長,每加一個選項都要回頭改核心程式。
插件化之後,核心只負責「依序呼叫」,每個功能各自是一個檔案。
架構拆解
1. 插件:一個物件,掛在生命週期上
每個插件就是一個有名稱、優先順序和 hooks 的物件:
export default {
name: 'plugin-redux',
priority: 50,
hooks: {
async afterCreate(ctx) {
if (!ctx.config.features?.redux) return;
// 依使用者選的 pnpm / npm / yarn 安裝
await ctx.utils.install(['@reduxjs/toolkit', 'react-redux']);
ctx.utils.safeInject({ targetPath: 'src/lib/redux/store.ts', content: STORE });
},
},
};插件自己判斷要不要執行。使用者沒勾 Redux,它就直接 return。
2. 自動載入:丟進資料夾就生效
啟動時掃描 plugins/ 資料夾,所有 plugin-*.js 都會自動註冊,不需要在任何地方手動 import:
for (const file of fs.readdirSync(dir)) {
if (!file.startsWith('plugin-') || !file.endsWith('.js')) continue;
const mod = await import(`file://${path.join(dir, file)}`);
if (mod.default) PluginRegistry.register(mod.default);
}註冊時依 priority 排序,數字越大越先執行。建立專案本體的 Next.js 插件是 100,一定最先跑;Redux 和 Auth 是 50,shadcn 是 40。
3. 生命週期:先依階段,再依插件
建立專案被切成七個階段:
init → resolve → validate → beforeCreate → create → afterCreate → destroy
執行器的邏輯很單純:外層跑階段,內層跑插件,有掛這個階段的插件才執行:
for (const phase of phases) {
for (const plugin of plugins) {
const handler = plugin.hooks?.[phase];
if (typeof handler !== 'function') continue;
await handler(context);
}
}「先階段、後插件」的順序很重要:所有插件的 create 都跑完,才會進入 afterCreate。所以 Redux、Auth 這些「裝在專案裡」的插件,可以放心假設專案已經存在。
4. Context:插件之間共用的東西
每個插件拿到同一個 context:專案路徑、使用者的選擇、執行指令的工具,以及一個讓插件之間交換資料的 state。插件不需要各自 import 工具,也不會互相直接呼叫。
v0.8:把插件修到能用
v0.7.1 時架構完成了,但功能沒跟上:Auth 產生的檔案結構和 create-next-app 對不上、Landing Page 模板沒有實作、Redux 固定用 npm 安裝、插件失敗只印警告卻顯示「完成」,v0.5 的安全寫檔也在重構時遺失了。這也是它後來很少被用到的原因。
v0.8 針對這些問題逐一修正:
1. Auth 改寫成 App Router 版本
改用 NextAuth v4 搭配 App Router,產生的檔案都放在 src/ 底下,和 create-next-app 的結構一致:
src/lib/auth.ts ← 驗證設定
src/app/api/auth/[...nextauth]/route.ts ← NextAuth 路由
src/app/login/page.tsx ← 登入頁
src/app/dashboard/page.tsx ← 需要登入的後台
src/components/auth/SignOutButton.tsx ← 登出按鈕
.env.local ← 每個專案自動產生不同的 NEXTAUTH_SECRET
測試時遇到一個新版 Next.js 的變化:新建立的專案預設開啟了 cacheComponents,在頁面裡直接讀取 session(會用到 cookies)會讓 build 失敗。解法是把讀取 session 的部分包進 <Suspense>,這個寫法不論有沒有開啟 cacheComponents 都能正常運作。
2. 安全寫檔變成每個插件都能用的工具
v0.5 的 safeInject 移進了 context,所有插件都透過它寫檔:目標路徑不能跑出專案資料夾,檔案已經存在時預設不覆蓋。
3. 錯誤分級
插件可以標記自己是不是「關鍵插件」:
- 關鍵插件(例如建立專案本體)失敗時,立刻中止整個流程。例如目標資料夾已經存在,就在
validate階段停下來,不會覆蓋任何東西 - 其他插件失敗時,繼續執行剩下的步驟,最後列出所有失敗的項目,並以錯誤狀態結束,不再顯示「完成」
4. 安裝指令統一由 context 提供
插件只要說「要裝什麼」,用 pnpm、npm 還是 yarn,由使用者在選單的選擇決定。執行一次性的 CLI(例如 create-next-app、shadcn)也一樣。
5. 其他修正
- 選擇「不使用 UI」時,不再安裝 Tailwind
- shadcn 改用新的
shadcn@latest init - 還沒實作的 Landing Page 先從選單拿掉
- 刪除重構後已經失效的舊程式
實際測試
在 Next.js 16 上實際建立專案:
- 後台模板 + Auth + Redux + shadcn(pnpm):建立成功,
next build通過;未登入會被導向登入頁,登入後進入後台,登出後回到登入頁 - Next.js 模板、不使用 UI(npm):使用 npm 安裝、沒有 Tailwind,
next build通過 - 目標資料夾已存在:在驗證階段就中止
發布到 npm
v0.8 已經發布到 npm,不用下載原始碼,一行指令就能使用:
npx @vince115/nextforge my-app接下來
- 補上 Landing Page 模板
小結
這個專案最大的收穫不是工具本身,而是插件化的思考方式:核心只定義流程,功能全部外掛。之後不管是 CLI、建置流程,或是任何「一連串步驟加上可選功能」的系統,都可以套用同樣的結構。
也提醒自己一件事:架構完成不等於產品完成。v0.7.1 停在「架構很漂亮,但打開不能用」的狀態;v0.8 補上的,正是讓最常用的那條路徑真正跑通。