Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add next.js (app directory) example #101

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/examples.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,16 @@ jobs:
version: 7.12.1
- run: pnpm install --filter with-karma
- run: pnpm test --filter with-karma

next:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- uses: pnpm/action-setup@v2
with:
version: 7.12.1
- run: pnpm install --filter with-next
- run: pnpm test --filter with-next
36 changes: 36 additions & 0 deletions examples/with-next/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
36 changes: 36 additions & 0 deletions examples/with-next/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
Binary file added examples/with-next/app/favicon.ico
Binary file not shown.
24 changes: 24 additions & 0 deletions examples/with-next/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import { MockProvider } from './mockProvider'

const inter = Inter({ subsets: ['latin'] })

export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en">
<body className={inter.className}>
<MockProvider>{children}</MockProvider>
</body>
</html>
)
}
31 changes: 31 additions & 0 deletions examples/with-next/app/mockProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use client'
import { useEffect, useState } from 'react'

export function MockProvider({
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about using suspense?

mockProvider.tsx

'use client'

let triggered = false

async function enableApiMocking() {
  const { worker } = await import('../mocks/browser')
  await worker.start()
}

export function MockProvider() {
  if (!triggered) {
    triggered = true
    throw enableApiMocking()
  }

  return null
}

layout.tsx

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <MockProvider />
        {children}
      </body>
    </html>
  )
}

By doing so, we can avoid wrapping children in the mock provider client component.
But I am not sure if this is a good solution.

useEffect Suspense
ss2 ss

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal of this is to defer the rendering of the children until the service worker is activated. You are proposing keeping the state internally but I don't see it affecting {children}. So they will render, and if they make any HTTP requests, those will not be intercepted because the worker is not ready yet.

children,
}: Readonly<{
children: React.ReactNode
}>) {
const [mockingEnabled, enableMocking] = useState(false)

useEffect(() => {
async function enableApiMocking() {
/**
* @fixme Next puts this import to the top of
* this module and runs it during the build
* in Node.js. This makes "msw/browser" import to fail.
*/
const { worker } = await import('../mocks/browser')
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Next.js puts this dynamic import from the browser runtime to the Node.js build by moving it to the top of the module.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about fixing like this?

if (typeof window !== 'undefined') {
  const { worker } = await import('../mocks/browser')
  await worker.start()
}

await worker.start()
enableMocking(true)
}

enableApiMocking()
}, [])

if (!mockingEnabled) {
return null
}

return <>{children}</>
}
50 changes: 50 additions & 0 deletions examples/with-next/app/movieList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use client'
import { useState } from 'react'

export type Movie = {
id: string
title: string
}

export function MovieList() {
const [movies, setMovies] = useState<Array<Movie>>([])

const fetchMovies = () => {
fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: `
query ListMovies {
movies {
id
title
}
}
`,
}),
})
.then((response) => response.json())
.then((response) => {
setMovies(response.data.movies)
})
.catch(() => setMovies([]))
}

return (
<div>
<button id="fetch-movies-button" onClick={fetchMovies}>
Fetch movies
</button>
{movies.length > 0 ? (
<ul id="movies-list">
{movies.map((movie) => (
<li key={movie.id}>{movie.title}</li>
))}
</ul>
) : null}
</div>
)
}
24 changes: 24 additions & 0 deletions examples/with-next/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { MovieList } from './movieList'

export type User = {
firstName: string
lastName: string
}

async function getUser() {
console.log('fetching user', fetch)
const response = await fetch('https://api.example.com/user')
const user = (await response.json()) as User
return user
}

export default async function Home() {
const user = await getUser()

return (
<main>
<p id="server-side-greeting">Hello, {user.firstName}!</p>
<MovieList />
</main>
)
}
6 changes: 6 additions & 0 deletions examples/with-next/instrumentation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
const { server } = await import('./mocks/node')
server.listen()
}
}
4 changes: 4 additions & 0 deletions examples/with-next/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)
32 changes: 32 additions & 0 deletions examples/with-next/mocks/handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { http, graphql, HttpResponse } from 'msw'
import type { User } from '../app/page'
import type { Movie } from '@/app/movieList'

export const handlers = [
http.get<never, never, User>('https://api.example.com/user', () => {
return HttpResponse.json({
firstName: 'John',
lastName: 'Maverick',
})
}),
graphql.query<{ movies: Array<Movie> }>('ListMovies', () => {
return HttpResponse.json({
data: {
movies: [
{
id: '6c6dba95-e027-4fe2-acab-e8c155a7f0ff',
title: 'The Lord of The Rings',
},
{
id: 'a2ae7712-75a7-47bb-82a9-8ed668e00fe3',
title: 'The Matrix',
},
{
id: '916fa462-3903-4656-9e76-3f182b37c56f',
title: 'Star Wars: The Empire Strikes Back',
},
],
},
})
}),
]
4 changes: 4 additions & 0 deletions examples/with-next/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)
30 changes: 30 additions & 0 deletions examples/with-next/next.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
instrumentationHook: true,
},
webpack(config, { isServer }) {
/**
* @fixme This is completely redundant. webpack should understand
* export conditions and don't try to import "msw/browser" code
* that's clearly marked as client-side only in the app.
*/
if (isServer) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a hack. I'm not sure why webpack has trouble resolving export conditions. I suspect this isn't webpack's fault. Next.js runs a pure client-side component in Node.js during SSR build, which results in webpack thinking those client-side imports must be resolved in Node.js.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kettanaito I think the 'use client' directive is a bit of a misnomer. Components marked with that directive can still be SSR and are by default in Next unless you lazy load with ssr: false. Obviously anything in useEffect would only run on the client, so I'm not sure why the dynamic import you have in the other file is placed in a Node.js runtime. Let me know if I'm missing any context.

Having said that, I pulled this repository down and ran dev and build and both succeeded.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, thanks for clarifying, @dbk91!

I suspect webpack extracts that import and puts it at the top of the module for whichever optimization. This is a bit odd since import() is a valid JavaScript API in the browser so it can certainly be client-side only.

I know this example succeeds. I've added tests to confirm that and they are passing. But I'm not looking for the first working thing. I'm looking for an integration that'd last and make sense for developers. This one, in its current state, doesn't, as it has a couple of fundamentals problems.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood, that makes sense! I totally missed your follow up messages in your original Tweet—I was expecting something non-functional and didn't realize there was extra work to get these tests passing.

Either way, I've been following this for quite some time and appreciate the work you've put into MSW and specifically this integration. My team was using it prior to upgrading to app router and we've sorely missed it, but that's on us for upgrading.

if (Array.isArray(config.resolve.alias)) {
config.resolve.alias.push({ name: 'msw/browser', alias: false })
} else {
config.resolve.alias['msw/browser'] = false
}
} else {
if (Array.isArray(config.resolve.alias)) {
config.resolve.alias.push({ name: 'msw/node', alias: false })
} else {
config.resolve.alias['msw/node'] = false
}
}

return config
},
}

export default nextConfig
26 changes: 26 additions & 0 deletions examples/with-next/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "with-next",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"test": "playwright test",
"postinstall": "pnpm exec playwright install"
},
"dependencies": {
"next": "14.1.0",
"react": "^18",
"react-dom": "^18"
},
"devDependencies": {
"@playwright/test": "^1.41.1",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"msw": "2.0.14",
"typescript": "^5"
}
}
12 changes: 12 additions & 0 deletions examples/with-next/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineConfig } from '@playwright/test'

export default defineConfig({
webServer: {
command: 'pnpm dev',
port: 3000,
reuseExistingServer: !process.env.CI,
},
use: {
baseURL: 'http://localhost:3000',
},
})
Loading