├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── config.js ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.css ├── App.js ├── App.test.js ├── dev-tools ├── dev-tools.css ├── dev-tools.js ├── dev-tools.local-example.js ├── feature-toggles.js └── load.js ├── feature-toggles.js ├── index.css ├── index.js ├── logo.svg └── setupTests.js /.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 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | *.local.* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # App DevTools 2 | 3 | A little demo of how you might implement custom App Developer Tools. 4 | 5 | In this example, we're using custom DevTools to make it easy to enable/disable 6 | ["feature toggles"](https://en.wikipedia.org/wiki/Feature_toggle). Please note 7 | that the concept of App DevTools has WAY more applicability than just feature 8 | toggles. 9 | 10 | Files to take a look at to learn how the feature toggles are implemented for 11 | this app: 12 | 13 | - `public/config.js` 14 | - `public/index.html` 15 | - `src/feature-toggles.js` 16 | - `src/App.js` 17 | 18 | Once you understand how the feature toggles work, then check out how the dev 19 | tools are installed and how they allow you to interact with the feature toggles 20 | by opening `src/index.js` and files in `src/dev-tools/`. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app-dev-tools", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "react": "^16.12.0", 10 | "react-dom": "^16.12.0", 11 | "react-scripts": "3.4.0" 12 | }, 13 | "scripts": { 14 | "start": "react-scripts start", 15 | "build": "react-scripts build", 16 | "test": "react-scripts test", 17 | "eject": "react-scripts eject" 18 | }, 19 | "eslintConfig": { 20 | "extends": "react-app" 21 | }, 22 | "browserslist": { 23 | "production": [ 24 | ">0.2%", 25 | "not dead", 26 | "not op_mini all" 27 | ], 28 | "development": [ 29 | "last 1 chrome version", 30 | "last 1 firefox version", 31 | "last 1 safari version" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /public/config.js: -------------------------------------------------------------------------------- 1 | // pretend that this is dynamically generated via a server 2 | 3 | window.APP_CONFIG = { 4 | featureToggles: {}, 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kentcdodds/app-dev-tools/ea916e44c0dec16cf2df4f0eb520d635d0896a3c/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 33 | 34 | 35 | 36 | 37 |
38 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kentcdodds/app-dev-tools/ea916e44c0dec16cf2df4f0eb520d635d0896a3c/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kentcdodds/app-dev-tools/ea916e44c0dec16cf2df4f0eb520d635d0896a3c/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | font-size: 40vmin; 7 | height: 40vmin; 8 | pointer-events: none; 9 | } 10 | 11 | @media (prefers-reduced-motion: no-preference) { 12 | .App-logo { 13 | animation: App-logo-spin infinite 20s linear; 14 | } 15 | } 16 | 17 | .App-header { 18 | background-color: #282c34; 19 | min-height: 100vh; 20 | display: flex; 21 | flex-direction: column; 22 | align-items: center; 23 | justify-content: center; 24 | font-size: calc(10px + 2vmin); 25 | color: white; 26 | } 27 | 28 | a { 29 | color: #61dafb; 30 | } 31 | 32 | @keyframes App-logo-spin { 33 | from { 34 | transform: rotate(0deg); 35 | } 36 | to { 37 | transform: rotate(360deg); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import featureToggles from './feature-toggles' 3 | import logo from './logo.svg' 4 | import './App.css' 5 | 6 | function App() { 7 | return ( 8 |
9 |
10 | {featureToggles.tacos ? ( 11 | 12 | 🌮 13 | 14 | ) : ( 15 | logo 16 | )} 17 | 27 | Learn about {featureToggles.tacos ? ' Tacos' : ' React'} 28 | 29 |

30 | This is just a demo for DevTools.{' '} 31 | {window.devToolsEnabled ? ( 32 | <> 33 | DevTools are enabled. The UI for the DevTools appears in the 34 | bottom left. Add{' '} 35 | 36 | "?dev-tools=false" 37 | {' '} 38 | to the URL to explicitely disable them (though in production they 39 | are disabled by default) 40 | 41 | ) : ( 42 | <> 43 | DevTools are disabled. Add{' '} 44 | 45 | "?dev-tools=true" 46 | {' '} 47 | to the URL to explicitely enable them (though in development they 48 | are enabled by default). The DevTools UI will appear in the bottom 49 | left. 50 | 51 | )} 52 |

53 | {process.env.NODE_ENV === 'production' ? ( 54 |

55 | Notice that these work even in production! But it doesn't cost 56 | anything to have them available for users who don't use them. Read 57 | the blog post to learn how. 58 |

59 | ) : null} 60 |

61 | 62 | View Code on GitHub 63 | 64 |

65 |

66 | Read{' '} 67 | 68 | the blog post. 69 | 70 |

71 |

72 | Watch{' '} 73 | 74 | the video. 75 | 76 |

77 |
78 |
79 | ) 80 | } 81 | 82 | export default App 83 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {render} from '@testing-library/react' 3 | import App from './App' 4 | 5 | test('renders learn react link', () => { 6 | const {getByText} = render() 7 | const linkElement = getByText(/learn about react/i) 8 | expect(linkElement).toBeInTheDocument() 9 | }) 10 | -------------------------------------------------------------------------------- /src/dev-tools/dev-tools.css: -------------------------------------------------------------------------------- 1 | #dev-tools { 2 | position: absolute; 3 | bottom: 0; 4 | background: black; 5 | opacity: 0.4; 6 | color: white; 7 | width: 100%; 8 | padding: 20px; 9 | height: 60px; 10 | width: 60px; 11 | transition: all 0.3s; 12 | } 13 | 14 | #dev-tools:hover { 15 | height: 300px; 16 | width: 100%; 17 | opacity: 0.9; 18 | } 19 | 20 | #dev-tools .tools { 21 | display: none; 22 | } 23 | 24 | #dev-tools:hover .tools { 25 | display: block; 26 | } 27 | -------------------------------------------------------------------------------- /src/dev-tools/dev-tools.js: -------------------------------------------------------------------------------- 1 | import './dev-tools.css' 2 | import featureToggles, {enable, disable} from './feature-toggles' 3 | import React from 'react' 4 | import ReactDOM from 'react-dom' 5 | 6 | function install() { 7 | window.devToolsEnabled = true 8 | // load local dev tools if it's there 9 | // NOTE: this is using some webpack-sepecific features. 10 | // if you're not using webpack, you might consider using 11 | // https://npm.im/preval.macro or https://npm.im/codegen.macro 12 | const requireDevToolsLocal = require.context( 13 | './', 14 | false, 15 | /dev-tools\.local\.js/, 16 | ) 17 | const local = requireDevToolsLocal.keys()[0] 18 | let LocalDevTools 19 | if (local) { 20 | LocalDevTools = requireDevToolsLocal(local).default 21 | } 22 | LocalDevTools = LocalDevTools || (() => null) 23 | 24 | function TacoTool() { 25 | const [tacos, setTacos] = React.useState(featureToggles.tacos) 26 | 27 | React.useEffect(() => { 28 | if (tacos) { 29 | enable('tacos') 30 | } else { 31 | disable('tacos') 32 | } 33 | }, [tacos]) 34 | 35 | return ( 36 |
37 | 45 |
46 | ) 47 | } 48 | 49 | function DevTools() { 50 | return ( 51 |
52 |
🛠
53 |
54 | 55 | 56 |
57 |
58 | ) 59 | } 60 | 61 | // add dev tools UI to the page 62 | const devToolsRoot = document.createElement('div') 63 | document.body.appendChild(devToolsRoot) 64 | ReactDOM.render(, devToolsRoot) 65 | } 66 | 67 | export {install} 68 | -------------------------------------------------------------------------------- /src/dev-tools/dev-tools.local-example.js: -------------------------------------------------------------------------------- 1 | // if you want to have custom dev tools, create a file right here called 2 | // dev-tools.local.js. 3 | // because the .gitignore lists *.local.* as ignored you can change anything 4 | // you want in your local dev-tools file and it won't impact anyone else. 5 | 6 | // Here's an example of some of the things you could do: 7 | import React from 'react' 8 | import {screen, waitForElementToBeRemoved} from '@testing-library/react' 9 | import userEvent from '@testing-library/user-event' 10 | import {enable} from './feature-toggles' 11 | 12 | // If I want to make sure that tacos is always enabled locally, I can uncomment this: 13 | enable('tacos') 14 | 15 | // you can do whatever hackery you want in here. 16 | // Like you could use React Testing Library to fill out the username add password on load: 17 | async function automaticallyLogin() { 18 | try { 19 | await userEvent.type(await screen.findByLabelText(/username/i), 'FAKE_USER') 20 | await userEvent.type( 21 | await screen.findByLabelText(/password/i), 22 | 'FAKE_PASSWORD', 23 | ) 24 | await userEvent.click(await screen.findByText(/submit/i)) 25 | await waitForElementToBeRemoved(() => screen.getByText(/loading/i)) 26 | } catch (error) { 27 | //ignore the error because we're probably already logged in so no need 28 | } 29 | } 30 | 31 | automaticallyLogin() 32 | 33 | // in the past I'd also have a history listener that would automatically execute 34 | // code when I navigated to a specific path (to auto-fill a large form I was 35 | // working on). 36 | 37 | // This is an optional component I can export to add a tool to the DevTools UI 38 | export default () =>
Local dev tools!
39 | -------------------------------------------------------------------------------- /src/dev-tools/feature-toggles.js: -------------------------------------------------------------------------------- 1 | import featureToggles from '../feature-toggles' 2 | 3 | const key = 'feature-toggles' 4 | 5 | // update featureToggles with what's in localStorage 6 | try { 7 | Object.assign(featureToggles, JSON.parse(window.localStorage.getItem(key))) 8 | } catch (error) { 9 | window.localStorage.removeItem(key) 10 | } 11 | 12 | const persist = () => 13 | window.localStorage.setItem(key, JSON.stringify(featureToggles)) 14 | 15 | function enable(name) { 16 | console.log(featureToggles, name, 'enabling') 17 | featureToggles[name] = true 18 | persist() 19 | } 20 | 21 | function disable(name) { 22 | console.log(featureToggles, name, 'disabling') 23 | featureToggles[name] = false 24 | persist() 25 | } 26 | 27 | export default featureToggles 28 | export {enable, disable} 29 | -------------------------------------------------------------------------------- /src/dev-tools/load.js: -------------------------------------------------------------------------------- 1 | function loadDevTools(callback) { 2 | // this allows you to explicitly disable it in development for example 3 | const explicitlyDisabled = 4 | window.location.search.includes('dev-tools=false') || 5 | window.localStorage.getItem('dev-tools') === 'false' 6 | 7 | const explicitlyEnabled = 8 | window.location.search.includes('dev-tools=true') || 9 | window.localStorage.getItem('dev-tools') === 'true' 10 | 11 | // we want it enabled by default everywhere but production and we also want 12 | // to support the dev tools in production (to make us more productive triaging production issues). 13 | // you can enable the DevTools via localStorage or the query string. 14 | if ( 15 | !explicitlyDisabled && 16 | (process.env.NODE_ENV === 'development' || explicitlyEnabled) 17 | ) { 18 | // use a dynamic import so the dev-tools code isn't bundled with the regular 19 | // app code so we don't worry about bundle size. 20 | import('./dev-tools') 21 | .then(devTools => devTools.install()) 22 | .finally(callback) 23 | } else { 24 | // if we don't need the DevTools, call the callback immediately. 25 | callback() 26 | } 27 | } 28 | 29 | export default loadDevTools 30 | -------------------------------------------------------------------------------- /src/feature-toggles.js: -------------------------------------------------------------------------------- 1 | // APP_CONFIG is set via the `config.js` script that's in /public 2 | const featureToggles = window.APP_CONFIG.featureToggles 3 | 4 | export default featureToggles 5 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import loadDevTools from './dev-tools/load' 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | import './index.css' 5 | import App from './App' 6 | 7 | // load and install the dev tools (if they need to be) 8 | // and when that's done, let's render the app 9 | // NOTE: if we don't need to install the devtools, then the callback 10 | // is called synchronously so there's no penalty for including this 11 | // in production. 12 | loadDevTools(() => { 13 | ReactDOM.render(, document.getElementById('root')) 14 | }) 15 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect' 6 | 7 | window.APP_CONFIG = {featureToggles: {}} 8 | --------------------------------------------------------------------------------