├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── .travis.yml ├── .vscode ├── launch.json └── settings.json ├── HACKING.md ├── LICENSE ├── README.md ├── babel.config.js ├── config ├── .prettierignore ├── babel-jest.js └── tsconfig.json ├── docs └── development │ ├── commit-messages.md │ └── publishing.md ├── examples └── react │ ├── README.md │ └── basic │ ├── .env │ ├── .eslintrc.js │ └── src │ ├── components.js │ ├── configureStore.js │ ├── index.js │ ├── pageReducer.js │ └── routes.js ├── jest.config.js ├── lerna.json ├── lint-staged.config.js ├── package.json ├── packages ├── boilerplate │ ├── .gitignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── babel.config.js │ ├── package.json │ ├── public │ │ └── favicon.ico │ ├── screenshot.png │ ├── server │ │ ├── serveDev.js │ │ ├── serveProd.js │ │ └── webpack.config.babel.js │ └── src │ │ ├── components │ │ ├── App.js │ │ ├── ArticlePromotion.js │ │ ├── Home.js │ │ ├── List.js │ │ ├── Sidebar.js │ │ └── Switcher.js │ │ ├── configureStore.browser.js │ │ ├── configureStore.server.js │ │ ├── css │ │ ├── App.css │ │ ├── Home.css │ │ ├── List.css │ │ ├── Sidebar.css │ │ └── Switcher.css │ │ ├── reducers │ │ ├── category.js │ │ ├── index.js │ │ ├── packages.js │ │ ├── page.js │ │ └── title.js │ │ ├── render.browser.js │ │ ├── render.server.js │ │ └── routes.js ├── integration-tests │ ├── .testsConfig.json │ ├── CHANGELOG.md │ ├── __helpers__ │ │ ├── awaitUrlChange.js │ │ ├── createTest.js │ │ └── fakeAsyncWork.js │ ├── __test-helpers__ │ │ ├── createLink.js │ │ ├── createSequence.js │ │ ├── fakeAsyncWork.js │ │ ├── reducerParameters.js │ │ ├── rudySetup.js │ │ ├── setupJest.js │ │ ├── setupThunk.js │ │ └── tempMock.js │ ├── __tests__ │ │ ├── Link │ │ │ ├── Link.js │ │ │ ├── NavLink.js │ │ │ └── __snapshots__ │ │ │ │ ├── Link.js.snap │ │ │ │ └── NavLink.js.snap │ │ └── integration │ │ │ ├── SPA.js │ │ │ ├── __snapshots__ │ │ │ ├── SPA.js.snap │ │ │ ├── anonymousThunk.js.snap │ │ │ ├── arrayCallback.js.snap │ │ │ ├── async.js.snap │ │ │ ├── autoDispatch.js.snap │ │ │ ├── autoDispatchFalse.js.snap │ │ │ ├── callStartTrue.js.snap │ │ │ ├── cancelPendingRequest.js.snap │ │ │ ├── complexRedirects.js.snap │ │ │ ├── createAction.js.snap │ │ │ ├── createRequest.js.snap │ │ │ ├── dispatchFrom.js.snap │ │ │ ├── dontDoubleDispatch.js.snap │ │ │ ├── entryState.js.snap │ │ │ ├── firstRoute.js.snap │ │ │ ├── formatRoutes.js.snap │ │ │ ├── hashes.js.snap │ │ │ ├── hydrate.js.snap │ │ │ ├── inheritedCallbacks.js.snap │ │ │ ├── middlewareAsFunction.js.snap │ │ │ ├── multipleRedirects.js.snap │ │ │ ├── onError.js.snap │ │ │ ├── optionsCallbacks.js.snap │ │ │ ├── overrideOptions.js.snap │ │ │ ├── params.js.snap │ │ │ ├── pathlessRoute.js.snap │ │ │ ├── queries.js.snap │ │ │ ├── redirectShortcut.js.snap │ │ │ ├── redirects.js.snap │ │ │ ├── returnFalse.js.snap │ │ │ ├── routeLevelMiddleware.js.snap │ │ │ ├── serverRedirect.js.snap │ │ │ ├── thunkCaching.js.snap │ │ │ └── uninheritedHistory.js.snap │ │ │ ├── actions │ │ │ ├── __snapshots__ │ │ │ │ ├── addRoutes.js.snap │ │ │ │ ├── changeBasename.js.snap │ │ │ │ ├── history.js.snap │ │ │ │ ├── notFound.js.snap │ │ │ │ └── redirect.js.snap │ │ │ ├── addRoutes.js │ │ │ ├── changeBasename.js │ │ │ ├── history.js │ │ │ ├── notFound.js │ │ │ └── redirect.js │ │ │ ├── anonymousThunk.js │ │ │ ├── arrayCallback.js │ │ │ ├── async.js │ │ │ ├── autoDispatch.js │ │ │ ├── autoDispatchFalse.js │ │ │ ├── browser │ │ │ ├── .eslintrc.js │ │ │ ├── actionsInCallbacks │ │ │ │ ├── __snapshots__ │ │ │ │ │ ├── jump.js.snap │ │ │ │ │ ├── jump2N.js.snap │ │ │ │ │ ├── push.js.snap │ │ │ │ │ ├── regularAction.js.snap │ │ │ │ │ ├── replace.js.snap │ │ │ │ │ ├── reset.js.snap │ │ │ │ │ ├── resetOnLoad.js.snap │ │ │ │ │ └── set.js.snap │ │ │ │ ├── jump.js │ │ │ │ ├── jump2N.js │ │ │ │ ├── push.js │ │ │ │ ├── regularAction.js │ │ │ │ ├── replace.js │ │ │ │ ├── reset.js │ │ │ │ ├── resetOnLoad.js │ │ │ │ └── set.js │ │ │ ├── history │ │ │ │ ├── __snapshots__ │ │ │ │ │ ├── jump.js.snap │ │ │ │ │ ├── jump2N.js.snap │ │ │ │ │ ├── push.js.snap │ │ │ │ │ ├── replace.js.snap │ │ │ │ │ ├── reset.js.snap │ │ │ │ │ ├── resetIndex.js.snap │ │ │ │ │ ├── resetIndexN.js.snap │ │ │ │ │ ├── set.js.snap │ │ │ │ │ └── setN.js.snap │ │ │ │ ├── jump.js │ │ │ │ ├── jump2N.js │ │ │ │ ├── push.js │ │ │ │ ├── replace.js │ │ │ │ ├── reset.js │ │ │ │ ├── resetIndex.js │ │ │ │ ├── resetIndexN.js │ │ │ │ ├── set.js │ │ │ │ └── setN.js │ │ │ ├── pop │ │ │ │ ├── __snapshots__ │ │ │ │ │ ├── actionCancelsPop.js.snap │ │ │ │ │ ├── doublePop.js.snap │ │ │ │ │ ├── oldAlgorithm.js.snap │ │ │ │ │ ├── pop.js.snap │ │ │ │ │ ├── popReturnFalse.js.snap │ │ │ │ │ ├── redirect.js.snap │ │ │ │ │ ├── redirectToCurrent.js.snap │ │ │ │ │ ├── redirectToPrev.js.snap │ │ │ │ │ └── triplePopBothDirections.js.snap │ │ │ │ ├── actionCancelsPop.js │ │ │ │ ├── doublePop.js │ │ │ │ ├── oldAlgorithm.js │ │ │ │ ├── pop.js │ │ │ │ ├── popCancelsAction.js │ │ │ │ ├── popReturnFalse.js │ │ │ │ ├── redirect.js │ │ │ │ ├── redirectToCurrent.js │ │ │ │ ├── redirectToPrev.js │ │ │ │ └── triplePopBothDirections.js │ │ │ └── sessionStorage │ │ │ │ ├── __snapshots__ │ │ │ │ ├── restoreFromBack.js.snap │ │ │ │ ├── restoreFromFront.js.snap │ │ │ │ └── restoreFromMiddle.js.snap │ │ │ │ ├── restoreFromBack.js │ │ │ │ ├── restoreFromFront.js │ │ │ │ └── restoreFromMiddle.js │ │ │ ├── callRoute.js │ │ │ ├── callStartTrue.js │ │ │ ├── cancelPendingRequest.js │ │ │ ├── complexRedirects.js │ │ │ ├── createAction.js │ │ │ ├── createRequest.js │ │ │ ├── createScene │ │ │ ├── __snapshots__ │ │ │ │ ├── actionCreators.js.snap │ │ │ │ ├── options.js.snap │ │ │ │ └── returnedUtilities.js.snap │ │ │ ├── actionCreators.js │ │ │ ├── options.js │ │ │ └── returnedUtilities.js │ │ │ ├── dispatchFrom.js │ │ │ ├── dontDoubleDispatch.js │ │ │ ├── entryState.js │ │ │ ├── firstRoute.js │ │ │ ├── formatRoutes.js │ │ │ ├── hashes.js │ │ │ ├── hydrate.js │ │ │ ├── inheritedCallbacks.js │ │ │ ├── middlewareAsFunction.js │ │ │ ├── multipleRedirects.js │ │ │ ├── onError.js │ │ │ ├── optionsCallbacks.js │ │ │ ├── overrideOptions.js │ │ │ ├── params.js │ │ │ ├── pathlessRoute.js │ │ │ ├── queries.js │ │ │ ├── redirectShortcut.js │ │ │ ├── redirects.js │ │ │ ├── returnFalse.js │ │ │ ├── routeLevelMiddleware.js │ │ │ ├── serverRedirect.js │ │ │ ├── thunkCaching.js │ │ │ └── uninheritedHistory.js │ ├── jest.config.js │ ├── package.json │ └── wallaby.js ├── middleware-change-page-title │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ └── index.js │ └── tests │ │ └── index.test.js ├── react │ ├── CHANGELOG.md │ ├── package.json │ └── src │ │ ├── index.js │ │ ├── link.jsx │ │ ├── provider.jsx │ │ └── utils │ │ ├── handlePress.js │ │ ├── index.js │ │ ├── preventDefault.js │ │ └── toUrlAndAction.js ├── rudy │ ├── .codeclimate.yml │ ├── .flowconfig │ ├── .gitignore │ ├── .npmignore │ ├── CHANGELOG.md │ ├── docs-old │ │ ├── _config.yml │ │ ├── action.md │ │ ├── client-only-api.md │ │ ├── connectRoutes.md │ │ ├── low-level-api.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 │ ├── docs │ │ ├── differences-from-rfr.md │ │ └── route.md │ ├── 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 │ │ │ ├── jest_v18.x.x.js │ │ │ ├── react-redux_v4.x.x.js │ │ │ └── redux_v3.x.x.js │ ├── package.json │ └── src │ │ ├── actions │ │ ├── addRoutes.js │ │ ├── changeBasename.js │ │ ├── clearCache.js │ │ ├── confirm.js │ │ ├── history.js │ │ ├── index.js │ │ ├── notFound.js │ │ └── redirect.js │ │ ├── core │ │ ├── compose.js │ │ ├── createHistory.js │ │ ├── createReducer.js │ │ ├── createRequest.js │ │ ├── createRouter.js │ │ └── index.js │ │ ├── createScene │ │ ├── index.js │ │ └── utils │ │ │ ├── camelCase.js │ │ │ ├── formatRoute.js │ │ │ ├── handleError.js │ │ │ ├── index.js │ │ │ ├── logExports.js │ │ │ └── makeActionCreator.js │ │ ├── flow-types.js │ │ ├── history │ │ ├── BrowserHistory.js │ │ ├── History.js │ │ ├── MemoryHistory.js │ │ └── utils │ │ │ ├── index.js │ │ │ ├── popListener.js │ │ │ ├── sessionStorage.js │ │ │ └── supports.js │ │ ├── index.js │ │ ├── middleware │ │ ├── anonymousThunk.js │ │ ├── call │ │ │ ├── index.js │ │ │ └── utils │ │ │ │ ├── autoDispatch.js │ │ │ │ ├── createCache.js │ │ │ │ ├── enhanceRoutes.js │ │ │ │ ├── index.js │ │ │ │ └── shouldCall.js │ │ ├── enter.js │ │ ├── index.js │ │ ├── pathlessRoute.js │ │ ├── restoreScroll.js │ │ ├── saveScroll.js │ │ ├── serverRedirect.js │ │ └── transformAction │ │ │ ├── index.js │ │ │ └── utils │ │ │ ├── formatAction.js │ │ │ ├── index.js │ │ │ └── replacePopAction.js │ │ ├── pathlessRoutes │ │ ├── addRoutes.js │ │ ├── callHistory.js │ │ ├── changeBasename.js │ │ ├── clearCache.js │ │ ├── confirm.js │ │ └── index.js │ │ ├── types.js │ │ └── utils │ │ ├── actionToUrl.js │ │ ├── actionToUrl.test.js │ │ ├── applyDefaults.js │ │ ├── callRoute.js │ │ ├── cleanBasename.js │ │ ├── compileUrl.js │ │ ├── doesRedirect.js │ │ ├── formatRoutes.js │ │ ├── index.js │ │ ├── isAction.js │ │ ├── isHydrate.js │ │ ├── isNotFound.js │ │ ├── isRedirect.js │ │ ├── locationToUrl.js │ │ ├── logError.js │ │ ├── matchUrl.js │ │ ├── nestAction.js │ │ ├── noOp.js │ │ ├── parseSearch.js │ │ ├── redirectShortcut.js │ │ ├── shouldTransition.js │ │ ├── toAction.js │ │ ├── toEntries.js │ │ ├── typeToScene.js │ │ ├── urlToAction.js │ │ ├── urlToAction.test.js │ │ └── urlToLocation.js ├── scroll-restorer │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── types │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json └── utils │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ ├── createSelector.ts │ ├── index.ts │ ├── isServer.ts │ └── supportsSessionStorage.ts │ ├── tests │ ├── createSelector.test.ts │ ├── isServer.test.ts │ └── tsconfig.json │ └── tsconfig.json ├── scripts └── git-release.sh └── yarn.lock /.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 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | dist 3 | .idea 4 | *.mobi 5 | 6 | .eslintcache 7 | 8 | **/node_modules/ 9 | **/.DS_Store 10 | 11 | packages/*/cjs/ 12 | packages/*/es/ 13 | packages/*/ts/ 14 | packages/*/tsconfig.tsbuildinfo 15 | packages/*/tests/ts/ 16 | packages/*/tests/tsconfig.tsbuildinfo 17 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignored everywhere 2 | **/node_modules/ 3 | 4 | **/.editorconfig 5 | **/.eslintignore 6 | **/.prettierignore 7 | **/.gitignore 8 | **/.flowconfig 9 | **/.npmignore 10 | **/.eslintcache 11 | 12 | # Ignored packages outside sub packages 13 | LICENSE 14 | yarn.lock 15 | yarn-error.log 16 | lerna-debug.log 17 | 18 | # Ignored extensions 19 | *.png 20 | *.snap 21 | *.log 22 | *.env 23 | *.sh 24 | *.tsbuildinfo 25 | 26 | # Ignored dirs in sub packages 27 | packages/*/ 28 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | singleQuote: true, 4 | trailingComma: 'all', 5 | arrowParens: 'always', 6 | proseWrap: 'always', 7 | } 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | cache: yarn 5 | script: 6 | - yarn run check 7 | notifications: 8 | email: false 9 | webhooks: 10 | urls: 11 | - https://webhooks.gitter.im/e/5156be73e058008e1ed2 12 | on_success: always # options: [always|never|change] default: always 13 | on_failure: always # options: [always|never|change] default: always 14 | on_start: never # options: [always|never|change] default: always 15 | branches: 16 | except: 17 | - /^v\d+\.\d+\.\d+$/ 18 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Boilerplate", 6 | "type": "node", 7 | "request": "launch", 8 | "cwd": "${workspaceFolder}/examples/boilerplate", 9 | "runtimeExecutable": "node", 10 | "runtimeArgs": ["buildServer/serveDev.js"], 11 | "port": 9229, 12 | "env": { 13 | "NODE_OPTIONS": "--inspect", 14 | "NODE_ENV": "development" 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.git": true, 4 | "**/.svn": true, 5 | "**/.hg": true, 6 | "**/CVS": true, 7 | "**/.DS_Store": true, 8 | "**/node_modules/**/*": true, 9 | "examples/boilerplate/buildClient/**/*": true, 10 | "examples/boilerplate/buildServer/**/*": true 11 | }, 12 | "eslint.autoFixOnSave": true, 13 | "eslint.validate": [ 14 | { "language": "javascript", "autoFix": true }, 15 | { "language": "javascriptreact", "autoFix": true }, 16 | { "language": "typescript", "autoFix": true }, 17 | { "language": "typescriptreact", "autoFix": true } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 respond-framework 4 | Copyright (c) 2017 James Gillmore 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const supportsStaticEsm = (caller) => 2 | Boolean(caller && caller.supportsStaticESM) 3 | 4 | module.exports = (api) => { 5 | const envEs = api.env('es') 6 | const test = api.env('test') 7 | const esModules = api.caller(supportsStaticEsm) || envEs 8 | return { 9 | overrides: [ 10 | { 11 | test: /\.tsx?$/, 12 | presets: ['@babel/preset-typescript'], 13 | }, 14 | { 15 | test: /\.jsx?$/, 16 | presets: ['@babel/preset-flow'], 17 | }, 18 | ], 19 | presets: [ 20 | [ 21 | '@babel/preset-env', 22 | { 23 | modules: esModules ? false : undefined, 24 | targets: 'maintained node versions, last 1 version, > 1%, not dead', 25 | }, 26 | ], 27 | '@babel/preset-react', 28 | ].filter(Boolean), 29 | plugins: [ 30 | '@babel/plugin-proposal-object-rest-spread', 31 | '@babel/plugin-proposal-class-properties', 32 | test && '@babel/plugin-transform-runtime', 33 | ].filter(Boolean), 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /config/.prettierignore: -------------------------------------------------------------------------------- 1 | # Used for helper packages 2 | 3 | **/.editorconfig 4 | **/.eslintignore 5 | **/.gitignore 6 | **/.flowconfig 7 | **/.npmignore 8 | **/LICENSE 9 | 10 | # Ignored extensions 11 | 12 | yarn-error.log 13 | lerna-debug.log 14 | *.png 15 | *.snap 16 | *.log 17 | *.ico 18 | *.sh 19 | *.tsbuildinfo 20 | 21 | # Ignored dirs in sub packages 22 | cjs/ 23 | es/ 24 | dist/ 25 | buildClient/ 26 | buildServer/ 27 | flow-typed/ 28 | ts/ 29 | CHANGELOG.md 30 | -------------------------------------------------------------------------------- /config/babel-jest.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const babelJest = require('babel-jest') 3 | 4 | module.exports = babelJest.createTransformer({ 5 | configFile: path.resolve(__dirname, '../babel.config.js'), 6 | }) 7 | -------------------------------------------------------------------------------- /config/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "ES6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, 5 | "module": "ES2015", 6 | "jsx": "preserve" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, 7 | "emitDeclarationOnly": true, 8 | "declarationMap": true, 9 | "listEmittedFiles": true, 10 | "declaration": true, 11 | 12 | /* Strict Type-Checking Options */ 13 | "strict": true /* Enable all strict type-checking options. */, 14 | 15 | /* Additional Checks */ 16 | "noUnusedLocals": true /* Report errors on unused locals. */, 17 | "noUnusedParameters": true /* Report errors on unused parameters. */, 18 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 19 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 20 | 21 | /* Module Resolution Options */ 22 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 23 | "baseUrl": "..", 24 | 25 | "composite": true, 26 | "paths": { 27 | "@respond-framework/*": ["*/src"] 28 | } 29 | }, 30 | "references": [ 31 | { "path": "../packages/scroll-restorer" }, 32 | { "path": "../packages/types" }, 33 | { "path": "../packages/utils" }, 34 | { "path": "../packages/utils/tests" } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /docs/development/commit-messages.md: -------------------------------------------------------------------------------- 1 | # Commit messages 2 | 3 | We rely on commits in the master branch to follow the 4 | [conventional commits](https://www.conventionalcommits.org/en/v1.0.0-beta.2/#specification) 5 | specification. These are used for changelog and version number generation. 6 | 7 | If commits in a feature branch don't follow the convention, then its also 8 | possible to generate a squashed merge commit when merging a pull request that 9 | does follow it. 10 | -------------------------------------------------------------------------------- /docs/development/publishing.md: -------------------------------------------------------------------------------- 1 | # Publishing new versions 2 | 3 | 1. Set up a personal access token in github with push access to the repository. 4 | Add the token to the environment with `$ export GH_TOKEN=`. This is to 5 | annotate the tags with github releases. 6 | 1. Authenticate with the NPM CLI (verify with `$ npm whoami`) 7 | 1. Temporarily disable branch protection on the repository (the lerna release 8 | script needs to push to master) 9 | 1. Do the release with lerna: 10 | `$ yarn run lerna publish [prerelease] [--preid ]`. This is the point 11 | of no return, if it succeeds in creating the releases on NPM. 12 | 1. Turn branch protection back on 13 | 14 | There will be a prompt to confirm the new versions of packages to be published. 15 | Changelogs will automatically be generated and new version numbers chosen based 16 | on the [commit history](./commit-messages.md). See the 17 | [lerna docs](https://github.com/lerna/lerna/tree/master/commands/publish#readme) 18 | for more details such as how to use pre releases, dist tags, etc. 19 | -------------------------------------------------------------------------------- /examples/react/README.md: -------------------------------------------------------------------------------- 1 | ## React examples 2 | 3 | ```bash 4 | create-react-app demo 5 | rm demo/src -rf 6 | cp -r basic/{.env,src} demo 7 | cd demo 8 | yarn add redux react-redux @respond-framework/rudy 9 | # git init # needed to apply patches from other examples. 10 | # install any additional libs used by applied patches. 11 | yarn start 12 | ``` 13 | -------------------------------------------------------------------------------- /examples/react/basic/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true 2 | -------------------------------------------------------------------------------- /examples/react/basic/.eslintrc.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | extends: [path.resolve(__dirname, '../../../.eslintrc.js')], 5 | env: { 6 | browser: true, 7 | }, 8 | rules: { 9 | 'no-param-reassign': 0, 10 | 'import/no-unresolved': 0, 11 | 'import/extensions': 0, 12 | 'react/jsx-filename-extension': 0, 13 | 'react/prop-types': ['error', { skipUndeclared: true }], 14 | 'import/no-extraneous-dependencies': 0, // There is no package.json 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /examples/react/basic/src/components.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | // Home component 5 | const Home = ({ visitUser }) => { 6 | const rndUserId = Math.floor(20 * Math.random()) 7 | return ( 8 |
9 |

Welcome home!

10 | 13 |
14 | ) 15 | } 16 | 17 | const ConnectedHome = connect( 18 | null, 19 | (dispatch) => ({ 20 | visitUser: (userId) => dispatch({ type: 'USER', params: { id: userId } }), 21 | }), 22 | )(Home) 23 | 24 | // User component 25 | const User = ({ goHome, userId }) => ( 26 |
27 |

{`User component: user ${userId}`}

28 | 31 |
32 | ) 33 | 34 | const ConnectedUser = connect( 35 | ({ location: { params } }) => ({ userId: params.id }), 36 | (dispatch) => ({ goHome: () => dispatch({ type: 'HOME' }) }), 37 | )(User) 38 | 39 | // 404 component 40 | const NotFound = ({ pathname }) => ( 41 |
42 |

404

43 | Page not found: {pathname} 44 |
45 | ) 46 | const ConnectedNotFound = connect(({ location: { pathname } }) => ({ 47 | pathname, 48 | }))(NotFound) 49 | 50 | export { 51 | ConnectedHome as Home, 52 | ConnectedUser as User, 53 | ConnectedNotFound as NotFound, 54 | } 55 | -------------------------------------------------------------------------------- /examples/react/basic/src/configureStore.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, combineReducers, compose, createStore } from 'redux' 2 | import { createRouter } from '@respond-framework/rudy' 3 | 4 | import routes from './routes' 5 | import page from './pageReducer' 6 | 7 | export default (preloadedState, initialEntries) => { 8 | const options = { initialEntries } 9 | const { reducer, middleware, firstRoute } = createRouter(routes, options) 10 | 11 | const rootReducer = combineReducers({ page, location: reducer }) 12 | const middlewares = applyMiddleware(middleware) 13 | const enhancers = compose(middlewares) 14 | 15 | const store = createStore(rootReducer, preloadedState, enhancers) 16 | 17 | return { store, firstRoute } 18 | } 19 | -------------------------------------------------------------------------------- /examples/react/basic/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect, Provider } from 'react-redux' 3 | import ReactDOM from 'react-dom' 4 | 5 | import configureStore from './configureStore' 6 | import * as components from './components' 7 | 8 | // App component 9 | const App = ({ page }) => { 10 | const Component = components[page] 11 | return 12 | } 13 | const ConnectedApp = connect(({ page }) => ({ page }))(App) 14 | 15 | // Redux setup 16 | const { store, firstRoute } = configureStore() 17 | 18 | function render() { 19 | ReactDOM.render( 20 | 21 | 22 | , 23 | document.getElementById('root'), 24 | ) 25 | } 26 | 27 | store.dispatch(firstRoute()).then(() => render()) 28 | -------------------------------------------------------------------------------- /examples/react/basic/src/pageReducer.js: -------------------------------------------------------------------------------- 1 | const components = { 2 | HOME: 'Home', 3 | USER: 'User', 4 | NOT_FOUND: 'NotFound', 5 | } 6 | 7 | export default (state = 'Home', action = {}) => components[action.type] || state 8 | -------------------------------------------------------------------------------- /examples/react/basic/src/routes.js: -------------------------------------------------------------------------------- 1 | export default { 2 | HOME: '/', 3 | USER: '/user/:id', 4 | } 5 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | transform: { 5 | '^.+\\.(j|t)sx?$': path.relative( 6 | process.cwd(), 7 | path.resolve(__dirname, './config/babel-jest'), 8 | ), 9 | }, 10 | testEnvironment: 'node', 11 | } 12 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "independent", 3 | "npmClient": "yarn", 4 | "useWorkspaces": true, 5 | "command": { 6 | "version": { 7 | "allowBranch": ["master"], 8 | "conventionalCommits": true, 9 | "githubRelease": true 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | const micromatch = require('micromatch') 2 | 3 | const eslintMatcher = '*.{js,jsx,ts,tsx}' 4 | 5 | module.exports = { 6 | '*': (files) => 7 | files.reduce( 8 | (commands, file) => [ 9 | ...commands, 10 | micromatch.isMatch(file, eslintMatcher) 11 | ? `eslint --cache --report-unused-disable-directives --max-warnings 0 --fix '${file}'` 12 | : `prettier --write '${file}'`, 13 | `git add '${file}'`, 14 | ], 15 | [], 16 | ), 17 | '**/*.ts?(x)': () => 'tsc -b config/tsconfig.json', 18 | } 19 | -------------------------------------------------------------------------------- /packages/boilerplate/.gitignore: -------------------------------------------------------------------------------- 1 | buildClient/ 2 | buildServer/ 3 | -------------------------------------------------------------------------------- /packages/boilerplate/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. -------------------------------------------------------------------------------- /packages/boilerplate/README.md: -------------------------------------------------------------------------------- 1 | 2 | Edit Redux-First Router Demo 3 | 4 | 5 | # Simple Universal Boilerplate of [Redux-First Router](https://github.com/faceyspacey/redux-first-router) 6 | 7 | ![redux-first-router-demo screenshot](./screenshot.png) 8 | 9 | > For a demo/boilerplate that goes even farther make sure to check out the 10 | > **["DEMO"](https://github.com/faceyspacey/redux-first-router-demo)**. A lot 11 | > more features and use-cases are covered there, but this _boilerplate_ is the 12 | > best place to start to learn the basics of RFR, especially if you're new to 13 | > any of these things: SSR, Code Splitting, Express, APIs, Webpack and Redux in 14 | > general. 15 | 16 | ## Installation 17 | 18 | ``` 19 | git clone https://github.com/faceyspacey/redux-first-router-boilerplate 20 | cd redux-first-router-boilerplate 21 | yarn 22 | yarn start 23 | ``` 24 | 25 | ## Files You Should Look At: 26 | 27 | _client code:_ 28 | 29 | - [**_src/configureStore.js_**](./src/configureStore.js) 30 | - [**_src/routesMap.js_**](./src/routesMap.js) - **_(the primary work of RFR)_** 31 | - [**_src/components/Switcher.js_**](./src/components/Switcher.js) - _(universal 32 | component concept)_ 33 | - [**_src/components/Sidebar.js_**](./src/components/Sidebar.js) - _(look at the 34 | different ways to link + dispatch URL-aware actions)_ 35 | 36 | _server code:_ 37 | 38 | - [**_server/index.js_**](./server/index.js) 39 | - [**_server/render.js_**](./server/render.js) - \*(super simple thanks to 40 | [webpack-flush-chunks](https://github.com/faceyspacey/webpack-flush-chunks) 41 | from our **_"Universal"_** product line)\* 42 | - [**_server/configureStore.js_**](./server/configureStore.js) - **_(observe how 43 | the matched route's thunk is awaited on)_** 44 | -------------------------------------------------------------------------------- /packages/boilerplate/babel.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = (api) => { 4 | const webpack = api.env('webpack') 5 | return { 6 | extends: path.resolve(__dirname, '../../babel.config.js'), 7 | plugins: [ 8 | webpack && 'react-hot-loader/babel', 9 | webpack && '@babel/plugin-syntax-dynamic-import', 10 | webpack && 'babel-plugin-universal-import', 11 | ].filter(Boolean), 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/boilerplate/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@respond-framework/boilerplate", 3 | "description": "Universal Rudy Demo", 4 | "version": "0.1.1-test.9", 5 | "main": "server/index.js", 6 | "author": "James Gillmore ", 7 | "license": "MIT", 8 | "private": true, 9 | "scripts": { 10 | "start": "npm run clean && cross-env NODE_ENV=development babel-watch server/serveDev.js", 11 | "start:prod": "npm run build && npm run serve", 12 | "serve": "node buildServer/serveProd.js", 13 | "build": "npm run build:node && npm run build:client && npm run build:server", 14 | "build:client": "webpack --config=buildServer/webpack.config.babel -p --progress --env.server=false", 15 | "build:server": "webpack --config=buildServer/webpack.config.babel -p --progress --env.server=true", 16 | "build:node": "babel --root-mode upward server/ -d buildServer/", 17 | "clean": "rimraf buildClient buildServer", 18 | "prettier": "prettier", 19 | "is-pretty": "prettier --ignore-path=../../config/.prettierignore '**/*' --list-different", 20 | "prettify": "prettier --ignore-path=../../config/.prettierignore '**/*' --write" 21 | }, 22 | "dependencies": { 23 | "@respond-framework/react": "^0.1.1-test.5", 24 | "@respond-framework/rudy": "^0.1.1-test.9", 25 | "core-js": "^3.2.1", 26 | "express": "^4.15.2", 27 | "react": "^16.8.0", 28 | "react-dom": "^16.8.0", 29 | "react-hot-loader": "^4.8.8", 30 | "react-redux": "^7.1.0", 31 | "react-universal-component": "^3.0.3", 32 | "redux": "^4.0.4", 33 | "redux-devtools-extension": "^2.13.5", 34 | "regenerator-runtime": "^0.13.3", 35 | "serve-favicon": "^2.4.5", 36 | "source-map-support": "^0.5.6", 37 | "webpack-flush-chunks": "^2.0.3" 38 | }, 39 | "devDependencies": { 40 | "babel-plugin-universal-import": "^3.0.3", 41 | "extract-css-chunks-webpack-plugin": "^3.1.3" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/boilerplate/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/respond-framework/rudy/177d8f354981c8ab8fbc890c5828cd3a5d8a5162/packages/boilerplate/public/favicon.ico -------------------------------------------------------------------------------- /packages/boilerplate/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/respond-framework/rudy/177d8f354981c8ab8fbc890c5828cd3a5d8a5162/packages/boilerplate/screenshot.png -------------------------------------------------------------------------------- /packages/boilerplate/server/serveDev.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | 3 | import 'source-map-support/register' 4 | import 'core-js/stable' 5 | import 'regenerator-runtime/runtime' 6 | import path from 'path' 7 | import express from 'express' 8 | import favicon from 'serve-favicon' 9 | import webpack from 'webpack' 10 | import webpackDevMiddleware from 'webpack-dev-middleware' 11 | import webpackHotMiddleware from 'webpack-hot-middleware' 12 | import webpackHotServerMiddleware from 'webpack-hot-server-middleware' 13 | import makeConfig from './webpack.config.babel' 14 | 15 | const clientConfig = makeConfig({ server: false }) 16 | const serverConfig = makeConfig({ server: true }) 17 | 18 | const { publicPath, outputPath } = clientConfig.output 19 | 20 | const app = express() 21 | 22 | app.use(favicon(path.resolve(__dirname, '../public', 'favicon.ico'))) 23 | 24 | // UNIVERSAL HMR + STATS HANDLING GOODNESS: 25 | 26 | const multiCompiler = webpack([clientConfig, serverConfig]) 27 | const clientCompiler = multiCompiler.compilers[0] 28 | 29 | app.use( 30 | webpackDevMiddleware(multiCompiler, { 31 | publicPath, 32 | serverSideRender: true, 33 | }), 34 | ) 35 | app.use(webpackHotMiddleware(clientCompiler)) 36 | 37 | // keeps serverRender updated with arg: { clientStats, outputPath } 38 | app.use( 39 | webpackHotServerMiddleware(multiCompiler, { 40 | serverRendererOptions: { outputPath }, 41 | chunkName: 'h', 42 | }), 43 | ) 44 | 45 | app.listen(3000, () => { 46 | // eslint-disable-next-line no-console 47 | console.log('Listening @ http://localhost:3000/') 48 | }) 49 | -------------------------------------------------------------------------------- /packages/boilerplate/server/serveProd.js: -------------------------------------------------------------------------------- 1 | import 'source-map-support/register' 2 | import 'core-js/stable' 3 | import 'regenerator-runtime/runtime' 4 | import { resolve } from 'path' 5 | import express from 'express' 6 | import favicon from 'serve-favicon' 7 | import clientStats from '../buildClient/stats.json' 8 | import serverRender from '../buildServer/h' 9 | import makeConfig from './webpack.config.babel' 10 | 11 | // ASSUMPTION: the compiled version of this file is one directory under the boilerplate root 12 | // (Otherwise importing '../buildXxxx' wouldn't work) 13 | 14 | const res = (...args) => resolve(__dirname, ...args) 15 | 16 | const { path: outputPath, publicPath } = makeConfig({ server: false }).output 17 | 18 | const app = express() 19 | 20 | app.use(favicon(res('../public', 'favicon.ico'))) 21 | 22 | // UNIVERSAL HMR + STATS HANDLING GOODNESS: 23 | 24 | app.use(publicPath, express.static(outputPath)) 25 | app.use(serverRender({ clientStats, outputPath })) 26 | 27 | app.listen(3000, () => { 28 | // eslint-disable-next-line no-console 29 | console.log('Listening @ http://localhost:3000/') 30 | }) 31 | -------------------------------------------------------------------------------- /packages/boilerplate/src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { hot } from 'react-hot-loader' 3 | 4 | import Sidebar from './Sidebar' 5 | import Switcher from './Switcher' 6 | 7 | import styles from '../css/App' 8 | 9 | const App = () => ( 10 |
11 | 12 | 13 |
14 | ) 15 | 16 | export default hot(module)(App) 17 | -------------------------------------------------------------------------------- /packages/boilerplate/src/components/ArticlePromotion.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { hot } from 'react-hot-loader' 3 | import styles from '../css/App' 4 | 5 | export default hot(module)(({ title, text, url }) => ( 6 |
7 |
{title}
8 | 9 | 15 | {text} 16 | 17 |
18 | )) 19 | -------------------------------------------------------------------------------- /packages/boilerplate/src/components/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { hot } from 'react-hot-loader' 3 | 4 | import ArticlePromotion from './ArticlePromotion' 5 | 6 | import styles from '../css/Home' 7 | 8 | const Home = () => ( 9 |
10 |

HOME

11 | 12 |
13 | logo 18 | 19 | RFR will become Rudy 20 | 21 | 26 |
27 | 28 | 34 | *One of our first users, Nicolas Delfino, designed the logo, check him 35 | out: @nico__delfino 36 | 37 |
38 | ) 39 | 40 | export default hot(module)(Home) 41 | -------------------------------------------------------------------------------- /packages/boilerplate/src/components/List.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { hot } from 'react-hot-loader' 4 | 5 | import ArticlePromotion from './ArticlePromotion' 6 | 7 | import styles from '../css/List' 8 | 9 | const List = ({ category, packages }) => ( 10 |
11 |
12 | Category: 13 | {category} 14 |
15 | 16 |
17 |
    18 | {packages.map((pkg) => ( 19 |
  • {pkg}
  • 20 | ))} 21 |
22 | 23 | {category === 'redux' ? ( 24 | 29 | ) : ( 30 | 35 | )} 36 |
37 |
38 | ) 39 | 40 | const mapStateToProps = (state) => ({ 41 | category: state.category, 42 | packages: state.packages, 43 | }) 44 | 45 | export default hot(module)(connect(mapStateToProps)(List)) 46 | -------------------------------------------------------------------------------- /packages/boilerplate/src/components/Sidebar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { NavLink, Link } from '@respond-framework/react' 4 | import styles from '../css/Sidebar' 5 | 6 | // TODO: Use the link package 7 | /* eslint-disable jsx-a11y/click-events-have-key-events */ 8 | 9 | // TODO: fix NavLink when object is passed via the "to" prop 10 | const Sidebar = ({ path, dispatch }) => ( 11 |
12 |

SEO-FRIENDLY LINKS

13 | 14 | 15 | Home 16 | 17 | 18 | 23 | Redux 24 | 25 | dispatch({ type: 'LIST', params: { category: 'react' } })} 29 | > 30 | React 31 | 32 | 33 | dispatch({ type: 'NOT_FOUND' })} 37 | > 38 | NOT_FOUND 39 | 40 | 41 |
42 | 43 |

EVENT HANDLERS

44 | 45 | dispatch({ type: 'HOME' })} 50 | > 51 | Home 52 | 53 | 54 | dispatch({ type: 'LIST', params: { category: 'redux' } })} 59 | > 60 | Redux 61 | 62 | 63 | dispatch({ type: 'LIST', params: { category: 'react' } })} 68 | > 69 | React 70 | 71 |
72 | ) 73 | 74 | const isActive = (actualPath, expectedPath) => 75 | actualPath === expectedPath ? styles.active : '' 76 | 77 | const mapStateToProps = (state) => ({ 78 | path: state.location.pathname, 79 | }) 80 | 81 | export default connect(mapStateToProps)(Sidebar) 82 | -------------------------------------------------------------------------------- /packages/boilerplate/src/components/Switcher.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import universal from 'react-universal-component' 4 | 5 | import styles from '../css/Switcher' 6 | 7 | const UniversalComponent = universal(({ page }) => import(`./${page}`), { 8 | minDelay: 500, 9 | 10 | loading: () => ( 11 |
12 |
13 |
14 | ), 15 | 16 | error: () =>
PAGE NOT FOUND - 404
, 17 | }) 18 | 19 | const Switcher = ({ page }) => ( 20 |
21 | 22 |
23 | ) 24 | 25 | const mapStateToProps = (state) => ({ 26 | page: state.page, 27 | }) 28 | 29 | export default connect(mapStateToProps)(Switcher) 30 | -------------------------------------------------------------------------------- /packages/boilerplate/src/configureStore.browser.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | import { createStore, applyMiddleware, compose, combineReducers } from 'redux' 4 | import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction' 5 | import { 6 | push, 7 | replace, 8 | jump, 9 | back, 10 | next, 11 | reset, 12 | set, 13 | setParams, 14 | setQuery, 15 | setState, 16 | setHash, 17 | setBasename, 18 | createRouter, 19 | } from '@respond-framework/rudy' 20 | 21 | import routes from './routes' 22 | import * as reducers from './reducers' 23 | 24 | export default (preloadedState, initialEntries) => { 25 | const options = { initialEntries, basenames: ['/foo', '/bar'] } 26 | const { reducer, middleware, firstRoute, api } = createRouter(routes, options) 27 | const { history, ctx } = api 28 | 29 | const rootReducer = combineReducers({ ...reducers, location: reducer }) 30 | const middlewares = applyMiddleware(middleware) 31 | const enhancers = composeEnhancers(middlewares) 32 | const store = createStore(rootReducer, preloadedState, enhancers) 33 | 34 | if (module.hot) { 35 | module.hot.accept('./reducers/index', () => { 36 | const newRootReducer = combineReducers({ ...reducers, location: reducer }) 37 | store.replaceReducer(newRootReducer) 38 | }) 39 | } 40 | 41 | if (typeof window !== 'undefined') { 42 | window.routes = routes 43 | window.store = store 44 | window.hist = history 45 | window.actions = actionCreators 46 | window.ctx = ctx 47 | } 48 | 49 | return { store, firstRoute, api } 50 | } 51 | 52 | const composeEnhancers = (...args) => 53 | typeof window !== 'undefined' 54 | ? composeWithDevTools({ actionCreators })(...args) 55 | : compose(...args) 56 | 57 | const actionCreators = { 58 | push, 59 | replace, 60 | jump, 61 | back, 62 | next, 63 | reset, 64 | set, 65 | setParams, 66 | setQuery, 67 | setState, 68 | setHash, 69 | setBasename, 70 | } 71 | -------------------------------------------------------------------------------- /packages/boilerplate/src/configureStore.server.js: -------------------------------------------------------------------------------- 1 | import { doesRedirect } from '@respond-framework/rudy' 2 | import configureStore from './configureStore.browser' 3 | 4 | export default async (req, res) => { 5 | const { store, firstRoute, api } = configureStore(undefined, req.url) 6 | const result = await store.dispatch(firstRoute()) 7 | if (doesRedirect(result, res)) return false 8 | 9 | const { status } = store.getState().location 10 | res.status(status) 11 | 12 | return { store, api } 13 | } 14 | -------------------------------------------------------------------------------- /packages/boilerplate/src/css/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | background: rgb(27, 32, 34); 4 | } 5 | 6 | .app { 7 | display: -webkit-box; 8 | display: -ms-flexbox; 9 | display: flex; 10 | -webkit-box-orient: horizontal; 11 | -webkit-box-direction: normal; 12 | -ms-flex-direction: row; 13 | flex-direction: row; 14 | padding: 10px; 15 | } 16 | 17 | .more { 18 | margin-top: 50px; 19 | color: red; 20 | font-size: 022px; 21 | -webkit-font-smoothing: antialiased; 22 | text-align: center; 23 | } 24 | 25 | .link { 26 | color: rgb(255, 44, 145); 27 | font-size: 26px; 28 | margin-top: 10px; 29 | text-shadow: 1px 1px 1px rgb(20, 30, 20); 30 | text-decoration: none; 31 | background: rgba(65, 50, 50, 0.9); 32 | padding: 16px; 33 | -webkit-font-smoothing: antialiased; 34 | display: block; 35 | } 36 | .link:hover { 37 | color: white; 38 | background: rgb(255, 44, 145); 39 | } 40 | -------------------------------------------------------------------------------- /packages/boilerplate/src/css/Home.css: -------------------------------------------------------------------------------- 1 | .home { 2 | background: #141414; 3 | border: 1px solid rgb(30, 30, 30); 4 | box-sizing: border-box; 5 | padding: 10px; 6 | display: block !important; 7 | } 8 | 9 | .title { 10 | color: rgb(255, 44, 49); 11 | font-size: 34px; 12 | text-align: center; 13 | } 14 | 15 | .content { 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | width: 100%; 20 | flex-direction: column; 21 | } 22 | 23 | .caption { 24 | color: rgb(150, 44, 49); 25 | font-size: 18px; 26 | -webkit-font-smoothing: antialiased; 27 | margin-top: 10px; 28 | text-shadow: 1px 1px 1px #2a0c0c; 29 | } 30 | 31 | .nico { 32 | position: absolute; 33 | bottom: 10px; 34 | left: 10px; 35 | color: pink; 36 | text-decoration: none; 37 | -webkit-font-smoothing: antialiased; 38 | } 39 | .nico:hover { 40 | text-decoration: underline; 41 | } 42 | -------------------------------------------------------------------------------- /packages/boilerplate/src/css/List.css: -------------------------------------------------------------------------------- 1 | .list { 2 | background: #141414; 3 | border: 1px solid rgb(30, 30, 30); 4 | box-sizing: border-box; 5 | display: block !important; 6 | padding: 10px; 7 | } 8 | 9 | .title { 10 | color: rgb(255, 44, 49); 11 | font-size: 34px; 12 | text-align: center; 13 | } 14 | 15 | .content { 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | width: 100%; 20 | flex-direction: column; 21 | } 22 | -------------------------------------------------------------------------------- /packages/boilerplate/src/css/Sidebar.css: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | width: 150px; 3 | margin-left: 5px; 4 | z-index: 2; 5 | } 6 | h2 { 7 | font-size: 11px; 8 | color: #646464; 9 | font-weight: normal; 10 | margin-top: 0px; 11 | text-shadow: 1px 1px 1px #2d2d2d; 12 | -webkit-font-smoothing: antialiased; 13 | } 14 | .sidebar a, 15 | .sidebar span { 16 | display: block; 17 | color: white; 18 | background: rgb(35, 40, 42); 19 | margin: 6px 0; 20 | padding: 10px; 21 | font-size: 0.875rem; 22 | line-height: 8px; 23 | text-decoration: none; 24 | text-align: center; 25 | cursor: pointer; 26 | font-family: sans-serif; 27 | -webkit-font-smoothing: antialiased; 28 | transition: 0.3s ease all; 29 | } 30 | 31 | .sidebar .active { 32 | background-image: linear-gradient( 33 | 270deg, 34 | #fed29d, 35 | #a58b66, 36 | rgb(255, 44, 145), 37 | rgb(200, 43, 48) 38 | ); 39 | background-size: 720%; 40 | box-shadow: 0 3px 3px rgba(0, 0, 0, 0.5); 41 | transition: 0s ease all; 42 | } 43 | .sidebar a:hover, 44 | .sidebar span:hover { 45 | background: rgb(80, 43, 48); 46 | } 47 | .sidebar .active a:hover, 48 | .sidebar .span span:hover { 49 | background: rgb(200, 43, 88); 50 | } 51 | 52 | .sidebar .active:hover { 53 | background: rgb(255, 30, 98); 54 | } 55 | -------------------------------------------------------------------------------- /packages/boilerplate/src/reducers/category.js: -------------------------------------------------------------------------------- 1 | export default (state = '', action = {}) => 2 | action.type === 'LIST' ? action.params.category : state 3 | -------------------------------------------------------------------------------- /packages/boilerplate/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | export { default as page } from './page' 2 | export { default as category } from './category' 3 | export { default as packages } from './packages' 4 | export { default as title } from './title' 5 | -------------------------------------------------------------------------------- /packages/boilerplate/src/reducers/packages.js: -------------------------------------------------------------------------------- 1 | export default (state = [], action = {}) => { 2 | switch (action.type) { 3 | case 'LIST_COMPLETE': 4 | return action.payload.packages 5 | default: 6 | return state 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/boilerplate/src/reducers/page.js: -------------------------------------------------------------------------------- 1 | export default (state = 'HOME', action = {}) => components[action.type] || state 2 | 3 | const components = { 4 | HOME: 'Home', 5 | LIST: 'List', 6 | NOT_FOUND: 'NotFound', 7 | } 8 | 9 | // NOTES: this is the primary reducer demonstrating how RFR replaces the need 10 | // for React Router's component. 11 | // 12 | // ALSO: Forget a switch, use a hash table for perf. 13 | -------------------------------------------------------------------------------- /packages/boilerplate/src/reducers/title.js: -------------------------------------------------------------------------------- 1 | export default (state = 'RFR Demo', action = {}) => { 2 | switch (action.type) { 3 | case 'HOME': 4 | return 'RFR Boilerplate' 5 | case 'LIST': 6 | return `RFR: ${capitalize(action.params.category)}` 7 | default: 8 | return state 9 | } 10 | } 11 | 12 | const capitalize = (str) => 13 | str.replace(/-/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase()) 14 | 15 | // RFR automatically changes the document.title for you :) 16 | -------------------------------------------------------------------------------- /packages/boilerplate/src/render.browser.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | import React from 'react' 4 | import ReactDOM from 'react-dom' 5 | import { Provider } from 'react-redux' 6 | import { RudyProvider } from '@respond-framework/react' 7 | import App from './components/App' 8 | import configureStore from './configureStore' 9 | 10 | const { store, firstRoute, api } = configureStore(window.REDUX_STATE) 11 | 12 | const root = document.getElementById('root') 13 | 14 | const render = () => 15 | ReactDOM.hydrate( 16 | 17 | 18 | 19 | 20 | , 21 | root, 22 | ) 23 | 24 | store.dispatch(firstRoute()).then(() => { 25 | render() 26 | }) 27 | 28 | if (module.hot) { 29 | module.hot.accept('./components/App', render) 30 | } 31 | -------------------------------------------------------------------------------- /packages/boilerplate/src/render.server.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/server' 3 | import { Provider } from 'react-redux' 4 | import { flushChunkNames } from 'react-universal-component/server' 5 | import flushChunks from 'webpack-flush-chunks' 6 | import { RudyProvider } from '@respond-framework/react' 7 | 8 | import configureStore from './configureStore' 9 | import App from './components/App' 10 | 11 | export default ({ clientStats }) => async (req, res, next) => { 12 | console.log('REQUESTED PATH:', req.url) // eslint-disable-line no-console 13 | try { 14 | const html = await renderToString(clientStats, req, res) 15 | return res.send(html) 16 | } catch (error) { 17 | return next(error) 18 | } 19 | } 20 | 21 | const renderToString = async (clientStats, req, res) => { 22 | console.log('REQUESTED PATH:', req.url) // eslint-disable-line no-console 23 | const { store, api } = await configureStore(req, res) 24 | if (!store) return '' // no store means redirect was already served 25 | 26 | const app = createApp(App, store, api) 27 | const appString = ReactDOM.renderToString(app) 28 | const state = store.getState() 29 | const stateJson = JSON.stringify(state) 30 | const chunkNames = flushChunkNames() 31 | const { js, styles, cssHash } = flushChunks(clientStats, { chunkNames }) 32 | 33 | console.log('CHUNK NAMES RENDERED', chunkNames) // eslint-disable-line no-console 34 | 35 | return ` 36 | 37 | 38 | 39 | ${state.title} 40 | ${styles} 41 | 42 | 43 | 44 |
${appString}
45 | ${cssHash} 46 | 47 | ${js} 48 | 49 | ` 50 | } 51 | 52 | const createApp = (Root, store, api) => ( 53 | 54 | 55 | 56 | 57 | 58 | ) 59 | -------------------------------------------------------------------------------- /packages/boilerplate/src/routes.js: -------------------------------------------------------------------------------- 1 | export default { 2 | HOME: { 3 | path: '/', 4 | onEnter: () => { 5 | // eslint-disable-next-line no-console,no-undef 6 | console.log(document.querySelector('.Home__content--319uD')) 7 | }, 8 | beforeEnter: async (req) => { 9 | // eslint-disable-next-line no-undef 10 | if (typeof window !== 'undefined' && window.foo) { 11 | await new Promise((res) => setTimeout(res, 3000)) 12 | } 13 | 14 | // eslint-disable-next-line no-undef 15 | if (typeof window !== 'undefined' && window.foo) { 16 | await req.dispatch({ type: 'LIST', params: { category: 'react' } }) 17 | } 18 | }, 19 | // beforeLeave: async ({ type }) => { 20 | // return false 21 | // await new Promise(res => setTimeout(res, 10000)) 22 | // return type === 'NOT_FOUND' 23 | // } 24 | }, 25 | // eslint-disable-next-line no-console 26 | PATHLESS: () => console.log('PATHLESS'), 27 | LIST: { 28 | path: '/list/:category', 29 | thunk: async ({ params }) => { 30 | const { category } = params 31 | const packages = await fetch(`/api/category/${category}`) 32 | 33 | if (packages.length === 0) { 34 | return { 35 | type: 'LIST', 36 | params: { category: 'redux' }, 37 | } 38 | } 39 | 40 | return { category, packages } 41 | }, 42 | }, 43 | } 44 | 45 | // this is essentially faking/mocking the fetch api 46 | // pretend this actually requested data over the network 47 | 48 | const fetch = async (path) => { 49 | await new Promise((res) => setTimeout(res, 500)) 50 | const category = path.replace('/api/category/', '') 51 | 52 | switch (category) { 53 | case 'redux': 54 | return ['reselect', 'recompose', 'redux-first-router'] 55 | case 'react': 56 | return [ 57 | 'react-router', 58 | 'react-transition-group', 59 | 'react-universal-component', 60 | ] 61 | default: 62 | return [] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/integration-tests/.testsConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "log": true, 3 | "availableOptions": { 4 | "log": true, 5 | "snipesOnly": true, 6 | "wallabyErrors": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/integration-tests/__helpers__/awaitUrlChange.js: -------------------------------------------------------------------------------- 1 | // `await pop() and await snapPop` do something similar to below (see `createTest.js`). 2 | // However we need a manual one that checks if the URL has changed as well, to insure 3 | // that actions dispatched after a pop come after the browser would have handled the 4 | // pop, which is asynchronous in some browsers (chrome), as well as Jest. In real life 5 | // the user takes at lesat 65ms to pop again. In code, it's too fast at 0ms. So we 6 | // wait a few more ms for it to actually change and trigger our pop handling code. 7 | 8 | /* eslint-env browser */ 9 | 10 | export default () => { 11 | const currentUrl = window.location.href 12 | return haschanged(currentUrl) 13 | } 14 | 15 | const haschanged = async (currentUrl, tries = 1) => { 16 | if (tries >= 10) { 17 | throw new Error('awaitUrlChange reached the maximum amount of tries (10)') 18 | } 19 | 20 | await new Promise((res) => setTimeout(res, 5)) 21 | if (currentUrl !== window.location.href) return 22 | return haschanged(currentUrl, ++tries) 23 | } 24 | 25 | export const windowHistoryGo = (n) => 26 | new Promise((resolve, reject) => { 27 | let timeout 28 | 29 | // Resolve when the resulting popstate event happens 30 | const onPopState = () => { 31 | clearTimeout(timeout) 32 | window.removeEventListener('popstate', onPopState) 33 | resolve() 34 | } 35 | window.addEventListener('popstate', onPopState) 36 | 37 | // Trigger the real go function 38 | window.history.go(n) 39 | 40 | // timeout after 1 second 41 | timeout = setTimeout(() => { 42 | window.removeEventListener('popstate', onPopState) 43 | reject() 44 | }, 1000) 45 | }) 46 | -------------------------------------------------------------------------------- /packages/integration-tests/__helpers__/fakeAsyncWork.js: -------------------------------------------------------------------------------- 1 | export default (cb) => 2 | new Promise((resolve) => { 3 | setTimeout(() => resolve(cb), 1) 4 | }) 5 | -------------------------------------------------------------------------------- /packages/integration-tests/__test-helpers__/createLink.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import renderer from 'react-test-renderer' 3 | import { createStore, applyMiddleware } from 'redux' 4 | import { Provider } from 'react-redux' 5 | 6 | import { createRouter } from '@respond-framework/rudy' 7 | 8 | import { Link, NavLink } from '@respond-framework/react' 9 | 10 | const createLink = async (props, initialPath, options, isNavLink) => { 11 | const link = isNavLink ? : 12 | 13 | const routes = { 14 | FIRST: '/first', 15 | SECOND: '/second/:param', 16 | THIRD: '/third', 17 | } 18 | 19 | const { middleware, reducer, firstRoute } = createRouter(routes, { 20 | initialEntries: initialPath || '/', 21 | ...options, 22 | }) 23 | 24 | const enhancer = applyMiddleware(middleware) 25 | const rootReducer = (state = {}, action = {}) => ({ 26 | location: reducer(state.location, action), 27 | }) 28 | 29 | const store = createStore(rootReducer, enhancer) 30 | await store.dispatch(firstRoute()) 31 | 32 | const component = renderer.create({link}) 33 | 34 | return { 35 | component, 36 | tree: component.toJSON(), 37 | store, 38 | } 39 | } 40 | 41 | export default createLink 42 | export const createNavLink = (path, props, options) => 43 | createLink(props, path, options, true) 44 | 45 | export const event = { preventDefault: () => undefined, button: 0 } 46 | -------------------------------------------------------------------------------- /packages/integration-tests/__test-helpers__/fakeAsyncWork.js: -------------------------------------------------------------------------------- 1 | export default (cb) => 2 | new Promise((resolve) => { 3 | setTimeout(() => resolve(cb), 1) 4 | }) 5 | -------------------------------------------------------------------------------- /packages/integration-tests/__test-helpers__/reducerParameters.js: -------------------------------------------------------------------------------- 1 | import { 2 | createHistory as createSmartHistory, 3 | createInitialState, 4 | NOT_FOUND, 5 | } from '@respond-framework/rudy' 6 | 7 | export default async (type, pathname) => { 8 | // eslint-disable-line import/prefer-default-export 9 | const history = createSmartHistory({ initialEntries: ['/first'] }) 10 | history.firstRoute.commit() 11 | 12 | const current = { pathname, url: pathname, type, params: { param: 'bar' } } 13 | const prev = { pathname: '/first', type: 'FIRST', params: {} } 14 | const routesMap = { 15 | FIRST: { path: '/first' }, 16 | SECOND: { path: '/second/:param' }, 17 | [NOT_FOUND]: { path: '/not-found' }, 18 | } 19 | 20 | return { 21 | type, 22 | pathname, 23 | current, 24 | prev, 25 | 26 | initialState: createInitialState(routesMap, history, {}), 27 | 28 | routesMap, 29 | history, 30 | 31 | action: { 32 | type, 33 | params: { param: 'bar' }, 34 | location: { 35 | url: pathname, 36 | pathname, 37 | prev, 38 | kind: 'load', 39 | entries: history.entries.slice(0), // history.entries.map(entry => entry.pathname) 40 | index: history.index, 41 | length: history.length, 42 | }, 43 | }, 44 | 45 | expectState(state) { 46 | expect(state.pathname).toEqual(pathname) 47 | expect(state.type).toEqual(type) 48 | expect(state.params).toEqual({ param: 'bar' }) 49 | expect(state.prev).toEqual(prev) 50 | expect(state.kind).toEqual('load') 51 | 52 | expect(state).toMatchSnapshot() 53 | }, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/integration-tests/__test-helpers__/setupJest.js: -------------------------------------------------------------------------------- 1 | const config = require('../.testsConfig.json') 2 | 3 | // this allows for: 4 | // 5 | // - toggling logging for all tests 6 | // - toggling whether `snap`, `snapPop` and `snapChange` just dispatch or dispatch + snap 7 | // (useful, for verifying "snipes" work without being cluttered by snapshot diffs) 8 | // - etc 9 | process.env.RUDY_OPTIONS = JSON.stringify(config) 10 | -------------------------------------------------------------------------------- /packages/integration-tests/__test-helpers__/setupThunk.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, combineReducers } from 'redux' 2 | import { createRouter } from '@respond-framework/rudy' 3 | 4 | export default async ( 5 | path = '/', 6 | thunkArg, 7 | opts, 8 | dispatchFirstRoute = true, 9 | ) => { 10 | const routesMap = { 11 | FIRST: '/first', 12 | SECOND: { path: '/second/:param', thunk: thunkArg }, 13 | THIRD: { path: '/third/:param' }, 14 | } 15 | 16 | const options = { extra: { arg: 'extra-arg' }, initialEntries: path, ...opts } 17 | 18 | const { middleware, reducer, api: rudy, firstRoute } = createRouter( 19 | routesMap, 20 | options, 21 | ) 22 | 23 | const rootReducer = combineReducers({ 24 | location: reducer, 25 | }) 26 | 27 | const enhancer = applyMiddleware(middleware) 28 | const store = createStore(rootReducer, enhancer) 29 | 30 | if (dispatchFirstRoute) await store.dispatch(firstRoute()) 31 | 32 | return { store, firstRoute, history: rudy.history } 33 | } 34 | -------------------------------------------------------------------------------- /packages/integration-tests/__test-helpers__/tempMock.js: -------------------------------------------------------------------------------- 1 | export default (module, fn, reset = true) => { 2 | if (reset) jest.resetModules() 3 | jest.doMock(module, fn) 4 | } 5 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/SPA.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../__helpers__/createTest' 2 | 3 | jest.mock('@respond-framework/rudy/utils/isHydrate', () => () => false) 4 | jest.mock('@respond-framework/utils/isServer', () => () => false) 5 | 6 | createTest('callbacks called on load if SPA', { 7 | FIRST: { 8 | path: '/first', 9 | beforeEnter() {}, 10 | thunk: ({ dispatch }) => dispatch({ type: 'REDIRECTED' }), 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/__snapshots__/cancelPendingRequest.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`new requests cancel current pending (not committed) requests 1`] = ` 4 | Object { 5 | "location": Object { 6 | "basename": "", 7 | "blocked": null, 8 | "direction": "forward", 9 | "entries": Array [ 10 | Object { 11 | "basename": "", 12 | "hash": "", 13 | "location": Object { 14 | "key": "345678", 15 | "pathname": "/first", 16 | "scene": "", 17 | "search": "", 18 | "url": "/first", 19 | }, 20 | "params": Object {}, 21 | "query": Object {}, 22 | "state": Object {}, 23 | "type": "FIRST", 24 | }, 25 | Object { 26 | "basename": "", 27 | "hash": "", 28 | "location": Object { 29 | "key": "345678", 30 | "pathname": "/third", 31 | "scene": "", 32 | "search": "", 33 | "url": "/third", 34 | }, 35 | "params": Object {}, 36 | "query": Object {}, 37 | "state": Object {}, 38 | "type": "THIRD", 39 | }, 40 | ], 41 | "from": null, 42 | "hash": "", 43 | "index": 1, 44 | "key": "345678", 45 | "kind": "push", 46 | "length": 2, 47 | "n": 1, 48 | "params": Object {}, 49 | "pathname": "/third", 50 | "pop": false, 51 | "prev": Object { 52 | "basename": "", 53 | "hash": "", 54 | "location": Object { 55 | "index": 0, 56 | "key": "345678", 57 | "pathname": "/first", 58 | "scene": "", 59 | "search": "", 60 | "url": "/first", 61 | }, 62 | "params": Object {}, 63 | "query": Object {}, 64 | "state": Object {}, 65 | "type": "FIRST", 66 | }, 67 | "query": Object {}, 68 | "ready": true, 69 | "scene": "", 70 | "search": "", 71 | "state": Object {}, 72 | "status": 200, 73 | "type": "THIRD", 74 | "universal": false, 75 | "url": "/third", 76 | }, 77 | "title": "THIRD_COMPLETE - \\"thirdThunk\\"", 78 | } 79 | `; 80 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/actions/addRoutes.js: -------------------------------------------------------------------------------- 1 | import { addRoutes } from '@respond-framework/rudy' 2 | import createTest from '../../../__helpers__/createTest' 3 | 4 | createTest( 5 | 'dispatch(addRoutes(routes))', 6 | {}, 7 | [], 8 | async ({ snap, getState }) => { 9 | const routes = { 10 | FOO: { 11 | path: '/foo', 12 | }, 13 | } 14 | 15 | await snap(addRoutes(routes)) 16 | await snap({ type: 'FOO' }) 17 | 18 | expect(getState().location.pathname).toEqual('/foo') 19 | }, 20 | ) 21 | 22 | createTest( 23 | 'dispatch(addRoutes(routes, formatRoute))', 24 | {}, 25 | [], 26 | async ({ snap, getState, dispatch }) => { 27 | const routes = { 28 | FOO: { 29 | path: '/foo', 30 | thunk: 'BAZ', 31 | }, 32 | BAZ: { 33 | path: '/baz', 34 | thunk: () => 'payload!', 35 | }, 36 | } 37 | 38 | const formatRoute = (route) => ({ 39 | ...route, 40 | path: '/bar', 41 | }) 42 | 43 | await snap(addRoutes(routes, formatRoute)) 44 | await snap({ type: 'FOO' }) 45 | 46 | expect(getState().location.pathname).toEqual('/bar') 47 | }, 48 | ) 49 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/actions/changeBasename.js: -------------------------------------------------------------------------------- 1 | import { changeBasename } from '@respond-framework/rudy' 2 | import createTest from '../../../__helpers__/createTest' 3 | 4 | createTest( 5 | 'dispatch(changeBasename(name))', 6 | {}, 7 | { 8 | basenames: ['/foo'], 9 | }, 10 | [changeBasename('/foo')], 11 | ) 12 | 13 | createTest( 14 | 'dispatch(changeBasename(name, action))', 15 | {}, 16 | { 17 | basenames: ['/foo'], 18 | }, 19 | [changeBasename('/foo', { type: 'REDIRECTED' })], 20 | ) 21 | 22 | createTest( 23 | 'dispatch(changeBasename(name)) with existing basename', 24 | {}, 25 | { 26 | basenames: ['/foo', '/bar'], 27 | }, 28 | ['/foo/first', changeBasename('/bar')], 29 | ) 30 | 31 | createTest( 32 | 'dispatch(changeBasename(name, action)) with existing basename', 33 | {}, 34 | { 35 | basenames: ['/foo', '/bar'], 36 | }, 37 | ['/foo/first', changeBasename('/bar', { type: 'REDIRECTED' })], 38 | ) 39 | 40 | createTest( 41 | 'automatically inherit existing basename', 42 | {}, 43 | { 44 | basenames: ['/bar'], 45 | }, 46 | ['/bar/first', { type: 'REDIRECTED' }], 47 | ) 48 | 49 | createTest( 50 | 'basenames without leading slashes are treated the same', 51 | {}, 52 | { 53 | basenames: ['foo', 'bar'], 54 | }, 55 | ['/foo/first', changeBasename('bar', { type: 'REDIRECTED' })], 56 | ) 57 | 58 | createTest( 59 | 'incorrect basename dispatches NOT_FOUND', 60 | {}, 61 | { 62 | basenames: ['/foo', '/bar'], 63 | }, 64 | ['/foo/first', changeBasename('/wrong')], 65 | ) 66 | 67 | createTest( 68 | 'support setting basename back to empty string', 69 | {}, 70 | { 71 | basenames: ['/bar'], 72 | }, 73 | ['/bar/first', { type: 'REDIRECTED', basename: '' }], 74 | ) 75 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/actions/notFound.js: -------------------------------------------------------------------------------- 1 | import { notFound, createScene } from '@respond-framework/rudy' 2 | 3 | import createTest from '../../../__helpers__/createTest' 4 | 5 | createTest('dispatch(notFound())', {}, [notFound()]) 6 | 7 | createTest('notFound on first load', {}, ['/non-existent']) 8 | 9 | createTest('dispatch(notFound(state))', {}, [notFound({ foo: 'bar' })]) 10 | 11 | const { routes } = createScene( 12 | { 13 | NOT_FOUND: { 14 | path: '/scene-level-not-found', 15 | }, 16 | }, 17 | { scene: 'scene' }, 18 | ) 19 | 20 | createTest('dispatch(notFound(state, forcedType))', routes, [ 21 | notFound({ foo: 'bar' }, 'scene/NOT_FOUND'), // createScene passes an alternate NOT_FOUND type 22 | ]) 23 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/actions/redirect.js: -------------------------------------------------------------------------------- 1 | import { redirect } from '@respond-framework/rudy' 2 | import createTest from '../../../__helpers__/createTest' 3 | 4 | createTest('dispatch(redirect(action))', {}, [redirect({ type: 'REDIRECTED' })]) 5 | 6 | createTest('dispatch(redirect(action, status))', {}, [ 7 | redirect({ type: 'REDIRECTED' }, 301), 8 | ]) 9 | 10 | createTest('redirect within beforeEnter', { 11 | SECOND: { 12 | path: '/second', 13 | beforeEnter: ({ dispatch }) => 14 | dispatch(redirect({ type: 'REDIRECTED' }, 301)), // this is unnecessary, redirects are automatic within callbacks, but for good measure should still work 15 | }, 16 | }) 17 | 18 | createTest('redirect within thunk', { 19 | SECOND: { 20 | path: '/second', 21 | thunk: ({ dispatch }) => dispatch(redirect({ type: 'REDIRECTED' })), 22 | }, 23 | }) 24 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/anonymousThunk.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../__helpers__/createTest' 2 | 3 | createTest( 4 | 'anonymous thunks can be dispatched', 5 | { 6 | SECOND: { 7 | path: '/second', 8 | beforeEnter: async ({ dispatch }) => { 9 | await dispatch(async ({ dispatch }) => dispatch({ type: 'REDIRECTED' })) 10 | }, 11 | }, 12 | }, 13 | async ({ dispatch, getState }) => { 14 | // test outside of route callbacks: 15 | const res = await dispatch(({ dispatch }) => 16 | dispatch({ type: 'REDIRECTED' }), 17 | ) 18 | 19 | expect(res).toMatchSnapshot() 20 | expect(getState()).toMatchSnapshot() 21 | }, 22 | ) 23 | 24 | createTest( 25 | 'anonymous thunks can return actions for automatic dispatch', 26 | { 27 | SECOND: { 28 | path: '/second', 29 | beforeEnter: ({ dispatch }) => dispatch(() => ({ type: 'REDIRECTED' })), 30 | }, 31 | }, 32 | async ({ dispatch, getState }) => { 33 | // test outside of route callbacks: 34 | const res = await dispatch(() => ({ type: 'REDIRECTED' })) 35 | 36 | expect(res).toMatchSnapshot() 37 | expect(getState()).toMatchSnapshot() 38 | }, 39 | ) 40 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/arrayCallback.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../__helpers__/createTest' 2 | 3 | createTest('callback can be array of functions', { 4 | FIRST: { 5 | path: '/first', 6 | thunk: [ 7 | (req) => { 8 | req.passOn = 'a' 9 | }, 10 | (req) => { 11 | req.passOn += 'b' 12 | }, 13 | (req) => `${req.passOn}c`, 14 | ], 15 | }, 16 | }) 17 | 18 | createTest('callback can be array of inherited callbacks', { 19 | FIRST: { 20 | path: '/first', 21 | thunk: [ 22 | (req) => { 23 | req.passOn = 'a' 24 | }, 25 | 'REDIRECTED', 26 | (req) => `${req.passOn}c`, 27 | ], 28 | }, 29 | REDIRECTED: { 30 | path: '/redirected', 31 | thunk: (req) => { 32 | req.passOn += 'ZZZ' 33 | }, 34 | }, 35 | }) 36 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/async.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../__helpers__/createTest' 2 | import fakeAsyncWork from '../../__helpers__/fakeAsyncWork' 3 | 4 | createTest('callbacks sequentially run as promises', { 5 | SECOND: { 6 | path: '/second', 7 | beforeEnter: async () => { 8 | await fakeAsyncWork() 9 | }, 10 | thunk: async () => { 11 | await fakeAsyncWork() 12 | }, 13 | onComplete: async () => { 14 | await fakeAsyncWork() 15 | return 'payload' 16 | }, 17 | }, 18 | }) 19 | 20 | createTest('callbacks sequentially run as promises /w redirect', { 21 | SECOND: { 22 | path: '/second', 23 | beforeEnter: async () => { 24 | await fakeAsyncWork() 25 | }, 26 | thunk: async ({ action }) => { 27 | await fakeAsyncWork() 28 | if (action.type !== 'SECOND') return 29 | return { type: 'REDIRECTED' } 30 | }, 31 | onComplete: async () => { 32 | await fakeAsyncWork() 33 | }, 34 | }, 35 | }) 36 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/autoDispatch.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../__helpers__/createTest' 2 | 3 | createTest('automatically dispatch action object returned from thunk', { 4 | SECOND: { 5 | path: '/second', 6 | thunk: () => ({ 7 | type: 'FOO', 8 | }), 9 | onComplete() {}, 10 | }, 11 | }) 12 | 13 | createTest('automatically infer non action object to be payload', { 14 | SECOND: { 15 | path: '/second', 16 | thunk: () => ({ foo: 'bar' }), 17 | onComplete() {}, 18 | }, 19 | }) 20 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/autoDispatchFalse.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../__helpers__/createTest' 2 | 3 | createTest( 4 | 'route.autoDispatch: false does not automatically dispatch callback return values', 5 | { 6 | SECOND: { 7 | path: '/second', 8 | autoDispatch: false, 9 | thunk: () => 'foo', 10 | }, 11 | }, 12 | ) 13 | 14 | createTest( 15 | 'options.autoDispatch: false does not automatically dispatch callback return values', 16 | { 17 | SECOND: { 18 | path: '/second', 19 | thunk: () => 'foo', 20 | }, 21 | }, 22 | { autoDispatch: false }, 23 | ) 24 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/browser/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/browser/actionsInCallbacks/jump.js: -------------------------------------------------------------------------------- 1 | import { jump } from '@respond-framework/rudy' 2 | import createTest, { resetBrowser } from '../../../../__helpers__/createTest' 3 | 4 | beforeEach(resetBrowser) 5 | 6 | createTest( 7 | 'jump before enter', 8 | { 9 | FIRST: '/', 10 | SECOND: { 11 | path: '/second', 12 | beforeEnter: () => jump(0, true), 13 | }, 14 | }, 15 | { testBrowser: true }, 16 | ) 17 | 18 | createTest( 19 | 'jump after enter', 20 | { 21 | FIRST: '/', 22 | SECOND: { 23 | path: '/second', 24 | thunk: () => jump(-1), 25 | }, 26 | }, 27 | { testBrowser: true }, 28 | ) 29 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/browser/actionsInCallbacks/jump2N.js: -------------------------------------------------------------------------------- 1 | import { jump } from '@respond-framework/rudy' 2 | import createTest, { 3 | resetBrowser, 4 | setupStore, 5 | } from '../../../../__helpers__/createTest' 6 | 7 | beforeEach(async () => { 8 | await resetBrowser() 9 | 10 | const routesMap = { 11 | FIRST: '/', 12 | SECOND: '/second', 13 | THIRD: '/third', 14 | } 15 | 16 | const { store, firstRoute, history } = setupStore(routesMap) 17 | 18 | const firstAction = firstRoute(false) 19 | await store.dispatch(firstAction) 20 | 21 | await store.dispatch({ type: 'SECOND' }) 22 | await store.dispatch({ type: 'THIRD' }) 23 | 24 | history.unlisten() 25 | }) 26 | 27 | createTest( 28 | 'jump(-2) before enter', 29 | { 30 | FIRST: '/', 31 | SECOND: '/second', 32 | THIRD: '/third', 33 | FOURTH: { 34 | path: '/fourth', 35 | beforeEnter: () => jump(-2), 36 | }, 37 | }, 38 | { testBrowser: true }, 39 | [{ type: 'FOURTH' }], 40 | ) 41 | 42 | createTest( 43 | 'jump(-2) after enter', 44 | { 45 | FIRST: '/', 46 | SECOND: '/second', 47 | THIRD: '/third', 48 | FOURTH: { 49 | path: '/fourth', 50 | thunk: () => jump(-2), 51 | }, 52 | }, 53 | { testBrowser: true }, 54 | [{ type: 'FOURTH' }], 55 | ) 56 | 57 | createTest( 58 | 'jump(-2) before enter on load', 59 | { 60 | FIRST: '/', 61 | SECOND: '/second', 62 | THIRD: { 63 | path: '/third', 64 | beforeEnter: () => jump(-2), 65 | }, 66 | }, 67 | { testBrowser: true }, 68 | [], 69 | ) 70 | 71 | createTest( 72 | 'jump(-2) after enter on load', 73 | { 74 | FIRST: '/', 75 | SECOND: '/second', 76 | THIRD: { 77 | path: '/third', 78 | thunk: () => jump(-2), 79 | }, 80 | }, 81 | { testBrowser: true }, 82 | [], 83 | ) 84 | 85 | createTest( 86 | 'jump(-2) in pathlessRoute', 87 | { 88 | FIRST: '/', 89 | SECOND: '/second', 90 | THIRD: '/third', 91 | PATHLESS: { 92 | thunk: () => jump(-2), 93 | }, 94 | }, 95 | { testBrowser: true }, 96 | [{ type: 'PATHLESS' }], 97 | ) 98 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/browser/actionsInCallbacks/push.js: -------------------------------------------------------------------------------- 1 | import { push } from '@respond-framework/rudy' 2 | import createTest, { resetBrowser } from '../../../../__helpers__/createTest' 3 | 4 | beforeEach(resetBrowser) 5 | 6 | createTest( 7 | 'push before enter', 8 | { 9 | FIRST: '/', 10 | SECOND: { 11 | path: '/second', 12 | beforeEnter: () => push('/redirected'), 13 | thunk() {}, 14 | }, 15 | }, 16 | { testBrowser: true }, 17 | ) 18 | 19 | createTest( 20 | 'push after enter', 21 | { 22 | FIRST: '/', 23 | SECOND: { 24 | path: '/second', 25 | thunk: () => push('/redirected'), 26 | onComplete() {}, 27 | }, 28 | }, 29 | { testBrowser: true }, 30 | ) 31 | 32 | createTest( 33 | 'push before enter on load', 34 | { 35 | FIRST: { 36 | path: '/', 37 | beforeEnter: () => push('/redirected'), 38 | thunk() {}, 39 | }, 40 | }, 41 | { testBrowser: true }, 42 | ) 43 | 44 | createTest( 45 | 'push after enter on load', 46 | { 47 | FIRST: { 48 | path: '/', 49 | thunk: () => push('/redirected'), 50 | onComplete() {}, 51 | }, 52 | }, 53 | { testBrowser: true }, 54 | ) 55 | 56 | createTest( 57 | 'push in pathlessRoute', 58 | { 59 | FIRST: '/', 60 | PATHLESS: { 61 | thunk: () => push('/redirected'), 62 | }, 63 | }, 64 | { testBrowser: true }, 65 | [{ type: 'PATHLESS' }], 66 | ) 67 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/browser/actionsInCallbacks/regularAction.js: -------------------------------------------------------------------------------- 1 | import { push } from '@respond-framework/rudy' 2 | import createTest, { resetBrowser } from '../../../../__helpers__/createTest' 3 | 4 | beforeEach(resetBrowser) 5 | 6 | createTest( 7 | 'dispatch before enter', 8 | { 9 | FIRST: '/', 10 | SECOND: { 11 | path: '/second', 12 | beforeEnter: () => ({ type: 'REDIRECTED' }), 13 | thunk() {}, 14 | }, 15 | }, 16 | { testBrowser: true }, 17 | ) 18 | 19 | createTest( 20 | 'dispatch after enter', 21 | { 22 | FIRST: '/', 23 | SECOND: { 24 | path: '/second', 25 | thunk: () => ({ type: 'REDIRECTED' }), 26 | onComplete() {}, 27 | }, 28 | }, 29 | { testBrowser: true }, 30 | ) 31 | 32 | createTest( 33 | 'dispatch before enter on load', 34 | { 35 | FIRST: { 36 | path: '/', 37 | beforeEnter: () => ({ type: 'REDIRECTED' }), 38 | thunk() {}, 39 | }, 40 | }, 41 | { testBrowser: true }, 42 | ) 43 | 44 | createTest( 45 | 'dispatch after enter on load', 46 | { 47 | FIRST: { 48 | path: '/', 49 | thunk: () => ({ type: 'REDIRECTED' }), 50 | onComplete() {}, 51 | }, 52 | }, 53 | { testBrowser: true }, 54 | ) 55 | 56 | createTest( 57 | 'dispatch in pathlessRoute', 58 | { 59 | FIRST: '/', 60 | PATHLESS: { 61 | thunk: () => ({ type: 'REDIRECTED' }), 62 | }, 63 | }, 64 | { testBrowser: true }, 65 | [{ type: 'PATHLESS' }], 66 | ) 67 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/browser/actionsInCallbacks/replace.js: -------------------------------------------------------------------------------- 1 | import { replace } from '@respond-framework/rudy' 2 | import createTest, { resetBrowser } from '../../../../__helpers__/createTest' 3 | 4 | beforeEach(resetBrowser) 5 | 6 | createTest( 7 | 'replace before enter', 8 | { 9 | FIRST: '/', 10 | SECOND: { 11 | path: '/second', 12 | beforeEnter: () => replace('/redirected'), 13 | thunk() {}, 14 | }, 15 | }, 16 | { testBrowser: true }, 17 | ) 18 | 19 | createTest( 20 | 'replace after enter', 21 | { 22 | FIRST: '/', 23 | SECOND: { 24 | path: '/second', 25 | thunk: () => replace('/redirected'), 26 | onComplete() {}, 27 | }, 28 | }, 29 | { testBrowser: true }, 30 | ) 31 | 32 | createTest( 33 | 'replace before enter on load', 34 | { 35 | FIRST: { 36 | path: '/', 37 | beforeEnter: () => replace('/redirected'), 38 | thunk() {}, 39 | }, 40 | }, 41 | { testBrowser: true }, 42 | ) 43 | 44 | createTest( 45 | 'replace after enter on load', 46 | { 47 | FIRST: { 48 | path: '/', 49 | thunk: () => replace('/redirected'), 50 | onComplete() {}, 51 | }, 52 | }, 53 | { testBrowser: true }, 54 | ) 55 | 56 | createTest( 57 | 'replace in pathlessRoute', 58 | { 59 | FIRST: '/', 60 | PATHLESS: { 61 | thunk: () => replace('/redirected'), 62 | }, 63 | }, 64 | { testBrowser: true }, 65 | [{ type: 'PATHLESS' }], 66 | ) 67 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/browser/history/jump.js: -------------------------------------------------------------------------------- 1 | import { locationToUrl, jump } from '@respond-framework/rudy' 2 | 3 | import createTest from '../../../../__helpers__/createTest' 4 | 5 | createTest( 6 | 'set(action, n)', 7 | { 8 | SECOND: '/second', 9 | FIRST: '/:foo?', 10 | }, 11 | { 12 | testBrowser: true, 13 | }, 14 | [], 15 | async ({ dispatch, snap, snapPop }) => { 16 | expect(locationToUrl(window.location)).toEqual('/') 17 | 18 | await dispatch({ type: 'SECOND' }) 19 | 20 | await snap(jump(-1)) 21 | expect(locationToUrl(window.location)).toEqual('/') 22 | 23 | await snapPop('forward') 24 | expect(locationToUrl(window.location)).toEqual('/second') 25 | 26 | await snapPop('back') 27 | 28 | await snap(jump(1)) 29 | expect(locationToUrl(window.location)).toEqual('/second') 30 | }, 31 | ) 32 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/browser/history/jump2N.js: -------------------------------------------------------------------------------- 1 | import { locationToUrl, jump } from '@respond-framework/rudy' 2 | import createTest, { resetBrowser } from '../../../../__helpers__/createTest' 3 | import { windowHistoryGo } from '../../../../__helpers__/awaitUrlChange' 4 | 5 | beforeEach(resetBrowser) 6 | 7 | createTest( 8 | 'set(action, n)', 9 | { 10 | SECOND: '/second', 11 | THIRD: '/third', 12 | FIRST: '/:foo?', 13 | }, 14 | { 15 | testBrowser: true, 16 | }, 17 | [], 18 | async ({ dispatch, snap, snapPop }) => { 19 | expect(locationToUrl(window.location)).toEqual('/') 20 | 21 | await dispatch({ type: 'SECOND' }) 22 | await dispatch({ type: 'THIRD' }) 23 | 24 | expect(locationToUrl(window.location)).toEqual('/third') 25 | 26 | await snap(jump(-2)) 27 | 28 | expect(locationToUrl(window.location)).toEqual('/') 29 | 30 | await snapPop('forward') 31 | expect(locationToUrl(window.location)).toEqual('/second') 32 | 33 | await snapPop('back') 34 | 35 | await dispatch(jump(2)) 36 | expect(locationToUrl(window.location)).toEqual('/third') 37 | }, 38 | ) 39 | 40 | createTest( 41 | 'history.go(n)', 42 | { 43 | SECOND: '/second', 44 | THIRD: '/third', 45 | FIRST: '/:foo?', 46 | }, 47 | { 48 | testBrowser: true, 49 | }, 50 | [], 51 | async ({ dispatch, snap }) => { 52 | expect(locationToUrl(window.location)).toEqual('/') 53 | 54 | await dispatch({ type: 'SECOND' }) 55 | await dispatch({ type: 'THIRD' }) 56 | 57 | expect(locationToUrl(window.location)).toEqual('/third') 58 | 59 | // This simulates what happens if the user right clicks on the back 60 | // button, and goes back by two steps 61 | await windowHistoryGo(-2) 62 | expect(locationToUrl(window.location)).toEqual('/') 63 | await snap() 64 | 65 | await windowHistoryGo(1) 66 | expect(locationToUrl(window.location)).toEqual('/second') 67 | await snap() 68 | 69 | await windowHistoryGo(-1) 70 | expect(locationToUrl(window.location)).toEqual('/') 71 | await snap() 72 | 73 | await windowHistoryGo(2) 74 | expect(locationToUrl(window.location)).toEqual('/third') 75 | await snap() 76 | }, 77 | ) 78 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/browser/history/push.js: -------------------------------------------------------------------------------- 1 | import { locationToUrl, push } from '@respond-framework/rudy' 2 | 3 | import createTest from '../../../../__helpers__/createTest' 4 | 5 | createTest( 6 | 'set(action, n)', 7 | { 8 | FIRST: '/', 9 | SECOND: '/second', 10 | }, 11 | { 12 | testBrowser: true, 13 | }, 14 | [], 15 | async ({ snap, pop }) => { 16 | expect(locationToUrl(window.location)).toEqual('/') 17 | 18 | await snap(push('/second', { foo: 'bar' })) 19 | expect(locationToUrl(window.location)).toEqual('/second') 20 | 21 | await pop('back') 22 | expect(locationToUrl(window.location)).toEqual('/') 23 | }, 24 | ) 25 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/browser/history/replace.js: -------------------------------------------------------------------------------- 1 | import { locationToUrl, replace } from '@respond-framework/rudy' 2 | 3 | import createTest from '../../../../__helpers__/createTest' 4 | 5 | createTest( 6 | 'set(action, n)', 7 | { 8 | FIRST: '/', 9 | SECOND: '/second', 10 | THIRD: '/third', 11 | FOURTH: '/fourth', 12 | }, 13 | { 14 | testBrowser: true, 15 | }, 16 | [], 17 | async ({ snap, dispatch }) => { 18 | expect(locationToUrl(window.location)).toEqual('/') 19 | 20 | await snap(replace('/second', { foo: 'bar' })) 21 | expect(locationToUrl(window.location)).toEqual('/second') 22 | 23 | await dispatch({ type: 'THIRD' }) 24 | 25 | await snap(replace('/fourth', { hey: 'yo' })) 26 | expect(locationToUrl(window.location)).toEqual('/fourth') 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/browser/history/set.js: -------------------------------------------------------------------------------- 1 | import { locationToUrl, set } from '@respond-framework/rudy' 2 | 3 | import createTest from '../../../../__helpers__/createTest' 4 | 5 | createTest( 6 | 'set(action)', 7 | { 8 | FIRST: '/:foo?', 9 | }, 10 | { 11 | testBrowser: true, 12 | basenames: ['/base'], 13 | }, 14 | [], 15 | async ({ snap }) => { 16 | const action = { 17 | query: { hell: 'yea' }, 18 | hash: 'yolo', 19 | basename: 'base', 20 | state: { something: 123 }, 21 | } 22 | 23 | await snap(set(action)) 24 | expect(locationToUrl(window.location)).toEqual('/base/?hell=yea#yolo') 25 | 26 | // for good measure, test overwriting it and changing the path 27 | 28 | const action2 = { 29 | ...action, 30 | params: { foo: 'bar' }, 31 | query: { hello: 'world', hell: undefined }, 32 | } 33 | 34 | await snap(set(action2)) 35 | expect(locationToUrl(window.location)).toEqual('/base/bar?hello=world#yolo') 36 | }, 37 | ) 38 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/browser/history/setN.js: -------------------------------------------------------------------------------- 1 | import { locationToUrl, set } from '@respond-framework/rudy' 2 | 3 | import createTest from '../../../../__helpers__/createTest' 4 | 5 | createTest( 6 | 'set(action, n)', 7 | { 8 | SECOND: '/second', 9 | FIRST: '/:foo?', 10 | }, 11 | { 12 | testBrowser: true, 13 | basenames: ['/base'], 14 | convertNumbers: true, 15 | }, 16 | [], 17 | async ({ dispatch, snap, snapPop }) => { 18 | expect(locationToUrl(window.location)).toEqual('/') 19 | 20 | await dispatch({ type: 'SECOND' }) 21 | 22 | const action = { 23 | params: { foo: 'bar' }, 24 | query: { hell: 'yea' }, 25 | hash: 'yolo', 26 | basename: 'base', 27 | state: { something: 123 }, 28 | } 29 | 30 | await snap(set(action, -1)) 31 | expect(locationToUrl(window.location)).toEqual('/second') 32 | 33 | await snapPop('back') 34 | expect(locationToUrl(window.location)).toEqual('/base/bar?hell=yea#yolo') 35 | 36 | await snapPop('forward') 37 | expect(locationToUrl(window.location)).toEqual('/second') 38 | }, 39 | ) 40 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/browser/pop/actionCancelsPop.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../../../__helpers__/createTest' 2 | import awaitUrlChange from '../../../../__helpers__/awaitUrlChange' 3 | 4 | createTest( 5 | 'pop then regular action (cancel pop)', 6 | { 7 | FIRST: '/', 8 | SECOND: { 9 | path: '/second', 10 | beforeEnter: () => new Promise((res) => setTimeout(res, 70)), 11 | }, 12 | THIRD: '/third', 13 | }, 14 | { testBrowser: true }, 15 | [], 16 | async ({ snap, pop, dispatch, getLocation }) => { 17 | await dispatch({ type: 'SECOND' }) 18 | await dispatch({ type: 'THIRD' }) 19 | 20 | expect(window.location.pathname).toEqual('/third') 21 | 22 | pop('back') // canceled 23 | await awaitUrlChange() 24 | 25 | await snap({ type: 'FIRST' }) // cancels "back to SECOND" 26 | 27 | expect(getLocation().type).toEqual('FIRST') 28 | expect(getLocation().index).toEqual(3) // would otherwise equal 0 due to automatic back/next handling 29 | expect(getLocation().length).toEqual(4) // would otherwise be 3 30 | expect(window.location.pathname).toEqual('/') 31 | 32 | // now let's test the real hidden browser history track to make sure it matches what we expect!!!!: 33 | await pop('back') 34 | expect(window.location.pathname).toEqual('/third') 35 | 36 | await pop('back') 37 | expect(window.location.pathname).toEqual('/second') 38 | 39 | await pop('back') 40 | expect(window.location.pathname).toEqual('/') 41 | }, 42 | ) 43 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/browser/pop/doublePop.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../../../__helpers__/createTest' 2 | 3 | createTest( 4 | 'double pop', 5 | { 6 | FIRST: '/', 7 | SECOND: { 8 | path: '/second', 9 | beforeEnter: () => new Promise((res) => setTimeout(res, 30)), 10 | }, 11 | THIRD: '/third', 12 | }, 13 | { testBrowser: true }, 14 | [], 15 | async ({ snapPop, dispatch, getLocation }) => { 16 | await dispatch({ type: 'SECOND' }) 17 | await dispatch({ type: 'THIRD' }) 18 | 19 | const res = snapPop('back') 20 | setTimeout(() => window.history.back(), 10) // canceled 21 | await res 22 | 23 | expect(getLocation().type).toEqual('SECOND') 24 | expect(window.location.pathname).toEqual('/second') 25 | }, 26 | ) 27 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/browser/pop/oldAlgorithm.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../../../__helpers__/createTest' 2 | 3 | createTest( 4 | 'double pop (when 4th index same as prev)', 5 | { 6 | FIRST: '/', 7 | SECOND: { 8 | path: '/second', 9 | beforeEnter: () => new Promise((res) => setTimeout(res, 30)), 10 | }, 11 | THIRD: '/third', 12 | FOURTH: '/fourth', 13 | }, 14 | { testBrowser: true }, 15 | [], 16 | async ({ pop, snapPop, dispatch, getLocation }) => { 17 | await dispatch({ type: 'SECOND' }) 18 | await dispatch({ type: 'THIRD' }) 19 | await dispatch({ type: 'FIRST' }) // HERE'S WHAT WE ARE TESTING 20 | // We had an old algorithm in `BrowserHistory._setupPopHandling` that would 21 | // be problematic if the prev route and 2 routes forward were the same. 22 | // The algo has been replaced with a solid one without this problem, 23 | // but we'll keep the test. 24 | 25 | await pop('back') 26 | const res = snapPop('back') 27 | setTimeout(() => window.history.back(), 10) // canceled (prev route is now FIRST) 28 | await res 29 | 30 | expect(getLocation().type).toEqual('SECOND') 31 | expect(window.location.pathname).toEqual('/second') 32 | }, 33 | ) 34 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/browser/pop/pop.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../../../__helpers__/createTest' 2 | 3 | createTest( 4 | 'pop', 5 | { 6 | FIRST: '/', 7 | SECOND: '/second', 8 | }, 9 | { testBrowser: true }, 10 | [], 11 | async ({ snapPop, dispatch, getLocation }) => { 12 | await dispatch({ type: 'SECOND' }) 13 | await snapPop('back') 14 | 15 | expect(getLocation().type).toEqual('FIRST') 16 | expect(window.location.pathname).toEqual('/') 17 | }, 18 | ) 19 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/browser/pop/popCancelsAction.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../../../__helpers__/createTest' 2 | 3 | createTest( 4 | 'regular action then pop (cancel regular action)', 5 | { 6 | FIRST: { 7 | path: '/', 8 | beforeEnter: () => new Promise((res) => setTimeout(res, 50)), 9 | }, 10 | SECOND: '/second', 11 | THIRD: { 12 | path: '/third', 13 | beforeEnter: () => new Promise((res) => setTimeout(res, 50)), 14 | }, 15 | }, 16 | { testBrowser: true }, 17 | [], 18 | async ({ snapPop, pop, dispatch, getLocation }) => { 19 | await dispatch({ type: 'SECOND' }) 20 | expect(window.location.pathname).toEqual('/second') 21 | 22 | const third = dispatch({ type: 'THIRD' }) // canceled 23 | await new Promise((res) => setTimeout(res, 10)) // let the previous action get to `beforeEnter` callback (since we did not await it) 24 | const res = pop('back') 25 | 26 | setTimeout(() => window.history.forward(), 15) // let's also check to make sure these are blocked during transition to FIRST 27 | setTimeout(() => window.history.back(), 20) 28 | setTimeout(() => window.history.forward(), 25) 29 | 30 | await Promise.all([res, third]) 31 | 32 | expect(getLocation().type).toEqual('FIRST') 33 | expect(getLocation().index).toEqual(0) // would otherwise equal 0 34 | expect(window.location.pathname).toEqual('/') 35 | }, 36 | ) 37 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/browser/pop/popReturnFalse.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../../../__helpers__/createTest' 2 | import awaitUrlChange from '../../../../__helpers__/awaitUrlChange' 3 | 4 | createTest( 5 | 'pop from route that wont let you leave', 6 | { 7 | FIRST: '/', 8 | SECOND: { 9 | path: '/second', 10 | beforeLeave: () => false, 11 | }, 12 | }, 13 | { testBrowser: true }, 14 | [], 15 | async ({ snapPop, dispatch, getLocation }) => { 16 | await dispatch({ type: 'SECOND' }) 17 | const res = await snapPop('back') 18 | console.log(res) 19 | await awaitUrlChange() 20 | 21 | expect(getLocation().type).toEqual('SECOND') 22 | expect(window.location.pathname).toEqual('/second') 23 | }, 24 | ) 25 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/browser/pop/redirect.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../../../__helpers__/createTest' 2 | 3 | createTest( 4 | 'pop redirect', 5 | { 6 | FIRST: { 7 | path: '/', 8 | beforeEnter: async ({ location }) => { 9 | if (location.kind === 'load') return 10 | await new Promise((res) => setTimeout(res, 1)) 11 | return { type: 'THIRD' } 12 | }, 13 | }, 14 | SECOND: '/second', 15 | THIRD: '/third', 16 | }, 17 | { testBrowser: true }, 18 | [], 19 | async ({ snapPop, pop, dispatch, getState, getLocation }) => { 20 | await dispatch({ type: 'SECOND' }) 21 | await snapPop('back') 22 | 23 | expect(getLocation().type).toEqual('THIRD') 24 | expect(getLocation().entries[0].location.url).toEqual('/third') 25 | 26 | expect(window.location.pathname).toEqual('/third') 27 | expect(getLocation().type).toEqual('THIRD') 28 | }, 29 | ) 30 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/browser/pop/redirectToCurrent.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../../../__helpers__/createTest' 2 | import awaitUrlChange from '../../../../__helpers__/awaitUrlChange' 3 | 4 | createTest( 5 | 'pop redirect to current URL', 6 | { 7 | FIRST: '/', 8 | SECOND: { 9 | path: '/second', 10 | beforeEnter: async ({ prevRoute }) => { 11 | if (prevRoute.type !== 'THIRD') return 12 | await new Promise((res) => setTimeout(res, 1)) 13 | return { type: 'THIRD' } 14 | }, 15 | }, 16 | THIRD: '/third', 17 | }, 18 | { testBrowser: true }, 19 | [], 20 | async ({ snapPop, pop, dispatch, getLocation }) => { 21 | await dispatch({ type: 'SECOND' }) 22 | await dispatch({ type: 'THIRD' }) 23 | 24 | await snapPop('back') 25 | await awaitUrlChange() 26 | 27 | expect(getLocation().type).toEqual('THIRD') 28 | expect(getLocation().index).toEqual(2) 29 | expect(getLocation().length).toEqual(3) 30 | 31 | expect(getLocation().entries[1].location.url).toEqual('/second') 32 | expect(getLocation().entries[2].location.url).toEqual('/third') 33 | 34 | expect(window.location.pathname).toEqual('/third') 35 | }, 36 | ) 37 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/browser/pop/redirectToPrev.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../../../__helpers__/createTest' 2 | 3 | createTest( 4 | 'pop redirect to prev URL', 5 | { 6 | FIRST: '/', 7 | SECOND: { 8 | path: '/second', 9 | beforeEnter: async ({ prevRoute, dispatch }) => { 10 | if (prevRoute.type !== 'THIRD') return 11 | await new Promise((res) => setTimeout(res, 1)) 12 | return { type: 'FIRST' } 13 | }, 14 | }, 15 | THIRD: '/third', 16 | }, 17 | { testBrowser: true }, 18 | [], 19 | async ({ snapPop, dispatch, getLocation }) => { 20 | await dispatch({ type: 'SECOND' }) 21 | await dispatch({ type: 'THIRD' }) 22 | await snapPop('back') 23 | 24 | expect(getLocation().type).toEqual('FIRST') 25 | expect(getLocation().index).toEqual(0) 26 | 27 | expect(getLocation().entries[0].location.url).toEqual('/') 28 | expect(getLocation().entries[1].location.url).toEqual('/second') 29 | 30 | expect(window.location.pathname).toEqual('/') 31 | }, 32 | ) 33 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/browser/pop/triplePopBothDirections.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../../../__helpers__/createTest' 2 | 3 | createTest( 4 | 'pop back then forward', 5 | { 6 | FIRST: '/', 7 | SECOND: { 8 | path: '/second', 9 | beforeEnter: () => new Promise((res) => setTimeout(res, 120)), 10 | }, 11 | THIRD: '/third', 12 | }, 13 | { testBrowser: true }, 14 | [], 15 | async ({ snapPop, dispatch, getLocation }) => { 16 | await dispatch({ type: 'SECOND' }) 17 | await dispatch({ type: 'THIRD' }) 18 | 19 | const mainPop = snapPop('back') 20 | 21 | setTimeout(() => window.history.back(), 10) 22 | setTimeout(() => window.history.forward(), 20) 23 | 24 | // and a few more for good measure (all should be blocked until the initial pop completes) 25 | setTimeout(() => window.history.forward(), 30) 26 | setTimeout(() => window.history.back(), 40) 27 | 28 | await mainPop 29 | expect(getLocation().type).toEqual('SECOND') 30 | expect(window.location.pathname).toEqual('/second') 31 | }, 32 | ) 33 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/browser/sessionStorage/restoreFromBack.js: -------------------------------------------------------------------------------- 1 | import { getSessionStorage } from '@respond-framework/rudy' 2 | import createTest, { setupStore } from '../../../../__helpers__/createTest' 3 | 4 | beforeAll(async () => { 5 | const routesMap = { 6 | FIRST: '/', 7 | SECOND: '/second', 8 | THIRD: '/third', 9 | } 10 | 11 | const { store, firstRoute, history } = setupStore(routesMap) 12 | 13 | const firstAction = firstRoute(false) 14 | await store.dispatch(firstAction) 15 | 16 | await store.dispatch({ type: 'SECOND' }) 17 | await store.dispatch({ type: 'THIRD' }) 18 | 19 | await history.back() 20 | await history.back() // history.entries will be at first entry now 21 | 22 | history.unlisten() 23 | }) 24 | 25 | createTest( 26 | 'restore history when index === 0', 27 | { 28 | FIRST: '/', 29 | SECOND: '/second', 30 | THIRD: '/third', 31 | }, 32 | { testBrowser: true }, 33 | [], 34 | async ({ snapPop, getLocation }) => { 35 | expect(getLocation()).toMatchSnapshot() 36 | expect(getSessionStorage()).toMatchSnapshot() 37 | 38 | await snapPop('forward') 39 | await snapPop('forward') 40 | 41 | expect(getLocation().type).toEqual('THIRD') 42 | expect(window.location.pathname).toEqual('/third') 43 | 44 | expect(getLocation().index).toEqual(2) 45 | expect(getLocation().length).toEqual(3) 46 | 47 | expect(getSessionStorage()).toMatchSnapshot() 48 | }, 49 | ) 50 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/browser/sessionStorage/restoreFromFront.js: -------------------------------------------------------------------------------- 1 | import { getSessionStorage } from '@respond-framework/rudy' 2 | import createTest, { setupStore } from '../../../../__helpers__/createTest' 3 | 4 | beforeAll(async () => { 5 | const routesMap = { 6 | FIRST: '/', 7 | SECOND: '/second', 8 | THIRD: '/third', 9 | } 10 | 11 | const { store, firstRoute, history } = setupStore(routesMap) 12 | 13 | const firstAction = firstRoute(false) 14 | await store.dispatch(firstAction) 15 | 16 | await store.dispatch({ type: 'SECOND' }) 17 | await store.dispatch({ type: 'THIRD' }) 18 | 19 | history.unlisten() 20 | }) 21 | 22 | createTest( 23 | 'restore history when index === entries.length - 1', 24 | { 25 | FIRST: '/', 26 | SECOND: '/second', 27 | THIRD: '/third', 28 | }, 29 | { testBrowser: true }, 30 | [], 31 | async ({ snapPop, getLocation }) => { 32 | expect(getLocation()).toMatchSnapshot() 33 | expect(getSessionStorage()).toMatchSnapshot() 34 | 35 | await snapPop('back') 36 | await snapPop('back') 37 | 38 | expect(getLocation().type).toEqual('FIRST') 39 | expect(window.location.pathname).toEqual('/') 40 | 41 | expect(getLocation().index).toEqual(0) 42 | expect(getLocation().length).toEqual(3) 43 | 44 | expect(getSessionStorage()).toMatchSnapshot() 45 | }, 46 | ) 47 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/browser/sessionStorage/restoreFromMiddle.js: -------------------------------------------------------------------------------- 1 | import { getSessionStorage } from '@respond-framework/rudy' 2 | import createTest, { setupStore } from '../../../../__helpers__/createTest' 3 | 4 | beforeAll(async () => { 5 | const routesMap = { 6 | FIRST: '/', 7 | SECOND: '/second', 8 | THIRD: '/third', 9 | } 10 | 11 | const { store, firstRoute, history } = setupStore(routesMap) 12 | 13 | const firstAction = firstRoute(false) 14 | await store.dispatch(firstAction) 15 | 16 | await store.dispatch({ type: 'SECOND' }) 17 | await store.dispatch({ type: 'THIRD' }) 18 | await history.back() // go back, so we can simulate leaving in the middle of the stack 19 | 20 | history.unlisten() 21 | }) 22 | 23 | createTest( 24 | 'restore history when index > 0', 25 | { 26 | FIRST: '/', 27 | SECOND: '/second', 28 | THIRD: '/third', 29 | }, 30 | { testBrowser: true }, 31 | [], 32 | async ({ snapPop, getLocation }) => { 33 | expect(getLocation()).toMatchSnapshot() 34 | expect(getSessionStorage()).toMatchSnapshot() 35 | 36 | await snapPop('back') 37 | 38 | expect(getLocation().type).toEqual('FIRST') 39 | expect(window.location.pathname).toEqual('/') 40 | 41 | expect(getLocation().index).toEqual(0) 42 | 43 | // although we left in the middle of the entries (at SECOND), 44 | // THIRD will still be present on return, since in a real browser 45 | // there is no reliable way to distinguish between these histories 46 | // 1. FIRST -> SECOND -> THIRD -> SECOND(back button) -> google.com -> SECOND(back button) 47 | // 2. FIRST -> SECOND -> THIRD -> google.com -> SECOND(right click back button, back 2 steps) 48 | expect(getLocation().length).toEqual(3) 49 | 50 | expect(getSessionStorage()).toMatchSnapshot() 51 | }, 52 | ) 53 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/callRoute.js: -------------------------------------------------------------------------------- 1 | import { callRoute } from '@respond-framework/rudy' 2 | import createTest from '../../__helpers__/createTest' 3 | 4 | createTest( 5 | 'callRoute(action | type, routeKey, ...args)', 6 | { 7 | SECOND: { 8 | path: '/second', 9 | foo: 'bar', 10 | }, 11 | THIRD: { 12 | path: '/third', 13 | doSomething: (action, arg1, arg2) => action.type + arg1 + arg2, 14 | }, 15 | }, 16 | [], 17 | async ({ routes }) => { 18 | const call = callRoute(routes) 19 | 20 | expect(call('NONE')).toEqual(null) 21 | 22 | expect(call('SECOND').foo).toEqual('bar') 23 | 24 | expect(call('SECOND', 'foo')).toEqual('bar') 25 | expect(call({ type: 'SECOND' }, 'foo')).toEqual('bar') 26 | 27 | expect(call('THIRD', 'doSomething', 'arg1', 'arg2')).toEqual( 28 | 'THIRDarg1arg2', 29 | ) 30 | }, 31 | ) 32 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/callStartTrue.js: -------------------------------------------------------------------------------- 1 | import { transformAction, call, enter, compose } from '@respond-framework/rudy' 2 | 3 | import createTest from '../../__helpers__/createTest' 4 | 5 | createTest('call({ start: true })', { 6 | SECOND: { 7 | path: '/second', 8 | thunk: async (req) => { 9 | await new Promise((res) => setTimeout(res, 5)) 10 | expect(req.getLocation().ready).toEqual(false) 11 | return 'SUCCESS!!' 12 | }, 13 | middleware: [ 14 | transformAction, 15 | call('thunk', { start: true }), 16 | enter, 17 | () => (req, next) => { 18 | expect(req.getLocation().ready).toEqual(true) 19 | return next() 20 | }, 21 | ], 22 | }, 23 | }) 24 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/cancelPendingRequest.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../__helpers__/createTest' 2 | 3 | createTest( 4 | 'new requests cancel current pending (not committed) requests', 5 | { 6 | FIRST: { 7 | path: '/first', 8 | beforeLeave(req) { 9 | if (req.type === 'THIRD') return 10 | return new Promise((res) => setTimeout(res, 10)) 11 | }, 12 | }, 13 | SECOND: { 14 | path: '/second', 15 | beforeEnter: () => 'secondBeforeEnter', // will not run because pipeline will be canceled 16 | }, 17 | THIRD: { 18 | path: '/third', 19 | thunk: () => 'thirdThunk', 20 | }, 21 | }, 22 | [], 23 | async ({ dispatch, getLocation, getState }) => { 24 | let res = dispatch({ type: 'SECOND' }) 25 | await dispatch({ type: 'THIRD' }) 26 | 27 | const location = getLocation() 28 | expect(location.type).toEqual('THIRD') 29 | 30 | res = await res 31 | expect(res.type).toEqual('SECOND') // res will still be the action that was canceled 32 | 33 | expect(getLocation().type).toEqual('THIRD') 34 | expect(getState()).toMatchSnapshot() 35 | }, 36 | ) 37 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/complexRedirects.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../__helpers__/createTest' 2 | 3 | createTest( 4 | 'pathlessRoute + anonymousThunks can perform redirects in the pipeline', 5 | { 6 | SECOND: { 7 | path: '/second', 8 | autoDispatch: false, 9 | beforeEnter: () => 'foo', 10 | thunk: async ({ dispatch }) => { 11 | await dispatch({ type: 'PATHLESS_NOT_INTERPUTING' }) 12 | await dispatch({ type: 'PATHLESS_A' }) 13 | }, 14 | onComplete: () => {}, 15 | }, 16 | PATHLESS_A: { 17 | thunk: async ({ dispatch }) => { 18 | await dispatch(async ({ dispatch }) => 19 | // anonymousThunk 20 | dispatch({ type: 'PATHLESS_B' }), 21 | ) 22 | }, 23 | }, 24 | PATHLESS_B: { 25 | thunk: async () => ({ type: 'REDIRECTED' }), // we'll reach here successfully in one pass through the pipeline 26 | }, 27 | PATHLESS_NOT_INTERPUTING: { 28 | thunk: () => {}, 29 | }, 30 | }, 31 | [{ type: 'SECOND' }], 32 | ) 33 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/createAction.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../__helpers__/createTest' 2 | 3 | createTest( 4 | 'add routeType_COMPLETE as type to action-like object that is missing type', 5 | { 6 | SECOND: { 7 | path: '/second', 8 | thunk: ({ dispatch }) => dispatch({ payload: 'foo' }), 9 | onComplete() {}, 10 | }, 11 | }, 12 | ) 13 | 14 | createTest('when isAction(action) === false set to payload', { 15 | SECOND: { 16 | path: '/second', 17 | thunk: ({ dispatch }) => dispatch({ foo: 'bar' }), 18 | onComplete() {}, 19 | }, 20 | }) 21 | 22 | createTest('non object argument can be payload', { 23 | SECOND: { 24 | path: '/second', 25 | thunk: ({ dispatch }) => dispatch('foo'), 26 | onComplete() {}, 27 | }, 28 | }) 29 | 30 | createTest('null payloads allowed', { 31 | SECOND: { 32 | path: '/second', 33 | thunk: ({ dispatch }) => dispatch(null), 34 | onComplete() {}, 35 | }, 36 | }) 37 | 38 | createTest('arg as type', { 39 | SECOND: { 40 | path: '/second', 41 | thunk: ({ dispatch }) => dispatch('FOO_BAR'), 42 | onComplete() {}, 43 | }, 44 | }) 45 | 46 | createTest('arg as @@library type', { 47 | SECOND: { 48 | path: '/second', 49 | thunk: ({ dispatch }) => dispatch('@@library/FOO_BAR'), 50 | onComplete() {}, 51 | }, 52 | }) 53 | 54 | createTest('arg as type from route', { 55 | SECOND: { 56 | path: '/second', 57 | thunk: ({ dispatch }) => dispatch('FIRST'), 58 | onComplete() {}, 59 | }, 60 | }) 61 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/createRequest.js: -------------------------------------------------------------------------------- 1 | import { _Request } from '@respond-framework/rudy' 2 | import createTest from '../../__helpers__/createTest' 3 | 4 | createTest( 5 | 'callback "req" argument has all goodies', 6 | { 7 | SECOND: { 8 | path: '/second', 9 | beforeEnter: (req) => { 10 | expect(req).toBeInstanceOf(_Request) 11 | 12 | expect(req.tmp).toBeDefined() 13 | expect(req.ctx).toMatchObject({ 14 | pending: { type: 'SECOND' }, 15 | busy: true, 16 | }) 17 | 18 | expect(req.routes).toBeDefined() 19 | expect(req.options).toBeDefined() 20 | expect(req.history).toBeDefined() 21 | 22 | expect(req.register).toBeDefined() 23 | expect(req.has).toBeDefined() 24 | 25 | expect(req.getLocation).toBeDefined() 26 | 27 | expect(req.dispatch).toBeDefined() 28 | expect(req.getState).toBeDefined() 29 | 30 | expect(req.action).toMatchObject({ type: 'SECOND' }) 31 | expect(req.route).toBeDefined() 32 | expect(req.prevRoute).toBeDefined() 33 | expect(req.error).toEqual(null) 34 | expect(req.scene).toEqual('') 35 | 36 | expect(req.realDispatch).toBeDefined() 37 | expect(req.commitHistory).toBeDefined() 38 | expect(req.commitDispatch).toBeDefined() 39 | 40 | expect(req.type).toBeDefined() 41 | expect(req.params).toBeDefined() 42 | expect(req.query).toBeDefined() 43 | expect(req.state).toBeDefined() 44 | expect(req.hash).toBeDefined() 45 | expect(req.basename).toBeDefined() 46 | expect(req.location).toBeDefined() 47 | 48 | expect(req.foo).toEqual(1) 49 | expect(req.bar).toEqual(2) 50 | }, 51 | thunk: (req) => { 52 | expect(req.ctx).toMatchObject({ pending: false, busy: true }) 53 | expect(req.tmp).toMatchObject({ 54 | committed: true, 55 | load: undefined, 56 | revertPop: undefined, 57 | }) 58 | }, 59 | }, 60 | }, 61 | { 62 | extra: { foo: 1, bar: 2 }, 63 | }, 64 | ) 65 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/createScene/actionCreators.js: -------------------------------------------------------------------------------- 1 | import { createScene } from '@respond-framework/rudy' 2 | import createTest from '../../../__helpers__/createTest' 3 | 4 | const routesMap = { 5 | SECOND: { 6 | path: '/second/:foo?', 7 | error: (error) => ({ ...error, foo: 'bar' }), 8 | }, 9 | THIRD: { 10 | path: '/third/:foo', 11 | action: (arg) => (req) => ({ params: { foo: arg } }), 12 | }, 13 | FOURTH: { 14 | path: '/fourth/:foo?', 15 | action: ['customCreator'], 16 | customCreator: (arg) => (req) => ({ params: { foo: arg } }), 17 | }, 18 | PLAIN: { 19 | action: (arg) => ({ foo: arg }), 20 | }, 21 | NOT_FOUND: '/not-found-foo', 22 | } 23 | 24 | const { actions, routes } = createScene(routesMap) 25 | 26 | createTest('createScene()', routes, [ 27 | ['actions.second()', actions.second()], 28 | ['actions.second(partialAction)', actions.second({ params: { foo: 'bar' } })], 29 | ['actions.second(params)', actions.second({ foo: 'bar' })], 30 | [ 31 | 'actions.second(thunk)', 32 | actions.second((req) => ({ foo: req.getState().title })), 33 | ], 34 | ['actions.second(action with wrong type)', actions.second({ type: 'WRONG' })], 35 | ['route.action - custom action creator', actions.third('baz')], 36 | [ 37 | 'route.action: [] - custom action creators (array) - actions.fourth.customCreator()', 38 | actions.fourth.customCreator('baz'), 39 | ], 40 | ['actions.third.error(new Error)', actions.third.error(new Error('fail'))], 41 | [ 42 | 'route.error - custom error action creator', 43 | actions.second.error(new Error('fail')), 44 | ], 45 | ['actions.third.complete(payload)', actions.third.complete({ foo: 'bar' })], 46 | [ 47 | 'actions.second.complete(thunk)', 48 | actions.second.complete(() => ({ foo: 'bar' })), 49 | ], 50 | ['route with just action creator', actions.plain('hello')], 51 | ['actions.notFound(state)', actions.notFound({ foo: 'bar' })], 52 | ['actions.notFound.complete(payload)', actions.notFound.complete('foo')], 53 | ]) 54 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/createScene/returnedUtilities.js: -------------------------------------------------------------------------------- 1 | import { createScene } from '@respond-framework/rudy' 2 | 3 | const routesMap = { 4 | SECOND: { 5 | path: '/second', 6 | error: (error) => ({ ...error, foo: 'bar' }), 7 | }, 8 | THIRD: { 9 | path: '/third', 10 | action: (arg) => (req, type) => ({ params: { foo: arg }, type }), 11 | }, 12 | FOURTH: { 13 | path: '/fourth', 14 | action: ['customCreator'], 15 | customCreator: (arg) => (req, type) => ({ params: { foo: arg }, type }), 16 | }, 17 | PLAIN: { 18 | action: (arg) => ({ foo: arg }), 19 | }, 20 | NOT_FOUND: '/not-found-foo', 21 | } 22 | 23 | test('createScene returns types, actions, routes, exportString', () => { 24 | const { types, actions, routes, exportString } = createScene(routesMap, { 25 | logExports: true, 26 | }) 27 | 28 | expect(types).toMatchSnapshot() 29 | expect(actions).toMatchSnapshot() 30 | expect(routes).toMatchSnapshot() 31 | expect(exportString).toMatchSnapshot() 32 | }) 33 | 34 | test('createScene returns types, actions, routes, exportString (/w scene + basename options)', () => { 35 | const { types, actions, routes, exportString } = createScene(routesMap, { 36 | scene: 'SCENE', 37 | basename: '/base-name', 38 | logExports: true, 39 | }) 40 | 41 | expect(types).toMatchSnapshot() 42 | expect(actions).toMatchSnapshot() 43 | expect(routes).toMatchSnapshot() 44 | expect(exportString).toMatchSnapshot() 45 | }) 46 | 47 | test('call createScene twice on same routes', () => { 48 | const { routes: r } = createScene(routesMap, { scene: 'scene' }) 49 | const { types, actions, routes: r2, exportString } = createScene(r, { 50 | scene: 'double', 51 | logExports: true, 52 | }) 53 | 54 | expect(types).toMatchSnapshot() 55 | expect(actions).toMatchSnapshot() 56 | expect(r2).toMatchSnapshot() 57 | expect(exportString).toMatchSnapshot() 58 | }) 59 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/dispatchFrom.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../__helpers__/createTest' 2 | 3 | createTest( 4 | 're-dispatching state.from behaves as if original action was dispatched', 5 | { 6 | SECOND: { 7 | path: '/second', 8 | beforeEnter: async ({ dispatch, action }) => { 9 | if (action.state.allow) return 10 | return dispatch({ type: 'REDIRECTED' }) 11 | }, 12 | }, 13 | }, 14 | [], 15 | async ({ dispatch, getState }) => { 16 | let res = await dispatch({ type: 'SECOND' }) 17 | 18 | expect(res.type).toEqual('REDIRECTED_COMPLETE') 19 | 20 | // users can simply dispatch `state.from` whenever they are ready to take the user 21 | // to where the user initially tried to go 22 | const { from } = getState().location 23 | expect(from).toMatchSnapshot() 24 | from.state = { allow: true } // for our simple example, we'll add this key/val to make `beforeEnter` not redirect 25 | res = await dispatch(from) 26 | 27 | expect(res.type).toEqual('SECOND') 28 | 29 | expect(res).toMatchSnapshot('response') 30 | expect(getState()).toMatchSnapshot('state') 31 | }, 32 | ) 33 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/dontDoubleDispatch.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../__helpers__/createTest' 2 | 3 | createTest( 4 | 'do NOT dispatch actions with identical URLs more than once', 5 | { 6 | SECOND: { 7 | path: '/second/:param', 8 | beforeEnter() {}, 9 | }, 10 | }, 11 | [], 12 | async ({ history, snap, getState, dispatch }) => { 13 | await snap({ 14 | type: 'SECOND', 15 | params: { param: 'foo' }, 16 | query: { foo: 'bar' }, 17 | hash: 'bla', 18 | }) 19 | await snap({ 20 | type: 'SECOND', 21 | params: { param: 'foo' }, 22 | query: { foo: 'bar' }, 23 | hash: 'bla', 24 | }) 25 | 26 | const state = getState() 27 | const res = await history.push('/second/foo?foo=bar#bla') 28 | expect(res).toMatchSnapshot() 29 | expect(state).toEqual(getState()) 30 | 31 | await dispatch(({ routes }) => { 32 | expect(routes.SECOND.beforeEnter).toHaveBeenCalledTimes(1) 33 | }) 34 | }, 35 | ) 36 | 37 | createTest( 38 | 'allow double dispatch when kind === "load"', 39 | { 40 | FIRST: { 41 | path: '/first', 42 | beforeEnter() {}, 43 | }, 44 | }, 45 | [{ type: 'FIRST' }], 46 | ) 47 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/entryState.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../__helpers__/createTest' 2 | 3 | createTest( 4 | 'state attached to history entry', 5 | { 6 | SECOND: { 7 | path: '/second', 8 | }, 9 | }, 10 | [{ type: 'SECOND', state: { foo: 'bar' } }], 11 | ) 12 | 13 | createTest( 14 | 'route.defaultState', 15 | { 16 | SECOND: { 17 | path: '/second', 18 | defaultState: { foo: 'bar' }, 19 | }, 20 | THIRD: { 21 | path: '/third', 22 | defaultState: (q) => ({ ...q, foo: 'bar' }), 23 | }, 24 | }, 25 | [{ type: 'SECOND', state: { key: 'correct' } }, { type: 'SECOND' }], 26 | async ({ history, snapChange }) => { 27 | const res = await history.push('/third', { abc: 123 }) 28 | snapChange(res) 29 | }, 30 | ) 31 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/firstRoute.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../__helpers__/createTest' 2 | 3 | // true is the default by the way 4 | createTest( 5 | 'firstRoute(true) resolves early on enter', 6 | { 7 | FIRST: { 8 | path: '/first', 9 | beforeEnter: () => 'beforeEnterComplete', 10 | thunk: async () => { 11 | await new Promise((res) => setTimeout(res, 5)) 12 | return 'notAwaited' 13 | }, 14 | }, 15 | }, 16 | { 17 | dispatchFirstRoute: false, // but tests are setup to pass `true` without this option (note: this option only exists in tests) 18 | }, 19 | [], 20 | async ({ firstRoute, dispatch, getState }) => { 21 | const res = await dispatch(firstRoute()) // by default creatTest dispatches: firstRoute(false), going through the whole pipeline 22 | 23 | expect(res.type).toEqual('FIRST') 24 | expect(getState().title).toEqual('FIRST') // would otherwise equal: FIRST_COMPLETE - "notAwaited" 25 | 26 | expect(res).toMatchSnapshot('response') 27 | expect(getState()).toMatchSnapshot('state') 28 | }, 29 | ) 30 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/formatRoutes.js: -------------------------------------------------------------------------------- 1 | import { notFound } from '@respond-framework/rudy' 2 | import createTest from '../../__helpers__/createTest' 3 | 4 | createTest('routes as path string', { 5 | FIRST: '/first', 6 | }) 7 | 8 | createTest('route as thunk function (pathless route)', { 9 | PATHLESS: ({ dispatch }) => dispatch({ type: 'REDIRECTED' }), 10 | }) 11 | 12 | createTest( 13 | 'custom NOT_FOUND route', 14 | { 15 | NOT_FOUND: { 16 | path: '/not-available', 17 | thunk: () => 'thunk done', 18 | }, 19 | }, 20 | ['/missed', notFound()], 21 | ) 22 | 23 | createTest( 24 | 'options.formatRoute', 25 | { 26 | FIRST: { 27 | foo: '/first', 28 | }, 29 | }, 30 | { 31 | formatRoute: (route, type, routes, isAddRoutes) => ({ 32 | ...route, 33 | type, 34 | path: route.path || route.foo, 35 | }), 36 | }, 37 | ) 38 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/hydrate.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../__helpers__/createTest' 2 | 3 | jest.mock('@respond-framework/rudy/utils/isHydrate', () => () => true) 4 | jest.mock('@respond-framework/utils/isServer', () => () => false) 5 | 6 | createTest('beforeEnter + thunk callbacks NOT called if isHydrate', { 7 | FIRST: { 8 | path: '/first', 9 | beforeLeave() {}, 10 | beforeEnter() {}, 11 | onEnter() {}, 12 | onLeave() {}, 13 | thunk() {}, 14 | onComplete() {}, 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/inheritedCallbacks.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../__helpers__/createTest' 2 | 3 | createTest( 4 | 'inherit individual callback from another route', 5 | { 6 | FIRST: { 7 | path: '/first', 8 | thunk: 'SECOND', 9 | }, 10 | SECOND: { 11 | path: '/second', 12 | thunk() {}, 13 | }, 14 | }, 15 | ['/first'], 16 | ) 17 | 18 | createTest( 19 | 'inherit all callbacks from another route', 20 | { 21 | FIRST: { 22 | path: '/first', 23 | inherit: 'SECOND', 24 | }, 25 | SECOND: { 26 | path: '/second', 27 | beforeEnter() {}, 28 | thunk() {}, 29 | onComplete() {}, 30 | }, 31 | }, 32 | ['/first'], 33 | ) 34 | 35 | createTest( 36 | 'recursively inherit callback', 37 | { 38 | FIRST: { 39 | path: '/first', 40 | thunk: 'SECOND', 41 | }, 42 | SECOND: { 43 | path: '/second', 44 | thunk: 'THIRD', 45 | }, 46 | THIRD: { 47 | path: '/third', 48 | thunk({ action }) { 49 | return `${action.type} - payload` 50 | }, 51 | }, 52 | }, 53 | ['/first'], 54 | ) 55 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/middlewareAsFunction.js: -------------------------------------------------------------------------------- 1 | import { 2 | compose, 3 | serverRedirect, 4 | pathlessRoute, 5 | anonymousThunk, 6 | transformAction, 7 | call, 8 | enter, 9 | changePageTitle, 10 | } from '@respond-framework/rudy' 11 | 12 | import createTest from '../../__helpers__/createTest' 13 | 14 | createTest( 15 | 'middleware argument can be a function that is responsible for the work of composePromise', 16 | { 17 | SECOND: { 18 | path: '/second', 19 | thunk() {}, 20 | }, 21 | }, 22 | { 23 | // `middlewareFunc` will actually be made to override the `middlewares` arg to `createRouter` 24 | // by `createTest.js`. The signature will become: `createRouter(routes, options, middlewareFunc)` 25 | middlewareFunc: (api, handleRedirects) => 26 | compose( 27 | [ 28 | serverRedirect, // short-circuiting middleware 29 | pathlessRoute('thunk'), 30 | anonymousThunk, 31 | transformAction, // pipeline starts here 32 | call('beforeLeave', { prev: true }), 33 | call('beforeEnter'), 34 | enter, 35 | changePageTitle(), 36 | call('onLeave', { prev: true }), 37 | call('onEnter'), 38 | call('thunk', { cache: true }), 39 | call('onComplete'), 40 | ], 41 | api, 42 | handleRedirects, 43 | ), 44 | }, 45 | ) 46 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/multipleRedirects.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../__helpers__/createTest' 2 | 3 | createTest('multiple redirects are honored', { 4 | SECOND: { 5 | path: '/second', 6 | beforeEnter: ({ dispatch }) => dispatch({ type: 'REDIRECTED' }), 7 | thunk() {}, 8 | }, 9 | REDIRECTED: { 10 | path: '/redirected', 11 | beforeEnter: ({ dispatch }) => dispatch({ type: 'REDIRECTED_AGAIN' }), 12 | thunk() {}, 13 | }, 14 | REDIRECTED_AGAIN: { 15 | path: '/redirected-again', 16 | thunk() {}, 17 | }, 18 | }) 19 | 20 | createTest('multiple redirects are honored after enter', { 21 | SECOND: { 22 | path: '/second', 23 | thunk: ({ dispatch }) => dispatch({ type: 'REDIRECTED_AFTER' }), 24 | onComplete() {}, 25 | }, 26 | REDIRECTED_AFTER: { 27 | path: '/redirected-after', 28 | thunk: ({ dispatch }) => dispatch({ type: 'REDIRECTED_AGAIN_AFTER' }), 29 | onComplete() {}, 30 | }, 31 | REDIRECTED_AGAIN_AFTER: { 32 | path: '/redirected-again-after', 33 | onComplete() {}, 34 | }, 35 | }) 36 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/onError.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../__helpers__/createTest' 2 | 3 | createTest('route onError called if other callbacks throw', { 4 | SECOND: { 5 | path: '/second', 6 | thunk: () => { 7 | throw new Error('thunk-failed') 8 | }, 9 | onError() {}, 10 | }, 11 | }) 12 | 13 | createTest('route onError dispatches redirect', { 14 | SECOND: { 15 | path: '/second', 16 | thunk: () => { 17 | throw new Error('thunk-failed') 18 | }, 19 | onError: () => ({ type: 'REDIRECTED' }), 20 | }, 21 | }) 22 | 23 | createTest('currentType_ERROR dispatched if no onError callback provided', { 24 | SECOND: { 25 | path: '/second', 26 | thunk: () => { 27 | throw new Error('thunk-failed') 28 | }, 29 | }, 30 | }) 31 | 32 | createTest( 33 | 'default options.onError skipped if options.onError === null', 34 | { 35 | SECOND: { 36 | path: '/second', 37 | thunk: () => { 38 | throw new Error('thunk-failed') 39 | }, 40 | onError() {}, 41 | }, 42 | }, 43 | { onError: null }, 44 | ) 45 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/optionsCallbacks.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../__helpers__/createTest' 2 | 3 | createTest( 4 | 'all options callbacks are called', 5 | { 6 | SECOND: { 7 | path: '/second', 8 | }, 9 | }, 10 | { 11 | beforeLeave() {}, 12 | beforeEnter() {}, 13 | onLeave() {}, 14 | onEnter() {}, 15 | thunk() {}, 16 | onComplete: ({ action }) => { 17 | if (action.type !== 'SECOND') return 18 | throw new Error('test-error') 19 | }, 20 | onError() {}, 21 | }, 22 | ) 23 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/overrideOptions.js: -------------------------------------------------------------------------------- 1 | import { 2 | compose, 3 | createHistory, 4 | createRequest, 5 | createReducer, 6 | shouldTransition, 7 | shouldCall, 8 | } from '@respond-framework/rudy' 9 | import createTest from '../../__helpers__/createTest' 10 | 11 | createTest( 12 | 'core capabilities can be overriden as options', 13 | { 14 | SECOND: { 15 | path: '/second', 16 | thunk() {}, 17 | }, 18 | }, 19 | { 20 | createHistory: (...args) => createHistory(...args), 21 | createReducer: (...args) => createReducer(...args), 22 | compose: (...args) => compose(...args), 23 | shouldTransition: (...args) => shouldTransition(...args), 24 | createRequest: (...args) => createRequest(...args), 25 | shouldCall: (...args) => shouldCall(...args), 26 | location: 'location', 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/pathlessRoute.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../__helpers__/createTest' 2 | 3 | createTest('pathless route thunk called', { 4 | PATHLESS: { 5 | thunk: async ({ dispatch }) => dispatch({ type: 'REDIRECTED' }), 6 | onComplete() {}, 7 | }, 8 | }) 9 | 10 | createTest('pathless route thunk errors trigger onError', { 11 | PATHLESS: { 12 | thunk: ({ dispatch }) => { 13 | throw new Error('fail') 14 | }, 15 | onError() {}, 16 | onComplete() {}, 17 | }, 18 | }) 19 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/redirectShortcut.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../__helpers__/createTest' 2 | 3 | createTest('redirect to type specified as redirect route option value', { 4 | SECOND: { 5 | path: '/second', 6 | redirect: 'REDIRECTED', 7 | }, 8 | }) 9 | 10 | createTest( 11 | 'redirect shortcut /w params passed on', 12 | { 13 | SECOND: { 14 | path: '/second/:param', 15 | redirect: 'REDIRECTED', 16 | }, 17 | REDIRECTED: { 18 | path: '/redirected/:param', 19 | onComplete: () => 'redirect_complete', 20 | }, 21 | }, 22 | [{ type: 'SECOND', params: { param: 'foo' } }], 23 | ) 24 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/redirects.js: -------------------------------------------------------------------------------- 1 | import createTest from '../../__helpers__/createTest' 2 | 3 | createTest('redirect before enter', { 4 | SECOND: { 5 | path: '/second', 6 | beforeEnter: async ({ dispatch }) => { 7 | await dispatch({ type: 'REDIRECTED' }) 8 | }, 9 | thunk() {}, 10 | }, 11 | }) 12 | 13 | createTest('redirect after enter', { 14 | SECOND: { 15 | path: '/second', 16 | thunk: ({ dispatch }) => dispatch({ type: 'REDIRECTED' }), 17 | onComplete() {}, 18 | }, 19 | }) 20 | 21 | createTest('redirect before enter (on firstRoute)', { 22 | FIRST: { 23 | path: '/first', 24 | beforeEnter: ({ dispatch }) => dispatch({ type: 'REDIRECTED' }), 25 | thunk() {}, 26 | }, 27 | }) 28 | 29 | createTest('redirect after enter (on firstRoute)', { 30 | FIRST: { 31 | path: '/first', 32 | thunk: ({ dispatch }) => dispatch({ type: 'REDIRECTED' }), 33 | onComplete() {}, 34 | }, 35 | }) 36 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/routeLevelMiddleware.js: -------------------------------------------------------------------------------- 1 | import { 2 | transformAction, 3 | call, 4 | enter, 5 | changePageTitle, 6 | compose, 7 | } from '@respond-framework/rudy' 8 | 9 | import createTest from '../../__helpers__/createTest' 10 | 11 | createTest( 12 | 'routes can specify route.middleware array to override global middleware', 13 | { 14 | SECOND: { 15 | path: '/second', 16 | onTransition: () => 'SUCCESS!', 17 | middleware: [ 18 | transformAction, 19 | enter, 20 | changePageTitle(), 21 | () => async (req, next) => { 22 | const res = await next() 23 | 24 | expect(res).toEqual({ type: 'SECOND_COMPLETE', payload: 'SUCCESS!' }) 25 | expect(req.getState().title).toEqual('SECOND_COMPLETE - "SUCCESS!"') 26 | 27 | return res 28 | }, 29 | call('onTransition'), 30 | () => () => 'foo', 31 | ], 32 | }, 33 | }, 34 | ) 35 | 36 | createTest( 37 | 'routes can specify route.middleware as function to override global middleware', 38 | { 39 | SECOND: { 40 | path: '/second', 41 | onTransition: () => 'SUCCESS!', 42 | middleware: (api, killOnRedirect) => 43 | compose( 44 | [ 45 | transformAction, 46 | call('onTransition'), 47 | enter, 48 | changePageTitle(), 49 | () => () => 'foo', 50 | ], 51 | api, 52 | killOnRedirect, 53 | ), 54 | }, 55 | }, 56 | ) 57 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/serverRedirect.js: -------------------------------------------------------------------------------- 1 | import { doesRedirect } from '@respond-framework/rudy' 2 | import createTest from '../../__helpers__/createTest' 3 | 4 | jest.mock('@respond-framework/utils/isServer', () => () => true) 5 | 6 | createTest( 7 | 'redirect on server not dispatched; instead redirect info returned', 8 | { 9 | FIRST: { 10 | path: '/first', 11 | beforeEnter() {}, 12 | thunk: ({ dispatch }) => { 13 | dispatch({ type: 'REDIRECTED' }) 14 | }, 15 | }, 16 | }, 17 | ) 18 | 19 | createTest( 20 | 'doesRedirect() on server returns true and if passed callback calls callback if true', 21 | { 22 | FIRST: { 23 | path: '/first', 24 | beforeEnter() {}, 25 | thunk: async ({ dispatch }) => { 26 | await dispatch({ type: 'PATHLESS' }) // redirect to a pathless route 27 | }, 28 | }, 29 | PATHLESS: { 30 | thunk: async ({ dispatch }) => { 31 | await dispatch(() => 32 | // and use anonymous thunk to confirm complex redirects work with the `serverRedirect` middleware 33 | ({ type: 'REDIRECTED' }), 34 | ) 35 | }, 36 | }, 37 | }, 38 | [], 39 | async ({ firstResponse }) => { 40 | expect(doesRedirect(firstResponse)).toEqual(true) 41 | 42 | const redirectFunc = jest.fn() 43 | doesRedirect(firstResponse, redirectFunc) 44 | 45 | expect(redirectFunc).toHaveBeenCalledWith(302, '/redirected', { 46 | location: { kind: 'replace', status: 302, url: '/redirected' }, 47 | status: 302, 48 | type: 'REDIRECTED', 49 | url: '/redirected', 50 | }) 51 | 52 | const expressResponse = { redirect: jest.fn() } 53 | doesRedirect(firstResponse, expressResponse) 54 | 55 | expect(expressResponse.redirect).toHaveBeenCalledWith(302, '/redirected') 56 | }, 57 | ) 58 | -------------------------------------------------------------------------------- /packages/integration-tests/__tests__/integration/uninheritedHistory.js: -------------------------------------------------------------------------------- 1 | import { History } from '@respond-framework/rudy' 2 | import createTest from '../../__helpers__/createTest' 3 | 4 | createTest( 5 | 'History class can be used directly', 6 | { 7 | FIRST: '/', 8 | SECOND: '/second', 9 | }, 10 | { 11 | createHistory: (routes, options) => new History(routes, options), 12 | }, 13 | ) 14 | -------------------------------------------------------------------------------- /packages/integration-tests/jest.config.js: -------------------------------------------------------------------------------- 1 | const rootConfig = require('../../jest.config') 2 | 3 | module.exports = { 4 | ...rootConfig, 5 | verbose: false, 6 | silent: true, 7 | testURL: 'http://localhost:3000', 8 | testMatch: ['**/__tests__/integration/**/*.js?(x)'], 9 | testEnvironment: 'jsdom', 10 | setupFilesAfterEnv: ['./__test-helpers__/setupJest.js'], 11 | setupFiles: ['jest-localstorage-mock'], 12 | testPathIgnorePatterns: ['/node_modules/', '.eslintrc.js'], 13 | moduleNameMapper: { 14 | '^@respond-framework\\/([^/]+)\\/(.*)': 15 | '/../../packages/$1/src/$2', 16 | '^@respond-framework\\/([^/]+)': '/../../packages/$1/src', 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /packages/integration-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "integration-tests", 3 | "version": "1.0.1-test.9", 4 | "description": "Integration tests for the respond framework", 5 | "main": "index.js", 6 | "repository": "https://github.com/respond-framework/rudy/tree/master/packages/integration-tests", 7 | "author": "James Gilmore, Daniel Playfair Cal", 8 | "license": "MIT", 9 | "private": true, 10 | "scripts": { 11 | "test": "jest", 12 | "prettier": "prettier", 13 | "is-pretty": "prettier --ignore-path=../../config/.prettierignore '**/*' --list-different", 14 | "prettify": "prettier --ignore-path=../../config/.prettierignore '**/*' --write" 15 | }, 16 | "dependencies": { 17 | "@respond-framework/rudy": "^0.1.1-test.9", 18 | "react": "^16.4.2", 19 | "react-redux": "^7.1.0", 20 | "react-test-renderer": "^16.4.2", 21 | "redux": "^4.0.4", 22 | "redux-thunk": "^2.3.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/middleware-change-page-title/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [1.0.1-test.4](https://github.com/respond-framework/rudy/tree/master/packages/middleware-change-page-title/compare/@respond-framework/middleware-change-page-title@1.0.1-test.3...@respond-framework/middleware-change-page-title@1.0.1-test.4) (2020-03-27) 7 | 8 | **Note:** Version bump only for package @respond-framework/middleware-change-page-title 9 | 10 | 11 | 12 | 13 | 14 | ## [1.0.1-test.3](https://github.com/respond-framework/rudy/tree/master/packages/middleware-change-page-title/compare/@respond-framework/middleware-change-page-title@1.0.1-test.2...@respond-framework/middleware-change-page-title@1.0.1-test.3) (2019-10-23) 15 | 16 | 17 | ### Code Refactoring 18 | 19 | * remove flow from builds ([332e730](https://github.com/respond-framework/rudy/tree/master/packages/middleware-change-page-title/commit/332e730)) 20 | 21 | 22 | ### BREAKING CHANGES 23 | 24 | * flow types are no longer available in published 25 | packages 26 | 27 | 28 | 29 | 30 | 31 | ## [1.0.1-test.2](https://github.com/respond-framework/rudy/tree/master/packages/middleware-change-page-title/compare/@respond-framework/middleware-change-page-title@1.0.1-test.1...@respond-framework/middleware-change-page-title@1.0.1-test.2) (2019-10-16) 32 | 33 | **Note:** Version bump only for package @respond-framework/middleware-change-page-title 34 | 35 | 36 | 37 | 38 | 39 | ## 1.0.1-test.1 (2019-04-15) 40 | 41 | **Note:** Version bump only for package @respond-framework/middleware-change-page-title 42 | 43 | 44 | 45 | 46 | 47 | ## 1.0.1-test.0 (2018-11-05) 48 | 49 | **Note:** Version bump only for package @respond-framework/middleware-change-page-title 50 | -------------------------------------------------------------------------------- /packages/middleware-change-page-title/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@respond-framework/middleware-change-page-title", 3 | "version": "1.0.1-test.4", 4 | "description": "Rudy middleware to synchronise the browser page title with redux state", 5 | "main": "cjs/index.js", 6 | "module": "es/index.js", 7 | "rudy-src-main": "src/index.js", 8 | "repository": "https://github.com/respond-framework/rudy/tree/master/packages/middleware-change-page-title", 9 | "contributors": [ 10 | "James Gilmore ", 11 | "Daniel Playfair Cal " 12 | ], 13 | "license": "MIT", 14 | "private": false, 15 | "publishConfig": { 16 | "access": "public" 17 | }, 18 | "files": [ 19 | "cjs", 20 | "es" 21 | ], 22 | "scripts": { 23 | "prepare": "yarn run build:cjs && yarn run build:es", 24 | "build:cjs": "babel --root-mode upward --source-maps true src -d cjs", 25 | "build:es": "BABEL_ENV=es babel --root-mode upward --source-maps true src -d es", 26 | "build": "yarn run build:cjs && yarn run build:es", 27 | "clean": "rimraf cjs && rimraf es", 28 | "prettier": "prettier", 29 | "is-pretty": "prettier --ignore-path=../../config/.prettierignore '**/*' --list-different", 30 | "prettify": "prettier --ignore-path=../../config/.prettierignore '**/*' --write", 31 | "test": "jest --config ../../jest.config.js --rootDir ." 32 | }, 33 | "dependencies": { 34 | "@respond-framework/utils": "^0.1.1-test.4" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/middleware-change-page-title/src/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | isServer as defaultIsServer, 3 | createSelector as defaultCreateSelector, 4 | } from '@respond-framework/utils' 5 | 6 | export default (options) => { 7 | const { 8 | title: keyOrSelector, 9 | isServer = defaultIsServer, 10 | createSelector = defaultCreateSelector, 11 | setTitle = (title) => { 12 | // eslint-disable-next-line no-undef 13 | window.document.title = title 14 | }, 15 | } = options || {} 16 | const selectTitleState = createSelector( 17 | 'title', 18 | keyOrSelector, 19 | ) 20 | 21 | return (api) => async (req, next) => { 22 | const title = selectTitleState(api.getState()) 23 | 24 | if (!isServer() && typeof title === 'string') { 25 | setTitle(title) 26 | } 27 | 28 | return next() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@respond-framework/react", 3 | "version": "0.1.1-test.5", 4 | "description": "React component to create links which dispatch rudy routing actions", 5 | "main": "cjs/index.js", 6 | "module": "es/index.js", 7 | "rudy-src-main": "src/index.js", 8 | "repository": "https://github.com/respond-framework/rudy/tree/master/packages/react", 9 | "author": "James Gilmore", 10 | "license": "MIT", 11 | "private": false, 12 | "publishConfig": { 13 | "access": "public" 14 | }, 15 | "files": [ 16 | "cjs", 17 | "es" 18 | ], 19 | "scripts": { 20 | "prepare": "yarn run build:cjs && yarn run build:es", 21 | "build:cjs": "babel --root-mode upward --source-maps true src -d cjs", 22 | "build:es": "BABEL_ENV=es babel --root-mode upward --source-maps true src -d es", 23 | "build": "yarn run build:cjs && yarn run build:es", 24 | "clean": "rimraf cjs && rimraf es", 25 | "prettier": "prettier", 26 | "is-pretty": "prettier --ignore-path=../../config/.prettierignore '**/*' --list-different", 27 | "prettify": "prettier --ignore-path=../../config/.prettierignore '**/*' --write" 28 | }, 29 | "dependencies": { 30 | "resolve-pathname": "^2.2.0" 31 | }, 32 | "peerDependencies": { 33 | "@respond-framework/rudy": "^0.1.0", 34 | "prop-types": "^15.6.2", 35 | "react": "^15 || ^16", 36 | "react-redux": "^5.0.0 || ^6.0.0 || ^7.0.0", 37 | "redux": "^3 || ^4" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/react/src/index.js: -------------------------------------------------------------------------------- 1 | export { RudyProvider, RudyConsumer } from './provider' 2 | export { Link, NavLink } from './link' 3 | -------------------------------------------------------------------------------- /packages/react/src/provider.jsx: -------------------------------------------------------------------------------- 1 | import React, { createContext } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const RudyContext = createContext('rudy-link') 5 | 6 | export const RudyProvider = ({ api, children }) => ( 7 | {children} 8 | ) 9 | 10 | RudyProvider.propTypes = { 11 | children: PropTypes.node.isRequired, 12 | // eslint-disable-next-line react/forbid-prop-types 13 | api: PropTypes.object.isRequired, 14 | } 15 | 16 | export const RudyConsumer = RudyContext.Consumer 17 | -------------------------------------------------------------------------------- /packages/react/src/utils/handlePress.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { redirect } from '@respond-framework/rudy' 3 | import type { Routes, ReceivedAction } from '@respond-framework/rudy' 4 | 5 | export type OnClick = false | ((SyntheticEvent) => ?boolean) 6 | export default ( 7 | action: ?ReceivedAction, 8 | routes: Routes, 9 | shouldDispatch: boolean, 10 | dispatch: Function, 11 | onClick?: ?OnClick, 12 | target: ?string, 13 | isRedirect?: boolean, 14 | fullUrl: string, 15 | history: Object, 16 | e: SyntheticEvent, 17 | ) => { 18 | const prevented = e.defaultPrevented 19 | const notModified = !isModified(e) 20 | let shouldGo = true 21 | 22 | if (onClick) { 23 | shouldGo = onClick(e) !== false // onClick can return false to prevent dispatch 24 | } 25 | 26 | if (!target && e && e.preventDefault && notModified) { 27 | e.preventDefault() 28 | } 29 | 30 | if ( 31 | action && 32 | shouldGo && 33 | shouldDispatch && 34 | !target && 35 | !prevented && 36 | notModified && 37 | e.button === 0 38 | ) { 39 | action = isRedirect ? redirect(action) : action 40 | return dispatch(action) 41 | } 42 | 43 | if (!action && !target && fullUrl.indexOf('http') === 0) { 44 | window.location.href = fullUrl 45 | } 46 | return undefined 47 | } 48 | 49 | const isModified = (e: Object) => 50 | !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) 51 | -------------------------------------------------------------------------------- /packages/react/src/utils/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default as toUrlAndAction } from './toUrlAndAction' 3 | export { default as handlePress } from './handlePress' 4 | export { default as preventDefault } from './preventDefault' 5 | 6 | export type { To } from './toUrlAndAction' 7 | export type { OnClick } from './handlePress' 8 | -------------------------------------------------------------------------------- /packages/react/src/utils/preventDefault.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export default (e: SyntheticEvent) => 4 | e && e.preventDefault && e.preventDefault() 5 | -------------------------------------------------------------------------------- /packages/react/src/utils/toUrlAndAction.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import resolvePathname from 'resolve-pathname' 3 | import { 4 | actionToUrl, 5 | toAction, 6 | findBasename, 7 | stripBasename, 8 | } from '@respond-framework/rudy' 9 | import type { Routes, Options } from '@respond-framework/rudy' 10 | 11 | export type To = string | Array | Object 12 | 13 | export default ( 14 | to?: ?To, 15 | routes: Routes, 16 | basename: ?string = '', 17 | currentPathname: string, 18 | options: Options, 19 | ): Object => { 20 | const { basenames } = options 21 | let url = '' 22 | let action 23 | 24 | if (!to) { 25 | url = '#' 26 | } 27 | if (to && typeof to === 'string') { 28 | url = to 29 | } else if (Array.isArray(to)) { 30 | if (to[0].charAt(0) === '/') { 31 | basename = to.shift() 32 | } 33 | 34 | url = `/${to.join('/')}` 35 | } else if (typeof to === 'object') { 36 | action = to 37 | 38 | try { 39 | ;({ url } = actionToUrl(action, { routes, options })) 40 | basename = action.basename || basename || '' 41 | } catch (e) { 42 | if (process.env.NODE_ENV === 'development') { 43 | // eslint-disable-next-line no-console 44 | console.warn('[rudy/Link] could not create path from action:', action) 45 | } 46 | 47 | url = '#' 48 | } 49 | } 50 | 51 | const bn = basenames && findBasename(url, basenames) 52 | 53 | if (bn) { 54 | basename = bn 55 | url = stripBasename(url, bn) 56 | } 57 | 58 | if (url.charAt(0) === '#') { 59 | url = `${currentPathname}${url}` 60 | } else if (url.charAt(0) !== '/') { 61 | url = resolvePathname(url, currentPathname) 62 | } 63 | 64 | const isExternal = url.indexOf('http') === 0 65 | 66 | if (!action && !isExternal) { 67 | const api = { routes, options } 68 | action = toAction(api, url) 69 | } 70 | 71 | if (basename) { 72 | action = { ...action, basename } 73 | } 74 | 75 | const fullUrl = isExternal ? url : basename + url 76 | return { fullUrl, action } 77 | } 78 | -------------------------------------------------------------------------------- /packages/rudy/.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 | -------------------------------------------------------------------------------- /packages/rudy/.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 | -------------------------------------------------------------------------------- /packages/rudy/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | coverage 3 | 4 | -------------------------------------------------------------------------------- /packages/rudy/.npmignore: -------------------------------------------------------------------------------- 1 | coverage 2 | __test-helpers__ 3 | __tests__ 4 | docs 5 | flow-typed 6 | src 7 | *.log 8 | examples 9 | node_modules 10 | 11 | .babelrc 12 | .codeclimate.yml 13 | .editorconfig 14 | .eslintrc.js 15 | .travis.yml 16 | wallaby.js 17 | webpack.config.js 18 | .eslintignore 19 | .flowconfig 20 | *.png 21 | -------------------------------------------------------------------------------- /packages/rudy/docs-old/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman 2 | -------------------------------------------------------------------------------- /packages/rudy/docs-old/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 | 12 | - https://github.com/faceyspacey/react-universal-component 13 | - https://github.com/faceyspacey/webpack-flush-chunks 14 | - https://github.com/faceyspacey/extract-css-chunks-webpack-plugin 15 | 16 | _Redux First Router_ will allow you to specify chunks in your `routesMap` and 17 | your `` components will have a `prefetch` prop you can set to `true` to 18 | prefetch associated chunks. An imperative API via the instance ref will exist 19 | too: 20 | 21 | ```js 22 | const routesMap = { 23 | FOO: { path: '/foo/:bar', chunks: [import('./Foo')] } 24 | } 25 | 26 | // declarative API: 27 | 28 | 29 | 30 | // imperative API: 31 | instance = i} href='/foo/123' /> 32 | instance.prefetch() 33 | ``` 34 | -------------------------------------------------------------------------------- /packages/rudy/docs-old/prior-art.md: -------------------------------------------------------------------------------- 1 | # Prior Art 2 | 3 | The following packages attempt in similar ways to reconcile the browser 4 | `history` with redux actions and state: 5 | 6 | - **redux-little-router** 7 | https://github.com/FormidableLabs/redux-little-router 8 | A tiny router for Redux that lets the URL do the talking. 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 13 | creators on navigation 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. 18 | If you are looking to read and write changes to the address bar via Redux this 19 | might be for you. 20 | 21 | - **Redux Unity Router** 22 | https://github.com/TimeRaider/redux-unity-router 23 | Simple routing for your redux application. The main purpose of this router is 24 | to mirror your browser history to the redux store and help you easily declare 25 | routes. 26 | 27 | ## More 28 | 29 | To check out even more, here's a complete list automatically scraped from NPM: 30 | 31 | https://github.com/markerikson/redux-ecosystem-links/blob/master/routing.md 32 | -------------------------------------------------------------------------------- /packages/rudy/docs-old/redux-first-router-flow-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/respond-framework/rudy/177d8f354981c8ab8fbc890c5828cd3a5d8a5162/packages/rudy/docs-old/redux-first-router-flow-chart.png -------------------------------------------------------------------------------- /packages/rudy/docs-old/scroll-restoration.md: -------------------------------------------------------------------------------- 1 | # Scroll Restoration 2 | 3 | Complete Scroll restoration and hash `#links` handling is addressed primarily by 4 | one of our companion packages: 5 | [redux-first-router-restore-scroll](https://github.com/faceyspacey/redux-first-router-restore-scroll) 6 | _(we like to save you the bytes sent to clients if you don't need it)_. In most 7 | cases all you need to do is: 8 | 9 | Example: 10 | 11 | ```js 12 | import restoreScroll from 'redux-first-router-restore-scroll' 13 | connectRoutes(history, routesMap, { restoreScroll: restoreScroll() }) 14 | ``` 15 | 16 | Visit 17 | [redux-first-router-restore-scroll](https://github.com/faceyspacey/redux-first-router-restore-scroll) 18 | for more information and advanced usage. 19 | 20 | ## Scroll Restoration for Elements other than `window` 21 | 22 | We got you covered. Please checkout 23 | [redux-first-router-scroll-container](https://github.com/faceyspacey/redux-first-router-scroll-container). 24 | 25 | ## Scroll Restoration for React Native 26 | 27 | We got you covered! Please checkout 28 | [redux-first-router-scroll-container-native](https://github.com/faceyspacey/redux-first-router-scroll-container-native). 29 | -------------------------------------------------------------------------------- /packages/rudy/flow-typed/npm/babel-plugin-transform-flow-strip-types_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 147af63ef1970e66fe8225d36f3df862 2 | // flow-typed version: <>/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 | -------------------------------------------------------------------------------- /packages/rudy/flow-typed/npm/babel-preset-es2015_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 092160e82da2f82dbd18af7c99d36eb6 2 | // flow-typed version: <>/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 | -------------------------------------------------------------------------------- /packages/rudy/flow-typed/npm/babel-preset-react_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 58ccb0b319de0258d3c76c9810681a43 2 | // flow-typed version: <>/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 | -------------------------------------------------------------------------------- /packages/rudy/flow-typed/npm/babel-preset-stage-0_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 88c3e8568a66c7e895ff3810de45e6ca 2 | // flow-typed version: <>/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 | -------------------------------------------------------------------------------- /packages/rudy/flow-typed/npm/eslint-plugin-flow-vars_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: aa450ea9af9fdcae473ebaa6e117bfda 2 | // flow-typed version: <>/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 | -------------------------------------------------------------------------------- /packages/rudy/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 | -------------------------------------------------------------------------------- /packages/rudy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@respond-framework/rudy", 3 | "version": "0.1.1-test.9", 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": "es/index.js", 7 | "rudy-src-main": "src/index.js", 8 | "files": [ 9 | "dist", 10 | "es" 11 | ], 12 | "scripts": { 13 | "prepare": "yarn run build:cjs && yarn run build:es", 14 | "build:cjs": "babel --root-mode upward --source-maps true src -d dist", 15 | "build:es": "BABEL_ENV=es babel --root-mode upward --source-maps true src -d es", 16 | "build": "yarn run build:cjs && yarn run build:es", 17 | "flow-watch": "clear; printf \"\\033[3J\" & npm run flow & fswatch -o ./ | xargs -n1 -I{} sh -c 'clear; printf \"\\033[3J\" && npm run flow'", 18 | "flow": "flow; test $? -eq 0 -o $? -eq 2", 19 | "clean": "rimraf dist && rimraf es && rimraf coverage", 20 | "prettier": "prettier", 21 | "is-pretty": "prettier --ignore-path=../../config/.prettierignore '**/*' --list-different", 22 | "prettify": "prettier --ignore-path=../../config/.prettierignore '**/*' --write", 23 | "test": "jest --config ../../jest.config.js --rootDir ." 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/respond-framework/rudy/tree/master/packages/rudy" 28 | }, 29 | "author": "James Gillmore ", 30 | "license": "MIT", 31 | "publishConfig": { 32 | "access": "public" 33 | }, 34 | "bugs": { 35 | "url": "https://github.com/respond-framework/rudy/issues" 36 | }, 37 | "homepage": "https://github.com/respond-framework/rudy", 38 | "dependencies": { 39 | "@respond-framework/middleware-change-page-title": "^1.0.1-test.4", 40 | "@respond-framework/scroll-restorer": "^0.1.0-test.4", 41 | "@respond-framework/utils": "^0.1.1-test.4", 42 | "path-to-regexp": "^2.1.0", 43 | "prop-types": "^15.6.0", 44 | "qs": "^6.5.1", 45 | "resolve-pathname": "^2.2.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/rudy/src/actions/addRoutes.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { ADD_ROUTES } from '../types' 4 | import type { Routes } from '../flow-types' 5 | 6 | export default (routes: Routes, formatRoute: ?Function) => ({ 7 | type: ADD_ROUTES, 8 | payload: { routes, formatRoute }, 9 | }) 10 | 11 | // NOTE: see `src/utils/formatRoutes.js` for implementation of corresponding pathlessRoute 12 | -------------------------------------------------------------------------------- /packages/rudy/src/actions/changeBasename.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Action } from '../flow-types' 3 | import { CHANGE_BASENAME } from '../types' 4 | 5 | export default (basename: string, action: ?Action): Object => { 6 | if (!action) { 7 | return { 8 | type: CHANGE_BASENAME, 9 | payload: { basename }, 10 | } 11 | } 12 | 13 | return { ...action, basename } 14 | } 15 | 16 | // NOTE: the first form with type `CHANGE_BASENAME` will trigger the pathlessRoute middleware 17 | // see `src/utils/formatRoutes.js` for implementation of corresponding pathlessRoute 18 | -------------------------------------------------------------------------------- /packages/rudy/src/actions/clearCache.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { CLEAR_CACHE } from '../types' 3 | 4 | export default (invalidator?: any, options?: Object) => ({ 5 | type: CLEAR_CACHE, 6 | payload: { invalidator, options }, 7 | }) 8 | 9 | // NOTE: see `src/utils/formatRoutes.js` for implementation of corresponding pathlessRoute 10 | -------------------------------------------------------------------------------- /packages/rudy/src/actions/confirm.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { CONFIRM } from '../types' 3 | 4 | export default (canLeave?: boolean = true) => ({ 5 | type: CONFIRM, 6 | payload: { canLeave }, 7 | }) 8 | 9 | // NOTE: see `src/utils/formatRoutes.js` for implementation of corresponding pathlessRoute 10 | -------------------------------------------------------------------------------- /packages/rudy/src/actions/index.js: -------------------------------------------------------------------------------- 1 | export { default as redirect } from './redirect' 2 | export { default as notFound } from './notFound' 3 | export { default as addRoutes } from './addRoutes' 4 | export { default as changeBasename } from './changeBasename' 5 | export { default as clearCache } from './clearCache' 6 | export { default as confirm } from './confirm' 7 | 8 | export { 9 | push, 10 | replace, 11 | jump, 12 | back, 13 | next, 14 | reset, 15 | set, 16 | setParams, 17 | setQuery, 18 | setState, 19 | setHash, 20 | setBasename, 21 | } from './history' 22 | -------------------------------------------------------------------------------- /packages/rudy/src/actions/notFound.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export default (state: ?Object, type: ?string) => 4 | ({ 5 | type: type || 'NOT_FOUND', // type not meant for user to supply; it's passed by generated action creators 6 | state, 7 | }: { state: ?Object, type: string }) 8 | -------------------------------------------------------------------------------- /packages/rudy/src/actions/redirect.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Action } from '../flow-types' 3 | 4 | export default (action: Action, status: number = 302) => ({ 5 | ...action, 6 | location: { 7 | // $FlowFixMe 8 | ...action.location, 9 | status, 10 | kind: 'replace', 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /packages/rudy/src/core/createHistory.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import BrowserHistory from '../history/BrowserHistory' 3 | import MemoryHistory from '../history/MemoryHistory' 4 | import { supportsDom } from '../history/utils' 5 | import type { Routes, Options } from '../flow-types' 6 | 7 | export default (routes: Routes, opts: Options = {}) => 8 | supportsDom() && opts.testBrowser !== false 9 | ? new BrowserHistory(routes, opts) 10 | : new MemoryHistory(routes, opts) 11 | -------------------------------------------------------------------------------- /packages/rudy/src/core/index.js: -------------------------------------------------------------------------------- 1 | export { default as createRouter } from './createRouter' 2 | export { default as createRequest, Request as _Request } from './createRequest' 3 | export { default as createHistory } from './createHistory' 4 | export { default as compose } from './compose' 5 | export { 6 | default as createReducer, 7 | createInitialState, 8 | createPrev, 9 | createPrevEmpty, 10 | } from './createReducer' 11 | -------------------------------------------------------------------------------- /packages/rudy/src/createScene/utils/formatRoute.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { isNotFound, typeToScene, formatRoute } from '../../utils' 3 | import type { RouteInput, Routes } from '../../flow-types' 4 | 5 | export default ( 6 | r: RouteInput, 7 | type: string, 8 | routes: Routes, 9 | formatter: ?Function, 10 | ) => { 11 | const route = formatRoute(r, type, routes, formatter) 12 | 13 | route.scene = typeToScene(type) 14 | // set default path for NOT_FOUND actions if necessary 15 | if (!route.path && isNotFound(type)) { 16 | route.path = route.scene 17 | ? // $FlowFixMe 18 | `/${r.scene.toLowerCase()}/not-found` 19 | : '/not-found' 20 | } 21 | 22 | return route 23 | } 24 | -------------------------------------------------------------------------------- /packages/rudy/src/createScene/utils/handleError.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export default (o: Object, type: string, basename: ?string) => 3 | o && o.error ? { type, basename, ...o } : { type, basename, error: o } 4 | -------------------------------------------------------------------------------- /packages/rudy/src/createScene/utils/index.js: -------------------------------------------------------------------------------- 1 | export { default as camelCase } from './camelCase' 2 | export { default as handleError } from './handleError' 3 | export { default as logExports } from './logExports' 4 | export { default as makeActionCreator } from './makeActionCreator' 5 | export { default as formatRoute } from './formatRoute' 6 | -------------------------------------------------------------------------------- /packages/rudy/src/createScene/utils/logExports.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { typeToScene } from '../../utils' 3 | 4 | export default (types, actions, routes, options) => { 5 | const opts = { ...options } 6 | opts.scene = typeToScene(Object.keys(routes)[0]) 7 | delete opts.logExports 8 | 9 | const optsString = JSON.stringify(opts) 10 | .replace(/"scene":/, 'scene: ') 11 | .replace(/"basename":/, 'basename: ') 12 | .replace(/"/g, "'") 13 | .replace('{', '{ ') 14 | .replace('}', ' }') 15 | .replace(/,/g, ', ') 16 | 17 | let t = '' 18 | for (const type in types) t += `\n\t${type},` 19 | 20 | let a = '' 21 | for (const action in actions) a += `\n\t${action},` 22 | 23 | // destructure createActions() 24 | let exports = `const { types, actions } = createScene(routes, ${optsString})` 25 | exports += `\n\nconst { ${t.slice(0, -1)}\n} = types` 26 | exports += `\n\nconst { ${a.slice(0, -1)}\n} = actions` 27 | 28 | // types exports 29 | exports += `\n\nexport {${t}` 30 | exports = `${exports.slice(0, -1)}\n}` 31 | 32 | // actions exports 33 | exports += `\n\nexport {${a}` 34 | exports = `${exports.slice(0, -1)}\n}` 35 | 36 | if (process.env.NODE_ENV !== 'test') console.log(exports) 37 | return exports 38 | } 39 | -------------------------------------------------------------------------------- /packages/rudy/src/history/MemoryHistory.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { isServer } from '@respond-framework/utils' 3 | import History from './History' 4 | import { restoreHistory, saveHistory } from './utils' 5 | import { toEntries } from '../utils' 6 | 7 | // Even though this is used primarily in environments without `window` (server + React Native), 8 | // it's also used as a fallback in browsers lacking the `history` API (<=IE9). In that now rare case, 9 | // the URL won't change once you enter the site, however, if you forward or back out of the site 10 | // we restore entries from `sessionStorage`. So essentially the application behavior is identical 11 | // to browsers with `history` except the URL doesn't change. 12 | 13 | // `initialEntries` can be: 14 | // [path, path, etc] or: path 15 | // [action, action, etc] or: action 16 | // [[path, state, key?], [path, state, key?], etc] or: [path, state, key?] 17 | // or any combination of different kinds 18 | 19 | export default class MemoryHistory extends History { 20 | _restore() { 21 | const { options: opts } = this 22 | const { initialIndex: i, initialEntries: ents, initialN: n } = opts 23 | const useSession = !isServer() && opts.testBrowser !== false 24 | 25 | opts.restore = opts.restore || (useSession && restoreHistory) 26 | opts.save = opts.save || (useSession && saveHistory) 27 | 28 | return opts.restore ? opts.restore(this) : toEntries(this, ents, i, n) // when used as a browser fallback, we restore from sessionStorage 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/rudy/src/history/utils/index.js: -------------------------------------------------------------------------------- 1 | export { supportsDom } from './supports' 2 | 3 | export { 4 | addPopListener, 5 | removePopListener, 6 | isExtraneousPopEvent, 7 | } from './popListener' 8 | 9 | export { 10 | saveHistory, 11 | restoreHistory, 12 | pushState, 13 | replaceState, 14 | go, 15 | getCurrentIndex, 16 | get, 17 | clear, 18 | } from './sessionStorage' 19 | -------------------------------------------------------------------------------- /packages/rudy/src/history/utils/popListener.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* eslint-env browser */ 3 | export const addPopListener = (onPop: Function, onHash: Function) => { 4 | const useHash = !supportsPopStateOnHashChange() 5 | 6 | addEventListener(window, 'popstate', onPop) 7 | if (useHash) addEventListener(window, 'hashchange', onHash) 8 | } 9 | 10 | export const removePopListener = (onPop: Function, onHash: Function) => { 11 | const useHash = !supportsPopStateOnHashChange() 12 | 13 | removeEventListener(window, 'popstate', onPop) 14 | if (useHash) removeEventListener(window, 'hashchange', onHash) 15 | } 16 | 17 | const addEventListener = (node, event, listener) => 18 | node.addEventListener 19 | ? node.addEventListener(event, listener, false) 20 | : node.attachEvent(`on${event}`, listener) 21 | 22 | const removeEventListener = (node, event, listener) => 23 | node.removeEventListener 24 | ? node.removeEventListener(event, listener, false) 25 | : node.detachEvent(`on${event}`, listener) 26 | 27 | // Returns true if browser fires popstate on hash change. IE10 and IE11 do not. 28 | const supportsPopStateOnHashChange = () => 29 | window.navigator.userAgent.indexOf('Trident') === -1 30 | 31 | /** 32 | * Returns true if a given popstate event is an extraneous WebKit event. 33 | * Accounts for the fact that Chrome on iOS fires real popstate events 34 | * containing undefined state when pressing the back button. 35 | */ 36 | export const isExtraneousPopEvent = (event: Object) => 37 | event.state === undefined && navigator.userAgent.indexOf('CriOS') === -1 38 | -------------------------------------------------------------------------------- /packages/rudy/src/history/utils/supports.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* global window */ 3 | 4 | // eslint-disable-next-line import/prefer-default-export 5 | export const supportsDom = () => 6 | !!( 7 | typeof window !== 'undefined' && 8 | window.document && 9 | window.document.createElement 10 | ) 11 | -------------------------------------------------------------------------------- /packages/rudy/src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as supports from './history/utils/supports' 3 | import * as popListener from './history/utils/popListener' 4 | import * as sessionStorage from './history/utils/sessionStorage' 5 | 6 | export * from './core' 7 | export { default as createScene } from './createScene' 8 | 9 | export { default as History } from './history/History' 10 | export { 11 | get as getSessionStorage, 12 | clear as clearSessionStorage, 13 | getCurrentIndex, 14 | } from './history/utils' 15 | export { default as MemoryHistory } from './history/MemoryHistory' 16 | export { default as BrowserHistory } from './history/BrowserHistory' 17 | 18 | export const utils = { 19 | supports, 20 | popListener, 21 | sessionStorage, 22 | } 23 | 24 | export * from './types' 25 | export * from './actions' 26 | export * from './utils' 27 | export * from './middleware' 28 | 29 | /** if you want to extend History, here is how you do it: 30 | 31 | import History from '@respond-framework/rudy' 32 | 33 | class MyHistory extends History { 34 | push(path) { 35 | const location = this.createAction(path) 36 | // do something custom 37 | } 38 | } 39 | 40 | // usage: 41 | 42 | import { createRouter } from '@respond-framework/rudy' 43 | import { createHistory as creatHist } from '@respond-framework/rudy' 44 | 45 | const createHistory = (routes, opts) => { 46 | if (opts.someCondition) return new MyHistory(routes, opts) 47 | return creatHist(routes, opts) 48 | } 49 | 50 | const { middleware, reducer, firstRoute } = createRouter(routes, { createHistory }) 51 | 52 | */ 53 | -------------------------------------------------------------------------------- /packages/rudy/src/middleware/anonymousThunk.js: -------------------------------------------------------------------------------- 1 | export default ({ options }) => { 2 | const shouldTransition = options.shouldTransition 3 | 4 | options.shouldTransition = (action, api) => { 5 | if (typeof action === 'function') return true 6 | return shouldTransition(action, api) 7 | } 8 | 9 | return (req, next) => { 10 | if (typeof req.action !== 'function') return next() 11 | 12 | const thunk = req.action 13 | const thunkResult = Promise.resolve(thunk(req)) 14 | 15 | return thunkResult.then((action) => 16 | action && !action._dispatched ? req.dispatch(action) : action, 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/rudy/src/middleware/call/utils/autoDispatch.js: -------------------------------------------------------------------------------- 1 | export default (req, callback, route, name, isOptCb) => 2 | Promise.resolve(callback(req)).then((res) => 3 | tryDispatch(req, res, route, name, isOptCb), 4 | ) 5 | 6 | const tryDispatch = (req, res, route, name, isOptCb) => { 7 | if (res === false) return false 8 | const hasReturn = res === null || (res && !res._dispatched) // `res._dispatched` indicates it was manually dispatched 9 | 10 | if (hasReturn && isAutoDispatch(route, req.options, isOptCb)) { 11 | // if no dispatch was detected, and a result was returned, dispatch it automatically 12 | return Promise.resolve(req.dispatch(res)) 13 | } 14 | 15 | return res 16 | } 17 | 18 | const isAutoDispatch = (route, options, isOptCb) => 19 | isOptCb 20 | ? options.autoDispatch === undefined 21 | ? true 22 | : options.autoDispatch 23 | : route.autoDispatch !== undefined 24 | ? route.autoDispatch 25 | : options.autoDispatch === undefined 26 | ? true 27 | : options.autoDispatch 28 | -------------------------------------------------------------------------------- /packages/rudy/src/middleware/call/utils/enhanceRoutes.js: -------------------------------------------------------------------------------- 1 | export default (name, routes, options) => { 2 | for (const type in routes) { 3 | const route = routes[type] 4 | const cb = route[name] 5 | const callback = findCallback(name, routes, cb, route, options) 6 | if (callback) route[name] = callback 7 | } 8 | 9 | return routes 10 | } 11 | 12 | const findCallback = (name, routes, callback, route, options) => { 13 | if (typeof callback === 'function') { 14 | return callback 15 | } 16 | if (Array.isArray(callback)) { 17 | const callbacks = callback 18 | const pipeline = callbacks.map((cb) => (req, next) => { 19 | cb = findCallback(name, routes, cb, route) 20 | 21 | const prom = Promise.resolve(cb(req)) 22 | return prom.then(complete(next)) 23 | }) 24 | 25 | const killOnRedirect = !!route.path 26 | return options.compose( 27 | pipeline, 28 | null, 29 | killOnRedirect, 30 | ) 31 | } 32 | if (typeof callback === 'string') { 33 | const type = callback 34 | const inheritedRoute = routes[`${route.scene}/${type}`] || routes[type] 35 | const cb = inheritedRoute[name] 36 | return findCallback(name, routes, cb, inheritedRoute) 37 | } 38 | if (typeof route.inherit === 'string') { 39 | const type = route.inherit 40 | const inheritedRoute = routes[`${route.scene}/${type}`] || routes[type] 41 | const cb = inheritedRoute[name] 42 | return findCallback(name, routes, cb, inheritedRoute) 43 | } 44 | } 45 | 46 | const complete = (next) => (res) => next().then(() => res) 47 | -------------------------------------------------------------------------------- /packages/rudy/src/middleware/call/utils/index.js: -------------------------------------------------------------------------------- 1 | export { default as enhanceRoutes } from './enhanceRoutes' 2 | export { default as shouldCall } from './shouldCall' 3 | export { default as createCache } from './createCache' 4 | export { default as autoDispatch } from './autoDispatch' 5 | -------------------------------------------------------------------------------- /packages/rudy/src/middleware/call/utils/shouldCall.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { isServer } from '@respond-framework/utils' 3 | import type { LocationState } from '../../../flow-types' 4 | import { isHydrate } from '../../../utils' 5 | 6 | export default (name, route, req, { runOnServer, runOnHydrate }) => { 7 | if (!route[name] && !req.options[name]) return false 8 | 9 | if (isHydrate(req) && !runOnHydrate) return false 10 | 11 | if (isServer() && !runOnServer) return false 12 | 13 | return allowBoth 14 | } 15 | 16 | const allowBoth = { route: true, options: true } 17 | 18 | // If for instance, you wanted to allow each route to decide 19 | // whether to skip options callbacks, here's a simple way to do it: 20 | // 21 | // return { 22 | // options: !route.skipOpts, // if true, don't make those calls 23 | // route: true 24 | // } 25 | // 26 | // You also could choose to automatically trigger option callbacks only as a fallback: 27 | // 28 | // return { 29 | // options: !route[name], 30 | // route: !!route[name] 31 | // } 32 | -------------------------------------------------------------------------------- /packages/rudy/src/middleware/enter.js: -------------------------------------------------------------------------------- 1 | import { isServer } from '@respond-framework/utils' 2 | import { redirectShortcut } from '../utils' 3 | 4 | export default (api) => async (req, next) => { 5 | if (req.route.redirect) { 6 | return redirectShortcut(req) 7 | } 8 | 9 | const res = req.enter() // commit history + action to state 10 | 11 | // return early on `load` so rendering can happen ASAP 12 | // i.e. before `thunk` is called but after potentially async auth in `beforeEnter` 13 | if (req.getKind() === 'load' && !isServer() && api.resolveFirstRouteOnEnter) { 14 | setTimeout(() => { 15 | next().then(() => { 16 | req.ctx.busy = false 17 | }) 18 | }, 0) // insure callbacks like `onEnter` are called after `ReactDOM.render`, which should immediately be called after dispatching `firstRoute()` 19 | 20 | // in `createRouter.js` this flag will indicate to keep the pipeline still "busy" so 21 | // that dispatches in `thunk` and other callbacks after `enter` are treated as redirects, 22 | // as automatically happens throughout the pipeline. It becomes unbusy in the timeout above. 23 | req.clientLoadBusy = true 24 | return res 25 | } 26 | 27 | return res.then(next).then(() => res) 28 | } 29 | -------------------------------------------------------------------------------- /packages/rudy/src/middleware/index.js: -------------------------------------------------------------------------------- 1 | export { default as transformAction } from './transformAction' 2 | export { default as enter } from './enter' 3 | export { default as call, shouldCall } from './call' 4 | export { default as saveScroll } from './saveScroll' 5 | export { default as restoreScroll } from './restoreScroll' 6 | 7 | export { default as pathlessRoute } from './pathlessRoute' 8 | export { default as anonymousThunk } from './anonymousThunk' 9 | export { default as serverRedirect } from './serverRedirect' 10 | export { 11 | default as changePageTitle, 12 | } from '@respond-framework/middleware-change-page-title' 13 | -------------------------------------------------------------------------------- /packages/rudy/src/middleware/pathlessRoute.js: -------------------------------------------------------------------------------- 1 | import { call } from './index' 2 | 3 | export default (...names) => (api) => { 4 | names[0] = names[0] || 'thunk' 5 | names[1] = names[1] || 'onComplete' 6 | 7 | const middlewares = names.map((name) => 8 | call(name, { runOnServer: true, skipOpts: true }), 9 | ) 10 | 11 | const pipeline = api.options.compose( 12 | middlewares, 13 | api, 14 | ) 15 | 16 | // Registering is currently only used when core features (like the 17 | // `addRoutes` action creator) depend on the middleware being available. 18 | // See `utils/formatRoutes.js` for how `has` is used to throw 19 | // errors when not available. 20 | api.register('pathlessRoute') 21 | 22 | return (req, next) => { 23 | const { route } = req 24 | const isPathless = route && !route.path 25 | 26 | if (isPathless && hasCallback(route, names)) { 27 | if (route.dispatch !== false) { 28 | req.action = req.commitDispatch(req.action) 29 | } 30 | 31 | return pipeline(req).then((res) => res || req.action) 32 | } 33 | 34 | return next() 35 | } 36 | } 37 | 38 | const hasCallback = (route, names) => 39 | names.find((name) => typeof route[name] === 'function') 40 | -------------------------------------------------------------------------------- /packages/rudy/src/middleware/restoreScroll.js: -------------------------------------------------------------------------------- 1 | export default (api) => { 2 | const { scrollRestorer } = api 3 | if (scrollRestorer) { 4 | return scrollRestorer.restoreScroll(api) 5 | } 6 | return (_, next) => next() 7 | } 8 | -------------------------------------------------------------------------------- /packages/rudy/src/middleware/saveScroll.js: -------------------------------------------------------------------------------- 1 | export default (api) => { 2 | const { scrollRestorer } = api 3 | if (scrollRestorer) { 4 | return scrollRestorer.saveScroll(api) 5 | } 6 | return (_, next) => next() 7 | } 8 | -------------------------------------------------------------------------------- /packages/rudy/src/middleware/serverRedirect.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { isServer } from '@respond-framework/utils' 3 | import { isRedirect, actionToUrl } from '../utils' 4 | import type { Redirect } from '../flow-types' 5 | 6 | export default (api: Redirect) => (req: Object, next: Function) => { 7 | if (isServer() && isRedirect(req.action)) { 8 | const { action } = req 9 | const { url } = actionToUrl(action, api) 10 | 11 | action.url = action.location.url = url 12 | action.status = action.location.status || 302 13 | 14 | // account for anonymous thunks potentially redirecting without returning itself 15 | // and not able to be discovered by regular means in `utils/createRequest.js` 16 | req.ctx.serverRedirect = true 17 | 18 | return action 19 | } 20 | 21 | return next() 22 | } 23 | -------------------------------------------------------------------------------- /packages/rudy/src/middleware/transformAction/index.js: -------------------------------------------------------------------------------- 1 | import { formatAction } from './utils' 2 | 3 | export default () => (req, next) => { 4 | if (!req.route.path) return next() 5 | 6 | req.action = formatAction(req) 7 | 8 | if (req.isDoubleDispatch()) return req.handleDoubleDispatch() // don't dispatch the same action twice 9 | 10 | const { type, params, query, state, hash, basename, location } = req.action 11 | Object.assign(req, { type, params, query, state, hash, basename, location }) // assign to `req` for conevenience (less destructuring in callbacks) 12 | 13 | return next().then(() => req.action) 14 | } 15 | -------------------------------------------------------------------------------- /packages/rudy/src/middleware/transformAction/utils/index.js: -------------------------------------------------------------------------------- 1 | export { default as formatAction } from './formatAction' 2 | export { 3 | default as replacePopAction, 4 | findNeighboringN, 5 | } from './replacePopAction' 6 | -------------------------------------------------------------------------------- /packages/rudy/src/middleware/transformAction/utils/replacePopAction.js: -------------------------------------------------------------------------------- 1 | // handle redirects from back/next actions, where we want to replace in place 2 | // instead of pushing a new entry to preserve proper movement along history track 3 | 4 | export default (n, url, curr, tmp) => { 5 | const { entries, index } = tmp.from.location 6 | 7 | if (!isNAdjacentToSameUrl(url, curr, n)) { 8 | const { url: prevUrl } = tmp.from.location 9 | n = tmp.revertPop ? null : n // if this back/next movement is due to a user-triggered pop (browser back/next buttons), we don't need to shift the browser history by n, since it's already been done 10 | return { n, entries, index, prevUrl } 11 | } 12 | 13 | const newIndex = index + n 14 | const { url: prevUrl } = entries[newIndex].location 15 | n = tmp.revertPop ? n : n * 2 16 | return { n, entries, index: newIndex, prevUrl } 17 | } 18 | 19 | const isNAdjacentToSameUrl = (url, curr, n) => { 20 | const { entries, index } = curr 21 | const loc = entries[index + n * 2] 22 | return loc && loc.location.url === url 23 | } 24 | 25 | export const findNeighboringN = (from, curr) => { 26 | const { entries, index } = curr 27 | 28 | const prev = entries[index - 1] 29 | if (prev && prev.location.url === from.location.url) return -1 30 | 31 | const next = entries[index + 1] 32 | if (next && next.location.url === from.location.url) return 1 33 | } 34 | -------------------------------------------------------------------------------- /packages/rudy/src/pathlessRoutes/addRoutes.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { enhanceRoutes } from '../middleware/call/utils' // unfortunate coupling (to potentially optional middleware) 3 | import { formatRoutes } from '../utils' 4 | import type { AddRoutes } from '../flow-types' 5 | 6 | export default (req: AddRoutes): void => { 7 | const { action, options, routes: allRoutes, has } = req 8 | 9 | const env = process.env.NODE_ENV 10 | 11 | if (env === 'development' && !has('pathlessRoute')) { 12 | throw new Error( 13 | '[rudy] "pathlessRoute" middleware is required to use "addRoutes" action creator.', 14 | ) 15 | } 16 | 17 | const { routes, formatRoute } = action.payload 18 | const formatter = formatRoute || options.formatRoute 19 | const newRoutes = formatRoutes(routes, formatter, true) 20 | const callbacks = options.callbacks || [] 21 | 22 | callbacks.forEach((name) => enhanceRoutes(name, newRoutes, options)) 23 | 24 | Object.assign(allRoutes, newRoutes) 25 | 26 | action.payload.routes = newRoutes 27 | action.payload.routesAdded = Object.keys(routes).length // we need something to triggering updating of Link components when routes added 28 | 29 | req.commitDispatch(action) 30 | } 31 | -------------------------------------------------------------------------------- /packages/rudy/src/pathlessRoutes/changeBasename.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Dispatch, Action } from '../flow-types' 3 | 4 | export default ({ 5 | getLocation, 6 | has, 7 | action, 8 | dispatch, 9 | }: { 10 | getLocation: Function, 11 | action: Action, 12 | has: Function, 13 | dispatch: Dispatch, 14 | }): Dispatch => { 15 | const env = process.env.NODE_ENV 16 | 17 | if (env === 'development' && !has('pathlessRoute')) { 18 | throw new Error( 19 | '[rudy] "pathlessRoute" middleware is required to use "changeBasename" action creator without passing an action.', 20 | ) 21 | } 22 | 23 | const { type, params, query, state, hash } = getLocation() 24 | const { basename } = action.payload 25 | 26 | return dispatch({ type, params, query, state, hash, basename }) 27 | } 28 | -------------------------------------------------------------------------------- /packages/rudy/src/pathlessRoutes/clearCache.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { ClearCache } from '../flow-types' 3 | 4 | export default ({ cache, action, has }: ClearCache): void => { 5 | const env = process.env.NODE_ENV 6 | 7 | if (env === 'development' && !has('pathlessRoute')) { 8 | throw new Error( 9 | '[rudy] "pathlessRoute" middleware is required to use "clearCache" action creator.', 10 | ) 11 | } 12 | 13 | const { invalidator, options } = action.payload 14 | 15 | cache.clear(invalidator, options) 16 | } 17 | -------------------------------------------------------------------------------- /packages/rudy/src/pathlessRoutes/confirm.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Confirm } from '../flow-types' 3 | 4 | export default (req: Confirm) => { 5 | const { ctx, action, has } = req 6 | const env = process.env.NODE_ENV 7 | 8 | if (env === 'development' && !has('pathlessRoute')) { 9 | throw new Error( 10 | '[rudy] "pathlessRoute" middleware is required to use "confirm" action creator.', 11 | ) 12 | } 13 | 14 | req._dispatched = true 15 | 16 | const { canLeave } = action.payload 17 | return ctx.confirm(canLeave) 18 | } 19 | -------------------------------------------------------------------------------- /packages/rudy/src/pathlessRoutes/index.js: -------------------------------------------------------------------------------- 1 | export { default as addRoutes } from './addRoutes' 2 | export { default as changeBasename } from './changeBasename' 3 | export { default as clearCache } from './clearCache' 4 | export { default as confirm } from './confirm' 5 | export { default as callHistory } from './callHistory' 6 | -------------------------------------------------------------------------------- /packages/rudy/src/types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export const PREFIX = '@@rudy' 3 | export const prefixType = (type: string, prefix?: string) => 4 | `${prefix || PREFIX}/${type}` 5 | 6 | export const CALL_HISTORY = prefixType('CALL_HISTORY') 7 | export const NOT_FOUND = prefixType('NOT_FOUND') 8 | export const ADD_ROUTES = prefixType('ADD_ROUTES') 9 | export const CHANGE_BASENAME = prefixType('CHANGE_BASENAME') 10 | export const CLEAR_CACHE = prefixType('CLEAR_CACHE') 11 | 12 | export const CONFIRM = prefixType('CONFIRM') 13 | export const BLOCK = prefixType('BLOCK', '@@skiprudy') // these skip middleware pipeline, and are reducer-only 14 | export const UNBLOCK = prefixType('UNBLOCK', '@@skiprudy') 15 | 16 | export const SET_FROM = prefixType('SET_FROM', '@@skiprudy') 17 | -------------------------------------------------------------------------------- /packages/rudy/src/utils/applyDefaults.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { 4 | Route, 5 | Options, 6 | ObjectDefault, 7 | StringDefault, 8 | } from '../flow-types' 9 | 10 | export const applyObjectDefault: ( 11 | ?ObjectDefault, 12 | ) => (?Object, Route, Options) => ?Object = (defaultValue) => 13 | defaultValue 14 | ? typeof defaultValue === 'function' 15 | ? defaultValue 16 | : (provided) => ({ ...defaultValue, ...provided }) 17 | : (provided) => provided 18 | 19 | export const applyStringDefault: ( 20 | ?StringDefault, 21 | ) => (string, Route, Options) => string = (defaultValue) => 22 | defaultValue 23 | ? typeof defaultValue === 'function' 24 | ? defaultValue 25 | : (provided) => provided || defaultValue 26 | : (provided) => provided 27 | -------------------------------------------------------------------------------- /packages/rudy/src/utils/callRoute.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Action, Routes } from '../flow-types' 3 | 4 | export default (routes: Routes) => ( 5 | action: Action | string | Object, 6 | key?: string, 7 | ...args: Array 8 | ) => { 9 | const type: string = typeof action === 'string' ? action : action.type 10 | const route = routes[type] 11 | if (!route) return null 12 | 13 | if (!key) return route 14 | if (typeof route[key] !== 'function') return route[key] 15 | 16 | action = typeof action === 'object' ? action : { type } 17 | return route[key](action, ...args) 18 | } 19 | 20 | // usage: 21 | // callRoute(routes)(action, key, ...args) 22 | -------------------------------------------------------------------------------- /packages/rudy/src/utils/cleanBasename.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export default (bn: string = '') => 4 | !bn ? '' : stripTrailingSlash(addLeadingSlash(bn)) 5 | 6 | const addLeadingSlash = (bn: string): string => 7 | bn.charAt(0) === '/' ? bn : `/${bn}` 8 | 9 | const stripTrailingSlash = (bn: string): string => 10 | bn.charAt(bn.length - 1) === '/' ? bn.slice(0, -1) : bn 11 | -------------------------------------------------------------------------------- /packages/rudy/src/utils/compileUrl.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { compile } from 'path-to-regexp' 3 | import { matchQuery, matchHash } from './matchUrl' 4 | 5 | import type { Route, Options } from '../flow-types' 6 | 7 | const toPathCache = {} 8 | 9 | export default ( 10 | path: string, 11 | params: Object = {}, 12 | query: ?Object, 13 | hash: ?string, 14 | route: Route = {}, 15 | opts: Options, 16 | ) => { 17 | const search = query ? stringify(query, route, opts) : '' 18 | 19 | if (route.query && !matchQuery(search, route.query, route, opts)) { 20 | throw new Error('[rudy] invalid query object') 21 | } 22 | 23 | if ( 24 | route.hash !== undefined && 25 | matchHash(hash, route.hash, route, opts) == null 26 | ) { 27 | throw new Error('[rudy] invalid hash value') 28 | } 29 | 30 | toPathCache[path] = toPathCache[path] || compile(path) 31 | const toPath = toPathCache[path] 32 | 33 | const p = toPath(params) 34 | const s = search ? `?${search}` : '' 35 | const h = hash ? `#${hash}` : '' 36 | 37 | return p + s + h 38 | } 39 | 40 | const stringify = (query, route: Route, opts: Options) => { 41 | const search = (route.stringifyQuery || opts.stringifyQuery)(query) 42 | 43 | if (process.env.NODE_ENV === 'development' && search.length > 2000) { 44 | // https://stackoverflow.com/questions/812925/what-is-the-maximum-possible-length-of-a-query-string 45 | // eslint-disable-next-line no-console 46 | console.error( 47 | `[rudy] query is too long: ${search.length} chars (max: 2000)`, 48 | ) 49 | } 50 | 51 | return search 52 | } 53 | -------------------------------------------------------------------------------- /packages/rudy/src/utils/doesRedirect.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { isRedirect } from './index' 3 | import type { LocationAction } from '../flow-types' 4 | 5 | export default ( 6 | action: LocationAction, 7 | redirectFunc: Function | Object, 8 | ): boolean => { 9 | if (isRedirect(action)) { 10 | const { url } = action.location 11 | const status = action.location.status || 302 12 | 13 | if (typeof redirectFunc === 'function') { 14 | redirectFunc(status, url, action) 15 | } else if (redirectFunc && typeof redirectFunc.redirect === 'function') { 16 | redirectFunc.redirect(status, url) 17 | } 18 | 19 | return true 20 | } 21 | 22 | return false 23 | } 24 | -------------------------------------------------------------------------------- /packages/rudy/src/utils/formatRoutes.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { 3 | ADD_ROUTES, 4 | CHANGE_BASENAME, 5 | CLEAR_CACHE, 6 | CONFIRM, 7 | CALL_HISTORY, 8 | } from '../types' 9 | import type { Routes, RoutesInput, RouteInput, RouteNames } from '../flow-types' 10 | 11 | import { 12 | addRoutes, 13 | changeBasename, 14 | clearCache, 15 | confirm, 16 | callHistory, 17 | } from '../pathlessRoutes' 18 | 19 | export default ( 20 | input: RoutesInput, 21 | formatter: ?Function, 22 | isAddRoutes: boolean = false, 23 | ): Routes => { 24 | const routes = isAddRoutes ? input : {} 25 | 26 | if (!isAddRoutes) { 27 | routes.NOT_FOUND = input.NOT_FOUND || { path: '/not-found' } 28 | Object.assign(routes, input) // insure '/not-found' matches over '/:param?' -- yes, browsers respect order assigned for non-numeric keys 29 | 30 | routes[ADD_ROUTES] = input[ADD_ROUTES] || { 31 | thunk: addRoutes, 32 | dispatch: false, 33 | } 34 | routes[CHANGE_BASENAME] = input[CHANGE_BASENAME] || { 35 | thunk: changeBasename, 36 | dispatch: false, 37 | } 38 | routes[CLEAR_CACHE] = input[CLEAR_CACHE] || { thunk: clearCache } 39 | routes[CONFIRM] = input[CONFIRM] || { thunk: confirm, dispatch: false } 40 | routes[CALL_HISTORY] = input[CALL_HISTORY] || { 41 | thunk: callHistory, 42 | dispatch: false, 43 | } 44 | } 45 | 46 | const types: RouteNames = Object.keys(routes) 47 | 48 | types.forEach((type: string): void => { 49 | const route: Object = formatRoute( 50 | routes[type], 51 | type, 52 | routes, 53 | formatter, 54 | isAddRoutes, 55 | ) 56 | 57 | route.type = type 58 | routes[type] = route 59 | }) 60 | 61 | return routes 62 | } 63 | 64 | export const formatRoute = ( 65 | r: RouteInput, 66 | type: string, 67 | routes: Routes, 68 | formatter: ?Function, 69 | isAddRoutes: boolean = false, 70 | ): RouteInput => { 71 | const route = typeof r === 'string' ? { path: r } : r 72 | 73 | if (formatter) { 74 | return formatter(route, type, routes, isAddRoutes) 75 | } 76 | 77 | if (typeof route === 'function') { 78 | return { thunk: route } 79 | } 80 | 81 | return route 82 | } 83 | -------------------------------------------------------------------------------- /packages/rudy/src/utils/index.js: -------------------------------------------------------------------------------- 1 | export { default as isHydrate } from './isHydrate' 2 | export { default as isAction } from './isAction' 3 | export { default as isNotFound } from './isNotFound' 4 | export { default as isRedirect } from './isRedirect' 5 | 6 | export { default as actionToUrl } from './actionToUrl' 7 | export { 8 | default as urlToAction, 9 | findBasename, 10 | stripBasename, 11 | } from './urlToAction' 12 | export { default as toAction } from './toAction' 13 | 14 | export { default as locationToUrl } from './locationToUrl' 15 | export { default as urlToLocation } from './urlToLocation' 16 | 17 | export { default as doesRedirect } from './doesRedirect' 18 | export { default as shouldTransition } from './shouldTransition' 19 | 20 | export { default as matchUrl } from './matchUrl' 21 | export { default as compileUrl } from './compileUrl' 22 | 23 | export { default as formatRoutes, formatRoute } from './formatRoutes' 24 | export { default as typeToScene } from './typeToScene' 25 | 26 | export { default as redirectShortcut } from './redirectShortcut' 27 | export { default as callRoute } from './callRoute' 28 | 29 | export { default as noOp } from './noOp' 30 | 31 | export { default as nestAction, createActionRef } from './nestAction' 32 | 33 | export { default as logError, onError } from './logError' 34 | 35 | export { default as cleanBasename } from './cleanBasename' 36 | export { default as parseSearch } from './parseSearch' 37 | 38 | export { default as toEntries, findInitialN } from './toEntries' 39 | -------------------------------------------------------------------------------- /packages/rudy/src/utils/isAction.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export default (a: any): boolean => 4 | a && 5 | (a.type || 6 | // History uses actions with undefined states 7 | a.hasOwnProperty('state') || // eslint-disable-line no-prototype-builtins 8 | a.params || 9 | a.query || 10 | a.hash !== undefined || 11 | a.basename !== undefined || 12 | a.payload || 13 | a.meta) 14 | -------------------------------------------------------------------------------- /packages/rudy/src/utils/isHydrate.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { isServer } from '@respond-framework/utils' 3 | 4 | export default (req: Object): boolean => { 5 | const { universal } = req.getLocation() 6 | return universal && !isServer() && req.getKind() === 'load' 7 | } 8 | -------------------------------------------------------------------------------- /packages/rudy/src/utils/isNotFound.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Action } from '../flow-types' 3 | 4 | export default (action: Action | string): boolean => { 5 | const type = typeof action === 'string' ? action : action.type || '' 6 | return type.indexOf('NOT_FOUND') > -1 && type.indexOf('NOT_FOUND_') === -1 // don't include types like `NOT_FOUND_COMPLETE` 7 | } 8 | -------------------------------------------------------------------------------- /packages/rudy/src/utils/isRedirect.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { LocationAction } from '../flow-types' 3 | 4 | export default (action: LocationAction): boolean => 5 | !!( 6 | action && 7 | action.location && 8 | (action.location.kind === 'replace' || action.location.from) 9 | ) // sometimes the kind will be back/next when automatic back/next detection is in play 10 | -------------------------------------------------------------------------------- /packages/rudy/src/utils/locationToUrl.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { ReceivedAction } from '../flow-types' 3 | 4 | export default (location: ReceivedAction): string => { 5 | if (typeof location === 'string') return location 6 | 7 | const { pathname, search, hash } = location 8 | 9 | let path = pathname || '/' 10 | 11 | if (search && search !== '?') { 12 | path += search.charAt(0) === '?' ? search : `?${search}` 13 | } 14 | 15 | if (hash && hash !== '#') { 16 | path += hash.charAt(0) === '#' ? hash : `#${hash}` 17 | } 18 | 19 | return path 20 | } 21 | -------------------------------------------------------------------------------- /packages/rudy/src/utils/noOp.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export default (): Promise => Promise.resolve() 3 | -------------------------------------------------------------------------------- /packages/rudy/src/utils/parseSearch.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import qs from 'qs' 3 | 4 | export default (search: string) => qs.parse(search, { decoder }) 5 | 6 | const decoder = (str: string, decode: Function): number => 7 | isNumber(str) ? Number.parseFloat(str) : decode(str) 8 | 9 | const isNumber = (str: string): boolean => !Number.isNaN(Number.parseFloat(str)) 10 | -------------------------------------------------------------------------------- /packages/rudy/src/utils/redirectShortcut.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Dispatch, NavigationToAction, Route, Routes } from '../flow-types' 3 | import { redirect } from '../actions' 4 | 5 | export default ({ 6 | route, 7 | routes, 8 | action, 9 | dispatch, 10 | }: { 11 | route: Route, 12 | routes: Routes, 13 | action: NavigationToAction, 14 | dispatch: Dispatch, 15 | }) => { 16 | const t = route.redirect 17 | // $FlowFixMe 18 | const scenicType = `${route.scene}/${t}` 19 | const type: string | Function | void = routes[scenicType] ? scenicType : t 20 | 21 | // $FlowFixMe 22 | return dispatch(redirect({ ...action, type }, 301)) 23 | } 24 | -------------------------------------------------------------------------------- /packages/rudy/src/utils/shouldTransition.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { PREFIX } from '../types' 3 | import type { Action, Route, Routes } from '../flow-types' 4 | 5 | export default ( 6 | action: Action, 7 | { routes }: { routes: Routes }, 8 | ): boolean | Route => { 9 | const { type = '' } = action 10 | const route = routes[type] 11 | 12 | return route || type.indexOf(PREFIX) > -1 13 | } 14 | -------------------------------------------------------------------------------- /packages/rudy/src/utils/toAction.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { actionToUrl, urlToAction } from './index' 3 | import type { Options, Routes, LocationState } from '../flow-types' 4 | // This will take anything you throw at it (a url string, action, or array: [url, state, key?]) 5 | // and convert it to a complete Rudy FSRA ("flux standard routing action"). 6 | 7 | // Standard Rudy practice is to convert incoming actions to their full URL form (url + state) 8 | // and then convert that to a FSRA. THIS DOES BOTH STEPS IN ONE WHEN NECESSSARY (i.e. for actions). 9 | 10 | export default ( 11 | api: { 12 | routes: Routes, 13 | options: Options, 14 | }, 15 | // TODO: make better annotations here 16 | entry: string | Object, 17 | st: ?Object, 18 | k: ?Object, 19 | ): LocationState => { 20 | if (Array.isArray(entry)) { 21 | // entry as array of [url, state, key?] 22 | const [url, state, key] = entry 23 | return urlToAction(api, url, state, key) 24 | } 25 | if (typeof entry === 'object') { 26 | // entry as action object 27 | const key = entry.location && entry.location.key // preserve existing key if existing FSRA 28 | const { url, state } = actionToUrl(entry, api) 29 | return urlToAction(api, url, state, key) 30 | } 31 | return urlToAction(api, entry, st, k) // entry as url string 32 | } 33 | -------------------------------------------------------------------------------- /packages/rudy/src/utils/toEntries.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { toAction } from './index' 3 | import type { Route } from '../flow-types' 4 | 5 | export default ( 6 | api: Route, 7 | entries: Array, 8 | index: number, 9 | n: ?number, 10 | ) => { 11 | entries = isSingleEntry(entries) ? [entries] : entries 12 | entries = entries.length === 0 ? ['/'] : entries 13 | entries = entries.map((e) => toAction(api, e)) 14 | 15 | index = index !== undefined ? index : entries.length - 1 // default to head of array 16 | index = Math.min(Math.max(index, 0), entries.length - 1) // insure the index is in range 17 | 18 | n = n || findInitialN(index, entries) // initial direction the user is going across the history track 19 | 20 | return { n, index, entries } 21 | } 22 | 23 | // When entries are restored on load, the direction is always forward if on an index > 0 24 | // because the corresponding entries are removed (just like a `push`), and you are now at the head. 25 | // Otherwise, if there are multiple entries and you are on the first, you're considered 26 | // to be going back, but if there is one, you're logically going forward. 27 | 28 | export const findInitialN = (index: number, entries: Array) => 29 | index > 0 ? 1 : entries.length > 1 ? -1 : 1 30 | const isSingleEntry = (e) => 31 | !Array.isArray(e) || 32 | // $FlowFixMe 33 | (typeof e[0] === 'string' && typeof e[1] === 'object' && !e[1].type) // pattern match: [string, state] 34 | -------------------------------------------------------------------------------- /packages/rudy/src/utils/typeToScene.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export default (type: string): string => { 4 | const i: number = type.lastIndexOf('/') 5 | return type.substr(0, i) 6 | } 7 | -------------------------------------------------------------------------------- /packages/rudy/src/utils/urlToLocation.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { HistoryLocation } from '../flow-types' 3 | 4 | const createLocationObject = (url: string): HistoryLocation => { 5 | let pathname = url || '/' 6 | let search = '' 7 | let hash = '' 8 | 9 | const hashIndex = pathname.indexOf('#') 10 | if (hashIndex !== -1) { 11 | hash = pathname.substr(hashIndex + 1) // remove # from hash 12 | pathname = pathname.substr(0, hashIndex) // remove hash value from pathname 13 | } 14 | 15 | const searchIndex = pathname.indexOf('?') 16 | if (searchIndex !== -1) { 17 | search = pathname.substr(searchIndex + 1) // remove ? from search 18 | pathname = pathname.substr(0, searchIndex) // remove search value from pathname 19 | } 20 | 21 | pathname = pathname || '/' // could be empty on URLs that like: '?foo=bar#hash 22 | 23 | return { pathname, search, hash } 24 | } 25 | 26 | export default (url: HistoryLocation | string): HistoryLocation => { 27 | if (typeof url === 'object' && url.pathname !== undefined) return url 28 | return createLocationObject(url) 29 | } 30 | -------------------------------------------------------------------------------- /packages/scroll-restorer/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [0.1.0-test.4](https://github.com/respond-framework/rudy/tree/master/packages/scroll-restorer/compare/@respond-framework/scroll-restorer@0.1.0-test.3...@respond-framework/scroll-restorer@0.1.0-test.4) (2020-03-27) 7 | 8 | 9 | ### Features 10 | 11 | * restore fallback for broken sessionStorage ([#74](https://github.com/respond-framework/rudy/tree/master/packages/scroll-restorer/issues/74)) ([8df1154](https://github.com/respond-framework/rudy/tree/master/packages/scroll-restorer/commit/8df1154)) 12 | 13 | 14 | 15 | 16 | 17 | # [0.1.0-test.3](https://github.com/respond-framework/rudy/tree/master/packages/scroll-restorer/compare/@respond-framework/scroll-restorer@0.1.0-test.2...@respond-framework/scroll-restorer@0.1.0-test.3) (2019-12-20) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * update to released scroll-behavior API ([#70](https://github.com/respond-framework/rudy/tree/master/packages/scroll-restorer/issues/70)) ([8e7d381](https://github.com/respond-framework/rudy/tree/master/packages/scroll-restorer/commit/8e7d381)) 23 | 24 | 25 | 26 | 27 | 28 | # 0.1.0-test.2 (2019-11-13) 29 | 30 | 31 | ### Features 32 | 33 | * scroll restoration ([#65](https://github.com/respond-framework/rudy/tree/master/packages/scroll-restorer/issues/65)) ([e326fd2](https://github.com/respond-framework/rudy/tree/master/packages/scroll-restorer/commit/e326fd2)) 34 | 35 | 36 | ### BREAKING CHANGES 37 | 38 | * scroll restoration is enabled by default 39 | 40 | Fixes https://github.com/respond-framework/rudy/issues/62 41 | -------------------------------------------------------------------------------- /packages/scroll-restorer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@respond-framework/scroll-restorer", 3 | "version": "0.1.0-test.4", 4 | "description": "Rudy middleware to restore scroll position after navigation", 5 | "main": "cjs/index.js", 6 | "module": "es/index.js", 7 | "rudy-src-main": "src/index.ts", 8 | "types": "ts/index.d.ts", 9 | "repository": "https://github.com/respond-framework/rudy/tree/master/packages/scroll-restorer", 10 | "contributors": [ 11 | "Daniel Playfair Cal " 12 | ], 13 | "license": "MIT", 14 | "private": false, 15 | "publishConfig": { 16 | "access": "public" 17 | }, 18 | "files": [ 19 | "cjs", 20 | "es", 21 | "ts" 22 | ], 23 | "scripts": { 24 | "prepare": "yarn run build", 25 | "build:cjs": "babel --root-mode upward --source-maps true -x .tsx,.ts,.js,.jsx src -d cjs", 26 | "build:es": "babel --root-mode upward --source-maps true -x .tsx,.ts,.js,.jsx --env-name es src -d es", 27 | "build:ts": "tsc -b", 28 | "build": "yarn run build:cjs && yarn run build:es && yarn run build:ts", 29 | "clean": "rimraf cjs es ts *.tsbuildinfo", 30 | "prettier": "prettier", 31 | "is-pretty": "prettier --ignore-path=../../config/.prettierignore '**/*' --list-different", 32 | "prettify": "prettier --ignore-path=../../config/.prettierignore '**/*' --write" 33 | }, 34 | "dependencies": { 35 | "@respond-framework/utils": "^0.1.1-test.4", 36 | "scroll-behavior": "^0.9.11" 37 | }, 38 | "peerDependencies": { 39 | "@respond-framework/types": "^0.1.1-test.1" 40 | }, 41 | "devDependencies": { 42 | "@respond-framework/types": "^0.1.1-test.4" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/scroll-restorer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../config/tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "ts" 6 | }, 7 | "include": ["src"], 8 | "references": [{ "path": "../types" }, { "path": "../utils" }] 9 | } 10 | -------------------------------------------------------------------------------- /packages/types/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.1.1-test.4](https://github.com/respond-framework/rudy/tree/master/packages/types/compare/@respond-framework/types@0.1.1-test.3...@respond-framework/types@0.1.1-test.4) (2019-12-20) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * update to released scroll-behavior API ([#70](https://github.com/respond-framework/rudy/tree/master/packages/types/issues/70)) ([8e7d381](https://github.com/respond-framework/rudy/tree/master/packages/types/commit/8e7d381)) 12 | 13 | 14 | 15 | 16 | 17 | ## [0.1.1-test.3](https://github.com/respond-framework/rudy/tree/master/packages/types/compare/@respond-framework/types@0.1.1-test.2...@respond-framework/types@0.1.1-test.3) (2019-11-13) 18 | 19 | 20 | ### Features 21 | 22 | * scroll restoration ([#65](https://github.com/respond-framework/rudy/tree/master/packages/types/issues/65)) ([e326fd2](https://github.com/respond-framework/rudy/tree/master/packages/types/commit/e326fd2)) 23 | 24 | 25 | ### BREAKING CHANGES 26 | 27 | * scroll restoration is enabled by default 28 | 29 | Fixes https://github.com/respond-framework/rudy/issues/62 30 | 31 | 32 | 33 | 34 | 35 | ## 0.1.1-test.2 (2019-10-23) 36 | 37 | 38 | ### Bug Fixes 39 | 40 | * correct types for prev on the first entry ([30932ab](https://github.com/respond-framework/rudy/tree/master/packages/types/commit/30932ab)) 41 | 42 | 43 | ### Features 44 | 45 | * add types package exporting shared types ([d78c566](https://github.com/respond-framework/rudy/tree/master/packages/types/commit/d78c566)) 46 | -------------------------------------------------------------------------------- /packages/types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@respond-framework/types", 3 | "version": "0.1.1-test.4", 4 | "description": "Shared types for Rudy", 5 | "rudy-src-main": "src/index.ts", 6 | "types": "ts/index.d.ts", 7 | "repository": "https://github.com/respond-framework/rudy/tree/master/packages/types", 8 | "contributors": [ 9 | "Daniel Playfair Cal " 10 | ], 11 | "license": "MIT", 12 | "private": false, 13 | "publishConfig": { 14 | "access": "public" 15 | }, 16 | "files": [ 17 | "ts" 18 | ], 19 | "scripts": { 20 | "prepare": "yarn run clean && yarn run build", 21 | "build": "tsc -b", 22 | "clean": "rimraf ts *.tsbuildinfo", 23 | "prettier": "prettier", 24 | "is-pretty": "prettier --ignore-path=../../config/.prettierignore '**/*' --list-different", 25 | "prettify": "prettier --ignore-path=../../config/.prettierignore '**/*' --write" 26 | }, 27 | "dependencies": { 28 | "scroll-behavior": "^0.9.11" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../config/tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "ts" 6 | }, 7 | "include": ["src"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/utils/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.1.1-test.4](https://github.com/respond-framework/rudy/tree/master/packages/utils/compare/@respond-framework/utils@0.1.1-test.3...@respond-framework/utils@0.1.1-test.4) (2020-03-27) 7 | 8 | 9 | ### Features 10 | 11 | * restore fallback for broken sessionStorage ([#74](https://github.com/respond-framework/rudy/tree/master/packages/utils/issues/74)) ([8df1154](https://github.com/respond-framework/rudy/tree/master/packages/utils/commit/8df1154)) 12 | 13 | 14 | 15 | 16 | 17 | ## [0.1.1-test.3](https://github.com/respond-framework/rudy/tree/master/packages/utils/compare/@respond-framework/utils@0.1.1-test.2...@respond-framework/utils@0.1.1-test.3) (2019-10-23) 18 | 19 | 20 | ### Code Refactoring 21 | 22 | * remove flow from builds ([332e730](https://github.com/respond-framework/rudy/tree/master/packages/utils/commit/332e730)) 23 | 24 | 25 | ### BREAKING CHANGES 26 | 27 | * flow types are no longer available in published 28 | packages 29 | 30 | 31 | 32 | 33 | 34 | ## [0.1.1-test.2](https://github.com/respond-framework/rudy/tree/master/packages/utils/compare/@respond-framework/utils@0.1.1-test.1...@respond-framework/utils@0.1.1-test.2) (2019-10-16) 35 | 36 | **Note:** Version bump only for package @respond-framework/utils 37 | 38 | 39 | 40 | 41 | 42 | ## 0.1.1-test.1 (2019-04-15) 43 | 44 | **Note:** Version bump only for package @respond-framework/utils 45 | 46 | 47 | 48 | 49 | 50 | ## 0.1.1-test.0 (2018-11-05) 51 | 52 | **Note:** Version bump only for package @respond-framework/utils 53 | -------------------------------------------------------------------------------- /packages/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@respond-framework/utils", 3 | "version": "0.1.1-test.4", 4 | "description": "Shared utilities for rudy", 5 | "main": "cjs/index.js", 6 | "module": "es/index.js", 7 | "rudy-src-main": "src/index.ts", 8 | "types": "ts/index.d.ts", 9 | "repository": "https://github.com/respond-framework/rudy/tree/master/packages/utils", 10 | "contributors": [ 11 | "James Gilmore ", 12 | "Daniel Playfair Cal " 13 | ], 14 | "license": "MIT", 15 | "private": false, 16 | "publishConfig": { 17 | "access": "public" 18 | }, 19 | "files": [ 20 | "cjs", 21 | "es", 22 | "ts" 23 | ], 24 | "scripts": { 25 | "prepare": "yarn run clean && yarn run build", 26 | "build:cjs": "babel --root-mode upward --source-maps true -x .tsx,.ts,.js,.jsx src -d cjs", 27 | "build:es": "babel --root-mode upward --source-maps true -x .tsx,.ts,.js,.jsx --env-name es src -d es", 28 | "build:ts": "tsc -b", 29 | "build": "yarn run build:cjs && yarn run build:es && yarn run build:ts", 30 | "clean": "rimraf cjs es ts *.tsbuildinfo tests/ts tests/*.tsbuildinfo", 31 | "prettier": "prettier", 32 | "is-pretty": "prettier --ignore-path=../../config/.prettierignore '**/*' --list-different", 33 | "prettify": "prettier --ignore-path=../../config/.prettierignore '**/*' --write", 34 | "test": "jest --config ../../jest.config.js --rootDir ." 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/utils/src/createSelector.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 2 | export default (name: string, selector?: string | Function): any => { 3 | if (typeof selector === 'function') { 4 | return selector 5 | } 6 | if (selector) { 7 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 8 | return (state: { [index: string]: any }): any => 9 | state ? state[selector] : undefined 10 | } 11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 | return (state: { [index: string]: any }): any => 13 | state ? state[name] : undefined 14 | } 15 | -------------------------------------------------------------------------------- /packages/utils/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as isServer } from './isServer' 2 | export { default as createSelector } from './createSelector' 3 | export { default as supportsSessionStorage } from './supportsSessionStorage' 4 | -------------------------------------------------------------------------------- /packages/utils/src/isServer.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | export default (): boolean => 4 | !( 5 | typeof window !== 'undefined' && 6 | window.document && 7 | window.document.createElement 8 | ) 9 | -------------------------------------------------------------------------------- /packages/utils/src/supportsSessionStorage.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | let _supportsSessionStorage: boolean 4 | 5 | export default (): boolean => { 6 | if (_supportsSessionStorage !== undefined) { 7 | return _supportsSessionStorage 8 | } 9 | try { 10 | window.sessionStorage.setItem('rudytestitem', 'testvalue') 11 | if (window.sessionStorage.getItem('rudytestitem') === 'testvalue') { 12 | window.sessionStorage.removeItem('rudytestitem') 13 | _supportsSessionStorage = true 14 | } else { 15 | _supportsSessionStorage = false 16 | } 17 | } catch { 18 | _supportsSessionStorage = false 19 | } 20 | if (!_supportsSessionStorage) { 21 | // eslint-disable-next-line no-console 22 | console.warn( 23 | '[rudy]: WARNING: This browser does not support sessionStorage!', 24 | ) 25 | } 26 | return _supportsSessionStorage 27 | } 28 | -------------------------------------------------------------------------------- /packages/utils/tests/createSelector.test.ts: -------------------------------------------------------------------------------- 1 | import createSelector from '../src/createSelector' 2 | 3 | describe('createSelector', () => { 4 | test('Returns undefined if the state is undefined', async () => { 5 | const selector = createSelector('test name') 6 | expect(selector()).toBeUndefined() 7 | }) 8 | 9 | test('Returns undefined if the state is null', async () => { 10 | const selector = createSelector('test name') 11 | expect(selector(null)).toBeUndefined() 12 | }) 13 | 14 | test('Returns the key for the name if no key/selector is provided', async () => { 15 | const selector = createSelector('test name') 16 | expect( 17 | selector({ 18 | 'test name': 'test value', 19 | }), 20 | ).toStrictEqual('test value') 21 | }) 22 | 23 | test('Returns the given key if a string is provided', async () => { 24 | const selector = createSelector( 25 | 'test name', 26 | 'test key', 27 | ) 28 | expect( 29 | selector({ 30 | 'test name': 'test value', 31 | 'test key': 'test keyed value', 32 | }), 33 | ).toStrictEqual('test keyed value') 34 | }) 35 | 36 | test('Uses a selector if provided', async () => { 37 | const state = { 38 | 'test name': 'test value', 39 | 'test key': 'test keyed value', 40 | } 41 | const providedSelector = jest.fn(() => 'selector return value') 42 | const selector = createSelector( 43 | 'test name', 44 | providedSelector, 45 | ) 46 | const result = selector(state) 47 | expect(result).toStrictEqual('selector return value') 48 | expect(providedSelector).toHaveBeenCalledTimes(1) 49 | expect(providedSelector).toHaveBeenCalledWith(state) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /packages/utils/tests/isServer.test.ts: -------------------------------------------------------------------------------- 1 | import isServer from '../src/isServer' 2 | 3 | // Not sure if there are published types for this 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | declare const global: any 6 | 7 | describe('isServer', () => { 8 | test('Returns true when window is not defined', async () => { 9 | expect(isServer()).toStrictEqual(true) 10 | expect(global.window).toBeUndefined() 11 | }) 12 | 13 | test('Returns true when window.document is not defined', async () => { 14 | global.window = {} 15 | expect(isServer()).toStrictEqual(true) 16 | expect(global.window.document).toBeUndefined() 17 | }) 18 | 19 | test('Returns true when window.document.createElement is not defined', async () => { 20 | global.window = { 21 | document: {}, 22 | } 23 | expect(isServer()).toStrictEqual(true) 24 | expect(global.window.document.createElement).toBeUndefined() 25 | }) 26 | 27 | test('Returns false when window.document.createElement is defined', async () => { 28 | global.window = { 29 | document: { 30 | createElement: true, 31 | }, 32 | } 33 | expect(isServer()).toStrictEqual(false) 34 | expect(global.window.document.createElement).toStrictEqual(true) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /packages/utils/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../config/tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "ts" 5 | }, 6 | "references": [{ "path": ".." }] 7 | } 8 | -------------------------------------------------------------------------------- /packages/utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../config/tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "ts" 6 | }, 7 | "include": ["src"] 8 | } 9 | -------------------------------------------------------------------------------- /scripts/git-release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | BRANCH=$(git rev-parse --abbrev-ref HEAD) 6 | 7 | yarn run lerna version --conventional-prerelease --preid from-git --no-git-tag-version --no-push --allow-branch $BRANCH --yes 8 | git add . 9 | git commit -m "Publish to git" 10 | 11 | for DIR in $(yarn run -s lerna changed --parseable); do 12 | ( 13 | VERSION=$(cat "${DIR}/package.json" | jq -r '.version') 14 | NAME=$(cat "${DIR}/package.json" | jq -r '.name') 15 | 16 | ( 17 | cd "$DIR" 18 | yarn run prepare 19 | ) 20 | yarn run npm-publish-git --dir "$DIR" --tag "${NAME}/${BRANCH}/${VERSION}" 21 | ) 22 | done 23 | --------------------------------------------------------------------------------