From f1d011eb05c7dc2277f421a680f2d64e4232d8dd Mon Sep 17 00:00:00 2001 From: Thomas Arrow Date: Fri, 27 Mar 2026 13:07:13 +0000 Subject: [PATCH] Try again to fix MSW Collaborative attempt in Amsterdam Bug: T407364 --- package.json | 9 +- public/mockServiceWorker.js | 416 ++++++++++++++-------- src/backend/mocks/browser.js | 4 +- src/backend/mocks/default_handlers_new.js | 38 ++ src/main.js | 15 +- vue.config.js | 9 - 6 files changed, 324 insertions(+), 167 deletions(-) create mode 100644 src/backend/mocks/default_handlers_new.js diff --git a/package.json b/package.json index bec7e2e5..ee495744 100644 --- a/package.json +++ b/package.json @@ -67,5 +67,10 @@ "node": ">= 11.0.0", "npm": ">= 6.4.1" }, - "readme": "ERROR: No README data found!" -} + "readme": "ERROR: No README data found!", + "msw": { + "workerDirectory": [ + "" + ] + } +} \ No newline at end of file diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js index d8703e53..461e2600 100644 --- a/public/mockServiceWorker.js +++ b/public/mockServiceWorker.js @@ -1,30 +1,41 @@ +/* eslint-disable */ +/* tslint:disable */ + /** * Mock Service Worker. * @see https://github.com/mswjs/msw * - Please do NOT modify this file. - * - Please do NOT serve this file on production. */ -/* eslint-disable */ -/* tslint:disable */ - -const INTEGRITY_CHECKSUM = 'd1e0e502f550d40a34bee90822e4bf98' -const bypassHeaderName = 'x-msw-bypass' -let clients = {} +const PACKAGE_VERSION = '2.12.7' +const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() -self.addEventListener('install', function () { - return self.skipWaiting() +addEventListener('install', function () { + self.skipWaiting() }) -self.addEventListener('activate', async function (event) { - return self.clients.claim() +addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) }) -self.addEventListener('message', async function (event) { - const clientId = event.source.id - const client = await event.currentTarget.clients.get(clientId) - const allClients = await self.clients.matchAll() - const allClientIds = allClients.map((client) => client.id) +addEventListener('message', async function (event) { + const clientId = Reflect.get(event.source || {}, 'id') + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) switch (event.data) { case 'KEEPALIVE_REQUEST': { @@ -37,29 +48,32 @@ 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 } case 'MOCK_ACTIVATE': { - clients = ensureKeys(allClientIds, clients) - clients[clientId] = true + activeClientIds.add(clientId) sendToClient(client, { type: 'MOCKING_ENABLED', - payload: true, + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, }) break } - case 'MOCK_DEACTIVATE': { - clients = ensureKeys(allClientIds, clients) - clients[clientId] = false - break - } - case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + const remainingClients = allClients.filter((client) => { return client.id !== clientId }) @@ -74,162 +88,262 @@ self.addEventListener('message', async function (event) { } }) -self.addEventListener('fetch', async function (event) { - const { clientId, request } = event - const requestClone = request.clone() - const getOriginalResponse = () => fetch(requestClone) +addEventListener('fetch', function (event) { + const requestInterceptedAt = Date.now() + + // Bypass navigation requests. + if (event.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') { + if ( + event.request.cache === 'only-if-cached' && + event.request.mode !== 'same-origin' + ) { return } - event.respondWith( - new Promise(async (resolve, reject) => { - const client = await event.target.clients.get(clientId) - - if ( - // Bypass mocking when no clients active - !client || - // Bypass mocking if the current client has mocking disabled - !clients[clientId] || - // Bypass mocking for navigation requests - request.mode === 'navigate' - ) { - return resolve(getOriginalResponse()) - } + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been terminated (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } - // Bypass requests with the explicit bypass header - if (requestClone.headers.get(bypassHeaderName) === 'true') { - const modifiedHeaders = serializeHeaders(requestClone.headers) - // Remove the bypass header to comply with the CORS preflight check - delete modifiedHeaders[bypassHeaderName] + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId, requestInterceptedAt)) +}) - const originalRequest = new Request(requestClone, { - headers: new Headers(modifiedHeaders), - }) +/** + * @param {FetchEvent} event + * @param {string} requestId + * @param {number} requestInterceptedAt + */ +async function handleRequest(event, requestId, requestInterceptedAt) { + const client = await resolveMainClient(event) + const requestCloneForEvents = event.request.clone() + const response = await getResponse( + event, + client, + requestId, + requestInterceptedAt, + ) - return resolve(fetch(originalRequest)) - } + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + const serializedRequest = await serializeRequest(requestCloneForEvents) - const reqHeaders = serializeHeaders(request.headers) - const body = await request.text() + // Clone the response so both the client and the library could consume it. + const responseClone = response.clone() - const rawClientMessage = await sendToClient(client, { - type: 'REQUEST', + sendToClient( + client, + { + type: 'RESPONSE', payload: { - url: request.url, - method: request.method, - headers: reqHeaders, - cache: request.cache, - mode: request.mode, - credentials: request.credentials, - destination: request.destination, - integrity: request.integrity, - redirect: request.redirect, - referrer: request.referrer, - referrerPolicy: request.referrerPolicy, - body, - bodyUsed: request.bodyUsed, - keepalive: request.keepalive, + isMockedResponse: IS_MOCKED_RESPONSE in response, + request: { + id: requestId, + ...serializedRequest, + }, + response: { + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + headers: Object.fromEntries(responseClone.headers.entries()), + body: responseClone.body, + }, }, - }) + }, + responseClone.body ? [serializedRequest.body, responseClone.body] : [], + ) + } - const clientMessage = rawClientMessage - - switch (clientMessage.type) { - case 'MOCK_SUCCESS': { - setTimeout( - resolve.bind(this, createResponse(clientMessage)), - clientMessage.payload.delay, - ) - break - } - - case 'MOCK_NOT_FOUND': { - return resolve(getOriginalResponse()) - } - - case 'NETWORK_ERROR': { - const { name, message } = clientMessage.payload - const networkError = new Error(message) - networkError.name = name - - // Rejecting a request Promise emulates a network error. - return reject(networkError) - } - - case 'INTERNAL_ERROR': { - const parsedBody = JSON.parse(clientMessage.payload.body) - - console.error( - `\ -[MSW] Request handler function for "%s %s" has thrown the following exception: - -${parsedBody.errorType}: ${parsedBody.message} -(see more detailed error stack trace in the mocked response body) - -This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error. -If you wish to mock an error response, please refer to this guide: https://mswjs.io/docs/recipes/mocking-error-responses\ - `, - request.method, - request.url, - ) - - return resolve(createResponse(clientMessage)) - } - } - }).catch((error) => { - console.error( - '[MSW] Failed to mock a "%s" request to "%s": %s', - request.method, - request.url, - error, + 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. + * @param {FetchEvent} event + * @returns {Promise} + */ +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + 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) + }) +} + +/** + * @param {FetchEvent} event + * @param {Client | undefined} client + * @param {string} requestId + * @param {number} requestInterceptedAt + * @returns {Promise} + */ +async function getResponse(event, client, requestId, requestInterceptedAt) { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = event.request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept') + if (acceptHeader) { + const values = acceptHeader.split(',').map((value) => value.trim()) + const filteredValues = values.filter( + (value) => value !== 'msw/passthrough', ) - }), + + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')) + } else { + headers.delete('accept') + } + } + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const serializedRequest = await serializeRequest(event.request) + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + interceptedAt: requestInterceptedAt, + ...serializedRequest, + }, + }, + [serializedRequest.body], ) -}) -function serializeHeaders(headers) { - const reqHeaders = {} - headers.forEach((value, name) => { - reqHeaders[name] = reqHeaders[name] - ? [].concat(reqHeaders[name]).concat(value) - : value - }) - return reqHeaders + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() } -function sendToClient(client, message) { +/** + * @param {Client} client + * @param {any} message + * @param {Array} transferrables + * @returns {Promise} + */ +function sendToClient(client, message, transferrables = []) { return new Promise((resolve, reject) => { const channel = new MessageChannel() channel.port1.onmessage = (event) => { if (event.data && event.data.error) { - reject(event.data.error) - } else { - resolve(event.data) + return reject(event.data.error) } + + resolve(event.data) } - client.postMessage(JSON.stringify(message), [channel.port2]) + client.postMessage(message, [ + channel.port2, + ...transferrables.filter(Boolean), + ]) }) } -function createResponse(clientMessage) { - return new Response(clientMessage.payload.body, { - ...clientMessage.payload, - headers: clientMessage.payload.headers, +/** + * @param {Response} response + * @returns {Response} + */ +function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, }) -} -function ensureKeys(keys, obj) { - return Object.keys(obj).reduce((acc, key) => { - if (keys.includes(key)) { - acc[key] = obj[key] - } + return mockedResponse +} - return acc - }, {}) +/** + * @param {Request} request + */ +async function serializeRequest(request) { + return { + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.arrayBuffer(), + keepalive: request.keepalive, + } } diff --git a/src/backend/mocks/browser.js b/src/backend/mocks/browser.js index 159e6b9e..145db11d 100644 --- a/src/backend/mocks/browser.js +++ b/src/backend/mocks/browser.js @@ -1,4 +1,4 @@ -import { setupWorker } from 'msw' -import { handlers } from './default_handlers' +import { setupWorker } from 'msw/browser' +import { handlers } from './default_handlers_new' export const worker = setupWorker(...handlers) diff --git a/src/backend/mocks/default_handlers_new.js b/src/backend/mocks/default_handlers_new.js new file mode 100644 index 00000000..2cf2cb17 --- /dev/null +++ b/src/backend/mocks/default_handlers_new.js @@ -0,0 +1,38 @@ +import { http, HttpResponse } from 'msw' + +function makeUser (email = 'test@local') { + return { + id: 1, + email, + verified: true, + created_at: '2020-01-01', + updated_at: '2020-01-01', + } +} + +export const handlers = [ + http.get('/api/auth/login', async ({ request, cookies }) => { + const { authToken } = cookies + if (authToken !== 'token_value') { + return new Response('Unauthorized', { + status: 401, + }) + } + // todo this might be broken below here + console.log('duck') + // const body = await request.json() + console.log('duck2') + const user = makeUser('thomas.arrow@wikimediasdfa.de') + return HttpResponse.json({user}) + }), + http.post('/api/auth/login', async ({ request }) => { + const body = await request.json() + console.log('fooooooo') + console.log(body) + const user = makeUser(body.email) + return HttpResponse.json({ + user, + }, { headers: { 'set-cookie': 'authToken=token_value', } } + ) + }), +] \ No newline at end of file diff --git a/src/main.js b/src/main.js index e0279418..9ac43ca5 100644 --- a/src/main.js +++ b/src/main.js @@ -12,11 +12,19 @@ import 'typeface-roboto/index.css' import 'vuetify/dist/vuetify.min.css' import config from '~/config' -if (process.env.NODE_ENV !== 'production' && config.API_MOCK === '1') { - const { worker } = require('./backend/mocks/browser') - worker.start() +async function enableMocking() { + if (process.env.NODE_ENV !== 'development') { + return + } + + const { worker } = await import('./backend/mocks/browser') + + // `worker.start()` returns a Promise that resolves + // once the Service Worker is up and ready to intercept requests. + return worker.start() } +enableMocking().then(() => { Vue.config.productionTip = false Vue.use(Vuetify) @@ -62,3 +70,4 @@ new Vue({ }) }, }) +}) diff --git a/vue.config.js b/vue.config.js index 8d27d368..1fabe86a 100644 --- a/vue.config.js +++ b/vue.config.js @@ -1,14 +1,5 @@ const path = require('path') module.exports = { - devServer: { - proxy: { - '^/api': { - target: process.env.VUE_APP_API_URL, - changeOrigin: true, - pathRewrite: {'^/api' : ''} - } - } - }, lintOnSave: false, runtimeCompiler: true, chainWebpack: config => {