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

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.
@@ -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, PathParser, PathStrategy } from '../types/index.js';
2
2
  export type PageDirConfig = Required<PageDirOptions>;
3
3
  /**
4
4
  * 规范化后的配置
@@ -14,6 +14,7 @@ export interface ResolvedConfig {
14
14
  configFileName: string;
15
15
  transform?: CodeTransformHook;
16
16
  extendRoute?: ExtendRouteHook;
17
+ pathParser?: PathParser;
17
18
  }
18
19
  /**
19
20
  * 规范化文件路由配置
@@ -56,7 +56,7 @@ function resolvePageConfigs(pages, root) {
56
56
  * @returns - 规范化后的配置对象
57
57
  */
58
58
  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;
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, pathParser } = options;
60
60
  const resolvedPages = resolvePageConfigs(pages, root);
61
61
  return {
62
62
  dts: typeof dts === 'string' ? dts : dts ? DEFAULT_DTS_FILE : false,
@@ -68,6 +68,7 @@ export function resolveConfig(options) {
68
68
  layoutFileName,
69
69
  configFileName,
70
70
  transform,
71
- extendRoute
71
+ extendRoute,
72
+ pathParser
72
73
  };
73
74
  }
@@ -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} 当配置无效时抛出错误
@@ -198,6 +198,19 @@ function validateExtendRoute(opts) {
198
198
  throw new Error('options.extendRoute 必须是函数');
199
199
  }
200
200
  }
201
+ /**
202
+ * 验证 pathParser 配置
203
+ *
204
+ * @param opts - 配置选项
205
+ * @throws {Error} 当配置无效时抛出错误
206
+ */
207
+ function validatePathParser(opts) {
208
+ if (opts.pathParser === undefined)
209
+ return;
210
+ if (typeof opts.pathParser !== 'function') {
211
+ throw new Error('options.pathParser 必须是函数');
212
+ }
213
+ }
201
214
  /**
202
215
  * 验证插件配置选项
203
216
  *
@@ -212,6 +225,7 @@ function validateExtendRoute(opts) {
212
225
  * 8. configFileName 配置
213
226
  * 9. transform 配置
214
227
  * 10. extendRoute 配置
228
+ * 11. pathParser 配置
215
229
  *
216
230
  * @param opts - 用户提供的配置选项
217
231
  * @throws {Error} 当配置无效时抛出错误
@@ -227,4 +241,5 @@ export function validateOptions(opts) {
227
241
  validateConfigFileName(opts);
228
242
  validateTransform(opts);
229
243
  validateExtendRoute(opts);
244
+ validatePathParser(opts);
230
245
  }
@@ -126,7 +126,7 @@ function formatComponent(component, importMode, importLines) {
126
126
  else {
127
127
  expr = `lazy(() => import(${importPath}))`;
128
128
  }
129
- return `${name}: ${expr}`;
129
+ return `${JSON.stringify(name)}: ${expr}`;
130
130
  });
131
131
  return `{ ${entries.join(', ')} }`;
132
132
  }
@@ -7,6 +7,7 @@
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';
@@ -66,7 +67,6 @@ export declare class FileRouter {
66
67
  /**
67
68
  * 处理文件
68
69
  * @param filePath - 文件路径
69
- * @param fileName - 文件名
70
70
  * @param page - 页面配置
71
71
  * @param pageMapping - 子路由
72
72
  * @param parent - 父节点
@@ -91,7 +91,6 @@ export declare class FileRouter {
91
91
  *
92
92
  * @param file - 文件绝对路径
93
93
  * @param name - 文件名
94
- * @param ext - 文件扩展名
95
94
  * @param pages - 页面配置,默认为 `config.pages`
96
95
  * @returns {string} - 文件类型,可选值有 `layout`、`config`、`page`、`ignore`
97
96
  */
@@ -100,10 +99,10 @@ export declare class FileRouter {
100
99
  * 检查文件是否为页面文件
101
100
  *
102
101
  * @param file - 文件绝对路径
103
- * @param pages - 页面配置,默认为 `config.pages`
102
+ * @param filter - 过滤配置,默认为 `config.pages`
104
103
  * @returns {boolean} - 是否为页面文件
105
104
  */
106
- private isPageFile;
105
+ isPageFile(file: string, filter?: FilterOptions | readonly FilterOptions[]): boolean;
107
106
  /**
108
107
  * 写入类型定义文件
109
108
  */
@@ -137,7 +137,7 @@ export class FileRouter {
137
137
  }
138
138
  else {
139
139
  // 处理文件
140
- route = this.processFile(filePath, dirent.name, page, pageMapping, parent);
140
+ route = this.processFile(filePath, page, pageMapping, parent);
141
141
  }
142
142
  if (route) {
143
143
  children.add(route);
@@ -169,18 +169,15 @@ export class FileRouter {
169
169
  /**
170
170
  * 处理文件
171
171
  * @param filePath - 文件路径
172
- * @param fileName - 文件名
173
172
  * @param page - 页面配置
174
173
  * @param pageMapping - 子路由
175
174
  * @param parent - 父节点
176
175
  * @private
177
176
  */
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);
177
+ processFile(filePath, page, pageMapping, parent) {
178
+ // 分离出路由 path 和视图命名
179
+ const { routePath, viewName = 'default' } = parseRoutePath(filePath, this.config.pathParser);
180
+ const fileType = this.getPageType(filePath, routePath, page);
184
181
  if (fileType === 'ignore')
185
182
  return null;
186
183
  // 处理分组配置文件
@@ -201,8 +198,6 @@ export class FileRouter {
201
198
  }
202
199
  return null;
203
200
  }
204
- // 分离出路由 path 和视图命名
205
- const { routePath, viewName } = parseRoutePath(baseName);
206
201
  // 处理分组布局文件
207
202
  if (fileType === 'layout') {
208
203
  if (!parent)
@@ -273,15 +268,14 @@ export class FileRouter {
273
268
  *
274
269
  * @param file - 文件绝对路径
275
270
  * @param name - 文件名
276
- * @param ext - 文件扩展名
277
271
  * @param pages - 页面配置,默认为 `config.pages`
278
272
  * @returns {string} - 文件类型,可选值有 `layout`、`config`、`page`、`ignore`
279
273
  */
280
- getPageType(file, name, ext, pages) {
281
- if (name === this.config.layoutFileName || name.startsWith(`${this.config.configFileName}@`)) {
274
+ getPageType(file, name, pages) {
275
+ if (name === this.config.layoutFileName) {
282
276
  return 'layout';
283
277
  }
284
- if (name === this.config.configFileName && (ext === '.ts' || ext === '.js')) {
278
+ if (name === this.config.configFileName && (file.endsWith('.ts') || file.endsWith('.js'))) {
285
279
  return 'config';
286
280
  }
287
281
  if (this.isPageFile(file, pages)) {
@@ -293,16 +287,16 @@ export class FileRouter {
293
287
  * 检查文件是否为页面文件
294
288
  *
295
289
  * @param file - 文件绝对路径
296
- * @param pages - 页面配置,默认为 `config.pages`
290
+ * @param filter - 过滤配置,默认为 `config.pages`
297
291
  * @returns {boolean} - 是否为页面文件
298
292
  */
299
- isPageFile(file, pages) {
300
- if (pages) {
301
- if (Array.isArray(pages)) {
302
- return !!isPageFileInDirs(file, pages);
293
+ isPageFile(file, filter) {
294
+ if (filter) {
295
+ if (Array.isArray(filter)) {
296
+ return !!isPageFileInDirs(file, filter);
303
297
  }
304
298
  else {
305
- return isPageFile(file, pages);
299
+ return isPageFile(file, filter);
306
300
  }
307
301
  }
308
302
  return !!isPageFileInDirs(file, this.config.pages);
@@ -376,16 +370,16 @@ export class FileRouter {
376
370
  const page = isPageFileInDirs(filePath, this.config.pages);
377
371
  if (!page)
378
372
  return false;
373
+ // 分离出路由 path 和视图命名
374
+ const { routePath, viewName } = parseRoutePath(filePath, this.config.pathParser);
379
375
  const dirPath = nodePath.dirname(filePath);
380
- const filename = nodePath.basename(filePath);
381
376
  const parent = this.fileMap.get(dirPath);
382
377
  const pageMapping = new Map();
383
378
  const prefix = parent ? '' : page.prefix;
384
379
  // 如果是命名文件,则先查找是否存在同名路由,存在则添加到同名路由的 children 中
385
- if (filename.includes('@')) {
386
- const baseName = filename.split('@')[0];
380
+ if (viewName) {
387
381
  const pages = parent ? parent.children : this.nodeTree;
388
- const newRoutePath = this.applyPathStrategy(prefix + baseName);
382
+ const newRoutePath = this.applyPathStrategy(prefix + routePath);
389
383
  let sameRoute = null;
390
384
  for (const route of pages) {
391
385
  if (route.path === newRoutePath) {
@@ -394,10 +388,10 @@ export class FileRouter {
394
388
  }
395
389
  }
396
390
  if (sameRoute) {
397
- pageMapping.set(baseName, sameRoute);
391
+ pageMapping.set(routePath, sameRoute);
398
392
  }
399
393
  }
400
- const route = this.processFile(filePath, filename, {
394
+ const route = this.processFile(filePath, {
401
395
  dir: dirPath,
402
396
  include: page.include,
403
397
  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
  }
@@ -11,6 +11,23 @@ export type ImportMode = 'lazy' | 'sync';
11
11
  */
12
12
  export type PathStrategy = 'kebab' | 'lowercase' | 'raw';
13
13
  export type PageSource = string | PageDirOptions;
14
+ /**
15
+ * 路径解析结果
16
+ */
17
+ export type PathParseResult = string | {
18
+ /** 解析后的路径 */
19
+ routePath: string;
20
+ /** 视图名称 */
21
+ viewName?: string;
22
+ };
23
+ /**
24
+ * 路径解析器
25
+ *
26
+ * @param basename - 文件名称(不包含扩展名)
27
+ * @param filePath - 完整的文件路径
28
+ * @returns {PathParseResult} 路径解析结果包含路径和视图名称
29
+ */
30
+ export type PathParser = (basename: string, filePath: string) => PathParseResult;
14
31
  /**
15
32
  * 页面目录选项
16
33
  */
@@ -60,6 +77,11 @@ export interface FileRouterOptions {
60
77
  /**
61
78
  * 路径转换策略
62
79
  *
80
+ * - 'kebab': 转换为 kebab-case
81
+ * - 'lowercase': 转换为 lowercase
82
+ * - 'raw': 不转换
83
+ *
84
+ * @values 'kebab' | 'lowercase' | 'raw'
63
85
  * @default 'kebab'
64
86
  */
65
87
  pathStrategy?: PathStrategy;
@@ -135,4 +157,12 @@ export interface FileRouterOptions {
135
157
  * ```
136
158
  */
137
159
  extendRoute?: ExtendRouteHook;
160
+ /**
161
+ * 路径解析器
162
+ *
163
+ * @param basename - 文件名称(不包含扩展名)或目录名称
164
+ * @param filePath - 完整的文件路径
165
+ * @returns {PathParseResult} 返回字符串path,或包含path和viewName的对象
166
+ */
167
+ pathParser?: PathParser;
138
168
  }
@@ -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.4",
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",