@trevrpc/trevrpc-js 0.1.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/LICENSE +21 -0
- package/README.md +202 -0
- package/bin/protoc-gen-trevrpc-js.js +17 -0
- package/examples/greeter/README.md +35 -0
- package/examples/greeter/client.js +172 -0
- package/examples/greeter/greeter.node.trevrpc.d.ts +53 -0
- package/examples/greeter/greeter.node.trevrpc.js +9 -0
- package/examples/greeter/greeter.proto +18 -0
- package/examples/greeter/greeter.trevrpc.d.ts +55 -0
- package/examples/greeter/greeter.trevrpc.js +103 -0
- package/examples/greeter/index.html +140 -0
- package/examples/greeter/static-server.js +168 -0
- package/package.json +89 -0
- package/src/advanced.d.ts +29 -0
- package/src/advanced.js +1 -0
- package/src/browser-channel.js +45 -0
- package/src/channel.js +375 -0
- package/src/client.js +1397 -0
- package/src/connect.js +6 -0
- package/src/connect.node.js +6 -0
- package/src/framing.js +618 -0
- package/src/generator.js +868 -0
- package/src/index.d.ts +577 -0
- package/src/index.js +3 -0
- package/src/index.node.d.ts +10 -0
- package/src/index.node.js +3 -0
- package/src/metadata.js +112 -0
- package/src/native-loader.js +94 -0
- package/src/node-advanced.d.ts +27 -0
- package/src/node-advanced.js +1 -0
- package/src/node-generated.d.ts +24 -0
- package/src/node-generated.js +126 -0
- package/src/node-index.d.ts +1 -0
- package/src/node-index.js +7 -0
- package/src/node.d.ts +298 -0
- package/src/node.js +1456 -0
- package/src/runtime.js +66 -0
- package/src/server-response.js +49 -0
- package/src/status.js +145 -0
- package/src/timer.js +29 -0
- package/src/webtransport.js +624 -0
- package/src/wire.js +403 -0
package/src/generator.js
ADDED
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
import protobuf from "protobufjs";
|
|
2
|
+
|
|
3
|
+
const DESCRIPTOR_PROTO = String.raw`
|
|
4
|
+
syntax = "proto2";
|
|
5
|
+
|
|
6
|
+
package google.protobuf;
|
|
7
|
+
|
|
8
|
+
message FileDescriptorProto {
|
|
9
|
+
optional string name = 1;
|
|
10
|
+
optional string package = 2;
|
|
11
|
+
repeated string dependency = 3;
|
|
12
|
+
repeated DescriptorProto message_type = 4;
|
|
13
|
+
repeated EnumDescriptorProto enum_type = 5;
|
|
14
|
+
repeated ServiceDescriptorProto service = 6;
|
|
15
|
+
optional FileOptions options = 8;
|
|
16
|
+
optional string syntax = 12;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
message DescriptorProto {
|
|
20
|
+
optional string name = 1;
|
|
21
|
+
repeated FieldDescriptorProto field = 2;
|
|
22
|
+
repeated DescriptorProto nested_type = 3;
|
|
23
|
+
repeated EnumDescriptorProto enum_type = 4;
|
|
24
|
+
optional MessageOptions options = 7;
|
|
25
|
+
repeated OneofDescriptorProto oneof_decl = 8;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
message FieldDescriptorProto {
|
|
29
|
+
enum Type {
|
|
30
|
+
TYPE_DOUBLE = 1;
|
|
31
|
+
TYPE_FLOAT = 2;
|
|
32
|
+
TYPE_INT64 = 3;
|
|
33
|
+
TYPE_UINT64 = 4;
|
|
34
|
+
TYPE_INT32 = 5;
|
|
35
|
+
TYPE_FIXED64 = 6;
|
|
36
|
+
TYPE_FIXED32 = 7;
|
|
37
|
+
TYPE_BOOL = 8;
|
|
38
|
+
TYPE_STRING = 9;
|
|
39
|
+
TYPE_GROUP = 10;
|
|
40
|
+
TYPE_MESSAGE = 11;
|
|
41
|
+
TYPE_BYTES = 12;
|
|
42
|
+
TYPE_UINT32 = 13;
|
|
43
|
+
TYPE_ENUM = 14;
|
|
44
|
+
TYPE_SFIXED32 = 15;
|
|
45
|
+
TYPE_SFIXED64 = 16;
|
|
46
|
+
TYPE_SINT32 = 17;
|
|
47
|
+
TYPE_SINT64 = 18;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
enum Label {
|
|
51
|
+
LABEL_OPTIONAL = 1;
|
|
52
|
+
LABEL_REQUIRED = 2;
|
|
53
|
+
LABEL_REPEATED = 3;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
optional string name = 1;
|
|
57
|
+
optional int32 number = 3;
|
|
58
|
+
optional Label label = 4;
|
|
59
|
+
optional Type type = 5;
|
|
60
|
+
optional string type_name = 6;
|
|
61
|
+
optional string default_value = 7;
|
|
62
|
+
optional int32 oneof_index = 9;
|
|
63
|
+
optional string json_name = 10;
|
|
64
|
+
optional bool proto3_optional = 17;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
message OneofDescriptorProto {
|
|
68
|
+
optional string name = 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
message EnumDescriptorProto {
|
|
72
|
+
optional string name = 1;
|
|
73
|
+
repeated EnumValueDescriptorProto value = 2;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
message EnumValueDescriptorProto {
|
|
77
|
+
optional string name = 1;
|
|
78
|
+
optional int32 number = 2;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
message ServiceDescriptorProto {
|
|
82
|
+
optional string name = 1;
|
|
83
|
+
repeated MethodDescriptorProto method = 2;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
message MethodDescriptorProto {
|
|
87
|
+
optional string name = 1;
|
|
88
|
+
optional string input_type = 2;
|
|
89
|
+
optional string output_type = 3;
|
|
90
|
+
optional bool client_streaming = 5 [default = false];
|
|
91
|
+
optional bool server_streaming = 6 [default = false];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
message FileOptions {
|
|
95
|
+
optional string java_package = 1;
|
|
96
|
+
optional string java_outer_classname = 8;
|
|
97
|
+
optional bool java_multiple_files = 10;
|
|
98
|
+
optional string go_package = 11;
|
|
99
|
+
optional string objc_class_prefix = 36;
|
|
100
|
+
optional string csharp_namespace = 37;
|
|
101
|
+
optional string php_namespace = 41;
|
|
102
|
+
optional string ruby_package = 45;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
message MessageOptions {
|
|
106
|
+
optional bool map_entry = 7;
|
|
107
|
+
}
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
const PLUGIN_PROTO = String.raw`
|
|
111
|
+
syntax = "proto2";
|
|
112
|
+
|
|
113
|
+
package google.protobuf.compiler;
|
|
114
|
+
|
|
115
|
+
message CodeGeneratorRequest {
|
|
116
|
+
repeated string file_to_generate = 1;
|
|
117
|
+
optional string parameter = 2;
|
|
118
|
+
repeated .google.protobuf.FileDescriptorProto proto_file = 15;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
message CodeGeneratorResponse {
|
|
122
|
+
optional string error = 1;
|
|
123
|
+
optional uint64 supported_features = 2;
|
|
124
|
+
repeated File file = 15;
|
|
125
|
+
|
|
126
|
+
message File {
|
|
127
|
+
optional string name = 1;
|
|
128
|
+
optional string insertion_point = 2;
|
|
129
|
+
optional string content = 15;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
`;
|
|
133
|
+
|
|
134
|
+
const pluginRoot = new protobuf.Root();
|
|
135
|
+
protobuf.parse(DESCRIPTOR_PROTO, pluginRoot);
|
|
136
|
+
protobuf.parse(PLUGIN_PROTO, pluginRoot);
|
|
137
|
+
const CodeGeneratorRequest = pluginRoot.lookupType("google.protobuf.compiler.CodeGeneratorRequest");
|
|
138
|
+
const CodeGeneratorResponse = pluginRoot.lookupType(
|
|
139
|
+
"google.protobuf.compiler.CodeGeneratorResponse",
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const FieldType = Object.freeze({
|
|
143
|
+
Double: 1,
|
|
144
|
+
Float: 2,
|
|
145
|
+
Int64: 3,
|
|
146
|
+
Uint64: 4,
|
|
147
|
+
Int32: 5,
|
|
148
|
+
Fixed64: 6,
|
|
149
|
+
Fixed32: 7,
|
|
150
|
+
Bool: 8,
|
|
151
|
+
String: 9,
|
|
152
|
+
Group: 10,
|
|
153
|
+
Message: 11,
|
|
154
|
+
Bytes: 12,
|
|
155
|
+
Uint32: 13,
|
|
156
|
+
Enum: 14,
|
|
157
|
+
Sfixed32: 15,
|
|
158
|
+
Sfixed64: 16,
|
|
159
|
+
Sint32: 17,
|
|
160
|
+
Sint64: 18,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const FieldLabel = Object.freeze({
|
|
164
|
+
Optional: 1,
|
|
165
|
+
Required: 2,
|
|
166
|
+
Repeated: 3,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const RESERVED_WORDS = new Set([
|
|
170
|
+
"break",
|
|
171
|
+
"case",
|
|
172
|
+
"catch",
|
|
173
|
+
"class",
|
|
174
|
+
"const",
|
|
175
|
+
"continue",
|
|
176
|
+
"debugger",
|
|
177
|
+
"default",
|
|
178
|
+
"delete",
|
|
179
|
+
"do",
|
|
180
|
+
"else",
|
|
181
|
+
"export",
|
|
182
|
+
"extends",
|
|
183
|
+
"finally",
|
|
184
|
+
"for",
|
|
185
|
+
"function",
|
|
186
|
+
"if",
|
|
187
|
+
"import",
|
|
188
|
+
"in",
|
|
189
|
+
"instanceof",
|
|
190
|
+
"new",
|
|
191
|
+
"return",
|
|
192
|
+
"super",
|
|
193
|
+
"switch",
|
|
194
|
+
"this",
|
|
195
|
+
"throw",
|
|
196
|
+
"try",
|
|
197
|
+
"typeof",
|
|
198
|
+
"var",
|
|
199
|
+
"void",
|
|
200
|
+
"while",
|
|
201
|
+
"with",
|
|
202
|
+
"yield",
|
|
203
|
+
]);
|
|
204
|
+
|
|
205
|
+
/** Runs the TrevRPC JavaScript code generator for protoc plugin input. */
|
|
206
|
+
export function runGenerator(input) {
|
|
207
|
+
const request = CodeGeneratorRequest.decode(input);
|
|
208
|
+
const response = generate(request);
|
|
209
|
+
return CodeGeneratorResponse.encode(response).finish();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Generates a protoc code generator response. */
|
|
213
|
+
export function generate(request) {
|
|
214
|
+
try {
|
|
215
|
+
const options = parseOptions(request.parameter ?? "");
|
|
216
|
+
const filesToGenerate = new Set(request.fileToGenerate ?? []);
|
|
217
|
+
const protoFiles = request.protoFile ?? [];
|
|
218
|
+
const rootJson = buildProtobufJson(protoFiles);
|
|
219
|
+
const typeNames = declarationTypeNames(protoFiles);
|
|
220
|
+
const files = [];
|
|
221
|
+
|
|
222
|
+
for (const file of protoFiles) {
|
|
223
|
+
if (!filesToGenerate.has(file.name) || (file.service ?? []).length === 0) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const fileName = outputFileName(file.name, options.fileSuffix);
|
|
228
|
+
const nodeFileName = outputFileName(file.name, `.node${options.fileSuffix}`);
|
|
229
|
+
files.push({
|
|
230
|
+
name: fileName,
|
|
231
|
+
content: generateFile(file, rootJson, options),
|
|
232
|
+
});
|
|
233
|
+
files.push({
|
|
234
|
+
name: declarationFileName(fileName),
|
|
235
|
+
content: generateDeclarationFile(protoFiles, file, typeNames, options),
|
|
236
|
+
});
|
|
237
|
+
files.push({
|
|
238
|
+
name: nodeFileName,
|
|
239
|
+
content: generateNodeFile(file, fileName, options),
|
|
240
|
+
});
|
|
241
|
+
files.push({
|
|
242
|
+
name: declarationFileName(nodeFileName),
|
|
243
|
+
content: generateNodeDeclarationFile(file, fileName, typeNames, options),
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return { file: files };
|
|
248
|
+
} catch (error) {
|
|
249
|
+
return { error: error.message };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Parses protoc plugin options for the JavaScript generator. */
|
|
254
|
+
export function parseOptions(parameter) {
|
|
255
|
+
const options = {
|
|
256
|
+
runtimeImport: "@trevrpc/trevrpc-js",
|
|
257
|
+
runtimeTypeImport: null,
|
|
258
|
+
nodeRuntimeImport: "@trevrpc/trevrpc-js/node/generated",
|
|
259
|
+
nodeRuntimeTypeImport: null,
|
|
260
|
+
fileSuffix: ".trevrpc.js",
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
for (const option of parameter.split(",")) {
|
|
264
|
+
if (option === "") {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const separator = option.indexOf("=");
|
|
269
|
+
if (separator === -1) {
|
|
270
|
+
throw new Error(`invalid trevrpc-js option ${JSON.stringify(option)}; expected key=value`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const key = option.slice(0, separator);
|
|
274
|
+
const value = option.slice(separator + 1);
|
|
275
|
+
switch (key) {
|
|
276
|
+
case "runtime_import":
|
|
277
|
+
options.runtimeImport = value;
|
|
278
|
+
break;
|
|
279
|
+
case "runtime_type_import":
|
|
280
|
+
options.runtimeTypeImport = value;
|
|
281
|
+
break;
|
|
282
|
+
case "node_runtime_import":
|
|
283
|
+
options.nodeRuntimeImport = value;
|
|
284
|
+
break;
|
|
285
|
+
case "node_runtime_type_import":
|
|
286
|
+
options.nodeRuntimeTypeImport = value;
|
|
287
|
+
break;
|
|
288
|
+
case "file_suffix":
|
|
289
|
+
options.fileSuffix = value;
|
|
290
|
+
break;
|
|
291
|
+
default:
|
|
292
|
+
throw new Error(`unknown trevrpc-js option ${JSON.stringify(key)}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
options.runtimeTypeImport = resolveTypeImport(
|
|
297
|
+
"runtime_import",
|
|
298
|
+
options.runtimeImport,
|
|
299
|
+
options.runtimeTypeImport,
|
|
300
|
+
);
|
|
301
|
+
options.nodeRuntimeTypeImport = resolveTypeImport(
|
|
302
|
+
"node_runtime_import",
|
|
303
|
+
options.nodeRuntimeImport,
|
|
304
|
+
options.nodeRuntimeTypeImport,
|
|
305
|
+
);
|
|
306
|
+
return options;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function resolveTypeImport(optionName, runtimeImport, typeImport) {
|
|
310
|
+
if (typeImport != null) {
|
|
311
|
+
return typeImport;
|
|
312
|
+
}
|
|
313
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*:/u.test(runtimeImport)) {
|
|
314
|
+
throw new Error(
|
|
315
|
+
`${optionName} ${JSON.stringify(runtimeImport)} requires an explicit ${optionName.replace("_import", "_type_import")}`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
return runtimeImport;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Builds a protobuf.js JSON root from protobuf file descriptors. */
|
|
322
|
+
export function buildProtobufJson(files) {
|
|
323
|
+
const root = { nested: {} };
|
|
324
|
+
|
|
325
|
+
for (const file of files) {
|
|
326
|
+
const scope = ensurePackage(root, file.package ?? "");
|
|
327
|
+
for (const enumType of file.enumType ?? []) {
|
|
328
|
+
scope.nested[enumType.name] = enumToJson(enumType);
|
|
329
|
+
}
|
|
330
|
+
for (const messageType of file.messageType ?? []) {
|
|
331
|
+
scope.nested[messageType.name] = messageToJson(messageType);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return root;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function generateFile(file, rootJson, options) {
|
|
339
|
+
const services = (file.service ?? []).map((service) => serviceDescriptor(file, service));
|
|
340
|
+
const serviceExports = services.map((service) => generateService(service)).join("\n");
|
|
341
|
+
|
|
342
|
+
return `// Code generated by protoc-gen-trevrpc-js. DO NOT EDIT.\n\nimport { createRoot, createServiceClient } from ${JSON.stringify(
|
|
343
|
+
options.runtimeImport,
|
|
344
|
+
)};\n\nexport const root = createRoot(${JSON.stringify(rootJson, null, 2)});\n\n${serviceExports}`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function generateDeclarationFile(protoFiles, file, typeNames, options) {
|
|
348
|
+
const messages = collectMessages(protoFiles)
|
|
349
|
+
.filter(({ message }) => !message.options?.mapEntry)
|
|
350
|
+
.map((message) => generateMessageDeclaration(message, typeNames))
|
|
351
|
+
.join("\n\n");
|
|
352
|
+
const services = (file.service ?? [])
|
|
353
|
+
.map((service) => serviceDescriptor(file, service))
|
|
354
|
+
.map((service) => generateServiceDeclaration(service, typeNames))
|
|
355
|
+
.join("\n\n");
|
|
356
|
+
const sections = [messages, services].filter(Boolean).join("\n\n");
|
|
357
|
+
|
|
358
|
+
return `// Code generated by protoc-gen-trevrpc-js. DO NOT EDIT.\n\nimport type { Long, Root } from "protobufjs";\n\nimport type { BidirectionalStreamingCall, CallOptions, ClientStreamingCall, ResponseAsyncIterable, RpcServiceDescriptor, Transport, UnaryResponse } from ${JSON.stringify(
|
|
359
|
+
options.runtimeTypeImport,
|
|
360
|
+
)};\n\nexport declare const root: Root;\n\n${sections}\n`;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function generateNodeFile(file, ordinaryFileName, options) {
|
|
364
|
+
const services = (file.service ?? []).map((service) => serviceDescriptor(file, service));
|
|
365
|
+
const serviceNames = services.map((service) => service.exportName).join(", ");
|
|
366
|
+
const registrations = services.map(generateNodeServerRegistration).join("\n");
|
|
367
|
+
return `// Code generated by protoc-gen-trevrpc-js. DO NOT EDIT.\n\nimport { registerTypedService } from ${JSON.stringify(
|
|
368
|
+
options.nodeRuntimeImport,
|
|
369
|
+
)};\nimport { ${serviceNames}, root } from ${JSON.stringify(localModuleSpecifier(ordinaryFileName))};\n\n${registrations}`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function generateNodeDeclarationFile(file, ordinaryFileName, typeNames, options) {
|
|
373
|
+
const services = (file.service ?? []).map((service) => serviceDescriptor(file, service));
|
|
374
|
+
const messageNames = [...new Set(typeNames.values())].join(", ");
|
|
375
|
+
const messageImport =
|
|
376
|
+
messageNames === ""
|
|
377
|
+
? ""
|
|
378
|
+
: `import type { ${messageNames} } from ${JSON.stringify(localModuleSpecifier(ordinaryFileName))};\n`;
|
|
379
|
+
const declarations = services
|
|
380
|
+
.map((service) => generateNodeServiceDeclaration(service, typeNames))
|
|
381
|
+
.join("\n\n");
|
|
382
|
+
return `// Code generated by protoc-gen-trevrpc-js. DO NOT EDIT.\n\nimport type { NodeServer, NodeServerCall, StreamingServerResponse, UnaryServerResponse } from ${JSON.stringify(
|
|
383
|
+
options.nodeRuntimeTypeImport,
|
|
384
|
+
)};\n${messageImport}\n${declarations}\n`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function generateService(service) {
|
|
388
|
+
const className = `${pascalIdentifier(service.name)}Client`;
|
|
389
|
+
const factoryName = `create${className}`;
|
|
390
|
+
const methods = Object.entries(service.methods)
|
|
391
|
+
.map(([jsName, method]) => generateServiceMethod(jsName, method))
|
|
392
|
+
.join("\n\n");
|
|
393
|
+
return `/** Service descriptor for the ${service.name} service. */\nexport const ${service.exportName} = Object.freeze(${JSON.stringify(service, null, 2)});\n\n/** Client for the ${service.name} service. */\nexport class ${className} {\n /** Creates a client for the ${service.name} service. */\n constructor(transport, options = {}) {\n this._client = createServiceClient(transport, ${service.exportName}, root, options);\n }\n\n${methods}\n}\n\n/** Creates a client for the ${service.name} service. */\nexport function ${factoryName}(transport, options = {}) {\n return new ${className}(transport, options);\n}\n`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function generateServiceMethod(jsName, method) {
|
|
397
|
+
if (method.kind === "clientStreaming" || method.kind === "bidirectionalStreaming") {
|
|
398
|
+
return ` /** Calls the ${method.name} RPC. */\n ${jsName}(options = {}) {\n return this._client.${jsName}(options);\n }`;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (method.kind === "unary") {
|
|
402
|
+
return ` /** Calls the ${method.name} RPC. */\n ${jsName}(request, options = {}) {\n return this._client.${jsName}(request, options);\n }\n\n /** Calls the ${method.name} RPC and returns response metadata. */\n ${jsName}WithResponse(request, options = {}) {\n return this._client.${jsName}WithResponse(request, options);\n }`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return ` /** Calls the ${method.name} RPC. */\n ${jsName}(request, options = {}) {\n return this._client.${jsName}(request, options);\n }`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function generateNodeServerRegistration(service) {
|
|
409
|
+
const registerName = `register${pascalIdentifier(service.name)}Server`;
|
|
410
|
+
return `/** Registers typed Node handlers for the ${service.name} service. */\nexport function ${registerName}(server, handlers) {\n return registerTypedService(server, root, ${service.exportName}, handlers);\n}\n`;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function generateServiceDeclaration(service, typeNames) {
|
|
414
|
+
const className = `${pascalIdentifier(service.name)}Client`;
|
|
415
|
+
const factoryName = `create${className}`;
|
|
416
|
+
const methods = Object.entries(service.methods)
|
|
417
|
+
.map(([jsName, method]) => generateMethodDeclaration(jsName, method, typeNames))
|
|
418
|
+
.join("\n");
|
|
419
|
+
|
|
420
|
+
return `/** Service descriptor for the ${service.name} service. */\nexport declare const ${service.exportName}: RpcServiceDescriptor;\n\n/** Client for the ${service.name} service. */\nexport declare class ${className} {\n /** Creates a client for the ${service.name} service. */\n constructor(transport: Transport, options?: CallOptions);\n\n${methods}\n}\n\n/** Creates a client for the ${service.name} service. */\nexport declare function ${factoryName}(transport: Transport, options?: CallOptions): ${className};`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function generateNodeServiceDeclaration(service, typeNames) {
|
|
424
|
+
const handlerName = `${pascalIdentifier(service.name)}Handlers`;
|
|
425
|
+
const registerName = `register${pascalIdentifier(service.name)}Server`;
|
|
426
|
+
const handlers = Object.entries(service.methods)
|
|
427
|
+
.map(([jsName, method]) => generateHandlerDeclaration(jsName, method, typeNames))
|
|
428
|
+
.join("\n");
|
|
429
|
+
return `/** Typed Node handlers for the ${service.name} service. */\nexport interface ${handlerName} {\n${handlers}\n}\n\n/** Registers typed Node handlers for the ${service.name} service. */\nexport declare function ${registerName}(server: NodeServer, handlers: ${handlerName}): NodeServer;`;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function generateMethodDeclaration(jsName, method, typeNames) {
|
|
433
|
+
const inputType = declarationTypeReference(method.inputType, typeNames);
|
|
434
|
+
const outputType = declarationTypeReference(method.outputType, typeNames);
|
|
435
|
+
|
|
436
|
+
switch (method.kind) {
|
|
437
|
+
case "unary":
|
|
438
|
+
return ` /** Calls the ${method.name} RPC. */\n ${jsName}(request: ${inputType}, options?: CallOptions): Promise<${outputType}>;\n /** Calls the ${method.name} RPC and returns response metadata. */\n ${jsName}WithResponse(request: ${inputType}, options?: CallOptions): Promise<UnaryResponse<${outputType}>>;`;
|
|
439
|
+
case "serverStreaming":
|
|
440
|
+
return ` /** Calls the ${method.name} RPC. */\n ${jsName}(request: ${inputType}, options?: CallOptions): Promise<ResponseAsyncIterable<${outputType}>>;`;
|
|
441
|
+
case "clientStreaming":
|
|
442
|
+
return ` /** Calls the ${method.name} RPC. */\n ${jsName}(options?: CallOptions): Promise<ClientStreamingCall<${inputType}, ${outputType}>>;`;
|
|
443
|
+
case "bidirectionalStreaming":
|
|
444
|
+
return ` /** Calls the ${method.name} RPC. */\n ${jsName}(options?: CallOptions): Promise<BidirectionalStreamingCall<${inputType}, ${outputType}>>;`;
|
|
445
|
+
default:
|
|
446
|
+
throw new Error(`unsupported generated RPC kind ${JSON.stringify(method.kind)}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function generateHandlerDeclaration(jsName, method, typeNames) {
|
|
451
|
+
const inputType = declarationTypeReference(method.inputType, typeNames);
|
|
452
|
+
const outputType = declarationTypeReference(method.outputType, typeNames);
|
|
453
|
+
const unaryResult = `${outputType} | UnaryServerResponse<${outputType}>`;
|
|
454
|
+
const streamingResult = `Iterable<${outputType}> | AsyncIterable<${outputType}> | StreamingServerResponse<${outputType}>`;
|
|
455
|
+
switch (method.kind) {
|
|
456
|
+
case "unary":
|
|
457
|
+
return ` ${jsName}?(request: ${inputType}, call: NodeServerCall): ${unaryResult} | Promise<${unaryResult}>;`;
|
|
458
|
+
case "serverStreaming":
|
|
459
|
+
return ` ${jsName}?(request: ${inputType}, call: NodeServerCall): ${streamingResult} | Promise<${streamingResult}>;`;
|
|
460
|
+
case "clientStreaming":
|
|
461
|
+
return ` ${jsName}?(requests: AsyncIterable<${inputType}>, call: NodeServerCall): ${unaryResult} | Promise<${unaryResult}>;`;
|
|
462
|
+
case "bidirectionalStreaming":
|
|
463
|
+
return ` ${jsName}?(requests: AsyncIterable<${inputType}>, call: NodeServerCall): ${streamingResult} | Promise<${streamingResult}>;`;
|
|
464
|
+
default:
|
|
465
|
+
throw new Error(`unsupported generated RPC kind ${JSON.stringify(method.kind)}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function serviceDescriptor(file, service) {
|
|
470
|
+
const fullName =
|
|
471
|
+
file.package === "" || file.package == null ? service.name : `${file.package}.${service.name}`;
|
|
472
|
+
const methods = {};
|
|
473
|
+
const usedNames = new Set();
|
|
474
|
+
|
|
475
|
+
for (const method of service.method ?? []) {
|
|
476
|
+
const jsName = uniqueName(lowerCamelIdentifier(method.name), usedNames);
|
|
477
|
+
methods[jsName] = {
|
|
478
|
+
name: method.name,
|
|
479
|
+
kind: methodKind(method),
|
|
480
|
+
inputType: stripLeadingDot(method.inputType),
|
|
481
|
+
outputType: stripLeadingDot(method.outputType),
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
name: service.name,
|
|
487
|
+
fullName,
|
|
488
|
+
exportName: `${pascalIdentifier(service.name)}Service`,
|
|
489
|
+
methods,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function declarationTypeNames(files) {
|
|
494
|
+
const names = new Map();
|
|
495
|
+
const usedNames = new Set();
|
|
496
|
+
|
|
497
|
+
for (const { fullName, message } of collectMessages(files)) {
|
|
498
|
+
if (message.options?.mapEntry) {
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
names.set(fullName, uniqueName(pascalIdentifier(lastTypeSegment(fullName)), usedNames));
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return names;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function collectMessages(files) {
|
|
509
|
+
const messages = [];
|
|
510
|
+
for (const file of files) {
|
|
511
|
+
messages.push(...collectFileMessages(file));
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return messages;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function collectFileMessages(file) {
|
|
518
|
+
const messages = [];
|
|
519
|
+
for (const message of file.messageType ?? []) {
|
|
520
|
+
collectMessage(messages, joinTypeName(file.package ?? "", message.name), message);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return messages;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function collectMessage(messages, fullName, message) {
|
|
527
|
+
messages.push({ fullName, message });
|
|
528
|
+
|
|
529
|
+
for (const nested of message.nestedType ?? []) {
|
|
530
|
+
collectMessage(messages, joinTypeName(fullName, nested.name), nested);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function generateMessageDeclaration({ fullName, message }, typeNames) {
|
|
535
|
+
const name = typeNames.get(fullName);
|
|
536
|
+
const mapEntries = new Map(
|
|
537
|
+
(message.nestedType ?? [])
|
|
538
|
+
.filter((nested) => nested.options?.mapEntry)
|
|
539
|
+
.map((nested) => [nested.name, nested]),
|
|
540
|
+
);
|
|
541
|
+
const fields = (message.field ?? [])
|
|
542
|
+
.map((field) => generateFieldDeclaration(field, mapEntries, typeNames, fullName))
|
|
543
|
+
.join("\n");
|
|
544
|
+
|
|
545
|
+
return `export interface ${name} {\n${fields}\n}`;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function generateFieldDeclaration(field, mapEntries, typeNames, currentFullName) {
|
|
549
|
+
const optional = field.label === FieldLabel.Required ? "" : "?";
|
|
550
|
+
return ` ${typescriptPropertyName(fieldJsonName(field))}${optional}: ${fieldTypeScriptType(
|
|
551
|
+
field,
|
|
552
|
+
mapEntries,
|
|
553
|
+
typeNames,
|
|
554
|
+
currentFullName,
|
|
555
|
+
)};`;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function fieldTypeScriptType(field, mapEntries, typeNames, currentFullName) {
|
|
559
|
+
const mapEntry = mapEntryForField(field, mapEntries);
|
|
560
|
+
if (mapEntry != null) {
|
|
561
|
+
const [, valueField] = mapEntry.field ?? [];
|
|
562
|
+
const valueType = scalarTypeScriptType(valueField, typeNames, currentFullName);
|
|
563
|
+
return `Record<string, ${valueType}>`;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const scalarType = scalarTypeScriptType(field, typeNames, currentFullName);
|
|
567
|
+
return field.label === FieldLabel.Repeated ? `${scalarType}[]` : scalarType;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function scalarTypeScriptType(field, typeNames, currentFullName) {
|
|
571
|
+
switch (field?.type) {
|
|
572
|
+
case FieldType.Double:
|
|
573
|
+
case FieldType.Float:
|
|
574
|
+
case FieldType.Int32:
|
|
575
|
+
case FieldType.Uint32:
|
|
576
|
+
case FieldType.Sint32:
|
|
577
|
+
case FieldType.Fixed32:
|
|
578
|
+
case FieldType.Sfixed32:
|
|
579
|
+
case FieldType.Enum:
|
|
580
|
+
return "number";
|
|
581
|
+
case FieldType.Int64:
|
|
582
|
+
case FieldType.Uint64:
|
|
583
|
+
case FieldType.Sint64:
|
|
584
|
+
case FieldType.Fixed64:
|
|
585
|
+
case FieldType.Sfixed64:
|
|
586
|
+
return "number | string | Long";
|
|
587
|
+
case FieldType.Bool:
|
|
588
|
+
return "boolean";
|
|
589
|
+
case FieldType.String:
|
|
590
|
+
return "string";
|
|
591
|
+
case FieldType.Bytes:
|
|
592
|
+
return "Uint8Array";
|
|
593
|
+
case FieldType.Group:
|
|
594
|
+
case FieldType.Message:
|
|
595
|
+
return declarationTypeReference(
|
|
596
|
+
resolveTypeName(field.typeName, typeNames, currentFullName),
|
|
597
|
+
typeNames,
|
|
598
|
+
);
|
|
599
|
+
default:
|
|
600
|
+
throw new Error(`unsupported protobuf field type ${field?.type}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function declarationTypeReference(typeName, typeNames) {
|
|
605
|
+
return typeNames.get(stripLeadingDot(typeName)) ?? "Record<string, unknown>";
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function resolveTypeName(typeName, typeNames, currentFullName) {
|
|
609
|
+
const stripped = stripLeadingDot(typeName);
|
|
610
|
+
if (typeNames.has(stripped)) {
|
|
611
|
+
return stripped;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const scope = currentFullName.split(".");
|
|
615
|
+
scope.pop();
|
|
616
|
+
while (scope.length > 0) {
|
|
617
|
+
const candidate = `${scope.join(".")}.${stripped}`;
|
|
618
|
+
if (typeNames.has(candidate)) {
|
|
619
|
+
return candidate;
|
|
620
|
+
}
|
|
621
|
+
scope.pop();
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return stripped;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function joinTypeName(prefix, name) {
|
|
628
|
+
const segment = name ?? "Message";
|
|
629
|
+
return prefix === "" || prefix == null ? segment : `${prefix}.${segment}`;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function typescriptPropertyName(name) {
|
|
633
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function methodKind(method) {
|
|
637
|
+
if (method.clientStreaming && method.serverStreaming) {
|
|
638
|
+
return "bidirectionalStreaming";
|
|
639
|
+
}
|
|
640
|
+
if (method.clientStreaming) {
|
|
641
|
+
return "clientStreaming";
|
|
642
|
+
}
|
|
643
|
+
if (method.serverStreaming) {
|
|
644
|
+
return "serverStreaming";
|
|
645
|
+
}
|
|
646
|
+
return "unary";
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function ensurePackage(root, packageName) {
|
|
650
|
+
let scope = root;
|
|
651
|
+
for (const part of packageName.split(".")) {
|
|
652
|
+
if (part === "") {
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
scope.nested ??= {};
|
|
657
|
+
scope.nested[part] ??= { nested: {} };
|
|
658
|
+
scope = scope.nested[part];
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
scope.nested ??= {};
|
|
662
|
+
return scope;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function messageToJson(message) {
|
|
666
|
+
const json = { fields: {} };
|
|
667
|
+
const nested = {};
|
|
668
|
+
const mapEntries = new Map();
|
|
669
|
+
|
|
670
|
+
for (const nestedMessage of message.nestedType ?? []) {
|
|
671
|
+
if (nestedMessage.options?.mapEntry) {
|
|
672
|
+
mapEntries.set(nestedMessage.name, nestedMessage);
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
nested[nestedMessage.name] = messageToJson(nestedMessage);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
for (const enumType of message.enumType ?? []) {
|
|
680
|
+
nested[enumType.name] = enumToJson(enumType);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
for (const field of message.field ?? []) {
|
|
684
|
+
json.fields[fieldJsonName(field)] = fieldToJson(field, mapEntries);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const oneofs = oneofsToJson(message);
|
|
688
|
+
if (Object.keys(oneofs).length > 0) {
|
|
689
|
+
json.oneofs = oneofs;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (Object.keys(nested).length > 0) {
|
|
693
|
+
json.nested = nested;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return json;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function enumToJson(enumType) {
|
|
700
|
+
const values = {};
|
|
701
|
+
for (const value of enumType.value ?? []) {
|
|
702
|
+
values[value.name] = value.number ?? 0;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
return { values };
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function fieldToJson(field, mapEntries) {
|
|
709
|
+
const json = {
|
|
710
|
+
type: fieldTypeName(field),
|
|
711
|
+
id: field.number ?? 0,
|
|
712
|
+
};
|
|
713
|
+
const mapEntry = mapEntryForField(field, mapEntries);
|
|
714
|
+
|
|
715
|
+
if (mapEntry != null) {
|
|
716
|
+
const [keyField, valueField] = mapEntry.field ?? [];
|
|
717
|
+
json.keyType = fieldTypeName(keyField);
|
|
718
|
+
json.type = fieldTypeName(valueField);
|
|
719
|
+
return json;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (field.label === FieldLabel.Repeated) {
|
|
723
|
+
json.rule = "repeated";
|
|
724
|
+
} else if (field.label === FieldLabel.Required) {
|
|
725
|
+
json.rule = "required";
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (field.defaultValue != null && field.defaultValue !== "") {
|
|
729
|
+
json.options = { default: field.defaultValue };
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return json;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function mapEntryForField(field, mapEntries) {
|
|
736
|
+
if (
|
|
737
|
+
field.label !== FieldLabel.Repeated ||
|
|
738
|
+
field.type !== FieldType.Message ||
|
|
739
|
+
field.typeName == null
|
|
740
|
+
) {
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
return mapEntries.get(lastTypeSegment(field.typeName)) ?? null;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function oneofsToJson(message) {
|
|
748
|
+
const oneofs = {};
|
|
749
|
+
for (const [index, oneof] of (message.oneofDecl ?? []).entries()) {
|
|
750
|
+
const fields = (message.field ?? [])
|
|
751
|
+
.filter((field) => field.oneofIndex === index)
|
|
752
|
+
.map(fieldJsonName);
|
|
753
|
+
if (fields.length > 0) {
|
|
754
|
+
oneofs[oneof.name] = { oneof: fields };
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return oneofs;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function fieldJsonName(field) {
|
|
762
|
+
return field.jsonName || lowerCamelIdentifier(field.name ?? "field");
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function fieldTypeName(field) {
|
|
766
|
+
switch (field?.type) {
|
|
767
|
+
case FieldType.Double:
|
|
768
|
+
return "double";
|
|
769
|
+
case FieldType.Float:
|
|
770
|
+
return "float";
|
|
771
|
+
case FieldType.Int64:
|
|
772
|
+
return "int64";
|
|
773
|
+
case FieldType.Uint64:
|
|
774
|
+
return "uint64";
|
|
775
|
+
case FieldType.Int32:
|
|
776
|
+
return "int32";
|
|
777
|
+
case FieldType.Fixed64:
|
|
778
|
+
return "fixed64";
|
|
779
|
+
case FieldType.Fixed32:
|
|
780
|
+
return "fixed32";
|
|
781
|
+
case FieldType.Bool:
|
|
782
|
+
return "bool";
|
|
783
|
+
case FieldType.String:
|
|
784
|
+
return "string";
|
|
785
|
+
case FieldType.Group:
|
|
786
|
+
case FieldType.Message:
|
|
787
|
+
case FieldType.Enum:
|
|
788
|
+
return stripLeadingDot(field.typeName);
|
|
789
|
+
case FieldType.Bytes:
|
|
790
|
+
return "bytes";
|
|
791
|
+
case FieldType.Uint32:
|
|
792
|
+
return "uint32";
|
|
793
|
+
case FieldType.Sfixed32:
|
|
794
|
+
return "sfixed32";
|
|
795
|
+
case FieldType.Sfixed64:
|
|
796
|
+
return "sfixed64";
|
|
797
|
+
case FieldType.Sint32:
|
|
798
|
+
return "sint32";
|
|
799
|
+
case FieldType.Sint64:
|
|
800
|
+
return "sint64";
|
|
801
|
+
default:
|
|
802
|
+
throw new Error(`unsupported protobuf field type ${field?.type}`);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function outputFileName(inputName, suffix) {
|
|
807
|
+
const extensionStart = inputName.lastIndexOf(".");
|
|
808
|
+
return `${extensionStart === -1 ? inputName : inputName.slice(0, extensionStart)}${suffix}`;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function localModuleSpecifier(fileName) {
|
|
812
|
+
return `./${fileName.slice(fileName.lastIndexOf("/") + 1)}`;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function declarationFileName(outputName) {
|
|
816
|
+
if (outputName.endsWith(".js")) {
|
|
817
|
+
return `${outputName.slice(0, -3)}.d.ts`;
|
|
818
|
+
}
|
|
819
|
+
if (outputName.endsWith(".mjs")) {
|
|
820
|
+
return `${outputName.slice(0, -4)}.d.mts`;
|
|
821
|
+
}
|
|
822
|
+
if (outputName.endsWith(".cjs")) {
|
|
823
|
+
return `${outputName.slice(0, -4)}.d.cts`;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
return `${outputName}.d.ts`;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function stripLeadingDot(name) {
|
|
830
|
+
return name?.startsWith(".") ? name.slice(1) : name;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function lastTypeSegment(name) {
|
|
834
|
+
const stripped = stripLeadingDot(name);
|
|
835
|
+
const parts = stripped.split(".");
|
|
836
|
+
return parts[parts.length - 1];
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function lowerCamelIdentifier(name) {
|
|
840
|
+
const pascal = pascalIdentifier(name);
|
|
841
|
+
const candidate = `${pascal[0].toLowerCase()}${pascal.slice(1)}`;
|
|
842
|
+
return RESERVED_WORDS.has(candidate) ? `${candidate}_` : candidate;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function pascalIdentifier(name) {
|
|
846
|
+
const words = String(name)
|
|
847
|
+
.split(/[^A-Za-z0-9]+/)
|
|
848
|
+
.filter(Boolean);
|
|
849
|
+
const candidate = words.map((word) => `${word[0].toUpperCase()}${word.slice(1)}`).join("");
|
|
850
|
+
const valid = candidate === "" ? "X" : candidate;
|
|
851
|
+
const prefixed = /^[0-9]/.test(valid) ? `_${valid}` : valid;
|
|
852
|
+
return RESERVED_WORDS.has(prefixed) ? `${prefixed}_` : prefixed;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function uniqueName(name, usedNames) {
|
|
856
|
+
if (!usedNames.has(name)) {
|
|
857
|
+
usedNames.add(name);
|
|
858
|
+
return name;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
for (let suffix = 2; ; suffix += 1) {
|
|
862
|
+
const candidate = `${name}${suffix}`;
|
|
863
|
+
if (!usedNames.has(candidate)) {
|
|
864
|
+
usedNames.add(candidate);
|
|
865
|
+
return candidate;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|