「腳手架cli」技術揭祕

我將以前搭建的一個掘金:vue3+ts企業級開發環境寫成了一個腳手架,本來設想用這個腳手架快速搭建公司各個項目的開發環境,正在一點點集成。先來看下效果:javascript

demo.gif

【文章目標】:css

  • 介紹一些工具包以及運用場景
  • 腳手架原理(開發思路)
  • 實現一個腳手架

爲了更好的理解和更有效率的學習,建議先下載這個項目lu-clivue

如下是腳手架中一般會用到的一些工具包,在後面的文章內容中我會給你們講解一下這些包的主要用途,先讓咱們來看看有哪些包:java

還有更多有趣好玩的包等你挖掘。node

硬核知識點集錦(工具包介紹以及運用場景)

其中關於包的細節應用我就不過多描述了,建議你們先npm init初始化一個開發項目,能夠先調用一下這些包看看都是用來作什麼的,效果是什麼樣子,便於後面的理解,官網地址都已經在上面給你們列出來了。react

1. 建立命令

commander包是一套完成的命令行解決方案,用來建立腳手架命令,如lucli create appgit

// index.js

#!/usr/bin/env node

const program = require("commander");

program
.version("0.0.1", "-v, --version") // 定義版本
.command("create app") // 命令名稱
.description("create an application") // 描述
.action((source,destination)=>{ // 執行回調
  console.log('1',source);
  console.log('2',destination);
  // 執行一些邏輯,例如如下的交互邏輯
})

//解析命令行
program.parse();
複製代碼

注意:必定要執行program.parse();解析命令,不然在你可能會直接傻掉,腳手架大業未始而崩殂。github

在開發環境下經過以下命令進行測試:正則表達式

node ./bin/index.js create app
複製代碼

當咱們的腳手架開發完成後,在package.jsonbin字段中進行映射。例如:vue-router

// package.json

"bin":{
  "lucli":"./index.js"
}
複製代碼

發佈在npm,當用戶在全局安裝完咱們的腳手架工具以後,index.js將會被映射到lucli對應的全局 bins中,這樣就能夠在命令行中執行了:

lucli create app
複製代碼

固然,在沒發佈以前,咱們還能夠經過npm link命令手動進行映射。

注意: 命令行權限問題,windows用戶建議管理員模式運行。mac用戶建議用sudo執行

npm link 

or

sudo npm install
複製代碼

執行完成後咱們也能夠執行lucli create app命令了。

2. 收集用戶交互信息

經過inquirer工具進行與用戶交互。

const inquirer = require("inquirer")

 inquirer.prompt([
    {
      name:"name", 
      message:"the name of project: ", // 項目名稱
      type:"input",// 字符類型
      default:"lucli-demo", // 默認名稱
      validate: (name) => { // 驗證名稱是否正確
        return validProjectName(name).errMessage || true;
      }
    },
    {
      name:"framework", 
      message:"project framework", // 項目框架
      type:"list",
      choices:[ // 選項
        {
          name: "vue + ts",
          value: "vue"
        },
        {
          name: "react",
          value: "react"
        }
      ]
    }
  ])
  .then((answers) => {
    console.log('結果:',answers);
  })
  .catch((error) => {
    if (error.isTtyError) {
      // Prompt couldn't be rendered in the current environment
    } else {
      // Something else went wrong
    }
  });
複製代碼

3. 肯定目標工程路徑

咱們在以上代碼中獲取到了用戶建立的工程名稱,咱們經過path肯定工程路徑。

const path = require("path");

// 記住這個targetDir
const targetDir = path.join(process.cwd(), '工程名稱');
複製代碼

targetDir是你目標工程在本地的絕對地址,例如:

/Users/lucas/repository/study/lu-cli
複製代碼

4. 匹配目錄

在腳手架中經過globby用來讀取咱們的模版目錄。

const globby = require("globby");

const files = await globby(["**/*"], { cwd: './template', dot: true })

console.log(files)
複製代碼

結果是一個基於你指定目錄下全部文件的路徑數組。

例如是這樣一個目錄:

├─template
│  ├─src
│  │  ├─index.js
│  │  ├─router
│  │  │  ├─index.ts

結果以下:
files: [ "src/index.js" , "src/router/index.ts" ]
複製代碼

以文件爲最小單元。 這個工具的具體做用在實戰中比較明顯,接着日後看。

5. 讀寫文件

fs-extrafs模塊的增強版,在原有功能的基礎上新增了一些api。

const fs = require("fs-extra")

// 讀取文件內容
const content = fs.readFileSync('文件路徑', 'utf-8'); 

// 寫成文件
fs.writeFileSync('文件路徑','文件內容')
複製代碼

一般用來渲染文件,除了以上方式,模版文件還能夠經過如下方式從代碼倉庫下載。

6. 下載倉庫代碼

經過download-git-repo從代碼倉庫git clone代碼。

const download = require("download-git-repo");

download('https://www.xxx..git', 'test/tmp', function (err) {
  console.log(err)
})
複製代碼

7. 模版渲染

經過ejs進行一般在模版中須要根據不一樣的條件進行渲染。例如:

  • package.json中根據用戶是否須要安裝babel的添加關於babel的一些配置。
  • main.js的文件中根據功能,渲染不一樣代碼。
  • xx.vue的模版中動態設置css預編譯。
  • ...
const ejs = require('ejs')

// demo-01
const template = (
  `<%_ if (isTrue) { _%>`
  + '內容'
  + `<%_ } _%>`
)

const newContent = ejs.render(template, {
  isTrue: true,
})

//demo-02
const template = (
  `<style lang="<%= cssPre %>">`
  +`.redColor{`
  +`color:red`
  +`}`
  + `</style>`
)

const newContent = ejs.render(template, {
  cssPre: 'less',
})
複製代碼

8. 子進程執行管理器

經過execa執行終端命令,好比npm install等命令,這個工具還能夠設置安裝源。

const executeCommand = (command, args, cwd) => {
  return new Promise((resolve, reject) => {
    const child = execa(command, args, {
      cwd,
      stdio: ['inherit', 'pipe', 'inherit'],
    })

    child.stdout.on('data', buffer => {
      const str = buffer.toString()
      if (/warning/.test(str)) {
        return
      }
      process.stdout.write(buffer)
    })

    child.on('close', code => {
      if (code !== 0) {
        reject(new Error(`command failed: ${command}`))
        return
      }
      resolve()
    })
  })
}

// 這裏的targetDir是上面咱們的目標工程路徑,在這個路徑下執行npm install
await executeCommand('npm', ['install'], targetDir)
複製代碼

9. 終端字符串樣式。

經過chalk實如今終端中展現不一樣樣式的字符串。

const chalk = require('chalk');

console.log(chalk.blue('Hello world!'));
複製代碼

10. 文件內容轉換成AST

vue-cli中經過vue-codemod將文件內容轉換成AST,從而實如今文件中注入代碼的功能,返回文件內容字符串。

const { runTransformation } = require("vue-codemod")

const fileInfo = {
  path: "src/main.js", 
  source: "文件內容"
}

// 對代碼進行解析獲得 AST,再將參數 imports 中的語句插入
const injectImports = (fileInfo, api, { imports }) => {
  const j = api.jscodeshift
  const root = j(fileInfo.source)

  const toImportAST = i => j(`${i}\n`).nodes()[0].program.body[0]
  const toImportHash = node => JSON.stringify({
    specifiers: node.specifiers.map(s => s.local.name),
    source: node.source.raw,
  })

  const declarations = root.find(j.ImportDeclaration)
  const importSet = new Set(declarations.nodes().map(toImportHash))
  const nonDuplicates = node => !importSet.has(toImportHash(node))
  const importASTNodes = imports.map(toImportAST).filter(nonDuplicates)

  if (declarations.length) {
    declarations
      .at(-1)
      // a tricky way to avoid blank line after the previous import
      .forEach(({ node }) => delete node.loc)
      .insertAfter(importASTNodes)
  } else {
    // no pre-existing import declarations
    root.get().node.program.body.unshift(...importASTNodes)
  }

  return root.toSource()
}

const params = { 
  imports: [ "import { store, key } from './store';" ]
}

const newContent = runTransformation(fileInfo, transformation, params)
複製代碼

腳手架原理(開發思路)

總結

腳手架原理就是經過收集到用戶交互信息後,根據不一樣定製化的需求,去讀取咱們提早準備好的模版信息,而後對個別文件作一些差別化的更新,更新的核心就是如何修改文件的內容,有三種方式:

  • 利用vue-codemod
  • 模版渲染
  • 正則匹配

這一塊會在後面詳細說明,最後將內容對應的都寫入到咱們的目標工程下面。在執行安裝命令。

讀取模版的方式有多種,你還能夠簡單粗暴的直接用download-git-repo的方式從遠程倉庫代碼下載。這種方式看你怎麼利用了,若是下載完整的項目代碼都不用作配置,那要準備的模版可能比較多,相對死板,你也能夠將通用模版放在倉庫中經過這種方式下載,而不放在項目中從而減小項目包大小。甚至涉及到版本更新的問題,各有利弊。一般咱們將模版放在項目中,方便維護。

核心原理都同樣,更多須要你費心神多是想一想怎麼組織好你的代碼更合理了。

如下我更多經過代碼結構的方式幫助你們去梳理具體要怎麼開發腳手架的一個思路。

開發思路

新建一個js文件用於建立腳手架命令,能夠經過node或者在package.json中用npm link的方式進行調試。

在命令執行完畢的回調中咱們建立用戶交互,等待用戶交互完成後,假設拿到了咱們這些信息:

{
    name:"lucli-demo", // 項目名稱
    framework:"vue", // vue框架
    funcList:["router","vuex","less"] // 功能列表
}
複製代碼

如今咱們的目標就明確了,構建一個集成了router,vuex,lessvue開發環境。接下來咱們大體思路就是下載模版,而後在對應的文件中注入代碼,好比在package.json中添加對應的依賴。

咱們將模版根據功能進行劃分,vue框架模版目錄以下:

vue-plugins/default/  // 默認模版
vue-plugins/router/ // 路由模版
vue-plugins/vuex/ // vuex模版
複製代碼

下載模版其實就是讀取模版內容而後生成一個文件:

// 寫成文件
fs.writeFileSync('文件路徑','文件內容')
複製代碼

那咱們生成多個文件是否是能夠經過循環遍歷一個對象,那基於此咱們設想將咱們要構建的文件列表都放在一個對象裏面,這個數組的結構以下:

{
  'src/main.js':'內容',
  'src/App.vue':'內容',
  'src/theme.less':'內容',
  'package.json':'內容'
  ...
}
複製代碼

接下來要作的就是去生成這個對象,首先咱們建立一個Generator類,咱們將最終要渲染的文件目錄放在裏面:

// src/Generator.js

class Generator{
  constructor(){
    this.files = {}; // 文件目錄
  }
}
複製代碼

經過globby去讀取咱們的模版目錄,而後遍歷這個對象,經過文件系統(fs-extra)去讀取對應的文件內容,咱們這邊以router模版爲例。

// Generator.js

class Generator{
  constructor(){
    this.files = {}; // 文件目錄
  }
  
  render(){
    // 讀取router功能模板
    const files = await globby(["**/*"], { cwd: './src/vue-plugins/router', dot: true })
    for (const rawPath of files) {
      // 讀取文件內容
      const content = getFileContent(rawPath)
      // 更新files對象
      this.files[rawPath] = content; 
    }
  }
}
複製代碼

咱們在每一個模版目錄下面都建立一個index.js,經過調用這個render方法將模版信息都保存在files這個對象中。

// src/vue-plugins/router/index.js

module.exports = (generator) => {
  // 渲染模版
  generator.render("vue-plugins/router/template");
}
複製代碼

咱們在獲取到功能列表後,循環這個列表,依次去執行這個render方法。

const generator = new Generator();
// 循環功能加載模版
_funcsList.forEach(funcName => {
  require(`../src/${pluginName}/${funcName}/index.js`)(generator);
});
複製代碼

固然,咱們也別忘了加載咱們的默認模板,畢竟默認模板纔是項目架構的主體。

這樣咱們的files對象就是要渲染成文件的目錄了。

剩下就是在特定的文件中作一些差別化的處理了,好比main.js中引入router等。

這就涉及到如何在一個文件中插入代碼了,有3種方式:

  • vue-cli中利用了vue-codemod這個包將文件內容轉換成AST,而後在AST對應的節點插入內容,再將AST轉換成文件內容。
  • 文件內容本質上會被讀取成一個字符串,插入代碼就是在對應的地方插入字符串而已,咱們能夠經過正則表達式的方式去匹配位置。
  • 經過模版渲染的方式根據條件去判斷要渲染的內容,或者將要插入的代碼經過變量的方式直接注入到內容中,這種方式須要你對模版作一些處理,若是你對模版渲染還不太瞭解的建議先去了解下ejs

關於package.json咱們單獨作處理,用簡單的對象合併作相關的差別化,若是有複雜的配置也能夠考慮模版渲染。

思想仍是同樣的,咱們在Generator中建立一個codeInFiles對象用來存放要插入代碼的文件以及要插入的內容,pkg對象存放package.json內容。

// src/Generator.js

class Generator{
  constructor(){
    this.pkg = {
      name,
      version: "1.0.0",
      description: "",
      scripts: {
        dev: "vite --mode development",
        build: "vue-tsc --noEmit && vite build",
        prebuild: "vue-tsc --noEmit && vite build --mode staging",
        serve: "vite preview",
      },
    }; // package.json
    this.files = {}; // 文件目錄
    this.codeInFiles = { // 要插入代碼的對象
      '路徑':new Set()
    };
  }
  
  // 更新要插入代碼的codeInFiles對象
  injectImports(path, source) {
    const _imports = this.codeInFiles[path] || (this.codeInFiles[path] = new Set());
    (Array.isArray(source) ? source : [source]).forEach(item => {
      _imports.add(item)
    })
  }
}
複製代碼

router功能爲例,在router功能模版的index.js中調用:

// src/vue-plugins/router/index.js

module.exports = (generator) => {

  // 渲染模版
  generator.render("vue-plugins/router/template");
  
   // 添加依賴
  generator.extendPackage({
    "dependencies": {
      "vue-router": "^4.0.10",
    }
  })
  
   // 注入代碼
  generator.injectImports("src/main.ts", "import router from './router';");
}
複製代碼

循環遍歷codeInFiles這個對象來進行插入代碼,將新的文件內容用來更新files對象,我這邊以vue-codemod爲例子:

// Generator.js

  // 處理package對象
  extendPackage(obj) {
    for (const key in obj) {
      const value = obj[key];
      if (isObject(value) && (key === 'dependencies' || key === 'devDependencies' || key === 'scripts')) {
        this.pkg[key] = Object.assign(this.pkg[key] || {}, value);
      } else {
        this.pkg[key] = value;
      }
    }
  }
  
  // 往files中插入代碼
  injectImports(){
    Object.keys(_codeInFiles).forEach(file => {
      const imports = _codeInFiles[file] instanceof Set ? Array.from(_codeInFiles[file]) : [];
      if (imports && imports.length) {
       // 將新插入代碼後的文件內容更新files對象
        _files[file] = runTransformation(
          { path: file, source: _files[file] },
          injectImports,
          { imports },
        )
      }
    })
}
複製代碼

而後根據文件目錄files依次生成文件便可。

// 生成package,json文件
fs.writeFileSync('package.json', JSON.stringify(this.pkg, null, 2))
// 生成其餘文件
Object.keys(files).forEach((name) => {
  fs.writeFileSync(filePath, files[name])
})
複製代碼

這樣咱們的項目架構就基本搭建好了。

最後經過execa安裝依賴便可。

// 執行npm install安裝依賴包
await executeCommand('npm', ['install'], targetDir)
複製代碼

腳手架實戰

提醒:實戰過程當中,做者不會說的特別細,代碼貼的不會很完整,爲了更好的學習效率:

建議先你們下載這個項目lu-cli,在實戰的時候能夠作爲參考,以及查找代碼。

初始化項目

經過npm init初始化咱們的腳手架項目,完善下目錄結構:

├─cli-project // 項目名稱
│  ├─bin 
│  │  ├─index.js // 命令文件
│  ├─src // 源碼
│  ├─package.json // 配置文件
│  ├─README.md 
複製代碼

建立命令

// bin/index.js

#!/usr/bin/env node
const program = require("commander");
const handlePrompts = require("../src/create");

program
  .version("0.0.1", "-v, --version")
  .command("create app")
  .description("create an application")
  .action(() => {
    // 處理交互
    handlePrompts();
  })

//解析命令行
program.parse();
複製代碼

運行調試

node ./bin/index.js create app
複製代碼

建立交互

咱們在根目錄下建立src/create.js用來處理交互邏輯,這一塊我就不過多贅述了,你能夠根據本身的想法設計交互方案,這塊的代碼我就不完整貼了:

// src/create.js

const inquirer = require("inquirer")
const boxen = require('boxen');
const chalk = require('chalk');
const path = require("path");
const { promptTypes } = require("./enum");
const getPromptsByType = require("./getPrompts");
const Generator = require("./Generator");

const {
  executeCommand,
  validProjectName
} = require("./utils/index");

module.exports = () => {
  // 打印咱們的歡迎信息
  console.log(chalk.green(boxen("歡迎使用 lucli ~", { borderStyle: 'classic', padding: 1, margin: 1 })));
    
  inquirer.prompt([
    {
      name: "name",
      message: "the name of project: ", // 項目名稱
      type: "input",// 字符類型
      default: "lucli-demo", // 默認名稱
      validate: (name) => {
        return validProjectName(name).errMessage || true;
      }
    },
    {
      name: "framework",
      message: "project framework", // 項目框架
      type: "list",
      choices: [
        {
          name: "vue + ts",
          value: promptTypes.VUE
        },
        {
          name: "react",
          value: promptTypes.REACT
        }
      ]
    }
  ]).then(answers=>{
    // 根據框架選擇prompts
    const prompts = getPromptsByType(answers.framework);
    if (prompts.length) {
      // 選擇功能
      inquirer.prompt(prompts).then(async (funcs) => {
        // 邏輯處理 will code
      })
    } else {
      console.log('抱歉,正在開發中,敬請期待!');
    }
  })
 }
複製代碼

建立Generator類

處理完交互了,咱們拿到了對應的信息:

{
    name:"lucli-demo", // 項目名稱
    framework:"vue", // vue框架
    funcList:["router","vuex","less"] // 功能列表
}
複製代碼

接下來就是生成files文件了,首先建立Generator類,初始化咱們的filescodeInFilespkg等對象以及一些處理函數。

這個類實際上是在開發中一點點的完善起來的,做者爲了偷懶就直接貼出來了。大家也能夠本身嘗試去寫一下。

// src/Generator.js

const path = require("path");
const ejs = require('ejs');
const fs = require("fs-extra");
const { runTransformation } = require("vue-codemod")
const {
  writeFileTree,
  injectImports,
  injectOptions,
  isObject
} = require("../src/utils/index")
const {
  isBinaryFileSync
} = require('isbinaryfile');

class Generator {

  constructor({ name, targetDir }) {
    this.targetDir = targetDir;
    this.pkg = { // package.json
      name,
      version: "1.0.0",
      description: "",
      scripts: {
        dev: "vite --mode development",
        build: "vue-tsc --noEmit && vite build",
        prebuild: "vue-tsc --noEmit && vite build --mode staging",
        serve: "vite preview",
      },
    };
    this.files = {}; // 文件目錄
    this.codeInFiles = {}; // 要插入代碼的文件
    this.optionInFiles = {}; // 注入項
    this.middlewareFuns = []; // 處理文件目錄的函數列表
  }

  // 處理package對象
  extendPackage(obj) {
    for (const key in obj) {
      const value = obj[key];
      if (isObject(value) && (key === 'dependencies' || key === 'devDependencies' || key === 'scripts')) {
        this.pkg[key] = Object.assign(this.pkg[key] || {}, value);
      } else {
        this.pkg[key] = value;
      }
    }
  }

  // 更新要插入代碼的對象
  injectImports(path, source) {
    const _imports = this.codeInFiles[path] || (this.codeInFiles[path] = new Set());
    (Array.isArray(source) ? source : [source]).forEach(item => {
      _imports.add(item)
    })
  }

  // 更新要插入的選項的對象
  injectOptions(path, source) {
    const _options = this.optionInFiles[path] || (this.optionInFiles[path] = new Set());
    (Array.isArray(source) ? source : [source]).forEach(item => {
      _options.add(item)
    })
  }

  // 解析文件內容
  resolveFile(sourcePath) {
    // 若是二進制文件則直接返回
    if (isBinaryFileSync(sourcePath)) {
      return fs.readFileSync(sourcePath);
    }
    const template = fs.readFileSync(sourcePath, 'utf-8');
    // 這邊沒什麼必要,若是你有模版渲染才須要加
    const content = ejs.render(template);
    return content;
  }

  // 渲染方法
  async render(source) {
    this.middlewareFuns.push(async () => {
      const relativePath = `./src/${source}`;
      const globby = require("globby");
      // 獲取文件目錄
      const files = await globby(["**/*"], { cwd: relativePath, dot: true })
      for (const rawPath of files) {
        // 獲取絕對地址用於讀取文件
        const sourcePath = path.resolve(relativePath, rawPath)
        const content = this.resolveFile(sourcePath)
        // 有文件內容
        if (Buffer.isBuffer(content) || /[^\s]/.test(content)) {
          this.files[rawPath] = content;
        }
      }
    })
  }

  // 執行函數
  async generator() {
  
    // 設置files的值
    for (const middleawre of this.middlewareFuns) {
      await middleawre();
    }
    
    const _files = this.files;
    const _codeInFiles = this.codeInFiles;
    const _optionsInFiles = this.optionInFiles;

    // 往files中插入代碼
    Object.keys(_codeInFiles).forEach(file => {
      const imports = _codeInFiles[file] instanceof Set ? Array.from(_codeInFiles[file]) : [];
      if (imports && imports.length) {
        _files[file] = runTransformation(
          { path: file, source: _files[file] },
          injectImports,
          { imports },
        )
      }
    })

    // 往files中插入代碼
    Object.keys(_optionsInFiles).forEach(file => {
      const injections = _optionsInFiles[file] instanceof Set ? Array.from(_optionsInFiles[file]) : [];
      if (injections && injections.length) {
        _files[file] = injectOptions(_files[file], injections);
      }
    })

    await writeFileTree(this.targetDir, this.files)

    // 生成package.json文件
    await writeFileTree(this.targetDir, {
      "package.json": JSON.stringify(this.pkg, null, 2)
    })
  }
}

module.exports = Generator;
複製代碼

加載模板

而後咱們肯定咱們的項目路徑,實例化一個Generator類,循環遍歷咱們的功能列表,去加載模版文件,從而構建咱們的files對象。具體的模版信息請參考項目中的模版。

// src/create.js

module.exports = () => {
  ...
  inquirer.prompt(prompts).then(async (funcs) => {
    // 邏輯處理 will code
   
   // 項目路徑
    const targetDir = path.join(process.cwd(), answers.name);
    // 建立實例
    const generator = new Generator({ name: answers.name, targetDir });
    
    const _funcsList = funcs.funcList;
    // 選擇css預編譯
    if (_funcsList.includes("precompile")) {
      const result = await inquirer.prompt([
        {
          name: "cssPrecle",
          message: "less or sass ?",
          type: "list",
          choices: [
            {
              name: "less",
              value: "less"
            },
            {
              name: "sass",
              value: "sass"
            }
          ]
        }
      ]);
      _funcsList.pop();
      // 添加預編譯依賴
      generator.extendPackage({
        "devDependencies": {
          [result.cssPrecle]: result.cssPrecle === "less" ? "^4.1.1" : "^1.35.2"
        }
      })
    }
    
    let pluginName = '';
    // 肯定框架模版
    switch (answers.framework) {
      case promptTypes.VUE:
        pluginName = 'vue-plugins'
        break;
      case promptTypes.REACT:
        pluginName = 'vue-plugins'
        break;
    };

    // 加載默認模版
    require(`../src/${pluginName}/default/index.js`)(generator);

    // 加載功能模版
    _funcsList.forEach(funcName => {
      require(`../src/${pluginName}/${funcName}/index.js`)(generator);
    });
    ...
  })
複製代碼
// src/vue-plugins/vuex/index.js

module.exports = (generator) => {
  // 添加依賴
  generator.extendPackage({
    "dependencies": {
      "vuex": "^4.0.2"
    }
  })

  // 注入代碼
  generator.injectImports("src/main.ts", "import { store, key } from './store';");

  // 注入選項
  generator.injectOptions("src/main.ts", ".use(store, key)");

  // 渲染模版
  generator.render("vue-plugins/vuex/template");
}
複製代碼

在使用inquirer的時候你們靈活的使用,好比關於css預編譯的選項,你也能夠經過type:expand的方式去作,八仙過海,各顯神通。

生成文件

而後執行generator函數將files對象寫成文件,最後安裝package.json中的依賴包:

//src/create.js

const {
  executeCommand,
  validProjectName
} = require("./utils/index");
...

// 執行渲染生成文件
await generator.generator();

// 執行npm install安裝依賴包
await executeCommand('npm', ['install'], targetDir)

console.log(chalk.green(boxen("構建成功", { borderStyle: 'double', padding: 1 })));
複製代碼

這樣咱們一個基礎的腳手架就已經開發完畢了。

最後

掌握了該項目基本上你不只能夠開發腳手架這樣的工具了,你還能夠去寫更多相似的工具去提升工做中的效率了。

這個項目其實還有許多能夠優化的地方,好比安裝依賴包的時候選擇安裝源,建立項目的時候檢測目錄下是否已經存在該文件夾了,還有關於一些開發規範的配置等等。有興趣的夥伴能夠去探索一下,以給我提交mr

有時間的話我後面也會接着更新這篇文章。

謝謝。

相關文章
相關標籤/搜索