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,
|
`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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue