diff --git a/README.markdown b/README.markdown index 3f43bc0a9..98453b1ac 100644 --- a/README.markdown +++ b/README.markdown @@ -509,6 +509,10 @@ Generated code will be placed in the Gradle build directory. Extension encode/decode methods are compliant with the `outputEncodeMethods` option, and if `unknownFields=true`, the `setExtension` and `getExtension` methods will be created for extendable messages, also compliant with `outputEncodeMethods` (setExtension = encode, getExtension = decode). +- With `--ts_proto_opt=outputIndex=true`, index files will be generated based on the proto package namespaces. + + This will disable `exportCommonSymbols` to avoid name collisions on the common symbols. + ### 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 900ee1a1f..529c27f36 100644 --- a/integration/codegen.ts +++ b/integration/codegen.ts @@ -4,7 +4,7 @@ import { parse } from "path"; import { promisify } from "util"; import { generateFile, makeUtils } from "../src/main"; import { createTypeMap } from "../src/types"; -import { prefixDisableLinter } from "../src/utils"; +import { generateIndexFiles } from "../src/utils"; import { getTsPoetOpts, optionsFromParameter } from "../src/options"; import { Context } from "../src/context"; import { generateTypeRegistry } from "../src/generate-type-registry"; @@ -56,6 +56,15 @@ async function generate(binFile: string, baseDir: string, parameter: string) { await promisify(writeFile)(filePath, code.toString({ ...getTsPoetOpts(options), path })); } + + if (options.outputIndex) { + for (const [path, code] of generateIndexFiles(request.protoFile, options)) { + const filePath = `${baseDir}/${path}`; + const dirPath = parse(filePath).dir; + await promisify(mkdir)(dirPath, { recursive: true }).catch(() => {}); + await promisify(writeFile)(filePath, code.toString({ ...getTsPoetOpts(options), path })); + } + } } main().then(() => { diff --git a/integration/output-index/a-test.ts b/integration/output-index/a-test.ts new file mode 100644 index 000000000..a81065ced --- /dev/null +++ b/integration/output-index/a-test.ts @@ -0,0 +1,14 @@ +import * as Index from '.'; + +describe('output-index', () => { + it('generates index files correctly', () => { + expect(Index).toMatchObject({ + base: { + A: { + encode: expect.any(Function), + decode: expect.any(Function), + }, + }, + }); + }); +}); diff --git a/integration/output-index/a.bin b/integration/output-index/a.bin new file mode 100644 index 000000000..1d87fd207 Binary files /dev/null and b/integration/output-index/a.bin differ diff --git a/integration/output-index/a.proto b/integration/output-index/a.proto new file mode 100644 index 000000000..d3dac7534 --- /dev/null +++ b/integration/output-index/a.proto @@ -0,0 +1,6 @@ +syntax = "proto3"; +package base; + +message A { + string a = 1; +} diff --git a/integration/output-index/a.ts b/integration/output-index/a.ts new file mode 100644 index 000000000..7e806a84a --- /dev/null +++ b/integration/output-index/a.ts @@ -0,0 +1,77 @@ +/* eslint-disable */ +import * as _m0 from "protobufjs/minimal"; + +export interface A { + a: string; +} + +function createBaseA(): A { + return { a: "" }; +} + +export const A = { + encode(message: A, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.a !== "") { + writer.uint32(10).string(message.a); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): A { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseA(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.a = reader.string(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): A { + return { a: isSet(object.a) ? String(object.a) : "" }; + }, + + toJSON(message: A): unknown { + const obj: any = {}; + message.a !== undefined && (obj.a = message.a); + return obj; + }, + + create, I>>(base?: I): A { + return A.fromPartial(base ?? {}); + }, + + fromPartial, I>>(object: I): A { + const message = createBaseA(); + message.a = object.a ?? ""; + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +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; +type Exact = P extends Builtin ? P + : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} diff --git a/integration/output-index/index.base.ts b/integration/output-index/index.base.ts new file mode 100644 index 000000000..b75bfa97f --- /dev/null +++ b/integration/output-index/index.base.ts @@ -0,0 +1,3 @@ +/* eslint-disable */ + +export * from "./a"; diff --git a/integration/output-index/index.ts b/integration/output-index/index.ts new file mode 100644 index 000000000..c41c2e7e5 --- /dev/null +++ b/integration/output-index/index.ts @@ -0,0 +1,3 @@ +/* eslint-disable */ + +export * as base from "./index.base"; diff --git a/integration/output-index/parameters.txt b/integration/output-index/parameters.txt new file mode 100644 index 000000000..855f60601 --- /dev/null +++ b/integration/output-index/parameters.txt @@ -0,0 +1 @@ +outputIndex=true \ No newline at end of file diff --git a/src/options.ts b/src/options.ts index 513d27095..925496534 100644 --- a/src/options.ts +++ b/src/options.ts @@ -80,6 +80,7 @@ export type Options = { useReadonlyTypes: boolean; useSnakeTypeName: boolean; outputExtensions: boolean; + outputIndex: boolean; M: { [from: string]: string }; }; @@ -130,6 +131,7 @@ export function defaultOptions(): Options { useReadonlyTypes: false, useSnakeTypeName: true, outputExtensions: false, + outputIndex: false, M: {}, }; } @@ -218,6 +220,10 @@ export function optionsFromParameter(parameter: string | undefined): Options { options.initializeFieldsAsUndefined = false; } + if (options.outputIndex) { + options.exportCommonSymbols = false; + } + return options; } diff --git a/src/plugin.ts b/src/plugin.ts index dd84b07f6..580f6b1e2 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,11 +1,6 @@ -import { - CodeGeneratorRequest, - CodeGeneratorResponse, - CodeGeneratorResponse_Feature, - FileDescriptorProto, -} from "ts-proto-descriptors"; +import { CodeGeneratorRequest, CodeGeneratorResponse, CodeGeneratorResponse_Feature } from "ts-proto-descriptors"; import { promisify } from "util"; -import { prefixDisableLinter, protoFilesToGenerate, readToBuffer } from "./utils"; +import { generateIndexFiles, protoFilesToGenerate, readToBuffer } from "./utils"; import { generateFile, makeUtils } from "./main"; import { createTypeMap } from "./types"; import { Context } from "./context"; @@ -46,6 +41,13 @@ async function main() { files.push({ name: path, content }); } + if (options.outputIndex) { + for (const [path, code] of generateIndexFiles(filesToGenerate, options)) { + const content = code.toString({ ...getTsPoetOpts(options), path }); + files.push({ name: path, content }); + } + } + const response = CodeGeneratorResponse.fromPartial({ file: files, supportedFeatures: CodeGeneratorResponse_Feature.FEATURE_PROTO3_OPTIONAL, diff --git a/src/utils.ts b/src/utils.ts index 9ba1a5956..df6e55008 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,5 @@ -import { code, Code, imp, Import } from "ts-poet"; +import * as path from "path"; +import { code, Code, imp, Import, joinCode } from "ts-poet"; import { CodeGeneratorRequest, FieldDescriptorProto, @@ -15,6 +16,48 @@ export function protoFilesToGenerate(request: CodeGeneratorRequest): FileDescrip return request.protoFile.filter((f) => request.fileToGenerate.includes(f.name)); } +type PackageTree = { + index: string; + chunks: Code[]; + leaves: { [k: string]: PackageTree }; +}; +export function generateIndexFiles(files: FileDescriptorProto[], options: Options): [string, Code][] { + const packageTree: PackageTree = { + index: "index.ts", + leaves: {}, + chunks: [], + }; + for (const { name, package: pkg } of files) { + const moduleName = name.replace(".proto", options.fileSuffix); + const pkgParts = pkg.length > 0 ? pkg.split(".") : []; + + const branch = pkgParts.reduce((branch, part, i): PackageTree => { + if (!(part in branch.leaves)) { + const prePkgParts = pkgParts.slice(0, i + 1); + const index = `index.${prePkgParts.join(".")}.ts`; + branch.chunks.push(code`export * as ${part} from "./${path.basename(index, ".ts")}";`); + branch.leaves[part] = { + index, + leaves: {}, + chunks: [], + }; + } + return branch.leaves[part]; + }, packageTree); + branch.chunks.push(code`export * from "./${moduleName}";`); + } + + const indexFiles: [string, Code][] = []; + let branches: PackageTree[] = [packageTree]; + let currentBranch; + while ((currentBranch = branches.pop())) { + indexFiles.push([currentBranch.index, joinCode(currentBranch.chunks)]); + branches.push(...Object.values(currentBranch.leaves)); + } + + return indexFiles; +} + export function readToBuffer(stream: ReadStream): Promise { return new Promise((resolve) => { const ret: Array = []; diff --git a/tests/options-test.ts b/tests/options-test.ts index f9c4f655f..45c0337b2 100644 --- a/tests/options-test.ts +++ b/tests/options-test.ts @@ -26,6 +26,7 @@ describe("options", () => { "outputClientImpl": false, "outputEncodeMethods": false, "outputExtensions": false, + "outputIndex": false, "outputJsonMethods": true, "outputPartialMethods": false, "outputSchema": false,