从0到1建立属于自己的脚手架三

598 阅读2分钟

从前面的一和二你会发现,这不就是最基本的git clone 的命令行嘛,这mcf-cli也不过如此,但是并没有实现vue-cli的功能啊

心急吃不着热豆腐,先学会爬,再学会走,下面我们就来看下,怎么实现和vue-cli一样的功能。

创建lib文件夹

/bin  # ------ 命令执行文件
    /mcf        # -----mcf入口文件
    /mcf-add    # ----- 添加修改模板文件
    /mcf-list   # ----- 查看模板列表
    /mcf-init   # ----- 初始化模板项目
    /mcf-delete # ----- 删除模板
/lib  # ------ 工具模块
package.json
template.json  # ------ 模板列表对象

template.json 模板列表对象为空

{}

在bin文件夹下创建 mcf-create-vue文件

#!/usr/bin/env node
const path = require('path')
const fs = require('fs')
const download = require('../lib/download')
const generator = require('../lib/generator')

// 命令行交互工具
const program = require('commander')
//使用shell使用的模式匹配文件,比如星号之类的
const glob = require('glob')

// 终端交互工具
const inquirer = require('inquirer')

// 终端字符串样式设置正确
const chalk = require('chalk')
// 各种日志级别的彩色符号
const logSymbols = require('log-symbols')

program.usage('<project-name>')
  .option('-r, --repository [repository]', 'assign to repository', 'minchao920917/app-vue-template')
  .parse(process.argv);

let projectName = program.args[0];

if (!projectName) {  // project-name 必填
  // 相当于执行命令的--help选项,显示help信息,这是commander内置的一个命令选项
  program.help()
  return
}

const list = glob.sync('*')  // 遍历当前目录
const rootName = path.basename(process.cwd()) // 获取执行当前命令的文件夹名称字符串

let next = undefined
if (list.length) {  // 如果当前目录不为空
  if (list.filter(name => {
    const fileName = path.resolve(process.cwd(), path.join('.', name))
    const isDir = fs.statSync(fileName).isDirectory()
    return name.indexOf(projectName) !== -1 && isDir
  }).length !== 0) {
    console.log(`项目${projectName}已经存在`)
    return
  }
  next = Promise.resolve(projectName)
} else if (rootName === projectName) {
  next = inquirer.prompt([
    {
      name: 'buildInCurrent',
      message: '当前目录为空,且目录名称和项目名称相同,是否直接在当前目录下创建新项目?',
      type: 'confirm',
      default: true
    }
  ]).then(answer => {
    return Promise.resolve(answer.buildInCurrent ? projectName : '.')
  })
} else {
  next = Promise.resolve(projectName)
}

next && go()

function go() {
  next.then(projectRoot => {
    if (projectRoot !== '.') {
      fs.mkdirSync(projectRoot)
    }
    return download(projectRoot, program.repository).then(target => {
      return {
        name: projectRoot,
        root: projectRoot,
        target: target
      }
    })
  }).then(context => {
    return inquirer.prompt([
      {
        name: 'projectName',
        message: '项目的名称',
        default: context.name
      }, {
        name: 'projectVersion',
        message: '项目的版本号',
        default: '1.0.0'
      }, {
        name: 'projectDescription',
        message: '项目的简介',
        default: `A project named ${context.name}`
      }
      , {
        name: 'projectAuthor',
        message: '项目的创建人',
        default: `minchao`
      }
    ]).then(answers => {
      return {
        ...context,
        metadata: {
          ...answers
        }
      }
    })
  }).then(context => {
    // 添加生成的逻辑
    return generator(context.metadata, context.target, path.parse(context.target).dir);
  }).then((res) => {
    // 成功用绿色显示,给出积极的反馈
    console.log(logSymbols.success, chalk.green('创建成功:)'))
    console.log(chalk.green(`cd ${projectName}\nnpm install\nnpm run dev`))
  }).catch(err => {
    // 失败了用红色,增强提示
    console.error(logSymbols.error, chalk.red(`创建失败:${error.message}`))
  })
}

在lib下创建download.js和generator.js

download.js

const path = require('path')
const ora = require('ora')
const download = require('download-git-repo')

module.exports = function (target, url) {
  target = path.join(target || '.', '.download-temp')
  return new Promise((resolve, reject) => {
    const spinner = ora(`正在下载项目模板,源地址:https://github.com/${url}`)
    spinner.start()
    download(url, target, (err) => {
      if (err) {
        spinner.fail()
        reject(err)
      } else {
        // 下载的模板存放在一个临时路径中,下载完成后,可以向下通知这个临时路径,以便后续处理
        spinner.succeed()
        resolve(target)
      }
    })
  })
}

generator.js

// npm i handlebars metalsmith -D
const Metalsmith = require('metalsmith')
const Handlebars = require('handlebars')
const rm = require('rimraf').sync

module.exports = function (metadata = {}, src, dest = '.') {
    if (!src) {
        return Promise.reject(new Error(`无效的source:${src}`))
    }

    return new Promise((resolve, reject) => {
        Metalsmith(process.cwd())
            .metadata(metadata)
            .clean(false)
            .source(src)
            .destination(dest)
            .use((files, metalsmith, done) => {
                const meta = metalsmith.metadata()
                // 目前仅定义替换package.json文件
                Object.keys(files).filter(x => x.includes('package.json')).forEach(fileName => {
                    const t = files[fileName].contents.toString()
                    
                    files[fileName].contents = new Buffer(Handlebars.compile(t)(meta))
                })
                done()
            }).build(err => {
                rm(src)
                err ? reject(err) : resolve()
            })
    })
}

在你的github上创建你的模板项目

例如 vue-template模板 如果你喜欢,请点个star鼓励下

相关地址

mcf-cli npm地址

mcf-cli vue模板地址

mcf-cli 源码地址 如果你喜欢,请点个star鼓励下

总结

各位你看到这里,你也许会说,这么简单吗?这里,我只能说,就是这么简单,先实现从模板复制一个项目到本地,然后再由命令行修改文件名,进而修改项目的配置信息。