@effected/lsp 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/LspFrame.js +277 -0
- package/LspProbe.js +126 -0
- package/LspProcess.js +282 -0
- package/LspStdio.js +117 -0
- package/LspTestFailure.js +37 -0
- package/README.md +144 -2
- package/index.d.ts +315 -0
- package/index.js +4 -0
- package/internal/LaunchFailed.js +35 -0
- package/internal/messages.js +30 -0
- package/package.json +49 -5
- package/testing.d.ts +307 -0
- package/testing.js +5 -0
- package/tsdoc-metadata.json +11 -0
package/testing.d.ts
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { Duration, Effect, PlatformError, Schema, Scope } from "effect";
|
|
2
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
3
|
+
//#region src/LspMessage.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* A JSON-RPC 2.0 message as an LSP frame body carries it: a request (an `id`
|
|
6
|
+
* and a `method`), a notification (a `method`, no `id`) or a response (an
|
|
7
|
+
* `id` and either `result` or `error`).
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* Structural on purpose. Every field but `jsonrpc` is optional, because one
|
|
11
|
+
* shape describes all three kinds and the reader distinguishes them by which
|
|
12
|
+
* fields are present: `method` and `id` is a request, `method` alone is a
|
|
13
|
+
* notification, `id` without `method` is a response. A decoded frame body is
|
|
14
|
+
* `unknown` until a caller has checked it is one of these.
|
|
15
|
+
*
|
|
16
|
+
* @public
|
|
17
|
+
*/
|
|
18
|
+
interface LspMessage {
|
|
19
|
+
/** Always `"2.0"`. */
|
|
20
|
+
readonly jsonrpc: "2.0";
|
|
21
|
+
/** Present on a request and a response; `null` on a response to a request whose id could not be read. */
|
|
22
|
+
readonly id?: number | string | null;
|
|
23
|
+
/** Present on a request and a notification. */
|
|
24
|
+
readonly method?: string;
|
|
25
|
+
/** A request's or notification's parameters. */
|
|
26
|
+
readonly params?: unknown;
|
|
27
|
+
/** A successful response's result; `null` is a valid result (`shutdown` answers it). */
|
|
28
|
+
readonly result?: unknown;
|
|
29
|
+
/** A failed response's error. */
|
|
30
|
+
readonly error?: {
|
|
31
|
+
readonly code: number;
|
|
32
|
+
readonly message: string;
|
|
33
|
+
readonly data?: unknown;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/LspTestFailure.d.ts
|
|
38
|
+
declare const LspTestFailure_base: Schema.Class<LspTestFailure, Schema.TaggedStruct<"LspTestFailure", {
|
|
39
|
+
/** Which failure occurred; see the list above. */
|
|
40
|
+
readonly reason: Schema.Literals<readonly ["StreamEnded", "InvalidFrame", "NotJsonRpc", "TimedOut"]>;
|
|
41
|
+
/** A human-readable description of what went wrong. */
|
|
42
|
+
readonly message: Schema.String;
|
|
43
|
+
}>, import("effect/Cause").YieldableError>;
|
|
44
|
+
/**
|
|
45
|
+
* Why {@link LspProbe.initialize} could not prove a Language Server boots.
|
|
46
|
+
*
|
|
47
|
+
* @remarks
|
|
48
|
+
* - `StreamEnded`: the server's stdout ended before the response the probe
|
|
49
|
+
* was waiting for, usually because it exited; a stream that ended inside a
|
|
50
|
+
* frame lands here too. The message carries the exit code and stderr.
|
|
51
|
+
* - `InvalidFrame`: stdout carried bytes that are not an LSP frame — most
|
|
52
|
+
* often a log line written to stdout, which is the protocol wire. The
|
|
53
|
+
* message carries the frame error, excerpt included.
|
|
54
|
+
* - `NotJsonRpc`: a well-framed body that is not a JSON-RPC 2.0 message.
|
|
55
|
+
* - `TimedOut`: the exchange outlived `LspProbeOptions.timeout`. The message
|
|
56
|
+
* names the step the probe was waiting on and the stderr so far; a server
|
|
57
|
+
* that ignores `exit` and waits for stdin to close fails here.
|
|
58
|
+
*
|
|
59
|
+
* A JSON-RPC error answering `initialize` or `shutdown` is not a failure: it
|
|
60
|
+
* is returned in the result for the caller to assert on.
|
|
61
|
+
*
|
|
62
|
+
* @public
|
|
63
|
+
*/
|
|
64
|
+
export declare class LspTestFailure extends LspTestFailure_base {}
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/LspProbe.d.ts
|
|
67
|
+
/**
|
|
68
|
+
* Options for {@link LspProbe.initialize}.
|
|
69
|
+
*
|
|
70
|
+
* @public
|
|
71
|
+
*/
|
|
72
|
+
interface LspProbeOptions {
|
|
73
|
+
/** `initialize`'s `rootUri`. Defaults to `null`: no workspace folder. */
|
|
74
|
+
readonly rootUri?: string | null;
|
|
75
|
+
/** `initialize`'s `processId`. Defaults to `null`, so the server never watches a client process for liveness. */
|
|
76
|
+
readonly processId?: number | null;
|
|
77
|
+
/** The client capabilities `initialize` announces. Defaults to `{}`. */
|
|
78
|
+
readonly capabilities?: {
|
|
79
|
+
readonly [key: string]: unknown;
|
|
80
|
+
};
|
|
81
|
+
/** `initialize`'s `initializationOptions`; omitted from the request unless given. */
|
|
82
|
+
readonly initializationOptions?: unknown;
|
|
83
|
+
/** A ceiling over the whole exchange, spawn excluded. Defaults to 30 seconds. */
|
|
84
|
+
readonly timeout?: Duration.Input;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* What {@link LspProbe.initialize} observed.
|
|
88
|
+
*
|
|
89
|
+
* @public
|
|
90
|
+
*/
|
|
91
|
+
interface LspProbeResult {
|
|
92
|
+
/** The id-1 `initialize` response. A refused handshake is still a response: assert `response.error === undefined`. */
|
|
93
|
+
readonly response: LspMessage;
|
|
94
|
+
/** The id-2 `shutdown` response; a clean server answers `result: null`. */
|
|
95
|
+
readonly shutdown: LspMessage;
|
|
96
|
+
/** Every message the server sent, in order: responses, notifications, and requests the probe left unanswered. */
|
|
97
|
+
readonly messages: ReadonlyArray<LspMessage>;
|
|
98
|
+
/** Everything the server wrote to stderr; a clean boot leaves it empty. */
|
|
99
|
+
readonly stderr: string;
|
|
100
|
+
/** The server's exit code; 0 means `exit` arrived after `shutdown`, as the specification requires. */
|
|
101
|
+
readonly exitCode: number;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The smallest proof that an installed Language Server bin boots: the whole
|
|
105
|
+
* LSP lifecycle over stdio, and exit code 0.
|
|
106
|
+
*
|
|
107
|
+
* @remarks
|
|
108
|
+
* The exchange is `initialize` (id 1), its response, the `initialized`
|
|
109
|
+
* notification, `shutdown` (id 2), its response, and the `exit`
|
|
110
|
+
* notification; then the probe waits for the server to exit. The caller
|
|
111
|
+
* asserts `response.error === undefined`, `exitCode === 0` and, for a clean
|
|
112
|
+
* boot, `stderr === ""` — this is the LSP half of a packed-install proof, the
|
|
113
|
+
* twin of `@effected/mcp/testing`'s `McpProbe`. Every frame is encoded with
|
|
114
|
+
* `LspFrame.encode` from the main entry, so `Content-Length` counts bytes.
|
|
115
|
+
*
|
|
116
|
+
* - `initialized` is sent because the specification requires it before any
|
|
117
|
+
* other request, `shutdown` included; it is skipped only when `initialize`
|
|
118
|
+
* answered with an error, and the sequence still ends `shutdown`, `exit`.
|
|
119
|
+
* - Stdin stays open after `exit`. The server must terminate itself on
|
|
120
|
+
* `exit`, as an editor never closes the pipe first; a server that waits
|
|
121
|
+
* for stdin to close fails with `TimedOut` instead of passing.
|
|
122
|
+
* - A request the server sends (`client/registerCapability`, say) is
|
|
123
|
+
* recorded in `messages` and never answered.
|
|
124
|
+
* - Never hangs: stdout that ends before a response fails `StreamEnded` with
|
|
125
|
+
* the exit code and stderr; bytes that are not a frame fail `InvalidFrame`;
|
|
126
|
+
* and the whole exchange runs under `timeout` (30 seconds by default),
|
|
127
|
+
* which fails `TimedOut` naming the step it was waiting on. The child is
|
|
128
|
+
* killed when the probe's scope closes.
|
|
129
|
+
* - The timeout reads `Clock`, so under `it.effect`'s virtual clock it
|
|
130
|
+
* never fires on its own: run the probe under `it.live`.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```ts
|
|
134
|
+
* import { LspProbe } from "@effected/lsp/testing";
|
|
135
|
+
* import { Effect } from "effect";
|
|
136
|
+
* import { ChildProcess } from "effect/process";
|
|
137
|
+
*
|
|
138
|
+
* const program = Effect.gen(function* () {
|
|
139
|
+
* const probe = yield* LspProbe.initialize(ChildProcess.make(process.execPath, ["./bin/lsp.js", "--stdio"]));
|
|
140
|
+
* // A clean boot: no JSON-RPC error, exit code 0, empty stderr.
|
|
141
|
+
* return probe.response.error === undefined && probe.exitCode === 0 && probe.stderr === "";
|
|
142
|
+
* });
|
|
143
|
+
* // Requires a `ChildProcessSpawner` from the platform layer.
|
|
144
|
+
* ```
|
|
145
|
+
*
|
|
146
|
+
* @public
|
|
147
|
+
*/
|
|
148
|
+
export declare class LspProbe {
|
|
149
|
+
private constructor();
|
|
150
|
+
/**
|
|
151
|
+
* Spawn `command`, run `initialize` → `initialized` → `shutdown` → `exit`,
|
|
152
|
+
* and collect every server message, stderr and the exit code.
|
|
153
|
+
*/
|
|
154
|
+
static readonly initialize: (command: ChildProcess.Command, options?: LspProbeOptions) => Effect.Effect<LspProbeResult, LspTestFailure | PlatformError.PlatformError, ChildProcessSpawner.ChildProcessSpawner>;
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/LspProcess.d.ts
|
|
158
|
+
/**
|
|
159
|
+
* Options for {@link LspProcess.stderrUntil}.
|
|
160
|
+
*
|
|
161
|
+
* @public
|
|
162
|
+
*/
|
|
163
|
+
interface LspProcessStderrUntilOptions {
|
|
164
|
+
/**
|
|
165
|
+
* How long to wait for the predicate to hold before failing with
|
|
166
|
+
* `TimedOut`. Real time: under `it.effect`'s `TestClock` it never fires,
|
|
167
|
+
* so a test that waits on stderr runs under `it.live`. Keep it below the
|
|
168
|
+
* test runner's own timeout, or the runner kills the test first.
|
|
169
|
+
*/
|
|
170
|
+
readonly timeout: Duration.Input;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* A spawned Language Server bin a test writes `Content-Length` frames to
|
|
174
|
+
* while it runs — the LSP twin of `@effected/mcp/testing`'s `McpProcess`.
|
|
175
|
+
*
|
|
176
|
+
* @remarks
|
|
177
|
+
* Build the command in the test file with `execPath` and an explicit `env`:
|
|
178
|
+
* `ChildProcess.make(process.execPath, [bin, "--stdio"], { env })`. The child
|
|
179
|
+
* lives for the scope `spawn` runs in, and is killed when it closes.
|
|
180
|
+
*
|
|
181
|
+
* - `nextMessage` fails `StreamEnded` once stdout ends — a stream that ends
|
|
182
|
+
* inside a frame included — so a child that exits early fails the test
|
|
183
|
+
* instead of hanging it. Bytes that are not a frame fail `InvalidFrame`; a
|
|
184
|
+
* frame whose body is not a JSON-RPC 2.0 message fails `NotJsonRpc`.
|
|
185
|
+
* - `send` writes one frame with `LspFrame.encode` from the main entry, so `Content-Length`
|
|
186
|
+
* counts bytes; `sendRaw` writes a string or bytes exactly as given, for a
|
|
187
|
+
* malformed frame or one frame split across writes.
|
|
188
|
+
* - `readUntilResponse` reads past notifications and server requests to the
|
|
189
|
+
* response with the matching id, and returns what it saw.
|
|
190
|
+
* - `closeStdin` is `Queue.end`, so every frame already sent is delivered
|
|
191
|
+
* before stdin closes. The specification makes a server exit on the `exit`
|
|
192
|
+
* notification with stdin still open; close stdin only to test a client
|
|
193
|
+
* disconnect.
|
|
194
|
+
* - Every stdout byte is kept, apart from the decoder: `stdoutSoFar` and
|
|
195
|
+
* `stdoutFinal` return it raw, and `assertOnlyFrames` proves it held
|
|
196
|
+
* nothing but well-formed frames, even after the decoder has failed.
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```ts
|
|
200
|
+
* import { LspProcess } from "@effected/lsp/testing";
|
|
201
|
+
* import { Effect } from "effect";
|
|
202
|
+
* import { ChildProcess } from "effect/process";
|
|
203
|
+
*
|
|
204
|
+
* const program = Effect.gen(function* () {
|
|
205
|
+
* const server = yield* LspProcess.spawn(ChildProcess.make(process.execPath, ["./bin/lsp.js", "--stdio"]));
|
|
206
|
+
* yield* server.send({ jsonrpc: "2.0", id: 1, method: "initialize", params: { processId: null, rootUri: null, capabilities: {} } });
|
|
207
|
+
* const { response } = yield* server.readUntilResponse(1);
|
|
208
|
+
* yield* server.send({ jsonrpc: "2.0", method: "initialized", params: {} });
|
|
209
|
+
* yield* server.send({ jsonrpc: "2.0", id: 2, method: "shutdown" });
|
|
210
|
+
* yield* server.readUntilResponse(2);
|
|
211
|
+
* yield* server.send({ jsonrpc: "2.0", method: "exit" });
|
|
212
|
+
* const exitCode = yield* server.exitCode;
|
|
213
|
+
* // Stdout carried nothing but frames: no stray log line before, between or after them.
|
|
214
|
+
* yield* server.assertOnlyFrames;
|
|
215
|
+
* return { response, exitCode };
|
|
216
|
+
* }).pipe(Effect.scoped);
|
|
217
|
+
* // Requires a `ChildProcessSpawner` from the platform layer.
|
|
218
|
+
* ```
|
|
219
|
+
*
|
|
220
|
+
* @public
|
|
221
|
+
*/
|
|
222
|
+
export declare class LspProcess {
|
|
223
|
+
/**
|
|
224
|
+
* Write one message to the child's stdin as a `Content-Length` frame.
|
|
225
|
+
*
|
|
226
|
+
* @remarks
|
|
227
|
+
* Never fails. A frame sent after `closeStdin` is dropped, and a write the
|
|
228
|
+
* child can no longer receive fails the stdin pump instead; the next
|
|
229
|
+
* `StreamEnded` message names that failure.
|
|
230
|
+
*/
|
|
231
|
+
readonly send: (message: unknown) => Effect.Effect<void>;
|
|
232
|
+
/**
|
|
233
|
+
* Write `bytes` to the child's stdin exactly as given: a string as UTF-8,
|
|
234
|
+
* bytes unchanged, with no framing added.
|
|
235
|
+
*
|
|
236
|
+
* @remarks
|
|
237
|
+
* For frames `send` cannot produce: a header without `Content-Length`, a
|
|
238
|
+
* body that is not JSON, or one frame split across several writes,
|
|
239
|
+
* including mid-character by passing bytes. Never fails, on the same terms
|
|
240
|
+
* as `send`.
|
|
241
|
+
*/
|
|
242
|
+
readonly sendRaw: (bytes: string | Uint8Array) => Effect.Effect<void>;
|
|
243
|
+
/**
|
|
244
|
+
* The next message on stdout.
|
|
245
|
+
*
|
|
246
|
+
* @remarks
|
|
247
|
+
* Fails `StreamEnded` once stdout ends, including inside a frame;
|
|
248
|
+
* `InvalidFrame` when stdout carried bytes that are not a frame (a log
|
|
249
|
+
* line on the wire); `NotJsonRpc` for a well-framed body that is not a
|
|
250
|
+
* JSON-RPC 2.0 message. A frame failure is final: every later read fails
|
|
251
|
+
* the same way.
|
|
252
|
+
*/
|
|
253
|
+
readonly nextMessage: Effect.Effect<LspMessage, LspTestFailure>;
|
|
254
|
+
/** Read messages up to and including the response with this id; `seen` holds every message read, the response last. */
|
|
255
|
+
readonly readUntilResponse: (id: string | number) => Effect.Effect<{
|
|
256
|
+
readonly response: LspMessage;
|
|
257
|
+
readonly seen: ReadonlyArray<LspMessage>;
|
|
258
|
+
}, LspTestFailure>;
|
|
259
|
+
/** End stdin after every frame already sent. */
|
|
260
|
+
readonly closeStdin: Effect.Effect<void>;
|
|
261
|
+
/** Wait for the child to exit. */
|
|
262
|
+
readonly exitCode: Effect.Effect<number, PlatformError.PlatformError>;
|
|
263
|
+
/** Everything written to stderr so far. */
|
|
264
|
+
readonly stderrSoFar: Effect.Effect<string>;
|
|
265
|
+
/**
|
|
266
|
+
* Wait until everything written to stderr so far satisfies `predicate`,
|
|
267
|
+
* and return that text.
|
|
268
|
+
*
|
|
269
|
+
* @remarks
|
|
270
|
+
* Event-driven: the predicate is checked against the current text at once,
|
|
271
|
+
* then again on every chunk the child writes, never on a polling timer.
|
|
272
|
+
* Fails `StreamEnded` when stderr ends (the child exited) without the
|
|
273
|
+
* predicate holding, and `TimedOut` once `options.timeout` passes; both
|
|
274
|
+
* messages carry the stderr seen so far. Reach for it whenever a report
|
|
275
|
+
* lands on a later tick than the messages a test reads, such as a crash
|
|
276
|
+
* guard's report once the server is serving. `options.timeout` is real
|
|
277
|
+
* time: run the test under `it.live`.
|
|
278
|
+
*/
|
|
279
|
+
readonly stderrUntil: (predicate: (stderr: string) => boolean, options: LspProcessStderrUntilOptions) => Effect.Effect<string, LspTestFailure>;
|
|
280
|
+
/** Everything written to stderr, once stderr has ended. Waits for the child to exit. */
|
|
281
|
+
readonly stderrFinal: Effect.Effect<string>;
|
|
282
|
+
/** Every byte written to stdout so far, undecoded. */
|
|
283
|
+
readonly stdoutSoFar: Effect.Effect<Uint8Array>;
|
|
284
|
+
/** Every byte written to stdout, once stdout has ended. Waits for the child to exit. */
|
|
285
|
+
readonly stdoutFinal: Effect.Effect<Uint8Array>;
|
|
286
|
+
/**
|
|
287
|
+
* Wait for stdout to end, then prove it carried nothing but well-formed
|
|
288
|
+
* `Content-Length` frames of JSON-RPC 2.0 messages, and return those
|
|
289
|
+
* messages.
|
|
290
|
+
*
|
|
291
|
+
* @remarks
|
|
292
|
+
* The stdout hygiene check: a stray byte before the first frame, between
|
|
293
|
+
* two frames or after the last fails `InvalidFrame`, whose message names
|
|
294
|
+
* the stream offset and quotes the offending bytes; so does a stream that
|
|
295
|
+
* ends inside a frame. A well-framed body that is not JSON-RPC fails
|
|
296
|
+
* `NotJsonRpc`. It reads the raw capture, not `nextMessage`'s queue, so
|
|
297
|
+
* the messages a test already read are still checked. It waits for the
|
|
298
|
+
* child to exit: send `exit` (or close stdin) first.
|
|
299
|
+
*/
|
|
300
|
+
readonly assertOnlyFrames: Effect.Effect<ReadonlyArray<LspMessage>, LspTestFailure>;
|
|
301
|
+
private constructor();
|
|
302
|
+
/** Spawn `command` for the life of the current scope. */
|
|
303
|
+
static readonly spawn: (command: ChildProcess.Command) => Effect.Effect<LspProcess, PlatformError.PlatformError, ChildProcessSpawner.ChildProcessSpawner | Scope.Scope>;
|
|
304
|
+
}
|
|
305
|
+
//#endregion
|
|
306
|
+
export type { LspMessage, LspProbeOptions, LspProbeResult, LspProcessStderrUntilOptions };
|
|
307
|
+
//# sourceMappingURL=testing.d.ts.map
|
package/testing.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// This file is read by tools that parse documentation comments conforming to the TSDoc standard.
|
|
2
|
+
// It should be published with your NPM package. It should not be tracked by Git.
|
|
3
|
+
{
|
|
4
|
+
"tsdocVersion": "0.12",
|
|
5
|
+
"toolPackages": [
|
|
6
|
+
{
|
|
7
|
+
"packageName": "@microsoft/api-extractor",
|
|
8
|
+
"packageVersion": "7.59.1"
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
}
|