├── .editorconfig ├── .eslintrc.js ├── .github └── workflows │ └── release.yaml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .lintstagedrc.json ├── .npmrc ├── .prettierignore ├── .vscode └── settings.json ├── LICENSE ├── README-zh_CN.md ├── README.md ├── __tests__ ├── guard-config-provider.test.tsx ├── guard-provider.test.tsx ├── guarded-route.test.tsx ├── guarded-routes.test.tsx ├── useGuardedRoutes.test.tsx └── utils.ts ├── commitlint.config.js ├── examples ├── auth │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.tsx │ │ ├── Home.tsx │ │ ├── Login.tsx │ │ ├── main.tsx │ │ ├── routes.tsx │ │ ├── store.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── basic │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── public │ └── vite.svg │ ├── src │ ├── About.tsx │ ├── App.tsx │ ├── Home.tsx │ ├── main.tsx │ ├── routes.tsx │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── jest.config.ts ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── prettier.config.js ├── src ├── guard-config-provider.tsx ├── guard-provider.tsx ├── guarded-route.tsx ├── guarded-routes.tsx ├── index.ts ├── internal │ ├── context.tsx │ ├── guard.tsx │ ├── useGuardConfigContext.ts │ ├── useGuardContext.ts │ ├── useInGuardContext.ts │ ├── usePrevious.ts │ └── utils.ts ├── type.ts └── useGuardedRoutes.ts ├── stylelint.config.js ├── tsconfig.json └── tsup.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@col0ring/eslint-config/test', '@col0ring/eslint-config'], 3 | rules: { 4 | 'testing-library/no-unnecessary-act': 'off', 5 | 'testing-library/await-async-query': 'off', 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: [main] 5 | jobs: 6 | public-to-npm: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v2 11 | - name: Install Pnpm 12 | uses: pnpm/action-setup@v2.0.1 13 | with: 14 | version: 7.0.0 15 | # Setup .npmrc file to publish to GitHub Packages 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v2 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | cache: 'pnpm' 21 | registry-url: 'https://registry.npmjs.org' 22 | - name: Install Dependencies 23 | run: pnpm install 24 | - name: Build Packages 25 | run: pnpm build 26 | - name: Run Test 27 | run: pnpm test 28 | - name: Run lint 29 | run: pnpm lint 30 | - name: Publish 31 | run: pnpm publish 32 | env: 33 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 34 | # Create Release 35 | - name: Read package.json 36 | uses: tyankatsu0105/read-package-version-actions@v1 37 | with: 38 | path: '.' 39 | id: package-version 40 | - name: Create Release Tag 41 | id: release_tag 42 | uses: yyx990803/release-tag@master 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | with: 46 | tag_name: v${{ steps.package-version.outputs.version }} 47 | release_name: v${{ steps.package-version.outputs.version }} 48 | prerelease: false 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx commitlint -e $HUSKY_GIT_PARAMS 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.css": ["npm run lint:stylelint", "npm run format"], 3 | "*.{js,jsx,ts,tsx}": ["npm run lint:eslint", "npm run format"], 4 | "*.{md,yaml,json,html}": ["npm run format"] 5 | } 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | __snapshots__ 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # Snowpack dependency directory (https://snowpack.dev/) 47 | web_modules/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Optional stylelint cache 59 | .stylelintcache 60 | 61 | # Microbundle cache 62 | .rpt2_cache/ 63 | .rts2_cache_cjs/ 64 | .rts2_cache_es/ 65 | .rts2_cache_umd/ 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variable files 77 | .env 78 | .env.development.local 79 | .env.test.local 80 | .env.production.local 81 | .env.local 82 | 83 | # parcel-bundler cache (https://parceljs.org/) 84 | .cache 85 | .parcel-cache 86 | 87 | # Next.js build output 88 | .next 89 | out 90 | 91 | # Nuxt.js build / generate output 92 | .nuxt 93 | dist 94 | 95 | # Gatsby files 96 | .cache/ 97 | # Comment in the public line in if your project uses Gatsby and not Next.js 98 | # https://nextjs.org/blog/next-9-1#public-directory-support 99 | # public 100 | 101 | # vuepress build output 102 | .vuepress/dist 103 | 104 | # vuepress v2.x temp and cache directory 105 | .temp 106 | .cache 107 | 108 | # Docusaurus cache and generated files 109 | .docusaurus 110 | 111 | # Serverless directories 112 | .serverless/ 113 | 114 | # FuseBox cache 115 | .fusebox/ 116 | 117 | # DynamoDB Local files 118 | .dynamodb/ 119 | 120 | # TernJS port file 121 | .tern-port 122 | 123 | # Stores VSCode versions used for testing VSCode extensions 124 | .vscode-test 125 | 126 | # yarn v2 127 | .yarn/cache 128 | .yarn/unplugged 129 | .yarn/build-state.yml 130 | .yarn/install-state.gz 131 | .pnp.* 132 | 133 | # output 134 | output 135 | *.min.* 136 | public 137 | temp 138 | 139 | # lock file 140 | *.lock 141 | package-lock.json 142 | pnpm-lock.yaml 143 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll": true 5 | }, 6 | "editor.formatOnSave": true, 7 | "files.insertFinalNewline": true, 8 | "eslint.validate": [ 9 | "javascript", 10 | "javascriptreact", 11 | "typescript", 12 | "typescriptreact" 13 | ], 14 | "[css]": { 15 | "editor.defaultFormatter": "esbenp.prettier-vscode" 16 | }, 17 | "[less]": { 18 | "editor.defaultFormatter": "esbenp.prettier-vscode" 19 | }, 20 | "[html]": { 21 | "editor.defaultFormatter": "esbenp.prettier-vscode" 22 | }, 23 | "[json]": { 24 | "editor.defaultFormatter": "esbenp.prettier-vscode" 25 | }, 26 | "[typescript]": { 27 | "editor.defaultFormatter": "esbenp.prettier-vscode" 28 | }, 29 | "[typescriptreact]": { 30 | "editor.defaultFormatter": "esbenp.prettier-vscode" 31 | }, 32 | "[javascript]": { 33 | "editor.defaultFormatter": "esbenp.prettier-vscode" 34 | }, 35 | "[javascriptreact]": { 36 | "editor.defaultFormatter": "esbenp.prettier-vscode" 37 | }, 38 | "[jsonc]": { 39 | "editor.defaultFormatter": "esbenp.prettier-vscode" 40 | }, 41 | "[markdown]": { 42 | "editor.defaultFormatter": "esbenp.prettier-vscode" 43 | }, 44 | "[yaml]": { 45 | "editor.defaultFormatter": "esbenp.prettier-vscode" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | Copyright (c) [2022] [Col0ring] 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /README-zh_CN.md: -------------------------------------------------------------------------------- 1 | # React-Router-Guarded-Routes 2 | 3 | > [English](./README.md) | 简体中文 4 | 5 | 一个用于 react-router v6 的路由守卫中间件,该中间件受到 [`react-router-guards`](https://github.com/Upstatement/react-router-guards) 启发。 6 | 7 | - [React-Router-Guarded-Routes](#react-router-guarded-routes) 8 | - [下载](#下载) 9 | - [使用](#使用) 10 | - [基本使用](#基本使用) 11 | - [路由守卫](#路由守卫) 12 | - [API](#api) 13 | - [类型](#类型) 14 | - [组件](#组件) 15 | - [GuardConfigProvider](#guardconfigprovider) 16 | - [属性](#属性) 17 | - [开始使用](#开始使用) 18 | - [GuardProvider](#guardprovider) 19 | - [属性](#属性-1) 20 | - [开始使用](#开始使用-1) 21 | - [GuardedRoutes](#guardedroutes) 22 | - [属性](#属性-2) 23 | - [开始使用](#开始使用-2) 24 | - [GuardedRoute](#guardedroute) 25 | - [属性](#属性-3) 26 | - [开始使用](#开始使用-3) 27 | - [Hooks](#hooks) 28 | - [useGuardedRoutes](#useguardedroutes) 29 | - [属性](#属性-4) 30 | - [开始使用](#开始使用-4) 31 | 32 | ## 下载 33 | 34 | ```sh 35 | npm install react-router-guarded-routes react-router --save 36 | # or 37 | yarn add react-router-guarded-routes react-router 38 | # or 39 | pnpm add react-router-guarded-routes react-router 40 | ``` 41 | 42 | ## 使用 43 | 44 | ### 基本使用 45 | 46 | 在 `BrowserRouter` 内部使用该库提供的 `GuardConfigProvider` 组件,然后像使用`react-router`一样使用它(适配 `react-router` 的 api)。 47 | 48 | ```tsx 49 | import { BrowserRouter } from 'react-router-dom' 50 | import { 51 | GuardConfigProvider, 52 | GuardedRoute, 53 | GuardedRoutes, 54 | } from 'react-router-guarded-routes' 55 | 56 | export default function App() { 57 | return ( 58 | 59 | 60 | 61 | foo} path="/foo" /> 62 | bar} path="/bar/*"> 63 | baz} path="/bar/baz" /> 64 | 65 | 66 | 67 | 68 | ) 69 | } 70 | ``` 71 | 72 | 使用 hooks: 73 | 74 | ```tsx 75 | import { 76 | GuardedRouteObject, 77 | useGuardedRoutes, 78 | } from 'react-router-guarded-routes' 79 | 80 | const routes: GuardedRouteObject[] = [ 81 | { path: '/foo', element:
foo
}, 82 | { 83 | path: '/bar/*', 84 | element:
bar
, 85 | children: [{ path: '/bar/baz', element:
baz
}], 86 | }, 87 | ] 88 | 89 | function Routes() { 90 | return {useGuardedRoutes([routes])} 91 | } 92 | 93 | export default function App() { 94 | return ( 95 | 96 | 97 | 98 | 99 | 100 | ) 101 | } 102 | ``` 103 | 104 | ### 路由守卫 105 | 106 | 你可以通过在 `GuardProvider` 组件中使用多个中间件来进行路由守卫,`GuardProvider` 可以接收一个 guards 数组和一个 fallback 元素(可以用于加载 loading 态)。 107 | 108 | ```tsx 109 | import { BrowserRouter } from 'react-router-dom' 110 | import { 111 | GuardConfigProvider, 112 | GuardedRoute, 113 | GuardedRoutes, 114 | GuardMiddleware, 115 | GuardProvider, 116 | } from 'react-router-guarded-routes' 117 | 118 | const logGuard: GuardMiddleware = (to, from, next) => { 119 | console.log(to) // { location, matches, route } 120 | console.log(from) 121 | next() // 调用 next() 来执行下一个中间件或者显示路由元素,它接受与 navigate(useNavigate())相同的参数并且行为一致。 122 | } 123 | 124 | const guards = [logGuard] 125 | 126 | // 也可以传入对象来判断是否需要注册中间件 127 | const barGuard: GuardMiddleware = { 128 | handler: (to, from, next) => { 129 | console.log('bar') 130 | next() 131 | }, 132 | register: (to, from) => { 133 | // only matched with `/bar` can be executed.å 134 | if (to.location.pathname.startsWith('/bar')) { 135 | return true 136 | } 137 | return false 138 | }, 139 | } 140 | 141 | const guards = [logGuard, barGuard] 142 | 143 | export default function App() { 144 | return ( 145 | 146 | 147 | {/* Guard all routes below. */} 148 | loading...} guards={guards}> 149 | 150 | foo} path="/foo" /> 151 | bar} path="/bar/*"> 152 | baz} path="/bar/baz" /> 153 | 154 | 155 | 156 | 157 | 158 | ) 159 | } 160 | ``` 161 | 162 | 当然,你也可以分别为每个路由设置 fallback 和路由守卫。 163 | 164 | ```tsx 165 | import { BrowserRouter, Outlet } from 'react-router-dom' 166 | import { 167 | GuardConfigProvider, 168 | GuardedRoute, 169 | GuardedRoutes, 170 | GuardMiddleware, 171 | GuardProvider, 172 | } from 'react-router-guarded-routes' 173 | 174 | const logGuard: GuardMiddleware = (to, from, next) => { 175 | console.log(to, from) 176 | next() 177 | } 178 | 179 | const fooGuard: GuardMiddleware = (to, from, next) => { 180 | console.log('foo') 181 | next() 182 | } 183 | 184 | const guards = [logGuard] 185 | const fooGuards = [fooGuard] 186 | 187 | export default function App() { 188 | return ( 189 | 190 | 191 | loading...} guards={guards}> 192 | 193 | loading foo...} 195 | guards={fooGuard} 196 | element={
foo
} 197 | path="/foo" 198 | /> 199 | 202 | bar 203 | 204 | 205 | } 206 | path="/bar/*" 207 | > 208 | baz} path="/bar/baz" /> 209 | 210 |
211 |
212 |
213 |
214 | ) 215 | } 216 | ``` 217 | 218 | 通过调用 `next.ctx('ctx value')` 来传递上下文信息,在下一个守卫中间件中通过 `ctxValue` 获取。 守卫中间件从外到内,从左到右执行。 219 | 220 | ```tsx 221 | 222 | loading...} 224 | guards={(to, from, next) => { 225 | next.ctx('ctx value') 226 | }} 227 | > 228 | 229 | { 231 | console.log(ctxValue) // ctx value 232 | next() 233 | }} 234 | element={
foo
} 235 | path="/foo" 236 | /> 237 |
238 |
239 |
240 | ``` 241 | 242 | 调用 `next.end()` 来忽略后续中间件。 243 | 244 | ```tsx 245 | 246 | loading...} 248 | guards={ 249 | ((to, from, next) => { 250 | next.end() 251 | }, 252 | () => { 253 | console.log('will not be called') 254 | }) 255 | } 256 | > 257 | 258 | { 260 | console.log('will not be called') 261 | }} 262 | element={
foo
} 263 | path="/foo" 264 | /> 265 |
266 |
267 |
268 | ``` 269 | 270 | ## API 271 | 272 | ### 类型 273 | 274 | ```ts 275 | import React from 'react' 276 | import { 277 | Location, 278 | NavigateFunction, 279 | RouteMatch, 280 | RouteObject, 281 | } from 'react-router' 282 | import { ReplacePick } from 'types-kit' 283 | 284 | export interface GuardedRouteConfig { 285 | guards?: GuardMiddleware[] 286 | fallback?: React.ReactNode 287 | [props: PropertyKey]: any 288 | } 289 | 290 | export type GuardedRouteObject = RouteObject & 291 | GuardedRouteConfig & { 292 | children?: GuardedRouteObject[] 293 | } 294 | 295 | export interface NextFunction extends NavigateFunction { 296 | (): void 297 | ctx: (value: T) => void 298 | end: () => void 299 | } 300 | 301 | export interface GuardedRouteMatch 302 | extends Omit, 'route'> { 303 | route: GuardedRouteObject 304 | } 305 | 306 | export interface ToGuardRouteOptions { 307 | location: Location 308 | matches: GuardedRouteMatch[] 309 | route: GuardedRouteObject 310 | } 311 | 312 | export interface FromGuardRouteOptions 313 | extends ReplacePick< 314 | ToGuardRouteOptions, 315 | ['location', 'route'], 316 | [ 317 | ToGuardRouteOptions['location'] | null, 318 | ToGuardRouteOptions['route'] | null 319 | ] 320 | > {} 321 | 322 | export interface ExternalOptions { 323 | ctxValue: T 324 | injectedValue: I 325 | } 326 | 327 | export type GuardMiddlewareFunction = ( 328 | to: ToGuardRouteOptions, 329 | from: FromGuardRouteOptions, 330 | next: NextFunction, 331 | externalOptions: ExternalOptions 332 | ) => Promise | void 333 | 334 | export type GuardMiddlewareObject = { 335 | handler: GuardMiddlewareFunction 336 | register?: ( 337 | to: ToGuardRouteOptions, 338 | from: FromGuardRouteOptions 339 | ) => Promise | boolean 340 | } 341 | export type GuardMiddleware = 342 | | GuardMiddlewareFunction 343 | | GuardMiddlewareObject 344 | ``` 345 | 346 | ### 组件 347 | 348 | #### GuardConfigProvider 349 | 350 | `GuardConfigProvider` 包含有整个路由相关的配置项,不应该在应用中存在多个,请确保它位于路由器内部的最顶层(`BrowserRouter` 和 `HashRouter`)。 351 | 352 | 并且它提供了是否运行保护中间件以及是否显示回退元素的 API: 353 | 354 | ##### 属性 355 | 356 | ```tsx 357 | import React from 'react' 358 | 359 | export interface GuardConfigProviderProps { 360 | enableGuard?: ( 361 | location: ToGuardRouteOptions, 362 | prevLocation: FromGuardRouteOptions 363 | ) => Promise | boolean 364 | enableFallback?: ( 365 | location: ToGuardRouteOptions, 366 | prevLocation: FromGuardRouteOptions 367 | ) => boolean 368 | children: React.ReactNode 369 | } 370 | ``` 371 | 372 | | 属性 | 可选 | 默认值 | 描述 | 373 | | ---------------- | :--: | :------------------------------------------------------------: | ---------------------- | 374 | | `enableGuards` | 是 | (to, from) => to.location.pathname !== from.location?.pathname | 是否执行中间件 | 375 | | `enableFallback` | 是 | () => true | 是否展示 fallback 元素 | 376 | 377 | ##### 开始使用 378 | 379 | ```tsx 380 | import { BrowserRouter } from 'react-router-dom' 381 | import { GuardConfigProvider } from 'react-router-guarded-routes' 382 | export default function App() { 383 | return ( 384 | 385 | 386 | { 387 | // routes 388 | } 389 | 390 | 391 | ) 392 | } 393 | ``` 394 | 395 | #### GuardProvider 396 | 397 | 为 `GuardedRoute` 提供公共的 `fallback` 元素守卫中间件。 398 | 399 | ##### 属性 400 | 401 | ```tsx 402 | import React from 'react' 403 | 404 | export interface GuardProviderProps { 405 | fallback?: React.ReactElement 406 | useInject?: ( 407 | to: ToGuardRouteOptions, 408 | from: FromGuardRouteOptions 409 | ) => Record 410 | guards?: GuardedRouteConfig['guards'] 411 | children: React.ReactNode 412 | } 413 | ``` 414 | 415 | | 属性 | 可选 | 默认值 | 描述 | 416 | | ----------- | :--: | :----: | -------------------------------------------------------------------------------------------------------- | 417 | | `fallback` | 是 | | 当 `GuardedRoute` 运行守卫中间件时显示的替代元素 | 418 | | `useInject` | 是 | | 一个可供守卫中间件使用的注入值(可以在内部使用 hooks),会自动合并嵌套 `GuardProvider` 的 `useInject` 值 | 419 | | `guards` | 是 | | 公共的路由守卫 | 420 | 421 | ##### 开始使用 422 | 423 | ```tsx 424 | import { BrowserRouter } from 'react-router-dom' 425 | import { 426 | GuardConfigProvider, 427 | GuardedRoute, 428 | GuardedRoutes, 429 | GuardMiddleware, 430 | GuardProvider, 431 | } from 'react-router-guarded-routes' 432 | 433 | const logGuard: GuardMiddleware = (to, from, next) => { 434 | console.log(to, from) 435 | next() 436 | } 437 | 438 | export default function App() { 439 | return ( 440 | 441 | 442 | loading...} guards={[logGuard]}> 443 | 444 | foo} path="/foo" /> 445 | 446 | 447 | 448 | 449 | ) 450 | } 451 | ``` 452 | 453 | 使用嵌套的 `GuardProvider`: 454 | 455 | ```tsx 456 | 457 | loading...}> 458 | 459 | foo} path="/foo" /> 460 | loading2...}> 461 | 464 | bar 465 | 466 | 467 | } 468 | path="/bar/*" 469 | > 470 | baz} path="/bar/baz" /> 471 | 472 | 473 | 474 | 475 | 476 | ``` 477 | 478 | 注入值: 479 | 480 | ```tsx 481 | import { createContext } from 'react' 482 | import { BrowserRouter } from 'react-router-dom' 483 | import { 484 | GuardConfigProvider, 485 | GuardedRoute, 486 | GuardedRoutes, 487 | GuardProvider, 488 | } from 'react-router-guarded-routes' 489 | 490 | export const AuthContext = createContext({ 491 | isLogin: false, 492 | }) 493 | 494 | export function useAuth() { 495 | return useContext(AuthContext) 496 | } 497 | 498 | export default function App() { 499 | return ( 500 | 501 | 502 | 503 | loading...} 505 | useInject={useAuth} 506 | guards={[ 507 | (to, from, next, { injectedValue }) => { 508 | console.log(injectedValue) // { isLogin: false } 509 | next() 510 | }, 511 | ]} 512 | > 513 | 514 | foo} path="/foo" /> 515 | 516 | 517 | 518 | 519 | 520 | ) 521 | } 522 | ``` 523 | 524 | #### GuardedRoutes 525 | 526 | 使用 `GuardedRoutes` 组件来替代 React Router 默认提供的 `Routes` 组件。 527 | 528 | ##### 属性 529 | 530 | ```tsx 531 | import { RoutesProps } from 'react-router' 532 | 533 | export interface GuardedRoutesProps extends RoutesProps {} 534 | ``` 535 | 536 | ##### 开始使用 537 | 538 | ```tsx 539 | 540 | 541 | 542 | foo} path="/foo" /> 543 | 544 | 545 | 546 | ``` 547 | 548 | #### GuardedRoute 549 | 550 | 使用 `GuardedRoute` 组件来替代 React Router 默认提供的 `Route` 组件,它允许额外接收守卫中间件与 fallback 元素作为属性。 551 | 552 | ##### 属性 553 | 554 | ```tsx 555 | import { Route } from 'react-router' 556 | type RouteProps = Parameters[0] 557 | 558 | export type GuardedRouteProps = RouteProps & GuardedRouteConfig 559 | ``` 560 | 561 | 下表包含该组件独有的属性: 562 | 563 | | 属性 | 可选 | 默认值 | 描述 | 564 | | ---------- | :--: | :----: | ----------------------------------------------------------------------------------------- | 565 | | `fallback` | 是 | | 当 `GuardedRoute` 运行守卫中间件时显示的替代元素. (会覆盖`GuardProvider`提供的 fallback) | 566 | | `guards` | 是 | | 路由守卫 | 567 | 568 | ##### 开始使用 569 | 570 | ```tsx 571 | 572 | foo} 574 | path="/foo" 575 | fallback={
loading...
} 576 | guards={[ 577 | (to, from, next) => { 578 | next() 579 | }, 580 | ]} 581 | /> 582 |
583 | ``` 584 | 585 | ### Hooks 586 | 587 | #### useGuardedRoutes 588 | 589 | 使用 `useGuardedRoutes` 可以来替代 React Router 默认提供的 `useRoutes` ,它为每个成员额外提供了 `fallback` 与 `guards` 属性。 590 | 591 | ##### 属性 592 | 593 | ```tsx 594 | import { useRoutes } from 'react-router' 595 | 596 | type LocationArg = Parameters[1] 597 | 598 | export function useGuardedRoutes( 599 | guardedRoutes: GuardedRouteObject[], 600 | locationArg?: LocationArg 601 | ): ReturnType 602 | ``` 603 | 604 | ##### 开始使用 605 | 606 | ```tsx 607 | import { 608 | GuardedRouteObject, 609 | useGuardedRoutes, 610 | } from 'react-router-guarded-routes' 611 | const routes: GuardedRouteObject[] = [ 612 | { 613 | path: '/foo', 614 | element:
foo
, 615 | fallback:
loading foo...
, 616 | guards: [(to, from, next) => next()], 617 | }, 618 | ] 619 | 620 | function Routes() { 621 | return <>{useGuardedRoutes(routes)} 622 | } 623 | 624 | export default function App() { 625 | return ( 626 | 627 | 628 | loading...}> 629 | 630 | 631 | 632 | 633 | ) 634 | } 635 | ``` 636 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React-Router-Guarded-Routes 2 | 3 | > English | [简体中文](./README-zh_CN.md) 4 | 5 | A guard middleware for react-router v6, inspired by [`react-router-guards`](https://github.com/Upstatement/react-router-guards). 6 | 7 | - [React-Router-Guarded-Routes](#react-router-guarded-routes) 8 | - [Install](#install) 9 | - [Usage](#usage) 10 | - [Basic](#basic) 11 | - [Guarding](#guarding) 12 | - [API](#api) 13 | - [Types](#types) 14 | - [Components](#components) 15 | - [GuardConfigProvider](#guardconfigprovider) 16 | - [Props](#props) 17 | - [Setup](#setup) 18 | - [GuardProvider](#guardprovider) 19 | - [Props](#props-1) 20 | - [Setup](#setup-1) 21 | - [GuardedRoutes](#guardedroutes) 22 | - [Props](#props-2) 23 | - [Setup](#setup-2) 24 | - [GuardedRoute](#guardedroute) 25 | - [Props](#props-3) 26 | - [Setup](#setup-3) 27 | - [Hooks](#hooks) 28 | - [useGuardedRoutes](#useguardedroutes) 29 | - [Props](#props-4) 30 | - [Setup](#setup-4) 31 | 32 | ## Install 33 | 34 | ```sh 35 | npm install react-router-guarded-routes react-router --save 36 | # or 37 | yarn add react-router-guarded-routes react-router 38 | # or 39 | pnpm add react-router-guarded-routes react-router 40 | ``` 41 | 42 | ## Usage 43 | 44 | ### Basic 45 | 46 | Provides `GuardConfigProvider` in `BrowserRouter`, and you can use it like `react-router` (compatible with the apis of `react-router`). 47 | 48 | ```tsx 49 | import { BrowserRouter } from 'react-router-dom' 50 | import { 51 | GuardConfigProvider, 52 | GuardedRoute, 53 | GuardedRoutes, 54 | } from 'react-router-guarded-routes' 55 | 56 | export default function App() { 57 | return ( 58 | 59 | 60 | 61 | foo} path="/foo" /> 62 | bar} path="/bar/*"> 63 | baz} path="/bar/baz" /> 64 | 65 | 66 | 67 | 68 | ) 69 | } 70 | ``` 71 | 72 | Use hooks: 73 | 74 | ```tsx 75 | import { 76 | GuardedRouteObject, 77 | useGuardedRoutes, 78 | } from 'react-router-guarded-routes' 79 | 80 | const routes: GuardedRouteObject[] = [ 81 | { path: '/foo', element:
foo
}, 82 | { 83 | path: '/bar/*', 84 | element:
bar
, 85 | children: [{ path: '/bar/baz', element:
baz
}], 86 | }, 87 | ] 88 | 89 | function Routes() { 90 | return {useGuardedRoutes([routes])} 91 | } 92 | 93 | export default function App() { 94 | return ( 95 | 96 | 97 | 98 | 99 | 100 | ) 101 | } 102 | ``` 103 | 104 | ### Guarding 105 | 106 | You can provide `GuardProvider` with multiple guards middleware for route guarding, `GuardProvider` can receive an array of guards and a fallback element (can be used to load loading state). 107 | 108 | ```tsx 109 | import { BrowserRouter } from 'react-router-dom' 110 | import { 111 | GuardConfigProvider, 112 | GuardedRoute, 113 | GuardedRoutes, 114 | GuardMiddleware, 115 | GuardProvider, 116 | } from 'react-router-guarded-routes' 117 | 118 | const logGuard: GuardMiddleware = (to, from, next) => { 119 | console.log(to) // { location, matches, route } 120 | console.log(from) 121 | next() // call next function to run the next middleware or show the route element, it accepts the same parameters as navigate (useNavigate()) and behaves consistently. 122 | } 123 | 124 | // you can use object to determine whether you need to register middleware 125 | const barGuard: GuardMiddleware = { 126 | handler: (to, from, next) => { 127 | console.log('bar') 128 | next() 129 | }, 130 | register: (to, from) => { 131 | // only matched with `/bar` can be executed. 132 | if (to.location.pathname.startsWith('/bar')) { 133 | return true 134 | } 135 | return false 136 | }, 137 | } 138 | 139 | const guards = [logGuard, barGuard] 140 | 141 | export default function App() { 142 | return ( 143 | 144 | 145 | {/* Guard all routes below. */} 146 | loading...} guards={guards}> 147 | 148 | foo} path="/foo" /> 149 | bar} path="/bar/*"> 150 | baz} path="/bar/baz" /> 151 | 152 | 153 | 154 | 155 | 156 | ) 157 | } 158 | ``` 159 | 160 | Of course, you can also set up separate fallbacks and guards for each route. 161 | 162 | ```tsx 163 | import { BrowserRouter, Outlet } from 'react-router-dom' 164 | import { 165 | GuardConfigProvider, 166 | GuardedRoute, 167 | GuardedRoutes, 168 | GuardMiddleware, 169 | GuardProvider, 170 | } from 'react-router-guarded-routes' 171 | 172 | const logGuard: GuardMiddleware = (to, from, next) => { 173 | console.log(to, from) 174 | next() 175 | } 176 | 177 | const fooGuard: GuardMiddleware = (to, from, next) => { 178 | console.log('foo') 179 | next() 180 | } 181 | 182 | const guards = [logGuard] 183 | const fooGuards = [fooGuard] 184 | 185 | export default function App() { 186 | return ( 187 | 188 | 189 | loading...} guards={guards}> 190 | 191 | loading foo...} 193 | guards={fooGuard} 194 | element={
foo
} 195 | path="/foo" 196 | /> 197 | 200 | bar 201 | 202 | 203 | } 204 | path="/bar/*" 205 | > 206 | baz} path="/bar/baz" /> 207 | 208 |
209 |
210 |
211 |
212 | ) 213 | } 214 | ``` 215 | 216 | You can also call `next.ctx('ctx value')` to transfer contextual information, and get it by `ctxValue` in the next guard middleware. The guard middleware is executed from outside to inside, left to right. 217 | 218 | ```tsx 219 | 220 | loading...} 222 | guards={(to, from, next) => { 223 | next.ctx('ctx value') 224 | }} 225 | > 226 | 227 | { 229 | console.log(ctxValue) // ctx value 230 | next() 231 | }} 232 | element={
foo
} 233 | path="/foo" 234 | /> 235 |
236 |
237 |
238 | ``` 239 | 240 | And call `next.end()` to ignore remaining middleware. 241 | 242 | ```tsx 243 | 244 | loading...} 246 | guards={ 247 | ((to, from, next) => { 248 | next.end() 249 | }, 250 | () => { 251 | console.log('will not be called') 252 | }) 253 | } 254 | > 255 | 256 | { 258 | console.log('will not be called') 259 | }} 260 | element={
foo
} 261 | path="/foo" 262 | /> 263 |
264 |
265 |
266 | ``` 267 | 268 | ## API 269 | 270 | ### Types 271 | 272 | ```ts 273 | import React from 'react' 274 | import { 275 | Location, 276 | NavigateFunction, 277 | RouteMatch, 278 | RouteObject, 279 | } from 'react-router' 280 | import { ReplacePick } from 'types-kit' 281 | 282 | export interface GuardedRouteConfig { 283 | guards?: GuardMiddleware[] 284 | fallback?: React.ReactNode 285 | [props: PropertyKey]: any 286 | } 287 | 288 | export type GuardedRouteObject = RouteObject & 289 | GuardedRouteConfig & { 290 | children?: GuardedRouteObject[] 291 | } 292 | 293 | export interface NextFunction extends NavigateFunction { 294 | (): void 295 | ctx: (value: T) => void 296 | end: () => void 297 | } 298 | 299 | export interface GuardedRouteMatch 300 | extends Omit, 'route'> { 301 | route: GuardedRouteObject 302 | } 303 | 304 | export interface ToGuardRouteOptions { 305 | location: Location 306 | matches: GuardedRouteMatch[] 307 | route: GuardedRouteObject 308 | } 309 | 310 | export interface FromGuardRouteOptions 311 | extends ReplacePick< 312 | ToGuardRouteOptions, 313 | ['location', 'route'], 314 | [ 315 | ToGuardRouteOptions['location'] | null, 316 | ToGuardRouteOptions['route'] | null 317 | ] 318 | > {} 319 | 320 | export interface ExternalOptions { 321 | ctxValue: T 322 | injectedValue: I 323 | } 324 | 325 | export type GuardMiddlewareFunction = ( 326 | to: ToGuardRouteOptions, 327 | from: FromGuardRouteOptions, 328 | next: NextFunction, 329 | externalOptions: ExternalOptions 330 | ) => Promise | void 331 | 332 | export type GuardMiddlewareObject = { 333 | handler: GuardMiddlewareFunction 334 | register?: ( 335 | to: ToGuardRouteOptions, 336 | from: FromGuardRouteOptions 337 | ) => Promise | boolean 338 | } 339 | export type GuardMiddleware = 340 | | GuardMiddlewareFunction 341 | | GuardMiddlewareObject 342 | ``` 343 | 344 | ### Components 345 | 346 | #### GuardConfigProvider 347 | 348 | The `GuardConfigProvider` has configuration about routing, should not be used more than one in an app, make sure it's at the topmost level inside the Router (`BrowserRouter` and `HashRouter`). 349 | 350 | And it provides APIs for whether to run guard middleware and whether to display the fallback element: 351 | 352 | ##### Props 353 | 354 | ```tsx 355 | import React from 'react' 356 | 357 | export interface GuardConfigProviderProps { 358 | enableGuard?: ( 359 | location: ToGuardRouteOptions, 360 | prevLocation: FromGuardRouteOptions 361 | ) => Promise | boolean 362 | enableFallback?: ( 363 | location: ToGuardRouteOptions, 364 | prevLocation: FromGuardRouteOptions 365 | ) => boolean 366 | children: React.ReactNode 367 | } 368 | ``` 369 | 370 | | Prop | Optional | Default | Description | 371 | | ---------------- | :------: | :------------------------------------------------------------: | --------------------------------------- | 372 | | `enableGuards` | Yes | (to, from) => to.location.pathname !== from.location?.pathname | whether to run guard middleware | 373 | | `enableFallback` | Yes | () => true | whether to display the fallback element | 374 | 375 | ##### Setup 376 | 377 | ```tsx 378 | import { BrowserRouter } from 'react-router-dom' 379 | import { GuardConfigProvider } from 'react-router-guarded-routes' 380 | export default function App() { 381 | return ( 382 | 383 | 384 | { 385 | // routes 386 | } 387 | 388 | 389 | ) 390 | } 391 | ``` 392 | 393 | #### GuardProvider 394 | 395 | It provides public fallback element and guard middleware for `GuardedRoute`. 396 | 397 | ##### Props 398 | 399 | ```tsx 400 | import React from 'react' 401 | 402 | export interface GuardProviderProps { 403 | fallback?: React.ReactElement 404 | useInject?: ( 405 | to: ToGuardRouteOptions, 406 | from: FromGuardRouteOptions 407 | ) => Record 408 | guards?: GuardedRouteConfig['guards'] 409 | children: React.ReactNode 410 | } 411 | ``` 412 | 413 | | Prop | Optional | Default | Description | 414 | | ----------- | :------: | :-----: | ------------------------------------------------------------------------------------------------------------------------------------------ | 415 | | `fallback` | Yes | | a fallback element to show when a `GuardedRoute` run guard middleware | 416 | | `useInject` | Yes | | an injected value (React hooks can be used) for guard middleware to use, will be automatically merged the values of nested `GuardProvider` | 417 | | `guards` | Yes | | the guards to set for routes inside the `GuardProvider` | 418 | 419 | ##### Setup 420 | 421 | ```tsx 422 | import { BrowserRouter } from 'react-router-dom' 423 | import { 424 | GuardConfigProvider, 425 | GuardedRoute, 426 | GuardedRoutes, 427 | GuardMiddleware, 428 | GuardProvider, 429 | } from 'react-router-guarded-routes' 430 | 431 | const logGuard: GuardMiddleware = (to, from, next) => { 432 | console.log(to, from) 433 | next() 434 | } 435 | 436 | export default function App() { 437 | return ( 438 | 439 | 440 | loading...} guards={[logGuard]}> 441 | 442 | foo} path="/foo" /> 443 | 444 | 445 | 446 | 447 | ) 448 | } 449 | ``` 450 | 451 | Use nested GuardProvider: 452 | 453 | ```tsx 454 | 455 | loading...}> 456 | 457 | foo} path="/foo" /> 458 | loading2...}> 459 | 462 | bar 463 | 464 | 465 | } 466 | path="/bar/*" 467 | > 468 | baz} path="/bar/baz" /> 469 | 470 | 471 | 472 | 473 | 474 | ``` 475 | 476 | Inject value: 477 | 478 | ```tsx 479 | import { createContext } from 'react' 480 | import { BrowserRouter } from 'react-router-dom' 481 | import { 482 | GuardConfigProvider, 483 | GuardedRoute, 484 | GuardedRoutes, 485 | GuardProvider, 486 | } from 'react-router-guarded-routes' 487 | 488 | export const AuthContext = createContext({ 489 | isLogin: false, 490 | }) 491 | 492 | export function useAuth() { 493 | return useContext(AuthContext) 494 | } 495 | 496 | export default function App() { 497 | return ( 498 | 499 | 500 | 501 | loading...} 503 | useInject={useAuth} 504 | guards={[ 505 | (to, from, next, { injectedValue }) => { 506 | console.log(injectedValue) // { isLogin: false } 507 | next() 508 | }, 509 | ]} 510 | > 511 | 512 | foo} path="/foo" /> 513 | 514 | 515 | 516 | 517 | 518 | ) 519 | } 520 | ``` 521 | 522 | #### GuardedRoutes 523 | 524 | The `GuardedRoutes` component acts as a replacement for the default `Routes` component provided by React Router. 525 | 526 | ##### Props 527 | 528 | ```tsx 529 | import { RoutesProps } from 'react-router' 530 | 531 | export interface GuardedRoutesProps extends RoutesProps {} 532 | ``` 533 | 534 | ##### Setup 535 | 536 | ```tsx 537 | 538 | 539 | 540 | foo} path="/foo" /> 541 | 542 | 543 | 544 | ``` 545 | 546 | #### GuardedRoute 547 | 548 | The `GuardedRoute` component acts as a replacement for the default `Route` component provided by React Router, allowing for routes to use guard middleware and accepting the same props as regular `Route`. 549 | 550 | ##### Props 551 | 552 | ```tsx 553 | import { Route } from 'react-router' 554 | type RouteProps = Parameters[0] 555 | 556 | export type GuardedRouteProps = RouteProps & GuardedRouteConfig 557 | ``` 558 | 559 | The following table explains the guard-specific props for this component. 560 | 561 | | Prop | Optional | Default | Description | 562 | | ---------- | :------: | :-----: | ---------------------------------------------------------------------------------------------------------------------------------- | 563 | | `fallback` | Yes | | a fallback element to show when a `GuardedRoute` run guard middleware. (it will override the fallback provided by `GuardProvider`) | 564 | | `guards` | Yes | | the guards to set for the route | 565 | 566 | ##### Setup 567 | 568 | ```tsx 569 | 570 | foo} 572 | path="/foo" 573 | fallback={
loading...
} 574 | guards={[ 575 | (to, from, next) => { 576 | next() 577 | }, 578 | ]} 579 | /> 580 |
581 | ``` 582 | 583 | ### Hooks 584 | 585 | #### useGuardedRoutes 586 | 587 | The `useGuardedRoutes` hook acts as a replacement for the default `useRoutes` hook provided by React Router, and additionally provides `fallback` and `guards` properties for each member. 588 | 589 | ##### Props 590 | 591 | ```tsx 592 | import { useRoutes } from 'react-router' 593 | 594 | type LocationArg = Parameters[1] 595 | 596 | export function useGuardedRoutes( 597 | guardedRoutes: GuardedRouteObject[], 598 | locationArg?: LocationArg 599 | ): ReturnType 600 | ``` 601 | 602 | ##### Setup 603 | 604 | ```tsx 605 | import { 606 | GuardedRouteObject, 607 | useGuardedRoutes, 608 | } from 'react-router-guarded-routes' 609 | const routes: GuardedRouteObject[] = [ 610 | { 611 | path: '/foo', 612 | element:
foo
, 613 | fallback:
loading foo...
, 614 | guards: [(to, from, next) => next()], 615 | }, 616 | ] 617 | 618 | function Routes() { 619 | return <>{useGuardedRoutes(routes)} 620 | } 621 | 622 | export default function App() { 623 | return ( 624 | 625 | 626 | loading...}> 627 | 628 | 629 | 630 | 631 | ) 632 | } 633 | ``` 634 | -------------------------------------------------------------------------------- /__tests__/guard-config-provider.test.tsx: -------------------------------------------------------------------------------- 1 | import { MemoryRouter, useNavigate } from 'react-router' 2 | import type { ReactTestRenderer } from 'react-test-renderer' 3 | import TestRenderer from 'react-test-renderer' 4 | import { 5 | GuardConfigProvider, 6 | GuardConfigProviderProps, 7 | GuardedRouteObject, 8 | useGuardedRoutes, 9 | } from '../src' 10 | import { createPromiseHandler, noop } from './utils' 11 | 12 | function RoutesRenderer({ 13 | routes, 14 | ...props 15 | }: { routes: GuardedRouteObject[] } & Omit< 16 | GuardConfigProviderProps, 17 | 'children' 18 | >) { 19 | return ( 20 | 21 | {useGuardedRoutes(routes)} 22 | 23 | ) 24 | } 25 | 26 | beforeAll(() => { 27 | jest.useFakeTimers() 28 | }) 29 | 30 | afterAll(() => { 31 | jest.useRealTimers() 32 | }) 33 | 34 | describe('', () => { 35 | it('should enable guard middleware and fallback element and by default', async () => { 36 | await TestRenderer.act(async () => { 37 | const routes: GuardedRouteObject[] = [ 38 | { 39 | path: 'home', 40 | element:

home

, 41 | fallback:
loading...
, 42 | guards: [() => {}], 43 | }, 44 | ] 45 | 46 | let renderer!: ReactTestRenderer 47 | await TestRenderer.act(() => { 48 | renderer = TestRenderer.create( 49 | 50 | 51 | 52 | ) 53 | }) 54 | 55 | expect(renderer.toJSON()).toMatchInlineSnapshot(` 56 |
57 | loading... 58 |
59 | `) 60 | }) 61 | }) 62 | 63 | describe('enableFallback', () => { 64 | it('always return false', async () => { 65 | await TestRenderer.act(async () => { 66 | const routes: GuardedRouteObject[] = [ 67 | { 68 | path: 'home', 69 | element:

home

, 70 | fallback:
loading...
, 71 | guards: [() => {}], 72 | }, 73 | ] 74 | 75 | let renderer!: ReactTestRenderer 76 | await TestRenderer.act(() => { 77 | renderer = TestRenderer.create( 78 | 79 | false} /> 80 | 81 | ) 82 | }) 83 | 84 | expect(renderer.toJSON()).toBeNull() 85 | }) 86 | }) 87 | 88 | it('should render fallback element when the path is matched', async () => { 89 | await TestRenderer.act(async () => { 90 | const promiseHandler = createPromiseHandler() 91 | const routes: GuardedRouteObject[] = [ 92 | { 93 | path: 'home', 94 | element:

home

, 95 | fallback:
loading...
, 96 | guards: [ 97 | (to, from, next) => { 98 | setTimeout(() => { 99 | next('/about') 100 | promiseHandler.call() 101 | }, 2000) 102 | }, 103 | ], 104 | }, 105 | { 106 | path: 'about', 107 | element:

about

, 108 | fallback:
loading...
, 109 | guards: [noop], 110 | }, 111 | ] 112 | 113 | let renderer!: ReactTestRenderer 114 | await TestRenderer.act(() => { 115 | renderer = TestRenderer.create( 116 | 117 | to.location.pathname === '/about'} 120 | /> 121 | 122 | ) 123 | }) 124 | 125 | expect(renderer.toJSON()).toBeNull() 126 | jest.runAllTimers() 127 | // await a micro task 128 | await promiseHandler.promise 129 | expect(renderer.toJSON()).toMatchInlineSnapshot(` 130 |
131 | loading... 132 |
133 | `) 134 | }) 135 | }) 136 | }) 137 | 138 | describe('enableGuards', () => { 139 | it('always return false', async () => { 140 | await TestRenderer.act(async () => { 141 | const routes: GuardedRouteObject[] = [ 142 | { 143 | path: 'home', 144 | element:

home

, 145 | fallback:
loading...
, 146 | guards: [() => {}], 147 | }, 148 | ] 149 | 150 | let renderer!: ReactTestRenderer 151 | await TestRenderer.act(() => { 152 | renderer = TestRenderer.create( 153 | 154 | false} /> 155 | 156 | ) 157 | }) 158 | 159 | expect(renderer.toJSON()).toMatchInlineSnapshot(` 160 |

161 | home 162 |

163 | `) 164 | }) 165 | }) 166 | 167 | it('should run guards when the path is matched', async () => { 168 | function Home() { 169 | const navigate = useNavigate() 170 | return ( 171 |
172 | home 173 | 174 |
175 | ) 176 | } 177 | await TestRenderer.act(async () => { 178 | const routes: GuardedRouteObject[] = [ 179 | { 180 | path: 'home', 181 | element: , 182 | fallback:
loading...
, 183 | guards: [noop], 184 | }, 185 | { 186 | path: 'about', 187 | element:

about

, 188 | fallback:
loading...
, 189 | guards: [noop], 190 | }, 191 | ] 192 | 193 | let renderer!: ReactTestRenderer 194 | await TestRenderer.act(() => { 195 | renderer = TestRenderer.create( 196 | 197 | to.location.pathname === '/about'} 200 | /> 201 | 202 | ) 203 | }) 204 | 205 | expect(renderer.toJSON()).toMatchInlineSnapshot(` 206 |
207 | home 208 | 213 |
214 | `) 215 | 216 | const button = renderer.root.findByType('button') 217 | 218 | await TestRenderer.act(() => { 219 | button.props.onClick() 220 | }) 221 | 222 | expect(renderer.toJSON()).toMatchInlineSnapshot(` 223 |
224 | loading... 225 |
226 | `) 227 | }) 228 | }) 229 | }) 230 | }) 231 | -------------------------------------------------------------------------------- /__tests__/guard-provider.test.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react' 2 | import { MemoryRouter, Outlet, useNavigate } from 'react-router' 3 | import type { ReactTestRenderer } from 'react-test-renderer' 4 | import TestRenderer from 'react-test-renderer' 5 | import { 6 | GuardConfigProvider, 7 | GuardedRouteObject, 8 | GuardProvider, 9 | useGuardedRoutes, 10 | } from '../src' 11 | import { createPromiseHandler, noop } from './utils' 12 | 13 | function RoutesRenderer({ routes }: { routes: GuardedRouteObject[] }) { 14 | return {useGuardedRoutes(routes)} 15 | } 16 | 17 | beforeAll(() => { 18 | jest.useFakeTimers() 19 | }) 20 | 21 | afterAll(() => { 22 | jest.useRealTimers() 23 | }) 24 | 25 | let consoleLog!: jest.SpyInstance 26 | 27 | beforeEach(() => { 28 | consoleLog = jest.spyOn(console, 'log').mockImplementation(() => {}) 29 | }) 30 | 31 | afterEach(() => { 32 | consoleLog.mockRestore() 33 | }) 34 | 35 | describe('', () => { 36 | it('should render the fallback element provided by when a route does not have a fallback element', () => { 37 | const routes: GuardedRouteObject[] = [ 38 | { 39 | element: ( 40 | loading...}> 41 | 42 | 43 | ), 44 | children: [{ path: 'home', element:

home

, guards: [noop] }], 45 | }, 46 | ] 47 | 48 | let renderer!: ReactTestRenderer 49 | TestRenderer.act(() => { 50 | renderer = TestRenderer.create( 51 | 52 | 53 | 54 | ) 55 | }) 56 | expect(renderer.toJSON()).toMatchInlineSnapshot(` 57 |
58 | loading... 59 |
60 | `) 61 | }) 62 | 63 | it('should render the fallback element provided by a route when it has a fallback element', () => { 64 | const routes: GuardedRouteObject[] = [ 65 | { 66 | element: ( 67 | loading...}> 68 | 69 | 70 | ), 71 | children: [ 72 | { 73 | path: 'home', 74 | element:

home

, 75 | guards: [noop], 76 | fallback:
loading home...
, 77 | }, 78 | ], 79 | }, 80 | ] 81 | 82 | let renderer!: ReactTestRenderer 83 | TestRenderer.act(() => { 84 | renderer = TestRenderer.create( 85 | 86 | 87 | 88 | ) 89 | }) 90 | expect(renderer.toJSON()).toMatchInlineSnapshot(` 91 |
92 | loading home... 93 |
94 | `) 95 | }) 96 | 97 | it('should run guard middleware provided by ', async () => { 98 | const promiseHandler = createPromiseHandler() 99 | let renderer!: ReactTestRenderer 100 | await TestRenderer.act(async () => { 101 | const routes: GuardedRouteObject[] = [ 102 | { 103 | element: ( 104 | { 107 | if (to.location.pathname !== '/about') { 108 | setTimeout(() => { 109 | next('/about') 110 | }, 2000) 111 | } else { 112 | promiseHandler.call() 113 | next() 114 | } 115 | }, 116 | ]} 117 | fallback={
loading...
} 118 | > 119 | 120 |
121 | ), 122 | children: [ 123 | { 124 | path: 'home', 125 | element:

home

, 126 | }, 127 | { 128 | path: 'about', 129 | element:

about

, 130 | }, 131 | ], 132 | }, 133 | ] 134 | 135 | await TestRenderer.act(() => { 136 | renderer = TestRenderer.create( 137 | 138 | 139 | 140 | ) 141 | }) 142 | expect(renderer.toJSON()).toMatchInlineSnapshot(` 143 |
144 | loading... 145 |
146 | `) 147 | jest.runAllTimers() 148 | }) 149 | // await a micro task 150 | await promiseHandler.promise 151 | expect(renderer.toJSON()).toMatchInlineSnapshot(` 152 |

153 | about 154 |

155 | `) 156 | }) 157 | 158 | it('show get the injected value', async () => { 159 | const state = { 160 | isLogin: false, 161 | } 162 | const AuthContext = createContext(state) 163 | function useAuth() { 164 | return useContext(AuthContext) 165 | } 166 | await TestRenderer.act(async () => { 167 | const routes: GuardedRouteObject[] = [ 168 | { 169 | element: ( 170 | { 173 | // eslint-disable-next-line no-console 174 | console.log(JSON.stringify(injectedValue)) 175 | next() 176 | }, 177 | ]} 178 | fallback={
loading...
} 179 | useInject={useAuth} 180 | > 181 | 182 |
183 | ), 184 | children: [{ path: 'home', element:

home

, guards: [noop] }], 185 | }, 186 | ] 187 | 188 | await TestRenderer.act(() => { 189 | TestRenderer.create( 190 | 191 | 192 | 193 | ) 194 | }) 195 | expect(consoleLog).toHaveBeenCalledWith( 196 | expect.stringContaining(JSON.stringify(state)) 197 | ) 198 | }) 199 | }) 200 | 201 | it('should get the ctx value', async () => { 202 | await TestRenderer.act(async () => { 203 | const routes: GuardedRouteObject[] = [ 204 | { 205 | element: ( 206 | { 209 | next.ctx('ctx value') 210 | }, 211 | ]} 212 | fallback={
loading...
} 213 | > 214 | 215 |
216 | ), 217 | children: [ 218 | { 219 | path: 'home', 220 | element:

home

, 221 | guards: [ 222 | (to, from, next, { ctxValue }) => { 223 | console.log(ctxValue) 224 | next() 225 | }, 226 | ], 227 | }, 228 | ], 229 | }, 230 | ] 231 | 232 | await TestRenderer.act(() => { 233 | TestRenderer.create( 234 | 235 | 236 | 237 | ) 238 | }) 239 | expect(consoleLog).toHaveBeenCalledWith( 240 | expect.stringContaining('ctx value') 241 | ) 242 | }) 243 | }) 244 | 245 | it('should ignore remaining middleware if `next.end()` is called', async () => { 246 | await TestRenderer.act(async () => { 247 | const routes: GuardedRouteObject[] = [ 248 | { 249 | element: ( 250 | { 253 | next.end() 254 | }, 255 | ]} 256 | fallback={
loading...
} 257 | > 258 | 259 |
260 | ), 261 | children: [ 262 | { 263 | path: 'home', 264 | element:

home

, 265 | guards: [ 266 | () => { 267 | console.log('home guard') 268 | }, 269 | ], 270 | }, 271 | ], 272 | }, 273 | ] 274 | let renderer!: ReactTestRenderer 275 | await TestRenderer.act(() => { 276 | renderer = TestRenderer.create( 277 | 278 | 279 | 280 | ) 281 | }) 282 | expect(consoleLog).not.toHaveBeenCalled() 283 | expect(renderer.toJSON()).toMatchInlineSnapshot(` 284 |

285 | home 286 |

287 | `) 288 | }) 289 | }) 290 | 291 | it('should register a guard middleware when matched a route', async () => { 292 | function Home() { 293 | const navigate = useNavigate() 294 | return ( 295 |
296 | home 297 | 298 |
299 | ) 300 | } 301 | let renderer!: ReactTestRenderer 302 | 303 | await TestRenderer.act(async () => { 304 | const routes: GuardedRouteObject[] = [ 305 | { 306 | element: ( 307 | {}, 311 | register(to) { 312 | if (to.location.pathname === '/about') { 313 | return true 314 | } 315 | return false 316 | }, 317 | }, 318 | ]} 319 | fallback={
loading...
} 320 | > 321 | 322 |
323 | ), 324 | children: [ 325 | { 326 | path: 'home', 327 | element: , 328 | }, 329 | { 330 | path: 'about', 331 | element:

about

, 332 | }, 333 | ], 334 | }, 335 | ] 336 | 337 | await TestRenderer.act(() => { 338 | renderer = TestRenderer.create( 339 | 340 | 341 | 342 | ) 343 | }) 344 | expect(renderer.toJSON()).toMatchInlineSnapshot(` 345 |
346 | home 347 | 352 |
353 | `) 354 | }) 355 | await TestRenderer.act(async () => { 356 | const button = renderer.root.findByType('button') 357 | await TestRenderer.act(() => { 358 | button.props.onClick() 359 | }) 360 | expect(renderer.toJSON()).toMatchInlineSnapshot(` 361 |
362 | loading... 363 |
364 | `) 365 | }) 366 | }) 367 | 368 | it('should include all guard middleware when there are nested providers', async () => { 369 | await TestRenderer.act(async () => { 370 | const routes: GuardedRouteObject[] = [ 371 | { 372 | element: ( 373 | loading...} 375 | guards={[ 376 | (to, from, next) => { 377 | // eslint-disable-next-line no-console 378 | console.log('guard 1') 379 | next() 380 | }, 381 | ]} 382 | > 383 | 384 | 385 | ), 386 | children: [ 387 | { 388 | element: ( 389 | loading2...} 391 | guards={[ 392 | (to, from, next) => { 393 | // eslint-disable-next-line no-console 394 | console.log('guard 2') 395 | next() 396 | }, 397 | ]} 398 | > 399 | 400 | 401 | ), 402 | children: [{ path: 'home', element:

home

}], 403 | }, 404 | ], 405 | }, 406 | ] 407 | 408 | await TestRenderer.act(() => { 409 | TestRenderer.create( 410 | 411 | 412 | 413 | ) 414 | }) 415 | }) 416 | expect(consoleLog).toHaveBeenCalledTimes(3) 417 | expect(consoleLog).toHaveBeenNthCalledWith( 418 | 1, 419 | expect.stringContaining(`guard 1`) 420 | ) 421 | expect(consoleLog).toHaveBeenNthCalledWith( 422 | 2, 423 | expect.stringContaining(`guard 1`) 424 | ) 425 | expect(consoleLog).toHaveBeenNthCalledWith( 426 | 3, 427 | expect.stringContaining(`guard 2`) 428 | ) 429 | }) 430 | }) 431 | -------------------------------------------------------------------------------- /__tests__/guarded-route.test.tsx: -------------------------------------------------------------------------------- 1 | import { MemoryRouter } from 'react-router' 2 | import type { ReactTestRenderer } from 'react-test-renderer' 3 | import TestRenderer from 'react-test-renderer' 4 | import { GuardConfigProvider, GuardedRoute, GuardedRoutes } from '../src' 5 | import { createPromiseHandler } from './utils' 6 | 7 | beforeAll(() => { 8 | jest.useFakeTimers() 9 | }) 10 | 11 | afterAll(() => { 12 | jest.useRealTimers() 13 | }) 14 | 15 | describe('A ', () => { 16 | it('renders its `element` prop', () => { 17 | let renderer!: ReactTestRenderer 18 | TestRenderer.act(() => { 19 | renderer = TestRenderer.create( 20 | 21 | 22 | 23 | Home} /> 24 | 25 | 26 | 27 | ) 28 | }) 29 | 30 | expect(renderer.toJSON()).toMatchInlineSnapshot(` 31 |

32 | Home 33 |

34 | `) 35 | }) 36 | 37 | it('renders its child routes when no `element` prop is given', () => { 38 | let renderer!: ReactTestRenderer 39 | TestRenderer.act(() => { 40 | renderer = TestRenderer.create( 41 | 42 | 43 | 44 | 45 | Home} /> 46 | 47 | 48 | 49 | 50 | ) 51 | }) 52 | 53 | expect(renderer.toJSON()).toMatchInlineSnapshot(` 54 |

55 | Home 56 |

57 | `) 58 | }) 59 | 60 | it('render its fallback element when guard middleware is given', async () => { 61 | await TestRenderer.act(async () => { 62 | const promiseHandler = createPromiseHandler() 63 | let renderer!: ReactTestRenderer 64 | await TestRenderer.act(() => { 65 | renderer = TestRenderer.create( 66 | 67 | 68 | 69 | Home} 72 | fallback={
loading...
} 73 | guards={[ 74 | (to, from, next) => { 75 | setTimeout(() => { 76 | next() 77 | promiseHandler.call() 78 | }, 2000) 79 | }, 80 | ]} 81 | /> 82 |
83 |
84 |
85 | ) 86 | }) 87 | expect(renderer.toJSON()).toMatchInlineSnapshot(` 88 |
89 | loading... 90 |
91 | `) 92 | jest.runAllTimers() 93 | await promiseHandler.promise 94 | expect(renderer.toJSON()).toMatchInlineSnapshot(` 95 |

96 | Home 97 |

98 | `) 99 | }) 100 | }) 101 | }) 102 | -------------------------------------------------------------------------------- /__tests__/guarded-routes.test.tsx: -------------------------------------------------------------------------------- 1 | import { MemoryRouter } from 'react-router' 2 | import TestRenderer from 'react-test-renderer' 3 | import { 4 | GuardConfigProvider, 5 | GuardedRoute, 6 | GuardedRouteProps, 7 | GuardedRoutes, 8 | GuardProvider, 9 | } from '../src' 10 | 11 | describe('', () => { 12 | let consoleWarn: jest.SpyInstance 13 | let consoleError: jest.SpyInstance 14 | 15 | beforeEach(() => { 16 | consoleWarn = jest.spyOn(console, 'warn').mockImplementation(() => {}) 17 | consoleError = jest.spyOn(console, 'error').mockImplementation(() => {}) 18 | }) 19 | 20 | afterEach(() => { 21 | consoleWarn.mockRestore() 22 | consoleError.mockRestore() 23 | }) 24 | 25 | it('renders with non-element children', () => { 26 | let renderer!: TestRenderer.ReactTestRenderer 27 | TestRenderer.act(() => { 28 | renderer = TestRenderer.create( 29 | 30 | 31 | 32 | Home} /> 33 | {false} 34 | {undefined} 35 | 36 | 37 | 38 | ) 39 | }) 40 | 41 | expect(renderer.toJSON()).toMatchInlineSnapshot(` 42 |

43 | Home 44 |

45 | `) 46 | }) 47 | 48 | it('renders with React.Fragment children', () => { 49 | let renderer!: TestRenderer.ReactTestRenderer 50 | TestRenderer.act(() => { 51 | renderer = TestRenderer.create( 52 | 53 | 54 | 55 | Home} /> 56 | <> 57 | Admin} /> 58 | 59 | 60 | 61 | 62 | ) 63 | }) 64 | 65 | expect(renderer.toJSON()).toMatchInlineSnapshot(` 66 |

67 | Admin 68 |

69 | `) 70 | }) 71 | 72 | it('renders with children', () => { 73 | let renderer!: TestRenderer.ReactTestRenderer 74 | TestRenderer.act(() => { 75 | renderer = TestRenderer.create( 76 | 77 | 78 | 79 | Home} /> 80 | 81 | Admin} /> 82 | 83 | 84 | 85 | 86 | ) 87 | }) 88 | 89 | expect(renderer.toJSON()).toMatchInlineSnapshot(` 90 |

91 | Admin 92 |

93 | `) 94 | }) 95 | 96 | it('throws if some is passed as a child of ', () => { 97 | const CustomRoute = (props: GuardedRouteProps) => ( 98 | 99 | ) 100 | 101 | expect(() => { 102 | TestRenderer.create( 103 | 104 | 105 | 106 | Home} /> 107 | Admin} /> 108 | 109 | 110 | 111 | ) 112 | }).toThrow(/children of must be a /) 113 | 114 | expect(consoleError).toHaveBeenCalledTimes(1) 115 | }) 116 | 117 | it('throws if a regular element (ex:
) is passed as a child of ', () => { 118 | expect(() => { 119 | TestRenderer.create( 120 | 121 | 122 | 123 | Home} /> 124 |
Admin } as any)} /> 125 | 126 | 127 | 128 | ) 129 | }).toThrow(/children of must be a /) 130 | 131 | expect(consoleError).toHaveBeenCalledTimes(1) 132 | }) 133 | 134 | it('throws if it has not been rendered in ', () => { 135 | expect(() => { 136 | TestRenderer.create( 137 | 138 | 139 | Home} /> 140 | 141 | 142 | ) 143 | }).toThrow(/ outside a ./) 144 | 145 | expect(consoleError).toHaveBeenCalledTimes(1) 146 | }) 147 | }) 148 | -------------------------------------------------------------------------------- /__tests__/useGuardedRoutes.test.tsx: -------------------------------------------------------------------------------- 1 | import { MemoryRouter } from 'react-router' 2 | import type { ReactTestRenderer } from 'react-test-renderer' 3 | import TestRenderer from 'react-test-renderer' 4 | import { 5 | GuardConfigProvider, 6 | GuardedRouteObject, 7 | useGuardedRoutes, 8 | } from '../src' 9 | import { createPromiseHandler } from './utils' 10 | 11 | function RoutesRenderer({ 12 | routes, 13 | location, 14 | }: { 15 | routes: GuardedRouteObject[] 16 | location?: Partial & { pathname: string } 17 | }) { 18 | return ( 19 | 20 | {useGuardedRoutes(routes, location)} 21 | 22 | ) 23 | } 24 | 25 | beforeAll(() => { 26 | jest.useFakeTimers() 27 | }) 28 | 29 | afterAll(() => { 30 | jest.useRealTimers() 31 | }) 32 | 33 | describe('useGuardedRoutes', () => { 34 | it('returns the matching element from a route config', () => { 35 | const routes: GuardedRouteObject[] = [ 36 | { path: 'home', element:

home

}, 37 | { path: 'about', element:

about

}, 38 | ] 39 | 40 | let renderer!: ReactTestRenderer 41 | TestRenderer.act(() => { 42 | renderer = TestRenderer.create( 43 | 44 | 45 | 46 | ) 47 | }) 48 | 49 | expect(renderer.toJSON()).toMatchInlineSnapshot(` 50 |

51 | home 52 |

53 | `) 54 | }) 55 | 56 | it('uses the `location` prop instead of context location`', () => { 57 | const routes: GuardedRouteObject[] = [ 58 | { path: 'one', element:

one

}, 59 | { path: 'two', element:

two

}, 60 | ] 61 | 62 | let renderer!: TestRenderer.ReactTestRenderer 63 | TestRenderer.act(() => { 64 | renderer = TestRenderer.create( 65 | 66 | 67 | 68 | ) 69 | }) 70 | 71 | expect(renderer.toJSON()).toMatchInlineSnapshot(` 72 |

73 | two 74 |

75 | `) 76 | }) 77 | 78 | describe('render with guards', () => { 79 | describe('when a guard does not call the `next()` function', () => { 80 | it('should render `null` when not has a fallback element', () => { 81 | const routes: GuardedRouteObject[] = [ 82 | { path: 'home', element:

home

, guards: [() => {}] }, 83 | ] 84 | 85 | let renderer!: ReactTestRenderer 86 | TestRenderer.act(() => { 87 | renderer = TestRenderer.create( 88 | 89 | 90 | 91 | ) 92 | }) 93 | expect(renderer.toJSON()).toBeNull() 94 | }) 95 | 96 | it('show render the fallback element when pass a fallback element as a parameter', () => { 97 | const routes: GuardedRouteObject[] = [ 98 | { 99 | path: 'home', 100 | element:

home

, 101 | guards: [() => {}], 102 | fallback:
loading...
, 103 | }, 104 | ] 105 | 106 | let renderer!: ReactTestRenderer 107 | TestRenderer.act(() => { 108 | renderer = TestRenderer.create( 109 | 110 | 111 | 112 | ) 113 | }) 114 | expect(renderer.toJSON()).toMatchInlineSnapshot(` 115 |
116 | loading... 117 |
118 | `) 119 | }) 120 | 121 | it('pass an object as a middleware and provide a register handler', async () => { 122 | await TestRenderer.act(async () => { 123 | const routes: GuardedRouteObject[] = [ 124 | { 125 | path: 'home', 126 | element:

home

, 127 | guards: [ 128 | { 129 | handler: () => {}, 130 | register: () => false, 131 | }, 132 | ], 133 | }, 134 | ] 135 | 136 | let renderer!: ReactTestRenderer 137 | await TestRenderer.act(() => { 138 | renderer = TestRenderer.create( 139 | 140 | 141 | 142 | ) 143 | }) 144 | expect(renderer.toJSON()).toMatchInlineSnapshot(` 145 |

146 | home 147 |

148 | `) 149 | }) 150 | }) 151 | }) 152 | 153 | describe('when a guard has called the `next()` function', () => { 154 | it('should render the route element after 2000ms', async () => { 155 | const promiseHandler = createPromiseHandler() 156 | await TestRenderer.act(async () => { 157 | const routes: GuardedRouteObject[] = [ 158 | { 159 | path: 'home', 160 | element:

home

, 161 | fallback:
loading...
, 162 | guards: [ 163 | (from, to, next) => { 164 | setTimeout(() => { 165 | next() 166 | promiseHandler.call() 167 | }, 2000) 168 | }, 169 | ], 170 | }, 171 | ] 172 | 173 | let renderer!: ReactTestRenderer 174 | await TestRenderer.act(() => { 175 | renderer = TestRenderer.create( 176 | 177 | 178 | 179 | ) 180 | }) 181 | expect(renderer.toJSON()).toMatchInlineSnapshot(` 182 |
183 | loading... 184 |
185 | `) 186 | jest.runAllTimers() 187 | // await a micro task 188 | await promiseHandler.promise 189 | expect(renderer.toJSON()).toMatchInlineSnapshot(` 190 |

191 | home 192 |

193 | `) 194 | }) 195 | }) 196 | }) 197 | }) 198 | }) 199 | -------------------------------------------------------------------------------- /__tests__/utils.ts: -------------------------------------------------------------------------------- 1 | export function noop() {} 2 | 3 | export function createPromiseHandler() { 4 | let call: () => void = () => {} 5 | const promise = new Promise((resolve) => { 6 | call = resolve 7 | }) 8 | return { 9 | call, 10 | promise, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * build: build tools (such as webpack, vite, ...) and scripts changes. 3 | * ci: ci changes. 4 | * feat: new features. 5 | * docs: docs changes. 6 | * fix: fix bug. 7 | * perf: performance optimization. 8 | * refactor: feature refactoring. 9 | * revert: revert the last commit. 10 | * style: coding style changes. 11 | * test: test changes. 12 | * anno: annotation changes. 13 | * type: types changes. 14 | * chore: misc. 15 | */ 16 | 17 | module.exports = { 18 | extends: ['@commitlint/config-angular'], 19 | rules: { 20 | 'type-enum': [ 21 | 2, 22 | 'always', 23 | [ 24 | 'build', 25 | 'ci', 26 | 'docs', 27 | 'feat', 28 | 'fix', 29 | 'perf', 30 | 'refactor', 31 | 'revert', 32 | 'style', 33 | 'test', 34 | 'anno', 35 | 'type', 36 | 'chore', 37 | ], 38 | ], 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /examples/auth/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/auth/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Router Guarded Routes - Auth 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "build": "tsc && vite build", 8 | "dev": "vite", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "react": "^18.0.0", 13 | "react-dom": "^18.0.0", 14 | "react-router": "^6.3.0", 15 | "react-router-dom": "^6.3.0", 16 | "react-router-guarded-routes": "workspace:*" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^18.0.15", 20 | "@types/react-dom": "^18.0.6", 21 | "@vitejs/plugin-react": "^2.0.0", 22 | "typescript": "^4.6.4", 23 | "vite": "^3.0.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/auth/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/auth/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo, useState } from 'react' 2 | import { BrowserRouter } from 'react-router-dom' 3 | import { 4 | GuardConfigProvider, 5 | GuardMiddleware, 6 | GuardProvider, 7 | useGuardedRoutes, 8 | } from 'react-router-guarded-routes' 9 | import { routes } from './routes' 10 | import { AuthContext, AuthContextValue, useAuth } from './store' 11 | 12 | const authGuard: GuardMiddleware = ( 13 | to, 14 | from, 15 | next, 16 | { injectedValue } 17 | ) => { 18 | if (to.location.pathname !== '/login' && !injectedValue.state.isLogin) { 19 | next('/login', { 20 | replace: true, 21 | }) 22 | return 23 | } 24 | if (to.location.pathname === '/login' && injectedValue.state.isLogin) { 25 | next('/home', { 26 | replace: true, 27 | }) 28 | return 29 | } 30 | next() 31 | } 32 | 33 | const Routes: React.FC = () => { 34 | return <>{useGuardedRoutes(routes)} 35 | } 36 | 37 | const App: React.FC = () => { 38 | const [isLogin, setIsLogin] = useState(false) 39 | const context: AuthContextValue = useMemo( 40 | () => ({ 41 | state: { 42 | isLogin, 43 | }, 44 | methods: { 45 | login: () => { 46 | setIsLogin(true) 47 | }, 48 | logout: () => { 49 | setIsLogin(false) 50 | }, 51 | }, 52 | }), 53 | [isLogin] 54 | ) 55 | return ( 56 | 57 | 58 | loading...
} 60 | useInject={useAuth} 61 | guards={[authGuard]} 62 | > 63 | 64 | 65 | 66 | 67 |
68 | 69 | ) 70 | } 71 | 72 | export default App 73 | -------------------------------------------------------------------------------- /examples/auth/src/Home.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link, useNavigate } from 'react-router-dom' 3 | import { 4 | GuardedRoute, 5 | GuardedRoutes, 6 | GuardProvider, 7 | } from 'react-router-guarded-routes' 8 | import { useAuth } from './store' 9 | 10 | const Home: React.FC = () => { 11 | const { methods } = useAuth() 12 | const navigate = useNavigate() 13 | return ( 14 |
15 |

Home

16 | 17 | 24 |
32 | Home-Foo 33 | Home-Bar 34 | Home-Ban 35 |
36 | 37 | loading home...
} 39 | guards={[ 40 | (to, from, next) => { 41 | if (to.location.pathname.includes('ban')) { 42 | setTimeout(() => { 43 | next(-1) 44 | }, 2000) 45 | } else { 46 | next() 47 | } 48 | }, 49 | ]} 50 | > 51 | foo} /> 52 | bar} /> 53 | loading ban...
} 56 | element={

ban

} 57 | /> 58 |
59 |
60 | 61 | ) 62 | } 63 | 64 | export default Home 65 | -------------------------------------------------------------------------------- /examples/auth/src/Login.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useNavigate } from 'react-router' 3 | import { useAuth } from './store' 4 | 5 | const Login: React.FC = () => { 6 | const navigate = useNavigate() 7 | const { methods } = useAuth() 8 | return ( 9 |
10 |

Login Page

11 | 12 | 13 |
14 | ) 15 | } 16 | 17 | export default Login 18 | -------------------------------------------------------------------------------- /examples/auth/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | 5 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 6 | 7 | 8 | 9 | ) 10 | -------------------------------------------------------------------------------- /examples/auth/src/routes.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate } from 'react-router' 2 | import { GuardedRouteObject } from 'react-router-guarded-routes' 3 | import Home from './Home' 4 | import Login from './Login' 5 | 6 | export const routes: GuardedRouteObject[] = [ 7 | { 8 | path: '/', 9 | element: , 10 | }, 11 | { 12 | // for the nested routes 13 | path: 'home/*', 14 | element: , 15 | }, 16 | { 17 | path: 'login', 18 | element: , 19 | }, 20 | ] 21 | -------------------------------------------------------------------------------- /examples/auth/src/store.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react' 2 | 3 | function noop() {} 4 | 5 | export interface AuthContextState { 6 | isLogin: boolean 7 | } 8 | export interface AuthContextMethods { 9 | login: () => void 10 | logout: () => void 11 | } 12 | export interface AuthContextValue { 13 | state: AuthContextState 14 | methods: AuthContextMethods 15 | } 16 | 17 | export function createInitialAuthValue(): AuthContextState { 18 | return { 19 | isLogin: false, 20 | } 21 | } 22 | 23 | export const AuthContext = createContext({ 24 | state: createInitialAuthValue(), 25 | methods: { 26 | login: noop, 27 | logout: noop, 28 | }, 29 | }) 30 | 31 | export function useAuth() { 32 | return useContext(AuthContext) 33 | } 34 | -------------------------------------------------------------------------------- /examples/auth/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/auth/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /examples/auth/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/auth/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react' 2 | import { defineConfig } from 'vite' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /examples/basic/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Router Guarded Routes - Basic 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic", 3 | "private": true, 4 | "scripts": { 5 | "build": "tsc && vite build", 6 | "dev": "vite", 7 | "preview": "vite preview" 8 | }, 9 | "dependencies": { 10 | "react": "^18.0.0", 11 | "react-dom": "^18.0.0", 12 | "react-router": "^6.3.0", 13 | "react-router-dom": "^6.3.0", 14 | "react-router-guarded-routes": "workspace:*" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^18.0.0", 18 | "@types/react-dom": "^18.0.0", 19 | "@vitejs/plugin-react": "^2.0.0", 20 | "typescript": "^4.6.3", 21 | "vite": "^3.0.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/basic/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/basic/src/About.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link, Outlet } from 'react-router-dom' 3 | import { GuardProvider } from 'react-router-guarded-routes' 4 | 5 | const About: React.FC = () => { 6 | return ( 7 |
8 |

About

9 |
17 | Home-Foo 18 | Home-Bar 19 |
20 | loading about...
} 22 | guards={[ 23 | { 24 | handler: (to, from, next) => { 25 | setTimeout(() => { 26 | next(-1) 27 | }, 1000) 28 | }, 29 | register: (to) => { 30 | if (to.location.pathname.includes('foo')) { 31 | return true 32 | } 33 | return false 34 | }, 35 | }, 36 | ]} 37 | > 38 | 39 |
40 | 41 | ) 42 | } 43 | 44 | export default About 45 | -------------------------------------------------------------------------------- /examples/basic/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { BrowserRouter, Link } from 'react-router-dom' 3 | import { 4 | GuardConfigProvider, 5 | GuardMiddleware, 6 | GuardProvider, 7 | useGuardedRoutes, 8 | } from 'react-router-guarded-routes' 9 | import { routes } from './routes' 10 | const logGuard: GuardMiddleware = (to, from, next) => { 11 | console.log('to: ', to) 12 | console.log('from: ', from) 13 | next() 14 | } 15 | const Routes: React.FC = () => { 16 | return <>{useGuardedRoutes(routes)} 17 | } 18 | const App: React.FC = () => { 19 | return ( 20 | 21 | 22 | loading...} guards={[logGuard]}> 23 |
31 | Home 32 | About 33 |
34 | 35 |
36 |
37 |
38 | ) 39 | } 40 | 41 | export default App 42 | -------------------------------------------------------------------------------- /examples/basic/src/Home.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { Link } from 'react-router-dom' 3 | import { 4 | GuardedRoute, 5 | GuardedRoutes, 6 | GuardProvider, 7 | } from 'react-router-guarded-routes' 8 | // use hooks in inject function 9 | function useInject() { 10 | return useState(0) 11 | } 12 | const Home: React.FC = () => { 13 | return ( 14 |
15 |

Home

16 |
24 | Home-Foo 25 | Home-Bar 26 | Home-Ban 27 |
28 | 29 | loading home...
} 31 | useInject={useInject} 32 | guards={[ 33 | (to, from, next, { injectedValue }) => { 34 | console.log(injectedValue) 35 | if (to.location.pathname.includes('ban')) { 36 | setTimeout(() => { 37 | next(-1) 38 | }, 2000) 39 | } else { 40 | setTimeout(() => { 41 | next() 42 | }, 500) 43 | } 44 | }, 45 | (to, from, next) => { 46 | console.log('`next.end()` is called') 47 | next.end() 48 | }, 49 | (to, from, next) => { 50 | console.log('will not be called') 51 | next() 52 | }, 53 | ]} 54 | > 55 | foo} /> 56 | bar} /> 57 | loading ban...} 60 | element={

ban

} 61 | /> 62 |
63 | 64 | 65 | ) 66 | } 67 | 68 | export default Home 69 | -------------------------------------------------------------------------------- /examples/basic/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | 5 | const root = document.getElementById('root') 6 | if (root) { 7 | ReactDOM.createRoot(root).render( 8 | 9 | 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /examples/basic/src/routes.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate, Outlet } from 'react-router' 2 | import { GuardedRouteObject, GuardProvider } from 'react-router-guarded-routes' 3 | import About from './About' 4 | import Home from './Home' 5 | 6 | export const routes: GuardedRouteObject[] = [ 7 | { 8 | path: '/', 9 | element: , 10 | }, 11 | { 12 | element: ( 13 | { 16 | next.ctx('ctx value') 17 | }, 18 | ]} 19 | > 20 | 21 | 22 | ), 23 | children: [ 24 | { 25 | // for the nested routes 26 | path: 'home/*', 27 | element: , 28 | }, 29 | { 30 | path: 'about', 31 | element: , 32 | children: [ 33 | { 34 | path: 'foo', 35 | element:

foo

, 36 | guards: [ 37 | (to, from, next) => { 38 | console.log('matched about foo') 39 | next() 40 | }, 41 | ], 42 | fallback:
loading about foo...
, 43 | }, 44 | { 45 | guards: [ 46 | (to, from, next, { ctxValue }) => { 47 | console.log('matched about bar') 48 | console.log('ctxValue:', ctxValue) 49 | next() 50 | }, 51 | ], 52 | path: 'bar', 53 | element:

bar

, 54 | }, 55 | ], 56 | }, 57 | ], 58 | }, 59 | ] 60 | -------------------------------------------------------------------------------- /examples/basic/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /examples/basic/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /examples/basic/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react' 2 | import { defineConfig } from 'vite' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import { Config } from 'jest' 2 | /* 3 | * For a detailed explanation regarding each configuration property and type check, visit: 4 | * https://jestjs.io/docs/configuration 5 | */ 6 | 7 | export default { 8 | // All imported modules in your tests should be mocked automatically 9 | // automock: false, 10 | 11 | // Stop running tests after `n` failures 12 | // bail: 0, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/private/var/folders/yq/lqxmrng57_zgmsc6tqj_px3h0000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls, instances, contexts and results before every test 18 | clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: true, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: undefined, 25 | 26 | // The directory where Jest should output its coverage files 27 | coverageDirectory: 'coverage', 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // Indicates which provider should be used to instrument code for coverage 35 | // coverageProvider: "babel", 36 | 37 | // A list of reporter names that Jest uses when writing coverage reports 38 | // coverageReporters: [ 39 | // "json", 40 | // "text", 41 | // "lcov", 42 | // "clover" 43 | // ], 44 | 45 | // An object that configures minimum threshold enforcement for coverage results 46 | // coverageThreshold: undefined, 47 | 48 | // A path to a custom dependency extractor 49 | // dependencyExtractor: undefined, 50 | 51 | // Make calling deprecated APIs throw helpful error messages 52 | // errorOnDeprecated: false, 53 | 54 | // The default configuration for fake timers 55 | // fakeTimers: { 56 | // "enableGlobally": false 57 | // }, 58 | 59 | // Force coverage collection from ignored files using an array of glob patterns 60 | // forceCoverageMatch: [], 61 | 62 | // A path to a module which exports an async function that is triggered once before all test suites 63 | // globalSetup: undefined, 64 | 65 | // A path to a module which exports an async function that is triggered once after all test suites 66 | // globalTeardown: undefined, 67 | 68 | // A set of global variables that need to be available in all test environments 69 | // globals: {}, 70 | 71 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 72 | // maxWorkers: "50%", 73 | 74 | // An array of directory names to be searched recursively up from the requiring module's location 75 | // moduleDirectories: [ 76 | // "node_modules" 77 | // ], 78 | 79 | // An array of file extensions your modules use 80 | // moduleFileExtensions: [ 81 | // "js", 82 | // "mjs", 83 | // "cjs", 84 | // "jsx", 85 | // "ts", 86 | // "tsx", 87 | // "json", 88 | // "node" 89 | // ], 90 | 91 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 92 | // moduleNameMapper: {}, 93 | 94 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 95 | // modulePathIgnorePatterns: [], 96 | 97 | // Activates notifications for test results 98 | // notify: false, 99 | 100 | // An enum that specifies notification mode. Requires { notify: true } 101 | // notifyMode: "failure-change", 102 | 103 | // A preset that is used as a base for Jest's configuration 104 | preset: 'ts-jest', 105 | 106 | // Run tests from one or more projects 107 | // projects: undefined, 108 | 109 | // Use this configuration option to add custom reporters to Jest 110 | // reporters: undefined, 111 | 112 | // Automatically reset mock state before every test 113 | // resetMocks: false, 114 | 115 | // Reset the module registry before running each individual test 116 | // resetModules: false, 117 | 118 | // A path to a custom resolver 119 | // resolver: undefined, 120 | 121 | // Automatically restore mock state and implementation before every test 122 | // restoreMocks: false, 123 | 124 | // The root directory that Jest should scan for tests and modules within 125 | // rootDir: undefined, 126 | 127 | // A list of paths to directories that Jest should use to search for files in 128 | // roots: [ 129 | // "" 130 | // ], 131 | 132 | // Allows you to use a custom runner instead of Jest's default test runner 133 | // runner: "jest-runner", 134 | 135 | // The paths to modules that run some code to configure or set up the testing environment before each test 136 | // setupFiles: [], 137 | 138 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 139 | // setupFilesAfterEnv: [], 140 | 141 | // The number of seconds after which a test is considered as slow and reported as such in the results. 142 | // slowTestThreshold: 5, 143 | 144 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 145 | // snapshotSerializers: [], 146 | 147 | // The test environment that will be used for testing 148 | testEnvironment: 'node', 149 | 150 | // Options that will be passed to the testEnvironment 151 | // testEnvironmentOptions: {}, 152 | 153 | // Adds a location field to test results 154 | // testLocationInResults: false, 155 | 156 | // The glob patterns Jest uses to detect test files 157 | testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)'], 158 | 159 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 160 | testPathIgnorePatterns: ['/node_modules/', '/__tests__/utils.ts'], 161 | 162 | // The regexp pattern or array of patterns that Jest uses to detect test files 163 | // testRegex: [], 164 | 165 | // This option allows the use of a custom results processor 166 | // testResultsProcessor: undefined, 167 | 168 | // This option allows use of a custom test runner 169 | // testRunner: "jest-circus/runner", 170 | 171 | // A map from regular expressions to paths to transformers 172 | // transform: undefined, 173 | 174 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 175 | // transformIgnorePatterns: [ 176 | // "/node_modules/", 177 | // "\\.pnp\\.[^\\/]+$" 178 | // ], 179 | 180 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 181 | // unmockedModulePathPatterns: undefined, 182 | 183 | // Indicates whether each individual test should be reported during the run 184 | // verbose: undefined, 185 | 186 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 187 | // watchPathIgnorePatterns: [], 188 | 189 | // Whether to use watchman for file crawling 190 | // watchman: true, 191 | } as Config 192 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-router-guarded-routes", 3 | "version": "0.4.5", 4 | "description": "a guard middleware for react-router v6", 5 | "keywords": [ 6 | "react", 7 | "router", 8 | "guard" 9 | ], 10 | "homepage": "https://github.com/Col0ring/react-router-guarded-routes", 11 | "bugs": { 12 | "url": "https://github.com/Col0ring/react-router-guarded-routes/issues" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git@github.com:Col0ring/react-router-guarded-routes.git" 17 | }, 18 | "license": "MIT", 19 | "author": { 20 | "name": "Col0ring", 21 | "email": "1561999073@qq.com" 22 | }, 23 | "main": "./dist/index.cjs.js", 24 | "module": "./dist/index.esm.js", 25 | "types": "./dist/index.d.ts", 26 | "files": [ 27 | "dist", 28 | "src" 29 | ], 30 | "scripts": { 31 | "build": "rimraf dist && tsup", 32 | "dev": "tsup --watch", 33 | "prepare": "husky install", 34 | "lint": "npm-run-all --parallel lint:* && npm run format", 35 | "lint:eslint": "eslint . --fix --ext .ts,.tsx,.js,.jsx", 36 | "lint:stylelint": "stylelint **/*.css --fix --allow-empty-input", 37 | "lint:tsc": "tsc --noEmit", 38 | "format": "cross-env NODE_ENV=production prettier . --write --no-error-on-unmatched-pattern", 39 | "test": "jest", 40 | "test:cov": "jest --coverage" 41 | }, 42 | "dependencies": { 43 | "types-kit": "^0.0.11" 44 | }, 45 | "devDependencies": { 46 | "@col0ring/eslint-config": "^0.0.13", 47 | "@col0ring/prettier-config": "^0.0.2", 48 | "@col0ring/stylelint-config": "^0.0.8", 49 | "@commitlint/cli": "^17.1.2", 50 | "@commitlint/config-angular": "^17.1.0", 51 | "@types/jest": "^29.1.2", 52 | "@types/react": "^18.0.21", 53 | "@types/react-dom": "^18.0.6", 54 | "@types/react-test-renderer": "^18.0.0", 55 | "cross-env": "^7.0.3", 56 | "eslint": "^8.25.0", 57 | "husky": "^8.0.1", 58 | "jest": "^29.2.0", 59 | "lint-staged": "^13.0.3", 60 | "npm-run-all": "^4.1.5", 61 | "prettier": "^2.7.1", 62 | "prettier-plugin-organize-imports": "^3.1.1", 63 | "prettier-plugin-packagejson": "^2.3.0", 64 | "react": "^18.2.0", 65 | "react-dom": "^18.2.0", 66 | "react-router": "^6.4.2", 67 | "react-test-renderer": "^18.2.0", 68 | "rimraf": "^3.0.2", 69 | "stylelint": "^14.13.0", 70 | "ts-jest": "^29.0.3", 71 | "tsup": "^6.2.3", 72 | "typescript": "^4.8.4" 73 | }, 74 | "peerDependencies": { 75 | "react": ">=17.0.0", 76 | "react-router": "^6.0.0" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - './' 3 | - 'examples/*' 4 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | const __PROD__ = process.env.NODE_ENV === 'production' 2 | module.exports = require('@col0ring/prettier-config')(__PROD__) 3 | -------------------------------------------------------------------------------- /src/guard-config-provider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react' 2 | import { useLocation } from 'react-router' 3 | import { GuardConfigContext, GuardConfigContextValue } from './internal/context' 4 | import { useInGuardConfigContext } from './internal/useInGuardContext' 5 | import { usePrevious } from './internal/usePrevious' 6 | import { invariant } from './internal/utils' 7 | 8 | export interface GuardConfigProviderProps 9 | extends Partial< 10 | Pick 11 | > { 12 | children: React.ReactNode 13 | } 14 | 15 | export const GuardConfigProvider: React.FC = ( 16 | props 17 | ) => { 18 | invariant( 19 | !useInGuardConfigContext(), 20 | `You cannot render a inside another .` + 21 | ` You should never have more than one in your app.` 22 | ) 23 | const { children, ...args } = props 24 | const location = useLocation() 25 | const prevLocation = usePrevious(location) 26 | 27 | const contextValue: GuardConfigContextValue = useMemo( 28 | () => ({ 29 | enableGuards: (to, from) => 30 | to.location.pathname !== from.location?.pathname, 31 | enableFallback: () => true, 32 | ...args, 33 | location: { 34 | to: location, 35 | from: prevLocation || null, 36 | }, 37 | }), 38 | [args, location, prevLocation] 39 | ) 40 | 41 | return ( 42 | 43 | {children} 44 | 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /src/guard-provider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react' 2 | import { GuardContext, GuardContextValue } from './internal/context' 3 | import { useGuardContext } from './internal/useGuardContext' 4 | 5 | export interface GuardProviderProps extends GuardContextValue { 6 | children: React.ReactNode 7 | } 8 | 9 | export const GuardProvider: React.FC = (props) => { 10 | const { children, ...args } = props 11 | const { guards, useInject } = useGuardContext() 12 | 13 | const guardContextValue: GuardContextValue = useMemo( 14 | () => ({ 15 | ...args, 16 | useInject: (to, from) => { 17 | return { 18 | ...useInject?.(to, from), 19 | ...args.useInject?.(to, from), 20 | } 21 | }, 22 | guards: [...(guards || []), ...(args.guards || [])], 23 | }), 24 | [args, guards, useInject] 25 | ) 26 | 27 | return ( 28 | 29 | {children} 30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /src/guarded-route.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Route } from 'react-router' 3 | import { invariant } from './internal/utils' 4 | import { GuardedRouteConfig } from './type' 5 | type RouteProps = Parameters[0] 6 | 7 | export type GuardedRouteProps = RouteProps & GuardedRouteConfig 8 | 9 | export const GuardedRoute: React.FC = () => { 10 | invariant( 11 | false, 12 | `A is only ever to be used as the child of element, ` + 13 | `never rendered directly. Please wrap your in a .` 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /src/guarded-routes.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react' 2 | import { Outlet, RoutesProps } from 'react-router' 3 | import { GuardProvider } from './guard-provider' 4 | import { GuardedRoute } from './guarded-route' 5 | import { useInGuardConfigContext } from './internal/useInGuardContext' 6 | import { invariant } from './internal/utils' 7 | import { GuardedRouteObject } from './type' 8 | import { useGuardedRoutes } from './useGuardedRoutes' 9 | 10 | export interface GuardedRoutesProps extends RoutesProps {} 11 | 12 | function createGuardedRoutesFromChildren(children: React.ReactNode) { 13 | const routes: GuardedRouteObject[] = [] 14 | 15 | React.Children.forEach(children, (element) => { 16 | if (!React.isValidElement(element)) { 17 | // Ignore non-elements. This allows people to more easily inline 18 | // conditionals in their route config. 19 | return 20 | } 21 | 22 | if (element.type === React.Fragment) { 23 | // Transparently support React.Fragment and its children. 24 | routes.push(...createGuardedRoutesFromChildren(element.props.children)) 25 | return 26 | } 27 | 28 | if (element.type === GuardProvider) { 29 | routes.push({ 30 | element: React.cloneElement( 31 | element, 32 | { 33 | ...element.props, 34 | }, 35 | 36 | ), 37 | children: createGuardedRoutesFromChildren(element.props.children), 38 | }) 39 | return 40 | } 41 | 42 | invariant( 43 | element.type === GuardedRoute, 44 | `[${ 45 | typeof element.type === 'string' ? element.type : element.type.name 46 | }] is not a component. All component children of must be a or ` 47 | ) 48 | 49 | const route = { 50 | ...element.props, 51 | } 52 | 53 | if (element.props.children) { 54 | route.children = createGuardedRoutesFromChildren(element.props.children) 55 | } 56 | 57 | routes.push(route) 58 | }) 59 | 60 | return routes 61 | } 62 | 63 | export const GuardedRoutes: React.FC = (props) => { 64 | invariant( 65 | useInGuardConfigContext(), 66 | `You cannot render the outside a .` 67 | ) 68 | const { children, location: locationProp } = props 69 | 70 | const routes = useMemo( 71 | () => createGuardedRoutesFromChildren(children), 72 | [children] 73 | ) 74 | return useGuardedRoutes(routes, locationProp) 75 | } 76 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './guard-config-provider' 2 | export * from './guard-provider' 3 | export * from './guarded-route' 4 | export * from './guarded-routes' 5 | export * from './type' 6 | export * from './useGuardedRoutes' 7 | -------------------------------------------------------------------------------- /src/internal/context.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext } from 'react' 2 | import { Location } from 'react-router' 3 | import { 4 | FromGuardRouteOptions, 5 | GuardedRouteConfig, 6 | ToGuardRouteOptions, 7 | } from '../type' 8 | export interface GuardContextValue { 9 | fallback?: React.ReactElement 10 | useInject?: ( 11 | to: ToGuardRouteOptions, 12 | from: FromGuardRouteOptions 13 | ) => Record 14 | guards?: GuardedRouteConfig['guards'] 15 | } 16 | export const GuardContext = createContext({}) 17 | 18 | export interface GuardConfigContextValue { 19 | enableGuards: ( 20 | to: ToGuardRouteOptions, 21 | from: FromGuardRouteOptions 22 | ) => Promise | boolean 23 | enableFallback: ( 24 | to: ToGuardRouteOptions, 25 | from: FromGuardRouteOptions 26 | ) => boolean 27 | location: { 28 | to: Location | null 29 | from: Location | null 30 | } 31 | } 32 | 33 | export const GuardConfigContext = createContext({ 34 | location: { 35 | to: null, 36 | from: null, 37 | }, 38 | enableGuards: (to, from) => to.location.pathname !== from.location?.pathname, 39 | enableFallback: () => true, 40 | }) 41 | -------------------------------------------------------------------------------- /src/internal/guard.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useCallback, 3 | useContext, 4 | useEffect, 5 | useMemo, 6 | useRef, 7 | useState, 8 | } from 'react' 9 | import { 10 | Location, 11 | NavigateOptions, 12 | To, 13 | UNSAFE_RouteContext as RouteContext, 14 | useNavigate, 15 | } from 'react-router' 16 | import { 17 | FromGuardRouteOptions, 18 | GuardedRouteObject, 19 | GuardMiddlewareFunction, 20 | NextFunction, 21 | ToGuardRouteOptions, 22 | } from '../type' 23 | import { useGuardConfigContext } from './useGuardConfigContext' 24 | import { useGuardContext } from './useGuardContext' 25 | import { usePrevious } from './usePrevious' 26 | import { isFunction, isNumber, isPromise, isUndefined, noop } from './utils' 27 | 28 | export interface GuardProps { 29 | route: GuardedRouteObject 30 | children?: React.ReactNode 31 | } 32 | 33 | enum ResolvedStatus { 34 | NEXT = 'next', 35 | TO = 'to', 36 | GO = 'go', 37 | End = 'end', 38 | } 39 | 40 | type GuardedResult = 41 | | ({ 42 | value: T 43 | } & ( 44 | | { 45 | type: ResolvedStatus.NEXT 46 | } 47 | | { 48 | type: ResolvedStatus.TO 49 | to: To 50 | options?: NavigateOptions 51 | } 52 | | { 53 | type: ResolvedStatus.GO 54 | delta: number 55 | } 56 | )) 57 | | { 58 | type: ResolvedStatus.End 59 | } 60 | 61 | export const Guard: React.FC = (props) => { 62 | const { children, route } = props 63 | const { guards: guardsProp, fallback: fallbackProp } = route 64 | const [validated, setValidated] = useState(false) 65 | const validatingRef = useRef(false) 66 | const { location, enableGuards, enableFallback } = useGuardConfigContext() 67 | const { 68 | guards: wrapperGuards, 69 | fallback, 70 | useInject = noop, 71 | } = useGuardContext() 72 | const navigate = useNavigate() 73 | const guards = useMemo( 74 | () => [...(wrapperGuards || []), ...(guardsProp || [])], 75 | [wrapperGuards, guardsProp] 76 | ) 77 | const hasGuard = useMemo(() => guards.length !== 0, [guards.length]) 78 | 79 | const { matches } = useContext(RouteContext) 80 | const prevMatches = usePrevious(matches) 81 | const toGuardRouteOptions: ToGuardRouteOptions = useMemo( 82 | () => ({ 83 | location: location.to as Location, 84 | matches, 85 | route: matches[matches.length - 1]?.route, 86 | }), 87 | [location.to, matches] 88 | ) 89 | const fromGuardRouteOptions: FromGuardRouteOptions = useMemo( 90 | () => ({ 91 | location: location.from, 92 | matches: prevMatches || [], 93 | route: prevMatches ? prevMatches[prevMatches.length - 1]?.route : null, 94 | }), 95 | [location.from, prevMatches] 96 | ) 97 | const injectedValue = useInject(toGuardRouteOptions, fromGuardRouteOptions) 98 | 99 | const canRunGuard = useMemo( 100 | () => enableGuards(toGuardRouteOptions, fromGuardRouteOptions), 101 | [enableGuards, fromGuardRouteOptions, toGuardRouteOptions] 102 | ) 103 | 104 | const canRunFallback = useMemo( 105 | () => enableFallback(toGuardRouteOptions, fromGuardRouteOptions), 106 | [enableFallback, fromGuardRouteOptions, toGuardRouteOptions] 107 | ) 108 | 109 | const runGuard = useCallback( 110 | (guard: GuardMiddlewareFunction, prevCtxValue: any) => { 111 | return new Promise>((resolve, reject) => { 112 | let ctxValue: any 113 | let called = false 114 | const next: NextFunction = ( 115 | ...args: [To, NavigateOptions?] | [number] | [] 116 | ) => { 117 | if (called) { 118 | return 119 | } 120 | called = true 121 | switch (args.length) { 122 | case 0: 123 | resolve({ 124 | type: ResolvedStatus.NEXT, 125 | value: ctxValue, 126 | }) 127 | break 128 | case 1: 129 | if (isNumber(args[0])) { 130 | resolve({ 131 | type: ResolvedStatus.GO, 132 | delta: args[0], 133 | value: ctxValue, 134 | }) 135 | } else { 136 | resolve({ 137 | type: ResolvedStatus.TO, 138 | to: args[0], 139 | value: ctxValue, 140 | }) 141 | } 142 | break 143 | case 2: 144 | resolve({ 145 | type: ResolvedStatus.TO, 146 | to: args[0], 147 | options: args[1], 148 | value: ctxValue, 149 | }) 150 | break 151 | } 152 | } 153 | next.ctx = (value) => { 154 | ctxValue = value 155 | return next() 156 | } 157 | next.end = () => { 158 | resolve({ 159 | type: ResolvedStatus.End, 160 | }) 161 | } 162 | async function handleGuard() { 163 | await guard(toGuardRouteOptions, fromGuardRouteOptions, next, { 164 | injectedValue, 165 | ctxValue: prevCtxValue, 166 | }) 167 | } 168 | try { 169 | handleGuard() 170 | } catch (error) { 171 | reject(error) 172 | } 173 | }) 174 | }, 175 | [fromGuardRouteOptions, injectedValue, toGuardRouteOptions] 176 | ) 177 | 178 | const runGuards = useCallback(async () => { 179 | setValidated(false) 180 | let ctxValue: any 181 | for (const guard of guards) { 182 | let registered = true 183 | let guardHandle: GuardMiddlewareFunction 184 | if (isFunction(guard)) { 185 | guardHandle = guard 186 | } else { 187 | guardHandle = guard.handler 188 | if (guard.register) { 189 | registered = await guard.register( 190 | toGuardRouteOptions, 191 | fromGuardRouteOptions 192 | ) 193 | } 194 | } 195 | if (!registered) { 196 | continue 197 | } 198 | const result = await runGuard(guardHandle, ctxValue) 199 | if (result.type === ResolvedStatus.End) { 200 | break 201 | } 202 | ctxValue = result.value 203 | if (result.type === ResolvedStatus.NEXT) { 204 | continue 205 | } else if (result.type === ResolvedStatus.GO) { 206 | navigate(result.delta) 207 | return 208 | } else if (result.type === ResolvedStatus.TO) { 209 | navigate(result.to, result.options) 210 | return 211 | } 212 | } 213 | setValidated(true) 214 | }, [fromGuardRouteOptions, guards, navigate, runGuard, toGuardRouteOptions]) 215 | 216 | const fallbackElement = useMemo(() => { 217 | return !isUndefined(fallbackProp) ? fallbackProp : fallback 218 | }, [fallback, fallbackProp]) 219 | 220 | useEffect(() => { 221 | async function validate() { 222 | // validating lock for strict mode. 223 | if (hasGuard && !validatingRef.current) { 224 | validatingRef.current = true 225 | await runGuards() 226 | validatingRef.current = false 227 | } 228 | } 229 | if (isPromise(canRunGuard)) { 230 | canRunGuard.then((done) => { 231 | if (done) { 232 | validate() 233 | } else { 234 | setValidated(true) 235 | } 236 | }) 237 | return 238 | } 239 | if (canRunGuard) { 240 | validate() 241 | } else { 242 | setValidated(true) 243 | } 244 | // eslint-disable-next-line react-hooks/exhaustive-deps 245 | }, [location.to]) 246 | 247 | if (hasGuard && !validated) { 248 | if (canRunFallback) { 249 | return <>{fallbackElement} 250 | } 251 | return null 252 | } 253 | return <>{children} 254 | } 255 | -------------------------------------------------------------------------------- /src/internal/useGuardConfigContext.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import { GuardConfigContext } from './context' 3 | 4 | export function useGuardConfigContext() { 5 | return useContext(GuardConfigContext) 6 | } 7 | -------------------------------------------------------------------------------- /src/internal/useGuardContext.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import { GuardContext } from './context' 3 | 4 | export function useGuardContext() { 5 | return useContext(GuardContext) 6 | } 7 | -------------------------------------------------------------------------------- /src/internal/useInGuardContext.ts: -------------------------------------------------------------------------------- 1 | import { useGuardConfigContext } from './useGuardConfigContext' 2 | 3 | export function useInGuardConfigContext() { 4 | return useGuardConfigContext().location.to !== null 5 | } 6 | -------------------------------------------------------------------------------- /src/internal/usePrevious.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react' 2 | 3 | export function usePrevious(state: T): T | undefined { 4 | const prevRef = useRef() 5 | const curRef = useRef() 6 | 7 | if (!Object.is(curRef.current, state)) { 8 | prevRef.current = curRef.current 9 | curRef.current = state 10 | } 11 | 12 | return prevRef.current 13 | } 14 | -------------------------------------------------------------------------------- /src/internal/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * assert 3 | */ 4 | export function invariant(cond: any, message: string): asserts cond { 5 | if (!cond) { 6 | throw new Error(message) 7 | } 8 | } 9 | 10 | export function noop() {} 11 | 12 | export function isPromise(value: any): value is Promise { 13 | return value instanceof Promise 14 | } 15 | 16 | export function isNumber(value: any): value is number { 17 | return typeof value === 'number' 18 | } 19 | 20 | export function isUndefined(value: any): value is undefined { 21 | return typeof value === 'undefined' 22 | } 23 | 24 | export function isFunction(value: any): value is (...args: any[]) => any { 25 | return typeof value === 'function' 26 | } 27 | -------------------------------------------------------------------------------- /src/type.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | Location, 4 | NavigateFunction, 5 | RouteMatch, 6 | RouteObject, 7 | } from 'react-router' 8 | import { ReplacePick } from 'types-kit' 9 | 10 | export interface GuardedRouteConfig { 11 | guards?: GuardMiddleware[] 12 | fallback?: React.ReactNode 13 | [props: PropertyKey]: any 14 | } 15 | 16 | export type GuardedRouteObject = RouteObject & 17 | GuardedRouteConfig & { 18 | children?: GuardedRouteObject[] 19 | } 20 | 21 | export interface NextFunction extends NavigateFunction { 22 | (): void 23 | ctx: (value: T) => void 24 | end: () => void 25 | } 26 | 27 | export interface GuardedRouteMatch 28 | extends Omit, 'route'> { 29 | route: GuardedRouteObject 30 | } 31 | 32 | export interface ToGuardRouteOptions { 33 | location: Location 34 | matches: GuardedRouteMatch[] 35 | route: GuardedRouteObject 36 | } 37 | 38 | export interface FromGuardRouteOptions 39 | extends ReplacePick< 40 | ToGuardRouteOptions, 41 | ['location', 'route'], 42 | [ 43 | ToGuardRouteOptions['location'] | null, 44 | ToGuardRouteOptions['route'] | null 45 | ] 46 | > {} 47 | 48 | export interface ExternalOptions { 49 | ctxValue: T 50 | injectedValue: I 51 | } 52 | 53 | export type GuardMiddlewareFunction = ( 54 | to: ToGuardRouteOptions, 55 | from: FromGuardRouteOptions, 56 | next: NextFunction, 57 | externalOptions: ExternalOptions 58 | ) => Promise | void 59 | 60 | export type GuardMiddlewareObject = { 61 | handler: GuardMiddlewareFunction 62 | register?: ( 63 | to: ToGuardRouteOptions, 64 | from: FromGuardRouteOptions 65 | ) => Promise | boolean 66 | } 67 | export type GuardMiddleware = 68 | | GuardMiddlewareFunction 69 | | GuardMiddlewareObject 70 | -------------------------------------------------------------------------------- /src/useGuardedRoutes.ts: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react' 2 | import { RouteObject, useRoutes } from 'react-router' 3 | import { Guard } from './internal/guard' 4 | import { GuardedRouteObject } from './type' 5 | 6 | type LocationArg = Parameters[1] 7 | 8 | function transformGuardedRoutes( 9 | guardedRoutes: GuardedRouteObject[] 10 | ): RouteObject[] { 11 | return guardedRoutes.map((guardedRoute, i) => { 12 | const { element, path, children } = guardedRoute 13 | return { 14 | ...guardedRoute, 15 | element: 16 | element !== undefined 17 | ? React.createElement( 18 | Guard, 19 | { 20 | key: path || i, 21 | route: guardedRoute, 22 | }, 23 | element 24 | ) 25 | : undefined, 26 | children: 27 | children !== undefined ? transformGuardedRoutes(children) : undefined, 28 | } as RouteObject 29 | }) 30 | } 31 | 32 | export function useGuardedRoutes( 33 | guardedRoutes: GuardedRouteObject[], 34 | locationArg?: LocationArg 35 | ) { 36 | const routes = useMemo( 37 | () => transformGuardedRoutes(guardedRoutes), 38 | [guardedRoutes] 39 | ) 40 | return useRoutes(routes, locationArg) 41 | } 42 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@col0ring/stylelint-config/basic'], 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "ESNext", 12 | "moduleResolution": "Node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react-jsx" 17 | }, 18 | "exclude": ["node_modules", "dist"] 19 | } 20 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | minify: true, 6 | dts: true, 7 | sourcemap: true, 8 | format: ['cjs', 'esm'], 9 | outExtension({ format }) { 10 | return { 11 | js: `.${format}.js`, 12 | } 13 | }, 14 | }) 15 | --------------------------------------------------------------------------------