From 361a2064adb26cb9292c97263f7758c8ca7f8356 Mon Sep 17 00:00:00 2001 From: willxue Date: Wed, 25 Mar 2026 13:07:11 +0800 Subject: [PATCH] fix: stabilize desktop startup bundling --- .prettierignore | 1 + CHANGELOG.md | 3 ++ apps/desktop/electron.vite.config.ts | 19 ++++++- .../src/main/app/bootstrapDesktopApp.ts | 18 +++---- .../src/main/app/rendererRuntime.test.ts | 19 +++++++ apps/desktop/src/main/app/rendererRuntime.ts | 7 +++ packages/config-eslint/base.mjs | 1 + tooling/scripts/desktop-smoke.mjs | 52 ++++++++++++++++--- 8 files changed, 103 insertions(+), 17 deletions(-) create mode 100644 apps/desktop/src/main/app/rendererRuntime.test.ts create mode 100644 apps/desktop/src/main/app/rendererRuntime.ts diff --git a/.prettierignore b/.prettierignore index c31b27b..a7cac10 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,3 +5,4 @@ dist out playwright-report test-results +PLAN[0-9]*.md diff --git a/CHANGELOG.md b/CHANGELOG.md index dfb0f6c..8742045 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ - 修复终态任务仍可再次触发 `Approve and run` 的问题,主进程会将其视为 no-op,渲染层也会在任务进入终态后禁用执行按钮。 - 修复 `Claude Code CLI` 适配器在同一 session 重复执行时复用事件 ID 的问题,避免时间线渲染出现重复 key。 - 为桌面主窗口补充受信导航策略,仅允许本地 `file://` 或开发服务器地址留在应用内,其它链接统一改走系统浏览器。 +- 修复桌面端构建把 `@mango/*` workspace 包 external 到运行时的问题,避免 clean checkout 下 Electron 启动时因缺少 `dist` 产物而报 `ERR_MODULE_NOT_FOUND`。 +- 强化桌面端 smoke 检查,校验主进程与 preload 产物中不再残留 `@mango/*` 裸导入,并兼容根级 `out/renderer` 输出路径。 +- 修复桌面端开发启动仍依赖旧版 `MAIN_WINDOW_VITE_*` 全局变量的问题,改为读取 `electron-vite` 当前注入的 `ELECTRON_RENDERER_URL` 并固定加载 `out/renderer/index.html`。 ## [0.1.0] - 2026-03-25 diff --git a/apps/desktop/electron.vite.config.ts b/apps/desktop/electron.vite.config.ts index 9351a8e..da4b8f4 100644 --- a/apps/desktop/electron.vite.config.ts +++ b/apps/desktop/electron.vite.config.ts @@ -3,6 +3,13 @@ import { resolve } from 'node:path' import react from '@vitejs/plugin-react' import { defineConfig, externalizeDepsPlugin } from 'electron-vite' +const internalWorkspacePackages = [ + '@mango/core', + '@mango/adapters', + '@mango/contracts', + '@mango/ui' +] + export default defineConfig({ main: { resolve: { @@ -13,7 +20,11 @@ export default defineConfig({ '@mango/ui': resolve(__dirname, '../../packages/ui/src/index.ts') } }, - plugins: [externalizeDepsPlugin()] + plugins: [ + externalizeDepsPlugin({ + exclude: internalWorkspacePackages + }) + ] }, preload: { resolve: { @@ -24,7 +35,11 @@ export default defineConfig({ '@mango/ui': resolve(__dirname, '../../packages/ui/src/index.ts') } }, - plugins: [externalizeDepsPlugin()] + plugins: [ + externalizeDepsPlugin({ + exclude: internalWorkspacePackages + }) + ] }, renderer: { resolve: { diff --git a/apps/desktop/src/main/app/bootstrapDesktopApp.ts b/apps/desktop/src/main/app/bootstrapDesktopApp.ts index a69e5db..7e48228 100644 --- a/apps/desktop/src/main/app/bootstrapDesktopApp.ts +++ b/apps/desktop/src/main/app/bootstrapDesktopApp.ts @@ -9,13 +9,13 @@ import { FileDesktopStore } from '../persistence/fileDesktopStore' import { SQLiteDesktopStore } from '../persistence/sqliteDesktopStore' import { DesktopController } from '../services/desktopController' import { isTrustedDesktopUrl } from './navigationPolicy' - -declare const MAIN_WINDOW_VITE_DEV_SERVER_URL: string | undefined -declare const MAIN_WINDOW_VITE_NAME: string +import { getRendererDevServerUrl, getRendererIndexHtmlPath } from './rendererRuntime' let mainWindow: BrowserWindow | null = null const createMainWindow = () => { + const rendererDevServerUrl = getRendererDevServerUrl(process.env) + mainWindow = new BrowserWindow({ width: 1540, height: 980, @@ -31,7 +31,7 @@ const createMainWindow = () => { }) const handleExternalNavigation = (url: string) => { - if (isTrustedDesktopUrl(url, MAIN_WINDOW_VITE_DEV_SERVER_URL)) { + if (isTrustedDesktopUrl(url, rendererDevServerUrl)) { return } @@ -42,12 +42,12 @@ const createMainWindow = () => { handleExternalNavigation(url) return { - action: isTrustedDesktopUrl(url, MAIN_WINDOW_VITE_DEV_SERVER_URL) ? 'allow' : 'deny' + action: isTrustedDesktopUrl(url, rendererDevServerUrl) ? 'allow' : 'deny' } }) mainWindow.webContents.on('will-navigate', (event, url) => { - if (isTrustedDesktopUrl(url, MAIN_WINDOW_VITE_DEV_SERVER_URL)) { + if (isTrustedDesktopUrl(url, rendererDevServerUrl)) { return } @@ -55,11 +55,11 @@ const createMainWindow = () => { handleExternalNavigation(url) }) - if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { - void mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL) + if (rendererDevServerUrl) { + void mainWindow.loadURL(rendererDevServerUrl) mainWindow.webContents.openDevTools({ mode: 'detach' }) } else { - void mainWindow.loadFile(join(__dirname, `../../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`)) + void mainWindow.loadFile(getRendererIndexHtmlPath(__dirname)) } } diff --git a/apps/desktop/src/main/app/rendererRuntime.test.ts b/apps/desktop/src/main/app/rendererRuntime.test.ts new file mode 100644 index 0000000..8ec38b2 --- /dev/null +++ b/apps/desktop/src/main/app/rendererRuntime.test.ts @@ -0,0 +1,19 @@ +import { join } from 'node:path' + +import { getRendererDevServerUrl, getRendererIndexHtmlPath } from './rendererRuntime' + +describe('rendererRuntime', () => { + it('reads the renderer dev server url from electron-vite env', () => { + expect( + getRendererDevServerUrl({ + ELECTRON_RENDERER_URL: 'http://localhost:5173' + } as NodeJS.ProcessEnv) + ).toBe('http://localhost:5173') + }) + + it('resolves the packaged renderer entry next to the main bundle', () => { + expect(getRendererIndexHtmlPath('D:/workspace/apps/desktop/out/main')).toBe( + join('D:/workspace/apps/desktop/out/main', '../renderer/index.html') + ) + }) +}) diff --git a/apps/desktop/src/main/app/rendererRuntime.ts b/apps/desktop/src/main/app/rendererRuntime.ts new file mode 100644 index 0000000..47be973 --- /dev/null +++ b/apps/desktop/src/main/app/rendererRuntime.ts @@ -0,0 +1,7 @@ +import { join } from 'node:path' + +export const getRendererDevServerUrl = (env: NodeJS.ProcessEnv): string | undefined => + env.ELECTRON_RENDERER_URL + +export const getRendererIndexHtmlPath = (currentDir: string): string => + join(currentDir, '../renderer/index.html') diff --git a/packages/config-eslint/base.mjs b/packages/config-eslint/base.mjs index db90116..336eb1b 100644 --- a/packages/config-eslint/base.mjs +++ b/packages/config-eslint/base.mjs @@ -15,6 +15,7 @@ export const baseConfig = tseslint.config( '**/out/**', '**/coverage/**', '**/.turbo/**', + '**/.worktrees/**', '**/*.d.ts', '**/*.d.mts', '**/playwright-report/**', diff --git a/tooling/scripts/desktop-smoke.mjs b/tooling/scripts/desktop-smoke.mjs index 1db4028..ae1f478 100644 --- a/tooling/scripts/desktop-smoke.mjs +++ b/tooling/scripts/desktop-smoke.mjs @@ -1,16 +1,56 @@ -import { access } from 'node:fs/promises' +import { access, readFile } from 'node:fs/promises' import { resolve } from 'node:path' const repoRoot = resolve(import.meta.dirname, '..', '..') -const requiredOutputs = [ - 'apps/desktop/out/main/index.js', - 'apps/desktop/out/preload/index.mjs', - 'apps/desktop/out/renderer/index.html' -] +const requiredOutputs = ['apps/desktop/out/main/index.js', 'apps/desktop/out/preload/index.mjs'] for (const relativePath of requiredOutputs) { await access(resolve(repoRoot, relativePath)) } +const rendererCandidates = ['out/renderer/index.html', 'apps/desktop/out/renderer/index.html'] + +let resolvedRendererOutput = null + +for (const relativePath of rendererCandidates) { + try { + await access(resolve(repoRoot, relativePath)) + resolvedRendererOutput = relativePath + break + } catch { + // Keep searching known renderer output locations. + } +} + +if (!resolvedRendererOutput) { + throw new Error( + `Desktop renderer output is missing. Checked: ${rendererCandidates + .map((relativePath) => resolve(repoRoot, relativePath)) + .join(', ')}` + ) +} + +const workspaceImportPattern = /['"]@mango\// +const legacyElectronViteGlobals = [/MAIN_WINDOW_VITE_DEV_SERVER_URL/, /MAIN_WINDOW_VITE_NAME/] + +for (const relativePath of [...requiredOutputs, resolvedRendererOutput]) { + const absolutePath = resolve(repoRoot, relativePath) + const contents = await readFile(absolutePath, 'utf8') + + if (workspaceImportPattern.test(contents)) { + throw new Error( + `Desktop bundle still contains workspace package imports at runtime: ${absolutePath}` + ) + } + + for (const pattern of legacyElectronViteGlobals) { + if (pattern.test(contents)) { + throw new Error( + `Desktop bundle still contains deprecated electron-vite globals at runtime: ${absolutePath}` + ) + } + } +} + console.log('Desktop smoke check passed.')