Simplify desktop development workflow after review

Refs #51

Signed-off-by: Gaurav Chaudhary <chaudharygaurav2004@gmail.com>
This commit is contained in:
Gaurav Chaudhary 2026-09-11 17:59:23 +05:30
parent f8565aecb0
commit 9b43ff72a9
6 changed files with 40 additions and 132 deletions

View file

@ -82,11 +82,10 @@ pnpm run start:desktop
`build:desktop` builds the frontend without PWA/service-worker registration, `build:desktop` builds the frontend without PWA/service-worker registration,
synchronizes the Electron platform, and compiles its TypeScript entrypoint. It uses synchronizes the Electron platform, and compiles its TypeScript entrypoint. It uses
the same branding environment as the web build and does not synchronize Android the same branding environment as the web build and does not synchronize Android
or iOS. The existing build scripts require Bash, Perl, and their usual asset tools. or iOS. `start:desktop` opens the last build without Vite; rerun `build:desktop` after
`start:desktop` opens the last build without Vite; rerun `build:desktop` after
frontend changes. The development URL is supplied only to the desktop run process. frontend changes. The development URL is supplied only to the desktop run process.
Capawesome records it in ignored generated configuration during development; Capawesome records it in ignored generated configuration during development;
production synchronization removes it, and local startup ignores inherited dev URLs. production synchronization removes it.
Run `pnpm run test:desktop` after building to check the Linux desktop window. On a Run `pnpm run test:desktop` after building to check the Linux desktop window. On a
headless Linux runner, use `xvfb-run -a pnpm run test:desktop`; Electron links headless Linux runner, use `xvfb-run -a pnpm run test:desktop`; Electron links
@ -96,15 +95,6 @@ root, because Chromium refuses to start that way. The separate smoke
suite does not start a web dev server or test installers. Windows and macOS desktop suite does not start a web dev server or test installers. Windows and macOS desktop
behavior is not verified by the Linux test. CI does not run it. behavior is not verified by the Linux test. CI does not run it.
When changing the desktop workflow, also verify `dev:desktop` in a disposable
checkout: change existing Svelte source and confirm HMR preserves a marker set on
`window` in DevTools, then restore the file. Check navigation, reload, blob workers,
and recovery after reloading while Vite restarts. Verify that Ctrl+C and quitting
Electron release the server port and leave no child processes, including on startup
failure or a TypeScript error. Finally stop development, rebuild, and confirm
`start:desktop` loads local assets with no Vite connection. These runtime checks
are separate from CI's lint and Electron TypeScript checks.
## Deployment ## Deployment
To run your own Flotilla, it's as simple as: To run your own Flotilla, it's as simple as:

View file

@ -26,18 +26,8 @@ const config: CapacitorConfig = {
}, },
}, },
server: { server: {
// Use this for live reload https://capacitorjs.com/docs/guides/live-reload url: process.env.FLOTILLA_DESKTOP_DEV_URL,
// url: "http://192.168.1.17:1847",
// cleartext: true,
}, },
} }
if (
process.argv.includes("run") &&
process.argv.includes("@capawesome/capacitor-electron") &&
process.env.FLOTILLA_DESKTOP_DEV_URL
) {
config.server = {...config.server, url: process.env.FLOTILLA_DESKTOP_DEV_URL}
}
export default config export default config

View file

@ -11,19 +11,13 @@ test("the desktop baseline renders, navigates, and keeps external pages outside"
const executablePath: string = createRequire(import.meta.url)( const executablePath: string = createRequire(import.meta.url)(
resolve("electron/node_modules/electron"), resolve("electron/node_modules/electron"),
) )
const options = { const app = await _electron.launch({
executablePath, executablePath,
// Chromium refuses to start as root with its sandbox on, which is what a CI container is. // Chromium refuses to start as root with its sandbox on, which is what a CI container is.
chromiumSandbox: process.getuid?.() !== 0, chromiumSandbox: process.getuid?.() !== 0,
args: [resolve("electron"), "--desktop-local"], args: [resolve("electron")],
env: { env: {...process.env, XDG_CONFIG_HOME: profile},
...process.env, })
XDG_CONFIG_HOME: profile,
FLOTILLA_DESKTOP_DEV_URL: "http://127.0.0.1:1/",
CAPACITOR_ELECTRON_DEV_SERVER_URL: "http://127.0.0.1:1/",
},
}
const app = await _electron.launch(options)
try { try {
const mainWindows = () => const mainWindows = () =>
@ -31,21 +25,8 @@ test("the desktop baseline renders, navigates, and keeps external pages outside"
await expect.poll(() => mainWindows().length).toBe(1) await expect.poll(() => mainWindows().length).toBe(1)
const [page] = mainWindows() const [page] = mainWindows()
const errors: string[] = [] const errors: string[] = []
const developmentRequests: string[] = []
page.on("pageerror", error => errors.push(error.message)) page.on("pageerror", error => errors.push(error.message))
page.on("request", request => {
if (/127\.0\.0\.1:1|\/@vite\/|\/@id\/|\/@fs\//.test(request.url())) {
developmentRequests.push(request.url())
}
})
expect(
await app.evaluate(({BrowserWindow}) => {
const window = BrowserWindow.getAllWindows()[0]
return {size: window.getSize(), resizable: window.isResizable()}
}),
).toEqual({size: [1200, 800], resizable: true})
await page.reload() await page.reload()
await expect(page.getByRole("heading")).toBeVisible() await expect(page.getByRole("heading")).toBeVisible()
@ -112,22 +93,9 @@ test("the desktop baseline renders, navigates, and keeps external pages outside"
}) })
await expect(page.locator("html")).toHaveAttribute("data-csp-violation", "script-src-elem") await expect(page.locator("html")).toHaveAttribute("data-csp-violation", "script-src-elem")
await expect(page.locator("html")).not.toHaveAttribute("data-inline-script-executed") await expect(page.locator("html")).not.toHaveAttribute("data-inline-script-executed")
expect(developmentRequests).toEqual([])
await app.evaluate(({BrowserWindow}) => BrowserWindow.getAllWindows()[0].setSize(1000, 700))
} finally { } finally {
await app.close() await app.close()
} }
const reopened = await _electron.launch(options)
try {
await reopened.firstWindow()
expect(
await reopened.evaluate(({BrowserWindow}) => BrowserWindow.getAllWindows()[0].getSize()),
).toEqual([1000, 700])
} finally {
await reopened.close()
}
} finally { } finally {
await rm(profile, {recursive: true, force: true}) await rm(profile, {recursive: true, force: true})
} }

View file

@ -1,23 +1,5 @@
import {app} from "electron"
import {createCapacitorElectronApp} from "@capawesome/capacitor-electron" import {createCapacitorElectronApp} from "@capawesome/capacitor-electron"
const devServerUrl = process.env.FLOTILLA_DESKTOP_DEV_URL
// Capawesome 0.1.1 has no public option to force local assets over an inherited dev URL.
if (app.isPackaged || process.argv.includes("--desktop-local") || !devServerUrl) {
delete process.env.CAPACITOR_ELECTRON_DEV_SERVER_URL
} else {
const url = new URL(devServerUrl)
if (
url.protocol !== "http:" ||
url.hostname !== "127.0.0.1" ||
devServerUrl !== `${url.origin}/` ||
process.env.CAPACITOR_ELECTRON_DEV_SERVER_URL !== devServerUrl
) {
throw new Error("Desktop development requires the matching loopback URL from cap run.")
}
}
createCapacitorElectronApp({ createCapacitorElectronApp({
window: {width: 1200, height: 800}, window: {width: 1200, height: 800},
csp: { csp: {

View file

@ -7,7 +7,7 @@
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"postinstall": "install-electron", "postinstall": "install-electron",
"start": "electron . --desktop-local" "start": "electron ."
}, },
"devDependencies": { "devDependencies": {
"electron": "^43.0.0", "electron": "^43.0.0",

View file

@ -1,5 +1,4 @@
import {spawn, spawnSync} from "node:child_process" import {spawn, spawnSync} from "node:child_process"
import {access, readFile} from "node:fs/promises"
import {createRequire} from "node:module" import {createRequire} from "node:module"
import {dirname, resolve} from "node:path" import {dirname, resolve} from "node:path"
import {fileURLToPath} from "node:url" import {fileURLToPath} from "node:url"
@ -9,7 +8,6 @@ const require = createRequire(import.meta.url)
const windows = process.platform === "win32" const windows = process.platform === "win32"
let server let server
let child let child
let interrupted = false
let forceShutdown let forceShutdown
const terminate = (signal = "SIGTERM") => { const terminate = (signal = "SIGTERM") => {
@ -37,47 +35,27 @@ const terminate = (signal = "SIGTERM") => {
} }
const onSignal = signal => { const onSignal = signal => {
interrupted = true
process.exitCode = signal === "SIGINT" ? 130 : 143 process.exitCode = signal === "SIGINT" ? 130 : 143
terminate() terminate()
forceShutdown ??= setTimeout(() => terminate("SIGKILL"), 5000) forceShutdown ??= setTimeout(() => terminate("SIGKILL"), 5000)
forceShutdown.unref() forceShutdown.unref()
} }
try { // Vite also exits on SIGTERM; clean up descendants even if its handler exits first.
try { const onExit = () => terminate("SIGKILL")
// Requiring Electron 43 downloads missing binaries; inspect the installation without running it.
const electron = resolve(root, "electron/node_modules/electron")
const executable = (await readFile(resolve(electron, "path.txt"), "utf8")).trim()
await access(
resolve(process.env.ELECTRON_OVERRIDE_DIST_PATH || resolve(electron, "dist"), executable),
)
} catch (error) {
throw new Error("Install the desktop dependencies first: npm ci --prefix electron", {
cause: error,
})
}
try {
process.chdir(root) process.chdir(root)
process.env.NODE_ENV = "development" process.env.NODE_ENV = "development"
process.env.FLOTILLA_DESKTOP = "1" process.env.FLOTILLA_DESKTOP = "1"
const {createServer} = await import("vite") const {createServer} = await import("vite")
process.on("SIGINT", onSignal)
process.on("SIGTERM", onSignal)
server = await createServer({ server = await createServer({
mode: "development", mode: "development",
server: {host: "127.0.0.1", strictPort: true}, server: {host: "127.0.0.1", strictPort: true},
}) })
if (!interrupted) {
await server.listen() await server.listen()
const url = new URL(server.resolvedUrls.local[0])
if (url.protocol !== "http:" || url.hostname !== "127.0.0.1" || url.href !== `${url.origin}/`) {
throw new Error("Desktop development requires an HTTP server on 127.0.0.1.")
}
if (!interrupted) {
server.printUrls() server.printUrls()
child = spawn( child = spawn(
process.execPath, process.execPath,
@ -92,25 +70,25 @@ try {
cwd: root, cwd: root,
stdio: "inherit", stdio: "inherit",
detached: !windows, detached: !windows,
env: {...process.env, FLOTILLA_DESKTOP_DEV_URL: url.href}, env: {...process.env, FLOTILLA_DESKTOP_DEV_URL: server.resolvedUrls.local[0]},
}, },
) )
process.on("SIGINT", onSignal)
process.on("SIGTERM", onSignal)
process.on("exit", onExit)
const code = await new Promise((resolve, reject) => { const code = await new Promise((resolve, reject) => {
child.once("error", reject) child.once("error", reject)
child.once("close", (code, signal) => resolve(signal ? 1 : code)) child.once("close", (code, signal) => resolve(signal ? 1 : code))
}) })
if (!interrupted) process.exitCode = code ?? 1 process.exitCode ||= code ?? 1
}
}
} catch (error) { } catch (error) {
if (!interrupted) {
console.error(error) console.error(error)
process.exitCode = 1 process.exitCode = 1
}
} finally { } finally {
terminate("SIGKILL") terminate("SIGKILL")
await server?.close() await server?.close()
clearTimeout(forceShutdown) clearTimeout(forceShutdown)
process.removeListener("SIGINT", onSignal) process.removeListener("SIGINT", onSignal)
process.removeListener("SIGTERM", onSignal) process.removeListener("SIGTERM", onSignal)
process.removeListener("exit", onExit)
} }