@tsrx/oxc 0.0.0-trusted-publishing-bootstrap → 0.8.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/README.md +141 -0
- package/THIRD_PARTY_NOTICES.md +49 -0
- package/bin/oxc-tsrx +2 -0
- package/bin/oxc-tsrx-fmt +2 -0
- package/bin/oxc-tsrx-lint +2 -0
- package/bin/oxc-tsrx-lsp +2 -0
- package/bin/oxfmt +2 -0
- package/bin/oxlint +2 -0
- package/dist/bin/oxc-tsrx-fmt.js +13 -0
- package/dist/bin/oxc-tsrx-lint.js +13 -0
- package/dist/bin/oxc-tsrx-lsp.js +13 -0
- package/dist/bin/oxc-tsrx.js +115 -0
- package/dist/bin/oxfmt.js +24 -0
- package/dist/bin/oxlint.js +33 -0
- package/dist/canonical-command.d.ts +50 -0
- package/dist/canonical-command.js +196 -0
- package/dist/compat.d.ts +149 -0
- package/dist/compat.js +1615 -0
- package/dist/editor-resolution.js +508 -0
- package/dist/format-cli.js +276 -0
- package/dist/format-invocation.js +97 -0
- package/dist/format.d.ts +1 -0
- package/dist/format.js +56 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +16 -0
- package/dist/lint-cli.js +487 -0
- package/dist/lint-invocation.js +192 -0
- package/dist/lint-js-plugins.js +819 -0
- package/dist/lint-plugins-dev.d.ts +1 -0
- package/dist/lint-plugins-dev.js +2 -0
- package/dist/lint-prestart.js +16 -0
- package/dist/lint.d.ts +1 -0
- package/dist/lint.js +2 -0
- package/dist/native-targets.js +76 -0
- package/dist/oxlint-lsp-multiplexer.js +622 -0
- package/dist/package-binary.js +29 -0
- package/dist/parser.d.ts +216 -0
- package/dist/parser.js +557 -0
- package/dist/process.js +88 -0
- package/dist/provider-resolve.d.ts +160 -0
- package/dist/provider-resolve.js +471 -0
- package/dist/providers-report.js +49 -0
- package/dist/runtime.js +323 -0
- package/dist/spawn-command.d.ts +20 -0
- package/dist/spawn-command.js +87 -0
- package/dist/tsrx-core-compat/facade.js +1184 -0
- package/dist/tsrx-core-compat/index.d.ts +6 -0
- package/dist/tsrx-core-compat/index.js +9 -0
- package/dist/tsrx-core-compat/style.js +525 -0
- package/dist/tsrx-core-compat/types/estree.d.ts +20 -0
- package/dist/tsrx-core-compat/types/index.d.ts +50 -0
- package/dist/tsrx-transfer.js +352 -0
- package/package.json +144 -5
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
import { discoverProviders, extensionOf, findProjectRoot } from "./provider-resolve.js";
|
|
2
|
+
import { spawnCommand } from "./spawn-command.js";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { basename, sep } from "node:path";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
+
import { closeSync, openSync, readSync } from "node:fs";
|
|
8
|
+
//#region src/oxlint-lsp-multiplexer.ts
|
|
9
|
+
const REGISTER_REQUEST_ID = "$/oxc-tsrx/register-capabilities";
|
|
10
|
+
const CANONICAL_SERVER_REQUEST_PREFIX = "$/oxc-tsrx/canonical-request/";
|
|
11
|
+
const PROVIDER_PREFIX = "$/oxc-tsrx/provider/";
|
|
12
|
+
const BROADCAST_NOTIFICATIONS = /* @__PURE__ */ new Set([
|
|
13
|
+
"workspace/didChangeConfiguration",
|
|
14
|
+
"workspace/didChangeWatchedFiles",
|
|
15
|
+
"workspace/didChangeWorkspaceFolders"
|
|
16
|
+
]);
|
|
17
|
+
function providerInitializeId(id) {
|
|
18
|
+
return `${PROVIDER_PREFIX}${id}/initialize`;
|
|
19
|
+
}
|
|
20
|
+
function providerShutdownId(id) {
|
|
21
|
+
return `${PROVIDER_PREFIX}${id}/shutdown`;
|
|
22
|
+
}
|
|
23
|
+
function providerRequestPrefix(id) {
|
|
24
|
+
return `${PROVIDER_PREFIX}${id}/request/`;
|
|
25
|
+
}
|
|
26
|
+
function requestKey(id) {
|
|
27
|
+
return `${typeof id}:${String(id)}`;
|
|
28
|
+
}
|
|
29
|
+
function isRequest(message) {
|
|
30
|
+
return message?.method !== void 0 && message.id !== void 0;
|
|
31
|
+
}
|
|
32
|
+
function isResponse(message) {
|
|
33
|
+
return message?.method === void 0 && message?.id !== void 0;
|
|
34
|
+
}
|
|
35
|
+
function textDocumentUri(message) {
|
|
36
|
+
const uri = message?.params?.textDocument?.uri ?? message?.params?.uri;
|
|
37
|
+
return typeof uri === "string" ? uri : null;
|
|
38
|
+
}
|
|
39
|
+
/** Accepts an array, a Set, or a discovered `index.extensions` object. */
|
|
40
|
+
function extensionSet(extensions) {
|
|
41
|
+
if (extensions === null || extensions === void 0) return /* @__PURE__ */ new Set();
|
|
42
|
+
const values = typeof extensions[Symbol.iterator] === "function" ? extensions : Object.keys(extensions);
|
|
43
|
+
return new Set([...values].map((extension) => String(extension).toLowerCase()));
|
|
44
|
+
}
|
|
45
|
+
/** The lowercase extension of the document a text-document message refers to. */
|
|
46
|
+
function documentExtension(message) {
|
|
47
|
+
if (typeof message?.method !== "string" || !message.method.startsWith("textDocument/")) return null;
|
|
48
|
+
const uri = textDocumentUri(message);
|
|
49
|
+
if (uri === null) return null;
|
|
50
|
+
try {
|
|
51
|
+
return extensionOf(decodeURIComponent(new URL(uri).pathname));
|
|
52
|
+
} catch {
|
|
53
|
+
return extensionOf(uri.split(/[?#]/u, 1)[0]);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Route only the extensions a discovered provider claims. Every other document
|
|
58
|
+
* message, and every non-document message, stays on canonical Oxlint.
|
|
59
|
+
*/
|
|
60
|
+
function isProviderDocumentMessage(message, extensions) {
|
|
61
|
+
const extension = documentExtension(message);
|
|
62
|
+
return extension !== null && extensionSet(extensions).has(extension);
|
|
63
|
+
}
|
|
64
|
+
function writeLspMessage(stream, message) {
|
|
65
|
+
const body = Buffer.from(JSON.stringify(message));
|
|
66
|
+
stream.write(Buffer.concat([Buffer.from(`Content-Length: ${body.length}\r\n\r\n`, "ascii"), body]));
|
|
67
|
+
}
|
|
68
|
+
function readLspMessages(stream, onMessage, onError = (error) => {
|
|
69
|
+
throw error;
|
|
70
|
+
}) {
|
|
71
|
+
let input = Buffer.alloc(0);
|
|
72
|
+
const onData = (chunk) => {
|
|
73
|
+
input = Buffer.concat([input, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
|
|
74
|
+
for (;;) {
|
|
75
|
+
const boundary = input.indexOf("\r\n\r\n");
|
|
76
|
+
if (boundary === -1) return;
|
|
77
|
+
const header = input.subarray(0, boundary).toString("ascii");
|
|
78
|
+
const match = /(?:^|\r\n)content-length:\s*(\d+)/iu.exec(header);
|
|
79
|
+
if (match === null) {
|
|
80
|
+
onError(/* @__PURE__ */ new Error(`LSP message is missing Content-Length: ${header}`));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const length = Number(match[1]);
|
|
84
|
+
const bodyStart = boundary + 4;
|
|
85
|
+
const bodyEnd = bodyStart + length;
|
|
86
|
+
if (input.length < bodyEnd) return;
|
|
87
|
+
const body = input.subarray(bodyStart, bodyEnd);
|
|
88
|
+
input = input.subarray(bodyEnd);
|
|
89
|
+
try {
|
|
90
|
+
onMessage(JSON.parse(body.toString("utf8")));
|
|
91
|
+
} catch (error) {
|
|
92
|
+
onError(/* @__PURE__ */ new Error(`LSP message contains invalid JSON: ${error instanceof Error ? error.message : String(error)}`));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
stream.on("data", onData);
|
|
98
|
+
return () => stream.off("data", onData);
|
|
99
|
+
}
|
|
100
|
+
function registrationRequest(extensions) {
|
|
101
|
+
const documentSelector = [...extensionSet(extensions)].sort().map((extension) => ({
|
|
102
|
+
scheme: "file",
|
|
103
|
+
pattern: `**/*${extension}`
|
|
104
|
+
}));
|
|
105
|
+
return {
|
|
106
|
+
jsonrpc: "2.0",
|
|
107
|
+
id: REGISTER_REQUEST_ID,
|
|
108
|
+
method: "client/registerCapability",
|
|
109
|
+
params: { registrations: [
|
|
110
|
+
{
|
|
111
|
+
id: "oxc-tsrx-did-open",
|
|
112
|
+
method: "textDocument/didOpen",
|
|
113
|
+
registerOptions: { documentSelector }
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: "oxc-tsrx-did-change",
|
|
117
|
+
method: "textDocument/didChange",
|
|
118
|
+
registerOptions: {
|
|
119
|
+
documentSelector,
|
|
120
|
+
syncKind: 1
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: "oxc-tsrx-did-save",
|
|
125
|
+
method: "textDocument/didSave",
|
|
126
|
+
registerOptions: { documentSelector }
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: "oxc-tsrx-did-close",
|
|
130
|
+
method: "textDocument/didClose",
|
|
131
|
+
registerOptions: { documentSelector }
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "oxc-tsrx-formatting",
|
|
135
|
+
method: "textDocument/formatting",
|
|
136
|
+
registerOptions: { documentSelector }
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: "oxc-tsrx-code-actions",
|
|
140
|
+
method: "textDocument/codeAction",
|
|
141
|
+
registerOptions: {
|
|
142
|
+
documentSelector,
|
|
143
|
+
codeActionKinds: ["quickfix"],
|
|
144
|
+
resolveProvider: false
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
] }
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function endpointExit(endpoint) {
|
|
151
|
+
return new Promise((resolve, reject) => {
|
|
152
|
+
endpoint.once("error", reject);
|
|
153
|
+
endpoint.once("close", (status, signal) => {
|
|
154
|
+
resolve({
|
|
155
|
+
status: status ?? 2,
|
|
156
|
+
signal
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The `initialize` a provider session should see.
|
|
163
|
+
*
|
|
164
|
+
* A provider resolves its configuration from the workspace root it is handed,
|
|
165
|
+
* and through that config the `jsPlugins` a project declares. When the editor
|
|
166
|
+
* opened a folder *above* the project that owns the provider, forwarding the
|
|
167
|
+
* client's root verbatim points the session at a directory with no
|
|
168
|
+
* `.oxlintrc.json`, so the project's own rules and JS plugins quietly stop
|
|
169
|
+
* applying in the editor while they still apply on the command line. Rewriting
|
|
170
|
+
* the root, and only the root, keeps both views of the project identical.
|
|
171
|
+
*/
|
|
172
|
+
function initializeForProvider(message, providerRoot) {
|
|
173
|
+
if (providerRoot === null) return message;
|
|
174
|
+
const uri = pathToFileURL(providerRoot).href;
|
|
175
|
+
return {
|
|
176
|
+
...message,
|
|
177
|
+
params: {
|
|
178
|
+
...message.params,
|
|
179
|
+
rootUri: uri,
|
|
180
|
+
rootPath: providerRoot,
|
|
181
|
+
workspaceFolders: [{
|
|
182
|
+
uri,
|
|
183
|
+
name: basename(providerRoot)
|
|
184
|
+
}]
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Compose canonical Oxlint and any discovered language provider servers behind
|
|
190
|
+
* one stdio LSP.
|
|
191
|
+
*
|
|
192
|
+
* `canonical` is a child-process-shaped endpoint. Each entry of `providers` is
|
|
193
|
+
* `{ id, extensions, start() }`; `start` is only called when the editor sends
|
|
194
|
+
* the first document message for an extension that provider claims, so an
|
|
195
|
+
* ordinary session never pays for a provider process. With an empty
|
|
196
|
+
* `providers` list every byte is forwarded to canonical Oxlint unchanged.
|
|
197
|
+
*/
|
|
198
|
+
function createOxlintLspMultiplexer({ clientInput, clientOutput, clientError, canonical, providers = [], providerRoot = null }) {
|
|
199
|
+
let clientInitialized = false;
|
|
200
|
+
let registered = false;
|
|
201
|
+
let registrationPending = false;
|
|
202
|
+
let initializeMessage = null;
|
|
203
|
+
let nextCanonicalServerRequest = 1;
|
|
204
|
+
const canonicalServerRequests = /* @__PURE__ */ new Map();
|
|
205
|
+
const clientRequestTargets = /* @__PURE__ */ new Map();
|
|
206
|
+
const disposeReaders = [];
|
|
207
|
+
const sessions = providers.map((provider) => ({
|
|
208
|
+
provider,
|
|
209
|
+
extensions: extensionSet(provider.extensions),
|
|
210
|
+
endpoint: null,
|
|
211
|
+
initializePending: false,
|
|
212
|
+
initialized: false,
|
|
213
|
+
failed: false,
|
|
214
|
+
started: false,
|
|
215
|
+
shutdownPending: false,
|
|
216
|
+
exit: null,
|
|
217
|
+
queued: [],
|
|
218
|
+
serverRequests: /* @__PURE__ */ new Map(),
|
|
219
|
+
nextServerRequest: 1
|
|
220
|
+
}));
|
|
221
|
+
const routedExtensions = new Set(sessions.flatMap((session) => [...session.extensions]));
|
|
222
|
+
const report = (message) => clientError.write(`oxlint (oxc-tsrx): ${message}\n`);
|
|
223
|
+
const sendCanonical = (message) => writeLspMessage(canonical.stdin, message);
|
|
224
|
+
const sendClient = (message) => writeLspMessage(clientOutput, message);
|
|
225
|
+
const sendSession = (session, message) => writeLspMessage(session.endpoint.stdin, message);
|
|
226
|
+
const protocolError = (source) => (error) => {
|
|
227
|
+
report(`${source} protocol error: ${error instanceof Error ? error.message : String(error)}`);
|
|
228
|
+
};
|
|
229
|
+
const failQueued = (session, reason) => {
|
|
230
|
+
for (const message of session.queued.splice(0)) {
|
|
231
|
+
if (!isRequest(message)) continue;
|
|
232
|
+
clientRequestTargets.delete(requestKey(message.id));
|
|
233
|
+
sendClient({
|
|
234
|
+
jsonrpc: "2.0",
|
|
235
|
+
id: message.id,
|
|
236
|
+
error: {
|
|
237
|
+
code: -32002,
|
|
238
|
+
message: reason
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
const startSession = (session) => {
|
|
244
|
+
if (session.started || !clientInitialized || !session.initialized || session.failed) return;
|
|
245
|
+
session.started = true;
|
|
246
|
+
sendSession(session, {
|
|
247
|
+
jsonrpc: "2.0",
|
|
248
|
+
method: "initialized",
|
|
249
|
+
params: {}
|
|
250
|
+
});
|
|
251
|
+
for (const message of session.queued.splice(0)) sendSession(session, message);
|
|
252
|
+
};
|
|
253
|
+
const onSessionMessage = (session, message) => {
|
|
254
|
+
const { id } = session.provider;
|
|
255
|
+
if (session.initializePending && isResponse(message) && message.id === providerInitializeId(id)) {
|
|
256
|
+
session.initializePending = false;
|
|
257
|
+
if (message.error !== void 0) {
|
|
258
|
+
session.failed = true;
|
|
259
|
+
report(`the ${id} language server failed to initialize: ${JSON.stringify(message.error)}`);
|
|
260
|
+
failQueued(session, `The ${id} language server did not initialize`);
|
|
261
|
+
} else {
|
|
262
|
+
session.initialized = true;
|
|
263
|
+
startSession(session);
|
|
264
|
+
}
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (session.shutdownPending && isResponse(message) && message.id === providerShutdownId(id)) {
|
|
268
|
+
session.shutdownPending = false;
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (isRequest(message)) {
|
|
272
|
+
const proxyId = `${providerRequestPrefix(id)}${session.nextServerRequest++}`;
|
|
273
|
+
session.serverRequests.set(requestKey(proxyId), message.id);
|
|
274
|
+
sendClient({
|
|
275
|
+
...message,
|
|
276
|
+
id: proxyId
|
|
277
|
+
});
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (isResponse(message)) clientRequestTargets.delete(requestKey(message.id));
|
|
281
|
+
sendClient(message);
|
|
282
|
+
};
|
|
283
|
+
const ensureSession = (session) => {
|
|
284
|
+
if (session.endpoint !== null || session.failed) return;
|
|
285
|
+
if (initializeMessage === null) return;
|
|
286
|
+
let endpoint;
|
|
287
|
+
try {
|
|
288
|
+
endpoint = session.provider.start();
|
|
289
|
+
} catch (error) {
|
|
290
|
+
session.failed = true;
|
|
291
|
+
report(`the ${session.provider.id} language server could not start: ${error instanceof Error ? error.message : String(error)}`);
|
|
292
|
+
failQueued(session, `The ${session.provider.id} language server could not start`);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
session.endpoint = endpoint;
|
|
296
|
+
session.exit = endpointExit(endpoint).catch((error) => ({
|
|
297
|
+
status: 2,
|
|
298
|
+
signal: null,
|
|
299
|
+
error
|
|
300
|
+
}));
|
|
301
|
+
disposeReaders.push(readLspMessages(endpoint.stdout, (message) => onSessionMessage(session, message), protocolError(`the ${session.provider.id} language server`)));
|
|
302
|
+
endpoint.stderr?.on("data", (chunk) => clientError.write(chunk));
|
|
303
|
+
session.initializePending = true;
|
|
304
|
+
sendSession(session, {
|
|
305
|
+
...initializeForProvider(initializeMessage, providerRoot),
|
|
306
|
+
id: providerInitializeId(session.provider.id)
|
|
307
|
+
});
|
|
308
|
+
};
|
|
309
|
+
const sessionFor = (message) => {
|
|
310
|
+
if (routedExtensions.size === 0) return null;
|
|
311
|
+
const extension = documentExtension(message);
|
|
312
|
+
if (extension === null) return null;
|
|
313
|
+
return sessions.find((session) => session.extensions.has(extension)) ?? null;
|
|
314
|
+
};
|
|
315
|
+
const deliver = (session, message) => {
|
|
316
|
+
ensureSession(session);
|
|
317
|
+
if (session.failed) {
|
|
318
|
+
if (isRequest(message)) {
|
|
319
|
+
clientRequestTargets.delete(requestKey(message.id));
|
|
320
|
+
sendClient({
|
|
321
|
+
jsonrpc: "2.0",
|
|
322
|
+
id: message.id,
|
|
323
|
+
error: {
|
|
324
|
+
code: -32002,
|
|
325
|
+
message: `The ${session.provider.id} language server is unavailable`
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (session.started) sendSession(session, message);
|
|
332
|
+
else session.queued.push(message);
|
|
333
|
+
};
|
|
334
|
+
const onClientMessage = (message) => {
|
|
335
|
+
if (isResponse(message)) {
|
|
336
|
+
if (registrationPending && message.id === REGISTER_REQUEST_ID) {
|
|
337
|
+
registrationPending = false;
|
|
338
|
+
if (message.error !== void 0) report(`the editor rejected provider capabilities: ${JSON.stringify(message.error)}`);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
for (const session of sessions) {
|
|
342
|
+
const original = session.serverRequests.get(requestKey(message.id));
|
|
343
|
+
if (original === void 0) continue;
|
|
344
|
+
session.serverRequests.delete(requestKey(message.id));
|
|
345
|
+
if (session.endpoint !== null) sendSession(session, {
|
|
346
|
+
...message,
|
|
347
|
+
id: original
|
|
348
|
+
});
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const canonicalRequest = canonicalServerRequests.get(requestKey(message.id));
|
|
352
|
+
if (canonicalRequest !== void 0) {
|
|
353
|
+
canonicalServerRequests.delete(requestKey(message.id));
|
|
354
|
+
sendCanonical({
|
|
355
|
+
...message,
|
|
356
|
+
id: canonicalRequest
|
|
357
|
+
});
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
sendCanonical(message);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (message.method === "initialize" && isRequest(message)) {
|
|
364
|
+
initializeMessage = message;
|
|
365
|
+
clientRequestTargets.set(requestKey(message.id), "canonical");
|
|
366
|
+
sendCanonical(message);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (message.method === "initialized") {
|
|
370
|
+
clientInitialized = true;
|
|
371
|
+
sendCanonical(message);
|
|
372
|
+
if (routedExtensions.size > 0 && !registered) {
|
|
373
|
+
registered = true;
|
|
374
|
+
registrationPending = true;
|
|
375
|
+
sendClient(registrationRequest(routedExtensions));
|
|
376
|
+
}
|
|
377
|
+
for (const session of sessions) startSession(session);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (message.method === "shutdown" && isRequest(message)) {
|
|
381
|
+
clientRequestTargets.set(requestKey(message.id), "canonical");
|
|
382
|
+
sendCanonical(message);
|
|
383
|
+
for (const session of sessions) {
|
|
384
|
+
if (!session.started) continue;
|
|
385
|
+
session.shutdownPending = true;
|
|
386
|
+
sendSession(session, {
|
|
387
|
+
...message,
|
|
388
|
+
id: providerShutdownId(session.provider.id)
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (message.method === "exit") {
|
|
394
|
+
sendCanonical(message);
|
|
395
|
+
for (const session of sessions) if (session.endpoint !== null) sendSession(session, message);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (message.method === "$/cancelRequest") {
|
|
399
|
+
const target = clientRequestTargets.get(requestKey(message.params?.id));
|
|
400
|
+
if (target !== void 0 && target !== "canonical") deliver(target, message);
|
|
401
|
+
else sendCanonical(message);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (BROADCAST_NOTIFICATIONS.has(message.method) && message.id === void 0) {
|
|
405
|
+
sendCanonical(message);
|
|
406
|
+
for (const session of sessions) if (session.started) sendSession(session, message);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const session = sessionFor(message);
|
|
410
|
+
if (session !== null) {
|
|
411
|
+
if (isRequest(message)) clientRequestTargets.set(requestKey(message.id), session);
|
|
412
|
+
deliver(session, message);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (isRequest(message)) clientRequestTargets.set(requestKey(message.id), "canonical");
|
|
416
|
+
sendCanonical(message);
|
|
417
|
+
};
|
|
418
|
+
const onCanonicalMessage = (message) => {
|
|
419
|
+
if (isRequest(message)) {
|
|
420
|
+
const proxyId = `${CANONICAL_SERVER_REQUEST_PREFIX}${nextCanonicalServerRequest++}`;
|
|
421
|
+
canonicalServerRequests.set(requestKey(proxyId), message.id);
|
|
422
|
+
sendClient({
|
|
423
|
+
...message,
|
|
424
|
+
id: proxyId
|
|
425
|
+
});
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (isResponse(message)) clientRequestTargets.delete(requestKey(message.id));
|
|
429
|
+
sendClient(message);
|
|
430
|
+
};
|
|
431
|
+
disposeReaders.push(readLspMessages(clientInput, onClientMessage, protocolError("editor")), readLspMessages(canonical.stdout, onCanonicalMessage, protocolError("canonical Oxlint")));
|
|
432
|
+
canonical.stderr?.on("data", (chunk) => clientError.write(chunk));
|
|
433
|
+
clientInput.on("end", () => {
|
|
434
|
+
canonical.stdin.end();
|
|
435
|
+
for (const session of sessions) if (session.endpoint !== null) session.endpoint.stdin.end();
|
|
436
|
+
});
|
|
437
|
+
return {
|
|
438
|
+
extensions: [...routedExtensions].sort(),
|
|
439
|
+
startedProviders: () => sessions.filter((session) => session.endpoint !== null).map(({ provider }) => provider.id),
|
|
440
|
+
closed: (async () => {
|
|
441
|
+
return {
|
|
442
|
+
canonical: await endpointExit(canonical),
|
|
443
|
+
providers: await Promise.all(sessions.filter((session) => session.exit !== null).map((session) => session.exit))
|
|
444
|
+
};
|
|
445
|
+
})(),
|
|
446
|
+
kill(signal) {
|
|
447
|
+
canonical.kill(signal);
|
|
448
|
+
for (const session of sessions) session.endpoint?.kill(signal);
|
|
449
|
+
},
|
|
450
|
+
dispose() {
|
|
451
|
+
for (const dispose of disposeReaders.splice(0)) dispose();
|
|
452
|
+
clientInput.pause?.();
|
|
453
|
+
clientInput.unref?.();
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function resolveCanonicalOxlintBinary() {
|
|
458
|
+
const require = createRequire(import.meta.url);
|
|
459
|
+
const canonicalManifest = require.resolve("oxlint-current/package.json");
|
|
460
|
+
const manifest = require(canonicalManifest);
|
|
461
|
+
const declared = typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.oxlint;
|
|
462
|
+
if (typeof declared !== "string" || declared.length === 0) throw new Error("oxlint-current does not declare the oxlint binary");
|
|
463
|
+
return fileURLToPath(new URL(declared, pathToFileURL(canonicalManifest)));
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* A declared `bin` entry may be a JavaScript wrapper or a native executable.
|
|
467
|
+
* Reading the shebang is a static file read, not an execution of the package.
|
|
468
|
+
*
|
|
469
|
+
* A UTF-8 byte-order mark sits in front of the `#!` and is common in files
|
|
470
|
+
* authored on Windows, so it is stripped before the test: misreading a Node
|
|
471
|
+
* wrapper as a native executable would make this spawn an extensionless file,
|
|
472
|
+
* which Windows cannot run at all.
|
|
473
|
+
*/
|
|
474
|
+
function usesNodeInterpreter(path) {
|
|
475
|
+
let descriptor;
|
|
476
|
+
try {
|
|
477
|
+
descriptor = openSync(path, "r");
|
|
478
|
+
const buffer = Buffer.alloc(128);
|
|
479
|
+
const read = readSync(descriptor, buffer, 0, 128, 0);
|
|
480
|
+
const shebang = buffer.subarray(0, read).toString("utf8").replace(/^\uFEFF/u, "").split("\n", 1)[0];
|
|
481
|
+
return shebang.startsWith("#!") && /\bnode(?:\.exe)?\b/u.test(shebang);
|
|
482
|
+
} catch {
|
|
483
|
+
return false;
|
|
484
|
+
} finally {
|
|
485
|
+
if (descriptor !== void 0) closeSync(descriptor);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Turn a discovered index into lazily startable language server sessions. Only
|
|
490
|
+
* providers that declare an `lsp` capability contribute; their claimed
|
|
491
|
+
* extensions are the exact routing set.
|
|
492
|
+
*/
|
|
493
|
+
function providerLspSessions(index, spawnProcess, childOptions) {
|
|
494
|
+
const sessions = [];
|
|
495
|
+
for (const provider of index?.providers ?? []) {
|
|
496
|
+
const extensions = [];
|
|
497
|
+
let command = null;
|
|
498
|
+
for (const language of provider.languages) {
|
|
499
|
+
const capability = language.capabilities?.lsp;
|
|
500
|
+
if (capability?.kind !== "bin") continue;
|
|
501
|
+
command = capability.path;
|
|
502
|
+
for (const extension of language.extensions) if (index.extensions?.[extension]?.package === provider.name) extensions.push(extension);
|
|
503
|
+
}
|
|
504
|
+
if (command === null || extensions.length === 0) continue;
|
|
505
|
+
sessions.push({
|
|
506
|
+
id: provider.id,
|
|
507
|
+
package: provider.name,
|
|
508
|
+
command,
|
|
509
|
+
extensions,
|
|
510
|
+
start: () => usesNodeInterpreter(command) ? spawnProcess(process.execPath, [command, "--stdio"], childOptions) : spawnCommand(command, ["--stdio"], childOptions, spawnProcess)
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
return sessions;
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Every project root that could own the provider index for this session, after
|
|
517
|
+
* the editor's own workspace root.
|
|
518
|
+
*
|
|
519
|
+
* VS Code opens whatever folder the user picked, and that is routinely a plain
|
|
520
|
+
* directory *above* the project that installed this package: a scaffold inside a
|
|
521
|
+
* demo folder, an app inside a repo that declares no workspace. Discovery rooted
|
|
522
|
+
* at the opened folder then finds no provider, registers no `.tsrx` capability,
|
|
523
|
+
* and the session serves nothing while looking perfectly healthy.
|
|
524
|
+
*
|
|
525
|
+
* This process is running out of the installing project's `node_modules`, so the
|
|
526
|
+
* path it was launched from names that project: everything before the first
|
|
527
|
+
* `node_modules` segment. Both the launched script and this module are checked,
|
|
528
|
+
* because a package manager may hand over either the symlink or the real path.
|
|
529
|
+
*/
|
|
530
|
+
function installingProjectRoots() {
|
|
531
|
+
const roots = [];
|
|
532
|
+
for (const candidate of [process.argv[1], fileURLToPath(import.meta.url)]) {
|
|
533
|
+
if (typeof candidate !== "string" || candidate.length === 0) continue;
|
|
534
|
+
const marker = candidate.indexOf(`${sep}node_modules${sep}`);
|
|
535
|
+
if (marker === -1) continue;
|
|
536
|
+
const root = candidate.slice(0, marker);
|
|
537
|
+
if (root.length > 0 && !roots.includes(root)) roots.push(root);
|
|
538
|
+
}
|
|
539
|
+
return roots;
|
|
540
|
+
}
|
|
541
|
+
async function discoverProviderIndex(cwd, report) {
|
|
542
|
+
try {
|
|
543
|
+
const root = await findProjectRoot(cwd);
|
|
544
|
+
let index = await discoverProviders({ root });
|
|
545
|
+
if (index.providers.length === 0) for (const candidate of installingProjectRoots()) {
|
|
546
|
+
if (candidate === root) continue;
|
|
547
|
+
const nested = await discoverProviders({ root: candidate });
|
|
548
|
+
if (nested.providers.length === 0) continue;
|
|
549
|
+
report(`the opened folder ${root} declares no language provider, so discovery used the project that installed this package instead: ${candidate}`);
|
|
550
|
+
index = nested;
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
for (const diagnostic of index.diagnostics) report(`${diagnostic.severity}: ${diagnostic.message}`);
|
|
554
|
+
return index;
|
|
555
|
+
} catch (error) {
|
|
556
|
+
report(`language provider discovery failed, continuing with canonical Oxlint only: ${error instanceof Error ? error.message : String(error)}`);
|
|
557
|
+
return {
|
|
558
|
+
root: cwd,
|
|
559
|
+
providers: [],
|
|
560
|
+
extensions: {},
|
|
561
|
+
diagnostics: []
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
async function runOxlintLspMultiplexer(args, options = {}) {
|
|
566
|
+
const spawnProcess = options.spawn ?? spawn;
|
|
567
|
+
const clientError = options.clientError ?? process.stderr;
|
|
568
|
+
const cwd = options.cwd ?? process.cwd();
|
|
569
|
+
const childOptions = {
|
|
570
|
+
cwd,
|
|
571
|
+
env: {
|
|
572
|
+
...process.env,
|
|
573
|
+
NO_COLOR: "1"
|
|
574
|
+
},
|
|
575
|
+
stdio: [
|
|
576
|
+
"pipe",
|
|
577
|
+
"pipe",
|
|
578
|
+
"pipe"
|
|
579
|
+
]
|
|
580
|
+
};
|
|
581
|
+
const index = options.index ?? await discoverProviderIndex(cwd, (message) => clientError.write(`oxlint (oxc-tsrx): ${message}\n`));
|
|
582
|
+
const providerOptions = typeof index?.root === "string" && index.root.length > 0 && index.root !== cwd ? {
|
|
583
|
+
...childOptions,
|
|
584
|
+
cwd: index.root
|
|
585
|
+
} : childOptions;
|
|
586
|
+
const providers = providerLspSessions(index, spawnProcess, providerOptions);
|
|
587
|
+
const canonical = spawnProcess(process.execPath, [resolveCanonicalOxlintBinary(), ...args], childOptions);
|
|
588
|
+
const multiplexer = createOxlintLspMultiplexer({
|
|
589
|
+
providerRoot: providerOptions === childOptions ? null : index.root,
|
|
590
|
+
clientInput: options.clientInput ?? process.stdin,
|
|
591
|
+
clientOutput: options.clientOutput ?? process.stdout,
|
|
592
|
+
clientError,
|
|
593
|
+
canonical,
|
|
594
|
+
providers
|
|
595
|
+
});
|
|
596
|
+
const shutdownGraceMs = 2e3;
|
|
597
|
+
let escalation;
|
|
598
|
+
const forwardSignal = (signal) => {
|
|
599
|
+
multiplexer.kill(signal);
|
|
600
|
+
if (escalation) return;
|
|
601
|
+
escalation = setTimeout(() => {
|
|
602
|
+
multiplexer.kill("SIGKILL");
|
|
603
|
+
escalation = setTimeout(() => process.exit(signal === "SIGINT" ? 130 : 143), shutdownGraceMs);
|
|
604
|
+
escalation.unref?.();
|
|
605
|
+
}, shutdownGraceMs);
|
|
606
|
+
escalation.unref?.();
|
|
607
|
+
};
|
|
608
|
+
const signals = ["SIGINT", "SIGTERM"];
|
|
609
|
+
for (const signal of signals) process.once(signal, forwardSignal);
|
|
610
|
+
try {
|
|
611
|
+
const exits = await multiplexer.closed;
|
|
612
|
+
if (exits.canonical.signal !== null) clientError.write?.(`canonical Oxlint exited with ${exits.canonical.signal}\n`);
|
|
613
|
+
for (const providerExit of exits.providers) if (providerExit.signal !== null) clientError.write?.(`a language provider server exited with ${providerExit.signal}\n`);
|
|
614
|
+
return Math.max(exits.canonical.status, ...exits.providers.map((exit) => exit.status), 0);
|
|
615
|
+
} finally {
|
|
616
|
+
if (escalation) clearTimeout(escalation);
|
|
617
|
+
multiplexer.dispose();
|
|
618
|
+
for (const signal of signals) process.off(signal, forwardSignal);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
//#endregion
|
|
622
|
+
export { createOxlintLspMultiplexer, documentExtension, isProviderDocumentMessage, providerLspSessions, readLspMessages, registrationRequest, runOxlintLspMultiplexer, writeLspMessage };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { statSync } from "node:fs";
|
|
5
|
+
//#region src/package-binary.ts
|
|
6
|
+
/** Resolve the executable declared by an installed npm package's `bin` field. */
|
|
7
|
+
function resolvePackageBinary(packageName, binaryName, fromUrl) {
|
|
8
|
+
const localRequire = createRequire(fromUrl);
|
|
9
|
+
const manifestPath = localRequire.resolve(`${packageName}/package.json`);
|
|
10
|
+
const manifest = localRequire(manifestPath);
|
|
11
|
+
const declared = typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.[binaryName];
|
|
12
|
+
if (typeof declared !== "string" || declared.length === 0) throw new Error(`${packageName} does not declare its ${binaryName} npm binary`);
|
|
13
|
+
const entry = resolve(dirname(manifestPath), declared);
|
|
14
|
+
let metadata;
|
|
15
|
+
try {
|
|
16
|
+
metadata = statSync(entry);
|
|
17
|
+
} catch {
|
|
18
|
+
throw new Error(`${packageName} declares a missing ${binaryName} npm binary at ${entry}`);
|
|
19
|
+
}
|
|
20
|
+
if (!metadata.isFile()) throw new Error(`${packageName} declares a non-file ${binaryName} npm binary at ${entry}`);
|
|
21
|
+
return entry;
|
|
22
|
+
}
|
|
23
|
+
/** Execute a declared JavaScript npm binary in this process. */
|
|
24
|
+
async function importDeclaredPackageBinary(packageName, binaryName, fromUrl) {
|
|
25
|
+
const entry = resolvePackageBinary(packageName, binaryName, fromUrl);
|
|
26
|
+
await import(pathToFileURL(entry).href);
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { importDeclaredPackageBinary, resolvePackageBinary };
|