diff --git a/packages/protobuf/src/codegen-info.ts b/packages/protobuf/src/codegen-info.ts index fd737ecd9..1f325678d 100644 --- a/packages/protobuf/src/codegen-info.ts +++ b/packages/protobuf/src/codegen-info.ts @@ -75,6 +75,7 @@ type RuntimeSymbolName = | "proto3" | "Message" | "PartialMessage" + | "PartialStrictMessage" | "PlainMessage" | "FieldList" | "MessageType" @@ -116,6 +117,7 @@ export const codegenInfo: CodegenInfo = { proto3: {typeOnly: false, privateImportPath: "./proto3.js", publicImportPath: packageName}, Message: {typeOnly: false, privateImportPath: "./message.js", publicImportPath: packageName}, PartialMessage: {typeOnly: true, privateImportPath: "./message.js", publicImportPath: packageName}, + PartialStrictMessage: {typeOnly: true, privateImportPath: "./message.js", publicImportPath: packageName}, PlainMessage: {typeOnly: true, privateImportPath: "./message.js", publicImportPath: packageName}, FieldList: {typeOnly: true, privateImportPath: "./field-list.js", publicImportPath: packageName}, MessageType: {typeOnly: true, privateImportPath: "./message-type.js", publicImportPath: packageName}, diff --git a/packages/protobuf/src/index.ts b/packages/protobuf/src/index.ts index 0a07feab7..add914e53 100644 --- a/packages/protobuf/src/index.ts +++ b/packages/protobuf/src/index.ts @@ -21,7 +21,12 @@ export { protoDelimited } from "./proto-delimited.js"; export { codegenInfo } from "./codegen-info.js"; export { Message } from "./message.js"; -export type { AnyMessage, PartialMessage, PlainMessage } from "./message.js"; +export type { + AnyMessage, + PartialMessage, + PartialStrictMessage, + PlainMessage, +} from "./message.js"; export { isMessage } from "./is-message.js"; export type { FieldInfo, OneofInfo } from "./field.js"; diff --git a/packages/protobuf/src/message.ts b/packages/protobuf/src/message.ts index cc9d3a1f1..d78c37a53 100644 --- a/packages/protobuf/src/message.ts +++ b/packages/protobuf/src/message.ts @@ -204,6 +204,22 @@ export type PartialMessage> = { [P in keyof T as T[P] extends Function ? never : P]?: PartialField; }; +export type PartialStrictMessage> = { + // eslint-disable-next-line @typescript-eslint/ban-types -- we use `Function` to identify methods + [P in keyof T as T[P] extends Function ? never : P]: PartialStrictField; +}; + +// prettier-ignore +type PartialStrictField = + F extends (Date | Uint8Array | bigint | boolean | string | number) ? F + : F extends Array ? Array> + : F extends ReadonlyArray ? ReadonlyArray> + : F extends Message ? PartialStrictMessage + : F extends OneofSelectedMessage ? {case: C; value: PartialStrictMessage} + : F extends { case: string | undefined; value?: unknown; } ? F + : F extends {[key: string|number]: Message} ? {[key: string|number]: PartialStrictMessage} + : F ; + // prettier-ignore type PartialField = F extends (Date | Uint8Array | bigint | boolean | string | number) ? F diff --git a/packages/protoc-gen-es/src/declaration.ts b/packages/protoc-gen-es/src/declaration.ts index 9c648891d..b0f2e3634 100644 --- a/packages/protoc-gen-es/src/declaration.ts +++ b/packages/protoc-gen-es/src/declaration.ts @@ -64,10 +64,11 @@ function generateEnum(schema: Schema, f: GeneratedFile, enumeration: DescEnum) { function generateMessage(schema: Schema, f: GeneratedFile, message: DescMessage) { const protoN = getNonEditionRuntime(schema, message.file); const { - PartialMessage, FieldList, Message, PlainMessage, + PartialStrictMessage, + PartialMessage, BinaryReadOptions, JsonReadOptions, JsonValue @@ -86,7 +87,7 @@ function generateMessage(schema: Schema, f: GeneratedFile, message: DescMessage) } f.print(); } - f.print(" constructor(data?: ", PartialMessage, "<", m, ">);"); + f.print(" constructor(data?: ", schema.strict ? PartialStrictMessage : PartialMessage, "<", m, ">);"); f.print(); generateWktMethods(schema, f, message); f.print(" static readonly runtime: typeof ", protoN, ";"); @@ -125,7 +126,7 @@ function generateOneof(schema: Schema, f: GeneratedFile, oneof: DescOneof) { f.print(` } | {`); } f.print(f.jsDoc(field, " ")); - const { typing } = getFieldTypeInfo(field); + const { typing } = getFieldTypeInfo(field, schema.strict); f.print(` value: `, typing, `;`); f.print(` case: "`, localName(field), `";`); } @@ -136,7 +137,7 @@ function generateOneof(schema: Schema, f: GeneratedFile, oneof: DescOneof) { function generateField(schema: Schema, f: GeneratedFile, field: DescField) { f.print(f.jsDoc(field, " ")); const e: Printable = []; - const { typing, optional } = getFieldTypeInfo(field); + const { typing, optional } = getFieldTypeInfo(field, schema.strict); if (!optional) { e.push(" ", localName(field), ": ", typing, ";"); } else { @@ -151,7 +152,7 @@ function generateExtension( f: GeneratedFile, ext: DescExtension, ) { - const { typing } = getFieldTypeInfo(ext); + const { typing } = getFieldTypeInfo(ext, schema.strict); const e = f.import(ext.extendee).toTypeOnly(); f.print(f.jsDoc(ext)); f.print(f.exportDecl("declare const", localName(ext)), ": ", schema.runtime.Extension, "<", e, ", ", typing, ">;"); @@ -232,7 +233,7 @@ function generateWktStaticMethods(schema: Schema, f: GeneratedFile, message: Des case "google.protobuf.BoolValue": case "google.protobuf.StringValue": case "google.protobuf.BytesValue": { - const {typing} = getFieldTypeInfo(ref.value); + const {typing} = getFieldTypeInfo(ref.value, schema.strict); f.print(" static readonly fieldWrapper: {") f.print(" wrapField(value: ", typing, "): ", message, ",") f.print(" unwrapField(value: ", message, "): ", typing, ",") diff --git a/packages/protoc-gen-es/src/typescript.ts b/packages/protoc-gen-es/src/typescript.ts index 0d2b7bc90..3f2a66af7 100644 --- a/packages/protoc-gen-es/src/typescript.ts +++ b/packages/protoc-gen-es/src/typescript.ts @@ -74,6 +74,7 @@ function generateMessage(schema: Schema, f: GeneratedFile, message: DescMessage) const protoN = getNonEditionRuntime(schema, message.file); const { PartialMessage, + PartialStrictMessage, FieldList, Message, PlainMessage, @@ -94,7 +95,7 @@ function generateMessage(schema: Schema, f: GeneratedFile, message: DescMessage) } f.print(); } - f.print(" constructor(data?: ", PartialMessage, "<", message, ">) {"); + f.print(" constructor(data?: ", schema.strict ? PartialStrictMessage : PartialMessage, "<", message, ">) {"); f.print(" super();"); f.print(" ", protoN, ".util.initPartial(data, this);"); f.print(" }"); @@ -148,7 +149,7 @@ function generateOneof(schema: Schema, f: GeneratedFile, oneof: DescOneof) { f.print(` } | {`); } f.print(f.jsDoc(field, " ")); - const { typing } = getFieldTypeInfo(field); + const { typing } = getFieldTypeInfo(field, schema.strict); f.print(` value: `, typing, `;`); f.print(` case: "`, localName(field), `";`); } @@ -159,7 +160,7 @@ function generateOneof(schema: Schema, f: GeneratedFile, oneof: DescOneof) { function generateField(schema: Schema, f: GeneratedFile, field: DescField) { f.print(f.jsDoc(field, " ")); const e: Printable = []; - const { typing, optional, typingInferrableFromZeroValue } = getFieldTypeInfo(field); + const { typing, optional, typingInferrableFromZeroValue } = getFieldTypeInfo(field, schema.strict); if (optional) { e.push(" ", localName(field), "?: ", typing, ";"); } else { @@ -184,7 +185,7 @@ function generateExtension( ext: DescExtension, ) { const protoN = getNonEditionRuntime(schema, ext.file); - const { typing } = getFieldTypeInfo(ext); + const { typing } = getFieldTypeInfo(ext, schema.strict); f.print(f.jsDoc(ext)); f.print(f.exportDecl("const", ext), " = ", protoN, ".makeExtension<", ext.extendee, ", ", typing, ">("); f.print(" ", f.string(ext.typeName), ", "); @@ -651,7 +652,7 @@ function generateWktStaticMethods(schema: Schema, f: GeneratedFile, message: Des case "google.protobuf.BoolValue": case "google.protobuf.StringValue": case "google.protobuf.BytesValue": { - const {typing} = getFieldTypeInfo(ref.value); + const {typing} = getFieldTypeInfo(ref.value, schema.strict); f.print(" static readonly fieldWrapper = {") f.print(" wrapField(value: ", typing, "): ", message, " {") f.print(" return new ", message, "({value});") diff --git a/packages/protoc-gen-es/src/util.ts b/packages/protoc-gen-es/src/util.ts index 5fa1f5a6a..5a63d2045 100644 --- a/packages/protoc-gen-es/src/util.ts +++ b/packages/protoc-gen-es/src/util.ts @@ -25,7 +25,10 @@ import { import type { Printable } from "@bufbuild/protoplugin/ecmascript"; import { localName } from "@bufbuild/protoplugin/ecmascript"; -export function getFieldTypeInfo(field: DescField | DescExtension): { +export function getFieldTypeInfo( + field: DescField | DescExtension, + strict: boolean, +): { typing: Printable; optional: boolean; typingInferrableFromZeroValue: boolean; @@ -38,7 +41,7 @@ export function getFieldTypeInfo(field: DescField | DescExtension): { typing.push(scalarTypeScriptType(field.scalar, field.longType)); optional = field.optional || - field.proto.label === FieldDescriptorProto_Label.REQUIRED; + (!strict && field.proto.label === FieldDescriptorProto_Label.REQUIRED); typingInferrableFromZeroValue = true; break; case "message": { @@ -64,7 +67,7 @@ export function getFieldTypeInfo(field: DescField | DescExtension): { }); optional = field.optional || - field.proto.label === FieldDescriptorProto_Label.REQUIRED; + (!strict && field.proto.label === FieldDescriptorProto_Label.REQUIRED); typingInferrableFromZeroValue = true; break; case "map": { diff --git a/packages/protoplugin/src/ecmascript/parameter.ts b/packages/protoplugin/src/ecmascript/parameter.ts index 3ae8bda2f..979fbb592 100644 --- a/packages/protoplugin/src/ecmascript/parameter.ts +++ b/packages/protoplugin/src/ecmascript/parameter.ts @@ -25,6 +25,7 @@ export interface ParsedParameter { importExtension: string; jsImportStyle: "module" | "legacy_commonjs"; sanitizedParameter: string; + strict: boolean; } export function parseParameter( @@ -38,6 +39,7 @@ export function parseParameter( const rewriteImports: RewriteImports = []; let importExtension = ".js"; let jsImportStyle: "module" | "legacy_commonjs" = "module"; + let strict = false; const rawParameters: string[] = []; for (const { key, value, raw } of splitParameter(parameter)) { // Whether this key/value plugin parameter pair should be @@ -135,6 +137,21 @@ export function parseParameter( } break; } + case "strict": { + switch (value) { + case "true": + case "1": + strict = true; + break; + case "false": + case "0": + strict = false; + break; + default: + throw new PluginOptionError(raw); + } + break; + } default: if (parseExtraOption === undefined) { throw new PluginOptionError(raw); @@ -154,6 +171,7 @@ export function parseParameter( const sanitizedParameter = rawParameters.join(","); return { + strict, targets, tsNocheck, bootstrapWkt, diff --git a/packages/protoplugin/src/ecmascript/runtime-imports.ts b/packages/protoplugin/src/ecmascript/runtime-imports.ts index 28fed57f1..43699a142 100644 --- a/packages/protoplugin/src/ecmascript/runtime-imports.ts +++ b/packages/protoplugin/src/ecmascript/runtime-imports.ts @@ -21,6 +21,7 @@ export interface RuntimeImports { proto3: ImportSymbol; Message: ImportSymbol; PartialMessage: ImportSymbol; + PartialStrictMessage: ImportSymbol; PlainMessage: ImportSymbol; FieldList: ImportSymbol; MessageType: ImportSymbol; @@ -47,6 +48,7 @@ export function createRuntimeImports(bootstrapWkt: boolean): RuntimeImports { proto3: infoToSymbol("proto3", bootstrapWkt), Message: infoToSymbol("Message", bootstrapWkt), PartialMessage: infoToSymbol("PartialMessage", bootstrapWkt), + PartialStrictMessage: infoToSymbol("PartialStrictMessage", bootstrapWkt), PlainMessage: infoToSymbol("PlainMessage", bootstrapWkt), FieldList: infoToSymbol("FieldList", bootstrapWkt), MessageType: infoToSymbol("MessageType", bootstrapWkt), diff --git a/packages/protoplugin/src/ecmascript/schema.ts b/packages/protoplugin/src/ecmascript/schema.ts index a9994d849..ca2114e3a 100644 --- a/packages/protoplugin/src/ecmascript/schema.ts +++ b/packages/protoplugin/src/ecmascript/schema.ts @@ -78,6 +78,11 @@ export interface Schema { * The original google.protobuf.compiler.CodeGeneratorRequest. */ readonly proto: CodeGeneratorRequest; + + /** + * strict mode with `required` support + */ + readonly strict: boolean; } interface SchemaController extends Schema { @@ -120,6 +125,7 @@ export function createSchema( const generatedFiles: GeneratedFileController[] = []; return { targets: parameter.targets, + strict: parameter.strict, runtime, proto: request, files: filesToGenerate,