Skip to content

Commit 56251ad

Browse files
authored
refactor: migrate to @bufbuild/protobuf for JS protobufs (#295)
* refactor: migrate to @bufbuild/protobuf for JS protobufs * bump versions
1 parent a753760 commit 56251ad

34 files changed

+782
-551
lines changed

packages/api-bindings-wrappers/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@aws/amazon-q-developer-cli-api-bindings-wrappers",
3-
"version": "0.1.0",
3+
"version": "0.2.0",
44
"license": "MIT OR Apache-2.0",
55
"author": "Amazon Web Services",
66
"repository": "https://github.com/aws/amazon-q-developer-cli",

packages/api-bindings/codegen/generate-requests.ts

+46-51
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
1-
import {
2-
Project,
3-
PropertySignature,
4-
SourceFile,
5-
CodeBlockWriter,
6-
IndentationText,
7-
} from "ts-morph";
1+
import { file_fig as file } from "@aws/amazon-q-developer-cli-proto/fig";
2+
import { CodeBlockWriter, IndentationText, Project } from "ts-morph";
83
import { readFileSync } from "node:fs";
94
import { join, dirname } from "node:path";
105
import { fileURLToPath } from "node:url";
@@ -30,21 +25,13 @@ const normalize = (type: string): string => {
3025
return capitalizeFirstLetter(normalized);
3126
};
3227

33-
const getSubmessageTypes = (bindings: SourceFile, interfaceName: string) => {
34-
const interfaceRef = bindings.getInterface(interfaceName)!;
35-
const submessage = interfaceRef.getProperties()[1];
36-
37-
const submessageUnion = submessage
38-
.getChildren()
39-
.filter((elm) => elm.getKindName() === "UnionType")[0];
40-
const literals = submessageUnion
41-
.getChildren()[0]
42-
.getChildren()
43-
.filter((elm) => elm.getKindName() === "TypeLiteral");
44-
const types = literals
45-
.map((elm) => elm.getChildren()[1])
46-
.map((elm) => elm.getChildren()[1]);
47-
return types.map((prop) => (prop as PropertySignature).getName());
28+
const getSubmessageTypes = (interfaceName: string) => {
29+
const message = file.messages.find(
30+
(message) => message.name === interfaceName,
31+
);
32+
return (
33+
message?.fields.map((type) => type.message?.name!).filter(Boolean) ?? []
34+
);
4835
};
4936

5037
const writeGenericSendRequestWithResponseFunction = (
@@ -54,23 +41,23 @@ const writeGenericSendRequestWithResponseFunction = (
5441
const lowercasedEndpoint = lowercaseFirstLetter(endpoint);
5542

5643
const template = `export async function send${endpoint}Request(
57-
request: ${endpoint}Request
44+
request: Omit<${endpoint}Request, "$typeName" | "$unknown">
5845
): Promise<${endpoint}Response> {
5946
return new Promise((resolve, reject) => {
6047
sendMessage(
61-
{ $case: "${lowercasedEndpoint}Request", ${lowercasedEndpoint}Request: request },
48+
{ case: "${lowercasedEndpoint}Request", value: create(${endpoint}RequestSchema, request) },
6249
(response) => {
63-
switch (response?.$case) {
50+
switch (response?.case) {
6451
case "${lowercasedEndpoint}Response":
65-
resolve(response.${lowercasedEndpoint}Response);
52+
resolve(response.value);
6653
break;
6754
case "error":
68-
reject(Error(response.error));
55+
reject(Error(response.value));
6956
break;
7057
default:
7158
reject(
7259
Error(
73-
\`Invalid response '\${response?.$case}' for '${endpoint}Request'\`
60+
\`Invalid response '\${response?.case}' for '${endpoint}Request'\`
7461
)
7562
);
7663
}
@@ -89,23 +76,23 @@ const writeGenericSendRequestFunction = (
8976
const lowercasedEndpoint = lowercaseFirstLetter(endpoint);
9077

9178
const template = `export async function send${endpoint}Request(
92-
request: ${endpoint}Request
79+
request: Omit<${endpoint}Request, "$typeName" | "$unknown">
9380
): Promise<void> {
9481
return new Promise((resolve, reject) => {
9582
sendMessage(
96-
{ $case: "${lowercasedEndpoint}Request", ${lowercasedEndpoint}Request: request },
83+
{ case: "${lowercasedEndpoint}Request", value: create(${endpoint}RequestSchema, request) },
9784
(response) => {
98-
switch (response?.$case) {
85+
switch (response?.case) {
9986
case "success":
10087
resolve();
10188
break;
10289
case "error":
103-
reject(Error(response.error));
90+
reject(Error(response.value));
10491
break;
10592
default:
10693
reject(
10794
Error(
108-
\`Invalid response '\${response?.$case}' for '${endpoint}Request'\`
95+
\`Invalid response '\${response?.case}' for '${endpoint}Request'\`
10996
)
11097
);
11198
}
@@ -124,28 +111,20 @@ const project = new Project({
124111

125112
project.addSourceFilesAtPaths(join(__dirname, "../src/*.ts"));
126113

127-
const text = readFileSync(
128-
"node_modules/@aws/amazon-q-developer-cli-proto/dist/fig.pb.ts",
129-
"utf8",
130-
);
131-
const protobufBindings = project.createSourceFile("fig.pb.ts", text);
132-
133-
const requestTypes = getSubmessageTypes(
134-
protobufBindings,
135-
"ClientOriginatedMessage",
114+
const requestTypes = getSubmessageTypes("ClientOriginatedMessage");
115+
const responseTypes = getSubmessageTypes("ServerOriginatedMessage").filter(
116+
(type) => type.includes("Response"),
136117
);
137-
const responseTypes = getSubmessageTypes(
138-
protobufBindings,
139-
"ServerOriginatedMessage",
140-
).filter((type) => type.includes("Response"));
141118

142119
const [requestsWithMatchingResponses, otherRequests] = requestTypes
143120
.filter((request) => request !== "notificationRequest")
144121
.reduce(
145122
(result, request) => {
146123
const [matchingResponse, other] = result;
147124

148-
const endpoint = lowercaseFirstLetter(normalize(request));
125+
const endpoint = normalize(request);
126+
127+
console.log(endpoint, requestTypes);
149128

150129
if (responseTypes.indexOf(`${endpoint}Response`) !== -1) {
151130
return [matchingResponse.concat([request]), other];
@@ -175,15 +154,31 @@ const sourceFile = project.createSourceFile(
175154
const responses = requestsWithMatchingResponses.map((request) =>
176155
request.replace("Request", "Response"),
177156
);
178-
const imports = requestsWithMatchingResponses
179-
.concat(responses)
180-
.concat(otherRequests)
157+
const requestSchemas = requestsWithMatchingResponses.map(
158+
(s) => `${s}Schema`,
159+
);
160+
const otherRequestSchemas = otherRequests.map((s) => `${s}Schema`);
161+
const imports = [
162+
requestsWithMatchingResponses,
163+
responses,
164+
otherRequests,
165+
requestSchemas,
166+
otherRequestSchemas,
167+
]
168+
.flat()
181169
.sort()
182170
.map(capitalizeFirstLetter);
183171
writer.writeLine(
184172
`import { \n${imports.join(",\n")}\n } from "@aws/amazon-q-developer-cli-proto/fig";`,
185173
);
186-
writer.writeLine(`import { sendMessage } from "./core.js";`).blankLine();
174+
writer
175+
.writeLine(
176+
[
177+
'import { sendMessage } from "./core.js";',
178+
'import { create } from "@bufbuild/protobuf";',
179+
].join("\n"),
180+
)
181+
.blankLine();
187182

188183
requestsWithMatchingResponses.forEach((request) =>
189184
writeGenericSendRequestWithResponseFunction(writer, normalize(request)),

packages/api-bindings/package.json

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@aws/amazon-q-developer-cli-api-bindings",
3-
"version": "0.1.0",
3+
"version": "0.2.0",
44
"license": "MIT OR Apache-2.0",
55
"author": "Amazon Web Services",
66
"repository": "https://github.com/aws/amazon-q-developer-cli",
@@ -22,7 +22,8 @@
2222
"generate-requests": "tsx codegen/generate-requests.ts && prettier -w src/requests.ts"
2323
},
2424
"dependencies": {
25-
"@aws/amazon-q-developer-cli-proto": "workspace:^"
25+
"@aws/amazon-q-developer-cli-proto": "workspace:^",
26+
"@bufbuild/protobuf": "2.2.3"
2627
},
2728
"devDependencies": {
2829
"@amzn/eslint-config": "workspace:^",
@@ -34,8 +35,6 @@
3435
"lint-staged": "^15.2.10",
3536
"prettier": "^3.4.2",
3637
"ts-morph": "^24.0.0",
37-
"ts-proto": "~2.5.0",
38-
"tslib": "^2.8.1",
3938
"tsx": "^4.19.2",
4039
"typescript": "^5.7.2"
4140
},

packages/api-bindings/src/auth.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ export function startPkceAuthorization({
5454

5555
export function finishPkceAuthorization({
5656
authRequestId,
57-
}: AuthFinishPkceAuthorizationRequest): Promise<AuthFinishPkceAuthorizationResponse> {
57+
}: Omit<
58+
AuthFinishPkceAuthorizationRequest,
59+
"$typeName"
60+
>): Promise<AuthFinishPkceAuthorizationResponse> {
5861
return sendAuthFinishPkceAuthorizationRequest({ authRequestId });
5962
}
6063

packages/api-bindings/src/core.ts

+26-22
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import {
2-
ServerOriginatedMessage,
3-
ClientOriginatedMessage,
2+
type ServerOriginatedMessage,
3+
type ClientOriginatedMessage,
4+
ClientOriginatedMessageSchema,
5+
ServerOriginatedMessageSchema,
46
} from "@aws/amazon-q-developer-cli-proto/fig";
57

68
import { b64ToBytes, bytesToBase64 } from "./utils.js";
9+
import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
710

811
interface GlobalAPIError {
912
error: string;
@@ -19,9 +22,9 @@ export type APIResponseHandler = (
1922
) => shouldKeepListening | void;
2023

2124
let messageId = 0;
22-
const handlers: Record<number, APIResponseHandler> = {};
25+
const handlers: Record<string, APIResponseHandler> = {};
2326

24-
export function setHandlerForId(handler: APIResponseHandler, id: number) {
27+
export function setHandlerForId(handler: APIResponseHandler, id: string) {
2528
handlers[id] = handler;
2629
}
2730

@@ -30,45 +33,46 @@ const receivedMessage = (response: ServerOriginatedMessage): void => {
3033
return;
3134
}
3235

33-
const handler = handlers[response.id];
36+
const id = response.id.toString();
37+
const handler = handlers[id];
3438

3539
if (!handler) {
3640
return;
3741
}
3842

39-
const keepListeningOnID = handlers[response.id](response.submessage);
43+
const keepListeningOnID = handlers[id](response.submessage);
4044

4145
if (!keepListeningOnID) {
42-
delete handlers[response.id];
46+
delete handlers[id];
4347
}
4448
};
4549

4650
export function sendMessage(
47-
message: ClientOriginatedMessage["submessage"],
51+
submessage: ClientOriginatedMessage["submessage"],
4852
handler?: APIResponseHandler,
4953
) {
50-
const request: ClientOriginatedMessage = {
51-
id: (messageId += 1),
52-
submessage: message,
53-
};
54+
const request: ClientOriginatedMessage = create(
55+
ClientOriginatedMessageSchema,
56+
{
57+
id: BigInt((messageId += 1)),
58+
submessage,
59+
},
60+
);
5461

5562
if (handler && request.id) {
56-
handlers[request.id] = handler;
63+
handlers[request.id.toString()] = handler;
5764
}
5865

59-
const buffer = ClientOriginatedMessage.encode(request).finish();
66+
const buffer = toBinary(ClientOriginatedMessageSchema, request);
6067

6168
if (
6269
window?.fig?.constants?.supportApiProto &&
6370
window?.fig?.constants?.apiProtoUrl
6471
) {
6572
const url = new URL(window.fig.constants.apiProtoUrl);
6673

67-
if (
68-
request.submessage?.$case &&
69-
typeof request.submessage?.$case === "string"
70-
) {
71-
url.pathname = `/${request.submessage.$case}`;
74+
if (typeof request.submessage?.case === "string") {
75+
url.pathname = `/${request.submessage.case}`;
7276
} else {
7377
url.pathname = "/unknown";
7478
}
@@ -81,8 +85,8 @@ export function sendMessage(
8185
body: buffer,
8286
}).then(async (res) => {
8387
const body = new Uint8Array(await res.arrayBuffer());
84-
const m = ServerOriginatedMessage.decode(body);
85-
receivedMessage(m);
88+
const message = fromBinary(ServerOriginatedMessageSchema, body);
89+
receivedMessage(message);
8690
});
8791
return;
8892
}
@@ -115,7 +119,7 @@ const setupEventListeners = (): void => {
115119
document.addEventListener(FigProtoMessageReceived, (event: Event) => {
116120
const raw = (event as CustomEvent).detail as string;
117121
const bytes = b64ToBytes(raw);
118-
const message = ServerOriginatedMessage.decode(bytes);
122+
const message = fromBinary(ServerOriginatedMessageSchema, bytes);
119123
receivedMessage(message);
120124
});
121125
};

packages/api-bindings/src/editbuffer.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ export function subscribe(
1212
return _subscribe(
1313
{ type: NotificationType.NOTIFY_ON_EDITBUFFFER_CHANGE },
1414
(notification) => {
15-
switch (notification?.type?.$case) {
15+
switch (notification?.type?.case) {
1616
case "editBufferNotification":
17-
return handler(notification.type.editBufferNotification);
17+
return handler(notification.type.value);
1818
default:
1919
break;
2020
}

packages/api-bindings/src/event.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@ export function subscribe<T>(
88
return _subscribe(
99
{ type: NotificationType.NOTIFY_ON_EVENT },
1010
(notification) => {
11-
switch (notification?.type?.$case) {
11+
switch (notification?.type?.case) {
1212
case "eventNotification": {
13-
const { eventName: name, payload } =
14-
notification.type.eventNotification;
13+
const { eventName: name, payload } = notification.type.value;
1514
if (name === eventName) {
1615
try {
1716
return handler(payload ? JSON.parse(payload) : null);

0 commit comments

Comments
 (0)