跳转到内容

通过“可交互式”命令行,实现 UnoAPI 初始化配置功能

前言

:::block-3 上期已经实现了 UnoAPI 从 0 到 1 的落地,本期开始写第一个功能代码:初始化配置文件

📜 文章将分为 6 个小节展开:[TOC] :::

正文

看似比较简单的功能,但涉及到的东西会比较杂,我会尽量按照开发思路“一步一步”进行讲解。

1️⃣. 实现“生成配置文件”函数

uno init --openapi-url https://...命令中,需要一个“生成配置文件”的方法,这个方法需要传进去一个可选参数openapiURL

关于这个方法的一些思路:

  • 它应该属于核心功能模块;
  • 在 core 包下新建一个config目录,并且提供一个函数generateConfigFile
  • 在函数中创建unoapi.config.ts文件;
  • 文件的内容最好通过读取一个模板文件tpl.ts获取,便于后期维护和修改。

根据以上信息,我们开始写代码:

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应该长这样:

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 ,最好单独配置更省事一点。

ts
{
  "compilerOptions": {
    "outDir": "dist",
    "declaration": true,
    "declarationDir": "dist/types"
  },
  "include": ["src"],
  "exclude": ["src/**/tpl.ts"]
}

但是需要将模板文件复制到输出目录,因为代码中需要读取这个模板的内容。

模板本质上就是一个 txt 纯文本文件,本应该使用模板字符串直接写在代码中的,但是为了看起来更直观、好维护才拆成了独立的 ts 文件。

因此,我们需要在每次编译完成后,将模板文件“复制”到输出目录中。

调整 core 包的编译脚本:

json
"scripts": {
  "build": "tsc -p tsconfig.json && node build/copyTpl.js"
}

实现build/copyTpl.js代码:

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中导出这个函数,考虑到后期可能还会有其他导出内容,所以直接使用*导出。

ts
export * from './config';

导出之后,我们还不能直接在 cli 包中使用。

❓ 为什么不能直接使用?

因为 core 包与 cli 包是两个独立的包,将来会独立发布到 npm 仓库中。如果直接使用,就会在代码上产生强关联,不能独立发布了。

所以我们在 cli 包中应该要这样使用:

ts
import { generateConfigFile } from '@unoapi/core';

但由于我们目前的@unoapi/core包并未真正发布,直接这样使用也是会报错的。

我们需要在 cli 包的package.json文件的dependencies中,添加"@unoapi/core": "workspace:*"

json
"dependencies": {
  "@unoapi/core": "workspace:*"
}

并且还需要执行build命令,最后再执行pnpm i -r进行安装。

bash
# 编译 ts 文件
pnpm build

# 安装依赖
pnpm i -r

此时,cli 包就能正常使用@unoapi/core包中的generateConfigFile函数了。

3️⃣. 判断配置文件是否存在

正常功能实现了,我们还需要处理一些边界情况。

🌈 比如:

用户已经生成过配置文件,并且所有配置都已经调整好了。如果下次不小心再次执行命令时,将以前的配置文件给覆盖掉了,那就很糟糕了。

因此,我们还需要在 core 包中,提供一个existsConfig函数,用于检测配置文件是否已存在。

ts
/**
 * 检查配置文件是否存在
 * @returns
 */
export async function existsConfig() {
  return fs.access(CONFIG_PATH).then(() => true).catch(() => false);
}

4️⃣. 通过“可交互式”命令行提示用户是否覆盖

为了增加用户的使用体验,如果配置文件已存在时,应该提示用户,并且让用户来决定“是否覆盖?”。

要实现“可交互式”命令行,我们需要安装一个依赖包:inquirer

这是完整的“初始化配置文件”的代码:

ts
#!/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这个函数。

这个函数是面向用户的函数,应该清晰地定义函数的出入参和各种数据类型。

ts
/**
 * 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 的项目或许可以帮到你。

此合集将会持续更新。

感谢阅读

大佬们,点个“关注”再走呗~

👇️👇️👇️