feat(pwa): add service worker, manifest, and subpath-aware build

The precache list is generated from Bun.build's outputs, so hashed asset
names cannot drift. A waiting worker prompts to reload rather than
pinning the user to a stale build.

Two fixes beyond the brief: naming.entry changed from "[dir]/[name].[ext]"
to "[name].[ext]" because [dir] resolved to "src", putting index.html at
dist/src/index.html instead of the dist root; and external:
["*.webmanifest", "*.svg"] was added because Bun's HTML bundler otherwise
tries to resolve <link> hrefs as module imports, which fails outright for
the not-yet-generated manifest.webmanifest and would hash icon.svg,
breaking the fixed paths the manifest and service worker depend on.

happy-dom is registered per-file in src/ui/registerSw.test.ts only, not
as a global test preload, so src/domain/smoke.test.ts still guards the
domain layer's DOM-free invariant.

PNG icons (192px/512px) are not included — binary image generation isn't
available here; a human needs to add them for full Chrome installability.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5 committed 2026-09-01 21:22:58 +02:00
1 parent 31f030fdea
commit c069ed3a94
10 files changed
+264 -5

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { applyUpdate, registerServiceWorker } from "../registerSw.ts";
export function UpdatePrompt() {
const [ready, setReady] = useState(false);
useEffect(() => {
registerServiceWorker(() => setReady(true));
}, []);
if (!ready) return null;
return (
<div className="fixed bottom-4 right-4 flex items-center gap-3 rounded border bg-background p-3 text-sm shadow-lg">
<span>A new version is ready.</span>
<Button size="sm" onClick={applyUpdate}>
Reload
</Button>
<Button size="sm" variant="ghost" onClick={() => setReady(false)}>
Later
</Button>
</div>
);
}