ts + esbuild搭建自定义cli脚手架
现如今,前端框架是多得数不过来,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】和大家一起探讨探讨
#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 ,记得点个星星哈
转载自:https://juejin.cn/post/7011300982713745416