From eb22078f3f345eef96a33803f721ed6d587817f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricard=20Sol=C3=A9?= Date: Tue, 20 Sep 2022 18:38:25 +0200 Subject: [PATCH] feat(options): adds protoc-gen-go-like M option This commit adds a new feature that aims to provide the same functionality as `M` for the go compiler (https://developers.google.com/protocol-buffers/docs/reference/go-generated#package). I opted for not modifying the type map, because we'll also want to figure out whether or not we need to generate the file, so we'll be accessing the `options.M` everywhere and I found it less confusing to always get mapping information from `options.M` instead of `options.M` or the type map depending on the decision that we're making (we need to know if we need to do a relative import or just the raw provided import too). Fixes #596 --- README.markdown | 8 + integration/codegen.ts | 3 + .../google/protobuf/timestamp.ts | 218 +++++++++++++ .../import-mapping/import-mapping-test.ts | 43 +++ integration/import-mapping/mapping.bin | Bin 0 -> 19707 bytes integration/import-mapping/mapping.proto | 29 ++ integration/import-mapping/mapping.ts | 301 ++++++++++++++++++ integration/import-mapping/parameters.txt | 1 + .../some/internal/repo/very_private.bin | Bin 0 -> 271 bytes .../some/internal/repo/very_private.proto | 7 + integration/omit-optionals/simple.bin | Bin 313 -> 313 bytes src/options.ts | 31 +- src/plugin.ts | 12 +- src/utils.ts | 10 +- tests/options-test.ts | 1 + 15 files changed, 650 insertions(+), 14 deletions(-) create mode 100644 integration/import-mapping/google/protobuf/timestamp.ts create mode 100644 integration/import-mapping/import-mapping-test.ts create mode 100644 integration/import-mapping/mapping.bin create mode 100644 integration/import-mapping/mapping.proto create mode 100644 integration/import-mapping/mapping.ts create mode 100644 integration/import-mapping/parameters.txt create mode 100644 integration/import-mapping/some/internal/repo/very_private.bin create mode 100644 integration/import-mapping/some/internal/repo/very_private.proto diff --git a/README.markdown b/README.markdown index 8ce87f3e9..76ec57765 100644 --- a/README.markdown +++ b/README.markdown @@ -450,6 +450,14 @@ Generated code will be placed in the Gradle build directory. - With `--ts_proto_opt=initializeFieldsAsUndefined=false`, all optional field initializers will be omited from the generated base instances. +- With `--ts_proto_opt=Mgoogle/protobuf/empty.proto=./google3/protobuf/empty`, ('M' means 'importMapping', similar to [protoc-gen-go](https://developers.google.com/protocol-buffers/docs/reference/go-generated#package)), the generated code import path for `./google/protobuf/empty.ts` will reflect the overridden value: + + - `Mfoo/bar.proto=@myorg/some-lib` will map `foo/bar.proto` imports into `import ... from '@myorg/some-lib'`. + - `Mfoo/bar.proto=./some/local/lib` will map `foo/bar.proto` imports into `import ... from './some/local/lib'`. + - `Mfoo/bar.proto=some-modules/some-lib` will map `foo/bar.proto` imports into `import ... from 'some-module/some-lib'`. + - **Note**: Uses are accummulated, so multiple values are expected in the form of `--ts_proto_opt=M... --ts_proto_opt=M...` (one `ts_proto_opt` per mapping). + - **Note**: Proto files that match mapped imports **will not be generated**. + ### NestJS Support We have a great way of working together with [nestjs](https://docs.nestjs.com/microservices/grpc). `ts-proto` generates `interfaces` and `decorators` for you controller, client. For more information see the [nestjs readme](NESTJS.markdown). diff --git a/integration/codegen.ts b/integration/codegen.ts index c3015e5a2..900ee1a1f 100644 --- a/integration/codegen.ts +++ b/integration/codegen.ts @@ -33,6 +33,9 @@ async function generate(binFile: string, baseDir: string, parameter: string) { for (let file of request.protoFile) { // Make a different utils per file to track per-file usage + if (options.M[file.name]) { + continue; + } const utils = makeUtils(options); const ctx: Context = { options, typeMap, utils }; const [path, code] = generateFile(ctx, file); diff --git a/integration/import-mapping/google/protobuf/timestamp.ts b/integration/import-mapping/google/protobuf/timestamp.ts new file mode 100644 index 000000000..5497f7183 --- /dev/null +++ b/integration/import-mapping/google/protobuf/timestamp.ts @@ -0,0 +1,218 @@ +/* eslint-disable */ +import * as Long from "long"; +import * as _m0 from "protobufjs/minimal"; + +export const protobufPackage = "google.protobuf"; + +/** + * A Timestamp represents a point in time independent of any time zone or local + * calendar, encoded as a count of seconds and fractions of seconds at + * nanosecond resolution. The count is relative to an epoch at UTC midnight on + * January 1, 1970, in the proleptic Gregorian calendar which extends the + * Gregorian calendar backwards to year one. + * + * All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap + * second table is needed for interpretation, using a [24-hour linear + * smear](https://developers.google.com/time/smear). + * + * The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By + * restricting to that range, we ensure that we can convert to and from [RFC + * 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings. + * + * # Examples + * + * Example 1: Compute Timestamp from POSIX `time()`. + * + * Timestamp timestamp; + * timestamp.set_seconds(time(NULL)); + * timestamp.set_nanos(0); + * + * Example 2: Compute Timestamp from POSIX `gettimeofday()`. + * + * struct timeval tv; + * gettimeofday(&tv, NULL); + * + * Timestamp timestamp; + * timestamp.set_seconds(tv.tv_sec); + * timestamp.set_nanos(tv.tv_usec * 1000); + * + * Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`. + * + * FILETIME ft; + * GetSystemTimeAsFileTime(&ft); + * UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime; + * + * // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z + * // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z. + * Timestamp timestamp; + * timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL)); + * timestamp.set_nanos((INT32) ((ticks % 10000000) * 100)); + * + * Example 4: Compute Timestamp from Java `System.currentTimeMillis()`. + * + * long millis = System.currentTimeMillis(); + * + * Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000) + * .setNanos((int) ((millis % 1000) * 1000000)).build(); + * + * Example 5: Compute Timestamp from Java `Instant.now()`. + * + * Instant now = Instant.now(); + * + * Timestamp timestamp = + * Timestamp.newBuilder().setSeconds(now.getEpochSecond()) + * .setNanos(now.getNano()).build(); + * + * Example 6: Compute Timestamp from current time in Python. + * + * timestamp = Timestamp() + * timestamp.GetCurrentTime() + * + * # JSON Mapping + * + * In JSON format, the Timestamp type is encoded as a string in the + * [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the + * format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z" + * where {year} is always expressed using four digits while {month}, {day}, + * {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional + * seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution), + * are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone + * is required. A proto3 JSON serializer should always use UTC (as indicated by + * "Z") when printing the Timestamp type and a proto3 JSON parser should be + * able to accept both UTC and other timezones (as indicated by an offset). + * + * For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past + * 01:30 UTC on January 15, 2017. + * + * In JavaScript, one can convert a Date object to this format using the + * standard + * [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) + * method. In Python, a standard `datetime.datetime` object can be converted + * to this format using + * [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with + * the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use + * the Joda Time's [`ISODateTimeFormat.dateTime()`]( + * http://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime%2D%2D + * ) to obtain a formatter capable of generating timestamps in this format. + */ +export interface Timestamp { + /** + * Represents seconds of UTC time since Unix epoch + * 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to + * 9999-12-31T23:59:59Z inclusive. + */ + seconds: number; + /** + * Non-negative fractions of a second at nanosecond resolution. Negative + * second values with fractions must still have non-negative nanos values + * that count forward in time. Must be from 0 to 999,999,999 + * inclusive. + */ + nanos: number; +} + +function createBaseTimestamp(): Timestamp { + return { seconds: 0, nanos: 0 }; +} + +export const Timestamp = { + encode(message: Timestamp, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.seconds !== 0) { + writer.uint32(8).int64(message.seconds); + } + if (message.nanos !== 0) { + writer.uint32(16).int32(message.nanos); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): Timestamp { + const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseTimestamp(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.seconds = longToNumber(reader.int64() as Long); + break; + case 2: + message.nanos = reader.int32(); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): Timestamp { + return { + seconds: isSet(object.seconds) ? Number(object.seconds) : 0, + nanos: isSet(object.nanos) ? Number(object.nanos) : 0, + }; + }, + + toJSON(message: Timestamp): unknown { + const obj: any = {}; + message.seconds !== undefined && (obj.seconds = Math.round(message.seconds)); + message.nanos !== undefined && (obj.nanos = Math.round(message.nanos)); + return obj; + }, + + fromPartial, I>>(object: I): Timestamp { + const message = createBaseTimestamp(); + message.seconds = object.seconds ?? 0; + message.nanos = object.nanos ?? 0; + return message; + }, +}; + +declare var self: any | undefined; +declare var window: any | undefined; +declare var global: any | undefined; +var globalThis: any = (() => { + if (typeof globalThis !== "undefined") { + return globalThis; + } + if (typeof self !== "undefined") { + return self; + } + if (typeof window !== "undefined") { + return window; + } + if (typeof global !== "undefined") { + return global; + } + throw "Unable to locate global object"; +})(); + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends Array ? Array> : T extends ReadonlyArray ? ReadonlyArray> + : T extends {} ? { [K in keyof T]?: DeepPartial } + : Partial; + +type KeysOfUnion = T extends T ? keyof T : never; +export type Exact = P extends Builtin ? P + : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; + +function longToNumber(long: Long): number { + if (long.gt(Number.MAX_SAFE_INTEGER)) { + throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER"); + } + return long.toNumber(); +} + +// If you get a compile-error about 'Constructor and ... have no overlap', +// add '--ts_proto_opt=esModuleInterop=true' as a flag when calling 'protoc'. +if (_m0.util.Long !== Long) { + _m0.util.Long = Long as any; + _m0.configure(); +} + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} diff --git a/integration/import-mapping/import-mapping-test.ts b/integration/import-mapping/import-mapping-test.ts new file mode 100644 index 000000000..d2a4ce410 --- /dev/null +++ b/integration/import-mapping/import-mapping-test.ts @@ -0,0 +1,43 @@ +import * as path from "node:path"; +import * as fs from "node:fs"; +import * as ts from "typescript"; + +describe("import-mapping", () => { + it("generates imports correctly", () => { + const generatedPath = path.join(__dirname, "mapping.ts"); + const generatedCode = fs.readFileSync(generatedPath, "utf8"); + const source = ts.createSourceFile(generatedPath, generatedCode, ts.ScriptTarget.ES2018); + + const actualImports = collectImports(source).map((imp) => imp.moduleSpecifier.getText(source).replace(/"/g, "")); + const expectedImports = [ + "@google/protobuf/duration", + "@google/protobuf/empty", + "wkt/google/protobuf/struct", + "./google/protobuf/timestamp", + "protobufjs/minimal", + "@myorg/proto-npm-package", + ]; + expect(actualImports.sort()).toEqual(expectedImports.sort()); + + expect(fs.existsSync(path.join("google", "protobuf", "duration.ts"))).toBeFalsy(); + expect(fs.existsSync(path.join("google", "protobuf", "empty.ts"))).toBeFalsy(); + expect(fs.existsSync(path.join("google", "protobuf", "struct.ts"))).toBeFalsy(); + expect(fs.existsSync(path.join("some", "internal", "repo", "very_private.ts"))).toBeFalsy(); + }); +}); + +function collectImports(node: ts.Node) { + let nodes: ts.ImportDeclaration[] = []; + if (isImportDeclaration(node)) { + nodes.push(node); + } else { + node.forEachChild((node) => { + nodes = [...nodes, ...collectImports(node)]; + }); + } + return nodes; +} + +function isImportDeclaration(node: ts.Node): node is ts.ImportDeclaration { + return node.kind === ts.SyntaxKind.ImportDeclaration; +} diff --git a/integration/import-mapping/mapping.bin b/integration/import-mapping/mapping.bin new file mode 100644 index 0000000000000000000000000000000000000000..28d0434e139369d296e27172d0b00ad32f4eccff GIT binary patch literal 19707 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 A)Bpeg literal 0 HcmV?d00001 diff --git a/integration/import-mapping/mapping.proto b/integration/import-mapping/mapping.proto new file mode 100644 index 000000000..7c3b3a27b --- /dev/null +++ b/integration/import-mapping/mapping.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; + +package import_mapping; + +import "google/protobuf/duration.proto"; // mapped to @google/... +import "google/protobuf/empty.proto"; // mapped to @google/... +import "google/protobuf/struct.proto"; // mapped to wkt/... +import "google/protobuf/timestamp.proto"; // not mapped +import "some/internal/repo/very_private.proto"; // mapped to @myorg/proto-npm-package + +message WithEmtpy { + google.protobuf.Empty empty = 1; +} + +message WithStruct { + google.protobuf.Struct strut = 1; +} + +message WithTimestamp { + google.protobuf.Timestamp timestamp = 1; +} + +message WithAll { + google.protobuf.Empty empty = 1; + google.protobuf.Struct strut = 2; + google.protobuf.Timestamp timestamp = 3; + google.protobuf.Duration duration = 4; + myorg.protos.VeryVerySecret very_very_secret = 5; +} diff --git a/integration/import-mapping/mapping.ts b/integration/import-mapping/mapping.ts new file mode 100644 index 000000000..c049e15a7 --- /dev/null +++ b/integration/import-mapping/mapping.ts @@ -0,0 +1,301 @@ +/* eslint-disable */ +import { Duration } from "@google/protobuf/duration"; +import { Empty } from "@google/protobuf/empty"; +import { VeryVerySecret } from "@myorg/proto-npm-package"; +import * as _m0 from "protobufjs/minimal"; +import { Struct } from "wkt/google/protobuf/struct"; +import { Timestamp } from "./google/protobuf/timestamp"; + +export const protobufPackage = "import_mapping"; + +export interface WithEmtpy { + empty: Empty | undefined; +} + +export interface WithStruct { + strut: { [key: string]: any } | undefined; +} + +export interface WithTimestamp { + timestamp: Date | undefined; +} + +export interface WithAll { + empty: Empty | undefined; + strut: { [key: string]: any } | undefined; + timestamp: Date | undefined; + duration: Duration | undefined; + veryVerySecret: VeryVerySecret | undefined; +} + +function createBaseWithEmtpy(): WithEmtpy { + return { empty: undefined }; +} + +export const WithEmtpy = { + encode(message: WithEmtpy, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.empty !== undefined) { + Empty.encode(message.empty, writer.uint32(10).fork()).ldelim(); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): WithEmtpy { + const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseWithEmtpy(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.empty = Empty.decode(reader, reader.uint32()); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): WithEmtpy { + return { empty: isSet(object.empty) ? Empty.fromJSON(object.empty) : undefined }; + }, + + toJSON(message: WithEmtpy): unknown { + const obj: any = {}; + message.empty !== undefined && (obj.empty = message.empty ? Empty.toJSON(message.empty) : undefined); + return obj; + }, + + fromPartial, I>>(object: I): WithEmtpy { + const message = createBaseWithEmtpy(); + message.empty = (object.empty !== undefined && object.empty !== null) ? Empty.fromPartial(object.empty) : undefined; + return message; + }, +}; + +function createBaseWithStruct(): WithStruct { + return { strut: undefined }; +} + +export const WithStruct = { + encode(message: WithStruct, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.strut !== undefined) { + Struct.encode(Struct.wrap(message.strut), writer.uint32(10).fork()).ldelim(); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): WithStruct { + const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseWithStruct(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.strut = Struct.unwrap(Struct.decode(reader, reader.uint32())); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): WithStruct { + return { strut: isObject(object.strut) ? object.strut : undefined }; + }, + + toJSON(message: WithStruct): unknown { + const obj: any = {}; + message.strut !== undefined && (obj.strut = message.strut); + return obj; + }, + + fromPartial, I>>(object: I): WithStruct { + const message = createBaseWithStruct(); + message.strut = object.strut ?? undefined; + return message; + }, +}; + +function createBaseWithTimestamp(): WithTimestamp { + return { timestamp: undefined }; +} + +export const WithTimestamp = { + encode(message: WithTimestamp, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.timestamp !== undefined) { + Timestamp.encode(toTimestamp(message.timestamp), writer.uint32(10).fork()).ldelim(); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): WithTimestamp { + const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseWithTimestamp(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.timestamp = fromTimestamp(Timestamp.decode(reader, reader.uint32())); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): WithTimestamp { + return { timestamp: isSet(object.timestamp) ? fromJsonTimestamp(object.timestamp) : undefined }; + }, + + toJSON(message: WithTimestamp): unknown { + const obj: any = {}; + message.timestamp !== undefined && (obj.timestamp = message.timestamp.toISOString()); + return obj; + }, + + fromPartial, I>>(object: I): WithTimestamp { + const message = createBaseWithTimestamp(); + message.timestamp = object.timestamp ?? undefined; + return message; + }, +}; + +function createBaseWithAll(): WithAll { + return { empty: undefined, strut: undefined, timestamp: undefined, duration: undefined, veryVerySecret: undefined }; +} + +export const WithAll = { + encode(message: WithAll, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.empty !== undefined) { + Empty.encode(message.empty, writer.uint32(10).fork()).ldelim(); + } + if (message.strut !== undefined) { + Struct.encode(Struct.wrap(message.strut), writer.uint32(18).fork()).ldelim(); + } + if (message.timestamp !== undefined) { + Timestamp.encode(toTimestamp(message.timestamp), writer.uint32(26).fork()).ldelim(); + } + if (message.duration !== undefined) { + Duration.encode(message.duration, writer.uint32(34).fork()).ldelim(); + } + if (message.veryVerySecret !== undefined) { + VeryVerySecret.encode(message.veryVerySecret, writer.uint32(42).fork()).ldelim(); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): WithAll { + const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseWithAll(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.empty = Empty.decode(reader, reader.uint32()); + break; + case 2: + message.strut = Struct.unwrap(Struct.decode(reader, reader.uint32())); + break; + case 3: + message.timestamp = fromTimestamp(Timestamp.decode(reader, reader.uint32())); + break; + case 4: + message.duration = Duration.decode(reader, reader.uint32()); + break; + case 5: + message.veryVerySecret = VeryVerySecret.decode(reader, reader.uint32()); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): WithAll { + return { + empty: isSet(object.empty) ? Empty.fromJSON(object.empty) : undefined, + strut: isObject(object.strut) ? object.strut : undefined, + timestamp: isSet(object.timestamp) ? fromJsonTimestamp(object.timestamp) : undefined, + duration: isSet(object.duration) ? Duration.fromJSON(object.duration) : undefined, + veryVerySecret: isSet(object.veryVerySecret) ? VeryVerySecret.fromJSON(object.veryVerySecret) : undefined, + }; + }, + + toJSON(message: WithAll): unknown { + const obj: any = {}; + message.empty !== undefined && (obj.empty = message.empty ? Empty.toJSON(message.empty) : undefined); + message.strut !== undefined && (obj.strut = message.strut); + message.timestamp !== undefined && (obj.timestamp = message.timestamp.toISOString()); + message.duration !== undefined && (obj.duration = message.duration ? Duration.toJSON(message.duration) : undefined); + message.veryVerySecret !== undefined && + (obj.veryVerySecret = message.veryVerySecret ? VeryVerySecret.toJSON(message.veryVerySecret) : undefined); + return obj; + }, + + fromPartial, I>>(object: I): WithAll { + const message = createBaseWithAll(); + message.empty = (object.empty !== undefined && object.empty !== null) ? Empty.fromPartial(object.empty) : undefined; + message.strut = object.strut ?? undefined; + message.timestamp = object.timestamp ?? undefined; + message.duration = (object.duration !== undefined && object.duration !== null) + ? Duration.fromPartial(object.duration) + : undefined; + message.veryVerySecret = (object.veryVerySecret !== undefined && object.veryVerySecret !== null) + ? VeryVerySecret.fromPartial(object.veryVerySecret) + : undefined; + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends Array ? Array> : T extends ReadonlyArray ? ReadonlyArray> + : T extends {} ? { [K in keyof T]?: DeepPartial } + : Partial; + +type KeysOfUnion = T extends T ? keyof T : never; +export type Exact = P extends Builtin ? P + : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; + +function toTimestamp(date: Date): Timestamp { + const seconds = date.getTime() / 1_000; + const nanos = (date.getTime() % 1_000) * 1_000_000; + return { seconds, nanos }; +} + +function fromTimestamp(t: Timestamp): Date { + let millis = t.seconds * 1_000; + millis += t.nanos / 1_000_000; + return new Date(millis); +} + +function fromJsonTimestamp(o: any): Date { + if (o instanceof Date) { + return o; + } else if (typeof o === "string") { + return new Date(o); + } else { + return fromTimestamp(Timestamp.fromJSON(o)); + } +} + +function isObject(value: any): boolean { + return typeof value === "object" && value !== null; +} + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} diff --git a/integration/import-mapping/parameters.txt b/integration/import-mapping/parameters.txt new file mode 100644 index 000000000..74bb5913f --- /dev/null +++ b/integration/import-mapping/parameters.txt @@ -0,0 +1 @@ +Mgoogle/protobuf/duration.proto=@google/protobuf/duration,Mgoogle/protobuf/empty.proto=@google/protobuf/empty,Mgoogle/protobuf/struct.proto=wkt/google/protobuf/struct,Mfoo/bar/baz.proto=@unused/bar,Msome/internal/repo/very_private.proto=@myorg/proto-npm-package diff --git a/integration/import-mapping/some/internal/repo/very_private.bin b/integration/import-mapping/some/internal/repo/very_private.bin new file mode 100644 index 0000000000000000000000000000000000000000..bd7bb4cb2260842e697f649ff059b6f8492841a6 GIT binary patch literal 271 zcma)%%?iRW5QH~PTIyItR+L&0L=ZgLgKyzc!KEjEj s.split("=")); - pairs.forEach(([key, _value]) => { - const value = _value === "true" ? true : _value === "false" ? false : _value; - if (options[key]) { + parameter.split(",").forEach((param) => { + // same as protoc-gen-go https://github.com/protocolbuffers/protobuf-go/blob/bf9455640daabb98c93b5b5e71628f3f813d57bb/compiler/protogen/protogen.go#L168-L171 + const optionSeparatorPos = param.indexOf("="); + const key = param.substring(0, optionSeparatorPos); + const value = parseParamValue(param.substring(optionSeparatorPos + 1)); + if (key.charAt(0) === "M") { + if (typeof value !== "string") { + console.warn(`ignoring invalid M option: '${param}'`); + } else { + const mKey = key.substring(1); + if (options.M[mKey]) { + console.warn(`received conflicting M options: '${param}' will override 'M${mKey}=${options.M[mKey]}'`); + } + if (param.endsWith(".ts")) { + console.warn(`received M option '${param}' ending in '.ts' this is usually a mistake`); + } + options.M[mKey] = value; + } + } else if (options[key]) { options[key] = [options[key], value]; } else { options[key] = value; @@ -217,6 +236,10 @@ function parseParameter(parameter: string): Options { return options; } +function parseParamValue(value: string): string | boolean { + return value === "true" ? true : value === "false" ? false : value; +} + export function getTsPoetOpts(_options: Options): ToStringOpts { const imports = ["protobufjs/minimal" + _options.importSuffix]; return { diff --git a/src/plugin.ts b/src/plugin.ts index c9e1ed181..dd84b07f6 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -26,11 +26,13 @@ async function main() { const filesToGenerate = options.emitImportedFiles ? request.protoFile : protoFilesToGenerate(request); const files = await Promise.all( - filesToGenerate.map(async (file) => { - const [path, code] = generateFile(ctx, file); - const content = code.toString({ ...getTsPoetOpts(options), path }); - return { name: path, content }; - }) + filesToGenerate + .filter((file) => !options.M[file.name]) + .map(async (file) => { + const [path, code] = generateFile(ctx, file); + const content = code.toString({ ...getTsPoetOpts(options), path }); + return { name: path, content }; + }) ); if (options.outputTypeRegistry) { diff --git a/src/utils.ts b/src/utils.ts index 6f22d05f2..11ebe1920 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -210,10 +210,10 @@ export function impFile(options: Options, spec: string) { } export function impProto(options: Options, module: string, type: string): Import { - const importString = `${type}@./${module}${options.fileSuffix}${options.importSuffix}`; - if (options.onlyTypes) { - return imp("t:" + importString); - } else { - return imp(importString); + const prefix = options.onlyTypes ? "t:" : ""; + const protoFile = `${module}.proto`; + if (options.M[protoFile]) { + return imp(`${prefix}${type}@${options.M[protoFile]}`); } + return imp(`${prefix}${type}@./${module}${options.fileSuffix}${options.importSuffix}`); } diff --git a/tests/options-test.ts b/tests/options-test.ts index 46d9564b7..8835aa9db 100644 --- a/tests/options-test.ts +++ b/tests/options-test.ts @@ -4,6 +4,7 @@ describe("options", () => { it("can set outputJsonMethods with nestJs=true", () => { expect(optionsFromParameter("nestJs=true,outputJsonMethods=true")).toMatchInlineSnapshot(` Object { + "M": Object {}, "addGrpcMetadata": false, "addNestjsRestParameter": false, "constEnums": false,