1.0.4 • Published 1 year ago

xrx-cli v1.0.4

Weekly downloads
-
License
ISC
Repository
-
Last release
1 year ago

发布一个脚手架

首先npm初始化项目 npm init

npm init

在node环境,为了支持esm,在package.json加上

"name": "xrx-cli", // 项目名称
"type": "module",

在根目录创建bin文件夹,用来存放bin命令的脚本文件。新建index.js

在index.js加上

#!/usr/bin/env node
console.log(111)

#!/usr/bin/env node 这是为了告诉系统,运行这个文件要使用哪个解析器,并且在哪里找到该解析器。

bin

在package.json加上

"bin": {
  "xrx-cli": "bin/index.js",
},

加上此属性之后,当别人安装自己的包,就会生成xrx-cli命令,能够在在终端上使用xrx-cli命令。

npm link

为了模拟安装后的效果,我们在脚手架项目的根目录执行

npm link

该命令会在npm的全局安装目录下,模拟“安装”这个包,其实是生成一个软连接,连接到该项目。所以我们实时改动的代码,在执行xrx-cli命令的时候,也会实时生效。

如果我们想在某个本地项目局部安装,就在项目执行

npm link xrx-cli

此时就会在该项目安装xrx-cli(在node_modules生成软连接)

局部解绑

npm link xrx-cli

全局解绑

npm link -g xrx-cli

commander
为了分析命令行参数,并且执行相应的命令,我们需要用到commander,它提供给用户命令行输入和参数解析

  • command(命令)

init
util/constant.js

export const ORGIN_TYPE = ['react', 'vue'];
export const ORGINS = {
  react: "git@github.com:XuRuiXi/xrx-react-env-template.git",
  vue: ""
}

util/index.js

import { fileURLToPath } from 'url';
import fs from 'fs';
import path from 'path';


// 根据 当前文件绝对路径 和  相对路径 返回绝对路径
const dirname = (filePath, relativePath) => {
  /* 
    fileURLToPath
    将文件URL转换为路径
    file:///C:/Users/xuruixi/Desktop/npm%E5%8C%85%E5%8F%91%E5%B8%83/xrx-cli/bin/cm.js
    --->    C:\Users\xuruixi\Desktop\npm包发布\xrx-cli\bin\cm.js
   */
  /* 
    path.dirname
    用来获取文件/目录所在的文件路径
    G:\tutorials\nodejs-path-dirname\index.js
    --->    G:\tutorials\nodejs-path-dirname
    G:\tutorials\nodejs-path-dirname
    --->    G:\tutorials
  */
  return `${path.dirname(fileURLToPath(filePath))}\\${relativePath.replace(/\//g, '\\')}`;
};

const deleteFolder = (path) => {
  var files = [];
  if(fs.existsSync(path)) {
    files = fs.readdirSync(path);
    files.forEach(function(file, index) {
      var curPath = path + "/" + file;
      if(fs.statSync(curPath).isDirectory()) { // recurse
        deleteFolder(curPath);
      } else { // delete file
        fs.unlinkSync(curPath);
      }
    });
    fs.rmdirSync(path);
  }
};

const testSshAddress = url => {
  const reg = /^.+\.git$/g;
  return reg.test(url);
}

export {
  dirname,
  deleteFolder,
  testSshAddress
}

bin/index.js

import fs from 'fs'
import { dirname, testSshAddress } from '../util/index.js'
import { program  } from 'commander'
import changeOrgin from '../commands/changeOrgin.js';
import resetOrgin from '../commands/resetOrgin.js';
import init from '../commands/init.js';
import { ORGIN_TYPE } from '../util/constant.js';
import chalk from 'chalk'

const packageText = fs.readFileSync(dirname(import.meta.url, '../package.json'), 'utf-8');
const json = JSON.parse(packageText);

program
  .name(json.name)
  .usage('<command>')
  .version(json.version)
  .description('一款用于初始化react / vue项目的脚手架')

program
  .command('init')
  .argument('[projectName]', '项目名称')
  .option('-t, --type <typeName>', 'react / vue')
  .description('初始化项目的命令')
  .action((projectName, opts) => {
    if (opts.type && !ORGIN_TYPE.includes(opts.type)) {
      console.log(chalk.red(`type只能是${ORGIN_TYPE.join('/')}其中一个`));
      return;
    }
    init(projectName, opts.type)
  })

我们声明了一个命令:init
它有一个可选的参数(projectName),当执行选项的时候(-t, --type),则必须输入参数(typeName),而且typeName只能是react / vue 其中一个。当输入完成之后执行action。init为项目初始化函数。


除了初始化项目,我们再声明一个可以改变react / vue 模板源的命令

changeOrgin

/template.json

{
  "react": "git@github.com:XuRuiXi/xrx-react-env-template.git",
  "vue": ""
}

commands/changeOrgin.js

import fs from 'fs';
import { dirname } from '../util/index.js';
import symbols from 'log-symbols'
import chalk from 'chalk'
chalk.level = 1;

export default function(orginType, orginAddress) {
  const templateText = fs.readFileSync(dirname(import.meta.url, '../template.json'), 'utf-8');
  const templateJson = JSON.parse(templateText);
  templateJson[orginType] = orginAddress;
  fs.writeFileSync(dirname(import.meta.url, '../template.json'), JSON.stringify(templateJson, null, 2), 'utf-8')
  console.log(symbols.success, chalk.green(`更改${orginType}的模板地址成功`))
  console.log('-------------')
  console.log(chalk.green('目前的模板地址如下:'))
  console.table(templateJson);
}

log-symbols:一个控制台显示图表的库,目前支持4种图表(info、success、warning、error) chalk:可以让我们更加简单的结合console.log输出各种颜色的字 chalk.level:0|1|2|3 官方解释是数字越大支持的颜色种类越多

bin/index.js

program
  .command('changeOrgin')
  .argument('<orginType>', '更改的源类型 react / vue')
  .argument('<orginAddress>', '更改的源地址')
  .description('更改react / vue的模板源')
  .action((orginType, orginAddress) => {
    if (!ORGIN_TYPE.includes(orginType)) {
      console.log(chalk.red(`orginType只能是${ORGIN_TYPE.join('/')}其中一个`));
      return;
    }
    if (!testSshAddress(orginAddress)) {
      console.log(chalk.red(`目前只支持项目的SSH地址`))
      return;
    }
    changeOrgin(orginType, orginAddress)
  })

我们再声明一个可以重置模板源的命令

resetOrgin

commands/resetOrgin.js

import { ORGINS } from "../util/constant.js";
import { dirname } from "../util/index.js";
import chalk from "chalk";
import symbol from 'log-symbols';
import fs from 'fs';
chalk.level = 1;

export default function() {
  fs.writeFileSync(dirname(import.meta.url, '../template.json'), JSON.stringify(ORGINS, null, 2), 'utf-8');
  const template = fs.readFileSync(dirname(import.meta.url, '../template.json'), 'utf-8');
  const templateJSON = JSON.parse(template);
  console.log(symbol.success, chalk.green('模板源重置成功'))
  console.log('--------------')
  console.log(chalk.green('目前的模板地址如下:'))
  console.table(templateJSON)
}

bin/index.js

program
  .command('resetOrgin')
  .description('重置回默认的模板源')
  .action(() => {
    resetOrgin();
  })

program.parse() 这个函数放在index.js的最后执行,因为只有执行了该方法,Command才能解析命令行参数。但是如果放在命令/选项之前执行,就会因为在这之前初始化导致没有这些命令/选项...

init
最后,我们回到最开始的init函数,它接收两个参数projectName(项目名称), typeName(项目类型)

commands/init.js

import fs from 'fs'
import { dirname, deleteFolder } from '../util/index.js'
import { resolve } from 'path'
import inquirer from 'inquirer'
import ora from 'ora'
import download from 'download-git-repo'
import symbols from 'log-symbols'
import chalk from 'chalk'

export default function(name, type){
  const json = fs.readFileSync(dirname(import.meta.url, '../template.json'), 'utf-8')
  const templateList = JSON.parse(json);
  chalk.level = 1;
  const question = [
    {
      name: 'projectName',
      type: 'input',
      message: '请输入项目名称',
      validate(val) {
        if (!val) {
          return '请输入项目名称'
        } else {
          return true
        }
      }
    },
    {
      type: 'list',
      name: 'projectType',
      message: '请选择项目类型:',
      default: 'react',
      choices: [
        { value: 'react', name: 'react' },
        { value: 'vue', name: 'vue' }
      ]
    },
    {
      type: 'list',
      name: 'jest',
      default: 'no',
      message: '是否集成Jest测试?',
      choices: [
        { value: 'no', name: 'no' },
        { value: 'yes', name: 'yes' }
      ]
    }
  ].filter(i => ![name && 'projectName', type && 'projectType'].includes(i.name))

  inquirer.prompt(question).then((answers) => {
    let { projectName = name, jest, projectType = type } = answers;
    let url = templateList[projectType]
  
    console.log(chalk.green('\n Start generating... \n'))
    // 出现加载图标
    const spinner = ora('Downloading...')
    spinner.start()
  
    download(`direct:${url}`, `./${projectName}`, { clone: true }, (err) => {
      if (err) {
        spinner.fail()
        console.log(chalk.red(symbols.error), chalk.red(`Generation failed. ${err}`))
        return
      }
      const json = fs.readFileSync(resolve(`./${projectName}/package.json`), 'utf-8');
      const newJson = JSON.parse(json);
      newJson.name = projectName;
      if (jest === 'no') {
        Reflect.deleteProperty(newJson.devDependencies, 'jest');
        Reflect.deleteProperty(newJson.devDependencies, '@types/jest');
        deleteFolder(resolve(`./${projectName}/test`));
      }
      fs.writeFileSync(resolve(`./${projectName}/package.json`), JSON.stringify(newJson, null, 2), 'utf-8', (err) => {
        if (err) throw err;
      });
  
      // 结束加载图标
      spinner.succeed()
      console.log(chalk.green(symbols.success), chalk.green('Generation completed!'))
      console.log('\n To get started')
      console.log(`\n    cd ${projectName} \n`)
    })
  })
}

ora:在终端显示loading状态

inquirer:让我们在命令行进行问答输入,支持手动输入、单选、多选...

download:可以让我们根据http / ssh 下载仓库代码