@effected/lsp 0.0.0-stage → 0.1.1

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/index.d.ts ADDED
@@ -0,0 +1,315 @@
1
+ import { Effect, Result, Runtime, Schema, Stream } from "effect";
2
+ //#region src/LspFrame.d.ts
3
+ /**
4
+ * Why a byte stream is not a sequence of LSP base-protocol frames.
5
+ *
6
+ * @remarks
7
+ * - `MissingContentLength`: the header block has no `Content-Length` field.
8
+ * - `InvalidContentLength`: a `Content-Length` that is not one decimal,
9
+ * non-negative safe integer, or a second `Content-Length` field.
10
+ * - `InvalidHeader`: a header line that is not `Name: value`, or a header
11
+ * byte outside ASCII. A server that writes a log line to stdout before its
12
+ * first frame lands here, because the stray text joins the header block.
13
+ * - `HeaderTooLarge`: more than 8 KiB without the `\r\n\r\n` terminator. The
14
+ * decoder refuses to buffer an unterminated header without bound.
15
+ * - `InvalidBody`: the body is not valid UTF-8 or not valid JSON.
16
+ * - `Truncated`: the input ended inside a frame.
17
+ *
18
+ * @public
19
+ */
20
+ export declare const LspFrameErrorCode: Schema.Literals<readonly ["MissingContentLength", "InvalidContentLength", "InvalidHeader", "HeaderTooLarge", "InvalidBody", "Truncated"]>;
21
+ /**
22
+ * The `code` of an {@link LspFrameError}.
23
+ *
24
+ * @public
25
+ */
26
+ export type LspFrameErrorCode = typeof LspFrameErrorCode.Type;
27
+ declare const LspFrameError_base: Schema.Class<LspFrameError, Schema.TaggedStruct<"LspFrameError", {
28
+ /** Which rule the frame broke; see {@link (LspFrameErrorCode:variable)}. */
29
+ readonly code: Schema.Literals<readonly ["MissingContentLength", "InvalidContentLength", "InvalidHeader", "HeaderTooLarge", "InvalidBody", "Truncated"]>;
30
+ /** The stream position of the offending frame's first byte. */
31
+ readonly offset: Schema.Number;
32
+ /** Up to 80 bytes of the frame's start, JSON-quoted. */
33
+ readonly excerpt: Schema.optionalKey<Schema.String>;
34
+ /** The UTF-8 or JSON failure behind an `InvalidBody`. */
35
+ readonly cause: Schema.optionalKey<Schema.Defect>;
36
+ }>, import("effect/Cause").YieldableError>;
37
+ /**
38
+ * A byte stream that is not a sequence of LSP base-protocol frames.
39
+ *
40
+ * @remarks
41
+ * `offset` is the stream position of the offending frame's first byte, so a
42
+ * caller can point at it; `excerpt` echoes up to 80 bytes of the frame's
43
+ * start, JSON-quoted so control characters stay visible; `cause` carries the
44
+ * UTF-8 or JSON failure of an `InvalidBody` as the original value, never a
45
+ * stringified copy. `message` is derived from those fields when read.
46
+ *
47
+ * @public
48
+ */
49
+ export declare class LspFrameError extends LspFrameError_base {
50
+ get message(): string;
51
+ }
52
+ /**
53
+ * What {@link LspFrame.decodeResult} decoded from one buffer.
54
+ *
55
+ * @public
56
+ */
57
+ interface LspFrameDecoded {
58
+ /** Every complete frame's parsed JSON body, in order. */
59
+ readonly messages: ReadonlyArray<unknown>;
60
+ /** The bytes of an incomplete trailing frame, copied; prepend them to the next chunk. Empty when the buffer ended on a frame boundary. */
61
+ readonly rest: Uint8Array;
62
+ }
63
+ /**
64
+ * The LSP base protocol's framing: a `Content-Length` header block, a blank
65
+ * line, and a UTF-8 JSON body of exactly that many bytes.
66
+ *
67
+ * @remarks
68
+ * `Content-Length` counts **bytes**, never characters. A hand-rolled frame
69
+ * that writes `body.length` is correct for ASCII and wrong by one for every
70
+ * extra byte of a multi-byte character, so the server reads into the next
71
+ * frame and the session fails far from the cause; {@link LspFrame.encode}
72
+ * counts the UTF-8 encoding.
73
+ *
74
+ * The decoder is incremental and pure: {@link LspFrame.decodeResult} takes a
75
+ * buffer and returns every complete frame plus the bytes of an incomplete
76
+ * trailing one, so a chunk boundary may fall anywhere — inside a header,
77
+ * inside a body, inside a multi-byte character — and one chunk may carry
78
+ * several frames. {@link LspFrame.decodeStream} threads that state across a
79
+ * `Stream` of chunks.
80
+ *
81
+ * Header fields end `\r\n` and the block ends `\r\n\r\n`; field names are
82
+ * case-insensitive; unknown fields such as `Content-Type` are accepted and
83
+ * ignored. A header longer than 8 KiB without its terminator fails rather
84
+ * than buffering without bound.
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * import { LspFrame } from "@effected/lsp";
89
+ * import { Result } from "effect";
90
+ *
91
+ * const bytes = LspFrame.encode({ jsonrpc: "2.0", method: "exit" });
92
+ * const decoded = LspFrame.decodeAllResult(bytes);
93
+ * console.log(Result.isSuccess(decoded) ? decoded.success : decoded.failure.message);
94
+ * // => [ { jsonrpc: '2.0', method: 'exit' } ]
95
+ * ```
96
+ *
97
+ * @public
98
+ */
99
+ export declare class LspFrame {
100
+ private constructor();
101
+ /**
102
+ * Frame one message: `Content-Length: N\r\n\r\n` followed by its JSON text,
103
+ * where `N` is the UTF-8 byte length of that text.
104
+ *
105
+ * @remarks
106
+ * A value JSON cannot encode (a `bigint`, a cycle) is a caller bug and
107
+ * throws; nothing a server sends reaches this function.
108
+ */
109
+ static encode(message: unknown): Uint8Array;
110
+ /**
111
+ * Decode every complete frame at the front of `bytes`, returning their
112
+ * bodies and the bytes of an incomplete trailing frame.
113
+ *
114
+ * @remarks
115
+ * The sync primitive; {@link LspFrame.decode} is its spanned `Effect`
116
+ * form. `offset` is the stream position of `bytes[0]` (default 0), so an
117
+ * error names a stream position rather than a buffer index. To decode a
118
+ * stream by hand, keep `rest`, prepend it to the next chunk, and advance
119
+ * `offset` by the bytes consumed.
120
+ */
121
+ static decodeResult(bytes: Uint8Array, offset?: number): Result.Result<LspFrameDecoded, LspFrameError>;
122
+ /** The `Effect` form of {@link LspFrame.decodeResult}, behind the `LspFrame.decode` span. */
123
+ static readonly decode: (bytes: Uint8Array<ArrayBufferLike>, offset?: number | undefined) => Effect.Effect<LspFrameDecoded, LspFrameError, never>;
124
+ /**
125
+ * Decode a complete buffer — a collected stdout — into its messages.
126
+ *
127
+ * @remarks
128
+ * A string is UTF-8 encoded first, so its byte counts are honoured.
129
+ * Leftover bytes after the last complete frame fail `Truncated`. The sync
130
+ * primitive; {@link LspFrame.decodeAll} is the spanned `Effect` form.
131
+ */
132
+ static decodeAllResult(input: Uint8Array | string): Result.Result<ReadonlyArray<unknown>, LspFrameError>;
133
+ /** The `Effect` form of {@link LspFrame.decodeAllResult}, behind the `LspFrame.decodeAll` span. */
134
+ static readonly decodeAll: (input: string | Uint8Array<ArrayBufferLike>) => Effect.Effect<readonly unknown[], LspFrameError, never>;
135
+ /**
136
+ * Decode a stream of byte chunks into a stream of message bodies.
137
+ *
138
+ * @remarks
139
+ * Chunk boundaries may fall anywhere. A malformed frame fails the stream
140
+ * with an {@link LspFrameError}; a stream that ends inside a frame fails
141
+ * `Truncated`, so a server that dies mid-write is never read as a clean end.
142
+ */
143
+ static decodeStream<E, R>(self: Stream.Stream<Uint8Array, E, R>): Stream.Stream<unknown, E | LspFrameError, R>;
144
+ }
145
+ //#endregion
146
+ //#region src/LspMessage.d.ts
147
+ /**
148
+ * A JSON-RPC 2.0 message as an LSP frame body carries it: a request (an `id`
149
+ * and a `method`), a notification (a `method`, no `id`) or a response (an
150
+ * `id` and either `result` or `error`).
151
+ *
152
+ * @remarks
153
+ * Structural on purpose. Every field but `jsonrpc` is optional, because one
154
+ * shape describes all three kinds and the reader distinguishes them by which
155
+ * fields are present: `method` and `id` is a request, `method` alone is a
156
+ * notification, `id` without `method` is a response. A decoded frame body is
157
+ * `unknown` until a caller has checked it is one of these.
158
+ *
159
+ * @public
160
+ */
161
+ interface LspMessage {
162
+ /** Always `"2.0"`. */
163
+ readonly jsonrpc: "2.0";
164
+ /** Present on a request and a response; `null` on a response to a request whose id could not be read. */
165
+ readonly id?: number | string | null;
166
+ /** Present on a request and a notification. */
167
+ readonly method?: string;
168
+ /** A request's or notification's parameters. */
169
+ readonly params?: unknown;
170
+ /** A successful response's result; `null` is a valid result (`shutdown` answers it). */
171
+ readonly result?: unknown;
172
+ /** A failed response's error. */
173
+ readonly error?: {
174
+ readonly code: number;
175
+ readonly message: string;
176
+ readonly data?: unknown;
177
+ };
178
+ }
179
+ //#endregion
180
+ //#region src/LspStdio.d.ts
181
+ /**
182
+ * How a Language Server session ended, as the server's own message loop
183
+ * observed it. {@link LspStdio.exitCode} maps it to the exit code the LSP
184
+ * specification requires.
185
+ *
186
+ * @remarks
187
+ * The kit does not own the message loop, so it cannot observe `shutdown` or
188
+ * `exit` itself: the server's loop records them and returns this value when
189
+ * it stops reading. A `vscode-languageserver` connection reports both through
190
+ * its `onShutdown` and `onExit` handlers; a hand-rolled loop sees the methods
191
+ * directly.
192
+ *
193
+ * @public
194
+ */
195
+ interface LspSessionEnd {
196
+ /**
197
+ * `"exit"`: the client sent the `exit` notification. `"closed"`: stdin
198
+ * ended with no `exit` among the messages it delivered.
199
+ */
200
+ readonly reason: "exit" | "closed";
201
+ /** Whether a `shutdown` request arrived before the session ended. */
202
+ readonly shutdownReceived: boolean;
203
+ }
204
+ /**
205
+ * What {@link LspStdio.teardown} needs from the host: a way to end the
206
+ * process with a code. Node's `process` satisfies it.
207
+ *
208
+ * @public
209
+ */
210
+ interface LspExitHost {
211
+ /** End the process with `code`. */
212
+ readonly exit: (code: number) => void;
213
+ }
214
+ /**
215
+ * Run an Effect Language Server over stdio without ever writing a log line or
216
+ * a failure report onto stdout, which is the LSP wire, and exit with the code
217
+ * the LSP specification requires.
218
+ *
219
+ * @remarks
220
+ * The `main.ts`, where `serve` is the server's own program and returns an
221
+ * {@link LspSessionEnd} when its message loop stops:
222
+ *
223
+ * ```ts
224
+ * NodeRuntime.runMain(LspStdio.launch(serve.pipe(Effect.provide(AppLayer))), { teardown: LspStdio.teardown(process) })
225
+ * ```
226
+ *
227
+ * - {@link LspStdio.launch} provides `LogToStderr` to the whole program, and
228
+ * reports a failure itself, on stderr, hiding it from `runMain`, whose own
229
+ * report is written outside anything the program can provide. A layer that
230
+ * fails to build (a missing `HOME`, a bad config) is such a failure.
231
+ * - {@link LspStdio.exitCode} is the specification's rule: `exit` without a
232
+ * prior `shutdown` exits 1, everything else 0. `launch` applies it to the
233
+ * program's result.
234
+ * - {@link LspStdio.teardown} keeps that code, maps an interrupt-only exit
235
+ * (SIGINT, SIGTERM) to 0 instead of 130, and ends the process explicitly:
236
+ * with stdin still open, as the specification leaves it after `exit`, the
237
+ * event loop never drains on its own.
238
+ *
239
+ * Never `NodeRuntime.runMain(serve.pipe(Effect.provide(Layer.succeed(References.LogToStderr, true))))`:
240
+ * it typechecks and serves, but a failure is still reported by `runMain`
241
+ * through `console.log`, straight onto the wire, where the client reads it as
242
+ * a malformed frame instead of an error.
243
+ *
244
+ * This is not an LSP framework: the message loop, `initialize` and every
245
+ * request handler stay with whatever the server is built on.
246
+ *
247
+ * @public
248
+ */
249
+ export declare class LspStdio {
250
+ private constructor();
251
+ /**
252
+ * The specification's exit code for a session: 1 when `exit` arrived
253
+ * without a prior `shutdown`, otherwise 0.
254
+ *
255
+ * @remarks
256
+ * A session whose stdin closed without `exit` (`"closed"`) is a client
257
+ * disconnect the specification gives no code for; it maps to 0, with or
258
+ * without `shutdown`.
259
+ */
260
+ static exitCode(end: LspSessionEnd): 0 | 1;
261
+ /**
262
+ * Run `program` with `LogToStderr` set, map its {@link LspSessionEnd} to
263
+ * an exit code with {@link LspStdio.exitCode}, and report any failure
264
+ * other than an interrupt on stderr here, re-raising it marked as already
265
+ * reported and keeping its exit code.
266
+ *
267
+ * @remarks
268
+ * `runMain` logs an unhandled failure from outside the program, where no
269
+ * `Effect.provide` the program applies reaches, and Effect's default logger
270
+ * writes through `console.log` unless `LogToStderr` is set. For a Language
271
+ * Server that is stdout, the wire. `launch` catches the cause inside its
272
+ * own `LogToStderr` provision, logs it there, and fails with an error
273
+ * `runMain` does not report again, whose `Runtime.errorExitCode` is the
274
+ * original failure's (1 unless it carries its own).
275
+ *
276
+ * `LogToStderr` reaches every logger that honours it: Effect's default
277
+ * logger and `Logger.consolePretty`. `Logger.consoleJson`,
278
+ * `Logger.consoleLogFmt` and `Logger.consoleStructured` write through
279
+ * `console.log` whatever it says; wrap a formatter in
280
+ * `Logger.withConsoleError` instead. `Console.log` is not a logger and
281
+ * still writes to stdout.
282
+ *
283
+ * The success value is the exit code; pair `launch` with
284
+ * {@link LspStdio.teardown}, which hands it to `runMain` and ends the
285
+ * process with it.
286
+ */
287
+ static readonly launch: <E, R>(program: Effect.Effect<LspSessionEnd, E, R>) => Effect.Effect<0 | 1, Error, R>;
288
+ /**
289
+ * The teardown for `runMain`: a success hands its value on as the exit
290
+ * code when it is a number (0 otherwise), an interrupt-only exit maps to 0,
291
+ * and anything else goes to `Runtime.defaultTeardown`; then `host.exit`
292
+ * ends the process with that code.
293
+ *
294
+ * @remarks
295
+ * SIGINT and SIGTERM interrupt the program, and both exit 0 here, not the
296
+ * 130 a default teardown reports.
297
+ *
298
+ * The explicit exit is the point. The specification makes the server
299
+ * terminate on `exit` with stdin still open, but `runMain` ends the process
300
+ * itself only for a non-zero code or a signal, and leaves a code-0 exit to
301
+ * the event loop draining. It never drains while stdin is open: a server
302
+ * reading stdin through `NodeStdio` sits until the client closes the pipe
303
+ * (probed), which an editor never does first. Any other handle the server
304
+ * does not own holds it the same way, such as the client-liveness interval
305
+ * `vscode-languageserver`'s node entry installs under `--clientProcessId`.
306
+ *
307
+ * `host` is the process: `LspStdio.teardown(process)`. The package never
308
+ * reads `process` itself. A non-zero code is ended by `runMain` before
309
+ * `host.exit` is reached.
310
+ */
311
+ static teardown(host: LspExitHost): Runtime.Teardown;
312
+ }
313
+ //#endregion
314
+ export type { LspExitHost, LspFrameDecoded, LspMessage, LspSessionEnd };
315
+ //# sourceMappingURL=index.d.ts.map
package/index.js ADDED
@@ -0,0 +1,4 @@
1
+ import { LspFrame, LspFrameError, LspFrameErrorCode } from "./LspFrame.js";
2
+ import { LspStdio } from "./LspStdio.js";
3
+
4
+ export { LspFrame, LspFrameError, LspFrameErrorCode, LspStdio };
@@ -0,0 +1,35 @@
1
+ import { Runtime } from "effect";
2
+
3
+ //#region src/internal/LaunchFailed.ts
4
+ /**
5
+ * The failure `LspStdio.launch` re-raises after reporting a failed server on
6
+ * stderr itself. `errorReported: false` keeps `runMain` from reporting it a
7
+ * second time — through a logger that writes to stdout, the LSP wire — and the
8
+ * original exit code rides along for the teardown.
9
+ *
10
+ * @remarks
11
+ * Both runtime markers are prototype getters rather than own fields, so a JSON
12
+ * or logger dump of the error does not carry them; the code lives in a private
13
+ * field.
14
+ *
15
+ * @internal
16
+ */
17
+ var LaunchFailed = class extends Error {
18
+ #code;
19
+ constructor(code) {
20
+ super(`the language server failed (exit ${code})`);
21
+ this.name = "LaunchFailed";
22
+ this.#code = code;
23
+ }
24
+ /** Already reported on stderr: `runMain` must not report it again. */
25
+ get [Runtime.errorReported]() {
26
+ return false;
27
+ }
28
+ /** The exit code the server failed with. */
29
+ get [Runtime.errorExitCode]() {
30
+ return this.#code;
31
+ }
32
+ };
33
+
34
+ //#endregion
35
+ export { LaunchFailed };
@@ -0,0 +1,30 @@
1
+ import { LspTestFailure } from "../LspTestFailure.js";
2
+
3
+ //#region src/internal/messages.ts
4
+ /** How much text a failure message echoes. */
5
+ const ECHO = 2e3;
6
+ /**
7
+ * Cut `text` to the echo limit, marking the cut.
8
+ *
9
+ * @internal
10
+ */
11
+ const truncate = (text) => text.length <= ECHO ? text : `${text.slice(0, ECHO)}…`;
12
+ /**
13
+ * A JSON object whose `jsonrpc` is `"2.0"`; a reader tells the three kinds
14
+ * apart by their fields.
15
+ *
16
+ * @internal
17
+ */
18
+ const isLspMessage = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && value.jsonrpc === "2.0";
19
+ /**
20
+ * The `NotJsonRpc` failure for a well-framed body that is no JSON-RPC 2.0 message.
21
+ *
22
+ * @internal
23
+ */
24
+ const notJsonRpc = (message) => new LspTestFailure({
25
+ reason: "NotJsonRpc",
26
+ message: `the server sent a frame that is not a JSON-RPC 2.0 message: ${truncate(JSON.stringify(message) ?? String(message))}`
27
+ });
28
+
29
+ //#endregion
30
+ export { isLspMessage, notJsonRpc, truncate };
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
- "name": "@effected/lsp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@effected/lsp",
3
+ "version": "0.1.1",
4
+ "private": false,
5
+ "description": "Language Server Protocol base-protocol framing for Effect v4, plus a packed-install initialize probe for a Language Server bin",
6
+ "keywords": [
7
+ "effect",
8
+ "lsp",
9
+ "language-server-protocol",
10
+ "json-rpc",
11
+ "content-length",
12
+ "effected"
13
+ ],
14
+ "homepage": "https://github.com/spencerbeggs/effected/tree/main/packages/lsp#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/spencerbeggs/effected/issues"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/spencerbeggs/effected.git",
21
+ "directory": "packages/lsp"
22
+ },
23
+ "license": "MIT",
24
+ "author": {
25
+ "name": "C. Spencer Beggs",
26
+ "email": "spencer@beggs.codes",
27
+ "url": "https://spencerbeg.gs"
28
+ },
29
+ "sideEffects": false,
30
+ "type": "module",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./index.d.ts",
34
+ "import": "./index.js",
35
+ "default": "./index.js"
36
+ },
37
+ "./testing": {
38
+ "types": "./testing.d.ts",
39
+ "import": "./testing.js",
40
+ "default": "./testing.js"
41
+ },
42
+ "./package.json": "./package.json"
43
+ },
44
+ "peerDependencies": {
45
+ "effect": "^4.0.0"
46
+ },
47
+ "engines": {
48
+ "node": ">=24.11.0"
49
+ }
50
+ }