fix(dev): prevent production service workers from controlling local builds
This commit is contained in:
parent
c3566a1f02
commit
4c4106fe8b
4 changed files with 69 additions and 4 deletions
|
|
@ -34,6 +34,9 @@ const nextConfig = {
|
|||
};
|
||||
|
||||
const withPWA = require("@ducanh2912/next-pwa").default({
|
||||
// Development and production share public/sw.js. Never let a build-generated
|
||||
// worker control HMR assets or rewrite the worker during local recompilation.
|
||||
disable: process.env.NODE_ENV === "development",
|
||||
dest: "public",
|
||||
cacheOnFrontEndNav: false,
|
||||
aggressiveFrontEndNavCaching: false,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,20 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import {configurePwa} from "@/lib/pwa-registration";
|
||||
|
||||
export function PwaRegister() {
|
||||
const started = React.useRef(false);
|
||||
React.useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
if (typeof window === "undefined" || started.current) return;
|
||||
started.current = true;
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker
|
||||
.register("/sw.js", { scope: "/" })
|
||||
.catch((err) => console.warn("SW registration failed:", err));
|
||||
void configurePwa({
|
||||
production: process.env.NODE_ENV === "production",
|
||||
serviceWorker: navigator.serviceWorker,
|
||||
origin: window.location.origin,
|
||||
reload: () => window.location.reload(),
|
||||
}).catch((err) => console.warn("SW setup failed:", err));
|
||||
}
|
||||
}, []);
|
||||
return null;
|
||||
|
|
|
|||
35
src/lib/pwa-registration.test.ts
Normal file
35
src/lib/pwa-registration.test.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import {it,expect,vi} from 'vitest';
|
||||
import {configurePwa} from './pwa-registration';
|
||||
const origin='http://localhost:6004';
|
||||
function registration(script='/sw.js',scope=origin+'/'){
|
||||
return {scope,active:{scriptURL:new URL(script,origin).href},unregister:vi.fn(async()=>true)};
|
||||
}
|
||||
function setup(registrations:ReturnType<typeof registration>[]=[],controlled=true){
|
||||
const serviceWorker={register:vi.fn(async()=>({})),getRegistrations:vi.fn(async()=>registrations),controller:controlled?{scriptURL:origin+'/sw.js'}:null};
|
||||
return {serviceWorker,reload:vi.fn(),origin};
|
||||
}
|
||||
it('registers the offline worker in production without removing registrations or reloading',async()=>{
|
||||
const input=setup([registration()]);
|
||||
await configurePwa({...input,production:true} as never);
|
||||
expect(input.serviceWorker.register).toHaveBeenCalledWith('/sw.js',{scope:'/'});
|
||||
expect(input.serviceWorker.getRegistrations).not.toHaveBeenCalled();expect(input.reload).not.toHaveBeenCalled();
|
||||
});
|
||||
it('removes a controlling app worker in development and reloads to release the old cache',async()=>{
|
||||
const own=registration();const input=setup([own]);
|
||||
await configurePwa({...input,production:false} as never);
|
||||
expect(own.unregister).toHaveBeenCalledTimes(1);expect(input.reload).toHaveBeenCalledTimes(1);
|
||||
expect(input.serviceWorker.register).not.toHaveBeenCalled();
|
||||
});
|
||||
it('does not reload a clean development browser or an uncontrolled page',async()=>{
|
||||
for(const input of [setup([]),setup([registration()],false)]){
|
||||
await configurePwa({...input,production:false} as never);
|
||||
expect(input.reload).not.toHaveBeenCalled();expect(input.serviceWorker.register).not.toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
it('preserves workers from another scope, script or origin',async()=>{
|
||||
const registrations=[registration('/other-worker.js'),registration('/sw.js',origin+'/another-app/'),registration('http://localhost:4001/sw.js','http://localhost:4001/')];
|
||||
const input=setup(registrations);
|
||||
await configurePwa({...input,production:false} as never);
|
||||
for(const reg of registrations)expect(reg.unregister).not.toHaveBeenCalled();
|
||||
expect(input.reload).not.toHaveBeenCalled();
|
||||
});
|
||||
21
src/lib/pwa-registration.ts
Normal file
21
src/lib/pwa-registration.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
type PwaEnvironment={
|
||||
production:boolean;
|
||||
serviceWorker:Pick<ServiceWorkerContainer,'register'|'getRegistrations'|'controller'>;
|
||||
origin:string;
|
||||
reload:()=>void;
|
||||
};
|
||||
/** Development assets must not be controlled by a worker generated by a production build. */
|
||||
export async function configurePwa({production,serviceWorker,origin,reload}:PwaEnvironment){
|
||||
if(production){await serviceWorker.register('/sw.js',{scope:'/'});return;}
|
||||
const workerUrl=new URL('/sw.js',origin).href;
|
||||
const registrations=await serviceWorker.getRegistrations();
|
||||
let removed=false;
|
||||
for(const registration of registrations){
|
||||
const worker=registration.active??registration.waiting??registration.installing;
|
||||
if(registration.scope!==new URL('/',origin).href||worker?.scriptURL!==workerUrl)continue;
|
||||
removed=await registration.unregister()||removed;
|
||||
}
|
||||
// Unregistering takes effect on the next navigation. Reload only when our
|
||||
// worker actually controlled this page, so clean dev browsers never loop.
|
||||
if(removed&&serviceWorker.controller?.scriptURL===workerUrl)reload();
|
||||
}
|
||||
Loading…
Reference in a new issue