From 272fbcfa8ab1f16da49f6630b2abd1a8f5f3c504 Mon Sep 17 00:00:00 2001 From: yiekheng Date: Sun, 10 May 2026 13:20:45 +0800 Subject: [PATCH] feat(web): PWA via @serwist/next + manifest + icons (P3/T22) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The web app is now installable on a phone home screen with offline fallback for static assets and the navigation shell. Pieces ------ - `src/app/manifest.webmanifest/route.ts` — dynamic manifest route. Standalone display mode, portrait orientation, dark theme matching the app, "any maskable" icons so the same PNG works for both regular launchers and Android adaptive icons. - `src/pwa/sw.ts` — service worker entry. Uses serwist's stock recipe: skipWaiting + clientsClaim so a new worker takes over on the next navigation, navigationPreload to race the network with the worker boot, and `defaultCache` for HTML-network-first / static-cache-first / image+font cache TTLs. - `next.config.ts` — wraps the existing config with `withSerwistInit`. Disabled in development (`NODE_ENV !== "production"`) because a service worker on every dev reload makes hot-reload extremely flaky. - `package.json` build script switched to `next build --webpack`. `@serwist/next` doesn't yet support Turbopack (it logs a warning and silently skips emitting `sw.js`), and Next 16 defaults the build to Turbopack. The dev server still uses Turbopack — only production builds switch to webpack. - `src/app/layout.tsx` metadata gains `manifest`, `icons.icon` (192 + 512 PNG), and `icons.apple` (180 PNG). The existing `appleWebApp.capable` already opts iOS into standalone mode. Icons ----- Generated by a tiny one-shot script (`scripts/gen-pwa-icons.ts`) that uses the workspace's already-installed sharp to render an SVG wordmark at 512 / 192 / 180 px. Placeholder branding (dark square with "cm" wordmark) — swap in real artwork later by editing the SVG in the script and re-running `pnpm --filter @cmbot/web run gen:icons`. Build artefacts --------------- - `apps/web/public/icon-512.png`, `icon-192.png`, `apple-touch-icon.png` ARE committed (stable input). - `apps/web/public/sw.js` and `swe-worker-*.js` are NOT — they're regenerated on every production build. Added to `.gitignore`. Verification ------------ - Production build emits `[serwist] Bundling the service worker script with the URL '/sw.js' and the scope '/'...` and `sw.js` shows up in `public/`. - `/manifest.webmanifest` is in the build's static-route table. - 249 web tests still passing. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 6 + apps/web/next-env.d.ts | 2 +- apps/web/next.config.ts | 15 +- apps/web/package.json | 9 +- apps/web/public/apple-touch-icon.png | Bin 0 -> 1013 bytes apps/web/public/icon-192.png | Bin 0 -> 1173 bytes apps/web/public/icon-512.png | Bin 0 -> 6282 bytes apps/web/scripts/gen-pwa-icons.ts | 69 +++++ apps/web/src/app/layout.tsx | 12 + .../web/src/app/manifest.webmanifest/route.ts | 29 +++ apps/web/src/pwa/sw.ts | 32 +++ pnpm-lock.yaml | 242 ++++++++++++++++++ 12 files changed, 412 insertions(+), 4 deletions(-) create mode 100644 apps/web/public/apple-touch-icon.png create mode 100644 apps/web/public/icon-192.png create mode 100644 apps/web/public/icon-512.png create mode 100644 apps/web/scripts/gen-pwa-icons.ts create mode 100644 apps/web/src/app/manifest.webmanifest/route.ts create mode 100644 apps/web/src/pwa/sw.ts diff --git a/.gitignore b/.gitignore index e54ff74..2fe3f12 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,12 @@ dist/ .turbo/ *.tsbuildinfo +# serwist emits these into apps/web/public/ on every production build. +# Icons (icon-*.png, apple-touch-icon.png) ARE committed; the generated +# service-worker bundle is regenerated by the build itself. +apps/web/public/sw.js +apps/web/public/swe-worker-*.js + # env files: per project decision, .env.development and .env.production # ARE committed to this private Gitea. Only ignore example overrides: .env.local diff --git a/apps/web/next-env.d.ts b/apps/web/next-env.d.ts index c4b7818..9edff1c 100644 --- a/apps/web/next-env.d.ts +++ b/apps/web/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index 7e59214..254f983 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -1,5 +1,6 @@ import type { NextConfig } from "next"; import { join } from "node:path"; +import withSerwistInit from "@serwist/next"; // Pin Turbopack's workspace root explicitly — pnpm + Turbopack can't always // infer it inside Docker bind mounts. @@ -34,4 +35,16 @@ const nextConfig: NextConfig = { }, }; -export default nextConfig; +// PWA: @serwist/next compiles `src/pwa/sw.ts` into `public/sw.js` at +// production build time, baking in the static-asset precache manifest. +// We disable it in dev because Turbopack + a service worker on every +// reload makes hot-reload extremely flaky. +const withSerwist = withSerwistInit({ + swSrc: "src/pwa/sw.ts", + swDest: "public/sw.js", + cacheOnNavigation: true, + reloadOnOnline: true, + disable: process.env.NODE_ENV !== "production", +}); + +export default withSerwist(nextConfig); diff --git a/apps/web/package.json b/apps/web/package.json index 8aa8adc..9607371 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -5,16 +5,18 @@ "type": "module", "scripts": { "dev": "next dev --hostname 0.0.0.0", - "build": "next build", + "build": "next build --webpack", "start": "next start --hostname 0.0.0.0", "lint": "next lint", "typecheck": "tsc --noEmit", - "test": "vitest run" + "test": "vitest run", + "gen:icons": "tsx scripts/gen-pwa-icons.ts" }, "dependencies": { "@cmbot/db": "workspace:*", "@cmbot/shared": "workspace:*", "@hookform/resolvers": "^5.2.2", + "@serwist/next": "^9.5.11", "@types/luxon": "^3.4.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -33,6 +35,7 @@ "react-dom": "^19.0.0", "react-hook-form": "^7.75.0", "server-only": "^0.0.1", + "serwist": "^9.5.11", "shadcn": "^4.7.0", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", @@ -47,7 +50,9 @@ "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitest/ui": "^2.1.9", + "sharp": "^0.34.5", "tailwindcss": "^4.0.0", + "tsx": "^4.19.0", "typescript": "^5.5.0", "vitest": "^2.1.9" } diff --git a/apps/web/public/apple-touch-icon.png b/apps/web/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f6acb7f415d4e1fac52864ff3182c26ac76b6624 GIT binary patch literal 1013 zcmeAS@N?(olHy`uVBq!ia0vp^TR@nD4M^IaWitX&oCO|{#S9GG!XV7ZFl&wk0|WDN zPZ!6KiaBquZRBNP=d$mo6w| z++&{n*nvZ2f#U-~5ckd~cd&;>gg(<~lnvb!`|I&RezOPfT6=!f{0HV^22WQ%mvv4F FO#s*1vLyfj literal 0 HcmV?d00001 diff --git a/apps/web/public/icon-192.png b/apps/web/public/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..03f690a8af82626f3d9de564dfc8f4ffe3f882f8 GIT binary patch literal 1173 zcmeH{T}V@57{}kUot+)`<=I?^HK%P;amdJpQce`kaVyV)#8y;Rq7rczH};`T`!z?J`~JJefqv_WuRotklUk5KO_94&zf^AkZ58aLTAeM5q_^ zS|IrdP6^lmXGH-!O&mu~hA0n--L-lsfei@S*mj=zi^CWC#J`X+FLTl-xf6BRMiA#QRpLY}Y{ACm z;P=$!fmZ9VucMR%Oi|$+n6NW@ag_GcfJbniIzE8Lg+rz30`J4ya0ohefb+5K)jcF^ zX6ndm;(gzZRX_g+=AQzCKU)(84qwFI(NQx_Q3Az(;tmwh`a0X!DYxfZ*-w U;HAjyPWaCt*X|PMaFNIV8~Ia5wEzGB literal 0 HcmV?d00001 diff --git a/apps/web/public/icon-512.png b/apps/web/public/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..dc07d6fb0b27555cd330531c963f8d93a625cecc GIT binary patch literal 6282 zcmeHMO-K}B7=CAGnw@kx+aNOq=!XkG=e&$ zbchlw4|XU*&4Mtp#@ikYCHx^W-GgX{LctE1;x;>f-`63$lqk$$nQxx?=9~9>f1dZ5 z*<0mhMQn^G1_0QS;=&35hdOisL#u}$ZPUNh!@=g_>Z<^;hoV0Mdiyv4E~lh0ud=Q0 z`+VB%+BA3J_>0A{&H3}K)vXC{_)Efed2aU6fxGR`>Yoy%%c}r+dM3Q`IU+rwwKLZM zAqnm%?(+5=28J?bWjPR>h~PxW4QqA{xETk{i4sA#ux>}5zJ{mG3C4IzWOJx%;nb?j z`X$=Dpa|E2cT>6nOALbR5M0ZcODaMWA@Gd$ATHz=3>$@Y|5_YS>$#c@GTqouWUj>@ z^GEIBib;@pZ6xw1Fgqy(XCOG8VyxM(kqJv4p6ZOQCiZVcx7E3kta(T1@=rn1Pv)LS z^V`lI6w%I=4D_t<=mE=|@F;ECP|UuytPc#)9*ImIuOOq2mVLk*b1lWx;jlDv`thTqh& z$>wve9r} zo%Ar80Hb1Ev>`#8G^6gzsV}}b5S5?~DMsVSM$Kv*5Re|;o%?M=J`PG>@ z3tL~9;AY3jv?<oQ)_QcrotIw{m5Ca*%HBlmvr53$uPUG z8*f&2Uss#e{&{z^c@F0}TQ-OGfo!(R3%%O>n~w-~_p-a!e*E%icQ3np$$$Ci(_Y<` Z8-Hc;VMCz4OZ{^JN=}s(4(5xUzX8vi)VKfu literal 0 HcmV?d00001 diff --git a/apps/web/scripts/gen-pwa-icons.ts b/apps/web/scripts/gen-pwa-icons.ts new file mode 100644 index 0000000..3e2ab06 --- /dev/null +++ b/apps/web/scripts/gen-pwa-icons.ts @@ -0,0 +1,69 @@ +#!/usr/bin/env tsx +/** + * Generate placeholder PWA icons (icon-512.png, icon-192.png, + * apple-touch-icon.png) into apps/web/public/. + * + * Run once via `pnpm --filter @cmbot/web run gen:icons`. The output is + * intentionally minimal — a dark square with the "cm" wordmark in + * a light bold sans-serif — until a designer hands us a real icon. + * + * Sharp is already in the workspace's node_modules (Baileys depends + * on it), so we re-use it here rather than introducing a new image + * library. Output is written as PNG with no alpha channel; the + * "any maskable" purpose in the manifest covers both regular launch + * icons and Android adaptive icons (which crop to their own shape). + */ + +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import sharp from "sharp"; + +const OUT_DIR = join(import.meta.dirname, "..", "public"); + +const BG = "#0a0a0a"; // matches the manifest theme_color +const FG = "#ffffff"; // wordmark +const TEXT = "cm"; + +/** Render the icon at the given pixel size. */ +async function renderIcon(size: number): Promise { + // Font size is tuned to fill ~half the icon's height with comfortable + // padding around the wordmark — same proportions Apple/Google use + // for their own home-screen icons. + const fontSize = Math.round(size * 0.46); + const svg = ` + + + ${TEXT} + + `.trim(); + return sharp(Buffer.from(svg)).png().toBuffer(); +} + +async function main(): Promise { + const targets: Array<{ size: number; filename: string }> = [ + { size: 512, filename: "icon-512.png" }, + { size: 192, filename: "icon-192.png" }, + { size: 180, filename: "apple-touch-icon.png" }, + ]; + for (const { size, filename } of targets) { + const png = await renderIcon(size); + const out = join(OUT_DIR, filename); + await writeFile(out, png); + console.log(` ${filename} (${size}×${size}, ${png.byteLength} bytes)`); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 171c7ea..e3bf223 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -9,6 +9,18 @@ export const metadata: Metadata = { title: "cm WhatsApp Bot", description: "Self-hosted WhatsApp reminder bot", applicationName: "cm WhatsApp Bot", + // PWA wiring: the manifest comes from the dynamic route at + // src/app/manifest.webmanifest/route.ts, the apple-touch-icon is + // emitted from public/, and `appleWebApp.capable` lets iOS treat the + // page like a standalone app when added to the home screen. + manifest: "/manifest.webmanifest", + icons: { + icon: [ + { url: "/icon-192.png", sizes: "192x192", type: "image/png" }, + { url: "/icon-512.png", sizes: "512x512", type: "image/png" }, + ], + apple: [{ url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" }], + }, appleWebApp: { capable: true, title: "cm WA Bot", statusBarStyle: "default" }, }; diff --git a/apps/web/src/app/manifest.webmanifest/route.ts b/apps/web/src/app/manifest.webmanifest/route.ts new file mode 100644 index 0000000..2bd8cc0 --- /dev/null +++ b/apps/web/src/app/manifest.webmanifest/route.ts @@ -0,0 +1,29 @@ +import { NextResponse } from "next/server"; + +/** + * PWA manifest. Served from `/manifest.webmanifest` so the document + * `` (set up via + * Next's metadata API in layout.tsx) can find it. + * + * `purpose: "any maskable"` lets the same icon work for both regular + * launch icons and Android maskable icons (where the OS crops the + * icon to a system-defined shape). `display: "standalone"` removes + * the browser chrome when launched from the home screen. + */ +export function GET() { + return NextResponse.json({ + name: "cm WhatsApp Bot", + short_name: "cm WA Bot", + description: "Self-hosted WhatsApp reminder bot", + start_url: "/", + scope: "/", + display: "standalone", + orientation: "portrait", + background_color: "#0a0a0a", + theme_color: "#0a0a0a", + icons: [ + { src: "/icon-192.png", sizes: "192x192", type: "image/png", purpose: "any maskable" }, + { src: "/icon-512.png", sizes: "512x512", type: "image/png", purpose: "any maskable" }, + ], + }); +} diff --git a/apps/web/src/pwa/sw.ts b/apps/web/src/pwa/sw.ts new file mode 100644 index 0000000..5d92319 --- /dev/null +++ b/apps/web/src/pwa/sw.ts @@ -0,0 +1,32 @@ +/// +import { defaultCache } from "@serwist/next/worker"; +import { Serwist } from "serwist"; + +declare const self: ServiceWorkerGlobalScope & { + __SW_MANIFEST: (string | { url: string; revision: string | null })[]; +}; + +/** + * The service worker entry. `@serwist/next` builds this file into + * `public/sw.js` at production build time and the manifest is + * substituted into `__SW_MANIFEST` (URLs the worker should precache). + * + * - `skipWaiting` + `clientsClaim`: a new worker takes over the page + * on the next navigation rather than waiting for every tab to + * close. Operators tend to live in one tab; faster updates win. + * - `navigationPreload: true`: tells the browser it can race the + * network fetch for navigations alongside the worker boot, cutting + * first-paint when the worker is cold. + * - `runtimeCaching: defaultCache`: serwist's stock recipe — HTML + * network-first with offline fallback, static assets cache-first, + * image / font caches with sensible TTLs. + */ +const serwist = new Serwist({ + precacheEntries: self.__SW_MANIFEST, + skipWaiting: true, + clientsClaim: true, + navigationPreload: true, + runtimeCaching: defaultCache, +}); + +serwist.addEventListeners(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad1c75f..4321d28 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,6 +84,9 @@ importers: '@hookform/resolvers': specifier: ^5.2.2 version: 5.2.2(react-hook-form@7.75.0(react@19.2.6)) + '@serwist/next': + specifier: ^9.5.11 + version: 9.5.11(next@16.2.6(@babel/core@7.29.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6)(typescript@5.9.3) '@types/luxon': specifier: ^3.4.2 version: 3.7.1 @@ -138,6 +141,9 @@ importers: server-only: specifier: ^0.0.1 version: 0.0.1 + serwist: + specifier: ^9.5.11 + version: 9.5.11(browserslist@4.28.2)(typescript@5.9.3) shadcn: specifier: ^4.7.0 version: 4.7.0(@types/node@22.19.18)(typescript@5.9.3) @@ -175,9 +181,15 @@ importers: '@vitest/ui': specifier: ^2.1.9 version: 2.1.9(vitest@2.1.9) + sharp: + specifier: ^0.34.5 + version: 0.34.5 tailwindcss: specifier: ^4.0.0 version: 4.3.0 + tsx: + specifier: ^4.19.0 + version: 4.21.0 typescript: specifier: ^5.5.0 version: 5.9.3 @@ -2158,6 +2170,57 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@serwist/build@9.5.11': + resolution: {integrity: sha512-PQfW+LhADYFOOp0PhEnjlgJCyKor6cYa06d3rID1OpiKzkmCApJV1WYfdTBB96jXaWv6OWcWSbSV4tqDLxvaVA==} + engines: {node: '>=18.0.0'} + peerDependencies: + typescript: '>=5.0.0' + peerDependenciesMeta: + typescript: + optional: true + + '@serwist/next@9.5.11': + resolution: {integrity: sha512-omT32H7U21ihCymSvOG9QeRJBuOEomJx4JdzKhUoqOW3DR10tH3m84VOHj3BvK0OcA7av3qj5FsyNFBB+f0n8A==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@serwist/cli': ^9.5.11 + next: '>=14.0.0' + react: '>=18.0.0' + typescript: '>=5.0.0' + peerDependenciesMeta: + '@serwist/cli': + optional: true + typescript: + optional: true + + '@serwist/utils@9.5.11': + resolution: {integrity: sha512-zqxmwuHqWA3OwN82Wo8gFZ9QBemygJP3cap5JWAOG4UyJZgUZfmBXAXj+IMaD4eKZ/6pqrxHHDZ9uSWZmJ1mXA==} + peerDependencies: + browserslist: '>=4' + peerDependenciesMeta: + browserslist: + optional: true + + '@serwist/webpack-plugin@9.5.11': + resolution: {integrity: sha512-SlvO3A1UMcc1htCzMtLCtPQK6yISCO7B859ixLv7EiY/yayXjVxGm9vHqkJYpQ768PWyjEZXRY/X6EGRMA6wJQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + typescript: '>=5.0.0' + webpack: 4.4.0 || ^5.9.0 + peerDependenciesMeta: + typescript: + optional: true + webpack: + optional: true + + '@serwist/window@9.5.11': + resolution: {integrity: sha512-OrH9srhmifUvY36NuukHSZby24XTEk4pHh3pfY0GBQzA9ouU1fYh+ORWhKxH7/wkVHRr3sc4YAhjtpfL14PjjQ==} + peerDependencies: + typescript: '>=5.0.0' + peerDependenciesMeta: + typescript: + optional: true + '@sindresorhus/merge-streams@4.0.0': resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} @@ -2342,6 +2405,9 @@ packages: '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/validate-npm-package-name@4.0.2': resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==} @@ -2612,6 +2678,10 @@ packages: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} + common-tags@1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + content-disposition@1.1.0: resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} engines: {node: '>=18'} @@ -3113,6 +3183,10 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -3172,6 +3246,9 @@ packages: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} + idb@8.0.3: + resolution: {integrity: sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -3327,6 +3404,9 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -3404,6 +3484,9 @@ packages: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + log-symbols@6.0.0: resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} engines: {node: '>=18'} @@ -3482,6 +3565,10 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + mpg123-decoder@1.0.3: resolution: {integrity: sha512-+fjxnWigodWJm3+4pndi+KUg9TBojgn31DPk85zEsim7C6s0X5Ztc/hQYdytXkwuGXH+aB0/aEkG40Emukv6oQ==} @@ -3673,6 +3760,10 @@ packages: resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} engines: {node: '>=12'} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -3790,6 +3881,10 @@ packages: resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} engines: {node: '>=20'} + pretty-bytes@6.1.1: + resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} + engines: {node: ^14.13.1 || >=16.0.0} + pretty-ms@9.3.0: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} @@ -3820,6 +3915,10 @@ packages: pump@3.0.4: resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + qified@0.10.1: resolution: {integrity: sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==} engines: {node: '>=20'} @@ -3988,6 +4087,11 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + semver@7.8.0: resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} engines: {node: '>=10'} @@ -4008,6 +4112,14 @@ packages: server-only@0.0.1: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + serwist@9.5.11: + resolution: {integrity: sha512-Bq6uwJFd4ET60BWI77v3VbazKHv6k7lECOiiCFwKyBu/slaCn0GHJ5L5RfsuJUKrnbD9lYUCDo6sqaKRM5M2vA==} + peerDependencies: + typescript: '>=5.0.0' + peerDependenciesMeta: + typescript: + optional: true + set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -4089,6 +4201,11 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions + split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -4235,6 +4352,9 @@ packages: resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} engines: {node: '>=16'} + tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + ts-morph@26.0.0: resolution: {integrity: sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==} @@ -4399,9 +4519,15 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + whatsapp-rust-bridge@0.5.3: resolution: {integrity: sha512-Xb3GAgtWQQJ30oI4a4pjM4+YUeli9CMLTwTIewUrb+AJMFElIkiT5uo+j1Zhc+amiV0Jj+LfX76c/EEZirJbGA==} + whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + which-module@2.0.1: resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} @@ -4496,6 +4622,9 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.4.1: + resolution: {integrity: sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q==} + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -6146,6 +6275,63 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} + '@serwist/build@9.5.11(browserslist@4.28.2)(typescript@5.9.3)': + dependencies: + '@serwist/utils': 9.5.11(browserslist@4.28.2) + common-tags: 1.8.2 + glob: 13.0.6 + pretty-bytes: 6.1.1 + source-map: 0.8.0-beta.0 + type-fest: 5.6.0 + zod: 4.4.1 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - browserslist + + '@serwist/next@9.5.11(next@16.2.6(@babel/core@7.29.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6)(typescript@5.9.3)': + dependencies: + '@serwist/build': 9.5.11(browserslist@4.28.2)(typescript@5.9.3) + '@serwist/utils': 9.5.11(browserslist@4.28.2) + '@serwist/webpack-plugin': 9.5.11(browserslist@4.28.2)(typescript@5.9.3) + '@serwist/window': 9.5.11(browserslist@4.28.2)(typescript@5.9.3) + browserslist: 4.28.2 + glob: 13.0.6 + kolorist: 1.8.0 + next: 16.2.6(@babel/core@7.29.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + semver: 7.7.4 + serwist: 9.5.11(browserslist@4.28.2)(typescript@5.9.3) + zod: 4.4.1 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - webpack + + '@serwist/utils@9.5.11(browserslist@4.28.2)': + optionalDependencies: + browserslist: 4.28.2 + + '@serwist/webpack-plugin@9.5.11(browserslist@4.28.2)(typescript@5.9.3)': + dependencies: + '@serwist/build': 9.5.11(browserslist@4.28.2)(typescript@5.9.3) + '@serwist/utils': 9.5.11(browserslist@4.28.2) + pretty-bytes: 6.1.1 + zod: 4.4.1 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - browserslist + + '@serwist/window@9.5.11(browserslist@4.28.2)(typescript@5.9.3)': + dependencies: + '@types/trusted-types': 2.0.7 + serwist: 9.5.11(browserslist@4.28.2)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - browserslist + '@sindresorhus/merge-streams@4.0.0': {} '@standard-schema/utils@0.3.0': {} @@ -6302,6 +6488,8 @@ snapshots: '@types/statuses@2.0.6': {} + '@types/trusted-types@2.0.7': {} + '@types/validate-npm-package-name@4.0.2': {} '@vitest/expect@2.1.9': @@ -6603,6 +6791,8 @@ snapshots: commander@14.0.3: {} + common-tags@1.8.2: {} + content-disposition@1.1.0: {} content-type@1.0.5: {} @@ -7094,6 +7284,12 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -7146,6 +7342,8 @@ snapshots: dependencies: safer-buffer: 2.1.2 + idb@8.0.3: {} + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -7245,6 +7443,8 @@ snapshots: kleur@4.1.5: {} + kolorist@1.8.0: {} + lightningcss-android-arm64@1.32.0: optional: true @@ -7300,6 +7500,8 @@ snapshots: dependencies: p-locate: 4.1.0 + lodash.sortby@4.7.0: {} + log-symbols@6.0.0: dependencies: chalk: 5.6.2 @@ -7358,6 +7560,8 @@ snapshots: minimist@1.2.8: {} + minipass@7.1.3: {} + mpg123-decoder@1.0.3: dependencies: '@wasm-audio-decoders/common': 9.0.7 @@ -7566,6 +7770,11 @@ snapshots: path-key@4.0.0: {} + path-scurry@2.0.2: + dependencies: + lru-cache: 11.3.6 + minipass: 7.1.3 + path-to-regexp@6.3.0: {} path-to-regexp@8.4.2: {} @@ -7693,6 +7902,8 @@ snapshots: powershell-utils@0.1.0: {} + pretty-bytes@6.1.1: {} + pretty-ms@9.3.0: dependencies: parse-ms: 4.0.0 @@ -7747,6 +7958,8 @@ snapshots: end-of-stream: 1.4.5 once: 1.4.0 + punycode@2.3.1: {} + qified@0.10.1: dependencies: hookified: 2.2.0 @@ -7980,6 +8193,8 @@ snapshots: semver@6.3.1: {} + semver@7.7.4: {} + semver@7.8.0: {} send@1.2.1: @@ -8014,6 +8229,15 @@ snapshots: server-only@0.0.1: {} + serwist@9.5.11(browserslist@4.28.2)(typescript@5.9.3): + dependencies: + '@serwist/utils': 9.5.11(browserslist@4.28.2) + idb: 8.0.3 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - browserslist + set-blocking@2.0.0: {} set-cookie-parser@3.1.0: {} @@ -8163,6 +8387,10 @@ snapshots: source-map@0.6.1: {} + source-map@0.8.0-beta.0: + dependencies: + whatwg-url: 7.1.0 + split2@4.2.0: {} stackback@0.0.2: {} @@ -8277,6 +8505,10 @@ snapshots: dependencies: tldts: 7.0.30 + tr46@1.0.1: + dependencies: + punycode: 2.3.1 + ts-morph@26.0.0: dependencies: '@ts-morph/common': 0.27.0 @@ -8429,8 +8661,16 @@ snapshots: web-streams-polyfill@3.3.3: {} + webidl-conversions@4.0.2: {} + whatsapp-rust-bridge@0.5.3: {} + whatwg-url@7.1.0: + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + which-module@2.0.1: {} which@2.0.2: @@ -8519,3 +8759,5 @@ snapshots: zod: 3.25.76 zod@3.25.76: {} + + zod@4.4.1: {}