From 0f22792861c1169175dfe76f3036f86be79c215f Mon Sep 17 00:00:00 2001 From: lichengshuo Date: Mon, 20 Jul 2026 16:46:40 +0800 Subject: [PATCH] fix(desktop): make electron:dev launcher work on Windows `new URL('..', import.meta.url).pathname` yields a leading-slash path like /E:/... on Windows, so Bun.spawn failed with ENOENT. Resolve desktopRoot via fileURLToPath, start Vite through its Node API instead of spawning `bun run dev`, and resolve the Electron binary directly per platform. Recovered from the discarded wip/capabilities snapshot (cb1208c9) as a standalone fix. Co-Authored-By: Claude Opus 4.8 --- desktop/scripts/dev-launcher.test.ts | 9 +++- desktop/scripts/electron-dev.ts | 68 ++++++++++++++++++++-------- 2 files changed, 56 insertions(+), 21 deletions(-) diff --git a/desktop/scripts/dev-launcher.test.ts b/desktop/scripts/dev-launcher.test.ts index 007e22df..e3119b60 100644 --- a/desktop/scripts/dev-launcher.test.ts +++ b/desktop/scripts/dev-launcher.test.ts @@ -1,5 +1,6 @@ +import path from 'node:path' import { describe, expect, it } from 'vitest' -import { createElectronDevEnv, DEFAULT_RENDERER_URL, mergeNoProxy } from './electron-dev' +import { createElectronDevEnv, DEFAULT_RENDERER_URL, mergeNoProxy, resolveElectronExecutable } from './electron-dev' describe('desktop dev launcher environment', () => { it('uses localhost and bypasses proxies for local renderer startup', () => { @@ -27,4 +28,8 @@ describe('desktop dev launcher environment', () => { it('deduplicates no_proxy entries', () => { expect(mergeNoProxy('localhost,127.0.0.1')).toBe('localhost,127.0.0.1,::1') }) -}) + + it('resolves Electron directly instead of shelling through bunx', () => { + expect(resolveElectronExecutable(path.resolve(import.meta.dirname, '..'), 'win32')).toMatch(/electron\.exe$/) + }) +}) \ No newline at end of file diff --git a/desktop/scripts/electron-dev.ts b/desktop/scripts/electron-dev.ts index 50e9fcf5..64eeab34 100644 --- a/desktop/scripts/electron-dev.ts +++ b/desktop/scripts/electron-dev.ts @@ -1,3 +1,9 @@ +import { spawn } from 'node:child_process' +import { existsSync } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { createServer, type ViteDevServer } from 'vite' + export const DEFAULT_RENDERER_URL = 'http://localhost:1420' export const LOCAL_NO_PROXY_ENTRIES = ['localhost', '127.0.0.1', '::1'] @@ -23,6 +29,20 @@ export function createElectronDevEnv(env: NodeJS.ProcessEnv = process.env) { } } +export function resolveElectronExecutable(desktopRoot: string, platform = process.platform) { + const candidates = platform === 'win32' + ? [path.join(desktopRoot, 'node_modules', 'electron', 'dist', 'electron.exe')] + : platform === 'darwin' + ? [path.join(desktopRoot, 'node_modules', 'electron', 'dist', 'Electron.app', 'Contents', 'MacOS', 'Electron')] + : [path.join(desktopRoot, 'node_modules', 'electron', 'dist', 'electron')] + + const electronPath = candidates.find(candidate => existsSync(candidate)) + if (!electronPath) { + throw new Error(`Electron executable not found under ${path.join(desktopRoot, 'node_modules', 'electron', 'dist')}. Run "cd desktop && bun install" first.`) + } + return electronPath +} + async function waitForRenderer(rendererUrl: string) { const deadline = Date.now() + 30_000 while (Date.now() < deadline) { @@ -36,47 +56,57 @@ async function waitForRenderer(rendererUrl: string) { throw new Error(`Timed out waiting for Vite renderer at ${rendererUrl}`) } +async function startVite(desktopRoot: string) { + const server = await createServer({ + root: desktopRoot, + configFile: path.join(desktopRoot, 'vite.config.ts'), + }) + await server.listen() + server.printUrls() + return server +} + +async function closeVite(server: ViteDevServer) { + await server.close().catch(() => undefined) +} + async function main() { - const desktopRoot = new URL('..', import.meta.url).pathname + const desktopRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') const childEnv = createElectronDevEnv() const rendererUrl = childEnv.ELECTRON_RENDERER_URL process.env.NO_PROXY = childEnv.NO_PROXY process.env.no_proxy = childEnv.no_proxy - const vite = Bun.spawn(['bun', 'run', 'dev'], { - cwd: desktopRoot, - env: childEnv, - stdout: 'inherit', - stderr: 'inherit', - }) + const vite = await startVite(desktopRoot) - function stopVite() { - vite.kill() + async function stopVite() { + await closeVite(vite) } process.on('SIGINT', () => { - stopVite() - process.exit(130) + void stopVite().finally(() => process.exit(130)) }) process.on('SIGTERM', () => { - stopVite() - process.exit(143) + void stopVite().finally(() => process.exit(143)) }) await waitForRenderer(rendererUrl) - const electron = Bun.spawn(['bunx', 'electron', './electron-dist/main.cjs'], { + const electron = spawn(resolveElectronExecutable(desktopRoot), ['./electron-dist/main.cjs'], { cwd: desktopRoot, env: childEnv, - stdout: 'inherit', - stderr: 'inherit', + stdio: 'inherit', + windowsHide: true, }) - const exitCode = await electron.exited - stopVite() + const exitCode = await new Promise((resolve, reject) => { + electron.once('error', reject) + electron.once('exit', code => resolve(code ?? 0)) + }) + await stopVite() process.exit(exitCode) } if (import.meta.main) { await main() -} +} \ No newline at end of file