Skip to content

Commit

Permalink
feat: updated to latest version of remix & added client-side mocking
Browse files Browse the repository at this point in the history
  • Loading branch information
twilsonn committed Jul 9, 2024
1 parent fbd56b0 commit 3710f14
Show file tree
Hide file tree
Showing 12 changed files with 541 additions and 46 deletions.
6 changes: 4 additions & 2 deletions msw/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions msw/app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -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,
<StrictMode>
<RemixBrowser />
</StrictMode>,
)
})
})
148 changes: 148 additions & 0 deletions msw/app/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -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(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
{
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(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
{
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)
})
}
4 changes: 4 additions & 0 deletions msw/app/mocks/browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'

export const worker = setupWorker(...handlers)
13 changes: 13 additions & 0 deletions msw/app/mocks/handlers.ts
Original file line number Diff line number Diff line change
@@ -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',
})
}),
]
4 changes: 4 additions & 0 deletions msw/app/mocks/node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { setupServer } from 'msw/node'
import { handlers } from './handlers'

export const server = setupServer(...handlers)
33 changes: 30 additions & 3 deletions msw/app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>, key) => {
obj[key] = process.env[key]!
return obj
}, {})
return JSON.stringify(env)
}

export function Layout({ children }: { children: React.ReactNode }) {
return (
Expand All @@ -19,11 +41,16 @@ export function Layout({ children }: { children: React.ReactNode }) {
{children}
<ScrollRestoration />
<Scripts />
<script
dangerouslySetInnerHTML={{
__html: `window.process={env:${getBrowserEnvironment()}}`,
}}
/>
</body>
</html>
);
)
}

export default function App() {
return <Outlet />;
return <Outlet />
}
28 changes: 11 additions & 17 deletions msw/app/routes/_index.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { json } from '@remix-run/node'
import { useLoaderData } from '@remix-run/react'

export const loader = async () => {
const data = await fetch("https://my-mock-api.com").then((response) =>
response.json(),
);

if (!data || typeof data.message !== "string") {
throw json({ message: "Server error" }, { status: 500 });
}

return json(data);
};
export async function loader() {
const res = await fetch(`${process.env.API_BASE}/user`)
const data = await res.json()
return json(data)
}

export default function Index() {
const data = useLoaderData<typeof loader>();

const data = useLoaderData<typeof loader>()
return (
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
<h1>{data.message}</h1>
<h1>Welcome to Remix</h1>
<pre>{JSON.stringify(data)}</pre>
</div>
);
)
}
9 changes: 0 additions & 9 deletions msw/mocks/handlers.cjs

This file was deleted.

7 changes: 0 additions & 7 deletions msw/mocks/index.cjs

This file was deleted.

21 changes: 13 additions & 8 deletions msw/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,43 @@
"type": "module",
"scripts": {
"build": "remix vite:build",
"dev": "binode --require ./mocks/index.cjs -- @remix-run/dev:remix vite:dev",
"dev": "remix vite:dev",
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
"start": "remix-serve ./build/server/index.js",
"typecheck": "tsc"
},
"dependencies": {
"@remix-run/node": "^2.9.2",
"@remix-run/react": "^2.9.2",
"@remix-run/serve": "^2.9.2",
"@remix-run/node": "^2.10.2",
"@remix-run/react": "^2.10.2",
"@remix-run/serve": "^2.10.2",
"isbot": "^4.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@remix-run/dev": "^2.9.2",
"@remix-run/dev": "^2.10.2",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"binode": "^1.0.5",
"autoprefixer": "^10.4.19",
"eslint": "^8.38.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"msw": "^2.3",
"msw": "^2.3.1",
"typescript": "^5.1.6",
"vite": "^5.1.0",
"vite-tsconfig-paths": "^4.2.1"
},
"engines": {
"node": ">=20.0.0"
},
"msw": {
"workerDirectory": [
"public"
]
}
}
}
Loading

0 comments on commit 3710f14

Please sign in to comment.