@biomejs/backend-jsonrpc 0.1.0-nightly.54e85a2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1 @@
1
+ {"name":"@biomejs/backend-jsonrpc","version":"0.1.0-nightly.54e85a2","main":"dist/index.js","scripts":{"test":"vitest","test:ci":"pnpm build && vitest --run","tsc":"tsc --noEmit","build":"tsc"},"homepage":"https://biomejs.dev","repository":{"type":"git","url":"https://github.com/biomejs/biome.git","directory":"npm/backend-jsonrpc"},"author":"Rome Tools Developers and Contributors","bugs":"https://github.com/biomejs/biome/issues","description":"Bindings to the JSON-RPC Workspace API of the Rome daemon","keywords":["JavaScript","TypeScript","format","lint","toolchain"],"engines":{"node":">=14.*"},"license":"MIT","devDependencies":{"@types/node":"^18.7.2","typescript":"^4.8.2","vite":"^3.0.8","vitest":"^0.22.0"},"optionalDependencies":{"@biomsjs/cli-win32-x64":"0.1.0-nightly.54e85a2","@biomsjs/cli-win32-arm64":"0.1.0-nightly.54e85a2","@biomsjs/cli-darwin-x64":"0.1.0-nightly.54e85a2","@biomsjs/cli-darwin-arm64":"0.1.0-nightly.54e85a2","@biomsjs/cli-linux-x64":"0.1.0-nightly.54e85a2","@biomsjs/cli-linux-arm64":"0.1.0-nightly.54e85a2"}}
package/src/command.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Gets the path of the Rome binary for the current platform
3
+ *
4
+ * @returns Filesystem path to the binary, or null if no prebuilt distribution exists for the current platform
5
+ */
6
+ export function getCommand(): string | null {
7
+ const { platform, arch } = process;
8
+
9
+ type PlatformPaths = {
10
+ [P in NodeJS.Platform]?: {
11
+ [A in NodeJS.Architecture]?: string;
12
+ };
13
+ };
14
+
15
+ const PLATFORMS: PlatformPaths = {
16
+ win32: {
17
+ x64: "@rometools/cli-win32-x64/rome.exe",
18
+ arm64: "@rometools/cli-win32-arm64/rome.exe",
19
+ },
20
+ darwin: {
21
+ x64: "@rometools/cli-darwin-x64/rome",
22
+ arm64: "@rometools/cli-darwin-arm64/rome",
23
+ },
24
+ linux: {
25
+ x64: "@rometools/cli-linux-x64/rome",
26
+ arm64: "@rometools/cli-linux-arm64/rome",
27
+ },
28
+ };
29
+
30
+ const binPath = PLATFORMS?.[platform]?.[arch];
31
+ if (!binPath) {
32
+ return null;
33
+ }
34
+
35
+ return require.resolve(binPath);
36
+ }
package/src/index.ts ADDED
@@ -0,0 +1,50 @@
1
+ import { getCommand } from "./command";
2
+ import { createSocket } from "./socket";
3
+ import { Transport } from "./transport";
4
+ import {
5
+ createWorkspace as wrapTransport,
6
+ type Workspace,
7
+ type RomePath,
8
+ } from "./workspace";
9
+
10
+ /**
11
+ * Create an instance of the Workspace client connected to a remote daemon
12
+ * instance through the JSON-RPC protocol
13
+ *
14
+ * @returns A Workspace client, or null if the underlying platform is not supported
15
+ */
16
+ export async function createWorkspace(): Promise<Workspace | null> {
17
+ const command = getCommand();
18
+ if (!command) {
19
+ return null;
20
+ }
21
+
22
+ return createWorkspaceWithBinary(command);
23
+ }
24
+
25
+ /**
26
+ * Create an instance of the Workspace client connected to a remote daemon
27
+ * instance through the JSON-RPC protocol, using the provided command to spawn
28
+ * the daemon if necessary
29
+ *
30
+ * @param command Path to the Rome binary distribution
31
+ * @returns A Workspace client, or null if the underlying platform is not supported
32
+ */
33
+ export async function createWorkspaceWithBinary(
34
+ command: string,
35
+ ): Promise<Workspace> {
36
+ const socket = await createSocket(command);
37
+ const transport = new Transport(socket);
38
+
39
+ await transport.request("initialize", {
40
+ capabilities: {},
41
+ client_info: {
42
+ name: "@biomejs/backend-jsonrpc",
43
+ version: "0.10.1-next",
44
+ },
45
+ });
46
+
47
+ return wrapTransport(transport);
48
+ }
49
+
50
+ export * from "./workspace";
package/src/socket.ts ADDED
@@ -0,0 +1,47 @@
1
+ import { spawn } from "child_process";
2
+ import { connect, type Socket } from "net";
3
+
4
+ function getSocket(command: string): Promise<string> {
5
+ return new Promise((resolve, reject) => {
6
+ const process = spawn(command, ["__print_socket"], {
7
+ stdio: "pipe",
8
+ });
9
+
10
+ process.on("error", reject);
11
+
12
+ let pipeName = "";
13
+ process.stdout.on("data", (data) => {
14
+ pipeName += data.toString("utf-8");
15
+ });
16
+
17
+ process.on("exit", (code) => {
18
+ if (code === 0) {
19
+ resolve(pipeName.trimEnd());
20
+ } else {
21
+ reject(
22
+ new Error(
23
+ `Command '${command} __print_socket' exited with code ${code}`,
24
+ ),
25
+ );
26
+ }
27
+ });
28
+ });
29
+ }
30
+
31
+ /**
32
+ * Ensure the Rome daemon server is running and create a Socket connected to the RPC channel
33
+ *
34
+ * @param command Path to the Rome daemon binary
35
+ * @returns Socket instance connected to the daemon
36
+ */
37
+ export async function createSocket(command: string): Promise<Socket> {
38
+ const path = await getSocket(command);
39
+ const socket = connect(path);
40
+
41
+ await new Promise((resolve, reject) => {
42
+ socket.once("error", reject);
43
+ socket.once("ready", resolve);
44
+ });
45
+
46
+ return socket;
47
+ }
@@ -0,0 +1,291 @@
1
+ interface Socket {
2
+ on(event: "data", fn: (data: Buffer) => void): void;
3
+ write(data: Buffer): void;
4
+ destroy(): void;
5
+ }
6
+
7
+ enum ReaderStateKind {
8
+ Header,
9
+ Body,
10
+ }
11
+
12
+ interface ReaderStateHeader {
13
+ readonly kind: ReaderStateKind.Header;
14
+ contentLength?: number;
15
+ contentType?: string;
16
+ }
17
+
18
+ interface ReaderStateBody {
19
+ readonly kind: ReaderStateKind.Body;
20
+ readonly contentLength: number;
21
+ readonly contentType?: string;
22
+ }
23
+
24
+ type ReaderState = ReaderStateHeader | ReaderStateBody;
25
+
26
+ interface JsonRpcRequest {
27
+ jsonrpc: "2.0";
28
+ id: number;
29
+ method: string;
30
+ params: any;
31
+ }
32
+
33
+ function isJsonRpcRequest(message: JsonRpcMessage): message is JsonRpcRequest {
34
+ return (
35
+ "id" in message &&
36
+ typeof message.id === "number" &&
37
+ "method" in message &&
38
+ typeof message.method === "string" &&
39
+ "params" in message
40
+ );
41
+ }
42
+
43
+ interface JsonRpcNotification {
44
+ jsonrpc: "2.0";
45
+ method: string;
46
+ params: any;
47
+ }
48
+
49
+ function isJsonRpcNotification(
50
+ message: JsonRpcMessage,
51
+ ): message is JsonRpcNotification {
52
+ return (
53
+ !("id" in message) &&
54
+ "method" in message &&
55
+ typeof message.method === "string" &&
56
+ "params" in message
57
+ );
58
+ }
59
+
60
+ type JsonRpcResponse =
61
+ | {
62
+ jsonrpc: "2.0";
63
+ id: number;
64
+ result: any;
65
+ }
66
+ | {
67
+ jsonrpc: "2.0";
68
+ id: number;
69
+ error: any;
70
+ };
71
+
72
+ function isJsonRpcResponse(
73
+ message: JsonRpcMessage,
74
+ ): message is JsonRpcResponse {
75
+ return (
76
+ "id" in message &&
77
+ typeof message.id === "number" &&
78
+ !("method" in message) &&
79
+ ("result" in message || "error" in message)
80
+ );
81
+ }
82
+
83
+ type JsonRpcMessage = JsonRpcRequest | JsonRpcNotification | JsonRpcResponse;
84
+
85
+ function isJsonRpcMessage(message: any): message is JsonRpcMessage {
86
+ return (
87
+ typeof message === "object" && message !== null && message.jsonrpc === "2.0"
88
+ );
89
+ }
90
+
91
+ interface PendingRequest {
92
+ resolve(result: any): void;
93
+ reject(error: any): void;
94
+ }
95
+
96
+ const MIME_JSONRPC = "application/vscode-jsonrpc";
97
+
98
+ /**
99
+ * Implements the Rome daemon server JSON-RPC protocol over a Socket instance
100
+ */
101
+ export class Transport {
102
+ /**
103
+ * Counter incremented for each outgoing request to generate a unique ID
104
+ */
105
+ private nextRequestId = 0;
106
+
107
+ /**
108
+ * Storage for the promise resolver functions of pending requests,
109
+ * keyed by ID of the request
110
+ */
111
+ private pendingRequests: Map<number, PendingRequest> = new Map();
112
+
113
+ constructor(private socket: Socket) {
114
+ socket.on("data", (data) => {
115
+ this.processIncoming(data);
116
+ });
117
+ }
118
+
119
+ /**
120
+ * Send a request to the remote server
121
+ *
122
+ * @param method Name of the remote method to call
123
+ * @param params Parameters object the remote method should be called with
124
+ * @return Promise resolving with the value returned by the remote method, or rejecting with an RPC error if the remote call failed
125
+ */
126
+ request(method: string, params: any): Promise<any> {
127
+ return new Promise((resolve, reject) => {
128
+ const id = this.nextRequestId++;
129
+ this.pendingRequests.set(id, { resolve, reject });
130
+ this.sendMessage({
131
+ jsonrpc: "2.0",
132
+ id,
133
+ method,
134
+ params,
135
+ });
136
+ });
137
+ }
138
+
139
+ /**
140
+ * Send a notification message to the remote server
141
+ *
142
+ * @param method Name of the remote method to call
143
+ * @param params Parameters object the remote method should be called with
144
+ */
145
+ notify(method: string, params: any) {
146
+ this.sendMessage({
147
+ jsonrpc: "2.0",
148
+ method,
149
+ params,
150
+ });
151
+ }
152
+
153
+ /**
154
+ * Destroy the internal socket instance for this Transport
155
+ */
156
+ destroy() {
157
+ this.socket.destroy();
158
+ }
159
+
160
+ private sendMessage(message: JsonRpcMessage) {
161
+ const body = Buffer.from(JSON.stringify(message));
162
+ const headers = Buffer.from(
163
+ `Content-Length: ${body.length}\r\n` +
164
+ `Content-Type: ${MIME_JSONRPC};charset=utf-8\r\n` +
165
+ `\r\n`,
166
+ );
167
+ this.socket.write(Buffer.concat([headers, body]));
168
+ }
169
+
170
+ private pendingData = Buffer.from("");
171
+ private readerState: ReaderState = {
172
+ kind: ReaderStateKind.Header,
173
+ };
174
+
175
+ private processIncoming(data: Buffer) {
176
+ this.pendingData = Buffer.concat([this.pendingData, data]);
177
+
178
+ while (this.pendingData.length > 0) {
179
+ if (this.readerState.kind === ReaderStateKind.Header) {
180
+ const lineBreakIndex = this.pendingData.indexOf("\n");
181
+ if (lineBreakIndex < 0) {
182
+ break;
183
+ }
184
+
185
+ const header = this.pendingData.subarray(0, lineBreakIndex + 1);
186
+ this.pendingData = this.pendingData.subarray(lineBreakIndex + 1);
187
+ this.processIncomingHeader(this.readerState, header.toString("utf-8"));
188
+ } else if (this.pendingData.length >= this.readerState.contentLength) {
189
+ const body = this.pendingData.subarray(
190
+ 0,
191
+ this.readerState.contentLength,
192
+ );
193
+ this.pendingData = this.pendingData.subarray(
194
+ this.readerState.contentLength,
195
+ );
196
+ this.processIncomingBody(body);
197
+
198
+ this.readerState = {
199
+ kind: ReaderStateKind.Header,
200
+ };
201
+ } else {
202
+ break;
203
+ }
204
+ }
205
+ }
206
+
207
+ private processIncomingHeader(readerState: ReaderStateHeader, line: string) {
208
+ if (line === "\r\n") {
209
+ const { contentLength, contentType } = readerState;
210
+ if (typeof contentLength !== "number") {
211
+ throw new Error(
212
+ `incoming message from the remote workspace is missing the Content-Length header`,
213
+ );
214
+ }
215
+
216
+ this.readerState = {
217
+ kind: ReaderStateKind.Body,
218
+ contentLength,
219
+ contentType,
220
+ };
221
+ return;
222
+ }
223
+
224
+ const colonIndex = line.indexOf(":");
225
+ if (colonIndex < 0) {
226
+ throw new Error(`could not find colon token in "${line}"`);
227
+ }
228
+
229
+ const headerName = line.substring(0, colonIndex);
230
+ const headerValue = line.substring(colonIndex + 1).trim();
231
+
232
+ switch (headerName) {
233
+ case "Content-Length": {
234
+ const value = parseInt(headerValue);
235
+ readerState.contentLength = value;
236
+ break;
237
+ }
238
+ case "Content-Type": {
239
+ if (!headerValue.startsWith(MIME_JSONRPC)) {
240
+ throw new Error(
241
+ `invalid value for Content-Type expected "${MIME_JSONRPC}", got "${headerValue}"`,
242
+ );
243
+ }
244
+
245
+ readerState.contentType = headerValue;
246
+ break;
247
+ }
248
+ default:
249
+ console.warn(`ignoring unknown header "${headerName}"`);
250
+ }
251
+ }
252
+
253
+ private processIncomingBody(buffer: Buffer) {
254
+ const data = buffer.toString("utf-8");
255
+ const body = JSON.parse(data);
256
+
257
+ if (isJsonRpcMessage(body)) {
258
+ if (isJsonRpcRequest(body)) {
259
+ // TODO: Not implemented at the moment
260
+ return;
261
+ }
262
+
263
+ if (isJsonRpcNotification(body)) {
264
+ // TODO: Not implemented at the moment
265
+ return;
266
+ }
267
+
268
+ if (isJsonRpcResponse(body)) {
269
+ const pendingRequest = this.pendingRequests.get(body.id);
270
+ if (pendingRequest) {
271
+ this.pendingRequests.delete(body.id);
272
+ const { resolve, reject } = pendingRequest;
273
+ if ("result" in body) {
274
+ resolve(body.result);
275
+ } else {
276
+ reject(body.error);
277
+ }
278
+ } else {
279
+ throw new Error(
280
+ `could not find any pending request matching RPC response ID ${body.id}`,
281
+ );
282
+ }
283
+ return;
284
+ }
285
+ }
286
+
287
+ throw new Error(
288
+ `failed to deserialize incoming message from remote workspace, "${data}" is not a valid JSON-RPC message body`,
289
+ );
290
+ }
291
+ }