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 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 ?? false;
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) {
@@ -70,6 +70,11 @@ export declare abstract class Router {
70
70
  * @private
71
71
  */
72
72
  private readonly _routeLocation;
73
+ /**
74
+ * 只读路由位置对象
75
+ * @private
76
+ */
77
+ private readonly _readonlyLocation;
73
78
  /**
74
79
  * 存储就绪状态的 Promise(延迟创建)
75
80
  * @private
@@ -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 readonly(this._routeLocation);
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
  *
@@ -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 (httpRegex.test(to))
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
- if (httpRegex.test(props.to)) {
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
  });
@@ -48,6 +48,8 @@ export interface RouterOptions {
48
48
  * 全局 props 注入配置
49
49
  *
50
50
  * 优先级低于 route 的 props 配置
51
+ *
52
+ * @default true
51
53
  */
52
54
  props?: boolean | InjectPropsHandler;
53
55
  /**
@@ -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 ? normalizeRoutePath(parent.fullPath + '/' + page.path) : page.path
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 name - 文件名
110
- * @param pages - 页面配置,默认为 `config.pages`
111
- * @returns {string} - 文件类型,可选值有 `layout`、`config`、`page`、`ignore`
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
- * @param filePath
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
- // 分离出路由 path 和视图命名
200
- const { path, viewName = 'default', options } = parsePageFile(filePath, this.config.pageParser);
201
- const fileType = this.getPageType(filePath, path, page);
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
- const content = this.readFile(filePath);
209
- const pageOptions = parseDefinePage(content, filePath);
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
- const content = this.readFile(filePath);
222
- if (checkDefaultExport(content, filePath)) {
223
- parent.components ?? (parent.components = {});
224
- parent.components[viewName] = filePath;
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
- const sameRoute = pageMapping.get(path);
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
- // 最终 path
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 name - 文件名
290
- * @param pages - 页面配置,默认为 `config.pages`
291
- * @returns {string} - 文件类型,可选值有 `layout`、`config`、`page`、`ignore`
354
+ * @param rawName - 文件名(不含扩展名和 @视图命名)
355
+ * @param pages - 页面配置
356
+ * @returns 文件类型
292
357
  */
293
- getPageType(file, name, pages) {
294
- if (name === this.config.layoutFileName) {
358
+ getPageType(file, rawName, pages) {
359
+ if (rawName === this.config.layoutFileName) {
295
360
  return 'layout';
296
361
  }
297
- if (name === this.config.configFileName && (file.endsWith('.ts') || file.endsWith('.js'))) {
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
- * @param filePath
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
- // 如果是命名文件,则先查找是否存在同名路由,存在则添加到同名路由的 children
392
- if (viewName) {
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
- }, pageMapping, parent);
412
- if (route) {
413
- this.fileMap.set(filePath, route);
414
- return true;
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 relativePath = path.relative(dir, file);
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
- * @returns 路由路径和视图名称
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
- * @returns 路由路径和视图名称
71
+ * @param [precomputed] - 预计算的文件信息,避免重复解析文件路径
72
+ * @returns 路径解析结果
72
73
  * @throws {PageParseError} 当路径解析失败时抛出
73
74
  */
74
- export function parsePageFile(filePath, parser) {
75
- const { basename } = extractFileInfo(filePath);
75
+ export function parsePageFile(filePath, parser, precomputed) {
76
+ const info = precomputed ?? extractFileInfo(filePath);
76
77
  if (!parser) {
77
- return defaultPageParser(basename);
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
- return { basename, ext };
95
+ const [rawName, viewName] = basename.split('@', 2);
96
+ return { basename, rawName, viewName };
92
97
  }
93
98
  /**
94
99
  * 解析默认路由路径(无自定义解析器)
95
100
  *
96
- * @param basename - 文件基本名称
101
+ * @param rawName - 路由名(@之前的部分)
102
+ * @param viewName - 视图名称(@之后的部分)
97
103
  * @returns 解析结果
98
104
  */
99
- function defaultPageParser(basename) {
100
- const [rawPath, viewName] = basename.split('@', 2);
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: basename,
105
- originalValue: rawPath,
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
- return defaultPageParser(result);
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 type PageParseResult = {
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,8 @@
1
+ import type { RouteNode } from '../types/index.js';
2
+ /**
3
+ * 查找路由
4
+ *
5
+ * @param routes - 路由列表
6
+ * @param path - 路径
7
+ */
8
+ export declare function findRoute(routes: RouteNode[], path: string): RouteNode | null;
@@ -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
+ }
@@ -9,3 +9,4 @@ export * from './logger.js';
9
9
  export * from './pathStrategy.js';
10
10
  export * from '../config/validate.js';
11
11
  export * from './fileReader.js';
12
+ export * from './findRoute.js';
@@ -9,3 +9,4 @@ export * from './logger.js';
9
9
  export * from './pathStrategy.js';
10
10
  export * from '../config/validate.js';
11
11
  export * from './fileReader.js';
12
+ export * from './findRoute.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vitarx-router",
3
- "version": "4.0.0-beta.16",
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": "npx release-cli"
87
+ "release": "pnpm dlx @vitarx/release-cli"
87
88
  }
88
89
  }