pw_protobuf_compiler: TS library
The Library class builds from a FileDescriptorSet binary and makes it
easier to find and manage proto modules.
Tested with 'bazel test //pw_protobuf_compiler/ts:proto_lib_test'
Bug: b/194329554
Change-Id: I6bb065a94be17619e3ed5f46708aeeba323dc322
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/59540
Pigweed-Auto-Submit: Jared Weinstein <jaredweinstein@google.com>
Commit-Queue: Auto-Submit <auto-submit@pigweed.google.com.iam.gserviceaccount.com>
Reviewed-by: Paul Mathieu <paulmathieu@google.com>
diff --git a/pw_protobuf_compiler/BUILD.bazel b/pw_protobuf_compiler/BUILD.bazel
index 4115f07..045285e 100644
--- a/pw_protobuf_compiler/BUILD.bazel
+++ b/pw_protobuf_compiler/BUILD.bazel
@@ -12,6 +12,7 @@
# License for the specific language governing permissions and limitations under
# the License.
load("@com_google_protobuf//:protobuf.bzl", "py_proto_library")
+load("@rules_proto_grpc//js:defs.bzl", "js_proto_library")
package(default_visibility = ["//visibility:public"])
@@ -30,3 +31,16 @@
"pw_protobuf_compiler_protos/test.proto",
],
)
+
+proto_library(
+ name = "test_protos",
+ srcs = [
+ "pw_protobuf_compiler_protos/nested/more_nesting/test.proto",
+ "pw_protobuf_compiler_protos/test.proto",
+ ],
+)
+
+js_proto_library(
+ name = "test_protos_tspb",
+ protos = ["//pw_protobuf_compiler:test_protos"],
+)
diff --git a/pw_protobuf_compiler/docs.rst b/pw_protobuf_compiler/docs.rst
index 54e633d..3712df5 100644
--- a/pw_protobuf_compiler/docs.rst
+++ b/pw_protobuf_compiler/docs.rst
@@ -30,6 +30,9 @@
| Python | ``python`` | Compiles using the standard Python protobuf |
| | | plugin, creating a ``pw_python_package``. |
+-------------+----------------+-----------------------------------------------+
+| Typescript | ``typescript`` | Compilation is supported in Bazel via |
+| | | @rules_proto_grpc. |
++-------------+----------------+-----------------------------------------------+
GN template
===========
diff --git a/pw_protobuf_compiler/ts/BUILD.bazel b/pw_protobuf_compiler/ts/BUILD.bazel
new file mode 100644
index 0000000..48b7a4b
--- /dev/null
+++ b/pw_protobuf_compiler/ts/BUILD.bazel
@@ -0,0 +1,53 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load("@npm//@bazel/jasmine:index.bzl", "jasmine_node_test")
+load("@npm//@bazel/typescript:index.bzl", "ts_library")
+
+package(default_visibility = ["//visibility:public"])
+
+ts_library(
+ name = "proto_lib",
+ srcs = ["proto_lib.ts"],
+ module_name = "pigweed/pw_protobuf_compiler/ts",
+ deps = [
+ "//pw_rpc:packet_proto_tspb",
+ "@npm//@types/google-protobuf",
+ "@npm//@types/node",
+ ],
+)
+
+ts_library(
+ name = "proto_lib_test_lib",
+ srcs = [
+ "proto_lib_test.ts",
+ ],
+ data = [
+ "//pw_protobuf_compiler:test_protos",
+ ],
+ deps = [
+ ":proto_lib",
+ "//pw_protobuf_compiler:test_protos_tspb",
+ "//pw_rpc:packet_proto_tspb",
+ "@npm//@types/google-protobuf",
+ "@npm//@types/jasmine",
+ ],
+)
+
+jasmine_node_test(
+ name = "proto_lib_test",
+ srcs = [
+ ":proto_lib_test_lib",
+ ],
+)
diff --git a/pw_protobuf_compiler/ts/proto_lib.ts b/pw_protobuf_compiler/ts/proto_lib.ts
new file mode 100644
index 0000000..8af431e
--- /dev/null
+++ b/pw_protobuf_compiler/ts/proto_lib.ts
@@ -0,0 +1,122 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+/** Tools for compiling and importing Javascript protos on the fly. */
+
+import * as fs from 'fs';
+import {Message} from 'google-protobuf';
+import {FileDescriptorSet} from 'google-protobuf/google/protobuf/descriptor_pb';
+
+export type MessageCreator = new () => Message;
+class MessageMap extends Map<string, MessageCreator> {}
+class ModuleMap extends Map<string, any> {}
+
+/**
+ * A collection of protocol buffer modules
+ */
+export class Library {
+ private fileDescriptorSet: FileDescriptorSet;
+ private messages: MessageMap;
+
+ /**
+ * Primary way to construct a Library.
+ *
+ * For example,
+ * ```
+ * const path = 'pw_rpc/ts/test_protos-descriptor-set.proto.bin'
+ * const lib = Library.fromFileDescriptorSet(path, 'test_protos_tspb');
+ * ```
+ * The example path is for a proto_library bazel rule named 'test_protos'.
+ *
+ * @param path String location of the FileDescriptorSet binary.
+ * @param protoLibName: String name for the js_proto_library dependency. This
+ * is used to map from fileDescriptor name to compiled proto path.
+ */
+ static async fromFileDescriptorSet(path: string, protoLibName: string):
+ Promise<Library> {
+ const binary = fs.readFileSync(path);
+ const fileDescriptorSet = FileDescriptorSet.deserializeBinary(binary);
+
+ let mods = new ModuleMap();
+ for (const file of fileDescriptorSet.getFileList()) {
+ const moduleName = this.buildModuleName(file.getName()!, protoLibName);
+ let mod = await import(moduleName);
+ const key = file.getName()!;
+ mods.set(key, mod);
+ }
+
+ return new Library(fileDescriptorSet, mods)
+ }
+
+ /**
+ * Maps from a file and js_proto_library name to a module path.
+ *
+ * For example the following rules would correspond with the call
+ * `buildModuleName('dir/test.proto', 'test_proto_tspb')`
+ *
+ * proto_library(
+ * name = "test_proto",
+ * srcs = [
+ * "dir/test.proto",
+ * ],
+ * )
+
+ * js_proto_library(
+ * name = "test_proto_tspb",
+ * protos = [":test_protos"],
+ * )
+ *
+ * @param {string} fileName Proto file in the form 'root/dir/test.proto'
+ * @param {string} protoLibName Name attribute of the relevant
+ * js_proto_library rule.
+ */
+ private static buildModuleName(fileName: string, protoLibName: string):
+ string {
+ const name = `${protoLibName}/${protoLibName}_pb/${fileName}`;
+ return name.replace(/\.proto$/, '_pb');
+ }
+
+ constructor(set: FileDescriptorSet, mods: ModuleMap) {
+ this.fileDescriptorSet = set;
+ this.messages = this.mapMessages(set, mods);
+ }
+
+ /**
+ * Creates a map between message identifier "{packageName}.{messageName}"
+ * and the Message class.
+ */
+ private mapMessages(set: FileDescriptorSet, mods: ModuleMap): MessageMap {
+ const messages = new MessageMap();
+ for (const fileDescriptor of set.getFileList()) {
+ const mod = mods.get(fileDescriptor.getName()!)!;
+ for (const messageType of fileDescriptor.getMessageTypeList()) {
+ const fullName =
+ fileDescriptor.getPackage()! + '.' + messageType.getName();
+ const message = mod[messageType.getName()!];
+ messages.set(fullName, message);
+ }
+ }
+ return messages;
+ }
+
+ /**
+ * Finds the Message class referenced by the identifier.
+ *
+ * @param identifier String identifier of the form
+ * "{packageName}.{messageName}" i.e: "pw.rpc.test.NewMessage".
+ */
+ getMessageCreator(identifier: string): MessageCreator|undefined {
+ return this.messages.get(identifier);
+ }
+}
diff --git a/pw_protobuf_compiler/ts/proto_lib_test.ts b/pw_protobuf_compiler/ts/proto_lib_test.ts
new file mode 100644
index 0000000..6a917f5
--- /dev/null
+++ b/pw_protobuf_compiler/ts/proto_lib_test.ts
@@ -0,0 +1,52 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+/* eslint-env browser, jasmine */
+import 'jasmine';
+
+import {Message} from 'test_protos_tspb/test_protos_tspb_pb/pw_protobuf_compiler/pw_protobuf_compiler_protos/nested/more_nesting/test_pb';
+
+import {Library} from './proto_lib';
+
+const DESCRIPTOR_BINARY_PATH =
+ 'pw_protobuf_compiler/test_protos-descriptor-set.proto.bin';
+const TEST_LIB_NAME = 'test_protos_tspb';
+
+describe('Library', () => {
+ it('constructed from invalid paths rejects promise', async () => {
+ await expectAsync(Library.fromFileDescriptorSet('garbage', TEST_LIB_NAME))
+ .toBeRejected();
+ await expectAsync(
+ Library.fromFileDescriptorSet(DESCRIPTOR_BINARY_PATH, 'garbage'))
+ .toBeRejected();
+ });
+
+ it('getMessageType returns message', async () => {
+ const lib = await Library.fromFileDescriptorSet(
+ DESCRIPTOR_BINARY_PATH, TEST_LIB_NAME);
+
+ let fetched = lib.getMessageCreator('pw.protobuf_compiler.test.Message');
+ expect(fetched).toEqual(Message);
+ });
+
+ it('getMessageType for invalid identifier returns undefined', async () => {
+ const lib = await Library.fromFileDescriptorSet(
+ DESCRIPTOR_BINARY_PATH, TEST_LIB_NAME);
+
+ let fetched = lib.getMessageCreator('pw');
+ expect(fetched).toBeUndefined();
+ fetched = lib.getMessageCreator('pw.test1.Garbage');
+ expect(fetched).toBeUndefined();
+ });
+});