Skip initial dev web client build (#2273)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo Hong
2026-05-18 14:30:36 -07:00
committed by GitHub
co-authored by Orca
parent e2322c7d68
commit 8a4d558c1d
3 changed files with 186 additions and 1 deletions
+13 -1
View File
@@ -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
}
@@ -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')
}
@@ -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<number>()
function sleep(ms: number): Promise<void> {
return new Promise((resolvePromise) => {
setTimeout(resolvePromise, ms)
})
}
async function waitFor(predicate: () => boolean, timeoutMs = 5000): Promise<void> {
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()
}
})
})