@trevrpc/trevrpc-js 1.5.0 → 1.7.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/src/generator.js DELETED
@@ -1,1152 +0,0 @@
1
- import protobuf from "protobufjs";
2
-
3
- import { Int64Representations } from "./int64.js";
4
-
5
- const DESCRIPTOR_PROTO = String.raw`
6
- syntax = "proto2";
7
-
8
- package google.protobuf;
9
-
10
- message FileDescriptorProto {
11
- optional string name = 1;
12
- optional string package = 2;
13
- repeated string dependency = 3;
14
- repeated DescriptorProto message_type = 4;
15
- repeated EnumDescriptorProto enum_type = 5;
16
- repeated ServiceDescriptorProto service = 6;
17
- optional FileOptions options = 8;
18
- optional string syntax = 12;
19
- }
20
-
21
- message DescriptorProto {
22
- optional string name = 1;
23
- repeated FieldDescriptorProto field = 2;
24
- repeated DescriptorProto nested_type = 3;
25
- repeated EnumDescriptorProto enum_type = 4;
26
- optional MessageOptions options = 7;
27
- repeated OneofDescriptorProto oneof_decl = 8;
28
- }
29
-
30
- message FieldDescriptorProto {
31
- enum Type {
32
- TYPE_DOUBLE = 1;
33
- TYPE_FLOAT = 2;
34
- TYPE_INT64 = 3;
35
- TYPE_UINT64 = 4;
36
- TYPE_INT32 = 5;
37
- TYPE_FIXED64 = 6;
38
- TYPE_FIXED32 = 7;
39
- TYPE_BOOL = 8;
40
- TYPE_STRING = 9;
41
- TYPE_GROUP = 10;
42
- TYPE_MESSAGE = 11;
43
- TYPE_BYTES = 12;
44
- TYPE_UINT32 = 13;
45
- TYPE_ENUM = 14;
46
- TYPE_SFIXED32 = 15;
47
- TYPE_SFIXED64 = 16;
48
- TYPE_SINT32 = 17;
49
- TYPE_SINT64 = 18;
50
- }
51
-
52
- enum Label {
53
- LABEL_OPTIONAL = 1;
54
- LABEL_REQUIRED = 2;
55
- LABEL_REPEATED = 3;
56
- }
57
-
58
- optional string name = 1;
59
- optional int32 number = 3;
60
- optional Label label = 4;
61
- optional Type type = 5;
62
- optional string type_name = 6;
63
- optional string default_value = 7;
64
- optional int32 oneof_index = 9;
65
- optional string json_name = 10;
66
- optional bool proto3_optional = 17;
67
- optional FieldOptions options = 8;
68
- }
69
-
70
- message OneofDescriptorProto {
71
- optional string name = 1;
72
- }
73
-
74
- message EnumDescriptorProto {
75
- optional string name = 1;
76
- repeated EnumValueDescriptorProto value = 2;
77
- }
78
-
79
- message EnumValueDescriptorProto {
80
- optional string name = 1;
81
- optional int32 number = 2;
82
- }
83
-
84
- message ServiceDescriptorProto {
85
- optional string name = 1;
86
- repeated MethodDescriptorProto method = 2;
87
- }
88
-
89
- message MethodDescriptorProto {
90
- optional string name = 1;
91
- optional string input_type = 2;
92
- optional string output_type = 3;
93
- optional bool client_streaming = 5 [default = false];
94
- optional bool server_streaming = 6 [default = false];
95
- optional MethodOptions options = 4;
96
- }
97
-
98
- // Only the trevrpc.method extension is modelled; protobuf.js keeps every other extension out of
99
- // the decoded message. The numbers are those of proto/trevrpc/options.proto.
100
- message MethodOptions {
101
- optional TrevrpcMethodOptions trevrpc_method = 58247;
102
- }
103
-
104
- message TrevrpcMethodOptions {
105
- optional bool unreliable_requests = 1;
106
- optional bool unreliable_responses = 2;
107
- // trevrpc.Compression, read as its number so that unknown values reach the check.
108
- optional int32 compression = 5;
109
- }
110
-
111
- message FieldOptions {
112
- enum JSType {
113
- JS_NORMAL = 0;
114
- JS_STRING = 1;
115
- JS_NUMBER = 2;
116
- }
117
- optional JSType jstype = 6;
118
- optional bool debug_redact = 16;
119
- }
120
-
121
- message FileOptions {
122
- optional string java_package = 1;
123
- optional string java_outer_classname = 8;
124
- optional bool java_multiple_files = 10;
125
- optional string go_package = 11;
126
- optional string objc_class_prefix = 36;
127
- optional string csharp_namespace = 37;
128
- optional string php_namespace = 41;
129
- optional string ruby_package = 45;
130
- }
131
-
132
- message MessageOptions {
133
- optional bool map_entry = 7;
134
- }
135
- `;
136
-
137
- const PLUGIN_PROTO = String.raw`
138
- syntax = "proto2";
139
-
140
- package google.protobuf.compiler;
141
-
142
- message CodeGeneratorRequest {
143
- repeated string file_to_generate = 1;
144
- optional string parameter = 2;
145
- repeated .google.protobuf.FileDescriptorProto proto_file = 15;
146
- }
147
-
148
- message CodeGeneratorResponse {
149
- optional string error = 1;
150
- optional uint64 supported_features = 2;
151
- repeated File file = 15;
152
-
153
- message File {
154
- optional string name = 1;
155
- optional string insertion_point = 2;
156
- optional string content = 15;
157
- }
158
- }
159
- `;
160
-
161
- const pluginRoot = new protobuf.Root();
162
- protobuf.parse(DESCRIPTOR_PROTO, pluginRoot);
163
- protobuf.parse(PLUGIN_PROTO, pluginRoot);
164
- const CodeGeneratorRequest = pluginRoot.lookupType("google.protobuf.compiler.CodeGeneratorRequest");
165
- const CodeGeneratorResponse = pluginRoot.lookupType(
166
- "google.protobuf.compiler.CodeGeneratorResponse",
167
- );
168
-
169
- const FieldType = Object.freeze({
170
- Double: 1,
171
- Float: 2,
172
- Int64: 3,
173
- Uint64: 4,
174
- Int32: 5,
175
- Fixed64: 6,
176
- Fixed32: 7,
177
- Bool: 8,
178
- String: 9,
179
- Group: 10,
180
- Message: 11,
181
- Bytes: 12,
182
- Uint32: 13,
183
- Enum: 14,
184
- Sfixed32: 15,
185
- Sfixed64: 16,
186
- Sint32: 17,
187
- Sint64: 18,
188
- });
189
-
190
- const FieldLabel = Object.freeze({
191
- Optional: 1,
192
- Required: 2,
193
- Repeated: 3,
194
- });
195
-
196
- /** The CodeGeneratorResponse features this generator supports. */
197
- const FEATURE_PROTO3_OPTIONAL = 1;
198
-
199
- /** Generic runtime types that generated declarations import only when they use them. */
200
- const GeneratedTypeImports = [
201
- "BidirectionalStreamingCall",
202
- "ClientStreamingCall",
203
- "ResponseStream",
204
- ];
205
-
206
- /**
207
- * Well-known types whose declarations the runtime exports, so that generated files share them
208
- * instead of declaring their own: the full name and whether it holds 64-bit integers, which the
209
- * runtime declares as bigint, so it is shared only when that is the representation.
210
- */
211
- const SharedWellKnownTypes = new Map([
212
- ["google.protobuf.Timestamp", { int64: true }],
213
- ["google.protobuf.Duration", { int64: true }],
214
- ["google.protobuf.Empty", { int64: false }],
215
- ["google.protobuf.Struct", { int64: false }],
216
- ["google.protobuf.Value", { int64: false }],
217
- ["google.protobuf.ListValue", { int64: false }],
218
- ["google.protobuf.NullValue", { int64: false }],
219
- ]);
220
-
221
- /** FieldOptions.JSType values. */
222
- const JSType = Object.freeze({ normal: 0, string: 1, number: 2 });
223
-
224
- const RESERVED_WORDS = new Set([
225
- "break",
226
- "case",
227
- "catch",
228
- "class",
229
- "const",
230
- "continue",
231
- "debugger",
232
- "default",
233
- "delete",
234
- "do",
235
- "else",
236
- "export",
237
- "extends",
238
- "finally",
239
- "for",
240
- "function",
241
- "if",
242
- "import",
243
- "in",
244
- "instanceof",
245
- "new",
246
- "return",
247
- "super",
248
- "switch",
249
- "this",
250
- "throw",
251
- "try",
252
- "typeof",
253
- "var",
254
- "void",
255
- "while",
256
- "with",
257
- "yield",
258
- ]);
259
-
260
- /** Runs the TrevRPC JavaScript code generator for protoc plugin input. */
261
- export function runGenerator(input) {
262
- const request = CodeGeneratorRequest.decode(input);
263
- const response = generate(request);
264
- return CodeGeneratorResponse.encode(response).finish();
265
- }
266
-
267
- /** Generates a protoc code generator response. */
268
- export function generate(request) {
269
- try {
270
- const options = parseOptions(request.parameter ?? "");
271
- const filesToGenerate = new Set(request.fileToGenerate ?? []);
272
- const protoFiles = request.protoFile ?? [];
273
- const rootJson = buildProtobufJson(protoFiles);
274
- const redacted = redactedTypes(protoFiles);
275
- const files = [];
276
-
277
- for (const file of protoFiles) {
278
- if (!filesToGenerate.has(file.name) || (file.service ?? []).length === 0) {
279
- continue;
280
- }
281
-
282
- const fileName = outputFileName(file.name, options.fileSuffix);
283
- const model = declarationModel(protoFiles, file, options);
284
- files.push({
285
- name: fileName,
286
- content: generateFile(file, rootJson, model, redacted, options),
287
- });
288
- files.push({
289
- name: declarationFileName(fileName),
290
- content: generateDeclarationFile(file, model, redacted, options),
291
- });
292
- }
293
-
294
- return { file: files, supportedFeatures: FEATURE_PROTO3_OPTIONAL };
295
- } catch (error) {
296
- return { error: error.message, supportedFeatures: FEATURE_PROTO3_OPTIONAL };
297
- }
298
- }
299
-
300
- /** Parses protoc plugin options for the JavaScript generator. */
301
- export function parseOptions(parameter) {
302
- const options = {
303
- runtimeImport: "@trevrpc/trevrpc-js",
304
- runtimeTypeImport: null,
305
- fileSuffix: ".trevrpc.js",
306
- int64: "bigint",
307
- };
308
-
309
- for (const option of parameter.split(",")) {
310
- if (option === "") {
311
- continue;
312
- }
313
-
314
- const separator = option.indexOf("=");
315
- if (separator === -1) {
316
- throw new Error(`invalid trevrpc-js option ${JSON.stringify(option)}; expected key=value`);
317
- }
318
-
319
- const key = option.slice(0, separator);
320
- const value = option.slice(separator + 1);
321
- switch (key) {
322
- case "runtime_import":
323
- options.runtimeImport = value;
324
- break;
325
- case "runtime_type_import":
326
- options.runtimeTypeImport = value;
327
- break;
328
- case "file_suffix":
329
- options.fileSuffix = value;
330
- break;
331
- case "int64":
332
- if (!Int64Representations.includes(value)) {
333
- throw new Error(
334
- `invalid trevrpc-js int64 ${JSON.stringify(value)}; expected one of ${Int64Representations.join(", ")}`,
335
- );
336
- }
337
- options.int64 = value;
338
- break;
339
- default:
340
- throw new Error(`unknown trevrpc-js option ${JSON.stringify(key)}`);
341
- }
342
- }
343
-
344
- if (options.runtimeTypeImport == null) {
345
- if (/^[A-Za-z][A-Za-z0-9+.-]*:/u.test(options.runtimeImport)) {
346
- throw new Error(
347
- `runtime_import ${JSON.stringify(options.runtimeImport)} requires an explicit runtime_type_import`,
348
- );
349
- }
350
- options.runtimeTypeImport = options.runtimeImport;
351
- }
352
- return options;
353
- }
354
-
355
- /** Builds a protobuf.js JSON root from protobuf file descriptors. */
356
- export function buildProtobufJson(files) {
357
- const root = { nested: {} };
358
-
359
- for (const file of files) {
360
- const scope = ensurePackage(root, file.package ?? "");
361
- for (const enumType of file.enumType ?? []) {
362
- scope.nested[enumType.name] = enumToJson(enumType);
363
- }
364
- for (const messageType of file.messageType ?? []) {
365
- scope.nested[messageType.name] = messageToJson(messageType);
366
- }
367
- }
368
-
369
- return root;
370
- }
371
-
372
- function generateFile(file, rootJson, model, redacted, options) {
373
- const services = (file.service ?? []).map((service) =>
374
- serviceDescriptor(file, service, redacted),
375
- );
376
- const serviceExports = services.map((service) => generateService(service)).join("\n");
377
- const enumExports = model.enums
378
- .filter(({ shared }) => !shared)
379
- .map(generateEnum)
380
- .join("\n\n");
381
-
382
- return `// Code generated by protoc-gen-trevrpc-js. DO NOT EDIT.\n\nimport { createRoot, createServiceClient } from ${JSON.stringify(
383
- options.runtimeImport,
384
- )};\n\nexport const root = createRoot(${JSON.stringify(rootJson, null, 2)}, { int64: ${JSON.stringify(options.int64)} });\n\n${enumExports === "" ? "" : `${enumExports}\n\n`}${serviceExports}`;
385
- }
386
-
387
- /** The runtime value of an enum: a frozen object of its values by name. */
388
- function generateEnum({ fullName, name, values }) {
389
- const entries = values.map(({ key, number }) => ` ${typescriptPropertyName(key)}: ${number},`);
390
- return `/** Values of the ${fullName} enum. */\nexport const ${name} = Object.freeze({\n${entries.join("\n")}\n});`;
391
- }
392
-
393
- function generateDeclarationFile(file, model, redacted, options) {
394
- const context = { model, runtimeTypes: new Set(), usesLong: false };
395
- const enums = model.enums
396
- .filter(({ shared }) => !shared)
397
- .map(generateEnumDeclaration)
398
- .join("\n\n");
399
- const messages = model.messages
400
- .filter(({ shared }) => !shared)
401
- .map((message) => generateMessageDeclarations(message, context))
402
- .join("\n\n");
403
- const services = (file.service ?? [])
404
- .map((service) => serviceDescriptor(file, service, redacted))
405
- .map((service) => generateServiceDeclaration(service, context))
406
- .join("\n\n");
407
- const sections = [enums, messages, services].filter(Boolean).join("\n\n");
408
- const protobufImports = context.usesLong ? "Long, Root" : "Root";
409
- const runtimeImports = [
410
- "CallOptions",
411
- "ClientOptions",
412
- "RpcServiceDescriptor",
413
- "Transport",
414
- ...context.runtimeTypes,
415
- ...GeneratedTypeImports.filter((name) => sections.includes(`${name}<`)),
416
- ].sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
417
-
418
- return `// Code generated by protoc-gen-trevrpc-js. DO NOT EDIT.\n\nimport type { ${protobufImports} } from "protobufjs";\n\nimport type { ${runtimeImports.join(", ")} } from ${JSON.stringify(
419
- options.runtimeTypeImport,
420
- )};\n\nexport declare const root: Root;\n\n${sections}\n`;
421
- }
422
-
423
- /** The declaration of an enum's runtime value and of the type of its values. */
424
- function generateEnumDeclaration({ fullName, name, values }) {
425
- const entries = values.map(({ key, number }) => `${typescriptPropertyName(key)}: ${number}`);
426
- return `/** Values of the ${fullName} enum. */\nexport declare const ${name}: Readonly<{ ${entries.join("; ")} }>;\n/** A value of the ${fullName} enum. */\nexport type ${name} = (typeof ${name})[keyof typeof ${name}];`;
427
- }
428
-
429
- function generateService(service) {
430
- const className = `${pascalIdentifier(service.name)}Client`;
431
- const factoryName = `create${className}`;
432
- const methods = Object.entries(service.methods)
433
- .map(([jsName, method]) => generateServiceMethod(jsName, method))
434
- .join("\n\n");
435
- 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`;
436
- }
437
-
438
- function generateServiceMethod(jsName, method) {
439
- if (method.kind === "clientStreaming" || method.kind === "bidirectionalStreaming") {
440
- return ` /** Calls the ${method.name} RPC. */\n ${jsName}(options = {}) {\n return this._client.${jsName}(options);\n }`;
441
- }
442
-
443
- return ` /** Calls the ${method.name} RPC. */\n ${jsName}(request, options = {}) {\n return this._client.${jsName}(request, options);\n }`;
444
- }
445
-
446
- function generateServiceDeclaration(service, context) {
447
- const className = `${pascalIdentifier(service.name)}Client`;
448
- const factoryName = `create${className}`;
449
- const methods = Object.entries(service.methods)
450
- .map(([jsName, method]) => generateMethodDeclaration(jsName, method, context))
451
- .join("\n");
452
-
453
- 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?: ClientOptions);\n\n${methods}\n}\n\n/** Creates a client for the ${service.name} service. */\nexport declare function ${factoryName}(transport: Transport, options?: ClientOptions): ${className};`;
454
- }
455
-
456
- function generateMethodDeclaration(jsName, method, context) {
457
- // Requests take the input shape; responses are decoded messages.
458
- const inputType = messageTypeReference(method.inputType, "input", context);
459
- const outputType = messageTypeReference(method.outputType, "output", context);
460
-
461
- switch (method.kind) {
462
- case "unary":
463
- return ` /** Calls the ${method.name} RPC. */\n ${jsName}(request: ${inputType}, options?: CallOptions): Promise<${outputType}>;`;
464
- case "serverStreaming":
465
- return ` /** Calls the ${method.name} RPC. */\n ${jsName}(request: ${inputType}, options?: CallOptions): Promise<ResponseStream<${outputType}>>;`;
466
- case "clientStreaming":
467
- return ` /** Calls the ${method.name} RPC. */\n ${jsName}(options?: CallOptions): Promise<ClientStreamingCall<${inputType}, ${outputType}>>;`;
468
- case "bidirectionalStreaming":
469
- return ` /** Calls the ${method.name} RPC. */\n ${jsName}(options?: CallOptions): Promise<BidirectionalStreamingCall<${inputType}, ${outputType}>>;`;
470
- default:
471
- throw new Error(`unsupported generated RPC kind ${JSON.stringify(method.kind)}`);
472
- }
473
- }
474
-
475
- function serviceDescriptor(file, service, redacted) {
476
- const fullName =
477
- file.package === "" || file.package == null ? service.name : `${file.package}.${service.name}`;
478
- const methods = {};
479
- const usedNames = new Set();
480
-
481
- for (const method of service.method ?? []) {
482
- const jsName = uniqueName(lowerCamelIdentifier(method.name), usedNames);
483
- const kind = methodKind(method);
484
- const inputType = stripLeadingDot(method.inputType);
485
- const outputType = stripLeadingDot(method.outputType);
486
- const methodName = `${fullName}.${method.name}`;
487
- const options = method.options?.trevrpcMethod;
488
- methods[jsName] = {
489
- name: method.name,
490
- kind,
491
- inputType,
492
- outputType,
493
- ...unreliableDirections(methodName, kind, options),
494
- ...compressionDirections(methodName, kind, options, { inputType, outputType }, redacted),
495
- };
496
- }
497
-
498
- return {
499
- name: service.name,
500
- fullName,
501
- exportName: `${pascalIdentifier(service.name)}Service`,
502
- methods,
503
- };
504
- }
505
-
506
- /**
507
- * The `(trevrpc.method)` flags of a method as descriptor fields, only for the ones that are set.
508
- * A flag on a direction that does not stream is an error.
509
- */
510
- function unreliableDirections(methodName, kind, options) {
511
- const directions = {};
512
- if (options?.unreliableRequests) {
513
- if (kind !== "clientStreaming" && kind !== "bidirectionalStreaming") {
514
- throw new Error(
515
- `${methodName}: unreliable_requests requires a client-streaming or bidirectional method`,
516
- );
517
- }
518
- directions.unreliableRequests = true;
519
- }
520
- if (options?.unreliableResponses) {
521
- if (kind !== "serverStreaming" && kind !== "bidirectionalStreaming") {
522
- throw new Error(
523
- `${methodName}: unreliable_responses requires a server-streaming or bidirectional method`,
524
- );
525
- }
526
- directions.unreliableResponses = true;
527
- }
528
- return directions;
529
- }
530
-
531
- /** `trevrpc.Compression` values, from proto/trevrpc/options.proto. */
532
- const Compression = { unspecified: 0, message: 1, stream: 2 };
533
-
534
- /**
535
- * The directions a method's `compression` lets compress, as descriptor fields, leaving out message
536
- * types with a field marked `debug_redact`. `COMPRESSION_MESSAGE` compresses messages without a
537
- * dictionary in both directions. `COMPRESSION_STREAM` does too, for the messages outside the
538
- * stream, and stream-compresses the directions that stream; on a unary method it is an error, and
539
- * so is a value this generator does not know.
540
- */
541
- function compressionDirections(methodName, kind, options, { inputType, outputType }, redacted) {
542
- const compression = options?.compression ?? Compression.unspecified;
543
- if (compression === Compression.unspecified) {
544
- return {};
545
- }
546
- if (compression !== Compression.message && compression !== Compression.stream) {
547
- throw new Error(`${methodName}: unknown compression ${compression}`);
548
- }
549
- const directions = {};
550
- if (!redacted.has(inputType)) {
551
- directions.compressRequests = true;
552
- }
553
- if (!redacted.has(outputType)) {
554
- directions.compressResponses = true;
555
- }
556
- if (compression === Compression.message) {
557
- return directions;
558
- }
559
- if (kind === "unary") {
560
- throw new Error(`${methodName}: COMPRESSION_STREAM requires a streaming method`);
561
- }
562
- if (
563
- (kind === "clientStreaming" || kind === "bidirectionalStreaming") &&
564
- !redacted.has(inputType)
565
- ) {
566
- directions.compressStreamRequests = true;
567
- }
568
- if (
569
- (kind === "serverStreaming" || kind === "bidirectionalStreaming") &&
570
- !redacted.has(outputType)
571
- ) {
572
- directions.compressStreamResponses = true;
573
- }
574
- return directions;
575
- }
576
-
577
- /**
578
- * The full names of the message types that contain a field marked `debug_redact`, directly or in
579
- * a message-typed field, however deeply nested.
580
- */
581
- function redactedTypes(files) {
582
- const messages = new Map(
583
- collectMessages(files).map(({ fullName, message }) => [fullName, message]),
584
- );
585
- const redacted = new Set();
586
- const visiting = new Set();
587
- const visit = (name) => {
588
- if (redacted.has(name)) {
589
- return true;
590
- }
591
- const message = messages.get(name);
592
- if (message === undefined || visiting.has(name)) {
593
- return false;
594
- }
595
- visiting.add(name);
596
- const found = (message.field ?? []).some(
597
- (field) =>
598
- field.options?.debugRedact === true ||
599
- (field.typeName != null && visit(stripLeadingDot(field.typeName))),
600
- );
601
- visiting.delete(name);
602
- if (found) {
603
- redacted.add(name);
604
- }
605
- return found;
606
- };
607
- for (const name of messages.keys()) {
608
- visit(name);
609
- }
610
- return redacted;
611
- }
612
-
613
- /**
614
- * The declarations of a generated file: its own messages and enums and every type they and its
615
- * services use, directly or through other messages. `messages` holds each message's `name` (the
616
- * decoded shape) and `initName` (the input shape), `enums` each enum's `name` and `values`, and
617
- * `names` maps full names to them. Well-known types the runtime declares are `shared`: their names
618
- * are reserved first and refer to the runtime's declarations.
619
- */
620
- function declarationModel(files, file, options) {
621
- const usedNames = new Set();
622
- const names = new Map();
623
- const isShared = (fullName) => {
624
- const shared = SharedWellKnownTypes.get(fullName);
625
- return shared !== undefined && (!shared.int64 || options.int64 === "bigint");
626
- };
627
- const used = usedTypes(files, file);
628
- const messages = collectMessages(files)
629
- .filter(({ fullName, message }) => used.has(fullName) && !message.options?.mapEntry)
630
- .map((entry) => ({ ...entry, shared: isShared(entry.fullName) }));
631
- const enums = collectEnums(files)
632
- .filter(({ fullName }) => used.has(fullName))
633
- .map((entry) => ({ ...entry, shared: isShared(entry.fullName) }));
634
- const declared = [...messages, ...enums];
635
-
636
- for (const entry of declared.filter(({ shared }) => shared)) {
637
- entry.name = uniqueName(lastTypeSegment(entry.fullName), usedNames);
638
- }
639
- for (const entry of declared.filter(({ shared }) => !shared)) {
640
- entry.name = uniqueName(pascalIdentifier(lastTypeSegment(entry.fullName)), usedNames);
641
- }
642
- for (const message of messages) {
643
- message.initName = message.shared
644
- ? `${message.name}Init`
645
- : uniqueName(`${message.name}Init`, usedNames);
646
- }
647
- for (const entry of declared) {
648
- names.set(entry.fullName, entry);
649
- }
650
- for (const enumEntry of enums) {
651
- enumEntry.values = enumValues(enumEntry);
652
- }
653
- return { messages, enums, names, int64: options.int64 };
654
- }
655
-
656
- /**
657
- * The full names of the types a generated file declares: the messages and enums `file` defines,
658
- * the request and response types of its services, and every type their fields use, transitively.
659
- */
660
- function usedTypes(files, file) {
661
- const messages = new Map(
662
- collectMessages(files).map(({ fullName, message }) => [fullName, message]),
663
- );
664
- const used = new Set();
665
- const pending = [
666
- ...collectMessages([file]).map(({ fullName }) => fullName),
667
- ...collectEnums([file]).map(({ fullName }) => fullName),
668
- ...(file.service ?? []).flatMap((service) =>
669
- (service.method ?? []).flatMap((method) =>
670
- [method.inputType, method.outputType].map(stripLeadingDot),
671
- ),
672
- ),
673
- ];
674
- while (pending.length > 0) {
675
- const fullName = pending.pop();
676
- if (used.has(fullName)) {
677
- continue;
678
- }
679
- used.add(fullName);
680
- for (const field of messages.get(fullName)?.field ?? []) {
681
- if (field.typeName != null) {
682
- pending.push(stripLeadingDot(field.typeName));
683
- }
684
- }
685
- }
686
- return used;
687
- }
688
-
689
- /**
690
- * The values of an enum, each with the `key` it has in the exported object: its name without the
691
- * enum's prefix (`TIME_INTERVAL_HOUR` of `TimeInterval` is `HOUR`) when every value has that prefix
692
- * and every key that leaves is a distinct identifier, as protobuf-es does; its full name otherwise.
693
- */
694
- function enumValues({ fullName, enumType }) {
695
- const values = (enumType.value ?? []).map((value) => ({
696
- name: value.name ?? "",
697
- number: value.number ?? 0,
698
- }));
699
- const prefix = `${upperSnakeCase(lastTypeSegment(fullName))}_`;
700
- const stripped = values.map(({ name }) => name.slice(prefix.length));
701
- const strip =
702
- values.length > 0 &&
703
- values.every(({ name }) => name.startsWith(prefix)) &&
704
- stripped.every((key) => /^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) &&
705
- new Set(stripped).size === stripped.length;
706
- return values.map((value, index) => ({ ...value, key: strip ? stripped[index] : value.name }));
707
- }
708
-
709
- /** Converts a PascalCase or camelCase name to UPPER_SNAKE_CASE. */
710
- function upperSnakeCase(name) {
711
- return name.replace(/([a-z0-9])([A-Z])/gu, "$1_$2").toUpperCase();
712
- }
713
-
714
- /** Every enum of `files`, top-level and nested in messages, with its full name. */
715
- function collectEnums(files) {
716
- const enums = [];
717
- for (const file of files) {
718
- const scope = file.package ?? "";
719
- for (const enumType of file.enumType ?? []) {
720
- enums.push({ fullName: joinTypeName(scope, enumType.name), enumType });
721
- }
722
- }
723
- for (const { fullName, message } of collectMessages(files)) {
724
- for (const enumType of message.enumType ?? []) {
725
- enums.push({ fullName: joinTypeName(fullName, enumType.name), enumType });
726
- }
727
- }
728
- return enums;
729
- }
730
-
731
- function collectMessages(files) {
732
- const messages = [];
733
- for (const file of files) {
734
- messages.push(...collectFileMessages(file));
735
- }
736
-
737
- return messages;
738
- }
739
-
740
- function collectFileMessages(file) {
741
- const messages = [];
742
- for (const message of file.messageType ?? []) {
743
- collectMessage(messages, joinTypeName(file.package ?? "", message.name), message);
744
- }
745
-
746
- return messages;
747
- }
748
-
749
- function collectMessage(messages, fullName, message) {
750
- messages.push({ fullName, message });
751
-
752
- for (const nested of message.nestedType ?? []) {
753
- collectMessage(messages, joinTypeName(fullName, nested.name), nested);
754
- }
755
- }
756
-
757
- /**
758
- * The two declarations of a message: the decoded shape that calls return and streams yield, in
759
- * which every field reads a value (scalars and enums their default, repeated fields and maps an
760
- * empty array and object, message and `optional` fields null when unset), and the input shape that
761
- * requests take, in which every field may be left out.
762
- */
763
- function generateMessageDeclarations({ fullName, message, name, initName }, context) {
764
- const mapEntries = new Map(
765
- (message.nestedType ?? [])
766
- .filter((nested) => nested.options?.mapEntry)
767
- .map((nested) => [nested.name, nested]),
768
- );
769
- const fields = (shape) =>
770
- (message.field ?? [])
771
- .map((field) => generateFieldDeclaration(field, mapEntries, shape, context))
772
- .join("\n");
773
- const body = (shape) => {
774
- const declared = fields(shape);
775
- return declared === "" ? "{}" : `{\n${declared}\n}`;
776
- };
777
-
778
- return `/** A decoded ${fullName} message. */\nexport interface ${name} ${body("output")}\n\n/** A ${fullName} message to send; every field may be left out. */\nexport interface ${initName} ${body("input")}`;
779
- }
780
-
781
- function generateFieldDeclaration(field, mapEntries, shape, context) {
782
- const optional = shape === "input" && field.label !== FieldLabel.Required ? "?" : "";
783
- const nullable = decodesAbsentAsNull(field) ? " | null" : "";
784
- const type = fieldTypeScriptType(field, mapEntries, shape, context);
785
- return ` ${typescriptPropertyName(fieldJsonName(field))}${optional}: ${type}${nullable};`;
786
- }
787
-
788
- /** Reports whether protobuf.js decodes the field as null when it is absent. */
789
- function decodesAbsentAsNull(field) {
790
- if (field.label === FieldLabel.Repeated) {
791
- return false;
792
- }
793
- return (
794
- field.proto3Optional === true ||
795
- field.type === FieldType.Message ||
796
- field.type === FieldType.Group
797
- );
798
- }
799
-
800
- function fieldTypeScriptType(field, mapEntries, shape, context) {
801
- const mapEntry = mapEntryForField(field, mapEntries);
802
- if (mapEntry != null) {
803
- const [, valueField] = mapEntry.field ?? [];
804
- return `Record<string, ${scalarTypeScriptType(valueField, shape, context)}>`;
805
- }
806
-
807
- const scalarType = scalarTypeScriptType(field, shape, context);
808
- return field.label === FieldLabel.Repeated ? `${arrayElementType(scalarType)}[]` : scalarType;
809
- }
810
-
811
- /** Parenthesizes a union so that it can be an array's element type. */
812
- function arrayElementType(type) {
813
- return type.includes(" | ") ? `(${type})` : type;
814
- }
815
-
816
- function scalarTypeScriptType(field, shape, context) {
817
- switch (field?.type) {
818
- case FieldType.Double:
819
- case FieldType.Float:
820
- case FieldType.Int32:
821
- case FieldType.Uint32:
822
- case FieldType.Sint32:
823
- case FieldType.Fixed32:
824
- case FieldType.Sfixed32:
825
- return "number";
826
- case FieldType.Enum:
827
- return declaredName(field.typeName, "output", context) ?? "number";
828
- case FieldType.Int64:
829
- case FieldType.Uint64:
830
- case FieldType.Sint64:
831
- case FieldType.Fixed64:
832
- case FieldType.Sfixed64:
833
- return int64TypeScriptType(field, shape, context);
834
- case FieldType.Bool:
835
- return "boolean";
836
- case FieldType.String:
837
- return "string";
838
- case FieldType.Bytes:
839
- return "Uint8Array";
840
- case FieldType.Group:
841
- case FieldType.Message:
842
- return messageTypeReference(field.typeName, shape, context);
843
- default:
844
- throw new Error(`unsupported protobuf field type ${field?.type}`);
845
- }
846
- }
847
-
848
- /**
849
- * The type of a 64-bit field: the representation the decoded shape has (the field's `jstype`, or
850
- * the generator's `int64` option), and every representation the encoder accepts in the input.
851
- */
852
- function int64TypeScriptType(field, shape, context) {
853
- const representation = int64Representation(field, context.model.int64);
854
- if (representation === "long") {
855
- context.usesLong = true;
856
- }
857
- if (shape === "input") {
858
- return representation === "long"
859
- ? "bigint | number | string | Long"
860
- : "bigint | number | string";
861
- }
862
- return representation === "long" ? "Long" : representation;
863
- }
864
-
865
- /** How decoded messages represent a 64-bit field: by its `jstype`, else by the `int64` option. */
866
- function int64Representation(field, representation) {
867
- switch (field.options?.jstype) {
868
- case JSType.string:
869
- return "string";
870
- case JSType.number:
871
- return "number";
872
- default:
873
- return representation;
874
- }
875
- }
876
-
877
- /**
878
- * The declared name of a message type in `shape`, `"output"` or `"input"`, noting the runtime
879
- * declarations it uses.
880
- */
881
- function messageTypeReference(typeName, shape, context) {
882
- return declaredName(typeName, shape, context) ?? "Record<string, unknown>";
883
- }
884
-
885
- /**
886
- * The declared name of a message or enum type in `shape` (enums have one for both), or undefined
887
- * for a type without a declaration. A name the runtime declares is noted for import.
888
- */
889
- function declaredName(typeName, shape, context) {
890
- const entry = context.model.names.get(stripLeadingDot(typeName));
891
- if (entry === undefined) {
892
- return undefined;
893
- }
894
- const name = shape === "input" && entry.initName !== undefined ? entry.initName : entry.name;
895
- if (entry.shared) {
896
- context.runtimeTypes.add(name);
897
- }
898
- return name;
899
- }
900
-
901
- function joinTypeName(prefix, name) {
902
- const segment = name ?? "Message";
903
- return prefix === "" || prefix == null ? segment : `${prefix}.${segment}`;
904
- }
905
-
906
- function typescriptPropertyName(name) {
907
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
908
- }
909
-
910
- function methodKind(method) {
911
- if (method.clientStreaming && method.serverStreaming) {
912
- return "bidirectionalStreaming";
913
- }
914
- if (method.clientStreaming) {
915
- return "clientStreaming";
916
- }
917
- if (method.serverStreaming) {
918
- return "serverStreaming";
919
- }
920
- return "unary";
921
- }
922
-
923
- function ensurePackage(root, packageName) {
924
- let scope = root;
925
- for (const part of packageName.split(".")) {
926
- if (part === "") {
927
- continue;
928
- }
929
-
930
- scope.nested ??= {};
931
- scope.nested[part] ??= { nested: {} };
932
- scope = scope.nested[part];
933
- }
934
-
935
- scope.nested ??= {};
936
- return scope;
937
- }
938
-
939
- function messageToJson(message) {
940
- const json = { fields: {} };
941
- const nested = {};
942
- const mapEntries = new Map();
943
-
944
- for (const nestedMessage of message.nestedType ?? []) {
945
- if (nestedMessage.options?.mapEntry) {
946
- mapEntries.set(nestedMessage.name, nestedMessage);
947
- continue;
948
- }
949
-
950
- nested[nestedMessage.name] = messageToJson(nestedMessage);
951
- }
952
-
953
- for (const enumType of message.enumType ?? []) {
954
- nested[enumType.name] = enumToJson(enumType);
955
- }
956
-
957
- for (const field of message.field ?? []) {
958
- json.fields[fieldJsonName(field)] = fieldToJson(field, mapEntries);
959
- }
960
-
961
- const oneofs = oneofsToJson(message);
962
- if (Object.keys(oneofs).length > 0) {
963
- json.oneofs = oneofs;
964
- }
965
-
966
- if (Object.keys(nested).length > 0) {
967
- json.nested = nested;
968
- }
969
-
970
- return json;
971
- }
972
-
973
- function enumToJson(enumType) {
974
- const values = {};
975
- for (const value of enumType.value ?? []) {
976
- values[value.name] = value.number ?? 0;
977
- }
978
-
979
- return { values };
980
- }
981
-
982
- function fieldToJson(field, mapEntries) {
983
- const json = {
984
- type: fieldTypeName(field),
985
- id: field.number ?? 0,
986
- };
987
- const mapEntry = mapEntryForField(field, mapEntries);
988
-
989
- if (mapEntry != null) {
990
- const [keyField, valueField] = mapEntry.field ?? [];
991
- json.keyType = fieldTypeName(keyField);
992
- json.type = fieldTypeName(valueField);
993
- return json;
994
- }
995
-
996
- if (field.label === FieldLabel.Repeated) {
997
- json.rule = "repeated";
998
- } else if (field.label === FieldLabel.Required) {
999
- json.rule = "required";
1000
- }
1001
-
1002
- if (field.defaultValue != null && field.defaultValue !== "") {
1003
- json.options = { default: field.defaultValue };
1004
- }
1005
- if (field.proto3Optional) {
1006
- // protobuf.js gives the field presence, decoding it as null when absent,
1007
- // only when this option marks its synthetic oneof.
1008
- json.options = { ...json.options, proto3_optional: true };
1009
- }
1010
- // The runtime reads it to decode the field as a string or number instead of the root's int64
1011
- // representation.
1012
- if (field.options?.jstype === JSType.string) {
1013
- json.options = { ...json.options, jstype: "JS_STRING" };
1014
- } else if (field.options?.jstype === JSType.number) {
1015
- json.options = { ...json.options, jstype: "JS_NUMBER" };
1016
- }
1017
-
1018
- return json;
1019
- }
1020
-
1021
- function mapEntryForField(field, mapEntries) {
1022
- if (
1023
- field.label !== FieldLabel.Repeated ||
1024
- field.type !== FieldType.Message ||
1025
- field.typeName == null
1026
- ) {
1027
- return null;
1028
- }
1029
-
1030
- return mapEntries.get(lastTypeSegment(field.typeName)) ?? null;
1031
- }
1032
-
1033
- function oneofsToJson(message) {
1034
- const oneofs = {};
1035
- for (const [index, oneof] of (message.oneofDecl ?? []).entries()) {
1036
- const fields = (message.field ?? [])
1037
- // A decoded descriptor reads an unset oneof_index as 0 from its
1038
- // prototype, so only an own property places the field in a oneof.
1039
- .filter((field) => Object.hasOwn(field, "oneofIndex") && field.oneofIndex === index)
1040
- .map(fieldJsonName);
1041
- if (fields.length > 0) {
1042
- oneofs[oneof.name] = { oneof: fields };
1043
- }
1044
- }
1045
-
1046
- return oneofs;
1047
- }
1048
-
1049
- function fieldJsonName(field) {
1050
- return field.jsonName || lowerCamelIdentifier(field.name ?? "field");
1051
- }
1052
-
1053
- function fieldTypeName(field) {
1054
- switch (field?.type) {
1055
- case FieldType.Double:
1056
- return "double";
1057
- case FieldType.Float:
1058
- return "float";
1059
- case FieldType.Int64:
1060
- return "int64";
1061
- case FieldType.Uint64:
1062
- return "uint64";
1063
- case FieldType.Int32:
1064
- return "int32";
1065
- case FieldType.Fixed64:
1066
- return "fixed64";
1067
- case FieldType.Fixed32:
1068
- return "fixed32";
1069
- case FieldType.Bool:
1070
- return "bool";
1071
- case FieldType.String:
1072
- return "string";
1073
- case FieldType.Group:
1074
- case FieldType.Message:
1075
- case FieldType.Enum:
1076
- return stripLeadingDot(field.typeName);
1077
- case FieldType.Bytes:
1078
- return "bytes";
1079
- case FieldType.Uint32:
1080
- return "uint32";
1081
- case FieldType.Sfixed32:
1082
- return "sfixed32";
1083
- case FieldType.Sfixed64:
1084
- return "sfixed64";
1085
- case FieldType.Sint32:
1086
- return "sint32";
1087
- case FieldType.Sint64:
1088
- return "sint64";
1089
- default:
1090
- throw new Error(`unsupported protobuf field type ${field?.type}`);
1091
- }
1092
- }
1093
-
1094
- function outputFileName(inputName, suffix) {
1095
- const extensionStart = inputName.lastIndexOf(".");
1096
- return `${extensionStart === -1 ? inputName : inputName.slice(0, extensionStart)}${suffix}`;
1097
- }
1098
-
1099
- function declarationFileName(outputName) {
1100
- if (outputName.endsWith(".js")) {
1101
- return `${outputName.slice(0, -3)}.d.ts`;
1102
- }
1103
- if (outputName.endsWith(".mjs")) {
1104
- return `${outputName.slice(0, -4)}.d.mts`;
1105
- }
1106
- if (outputName.endsWith(".cjs")) {
1107
- return `${outputName.slice(0, -4)}.d.cts`;
1108
- }
1109
-
1110
- return `${outputName}.d.ts`;
1111
- }
1112
-
1113
- function stripLeadingDot(name) {
1114
- return name?.startsWith(".") ? name.slice(1) : name;
1115
- }
1116
-
1117
- function lastTypeSegment(name) {
1118
- const stripped = stripLeadingDot(name);
1119
- const parts = stripped.split(".");
1120
- return parts[parts.length - 1];
1121
- }
1122
-
1123
- function lowerCamelIdentifier(name) {
1124
- const pascal = pascalIdentifier(name);
1125
- const candidate = `${pascal[0].toLowerCase()}${pascal.slice(1)}`;
1126
- return RESERVED_WORDS.has(candidate) ? `${candidate}_` : candidate;
1127
- }
1128
-
1129
- function pascalIdentifier(name) {
1130
- const words = String(name)
1131
- .split(/[^A-Za-z0-9]+/)
1132
- .filter(Boolean);
1133
- const candidate = words.map((word) => `${word[0].toUpperCase()}${word.slice(1)}`).join("");
1134
- const valid = candidate === "" ? "X" : candidate;
1135
- const prefixed = /^[0-9]/.test(valid) ? `_${valid}` : valid;
1136
- return RESERVED_WORDS.has(prefixed) ? `${prefixed}_` : prefixed;
1137
- }
1138
-
1139
- function uniqueName(name, usedNames) {
1140
- if (!usedNames.has(name)) {
1141
- usedNames.add(name);
1142
- return name;
1143
- }
1144
-
1145
- for (let suffix = 2; ; suffix += 1) {
1146
- const candidate = `${name}${suffix}`;
1147
- if (!usedNames.has(candidate)) {
1148
- usedNames.add(candidate);
1149
- return candidate;
1150
- }
1151
- }
1152
- }