nextforge:用插件與生命週期設計一個 Next.js 專案產生器

2026年5月4日

nextforge 互動選單

為什麼要做一個 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 補上的,正是讓最常用的那條路徑真正跑通。

📦 npm | 💻 GitHub