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,
synchronizes the Electron platform, and compiles its TypeScript entrypoint. It uses
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.
`start:desktop` opens the last build without Vite; rerun `build:desktop` after
or iOS. `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.
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
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
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
To run your own Flotilla, it's as simple as:

View file

@ -26,18 +26,8 @@ const config: CapacitorConfig = {
},
},
server: {
// Use this for live reload https://capacitorjs.com/docs/guides/live-reload
// url: "http://192.168.1.17:1847",
// cleartext: true,
url: process.env.FLOTILLA_DESKTOP_DEV_URL,
},
}
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

View file

@ -11,19 +11,13 @@ test("the desktop baseline renders, navigates, and keeps external pages outside"
const executablePath: string = createRequire(import.meta.url)(
resolve("electron/node_modules/electron"),
)
const options = {
const app = await _electron.launch({
executablePath,
// Chromium refuses to start as root with its sandbox on, which is what a CI container is.
chromiumSandbox: process.getuid?.() !== 0,
args: [resolve("electron"), "--desktop-local"],
env: {
...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)
args: [resolve("electron")],
env: {...process.env, XDG_CONFIG_HOME: profile},
})
try {
const mainWindows = () =>
@ -31,21 +25,8 @@ test("the desktop baseline renders, navigates, and keeps external pages outside"
await expect.poll(() => mainWindows().length).toBe(1)
const [page] = mainWindows()
const errors: string[] = []
const developmentRequests: string[] = []
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 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")).not.toHaveAttribute("data-inline-script-executed")
expect(developmentRequests).toEqual([])
await app.evaluate(({BrowserWindow}) => BrowserWindow.getAllWindows()[0].setSize(1000, 700))
} finally {
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 {
await rm(profile, {recursive: true, force: true})
}

View file

@ -1,23 +1,5 @@
import {app} from "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({
window: {width: 1200, height: 800},
csp: {

View file

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

View file

@ -1,5 +1,4 @@
import {spawn, spawnSync} from "node:child_process"
import {access, readFile} from "node:fs/promises"
import {createRequire} from "node:module"
import {dirname, resolve} from "node:path"
import {fileURLToPath} from "node:url"
@ -9,7 +8,6 @@ const require = createRequire(import.meta.url)
const windows = process.platform === "win32"
let server
let child
let interrupted = false
let forceShutdown
const terminate = (signal = "SIGTERM") => {
@ -37,80 +35,60 @@ const terminate = (signal = "SIGTERM") => {
}
const onSignal = signal => {
interrupted = true
process.exitCode = signal === "SIGINT" ? 130 : 143
terminate()
forceShutdown ??= setTimeout(() => terminate("SIGKILL"), 5000)
forceShutdown.unref()
}
try {
try {
// 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,
})
}
// Vite also exits on SIGTERM; clean up descendants even if its handler exits first.
const onExit = () => terminate("SIGKILL")
try {
process.chdir(root)
process.env.NODE_ENV = "development"
process.env.FLOTILLA_DESKTOP = "1"
const {createServer} = await import("vite")
process.on("SIGINT", onSignal)
process.on("SIGTERM", onSignal)
server = await createServer({
mode: "development",
server: {host: "127.0.0.1", strictPort: true},
})
if (!interrupted) {
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()
child = spawn(
process.execPath,
[
// Capacitor 8.3.4 otherwise logs rejected platform hooks without failing the process.
"--unhandled-rejections=strict",
require.resolve("@capacitor/cli/bin/capacitor"),
"run",
"@capawesome/capacitor-electron",
],
{
cwd: root,
stdio: "inherit",
detached: !windows,
env: {...process.env, FLOTILLA_DESKTOP_DEV_URL: url.href},
},
)
const code = await new Promise((resolve, reject) => {
child.once("error", reject)
child.once("close", (code, signal) => resolve(signal ? 1 : code))
})
if (!interrupted) process.exitCode = code ?? 1
}
}
await server.listen()
server.printUrls()
child = spawn(
process.execPath,
[
// Capacitor 8.3.4 otherwise logs rejected platform hooks without failing the process.
"--unhandled-rejections=strict",
require.resolve("@capacitor/cli/bin/capacitor"),
"run",
"@capawesome/capacitor-electron",
],
{
cwd: root,
stdio: "inherit",
detached: !windows,
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) => {
child.once("error", reject)
child.once("close", (code, signal) => resolve(signal ? 1 : code))
})
process.exitCode ||= code ?? 1
} catch (error) {
if (!interrupted) {
console.error(error)
process.exitCode = 1
}
console.error(error)
process.exitCode = 1
} finally {
terminate("SIGKILL")
await server?.close()
clearTimeout(forceShutdown)
process.removeListener("SIGINT", onSignal)
process.removeListener("SIGTERM", onSignal)
process.removeListener("exit", onExit)
}