vitarx-router 4.0.2 → 4.0.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.
@@ -6,10 +6,22 @@
6
6
  */
7
7
  import type * as BabelTypes from '@babel/types';
8
8
  import type { PageOptions } from '../types/index.js';
9
+ /**
10
+ * 不支持节点的警告信息
11
+ */
12
+ export interface UnsupportedNodeWarning {
13
+ /** 不支持的 AST 节点类型 */
14
+ nodeType: string;
15
+ /** 属性访问路径,如 "meta.nav.items[0].label" */
16
+ path: string;
17
+ /** 修复建议 */
18
+ suggestion: string;
19
+ }
9
20
  /**
10
21
  * 从对象表达式提取页面配置
11
22
  *
12
23
  * @param node - 对象表达式节点
13
- * @returns 页面配置
24
+ * @param warnings - 警告收集器(可选,不传则不收集警告)
25
+ * @returns 页面配置和警告列表
14
26
  */
15
- export declare function extractPageOptions(node: BabelTypes.ObjectExpression): PageOptions;
27
+ export declare function extractPageOptions(node: BabelTypes.ObjectExpression, warnings?: UnsupportedNodeWarning[]): PageOptions;
@@ -1,12 +1,46 @@
1
+ /**
2
+ * 不支持节点类型到修复建议的映射
3
+ */
4
+ const UNSUPPORTED_NODE_SUGGESTIONS = {
5
+ Identifier: '不支持变量引用,请使用字面量值。例如将 { name: myVar } 改为 { name: "value" }',
6
+ MemberExpression: '不支持属性访问表达式,请使用字面量值。例如将 { api: config.url } 改为 { api: "https://example.com" }',
7
+ CallExpression: '不支持函数调用,请使用字面量值。例如将 { id: genId() } 改为 { id: "static-id" }',
8
+ ArrowFunctionExpression: '不支持箭头函数,请使用字面量值。例如将 { handler: () => {} } 改为 { handler: "handlerName" }',
9
+ FunctionExpression: '不支持函数表达式,请使用字面量值。例如将 { handler: function() {} } 改为 { handler: "handlerName" }',
10
+ NewExpression: '不支持 new 表达式(除 new RegExp 外),请使用字面量值',
11
+ SpreadElement: '不支持展开运算符,请显式列出所有属性',
12
+ ConditionalExpression: '不支持三元表达式,请使用字面量值。例如将 { val: cond ? "a" : "b" } 改为 { val: "a" }',
13
+ BinaryExpression: '不支持二元运算表达式,请使用字面量值。例如将 { val: 1 + 2 } 改为 { val: 3 }',
14
+ LogicalExpression: '不支持逻辑运算表达式,请使用字面量值。例如将 { val: a || "default" } 改为 { val: "default" }',
15
+ SequenceExpression: '不支持逗号表达式,请使用字面量值',
16
+ AssignmentExpression: '不支持赋值表达式,请使用字面量值',
17
+ UpdateExpression: '不支持更新表达式,请使用字面量值。例如将 { val: i++ } 改为 { val: 1 }',
18
+ TaggedTemplateExpression: '不支持标签模板字符串,请使用普通字符串字面量',
19
+ ClassExpression: '不支持类表达式,请使用字面量值',
20
+ ObjectMethod: '不支持对象方法定义,请使用字面量值。例如将 { fn() {} } 改为 { fn: "methodName" }'
21
+ };
22
+ /**
23
+ * 获取不支持节点的修复建议
24
+ *
25
+ * @param nodeType - AST 节点类型
26
+ * @returns 修复建议
27
+ */
28
+ function getSuggestion(nodeType) {
29
+ return (UNSUPPORTED_NODE_SUGGESTIONS[nodeType] ??
30
+ `不支持 ${nodeType} 类型的表达式,definePage 配置仅支持静态字面量值(字符串、数字、布尔、null、对象、数组、正则、一元表达式、简单模板字符串)`);
31
+ }
1
32
  /**
2
33
  * 提取字面量值
3
34
  *
4
35
  * 从 AST 节点提取 JavaScript 字面量值。
36
+ * 遇到不支持的节点类型时,将警告信息收集到 warnings 数组中。
5
37
  *
6
38
  * @param node - AST 节点
39
+ * @param currentPath - 当前属性访问路径
40
+ * @param warnings - 警告收集器
7
41
  * @returns 提取的值
8
42
  */
9
- function extractAstLiteralValue(node) {
43
+ function extractAstLiteralValue(node, currentPath, warnings) {
10
44
  switch (node.type) {
11
45
  case 'StringLiteral':
12
46
  return node.value;
@@ -16,11 +50,57 @@ function extractAstLiteralValue(node) {
16
50
  return node.value;
17
51
  case 'NullLiteral':
18
52
  return null;
53
+ case 'RegExpLiteral':
54
+ return new RegExp(node.pattern, node.flags || '');
55
+ case 'TemplateLiteral': {
56
+ // 仅支持无表达式的简单模板字符串,如 `hello`
57
+ if (node.quasis.length === 1 && node.expressions.length === 0) {
58
+ return node.quasis[0].value.cooked;
59
+ }
60
+ warnings.push({
61
+ nodeType: node.type,
62
+ path: currentPath,
63
+ suggestion: '不支持带插值表达式的模板字符串,请使用纯字符串字面量。例如将 `Hello ${name}` 改为 "Hello World"'
64
+ });
65
+ return undefined;
66
+ }
67
+ case 'UnaryExpression': {
68
+ const arg = extractAstLiteralValue(node.argument, currentPath, warnings);
69
+ if (arg === undefined || arg === null)
70
+ return undefined;
71
+ switch (node.operator) {
72
+ case '-':
73
+ return -arg;
74
+ case '+':
75
+ return +arg;
76
+ case '!':
77
+ return !arg;
78
+ case '~':
79
+ return ~arg;
80
+ case 'void':
81
+ return undefined;
82
+ default:
83
+ warnings.push({
84
+ nodeType: node.type,
85
+ path: currentPath,
86
+ suggestion: `不支持 "${node.operator}" 一元运算符,仅支持 -, +, !, ~, void`
87
+ });
88
+ return undefined;
89
+ }
90
+ }
19
91
  case 'ArrayExpression':
20
- return node.elements.map(elem => (elem ? extractAstLiteralValue(elem) : null));
92
+ return node.elements.map((elem, index) => elem ? extractAstLiteralValue(elem, `${currentPath}[${index}]`, warnings) : null);
21
93
  case 'ObjectExpression': {
22
94
  const obj = {};
23
95
  for (const prop of node.properties) {
96
+ if (prop.type === 'ObjectMethod') {
97
+ warnings.push({
98
+ nodeType: 'ObjectMethod',
99
+ path: currentPath,
100
+ suggestion: getSuggestion('ObjectMethod')
101
+ });
102
+ continue;
103
+ }
24
104
  if (prop.type !== 'ObjectProperty')
25
105
  continue;
26
106
  const key = prop.key.type === 'Identifier'
@@ -29,13 +109,19 @@ function extractAstLiteralValue(node) {
29
109
  ? prop.key.value
30
110
  : null;
31
111
  if (key) {
32
- obj[key] = extractAstLiteralValue(prop.value);
112
+ obj[key] = extractAstLiteralValue(prop.value, `${currentPath}.${key}`, warnings);
33
113
  }
34
114
  }
35
115
  return obj;
36
116
  }
37
- default:
117
+ default: {
118
+ warnings.push({
119
+ nodeType: node.type,
120
+ path: currentPath,
121
+ suggestion: getSuggestion(node.type)
122
+ });
38
123
  return undefined;
124
+ }
39
125
  }
40
126
  }
41
127
  /**
@@ -68,9 +154,11 @@ function extractStringRecord(node) {
68
154
  * 提取 meta 值
69
155
  *
70
156
  * @param node - AST 节点
157
+ * @param currentPath - 当前属性访问路径
158
+ * @param warnings - 警告收集器
71
159
  * @returns meta 对象
72
160
  */
73
- function extractMetaValue(node) {
161
+ function extractMetaValue(node, currentPath, warnings) {
74
162
  if (node.type !== 'ObjectExpression')
75
163
  return undefined;
76
164
  const meta = {};
@@ -84,7 +172,7 @@ function extractMetaValue(node) {
84
172
  : null;
85
173
  if (!key)
86
174
  continue;
87
- meta[key] = extractAstLiteralValue(prop.value);
175
+ meta[key] = extractAstLiteralValue(prop.value, `${currentPath}.${key}`, warnings);
88
176
  }
89
177
  return meta;
90
178
  }
@@ -222,10 +310,12 @@ function extractAliasValue(node) {
222
310
  * 从对象表达式提取页面配置
223
311
  *
224
312
  * @param node - 对象表达式节点
225
- * @returns 页面配置
313
+ * @param warnings - 警告收集器(可选,不传则不收集警告)
314
+ * @returns 页面配置和警告列表
226
315
  */
227
- export function extractPageOptions(node) {
316
+ export function extractPageOptions(node, warnings) {
228
317
  const options = {};
318
+ const warnList = warnings ?? [];
229
319
  for (const prop of node.properties) {
230
320
  if (prop.type !== 'ObjectProperty')
231
321
  continue;
@@ -243,7 +333,7 @@ export function extractPageOptions(node) {
243
333
  }
244
334
  break;
245
335
  case 'meta':
246
- options.meta = extractMetaValue(prop.value);
336
+ options.meta = extractMetaValue(prop.value, 'meta', warnList);
247
337
  break;
248
338
  case 'pattern':
249
339
  options.pattern = extractPatternValue(prop.value);
@@ -56,6 +56,7 @@ export function parseDefinePage(content, filePath) {
56
56
  try {
57
57
  const ast = parseCode(content);
58
58
  const routeOptionsList = [];
59
+ const warnings = [];
59
60
  babelTraverse(ast, {
60
61
  CallExpression(nodePath) {
61
62
  const { node } = nodePath;
@@ -66,7 +67,7 @@ export function parseDefinePage(content, filePath) {
66
67
  if (!arg || arg.type !== 'ObjectExpression') {
67
68
  return;
68
69
  }
69
- const options = extractPageOptions(arg);
70
+ const options = extractPageOptions(arg, warnings);
70
71
  routeOptionsList.push(options);
71
72
  }
72
73
  });
@@ -76,6 +77,10 @@ export function parseDefinePage(content, filePath) {
76
77
  if (routeOptionsList.length > 1) {
77
78
  warn('检测到多个 definePage 调用,将合并所有配置,建议每个文件只调用一次 definePage', `in ${filePath}`);
78
79
  }
80
+ // 输出不支持节点的警告
81
+ for (const w of warnings) {
82
+ warn(`definePage 配置解析警告: 路径 "${w.path}" 处遇到不支持的 ${w.nodeType} 节点,该属性值将被忽略。${w.suggestion}`, `in ${filePath}`);
83
+ }
79
84
  return mergePageOptions(...routeOptionsList);
80
85
  }
81
86
  catch (e) {
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { type PageDirConfig, type ResolvedConfig } from '../config/index.js';
11
11
  import { type FileInfo } from '../parser/parsePage.js';
12
- import type { PageOptions, PageParseResult, ScanNode } from '../types/index.js';
12
+ import type { PageParseResult, ScanNode } from '../types/index.js';
13
13
  /**
14
14
  * 扫描阶段的目录配置
15
15
  *
@@ -97,24 +97,4 @@ export interface ProcessPageFileParams {
97
97
  * @returns 新创建的路由节点;合并到已有路由时返回 null
98
98
  */
99
99
  export declare function processPageFile(params: ProcessPageFileParams, context: ProcessorContext): ScanNode | null;
100
- /**
101
- * 解析分组目录的自定义路径和选项
102
- *
103
- * 通过 groupParser 解析目录名,支持两种返回格式:
104
- * 1. 字符串:仅自定义路径
105
- * 2. 对象:自定义路径 + 路由选项(如 meta、name 等)
106
- *
107
- * 未配置 groupParser 时,直接使用原始目录名作为路径。
108
- *
109
- * @param fileName - 目录名
110
- * @param filePath - 目录完整路径
111
- * @param groupParser - 分组解析器,可选
112
- * @returns 解析后的路径和选项
113
- */
114
- export declare function parseGroupResult(fileName: string, filePath: string, groupParser?: (dirName: string, dirPath: string) => string | {
115
- path: string;
116
- options?: PageOptions;
117
- }): {
118
- routePath: string;
119
- options?: PageOptions;
120
- };
100
+ export { parseGroupResult } from '../utils/groupParser.js';
@@ -104,27 +104,5 @@ export function processPageFile(params, context) {
104
104
  pageMapping.set(parsed.path, route);
105
105
  return route;
106
106
  }
107
- /**
108
- * 解析分组目录的自定义路径和选项
109
- *
110
- * 通过 groupParser 解析目录名,支持两种返回格式:
111
- * 1. 字符串:仅自定义路径
112
- * 2. 对象:自定义路径 + 路由选项(如 meta、name 等)
113
- *
114
- * 未配置 groupParser 时,直接使用原始目录名作为路径。
115
- *
116
- * @param fileName - 目录名
117
- * @param filePath - 目录完整路径
118
- * @param groupParser - 分组解析器,可选
119
- * @returns 解析后的路径和选项
120
- */
121
- export function parseGroupResult(fileName, filePath, groupParser) {
122
- if (!groupParser) {
123
- return { routePath: fileName };
124
- }
125
- const result = groupParser(fileName, filePath);
126
- if (typeof result === 'string') {
127
- return { routePath: result };
128
- }
129
- return { routePath: result.path, options: result.options };
130
- }
107
+ // parseGroupResult 已移至 utils/groupParser.ts,此处重新导出以保持向后兼容
108
+ export { parseGroupResult } from '../utils/groupParser.js';
@@ -122,6 +122,7 @@ export class FileRouter {
122
122
  fileMap: this.fileMap,
123
123
  pages: this.config.pages,
124
124
  pageParser: this.config.pageParser,
125
+ groupParser: this.config.groupParser,
125
126
  pathStrategy: this.config.pathStrategy
126
127
  });
127
128
  }
@@ -1,10 +1,11 @@
1
1
  import { type PageDirConfig } from '../config/resolve.js';
2
- import type { PageParser, PathStrategy, ScanNode } from '../types/index.js';
2
+ import type { GroupParser, PageParser, PathStrategy, ScanNode } from '../types/index.js';
3
3
  import { type FileInfo } from './parsePage.js';
4
4
  interface RouteFullPathContext {
5
5
  fileMap: Map<string, ScanNode>;
6
6
  pages: readonly PageDirConfig[];
7
7
  pageParser?: PageParser;
8
+ groupParser?: GroupParser;
8
9
  pathStrategy: PathStrategy;
9
10
  }
10
11
  /**
@@ -5,6 +5,7 @@
5
5
  * 支持已跟踪(在路由树中)和未跟踪(尚未扫描)两种场景。
6
6
  */
7
7
  import nodePath from 'node:path';
8
+ import { parseGroupResult } from '../utils/groupParser.js';
8
9
  import { applyPathStrategy } from '../utils/pathStrategy.js';
9
10
  import { normalizeRoutePath, resolvePathVariable } from '../utils/pathUtils.js';
10
11
  import { isPageFileInDirs } from './filterUtils.js';
@@ -48,27 +49,39 @@ function computeNodeFullPath(node) {
48
49
  * @returns 完整路由路径,非页面文件返回 null
49
50
  */
50
51
  export function computeRouteFullPath(filePath, fileInfo, context) {
51
- const { fileMap, pages, pageParser, pathStrategy } = context;
52
+ const { fileMap, pages, pageParser, groupParser, pathStrategy } = context;
53
+ // 场景一:文件已在路由树中,直接沿 parent 链计算 fullPath
52
54
  const node = fileMap.get(filePath);
53
55
  if (node) {
54
56
  return computeNodeFullPath(node);
55
57
  }
58
+ // 场景二:文件尚未被扫描跟踪,需要从文件路径手动计算
56
59
  const page = isPageFileInDirs(filePath, pages);
57
60
  if (!page)
58
61
  return null;
62
+ // 解析文件名得到路径段(index 文件不产生路径段)
59
63
  const parsed = parsePageFile(filePath, pageParser, fileInfo);
60
- const pathSegment = parsed.path === 'index' ? '' : applyFullPathStrategy(parsed.path, pathStrategy);
64
+ const pathSegment = parsed.path === 'index' ? '' : parsed.path;
61
65
  const segments = pathSegment ? [pathSegment] : [];
66
+ // 从文件所在目录向上遍历至 pages.dir 根目录,逐级收集路径段
62
67
  let dirPath = nodePath.dirname(filePath);
63
68
  while (dirPath.length > page.dir.length) {
64
69
  const dirNode = fileMap.get(dirPath);
65
70
  if (dirNode) {
71
+ // 目录已在路由树中,直接复用其 fullPath 作为前缀,无需继续向上遍历
66
72
  const parentFullPath = computeNodeFullPath(dirNode);
67
- return normalizeRoutePath(parentFullPath + '/' + segments.join('/'));
73
+ segments.unshift(parentFullPath);
74
+ return normalizeRoutePath(applyFullPathStrategy(segments.join('/'), pathStrategy));
68
75
  }
69
- segments.unshift(applyFullPathStrategy(nodePath.basename(dirPath), pathStrategy));
76
+ // 目录未被跟踪,通过 groupParser 解析目录名(如 "1.user" → "user"),再作为路径段
77
+ const dirName = nodePath.basename(dirPath);
78
+ const { routePath: parsedDirName } = parseGroupResult(dirName, dirPath, groupParser);
79
+ segments.unshift(parsedDirName);
70
80
  dirPath = nodePath.dirname(dirPath);
71
81
  }
72
- const prefix = page.prefix ? applyFullPathStrategy(page.prefix, pathStrategy) : '';
73
- return normalizeRoutePath(prefix + '/' + segments.join('/'));
82
+ // 顶层路由拼接 prefix
83
+ if (page.prefix)
84
+ segments.unshift(page.prefix);
85
+ // 统一应用路径策略(命名转换 + 动态参数转换)并规范化
86
+ return normalizeRoutePath(applyFullPathStrategy(segments.join('/'), pathStrategy));
74
87
  }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @fileoverview 分组目录解析工具
3
+ *
4
+ * 提供 groupParser 的调用封装,将目录名解析为路由路径和选项。
5
+ * 供扫描器和路径计算等模块共同使用。
6
+ */
7
+ import type { PageOptions } from '../types/index.js';
8
+ /**
9
+ * 解析分组目录的自定义路径和选项
10
+ *
11
+ * 通过 groupParser 解析目录名,支持两种返回格式:
12
+ * 1. 字符串:仅自定义路径
13
+ * 2. 对象:自定义路径 + 路由选项(如 meta、name 等)
14
+ *
15
+ * 未配置 groupParser 时,直接使用原始目录名作为路径。
16
+ *
17
+ * @param fileName - 目录名
18
+ * @param filePath - 目录完整路径
19
+ * @param groupParser - 分组解析器,可选
20
+ * @returns 解析后的路径和选项
21
+ */
22
+ export declare function parseGroupResult(fileName: string, filePath: string, groupParser?: (dirName: string, dirPath: string) => string | {
23
+ path: string;
24
+ options?: PageOptions;
25
+ }): {
26
+ routePath: string;
27
+ options?: PageOptions;
28
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * 解析分组目录的自定义路径和选项
3
+ *
4
+ * 通过 groupParser 解析目录名,支持两种返回格式:
5
+ * 1. 字符串:仅自定义路径
6
+ * 2. 对象:自定义路径 + 路由选项(如 meta、name 等)
7
+ *
8
+ * 未配置 groupParser 时,直接使用原始目录名作为路径。
9
+ *
10
+ * @param fileName - 目录名
11
+ * @param filePath - 目录完整路径
12
+ * @param groupParser - 分组解析器,可选
13
+ * @returns 解析后的路径和选项
14
+ */
15
+ export function parseGroupResult(fileName, filePath, groupParser) {
16
+ if (!groupParser) {
17
+ return { routePath: fileName };
18
+ }
19
+ const result = groupParser(fileName, filePath);
20
+ if (typeof result === 'string') {
21
+ return { routePath: result };
22
+ }
23
+ return { routePath: result.path, options: result.options };
24
+ }
@@ -10,3 +10,4 @@ export * from './pathStrategy.js';
10
10
  export * from '../config/validate.js';
11
11
  export * from './fileReader.js';
12
12
  export * from './findRoute.js';
13
+ export * from './groupParser.js';
@@ -10,3 +10,4 @@ export * from './pathStrategy.js';
10
10
  export * from '../config/validate.js';
11
11
  export * from './fileReader.js';
12
12
  export * from './findRoute.js';
13
+ export * from './groupParser.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vitarx-router",
3
- "version": "4.0.2",
3
+ "version": "4.0.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",