@kimesh/router-generator 0.0.1

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.mjs ADDED
@@ -0,0 +1,2029 @@
1
+ import * as path from "pathe";
2
+ import * as fs from "node:fs";
3
+ import { watch } from "chokidar";
4
+ import fg from "fast-glob";
5
+ import { parse } from "@vue/compiler-sfc";
6
+ import { parseSync } from "oxc-parser";
7
+ import { consola } from "consola";
8
+
9
+ //#region src/parser.ts
10
+ /**
11
+ * @kimesh/router-generator - OXC-Powered Route Parser
12
+ *
13
+ * Parse Vue SFC files to extract route definition information using OXC.
14
+ * This replaces the previous regex-based approach for more accurate parsing.
15
+ */
16
+ const logger$2 = consola.withTag("kimesh:router:parser");
17
+ /**
18
+ * Parse a Vue SFC file to extract route definition information using OXC
19
+ */
20
+ function parseRouteFile(code, filePath) {
21
+ try {
22
+ const { descriptor, errors } = parse(code, { filename: filePath });
23
+ if (errors.length > 0) {
24
+ logger$2.debug(`SFC parse errors in ${filePath}:`, errors);
25
+ return createEmptyParsedRoute(filePath);
26
+ }
27
+ const isLayout = filePath.includes("_layout") || filePath.includes("layout.vue");
28
+ const scriptContent = descriptor.script?.content || "";
29
+ const result = parseWithOxc(scriptContent, descriptor.scriptSetup?.content || "", filePath);
30
+ return {
31
+ hasRouteDefinition: result.hasRouteDefinition ?? false,
32
+ routePath: result.routePath ?? null,
33
+ hasLoader: result.hasLoader ?? false,
34
+ hasMeta: result.hasMeta ?? false,
35
+ hasValidateSearch: result.hasValidateSearch ?? false,
36
+ isLayout,
37
+ routeScriptContent: result.hasRouteDefinition ? scriptContent : null,
38
+ loaderOptions: result.loaderOptions,
39
+ middleware: result.middleware,
40
+ pageMeta: result.pageMeta
41
+ };
42
+ } catch (error) {
43
+ logger$2.debug(`Failed to parse ${filePath}:`, error);
44
+ return createEmptyParsedRoute(filePath);
45
+ }
46
+ }
47
+ /**
48
+ * Parse script content with OXC
49
+ */
50
+ function parseWithOxc(scriptContent, scriptSetupContent, filePath) {
51
+ const result = {
52
+ hasRouteDefinition: false,
53
+ routePath: null,
54
+ hasLoader: false,
55
+ hasMeta: false,
56
+ hasValidateSearch: false
57
+ };
58
+ if (scriptContent) try {
59
+ const parsed = parseSync(filePath + ".ts", scriptContent, { sourceType: "module" });
60
+ if (parsed.errors.length === 0) extractFromAST(parsed.program, result);
61
+ } catch (e) {
62
+ logger$2.debug(`OXC parse error for script in ${filePath}:`, e);
63
+ }
64
+ if (scriptSetupContent) try {
65
+ const parsed = parseSync(filePath + ".setup.ts", scriptSetupContent, { sourceType: "module" });
66
+ if (parsed.errors.length === 0) extractFromSetupAST(parsed.program, result);
67
+ } catch (e) {}
68
+ return result;
69
+ }
70
+ /**
71
+ * Extract route metadata from regular script AST
72
+ */
73
+ function extractFromAST(program, result) {
74
+ for (const node of program.body) {
75
+ if (node.type === "ExportNamedDeclaration" && node.declaration) {
76
+ const decl = node.declaration;
77
+ if (decl.type === "VariableDeclaration") for (const declarator of decl.declarations) {
78
+ if (declarator.id?.name === "Route" && declarator.init) {
79
+ const init = declarator.init;
80
+ if (init.type === "CallExpression" && init.callee?.type === "CallExpression") {
81
+ const inner = init.callee;
82
+ if (isCallTo(inner, "createFileRoute")) {
83
+ const arg = inner.arguments?.[0];
84
+ if (arg?.type === "Literal" && typeof arg.value === "string") result.routePath = arg.value;
85
+ const routeOptions = init.arguments?.[0];
86
+ if (routeOptions?.type === "ObjectExpression") {
87
+ result.hasRouteDefinition = true;
88
+ for (const prop of routeOptions.properties || []) {
89
+ if (prop.type !== "Property" || prop.key?.type !== "Identifier") continue;
90
+ switch (prop.key.name) {
91
+ case "loader":
92
+ result.hasLoader = true;
93
+ break;
94
+ case "meta":
95
+ result.hasMeta = true;
96
+ break;
97
+ case "validateSearch":
98
+ result.hasValidateSearch = true;
99
+ break;
100
+ }
101
+ }
102
+ }
103
+ }
104
+ }
105
+ if (init.type === "CallExpression" && isCallTo(init, "createFileRoute")) {
106
+ const arg = init.arguments?.[0];
107
+ if (arg?.type === "Literal" && typeof arg.value === "string") result.routePath = arg.value;
108
+ }
109
+ }
110
+ if (declarator.id?.name === "loader") {
111
+ result.hasLoader = true;
112
+ if (isCallTo(declarator.init, "defineLoader")) result.loaderOptions = extractLoaderOptions(declarator.init);
113
+ }
114
+ if (declarator.id?.name === "middleware") {
115
+ if (isCallTo(declarator.init, "defineMiddleware")) result.middleware = extractMiddlewareNames(declarator.init);
116
+ }
117
+ }
118
+ }
119
+ if (node.type === "ExpressionStatement") {
120
+ const expr = node.expression;
121
+ if (expr.type === "CallExpression") {
122
+ if (isCallTo(expr, "definePageMeta")) {
123
+ result.hasMeta = true;
124
+ result.pageMeta = extractPageMetaOptions(expr);
125
+ }
126
+ if (isCallTo(expr, "defineRoute")) {
127
+ result.hasRouteDefinition = true;
128
+ extractDefineRouteOptions(expr, result);
129
+ }
130
+ }
131
+ }
132
+ }
133
+ }
134
+ /**
135
+ * Extract from setup script AST
136
+ */
137
+ function extractFromSetupAST(program, result) {
138
+ for (const node of program.body) if (node.type === "ExpressionStatement") {
139
+ const expr = node.expression;
140
+ if (expr.type === "CallExpression") {
141
+ if (isCallTo(expr, "definePageMeta")) {
142
+ result.hasMeta = true;
143
+ result.pageMeta = extractPageMetaOptions(expr);
144
+ }
145
+ }
146
+ }
147
+ }
148
+ /**
149
+ * Check if a CallExpression calls a specific function
150
+ */
151
+ function isCallTo(node, name) {
152
+ if (!node || node.type !== "CallExpression") return false;
153
+ if (node.callee?.type === "Identifier") return node.callee.name === name;
154
+ return false;
155
+ }
156
+ /**
157
+ * Extract loader options from defineLoader call
158
+ */
159
+ function extractLoaderOptions(call) {
160
+ const options = {
161
+ isAsync: false,
162
+ hasBeforeLoad: false,
163
+ dependencies: []
164
+ };
165
+ const arg = call.arguments?.[0];
166
+ if (!arg) return options;
167
+ if (isFunctionExpression(arg)) {
168
+ options.isAsync = arg.async === true;
169
+ return options;
170
+ }
171
+ if (arg.type === "ObjectExpression") extractLoaderObjectOptions(arg, options);
172
+ return options;
173
+ }
174
+ const FUNCTION_EXPRESSION_TYPES = new Set(["ArrowFunctionExpression", "FunctionExpression"]);
175
+ function isFunctionExpression(node) {
176
+ return FUNCTION_EXPRESSION_TYPES.has(node.type);
177
+ }
178
+ function extractLoaderObjectOptions(arg, options) {
179
+ for (const prop of arg.properties || []) {
180
+ if (prop.type !== "Property" || prop.key?.type !== "Identifier") continue;
181
+ switch (prop.key.name) {
182
+ case "loader":
183
+ if (isFunctionExpression(prop.value)) options.isAsync = prop.value.async === true;
184
+ break;
185
+ case "beforeLoad":
186
+ options.hasBeforeLoad = true;
187
+ break;
188
+ case "dependencies":
189
+ if (prop.value?.type === "ArrayExpression") options.dependencies = extractStringArrayElements(prop.value.elements);
190
+ break;
191
+ case "cache":
192
+ if (prop.value?.type === "Literal" && typeof prop.value.value === "string") options.cacheStrategy = prop.value.value;
193
+ break;
194
+ }
195
+ }
196
+ }
197
+ function extractStringArrayElements(elements) {
198
+ return elements.filter((e) => e?.type === "Literal" && typeof e.value === "string").map((e) => e.value);
199
+ }
200
+ /**
201
+ * Extract middleware names from defineMiddleware call
202
+ */
203
+ function extractMiddlewareNames(call) {
204
+ const names = [];
205
+ const arg = call.arguments?.[0];
206
+ if (!arg) return names;
207
+ if (arg.type === "ArrayExpression") {
208
+ for (const elem of arg.elements || []) if (elem?.type === "Literal" && typeof elem.value === "string") names.push(elem.value);
209
+ }
210
+ if (arg.type === "Literal" && typeof arg.value === "string") names.push(arg.value);
211
+ return names;
212
+ }
213
+ /**
214
+ * Extract page meta options from definePageMeta call
215
+ */
216
+ function extractPageMetaOptions(call) {
217
+ const options = {};
218
+ const arg = call.arguments?.[0];
219
+ if (!arg || arg.type !== "ObjectExpression") return options;
220
+ for (const prop of arg.properties || []) {
221
+ if (prop.type !== "Property" || prop.key?.type !== "Identifier") continue;
222
+ const key = prop.key.name;
223
+ const { value, serializable } = extractSerializableValue(prop.value);
224
+ if (!serializable) continue;
225
+ assignPageMetaOption(options, key, value);
226
+ }
227
+ return options;
228
+ }
229
+ function assignPageMetaOption(options, key, value) {
230
+ switch (key) {
231
+ case "name":
232
+ case "path":
233
+ case "redirect":
234
+ if (typeof value === "string") options[key] = value;
235
+ break;
236
+ case "layout":
237
+ if (typeof value === "string" || value === false) options.layout = value;
238
+ break;
239
+ case "alias":
240
+ if (typeof value === "string" || Array.isArray(value)) options.alias = value;
241
+ break;
242
+ case "transition":
243
+ case "keepAlive":
244
+ if (typeof value === "boolean" || typeof value === "string") options[key] = value;
245
+ break;
246
+ case "meta":
247
+ if (typeof value === "object" && value !== null) options.meta = value;
248
+ break;
249
+ }
250
+ }
251
+ /**
252
+ * Extract options from defineRoute call
253
+ */
254
+ function extractDefineRouteOptions(call, result) {
255
+ const arg = call.arguments?.[0];
256
+ if (!arg || arg.type !== "ObjectExpression") return;
257
+ for (const prop of arg.properties || []) {
258
+ if (prop.type !== "Property" || prop.key?.type !== "Identifier") continue;
259
+ switch (prop.key.name) {
260
+ case "path":
261
+ if (prop.value?.type === "Literal" && typeof prop.value.value === "string") result.routePath = prop.value.value;
262
+ break;
263
+ case "loader":
264
+ result.hasLoader = true;
265
+ break;
266
+ case "meta":
267
+ result.hasMeta = true;
268
+ break;
269
+ case "validateSearch":
270
+ result.hasValidateSearch = true;
271
+ break;
272
+ }
273
+ }
274
+ }
275
+ const NOT_SERIALIZABLE = {
276
+ value: void 0,
277
+ serializable: false
278
+ };
279
+ /**
280
+ * Extract JSON-serializable value from AST node
281
+ */
282
+ function extractSerializableValue(node) {
283
+ if (!node) return NOT_SERIALIZABLE;
284
+ switch (node.type) {
285
+ case "Literal":
286
+ case "BooleanLiteral": return {
287
+ value: node.value,
288
+ serializable: true
289
+ };
290
+ case "ArrayExpression": return extractArrayValue(node);
291
+ case "ObjectExpression": return extractObjectValue(node);
292
+ case "UnaryExpression": return extractUnaryValue(node);
293
+ case "Identifier": return extractIdentifierValue(node);
294
+ default: return NOT_SERIALIZABLE;
295
+ }
296
+ }
297
+ function extractArrayValue(node) {
298
+ const values = [];
299
+ for (const element of node.elements || []) {
300
+ if (!element) continue;
301
+ const result = extractSerializableValue(element);
302
+ if (!result.serializable) return NOT_SERIALIZABLE;
303
+ values.push(result.value);
304
+ }
305
+ return {
306
+ value: values,
307
+ serializable: true
308
+ };
309
+ }
310
+ function extractObjectValue(node) {
311
+ const obj = {};
312
+ for (const prop of node.properties || []) {
313
+ if (prop.type !== "Property") continue;
314
+ const key = getPropertyKey(prop);
315
+ if (!key) continue;
316
+ const result = extractSerializableValue(prop.value);
317
+ if (!result.serializable) return NOT_SERIALIZABLE;
318
+ obj[key] = result.value;
319
+ }
320
+ return {
321
+ value: obj,
322
+ serializable: true
323
+ };
324
+ }
325
+ function getPropertyKey(prop) {
326
+ if (prop.key?.type === "Identifier") return prop.key.name;
327
+ if (prop.key?.type === "Literal") return String(prop.key.value);
328
+ return null;
329
+ }
330
+ function extractUnaryValue(node) {
331
+ if (node.operator === "-" && node.argument?.type === "Literal") return {
332
+ value: -node.argument.value,
333
+ serializable: true
334
+ };
335
+ return NOT_SERIALIZABLE;
336
+ }
337
+ function extractIdentifierValue(node) {
338
+ if (node.name === "undefined") return {
339
+ value: void 0,
340
+ serializable: true
341
+ };
342
+ return NOT_SERIALIZABLE;
343
+ }
344
+ /**
345
+ * Create an empty parsed route
346
+ */
347
+ function createEmptyParsedRoute(filePath) {
348
+ return {
349
+ hasRouteDefinition: false,
350
+ routePath: null,
351
+ hasLoader: false,
352
+ hasMeta: false,
353
+ hasValidateSearch: false,
354
+ isLayout: filePath.includes("_layout") || filePath.includes("layout.vue"),
355
+ routeScriptContent: null
356
+ };
357
+ }
358
+
359
+ //#endregion
360
+ //#region src/scaffolder.ts
361
+ /**
362
+ * Check if file content needs scaffolding (empty or invalid SFC)
363
+ */
364
+ function needsScaffolding(content) {
365
+ const trimmed = content.trim();
366
+ if (!trimmed) return true;
367
+ if (!trimmed.includes("<template") && !trimmed.includes("<script")) return true;
368
+ return false;
369
+ }
370
+ /**
371
+ * Generate Vue SFC boilerplate for a route file
372
+ */
373
+ function generateRouteScaffold(options) {
374
+ const { routePath, isLayout } = options;
375
+ if (isLayout) return `<script setup lang="ts">
376
+ <\/script>
377
+
378
+ <template>
379
+ <div>
380
+ <slot />
381
+ </div>
382
+ </template>
383
+ `;
384
+ return `<script lang="ts">
385
+ export const Route = createFileRoute('${routePath}')({})
386
+ <\/script>
387
+
388
+ <script setup lang="ts">
389
+ <\/script>
390
+
391
+ <template>
392
+ <div>
393
+ <h1>Hello "${routePath === "/" ? "Home" : routePath}"!</h1>
394
+ </div>
395
+ </template>
396
+ `;
397
+ }
398
+
399
+ //#endregion
400
+ //#region src/route-utils.ts
401
+ /**
402
+ * Check if directory path is root (empty or ".")
403
+ */
404
+ function isRootDir(dirPath) {
405
+ return dirPath === "." || dirPath === "";
406
+ }
407
+ /**
408
+ * Extract dynamic param name from segment (e.g., "$id" -> "id")
409
+ */
410
+ function extractDynamicParam(segment) {
411
+ const match = segment.match(/^\$(.+)$/);
412
+ return match ? match[1] : null;
413
+ }
414
+ /**
415
+ * Check if filename uses dot notation for flat routes
416
+ */
417
+ function isDotNotationRoute(fileName) {
418
+ return fileName.includes(".") && !fileName.startsWith(".") && !fileName.includes("[");
419
+ }
420
+ /**
421
+ * Unescape bracket notation in filenames
422
+ * e.g., script[.]js -> script.js, api[.]v1 -> api.v1
423
+ */
424
+ function unescapeBrackets(segment) {
425
+ return segment.replace(/\[(.)\]/g, "$1");
426
+ }
427
+ /**
428
+ * Process a single path segment for dynamic params and special conventions
429
+ *
430
+ * Conventions:
431
+ * - (auth) -> pathless group folder, returns empty string
432
+ * - _auth -> pathless layout folder, returns empty string
433
+ * - posts_ -> layout escape, strips trailing underscore
434
+ * - $id -> dynamic param :id
435
+ * - -folder -> excluded from routing (dash prefix)
436
+ * - [x] -> escape special characters
437
+ */
438
+ function processPathSegment(segment) {
439
+ if (segment.startsWith("-")) return "";
440
+ if (segment.startsWith("(") && segment.endsWith(")")) return "";
441
+ if (segment.startsWith("_") && !segment.includes("$")) return "";
442
+ if (segment.endsWith("_") && segment.length > 1) return segment.slice(0, -1);
443
+ const dynamicParam = extractDynamicParam(segment);
444
+ if (dynamicParam) return `:${dynamicParam}`;
445
+ return unescapeBrackets(segment);
446
+ }
447
+ /**
448
+ * Process directory path into route segments
449
+ */
450
+ function processDirectoryPath(dirPath) {
451
+ return dirPath.split("/").filter((part) => Boolean(part) && part !== ".").map(processPathSegment).filter(Boolean).join("/");
452
+ }
453
+ /**
454
+ * Build route path from directory and segment
455
+ */
456
+ function buildRoutePath(dirPath, segment) {
457
+ if (isRootDir(dirPath)) return segment ? `/${segment}` : "/";
458
+ const processedDir = processDirectoryPath(dirPath);
459
+ const basePath = processedDir ? `/${processedDir}` : "";
460
+ if (!segment) return basePath || "/";
461
+ return `${basePath}/${segment}`;
462
+ }
463
+ /**
464
+ * Create a standard RouteParam for a dynamic parameter
465
+ */
466
+ function createDynamicParam(name) {
467
+ return {
468
+ name,
469
+ optional: false,
470
+ isSplat: false,
471
+ repeatable: false
472
+ };
473
+ }
474
+ /**
475
+ * Create a catch-all RouteParam
476
+ */
477
+ function createCatchAllParam() {
478
+ return {
479
+ name: "pathMatch",
480
+ optional: false,
481
+ isSplat: true,
482
+ repeatable: true
483
+ };
484
+ }
485
+ /**
486
+ * Build a glob pattern for route file scanning
487
+ */
488
+ function buildGlobPattern(extensions) {
489
+ const exts = extensions.map((e) => e.startsWith(".") ? e : `.${e}`);
490
+ if (exts.length === 1) return `**/*${exts[0]}`;
491
+ return `**/*.{${exts.map((e) => e.slice(1)).join(",")}}`;
492
+ }
493
+
494
+ //#endregion
495
+ //#region src/scanner.ts
496
+ /**
497
+ * Route file scanner
498
+ * Scans the routes directory and builds route nodes
499
+ */
500
+ var RouteScanner = class {
501
+ config;
502
+ routeNodes = /* @__PURE__ */ new Map();
503
+ constructor(config) {
504
+ this.config = config;
505
+ }
506
+ /**
507
+ * Scan routes directory and return route nodes
508
+ */
509
+ async scan() {
510
+ const files = await fg(this.buildGlobPattern(), {
511
+ cwd: this.config.routesDirPath,
512
+ onlyFiles: true,
513
+ ignore: ["**/node_modules/**"]
514
+ });
515
+ const directories = await fg("**/", {
516
+ cwd: this.config.routesDirPath,
517
+ onlyDirectories: true,
518
+ ignore: ["**/node_modules/**"]
519
+ });
520
+ const dirSet = new Set(directories.map((d) => d.replace(/\/$/, "")));
521
+ this.routeNodes.clear();
522
+ for (const file of files.sort()) {
523
+ const node = await this.processFile(file, dirSet);
524
+ if (node) this.routeNodes.set(file, node);
525
+ }
526
+ return Array.from(this.routeNodes.values());
527
+ }
528
+ /**
529
+ * Process a single route file
530
+ */
531
+ async processFile(filePath, dirSet) {
532
+ const fullPath = path.join(this.config.routesDirPath, filePath);
533
+ const fileName = path.basename(filePath, path.extname(filePath));
534
+ const dirPath = path.dirname(filePath);
535
+ const potentialDirPath = isRootDir(dirPath) ? fileName : `${dirPath}/${fileName}`;
536
+ const hasMatchingDirectory = dirSet.has(potentialDirPath);
537
+ const { type, routePath, params, rawSegment } = this.parseFileName(fileName, dirPath, hasMatchingDirectory);
538
+ let content = fs.readFileSync(fullPath, "utf-8");
539
+ if (needsScaffolding(content)) {
540
+ const isLayout = type === "layout";
541
+ const isDynamic = type === "dynamic";
542
+ const paramName = params[0]?.name;
543
+ content = generateRouteScaffold({
544
+ routePath,
545
+ isLayout,
546
+ isDynamic,
547
+ paramName
548
+ });
549
+ fs.writeFileSync(fullPath, content, "utf-8");
550
+ console.log(`[kimesh:router] Scaffolded route: ${filePath}`);
551
+ }
552
+ const parsed = parseRouteFile(content, fullPath);
553
+ return {
554
+ filePath,
555
+ fullPath,
556
+ routePath,
557
+ variableName: this.generateVariableName(filePath),
558
+ type,
559
+ routeName: this.generateRouteName(routePath),
560
+ isLazy: this.shouldBeLazy(filePath),
561
+ params,
562
+ parsed,
563
+ children: [],
564
+ rawSegment
565
+ };
566
+ }
567
+ /**
568
+ * Parse filename to determine route type and path
569
+ *
570
+ * Kimesh file-based routing conventions:
571
+ * - __root.vue → root layout (wraps everything)
572
+ * - index.vue → index route
573
+ * - about.vue → regular page (/about)
574
+ * - $id.vue → dynamic param (/users/:id)
575
+ * - $.vue → catch-all (splat route)
576
+ * - _auth.vue → pathless layout (underscore prefix, no URL segment)
577
+ * - posts.vue + posts/ → layout route (file + matching directory)
578
+ * - posts.index.vue → flat route notation (dot separator)
579
+ */
580
+ parseFileName(fileName, dirPath, hasMatchingDirectory = false) {
581
+ const rawSegment = fileName;
582
+ if (fileName === "__root") return {
583
+ type: "layout",
584
+ routePath: "/",
585
+ params: [],
586
+ rawSegment
587
+ };
588
+ if (fileName.startsWith("_")) return {
589
+ type: "layout",
590
+ routePath: buildRoutePath(dirPath, ""),
591
+ params: [],
592
+ rawSegment
593
+ };
594
+ if (fileName === "index") return {
595
+ type: "index",
596
+ routePath: buildRoutePath(dirPath, ""),
597
+ params: [],
598
+ rawSegment
599
+ };
600
+ if (isDotNotationRoute(fileName)) return this.parseDotNotationRoute(fileName, dirPath, rawSegment);
601
+ if (fileName === "$") return {
602
+ type: "catch-all",
603
+ routePath: buildRoutePath(dirPath, ":pathMatch(.*)*"),
604
+ params: [createCatchAllParam()],
605
+ rawSegment
606
+ };
607
+ const dynamicParam = extractDynamicParam(fileName);
608
+ if (dynamicParam) return {
609
+ type: "dynamic",
610
+ routePath: buildRoutePath(dirPath, `:${dynamicParam}`),
611
+ params: [createDynamicParam(dynamicParam)],
612
+ rawSegment
613
+ };
614
+ if (hasMatchingDirectory) return {
615
+ type: "layout",
616
+ routePath: buildRoutePath(dirPath, fileName),
617
+ params: [],
618
+ rawSegment
619
+ };
620
+ return {
621
+ type: "page",
622
+ routePath: buildRoutePath(dirPath, fileName),
623
+ params: [],
624
+ rawSegment
625
+ };
626
+ }
627
+ parseDotNotationRoute(fileName, dirPath, rawSegment) {
628
+ const segments = fileName.split(".");
629
+ const lastSegment = segments[segments.length - 1];
630
+ const parentSegments = segments.slice(0, -1);
631
+ const virtualDir = isRootDir(dirPath) ? parentSegments.join("/") : `${dirPath}/${parentSegments.join("/")}`;
632
+ if (lastSegment === "index") return {
633
+ type: "index",
634
+ routePath: buildRoutePath(virtualDir, ""),
635
+ params: [],
636
+ rawSegment
637
+ };
638
+ const dynamicParam = extractDynamicParam(lastSegment);
639
+ if (dynamicParam) return {
640
+ type: "dynamic",
641
+ routePath: buildRoutePath(virtualDir, `:${dynamicParam}`),
642
+ params: [createDynamicParam(dynamicParam)],
643
+ rawSegment
644
+ };
645
+ return {
646
+ type: "page",
647
+ routePath: buildRoutePath(virtualDir, lastSegment),
648
+ params: [],
649
+ rawSegment
650
+ };
651
+ }
652
+ /**
653
+ * Generate a valid JavaScript variable name from file path
654
+ */
655
+ generateVariableName(filePath) {
656
+ return filePath.replace(/\.[^/.]+$/, "").replace(/[\/\\]/g, "_").replace(/[\[\]$().+-]/g, "_").replace(/^_+|_+$/g, "").replace(/_+/g, "_").replace(/^(\d)/, "_$1");
657
+ }
658
+ /**
659
+ * Generate route name from route path
660
+ */
661
+ generateRouteName(routePath) {
662
+ if (routePath === "/") return "index";
663
+ return routePath.slice(1).replace(/\//g, "-").replace(/:/g, "").replace(/\(.*?\)\*/g, "catchAll").replace(/\?/g, "");
664
+ }
665
+ /**
666
+ * Check if route should be lazy loaded based on config
667
+ */
668
+ shouldBeLazy(filePath) {
669
+ const { importMode } = this.config;
670
+ if (typeof importMode === "function") return importMode(filePath) === "async";
671
+ return importMode === "async";
672
+ }
673
+ /**
674
+ * Get all scanned route nodes
675
+ */
676
+ getNodes() {
677
+ return Array.from(this.routeNodes.values());
678
+ }
679
+ /**
680
+ * Build glob pattern for file scanning based on configured extensions
681
+ */
682
+ buildGlobPattern() {
683
+ return buildGlobPattern(this.config.extensions);
684
+ }
685
+ };
686
+ /**
687
+ * Create a route scanner instance
688
+ */
689
+ function createScanner(config) {
690
+ return new RouteScanner(config);
691
+ }
692
+
693
+ //#endregion
694
+ //#region src/tree-builder.ts
695
+ /**
696
+ * Build a route tree from flat route nodes
697
+ */
698
+ var RouteTreeBuilder = class {
699
+ config;
700
+ root;
701
+ layoutNodes = /* @__PURE__ */ new Map();
702
+ pathlessLayoutMap = /* @__PURE__ */ new Map();
703
+ constructor(config) {
704
+ this.config = config;
705
+ this.root = this.createTreeNode("", "/");
706
+ }
707
+ /**
708
+ * Build route tree from scanned nodes
709
+ */
710
+ build(nodes) {
711
+ const { layouts, pages } = this.separateLayoutsAndPages(nodes);
712
+ for (const layout of layouts) this.registerLayout(layout);
713
+ const { pathlessPages, regularPages } = this.groupPagesByPathlessLayout(pages);
714
+ for (const page of regularPages) this.insertNode(page);
715
+ const routes = this.flattenTree();
716
+ this.addPathlessLayoutRoutes(routes, pathlessPages);
717
+ return routes;
718
+ }
719
+ separateLayoutsAndPages(nodes) {
720
+ const layouts = [];
721
+ const pages = [];
722
+ for (const node of nodes) if (node.type === "layout") layouts.push(node);
723
+ else pages.push(node);
724
+ return {
725
+ layouts,
726
+ pages
727
+ };
728
+ }
729
+ registerLayout(node) {
730
+ const fileName = path.basename(node.filePath, path.extname(node.filePath));
731
+ const dir = path.dirname(node.filePath);
732
+ if (fileName === "__root") {
733
+ this.layoutNodes.set("", node);
734
+ return;
735
+ }
736
+ const layoutDir = isRootDir(dir) ? fileName : `${dir}/${fileName}`;
737
+ if (fileName.startsWith("_") && !fileName.startsWith("__")) {
738
+ this.layoutNodes.set(layoutDir, node);
739
+ const folderPath = isRootDir(dir) ? `${fileName}/` : `${dir}/${fileName}/`;
740
+ this.pathlessLayoutMap.set(folderPath, layoutDir);
741
+ } else this.layoutNodes.set(layoutDir, node);
742
+ }
743
+ groupPagesByPathlessLayout(pages) {
744
+ const pathlessPages = /* @__PURE__ */ new Map();
745
+ const regularPages = [];
746
+ for (const page of pages) {
747
+ const matchedPathless = this.findMatchingPathlessLayout(page);
748
+ if (matchedPathless) {
749
+ if (!pathlessPages.has(matchedPathless)) pathlessPages.set(matchedPathless, []);
750
+ pathlessPages.get(matchedPathless).push(page);
751
+ } else regularPages.push(page);
752
+ }
753
+ return {
754
+ pathlessPages,
755
+ regularPages
756
+ };
757
+ }
758
+ findMatchingPathlessLayout(page) {
759
+ for (const [folderPath, layoutKey] of this.pathlessLayoutMap) if (page.filePath.startsWith(folderPath)) return layoutKey;
760
+ return null;
761
+ }
762
+ addPathlessLayoutRoutes(routes, pathlessPages) {
763
+ if (pathlessPages.size === 0) return;
764
+ const rootLayout = routes.find((r) => r.type === "layout" && r.routePath === "/");
765
+ for (const [layoutKey, layoutPages] of pathlessPages) {
766
+ const layout = this.layoutNodes.get(layoutKey);
767
+ if (!layout) continue;
768
+ layout.children = layoutPages.map((page) => {
769
+ page.parent = layout;
770
+ return page;
771
+ });
772
+ if (rootLayout) {
773
+ layout.parent = rootLayout;
774
+ rootLayout.children = rootLayout.children || [];
775
+ rootLayout.children.push(layout);
776
+ } else routes.push(layout);
777
+ }
778
+ }
779
+ /**
780
+ * Insert a node into the tree
781
+ */
782
+ insertNode(node) {
783
+ const segments = this.getPathSegments(node.routePath);
784
+ let current = this.root;
785
+ for (let i = 0; i < segments.length; i++) {
786
+ const segment = segments[i];
787
+ const isLast = i === segments.length - 1;
788
+ if (!current.children.has(segment)) {
789
+ const routePath = "/" + segments.slice(0, i + 1).join("/");
790
+ current.children.set(segment, this.createTreeNode(segment, routePath, current));
791
+ }
792
+ current = current.children.get(segment);
793
+ if (isLast) {
794
+ current.node = node;
795
+ node.parent = current.parent?.node;
796
+ }
797
+ }
798
+ if (segments.length === 0 && node.type === "index") this.root.node = node;
799
+ }
800
+ /**
801
+ * Get path segments from route path
802
+ */
803
+ getPathSegments(routePath) {
804
+ return routePath.split("/").filter(Boolean);
805
+ }
806
+ /**
807
+ * Create a tree node
808
+ */
809
+ createTreeNode(segment, routePath, parent) {
810
+ return {
811
+ segment,
812
+ routePath,
813
+ children: /* @__PURE__ */ new Map(),
814
+ parent
815
+ };
816
+ }
817
+ /**
818
+ * Flatten tree to route array
819
+ */
820
+ flattenTree() {
821
+ const routes = [];
822
+ const rootLayout = this.layoutNodes.get("");
823
+ if (rootLayout) {
824
+ const children = this.flattenChildren(this.root);
825
+ if (this.root.node) {
826
+ this.root.node.parent = rootLayout;
827
+ children.unshift(this.root.node);
828
+ }
829
+ for (const child of children) child.parent = rootLayout;
830
+ rootLayout.children = children;
831
+ routes.push(rootLayout);
832
+ } else {
833
+ const children = this.flattenChildren(this.root);
834
+ if (this.root.node) children.unshift(this.root.node);
835
+ routes.push(...children);
836
+ }
837
+ return routes;
838
+ }
839
+ /**
840
+ * Flatten children of a tree node
841
+ */
842
+ flattenChildren(treeNode, currentDir = "") {
843
+ const children = [];
844
+ const sortedEntries = Array.from(treeNode.children.entries()).sort(([a], [b]) => this.compareSegments(a, b));
845
+ for (const [segment, child] of sortedEntries) {
846
+ const childDir = currentDir ? `${currentDir}/${segment}` : segment;
847
+ const flattenedChildren = this.flattenChildNode(child, childDir);
848
+ children.push(...flattenedChildren);
849
+ }
850
+ return children;
851
+ }
852
+ flattenChildNode(child, childDir) {
853
+ const layout = this.layoutNodes.get(childDir);
854
+ if (layout) return [this.createLayoutWithChildren(layout, child, childDir)];
855
+ if (child.node) return this.flattenNodeWithChildren(child, childDir);
856
+ return this.flattenChildren(child, childDir);
857
+ }
858
+ createLayoutWithChildren(layout, child, childDir) {
859
+ layout.children = this.flattenChildren(child, childDir);
860
+ if (child.node && child.node.type !== "layout") {
861
+ child.node.parent = layout;
862
+ layout.children.unshift(child.node);
863
+ }
864
+ for (const layoutChild of layout.children) layoutChild.parent = layout;
865
+ return layout;
866
+ }
867
+ flattenNodeWithChildren(child, childDir) {
868
+ const node = child.node;
869
+ if (node.type === "layout") {
870
+ node.children = this.flattenChildren(child, childDir);
871
+ return [node];
872
+ }
873
+ return [node, ...this.flattenChildren(child, childDir)];
874
+ }
875
+ /**
876
+ * Compare segments for sorting
877
+ * Order: static > dynamic > catch-all
878
+ */
879
+ compareSegments(a, b) {
880
+ const aScore = this.getSegmentScore(a);
881
+ const bScore = this.getSegmentScore(b);
882
+ if (aScore !== bScore) return aScore - bScore;
883
+ return a.localeCompare(b);
884
+ }
885
+ /**
886
+ * Get sorting score for a segment
887
+ */
888
+ getSegmentScore(segment) {
889
+ if (segment.includes("(.*)*") || segment.includes("pathMatch")) return 100;
890
+ if (segment.startsWith(":")) return 50;
891
+ return 0;
892
+ }
893
+ };
894
+ /**
895
+ * Create a route tree builder
896
+ */
897
+ function createTreeBuilder(config) {
898
+ return new RouteTreeBuilder(config);
899
+ }
900
+ /**
901
+ * Build route tree from nodes
902
+ */
903
+ function buildRouteTree(nodes, config) {
904
+ return new RouteTreeBuilder(config).build(nodes);
905
+ }
906
+
907
+ //#endregion
908
+ //#region src/codegen/routes.ts
909
+ /**
910
+ * Generate routes.gen.ts content
911
+ */
912
+ function generateRoutes(routes, config) {
913
+ const imports = [];
914
+ const importMap = /* @__PURE__ */ new Map();
915
+ const routeDefImportMap = /* @__PURE__ */ new Map();
916
+ collectImports(routes, config, imports, importMap);
917
+ collectRouteDefinitionImports(routes, config, imports, routeDefImportMap);
918
+ const routeRecords = generateRouteRecords(routes, config, importMap, routeDefImportMap);
919
+ return `// Auto-generated by @kimesh/router-generator
920
+ // Do not edit this file manually
921
+
922
+ ${imports.join("\n")}
923
+
924
+ export const routes = ${routeRecords}
925
+ `;
926
+ }
927
+ /**
928
+ * Collect imports from route nodes
929
+ */
930
+ function collectImports(routes, config, imports, importMap) {
931
+ for (const route of routes) {
932
+ if (!route.isLazy) {
933
+ const importPath = getImportPath(route, config);
934
+ const importName = route.variableName;
935
+ imports.push(`import ${importName} from '${importPath}'`);
936
+ importMap.set(route.filePath, importName);
937
+ }
938
+ if (route.children.length > 0) collectImports(route.children, config, imports, importMap);
939
+ }
940
+ }
941
+ /**
942
+ * Collect route definition imports for routes with loaders/meta
943
+ * Uses ?route query to extract route definitions at build time (Vite 8/rolldown compatible)
944
+ */
945
+ function collectRouteDefinitionImports(routes, config, imports, routeDefImportMap) {
946
+ for (const route of routes) {
947
+ if (route.parsed.hasRouteDefinition) {
948
+ const importPath = getImportPath(route, config);
949
+ const defName = `${route.variableName}_def`;
950
+ imports.push(`import ${defName} from '${importPath}?route'`);
951
+ routeDefImportMap.set(route.filePath, defName);
952
+ }
953
+ if (route.children.length > 0) collectRouteDefinitionImports(route.children, config, imports, routeDefImportMap);
954
+ }
955
+ }
956
+ /**
957
+ * Get import path relative to generated file
958
+ */
959
+ function getImportPath(route, config) {
960
+ const fromPath = config.generatedDirPath;
961
+ const toPath = route.fullPath;
962
+ let relativePath = path.relative(fromPath, toPath);
963
+ if (!relativePath.startsWith(".")) relativePath = "./" + relativePath;
964
+ return relativePath;
965
+ }
966
+ /**
967
+ * Generate route records array
968
+ */
969
+ function generateRouteRecords(routes, config, importMap, routeDefImportMap, indent = 0) {
970
+ const ctx = {
971
+ config,
972
+ importMap,
973
+ routeDefImportMap
974
+ };
975
+ const pad = " ".repeat(indent);
976
+ const innerPad = " ".repeat(indent + 1);
977
+ return `[\n${routes.map((route) => generateSingleRouteRecord(route, ctx, innerPad, indent)).join(",\n")}\n${pad}]`;
978
+ }
979
+ function generateSingleRouteRecord(route, ctx, innerPad, indent) {
980
+ const lines = [`${innerPad}{`];
981
+ const routePath = route.parent ? getRelativePath(route) : route.routePath;
982
+ lines.push(`${innerPad} path: '${routePath}',`);
983
+ if (shouldIncludeName(route)) lines.push(`${innerPad} name: '${route.routeName}',`);
984
+ lines.push(generateComponentLine(route, ctx, innerPad));
985
+ const routeDefName = ctx.routeDefImportMap.get(route.filePath);
986
+ const metaProps = [];
987
+ if (routeDefName) metaProps.push(`__kimesh: ${routeDefName}`);
988
+ if (route.layer) metaProps.push(`__kimeshLayer: '${route.layer}'`);
989
+ if (metaProps.length > 0) lines.push(`${innerPad} meta: { ${metaProps.join(", ")} },`);
990
+ if (route.children.length > 0) {
991
+ const childRecords = generateRouteRecords(route.children, ctx.config, ctx.importMap, ctx.routeDefImportMap, indent + 2);
992
+ lines.push(`${innerPad} children: ${childRecords},`);
993
+ }
994
+ lines.push(`${innerPad}}`);
995
+ return lines.join("\n");
996
+ }
997
+ function shouldIncludeName(route) {
998
+ return route.type !== "layout" || route.children.length === 0;
999
+ }
1000
+ function generateComponentLine(route, ctx, innerPad) {
1001
+ if (route.isLazy) return `${innerPad} component: () => import('${getImportPath(route, ctx.config)}'),`;
1002
+ return `${innerPad} component: ${ctx.importMap.get(route.filePath) || route.variableName},`;
1003
+ }
1004
+ /**
1005
+ * Get path relative to parent route
1006
+ */
1007
+ function getRelativePath(route) {
1008
+ if (!route.parent) return route.routePath;
1009
+ const parentPath = route.parent.routePath;
1010
+ const routePath = route.routePath;
1011
+ if (parentPath === "/") return routePath === "/" ? "" : stripLeadingSlash(routePath);
1012
+ let relativePath = routePath;
1013
+ if (relativePath.startsWith(parentPath)) relativePath = relativePath.slice(parentPath.length);
1014
+ relativePath = stripLeadingSlash(relativePath);
1015
+ return !relativePath && route.type === "index" ? "" : relativePath;
1016
+ }
1017
+ function stripLeadingSlash(path$1) {
1018
+ return path$1.startsWith("/") ? path$1.slice(1) : path$1;
1019
+ }
1020
+
1021
+ //#endregion
1022
+ //#region src/codegen/types.ts
1023
+ /**
1024
+ * Generate typed-routes.d.ts content with comprehensive type safety
1025
+ */
1026
+ function generateRouteTypes(routes) {
1027
+ const routeInfos = collectAllRoutes(routes);
1028
+ const routeMapEntries = routeInfos.map((r) => generateRouteRecordInfo(r)).join("\n");
1029
+ const routePathsUnion = routeInfos.length > 0 ? routeInfos.map((r) => `'${r.path}'`).join(" | ") : "never";
1030
+ return `/* eslint-disable */
1031
+ /* prettier-ignore */
1032
+ // @ts-nocheck
1033
+ // Auto-generated by @kimesh/router-generator
1034
+ // Do not edit this file manually
1035
+
1036
+ import type {
1037
+ RouteRecordInfo,
1038
+ ParamValue,
1039
+ ParamValueOneOrMore,
1040
+ ParamValueZeroOrMore,
1041
+ ParamValueZeroOrOne,
1042
+ } from 'vue-router'
1043
+
1044
+ // Augment @kimesh/router-runtime for typed composables
1045
+ declare module '@kimesh/router-runtime' {
1046
+ export interface RouteNamedMap {
1047
+ ${routeMapEntries}
1048
+ }
1049
+ }
1050
+
1051
+ // Augment vue-router for typed router.push(), useRoute(), etc.
1052
+ declare module 'vue-router' {
1053
+ export interface TypesConfig {
1054
+ RouteNamedMap: import('@kimesh/router-runtime').RouteNamedMap
1055
+ }
1056
+ }
1057
+
1058
+ /** Union of all route names/paths */
1059
+ export type RouteNames = ${routePathsUnion}
1060
+
1061
+ /** Union of all route paths */
1062
+ export type RoutePaths = ${routePathsUnion}
1063
+ `;
1064
+ }
1065
+ /**
1066
+ * Collect all routes recursively into flat array
1067
+ */
1068
+ function collectAllRoutes(routes) {
1069
+ const infos = [];
1070
+ collectRoutesRecursively(routes, "", infos);
1071
+ return infos;
1072
+ }
1073
+ function collectRoutesRecursively(nodes, parentPath, infos) {
1074
+ for (const route of nodes) {
1075
+ const fullPath = computeFullPath(route.routePath, parentPath);
1076
+ if (route.type === "layout") {
1077
+ if (route.children.length > 0) {
1078
+ const childParentPath = fullPath === "/" ? "" : fullPath;
1079
+ collectRoutesRecursively(route.children, childParentPath, infos);
1080
+ }
1081
+ continue;
1082
+ }
1083
+ infos.push({
1084
+ name: fullPath,
1085
+ path: fullPath,
1086
+ params: collectParamsFromPath(fullPath),
1087
+ children: collectChildPaths(route.children, fullPath)
1088
+ });
1089
+ if (route.children.length > 0) collectRoutesRecursively(route.children, fullPath, infos);
1090
+ }
1091
+ }
1092
+ function computeFullPath(routePath, parentPath) {
1093
+ if (routePath === "" || routePath === "/") return parentPath || "/";
1094
+ if (routePath.startsWith("/")) return routePath;
1095
+ return parentPath === "/" ? `/${routePath}` : `${parentPath}/${routePath}`;
1096
+ }
1097
+ function collectChildPaths(children, parentFullPath) {
1098
+ return children.filter((c) => c.type !== "layout").map((c) => computeFullPath(c.routePath, parentFullPath));
1099
+ }
1100
+ /**
1101
+ * Extract params from a route path
1102
+ * e.g., '/users/:userId/posts/:postId' -> [{ name: 'userId', ... }, { name: 'postId', ... }]
1103
+ */
1104
+ function collectParamsFromPath(routePath) {
1105
+ const params = [];
1106
+ const regex = /:([a-zA-Z_][a-zA-Z0-9_]*)(\?)?(\+)?(\*)?/g;
1107
+ let match;
1108
+ while ((match = regex.exec(routePath)) !== null) {
1109
+ const name = match[1];
1110
+ const hasQuestion = match[2] === "?";
1111
+ const hasPlus = match[3] === "+";
1112
+ const hasStar = match[4] === "*";
1113
+ params.push({
1114
+ name,
1115
+ optional: hasQuestion || hasStar,
1116
+ isSplat: hasStar,
1117
+ repeatable: hasPlus || hasStar
1118
+ });
1119
+ }
1120
+ return params;
1121
+ }
1122
+ /**
1123
+ * Generate RouteRecordInfo entry for a route
1124
+ */
1125
+ function generateRouteRecordInfo(route) {
1126
+ const rawParams = generateParamsType(route.params, true);
1127
+ const params = generateParamsType(route.params, false);
1128
+ const children = route.children.length > 0 ? route.children.map((c) => `'${c}'`).join(" | ") : "never";
1129
+ return ` '${route.name}': RouteRecordInfo<
1130
+ '${route.name}',
1131
+ '${route.path}',
1132
+ ${rawParams},
1133
+ ${params},
1134
+ ${children}
1135
+ >,`;
1136
+ }
1137
+ /**
1138
+ * Generate params type object
1139
+ */
1140
+ function generateParamsType(params, isRaw) {
1141
+ if (params.length === 0) return "Record<never, never>";
1142
+ return `{ ${params.map((p) => formatParamEntry(p, isRaw)).join("; ")} }`;
1143
+ }
1144
+ function formatParamEntry(param, isRaw) {
1145
+ const optional = param.optional ? "?" : "";
1146
+ const type = getParamValueType(param, isRaw);
1147
+ return `${param.name}${optional}: ${type}`;
1148
+ }
1149
+ function getParamValueType(param, isRaw) {
1150
+ if (param.repeatable) return param.optional ? `ParamValueZeroOrMore<${isRaw}>` : `ParamValueOneOrMore<${isRaw}>`;
1151
+ if (param.optional) return `ParamValueZeroOrOne<${isRaw}>`;
1152
+ return `ParamValue<${isRaw}>`;
1153
+ }
1154
+
1155
+ //#endregion
1156
+ //#region src/extract-route.ts
1157
+ const ROUTE_QUERY_RE = /[?&]route\b/;
1158
+ const CREATE_FILE_ROUTE_START_RE = /export\s+const\s+Route\s*=\s*createFileRoute\s*\(\s*['"`][^'"`]*['"`]\s*\)\s*\(/;
1159
+ const EMPTY_EXPORT = "export default {}";
1160
+ /**
1161
+ * Check if the id has a ?route query parameter
1162
+ */
1163
+ function isRouteQuery(id) {
1164
+ return ROUTE_QUERY_RE.test(id);
1165
+ }
1166
+ /**
1167
+ * Get the base path without the ?route query
1168
+ */
1169
+ function getBasePath(id) {
1170
+ return id.split("?")[0];
1171
+ }
1172
+ /**
1173
+ * Extract route definition from a Vue SFC file
1174
+ * Returns the route config object as a module with default export
1175
+ */
1176
+ function extractRouteDefinition(filePath) {
1177
+ const code = fs.readFileSync(filePath, "utf-8");
1178
+ const match = CREATE_FILE_ROUTE_START_RE.exec(code);
1179
+ if (!match) return EMPTY_EXPORT;
1180
+ const routeObject = extractBalancedObject(code, match.index + match[0].length);
1181
+ if (!routeObject) return EMPTY_EXPORT;
1182
+ return `${extractUsedImports(code, routeObject, path.dirname(filePath))}\nexport default ${routeObject}`;
1183
+ }
1184
+ /**
1185
+ * Extract a balanced object starting from a position in the code
1186
+ * Handles nested braces, strings, template literals, and comments
1187
+ */
1188
+ function extractBalancedObject(code, startPos) {
1189
+ let i = skipWhitespace(code, startPos);
1190
+ if (code[i] !== "{") return null;
1191
+ const objectStart = i;
1192
+ const state = {
1193
+ inString: null,
1194
+ inTemplateString: false,
1195
+ inLineComment: false,
1196
+ inBlockComment: false,
1197
+ depth: 0
1198
+ };
1199
+ for (; i < code.length; i++) {
1200
+ const result = processCharacter(code, i, state);
1201
+ if (result.skip) {
1202
+ i = result.newIndex ?? i;
1203
+ continue;
1204
+ }
1205
+ if (result.foundEnd) return code.slice(objectStart, i + 1);
1206
+ }
1207
+ return null;
1208
+ }
1209
+ function skipWhitespace(code, pos) {
1210
+ while (pos < code.length && /\s/.test(code[pos])) pos++;
1211
+ return pos;
1212
+ }
1213
+ function processCharacter(code, i, state) {
1214
+ const char = code[i];
1215
+ const nextChar = code[i + 1];
1216
+ const prevChar = code[i - 1];
1217
+ if (state.inLineComment) {
1218
+ if (char === "\n") state.inLineComment = false;
1219
+ return { skip: true };
1220
+ }
1221
+ if (state.inBlockComment) {
1222
+ if (char === "*" && nextChar === "/") {
1223
+ state.inBlockComment = false;
1224
+ return {
1225
+ skip: true,
1226
+ newIndex: i + 1
1227
+ };
1228
+ }
1229
+ return { skip: true };
1230
+ }
1231
+ if (state.inTemplateString) {
1232
+ if (char === "`") state.inTemplateString = false;
1233
+ return { skip: true };
1234
+ }
1235
+ if (state.inString) {
1236
+ if (char === state.inString && prevChar !== "\\") state.inString = null;
1237
+ return { skip: true };
1238
+ }
1239
+ if (char === "/" && nextChar === "/") {
1240
+ state.inLineComment = true;
1241
+ return { skip: true };
1242
+ }
1243
+ if (char === "/" && nextChar === "*") {
1244
+ state.inBlockComment = true;
1245
+ return {
1246
+ skip: true,
1247
+ newIndex: i + 1
1248
+ };
1249
+ }
1250
+ if (char === "`") {
1251
+ state.inTemplateString = true;
1252
+ return { skip: true };
1253
+ }
1254
+ if (char === "\"" || char === "'") {
1255
+ state.inString = char;
1256
+ return { skip: true };
1257
+ }
1258
+ if (char === "{") {
1259
+ state.depth++;
1260
+ return { skip: false };
1261
+ }
1262
+ if (char === "}") {
1263
+ state.depth--;
1264
+ if (state.depth === 0) return {
1265
+ skip: false,
1266
+ foundEnd: true
1267
+ };
1268
+ }
1269
+ return { skip: false };
1270
+ }
1271
+ const IMPORT_REGEX = /import\s+(?:\{([^}]+)\}|(\w+))\s+from\s+['"]([^'"]+)['"]/g;
1272
+ const SKIP_IMPORT_SOURCES = new Set(["vue", "@kimesh/router-runtime"]);
1273
+ /**
1274
+ * Extract imports that are used in the route object
1275
+ * Converts relative imports to absolute paths for virtual module compatibility
1276
+ */
1277
+ function extractUsedImports(code, routeObject, fileDir) {
1278
+ const namedImports = /* @__PURE__ */ new Map();
1279
+ const defaultImports = /* @__PURE__ */ new Map();
1280
+ let match;
1281
+ while ((match = IMPORT_REGEX.exec(code)) !== null) processImportMatch(match, routeObject, fileDir, namedImports, defaultImports);
1282
+ return buildImportStatements(namedImports, defaultImports);
1283
+ }
1284
+ function processImportMatch(match, routeObject, fileDir, namedImports, defaultImports) {
1285
+ const namedImportStr = match[1];
1286
+ const defaultImport = match[2];
1287
+ let source = match[3];
1288
+ if (shouldSkipImport(source)) return;
1289
+ source = resolveImportSource(source, fileDir);
1290
+ if (namedImportStr) addUsedNamedImports(namedImportStr, source, routeObject, namedImports);
1291
+ else if (defaultImport && routeObject.includes(defaultImport)) defaultImports.set(source, defaultImport);
1292
+ }
1293
+ function shouldSkipImport(source) {
1294
+ for (const skip of SKIP_IMPORT_SOURCES) if (source.includes(skip)) return true;
1295
+ return false;
1296
+ }
1297
+ function resolveImportSource(source, fileDir) {
1298
+ if (source.startsWith(".")) return path.resolve(fileDir, source);
1299
+ return source;
1300
+ }
1301
+ function addUsedNamedImports(namedImportStr, source, routeObject, namedImports) {
1302
+ const usedImports = namedImportStr.split(",").map((s) => s.trim().split(" as ")[0].trim()).filter((name) => {
1303
+ return (/* @__PURE__ */ new RegExp(`\\b${name}\\b`)).test(routeObject);
1304
+ });
1305
+ if (usedImports.length === 0) return;
1306
+ if (!namedImports.has(source)) namedImports.set(source, /* @__PURE__ */ new Set());
1307
+ for (const imp of usedImports) namedImports.get(source).add(imp);
1308
+ }
1309
+ function buildImportStatements(namedImports, defaultImports) {
1310
+ const statements = [];
1311
+ for (const [source, specifiers] of namedImports) statements.push(`import { ${Array.from(specifiers).join(", ")} } from '${source}'`);
1312
+ for (const [source, name] of defaultImports) statements.push(`import ${name} from '${source}'`);
1313
+ return statements.join("\n");
1314
+ }
1315
+
1316
+ //#endregion
1317
+ //#region src/layer-collector.ts
1318
+ /**
1319
+ * @kimesh/router-generator - Layer Route Collector
1320
+ *
1321
+ * Collects routes from multiple layers with priority-based resolution.
1322
+ */
1323
+ const logger$1 = consola.withTag("kimesh:router:layer-collector");
1324
+ /**
1325
+ * Collect routes from all configured layers and build proper route trees
1326
+ */
1327
+ async function collectLayerRoutes(sources, config) {
1328
+ const startTime = performance.now();
1329
+ const allRoutes = [];
1330
+ const byLayer = /* @__PURE__ */ new Map();
1331
+ const perLayerTiming = /* @__PURE__ */ new Map();
1332
+ const scanPromises = [...sources].sort((a, b) => a.priority - b.priority).map(async (source) => {
1333
+ const layerStartTime = performance.now();
1334
+ return {
1335
+ source,
1336
+ routes: buildLayerRouteTree(await scanLayerRoutes(source, config), source),
1337
+ time: performance.now() - layerStartTime
1338
+ };
1339
+ });
1340
+ const results = await Promise.all(scanPromises);
1341
+ for (const { source, routes, time } of results) {
1342
+ allRoutes.push(...routes);
1343
+ byLayer.set(source.layer, routes);
1344
+ perLayerTiming.set(source.layer, time);
1345
+ logger$1.debug(`Layer '${source.layer}': ${routes.length} route tree(s) in ${time.toFixed(1)}ms`);
1346
+ }
1347
+ const totalTime = performance.now() - startTime;
1348
+ logger$1.info(`Collected ${allRoutes.length} routes from ${sources.length} layers in ${totalTime.toFixed(1)}ms`);
1349
+ return {
1350
+ routes: allRoutes,
1351
+ byLayer,
1352
+ timing: {
1353
+ totalMs: totalTime,
1354
+ perLayer: perLayerTiming
1355
+ }
1356
+ };
1357
+ }
1358
+ /**
1359
+ * Build a proper route tree for a layer's routes
1360
+ * Layouts become parents with children, pages become leaves
1361
+ *
1362
+ * Routes are organized by their file path which directly maps to URL path.
1363
+ * Example: routes/blog/_layout.vue → /blog layout
1364
+ * routes/blog/index.vue → /blog (index under /blog layout)
1365
+ * routes/blog/$postId.vue → /blog/:postId (child under /blog layout)
1366
+ */
1367
+ function buildLayerRouteTree(flatRoutes, source) {
1368
+ if (flatRoutes.length === 0) return [];
1369
+ const layoutMap = /* @__PURE__ */ new Map();
1370
+ const pages = [];
1371
+ for (const route of flatRoutes) if (route.type === "layout") {
1372
+ const layoutPath = route.finalRoutePath;
1373
+ layoutMap.set(layoutPath, route);
1374
+ } else pages.push(route);
1375
+ if (layoutMap.size === 0) return pages.map((page) => ({
1376
+ ...page,
1377
+ routePath: page.finalRoutePath
1378
+ }));
1379
+ let rootLayout = null;
1380
+ let rootLayoutPath = "";
1381
+ for (const [layoutPath, layout] of layoutMap) if (!rootLayout || layoutPath.length < rootLayoutPath.length) {
1382
+ rootLayout = layout;
1383
+ rootLayoutPath = layoutPath;
1384
+ }
1385
+ if (!rootLayout) return pages.map((page) => ({
1386
+ ...page,
1387
+ routePath: page.finalRoutePath
1388
+ }));
1389
+ const children = [];
1390
+ const normalizedBase = rootLayoutPath;
1391
+ for (const page of pages) {
1392
+ const pagePath = page.finalRoutePath;
1393
+ if (pagePath === normalizedBase || pagePath === normalizedBase + "/") {
1394
+ const indexRoute = {
1395
+ ...page,
1396
+ routePath: "",
1397
+ parent: rootLayout
1398
+ };
1399
+ children.unshift(indexRoute);
1400
+ } else if (pagePath.startsWith(normalizedBase + "/") || normalizedBase === "/" && pagePath.startsWith("/")) {
1401
+ const relativePath = normalizedBase === "/" ? pagePath.slice(1) : pagePath.slice(normalizedBase.length + 1);
1402
+ const childRoute = {
1403
+ ...page,
1404
+ routePath: relativePath,
1405
+ parent: rootLayout
1406
+ };
1407
+ children.push(childRoute);
1408
+ }
1409
+ }
1410
+ return [{
1411
+ ...rootLayout,
1412
+ routePath: rootLayout.finalRoutePath,
1413
+ children
1414
+ }];
1415
+ }
1416
+ /**
1417
+ * Scan routes from a single layer
1418
+ */
1419
+ async function scanLayerRoutes(source, config) {
1420
+ const { layer, priority, routesDir, basePath } = source;
1421
+ if (!fs.existsSync(routesDir)) {
1422
+ logger$1.debug(`Routes directory not found for layer '${layer}': ${routesDir}`);
1423
+ return [];
1424
+ }
1425
+ const files = await fg(buildGlobPattern(config.extensions), {
1426
+ cwd: routesDir,
1427
+ onlyFiles: true,
1428
+ ignore: ["**/node_modules/**"]
1429
+ });
1430
+ const directories = await fg("**/", {
1431
+ cwd: routesDir,
1432
+ onlyDirectories: true,
1433
+ ignore: ["**/node_modules/**"]
1434
+ });
1435
+ const dirSet = new Set(directories.map((d) => d.replace(/\/$/, "")));
1436
+ const routes = [];
1437
+ for (const file of files.sort()) {
1438
+ const node = await processLayerFile(file, routesDir, source, config, dirSet);
1439
+ if (node) routes.push(node);
1440
+ }
1441
+ return routes;
1442
+ }
1443
+ /**
1444
+ * Process a single route file from a layer
1445
+ *
1446
+ * Supports layout route pattern: posts.vue + posts/ directory
1447
+ * When a file like posts.vue has a matching posts/ directory, it becomes a layout
1448
+ */
1449
+ async function processLayerFile(filePath, routesDir, source, config, dirSet) {
1450
+ const fullPath = path.join(routesDir, filePath);
1451
+ const fileName = path.basename(filePath, path.extname(filePath));
1452
+ const dirPath = path.dirname(filePath);
1453
+ if (fileName.startsWith("-")) return null;
1454
+ const potentialDirPath = isRootDir(dirPath) ? fileName : `${dirPath}/${fileName}`;
1455
+ const hasMatchingDirectory = dirSet.has(potentialDirPath);
1456
+ let { type, routePath, params, rawSegment, isPathlessLayout } = parseFileName(fileName, dirPath);
1457
+ if (hasMatchingDirectory && type === "page") {
1458
+ type = "layout";
1459
+ routePath = buildRoutePath(dirPath, fileName);
1460
+ logger$1.debug(`Layout route detected: ${fileName} -> ${routePath}`);
1461
+ }
1462
+ let content;
1463
+ try {
1464
+ content = fs.readFileSync(fullPath, "utf-8");
1465
+ } catch {
1466
+ logger$1.warn(`Could not read file: ${fullPath}`);
1467
+ return null;
1468
+ }
1469
+ if (needsScaffolding(content)) {
1470
+ const isLayout = type === "layout";
1471
+ const isDynamic = type === "dynamic";
1472
+ const paramName = params[0]?.name;
1473
+ content = generateRouteScaffold({
1474
+ routePath,
1475
+ isLayout,
1476
+ isDynamic,
1477
+ paramName
1478
+ });
1479
+ fs.writeFileSync(fullPath, content, "utf-8");
1480
+ logger$1.debug(`Scaffolded route: ${source.layer}:${filePath}`);
1481
+ }
1482
+ const parsed = parseRouteFile(content, fullPath);
1483
+ const variableName = generateVariableName(filePath, source.layer);
1484
+ const finalRoutePath = applyBasePath(routePath, source.basePath);
1485
+ const routeName = generateRouteName(finalRoutePath, source.layer);
1486
+ const isLazy = fileName.endsWith(".lazy") || shouldBeLazy(filePath, config);
1487
+ return {
1488
+ filePath,
1489
+ fullPath,
1490
+ routePath,
1491
+ variableName,
1492
+ type,
1493
+ routeName,
1494
+ isLazy,
1495
+ params,
1496
+ parsed,
1497
+ children: [],
1498
+ rawSegment,
1499
+ layer: source.layer,
1500
+ layerPriority: source.priority,
1501
+ layerBasePath: source.basePath,
1502
+ finalRoutePath
1503
+ };
1504
+ }
1505
+ /**
1506
+ * Parse filename to determine route type and path
1507
+ *
1508
+ * File-based routing conventions:
1509
+ * - __root.vue → root layout (wraps all routes)
1510
+ * - index.vue → index route
1511
+ * - route.vue → alternative index route
1512
+ * - about.vue → regular page
1513
+ * - $id.vue → dynamic param
1514
+ * - $.vue → catch-all (splat route)
1515
+ * - (group)/ → pathless group folder (route group)
1516
+ * - _pathless.vue + _pathless/ → pathless layout (underscore prefix)
1517
+ * - posts.vue + posts/ → layout route
1518
+ * - posts.index.vue → flat route notation (dot separator)
1519
+ * - posts_/ → layout escape (underscore suffix)
1520
+ * - -utils.vue → excluded from routing (dash prefix)
1521
+ * - [x] → escape special characters (e.g., script[.]js.vue → /script.js)
1522
+ */
1523
+ function parseFileName(fileName, dirPath) {
1524
+ let rawSegment = fileName;
1525
+ if (fileName.endsWith(".lazy")) {
1526
+ fileName = fileName.slice(0, -5);
1527
+ rawSegment = fileName;
1528
+ }
1529
+ if (fileName.startsWith("-")) return {
1530
+ type: "page",
1531
+ routePath: "",
1532
+ params: [],
1533
+ rawSegment
1534
+ };
1535
+ if (fileName === "__root") return {
1536
+ type: "layout",
1537
+ routePath: "/",
1538
+ params: [],
1539
+ rawSegment
1540
+ };
1541
+ if (fileName.startsWith("_") && !fileName.startsWith("__")) return {
1542
+ type: "layout",
1543
+ routePath: buildRoutePath(dirPath, ""),
1544
+ params: [],
1545
+ rawSegment,
1546
+ isPathlessLayout: true
1547
+ };
1548
+ if (fileName === "index" || fileName === "route") return {
1549
+ type: "index",
1550
+ routePath: buildRoutePath(dirPath, ""),
1551
+ params: [],
1552
+ rawSegment
1553
+ };
1554
+ if (isDotNotationRoute(fileName)) return parseDotNotationRoute(fileName, dirPath, rawSegment);
1555
+ if (fileName === "$") return {
1556
+ type: "catch-all",
1557
+ routePath: buildRoutePath(dirPath, ":pathMatch(.*)*"),
1558
+ params: [createCatchAllParam()],
1559
+ rawSegment
1560
+ };
1561
+ const dynamicParam = extractDynamicParam(fileName);
1562
+ if (dynamicParam) return {
1563
+ type: "dynamic",
1564
+ routePath: buildRoutePath(dirPath, `:${dynamicParam}`),
1565
+ params: [createDynamicParam(dynamicParam)],
1566
+ rawSegment
1567
+ };
1568
+ return {
1569
+ type: "page",
1570
+ routePath: buildRoutePath(dirPath, unescapeBrackets(fileName)),
1571
+ params: [],
1572
+ rawSegment
1573
+ };
1574
+ }
1575
+ function parseDotNotationRoute(fileName, dirPath, rawSegment) {
1576
+ const segments = fileName.split(".");
1577
+ const lastSegment = segments[segments.length - 1];
1578
+ const parentSegments = segments.slice(0, -1);
1579
+ const virtualDir = isRootDir(dirPath) ? parentSegments.join("/") : `${dirPath}/${parentSegments.join("/")}`;
1580
+ if (lastSegment === "index") return {
1581
+ type: "index",
1582
+ routePath: buildRoutePath(virtualDir, ""),
1583
+ params: [],
1584
+ rawSegment
1585
+ };
1586
+ const dynamicParam = extractDynamicParam(lastSegment);
1587
+ if (dynamicParam) return {
1588
+ type: "dynamic",
1589
+ routePath: buildRoutePath(virtualDir, `:${dynamicParam}`),
1590
+ params: [createDynamicParam(dynamicParam)],
1591
+ rawSegment
1592
+ };
1593
+ return {
1594
+ type: "page",
1595
+ routePath: buildRoutePath(virtualDir, unescapeBrackets(lastSegment)),
1596
+ params: [],
1597
+ rawSegment
1598
+ };
1599
+ }
1600
+ /**
1601
+ * Apply basePath prefix to route path
1602
+ */
1603
+ function applyBasePath(routePath, basePath) {
1604
+ if (!basePath) return routePath;
1605
+ const normalizedBase = basePath.startsWith("/") ? basePath : `/${basePath}`;
1606
+ const base = normalizedBase.endsWith("/") ? normalizedBase.slice(0, -1) : normalizedBase;
1607
+ if (routePath === "/") return base || "/";
1608
+ return `${base}${routePath}`;
1609
+ }
1610
+ /**
1611
+ * Generate a unique variable name for a route
1612
+ */
1613
+ function generateVariableName(filePath, layer) {
1614
+ const withoutExt = filePath.replace(/\.[^/.]+$/, "");
1615
+ return (layer === "app" ? "" : `${sanitizeForVariable(layer)}_`) + withoutExt.replace(/[\/\\]/g, "_").replace(/[\[\]$().+-]/g, "_").replace(/^_+|_+$/g, "").replace(/_+/g, "_").replace(/^(\d)/, "_$1");
1616
+ }
1617
+ /**
1618
+ * Sanitize string for use in variable name
1619
+ */
1620
+ function sanitizeForVariable(str) {
1621
+ return str.replace(/^@/, "").replace(/[\/\\@-]/g, "_").replace(/^_+|_+$/g, "").replace(/_+/g, "_");
1622
+ }
1623
+ /**
1624
+ * Generate route name from route path
1625
+ */
1626
+ function generateRouteName(routePath, layer) {
1627
+ const layerPrefix = layer === "app" ? "" : `${layer.replace(/[@\/]/g, "-")}-`;
1628
+ if (routePath === "/") return `${layerPrefix}index`;
1629
+ return `${layerPrefix}${routePath.slice(1).replace(/\//g, "-").replace(/:/g, "").replace(/\(.*?\)\*/g, "catchAll").replace(/\?/g, "")}`;
1630
+ }
1631
+ /**
1632
+ * Check if route should be lazy loaded
1633
+ */
1634
+ function shouldBeLazy(filePath, config) {
1635
+ const { importMode } = config;
1636
+ if (typeof importMode === "function") return importMode(filePath) === "async";
1637
+ return importMode === "async";
1638
+ }
1639
+
1640
+ //#endregion
1641
+ //#region src/plugin.ts
1642
+ const VIRTUAL_ROUTES_ID = "\0virtual:kimesh-routes";
1643
+ const VIRTUAL_ROUTE_PREFIX = "\0kimesh-route:";
1644
+ const DEFAULT_CONFIG = {
1645
+ srcDir: "src",
1646
+ routesDir: "routes",
1647
+ generatedDir: ".kimesh",
1648
+ extensions: [".vue"],
1649
+ importMode: "async"
1650
+ };
1651
+ /**
1652
+ * Kimesh Router Generator Vite Plugin
1653
+ */
1654
+ function kimeshRouterGenerator(options = {}) {
1655
+ const debug = options.debug ?? false;
1656
+ let config;
1657
+ let server = null;
1658
+ let scanner;
1659
+ let routes = [];
1660
+ const log = (...args) => {
1661
+ if (debug) console.log("[kimesh:router]", ...args);
1662
+ };
1663
+ /**
1664
+ * Resolve layer routes - supports both static config and lazy getter
1665
+ */
1666
+ const resolveLayerRoutes = () => {
1667
+ if (options.getLayerRoutes) return options.getLayerRoutes();
1668
+ return options.layerRoutes ?? [];
1669
+ };
1670
+ return {
1671
+ name: "kimesh:router-generator",
1672
+ enforce: "pre",
1673
+ configResolved(viteConfig) {
1674
+ const root = viteConfig.root;
1675
+ const mergedConfig = {
1676
+ ...DEFAULT_CONFIG,
1677
+ ...options
1678
+ };
1679
+ config = {
1680
+ ...mergedConfig,
1681
+ root,
1682
+ routesDirPath: path.resolve(root, mergedConfig.srcDir, mergedConfig.routesDir),
1683
+ generatedDirPath: path.resolve(root, mergedConfig.generatedDir)
1684
+ };
1685
+ scanner = new RouteScanner(config);
1686
+ log("Config resolved:", {
1687
+ routesDirPath: config.routesDirPath,
1688
+ generatedDirPath: config.generatedDirPath,
1689
+ layerCount: resolveLayerRoutes().length
1690
+ });
1691
+ },
1692
+ async buildStart() {
1693
+ await generateRoutesOnce();
1694
+ },
1695
+ configureServer(_server) {
1696
+ server = _server;
1697
+ const watchPaths = [config.routesDirPath];
1698
+ const layerRoutes = resolveLayerRoutes();
1699
+ for (const layer of layerRoutes) if (fs.existsSync(layer.routesDir)) watchPaths.push(layer.routesDir);
1700
+ const watcher = watch(watchPaths, {
1701
+ ignoreInitial: true,
1702
+ ignored: ["**/.kimesh/**", "**/node_modules/**"]
1703
+ });
1704
+ watcher.on("add", handleFileChange);
1705
+ watcher.on("unlink", handleFileChange);
1706
+ log("Watching routes at:", watchPaths);
1707
+ },
1708
+ resolveId(id, importer) {
1709
+ if (id === "virtual:kimesh-routes" || id === "~kimesh/routes") return VIRTUAL_ROUTES_ID;
1710
+ if (isRouteQuery(id)) {
1711
+ const basePath = getBasePath(id);
1712
+ return VIRTUAL_ROUTE_PREFIX + (importer ? path.resolve(path.dirname(importer.replace(/\0/, "")), basePath) : basePath);
1713
+ }
1714
+ return null;
1715
+ },
1716
+ load(id) {
1717
+ if (id === VIRTUAL_ROUTES_ID) return generateRoutes(routes, config);
1718
+ if (id.startsWith(VIRTUAL_ROUTE_PREFIX)) {
1719
+ const filePath = id.slice(14);
1720
+ log("Extracting route definition from:", filePath);
1721
+ return extractRouteDefinition(filePath);
1722
+ }
1723
+ return null;
1724
+ }
1725
+ };
1726
+ async function generateRoutesOnce() {
1727
+ try {
1728
+ if (!fs.existsSync(config.routesDirPath)) {
1729
+ log("Routes directory does not exist:", config.routesDirPath);
1730
+ routes = [];
1731
+ }
1732
+ log("Scanning routes in:", config.routesDirPath);
1733
+ const scannedNodes = fs.existsSync(config.routesDirPath) ? await scanner.scan() : [];
1734
+ log("Scanned app nodes:", scannedNodes.length, scannedNodes.map((n) => n.filePath));
1735
+ let appRoutes = buildRouteTree(scannedNodes, config);
1736
+ log("Built app routes:", appRoutes.length, appRoutes.map((r) => r.routePath));
1737
+ const layerRoutes = resolveLayerRoutes();
1738
+ if (layerRoutes.length > 0) {
1739
+ const layerResult = await collectLayerRoutes(layerRoutes.map((l) => ({
1740
+ layer: l.layerName,
1741
+ routesDir: l.routesDir,
1742
+ basePath: l.basePath,
1743
+ priority: l.priority
1744
+ })), config);
1745
+ log("Collected layer routes:", layerResult.routes.length);
1746
+ routes = mergeRoutes(appRoutes, layerResult.routes, config);
1747
+ } else routes = appRoutes;
1748
+ log("Final merged routes:", routes.length, routes.map((r) => r.routePath));
1749
+ await writeGeneratedFiles();
1750
+ console.log(`[kimesh:router] Generated ${routes.length} route(s)`);
1751
+ } catch (error) {
1752
+ console.error("[kimesh:router] Error generating routes:", error);
1753
+ }
1754
+ }
1755
+ async function writeGeneratedFiles() {
1756
+ if (!fs.existsSync(config.generatedDirPath)) fs.mkdirSync(config.generatedDirPath, { recursive: true });
1757
+ const routesCode = generateRoutes(routes, config);
1758
+ const routesPath = path.join(config.generatedDirPath, "routes.gen.ts");
1759
+ fs.writeFileSync(routesPath, routesCode, "utf-8");
1760
+ const typesCode = generateRouteTypes(routes);
1761
+ const typesPath = path.join(config.generatedDirPath, "typed-routes.d.ts");
1762
+ fs.writeFileSync(typesPath, typesCode, "utf-8");
1763
+ const layerRoutes = resolveLayerRoutes();
1764
+ for (const layer of layerRoutes) {
1765
+ let layerRoot = layer.layerPath || path.dirname(layer.routesDir);
1766
+ try {
1767
+ layerRoot = fs.realpathSync(layerRoot);
1768
+ } catch {}
1769
+ const layerKimeshDir = path.join(layerRoot, ".kimesh");
1770
+ if (!fs.existsSync(layerKimeshDir)) fs.mkdirSync(layerKimeshDir, { recursive: true });
1771
+ const layerTypesPath = path.join(layerKimeshDir, "typed-routes.d.ts");
1772
+ fs.writeFileSync(layerTypesPath, typesCode, "utf-8");
1773
+ log("Generated layer types:", {
1774
+ layer: layer.layerName,
1775
+ path: layerTypesPath
1776
+ });
1777
+ }
1778
+ log("Generated files:", {
1779
+ routesPath,
1780
+ typesPath
1781
+ });
1782
+ }
1783
+ async function handleFileChange(filePath) {
1784
+ log("File changed:", filePath);
1785
+ await generateRoutesOnce();
1786
+ if (server) {
1787
+ const routesModule = server.moduleGraph.getModuleById(VIRTUAL_ROUTES_ID);
1788
+ if (routesModule) server.moduleGraph.invalidateModule(routesModule);
1789
+ server.ws.send({
1790
+ type: "full-reload",
1791
+ path: "*"
1792
+ });
1793
+ }
1794
+ }
1795
+ }
1796
+ /**
1797
+ * Merge app routes with layer routes
1798
+ * App routes (priority 0) take precedence over layer routes
1799
+ * Layer routes are nested under the host's root layout if one exists
1800
+ * If app has a layout route for a path, layer routes become its children
1801
+ */
1802
+ function mergeRoutes(appRoutes, layerRoutes, _config) {
1803
+ const rootLayout = findRootLayout(appRoutes);
1804
+ if (rootLayout && rootLayout.children) return mergeWithRootLayout(appRoutes, layerRoutes, rootLayout);
1805
+ return mergeWithoutRootLayout(appRoutes, layerRoutes);
1806
+ }
1807
+ function findRootLayout(routes) {
1808
+ return routes.find((r) => r.routePath === "/" && r.type === "layout") || null;
1809
+ }
1810
+ function mergeWithRootLayout(appRoutes, layerRoutes, rootLayout) {
1811
+ const layoutRoutesByPath = buildLayoutRoutesMap(rootLayout.children);
1812
+ for (const layerRoute of layerRoutes) {
1813
+ const relativePath = normalizeRoutePath(layerRoute.routePath);
1814
+ const hostLayoutRoute = layoutRoutesByPath.get(relativePath);
1815
+ if (hostLayoutRoute) nestLayerUnderHost(layerRoute, hostLayoutRoute);
1816
+ else addLayerAsSibling(layerRoute, relativePath, rootLayout, layoutRoutesByPath);
1817
+ }
1818
+ sortRouteChildren(rootLayout.children);
1819
+ return appRoutes;
1820
+ }
1821
+ function buildLayoutRoutesMap(children) {
1822
+ const map = /* @__PURE__ */ new Map();
1823
+ for (const child of children) if (child.routePath && !child.routePath.includes(":")) {
1824
+ const normalizedPath = normalizeRoutePath(child.routePath);
1825
+ map.set(normalizedPath, child);
1826
+ }
1827
+ return map;
1828
+ }
1829
+ function normalizeRoutePath(routePath) {
1830
+ return routePath.startsWith("/") ? routePath.slice(1) : routePath;
1831
+ }
1832
+ function nestLayerUnderHost(layerRoute, hostLayoutRoute) {
1833
+ hostLayoutRoute.children = hostLayoutRoute.children || [];
1834
+ hostLayoutRoute.type = "layout";
1835
+ const layerWrapper = {
1836
+ ...layerRoute,
1837
+ routePath: "",
1838
+ parent: hostLayoutRoute
1839
+ };
1840
+ hostLayoutRoute.children.push(layerWrapper);
1841
+ }
1842
+ function addLayerAsSibling(layerRoute, relativePath, rootLayout, layoutRoutesByPath) {
1843
+ const nestedRoute = {
1844
+ ...layerRoute,
1845
+ routePath: relativePath,
1846
+ parent: rootLayout
1847
+ };
1848
+ rootLayout.children.push(nestedRoute);
1849
+ layoutRoutesByPath.set(relativePath, nestedRoute);
1850
+ }
1851
+ function sortRouteChildren(children) {
1852
+ children.sort((a, b) => {
1853
+ if (a.routePath === "") return -1;
1854
+ if (b.routePath === "") return 1;
1855
+ return a.routePath.localeCompare(b.routePath);
1856
+ });
1857
+ }
1858
+ function mergeWithoutRootLayout(appRoutes, layerRoutes) {
1859
+ const merged = [...appRoutes];
1860
+ const usedPaths = new Set(appRoutes.map((r) => r.routePath));
1861
+ for (const layerRoute of layerRoutes) if (!usedPaths.has(layerRoute.routePath)) {
1862
+ merged.push(layerRoute);
1863
+ usedPaths.add(layerRoute.routePath);
1864
+ }
1865
+ merged.sort((a, b) => a.routePath.localeCompare(b.routePath));
1866
+ return merged;
1867
+ }
1868
+
1869
+ //#endregion
1870
+ //#region src/layout-resolver.ts
1871
+ /**
1872
+ * @kimesh/router-generator - Layout Resolver
1873
+ *
1874
+ * Resolves layout hierarchy using nested wrapping strategy.
1875
+ * Host layouts wrap layer layouts (outer → inner).
1876
+ */
1877
+ const logger = consola.withTag("kimesh:router:layout");
1878
+ /**
1879
+ * Layout resolver for nested wrapping
1880
+ *
1881
+ * Key principle: Host layouts WRAP layer layouts (outer → inner)
1882
+ * - Host root layout is always outermost
1883
+ * - Host path-specific layouts come next
1884
+ * - Layer layouts are innermost (closest to page content)
1885
+ */
1886
+ var LayoutResolver = class {
1887
+ hostLayouts = /* @__PURE__ */ new Map();
1888
+ layerLayouts = /* @__PURE__ */ new Map();
1889
+ layerMounts = /* @__PURE__ */ new Map();
1890
+ /**
1891
+ * Build layout chains for all routes
1892
+ */
1893
+ resolveLayoutChains(routes, layerMounts) {
1894
+ this.layerMounts = layerMounts;
1895
+ this.categorizeLayouts(routes);
1896
+ const chains = /* @__PURE__ */ new Map();
1897
+ const pageRoutes = routes.filter((r) => r.type !== "layout");
1898
+ for (const page of pageRoutes) {
1899
+ const chain = this.buildLayoutChain(page);
1900
+ chains.set(page.finalRoutePath, chain);
1901
+ }
1902
+ logger.debug(`Resolved layout chains for ${chains.size} pages`);
1903
+ return chains;
1904
+ }
1905
+ /**
1906
+ * Categorize layouts into host and layer layouts
1907
+ */
1908
+ categorizeLayouts(routes) {
1909
+ this.hostLayouts.clear();
1910
+ this.layerLayouts.clear();
1911
+ const layoutRoutes = routes.filter((r) => r.type === "layout");
1912
+ for (const route of layoutRoutes) {
1913
+ const layoutName = this.extractLayoutName(route.filePath);
1914
+ const layout = {
1915
+ filePath: route.fullPath,
1916
+ name: layoutName,
1917
+ routePath: route.routePath,
1918
+ layer: route.layer,
1919
+ priority: route.layerPriority,
1920
+ source: route.layer === "app" ? "host" : "layer",
1921
+ mountPath: route.layerBasePath
1922
+ };
1923
+ if (route.layer === "app") {
1924
+ const key = this.makeLayoutKey(route.routePath, layoutName);
1925
+ this.hostLayouts.set(key, layout);
1926
+ logger.debug(`Host layout: ${key} -> ${route.fullPath}`);
1927
+ } else {
1928
+ if (!this.layerLayouts.has(route.layer)) this.layerLayouts.set(route.layer, /* @__PURE__ */ new Map());
1929
+ const key = this.makeLayoutKey(route.routePath, layoutName);
1930
+ this.layerLayouts.get(route.layer).set(key, layout);
1931
+ logger.debug(`Layer layout: ${route.layer}:${key} -> ${route.fullPath}`);
1932
+ }
1933
+ }
1934
+ }
1935
+ /**
1936
+ * Build the complete layout chain for a page
1937
+ *
1938
+ * Order: Host root → Host path-specific → Layer root → Layer path-specific → Page
1939
+ */
1940
+ buildLayoutChain(page) {
1941
+ const layouts = [];
1942
+ const pageMeta = page.parsed.pageMeta;
1943
+ if (pageMeta?.layout === false) return {
1944
+ layouts: [],
1945
+ page
1946
+ };
1947
+ const requestedLayout = pageMeta?.layout;
1948
+ const hostChain = this.collectHostLayouts(page.finalRoutePath, requestedLayout);
1949
+ layouts.push(...hostChain);
1950
+ if (page.layer !== "app") {
1951
+ const layerChain = this.collectLayerLayouts(page.layer, page.routePath, page.layerBasePath, requestedLayout);
1952
+ layouts.push(...layerChain);
1953
+ }
1954
+ return {
1955
+ layouts,
1956
+ page
1957
+ };
1958
+ }
1959
+ /**
1960
+ * Collect host layouts from root to most specific path
1961
+ */
1962
+ collectHostLayouts(routePath, requestedLayout) {
1963
+ const layouts = [];
1964
+ const segments = routePath.split("/").filter(Boolean);
1965
+ const rootLayout = this.hostLayouts.get(this.makeLayoutKey("/", requestedLayout)) || this.hostLayouts.get("/");
1966
+ if (rootLayout) layouts.push(rootLayout);
1967
+ let currentPath = "";
1968
+ for (const segment of segments) {
1969
+ if (segment.startsWith(":")) continue;
1970
+ currentPath += "/" + segment;
1971
+ const layout = this.hostLayouts.get(this.makeLayoutKey(currentPath, requestedLayout)) || this.hostLayouts.get(currentPath);
1972
+ if (layout && !layouts.includes(layout)) layouts.push(layout);
1973
+ }
1974
+ return layouts;
1975
+ }
1976
+ /**
1977
+ * Collect layer layouts from layer root to most specific path
1978
+ */
1979
+ collectLayerLayouts(layerName, routePath, mountPath, requestedLayout) {
1980
+ const layouts = [];
1981
+ const layerLayoutMap = this.layerLayouts.get(layerName);
1982
+ if (!layerLayoutMap) return layouts;
1983
+ const segments = routePath.split("/").filter(Boolean);
1984
+ const rootLayout = layerLayoutMap.get(this.makeLayoutKey("/", requestedLayout)) || layerLayoutMap.get("/");
1985
+ if (rootLayout) layouts.push(rootLayout);
1986
+ let currentPath = "";
1987
+ for (const segment of segments) {
1988
+ if (segment.startsWith(":")) continue;
1989
+ currentPath += "/" + segment;
1990
+ const layout = layerLayoutMap.get(this.makeLayoutKey(currentPath, requestedLayout)) || layerLayoutMap.get(currentPath);
1991
+ if (layout && !layouts.includes(layout)) layouts.push(layout);
1992
+ }
1993
+ return layouts;
1994
+ }
1995
+ /**
1996
+ * Extract layout name from file path
1997
+ * _layout.admin.vue → 'admin'
1998
+ * _layout.vue → undefined
1999
+ */
2000
+ extractLayoutName(filePath) {
2001
+ const basename = path.basename(filePath, path.extname(filePath));
2002
+ if (basename.startsWith("_layout.") && basename !== "_layout") return basename.replace("_layout.", "");
2003
+ }
2004
+ /**
2005
+ * Make a unique key for layout lookup
2006
+ */
2007
+ makeLayoutKey(routePath, layoutName) {
2008
+ if (layoutName) return `${routePath}:${layoutName}`;
2009
+ return routePath;
2010
+ }
2011
+ };
2012
+ /**
2013
+ * Generate nested layout component structure for Vue Router
2014
+ *
2015
+ * This generates the component tree where layouts wrap their children
2016
+ */
2017
+ function generateLayoutStructure(chain) {
2018
+ if (chain.layouts.length === 0) return { component: chain.page.fullPath };
2019
+ let current = { component: chain.page.fullPath };
2020
+ for (let i = chain.layouts.length - 1; i >= 0; i--) current = {
2021
+ component: chain.layouts[i].filePath,
2022
+ children: [current]
2023
+ };
2024
+ return current;
2025
+ }
2026
+
2027
+ //#endregion
2028
+ export { LayoutResolver, RouteScanner, RouteTreeBuilder, buildGlobPattern, buildRoutePath, buildRouteTree, collectLayerRoutes, createCatchAllParam, createDynamicParam, createEmptyParsedRoute, createScanner, createTreeBuilder, extractDynamicParam, generateLayoutStructure, generateRouteTypes, generateRoutes, isDotNotationRoute, isRootDir, kimeshRouterGenerator, parseRouteFile, processDirectoryPath, processPathSegment, unescapeBrackets };
2029
+ //# sourceMappingURL=index.mjs.map