From 3710f143eaf37d284fd6b9a404c37dbf7b0122e3 Mon Sep 17 00:00:00 2001 From: Tyler Wilson Date: Tue, 9 Jul 2024 22:36:23 +0100 Subject: [PATCH] feat: updated to latest version of remix & added client-side mocking --- msw/README.md | 6 +- msw/app/entry.client.tsx | 30 ++++ msw/app/entry.server.tsx | 148 +++++++++++++++++ msw/app/mocks/browser.ts | 4 + msw/app/mocks/handlers.ts | 13 ++ msw/app/mocks/node.ts | 4 + msw/app/root.tsx | 33 +++- msw/app/routes/_index.tsx | 28 ++-- msw/mocks/handlers.cjs | 9 - msw/mocks/index.cjs | 7 - msw/package.json | 21 ++- msw/public/mockServiceWorker.js | 284 ++++++++++++++++++++++++++++++++ 12 files changed, 541 insertions(+), 46 deletions(-) create mode 100644 msw/app/entry.client.tsx create mode 100644 msw/app/entry.server.tsx create mode 100644 msw/app/mocks/browser.ts create mode 100644 msw/app/mocks/handlers.ts create mode 100644 msw/app/mocks/node.ts delete mode 100644 msw/mocks/handlers.cjs delete mode 100644 msw/mocks/index.cjs create mode 100644 msw/public/mockServiceWorker.js diff --git a/msw/README.md b/msw/README.md index ea9d32e9..da3a2797 100644 --- a/msw/README.md +++ b/msw/README.md @@ -21,8 +21,10 @@ You can read more about the use cases of MSW [here](https://mswjs.io/docs/#when- ## Relevant files -- [mocks](./mocks/index.cjs) - registers the Node HTTP mock server -- [handlers](./mocks/handlers.cjs) - describes the HTTP mocks +- [server-side mocks](./app/mocks/node.ts) - registers the Node mock server +- [client-side mocks](./app/mocks/browser.ts) - registers the browser (Worker) mock server +- [handlers](./app/mocks/handlers.ts) - describes the HTTP mocks +- [root](./app/root.tsx) - added script to expose the API_BASE environment variable to client-side - [package.json](./package.json) ## Related Links diff --git a/msw/app/entry.client.tsx b/msw/app/entry.client.tsx new file mode 100644 index 00000000..703e399f --- /dev/null +++ b/msw/app/entry.client.tsx @@ -0,0 +1,30 @@ +/** + * By default, Remix will handle hydrating your app on the client for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/file-conventions/entry.client + */ + +import { RemixBrowser } from '@remix-run/react' +import { startTransition, StrictMode } from 'react' +import { hydrateRoot } from 'react-dom/client' + +// if in dev mode, import the worker for msw integration and start the worker +async function prepareApp() { + if (process.env.NODE_ENV === 'development') { + const { worker } = await import('./mocks/browser') + return worker.start() + } + + return Promise.resolve() +} + +prepareApp().then(() => { + startTransition(() => { + hydrateRoot( + document, + + + , + ) + }) +}) diff --git a/msw/app/entry.server.tsx b/msw/app/entry.server.tsx new file mode 100644 index 00000000..045252d6 --- /dev/null +++ b/msw/app/entry.server.tsx @@ -0,0 +1,148 @@ +/** + * By default, Remix will handle generating the HTTP Response for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/file-conventions/entry.server + */ + +import { PassThrough } from 'node:stream' + +import type { AppLoadContext, EntryContext } from '@remix-run/node' +import { createReadableStreamFromReadable } from '@remix-run/node' +import { RemixServer } from '@remix-run/react' +import { isbot } from 'isbot' +import { renderToPipeableStream } from 'react-dom/server' + +// import server for msw integration +import { server } from './mocks/node' + +const ABORT_DELAY = 5_000 + +// if in dev mode, start the node server +if (process.env.NODE_ENV === 'development') { + server.listen() +} + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, + // This is ignored so we can keep it in the template for visibility. Feel + // free to delete this parameter in your app if you're not using it! + // eslint-disable-next-line @typescript-eslint/no-unused-vars + loadContext: AppLoadContext, +) { + return isbot(request.headers.get('user-agent') || '') + ? handleBotRequest( + request, + responseStatusCode, + responseHeaders, + remixContext, + ) + : handleBrowserRequest( + request, + responseStatusCode, + responseHeaders, + remixContext, + ) +} + +function handleBotRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, +) { + return new Promise((resolve, reject) => { + let shellRendered = false + const { pipe, abort } = renderToPipeableStream( + , + { + onAllReady() { + shellRendered = true + const body = new PassThrough() + const stream = createReadableStreamFromReadable(body) + + responseHeaders.set('Content-Type', 'text/html') + + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }), + ) + + pipe(body) + }, + onShellError(error: unknown) { + reject(error) + }, + onError(error: unknown) { + responseStatusCode = 500 + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error) + } + }, + }, + ) + + setTimeout(abort, ABORT_DELAY) + }) +} + +function handleBrowserRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, +) { + return new Promise((resolve, reject) => { + let shellRendered = false + const { pipe, abort } = renderToPipeableStream( + , + { + onShellReady() { + shellRendered = true + const body = new PassThrough() + const stream = createReadableStreamFromReadable(body) + + responseHeaders.set('Content-Type', 'text/html') + + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }), + ) + + pipe(body) + }, + onShellError(error: unknown) { + reject(error) + }, + onError(error: unknown) { + responseStatusCode = 500 + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error) + } + }, + }, + ) + + setTimeout(abort, ABORT_DELAY) + }) +} diff --git a/msw/app/mocks/browser.ts b/msw/app/mocks/browser.ts new file mode 100644 index 00000000..4dd03f09 --- /dev/null +++ b/msw/app/mocks/browser.ts @@ -0,0 +1,4 @@ +import { setupWorker } from 'msw/browser' +import { handlers } from './handlers' + +export const worker = setupWorker(...handlers) diff --git a/msw/app/mocks/handlers.ts b/msw/app/mocks/handlers.ts new file mode 100644 index 00000000..d616a9de --- /dev/null +++ b/msw/app/mocks/handlers.ts @@ -0,0 +1,13 @@ +import { http, HttpResponse } from 'msw' + +export const handlers = [ + // Intercept "GET ${process.env.API_BASE}/user" requests... + http.get(`${process.env.API_BASE}/user`, () => { + // ...and respond to them using this JSON response. + return HttpResponse.json({ + id: 'c7b3d8e0-5e0b-4b0f-8b3a-3b9f4b3d3b3d', + firstName: 'John', + lastName: 'Maverick', + }) + }), +] diff --git a/msw/app/mocks/node.ts b/msw/app/mocks/node.ts new file mode 100644 index 00000000..86f7d615 --- /dev/null +++ b/msw/app/mocks/node.ts @@ -0,0 +1,4 @@ +import { setupServer } from 'msw/node' +import { handlers } from './handlers' + +export const server = setupServer(...handlers) diff --git a/msw/app/root.tsx b/msw/app/root.tsx index e82f26fd..e74979c1 100644 --- a/msw/app/root.tsx +++ b/msw/app/root.tsx @@ -4,7 +4,29 @@ import { Outlet, Scripts, ScrollRestoration, -} from "@remix-run/react"; +} from '@remix-run/react' +import './tailwind.css' + +/** + * Retrieves and stringifies specific environment variables for browser exposure. + * + * @function getBrowserEnvironment + * @returns {string} A JSON string containing the public environment variables. + * + * @note + * - Only variables listed in `exposedVariables` will be included in the output. + * - Do not add secret variables to the `exposedVariables` array. + */ +const getBrowserEnvironment = () => { + const exposedVariables = ['API_BASE'] + const env = Object.keys(process.env) + .filter((key) => exposedVariables.includes(key)) + .reduce((obj: Record, key) => { + obj[key] = process.env[key]! + return obj + }, {}) + return JSON.stringify(env) +} export function Layout({ children }: { children: React.ReactNode }) { return ( @@ -19,11 +41,16 @@ export function Layout({ children }: { children: React.ReactNode }) { {children} +