【脚手架】从0到1搭建React18+TS4.x+Webpack5项目(四)发布脚手架

4,352 阅读12分钟

本文正在参加「金石计划」

flag:每月至少产出三篇高质量文章~

在之前已经基于 React18+TS4.x+Webpack5 从0到1搭建了一个 React 基本项目架子,具体的步骤见下面三篇:

这是第四篇,我们将发布我们的脚手架!

本篇文章代码:react18-ts4-webpack5-starter - branch:cha-04

0、前言

想必前端同学对于脚手架应该都不陌生,比如我们经常使用 CRA(create-react-app)vue-cli ,它可以帮助我们快速的初始化一个项目,无需从零配置,极大方便我们的开发。

但在实际开发工作中,可能会有一些定制化的需求、配置或者开发规范,而通用的官方脚手架通常都是最基本的架子,可能无法满足实际需求,这就会导致我们每新开一个项目就会做一些重复性的前置工作,是完全没必要的,这时候就需要我们自己定制化开发一些适合公司业务的脚手架了。

而且,通常大一点的公司为了提高前端开发的效率和质量,通常会选择定制化开发自己的脚手架工具。这么做的目的主要是:

  1. 统一开发规范:不同的开发者可能有不同的编码风格和开发习惯,这会导致代码风格不一致、难以维护。而使用自己定制的脚手架可以规定统一的开发规范,减少了这种情况的发生。
  2. 模板和工具集成:大公司通常有自己的项目和业务需求,可以根据自己的业务场景和需求定制化一套适用的脚手架模板和工具集成,能够大大减少开发过程中的重复劳动和人为出错的可能性。
  3. 提高开发效率:自己定制的脚手架可以提供一些常用的功能和组件,如路由、状态管理、UI组件库等,这些都可以减少开发者的编码时间和提高开发效率。
  4. 保证代码质量:使用自己定制的脚手架可以通过内置的代码检测工具和测试工具等来保证代码质量,减少出现BUG的可能性。

定制化开发自己的脚手架可以帮助公司实现前端开发的标准化、规范化、自动化,提高开发效率和代码质量,降低维护成本。

接下来就开始脚手架的制作。

1、项目初始化

npm init -y

会生成一个 package.json 文件:

{
  "name": "ljy-create-react-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

2、配置入口文件

demo 下创建 bin 文件夹,并在里面创建 node 入口文件 index .js,编辑 index.js 文件:

#!/usr/bin/env node

console.log('ljy')

第一行代码 #!/usr/bin/env node 是一个 Node.js 脚本的开头,它指定了脚本应该使用的解释器,即在环境变量$PATH中查找"node"可执行文件。此行告诉操作系统这个文件应该使用node作为解释器来执行。该脚本可以在类Unix系统(例如Linux、macOS等)中直接运行。

例如,如果你有一个名为index.jsNode.js脚本,包含这个行,那么你可以在终端输入以下命令来运行它:

$ ./index.js

如果没有这行,操作系统将无法识别它是一个Node.js脚本,可能会尝试使用默认解释器来执行,例如Bash。这样可能导致错误,因为Bash不理解Node.js代码。

然后在 package.json 中添加 bin 字段:

{
  "name": "ljy-create-react-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "bin": {
    "ljy": "bin/index.js"
  },
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

3、npm link —— 链接到全局

在文件目录下运行 npm link 将项目链接到本地环境,就可以临时实现 demo 指令全局调用。(--force 参数可以强制覆盖原有指令)

npm link

然后执行 ljy,发现命令行被执行了!并打印出了我们在入口文件中输出的代码。

4、commander —— 指令系统

4.1 基本指令

比如我们会通过不同的指令去做不同的事情:

  • ljy --version
  • ljy --help
  • ljy create xxx
  • ...

通常会使用一个第三方库:commander,很多脚手架都是基于这个库来配置指令,那我们先安装:

npm install commander

使用:

#!/usr/bin/env node

const program = require("commander");

program.version(require("../package.json").version);

// 解析用户执行时输入的参数,process.argv 是 nodejs 提供的属性
// 比如:npm run server --port 3000,后面的 --port 3000 就是用户输入的参数
program.parse(process.argv);

commander 自身附带了 --help 指令,导入成功后,在命令行执行 ljy --help,可以打印出基本的帮助提示。然后我们就可以使用如下的指令了:

image.png

就是这么简单!

为简化使用,Commander 提供了一个全局对象。本文档的示例代码均按此方法使用:

// CommonJS (.cjs)
const { program } = require('commander');

如果程序较为复杂,用户需要以多种方式来使用 Commander,如单元测试等。创建本地 Command 对象是一种更好的方式:

// CommonJS (.cjs)
const { Command } = require('commander');
const program = new Command();
// ECMAScript (.mjs)
import { Command } from 'commander';
const program = new Command();
// TypeScript (.ts)
import { Command } from 'commander';
const program = new Command();

下面来看第一个例子:

#!/usr/bin/env node

const { program } = require("commander");

program.option("--first").option("-s, --separator <char>");

program.parse();

const options = program.opts();
const limit = options.first ? 1 : undefined;

console.log("args", program.args);
console.log(program.args[0].split(options.separator, limit));

然后执行下面的指令:

% ljy -s / --first a/b/c
args [ 'a/b/c' ]
[ 'a' ]

这是一个使用子命令并带有帮助描述的更完整的程序。在多命令程序中,每个命令(或命令的独立可执行文件)都有一个操作处理程序。

再来看一个:

#!/usr/bin/env node

const { Command } = require("commander");
const program = new Command();

program
  .command("split")
  .description("Split a string into substrings and display as an array")
  .argument("<string>", "string to split")
  .option("--first", "display just the first substring")
  .option("-s, --separator <char>", "separator character", ",")
  .action((str, options) => {
    const limit = options.first ? 1 : undefined;
    console.log(str.split(options.separator, limit));
  });

program.parse();

然后执行下面的指令:

% ljy split --separator=/ a/b/c
[ 'a', 'b', 'c' ]

4.2 命令行参数 options

Commander 使用.option()方法来定义选项,同时可以附加选项的简介。每个选项可以定义一个短选项名称(-后面接单个字符)和一个长选项名称(--后面接一个或多个单词),使用逗号、空格或|分隔。

解析后的选项可以通过Command对象上的.opts()方法获取,同时会被传递给命令处理函数。

对于多个单词的长选项,选项名会转为驼峰命名法(camel-case),例如--template-engine选项可通过program.opts().templateEngine获取。

  • -:简写
  • --:全称
  • <xxx>:代表必填参数(后面不跟参数会提示少参数)

选项及其选项参数可以用空格分隔,也可以组合成同一个参数。选项参数可以直接跟在短选项之后,也可以在长选项后面加上 =

serve -p 80
serve -p80
serve --port 80
serve --port=80

--可以标记选项的结束,后续的参数均不会被命令解释,可以正常使用。

默认情况下,选项在命令行中的顺序不固定,一个选项可以在其他参数之前或之后指定。

.opts()不够用时,还有其他相关方法:

  • .optsWithGlobals()返回合并的本地和全局选项值
  • .getOptionValue().setOptionValue()操作单个选项的值
  • .getOptionValueSource().setOptionValueWithSource()包括选项值的来源

4.2.1 常用选项类型、boolean 型选项和带参数选项

有两种最常用的选项,一类是 boolean 型选项,选项无需配置参数,另一类选项则可以设置参数(使用尖括号声明在该选项后,如--expect <value>)。如果在命令行中不指定具体的选项及参数,则会被定义为undefined

program
  .option('-d, --debug', 'output extra debugging')
  .option('-s, --small', 'small pizza size')
  .option('-p, --pizza-type <type>', 'flavour of pizza');

program.parse(process.argv);

const options = program.opts();
if (options.debug) console.log(options);
console.log('pizza details:');
if (options.small) console.log('- small pizza size');
if (options.pizzaType) console.log(`- ${options.pizzaType}`);

执行指令:

ljy-create-react-app % ljy -p
error: option '-p, --pizza-type <type>' argument missing

ljy-create-react-app % ljy -d
{ debug: true }
pizza details:

ljy-create-react-app % ljy -s
pizza details:
- small pizza size

ljy-create-react-app % ljy -d -s -p pizzahut
{ debug: true, small: true, pizzaType: 'pizzahut' }
pizza details:
- small pizza size
- pizzahut

ljy-create-react-app % ljy --pizza-type=cheese
pizza details:
- cheese

多个布尔短选项可以在破折号之后组合在一起,并且可以跟一个取值的单一选项。 例如 -d -s -p cheese 可以写成 -ds -p cheese 甚至 -dsp cheese

ljy-create-react-app % ljy -dsp cheese
{ debug: true, small: true, pizzaType: 'cheese' }
pizza details:
- small pizza size
- cheese

具有预期选项参数的选项是贪婪的,并且无论值如何,都会消耗参数。 所以 --id -xyz 读取 -xyz 作为选项参数。

通过program.parse(arguments)方法处理参数,没有被使用的选项会存放在program.args数组中。该方法的参数是可选的,默认值为process.argv

4.2.2 选项的默认值

选项可以设置一个默认值。

#!/usr/bin/env node

const { Command } = require("commander");
const program = new Command();

program.option(
  "-c, --cheese <type>",
  "add the specified type of cheese",
  "blue"
);

program.parse();

console.log(`cheese: ${program.opts().cheese}`);
ljy-create-react-app % ljy
cheese: blue

ljy-create-react-app % ljy --cheese pizzahut
cheese: pizzahut

4.2.3 必填选项

通过.requiredOption()方法可以设置选项为必填。必填选项要么设有默认值,要么必须在命令行中输入,对应的属性字段在解析时必定会有赋值。该方法其余参数与.option()一致。

#!/usr/bin/env node

const { Command } = require("commander");
const program = new Command();

program.requiredOption("-c, --cheese <type>", "pizza must have cheese");

program.parse();
ljy-create-react-app % ljy
error: required option '-c, --cheese <type>' not specified

详细的信息可以看官方文档 - 中文

4.3 监听指令

#!/usr/bin/env node

// ...

// 监听指令
program.on("--help", function () {
  console.log(""); // 一般打一行空行,便于阅读
  console.log("others");
});

program.parse(process.argv);

// ...

image.png

4.4 归档指令

一般我们会将这些指令单独放在一个地方去归档,以便于以后维护,比如在根目录中新建一个lib来专门放这些指令的信息,将 help 指令的信息放在—— lib/core/help.js

const program = require('commander');

const helpOptions = () => {

  // 增加自己的options
  program.option('-d --dest <dest>', 'A destination folder,例如: -d /src/home/index.js')
  program.option('-f --framework <framework>', 'Your framework,例如: React / Vue')

  // 监听指令
  program.on('--help', function(){
    console.log('')
    console.log('Others')
    console.log(' others')
  })
}

module.exports = helpOptions;
#!/usr/bin/env node

const program =  require('commander');

// 查看版本号
program.version(require('./package.json').version);

const helpOptions = require('./lib/core/help');

// 帮助和可选信息
helpOptions();

program.parse(process.argv);

测试:

4.5 配置指令 command

通过.command().addCommand()可以配置命令,有两种实现方式:为命令绑定处理函数,或者将命令单独写成一个可执行文件(详述见后文)。子命令支持嵌套。

.command()的第一个参数为命令名称。命令参数可以跟在名称后面,也可以用.argument()单独指定。参数可为必选的(尖括号表示)、可选的(方括号表示)或变长参数(点号表示,如果使用,只能是最后一个参数)。

使用.addCommand()program增加配置好的子命令。

// 通过绑定处理函数实现命令(这里的指令描述为放在`.command`中)
// 返回新生成的命令(即该子命令)以供继续配置
program
  .command('clone <source> [destination]')
  .description('clone a repository into a newly created directory')
  .action((source, destination) => {
    console.log('clone command called');
  });

// 通过独立的的可执行文件实现命令 (注意这里指令描述是作为`.command`的第二个参数)
// 返回最顶层的命令以供继续添加子命令
program
  .command('start <service>', 'start named service')
  .command('stop [service]', 'stop named service, or all if no name supplied');

// 分别装配命令
// 返回最顶层的命令以供继续添加子命令
program
  .addCommand(build.makeBuildCommand());

使用.command()addCommand()来指定选项的相关设置。当设置hidden: true时,该命令不会打印在帮助信息里。当设置isDefault: true时,若没有指定其他子命令,则会默认执行这个命令。

4.6 指令参数

如上所述,子命令的参数可以通过.command()指定。对于有独立可执行文件的子命令来说,参数只能以这种方法指定。而对其他子命令,参数也可用以下方法。

Command对象上使用.argument()来按次序指定命令参数。该方法接受参数名称和参数描述。参数可为必选的(尖括号表示,例如<required>)或可选的(方括号表示,例如[optional])。

#!/usr/bin/env node

const { Command } = require("commander");
const program = new Command();

program
  .version("0.1.0")
  .argument("<username>", "user to login")
  .argument("[password]", "password for user, if required", "no password given")
  .action((username, password) => {
    console.log("username:", username);
    console.log("password:", password);
  });

program.parse();

测试:

ljy-create-react-app % ljy tom 123456
username: tom
password: 123456

在参数名后加上...来声明可变参数,且只有最后一个参数支持这种用法。可变参数会以数组的形式传递给处理函数。例如:

program
  .version('0.1.0')
  .command('rmdir')
  .argument('<dirs...>')
  .action(function (dirs) {
    dirs.forEach((dir) => {
      console.log('rmdir %s', dir);
    });
  });

有一种便捷方式可以一次性指定多个参数,但不包含参数描述:

program
  .arguments('<username> <password>');

4.6.1 其他参数配置

有少数附加功能可以直接构造Argument对象,对参数进行更详尽的配置。

program
  .addArgument(new commander.Argument('<drink-size>', 'drink cup size').choices(['small', 'medium', 'large']))
  .addArgument(new commander.Argument('[timeout]', 'timeout in seconds').default(60, 'one minute'))

4.6.2 自定义参数处理

选项的参数可以通过自定义函数来处理(与处理选项参数时类似),该函数接收两个参数:用户新输入的参数值和当前已有的参数值(即上一次调用自定义处理函数后的返回值),返回新的命令参数值。

处理后的参数值会传递给命令处理函数,同时可通过.processedArgs获取。可以在自定义函数的后面设置命令参数的默认值或初始值。

program
  .command('add')
  .argument('<first>', 'integer argument', myParseInt)
  .argument('[second]', 'integer argument', myParseInt, 1000)
  .action((first, second) => {
    console.log(`${first} + ${second} = ${first + second}`);
  })
;

4.7 处理函数

命令处理函数的参数,为该命令声明的所有参数,除此之外还会附加两个额外参数:一个是解析出的选项,另一个则是该命令对象自身。

program
  .argument('<name>')
  .option('-t, --title <honorific>', 'title to use before name')
  .option('-d, --debug', 'display some debugging')
  .action((name, options, command) => {
    if (options.debug) {
      console.error('Called %s with options %o', command.name(), options);
    }
    const title = options.title ? `${options.title} ` : '';
    console.log(`Thank-you ${title}${name}`);
  });

测试:

ljy-create-react-app % ljy --title=hello kevin -d
Called ljy with options { title: 'hello', debug: true }
Thank-you hello kevin

如果你愿意,你可以跳过为处理函数声明参数直接使用 command。 this 关键字设置为运行命令,可以在函数表达式中使用(但不能从箭头函数中使用)。

program
  .command('serve')
  .argument('<script>')
  .option('-p, --port <number>', 'port number', 80)
  .action(function() {
    console.error('Run script %s on port %s', this.args[0], this.opts().port);
  });

处理函数支持async,相应的,需要使用.parseAsync代替.parse

async function run() { /* 在这里编写代码 */ }

async function main() {
  program
    .command('run')
    .action(run);
  await program.parseAsync(process.argv);
}

使用命令时,所给的选项和命令参数会被验证是否有效。凡是有未知的选项,或缺少所需的命令参数,都会报错。 如要允许使用未知的选项,可以调用.allowUnknownOption()。默认情况下,传入过多的参数并不报错,但也可以通过调用.allowExcessArguments(false)来启用过多参数的报错。

4.8 生命周期钩子

可以在命令的生命周期事件上设置回调函数。

program
  .option('-t, --trace', 'display trace statements for commands')
  .hook('preAction', (thisCommand, actionCommand) => {
    if (thisCommand.opts().trace) {
      console.log(`About to call action handler for subcommand: ${actionCommand.name()}`);
      console.log('arguments: %O', actionCommand.args);
      console.log('options: %o', actionCommand.opts());
    }
  });

钩子函数支持async,相应的,需要使用.parseAsync代替.parse。一个事件上可以添加多个钩子。

支持的事件有:

事件名称触发时机参数列表
preActionpostAction本命令或其子命令的处理函数执行前/后(thisCommand, actionCommand)
preSubcommand在其直接子命令解析之前调用(thisCommand, subcommand)

5、设计 create action

一般我们会将这些指令单独放在一个地方去归档,以便于以后维护,比如在根目录中新建一个lib来专门放这些指令的信息,将help指令的信息放在lib/core/help.js,创建指令的信息放在lib/core/create.js中。

5.1 helpOptions

下面实现 helpOptions

// lib/core/help.js

const program = require('commander');

const helpOptions = () => {

  // 增加自己的options
  program.option('-d --dest <dest>', 'A destination folder,例如: -d /src/home/index.js')
  program.option('-f --framework <framework>', 'Your framework,例如: React / Vue')

  // 监听指令
  program.on('--help', function(){
    console.log('')
    console.log('Others')
    console.log(' others')
  })
}

module.exports = helpOptions;

bin/index.js 中使用:

// bin/index.js

#!/usr/bin/env node

const program =  require('commander');

// 查看版本号
program.version(require('../package.json').version);

const helpOptions = require('../lib/core/help');

// 帮助和可选信息
helpOptions();

program.parse(process.argv);

测试:

ljy-create-react-app % ljy --help
Usage: ljy [options]

Options:
  -V, --version               output the version number
  -d --dest <dest>            A destination folder,例如: -d /src/home/index.js
  -f --framework <framework>  Your framework,例如: React / Vue
  -h, --help                  display help for command

Others
 others

5.2 createCommands

再实现 createCommands:

const program = require('commander');

const createCommands = () => {
  program
  .command('create <project> [others...]')
  .description('clone a repo into a folder')
  .action((project, others) => {
    console.log('project', project);
    console.log('others', others)
  })
}

module.exports = createCommands;

并引入到 bin/index.js 中:

#!/usr/bin/env node

const program =  require('commander');

// 查看版本号
program.version(require('./package.json').version);

const helpOptions = require('./lib/core/help');
const createCommands =  require('./lib/core/create');

// 帮助和可选信息
helpOptions();

// 创建指令
createCommands();

program.parse(process.argv);

测试:

ljy-create-react-app % ljy create xxx
project xxx
others []

5.3 createProjectActions

action 的回调函数就是我们脚手架的核心流程了,将其抽离到一个单独的文件 lib/core/actions.js 中:

// lib/core/actions.js

// 封装create指令的acitons
const createProjectActions = (project, others) => {
  console.log('project', project);
  console.log('others', others)

  // 1,clone项目

  // 2,运行 npm install

  // 3,运行 npm run dev
}

module.exports = createProjectActions;

lib/core/create.js 中使用:

const program = require('commander');
const createProjectActions = require('./actions');

const createCommands = () => {
  program
  .command('create <project> [others...]')
  .description('clone a repo into a folder')
  .action(createProjectActions)
}

module.exports = createCommands;

5.3.1 clone项目

clone 项目一般会用到一个工具库:download-git-repo,它是放在 npmgitlab 上的,在 github 上面没有仓库,vue-cli 用的也是这个来下载项目模板。

先下载库:

npm install download-git-repo

这个库不好的地方就是它使用的写法比较老旧:

download('flippidippi/download-git-repo-fixture', 'test/tmp', function (err) {
  console.log(err ? 'Error' : 'Success')
})

我们一般会将repo地址提取出来,方便以后进行维护:

image.png

// lib/config/repo-config.js

// github 为了政治正确 把默认分支改成了main,所以这里需要再后面带一个分支信息,否则会报错
const reactRepo =
  "direct:https://github.com/ian-kevin126/react18-ts4-webpack5-starter#main";

module.exports = {
  reactRepo,
};

我们的操作都是在回调里面去做,如果当操作很多的时候,就会造成回调地狱。所幸,node提供了一个模块可以改变这种操作—— promisify,可以将这种方法转化成 promise 形式。

// 封装create指令的acitons
const { promisify } = require('util')
const download = promisify(require('download-git-repo'))
const { reactRepo } = require('../config/repo-config')

// callback ---> promisify ---> Promise ---> async await
const createProjectActions = async (project, others) => {
  // 1,clone项目
  await download(reactRepo, project, { clone: true })

  // 2,运行npm install

  // 3,运行npm run dev
}

module.exports = createProjectActions

现在我们测试一下这个指令,新建一个测试文件夹,然后在终端运行:

ljy create demo-repo

image.png

可以发现,我们刚刚配置的 github repo 里面的项目被我们clone下来了。

5.3.2 执行 npm install

接下来,我们希望在clone完项目代码之后,自动执行 package.json 中的 dependencies 依赖的安装,并且将依赖下载的信息打印在控制台里。

需要注意的是,我这里使用的是 pnpm,如果你没有安装这个工具,可能会报错。

还可以使用 chalk 对控制台信息做一些美化,先安装依赖:

npm i chalk@4.0.0

然后就可以在代码中使用了:

// 封装create指令的acitons
const chalk = require("chalk");

const { promisify } = require("util");
const download = promisify(require("download-git-repo"));

const { reactRepo } = require("../config/repo-config");
const { spawnCommand } = require("../utils/terminal");

// callback ---> promisify ---> Promise ---> async await
const createProjectActions = async (project, others) => {
  console.log(chalk.green.underline.bold("> Start download repo..."));

  // 1,clone项目
  await download(reactRepo, project, { clone: true });
  console.log(chalk.green("> 模板下载完成,开始 pnpm install..."));

  // 2. 执行 npm install
  // 需要判断一下平台,window 执行 npm 命令实际执行的 npm.cmd
  const command = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
  await spawnCommand(command, ["install"], { cwd: `./${project}` });

  // 3,运行npm run dev
  await spawnCommand(command, ["run", "dev"], { cwd: `./${project}` });
};

module.exports = createProjectActions;

附终端命令相关的执行方法封装:

// lib/utils/terminal.js

/**
 * 终端命令相关
 */
const { spawn } = require("child_process");

// 创建进程执行终端命令
const spawnCommand = (command, args, options) => {
  return new Promise((resolve, reject) => {
    // 通过 spawn 创建一个子进程,并把进程返回
    const childProcess = spawn(command, args, options);
    // 将子进程输出的东西放进当前进程的全局变量 process 的 stdout 中
    // 比如说,当子进程执行 npm install,执行完的时候,会输出一些信息
    // childProcess.stdout 就是这个输出信息流,通过 pipe 将流信息存到当前进程(主进程)
    childProcess.stdout.pipe(process.stdout);
    // 将子进程错误信息放进当前进程
    childProcess.stderr.pipe(process.stderr);

    childProcess.on("close", (code) => {
      if (code === 0) {
        resolve();
      } else {
        reject(`错误:${code}`);
      }
    });
  });
};

module.exports = {
  spawnCommand,
};

执行 ljy create demo-repo

image.png

就可以看到已经将代码拉到了本地了!

5.4 命令行 loading 效果

ora 使用非常简单,可以直接看下面的案例。更多使用: ora 文档,利用 ora 来实现一个简单的命令行 loading 效果。

先安装 ora:

npm i ora@5.4.0

然后在 acrtions.js 中使用:

// ...
 
const ora = require("ora");

// ...

// 定义一个loading
const gitRepoSpinner = ora("Downloading github repo, please wait a while...");

// callback ---> promisify ---> Promise ---> async await
const createProjectActions = async (project, others) => {
  console.log(chalk.green.underline.bold("> Start download repo..."));
  gitRepoSpinner.start();

  // 1,clone项目
  await download(reactRepo, project, { clone: true });

  gitRepoSpinner.succeed();

  // ...
};

module.exports = createProjectActions;

重新执行 ljy create demo-repo,就可以看到控制台有了 loading 效果:

image.png

5.5 ASCII 的艺术字

figlet 模块可以将 text 文本转化成生成基于 ASCII 的艺术字。

先安装依赖:

npm install figlet

然后在 actions.js 中使用:

// ...

var figlet = require("figlet");

// ...

// callback ---> promisify ---> Promise ---> async await
const createProjectActions = async (project, others) => {
  console.log(
    "\r\n" +
      figlet.textSync("LJY-CLI", {
        font: "Big",
        horizontalLayout: "default",
        verticalLayout: "default",
        width: 80,
        whitespaceBreak: true,
      }) +
      "\r\n"
  );

  // ...
};

module.exports = createProjectActions;

重新执行 ljy create demo-repo,就会看到:

image.png

是不是有那味儿了!figlet 提供了多种字体,可以去官网选择你喜欢的字体。

6、发布脚手架

  1. 登录npm账号
  2. 在终端 npm login,输入账户密码
  3. npm publish

就这么简单,然后你就可以在 npm 官网看到你的 package 了!

这是我的:

end~