建立一個專屬的 CLI

LaughingZhu發表於2024-10-25

作為一個前端,基本上每次初始化專案都會用到腳手架,透過一些腳手架可以快速的搭建一個前端的專案並整合一些所需的功能模組,避免自己每次都手動一個一個去安裝。安裝各個包的這個過程其實沒啥營養,透過封裝一個腳手架來跳過這個步驟,把精力聚焦到功能研發上。

由於最近自己在寫專案都是相同的技術棧:Nextjs + TailwindCSS + TypeScript + ShadcnUI ,有時候如果忘記了 ShadcnUI 安裝的命令的話,還得去 ShadcnUI 官網 去檢視先關的文件。正好最近在看前端工程化相關的內容,簡單封裝一個 cli ,為了以後給 DevNow 來擴充套件一些內容模版做些基建。說 · 感覺就逼格就上來了😎。

順便給自己的開源部落格專案打個廣告,歡迎大家體驗、star:
DevNow 是一個精簡的開源技術部落格專案模版,支援 Vercel 一鍵部署,支援評論、搜尋等功能,歡迎大家體驗。

1. 初始化

新建一個 cli 資料夾 用來存放我們相關的內容。

|-- cli
  |-- index.js

我們主要的 cli 內容都在 index.js 中,接下來我們來實現一下。先來看完整程式碼如下:

#!/usr/bin/env node

const { Command } = require('commander');
const { execSync } = require('child_process');
const prompts = require('prompts');

const program = new Command();
let projectPath = '';
async function initProject() {
  console.log('歡迎使用專案初始化工具!');

  const opts = program.opts();

  // 詢問使用者輸入和選擇
  const response = await prompts([
    {
      type: 'text',
      name: 'projectName',
      message: '請輸入專案名稱:',
      validate: (name) => (name ? true : '專案名稱不能為空'),
      initial: projectPath
    },
    {
      type: 'select',
      name: 'template',
      message: '請選擇專案模板:',
      choices: [
        { title: 'JavaScript', value: 'javascript' },
        { title: 'TypeScript', value: 'typescript' }
      ],
      initial: opts.ts ? 1 : 0 // 預設 TypeScript
    },
    {
      type: 'select',
      name: 'tailwindCSS',
      message: 'Would you like to use Tailwind CSS?',
      choices: [
        { title: 'No', value: false },
        { title: 'Yes', value: true }
      ],
      initial: opts.tailwind ? 1 : 0 // 預設 TypeScript
    },
    {
      type: 'select',
      name: 'eslint',
      message: 'Would you like to use eslint',
      choices: [
        { title: 'No', value: false },
        { title: 'Yes', value: true }
      ],
      initial: opts.eslint ? 1 : 0 // 預設 eslint
    },
    {
      type: 'select',
      name: 'shadcnUI',
      message: 'Would you like to use Shadcn/ui?',
      choices: [
        { title: 'No', value: false },
        { title: 'Yes', value: true }
      ],
      initial: opts.shadcnUI ? 1 : 0 // 預設 shadcnUI
    }
  ]);

  // 專案初始化命令拼接
  const templateFlag = (response.template || opts.ts) === 'typescript' ? '--typescript' : '';
  const eslintFlag = response.eslint || opts.eslint ? '--eslint' : '';
  const tailwindCSS = response.tailwindCSS || opts.tailwind ? '--tailwind' : '';

  try {
    // 執行 Next.js 初始化命令
    console.log(`正在建立專案:${response.projectName}...`);
    execSync(
      `pnpm create next-app@latest ${response.projectName} ${templateFlag} ${eslintFlag} ${tailwindCSS} --turbopack --app --src-dir --import-alias "@/*"`,
      { stdio: 'inherit' }
    );

    // 切換到專案目錄
    process.chdir(response.projectName);

    // 安裝 shadcn/ui
    if (response.shadcnUI || opts.shadcnUI) {
      console.log('安裝 shadcn/ui...');
      execSync('pnpm dlx shadcn@latest init -d', { stdio: 'inherit' });
    }

    console.log('專案初始化完成!');
    console.log(`請執行以下命令進入專案並啟動開發伺服器:`);
    console.log(`  cd ${response.projectName}`);
    console.log(`  pnpm dev`);
  } catch (error) {
    console.error('專案建立失敗:', error.message);
  }
}

// 定義 CLI 命令
program
  .name('create-next-app')
  .description('建立一個 Next.js 專案')
  .argument('<project-name>', '專案名稱')
  .option('--ts, --typescript', 'Initialize as a TypeScript project. (default)')
  .option('--tailwind', 'Initialize with Tailwind CSS config. (default)')
  .option('--eslint', 'Initialize with ESLint config.')
  .option('--shadcnUI', 'Enable ShadcnUi.')
  .action((name) => {
    projectPath = name;
    initProject();
  });

// 解析命令列引數
program.parse(process.argv);

1.1 execSync

execSyncNode.js 的內建模組 child_process 中的一個同步執行命令的方法。它會在當前程序中執行指定的系統命令,並返回結果。

1.2 Commadner.js

Commander.js 完整的 node.js 命令列解決方案。編寫程式碼來描述你的命令列介面。 Commander 負責將引數解析為選項和命令引數,為問題顯示使用錯誤,並實現一個有幫助的系統。

// 定義 CLI 命令
program
  .name('create-next-app')
  .description('建立一個 Next.js 專案')
  .argument('<project-name>', '專案名稱')
  .option('--ts, --typescript', 'Initialize as a TypeScript project. (default)')
  .option('--tailwind', 'Initialize with Tailwind CSS config. (default)')
  .option('--eslint', 'Initialize with ESLint config.')
  .option('--shadcnUI', 'Enable ShadcnUi.')
  .action((name) => {
    projectPath = name;
    initProject();
  });

// 解析命令列引數
program.parse(process.argv);

這段程式碼透過 commander.js 定義了一個功能豐富的 CLI 工具,允許使用者透過不同的選項初始化一個定製化的 Next.js 專案。 parse(process.argv) 是核心步驟,用來解析命令列中的引數並執行對應的邏輯。
我們透過 --help 可以看到一些可選的配置引數等等內容。

1.3 prompts

prompts 是一個輕量級、使用者友好的互動式 CLI 庫,用於在命令列中向使用者提出問題並收集輸入。它支援多種型別的提示,比如文字輸入、選擇、多選、確認等,允許開發者靈活地設計命令列應用的互動體驗。

透過使用 prompts 庫,我們可以在 CLI 中實現類似 Next.js 的互動體驗,這樣可以在初始化專案時根據需要選擇不同的選項,體驗會更好一點。

const response = await prompts([
  {
    type: 'text',
    name: 'projectName',
    message: '請輸入專案名稱:',
    validate: (name) => (name ? true : '專案名稱不能為空'),
    initial: projectPath
  },
  {
    type: 'select',
    name: 'template',
    message: '請選擇專案模板:',
    choices: [
      { title: 'JavaScript', value: 'javascript' },
      { title: 'TypeScript', value: 'typescript' }
    ],
    initial: opts.ts ? 1 : 0 // 預設 TypeScript
  },
  {
    type: 'select',
    name: 'tailwindCSS',
    message: 'Would you like to use Tailwind CSS?',
    choices: [
      { title: 'No', value: false },
      { title: 'Yes', value: true }
    ],
    initial: opts.tailwind ? 1 : 0 // 預設 TypeScript
  },
  {
    type: 'select',
    name: 'eslint',
    message: 'Would you like to use eslint',
    choices: [
      { title: 'No', value: false },
      { title: 'Yes', value: true }
    ],
    initial: opts.eslint ? 1 : 0 // 預設 eslint
  },
  {
    type: 'select',
    name: 'shadcnUI',
    message: 'Would you like to use Shadcn/ui?',
    choices: [
      { title: 'No', value: false },
      { title: 'Yes', value: true }
    ],
    initial: opts.shadcnUI ? 1 : 0 // 預設 shadcnUI
  }
]);

這裡透過配置了一些選項,結合了 command 初始化命令列的預設選項,效果如下:

1.4 執行程式碼

try {
  // 執行 Next.js 初始化命令
  console.log(`正在建立專案:${response.projectName}...`);
  execSync(
    `pnpm create next-app@latest ${response.projectName} ${templateFlag} ${eslintFlag} ${tailwindCSS} --turbopack --app --src-dir --import-alias "@/*"`,
    { stdio: 'inherit' }
  );

  // 切換到專案目錄
  process.chdir(response.projectName);

  // 安裝 shadcn/ui
  if (response.shadcnUI || opts.shadcnUI) {
    console.log('安裝 shadcn/ui...');
    execSync('pnpm dlx shadcn@latest init -d', { stdio: 'inherit' });
  }

  console.log('專案初始化完成!');
  console.log(`請執行以下命令進入專案並啟動開發伺服器:`);
  console.log(`  cd ${response.projectName}`);
  console.log(`  pnpm dev`);
} catch (error) {
  console.error('專案建立失敗:', error.message);
}

這裡就是主要的執行內容,透過前面的配置引數來定製化執行,首先是 Nextjs 相關內容,可以看到我預設了一些配置,這些看個人的需求了,感覺這些東西預設的會更好。最後就是根據配置引數判斷是否要安裝 ShadcnUI .

其實到這裡 CLI 的基本內容就完事了,我們可以在本地執行 node index.js cli-test --tailwind 去測試下。

注意:
本地 Node 環境中需要安裝 commanderprompts 兩個庫,不然會報錯。

當透過 npx create-next-app 等命令初始化專案時,你不需要擔心 commander 等依賴的安裝。工具本身已經打包好了這些依賴,並透過 npx 或 pnpm dlx 臨時載入執行,簡化了使用流程。

2. 釋出

這裡簡單記錄下吧,這個應該很多人已經會了,

2.1 npm init

首先透過 npm init 初始化,透過提示可以生成一些配置檔案,這個時候專案結構應該是:

|-- cli
  |-- index.js
  |-- package.json

package.json 主要內容如下:

{
  "dependencies": {
    "commander": "^12.1.0",
    "prompts": "^2.4.2"
  },
  "name": "create-devnow-app",
  "version": "0.0.6",
  "description": "create devnow app",
  "main": "./index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "LaughingZhu",
  "license": "MIT",
  "bin": {
    "create-devnow-app": "./index.js"
  }
}

確保你的 package.json 檔案中定義了正確的 bin 配置。這樣才能執行。

2.2 登入

npm login

2.3 釋出

npm publish

使用自己專屬的 CLI 😏

pnpm dlx create-devnow-app@latest my-app

大功告成,這樣在後續就可以直接使用了,如果業務中需要其他的一些配置的話,可以透過相同的方式整合,比如 T3 這個專案,就整合了 TRPC 等內容到腳手架中,可以方便快速構建一個全棧的專案。

相關文章