├── .gitignore ├── packages ├── common │ ├── value.web.js │ ├── package.json │ └── src │ │ └── PageNotFound.js ├── normandy │ ├── src │ │ ├── App.css │ │ ├── components │ │ │ ├── Store.js │ │ │ ├── Todo.js │ │ │ └── __old_Todo.js │ │ └── App.js │ └── package.json └── home │ ├── src │ ├── index.css │ ├── index.js │ ├── __mocks__ │ │ └── react-router-dom.js │ ├── setupTests.js │ ├── App.css │ ├── App.test.js │ ├── Components.js │ ├── logo.svg │ ├── App.js │ └── registerServiceWorker.js │ ├── public │ ├── favicon.ico │ ├── manifest.json │ └── index.html │ ├── config-overrides.js │ └── package.json ├── package.json ├── TODO.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | packages/home/build/ 3 | -------------------------------------------------------------------------------- /packages/common/value.web.js: -------------------------------------------------------------------------------- 1 | export default "value in web"; 2 | -------------------------------------------------------------------------------- /packages/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "common", 3 | "version": "0.0.1" 4 | } 5 | -------------------------------------------------------------------------------- /packages/normandy/src/App.css: -------------------------------------------------------------------------------- 1 | #normandy { 2 | text-align: left; 3 | padding: 10px 75px; 4 | } 5 | -------------------------------------------------------------------------------- /packages/home/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /packages/home/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/devcons/master/packages/home/public/favicon.ico -------------------------------------------------------------------------------- /packages/normandy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "normandy", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "mobx": "3.5.1", 6 | "mobx-react": "4.4.1" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/home/config-overrides.js: -------------------------------------------------------------------------------- 1 | const rewireYarnWorkspaces = require("react-app-rewire-yarn-workspaces"); 2 | 3 | module.exports = function override(config, env) { 4 | return rewireYarnWorkspaces(config, env); 5 | }; 6 | -------------------------------------------------------------------------------- /packages/common/src/PageNotFound.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const PageNotFound = ({ location }) => ( 4 |
5 |

Page Not Found

6 |

7 | No match for {location.pathname} 8 |

9 |
10 | ); 11 | -------------------------------------------------------------------------------- /packages/home/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | // Commented out during prototype stage 7 | // import registerServiceWorker from "./registerServiceWorker"; 8 | 9 | ReactDOM.render(, document.getElementById('root')); 10 | // registerServiceWorker(); 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": ["packages/home", "packages/common", "packages/normandy"], 4 | "scripts": { 5 | "start": "cd packages/home && yarn start", 6 | "build": "cd packages/home && yarn build", 7 | "test": "cd packages/home && yarn test", 8 | "serve": 9 | "(test -d packages/home/build || yarn build) && serve -s packages/home/build" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/home/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 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /packages/home/src/__mocks__/react-router-dom.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // The tip to use thi comes from: 4 | // https://medium.com/@antonybudianto/react-router-testing-with-jest-and-enzyme-17294fefd303 5 | 6 | const rrd = require('react-router-dom'); 7 | 8 | // Just render plain div with its children 9 | rrd.BrowserRouter = ({ children }) =>
{children}
; 10 | 11 | module.exports = rrd; 12 | -------------------------------------------------------------------------------- /packages/home/src/setupTests.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | // For extra convenient assertions 5 | // https://github.com/FormidableLabs/enzyme-matchers#assertions 6 | import 'jest-enzyme'; 7 | 8 | configure({ adapter: new Adapter() }); 9 | 10 | const localStorageMock = { 11 | getItem: jest.fn(), 12 | setItem: jest.fn(), 13 | clear: jest.fn() 14 | }; 15 | global.localStorage = localStorageMock; 16 | 17 | const sessionStorageMock = { 18 | getItem: jest.fn(), 19 | setItem: jest.fn(), 20 | clear: jest.fn() 21 | }; 22 | global.sessionStorage = sessionStorageMock; 23 | -------------------------------------------------------------------------------- /packages/home/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "home", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "enzyme": "3.3.0", 7 | "enzyme-adapter-react-16": "1.1.1", 8 | "jest-enzyme": "4.2.0", 9 | "react": "16.2.0", 10 | "react-dom": "16.2.0", 11 | "react-loadable": "5.3.1", 12 | "react-router-dom": "4.2.2", 13 | "react-scripts": "1.1.1", 14 | "react-test-renderer": "16.2.0" 15 | }, 16 | "scripts": { 17 | "start": "react-app-rewired start", 18 | "build": "react-app-rewired build", 19 | "test": "react-app-rewired test --env=jsdom" 20 | }, 21 | "devDependencies": { 22 | "react-app-rewire-yarn-workspaces": "1.0.2", 23 | "react-app-rewired": "1.4.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/home/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .App-header { 11 | background-color: #222; 12 | height: 150px; 13 | padding: 20px; 14 | color: white; 15 | } 16 | 17 | .App-title { 18 | font-size: 1.5em; 19 | } 20 | 21 | .App-intro { 22 | font-size: large; 23 | } 24 | 25 | @keyframes App-logo-spin { 26 | from { 27 | transform: rotate(0deg); 28 | } 29 | to { 30 | transform: rotate(360deg); 31 | } 32 | } 33 | 34 | footer { 35 | margin-top: 100px; 36 | border-top: 1px solid #efefef; 37 | font-size: 80%; 38 | } 39 | 40 | header li { 41 | display: inline; 42 | border: 1px solid #666; 43 | padding: 3px 6px; 44 | } 45 | header li a { 46 | color: white; 47 | } 48 | header li a.active { 49 | font-weight: bold; 50 | text-decoration: none; 51 | } 52 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | TODO 2 | 3 | * Setting a delay on the Loadable() as an option doesn't seem to work. 4 | 5 | * If the `home` app and the `normandy` app shares some third-party package, 6 | will the bundle for normandy "repeat" this? 7 | 8 | * Pretend the `home` app signs in and generates a token (JWT?). This should 9 | be passed into every sub-app. 10 | 11 | * CircleCI example. 12 | 13 | * Why did I not have to do these `.babelrc` tricks? 14 | https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/guides/code-splitting.md 15 | 16 | * having to mess with prefixes inside routes for Normandy app is sucky. 17 | 18 | * Normandy-only tests, for example. 19 | 20 | * Unit testing to run `/normandy` in the home App fails hard. 21 | 22 | DONE 23 | 24 | * Running tests. 25 | 26 | * Use source-map-explorer to inspect build chunks and where heavy things 27 | like redux ends up. 28 | 29 | * Mix things like Redux in different apps. 30 | -------------------------------------------------------------------------------- /packages/normandy/src/components/Store.js: -------------------------------------------------------------------------------- 1 | import { action, extendObservable } from 'mobx'; 2 | 3 | // function cmp() 4 | class Store { 5 | constructor() { 6 | extendObservable(this, { 7 | todos: [], 8 | nextId: 0, 9 | get orderedTodos() { 10 | const copy = Array.from(this.todos); 11 | copy.sort((a, b) => { 12 | if (a.done === b.done) { 13 | return a.id - b.id; 14 | } else if (a.done) { 15 | return 1; 16 | } else { 17 | return -1; 18 | } 19 | }); 20 | return copy; 21 | }, 22 | addTodo: action(text => { 23 | this.todos.push({ 24 | text: text, 25 | id: ++this.nextId, 26 | done: false 27 | }); 28 | }), 29 | toggleDone: action(thisItem => { 30 | this.todos = this.todos.map(item => { 31 | if (item.id === thisItem.id) { 32 | item.done = !item.done; 33 | } 34 | return item; 35 | }); 36 | }) 37 | }); 38 | } 39 | } 40 | 41 | const store = new Store(); 42 | // const store = (window.store = new Store()); 43 | 44 | export default store; 45 | -------------------------------------------------------------------------------- /packages/home/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { mount } from 'enzyme'; 4 | import { MemoryRouter } from 'react-router'; 5 | import App from './App'; 6 | import { Home, About, SignIn, PageNotFound } from './Components'; 7 | 8 | it('renders home App without crashing', () => { 9 | shallow(); 10 | }); 11 | 12 | it('renders Home by default', () => { 13 | const wrapper = mount( 14 | 15 | 16 | 17 | ); 18 | expect(wrapper.find(Home)).toHaveLength(1); 19 | expect(wrapper.find(PageNotFound)).toHaveLength(0); 20 | }); 21 | 22 | it('renders PageNotFound if no match', () => { 23 | const wrapper = mount( 24 | 25 | 26 | 27 | ); 28 | expect(wrapper.find(Home)).toHaveLength(0); 29 | expect(wrapper.find(PageNotFound)).toHaveLength(1); 30 | }); 31 | 32 | // XXX This fails! 33 | // Nasty webpack error. 34 | // it('renders Normandy home app', () => { 35 | // const wrapper = mount( 36 | // 37 | // 38 | // 39 | // ); 40 | // // expect(wrapper.find(Home)).toHaveLength(1); 41 | // // expect(wrapper.find(PageNotFound)).toHaveLength(0); 42 | // }); 43 | -------------------------------------------------------------------------------- /packages/normandy/src/components/Todo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { observer } from 'mobx-react'; 4 | import store from './Store'; 5 | 6 | const TodoApp = observer( 7 | class TodoApp extends React.Component { 8 | render() { 9 | return ( 10 |
11 |

The Normandy Todo App

12 |
13 | An excuse to use ReduxMobx 14 |
15 |
    16 | {store.orderedTodos.map(todo => { 17 | return ( 18 |
  • { 21 | event.preventDefault(); 22 | store.toggleDone(todo); 23 | }} 24 | style={{ 25 | cursor: 'pointer', 26 | textDecoration: todo.done ? 'line-through' : 'none' 27 | }} 28 | > 29 | {todo.text} 30 |
  • 31 | ); 32 | })} 33 |
34 |
{ 36 | event.preventDefault(); 37 | const value = this.refs.text.value.trim(); 38 | if (value) { 39 | store.addTodo(value); 40 | this.refs.text.value = ''; 41 | } 42 | }} 43 | > 44 | 45 | 46 |
47 |
48 | ); 49 | } 50 | } 51 | ); 52 | 53 | export default TodoApp; 54 | -------------------------------------------------------------------------------- /packages/normandy/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | BrowserRouter as Router, 4 | Route, 5 | NavLink, 6 | Link, 7 | Switch 8 | } from 'react-router-dom'; 9 | 10 | import './App.css'; 11 | 12 | import TodoApp from './components/Todo'; 13 | 14 | class App extends React.Component { 15 | // componentDidMount() { 16 | // console.log(this.props.location); 17 | // } 18 | render() { 19 | const prefix = this.props.match.url; 20 | return ( 21 |
22 |

Normandy

23 |
24 | 25 | 26 | 27 | {/* */} 28 | {/* */} 29 | {/* */} 30 |
31 | ); 32 | } 33 | } 34 | 35 | export default App; 36 | 37 | class Home extends React.Component { 38 | render() { 39 | return ( 40 |
41 |

Welcome

42 |

43 | Normandy's Todo App 44 |

45 |

46 | Normandy's About page 47 |

48 |
49 | ); 50 | } 51 | } 52 | 53 | class About extends React.Component { 54 | render() { 55 | return ( 56 |
57 |

About The Normandy App

58 |

Not sure what to say here...

59 |

60 | Back to Normandy homepage 61 |

62 |
63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/home/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /packages/home/src/Components.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | export const LoadingWrapper = name => { 5 | return props => { 6 | if (props.error) { 7 | return
Error loading {name}!
; 8 | } else if (props.timedOut) { 9 | return
Taking a long time...
; 10 | } else if (props.pastDelay) { 11 | return
Loading {name}...
; 12 | } else { 13 | return null; 14 | } 15 | }; 16 | }; 17 | 18 | export class Home extends React.Component { 19 | render() { 20 | return ( 21 |
22 |

Welcome To The Delivery Console

23 |

Pick an app in the Menu above

24 |
25 | ); 26 | } 27 | } 28 | 29 | export class About extends React.Component { 30 | render() { 31 | return ( 32 |
33 |

About This App

34 |

35 | The Delivery Console spans multiple React components that might all 36 | function differently. 37 |

38 |

39 | Back to Home homepage 40 |

41 |
42 | ); 43 | } 44 | } 45 | 46 | export class SignIn extends React.PureComponent { 47 | fakeSignIn = event => { 48 | event.preventDefault(); 49 | const camefrom = window.sessionStorage.getItem('camefrom'); 50 | if (window.confirm('Is your name Slim?')) { 51 | console.log('Sign in'); 52 | } 53 | // alert("Imagine a Sign In modal right here and now."); 54 | if (camefrom) { 55 | alert(`After successful log in, go to: ${camefrom}`); 56 | } 57 | }; 58 | render() { 59 | return ( 60 |
61 |

Sign In

62 |
63 | 64 |
65 |
66 | ); 67 | } 68 | } 69 | 70 | export const PageNotFound = ({ location }) => ( 71 |
72 |

Page Not Found

73 |

74 | No match for {location.pathname} 75 |

76 |
77 | ); 78 | -------------------------------------------------------------------------------- /packages/normandy/src/components/__old_Todo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { combineReducers, createStore } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | // import { createStore } from 'redux'; 5 | // import { Provider } from 'react-redux'; 6 | 7 | // 8 | // Actions 9 | let nextTodoId = 0; 10 | export const addTodo = text => ({ 11 | type: 'ADD_TODO', 12 | id: nextTodoId++, 13 | text 14 | }); 15 | 16 | export const setVisibilityFilter = filter => ({ 17 | type: 'SET_VISIBILITY_FILTER', 18 | filter 19 | }); 20 | 21 | export const toggleTodo = id => ({ 22 | type: 'TOGGLE_TODO', 23 | id 24 | }); 25 | 26 | // 27 | // 28 | 29 | const getVisibleTodos = (todos, filter) => { 30 | switch (filter) { 31 | case 'SHOW_ALL': 32 | return todos; 33 | case 'SHOW_COMPLETED': 34 | return todos.filter(t => t.completed); 35 | case 'SHOW_ACTIVE': 36 | return todos.filter(t => !t.completed); 37 | default: 38 | throw new Error('Unknown filter: ' + filter); 39 | } 40 | }; 41 | 42 | const mapStateToProps = state => ({ 43 | todos: getVisibleTodos(state.todos.present, state.visibilityFilter) 44 | }); 45 | 46 | const mapDispatchToProps = { 47 | onTodoClick: toggleTodo 48 | }; 49 | 50 | const store = createStore(reducer); 51 | 52 | class App extends React.Component { 53 | render() { 54 | return ( 55 | 56 | 57 | 58 | ); 59 | } 60 | } 61 | class Todo extends React.Component { 62 | render() { 63 | // const foo = 'bar'; 64 | console.log(this.props); 65 | return ( 66 |
67 |

The Normandy Todo App

68 |
An excuse to use Redux
69 |
    70 |
  • 71 |
72 |
73 | ); 74 | } 75 | } 76 | 77 | const TodoApp = connect(mapStateToProps, mapDispatchToProps)(Todo); 78 | 79 | // const mapStateToProps = state => { 80 | // return { 81 | // todos: state.todos 82 | // }; 83 | // }; 84 | // 85 | // const TodoApp = connect(mapStateToProps, mapDispatchToProps)(TodoList); 86 | 87 | export default TodoApp; 88 | -------------------------------------------------------------------------------- /packages/home/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Delivery Console Prototype v0 2 | 3 | This is an experimental implementation of 4 | [`create-react-app`](https://github.com/facebookincubator/create-react-app) with 5 | `react-router-dom`, `react-loadable` and `yarn workspaces`. 6 | 7 | ## To run 8 | 9 | For this to work you first need to enable workspaces in `yarn`. Simply run: 10 | 11 | ```sh 12 | $ yarn config set workspaces-experimental true 13 | ``` 14 | 15 | That should update your `~/.yarnrc`. 16 | 17 | ```sh 18 | $ git clone https://github.com/peterbe/devcons.git 19 | $ yarn 20 | $ yarn start 21 | $ open http://localhost:3000 22 | ``` 23 | 24 | ## To build and serve 25 | 26 | Globally install [`serve`](https://www.npmjs.com/package/serve) 27 | 28 | ```sh 29 | $ yarn serve 30 | $ open http://localhost:5000 31 | ``` 32 | 33 | ## To run tests 34 | 35 | ```sh 36 | $ yarn test 37 | ``` 38 | 39 | Note that `jest` only runs tests based on the files since the last commit. 40 | The interactive test running should mention this and you can press `a` to 41 | run all tests. 42 | 43 | ## Source map exploration 44 | 45 | The whole point of using async loading is to avoid the cost of having to 46 | download a monster .js bundle for the union of all packages in all 47 | different workspaces. To prove that the lazy loaded chunks "self-contain" 48 | the packages only they need. To see this in action, globally install 49 | [`source-map-explorer`](https://www.npmjs.com/package/source-map-explorer). 50 | 51 | ```sh 52 | $ yarn global add source-map-explorer 53 | $ yarn build 54 | $ ls packages/home/build/static/js 55 | 0.0c747db6.chunk.js 0.0c747db6.chunk.js.map main.6d9855f0.js main.6d9855f0.js.map 56 | $ source-map-explorer packages/home/build/static/js/0.0c747db6.chunk.js 57 | $ source-map-explorer packages/home/build/static/js/main.6d9855f0.js 58 | ``` 59 | 60 | ## How it works 61 | 62 | The core React app is the `packages/home` one. It's the one that imports 63 | the other components from the other workspace packages. 64 | 65 | It uses `react-router-dom` to redirect traffic to components in other 66 | packages and when it does so it uses `react-loadable` which 67 | asynchronously loads them. 68 | 69 | ### Rational 70 | 71 | The goal is that each app is a contained folder containing a bunch of 72 | React components and it's own `package.json` where it can maintain a list 73 | of vendor packages these app components need. This makes for better 74 | organization of files. Each app feels more self-contained even though 75 | it's only imports for the `home` app. 76 | 77 | The user experience is ideal in that minimal build JS is downloaded 78 | if the user only uses some few apps, for example, only using the `home` 79 | app (to sign in and as landing page) and the Normandy app. Then xhe 80 | shouldn't have to download all the resources that `someotherapp` requires. 81 | 82 | ## Caveats 83 | 84 | * When working on an app/component that isn't imported you don't get 85 | those `create-react-app` `eslint` warnings immediately. 86 | That's because the code you might be working on isn't imported yet. 87 | Perhaps this can be solved by some hack that depends on 88 | `process.env.NODE_ENV` so that it imports all files during development. 89 | 90 | * Perhaps it's not convenient to load the `home` and have the app you're 91 | strongly interested in being async loaded. See point above. 92 | Perhaps it's possible to write a, for example, `packages/normandy/index.js` 93 | that you can start with `yarn start` specifically in that directory. Then 94 | it could have a mock of the `home` app's top bar and nav etc. 95 | -------------------------------------------------------------------------------- /packages/home/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | BrowserRouter as Router, 4 | Route, 5 | NavLink, 6 | Link, 7 | Switch 8 | } from 'react-router-dom'; 9 | import Loadable from 'react-loadable'; 10 | import './App.css'; 11 | 12 | import { 13 | Home, 14 | About, 15 | SignIn, 16 | PageNotFound, 17 | LoadingWrapper 18 | } from './Components'; 19 | // import { PageNotFound } from 'common/src/PageNotFound'; 20 | // import { PageNotFound } from 'common/src/Loading'; 21 | // import { App as NormandyApp } from "normandy/src/App"; 22 | // import { App as NormandyApp } from 'normandy/src/App'; 23 | // console.log("TEST", NormandyApp); 24 | 25 | // const LoadableNormandyApp = Loadable({ 26 | // loader: () => import('normandy/src/App'), 27 | // loading() { 28 | // return
Loading...
; 29 | // } 30 | // }); 31 | 32 | // XXX Make this more generic and not hardcoded. 33 | const Normandy = Loadable({ 34 | loader: () => import('normandy/src/App'), 35 | loading: LoadingWrapper('Normandy') 36 | }); 37 | 38 | class App extends React.Component { 39 | setCameFrom = event => { 40 | let camefrom = window.location.pathname; 41 | if (window.location.search) { 42 | camefrom += window.location.search; 43 | } 44 | window.sessionStorage.setItem('camefrom', camefrom); 45 | }; 46 | 47 | componentDidMount() { 48 | // XXX There's a cool thing we could do. 49 | // Now that the home app has loaded, we could ask our localStorage 50 | // or something for the users preferred app(s). Suppose we 51 | // can figure out that this user often loads Normandy, we could 52 | // trigger `Normandy.preload()` 53 | // this._preloadApp('normandy'); 54 | } 55 | 56 | onNavLinkHover = app => { 57 | this._preloadApp(app); 58 | }; 59 | 60 | // Memory of which apps have been preloaded. 61 | preloadedApps = new Set(); 62 | 63 | _preloadApp = app => { 64 | if (!this.preloadedApps.has(app)) { 65 | this.preloadedApps.add(app); 66 | switch (app) { 67 | case 'normandy': 68 | Normandy.preload(); 69 | break; 70 | default: 71 | console.warn(`Not sure how to reload: ${app}`); 72 | } 73 | } 74 | }; 75 | 76 | render() { 77 | return ( 78 | 79 |
80 |
81 | 87 | Sign In 88 | 89 |

Delivery Console

90 |
    91 |
  • 92 | Console Home 93 |
  • 94 |
  • 95 | this.onNavLinkHover('normandy')} 98 | > 99 | Normandy 100 | 101 |
  • 102 |
103 |
104 |
105 | 106 | 107 | 108 | {/* { 111 | console.log("PROPS", props); 112 | return ; 113 | }} 114 | /> */} 115 | 116 | {/* */} 117 | {/* */} 118 | {/* */} 119 | 120 | {/* */} 121 | {/* { 124 | // console.log("PROPS", props.location); 125 | return ; 126 | }} 127 | /> */} 128 | 129 | 130 |
131 | 137 |
138 |
139 | ); 140 | } 141 | } 142 | 143 | export default App; 144 | -------------------------------------------------------------------------------- /packages/home/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | --------------------------------------------------------------------------------