Simplify desktop development workflow after review
Refs #51 Signed-off-by: Gaurav Chaudhary <chaudharygaurav2004@gmail.com>
This commit is contained in:
parent
f8565aecb0
commit
9b43ff72a9
6 changed files with 40 additions and 132 deletions
14
README.md
14
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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
"scripts": {
|
||||
"build": "tsc",
|
||||
"postinstall": "install-electron",
|
||||
"start": "electron . --desktop-local"
|
||||
"start": "electron ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^43.0.0",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue