czh-api 1.0.0
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.
- package/README.md +104 -0
- package/dist/commands/bill.js +1 -0
- package/dist/commands/build.js +129 -0
- package/dist/commands/init.js +66 -0
- package/dist/core/parser.js +315 -0
- package/dist/index.js +37 -0
- package/dist/templates/api.hbs +23 -0
- package/dist/templates/index.hbs +3 -0
- package/dist/templates/types.hbs +1 -0
- package/package.json +40 -0
- package/scripts/postbuild.js +49 -0
- package/src/commands/bill.ts +1 -0
- package/src/commands/build.ts +148 -0
- package/src/commands/init.ts +58 -0
- package/src/core/parser.ts +363 -0
- package/src/index.ts +39 -0
- package/src/templates/api.hbs +23 -0
- package/src/templates/index.hbs +3 -0
- package/src/templates/types.hbs +1 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description {{description}}
|
|
3
|
+
{{#if jsdocParams}}
|
|
4
|
+
{{#each jsdocParams}}
|
|
5
|
+
* @param { {{this.type}} } {{#unless this.required}}[{{/unless}}{{this.name}}{{#unless this.required}}]{{/unless}} - {{this.description}}
|
|
6
|
+
{{/each}}
|
|
7
|
+
{{/if}}
|
|
8
|
+
*/
|
|
9
|
+
export const {{functionName}} = ({{#if hasParams}}params: {{requestParamsTypeName}}{{/if}}{{#if hasData}}{{#if hasParams}}, {{/if}}data: {{requestBodyTypeName}}{{/if}}) => {
|
|
10
|
+
return http.request<{{responseTypeName}}>({
|
|
11
|
+
url: `{{path}}`,
|
|
12
|
+
method: '{{method}}',
|
|
13
|
+
{{#if hasParams}}
|
|
14
|
+
params,
|
|
15
|
+
{{/if}}
|
|
16
|
+
{{#if hasData}}
|
|
17
|
+
data,
|
|
18
|
+
{{/if}}
|
|
19
|
+
{{#if contentType}}
|
|
20
|
+
headers: { 'Content-Type': '{{contentType}}' },
|
|
21
|
+
{{/if}}
|
|
22
|
+
});
|
|
23
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{{{content}}}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "czh-api",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A CLI tool to generate TypeScript API clients from Swagger/OpenAPI documents.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"czh-api": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "rimraf dist && tsc && node scripts/postbuild.js",
|
|
11
|
+
"dev": "ts-node src/index.ts",
|
|
12
|
+
"prepublishOnly": "npm run build",
|
|
13
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"api",
|
|
17
|
+
"typescript"
|
|
18
|
+
],
|
|
19
|
+
"author": "Czh",
|
|
20
|
+
"license": "ISC",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@types/pinyin": "^2.10.2",
|
|
23
|
+
"axios": "^1.10.0",
|
|
24
|
+
"chalk": "^4.1.2",
|
|
25
|
+
"commander": "^14.0.0",
|
|
26
|
+
"handlebars": "^4.7.8",
|
|
27
|
+
"json-schema-to-typescript": "^15.0.4",
|
|
28
|
+
"pinyin": "^4.0.0",
|
|
29
|
+
"rimraf": "^6.0.1",
|
|
30
|
+
"swagger-parser": "^10.0.3",
|
|
31
|
+
"typescript": "^5.8.3"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/handlebars": "^4.0.40",
|
|
35
|
+
"@types/node": "^24.0.10",
|
|
36
|
+
"@types/swagger-parser": "^4.0.3",
|
|
37
|
+
"openapi-types": "^12.1.3",
|
|
38
|
+
"ts-node": "^10.9.2"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @description:
|
|
3
|
+
* @Author: czh
|
|
4
|
+
* @Date: 2025-07-02 14:16:56
|
|
5
|
+
* @LastEditors: Czh
|
|
6
|
+
* @LastEditTime: 2025-07-02 15:12:03
|
|
7
|
+
*/
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const filePath = path.join(__dirname, '../dist/index.js');
|
|
12
|
+
const shebang = '#!/usr/bin/env node\n';
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
let data = fs.readFileSync(filePath, 'utf8');
|
|
16
|
+
|
|
17
|
+
if (!data.startsWith(shebang)) {
|
|
18
|
+
data = shebang + data;
|
|
19
|
+
fs.writeFileSync(filePath, data, 'utf8');
|
|
20
|
+
console.log('Shebang added to dist/index.js');
|
|
21
|
+
} else {
|
|
22
|
+
console.log('Shebang already exists in dist/index.js');
|
|
23
|
+
}
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error('Error adding shebang:', error);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const sourceDir = path.join(__dirname, '../src/templates');
|
|
30
|
+
const destDir = path.join(__dirname, '../dist/templates');
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
if (!fs.existsSync(destDir)) {
|
|
34
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const templateFiles = fs.readdirSync(sourceDir);
|
|
38
|
+
|
|
39
|
+
for (const file of templateFiles) {
|
|
40
|
+
const srcFile = path.join(sourceDir, file);
|
|
41
|
+
const destFile = path.join(destDir, file);
|
|
42
|
+
fs.copyFileSync(srcFile, destFile);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log('Successfully copied templates to dist/templates');
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error('Error copying templates:', error);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @description:
|
|
3
|
+
* @Author: czh
|
|
4
|
+
* @Date: 2025-07-02 10:39:30
|
|
5
|
+
* @LastEditors: Czh
|
|
6
|
+
* @LastEditTime: 2025-07-02 11:47:00
|
|
7
|
+
*/
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
const SwaggerParser = require('swagger-parser');
|
|
12
|
+
import { OpenAPI, OpenAPIV3 } from 'openapi-types';
|
|
13
|
+
import { processApi } from '../core/parser';
|
|
14
|
+
import Handlebars from 'handlebars';
|
|
15
|
+
import { rimraf } from 'rimraf';
|
|
16
|
+
import axios from 'axios';
|
|
17
|
+
import { compile } from 'json-schema-to-typescript';
|
|
18
|
+
|
|
19
|
+
interface IConfig {
|
|
20
|
+
url: string;
|
|
21
|
+
outputDir: string;
|
|
22
|
+
httpClientPath: string;
|
|
23
|
+
templates?: string;
|
|
24
|
+
customImports?: string[];
|
|
25
|
+
excludePaths?: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const handleBuild = async () => {
|
|
29
|
+
try {
|
|
30
|
+
console.log(chalk.blue('Building API from source...'));
|
|
31
|
+
|
|
32
|
+
// Load config
|
|
33
|
+
const configPath = path.join(process.cwd(), 'czh-api.config.json');
|
|
34
|
+
if (!fs.existsSync(configPath)) {
|
|
35
|
+
console.error(chalk.red('错误:找不到 czh-api.config.json 配置文件。请先运行 `czh-api init`。'));
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
const config:IConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
39
|
+
|
|
40
|
+
// Fetch and parse Swagger/OpenAPI document
|
|
41
|
+
console.log(chalk.blue(`正在从 ${config.url} 获取 API 文档...`));
|
|
42
|
+
const response = await axios.get(config.url);
|
|
43
|
+
const api = await SwaggerParser.bundle(response.data);
|
|
44
|
+
console.log(chalk.green('Swagger document fetched and bundled successfully.'));
|
|
45
|
+
|
|
46
|
+
// Process the API document
|
|
47
|
+
const modules = processApi(api, config.excludePaths || []);
|
|
48
|
+
console.log(chalk.blue('API processed into modules.'));
|
|
49
|
+
|
|
50
|
+
// Clean output directory
|
|
51
|
+
const outputDir = path.join(process.cwd(), config.outputDir);
|
|
52
|
+
if (fs.existsSync(outputDir)) {
|
|
53
|
+
await rimraf(outputDir);
|
|
54
|
+
}
|
|
55
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
56
|
+
console.log(chalk.yellow(`Cleaned output directory: ${outputDir}`));
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
const readTemplate = (templatePath: string | undefined, templateName: string): string => {
|
|
60
|
+
const defaultPath = path.resolve(__dirname, '../templates', templateName);
|
|
61
|
+
const userPath = templatePath ? path.join(process.cwd(), templatePath, templateName) : defaultPath;
|
|
62
|
+
try {
|
|
63
|
+
return fs.readFileSync(userPath, 'utf-8');
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return fs.readFileSync(defaultPath, 'utf-8');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const apiTemplateStr = readTemplate(config.templates, 'api.hbs');
|
|
70
|
+
const apiTemplate = Handlebars.compile(apiTemplateStr);
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
for (const moduleName in modules) {
|
|
74
|
+
const module = modules[moduleName];
|
|
75
|
+
const moduleDir = path.join(outputDir, moduleName);
|
|
76
|
+
fs.mkdirSync(moduleDir, { recursive: true });
|
|
77
|
+
|
|
78
|
+
let typesContent = '';
|
|
79
|
+
// Generate types.ts
|
|
80
|
+
if (Object.keys(module.schemas).length > 0) {
|
|
81
|
+
const schemasWithTitles: { [key: string]: OpenAPIV3.SchemaObject } = {};
|
|
82
|
+
for (const schemaName in module.schemas) {
|
|
83
|
+
schemasWithTitles[schemaName] = {
|
|
84
|
+
...module.schemas[schemaName],
|
|
85
|
+
title: schemaName,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const rootSchemaForCompiler = {
|
|
90
|
+
title: 'schemas',
|
|
91
|
+
type: 'object',
|
|
92
|
+
properties: {},
|
|
93
|
+
additionalProperties: false,
|
|
94
|
+
definitions: schemasWithTitles,
|
|
95
|
+
components: {
|
|
96
|
+
schemas: schemasWithTitles
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
typesContent = await compile(rootSchemaForCompiler as any, 'schemas', {
|
|
101
|
+
bannerComment: '/* eslint-disable */\n/**\n* This file was automatically generated by czh-api.\n* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,\n* and run czh-api build to regenerate this file.\n*/',
|
|
102
|
+
unreachableDefinitions: true,
|
|
103
|
+
additionalProperties: false,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
typesContent = typesContent.replace(/export interface \w+ \{\s*\}\n/g, '');
|
|
107
|
+
const refCommentRegex = /\/\*\*\n \* This interface was referenced by `\w+`'s JSON-Schema[\s\S]*?\*\/\n/g;
|
|
108
|
+
typesContent = typesContent.replace(refCommentRegex, '');
|
|
109
|
+
typesContent = typesContent.replace(/:\s*\{\}\[\]/g, ': any[]');
|
|
110
|
+
typesContent = typesContent.replace(/\[k: string\]: \{\}/g, '[k: string]: any');
|
|
111
|
+
const inlineIndexSignatureRegex = /:\s*\{\s*\/\*\*[\s\S]*?\*\/[\s\n\r]*\[k: string\]: any;\s*\};/g;
|
|
112
|
+
typesContent = typesContent.replace(inlineIndexSignatureRegex, ': any;');
|
|
113
|
+
typesContent = typesContent.replace(/: \{\}/g, ': any');
|
|
114
|
+
typesContent = typesContent.replace(/\}\n/g, '}\n\n');
|
|
115
|
+
|
|
116
|
+
fs.writeFileSync(path.join(moduleDir, 'types.ts'), typesContent);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Generate API file
|
|
120
|
+
const allReferencedTypes = [...new Set(module.endpoints.flatMap(e => e.referencedTypes))];
|
|
121
|
+
const customImports = config.customImports || [`import http from "${config.httpClientPath}";`];
|
|
122
|
+
|
|
123
|
+
let apiFileContent = '';
|
|
124
|
+
|
|
125
|
+
if (module.description) {
|
|
126
|
+
apiFileContent += `/**\n * @description ${module.description}\n */\n\n`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
apiFileContent += `${customImports.join('\n')}\n`;
|
|
130
|
+
|
|
131
|
+
if (allReferencedTypes.length > 0) {
|
|
132
|
+
apiFileContent += `import type { ${allReferencedTypes.join(', ')} } from './types';\n\n`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
apiFileContent += module.endpoints.map(endpoint => apiTemplate(endpoint)).join('\n\n');
|
|
136
|
+
fs.writeFileSync(path.join(moduleDir, `${moduleName}.ts`), apiFileContent);
|
|
137
|
+
|
|
138
|
+
// Generate index.ts
|
|
139
|
+
const exportTypes = typesContent ? `\nexport * from './types';` : '';
|
|
140
|
+
fs.writeFileSync(path.join(moduleDir, 'index.ts'), `export * from './${moduleName}';${exportTypes}\n`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
console.log(chalk.green.bold('API code generated successfully!'));
|
|
144
|
+
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error(chalk.red('An error occurred during build process:'), error);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @description:
|
|
3
|
+
* @Author: czh
|
|
4
|
+
* @Date: 2025-07-02 10:39:17
|
|
5
|
+
* @LastEditors: Czh
|
|
6
|
+
* @LastEditTime: 2025-07-02 11:45:10
|
|
7
|
+
*/
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import fs from 'fs/promises';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
|
|
12
|
+
const defaultConfig = {
|
|
13
|
+
url: 'https://petstore.swagger.io/v2/swagger.json',
|
|
14
|
+
outputDir: './src/api',
|
|
15
|
+
templates: './czh-api-template',
|
|
16
|
+
customImports: ['import http from "@/utils/http";'],
|
|
17
|
+
excludePaths: []
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const templates = ['api.hbs', 'index.hbs', 'types.hbs'];
|
|
21
|
+
|
|
22
|
+
export const handleInit = async () => {
|
|
23
|
+
console.log(chalk.green('Initializing czh-api...'));
|
|
24
|
+
|
|
25
|
+
const configPath = path.join(process.cwd(), 'czh-api.config.json');
|
|
26
|
+
const templatesPath = path.join(process.cwd(), 'czh-api-template');
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
await fs.access(configPath);
|
|
30
|
+
console.log(chalk.yellow('Config file already exists. Aborting.'));
|
|
31
|
+
return;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
// File doesn't exist, which is what we want.
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
// Write config file
|
|
38
|
+
await fs.writeFile(configPath, JSON.stringify(defaultConfig, null, 2));
|
|
39
|
+
console.log(chalk.cyan(`Created config file: ${configPath}`));
|
|
40
|
+
|
|
41
|
+
// Create templates directory
|
|
42
|
+
await fs.mkdir(templatesPath, { recursive: true });
|
|
43
|
+
|
|
44
|
+
// Copy template files
|
|
45
|
+
for (const templateName of templates) {
|
|
46
|
+
const sourcePath = path.resolve(__dirname, '../templates', templateName);
|
|
47
|
+
const destPath = path.join(templatesPath, templateName);
|
|
48
|
+
await fs.copyFile(sourcePath, destPath);
|
|
49
|
+
console.log(chalk.cyan(`Created template: ${destPath}`));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log(chalk.green.bold('Initialization complete!'));
|
|
53
|
+
console.log(chalk.white('You can now edit the config and templates, then run `czh-api build`.'));
|
|
54
|
+
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error(chalk.red('An error occurred during initialization:'), error);
|
|
57
|
+
}
|
|
58
|
+
};
|