├── public
├── favicon.ico
├── logo192.png
├── logo512.png
├── robots.txt
├── config.js
├── manifest.json
└── index.html
├── src
├── feature-toggles.js
├── App.test.js
├── setupTests.js
├── index.css
├── dev-tools
│ ├── dev-tools.css
│ ├── feature-toggles.js
│ ├── load.js
│ ├── dev-tools.local-example.js
│ └── dev-tools.js
├── index.js
├── App.css
├── logo.svg
└── App.js
├── .gitignore
├── README.md
└── package.json
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kentcdodds/app-dev-tools/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kentcdodds/app-dev-tools/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kentcdodds/app-dev-tools/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/config.js:
--------------------------------------------------------------------------------
1 | // pretend that this is dynamically generated via a server
2 |
3 | window.APP_CONFIG = {
4 | featureToggles: {},
5 | }
6 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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.*
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
38 | Enable Tacos:{' '}
39 | setTacos(e.target.checked)}
43 | />
44 |
45 |
46 | )
47 | }
48 |
49 | function DevTools() {
50 | return (
51 |
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 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
33 |
34 |
35 |
36 | You need to enable JavaScript to run this app.
37 |
38 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/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 |
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 |
--------------------------------------------------------------------------------