@ubean/vue 0.2.0
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/LICENSE +21 -0
- package/README.md +493 -0
- package/README.zh-CN.md +500 -0
- package/dist/generator/index.d.ts +125 -0
- package/dist/generator/index.js +369 -0
- package/dist/index.d.ts +782 -0
- package/dist/index.js +1073 -0
- package/dist/types-VHF1RJu2.d.ts +132 -0
- package/dist/vite.d.ts +164 -0
- package/dist/vite.js +1189 -0
- package/package.json +67 -0
package/dist/vite.js
ADDED
|
@@ -0,0 +1,1189 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, extname, isAbsolute, join, relative } from "pathe";
|
|
3
|
+
import { glob } from "tinyglobby";
|
|
4
|
+
import { pascalCase } from "scule";
|
|
5
|
+
import { withBase, withLeadingSlash, withoutTrailingSlash } from "ufo";
|
|
6
|
+
//#region src/extract-page.ts
|
|
7
|
+
/**
|
|
8
|
+
* `definePage(...)` 宏参数提取器(构建期,零 AST 依赖的括号平衡扫描)。
|
|
9
|
+
*
|
|
10
|
+
* 所有权:`@ubean/vue`;`@ubean/scan` re-export `extractDefinePage*` 保持
|
|
11
|
+
* 向后兼容,其服务端 `defineHandlerMeta` 提取复用下方导出的通用
|
|
12
|
+
* `extractCallObject`。
|
|
13
|
+
*/
|
|
14
|
+
function extractScriptContent(code) {
|
|
15
|
+
if (!code.includes("<script")) return code;
|
|
16
|
+
let scriptContent = "";
|
|
17
|
+
const scriptRegex = /<script([^>]*)>([\s\S]*?)<\/script>/g;
|
|
18
|
+
let match;
|
|
19
|
+
while ((match = scriptRegex.exec(code)) !== null) {
|
|
20
|
+
const attrs = match[1] || "";
|
|
21
|
+
const content = match[2] || "";
|
|
22
|
+
if (attrs.includes("setup")) return content;
|
|
23
|
+
scriptContent += `${content}\n`;
|
|
24
|
+
}
|
|
25
|
+
return scriptContent || null;
|
|
26
|
+
}
|
|
27
|
+
function findBalancedCall(code, funcName) {
|
|
28
|
+
const callPattern = new RegExp(`\\b${funcName}\\s*\\(`, "g");
|
|
29
|
+
let match;
|
|
30
|
+
while ((match = callPattern.exec(code)) !== null) {
|
|
31
|
+
const lineStart = code.lastIndexOf("\n", match.index) + 1;
|
|
32
|
+
if (code.slice(lineStart, match.index).includes("//")) continue;
|
|
33
|
+
const startIdx = match.index + match[0].length;
|
|
34
|
+
let depth = 1;
|
|
35
|
+
let i = startIdx;
|
|
36
|
+
let inString = null;
|
|
37
|
+
let escaped = false;
|
|
38
|
+
while (i < code.length && depth > 0) {
|
|
39
|
+
const ch = code[i];
|
|
40
|
+
if (escaped) {
|
|
41
|
+
escaped = false;
|
|
42
|
+
i++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (ch === "\\") {
|
|
46
|
+
escaped = true;
|
|
47
|
+
i++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (inString) {
|
|
51
|
+
if (ch === inString) inString = null;
|
|
52
|
+
i++;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (ch === "\"" || ch === "'" || ch === "`") {
|
|
56
|
+
inString = ch;
|
|
57
|
+
i++;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (ch === "(" || ch === "{" || ch === "[") depth++;
|
|
61
|
+
else if (ch === ")" || ch === "}" || ch === "]") {
|
|
62
|
+
depth--;
|
|
63
|
+
if (depth === 0 && ch === ")") return code.slice(startIdx, i);
|
|
64
|
+
}
|
|
65
|
+
i++;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
function skipWhitespace(code, pos) {
|
|
71
|
+
while (pos < code.length && /\s/.test(code[pos])) pos++;
|
|
72
|
+
return pos;
|
|
73
|
+
}
|
|
74
|
+
function parseStringValue(code, pos) {
|
|
75
|
+
const quote = code[pos];
|
|
76
|
+
if (quote !== "\"" && quote !== "'" && quote !== "`") return null;
|
|
77
|
+
pos++;
|
|
78
|
+
let value = "";
|
|
79
|
+
let escaped = false;
|
|
80
|
+
while (pos < code.length) {
|
|
81
|
+
const ch = code[pos];
|
|
82
|
+
if (escaped) {
|
|
83
|
+
value += ch;
|
|
84
|
+
escaped = false;
|
|
85
|
+
pos++;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (ch === "\\") {
|
|
89
|
+
escaped = true;
|
|
90
|
+
pos++;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (ch === quote) {
|
|
94
|
+
if (quote === "`") {
|
|
95
|
+
value += ch;
|
|
96
|
+
pos++;
|
|
97
|
+
if (pos >= code.length || code[pos] !== "`") {
|
|
98
|
+
pos--;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
pos++;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
value += ch;
|
|
107
|
+
pos++;
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
value,
|
|
111
|
+
pos
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function parseIdentifier(code, pos) {
|
|
115
|
+
const start = pos;
|
|
116
|
+
while (pos < code.length && /[\w$]/.test(code[pos])) pos++;
|
|
117
|
+
if (pos === start) return null;
|
|
118
|
+
return {
|
|
119
|
+
name: code.slice(start, pos),
|
|
120
|
+
pos
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function parseValue(code, pos) {
|
|
124
|
+
pos = skipWhitespace(code, pos);
|
|
125
|
+
if (pos >= code.length) return null;
|
|
126
|
+
const ch = code[pos];
|
|
127
|
+
if (ch === "\"" || ch === "'" || ch === "`") {
|
|
128
|
+
const str = parseStringValue(code, pos);
|
|
129
|
+
if (str) return {
|
|
130
|
+
value: str.value,
|
|
131
|
+
pos: str.pos
|
|
132
|
+
};
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
if (ch === "{") return parseObjectValue(code, pos);
|
|
136
|
+
if (ch === "[") return parseArrayValue(code, pos);
|
|
137
|
+
if (ch === "t" && code.slice(pos, pos + 4) === "true") return {
|
|
138
|
+
value: true,
|
|
139
|
+
pos: pos + 4
|
|
140
|
+
};
|
|
141
|
+
if (ch === "f" && code.slice(pos, pos + 5) === "false") return {
|
|
142
|
+
value: false,
|
|
143
|
+
pos: pos + 5
|
|
144
|
+
};
|
|
145
|
+
if (ch === "n" && code.slice(pos, pos + 4) === "null") return {
|
|
146
|
+
value: null,
|
|
147
|
+
pos: pos + 4
|
|
148
|
+
};
|
|
149
|
+
const numMatch = code.slice(pos).match(/^-?\d+\.?\d*(?:[eE][+-]?\d+)?/);
|
|
150
|
+
if (numMatch) return {
|
|
151
|
+
value: Number(numMatch[0]),
|
|
152
|
+
pos: pos + numMatch[0].length
|
|
153
|
+
};
|
|
154
|
+
const ident = parseIdentifier(code, pos);
|
|
155
|
+
if (ident) return {
|
|
156
|
+
value: ident.name,
|
|
157
|
+
pos: ident.pos
|
|
158
|
+
};
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
function parseObjectValue(code, pos) {
|
|
162
|
+
if (code[pos] !== "{") return null;
|
|
163
|
+
pos++;
|
|
164
|
+
const result = {};
|
|
165
|
+
pos = skipWhitespace(code, pos);
|
|
166
|
+
if (code[pos] === "}") return {
|
|
167
|
+
value: result,
|
|
168
|
+
pos: pos + 1
|
|
169
|
+
};
|
|
170
|
+
while (pos < code.length) {
|
|
171
|
+
pos = skipWhitespace(code, pos);
|
|
172
|
+
let key = null;
|
|
173
|
+
if (code[pos] === "\"" || code[pos] === "'") {
|
|
174
|
+
const keyStr = parseStringValue(code, pos);
|
|
175
|
+
if (keyStr) {
|
|
176
|
+
key = keyStr.value;
|
|
177
|
+
pos = keyStr.pos;
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
const ident = parseIdentifier(code, pos);
|
|
181
|
+
if (ident) {
|
|
182
|
+
key = ident.name;
|
|
183
|
+
pos = ident.pos;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (!key) break;
|
|
187
|
+
pos = skipWhitespace(code, pos);
|
|
188
|
+
if (code[pos] !== ":") break;
|
|
189
|
+
pos++;
|
|
190
|
+
pos = skipWhitespace(code, pos);
|
|
191
|
+
const val = parseValue(code, pos);
|
|
192
|
+
if (val) {
|
|
193
|
+
result[key] = val.value;
|
|
194
|
+
pos = val.pos;
|
|
195
|
+
}
|
|
196
|
+
pos = skipWhitespace(code, pos);
|
|
197
|
+
if (code[pos] === ",") {
|
|
198
|
+
pos++;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (code[pos] === "}") {
|
|
202
|
+
pos++;
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
value: result,
|
|
209
|
+
pos
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function parseArrayValue(code, pos) {
|
|
213
|
+
if (code[pos] !== "[") return null;
|
|
214
|
+
pos++;
|
|
215
|
+
const result = [];
|
|
216
|
+
pos = skipWhitespace(code, pos);
|
|
217
|
+
if (code[pos] === "]") return {
|
|
218
|
+
value: result,
|
|
219
|
+
pos: pos + 1
|
|
220
|
+
};
|
|
221
|
+
while (pos < code.length) {
|
|
222
|
+
pos = skipWhitespace(code, pos);
|
|
223
|
+
const val = parseValue(code, pos);
|
|
224
|
+
if (val) {
|
|
225
|
+
result.push(val.value);
|
|
226
|
+
pos = val.pos;
|
|
227
|
+
}
|
|
228
|
+
pos = skipWhitespace(code, pos);
|
|
229
|
+
if (code[pos] === ",") {
|
|
230
|
+
pos++;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (code[pos] === "]") {
|
|
234
|
+
pos++;
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
value: result,
|
|
241
|
+
pos
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function parseObjectLiteral(code) {
|
|
245
|
+
const start = skipWhitespace(code, 0);
|
|
246
|
+
if (code[start] !== "{") return {};
|
|
247
|
+
const parsed = parseObjectValue(code, start);
|
|
248
|
+
if (parsed) return parsed.value;
|
|
249
|
+
const result = {};
|
|
250
|
+
const simpleRegex = /(\w+)\s*:\s*(['"`])((?:(?!\2)[^\\]|\\.)*)\2/g;
|
|
251
|
+
let m;
|
|
252
|
+
while ((m = simpleRegex.exec(code)) !== null) result[m[1]] = m[3];
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* 通用宏调用提取:在代码中查找 `funcName({...})` 形式的调用并解析其
|
|
257
|
+
* 对象字面量参数。供本包的 `definePage` 提取与 `@ubean/scan` 的
|
|
258
|
+
* `defineHandlerMeta` 提取共用(单一解析器实现)。
|
|
259
|
+
*/
|
|
260
|
+
function extractCallObject(code, funcName) {
|
|
261
|
+
const argStr = findBalancedCall(code, funcName);
|
|
262
|
+
if (!argStr) return null;
|
|
263
|
+
const trimmed = argStr.trim();
|
|
264
|
+
if (!trimmed.startsWith("{")) return null;
|
|
265
|
+
return parseObjectLiteral(trimmed);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Normalize a parsed `head` value into a `PageHead` object。
|
|
269
|
+
*
|
|
270
|
+
* Mirrors the markdown frontmatter head validation so Vue pages (via
|
|
271
|
+
* `definePage`) and Markdown pages (via frontmatter) share the same rules.
|
|
272
|
+
*/
|
|
273
|
+
function normalizePageHead(raw) {
|
|
274
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
275
|
+
const src = raw;
|
|
276
|
+
const head = {};
|
|
277
|
+
if (typeof src.title === "string") head.title = src.title;
|
|
278
|
+
if (Array.isArray(src.meta)) head.meta = src.meta;
|
|
279
|
+
if (Array.isArray(src.link)) head.link = src.link;
|
|
280
|
+
if (Array.isArray(src.script)) head.script = src.script;
|
|
281
|
+
if (src.htmlAttrs && typeof src.htmlAttrs === "object") head.htmlAttrs = src.htmlAttrs;
|
|
282
|
+
if (src.bodyAttrs && typeof src.bodyAttrs === "object") head.bodyAttrs = src.bodyAttrs;
|
|
283
|
+
return Object.keys(head).length > 0 ? head : void 0;
|
|
284
|
+
}
|
|
285
|
+
function extractDefinePageFromCode(code) {
|
|
286
|
+
const scriptContent = extractScriptContent(code);
|
|
287
|
+
if (!scriptContent) return null;
|
|
288
|
+
const parsed = extractCallObject(scriptContent, "definePage");
|
|
289
|
+
if (!parsed) return null;
|
|
290
|
+
const result = {};
|
|
291
|
+
if (typeof parsed.name === "string") result.name = parsed.name;
|
|
292
|
+
if (typeof parsed.path === "string") result.path = parsed.path;
|
|
293
|
+
if (parsed.layout === false) result.layout = false;
|
|
294
|
+
else if (typeof parsed.layout === "string" && parsed.layout !== "default") result.layout = parsed.layout;
|
|
295
|
+
else if (Array.isArray(parsed.layout)) {
|
|
296
|
+
const layouts = parsed.layout.filter((l) => typeof l === "string");
|
|
297
|
+
if (layouts.length > 0) result.layout = layouts;
|
|
298
|
+
}
|
|
299
|
+
if (typeof parsed.reuse === "string") result.reuse = parsed.reuse;
|
|
300
|
+
if (parsed.meta && typeof parsed.meta === "object") result.meta = parsed.meta;
|
|
301
|
+
if (typeof parsed.requiresAuth === "boolean") result.requiresAuth = parsed.requiresAuth;
|
|
302
|
+
if (typeof parsed.cache === "boolean") result.cache = parsed.cache;
|
|
303
|
+
if (typeof parsed.transition === "string") result.transition = parsed.transition;
|
|
304
|
+
const head = normalizePageHead(parsed.head);
|
|
305
|
+
if (head) result.head = head;
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
async function extractDefinePage(filePath) {
|
|
309
|
+
return extractDefinePageFromCode(await readFile(filePath, "utf-8"));
|
|
310
|
+
}
|
|
311
|
+
//#endregion
|
|
312
|
+
//#region src/route-name.ts
|
|
313
|
+
/**
|
|
314
|
+
* 路由名生成器(页面路由所有权归属 `@ubean/vue`)。
|
|
315
|
+
* `@ubean/scan` 聚合层 re-export 保持向后兼容;服务端专用的
|
|
316
|
+
* `generateApiRouteId` 保留在 `@ubean/scan`。
|
|
317
|
+
*/
|
|
318
|
+
function generateRouteName(routePath) {
|
|
319
|
+
if (routePath === "/" || routePath === "") return "Index";
|
|
320
|
+
const segments = routePath.replace(/^\//, "").replace(/\/$/, "").split("/");
|
|
321
|
+
const nameSegments = [];
|
|
322
|
+
for (const segment of segments) {
|
|
323
|
+
if (segment.startsWith("(") && segment.endsWith(")")) continue;
|
|
324
|
+
if (segment.startsWith("[...")) {
|
|
325
|
+
const param = segment.slice(4, -1);
|
|
326
|
+
nameSegments.push(`All${formatParamName(param)}`);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (segment.startsWith("[[")) {
|
|
330
|
+
const param = segment.slice(2, -2);
|
|
331
|
+
nameSegments.push(`${formatParamName(param)}Optional`);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (segment.startsWith("[")) {
|
|
335
|
+
const param = segment.slice(1, -1);
|
|
336
|
+
nameSegments.push(formatParamName(param));
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
nameSegments.push(pascalCase(segment));
|
|
340
|
+
}
|
|
341
|
+
return nameSegments.join("") || "Index";
|
|
342
|
+
}
|
|
343
|
+
function generateLayoutName(layoutPath) {
|
|
344
|
+
const base = layoutPath.replace(/\.(vue|ts)$/, "");
|
|
345
|
+
if (base === "default" || base === "default/index") return "default";
|
|
346
|
+
return base.split("/").filter(Boolean).map((s) => pascalCase(s)).join("");
|
|
347
|
+
}
|
|
348
|
+
function formatParamName(param) {
|
|
349
|
+
const cleaned = param.replace(/^\.{3}/, "").replace(/\?$/, "");
|
|
350
|
+
return pascalCase(cleaned);
|
|
351
|
+
}
|
|
352
|
+
//#endregion
|
|
353
|
+
//#region src/route-path.ts
|
|
354
|
+
/**
|
|
355
|
+
* 路由路径算法(页面路由所有权归属 `@ubean/vue`,服务端 API 路由复用同一解析器):
|
|
356
|
+
* - stripRouteGroups
|
|
357
|
+
* - ParsedRoutePath 类型
|
|
358
|
+
* - parseMatchers
|
|
359
|
+
* - filePathToRoute
|
|
360
|
+
*
|
|
361
|
+
* `@ubean/scan` 聚合层 re-export 本文件导出保持向后兼容。
|
|
362
|
+
*/
|
|
363
|
+
const EXTENSION_REGEX = /\.(mjs|js|jsx|cjs|ts|tsx|mts|cts|vue|md|mdx)$/i;
|
|
364
|
+
const METHOD_SUFFIX_REGEX = /\.(connect|delete|get|head|options|patch|post|put|trace)$/i;
|
|
365
|
+
const ENV_SUFFIX_REGEX = /\.(dev|prod|prerender)$/i;
|
|
366
|
+
const MIXED_SUFFIX_REGEX = /\.(connect|delete|get|head|options|patch|post|put|trace)\.(dev|prod|prerender)$/i;
|
|
367
|
+
const DYNAMIC_PARAM_REGEX = /\[([^\]]+)\]/g;
|
|
368
|
+
const CATCH_ALL_REGEX = /\[\.{3}([^\]]+)\]/g;
|
|
369
|
+
const OPTIONAL_PARAM_REGEX = /\[\[([^\]]+)\]\]/g;
|
|
370
|
+
const ROUTE_GROUP_REGEX = /\(([^(/\\]+)\)[/\\]/g;
|
|
371
|
+
const ROUTE_GROUP_TRAILING_REGEX = /\(([^(/\\]+)\)$/;
|
|
372
|
+
/**
|
|
373
|
+
* `[id=numeric]` / `[...slug=anything]` matcher 语法解析。
|
|
374
|
+
*
|
|
375
|
+
* 捕获组:
|
|
376
|
+
* $1 = 可选的 `...`(catch-all 前缀)
|
|
377
|
+
* $2 = 参数名(id / slug / ...)
|
|
378
|
+
* $3 = 可选的 `=matcherName`
|
|
379
|
+
*/
|
|
380
|
+
const DYNAMIC_PARAM_WITH_MATCHER_REGEX = /\[(\.{3})?([A-Za-z_][\w-]*)(?:=([A-Za-z_][\w-]*))?\]/g;
|
|
381
|
+
const INDEX_FILE_REGEX = /\/index$/;
|
|
382
|
+
function stripRouteGroups(path) {
|
|
383
|
+
let result = path.replace(ROUTE_GROUP_REGEX, "");
|
|
384
|
+
result = result.replace(ROUTE_GROUP_TRAILING_REGEX, "");
|
|
385
|
+
return result;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* 解析 `[param=matcher]` 语法,从原始路径中提取 matcher 名称映射,并把
|
|
389
|
+
* `=matcher` 后缀剥离,以便后续正则能正确识别为普通动态参数。
|
|
390
|
+
*
|
|
391
|
+
* @example
|
|
392
|
+
* parseMatchers('users/[id=numeric]') → { cleaned: 'users/[id]', matchers: { id: 'numeric' } }
|
|
393
|
+
* parseMatchers('blog/[...slug=any]') → { cleaned: 'blog/[...slug]', matchers: { slug: 'any' } }
|
|
394
|
+
* parseMatchers('users/[id]') → { cleaned: 'users/[id]', matchers: undefined }
|
|
395
|
+
*/
|
|
396
|
+
function parseMatchers(filePath) {
|
|
397
|
+
const matchers = {};
|
|
398
|
+
let hasMatchers = false;
|
|
399
|
+
const cleaned = filePath.replace(DYNAMIC_PARAM_WITH_MATCHER_REGEX, (full, dots, name, matcherName) => {
|
|
400
|
+
if (matcherName) {
|
|
401
|
+
matchers[name] = matcherName;
|
|
402
|
+
hasMatchers = true;
|
|
403
|
+
}
|
|
404
|
+
return dots ? `[${dots}${name}]` : `[${name}]`;
|
|
405
|
+
});
|
|
406
|
+
return hasMatchers ? {
|
|
407
|
+
cleaned,
|
|
408
|
+
matchers
|
|
409
|
+
} : { cleaned: filePath };
|
|
410
|
+
}
|
|
411
|
+
function filePathToRoute(filePath, prefix = "/") {
|
|
412
|
+
const { cleaned: matcherStripped, matchers } = parseMatchers(filePath);
|
|
413
|
+
let route = matcherStripped;
|
|
414
|
+
let method;
|
|
415
|
+
let env;
|
|
416
|
+
route = route.replace(EXTENSION_REGEX, "");
|
|
417
|
+
const mixedMatch = route.match(MIXED_SUFFIX_REGEX);
|
|
418
|
+
if (mixedMatch && mixedMatch.index !== void 0) {
|
|
419
|
+
route = route.slice(0, mixedMatch.index);
|
|
420
|
+
method = mixedMatch[1].toLowerCase();
|
|
421
|
+
env = mixedMatch[2];
|
|
422
|
+
} else {
|
|
423
|
+
const methodMatch = route.match(METHOD_SUFFIX_REGEX);
|
|
424
|
+
if (methodMatch && methodMatch.index !== void 0) {
|
|
425
|
+
route = route.slice(0, methodMatch.index);
|
|
426
|
+
method = methodMatch[1].toLowerCase();
|
|
427
|
+
}
|
|
428
|
+
const envMatch = route.match(ENV_SUFFIX_REGEX);
|
|
429
|
+
if (envMatch && envMatch.index !== void 0) {
|
|
430
|
+
route = route.slice(0, envMatch.index);
|
|
431
|
+
env = envMatch[1];
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
route = stripRouteGroups(route);
|
|
435
|
+
route = route.replace(CATCH_ALL_REGEX, (_, p) => `**:${p.replace(/[^\w-]/g, "_")}`);
|
|
436
|
+
route = route.replace(OPTIONAL_PARAM_REGEX, (_, p) => `:${p.replace(/[^\w-]/g, "_")}?`);
|
|
437
|
+
route = route.replace(DYNAMIC_PARAM_REGEX, (_, p) => `:${p.replace(/[^\w-]/g, "_")}`);
|
|
438
|
+
route = withLeadingSlash(withoutTrailingSlash(withBase(route, prefix)));
|
|
439
|
+
route = route.replace(INDEX_FILE_REGEX, "") || "/";
|
|
440
|
+
return {
|
|
441
|
+
route,
|
|
442
|
+
method,
|
|
443
|
+
env,
|
|
444
|
+
cleaned: matcherStripped,
|
|
445
|
+
matchers
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
//#endregion
|
|
449
|
+
//#region src/scan-pages.ts
|
|
450
|
+
/**
|
|
451
|
+
* 页面/布局扫描器(页面路由所有权归属 `@ubean/vue`)。
|
|
452
|
+
*
|
|
453
|
+
* 能力(从 `@ubean/scan` 沉淀并增强):
|
|
454
|
+
* - 多 `pagesDir` / `layoutsDir`(先到先得去重,可分层叠加目录)
|
|
455
|
+
* - reuse 路由(`.reuse.ts` / `.reuse.js`,纯元数据文件单独注入 `definePage`)+ cache 继承
|
|
456
|
+
* - markdown 页面(opt-in,`@ubean/markdown` 按需加载解析 frontmatter)
|
|
457
|
+
* - 页面级 head(opt-in,`definePage({ head })` / frontmatter `head`)
|
|
458
|
+
* - 特殊页:`404` / `loading` / `error`(仅页面目录根级)
|
|
459
|
+
* - 并行路由 `@slot/` 与拦截路由 `(..)target/` `(.)target/` `(...)target/`
|
|
460
|
+
* - `[param=matcher]` 语法 → matchers 映射
|
|
461
|
+
*
|
|
462
|
+
* `@ubean/scan` 聚合层的 `scanProject` 委托本模块。
|
|
463
|
+
*/
|
|
464
|
+
/** 轻量告警(去重)—— 保持本包零 `@ubean/*` 硬依赖,不用 @ubean/logger。 */
|
|
465
|
+
const _warned$1 = /* @__PURE__ */ new Set();
|
|
466
|
+
function warn$1(message) {
|
|
467
|
+
if (_warned$1.has(message)) return;
|
|
468
|
+
_warned$1.add(message);
|
|
469
|
+
console.warn(`[ubean/vue] ${message}`);
|
|
470
|
+
}
|
|
471
|
+
let _frontmatterParser;
|
|
472
|
+
async function getFrontmatterParser() {
|
|
473
|
+
if (_frontmatterParser !== void 0) return _frontmatterParser;
|
|
474
|
+
try {
|
|
475
|
+
const mod = await import("@ubean/markdown");
|
|
476
|
+
if (typeof mod.parseFrontmatter === "function") _frontmatterParser = mod.parseFrontmatter;
|
|
477
|
+
else _frontmatterParser = null;
|
|
478
|
+
} catch {
|
|
479
|
+
warn$1("`markdown` is enabled but `@ubean/markdown` is not installed — frontmatter parsing will be skipped. Install it to enable markdown pages.");
|
|
480
|
+
_frontmatterParser = null;
|
|
481
|
+
}
|
|
482
|
+
return _frontmatterParser;
|
|
483
|
+
}
|
|
484
|
+
/** Build a `PageHead` from Markdown frontmatter(与 definePage head 校验规则一致)。 */
|
|
485
|
+
function buildMarkdownHead(fm, enabled) {
|
|
486
|
+
if (!enabled || !fm || typeof fm.head !== "object" || fm.head === null) return void 0;
|
|
487
|
+
const fmHead = fm.head;
|
|
488
|
+
const head = {};
|
|
489
|
+
if (typeof fmHead.title === "string") head.title = fmHead.title;
|
|
490
|
+
if (Array.isArray(fmHead.meta)) head.meta = fmHead.meta;
|
|
491
|
+
if (Array.isArray(fmHead.link)) head.link = fmHead.link;
|
|
492
|
+
if (Array.isArray(fmHead.script)) head.script = fmHead.script;
|
|
493
|
+
if (fmHead.htmlAttrs && typeof fmHead.htmlAttrs === "object") head.htmlAttrs = fmHead.htmlAttrs;
|
|
494
|
+
if (fmHead.bodyAttrs && typeof fmHead.bodyAttrs === "object") head.bodyAttrs = fmHead.bodyAttrs;
|
|
495
|
+
return Object.keys(head).length > 0 ? head : void 0;
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Extract parallel route slot name and intercept info from a relative file path.
|
|
499
|
+
*
|
|
500
|
+
* Parallel routes: `@slotName/page.vue` → slot = 'slotName'
|
|
501
|
+
* Intercepting routes:
|
|
502
|
+
* `(..)target/page.vue` → intercept from parent, target = 'target'
|
|
503
|
+
* `(.)target/page.vue` → intercept from same level, target = 'target'
|
|
504
|
+
* `(...)target/page.vue` → intercept from root, target = 'target'
|
|
505
|
+
*/
|
|
506
|
+
function extractSlotAndIntercept(fileBase) {
|
|
507
|
+
const segments = fileBase.split("/");
|
|
508
|
+
let slot;
|
|
509
|
+
let interceptFrom;
|
|
510
|
+
let interceptTarget;
|
|
511
|
+
const cleanedSegments = [];
|
|
512
|
+
for (let i = 0; i < segments.length; i++) {
|
|
513
|
+
const seg = segments[i];
|
|
514
|
+
if (seg.startsWith("@")) {
|
|
515
|
+
slot = seg.slice(1);
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
const interceptMatch = seg.match(/^\((\.{1,3})\)(.+)$/);
|
|
519
|
+
if (interceptMatch) {
|
|
520
|
+
const dots = interceptMatch[1];
|
|
521
|
+
interceptTarget = interceptMatch[2];
|
|
522
|
+
const prefixSegments = cleanedSegments.slice(0, i);
|
|
523
|
+
if (dots === "..") interceptFrom = `/${prefixSegments.slice(0, -1).join("/")}`;
|
|
524
|
+
else if (dots === "...") interceptFrom = "/";
|
|
525
|
+
else interceptFrom = `/${prefixSegments.join("/")}`;
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
cleanedSegments.push(seg);
|
|
529
|
+
}
|
|
530
|
+
return {
|
|
531
|
+
cleanedBase: cleanedSegments.join("/"),
|
|
532
|
+
slot,
|
|
533
|
+
interceptFrom,
|
|
534
|
+
interceptTarget
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Normalize a `string | string[]` dir entry into `string[]`。Falsy 值回退到
|
|
539
|
+
* 默认值;空数组同样回退,保证下游 glob 始终拿到非空列表。
|
|
540
|
+
*/
|
|
541
|
+
function normalizeDirs(value, fallback) {
|
|
542
|
+
if (value === void 0 || value === null || value === "") return [fallback];
|
|
543
|
+
if (Array.isArray(value)) {
|
|
544
|
+
const filtered = value.filter((d) => typeof d === "string" && d.length > 0);
|
|
545
|
+
return filtered.length > 0 ? filtered : [fallback];
|
|
546
|
+
}
|
|
547
|
+
return [value];
|
|
548
|
+
}
|
|
549
|
+
/** 目录条目 → 绝对路径(相对 `srcDir` 或已是绝对路径)。 */
|
|
550
|
+
function resolveDirs(srcDir, value, fallback) {
|
|
551
|
+
return normalizeDirs(value, fallback).map((single) => isAbsolute(single) ? single : join(srcDir, single));
|
|
552
|
+
}
|
|
553
|
+
function toPosixPath(p) {
|
|
554
|
+
return p.replace(/\\/g, "/");
|
|
555
|
+
}
|
|
556
|
+
const DEFAULT_IGNORE = [
|
|
557
|
+
"**/*.test.*",
|
|
558
|
+
"**/*.spec.*",
|
|
559
|
+
"**/*.d.ts"
|
|
560
|
+
];
|
|
561
|
+
/** 解析 markdown 配置 → 参与扫描的 markdown 扩展名列表(空 = 关闭)。 */
|
|
562
|
+
function resolveMarkdownExts(markdown) {
|
|
563
|
+
if (markdown === true) return ["md", "mdx"];
|
|
564
|
+
if (markdown === "md" || markdown === "mdx") return [markdown];
|
|
565
|
+
return [];
|
|
566
|
+
}
|
|
567
|
+
async function scanPages(options) {
|
|
568
|
+
const srcDir = isAbsolute(options.srcDir) ? options.srcDir : join(options.cwd, options.srcDir);
|
|
569
|
+
const ignore = [...DEFAULT_IGNORE, ...options.ignore || []];
|
|
570
|
+
const markdownExts = resolveMarkdownExts(options.markdown);
|
|
571
|
+
const headEnabled = options.head === true;
|
|
572
|
+
const extensions = [...options.extensions && options.extensions.length > 0 ? options.extensions : [
|
|
573
|
+
"vue",
|
|
574
|
+
"tsx",
|
|
575
|
+
"jsx"
|
|
576
|
+
]];
|
|
577
|
+
for (const ext of markdownExts) if (!extensions.includes(ext)) extensions.push(ext);
|
|
578
|
+
const pagesDirs = resolveDirs(srcDir, options.pagesDir, "pages");
|
|
579
|
+
const layoutsDirs = resolveDirs(srcDir, options.layoutsDir, "layouts");
|
|
580
|
+
const pagesPattern = [`**/*.{${extensions.join(",")}}`, "**/*.reuse.{ts,js}"];
|
|
581
|
+
const layoutsPattern = "**/*.{vue,ts}";
|
|
582
|
+
const pages = [];
|
|
583
|
+
const layouts = [];
|
|
584
|
+
const seenPagePaths = /* @__PURE__ */ new Set();
|
|
585
|
+
const seenLayoutPaths = /* @__PURE__ */ new Set();
|
|
586
|
+
let notFoundPage;
|
|
587
|
+
let loadingPage;
|
|
588
|
+
let errorPage;
|
|
589
|
+
for (const dir of pagesDirs) {
|
|
590
|
+
const files = await glob(pagesPattern, {
|
|
591
|
+
cwd: dir,
|
|
592
|
+
dot: true,
|
|
593
|
+
ignore: [
|
|
594
|
+
...ignore,
|
|
595
|
+
"**/components/**",
|
|
596
|
+
"**/_*"
|
|
597
|
+
],
|
|
598
|
+
absolute: true
|
|
599
|
+
}).catch(() => []);
|
|
600
|
+
for (const fullPath of files.sort()) {
|
|
601
|
+
if (seenPagePaths.has(fullPath)) continue;
|
|
602
|
+
seenPagePaths.add(fullPath);
|
|
603
|
+
const relativePath = toPosixPath(relative(dir, fullPath));
|
|
604
|
+
const ext = extname(relativePath);
|
|
605
|
+
const base = basename(relativePath, ext);
|
|
606
|
+
if (base.startsWith("_")) continue;
|
|
607
|
+
const isMarkdown = ext === ".md" || ext === ".mdx";
|
|
608
|
+
const isReuse = !isMarkdown && /\.reuse\.(ts|js)$/.test(relativePath);
|
|
609
|
+
const pageBase = isReuse ? base.slice(0, -6) : base;
|
|
610
|
+
const dirPart = dirname(relativePath) === "." ? "" : dirname(relativePath);
|
|
611
|
+
const { cleanedBase, slot, interceptFrom, interceptTarget } = extractSlotAndIntercept(dirPart ? `${dirPart}/${pageBase}` : pageBase);
|
|
612
|
+
const fileBase = cleanedBase;
|
|
613
|
+
if (!isReuse && dirPart === "" && (pageBase === "404" || pageBase === "loading" || pageBase === "error")) {
|
|
614
|
+
const { route: specialRoute, cleaned: specialCleaned } = filePathToRoute(fileBase);
|
|
615
|
+
const specialName = pageBase === "404" ? "NotFound" : generateRouteName(specialCleaned || specialRoute);
|
|
616
|
+
const specialPage = {
|
|
617
|
+
fullPath,
|
|
618
|
+
relativePath,
|
|
619
|
+
dirname: dirname(relativePath),
|
|
620
|
+
basename: basename(relativePath),
|
|
621
|
+
name: specialName,
|
|
622
|
+
route: specialRoute,
|
|
623
|
+
path: specialRoute,
|
|
624
|
+
layout: void 0,
|
|
625
|
+
cache: void 0,
|
|
626
|
+
isReuse: false,
|
|
627
|
+
isMarkdown,
|
|
628
|
+
reuseTarget: void 0,
|
|
629
|
+
pageMeta: void 0,
|
|
630
|
+
frontmatter: void 0
|
|
631
|
+
};
|
|
632
|
+
if (pageBase === "404") notFoundPage ??= specialPage;
|
|
633
|
+
else if (pageBase === "loading") loadingPage ??= specialPage;
|
|
634
|
+
else errorPage ??= specialPage;
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
const { route, cleaned, matchers } = filePathToRoute(fileBase);
|
|
638
|
+
const name = generateRouteName(cleaned || route);
|
|
639
|
+
let pageMeta = null;
|
|
640
|
+
let frontmatter;
|
|
641
|
+
if (isMarkdown) try {
|
|
642
|
+
const content = await readFile(fullPath, "utf-8");
|
|
643
|
+
const parser = await getFrontmatterParser();
|
|
644
|
+
if (parser) {
|
|
645
|
+
frontmatter = parser(content).data;
|
|
646
|
+
pageMeta = {
|
|
647
|
+
name: frontmatter?.name || name,
|
|
648
|
+
path: frontmatter?.path || route,
|
|
649
|
+
layout: frontmatter?.layout,
|
|
650
|
+
cache: frontmatter?.cache,
|
|
651
|
+
head: buildMarkdownHead(frontmatter, headEnabled)
|
|
652
|
+
};
|
|
653
|
+
} else pageMeta = {
|
|
654
|
+
name,
|
|
655
|
+
path: route
|
|
656
|
+
};
|
|
657
|
+
} catch {
|
|
658
|
+
pageMeta = null;
|
|
659
|
+
}
|
|
660
|
+
else pageMeta = await readFile(fullPath, "utf-8").then((code) => extractDefinePageFromCode(code)).catch(() => null);
|
|
661
|
+
if (pageMeta?.head && !headEnabled) {
|
|
662
|
+
warn$1("`definePage({ head })` / frontmatter `head` declared but the `head` option is disabled — head will be ignored.");
|
|
663
|
+
delete pageMeta.head;
|
|
664
|
+
}
|
|
665
|
+
pages.push({
|
|
666
|
+
fullPath,
|
|
667
|
+
relativePath,
|
|
668
|
+
dirname: dirname(relativePath),
|
|
669
|
+
basename: basename(relativePath),
|
|
670
|
+
name: pageMeta?.name || name,
|
|
671
|
+
route: pageMeta?.path || route,
|
|
672
|
+
path: pageMeta?.path || route,
|
|
673
|
+
layout: pageMeta?.layout,
|
|
674
|
+
cache: pageMeta?.cache,
|
|
675
|
+
isReuse,
|
|
676
|
+
isMarkdown,
|
|
677
|
+
reuseTarget: pageMeta?.reuse,
|
|
678
|
+
pageMeta: pageMeta || void 0,
|
|
679
|
+
frontmatter,
|
|
680
|
+
slot,
|
|
681
|
+
interceptFrom,
|
|
682
|
+
interceptTarget,
|
|
683
|
+
matchers
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
for (const dir of layoutsDirs) {
|
|
688
|
+
const files = await glob(layoutsPattern, {
|
|
689
|
+
cwd: dir,
|
|
690
|
+
dot: true,
|
|
691
|
+
ignore: [...ignore, "**/_*"],
|
|
692
|
+
absolute: true
|
|
693
|
+
}).catch(() => []);
|
|
694
|
+
for (const fullPath of files.sort()) {
|
|
695
|
+
if (seenLayoutPaths.has(fullPath)) continue;
|
|
696
|
+
seenLayoutPaths.add(fullPath);
|
|
697
|
+
const relativePath = toPosixPath(relative(dir, fullPath));
|
|
698
|
+
const base = basename(relativePath, extname(relativePath));
|
|
699
|
+
const dirPart = dirname(relativePath) === "." ? "" : dirname(relativePath);
|
|
700
|
+
const layoutBase = base === "index" ? dirPart : dirPart ? `${dirPart}/${base}` : base;
|
|
701
|
+
const isDefault = base === "default" || base === "index" && !dirPart;
|
|
702
|
+
const name = layoutBase || "default";
|
|
703
|
+
layouts.push({
|
|
704
|
+
fullPath,
|
|
705
|
+
relativePath,
|
|
706
|
+
dirname: dirname(relativePath),
|
|
707
|
+
basename: basename(relativePath),
|
|
708
|
+
name,
|
|
709
|
+
path: toPosixPath(relativePath),
|
|
710
|
+
isDefault
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
const regularPageNames = new Set(pages.filter((p) => !p.isReuse).map((p) => p.name));
|
|
715
|
+
for (const page of pages) if (page.isReuse && page.reuseTarget && !regularPageNames.has(page.reuseTarget)) warn$1(`Reuse page "${page.name}" references target "${page.reuseTarget}" which does not exist. Available page targets: ${[...regularPageNames].join(", ") || "(none)"}`);
|
|
716
|
+
const targetCacheMap = /* @__PURE__ */ new Map();
|
|
717
|
+
for (const p of pages) if (!p.isReuse) targetCacheMap.set(p.name, p.cache);
|
|
718
|
+
for (const page of pages) if (page.isReuse && page.reuseTarget && page.cache === void 0) {
|
|
719
|
+
if (targetCacheMap.get(page.reuseTarget) === true) {
|
|
720
|
+
page.cache = true;
|
|
721
|
+
if (page.pageMeta) page.pageMeta.cache = true;
|
|
722
|
+
else page.pageMeta = { cache: true };
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return {
|
|
726
|
+
pages,
|
|
727
|
+
layouts,
|
|
728
|
+
notFoundPage,
|
|
729
|
+
loadingPage,
|
|
730
|
+
errorPage
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
//#endregion
|
|
734
|
+
//#region src/virtual-pages.ts
|
|
735
|
+
/**
|
|
736
|
+
* 页面虚拟模块生成器(页面路由所有权归属 `@ubean/vue`)。
|
|
737
|
+
*
|
|
738
|
+
* 输出统一的模块形态,同时服务两个消费方:
|
|
739
|
+
* - 精简内核:`virtual:ubean-vue-routes`(`@ubean/vue/vite` 插件)
|
|
740
|
+
* - 框架层:`virtual:ubean-pages`(`packages/vite` 经 `defineVirtualModule` 包装)
|
|
741
|
+
*
|
|
742
|
+
* 导出面(纯 JS,SSR-safe):
|
|
743
|
+
* - `routes` —— vue-router `RouteRecordRaw[]`
|
|
744
|
+
* - `pageNames` / `layoutNames` / `defaultLayout`
|
|
745
|
+
* - `resolvePageComponent` / `resolveLayoutComponent` /
|
|
746
|
+
* `resolveLoadingComponent` / `resolveErrorComponent`
|
|
747
|
+
* - `loadingComponent` / `errorComponent`(精简内核 BC:loader 或 null)
|
|
748
|
+
* - `hasNotFoundPage` / `hasErrorPage`
|
|
749
|
+
* - `pages` / `layouts` 元信息映射
|
|
750
|
+
*/
|
|
751
|
+
/** `** :param`(scanner 方言)→ vue-router `:param(.*)*`。 */
|
|
752
|
+
function toVueRouterPath(route) {
|
|
753
|
+
return route.replace(/\*\*:(\w[\w-]*)/g, ":$1(.*)*");
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Sort pages so that reuse routes come after their targets.
|
|
757
|
+
*
|
|
758
|
+
* A reuse route generates `const Page_Reuse = Page_Target;` in the virtual
|
|
759
|
+
* module. If the reuse route is declared before its target, JavaScript's TDZ
|
|
760
|
+
* throws `Cannot access 'Page_Target' before initialization`. This
|
|
761
|
+
* topological sort ensures targets are always emitted before their reuse
|
|
762
|
+
* routes.
|
|
763
|
+
*/
|
|
764
|
+
function sortPagesByReuseDependency(pages) {
|
|
765
|
+
const result = [];
|
|
766
|
+
const visited = /* @__PURE__ */ new Set();
|
|
767
|
+
const byName = /* @__PURE__ */ new Map();
|
|
768
|
+
for (const p of pages) byName.set(p.name, p);
|
|
769
|
+
function visit(page) {
|
|
770
|
+
if (visited.has(page.name)) return;
|
|
771
|
+
visited.add(page.name);
|
|
772
|
+
if (page.isReuse && page.reuseTarget && byName.has(page.reuseTarget)) visit(byName.get(page.reuseTarget));
|
|
773
|
+
result.push(page);
|
|
774
|
+
}
|
|
775
|
+
for (const p of pages) visit(p);
|
|
776
|
+
return result;
|
|
777
|
+
}
|
|
778
|
+
function varNameFor(name) {
|
|
779
|
+
return `Page_${name.replace(/[^a-zA-Z0-9]/g, "_")}`;
|
|
780
|
+
}
|
|
781
|
+
function layoutVarNameFor(name) {
|
|
782
|
+
return `Layout_${name.replace(/[^a-zA-Z0-9]/g, "_")}`;
|
|
783
|
+
}
|
|
784
|
+
/** 组装单条路由的 meta(JSON 序列化;undefined 字段自动省略)。 */
|
|
785
|
+
function buildRouteMeta(page, extra = {}) {
|
|
786
|
+
const meta = {
|
|
787
|
+
pageName: page.name,
|
|
788
|
+
layout: page.layout,
|
|
789
|
+
cache: page.cache === true ? true : void 0,
|
|
790
|
+
reuseTarget: page.isReuse ? page.reuseTarget : void 0,
|
|
791
|
+
transition: page.pageMeta?.transition,
|
|
792
|
+
requiresAuth: page.pageMeta?.requiresAuth === true ? true : void 0,
|
|
793
|
+
matchers: page.matchers && Object.keys(page.matchers).length > 0 ? page.matchers : void 0,
|
|
794
|
+
head: page.pageMeta?.head,
|
|
795
|
+
...page.pageMeta?.meta,
|
|
796
|
+
...extra
|
|
797
|
+
};
|
|
798
|
+
return JSON.stringify(meta);
|
|
799
|
+
}
|
|
800
|
+
/** Generate the virtual module source(plain JS — no TS syntax, SSR-safe)。 */
|
|
801
|
+
function generatePagesModuleSource(input) {
|
|
802
|
+
const { pages, layouts } = input;
|
|
803
|
+
const pageLoaders = [];
|
|
804
|
+
const layoutLoaders = [];
|
|
805
|
+
const routeEntries = [];
|
|
806
|
+
const pageByName = /* @__PURE__ */ new Map();
|
|
807
|
+
for (const p of pages) pageByName.set(p.name, p);
|
|
808
|
+
const sortedPages = sortPagesByReuseDependency(pages);
|
|
809
|
+
for (const p of sortedPages) {
|
|
810
|
+
const varName = varNameFor(p.name);
|
|
811
|
+
if (p.isReuse && p.reuseTarget && pageByName.has(p.reuseTarget)) pageLoaders.push(`const ${varName} = ${varNameFor(p.reuseTarget)};`);
|
|
812
|
+
else pageLoaders.push(`const ${varName} = () => import(${JSON.stringify(p.fullPath)}).then(m => m.default || m);`);
|
|
813
|
+
}
|
|
814
|
+
if (input.notFoundPage) pageLoaders.push(`const Page_NotFound = () => import(${JSON.stringify(input.notFoundPage.fullPath)}).then(m => m.default || m);`);
|
|
815
|
+
const namedPages = input.notFoundPage ? [...sortedPages, input.notFoundPage] : sortedPages;
|
|
816
|
+
const routeGroups = /* @__PURE__ */ new Map();
|
|
817
|
+
const interceptPages = [];
|
|
818
|
+
for (const p of sortedPages) {
|
|
819
|
+
if (p.interceptTarget) {
|
|
820
|
+
interceptPages.push(p);
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
const routerPath = toVueRouterPath(p.route);
|
|
824
|
+
let group = routeGroups.get(routerPath);
|
|
825
|
+
if (!group) {
|
|
826
|
+
group = { slots: /* @__PURE__ */ new Map() };
|
|
827
|
+
routeGroups.set(routerPath, group);
|
|
828
|
+
}
|
|
829
|
+
if (p.slot) group.slots.set(p.slot, p);
|
|
830
|
+
else if (!group.default) group.default = p;
|
|
831
|
+
}
|
|
832
|
+
for (const [routerPath, group] of routeGroups) {
|
|
833
|
+
const defaultPage = group.default;
|
|
834
|
+
const slotPages = [...group.slots.values()];
|
|
835
|
+
if (!defaultPage && slotPages.length === 0) continue;
|
|
836
|
+
const primaryPage = defaultPage || slotPages[0];
|
|
837
|
+
if (slotPages.length > 0) {
|
|
838
|
+
const componentParts = [];
|
|
839
|
+
if (defaultPage) componentParts.push(`default: ${varNameFor(defaultPage.name)}`);
|
|
840
|
+
for (const sp of slotPages) componentParts.push(`${JSON.stringify(sp.slot)}: ${varNameFor(sp.name)}`);
|
|
841
|
+
routeEntries.push(` { path: ${JSON.stringify(routerPath)}, name: ${JSON.stringify(primaryPage.name)}, components: { ${componentParts.join(", ")} }, meta: ${buildRouteMeta(primaryPage, { parallelSlots: slotPages.map((s) => s.slot) })} }`);
|
|
842
|
+
} else routeEntries.push(` { path: ${JSON.stringify(routerPath)}, name: ${JSON.stringify(defaultPage.name)}, component: ${varNameFor(defaultPage.name)}, meta: ${buildRouteMeta(defaultPage)} }`);
|
|
843
|
+
}
|
|
844
|
+
for (const p of interceptPages) {
|
|
845
|
+
const routerPath = toVueRouterPath(p.route);
|
|
846
|
+
const interceptName = `__intercept_${p.name}`;
|
|
847
|
+
routeEntries.push(` { path: ${JSON.stringify(routerPath)}, name: ${JSON.stringify(interceptName)}, component: ${varNameFor(p.name)}, meta: ${buildRouteMeta(p, {
|
|
848
|
+
interceptFrom: p.interceptFrom,
|
|
849
|
+
interceptTarget: p.interceptTarget,
|
|
850
|
+
isIntercepting: true
|
|
851
|
+
})} }`);
|
|
852
|
+
}
|
|
853
|
+
let hasNotFound = false;
|
|
854
|
+
if (input.notFoundPage) {
|
|
855
|
+
hasNotFound = true;
|
|
856
|
+
routeEntries.push(` { path: ${JSON.stringify("/:pathMatch(.*)*")}, name: "NotFound", component: Page_NotFound, meta: { pageName: "NotFound" } }`);
|
|
857
|
+
}
|
|
858
|
+
let loadingLoaderName = "null";
|
|
859
|
+
if (input.loadingPage) {
|
|
860
|
+
loadingLoaderName = "LoadingPage";
|
|
861
|
+
pageLoaders.push(`const ${loadingLoaderName} = () => import(${JSON.stringify(input.loadingPage.fullPath)}).then(m => m.default || m);`);
|
|
862
|
+
}
|
|
863
|
+
let errorLoaderName = "null";
|
|
864
|
+
if (input.errorPage) {
|
|
865
|
+
errorLoaderName = "ErrorPage";
|
|
866
|
+
pageLoaders.push(`const ${errorLoaderName} = () => import(${JSON.stringify(input.errorPage.fullPath)}).then(m => m.default || m);`);
|
|
867
|
+
}
|
|
868
|
+
for (const l of layouts) layoutLoaders.push(`const ${layoutVarNameFor(l.name)} = () => import(${JSON.stringify(l.fullPath)}).then(m => m.default || m);`);
|
|
869
|
+
const defaultLayout = layouts.find((l) => l.isDefault);
|
|
870
|
+
const defaultLayoutName = defaultLayout ? defaultLayout.name : "null";
|
|
871
|
+
const layoutLoaderMap = layouts.map((l) => ` ${JSON.stringify(l.name)}: ${layoutVarNameFor(l.name)}`).join(",\n");
|
|
872
|
+
return `// Generated by @ubean/vue — file-based pages virtual module. Do not edit.
|
|
873
|
+
/* eslint-disable */
|
|
874
|
+
|
|
875
|
+
${pageLoaders.join("\n")}
|
|
876
|
+
${layoutLoaders.join("\n")}
|
|
877
|
+
|
|
878
|
+
export const routes = [
|
|
879
|
+
${routeEntries.join(",\n")}
|
|
880
|
+
];
|
|
881
|
+
|
|
882
|
+
const _layoutLoaders = {
|
|
883
|
+
${layoutLoaderMap}
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
const _pageLoaders = {
|
|
887
|
+
${namedPages.map((p) => ` ${JSON.stringify(p.name)}: ${varNameFor(p.name)}`).join(",\n")}
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
export const pageNames = [${namedPages.map((p) => JSON.stringify(p.name)).join(", ")}];
|
|
891
|
+
export const layoutNames = [${layouts.map((l) => JSON.stringify(l.name)).join(", ")}];
|
|
892
|
+
export const defaultLayout = ${defaultLayoutName === "null" ? "null" : JSON.stringify(defaultLayoutName)};
|
|
893
|
+
|
|
894
|
+
export function resolvePageComponent(name) {
|
|
895
|
+
const loader = _pageLoaders[name];
|
|
896
|
+
if (!loader) {
|
|
897
|
+
return Promise.reject(new Error('[ubean] Page component not found: ' + name));
|
|
898
|
+
}
|
|
899
|
+
return loader();
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
export function resolveLayoutComponent(name) {
|
|
903
|
+
if (!name) return Promise.resolve(null);
|
|
904
|
+
const loader = _layoutLoaders[name];
|
|
905
|
+
if (!loader) {
|
|
906
|
+
return Promise.resolve(null);
|
|
907
|
+
}
|
|
908
|
+
return loader();
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
export function resolveLoadingComponent() {
|
|
912
|
+
${loadingLoaderName === "null" ? "return Promise.resolve(null);" : `return ${loadingLoaderName}();`}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
export function resolveErrorComponent() {
|
|
916
|
+
${errorLoaderName === "null" ? "return Promise.resolve(null);" : `return ${errorLoaderName}();`}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
export const loadingComponent = ${loadingLoaderName === "null" ? "null" : loadingLoaderName};
|
|
920
|
+
export const errorComponent = ${errorLoaderName === "null" ? "null" : errorLoaderName};
|
|
921
|
+
|
|
922
|
+
export function hasNotFoundPage() {
|
|
923
|
+
return ${hasNotFound};
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
export function hasErrorPage() {
|
|
927
|
+
return ${errorLoaderName !== "null"};
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
export const pages = {
|
|
931
|
+
${pages.map((p) => ` ${JSON.stringify(p.name)}: { name: ${JSON.stringify(p.name)}, route: ${JSON.stringify(p.route)}, path: ${JSON.stringify(p.path)}, layout: ${JSON.stringify(p.layout)}, isReuse: ${p.isReuse}, reuseTarget: ${JSON.stringify(p.reuseTarget)} }`).join(",\n")}
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
export const layouts = {
|
|
935
|
+
${layouts.map((l) => ` ${JSON.stringify(l.name)}: { name: ${JSON.stringify(l.name)}, isDefault: ${l.isDefault} }`).join(",\n")}
|
|
936
|
+
};
|
|
937
|
+
`.trimEnd().concat("\n");
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* 从 vue-router 风格路径中提取参数信息。
|
|
941
|
+
*
|
|
942
|
+
* 支持:`:name`、`:name?`、`:name*`、`:name+`、`:name(...)`。
|
|
943
|
+
* 不识别 `:pathMatch(.*)*`(catch-all),视为无参数。
|
|
944
|
+
*/
|
|
945
|
+
function extractRouteParams(path) {
|
|
946
|
+
const params = [];
|
|
947
|
+
const paramRegex = /:([A-Za-z_][A-Za-z0-9_]*)(?:\([^)]*\))?([?*+]?)/g;
|
|
948
|
+
let match;
|
|
949
|
+
while ((match = paramRegex.exec(path)) !== null) {
|
|
950
|
+
const name = match[1];
|
|
951
|
+
const modifier = match[2];
|
|
952
|
+
const optional = modifier === "?" || modifier === "*";
|
|
953
|
+
if (!params.some((p) => p.name === name)) params.push({
|
|
954
|
+
name,
|
|
955
|
+
optional
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
return params;
|
|
959
|
+
}
|
|
960
|
+
/** 渲染参数类型字面量(`ParamValue<true/false>` / `ParamValueZeroOrOne<...>`)。 */
|
|
961
|
+
function renderParamsType(params, isRaw) {
|
|
962
|
+
if (params.length === 0) return "Record<never, never>";
|
|
963
|
+
return `{\n${params.map((p) => {
|
|
964
|
+
const optionalMarker = p.optional ? "?" : "";
|
|
965
|
+
const type = p.optional ? `ParamValueZeroOrOne<${isRaw ? "true" : "false"}>` : `ParamValue<${isRaw ? "true" : "false"}>`;
|
|
966
|
+
return ` ${p.name}${optionalMarker}: ${type}`;
|
|
967
|
+
}).join(",\n")}\n }`;
|
|
968
|
+
}
|
|
969
|
+
function renderRouteRecordInfo(page) {
|
|
970
|
+
const params = extractRouteParams(page.route);
|
|
971
|
+
const paramsRaw = renderParamsType(params, true);
|
|
972
|
+
const paramsResolved = renderParamsType(params, false);
|
|
973
|
+
return `RouteRecordInfo<${JSON.stringify(page.name)}, ${JSON.stringify(page.route)}, ${paramsRaw}, ${paramsResolved}>`;
|
|
974
|
+
}
|
|
975
|
+
/** 虚拟模块导出面声明(`declare module` 内部内容,环境声明产物使用)。 */
|
|
976
|
+
function renderVirtualModuleBody(input, moduleId) {
|
|
977
|
+
const pages = input.pages;
|
|
978
|
+
return `declare module '${moduleId}' {
|
|
979
|
+
export const routes: import('vue-router').RouteRecordRaw[];
|
|
980
|
+
export type PageName = ${pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never"}${input.notFoundPage ? ` | ${JSON.stringify(input.notFoundPage.name)}` : ""};
|
|
981
|
+
export const pageNames: PageName[];
|
|
982
|
+
export const layoutNames: string[];
|
|
983
|
+
export const defaultLayout: string | null;
|
|
984
|
+
export const loadingComponent: import('vue').Component | null;
|
|
985
|
+
export const errorComponent: import('vue').Component | null;
|
|
986
|
+
export function resolvePageComponent(name: string): Promise<import('vue').Component>;
|
|
987
|
+
export function resolveLayoutComponent(name: string): Promise<import('vue').Component | null>;
|
|
988
|
+
export function resolveLoadingComponent(): Promise<import('vue').Component | null>;
|
|
989
|
+
export function resolveErrorComponent(): Promise<import('vue').Component | null>;
|
|
990
|
+
export function hasNotFoundPage(): boolean;
|
|
991
|
+
export function hasErrorPage(): boolean;
|
|
992
|
+
}`;
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Generate the ambient module declaration d.ts for the virtual module.
|
|
996
|
+
*
|
|
997
|
+
* 产物必须是 **script 文件**(无顶层 import/export)—— `declare module` 在
|
|
998
|
+
* script 上下文中才注册「环境模块声明」;在 module 文件中会被当作模块增强,
|
|
999
|
+
* 对不存在的模块无法生效。由 `/vite` 插件写入 `<root>/ubean-vue-routes.d.ts`。
|
|
1000
|
+
*/
|
|
1001
|
+
function generateVirtualModuleDts(input, moduleId = "virtual:ubean-vue-routes") {
|
|
1002
|
+
return `// Generated by @ubean/vue — do not edit.
|
|
1003
|
+
/* eslint-disable */
|
|
1004
|
+
|
|
1005
|
+
${renderVirtualModuleBody(input, moduleId)}
|
|
1006
|
+
`;
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Generate `typed-router.d.ts` content for the virtual module.
|
|
1010
|
+
*
|
|
1011
|
+
* 包含 `vue-router/auto-routes` 的 `RouteNamedMap` 增强,让 vue-router 的
|
|
1012
|
+
* `useRoute<Name>(name)` / `RouterLink` 能推断 `route.params` 类型。
|
|
1013
|
+
*
|
|
1014
|
+
* 产物是 **module 文件**(顶层 `export {}`)—— `declare module` 块按「模块
|
|
1015
|
+
* 增强」语义合并进真实的 'vue-router' / 'vue-router/auto-routes' 模块;
|
|
1016
|
+
* 若以 script 形式输出,同名环境模块声明会整体遮蔽真实包的类型。
|
|
1017
|
+
* 虚拟模块自身的环境声明见 `generateVirtualModuleDts`。
|
|
1018
|
+
*/
|
|
1019
|
+
function generateTypedRouter(input, _moduleId = "virtual:ubean-vue-routes") {
|
|
1020
|
+
const pages = input.pages;
|
|
1021
|
+
return `// Generated by @ubean/vue — do not edit.
|
|
1022
|
+
/* eslint-disable */
|
|
1023
|
+
|
|
1024
|
+
// 本文件为 module(顶层 export)—— declare module 块按「模块增强」语义合并。
|
|
1025
|
+
// 虚拟模块 'virtual:ubean-vue-routes' 的环境声明在伴生文件 ubean-vue-routes.d.ts。
|
|
1026
|
+
export {};
|
|
1027
|
+
|
|
1028
|
+
declare module 'vue-router/auto-routes' {
|
|
1029
|
+
import type { RouteRecordInfo, ParamValue, ParamValueZeroOrOne } from 'vue-router';
|
|
1030
|
+
|
|
1031
|
+
export interface RouteNamedMap {
|
|
1032
|
+
${pages.length > 0 ? pages.map((p) => ` "${p.name}": ${renderRouteRecordInfo(p)};`).join("\n") : " // (no pages scanned)"}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
declare module 'vue-router' {
|
|
1037
|
+
export interface TypesConfig {
|
|
1038
|
+
RouteNamedMap: import('vue-router/auto-routes').RouteNamedMap;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
`;
|
|
1042
|
+
}
|
|
1043
|
+
//#endregion
|
|
1044
|
+
//#region src/vite.ts
|
|
1045
|
+
/**
|
|
1046
|
+
* `@ubean/vue/vite` —— 精简客户端路由 Vite 插件(页面路由唯一所有者)。
|
|
1047
|
+
*
|
|
1048
|
+
* 能力:
|
|
1049
|
+
* - 多 `pagesDir` / `layoutsDir` 扫描(先到先得去重)
|
|
1050
|
+
* - reuse 路由、特殊页(404/loading/error)、并行路由 `@slot/`、拦截路由
|
|
1051
|
+
* - `[param=matcher]` 语法(matchers 注入 `route.meta`)
|
|
1052
|
+
* - markdown 页面(opt-in,默认 false,`@ubean/markdown` 按需加载)
|
|
1053
|
+
* - 页面级 head(opt-in,默认 false,写入 `route.meta.head`)
|
|
1054
|
+
* - `typed-router.d.ts` 生成(RouteNamedMap 完整类型推断,产物在 `dtsDir`,默认 `.ubean`)
|
|
1055
|
+
*
|
|
1056
|
+
* 框架层(`@ubean/vite`)通过 `generatePagesModuleSource` /
|
|
1057
|
+
* `scanPages` / `generateTypedRouter` 复用同一套生成器。
|
|
1058
|
+
*/
|
|
1059
|
+
const VUE_ROUTES_MODULE_ID = "virtual:ubean-vue-routes";
|
|
1060
|
+
const RESOLVED_MODULE_ID = `\0${VUE_ROUTES_MODULE_ID}`;
|
|
1061
|
+
/** 剥离 `definePage({...})` 宏调用(构建期,扫描后源码中不再需要)。 */
|
|
1062
|
+
function stripDefinePageCalls(code) {
|
|
1063
|
+
return code.replace(/(^|\n)[ \t]*(?:export[ \t]+)?definePage\s*\(\s*\{[\s\S]*?\}\s*\)[ \t]*;?/g, "$1");
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* 扫描客户端页面/布局(对外工具函数,`root` 为项目根)。
|
|
1067
|
+
*/
|
|
1068
|
+
async function scanClientPages(root, options = {}) {
|
|
1069
|
+
return scanPages({
|
|
1070
|
+
cwd: root,
|
|
1071
|
+
srcDir: root,
|
|
1072
|
+
pagesDir: options.pagesDir ?? "src/pages",
|
|
1073
|
+
layoutsDir: options.layoutsDir ?? "src/layouts",
|
|
1074
|
+
extensions: options.extensions,
|
|
1075
|
+
ignore: options.ignore,
|
|
1076
|
+
markdown: options.markdown,
|
|
1077
|
+
head: options.head
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
const _warned = /* @__PURE__ */ new Set();
|
|
1081
|
+
function warn(message) {
|
|
1082
|
+
if (_warned.has(message)) return;
|
|
1083
|
+
_warned.add(message);
|
|
1084
|
+
console.warn(`[ubean/vue] ${message}`);
|
|
1085
|
+
}
|
|
1086
|
+
function ubeanVueVite(options = {}) {
|
|
1087
|
+
let root = process.cwd();
|
|
1088
|
+
let scan = null;
|
|
1089
|
+
let markdownApi = null;
|
|
1090
|
+
const markdownEnabled = options.markdown === true || options.markdown === "md" || options.markdown === "mdx";
|
|
1091
|
+
const mdxEnabled = options.markdown === true || options.markdown === "mdx";
|
|
1092
|
+
async function loadMarkdownApi() {
|
|
1093
|
+
if (!markdownEnabled) return null;
|
|
1094
|
+
try {
|
|
1095
|
+
const mod = await import("@ubean/markdown");
|
|
1096
|
+
if (typeof mod.parseFrontmatter === "function" && typeof mod.markdownToHtml === "function") return mod;
|
|
1097
|
+
} catch {}
|
|
1098
|
+
warn("`markdown` is enabled but `@ubean/markdown` is not installed — markdown pages will not render.");
|
|
1099
|
+
return null;
|
|
1100
|
+
}
|
|
1101
|
+
async function rescan() {
|
|
1102
|
+
scan = await scanClientPages(root, options);
|
|
1103
|
+
if (options.generateTypes !== false) await writeTypedRouter();
|
|
1104
|
+
return scan;
|
|
1105
|
+
}
|
|
1106
|
+
async function writeTypedRouter() {
|
|
1107
|
+
if (!scan) return;
|
|
1108
|
+
const dtsDirOption = options.dtsDir ?? ".ubean";
|
|
1109
|
+
const dtsDir = isAbsolute(dtsDirOption) ? dtsDirOption : join(root, dtsDirOption);
|
|
1110
|
+
const files = [[join(dtsDir, "ubean-vue-routes.d.ts"), generateVirtualModuleDts(scan, VUE_ROUTES_MODULE_ID)], [join(dtsDir, "typed-router.d.ts"), generateTypedRouter(scan, VUE_ROUTES_MODULE_ID)]];
|
|
1111
|
+
for (const [outPath, content] of files) try {
|
|
1112
|
+
if (await readFile(outPath, "utf-8").catch(() => null) !== content) {
|
|
1113
|
+
await mkdir(dtsDir, { recursive: true });
|
|
1114
|
+
await writeFile(outPath, content, "utf-8");
|
|
1115
|
+
}
|
|
1116
|
+
} catch {}
|
|
1117
|
+
}
|
|
1118
|
+
function isUnderPagesDir(id) {
|
|
1119
|
+
return (Array.isArray(options.pagesDir) ? options.pagesDir : [options.pagesDir ?? "src/pages"]).map((d) => isAbsolute(d) ? d : join(root, d)).some((d) => id.startsWith(`${d}/`) || id.startsWith(`${d}\\`));
|
|
1120
|
+
}
|
|
1121
|
+
return {
|
|
1122
|
+
name: "ubean-vue-routes",
|
|
1123
|
+
enforce: "pre",
|
|
1124
|
+
configResolved(config) {
|
|
1125
|
+
root = config.root;
|
|
1126
|
+
},
|
|
1127
|
+
async buildStart() {
|
|
1128
|
+
markdownApi = await loadMarkdownApi();
|
|
1129
|
+
await rescan();
|
|
1130
|
+
},
|
|
1131
|
+
resolveId(id) {
|
|
1132
|
+
if (id === "virtual:ubean-vue-routes") return RESOLVED_MODULE_ID;
|
|
1133
|
+
return null;
|
|
1134
|
+
},
|
|
1135
|
+
load(id) {
|
|
1136
|
+
if (id === RESOLVED_MODULE_ID) {
|
|
1137
|
+
if (!scan) return rescan().then((result) => generatePagesModuleSource(result));
|
|
1138
|
+
return generatePagesModuleSource(scan);
|
|
1139
|
+
}
|
|
1140
|
+
return null;
|
|
1141
|
+
},
|
|
1142
|
+
transform(code, id) {
|
|
1143
|
+
const [filePath] = id.split("?", 2);
|
|
1144
|
+
if (!isUnderPagesDir(filePath)) return null;
|
|
1145
|
+
if (filePath.endsWith(".md") || mdxEnabled && filePath.endsWith(".mdx")) {
|
|
1146
|
+
if (!markdownApi) return null;
|
|
1147
|
+
if (filePath.endsWith(".mdx") && typeof markdownApi.compileMdx === "function") return markdownApi.compileMdx(code, { filePath: relative(root, filePath) }).then((result) => ({
|
|
1148
|
+
code: result.code,
|
|
1149
|
+
map: null
|
|
1150
|
+
}));
|
|
1151
|
+
const { data: frontmatter, content } = markdownApi.parseFrontmatter(code);
|
|
1152
|
+
const html = markdownApi.markdownToHtml(content);
|
|
1153
|
+
return {
|
|
1154
|
+
code: [
|
|
1155
|
+
`import { h } from 'vue';`,
|
|
1156
|
+
`export const frontmatter = ${JSON.stringify(frontmatter)};`,
|
|
1157
|
+
`const _html = ${JSON.stringify(html)};`,
|
|
1158
|
+
`export default {`,
|
|
1159
|
+
` name: ${JSON.stringify(`MdPage_${relative(root, filePath).replace(/[^\w]/g, "_")}`)},`,
|
|
1160
|
+
` data() { return { frontmatter }; },`,
|
|
1161
|
+
` render() { return h('div', { class: 'ubean-md-page', innerHTML: _html }); }`,
|
|
1162
|
+
`};`
|
|
1163
|
+
].join("\n"),
|
|
1164
|
+
map: null
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
if (/\.(vue|tsx?|jsx?)$/.test(filePath) && code.includes("definePage")) return {
|
|
1168
|
+
code: stripDefinePageCalls(code),
|
|
1169
|
+
map: null
|
|
1170
|
+
};
|
|
1171
|
+
return null;
|
|
1172
|
+
},
|
|
1173
|
+
configureServer(server) {
|
|
1174
|
+
const pageExts = markdownEnabled ? /\.(vue|tsx?|jsx?|md|mdx)$/ : /\.(vue|tsx?|jsx?)$/;
|
|
1175
|
+
const onChange = (file) => {
|
|
1176
|
+
if (!pageExts.test(file) || !isUnderPagesDir(file)) return;
|
|
1177
|
+
rescan().then(() => {
|
|
1178
|
+
const mod = server.moduleGraph.getModuleById(RESOLVED_MODULE_ID);
|
|
1179
|
+
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
1180
|
+
server.ws.send({ type: "full-reload" });
|
|
1181
|
+
});
|
|
1182
|
+
};
|
|
1183
|
+
server.watcher.on("add", onChange);
|
|
1184
|
+
server.watcher.on("unlink", onChange);
|
|
1185
|
+
}
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
//#endregion
|
|
1189
|
+
export { VUE_ROUTES_MODULE_ID, ubeanVueVite as default, ubeanVueVite, extractCallObject, extractDefinePage, extractDefinePageFromCode, extractSlotAndIntercept, filePathToRoute, generateLayoutName, generatePagesModuleSource, generateRouteName, generateTypedRouter, generateVirtualModuleDts, normalizePageHead, parseMatchers, scanClientPages, scanPages, stripDefinePageCalls, stripRouteGroups };
|