Skip to content

Commit

Permalink
feat: Generate Index Files (#821)
Browse files Browse the repository at this point in the history
* feat: output index files

* chore: add outputIndex to readme

* feat: remove extra index nesting

* chore: update readme

* test: output-index

* fix: lint

* fix: options-test snapshot
  • Loading branch information
blorgon1 committed May 23, 2023
1 parent 75f9f4a commit 85bf206
Show file tree
Hide file tree
Showing 13 changed files with 178 additions and 9 deletions.
4 changes: 4 additions & 0 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
11 changes: 10 additions & 1 deletion integration/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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(() => {
Expand Down
14 changes: 14 additions & 0 deletions integration/output-index/a-test.ts
Original file line number Diff line number Diff line change
@@ -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),
},
},
});
});
});
Binary file added integration/output-index/a.bin
Binary file not shown.
6 changes: 6 additions & 0 deletions integration/output-index/a.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
syntax = "proto3";
package base;

message A {
string a = 1;
}
77 changes: 77 additions & 0 deletions integration/output-index/a.ts
Original file line number Diff line number Diff line change
@@ -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 extends Exact<DeepPartial<A>, I>>(base?: I): A {
return A.fromPartial(base ?? {});
},

fromPartial<I extends Exact<DeepPartial<A>, 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> = T extends Builtin ? T
: T extends Array<infer U> ? Array<DeepPartial<U>> : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>;

type KeysOfUnion<T> = T extends T ? keyof T : never;
type Exact<P, I extends P> = P extends Builtin ? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };

function isSet(value: any): boolean {
return value !== null && value !== undefined;
}
3 changes: 3 additions & 0 deletions integration/output-index/index.base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/* eslint-disable */

export * from "./a";
3 changes: 3 additions & 0 deletions integration/output-index/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/* eslint-disable */

export * as base from "./index.base";
1 change: 1 addition & 0 deletions integration/output-index/parameters.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
outputIndex=true
6 changes: 6 additions & 0 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export type Options = {
useReadonlyTypes: boolean;
useSnakeTypeName: boolean;
outputExtensions: boolean;
outputIndex: boolean;
M: { [from: string]: string };
};

Expand Down Expand Up @@ -130,6 +131,7 @@ export function defaultOptions(): Options {
useReadonlyTypes: false,
useSnakeTypeName: true,
outputExtensions: false,
outputIndex: false,
M: {},
};
}
Expand Down Expand Up @@ -218,6 +220,10 @@ export function optionsFromParameter(parameter: string | undefined): Options {
options.initializeFieldsAsUndefined = false;
}

if (options.outputIndex) {
options.exportCommonSymbols = false;
}

return options;
}

Expand Down
16 changes: 9 additions & 7 deletions src/plugin.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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,
Expand Down
45 changes: 44 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<PackageTree>((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<Buffer> {
return new Promise((resolve) => {
const ret: Array<Buffer | string> = [];
Expand Down
1 change: 1 addition & 0 deletions tests/options-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe("options", () => {
"outputClientImpl": false,
"outputEncodeMethods": false,
"outputExtensions": false,
"outputIndex": false,
"outputJsonMethods": true,
"outputPartialMethods": false,
"outputSchema": false,
Expand Down

0 comments on commit 85bf206

Please sign in to comment.