diff --git a/.husky/prepare-commit-msg b/.husky/prepare-commit-msg index 73a4cdc..5c18e4b 100755 --- a/.husky/prepare-commit-msg +++ b/.husky/prepare-commit-msg @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -exec < /dev/tty && git cz --hook || true +exec < /dev/tty && node_modules/.bin/cz --hook || true diff --git a/README.md b/README.md index 38439c0..c1c1fc5 100644 --- a/README.md +++ b/README.md @@ -137,10 +137,7 @@ yarn add -D pinia @pinia/nuxt ```ts export default defineNuxtConfig({ - buildModules: [ - // https://composition-api.nuxtjs.org/getting-started/setup#quick-start - '@pinia/nuxt', - ], + modules: ['@pinia/nuxt'], }) ``` @@ -203,7 +200,7 @@ yarn add -D @vueuse/nuxt ```ts export default defineNuxtConfig({ - buildModules: ['@vueuse/nuxt'], + modules: ['@vueuse/nuxt', '@pinia/nuxt'], vueuse: { ssrHandlers: true, }, diff --git a/nuxt.config.ts b/nuxt.config.ts index aef4e1b..37deee2 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -4,15 +4,13 @@ import { defineNuxtConfig } from 'nuxt' export default defineNuxtConfig({ app: { head: { - link: [ - { rel: 'icon', type: 'image/x-icon', href: '/public/favicon.ico' }, - ], + link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }], }, }, typescript: { strict: true, }, - buildModules: ['@pinia/nuxt', '@vueuse/nuxt'], + modules: ['@vueuse/nuxt', '@pinia/nuxt'], vueuse: { ssrHandlers: true, }, diff --git a/package.json b/package.json index afa4842..f0bc568 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,8 @@ "devDependencies": { "@nuxt/test-utils": "^3.0.0-rc.3", "@nuxtjs/eslint-config-typescript": "^9.0.0", - "@pinia/nuxt": "^0.1.8", - "@vueuse/nuxt": "^8.3.1", + "@pinia/nuxt": "^0.4.1", + "@vueuse/nuxt": "^9.1.0", "axios": "^0.27.2", "cz-conventional-changelog": "3.3.0", "eslint": "^8.14.0", @@ -33,15 +33,15 @@ "eslint-plugin-prettier": "^4.0.0", "husky": "^7.0.0", "lint-staged": "^12.4.1", - "msw": "^0.39.2", - "nuxt": "3.0.0-rc.1", - "pinia": "^2.0.13", + "msw": "^0.45.0", + "nuxt": "3.0.0-rc.8", + "pinia": "^2.0.20", "prettier": "^2.6.2", - "prettier-plugin-tailwindcss": "^0.1.10", - "tailwindcss": "^3.0.24", - "testcafe": "^1.18.6", + "prettier-plugin-tailwindcss": "^0.1.13", + "tailwindcss": "^3.1.8", + "testcafe": "^1.20.1", "typescript": "^4.6.4", - "vitest": "^0.12.4" + "vitest": "^0.22.1" }, "config": { "commitizen": { diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js index 0966a9d..8afaaf7 100644 --- a/public/mockServiceWorker.js +++ b/public/mockServiceWorker.js @@ -2,22 +2,21 @@ /* tslint:disable */ /** - * Mock Service Worker (0.39.2). + * Mock Service Worker (0.45.0). * @see https://github.com/mswjs/msw * - Please do NOT modify this file. * - Please do NOT serve this file on production. */ -const INTEGRITY_CHECKSUM = '02f4ad4a2797f85668baf196e553d929' -const bypassHeaderName = 'x-msw-bypass' +const INTEGRITY_CHECKSUM = 'b3066ef78c2f9090b4ce87e874965995' const activeClientIds = new Set() self.addEventListener('install', function () { - return self.skipWaiting() + self.skipWaiting() }) -self.addEventListener('activate', async function (event) { - return self.clients.claim() +self.addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) }) self.addEventListener('message', async function (event) { @@ -33,7 +32,9 @@ self.addEventListener('message', async function (event) { return } - const allClients = await self.clients.matchAll() + const allClients = await self.clients.matchAll({ + type: 'window', + }) switch (event.data) { case 'KEEPALIVE_REQUEST': { @@ -83,30 +84,58 @@ self.addEventListener('message', async function (event) { } }) -// Resolve the "main" client for the given event. -// Client that issues a request doesn't necessarily equal the client -// that registered the worker. It's with the latter the worker should -// communicate with during the response resolving phase. -async function resolveMainClient(event) { - const client = await self.clients.get(event.clientId) +self.addEventListener('fetch', function (event) { + const { request } = event + const accept = request.headers.get('accept') || '' - if (client.frameType === 'top-level') { - return client + // Bypass server-sent events. + if (accept.includes('text/event-stream')) { + return } - const allClients = await self.clients.matchAll() + // Bypass navigation requests. + if (request.mode === 'navigate') { + return + } - return allClients - .filter((client) => { - // Get only those clients that are currently visible. - return client.visibilityState === 'visible' - }) - .find((client) => { - // Find the client ID that's recorded in the - // set of clients that have registered the worker. - return activeClientIds.has(client.id) - }) -} + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + // Generate unique request ID. + const requestId = Math.random().toString(16).slice(2) + + event.respondWith( + handleRequest(event, requestId).catch((error) => { + if (error.name === 'NetworkError') { + console.warn( + '[MSW] Successfully emulated a network error for the "%s %s" request.', + request.method, + request.url, + ) + return + } + + // At this point, any exception indicates an issue with the original request/response. + console.error( + `\ +[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`, + request.method, + request.url, + `${error.name}: ${error.message}`, + ) + }), + ) +}) async function handleRequest(event, requestId) { const client = await resolveMainClient(event) @@ -128,7 +157,7 @@ async function handleRequest(event, requestId) { statusText: clonedResponse.statusText, body: clonedResponse.body === null ? null : await clonedResponse.text(), - headers: serializeHeaders(clonedResponse.headers), + headers: Object.fromEntries(clonedResponse.headers.entries()), redirected: clonedResponse.redirected, }, }) @@ -138,14 +167,54 @@ async function handleRequest(event, requestId) { return response } +// Resolve the main client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (client.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + async function getResponse(event, client, requestId) { const { request } = event - const requestClone = request.clone() - const getOriginalResponse = () => fetch(requestClone) + const clonedRequest = request.clone() + + function passthrough() { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the cilent). + const headers = Object.fromEntries(clonedRequest.headers.entries()) - // Bypass mocking when the request client is not active. + // Remove MSW-specific request headers so the bypassed requests + // comply with the server's CORS preflight check. + // Operate with the headers as an object because request "Headers" + // are immutable. + delete headers['x-msw-bypass'] + + return fetch(clonedRequest, { headers }) + } + + // Bypass mocking when the client is not active. if (!client) { - return getOriginalResponse() + return passthrough() } // Bypass initial page load requests (i.e. static assets). @@ -153,34 +222,23 @@ async function getResponse(event, client, requestId) { // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet // and is not ready to handle requests. if (!activeClientIds.has(client.id)) { - return await getOriginalResponse() + return passthrough() } - // Bypass requests with the explicit bypass header - if (requestClone.headers.get(bypassHeaderName) === 'true') { - const cleanRequestHeaders = serializeHeaders(requestClone.headers) - - // Remove the bypass header to comply with the CORS preflight check. - delete cleanRequestHeaders[bypassHeaderName] - - const originalRequest = new Request(requestClone, { - headers: new Headers(cleanRequestHeaders), - }) - - return fetch(originalRequest) + // Bypass requests with the explicit bypass header. + // Such requests can be issued by "ctx.fetch()". + if (request.headers.get('x-msw-bypass') === 'true') { + return passthrough() } - // Send the request to the client-side MSW. - const reqHeaders = serializeHeaders(request.headers) - const body = await request.text() - + // Notify the client that a request has been intercepted. const clientMessage = await sendToClient(client, { type: 'REQUEST', payload: { id: requestId, url: request.url, method: request.method, - headers: reqHeaders, + headers: Object.fromEntries(request.headers.entries()), cache: request.cache, mode: request.mode, credentials: request.credentials, @@ -189,115 +247,32 @@ async function getResponse(event, client, requestId) { redirect: request.redirect, referrer: request.referrer, referrerPolicy: request.referrerPolicy, - body, + body: await request.text(), bodyUsed: request.bodyUsed, keepalive: request.keepalive, }, }) switch (clientMessage.type) { - case 'MOCK_SUCCESS': { - return delayPromise( - () => respondWithMock(clientMessage), - clientMessage.payload.delay, - ) + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) } case 'MOCK_NOT_FOUND': { - return getOriginalResponse() + return passthrough() } case 'NETWORK_ERROR': { - const { name, message } = clientMessage.payload + const { name, message } = clientMessage.data const networkError = new Error(message) networkError.name = name - // Rejecting a request Promise emulates a network error. + // Rejecting a "respondWith" promise emulates a network error. throw networkError } - - case 'INTERNAL_ERROR': { - const parsedBody = JSON.parse(clientMessage.payload.body) - - console.error( - `\ -[MSW] Uncaught exception in the request handler for "%s %s": - -${parsedBody.location} - -This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error, as it indicates a mistake in your code. If you wish to mock an error response, please see this guide: https://mswjs.io/docs/recipes/mocking-error-responses\ -`, - request.method, - request.url, - ) - - return respondWithMock(clientMessage) - } } - return getOriginalResponse() -} - -self.addEventListener('fetch', function (event) { - const { request } = event - const accept = request.headers.get('accept') || '' - - // Bypass server-sent events. - if (accept.includes('text/event-stream')) { - return - } - - // Bypass navigation requests. - if (request.mode === 'navigate') { - return - } - - // Opening the DevTools triggers the "only-if-cached" request - // that cannot be handled by the worker. Bypass such requests. - if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { - return - } - - // Bypass all requests when there are no active clients. - // Prevents the self-unregistered worked from handling requests - // after it's been deleted (still remains active until the next reload). - if (activeClientIds.size === 0) { - return - } - - const requestId = uuidv4() - - return event.respondWith( - handleRequest(event, requestId).catch((error) => { - if (error.name === 'NetworkError') { - console.warn( - '[MSW] Successfully emulated a network error for the "%s %s" request.', - request.method, - request.url, - ) - return - } - - // At this point, any exception indicates an issue with the original request/response. - console.error( - `\ -[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`, - request.method, - request.url, - `${error.name}: ${error.message}`, - ) - }), - ) -}) - -function serializeHeaders(headers) { - const reqHeaders = {} - headers.forEach((value, name) => { - reqHeaders[name] = reqHeaders[name] - ? [].concat(reqHeaders[name]).concat(value) - : value - }) - return reqHeaders + return passthrough() } function sendToClient(client, message) { @@ -312,27 +287,17 @@ function sendToClient(client, message) { resolve(event.data) } - client.postMessage(JSON.stringify(message), [channel.port2]) + client.postMessage(message, [channel.port2]) }) } -function delayPromise(cb, duration) { +function sleep(timeMs) { return new Promise((resolve) => { - setTimeout(() => resolve(cb()), duration) + setTimeout(resolve, timeMs) }) } -function respondWithMock(clientMessage) { - return new Response(clientMessage.payload.body, { - ...clientMessage.payload, - headers: clientMessage.payload.headers, - }) -} - -function uuidv4() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { - const r = (Math.random() * 16) | 0 - const v = c == 'x' ? r : (r & 0x3) | 0x8 - return v.toString(16) - }) +async function respondWithMock(response) { + await sleep(response.delay) + return new Response(response.body, response) }