@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/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 };