├── .eslintrc.json ├── next.config.js ├── public ├── favicon.ico ├── vercel.svg └── mockServiceWorker.js ├── pages ├── _app.js ├── api │ └── hello.js └── index.js ├── .storybook ├── main.js └── preview.js ├── styles ├── globals.css └── Home.module.css ├── stories ├── Header.stories.jsx ├── pages │ └── home.stories.jsx ├── header.css ├── Page.stories.jsx ├── button.css ├── Button.stories.jsx ├── assets │ ├── direction.svg │ ├── flow.svg │ ├── code-brackets.svg │ ├── comments.svg │ ├── repo.svg │ ├── plugin.svg │ ├── stackalt.svg │ └── colors.svg ├── page.css ├── Button.jsx ├── Header.jsx ├── Page.jsx └── Introduction.stories.mdx ├── .gitignore ├── package.json └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storybookjs/nextjs-example/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | 3 | function MyApp({ Component, pageProps }) { 4 | return 5 | } 6 | 7 | export default MyApp 8 | -------------------------------------------------------------------------------- /pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default function handler(req, res) { 4 | res.status(200).json({ name: 'John Doe' }) 5 | } 6 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "stories": [ 3 | "../stories/**/*.stories.mdx", 4 | "../stories/**/*.stories.@(js|jsx|ts|tsx)" 5 | ], 6 | "addons": [ 7 | "@storybook/addon-links", 8 | "@storybook/addon-essentials" 9 | ], 10 | "core": { 11 | "builder": "webpack5" 12 | } 13 | } -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /stories/Header.stories.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Header } from './Header'; 4 | 5 | export default { 6 | title: 'Example/Header', 7 | component: Header, 8 | }; 9 | 10 | const Template = (args) =>
; 11 | 12 | export const LoggedIn = Template.bind({}); 13 | LoggedIn.args = { 14 | user: {}, 15 | }; 16 | 17 | export const LoggedOut = Template.bind({}); 18 | LoggedOut.args = {}; 19 | -------------------------------------------------------------------------------- /stories/pages/home.stories.jsx: -------------------------------------------------------------------------------- 1 | import Home, { getServerSideProps } from "../../pages/index"; 2 | 3 | export default { 4 | title: "Pages/Home", 5 | component: Home, 6 | }; 7 | 8 | export const HomePage = (args, { loaded: { name } }) => ( 9 | 10 | ); 11 | HomePage.args = { name: "John Dough" }; 12 | HomePage.loaders = [ 13 | async () => { 14 | let data = await getServerSideProps(); 15 | 16 | return data.props; 17 | }, 18 | ]; 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /stories/header.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 4 | padding: 15px 20px; 5 | display: flex; 6 | align-items: center; 7 | justify-content: space-between; 8 | } 9 | 10 | svg { 11 | display: inline-block; 12 | vertical-align: top; 13 | } 14 | 15 | h1 { 16 | font-weight: 900; 17 | font-size: 20px; 18 | line-height: 1; 19 | margin: 6px 0 6px 10px; 20 | display: inline-block; 21 | vertical-align: top; 22 | } 23 | 24 | button + button { 25 | margin-left: 10px; 26 | } 27 | -------------------------------------------------------------------------------- /stories/Page.stories.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Page } from './Page'; 4 | import * as HeaderStories from './Header.stories'; 5 | 6 | export default { 7 | title: 'Example/Page', 8 | component: Page, 9 | }; 10 | 11 | const Template = (args) => ; 12 | 13 | export const LoggedIn = Template.bind({}); 14 | LoggedIn.args = { 15 | // More on composing args: https://storybook.js.org/docs/react/writing-stories/args#args-composition 16 | ...HeaderStories.LoggedIn.args, 17 | }; 18 | 19 | export const LoggedOut = Template.bind({}); 20 | LoggedOut.args = { 21 | ...HeaderStories.LoggedOut.args, 22 | }; 23 | -------------------------------------------------------------------------------- /stories/button.css: -------------------------------------------------------------------------------- 1 | .storybook-button { 2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | font-weight: 700; 4 | border: 0; 5 | border-radius: 3em; 6 | cursor: pointer; 7 | display: inline-block; 8 | line-height: 1; 9 | } 10 | .storybook-button--primary { 11 | color: white; 12 | background-color: #1ea7fd; 13 | } 14 | .storybook-button--secondary { 15 | color: #333; 16 | background-color: transparent; 17 | box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; 18 | } 19 | .storybook-button--small { 20 | font-size: 12px; 21 | padding: 10px 16px; 22 | } 23 | .storybook-button--medium { 24 | font-size: 14px; 25 | padding: 11px 20px; 26 | } 27 | .storybook-button--large { 28 | font-size: 16px; 29 | padding: 12px 24px; 30 | } 31 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | 3 | import * as NextImage from "next/image"; 4 | 5 | import { setupWorker, rest } from "msw"; 6 | 7 | if (typeof global.process === "undefined") { 8 | const worker = setupWorker( 9 | rest.get("http://localhost:3000/api/hello", (req, res, ctx) => { 10 | return res(ctx.json({ name: "John Doh" })); 11 | }) 12 | ); 13 | worker.start(); 14 | } 15 | 16 | const OriginalNextImage = NextImage.default; 17 | 18 | Object.defineProperty(NextImage, "default", { 19 | configurable: true, 20 | value: (props) => , 21 | }); 22 | 23 | export const parameters = { 24 | actions: { argTypesRegex: "^on[A-Z].*" }, 25 | controls: { 26 | matchers: { 27 | color: /(background|color)$/i, 28 | date: /Date$/, 29 | }, 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "storybook": "start-storybook -p 6006 -s ./public", 11 | "build-storybook": "build-storybook -s public" 12 | }, 13 | "dependencies": { 14 | "next": "^12.0.1", 15 | "react": "17.0.2", 16 | "react-dom": "17.0.2" 17 | }, 18 | "devDependencies": { 19 | "@babel/core": "^7.15.8", 20 | "@storybook/addon-actions": "^6.3.12", 21 | "@storybook/addon-essentials": "^6.3.12", 22 | "@storybook/addon-links": "^6.3.12", 23 | "@storybook/builder-webpack5": "^6.3.12", 24 | "@storybook/manager-webpack5": "^6.3.12", 25 | "@storybook/react": "^6.3.12", 26 | "babel-loader": "^8.2.2", 27 | "eslint": "^8.1.0", 28 | "eslint-config-next": "^12.0.1", 29 | "msw": "^0.35.0" 30 | }, 31 | "msw": { 32 | "workerDirectory": "public" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /stories/Button.stories.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Button } from './Button'; 4 | 5 | // More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 6 | export default { 7 | title: 'Example/Button', 8 | component: Button, 9 | // More on argTypes: https://storybook.js.org/docs/react/api/argtypes 10 | argTypes: { 11 | backgroundColor: { control: 'color' }, 12 | }, 13 | }; 14 | 15 | // More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args 16 | const Template = (args) => 19 | ); 20 | }; 21 | 22 | Button.propTypes = { 23 | /** 24 | * Is this the principal call to action on the page? 25 | */ 26 | primary: PropTypes.bool, 27 | /** 28 | * What background color to use 29 | */ 30 | backgroundColor: PropTypes.string, 31 | /** 32 | * How large should the button be? 33 | */ 34 | size: PropTypes.oneOf(['small', 'medium', 'large']), 35 | /** 36 | * Button contents 37 | */ 38 | label: PropTypes.string.isRequired, 39 | /** 40 | * Optional click handler 41 | */ 42 | onClick: PropTypes.func, 43 | }; 44 | 45 | Button.defaultProps = { 46 | backgroundColor: null, 47 | primary: false, 48 | size: 'medium', 49 | onClick: undefined, 50 | }; 51 | -------------------------------------------------------------------------------- /stories/assets/flow.svg: -------------------------------------------------------------------------------- 1 | illustration/flow -------------------------------------------------------------------------------- /stories/assets/code-brackets.svg: -------------------------------------------------------------------------------- 1 | illustration/code-brackets -------------------------------------------------------------------------------- /stories/assets/comments.svg: -------------------------------------------------------------------------------- 1 | illustration/comments -------------------------------------------------------------------------------- /stories/assets/repo.svg: -------------------------------------------------------------------------------- 1 | illustration/repo -------------------------------------------------------------------------------- /stories/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { Button } from './Button'; 5 | import './header.css'; 6 | 7 | export const Header = ({ user, onLogin, onLogout, onCreateAccount }) => ( 8 |
9 |
10 |
11 | 12 | 13 | 17 | 21 | 25 | 26 | 27 |

Acme

28 |
29 |
30 | {user ? ( 31 |
39 |
40 |
41 | ); 42 | 43 | Header.propTypes = { 44 | user: PropTypes.shape({}), 45 | onLogin: PropTypes.func.isRequired, 46 | onLogout: PropTypes.func.isRequired, 47 | onCreateAccount: PropTypes.func.isRequired, 48 | }; 49 | 50 | Header.defaultProps = { 51 | user: null, 52 | }; 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 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). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | 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. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /stories/assets/plugin.svg: -------------------------------------------------------------------------------- 1 | illustration/plugin -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | min-height: 100vh; 3 | padding: 0 0.5rem; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | height: 100vh; 9 | } 10 | 11 | .main { 12 | padding: 5rem 0; 13 | flex: 1; 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: center; 17 | align-items: center; 18 | } 19 | 20 | .footer { 21 | width: 100%; 22 | height: 100px; 23 | border-top: 1px solid #eaeaea; 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | } 28 | 29 | .footer a { 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | flex-grow: 1; 34 | } 35 | 36 | .title a { 37 | color: #0070f3; 38 | text-decoration: none; 39 | } 40 | 41 | .title a:hover, 42 | .title a:focus, 43 | .title a:active { 44 | text-decoration: underline; 45 | } 46 | 47 | .title { 48 | margin: 0; 49 | line-height: 1.15; 50 | font-size: 4rem; 51 | } 52 | 53 | .title, 54 | .description { 55 | text-align: center; 56 | } 57 | 58 | .description { 59 | line-height: 1.5; 60 | font-size: 1.5rem; 61 | } 62 | 63 | .code { 64 | background: #fafafa; 65 | border-radius: 5px; 66 | padding: 0.75rem; 67 | font-size: 1.1rem; 68 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 69 | Bitstream Vera Sans Mono, Courier New, monospace; 70 | } 71 | 72 | .grid { 73 | display: flex; 74 | align-items: center; 75 | justify-content: center; 76 | flex-wrap: wrap; 77 | max-width: 800px; 78 | margin-top: 3rem; 79 | } 80 | 81 | .card { 82 | margin: 1rem; 83 | padding: 1.5rem; 84 | text-align: left; 85 | color: inherit; 86 | text-decoration: none; 87 | border: 1px solid #eaeaea; 88 | border-radius: 10px; 89 | transition: color 0.15s ease, border-color 0.15s ease; 90 | width: 45%; 91 | } 92 | 93 | .card:hover, 94 | .card:focus, 95 | .card:active { 96 | color: #0070f3; 97 | border-color: #0070f3; 98 | } 99 | 100 | .card h2 { 101 | margin: 0 0 1rem 0; 102 | font-size: 1.5rem; 103 | } 104 | 105 | .card p { 106 | margin: 0; 107 | font-size: 1.25rem; 108 | line-height: 1.5; 109 | } 110 | 111 | .logo { 112 | height: 1em; 113 | margin-left: 0.5rem; 114 | } 115 | 116 | @media (max-width: 600px) { 117 | .grid { 118 | width: 100%; 119 | flex-direction: column; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /stories/assets/stackalt.svg: -------------------------------------------------------------------------------- 1 | illustration/stackalt -------------------------------------------------------------------------------- /stories/Page.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { Header } from './Header'; 5 | import './page.css'; 6 | 7 | export const Page = ({ user, onLogin, onLogout, onCreateAccount }) => ( 8 |
9 |
10 | 11 |
12 |

Pages in Storybook

13 |

14 | We recommend building UIs with a{' '} 15 | 16 | component-driven 17 | {' '} 18 | process starting with atomic components and ending with pages. 19 |

20 |

21 | Render pages with mock data. This makes it easy to build and review page states without 22 | needing to navigate to them in your app. Here are some handy patterns for managing page data 23 | in Storybook: 24 |

25 |
    26 |
  • 27 | Use a higher-level connected component. Storybook helps you compose such data from the 28 | "args" of child component stories 29 |
  • 30 |
  • 31 | Assemble data in the page component from your services. You can mock these services out 32 | using Storybook. 33 |
  • 34 |
35 |

36 | Get a guided tutorial on component-driven development at{' '} 37 | 38 | Storybook tutorials 39 | 40 | . Read more in the{' '} 41 | 42 | docs 43 | 44 | . 45 |

46 |
47 | Tip Adjust the width of the canvas with the{' '} 48 | 49 | 50 | 55 | 56 | 57 | Viewports addon in the toolbar 58 |
59 |
60 |
61 | ); 62 | Page.propTypes = { 63 | user: PropTypes.shape({}), 64 | onLogin: PropTypes.func.isRequired, 65 | onLogout: PropTypes.func.isRequired, 66 | onCreateAccount: PropTypes.func.isRequired, 67 | }; 68 | 69 | Page.defaultProps = { 70 | user: null, 71 | }; 72 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import Image from 'next/image' 3 | import styles from '../styles/Home.module.css' 4 | 5 | export async function getServerSideProps() { 6 | const res = await fetch(`http://localhost:3000/api/hello`) 7 | const data = await res.json() 8 | 9 | if (!data) { 10 | return { 11 | notFound: true, 12 | } 13 | } 14 | 15 | return { 16 | props: data, // will be passed to the page component as props 17 | } 18 | } 19 | 20 | export default function Home({ name }) { 21 | return ( 22 | 83 | ) 84 | } 85 | -------------------------------------------------------------------------------- /stories/Introduction.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/addon-docs'; 2 | import Code from './assets/code-brackets.svg'; 3 | import Colors from './assets/colors.svg'; 4 | import Comments from './assets/comments.svg'; 5 | import Direction from './assets/direction.svg'; 6 | import Flow from './assets/flow.svg'; 7 | import Plugin from './assets/plugin.svg'; 8 | import Repo from './assets/repo.svg'; 9 | import StackAlt from './assets/stackalt.svg'; 10 | 11 | 12 | 13 | 116 | 117 | # Welcome to Storybook 118 | 119 | Storybook helps you build UI components in isolation from your app's business logic, data, and context. 120 | That makes it easy to develop hard-to-reach states. Save these UI states as **stories** to revisit during development, testing, or QA. 121 | 122 | Browse example stories now by navigating to them in the sidebar. 123 | View their code in the `src/stories` directory to learn how they work. 124 | We recommend building UIs with a [**component-driven**](https://componentdriven.org) process starting with atomic components and ending with pages. 125 | 126 |
Configure
127 | 128 | 174 | 175 |
Learn
176 | 177 | 207 | 208 |
209 | TipEdit the Markdown in{' '} 210 | src/stories/Introduction.stories.mdx 211 |
212 | -------------------------------------------------------------------------------- /stories/assets/colors.svg: -------------------------------------------------------------------------------- 1 | illustration/colors -------------------------------------------------------------------------------- /public/mockServiceWorker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | 4 | /** 5 | * Mock Service Worker (0.35.0). 6 | * @see https://github.com/mswjs/msw 7 | * - Please do NOT modify this file. 8 | * - Please do NOT serve this file on production. 9 | */ 10 | 11 | const INTEGRITY_CHECKSUM = 'f0a916b13c8acc2b526a03a6d26df85f' 12 | const bypassHeaderName = 'x-msw-bypass' 13 | const activeClientIds = new Set() 14 | 15 | self.addEventListener('install', function () { 16 | return self.skipWaiting() 17 | }) 18 | 19 | self.addEventListener('activate', async function (event) { 20 | return self.clients.claim() 21 | }) 22 | 23 | self.addEventListener('message', async function (event) { 24 | const clientId = event.source.id 25 | 26 | if (!clientId || !self.clients) { 27 | return 28 | } 29 | 30 | const client = await self.clients.get(clientId) 31 | 32 | if (!client) { 33 | return 34 | } 35 | 36 | const allClients = await self.clients.matchAll() 37 | 38 | switch (event.data) { 39 | case 'KEEPALIVE_REQUEST': { 40 | sendToClient(client, { 41 | type: 'KEEPALIVE_RESPONSE', 42 | }) 43 | break 44 | } 45 | 46 | case 'INTEGRITY_CHECK_REQUEST': { 47 | sendToClient(client, { 48 | type: 'INTEGRITY_CHECK_RESPONSE', 49 | payload: INTEGRITY_CHECKSUM, 50 | }) 51 | break 52 | } 53 | 54 | case 'MOCK_ACTIVATE': { 55 | activeClientIds.add(clientId) 56 | 57 | sendToClient(client, { 58 | type: 'MOCKING_ENABLED', 59 | payload: true, 60 | }) 61 | break 62 | } 63 | 64 | case 'MOCK_DEACTIVATE': { 65 | activeClientIds.delete(clientId) 66 | break 67 | } 68 | 69 | case 'CLIENT_CLOSED': { 70 | activeClientIds.delete(clientId) 71 | 72 | const remainingClients = allClients.filter((client) => { 73 | return client.id !== clientId 74 | }) 75 | 76 | // Unregister itself when there are no more clients 77 | if (remainingClients.length === 0) { 78 | self.registration.unregister() 79 | } 80 | 81 | break 82 | } 83 | } 84 | }) 85 | 86 | // Resolve the "master" client for the given event. 87 | // Client that issues a request doesn't necessarily equal the client 88 | // that registered the worker. It's with the latter the worker should 89 | // communicate with during the response resolving phase. 90 | async function resolveMasterClient(event) { 91 | const client = await self.clients.get(event.clientId) 92 | 93 | if (client.frameType === 'top-level') { 94 | return client 95 | } 96 | 97 | const allClients = await self.clients.matchAll() 98 | 99 | return allClients 100 | .filter((client) => { 101 | // Get only those clients that are currently visible. 102 | return client.visibilityState === 'visible' 103 | }) 104 | .find((client) => { 105 | // Find the client ID that's recorded in the 106 | // set of clients that have registered the worker. 107 | return activeClientIds.has(client.id) 108 | }) 109 | } 110 | 111 | async function handleRequest(event, requestId) { 112 | const client = await resolveMasterClient(event) 113 | const response = await getResponse(event, client, requestId) 114 | 115 | // Send back the response clone for the "response:*" life-cycle events. 116 | // Ensure MSW is active and ready to handle the message, otherwise 117 | // this message will pend indefinitely. 118 | if (client && activeClientIds.has(client.id)) { 119 | ;(async function () { 120 | const clonedResponse = response.clone() 121 | sendToClient(client, { 122 | type: 'RESPONSE', 123 | payload: { 124 | requestId, 125 | type: clonedResponse.type, 126 | ok: clonedResponse.ok, 127 | status: clonedResponse.status, 128 | statusText: clonedResponse.statusText, 129 | body: 130 | clonedResponse.body === null ? null : await clonedResponse.text(), 131 | headers: serializeHeaders(clonedResponse.headers), 132 | redirected: clonedResponse.redirected, 133 | }, 134 | }) 135 | })() 136 | } 137 | 138 | return response 139 | } 140 | 141 | async function getResponse(event, client, requestId) { 142 | const { request } = event 143 | const requestClone = request.clone() 144 | const getOriginalResponse = () => fetch(requestClone) 145 | 146 | // Bypass mocking when the request client is not active. 147 | if (!client) { 148 | return getOriginalResponse() 149 | } 150 | 151 | // Bypass initial page load requests (i.e. static assets). 152 | // The absence of the immediate/parent client in the map of the active clients 153 | // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet 154 | // and is not ready to handle requests. 155 | if (!activeClientIds.has(client.id)) { 156 | return await getOriginalResponse() 157 | } 158 | 159 | // Bypass requests with the explicit bypass header 160 | if (requestClone.headers.get(bypassHeaderName) === 'true') { 161 | const cleanRequestHeaders = serializeHeaders(requestClone.headers) 162 | 163 | // Remove the bypass header to comply with the CORS preflight check. 164 | delete cleanRequestHeaders[bypassHeaderName] 165 | 166 | const originalRequest = new Request(requestClone, { 167 | headers: new Headers(cleanRequestHeaders), 168 | }) 169 | 170 | return fetch(originalRequest) 171 | } 172 | 173 | // Send the request to the client-side MSW. 174 | const reqHeaders = serializeHeaders(request.headers) 175 | const body = await request.text() 176 | 177 | const clientMessage = await sendToClient(client, { 178 | type: 'REQUEST', 179 | payload: { 180 | id: requestId, 181 | url: request.url, 182 | method: request.method, 183 | headers: reqHeaders, 184 | cache: request.cache, 185 | mode: request.mode, 186 | credentials: request.credentials, 187 | destination: request.destination, 188 | integrity: request.integrity, 189 | redirect: request.redirect, 190 | referrer: request.referrer, 191 | referrerPolicy: request.referrerPolicy, 192 | body, 193 | bodyUsed: request.bodyUsed, 194 | keepalive: request.keepalive, 195 | }, 196 | }) 197 | 198 | switch (clientMessage.type) { 199 | case 'MOCK_SUCCESS': { 200 | return delayPromise( 201 | () => respondWithMock(clientMessage), 202 | clientMessage.payload.delay, 203 | ) 204 | } 205 | 206 | case 'MOCK_NOT_FOUND': { 207 | return getOriginalResponse() 208 | } 209 | 210 | case 'NETWORK_ERROR': { 211 | const { name, message } = clientMessage.payload 212 | const networkError = new Error(message) 213 | networkError.name = name 214 | 215 | // Rejecting a request Promise emulates a network error. 216 | throw networkError 217 | } 218 | 219 | case 'INTERNAL_ERROR': { 220 | const parsedBody = JSON.parse(clientMessage.payload.body) 221 | 222 | console.error( 223 | `\ 224 | [MSW] Uncaught exception in the request handler for "%s %s": 225 | 226 | ${parsedBody.location} 227 | 228 | This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error, as it indicates a mistake in your code. If you wish to mock an error response, please see this guide: https://mswjs.io/docs/recipes/mocking-error-responses\ 229 | `, 230 | request.method, 231 | request.url, 232 | ) 233 | 234 | return respondWithMock(clientMessage) 235 | } 236 | } 237 | 238 | return getOriginalResponse() 239 | } 240 | 241 | self.addEventListener('fetch', function (event) { 242 | const { request } = event 243 | const accept = request.headers.get('accept') || '' 244 | 245 | // Bypass server-sent events. 246 | if (accept.includes('text/event-stream')) { 247 | return 248 | } 249 | 250 | // Bypass navigation requests. 251 | if (request.mode === 'navigate') { 252 | return 253 | } 254 | 255 | // Opening the DevTools triggers the "only-if-cached" request 256 | // that cannot be handled by the worker. Bypass such requests. 257 | if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { 258 | return 259 | } 260 | 261 | // Bypass all requests when there are no active clients. 262 | // Prevents the self-unregistered worked from handling requests 263 | // after it's been deleted (still remains active until the next reload). 264 | if (activeClientIds.size === 0) { 265 | return 266 | } 267 | 268 | const requestId = uuidv4() 269 | 270 | return event.respondWith( 271 | handleRequest(event, requestId).catch((error) => { 272 | if (error.name === 'NetworkError') { 273 | console.warn( 274 | '[MSW] Successfully emulated a network error for the "%s %s" request.', 275 | request.method, 276 | request.url, 277 | ) 278 | return 279 | } 280 | 281 | // At this point, any exception indicates an issue with the original request/response. 282 | console.error( 283 | `\ 284 | [MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`, 285 | request.method, 286 | request.url, 287 | `${error.name}: ${error.message}`, 288 | ) 289 | }), 290 | ) 291 | }) 292 | 293 | function serializeHeaders(headers) { 294 | const reqHeaders = {} 295 | headers.forEach((value, name) => { 296 | reqHeaders[name] = reqHeaders[name] 297 | ? [].concat(reqHeaders[name]).concat(value) 298 | : value 299 | }) 300 | return reqHeaders 301 | } 302 | 303 | function sendToClient(client, message) { 304 | return new Promise((resolve, reject) => { 305 | const channel = new MessageChannel() 306 | 307 | channel.port1.onmessage = (event) => { 308 | if (event.data && event.data.error) { 309 | return reject(event.data.error) 310 | } 311 | 312 | resolve(event.data) 313 | } 314 | 315 | client.postMessage(JSON.stringify(message), [channel.port2]) 316 | }) 317 | } 318 | 319 | function delayPromise(cb, duration) { 320 | return new Promise((resolve) => { 321 | setTimeout(() => resolve(cb()), duration) 322 | }) 323 | } 324 | 325 | function respondWithMock(clientMessage) { 326 | return new Response(clientMessage.payload.body, { 327 | ...clientMessage.payload, 328 | headers: clientMessage.payload.headers, 329 | }) 330 | } 331 | 332 | function uuidv4() { 333 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 334 | const r = (Math.random() * 16) | 0 335 | const v = c == 'x' ? r : (r & 0x3) | 0x8 336 | return v.toString(16) 337 | }) 338 | } 339 | --------------------------------------------------------------------------------