@ubean/routing 0.1.2 → 0.1.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.
- package/dist/generator/index.d.ts +89 -0
- package/dist/generator/index.js +300 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.js +1057 -0
- package/dist/types.d.ts +157 -0
- package/dist/types.js +19 -0
- package/package.json +5 -5
package/dist/index.js
ADDED
|
@@ -0,0 +1,1057 @@
|
|
|
1
|
+
import { GLOB_LAYOUT_PATTERN, GLOB_SCAN_PATTERN, GLOB_VUE_PATTERN, HTTP_METHODS } from "./types.js";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { filePathToRoute, filePathToRoute as filePathToRoute$1, stripRouteGroups } from "@ubean/utils";
|
|
4
|
+
import { consola } from "consola";
|
|
5
|
+
import { basename, dirname, extname, isAbsolute, join, relative } from "pathe";
|
|
6
|
+
import { glob } from "tinyglobby";
|
|
7
|
+
import { pascalCase } from "scule";
|
|
8
|
+
import { addRoute, createRouter, findRoute } from "rou3";
|
|
9
|
+
//#region src/define-page.ts
|
|
10
|
+
function extractScriptContent(code) {
|
|
11
|
+
if (!code.includes("<script")) return code;
|
|
12
|
+
let scriptContent = "";
|
|
13
|
+
const scriptRegex = /<script([^>]*)>([\s\S]*?)<\/script>/g;
|
|
14
|
+
let match;
|
|
15
|
+
while ((match = scriptRegex.exec(code)) !== null) {
|
|
16
|
+
const attrs = match[1] || "";
|
|
17
|
+
const content = match[2] || "";
|
|
18
|
+
if (attrs.includes("setup")) return content;
|
|
19
|
+
scriptContent += `${content}\n`;
|
|
20
|
+
}
|
|
21
|
+
return scriptContent || null;
|
|
22
|
+
}
|
|
23
|
+
function findBalancedCall(code, funcName) {
|
|
24
|
+
const callPattern = new RegExp(`\\b${funcName}\\s*\\(`, "g");
|
|
25
|
+
let match;
|
|
26
|
+
while ((match = callPattern.exec(code)) !== null) {
|
|
27
|
+
const startIdx = match.index + match[0].length;
|
|
28
|
+
let depth = 1;
|
|
29
|
+
let i = startIdx;
|
|
30
|
+
let inString = null;
|
|
31
|
+
let escaped = false;
|
|
32
|
+
while (i < code.length && depth > 0) {
|
|
33
|
+
const ch = code[i];
|
|
34
|
+
if (escaped) {
|
|
35
|
+
escaped = false;
|
|
36
|
+
i++;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (ch === "\\") {
|
|
40
|
+
escaped = true;
|
|
41
|
+
i++;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (inString) {
|
|
45
|
+
if (ch === inString) inString = null;
|
|
46
|
+
i++;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (ch === "\"" || ch === "'" || ch === "`") {
|
|
50
|
+
inString = ch;
|
|
51
|
+
i++;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (ch === "(" || ch === "{" || ch === "[") depth++;
|
|
55
|
+
else if (ch === ")" || ch === "}" || ch === "]") {
|
|
56
|
+
depth--;
|
|
57
|
+
if (depth === 0 && ch === ")") return code.slice(startIdx, i);
|
|
58
|
+
}
|
|
59
|
+
i++;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
function skipWhitespace(code, pos) {
|
|
65
|
+
while (pos < code.length && /\s/.test(code[pos])) pos++;
|
|
66
|
+
return pos;
|
|
67
|
+
}
|
|
68
|
+
function parseStringValue(code, pos) {
|
|
69
|
+
const quote = code[pos];
|
|
70
|
+
if (quote !== "\"" && quote !== "'" && quote !== "`") return null;
|
|
71
|
+
pos++;
|
|
72
|
+
let value = "";
|
|
73
|
+
let escaped = false;
|
|
74
|
+
while (pos < code.length) {
|
|
75
|
+
const ch = code[pos];
|
|
76
|
+
if (escaped) {
|
|
77
|
+
value += ch;
|
|
78
|
+
escaped = false;
|
|
79
|
+
pos++;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (ch === "\\") {
|
|
83
|
+
escaped = true;
|
|
84
|
+
pos++;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (ch === quote) if (quote === "`") {
|
|
88
|
+
value += ch;
|
|
89
|
+
pos++;
|
|
90
|
+
if (pos >= code.length || code[pos] !== "`") {
|
|
91
|
+
pos--;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
pos++;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
value += ch;
|
|
99
|
+
pos++;
|
|
100
|
+
}
|
|
101
|
+
if (quote !== "`") return {
|
|
102
|
+
value,
|
|
103
|
+
pos
|
|
104
|
+
};
|
|
105
|
+
return {
|
|
106
|
+
value,
|
|
107
|
+
pos
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function parseIdentifier(code, pos) {
|
|
111
|
+
const start = pos;
|
|
112
|
+
while (pos < code.length && /[\w$]/.test(code[pos])) pos++;
|
|
113
|
+
if (pos === start) return null;
|
|
114
|
+
return {
|
|
115
|
+
name: code.slice(start, pos),
|
|
116
|
+
pos
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function parseValue(code, pos) {
|
|
120
|
+
pos = skipWhitespace(code, pos);
|
|
121
|
+
if (pos >= code.length) return null;
|
|
122
|
+
const ch = code[pos];
|
|
123
|
+
if (ch === "\"" || ch === "'" || ch === "`") {
|
|
124
|
+
const str = parseStringValue(code, pos);
|
|
125
|
+
if (str) return {
|
|
126
|
+
value: str.value,
|
|
127
|
+
pos: str.pos
|
|
128
|
+
};
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
if (ch === "{") return parseObjectValue(code, pos);
|
|
132
|
+
if (ch === "[") return parseArrayValue(code, pos);
|
|
133
|
+
if (ch === "t" && code.slice(pos, pos + 4) === "true") return {
|
|
134
|
+
value: true,
|
|
135
|
+
pos: pos + 4
|
|
136
|
+
};
|
|
137
|
+
if (ch === "f" && code.slice(pos, pos + 5) === "false") return {
|
|
138
|
+
value: false,
|
|
139
|
+
pos: pos + 5
|
|
140
|
+
};
|
|
141
|
+
if (ch === "n" && code.slice(pos, pos + 4) === "null") return {
|
|
142
|
+
value: null,
|
|
143
|
+
pos: pos + 4
|
|
144
|
+
};
|
|
145
|
+
const numMatch = code.slice(pos).match(/^-?\d+\.?\d*(?:[eE][+-]?\d+)?/);
|
|
146
|
+
if (numMatch) return {
|
|
147
|
+
value: Number(numMatch[0]),
|
|
148
|
+
pos: pos + numMatch[0].length
|
|
149
|
+
};
|
|
150
|
+
const ident = parseIdentifier(code, pos);
|
|
151
|
+
if (ident) return {
|
|
152
|
+
value: ident.name,
|
|
153
|
+
pos: ident.pos
|
|
154
|
+
};
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
function parseObjectValue(code, pos) {
|
|
158
|
+
if (code[pos] !== "{") return null;
|
|
159
|
+
pos++;
|
|
160
|
+
const result = {};
|
|
161
|
+
pos = skipWhitespace(code, pos);
|
|
162
|
+
if (code[pos] === "}") return {
|
|
163
|
+
value: result,
|
|
164
|
+
pos: pos + 1
|
|
165
|
+
};
|
|
166
|
+
while (pos < code.length) {
|
|
167
|
+
pos = skipWhitespace(code, pos);
|
|
168
|
+
let key = null;
|
|
169
|
+
if (code[pos] === "\"" || code[pos] === "'") {
|
|
170
|
+
const keyStr = parseStringValue(code, pos);
|
|
171
|
+
if (keyStr) {
|
|
172
|
+
key = keyStr.value;
|
|
173
|
+
pos = keyStr.pos;
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
const ident = parseIdentifier(code, pos);
|
|
177
|
+
if (ident) {
|
|
178
|
+
key = ident.name;
|
|
179
|
+
pos = ident.pos;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (!key) break;
|
|
183
|
+
pos = skipWhitespace(code, pos);
|
|
184
|
+
if (code[pos] !== ":") break;
|
|
185
|
+
pos++;
|
|
186
|
+
pos = skipWhitespace(code, pos);
|
|
187
|
+
const val = parseValue(code, pos);
|
|
188
|
+
if (val) {
|
|
189
|
+
result[key] = val.value;
|
|
190
|
+
pos = val.pos;
|
|
191
|
+
}
|
|
192
|
+
pos = skipWhitespace(code, pos);
|
|
193
|
+
if (code[pos] === ",") {
|
|
194
|
+
pos++;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (code[pos] === "}") {
|
|
198
|
+
pos++;
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
value: result,
|
|
205
|
+
pos
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function parseArrayValue(code, pos) {
|
|
209
|
+
if (code[pos] !== "[") return null;
|
|
210
|
+
pos++;
|
|
211
|
+
const result = [];
|
|
212
|
+
pos = skipWhitespace(code, pos);
|
|
213
|
+
if (code[pos] === "]") return {
|
|
214
|
+
value: result,
|
|
215
|
+
pos: pos + 1
|
|
216
|
+
};
|
|
217
|
+
while (pos < code.length) {
|
|
218
|
+
pos = skipWhitespace(code, pos);
|
|
219
|
+
const val = parseValue(code, pos);
|
|
220
|
+
if (val) {
|
|
221
|
+
result.push(val.value);
|
|
222
|
+
pos = val.pos;
|
|
223
|
+
}
|
|
224
|
+
pos = skipWhitespace(code, pos);
|
|
225
|
+
if (code[pos] === ",") {
|
|
226
|
+
pos++;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (code[pos] === "]") {
|
|
230
|
+
pos++;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
value: result,
|
|
237
|
+
pos
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function parseObjectLiteral(code) {
|
|
241
|
+
const start = skipWhitespace(code, 0);
|
|
242
|
+
if (code[start] !== "{") return {};
|
|
243
|
+
const parsed = parseObjectValue(code, start);
|
|
244
|
+
if (parsed) return parsed.value;
|
|
245
|
+
const result = {};
|
|
246
|
+
const simpleRegex = /(\w+)\s*:\s*(['"`])((?:(?!\2)[^\\]|\\.)*)\2/g;
|
|
247
|
+
let m;
|
|
248
|
+
while ((m = simpleRegex.exec(code)) !== null) result[m[1]] = m[3];
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
function extractAndParseCall(code, funcName) {
|
|
252
|
+
const argStr = findBalancedCall(code, funcName);
|
|
253
|
+
if (!argStr) return null;
|
|
254
|
+
const trimmed = argStr.trim();
|
|
255
|
+
if (!trimmed.startsWith("{")) return null;
|
|
256
|
+
return parseObjectLiteral(trimmed);
|
|
257
|
+
}
|
|
258
|
+
function extractDefinePageFromCode(code) {
|
|
259
|
+
const scriptContent = extractScriptContent(code);
|
|
260
|
+
if (!scriptContent) return null;
|
|
261
|
+
const parsed = extractAndParseCall(scriptContent, "definePage");
|
|
262
|
+
if (!parsed) return null;
|
|
263
|
+
const result = {};
|
|
264
|
+
if (typeof parsed.name === "string") result.name = parsed.name;
|
|
265
|
+
if (typeof parsed.path === "string") result.path = parsed.path;
|
|
266
|
+
if (parsed.layout === false || typeof parsed.layout === "string" && parsed.layout !== "default") result.layout = parsed.layout;
|
|
267
|
+
if (typeof parsed.reuse === "string") result.reuse = parsed.reuse;
|
|
268
|
+
if (parsed.meta && typeof parsed.meta === "object") result.meta = parsed.meta;
|
|
269
|
+
if (typeof parsed.requiresAuth === "boolean") result.requiresAuth = parsed.requiresAuth;
|
|
270
|
+
if (typeof parsed.cache === "boolean") result.cache = parsed.cache;
|
|
271
|
+
if (typeof parsed.middleware === "string") result.middleware = parsed.middleware;
|
|
272
|
+
else if (Array.isArray(parsed.middleware)) result.middleware = parsed.middleware.filter((m) => typeof m === "string");
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
function extractDefineMetaFromCode(code) {
|
|
276
|
+
const scriptContent = extractScriptContent(code);
|
|
277
|
+
if (!scriptContent) return null;
|
|
278
|
+
const parsed = extractAndParseCall(scriptContent, "defineHandlerMeta");
|
|
279
|
+
if (!parsed) return null;
|
|
280
|
+
const result = {};
|
|
281
|
+
if (typeof parsed.requiresAuth === "boolean") result.requiresAuth = parsed.requiresAuth;
|
|
282
|
+
if (parsed.meta && typeof parsed.meta === "object") result.meta = parsed.meta;
|
|
283
|
+
else {
|
|
284
|
+
const knownKeys = /* @__PURE__ */ new Set(["requiresAuth", "meta"]);
|
|
285
|
+
const extra = {};
|
|
286
|
+
for (const [key, val] of Object.entries(parsed)) if (!knownKeys.has(key)) extra[key] = val;
|
|
287
|
+
if (Object.keys(extra).length > 0) result.meta = extra;
|
|
288
|
+
}
|
|
289
|
+
return result;
|
|
290
|
+
}
|
|
291
|
+
async function extractDefinePage(filePath) {
|
|
292
|
+
return extractDefinePageFromCode(await readFile(filePath, "utf-8"));
|
|
293
|
+
}
|
|
294
|
+
async function extractDefineMeta(filePath) {
|
|
295
|
+
return extractDefineMetaFromCode(await readFile(filePath, "utf-8"));
|
|
296
|
+
}
|
|
297
|
+
//#endregion
|
|
298
|
+
//#region src/detect-exports.ts
|
|
299
|
+
const EXPORT_NAMED_REGEX = /export\s+(?:async\s+)?(?:function\s+|const\s+|let\s+|var\s+)?(\w+)/g;
|
|
300
|
+
const EXPORT_LIST_REGEX = /export\s*\{([^}]+)\}/g;
|
|
301
|
+
const EXPORT_DEFINE_META_REGEX = /defineHandlerMeta\s*\(/;
|
|
302
|
+
const EXPORT_CONST_META_REGEX = /export\s+const\s+meta\s*=\s*(\{[\s\S]*?\})(?:\s*;|\s*$)/m;
|
|
303
|
+
const META_REQUIRES_AUTH_REGEX = /requiresAuth\s*:\s*(true|false)/;
|
|
304
|
+
async function detectHttpExports(filePath) {
|
|
305
|
+
return detectHttpExportsFromCode(await readFile(filePath, "utf-8"));
|
|
306
|
+
}
|
|
307
|
+
function detectHttpExportsFromCode(code) {
|
|
308
|
+
const exports = /* @__PURE__ */ new Set();
|
|
309
|
+
const httpMethods = [];
|
|
310
|
+
let match;
|
|
311
|
+
EXPORT_NAMED_REGEX.lastIndex = 0;
|
|
312
|
+
while ((match = EXPORT_NAMED_REGEX.exec(code)) !== null) {
|
|
313
|
+
const name = match[1];
|
|
314
|
+
exports.add(name);
|
|
315
|
+
const lower = name.toLowerCase();
|
|
316
|
+
if (HTTP_METHODS.includes(name) && !httpMethods.includes(lower)) httpMethods.push(lower);
|
|
317
|
+
}
|
|
318
|
+
EXPORT_LIST_REGEX.lastIndex = 0;
|
|
319
|
+
while ((match = EXPORT_LIST_REGEX.exec(code)) !== null) {
|
|
320
|
+
const list = match[1];
|
|
321
|
+
for (const item of list.split(",")) {
|
|
322
|
+
const name = item.trim().split(/\s+as\s+/).pop()?.trim();
|
|
323
|
+
if (name) {
|
|
324
|
+
exports.add(name);
|
|
325
|
+
const upper = name.toUpperCase();
|
|
326
|
+
const lower = name.toLowerCase();
|
|
327
|
+
if (HTTP_METHODS.includes(upper) && !httpMethods.includes(lower)) httpMethods.push(lower);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const hasDefineMeta = EXPORT_DEFINE_META_REGEX.test(code);
|
|
332
|
+
const fileMeta = extractFileMeta(code);
|
|
333
|
+
const hasMeta = hasDefineMeta || !!fileMeta;
|
|
334
|
+
return {
|
|
335
|
+
exports: [...exports],
|
|
336
|
+
httpMethods,
|
|
337
|
+
hasMeta,
|
|
338
|
+
fileMeta
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
function extractFileMeta(code) {
|
|
342
|
+
const match = code.match(EXPORT_CONST_META_REGEX);
|
|
343
|
+
if (!match) return void 0;
|
|
344
|
+
const metaBlock = match[1];
|
|
345
|
+
const meta = {};
|
|
346
|
+
const requiresAuthMatch = metaBlock.match(META_REQUIRES_AUTH_REGEX);
|
|
347
|
+
if (requiresAuthMatch) meta.requiresAuth = requiresAuthMatch[1] === "true";
|
|
348
|
+
return Object.keys(meta).length > 0 ? meta : void 0;
|
|
349
|
+
}
|
|
350
|
+
//#endregion
|
|
351
|
+
//#region src/scan.ts
|
|
352
|
+
const logger = consola.withTag("ubean-routing");
|
|
353
|
+
let _frontmatterParser;
|
|
354
|
+
async function getFrontmatterParser() {
|
|
355
|
+
if (_frontmatterParser !== void 0) return _frontmatterParser;
|
|
356
|
+
try {
|
|
357
|
+
_frontmatterParser = (await import("@ubean/markdown")).parseFrontmatter;
|
|
358
|
+
} catch {
|
|
359
|
+
logger.warn("@ubean/markdown is not installed. Markdown frontmatter parsing will be skipped. Install it as a dependency to enable frontmatter-based page metadata.");
|
|
360
|
+
_frontmatterParser = null;
|
|
361
|
+
}
|
|
362
|
+
return _frontmatterParser;
|
|
363
|
+
}
|
|
364
|
+
const DEFAULT_DIRS = {
|
|
365
|
+
routes: "routes",
|
|
366
|
+
middleware: "middleware",
|
|
367
|
+
pages: "pages",
|
|
368
|
+
layouts: "layouts",
|
|
369
|
+
plugins: "plugins",
|
|
370
|
+
crons: "crons",
|
|
371
|
+
queues: "queues",
|
|
372
|
+
locales: "locales"
|
|
373
|
+
};
|
|
374
|
+
/**
|
|
375
|
+
* Normalize a `string | string[]` dir entry into `string[]`. Falsy values
|
|
376
|
+
* fall back to the provided default so callers always receive a non-empty
|
|
377
|
+
* list. Empty arrays are also replaced with the default to keep downstream
|
|
378
|
+
* globbing simple.
|
|
379
|
+
*/
|
|
380
|
+
function normalizeDirs(value, fallback) {
|
|
381
|
+
if (value === void 0 || value === null || value === "") return [fallback];
|
|
382
|
+
if (Array.isArray(value)) {
|
|
383
|
+
const filtered = value.filter((d) => typeof d === "string" && d.length > 0);
|
|
384
|
+
return filtered.length > 0 ? filtered : [fallback];
|
|
385
|
+
}
|
|
386
|
+
return [value];
|
|
387
|
+
}
|
|
388
|
+
function splitOrderPrefix(name) {
|
|
389
|
+
const match = name.match(/^(\d+)\.(.+)$/);
|
|
390
|
+
if (match) return {
|
|
391
|
+
order: Number(match[1]),
|
|
392
|
+
cleanName: match[2]
|
|
393
|
+
};
|
|
394
|
+
return {
|
|
395
|
+
order: 0,
|
|
396
|
+
cleanName: name
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
function toPosixPath(p) {
|
|
400
|
+
return p.replace(/\\/g, "/");
|
|
401
|
+
}
|
|
402
|
+
async function scanProject(options) {
|
|
403
|
+
const dirs = {
|
|
404
|
+
...DEFAULT_DIRS,
|
|
405
|
+
...options.dirs
|
|
406
|
+
};
|
|
407
|
+
const ignore = options.ignore || [
|
|
408
|
+
"**/*.test.*",
|
|
409
|
+
"**/*.spec.*",
|
|
410
|
+
"**/_*",
|
|
411
|
+
"**/*.d.ts"
|
|
412
|
+
];
|
|
413
|
+
const srcDir = isAbsolute(options.srcDir) ? options.srcDir : join(options.cwd, options.srcDir);
|
|
414
|
+
const [apiRoutes, middlewares, pages, layouts, plugins, crons, queues, locales, appEntry, serverEntry] = await Promise.all([
|
|
415
|
+
scanApiRoutes(srcDir, dirs.routes, ignore),
|
|
416
|
+
scanMiddlewares(srcDir, dirs.middleware, ignore),
|
|
417
|
+
scanPages(srcDir, dirs.pages, ignore),
|
|
418
|
+
scanLayouts(srcDir, dirs.layouts, ignore),
|
|
419
|
+
scanPlugins(srcDir, dirs.plugins, ignore),
|
|
420
|
+
scanCrons(srcDir, dirs.crons, ignore),
|
|
421
|
+
scanQueues(srcDir, dirs.queues, ignore),
|
|
422
|
+
scanLocales(srcDir, dirs.locales, ignore),
|
|
423
|
+
scanAppEntry(srcDir),
|
|
424
|
+
scanServerEntry(srcDir)
|
|
425
|
+
]);
|
|
426
|
+
const regularPageNames = new Set(pages.filter((p) => !p.isReuse).map((p) => p.name));
|
|
427
|
+
for (const page of pages) if (page.isReuse && page.reuseTarget && !regularPageNames.has(page.reuseTarget)) logger.warn(`Reuse page "${page.name}" references target "${page.reuseTarget}" which does not exist. Available page targets: ${[...regularPageNames].join(", ") || "(none)"}`);
|
|
428
|
+
return {
|
|
429
|
+
apiRoutes,
|
|
430
|
+
middlewares,
|
|
431
|
+
pages,
|
|
432
|
+
layouts,
|
|
433
|
+
plugins,
|
|
434
|
+
crons,
|
|
435
|
+
queues,
|
|
436
|
+
locales,
|
|
437
|
+
defaultLocale: locales.find((l) => l.isDefault)?.code || locales[0]?.code,
|
|
438
|
+
appEntry,
|
|
439
|
+
serverEntry
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
async function scanApiRoutes(srcDir, dirName, ignore) {
|
|
443
|
+
const dirNames = normalizeDirs(dirName, "routes");
|
|
444
|
+
const routes = [];
|
|
445
|
+
for (const single of dirNames) {
|
|
446
|
+
const dir = join(srcDir, single);
|
|
447
|
+
const files = await glob(GLOB_SCAN_PATTERN, {
|
|
448
|
+
cwd: dir,
|
|
449
|
+
dot: true,
|
|
450
|
+
ignore: [
|
|
451
|
+
...ignore,
|
|
452
|
+
"**/*.vue",
|
|
453
|
+
"**/_*"
|
|
454
|
+
],
|
|
455
|
+
absolute: true
|
|
456
|
+
}).catch(() => []);
|
|
457
|
+
for (const fullPath of files.sort()) {
|
|
458
|
+
const relativePath = toPosixPath(relative(dir, fullPath));
|
|
459
|
+
const base = basename(relativePath, extname(relativePath));
|
|
460
|
+
const dirPart = dirname(relativePath) === "." ? "" : dirname(relativePath);
|
|
461
|
+
const parsed = filePathToRoute$1(dirPart ? `${dirPart}/${base}` : base);
|
|
462
|
+
const detected = await detectHttpExports(fullPath).catch(() => ({
|
|
463
|
+
exports: [],
|
|
464
|
+
httpMethods: [],
|
|
465
|
+
hasMeta: false,
|
|
466
|
+
fileMeta: void 0
|
|
467
|
+
}));
|
|
468
|
+
const methods = detected.httpMethods.length > 0 ? detected.httpMethods : parsed.method ? [parsed.method.toLowerCase()] : [];
|
|
469
|
+
for (const method of methods.length > 0 ? methods : HTTP_METHODS.map((m) => m.toLowerCase())) routes.push({
|
|
470
|
+
fullPath,
|
|
471
|
+
relativePath,
|
|
472
|
+
dirname: dirname(relativePath),
|
|
473
|
+
basename: basename(relativePath),
|
|
474
|
+
route: parsed.route,
|
|
475
|
+
method,
|
|
476
|
+
env: parsed.env,
|
|
477
|
+
exports: detected.exports,
|
|
478
|
+
hasMeta: detected.hasMeta,
|
|
479
|
+
fileMeta: detected.fileMeta
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return routes;
|
|
484
|
+
}
|
|
485
|
+
async function scanMiddlewares(srcDir, dirName, ignore) {
|
|
486
|
+
const dirNames = normalizeDirs(dirName, "middleware");
|
|
487
|
+
const results = [];
|
|
488
|
+
for (const single of dirNames) {
|
|
489
|
+
const dir = join(srcDir, single);
|
|
490
|
+
const files = await glob(GLOB_SCAN_PATTERN, {
|
|
491
|
+
cwd: dir,
|
|
492
|
+
dot: true,
|
|
493
|
+
ignore: [
|
|
494
|
+
...ignore,
|
|
495
|
+
"**/*.vue",
|
|
496
|
+
"**/index.*",
|
|
497
|
+
"**/_*"
|
|
498
|
+
],
|
|
499
|
+
absolute: true
|
|
500
|
+
}).catch(() => []);
|
|
501
|
+
for (const fullPath of files.sort()) {
|
|
502
|
+
const relativePath = toPosixPath(relative(dir, fullPath));
|
|
503
|
+
const { order, cleanName } = splitOrderPrefix(basename(relativePath, extname(relativePath)));
|
|
504
|
+
results.push({
|
|
505
|
+
fullPath,
|
|
506
|
+
relativePath,
|
|
507
|
+
dirname: dirname(relativePath),
|
|
508
|
+
basename: basename(relativePath),
|
|
509
|
+
order,
|
|
510
|
+
global: cleanName === "global" || cleanName.startsWith("global.")
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return results;
|
|
515
|
+
}
|
|
516
|
+
async function scanPages(srcDir, dirName, ignore) {
|
|
517
|
+
const dirNames = normalizeDirs(dirName, "pages");
|
|
518
|
+
const pages = [];
|
|
519
|
+
const seenFullPaths = /* @__PURE__ */ new Set();
|
|
520
|
+
for (const single of dirNames) {
|
|
521
|
+
const dir = join(srcDir, single);
|
|
522
|
+
const files = await glob(GLOB_VUE_PATTERN, {
|
|
523
|
+
cwd: dir,
|
|
524
|
+
dot: true,
|
|
525
|
+
ignore: [
|
|
526
|
+
...ignore,
|
|
527
|
+
"**/components/**",
|
|
528
|
+
"**/_*"
|
|
529
|
+
],
|
|
530
|
+
absolute: true
|
|
531
|
+
}).catch(() => []);
|
|
532
|
+
for (const fullPath of files.sort()) {
|
|
533
|
+
if (seenFullPaths.has(fullPath)) continue;
|
|
534
|
+
seenFullPaths.add(fullPath);
|
|
535
|
+
const relativePath = toPosixPath(relative(dir, fullPath));
|
|
536
|
+
const ext = extname(relativePath);
|
|
537
|
+
const base = basename(relativePath, ext);
|
|
538
|
+
if (base.startsWith("_")) continue;
|
|
539
|
+
const isMarkdown = ext === ".md" || ext === ".mdx";
|
|
540
|
+
const isReuse = !isMarkdown && base.endsWith(".reuse");
|
|
541
|
+
const pageBase = isReuse ? base.slice(0, -6) : base;
|
|
542
|
+
const dirPart = dirname(relativePath) === "." ? "" : dirname(relativePath);
|
|
543
|
+
const { route } = filePathToRoute$1(dirPart ? `${dirPart}/${pageBase}` : pageBase);
|
|
544
|
+
const name = routeToName(route);
|
|
545
|
+
let pageMeta = null;
|
|
546
|
+
let frontmatter;
|
|
547
|
+
if (isMarkdown) try {
|
|
548
|
+
const content = await readFile(fullPath, "utf-8");
|
|
549
|
+
const parser = await getFrontmatterParser();
|
|
550
|
+
if (parser) {
|
|
551
|
+
frontmatter = parser(content).data;
|
|
552
|
+
pageMeta = {
|
|
553
|
+
name: frontmatter?.name || name,
|
|
554
|
+
path: frontmatter?.path || route,
|
|
555
|
+
layout: frontmatter?.layout,
|
|
556
|
+
cache: frontmatter?.cache,
|
|
557
|
+
head: buildMarkdownHead(frontmatter)
|
|
558
|
+
};
|
|
559
|
+
} else pageMeta = {
|
|
560
|
+
name,
|
|
561
|
+
path: route
|
|
562
|
+
};
|
|
563
|
+
} catch {
|
|
564
|
+
pageMeta = null;
|
|
565
|
+
}
|
|
566
|
+
else pageMeta = await extractDefinePage(fullPath).catch(() => null);
|
|
567
|
+
pages.push({
|
|
568
|
+
fullPath,
|
|
569
|
+
relativePath,
|
|
570
|
+
dirname: dirname(relativePath),
|
|
571
|
+
basename: basename(relativePath),
|
|
572
|
+
name: pageMeta?.name || name,
|
|
573
|
+
route: pageMeta?.path || route,
|
|
574
|
+
path: pageMeta?.path || route,
|
|
575
|
+
layout: pageMeta?.layout,
|
|
576
|
+
cache: pageMeta?.cache,
|
|
577
|
+
isReuse,
|
|
578
|
+
isMarkdown,
|
|
579
|
+
reuseTarget: pageMeta?.reuse,
|
|
580
|
+
pageMeta: pageMeta || void 0,
|
|
581
|
+
frontmatter
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return pages;
|
|
586
|
+
}
|
|
587
|
+
async function scanLayouts(srcDir, dirName, ignore) {
|
|
588
|
+
const dirNames = normalizeDirs(dirName, "layouts");
|
|
589
|
+
const results = [];
|
|
590
|
+
const seenFullPaths = /* @__PURE__ */ new Set();
|
|
591
|
+
for (const single of dirNames) {
|
|
592
|
+
const dir = join(srcDir, single);
|
|
593
|
+
const files = await glob(GLOB_LAYOUT_PATTERN, {
|
|
594
|
+
cwd: dir,
|
|
595
|
+
dot: true,
|
|
596
|
+
ignore: [...ignore, "**/_*"],
|
|
597
|
+
absolute: true
|
|
598
|
+
}).catch(() => []);
|
|
599
|
+
for (const fullPath of files.sort()) {
|
|
600
|
+
if (seenFullPaths.has(fullPath)) continue;
|
|
601
|
+
seenFullPaths.add(fullPath);
|
|
602
|
+
const relativePath = toPosixPath(relative(dir, fullPath));
|
|
603
|
+
const base = basename(relativePath, extname(relativePath));
|
|
604
|
+
const dirPart = dirname(relativePath) === "." ? "" : dirname(relativePath);
|
|
605
|
+
const layoutBase = base === "index" ? dirPart : dirPart ? `${dirPart}/${base}` : base;
|
|
606
|
+
const isDefault = base === "default" || base === "index" && !dirPart;
|
|
607
|
+
const name = layoutBase || "default";
|
|
608
|
+
results.push({
|
|
609
|
+
fullPath,
|
|
610
|
+
relativePath,
|
|
611
|
+
dirname: dirname(relativePath),
|
|
612
|
+
basename: basename(relativePath),
|
|
613
|
+
name,
|
|
614
|
+
path: toPosixPath(relativePath),
|
|
615
|
+
isDefault
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return results;
|
|
620
|
+
}
|
|
621
|
+
async function scanPlugins(srcDir, dirName, ignore) {
|
|
622
|
+
const dirNames = normalizeDirs(dirName, "plugins");
|
|
623
|
+
const results = [];
|
|
624
|
+
for (const single of dirNames) {
|
|
625
|
+
const dir = join(srcDir, single);
|
|
626
|
+
const files = await glob(GLOB_SCAN_PATTERN, {
|
|
627
|
+
cwd: dir,
|
|
628
|
+
dot: true,
|
|
629
|
+
ignore: [
|
|
630
|
+
...ignore,
|
|
631
|
+
"**/*.vue",
|
|
632
|
+
"**/_*"
|
|
633
|
+
],
|
|
634
|
+
absolute: true
|
|
635
|
+
}).catch(() => []);
|
|
636
|
+
for (const fullPath of files.sort()) {
|
|
637
|
+
const relativePath = toPosixPath(relative(dir, fullPath));
|
|
638
|
+
const { order } = splitOrderPrefix(basename(relativePath, extname(relativePath)));
|
|
639
|
+
results.push({
|
|
640
|
+
fullPath,
|
|
641
|
+
relativePath,
|
|
642
|
+
dirname: dirname(relativePath),
|
|
643
|
+
basename: basename(relativePath),
|
|
644
|
+
order
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return results;
|
|
649
|
+
}
|
|
650
|
+
const APP_EXTENSIONS = [
|
|
651
|
+
"ts",
|
|
652
|
+
"js",
|
|
653
|
+
"mjs",
|
|
654
|
+
"mts"
|
|
655
|
+
];
|
|
656
|
+
async function scanCrons(srcDir, dirName, ignore) {
|
|
657
|
+
const dirNames = normalizeDirs(dirName, "crons");
|
|
658
|
+
const results = [];
|
|
659
|
+
for (const single of dirNames) {
|
|
660
|
+
const dir = join(srcDir, single);
|
|
661
|
+
const files = await glob(GLOB_SCAN_PATTERN, {
|
|
662
|
+
cwd: dir,
|
|
663
|
+
dot: true,
|
|
664
|
+
ignore: [
|
|
665
|
+
...ignore,
|
|
666
|
+
"**/*.vue",
|
|
667
|
+
"**/_*"
|
|
668
|
+
],
|
|
669
|
+
absolute: true
|
|
670
|
+
}).catch(() => []);
|
|
671
|
+
for (const fullPath of files.sort()) {
|
|
672
|
+
const relativePath = toPosixPath(relative(dir, fullPath));
|
|
673
|
+
const { cleanName } = splitOrderPrefix(basename(relativePath, extname(relativePath)));
|
|
674
|
+
results.push({
|
|
675
|
+
fullPath,
|
|
676
|
+
relativePath,
|
|
677
|
+
dirname: dirname(relativePath),
|
|
678
|
+
basename: basename(relativePath),
|
|
679
|
+
name: cleanName
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return results;
|
|
684
|
+
}
|
|
685
|
+
async function scanQueues(srcDir, dirName, ignore) {
|
|
686
|
+
const dirNames = normalizeDirs(dirName, "queues");
|
|
687
|
+
const results = [];
|
|
688
|
+
for (const single of dirNames) {
|
|
689
|
+
const dir = join(srcDir, single);
|
|
690
|
+
const files = await glob(GLOB_SCAN_PATTERN, {
|
|
691
|
+
cwd: dir,
|
|
692
|
+
dot: true,
|
|
693
|
+
ignore: [
|
|
694
|
+
...ignore,
|
|
695
|
+
"**/*.vue",
|
|
696
|
+
"**/_*"
|
|
697
|
+
],
|
|
698
|
+
absolute: true
|
|
699
|
+
}).catch(() => []);
|
|
700
|
+
for (const fullPath of files.sort()) {
|
|
701
|
+
const relativePath = toPosixPath(relative(dir, fullPath));
|
|
702
|
+
const { cleanName } = splitOrderPrefix(basename(relativePath, extname(relativePath)));
|
|
703
|
+
results.push({
|
|
704
|
+
fullPath,
|
|
705
|
+
relativePath,
|
|
706
|
+
dirname: dirname(relativePath),
|
|
707
|
+
basename: basename(relativePath),
|
|
708
|
+
name: cleanName
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return results;
|
|
713
|
+
}
|
|
714
|
+
const LOCALE_GLOB_PATTERN = "**/*.{json,json5,yaml,yml,js,mjs,cjs,ts,mts,cts}";
|
|
715
|
+
function parseSimpleYaml(content) {
|
|
716
|
+
const result = {};
|
|
717
|
+
const lines = content.split("\n");
|
|
718
|
+
let currentObj = result;
|
|
719
|
+
const stack = [];
|
|
720
|
+
for (const line of lines) {
|
|
721
|
+
const trimmed = line.replace(/#.*$/, "").trimEnd();
|
|
722
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
723
|
+
const indent = line.length - line.trimStart().length;
|
|
724
|
+
const match = trimmed.match(/^([\w.-]+)\s*:\s*(.*)$/);
|
|
725
|
+
if (!match) continue;
|
|
726
|
+
const [, key, value] = match;
|
|
727
|
+
while (stack.length > 0 && stack[stack.length - 1].indent >= indent) stack.pop();
|
|
728
|
+
currentObj = stack.length > 0 ? stack[stack.length - 1].obj : result;
|
|
729
|
+
if (value === "" || value === void 0) {
|
|
730
|
+
const newObj = {};
|
|
731
|
+
currentObj[key] = newObj;
|
|
732
|
+
stack.push({
|
|
733
|
+
obj: newObj,
|
|
734
|
+
indent
|
|
735
|
+
});
|
|
736
|
+
currentObj = newObj;
|
|
737
|
+
} else {
|
|
738
|
+
let parsed = value;
|
|
739
|
+
if (value === "true") parsed = true;
|
|
740
|
+
else if (value === "false") parsed = false;
|
|
741
|
+
else if (value === "null") parsed = null;
|
|
742
|
+
else if (/^-?\d+$/.test(value)) parsed = Number.parseInt(value, 10);
|
|
743
|
+
else if (/^-?\d+\.\d+$/.test(value)) parsed = Number.parseFloat(value);
|
|
744
|
+
else if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) parsed = value.slice(1, -1);
|
|
745
|
+
currentObj[key] = parsed;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return result;
|
|
749
|
+
}
|
|
750
|
+
async function loadLocaleFile(fullPath, ext) {
|
|
751
|
+
const extract = (data) => {
|
|
752
|
+
const isWrapper = (typeof data.name === "string" || data.dir === "ltr" || data.dir === "rtl" || typeof data.isDefault === "boolean") && typeof data.messages === "object" && data.messages !== null;
|
|
753
|
+
return {
|
|
754
|
+
messages: isWrapper ? data.messages : data,
|
|
755
|
+
meta: isWrapper ? {
|
|
756
|
+
name: data.name,
|
|
757
|
+
dir: data.dir,
|
|
758
|
+
isDefault: data.isDefault
|
|
759
|
+
} : void 0
|
|
760
|
+
};
|
|
761
|
+
};
|
|
762
|
+
if (ext === ".json" || ext === ".json5") {
|
|
763
|
+
const content = await readFile(fullPath, "utf-8");
|
|
764
|
+
return extract(JSON.parse(content));
|
|
765
|
+
}
|
|
766
|
+
if (ext === ".yaml" || ext === ".yml") return extract(parseSimpleYaml(await readFile(fullPath, "utf-8")));
|
|
767
|
+
if (ext === ".js" || ext === ".mjs" || ext === ".cjs" || ext === ".ts" || ext === ".mts" || ext === ".cts") {
|
|
768
|
+
const mod = await import(
|
|
769
|
+
/* @vite-ignore */
|
|
770
|
+
fullPath
|
|
771
|
+
).catch(() => null);
|
|
772
|
+
if (mod) return extract(mod.default || mod);
|
|
773
|
+
}
|
|
774
|
+
return { messages: {} };
|
|
775
|
+
}
|
|
776
|
+
async function scanLocales(srcDir, dirName, ignore) {
|
|
777
|
+
const dirNames = normalizeDirs(dirName, "locales");
|
|
778
|
+
const locales = [];
|
|
779
|
+
const seenFullPaths = /* @__PURE__ */ new Set();
|
|
780
|
+
for (const single of dirNames) {
|
|
781
|
+
const dir = join(srcDir, single);
|
|
782
|
+
const files = await glob(LOCALE_GLOB_PATTERN, {
|
|
783
|
+
cwd: dir,
|
|
784
|
+
dot: true,
|
|
785
|
+
ignore: [
|
|
786
|
+
...ignore,
|
|
787
|
+
"**/*.vue",
|
|
788
|
+
"**/_*"
|
|
789
|
+
],
|
|
790
|
+
absolute: true
|
|
791
|
+
}).catch(() => []);
|
|
792
|
+
for (const fullPath of files.sort()) {
|
|
793
|
+
if (seenFullPaths.has(fullPath)) continue;
|
|
794
|
+
seenFullPaths.add(fullPath);
|
|
795
|
+
const relativePath = toPosixPath(relative(dir, fullPath));
|
|
796
|
+
const ext = extname(relativePath);
|
|
797
|
+
const base = basename(relativePath, ext);
|
|
798
|
+
const { order: _order, cleanName: code } = splitOrderPrefix(base);
|
|
799
|
+
const dirPart = dirname(relativePath) === "." ? "" : dirname(relativePath);
|
|
800
|
+
let namespace;
|
|
801
|
+
let finalCode = code;
|
|
802
|
+
let isDefault = code === "default" || base.startsWith("default.");
|
|
803
|
+
if (dirPart) {
|
|
804
|
+
const dirParts = dirPart.split("/");
|
|
805
|
+
const isIndexFile = code === "index";
|
|
806
|
+
finalCode = dirParts[0];
|
|
807
|
+
const nsParts = [];
|
|
808
|
+
nsParts.push(...dirParts.slice(1));
|
|
809
|
+
if (!isIndexFile) nsParts.push(code);
|
|
810
|
+
if (nsParts.length > 0) namespace = nsParts.join(".");
|
|
811
|
+
}
|
|
812
|
+
let name;
|
|
813
|
+
let dirVal;
|
|
814
|
+
try {
|
|
815
|
+
const { meta } = await loadLocaleFile(fullPath, ext);
|
|
816
|
+
if (meta?.name) name = meta.name;
|
|
817
|
+
if (meta?.dir) dirVal = meta.dir;
|
|
818
|
+
if (meta?.isDefault) isDefault = true;
|
|
819
|
+
} catch {
|
|
820
|
+
logger.warn(`Failed to parse locale file: ${relativePath}`);
|
|
821
|
+
}
|
|
822
|
+
if (isDefault && finalCode === "default") finalCode = "en";
|
|
823
|
+
locales.push({
|
|
824
|
+
fullPath,
|
|
825
|
+
relativePath,
|
|
826
|
+
dirname: dirname(relativePath),
|
|
827
|
+
basename: basename(relativePath),
|
|
828
|
+
code: finalCode,
|
|
829
|
+
namespace,
|
|
830
|
+
isDefault,
|
|
831
|
+
name,
|
|
832
|
+
dir: dirVal
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return locales;
|
|
837
|
+
}
|
|
838
|
+
async function scanAppEntry(srcDir) {
|
|
839
|
+
const result = {
|
|
840
|
+
shared: { exists: false },
|
|
841
|
+
server: { exists: false },
|
|
842
|
+
client: { exists: false }
|
|
843
|
+
};
|
|
844
|
+
const findEntry = async (baseName) => {
|
|
845
|
+
for (const ext of APP_EXTENSIONS) {
|
|
846
|
+
const fullPath = join(srcDir, `${baseName}.${ext}`);
|
|
847
|
+
if (await fileExists(fullPath)) return {
|
|
848
|
+
exists: true,
|
|
849
|
+
fullPath,
|
|
850
|
+
relativePath: `${baseName}.${ext}`
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
return { exists: false };
|
|
854
|
+
};
|
|
855
|
+
const [shared, server, client] = await Promise.all([
|
|
856
|
+
findEntry("app"),
|
|
857
|
+
findEntry("app.server"),
|
|
858
|
+
findEntry("app.client")
|
|
859
|
+
]);
|
|
860
|
+
result.shared = shared;
|
|
861
|
+
result.server = server;
|
|
862
|
+
result.client = client;
|
|
863
|
+
return result;
|
|
864
|
+
}
|
|
865
|
+
async function scanServerEntry(srcDir) {
|
|
866
|
+
const result = {
|
|
867
|
+
shared: { exists: false },
|
|
868
|
+
dev: { exists: false },
|
|
869
|
+
prod: { exists: false }
|
|
870
|
+
};
|
|
871
|
+
const findEntry = async (baseName) => {
|
|
872
|
+
for (const ext of APP_EXTENSIONS) {
|
|
873
|
+
const fullPath = join(srcDir, `${baseName}.${ext}`);
|
|
874
|
+
if (await fileExists(fullPath)) return {
|
|
875
|
+
exists: true,
|
|
876
|
+
fullPath,
|
|
877
|
+
relativePath: `${baseName}.${ext}`
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
return { exists: false };
|
|
881
|
+
};
|
|
882
|
+
const [shared, dev, prod] = await Promise.all([
|
|
883
|
+
findEntry("server"),
|
|
884
|
+
findEntry("server.dev"),
|
|
885
|
+
findEntry("server.prod")
|
|
886
|
+
]);
|
|
887
|
+
result.shared = shared;
|
|
888
|
+
result.dev = dev;
|
|
889
|
+
result.prod = prod;
|
|
890
|
+
return result;
|
|
891
|
+
}
|
|
892
|
+
async function fileExists(path) {
|
|
893
|
+
const { existsSync } = await import("node:fs");
|
|
894
|
+
return existsSync(path);
|
|
895
|
+
}
|
|
896
|
+
function routeToName(route) {
|
|
897
|
+
if (route === "/") return "Index";
|
|
898
|
+
return route.replace(/^\//, "").replace(/\/$/, "").split("/").map((seg) => {
|
|
899
|
+
if (seg.startsWith(":")) return seg.slice(1).replace("?", "").split("_").map(capitalize).join("");
|
|
900
|
+
return seg.split(/[-_]/).map(capitalize).join("");
|
|
901
|
+
}).join("");
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Build a `PageHead` from Markdown frontmatter.
|
|
905
|
+
*
|
|
906
|
+
* Only the `head` field is used, matching `unplugin-vue-markdown`'s
|
|
907
|
+
* `headField: 'head'` option so SSR and client-side `useHead` stay in sync.
|
|
908
|
+
*
|
|
909
|
+
* Supported shape:
|
|
910
|
+
* ```yaml
|
|
911
|
+
* head:
|
|
912
|
+
* title: "Page Title"
|
|
913
|
+
* meta:
|
|
914
|
+
* - name: description
|
|
915
|
+
* content: ...
|
|
916
|
+
* link:
|
|
917
|
+
* - rel: canonical
|
|
918
|
+
* href: ...
|
|
919
|
+
* ```
|
|
920
|
+
*/
|
|
921
|
+
function buildMarkdownHead(fm) {
|
|
922
|
+
if (!fm || typeof fm.head !== "object" || fm.head === null) return void 0;
|
|
923
|
+
const fmHead = fm.head;
|
|
924
|
+
const head = {};
|
|
925
|
+
if (typeof fmHead.title === "string") head.title = fmHead.title;
|
|
926
|
+
if (Array.isArray(fmHead.meta)) head.meta = fmHead.meta;
|
|
927
|
+
if (Array.isArray(fmHead.link)) head.link = fmHead.link;
|
|
928
|
+
if (Array.isArray(fmHead.script)) head.script = fmHead.script;
|
|
929
|
+
if (fmHead.htmlAttrs && typeof fmHead.htmlAttrs === "object") head.htmlAttrs = fmHead.htmlAttrs;
|
|
930
|
+
if (fmHead.bodyAttrs && typeof fmHead.bodyAttrs === "object") head.bodyAttrs = fmHead.bodyAttrs;
|
|
931
|
+
return Object.keys(head).length > 0 ? head : void 0;
|
|
932
|
+
}
|
|
933
|
+
function capitalize(s) {
|
|
934
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
935
|
+
}
|
|
936
|
+
//#endregion
|
|
937
|
+
//#region src/route-name.ts
|
|
938
|
+
function generateRouteName(routePath) {
|
|
939
|
+
if (routePath === "/" || routePath === "") return "Index";
|
|
940
|
+
const segments = routePath.replace(/^\//, "").replace(/\/$/, "").split("/");
|
|
941
|
+
const nameSegments = [];
|
|
942
|
+
for (const segment of segments) {
|
|
943
|
+
if (segment.startsWith("(") && segment.endsWith(")")) continue;
|
|
944
|
+
if (segment.startsWith("[...")) {
|
|
945
|
+
const param = segment.slice(4, -1);
|
|
946
|
+
nameSegments.push(`All${formatParamName(param)}`);
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
if (segment.startsWith("[[")) {
|
|
950
|
+
const param = segment.slice(2, -2);
|
|
951
|
+
nameSegments.push(`${formatParamName(param)}Optional`);
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
if (segment.startsWith("[")) {
|
|
955
|
+
const param = segment.slice(1, -1);
|
|
956
|
+
nameSegments.push(formatParamName(param));
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
nameSegments.push(pascalCase(segment));
|
|
960
|
+
}
|
|
961
|
+
return nameSegments.join("") || "Index";
|
|
962
|
+
}
|
|
963
|
+
function generateLayoutName(layoutPath) {
|
|
964
|
+
const base = layoutPath.replace(/\.(vue|ts)$/, "");
|
|
965
|
+
if (base === "default" || base === "default/index") return "default";
|
|
966
|
+
return base.split("/").filter(Boolean).map((s) => pascalCase(s)).join("");
|
|
967
|
+
}
|
|
968
|
+
function generateApiRouteId(method, routePath) {
|
|
969
|
+
const routeName = generateRouteName(routePath);
|
|
970
|
+
return `${method.toLowerCase()}${routeName}`;
|
|
971
|
+
}
|
|
972
|
+
function formatParamName(param) {
|
|
973
|
+
return pascalCase(param.replace(/^\.{3}/, "").replace(/\?$/, ""));
|
|
974
|
+
}
|
|
975
|
+
//#endregion
|
|
976
|
+
//#region src/router.ts
|
|
977
|
+
var UbeanRouter = class {
|
|
978
|
+
apiContext;
|
|
979
|
+
middlewares;
|
|
980
|
+
pages;
|
|
981
|
+
layouts;
|
|
982
|
+
constructor() {
|
|
983
|
+
this.apiContext = createRouter();
|
|
984
|
+
this.middlewares = [];
|
|
985
|
+
this.pages = /* @__PURE__ */ new Map();
|
|
986
|
+
this.layouts = /* @__PURE__ */ new Map();
|
|
987
|
+
}
|
|
988
|
+
addApiRoute(route) {
|
|
989
|
+
const data = {
|
|
990
|
+
method: route.method?.toUpperCase() || "ALL",
|
|
991
|
+
path: route.route,
|
|
992
|
+
id: `${route.method}:${route.route}`,
|
|
993
|
+
filePath: route.fullPath
|
|
994
|
+
};
|
|
995
|
+
addRoute(this.apiContext, data.method, route.route, data);
|
|
996
|
+
}
|
|
997
|
+
addMiddleware(mw) {
|
|
998
|
+
this.middlewares.push({
|
|
999
|
+
path: "/**",
|
|
1000
|
+
filePath: mw.fullPath,
|
|
1001
|
+
order: mw.order,
|
|
1002
|
+
global: mw.global
|
|
1003
|
+
});
|
|
1004
|
+
this.middlewares.sort((a, b) => a.order - b.order);
|
|
1005
|
+
}
|
|
1006
|
+
addPage(page) {
|
|
1007
|
+
this.pages.set(page.name, {
|
|
1008
|
+
name: page.name,
|
|
1009
|
+
path: page.route,
|
|
1010
|
+
filePath: page.fullPath,
|
|
1011
|
+
layout: page.layout,
|
|
1012
|
+
reuseTarget: page.reuseTarget
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
addLayout(layout) {
|
|
1016
|
+
this.layouts.set(layout.name, {
|
|
1017
|
+
name: layout.name,
|
|
1018
|
+
filePath: layout.fullPath,
|
|
1019
|
+
isDefault: layout.isDefault
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
matchApi(method, path) {
|
|
1023
|
+
return findRoute(this.apiContext, method.toUpperCase(), path)?.data;
|
|
1024
|
+
}
|
|
1025
|
+
getMiddlewares() {
|
|
1026
|
+
return [...this.middlewares];
|
|
1027
|
+
}
|
|
1028
|
+
getPages() {
|
|
1029
|
+
return [...this.pages.values()];
|
|
1030
|
+
}
|
|
1031
|
+
getPage(name) {
|
|
1032
|
+
return this.pages.get(name);
|
|
1033
|
+
}
|
|
1034
|
+
getLayout(name) {
|
|
1035
|
+
return this.layouts.get(name);
|
|
1036
|
+
}
|
|
1037
|
+
getDefaultLayout() {
|
|
1038
|
+
return [...this.layouts.values()].find((l) => l.isDefault);
|
|
1039
|
+
}
|
|
1040
|
+
getLayouts() {
|
|
1041
|
+
return [...this.layouts.values()];
|
|
1042
|
+
}
|
|
1043
|
+
getPageRouteNames() {
|
|
1044
|
+
return [...this.pages.keys()];
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
let _router = null;
|
|
1048
|
+
function useRouter() {
|
|
1049
|
+
if (!_router) _router = new UbeanRouter();
|
|
1050
|
+
return _router;
|
|
1051
|
+
}
|
|
1052
|
+
function createUbeanRouter() {
|
|
1053
|
+
_router = new UbeanRouter();
|
|
1054
|
+
return _router;
|
|
1055
|
+
}
|
|
1056
|
+
//#endregion
|
|
1057
|
+
export { UbeanRouter, createUbeanRouter, detectHttpExports, detectHttpExportsFromCode, extractDefineMeta, extractDefineMetaFromCode, extractDefinePage, extractDefinePageFromCode, filePathToRoute, generateApiRouteId, generateLayoutName, generateRouteName, scanProject, stripRouteGroups, useRouter };
|