@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.
- package/README.md +459 -9
- package/dist/cli.js +0 -0
- package/dist/extract-meta.d.ts +27 -11
- package/dist/extract-meta.d.ts.map +1 -1
- package/dist/extract-meta.js +105 -16
- package/dist/extract-meta.js.map +1 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +215 -25
- package/dist/index.js.map +1 -1
- package/dist/vite.d.ts +51 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +242 -0
- package/dist/vite.js.map +1 -0
- package/package.json +7 -5
- package/dist/runtime/index.d.ts +0 -49
- package/dist/runtime/index.d.ts.map +0 -1
- package/dist/runtime/index.js +0 -28
- package/dist/runtime/index.js.map +0 -1
- package/dist/runtime/types.d.ts +0 -16
- package/dist/runtime/types.d.ts.map +0 -1
- package/dist/runtime/types.js +0 -5
- package/dist/runtime/types.js.map +0 -1
package/dist/extract-meta.js
CHANGED
|
@@ -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
|
|
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
|
|
11
|
+
export function extractRouteConfig(filePath) {
|
|
7
12
|
try {
|
|
8
13
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
9
14
|
const { descriptor } = parse(content);
|
|
10
|
-
//
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
//
|
|
16
|
-
|
|
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
|
|
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
|
-
*
|
|
26
|
-
* The content should be valid JSON or JavaScript object literal
|
|
71
|
+
* Extract defineRoute() call content from script setup
|
|
27
72
|
*/
|
|
28
|
-
function
|
|
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
|
-
|
|
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
|
|
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
|
package/dist/extract-meta.js.map
CHANGED
|
@@ -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;
|
|
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
|
|
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?:
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,
|
|
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(
|
|
114
|
-
lines.push(`${nextIndent}name: ${JSON.stringify(
|
|
115
|
-
lines.push(`${nextIndent}component: () => import(${JSON.stringify(
|
|
116
|
-
//
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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('}');
|