diff --git a/README.md b/README.md index 1f51cf71..e87d3876 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/capacitor.config.ts b/capacitor.config.ts index b7a7a0a7..68988976 100644 --- a/capacitor.config.ts +++ b/capacitor.config.ts @@ -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 diff --git a/e2e/desktop/smoke.spec.ts b/e2e/desktop/smoke.spec.ts index 5229fb6f..cd657feb 100644 --- a/e2e/desktop/smoke.spec.ts +++ b/e2e/desktop/smoke.spec.ts @@ -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}) } diff --git a/electron/main.ts b/electron/main.ts index 2d27a677..3b3d3639 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -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: { diff --git a/electron/package.json b/electron/package.json index 736080a7..788db736 100644 --- a/electron/package.json +++ b/electron/package.json @@ -7,7 +7,7 @@ "scripts": { "build": "tsc", "postinstall": "install-electron", - "start": "electron . --desktop-local" + "start": "electron ." }, "devDependencies": { "electron": "^43.0.0", diff --git a/scripts/dev-desktop.mjs b/scripts/dev-desktop.mjs index 1c6bef83..2acdffed 100644 --- a/scripts/dev-desktop.mjs +++ b/scripts/dev-desktop.mjs @@ -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) }