Skip to content

Commit

Permalink
Custom service worker with msw integration (#104)
Browse files Browse the repository at this point in the history
* Use custom serviceWorker in vite pwa plugin

* Merge MSW into custom service worker

* Fixed msw on dev environment

* Updated packages

* Wait for service worker to register before loading app

* Fixed UI blocking if not using msw

* Moved app loader to shared
  • Loading branch information
pkirilin authored May 14, 2024
1 parent ac813ca commit 8780a1b
Show file tree
Hide file tree
Showing 97 changed files with 1,584 additions and 933 deletions.
1,645 changes: 970 additions & 675 deletions src/frontend/.pnp.cjs

Large diffs are not rendered by default.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
9 changes: 5 additions & 4 deletions src/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"react-router-dom": "^6.22.0",
"redux": "^4.2.0",
"use-debounce": "^10.0.0",
"workbox-window": "^7.0.0"
"workbox-window": "^7.1.0"
},
"devDependencies": {
"@mswjs/data": "^0.16.1",
Expand Down Expand Up @@ -64,13 +64,14 @@
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-testing-library": "^6.2.0",
"jsdom": "^22.1.0",
"msw": "^2.0.9",
"msw": "^2.3.0",
"prettier": "^3.1.0",
"typescript": "5.2.2",
"vite": "^5.0.3",
"vite-plugin-pwa": "^0.19.2",
"vite-plugin-pwa": "^0.20.0",
"vitest": "^0.34.6",
"vitest-preview": "^0.0.1"
"vitest-preview": "^0.0.1",
"workbox-precaching": "^7.1.0"
},
"browserslist": {
"production": [
Expand Down
28 changes: 10 additions & 18 deletions src/frontend/public/mockServiceWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
/* tslint:disable */

/**
* Mock Service Worker (2.0.9).
* Mock Service Worker.
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
* - Please do NOT serve this file on production.
*/

const INTEGRITY_CHECKSUM = '0877fcdc026242810f5bfde0d7178db4'
const PACKAGE_VERSION = '2.3.0'
const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()

Expand Down Expand Up @@ -48,7 +49,10 @@ self.addEventListener('message', async function (event) {
case 'INTEGRITY_CHECK_REQUEST': {
sendToClient(client, {
type: 'INTEGRITY_CHECK_RESPONSE',
payload: INTEGRITY_CHECKSUM,
payload: {
packageVersion: PACKAGE_VERSION,
checksum: INTEGRITY_CHECKSUM,
},
})
break
}
Expand Down Expand Up @@ -121,11 +125,6 @@ async function handleRequest(event, requestId) {
if (client && activeClientIds.has(client.id)) {
;(async function () {
const responseClone = response.clone()
// When performing original requests, response body will
// always be a ReadableStream, even for 204 responses.
// But when creating a new Response instance on the client,
// the body for a 204 response must be null.
const responseBody = response.status === 204 ? null : responseClone.body

sendToClient(
client,
Expand All @@ -137,11 +136,11 @@ async function handleRequest(event, requestId) {
type: responseClone.type,
status: responseClone.status,
statusText: responseClone.statusText,
body: responseBody,
body: responseClone.body,
headers: Object.fromEntries(responseClone.headers.entries()),
},
},
[responseBody],
[responseClone.body],
)
})()
}
Expand Down Expand Up @@ -207,13 +206,6 @@ async function getResponse(event, client, requestId) {
return passthrough()
}

// Bypass requests with the explicit bypass header.
// Such requests can be issued by "ctx.fetch()".
const mswIntention = request.headers.get('x-msw-intention')
if (['bypass', 'passthrough'].includes(mswIntention)) {
return passthrough()
}

// Notify the client that a request has been intercepted.
const requestBuffer = await request.arrayBuffer()
const clientMessage = await sendToClient(
Expand Down Expand Up @@ -245,7 +237,7 @@ async function getResponse(event, client, requestId) {
return respondWithMock(clientMessage.data)
}

case 'MOCK_NOT_FOUND': {
case 'PASSTHROUGH': {
return passthrough()
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/frontend/src/app/Root/Root.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { type PropsWithChildren, type FC } from 'react';
import { RouterProvider } from 'react-router-dom';
import { AppLoader } from '@/shared/ui';
import { createAppRouter } from '../router';
import { RootLoader } from './RootLoader';

export const Root: FC<PropsWithChildren> = ({ children }) => (
<RouterProvider router={createAppRouter(children)} fallbackElement={<RootLoader />} />
<RouterProvider router={createAppRouter(children)} fallbackElement={<AppLoader />} />
);
6 changes: 3 additions & 3 deletions src/frontend/src/app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import 'date-fns';
import { initBrowserMockApi } from 'tests/mockApi';
import { createRoot } from 'react-dom/client';
import { GOOGLE_ANALYTICS_ENABLED, MSW_ENABLED } from '@/shared/config';
import { initGoogleAnalytics } from './googleAnalytics';
import { Root } from './Root';
import { RootProvider } from './RootProvider';
import { store } from './store';

void (async () => {
if (MSW_ENABLED) {
(async () => {
if (!import.meta.env.PROD && MSW_ENABLED) {
const { initBrowserMockApi } = await import('@tests/mockApi');
await initBrowserMockApi();
}

Expand Down
18 changes: 18 additions & 0 deletions src/frontend/src/app/serviceWorker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { cleanupOutdatedCaches, precacheAndRoute } from 'workbox-precaching';
import { MSW_ENABLED } from '@/shared/config';

declare let self: ServiceWorkerGlobalScope;

if (import.meta.env.PROD && MSW_ENABLED) {
importScripts('/mockServiceWorker.js');
}

self.addEventListener('message', event => {
if (event.data?.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});

cleanupOutdatedCaches();

precacheAndRoute(self.__WB_MANIFEST);
22 changes: 22 additions & 0 deletions src/frontend/src/features/updateApp/model.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import {
createContext,
useContext,
useMemo,
useState,
} from 'react';
import { MSW_ENABLED } from '@/shared/config';
import { AppLoader } from '@/shared/ui';

export interface State {
updateAvailable: boolean;
Expand All @@ -22,10 +25,25 @@ export const Context = createContext<State>({

export const useUpdateApp = (): State => useContext(Context);

const SHOULD_WAIT_FOR_MSW = import.meta.env.PROD && MSW_ENABLED;

export const Provider: FC<PropsWithChildren> = ({ children }) => {
const [appReadyToStart, setAppReadyToStart] = useState(!SHOULD_WAIT_FOR_MSW);

const registerSW = useRegisterSW({
onRegisteredSW: async (_, registration) => {
await registration?.update();

if (SHOULD_WAIT_FOR_MSW) {
const { initBrowserMockApi } = await import('@tests/mockApi');
await initBrowserMockApi();
}

setAppReadyToStart(true);
},
onRegisterError: error => {
// eslint-disable-next-line no-console
console.error('Service worker registration error: ', error);
},
});

Expand All @@ -49,5 +67,9 @@ export const Provider: FC<PropsWithChildren> = ({ children }) => {
[close, updateAvailable, reload],
);

if (!appReadyToStart) {
return <AppLoader />;
}

return <Context.Provider value={state}>{children}</Context.Provider>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { type FC } from 'react';
import { AppName } from '@/shared/ui';
import { CenteredLayout } from '@/widgets/layout';

export const RootLoader: FC = () => (
export const AppLoader: FC = () => (
<Grow in>
<Box>
<CenteredLayout>
Expand Down
1 change: 1 addition & 0 deletions src/frontend/src/shared/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './AppFab';
export * from './AppName';
export * from './AppSelect';
export * from './AppShell';
export * from './AppLoader';
export * from './Button';
export * from './DatePicker';
export * from './Dialog';
Expand Down
4 changes: 4 additions & 0 deletions src/frontend/tests/mockApi/initBrowserMockApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export const initBrowserMockApi = async (): Promise<void> => {
await initMockApiDb();

await worker.start({
serviceWorker: {
url: import.meta.env.PROD ? '/serviceWorker.js' : '/mockServiceWorker.js',
},

onUnhandledRequest: (request, print) => {
if (IGNORED_URL_PATTERNS.some(regexp => regexp.test(request.url))) {
return;
Expand Down
3 changes: 2 additions & 1 deletion src/frontend/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"lib": [
"dom",
"dom.iterable",
"esnext"
"esnext",
"WebWorker"
],
"allowJs": true,
"skipLibCheck": true,
Expand Down
3 changes: 3 additions & 0 deletions src/frontend/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export default defineConfig({
plugins: [
react(),
pwa({
strategies: 'injectManifest',
srcDir: 'src/app',
filename: 'serviceWorker.ts',
manifest: {
name: 'Food Diary',
short_name: 'Food Diary',
Expand Down
Loading

0 comments on commit 8780a1b

Please sign in to comment.