ts-proto-client 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 +196 -0
- package/dist/index.js +269 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# ts-proto-client
|
|
2
|
+
|
|
3
|
+
基于 Proto 文件自动生成 TypeScript 类型、请求接口及 Hooks 的 CLI 工具。
|
|
4
|
+
|
|
5
|
+
## 功能
|
|
6
|
+
|
|
7
|
+
- 自动解析 Proto 文件
|
|
8
|
+
- 生成 TypeScript 类型定义
|
|
9
|
+
- 生成 API 请求方法
|
|
10
|
+
- 生成 React Hooks(规划中)
|
|
11
|
+
- 支持远程 Proto 仓库同步(规划中)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 安装前准备
|
|
16
|
+
|
|
17
|
+
本工具依赖 `protoc`,请先安装 Protocol Buffers 编译器。
|
|
18
|
+
|
|
19
|
+
### macOS
|
|
20
|
+
|
|
21
|
+
安装:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
brew install protobuf
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
验证:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
protoc --version
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
输出示例:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
libprotoc 35.1
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
### Windows
|
|
42
|
+
|
|
43
|
+
1. 打开 Protocol Buffers Releases 页面:
|
|
44
|
+
|
|
45
|
+
https://github.com/protocolbuffers/protobuf/releases
|
|
46
|
+
|
|
47
|
+
2. 下载对应版本:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
protoc-35.1-win64.zip
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
3. 解压到目录,例如:
|
|
54
|
+
|
|
55
|
+
```text
|
|
56
|
+
D:\protoc
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
目录结构:
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
D:\protoc
|
|
63
|
+
├─ bin
|
|
64
|
+
│ └─ protoc.exe
|
|
65
|
+
├─ include
|
|
66
|
+
└─ readme.txt
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
4. 将以下目录添加到系统环境变量 `Path`:
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
D:\protoc\bin
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
5. 打开新的终端验证:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
protoc --version
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
输出示例:
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
libprotoc 35.1
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 使用方式
|
|
90
|
+
|
|
91
|
+
### 初始化配置
|
|
92
|
+
|
|
93
|
+
在项目根目录执行:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npx proto-gen init
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
执行后会生成配置文件:
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
proto.config.json
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
示例:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"ProtoRoot": "../protos",
|
|
110
|
+
"ProtoInput": "hospital/hospital.proto",
|
|
111
|
+
"ProtoTypeOutputPath": "./src/proto-type",
|
|
112
|
+
"ApiOutputPath": "./src/api"
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
### 生成 TypeScript 类型和接口
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
npx proto-gen generate
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
执行后将根据配置自动生成:
|
|
125
|
+
|
|
126
|
+
```text
|
|
127
|
+
src
|
|
128
|
+
├─ proto-type
|
|
129
|
+
│ └─ *.ts
|
|
130
|
+
└─ api
|
|
131
|
+
└─ *.ts
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 配置说明
|
|
137
|
+
|
|
138
|
+
| 字段 | 说明 |
|
|
139
|
+
|--------|--------|
|
|
140
|
+
| ProtoRoot | Proto 根目录 |
|
|
141
|
+
| ProtoInput | 入口 Proto 文件 |
|
|
142
|
+
| ProtoTypeOutputPath | TypeScript 类型输出目录 |
|
|
143
|
+
| ApiOutputPath | API 文件输出目录 |
|
|
144
|
+
|
|
145
|
+
示例:
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"ProtoRoot": "../protos",
|
|
150
|
+
"ProtoInput": "hospital/hospital.proto",
|
|
151
|
+
"ProtoTypeOutputPath": "./src/proto-type",
|
|
152
|
+
"ApiOutputPath": "./src/api"
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## 命令
|
|
159
|
+
|
|
160
|
+
### 初始化配置
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
npx proto-gen init
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### 生成代码
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
npx proto-gen generate
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## 目录示例
|
|
175
|
+
|
|
176
|
+
```text
|
|
177
|
+
project
|
|
178
|
+
├─ proto.config.json
|
|
179
|
+
├─ src
|
|
180
|
+
│ ├─ api
|
|
181
|
+
│ └─ proto-type
|
|
182
|
+
└─ protos
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## 版本要求
|
|
188
|
+
|
|
189
|
+
- Node.js >= 18
|
|
190
|
+
- Protocol Buffers (protoc) >= 35
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/init.ts
|
|
7
|
+
import prompts from "prompts";
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
var ConfigFilePath = "proto.config.json";
|
|
10
|
+
async function init() {
|
|
11
|
+
const answers = await prompts([
|
|
12
|
+
{
|
|
13
|
+
type: "text",
|
|
14
|
+
name: "ProtoRoot",
|
|
15
|
+
message: "proto \u8F93\u5165\u6587\u4EF6",
|
|
16
|
+
initial: "../protos"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
type: "text",
|
|
20
|
+
name: "ProtoInputPath",
|
|
21
|
+
message: "proto \u8F93\u5165\u6587\u4EF6",
|
|
22
|
+
initial: "hospital/hospital.proto"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
type: "text",
|
|
26
|
+
name: "ProtoTypeOutputPath",
|
|
27
|
+
message: "\u7C7B\u578B\u8F93\u51FA\u8DEF\u5F84",
|
|
28
|
+
initial: "dist/proto-type"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
type: "text",
|
|
32
|
+
name: "ProtoApiOutputPath",
|
|
33
|
+
message: "\u63A5\u53E3\u8F93\u51FA\u8DEF\u5F84",
|
|
34
|
+
initial: "dist/proto-api"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
type: "text",
|
|
38
|
+
name: "ImportTypePath",
|
|
39
|
+
message: "\u7C7B\u578B\u6587\u4EF6\u5BFC\u5165\u63A5\u53E3\u7684\u8DEF\u5F84",
|
|
40
|
+
initial: "../proto-type/hospital/hospital"
|
|
41
|
+
}
|
|
42
|
+
]);
|
|
43
|
+
const content = [
|
|
44
|
+
"{",
|
|
45
|
+
` "ProtoRoot": "${answers.ProtoRoot}",`,
|
|
46
|
+
` "ProtoInput": "${answers.ProtoInputPath}",`,
|
|
47
|
+
` "ProtoTypeOutputPath": "${answers.ProtoTypeOutputPath}",`,
|
|
48
|
+
` "ProtoApiOutputPath": "${answers.ProtoApiOutputPath}",`,
|
|
49
|
+
` "ImportTypePath": "${answers.ImportTypePath}"`,
|
|
50
|
+
"}"
|
|
51
|
+
].join("\n");
|
|
52
|
+
fs.writeFileSync(ConfigFilePath, content);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/commands/generate.ts
|
|
56
|
+
import fs5 from "fs";
|
|
57
|
+
import path4 from "path";
|
|
58
|
+
|
|
59
|
+
// src/generators/api.ts
|
|
60
|
+
import fs3 from "fs";
|
|
61
|
+
import path2 from "path";
|
|
62
|
+
|
|
63
|
+
// src/utils/index.ts
|
|
64
|
+
import { Root, Service } from "protobufjs";
|
|
65
|
+
import path from "path";
|
|
66
|
+
import fs2 from "fs";
|
|
67
|
+
|
|
68
|
+
// src/constants.ts
|
|
69
|
+
var HTTP_METHODS = [
|
|
70
|
+
"get",
|
|
71
|
+
"post",
|
|
72
|
+
"put",
|
|
73
|
+
"delete",
|
|
74
|
+
"patch"
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
// src/utils/index.ts
|
|
78
|
+
function createRoot(rootPath) {
|
|
79
|
+
const INCLUDE_PATHS = [
|
|
80
|
+
rootPath,
|
|
81
|
+
path.join(rootPath, "/third_party/googleapis")
|
|
82
|
+
];
|
|
83
|
+
const root = new Root();
|
|
84
|
+
root.resolvePath = (_, target) => {
|
|
85
|
+
for (const includePath of INCLUDE_PATHS) {
|
|
86
|
+
const filePath = path.join(
|
|
87
|
+
includePath,
|
|
88
|
+
target
|
|
89
|
+
);
|
|
90
|
+
if (fs2.existsSync(filePath)) {
|
|
91
|
+
return filePath;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return target;
|
|
95
|
+
};
|
|
96
|
+
return root;
|
|
97
|
+
}
|
|
98
|
+
function findServices(namespace, result = []) {
|
|
99
|
+
if (!namespace.nested) return result;
|
|
100
|
+
for (const item of Object.values(namespace.nested)) {
|
|
101
|
+
if (item instanceof Service) {
|
|
102
|
+
result.push(item);
|
|
103
|
+
}
|
|
104
|
+
findServices(item, result);
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
function getHttpInfo(method) {
|
|
109
|
+
const http = method.parsedOptions?.find(
|
|
110
|
+
(item) => item["(google.api.http)"]
|
|
111
|
+
)?.["(google.api.http)"];
|
|
112
|
+
if (!http) return null;
|
|
113
|
+
const methodName = HTTP_METHODS.find(
|
|
114
|
+
(key) => http[key]
|
|
115
|
+
);
|
|
116
|
+
if (!methodName) return null;
|
|
117
|
+
return {
|
|
118
|
+
method: methodName,
|
|
119
|
+
url: http[methodName],
|
|
120
|
+
summary: method.options?.["(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation).summary"]
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function parseProto(protosPath, protoFilePath) {
|
|
124
|
+
const rootPath = path.resolve(process.cwd(), protosPath);
|
|
125
|
+
const root = createRoot(rootPath);
|
|
126
|
+
root.loadSync(path.join(rootPath, protoFilePath));
|
|
127
|
+
const services = findServices(root);
|
|
128
|
+
const apis = {};
|
|
129
|
+
for (const service of services) {
|
|
130
|
+
const api = [];
|
|
131
|
+
for (const method of Object.values(service.methods)) {
|
|
132
|
+
const httpInfo = getHttpInfo(method);
|
|
133
|
+
if (!httpInfo) continue;
|
|
134
|
+
api.push({
|
|
135
|
+
name: method.name,
|
|
136
|
+
requestType: method.requestType,
|
|
137
|
+
responseType: method.responseType,
|
|
138
|
+
...httpInfo
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
apis[service.name] = api;
|
|
142
|
+
}
|
|
143
|
+
return apis;
|
|
144
|
+
}
|
|
145
|
+
function generateApiCode(apis, importTypePath) {
|
|
146
|
+
const modules = [];
|
|
147
|
+
for (const [key, api] of Object.entries(apis)) {
|
|
148
|
+
const types = /* @__PURE__ */ new Set();
|
|
149
|
+
const functions = [];
|
|
150
|
+
for (const data of api) {
|
|
151
|
+
types.add(data.requestType);
|
|
152
|
+
types.add(data.responseType);
|
|
153
|
+
const requestField = data.method === "get" ? "params: data" : "data";
|
|
154
|
+
const lines = [];
|
|
155
|
+
if (data.summary) lines.push(`// ${data.summary}`);
|
|
156
|
+
lines.push(
|
|
157
|
+
`export function ${data.name}(data: ${data.requestType}) {`,
|
|
158
|
+
` return service<${data.responseType}>({`,
|
|
159
|
+
` url: "${data.url.replace("/api", "")}",`,
|
|
160
|
+
` method: "${data.method}",`,
|
|
161
|
+
` ${requestField}`,
|
|
162
|
+
` })`,
|
|
163
|
+
`}`
|
|
164
|
+
);
|
|
165
|
+
functions.push(
|
|
166
|
+
lines.join("\n")
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
const imports = [...types].sort();
|
|
170
|
+
modules.push({
|
|
171
|
+
[key]: [
|
|
172
|
+
`import { service } from "@/plugins";`,
|
|
173
|
+
"",
|
|
174
|
+
"import type {",
|
|
175
|
+
` ${imports.join(",\n ")}`,
|
|
176
|
+
`} from "${importTypePath}";`,
|
|
177
|
+
"",
|
|
178
|
+
functions.join("\n\n")
|
|
179
|
+
].join("\n")
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return modules;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/generators/api.ts
|
|
186
|
+
function generateApi() {
|
|
187
|
+
const config = JSON.parse(fs3.readFileSync("proto.config.json", "utf8"));
|
|
188
|
+
const outputPath = path2.resolve(process.cwd(), config.ProtoApiOutputPath);
|
|
189
|
+
if (!fs3.existsSync(outputPath)) {
|
|
190
|
+
fs3.mkdirSync(outputPath, { recursive: true });
|
|
191
|
+
}
|
|
192
|
+
const apis = parseProto(config.ProtoRoot, config.ProtoInput);
|
|
193
|
+
const codes = generateApiCode(apis, config.ImportTypePath);
|
|
194
|
+
for (const code of codes) {
|
|
195
|
+
for (const [name, content] of Object.entries(code)) {
|
|
196
|
+
fs3.writeFileSync(`${outputPath}/${name}.ts`, content, "utf8");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
console.log("\u2705 Generate Success");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// src/generators/type.ts
|
|
203
|
+
import { execSync } from "child_process";
|
|
204
|
+
import { createRequire } from "module";
|
|
205
|
+
import fs4 from "fs";
|
|
206
|
+
import path3 from "path";
|
|
207
|
+
function generateTypes() {
|
|
208
|
+
const require2 = createRequire(import.meta.url);
|
|
209
|
+
const tsProtoPkg = require2.resolve("ts-proto/package.json");
|
|
210
|
+
const tsProtoRoot = path3.dirname(tsProtoPkg);
|
|
211
|
+
const pluginName = process.platform === "win32" ? "protoc-gen-ts_proto.cmd" : "protoc-gen-ts_proto";
|
|
212
|
+
const pluginPath = path3.resolve(tsProtoRoot, `./node_modules/.bin/${pluginName}`);
|
|
213
|
+
const config = JSON.parse(fs4.readFileSync("proto.config.json", "utf8"));
|
|
214
|
+
const outputPath = path3.resolve(process.cwd(), config.ProtoTypeOutputPath);
|
|
215
|
+
if (!fs4.existsSync(outputPath)) {
|
|
216
|
+
fs4.mkdirSync(outputPath, { recursive: true });
|
|
217
|
+
}
|
|
218
|
+
const rootPath = path3.resolve(process.cwd(), config.ProtoRoot);
|
|
219
|
+
const configs = [
|
|
220
|
+
"protoc",
|
|
221
|
+
`--plugin=protoc-gen-ts_proto=${pluginPath}`,
|
|
222
|
+
`--ts_proto_out=${outputPath}`,
|
|
223
|
+
"--ts_proto_opt=onlyTypes=true",
|
|
224
|
+
// 仅生成 TypeScript
|
|
225
|
+
"--ts_proto_opt=useOptionals=messages",
|
|
226
|
+
// message 字段生成可选属性
|
|
227
|
+
"--ts_proto_opt=esModuleInterop=true",
|
|
228
|
+
// 使用 ES Module 风格导入
|
|
229
|
+
"--ts_proto_opt=comments=false",
|
|
230
|
+
// 去掉注释
|
|
231
|
+
`--proto_path=${rootPath}`,
|
|
232
|
+
`--proto_path=${path3.join(rootPath, "/third_party/googleapis")}`,
|
|
233
|
+
// protos 中的 google 依赖
|
|
234
|
+
path3.join(rootPath, config.ProtoInput)
|
|
235
|
+
];
|
|
236
|
+
execSync(configs.join(" "), { stdio: "inherit" });
|
|
237
|
+
fs4.rmSync(
|
|
238
|
+
path3.join(outputPath, "google"),
|
|
239
|
+
{ recursive: true, force: true }
|
|
240
|
+
);
|
|
241
|
+
fs4.rmSync(
|
|
242
|
+
path3.join(outputPath, "common.ts"),
|
|
243
|
+
{ force: true }
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/commands/generate.ts
|
|
248
|
+
async function generate() {
|
|
249
|
+
if (!fs5.existsSync("proto.config.json")) {
|
|
250
|
+
console.log("\u8BF7\u521D\u59CB\u5316\u914D\u7F6E");
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const config = JSON.parse(
|
|
254
|
+
fs5.readFileSync("proto.config.json", "utf8")
|
|
255
|
+
);
|
|
256
|
+
if (!fs5.existsSync(path4.resolve(process.cwd(), config.ProtoRoot))) {
|
|
257
|
+
console.log(`proto \u9879\u76EE\u4E0D\u5B58\u5728(${config.ProtoRoot})`);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
generateTypes();
|
|
261
|
+
generateApi();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/index.ts
|
|
265
|
+
var program = new Command();
|
|
266
|
+
program.name("proto-gen").description("proto code generator").version("1.0.0");
|
|
267
|
+
program.command("init").action(init);
|
|
268
|
+
program.command("generate").action(generate);
|
|
269
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ts-proto-client",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "tsx src/index.ts",
|
|
8
|
+
"build": "tsup src/index.ts --format esm --clean"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"proto-gen": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"keywords": [],
|
|
17
|
+
"author": "",
|
|
18
|
+
"license": "ISC",
|
|
19
|
+
"packageManager": "pnpm@10.14.0",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"commander": "^15.0.0",
|
|
22
|
+
"prompts": "^2.4.2",
|
|
23
|
+
"protobufjs": "^8.6.3",
|
|
24
|
+
"ts-proto": "^2.11.8"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^25.9.3",
|
|
28
|
+
"@types/prompts": "^2.4.9",
|
|
29
|
+
"tsup": "^8.5.1",
|
|
30
|
+
"tsx": "^4.22.4",
|
|
31
|
+
"typescript": "^6.0.3"
|
|
32
|
+
}
|
|
33
|
+
}
|