├── .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 |
--------------------------------------------------------------------------------