@pactor-app/router 0.1.0
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/LICENSE +21 -0
- package/README.md +94 -0
- package/dist/chunk-JY7Z55CR.js +53 -0
- package/dist/index.cjs +211 -0
- package/dist/index.d.cts +149 -0
- package/dist/index.d.ts +149 -0
- package/dist/index.js +137 -0
- package/dist/react.cjs +104 -0
- package/dist/react.d.cts +41 -0
- package/dist/react.d.ts +41 -0
- package/dist/react.js +55 -0
- package/package.json +80 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pactor Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# @pactor-app/router
|
|
2
|
+
|
|
3
|
+
[Pactor](https://github.com/426-330/pactor) 的路由模块:菜单模型与 MenuRegistry、极简路径匹配(PathMatcher)、导航桥接(NavigationBridge / MemoryNavigation)、@pactor-app/runtime RouterAdapter 桥接,以及 React Router 动态路由绑定(`<DynamicDslRoute>`)。
|
|
4
|
+
|
|
5
|
+
菜单声明 `path` + `pageId`(不绑定 React 组件),Dynamic Router 把 URL 解析为 pageId 与路由上下文;页面资源加载(pageId → loadPage)由应用组合层串联,不在本包。
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @pactor-app/router
|
|
11
|
+
# 或
|
|
12
|
+
pnpm add @pactor-app/router
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
核心入口(`@pactor-app/router`)零 React 依赖,可纯 Node 使用与测试;React 绑定在独立子入口 `@pactor-app/router/react`,仅该子入口需要 peer 依赖 `react >=18` 与 `react-router-dom >=6`(开发基准为 react-router-dom v7,仅用 v6/v7 共有的稳定 API)。
|
|
16
|
+
|
|
17
|
+
## 示例
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import {
|
|
21
|
+
MenuRegistry,
|
|
22
|
+
MemoryNavigation,
|
|
23
|
+
createRouterAdapter,
|
|
24
|
+
resolveRoute,
|
|
25
|
+
} from '@pactor-app/router';
|
|
26
|
+
|
|
27
|
+
const registry = new MenuRegistry([
|
|
28
|
+
{
|
|
29
|
+
id: 'customer',
|
|
30
|
+
type: 'group',
|
|
31
|
+
title: '客户管理',
|
|
32
|
+
children: [
|
|
33
|
+
{ id: 'customer-list', type: 'page', title: '客户列表', path: '/customers', pageId: 'customer-list' },
|
|
34
|
+
{
|
|
35
|
+
id: 'customer-edit', type: 'page', title: '编辑客户',
|
|
36
|
+
path: '/customers/:id/edit', pageId: 'customer-edit',
|
|
37
|
+
hidden: true, activeMenu: 'customer-list',
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
// 1. URL → RouteContext(隐藏详情页参与匹配,activeMenu 指向列表页)
|
|
44
|
+
const route = resolveRoute(registry, { pathname: '/customers/123/edit', search: '?tab=risk' });
|
|
45
|
+
// → { path, params: { id: '123' }, query: { tab: 'risk' }, pageId: 'customer-edit',
|
|
46
|
+
// menuId: 'customer-edit', activeMenu: 'customer-list' }
|
|
47
|
+
|
|
48
|
+
// 2. 桥接 @pactor-app/runtime:DSL navigate 动作驱动导航
|
|
49
|
+
const navigation = new MemoryNavigation();
|
|
50
|
+
const runtime = createPageRuntime(pageDsl, {
|
|
51
|
+
router: createRouterAdapter(navigation), // navigate(to) → bridge.push(to)
|
|
52
|
+
route: { params: route.params, query: route.query }, // ${route.params.id} 可求值
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
React 绑定(`@pactor-app/router/react`):
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
import { DynamicDslRoute, useReactRouterBridge } from '@pactor-app/router/react';
|
|
60
|
+
import { Route, Routes } from 'react-router-dom';
|
|
61
|
+
|
|
62
|
+
<Routes>
|
|
63
|
+
<Route
|
|
64
|
+
path="*"
|
|
65
|
+
element={
|
|
66
|
+
<DynamicDslRoute
|
|
67
|
+
registry={registry}
|
|
68
|
+
render={({ pageId, route }) => <DslPage pageId={pageId} route={route} />}
|
|
69
|
+
fallback={<NotFound />}
|
|
70
|
+
forbidden={<Forbidden />}
|
|
71
|
+
permissionChecker={permissionStore}
|
|
72
|
+
/>
|
|
73
|
+
}
|
|
74
|
+
/>
|
|
75
|
+
</Routes>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## 功能一览
|
|
79
|
+
|
|
80
|
+
- **MenuItem**:对齐设计文档第 30 节字段(id/type/title/icon/order/path/pageId/permission/hidden/disabled/external/target/keepAlive/badge/activeMenu/defaultOpen/breadcrumb/tab/children),`permission` 用 @pactor-app/core 的 `PermissionConfig`。
|
|
81
|
+
- **MenuRegistry**:由菜单树构建可见菜单树(`tree()`,过滤 hidden)+ 扁平可路由条目列表(`list()`,非 group 且有 path,按 order 排序、未指定者保持声明顺序在后);`findById`(含 group 与隐藏项)、`findByPath`(按 list() 顺序模式匹配,隐藏页参与,未命中 null)。
|
|
82
|
+
- **PathMatcher**:`match(pattern, pathname)` 仅支持静态段与 `:param` 段,整路径精确匹配(段数一致),不支持 `*` 与可选段;`parseQuery(search)` 解析查询串;`resolveRoute(registry, location)` 产出 `RouteContext`(path/params/query/pageId/menuId/activeMenu,activeMenu 取菜单项 activeMenu ?? 自身 id)。
|
|
83
|
+
- **NavigationBridge / MemoryNavigation**:`getLocation` / `push` / `replace` / `subscribe`(返回退订函数);MemoryNavigation 为纯 Node 内存历史栈实现,另支持 `back()`。
|
|
84
|
+
- **`createRouterAdapter(bridge)`**:结构化兼容 @pactor-app/runtime `RouterAdapter`,`navigate(to)` → `bridge.push(to)`。
|
|
85
|
+
- **`<DynamicDslRoute>`**(子入口 `@pactor-app/router/react`):useLocation 驱动重新解析——未命中渲染 `fallback`(404)、配置 `permissionChecker`(@pactor-app/permission `checkPagePermission` 守卫)且无权限渲染 `forbidden`(403),否则 `render({ pageId, route })`。
|
|
86
|
+
- **`useReactRouterBridge()`**:把 useNavigate / useLocation 包装为 NavigationBridge(React Router 自身驱动重渲染,subscribe 为 no-op)。
|
|
87
|
+
|
|
88
|
+
## 文档
|
|
89
|
+
|
|
90
|
+
完整文档见 <https://github.com/426-330/pactor/tree/main/docs>(`pnpm docs:dev` 本地启动文档站)。
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
[MIT](../../LICENSE)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// src/path-matcher.ts
|
|
2
|
+
function toSegments(path) {
|
|
3
|
+
return path.split("/").filter((segment) => segment.length > 0);
|
|
4
|
+
}
|
|
5
|
+
function match(pattern, pathname) {
|
|
6
|
+
const patternSegments = toSegments(pattern);
|
|
7
|
+
const pathSegments = toSegments(pathname);
|
|
8
|
+
if (patternSegments.length !== pathSegments.length) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const params = {};
|
|
12
|
+
for (let i = 0; i < patternSegments.length; i += 1) {
|
|
13
|
+
const patternSegment = patternSegments[i];
|
|
14
|
+
const pathSegment = pathSegments[i];
|
|
15
|
+
if (patternSegment.startsWith(":")) {
|
|
16
|
+
params[patternSegment.slice(1)] = pathSegment;
|
|
17
|
+
} else if (patternSegment !== pathSegment) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return { params };
|
|
22
|
+
}
|
|
23
|
+
function parseQuery(search) {
|
|
24
|
+
const query = {};
|
|
25
|
+
if (search === "" || search === "?") {
|
|
26
|
+
return query;
|
|
27
|
+
}
|
|
28
|
+
for (const [key, value] of new URLSearchParams(search)) {
|
|
29
|
+
query[key] = value;
|
|
30
|
+
}
|
|
31
|
+
return query;
|
|
32
|
+
}
|
|
33
|
+
function resolveRoute(registry, location) {
|
|
34
|
+
const hit = registry.findByPath(location.pathname);
|
|
35
|
+
if (hit === null) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const { menu, params } = hit;
|
|
39
|
+
return {
|
|
40
|
+
path: location.pathname,
|
|
41
|
+
params,
|
|
42
|
+
query: parseQuery(location.search ?? ""),
|
|
43
|
+
pageId: menu.pageId,
|
|
44
|
+
menuId: menu.id,
|
|
45
|
+
activeMenu: menu.activeMenu ?? menu.id
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export {
|
|
50
|
+
match,
|
|
51
|
+
parseQuery,
|
|
52
|
+
resolveRoute
|
|
53
|
+
};
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
MemoryNavigation: () => MemoryNavigation,
|
|
24
|
+
MenuRegistry: () => MenuRegistry,
|
|
25
|
+
createRouterAdapter: () => createRouterAdapter,
|
|
26
|
+
match: () => match,
|
|
27
|
+
parseQuery: () => parseQuery,
|
|
28
|
+
resolveRoute: () => resolveRoute
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(index_exports);
|
|
31
|
+
|
|
32
|
+
// src/path-matcher.ts
|
|
33
|
+
function toSegments(path) {
|
|
34
|
+
return path.split("/").filter((segment) => segment.length > 0);
|
|
35
|
+
}
|
|
36
|
+
function match(pattern, pathname) {
|
|
37
|
+
const patternSegments = toSegments(pattern);
|
|
38
|
+
const pathSegments = toSegments(pathname);
|
|
39
|
+
if (patternSegments.length !== pathSegments.length) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const params = {};
|
|
43
|
+
for (let i = 0; i < patternSegments.length; i += 1) {
|
|
44
|
+
const patternSegment = patternSegments[i];
|
|
45
|
+
const pathSegment = pathSegments[i];
|
|
46
|
+
if (patternSegment.startsWith(":")) {
|
|
47
|
+
params[patternSegment.slice(1)] = pathSegment;
|
|
48
|
+
} else if (patternSegment !== pathSegment) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { params };
|
|
53
|
+
}
|
|
54
|
+
function parseQuery(search) {
|
|
55
|
+
const query = {};
|
|
56
|
+
if (search === "" || search === "?") {
|
|
57
|
+
return query;
|
|
58
|
+
}
|
|
59
|
+
for (const [key, value] of new URLSearchParams(search)) {
|
|
60
|
+
query[key] = value;
|
|
61
|
+
}
|
|
62
|
+
return query;
|
|
63
|
+
}
|
|
64
|
+
function resolveRoute(registry, location) {
|
|
65
|
+
const hit = registry.findByPath(location.pathname);
|
|
66
|
+
if (hit === null) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const { menu, params } = hit;
|
|
70
|
+
return {
|
|
71
|
+
path: location.pathname,
|
|
72
|
+
params,
|
|
73
|
+
query: parseQuery(location.search ?? ""),
|
|
74
|
+
pageId: menu.pageId,
|
|
75
|
+
menuId: menu.id,
|
|
76
|
+
activeMenu: menu.activeMenu ?? menu.id
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/menu.ts
|
|
81
|
+
function filterHidden(items) {
|
|
82
|
+
const result = [];
|
|
83
|
+
for (const item of items) {
|
|
84
|
+
if (item.hidden) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (item.children !== void 0) {
|
|
88
|
+
result.push({ ...item, children: filterHidden(item.children) });
|
|
89
|
+
} else {
|
|
90
|
+
result.push(item);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
function sortByOrder(items) {
|
|
96
|
+
return [...items].sort(
|
|
97
|
+
(a, b) => (a.order ?? Number.POSITIVE_INFINITY) - (b.order ?? Number.POSITIVE_INFINITY)
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
var MenuRegistry = class {
|
|
101
|
+
constructor(menus) {
|
|
102
|
+
this.byId = /* @__PURE__ */ new Map();
|
|
103
|
+
this.visibleTree = filterHidden(menus);
|
|
104
|
+
const flat = [];
|
|
105
|
+
const walk = (items) => {
|
|
106
|
+
for (const item of items) {
|
|
107
|
+
this.byId.set(item.id, item);
|
|
108
|
+
if (item.type !== "group" && item.path !== void 0) {
|
|
109
|
+
flat.push(item);
|
|
110
|
+
}
|
|
111
|
+
if (item.children !== void 0) {
|
|
112
|
+
walk(item.children);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
walk(menus);
|
|
117
|
+
this.entries = sortByOrder(flat);
|
|
118
|
+
}
|
|
119
|
+
/** 可见菜单树(过滤 hidden,供 layout 渲染) */
|
|
120
|
+
tree() {
|
|
121
|
+
return this.visibleTree;
|
|
122
|
+
}
|
|
123
|
+
/** 扁平可路由条目列表,按 order 排序(未指定者保持声明顺序在后) */
|
|
124
|
+
list() {
|
|
125
|
+
return this.entries;
|
|
126
|
+
}
|
|
127
|
+
/** 按 id 查找(含 group 与隐藏项),未找到返回 undefined */
|
|
128
|
+
findById(id) {
|
|
129
|
+
return this.byId.get(id);
|
|
130
|
+
}
|
|
131
|
+
/** 按 list() 顺序逐个模式匹配;隐藏页参与匹配;未命中返回 null */
|
|
132
|
+
findByPath(pathname) {
|
|
133
|
+
for (const menu of this.entries) {
|
|
134
|
+
const result = match(menu.path, pathname);
|
|
135
|
+
if (result !== null) {
|
|
136
|
+
return { menu, params: result.params };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// src/navigation.ts
|
|
144
|
+
function splitTo(to) {
|
|
145
|
+
const queryIndex = to.indexOf("?");
|
|
146
|
+
if (queryIndex === -1) {
|
|
147
|
+
return { pathname: to, search: "" };
|
|
148
|
+
}
|
|
149
|
+
return { pathname: to.slice(0, queryIndex), search: to.slice(queryIndex) };
|
|
150
|
+
}
|
|
151
|
+
var MemoryNavigation = class {
|
|
152
|
+
constructor(initial = "/") {
|
|
153
|
+
this.index = 0;
|
|
154
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
155
|
+
this.history = [splitTo(initial)];
|
|
156
|
+
}
|
|
157
|
+
getLocation() {
|
|
158
|
+
return this.history[this.index];
|
|
159
|
+
}
|
|
160
|
+
push(to) {
|
|
161
|
+
this.history = this.history.slice(0, this.index + 1);
|
|
162
|
+
this.history.push(splitTo(to));
|
|
163
|
+
this.index += 1;
|
|
164
|
+
this.notify();
|
|
165
|
+
}
|
|
166
|
+
replace(to) {
|
|
167
|
+
this.history[this.index] = splitTo(to);
|
|
168
|
+
this.notify();
|
|
169
|
+
}
|
|
170
|
+
/** 回退一步;已在栈底时不动作并返回 false */
|
|
171
|
+
back() {
|
|
172
|
+
if (this.index === 0) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
this.index -= 1;
|
|
176
|
+
this.notify();
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
/** 当前历史栈长度(调试与测试观察用) */
|
|
180
|
+
get size() {
|
|
181
|
+
return this.history.length;
|
|
182
|
+
}
|
|
183
|
+
subscribe(listener) {
|
|
184
|
+
this.listeners.add(listener);
|
|
185
|
+
return () => {
|
|
186
|
+
this.listeners.delete(listener);
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
notify() {
|
|
190
|
+
const location = this.getLocation();
|
|
191
|
+
for (const listener of [...this.listeners]) {
|
|
192
|
+
listener(location);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
function createRouterAdapter(bridge) {
|
|
197
|
+
return {
|
|
198
|
+
navigate(to) {
|
|
199
|
+
bridge.push(to);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
204
|
+
0 && (module.exports = {
|
|
205
|
+
MemoryNavigation,
|
|
206
|
+
MenuRegistry,
|
|
207
|
+
createRouterAdapter,
|
|
208
|
+
match,
|
|
209
|
+
parseQuery,
|
|
210
|
+
resolveRoute
|
|
211
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { PermissionConfig } from '@pactor-app/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 菜单项模型(设计文档第 30 节,决策 D1)。
|
|
5
|
+
*
|
|
6
|
+
* 菜单声明 `path` + `pageId`,不绑定 React 组件;Dynamic Router 把
|
|
7
|
+
* URL 解析为 pageId 与路由上下文。`keepAlive` / `tab` / `breadcrumb`
|
|
8
|
+
* 等 UI 语义字段由 @pactor-app/ui 的 layout 层消费,本包只保留字段与 activeMenu 解析。
|
|
9
|
+
*/
|
|
10
|
+
interface MenuItem {
|
|
11
|
+
id: string;
|
|
12
|
+
type: 'group' | 'page' | 'link';
|
|
13
|
+
title: string;
|
|
14
|
+
icon?: string;
|
|
15
|
+
order?: number;
|
|
16
|
+
path?: string;
|
|
17
|
+
pageId?: string;
|
|
18
|
+
/** 访问权限(@pactor-app/core PermissionConfig,守卫由 @pactor-app/permission 提供) */
|
|
19
|
+
permission?: PermissionConfig;
|
|
20
|
+
/** 隐藏页:不出现在可见菜单树,但参与路径匹配(详情页场景) */
|
|
21
|
+
hidden?: boolean;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
external?: boolean;
|
|
24
|
+
target?: string;
|
|
25
|
+
keepAlive?: boolean;
|
|
26
|
+
badge?: string | number;
|
|
27
|
+
/** 高亮菜单 id:命中该项时高亮另一个菜单项(缺省为自身 id) */
|
|
28
|
+
activeMenu?: string;
|
|
29
|
+
defaultOpen?: boolean;
|
|
30
|
+
breadcrumb?: boolean;
|
|
31
|
+
tab?: boolean;
|
|
32
|
+
children?: MenuItem[];
|
|
33
|
+
}
|
|
34
|
+
/** findByPath 命中结果(`menu` 字段预留供 layout 推导面包屑等) */
|
|
35
|
+
interface MenuPathMatch {
|
|
36
|
+
menu: MenuItem;
|
|
37
|
+
params: Record<string, string>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* MenuRegistry(决策 D2):由菜单树构建,产出可见菜单树(过滤 hidden)
|
|
41
|
+
* + 扁平可路由条目列表(type 非 group 且有 path),并维护 id 索引。
|
|
42
|
+
*
|
|
43
|
+
* - `findByPath` 按 list() 顺序逐个做路径模式匹配,隐藏页参与匹配;
|
|
44
|
+
* - `findById` 覆盖全树(含 group 与隐藏项)。
|
|
45
|
+
*/
|
|
46
|
+
declare class MenuRegistry {
|
|
47
|
+
private readonly visibleTree;
|
|
48
|
+
private readonly entries;
|
|
49
|
+
private readonly byId;
|
|
50
|
+
constructor(menus: readonly MenuItem[]);
|
|
51
|
+
/** 可见菜单树(过滤 hidden,供 layout 渲染) */
|
|
52
|
+
tree(): MenuItem[];
|
|
53
|
+
/** 扁平可路由条目列表,按 order 排序(未指定者保持声明顺序在后) */
|
|
54
|
+
list(): MenuItem[];
|
|
55
|
+
/** 按 id 查找(含 group 与隐藏项),未找到返回 undefined */
|
|
56
|
+
findById(id: string): MenuItem | undefined;
|
|
57
|
+
/** 按 list() 顺序逐个模式匹配;隐藏页参与匹配;未命中返回 null */
|
|
58
|
+
findByPath(pathname: string): MenuPathMatch | null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** match 命中结果 */
|
|
62
|
+
interface PathMatchResult {
|
|
63
|
+
params: Record<string, string>;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 路径模式匹配(决策 D3):仅支持静态段与 `:param` 段
|
|
67
|
+
* (如 `/customers/:id/edit`),整路径精确匹配(段数一致)。
|
|
68
|
+
* V1 不支持 `*` 通配与可选段。
|
|
69
|
+
*/
|
|
70
|
+
declare function match(pattern: string, pathname: string): PathMatchResult | null;
|
|
71
|
+
/**
|
|
72
|
+
* 解析查询串:`?tab=risk&page=2` → `{ tab: 'risk', page: '2' }`;
|
|
73
|
+
* 空字符串与裸 `?` 返回空对象。
|
|
74
|
+
*/
|
|
75
|
+
declare function parseQuery(search: string): Record<string, string>;
|
|
76
|
+
/** 路由上下文(设计文档第 32 节,决策 D3) */
|
|
77
|
+
interface RouteContext {
|
|
78
|
+
/** 当前 pathname */
|
|
79
|
+
path: string;
|
|
80
|
+
params: Record<string, string>;
|
|
81
|
+
query: Record<string, string>;
|
|
82
|
+
pageId?: string;
|
|
83
|
+
menuId?: string;
|
|
84
|
+
/** 高亮菜单 id:菜单项 activeMenu ?? 自身 id */
|
|
85
|
+
activeMenu?: string;
|
|
86
|
+
}
|
|
87
|
+
/** resolveRoute 的入参位置(search 可省略,视为无查询串) */
|
|
88
|
+
interface RouteLocation {
|
|
89
|
+
pathname: string;
|
|
90
|
+
search?: string;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 把当前位置解析为 RouteContext:findByPath 命中 →
|
|
94
|
+
* `{ path, params, query, pageId, menuId, activeMenu }`;未命中返回 null。
|
|
95
|
+
*/
|
|
96
|
+
declare function resolveRoute(registry: MenuRegistry, location: RouteLocation): RouteContext | null;
|
|
97
|
+
|
|
98
|
+
/** 导航位置(pathname + search,search 含前导 `?` 或为空串) */
|
|
99
|
+
interface NavigationLocation {
|
|
100
|
+
pathname: string;
|
|
101
|
+
search: string;
|
|
102
|
+
}
|
|
103
|
+
/** location 变化监听器 */
|
|
104
|
+
type NavigationListener = (location: NavigationLocation) => void;
|
|
105
|
+
/**
|
|
106
|
+
* 导航桥接抽象(决策 D4):核心逻辑与具体路由库
|
|
107
|
+
* (React Router / hash / memory)解耦。
|
|
108
|
+
*/
|
|
109
|
+
interface NavigationBridge {
|
|
110
|
+
getLocation(): NavigationLocation;
|
|
111
|
+
push(to: string): void;
|
|
112
|
+
replace(to: string): void;
|
|
113
|
+
/** 订阅 location 变化,返回退订函数 */
|
|
114
|
+
subscribe(listener: NavigationListener): () => void;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* MemoryNavigation(决策 D4):零依赖内存导航,维护历史栈。
|
|
118
|
+
* push 追加历史(截断 forward 分支)、replace 替换当前、back 回退;
|
|
119
|
+
* 任何变化通知订阅者。可纯 Node 使用与测试。
|
|
120
|
+
*/
|
|
121
|
+
declare class MemoryNavigation implements NavigationBridge {
|
|
122
|
+
private history;
|
|
123
|
+
private index;
|
|
124
|
+
private readonly listeners;
|
|
125
|
+
constructor(initial?: string);
|
|
126
|
+
getLocation(): NavigationLocation;
|
|
127
|
+
push(to: string): void;
|
|
128
|
+
replace(to: string): void;
|
|
129
|
+
/** 回退一步;已在栈底时不动作并返回 false */
|
|
130
|
+
back(): boolean;
|
|
131
|
+
/** 当前历史栈长度(调试与测试观察用) */
|
|
132
|
+
get size(): number;
|
|
133
|
+
subscribe(listener: NavigationListener): () => void;
|
|
134
|
+
private notify;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* runtime RouterAdapter 的结构化形状(与 @pactor-app/runtime 的 RouterAdapter
|
|
138
|
+
* 结构化兼容,避免对 @pactor-app/runtime 的运行时依赖;编译期断言见测试)。
|
|
139
|
+
*/
|
|
140
|
+
interface RouterAdapterLike {
|
|
141
|
+
navigate(to: string): void;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* 桥接 @pactor-app/runtime 的 RouterAdapter(决策 D4):DSL navigate 动作
|
|
145
|
+
* 经 runtime `route.navigate(to)` → `adapter.navigate(to)` → `bridge.push(to)`。
|
|
146
|
+
*/
|
|
147
|
+
declare function createRouterAdapter(bridge: NavigationBridge): RouterAdapterLike;
|
|
148
|
+
|
|
149
|
+
export { MemoryNavigation, type MenuItem, type MenuPathMatch, MenuRegistry, type NavigationBridge, type NavigationListener, type NavigationLocation, type PathMatchResult, type RouteContext, type RouteLocation, type RouterAdapterLike, createRouterAdapter, match, parseQuery, resolveRoute };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { PermissionConfig } from '@pactor-app/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 菜单项模型(设计文档第 30 节,决策 D1)。
|
|
5
|
+
*
|
|
6
|
+
* 菜单声明 `path` + `pageId`,不绑定 React 组件;Dynamic Router 把
|
|
7
|
+
* URL 解析为 pageId 与路由上下文。`keepAlive` / `tab` / `breadcrumb`
|
|
8
|
+
* 等 UI 语义字段由 @pactor-app/ui 的 layout 层消费,本包只保留字段与 activeMenu 解析。
|
|
9
|
+
*/
|
|
10
|
+
interface MenuItem {
|
|
11
|
+
id: string;
|
|
12
|
+
type: 'group' | 'page' | 'link';
|
|
13
|
+
title: string;
|
|
14
|
+
icon?: string;
|
|
15
|
+
order?: number;
|
|
16
|
+
path?: string;
|
|
17
|
+
pageId?: string;
|
|
18
|
+
/** 访问权限(@pactor-app/core PermissionConfig,守卫由 @pactor-app/permission 提供) */
|
|
19
|
+
permission?: PermissionConfig;
|
|
20
|
+
/** 隐藏页:不出现在可见菜单树,但参与路径匹配(详情页场景) */
|
|
21
|
+
hidden?: boolean;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
external?: boolean;
|
|
24
|
+
target?: string;
|
|
25
|
+
keepAlive?: boolean;
|
|
26
|
+
badge?: string | number;
|
|
27
|
+
/** 高亮菜单 id:命中该项时高亮另一个菜单项(缺省为自身 id) */
|
|
28
|
+
activeMenu?: string;
|
|
29
|
+
defaultOpen?: boolean;
|
|
30
|
+
breadcrumb?: boolean;
|
|
31
|
+
tab?: boolean;
|
|
32
|
+
children?: MenuItem[];
|
|
33
|
+
}
|
|
34
|
+
/** findByPath 命中结果(`menu` 字段预留供 layout 推导面包屑等) */
|
|
35
|
+
interface MenuPathMatch {
|
|
36
|
+
menu: MenuItem;
|
|
37
|
+
params: Record<string, string>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* MenuRegistry(决策 D2):由菜单树构建,产出可见菜单树(过滤 hidden)
|
|
41
|
+
* + 扁平可路由条目列表(type 非 group 且有 path),并维护 id 索引。
|
|
42
|
+
*
|
|
43
|
+
* - `findByPath` 按 list() 顺序逐个做路径模式匹配,隐藏页参与匹配;
|
|
44
|
+
* - `findById` 覆盖全树(含 group 与隐藏项)。
|
|
45
|
+
*/
|
|
46
|
+
declare class MenuRegistry {
|
|
47
|
+
private readonly visibleTree;
|
|
48
|
+
private readonly entries;
|
|
49
|
+
private readonly byId;
|
|
50
|
+
constructor(menus: readonly MenuItem[]);
|
|
51
|
+
/** 可见菜单树(过滤 hidden,供 layout 渲染) */
|
|
52
|
+
tree(): MenuItem[];
|
|
53
|
+
/** 扁平可路由条目列表,按 order 排序(未指定者保持声明顺序在后) */
|
|
54
|
+
list(): MenuItem[];
|
|
55
|
+
/** 按 id 查找(含 group 与隐藏项),未找到返回 undefined */
|
|
56
|
+
findById(id: string): MenuItem | undefined;
|
|
57
|
+
/** 按 list() 顺序逐个模式匹配;隐藏页参与匹配;未命中返回 null */
|
|
58
|
+
findByPath(pathname: string): MenuPathMatch | null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** match 命中结果 */
|
|
62
|
+
interface PathMatchResult {
|
|
63
|
+
params: Record<string, string>;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 路径模式匹配(决策 D3):仅支持静态段与 `:param` 段
|
|
67
|
+
* (如 `/customers/:id/edit`),整路径精确匹配(段数一致)。
|
|
68
|
+
* V1 不支持 `*` 通配与可选段。
|
|
69
|
+
*/
|
|
70
|
+
declare function match(pattern: string, pathname: string): PathMatchResult | null;
|
|
71
|
+
/**
|
|
72
|
+
* 解析查询串:`?tab=risk&page=2` → `{ tab: 'risk', page: '2' }`;
|
|
73
|
+
* 空字符串与裸 `?` 返回空对象。
|
|
74
|
+
*/
|
|
75
|
+
declare function parseQuery(search: string): Record<string, string>;
|
|
76
|
+
/** 路由上下文(设计文档第 32 节,决策 D3) */
|
|
77
|
+
interface RouteContext {
|
|
78
|
+
/** 当前 pathname */
|
|
79
|
+
path: string;
|
|
80
|
+
params: Record<string, string>;
|
|
81
|
+
query: Record<string, string>;
|
|
82
|
+
pageId?: string;
|
|
83
|
+
menuId?: string;
|
|
84
|
+
/** 高亮菜单 id:菜单项 activeMenu ?? 自身 id */
|
|
85
|
+
activeMenu?: string;
|
|
86
|
+
}
|
|
87
|
+
/** resolveRoute 的入参位置(search 可省略,视为无查询串) */
|
|
88
|
+
interface RouteLocation {
|
|
89
|
+
pathname: string;
|
|
90
|
+
search?: string;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 把当前位置解析为 RouteContext:findByPath 命中 →
|
|
94
|
+
* `{ path, params, query, pageId, menuId, activeMenu }`;未命中返回 null。
|
|
95
|
+
*/
|
|
96
|
+
declare function resolveRoute(registry: MenuRegistry, location: RouteLocation): RouteContext | null;
|
|
97
|
+
|
|
98
|
+
/** 导航位置(pathname + search,search 含前导 `?` 或为空串) */
|
|
99
|
+
interface NavigationLocation {
|
|
100
|
+
pathname: string;
|
|
101
|
+
search: string;
|
|
102
|
+
}
|
|
103
|
+
/** location 变化监听器 */
|
|
104
|
+
type NavigationListener = (location: NavigationLocation) => void;
|
|
105
|
+
/**
|
|
106
|
+
* 导航桥接抽象(决策 D4):核心逻辑与具体路由库
|
|
107
|
+
* (React Router / hash / memory)解耦。
|
|
108
|
+
*/
|
|
109
|
+
interface NavigationBridge {
|
|
110
|
+
getLocation(): NavigationLocation;
|
|
111
|
+
push(to: string): void;
|
|
112
|
+
replace(to: string): void;
|
|
113
|
+
/** 订阅 location 变化,返回退订函数 */
|
|
114
|
+
subscribe(listener: NavigationListener): () => void;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* MemoryNavigation(决策 D4):零依赖内存导航,维护历史栈。
|
|
118
|
+
* push 追加历史(截断 forward 分支)、replace 替换当前、back 回退;
|
|
119
|
+
* 任何变化通知订阅者。可纯 Node 使用与测试。
|
|
120
|
+
*/
|
|
121
|
+
declare class MemoryNavigation implements NavigationBridge {
|
|
122
|
+
private history;
|
|
123
|
+
private index;
|
|
124
|
+
private readonly listeners;
|
|
125
|
+
constructor(initial?: string);
|
|
126
|
+
getLocation(): NavigationLocation;
|
|
127
|
+
push(to: string): void;
|
|
128
|
+
replace(to: string): void;
|
|
129
|
+
/** 回退一步;已在栈底时不动作并返回 false */
|
|
130
|
+
back(): boolean;
|
|
131
|
+
/** 当前历史栈长度(调试与测试观察用) */
|
|
132
|
+
get size(): number;
|
|
133
|
+
subscribe(listener: NavigationListener): () => void;
|
|
134
|
+
private notify;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* runtime RouterAdapter 的结构化形状(与 @pactor-app/runtime 的 RouterAdapter
|
|
138
|
+
* 结构化兼容,避免对 @pactor-app/runtime 的运行时依赖;编译期断言见测试)。
|
|
139
|
+
*/
|
|
140
|
+
interface RouterAdapterLike {
|
|
141
|
+
navigate(to: string): void;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* 桥接 @pactor-app/runtime 的 RouterAdapter(决策 D4):DSL navigate 动作
|
|
145
|
+
* 经 runtime `route.navigate(to)` → `adapter.navigate(to)` → `bridge.push(to)`。
|
|
146
|
+
*/
|
|
147
|
+
declare function createRouterAdapter(bridge: NavigationBridge): RouterAdapterLike;
|
|
148
|
+
|
|
149
|
+
export { MemoryNavigation, type MenuItem, type MenuPathMatch, MenuRegistry, type NavigationBridge, type NavigationListener, type NavigationLocation, type PathMatchResult, type RouteContext, type RouteLocation, type RouterAdapterLike, createRouterAdapter, match, parseQuery, resolveRoute };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import {
|
|
2
|
+
match,
|
|
3
|
+
parseQuery,
|
|
4
|
+
resolveRoute
|
|
5
|
+
} from "./chunk-JY7Z55CR.js";
|
|
6
|
+
|
|
7
|
+
// src/menu.ts
|
|
8
|
+
function filterHidden(items) {
|
|
9
|
+
const result = [];
|
|
10
|
+
for (const item of items) {
|
|
11
|
+
if (item.hidden) {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (item.children !== void 0) {
|
|
15
|
+
result.push({ ...item, children: filterHidden(item.children) });
|
|
16
|
+
} else {
|
|
17
|
+
result.push(item);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
function sortByOrder(items) {
|
|
23
|
+
return [...items].sort(
|
|
24
|
+
(a, b) => (a.order ?? Number.POSITIVE_INFINITY) - (b.order ?? Number.POSITIVE_INFINITY)
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
var MenuRegistry = class {
|
|
28
|
+
constructor(menus) {
|
|
29
|
+
this.byId = /* @__PURE__ */ new Map();
|
|
30
|
+
this.visibleTree = filterHidden(menus);
|
|
31
|
+
const flat = [];
|
|
32
|
+
const walk = (items) => {
|
|
33
|
+
for (const item of items) {
|
|
34
|
+
this.byId.set(item.id, item);
|
|
35
|
+
if (item.type !== "group" && item.path !== void 0) {
|
|
36
|
+
flat.push(item);
|
|
37
|
+
}
|
|
38
|
+
if (item.children !== void 0) {
|
|
39
|
+
walk(item.children);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
walk(menus);
|
|
44
|
+
this.entries = sortByOrder(flat);
|
|
45
|
+
}
|
|
46
|
+
/** 可见菜单树(过滤 hidden,供 layout 渲染) */
|
|
47
|
+
tree() {
|
|
48
|
+
return this.visibleTree;
|
|
49
|
+
}
|
|
50
|
+
/** 扁平可路由条目列表,按 order 排序(未指定者保持声明顺序在后) */
|
|
51
|
+
list() {
|
|
52
|
+
return this.entries;
|
|
53
|
+
}
|
|
54
|
+
/** 按 id 查找(含 group 与隐藏项),未找到返回 undefined */
|
|
55
|
+
findById(id) {
|
|
56
|
+
return this.byId.get(id);
|
|
57
|
+
}
|
|
58
|
+
/** 按 list() 顺序逐个模式匹配;隐藏页参与匹配;未命中返回 null */
|
|
59
|
+
findByPath(pathname) {
|
|
60
|
+
for (const menu of this.entries) {
|
|
61
|
+
const result = match(menu.path, pathname);
|
|
62
|
+
if (result !== null) {
|
|
63
|
+
return { menu, params: result.params };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// src/navigation.ts
|
|
71
|
+
function splitTo(to) {
|
|
72
|
+
const queryIndex = to.indexOf("?");
|
|
73
|
+
if (queryIndex === -1) {
|
|
74
|
+
return { pathname: to, search: "" };
|
|
75
|
+
}
|
|
76
|
+
return { pathname: to.slice(0, queryIndex), search: to.slice(queryIndex) };
|
|
77
|
+
}
|
|
78
|
+
var MemoryNavigation = class {
|
|
79
|
+
constructor(initial = "/") {
|
|
80
|
+
this.index = 0;
|
|
81
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
82
|
+
this.history = [splitTo(initial)];
|
|
83
|
+
}
|
|
84
|
+
getLocation() {
|
|
85
|
+
return this.history[this.index];
|
|
86
|
+
}
|
|
87
|
+
push(to) {
|
|
88
|
+
this.history = this.history.slice(0, this.index + 1);
|
|
89
|
+
this.history.push(splitTo(to));
|
|
90
|
+
this.index += 1;
|
|
91
|
+
this.notify();
|
|
92
|
+
}
|
|
93
|
+
replace(to) {
|
|
94
|
+
this.history[this.index] = splitTo(to);
|
|
95
|
+
this.notify();
|
|
96
|
+
}
|
|
97
|
+
/** 回退一步;已在栈底时不动作并返回 false */
|
|
98
|
+
back() {
|
|
99
|
+
if (this.index === 0) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
this.index -= 1;
|
|
103
|
+
this.notify();
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
/** 当前历史栈长度(调试与测试观察用) */
|
|
107
|
+
get size() {
|
|
108
|
+
return this.history.length;
|
|
109
|
+
}
|
|
110
|
+
subscribe(listener) {
|
|
111
|
+
this.listeners.add(listener);
|
|
112
|
+
return () => {
|
|
113
|
+
this.listeners.delete(listener);
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
notify() {
|
|
117
|
+
const location = this.getLocation();
|
|
118
|
+
for (const listener of [...this.listeners]) {
|
|
119
|
+
listener(location);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
function createRouterAdapter(bridge) {
|
|
124
|
+
return {
|
|
125
|
+
navigate(to) {
|
|
126
|
+
bridge.push(to);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
export {
|
|
131
|
+
MemoryNavigation,
|
|
132
|
+
MenuRegistry,
|
|
133
|
+
createRouterAdapter,
|
|
134
|
+
match,
|
|
135
|
+
parseQuery,
|
|
136
|
+
resolveRoute
|
|
137
|
+
};
|
package/dist/react.cjs
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/react.ts
|
|
21
|
+
var react_exports = {};
|
|
22
|
+
__export(react_exports, {
|
|
23
|
+
DynamicDslRoute: () => DynamicDslRoute,
|
|
24
|
+
useReactRouterBridge: () => useReactRouterBridge
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(react_exports);
|
|
27
|
+
var import_permission = require("@pactor-app/permission");
|
|
28
|
+
var import_react = require("react");
|
|
29
|
+
var import_react_router_dom = require("react-router-dom");
|
|
30
|
+
|
|
31
|
+
// src/path-matcher.ts
|
|
32
|
+
function parseQuery(search) {
|
|
33
|
+
const query = {};
|
|
34
|
+
if (search === "" || search === "?") {
|
|
35
|
+
return query;
|
|
36
|
+
}
|
|
37
|
+
for (const [key, value] of new URLSearchParams(search)) {
|
|
38
|
+
query[key] = value;
|
|
39
|
+
}
|
|
40
|
+
return query;
|
|
41
|
+
}
|
|
42
|
+
function resolveRoute(registry, location) {
|
|
43
|
+
const hit = registry.findByPath(location.pathname);
|
|
44
|
+
if (hit === null) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const { menu, params } = hit;
|
|
48
|
+
return {
|
|
49
|
+
path: location.pathname,
|
|
50
|
+
params,
|
|
51
|
+
query: parseQuery(location.search ?? ""),
|
|
52
|
+
pageId: menu.pageId,
|
|
53
|
+
menuId: menu.id,
|
|
54
|
+
activeMenu: menu.activeMenu ?? menu.id
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/react.ts
|
|
59
|
+
function DynamicDslRoute(props) {
|
|
60
|
+
const {
|
|
61
|
+
registry,
|
|
62
|
+
render,
|
|
63
|
+
fallback = null,
|
|
64
|
+
forbidden = null,
|
|
65
|
+
permissionChecker
|
|
66
|
+
} = props;
|
|
67
|
+
const location = (0, import_react_router_dom.useLocation)();
|
|
68
|
+
const hit = registry.findByPath(location.pathname);
|
|
69
|
+
const route = resolveRoute(registry, {
|
|
70
|
+
pathname: location.pathname,
|
|
71
|
+
search: location.search
|
|
72
|
+
});
|
|
73
|
+
if (hit === null || route === null || hit.menu.pageId === void 0) {
|
|
74
|
+
return fallback;
|
|
75
|
+
}
|
|
76
|
+
if (permissionChecker !== void 0 && !(0, import_permission.checkPagePermission)(hit.menu, permissionChecker)) {
|
|
77
|
+
return forbidden;
|
|
78
|
+
}
|
|
79
|
+
return render({ pageId: hit.menu.pageId, route });
|
|
80
|
+
}
|
|
81
|
+
function useReactRouterBridge() {
|
|
82
|
+
const navigate = (0, import_react_router_dom.useNavigate)();
|
|
83
|
+
const location = (0, import_react_router_dom.useLocation)();
|
|
84
|
+
const locationRef = (0, import_react.useRef)(location);
|
|
85
|
+
locationRef.current = location;
|
|
86
|
+
return (0, import_react.useMemo)(
|
|
87
|
+
() => ({
|
|
88
|
+
getLocation: () => ({
|
|
89
|
+
pathname: locationRef.current.pathname,
|
|
90
|
+
search: locationRef.current.search
|
|
91
|
+
}),
|
|
92
|
+
push: (to) => navigate(to),
|
|
93
|
+
replace: (to) => navigate(to, { replace: true }),
|
|
94
|
+
subscribe: () => () => {
|
|
95
|
+
}
|
|
96
|
+
}),
|
|
97
|
+
[navigate]
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
101
|
+
0 && (module.exports = {
|
|
102
|
+
DynamicDslRoute,
|
|
103
|
+
useReactRouterBridge
|
|
104
|
+
});
|
package/dist/react.d.cts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { PermissionChecker } from '@pactor-app/permission';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { MenuRegistry, RouteContext, NavigationBridge } from './index.cjs';
|
|
4
|
+
import '@pactor-app/core';
|
|
5
|
+
|
|
6
|
+
/** 命中页面时 render 的入参 */
|
|
7
|
+
interface RenderPageArgs {
|
|
8
|
+
pageId: string;
|
|
9
|
+
route: RouteContext;
|
|
10
|
+
}
|
|
11
|
+
/** DynamicDslRoute 的 props(决策 D5) */
|
|
12
|
+
interface DynamicDslRouteProps {
|
|
13
|
+
registry: MenuRegistry;
|
|
14
|
+
/** 命中且有权限时渲染页面 */
|
|
15
|
+
render(page: RenderPageArgs): ReactNode;
|
|
16
|
+
/** 路径未命中(404)渲染内容,缺省为 null */
|
|
17
|
+
fallback?: ReactNode;
|
|
18
|
+
/** 无权限(403)渲染内容,缺省为 null */
|
|
19
|
+
forbidden?: ReactNode;
|
|
20
|
+
/** 权限判定(@pactor-app/permission PermissionChecker,直接传 PermissionStore 即可) */
|
|
21
|
+
permissionChecker?: PermissionChecker;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* DynamicDslRoute(设计文档第 31 节,决策 D5),用法:
|
|
25
|
+
* `<Route path="*" element={<DynamicDslRoute registry={...} render={...} />} />`。
|
|
26
|
+
*
|
|
27
|
+
* useLocation 变化 → 重新解析:未命中(或命中项无 pageId)渲染 fallback;
|
|
28
|
+
* 配置了 permissionChecker 且页面无权限时渲染 forbidden(不调用 render);
|
|
29
|
+
* 否则 `render({ pageId, route })`。
|
|
30
|
+
*/
|
|
31
|
+
declare function DynamicDslRoute(props: DynamicDslRouteProps): ReactNode;
|
|
32
|
+
/**
|
|
33
|
+
* 把 React Router 的 useNavigate / useLocation 包装为 NavigationBridge
|
|
34
|
+
* (决策 D5)。仅用稳定 API,兼容 react-router-dom v6/v7。
|
|
35
|
+
*
|
|
36
|
+
* 注意:React Router 自身驱动组件重渲染,无裸事件源,故 subscribe
|
|
37
|
+
* 返回 no-op 退订函数;getLocation 始终返回当前渲染的 location。
|
|
38
|
+
*/
|
|
39
|
+
declare function useReactRouterBridge(): NavigationBridge;
|
|
40
|
+
|
|
41
|
+
export { DynamicDslRoute, type DynamicDslRouteProps, type RenderPageArgs, useReactRouterBridge };
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { PermissionChecker } from '@pactor-app/permission';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { MenuRegistry, RouteContext, NavigationBridge } from './index.js';
|
|
4
|
+
import '@pactor-app/core';
|
|
5
|
+
|
|
6
|
+
/** 命中页面时 render 的入参 */
|
|
7
|
+
interface RenderPageArgs {
|
|
8
|
+
pageId: string;
|
|
9
|
+
route: RouteContext;
|
|
10
|
+
}
|
|
11
|
+
/** DynamicDslRoute 的 props(决策 D5) */
|
|
12
|
+
interface DynamicDslRouteProps {
|
|
13
|
+
registry: MenuRegistry;
|
|
14
|
+
/** 命中且有权限时渲染页面 */
|
|
15
|
+
render(page: RenderPageArgs): ReactNode;
|
|
16
|
+
/** 路径未命中(404)渲染内容,缺省为 null */
|
|
17
|
+
fallback?: ReactNode;
|
|
18
|
+
/** 无权限(403)渲染内容,缺省为 null */
|
|
19
|
+
forbidden?: ReactNode;
|
|
20
|
+
/** 权限判定(@pactor-app/permission PermissionChecker,直接传 PermissionStore 即可) */
|
|
21
|
+
permissionChecker?: PermissionChecker;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* DynamicDslRoute(设计文档第 31 节,决策 D5),用法:
|
|
25
|
+
* `<Route path="*" element={<DynamicDslRoute registry={...} render={...} />} />`。
|
|
26
|
+
*
|
|
27
|
+
* useLocation 变化 → 重新解析:未命中(或命中项无 pageId)渲染 fallback;
|
|
28
|
+
* 配置了 permissionChecker 且页面无权限时渲染 forbidden(不调用 render);
|
|
29
|
+
* 否则 `render({ pageId, route })`。
|
|
30
|
+
*/
|
|
31
|
+
declare function DynamicDslRoute(props: DynamicDslRouteProps): ReactNode;
|
|
32
|
+
/**
|
|
33
|
+
* 把 React Router 的 useNavigate / useLocation 包装为 NavigationBridge
|
|
34
|
+
* (决策 D5)。仅用稳定 API,兼容 react-router-dom v6/v7。
|
|
35
|
+
*
|
|
36
|
+
* 注意:React Router 自身驱动组件重渲染,无裸事件源,故 subscribe
|
|
37
|
+
* 返回 no-op 退订函数;getLocation 始终返回当前渲染的 location。
|
|
38
|
+
*/
|
|
39
|
+
declare function useReactRouterBridge(): NavigationBridge;
|
|
40
|
+
|
|
41
|
+
export { DynamicDslRoute, type DynamicDslRouteProps, type RenderPageArgs, useReactRouterBridge };
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveRoute
|
|
3
|
+
} from "./chunk-JY7Z55CR.js";
|
|
4
|
+
|
|
5
|
+
// src/react.ts
|
|
6
|
+
import {
|
|
7
|
+
checkPagePermission
|
|
8
|
+
} from "@pactor-app/permission";
|
|
9
|
+
import { useMemo, useRef } from "react";
|
|
10
|
+
import { useLocation, useNavigate } from "react-router-dom";
|
|
11
|
+
function DynamicDslRoute(props) {
|
|
12
|
+
const {
|
|
13
|
+
registry,
|
|
14
|
+
render,
|
|
15
|
+
fallback = null,
|
|
16
|
+
forbidden = null,
|
|
17
|
+
permissionChecker
|
|
18
|
+
} = props;
|
|
19
|
+
const location = useLocation();
|
|
20
|
+
const hit = registry.findByPath(location.pathname);
|
|
21
|
+
const route = resolveRoute(registry, {
|
|
22
|
+
pathname: location.pathname,
|
|
23
|
+
search: location.search
|
|
24
|
+
});
|
|
25
|
+
if (hit === null || route === null || hit.menu.pageId === void 0) {
|
|
26
|
+
return fallback;
|
|
27
|
+
}
|
|
28
|
+
if (permissionChecker !== void 0 && !checkPagePermission(hit.menu, permissionChecker)) {
|
|
29
|
+
return forbidden;
|
|
30
|
+
}
|
|
31
|
+
return render({ pageId: hit.menu.pageId, route });
|
|
32
|
+
}
|
|
33
|
+
function useReactRouterBridge() {
|
|
34
|
+
const navigate = useNavigate();
|
|
35
|
+
const location = useLocation();
|
|
36
|
+
const locationRef = useRef(location);
|
|
37
|
+
locationRef.current = location;
|
|
38
|
+
return useMemo(
|
|
39
|
+
() => ({
|
|
40
|
+
getLocation: () => ({
|
|
41
|
+
pathname: locationRef.current.pathname,
|
|
42
|
+
search: locationRef.current.search
|
|
43
|
+
}),
|
|
44
|
+
push: (to) => navigate(to),
|
|
45
|
+
replace: (to) => navigate(to, { replace: true }),
|
|
46
|
+
subscribe: () => () => {
|
|
47
|
+
}
|
|
48
|
+
}),
|
|
49
|
+
[navigate]
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
export {
|
|
53
|
+
DynamicDslRoute,
|
|
54
|
+
useReactRouterBridge
|
|
55
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pactor-app/router",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pactor router: menu registry, path matcher, navigation bridge and React Router dynamic route bindings",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pactor",
|
|
7
|
+
"dsl",
|
|
8
|
+
"server-driven-ui",
|
|
9
|
+
"router",
|
|
10
|
+
"menu"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/426-330/pactor.git",
|
|
16
|
+
"directory": "packages/router"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/426-330/pactor/tree/main/packages/router",
|
|
19
|
+
"bugs": "https://github.com/426-330/pactor/issues",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"main": "./dist/index.cjs",
|
|
22
|
+
"module": "./dist/index.js",
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"import": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"default": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"require": {
|
|
31
|
+
"types": "./dist/index.d.cts",
|
|
32
|
+
"default": "./dist/index.cjs"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"./react": {
|
|
36
|
+
"import": {
|
|
37
|
+
"types": "./dist/react.d.ts",
|
|
38
|
+
"default": "./dist/react.js"
|
|
39
|
+
},
|
|
40
|
+
"require": {
|
|
41
|
+
"types": "./dist/react.d.cts",
|
|
42
|
+
"default": "./dist/react.cjs"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"sideEffects": false,
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=18"
|
|
49
|
+
},
|
|
50
|
+
"files": [
|
|
51
|
+
"dist"
|
|
52
|
+
],
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@pactor-app/core": "0.1.0",
|
|
58
|
+
"@pactor-app/permission": "0.1.0"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"react": ">=18",
|
|
62
|
+
"react-router-dom": ">=6"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@testing-library/dom": "^10.4.1",
|
|
66
|
+
"@testing-library/react": "^16.3.2",
|
|
67
|
+
"@types/react": "^19.2.18",
|
|
68
|
+
"@types/react-dom": "^19.2.4",
|
|
69
|
+
"jsdom": "^30.0.1",
|
|
70
|
+
"react": "^19.2.8",
|
|
71
|
+
"react-dom": "^19.2.8",
|
|
72
|
+
"react-router-dom": "^7.18.2",
|
|
73
|
+
"@pactor-app/runtime": "0.1.0"
|
|
74
|
+
},
|
|
75
|
+
"scripts": {
|
|
76
|
+
"build": "tsup",
|
|
77
|
+
"test": "vitest run",
|
|
78
|
+
"typecheck": "tsc --noEmit"
|
|
79
|
+
}
|
|
80
|
+
}
|