@zphhpzzph/vue-route-gen 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,48 +1,137 @@
1
1
  import fs from 'node:fs';
2
2
  import { parse } from '@vue/compiler-sfc';
3
3
  /**
4
- * Parse Vue SFC and extract metadata from <route> custom block
4
+ * Parse Vue SFC and extract complete route configuration
5
+ * Supports both <route> custom block and defineRoute() macro
6
+ *
7
+ * @param filePath - Path to the Vue SFC file
8
+ * @returns Route configuration override, or undefined if no custom config
9
+ * @throws Error if both <route> block and defineRoute() are present
5
10
  */
6
- export function extractRouteMeta(filePath) {
11
+ export function extractRouteConfig(filePath) {
7
12
  try {
8
13
  const content = fs.readFileSync(filePath, 'utf-8');
9
14
  const { descriptor } = parse(content);
10
- // Find the <route> custom block
11
- const routeBlock = descriptor.customBlocks.find((block) => block.type === 'route');
12
- if (!routeBlock) {
13
- return {};
15
+ // Detect conflict between <route> block and defineRoute()
16
+ const hasRouteBlock = descriptor.customBlocks.some((b) => b.type === 'route');
17
+ const hasDefineRoute = descriptor.scriptSetup?.content.includes('defineRoute');
18
+ if (hasRouteBlock && hasDefineRoute) {
19
+ throw new Error(`[vue-route-gen] Error in ${filePath}:\n` +
20
+ 'Cannot use both <route> custom block and defineRoute() macro.\n' +
21
+ 'Please choose one method:\n' +
22
+ ' - Use <route> block in template section\n' +
23
+ ' - Use defineRoute() in <script setup>\n\n' +
24
+ 'Example:\n' +
25
+ ' <route>{ "path": "/custom" }</route>\n' +
26
+ ' OR\n' +
27
+ ' <script setup>\n' +
28
+ ' defineRoute({ path: "/custom" })\n' +
29
+ ' </script>');
30
+ }
31
+ // Extract from <route> block
32
+ if (hasRouteBlock) {
33
+ const routeBlock = descriptor.customBlocks.find((b) => b.type === 'route');
34
+ return parseRouteConfig(routeBlock.content);
14
35
  }
15
- // Parse the content of the <route> block
16
- return parseRouteBlockContent(routeBlock.content);
36
+ // Extract from defineRoute() call
37
+ if (hasDefineRoute) {
38
+ const defineRouteCall = extractDefineRouteCall(descriptor.scriptSetup.content);
39
+ if (defineRouteCall) {
40
+ return evaluateDefineRouteCall(defineRouteCall, descriptor.scriptSetup.content);
41
+ }
42
+ }
43
+ return undefined;
44
+ }
45
+ catch (error) {
46
+ // Re-throw conflict errors as they are user errors
47
+ if (error instanceof Error && error.message.includes('Cannot use both')) {
48
+ throw error;
49
+ }
50
+ // Warn about other errors but don't fail the build
51
+ console.warn(`[vue-route-gen] Failed to extract route config from ${filePath}:`, error);
52
+ return undefined;
53
+ }
54
+ }
55
+ /**
56
+ * Parse Vue SFC and extract metadata from <route> custom block
57
+ * @deprecated Use extractRouteConfig() instead for full configuration support
58
+ */
59
+ export function extractRouteMeta(filePath) {
60
+ try {
61
+ const config = extractRouteConfig(filePath);
62
+ return config?.meta ?? {};
17
63
  }
18
64
  catch (error) {
19
- // If parsing fails, return empty meta
65
+ // If config extraction fails (e.g., conflict), return empty meta
20
66
  console.warn(`Failed to extract meta from ${filePath}:`, error);
21
67
  return {};
22
68
  }
23
69
  }
24
70
  /**
25
- * Parse the content of <route> custom block
26
- * The content should be valid JSON or JavaScript object literal
71
+ * Extract defineRoute() call content from script setup
27
72
  */
28
- function parseRouteBlockContent(content) {
73
+ function extractDefineRouteCall(scriptContent) {
74
+ // Match: const xxx = defineRoute(...) or defineRoute(...)
75
+ // Support both variable assignment and direct call
76
+ const patterns = [
77
+ // const/let/var xxx = defineRoute(...)
78
+ /(?:const|let|var)\s+\w+\s*=\s*defineRoute\s*\(([\s\S]*?)\)\s*(?:;|$)/,
79
+ // Direct defineRoute(...) call
80
+ /defineRoute\s*\(([\s\S]*?)\)\s*(?:;|$)/,
81
+ ];
82
+ for (const pattern of patterns) {
83
+ const match = scriptContent.match(pattern);
84
+ if (match) {
85
+ return match[1];
86
+ }
87
+ }
88
+ return null;
89
+ }
90
+ /**
91
+ * Evaluate defineRoute() call content with constant replacement
92
+ * Phase 1 MVP: Simple evaluation without complex constant resolution
93
+ */
94
+ function evaluateDefineRouteCall(callContent, scriptContent) {
95
+ try {
96
+ // Phase 1 MVP: Direct evaluation without constant replacement
97
+ // Future enhancement: Parse imports and replace constants
98
+ return parseRouteConfig(callContent);
99
+ }
100
+ catch (error) {
101
+ console.warn('[vue-route-gen] Failed to evaluate defineRoute() call:', error);
102
+ return undefined;
103
+ }
104
+ }
105
+ /**
106
+ * Parse route configuration from content
107
+ * Supports both JSON and JavaScript object literal syntax
108
+ */
109
+ function parseRouteConfig(content) {
29
110
  const trimmed = content.trim();
30
111
  try {
31
112
  // Try parsing as JSON first
32
- return JSON.parse(trimmed);
113
+ const parsed = JSON.parse(trimmed);
114
+ return parsed;
33
115
  }
34
116
  catch {
35
117
  // If JSON parsing fails, try evaluating as JavaScript object literal
36
118
  try {
37
119
  // Use Function constructor for safe evaluation
38
- // This handles things like unquoted keys, trailing commas, etc.
39
120
  const fn = new Function(`return (${trimmed});`);
40
121
  return fn();
41
122
  }
42
123
  catch (error) {
43
- console.warn('Failed to parse route block content:', error);
44
- return {};
124
+ console.warn('[vue-route-gen] Failed to parse route config:', error);
125
+ return undefined;
45
126
  }
46
127
  }
47
128
  }
129
+ /**
130
+ * Parse the content of <route> custom block
131
+ * The content should be valid JSON or JavaScript object literal
132
+ * @deprecated Use parseRouteConfig() instead
133
+ */
134
+ function parseRouteBlockContent(content) {
135
+ return parseRouteConfig(content) ?? {};
136
+ }
48
137
  //# sourceMappingURL=extract-meta.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"extract-meta.js","sourceRoot":"","sources":["../src/extract-meta.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAY1C;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,QAAgB;IAC/C,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QACnD,MAAM,EAAE,UAAU,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;QAEtC,gCAAgC;QAChC,MAAM,UAAU,GAAG,UAAU,CAAC,YAAY,CAAC,IAAI,CAC7C,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,OAAO,CAClC,CAAC;QAEF,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,yCAAyC;QACzC,OAAO,sBAAsB,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;IACpD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,sCAAsC;QACtC,OAAO,CAAC,IAAI,CAAC,+BAA+B,QAAQ,GAAG,EAAE,KAAK,CAAC,CAAC;QAChE,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,SAAS,sBAAsB,CAAC,OAAe;IAC7C,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IAE/B,IAAI,CAAC;QACH,4BAA4B;QAC5B,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,qEAAqE;QACrE,IAAI,CAAC;YACH,+CAA+C;YAC/C,gEAAgE;YAChE,MAAM,EAAE,GAAG,IAAI,QAAQ,CAAC,WAAW,OAAO,IAAI,CAAC,CAAC;YAChD,OAAO,EAAE,EAAE,CAAC;QACd,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,sCAAsC,EAAE,KAAK,CAAC,CAAC;YAC5D,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"extract-meta.js","sourceRoot":"","sources":["../src/extract-meta.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { type RouteMeta } from './extract-meta.js';
2
- export type { RouteMeta } from './extract-meta.js';
1
+ import { type RouteConfigOverride } from './extract-meta.js';
3
2
  export interface GenerateRoutesOptions {
4
3
  pagesDir?: string;
5
4
  outFile?: string;
@@ -10,7 +9,8 @@ export interface RouteEntry {
10
9
  importPath: string;
11
10
  children: RouteEntry[];
12
11
  params?: string[];
13
- meta?: RouteMeta;
12
+ meta?: Record<string, any>;
13
+ configOverride?: RouteConfigOverride;
14
14
  }
15
15
  export interface RouteData {
16
16
  routes: RouteEntry[];
@@ -18,6 +18,11 @@ export interface RouteData {
18
18
  routePathList: readonly string[];
19
19
  routePathByName: [string, string][];
20
20
  routeParamsByName: [string, string[]][];
21
+ routeKeyData: Array<{
22
+ name: string;
23
+ path: string;
24
+ defaultName: string;
25
+ }>;
21
26
  }
22
27
  export declare function generateRoutes({ pagesDir, outFile, }?: GenerateRoutesOptions): boolean;
23
28
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAErE,YAAY,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAKnD,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,UAAU,EAAE,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,CAAC,EAAE,SAAS,CAAC;CAClB;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,eAAe,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;IACpC,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;CACzC;AAmhBD,wBAAgB,cAAc,CAAC,EAC7B,QAAmD,EACnD,OAAgE,GACjE,GAAE,qBAA0B,GAAG,OAAO,CAiCtC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAwC,KAAK,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAyDnG,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,UAAU,EAAE,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC3B,cAAc,CAAC,EAAE,mBAAmB,CAAC;CACtC;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,eAAe,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;IACpC,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;IACxC,YAAY,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC1E;AAgrBD,wBAAgB,cAAc,CAAC,EAC7B,QAAmD,EACnD,OAAgE,GACjE,GAAE,qBAA0B,GAAG,OAAO,CAiCtC"}
package/dist/index.js CHANGED
@@ -1,8 +1,54 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { extractRouteMeta } from './extract-meta.js';
3
+ import { extractRouteMeta, extractRouteConfig } from './extract-meta.js';
4
4
  const EXCLUDED_DIRS = new Set(['components', 'hooks', 'services', 'types', 'constants', 'utils']);
5
5
  const CACHE_FILE = path.resolve(process.cwd(), 'node_modules/.cache/route-gen.json');
6
+ /**
7
+ * Convert a value to its TypeScript literal type representation
8
+ * Generates precise literal types instead of wide types like string/boolean
9
+ */
10
+ function valueToLiteralType(value) {
11
+ if (value === null) {
12
+ return 'null';
13
+ }
14
+ if (value === undefined) {
15
+ return 'undefined';
16
+ }
17
+ const valueType = typeof value;
18
+ // Handle primitive types with literal values
19
+ if (valueType === 'string') {
20
+ return JSON.stringify(value); // Returns "value" with quotes
21
+ }
22
+ if (valueType === 'boolean') {
23
+ return value ? 'true' : 'false';
24
+ }
25
+ if (valueType === 'number') {
26
+ return value.toString();
27
+ }
28
+ // Handle arrays
29
+ if (Array.isArray(value)) {
30
+ if (value.length === 0) {
31
+ return '[]';
32
+ }
33
+ const elementType = value.map(v => valueToLiteralType(v)).join(' | ');
34
+ return `[${elementType}]`;
35
+ }
36
+ // Handle objects
37
+ if (valueType === 'object') {
38
+ const entries = Object.entries(value);
39
+ if (entries.length === 0) {
40
+ return '{}';
41
+ }
42
+ const fields = entries
43
+ .map(([key, val]) => {
44
+ const literalType = valueToLiteralType(val);
45
+ return ` ${key}: ${literalType}`;
46
+ })
47
+ .join(';\n');
48
+ return `{\n${fields}\n }`;
49
+ }
50
+ return 'any';
51
+ }
6
52
  function normalizePath(p) {
7
53
  return p.split(path.sep).join('/');
8
54
  }
@@ -97,6 +143,32 @@ function segmentsToPath(segments, leadingSlash) {
97
143
  }
98
144
  return cleaned;
99
145
  }
146
+ /**
147
+ * Apply route configuration override to default route entry
148
+ * Merges user-provided config with auto-generated defaults
149
+ */
150
+ function applyConfigOverride(route) {
151
+ if (!route.configOverride) {
152
+ return route;
153
+ }
154
+ const override = route.configOverride;
155
+ const merged = {
156
+ ...route,
157
+ // User config takes precedence
158
+ path: override.path ?? route.path,
159
+ name: override.name ?? route.name,
160
+ // component is always auto-generated from the file
161
+ importPath: route.importPath,
162
+ };
163
+ // Merge meta: user config overrides defaults
164
+ if (override.meta || route.meta) {
165
+ merged.meta = {
166
+ ...route.meta,
167
+ ...override.meta,
168
+ };
169
+ }
170
+ return merged;
171
+ }
100
172
  function joinPaths(parent, child) {
101
173
  if (!child) {
102
174
  return parent || '/';
@@ -106,23 +178,82 @@ function joinPaths(parent, child) {
106
178
  }
107
179
  return `${parent.replace(/\/$/, '')}/${child}`.replace(/\/+/g, '/');
108
180
  }
181
+ function renderMetaAsConst(meta, indent) {
182
+ const lines = [];
183
+ const nextIndent = `${indent} `;
184
+ lines.push(`${indent}{`);
185
+ for (const [key, value] of Object.entries(meta)) {
186
+ if (typeof value === 'string') {
187
+ lines.push(`${nextIndent}${JSON.stringify(key)}: ${JSON.stringify(value)},`);
188
+ }
189
+ else if (typeof value === 'boolean') {
190
+ lines.push(`${nextIndent}${JSON.stringify(key)}: ${value},`);
191
+ }
192
+ else if (typeof value === 'number') {
193
+ lines.push(`${nextIndent}${JSON.stringify(key)}: ${value},`);
194
+ }
195
+ else if (Array.isArray(value)) {
196
+ const arrayStr = value.map(v => JSON.stringify(v)).join(', ');
197
+ lines.push(`${nextIndent}${JSON.stringify(key)}: [${arrayStr}],`);
198
+ }
199
+ else if (typeof value === 'object' && value !== null) {
200
+ lines.push(`${nextIndent}${JSON.stringify(key)}: ${JSON.stringify(value)},`);
201
+ }
202
+ else {
203
+ lines.push(`${nextIndent}${JSON.stringify(key)}: ${JSON.stringify(value)},`);
204
+ }
205
+ }
206
+ lines.push(`${indent}} as const`);
207
+ return lines.join('\n');
208
+ }
109
209
  function renderRoute(route, indent = ' ') {
110
210
  const nextIndent = `${indent} `;
111
211
  const lines = [];
212
+ // Apply configuration override
213
+ const finalRoute = applyConfigOverride(route);
214
+ const override = route.configOverride;
112
215
  lines.push(`${indent}{`);
113
- lines.push(`${nextIndent}path: ${JSON.stringify(route.path)},`);
114
- lines.push(`${nextIndent}name: ${JSON.stringify(route.name)},`);
115
- lines.push(`${nextIndent}component: () => import(${JSON.stringify(route.importPath)}),`);
116
- // Add meta if present
117
- if (route.meta && Object.keys(route.meta).length > 0) {
118
- lines.push(`${nextIndent}meta: ${JSON.stringify(route.meta)},`);
119
- }
120
- if (route.children.length === 0) {
216
+ lines.push(`${nextIndent}path: ${JSON.stringify(finalRoute.path)},`);
217
+ lines.push(`${nextIndent}name: ${JSON.stringify(finalRoute.name)},`);
218
+ lines.push(`${nextIndent}component: () => import(${JSON.stringify(finalRoute.importPath)}),`);
219
+ // Render additional RouteRecordRaw fields from override
220
+ if (override) {
221
+ if (override.alias !== undefined) {
222
+ if (Array.isArray(override.alias)) {
223
+ lines.push(`${nextIndent}alias: [${override.alias.map(a => JSON.stringify(a)).join(', ')}],`);
224
+ }
225
+ else {
226
+ lines.push(`${nextIndent}alias: ${JSON.stringify(override.alias)},`);
227
+ }
228
+ }
229
+ if (override.redirect !== undefined) {
230
+ lines.push(`${nextIndent}redirect: ${JSON.stringify(override.redirect)},`);
231
+ }
232
+ if (override.props !== undefined) {
233
+ if (typeof override.props === 'boolean') {
234
+ lines.push(`${nextIndent}props: ${override.props},`);
235
+ }
236
+ else if (typeof override.props === 'object') {
237
+ lines.push(`${nextIndent}props: ${JSON.stringify(override.props)},`);
238
+ }
239
+ else {
240
+ lines.push(`${nextIndent}props: ${override.props},`);
241
+ }
242
+ }
243
+ if (override.beforeEnter !== undefined) {
244
+ lines.push(`${nextIndent}beforeEnter: ${override.beforeEnter},`);
245
+ }
246
+ }
247
+ // Add meta if present (rendered as const for type inference)
248
+ if (finalRoute.meta && Object.keys(finalRoute.meta).length > 0) {
249
+ lines.push(`${nextIndent}meta: ${renderMetaAsConst(finalRoute.meta, nextIndent)},`);
250
+ }
251
+ if (finalRoute.children.length === 0) {
121
252
  lines.push(`${nextIndent}children: [],`);
122
253
  }
123
254
  else {
124
255
  lines.push(`${nextIndent}children: [`);
125
- lines.push(route.children
256
+ lines.push(finalRoute.children
126
257
  .map((child) => renderRoute(child, `${nextIndent} `))
127
258
  .join(',\n'));
128
259
  lines.push(`${nextIndent}],`);
@@ -205,22 +336,27 @@ function buildRoutes({ pagesDir, outFile }) {
205
336
  }
206
337
  const routeEntries = [];
207
338
  const standaloneRoutes = standalonePages.map((page) => {
208
- const name = page.segments.join('-');
339
+ const defaultName = page.segments.join('-');
209
340
  const routePath = segmentsToPath(page.segments, true);
210
341
  const params = page.segments
211
342
  .map((s) => extractParamName(s))
212
343
  .filter((p) => p !== null);
213
- routeEntries.push({ name, path: routePath, params });
214
- // Extract meta from page component
344
+ // Extract config and meta from page component
215
345
  const fullPath = path.resolve(pagesDir, page.importPath.replace(/^\.\//, ''));
346
+ const configOverride = extractRouteConfig(fullPath);
216
347
  const meta = extractRouteMeta(fullPath);
348
+ // Apply config override to get the final name and path
349
+ const finalName = configOverride?.name ?? defaultName;
350
+ const finalPath = configOverride?.path ?? routePath;
351
+ routeEntries.push({ name: finalName, path: finalPath, params, defaultName });
217
352
  return {
218
353
  path: routePath,
219
- name,
354
+ name: finalName, // Use overridden name
220
355
  importPath: page.importPath,
221
356
  children: [],
222
357
  params,
223
358
  meta,
359
+ configOverride,
224
360
  };
225
361
  });
226
362
  const layoutRoutes = Array.from(layoutGroups.entries())
@@ -234,26 +370,31 @@ function buildRoutes({ pagesDir, outFile }) {
234
370
  const layoutParams = layout.segments
235
371
  .map((s) => extractParamName(s))
236
372
  .filter((p) => p !== null);
237
- routeEntries.push({ name: layoutName, path: layoutPath, params: layoutParams });
373
+ routeEntries.push({ name: layoutName, path: layoutPath, params: layoutParams, defaultName: layoutName });
238
374
  const children = sortedPages.map((page) => {
239
- const name = page.segments.join('-');
375
+ const defaultName = page.segments.join('-');
240
376
  const relativeSegments = page.segments.slice(layout.segments.length);
241
377
  const childPath = segmentsToPath(relativeSegments, false);
242
378
  const fullPath = joinPaths(layoutPath, childPath);
243
379
  const params = relativeSegments
244
380
  .map((s) => extractParamName(s))
245
381
  .filter((p) => p !== null);
246
- routeEntries.push({ name, path: fullPath, params });
247
- // Extract meta from page component
382
+ // Extract config and meta from page component
248
383
  const pageFilePath = path.resolve(pagesDir, page.importPath.replace(/^\.\//, ''));
384
+ const configOverride = extractRouteConfig(pageFilePath);
249
385
  const meta = extractRouteMeta(pageFilePath);
386
+ // Apply config override to get the final name and path
387
+ const finalName = configOverride?.name ?? defaultName;
388
+ const finalPath = configOverride?.path ?? fullPath;
389
+ routeEntries.push({ name: finalName, path: finalPath, params, defaultName });
250
390
  return {
251
391
  path: childPath,
252
- name,
392
+ name: finalName, // Use overridden name
253
393
  importPath: page.importPath,
254
394
  children: [],
255
395
  params,
256
396
  meta,
397
+ configOverride,
257
398
  };
258
399
  });
259
400
  return {
@@ -283,16 +424,25 @@ function buildRoutes({ pagesDir, outFile }) {
283
424
  routePathList: uniquePaths,
284
425
  routePathByName,
285
426
  routeParamsByName: Array.from(paramsByName.entries()),
427
+ routeKeyData: routeEntries,
286
428
  };
287
429
  }
288
- function renderRoutesFile({ routes, routeNameList, routePathList, routePathByName, routeParamsByName }) {
430
+ function renderRoutesFile({ routes, routeNameList, routePathList, routePathByName, routeParamsByName, routeKeyData }) {
289
431
  const lines = [];
290
432
  const pathByName = new Map(routePathByName);
291
433
  const paramsByName = new Map(routeParamsByName);
292
434
  const routeKeyEntries = [];
293
435
  const usedKeys = new Set();
436
+ // Build map of route entries by name for quick lookup
437
+ const entryMap = new Map();
438
+ for (const entry of routeKeyData) {
439
+ entryMap.set(entry.name, { name: entry.name, path: entry.path, defaultName: entry.defaultName });
440
+ }
294
441
  for (const name of routeNameList) {
295
- const baseKey = toConstKey(name) || 'ROUTE';
442
+ const entry = entryMap.get(name);
443
+ if (!entry)
444
+ continue;
445
+ const baseKey = toConstKey(entry.defaultName) || 'ROUTE';
296
446
  let key = baseKey;
297
447
  let suffix = 1;
298
448
  while (usedKeys.has(key)) {
@@ -302,8 +452,9 @@ function renderRoutesFile({ routes, routeNameList, routePathList, routePathByNam
302
452
  usedKeys.add(key);
303
453
  routeKeyEntries.push({
304
454
  key,
305
- name,
306
- path: pathByName.get(name),
455
+ name: entry.name,
456
+ path: entry.path,
457
+ defaultName: entry.defaultName,
307
458
  });
308
459
  }
309
460
  lines.push('// This file is auto-generated by @zphhpzzph/vue-route-gen.');
@@ -366,6 +517,43 @@ function renderRoutesFile({ routes, routeNameList, routePathList, routePathByNam
366
517
  lines.push('');
367
518
  lines.push('export type RouteParamsByName<T extends RouteName> = RouteParams[T];');
368
519
  lines.push('');
520
+ // Collect all unique meta keys across all routes
521
+ const allMetaKeys = new Set();
522
+ for (const route of routes) {
523
+ if (route.meta) {
524
+ Object.keys(route.meta).forEach(key => allMetaKeys.add(key));
525
+ }
526
+ }
527
+ const sortedMetaKeys = Array.from(allMetaKeys).sort();
528
+ // Generate route meta types with literal types
529
+ lines.push('// Route metadata types (extracted from <route> blocks)');
530
+ lines.push('// Uses literal types for precise type inference');
531
+ lines.push('// Missing fields are typed as undefined to ensure consistent shape');
532
+ lines.push('export interface RouteMetaMap {');
533
+ for (const route of routes) {
534
+ const routeName = route.name;
535
+ const meta = route.meta || {};
536
+ // Generate type definition with all possible fields
537
+ // Missing fields are typed as undefined
538
+ const metaFields = sortedMetaKeys
539
+ .map((key) => {
540
+ if (key in meta) {
541
+ const literalType = valueToLiteralType(meta[key]);
542
+ return ` ${key}: ${literalType};`;
543
+ }
544
+ else {
545
+ return ` ${key}: undefined;`;
546
+ }
547
+ })
548
+ .join('\n');
549
+ lines.push(` '${routeName}': {`);
550
+ lines.push(metaFields);
551
+ lines.push(` };`);
552
+ }
553
+ lines.push('}');
554
+ lines.push('');
555
+ lines.push('export type RouteMetaByName<T extends RouteName> = RouteMetaMap[T];');
556
+ lines.push('');
369
557
  lines.push('export const routes = [');
370
558
  lines.push(routes.map((route) => renderRoute(route)).join(',\n'));
371
559
  lines.push('] satisfies RouteRecordRaw[];');
@@ -379,19 +567,21 @@ function renderRoutesFile({ routes, routeNameList, routePathList, routePathByNam
379
567
  // Generate useRoute with proper types
380
568
  lines.push('/**');
381
569
  lines.push(' * Type-safe useRoute hook');
382
- lines.push(' * Route params are typed based on the current route name');
570
+ lines.push(' * Route params and meta are typed based on the current route name');
383
571
  lines.push(' *');
384
572
  lines.push(' * @example');
385
573
  lines.push(' * ```ts');
386
574
  lines.push(' * const route = useRoute();');
387
575
  lines.push(' * // route.params.id is typed as string if route has :id param');
576
+ lines.push(' * // route.meta.title is typed based on the route\'s <route> block');
388
577
  lines.push(' * ```');
389
578
  lines.push(' */');
390
579
  lines.push('export function useRoute<TName extends RouteName = RouteName>(');
391
580
  lines.push(' name?: TName');
392
- lines.push('): Omit<RouteLocationNormalizedLoaded, \'params\' | \'name\'> & {');
581
+ lines.push('): Omit<RouteLocationNormalizedLoaded, \'params\' | \'name\' | \'meta\'> & {');
393
582
  lines.push(' name: TName;');
394
583
  lines.push(' params: TName extends keyof RouteParams ? RouteParams[TName] : RouteParams[RouteName];');
584
+ lines.push(' meta: TName extends keyof RouteMetaMap ? RouteMetaMap[TName] : RouteMetaMap[RouteName];');
395
585
  lines.push('} {');
396
586
  lines.push(' return vueUseRoute() as any;');
397
587
  lines.push('}');