diff --git a/packages/protoc-gen-es/src/declaration.ts b/packages/protoc-gen-es/src/declaration.ts index 43522073a..0c4952dd6 100644 --- a/packages/protoc-gen-es/src/declaration.ts +++ b/packages/protoc-gen-es/src/declaration.ts @@ -124,7 +124,7 @@ function generateOneof(schema: Schema, f: GeneratedFile, oneof: DescOneof) { f.print(` } | {`); } f.print(f.jsDoc(field, " ")); - const { typing } = getFieldTypeInfo(field, f); + const { typing } = getFieldTypeInfo(field); f.print(` value: `, typing, `;`); f.print(` case: "`, localName(field), `";`); } @@ -135,7 +135,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, f); + const { typing, optional } = getFieldTypeInfo(field); if (!optional) { e.push(" ", localName(field), ": ", typing, ";"); } else { @@ -150,7 +150,7 @@ function generateExtension( f: GeneratedFile, ext: DescExtension, ) { - const { typing } = getFieldTypeInfo(ext, f); + const { typing } = getFieldTypeInfo(ext); f.print(f.jsDoc(ext)); f.print(f.exportDecl("declare const", ext), ": ", schema.runtime.Extension, "<", ext.extendee, ", ", typing, ">;"); f.print(); @@ -230,7 +230,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, f); + const {typing} = getFieldTypeInfo(ref.value); 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/javascript.ts b/packages/protoc-gen-es/src/javascript.ts index a6c3776e8..0c98838fc 100644 --- a/packages/protoc-gen-es/src/javascript.ts +++ b/packages/protoc-gen-es/src/javascript.ts @@ -169,10 +169,7 @@ export function getFieldInfoLiteral(schema: Schema, field: DescField | DescExten } else if (field.proto.label === FieldDescriptorProto_Label.REQUIRED) { e.push(`req: true, `); } - const defaultValue = getFieldDefaultValueExpression(field, { - enumAs: "enum_value_ref", - protoInt64Symbol: schema.runtime.protoInt64, - }); + const defaultValue = getFieldDefaultValueExpression(field); if (defaultValue !== undefined) { e.push(`default: `, defaultValue, `, `); } diff --git a/packages/protoc-gen-es/src/typescript.ts b/packages/protoc-gen-es/src/typescript.ts index e577370c2..0d2b7bc90 100644 --- a/packages/protoc-gen-es/src/typescript.ts +++ b/packages/protoc-gen-es/src/typescript.ts @@ -148,7 +148,7 @@ function generateOneof(schema: Schema, f: GeneratedFile, oneof: DescOneof) { f.print(` } | {`); } f.print(f.jsDoc(field, " ")); - const { typing } = getFieldTypeInfo(field, f); + const { typing } = getFieldTypeInfo(field); f.print(` value: `, typing, `;`); f.print(` case: "`, localName(field), `";`); } @@ -159,7 +159,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, f); + const { typing, optional, typingInferrableFromZeroValue } = getFieldTypeInfo(field); if (optional) { e.push(" ", localName(field), "?: ", typing, ";"); } else { @@ -168,10 +168,7 @@ function generateField(schema: Schema, f: GeneratedFile, field: DescField) { } else { e.push(" ", localName(field), ": ", typing); } - const zeroValue = getFieldZeroValueExpression(field, { - enumAs: "enum_value_ref", - protoInt64Symbol: schema.runtime.protoInt64, - }); + const zeroValue = getFieldZeroValueExpression(field); if (zeroValue !== undefined) { e.push(" = ", zeroValue); } @@ -187,7 +184,7 @@ function generateExtension( ext: DescExtension, ) { const protoN = getNonEditionRuntime(schema, ext.file); - const { typing } = getFieldTypeInfo(ext, f); + const { typing } = getFieldTypeInfo(ext); f.print(f.jsDoc(ext)); f.print(f.exportDecl("const", ext), " = ", protoN, ".makeExtension<", ext.extendee, ", ", typing, ">("); f.print(" ", f.string(ext.typeName), ", "); @@ -654,7 +651,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, f); + const {typing} = getFieldTypeInfo(ref.value); 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 309d2e6c5..5fa1f5a6a 100644 --- a/packages/protoc-gen-es/src/util.ts +++ b/packages/protoc-gen-es/src/util.ts @@ -14,26 +14,18 @@ import { codegenInfo, - DescEnum, DescEnumValue, DescExtension, DescField, - DescMessage, FieldDescriptorProto_Label, LongType, - protoInt64, ScalarType, + ScalarValue, } from "@bufbuild/protobuf"; import type { Printable } from "@bufbuild/protoplugin/ecmascript"; -import { literalString, localName } from "@bufbuild/protoplugin/ecmascript"; -import type { ImportSymbol } from "@bufbuild/protoplugin/src/ecmascript"; +import { localName } from "@bufbuild/protoplugin/ecmascript"; -export function getFieldTypeInfo( - field: DescField | DescExtension, - opt: { - import: (desc: DescEnum | DescMessage) => ImportSymbol; - }, -): { +export function getFieldTypeInfo(field: DescField | DescExtension): { typing: Printable; optional: boolean; typingInferrableFromZeroValue: boolean; @@ -54,14 +46,22 @@ export function getFieldTypeInfo( if (baseType !== undefined) { typing.push(scalarTypeScriptType(baseType, LongType.BIGINT)); } else { - typing.push(opt.import(field.message).toTypeOnly()); + typing.push({ + kind: "es_ref_message", + type: field.message, + typeOnly: true, + }); } optional = true; typingInferrableFromZeroValue = true; break; } case "enum": - typing.push(opt.import(field.enum).toTypeOnly()); + typing.push({ + kind: "es_ref_enum", + type: field.enum, + typeOnly: true, + }); optional = field.optional || field.proto.label === FieldDescriptorProto_Label.REQUIRED; @@ -81,7 +81,7 @@ export function getFieldTypeInfo( keyType = "string"; break; } - let valueType; + let valueType: Printable; switch (field.mapValue.kind) { case "scalar": valueType = scalarTypeScriptType( @@ -90,10 +90,18 @@ export function getFieldTypeInfo( ); break; case "message": - valueType = opt.import(field.mapValue.message).toTypeOnly(); + valueType = { + kind: "es_ref_message", + type: field.mapValue.message, + typeOnly: true, + }; break; case "enum": - valueType = opt.import(field.mapValue.enum).toTypeOnly(); + valueType = { + kind: "es_ref_enum", + type: field.mapValue.enum, + typeOnly: true, + }; break; } typing.push("{ [key: ", keyType, "]: ", valueType, " }"); @@ -110,25 +118,16 @@ export function getFieldTypeInfo( return { typing, optional, typingInferrableFromZeroValue }; } -type GetFieldExpressionOptions = - | { - enumAs: "enum_value_ref" | "enum_value_integer"; - importEnum?: (desc: DescEnum) => ImportSymbol; - protoInt64Symbol: ImportSymbol; - } - | { - enumAs: "enum_value_integer_as_ref"; - importEnum: (desc: DescEnum) => ImportSymbol; - protoInt64Symbol: ImportSymbol; - }; - /** * Return a printable expression for the default value of a field. * Only applicable for singular scalar and enum fields. */ export function getFieldDefaultValueExpression( field: DescField | DescExtension, - opt: GetFieldExpressionOptions, + enumAs: + | "enum_value_as_is" + | "enum_value_as_integer" + | "enum_value_as_cast_integer" = "enum_value_as_is", ): Printable | undefined { if (field.repeated) { return undefined; @@ -150,10 +149,10 @@ export function getFieldDefaultValueExpression( `invalid enum default value: ${String(defaultValue)} for ${enumValue}`, ); } - return literalEnumValue(enumValue, opt); + return literalEnumValue(enumValue, enumAs); } case "scalar": - return literalScalarValue(defaultValue, field, opt.protoInt64Symbol); + return literalScalarValue(defaultValue, field); } } @@ -169,7 +168,10 @@ export function getFieldDefaultValueExpression( */ export function getFieldZeroValueExpression( field: DescField | DescExtension, - opt: GetFieldExpressionOptions, + enumAs: + | "enum_value_as_is" + | "enum_value_as_integer" + | "enum_value_as_cast_integer" = "enum_value_as_is", ): Printable | undefined { if (field.repeated) { return "[]"; @@ -186,22 +188,21 @@ export function getFieldZeroValueExpression( throw new Error("invalid enum: missing at least one value"); } const zeroValue = field.enum.values[0]; - return literalEnumValue(zeroValue, opt); + return literalEnumValue(zeroValue, enumAs); } case "scalar": { - const defaultValue = codegenInfo.scalarDefaultValue( + const defaultValue = codegenInfo.scalarZeroValue( field.scalar, field.longType, - ) as string | boolean | number | bigint | Uint8Array; - return literalScalarValue(defaultValue, field, opt.protoInt64Symbol); + ); + return literalScalarValue(defaultValue, field); } } } function literalScalarValue( - value: string | number | bigint | boolean | Uint8Array, + value: ScalarValue, field: (DescField | DescExtension) & { fieldKind: "scalar" }, - protoInt64Symbol: ImportSymbol, ): Printable { switch (field.scalar) { case ScalarType.DOUBLE: @@ -230,7 +231,7 @@ function literalScalarValue( `Unexpected value for ${ScalarType[field.scalar]} ${field.toString()}: ${String(value)}`, ); } - return literalString(value); + return { kind: "es_string", value }; case ScalarType.BYTES: if (!(value instanceof Uint8Array)) { throw new Error( @@ -243,36 +244,35 @@ function literalScalarValue( case ScalarType.SFIXED64: case ScalarType.UINT64: case ScalarType.FIXED64: - switch (typeof value) { - case "bigint": - if (value == protoInt64.zero) { - return [protoInt64Symbol, ".zero"]; - } - switch (field.scalar) { - case ScalarType.UINT64: - case ScalarType.FIXED64: - return [protoInt64Symbol, `.uParse("${value.toString()}")`]; - default: - return [protoInt64Symbol, `.parse("${value.toString()}")`]; - } - case "string": - return literalString(value); - default: - throw new Error( - `Unexpected value for ${ScalarType[field.scalar]} ${field.toString()}: ${String(value)}`, - ); + if (typeof value != "bigint" && typeof value != "string") { + throw new Error( + `Unexpected value for ${ScalarType[field.scalar]} ${field.toString()}: ${String(value)}`, + ); } + return { + kind: "es_proto_int64", + type: field.scalar, + longType: field.longType, + value, + }; } } function literalEnumValue( value: DescEnumValue, - opt: GetFieldExpressionOptions, + enumAs: + | "enum_value_as_is" + | "enum_value_as_integer" + | "enum_value_as_cast_integer", ): Printable { - switch (opt.enumAs) { - case "enum_value_ref": - return [value.parent, ".", localName(value)]; - case "enum_value_integer": + switch (enumAs) { + case "enum_value_as_is": + return [ + { kind: "es_ref_enum", type: value.parent, typeOnly: false }, + ".", + localName(value), + ]; + case "enum_value_as_integer": return [ value.number, " /* ", @@ -281,11 +281,11 @@ function literalEnumValue( value.name, " */", ]; - case "enum_value_integer_as_ref": + case "enum_value_as_cast_integer": return [ value.number, " as ", - opt.importEnum(value.parent).toTypeOnly(), + { kind: "es_ref_enum", type: value.parent, typeOnly: true }, ".", localName(value), ]; diff --git a/packages/protoplugin-test/src/file-es_proto_int64.test.ts b/packages/protoplugin-test/src/file-es_proto_int64.test.ts new file mode 100644 index 000000000..66131c678 --- /dev/null +++ b/packages/protoplugin-test/src/file-es_proto_int64.test.ts @@ -0,0 +1,139 @@ +// Copyright 2021-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { describe, expect, test } from "@jest/globals"; +import { createTestPluginAndRun } from "./helpers"; +import type { GeneratedFile, Schema } from "@bufbuild/protoplugin/ecmascript"; +import { LongType, ScalarType } from "@bufbuild/protobuf"; + +describe("file print", () => { + describe(`"es_proto_int64`, () => { + test("should honor LongType.STRING", async () => { + const lines = await testGenerate((f) => { + f.print({ + kind: "es_proto_int64", + type: ScalarType.INT64, + longType: LongType.STRING, + value: 123n, + }); + }); + expect(lines).toStrictEqual([`"123"`]); + }); + + test("should honor LongType.STRING for 0", async () => { + const lines = await testGenerate((f) => { + f.print({ + kind: "es_proto_int64", + type: ScalarType.INT64, + longType: LongType.STRING, + value: 0n, + }); + }); + expect(lines).toStrictEqual([`"0"`]); + }); + + test("should honor LongType.STRING for string value", async () => { + const lines = await testGenerate((f) => { + f.print({ + kind: "es_proto_int64", + type: ScalarType.INT64, + longType: LongType.STRING, + value: "123", + }); + }); + expect(lines).toStrictEqual([`"123"`]); + }); + + const signedTypes = [ + ScalarType.INT64, + ScalarType.SINT64, + ScalarType.SFIXED64, + ] as const; + for (const t of signedTypes) { + test(`should use protoInt64.zero for ${ScalarType[t]}`, async () => { + const lines = await testGenerate((f) => { + f.print({ + kind: "es_proto_int64", + type: t, + longType: LongType.BIGINT, + value: 0n, + }); + }); + expect(lines).toStrictEqual([ + `import { protoInt64 } from "@bufbuild/protobuf";`, + ``, + `protoInt64.zero`, + ]); + }); + test(`should use protoInt64.parse for ${ScalarType[t]}`, async () => { + const lines = await testGenerate((f) => { + f.print({ + kind: "es_proto_int64", + type: t, + longType: LongType.BIGINT, + value: 123n, + }); + }); + expect(lines).toStrictEqual([ + `import { protoInt64 } from "@bufbuild/protobuf";`, + ``, + `protoInt64.parse("123")`, + ]); + }); + } + + const unsignedTypes = [ScalarType.UINT64, ScalarType.FIXED64] as const; + for (const t of unsignedTypes) { + test(`should use protoInt64.zero for ${ScalarType[t]}`, async () => { + const lines = await testGenerate((f) => { + f.print({ + kind: "es_proto_int64", + type: t, + longType: LongType.BIGINT, + value: 0n, + }); + }); + expect(lines).toStrictEqual([ + `import { protoInt64 } from "@bufbuild/protobuf";`, + ``, + `protoInt64.zero`, + ]); + }); + test(`should use protoInt64.uParse for ${ScalarType[t]}`, async () => { + const lines = await testGenerate((f) => { + f.print({ + kind: "es_proto_int64", + type: t, + longType: LongType.BIGINT, + value: 123n, + }); + }); + expect(lines).toStrictEqual([ + `import { protoInt64 } from "@bufbuild/protobuf";`, + ``, + `protoInt64.uParse("123")`, + ]); + }); + } + }); + + async function testGenerate(opt: (f: GeneratedFile, schema: Schema) => void) { + return createTestPluginAndRun({ + proto: `syntax="proto3";`, + parameter: "target=ts", + generateAny: opt, + returnLinesOfFirstFile: true, + }); + } +}); diff --git a/packages/protoplugin-test/src/file-es_ref_enum.test.ts b/packages/protoplugin-test/src/file-es_ref_enum.test.ts new file mode 100644 index 000000000..c8f3a3143 --- /dev/null +++ b/packages/protoplugin-test/src/file-es_ref_enum.test.ts @@ -0,0 +1,60 @@ +// Copyright 2021-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { describe, expect, test } from "@jest/globals"; +import { createTestPluginAndRun } from "./helpers"; +import type { GeneratedFile, Schema } from "@bufbuild/protoplugin/ecmascript"; + +describe("file print", () => { + test(`should print "es_ref_enum" Printable`, async () => { + const lines = await testGenerate((f, schema) => { + f.print({ + kind: "es_ref_enum", + type: schema.files[0].enums[0], + typeOnly: false, + }); + }); + expect(lines).toStrictEqual([ + `import { Foo } from "./x_pb.js";`, + ``, + `Foo`, + ]); + }); + test(`should print "es_ref_enum" Printable type-only`, async () => { + const lines = await testGenerate((f, schema) => { + f.print({ + kind: "es_ref_enum", + type: schema.files[0].enums[0], + typeOnly: true, + }); + }); + expect(lines).toStrictEqual([ + `import type { Foo } from "./x_pb.js";`, + ``, + `Foo`, + ]); + }); + + async function testGenerate(opt: (f: GeneratedFile, schema: Schema) => void) { + return createTestPluginAndRun({ + proto: ` + syntax="proto3"; + enum Foo { FOO_UNSPECIFIED = 0; } + `, + parameter: "target=ts", + generateAny: opt, + returnLinesOfFirstFile: true, + }); + } +}); diff --git a/packages/protoplugin-test/src/file-es_ref_message.test.ts b/packages/protoplugin-test/src/file-es_ref_message.test.ts new file mode 100644 index 000000000..7c386b068 --- /dev/null +++ b/packages/protoplugin-test/src/file-es_ref_message.test.ts @@ -0,0 +1,60 @@ +// Copyright 2021-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { describe, expect, test } from "@jest/globals"; +import { createTestPluginAndRun } from "./helpers"; +import type { GeneratedFile, Schema } from "@bufbuild/protoplugin/ecmascript"; + +describe("file print", () => { + test(`should print "es_ref_message" Printable`, async () => { + const lines = await testGenerate((f, schema) => { + f.print({ + kind: "es_ref_message", + type: schema.files[0].messages[0], + typeOnly: false, + }); + }); + expect(lines).toStrictEqual([ + `import { Person } from "./x_pb.js";`, + ``, + `Person`, + ]); + }); + test(`should print "es_ref_message" Printable type-only`, async () => { + const lines = await testGenerate((f, schema) => { + f.print({ + kind: "es_ref_message", + type: schema.files[0].messages[0], + typeOnly: true, + }); + }); + expect(lines).toStrictEqual([ + `import type { Person } from "./x_pb.js";`, + ``, + `Person`, + ]); + }); + + async function testGenerate(opt: (f: GeneratedFile, schema: Schema) => void) { + return createTestPluginAndRun({ + proto: ` + syntax="proto3"; + message Person {} + `, + parameter: "target=ts", + generateAny: opt, + returnLinesOfFirstFile: true, + }); + } +}); diff --git a/packages/protoplugin-test/src/file-es_string.test.ts b/packages/protoplugin-test/src/file-es_string.test.ts new file mode 100644 index 000000000..2da6a7c3c --- /dev/null +++ b/packages/protoplugin-test/src/file-es_string.test.ts @@ -0,0 +1,38 @@ +// Copyright 2021-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { describe, expect, test } from "@jest/globals"; +import { createTestPluginAndRun } from "./helpers"; +import type { GeneratedFile, Schema } from "@bufbuild/protoplugin/ecmascript"; + +describe("file print", () => { + test(`should print "es_string" Printable`, async () => { + const lines = await testGenerate((f) => { + f.print({ + kind: "es_string", + value: `ab"c`, + }); + }); + expect(lines).toStrictEqual([`"ab\\"c"`]); + }); + + async function testGenerate(opt: (f: GeneratedFile, schema: Schema) => void) { + return createTestPluginAndRun({ + proto: `syntax="proto3";`, + parameter: "target=ts", + generateAny: opt, + returnLinesOfFirstFile: true, + }); + } +}); diff --git a/packages/protoplugin/src/ecmascript/generated-file.ts b/packages/protoplugin/src/ecmascript/generated-file.ts index 739426706..44078a7c8 100644 --- a/packages/protoplugin/src/ecmascript/generated-file.ts +++ b/packages/protoplugin/src/ecmascript/generated-file.ts @@ -12,22 +12,30 @@ // See the License for the specific language governing permissions and // limitations under the License. -import type { +import { AnyDesc, DescEnum, DescExtension, DescFile, DescMessage, + LongType, + protoInt64, + ScalarType, } from "@bufbuild/protobuf"; import type { ImportSymbol } from "./import-symbol.js"; import { createImportSymbol } from "./import-symbol.js"; -import { literalString } from "./legacy-gencommon.js"; import type { RuntimeImports } from "./runtime-imports.js"; import { makeImportPathRelative } from "./import-path.js"; -import type { ExportDeclaration } from "./export-declaration.js"; -import { createExportDeclaration } from "./export-declaration.js"; import type { JSDocBlock } from "./jsdoc.js"; import { createJsDocBlock } from "./jsdoc.js"; +import { + createExportDeclaration, + ExportDeclaration, + LiteralProtoInt64, + LiteralString, + RefDescEnum, + RefDescMessage, +} from "./opaque-printables"; /** * All types that can be passed to GeneratedFile.print() @@ -41,6 +49,10 @@ export type Printable = | ImportSymbol | ExportDeclaration | JSDocBlock + | LiteralString + | LiteralProtoInt64 + | RefDescMessage + | RefDescEnum | DescMessage | DescEnum | DescExtension @@ -218,7 +230,8 @@ export function createGeneratedFile( return createExportDeclaration(declaration, name); }, string(string) { - return literalString(string); + // We do not use LiteralString, which was added later, to maintain backwards compatibility + return escapeString(string); }, jsDoc(textOrDesc, indentation) { return createJsDocBlock(textOrDesc, indentation); @@ -279,16 +292,16 @@ function elToContent( alias == undefined ? name : `${name}: ${alias}`, ); const what = `{ ${p.join(", ")} }`; - c.push(`const ${what} = require(${literalString(from)});\n`); + c.push(`const ${what} = require(${escapeString(from)});\n`); } else { const p = names.map(({ name, alias }) => alias == undefined ? name : `${name} as ${alias}`, ); const what = `{ ${p.join(", ")} }`; if (typeOnly) { - c.push(`import type ${what} from ${literalString(from)};\n`); + c.push(`import type ${what} from ${escapeString(from)};\n`); } else { - c.push(`import ${what} from ${literalString(from)};\n`); + c.push(`import ${what} from ${escapeString(from)};\n`); } } }, @@ -349,17 +362,17 @@ function printableToEl( el.push(p); break; case "number": - el.push(...literalNumber(p, runtimeImports)); + elNumber(el, p, runtimeImports); break; case "boolean": el.push(p.toString()); break; case "bigint": - el.push(...literalBigint(p, runtimeImports)); + elBigint(el, p, runtimeImports); break; case "object": if (p instanceof Uint8Array) { - el.push(literalUint8Array(p)); + elUint8Array(el, p); break; } switch (p.kind) { @@ -369,6 +382,12 @@ function printableToEl( case "es_jsdoc": el.push(p.toString()); break; + case "es_string": + el.push(escapeString(p.value)); + break; + case "es_proto_int64": + elProtoInt64(el, p, runtimeImports); + break; case "es_export_decl": el.push({ kind: "es_export_stmt", @@ -379,6 +398,14 @@ function printableToEl( : createTypeImport(p.name).name, }); break; + case "es_ref_message": + case "es_ref_enum": + el.push( + p.typeOnly + ? createTypeImport(p.type).toTypeOnly() + : createTypeImport(p.type), + ); + break; case "message": case "extension": case "enum": @@ -537,42 +564,99 @@ function processImports( return symbolToIdentifier; } -function literalNumber( - value: number, +function elBigint( + el: El[], + value: bigint, runtimeImports: RuntimeImports, -): El[] | string { - if (Number.isNaN(value)) { - return [runtimeImports.protoDouble, ".NaN"]; - } - if (value === Number.POSITIVE_INFINITY) { - return [runtimeImports.protoDouble, ".POSITIVE_INFINITY"]; - } - if (value === Number.NEGATIVE_INFINITY) { - return [runtimeImports.protoDouble, ".NEGATIVE_INFINITY"]; +): void { + if (value == protoInt64.zero) { + // Loose comparison will match between 0n and 0. + el.push(runtimeImports.protoInt64, ".zero"); + } else { + el.push( + runtimeImports.protoInt64, + value > 0 ? ".uParse(" : ".parse(", + escapeString(value.toString()), + ")", + ); } - return value.toString(10); } -function literalBigint(value: bigint, runtimeImports: RuntimeImports): El[] { - // Loose comparison will match between 0n and 0. - if (value == (0 as unknown as bigint)) { - return [runtimeImports.protoInt64, ".zero"]; +function elNumber( + el: El[], + value: number, + runtimeImports: RuntimeImports, +): void { + if (Number.isNaN(value)) { + el.push(runtimeImports.protoDouble, ".NaN"); + } else if (value === Number.POSITIVE_INFINITY) { + el.push(runtimeImports.protoDouble, ".POSITIVE_INFINITY"); + } else if (value === Number.NEGATIVE_INFINITY) { + el.push(runtimeImports.protoDouble, ".NEGATIVE_INFINITY"); + } else { + el.push(value.toString(10)); } - return [ - runtimeImports.protoInt64, - value > 0 ? ".uParse(" : ".parse(", - literalString(value.toString()), - ")", - ]; } -function literalUint8Array(value: Uint8Array): string { +function elUint8Array(el: El[], value: Uint8Array): void { if (value.length === 0) { - return "new Uint8Array(0)"; + el.push("new Uint8Array(0)"); + return; } + el.push("new Uint8Array(["); const strings: string[] = []; for (const n of value) { strings.push("0x" + n.toString(16).toUpperCase().padStart(2, "0")); } - return `new Uint8Array([${strings.join(", ")}])`; + el.push(strings.join(", ")); + el.push("])"); +} + +function elProtoInt64( + el: El[], + literal: LiteralProtoInt64, + runtimeImports: RuntimeImports, +): void { + switch (literal.longType) { + case LongType.STRING: + el.push(escapeString(literal.value.toString())); + break; + case LongType.BIGINT: + if (literal.value == protoInt64.zero) { + // Loose comparison will match between 0n and 0. + el.push(runtimeImports.protoInt64, ".zero"); + break; + } + switch (literal.type) { + case ScalarType.UINT64: + case ScalarType.FIXED64: + el.push(runtimeImports.protoInt64); + el.push(".uParse("); + el.push(escapeString(literal.value.toString())); + el.push(")"); + break; + default: + el.push(runtimeImports.protoInt64); + el.push(".parse("); + el.push(escapeString(literal.value.toString())); + el.push(")"); + break; + } + } +} + +function escapeString(value: string): string { + return ( + '"' + + value + .split("\\") + .join("\\\\") + .split('"') + .join('\\"') + .split("\r") + .join("\\r") + .split("\n") + .join("\\n") + + '"' + ); } diff --git a/packages/protoplugin/src/ecmascript/index.ts b/packages/protoplugin/src/ecmascript/index.ts index 284be3d22..a89da5a14 100644 --- a/packages/protoplugin/src/ecmascript/index.ts +++ b/packages/protoplugin/src/ecmascript/index.ts @@ -20,7 +20,6 @@ import { } from "@bufbuild/protobuf"; import { Printable } from "./generated-file.js"; import { createJsDocBlock as createJsDocBlockInternal } from "./jsdoc.js"; -import { literalString as literalStringInternal } from "./legacy-gencommon.js"; export { reifyWkt } from "./reify-wkt.js"; export type { Target } from "./target.js"; @@ -36,20 +35,14 @@ export { getFieldExplicitDefaultValue, getFieldIntrinsicDefaultValue, getFieldTyping, + literalString, } from "./legacy-gencommon.js"; export { findCustomScalarOption, findCustomMessageOption, findCustomEnumOption, -} from "./custom-options.js"; - -/** - * @deprecated Please use GeneratedFile.string() instead - */ -export function literalString(value: string): string { - return literalStringInternal(value); -} +} from "./legacy-custom-options.js"; /** * @deprecated Please use GeneratedFile.jsDoc() instead diff --git a/packages/protoplugin/src/ecmascript/custom-options.ts b/packages/protoplugin/src/ecmascript/legacy-custom-options.ts similarity index 100% rename from packages/protoplugin/src/ecmascript/custom-options.ts rename to packages/protoplugin/src/ecmascript/legacy-custom-options.ts diff --git a/packages/protoplugin/src/ecmascript/legacy-gencommon.ts b/packages/protoplugin/src/ecmascript/legacy-gencommon.ts index e0221fbdd..8d83ce16b 100644 --- a/packages/protoplugin/src/ecmascript/legacy-gencommon.ts +++ b/packages/protoplugin/src/ecmascript/legacy-gencommon.ts @@ -24,6 +24,9 @@ import type { ImportSymbol } from "./import-symbol.js"; const { localName, getUnwrappedFieldType, scalarZeroValue } = codegenInfo; +/** + * @deprecated Please use GeneratedFile.string() instead + */ export function literalString(value: string): string { return ( '"' + diff --git a/packages/protoplugin/src/ecmascript/export-declaration.ts b/packages/protoplugin/src/ecmascript/opaque-printables.ts similarity index 59% rename from packages/protoplugin/src/ecmascript/export-declaration.ts rename to packages/protoplugin/src/ecmascript/opaque-printables.ts index 5eedb71a8..495d25837 100644 --- a/packages/protoplugin/src/ecmascript/export-declaration.ts +++ b/packages/protoplugin/src/ecmascript/opaque-printables.ts @@ -12,7 +12,42 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { DescEnum, DescExtension, DescMessage } from "@bufbuild/protobuf"; +import { + DescEnum, + DescExtension, + DescMessage, + LongType, + ScalarType, +} from "@bufbuild/protobuf"; + +export type LiteralProtoInt64 = { + readonly kind: "es_proto_int64"; + type: + | ScalarType.INT64 + | ScalarType.SINT64 + | ScalarType.SFIXED64 + | ScalarType.UINT64 + | ScalarType.FIXED64; + longType: LongType; + value: bigint | string; +}; + +export type LiteralString = { + readonly kind: "es_string"; + value: string; +}; + +export type RefDescMessage = { + readonly kind: "es_ref_message"; + type: DescMessage; + typeOnly: boolean; +}; + +export type RefDescEnum = { + readonly kind: "es_ref_enum"; + type: DescEnum; + typeOnly: boolean; +}; export type ExportDeclaration = { readonly kind: "es_export_decl";