Skip to content

Commit b233bb3

Browse files
authored
feat(lsp): add oxlint LSP server support (#4)
* feat(lsp): add oxlint LSP server support Add built-in oxlint LSP server based on opencode PR #5570. - Add OxlintServer with smart binary resolution (oxlint --lsp first, then oxc_language_server fallback) - Support JS/TS and framework extensions (.vue, .astro, .svelte) - Export OxlintServer from module - Add comprehensive tests Closes #3 * fix(lsp): improve error handling in OxlintServer spawn Address PR review feedback: - Check for ENOENT specifically in resolveBin, log other errors - Check oxlint --help exit code before trusting output - Add diagnostic message when neither binary is found
1 parent baaffc9 commit b233bb3

3 files changed

Lines changed: 136 additions & 0 deletions

File tree

packages/lsp/src/__tests__/server.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
LSP_SERVERS,
44
TypescriptServer,
55
DenoServer,
6+
OxlintServer,
67
PyrightServer,
78
GoplsServer,
89
RustAnalyzerServer,
@@ -17,6 +18,7 @@ describe("LSP_SERVERS", () => {
1718
const serverIds = LSP_SERVERS.map((s) => s.id);
1819
expect(serverIds).toContain("typescript");
1920
expect(serverIds).toContain("deno");
21+
expect(serverIds).toContain("oxlint");
2022
expect(serverIds).toContain("pyright");
2123
expect(serverIds).toContain("gopls");
2224
expect(serverIds).toContain("rust-analyzer");
@@ -56,6 +58,37 @@ describe("DenoServer", () => {
5658
});
5759
});
5860

61+
describe("OxlintServer", () => {
62+
test("has correct id", () => {
63+
expect(OxlintServer.id).toBe("oxlint");
64+
});
65+
66+
test("supports JavaScript/TypeScript extensions", () => {
67+
expect(OxlintServer.extensions).toContain(".ts");
68+
expect(OxlintServer.extensions).toContain(".tsx");
69+
expect(OxlintServer.extensions).toContain(".js");
70+
expect(OxlintServer.extensions).toContain(".jsx");
71+
expect(OxlintServer.extensions).toContain(".mjs");
72+
expect(OxlintServer.extensions).toContain(".cjs");
73+
expect(OxlintServer.extensions).toContain(".mts");
74+
expect(OxlintServer.extensions).toContain(".cts");
75+
});
76+
77+
test("supports framework-specific extensions", () => {
78+
expect(OxlintServer.extensions).toContain(".vue");
79+
expect(OxlintServer.extensions).toContain(".astro");
80+
expect(OxlintServer.extensions).toContain(".svelte");
81+
});
82+
83+
test("has root function", () => {
84+
expect(typeof OxlintServer.root).toBe("function");
85+
});
86+
87+
test("has spawn function", () => {
88+
expect(typeof OxlintServer.spawn).toBe("function");
89+
});
90+
});
91+
5992
describe("PyrightServer", () => {
6093
test("has correct id", () => {
6194
expect(PyrightServer.id).toBe("pyright");

packages/lsp/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,7 @@ export {
385385
LSP_SERVERS,
386386
TypescriptServer,
387387
DenoServer,
388+
OxlintServer,
388389
PyrightServer,
389390
GoplsServer,
390391
RustAnalyzerServer,

packages/lsp/src/server.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,12 +213,114 @@ export const RustAnalyzerServer: LSPServerInfo = {
213213
},
214214
};
215215

216+
/**
217+
* Oxlint Language Server
218+
* Based on opencode PR #5570
219+
*/
220+
export const OxlintServer: LSPServerInfo = {
221+
id: "oxlint",
222+
extensions: [
223+
".ts",
224+
".tsx",
225+
".js",
226+
".jsx",
227+
".mjs",
228+
".cjs",
229+
".mts",
230+
".cts",
231+
".vue",
232+
".astro",
233+
".svelte",
234+
],
235+
root: nearestRoot([
236+
".oxlintrc.json",
237+
"package-lock.json",
238+
"bun.lockb",
239+
"bun.lock",
240+
"pnpm-lock.yaml",
241+
"yarn.lock",
242+
"package.json",
243+
]),
244+
async spawn(root) {
245+
const ext = process.platform === "win32" ? ".cmd" : "";
246+
247+
const resolveBin = async (
248+
binName: string
249+
): Promise<string | undefined> => {
250+
const localBin = path.join(root, "node_modules", ".bin", binName + ext);
251+
try {
252+
await fs.access(localBin);
253+
return localBin;
254+
} catch (err: unknown) {
255+
// Only ignore ENOENT (file not found), log other errors
256+
const isNotFound =
257+
err instanceof Error &&
258+
"code" in err &&
259+
(err as NodeJS.ErrnoException).code === "ENOENT";
260+
if (!isNotFound) {
261+
console.error(`[oxlint] Cannot access local binary at ${localBin}:`, err);
262+
}
263+
}
264+
265+
// Check global PATH
266+
const globalBin = Bun.which(binName);
267+
if (globalBin) return globalBin;
268+
269+
return undefined;
270+
};
271+
272+
// Try oxlint with --lsp flag first
273+
const lintBin = await resolveBin("oxlint");
274+
if (lintBin) {
275+
const proc = Bun.spawn([lintBin, "--help"], {
276+
stdout: "pipe",
277+
stderr: "pipe",
278+
});
279+
const exitCode = await proc.exited;
280+
281+
if (exitCode !== 0) {
282+
const stderr = await new Response(proc.stderr).text();
283+
console.warn(
284+
`[oxlint] Binary at ${lintBin} failed --help (exit ${exitCode}): ${stderr.slice(0, 200)}`
285+
);
286+
} else {
287+
const help = await new Response(proc.stdout).text();
288+
if (help.includes("--lsp")) {
289+
return {
290+
process: spawn(lintBin, ["--lsp"], {
291+
cwd: root,
292+
}),
293+
};
294+
}
295+
}
296+
}
297+
298+
// Fallback to oxc_language_server
299+
const serverBin = await resolveBin("oxc_language_server");
300+
if (serverBin) {
301+
return {
302+
process: spawn(serverBin, [], {
303+
cwd: root,
304+
}),
305+
};
306+
}
307+
308+
// Neither found - log diagnostic
309+
console.warn(
310+
`[oxlint] Could not start oxlint LSP server for ${root}. ` +
311+
`Install with: npm install -D oxlint`
312+
);
313+
return undefined;
314+
},
315+
};
316+
216317
/**
217318
* All available LSP servers
218319
*/
219320
export const LSP_SERVERS: LSPServerInfo[] = [
220321
DenoServer, // Deno first, higher priority for Deno projects
221322
TypescriptServer,
323+
OxlintServer,
222324
PyrightServer,
223325
GoplsServer,
224326
RustAnalyzerServer,

0 commit comments

Comments
 (0)