From 7f6f25eb224860f7dc0602230d5eb881142d1ec8 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Thu, 22 Oct 2026 15:01:26 +0900 Subject: [PATCH 1/3] fix(lsp): fall back to downloaded dart sdk when system dart cannot run A version-manager shim (e.g. mise) can resolve on PATH but exit non-zero when no SDK is installed for the project. Probe `dart --version` and use the system dart only if it exits 0; otherwise log a warning and fall back to the auto-downloaded SDK. Refs #127 --- packages/lsp/src/server/dart.ts | 27 ++++++++++++++++++++-- packages/lsp/test/unit/server.test.ts | 32 +++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/packages/lsp/src/server/dart.ts b/packages/lsp/src/server/dart.ts index b942f8d..55d6990 100644 --- a/packages/lsp/src/server/dart.ts +++ b/packages/lsp/src/server/dart.ts @@ -111,13 +111,36 @@ async function setupDartDependencies(platformId: PlatformId): Promise { + const systemDart = Bun.which('dart') + if (!systemDart) { + return undefined + } + + const proc = Bun.spawn([systemDart, '--version'], { + stdout: 'ignore', + stderr: 'pipe', + }) + const exitCode = await proc.exited + if (exitCode !== 0) { + const stderr = await new Response(proc.stderr).text() + log.warn({ systemDart, exitCode, stderr: stderr.slice(0, 200) }, 'System Dart failed --version, trying auto-download') + return undefined + } + + return systemDart +} + export const DartServer: LSPServerInfo = { id: 'dart', extensions: ['.dart'], root: nearestRoot(['pubspec.yaml', 'pubspec.lock']), async spawn(root) { - // Try system dart first - const systemDart = Bun.which('dart') + const systemDart = await findSystemDart() if (systemDart) { try { const proc = spawn(systemDart, ['language-server', '--client-id', 'dora.dart', '--client-version', '1.0'], { diff --git a/packages/lsp/test/unit/server.test.ts b/packages/lsp/test/unit/server.test.ts index 1870213..9460b11 100644 --- a/packages/lsp/test/unit/server.test.ts +++ b/packages/lsp/test/unit/server.test.ts @@ -398,6 +398,38 @@ describe('DartServer', () => { await fs.rm(tempDir, { recursive: true, force: true }) } }) + + describe('findSystemDart', () => { + // Bun.which reads PATH at process startup, so probe in a child process with a fake PATH + async function findSystemDartWith(dartScript: string): Promise<{ result: string, fakeDart: string }> { + const binDir = await fs.mkdtemp(path.join(os.tmpdir(), 'dart-bin-')) + try { + const fakeDart = path.join(binDir, 'dart') + await fs.writeFile(fakeDart, dartScript, { mode: 0o755 }) + const dartModule = path.join(import.meta.dir, '../../src/server/dart.ts') + const proc = Bun.spawn( + [process.execPath, '-e', `import { findSystemDart } from ${JSON.stringify(dartModule)}; console.log(String(await findSystemDart()))`], + { env: { ...process.env, PATH: binDir }, stdout: 'pipe', stderr: 'ignore' }, + ) + const stdout = await new Response(proc.stdout).text() + await proc.exited + return { result: stdout.trim().split('\n').at(-1) ?? '', fakeDart } + } + finally { + await fs.rm(binDir, { recursive: true, force: true }) + } + } + + test('ignores a dart on PATH that fails --version', async () => { + const { result } = await findSystemDartWith('#!/bin/sh\necho "No version is set for shim: dart" >&2\nexit 1\n') + expect(result).toBe('undefined') + }) + + test('returns a dart on PATH that runs --version', async () => { + const { result, fakeDart } = await findSystemDartWith('#!/bin/sh\nexit 0\n') + expect(result).toBe(fakeDart) + }) + }) }) describe('VueServer', () => { From a32cfb1387bdaba13735fb08109967fa0415151c Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Thu, 22 Oct 2026 15:50:28 +0900 Subject: [PATCH 2/3] chore(lsp): apply AI code review suggestions Harden the system dart probe: catch spawn errors, run it in the project root, and bound it with a 10s timeout. Skip the shell-based tests on Windows. Refs #127 --- packages/lsp/src/server/dart.ts | 30 ++++++++++++++++++--------- packages/lsp/test/unit/server.test.ts | 21 ++++++++++++++++--- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/packages/lsp/src/server/dart.ts b/packages/lsp/src/server/dart.ts index 55d6990..0917765 100644 --- a/packages/lsp/src/server/dart.ts +++ b/packages/lsp/src/server/dart.ts @@ -111,24 +111,34 @@ async function setupDartDependencies(platformId: PlatformId): Promise { +export async function findSystemDart(root: string): Promise { const systemDart = Bun.which('dart') if (!systemDart) { return undefined } - const proc = Bun.spawn([systemDart, '--version'], { - stdout: 'ignore', - stderr: 'pipe', - }) - const exitCode = await proc.exited - if (exitCode !== 0) { - const stderr = await new Response(proc.stderr).text() - log.warn({ systemDart, exitCode, stderr: stderr.slice(0, 200) }, 'System Dart failed --version, trying auto-download') + try { + // Run in the project root: version managers resolve the SDK per directory + const proc = Bun.spawn([systemDart, '--version'], { + cwd: root, + stdout: 'ignore', + stderr: 'ignore', + timeout: DART_PROBE_TIMEOUT_MS, + }) + const exitCode = await proc.exited + if (exitCode !== 0) { + log.warn({ systemDart, exitCode, signal: proc.signalCode }, 'System Dart failed --version, trying auto-download') + return undefined + } + } + catch (err) { + log.warn({ systemDart, err }, 'Failed to spawn system Dart for version check, trying auto-download') return undefined } @@ -140,7 +150,7 @@ export const DartServer: LSPServerInfo = { extensions: ['.dart'], root: nearestRoot(['pubspec.yaml', 'pubspec.lock']), async spawn(root) { - const systemDart = await findSystemDart() + const systemDart = await findSystemDart(root) if (systemDart) { try { const proc = spawn(systemDart, ['language-server', '--client-id', 'dora.dart', '--client-version', '1.0'], { diff --git a/packages/lsp/test/unit/server.test.ts b/packages/lsp/test/unit/server.test.ts index 9460b11..dfffe30 100644 --- a/packages/lsp/test/unit/server.test.ts +++ b/packages/lsp/test/unit/server.test.ts @@ -399,16 +399,20 @@ describe('DartServer', () => { } }) - describe('findSystemDart', () => { + describe.skipIf(process.platform === 'win32')('findSystemDart', () => { // Bun.which reads PATH at process startup, so probe in a child process with a fake PATH - async function findSystemDartWith(dartScript: string): Promise<{ result: string, fakeDart: string }> { + async function findSystemDartWith(dartScript: string, rootFiles: string[] = []): Promise<{ result: string, fakeDart: string }> { const binDir = await fs.mkdtemp(path.join(os.tmpdir(), 'dart-bin-')) + const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'dart-root-')) try { const fakeDart = path.join(binDir, 'dart') await fs.writeFile(fakeDart, dartScript, { mode: 0o755 }) + for (const file of rootFiles) { + await fs.writeFile(path.join(rootDir, file), '') + } const dartModule = path.join(import.meta.dir, '../../src/server/dart.ts') const proc = Bun.spawn( - [process.execPath, '-e', `import { findSystemDart } from ${JSON.stringify(dartModule)}; console.log(String(await findSystemDart()))`], + [process.execPath, '-e', `import { findSystemDart } from ${JSON.stringify(dartModule)}; console.log(String(await findSystemDart(${JSON.stringify(rootDir)})))`], { env: { ...process.env, PATH: binDir }, stdout: 'pipe', stderr: 'ignore' }, ) const stdout = await new Response(proc.stdout).text() @@ -417,6 +421,7 @@ describe('DartServer', () => { } finally { await fs.rm(binDir, { recursive: true, force: true }) + await fs.rm(rootDir, { recursive: true, force: true }) } } @@ -425,10 +430,20 @@ describe('DartServer', () => { expect(result).toBe('undefined') }) + test('ignores a dart on PATH that cannot be spawned', async () => { + const { result } = await findSystemDartWith('\x7FELF not a real binary') + expect(result).toBe('undefined') + }) + test('returns a dart on PATH that runs --version', async () => { const { result, fakeDart } = await findSystemDartWith('#!/bin/sh\nexit 0\n') expect(result).toBe(fakeDart) }) + + test('probes --version in the project root', async () => { + const { result, fakeDart } = await findSystemDartWith('#!/bin/sh\n[ -f .tool-versions ] || exit 1\n', ['.tool-versions']) + expect(result).toBe(fakeDart) + }) }) }) From a13a4f61982a539b51abda55b39f264d23133bc3 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Thu, 22 Oct 2026 15:58:17 +0900 Subject: [PATCH 3/3] refactor(lsp): look up system dart on the current PATH Bun.which defaults to the PATH captured at process startup. Passing process.env.PATH lets findSystemDart follow the current environment, so its unit tests can run in-process and count toward coverage instead of probing through a child bun process. Refs #127 --- packages/lsp/src/server/dart.ts | 3 ++- packages/lsp/test/unit/server.test.ts | 20 ++++++++------------ 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/lsp/src/server/dart.ts b/packages/lsp/src/server/dart.ts index 0917765..051aea1 100644 --- a/packages/lsp/src/server/dart.ts +++ b/packages/lsp/src/server/dart.ts @@ -118,7 +118,8 @@ const DART_PROBE_TIMEOUT_MS = 10_000 * on PATH while exiting non-zero because no SDK version is configured. */ export async function findSystemDart(root: string): Promise { - const systemDart = Bun.which('dart') + // Bun.which defaults to the PATH captured at startup; follow the current environment + const systemDart = Bun.which('dart', { PATH: process.env.PATH ?? '' }) if (!systemDart) { return undefined } diff --git a/packages/lsp/test/unit/server.test.ts b/packages/lsp/test/unit/server.test.ts index dfffe30..1749aaa 100644 --- a/packages/lsp/test/unit/server.test.ts +++ b/packages/lsp/test/unit/server.test.ts @@ -18,6 +18,7 @@ import { TypescriptServer, VueServer, } from '../../src/server' +import { findSystemDart } from '../../src/server/dart' describe('LSP_SERVERS', () => { test('contains expected servers', () => { @@ -400,26 +401,21 @@ describe('DartServer', () => { }) describe.skipIf(process.platform === 'win32')('findSystemDart', () => { - // Bun.which reads PATH at process startup, so probe in a child process with a fake PATH - async function findSystemDartWith(dartScript: string, rootFiles: string[] = []): Promise<{ result: string, fakeDart: string }> { + async function findSystemDartWith(dartScript: string, rootFiles: string[] = []): Promise<{ result: string | undefined, fakeDart: string }> { const binDir = await fs.mkdtemp(path.join(os.tmpdir(), 'dart-bin-')) const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'dart-root-')) + const savedPath = process.env.PATH try { const fakeDart = path.join(binDir, 'dart') await fs.writeFile(fakeDart, dartScript, { mode: 0o755 }) for (const file of rootFiles) { await fs.writeFile(path.join(rootDir, file), '') } - const dartModule = path.join(import.meta.dir, '../../src/server/dart.ts') - const proc = Bun.spawn( - [process.execPath, '-e', `import { findSystemDart } from ${JSON.stringify(dartModule)}; console.log(String(await findSystemDart(${JSON.stringify(rootDir)})))`], - { env: { ...process.env, PATH: binDir }, stdout: 'pipe', stderr: 'ignore' }, - ) - const stdout = await new Response(proc.stdout).text() - await proc.exited - return { result: stdout.trim().split('\n').at(-1) ?? '', fakeDart } + process.env.PATH = binDir + return { result: await findSystemDart(rootDir), fakeDart } } finally { + process.env.PATH = savedPath await fs.rm(binDir, { recursive: true, force: true }) await fs.rm(rootDir, { recursive: true, force: true }) } @@ -427,12 +423,12 @@ describe('DartServer', () => { test('ignores a dart on PATH that fails --version', async () => { const { result } = await findSystemDartWith('#!/bin/sh\necho "No version is set for shim: dart" >&2\nexit 1\n') - expect(result).toBe('undefined') + expect(result).toBeUndefined() }) test('ignores a dart on PATH that cannot be spawned', async () => { const { result } = await findSystemDartWith('\x7FELF not a real binary') - expect(result).toBe('undefined') + expect(result).toBeUndefined() }) test('returns a dart on PATH that runs --version', async () => {