Skip to content

feat(app): add PWA support with service worker and update prompt#31279

Closed
shoootyou wants to merge 32 commits into
anomalyco:devfrom
shoootyou:feat/pwa-support
Closed

feat(app): add PWA support with service worker and update prompt#31279
shoootyou wants to merge 32 commits into
anomalyco:devfrom
shoootyou:feat/pwa-support

Conversation

@shoootyou

Copy link
Copy Markdown

Issue for this PR

Closes #19174
Closes #19119
Closes #27933
Addresses #27931
Addresses #30405
Addresses #19301

Note: #30405 is also addressed by the open PR #30399 — both add crossorigin="use-credentials" to the manifest link. Happy to drop that commit from this PR if #30399 merges first.

Type of change

  • Bug fix
  • New feature

What does this PR do?

The web UI had no service worker, so every reload re-fetched all assets and the app couldn't be installed as a PWA.

Service worker — adds vite-plugin-pwa with registerType: 'prompt' and Workbox precaching for the static shell (JS, CSS, fonts, icons). Only explicit SPA routes receive the navigation fallback — all other paths pass to the network by default (navigateFallbackAllowlist). SW is disabled in dev to preserve Vite HMR.

Update promptPwaUpdatePrompt component shows a non-blocking banner when a new SW is waiting. Reload activates it; Dismiss defers it. Mounted outside AppBaseProviders so it survives ErrorBoundary and connection errors.

Manifest fixes — adds id, scope, start_url, description, and screenshots fields. Fixes theme_color from #ffffff to #F8F7F7. Separate maskable and any icon entries per Chrome/Lighthouse guidance.

iOS — adds apple-mobile-web-app-capable, status bar style, and apple-touch-icon links for iPad (152×152, 167×167) alongside the existing 180×180.

Cache-ControlCache-Control: immutable on /assets/* in _headers, no-cache on root-level assets.

Proxy fixcrossorigin="use-credentials" on the manifest link (same fix as #30399).

How did you verify your code works?

  • bun test in packages/app passes (component tests B1–B9, manifest field tests).
  • bun run build + bun run serve in packages/app — install prompt appeared, SW registered and cached the shell, update banner showed after a second build and reload.
  • Tested on Android Chrome (standalone install) and iOS Safari (add to home screen) — both work.
  • Lighthouse PWA audit — all installability checks pass.

Screenshots / recordings

Screenshots of the install prompt, update banner, and Lighthouse audit available on request.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@shoootyou shoootyou requested a review from adamdotdevin as a code owner June 7, 2026 20:22
@github-actions

github-actions Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

Potential related PRs found:

  1. fix(app): support authenticated web app manifests #30399fix(app): support authenticated web app manifests

  2. fix(server): add Cache-Control headers to static assets #27934fix(server): add Cache-Control headers to static assets

    • Related: Addresses cache control strategy; PR 31279 also adds Cache-Control: immutable on /assets/* in _headers.
  3. fix(app): add service worker for cache-first static asset loading #27936fix(app): add service worker for cache-first static asset loading

    • Related: Another service worker implementation for static asset caching, which overlaps with PR 31279's Workbox precaching approach.

These are not duplicates of PR #31279 but represent overlapping work on the same system (PWA, service workers, caching). The PR author is already aware of the #30399 overlap and has offered to coordinate.

@shoootyou

Copy link
Copy Markdown
Author

Tested locally

Screen.Recording.2026-06-07.at.22.14.49.mov

shoootyou added a commit to shoootyou/opencode that referenced this pull request Jun 8, 2026
Adds full PWA support to packages/app/.

- Service worker via vite-plugin-pwa@1.3.0 with registerType: 'prompt'
- PwaUpdatePrompt component — non-blocking update banner
- Full site.webmanifest with all required fields and screenshots
- iOS meta tags + apple-touch-icon for iPhone, iPad, iPad Pro
- navigateFallbackAllowlist — only SPA routes get the SW fallback
- Cache-Control headers (immutable on assets, no-cache on root)
- crossorigin=use-credentials on manifest link for auth-proxy compat
- OG meta tags + screenshot labels
- Tests: B1-B9 component tests, manifest field tests

Upstream PR: anomalyco#31279
Upstream issues: anomalyco#19174, anomalyco#19119, anomalyco#27933, anomalyco#27931, anomalyco#30405, anomalyco#19301

Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
shoootyou and others added 22 commits June 10, 2026 00:19
- install vite-plugin-pwa@1.3.0 and workbox-window
- configure VitePWA with prompt registration and NetworkOnly for localhost:4096
- implement PwaUpdatePrompt component (B1-B8) using SolidJS programmatic API
- mount PwaUpdatePrompt outside provider tree in entry.tsx
- add vite-plugin-pwa/solid types to tsconfig
- add solid-web-browser-shim.ts preload to fix bun test JSX resolution
  (bun test uses node condition for solid-js; shim redirects to browser
   builds and provides React.createElement shim for JSX compat)
- update bunfig.toml to load solid-web-browser-shim.ts in test preload

Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
- set id: '/' to stabilize app identity across domains
- set start_url: '/' to fix Lighthouse 'start_url is not valid' error

Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
- resolve from src/ → app/public/, not packages/public/

Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
useRegisterSW returns needRefresh as [Accessor<boolean>, Setter<boolean>]
not a bare accessor. Calling it directly threw TypeError at runtime.

Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
Real API returns needRefresh as [Accessor<boolean>, Setter<boolean>].
Previous bare-accessor mock hid the runtime TypeError.

Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
Import JSX type directly from solid-js instead of the invalid
typeof import('solid-js').JSX.Element namespace reference.

Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
Navigation requests are relative paths in production; the absolute
URL pattern for localhost:4096 was misleading and had no effect.

Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
Without this, the manifest is not in the SW precache and a network
miss during install eligibility check silently prevents the install prompt.

Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
…gnal gate

- call SolidJS render dispose() to prevent reactive tree leaks per test
- extract findButton() helper with exact label matching
- B9 now pins that needRefresh() participates in visibility gate

Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
- add apple-mobile-web-app-capable for standalone mode on pre-iOS-17
- add apple-mobile-web-app-status-bar-style and title
- add 152x152 and 167x167 apple-touch-icon links for iPad and iPad Pro

Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
- add scope: '/' for explicit PWA scope declaration
- add purpose:any entry for 192x192 icon (Android older launchers)
- add SVG icon entry for Chromium desktop crisp rendering

Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
Resized from apple-touch-icon-v3.png (180x180) for:
- 152x152: iPad / iPad mini (2x)
- 167x167: iPad Pro retina

Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
Use space-separated 'maskable any' per W3C spec instead of two
separate entries — avoids Lighthouse audit warnings.

Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
…rompt

Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
Add onRegisterError callback to show the banner again if the
service worker update fails, preventing silent prompt loss.

Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
apple-touch-icon.png, favicon-96x96.png, favicon.ico, favicon.svg
are all superseded by their -v3 counterparts referenced in index.html.

Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
…ations

- B10: verify banner reappears after SW registration error (onRegisterError recovery path)
- remove '(fails until E5)' suffixes from manifest test names

Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
Use portable path resolution instead of machine-specific absolute path.

Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
shoootyou and others added 9 commits June 10, 2026 00:21
role=status does not universally imply a live region across all AT.
Explicit aria-live=polite ensures screen readers announce the update
banner when it appears.

Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
…waUpdatePrompt

- replace neutral-* with semantic design system tokens for theme compat
- remove aria-live=polite (redundant — role=status implies it per ARIA spec)

Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
It is a build-time tool only — workbox-window stays in dependencies
as it is imported at runtime via virtual:pwa-register/solid.

Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
- 'maskable any' combined purpose discouraged per Chrome/Lighthouse
- SVG icon removed from manifest (fails to load in SW context)
- use separate maskable/any entries for 512x512 PNG
- replace PWA screenshots with real captures (1440x940 desktop, 390x844 mobile)

Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
- add crossorigin=use-credentials to manifest link for auth proxy compat
- add Cache-Control: immutable to /assets/* in _headers for edge caching

Addresses anomalyco#30405, anomalyco#19301, anomalyco#27931

Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
…nylist

- remove onRegisterError: setShow(true) was dead code (needRefresh() stays
  false after handleReload, so the show() && needRefresh() gate never passed)
- add purpose:any entry for 192x192 icon in manifest
- add /pty/* to navigateFallbackDenylist

Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
The component's onRegisterError set show(true) but needRefresh()
stays false after handleReload, so show() && needRefresh() never
passed. The behavior was untestable in production.

Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
bunfig.toml already declares both happydom.ts and solid-web-browser-shim.ts
as test preloads. The explicit --preload flags in package.json scripts caused
GlobalRegistrator.register() to run twice per test process.

Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
…x192 any icon test

- add og:title, og:description, og:url to index.html
- add label field to both screenshots in site.webmanifest
- add Cache-Control: no-cache to root-level JS/CSS/MJS in _headers
- pin 192x192 any-purpose icon in manifest test

Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
…ow deps

Lockfile was missing vite-plugin-pwa@1.3.0 and workbox-window@7.4.1 entries
after cherry-pick rebase from origin/dev (which lacks these deps).

Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
@shoootyou

Copy link
Copy Markdown
Author

👋 Heads-up: I've opened a fresh PR that supersedes this one — #32162.

This branch predates the v1.17.x changes to the app build/serving layer and no longer applies/builds cleanly against the current dev. Rather than force-rebase a stale branch, #32162 re-implements the same PWA support (service worker + install/update prompt + manifest/iOS/Cache-Control fixes) cleanly on top of current dev, at parity with this PR, with the SPA navigation-fallback allowlist and tests updated to match.

Continuing the discussion in #32162. 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant