likes
comments
collection
share

ts + esbuild搭建自定义cli脚手架

作者站长头像
站长
· 阅读数 21

现如今,前端框架是多得数不过来,3大主角:react / vue / angular 更是入门的基础,而学习这些框架最开始入手就是学习使用他们的脚手架,例如:create-react-app,vue-cli,angular-cli,可快速的创建好项目,提高效率;

但是我们不能只限于会用,知道其原理更能让我们在团队中,特别是做基建的时候起到很大的帮助。接下来我们一起了解下

脚手架的基本流程

这里拿create-react-app举个例子:

使用命令行创建项目

$ create-react-app my-project

则马上生成对应的模版:(cra没有像vue-cli那么多模版选择)

my-project

├─ node_module
├─ public
│  ├─ favicon.ico
│  ├─ index.html
│  ....(省略其他的)
├─ src
│  ├─ App.js
│  ├─ index.js
│  └─ ...(省略其他的)
└─ package.json

像这样通过一句命令行,或者多几步交互选择,就可以快速生成一个项目,这里我用ts写的一个脚手架【dyi-cli】和大家一起探讨探讨

ts + esbuild搭建自定义cli脚手架

#1 新建项目 dyi-cli-demo

$ mkdir dyi-cli-demo && cd dyi-cli-demo && npm init  && tsc --init

#2 配置ts

$ tsc --init

我们简单的配置下tsconfig.json

{
  "compilerOptions": {
    "target": "es6", 
    "module": "commonjs", 
    "outDir": "./lib", 
    "baseUrl": "./src",
    "strict": true, 
    "moduleResolution": "node",
    "esModuleInterop": true, 
    "skipLibCheck": true, 
    "forceConsistentCasingInFileNames": true, 
  },
  "include": [
    "./src",
  ]
}

#3 可先一次性将我们的依赖包先安装好

$ npm i commander chalk dyi-tool fs-extra inquirer ora util typescript glob esbuild --save-dev

#4 创建入口文件 src/bin/cli.ts

#! /usr/bin/env node

import { Command } from 'commander'
import { chalk } from 'chalk'

const program = new Command()

// 配置帮助信息
program.on('--help', () => {
  console.log(
    `\r\n Run ${chalk.green(
      `dyi <command> --help`,
    )} to understand the details\r\n `,
  )
})
program.parse(process.argv)

#5 package.json配置如下

{
  "name": "dyi-cli-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "bin": {
    "dyi": "./lib/bin/cli.js"
  },
  ....
}

执行tsc,生成lib文件夹及其编译成js文件, 再npm link 链接到全局

tsc && npm link

执行dyi,验证我们的cli命令已经生效

$ dyi
Usage: dyi <command> [option]

Options:
  -V, --version                    output the version number
  -h, --help                       display help for command

Commands:
  create [options] <project-name>  create a new project
  help [command]                   display help for command

 Run dyi <command> --help to understand the details

好了,我们的cli命令已经成功了,加下来来完善我们的cli.ts:

#! /usr/bin/env node

import chalk from 'chalk'
import { Command } from 'commander'
import create from '../create'
const program = new Command()

// 创建文件命令
program
  .command('create <project-name>')
  .description('create a new project')
  .option('-f --force', 'if it exist, overwrite directory')
  .action((name: string, options: any) => {
   	console.log('准备创建的项目名称', name, options)
  })

// 配置版本号信息
program.version(require(../../package.json).version).usage('<command> [option]')

// 配置帮助信息
program.on('--help', () => {
  console.log(
    `\r\n Run ${chalk.green(
      `dyi <command> --help`,
    )} to understand the details \r\n `,
  )
})

// 解析参数
program.parse(process.argv)

执行 dyi create my-project

➜  dyi-cli-demo git:(master) ✗ dyi create my-project
准备创建的项目名称 my-project {}

可以看到,我们成功的执行了dyi create < app-name >, 并且成功得到回调,接下来我们可以在这里做我们想做的事情啦。

#6 创建 src/create.ts文件

import chalk from 'chalk'
import { existsSync, remove } from 'fs-extra'
import path from 'path'

const Create = async (name: string, options: any) => {
  // 1.获取当前位置(当前输入命令行的位置)
  const cwd = process.cwd()

  // 2.需要创建的文件(在当前输入命名的位置进行创建)
  const targetPath = path.join(cwd, name)

  // 3.通过交互式命令行,选择我们要创建的模版
  
  // 4.判断项目是否已存在
  if (existsSync(targetPath)) {
    // 强制替换: dyi create my-project -f
    if (options.force) {
      await remove(targetPath)
    } else {
      // 如果存在,则通过交互式命令询问是否覆盖项目
    }
  }
  
	// 5.复制我们准备好的模版
  console.log(
    `${chalk.green('\n Successfully created project')}  ${chalk.cyan(name)}`,
  )
}

export default Create

#7 创建项目模版 src/template,目录如下:

├─ src
│  ├─ template
│  ├── react-tmp
│  ├──── ...(省略,react项目文件)   
│  ├── vue-tmp
│  ├──── ...(省略,vue项目文件) 
│  ├── taro-tmp
│  ├──── ...(省略,taro项目文件) 
└─ ...(省略其他的)

上面的我们已经把create.ts涉及的功能和逻辑都梳理出来了,接下来我会一次性将该模块写好,一些依赖包不熟悉的,可以自行查下,这里不做讲解哈,

完整的create.ts文件:

import chalk from 'chalk'
import { copyDir } from 'dyi-tool'
import { existsSync, remove } from 'fs-extra'
import { prompt } from 'inquirer'
import ora from 'ora'
import path from 'path'

const Create = async (name: string, options: any) => {
  // 1.获取当前位置(当前输入命令行的位置)
  const cwd = process.cwd()

  // 2.需要创建的文件(在当前输入命名的位置进行创建)
  const targetPath = path.join(cwd, name)
	
  // 3.通过交互式命令行,选择我们要创建的模版
  const { projectName } = await prompt({
    name: 'projectName',
    type: 'list',
    choices: [
      { name: 'react-tmp', value: 'react-tmp' },
      { name: 'vue-tmp', value: 'vue-tmp' },
      { name: 'taro-tmp', value: 'taro-tmp' },
    ],
    message: '请选择一个项目模版进行创建',
  })
  
  // 4.判断项目是否已存在
  if (existsSync(targetPath)) {
    // 强制替换: dyi create my-project -f
    if (options.force) {
      await remove(targetPath)
    } else {
      // 如果存在,则通过交互式命令询问是否覆盖项目
      const { replace } = await prompt([
        {
          name: 'replace',
          type: 'list',
          message: `项目已存在、是否确认覆盖? ${chalk.grey(
            '覆盖后原项目无法恢复',
          )}`,
          choices: [
            { name: '确认覆盖', value: true },
            { name: '再考虑下,暂不覆盖', value: false },
          ],
        },
      ])
      if (!replace) {
        return
      }
      await remove(targetPath)
    }
  }
  
	// 5.复制我们准备好的模版
  const spinner = ora('downloading template...')
  spinner.start()
  
  // copyDir复制文件夹的内容
  const res = await copyDir(`./src/template/${projectName}`, `./${name}`)
  if (res === false) {
    console.log(chalk.red(`downloading failed ...`))
    spinner.fail('downloading failed ...')
    return false
  }
  spinner.succeed()
  console.log(
    `${chalk.green('\n Successfully created project')}  ${chalk.cyan(name)}`,
  )
  console.log(`\n cd ${chalk.cyan(name)}`)
  console.log('\n npm install')
  console.log('\n npm run dev \n')
}

export default Create

这里说下 copyDir 是我是为大家写的一个复制文件夹的方法,具体用法可查看 dyi-tool 依赖包

在我们的cli.ts中引入create方法

....
import create from '../create'

// 创建文件命令
program
  .command('create <project-name>')
  .description('create a new project')
  .option('-f --force', 'if it exist, overwrite directory')
  .action((name: string, options: any) => {
     create(name, options)
  })

...

基本配置文件我们已经写好了,试试效果,执行tsp & npm link,创建我们的项目 dyi create my-project

$ dyi create my-project

选择我们需要的模版

➜  dyi-cli-demo git:(master) ✗ dyi create my-project 
? 请选择一个项目模版进行创建 (Use arrow keys)
❯ react-tmp 
  vue-tmp 
  taro-tmp
  
// 选择好模版之后 ==================

➜  dyi-cli-demo git:(master) ✗ dyi create my-project 
? 请选择一个项目模版进行创建 react-tmp

✔ downloading template...

 Successfully created project  my-project

 cd my-project

 npm install

 npm run dev 

创建成功,并且在根目录下面生成我们的项目模版项目

├─ node_module
├─ my-project
│  ├─ (...项目模版)
├─ mode_module
│  ....(依赖包)
├─ src
│  ├─ bin
│  ├─── cli.ts
│  ├─ template
│  ├─── react-tmp
│  ├─── vue-tmp
│  ├─── taro-tmp
└─ package.json

到了这里,有小伙伴会问,说好的esbuild呢? 新时代的我们都讲究效率,就不啰嗦,直接上esbuild, 又快又简单的构建工具;

#8 使用esbuild构建打包我们的项目

安装esbuild

$ npm i esbuild --save -dev

根目录下创建build.js文件,src/build.js

const esbuild = require('esbuild')
const path = require('path')
const glob = require('glob')
const chalk = require('chalk')
const fs = require('fs-extra')
const libPath = path.join(process.cwd(), 'lib')

/** 假如lib文件夹已存在,则清空 */
if (fs.existsSync(libPath)) {
  fs.emptyDirSync(libPath)
}

/** 匹配src文件夹下所有ts文件 */
const matchFiles = async () => {
  return await new Promise((resolve) => {
    glob('src/**/*.ts', { root: process.cwd() }, function (err, files) {
      if (err) {
        console.error(err)
        process.exit(1)
      }
      resolve(files)
    })
  })
}
/** esbuild 配置 */
const build = async function () {
  await esbuild.build({
    entryPoints: await matchFiles(),
    bundle: false,
    splitting: false,
    outdir: path.join(process.cwd(), 'lib'),
    format: 'cjs',
    platform: 'node',
    minify: false,
    color: true,
  })
  console.log(chalk.green('success build \r\n'))
}

build()

修改package.json文件

"scripts": {
	"build": "node build.js"
},

执行 npm run build,会在根目录下面生成lib文件夹,

$ npm run build

$ npm link 

$ dyi create my-project

好了,到了这里基本整个搭建过程已经完成了,这里只讲了简单的教程和思路,至于下载的模版,大家也可以选择下载远程的模版,这里推荐download-git-repo依赖包,大家可以自己研究下;

更具体的模版可以看下我的github上的dyi-cli-demo ,记得点个星星哈