通过“可交互式”命令行,实现 UnoAPI 初始化配置功能
前言
:::block-3 上期已经实现了 UnoAPI 从 0 到 1 的落地,本期开始写第一个功能代码:初始化配置文件。
📜 文章将分为 6 个小节展开:[TOC] :::
正文
看似比较简单的功能,但涉及到的东西会比较杂,我会尽量按照开发思路“一步一步”进行讲解。
1️⃣. 实现“生成配置文件”函数
在uno init --openapi-url https://...命令中,需要一个“生成配置文件”的方法,这个方法需要传进去一个可选参数openapiURL。
关于这个方法的一些思路:
- 它应该属于核心功能模块;
- 在 core 包下新建一个
config目录,并且提供一个函数generateConfigFile; - 在函数中创建
unoapi.config.ts文件; - 文件的内容最好通过读取一个模板文件
tpl.ts获取,便于后期维护和修改。
根据以上信息,我们开始写代码:
// packages/core/config/index.ts
import * as fs from 'fs/promises';
const CONFIG_PATH = process.cwd() + '/unoapi.config.ts';
/**
* 生成配置文件
* @param url OpenAPI URL 地址
*/
export async function generateConfigFile(url?: string) {
const tpl = await fs.readFile(__dirname + '/tpl.ts', 'utf-8');
const configContent = tpl.replace('${openapiUrl}', url || 'https://api.example.com/openapi.json');
// 在项目根目录生成 unoapi.config.ts
await fs.writeFile(CONFIG_PATH, configContent, 'utf-8');
}配置文件模板tpl.ts应该长这样:
import { defineUnoAPIConfig } from '@unoapi/core';
export default defineUnoAPIConfig({
openapiUrl: '${openapiUrl}', // 支持返回 Promise 的回调函数
// output: 'src/api', // 如需单独指定模型输出目录:['src/api', 'src/models'],
// cacheFile: 'src/api/openapi.cache.json', // 缓存目录
// typeMapping: [['DateTime', 'string'], [...]], // 自定义类型映射优先
// funcTpl: (context) => { // 自定义 API 函数
// // 返回自定义 API 函数的字符串
// return `export function...`;
// },
});这个模板的内容就跟用户看到的内容一致,不过我们需要替换掉模板中的${openapiUrl}。
模板文件并非真实的代码,因此不需要编译 ❗️❗️❗️
所以需要在tsconfig.json文件中排除掉,顺便修改一下上期的一个问题:子包不要共享主包的tsconfig.json ,最好单独配置更省事一点。
{
"compilerOptions": {
"outDir": "dist",
"declaration": true,
"declarationDir": "dist/types"
},
"include": ["src"],
"exclude": ["src/**/tpl.ts"]
}但是需要将模板文件复制到输出目录,因为代码中需要读取这个模板的内容。
“
模板本质上就是一个 txt 纯文本文件,本应该使用模板字符串直接写在代码中的,但是为了看起来更直观、好维护才拆成了独立的 ts 文件。
因此,我们需要在每次编译完成后,将模板文件“复制”到输出目录中。
调整 core 包的编译脚本:
"scripts": {
"build": "tsc -p tsconfig.json && node build/copyTpl.js"
}实现build/copyTpl.js代码:
const fs = require('fs/promises');
const path = require('path');
const src = path.join(__dirname, '../src/config/tpl.ts');
const dest = path.join(__dirname, '../dist/config/tpl.ts');
fs.copyFile(src, dest);2️⃣. 导出 generateConfigFile 函数
我们需要在入口文件src/index.ts中导出这个函数,考虑到后期可能还会有其他导出内容,所以直接使用*导出。
export * from './config';导出之后,我们还不能直接在 cli 包中使用。
“
❓ 为什么不能直接使用?
因为 core 包与 cli 包是两个独立的包,将来会独立发布到 npm 仓库中。如果直接使用,就会在代码上产生强关联,不能独立发布了。
所以我们在 cli 包中应该要这样使用:
import { generateConfigFile } from '@unoapi/core';但由于我们目前的@unoapi/core包并未真正发布,直接这样使用也是会报错的。
我们需要在 cli 包的package.json文件的dependencies中,添加"@unoapi/core": "workspace:*"。
"dependencies": {
"@unoapi/core": "workspace:*"
}并且还需要执行build命令,最后再执行pnpm i -r进行安装。
# 编译 ts 文件
pnpm build
# 安装依赖
pnpm i -r此时,cli 包就能正常使用@unoapi/core包中的generateConfigFile函数了。
3️⃣. 判断配置文件是否存在
正常功能实现了,我们还需要处理一些边界情况。
“
🌈 比如:
用户已经生成过配置文件,并且所有配置都已经调整好了。如果下次不小心再次执行命令时,将以前的配置文件给覆盖掉了,那就很糟糕了。
因此,我们还需要在 core 包中,提供一个existsConfig函数,用于检测配置文件是否已存在。
/**
* 检查配置文件是否存在
* @returns
*/
export async function existsConfig() {
return fs.access(CONFIG_PATH).then(() => true).catch(() => false);
}4️⃣. 通过“可交互式”命令行提示用户是否覆盖
为了增加用户的使用体验,如果配置文件已存在时,应该提示用户,并且让用户来决定“是否覆盖?”。
要实现“可交互式”命令行,我们需要安装一个依赖包:inquirer。
这是完整的“初始化配置文件”的代码:
#!/usr/bin/env node
import { Command } from 'commander';
import inquirer from 'inquirer';
import { generateConfigFile, existsConfig } from '@unoapi/core';
const program = new Command();
// init 初始化配置文件
program
.command('init')
.option('-u, --openapi-url [openapiUrl]', 'OpenAPI URL 地址')
.description('初始化 UnoAPI 配置文件')
.action(async (options) => {
if (await existsConfig()) {
// 让用户确认
const { isOverwrite } = await inquirer.prompt({
type: 'confirm',
name: 'isOverwrite',
message: '配置文件已存在,是否覆盖?',
default: false,
});
if (!isOverwrite) {
console.error('已取消');
process.exit(1);
}
}
const { openapiUrl } = options;
try {
await generateConfigFile(openapiUrl);
console.log('配置文件创建成功:unoapi.config.ts');
} catch (error) {
console.error('配置文件创建失败:', error);
process.exit(1);
}
});这是覆盖配置文件时的“可交互式”效果:

5️⃣. 定义配置文件
虽然已经生成了uniapi.config.ts配置文件,但现在它是“完全不可用”的,因为还没有提供defineUnoAPIConfig这个函数。
这个函数是面向用户的函数,应该清晰地定义函数的出入参和各种数据类型。
/**
* OpenAPI JSON 文档
*/
interface OpenAPIJsonDoc {
openapi: string;
}
/**
* UnoAPI 上下文
*/
interface UnoAPIContext {
}
/**
* UnoAPI 配置项
*/
interface UnoAPIConfig {
/**
* OpenAPI URL 地址,可以是字符串或返回字符串的函数
*/
openapiUrl: string | (() => Promise<OpenAPIJsonDoc> | OpenAPIJsonDoc);
output?: string | [string, string];
cacheFile?: string;
typeMapping?: string[][];
funcTpl?: (context: UnoAPIContext) => string;
}
/**
* 定义 UnoAPI 配置
* @param config
* @returns
*/
export async function defineUnoAPIConfig(config: UnoAPIConfig | (() => UnoAPIConfig | Promise<UnoAPIConfig>)) {
if (typeof config === 'function') {
return await config();
}
return config;
}根据目前能想到的情况,暂且先定义这么多,后期根据具体情况再进行修改。
后期我们需要“执行”这个面向用户的函数,来拿到用户配置的数据。
6️⃣. 目录结构
这是目前最新的目录结构:

预估 UnoAPI 项目的整体进度当前处在 20%左右(不包括 vscode 扩展),后期大概还需要做的事情有:
- 下载并解析 OpenAPI 文档;
- 生成 API 函数,难点估计于自定义模板方面;
- 生成数据模型,可能将是最难的一部分,主要难点在于各个模型之间、模型与 API 函数之间的关联性;
- 同步更新 API 函数与数据模型;
- 测试、完善、发布相关工作。
如果你也想做一个这样的工具,又不知道如何入手,那么这个从 0 到 1 的项目或许可以帮到你。
此合集将会持续更新。
感谢阅读
大佬们,点个“关注”再走呗~
👇️👇️👇️