@navojs/react-router 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # @navojs/react-router
2
+
3
+ React Router integration for [Navo](https://github.com/flycran/navo) — declarative navigation and permission-based routing.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @navojs/react-router react-router
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ### 1. Define your navigation structure
14
+
15
+ ```tsx
16
+ import { createNavo } from '@navojs/react-router'
17
+
18
+ const navo = createNavo([
19
+ { id: 'dashboard', label: 'Dashboard', path: '/dashboard' },
20
+ {
21
+ id: 'users',
22
+ label: 'Users',
23
+ path: '/users',
24
+ children: [
25
+ { id: 'user-list', label: 'User List', path: 'list' },
26
+ { id: 'user-detail', label: 'User Detail', path: ':id', affiliated: true },
27
+ ],
28
+ },
29
+ { id: 'settings', label: 'Settings', path: '/settings' },
30
+ ])
31
+ ```
32
+
33
+ ### 2. Wrap your app with `NavoProvider`
34
+
35
+ ```tsx
36
+ import { NavoProvider } from '@navojs/react-router'
37
+ import { BrowserRouter } from 'react-router'
38
+
39
+ function App() {
40
+ return (
41
+ <BrowserRouter>
42
+ <NavoProvider navo={navo}>
43
+ <AppRoutes />
44
+ </NavoProvider>
45
+ </BrowserRouter>
46
+ )
47
+ }
48
+ ```
49
+
50
+ ### 3. Generate routes from your navo structure
51
+
52
+ ```tsx
53
+ import { generateRoutes } from '@navojs/react-router'
54
+ import { useRoutes } from 'react-router'
55
+
56
+ function AppRoutes() {
57
+ const routes = generateRoutes(navo)
58
+ return useRoutes(routes)
59
+ }
60
+ ```
61
+
62
+ ## Permission Control
63
+
64
+ ```tsx
65
+ import { useCanAccess } from '@navojs/react-router'
66
+
67
+ function PermissionGate() {
68
+ const onCanAccess = useCanAccess()
69
+
70
+ useEffect(() => {
71
+ // Provide your access control logic
72
+ onCanAccess((node) => {
73
+ return userPermissions.includes(node.id)
74
+ })
75
+ }, [])
76
+
77
+ return null
78
+ }
79
+ ```
80
+
81
+ ## Hooks
82
+
83
+ ### `useCanAccess()`
84
+
85
+ Returns the `onCanAccess` callback for setting up permission checks.
86
+
87
+ ### `useMatchedNodes()`
88
+
89
+ Returns the currently active navigation nodes and their IDs — useful for highlighting menus and breadcrumbs.
90
+
91
+ ```tsx
92
+ const { matchedNodes, matchedIds } = useMatchedNodes()
93
+ // matchedIds: ['dashboard', 'users', 'user-list']
94
+ // matchedNodes: corresponding NavoNode objects
95
+ ```
96
+
97
+ ### `useNavo()`
98
+
99
+ Returns utility functions for working with the navigation tree:
100
+
101
+ ```tsx
102
+ const { nodes, getNodeById, getPathById, hasCanAccess } = useNavo()
103
+
104
+ // Get all authorized nodes
105
+ nodes.forEach(node => console.log(node.label))
106
+
107
+ // Check access
108
+ if (hasCanAccess('settings')) { /* ... */ }
109
+
110
+ // Get path by id
111
+ const path = getPathById('user-list') // '/users/list'
112
+ ```
113
+
114
+ ## Route Escape Hatch
115
+
116
+ You can pass native React Router route properties through the `route` field:
117
+
118
+ ```tsx
119
+ createNavo([
120
+ {
121
+ id: 'home',
122
+ path: '/',
123
+ route: {
124
+ loader: () => fetchData(),
125
+ caseSensitive: true,
126
+ errorElement: <ErrorPage />,
127
+ },
128
+ },
129
+ ])
130
+ ```
131
+
132
+ ## API
133
+
134
+ ### `generateRoutes(navo)`
135
+
136
+ Converts a Navo instance into an array of React Router `RouteObject`s with automatic index redirects.
137
+
138
+ ### `NavoProvider`
139
+
140
+ React context provider that holds the Navo instance and authentication state.
141
+
142
+ ### `NavoRedirect`
143
+
144
+ Internal component that handles automatic redirection to the resolved path (e.g., redirecting `/users` to `/users/list`).
145
+
146
+ ## License
147
+
148
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,134 @@
1
+ // src/index.ts
2
+ export * from "@navojs/core";
3
+
4
+ // src/hooks.ts
5
+ import { Navo } from "@navojs/core";
6
+ import { useContext as useContext2, useMemo as useMemo2 } from "react";
7
+ import { useMatches } from "react-router";
8
+
9
+ // src/main.tsx
10
+ import { createContext, useContext, useMemo, useState } from "react";
11
+ import {
12
+ Navigate,
13
+ useLocation
14
+ } from "react-router";
15
+ import { jsxDEV } from "react/jsx-dev-runtime";
16
+ function NavoRedirect({ navNode }) {
17
+ const navo = useContext(NavoContext);
18
+ const location = useLocation();
19
+ const resolveNode = navNode ? navo?.authedIdMap.get(navNode.id) : null;
20
+ if (resolveNode)
21
+ return /* @__PURE__ */ jsxDEV(Navigate, {
22
+ to: resolveNode.path,
23
+ replace: true
24
+ }, undefined, false, undefined, this);
25
+ if (navo && location.pathname === navo.originNavo.basePath)
26
+ return /* @__PURE__ */ jsxDEV(Navigate, {
27
+ to: navo.authedRootResolvePath,
28
+ replace: true
29
+ }, undefined, false, undefined, this);
30
+ }
31
+ var NavoContext = createContext(null);
32
+ function NavoProvider({ children, navo }) {
33
+ const [state, setState] = useState({
34
+ nodes: navo.nodes,
35
+ idMap: navo.idMap,
36
+ authedNodes: navo.authedNodes,
37
+ authedIdMap: navo.authedIdMap,
38
+ authedRootResolvePath: navo.authedRootResolvePath
39
+ });
40
+ const value = useMemo(() => ({
41
+ ...state,
42
+ originNavo: navo,
43
+ onCanAccess: (canAccess) => {
44
+ navo.authenticat(canAccess);
45
+ setState({
46
+ nodes: navo.nodes,
47
+ idMap: navo.idMap,
48
+ authedNodes: navo.authedNodes,
49
+ authedIdMap: navo.authedIdMap,
50
+ authedRootResolvePath: navo.authedRootResolvePath
51
+ });
52
+ }
53
+ }), [navo, state]);
54
+ return /* @__PURE__ */ jsxDEV(NavoContext.Provider, {
55
+ value,
56
+ children
57
+ }, undefined, false, undefined, this);
58
+ }
59
+ function generateRoutes(navo) {
60
+ const navNodes = navo.nodes;
61
+ const r = (navNodes2) => {
62
+ const routes = [];
63
+ for (let i = 0;i < navNodes2.length; i++) {
64
+ const navNode = navNodes2[i];
65
+ const route = {
66
+ ...navNode.route,
67
+ id: navNode.id,
68
+ path: navNode.originPath
69
+ };
70
+ if (navNode.children?.length) {
71
+ route.children = r(navNode.children);
72
+ }
73
+ routes.push(route);
74
+ }
75
+ if (navNodes2.length) {
76
+ routes.push({
77
+ index: true,
78
+ element: /* @__PURE__ */ jsxDEV(NavoRedirect, {
79
+ navNode: navNodes2[0].parent
80
+ }, undefined, false, undefined, this)
81
+ });
82
+ }
83
+ return routes;
84
+ };
85
+ return r(navNodes);
86
+ }
87
+
88
+ // src/hooks.ts
89
+ var useCanAccess = () => {
90
+ const navo = useContext2(NavoContext);
91
+ if (!navo)
92
+ throw new Error("useCanAccess must be used within a NavoProvider.");
93
+ return navo.onCanAccess;
94
+ };
95
+ var useMatchedNodes = () => {
96
+ const navo = useContext2(NavoContext);
97
+ if (!navo)
98
+ throw new Error("useCurrentNode must be used within a NavoProvider.");
99
+ const matches = useMatches();
100
+ return useMemo2(() => {
101
+ const matchedNodes = [];
102
+ const matchedIds = [];
103
+ for (let i = matches.length - 1;i >= 0; i--) {
104
+ const element = matches[i];
105
+ const node = navo.authedIdMap.get(element.id);
106
+ if (node && !Navo.isAffiliated(node)) {
107
+ matchedNodes.push(node);
108
+ matchedIds.push(node.id);
109
+ }
110
+ }
111
+ return {
112
+ matchedNodes: matchedNodes.reverse(),
113
+ matchedIds: matchedIds.reverse()
114
+ };
115
+ }, [matches, navo.authedIdMap]);
116
+ };
117
+ var useNavo = () => {
118
+ const navo = useContext2(NavoContext);
119
+ if (!navo)
120
+ throw new Error("useNavoIdMap must be used within a NavoProvider.");
121
+ return {
122
+ nodes: navo.authedNodes,
123
+ getNodeById: (id) => navo.authedIdMap.get(id),
124
+ getPathById: (id) => navo.authedIdMap.get(id)?.path,
125
+ hasCanAccess: (id) => navo.authedIdMap.has(id)
126
+ };
127
+ };
128
+ export {
129
+ useNavo,
130
+ useMatchedNodes,
131
+ useCanAccess,
132
+ generateRoutes,
133
+ NavoProvider
134
+ };
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@navojs/react-router",
3
+ "version": "1.0.1",
4
+ "description": "React Router integration for Navo — declarative navigation and permission-based routing",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "bun --watch ./build.ts",
8
+ "build": "bun ./build.ts",
9
+ "check": "tsgo --noEmit --project tsconfig.json",
10
+ "test": "tsgo --noEmit --project tsconfig.test.json && bun test"
11
+ },
12
+ "main": "dist/index.js",
13
+ "module": "dist/index.js",
14
+ "types": "src/index.ts",
15
+ "exports": {
16
+ "import": "./dist/index.js",
17
+ "require": "./dist/index.js",
18
+ "types": "./src/index.ts"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "src"
23
+ ],
24
+ "keywords": [
25
+ "navo",
26
+ "react-router",
27
+ "navigation",
28
+ "routing",
29
+ "permissions",
30
+ "acl"
31
+ ],
32
+ "author": "flycran",
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/flycran/navo",
37
+ "directory": "packages/react-router"
38
+ },
39
+ "dependencies": {
40
+ "@navojs/core": "workspace:*",
41
+ "@types/react": "19.2.14"
42
+ },
43
+ "devDependencies": {
44
+ "@navojs/config": "workspace:*"
45
+ },
46
+ "peerDependencies": {
47
+ "react-router": "^7.18.0",
48
+ "react": ">=18"
49
+ }
50
+ }
package/src/hooks.ts ADDED
@@ -0,0 +1,80 @@
1
+ import { Navo, type NavoNode } from '@navojs/core'
2
+ import { useContext, useMemo } from 'react'
3
+ import { useMatches } from 'react-router'
4
+ import { NavoContext } from './main'
5
+
6
+ /**
7
+ * 鉴权回调
8
+ * @example
9
+ * ```ts
10
+ * const onCanAccess = useCanAccess()
11
+ * useEffect(() => {
12
+ * onCanAccess((node) => {
13
+ * return accessList.includes(node.id)
14
+ * })
15
+ * }, [])
16
+ * ```
17
+ */
18
+ export const useCanAccess = () => {
19
+ const navo = useContext(NavoContext)
20
+ if (!navo) throw new Error('useCanAccess must be used within a NavoProvider.')
21
+ return navo.onCanAccess
22
+ }
23
+
24
+ /**
25
+ * 返回当前活动的节点匹配项
26
+ * @example
27
+ * ```ts
28
+ * const { matchedIds } = useMatchedNodes()
29
+ * const openKeys = matchedIds.slice(0, -1)
30
+ * const selectedKey = matchedIds.at(-1)
31
+ * ```
32
+ */
33
+ export const useMatchedNodes = () => {
34
+ const navo = useContext(NavoContext)
35
+ if (!navo) throw new Error('useCurrentNode must be used within a NavoProvider.')
36
+ const matches = useMatches()
37
+
38
+ return useMemo(() => {
39
+ const matchedNodes: NavoNode[] = []
40
+ const matchedIds: string[] = []
41
+ for (let i = matches.length - 1; i >= 0; i--) {
42
+ const element = matches[i]
43
+ const node = navo.authedIdMap.get(element.id)
44
+
45
+ if (node && !Navo.isAffiliated(node)) {
46
+ matchedNodes.push(node)
47
+ matchedIds.push(node.id)
48
+ }
49
+ }
50
+
51
+ return {
52
+ matchedNodes: matchedNodes.reverse(),
53
+ matchedIds: matchedIds.reverse(),
54
+ }
55
+ }, [matches, navo.authedIdMap])
56
+ }
57
+
58
+ export type UseNavoReturn = {
59
+ /** 已鉴权节点 */
60
+ nodes: NavoNode[]
61
+ /** 根据id获取节点 */
62
+ getNodeById: (id: string) => NavoNode | undefined
63
+ /** 根据id获取路径 */
64
+ getPathById: (id: string) => string | undefined
65
+ /** 判断是否有权限访问该节点 */
66
+ hasCanAccess: (id: string) => boolean
67
+ }
68
+
69
+ /** 获取Navo节点和实用工具 */
70
+ export const useNavo = (): UseNavoReturn => {
71
+ const navo = useContext(NavoContext)
72
+ if (!navo) throw new Error('useNavoIdMap must be used within a NavoProvider.')
73
+
74
+ return {
75
+ nodes: navo.authedNodes,
76
+ getNodeById: (id: string) => navo.authedIdMap.get(id),
77
+ getPathById: (id: string) => navo.authedIdMap.get(id)?.path,
78
+ hasCanAccess: (id: string) => navo.authedIdMap.has(id),
79
+ }
80
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from '@navojs/core'
2
+ export * from './hooks'
3
+ export type * from './main'
4
+ export { generateRoutes, NavoProvider } from './main'
package/src/main.tsx ADDED
@@ -0,0 +1,106 @@
1
+ import type { CanAccess, Navo, NavoNode, NavoNodeInput } from '@navojs/core'
2
+ import { createContext, useContext, useMemo, useState } from 'react'
3
+ import {
4
+ type BaseRouteObject,
5
+ Navigate,
6
+ type NonIndexRouteObject,
7
+ type RouteObject,
8
+ useLocation,
9
+ } from 'react-router'
10
+
11
+ export type NavNodeEscapeHatch = Omit<BaseRouteObject, 'id' | 'path'>
12
+
13
+ declare module '@navojs/core' {
14
+ interface NavoNodeInput {
15
+ route?: NavNodeEscapeHatch
16
+ }
17
+ }
18
+
19
+ /** 内置的自动重定向到真实页面 */
20
+ function NavoRedirect({ navNode }: { navNode?: NavoNode | null }) {
21
+ const navo = useContext(NavoContext)
22
+ const location = useLocation()
23
+
24
+ const resolveNode = navNode ? navo?.authedIdMap.get(navNode.id) : null
25
+ if (resolveNode) return <Navigate to={resolveNode.path} replace />
26
+ if (navo && location.pathname === navo.originNavo.basePath)
27
+ return <Navigate to={navo.authedRootResolvePath} replace />
28
+ }
29
+
30
+ export interface NavoContextProps {
31
+ originNavo: Navo<NavoNodeInput[]>
32
+ nodes: NavoNode[]
33
+ idMap: Map<string, NavoNode>
34
+ authedNodes: NavoNode[]
35
+ authedIdMap: Map<string, NavoNode>
36
+ authedRootResolvePath: string
37
+ onCanAccess: (node: CanAccess) => void
38
+ }
39
+
40
+ export const NavoContext = createContext<NavoContextProps | null>(null)
41
+
42
+ export type NavoProviderProps<T extends NavoNodeInput[]> = {
43
+ navo: Navo<T>
44
+ children?: React.ReactNode
45
+ }
46
+
47
+ /** Navo Provider */
48
+ export function NavoProvider<T extends NavoNodeInput[]>({ children, navo }: NavoProviderProps<T>) {
49
+ const [state, setState] = useState({
50
+ nodes: navo.nodes,
51
+ idMap: navo.idMap,
52
+ authedNodes: navo.authedNodes,
53
+ authedIdMap: navo.authedIdMap,
54
+ authedRootResolvePath: navo.authedRootResolvePath,
55
+ })
56
+
57
+ const value = useMemo(
58
+ () => ({
59
+ ...state,
60
+ originNavo: navo,
61
+ onCanAccess: (canAccess?: CanAccess) => {
62
+ navo.authenticat(canAccess)
63
+ setState({
64
+ nodes: navo.nodes,
65
+ idMap: navo.idMap,
66
+ authedNodes: navo.authedNodes,
67
+ authedIdMap: navo.authedIdMap,
68
+ authedRootResolvePath: navo.authedRootResolvePath,
69
+ })
70
+ },
71
+ }),
72
+ [navo, state]
73
+ )
74
+
75
+ return <NavoContext.Provider value={value}>{children}</NavoContext.Provider>
76
+ }
77
+
78
+ /** 生成react router routes */
79
+ export function generateRoutes<T extends NavoNodeInput[]>(navo: Navo<T>): RouteObject[] {
80
+ const navNodes = navo.nodes
81
+ const r = (navNodes: NavoNode[]) => {
82
+ const routes: RouteObject[] = []
83
+ for (let i = 0; i < navNodes.length; i++) {
84
+ const navNode = navNodes[i]
85
+ const route: NonIndexRouteObject = {
86
+ ...navNode.route,
87
+ id: navNode.id,
88
+ path: navNode.originPath,
89
+ }
90
+ if (navNode.children?.length) {
91
+ route.children = r(navNode.children)
92
+ }
93
+ routes.push(route)
94
+ }
95
+
96
+ if (navNodes.length) {
97
+ routes.push({
98
+ index: true,
99
+ element: <NavoRedirect navNode={navNodes[0].parent} />,
100
+ })
101
+ }
102
+ return routes
103
+ }
104
+
105
+ return r(navNodes)
106
+ }