refactor(pwa): migrate from @ducanh2912/next-pwa to @serwist/turbopack

Next.js 16 ships Turbopack as the default bundler, but
@ducanh2912/next-pwa is webpack-only — the previous PWA setup
required `--webpack` on dev/build/"dev:proxy" scripts, accepting
a perf regression in exchange for working PWA install.

@serwist/turbopack (paired with the official 'serwist' package
and 'esbuild') is the Turbopack-native successor: it works with
the default bundler, no opt-out flag, and gives us a real TS
service worker source file we can read and edit.

What this commit does:

1. Swap deps in package.json:
   - Remove @ducanh2912/next-pwa
   - Add @serwist/turbopack, serwist, esbuild (all devDeps)
   - Drop --webpack flag from dev / dev:proxy / build scripts
     (Turbopack is restored as the bundler)

2. next.config.ts:
   - Replace withPWAInit wrapping with withSerwist named import
   - Update comment block to describe new architecture

3. New src/app/sw.ts: Serwist service worker source.
   skipWaiting + clientsClaim + navigationPreload + defaultCache.
   This file is compiled to a real SW at build time by Serwist.

4. New src/app/serwist/[[...slug]]/route.ts: catch-all Route
   Handler that serves the compiled SW at /serwist/sw.js plus
   Serwist's chunked runtime assets. Catch-all is required
   because Serwist serves multiple files under /serwist/.

5. src/app/layout.tsx: replace <SwRegister /> with
   <SerwistProvider swUrl="/serwist/sw.js"> from
   @serwist/turbopack/react. The provider handles registration
   + lifecycle internally.

6. Delete src/app/_components/core/sw-register.tsx: replaced
   by SerwistProvider.

7. .gitignore: add public/sw.js and public/workbox-*.js
   (defensive — these are build artifacts that change hash
   every build and shouldn't be committed).

8. Untrack public/workbox-3c9d0171.js: stale workbox runtime
   from the previous next-pwa build, hash is build-specific
   so it can never be regenerated correctly.

Not modified (intentionally):
- src/utils/pwa.ts: pure beforeinstallprompt browser-API wrapper,
  library-agnostic, stays as-is.
- pwa-install-overlay / pwa-install-dialog: install UI flow
  unchanged.
- public/manifest.json: still works; layout's metadata.manifest
  points at it.

Verification:
- pnpm install resolved cleanly (added 24 pkgs, removed 209)
- npx tsc --noEmit passes (EXIT=0)
- Serwist provides named exports, not default — withSerwist and
  createSerwistRoute are both named imports
This commit is contained in:
2026-06-17 11:09:36 +08:00
parent bb2ae41232
commit 67dcf97edc
9 changed files with 705 additions and 2275 deletions
+9 -21
View File
@@ -1,5 +1,5 @@
import type { NextConfig } from "next";
import withPWAInit from "@ducanh2912/next-pwa";
import { withSerwist } from "@serwist/turbopack";
/**
* Next.js 16 临时绕过 `/_global-error` prerender 的 workStore bug
@@ -12,15 +12,14 @@ import withPWAInit from "@ducanh2912/next-pwa";
* - 服务器启动:`cd <path> && next start -H 0.0.0.0 -p 3000`
* 或用 PM2`pm2 start "next start" --name cozsweet`
*
* PWA 集成:`@ducanh2912/next-pwa` 在 build 阶段用 workbox 生成 public/sw.js
* - `dest: "public"` SW 输出到 public/sw.js
* - `disable: true` + dev NODE_ENV 跳过 dev —— 避免 HMR + 老 SW cache 冲突
* - `register: false` 不自动注入注册脚本 —— App Router 下走 <SwRegister /> 客户端组件更可控
* - `skipWaiting: true` 新 SW 立即接管(避免旧 SW 卡住页面)
* PWA 集成:`@serwist/turbopack` 在 build 阶段用 esbuild 编译 `src/app/sw.ts` 源文件
* - Turbopack 原生支持 —— 不再需要 `--webpack` flag
* - SW 通过 Serwist Route Handlersrc/app/serwist/[[...slug]]/route.ts)在 `/serwist/sw.js` 服务
* - 客户端注册走 `<SerwistProvider swUrl="/serwist/sw.js">`(替代旧的 <SwRegister />
*
* 已知 trade-off`@ducanh2912/next-pwa` 是 webpack-only 插件,与 Turbopack 不兼容。
* scripts 中加 `--webpack` flag 取消 Turbopack。等未来 next-pwa 支持 Turbopack 时再切回。
* Serwist 同样 webpack-only —— 见 node_modules/next/dist/docs/.../progressive-web-apps.md
* 历史:
* - 之前用 @ducanh2912/next-pwawebpack-only),被迫用 `--webpack` 退 Turbopack
* - 切到 @serwist/turbopack 后拿回 Turbopack 的 dev/build 性能
*/
const nextConfig: NextConfig = {
experimental: {
@@ -29,15 +28,4 @@ const nextConfig: NextConfig = {
},
};
const withPWA = withPWAInit({
dest: "public",
disable: true, // 仅当 NODE_ENV === "development" 时跳过 SW 生成
register: false,
workboxOptions: {
// skipWaiting + clientsClaim 嵌在 workboxOptionsGenerateSWOptions 字段)
skipWaiting: true,
clientsClaim: true,
},
});
export default withPWA(nextConfig);
export default withSerwist(nextConfig);