vitarx-router 4.0.0-beta.16 → 4.0.0-beta.18
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 +1 -14
- package/dist/components/RouterView.js +1 -1
- package/dist/core/router/router.d.ts +5 -0
- package/dist/core/router/router.js +12 -1
- package/dist/core/shared/link.d.ts +7 -0
- package/dist/core/shared/link.js +11 -8
- package/dist/core/types/options.d.ts +2 -0
- package/dist/file-router/generator/generateRoutes.js +1 -1
- package/dist/file-router/index.d.ts +77 -6
- package/dist/file-router/index.js +168 -75
- package/dist/file-router/parser/filterUtils.js +1 -2
- package/dist/file-router/parser/parsePage.d.ts +26 -2
- package/dist/file-router/parser/parsePage.js +21 -15
- package/dist/file-router/parser/routePath.d.ts +22 -0
- package/dist/file-router/parser/routePath.js +74 -0
- package/dist/file-router/types/options.d.ts +2 -2
- package/dist/file-router/utils/findRoute.d.ts +8 -0
- package/dist/file-router/utils/findRoute.js +22 -0
- package/dist/file-router/utils/index.d.ts +1 -0
- package/dist/file-router/utils/index.js +1 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -423,20 +423,7 @@ export const router = createRouter({ routes })
|
|
|
423
423
|
|
|
424
424
|
### 文件路由配置选项
|
|
425
425
|
|
|
426
|
-
|
|
427
|
-
|------------------|--------------------------------------------|---------------|----------|
|
|
428
|
-
| `pages` | `PageSource \| readonly PageSource[]` | `'src/pages'` | 页面来源配置 |
|
|
429
|
-
| `pathStrategy` | `'kebab' \| 'lowercase' \| 'raw'` | `'kebab'` | 路径格式化策略 |
|
|
430
|
-
| `importMode` | `'lazy' \| 'sync' \| ImportModeFunction` | `'lazy'` | 组件导入模式 |
|
|
431
|
-
| `injectImports` | `readonly string[]` | - | 自定义导入语句 |
|
|
432
|
-
| `dts` | `boolean \| string` | `false` | 类型声明文件配置 |
|
|
433
|
-
| `layoutFileName` | `string` | `'_layout'` | 布局文件名 |
|
|
434
|
-
| `configFileName` | `string` | `'_config'` | 分组配置文件名 |
|
|
435
|
-
| `transform` | `CodeTransformHook` | - | 代码转换钩子 |
|
|
436
|
-
| `extendRoute` | `ExtendRouteHook` | - | 路由扩展钩子 |
|
|
437
|
-
| `pathParser` | `PathParser` | - | 自定义路径解析器 |
|
|
438
|
-
|
|
439
|
-
详细配置请参考 [File Router 文档](src/file-router/README.md)。
|
|
426
|
+
请参考 [File Router 文档](src/file-router/README.md)。
|
|
440
427
|
|
|
441
428
|
## TypeScript 支持
|
|
442
429
|
|
|
@@ -29,7 +29,7 @@ export function RouterView(props) {
|
|
|
29
29
|
const currentRoute = matchedRoute.value;
|
|
30
30
|
if (!currentRoute)
|
|
31
31
|
return null;
|
|
32
|
-
let injectProps = currentRoute.props?.[viewName.value] ?? router.config.props ??
|
|
32
|
+
let injectProps = currentRoute.props?.[viewName.value] ?? router.config.props ?? true;
|
|
33
33
|
if (injectProps === false)
|
|
34
34
|
return null; // 如果属性为 false,返回null
|
|
35
35
|
if (injectProps === true && currentRoute.pattern) {
|
|
@@ -34,6 +34,16 @@ export class Router {
|
|
|
34
34
|
writable: true,
|
|
35
35
|
value: void 0
|
|
36
36
|
});
|
|
37
|
+
/**
|
|
38
|
+
* 只读路由位置对象
|
|
39
|
+
* @private
|
|
40
|
+
*/
|
|
41
|
+
Object.defineProperty(this, "_readonlyLocation", {
|
|
42
|
+
enumerable: true,
|
|
43
|
+
configurable: true,
|
|
44
|
+
writable: true,
|
|
45
|
+
value: void 0
|
|
46
|
+
});
|
|
37
47
|
/**
|
|
38
48
|
* 存储就绪状态的 Promise(延迟创建)
|
|
39
49
|
* @private
|
|
@@ -159,12 +169,13 @@ export class Router {
|
|
|
159
169
|
matched: shallowReactive([]),
|
|
160
170
|
meta: shallowReactive({})
|
|
161
171
|
});
|
|
172
|
+
this._readonlyLocation = readonly(this._routeLocation);
|
|
162
173
|
}
|
|
163
174
|
/**
|
|
164
175
|
* 获取当前路由位置对象
|
|
165
176
|
*/
|
|
166
177
|
get route() {
|
|
167
|
-
return
|
|
178
|
+
return this._readonlyLocation;
|
|
168
179
|
}
|
|
169
180
|
/**
|
|
170
181
|
* 获取解析后的路由记录数组
|
|
@@ -48,6 +48,13 @@ export interface UseLinkReturn {
|
|
|
48
48
|
*/
|
|
49
49
|
navigate: (e?: MouseEvent) => Promise<NavigateResult | void>;
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* 判断是否为外部链接
|
|
53
|
+
*
|
|
54
|
+
* @param href - 链接地址
|
|
55
|
+
* @returns 是否为外部链接
|
|
56
|
+
*/
|
|
57
|
+
export declare function isExternalLink(href: string): boolean;
|
|
51
58
|
/**
|
|
52
59
|
* 创建一个链接助手,用于处理路由导航、生成链接属性及判断激活状态。
|
|
53
60
|
*
|
package/dist/core/shared/link.js
CHANGED
|
@@ -14,6 +14,15 @@ const handleTransition = async (callback) => {
|
|
|
14
14
|
const transition = document.startViewTransition(callback);
|
|
15
15
|
await transition.finished;
|
|
16
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* 判断是否为外部链接
|
|
19
|
+
*
|
|
20
|
+
* @param href - 链接地址
|
|
21
|
+
* @returns 是否为外部链接
|
|
22
|
+
*/
|
|
23
|
+
export function isExternalLink(href) {
|
|
24
|
+
return href.startsWith('http://') || href.startsWith('https://') || href.startsWith('//');
|
|
25
|
+
}
|
|
17
26
|
/**
|
|
18
27
|
* 创建一个链接助手,用于处理路由导航、生成链接属性及判断激活状态。
|
|
19
28
|
*
|
|
@@ -31,7 +40,6 @@ const handleTransition = async (callback) => {
|
|
|
31
40
|
*/
|
|
32
41
|
export function useLink(props) {
|
|
33
42
|
const router = useRouter();
|
|
34
|
-
const httpRegex = /^(https?):\/\/[^\s\/$.?#].\S*$/i;
|
|
35
43
|
/**
|
|
36
44
|
* 计算属性:解析目标路由
|
|
37
45
|
* @returns 返回解析后的路由位置对象,如果无效则返回 null
|
|
@@ -45,7 +53,7 @@ export function useLink(props) {
|
|
|
45
53
|
}
|
|
46
54
|
else if (isString(to)) {
|
|
47
55
|
// 如果是 HTTP/HTTPS 链接则返回 null
|
|
48
|
-
if (
|
|
56
|
+
if (isExternalLink(to))
|
|
49
57
|
return null;
|
|
50
58
|
target = { index: to };
|
|
51
59
|
}
|
|
@@ -100,12 +108,7 @@ export function useLink(props) {
|
|
|
100
108
|
return props.to.index;
|
|
101
109
|
}
|
|
102
110
|
if (isString(props.to)) {
|
|
103
|
-
|
|
104
|
-
return props.to;
|
|
105
|
-
}
|
|
106
|
-
if (props.to.startsWith('/')) {
|
|
107
|
-
return props.to;
|
|
108
|
-
}
|
|
111
|
+
return props.to;
|
|
109
112
|
}
|
|
110
113
|
return 'javascript:void(0)';
|
|
111
114
|
});
|
|
@@ -62,7 +62,7 @@ function buildRouteNode(page, extendRoute, parent) {
|
|
|
62
62
|
isGroup: page.isGroup,
|
|
63
63
|
filePath: page.filePath,
|
|
64
64
|
path: page.path,
|
|
65
|
-
fullPath: parent ?
|
|
65
|
+
fullPath: normalizeRoutePath(parent ? `${parent.fullPath}/${page.path}` : page.path)
|
|
66
66
|
};
|
|
67
67
|
// 处理组件配置
|
|
68
68
|
if (page.components) {
|
|
@@ -10,8 +10,8 @@ import { type GenerateResult } from './generator/index.js';
|
|
|
10
10
|
import { type FilterOptions } from './parser/index.js';
|
|
11
11
|
import type { FileRouterOptions, ScanNode } from './types/index.js';
|
|
12
12
|
export { resolvePageConfigs } from './config/resolve.js';
|
|
13
|
-
export { mergePageOptions } from './macros/definePage.js';
|
|
14
13
|
export * from './generator/index.js';
|
|
14
|
+
export { mergePageOptions } from './macros/definePage.js';
|
|
15
15
|
export type * from './types/index.js';
|
|
16
16
|
export * from './utils/logger.js';
|
|
17
17
|
/**
|
|
@@ -82,13 +82,71 @@ export declare class FileRouter {
|
|
|
82
82
|
private processDir;
|
|
83
83
|
/**
|
|
84
84
|
* 处理文件
|
|
85
|
+
*
|
|
86
|
+
* 根据文件类型分发到对应的处理器。
|
|
87
|
+
*
|
|
85
88
|
* @param filePath - 文件路径
|
|
86
89
|
* @param page - 页面配置
|
|
87
|
-
* @param pageMapping -
|
|
90
|
+
* @param pageMapping - 同路径路由映射
|
|
88
91
|
* @param parent - 父节点
|
|
89
92
|
* @private
|
|
90
93
|
*/
|
|
91
94
|
private processFile;
|
|
95
|
+
/**
|
|
96
|
+
* 解析文件信息与类型
|
|
97
|
+
*
|
|
98
|
+
* 统一入口,避免多处重复调用 extractFileInfo + getPageType。
|
|
99
|
+
*
|
|
100
|
+
* @param filePath - 文件路径
|
|
101
|
+
* @param page - 页面配置
|
|
102
|
+
* @returns 文件信息与类型
|
|
103
|
+
*/
|
|
104
|
+
private resolveFile;
|
|
105
|
+
/**
|
|
106
|
+
* 处理分组配置文件
|
|
107
|
+
*
|
|
108
|
+
* 解析 definePage 宏并合并到父路由选项中。
|
|
109
|
+
*
|
|
110
|
+
* @param filePath - 文件路径
|
|
111
|
+
* @param parent - 父节点
|
|
112
|
+
*/
|
|
113
|
+
private processConfigFile;
|
|
114
|
+
/**
|
|
115
|
+
* 处理分组布局文件
|
|
116
|
+
*
|
|
117
|
+
* 将布局组件注册到父路由的 components 中。
|
|
118
|
+
*
|
|
119
|
+
* @param filePath - 文件路径
|
|
120
|
+
* @param fileInfo - 文件信息
|
|
121
|
+
* @param parent - 父节点
|
|
122
|
+
*/
|
|
123
|
+
private processLayoutFile;
|
|
124
|
+
/**
|
|
125
|
+
* 处理页面文件
|
|
126
|
+
*
|
|
127
|
+
* 解析路由路径、视图命名和页面选项,创建或合并路由节点。
|
|
128
|
+
*
|
|
129
|
+
* @param filePath - 文件路径
|
|
130
|
+
* @param fileInfo - 文件信息
|
|
131
|
+
* @param page - 页面配置
|
|
132
|
+
* @param pageMapping - 同路径路由映射
|
|
133
|
+
* @param parent - 父节点
|
|
134
|
+
* @param [precomputedParsed] - 预计算的解析结果,避免重复调用 parsePageFile
|
|
135
|
+
* @returns 新创建的路由节点,或 null(合并到已有路由时)
|
|
136
|
+
*/
|
|
137
|
+
private processPageFile;
|
|
138
|
+
/**
|
|
139
|
+
* 在已有路由树中查找同路径路由
|
|
140
|
+
*
|
|
141
|
+
* 用于 addPage 场景:新增文件时需要检查是否已存在同路径的路由节点,
|
|
142
|
+
* 以便将命名视图合并到已有路由而非创建重复路由。
|
|
143
|
+
*
|
|
144
|
+
* @param pathKey - 标准化后的路由路径
|
|
145
|
+
* @param prefix - 路径前缀
|
|
146
|
+
* @param parent - 父节点
|
|
147
|
+
* @returns 同路径的路由节点,未找到返回 null
|
|
148
|
+
*/
|
|
149
|
+
private findSameRoute;
|
|
92
150
|
/**
|
|
93
151
|
* 应用路径策略
|
|
94
152
|
*
|
|
@@ -106,9 +164,9 @@ export declare class FileRouter {
|
|
|
106
164
|
* 获取文件类型
|
|
107
165
|
*
|
|
108
166
|
* @param file - 文件绝对路径
|
|
109
|
-
* @param
|
|
110
|
-
* @param pages -
|
|
111
|
-
* @returns
|
|
167
|
+
* @param rawName - 文件名(不含扩展名和 @视图命名)
|
|
168
|
+
* @param pages - 页面配置
|
|
169
|
+
* @returns 文件类型
|
|
112
170
|
*/
|
|
113
171
|
private getPageType;
|
|
114
172
|
/**
|
|
@@ -119,6 +177,16 @@ export declare class FileRouter {
|
|
|
119
177
|
* @returns {boolean} - 是否为页面文件
|
|
120
178
|
*/
|
|
121
179
|
isPageFile(file: string, filter?: FilterOptions | readonly FilterOptions[]): boolean;
|
|
180
|
+
/**
|
|
181
|
+
* 获取文件的完整路由路径
|
|
182
|
+
*
|
|
183
|
+
* 判断文件是否为页面文件,如果是则计算其最终生成的路由 fullPath。
|
|
184
|
+
* 非页面文件(布局文件、配置文件等)返回 null。
|
|
185
|
+
*
|
|
186
|
+
* @param filePath - 文件绝对路径
|
|
187
|
+
* @returns 完整路由路径,非页面文件返回 null
|
|
188
|
+
*/
|
|
189
|
+
getRouteFullPath(filePath: string): string | null;
|
|
122
190
|
/**
|
|
123
191
|
* 写入类型定义文件
|
|
124
192
|
*/
|
|
@@ -147,7 +215,10 @@ export declare class FileRouter {
|
|
|
147
215
|
/**
|
|
148
216
|
* 添加页面文件
|
|
149
217
|
*
|
|
150
|
-
*
|
|
218
|
+
* 根据文件类型直接调用对应处理器,避免重复类型判断和文件解析。
|
|
219
|
+
*
|
|
220
|
+
* @param filePath - 文件路径
|
|
221
|
+
* @returns 是否创建了新的路由节点
|
|
151
222
|
*/
|
|
152
223
|
addPage(filePath: string): boolean;
|
|
153
224
|
/**
|
|
@@ -16,11 +16,12 @@ import { resolveConfig } from './config/index.js';
|
|
|
16
16
|
import { generateRoutes } from './generator/index.js';
|
|
17
17
|
import { isEqualPageOptions, mergePageOptions, parseDefinePage, removeDefinePage } from './macros/index.js';
|
|
18
18
|
import { checkDefaultExport, isPageFile, isPageFileInDirs } from './parser/index.js';
|
|
19
|
-
import { parsePageFile } from './parser/parsePage.js';
|
|
19
|
+
import { extractFileInfo, parsePageFile } from './parser/parsePage.js';
|
|
20
|
+
import { computeRouteFullPath } from './parser/routePath.js';
|
|
20
21
|
import { applyPathStrategy, info, normalizePathSeparator, readFileContent, resolvePathVariable, validateOptions, warn } from './utils/index.js';
|
|
21
22
|
export { resolvePageConfigs } from './config/resolve.js';
|
|
22
|
-
export { mergePageOptions } from './macros/definePage.js';
|
|
23
23
|
export * from './generator/index.js';
|
|
24
|
+
export { mergePageOptions } from './macros/definePage.js';
|
|
24
25
|
export * from './utils/logger.js';
|
|
25
26
|
/**
|
|
26
27
|
* 文件路由管理器
|
|
@@ -189,66 +190,112 @@ export class FileRouter {
|
|
|
189
190
|
}
|
|
190
191
|
/**
|
|
191
192
|
* 处理文件
|
|
193
|
+
*
|
|
194
|
+
* 根据文件类型分发到对应的处理器。
|
|
195
|
+
*
|
|
192
196
|
* @param filePath - 文件路径
|
|
193
197
|
* @param page - 页面配置
|
|
194
|
-
* @param pageMapping -
|
|
198
|
+
* @param pageMapping - 同路径路由映射
|
|
195
199
|
* @param parent - 父节点
|
|
196
200
|
* @private
|
|
197
201
|
*/
|
|
198
202
|
processFile(filePath, page, pageMapping, parent) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (fileType === 'ignore')
|
|
203
|
-
return null;
|
|
204
|
-
// 处理分组配置文件
|
|
205
|
-
if (fileType === 'config') {
|
|
206
|
-
if (!parent)
|
|
203
|
+
const { fileInfo, fileType } = this.resolveFile(filePath, page);
|
|
204
|
+
switch (fileType) {
|
|
205
|
+
case 'ignore':
|
|
207
206
|
return null;
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (pageOptions) {
|
|
211
|
-
parent.options = mergePageOptions(parent.options, pageOptions);
|
|
212
|
-
parent.dirConfigFile = filePath;
|
|
213
|
-
this.fileMap.set(filePath, parent);
|
|
214
|
-
}
|
|
215
|
-
return null;
|
|
216
|
-
}
|
|
217
|
-
// 处理分组布局文件
|
|
218
|
-
if (fileType === 'layout') {
|
|
219
|
-
if (!parent)
|
|
207
|
+
case 'config':
|
|
208
|
+
this.processConfigFile(filePath, parent);
|
|
220
209
|
return null;
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
210
|
+
case 'layout':
|
|
211
|
+
this.processLayoutFile(filePath, fileInfo, parent);
|
|
212
|
+
return null;
|
|
213
|
+
case 'page':
|
|
214
|
+
return this.processPageFile(filePath, fileInfo, page, pageMapping, parent);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* 解析文件信息与类型
|
|
219
|
+
*
|
|
220
|
+
* 统一入口,避免多处重复调用 extractFileInfo + getPageType。
|
|
221
|
+
*
|
|
222
|
+
* @param filePath - 文件路径
|
|
223
|
+
* @param page - 页面配置
|
|
224
|
+
* @returns 文件信息与类型
|
|
225
|
+
*/
|
|
226
|
+
resolveFile(filePath, page) {
|
|
227
|
+
const fileInfo = extractFileInfo(filePath);
|
|
228
|
+
const fileType = this.getPageType(filePath, fileInfo.rawName, page);
|
|
229
|
+
return { fileInfo, fileType };
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* 处理分组配置文件
|
|
233
|
+
*
|
|
234
|
+
* 解析 definePage 宏并合并到父路由选项中。
|
|
235
|
+
*
|
|
236
|
+
* @param filePath - 文件路径
|
|
237
|
+
* @param parent - 父节点
|
|
238
|
+
*/
|
|
239
|
+
processConfigFile(filePath, parent) {
|
|
240
|
+
if (!parent)
|
|
241
|
+
return;
|
|
242
|
+
const content = this.readFile(filePath);
|
|
243
|
+
const pageOptions = parseDefinePage(content, filePath);
|
|
244
|
+
if (pageOptions) {
|
|
245
|
+
parent.options = mergePageOptions(parent.options, pageOptions);
|
|
246
|
+
parent.dirConfigFile = filePath;
|
|
226
247
|
this.fileMap.set(filePath, parent);
|
|
227
|
-
return null;
|
|
228
248
|
}
|
|
229
|
-
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* 处理分组布局文件
|
|
252
|
+
*
|
|
253
|
+
* 将布局组件注册到父路由的 components 中。
|
|
254
|
+
*
|
|
255
|
+
* @param filePath - 文件路径
|
|
256
|
+
* @param fileInfo - 文件信息
|
|
257
|
+
* @param parent - 父节点
|
|
258
|
+
*/
|
|
259
|
+
processLayoutFile(filePath, fileInfo, parent) {
|
|
260
|
+
if (!parent)
|
|
261
|
+
return;
|
|
230
262
|
const content = this.readFile(filePath);
|
|
231
|
-
|
|
232
|
-
|
|
263
|
+
if (checkDefaultExport(content, filePath)) {
|
|
264
|
+
parent.components ?? (parent.components = {});
|
|
265
|
+
parent.components[fileInfo.viewName ?? 'default'] = filePath;
|
|
266
|
+
}
|
|
267
|
+
this.fileMap.set(filePath, parent);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* 处理页面文件
|
|
271
|
+
*
|
|
272
|
+
* 解析路由路径、视图命名和页面选项,创建或合并路由节点。
|
|
273
|
+
*
|
|
274
|
+
* @param filePath - 文件路径
|
|
275
|
+
* @param fileInfo - 文件信息
|
|
276
|
+
* @param page - 页面配置
|
|
277
|
+
* @param pageMapping - 同路径路由映射
|
|
278
|
+
* @param parent - 父节点
|
|
279
|
+
* @param [precomputedParsed] - 预计算的解析结果,避免重复调用 parsePageFile
|
|
280
|
+
* @returns 新创建的路由节点,或 null(合并到已有路由时)
|
|
281
|
+
*/
|
|
282
|
+
processPageFile(filePath, fileInfo, page, pageMapping, parent, precomputedParsed) {
|
|
283
|
+
const parsed = precomputedParsed ?? parsePageFile(filePath, this.config.pageParser, fileInfo);
|
|
284
|
+
const viewName = parsed.viewName ?? 'default';
|
|
285
|
+
const content = this.readFile(filePath);
|
|
286
|
+
const sameRoute = pageMapping.get(parsed.path);
|
|
233
287
|
if (sameRoute) {
|
|
234
288
|
if (!checkDefaultExport(content, filePath))
|
|
235
289
|
return null;
|
|
236
|
-
// 添加命名组件
|
|
237
290
|
sameRoute.components[viewName] = filePath;
|
|
238
|
-
// 更新文件映射表
|
|
239
291
|
this.fileMap.set(filePath, sameRoute);
|
|
240
292
|
return null;
|
|
241
293
|
}
|
|
242
|
-
// 解析页面选项
|
|
243
294
|
const definePageOptions = parseDefinePage(content, filePath);
|
|
244
|
-
|
|
245
|
-
const pageOptions = mergePageOptions(options, definePageOptions);
|
|
246
|
-
// 忽略不具备默认导出,且无重定向配置的文件
|
|
295
|
+
const pageOptions = mergePageOptions(parsed.options, definePageOptions);
|
|
247
296
|
if (!pageOptions.redirect && !checkDefaultExport(content, filePath))
|
|
248
297
|
return null;
|
|
249
|
-
|
|
250
|
-
const finalPath = this.applyPathStrategy((parent ? '' : page.prefix) + (path === 'index' ? '' : path));
|
|
251
|
-
// 创建路由对象
|
|
298
|
+
const finalPath = this.applyPathStrategy((parent ? '' : page.prefix) + (parsed.path === 'index' ? '' : parsed.path));
|
|
252
299
|
const route = {
|
|
253
300
|
isGroup: false,
|
|
254
301
|
parent,
|
|
@@ -258,13 +305,31 @@ export class FileRouter {
|
|
|
258
305
|
[viewName]: filePath
|
|
259
306
|
}
|
|
260
307
|
};
|
|
261
|
-
// 添加页面选项
|
|
262
308
|
if (Object.keys(pageOptions).length)
|
|
263
309
|
route.options = pageOptions;
|
|
264
|
-
|
|
265
|
-
pageMapping.set(path, route);
|
|
310
|
+
pageMapping.set(parsed.path, route);
|
|
266
311
|
return route;
|
|
267
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* 在已有路由树中查找同路径路由
|
|
315
|
+
*
|
|
316
|
+
* 用于 addPage 场景:新增文件时需要检查是否已存在同路径的路由节点,
|
|
317
|
+
* 以便将命名视图合并到已有路由而非创建重复路由。
|
|
318
|
+
*
|
|
319
|
+
* @param pathKey - 标准化后的路由路径
|
|
320
|
+
* @param prefix - 路径前缀
|
|
321
|
+
* @param parent - 父节点
|
|
322
|
+
* @returns 同路径的路由节点,未找到返回 null
|
|
323
|
+
*/
|
|
324
|
+
findSameRoute(pathKey, prefix, parent) {
|
|
325
|
+
const newRoutePath = this.applyPathStrategy(prefix + pathKey);
|
|
326
|
+
const pages = parent ? parent.children : this.nodeTree;
|
|
327
|
+
for (const route of pages) {
|
|
328
|
+
if (route.path === newRoutePath)
|
|
329
|
+
return route;
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
268
333
|
/**
|
|
269
334
|
* 应用路径策略
|
|
270
335
|
*
|
|
@@ -286,15 +351,15 @@ export class FileRouter {
|
|
|
286
351
|
* 获取文件类型
|
|
287
352
|
*
|
|
288
353
|
* @param file - 文件绝对路径
|
|
289
|
-
* @param
|
|
290
|
-
* @param pages -
|
|
291
|
-
* @returns
|
|
354
|
+
* @param rawName - 文件名(不含扩展名和 @视图命名)
|
|
355
|
+
* @param pages - 页面配置
|
|
356
|
+
* @returns 文件类型
|
|
292
357
|
*/
|
|
293
|
-
getPageType(file,
|
|
294
|
-
if (
|
|
358
|
+
getPageType(file, rawName, pages) {
|
|
359
|
+
if (rawName === this.config.layoutFileName) {
|
|
295
360
|
return 'layout';
|
|
296
361
|
}
|
|
297
|
-
if (
|
|
362
|
+
if (rawName === this.config.configFileName && (file.endsWith('.ts') || file.endsWith('.js'))) {
|
|
298
363
|
return 'config';
|
|
299
364
|
}
|
|
300
365
|
if (this.isPageFile(file, pages)) {
|
|
@@ -320,6 +385,29 @@ export class FileRouter {
|
|
|
320
385
|
}
|
|
321
386
|
return !!isPageFileInDirs(file, this.config.pages);
|
|
322
387
|
}
|
|
388
|
+
/**
|
|
389
|
+
* 获取文件的完整路由路径
|
|
390
|
+
*
|
|
391
|
+
* 判断文件是否为页面文件,如果是则计算其最终生成的路由 fullPath。
|
|
392
|
+
* 非页面文件(布局文件、配置文件等)返回 null。
|
|
393
|
+
*
|
|
394
|
+
* @param filePath - 文件绝对路径
|
|
395
|
+
* @returns 完整路由路径,非页面文件返回 null
|
|
396
|
+
*/
|
|
397
|
+
getRouteFullPath(filePath) {
|
|
398
|
+
if (!this.isPageFile(filePath))
|
|
399
|
+
return null;
|
|
400
|
+
const fileInfo = extractFileInfo(filePath);
|
|
401
|
+
const fileType = this.getPageType(filePath, fileInfo.rawName);
|
|
402
|
+
if (fileType !== 'page')
|
|
403
|
+
return null;
|
|
404
|
+
return computeRouteFullPath(filePath, fileInfo, {
|
|
405
|
+
fileMap: this.fileMap,
|
|
406
|
+
pages: this.config.pages,
|
|
407
|
+
pageParser: this.config.pageParser,
|
|
408
|
+
pathStrategy: this.config.pathStrategy
|
|
409
|
+
});
|
|
410
|
+
}
|
|
323
411
|
/**
|
|
324
412
|
* 写入类型定义文件
|
|
325
413
|
*/
|
|
@@ -376,44 +464,49 @@ export class FileRouter {
|
|
|
376
464
|
/**
|
|
377
465
|
* 添加页面文件
|
|
378
466
|
*
|
|
379
|
-
*
|
|
467
|
+
* 根据文件类型直接调用对应处理器,避免重复类型判断和文件解析。
|
|
468
|
+
*
|
|
469
|
+
* @param filePath - 文件路径
|
|
470
|
+
* @returns 是否创建了新的路由节点
|
|
380
471
|
*/
|
|
381
472
|
addPage(filePath) {
|
|
382
473
|
const page = isPageFileInDirs(filePath, this.config.pages);
|
|
383
474
|
if (!page)
|
|
384
475
|
return false;
|
|
385
|
-
// 分离出路由 path 和视图命名
|
|
386
|
-
const { path, viewName } = parsePageFile(filePath, this.config.pageParser);
|
|
387
476
|
const dirPath = nodePath.dirname(filePath);
|
|
388
477
|
const parent = this.fileMap.get(dirPath);
|
|
389
|
-
const pageMapping = new Map();
|
|
390
478
|
const prefix = parent ? '' : page.prefix;
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const pages = parent ? parent.children : this.nodeTree;
|
|
394
|
-
const newRoutePath = this.applyPathStrategy(prefix + path);
|
|
395
|
-
let sameRoute = null;
|
|
396
|
-
for (const route of pages) {
|
|
397
|
-
if (route.path === newRoutePath) {
|
|
398
|
-
sameRoute = route;
|
|
399
|
-
break;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
if (sameRoute) {
|
|
403
|
-
pageMapping.set(path, sameRoute);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
const route = this.processFile(filePath, {
|
|
479
|
+
const { fileInfo, fileType } = this.resolveFile(filePath, page);
|
|
480
|
+
const pageConfig = {
|
|
407
481
|
dir: dirPath,
|
|
408
482
|
include: page.include,
|
|
409
483
|
exclude: page.exclude,
|
|
410
484
|
prefix
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
485
|
+
};
|
|
486
|
+
switch (fileType) {
|
|
487
|
+
case 'ignore':
|
|
488
|
+
return false;
|
|
489
|
+
case 'config':
|
|
490
|
+
this.processConfigFile(filePath, parent);
|
|
491
|
+
return false;
|
|
492
|
+
case 'layout':
|
|
493
|
+
this.processLayoutFile(filePath, fileInfo, parent);
|
|
494
|
+
return false;
|
|
495
|
+
case 'page': {
|
|
496
|
+
const parsed = parsePageFile(filePath, this.config.pageParser, fileInfo);
|
|
497
|
+
const pageMapping = new Map();
|
|
498
|
+
const sameRoute = this.findSameRoute(parsed.path, prefix, parent);
|
|
499
|
+
if (sameRoute) {
|
|
500
|
+
pageMapping.set(parsed.path, sameRoute);
|
|
501
|
+
}
|
|
502
|
+
const route = this.processPageFile(filePath, fileInfo, pageConfig, pageMapping, parent, parsed);
|
|
503
|
+
if (route) {
|
|
504
|
+
this.fileMap.set(filePath, route);
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
415
509
|
}
|
|
416
|
-
return false;
|
|
417
510
|
}
|
|
418
511
|
/**
|
|
419
512
|
* 移除指定的文件或目录
|
|
@@ -20,8 +20,7 @@ export function isPageFile(file, options) {
|
|
|
20
20
|
const { dir, include, exclude } = options;
|
|
21
21
|
if (!file.startsWith(dir))
|
|
22
22
|
return false;
|
|
23
|
-
const
|
|
24
|
-
const normalizedPath = normalizePathSeparator(relativePath);
|
|
23
|
+
const normalizedPath = normalizePathSeparator(path.relative(dir, file));
|
|
25
24
|
if (include.length === 0)
|
|
26
25
|
return true;
|
|
27
26
|
return micromatch.isMatch(normalizedPath, include, { dot: true, noext: true, ignore: exclude });
|
|
@@ -29,7 +29,31 @@ export declare class PageParseError extends TypeError {
|
|
|
29
29
|
*
|
|
30
30
|
* @param filePath - 文件路径
|
|
31
31
|
* @param [parser] - 自定义解析器
|
|
32
|
-
* @
|
|
32
|
+
* @param [precomputed] - 预计算的文件信息,避免重复解析文件路径
|
|
33
|
+
* @returns 路径解析结果
|
|
33
34
|
* @throws {PageParseError} 当路径解析失败时抛出
|
|
34
35
|
*/
|
|
35
|
-
export declare function parsePageFile(filePath: string, parser?: PageParser): PageParseResult;
|
|
36
|
+
export declare function parsePageFile(filePath: string, parser?: PageParser, precomputed?: FileInfo): PageParseResult;
|
|
37
|
+
/**
|
|
38
|
+
* 文件信息
|
|
39
|
+
*
|
|
40
|
+
* 从文件路径中提取的结构化信息,作为文件名解析的唯一来源。
|
|
41
|
+
*/
|
|
42
|
+
export interface FileInfo {
|
|
43
|
+
/** 文件名(不含扩展名),如 home@sidebar → home@sidebar */
|
|
44
|
+
basename: string;
|
|
45
|
+
/** 路由名(@之前的部分),如 home@sidebar → home */
|
|
46
|
+
rawName: string;
|
|
47
|
+
/** 视图名称(@之后的部分),如 home@sidebar → sidebar */
|
|
48
|
+
viewName: string | undefined;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 提取文件信息
|
|
52
|
+
*
|
|
53
|
+
* 从文件路径中提取文件名、路由名和视图名称。
|
|
54
|
+
* 这是文件名解析的唯一入口,其他模块应复用此函数而非重复实现。
|
|
55
|
+
*
|
|
56
|
+
* @param filePath - 文件路径
|
|
57
|
+
* @returns 文件信息
|
|
58
|
+
*/
|
|
59
|
+
export declare function extractFileInfo(filePath: string): FileInfo;
|
|
@@ -68,41 +68,46 @@ export class PageParseError extends TypeError {
|
|
|
68
68
|
*
|
|
69
69
|
* @param filePath - 文件路径
|
|
70
70
|
* @param [parser] - 自定义解析器
|
|
71
|
-
* @
|
|
71
|
+
* @param [precomputed] - 预计算的文件信息,避免重复解析文件路径
|
|
72
|
+
* @returns 路径解析结果
|
|
72
73
|
* @throws {PageParseError} 当路径解析失败时抛出
|
|
73
74
|
*/
|
|
74
|
-
export function parsePageFile(filePath, parser) {
|
|
75
|
-
const
|
|
75
|
+
export function parsePageFile(filePath, parser, precomputed) {
|
|
76
|
+
const info = precomputed ?? extractFileInfo(filePath);
|
|
76
77
|
if (!parser) {
|
|
77
|
-
return defaultPageParser(
|
|
78
|
+
return defaultPageParser(info.rawName, info.viewName);
|
|
78
79
|
}
|
|
79
|
-
const result = parser(basename, filePath);
|
|
80
|
+
const result = parser(info.basename, filePath);
|
|
80
81
|
return parseCustomResult(result, filePath);
|
|
81
82
|
}
|
|
82
83
|
/**
|
|
83
84
|
* 提取文件信息
|
|
84
85
|
*
|
|
86
|
+
* 从文件路径中提取文件名、路由名和视图名称。
|
|
87
|
+
* 这是文件名解析的唯一入口,其他模块应复用此函数而非重复实现。
|
|
88
|
+
*
|
|
85
89
|
* @param filePath - 文件路径
|
|
86
|
-
* @returns
|
|
90
|
+
* @returns 文件信息
|
|
87
91
|
*/
|
|
88
|
-
function extractFileInfo(filePath) {
|
|
92
|
+
export function extractFileInfo(filePath) {
|
|
89
93
|
const ext = path.extname(filePath);
|
|
90
94
|
const basename = path.basename(filePath, ext);
|
|
91
|
-
|
|
95
|
+
const [rawName, viewName] = basename.split('@', 2);
|
|
96
|
+
return { basename, rawName, viewName };
|
|
92
97
|
}
|
|
93
98
|
/**
|
|
94
99
|
* 解析默认路由路径(无自定义解析器)
|
|
95
100
|
*
|
|
96
|
-
* @param
|
|
101
|
+
* @param rawName - 路由名(@之前的部分)
|
|
102
|
+
* @param viewName - 视图名称(@之后的部分)
|
|
97
103
|
* @returns 解析结果
|
|
98
104
|
*/
|
|
99
|
-
function defaultPageParser(
|
|
100
|
-
const
|
|
101
|
-
const routePath = normalizeRoutePath(rawPath);
|
|
105
|
+
function defaultPageParser(rawName, viewName) {
|
|
106
|
+
const routePath = normalizeRoutePath(rawName);
|
|
102
107
|
if (!routePath) {
|
|
103
108
|
throw new PageParseError('PageParser returned empty path', {
|
|
104
|
-
filePath:
|
|
105
|
-
originalValue:
|
|
109
|
+
filePath: rawName,
|
|
110
|
+
originalValue: rawName,
|
|
106
111
|
field: 'path'
|
|
107
112
|
});
|
|
108
113
|
}
|
|
@@ -121,7 +126,8 @@ function defaultPageParser(basename) {
|
|
|
121
126
|
*/
|
|
122
127
|
function parseCustomResult(result, filePath) {
|
|
123
128
|
if (typeof result === 'string') {
|
|
124
|
-
|
|
129
|
+
const [rawName, viewName] = result.split('@', 2);
|
|
130
|
+
return defaultPageParser(rawName, viewName);
|
|
125
131
|
}
|
|
126
132
|
if (result && typeof result === 'object' && !Array.isArray(result)) {
|
|
127
133
|
return parseObjectResult(result, filePath);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type PageDirConfig } from '../config/resolve.js';
|
|
2
|
+
import type { PageParser, PathStrategy, ScanNode } from '../types/index.js';
|
|
3
|
+
import { type FileInfo } from './parsePage.js';
|
|
4
|
+
interface RouteFullPathContext {
|
|
5
|
+
fileMap: Map<string, ScanNode>;
|
|
6
|
+
pages: readonly PageDirConfig[];
|
|
7
|
+
pageParser?: PageParser;
|
|
8
|
+
pathStrategy: PathStrategy;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 计算文件的完整路由路径
|
|
12
|
+
*
|
|
13
|
+
* 判断文件是否为页面文件,如果是则计算其最终生成的路由 fullPath。
|
|
14
|
+
* 非页面文件(布局文件、配置文件等)返回 null。
|
|
15
|
+
*
|
|
16
|
+
* @param filePath - 文件绝对路径
|
|
17
|
+
* @param fileInfo - 文件信息
|
|
18
|
+
* @param context - 路由计算上下文
|
|
19
|
+
* @returns 完整路由路径,非页面文件返回 null
|
|
20
|
+
*/
|
|
21
|
+
export declare function computeRouteFullPath(filePath: string, fileInfo: FileInfo, context: RouteFullPathContext): string | null;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview 路由路径计算辅助函数
|
|
3
|
+
*
|
|
4
|
+
* 从文件路径计算最终生成的路由 fullPath,
|
|
5
|
+
* 支持已跟踪(在路由树中)和未跟踪(尚未扫描)两种场景。
|
|
6
|
+
*/
|
|
7
|
+
import nodePath from 'node:path';
|
|
8
|
+
import { applyPathStrategy } from '../utils/pathStrategy.js';
|
|
9
|
+
import { normalizeRoutePath, resolvePathVariable } from '../utils/pathUtils.js';
|
|
10
|
+
import { isPageFileInDirs } from './filterUtils.js';
|
|
11
|
+
import { parsePageFile } from './parsePage.js';
|
|
12
|
+
/**
|
|
13
|
+
* 应用路径策略(命名转换 + 动态参数转换)
|
|
14
|
+
*
|
|
15
|
+
* @param path - 路径
|
|
16
|
+
* @param strategy - 路径策略
|
|
17
|
+
* @returns 转换后的路径
|
|
18
|
+
*/
|
|
19
|
+
function applyFullPathStrategy(path, strategy) {
|
|
20
|
+
return resolvePathVariable(applyPathStrategy(path, strategy));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 从节点树计算完整路由路径
|
|
24
|
+
*
|
|
25
|
+
* 沿 parent 链向上拼接所有节点的 path,与生成阶段 fullPath 的计算逻辑一致。
|
|
26
|
+
*
|
|
27
|
+
* @param node - 路由节点
|
|
28
|
+
* @returns 完整路由路径
|
|
29
|
+
*/
|
|
30
|
+
function computeNodeFullPath(node) {
|
|
31
|
+
const segments = [];
|
|
32
|
+
let current = node;
|
|
33
|
+
while (current) {
|
|
34
|
+
segments.unshift(current.path);
|
|
35
|
+
current = current.parent;
|
|
36
|
+
}
|
|
37
|
+
return normalizeRoutePath(segments.join('/'));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 计算文件的完整路由路径
|
|
41
|
+
*
|
|
42
|
+
* 判断文件是否为页面文件,如果是则计算其最终生成的路由 fullPath。
|
|
43
|
+
* 非页面文件(布局文件、配置文件等)返回 null。
|
|
44
|
+
*
|
|
45
|
+
* @param filePath - 文件绝对路径
|
|
46
|
+
* @param fileInfo - 文件信息
|
|
47
|
+
* @param context - 路由计算上下文
|
|
48
|
+
* @returns 完整路由路径,非页面文件返回 null
|
|
49
|
+
*/
|
|
50
|
+
export function computeRouteFullPath(filePath, fileInfo, context) {
|
|
51
|
+
const { fileMap, pages, pageParser, pathStrategy } = context;
|
|
52
|
+
const node = fileMap.get(filePath);
|
|
53
|
+
if (node) {
|
|
54
|
+
return computeNodeFullPath(node);
|
|
55
|
+
}
|
|
56
|
+
const page = isPageFileInDirs(filePath, pages);
|
|
57
|
+
if (!page)
|
|
58
|
+
return null;
|
|
59
|
+
const parsed = parsePageFile(filePath, pageParser, fileInfo);
|
|
60
|
+
const pathSegment = parsed.path === 'index' ? '' : applyFullPathStrategy(parsed.path, pathStrategy);
|
|
61
|
+
const segments = pathSegment ? [pathSegment] : [];
|
|
62
|
+
let dirPath = nodePath.dirname(filePath);
|
|
63
|
+
while (dirPath.length > page.dir.length) {
|
|
64
|
+
const dirNode = fileMap.get(dirPath);
|
|
65
|
+
if (dirNode) {
|
|
66
|
+
const parentFullPath = computeNodeFullPath(dirNode);
|
|
67
|
+
return normalizeRoutePath(parentFullPath + '/' + segments.join('/'));
|
|
68
|
+
}
|
|
69
|
+
segments.unshift(applyFullPathStrategy(nodePath.basename(dirPath), pathStrategy));
|
|
70
|
+
dirPath = nodePath.dirname(dirPath);
|
|
71
|
+
}
|
|
72
|
+
const prefix = page.prefix ? applyFullPathStrategy(page.prefix, pathStrategy) : '';
|
|
73
|
+
return normalizeRoutePath(prefix + '/' + segments.join('/'));
|
|
74
|
+
}
|
|
@@ -54,7 +54,7 @@ export type PageSource = string | PageDirOptions;
|
|
|
54
54
|
/**
|
|
55
55
|
* 路径解析结果
|
|
56
56
|
*/
|
|
57
|
-
export
|
|
57
|
+
export interface PageParseResult {
|
|
58
58
|
/** 解析后的路径 如:home.jsx -> 'home' */
|
|
59
59
|
path: string;
|
|
60
60
|
/**
|
|
@@ -63,7 +63,7 @@ export type PageParseResult = {
|
|
|
63
63
|
options?: PageOptions;
|
|
64
64
|
/** 视图名称 如:home.nav.jsx -> 'nav' */
|
|
65
65
|
viewName?: string;
|
|
66
|
-
}
|
|
66
|
+
}
|
|
67
67
|
/**
|
|
68
68
|
* 路径解析器
|
|
69
69
|
*
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 查找路由
|
|
3
|
+
*
|
|
4
|
+
* @param routes - 路由列表
|
|
5
|
+
* @param path - 路径
|
|
6
|
+
*/
|
|
7
|
+
export function findRoute(routes, path) {
|
|
8
|
+
for (const route of routes) {
|
|
9
|
+
if (!route.isGroup && route.fullPath === path)
|
|
10
|
+
return route;
|
|
11
|
+
if (route.isGroup && route.children) {
|
|
12
|
+
for (const child of route.children) {
|
|
13
|
+
if (child.fullPath === path)
|
|
14
|
+
return route;
|
|
15
|
+
if (child.isGroup && child.children) {
|
|
16
|
+
return findRoute(child.children, path);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vitarx-router",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.18",
|
|
4
4
|
"description": "Official routing solution for Vitarx framework with declarative routing, navigation guards, dynamic routes, file-based routing with HMR, and full TypeScript support.",
|
|
5
5
|
"author": "ZhuChonglin <8210856@qq.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -76,13 +76,14 @@
|
|
|
76
76
|
"README.md"
|
|
77
77
|
],
|
|
78
78
|
"scripts": {
|
|
79
|
-
"build": "rimraf dist && vitest run && tsc && pnpm check:circular",
|
|
79
|
+
"build": "rimraf dist && vitest run && tsc -p tsconfig.build.json && pnpm check:circular",
|
|
80
|
+
"typecheck": "tsc -p tsconfig.build.json --noEmit",
|
|
80
81
|
"check:circular": "madge --extensions js --circular dist --warning --exclude '.*\\.d\\.ts$'",
|
|
81
82
|
"test": "vitest run",
|
|
82
83
|
"test:core": "vitest run --project jsdom",
|
|
83
84
|
"test:plugin-vite": "vitest run --project plugin-vite",
|
|
84
85
|
"test:watch": "vitest watch",
|
|
85
86
|
"test:coverage": "vitest run --coverage",
|
|
86
|
-
"release": "
|
|
87
|
+
"release": "pnpm dlx @vitarx/release-cli"
|
|
87
88
|
}
|
|
88
89
|
}
|