mkdir cli && cd cli
npm init --yes
创建src目录及src/cli.js文件,cli.js文件内容如下
export function cli(args) {
console.log(args);
}
创建src/bin/index.js,为了能够正常使用esm
模块,我们需要先安装,执行npm install esm
#!/usr/bin/env node
require = require('esm')(module /*, options*/);
require('../src/cli').cli(process.argv);
npm link
就能在终端使用xbc
执行脚本啦{
"name": "xbc-cli",
"version": "1.0.0",
"main": "src/index.js",
"bin": {
"xbc": "bin/index.js"
},
"dependencies": {
"esm": "^3.2.18"
}
}
在解析处理命令行输入之前,我们需要设计命令行支持的几个选项,如下。
[template]
:支持默认的几种模板类型,用户可以通过 select 进行选择。--git
:等同于git init
去创建一个新的 Git 项目。--install
:支持自动下载项目依赖。--yes
:跳过命令行交互,直接使用默认配置。npm install inquirer arg
编写命令行参数解析逻辑,在cli.js中添加:
import arg from 'arg';
// 解析命令行参数为 options
function parseArgumentsIntoOptions(rawArgs) {
// 使用 arg 进行解析
const args = arg({
'--git': Boolean,
'--yes': Boolean,
'--install': Boolean,
'-g': '--git',
'-y': '--yes',
'-i': '--install',
}, {
argv: rawArgs.slice(2),
});
return {
skipPrompts: args['--yes'] || false,
git: args['--git'] || false,
template: args._[0],
runInstall: args['--install'] || false,
}
}
export function cli(args) {
// 获取命令行配置
let options = parseArgumentsIntoOptions(args);
console.log(options);
}
实现使用默认配置和交互式配置选择逻辑,如下代码:
import arg from 'arg';
import inquirer from 'inquirer';
function parseArgumentsIntoOptions(rawArgs) {
// ...
}
async function promptForMissingOptions(options) {
// 默认使用名为 JavaScript 的模板
const defaultTemplate = 'JavaScript';
// 使用默认模板则直接返回
if (options.skipPrompts) {
return {...options,
template: options.template || defaultTemplate,
};
}
// 准备交互式问题
const questions = [];
if (!options.template) {
questions.push({
type: 'list',
name: 'template',
message: 'Please choose which project template to use',
choices: ['JavaScript', 'TypeScript'],
default: defaultTemplate,
});
}
if (!options.git) {
questions.push({
type: 'confirm',
name: 'git',
message: 'Initialize a git repository?',
default: false,
});
}
// 使用 inquirer 进行交互式查询,并获取用户答案选项
const answers = await inquirer.prompt(questions);
return {...options,
template: options.template || answers.template,
git: options.git || answers.git,
};
}
export async function cli(args) {
let options = parseArgumentsIntoOptions(args);
options = await promptForMissingOptions(options);
console.log(options);
}
下面我们需要完成下载模板到本地的逻辑,我们事先准备好两种名为typescript
和javascript
的模板,并将相关的模板存储在项目的根目录中
我们使用ncp
包实现跨平台递归拷贝文件,使用chalk
做个性化输出。安装相关依赖如下:
npm install ncp chalk
在src/
目录下,创建新的文件main.js
,代码如下:
import chalk from 'chalk';
import fs from 'fs';
import ncp from 'ncp';
import path from 'path';
import {
promisify
}
from 'util';
const access = promisify(fs.access);
const copy = promisify(ncp);
// 递归拷贝文件
async function copyTemplateFiles(options) {
return copy(options.templateDirectory, options.targetDirectory, {
clobber: false,
});
}
// 创建项目
export async function createProject(options) {
options = {...options,
targetDirectory: options.targetDirectory || process.cwd(),
};
const currentFileUrl = import.meta.url;
const templateDir = path.resolve(new URL(currentFileUrl).pathname, '../../templates', options.template.toLowerCase());
options.templateDirectory = templateDir;
try {
// 判断模板是否存在
await access(templateDir, fs.constants.R_OK);
} catch (err) {
// 模板不存在
console.error('%s Invalid template name', chalk.red.bold('ERROR'));
process.exit(1);
}
// 拷贝模板
await copyTemplateFiles(options);
console.log('%s Project ready', chalk.green.bold('DONE'));
return true;
}
接下来,我们需要完成git
的初始化以及依赖安装工作,这时候需要用到以下内容。
execa
:允许开发中使用类似git
的外部命令。
pkg-install
:使用yarn install
或npm install
安装依赖。
listr
:给出当前进度 progress。
npm install execa pkg-install listr
更新main.js
const { projectInstall } = require('pkg-install');
const { access } = require('fs/promises');
const chalk = require('chalk');
const fs = require('fs');
const path = require('path');
const execa = require('execa');
const Listr = require('listr');
const util = require('util');
const ncp = require('ncp');
const copy = util.promisify(ncp);
// 拷贝模板
async function copyTemplateFiles(options) {
return copy(options.templateDirectory, options.targetDirectory, {
clobber: false,
});
}
// 初始化 git
async function initGit(options) {
// 执行 git init
const result = await execa('git', ['init'], {
cwd: options.targetDirectory,
});
if (result.failed) {
return Promise.reject(new Error('Failed to initialize git'));
}
return;
}
// 创建项目
export async function createProject(options) {
options = {
...options,
targetDirectory: options.targetDirectory || process.cwd()
};
const templateDir = path.resolve(
__dirname,
'../templates',
options.template
);
options.templateDirectory = templateDir;
try {
console.log(templateDir);
// 判断模板是否存在
await access(templateDir, fs.constants.R_OK);
} catch (err) {
console.error('%s Invalid template name', chalk.red.bold('ERROR'));
process.exit(1);
}
// 声明 tasks
const tasks = new Listr([
{
title: 'Copy project files',
task: () => copyTemplateFiles(options),
},
{
title: 'Initialize git',
task: () => initGit(options),
enabled: () => options.git,
},
{
title: 'Install dependencies',
task: () =>
projectInstall({
cwd: options.targetDirectory,
}),
skip: () =>
!options.runInstall
? 'Pass --install to automatically install dependencies'
: undefined,
},
]);
// 并行执行 tasks
await tasks.run();
console.log('%s Project ready', chalk.green.bold('DONE'));
return true;
}
此时一个简单的脚手架已经完成,文件模板和cli命令根据实际需求自行配置扩展