vitarx-router 4.0.0-beta.3 → 4.0.0-beta.5

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,17 +423,18 @@ 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'` | `'lazy'` | 组件导入模式 |
431
- | `injectImports` | `readonly string[]` | - | 自定义导入语句 |
432
- | `dts` | `boolean \| string` | `false` | 类型声明文件配置 |
433
- | `layoutFileName` | `string` | `'_layout'` | 布局文件名 |
434
- | `configFileName` | `string` | `'_config'` | 分组配置文件名 |
435
- | `transform` | `CodeTransformHook` | - | 代码转换钩子 |
436
- | `extendRoute` | `ExtendRouteHook` | - | 路由扩展钩子 |
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` | - | 自定义路径解析器 |
437
438
 
438
439
  详细配置请参考 [File Router 文档](src/file-router/README.md)。
439
440
 
@@ -20,8 +20,7 @@ export function useRoute(global = false) {
20
20
  return route;
21
21
  }
22
22
  const record = route.matched[depth];
23
- if (!record) {
23
+ if (!record)
24
24
  return route;
25
- }
26
25
  return createRouteProxy(route, record);
27
26
  }
@@ -1,4 +1,4 @@
1
- import type { CodeTransformHook, ExtendRouteHook, FileRouterOptions, ImportMode, PageDirOptions, PathStrategy } from '../types/index.js';
1
+ import type { CodeTransformHook, ExtendRouteHook, FileRouterOptions, ImportMode, PageDirOptions, PageSource, PathParser, PathStrategy } from '../types/index.js';
2
2
  export type PageDirConfig = Required<PageDirOptions>;
3
3
  /**
4
4
  * 规范化后的配置
@@ -14,7 +14,22 @@ export interface ResolvedConfig {
14
14
  configFileName: string;
15
15
  transform?: CodeTransformHook;
16
16
  extendRoute?: ExtendRouteHook;
17
+ pathParser?: PathParser;
17
18
  }
19
+ /**
20
+ * 将 pages 配置规范化为 PageConfig 数组
21
+ *
22
+ * 支持四种输入格式:
23
+ * 1. 字符串:单个目录路径
24
+ * 2. 对象:单个目录配置
25
+ * 3. 字符串数组:多个目录路径
26
+ * 4. 对象数组:每个目录独立配置
27
+ *
28
+ * @param pages - 用户配置的 pages
29
+ * @param root - 项目根目录路径(用于解析相对路径)
30
+ * @returns - 规范化后的目录配置数组
31
+ */
32
+ export declare function resolvePageConfigs(pages: PageSource | readonly PageSource[], root: string): PageDirConfig[];
18
33
  /**
19
34
  * 规范化文件路由配置
20
35
  *
@@ -4,7 +4,6 @@
4
4
  * 提供页面目录配置的处理和文件检查功能。
5
5
  * 与构建工具无关,可在任何 Node.js 环境中使用。
6
6
  */
7
- import { accessSync, constants } from 'node:fs';
8
7
  import path from 'node:path';
9
8
  import { DEFAULT_CONFIG_FILE, DEFAULT_DTS_FILE, DEFAULT_EXCLUDE, DEFAULT_INCLUDE, DEFAULT_LAYOUT_FILE, DEFAULT_PAGES_DIR } from '../constants.js';
10
9
  const DEFAULT_PAGE_CONFIG = {
@@ -26,7 +25,7 @@ const DEFAULT_PAGE_CONFIG = {
26
25
  * @param root - 项目根目录路径(用于解析相对路径)
27
26
  * @returns - 规范化后的目录配置数组
28
27
  */
29
- function resolvePageConfigs(pages, root) {
28
+ export function resolvePageConfigs(pages, root) {
30
29
  const list = Array.isArray(pages) ? pages : [pages];
31
30
  return list.map(page => {
32
31
  const config = typeof page === 'string' ? { dir: page } : page;
@@ -38,12 +37,6 @@ function resolvePageConfigs(pages, root) {
38
37
  if (resolved.prefix !== '/' && !resolved.prefix.startsWith('/')) {
39
38
  resolved.prefix = '/' + resolved.prefix;
40
39
  }
41
- try {
42
- accessSync(resolved.dir, constants.R_OK | constants.W_OK);
43
- }
44
- catch (error) {
45
- throw new Error(`File router: Pages directory "${resolved.dir}" does not exist or is not accessible`);
46
- }
47
40
  return resolved;
48
41
  });
49
42
  }
@@ -56,7 +49,7 @@ function resolvePageConfigs(pages, root) {
56
49
  * @returns - 规范化后的配置对象
57
50
  */
58
51
  export function resolveConfig(options) {
59
- const { dts = false, root = process.cwd(), pages = DEFAULT_PAGES_DIR, importMode = 'lazy', injectImports = [], pathStrategy = 'kebab', layoutFileName = DEFAULT_LAYOUT_FILE, configFileName = DEFAULT_CONFIG_FILE, transform, extendRoute } = options;
52
+ const { dts = false, root = process.cwd(), pages = DEFAULT_PAGES_DIR, importMode = 'lazy', injectImports = [], pathStrategy = 'kebab', layoutFileName = DEFAULT_LAYOUT_FILE, configFileName = DEFAULT_CONFIG_FILE, transform, extendRoute, pathParser } = options;
60
53
  const resolvedPages = resolvePageConfigs(pages, root);
61
54
  return {
62
55
  dts: typeof dts === 'string' ? dts : dts ? DEFAULT_DTS_FILE : false,
@@ -68,6 +61,7 @@ export function resolveConfig(options) {
68
61
  layoutFileName,
69
62
  configFileName,
70
63
  transform,
71
- extendRoute
64
+ extendRoute,
65
+ pathParser
72
66
  };
73
67
  }
@@ -19,6 +19,7 @@ import type { FileRouterOptions } from '../types/index.js';
19
19
  * 8. configFileName 配置
20
20
  * 9. transform 配置
21
21
  * 10. extendRoute 配置
22
+ * 11. pathParser 配置
22
23
  *
23
24
  * @param opts - 用户提供的配置选项
24
25
  * @throws {Error} 当配置无效时抛出错误
@@ -84,9 +84,11 @@ function validateRoot(opts) {
84
84
  function validateImportMode(opts) {
85
85
  if (opts.importMode === undefined)
86
86
  return;
87
+ if (typeof opts.importMode === 'function')
88
+ return;
87
89
  const validModes = ['lazy', 'sync'];
88
90
  if (!validModes.includes(opts.importMode)) {
89
- throw new Error(`options.importMode 必须是 'lazy''sync'`);
91
+ throw new Error(`options.importMode 必须是 'lazy''sync' 或函数`);
90
92
  }
91
93
  }
92
94
  /**
@@ -198,6 +200,19 @@ function validateExtendRoute(opts) {
198
200
  throw new Error('options.extendRoute 必须是函数');
199
201
  }
200
202
  }
203
+ /**
204
+ * 验证 pathParser 配置
205
+ *
206
+ * @param opts - 配置选项
207
+ * @throws {Error} 当配置无效时抛出错误
208
+ */
209
+ function validatePathParser(opts) {
210
+ if (opts.pathParser === undefined)
211
+ return;
212
+ if (typeof opts.pathParser !== 'function') {
213
+ throw new Error('options.pathParser 必须是函数');
214
+ }
215
+ }
201
216
  /**
202
217
  * 验证插件配置选项
203
218
  *
@@ -212,6 +227,7 @@ function validateExtendRoute(opts) {
212
227
  * 8. configFileName 配置
213
228
  * 9. transform 配置
214
229
  * 10. extendRoute 配置
230
+ * 11. pathParser 配置
215
231
  *
216
232
  * @param opts - 用户提供的配置选项
217
233
  * @throws {Error} 当配置无效时抛出错误
@@ -227,4 +243,5 @@ export function validateOptions(opts) {
227
243
  validateConfigFileName(opts);
228
244
  validateTransform(opts);
229
245
  validateExtendRoute(opts);
246
+ validatePathParser(opts);
230
247
  }
@@ -115,7 +115,6 @@ function buildRoutes(pages, extendRoute, parent) {
115
115
  * @returns 格式化后的组件表达式代码
116
116
  */
117
117
  function formatComponent(component, importMode, importLines) {
118
- // 处理命名视图
119
118
  const entries = Object.entries(component).map(([name, file]) => {
120
119
  const importPath = JSON.stringify(file);
121
120
  let expr;
@@ -123,10 +122,17 @@ function formatComponent(component, importMode, importLines) {
123
122
  expr = pathToUniqueName(file);
124
123
  importLines.add(`import ${expr} from ${importPath}`);
125
124
  }
126
- else {
125
+ else if (importMode === 'lazy') {
127
126
  expr = `lazy(() => import(${importPath}))`;
128
127
  }
129
- return `${name}: ${expr}`;
128
+ else {
129
+ expr = importMode({
130
+ importPath,
131
+ filePath: file,
132
+ addImport: statement => importLines.add(statement)
133
+ });
134
+ }
135
+ return `${JSON.stringify(name)}: ${expr}`;
130
136
  });
131
137
  return `{ ${entries.join(', ')} }`;
132
138
  }
@@ -215,26 +221,23 @@ function generateRouteCode(route, indent, isLast, importMode, importLines) {
215
221
  function generateRoutesCode(routes, importMode = 'lazy', customImports, indent = ' ') {
216
222
  const importLines = new Set();
217
223
  const codeLines = [];
218
- // 添加 lazy 导入(如果需要)
219
224
  if (importMode === 'lazy') {
220
225
  importLines.add(`import { lazy } from 'vitarx'`);
221
226
  }
222
- // 添加自定义导入语句
223
227
  if (customImports && customImports.length > 0) {
224
228
  customImports.forEach(imp => importLines.add(imp));
225
229
  }
226
- // 添加空行
227
- if (importMode === 'lazy' || (customImports && customImports.length > 0)) {
228
- codeLines.push('');
229
- }
230
- // 生成路由数组
231
230
  codeLines.push('export default [');
232
231
  for (let i = 0; i < routes.length; i++) {
233
232
  const route = routes[i];
234
233
  codeLines.push(...generateRouteCode(route, indent, i === routes.length - 1, importMode, importLines));
235
234
  }
236
235
  codeLines.push(']');
237
- return Array.from(importLines.values()).concat(codeLines).join('\n');
236
+ const allImports = Array.from(importLines.values());
237
+ if (allImports.length > 0) {
238
+ allImports.push('');
239
+ }
240
+ return allImports.concat(codeLines).join('\n');
238
241
  }
239
242
  /**
240
243
  * 生成路由配置代码
@@ -7,9 +7,11 @@
7
7
  import type { GeneratorResult } from '@babel/generator';
8
8
  import { ResolvedConfig } from './config/index.js';
9
9
  import { type GenerateResult } from './generator/index.js';
10
+ import { type FilterOptions } from './parser/index.js';
10
11
  import type { FileRouterOptions, ParsedNode } from './types/index.js';
11
12
  export type * from './types/index.js';
12
13
  export * from './utils/logger.js';
14
+ export { resolvePageConfigs } from './config/resolve.js';
13
15
  /**
14
16
  * 文件路由管理器
15
17
  */
@@ -66,7 +68,6 @@ export declare class FileRouter {
66
68
  /**
67
69
  * 处理文件
68
70
  * @param filePath - 文件路径
69
- * @param fileName - 文件名
70
71
  * @param page - 页面配置
71
72
  * @param pageMapping - 子路由
72
73
  * @param parent - 父节点
@@ -91,7 +92,6 @@ export declare class FileRouter {
91
92
  *
92
93
  * @param file - 文件绝对路径
93
94
  * @param name - 文件名
94
- * @param ext - 文件扩展名
95
95
  * @param pages - 页面配置,默认为 `config.pages`
96
96
  * @returns {string} - 文件类型,可选值有 `layout`、`config`、`page`、`ignore`
97
97
  */
@@ -100,10 +100,10 @@ export declare class FileRouter {
100
100
  * 检查文件是否为页面文件
101
101
  *
102
102
  * @param file - 文件绝对路径
103
- * @param pages - 页面配置,默认为 `config.pages`
103
+ * @param filter - 过滤配置,默认为 `config.pages`
104
104
  * @returns {boolean} - 是否为页面文件
105
105
  */
106
- private isPageFile;
106
+ isPageFile(file: string, filter?: FilterOptions | readonly FilterOptions[]): boolean;
107
107
  /**
108
108
  * 写入类型定义文件
109
109
  */
@@ -17,8 +17,9 @@ 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
19
  import { parseRoutePath } from './parser/parsePage.js';
20
- import { applyPathStrategy, info, normalizePathSeparator, readFileContent, resolvePathVariable, validateOptions } from './utils/index.js';
20
+ import { applyPathStrategy, info, normalizePathSeparator, readFileContent, resolvePathVariable, validateOptions, warn } from './utils/index.js';
21
21
  export * from './utils/logger.js';
22
+ export { resolvePageConfigs } from './config/resolve.js';
22
23
  /**
23
24
  * 文件路由管理器
24
25
  */
@@ -98,6 +99,10 @@ export class FileRouter {
98
99
  scanPages() {
99
100
  const pages = [];
100
101
  for (const page of this.config.pages) {
102
+ if (!existsSync(page.dir)) {
103
+ warn(`Directory ${page.dir} does not exist, please check your configuration.`);
104
+ continue;
105
+ }
101
106
  if (page.group && page.prefix) {
102
107
  const route = {
103
108
  filePath: page.dir,
@@ -137,7 +142,7 @@ export class FileRouter {
137
142
  }
138
143
  else {
139
144
  // 处理文件
140
- route = this.processFile(filePath, dirent.name, page, pageMapping, parent);
145
+ route = this.processFile(filePath, page, pageMapping, parent);
141
146
  }
142
147
  if (route) {
143
148
  children.add(route);
@@ -169,18 +174,15 @@ export class FileRouter {
169
174
  /**
170
175
  * 处理文件
171
176
  * @param filePath - 文件路径
172
- * @param fileName - 文件名
173
177
  * @param page - 页面配置
174
178
  * @param pageMapping - 子路由
175
179
  * @param parent - 父节点
176
180
  * @private
177
181
  */
178
- processFile(filePath, fileName, page, pageMapping, parent) {
179
- // 获取扩展名称
180
- const ext = nodePath.extname(fileName);
181
- // 获取文件名
182
- const baseName = nodePath.basename(fileName, ext);
183
- const fileType = this.getPageType(filePath, baseName, ext, page);
182
+ processFile(filePath, page, pageMapping, parent) {
183
+ // 分离出路由 path 和视图命名
184
+ const { routePath, viewName = 'default' } = parseRoutePath(filePath, this.config.pathParser);
185
+ const fileType = this.getPageType(filePath, routePath, page);
184
186
  if (fileType === 'ignore')
185
187
  return null;
186
188
  // 处理分组配置文件
@@ -201,8 +203,6 @@ export class FileRouter {
201
203
  }
202
204
  return null;
203
205
  }
204
- // 分离出路由 path 和视图命名
205
- const { routePath, viewName } = parseRoutePath(baseName);
206
206
  // 处理分组布局文件
207
207
  if (fileType === 'layout') {
208
208
  if (!parent)
@@ -273,15 +273,14 @@ export class FileRouter {
273
273
  *
274
274
  * @param file - 文件绝对路径
275
275
  * @param name - 文件名
276
- * @param ext - 文件扩展名
277
276
  * @param pages - 页面配置,默认为 `config.pages`
278
277
  * @returns {string} - 文件类型,可选值有 `layout`、`config`、`page`、`ignore`
279
278
  */
280
- getPageType(file, name, ext, pages) {
281
- if (name === this.config.layoutFileName || name.startsWith(`${this.config.configFileName}@`)) {
279
+ getPageType(file, name, pages) {
280
+ if (name === this.config.layoutFileName) {
282
281
  return 'layout';
283
282
  }
284
- if (name === this.config.configFileName && (ext === '.ts' || ext === '.js')) {
283
+ if (name === this.config.configFileName && (file.endsWith('.ts') || file.endsWith('.js'))) {
285
284
  return 'config';
286
285
  }
287
286
  if (this.isPageFile(file, pages)) {
@@ -293,16 +292,16 @@ export class FileRouter {
293
292
  * 检查文件是否为页面文件
294
293
  *
295
294
  * @param file - 文件绝对路径
296
- * @param pages - 页面配置,默认为 `config.pages`
295
+ * @param filter - 过滤配置,默认为 `config.pages`
297
296
  * @returns {boolean} - 是否为页面文件
298
297
  */
299
- isPageFile(file, pages) {
300
- if (pages) {
301
- if (Array.isArray(pages)) {
302
- return !!isPageFileInDirs(file, pages);
298
+ isPageFile(file, filter) {
299
+ if (filter) {
300
+ if (Array.isArray(filter)) {
301
+ return !!isPageFileInDirs(file, filter);
303
302
  }
304
303
  else {
305
- return isPageFile(file, pages);
304
+ return isPageFile(file, filter);
306
305
  }
307
306
  }
308
307
  return !!isPageFileInDirs(file, this.config.pages);
@@ -376,16 +375,16 @@ export class FileRouter {
376
375
  const page = isPageFileInDirs(filePath, this.config.pages);
377
376
  if (!page)
378
377
  return false;
378
+ // 分离出路由 path 和视图命名
379
+ const { routePath, viewName } = parseRoutePath(filePath, this.config.pathParser);
379
380
  const dirPath = nodePath.dirname(filePath);
380
- const filename = nodePath.basename(filePath);
381
381
  const parent = this.fileMap.get(dirPath);
382
382
  const pageMapping = new Map();
383
383
  const prefix = parent ? '' : page.prefix;
384
384
  // 如果是命名文件,则先查找是否存在同名路由,存在则添加到同名路由的 children 中
385
- if (filename.includes('@')) {
386
- const baseName = filename.split('@')[0];
385
+ if (viewName) {
387
386
  const pages = parent ? parent.children : this.nodeTree;
388
- const newRoutePath = this.applyPathStrategy(prefix + baseName);
387
+ const newRoutePath = this.applyPathStrategy(prefix + routePath);
389
388
  let sameRoute = null;
390
389
  for (const route of pages) {
391
390
  if (route.path === newRoutePath) {
@@ -394,10 +393,10 @@ export class FileRouter {
394
393
  }
395
394
  }
396
395
  if (sameRoute) {
397
- pageMapping.set(baseName, sameRoute);
396
+ pageMapping.set(routePath, sameRoute);
398
397
  }
399
398
  }
400
- const route = this.processFile(filePath, filename, {
399
+ const route = this.processFile(filePath, {
401
400
  dir: dirPath,
402
401
  include: page.include,
403
402
  exclude: page.exclude,
@@ -1,23 +1,35 @@
1
+ import type { PathParser, PathParseResult } from '../types/index.js';
1
2
  /**
2
- * @fileoverview 页面解析模块
3
+ * 路径解析错误类
3
4
  *
4
- * 负责解析页面文件路径,提取路由信息,包括:
5
- * - 路由路径转换
6
- *
7
- * 与构建工具无关,可在任何 Node.js 环境中使用。
5
+ * 提供详细的错误上下文信息,包括文件路径、错误类型和原始值。
8
6
  */
9
- type PathParseResult = {
10
- viewName: string;
11
- routePath: string;
12
- };
7
+ export declare class PathParseError extends TypeError {
8
+ /** 文件路径上下文 */
9
+ readonly filePath?: string;
10
+ /** 错误字段名称 */
11
+ readonly field?: string;
12
+ /** 原始值 */
13
+ readonly originalValue?: unknown;
14
+ constructor(message: string, options?: {
15
+ filePath?: string;
16
+ field?: string;
17
+ originalValue?: unknown;
18
+ cause?: Error;
19
+ });
20
+ /**
21
+ * 生成详细的错误信息
22
+ */
23
+ toString(): string;
24
+ }
13
25
  /**
14
- * 解析视图名称
26
+ * 解析路由路径和视图名称
15
27
  *
16
- * 从文件名中提取命名视图名称。
28
+ * 从文件名中提取路由路径和命名视图名称。
17
29
  *
18
- * @param baseName - 文件基础名,不包含后缀
19
- * @param defaultName - 默认视图名称
20
- * @returns 视图名称和去除视图名的基础名
30
+ * @param filePath - 文件路径
31
+ * @param [parser] - 路径解析器
32
+ * @returns 路由路径和视图名称
33
+ * @throws {PathParseError} 当路径解析失败时抛出
21
34
  */
22
- export declare function parseRoutePath(baseName: string, defaultName?: string): PathParseResult;
23
- export {};
35
+ export declare function parseRoutePath(filePath: string, parser?: PathParser): Exclude<PathParseResult, string>;
@@ -6,16 +6,215 @@
6
6
  *
7
7
  * 与构建工具无关,可在任何 Node.js 环境中使用。
8
8
  */
9
+ import path from 'node:path';
9
10
  /**
10
- * 解析视图名称
11
+ * 路径解析错误类
11
12
  *
12
- * 从文件名中提取命名视图名称。
13
+ * 提供详细的错误上下文信息,包括文件路径、错误类型和原始值。
14
+ */
15
+ export class PathParseError extends TypeError {
16
+ constructor(message, options) {
17
+ super(message, { cause: options?.cause });
18
+ /** 文件路径上下文 */
19
+ Object.defineProperty(this, "filePath", {
20
+ enumerable: true,
21
+ configurable: true,
22
+ writable: true,
23
+ value: void 0
24
+ });
25
+ /** 错误字段名称 */
26
+ Object.defineProperty(this, "field", {
27
+ enumerable: true,
28
+ configurable: true,
29
+ writable: true,
30
+ value: void 0
31
+ });
32
+ /** 原始值 */
33
+ Object.defineProperty(this, "originalValue", {
34
+ enumerable: true,
35
+ configurable: true,
36
+ writable: true,
37
+ value: void 0
38
+ });
39
+ this.name = 'PathParseError';
40
+ this.filePath = options?.filePath;
41
+ this.field = options?.field;
42
+ this.originalValue = options?.originalValue;
43
+ if (Error.captureStackTrace) {
44
+ Error.captureStackTrace(this, PathParseError);
45
+ }
46
+ }
47
+ /**
48
+ * 生成详细的错误信息
49
+ */
50
+ toString() {
51
+ let details = `${this.name}: ${this.message}`;
52
+ if (this.filePath) {
53
+ details += `\n File: ${this.filePath}`;
54
+ }
55
+ if (this.field) {
56
+ details += `\n Field: ${this.field}`;
57
+ }
58
+ if (this.originalValue !== undefined) {
59
+ details += `\n Value: ${String(this.originalValue)}`;
60
+ }
61
+ return details;
62
+ }
63
+ }
64
+ /**
65
+ * 解析路由路径和视图名称
66
+ *
67
+ * 从文件名中提取路由路径和命名视图名称。
68
+ *
69
+ * @param filePath - 文件路径
70
+ * @param [parser] - 路径解析器
71
+ * @returns 路由路径和视图名称
72
+ * @throws {PathParseError} 当路径解析失败时抛出
73
+ */
74
+ export function parseRoutePath(filePath, parser) {
75
+ const { basename } = extractFileInfo(filePath);
76
+ if (!parser) {
77
+ return parseDefaultRoutePath(basename);
78
+ }
79
+ const result = parser(basename, filePath);
80
+ return parseCustomRouteResult(result, filePath);
81
+ }
82
+ /**
83
+ * 提取文件信息
84
+ *
85
+ * @param filePath - 文件路径
86
+ * @returns 文件基本信息
87
+ */
88
+ function extractFileInfo(filePath) {
89
+ const ext = path.extname(filePath);
90
+ const basename = path.basename(filePath, ext);
91
+ return { basename, ext };
92
+ }
93
+ /**
94
+ * 解析默认路由路径(无自定义解析器)
95
+ *
96
+ * @param basename - 文件基本名称
97
+ * @returns 解析结果
98
+ */
99
+ function parseDefaultRoutePath(basename) {
100
+ const [routePath, viewName] = basename.split('@', 2);
101
+ return {
102
+ routePath,
103
+ viewName: viewName || 'default'
104
+ };
105
+ }
106
+ /**
107
+ * 解析自定义路由结果
108
+ *
109
+ * @param result - 解析器返回的结果
110
+ * @param filePath - 文件路径(用于错误上下文)
111
+ * @returns 解析结果
112
+ * @throws {PathParseError} 当结果无效时抛出
113
+ */
114
+ function parseCustomRouteResult(result, filePath) {
115
+ if (typeof result === 'string') {
116
+ return parseStringResult(result, filePath);
117
+ }
118
+ if (result && typeof result === 'object' && !Array.isArray(result)) {
119
+ return parseObjectResult(result, filePath);
120
+ }
121
+ throw new PathParseError('pathParser returned invalid result type', {
122
+ filePath,
123
+ originalValue: result,
124
+ field: 'result'
125
+ });
126
+ }
127
+ /**
128
+ * 解析字符串类型的结果
129
+ *
130
+ * @param result - 字符串结果
131
+ * @param filePath - 文件路径
132
+ * @returns 解析结果
133
+ * @throws {PathParseError} 当路径无效时抛出
134
+ */
135
+ function parseStringResult(result, filePath) {
136
+ const routePath = normalizeRoutePath(result);
137
+ if (!routePath) {
138
+ throw new PathParseError('pathParser returned empty routePath', {
139
+ filePath,
140
+ originalValue: result,
141
+ field: 'routePath'
142
+ });
143
+ }
144
+ return { routePath, viewName: 'default' };
145
+ }
146
+ /**
147
+ * 解析对象类型的结果
148
+ *
149
+ * @param result - 对象结果
150
+ * @param filePath - 文件路径
151
+ * @returns 解析结果
152
+ * @throws {PathParseError} 当结果无效时抛出
153
+ */
154
+ function parseObjectResult(result, filePath) {
155
+ const { routePath: rawRoutePath, viewName } = result;
156
+ validateRoutePathType(rawRoutePath, filePath);
157
+ const routePath = normalizeRoutePath(rawRoutePath);
158
+ if (!routePath) {
159
+ throw new PathParseError('pathParser returned empty routePath after normalization', {
160
+ filePath,
161
+ originalValue: rawRoutePath,
162
+ field: 'routePath'
163
+ });
164
+ }
165
+ validateViewName(viewName, filePath);
166
+ return {
167
+ routePath,
168
+ viewName: viewName || 'default'
169
+ };
170
+ }
171
+ /**
172
+ * 验证路由路径类型
173
+ *
174
+ * @param routePath - 路由路径
175
+ * @param filePath - 文件路径(用于错误上下文)
176
+ * @throws {PathParseError} 当类型无效时抛出
177
+ */
178
+ function validateRoutePathType(routePath, filePath) {
179
+ if (typeof routePath !== 'string') {
180
+ throw new PathParseError('pathParser returned non-string routePath', {
181
+ filePath,
182
+ originalValue: routePath,
183
+ field: 'routePath'
184
+ });
185
+ }
186
+ if (!routePath.trim()) {
187
+ throw new PathParseError('pathParser returned empty or whitespace-only routePath', {
188
+ filePath,
189
+ originalValue: routePath,
190
+ field: 'routePath'
191
+ });
192
+ }
193
+ }
194
+ /**
195
+ * 验证视图名称
196
+ *
197
+ * @param viewName - 视图名称
198
+ * @param filePath - 文件路径(用于错误上下文)
199
+ * @throws {PathParseError} 当视图名称无效时抛出
200
+ */
201
+ function validateViewName(viewName, filePath) {
202
+ if (viewName !== undefined && typeof viewName !== 'string') {
203
+ throw new PathParseError('pathParser returned non-string viewName', {
204
+ filePath,
205
+ originalValue: viewName,
206
+ field: 'viewName'
207
+ });
208
+ }
209
+ }
210
+ /**
211
+ * 标准化路由路径
212
+ *
213
+ * 去除路径首尾空白和开头的斜杠。
13
214
  *
14
- * @param baseName - 文件基础名,不包含后缀
15
- * @param defaultName - 默认视图名称
16
- * @returns 视图名称和去除视图名的基础名
215
+ * @param routePath - 原始路由路径
216
+ * @returns 标准化后的路由路径
17
217
  */
18
- export function parseRoutePath(baseName, defaultName = 'default') {
19
- let [routePath, viewName] = baseName.split('@');
20
- return { viewName: viewName || defaultName, routePath };
218
+ function normalizeRoutePath(routePath) {
219
+ return routePath.trim().replace(/^\/+/, '');
21
220
  }
@@ -1,16 +1,68 @@
1
1
  import type { CodeTransformHook, ExtendRouteHook } from './hooks.js';
2
+ /**
3
+ * 自定义导入模式函数的上下文
4
+ */
5
+ export interface ImportModeContext {
6
+ /**
7
+ * 组件文件路径(已 JSON.stringify)
8
+ */
9
+ importPath: string;
10
+ /**
11
+ * 组件文件原始路径
12
+ */
13
+ filePath: string;
14
+ /**
15
+ * 添加导入语句
16
+ * 用于向生成的代码顶部添加 import 语句
17
+ */
18
+ addImport: (statement: string) => void;
19
+ }
20
+ /**
21
+ * 自定义导入模式函数
22
+ *
23
+ * @param context - 导入上下文
24
+ * @returns 组件表达式代码
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * // 自定义导入模式:使用 React.lazy
29
+ * (context) => {
30
+ * context.addImport(`import { lazy } from 'react'`)
31
+ * return `lazy(() => import(${context.importPath}))`
32
+ * }
33
+ * ```
34
+ */
35
+ export type ImportModeFunction = (context: ImportModeContext) => string;
2
36
  /**
3
37
  * 组件导入模式。
4
38
  *
5
- * - 'lazy': 生成懒加载表达式
6
- * - 'sync': 同步加载组件
39
+ * - 'lazy': 生成懒加载表达式 `lazy(() => import(path))`
40
+ * - 'sync': 同步加载组件,生成 `import` 语句
41
+ * - 函数: 自定义导入逻辑
7
42
  */
8
- export type ImportMode = 'lazy' | 'sync';
43
+ export type ImportMode = 'lazy' | 'sync' | ImportModeFunction;
9
44
  /**
10
45
  * 生成路径的策略。
11
46
  */
12
47
  export type PathStrategy = 'kebab' | 'lowercase' | 'raw';
13
48
  export type PageSource = string | PageDirOptions;
49
+ /**
50
+ * 路径解析结果
51
+ */
52
+ export type PathParseResult = string | {
53
+ /** 解析后的路径 */
54
+ routePath: string;
55
+ /** 视图名称 */
56
+ viewName?: string;
57
+ };
58
+ /**
59
+ * 路径解析器
60
+ *
61
+ * @param basename - 文件名称(不包含扩展名)
62
+ * @param filePath - 完整的文件路径
63
+ * @returns {PathParseResult} 路径解析结果包含路径和视图名称
64
+ */
65
+ export type PathParser = (basename: string, filePath: string) => PathParseResult;
14
66
  /**
15
67
  * 页面目录选项
16
68
  */
@@ -60,12 +112,33 @@ export interface FileRouterOptions {
60
112
  /**
61
113
  * 路径转换策略
62
114
  *
115
+ * - 'kebab': 转换为 kebab-case
116
+ * - 'lowercase': 转换为 lowercase
117
+ * - 'raw': 不转换
118
+ *
119
+ * @values 'kebab' | 'lowercase' | 'raw'
63
120
  * @default 'kebab'
64
121
  */
65
122
  pathStrategy?: PathStrategy;
66
123
  /**
67
124
  * 组件导入模式
68
125
  *
126
+ * - 'lazy': 生成懒加载表达式 `lazy(() => import(path))`
127
+ * - 'sync': 同步加载组件,生成 `import` 语句
128
+ * - 函数: 自定义导入逻辑
129
+ *
130
+ * @example
131
+ * ```ts
132
+ * // 使用预设模式
133
+ * importMode: 'lazy'
134
+ *
135
+ * // 使用自定义函数
136
+ * importMode: (context) => {
137
+ * context.addImport(`import { lazy } from 'react'`)
138
+ * return `lazy(() => import(${context.importPath}))`
139
+ * }
140
+ * ```
141
+ *
69
142
  * @default 'lazy'
70
143
  */
71
144
  importMode?: ImportMode;
@@ -135,4 +208,12 @@ export interface FileRouterOptions {
135
208
  * ```
136
209
  */
137
210
  extendRoute?: ExtendRouteHook;
211
+ /**
212
+ * 路径解析器
213
+ *
214
+ * @param basename - 文件名称(不包含扩展名)或目录名称
215
+ * @param filePath - 完整的文件路径
216
+ * @returns {PathParseResult} 返回字符串path,或包含path和viewName的对象
217
+ */
218
+ pathParser?: PathParser;
138
219
  }
@@ -14,7 +14,6 @@ export default function VitarxRouter(options = {}) {
14
14
  let isPreview = false;
15
15
  return {
16
16
  name: 'vite-plugin-vitarx-router',
17
- enforce: 'pre',
18
17
  config(_, env) {
19
18
  isPreview = !!env.isPreview;
20
19
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vitarx-router",
3
- "version": "4.0.0-beta.3",
3
+ "version": "4.0.0-beta.5",
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",