From 311956e417ffc45a5ea7ce6b1ead4cbc0e4ed421 Mon Sep 17 00:00:00 2001 From: Spocke Date: Wed, 16 Oct 2024 10:41:15 +0200 Subject: [PATCH 1/3] TINY-11415: added static service worker --- CHANGELOG.md | 6 + .../main/ts/bedrock/server/RunnerRoutes.ts | 1 + .../src/resources/js/mockServiceWorker.js | 284 ++++++++++++++++++ 3 files changed, 291 insertions(+) create mode 100644 modules/server/src/resources/js/mockServiceWorker.js diff --git a/CHANGELOG.md b/CHANGELOG.md index f51bc650..11fb8455 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 14.1.5 - 2024-10-18 + +### Added + +- Added static mock service worker js file. #TINY-11415 + ## 14.1.4 - 2024-03-27 ### Fixed diff --git a/modules/server/src/main/ts/bedrock/server/RunnerRoutes.ts b/modules/server/src/main/ts/bedrock/server/RunnerRoutes.ts index 0d8c47d2..0444ecd8 100644 --- a/modules/server/src/main/ts/bedrock/server/RunnerRoutes.ts +++ b/modules/server/src/main/ts/bedrock/server/RunnerRoutes.ts @@ -101,6 +101,7 @@ export const generate = async (mode: string, projectdir: string, basedir: string Routes.routing('GET', '/lib/jquery', path.dirname(require.resolve('jquery'))), Routes.routing('GET', '/lib/core-js-bundle', path.dirname(require.resolve('core-js-bundle'))), Routes.routing('GET', '/css', path.join(basedir, 'src/resources/css')), + Routes.rewrite('GET', '/', '/mockServiceWorker.js', path.join(basedir, 'src/resources/js/mockServiceWorker.js')), // test code Routes.asyncJs('GET', '/compiled/tests.js', (done) => { diff --git a/modules/server/src/resources/js/mockServiceWorker.js b/modules/server/src/resources/js/mockServiceWorker.js new file mode 100644 index 00000000..89f49bfe --- /dev/null +++ b/modules/server/src/resources/js/mockServiceWorker.js @@ -0,0 +1,284 @@ +/* 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. + */ + +const PACKAGE_VERSION = '2.4.11' +const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +self.addEventListener('install', function () { + self.skipWaiting() +}) + +self.addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +self.addEventListener('message', async function (event) { + const clientId = 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: true, + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + 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 + } + } +}) + +self.addEventListener('fetch', function (event) { + const { request } = event + + // 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 + } + + // Generate unique request ID. + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId)) +}) + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const response = await getResponse(event, client, requestId) + + // 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)) { + ;(async function () { + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + requestId, + isMockedResponse: IS_MOCKED_RESPONSE in response, + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + body: responseClone.body, + headers: Object.fromEntries(responseClone.headers.entries()), + }, + }, + [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. +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 + + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = request.clone() + + function passthrough() { + const headers = Object.fromEntries(requestClone.headers.entries()) + + // Remove internal MSW request header so the passthrough request + // complies with any potential CORS preflight checks on the server. + // Some servers forbid unknown request headers. + delete headers['x-msw-intention'] + + 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 requestBuffer = await request.arrayBuffer() + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + 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: requestBuffer, + keepalive: request.keepalive, + }, + }, + [requestBuffer], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +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].concat(transferrables.filter(Boolean)), + ) + }) +} + +async 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 +} From 67fab756efad27039d1634c9693559bbb7df205d Mon Sep 17 00:00:00 2001 From: Spocke Date: Thu, 17 Oct 2024 20:17:00 +0200 Subject: [PATCH 2/3] TINY-11415: Fixed jenkinsfile --- Jenkinsfile | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index a20104b4..b0fbdeca 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -21,19 +21,35 @@ timestamps { stage("test") { exec('yarn test') - - bedrockBrowsers( - prepareTests: { - yarnInstall() - exec('yarn build') - }, - testDirs: [ 'modules/sample/src/test/ts/**/pass' ], - custom: '--config modules/sample/tsconfig.json --customRoutes modules/sample/routes.json --polyfills Promise Symbol' - ) } + } + + // Testing + stage("bedrock testing") { + bedrockRemoteBrowsers( + platforms: [ + [ browser: 'chrome', provider: 'aws', buckets: 2 ], + [ browser: 'firefox', provider: 'aws', buckets: 2 ], + [ browser: 'edge', provider: 'lambdatest', buckets: 1 ], + [ browser: 'chrome', provider: 'lambdatest', os: 'macOS Sonoma', buckets: 1 ], + [ browser: 'firefox', provider: 'lambdatest', os: 'macOS Sonoma', buckets: 1 ], + [ browser: 'safari', provider: 'lambdatest', os: 'macOS Sonoma', buckets: 1 ], + ], + prepareTests: { + yarnInstall() + sh 'yarn build' + }, + testDirs: [ 'modules/sample/src/test/ts/**/pass' ], + custom: '--config modules/sample/tsconfig.json --customRoutes modules/sample/routes.json --polyfills Promise Symbol' + ) + } - if (isReleaseBranch()) { - stage("publish") { + // Publish + if (isReleaseBranch()) { + stage("publish") { + tinyPods.node() { + yarnInstall() + sh 'yarn build' tinyNpm.withNpmPublishCredentials { // We need to tell git to ignore the changes to .npmrc when publishing exec('git update-index --assume-unchanged .npmrc') From 55ee8bc66cf44dda587df9061e6c5f6495fe52f7 Mon Sep 17 00:00:00 2001 From: Spocke Date: Fri, 18 Oct 2024 16:18:33 +0200 Subject: [PATCH 3/3] TINY-11415: Switched to using node resolve from project root --- CHANGELOG.md | 2 +- .../src/main/ts/bedrock/server/Routes.ts | 23 ++ .../main/ts/bedrock/server/RunnerRoutes.ts | 2 +- .../src/resources/js/mockServiceWorker.js | 284 ------------------ 4 files changed, 25 insertions(+), 286 deletions(-) delete mode 100644 modules/server/src/resources/js/mockServiceWorker.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 11fb8455..95758e75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Added static mock service worker js file. #TINY-11415 +- Added static mock service worker js file mapping. #TINY-11415 ## 14.1.4 - 2024-03-27 diff --git a/modules/server/src/main/ts/bedrock/server/Routes.ts b/modules/server/src/main/ts/bedrock/server/Routes.ts index 61fe0f3c..0b8173fe 100644 --- a/modules/server/src/main/ts/bedrock/server/Routes.ts +++ b/modules/server/src/main/ts/bedrock/server/Routes.ts @@ -214,3 +214,26 @@ export const nodeResolve = (method: HTTPMethod, prefix: string, source: string): }; }; +export const nodeResolveFile = (method: HTTPMethod, url: string, projectDir: string, moduleName: string, subPath: string): Route => { + const go: RouteGoFunc = (request, response, done) => { + const failure = (status: number, data: string) => { + doResponse(request, response, status, 'text/plain', data); + done(); + }; + + try { + const moduleResolvedPath = require.resolve(path.join(moduleName, 'package.json'), { paths: [ projectDir ] }); + const router = createServer(path.dirname(moduleResolvedPath)); + request.url = '/' + subPath; + router(request, response, done); + } catch (e) { + failure(404, `Failed to resolve static node file for module path: ${moduleName}/${subPath}`); + } + }; + + return { + matches: [Matchers.methodMatch(method), Matchers.urlMatch(url)], + go + }; +}; + diff --git a/modules/server/src/main/ts/bedrock/server/RunnerRoutes.ts b/modules/server/src/main/ts/bedrock/server/RunnerRoutes.ts index 0444ecd8..02628c03 100644 --- a/modules/server/src/main/ts/bedrock/server/RunnerRoutes.ts +++ b/modules/server/src/main/ts/bedrock/server/RunnerRoutes.ts @@ -101,7 +101,7 @@ export const generate = async (mode: string, projectdir: string, basedir: string Routes.routing('GET', '/lib/jquery', path.dirname(require.resolve('jquery'))), Routes.routing('GET', '/lib/core-js-bundle', path.dirname(require.resolve('core-js-bundle'))), Routes.routing('GET', '/css', path.join(basedir, 'src/resources/css')), - Routes.rewrite('GET', '/', '/mockServiceWorker.js', path.join(basedir, 'src/resources/js/mockServiceWorker.js')), + Routes.nodeResolveFile('GET', '/mockServiceWorker.js', projectdir, 'msw', 'lib/mockServiceWorker.js'), // test code Routes.asyncJs('GET', '/compiled/tests.js', (done) => { diff --git a/modules/server/src/resources/js/mockServiceWorker.js b/modules/server/src/resources/js/mockServiceWorker.js deleted file mode 100644 index 89f49bfe..00000000 --- a/modules/server/src/resources/js/mockServiceWorker.js +++ /dev/null @@ -1,284 +0,0 @@ -/* 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. - */ - -const PACKAGE_VERSION = '2.4.11' -const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423' -const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') -const activeClientIds = new Set() - -self.addEventListener('install', function () { - self.skipWaiting() -}) - -self.addEventListener('activate', function (event) { - event.waitUntil(self.clients.claim()) -}) - -self.addEventListener('message', async function (event) { - const clientId = 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: true, - }) - break - } - - case 'MOCK_DEACTIVATE': { - activeClientIds.delete(clientId) - 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 - } - } -}) - -self.addEventListener('fetch', function (event) { - const { request } = event - - // 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 - } - - // Generate unique request ID. - const requestId = crypto.randomUUID() - event.respondWith(handleRequest(event, requestId)) -}) - -async function handleRequest(event, requestId) { - const client = await resolveMainClient(event) - const response = await getResponse(event, client, requestId) - - // 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)) { - ;(async function () { - const responseClone = response.clone() - - sendToClient( - client, - { - type: 'RESPONSE', - payload: { - requestId, - isMockedResponse: IS_MOCKED_RESPONSE in response, - type: responseClone.type, - status: responseClone.status, - statusText: responseClone.statusText, - body: responseClone.body, - headers: Object.fromEntries(responseClone.headers.entries()), - }, - }, - [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. -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 - - // Clone the request because it might've been already used - // (i.e. its body has been read and sent to the client). - const requestClone = request.clone() - - function passthrough() { - const headers = Object.fromEntries(requestClone.headers.entries()) - - // Remove internal MSW request header so the passthrough request - // complies with any potential CORS preflight checks on the server. - // Some servers forbid unknown request headers. - delete headers['x-msw-intention'] - - 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 requestBuffer = await request.arrayBuffer() - const clientMessage = await sendToClient( - client, - { - type: 'REQUEST', - payload: { - id: requestId, - 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: requestBuffer, - keepalive: request.keepalive, - }, - }, - [requestBuffer], - ) - - switch (clientMessage.type) { - case 'MOCK_RESPONSE': { - return respondWithMock(clientMessage.data) - } - - case 'PASSTHROUGH': { - return passthrough() - } - } - - return passthrough() -} - -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].concat(transferrables.filter(Boolean)), - ) - }) -} - -async 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 -}