superun-ai-cli 0.1.21 → 0.1.22
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 +15 -0
- package/dist/api/agent-query-api.d.ts +7 -0
- package/dist/api/agent-query-api.js +44 -0
- package/dist/api/agent-query-api.js.map +1 -1
- package/dist/commands/chat/publish.js +15 -2
- package/dist/commands/chat/publish.js.map +1 -1
- package/dist/contracts/cli-output.d.ts +22 -0
- package/dist/contracts/node-wire.d.ts +5 -0
- package/dist/contracts/node-wire.js +1 -0
- package/dist/contracts/node-wire.js.map +1 -1
- package/dist/conversation/next-actions.js +4 -2
- package/dist/conversation/next-actions.js.map +1 -1
- package/dist/conversation/project-routing.d.ts +15 -0
- package/dist/conversation/project-routing.js +85 -0
- package/dist/conversation/project-routing.js.map +1 -0
- package/dist/conversation/route-parser.d.ts +105 -0
- package/dist/conversation/route-parser.js +783 -0
- package/dist/conversation/route-parser.js.map +1 -0
- package/dist/runtime.d.ts +2 -0
- package/dist/runtime.js +29 -14
- package/dist/runtime.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 路由解析工具
|
|
3
|
+
* @author xiuyu.yi
|
|
4
|
+
* 移植自 Glow client/src/pages/project/utils/route-parser.ts,保持同源分组和排序规则。
|
|
5
|
+
*
|
|
6
|
+
* 从 App.tsx / routes.tsx 内容中解析路由信息,支持:
|
|
7
|
+
* - 多端路由分端:JSX 取 handle={{ side }}(结构化优先)/ `// @side:` 注释兜底;data-mode 取 RouteObject 的 handle.side
|
|
8
|
+
* - JSX <Route>(App.tsx)与 RouteObject[](routes.tsx,data-mode/SSR)两种路由源
|
|
9
|
+
* - 嵌套路由 / 扁平路由 / 单组件应用
|
|
10
|
+
*/
|
|
11
|
+
const HOME_SIDE_KEY = '__home__';
|
|
12
|
+
/** 路由配置文件查找顺序:独立 routes.tsx 优先于 App.tsx */
|
|
13
|
+
export const ROUTE_CONFIG_FILENAMES = ['src/routes.tsx', 'src/App.tsx'];
|
|
14
|
+
export function matchesRouteConfigFilename(name, filename) {
|
|
15
|
+
const basename = filename.split('/').pop();
|
|
16
|
+
return (name === filename || name.endsWith(`/${filename}`) || name === basename || name.endsWith(`/${basename}`));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 从 attachments 中查找带 content 的路由配置文件(routes.tsx 优先,其次 App.tsx)
|
|
20
|
+
* 原实现作者:lidewen
|
|
21
|
+
*/
|
|
22
|
+
export function findRouteConfigAttachment(attachments) {
|
|
23
|
+
for (const filename of ROUTE_CONFIG_FILENAMES) {
|
|
24
|
+
const found = attachments.find((att) => matchesRouteConfigFilename(att.name, filename) && att.content);
|
|
25
|
+
if (found)
|
|
26
|
+
return found;
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 规范化 import 路径
|
|
32
|
+
* 将 @/ 或 ./ 转换为 src/,并添加文件扩展名
|
|
33
|
+
*/
|
|
34
|
+
export const normalizeImportPath = (importPath) => {
|
|
35
|
+
let normalized = importPath;
|
|
36
|
+
if (normalized.startsWith('@/')) {
|
|
37
|
+
normalized = normalized.replace('@/', 'src/');
|
|
38
|
+
}
|
|
39
|
+
else if (normalized.startsWith('./')) {
|
|
40
|
+
normalized = normalized.replace('./', 'src/');
|
|
41
|
+
}
|
|
42
|
+
if (!normalized.endsWith('.tsx') && !normalized.endsWith('.ts')) {
|
|
43
|
+
normalized += '.tsx';
|
|
44
|
+
}
|
|
45
|
+
return normalized;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* 判断路由是否为动态路由(包含 :param 动态参数或 * 通配符)
|
|
49
|
+
* 例如:/users/:id -> true, /products/:category/:slug -> true, /about -> false
|
|
50
|
+
*/
|
|
51
|
+
export const isDynamicRoute = (path) => {
|
|
52
|
+
return /:[^/]+/.test(path) || path.includes('*');
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* 将组件名转换为友好的显示名称
|
|
56
|
+
* 例如:UserProfile -> User Profile
|
|
57
|
+
*/
|
|
58
|
+
export const toFriendlyName = (componentName) => {
|
|
59
|
+
return componentName.replace(/([A-Z])/g, ' $1').trim();
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* 解析 import 语句,建立组件名到文件路径的映射
|
|
63
|
+
*/
|
|
64
|
+
export const parseImportMap = (content) => {
|
|
65
|
+
const importMap = {};
|
|
66
|
+
// 匹配默认导入: import Component from '...'
|
|
67
|
+
const defaultImportRegex = /import\s+([^\s{]+)\s+from\s+['"]([^'"]+)['"]/g;
|
|
68
|
+
for (const match of content.matchAll(defaultImportRegex)) {
|
|
69
|
+
const componentName = match[1];
|
|
70
|
+
const importPath = match[2];
|
|
71
|
+
importMap[componentName] = normalizeImportPath(importPath);
|
|
72
|
+
}
|
|
73
|
+
// 匹配命名导入: import { Component1, Component2 } from '...'
|
|
74
|
+
const namedImportRegex = /import\s*{([^}]+)}\s*from\s*['"]([^'"]+)['"]/g;
|
|
75
|
+
for (const namedImportMatch of content.matchAll(namedImportRegex)) {
|
|
76
|
+
const namedImports = namedImportMatch[1];
|
|
77
|
+
const importPath = namedImportMatch[2];
|
|
78
|
+
const normalizedPath = normalizeImportPath(importPath);
|
|
79
|
+
const componentNames = namedImports.split(',').map((name) => name.trim());
|
|
80
|
+
componentNames.forEach((componentName) => {
|
|
81
|
+
const finalName = componentName.includes(' as ')
|
|
82
|
+
? componentName.split(' as ')[1].trim()
|
|
83
|
+
: componentName;
|
|
84
|
+
importMap[finalName] = normalizedPath;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return importMap;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* 从 `fromIndex` 起查找下一个 `<Route` 组件起始位置(使用词边界,避免把 `<Routes` 误当成 `<Route`)。
|
|
91
|
+
* 原实现作者:lidewen
|
|
92
|
+
*/
|
|
93
|
+
function indexOfNextRouteTag(routeBlock, fromIndex) {
|
|
94
|
+
const re = /<Route\b/g;
|
|
95
|
+
re.lastIndex = fromIndex;
|
|
96
|
+
const m = re.exec(routeBlock);
|
|
97
|
+
return m ? m.index : -1;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* 从 `const xxxRoutes = ( ... )` 的路由块推断该端 basePath(对应最外层 Route 的 path)。
|
|
101
|
+
* 只在「第一个 `<Route` 到第二个 `<Route` 之前」的片段里找 path,避免把子路由的 path 误当成父级 base。
|
|
102
|
+
* 若父级为无 path 的 layout(仅 element),则视为挂载在根 `/`。
|
|
103
|
+
* 原实现作者:lidewen
|
|
104
|
+
*/
|
|
105
|
+
export function inferGroupBasePathFromRouteBlock(routeBlock) {
|
|
106
|
+
const absolutePaths = [];
|
|
107
|
+
const pathRegex = /<Route\s+path=["']([^"']+)["']/g;
|
|
108
|
+
for (const match of routeBlock.matchAll(pathRegex)) {
|
|
109
|
+
const routePath = match[1];
|
|
110
|
+
if (routePath.startsWith('/')) {
|
|
111
|
+
absolutePaths.push(routePath);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/*
|
|
115
|
+
* 优先取「作为其他绝对路径前缀的最长 path」作为 layout basePath。
|
|
116
|
+
* 场景:管理后台先声明 /admin/login,再声明带嵌套子路由的 /admin layout,
|
|
117
|
+
* 若仍取第一个 Route 会得到 /admin/login,导致子路由拼错。
|
|
118
|
+
*/
|
|
119
|
+
let layoutBasePath = null;
|
|
120
|
+
for (const candidate of absolutePaths) {
|
|
121
|
+
const prefix = candidate.endsWith('/') ? candidate : `${candidate}/`;
|
|
122
|
+
const hasChildPath = absolutePaths.some((other) => other !== candidate && other.startsWith(prefix));
|
|
123
|
+
if (hasChildPath && (!layoutBasePath || candidate.length > layoutBasePath.length)) {
|
|
124
|
+
layoutBasePath = candidate;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (layoutBasePath) {
|
|
128
|
+
return layoutBasePath;
|
|
129
|
+
}
|
|
130
|
+
const firstIdx = indexOfNextRouteTag(routeBlock, 0);
|
|
131
|
+
if (firstIdx === -1)
|
|
132
|
+
return '/';
|
|
133
|
+
const secondIdx = indexOfNextRouteTag(routeBlock, firstIdx + 1);
|
|
134
|
+
const parentHead = secondIdx === -1 ? routeBlock.slice(firstIdx) : routeBlock.slice(firstIdx, secondIdx);
|
|
135
|
+
const pathMatch = parentHead.match(/path=["']([^"']+)["']/);
|
|
136
|
+
return pathMatch ? pathMatch[1] : '/';
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* 从路由块抽出顶层 <Route> 的 handle={{ side: "X" }} 的 X。
|
|
140
|
+
* 这是结构化的分端标记,比 `// @side:` 注释更抗格式化/重生成;约定里每端顶层 <Route> 带 handle、
|
|
141
|
+
* 子路由继承不重复,故取首个匹配即该组的 side。读不到(无 handle)→ undefined,由调用方回退注释/变量名。
|
|
142
|
+
*/
|
|
143
|
+
export function extractHandleSideFromRouteBlock(routeBlock) {
|
|
144
|
+
const m = routeBlock.match(/handle=\{\{\s*side:\s*["']([^"']+)["']/);
|
|
145
|
+
return m ? m[1] : undefined;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* 从嵌套路由块中解析路由
|
|
149
|
+
*/
|
|
150
|
+
export const parseNestedRoutes = (routeBlock, parentPath, side, importMap) => {
|
|
151
|
+
const routes = [];
|
|
152
|
+
let hasIndexRoute = false;
|
|
153
|
+
let parentLayoutRoute = null;
|
|
154
|
+
// 匹配 index 路由(index 与 element 之间允许夹 handle 等属性): <Route index element={<Component />} />
|
|
155
|
+
const indexRouteRegex = /<Route\s+index\b[^>]*?element=\{[^}]*<([^\s/>]+)[^}]*\}[^/>]*\/?>/g;
|
|
156
|
+
for (const match of routeBlock.matchAll(indexRouteRegex)) {
|
|
157
|
+
const componentName = match[1];
|
|
158
|
+
if (componentName === 'Navigate')
|
|
159
|
+
continue;
|
|
160
|
+
hasIndexRoute = true;
|
|
161
|
+
const codePath = importMap[componentName];
|
|
162
|
+
routes.push({ path: parentPath, name: toFriendlyName(componentName), codePath, side });
|
|
163
|
+
}
|
|
164
|
+
// 匹配带 path 的子路由(path 与 element 之间允许夹 handle 等属性): <Route path="xxx" element={<Component />} />
|
|
165
|
+
const childRouteRegex = /<Route\s+path=["']([^"']+)["'][^>]*?element=\{[^}]*<([^\s/>]+)[^}]*\}[^/>]*\/?>/g;
|
|
166
|
+
for (const match of routeBlock.matchAll(childRouteRegex)) {
|
|
167
|
+
const childPath = match[1];
|
|
168
|
+
const componentName = match[2];
|
|
169
|
+
if (componentName === 'Navigate' || childPath === '*')
|
|
170
|
+
continue;
|
|
171
|
+
// 判断是否为父路由 layout(path 与 parentPath 相同)
|
|
172
|
+
if (childPath === parentPath) {
|
|
173
|
+
// 暂存父路由 layout,后续根据是否有 index 路由决定是否添加
|
|
174
|
+
const codePath = importMap[componentName];
|
|
175
|
+
parentLayoutRoute = { path: parentPath, name: toFriendlyName(componentName), codePath, side };
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
// 拼接完整路径(parent 为根时避免 `//space`)
|
|
179
|
+
const fullPath = childPath.startsWith('/')
|
|
180
|
+
? childPath
|
|
181
|
+
: parentPath === '/' || parentPath === ''
|
|
182
|
+
? `/${childPath}`
|
|
183
|
+
: `${parentPath}/${childPath}`;
|
|
184
|
+
const codePath = importMap[componentName];
|
|
185
|
+
routes.push({ path: fullPath, name: toFriendlyName(componentName), codePath, side });
|
|
186
|
+
}
|
|
187
|
+
// 如果没有 index 路由,但有父 layout,则用父 layout 兜底
|
|
188
|
+
if (!hasIndexRoute && parentLayoutRoute) {
|
|
189
|
+
routes.unshift(parentLayoutRoute);
|
|
190
|
+
}
|
|
191
|
+
return routes;
|
|
192
|
+
};
|
|
193
|
+
/**
|
|
194
|
+
* 从变量名推断 side 名称
|
|
195
|
+
* 例如:adminRoutes -> Admin, userRoutes -> User, workspaceGroup -> Workspace
|
|
196
|
+
*/
|
|
197
|
+
export const inferSideFromVarName = (varName) => {
|
|
198
|
+
// 移除 Routes / Group / Pages / Side 等常见路由组后缀
|
|
199
|
+
const baseName = varName.replace(/(?:Routes|Group|Pages|Side)$/i, '');
|
|
200
|
+
if (!baseName)
|
|
201
|
+
return 'Other';
|
|
202
|
+
// 首字母大写
|
|
203
|
+
return baseName.charAt(0).toUpperCase() + baseName.slice(1).toLowerCase();
|
|
204
|
+
};
|
|
205
|
+
/**
|
|
206
|
+
* 解析扁平路由(兼容旧格式,无 @side 注释)
|
|
207
|
+
*/
|
|
208
|
+
export const parseFlatRoutes = (content, importMap) => {
|
|
209
|
+
const routes = [];
|
|
210
|
+
const navigateRoutes = [];
|
|
211
|
+
// 匹配普通路由(path 与 element 之间允许夹 handle 等属性) <Route path="..." element={<Component />} />
|
|
212
|
+
const routeRegex = /<Route\s+path=["']([^"']+)["'][^>]*?element=\{[^}]*<([^\s/>]+)[^}]*\}[^/>]*\/?>/g;
|
|
213
|
+
for (const match of content.matchAll(routeRegex)) {
|
|
214
|
+
const path = match[1];
|
|
215
|
+
const componentName = match[2];
|
|
216
|
+
if (componentName === 'Navigate' || path === '*')
|
|
217
|
+
continue;
|
|
218
|
+
const codePath = importMap[componentName];
|
|
219
|
+
routes.push({ path, name: toFriendlyName(componentName), codePath, side: 'default' });
|
|
220
|
+
}
|
|
221
|
+
// 匹配 Navigate 重定向(path 与 element 之间允许夹 handle 等属性)
|
|
222
|
+
const navigateRegex = /<Route\s+path=["']([^"']+)["'][^>]*?element=\{[^}]*<Navigate[^>]*to=["']([^"']+)["'][^}]*\}[^/>]*\/?>/g;
|
|
223
|
+
for (const match of content.matchAll(navigateRegex)) {
|
|
224
|
+
const path = match[1];
|
|
225
|
+
const to = match[2];
|
|
226
|
+
navigateRoutes.push({ path, to });
|
|
227
|
+
}
|
|
228
|
+
// 处理 Navigate 路由
|
|
229
|
+
navigateRoutes.forEach(({ to }) => {
|
|
230
|
+
const targetRoute = routes.find((route) => route.path === to);
|
|
231
|
+
const exists = routes.some((r) => r.path === to);
|
|
232
|
+
if (exists)
|
|
233
|
+
return;
|
|
234
|
+
routes.push({
|
|
235
|
+
path: to,
|
|
236
|
+
name: targetRoute?.name || 'Home',
|
|
237
|
+
codePath: targetRoute?.codePath,
|
|
238
|
+
side: 'default',
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
// 如果没有解析到路由,尝试解析单组件应用
|
|
242
|
+
if (routes.length === 0) {
|
|
243
|
+
const returnIndex = content.indexOf('return');
|
|
244
|
+
if (returnIndex !== -1) {
|
|
245
|
+
const afterReturn = content.slice(returnIndex);
|
|
246
|
+
const uppercaseTagRegex = /<\s*([A-Z][A-Za-z0-9_]*)\b[^>]*\/?>(?!\s*\})/g;
|
|
247
|
+
const ignoredComponents = new Set([
|
|
248
|
+
'Routes',
|
|
249
|
+
'Route',
|
|
250
|
+
'Navigate',
|
|
251
|
+
'Fragment',
|
|
252
|
+
'React',
|
|
253
|
+
'React.Fragment',
|
|
254
|
+
]);
|
|
255
|
+
for (const m of afterReturn.matchAll(uppercaseTagRegex)) {
|
|
256
|
+
const candidate = m[1];
|
|
257
|
+
if (ignoredComponents.has(candidate))
|
|
258
|
+
continue;
|
|
259
|
+
const codePath = importMap[candidate];
|
|
260
|
+
const friendlyName = toFriendlyName(candidate);
|
|
261
|
+
routes.push({ path: '/', name: friendlyName || 'Home', codePath, side: 'default' });
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return routes;
|
|
267
|
+
};
|
|
268
|
+
/**
|
|
269
|
+
* 从 App.tsx 内容中解析路由分组
|
|
270
|
+
*
|
|
271
|
+
* 解析优先级:
|
|
272
|
+
* 1. 带 @side 注释的路由组
|
|
273
|
+
* 2. 缺少注释的 xxxRoutes 变量
|
|
274
|
+
* 3. 扁平路由
|
|
275
|
+
* 4. 默认路由
|
|
276
|
+
*/
|
|
277
|
+
export const parseRouteGroupsFromAppTsx = (content, onFallback) => {
|
|
278
|
+
const groups = [];
|
|
279
|
+
const importMap = parseImportMap(content);
|
|
280
|
+
const parsedVarNames = new Set(); // 记录已解析的变量名
|
|
281
|
+
try {
|
|
282
|
+
// 第一步:匹配带 @side 注释的路由组
|
|
283
|
+
// 变量名不限后缀:只要紧邻 `// @side:` 注释即视为路由组(AI 生成的变量名可能是 xxxGroup/xxxSide 等)。
|
|
284
|
+
const routeGroupRegex = /\/\/\s*@side:\s*(\S+).*\n\s*const\s+(\w+)\s*=\s*(?:\(\s*([\s\S]*?)\n\)|([\s\S]*?));/g;
|
|
285
|
+
for (const match of content.matchAll(routeGroupRegex)) {
|
|
286
|
+
const commentSide = match[1];
|
|
287
|
+
const varName = match[2];
|
|
288
|
+
// match[3] 是带括号格式的内容,match[4] 是不带括号格式的内容
|
|
289
|
+
const routeBlock = (match[3] || match[4]);
|
|
290
|
+
parsedVarNames.add(varName); // 记录已解析的变量名
|
|
291
|
+
// 结构化 handle={{ side }} 优先(抗格式化/重生成),// @side: 注释兜底
|
|
292
|
+
const side = extractHandleSideFromRouteBlock(routeBlock) || commentSide;
|
|
293
|
+
const basePath = inferGroupBasePathFromRouteBlock(routeBlock);
|
|
294
|
+
// 解析嵌套路由
|
|
295
|
+
const routes = parseNestedRoutes(routeBlock, basePath, side, importMap);
|
|
296
|
+
if (routes.length > 0) {
|
|
297
|
+
groups.push({
|
|
298
|
+
side,
|
|
299
|
+
label: side,
|
|
300
|
+
basePath,
|
|
301
|
+
routes,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// 第二步:兜底 - 匹配所有 const xxx = (...) 括号赋值,找出缺少 @side 注释的路由组。
|
|
306
|
+
// 变量名不限后缀(AI 可能命名 xxxGroup 等),改由「块内确实含 <Route」来判定是否路由组,
|
|
307
|
+
// 避免误伤 const App = (...) / const router = (...) 这类同样用括号包裹的普通常量。
|
|
308
|
+
const allRouteVarsRegex = /const\s+(\w+)\s*=\s*\(\s*([\s\S]*?)\n\);/g;
|
|
309
|
+
let otherCounter = 0;
|
|
310
|
+
for (const match of content.matchAll(allRouteVarsRegex)) {
|
|
311
|
+
const varName = match[1];
|
|
312
|
+
const routeBlock = match[2];
|
|
313
|
+
// 跳过已解析的变量
|
|
314
|
+
if (parsedVarNames.has(varName)) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
// fallback 捕获的是 `const xxx = (...)` 的括号体:只有 RHS 自身以 JSX Route/Fragment 开头,
|
|
318
|
+
// 才能认为它是一个路由组。避免把 `const Foo = () => { ... }` 的形参括号当成路由组起点,
|
|
319
|
+
// 进而跨块吞到后面的真实路由组并生成重复端(如“采集后台1”)。
|
|
320
|
+
const normalizedRouteBlock = routeBlock.trimStart();
|
|
321
|
+
const startsWithRouteJsx = /^<Route\b/.test(normalizedRouteBlock);
|
|
322
|
+
const startsWithFragmentJsx = /^<(?:>|Fragment\b|React\.Fragment\b)/.test(normalizedRouteBlock);
|
|
323
|
+
if ((!startsWithRouteJsx && !startsWithFragmentJsx) || !/<Route\b/.test(routeBlock)) {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
parsedVarNames.add(varName);
|
|
327
|
+
// 结构化 handle={{ side }} 优先,否则从变量名推断
|
|
328
|
+
let inferredSide = extractHandleSideFromRouteBlock(routeBlock) || inferSideFromVarName(varName);
|
|
329
|
+
// 检查推断的 side 是否已存在,若存在则添加序号
|
|
330
|
+
const existingSides = new Set(groups.map((g) => g.side));
|
|
331
|
+
if (existingSides.has(inferredSide)) {
|
|
332
|
+
otherCounter++;
|
|
333
|
+
inferredSide = `${inferredSide}${otherCounter}`;
|
|
334
|
+
}
|
|
335
|
+
const basePath = inferGroupBasePathFromRouteBlock(routeBlock);
|
|
336
|
+
// 解析嵌套路由
|
|
337
|
+
const routes = parseNestedRoutes(routeBlock, basePath, inferredSide, importMap);
|
|
338
|
+
if (routes.length > 0) {
|
|
339
|
+
groups.push({
|
|
340
|
+
side: inferredSide,
|
|
341
|
+
label: inferredSide,
|
|
342
|
+
basePath,
|
|
343
|
+
routes,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// 第 2.5 步:检测独立 index 路由(主页端)
|
|
348
|
+
// 当已存在其他端时,查找不属于任何 xxxRoutes 变量的根级 index 路由
|
|
349
|
+
if (groups.length > 0) {
|
|
350
|
+
const capturedCodePaths = new Set();
|
|
351
|
+
const capturedNames = new Set();
|
|
352
|
+
for (const g of groups) {
|
|
353
|
+
for (const r of g.routes) {
|
|
354
|
+
if (r.codePath)
|
|
355
|
+
capturedCodePaths.add(r.codePath);
|
|
356
|
+
capturedNames.add(r.name);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const standaloneIndexRegex = /<Route\s+index\s+element=\{[^}]*<([^\s/>]+)[^}]*\}[^/>]*\/?>/g;
|
|
360
|
+
for (const match of content.matchAll(standaloneIndexRegex)) {
|
|
361
|
+
const componentName = match[1];
|
|
362
|
+
if (componentName === 'Navigate')
|
|
363
|
+
continue;
|
|
364
|
+
const codePath = importMap[componentName];
|
|
365
|
+
const friendlyName = toFriendlyName(componentName);
|
|
366
|
+
if ((codePath && capturedCodePaths.has(codePath)) || capturedNames.has(friendlyName))
|
|
367
|
+
continue;
|
|
368
|
+
groups.unshift({
|
|
369
|
+
side: HOME_SIDE_KEY,
|
|
370
|
+
label: '主页端',
|
|
371
|
+
basePath: '/',
|
|
372
|
+
routes: [{ path: '/', name: friendlyName, codePath, side: HOME_SIDE_KEY }],
|
|
373
|
+
});
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
// 第三步:如果没有找到任何路由组,回退到解析普通路由
|
|
378
|
+
if (groups.length === 0) {
|
|
379
|
+
const fallbackRoutes = parseFlatRoutes(content, importMap);
|
|
380
|
+
if (fallbackRoutes.length > 0) {
|
|
381
|
+
groups.push({
|
|
382
|
+
side: 'default',
|
|
383
|
+
label: 'Routes',
|
|
384
|
+
basePath: '/',
|
|
385
|
+
routes: fallbackRoutes,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// 第四步:如果还是没有路由,提供默认路由
|
|
390
|
+
if (groups.length === 0) {
|
|
391
|
+
onFallback?.();
|
|
392
|
+
groups.push({
|
|
393
|
+
side: 'default',
|
|
394
|
+
label: 'Routes',
|
|
395
|
+
basePath: '/',
|
|
396
|
+
routes: [{ path: '/', name: 'Home', side: 'default' }],
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
onFallback?.();
|
|
402
|
+
groups.push({
|
|
403
|
+
side: 'default',
|
|
404
|
+
label: 'Routes',
|
|
405
|
+
basePath: '/',
|
|
406
|
+
routes: [{ path: '/', name: 'Home', side: 'default' }],
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
return groups;
|
|
410
|
+
};
|
|
411
|
+
/** 从 i 起跳过空白与 // 行注释 / 块注释,返回新下标 */
|
|
412
|
+
function skipTrivia(s, i) {
|
|
413
|
+
for (;;) {
|
|
414
|
+
const c = s[i];
|
|
415
|
+
if (c === ' ' || c === '\t' || c === '\n' || c === '\r') {
|
|
416
|
+
i++;
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
if (c === '/' && s[i + 1] === '/') {
|
|
420
|
+
i += 2;
|
|
421
|
+
while (i < s.length && s[i] !== '\n')
|
|
422
|
+
i++;
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (c === '/' && s[i + 1] === '*') {
|
|
426
|
+
i += 2;
|
|
427
|
+
while (i < s.length && !(s[i] === '*' && s[i + 1] === '/'))
|
|
428
|
+
i++;
|
|
429
|
+
i += 2;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
return i;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
/** s[i] 为引号(' " `),跳到匹配结束引号之后;模板串内 ${...} 做花括号配平 */
|
|
436
|
+
function skipString(s, i) {
|
|
437
|
+
const quote = s[i];
|
|
438
|
+
i++;
|
|
439
|
+
while (i < s.length) {
|
|
440
|
+
const c = s[i];
|
|
441
|
+
if (c === '\\') {
|
|
442
|
+
i += 2;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (quote === '`' && c === '$' && s[i + 1] === '{') {
|
|
446
|
+
i = skipBalanced(s, i + 1);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (c === quote)
|
|
450
|
+
return i + 1;
|
|
451
|
+
i++;
|
|
452
|
+
}
|
|
453
|
+
return i;
|
|
454
|
+
}
|
|
455
|
+
/** s[i] 为 ( [ {,跳到匹配闭合之后(内部字符串/注释/嵌套都正确跳过) */
|
|
456
|
+
function skipBalanced(s, i) {
|
|
457
|
+
const open = s[i];
|
|
458
|
+
const close = open === '(' ? ')' : open === '[' ? ']' : '}';
|
|
459
|
+
let depth = 0;
|
|
460
|
+
while (i < s.length) {
|
|
461
|
+
const c = s[i];
|
|
462
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
463
|
+
i = skipString(s, i);
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (c === '/' && (s[i + 1] === '/' || s[i + 1] === '*')) {
|
|
467
|
+
i = skipTrivia(s, i);
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (c === open) {
|
|
471
|
+
depth++;
|
|
472
|
+
i++;
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
if (c === close) {
|
|
476
|
+
depth--;
|
|
477
|
+
i++;
|
|
478
|
+
if (depth === 0)
|
|
479
|
+
return i;
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
i++;
|
|
483
|
+
}
|
|
484
|
+
return i;
|
|
485
|
+
}
|
|
486
|
+
/** 从 i 起跳过一个属性值,停在顶层 `,` 或对象闭合 `}` 处(返回该下标)。(){}[]、字符串、注释配平。 */
|
|
487
|
+
function skipValue(s, i) {
|
|
488
|
+
while (i < s.length) {
|
|
489
|
+
const c = s[i];
|
|
490
|
+
if (c === ',' || c === '}')
|
|
491
|
+
return i;
|
|
492
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
493
|
+
i = skipString(s, i);
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (c === '(' || c === '[' || c === '{') {
|
|
497
|
+
i = skipBalanced(s, i);
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
if (c === '/' && (s[i + 1] === '/' || s[i + 1] === '*')) {
|
|
501
|
+
i = skipTrivia(s, i);
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
i++;
|
|
505
|
+
}
|
|
506
|
+
return i;
|
|
507
|
+
}
|
|
508
|
+
/** s[i] 为 '{',解析为 RouteObjectNode(只关心路由相关键) */
|
|
509
|
+
function parseRouteObjectAt(s, i) {
|
|
510
|
+
const node = { index: false, hasChildren: false, children: [] };
|
|
511
|
+
i++; // 跳过起始花括号
|
|
512
|
+
for (;;) {
|
|
513
|
+
i = skipTrivia(s, i);
|
|
514
|
+
if (i >= s.length || s[i] === '}') {
|
|
515
|
+
i++;
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
if (s[i] === ',') {
|
|
519
|
+
i++;
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
if (s[i] === '.' && s[i + 1] === '.' && s[i + 2] === '.') {
|
|
523
|
+
i = skipValue(s, i); // 展开 ...x
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
// key:标识符 或 带引号字符串
|
|
527
|
+
let key = '';
|
|
528
|
+
if (s[i] === '"' || s[i] === "'") {
|
|
529
|
+
const ke = skipString(s, i);
|
|
530
|
+
key = s.slice(i + 1, ke - 1);
|
|
531
|
+
i = ke;
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
const m = /^[A-Za-z0-9_$]+/.exec(s.slice(i));
|
|
535
|
+
if (m) {
|
|
536
|
+
key = m[0];
|
|
537
|
+
i += m[0].length;
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
i++;
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
i = skipTrivia(s, i);
|
|
545
|
+
let valueText = '';
|
|
546
|
+
let arrayStart = -1;
|
|
547
|
+
if (s[i] === ':') {
|
|
548
|
+
i++;
|
|
549
|
+
i = skipTrivia(s, i);
|
|
550
|
+
const vs = i;
|
|
551
|
+
if (s[i] === '[')
|
|
552
|
+
arrayStart = i;
|
|
553
|
+
const ve = skipValue(s, i);
|
|
554
|
+
valueText = s.slice(vs, ve);
|
|
555
|
+
i = ve;
|
|
556
|
+
}
|
|
557
|
+
if (key === 'path') {
|
|
558
|
+
const sm = /^\s*['"]([^'"]*)['"]/.exec(valueText);
|
|
559
|
+
if (sm)
|
|
560
|
+
node.path = sm[1];
|
|
561
|
+
}
|
|
562
|
+
else if (key === 'index') {
|
|
563
|
+
node.index = /^\s*true\b/.test(valueText);
|
|
564
|
+
}
|
|
565
|
+
else if (key === 'handle') {
|
|
566
|
+
const sm = /side\s*:\s*['"]([^'"]+)['"]/.exec(valueText);
|
|
567
|
+
if (sm)
|
|
568
|
+
node.side = sm[1];
|
|
569
|
+
}
|
|
570
|
+
else if (key === 'element' || key === 'Component') {
|
|
571
|
+
const nm = /^\s*<\s*([A-Za-z_$][\w.]*)/.exec(valueText);
|
|
572
|
+
if (nm)
|
|
573
|
+
node.elementName = nm[1];
|
|
574
|
+
}
|
|
575
|
+
else if (key === 'children' && arrayStart >= 0) {
|
|
576
|
+
node.children = parseRouteObjectArray(s.slice(arrayStart));
|
|
577
|
+
node.hasChildren = node.children.length > 0;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return { node, end: i };
|
|
581
|
+
}
|
|
582
|
+
/** 文本以 '[' 开头(可有前导空白),解析其中顶层对象为 RouteObjectNode[],到匹配 ']' 即止 */
|
|
583
|
+
function parseRouteObjectArray(s) {
|
|
584
|
+
const nodes = [];
|
|
585
|
+
let i = skipTrivia(s, 0);
|
|
586
|
+
if (s[i] !== '[')
|
|
587
|
+
return nodes;
|
|
588
|
+
i++;
|
|
589
|
+
for (;;) {
|
|
590
|
+
i = skipTrivia(s, i);
|
|
591
|
+
if (i >= s.length || s[i] === ']')
|
|
592
|
+
break;
|
|
593
|
+
if (s[i] === ',') {
|
|
594
|
+
i++;
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
if (s[i] === '{') {
|
|
598
|
+
const { node, end } = parseRouteObjectAt(s, i);
|
|
599
|
+
nodes.push(node);
|
|
600
|
+
i = end;
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
i = skipValue(s, i); // 非对象元素(展开/变量引用)跳过
|
|
604
|
+
}
|
|
605
|
+
return nodes;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* 收集文件里所有「路由数组 const」声明(含外层 [])及其紧邻的 `// @side:` 注释。
|
|
609
|
+
* 覆盖两种 data-mode 写法:① 单一 `export const routes = [...]`;
|
|
610
|
+
* ② 多端分写——`// @side: X` + `const xxxRoutes: RouteObject[] = [...]`,再由 `export const routes` 用 spread 拼装
|
|
611
|
+
* (spread 引用的源数组本身就是这里的某个 const,直接解析它即可,无需展开 spread)。
|
|
612
|
+
* 跳过解构 `const [a] = ...` 与非数组 `const f = () => {}`(`=\s*\[` 把关)。
|
|
613
|
+
*/
|
|
614
|
+
function findRouteArrayConsts(content) {
|
|
615
|
+
const out = [];
|
|
616
|
+
const declRe = /(?:\/\/\s*@side:\s*(\S+)[^\n]*\r?\n\s*)?(?:export\s+)?const\s+\w+\s*(?::[^=\n]*)?=\s*\[/g;
|
|
617
|
+
let m;
|
|
618
|
+
while ((m = declRe.exec(content)) !== null) {
|
|
619
|
+
const start = m.index + m[0].length - 1; // 指向数组的 '['
|
|
620
|
+
const end = skipBalanced(content, start);
|
|
621
|
+
out.push({ side: m[1], arrayText: content.slice(start, end) });
|
|
622
|
+
declRe.lastIndex = end; // 跳过整个数组体,避免匹配到数组内部嵌套的 const
|
|
623
|
+
}
|
|
624
|
+
return out;
|
|
625
|
+
}
|
|
626
|
+
function joinRoutePath(parent, child) {
|
|
627
|
+
const p = (parent || '').replace(/\/+$/, '');
|
|
628
|
+
const c = (child || '').replace(/^\/+/, '');
|
|
629
|
+
const joined = `${p}/${c}`.replace(/\/{2,}/g, '/');
|
|
630
|
+
return joined === '' ? '/' : joined;
|
|
631
|
+
}
|
|
632
|
+
function normalizeRoutePath(p) {
|
|
633
|
+
if (!p)
|
|
634
|
+
return '/';
|
|
635
|
+
const n = `/${p}`.replace(/\/{2,}/g, '/').replace(/\/+$/, '');
|
|
636
|
+
return n === '' ? '/' : n;
|
|
637
|
+
}
|
|
638
|
+
function lastSegmentHasExt(p) {
|
|
639
|
+
const last = (p || '').split('/').pop() || '';
|
|
640
|
+
return /\.[^/]+$/.test(last);
|
|
641
|
+
}
|
|
642
|
+
/** 可导航具体页面:以 / 开头、无通配 *、无动态段 :x、末段无文件扩展名 */
|
|
643
|
+
function isNavigablePath(p) {
|
|
644
|
+
return p.startsWith('/') && !p.includes('*') && !/(^|\/):[^/]+/.test(p) && !lastSegmentHasExt(p);
|
|
645
|
+
}
|
|
646
|
+
/** 无组件名时按路径末段派生展示名:'/' -> 'Home','/order-history' -> 'Order History' */
|
|
647
|
+
function pathToDisplayName(p) {
|
|
648
|
+
if (!p || p === '/')
|
|
649
|
+
return 'Home';
|
|
650
|
+
const seg = p.split('/').filter(Boolean).pop() || '';
|
|
651
|
+
const words = seg
|
|
652
|
+
.replace(/^[:*]/, '')
|
|
653
|
+
.split(/[-_\s]+/)
|
|
654
|
+
.filter(Boolean)
|
|
655
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1));
|
|
656
|
+
return words.join(' ') || 'Home';
|
|
657
|
+
}
|
|
658
|
+
/** DFS 收集可导航路由:有 index 子路由的节点不重复吐自身 path(由 index 提供该路径) */
|
|
659
|
+
function collectRouteEntries(nodes, parentPath, parentSide, out) {
|
|
660
|
+
for (const node of nodes) {
|
|
661
|
+
const side = node.side || parentSide;
|
|
662
|
+
const fullPath = node.path != null ? joinRoutePath(parentPath, node.path) : parentPath;
|
|
663
|
+
const isRedirect = node.elementName === 'Navigate';
|
|
664
|
+
const nameFrom = (p) => node.elementName && !isRedirect ? toFriendlyName(node.elementName) : pathToDisplayName(p);
|
|
665
|
+
const hasIndexChild = node.children.some((c) => c.index);
|
|
666
|
+
if (node.path != null && !hasIndexChild) {
|
|
667
|
+
const np = normalizeRoutePath(fullPath);
|
|
668
|
+
if (!isRedirect && isNavigablePath(np))
|
|
669
|
+
out.push({ path: np, name: nameFrom(np), side });
|
|
670
|
+
}
|
|
671
|
+
else if (node.path == null && node.index && parentPath) {
|
|
672
|
+
const np = normalizeRoutePath(parentPath);
|
|
673
|
+
if (!isRedirect && isNavigablePath(np))
|
|
674
|
+
out.push({ path: np, name: nameFrom(np), side });
|
|
675
|
+
}
|
|
676
|
+
if (node.children.length)
|
|
677
|
+
collectRouteEntries(node.children, fullPath, side, out);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
/** 多端分组时推断该端 basePath:各路径最长公共段前缀,没有则 '/' */
|
|
681
|
+
function inferBasePathFromPaths(paths) {
|
|
682
|
+
const segLists = paths.map((p) => p.split('/').filter(Boolean));
|
|
683
|
+
if (segLists.length === 0)
|
|
684
|
+
return '/';
|
|
685
|
+
const first = segLists[0];
|
|
686
|
+
const common = [];
|
|
687
|
+
for (let idx = 0; idx < first.length; idx++) {
|
|
688
|
+
const seg = first[idx];
|
|
689
|
+
if (segLists.every((s) => s[idx] === seg))
|
|
690
|
+
common.push(seg);
|
|
691
|
+
else
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
return common.length ? '/' + common.join('/') : '/';
|
|
695
|
+
}
|
|
696
|
+
function entriesToGroups(entries) {
|
|
697
|
+
if (entries.length === 0)
|
|
698
|
+
return [];
|
|
699
|
+
// 按 path 去重(首条优先;后到的若带 side 而原条没有则补上 side)
|
|
700
|
+
const byPath = new Map();
|
|
701
|
+
for (const e of entries) {
|
|
702
|
+
const prev = byPath.get(e.path);
|
|
703
|
+
if (!prev)
|
|
704
|
+
byPath.set(e.path, e);
|
|
705
|
+
else if (!prev.side && e.side)
|
|
706
|
+
byPath.set(e.path, e);
|
|
707
|
+
}
|
|
708
|
+
const list = Array.from(byPath.values());
|
|
709
|
+
const distinctSides = Array.from(new Set(list.map((e) => e.side).filter((s) => !!s)));
|
|
710
|
+
// 无 side / 单端 → 单个 default 组(label 'Routes',地址栏不显示侧边切换)
|
|
711
|
+
if (distinctSides.length <= 1) {
|
|
712
|
+
return [
|
|
713
|
+
{
|
|
714
|
+
side: 'default',
|
|
715
|
+
label: 'Routes',
|
|
716
|
+
basePath: '/',
|
|
717
|
+
routes: list.map((e) => ({ path: e.path, name: e.name, side: 'default' })),
|
|
718
|
+
},
|
|
719
|
+
];
|
|
720
|
+
}
|
|
721
|
+
const groups = [];
|
|
722
|
+
for (const side of distinctSides) {
|
|
723
|
+
const es = list.filter((e) => e.side === side);
|
|
724
|
+
groups.push({
|
|
725
|
+
side,
|
|
726
|
+
label: side,
|
|
727
|
+
basePath: inferBasePathFromPaths(es.map((e) => e.path)),
|
|
728
|
+
routes: es.map((e) => ({ path: e.path, name: e.name, side })),
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
return groups;
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* 从 routes.tsx 的 RouteObject[] 解析路由分组(data-mode / SSR 项目)。
|
|
735
|
+
* 解析不出(非 RouteObject、无 routes 数组、异常)→ 返回 [],交回上层兜底。
|
|
736
|
+
*/
|
|
737
|
+
export const parseRouteGroupsFromRouteObjects = (content) => {
|
|
738
|
+
try {
|
|
739
|
+
const arrays = findRouteArrayConsts(content);
|
|
740
|
+
if (arrays.length === 0)
|
|
741
|
+
return [];
|
|
742
|
+
const entries = [];
|
|
743
|
+
for (const { side, arrayText } of arrays) {
|
|
744
|
+
const nodes = parseRouteObjectArray(arrayText);
|
|
745
|
+
// 注释 side 作为 parentSide(节点自带 handle.side 时仍以 handle 为先,见 collectRouteEntries)
|
|
746
|
+
collectRouteEntries(nodes, '', side, entries);
|
|
747
|
+
}
|
|
748
|
+
if (entries.length === 0)
|
|
749
|
+
return [];
|
|
750
|
+
return entriesToGroups(entries);
|
|
751
|
+
}
|
|
752
|
+
catch (error) {
|
|
753
|
+
return [];
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
/**
|
|
757
|
+
* 路由分组统一入口(自动分发):
|
|
758
|
+
* - 含 JSX `<Route>` → 走 parseRouteGroupsFromAppTsx(CSR / 原型项目)
|
|
759
|
+
* - 否则尝试 RouteObject[] 解析(data-mode / SSR 的 routes.tsx)
|
|
760
|
+
* - 都解析不出 → 回到 parseRouteGroupsFromAppTsx 的兜底(默认单条 /)
|
|
761
|
+
*/
|
|
762
|
+
export const parseRouteGroups = (content, onFallback) => {
|
|
763
|
+
if (/<Route\b/.test(content))
|
|
764
|
+
return parseRouteGroupsFromAppTsx(content, onFallback);
|
|
765
|
+
const groups = parseRouteGroupsFromRouteObjects(content);
|
|
766
|
+
if (groups.length > 0)
|
|
767
|
+
return groups;
|
|
768
|
+
return parseRouteGroupsFromAppTsx(content, onFallback);
|
|
769
|
+
};
|
|
770
|
+
/**
|
|
771
|
+
* 创建默认路由分组
|
|
772
|
+
*/
|
|
773
|
+
export const createDefaultRouteGroups = () => {
|
|
774
|
+
return [
|
|
775
|
+
{
|
|
776
|
+
side: 'default',
|
|
777
|
+
label: 'Routes',
|
|
778
|
+
basePath: '/',
|
|
779
|
+
routes: [{ path: '/', name: 'Home', side: 'default' }],
|
|
780
|
+
},
|
|
781
|
+
];
|
|
782
|
+
};
|
|
783
|
+
//# sourceMappingURL=route-parser.js.map
|