diff --git a/.gitattributes b/.gitattributes index 7ace50f31..0976a728c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,3 @@ -# LF in every checkout, Windows included: format-check, the msw worker check and -# tests that parse repo files compare bytes, so a CRLF working copy fails them. +# LF in every checkout, Windows included: format-check and tests that parse repo +# files compare bytes, so a CRLF working copy fails them. * text=auto eol=lf diff --git a/.github/claude/issue-analyst-context.md b/.github/claude/issue-analyst-context.md index 042fd34e2..b31354402 100644 --- a/.github/claude/issue-analyst-context.md +++ b/.github/claude/issue-analyst-context.md @@ -152,7 +152,7 @@ It is paths-filtered, so a docs-only or workflow-only change produces no run. | `golangci` | `golangci-lint` | | `fuzz-smoke` | 30s each on `FuzzParseLink` and `FuzzDecodeCertPin` | | `codegen` | `npm run gen` then `git diff --exit-code` on the generated files | -| `frontend` | MSW worker drift, lint, format:check, typecheck, `npm test` (Vitest + headless-Chromium Storybook), build, build-storybook, `npm audit` | +| `frontend` | lint, format:check, typecheck, `npm test` (Vitest + headless-Chromium Storybook), build, build-storybook, `npm audit` | **What CI does NOT prove.** These test families `t.Skip` unless an environment variable is set, and CI sets only the PostgreSQL ones above: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fedfe4403..a841bc562 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -201,9 +201,6 @@ jobs: - name: Install run: npm ci working-directory: frontend - - name: Verify generated MSW worker is current - run: git diff --exit-code -- public/mockServiceWorker.js package-lock.json - working-directory: frontend - name: Lint run: npm run lint working-directory: frontend diff --git a/Makefile b/Makefile index 26ec51307..de5c3a1d2 100644 --- a/Makefile +++ b/Makefile @@ -45,10 +45,6 @@ format-check: ## oxfmt in check mode on frontend sources typecheck: ## tsc --noEmit cd $(FRONTEND) && npm run typecheck -.PHONY: msw-worker-check -msw-worker-check: ## Verify the tracked worker matches the installed MSW runtime - cmp $(FRONTEND)/public/mockServiceWorker.js $(FRONTEND)/node_modules/msw/lib/mockServiceWorker.js - .PHONY: test-go test-go: dist-stub ## Go tests (shuffle, no cache) go test -shuffle=on -count=1 $(GO_PKGS) @@ -91,5 +87,5 @@ build-storybook: ## Build the static Storybook (compile-checks all stories) # The PR gate. Matches ci.yml: codegen freshness, both linters, the formatter, # typecheck, both test suites, a full build, and the Storybook compile-check. .PHONY: verify -verify: gen-check lint format-check typecheck msw-worker-check test build build-storybook ## Full local gate (mirrors CI) +verify: gen-check lint format-check typecheck test build build-storybook ## Full local gate (mirrors CI) @echo "verify: OK" diff --git a/frontend/package.json b/frontend/package.json index 6afc3a5bf..0db606bde 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -115,10 +115,5 @@ "tree-sitter-json": false, "msw": false, "esbuild": false - }, - "msw": { - "workerDirectory": [ - "public" - ] } } diff --git a/frontend/public/mockServiceWorker.js b/frontend/public/mockServiceWorker.js deleted file mode 100644 index 0c970efc9..000000000 --- a/frontend/public/mockServiceWorker.js +++ /dev/null @@ -1,361 +0,0 @@ -/* eslint-disable */ -/* tslint:disable */ - -/** - * Mock Service Worker. - * @see https://github.com/mswjs/msw - * - Please do NOT modify this file. - */ - -const PACKAGE_VERSION = '2.15.0' -const INTEGRITY_CHECKSUM = '03cb67ac84128e63d7cd722a6e5b7f1e' -const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') -const activeClientIds = new Set() - -addEventListener('install', function () { - self.skipWaiting() -}) - -addEventListener('activate', function (event) { - event.waitUntil(self.clients.claim()) -}) - -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': { - sendToClient(client, { - type: 'KEEPALIVE_RESPONSE', - }) - break - } - - case 'INTEGRITY_CHECK_REQUEST': { - sendToClient(client, { - type: 'INTEGRITY_CHECK_RESPONSE', - payload: { - packageVersion: PACKAGE_VERSION, - checksum: INTEGRITY_CHECKSUM, - }, - }) - break - } - - case 'MOCK_ACTIVATE': { - activeClientIds.add(clientId) - - sendToClient(client, { - type: 'MOCKING_ENABLED', - payload: { - client: { - id: client.id, - frameType: client.frameType, - }, - }, - }) - break - } - - case 'CLIENT_CLOSED': { - activeClientIds.delete(clientId) - - const remainingClients = allClients.filter((client) => { - return client.id !== clientId - }) - - // Unregister itself when there are no more clients - if (remainingClients.length === 0) { - self.registration.unregister() - } - - break - } - } -}) - -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 ( - event.request.cache === 'only-if-cached' && - event.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 terminated (still remains active until the next reload). - if (activeClientIds.size === 0) { - return - } - - const requestId = crypto.randomUUID() - event.respondWith(handleRequest(event, requestId, requestInterceptedAt)) -}) - -/** - * @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, - ) - - // 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) - - // Omit the body of server-sent event stream responses. - // Cloning such responses would prevent client-side stream cancelations - // from reaching the original stream (a teed stream only cancels its - // source once both of its branches cancel) and would buffer the - // entire stream into the unconsumed clone indefinitely. - const isEventStreamResponse = response.headers - .get('content-type') - ?.toLowerCase() - .startsWith('text/event-stream') - - // Clone the response so both the client and the library could consume it. - const responseClone = isEventStreamResponse ? null : response.clone() - - sendToClient( - client, - { - type: 'RESPONSE', - payload: { - isMockedResponse: IS_MOCKED_RESPONSE in response, - request: { - id: requestId, - ...serializedRequest, - }, - response: { - type: response.type, - status: response.status, - statusText: response.statusText, - headers: Object.fromEntries(response.headers.entries()), - body: responseClone ? responseClone.body : null, - }, - }, - }, - responseClone && responseClone.body - ? [serializedRequest.body, responseClone.body] - : [], - ) - } - - 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], - ) - - switch (clientMessage.type) { - case 'MOCK_RESPONSE': { - return respondWithMock(clientMessage.data) - } - - case 'PASSTHROUGH': { - return passthrough() - } - } - - return passthrough() -} - -/** - * @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) { - return reject(event.data.error) - } - - resolve(event.data) - } - - client.postMessage(message, [ - channel.port2, - ...transferrables.filter(Boolean), - ]) - }) -} - -/** - * @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, - }) - - return mockedResponse -} - -/** - * @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, - } -}