Skip to content

Commit c9ab979

Browse files
authored
Merge pull request #23 from wafflestudio/4-feature-msw-setting
msw 기본 설정
2 parents 43ea18e + f02241d commit c9ab979

File tree

5 files changed

+395
-5
lines changed

5 files changed

+395
-5
lines changed

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,5 +81,10 @@
8181
"typescript-eslint": "^8.46.4",
8282
"vite": "^7.2.4",
8383
"vitest": "^4.0.16"
84+
},
85+
"msw": {
86+
"workerDirectory": [
87+
"public"
88+
]
8489
}
8590
}

public/mockServiceWorker.js

Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
2+
/* tslint:disable */
3+
4+
/**
5+
* Mock Service Worker.
6+
* @see https://github.com/mswjs/msw
7+
* - Please do NOT modify this file.
8+
*/
9+
10+
const PACKAGE_VERSION = '2.12.7'
11+
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
12+
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
13+
const activeClientIds = new Set()
14+
15+
addEventListener('install', function () {
16+
self.skipWaiting()
17+
})
18+
19+
addEventListener('activate', function (event) {
20+
event.waitUntil(self.clients.claim())
21+
})
22+
23+
addEventListener('message', async function (event) {
24+
const clientId = Reflect.get(event.source || {}, 'id')
25+
26+
if (!clientId || !self.clients) {
27+
return
28+
}
29+
30+
const client = await self.clients.get(clientId)
31+
32+
if (!client) {
33+
return
34+
}
35+
36+
const allClients = await self.clients.matchAll({
37+
type: 'window',
38+
})
39+
40+
switch (event.data) {
41+
case 'KEEPALIVE_REQUEST': {
42+
sendToClient(client, {
43+
type: 'KEEPALIVE_RESPONSE',
44+
})
45+
break
46+
}
47+
48+
case 'INTEGRITY_CHECK_REQUEST': {
49+
sendToClient(client, {
50+
type: 'INTEGRITY_CHECK_RESPONSE',
51+
payload: {
52+
packageVersion: PACKAGE_VERSION,
53+
checksum: INTEGRITY_CHECKSUM,
54+
},
55+
})
56+
break
57+
}
58+
59+
case 'MOCK_ACTIVATE': {
60+
activeClientIds.add(clientId)
61+
62+
sendToClient(client, {
63+
type: 'MOCKING_ENABLED',
64+
payload: {
65+
client: {
66+
id: client.id,
67+
frameType: client.frameType,
68+
},
69+
},
70+
})
71+
break
72+
}
73+
74+
case 'CLIENT_CLOSED': {
75+
activeClientIds.delete(clientId)
76+
77+
const remainingClients = allClients.filter((client) => {
78+
return client.id !== clientId
79+
})
80+
81+
// Unregister itself when there are no more clients
82+
if (remainingClients.length === 0) {
83+
self.registration.unregister()
84+
}
85+
86+
break
87+
}
88+
}
89+
})
90+
91+
addEventListener('fetch', function (event) {
92+
const requestInterceptedAt = Date.now()
93+
94+
// Bypass navigation requests.
95+
if (event.request.mode === 'navigate') {
96+
return
97+
}
98+
99+
// Opening the DevTools triggers the "only-if-cached" request
100+
// that cannot be handled by the worker. Bypass such requests.
101+
if (
102+
event.request.cache === 'only-if-cached' &&
103+
event.request.mode !== 'same-origin'
104+
) {
105+
return
106+
}
107+
108+
// Bypass all requests when there are no active clients.
109+
// Prevents the self-unregistered worked from handling requests
110+
// after it's been terminated (still remains active until the next reload).
111+
if (activeClientIds.size === 0) {
112+
return
113+
}
114+
115+
const requestId = crypto.randomUUID()
116+
event.respondWith(handleRequest(event, requestId, requestInterceptedAt))
117+
})
118+
119+
/**
120+
* @param {FetchEvent} event
121+
* @param {string} requestId
122+
* @param {number} requestInterceptedAt
123+
*/
124+
async function handleRequest(event, requestId, requestInterceptedAt) {
125+
const client = await resolveMainClient(event)
126+
const requestCloneForEvents = event.request.clone()
127+
const response = await getResponse(
128+
event,
129+
client,
130+
requestId,
131+
requestInterceptedAt
132+
)
133+
134+
// Send back the response clone for the "response:*" life-cycle events.
135+
// Ensure MSW is active and ready to handle the message, otherwise
136+
// this message will pend indefinitely.
137+
if (client && activeClientIds.has(client.id)) {
138+
const serializedRequest = await serializeRequest(requestCloneForEvents)
139+
140+
// Clone the response so both the client and the library could consume it.
141+
const responseClone = response.clone()
142+
143+
sendToClient(
144+
client,
145+
{
146+
type: 'RESPONSE',
147+
payload: {
148+
isMockedResponse: IS_MOCKED_RESPONSE in response,
149+
request: {
150+
id: requestId,
151+
...serializedRequest,
152+
},
153+
response: {
154+
type: responseClone.type,
155+
status: responseClone.status,
156+
statusText: responseClone.statusText,
157+
headers: Object.fromEntries(responseClone.headers.entries()),
158+
body: responseClone.body,
159+
},
160+
},
161+
},
162+
responseClone.body ? [serializedRequest.body, responseClone.body] : []
163+
)
164+
}
165+
166+
return response
167+
}
168+
169+
/**
170+
* Resolve the main client for the given event.
171+
* Client that issues a request doesn't necessarily equal the client
172+
* that registered the worker. It's with the latter the worker should
173+
* communicate with during the response resolving phase.
174+
* @param {FetchEvent} event
175+
* @returns {Promise<Client | undefined>}
176+
*/
177+
async function resolveMainClient(event) {
178+
const client = await self.clients.get(event.clientId)
179+
180+
if (activeClientIds.has(event.clientId)) {
181+
return client
182+
}
183+
184+
if (client?.frameType === 'top-level') {
185+
return client
186+
}
187+
188+
const allClients = await self.clients.matchAll({
189+
type: 'window',
190+
})
191+
192+
return allClients
193+
.filter((client) => {
194+
// Get only those clients that are currently visible.
195+
return client.visibilityState === 'visible'
196+
})
197+
.find((client) => {
198+
// Find the client ID that's recorded in the
199+
// set of clients that have registered the worker.
200+
return activeClientIds.has(client.id)
201+
})
202+
}
203+
204+
/**
205+
* @param {FetchEvent} event
206+
* @param {Client | undefined} client
207+
* @param {string} requestId
208+
* @param {number} requestInterceptedAt
209+
* @returns {Promise<Response>}
210+
*/
211+
async function getResponse(event, client, requestId, requestInterceptedAt) {
212+
// Clone the request because it might've been already used
213+
// (i.e. its body has been read and sent to the client).
214+
const requestClone = event.request.clone()
215+
216+
function passthrough() {
217+
// Cast the request headers to a new Headers instance
218+
// so the headers can be manipulated with.
219+
const headers = new Headers(requestClone.headers)
220+
221+
// Remove the "accept" header value that marked this request as passthrough.
222+
// This prevents request alteration and also keeps it compliant with the
223+
// user-defined CORS policies.
224+
const acceptHeader = headers.get('accept')
225+
if (acceptHeader) {
226+
const values = acceptHeader.split(',').map((value) => value.trim())
227+
const filteredValues = values.filter(
228+
(value) => value !== 'msw/passthrough'
229+
)
230+
231+
if (filteredValues.length > 0) {
232+
headers.set('accept', filteredValues.join(', '))
233+
} else {
234+
headers.delete('accept')
235+
}
236+
}
237+
238+
return fetch(requestClone, { headers })
239+
}
240+
241+
// Bypass mocking when the client is not active.
242+
if (!client) {
243+
return passthrough()
244+
}
245+
246+
// Bypass initial page load requests (i.e. static assets).
247+
// The absence of the immediate/parent client in the map of the active clients
248+
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
249+
// and is not ready to handle requests.
250+
if (!activeClientIds.has(client.id)) {
251+
return passthrough()
252+
}
253+
254+
// Notify the client that a request has been intercepted.
255+
const serializedRequest = await serializeRequest(event.request)
256+
const clientMessage = await sendToClient(
257+
client,
258+
{
259+
type: 'REQUEST',
260+
payload: {
261+
id: requestId,
262+
interceptedAt: requestInterceptedAt,
263+
...serializedRequest,
264+
},
265+
},
266+
[serializedRequest.body]
267+
)
268+
269+
switch (clientMessage.type) {
270+
case 'MOCK_RESPONSE': {
271+
return respondWithMock(clientMessage.data)
272+
}
273+
274+
case 'PASSTHROUGH': {
275+
return passthrough()
276+
}
277+
}
278+
279+
return passthrough()
280+
}
281+
282+
/**
283+
* @param {Client} client
284+
* @param {any} message
285+
* @param {Array<Transferable>} transferrables
286+
* @returns {Promise<any>}
287+
*/
288+
function sendToClient(client, message, transferrables = []) {
289+
return new Promise((resolve, reject) => {
290+
const channel = new MessageChannel()
291+
292+
channel.port1.onmessage = (event) => {
293+
if (event.data && event.data.error) {
294+
return reject(event.data.error)
295+
}
296+
297+
resolve(event.data)
298+
}
299+
300+
client.postMessage(message, [
301+
channel.port2,
302+
...transferrables.filter(Boolean),
303+
])
304+
})
305+
}
306+
307+
/**
308+
* @param {Response} response
309+
* @returns {Response}
310+
*/
311+
function respondWithMock(response) {
312+
// Setting response status code to 0 is a no-op.
313+
// However, when responding with a "Response.error()", the produced Response
314+
// instance will have status code set to 0. Since it's not possible to create
315+
// a Response instance with status code 0, handle that use-case separately.
316+
if (response.status === 0) {
317+
return Response.error()
318+
}
319+
320+
const mockedResponse = new Response(response.body, response)
321+
322+
Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
323+
value: true,
324+
enumerable: true,
325+
})
326+
327+
return mockedResponse
328+
}
329+
330+
/**
331+
* @param {Request} request
332+
*/
333+
async function serializeRequest(request) {
334+
return {
335+
url: request.url,
336+
mode: request.mode,
337+
method: request.method,
338+
headers: Object.fromEntries(request.headers.entries()),
339+
cache: request.cache,
340+
credentials: request.credentials,
341+
destination: request.destination,
342+
integrity: request.integrity,
343+
redirect: request.redirect,
344+
referrer: request.referrer,
345+
referrerPolicy: request.referrerPolicy,
346+
body: await request.arrayBuffer(),
347+
keepalive: request.keepalive,
348+
}
349+
}

src/main.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,19 @@ import { RouterProvider } from '@tanstack/react-router'
44
import { router } from './router'
55
import './index.css'
66

7-
createRoot(document.getElementById('root')!).render(
8-
<StrictMode>
9-
<RouterProvider router={router} />
10-
</StrictMode>
11-
)
7+
async function enableMocking() {
8+
if (import.meta.env.DEV) {
9+
const { worker } = await import('./mocks/browser')
10+
return worker.start({
11+
onUnhandledRequest: 'bypass',
12+
})
13+
}
14+
}
15+
16+
enableMocking().then(() => {
17+
createRoot(document.getElementById('root')!).render(
18+
<StrictMode>
19+
<RouterProvider router={router} />
20+
</StrictMode>
21+
)
22+
})

src/mocks/browser.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { setupWorker } from 'msw/browser'
2+
import { handlers } from './handlers'
3+
4+
export const worker = setupWorker(...handlers)

0 commit comments

Comments
 (0)