Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 01934bcf79a1a826d58ea95b727576aa60644719
parent cd6ec28eae72d7ad2eedc92406f55c4421753eb1
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Thu,  2 Jul 2026 01:57:51 -0400

Phase 6a: PWA opt-in — dumb-by-default site instances

Make an installable PWA (service worker + web manifest + installability) an
opt-in axis independent of the shell, per the federation plan:

- New export/app/lib/mode.ts: instanceMode() ("hub" iff INSTANCE_MODE=hub, else
  "site") and shipsPwa() (true in hub mode; in site mode reads the selected
  site's `pwa` config flag — default false).
- layout.tsx gates <ServiceWorkerRegister/> and the manifest metadata link on
  shipsPwa(). A dumb instance renders neither.
- Relocate the SW source out of the auto-copied public/ dir
  (export/service-worker/site-sw.js) and have compose-site emit public/sw.js
  ONLY when the site ships a PWA — so a dumb instance emits no /sw.js at all,
  not just an unregistered one. public/sw.js is now generated (gitignored),
  mirroring site.json/_headers.
- compose-site's shipsPwa() resolves from site.pwa (|| INSTANCE_MODE=hub),
  matching mode.ts, so the descriptor's pwa field and the app shell agree.

BEHAVIOR CHANGE (approved): existing sites without `pwa: true` are no longer
installable; they stay fully federatable (CORS JSON + /site.json) but ship no
service worker. Set pwa: true in a site's config to keep it installable. The
federation contract is emitted regardless of pwa.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Diffstat:
M.gitignore | 1+
Mcommon/bin/compose-site.ts | 29++++++++++++++++++++++++-----
Mexport/app/layout.tsx | 9++++++---
Aexport/app/lib/mode.ts | 29+++++++++++++++++++++++++++++
Rexport/public/sw.js -> export/service-worker/site-sw.js | 0
5 files changed, 60 insertions(+), 8 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -58,6 +58,7 @@ yarn-error.log* /export/public/duplicates.json /export/public/site.json /export/public/_headers +/export/public/sw.js /export/.export-index/ /transcripts/index.mdb/ diff --git a/common/bin/compose-site.ts b/common/bin/compose-site.ts @@ -57,7 +57,7 @@ async function emitFederationFiles( if (!(await exists(manifestPath))) return; // no composed data → no descriptor const manifest = JSON.parse(await readFile(manifestPath, "utf8")) as Manifest; const descriptor = buildSiteDescriptor(site, manifest, resolveSocialLinks(site), { - pwa: shipsPwa(), + pwa: shipsPwa(site), hubUrl: resolveHubUrl(site), }); await writeFile( @@ -66,10 +66,26 @@ async function emitFederationFiles( ); } -// Whether this build ships an installable PWA. Site builds default to a dumb -// instance unless SHIP_PWA is set (see export/app/lib/mode.ts shipsPwa()). -function shipsPwa(): boolean { - return process.env.SHIP_PWA === "1" || process.env.INSTANCE_MODE === "hub"; +// Whether this build ships an installable PWA. Resolved from the site's `pwa` +// config flag (site builds are dumb instances by default) or forced on in hub +// mode. Keep in sync with export/app/lib/mode.ts shipsPwa(). +function shipsPwa(site: Site): boolean { + return site.pwa === true || process.env.INSTANCE_MODE === "hub"; +} + +// Compose the service worker into the served public dir ONLY when this instance +// ships a PWA. The SW source lives outside public/ (export/service-worker/) so +// a dumb instance emits no /sw.js at all — not just an unregistered one. Mirrors +// how site.json/_headers are generated rather than checked in. +async function composeServiceWorker( + site: Site, + paths: ReturnType<typeof getPaths>, +): Promise<void> { + const dest = path.join(paths.exportPublicDir, "sw.js"); + await rm(dest, { force: true }); + if (!shipsPwa(site)) return; + const src = path.join(paths.monorepoRoot, "export", "service-worker", "site-sw.js"); + if (await exists(src)) await cp(src, dest); } async function exists(p: string): Promise<boolean> { @@ -182,6 +198,9 @@ async function main(): Promise<void> { // --- federation contract: /site.json descriptor + CORS _headers --- await emitFederationFiles(site, paths); + // --- service worker (only when this instance ships a PWA) --- + await composeServiceWorker(site, paths); + const channelDirs = (await readdir(paths.exportTranscriptsDir).catch( () => [] as string[], )).length; diff --git a/export/app/layout.tsx b/export/app/layout.tsx @@ -5,6 +5,7 @@ import { ThemeProvider } from "yt-dlp-transcript-common/components/ThemeProvider import { QueryProvider } from "yt-dlp-transcript-common/components/QueryProvider"; import { parseAccent, siteAccentVars } from "yt-dlp-transcript-common/lib/accent"; import { currentSite } from "./lib/site"; +import { shipsPwa } from "./lib/mode"; import Header from "./components/Header"; import Footer from "./components/Footer"; import { ServiceWorkerRegister } from "./components/ServiceWorkerRegister"; @@ -23,8 +24,10 @@ export function generateMetadata(): Metadata { template: `%s — ${s.siteTitle}`, }, description: s.siteDescription, - // Installable PWA: manifest + icon set + iOS standalone hints. - manifest: "/manifest.webmanifest", + // Installability is opt-in: only a PWA-shipping instance links the manifest + // (and registers the SW below). A dumb instance omits both — it stays + // federatable JSON, but not independently installable. + ...(shipsPwa() ? { manifest: "/manifest.webmanifest" } : {}), icons: { icon: [ { url: "/icons/icon-192.png", sizes: "192x192", type: "image/png" }, @@ -62,7 +65,7 @@ export default async function RootLayout({ > <body className="min-h-full flex flex-col bg-background text-foreground font-sans"> <ThemeScript defaultTheme="base" defaultMode="system" /> - <ServiceWorkerRegister /> + {shipsPwa() && <ServiceWorkerRegister />} <ThemeProvider defaultTheme="base" defaultMode="system"> <QueryProvider> <Header /> diff --git a/export/app/lib/mode.ts b/export/app/lib/mode.ts @@ -0,0 +1,29 @@ +import { currentSite } from "./site"; + +// The export app renders one of two shells, selected at build time: +// site (default): a single-site archive, exactly as before. +// hub: the federated Archilyzer hub, reading many sites at runtime. +// +// The single-site path is never touched unless INSTANCE_MODE is explicitly +// "hub", so a plain `next build` stays a site build. +export type InstanceMode = "site" | "hub"; + +export function instanceMode(): InstanceMode { + return process.env.INSTANCE_MODE === "hub" ? "hub" : "site"; +} + +// Whether this build ships an installable PWA — the "dangerous permissions" +// surface: a service worker, a web manifest, and installability. This is an +// axis INDEPENDENT of the shell: +// - hub mode always ships the PWA (Archilyzer IS the installable app); +// - site mode is a dumb instance by default (federatable JSON only, not +// installable) and opts in per site via the `pwa` config flag. +// +// Resolved purely from config that is already available at build/render time +// (INSTANCE_MODE env + the selected site's `pwa`), so the app shell and the +// compose step agree without threading an extra env var. Keep this in sync with +// compose-site.ts's shipsPwa(). +export function shipsPwa(): boolean { + if (instanceMode() === "hub") return true; + return currentSite().pwa === true; +} diff --git a/export/public/sw.js b/export/service-worker/site-sw.js