├── .github └── workflows │ └── release.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .storybook ├── local-preset.js ├── main.ts └── preview.ts ├── CHANGELOG.md ├── LICENSE ├── README.md ├── manager.js ├── package-lock.json ├── package.json ├── preview.js ├── preview.png ├── scripts ├── eject-typescript.js ├── prepublish-checks.js └── welcome.js ├── src ├── Docs.mdx ├── Panel.tsx ├── Title.tsx ├── components │ └── PanelContent.tsx ├── constants.ts ├── index.ts ├── manager.tsx ├── preview.ts ├── stories │ ├── Display.stories.tsx │ ├── Display.tsx │ ├── DisplayLocation.stories.ts │ ├── DisplayLocation.tsx │ ├── DisplayLocations.stories.ts │ └── DisplayLocations.tsx ├── types.ts └── withApolloClient.tsx ├── tsconfig.json ├── tsup.config.ts └── vite.config.ts /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: [push] 4 | 5 | jobs: 6 | release: 7 | runs-on: ubuntu-latest 8 | if: ${{ !contains(github.event.head_commit.message, 'ci skip') && !contains(github.event.head_commit.message, 'skip ci') }} 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | - name: Prepare repository 13 | run: git fetch --unshallow --tags 14 | 15 | - name: Use Node.js 18.x 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: 18.x 19 | 20 | - name: Install dependencies 21 | run: npm install --ignore-scripts 22 | 23 | - name: Create Release 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 27 | run: | 28 | npm run release 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | storybook-static/ 4 | build-storybook.log 5 | .DS_Store 6 | .env -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.storybook/local-preset.js: -------------------------------------------------------------------------------- 1 | /** 2 | * to load the built addon in this test Storybook 3 | */ 4 | function previewAnnotations(entry = []) { 5 | return [...entry, require.resolve("../dist/preview.js")]; 6 | } 7 | 8 | function managerEntries(entry = []) { 9 | return [...entry, require.resolve("../dist/manager.js")]; 10 | } 11 | 12 | module.exports = { 13 | managerEntries, 14 | previewAnnotations, 15 | }; 16 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from "@storybook/react-vite"; 2 | const config: StorybookConfig = { 3 | stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], 4 | addons: [ 5 | "@storybook/addon-links", 6 | "@storybook/addon-essentials", 7 | "@storybook/addon-interactions", 8 | "./local-preset.js", 9 | ], 10 | framework: { 11 | name: "@storybook/react-vite", 12 | options: {}, 13 | }, 14 | docs: { 15 | autodocs: "tag", 16 | }, 17 | }; 18 | export default config; 19 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from "@storybook/react"; 2 | 3 | const preview: Preview = { 4 | parameters: { 5 | controls: { 6 | matchers: { 7 | color: /(background|color)$/i, 8 | date: /Date$/, 9 | }, 10 | }, 11 | }, 12 | initialGlobals: { 13 | background: { value: "light" }, 14 | }, 15 | }; 16 | 17 | export default preview; 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v8.1.2 (Fri Feb 21 2025) 2 | 3 | #### 🐛 Bug Fix 4 | 5 | - fix: react 18 issues [#131](https://github.com/lifeiscontent/storybook-addon-apollo-client/pull/131) ([@lifeiscontent](https://github.com/lifeiscontent)) 6 | 7 | #### Authors: 1 8 | 9 | - Aaron ([@lifeiscontent](https://github.com/lifeiscontent)) 10 | 11 | --- 12 | 13 | # v8.1.1 (Sun Feb 16 2025) 14 | 15 | #### 🐛 Bug Fix 16 | 17 | - chore: add variable matcher example [#129](https://github.com/lifeiscontent/storybook-addon-apollo-client/pull/129) ([@lifeiscontent](https://github.com/lifeiscontent)) 18 | 19 | #### Authors: 1 20 | 21 | - Aaron ([@lifeiscontent](https://github.com/lifeiscontent)) 22 | 23 | --- 24 | 25 | # v8.1.0 (Sat Feb 15 2025) 26 | 27 | #### 🚀 Enhancement 28 | 29 | - fix: react 18 [#128](https://github.com/lifeiscontent/storybook-addon-apollo-client/pull/128) ([@lifeiscontent](https://github.com/lifeiscontent)) 30 | 31 | #### Authors: 1 32 | 33 | - Aaron ([@lifeiscontent](https://github.com/lifeiscontent)) 34 | 35 | --- 36 | 37 | # v8.0.0 (Thu Jan 23 2025) 38 | 39 | #### 💥 Breaking Change 40 | 41 | - feat: react 19 support [#125](https://github.com/lifeiscontent/storybook-addon-apollo-client/pull/125) ([@lifeiscontent](https://github.com/lifeiscontent) [@yannbf](https://github.com/yannbf)) 42 | 43 | #### 🐛 Bug Fix 44 | 45 | - Fix React 19 support [#126](https://github.com/lifeiscontent/storybook-addon-apollo-client/pull/126) ([@yannbf](https://github.com/yannbf)) 46 | 47 | #### Authors: 2 48 | 49 | - Aaron ([@lifeiscontent](https://github.com/lifeiscontent)) 50 | - Yann Braga ([@yannbf](https://github.com/yannbf)) 51 | 52 | --- 53 | 54 | # v7.3.0 (Fri Apr 26 2024) 55 | 56 | #### 🚀 Enhancement 57 | 58 | - build(deps-dev): bump vite from 5.1.6 to 5.1.7 [#115](https://github.com/lifeiscontent/storybook-addon-apollo-client/pull/115) ([@dependabot[bot]](https://github.com/dependabot[bot])) 59 | 60 | #### Authors: 1 61 | 62 | - [@dependabot[bot]](https://github.com/dependabot[bot]) 63 | 64 | --- 65 | 66 | # v7.1.0 (Fri Apr 26 2024) 67 | 68 | #### 🚀 Enhancement 69 | 70 | - chore: fix defaults [#114](https://github.com/lifeiscontent/storybook-addon-apollo-client/pull/114) ([@lifeiscontent](https://github.com/lifeiscontent)) 71 | 72 | #### Authors: 1 73 | 74 | - Aaron ([@lifeiscontent](https://github.com/lifeiscontent)) 75 | 76 | --- 77 | 78 | # v7.0.0 (Sun Apr 21 2024) 79 | 80 | #### 💥 Breaking Change 81 | 82 | - chore: migrate to addon kit [#112](https://github.com/lifeiscontent/storybook-addon-apollo-client/pull/112) ([@lifeiscontent](https://github.com/lifeiscontent)) 83 | 84 | #### Authors: 1 85 | 86 | - Aaron ([@lifeiscontent](https://github.com/lifeiscontent)) 87 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Storybook contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Storybook Addon Apollo Client 2 | 3 | Use Apollo Client in your Storybook stories. 4 | 5 | ## Versions 6 | 7 | - If you're using Apollo Client 2.x and Storybook 5.x use version 1.x 8 | - If you're using Apollo Client 2.x or 3.x and Storybook 6.x use version 4.x 9 | - If you're using Apollo Client 2.x or 3.x and Storybook 7.x use version 5.x 10 | - If you're using Apollo Client 2.x or 3.x and Storybook 8.x use version 7.x 11 | - If you're using Apollo Client 3.x and Storybook 8.3+ use version 8.x 12 | 13 | ## Known issues 14 | 15 | due to how MockedProvider works in Apollo, you will have to hard refresh when visiting sub stories (visiting stories that exist in the same file) to get the expected results, please upvote my comment here to see if we can get this fixed: https://github.com/apollographql/apollo-client/issues/9738#issuecomment-1606316338 16 | 17 | ## Install 18 | 19 | **pnpm** 20 | 21 | ```shell 22 | pnpm add -D storybook-addon-apollo-client 23 | ``` 24 | 25 | **yarn** 26 | 27 | ```shell 28 | yarn add -D storybook-addon-apollo-client 29 | ``` 30 | 31 | **npm** 32 | 33 | ```shell 34 | npm install -D storybook-addon-apollo-client 35 | ``` 36 | 37 | Add the addon to your configuration in `.storybook/main.ts` 38 | 39 | ```js 40 | export default { 41 | ...config, 42 | addons: [ 43 | ...yourAddons 44 | "storybook-addon-apollo-client", 45 | ], 46 | }; 47 | ``` 48 | 49 | ## 7.0 Features 50 | 51 | - removed `globalMocks` key to favor composition 52 | 53 | ### Migrate from 5.x+ to 7.x 54 | 55 | #### Example of < 7.x 56 | 57 | **preview.ts** 58 | 59 | ```js 60 | import { MockedProvider } from "@apollo/client/testing"; // Use for Apollo Version 3+ 61 | // import { MockedProvider } from "@apollo/react-testing"; // Use for Apollo Version < 3 62 | 63 | export const preview = { 64 | parameters: { 65 | apolloClient: { 66 | MockedProvider, 67 | globalMocks: [ 68 | // whatever mocks you want here 69 | ], 70 | }, 71 | }, 72 | }; 73 | ``` 74 | 75 | #### Example of 7.x 76 | 77 | **preview.ts** 78 | 79 | ```js 80 | // Whatever you want here, but not Apollo Client related 81 | ``` 82 | 83 | **component.stories.ts** 84 | 85 | ```ts 86 | import type { Meta } from "@storybook/react"; 87 | import { globalMocks } from "./globalMocks"; 88 | import { otherMocks } from "./otherMocks"; 89 | import { YourComponent, YOUR_QUERY } from "./component"; 90 | 91 | export const meta: Meta = { 92 | component: YourComponent, 93 | parameters: { 94 | apolloClient: { 95 | mocks: [ 96 | ...globalMocks, 97 | ...otherMocks, 98 | { 99 | request: { 100 | query: YOUR_QUERY, 101 | }, 102 | result: { 103 | data: { 104 | // your data here 105 | }, 106 | }, 107 | }, 108 | ], 109 | }, 110 | }, 111 | }; 112 | ``` 113 | 114 | ## Upgrading from a previous version below 6.x 115 | 116 | In previous versions, we had a decorator called `withApolloClient` this is no longer nesscessary. If you're upgrading from this API here are the following changes that you'll need to make: 117 | 118 | 1. remove all code referencing the deprecated withApolloClient decorator. 119 | 2. follow install instructions 120 | 121 | ## Writing your stories with queries 122 | 123 | ```jsx 124 | import DashboardPage, { DashboardPageQuery } from "."; 125 | 126 | export default { 127 | title: "My Story", 128 | }; 129 | 130 | export const Example = () => ; 131 | 132 | Example.parameters = { 133 | apolloClient: { 134 | mocks: [ 135 | { 136 | request: { 137 | query: DashboardPageQuery, 138 | }, 139 | result: { 140 | data: { 141 | viewer: null, 142 | }, 143 | }, 144 | }, 145 | ], 146 | }, 147 | }; 148 | ``` 149 | 150 | Read more about the options available for MockedProvider at https://www.apollographql.com/docs/react/development-testing/testing 151 | 152 | ### Usage 153 | 154 | In Storybook, click "Show Addons" and navigate to the "Apollo Client" tab. 155 | 156 | ![Addon UI Preview](preview.png) 157 | 158 | ## Example App 159 | 160 | To see real world usage of how to use this addon, check out the example app: 161 | 162 | https://github.com/lifeiscontent/realworld 163 | 164 | ## Loading State 165 | 166 | You can use the `delay` parameter to simulate loading state. 167 | 168 | ```js 169 | import DashboardPage, { DashboardPageQuery } from "."; 170 | 171 | export default { 172 | title: "My Story", 173 | }; 174 | 175 | export const Example = () => ; 176 | 177 | Example.parameters = { 178 | apolloClient: { 179 | mocks: [ 180 | { 181 | // Use `delay` parameter to increase loading time 182 | delay: 1000, 183 | request: { 184 | query: DashboardPageQuery, 185 | }, 186 | result: { 187 | data: {}, 188 | }, 189 | }, 190 | ], 191 | }, 192 | }; 193 | ``` 194 | 195 | ## Error State 196 | 197 | You can use the `error` parameter to create error state. 198 | 199 | ```js 200 | import DashboardPage, { DashboardPageQuery } from "."; 201 | 202 | export default { 203 | title: "My Story", 204 | }; 205 | 206 | export const Example = () => ; 207 | 208 | Example.parameters = { 209 | apolloClient: { 210 | mocks: [ 211 | { 212 | request: { 213 | query: DashboardPageQuery, 214 | }, 215 | error: new ApolloError("This is a mock network error"), 216 | }, 217 | ], 218 | }, 219 | }; 220 | ``` 221 | -------------------------------------------------------------------------------- /manager.js: -------------------------------------------------------------------------------- 1 | import "./dist/manager"; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storybook-addon-apollo-client", 3 | "version": "8.1.2", 4 | "description": "Use Apollo Client in your Storybook stories.", 5 | "keywords": [ 6 | "storybook-addon", 7 | "data-state", 8 | "apollo", 9 | "popular", 10 | "graphql", 11 | "storybook-addons" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/lifeiscontent/storybook-addon-apollo-client" 16 | }, 17 | "type": "module", 18 | "license": "MIT", 19 | "author": "Aaron Reisman ", 20 | "exports": { 21 | ".": { 22 | "types": "./dist/index.d.ts", 23 | "import": "./dist/index.js", 24 | "require": "./dist/index.cjs" 25 | }, 26 | "./preview": { 27 | "types": "./dist/index.d.ts", 28 | "import": "./dist/preview.js", 29 | "require": "./dist/preview.cjs" 30 | }, 31 | "./manager": "./dist/manager.js", 32 | "./package.json": "./package.json" 33 | }, 34 | "files": [ 35 | "dist/**/*", 36 | "README.md", 37 | "*.js", 38 | "*.d.ts" 39 | ], 40 | "scripts": { 41 | "build": "tsup", 42 | "build:watch": "npm run build -- --watch", 43 | "test": "echo \"Error: no test specified\" && exit 1", 44 | "start": "run-p build:watch \"storybook --quiet\"", 45 | "prerelease": "zx scripts/prepublish-checks.js", 46 | "format": "prettier --write .", 47 | "release": "npm run build && auto shipit", 48 | "storybook": "storybook dev -p 6006", 49 | "build-storybook": "storybook build" 50 | }, 51 | "devDependencies": { 52 | "@storybook/addon-essentials": "8.5.8", 53 | "@storybook/addon-interactions": "8.5.8", 54 | "@storybook/addon-links": "8.5.8", 55 | "@storybook/blocks": "8.5.8", 56 | "@storybook/react": "8.5.8", 57 | "@storybook/react-vite": "8.5.8", 58 | "@storybook/test": "8.5.8", 59 | "@types/node": "22.13.4", 60 | "@types/react": "19.0.10", 61 | "@types/react-dom": "19.0.4", 62 | "@vitejs/plugin-react": "4.3.4", 63 | "auto": "11.3.0", 64 | "boxen": "8.0.1", 65 | "dedent": "1.5.3", 66 | "npm-run-all2": "7.0.2", 67 | "prettier": "3.5.1", 68 | "prompts": "2.4.2", 69 | "react": "19.0.0", 70 | "storybook": "8.5.8", 71 | "tsup": "8.3.6", 72 | "typescript": "5.7.3", 73 | "vite": "6.1.1", 74 | "zx": "8.3.2" 75 | }, 76 | "peerDependencies": { 77 | "@apollo/client": "^3.0.0", 78 | "graphql": "^16.0.0", 79 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", 80 | "storybook": "^8.3.0" 81 | }, 82 | "publishConfig": { 83 | "access": "public" 84 | }, 85 | "bundler": { 86 | "exportEntries": [ 87 | "src/index.ts" 88 | ], 89 | "managerEntries": [ 90 | "src/manager.tsx" 91 | ], 92 | "previewEntries": [ 93 | "src/preview.ts" 94 | ], 95 | "nodeEntries": [] 96 | }, 97 | "storybook": { 98 | "displayName": "Apollo Client", 99 | "supportedFrameworks": [ 100 | "react" 101 | ], 102 | "icon": "https://raw.githubusercontent.com/apollographql/apollo-client-devtools/main/assets/apollo-wordmark.svg" 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /preview.js: -------------------------------------------------------------------------------- 1 | export * from "./dist/preview"; 2 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifeiscontent/storybook-addon-apollo-client/ab11fef9d74a85394622948c7fc8f9347a200203/preview.png -------------------------------------------------------------------------------- /scripts/eject-typescript.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | 3 | // Copy TS files and delete src 4 | await $`cp -r ./src ./srcTS`; 5 | await $`rm -rf ./src`; 6 | await $`mkdir ./src`; 7 | 8 | // Install Babel and TS preset 9 | console.log(chalk.green` 10 | 11 | 🔃 Installing dependencies... 12 | 13 | `); 14 | await $`npm install --save-dev @babel/cli @babel/preset-typescript --ignore-scripts`; 15 | 16 | // Convert TS code to JS 17 | await $`babel --no-babelrc --presets @babel/preset-typescript ./srcTS -d ./src --extensions \".js,.jsx,.ts,.tsx\" --ignore "./srcTS/typings.d.ts"`; 18 | 19 | // Format the newly created .js files 20 | console.log(chalk.green` 21 | 22 | 💅 Format the newly created .js files... 23 | 24 | `); 25 | await $`prettier --write ./src`; 26 | 27 | // Add in minimal files required for the TS build setup 28 | console.log(chalk.green` 29 | 30 | ➕ Add minimal files required for the TS build setup 31 | 32 | `); 33 | await $`prettier --write ./src`; 34 | await $`touch ./src/dummy.ts`; 35 | await $`printf "export {};" >> ./src/dummy.ts`; 36 | 37 | await $`touch ./src/typings.d.ts`; 38 | await $`printf 'declare module "global";' >> ./src/typings.d.ts`; 39 | 40 | // Clean up 41 | await $`rm -rf ./srcTS`; 42 | console.log(chalk.green` 43 | 44 | 🧹 Clean up... 45 | 46 | `); 47 | await $`npm uninstall @babel/cli @babel/preset-typescript --ignore-scripts`; 48 | 49 | console.log( 50 | chalk.green.bold` 51 | TypeScript Ejection complete!`, 52 | chalk.green` 53 | Addon code converted with JS. The TypeScript build setup is still available in case you want to adopt TypeScript in the future. 54 | `, 55 | ); 56 | -------------------------------------------------------------------------------- /scripts/prepublish-checks.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | 3 | import boxen from "boxen"; 4 | import dedent from "dedent"; 5 | import { readFile } from "node:fs/promises"; 6 | import { globalPackages as globalManagerPackages } from "storybook/internal/manager/globals"; 7 | import { globalPackages as globalPreviewPackages } from "storybook/internal/preview/globals"; 8 | 9 | const packageJson = await readFile("./package.json", "utf8").then(JSON.parse); 10 | 11 | const name = packageJson.name; 12 | const displayName = packageJson.storybook.displayName; 13 | 14 | let exitCode = 0; 15 | $.verbose = false; 16 | 17 | /** 18 | * Check that meta data has been updated 19 | */ 20 | if (name.includes("addon-kit") || displayName.includes("Addon Kit")) { 21 | console.error( 22 | boxen( 23 | dedent` 24 | ${chalk.red.bold("Missing metadata")} 25 | 26 | ${chalk.red(dedent`Your package name and/or displayName includes default values from the Addon Kit. 27 | The addon gallery filters out all such addons. 28 | 29 | Please configure appropriate metadata before publishing your addon. For more info, see: 30 | https://storybook.js.org/docs/react/addons/addon-catalog#addon-metadata`)}`, 31 | { padding: 1, borderColor: "red" }, 32 | ), 33 | ); 34 | 35 | exitCode = 1; 36 | } 37 | 38 | /** 39 | * Check that README has been updated 40 | */ 41 | const readmeTestStrings = 42 | "# Storybook Addon Kit|Click the \\*\\*Use this template\\*\\* button to get started.|https://user-images.githubusercontent.com/42671/106809879-35b32000-663a-11eb-9cdc-89f178b5273f.gif"; 43 | 44 | if ((await $`cat README.md | grep -E ${readmeTestStrings}`.exitCode) == 0) { 45 | console.error( 46 | boxen( 47 | dedent` 48 | ${chalk.red.bold("README not updated")} 49 | 50 | ${chalk.red(dedent`You are using the default README.md file that comes with the addon kit. 51 | Please update it to provide info on what your addon does and how to use it.`)} 52 | `, 53 | { padding: 1, borderColor: "red" }, 54 | ), 55 | ); 56 | 57 | exitCode = 1; 58 | } 59 | 60 | /** 61 | * Check that globalized packages are not incorrectly listed as peer dependencies 62 | */ 63 | const peerDependencies = Object.keys(packageJson.peerDependencies || {}); 64 | const globalPackages = [...globalManagerPackages, ...globalPreviewPackages]; 65 | peerDependencies.forEach((dependency) => { 66 | if (globalPackages.includes(dependency) && dependency !== "react") { 67 | console.error( 68 | boxen( 69 | dedent` 70 | ${chalk.red.bold("Unnecessary peer dependency")} 71 | 72 | ${chalk.red(dedent`You have a peer dependency on ${chalk.bold(dependency)} which is most likely unnecessary 73 | as that is provided by Storybook directly. 74 | Check the "bundling" section in README.md for more information. 75 | If you are absolutely sure you are doing it correct, you should remove this check from scripts/prepublish-checks.js.`)} 76 | `, 77 | { padding: 1, borderColor: "red" }, 78 | ), 79 | ); 80 | 81 | exitCode = 1; 82 | } 83 | }); 84 | 85 | process.exit(exitCode); 86 | -------------------------------------------------------------------------------- /scripts/welcome.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable eslint-comments/disable-enable-pair */ 2 | /* eslint-disable no-console */ 3 | import prompts from "prompts"; 4 | import { dedent } from "ts-dedent"; 5 | import { dirname, resolve } from "path"; 6 | import { readFile, writeFile } from "fs/promises"; 7 | import { execSync } from "child_process"; 8 | import { fileURLToPath } from "url"; 9 | 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = dirname(__filename); 12 | 13 | // CLI questions 14 | const questions = [ 15 | { 16 | type: "text", 17 | name: "authorName", 18 | initial: "", 19 | message: "What is the package author name?*", 20 | validate: (name) => (name === "" ? "Name can't be empty" : true), 21 | }, 22 | { 23 | type: "text", 24 | name: "authorEmail", 25 | initial: "", 26 | message: "What is the package author email?", 27 | }, 28 | { 29 | type: "text", 30 | name: "packageName", 31 | message: "What is the addon package name (eg: storybook-addon-something)?*", 32 | validate: (name) => (name === "" ? "Package name can't be empty" : true), 33 | }, 34 | { 35 | type: "text", 36 | name: "displayName", 37 | message: 38 | "What is the addon display name (this will be used in the addon catalog)?*", 39 | validate: (name) => 40 | name === "" 41 | ? "Display name can't be empty. For more info, see: https://storybook.js.org/docs/react/addons/addon-catalog#addon-metadata" 42 | : true, 43 | }, 44 | { 45 | type: "text", 46 | name: "addonDescription", 47 | initial: "", 48 | message: "Write a short description of the addon*", 49 | validate: (name) => (name === "" ? "Description can't be empty" : true), 50 | }, 51 | { 52 | type: "text", 53 | name: "repoUrl", 54 | message: "Git repo URL for your addon package (https://github.com/...)*", 55 | validate: (url) => (url === "" ? "URL can't be empty" : true), 56 | }, 57 | { 58 | type: "text", 59 | name: "addonIcon", 60 | initial: 61 | "https://user-images.githubusercontent.com/321738/63501763-88dbf600-c4cc-11e9-96cd-94adadc2fd72.png", 62 | message: "URL of your addon icon", 63 | }, 64 | { 65 | type: "list", 66 | name: "keywords", 67 | initial: "storybook-addons", 68 | message: "Enter addon keywords (comma separated)", 69 | separator: ",", 70 | format: (keywords) => 71 | keywords 72 | .concat(["storybook-addons"]) 73 | .map((k) => `"${k}"`) 74 | .join(", "), 75 | }, 76 | { 77 | type: "list", 78 | name: "supportedFrameworks", 79 | initial: 80 | "react, vue, angular, web-components, ember, html, svelte, preact, react-native", 81 | message: "List of frameworks you support (comma separated)?", 82 | separator: ",", 83 | format: (frameworks) => frameworks.map((k) => `"${k}"`).join(", "), 84 | }, 85 | ]; 86 | 87 | const REPLACE_TEMPLATES = { 88 | packageName: "storybook-addon-kit", 89 | addonDescription: "everything you need to build a Storybook addon", 90 | packageAuthor: "package-author", 91 | repoUrl: "https://github.com/storybookjs/storybook-addon-kit", 92 | keywords: `"storybook-addons"`, 93 | displayName: "Addon Kit", 94 | supportedFrameworks: `"supported-frameworks"`, 95 | }; 96 | 97 | const bold = (message) => `\u001b[1m${message}\u001b[22m`; 98 | const magenta = (message) => `\u001b[35m${message}\u001b[39m`; 99 | const blue = (message) => `\u001b[34m${message}\u001b[39m`; 100 | 101 | const main = async () => { 102 | console.log( 103 | bold( 104 | magenta( 105 | dedent` 106 | Welcome to Storybook addon-kit! 107 | Please answer the following questions while we prepare this project for you:\n 108 | `, 109 | ), 110 | ), 111 | ); 112 | 113 | const { 114 | authorName, 115 | authorEmail, 116 | packageName, 117 | addonDescription, 118 | repoUrl, 119 | displayName, 120 | keywords, 121 | supportedFrameworks, 122 | } = await prompts(questions); 123 | 124 | if (!authorName || !packageName) { 125 | console.log( 126 | `\nProcess canceled by the user. Feel free to run ${bold( 127 | "npm run postinstall", 128 | )} to execute the installation steps again!`, 129 | ); 130 | process.exit(0); 131 | } 132 | 133 | const authorField = authorName + (authorEmail ? ` <${authorEmail}>` : ""); 134 | 135 | const packageJson = resolve(__dirname, `../package.json`); 136 | 137 | console.log(`\n👷 Updating package.json...`); 138 | let packageJsonContents = await readFile(packageJson, "utf-8"); 139 | 140 | packageJsonContents = packageJsonContents 141 | .replace(REPLACE_TEMPLATES.packageName, packageName) 142 | .replace(REPLACE_TEMPLATES.addonDescription, addonDescription) 143 | .replace(REPLACE_TEMPLATES.packageAuthor, authorField) 144 | .replace(REPLACE_TEMPLATES.keywords, keywords) 145 | .replace(REPLACE_TEMPLATES.repoUrl, repoUrl) 146 | .replace(REPLACE_TEMPLATES.displayName, displayName) 147 | .replace(REPLACE_TEMPLATES.supportedFrameworks, supportedFrameworks) 148 | .replace(/\s*"postinstall".*node.*scripts\/welcome.js.*",/, ""); 149 | 150 | await writeFile(packageJson, packageJsonContents); 151 | 152 | console.log("📝 Updating the README..."); 153 | const readme = resolve(__dirname, `../README.md`); 154 | let readmeContents = await readFile(readme, "utf-8"); 155 | 156 | const regex = /<\!-- README START -->([\s\S]*)<\!-- README END -->/g; 157 | 158 | readmeContents = readmeContents.replace( 159 | regex, 160 | dedent` 161 | # Storybook Addon ${displayName} 162 | ${addonDescription} 163 | `, 164 | ); 165 | 166 | await writeFile(readme, readmeContents); 167 | 168 | console.log(`📦 Creating a commit...`); 169 | execSync('git add . && git commit -m "project setup" --no-verify'); 170 | 171 | console.log( 172 | dedent`\n 173 | 🚀 All done! Run \`npm run start\` to get started. 174 | 175 | Thanks for using this template, ${authorName.split(" ")[0]}! ❤️ 176 | 177 | Feel free to open issues in case there are bugs/feature requests at: 178 | 179 | ${bold(blue("https://github.com/storybookjs/addon-kit"))}\n 180 | `, 181 | ); 182 | }; 183 | 184 | main().catch((e) => console.log(`Something went wrong: ${e}`)); 185 | -------------------------------------------------------------------------------- /src/Docs.mdx: -------------------------------------------------------------------------------- 1 | import ReadMe from '../README.md?raw'; 2 | 3 | import { Markdown } from '@storybook/blocks'; 4 | 5 | {ReadMe} 8 | -------------------------------------------------------------------------------- /src/Panel.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import type { MockedResponse } from "@apollo/client/testing"; 3 | import type { OperationDefinitionNode } from "graphql"; 4 | 5 | import { AddonPanel, Form } from "storybook/internal/components"; 6 | import { STORY_CHANGED, STORY_RENDERED } from "storybook/internal/core-events"; 7 | import { useAddonState, useChannel } from "storybook/internal/manager-api"; 8 | import { Addon_RenderOptions } from "storybook/internal/types"; 9 | 10 | import { PanelContent } from "./components/PanelContent"; 11 | import { ADDON_ID, EVENTS } from "./constants"; 12 | import { ApolloClientAddonState } from "./types"; 13 | 14 | const getMockName = (mockedResponse: MockedResponse): string => { 15 | if (mockedResponse.request.operationName) { 16 | return mockedResponse.request.operationName; 17 | } 18 | 19 | const operationDefinition = mockedResponse.request.query.definitions.find( 20 | (definition): definition is OperationDefinitionNode => 21 | definition.kind === "OperationDefinition", 22 | ); 23 | 24 | if (operationDefinition && operationDefinition.name) { 25 | return operationDefinition.name.value; 26 | } 27 | 28 | return `Unnamed`; 29 | }; 30 | 31 | export const Panel: React.FC> = ({ 32 | active = false, 33 | }) => { 34 | const [activeMockIndex, setActiveMockIndex] = React.useState(-1); 35 | const [state, setState] = useAddonState(ADDON_ID, { 36 | mocks: [], 37 | queries: [], 38 | }); 39 | 40 | const emit = useChannel({ 41 | [EVENTS.RESULT]: (apolloAddonState: ApolloClientAddonState) => { 42 | setState(apolloAddonState); 43 | setActiveMockIndex((prev) => { 44 | if (prev === -1) { 45 | return apolloAddonState.mocks.length ? 0 : -1; 46 | } 47 | 48 | return apolloAddonState.mocks[prev] 49 | ? prev 50 | : apolloAddonState.mocks.length 51 | ? 0 52 | : -1; 53 | }); 54 | }, 55 | [STORY_RENDERED]: () => { 56 | emit(EVENTS.REQUEST); 57 | }, 58 | [STORY_CHANGED]: () => { 59 | emit(EVENTS.REQUEST); 60 | }, 61 | }); 62 | 63 | React.useEffect(() => { 64 | emit(EVENTS.REQUEST); 65 | }, []); 66 | 67 | const activeMock = state.mocks[activeMockIndex]; 68 | const activeQuery = state.queries[activeMockIndex]; 69 | 70 | return ( 71 | 72 | <> 73 | 74 | { 79 | const { value } = event.currentTarget; 80 | setActiveMockIndex(Number(value)); 81 | }} 82 | > 83 | 84 | {state.mocks.map((mockedResponse, index) => ( 85 | 88 | ))} 89 | 90 | 91 | 92 | 93 | 94 | ); 95 | }; 96 | -------------------------------------------------------------------------------- /src/Title.tsx: -------------------------------------------------------------------------------- 1 | import { useParameter } from "storybook/internal/manager-api"; 2 | import { PARAM_KEY } from "./constants"; 3 | 4 | export function Title() { 5 | const { mocks = [] } = useParameter(PARAM_KEY, { 6 | mocks: [], 7 | }); 8 | 9 | return mocks.length ? `Apollo Client (${mocks.length})` : "Apollo Client"; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/PanelContent.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import type { MockedResponse } from "@apollo/client/testing"; 4 | import { 5 | Button, 6 | Placeholder, 7 | SyntaxHighlighter, 8 | TabsState, 9 | } from "storybook/internal/components"; 10 | import { convert, styled, themes } from "storybook/internal/theming"; 11 | 12 | export const RequestDataButton = styled(Button)({ 13 | marginTop: "1rem", 14 | }); 15 | 16 | interface PanelContentProps { 17 | mock?: MockedResponse; 18 | query?: string; 19 | } 20 | 21 | function TabContent({ 22 | children, 23 | fallback, 24 | language, 25 | }: { 26 | children: any; 27 | fallback: string; 28 | language: "json" | "graphql"; 29 | }) { 30 | return children ? ( 31 | 32 | {language === "json" ? JSON.stringify(children, null, 2) : children} 33 | 34 | ) : ( 35 | {fallback} 36 | ); 37 | } 38 | 39 | /** 40 | * Checkout https://github.com/storybookjs/storybook/blob/next/code/addons/jest/src/components/Panel.tsx 41 | * for a real world example 42 | */ 43 | export const PanelContent: React.FC = ({ mock, query }) => { 44 | if (!mock) { 45 | return No mock selected; 46 | } 47 | 48 | return ( 49 | 54 |
59 | 60 | {query} 61 | 62 |
63 |
68 | 69 | {mock.request.variables} 70 | 71 |
72 |
77 | 78 | {mock.request.extensions} 79 | 80 |
81 |
86 | 87 | {mock.request.context} 88 | 89 |
90 |
95 | 96 | {mock.result} 97 | 98 |
99 |
104 | 105 | {mock.error} 106 | 107 |
108 |
109 | ); 110 | }; 111 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const ADDON_ID = "storybook/apollo-client"; 2 | export const PANEL_ID = `${ADDON_ID}/panel`; 3 | export const PARAM_KEY = `apolloClient`; 4 | 5 | export const EVENTS = { 6 | RESULT: `${ADDON_ID}/result`, 7 | REQUEST: `${ADDON_ID}/request`, 8 | CLEAR: `${ADDON_ID}/clear`, 9 | }; 10 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // make it work with --isolatedModules 2 | export default {}; 3 | -------------------------------------------------------------------------------- /src/manager.tsx: -------------------------------------------------------------------------------- 1 | import { addons, types } from "storybook/internal/manager-api"; 2 | import { ADDON_ID, PANEL_ID } from "./constants"; 3 | import { Panel } from "./Panel"; 4 | import { Title } from "./Title"; 5 | import { ApolloClientParameters } from "./types"; 6 | 7 | declare module "@storybook/react" { 8 | interface Parameters extends ApolloClientParameters {} 9 | } 10 | 11 | /** 12 | * Note: if you want to use JSX in this file, rename it to `manager.tsx` 13 | * and update the entry prop in tsup.config.ts to use "src/manager.tsx", 14 | */ 15 | 16 | // Register the addon 17 | addons.register(ADDON_ID, () => { 18 | // Register the panel 19 | addons.add(PANEL_ID, { 20 | type: types.PANEL, 21 | title: Title, 22 | match: ({ viewMode }) => viewMode === "story", 23 | render: Panel, 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from "@storybook/react"; 2 | import { withApolloClient } from "./withApolloClient"; 3 | 4 | const preview: Preview = { 5 | decorators: [withApolloClient], 6 | }; 7 | 8 | export default preview; 9 | -------------------------------------------------------------------------------- /src/stories/Display.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { Display } from "./Display"; 4 | 5 | const meta: Meta = { 6 | title: "Example/Display", 7 | component: Display, 8 | tags: ["autodocs"], 9 | }; 10 | 11 | export default meta; 12 | type Story = StoryObj; 13 | 14 | export const Default: Story = {}; 15 | -------------------------------------------------------------------------------- /src/stories/Display.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function Display() { 4 | return ( 5 |
6 |

Display

7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/stories/DisplayLocation.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { DisplayLocation, GET_LOCATION_QUERY } from "./DisplayLocation"; 4 | import { ApolloError } from "@apollo/client"; 5 | import { expect, fn, within } from "@storybook/test"; 6 | import { MockedResponse } from "@apollo/client/testing"; 7 | 8 | const meta: Meta = { 9 | title: "Example/DisplayLocation", 10 | component: DisplayLocation, 11 | args: { 12 | locationId: 1, 13 | }, 14 | tags: ["autodocs"], 15 | }; 16 | 17 | export default meta; 18 | type Story = StoryObj; 19 | 20 | export const WithResponse: Story = { 21 | parameters: { 22 | apolloClient: { 23 | mocks: [ 24 | { 25 | request: { 26 | query: GET_LOCATION_QUERY, 27 | variables: { 28 | locationId: 1, 29 | }, 30 | }, 31 | result: { 32 | data: { 33 | location: { 34 | id: 1, 35 | name: "Location 1", 36 | description: "This is a location", 37 | photo: "https://placehold.co/400x250", 38 | __typename: "Location", 39 | }, 40 | }, 41 | }, 42 | }, 43 | ], 44 | }, 45 | }, 46 | }; 47 | 48 | export const WithDelayedResponse: Story = { 49 | parameters: { 50 | apolloClient: { 51 | mocks: [ 52 | { 53 | delay: 1000, 54 | request: { 55 | query: GET_LOCATION_QUERY, 56 | variables: { 57 | locationId: 1, 58 | }, 59 | }, 60 | result: { 61 | data: { 62 | location: { 63 | id: 1, 64 | name: "Location 1", 65 | description: "This is a location", 66 | photo: "https://placehold.co/400x250", 67 | __typename: "Location", 68 | }, 69 | }, 70 | }, 71 | }, 72 | ], 73 | }, 74 | }, 75 | }; 76 | 77 | export const WithError: Story = { 78 | parameters: { 79 | apolloClient: { 80 | mocks: [ 81 | { 82 | request: { 83 | query: GET_LOCATION_QUERY, 84 | variables: { 85 | locationId: 1, 86 | }, 87 | }, 88 | error: new ApolloError({ errorMessage: "Could not get location" }), 89 | }, 90 | ], 91 | }, 92 | }, 93 | }; 94 | 95 | export const WithVariableMatcher: Story = { 96 | parameters: { 97 | apolloClient: { 98 | mocks: [ 99 | { 100 | request: { 101 | query: GET_LOCATION_QUERY, 102 | }, 103 | variableMatcher: fn(() => true), 104 | result: { 105 | data: { 106 | location: { 107 | id: 1, 108 | name: "Location 1", 109 | description: "This is a location", 110 | photo: "https://placehold.co/400x250", 111 | __typename: "Location", 112 | }, 113 | }, 114 | }, 115 | }, 116 | ], 117 | }, 118 | }, 119 | play: async ({ parameters, canvasElement }) => { 120 | const canvas = within(canvasElement); 121 | const mock = parameters.apolloClient!.mocks![0]!; 122 | await expect( 123 | // @ts-expect-error - storybook types are wrong 124 | canvas.getByRole("heading", { name: mock.result.data.location.name }), 125 | ).toBeInTheDocument(); 126 | await expect( 127 | canvas.getByRole("img", { name: "location-reference" }), 128 | // @ts-expect-error - storybook types are wrong 129 | ).toHaveAttribute("src", mock.result.data.location.photo); 130 | await expect( 131 | // @ts-expect-error - storybook types are wrong 132 | canvas.getByText(mock.result.data.location.description), 133 | ).toBeInTheDocument(); 134 | await expect(mock.variableMatcher).toHaveBeenCalledWith({ locationId: 1 }); 135 | }, 136 | }; 137 | -------------------------------------------------------------------------------- /src/stories/DisplayLocation.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useQuery, gql } from "@apollo/client"; 3 | 4 | export const GET_LOCATION_QUERY = gql` 5 | query GetLocation($locationId: Int!) { 6 | location(id: $locationId) { 7 | id 8 | name 9 | description 10 | photo 11 | } 12 | } 13 | `; 14 | 15 | export function DisplayLocation({ locationId }: { locationId: number }) { 16 | const { loading, error, data } = useQuery(GET_LOCATION_QUERY, { 17 | variables: { locationId }, 18 | }); 19 | 20 | if (loading) return

Loading...

; 21 | if (error) return

Error : {error.message}

; 22 | 23 | const { 24 | location: { name, description, photo } = { 25 | name: undefined, 26 | description: undefined, 27 | photo: undefined, 28 | }, 29 | } = data ?? {}; 30 | 31 | return ( 32 |
33 |

{name}

34 | location-reference 35 |
36 | About this location: 37 |

{description}

38 |
39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/stories/DisplayLocations.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { DisplayLocations, GET_LOCATIONS_QUERY } from "./DisplayLocations"; 4 | import { GET_LOCATION_QUERY } from "./DisplayLocation"; 5 | import { ApolloError } from "@apollo/client"; 6 | 7 | const meta: Meta = { 8 | title: "Example/DisplayLocations", 9 | component: DisplayLocations, 10 | args: { 11 | locationId: 1, 12 | }, 13 | tags: ["autodocs"], 14 | }; 15 | 16 | export default meta; 17 | type Story = StoryObj; 18 | 19 | export const WithResponse: Story = { 20 | parameters: { 21 | apolloClient: { 22 | mocks: [ 23 | { 24 | request: { 25 | query: GET_LOCATIONS_QUERY, 26 | }, 27 | result: { 28 | data: { 29 | locations: Array.from({ length: 3 }).map((_, index) => ({ 30 | id: index + 1, 31 | name: `Location ${index + 1}`, 32 | description: "This is a location", 33 | photo: "https://placehold.co/400x250", 34 | __typename: "Location", 35 | })), 36 | }, 37 | }, 38 | }, 39 | { 40 | delay: 1000, 41 | request: { 42 | query: GET_LOCATION_QUERY, 43 | variables: { 44 | locationId: 1, 45 | }, 46 | }, 47 | result: { 48 | data: { 49 | location: { 50 | id: 1, 51 | name: "Location 1", 52 | description: "This is a location", 53 | photo: "https://placehold.co/400x250", 54 | __typename: "Location", 55 | }, 56 | }, 57 | }, 58 | }, 59 | ], 60 | }, 61 | }, 62 | }; 63 | 64 | export const WithDelayedResponse: Story = { 65 | parameters: { 66 | apolloClient: { 67 | mocks: [ 68 | { 69 | delay: 1000, 70 | request: { 71 | query: GET_LOCATIONS_QUERY, 72 | }, 73 | result: { 74 | data: { 75 | locations: Array.from({ length: 3 }).map((_, index) => ({ 76 | id: index + 1, 77 | name: `Location ${index + 1}`, 78 | description: "This is a location", 79 | photo: "https://placehold.co/400x250", 80 | __typename: "Location", 81 | })), 82 | }, 83 | }, 84 | }, 85 | { 86 | delay: 1000, 87 | request: { 88 | query: GET_LOCATION_QUERY, 89 | variables: { 90 | locationId: 1, 91 | }, 92 | }, 93 | result: { 94 | data: { 95 | location: { 96 | id: 1, 97 | name: "Location 1", 98 | description: "This is a location", 99 | photo: "https://placehold.co/400x250", 100 | __typename: "Location", 101 | }, 102 | }, 103 | }, 104 | }, 105 | ], 106 | }, 107 | }, 108 | }; 109 | 110 | export const WithError: Story = { 111 | parameters: { 112 | apolloClient: { 113 | mocks: [ 114 | { 115 | request: { 116 | query: GET_LOCATIONS_QUERY, 117 | }, 118 | error: new ApolloError({ errorMessage: "Could not get locations" }), 119 | }, 120 | ], 121 | }, 122 | }, 123 | }; 124 | -------------------------------------------------------------------------------- /src/stories/DisplayLocations.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useQuery, gql } from "@apollo/client"; 3 | 4 | export const GET_LOCATIONS_QUERY = gql` 5 | query GetLocations { 6 | locations { 7 | id 8 | name 9 | description 10 | photo 11 | } 12 | } 13 | `; 14 | 15 | export function DisplayLocations() { 16 | const { loading, error, data } = useQuery(GET_LOCATIONS_QUERY); 17 | 18 | if (loading) return

Loading...

; 19 | if (error) return

Error : {error.message}

; 20 | 21 | return data.locations.map(({ id, name, description, photo }: any) => ( 22 |
23 |

{name}

24 | location-reference 25 |
26 | About this location: 27 |

{description}

28 |
29 |
30 | )); 31 | } 32 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | MockedResponse, 3 | MockedProviderProps, 4 | } from "@apollo/client/testing"; 5 | 6 | export type ApolloClientAddonState = { 7 | mocks: MockedResponse[]; 8 | queries: string[]; 9 | }; 10 | 11 | export type ApolloClientParameters = { 12 | apolloClient?: Partial>; 13 | }; 14 | -------------------------------------------------------------------------------- /src/withApolloClient.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useChannel } from "storybook/internal/preview-api"; 3 | import { EVENTS, PARAM_KEY } from "./constants"; 4 | import { print } from "graphql"; 5 | import { MockedProvider } from "@apollo/client/testing"; 6 | import { Decorator } from "@storybook/react"; 7 | 8 | export const withApolloClient: Decorator = (Story, context) => { 9 | const props = context.parameters[PARAM_KEY]; 10 | 11 | const emit = useChannel({ 12 | [EVENTS.REQUEST]: () => { 13 | emit(EVENTS.RESULT, { 14 | mocks: props?.mocks ?? [], 15 | queries: props?.mocks?.map((mock) => print(mock.request.query)) ?? [], 16 | }); 17 | }, 18 | [EVENTS.CLEAR]: () => { 19 | emit(EVENTS.RESULT, { 20 | mocks: [], 21 | queries: [], 22 | }); 23 | }, 24 | }); 25 | 26 | if (!props) { 27 | return ; 28 | } 29 | 30 | return ( 31 | 32 | 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "baseUrl": ".", 5 | "esModuleInterop": true, 6 | "experimentalDecorators": true, 7 | "incremental": false, 8 | "isolatedModules": true, 9 | "jsx": "react", 10 | "lib": ["es2020", "dom"], 11 | "module": "commonjs", 12 | "noImplicitAny": true, 13 | "noUncheckedIndexedAccess": true, 14 | "strict": true, 15 | "rootDir": "./src", 16 | "skipLibCheck": true, 17 | "target": "ES2020" 18 | }, 19 | "include": ["src/**/*"] 20 | } 21 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, type Options } from "tsup"; 2 | import { readFile } from "node:fs/promises"; 3 | import { globalPackages as globalManagerPackages } from "storybook/internal/manager/globals"; 4 | import { globalPackages as globalPreviewPackages } from "storybook/internal/preview/globals"; 5 | 6 | // The current browsers supported by Storybook v7 7 | const BROWSER_TARGET: Options["target"] = [ 8 | "chrome100", 9 | "safari15", 10 | "firefox91", 11 | ]; 12 | const NODE_TARGET: Options["target"] = ["node18"]; 13 | 14 | type BundlerConfig = { 15 | bundler?: { 16 | exportEntries?: string[]; 17 | nodeEntries?: string[]; 18 | managerEntries?: string[]; 19 | previewEntries?: string[]; 20 | }; 21 | }; 22 | 23 | export default defineConfig(async (options) => { 24 | // reading the three types of entries from package.json, which has the following structure: 25 | // { 26 | // ... 27 | // "bundler": { 28 | // "exportEntries": ["./src/index.ts"], 29 | // "managerEntries": ["./src/manager.ts"], 30 | // "previewEntries": ["./src/preview.ts"] 31 | // } 32 | // } 33 | const packageJson = (await readFile("./package.json", "utf8").then( 34 | JSON.parse, 35 | )) as BundlerConfig; 36 | const { 37 | bundler: { 38 | exportEntries = [], 39 | managerEntries = [], 40 | previewEntries = [], 41 | nodeEntries = [], 42 | } = {}, 43 | } = packageJson; 44 | 45 | const commonConfig: Options = { 46 | splitting: false, 47 | minify: !options.watch, 48 | treeshake: true, 49 | sourcemap: true, 50 | clean: options.watch ? false : true, 51 | }; 52 | 53 | const configs: Options[] = []; 54 | 55 | // export entries are entries meant to be manually imported by the user 56 | // they are not meant to be loaded by the manager or preview 57 | // they'll be usable in both node and browser environments, depending on which features and modules they depend on 58 | if (exportEntries.length) { 59 | configs.push({ 60 | ...commonConfig, 61 | entry: exportEntries, 62 | dts: { 63 | resolve: true, 64 | }, 65 | format: ["esm", "cjs"], 66 | target: [...BROWSER_TARGET, ...NODE_TARGET], 67 | platform: "neutral", 68 | external: [...globalManagerPackages, ...globalPreviewPackages], 69 | }); 70 | } 71 | 72 | // manager entries are entries meant to be loaded into the manager UI 73 | // they'll have manager-specific packages externalized and they won't be usable in node 74 | // they won't have types generated for them as they're usually loaded automatically by Storybook 75 | if (managerEntries.length) { 76 | configs.push({ 77 | ...commonConfig, 78 | entry: managerEntries, 79 | format: ["esm"], 80 | target: BROWSER_TARGET, 81 | platform: "browser", 82 | external: globalManagerPackages, 83 | }); 84 | } 85 | 86 | // preview entries are entries meant to be loaded into the preview iframe 87 | // they'll have preview-specific packages externalized and they won't be usable in node 88 | // they'll have types generated for them so they can be imported when setting up Portable Stories 89 | if (previewEntries.length) { 90 | configs.push({ 91 | ...commonConfig, 92 | entry: previewEntries, 93 | dts: { 94 | resolve: true, 95 | }, 96 | format: ["esm", "cjs"], 97 | target: BROWSER_TARGET, 98 | platform: "browser", 99 | external: globalPreviewPackages, 100 | }); 101 | } 102 | 103 | // node entries are entries meant to be used in node-only 104 | // this is useful for presets, which are loaded by Storybook when setting up configurations 105 | // they won't have types generated for them as they're usually loaded automatically by Storybook 106 | if (nodeEntries.length) { 107 | configs.push({ 108 | ...commonConfig, 109 | entry: nodeEntries, 110 | format: ["cjs"], 111 | target: NODE_TARGET, 112 | platform: "node", 113 | }); 114 | } 115 | 116 | return configs; 117 | }); 118 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | --------------------------------------------------------------------------------