Added minimal Capacitor Electron desktop baseline (#424)

This commit is contained in:
Gaurav Chaudhary 2026-09-07 17:42:52 +00:00 committed by hodlbod
parent 57905be904
commit cec08e3353
23 changed files with 505 additions and 80 deletions

View file

@ -31,6 +31,11 @@ jobs:
- name: Check
run: pnpm run check
- name: Check desktop TypeScript
run: |
npm ci --prefix electron --ignore-scripts
npm --prefix electron run build
- name: Build
if: github.event_name != 'pull_request'
run: pnpm run build
@ -76,3 +81,27 @@ jobs:
path: build
retention-days: 7
if-no-files-found: error
desktop-smoke:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
- name: Install dependencies
run: |
corepack enable && pnpm i --frozen-lockfile
npm ci --prefix electron
pnpm exec playwright install-deps chromium
- name: Build desktop
run: pnpm run build:desktop
- name: Test desktop
run: xvfb-run -a pnpm run test:desktop

1
.gitignore vendored
View file

@ -85,3 +85,4 @@ CLAUDE.md
.DS_Store
Thumbs.db
package-lock.json
!electron/package-lock.json

View file

@ -45,6 +45,38 @@ If you're deploying a custom version of flotilla, be sure to remove the `plausib
See [CONTRIBUTING.md](CONTRIBUTING.md).
### Desktop development (Linux)
The Electron target is a development baseline. It has no supported installers,
release pipeline, packaging configuration, or auto-update setup.
**Use disposable accounts only.** The current secure-storage plugin falls back to
unencrypted `localStorage` on desktop. This is not secure desktop credential or
private-key storage. OS-protected secret storage is required before distribution.
Install the root dependencies with pnpm and the Electron subproject with npm,
following the platform's documented setup. Installing that subproject separately avoids
downloading Electron for ordinary web/mobile installs:
```sh
pnpm install --frozen-lockfile
npm ci --prefix electron
pnpm run build:desktop
pnpm run dev: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. `dev:desktop` opens those built assets; rerun `build:desktop` after frontend
changes. Live reload is deferred to the desktop configuration work.
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`. 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 builds and runs this suite under
Xvfb in a separate desktop job.
## Deployment
To run your own Flotilla, it's as simple as:

View file

@ -0,0 +1,10 @@
import {defineConfig} from "@playwright/test"
export default defineConfig({
testDir: ".",
forbidOnly: !!process.env.CI,
timeout: 60_000,
expect: {timeout: 15_000},
workers: 1,
reporter: "list",
})

101
e2e/desktop/smoke.spec.ts Normal file
View file

@ -0,0 +1,101 @@
import {mkdtemp, rm} from "node:fs/promises"
import {createRequire} from "node:module"
import {tmpdir} from "node:os"
import {join, resolve} from "node:path"
import {_electron, expect, test} from "@playwright/test"
test("the desktop baseline renders, navigates, and keeps external pages outside", async () => {
const profile = await mkdtemp(join(tmpdir(), "flotilla-desktop-"))
try {
const executablePath: string = createRequire(import.meta.url)(
resolve("electron/node_modules/electron"),
)
const app = await _electron.launch({
executablePath,
chromiumSandbox: true,
args: [resolve("electron")],
env: {...process.env, XDG_CONFIG_HOME: profile},
})
try {
const mainWindows = () =>
app.windows().filter(page => page.url().startsWith("capacitor-electron://"))
await expect.poll(() => mainWindows().length).toBe(1)
const [page] = mainWindows()
const errors: string[] = []
page.on("pageerror", error => errors.push(error.message))
await page.reload()
await expect(page.getByRole("heading")).toBeVisible()
const origin = await page.evaluate(() => location.origin)
expect(origin).toMatch(/^capacitor-electron:\/\//)
await page.getByRole("button", {name: "Log in", exact: true}).click()
await expect(page.getByTestId("login")).toBeVisible()
await page.goto(`${origin}/settings/profile`)
await expect(page.getByRole("heading")).toBeVisible()
await page.reload()
await expect(page.getByRole("heading")).toBeVisible()
expect(new URL(page.url()).pathname).toBe("/settings/profile")
expect(
await page.evaluate(async () => {
const url = URL.createObjectURL(
new Blob(["onmessage = event => postMessage(event.data)"], {type: "text/javascript"}),
)
const worker = new Worker(url)
try {
return await new Promise<string>((resolve, reject) => {
worker.onmessage = event => resolve(event.data)
worker.onerror = event => reject(new Error(event.message))
worker.postMessage("desktop worker ready")
})
} finally {
worker.terminate()
URL.revokeObjectURL(url)
}
}),
).toBe("desktop worker ready")
const externalUrls = await app.evaluateHandle(({shell}) => {
const urls: string[] = []
shell.openExternal = async (url: string) => {
urls.push(url)
}
return urls
})
const externalLink = page.locator('a[target="_blank"][href^="https:"]').first()
const externalUrl = await externalLink.evaluate((link: HTMLAnchorElement) => link.href)
await externalLink.click()
await expect.poll(() => externalUrls.jsonValue()).toContain(externalUrl)
expect(await page.evaluate(() => location.origin)).toBe(origin)
expect(app.windows()).toHaveLength(1)
expect(errors).toEqual([])
await page.evaluate(() => {
document.addEventListener(
"securitypolicyviolation",
({effectiveDirective}) => {
document.documentElement.dataset.cspViolation = effectiveDirective
},
{once: true},
)
const script = document.createElement("script")
script.textContent = "document.documentElement.dataset.inlineScriptExecuted = 'true'"
document.head.appendChild(script)
script.remove()
})
await expect(page.locator("html")).toHaveAttribute("data-csp-violation", "script-src-elem")
await expect(page.locator("html")).not.toHaveAttribute("data-inline-script-executed")
} finally {
await app.close()
}
} finally {
await rm(profile, {recursive: true, force: true})
}
})

6
electron/.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
node_modules
build
dist
app
generated
vendor

7
electron/main.ts Normal file
View file

@ -0,0 +1,7 @@
import {createCapacitorElectronApp} from "@capawesome/capacitor-electron"
createCapacitorElectronApp({
window: {width: 1200, height: 800},
// SvelteKit's meta CSP owns scripts; the platform default would block its hashed bootstrap.
csp: {policy: "base-uri 'self'; object-src 'none'"},
})

189
electron/package-lock.json generated Normal file
View file

@ -0,0 +1,189 @@
{
"name": "flotilla-electron",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "flotilla-electron",
"version": "0.0.0",
"devDependencies": {
"electron": "^43.0.0",
"typescript": "^5.5.0"
}
},
"node_modules/@electron-internal/extract-zip": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.5.tgz",
"integrity": "sha512-+bqFCP98pLI0Tt0XQo1TmlXtwjWchISndDOxCkEcIuUgXWpBnLyRI+2DU+mesvnMMX6L1XDqYNA0lXNDHd/yiA==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=22.12.0"
}
},
"node_modules/@electron/get": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@electron/get/-/get-5.1.0.tgz",
"integrity": "sha512-3kSBtG8ObcTVfXanm5vVJ6UnBLEVmVsRk1M+vGqCuMBV+XLCbJYuWQful+yIy0GQDsSlK0kHEriEHn7SPk4EnA==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.1.1",
"env-paths": "^3.0.0",
"graceful-fs": "^4.2.11",
"progress": "^2.0.3",
"semver": "^7.6.3",
"sumchecker": "^3.0.1"
},
"engines": {
"node": ">=22.12.0"
},
"optionalDependencies": {
"undici": "^7.24.4"
}
},
"node_modules/@types/node": {
"version": "24.13.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz",
"integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.18.0"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/electron": {
"version": "43.6.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-43.6.0.tgz",
"integrity": "sha512-DqVKYV+FXheMSLTxcMQ+NCo78BDgpnToSyIzXctlUtbP3lRGEuoo1P+C2n/90rJ7TvHgzP0bpP9fbbXxp4noIg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@electron-internal/extract-zip": "^1.0.1",
"@electron/get": "^5.0.0",
"@types/node": "^24.9.0"
},
"bin": {
"electron": "cli.js",
"install-electron": "install.js"
},
"engines": {
"node": ">= 22.12.0"
}
},
"node_modules/env-paths": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz",
"integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"license": "ISC"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/sumchecker": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
"integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"debug": "^4.1.0"
},
"engines": {
"node": ">= 8.0"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici": {
"version": "7.29.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz",
"integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=20.18.1"
}
},
"node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"dev": true,
"license": "MIT"
}
}
}

15
electron/package.json Normal file
View file

@ -0,0 +1,15 @@
{
"name": "flotilla-electron",
"private": true,
"version": "0.0.0",
"description": "Electron platform project for Flotilla.",
"main": "build/main.js",
"scripts": {
"build": "tsc",
"start": "electron ."
},
"devDependencies": {
"electron": "^43.0.0",
"typescript": "^5.5.0"
}
}

15
electron/tsconfig.json Normal file
View file

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "es2022",
"module": "node16",
"moduleResolution": "node16",
"outDir": "build",
"strict": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true
},
"include": [
"main.ts"
]
}

View file

@ -5,14 +5,17 @@
"scripts": {
"dev": "vite dev",
"build": "./scripts/build.sh",
"build:desktop": "bash scripts/build-desktop.sh",
"dev:desktop": "npm --prefix electron start",
"build:server": "vite build --config vite.config.server.ts",
"start": "node server.js",
"release:android": "./scripts/build.sh && cap build android --androidreleasetype APK --signing-type apksigner",
"bump": "node scripts/bump-version.mjs",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check src e2e playwright.config.ts && eslint src e2e",
"lint": "prettier --check src e2e playwright.config.ts electron/*.ts && eslint src e2e electron/*.ts",
"test": "playwright test",
"test:desktop": "playwright test --config e2e/desktop/playwright.config.ts",
"test:ui": "playwright test --ui",
"format": "git diff head --name-only --diff-filter d | grep -E '(js|ts|svelte|css)$' | xargs -r prettier --write",
"format:all": "prettier --write src",
@ -63,6 +66,7 @@
"@capacitor/share": "^8.0.1",
"@capawesome/capacitor-android-dark-mode-support": "^8.0.0",
"@capawesome/capacitor-badge": "^8.0.0",
"@capawesome/capacitor-electron": "0.1.1",
"@getalby/lightning-tools": "^6.1.0",
"@getalby/sdk": "^5.1.2",
"@hono/node-server": "^2.0.0",

Binary file not shown.

8
scripts/build-desktop.sh Normal file
View file

@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -e
export FLOTILLA_DESKTOP=1
source ./scripts/build-web.sh
pnpm exec cap sync @capawesome/capacitor-electron
npm --prefix electron run build

36
scripts/build-web.sh Normal file
View file

@ -0,0 +1,36 @@
#!/usr/bin/env bash
temp_env=$(declare -p -x)
if [ -f .env ]; then
source .env
fi
# Avoid overwriting env vars provided directly
# https://stackoverflow.com/a/69127685/1467342
eval "$temp_env"
if [[ -z $VITE_BUILD_HASH ]]; then
export VITE_BUILD_HASH=$(git rev-parse --short HEAD)
fi
if [[ $VITE_PLATFORM_LOGO =~ ^https:// ]]; then
curl -fSL "$VITE_PLATFORM_LOGO" -o static/logo.png
export VITE_PLATFORM_LOGO=static/logo.png
# @capacitor/assets reads its source logo from assets/, not static/, so
# propagate a custom platform logo there too for the native app icon/splash
cp "$VITE_PLATFORM_LOGO" assets/logo.png
cp "$VITE_PLATFORM_LOGO" assets/logo-dark.png
fi
# Ensure generator uses local path (dotenv may have loaded URL from .env)
VITE_PLATFORM_LOGO="${VITE_PLATFORM_LOGO}" npx pwa-assets-generator
npx vite build
# Replace index.html variables with stuff from our env
perl -i -pe"s|{DESCRIPTION}|$VITE_PLATFORM_DESCRIPTION|g" build/index.html
perl -i -pe"s|{ACCENT}|$VITE_PLATFORM_ACCENT|g" build/index.html
perl -i -pe"s|{NAME}|$VITE_PLATFORM_NAME|g" build/index.html
perl -i -pe"s|{URL}|$VITE_PLATFORM_URL|g" build/index.html

View file

@ -1,38 +1,6 @@
#!/usr/bin/env bash
temp_env=$(declare -p -x)
if [ -f .env ]; then
source .env
fi
# Avoid overwriting env vars provided directly
# https://stackoverflow.com/a/69127685/1467342
eval "$temp_env"
if [[ -z $VITE_BUILD_HASH ]]; then
export VITE_BUILD_HASH=$(git rev-parse --short HEAD)
fi
if [[ $VITE_PLATFORM_LOGO =~ ^https:// ]]; then
curl -fSL "$VITE_PLATFORM_LOGO" -o static/logo.png
export VITE_PLATFORM_LOGO=static/logo.png
# @capacitor/assets reads its source logo from assets/, not static/, so
# propagate a custom platform logo there too for the native app icon/splash
cp "$VITE_PLATFORM_LOGO" assets/logo.png
cp "$VITE_PLATFORM_LOGO" assets/logo-dark.png
fi
# Ensure generator uses local path (dotenv may have loaded URL from .env)
VITE_PLATFORM_LOGO="${VITE_PLATFORM_LOGO}" npx pwa-assets-generator
npx vite build
# Replace index.html variables with stuff from our env
perl -i -pe"s|{DESCRIPTION}|$VITE_PLATFORM_DESCRIPTION|g" build/index.html
perl -i -pe"s|{ACCENT}|$VITE_PLATFORM_ACCENT|g" build/index.html
perl -i -pe"s|{NAME}|$VITE_PLATFORM_NAME|g" build/index.html
perl -i -pe"s|{URL}|$VITE_PLATFORM_URL|g" build/index.html
source ./scripts/build-web.sh
npx cap sync
npx @capacitor/assets generate \

View file

@ -29,7 +29,7 @@
<h1 class="heading">Welcome to {PLATFORM_NAME}!</h1>
<p class="text-center">{PLATFORM_DESCRIPTION}</p>
</div>
<Button onclick={logIn}>
<Button aria-label="Log in" onclick={logIn}>
<CardButton primary>
{#snippet icon()}
<div><Icon icon={Login} size={7} /></div>

View file

@ -90,13 +90,13 @@
const hasSigner = $derived(getNip07() || signers.length > 0)
onMount(async () => {
if (Capacitor.isNativePlatform()) {
if (Capacitor.getPlatform() === "android") {
signers = await getNip55()
}
})
</script>
<Modal>
<Modal data-testid="login">
<ModalBody>
<h1 class="heading">Log in with Nostr</h1>
<p class="m-auto max-w-sm text-center">

View file

@ -4,7 +4,7 @@ import {Keyboard} from "@capacitor/keyboard"
import {noop} from "@welshman/lib"
export const syncKeyboard = () => {
if (!Capacitor.isNativePlatform()) return noop
if (!Capacitor.isPluginAvailable("Keyboard")) return noop
const showListener = Keyboard.addListener("keyboardWillShow", () => {
document.body.classList.add("keyboard-open")

View file

@ -17,7 +17,7 @@ export class Push {
if (Capacitor.getPlatform() === "android" && useFallback) {
Push._adapter = new AndroidFallbackNotifications()
} else if (Capacitor.isNativePlatform()) {
} else if (Capacitor.isPluginAvailable("PushNotifications")) {
Push._adapter = new CapacitorNotifications()
} else {
Push._adapter = new WebNotifications()

View file

@ -178,7 +178,7 @@ export const downloadText = async (filename: string, text: string) => {
// hand it to the native share sheet, letting the user save it to Files,
// Drive, a password manager, etc. Cache is the directory our FileProvider
// (android/app/src/main/res/xml/file_paths.xml) is configured to serve.
if (Capacitor.isNativePlatform()) {
if (["android", "ios"].includes(Capacitor.getPlatform())) {
const {Filesystem, Directory, Encoding} = await import("@capacitor/filesystem")
const {Share} = await import("@capacitor/share")

View file

@ -65,7 +65,7 @@
pomadeContext.setSignerUrls(env.POMADE_SIGNERS)
pomadeContext.setArgonWorker(import("@pomade/core/argon-worker.js?worker"))
if (Capacitor.isNativePlatform()) {
if (Capacitor.getPlatform() === "android") {
setNip55Plugin(NostrSignerPlugin)
}

View file

@ -5,6 +5,9 @@ import {vitePreprocess} from "@sveltejs/vite-plugin-svelte"
export default {
preprocess: vitePreprocess(),
kit: {
serviceWorker: {
register: process.env.FLOTILLA_DESKTOP !== "1",
},
adapter: adapter({
fallback: "index.html",
}),
@ -16,7 +19,7 @@ export default {
},
csp: {
directives: {
"script-src": ["self", "wasm-unsafe-eval", "plausible.coracle.social", "sha256-NpqGpeZTuPniNAucgyfqzWy9iIHwOswFzPzpigwvp/c="],
"script-src": ["self", "wasm-unsafe-eval", "https://plausible.coracle.social", "sha256-NpqGpeZTuPniNAucgyfqzWy9iIHwOswFzPzpigwvp/c="],
"worker-src": ["self", "blob:"],
"style-src": ["self", "unsafe-inline"],
"frame-src": ["none"],

View file

@ -44,44 +44,45 @@ export default defineConfig({
},
plugins: [
sveltekit(),
SvelteKitPWA({
registerType: "autoUpdate",
injectRegister: "auto",
// adapter-static writes the fallback shell after the service worker is generated, so the
// plugin can't glob it — this precaches "/" explicitly, giving workbox's navigation
// fallback something to bind to.
kit: {
spa: true,
adapterFallback: "/",
},
workbox: {
maximumFileSizeToCacheInBytes: 5 * 1024 ** 2, // 5 MB or set to something else
// The service worker is generated before SvelteKit prerenders anything, so the
// "prerendered/**" glob @vite-pwa/sveltekit forces in never matches. It only forces it
// when modifyURLPrefix is unset, and an empty one is a no-op.
modifyURLPrefix: {},
globPatterns: ["client/**/*.{js,css,ico,png,svg,webp,webmanifest}"],
},
manifest: {
name: process.env.VITE_PLATFORM_NAME,
short_name: process.env.VITE_PLATFORM_NAME,
theme_color: process.env.VITE_PLATFORM_ACCENT,
description: process.env.VITE_PLATFORM_DESCRIPTION,
// @ts-ignore
permissions: ["clipboardRead", "clipboardWrite", "unlimitedStorage"],
icons: [
{src: "pwa-64x64.png", sizes: "64x64", type: "image/png"},
{src: "pwa-192x192.png", sizes: "192x192", type: "image/png"},
{src: "pwa-512x512.png", sizes: "512x512", type: "image/png", purpose: "any"},
{
src: "maskable-icon-512x512.png",
sizes: "512x512",
type: "image/png",
purpose: "maskable",
},
],
},
}),
process.env.FLOTILLA_DESKTOP !== "1" &&
SvelteKitPWA({
registerType: "autoUpdate",
injectRegister: "auto",
// adapter-static writes the fallback shell after the service worker is generated, so the
// plugin can't glob it — this precaches "/" explicitly, giving workbox's navigation
// fallback something to bind to.
kit: {
spa: true,
adapterFallback: "/",
},
workbox: {
maximumFileSizeToCacheInBytes: 5 * 1024 ** 2, // 5 MB or set to something else
// The service worker is generated before SvelteKit prerenders anything, so the
// "prerendered/**" glob @vite-pwa/sveltekit forces in never matches. It only forces it
// when modifyURLPrefix is unset, and an empty one is a no-op.
modifyURLPrefix: {},
globPatterns: ["client/**/*.{js,css,ico,png,svg,webp,webmanifest}"],
},
manifest: {
name: process.env.VITE_PLATFORM_NAME,
short_name: process.env.VITE_PLATFORM_NAME,
theme_color: process.env.VITE_PLATFORM_ACCENT,
description: process.env.VITE_PLATFORM_DESCRIPTION,
// @ts-ignore
permissions: ["clipboardRead", "clipboardWrite", "unlimitedStorage"],
icons: [
{src: "pwa-64x64.png", sizes: "64x64", type: "image/png"},
{src: "pwa-192x192.png", sizes: "192x192", type: "image/png"},
{src: "pwa-512x512.png", sizes: "512x512", type: "image/png", purpose: "any"},
{
src: "maskable-icon-512x512.png",
sizes: "512x512",
type: "image/png",
purpose: "maskable",
},
],
},
}),
svg({
svgoOptions: {
multipass: true,