Skip to content

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

Open
shoootyou wants to merge 37 commits into
anomalyco:devfrom
shoootyou:feat/pwa-support
Open

feat(app): add PWA support with service worker and update prompt#31279
shoootyou wants to merge 37 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 and others added 30 commits June 7, 2026 15:24
- set theme_color and background_color to #F8F7F7 to match <head> meta
- add purpose:any icon entry (512x512) to satisfy Lighthouse PWA audit

Co-authored-by: yui-soul <yui-soul@users.noreply.github.com>
- 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>
…ld tests

- Fix createComponent(Show, ...) overload ambiguity via props cast
- Fix _banner() call: template() returns a factory function, not an element;
  call it directly instead of .cloneNode() on the function reference
- Add tests for start_url, id, theme_color exact value, and background_color
  in pwa.manifest.test.ts — all fields listed in plan must_haves.artifacts

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>
Verifies banner reappears if SW fires onNeedRefresh after user dismisses.

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>
- desktop: 1280x720 (form_factor: wide)
- mobile: 390x844 (form_factor: narrow)
- source: opencode.ai docs screenshot

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>
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>
The mock updateServiceWorker resets needRefresh to false on Reload.
triggerRegisterError must restore it before firing onRegisterError
so the show() && needRefresh() gate can pass and B10 is valid.

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>
shoootyou and others added 7 commits June 7, 2026 17:30
- '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>
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>
@shoootyou shoootyou requested a review from adamdotdevin as a code owner June 7, 2026 20:22
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 7, 2026

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

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