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.
Files changed (3) hide show
  1. package/README.md +196 -0
  2. package/dist/index.js +269 -0
  3. 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
+ }