@zhuh/oig 8.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +107 -0
  2. package/dist/index.js +274 -0
  3. package/package.json +64 -0
package/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # @zhuh/oig
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@zhuh/oig.svg)](https://www.npmjs.com/package/@zhuh/oig)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ > 从 OpenAPI/Swagger 规范自动生成 TypeScript API 客户端
7
+
8
+ ## 功能特性
9
+
10
+ - 从 OpenAPI 协议直接生成高质量完整可用的 TS
11
+ - 支持本地文件和远程 URL
12
+ - 自动生成类型定义和 API 函数
13
+ - 基于 axios 的 HTTP 客户端封装
14
+ - 自动格式化生成的代码(Biome)
15
+ - 批量生成多个 API
16
+
17
+ ## 安装
18
+
19
+ ```bash
20
+ npm install -g @zhuh/oig
21
+ # 或
22
+ pnpm add -g @zhuh/oig
23
+ ```
24
+
25
+ ## 快速开始
26
+
27
+ 1. 创建配置文件 `src/api/openapi_api.json`:
28
+
29
+ ```json
30
+ {
31
+ "api_addresses": [
32
+ {
33
+ "definition": "petstore",
34
+ "address": "https://petstore.swagger.io/v2/swagger.json"
35
+ }
36
+ ]
37
+ }
38
+ ```
39
+
40
+ 2. 运行生成命令:
41
+
42
+ ```bash
43
+ @zhuh/oig
44
+ ```
45
+
46
+ 3. 查看生成的文件:
47
+
48
+ ```
49
+ src/api/petstore/
50
+ ├── _schemas.gen.ts # 公共 schema
51
+ ├── pet.schema.ts # Pet 相关 schema
52
+ ├── pet.ts # Pet API 函数
53
+ └── _export.ts # 汇总导出
54
+ ```
55
+
56
+ ## 配置说明
57
+
58
+ | 字段 | 类型 | 必填 | 说明 |
59
+ |------|------|------|------|
60
+ | `definition` | string | 是 | 输出目录名 |
61
+ | `address` | string | 是 | Schema 文件路径或 URL |
62
+ | `generate` | boolean | 否 | 是否生成(默认 true) |
63
+
64
+ ## CLI 参数
65
+
66
+ | 参数 | 说明 |
67
+ |------|------|
68
+ | `-s <path>` | 直接指定 schema 路径(兼容 v1) |
69
+ | `-d` | 开发模式(显示详细警告) |
70
+
71
+ ## 生成的文件结构
72
+
73
+ 每个 definition 生成:
74
+
75
+ - `_schemas.gen.ts` — 公共 schema(`#/components/schemas`)
76
+ - `[tag].schema.ts` — 标签专用 schema
77
+ - `[tag].ts` — 标签 API 实现(基于 axios)
78
+ - `_export.ts` — 汇总导出文件
79
+
80
+ ## HTTP 客户端
81
+
82
+ 生成的代码使用 `src/api/_http.ts` 封装的 axios 实例:
83
+
84
+ - 自动处理业务错误码(`data.code !== 0`)
85
+ - 60 秒超时
86
+ - 支持自定义配置
87
+
88
+ ## 开发
89
+
90
+ ```bash
91
+ # 构建
92
+ pnpm build
93
+
94
+ # 格式化
95
+ pnpm format
96
+ ```
97
+
98
+ ## 技术栈
99
+
100
+ - TypeScript + Rollup
101
+ - Biome(代码格式化)
102
+ - @zhuh/orval(代码生成核心)
103
+ - pnpm(包管理)
104
+
105
+ ## 许可证
106
+
107
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,274 @@
1
+ #!/usr/bin/env node
2
+ import { readFile, access, mkdir, writeFile } from 'node:fs/promises';
3
+ import { resolve, relative, parse } from 'node:path/posix';
4
+ import * as p from '@clack/prompts';
5
+ import { isString, kebab } from '@orval/core';
6
+ import { normalizeOptions, resolveSpec, applyTransformer, _effect_getApiBuilder, getApiSchemas } from '@zhuh/orval';
7
+ import { deleteAsync } from 'del';
8
+ import { execa } from 'execa';
9
+ import pLimit from 'p-limit';
10
+ import pc from 'picocolors';
11
+
12
+ const workspace = process.cwd();
13
+ const args = process.argv;
14
+ const ISDEV = args.includes('-d');
15
+ const spin = p.spinner();
16
+ async function loadConfig() {
17
+ const configFileRaw = await readFile(resolve('src/api', 'openapi_api.json'), 'utf8');
18
+ return JSON.parse(configFileRaw);
19
+ }
20
+ async function generateAPIFile(api) {
21
+ const { address, definition } = api;
22
+ const outdir = `src/api/${definition}`;
23
+ const outdirAbsolute = resolve(outdir);
24
+ const configOptions = await normalizeOptions({
25
+ input: address,
26
+ output: {
27
+ httpClient: 'axios',
28
+ target: outdirAbsolute,
29
+ override: {
30
+ mutator: {
31
+ path: resolve('src/api/_http.ts'),
32
+ name: '_http',
33
+ },
34
+ },
35
+ },
36
+ });
37
+ const spec = await resolveSpec(configOptions.input.target, configOptions.input.parserOptions);
38
+ const transformedOpenApi = await applyTransformer(spec, configOptions.input.override.transformer, workspace);
39
+ const { operations: apiOperations, schemas: apiSchemas } = await _effect_getApiBuilder({
40
+ input: configOptions.input,
41
+ output: configOptions.output,
42
+ context: {
43
+ target: isString(configOptions.input.target)
44
+ ? configOptions.input.target
45
+ : workspace,
46
+ workspace,
47
+ spec: transformedOpenApi,
48
+ output: configOptions.output,
49
+ },
50
+ });
51
+ // 建好文件夹
52
+ await access(outdirAbsolute)
53
+ .then(async () => {
54
+ try {
55
+ await deleteAsync([
56
+ `${outdir}/*`,
57
+ `!${outdir}/*/`,
58
+ `!${outdir}/_http.ts`,
59
+ `!${outdir}/openapi_api.json`,
60
+ ]);
61
+ }
62
+ catch (error) {
63
+ if (ISDEV) {
64
+ console.warn(`\n${pc.bgYellowBright(pc.green('只是警告'))}`, 'deleteAsync错误', error);
65
+ }
66
+ }
67
+ })
68
+ .catch(async (error) => {
69
+ if (ISDEV) {
70
+ console.warn(`\n${pc.bgYellowBright(pc.green('只是警告'))}`, `访问文件夹${outdirAbsolute}错误`, error);
71
+ }
72
+ await mkdir(outdirAbsolute, {
73
+ recursive: true,
74
+ });
75
+ });
76
+ const apiFileGenerates = [];
77
+ const limit = pLimit(8);
78
+ const exportFiles = [];
79
+ /**
80
+ * 所有的通用模型(#/components/schemas)不分组写入一个文件
81
+ */
82
+ const allCommonSchema = getApiSchemas({
83
+ input: configOptions.input,
84
+ output: configOptions.output,
85
+ target: isString(configOptions.input.target)
86
+ ? configOptions.input.target
87
+ : workspace,
88
+ workspace,
89
+ spec: transformedOpenApi,
90
+ });
91
+ const writeCommonSchema = async () => {
92
+ if (allCommonSchema.filter((c) => c.model).length === 0)
93
+ return;
94
+ await writeFile(resolve(outdirAbsolute, '_schemas.gen.ts'), allCommonSchema.map((s) => s.model).join('\n'));
95
+ exportFiles.push('_schemas.gen.ts');
96
+ };
97
+ const apiOperationValues = Object.values(apiOperations);
98
+ const tags = [...new Set(apiOperationValues.flatMap(({ tags }) => tags))];
99
+ tags.forEach((tag) => {
100
+ apiFileGenerates.push(limit(async () => {
101
+ const operations = apiOperationValues.filter(({ tags }) => tags.includes(tag));
102
+ /**
103
+ * 统计出tag用哪些接口函数,以及用到了哪些schema,schema有引用了哪些modelSchema
104
+ */
105
+ const implementations = operations.map(({ implementation }) => implementation);
106
+ /**
107
+ * 这是接口函数文件用到的通用schema
108
+ * [tag].ts import _schemas.gen.ts
109
+ */
110
+ let commonSchemaImports = [];
111
+ /**
112
+ * 这是接口函数文件用到的特用schema
113
+ * [tag].ts import [tag].schema.ts
114
+ */
115
+ const schemaImports = [];
116
+ /**
117
+ * 这是接口函数特用schema用到的通用schema
118
+ * [tag].schema.ts import _schemas.gen.ts
119
+ */
120
+ const schemaCommonImports = [];
121
+ operations
122
+ .flatMap(({ imports }) => imports)
123
+ .forEach((meta) => {
124
+ // 这是接口函数文件用到的通用schema
125
+ if (meta.schemaName) {
126
+ if (!commonSchemaImports.find((s) => s.schemaName === meta.schemaName)) {
127
+ commonSchemaImports.push(meta);
128
+ }
129
+ return;
130
+ }
131
+ // 这是接口函数文件用到的特用schema
132
+ if (!schemaImports.find((s) => s.name === meta.name)) {
133
+ schemaImports.push(meta);
134
+ }
135
+ const target = apiSchemas.find((schema) => schema.name === meta.name);
136
+ if (target) {
137
+ // 这是接口函数特用schema用到的通用schema
138
+ schemaCommonImports.push(...target.imports.filter(({ name }) => name));
139
+ }
140
+ });
141
+ // 添加不是$ref的schema
142
+ schemaCommonImports.forEach((schemaCommon) => {
143
+ const exported = allCommonSchema
144
+ .map((s) => s.name)
145
+ .find((name) => name === schemaCommon.name);
146
+ if (!exported) {
147
+ const target = apiSchemas.find((s) => s.name === schemaCommon.name);
148
+ if (target) {
149
+ allCommonSchema.unshift(target);
150
+ }
151
+ }
152
+ });
153
+ // 把不存在的过滤掉
154
+ const allCommonSchemaName = allCommonSchema.map((s) => s.name);
155
+ commonSchemaImports = commonSchemaImports.map((schema) => {
156
+ if (!allCommonSchemaName.includes(schema.name)) {
157
+ return {
158
+ ...schema,
159
+ name: '',
160
+ };
161
+ }
162
+ if (schema.schemaName &&
163
+ !allCommonSchemaName.includes(schema.schemaName)) {
164
+ return {
165
+ ...schema,
166
+ schemaName: '',
167
+ };
168
+ }
169
+ return { ...schema };
170
+ });
171
+ await Promise.all([
172
+ (async () => {
173
+ // [tag].schema.ts
174
+ const schemaRaw = `${schemaCommonImports.length === 0
175
+ ? ''
176
+ : `import type {${[...new Set(schemaCommonImports.map(({ name }) => name))].join(',')}} from './_schemas.gen'
177
+ `}
178
+ ${[...new Set(schemaImports.map(({ name }) => name))].map((name) => apiSchemas.find((s) => s.name === name)?.model ?? '').join('\n')}
179
+ `;
180
+ if (schemaRaw.trim().length > 0) {
181
+ await writeFile(resolve(outdirAbsolute, `${kebab(tag)}.schema.ts`), schemaRaw);
182
+ exportFiles.push(`${kebab(tag)}.schema.ts`);
183
+ }
184
+ })(),
185
+ (async () => {
186
+ let mutatorPath = relative(outdir, 'src/api/_http');
187
+ if (!mutatorPath.startsWith('..')) {
188
+ mutatorPath = `./${mutatorPath}`;
189
+ }
190
+ // [tag].ts
191
+ const implementationRaw = `import { _http } from '${mutatorPath}'
192
+ ${commonSchemaImports.length === 0
193
+ ? ''
194
+ : `import type {${[...new Set(commonSchemaImports.flatMap(({ name, schemaName }) => [name, schemaName]).filter(Boolean))].join(',')}} from './_schemas.gen'
195
+ `}${schemaImports.length === 0
196
+ ? ''
197
+ : `import type {${[...new Set(schemaImports.map(({ name }) => name))].join(',')}} from './${kebab(tag)}.schema'
198
+ `}
199
+ ${implementations.map((implementation) => implementation).join('\n')}`;
200
+ if (implementationRaw.trim().length > 0) {
201
+ await writeFile(resolve(outdirAbsolute, `${kebab(tag)}.ts`), implementationRaw);
202
+ exportFiles.push(`${kebab(tag)}.ts`);
203
+ }
204
+ })(),
205
+ ]);
206
+ }));
207
+ });
208
+ apiFileGenerates.push(limit(async () => await writeCommonSchema()));
209
+ await Promise.all(apiFileGenerates);
210
+ if (exportFiles.length > 0) {
211
+ await writeFile(resolve(outdirAbsolute, '_export.ts'), `${exportFiles.map((f) => `export * from './${parse(f).name}'`).join('\n')}
212
+ `);
213
+ }
214
+ try {
215
+ await execa('pnpm', [
216
+ 'biome',
217
+ 'check',
218
+ '--write',
219
+ outdirAbsolute,
220
+ '--formatter-enabled=true',
221
+ '--javascript-formatter-quote-style=single',
222
+ '--semicolons=as-needed',
223
+ '--linter-enabled=false',
224
+ ]);
225
+ }
226
+ catch (error) {
227
+ if (ISDEV) {
228
+ console.warn(`\n${pc.bgYellowBright(pc.green('只是警告'))}`, 'biome格式化错误', error);
229
+ }
230
+ }
231
+ }
232
+ async function main() {
233
+ p.intro(`V_${pc.bgYellowBright(pc.green(ISDEV ? new Date().toLocaleString() : '8.9.1'))}`);
234
+ let config = null;
235
+ // 兼容版本1用法
236
+ const parseV1SchemaPath = () => {
237
+ const target = args.indexOf('-s');
238
+ if (target === -1)
239
+ return;
240
+ return args[target + 1];
241
+ };
242
+ const schemaPath = parseV1SchemaPath();
243
+ if (schemaPath) {
244
+ config = {
245
+ api_addresses: [
246
+ {
247
+ address: schemaPath,
248
+ definition: '',
249
+ },
250
+ ],
251
+ };
252
+ }
253
+ else {
254
+ config = await loadConfig();
255
+ }
256
+ const limit = pLimit(1);
257
+ const generates = config.api_addresses
258
+ .filter(({ generate }) => generate !== false)
259
+ .map((api) => limit(async () => {
260
+ const definition = api.definition || 'root';
261
+ spin.start(`正在生成:${definition}`);
262
+ await generateAPIFile(api).catch((error) => {
263
+ spin.stop(`⁉️ ${definition}`);
264
+ throw error;
265
+ });
266
+ spin.stop(`✅ ${definition}`);
267
+ }));
268
+ await Promise.all(generates);
269
+ p.outro('mymymytg 👏👏👏');
270
+ }
271
+ main().catch((error) => {
272
+ console.error('❌ 发生错误', error);
273
+ process.exit(1);
274
+ });
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@zhuh/oig",
3
+ "description": "从 OpenAPI/Swagger 规范自动生成 TypeScript API 客户端",
4
+ "keywords": [
5
+ "openapi",
6
+ "swagger",
7
+ "typescript",
8
+ "codegen",
9
+ "api",
10
+ "client",
11
+ "axios",
12
+ "orval",
13
+ "generator",
14
+ "cli"
15
+ ],
16
+ "private": false,
17
+ "version": "8.9.1",
18
+ "type": "module",
19
+ "bin": {
20
+ "@zhuh/oig": "dist/index.js"
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "scripts": {
26
+ "build": "rollup -c ./rollup.config.ts --configImportAttributesKey with",
27
+ "format": "biome check --write . --use-editorconfig=true"
28
+ },
29
+ "devDependencies": {
30
+ "@biomejs/biome": "^2.4.14",
31
+ "@rollup/plugin-commonjs": "^29.0.2",
32
+ "@rollup/plugin-node-resolve": "^16.0.3",
33
+ "@rollup/plugin-replace": "^6.0.3",
34
+ "@rollup/plugin-typescript": "^12.3.0",
35
+ "@types/node": "^25.6.0",
36
+ "axios": "^1.16.0",
37
+ "bumpp": "^11.1.0",
38
+ "openapi3-ts": "^4.5.0",
39
+ "rollup": "^4.60.3",
40
+ "tslib": "^2.8.1",
41
+ "typescript": "^5.9.3"
42
+ },
43
+ "dependencies": {
44
+ "@clack/prompts": "^1.3.0",
45
+ "@orval/core": "^8.9.1",
46
+ "@zhuh/orval": "8.9.1",
47
+ "del": "^8.0.1",
48
+ "execa": "^9.6.1",
49
+ "p-limit": "^7.3.0",
50
+ "picocolors": "^1.1.1"
51
+ },
52
+ "resolutions": {
53
+ "whatwg-url": "16.0.1"
54
+ },
55
+ "overrides": {
56
+ "whatwg-url": "16.0.1"
57
+ },
58
+ "pnpm": {
59
+ "onlyBuiltDependencies": [
60
+ "@biomejs/biome",
61
+ "esbuild"
62
+ ]
63
+ }
64
+ }