From 8a4d558c1da489fd3e6f30d0e6ca573ee99bb918 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Mon, 18 May 2026 17:30:36 -0400 Subject: [PATCH] Skip initial dev web client build (#2273) Co-authored-by: Orca --- config/scripts/run-electron-vite-dev.mjs | 14 +- .../startup/__fixtures__/fake-vite-cli.mjs | 9 + .../startup/run-electron-vite-dev-web.test.ts | 164 ++++++++++++++++++ 3 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 src/main/startup/__fixtures__/fake-vite-cli.mjs create mode 100644 src/main/startup/run-electron-vite-dev-web.test.ts diff --git a/config/scripts/run-electron-vite-dev.mjs b/config/scripts/run-electron-vite-dev.mjs index 8117cc2e409..109b55da064 100644 --- a/config/scripts/run-electron-vite-dev.mjs +++ b/config/scripts/run-electron-vite-dev.mjs @@ -260,6 +260,10 @@ function getMtimeMs(filePath) { } } +function getDevWebClientIndexPath() { + return path.join(repoRoot, 'out', 'web', 'web-index.html') +} + function latestMtimeMs(targetPath) { const stat = (() => { try { @@ -285,7 +289,7 @@ function latestMtimeMs(targetPath) { } function isDevWebClientFresh() { - const outputMtime = getMtimeMs(path.join(repoRoot, 'out', 'web', 'web-index.html')) + const outputMtime = getMtimeMs(getDevWebClientIndexPath()) if (outputMtime === 0) { return false } @@ -302,6 +306,14 @@ function prepareDevWebClient() { if (process.env.ORCA_SKIP_DEV_WEB_PREPARE === '1' || isHelpOrVersion) { return } + // Why: fresh worktrees should start Electron immediately; pairing already + // falls back to non-browser URLs when the optional web bundle is unavailable. + if (!existsSync(getDevWebClientIndexPath()) && process.env.ORCA_DEV_WEB_PREPARE !== '1') { + console.error( + '[orca-dev] Web client bundle missing; skipping pairing web build. Run `pnpm run build:web` or set ORCA_DEV_WEB_PREPARE=1 when you need browser pairing.' + ) + return + } if (isDevWebClientFresh()) { return } diff --git a/src/main/startup/__fixtures__/fake-vite-cli.mjs b/src/main/startup/__fixtures__/fake-vite-cli.mjs new file mode 100644 index 00000000000..42d426df640 --- /dev/null +++ b/src/main/startup/__fixtures__/fake-vite-cli.mjs @@ -0,0 +1,9 @@ +import { mkdirSync, writeFileSync } from 'node:fs' +import path from 'node:path' + +const markerFile = process.env.ORCA_DEV_WRAPPER_TEST_VITE_FILE + +if (markerFile) { + mkdirSync(path.dirname(markerFile), { recursive: true }) + writeFileSync(markerFile, `${process.argv.slice(2).join('\n')}\n`, 'utf8') +} diff --git a/src/main/startup/run-electron-vite-dev-web.test.ts b/src/main/startup/run-electron-vite-dev-web.test.ts new file mode 100644 index 00000000000..c87575e23cd --- /dev/null +++ b/src/main/startup/run-electron-vite-dev-web.test.ts @@ -0,0 +1,164 @@ +import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' +import { spawn } from 'node:child_process' +import { afterEach, describe, expect, it } from 'vitest' + +const processesToCleanUp = new Set() + +function sleep(ms: number): Promise { + return new Promise((resolvePromise) => { + setTimeout(resolvePromise, ms) + }) +} + +async function waitFor(predicate: () => boolean, timeoutMs = 5000): Promise { + const start = Date.now() + while (!predicate()) { + if (Date.now() - start > timeoutMs) { + throw new Error('Timed out waiting for condition') + } + await sleep(50) + } +} + +function stashWebBuild(): () => void { + const outWebPath = resolve('out/web') + if (!existsSync(outWebPath)) { + return () => { + rmSync(outWebPath, { recursive: true, force: true }) + } + } + + const tempDir = mkdtempSync(join(tmpdir(), 'orca-dev-web-stash-')) + const stashedPath = join(tempDir, 'web') + renameSync(outWebPath, stashedPath) + return () => { + rmSync(outWebPath, { recursive: true, force: true }) + mkdirSync(resolve('out'), { recursive: true }) + renameSync(stashedPath, outWebPath) + rmSync(tempDir, { recursive: true, force: true }) + } +} + +describe('run-electron-vite-dev web client prepare', () => { + afterEach(() => { + for (const pid of processesToCleanUp) { + try { + process.kill(pid, 'SIGKILL') + } catch (error) { + const code = error && typeof error === 'object' && 'code' in error ? error.code : null + if (code !== 'ESRCH') { + throw error + } + } + } + processesToCleanUp.clear() + }) + + it('skips the initial web client build when no bundle exists', async () => { + const restoreWebBuild = stashWebBuild() + const tempDir = mkdtempSync(join(tmpdir(), 'orca-dev-wrapper-')) + const pidFile = join(tempDir, 'grandchild.pid') + const envFile = join(tempDir, 'env.json') + const viteFile = join(tempDir, 'vite.txt') + const wrapperPath = resolve('config/scripts/run-electron-vite-dev.mjs') + const fakeCliPath = resolve('src/main/startup/__fixtures__/fake-electron-vite-dev-cli.mjs') + const fakeVitePath = resolve('src/main/startup/__fixtures__/fake-vite-cli.mjs') + let stderr = '' + + try { + const wrapper = spawn(process.execPath, [wrapperPath, '--remote-debugging-port=9446'], { + cwd: resolve('.'), + env: { + ...process.env, + ORCA_ELECTRON_VITE_CLI: fakeCliPath, + ORCA_VITE_CLI: fakeVitePath, + ORCA_SKIP_DEV_CLI_PREPARE: '1', + ORCA_SKIP_DEV_ELECTRON_APP_PREPARE: '1', + ORCA_DEV_WRAPPER_TEST_PID_FILE: pidFile, + ORCA_DEV_WRAPPER_TEST_ENV_FILE: envFile, + ORCA_DEV_WRAPPER_TEST_VITE_FILE: viteFile + }, + stdio: ['ignore', 'ignore', 'pipe'] + }) + + expect(wrapper.pid).toBeTypeOf('number') + processesToCleanUp.add(wrapper.pid!) + wrapper.stderr.on('data', (chunk) => { + stderr += String(chunk) + }) + + await waitFor(() => { + try { + return readFileSync(envFile, 'utf8').trim().length > 0 + } catch { + return false + } + }) + + expect(existsSync(viteFile)).toBe(false) + expect(stderr).toContain('Web client bundle missing; skipping pairing web build.') + + const grandchildPid = Number.parseInt(readFileSync(pidFile, 'utf8').trim(), 10) + if (Number.isFinite(grandchildPid)) { + processesToCleanUp.add(grandchildPid) + } + + wrapper.kill('SIGINT') + } finally { + restoreWebBuild() + } + }) + + it('builds the missing web client bundle when explicitly requested', async () => { + const restoreWebBuild = stashWebBuild() + const tempDir = mkdtempSync(join(tmpdir(), 'orca-dev-wrapper-')) + const pidFile = join(tempDir, 'grandchild.pid') + const envFile = join(tempDir, 'env.json') + const viteFile = join(tempDir, 'vite.txt') + const wrapperPath = resolve('config/scripts/run-electron-vite-dev.mjs') + const fakeCliPath = resolve('src/main/startup/__fixtures__/fake-electron-vite-dev-cli.mjs') + const fakeVitePath = resolve('src/main/startup/__fixtures__/fake-vite-cli.mjs') + + try { + const wrapper = spawn(process.execPath, [wrapperPath, '--remote-debugging-port=9447'], { + cwd: resolve('.'), + env: { + ...process.env, + ORCA_ELECTRON_VITE_CLI: fakeCliPath, + ORCA_VITE_CLI: fakeVitePath, + ORCA_SKIP_DEV_CLI_PREPARE: '1', + ORCA_SKIP_DEV_ELECTRON_APP_PREPARE: '1', + ORCA_DEV_WEB_PREPARE: '1', + ORCA_DEV_WRAPPER_TEST_PID_FILE: pidFile, + ORCA_DEV_WRAPPER_TEST_ENV_FILE: envFile, + ORCA_DEV_WRAPPER_TEST_VITE_FILE: viteFile + }, + stdio: 'ignore' + }) + + expect(wrapper.pid).toBeTypeOf('number') + processesToCleanUp.add(wrapper.pid!) + + await waitFor(() => { + try { + return readFileSync(envFile, 'utf8').trim().length > 0 + } catch { + return false + } + }) + + expect(readFileSync(viteFile, 'utf8')).toContain('build') + + const grandchildPid = Number.parseInt(readFileSync(pidFile, 'utf8').trim(), 10) + if (Number.isFinite(grandchildPid)) { + processesToCleanUp.add(grandchildPid) + } + + wrapper.kill('SIGINT') + } finally { + restoreWebBuild() + } + }) +})