├── .babelrc ├── .codeclimate.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .flowconfig ├── .github └── CODEOWNERS ├── .gitignore ├── .npmignore ├── .snyk ├── .travis.yml ├── LICENSE ├── README.md ├── __test-helpers__ ├── reducerParameters.js ├── setup.js ├── setupJest.js └── setupThunk.js ├── __tests__ ├── __snapshots__ │ ├── action-creators.js.snap │ ├── connectRoutes.js.snap │ ├── createLocationReducer.js.snap │ └── pure-utils.js.snap ├── action-creators.js ├── clientOnlyApi.js ├── connectRoutes.js ├── createLocationReducer.js └── pure-utils.js ├── docs ├── _config.yml ├── action.md ├── addRoutes.md ├── blocking-navigation.md ├── client-only-api.md ├── connectRoutes.md ├── low-level-api.md ├── migration.md ├── prefetching.md ├── prior-art.md ├── query-strings.md ├── react-native.md ├── reducer.md ├── redux-first-router-flow-chart.png ├── redux-persist.md ├── scroll-restoration.md ├── server-rendering.md └── url-parsing.md ├── examples ├── README.md ├── change-title │ ├── README.md │ └── change-title.patch ├── links │ ├── README.md │ └── links.patch ├── minimal │ ├── .env │ ├── .eslintrc.js │ └── src │ │ ├── App.js │ │ ├── components.js │ │ ├── configureStore.js │ │ ├── index.js │ │ └── pageReducer.js ├── redux-devtools │ ├── README.md │ └── redux-devtools.patch └── thunks │ ├── README.md │ └── thunks.patch ├── flow-typed └── npm │ ├── babel-cli_vx.x.x.js │ ├── babel-eslint_vx.x.x.js │ ├── babel-plugin-transform-flow-strip-types_vx.x.x.js │ ├── babel-preset-es2015_vx.x.x.js │ ├── babel-preset-react_vx.x.x.js │ ├── babel-preset-stage-0_vx.x.x.js │ ├── eslint-plugin-babel_vx.x.x.js │ ├── eslint-plugin-flow-vars_vx.x.x.js │ ├── eslint-plugin-flowtype_vx.x.x.js │ ├── eslint-plugin-import_vx.x.x.js │ ├── eslint-plugin-react_vx.x.x.js │ ├── eslint_vx.x.x.js │ ├── flow-bin_v0.x.x.js │ ├── flow-copy-source_vx.x.x.js │ ├── jest_v18.x.x.js │ ├── react-redux_v4.x.x.js │ └── redux_v3.x.x.js ├── package.json ├── renovate.json ├── src ├── action-creators │ ├── addRoutes.js │ ├── historyCreateAction.js │ ├── middlewareCreateAction.js │ ├── middlewareCreateNotFoundAction.js │ └── redirect.js ├── connectRoutes.js ├── flow-types.js ├── index.js ├── pure-utils │ ├── actionToPath.js │ ├── attemptCallRouteThunk.js │ ├── canUseDom.js │ ├── changePageTitle.js │ ├── confirmLeave.js │ ├── createThunk.js │ ├── isLocationAction.js │ ├── isReactNative.js │ ├── isRedirectAction.js │ ├── isServer.js │ ├── nestAction.js │ ├── objectValues.js │ ├── pathToAction.js │ ├── pathnamePlusSearch.js │ └── setKind.js └── reducer │ └── createLocationReducer.js ├── troubleshooting.md ├── wallaby.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | ["env", { 5 | "targets": { 6 | "browsers": ["last 4 versions", "not ie <= 9"] 7 | }, 8 | "modules": false 9 | }], 10 | "stage-0" 11 | ], 12 | "env": { 13 | "development": { 14 | "presets": [ 15 | "react", 16 | ["env", { 17 | "targets": { 18 | "node": "current" 19 | } 20 | }] 21 | ] 22 | }, 23 | "test": { 24 | "presets": [ 25 | "react", 26 | ["env", { 27 | "targets": { 28 | "node": "current" 29 | } 30 | }] 31 | ] 32 | } 33 | }, 34 | "plugins": ["transform-flow-strip-types"] 35 | } 36 | 37 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | languages: 2 | JavaScript: true 3 | 4 | engines: 5 | duplication: 6 | enabled: true 7 | config: 8 | languages: 9 | - javascript: 10 | fixme: 11 | enabled: true 12 | eslint: 13 | enabled: true 14 | config: 15 | config: .eslintrc.js 16 | checks: 17 | import/no-unresolved: 18 | enabled: false 19 | import/extensions: 20 | enabled: false 21 | 22 | ratings: 23 | paths: 24 | - "src/**" 25 | 26 | exclude_paths: 27 | - "docs/" 28 | - "dist/" 29 | - "flow-typed/" 30 | - "node_modules/" 31 | - ".vscode/" 32 | - ".eslintrc.js" 33 | - "**/*.snap" 34 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | [*] 3 | indent_style = space 4 | indent_size = 2 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | [*.md] 10 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | flow-typed 3 | node_modules 4 | docs 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'babel-eslint', 3 | parserOptions: { 4 | ecmaFeatures: { 5 | generators: true, 6 | experimentalObjectRestSpread: true 7 | }, 8 | sourceType: 'module', 9 | allowImportExportEverywhere: false 10 | }, 11 | plugins: ['flowtype'], 12 | extends: ['airbnb', 'plugin:flowtype/recommended'], 13 | settings: { 14 | flowtype: { 15 | onlyFilesWithFlowAnnotation: true 16 | } 17 | }, 18 | globals: { 19 | window: true, 20 | document: true, 21 | __dirname: true, 22 | __DEV__: true, 23 | CONFIG: true, 24 | process: true, 25 | jest: true, 26 | describe: true, 27 | test: true, 28 | it: true, 29 | expect: true, 30 | beforeEach: true 31 | }, 32 | 'import/resolver': { 33 | node: { 34 | extensions: ['.js', '.css', '.json', '.styl'] 35 | } 36 | }, 37 | 'import/extensions': ['.js'], 38 | 'import/ignore': ['node_modules', 'flow-typed', '\\.(css|styl|svg|json)$'], 39 | rules: { 40 | 'no-shadow': 0, 41 | 'no-use-before-define': 0, 42 | 'no-param-reassign': 0, 43 | 'react/prop-types': 0, 44 | 'react/no-render-return-value': 0, 45 | 'no-confusing-arrow': 0, 46 | 'no-underscore-dangle': 0, 47 | 'no-plusplus': 0, 48 | camelcase: 1, 49 | 'prefer-template': 1, 50 | 'react/no-array-index-key': 1, 51 | 'global-require': 1, 52 | 'react/jsx-indent': 1, 53 | 'dot-notation': 1, 54 | 'import/no-named-default': 1, 55 | 'no-unused-vars': 1, 56 | 'import/no-unresolved': 1, 57 | 'flowtype/no-weak-types': 1, 58 | 'consistent-return': 1, 59 | 'no-nested-ternary': 1, 60 | 'no-return-assign': 1, 61 | 'no-continue': 1, 62 | semi: [2, 'never'], 63 | 'no-console': [2, { allow: ['warn', 'error'] }], 64 | 'flowtype/semi': [2, 'never'], 65 | 'jsx-quotes': [2, 'prefer-single'], 66 | 'react/jsx-filename-extension': [2, { extensions: ['.jsx', '.js'] }], 67 | 'spaced-comment': [2, 'always', { markers: ['?'] }], 68 | 'arrow-parens': [2, 'as-needed', { requireForBlockBody: false }], 69 | 'brace-style': [2, 'stroustrup'], 70 | 'no-unused-expressions': [ 71 | 2, 72 | { 73 | allowShortCircuit: true, 74 | allowTernary: true, 75 | allowTaggedTemplates: true 76 | } 77 | ], 78 | 'import/no-extraneous-dependencies': [ 79 | 'error', 80 | { 81 | devDependencies: true, 82 | optionalDependencies: true, 83 | peerDependencies: true 84 | } 85 | ], 86 | 'comma-dangle': [ 87 | 2, 88 | { 89 | arrays: 'never', 90 | objects: 'never', 91 | imports: 'never', 92 | exports: 'never', 93 | functions: 'never' 94 | } 95 | ], 96 | 'max-len': [ 97 | 'error', 98 | { 99 | code: 80, 100 | tabWidth: 2, 101 | ignoreUrls: true, 102 | ignoreComments: true, 103 | ignoreRegExpLiterals: true, 104 | ignoreStrings: true, 105 | ignoreTemplateLiterals: true 106 | } 107 | ], 108 | 'react/sort-comp': [ 109 | 2, 110 | { 111 | order: [ 112 | 'propTypes', 113 | 'props', 114 | 'state', 115 | 'defaultProps', 116 | 'contextTypes', 117 | 'childContextTypes', 118 | 'getChildContext', 119 | 'static-methods', 120 | 'lifecycle', 121 | 'everything-else', 122 | 'render' 123 | ] 124 | } 125 | ] 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/config-chain/.* 3 | .*/node_modules/npmconf/.* 4 | 5 | [include] 6 | 7 | [libs] 8 | 9 | [options] 10 | 11 | esproposal.class_static_fields=enable 12 | esproposal.class_instance_fields=enable 13 | 14 | module.file_ext=.js 15 | module.file_ext=.json 16 | module.system=haste 17 | 18 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe 19 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue 20 | suppress_comment=\\(.\\|\n\\)*\\$FlowGlobal 21 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # redux-first-router maintainers 2 | * @ScriptedAlchemy 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | coverage 3 | node_modules 4 | *.log 5 | .idea 6 | .DS_Store 7 | package-lock.json 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage 2 | __test-helpers__ 3 | __tests__ 4 | docs 5 | flow-typed 6 | src 7 | examples 8 | .github 9 | .idea 10 | 11 | *.log 12 | 13 | .babelrc 14 | .codeclimate.yml 15 | .editorconfig 16 | .eslintrc.js 17 | .snyk 18 | .travis.yml 19 | wallaby.js 20 | webpack.config.js 21 | .eslintignore 22 | .flowconfig 23 | *.png 24 | troubleshooting.md 25 | yarn.lock 26 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.12.0 3 | # ignores vulnerabilities until expiry date; change duration by modifying expiry date 4 | ignore: 5 | 'npm:chownr:20180731': 6 | - cacache > chownr: 7 | reason: 'Introduced via semantic-release, not relevant in production' 8 | - npm-profile > make-fetch-happen > cacache > chownr: 9 | reason: 'Introduced via semantic-release, not relevant in production' 10 | - libcipm > pacote > cacache > chownr: 11 | reason: 'Introduced via semantic-release, not relevant for production' 12 | - libcipm > pacote > tar > chownr: 13 | reason: 'Introduced via semantic-release, not relevant for production' 14 | - npm-registry-fetch > make-fetch-happen > cacache > chownr: 15 | reason: 'Introduced via semantic-release, not relevant for production' 16 | - libcipm > pacote > make-fetch-happen > cacache > chownr: 17 | reason: 'Introduced via semantic-release, not relevant for production' 18 | - libnpmhook > npm-registry-fetch > make-fetch-happen > cacache > chownr: 19 | reason: 'Introduced via semantic-release, not relevant for production' 20 | 'npm:mem:20180117': 21 | - libnpx > yargs > os-locale > mem: 22 | reason: 'Introduced via semantic-release, not relevant for production' 23 | patch: {} 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | cache: yarn 5 | install: yarn --ignore-engines # ignore engines to test node 6, otherwise it fails on engine check 6 | jobs: 7 | include: 8 | - stage: Build 9 | name: Travis Status 10 | script: npx travis-github-status 11 | name: Linting 12 | script: npm run lint 13 | name: Flow 14 | script: npm run flow 15 | name: Snyk 16 | script: snyk 17 | notifications: 18 | email: false 19 | webhooks: 20 | urls: 21 | - https://webhooks.gitter.im/e/5156be73e058008e1ed2 22 | on_success: always # options: [always|never|change] default: always 23 | on_failure: always # options: [always|never|change] default: always 24 | on_start: never # options: [always|never|change] default: always 25 | after_success: 26 | - npm run semantic-release 27 | branches: 28 | except: 29 | - /^v\d+\.\d+\.\d+$/ 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 James Gillmore 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 | # Redux-First Router 2 | Think of your app in terms of _states_, not _routes_ or _components_. Connect your components and just dispatch _Flux Standard Actions_! 3 | 4 |

5 | 6 | Version 7 | 8 | 9 | 10 | Min Node Version: 6 11 | 12 | 13 | 14 | 15 | Downloads 16 | 17 | 18 | 19 | Build Status 20 | 21 | 22 | ## Motivation 23 | To be able to use Redux *as is* while keeping the address bar in sync. To define paths as actions, and handle path params and query strings as action payloads. 24 | 25 | The address bar and Redux actions should be *bi-directionally* mapped, including via the browser's back/forward buttons. Dispatch an action and the address bar updates. 26 | Change the address, and an action is dispatched. 27 | 28 | In addition, here are some obstacles **Redux-First Router** seeks to *avoid*: 29 | 30 | * Rendering from state that doesn't come from Redux 31 | * Dealing with the added complexity from having state outside of Redux 32 | * Cluttering components with route-related code 33 | * Large API surface areas of frameworks like `react-router` and `next.js` 34 | * Routing frameworks getting in the way of optimizing animations (such as when animations coincide with component updates). 35 | * Having to do route changes differently in order to support server-side rendering. 36 | 37 | ## Usage 38 | 39 | ### Install 40 | `yarn add redux-first-router` 41 | 42 | (A minimal `` component exists in the separate package [`redux-first-router-link`](https://github.com/faceyspacey/redux-first-router-link).) 43 | 44 | ### Quickstart 45 | 46 | ```js 47 | // configureStore.js 48 | import { applyMiddleware, combineReducers, compose, createStore } from 'redux' 49 | import { connectRoutes } from 'redux-first-router' 50 | 51 | import page from './pageReducer' 52 | 53 | const routesMap = { 54 | HOME: '/', 55 | USER: '/user/:id' 56 | } 57 | 58 | export default function configureStore(preloadedState) { 59 | const { reducer, middleware, enhancer } = connectRoutes(routesMap) 60 | 61 | const rootReducer = combineReducers({ page, location: reducer }) 62 | const middlewares = applyMiddleware(middleware) 63 | const enhancers = compose(enhancer, middlewares) 64 | 65 | const store = createStore(rootReducer, preloadedState, enhancers) 66 | 67 | return { store } 68 | } 69 | ``` 70 | 71 | ```js 72 | // pageReducer.js 73 | import { NOT_FOUND } from 'redux-first-router' 74 | 75 | const components = { 76 | HOME: 'Home', 77 | USER: 'User', 78 | [NOT_FOUND]: 'NotFound' 79 | } 80 | 81 | export default (state = 'HOME', action = {}) => components[action.type] || state 82 | ``` 83 | 84 | ```js 85 | // App.js 86 | import React from 'react' 87 | import { connect } from 'react-redux' 88 | 89 | // Contains 'Home', 'User' and 'NotFound' 90 | import * as components from './components'; 91 | 92 | const App = ({ page }) => { 93 | const Component = components[page] 94 | return 95 | } 96 | 97 | const mapStateToProps = ({ page }) => ({ page }) 98 | 99 | export default connect(mapStateToProps)(App) 100 | ``` 101 | 102 | ```js 103 | // components.js 104 | import React from 'react' 105 | import { connect } from 'react-redux' 106 | 107 | const Home = () =>

Home

108 | 109 | const User = ({ userId }) =>

{`User ${userId}`}

110 | const mapStateToProps = ({ location }) => ({ 111 | userId: location.payload.id 112 | }) 113 | const ConnectedUser = connect(mapStateToProps)(User) 114 | 115 | const NotFound = () =>

404

116 | 117 | export { Home, ConnectedUser as User, NotFound } 118 | ``` 119 | 120 | ## Documentation 121 | 122 | ### Basics 123 | 124 | #### Flow Chart 125 | ![Redux First Router Flow Chart](https://github.com/faceyspacey/redux-first-router/raw/master/docs/redux-first-router-flow-chart.png) 126 | 127 | #### [connectRoutes](https://github.com/faceyspacey/redux-first-router/blob/master/docs/connectRoutes.md) 128 | connectRoutes is the primary "work" you will do to get Redux First 129 | Router going. It's all about creating and maintaining a pairing of 130 | action types and dynamic express style route paths. If you use our `````` component and pass an action as its href prop, you can change the URLs you use here any time without having to change your application code. 131 | 132 | #### [URL parsing](https://github.com/faceyspacey/redux-first-router/blob/master/docs/url-parsing.md) 133 | Besides the simple option of matching a literal path, all matching capabilities of the path-to-regexp package we use are now supported, except unnamed parameters. 134 | 135 | #### [Flux Standard Actions](https://github.com/faceyspacey/redux-first-router/blob/master/docs/action.md) 136 | One of the goals of Redux First Router is to NOT alter your actions 137 | and be 100% flux standard action-compliant. That allows for automatic 138 | support for packages such as redux-actions. 139 | 140 | #### [Location Reducer](https://github.com/faceyspacey/redux-first-router/blob/master/docs/reducer.md) 141 | The location reducer primarily maintains the state of the current pathname and action dispatched (type + payload). That's its core mission. 142 | 143 | #### [Link Component](https://github.com/faceyspacey/redux-first-router-link) 144 | A minimal link component exists in the separate package redux-first-router-link. 145 | 146 | #### [Query Strings](https://github.com/faceyspacey/redux-first-router/blob/master/docs/query-strings.md) 147 | Queries can be dispatched by assigning a query object containing key/vals to an action, its payload object or its meta object. 148 | 149 | #### [React Native](https://github.com/faceyspacey/redux-first-router/blob/master/docs/react-native.md) 150 | Redux First Router has been thought up from the ground up with React Native (and server environments) in mind. They both make use of the history package's createMemoryHistory. In coordination, we are able to present you with a first-rate developer experience when it comes to URL-handling on native. We hope you come away feeling: "this is what I've been waiting for." 151 | 152 | ### Advanced 153 | 154 | #### [addRoutes](https://github.com/faceyspacey/redux-first-router/blob/master/docs/addRoutes.md) 155 | Sometimes you may want to dynamically add routes to routesMap, for 156 | example so that you can codesplit routesMap. You can do this using the 157 | addRoutes function. 158 | 159 | #### [Blocking navigation](https://github.com/faceyspacey/redux-first-router/blob/master/docs/blocking-navigation.md) 160 | Sometimes you may want to block navigation away from the current 161 | route, for example to prompt the user to save their changes. 162 | 163 | #### [Scroll Restoration](https://github.com/faceyspacey/redux-first-router/blob/master/docs/scroll-restoration.md) 164 | Complete Scroll restoration and hash #links handling is addressed primarily by one of our companion packages: redux-first-router-restore-scroll (we like to save you the bytes sent to clients if you don't need it). 165 | 166 | #### [Server Side Rendering](https://github.com/faceyspacey/redux-first-router/blob/master/docs/server-rendering.md) 167 | Ok, this is the biggest example here, but given what it does, we think it's extremely concise and sensible. 168 | 169 | #### [Client-Only API](https://github.com/faceyspacey/redux-first-router/blob/master/docs/client-only-api.md) 170 | The following are features you should avoid unless you have a reason 171 | that makes sense to use them. These features revolve around the 172 | history package's API. They make the most sense in React Native--for 173 | things like back button handling. 174 | 175 | #### [Low-level API](https://github.com/faceyspacey/redux-first-router/blob/master/docs/low-level-api.md) 176 | Below are some additional methods we export. The target user is 177 | package authors. Application developers will rarely need this. 178 | 179 | #### [Version 2 Migration Steps](https://github.com/faceyspacey/redux-first-router/blob/master/docs/migration.md) 180 | In earlier versions history was a peerDependency, this is no longer 181 | the case since version 2 has its own history management tool. This 182 | means that the arguments passed to connectRoutes(documentation) need 183 | to be changed. 184 | 185 | #### [Usage with redux-persist](https://github.com/faceyspacey/redux-first-router/blob/master/docs/redux-persist.md) 186 | You might run into a situation where you want to trigger a redirect as soon as possible in case some particular piece of state is or is not set. A possible use case could be persisting checkout state, e.g. checkoutSteps.step1Completed. 187 | 188 | #### [Prior Art](https://github.com/faceyspacey/redux-first-router/blob/master/docs/prior-art.md) 189 | These packages attempt in similar ways to reconcile the browser 190 | history with redux actions and state. 191 | 192 | ### Recipes 193 | 194 | - [Dispatching thunks & pathless routes](./examples/thunks) 195 | - [SEO-friendly styled links](./examples/links) 196 | - [Automatically changing page ``](./examples/change-title) 197 | - [Use Redux Devtools to debug route changes](./examples/redux-devtools) 198 | 199 | ### Help add more recipes for these use cases. PR's welcome! 200 | 201 | *Topics for things you can do with redux-first-router but need examples written:* 202 | 203 | - *Performing redirects bases on `state` and `payload`.* 204 | - *Use hash-based routes/history (*see the [migration instructions](./docs/migration.md)*)* 205 | - *Restoring scroll position* 206 | - *Handling optional URL fragments and query strings* 207 | - *Route change pre- & post-processing* 208 | - *Code-splitting* 209 | - *Server-side rendering* 210 | - *Usage together with `react-universal-component`, `babel-plugin-universal-import`, `webpack-flush-chunks`.* 211 | 212 | ## Where is new feature development occuring? 213 | Feature development efforts are occuring in the [Respond framework Rudy repository](https://github.com/respond-framework/rudy). 214 | 215 | ## Contributing 216 | We use [commitizen](https://github.com/commitizen/cz-cli), run `npm run cm` to make commits. A command-line form will appear, requiring you answer a few questions to automatically produce a nicely formatted commit. Releases, semantic version numbers, tags, changelogs and publishing will automatically be handled based on these commits thanks to [semantic-release](https:/ 217 | /github.com/semantic-release/semantic-release). 218 | 219 | ## Community And Related Projects 220 | 221 | - [Reactlandia chat lobby](https://gitter.im/Reactlandia/Lobby) 222 | 223 | - [react-universal-component](https://github.com/faceyspacey/react-universal-component). Made to work perfectly with Redux-First Router. 224 | 225 | - [webpack-flush-chunks](https://github.com/faceyspacey/webpack-flush-chunks). The foundation of our `Universal` product line. 226 | -------------------------------------------------------------------------------- /__test-helpers__/reducerParameters.js: -------------------------------------------------------------------------------- 1 | import { createMemoryHistory } from 'rudy-history' 2 | 3 | import { getInitialState } from '../src/reducer/createLocationReducer' 4 | 5 | export default (type, pathname) => { 6 | // eslint-disable-line import/prefer-default-export 7 | const history = createMemoryHistory({ initialEntries: ['/first'] }) 8 | history.push(pathname) 9 | 10 | const current = { pathname, type, payload: { param: 'bar' } } 11 | const prev = { pathname: '/first', type: 'FIRST', payload: {} } 12 | const routesMap = { 13 | FIRST: '/first', 14 | SECOND: '/second/:param' 15 | } 16 | 17 | return { 18 | type, 19 | pathname, 20 | current, 21 | prev, 22 | 23 | initialState: getInitialState( 24 | prev.pathname, 25 | {}, 26 | prev.type, 27 | prev.payload, 28 | routesMap, 29 | history 30 | ), 31 | 32 | routesMap, 33 | 34 | action: { 35 | type, 36 | payload: { param: 'bar' }, 37 | meta: { 38 | location: { 39 | current, 40 | prev, 41 | kind: 'load', 42 | history: { 43 | entries: history.entries.slice(0), // history.entries.map(entry => entry.pathname) 44 | index: history.index, 45 | length: history.length 46 | } 47 | } 48 | } 49 | }, 50 | 51 | expectState(state) { 52 | expect(state.pathname).toEqual(pathname) 53 | expect(state.type).toEqual(type) 54 | expect(state.payload).toEqual({ param: 'bar' }) 55 | expect(state.prev).toEqual(prev) 56 | expect(state.kind).toEqual('load') 57 | 58 | expect(state).toMatchSnapshot() 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /__test-helpers__/setup.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, createStore, compose } from 'redux' 2 | import reduxThunk from 'redux-thunk' 3 | import connectRoutes from '../src/connectRoutes' 4 | 5 | const setup = ( 6 | path = '/', 7 | options = { title: 'title', location: 'location' }, 8 | routesMap 9 | ) => { 10 | routesMap = routesMap || { 11 | FIRST: '/first', 12 | SECOND: '/second/:param', 13 | THIRD: '/third' 14 | } 15 | 16 | options.initialEntries = path 17 | 18 | options.extra = 'extra-arg' 19 | const tools = connectRoutes(routesMap, options) 20 | return { ...tools, routesMap } 21 | } 22 | 23 | export default setup 24 | 25 | export const setupAll = ( 26 | path, 27 | options, 28 | { rootReducer, preLoadedState, routesMap } = {} 29 | ) => { 30 | const tools = setup(path, options, routesMap) 31 | const { middleware, reducer, enhancer } = tools 32 | const middlewares = applyMiddleware(reduxThunk, middleware) 33 | const enhancers = compose(enhancer, middlewares) 34 | 35 | rootReducer = 36 | rootReducer || 37 | ((state = {}, action = {}) => ({ 38 | location: reducer(state.location, action), 39 | title: action.type 40 | })) 41 | 42 | const store = createStore(rootReducer, preLoadedState, enhancers) 43 | return { 44 | ...tools, 45 | store 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /__test-helpers__/setupJest.js: -------------------------------------------------------------------------------- 1 | // the history package generates keys for history entries using Math.random 2 | // this makes it deterministic (note: it can be anything since we dont use it) 3 | global.Math.random = () => '123456789' 4 | -------------------------------------------------------------------------------- /__test-helpers__/setupThunk.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose, combineReducers } from 'redux' 2 | import connectRoutes from '../src/connectRoutes' 3 | 4 | export default (path = '/', thunkArg, opts) => { 5 | const routesMap = { 6 | FIRST: '/first', 7 | SECOND: { path: '/second/:param', thunk: thunkArg }, 8 | THIRD: { path: '/third/:param' } 9 | } 10 | 11 | const options = { extra: 'extra-arg', initialEntries: path, ...opts } 12 | 13 | const { middleware, enhancer, thunk, reducer, history } = connectRoutes( 14 | routesMap, 15 | options 16 | ) 17 | 18 | const rootReducer = combineReducers({ 19 | location: reducer 20 | }) 21 | 22 | const middlewares = applyMiddleware(middleware) 23 | 24 | const store = createStore(rootReducer, compose(enhancer, middlewares)) 25 | 26 | return { store, thunk, history } 27 | } 28 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/action-creators.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`addRoutes(routes) - adds routes to routesMap 1`] = ` 4 | Object { 5 | "location": Object { 6 | "hasSSR": undefined, 7 | "history": undefined, 8 | "kind": "load", 9 | "pathname": "/", 10 | "payload": Object {}, 11 | "prev": Object { 12 | "pathname": "", 13 | "payload": Object {}, 14 | "type": "", 15 | }, 16 | "routesMap": Object { 17 | "BAR": Object { 18 | "path": "/bar", 19 | }, 20 | "FIRST": "/first", 21 | "FOO": "/foo", 22 | "SECOND": "/second/:param", 23 | "THIRD": "/third", 24 | }, 25 | "type": "@@redux-first-router/NOT_FOUND", 26 | }, 27 | "title": "@@redux-first-router/ADD_ROUTES", 28 | } 29 | `; 30 | 31 | exports[`historyCreateAction() - returns action created when history/address_bar chanages 1`] = ` 32 | Object { 33 | "meta": Object { 34 | "location": Object { 35 | "current": Object { 36 | "pathname": "/info/foo", 37 | "payload": Object { 38 | "param": "foo", 39 | }, 40 | "type": "INFO_PARAM", 41 | }, 42 | "history": undefined, 43 | "kind": "pop", 44 | "prev": Object { 45 | "pathname": "/prev", 46 | "payload": Object {}, 47 | "type": "PREV", 48 | }, 49 | }, 50 | }, 51 | "payload": Object { 52 | "param": "foo", 53 | }, 54 | "type": "INFO_PARAM", 55 | } 56 | `; 57 | 58 | exports[`middlewareCreate() - [action not matched to any routePath] 1`] = ` 59 | Object { 60 | "meta": Object { 61 | "location": Object { 62 | "current": Object { 63 | "pathname": "/not-found", 64 | "payload": Object { 65 | "someKey": "foo", 66 | }, 67 | "type": "@@redux-first-router/NOT_FOUND", 68 | }, 69 | "history": undefined, 70 | "kind": undefined, 71 | "prev": Object { 72 | "pathname": "/prev", 73 | "payload": Object {}, 74 | "type": "PREV", 75 | }, 76 | }, 77 | }, 78 | "payload": Object { 79 | "someKey": "foo", 80 | }, 81 | "type": "@@redux-first-router/NOT_FOUND", 82 | } 83 | `; 84 | 85 | exports[`middlewareCreate() - returns action created when middleware detects connected/matched action.type 1`] = ` 86 | Object { 87 | "meta": Object { 88 | "location": Object { 89 | "current": Object { 90 | "pathname": "/info/foo", 91 | "payload": Object { 92 | "param": "foo", 93 | }, 94 | "type": "INFO_PARAM", 95 | }, 96 | "history": undefined, 97 | "kind": "push", 98 | "prev": Object { 99 | "pathname": "/prev", 100 | "payload": Object {}, 101 | "type": "PREV", 102 | }, 103 | }, 104 | }, 105 | "payload": Object { 106 | "param": "foo", 107 | }, 108 | "type": "INFO_PARAM", 109 | } 110 | `; 111 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/connectRoutes.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`QUERY: currentPathName changes, but pathname stays the same (only query changes) 1`] = ` 4 | Object { 5 | "location": Object { 6 | "hasSSR": undefined, 7 | "history": undefined, 8 | "kind": "push", 9 | "pathname": "/first", 10 | "payload": Object {}, 11 | "prev": Object { 12 | "pathname": "/first", 13 | "payload": Object {}, 14 | "query": Object { 15 | "baz": "69", 16 | "foo": "bar", 17 | }, 18 | "search": "foo=bar&baz=69", 19 | "type": "FIRST", 20 | }, 21 | "query": Object { 22 | "baz": "70", 23 | "foo": "car", 24 | }, 25 | "routesMap": Object { 26 | "FIRST": "/first", 27 | "SECOND": "/second/:param", 28 | "THIRD": "/third", 29 | }, 30 | "search": "foo=car&baz=70", 31 | "type": "FIRST", 32 | }, 33 | "title": "FIRST", 34 | } 35 | `; 36 | 37 | exports[`QUERY: dispatched as action.meta.query 1`] = ` 38 | Object { 39 | "location": Object { 40 | "hasSSR": undefined, 41 | "history": undefined, 42 | "kind": "push", 43 | "pathname": "/third", 44 | "payload": Object {}, 45 | "prev": Object { 46 | "pathname": "/first", 47 | "payload": Object {}, 48 | "query": Object { 49 | "baz": 69, 50 | "foo": "bar", 51 | }, 52 | "search": "baz=69&foo=bar", 53 | "type": "FIRST", 54 | }, 55 | "query": Object { 56 | "baz": 69, 57 | "foo": "bar", 58 | }, 59 | "routesMap": Object { 60 | "FIRST": "/first", 61 | "SECOND": "/second/:param", 62 | "THIRD": "/third", 63 | }, 64 | "search": "baz=69&foo=bar", 65 | "type": "THIRD", 66 | }, 67 | "title": "THIRD", 68 | } 69 | `; 70 | 71 | exports[`QUERY: dispatched as action.payload.query 1`] = ` 72 | Object { 73 | "location": Object { 74 | "hasSSR": undefined, 75 | "history": undefined, 76 | "kind": "back", 77 | "pathname": "/third", 78 | "payload": Object {}, 79 | "prev": Object { 80 | "pathname": "/first", 81 | "payload": Object {}, 82 | "type": "FIRST", 83 | }, 84 | "routesMap": Object { 85 | "FIRST": "/first", 86 | "SECOND": "/second/:param", 87 | "THIRD": "/third", 88 | }, 89 | "type": "THIRD", 90 | }, 91 | "title": "THIRD", 92 | } 93 | `; 94 | 95 | exports[`QUERY: dispatched as action.query 1`] = ` 96 | Object { 97 | "location": Object { 98 | "hasSSR": undefined, 99 | "history": undefined, 100 | "kind": "push", 101 | "pathname": "/third", 102 | "payload": Object {}, 103 | "prev": Object { 104 | "pathname": "/first", 105 | "payload": Object {}, 106 | "query": Object { 107 | "baz": 69, 108 | "foo": "bar", 109 | }, 110 | "search": "baz=69&foo=bar", 111 | "type": "FIRST", 112 | }, 113 | "query": Object { 114 | "baz": 69, 115 | "foo": "bar", 116 | }, 117 | "routesMap": Object { 118 | "FIRST": "/first", 119 | "SECOND": "/second/:param", 120 | "THIRD": "/third", 121 | }, 122 | "search": "baz=69&foo=bar", 123 | "type": "THIRD", 124 | }, 125 | "title": "THIRD", 126 | } 127 | `; 128 | 129 | exports[`QUERY: generated from pathToAction within <Link /> 1`] = ` 130 | Object { 131 | "location": Object { 132 | "hasSSR": undefined, 133 | "history": undefined, 134 | "kind": "push", 135 | "pathname": "/first", 136 | "payload": Object {}, 137 | "prev": Object { 138 | "pathname": "/first", 139 | "payload": Object {}, 140 | "query": Object { 141 | "baz": "69", 142 | "foo": "bar", 143 | }, 144 | "search": "baz=69&foo=bar", 145 | "type": "FIRST", 146 | }, 147 | "query": Object { 148 | "baz": "70", 149 | "foo": "car", 150 | }, 151 | "routesMap": Object { 152 | "FIRST": "/first", 153 | "SECOND": "/second/:param", 154 | "THIRD": "/third", 155 | }, 156 | "search": "baz=70&foo=car", 157 | "type": "FIRST", 158 | }, 159 | "title": "FIRST", 160 | } 161 | `; 162 | 163 | exports[`QUERY: history.push("/path?search=foo") 1`] = ` 164 | Object { 165 | "location": Object { 166 | "hasSSR": undefined, 167 | "history": undefined, 168 | "kind": "push", 169 | "pathname": "/third", 170 | "payload": Object {}, 171 | "prev": Object { 172 | "pathname": "/first", 173 | "payload": Object {}, 174 | "query": Object { 175 | "baz": "69", 176 | "foo": "bar", 177 | }, 178 | "search": "foo=bar&baz=69", 179 | "type": "FIRST", 180 | }, 181 | "query": Object { 182 | "baz": "69", 183 | "foo": "bar", 184 | }, 185 | "routesMap": Object { 186 | "FIRST": "/first", 187 | "SECOND": "/second/:param", 188 | "THIRD": "/third", 189 | }, 190 | "search": "foo=bar&baz=69", 191 | "type": "THIRD", 192 | }, 193 | "title": "THIRD", 194 | } 195 | `; 196 | 197 | exports[`enhancer -> _historyAttemptDispatchAction() dispatches action matching pathname when history location changes 1`] = ` 198 | Object { 199 | "meta": Object { 200 | "location": Object { 201 | "current": Object { 202 | "pathname": "/second/foo", 203 | "payload": Object { 204 | "param": "foo", 205 | }, 206 | "type": "SECOND", 207 | }, 208 | "history": undefined, 209 | "kind": "pop", 210 | "prev": Object { 211 | "pathname": "", 212 | "payload": Object {}, 213 | "type": "", 214 | }, 215 | }, 216 | }, 217 | "payload": Object { 218 | "param": "foo", 219 | }, 220 | "type": "SECOND", 221 | } 222 | `; 223 | 224 | exports[`middleware dispatches location-aware action, changes address bar + document.title 1`] = ` 225 | Object { 226 | "hasSSR": undefined, 227 | "history": undefined, 228 | "kind": "load", 229 | "pathname": "/", 230 | "payload": Object {}, 231 | "prev": Object { 232 | "pathname": "", 233 | "payload": Object {}, 234 | "type": "", 235 | }, 236 | "routesMap": Object { 237 | "FIRST": "/first", 238 | "SECOND": "/second/:param", 239 | "THIRD": "/third", 240 | }, 241 | "type": "@@redux-first-router/NOT_FOUND", 242 | } 243 | `; 244 | 245 | exports[`middleware dispatches location-aware action, changes address bar + document.title 2`] = ` 246 | Object { 247 | "meta": Object { 248 | "location": Object { 249 | "current": Object { 250 | "pathname": "/second/bar", 251 | "payload": Object { 252 | "param": "bar", 253 | }, 254 | "type": "SECOND", 255 | }, 256 | "history": undefined, 257 | "kind": "push", 258 | "prev": Object { 259 | "pathname": "/", 260 | "payload": Object {}, 261 | "type": "@@redux-first-router/NOT_FOUND", 262 | }, 263 | }, 264 | }, 265 | "payload": Object { 266 | "param": "bar", 267 | }, 268 | "type": "SECOND", 269 | } 270 | `; 271 | 272 | exports[`middleware dispatches location-aware action, changes address bar + document.title 3`] = ` 273 | Object { 274 | "location": Object { 275 | "hasSSR": undefined, 276 | "history": undefined, 277 | "kind": "push", 278 | "pathname": "/second/bar", 279 | "payload": Object { 280 | "param": "bar", 281 | }, 282 | "prev": Object { 283 | "pathname": "/", 284 | "payload": Object {}, 285 | "type": "@@redux-first-router/NOT_FOUND", 286 | }, 287 | "routesMap": Object { 288 | "FIRST": "/first", 289 | "SECOND": "/second/:param", 290 | "THIRD": "/third", 291 | }, 292 | "type": "SECOND", 293 | }, 294 | "title": "SECOND", 295 | } 296 | `; 297 | 298 | exports[`middleware if onBeforeChange dispatches redirect, route changes with kind === "redirect" 1`] = ` 299 | Object { 300 | "hasSSR": undefined, 301 | "history": undefined, 302 | "kind": "redirect", 303 | "pathname": "/third", 304 | "payload": Object {}, 305 | "prev": Object { 306 | "pathname": "/second/bar", 307 | "payload": Object { 308 | "param": "bar", 309 | }, 310 | "type": "SECOND", 311 | }, 312 | "routesMap": Object { 313 | "FIRST": "/first", 314 | "SECOND": "/second/:param", 315 | "THIRD": "/third", 316 | }, 317 | "type": "THIRD", 318 | } 319 | `; 320 | 321 | exports[`middleware onBeforeChange redirect on server results in 1 history entry 1`] = ` 322 | Object { 323 | "hasSSR": true, 324 | "history": undefined, 325 | "kind": "redirect", 326 | "pathname": "/third", 327 | "payload": Object {}, 328 | "prev": Object { 329 | "pathname": "/second/bar", 330 | "payload": Object { 331 | "param": "bar", 332 | }, 333 | "type": "SECOND", 334 | }, 335 | "routesMap": Object { 336 | "FIRST": "/first", 337 | "SECOND": "/second/:param", 338 | "THIRD": "/third", 339 | }, 340 | "type": "THIRD", 341 | } 342 | `; 343 | 344 | exports[`middleware user dispatches NOT_FOUND and middleware adds missing info to action 1`] = ` 345 | Object { 346 | "meta": Object { 347 | "location": Object { 348 | "current": Object { 349 | "pathname": "/first", 350 | "payload": Object {}, 351 | "type": "@@redux-first-router/NOT_FOUND", 352 | }, 353 | "history": undefined, 354 | "kind": "load", 355 | "prev": Object { 356 | "pathname": "/first", 357 | "payload": Object {}, 358 | "type": "FIRST", 359 | }, 360 | }, 361 | }, 362 | "payload": Object {}, 363 | "type": "@@redux-first-router/NOT_FOUND", 364 | } 365 | `; 366 | 367 | exports[`middleware user dispatches NOT_FOUND redirect and middleware adds missing info to action 1`] = ` 368 | Object { 369 | "meta": Object { 370 | "location": Object { 371 | "current": Object { 372 | "pathname": "/not-found", 373 | "payload": Object {}, 374 | "type": "@@redux-first-router/NOT_FOUND", 375 | }, 376 | "history": undefined, 377 | "kind": "redirect", 378 | "prev": Object { 379 | "pathname": "/first", 380 | "payload": Object {}, 381 | "type": "FIRST", 382 | }, 383 | }, 384 | }, 385 | "payload": Object {}, 386 | "type": "@@redux-first-router/NOT_FOUND", 387 | } 388 | `; 389 | 390 | exports[`title and location options as selector functions 1`] = ` 391 | Object { 392 | "meta": Object { 393 | "location": Object { 394 | "current": Object { 395 | "pathname": "/first", 396 | "payload": Object {}, 397 | "type": "FIRST", 398 | }, 399 | "history": undefined, 400 | "kind": "push", 401 | "prev": Object { 402 | "pathname": "/first", 403 | "payload": Object {}, 404 | "type": "FIRST", 405 | }, 406 | }, 407 | }, 408 | "payload": Object {}, 409 | "type": "FIRST", 410 | } 411 | `; 412 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/createLocationReducer.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`createLocationReducer() - maintains address bar pathname state and current + previous matched location-aware actions 1`] = ` 4 | Object { 5 | "hasSSR": undefined, 6 | "history": Object { 7 | "entries": Array [ 8 | Object { 9 | "hash": "", 10 | "key": "345678", 11 | "pathname": "/first", 12 | "search": "", 13 | "state": undefined, 14 | }, 15 | Object { 16 | "hash": "", 17 | "key": "345678", 18 | "pathname": "/second/bar", 19 | "search": "", 20 | "state": undefined, 21 | }, 22 | ], 23 | "index": 1, 24 | "length": 2, 25 | }, 26 | "kind": "load", 27 | "pathname": "/second/bar", 28 | "payload": Object { 29 | "param": "bar", 30 | }, 31 | "prev": Object { 32 | "pathname": "/first", 33 | "payload": Object {}, 34 | "type": "FIRST", 35 | }, 36 | "routesMap": Object { 37 | "FIRST": "/first", 38 | "SECOND": "/second/:param", 39 | }, 40 | "type": "SECOND", 41 | } 42 | `; 43 | 44 | exports[`createLocationReducer() - reduces action.meta.location.kind being updated 1`] = ` 45 | Object { 46 | "hasSSR": undefined, 47 | "history": Object { 48 | "entries": Array [ 49 | Object { 50 | "hash": "", 51 | "key": "345678", 52 | "pathname": "/first", 53 | "search": "", 54 | "state": undefined, 55 | }, 56 | Object { 57 | "hash": "", 58 | "key": "345678", 59 | "pathname": "/first", 60 | "search": "", 61 | "state": undefined, 62 | }, 63 | ], 64 | "index": 1, 65 | "length": 2, 66 | }, 67 | "kind": "load", 68 | "pathname": "/first", 69 | "payload": Object { 70 | "param": "bar", 71 | }, 72 | "prev": Object { 73 | "pathname": "/first", 74 | "payload": Object {}, 75 | "type": "FIRST", 76 | }, 77 | "routesMap": Object { 78 | "FIRST": "/first", 79 | "SECOND": "/second/:param", 80 | }, 81 | "type": "FIRST", 82 | } 83 | `; 84 | 85 | exports[`getInitialState() returns state.history === undefined when using createBrowserHistory 1`] = ` 86 | Object { 87 | "hasSSR": undefined, 88 | "history": undefined, 89 | "kind": undefined, 90 | "pathname": "/first", 91 | "payload": Object {}, 92 | "prev": Object { 93 | "pathname": "", 94 | "payload": Object {}, 95 | "type": "", 96 | }, 97 | "routesMap": Object { 98 | "FIRST": "/first", 99 | }, 100 | "type": "FIRST", 101 | } 102 | `; 103 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/pure-utils.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`nestAction(pathname, receivedAction, prevLocation, history, kind?) nestAction properly formats/nests action object 1`] = ` 4 | Object { 5 | "meta": Object { 6 | "info": "something", 7 | "location": Object { 8 | "current": Object { 9 | "pathname": "/path", 10 | "payload": Object { 11 | "bar": "baz", 12 | }, 13 | "type": "FOO", 14 | }, 15 | "history": undefined, 16 | "kind": undefined, 17 | "prev": Object { 18 | "pathname": "previous", 19 | "payload": Object { 20 | "bla": "prev", 21 | }, 22 | "type": "PREV", 23 | }, 24 | }, 25 | }, 26 | "payload": Object { 27 | "bar": "baz", 28 | }, 29 | "type": "FOO", 30 | } 31 | `; 32 | 33 | exports[`nestAction(pathname, receivedAction, prevLocation, history, kind?) nestHistory formats simplified history object for action + state 1`] = ` 34 | Object { 35 | "entries": Array [ 36 | Object { 37 | "hash": "", 38 | "key": "345678", 39 | "pathname": "/", 40 | "search": "", 41 | "state": undefined, 42 | }, 43 | Object { 44 | "hash": "", 45 | "key": "345678", 46 | "pathname": "/foo", 47 | "search": "", 48 | "state": undefined, 49 | }, 50 | Object { 51 | "hash": "", 52 | "key": "345678", 53 | "pathname": "/bar/baz", 54 | "search": "", 55 | "state": undefined, 56 | }, 57 | ], 58 | "index": 2, 59 | "length": 3, 60 | } 61 | `; 62 | -------------------------------------------------------------------------------- /__tests__/action-creators.js: -------------------------------------------------------------------------------- 1 | import { createMemoryHistory } from 'rudy-history' 2 | 3 | import historyCreateAction from '../src/action-creators/historyCreateAction' 4 | import middlewareCreate from '../src/action-creators/middlewareCreateAction' 5 | import redirect from '../src/action-creators/redirect' 6 | import addRoutes from '../src/action-creators/addRoutes' 7 | import { NOT_FOUND } from '../src/index' 8 | 9 | import { setupAll } from '../__test-helpers__/setup' 10 | 11 | it('historyCreateAction() - returns action created when history/address_bar chanages', () => { 12 | const history = createMemoryHistory() 13 | const pathname = '/info/foo' 14 | const prevLocation = { pathname: '/prev', type: 'PREV', payload: {} } 15 | const kind = 'pop' 16 | const routesMap = { 17 | INFO: '/info', 18 | INFO_PARAM: '/info/:param' 19 | } 20 | 21 | const action = historyCreateAction( 22 | pathname, 23 | routesMap, 24 | prevLocation, 25 | history, 26 | kind 27 | ) /*? $.meta.location */ 28 | 29 | expect(action).toMatchSnapshot() 30 | 31 | expect(action.type).toEqual('INFO_PARAM') 32 | expect(action.payload).toEqual({ param: 'foo' }) 33 | 34 | expect(action.type).toEqual(action.meta.location.current.type) 35 | expect(action.payload).toEqual(action.meta.location.current.payload) 36 | 37 | expect(action.meta.location.prev).toEqual(prevLocation) 38 | expect(action.meta.location.current).toEqual({ 39 | pathname, 40 | type: 'INFO_PARAM', 41 | payload: { param: 'foo' } 42 | }) 43 | 44 | expect(action.meta.location.kind).toEqual('pop') 45 | }) 46 | 47 | it('middlewareCreate() - returns action created when middleware detects connected/matched action.type', () => { 48 | const history = createMemoryHistory() 49 | const receivedAction = { type: 'INFO_PARAM', payload: { param: 'foo' } } 50 | const routesMap = { 51 | INFO: '/info', 52 | INFO_PARAM: '/info/:param' 53 | } 54 | const prevLocation = { pathname: '/prev', type: 'PREV', payload: {} } 55 | 56 | const action = middlewareCreate( 57 | receivedAction, 58 | routesMap, 59 | prevLocation, 60 | history 61 | ) /*? $.meta.location */ 62 | 63 | expect(action.type).toEqual('INFO_PARAM') 64 | expect(action.payload).toEqual({ param: 'foo' }) 65 | 66 | expect(action.type).toEqual(action.meta.location.current.type) 67 | expect(action.payload).toEqual(action.meta.location.current.payload) 68 | 69 | expect(action.meta.location.prev).toEqual(prevLocation) 70 | expect(action.meta.location.current).toEqual({ 71 | pathname: '/info/foo', 72 | type: 'INFO_PARAM', 73 | payload: { param: 'foo' } 74 | }) 75 | 76 | expect(action.meta.location.kind).toEqual('push') 77 | 78 | expect(action).toMatchSnapshot() 79 | }) 80 | 81 | it('middlewareCreate() - [action not matched to any routePath]', () => { 82 | const history = createMemoryHistory() 83 | const receivedAction = { type: 'BLA', payload: { someKey: 'foo' } } 84 | const routesMap = { 85 | INFO: '/info', 86 | INFO_PARAM: '/info/:param' 87 | } 88 | const prevLocation = { pathname: '/prev', type: 'PREV', payload: {} } 89 | 90 | const action = middlewareCreate( 91 | receivedAction, 92 | routesMap, 93 | prevLocation, 94 | history, 95 | '/not-found' 96 | ) /*? $.meta.location */ 97 | 98 | expect(action.type).toEqual(NOT_FOUND) 99 | expect(action.payload).toEqual({ someKey: 'foo' }) 100 | 101 | expect(action.meta.location.prev).toEqual(prevLocation) 102 | expect(action.meta.location.current.pathname).toEqual('/not-found') 103 | 104 | expect(action).toMatchSnapshot() 105 | }) 106 | 107 | it('redirect(action) - sets action.meta.location.kind === "redirect"', () => { 108 | const receivedAction = { type: 'ANYTHING' } 109 | const action = redirect(receivedAction) /*? */ 110 | 111 | expect(action.meta.location.kind).toEqual('redirect') 112 | }) 113 | 114 | it('addRoutes(routes) - adds routes to routesMap', () => { 115 | const newRoutes = { 116 | FOO: '/foo', 117 | BAR: { path: '/bar' } 118 | } 119 | 120 | const { store } = setupAll() 121 | 122 | const thunk = addRoutes(newRoutes) 123 | store.dispatch(thunk) 124 | expect(store.getState()).toMatchSnapshot() 125 | 126 | store.dispatch({ type: 'FOO' }) 127 | expect(store.getState().location.type).toEqual('FOO') 128 | }) 129 | -------------------------------------------------------------------------------- /__tests__/clientOnlyApi.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux' 2 | 3 | import setup, { setupAll } from '../__test-helpers__/setup' 4 | import { push, replace, back, next } from '../src/connectRoutes' 5 | 6 | it('push: verify client-only `push` function calls `history.push()` using history from enclosed state', () => { 7 | jest.useFakeTimers() 8 | const { store, history, windowDocument } = setupAll('/first') 9 | 10 | push('/second/bar') // THIS IS THE TARGET OF THE TEST. Notice `push` is imported 11 | const { location } = store.getState() 12 | 13 | expect(location.type).toEqual('SECOND') 14 | expect(location.pathname).toEqual('/second/bar') 15 | 16 | jest.runAllTimers() // title set in next tick 17 | expect(windowDocument.title).toEqual('SECOND') 18 | 19 | expect(history.length).toEqual(2) 20 | }) 21 | 22 | it('replace: verify client-only `replace` function calls `history.replace()` using history from enclosed state', () => { 23 | const { store, history } = setupAll('/first') 24 | 25 | replace('/second/bar') 26 | const { location } = store.getState() 27 | 28 | expect(location.type).toEqual('SECOND') 29 | expect(location.pathname).toEqual('/second/bar') 30 | 31 | expect(history.length).toEqual(1) // key difference between this test and previous `push` test 32 | }) 33 | 34 | it('back: verify client-only `back` and `next` functions call `history.goBack/goForward()` using history from enclosed state', () => { 35 | const { store, history } = setupAll('/first') 36 | 37 | history.push('/second/bar') 38 | let location = store.getState().location 39 | expect(location.type).toEqual('SECOND') 40 | expect(location.pathname).toEqual('/second/bar') 41 | 42 | back() // THIS IS WHAT WE ARE VERIFYING 43 | location = store.getState().location 44 | expect(location.type).toEqual('FIRST') 45 | expect(location.pathname).toEqual('/first') 46 | 47 | next() // THIS IS WHAT WE ARE VERIFYING 48 | location = store.getState().location 49 | expect(location.type).toEqual('SECOND') 50 | expect(location.pathname).toEqual('/second/bar') 51 | }) 52 | 53 | it('verify window.document is not used server side', () => { 54 | window.isSSR = true 55 | jest.useFakeTimers() 56 | 57 | const { store, windowDocument } = setupAll('/first') 58 | 59 | store.dispatch({ type: 'SECOND', payload: { param: 'foo' } }) 60 | 61 | const originalTitle = document.title 62 | jest.runAllTimers() // title set in next tick 63 | expect(windowDocument.title).toEqual('SECOND') // fake document object used instead 64 | expect(document.title).toEqual(originalTitle) 65 | 66 | delete window.isSSR 67 | }) 68 | -------------------------------------------------------------------------------- /__tests__/createLocationReducer.js: -------------------------------------------------------------------------------- 1 | import { createMemoryHistory } from 'rudy-history' 2 | import createLocationReducer, { 3 | getInitialState 4 | } from '../src/reducer/createLocationReducer' 5 | import { NOT_FOUND } from '../src/index' 6 | import reducerParameters from '../__test-helpers__/reducerParameters' 7 | 8 | it('createLocationReducer() - maintains address bar pathname state and current + previous matched location-aware actions', () => { 9 | const { initialState, routesMap, action, expectState } = reducerParameters( 10 | 'SECOND', 11 | '/second/bar' 12 | ) 13 | 14 | const reducer = createLocationReducer(initialState, routesMap) 15 | const state = reducer(undefined, action) /*? */ 16 | 17 | expectState(state) 18 | }) 19 | 20 | it('createLocationReducer() - reduces action.meta.location.kind being updated', () => { 21 | const { initialState, action, routesMap, expectState } = reducerParameters( 22 | 'FIRST', 23 | '/first' 24 | ) 25 | 26 | const reducer = createLocationReducer(initialState, routesMap) 27 | const state = reducer(undefined, action) /*? */ 28 | 29 | const nextAction = { 30 | ...action, 31 | meta: { 32 | location: { 33 | ...action.meta.location, 34 | kind: 'push' 35 | } 36 | } 37 | } 38 | const nextState = reducer(state, nextAction) /*? */ 39 | 40 | expectState(state) 41 | expect(nextState.kind).toEqual('push') 42 | }) 43 | 44 | it('locationReducer() reduces action.type === NOT_FOUND', () => { 45 | const { initialState, routesMap, action } = reducerParameters( 46 | NOT_FOUND, 47 | '/foo' 48 | ) 49 | 50 | const reducer = createLocationReducer(initialState, routesMap) 51 | const state = reducer(undefined, action) /*? */ 52 | 53 | expect(state.type).toEqual(NOT_FOUND) 54 | expect(state.pathname).toEqual('/foo') 55 | }) 56 | 57 | it('locationReducer() reduces non matched action.type and returns initialState', () => { 58 | const { initialState, routesMap, action } = reducerParameters( 59 | 'THIRD', 60 | '/third' 61 | ) 62 | 63 | const reducer = createLocationReducer(initialState, routesMap) 64 | const state = reducer(undefined, action) /*? */ 65 | 66 | expect(state).toEqual(initialState) 67 | }) 68 | 69 | it('getInitialState() returns state.history === undefined when using createBrowserHistory', () => { 70 | const pathname = '/first' 71 | const history = createMemoryHistory({ initialEntries: [pathname] }) 72 | const current = { pathname, type: 'FIRST', payload: {} } 73 | const routesMap = { 74 | FIRST: '/first' 75 | } 76 | 77 | history.entries = undefined 78 | const initialState = getInitialState( 79 | current.pathname, 80 | {}, 81 | current.type, 82 | current.payload, 83 | routesMap, 84 | history 85 | ) 86 | 87 | expect(initialState.history).not.toBeDefined() 88 | expect(initialState).toMatchSnapshot() 89 | }) 90 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /docs/action.md: -------------------------------------------------------------------------------- 1 | # Flux Standard Actions (FSA) 2 | One of the goals of **Redux First Router** is to *NOT* alter your actions and be 100% *flux standard action*-compliant. That allows 3 | for automatic support for packages such as `redux-actions`. 4 | 5 | So simply put, to do that, we stuffed all the info our middleware, reducer, etc, depends on in the `meta` key of your actions. 6 | 7 | Without further ado, let's take a look at what your actions look like--here's our pure utility function, `nestAction()`, used to format 8 | and nest your actions: 9 | 10 | 11 | ## The Meta key is the *key* 12 | *Note: If you or other middleware are making use of the `meta` key we'll make sure to hold on to that info as well.* 13 | 14 | ```javascript 15 | const nestAction = ( 16 | pathname: string, 17 | receivedAction: Action, 18 | prev: Location, 19 | kind?: string 20 | ): Action => { 21 | const { type, payload = {}, meta } = receivedAction 22 | 23 | return { 24 | type, // this will remain exactly what you dispatched 25 | payload, // this will remain exactly what you dispatched 26 | 27 | // no additional keys! 28 | 29 | meta: { // all routing information crammed into the meta key 30 | ...meta, 31 | location: { 32 | current: { 33 | pathname, 34 | type, 35 | payload 36 | }, 37 | prev, 38 | kind 39 | } 40 | } 41 | } 42 | } 43 | ``` 44 | 45 | So in short, we take a more basic action you dispatch (or that the address-bar listening enhancer dispatches) and assign 46 | all the location-related information we have to the `location` key within the `meta` key. 47 | 48 | ## Flow Type 49 | For an even clearer sense of what is on the `location` key of your *flux standard actions*, here's its ***Flow*** type: 50 | 51 | ```javascript 52 | type Action = { 53 | type: string, 54 | payload: Object, 55 | meta: Meta 56 | } 57 | 58 | type Meta = { 59 | location: { 60 | current: Location, 61 | prev: Location, 62 | kind: 'load' | 'redirect' | 'back' | 'next' | 'pop' 63 | } 64 | } 65 | 66 | type Location = { 67 | pathname: string, 68 | type: string, 69 | payload: Object 70 | } 71 | ``` 72 | 73 | ## The error key 74 | RFR will ignore actions which contain a truthy `error` key. 75 | This is in case you want to use a different middleware to handle errors. 76 | 77 | ## Conclusion 78 | You will rarely need to inspect the `meta` key. It's primarily for use by our `location` reducer. However, a common 79 | use for it is to use the `kind` key to make some determinations in your 80 | reducers. `pop` simply indicates the browser back/next buttons were used, where as `back` and `next` indicate explicitly 81 | which direction you were going, which we can determine when using `createMemoryHistory` such as in React Native. In conjunction with `kind`, you can use the `prev` route to 82 | do things like declaratively trigger fancy animations in your components because it will indicate which direction 83 | the user is moving in a funnel/sequence of pages. 84 | -------------------------------------------------------------------------------- /docs/addRoutes.md: -------------------------------------------------------------------------------- 1 | # addRoutes 2 | 3 | Sometimes you may want to dynamically add routes to routesMap, 4 | for example so that you can codesplit routesMap. 5 | You can do this using the `addRoutes` function. 6 | 7 | ```javascript 8 | import { addRoutes } from 'redux-first-router' 9 | 10 | const newRoutes = { 11 | DYNAMIC_ROUTE: '/some/path' 12 | } 13 | 14 | store.dispatch(addRoutes(newRoutes)) 15 | ``` 16 | 17 | The new routes are added to routesMap after the existing routes, 18 | so existing routes will take precedence over the newly added routes 19 | in the case that they overlap. 20 | 21 | See the [original documentation in a comment](https://github.com/faceyspacey/redux-first-router/issues/62#issuecomment-322558836) 22 | -------------------------------------------------------------------------------- /docs/blocking-navigation.md: -------------------------------------------------------------------------------- 1 | # Blocking navigation 2 | 3 | Sometimes you may want to block navigation away from the current route, 4 | for example to prompt the user to save their changes. 5 | 6 | This is supported - here's how you use it: 7 | 8 | ```js 9 | const routesMap = { 10 | HOME: '/' 11 | FOO: { 12 | path: '/foo', 13 | confirmLeave: (state, action) => { 14 | if (!state.formComplete && action.type === 'HOME' ) { 15 | return 'Are you sure you want to leave without completing your purchase?' 16 | } 17 | } 18 | } 19 | } 20 | ``` 21 | 22 | So each route can have a `confirmLeave` option, and if you return a string it will be shown in the `confirm` yes|no dialog. If you return a falsy value such as undefined, the user will be able to navigate away from the current route without being shown any dialog. 23 | 24 | If you'd like to customize that dialog (which is required in React Native since there is no `window.confirm` in React Native), you can pass a `displayConfirmLeave` option to `connectRoutes` like so: 25 | 26 | ```js 27 | const options = { 28 | displayConfirmLeave: (message, callback) => { 29 | showModalConfirmationUI({ 30 | message, 31 | stay: () => callback(false), 32 | leave: () => callback(true) 33 | }) 34 | } 35 | } 36 | 37 | connectRoutes(history, routesMap, options) 38 | ``` 39 | > so `showModalConfirmationUI` is an example of a function you can make to display a confirmation modal. If the user presses **OK** call the `callback` with `true` to proceed with navigating to the the route the user was going to. And pass `false` to block the user. 40 | 41 | One special thing to note is that if you define this function in the same scope that is likely created below, you can use `store.dispatch` to trigger showing the modal instead. You could even do: 42 | 43 | ```js 44 | store.dispatch({ type: 'SHOW_BLOCK_NAVIGATION_MODAL', payload: { callback } }) 45 | ``` 46 | 47 | and then grab the callback in your `<Modal />` component. Since this is happening solely on the client and the store will never need to serialize that `callback` (as you do when rehydrating from the server), this is a fine pattern. Redux store state can contain functions, components, etc, if you choose. The result in this case is something highly idiomatic when it comes to how you render the React component corresponding to the modal. *No imperative tricks required.* 48 | 49 | Here's a final example: 50 | 51 | *src/reducers/blockNavigation.js:* 52 | 53 | ```js 54 | export default (state = {}, action = {}) => { 55 | switch (type): { 56 | case 'SHOW_BLOCK_NAVIGATION_MODAL': 57 | const { message, canLeave } = action.payload 58 | return { message, canLeave } 59 | case 'HIDE_BLOCK_NAVIGATION_MODEL': 60 | return {} 61 | default: 62 | return state 63 | } 64 | } 65 | ``` 66 | 67 | *src/components/BlockModal.js:* 68 | 69 | ```js 70 | const BlockModal = ({ show, message, cancel, ok }) => 71 | !show 72 | ? null 73 | : <div className={styles.modal}> 74 | <h1>{message}</h1> 75 | 76 | <div className={styles.modalFooter}> 77 | <span onClick={cancel}>CANCEL</span> 78 | <span onClick{ok}>OK</span> 79 | </div> 80 | </div> 81 | 82 | const mapState = ({ blockNavigation: { message, canLeave } }) => ({ 83 | show: !!message, 84 | message, 85 | cancel: () => canLeave(false), 86 | ok: () => canLeave(true) 87 | }) 88 | 89 | export default connect(mapState)(BlockModal) 90 | ``` 91 | > obviously you could wrap `<BlockModal />` in a [transition-group](https://github.com/faceyspacey/transition-group) to create a nice fadeIn/Out animation 92 | 93 | 94 | *src/components/App.js* 95 | 96 | ```js 97 | export default () => 98 | <div> 99 | <OtherStuff /> 100 | <BlockModal /> 101 | </div> 102 | ``` 103 | 104 | 105 | *src/configureStore.js:* 106 | ```js 107 | const routesMap = { 108 | HOME: '/' 109 | FOO: { 110 | path: '/foo', 111 | confirmLeave: (state, action) => { 112 | if (!state.formComplete && action.type === 'HOME' ) { 113 | return 'Are you sure you want to leave without completing your purchase?' 114 | } 115 | } 116 | } 117 | } 118 | 119 | const options = { 120 | displayConfirmLeave: (message, callback) => { 121 | const canLeave = can => { 122 | store.dispatch({ type: 'HIDE_BLOCK_NAVIGATION_MODEL' }) // hide modal 123 | return callback(can) // navigate to next route or stay where ur at 124 | } 125 | 126 | store.dispatch({ 127 | type: 'SHOW_BLOCK_NAVIGATION_MODAL', 128 | payload: { message, canLeave } 129 | }) 130 | } 131 | } 132 | 133 | const { reducer, middleware, enhancer } = connectRoutes(history, routesMap, options) 134 | 135 | const rootReducer = combineReducers({ ...reducers, location: reducer }) 136 | const middlewares = applyMiddleware(middleware) 137 | const enhancers = composeEnhancers(enhancer, middlewares) 138 | const store = createStore(rootReducer, preLoadedState, enhancers) 139 | ``` 140 | -------------------------------------------------------------------------------- /docs/client-only-api.md: -------------------------------------------------------------------------------- 1 | # Client-Only API 2 | The following are features you should avoid unless you have a reason that makes sense to use them. These features revolve around the [history package's](npmjs.com/package/history) API. They make the most sense in React Native--for things like back button handling. If you're using our *React Navigation* tools, you also won't want to use this as `StackRouter` doesn't jive with a plain sequence of history entries. On web, you'll rarely need it as you'll want to use our [Link component](https://github.com/faceyspacey/redux-first-router-link) to create real links embedded in the page for SEO/SSR instead. 3 | 4 | One case for web though--if you're curious--is the fake address bar you've probably seen in one our examples. If you have such needs, go for it. 5 | 6 | *Takeaway:* On web, force yourself to use our `<Link />` package so that real `<a>` tags get embedded in the page for SEO and link-sharing benefits; beware of using the below methods. 7 | 8 | 9 | 10 | ## Imperative Methods 11 | 12 | * **push:** (path) => void 13 | * **replace:** (path) => void 14 | * **back:** () => void 15 | * **next:** () => void 16 | * **go:** (number) => void 17 | * **canGoBack:** (path) => boolean 18 | * **canGoForward:** () => boolean 19 | * **prevPath:** () => ?string 20 | * **nextPath:** () => ?string 21 | 22 | **You can import them like so:** 23 | 24 | ```javascript 25 | import { back, canGoBack } from 'redux-first-router' 26 | ``` 27 | > For a complete example, see the [React Native Android BackHandler Example](./react-native.md#android-backhandler). 28 | 29 | Keep in mind these methods should not be called until you call `connectRoutes`. This is almost always fine, as your store configuration typically happens before your app even renders once. 30 | 31 | *Note: do NOT rely on these methods on the server, as they do not make use of enclosed* ***per request*** *state. If you must, use the corresponding 32 | methods on the `history` object you create per request which you pass to `connectRoutes(history`). Some of our methods are convenience methods for what you can do with `history`, so don't expect `history` to have all the above methods, but you can achieve the same. See the [history package's docs](https://github.com/ReactTraining/history) 33 | for more info.* 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /docs/low-level-api.md: -------------------------------------------------------------------------------- 1 | # Low-level API 2 | 3 | Below are some additional methods we export. The target user is package authors. Application developers will rarely need this. 4 | 5 | ## `actionToPath` and `pathToAction` 6 | These methods are also exported: 7 | 8 | ```javascript 9 | import { actionToPath, pathToAction } from 'redux-first-router' 10 | 11 | const { routesMap } = store.getState().location 12 | 13 | const path = actionToPath(action, routesMap, querySerializer) 14 | const action = pathToAction(path, routesMap, querySerializer, basename, strict) 15 | ``` 16 | 17 | The `querySerializer`, `basename`, and `strict` arguments are optional. 18 | 19 | The `querySerializer` argument is the same as the one passed to `connectRoutes`. 20 | It defaults to undefined. 21 | 22 | The `basename` and `strict` arguments default to the value passed the last time `connectRoutes` was called. 23 | The `basename` argument works the same way as the one passed to `routesMap`. 24 | `actionToPath` does not apply any `basename` transformation. 25 | When `strict` is `true`, the presence or absence of a trailing slash is required to match the route path. 26 | 27 | You will need the `routesMap` you made, which you can import from where you created it or you can 28 | get any time from your store. 29 | 30 | Our `<Link />` and `<NavLink />` components from [`redux-first-router-link`](https://github.com/faceyspacey/redux-first-router-link), 31 | generates your links using these methods. 32 | 33 | 34 | ## `isLocationAction` 35 | 36 | A simple utility to determine if an action is a location action transformed by the middleware. It can be useful if you want to know what sort of action you're dealing with before you decide what to do with it. You likely won't need this unless you're building associated packages. 37 | 38 | 39 | ## History 40 | 41 | You can get access to the `history` object that you initially created, but from anywhere in your code without having to pass it down: 42 | 43 | ```js 44 | import { history } from 'redux-first-router' 45 | 46 | // notice that you must call it as a function 47 | history().entries.map(entry => entry.pathname) 48 | history().index 49 | history().length 50 | history().action 51 | // etc 52 | ``` 53 | 54 | Keep in mind `history()` will return undefined until you call `connectRoutes`. This is usually fine, as your store configuration typically happens before your app even renders once. 55 | 56 | View the [history package](https://www.npmjs.com/package/history) for more info. 57 | -------------------------------------------------------------------------------- /docs/migration.md: -------------------------------------------------------------------------------- 1 | In earlier versions `history` was a `peerDependency`, this is no longer the case since version 2 has its own history management tool. This means that the arguments passed to `connectRoutes`([documentation](https://github.com/faceyspacey/redux-first-router/blob/master/docs/connectRoutes.md)) need to be changed from this: 2 | 3 | 4 | ```js 5 | const { reducer, middleware, enhancer, thunk } = connectRoutes( 6 | history, 7 | routesMap, 8 | options 9 | ) 10 | ``` 11 | to this: 12 | 13 | ```js 14 | const { reducer, middleware, enhancer, thunk } = connectRoutes( 15 | routesMap, { 16 | ...options, 17 | initialEntries 18 | }) 19 | ``` 20 | 21 | If you're using a custom history type, you can still import `createHashHistory` or `createMemoryHistory` from either the `history` package or `rudy-history` and add to your options as `createHistory`. 22 | 23 | Change commit in `redux-first-router-demo`: [here](https://github.com/ScriptedAlchemy/redux-first-router-demo/commit/6c8238eee713ce0079aeae1ce328d305bddd0ee3#diff-04298622441e55a9d9b5691873f8490b). 24 | 25 | And inside of your `configureStore.js` [file](https://github.com/ScriptedAlchemy/redux-first-router-demo/blob/6c8238eee713ce0079aeae1ce328d305bddd0ee3/server/configureStore.js) if you are server side rendering, change this: 26 | 27 | ```js 28 | const history = createHistory({ initialEntries: [req.path] }) 29 | const { store, thunk } = configureStore(history, preLoadedState) 30 | ``` 31 | 32 | To this: 33 | 34 | ```js 35 | const { store, thunk } = configureStore(preLoadedState, [req.path]) 36 | ``` 37 | 38 | Change commit in `redux-first-router-demo`: [here](https://github.com/ScriptedAlchemy/redux-first-router-demo/commit/6c8238eee713ce0079aeae1ce328d305bddd0ee3#diff-538a809ba00b97f8cf4ef2f28accee51). 39 | -------------------------------------------------------------------------------- /docs/prefetching.md: -------------------------------------------------------------------------------- 1 | ## Prefetching - coming soon! 2 | 3 | For now, checkout: 4 | 5 | **articles:** 6 | 7 | - https://medium.com/webpack/how-to-use-webpacks-new-magic-comment-feature-with-react-universal-component-ssr-a38fd3e296a 8 | - https://hackernoon.com/code-cracked-for-code-splitting-ssr-in-reactlandia-react-loadable-webpack-flush-chunks-and-1a6b0112a8b8 9 | 10 | **pre-requesite packages:** 11 | - https://github.com/faceyspacey/react-universal-component 12 | - https://github.com/faceyspacey/webpack-flush-chunks 13 | - https://github.com/faceyspacey/extract-css-chunks-webpack-plugin 14 | 15 | *Redux First Router* will allow you to specify chunks in your `routesMap` and your `<Link />` components will have a `prefetch` prop you can set to `true` to prefetch associated chunks. An imperative API via the instance ref will exist too: 16 | 17 | ```js 18 | const routesMap = { 19 | FOO: { path: '/foo/:bar', chunks: [import('./Foo')] } 20 | } 21 | 22 | // declarative API: 23 | <Link prefetch href='/foo/123' /> 24 | <Link prefetch href={{ type: 'FOO', payload: { bar: 456 } }} /> 25 | 26 | // imperative API: 27 | <Link ref={i => instance = i} href='/foo/123' /> 28 | instance.prefetch() 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/prior-art.md: -------------------------------------------------------------------------------- 1 | # Prior Art 2 | 3 | The following packages attempt in similar ways to reconcile the browser `history` with redux actions and state: 4 | 5 | - **redux-little-router** 6 | https://github.com/FormidableLabs/redux-little-router 7 | A tiny router for Redux that lets the URL do the talking. 8 | 9 | 10 | - **universal-redux-router** 11 | https://github.com/colinmeinke/universal-redux-router 12 | A router that turns URL params into first-class Redux state and runs action creators on navigation 13 | 14 | 15 | - **redux-history-sync** 16 | https://github.com/cape-io/redux-history-sync 17 | Essentially, this module syncs browser history locations with a Redux store. If you are looking to read and write changes to the address bar via Redux this might be for you. 18 | 19 | - **Redux Unity Router** 20 | https://github.com/TimeRaider/redux-unity-router 21 | Simple routing for your redux application. The main purpose of this router is to mirror your browser history to the redux store and help you easily declare routes. 22 | 23 | 24 | ## More 25 | To check out even more, here's a complete list automatically scraped from NPM: 26 | 27 | https://github.com/markerikson/redux-ecosystem-links/blob/master/routing.md 28 | -------------------------------------------------------------------------------- /docs/query-strings.md: -------------------------------------------------------------------------------- 1 | ## Query Strings 2 | 3 | Queries can be dispatched by assigning a `query` object containing key/vals to an `action`, its `payload` object or its `meta` object. 4 | 5 | > You will never have to deal with `search` strings directly, unless of course you want to manually supply them to `<Link />` as the `to` prop. The recommended approach is to use action objects though. 6 | 7 | The `query` key would be solely allowed on the `action` itself, but to support *[Flux Standard Actions](https://github.com/acdlite/flux-standard-action)* we have to provide the alternate 2 strategies. 8 | 9 | The recommended approach is to put it on the `action` unless you're using FSAs, in which case it's up to you. 10 | 11 | 12 | ## Where `query` lives on your actions? 13 | 14 | By the time actions reach your reducers (after they're transformed by the middleware), the `query` will exist on actions at: 15 | 16 | - `action.query` *(only if you supplied it here)* 17 | - `action.meta.query` *(only if you supplied it here)* 18 | - `action.payload.query` *(only if you supplied it here)* 19 | - `action.meta.location.current.query` *(always)* 20 | 21 | The `<Link />` component--if you supply a string for the `to` prop--will put it on the `meta` key. If you want it elsewhere, it's recommended anyway to generate your URLs by supplying actions, in which case where you put it will be respected. 22 | 23 | The actual search string will only ever exist at: 24 | - `action.meta.location.current.search` 25 | 26 | ## Some Example Actions 27 | 28 | Using links, here is some examples of how to format these actions: 29 | 30 | *action.query:* 31 | ```js 32 | <Link to={{ type: 'ANYTHING', payload: { key: val }, query: { foo: 'bar' } }}> 33 | ``` 34 | 35 | *action.payload.query:* 36 | ```js 37 | <Link to={{ type: 'ANYTHING', payload: { key: val, query: { foo: 'bar' } } }}> 38 | ``` 39 | 40 | *action.meta.query:* 41 | ```js 42 | <Link to={{ type: 'ANYTHING', payload: { key: val }, meta: { query: { foo: 'bar' } } }}> 43 | ``` 44 | 45 | The matching route would be: 46 | 47 | ```js 48 | const routesMap = { 49 | ANYTHING: '/anything/:key' 50 | } 51 | ``` 52 | 53 | Notice there is no path params for query key/vals. They don't affect the determination of which route is selected. Only the `type` and `payload` do. The same `type` is shared no matter what query is provided.. 54 | 55 | 56 | ## Where will `query` + `search` live in `location` state? 57 | 58 | Here: 59 | 60 | ```js 61 | store.getState().location.query 62 | store.getState().location.search 63 | ``` 64 | 65 | 66 | ## How to Enable Query String Serialization 67 | 68 | To enable query string serialization you must provide a `querySerializer` option to `connectRoutes` like this: 69 | 70 | ```js 71 | import queryString from 'query-string' 72 | 73 | connectRoutes(routesMap, { 74 | querySerializer: queryString 75 | }) 76 | ``` 77 | If you would like to create your own, you can pass any object with a `stringify` and `parse` function. The former takes an object and creates a query string and the latter takes a `string` and creates an object. 78 | 79 | Also note that the `query-string` package will produce strings for numbers. There's a [package](https://github.com/mariusc23/express-query-int) made for express that will format numbers into `ints`, which you may be interested in learning from if you want to achieve that goal. I couldn't find a general one identical to this one. If you make it, publish it to NPM and make a PR to this doc with a link to it. *I think it's a mistake that numbers aren't converted for you.* 80 | 81 | If you are using server side rendering you will need to make sure that `connectRoutes` receives proper configuration object containing full req.path including query in `initialEntries`. 82 | 83 | With Express `req.path` does not contain the query. In this case you need to use `initialEntries: [req.originalUrl]` instead of `initialEntries: [req.path]` or if your server of choice does not give you `req.originalUrl` then manually append query string to recreate full path. E.g. (#56). 84 | 85 | 86 | ## CodeSandBox 87 | You can test out query support on codesandbox here: 88 | 89 | <a href="https://codesandbox.io/s/pgp5mekzm?module=%2Foptions.js" target="_blank"> 90 | <img alt="Edit Redux-First Router Demo" src="https://codesandbox.io/static/img/play-codesandbox.svg"> 91 | </a> 92 | -------------------------------------------------------------------------------- /docs/reducer.md: -------------------------------------------------------------------------------- 1 | # Reducer 2 | The location reducer primarily maintains the state of the current `pathname` and action dispatched (`type` + `payload`). 3 | That's its core mission. 4 | 5 | In addition, it maintains similar state for the previous route on the `prev` key, as well as the kind of action on the `kind` key. Here are the kinds you can expect: 6 | 7 | * *load*: if the current route was the first route the app loaded on, `load` will be true 8 | * *redirect*: if the current route was reached as the result of a redirect 9 | * *next*: if the current route was reached by going forward (and not a *push*) 10 | * *back*: if the current route was reached by going back 11 | * *pop*: if the user has used the browser back/forward buttons and we can't determine the direction (which is typical in the browser using `createBrowserHistory`. If you're using `createMemoryHistory`, the kind will know if you're going forward or back.) 12 | 13 | If the app is utilizing server side rendering, a `hasSSR` key will be set to true. 14 | 15 | Lastly, your `routesMap` will also be stored for use by, for instance, *redux-first-router-link's* `<Link />` component. 16 | 17 | Here's an example of the initialState that will be created for your location reducer: 18 | 19 | ## Example Initial State 20 | 21 | ```javascript 22 | const initialState = { 23 | pathname: '/example/url', 24 | type: 'EXAMPLE', 25 | payload: { param: 'url' }, 26 | prev: { 27 | pathname: '', 28 | type: '', 29 | payload: {} 30 | }, 31 | kind: undefined, 32 | hasSSR: isServer() ? true : undefined, 33 | routesMap: { 34 | EXAMPLE: '/example/:param', 35 | } 36 | } 37 | ``` 38 | 39 | 40 | ## Reducer 41 | And here's a slightly simplified version of the `location` reducer that will ultimately be powering your app: 42 | 43 | ```javascript 44 | const locationReducer = (state = initialState, action = {}) => { 45 | if (routesMap[action.type]) { 46 | return { 47 | pathname: action.meta.location.current.pathname, 48 | type: action.type, 49 | payload: { ...action.payload }, 50 | prev: action.meta.location.prev, 51 | kind: action.meta.location.kind, 52 | hasSSR: state.hasSSR, 53 | routesMap 54 | } 55 | } 56 | 57 | return state 58 | } 59 | ``` 60 | 61 | 62 | ## Flow Type 63 | To get a precise sense of what values your `location` reducer will store, here's its ***Flow*** type: 64 | 65 | ```javascript 66 | type Location = { 67 | pathname: string, // current path + action 68 | type: string, 69 | payload: Object 70 | 71 | prev: { // previous path + action 72 | pathname: string, 73 | type: string, 74 | payload: Object 75 | }, 76 | 77 | kind?: string, // extra info 78 | hasSSR?: true, 79 | 80 | routesMap: RoutesMap // your routes, for reference 81 | } 82 | 83 | type RoutesMap = { 84 | [key: string]: string | RouteObject 85 | } 86 | 87 | type RouteObject = { 88 | path: string, 89 | capitalizedWords?: boolean, 90 | coerceNumbers?: boolean, 91 | toPath?: (param: string, key?: string) => string, 92 | fromPath?: (path: string, key?: string) => string, 93 | thunk?: (dispatch: Function, getState: Function) => Promise<any> 94 | } 95 | ``` 96 | 97 | 98 | ## History State (via `createMemoryHistory` only) 99 | 100 | The `location` state and the `action.meta.location` object *on the server or in environments where you used `createMemoryHistory` 101 | to create your history (such as React Native)* will also maintain information about the history stack. It can be found within the `history` key, and this 102 | is its shape: 103 | 104 | ```javascript 105 | type History: { 106 | index: number, // index of focused entry/path 107 | length: number, // total # of entries/paths 108 | entries: Entry // array of objects containting paths 109 | } 110 | 111 | type Entry: { 112 | pathname: string 113 | } 114 | ``` 115 | 116 | This is different from what the `history` package maintains in that you can use Redux to reactively respond to its changes. Here's an example: 117 | 118 | ```js 119 | import React from 'react' 120 | import { connect } from 'react-redux' 121 | 122 | const MyComponent = ({ isLast, path }) => 123 | isLast ? <div>last</div> : <div>{path}</div> 124 | 125 | const mapStateToProps = ({ location: { history } }) => ({ 126 | isLast: history.index === history.length - 1, 127 | path: history.entries[history.index].pathname 128 | }) 129 | 130 | export default connect(mapStateToProps)(MyComponent) 131 | ``` 132 | > By the way, this example also showcases the ultimate goal of **Redux First Router:** *to stay within the "intuitive" workflow of standard Redux patterns*. 133 | 134 | 135 | If you're wondering why such state is limited to `createMemoryHistory`, it's because it can't be consistently maintained in the browser. Here's why: 136 | 137 | [would it be possible for createBrowserHistory to also have entries and index? #441](https://github.com/ReactTraining/history/issues/441) 138 | 139 | In short, the browser will maintain the history for your website even if you refresh the page, whereas from our app's perspective, 140 | if that happens, we'll lose awareness of the history stack. `sessionStorage` almost can solve the issue, but because of various 141 | browser inconsitencies (e.g. when cookies are blocked, you can't recall `sessionStorage`), it becomes unreliable and therefore 142 | not worth it. 143 | 144 | 145 | ***When might I have use for it though?*** 146 | 147 | Well, you see the fake browser we made in our playground on *webpackbin*, right? We emulate the browser's back/next buttons 148 | using it. If you have the need to make such a demo or something similar, totally use it. 149 | -------------------------------------------------------------------------------- /docs/redux-first-router-flow-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faceyspacey/redux-first-router/124daec212ad4593431bbb582fe41c980c12a4f2/docs/redux-first-router-flow-chart.png -------------------------------------------------------------------------------- /docs/redux-persist.md: -------------------------------------------------------------------------------- 1 | ## Usage with redux-persist 2 | 3 | ⚠️ WARNING: redux-persist is undergoing [a big refactor](https://github.com/rt2zz/redux-persist/tree/v5) at the moment. This doc is based on the currently [stable v4.6.0](https://github.com/rt2zz/redux-persist/releases/tag/v4.6.0). v5 might or might nor break this approach ⚠️ 4 | 5 | 6 | #### Using the Cookies storage adapter 7 | You might run into a situation where you want to trigger a redirect as soon as possible in case some particular piece of state is or is not set. 8 | A possible use case could be persisting checkout state, e.g. `checkoutSteps.step1Completed`. 9 | 10 | To do this, the only method I'm aware of is using good old Cookies so we already have the state available during the server rendering cycle. 11 | 12 | ```js 13 | // routesMap.js 14 | // ... 15 | 16 | const canContinueIf = (stepCompleted, dispatch, fallbackRoute) => { 17 | if (!stepCompleted) { 18 | const action = redirect({ type: fallbackRoute }) 19 | dispatch(action) 20 | } 21 | 22 | // ... 23 | 24 | export default { 25 | // ... 26 | CHECKOUT_STEP_2: { 27 | path: '/checkout-step-2', 28 | thunk: (dispatch, getState) => { 29 | const { checkoutSteps } = getState() 30 | canContinueIf(checkoutSteps.step1Completed, dispatch, 'CHECKOUT_STEP_1') 31 | } 32 | } 33 | // ... 34 | } 35 | ``` 36 | 37 | ```js 38 | // server/configureStore.js 39 | //... 40 | 41 | const parseCookies = (cookies) => { 42 | const parsedCookies = {} 43 | Object.keys(cookies).forEach((key) => { 44 | const value = cookies[key] 45 | const decodedKey = decodeURIComponent(key) 46 | const keyWithoutReduxPersistPrefix = decodedKey.replace(/reduxPersist:/, '') 47 | if (key !== 'reduxPersistIndex') { // TODO: This could be expanded into a real black- or whitelist 48 | parsedCookies[keyWithoutReduxPersistPrefix] = JSON.parse(value) 49 | } 50 | }) 51 | return parsedCookies 52 | } 53 | 54 | //... 55 | 56 | export default async (req, res) => { 57 | // ... 58 | const parsedCookies = parseCookies(req.cookies) 59 | const preLoadedState = { ...parsedCookies } // onBeforeChange will authenticate using this 60 | // ... 61 | } 62 | ``` 63 | 64 | ```js 65 | // src/configureStore.js 66 | //... 67 | 68 | export default (history, preLoadedState) => { 69 | // … 70 | const store = createStore(rootReducer, preLoadedState, enhancers) 71 | 72 | if (!isServer) { 73 | persistStore(store, { 74 | blacklist: ['location', 'page'], 75 | storage: new CookieStorage() 76 | }) 77 | } 78 | // … 79 | } 80 | ``` 81 | 82 | ```js 83 | // server/index.js 84 | import cookieParser from 'cookie-parser' 85 | 86 | // ... 87 | 88 | app.use(cookieParser()) 89 | 90 | // ... 91 | ``` 92 | -------------------------------------------------------------------------------- /docs/scroll-restoration.md: -------------------------------------------------------------------------------- 1 | # Scroll Restoration 2 | 3 | Complete Scroll restoration and hash `#links` handling is addressed primarily by one of our companion packages: [redux-first-router-restore-scroll](https://github.com/faceyspacey/redux-first-router-restore-scroll) *(we like to save you the bytes sent to clients if you don't need it)*. In most cases all you need to do is: 4 | 5 | Example: 6 | 7 | ```js 8 | import restoreScroll from 'redux-first-router-restore-scroll' 9 | connectRoutes(history, routesMap, { restoreScroll: restoreScroll() }) 10 | ``` 11 | 12 | 13 | Visit [redux-first-router-restore-scroll](https://github.com/faceyspacey/redux-first-router-restore-scroll) for more information and advanced usage. 14 | 15 | 16 | ## Scroll Restoration for Elements other than `window` 17 | We got you covered. Please checkout [redux-first-router-scroll-container](https://github.com/faceyspacey/redux-first-router-scroll-container). 18 | 19 | ## Scroll Restoration for React Native 20 | We got you covered! Please checkout [redux-first-router-scroll-container-native](https://github.com/faceyspacey/redux-first-router-scroll-container-native). 21 | 22 | -------------------------------------------------------------------------------- /docs/server-rendering.md: -------------------------------------------------------------------------------- 1 | # Server Side Rendering (using thunk) 2 | Ok, this is the biggest example here, but given what it does, we think it's extremely concise and sensible. 3 | 4 | Since the middleware handles the actions it receives asyncronously, on the server you simply `await` the result of a possible matching thunk: 5 | 6 | *server/configureStore.js:* 7 | ```js 8 | import { createStore, applyMiddleware, compose, combineReducers } from 'redux' 9 | import createHistory from 'history/createMemoryHistory' 10 | import { connectRoutes } from 'redux-first-router' 11 | 12 | export default async function configureStore(req) { 13 | const history = createHistory({ initialEntries: [req.path] }) // match initial route to express path 14 | 15 | const routesMap = { 16 | UNAVAILABLE: '/unavailable', 17 | ENTITY: { 18 | path: '/entity/:slug', 19 | thunk: async (dispatch, getState) => { 20 | const { slug } = getState().location.payload 21 | const data = await fetch(`/api/entity/${slug}`) 22 | const entity = await data.json() 23 | const action = { type: 'ENTITY_FOUND', payload: { entity } } // you handle this action type 24 | 25 | dispatch(action) 26 | } 27 | }, 28 | } 29 | 30 | const { reducer, middleware, enhancer, thunk } = connectRoutes(history, routesMap) // notice `thunk` 31 | const rootReducer = combineReducers({ location: reducer }) 32 | // note the order that the enhancer and middleware are composed in: enhancer first, then middleware 33 | const store = createStore(rootReducer, compose(enhancer, applyMiddleware(middleware))) 34 | 35 | // using redux-thunk perhaps request and dispatch some app-wide state as well, e.g: 36 | // await Promise.all([ store.dispatch(myThunkA), store.dispatch(myThunkB) ]) 37 | 38 | await thunk(store) // THE WORK: if there is a thunk for current route, it will be awaited here 39 | 40 | return store 41 | } 42 | ``` 43 | 44 | *server/serverRender.js:* 45 | ```javascript 46 | import ReactDOM from 'react-dom/server' 47 | import { Provider } from 'react-redux' 48 | import configureStore from './configureStore' 49 | import App from './components/App' 50 | 51 | export default async function serverRender(req, res) { 52 | const store = await configureStore(req) 53 | 54 | const appString = ReactDOM.renderToString(<Provider store={store}><App /></Provider>) 55 | const stateJson = JSON.stringify(store.getState()) 56 | 57 | // in a real app, you would use webpack-flush-chunks to pass a prop 58 | // containing scripts and stylesheets to serve in the final string: 59 | return res.send( 60 | `<!doctype html> 61 | <html> 62 | <body> 63 | <div id="root">${appString}</div> 64 | <script>window.REDUX_STATE = ${stateJson}</script> 65 | <script src="/static/main.js" /> 66 | </body> 67 | </html>` 68 | ) 69 | } 70 | ``` 71 | 72 | *server/index.js.js:* 73 | ```js 74 | import express from 'express' 75 | import serverRender from './serverRender' 76 | 77 | const app = express() 78 | app.get('*', serverRender) 79 | http.createServer(app).listen(3000) 80 | ``` 81 | 82 | *Note: on the server you won't double dispatch your thunks. Unlike the client, calling the matching thunk is intentionally not automatic so that you can `await` the promise before sending your HTML to the browser. And of course the `thunk` returned from `connectRoutes` will automatically match the current route if called.* 83 | 84 | 85 | ## Redirects + `NOT_FOUND` Example 86 | 87 | *server/configureStore.js:* 88 | ```js 89 | import { createStore, applyMiddleware, compose, combineReducers } from 'redux' 90 | import createHistory from 'history/createMemoryHistory' 91 | import { connectRoutes, redirect, NOT_FOUND } from 'redux-first-router' 92 | 93 | export default async function configureStore(req, res) { 94 | const history = createHistory({ initialEntries: [req.path] }) 95 | 96 | const routesMap = { 97 | UNAVAILABLE: '/unavailable', 98 | LOGIN: '/login', 99 | PRIVATE_AREA: { 100 | path: '/private-area', 101 | thunk: (dispatch, getState) => { 102 | const { isLoggedIn } = getState() // up to you to handle via standard redux techniques 103 | 104 | if (!isLoggedIn) { 105 | const action = redirect({ type: 'LOGIN' })// action tells middleware to use history.replace() 106 | dispatch(action) // on the server you detect a redirect as done below 107 | } 108 | } 109 | } 110 | } 111 | 112 | const { reducer, middleware, enhancer, thunk } = connectRoutes(history, routesMap) 113 | const rootReducer = combineReducers({ location: reducer }) 114 | // enhancer first, then middleware 115 | const store = createStore(rootReducer, compose(enhancer, applyMiddleware(middleware))) 116 | 117 | // the idiomatic way to handle redirects 118 | // serverRender.js will short-circuit since the redirect is made here already 119 | let location = store.getState().location 120 | if (doesRedirect(location, res)) return false 121 | 122 | await thunk(store) // dont worry if your thunk doesn't return a promise 123 | 124 | // the idiomatic way to handle routes not found :) 125 | // your component's should also detect this state and render a 404 scene 126 | const status = location.type === NOT_FOUND ? 404 : 200 127 | res.status(status) 128 | 129 | return store 130 | } 131 | 132 | const doesRedirect = ({ kind, pathname, search }, res) => { 133 | if (kind === 'redirect') { 134 | res.redirect(302, search ? `${pathname}?${search}` : pathname); // the request completes here, therefore we must short-circuit after 135 | return true 136 | } 137 | } 138 | ``` 139 | 140 | *server/serverRender.js:* 141 | ```javascript 142 | import ReactDOM from 'react-dom/server' 143 | import { Provider } from 'react-redux' 144 | import configureStore from './configureStore' 145 | import App from './components/App' 146 | 147 | export default async function serverRender(req, res) { 148 | const store = await configureStore(req, res) // pass res now too 149 | if (!store) return // no store means redirect was already served 150 | 151 | const appString = ReactDOM.renderToString(<Provider store={store}><App /></Provider>) 152 | const stateJson = JSON.stringify(store.getState()) 153 | 154 | return res.send( 155 | `<!doctype html> 156 | <html> 157 | <body> 158 | <div id="root">${appString}</div> 159 | <script>window.REDUX_STATE = ${stateJson}</script> 160 | <script src="/static/main.js" /> 161 | </body> 162 | </html>` 163 | ) 164 | } 165 | ``` 166 | 167 | > Note: this example doubles as an example of how to use `redirect` in an SPA without SSR. `thunk` usage is the same whether you're doing SSR or not. You should be sharing the same `routesMap` between client and server code. You likely can share even more. The idiomatic approach is to create a shared [`src/configureStore.js`](https://github.com/faceyspacey/redux-first-router-demo/blob/master/server/configureStore.js#L10) file that does most of the work. Then in `server/configureStore.js`, handle the things that the client is NOT responsible for: 168 | 169 | - redirects 170 | - `NOT_FOUND` 171 | - global data-fetching 172 | - `await thunk`. 173 | 174 | 175 | ## Note on Redirects 176 | 177 | *Why are redirect actions any different from regular actions?* 178 | 179 | To answer that question, imagine instead 180 | you *pushed* a URL on to the address bar for `/login` when the user tried to access a private area. Now imagine, the user 181 | presses the browser *BACK* button. The user will now be redirected back to `login` again and again. The user will struggle to go farther 182 | back in the history stack, which the user very well may want to do if he/she does not want to login at this time and 183 | just wants to get back to where he/she was at. 184 | 185 | By using `history.replace()` behind the scenes, the private URL the user tried 186 | to access now becomes the `/login` URL in the stack, and the user can go back to the previous page just as he/she would expect. 187 | 188 | On the server, this is another important anomaly because you don't want to render the `/login` page under the `/private-area` URL. 189 | The idiomatic way to handle that is the same as `NOT_FOUND` and therefore succinct and consistent. 190 | 191 | ## Notes on `NOT_FOUND` 192 | 193 | `NOT_FOUND` is no different than any action you can dispatch yourself. The only difference is that *RFR* also knows to dispatch it. It will be dispatched when no routes match the URL or if you dispatch an action that doesn't match a route path. Therefore it should be your catch-all action type to display a pretty page that shows the resource is missing. 194 | -------------------------------------------------------------------------------- /docs/url-parsing.md: -------------------------------------------------------------------------------- 1 | # URL parsing 2 | 3 | Besides the simple option of matching a literal path, all matching capabilities of the `path-to-regexp` package we use are now supported, *except* [unnamed parameters](https://github.com/pillarjs/path-to-regexp#unnamed-parameters). 4 | 5 | Let's go through what we support. Usually it's best to start with the simplest example, but I think most people looking at this get this stuff. We'll start with one of the more complicated use cases just to see how far we can take this: 6 | 7 | ## Multi Segment Parameters 8 | 9 | So for example, imagine you're github.com and you're now using Redux-First Router :), and you need to match all the potential dynamic paths for files in repos, here's how you do it: 10 | 11 | ```js 12 | const routesMap = { 13 | REPO: '/:user/:repo/block/:branch/:filePath+' 14 | } 15 | ``` 16 | 17 | So that will match: 18 | - https://github.com/faceyspacey/redux-first-router/blob/master/src/connectRoutes.js 19 | - https://github.com/faceyspacey/redux-first-router/blob/master/src/pure-utils/pathToAction.js 20 | - etc 21 | 22 | but not: 23 | - https://github.com/faceyspacey/redux-first-router/blob/master 24 | 25 | And if you visit that URL, you will see it in fact doesn't exist. If it did, you would use an asterisk (for 0 or more matches as in regexes) like so: 26 | 27 | ```js 28 | const routesMap = { 29 | REPO: '/:user/:repo/block/:branch/:filePath*' 30 | } 31 | ``` 32 | 33 | So the above 2 options will match a varying number of path segments. Here's what a corresponding action might look like: 34 | 35 | 36 | ```js 37 | const action = { 38 | type: 'REPO', 39 | payload: { 40 | user: 'faceyspacey', 41 | repo: 'redux-first-router', 42 | branch: 'master', 43 | filePath: 'src/pure-utils/pathToAction.js' 44 | } 45 | } 46 | ``` 47 | 48 | Pretty cool, eh! 49 | 50 | > The inspiration actually came from a [PR](https://github.com/CompuIves/codesandbox-client/pull/49) I did to CodeSandBox. I didn't actually implement this there, but I was thinking about it around that time. I.e. that CodeSandBox should have the full file paths in the URLs like github. Currently it's as a URL-encoded query param, but in the future *(perhaps with RFR)*, they'll be able to do what Github does as well. 51 | 52 | ## Optional Single Segment Parameters 53 | 54 | However, you usually just want to add optional *single segment* params like this: 55 | 56 | ```js 57 | const routesMap = { 58 | ISSUES: '/:user/:repo/issues/:id?' 59 | } 60 | ``` 61 | 62 | So with that you can visit both: 63 | - `https://github.com/faceyspacey/redux-first-router/issues` 64 | - `https://github.com/faceyspacey/redux-first-router/issues/83` 65 | 66 | Here's the 2 actions that would match that respectively: 67 | 68 | - `const action = { type: 'ISSUES' }` 69 | - `const action = { type: 'ISSUES', payload: { id: 83} }` 70 | 71 | And that's basically the *"80% use-case"* most powerful feature here. I.e. when you want to use the same type for a few similar URLs, while getting an optional parameter. 72 | 73 | > note: you can also have optional params in the middle, eg: `/foo/:optional?/bar/:anotherOptional?` So all 3 of `/foo/bla/bar/baz` and `/foo/bar/baz` and `/foo/bar` will match :) 74 | 75 | ## Optional Static Segments ("aliases") 76 | 77 | The absolute most common *(and simpler*) use case for such a thing is when you want `/items` and `/items/list` to have the same type **(because they are aliases of each other )**. You accomplish it slightly differently: 78 | 79 | ```js 80 | const routesMap = { 81 | HOME: '/home/(list)?', 82 | } 83 | ``` 84 | 85 | or 86 | 87 | ```js 88 | const routesMap = { 89 | HOME: '/home/(list)*', 90 | } 91 | ``` 92 | 93 | Both are the same. It would be nice if you didn't have to wrap `list` in parentheses, but you have to. That's fine. *Keep in mind this isn't a parameter; the 2nd path segment has to be `list` or not be there.* 94 | 95 | Also, the common convention we should use is the the question mark `?` instead of the asterisk `*`. We should reserve the asterisk for where it has unique capabilities, specifically the ability to match a varying number of path segments (along with `+`) as in the initial examples with the github file paths. 96 | 97 | ## Regexes 98 | 99 | Another thing to note about the last one is that `(list)` is in fact a regex. So you can use regexes to further refine what paths match. You can do so with parameter segments as well: 100 | 101 | ```js 102 | const routesMap = { 103 | HOME: '/posts/:id(\\d+)', 104 | } 105 | ``` 106 | 107 | So essentially you follow a dynamic segment with a regex in parentheses and the param will have to match the regex. So this would match: 108 | - `/posts/123` 109 | but this wouldn't: 110 | - `/posts/foo-bar` 111 | 112 | 113 | ## Multiple *Multi Segment Parameters* 114 | Last use case: say you want to have multiple params with a varying number of segments. Here's how you do it: 115 | 116 | ```js 117 | const routesMap = { 118 | HOME: '/home/:segments1+/bla/:segments2+', 119 | } 120 | ``` 121 | 122 | So a url like `/home/foo/bar/bla/baz/bat/cat` will result in an `action` like this: 123 | 124 | ```js 125 | const action = { 126 | type: 'HOME', 127 | payload: { 128 | segments1: 'foo/bar', 129 | segments2: 'baz/bat/cat' 130 | } 131 | } 132 | ``` 133 | 134 | So yes, you can have multiple *"multi segments parameters"*. 135 | 136 | One thing to note is you could also accomplish this like this: `HOME: '/home/:segments1(.*)/bla/:segments2(.*)'`. 137 | 138 | ---- 139 | 140 | Final reminder: if you do plan to use *multi segment parameters*, they have to be named. This won't work: 141 | `/home/(.*)/bla/(.*)`. 142 | 143 | Well, the truth is it will, and given the previous URL you will have a payload of: 144 | 145 | ```js 146 | const action = { 147 | type: 'HOME', 148 | payload: { 149 | 0: 'foo/bar', 150 | 1: 'baz/bat/cat' 151 | } 152 | } 153 | ``` 154 | 155 | But consider that "undefined" functionality. Don't rely on that. Name your segments! Like any other key in your payload. That's the goal here. Originally I had the idea of making an array at `payload.segments`, but then I realized it was possible to name them. So naming all params is the RFR way. 156 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | To run: 4 | 5 | * `create-react-app project` 6 | * `rm project/src -rf` 7 | * `cp -r minimal/{.env,src} project` 8 | * `cd project` 9 | * `yarn add redux react-redux redux-first-router` 10 | * `// git init` 11 | * `// apply patches from other examples, re-run 'yarn install' if needed` 12 | * `yarn start` 13 | -------------------------------------------------------------------------------- /examples/change-title/README.md: -------------------------------------------------------------------------------- 1 | ### Automatically change `<title>` based on the route 2 | 3 | ```js 4 | // reducers/title.js 5 | const DEFAULT = 'RFR demo' 6 | 7 | export default (state = DEFAULT, action = {}) => { 8 | switch (action.type) { 9 | case 'HOME': 10 | return DEFAULT 11 | case 'USER': 12 | return `${DEFAULT} - user ${action.payload.id}` 13 | default: 14 | return state 15 | } 16 | } 17 | ``` 18 | 19 | ```js 20 | // reducers/index.js 21 | export { default as title } from './title' 22 | ``` 23 | 24 | ```diff 25 | // configureStore.js 26 | + import * as reducers from './reducers' 27 | import page from './pageReducer' 28 | 29 | - const rootReducer = combineReducers({ page, location: reducer }) 30 | + const rootReducer = combineReducers({ ...reducers, page, location: reducer }) 31 | ``` 32 | -------------------------------------------------------------------------------- /examples/change-title/change-title.patch: -------------------------------------------------------------------------------- 1 | diff --git a/src/configureStore.js b/src/configureStore.js 2 | index ae0d695..31d9319 100644 3 | --- a/src/configureStore.js 4 | +++ b/src/configureStore.js 5 | @@ -1,6 +1,7 @@ 6 | import { applyMiddleware, combineReducers, compose, createStore } from 'redux' 7 | import { connectRoutes } from 'redux-first-router' 8 | 9 | +import * as reducers from './reducers' 10 | import page from './pageReducer' 11 | 12 | const routesMap = { 13 | @@ -11,7 +12,7 @@ const routesMap = { 14 | export default function configureStore(preloadedState) { 15 | const { reducer, middleware, enhancer, thunk } = connectRoutes(routesMap) 16 | 17 | - const rootReducer = combineReducers({ page, location: reducer }) 18 | + const rootReducer = combineReducers({ ...reducers, page, location: reducer }) 19 | const middlewares = applyMiddleware(middleware) 20 | const enhancers = compose(enhancer, middlewares) 21 | 22 | diff --git a/src/reducers/index.js b/src/reducers/index.js 23 | new file mode 100644 24 | index 0000000..d01643b 25 | --- /dev/null 26 | +++ b/src/reducers/index.js 27 | @@ -0,0 +1 @@ 28 | +export { default as title } from './title' 29 | diff --git a/src/reducers/title.js b/src/reducers/title.js 30 | new file mode 100644 31 | index 0000000..578fe78 32 | --- /dev/null 33 | +++ b/src/reducers/title.js 34 | @@ -0,0 +1,12 @@ 35 | +const DEFAULT = 'RFR demo' 36 | + 37 | +export default (state = DEFAULT, action = {}) => { 38 | + switch (action.type) { 39 | + case 'HOME': 40 | + return DEFAULT 41 | + case 'USER': 42 | + return `${DEFAULT} - user ${action.payload.id}` 43 | + default: 44 | + return state 45 | + } 46 | +} 47 | -------------------------------------------------------------------------------- /examples/links/README.md: -------------------------------------------------------------------------------- 1 | ### Embedding SEO-friendly links 2 | 3 | Use our small [`<Link>` package](https://github.com/faceyspacey/redux-first-router-link) to ensure that an actual anchor is embedded for SEO benefits: 4 | 5 | `yarn install redux-first-router-link` 6 | 7 | If SEO is not required, navigation can of course be done entirely through actions as in the `<button>` example below. 8 | 9 | ```js 10 | // App.js 11 | import Link from 'redux-first-router-link' 12 | 13 | const App = ({ page, changeUser }) => { 14 | const Component = components[page] 15 | return ( 16 | <div> 17 | <Link to="/user/123">User 123</Link> 18 | <Link to={{ type: 'USER', payload: { id: 456 } }}>User 456</Link> 19 | <button onClick={() => changeUser(789)}>User 789</button> 20 | <Component /> 21 | </div> 22 | ) 23 | } 24 | 25 | const mapStateToProps = ({ page }) => ({ page }) 26 | 27 | const mapDispatchToProps = dispatch => ({ 28 | changeUser: id => dispatch({ type: 'USER', payload: { id } }) 29 | }) 30 | export default connect(mapStateToProps, mapDispatchToProps)(App) 31 | ``` 32 | 33 | The **recommended approach** is to use the `<Link to={action}>` method because it keeps the URL scheme separate from the components. URLs can then be changed by just editing `routesMap.js`. 34 | 35 | ### Styling active links 36 | 37 | [`redux-first-router-link`](https://github.com/faceyspacey/redux-first-router-link) also provides the stylable `{ NavLink }`: 38 | 39 | ```js 40 | <NavLink to={{homeAction}} activeStyle={{ color: 'red' }}>Home</NavLink> 41 | ``` 42 | ```js 43 | <NavLink to={{homeAction}} activeClassName="my-active-link-css-class">Home</NavLink> 44 | ``` 45 | Examples using a CSS-in-JS approach such as [`emotion`](https://emotion.sh/): 46 | ```js 47 | import styled from 'react-emotion' 48 | 49 | const StyledNavLink = styled(NavLink)` 50 | &.my-active-link-class { 51 | color: peru 52 | } 53 | `; 54 | 55 | {/* Default activeClassName "active" overridden only for demonstration purposes */} 56 | <StyledNavLink to={{homeAction}} activeClassName="my-active-link-class">Home</StyledNavLink> 57 | ``` 58 | or 59 | ```js 60 | import { css } from 'react-emotion' 61 | 62 | const activeLinkStyle = css` 63 | color: dodgerblue; 64 | `; 65 | 66 | <NavLink to={{homeAction}} activeClassName={activeLinkStyle}>Home</NavLink> 67 | ``` 68 | 69 | Documentation and more examples can be found in the [GitHub repo](https://github.com/faceyspacey/redux-first-router-link). 70 | -------------------------------------------------------------------------------- /examples/links/links.patch: -------------------------------------------------------------------------------- 1 | diff --git a/src/App.js b/src/App.js 2 | index 3f28b74..295485b 100644 3 | --- a/src/App.js 4 | +++ b/src/App.js 5 | @@ -1,14 +1,26 @@ 6 | import React from 'react' 7 | import { connect } from 'react-redux' 8 | +import Link from 'redux-first-router-link' 9 | 10 | // Contains 'Home', 'User' and 'NotFound' 11 | import * as components from './components' 12 | 13 | -const App = ({ page }) => { 14 | +const App = ({ page, changeUser }) => { 15 | const Component = components[page] 16 | - return <Component /> 17 | + return ( 18 | + <div> 19 | + <Link to="/user/123">User 123</Link> 20 | + <Link to={{ type: 'USER', payload: { id: 456 } }}>User 456</Link> 21 | + <button onClick={() => changeUser(789)}>User 789</button> 22 | + <Component /> 23 | + </div> 24 | + ) 25 | } 26 | 27 | const mapStateToProps = ({ page }) => ({ page }) 28 | 29 | -export default connect(mapStateToProps)(App) 30 | +const mapDispatchToProps = dispatch => ({ 31 | + changeUser: id => dispatch({ type: 'USER', payload: { id } }) 32 | +}) 33 | + 34 | +export default connect(mapStateToProps, mapDispatchToProps)(App) 35 | -------------------------------------------------------------------------------- /examples/minimal/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true 2 | -------------------------------------------------------------------------------- /examples/minimal/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | 'import/no-unresolved': 0, 4 | 'import/extensions': 0 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/minimal/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | // Contains 'Home', 'User' and 'NotFound' 5 | import * as components from './components' 6 | 7 | const App = ({ page }) => { 8 | const Component = components[page] 9 | return <Component /> 10 | } 11 | 12 | const mapStateToProps = ({ page }) => ({ page }) 13 | 14 | export default connect(mapStateToProps)(App) 15 | -------------------------------------------------------------------------------- /examples/minimal/src/components.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | const Home = () => <h3>Home</h3> 5 | 6 | const User = ({ userId }) => <h3>{`User ${userId}`}</h3> 7 | const mapStateToProps = ({ location }) => ({ 8 | userId: location.payload.id 9 | }) 10 | const ConnectedUser = connect(mapStateToProps)(User) 11 | 12 | const NotFound = () => <h3>404</h3> 13 | 14 | 15 | export { Home, ConnectedUser as User, NotFound } 16 | 17 | -------------------------------------------------------------------------------- /examples/minimal/src/configureStore.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, combineReducers, compose, createStore } from 'redux' 2 | import { connectRoutes } from 'redux-first-router' 3 | 4 | import page from './pageReducer' 5 | 6 | const routesMap = { 7 | HOME: '/', 8 | USER: '/user/:id' 9 | } 10 | 11 | export default function configureStore(preloadedState) { 12 | const { reducer, middleware, enhancer } = connectRoutes(routesMap) 13 | 14 | const rootReducer = combineReducers({ page, location: reducer }) 15 | const middlewares = applyMiddleware(middleware) 16 | const enhancers = compose(enhancer, middlewares) 17 | 18 | const store = createStore(rootReducer, preloadedState, enhancers) 19 | 20 | return { store } 21 | } 22 | -------------------------------------------------------------------------------- /examples/minimal/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Provider } from 'react-redux' 3 | import ReactDOM from 'react-dom' 4 | 5 | import configureStore from './configureStore' 6 | import App from './App' 7 | 8 | const { store } = configureStore() 9 | 10 | ReactDOM.render( 11 | <Provider store={store}> 12 | <App /> 13 | </Provider>, 14 | document.getElementById('root') 15 | ) 16 | -------------------------------------------------------------------------------- /examples/minimal/src/pageReducer.js: -------------------------------------------------------------------------------- 1 | import { NOT_FOUND } from 'redux-first-router' 2 | 3 | const components = { 4 | HOME: 'Home', 5 | USER: 'User', 6 | [NOT_FOUND]: 'NotFound' 7 | } 8 | 9 | export default (state = 'HOME', action = {}) => components[action.type] || state 10 | -------------------------------------------------------------------------------- /examples/redux-devtools/README.md: -------------------------------------------------------------------------------- 1 | ### Usage with Redux DevTools 2 | 3 | Dispatch `{ type: 'USER', payload: { id: 13 } }` and note how the address bar changes. 4 | Then use the _Inspector_ to time travel back to `HOME`. 5 | -------------------------------------------------------------------------------- /examples/redux-devtools/redux-devtools.patch: -------------------------------------------------------------------------------- 1 | diff --git a/src/configureStore.js b/src/configureStore.js 2 | index ae0d695..7bd5c7d 100644 3 | --- a/src/configureStore.js 4 | +++ b/src/configureStore.js 5 | @@ -13,7 +13,11 @@ export default function configureStore(preloadedState) { 6 | 7 | const rootReducer = combineReducers({ page, location: reducer }) 8 | const middlewares = applyMiddleware(middleware) 9 | - const enhancers = compose(enhancer, middlewares) 10 | + const composeEnhancers = 11 | + typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ 12 | + ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) 13 | + : compose 14 | + const enhancers = composeEnhancers(enhancer, middlewares) 15 | 16 | const store = createStore(rootReducer, preloadedState, enhancers) 17 | 18 | -------------------------------------------------------------------------------- /examples/thunks/README.md: -------------------------------------------------------------------------------- 1 | ### Dispatching thunks on route changes 2 | 3 | When a page depends on a resource (e.g. an API call), the route change can implicitly trigger a _thunk_ (an async action) to fetch it: 4 | 5 | ```js 6 | const routesMap = { 7 | HOME: { 8 | path: '/', 9 | thunk: (dispatch, getState) => { 10 | // const { myParams } = getState().location.payload; 11 | // const response = await fetch(...) 12 | // dispatch({ type: 'FETCH_READY', ... }) 13 | } 14 | } 15 | } 16 | ``` 17 | 18 | This is a fundamental part of `redux-first-router` which has several benefits compared to other routers: 19 | - It requires fewer actions because the initial "setup" action is implied by the route change. 20 | - It keeps global state decoupled from components and their lifecycle methods, and helps to maintain a top-down app structure. In particular it avoids the anti-pattern of triggering actions from `componentDidMount`. 21 | - It achieves this without depending on additional libraries such as [redux-thunk](https://github.com/reduxjs/redux-thunk) or [redux-saga](https://github.com/redux-saga/redux-saga). 22 | 23 | ### Pathless routes 24 | 25 | A _pathless route_ is a `routesMap` entry without a `path`, which thus is not dispatched as a result of any route change and does not affect the address when dispatched. 26 | While this might be a surprising concept when first learning about `routesMap`, it is a key ingredient in the vision of organizing apps by defining all "loading" actions in the same place and same manner. 27 | 28 | ### Demo 29 | 30 | The patch contains a minimalistic implementation of [The Star Wars API](https://swapi.dev/) which showcases the above concepts. 31 | After applying it, see the relevant parts in action by navigating to **/swapi/people/1** and **/swapi/invalid/route**. 32 | -------------------------------------------------------------------------------- /examples/thunks/thunks.patch: -------------------------------------------------------------------------------- 1 | diff --git a/src/components.js b/src/components.js 2 | index 11e6f74..d4480fd 100644 3 | --- a/src/components.js 4 | +++ b/src/components.js 5 | @@ -13,4 +13,5 @@ const NotFound = () => <h3>404</h3> 6 | 7 | 8 | export { Home, ConnectedUser as User, NotFound } 9 | +export { default as Swapi } from './swapi'; 10 | 11 | diff --git a/src/configureStore.js b/src/configureStore.js 12 | index aef2ce8..c32bacb 100644 13 | --- a/src/configureStore.js 14 | +++ b/src/configureStore.js 15 | @@ -1,17 +1,14 @@ 16 | import { applyMiddleware, combineReducers, compose, createStore } from 'redux' 17 | import { connectRoutes } from 'redux-first-router' 18 | 19 | +import routesMap from './routesMap' 20 | import page from './pageReducer' 21 | - 22 | -const routesMap = { 23 | - HOME: '/', 24 | - USER: '/user/:id' 25 | -} 26 | +import swapi from './swapiReducer' 27 | 28 | export default function configureStore(preloadedState) { 29 | const { reducer, middleware, enhancer } = connectRoutes(routesMap) 30 | 31 | - const rootReducer = combineReducers({ page, location: reducer }) 32 | + const rootReducer = combineReducers({ page, swapi, location: reducer }) 33 | const middlewares = applyMiddleware(middleware) 34 | const enhancers = compose(enhancer, middlewares) 35 | 36 | diff --git a/src/pageReducer.js b/src/pageReducer.js 37 | index 582f04d..262f9d6 100644 38 | --- a/src/pageReducer.js 39 | +++ b/src/pageReducer.js 40 | @@ -3,6 +3,7 @@ import { NOT_FOUND } from 'redux-first-router' 41 | const components = { 42 | HOME: 'Home', 43 | USER: 'User', 44 | + SWAPI: 'Swapi', 45 | [NOT_FOUND]: 'NotFound' 46 | } 47 | 48 | diff --git a/src/routesMap.js b/src/routesMap.js 49 | new file mode 100644 50 | index 0000000..0800aea 51 | --- /dev/null 52 | +++ b/src/routesMap.js 53 | @@ -0,0 +1,39 @@ 54 | +export default { 55 | + HOME: '/', 56 | + USER: '/user/:id', 57 | + SWAPI: { 58 | + path: '/swapi/:type/:id', 59 | + thunk: async (dispatch, getState) => { 60 | + // Will only match this route if type and id are set. For optional segments, use ".../:type?/:id?". 61 | + const { type, id } = getState().location.payload 62 | + const url = `https://swapi.co/api/${type}/${id}` 63 | + 64 | + try { 65 | + const response = await fetch(url) 66 | + if (response.ok) { 67 | + const data = await response.json() 68 | + // Note that only one action is required, since fetching is triggered by the router. 69 | + dispatch({ type: 'SWAPI_FETCHED', payload: { data } }) 70 | + return 71 | + } 72 | + } catch (_) {} 73 | + // Something went wrong, update the response data with the API's usage overview, without changing route. 74 | + dispatch({ type: 'SWAPI_HELP' }) 75 | + } 76 | + }, 77 | + // Below is a _pathless_ route! Despite being defined in routesMap, it's not connected to the route, 78 | + // but is supported in order to have a uniform thunk interface even when routes are not involved. 79 | + // Defining all "setup" actions this way helps structure the app and reduce Redux boilerplate. 80 | + SWAPI_HELP: { 81 | + thunk: async (dispatch, getState) => { 82 | + const action = { type: 'SWAPI_FETCHED', payload: { hasError: true } }; 83 | + try { 84 | + const response = await fetch('https://swapi.co/api/') 85 | + action.payload.data = response.ok ? await response.json() : `Status: ${response.status}` 86 | + } catch (error) { 87 | + action.payload.data = `Error: ${error.message}` 88 | + } 89 | + dispatch(action) 90 | + } 91 | + } 92 | +} 93 | diff --git a/src/swapi.js b/src/swapi.js 94 | new file mode 100644 95 | index 0000000..83394c1 96 | --- /dev/null 97 | +++ b/src/swapi.js 98 | @@ -0,0 +1,22 @@ 99 | +import React from 'react'; 100 | +import { connect } from 'react-redux' 101 | + 102 | +const Swapi = ({ data, hasError, query }) => { 103 | + const title = !data ? `SWAPI: Loading ${query}...` : `SWAPI: ${query}`; 104 | + const hint = <small><em>(Hint: try people/1/ or planets/3/ or starships/9/)</em></small> 105 | + return ( 106 | + <div> 107 | + <h3>{title}</h3> 108 | + {data && <pre>{JSON.stringify(data, null, 2)}</pre>} 109 | + {hasError && hint} 110 | + </div> 111 | + ) 112 | +} 113 | + 114 | +const mapStateToProps = ({ location: { payload }, swapi: { data, hasError }}) => ({ 115 | + data, 116 | + hasError, 117 | + query: `${payload.type}/${payload.id}` 118 | +}) 119 | + 120 | +export default connect(mapStateToProps)(Swapi) 121 | diff --git a/src/swapiReducer.js b/src/swapiReducer.js 122 | new file mode 100644 123 | index 0000000..302455b 124 | --- /dev/null 125 | +++ b/src/swapiReducer.js 126 | @@ -0,0 +1,7 @@ 127 | +export default (state = {}, action = {}) => { 128 | + if (action.type !== 'SWAPI_FETCHED') { 129 | + return state 130 | + } 131 | + const { data, hasError = false } = action.payload 132 | + return { data, hasError } 133 | +} 134 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-cli_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 4435fbf02a5bad6d14e094416a80e996 2 | // flow-typed version: <<STUB>>/babel-cli_v^6.18.0/flow_v0.38.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-cli' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-cli' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-cli/bin/babel-doctor' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'babel-cli/bin/babel-external-helpers' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'babel-cli/bin/babel-node' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'babel-cli/bin/babel' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'babel-cli/lib/_babel-node' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'babel-cli/lib/babel-external-helpers' { 46 | declare module.exports: any; 47 | } 48 | 49 | declare module 'babel-cli/lib/babel-node' { 50 | declare module.exports: any; 51 | } 52 | 53 | declare module 'babel-cli/lib/babel/dir' { 54 | declare module.exports: any; 55 | } 56 | 57 | declare module 'babel-cli/lib/babel/file' { 58 | declare module.exports: any; 59 | } 60 | 61 | declare module 'babel-cli/lib/babel/index' { 62 | declare module.exports: any; 63 | } 64 | 65 | declare module 'babel-cli/lib/babel/util' { 66 | declare module.exports: any; 67 | } 68 | 69 | // Filename aliases 70 | declare module 'babel-cli/bin/babel-doctor.js' { 71 | declare module.exports: $Exports<'babel-cli/bin/babel-doctor'>; 72 | } 73 | declare module 'babel-cli/bin/babel-external-helpers.js' { 74 | declare module.exports: $Exports<'babel-cli/bin/babel-external-helpers'>; 75 | } 76 | declare module 'babel-cli/bin/babel-node.js' { 77 | declare module.exports: $Exports<'babel-cli/bin/babel-node'>; 78 | } 79 | declare module 'babel-cli/bin/babel.js' { 80 | declare module.exports: $Exports<'babel-cli/bin/babel'>; 81 | } 82 | declare module 'babel-cli/index' { 83 | declare module.exports: $Exports<'babel-cli'>; 84 | } 85 | declare module 'babel-cli/index.js' { 86 | declare module.exports: $Exports<'babel-cli'>; 87 | } 88 | declare module 'babel-cli/lib/_babel-node.js' { 89 | declare module.exports: $Exports<'babel-cli/lib/_babel-node'>; 90 | } 91 | declare module 'babel-cli/lib/babel-external-helpers.js' { 92 | declare module.exports: $Exports<'babel-cli/lib/babel-external-helpers'>; 93 | } 94 | declare module 'babel-cli/lib/babel-node.js' { 95 | declare module.exports: $Exports<'babel-cli/lib/babel-node'>; 96 | } 97 | declare module 'babel-cli/lib/babel/dir.js' { 98 | declare module.exports: $Exports<'babel-cli/lib/babel/dir'>; 99 | } 100 | declare module 'babel-cli/lib/babel/file.js' { 101 | declare module.exports: $Exports<'babel-cli/lib/babel/file'>; 102 | } 103 | declare module 'babel-cli/lib/babel/index.js' { 104 | declare module.exports: $Exports<'babel-cli/lib/babel/index'>; 105 | } 106 | declare module 'babel-cli/lib/babel/util.js' { 107 | declare module.exports: $Exports<'babel-cli/lib/babel/util'>; 108 | } 109 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-eslint_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 24b3a8e92da7cc00dc06397f328fb5de 2 | // flow-typed version: <<STUB>>/babel-eslint_v^6.1.2/flow_v0.38.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-eslint' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-eslint' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-eslint/babylon-to-espree/attachComments' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'babel-eslint/babylon-to-espree/convertTemplateType' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'babel-eslint/babylon-to-espree/index' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'babel-eslint/babylon-to-espree/toAST' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'babel-eslint/babylon-to-espree/toToken' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'babel-eslint/babylon-to-espree/toTokens' { 46 | declare module.exports: any; 47 | } 48 | 49 | // Filename aliases 50 | declare module 'babel-eslint/babylon-to-espree/attachComments.js' { 51 | declare module.exports: $Exports<'babel-eslint/babylon-to-espree/attachComments'>; 52 | } 53 | declare module 'babel-eslint/babylon-to-espree/convertTemplateType.js' { 54 | declare module.exports: $Exports<'babel-eslint/babylon-to-espree/convertTemplateType'>; 55 | } 56 | declare module 'babel-eslint/babylon-to-espree/index.js' { 57 | declare module.exports: $Exports<'babel-eslint/babylon-to-espree/index'>; 58 | } 59 | declare module 'babel-eslint/babylon-to-espree/toAST.js' { 60 | declare module.exports: $Exports<'babel-eslint/babylon-to-espree/toAST'>; 61 | } 62 | declare module 'babel-eslint/babylon-to-espree/toToken.js' { 63 | declare module.exports: $Exports<'babel-eslint/babylon-to-espree/toToken'>; 64 | } 65 | declare module 'babel-eslint/babylon-to-espree/toTokens.js' { 66 | declare module.exports: $Exports<'babel-eslint/babylon-to-espree/toTokens'>; 67 | } 68 | declare module 'babel-eslint/index' { 69 | declare module.exports: $Exports<'babel-eslint'>; 70 | } 71 | declare module 'babel-eslint/index.js' { 72 | declare module.exports: $Exports<'babel-eslint'>; 73 | } 74 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-plugin-transform-flow-strip-types_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 147af63ef1970e66fe8225d36f3df862 2 | // flow-typed version: <<STUB>>/babel-plugin-transform-flow-strip-types_v^6.22.0/flow_v0.38.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-plugin-transform-flow-strip-types' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-plugin-transform-flow-strip-types' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-plugin-transform-flow-strip-types/lib/index' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'babel-plugin-transform-flow-strip-types/lib/index.js' { 31 | declare module.exports: $Exports<'babel-plugin-transform-flow-strip-types/lib/index'>; 32 | } 33 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-preset-es2015_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 092160e82da2f82dbd18af7c99d36eb6 2 | // flow-typed version: <<STUB>>/babel-preset-es2015_v^6.18.0/flow_v0.38.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-preset-es2015' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-preset-es2015' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-preset-es2015/lib/index' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'babel-preset-es2015/lib/index.js' { 31 | declare module.exports: $Exports<'babel-preset-es2015/lib/index'>; 32 | } 33 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-preset-react_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 58ccb0b319de0258d3c76c9810681a43 2 | // flow-typed version: <<STUB>>/babel-preset-react_v^6.16.0/flow_v0.38.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-preset-react' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-preset-react' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-preset-react/lib/index' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'babel-preset-react/lib/index.js' { 31 | declare module.exports: $Exports<'babel-preset-react/lib/index'>; 32 | } 33 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-preset-stage-0_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 88c3e8568a66c7e895ff3810de45e6ca 2 | // flow-typed version: <<STUB>>/babel-preset-stage-0_v^6.16.0/flow_v0.38.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-preset-stage-0' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-preset-stage-0' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-preset-stage-0/lib/index' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'babel-preset-stage-0/lib/index.js' { 31 | declare module.exports: $Exports<'babel-preset-stage-0/lib/index'>; 32 | } 33 | -------------------------------------------------------------------------------- /flow-typed/npm/eslint-plugin-babel_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 7c5ac13e602c779e233d563f6c9b8133 2 | // flow-typed version: <<STUB>>/eslint-plugin-babel_v^3.3.0/flow_v0.38.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'eslint-plugin-babel' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'eslint-plugin-babel' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'eslint-plugin-babel/rules/array-bracket-spacing' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'eslint-plugin-babel/rules/arrow-parens' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'eslint-plugin-babel/rules/flow-object-type' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'eslint-plugin-babel/rules/func-params-comma-dangle' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'eslint-plugin-babel/rules/generator-star-spacing' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'eslint-plugin-babel/rules/new-cap' { 46 | declare module.exports: any; 47 | } 48 | 49 | declare module 'eslint-plugin-babel/rules/no-await-in-loop' { 50 | declare module.exports: any; 51 | } 52 | 53 | declare module 'eslint-plugin-babel/rules/object-curly-spacing' { 54 | declare module.exports: any; 55 | } 56 | 57 | declare module 'eslint-plugin-babel/rules/object-shorthand' { 58 | declare module.exports: any; 59 | } 60 | 61 | declare module 'eslint-plugin-babel/tests/array-bracket-spacing' { 62 | declare module.exports: any; 63 | } 64 | 65 | declare module 'eslint-plugin-babel/tests/arrow-parens' { 66 | declare module.exports: any; 67 | } 68 | 69 | declare module 'eslint-plugin-babel/tests/flow-object-type' { 70 | declare module.exports: any; 71 | } 72 | 73 | declare module 'eslint-plugin-babel/tests/func-params-comma-dangle' { 74 | declare module.exports: any; 75 | } 76 | 77 | declare module 'eslint-plugin-babel/tests/generator-star-spacing' { 78 | declare module.exports: any; 79 | } 80 | 81 | declare module 'eslint-plugin-babel/tests/new-cap' { 82 | declare module.exports: any; 83 | } 84 | 85 | declare module 'eslint-plugin-babel/tests/no-await-in-loop' { 86 | declare module.exports: any; 87 | } 88 | 89 | declare module 'eslint-plugin-babel/tests/object-curly-spacing' { 90 | declare module.exports: any; 91 | } 92 | 93 | declare module 'eslint-plugin-babel/tests/object-shorthand' { 94 | declare module.exports: any; 95 | } 96 | 97 | // Filename aliases 98 | declare module 'eslint-plugin-babel/index' { 99 | declare module.exports: $Exports<'eslint-plugin-babel'>; 100 | } 101 | declare module 'eslint-plugin-babel/index.js' { 102 | declare module.exports: $Exports<'eslint-plugin-babel'>; 103 | } 104 | declare module 'eslint-plugin-babel/rules/array-bracket-spacing.js' { 105 | declare module.exports: $Exports<'eslint-plugin-babel/rules/array-bracket-spacing'>; 106 | } 107 | declare module 'eslint-plugin-babel/rules/arrow-parens.js' { 108 | declare module.exports: $Exports<'eslint-plugin-babel/rules/arrow-parens'>; 109 | } 110 | declare module 'eslint-plugin-babel/rules/flow-object-type.js' { 111 | declare module.exports: $Exports<'eslint-plugin-babel/rules/flow-object-type'>; 112 | } 113 | declare module 'eslint-plugin-babel/rules/func-params-comma-dangle.js' { 114 | declare module.exports: $Exports<'eslint-plugin-babel/rules/func-params-comma-dangle'>; 115 | } 116 | declare module 'eslint-plugin-babel/rules/generator-star-spacing.js' { 117 | declare module.exports: $Exports<'eslint-plugin-babel/rules/generator-star-spacing'>; 118 | } 119 | declare module 'eslint-plugin-babel/rules/new-cap.js' { 120 | declare module.exports: $Exports<'eslint-plugin-babel/rules/new-cap'>; 121 | } 122 | declare module 'eslint-plugin-babel/rules/no-await-in-loop.js' { 123 | declare module.exports: $Exports<'eslint-plugin-babel/rules/no-await-in-loop'>; 124 | } 125 | declare module 'eslint-plugin-babel/rules/object-curly-spacing.js' { 126 | declare module.exports: $Exports<'eslint-plugin-babel/rules/object-curly-spacing'>; 127 | } 128 | declare module 'eslint-plugin-babel/rules/object-shorthand.js' { 129 | declare module.exports: $Exports<'eslint-plugin-babel/rules/object-shorthand'>; 130 | } 131 | declare module 'eslint-plugin-babel/tests/array-bracket-spacing.js' { 132 | declare module.exports: $Exports<'eslint-plugin-babel/tests/array-bracket-spacing'>; 133 | } 134 | declare module 'eslint-plugin-babel/tests/arrow-parens.js' { 135 | declare module.exports: $Exports<'eslint-plugin-babel/tests/arrow-parens'>; 136 | } 137 | declare module 'eslint-plugin-babel/tests/flow-object-type.js' { 138 | declare module.exports: $Exports<'eslint-plugin-babel/tests/flow-object-type'>; 139 | } 140 | declare module 'eslint-plugin-babel/tests/func-params-comma-dangle.js' { 141 | declare module.exports: $Exports<'eslint-plugin-babel/tests/func-params-comma-dangle'>; 142 | } 143 | declare module 'eslint-plugin-babel/tests/generator-star-spacing.js' { 144 | declare module.exports: $Exports<'eslint-plugin-babel/tests/generator-star-spacing'>; 145 | } 146 | declare module 'eslint-plugin-babel/tests/new-cap.js' { 147 | declare module.exports: $Exports<'eslint-plugin-babel/tests/new-cap'>; 148 | } 149 | declare module 'eslint-plugin-babel/tests/no-await-in-loop.js' { 150 | declare module.exports: $Exports<'eslint-plugin-babel/tests/no-await-in-loop'>; 151 | } 152 | declare module 'eslint-plugin-babel/tests/object-curly-spacing.js' { 153 | declare module.exports: $Exports<'eslint-plugin-babel/tests/object-curly-spacing'>; 154 | } 155 | declare module 'eslint-plugin-babel/tests/object-shorthand.js' { 156 | declare module.exports: $Exports<'eslint-plugin-babel/tests/object-shorthand'>; 157 | } 158 | -------------------------------------------------------------------------------- /flow-typed/npm/eslint-plugin-flow-vars_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: aa450ea9af9fdcae473ebaa6e117bfda 2 | // flow-typed version: <<STUB>>/eslint-plugin-flow-vars_v^0.5.0/flow_v0.38.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'eslint-plugin-flow-vars' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'eslint-plugin-flow-vars' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'eslint-plugin-flow-vars/define-flow-type' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'eslint-plugin-flow-vars/use-flow-type' { 30 | declare module.exports: any; 31 | } 32 | 33 | // Filename aliases 34 | declare module 'eslint-plugin-flow-vars/define-flow-type.js' { 35 | declare module.exports: $Exports<'eslint-plugin-flow-vars/define-flow-type'>; 36 | } 37 | declare module 'eslint-plugin-flow-vars/index' { 38 | declare module.exports: $Exports<'eslint-plugin-flow-vars'>; 39 | } 40 | declare module 'eslint-plugin-flow-vars/index.js' { 41 | declare module.exports: $Exports<'eslint-plugin-flow-vars'>; 42 | } 43 | declare module 'eslint-plugin-flow-vars/use-flow-type.js' { 44 | declare module.exports: $Exports<'eslint-plugin-flow-vars/use-flow-type'>; 45 | } 46 | -------------------------------------------------------------------------------- /flow-typed/npm/eslint-plugin-flowtype_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 8a1d5147a0d7527cc6f338a678839eb4 2 | // flow-typed version: <<STUB>>/eslint-plugin-flowtype_v^2.30.0/flow_v0.38.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'eslint-plugin-flowtype' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'eslint-plugin-flowtype' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'eslint-plugin-flowtype/bin/readmeAssertions' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'eslint-plugin-flowtype/dist/index' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'eslint-plugin-flowtype/dist/rules/booleanStyle' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'eslint-plugin-flowtype/dist/rules/defineFlowType' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'eslint-plugin-flowtype/dist/rules/delimiterDangle' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'eslint-plugin-flowtype/dist/rules/genericSpacing' { 46 | declare module.exports: any; 47 | } 48 | 49 | declare module 'eslint-plugin-flowtype/dist/rules/noDupeKeys' { 50 | declare module.exports: any; 51 | } 52 | 53 | declare module 'eslint-plugin-flowtype/dist/rules/noPrimitiveConstructorTypes' { 54 | declare module.exports: any; 55 | } 56 | 57 | declare module 'eslint-plugin-flowtype/dist/rules/noWeakTypes' { 58 | declare module.exports: any; 59 | } 60 | 61 | declare module 'eslint-plugin-flowtype/dist/rules/objectTypeDelimiter' { 62 | declare module.exports: any; 63 | } 64 | 65 | declare module 'eslint-plugin-flowtype/dist/rules/requireParameterType' { 66 | declare module.exports: any; 67 | } 68 | 69 | declare module 'eslint-plugin-flowtype/dist/rules/requireReturnType' { 70 | declare module.exports: any; 71 | } 72 | 73 | declare module 'eslint-plugin-flowtype/dist/rules/requireValidFileAnnotation' { 74 | declare module.exports: any; 75 | } 76 | 77 | declare module 'eslint-plugin-flowtype/dist/rules/requireVariableType' { 78 | declare module.exports: any; 79 | } 80 | 81 | declare module 'eslint-plugin-flowtype/dist/rules/semi' { 82 | declare module.exports: any; 83 | } 84 | 85 | declare module 'eslint-plugin-flowtype/dist/rules/sortKeys' { 86 | declare module.exports: any; 87 | } 88 | 89 | declare module 'eslint-plugin-flowtype/dist/rules/spaceAfterTypeColon' { 90 | declare module.exports: any; 91 | } 92 | 93 | declare module 'eslint-plugin-flowtype/dist/rules/spaceBeforeGenericBracket' { 94 | declare module.exports: any; 95 | } 96 | 97 | declare module 'eslint-plugin-flowtype/dist/rules/spaceBeforeTypeColon' { 98 | declare module.exports: any; 99 | } 100 | 101 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateFunctions' { 102 | declare module.exports: any; 103 | } 104 | 105 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateObjectTypeIndexer' { 106 | declare module.exports: any; 107 | } 108 | 109 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateObjectTypeProperty' { 110 | declare module.exports: any; 111 | } 112 | 113 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateReturnType' { 114 | declare module.exports: any; 115 | } 116 | 117 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateTypeCastExpression' { 118 | declare module.exports: any; 119 | } 120 | 121 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateTypical' { 122 | declare module.exports: any; 123 | } 124 | 125 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/index' { 126 | declare module.exports: any; 127 | } 128 | 129 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/reporter' { 130 | declare module.exports: any; 131 | } 132 | 133 | declare module 'eslint-plugin-flowtype/dist/rules/typeIdMatch' { 134 | declare module.exports: any; 135 | } 136 | 137 | declare module 'eslint-plugin-flowtype/dist/rules/unionIntersectionSpacing' { 138 | declare module.exports: any; 139 | } 140 | 141 | declare module 'eslint-plugin-flowtype/dist/rules/useFlowType' { 142 | declare module.exports: any; 143 | } 144 | 145 | declare module 'eslint-plugin-flowtype/dist/rules/validSyntax' { 146 | declare module.exports: any; 147 | } 148 | 149 | declare module 'eslint-plugin-flowtype/dist/utilities/checkFlowFileAnnotation' { 150 | declare module.exports: any; 151 | } 152 | 153 | declare module 'eslint-plugin-flowtype/dist/utilities/fuzzyStringMatch' { 154 | declare module.exports: any; 155 | } 156 | 157 | declare module 'eslint-plugin-flowtype/dist/utilities/getParameterName' { 158 | declare module.exports: any; 159 | } 160 | 161 | declare module 'eslint-plugin-flowtype/dist/utilities/getTokenAfterParens' { 162 | declare module.exports: any; 163 | } 164 | 165 | declare module 'eslint-plugin-flowtype/dist/utilities/getTokenBeforeParens' { 166 | declare module.exports: any; 167 | } 168 | 169 | declare module 'eslint-plugin-flowtype/dist/utilities/index' { 170 | declare module.exports: any; 171 | } 172 | 173 | declare module 'eslint-plugin-flowtype/dist/utilities/isFlowFile' { 174 | declare module.exports: any; 175 | } 176 | 177 | declare module 'eslint-plugin-flowtype/dist/utilities/isFlowFileAnnotation' { 178 | declare module.exports: any; 179 | } 180 | 181 | declare module 'eslint-plugin-flowtype/dist/utilities/iterateFunctionNodes' { 182 | declare module.exports: any; 183 | } 184 | 185 | declare module 'eslint-plugin-flowtype/dist/utilities/quoteName' { 186 | declare module.exports: any; 187 | } 188 | 189 | declare module 'eslint-plugin-flowtype/dist/utilities/spacingFixers' { 190 | declare module.exports: any; 191 | } 192 | 193 | // Filename aliases 194 | declare module 'eslint-plugin-flowtype/bin/readmeAssertions.js' { 195 | declare module.exports: $Exports<'eslint-plugin-flowtype/bin/readmeAssertions'>; 196 | } 197 | declare module 'eslint-plugin-flowtype/dist/index.js' { 198 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/index'>; 199 | } 200 | declare module 'eslint-plugin-flowtype/dist/rules/booleanStyle.js' { 201 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/booleanStyle'>; 202 | } 203 | declare module 'eslint-plugin-flowtype/dist/rules/defineFlowType.js' { 204 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/defineFlowType'>; 205 | } 206 | declare module 'eslint-plugin-flowtype/dist/rules/delimiterDangle.js' { 207 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/delimiterDangle'>; 208 | } 209 | declare module 'eslint-plugin-flowtype/dist/rules/genericSpacing.js' { 210 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/genericSpacing'>; 211 | } 212 | declare module 'eslint-plugin-flowtype/dist/rules/noDupeKeys.js' { 213 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/noDupeKeys'>; 214 | } 215 | declare module 'eslint-plugin-flowtype/dist/rules/noPrimitiveConstructorTypes.js' { 216 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/noPrimitiveConstructorTypes'>; 217 | } 218 | declare module 'eslint-plugin-flowtype/dist/rules/noWeakTypes.js' { 219 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/noWeakTypes'>; 220 | } 221 | declare module 'eslint-plugin-flowtype/dist/rules/objectTypeDelimiter.js' { 222 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/objectTypeDelimiter'>; 223 | } 224 | declare module 'eslint-plugin-flowtype/dist/rules/requireParameterType.js' { 225 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/requireParameterType'>; 226 | } 227 | declare module 'eslint-plugin-flowtype/dist/rules/requireReturnType.js' { 228 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/requireReturnType'>; 229 | } 230 | declare module 'eslint-plugin-flowtype/dist/rules/requireValidFileAnnotation.js' { 231 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/requireValidFileAnnotation'>; 232 | } 233 | declare module 'eslint-plugin-flowtype/dist/rules/requireVariableType.js' { 234 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/requireVariableType'>; 235 | } 236 | declare module 'eslint-plugin-flowtype/dist/rules/semi.js' { 237 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/semi'>; 238 | } 239 | declare module 'eslint-plugin-flowtype/dist/rules/sortKeys.js' { 240 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/sortKeys'>; 241 | } 242 | declare module 'eslint-plugin-flowtype/dist/rules/spaceAfterTypeColon.js' { 243 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/spaceAfterTypeColon'>; 244 | } 245 | declare module 'eslint-plugin-flowtype/dist/rules/spaceBeforeGenericBracket.js' { 246 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/spaceBeforeGenericBracket'>; 247 | } 248 | declare module 'eslint-plugin-flowtype/dist/rules/spaceBeforeTypeColon.js' { 249 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/spaceBeforeTypeColon'>; 250 | } 251 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateFunctions.js' { 252 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateFunctions'>; 253 | } 254 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateObjectTypeIndexer.js' { 255 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateObjectTypeIndexer'>; 256 | } 257 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateObjectTypeProperty.js' { 258 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateObjectTypeProperty'>; 259 | } 260 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateReturnType.js' { 261 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateReturnType'>; 262 | } 263 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateTypeCastExpression.js' { 264 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateTypeCastExpression'>; 265 | } 266 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateTypical.js' { 267 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/typeColonSpacing/evaluateTypical'>; 268 | } 269 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/index.js' { 270 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/typeColonSpacing/index'>; 271 | } 272 | declare module 'eslint-plugin-flowtype/dist/rules/typeColonSpacing/reporter.js' { 273 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/typeColonSpacing/reporter'>; 274 | } 275 | declare module 'eslint-plugin-flowtype/dist/rules/typeIdMatch.js' { 276 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/typeIdMatch'>; 277 | } 278 | declare module 'eslint-plugin-flowtype/dist/rules/unionIntersectionSpacing.js' { 279 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/unionIntersectionSpacing'>; 280 | } 281 | declare module 'eslint-plugin-flowtype/dist/rules/useFlowType.js' { 282 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/useFlowType'>; 283 | } 284 | declare module 'eslint-plugin-flowtype/dist/rules/validSyntax.js' { 285 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/validSyntax'>; 286 | } 287 | declare module 'eslint-plugin-flowtype/dist/utilities/checkFlowFileAnnotation.js' { 288 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/utilities/checkFlowFileAnnotation'>; 289 | } 290 | declare module 'eslint-plugin-flowtype/dist/utilities/fuzzyStringMatch.js' { 291 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/utilities/fuzzyStringMatch'>; 292 | } 293 | declare module 'eslint-plugin-flowtype/dist/utilities/getParameterName.js' { 294 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/utilities/getParameterName'>; 295 | } 296 | declare module 'eslint-plugin-flowtype/dist/utilities/getTokenAfterParens.js' { 297 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/utilities/getTokenAfterParens'>; 298 | } 299 | declare module 'eslint-plugin-flowtype/dist/utilities/getTokenBeforeParens.js' { 300 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/utilities/getTokenBeforeParens'>; 301 | } 302 | declare module 'eslint-plugin-flowtype/dist/utilities/index.js' { 303 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/utilities/index'>; 304 | } 305 | declare module 'eslint-plugin-flowtype/dist/utilities/isFlowFile.js' { 306 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/utilities/isFlowFile'>; 307 | } 308 | declare module 'eslint-plugin-flowtype/dist/utilities/isFlowFileAnnotation.js' { 309 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/utilities/isFlowFileAnnotation'>; 310 | } 311 | declare module 'eslint-plugin-flowtype/dist/utilities/iterateFunctionNodes.js' { 312 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/utilities/iterateFunctionNodes'>; 313 | } 314 | declare module 'eslint-plugin-flowtype/dist/utilities/quoteName.js' { 315 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/utilities/quoteName'>; 316 | } 317 | declare module 'eslint-plugin-flowtype/dist/utilities/spacingFixers.js' { 318 | declare module.exports: $Exports<'eslint-plugin-flowtype/dist/utilities/spacingFixers'>; 319 | } 320 | -------------------------------------------------------------------------------- /flow-typed/npm/eslint-plugin-import_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 84c5e55bff091cbd9594af11fc665d9e 2 | // flow-typed version: <<STUB>>/eslint-plugin-import_v^2.2.0/flow_v0.38.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'eslint-plugin-import' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'eslint-plugin-import' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'eslint-plugin-import/config/electron' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'eslint-plugin-import/config/errors' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'eslint-plugin-import/config/react-native' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'eslint-plugin-import/config/react' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'eslint-plugin-import/config/recommended' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'eslint-plugin-import/config/stage-0' { 46 | declare module.exports: any; 47 | } 48 | 49 | declare module 'eslint-plugin-import/config/warnings' { 50 | declare module.exports: any; 51 | } 52 | 53 | declare module 'eslint-plugin-import/lib/core/importType' { 54 | declare module.exports: any; 55 | } 56 | 57 | declare module 'eslint-plugin-import/lib/core/staticRequire' { 58 | declare module.exports: any; 59 | } 60 | 61 | declare module 'eslint-plugin-import/lib/ExportMap' { 62 | declare module.exports: any; 63 | } 64 | 65 | declare module 'eslint-plugin-import/lib/importDeclaration' { 66 | declare module.exports: any; 67 | } 68 | 69 | declare module 'eslint-plugin-import/lib/index' { 70 | declare module.exports: any; 71 | } 72 | 73 | declare module 'eslint-plugin-import/lib/rules/default' { 74 | declare module.exports: any; 75 | } 76 | 77 | declare module 'eslint-plugin-import/lib/rules/export' { 78 | declare module.exports: any; 79 | } 80 | 81 | declare module 'eslint-plugin-import/lib/rules/extensions' { 82 | declare module.exports: any; 83 | } 84 | 85 | declare module 'eslint-plugin-import/lib/rules/first' { 86 | declare module.exports: any; 87 | } 88 | 89 | declare module 'eslint-plugin-import/lib/rules/imports-first' { 90 | declare module.exports: any; 91 | } 92 | 93 | declare module 'eslint-plugin-import/lib/rules/max-dependencies' { 94 | declare module.exports: any; 95 | } 96 | 97 | declare module 'eslint-plugin-import/lib/rules/named' { 98 | declare module.exports: any; 99 | } 100 | 101 | declare module 'eslint-plugin-import/lib/rules/namespace' { 102 | declare module.exports: any; 103 | } 104 | 105 | declare module 'eslint-plugin-import/lib/rules/newline-after-import' { 106 | declare module.exports: any; 107 | } 108 | 109 | declare module 'eslint-plugin-import/lib/rules/no-absolute-path' { 110 | declare module.exports: any; 111 | } 112 | 113 | declare module 'eslint-plugin-import/lib/rules/no-amd' { 114 | declare module.exports: any; 115 | } 116 | 117 | declare module 'eslint-plugin-import/lib/rules/no-commonjs' { 118 | declare module.exports: any; 119 | } 120 | 121 | declare module 'eslint-plugin-import/lib/rules/no-deprecated' { 122 | declare module.exports: any; 123 | } 124 | 125 | declare module 'eslint-plugin-import/lib/rules/no-duplicates' { 126 | declare module.exports: any; 127 | } 128 | 129 | declare module 'eslint-plugin-import/lib/rules/no-dynamic-require' { 130 | declare module.exports: any; 131 | } 132 | 133 | declare module 'eslint-plugin-import/lib/rules/no-extraneous-dependencies' { 134 | declare module.exports: any; 135 | } 136 | 137 | declare module 'eslint-plugin-import/lib/rules/no-internal-modules' { 138 | declare module.exports: any; 139 | } 140 | 141 | declare module 'eslint-plugin-import/lib/rules/no-mutable-exports' { 142 | declare module.exports: any; 143 | } 144 | 145 | declare module 'eslint-plugin-import/lib/rules/no-named-as-default-member' { 146 | declare module.exports: any; 147 | } 148 | 149 | declare module 'eslint-plugin-import/lib/rules/no-named-as-default' { 150 | declare module.exports: any; 151 | } 152 | 153 | declare module 'eslint-plugin-import/lib/rules/no-named-default' { 154 | declare module.exports: any; 155 | } 156 | 157 | declare module 'eslint-plugin-import/lib/rules/no-namespace' { 158 | declare module.exports: any; 159 | } 160 | 161 | declare module 'eslint-plugin-import/lib/rules/no-nodejs-modules' { 162 | declare module.exports: any; 163 | } 164 | 165 | declare module 'eslint-plugin-import/lib/rules/no-restricted-paths' { 166 | declare module.exports: any; 167 | } 168 | 169 | declare module 'eslint-plugin-import/lib/rules/no-unassigned-import' { 170 | declare module.exports: any; 171 | } 172 | 173 | declare module 'eslint-plugin-import/lib/rules/no-unresolved' { 174 | declare module.exports: any; 175 | } 176 | 177 | declare module 'eslint-plugin-import/lib/rules/no-webpack-loader-syntax' { 178 | declare module.exports: any; 179 | } 180 | 181 | declare module 'eslint-plugin-import/lib/rules/order' { 182 | declare module.exports: any; 183 | } 184 | 185 | declare module 'eslint-plugin-import/lib/rules/prefer-default-export' { 186 | declare module.exports: any; 187 | } 188 | 189 | declare module 'eslint-plugin-import/lib/rules/unambiguous' { 190 | declare module.exports: any; 191 | } 192 | 193 | declare module 'eslint-plugin-import/memo-parser/index' { 194 | declare module.exports: any; 195 | } 196 | 197 | // Filename aliases 198 | declare module 'eslint-plugin-import/config/electron.js' { 199 | declare module.exports: $Exports<'eslint-plugin-import/config/electron'>; 200 | } 201 | declare module 'eslint-plugin-import/config/errors.js' { 202 | declare module.exports: $Exports<'eslint-plugin-import/config/errors'>; 203 | } 204 | declare module 'eslint-plugin-import/config/react-native.js' { 205 | declare module.exports: $Exports<'eslint-plugin-import/config/react-native'>; 206 | } 207 | declare module 'eslint-plugin-import/config/react.js' { 208 | declare module.exports: $Exports<'eslint-plugin-import/config/react'>; 209 | } 210 | declare module 'eslint-plugin-import/config/recommended.js' { 211 | declare module.exports: $Exports<'eslint-plugin-import/config/recommended'>; 212 | } 213 | declare module 'eslint-plugin-import/config/stage-0.js' { 214 | declare module.exports: $Exports<'eslint-plugin-import/config/stage-0'>; 215 | } 216 | declare module 'eslint-plugin-import/config/warnings.js' { 217 | declare module.exports: $Exports<'eslint-plugin-import/config/warnings'>; 218 | } 219 | declare module 'eslint-plugin-import/lib/core/importType.js' { 220 | declare module.exports: $Exports<'eslint-plugin-import/lib/core/importType'>; 221 | } 222 | declare module 'eslint-plugin-import/lib/core/staticRequire.js' { 223 | declare module.exports: $Exports<'eslint-plugin-import/lib/core/staticRequire'>; 224 | } 225 | declare module 'eslint-plugin-import/lib/ExportMap.js' { 226 | declare module.exports: $Exports<'eslint-plugin-import/lib/ExportMap'>; 227 | } 228 | declare module 'eslint-plugin-import/lib/importDeclaration.js' { 229 | declare module.exports: $Exports<'eslint-plugin-import/lib/importDeclaration'>; 230 | } 231 | declare module 'eslint-plugin-import/lib/index.js' { 232 | declare module.exports: $Exports<'eslint-plugin-import/lib/index'>; 233 | } 234 | declare module 'eslint-plugin-import/lib/rules/default.js' { 235 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/default'>; 236 | } 237 | declare module 'eslint-plugin-import/lib/rules/export.js' { 238 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/export'>; 239 | } 240 | declare module 'eslint-plugin-import/lib/rules/extensions.js' { 241 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/extensions'>; 242 | } 243 | declare module 'eslint-plugin-import/lib/rules/first.js' { 244 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/first'>; 245 | } 246 | declare module 'eslint-plugin-import/lib/rules/imports-first.js' { 247 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/imports-first'>; 248 | } 249 | declare module 'eslint-plugin-import/lib/rules/max-dependencies.js' { 250 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/max-dependencies'>; 251 | } 252 | declare module 'eslint-plugin-import/lib/rules/named.js' { 253 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/named'>; 254 | } 255 | declare module 'eslint-plugin-import/lib/rules/namespace.js' { 256 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/namespace'>; 257 | } 258 | declare module 'eslint-plugin-import/lib/rules/newline-after-import.js' { 259 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/newline-after-import'>; 260 | } 261 | declare module 'eslint-plugin-import/lib/rules/no-absolute-path.js' { 262 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-absolute-path'>; 263 | } 264 | declare module 'eslint-plugin-import/lib/rules/no-amd.js' { 265 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-amd'>; 266 | } 267 | declare module 'eslint-plugin-import/lib/rules/no-commonjs.js' { 268 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-commonjs'>; 269 | } 270 | declare module 'eslint-plugin-import/lib/rules/no-deprecated.js' { 271 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-deprecated'>; 272 | } 273 | declare module 'eslint-plugin-import/lib/rules/no-duplicates.js' { 274 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-duplicates'>; 275 | } 276 | declare module 'eslint-plugin-import/lib/rules/no-dynamic-require.js' { 277 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-dynamic-require'>; 278 | } 279 | declare module 'eslint-plugin-import/lib/rules/no-extraneous-dependencies.js' { 280 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-extraneous-dependencies'>; 281 | } 282 | declare module 'eslint-plugin-import/lib/rules/no-internal-modules.js' { 283 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-internal-modules'>; 284 | } 285 | declare module 'eslint-plugin-import/lib/rules/no-mutable-exports.js' { 286 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-mutable-exports'>; 287 | } 288 | declare module 'eslint-plugin-import/lib/rules/no-named-as-default-member.js' { 289 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-named-as-default-member'>; 290 | } 291 | declare module 'eslint-plugin-import/lib/rules/no-named-as-default.js' { 292 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-named-as-default'>; 293 | } 294 | declare module 'eslint-plugin-import/lib/rules/no-named-default.js' { 295 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-named-default'>; 296 | } 297 | declare module 'eslint-plugin-import/lib/rules/no-namespace.js' { 298 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-namespace'>; 299 | } 300 | declare module 'eslint-plugin-import/lib/rules/no-nodejs-modules.js' { 301 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-nodejs-modules'>; 302 | } 303 | declare module 'eslint-plugin-import/lib/rules/no-restricted-paths.js' { 304 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-restricted-paths'>; 305 | } 306 | declare module 'eslint-plugin-import/lib/rules/no-unassigned-import.js' { 307 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-unassigned-import'>; 308 | } 309 | declare module 'eslint-plugin-import/lib/rules/no-unresolved.js' { 310 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-unresolved'>; 311 | } 312 | declare module 'eslint-plugin-import/lib/rules/no-webpack-loader-syntax.js' { 313 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-webpack-loader-syntax'>; 314 | } 315 | declare module 'eslint-plugin-import/lib/rules/order.js' { 316 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/order'>; 317 | } 318 | declare module 'eslint-plugin-import/lib/rules/prefer-default-export.js' { 319 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/prefer-default-export'>; 320 | } 321 | declare module 'eslint-plugin-import/lib/rules/unambiguous.js' { 322 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/unambiguous'>; 323 | } 324 | declare module 'eslint-plugin-import/memo-parser/index.js' { 325 | declare module.exports: $Exports<'eslint-plugin-import/memo-parser/index'>; 326 | } 327 | -------------------------------------------------------------------------------- /flow-typed/npm/flow-bin_v0.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 6a5610678d4b01e13bbfbbc62bdaf583 2 | // flow-typed version: 3817bc6980/flow-bin_v0.x.x/flow_>=v0.25.x 3 | 4 | declare module "flow-bin" { 5 | declare module.exports: string; 6 | } 7 | -------------------------------------------------------------------------------- /flow-typed/npm/flow-copy-source_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 8c365eb8db088cf97dbcf8a3abbef4ca 2 | // flow-typed version: <<STUB>>/flow-copy-source_v^1.1.0/flow_v0.38.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'flow-copy-source' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'flow-copy-source' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'flow-copy-source/bin/flow-copy-source' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'flow-copy-source/src/index' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'flow-copy-source/src/kefir-copy-file' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'flow-copy-source/src/kefir-glob' { 38 | declare module.exports: any; 39 | } 40 | 41 | // Filename aliases 42 | declare module 'flow-copy-source/bin/flow-copy-source.js' { 43 | declare module.exports: $Exports<'flow-copy-source/bin/flow-copy-source'>; 44 | } 45 | declare module 'flow-copy-source/src/index.js' { 46 | declare module.exports: $Exports<'flow-copy-source/src/index'>; 47 | } 48 | declare module 'flow-copy-source/src/kefir-copy-file.js' { 49 | declare module.exports: $Exports<'flow-copy-source/src/kefir-copy-file'>; 50 | } 51 | declare module 'flow-copy-source/src/kefir-glob.js' { 52 | declare module.exports: $Exports<'flow-copy-source/src/kefir-glob'>; 53 | } 54 | -------------------------------------------------------------------------------- /flow-typed/npm/jest_v18.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: e49570b0f5e396c7206dda452bd6f004 2 | // flow-typed version: 1590d813f4/jest_v18.x.x/flow_>=v0.33.x 3 | 4 | type JestMockFn = { 5 | (...args: Array<any>): any, 6 | /** 7 | * An object for introspecting mock calls 8 | */ 9 | mock: { 10 | /** 11 | * An array that represents all calls that have been made into this mock 12 | * function. Each call is represented by an array of arguments that were 13 | * passed during the call. 14 | */ 15 | calls: Array<Array<any>>, 16 | /** 17 | * An array that contains all the object instances that have been 18 | * instantiated from this mock function. 19 | */ 20 | instances: mixed, 21 | }, 22 | /** 23 | * Resets all information stored in the mockFn.mock.calls and 24 | * mockFn.mock.instances arrays. Often this is useful when you want to clean 25 | * up a mock's usage data between two assertions. 26 | */ 27 | mockClear(): Function, 28 | /** 29 | * Resets all information stored in the mock. This is useful when you want to 30 | * completely restore a mock back to its initial state. 31 | */ 32 | mockReset(): Function, 33 | /** 34 | * Accepts a function that should be used as the implementation of the mock. 35 | * The mock itself will still record all calls that go into and instances 36 | * that come from itself -- the only difference is that the implementation 37 | * will also be executed when the mock is called. 38 | */ 39 | mockImplementation(fn: Function): JestMockFn, 40 | /** 41 | * Accepts a function that will be used as an implementation of the mock for 42 | * one call to the mocked function. Can be chained so that multiple function 43 | * calls produce different results. 44 | */ 45 | mockImplementationOnce(fn: Function): JestMockFn, 46 | /** 47 | * Just a simple sugar function for returning `this` 48 | */ 49 | mockReturnThis(): void, 50 | /** 51 | * Deprecated: use jest.fn(() => value) instead 52 | */ 53 | mockReturnValue(value: any): JestMockFn, 54 | /** 55 | * Sugar for only returning a value once inside your mock 56 | */ 57 | mockReturnValueOnce(value: any): JestMockFn, 58 | } 59 | 60 | type JestAsymmetricEqualityType = { 61 | /** 62 | * A custom Jasmine equality tester 63 | */ 64 | asymmetricMatch(value: mixed): boolean, 65 | } 66 | 67 | type JestCallsType = { 68 | allArgs(): mixed, 69 | all(): mixed, 70 | any(): boolean, 71 | count(): number, 72 | first(): mixed, 73 | mostRecent(): mixed, 74 | reset(): void, 75 | } 76 | 77 | type JestClockType = { 78 | install(): void, 79 | mockDate(date: Date): void, 80 | tick(): void, 81 | uninstall(): void, 82 | } 83 | 84 | type JestMatcherResult = { 85 | message?: string | ()=>string, 86 | pass: boolean, 87 | } 88 | 89 | type JestMatcher = (actual: any, expected: any) => JestMatcherResult; 90 | 91 | type JestExpectType = { 92 | not: JestExpectType, 93 | /** 94 | * If you have a mock function, you can use .lastCalledWith to test what 95 | * arguments it was last called with. 96 | */ 97 | lastCalledWith(...args: Array<any>): void, 98 | /** 99 | * toBe just checks that a value is what you expect. It uses === to check 100 | * strict equality. 101 | */ 102 | toBe(value: any): void, 103 | /** 104 | * Use .toHaveBeenCalled to ensure that a mock function got called. 105 | */ 106 | toBeCalled(): void, 107 | /** 108 | * Use .toBeCalledWith to ensure that a mock function was called with 109 | * specific arguments. 110 | */ 111 | toBeCalledWith(...args: Array<any>): void, 112 | /** 113 | * Using exact equality with floating point numbers is a bad idea. Rounding 114 | * means that intuitive things fail. 115 | */ 116 | toBeCloseTo(num: number, delta: any): void, 117 | /** 118 | * Use .toBeDefined to check that a variable is not undefined. 119 | */ 120 | toBeDefined(): void, 121 | /** 122 | * Use .toBeFalsy when you don't care what a value is, you just want to 123 | * ensure a value is false in a boolean context. 124 | */ 125 | toBeFalsy(): void, 126 | /** 127 | * To compare floating point numbers, you can use toBeGreaterThan. 128 | */ 129 | toBeGreaterThan(number: number): void, 130 | /** 131 | * To compare floating point numbers, you can use toBeGreaterThanOrEqual. 132 | */ 133 | toBeGreaterThanOrEqual(number: number): void, 134 | /** 135 | * To compare floating point numbers, you can use toBeLessThan. 136 | */ 137 | toBeLessThan(number: number): void, 138 | /** 139 | * To compare floating point numbers, you can use toBeLessThanOrEqual. 140 | */ 141 | toBeLessThanOrEqual(number: number): void, 142 | /** 143 | * Use .toBeInstanceOf(Class) to check that an object is an instance of a 144 | * class. 145 | */ 146 | toBeInstanceOf(cls: Class<*>): void, 147 | /** 148 | * .toBeNull() is the same as .toBe(null) but the error messages are a bit 149 | * nicer. 150 | */ 151 | toBeNull(): void, 152 | /** 153 | * Use .toBeTruthy when you don't care what a value is, you just want to 154 | * ensure a value is true in a boolean context. 155 | */ 156 | toBeTruthy(): void, 157 | /** 158 | * Use .toBeUndefined to check that a variable is undefined. 159 | */ 160 | toBeUndefined(): void, 161 | /** 162 | * Use .toContain when you want to check that an item is in a list. For 163 | * testing the items in the list, this uses ===, a strict equality check. 164 | */ 165 | toContain(item: any): void, 166 | /** 167 | * Use .toContainEqual when you want to check that an item is in a list. For 168 | * testing the items in the list, this matcher recursively checks the 169 | * equality of all fields, rather than checking for object identity. 170 | */ 171 | toContainEqual(item: any): void, 172 | /** 173 | * Use .toEqual when you want to check that two objects have the same value. 174 | * This matcher recursively checks the equality of all fields, rather than 175 | * checking for object identity. 176 | */ 177 | toEqual(value: any): void, 178 | /** 179 | * Use .toHaveBeenCalled to ensure that a mock function got called. 180 | */ 181 | toHaveBeenCalled(): void, 182 | /** 183 | * Use .toHaveBeenCalledTimes to ensure that a mock function got called exact 184 | * number of times. 185 | */ 186 | toHaveBeenCalledTimes(number: number): void, 187 | /** 188 | * Use .toHaveBeenCalledWith to ensure that a mock function was called with 189 | * specific arguments. 190 | */ 191 | toHaveBeenCalledWith(...args: Array<any>): void, 192 | /** 193 | * Check that an object has a .length property and it is set to a certain 194 | * numeric value. 195 | */ 196 | toHaveLength(number: number): void, 197 | /** 198 | * 199 | */ 200 | toHaveProperty(propPath: string, value?: any): void, 201 | /** 202 | * Use .toMatch to check that a string matches a regular expression. 203 | */ 204 | toMatch(regexp: RegExp): void, 205 | /** 206 | * Use .toMatchObject to check that a javascript object matches a subset of the properties of an object. 207 | */ 208 | toMatchObject(object: Object): void, 209 | /** 210 | * This ensures that a React component matches the most recent snapshot. 211 | */ 212 | toMatchSnapshot(name?: string): void, 213 | /** 214 | * Use .toThrow to test that a function throws when it is called. 215 | */ 216 | toThrow(message?: string | Error): void, 217 | /** 218 | * Use .toThrowError to test that a function throws a specific error when it 219 | * is called. The argument can be a string for the error message, a class for 220 | * the error, or a regex that should match the error. 221 | */ 222 | toThrowError(message?: string | Error | RegExp): void, 223 | /** 224 | * Use .toThrowErrorMatchingSnapshot to test that a function throws a error 225 | * matching the most recent snapshot when it is called. 226 | */ 227 | toThrowErrorMatchingSnapshot(): void, 228 | } 229 | 230 | type JestObjectType = { 231 | /** 232 | * Disables automatic mocking in the module loader. 233 | * 234 | * After this method is called, all `require()`s will return the real 235 | * versions of each module (rather than a mocked version). 236 | */ 237 | disableAutomock(): JestObjectType, 238 | /** 239 | * An un-hoisted version of disableAutomock 240 | */ 241 | autoMockOff(): JestObjectType, 242 | /** 243 | * Enables automatic mocking in the module loader. 244 | */ 245 | enableAutomock(): JestObjectType, 246 | /** 247 | * An un-hoisted version of enableAutomock 248 | */ 249 | autoMockOn(): JestObjectType, 250 | /** 251 | * Resets the state of all mocks. Equivalent to calling .mockReset() on every 252 | * mocked function. 253 | */ 254 | resetAllMocks(): JestObjectType, 255 | /** 256 | * Removes any pending timers from the timer system. 257 | */ 258 | clearAllTimers(): void, 259 | /** 260 | * The same as `mock` but not moved to the top of the expectation by 261 | * babel-jest. 262 | */ 263 | doMock(moduleName: string, moduleFactory?: any): JestObjectType, 264 | /** 265 | * The same as `unmock` but not moved to the top of the expectation by 266 | * babel-jest. 267 | */ 268 | dontMock(moduleName: string): JestObjectType, 269 | /** 270 | * Returns a new, unused mock function. Optionally takes a mock 271 | * implementation. 272 | */ 273 | fn(implementation?: Function): JestMockFn, 274 | /** 275 | * Determines if the given function is a mocked function. 276 | */ 277 | isMockFunction(fn: Function): boolean, 278 | /** 279 | * Given the name of a module, use the automatic mocking system to generate a 280 | * mocked version of the module for you. 281 | */ 282 | genMockFromModule(moduleName: string): any, 283 | /** 284 | * Mocks a module with an auto-mocked version when it is being required. 285 | * 286 | * The second argument can be used to specify an explicit module factory that 287 | * is being run instead of using Jest's automocking feature. 288 | * 289 | * The third argument can be used to create virtual mocks -- mocks of modules 290 | * that don't exist anywhere in the system. 291 | */ 292 | mock(moduleName: string, moduleFactory?: any): JestObjectType, 293 | /** 294 | * Resets the module registry - the cache of all required modules. This is 295 | * useful to isolate modules where local state might conflict between tests. 296 | */ 297 | resetModules(): JestObjectType, 298 | /** 299 | * Exhausts the micro-task queue (usually interfaced in node via 300 | * process.nextTick). 301 | */ 302 | runAllTicks(): void, 303 | /** 304 | * Exhausts the macro-task queue (i.e., all tasks queued by setTimeout(), 305 | * setInterval(), and setImmediate()). 306 | */ 307 | runAllTimers(): void, 308 | /** 309 | * Exhausts all tasks queued by setImmediate(). 310 | */ 311 | runAllImmediates(): void, 312 | /** 313 | * Executes only the macro task queue (i.e. all tasks queued by setTimeout() 314 | * or setInterval() and setImmediate()). 315 | */ 316 | runTimersToTime(msToRun: number): void, 317 | /** 318 | * Executes only the macro-tasks that are currently pending (i.e., only the 319 | * tasks that have been queued by setTimeout() or setInterval() up to this 320 | * point) 321 | */ 322 | runOnlyPendingTimers(): void, 323 | /** 324 | * Explicitly supplies the mock object that the module system should return 325 | * for the specified module. Note: It is recommended to use jest.mock() 326 | * instead. 327 | */ 328 | setMock(moduleName: string, moduleExports: any): JestObjectType, 329 | /** 330 | * Indicates that the module system should never return a mocked version of 331 | * the specified module from require() (e.g. that it should always return the 332 | * real module). 333 | */ 334 | unmock(moduleName: string): JestObjectType, 335 | /** 336 | * Instructs Jest to use fake versions of the standard timer functions 337 | * (setTimeout, setInterval, clearTimeout, clearInterval, nextTick, 338 | * setImmediate and clearImmediate). 339 | */ 340 | useFakeTimers(): JestObjectType, 341 | /** 342 | * Instructs Jest to use the real versions of the standard timer functions. 343 | */ 344 | useRealTimers(): JestObjectType, 345 | } 346 | 347 | type JestSpyType = { 348 | calls: JestCallsType, 349 | } 350 | 351 | /** Runs this function after every test inside this context */ 352 | declare function afterEach(fn: Function): void; 353 | /** Runs this function before every test inside this context */ 354 | declare function beforeEach(fn: Function): void; 355 | /** Runs this function after all tests have finished inside this context */ 356 | declare function afterAll(fn: Function): void; 357 | /** Runs this function before any tests have started inside this context */ 358 | declare function beforeAll(fn: Function): void; 359 | /** A context for grouping tests together */ 360 | declare function describe(name: string, fn: Function): void; 361 | 362 | /** An individual test unit */ 363 | declare var it: { 364 | /** 365 | * An individual test unit 366 | * 367 | * @param {string} Name of Test 368 | * @param {Function} Test 369 | */ 370 | (name: string, fn?: Function): ?Promise<void>, 371 | /** 372 | * Only run this test 373 | * 374 | * @param {string} Name of Test 375 | * @param {Function} Test 376 | */ 377 | only(name: string, fn?: Function): ?Promise<void>, 378 | /** 379 | * Skip running this test 380 | * 381 | * @param {string} Name of Test 382 | * @param {Function} Test 383 | */ 384 | skip(name: string, fn?: Function): ?Promise<void>, 385 | /** 386 | * Run the test concurrently 387 | * 388 | * @param {string} Name of Test 389 | * @param {Function} Test 390 | */ 391 | concurrent(name: string, fn?: Function): ?Promise<void>, 392 | }; 393 | declare function fit(name: string, fn: Function): ?Promise<void>; 394 | /** An individual test unit */ 395 | declare var test: typeof it; 396 | /** A disabled group of tests */ 397 | declare var xdescribe: typeof describe; 398 | /** A focused group of tests */ 399 | declare var fdescribe: typeof describe; 400 | /** A disabled individual test */ 401 | declare var xit: typeof it; 402 | /** A disabled individual test */ 403 | declare var xtest: typeof it; 404 | 405 | /** The expect function is used every time you want to test a value */ 406 | declare var expect: { 407 | /** The object that you want to make assertions against */ 408 | (value: any): JestExpectType, 409 | /** Add additional Jasmine matchers to Jest's roster */ 410 | extend(matchers: {[name:string]: JestMatcher}): void, 411 | assertions(expectedAssertions: number): void, 412 | any(value: mixed): JestAsymmetricEqualityType, 413 | anything(): void, 414 | arrayContaining(value: Array<mixed>): void, 415 | objectContaining(value: Object): void, 416 | stringMatching(value: string): void, 417 | }; 418 | 419 | // TODO handle return type 420 | // http://jasmine.github.io/2.4/introduction.html#section-Spies 421 | declare function spyOn(value: mixed, method: string): Object; 422 | 423 | /** Holds all functions related to manipulating test runner */ 424 | declare var jest: JestObjectType 425 | 426 | /** 427 | * The global Jamine object, this is generally not exposed as the public API, 428 | * using features inside here could break in later versions of Jest. 429 | */ 430 | declare var jasmine: { 431 | DEFAULT_TIMEOUT_INTERVAL: number, 432 | any(value: mixed): JestAsymmetricEqualityType, 433 | anything(): void, 434 | arrayContaining(value: Array<mixed>): void, 435 | clock(): JestClockType, 436 | createSpy(name: string): JestSpyType, 437 | createSpyObj(baseName: string, methodNames: Array<string>): {[methodName: string]: JestSpyType}, 438 | objectContaining(value: Object): void, 439 | stringMatching(value: string): void, 440 | } 441 | -------------------------------------------------------------------------------- /flow-typed/npm/react-redux_v4.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 114add35a1264a6ed8b59d446306b830 2 | // flow-typed version: aff2bf770e/react-redux_v4.x.x/flow_>=v0.53.x 3 | 4 | import type { Dispatch, Store } from 'redux' 5 | 6 | declare module 'react-redux' { 7 | /* 8 | 9 | S = State 10 | A = Action 11 | OP = OwnProps 12 | SP = StateProps 13 | DP = DispatchProps 14 | 15 | */ 16 | 17 | declare type MapStateToProps<S, OP: Object, SP: Object> = ( 18 | state: S, 19 | ownProps: OP 20 | ) => SP 21 | 22 | declare type MapDispatchToProps<A, OP: Object, DP: Object> = 23 | | ((dispatch: Dispatch<A>, ownProps: OP) => DP) 24 | | DP 25 | 26 | declare type MergeProps<SP, DP: Object, OP: Object, P: Object> = ( 27 | stateProps: SP, 28 | dispatchProps: DP, 29 | ownProps: OP 30 | ) => P 31 | 32 | declare class ConnectedComponent<OP, P> extends React$Component<OP> { 33 | static WrappedComponent: Class<React$Component<P>>, 34 | getWrappedInstance(): React$Component<P>, 35 | props: OP, 36 | state: void 37 | } 38 | 39 | declare type ConnectedComponentClass<OP, P> = Class<ConnectedComponent<OP, P>> 40 | 41 | declare type Connector<OP, P> = ( 42 | component: React$ComponentType<P> 43 | ) => ConnectedComponentClass<OP, P> 44 | 45 | declare class Provider<S, A> 46 | extends React$Component<{ 47 | store: Store<S, A>, 48 | children?: any 49 | }> {} 50 | 51 | declare type ConnectOptions = { 52 | pure?: boolean, 53 | withRef?: boolean 54 | } 55 | 56 | declare type Null = null | void 57 | 58 | declare function connect<A, OP>( 59 | ...rest: Array<void> // <= workaround for https://github.com/facebook/flow/issues/2360 60 | ): Connector<OP, $Supertype<{ dispatch: Dispatch<A> } & OP>> 61 | 62 | declare function connect<A, OP>( 63 | mapStateToProps: Null, 64 | mapDispatchToProps: Null, 65 | mergeProps: Null, 66 | options: ConnectOptions 67 | ): Connector<OP, $Supertype<{ dispatch: Dispatch<A> } & OP>> 68 | 69 | declare function connect<S, A, OP, SP>( 70 | mapStateToProps: MapStateToProps<S, OP, SP>, 71 | mapDispatchToProps: Null, 72 | mergeProps: Null, 73 | options?: ConnectOptions 74 | ): Connector<OP, $Supertype<SP & { dispatch: Dispatch<A> } & OP>> 75 | 76 | declare function connect<A, OP, DP>( 77 | mapStateToProps: Null, 78 | mapDispatchToProps: MapDispatchToProps<A, OP, DP>, 79 | mergeProps: Null, 80 | options?: ConnectOptions 81 | ): Connector<OP, $Supertype<DP & OP>> 82 | 83 | declare function connect<S, A, OP, SP, DP>( 84 | mapStateToProps: MapStateToProps<S, OP, SP>, 85 | mapDispatchToProps: MapDispatchToProps<A, OP, DP>, 86 | mergeProps: Null, 87 | options?: ConnectOptions 88 | ): Connector<OP, $Supertype<SP & DP & OP>> 89 | 90 | declare function connect<S, A, OP, SP, DP, P>( 91 | mapStateToProps: MapStateToProps<S, OP, SP>, 92 | mapDispatchToProps: MapDispatchToProps<A, OP, DP>, 93 | mergeProps: MergeProps<SP, DP, OP, P>, 94 | options?: ConnectOptions 95 | ): Connector<OP, P> 96 | } 97 | -------------------------------------------------------------------------------- /flow-typed/npm/redux_v3.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: ba132c96664f1a05288f3eb2272a3c35 2 | // flow-typed version: c4bbd91cfc/redux_v3.x.x/flow_>=v0.33.x 3 | 4 | declare module 'redux' { 5 | 6 | /* 7 | 8 | S = State 9 | A = Action 10 | 11 | */ 12 | 13 | 14 | declare type Dispatch<A: { type: $Subtype<string> }> = (action: A) => A; 15 | 16 | declare type MiddlewareAPI<S, A> = { 17 | dispatch: Dispatch<A>; 18 | getState(): S; 19 | }; 20 | 21 | declare type Store<S, A> = { 22 | // rewrite MiddlewareAPI members in order to get nicer error messages (intersections produce long messages) 23 | dispatch: Dispatch<A>; 24 | getState(): S; 25 | subscribe(listener: () => void): () => void; 26 | replaceReducer(nextReducer: Reducer<S, A>): void 27 | }; 28 | 29 | declare type Reducer<S, A> = (state: S, action: A) => S; 30 | 31 | declare type Middleware<S, A> = 32 | (api: MiddlewareAPI<S, A>) => 33 | (next: Dispatch<A>) => Dispatch<A>; 34 | 35 | declare type StoreCreator<S, A> = { 36 | (reducer: Reducer<S, A>, enhancer?: StoreEnhancer<S, A>): Store<S, A>; 37 | (reducer: Reducer<S, A>, preloadedState: S, enhancer?: StoreEnhancer<S, A>): Store<S, A>; 38 | }; 39 | 40 | declare type StoreEnhancer<S, A> = (next: StoreCreator<S, A>) => StoreCreator<S, A>; 41 | 42 | declare function createStore<S, A>(reducer: Reducer<S, A>, enhancer?: StoreEnhancer<S, A>): Store<S, A>; 43 | declare function createStore<S, A>(reducer: Reducer<S, A>, preloadedState: S, enhancer?: StoreEnhancer<S, A>): Store<S, A>; 44 | 45 | declare function applyMiddleware<S, A>(...middlewares: Array<Middleware<S, A>>): StoreEnhancer<S, A>; 46 | 47 | declare type ActionCreator<A, B> = (...args: Array<B>) => A; 48 | declare type ActionCreators<K, A> = { [key: K]: ActionCreator<A, any> }; 49 | 50 | declare function bindActionCreators<A, C: ActionCreator<A, any>>(actionCreator: C, dispatch: Dispatch<A>): C; 51 | declare function bindActionCreators<A, K, C: ActionCreators<K, A>>(actionCreators: C, dispatch: Dispatch<A>): C; 52 | 53 | declare function combineReducers<O: Object, A>(reducers: O): Reducer<$ObjMap<O, <S>(r: Reducer<S, any>) => S>, A>; 54 | 55 | declare function compose<S, A>(...fns: Array<StoreEnhancer<S, A>>): Function; 56 | 57 | } 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-first-router", 3 | "version": "2.1.1", 4 | "description": "think of your app in states not routes (and, yes, while keeping the address bar in sync)", 5 | "main": "dist/index.js", 6 | "module": "dist/es", 7 | "engines": { 8 | "node": ">=6.0.0" 9 | }, 10 | "scripts": { 11 | "build": "babel src -d dist", 12 | "build:es": "BABEL_ENV=es babel src -d dist/es", 13 | "build:umd": "BABEL_ENV=commonjs NODE_ENV=development webpack src/index.js -o dist/redux-first-router.js", 14 | "build:umd:min": "BABEL_ENV=commonjs NODE_ENV=production webpack src/index.js -o dist/redux-first-router.min.js", 15 | "flow-copy": "flow-copy-source src dist", 16 | "flow-watch": "clear; printf \"\\033[3J\" & npm run flow & fswatch -o ./ | xargs -n1 -I{} sh -c 'clear; printf \"\\033[3J\" && npm run flow'", 17 | "flow": "npm run flow-copy && flow; test $? -eq 0 -o $? -eq 2", 18 | "clean": "rimraf dist && mkdir dist && rimraf coverage", 19 | "test": "jest", 20 | "lint": "eslint --fix ./", 21 | "format": "prettier --single-quote --parser=flow --semi=false --write '{src,__tests__,__test-helpers__}/**/*.js' && npm run lint", 22 | "cm": "git-cz", 23 | "semantic-release": "npx semantic-release", 24 | "standard-version": "node_modules/.bin/standard-version", 25 | "local-semantic-release": "git checkout master; git pull origin master && npm run standard-version && git push --follow-tags origin master && npm publish", 26 | "prepublishOnly": "npm run clean && npm run build && npm run flow-copy && npm run build:umd && npm run build:umd:min && npm run build:es" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/faceyspacey/redux-first-router.git" 31 | }, 32 | "author": "James Gillmore <james@faceyspacey.com>", 33 | "maintainers": [ 34 | "Zack Jackson <zack@ScriptedAlchemy.com>" 35 | ], 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/faceyspacey/redux-first-router/issues" 39 | }, 40 | "homepage": "https://github.com/faceyspacey/redux-first-router#readme", 41 | "dependencies": { 42 | "rudy-history": "^1.0.0", 43 | "rudy-match-path": "^0.3.0" 44 | }, 45 | "devDependencies": { 46 | "babel-cli": "6.26.0", 47 | "babel-core": "6.26.3", 48 | "babel-eslint": "10.1.0", 49 | "babel-loader": "8.1.0", 50 | "babel-plugin-transform-flow-strip-types": "6.22.0", 51 | "babel-preset-env": "1.7.0", 52 | "babel-preset-es2015": "6.24.1", 53 | "babel-preset-flow": "6.23.0", 54 | "babel-preset-react": "6.24.1", 55 | "babel-preset-stage-0": "6.24.1", 56 | "commitizen": "4.0.3", 57 | "cz-conventional-changelog": "3.2.0", 58 | "eslint": "6.8.0", 59 | "eslint-config-airbnb": "18.1.0", 60 | "eslint-plugin-flowtype": "2.50.3", 61 | "eslint-plugin-import": "2.20.1", 62 | "eslint-plugin-jsx-a11y": "6.2.3", 63 | "eslint-plugin-react": "6.10.3", 64 | "flow-bin": "0.121.0", 65 | "flow-copy-source": "2.0.9", 66 | "husky": "4.2.3", 67 | "jest": "25.1.0", 68 | "lint-staged": "10.0.8", 69 | "prettier": "2.0.2", 70 | "query-string": "6.12.1", 71 | "redux": "4.0.5", 72 | "redux-thunk": "2.3.0", 73 | "rimraf": "3.0.2", 74 | "semantic-release": "17.2.3", 75 | "snyk": "1.303.1", 76 | "standard-version": "8.0.1", 77 | "travis-github-status": "1.6.3", 78 | "uglifyjs-webpack-plugin": "2.2.0", 79 | "webpack": "4.43.0", 80 | "webpack-cli": "3.3.11" 81 | }, 82 | "peerDependencies": { 83 | "redux": "*" 84 | }, 85 | "jest": { 86 | "verbose": true, 87 | "silent": true, 88 | "testURL": "http://localhost:3000", 89 | "setupTestFrameworkScriptFile": "./__test-helpers__/setupJest.js", 90 | "moduleFileExtensions": [ 91 | "js" 92 | ] 93 | }, 94 | "config": { 95 | "commitizen": { 96 | "path": "./node_modules/cz-conventional-changelog" 97 | } 98 | }, 99 | "lint-staged": { 100 | "*.js": [ 101 | "prettier --single-quote --parser=flow --semi=false --write", 102 | "eslint --fix", 103 | "git add" 104 | ] 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | ":combinePatchMinorReleases", 5 | ":ignoreUnstable", 6 | ":prImmediately", 7 | ":semanticPrefixFixDepsChoreOthers", 8 | ":updateNotScheduled", 9 | ":automergeDisabled", 10 | ":ignoreModulesAndTests", 11 | ":maintainLockFilesDisabled", 12 | ":autodetectPinVersions", 13 | ":prHourlyLimit4", 14 | ":prConcurrentLimit20", 15 | "group:monorepos", 16 | "group:recommended", 17 | "helpers:disableTypesNodeMajor", 18 | ":pinAllExceptPeerDependencies", 19 | ":pinOnlyDevDependencies" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/action-creators/addRoutes.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { ADD_ROUTES } from '../index' 4 | import type { RoutesMap, Dispatch } from '../flow-types' 5 | 6 | export default (routes: RoutesMap) => (dispatch: Dispatch) => 7 | dispatch({ type: ADD_ROUTES, payload: { routes } }) 8 | -------------------------------------------------------------------------------- /src/action-creators/historyCreateAction.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { 3 | RoutesMap, 4 | Location, 5 | Action, 6 | History, 7 | QuerySerializer 8 | } from '../flow-types' 9 | import pathToAction from '../pure-utils/pathToAction' 10 | import nestAction from '../pure-utils/nestAction' 11 | 12 | export default ( 13 | pathname: string, 14 | routesMap: RoutesMap, 15 | prevLocation: Location, 16 | history: History, 17 | kind: string, 18 | serializer?: QuerySerializer, 19 | prevPath?: string, 20 | prevLength?: number 21 | ): Action => { 22 | const action = pathToAction(pathname, routesMap, serializer) 23 | kind = getKind(!!history.entries, history, kind, prevPath, prevLength) 24 | return nestAction(pathname, action, prevLocation, history, kind) 25 | } 26 | 27 | const getKind = ( 28 | isMemoryHistory: boolean, 29 | history: History, 30 | kind: string, 31 | prevPath: ?string, 32 | prevLength: ?number 33 | ): string => { 34 | if (!isMemoryHistory || !prevPath || kind !== 'pop') { 35 | return kind 36 | } 37 | 38 | if (isBack(history, prevPath)) { 39 | return 'back' 40 | } 41 | else if (isNext(history, prevPath, prevLength)) { 42 | return 'next' 43 | } 44 | 45 | return kind 46 | } 47 | 48 | const isBack = (hist: History, path: string): boolean => { 49 | const next = hist.entries[hist.index + 1] 50 | return next && next.pathname === path 51 | } 52 | 53 | const isNext = (hist: History, path: string, length: ?number): boolean => { 54 | const prev = hist.entries[hist.index - 1] 55 | const notPushed = length === hist.length 56 | 57 | return prev && prev.pathname === path && notPushed 58 | } 59 | -------------------------------------------------------------------------------- /src/action-creators/middlewareCreateAction.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { 3 | RoutesMap, 4 | Location, 5 | Action, 6 | ReceivedAction, 7 | History, 8 | QuerySerializer 9 | } from '../flow-types' 10 | import actionToPath from '../pure-utils/actionToPath' 11 | import nestAction from '../pure-utils/nestAction' 12 | import { NOT_FOUND } from '../index' 13 | 14 | const __DEV__ = process.env.NODE_ENV !== 'production' 15 | 16 | export default ( 17 | action: Object, 18 | routesMap: RoutesMap, 19 | prevLocation: Location, 20 | history: History, 21 | notFoundPath: string, 22 | serializer?: QuerySerializer 23 | ): Action => { 24 | try { 25 | const pathname = actionToPath(action, routesMap, serializer) 26 | const kind = getKind(!!history.entries, pathname, history, action) 27 | return nestAction(pathname, action, prevLocation, history, kind) 28 | } 29 | catch (e) { 30 | if (__DEV__) { 31 | console.error('[redux-first-router] Internal exception when parsing action, fallback to NOT_FOUND. Original exception: ', e) 32 | } 33 | 34 | const payload = { ...action.payload } 35 | 36 | return nestAction( 37 | notFoundPath || prevLocation.pathname || '/', 38 | { ...action, type: NOT_FOUND, payload }, 39 | prevLocation, 40 | history 41 | ) 42 | } 43 | } 44 | 45 | // REACT NATIVE FEATURE: 46 | // emulate npm `history` package and `historyCreateAction` so that actions 47 | // and state indicate the user went back or forward. The idea is if you are 48 | // going back or forward to a route you were just at, apps can determine 49 | // from `state.location.kind === 'back|next'` and `action.kind` that things like 50 | // scroll position should be restored. 51 | // NOTE: for testability, history is also returned to make this a pure function 52 | const getKind = ( 53 | isMemoryHistory: boolean, 54 | pathname: string, 55 | history: History, 56 | action: ReceivedAction 57 | ): ?string => { 58 | const kind = action.meta && action.meta.location && action.meta.location.kind 59 | 60 | if (kind) { 61 | return kind 62 | } 63 | else if (!isMemoryHistory) { 64 | return 'push' 65 | } 66 | 67 | if (goingBack(history, pathname)) { 68 | history.index-- 69 | return 'back' 70 | } 71 | else if (goingForward(history, pathname)) { 72 | history.index++ 73 | return 'next' 74 | } 75 | 76 | return 'push' 77 | } 78 | 79 | const goingBack = (hist: History, path: string): boolean => { 80 | const prev = hist.entries[hist.index - 1] 81 | return prev && prev.pathname === path 82 | } 83 | 84 | const goingForward = (hist: History, path: string): boolean => { 85 | const next = hist.entries[hist.index + 1] 86 | return next && next.pathname === path 87 | } 88 | -------------------------------------------------------------------------------- /src/action-creators/middlewareCreateNotFoundAction.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Location, Action, LocationState, History } from '../flow-types' 3 | import nestAction from '../pure-utils/nestAction' 4 | import { NOT_FOUND } from '../index' 5 | 6 | export default ( 7 | action: Object, 8 | location: LocationState, 9 | prevLocation: Location, 10 | history: History, 11 | notFoundPath: string 12 | ): Action => { 13 | const { payload } = action 14 | 15 | const meta = action.meta 16 | const prevPath = location.pathname 17 | 18 | const kind = 19 | (meta && meta.location && meta.location.kind) || // use case: kind === 'redirect' 20 | (location.kind === 'load' && 'load') || 21 | 'push' 22 | 23 | const pathname = 24 | (meta && meta.notFoundPath) || 25 | (kind === 'redirect' && notFoundPath) || 26 | prevPath || 27 | '/' 28 | 29 | return nestAction( 30 | pathname, 31 | { type: NOT_FOUND, payload }, 32 | prevLocation, 33 | history, 34 | kind 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /src/action-creators/redirect.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Action } from '../flow-types' 3 | import setKind from '../pure-utils/setKind' 4 | 5 | export default (action: Action, type?: string, payload?: any) => { 6 | action = setKind(action, 'redirect') 7 | 8 | if (type) { 9 | action.type = type 10 | } 11 | 12 | if (payload) { 13 | action.payload = payload 14 | } 15 | 16 | return action 17 | } 18 | -------------------------------------------------------------------------------- /src/flow-types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Dispatch as ReduxDispatch, Store as ReduxStore } from 'redux' 3 | 4 | export type Dispatch = ReduxDispatch<*> 5 | export type GetState = () => Object 6 | export type RouteString = string 7 | export type ConfirmLeave = (state: Object, action: Object) => ?string 8 | 9 | export type Bag = { 10 | action: ReceivedAction | Action, 11 | extra: any 12 | } 13 | 14 | export type RouteObject = { 15 | path?: string, 16 | capitalizedWords?: boolean, 17 | coerceNumbers?: boolean, 18 | toPath?: (param: string, key?: string) => string, 19 | fromPath?: (path: string, key?: string) => string, 20 | thunk?: ( 21 | dispatch: Dispatch, 22 | getState: GetState, 23 | bag: Bag 24 | ) => any | Promise<any>, 25 | navKey?: string, 26 | confirmLeave?: ConfirmLeave, 27 | meta?: Object 28 | } 29 | 30 | export type Route = RouteString | RouteObject 31 | 32 | export type RoutesMap = { 33 | [key: string]: Route 34 | } 35 | 36 | export type Router = { 37 | getStateForActionOriginal: (action: Object, state: ?Object) => ?Object, 38 | getStateForAction: (action: Object, state: ?Object) => ?Object, 39 | getPathAndParamsForState: ( 40 | state: Object 41 | ) => { path: ?string, params: ?Object }, 42 | getActionForPathAndParams: (path: string) => ?Object 43 | } 44 | 45 | export type Navigator = { 46 | router: Router 47 | } 48 | 49 | export type Navigators = { 50 | [key: string]: Navigator 51 | } 52 | 53 | export type Routes = Array<Route> // eslint-disable-line no-undef 54 | 55 | export type SelectLocationState = (state: Object) => LocationState 56 | export type SelectTitleState = (state: Object) => string 57 | 58 | export type QuerySerializer = { 59 | stringify: (params: Object) => string, 60 | parse: (queryString: string) => Object 61 | } 62 | 63 | export type DisplayConfirmLeave = ( 64 | message: string, 65 | callback: (canLeave: boolean) => void 66 | ) => void 67 | 68 | export type Options = { // eslint-disable-line no-undef 69 | title?: string | SelectTitleState, 70 | location?: string | SelectLocationState, 71 | notFoundPath?: string, 72 | scrollTop?: boolean, 73 | onBeforeChange?: (dispatch: Dispatch, getState: GetState, bag: Bag) => void, 74 | onAfterChange?: (dispatch: Dispatch, getState: GetState, bag: Bag) => void, 75 | onBackNext?: (dispatch: Dispatch, getState: GetState, bag: Bag) => void, 76 | restoreScroll?: History => ScrollBehavior, 77 | initialDispatch?: boolean, 78 | querySerializer?: QuerySerializer, 79 | displayConfirmLeave?: DisplayConfirmLeave, 80 | basename?: string, 81 | initialEntries?: string | Array<string>, 82 | createHistory?: (options?: Object) => History, 83 | navigators?: { 84 | navigators: Navigators, 85 | patchNavigators: (navigators: Navigators) => void, 86 | actionToNavigation: ( 87 | navigators: Navigators, 88 | action: Object, 89 | navigationAction: ?NavigationAction, 90 | route: ?Route 91 | ) => Object, 92 | navigationToAction: ( 93 | navigators: Navigators, 94 | store: Store, 95 | routesMap: RoutesMap, 96 | action: Object 97 | ) => { 98 | action: Object, 99 | navigationAction: ?NavigationAction 100 | } 101 | }, 102 | strict?: boolean, 103 | extra?: any 104 | } 105 | 106 | export type ScrollBehavior = Object 107 | 108 | export type Params = Object // eslint-disable-line no-undef 109 | export type Payload = Object 110 | 111 | export type LocationState = { 112 | pathname: string, 113 | type: string, 114 | payload: Payload, 115 | query?: Object, 116 | search?: string, 117 | prev: Location, 118 | kind: ?string, 119 | history: ?HistoryData, 120 | routesMap: RoutesMap, 121 | hasSSR?: true 122 | } 123 | 124 | export type Location = { 125 | pathname: string, 126 | type: string, 127 | payload: Payload, 128 | query?: Object, 129 | search?: string 130 | } 131 | 132 | export type ActionMetaLocation = { 133 | current: Location, 134 | prev: Location, 135 | kind: ?string, 136 | history: ?HistoryData 137 | } 138 | 139 | export type NavigationAction = { 140 | type: string, 141 | key?: ?string, 142 | navKey?: ?string, 143 | routeName?: string, 144 | actions?: Array<NavigationAction>, 145 | action?: NavigationAction, 146 | params?: Object, 147 | meta?: Object 148 | } 149 | 150 | export type Meta = { 151 | location: ActionMetaLocation, 152 | notFoundPath?: string, 153 | navigation?: NavigationAction, 154 | query?: Object, 155 | search?: string 156 | } 157 | 158 | export type HistoryData = { 159 | entries: Array<{ pathname: string }>, 160 | index: number, 161 | length: number 162 | } 163 | 164 | export type Action = { 165 | type: string, 166 | payload: Payload, 167 | meta: Meta, 168 | query?: Object, 169 | navKey?: ?string, 170 | error?: ?mixed 171 | } 172 | 173 | export type ReceivedAction = { 174 | type: string, 175 | payload: Payload, 176 | meta?: Object, 177 | query?: Object, 178 | search?: string, 179 | navKey?: ?string 180 | } 181 | 182 | export type Listener = (HistoryLocation, HistoryAction) => void 183 | export type Listen = Listener => void 184 | export type Push = (pathname: string) => void 185 | export type Replace = (pathname: string) => void 186 | export type GoBack = () => void 187 | export type GoForward = () => void 188 | export type Go = number => void 189 | export type CanGo = number => boolean 190 | export type BlockFunction = ( 191 | location: HistoryLocation, 192 | action: string 193 | ) => ?string 194 | 195 | export type History = { 196 | listen: Listen, 197 | push: Push, 198 | replace: Replace, 199 | goBack: GoBack, 200 | goForward: GoForward, 201 | go: Go, 202 | canGo: CanGo, 203 | entries: Array<{ pathname: string }>, 204 | index: number, 205 | length: number, 206 | location: HistoryLocation, 207 | block: (func: BlockFunction) => void 208 | } 209 | 210 | export type HistoryLocation = { 211 | pathname: string, 212 | search?: string 213 | } 214 | 215 | export type HistoryAction = string 216 | 217 | export type Document = Object // eslint-disable-line no-undef 218 | 219 | export type Store = ReduxStore<*, *> 220 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { 3 | default as connectRoutes, 4 | push, 5 | replace, 6 | back, 7 | next, 8 | go, 9 | canGo, 10 | canGoBack, 11 | canGoForward, 12 | prevPath, 13 | nextPath, 14 | history, 15 | scrollBehavior, 16 | updateScroll, 17 | selectLocationState, 18 | getOptions 19 | } from './connectRoutes' 20 | 21 | export const NOT_FOUND = '@@redux-first-router/NOT_FOUND' 22 | export const ADD_ROUTES = '@@redux-first-router/ADD_ROUTES' 23 | 24 | export { default as redirect } from './action-creators/redirect' 25 | 26 | export { default as actionToPath } from './pure-utils/actionToPath' 27 | export { default as pathToAction } from './pure-utils/pathToAction' 28 | export { default as isLocationAction } from './pure-utils/isLocationAction' 29 | export { default as setKind } from './pure-utils/setKind' 30 | export { default as addRoutes } from './action-creators/addRoutes' 31 | 32 | export type { 33 | RouteString, 34 | RouteObject, 35 | Route, 36 | RoutesMap, 37 | Options, 38 | Params, 39 | Payload, 40 | LocationState, 41 | Location, 42 | Meta, 43 | Action, 44 | ReceivedAction, 45 | Listener, 46 | Listen, 47 | Push, 48 | GoBack, 49 | History, 50 | HistoryLocation, 51 | Document, 52 | Navigators, 53 | Navigator, 54 | Store, 55 | NavigationAction, 56 | Router 57 | } from './flow-types' 58 | -------------------------------------------------------------------------------- /src/pure-utils/actionToPath.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { compileParamsToPath } from 'rudy-match-path' 3 | import type { 4 | Route, 5 | Payload, 6 | Params, 7 | RoutesMap, 8 | ReceivedAction as Action, 9 | QuerySerializer 10 | } from '../flow-types' 11 | 12 | export default ( 13 | action: Action, 14 | routesMap: RoutesMap, 15 | serializer?: QuerySerializer 16 | ): string => { 17 | const route = routesMap[action.type] 18 | const routePath = typeof route === 'object' ? route.path : route 19 | const params = _payloadToParams(route, action.payload) 20 | const path = compileParamsToPath(routePath, params) || '/' 21 | 22 | const query = 23 | action.query || 24 | (action.meta && action.meta.query) || 25 | (action.payload && action.payload.query) 26 | 27 | const search = query && serializer && serializer.stringify(query) 28 | 29 | return search ? `${path}?${search}` : path 30 | } 31 | 32 | const _payloadToParams = (route: Route, params: Payload = {}): Params => 33 | Object.keys(params).reduce((sluggifedParams, key) => { 34 | const segment = params[key] 35 | // $FlowFixMe 36 | sluggifedParams[key] = transformSegment(segment, route, key) 37 | return sluggifedParams 38 | }, {}) 39 | 40 | const transformSegment = (segment: string, route: Route, key: string) => { 41 | if (typeof route.toPath === 'function') { 42 | return route.toPath(segment, key) 43 | } 44 | else if (typeof segment === 'string') { 45 | // Ask James "should it return arrays?" 46 | if (segment.includes('/')) { 47 | return segment.split('/') 48 | } 49 | 50 | if (route.capitalizedWords === true) { 51 | return segment.replace(/ /g, '-').toLowerCase() 52 | } 53 | 54 | return segment 55 | } 56 | else if (typeof segment === 'number') { 57 | return segment 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/pure-utils/attemptCallRouteThunk.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { updateScroll } from '../connectRoutes' 3 | import type { 4 | Dispatch, 5 | GetState, 6 | RouteObject, 7 | LocationState, 8 | SelectLocationState, 9 | Bag 10 | } from '../flow-types' 11 | 12 | export default ( 13 | dispatch: Dispatch, 14 | getState: GetState, 15 | route: RouteObject, 16 | selectLocationState: SelectLocationState, 17 | bag: Bag 18 | ) => { 19 | if (typeof window !== 'undefined') { 20 | const thunk = route.thunk 21 | 22 | if (typeof thunk === 'function') { 23 | const { kind, hasSSR }: LocationState = selectLocationState(getState()) 24 | 25 | // call thunks always if it's not initial load of the app or only if it's load 26 | // without SSR setup yet, so app state is setup on client when prototyping, 27 | // such as with with webpack-dev-server before server infrastructure is built. 28 | // NEW: if there is no path, it's assumed to be a pathless route, which is always called. 29 | if (kind !== 'load' || (kind === 'load' && !hasSSR) || !route.path) { 30 | const prom = thunk(dispatch, getState, bag) 31 | 32 | if (prom && typeof prom.next === 'function') { 33 | prom.next(updateScroll) 34 | } 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/pure-utils/canUseDom.js: -------------------------------------------------------------------------------- 1 | export default typeof window !== 'undefined' && 2 | window.document && 3 | window.document.createElement 4 | -------------------------------------------------------------------------------- /src/pure-utils/changePageTitle.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Document } from '../flow-types' 3 | 4 | export default (doc: Document, title: ?string): ?string => { 5 | if (typeof title === 'string' && doc.title !== title) { 6 | return (doc.title = title) 7 | } 8 | 9 | return null 10 | } 11 | 12 | export const getDocument = (): Document => { 13 | const isSSRTest = process.env.NODE_ENV === 'test' && typeof window !== 'undefined' && window.isSSR 14 | 15 | return typeof document !== 'undefined' && !isSSRTest ? document : {} 16 | } 17 | -------------------------------------------------------------------------------- /src/pure-utils/confirmLeave.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import pathToAction from './pathToAction' 4 | 5 | import type { 6 | Action, 7 | Location, 8 | History, 9 | HistoryLocation, 10 | Store, 11 | ConfirmLeave, 12 | SelectLocationState, 13 | QuerySerializer, 14 | DisplayConfirmLeave 15 | } from '../flow-types' 16 | 17 | let _unblock 18 | let _removeConfirmBlocking 19 | let _displayConfirmLeave 20 | 21 | export const clearBlocking = () => { 22 | _unblock && _unblock() 23 | _removeConfirmBlocking && _removeConfirmBlocking() 24 | } 25 | 26 | // This is the default `displayConfirmLeave` handler. 27 | // It receives the message to display and a callback to call when complete. 28 | // Pass `true` to the callback to proceed with leaving the current route. 29 | 30 | const defaultDisplayConfirmLeave = (message, callback) => { 31 | const hasConfirm = typeof window !== 'undefined' && window.confirm 32 | 33 | if (!hasConfirm) { 34 | throw new Error('[rudy] environment requires `displayConfirmLeave` option') 35 | } 36 | 37 | const canLeave = window.confirm(message) 38 | 39 | callback(canLeave) 40 | } 41 | 42 | // createConfirm is called whenever you enter a route that has a `confirmLeave` 43 | // option. It tells the history package to block via `history.block`, but 44 | // to determine to do so based on our redux state-centric `confirm` handler. 45 | // This handler is also returned for use in the middleware to block when 46 | // leaving the current route via actions (i.e. as opposed to browser buttons) 47 | 48 | export const createConfirm = ( 49 | confirmLeave: ConfirmLeave, 50 | store: Store, 51 | selectLocationState: SelectLocationState, 52 | history: History, 53 | querySerializer?: QuerySerializer, 54 | removeConfirmBlocking: Function 55 | ) => { 56 | const confirm = (location: HistoryLocation | Location) => { 57 | const state = store.getState() 58 | const routesMap = selectLocationState(state).routesMap 59 | const pathname = [location.pathname, location.search].filter(Boolean).join('?') 60 | const action = pathToAction(pathname, routesMap, querySerializer) 61 | const response = confirmLeave(state, action) 62 | 63 | // we use the confirmLeave function manually in onBeforeChange, so we must 64 | // manually clear blocking that history.block would otherwise handle, plus 65 | // we remove additional onBeforeChange blocking via _removeConfirmBlocking 66 | if (!response) clearBlocking() 67 | return response 68 | } 69 | 70 | _unblock = history.block(confirm) 71 | _removeConfirmBlocking = removeConfirmBlocking 72 | 73 | return confirm 74 | } 75 | 76 | // confirmUI here is triggered only by onBeforeChange: 77 | 78 | export const confirmUI = (message: string, store: Store, action: Action) => { 79 | const cb = canLeave => { 80 | if (canLeave) { 81 | clearBlocking() 82 | store.dispatch(action) 83 | } 84 | } 85 | 86 | _displayConfirmLeave(message, cb) 87 | } 88 | 89 | export const getUserConfirmation = (message: string, cb: boolean => void) => { 90 | _displayConfirmLeave(message, canLeave => { 91 | if (canLeave) clearBlocking() 92 | cb(canLeave) 93 | }) 94 | } 95 | 96 | export const setDisplayConfirmLeave = ( 97 | displayConfirmLeave?: DisplayConfirmLeave 98 | ) => { 99 | _displayConfirmLeave = displayConfirmLeave || defaultDisplayConfirmLeave 100 | } 101 | -------------------------------------------------------------------------------- /src/pure-utils/createThunk.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Store } from 'redux' 3 | import type { RoutesMap, SelectLocationState, Bag } from '../flow-types' 4 | 5 | export default ( 6 | routesMap: RoutesMap, 7 | selectLocationState: SelectLocationState, 8 | bag: Bag 9 | ) => ({ dispatch, getState }: Store<*, *>): Promise<*> => { 10 | const { type } = selectLocationState(getState()) 11 | const route = routesMap[type] 12 | 13 | if (route && typeof route.thunk === 'function') { 14 | return Promise.resolve(route.thunk(dispatch, getState, bag)) 15 | } 16 | 17 | return Promise.resolve() 18 | } 19 | -------------------------------------------------------------------------------- /src/pure-utils/isLocationAction.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Action, ReceivedAction } from '../flow-types' 3 | 4 | export default (action: Object): boolean => 5 | !!(action.meta && action.meta.location && action.meta.location.current) 6 | -------------------------------------------------------------------------------- /src/pure-utils/isReactNative.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export default (): boolean => 4 | typeof window !== 'undefined' && 5 | typeof window.navigator !== 'undefined' && 6 | window.navigator.product === 'ReactNative' 7 | -------------------------------------------------------------------------------- /src/pure-utils/isRedirectAction.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Action } from '../flow-types' 3 | 4 | export default (action: Action): boolean => 5 | !!(action && 6 | action.meta && 7 | action.meta.location && 8 | action.meta.location.kind === 'redirect') 9 | -------------------------------------------------------------------------------- /src/pure-utils/isServer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export default (): boolean => typeof window === 'undefined' || !!window.SSRtest 4 | -------------------------------------------------------------------------------- /src/pure-utils/nestAction.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Action, Location, ReceivedAction, History } from '../flow-types' 3 | 4 | export default ( 5 | pathname: string, 6 | action: ReceivedAction, 7 | prev: Location, 8 | history: History, // not used currently, but be brought back 9 | kind: ?string 10 | ): Action => { 11 | const { type, payload = {}, meta = {} } = action 12 | const query = action.query || meta.query || payload.query 13 | const parts = pathname.split('?') 14 | const search = parts[1] 15 | 16 | return { 17 | ...action, 18 | ...(action.query && { query }), 19 | type, 20 | payload, 21 | meta: { 22 | ...meta, 23 | ...(meta.query && { query }), 24 | location: { 25 | current: { 26 | pathname: parts[0], 27 | type, 28 | payload, 29 | ...(query && { query, search }) 30 | }, 31 | prev, 32 | kind, 33 | history: undefined 34 | } 35 | } 36 | } 37 | } 38 | 39 | export const nestHistory = (history: History) => 40 | (history.entries 41 | ? { 42 | index: history.index, 43 | length: history.entries.length, 44 | entries: history.entries.slice(0) // history.entries.map(entry => entry.pathname) 45 | } 46 | : undefined) 47 | -------------------------------------------------------------------------------- /src/pure-utils/objectValues.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { RoutesMap, Routes } from '../flow-types' 3 | 4 | export default (routes: RoutesMap): Routes => 5 | Object.keys(routes).map(key => routes[key]) 6 | -------------------------------------------------------------------------------- /src/pure-utils/pathToAction.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { compilePath } from 'rudy-match-path' 3 | import { stripBasename } from 'rudy-history/PathUtils' 4 | import { NOT_FOUND, getOptions } from '../index' 5 | import objectValues from './objectValues' 6 | 7 | import type { RoutesMap, ReceivedAction, QuerySerializer } from '../flow-types' 8 | 9 | export default ( 10 | pathname: string, 11 | routesMap: RoutesMap, 12 | serializer?: QuerySerializer, 13 | basename?: string | void = getOptions().basename, 14 | strict?: boolean | void = getOptions().strict 15 | ): ReceivedAction => { 16 | const parts = pathname.split('?') 17 | const search = parts[1] 18 | const query = search && serializer && serializer.parse(search) 19 | const routes = objectValues(routesMap) 20 | const routeTypes = Object.keys(routesMap) 21 | 22 | pathname = basename ? stripBasename(parts[0], basename) : parts[0] 23 | 24 | let i = 0 25 | let match 26 | let keys 27 | 28 | while (!match && i < routes.length) { 29 | const regPath = typeof routes[i] === 'string' ? routes[i] : routes[i].path // route may be an object containing a route or a route string itself 30 | 31 | if (!regPath) { 32 | i++ 33 | continue 34 | } 35 | 36 | const { re, keys: k } = compilePath(regPath, { strict }) 37 | match = re.exec(pathname) 38 | keys = k 39 | i++ 40 | } 41 | 42 | if (match) { 43 | i-- 44 | 45 | const capitalizedWords = 46 | typeof routes[i] === 'object' && routes[i].capitalizedWords 47 | 48 | 49 | const coerceNumbers = 50 | typeof routes[i] === 'object' && routes[i].coerceNumbers 51 | 52 | const fromPath = 53 | routes[i] && 54 | typeof routes[i].fromPath === 'function' && 55 | routes[i].fromPath 56 | 57 | const userMeta = typeof routes[i] === 'object' && routes[i].meta 58 | 59 | const type = routeTypes[i] 60 | 61 | const payload = (keys || []).reduce((payload, key, index) => { 62 | let val = match && match[index + 1] // item at index 0 is the overall match, whereas those after correspond to the key's index 63 | 64 | if (typeof val === 'string') { 65 | if (fromPath) { 66 | val = fromPath && fromPath(val, key.name) 67 | } 68 | else if (coerceNumbers && isNumber(val)) { 69 | val = parseFloat(val) 70 | } 71 | else if (capitalizedWords) { 72 | val = val.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()) // 'my-category' -> 'My Category' 73 | } 74 | } 75 | 76 | payload[key.name] = val 77 | return payload 78 | }, {}) 79 | 80 | const meta = { 81 | ...(userMeta ? { meta: userMeta } : {}), 82 | ...(query ? { query } : {}) 83 | } 84 | return { type, payload, meta } 85 | } 86 | 87 | // This will basically will only end up being called if the developer is manually calling history.push(). 88 | // Or, if visitors visit an invalid URL, the developer can use the NOT_FOUND type to show a not-found page to 89 | const meta = { notFoundPath: pathname, ...(query ? { query } : {}) } 90 | return { type: NOT_FOUND, payload: {}, meta } 91 | } 92 | 93 | const isNumber = (val: string) => /^\d+$/.test(val) 94 | -------------------------------------------------------------------------------- /src/pure-utils/pathnamePlusSearch.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | type Location = { 3 | pathname: string, 4 | search?: string 5 | } 6 | 7 | export default ({ pathname, search }: Location) => { 8 | if (search) { 9 | if (search.indexOf('?') !== 0) { 10 | search = `?${search}` 11 | } 12 | 13 | return `${pathname}${search}` 14 | } 15 | 16 | return pathname 17 | } 18 | -------------------------------------------------------------------------------- /src/pure-utils/setKind.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Action } from '../flow-types' 3 | 4 | export default (action: Action, kind: string) => { 5 | action.meta = action.meta || {} 6 | action.meta.location = action.meta.location || {} 7 | action.meta.location.kind = kind 8 | 9 | return action 10 | } 11 | -------------------------------------------------------------------------------- /src/reducer/createLocationReducer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { NOT_FOUND, ADD_ROUTES } from '../index' 3 | import isServer from '../pure-utils/isServer' 4 | import { nestHistory } from '../pure-utils/nestAction' 5 | import type { 6 | LocationState, 7 | RoutesMap, 8 | Action, 9 | Payload, 10 | History 11 | } from '../flow-types' 12 | 13 | export default (initialState: LocationState, routesMap: RoutesMap) => ( 14 | state: LocationState = initialState, 15 | action: Action 16 | ): LocationState => { 17 | routesMap = state.routesMap || routesMap 18 | const route = routesMap[action.type] 19 | 20 | if ( 21 | action.type === NOT_FOUND || 22 | (route && 23 | !action.error && 24 | (typeof route === 'string' || route.path) && 25 | (action.meta.location.current.pathname !== state.pathname || 26 | action.meta.location.current.search !== state.search || 27 | action.meta.location.kind === 'load')) 28 | ) { 29 | const query = action.meta.location.current.query 30 | const search = action.meta.location.current.search 31 | 32 | return { 33 | pathname: action.meta.location.current.pathname, 34 | type: action.type, 35 | payload: { ...action.payload }, 36 | ...(query && { query, search }), 37 | prev: action.meta.location.prev, 38 | kind: action.meta.location.kind, 39 | history: action.meta.location.history, 40 | hasSSR: state.hasSSR, 41 | routesMap 42 | } 43 | } 44 | else if ( 45 | route && 46 | !action.error && 47 | (typeof route === 'string' || route.path) && 48 | (action.meta.location.current.pathname === state.pathname && 49 | action.meta.location.current.search === state.search && 50 | action.meta.location.kind !== state.kind 51 | ) 52 | ) { 53 | return { 54 | ...state, 55 | kind: action.meta.location.kind 56 | } 57 | } 58 | else if (action.type === ADD_ROUTES) { 59 | return { 60 | ...state, 61 | routesMap: { ...state.routesMap, ...action.payload.routes } 62 | } 63 | } 64 | 65 | return state 66 | } 67 | 68 | export const getInitialState = ( 69 | currentPathname: string, 70 | meta: ?{ search?: string, query?: Object }, 71 | type: string, 72 | payload: Payload, 73 | routesMap: RoutesMap, 74 | history: History 75 | ): LocationState => ({ 76 | search: currentPathname.split('?')[1], 77 | pathname: currentPathname.split('?')[0], 78 | type, 79 | payload, 80 | ...meta, 81 | prev: { 82 | pathname: '', 83 | type: '', 84 | payload: {} 85 | }, 86 | kind: undefined, 87 | history: nestHistory(history), 88 | hasSSR: isServer() ? true : undefined, // client uses initial server `hasSSR` state setup here 89 | routesMap 90 | }) 91 | -------------------------------------------------------------------------------- /troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | ### location reducer and window.location 4 | 5 | If you called your routing handler reducer `location`, it is possible to confound with the global javascript variable of the browser `window.location`. 6 | Indeed, `window.location` or `location` is a callable global variable in the browser. 7 | When writing function that includes destructuring with the location reducer, it is possible to write: 8 | 9 | ```javascript 10 | const f = ({location: type}) => console.log(location); 11 | ``` 12 | 13 | This code will not raise an error, because it will log the `window.location` variable. 14 | So be careful when using destructuring with `location`. It can lead to manipulate unwanted object. 15 | -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | module.exports = wallaby => { 2 | process.env.NODE_ENV = 'test' 3 | 4 | return { 5 | files: [ 6 | { pattern: 'src/**/*.js', load: false }, 7 | { pattern: 'package.json', load: false }, 8 | { pattern: '__tests__/**/*.snap', load: false }, 9 | { pattern: '__test-helpers__/**/*.js', load: false } 10 | ], 11 | 12 | filesWithNoCoverageCalculated: ['__test-helpers__/**/*.js'], 13 | 14 | tests: ['__tests__/**/*.js'], 15 | 16 | env: { 17 | type: 'node', 18 | runner: 'node' 19 | }, 20 | 21 | testFramework: 'jest', 22 | compilers: { 23 | '**/*.js': wallaby.compilers.babel({ babelrc: true }) 24 | }, 25 | setup(wallaby) { 26 | const conf = require('./package.json').jest 27 | wallaby.testFramework.configure(conf) 28 | }, 29 | // runAllTestsInAffectedTestFile: true, 30 | // runAllTestsInAffectedTestGroup: true, 31 | debug: false 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin') 3 | 4 | const env = process.env.NODE_ENV 5 | 6 | const config = { 7 | mode: 'development', 8 | module: { 9 | rules: [{ test: /\.js$/, use: ['babel-loader'], exclude: /node_modules/ }] 10 | }, 11 | output: { 12 | library: 'ReduxFirstRouter', 13 | libraryTarget: 'umd' 14 | }, 15 | plugins: [ 16 | new webpack.optimize.OccurrenceOrderPlugin(), 17 | new webpack.DefinePlugin({ 18 | 'process.env.NODE_ENV': JSON.stringify(env) 19 | }) 20 | ] 21 | } 22 | 23 | if (env === 'production') { 24 | config.mode = 'production' 25 | config.optimization = { 26 | minimizer: [ 27 | new UglifyJSPlugin({ 28 | uglifyOptions: { 29 | output: { 30 | comments: false, 31 | ascii_only: true 32 | }, 33 | compress: { 34 | comparisons: false 35 | } 36 | } 37 | }) 38 | ] 39 | } 40 | } 41 | 42 | module.exports = config 43 | --------------------------------------------------------------------------------