nuxt-typed-router 1.2.5 โ 2.0.0-beta.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 +14 -4
- package/dist/module.d.ts +1 -9
- package/dist/module.json +1 -1
- package/dist/module.mjs +456 -277
- package/main.d.ts +0 -1
- package/package.json +9 -7
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# ๐๐ฆ Typed Router for Nuxt
|
|
2
2
|
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img width='100' src="https://raw.githubusercontent.com/victorgarciaesgi/nuxt-typed-router/master/.github/images/logo.png" alt="nuxt-typed-router logo">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
|
|
3
8
|
[npm-version-src]: https://img.shields.io/npm/v/nuxt-typed-router.svg
|
|
4
9
|
[npm-version-href]: https://www.npmjs.com/package/nuxt-typed-router
|
|
5
10
|
[npm-downloads-src]: https://img.shields.io/npm/dm/nuxt-typed-router.svg
|
|
@@ -9,9 +14,9 @@
|
|
|
9
14
|
[![npm version][npm-version-src]][npm-version-href]
|
|
10
15
|
[![npm downloads][npm-downloads-src]][npm-downloads-href]
|
|
11
16
|
[![npm downloads][npm-total-downloads-src]][npm-downloads-href]
|
|
12
|
-
<img src='https://img.shields.io/npm/l/
|
|
17
|
+
<img src='https://img.shields.io/npm/l/nuxt-typed-router.svg'>
|
|
13
18
|
|
|
14
|
-
|
|
19
|
+
## Provide a type safe router to Nuxt with auto-generated typed definitions for route names and autocompletion for route params
|
|
15
20
|
|
|
16
21
|
- ๐ Provides a hook `useTypedRouter` that returns an alias of `$typedRouter` and also a typed list of your routes
|
|
17
22
|
- ๐ Expose a global method `$typedRouter` (clone of vue-router), but typed with the routes defined in `pages` directory
|
|
@@ -28,7 +33,7 @@ Demo ๐งช : [nuxt-typed-router-demo](https://github.com/victorgarciaesgi/nuxt-ty
|
|
|
28
33
|
|
|
29
34
|
<br/>
|
|
30
35
|
<p align="center">
|
|
31
|
-
<img src="https://github.com/victorgarciaesgi/nuxt-typed-router/blob/master/
|
|
36
|
+
<img src="https://github.com/victorgarciaesgi/nuxt-typed-router/blob/master/.github/images/in-action.gif?raw=true"/>
|
|
32
37
|
</p>
|
|
33
38
|
<br/>
|
|
34
39
|
|
|
@@ -78,6 +83,11 @@ interface ModuleOptions {
|
|
|
78
83
|
* @default "routerPagesNames"
|
|
79
84
|
* */
|
|
80
85
|
routesObjectName?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Set to false if you don't want a plugin generated
|
|
88
|
+
* @default false
|
|
89
|
+
*/
|
|
90
|
+
plugin?: boolean;
|
|
81
91
|
}
|
|
82
92
|
```
|
|
83
93
|
|
|
@@ -100,7 +110,7 @@ You can specify the output dir of the generated files in your configuration. It
|
|
|
100
110
|
|
|
101
111
|
```ts
|
|
102
112
|
export default defineNuxtConfig({
|
|
103
|
-
|
|
113
|
+
modules: ['nuxt-typed-router'],
|
|
104
114
|
nuxtTypedRouter: {
|
|
105
115
|
outDir: './generated',
|
|
106
116
|
},
|
package/dist/module.d.ts
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
2
|
|
|
3
3
|
interface ModuleOptions {
|
|
4
|
-
/** Output directory where you cant the files to be saved (ex: "./generated")
|
|
5
|
-
* @default "<srcDir>/generated"
|
|
6
|
-
*/
|
|
7
|
-
outDir?: string;
|
|
8
|
-
/** Name of the routesNames object (ex: "routesTree")
|
|
9
|
-
* @default "routerPagesNames"
|
|
10
|
-
* */
|
|
11
|
-
routesObjectName?: string;
|
|
12
4
|
/**
|
|
13
5
|
* Set to false if you don't want a plugin generated
|
|
14
|
-
* @default
|
|
6
|
+
* @default false
|
|
15
7
|
*/
|
|
16
8
|
plugin?: boolean;
|
|
17
9
|
}
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,13 +1,331 @@
|
|
|
1
|
+
import { fileURLToPath } from 'url';
|
|
1
2
|
import { extendPages, defineNuxtModule } from '@nuxt/kit';
|
|
2
3
|
import chalk from 'chalk';
|
|
3
4
|
import logSymbols from 'log-symbols';
|
|
4
5
|
import prettier from 'prettier';
|
|
5
6
|
import fs from 'fs';
|
|
6
|
-
import { fileURLToPath } from 'url';
|
|
7
7
|
import { dirname, resolve } from 'pathe';
|
|
8
8
|
import mkdirp from 'mkdirp';
|
|
9
9
|
import { camelCase } from 'lodash-es';
|
|
10
10
|
|
|
11
|
+
const staticTypesImports = `
|
|
12
|
+
import type {
|
|
13
|
+
NavigationFailure,
|
|
14
|
+
RouteLocation,
|
|
15
|
+
RouteLocationNormalizedLoaded,
|
|
16
|
+
RouteLocationOptions,
|
|
17
|
+
RouteQueryAndHash,
|
|
18
|
+
RouteLocationRaw,
|
|
19
|
+
Router,
|
|
20
|
+
} from 'vue-router';
|
|
21
|
+
import type { DefineComponent } from 'vue';
|
|
22
|
+
import type { NuxtLinkProps } from '#app';
|
|
23
|
+
import type {
|
|
24
|
+
TypedRouteList,
|
|
25
|
+
TypedRouteNamedMapper,
|
|
26
|
+
TypedRouteParams,
|
|
27
|
+
ResolvedTypedRouteNamedMapper,
|
|
28
|
+
} from './__routes';
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
const staticTypeUtils = `
|
|
32
|
+
// Type utils
|
|
33
|
+
type ExtractRequiredParameters<T extends Record<string, any>> = Pick<
|
|
34
|
+
T,
|
|
35
|
+
{ [K in keyof T]: undefined extends T[K] ? never : K }[keyof T]
|
|
36
|
+
>;
|
|
37
|
+
|
|
38
|
+
type HasOneRequiredParameter<T extends TypedRouteList> = [TypedRouteParams[T]] extends [never]
|
|
39
|
+
? false
|
|
40
|
+
: [keyof ExtractRequiredParameters<TypedRouteParams[T]>] extends [undefined]
|
|
41
|
+
? false
|
|
42
|
+
: true;
|
|
43
|
+
|
|
44
|
+
type TypedLocationAsRelativeRaw<T extends TypedRouteList> = {
|
|
45
|
+
name?: T;
|
|
46
|
+
} & ([TypedRouteParams[T]] extends [never]
|
|
47
|
+
? {}
|
|
48
|
+
: HasOneRequiredParameter<T> extends false
|
|
49
|
+
? { params?: TypedRouteParams[T] }
|
|
50
|
+
: { params: TypedRouteParams[T] });
|
|
51
|
+
|
|
52
|
+
type ResolvedTypedLocationAsRelativeRaw<T extends TypedRouteList> = {
|
|
53
|
+
name?: T;
|
|
54
|
+
} & ([TypedRouteParams[T]] extends [never] ? {} : { params: Required<TypedRouteParams[T]> });
|
|
55
|
+
|
|
56
|
+
type TypedRouteLocationRaw = RouteQueryAndHash & TypedRouteNamedMapper & RouteLocationOptions;
|
|
57
|
+
|
|
58
|
+
type _TypedRoute = Omit<RouteLocationNormalizedLoaded, 'name' | 'params'> &
|
|
59
|
+
ResolvedTypedRouteNamedMapper;
|
|
60
|
+
type _TypedNamedRoute<T extends TypedRouteList> = Omit<
|
|
61
|
+
RouteLocationNormalizedLoaded,
|
|
62
|
+
'name' | 'params'
|
|
63
|
+
> &
|
|
64
|
+
ResolvedTypedLocationAsRelativeRaw<T>;
|
|
65
|
+
|
|
66
|
+
/** Augmented Router interface */
|
|
67
|
+
interface _TypedRouter
|
|
68
|
+
extends Omit<Router, 'removeRoute' | 'hasRoute' | 'resolve' | 'push' | 'replace'> {
|
|
69
|
+
/**
|
|
70
|
+
* Remove an existing route by its name.
|
|
71
|
+
*
|
|
72
|
+
* @param name - Name of the route to remove
|
|
73
|
+
*/
|
|
74
|
+
removeRoute(name: TypedRouteList): void;
|
|
75
|
+
/**
|
|
76
|
+
* Checks if a route with a given name exists
|
|
77
|
+
*
|
|
78
|
+
* @param name - Name of the route to check
|
|
79
|
+
*/
|
|
80
|
+
hasRoute(name: TypedRouteList): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Returns the {@link RouteLocation | normalized version} of a
|
|
83
|
+
* {@link RouteLocationRaw | route location}. Also includes an \`href\` property
|
|
84
|
+
* that includes any existing \`base\`. By default the \`currentLocation\` used is
|
|
85
|
+
* \`route.currentRoute\` and should only be overriden in advanced use cases.
|
|
86
|
+
*
|
|
87
|
+
* @param to - Raw route location to resolve
|
|
88
|
+
* @param currentLocation - Optional current location to resolve against
|
|
89
|
+
*/
|
|
90
|
+
resolve(
|
|
91
|
+
to: TypedRouteLocationRaw,
|
|
92
|
+
currentLocation?: RouteLocationNormalizedLoaded
|
|
93
|
+
): RouteLocation & {
|
|
94
|
+
href: string;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Programmatically navigate to a new URL by pushing an entry in the history
|
|
98
|
+
* stack.
|
|
99
|
+
*
|
|
100
|
+
* @param to - Route location to navigate to
|
|
101
|
+
*/
|
|
102
|
+
push(to: TypedRouteLocationRaw): Promise<NavigationFailure | void | undefined>;
|
|
103
|
+
/**
|
|
104
|
+
* Programmatically navigate to a new URL by replacing the current entry in
|
|
105
|
+
* the history stack.
|
|
106
|
+
*
|
|
107
|
+
* @param to - Route location to navigate to
|
|
108
|
+
*/
|
|
109
|
+
replace(to: TypedRouteLocationRaw): Promise<NavigationFailure | void | undefined>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface TypedRouter extends _TypedRouter {}
|
|
113
|
+
export type TypedRoute = _TypedRoute;
|
|
114
|
+
export type TypedNamedRoute<T extends TypedRouteList> = _TypedNamedRoute<T>;
|
|
115
|
+
|
|
116
|
+
declare global {
|
|
117
|
+
export interface TypedRouter extends _TypedRouter {}
|
|
118
|
+
export type TypedRoute = _TypedRoute;
|
|
119
|
+
export type TypedNamedRoute<T extends TypedRouteList> = _TypedNamedRoute<T>;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
type TypedNuxtLinkProps = Omit<NuxtLinkProps, 'to'> & {
|
|
123
|
+
to: Omit<Exclude<RouteLocationRaw, string>, 'name'> & TypedRouteNamedMapper;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
type _NuxtLink = DefineComponent<
|
|
127
|
+
TypedNuxtLinkProps,
|
|
128
|
+
{},
|
|
129
|
+
{},
|
|
130
|
+
import('vue').ComputedOptions,
|
|
131
|
+
import('vue').MethodOptions,
|
|
132
|
+
import('vue').ComponentOptionsMixin,
|
|
133
|
+
import('vue').ComponentOptionsMixin,
|
|
134
|
+
{},
|
|
135
|
+
string,
|
|
136
|
+
import('vue').VNodeProps &
|
|
137
|
+
import('vue').AllowedComponentProps &
|
|
138
|
+
import('vue').ComponentCustomProps,
|
|
139
|
+
Readonly<TypedNuxtLinkProps>,
|
|
140
|
+
{}
|
|
141
|
+
>;
|
|
142
|
+
|
|
143
|
+
declare module '@vue/runtime-core' {
|
|
144
|
+
export interface GlobalComponents {
|
|
145
|
+
NuxtLink: _NuxtLink;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
`;
|
|
149
|
+
|
|
150
|
+
const watermarkTemplate = `
|
|
151
|
+
// @ts-nocheck
|
|
152
|
+
// eslint-disable
|
|
153
|
+
/**
|
|
154
|
+
* ---------------------------------------------------
|
|
155
|
+
* \u{1F697}\u{1F6A6} Generated by nuxt-typed-router. Do not modify !
|
|
156
|
+
* ---------------------------------------------------
|
|
157
|
+
* */
|
|
158
|
+
|
|
159
|
+
`;
|
|
160
|
+
|
|
161
|
+
function createDeclarationRoutesFile() {
|
|
162
|
+
return `
|
|
163
|
+
${watermarkTemplate}
|
|
164
|
+
|
|
165
|
+
${staticTypesImports}
|
|
166
|
+
|
|
167
|
+
${staticTypeUtils}
|
|
168
|
+
`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function createRuntimeIndexFile() {
|
|
172
|
+
return `
|
|
173
|
+
${watermarkTemplate}
|
|
174
|
+
export * from './__routes';
|
|
175
|
+
export * from './__useTypedRouter';
|
|
176
|
+
export * from './__useTypedRoute';
|
|
177
|
+
`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function createRuntimePluginFile(routesDeclTemplate) {
|
|
181
|
+
return `
|
|
182
|
+
${watermarkTemplate}
|
|
183
|
+
import { defineNuxtPlugin } from '#app';
|
|
184
|
+
|
|
185
|
+
export default defineNuxtPlugin(() => {
|
|
186
|
+
const router = useRouter();
|
|
187
|
+
const route = useRoute();
|
|
188
|
+
const routesNames = ${routesDeclTemplate};
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
provide: {
|
|
192
|
+
typedRouter: router as TypedRouter,
|
|
193
|
+
typedRoute: route as TypedRoute,
|
|
194
|
+
routesNames,
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
});
|
|
198
|
+
`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function createTypedRouteListExport(routesList) {
|
|
202
|
+
return `export type TypedRouteList = ${routesList.map((m) => `'${m}'`).join("|\n")}`;
|
|
203
|
+
}
|
|
204
|
+
function createTypedRouteParamsExport(routesParams) {
|
|
205
|
+
return `export type TypedRouteParams = {
|
|
206
|
+
${routesParams.map(
|
|
207
|
+
({ name, params }) => `"${name}": ${params.length ? `{
|
|
208
|
+
${params.map(({ key, required, type }) => `"${key}"${required ? "" : "?"}: ${type}`).join(",\n")}
|
|
209
|
+
}` : "never"}`
|
|
210
|
+
).join(",\n")}
|
|
211
|
+
}`;
|
|
212
|
+
}
|
|
213
|
+
function createTypedRouteNamedMapperExport(routesParams) {
|
|
214
|
+
return `export type TypedRouteNamedMapper =
|
|
215
|
+
${routesParams.map(
|
|
216
|
+
({ name, params }) => `{name: "${name}" ${params.length ? `, params${params.some((s) => s.required) ? "" : "?"}: {
|
|
217
|
+
${params.map(({ key, required, type }) => `"${key}"${required ? "" : "?"}: ${type}`).join(",\n")}
|
|
218
|
+
}` : ""}}`
|
|
219
|
+
).join("|\n")}
|
|
220
|
+
`;
|
|
221
|
+
}
|
|
222
|
+
function createResolvedTypedRouteNamedMapperExport(routesParams) {
|
|
223
|
+
return `export type ResolvedTypedRouteNamedMapper =
|
|
224
|
+
{
|
|
225
|
+
name: TypedRouteList;
|
|
226
|
+
params: unknown;
|
|
227
|
+
} & (
|
|
228
|
+
${routesParams.map(
|
|
229
|
+
({ name, params }) => `{name: "${name}" ${params.length ? `, params: {
|
|
230
|
+
${params.map(({ key, type }) => `"${key}": ${type}`).join(",\n")}
|
|
231
|
+
}` : ""}}`
|
|
232
|
+
).join("|\n")}
|
|
233
|
+
)
|
|
234
|
+
`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function createRuntimeRoutesFile({
|
|
238
|
+
routesList,
|
|
239
|
+
routesObjectTemplate,
|
|
240
|
+
routesDeclTemplate,
|
|
241
|
+
routesParams
|
|
242
|
+
}) {
|
|
243
|
+
return `
|
|
244
|
+
${watermarkTemplate}
|
|
245
|
+
|
|
246
|
+
export const routesNames = ${routesObjectTemplate};
|
|
247
|
+
|
|
248
|
+
${createTypedRouteListExport(routesList)}
|
|
249
|
+
|
|
250
|
+
export type RouteListDecl = ${routesDeclTemplate};
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Routes params are only required for the exact targeted route name,
|
|
254
|
+
* vue-router behaviour allow to navigate between children routes without the need to provide all the params every time.
|
|
255
|
+
* So we can't enforce params when navigating between routes, only a \`[xxx].vue\` page will have required params in the type definition
|
|
256
|
+
*
|
|
257
|
+
*
|
|
258
|
+
* */
|
|
259
|
+
|
|
260
|
+
${createTypedRouteParamsExport(routesParams)}
|
|
261
|
+
|
|
262
|
+
${createTypedRouteNamedMapperExport(routesParams)}
|
|
263
|
+
|
|
264
|
+
${createResolvedTypedRouteNamedMapperExport(routesParams)}
|
|
265
|
+
`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function createUseTypedRouteFile(routesDeclTemplate) {
|
|
269
|
+
return `
|
|
270
|
+
${watermarkTemplate}
|
|
271
|
+
import { useRoute } from '#app';
|
|
272
|
+
import { TypedRouteList } from './__routes';
|
|
273
|
+
|
|
274
|
+
/** Acts the same as \`useRoute\`, but typed.
|
|
275
|
+
*
|
|
276
|
+
* @exemple
|
|
277
|
+
*
|
|
278
|
+
* \`\`\`ts
|
|
279
|
+
* const route = useTypedRoute();
|
|
280
|
+
* \`\`\`
|
|
281
|
+
*
|
|
282
|
+
* \`\`\`ts
|
|
283
|
+
* const route = useTypedRoute('my-route-with-param-id');
|
|
284
|
+
* route.params.id // autocompletes!
|
|
285
|
+
* \`\`\`
|
|
286
|
+
*
|
|
287
|
+
* \`\`\`ts
|
|
288
|
+
* const route = useTypedRoute();
|
|
289
|
+
* if (route.name === 'my-route-with-param-id') {
|
|
290
|
+
* route.params.id // autocompletes!
|
|
291
|
+
* }
|
|
292
|
+
* \`\`\`
|
|
293
|
+
*/
|
|
294
|
+
export function useTypedRoute<T extends TypedRouteList = never>(
|
|
295
|
+
name?: T
|
|
296
|
+
): [T] extends [never] ? TypedRoute : TypedNamedRoute<T> {
|
|
297
|
+
const route = useRoute();
|
|
298
|
+
|
|
299
|
+
return route as any;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function createRuntimeUseTypedRouterFile(routesDeclTemplate) {
|
|
306
|
+
return `
|
|
307
|
+
${watermarkTemplate}
|
|
308
|
+
import { useRouter } from '#app';
|
|
309
|
+
import { TypedRouter } from './typed-router';
|
|
310
|
+
import { RouteListDecl } from './__routes';
|
|
311
|
+
|
|
312
|
+
/** Returns instances of $typedRouter and $routesList fully typed to use in your components or your Vuex/Pinia store
|
|
313
|
+
*
|
|
314
|
+
* @exemple
|
|
315
|
+
*
|
|
316
|
+
* \`\`\`ts
|
|
317
|
+
* const { router, routes } = useTypedRouter();
|
|
318
|
+
* \`\`\`
|
|
319
|
+
*/
|
|
320
|
+
export function useTypedRouter(): TypedRouter {
|
|
321
|
+
const router = useRouter();
|
|
322
|
+
|
|
323
|
+
return router;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
`;
|
|
327
|
+
}
|
|
328
|
+
|
|
11
329
|
const { resolveConfig, format } = prettier;
|
|
12
330
|
const defaultPrettierOptions = {
|
|
13
331
|
printWidth: 100,
|
|
@@ -36,8 +354,13 @@ async function formatOutputWithPrettier(template) {
|
|
|
36
354
|
}
|
|
37
355
|
|
|
38
356
|
dirname(fileURLToPath(import.meta.url));
|
|
39
|
-
async function
|
|
357
|
+
async function processPathAndWriteFile({
|
|
358
|
+
content,
|
|
359
|
+
fileName,
|
|
360
|
+
srcDir
|
|
361
|
+
}) {
|
|
40
362
|
try {
|
|
363
|
+
const outDir = `.nuxt/typed-router`;
|
|
41
364
|
const processedOutDir = resolve(srcDir, outDir);
|
|
42
365
|
const outputFile = resolve(process.cwd(), `${processedOutDir}/${fileName}`);
|
|
43
366
|
const formatedContent = await formatOutputWithPrettier(content);
|
|
@@ -63,6 +386,87 @@ async function writeFile(path, content) {
|
|
|
63
386
|
}
|
|
64
387
|
}
|
|
65
388
|
|
|
389
|
+
function handlePluginFileSave({
|
|
390
|
+
nuxt,
|
|
391
|
+
srcDir,
|
|
392
|
+
routesDeclTemplate
|
|
393
|
+
}) {
|
|
394
|
+
const pluginName = "__typed-router.ts";
|
|
395
|
+
nuxt.hook("build:done", async () => {
|
|
396
|
+
const pluginFolder = `${srcDir}/plugins`;
|
|
397
|
+
await processPathAndWriteFile({
|
|
398
|
+
outDir: pluginFolder,
|
|
399
|
+
srcDir,
|
|
400
|
+
fileName: pluginName,
|
|
401
|
+
content: createRuntimePluginFile(routesDeclTemplate)
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function saveGeneratedFiles({
|
|
407
|
+
srcDir,
|
|
408
|
+
outputData: { routesDeclTemplate, routesList, routesObjectTemplate, routesParams }
|
|
409
|
+
}) {
|
|
410
|
+
const filesMap = [
|
|
411
|
+
{
|
|
412
|
+
fileName: "__useTypedRouter.ts",
|
|
413
|
+
content: createRuntimeUseTypedRouterFile()
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
fileName: "__useTypedRoute.ts",
|
|
417
|
+
content: createUseTypedRouteFile()
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
fileName: `__routes.ts`,
|
|
421
|
+
content: createRuntimeRoutesFile({
|
|
422
|
+
routesList,
|
|
423
|
+
routesObjectTemplate,
|
|
424
|
+
routesDeclTemplate,
|
|
425
|
+
routesParams
|
|
426
|
+
})
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
fileName: `typed-router.d.ts`,
|
|
430
|
+
content: createDeclarationRoutesFile()
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
fileName: "index.ts",
|
|
434
|
+
content: createRuntimeIndexFile()
|
|
435
|
+
}
|
|
436
|
+
];
|
|
437
|
+
await Promise.all(
|
|
438
|
+
filesMap.map(({ content, fileName }) => processPathAndWriteFile({ srcDir, content, fileName }))
|
|
439
|
+
);
|
|
440
|
+
console.log(logSymbols.success, `[typed-router] Routes definitions generated`);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function isItemLast(array, index) {
|
|
444
|
+
return array ? index === array.length - 1 : false;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const routeParamExtractRegxp = /:(\w+)/;
|
|
448
|
+
function extractRouteParamsFromPath(path, isIndexFileForRouting, previousParams) {
|
|
449
|
+
const params = path.match(routeParamExtractRegxp) ?? [];
|
|
450
|
+
params?.shift();
|
|
451
|
+
let allMergedParams = params.map(
|
|
452
|
+
(m) => ({
|
|
453
|
+
key: m,
|
|
454
|
+
type: "string | number",
|
|
455
|
+
required: true
|
|
456
|
+
})
|
|
457
|
+
);
|
|
458
|
+
if (previousParams?.length) {
|
|
459
|
+
allMergedParams = previousParams.map((m) => ({ ...m, required: false })).concat(allMergedParams);
|
|
460
|
+
}
|
|
461
|
+
if (!params.length && isIndexFileForRouting) {
|
|
462
|
+
const lastItem = allMergedParams[allMergedParams.length - 1];
|
|
463
|
+
if (lastItem) {
|
|
464
|
+
lastItem.required = true;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return allMergedParams;
|
|
468
|
+
}
|
|
469
|
+
|
|
66
470
|
function extractMatchingSiblings(mainRoute, siblingRoutes) {
|
|
67
471
|
return siblingRoutes?.filter((s) => {
|
|
68
472
|
const chunkName = extractChunkMain(mainRoute.file);
|
|
@@ -92,61 +496,6 @@ function extractChunkMain(chunkName) {
|
|
|
92
496
|
return chunkArray?.join("/");
|
|
93
497
|
}
|
|
94
498
|
|
|
95
|
-
const routeParamExtractRegxp = /:(\w+)/;
|
|
96
|
-
function extractRouteParamsFromPath(path, previousParams) {
|
|
97
|
-
const params = path.match(routeParamExtractRegxp) ?? [];
|
|
98
|
-
params?.shift();
|
|
99
|
-
let allMergedParams = params.map(
|
|
100
|
-
(m) => ({
|
|
101
|
-
key: m,
|
|
102
|
-
type: "string | number",
|
|
103
|
-
required: true
|
|
104
|
-
})
|
|
105
|
-
);
|
|
106
|
-
if (previousParams?.length) {
|
|
107
|
-
allMergedParams = allMergedParams.concat(
|
|
108
|
-
previousParams.map((m) => ({ ...m, required: false }))
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
return allMergedParams;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function isItemLast(array, index) {
|
|
115
|
-
return index === array.length - 1;
|
|
116
|
-
}
|
|
117
|
-
function constructRouteMap(routesConfig) {
|
|
118
|
-
try {
|
|
119
|
-
let routesObjectTemplate = "{";
|
|
120
|
-
let routesDeclTemplate = "{";
|
|
121
|
-
let routesList = [];
|
|
122
|
-
let routesParams = [];
|
|
123
|
-
const output = { routesObjectTemplate, routesDeclTemplate, routesList, routesParams };
|
|
124
|
-
startGeneratorProcedure({
|
|
125
|
-
output,
|
|
126
|
-
routesConfig
|
|
127
|
-
});
|
|
128
|
-
return output;
|
|
129
|
-
} catch (e) {
|
|
130
|
-
throw new Error("Generation failed");
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
function startGeneratorProcedure({
|
|
134
|
-
output,
|
|
135
|
-
routesConfig
|
|
136
|
-
}) {
|
|
137
|
-
routesConfig.forEach((route, index) => {
|
|
138
|
-
const rootSiblingsRoutes = routesConfig.filter((rt) => rt.chunkName !== route.chunkName);
|
|
139
|
-
walkThoughRoutes({
|
|
140
|
-
route,
|
|
141
|
-
level: 0,
|
|
142
|
-
output,
|
|
143
|
-
siblings: rootSiblingsRoutes,
|
|
144
|
-
isLast: isItemLast(routesConfig, index)
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
output.routesObjectTemplate += "}";
|
|
148
|
-
output.routesDeclTemplate += "}";
|
|
149
|
-
}
|
|
150
499
|
function walkThoughRoutes({
|
|
151
500
|
route,
|
|
152
501
|
level,
|
|
@@ -168,7 +517,7 @@ function walkThoughRoutes({
|
|
|
168
517
|
const nameKey = camelCase(parentPath || "index");
|
|
169
518
|
output.routesObjectTemplate += `${nameKey}:{`;
|
|
170
519
|
output.routesDeclTemplate += `"${nameKey}":{`;
|
|
171
|
-
const allRouteParams = extractRouteParamsFromPath(route.path, previousParams);
|
|
520
|
+
const allRouteParams = extractRouteParamsFromPath(route.path, false, previousParams);
|
|
172
521
|
childrenChunks?.map(
|
|
173
522
|
(routeConfig, index) => walkThoughRoutes({
|
|
174
523
|
route: routeConfig,
|
|
@@ -193,7 +542,12 @@ function walkThoughRoutes({
|
|
|
193
542
|
output.routesObjectTemplate += `'${keyName}': '${route.name}' as const,`;
|
|
194
543
|
output.routesDeclTemplate += `"${keyName}": "${route.name}"${isLast ? "" : ","}`;
|
|
195
544
|
output.routesList.push(route.name);
|
|
196
|
-
const
|
|
545
|
+
const isIndexFileForRouting = route.path === "";
|
|
546
|
+
const allRouteParams = extractRouteParamsFromPath(
|
|
547
|
+
route.path,
|
|
548
|
+
isIndexFileForRouting,
|
|
549
|
+
previousParams
|
|
550
|
+
);
|
|
197
551
|
output.routesParams.push({
|
|
198
552
|
name: route.name,
|
|
199
553
|
params: allRouteParams
|
|
@@ -201,229 +555,49 @@ function walkThoughRoutes({
|
|
|
201
555
|
}
|
|
202
556
|
}
|
|
203
557
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
} from 'vue-router';
|
|
219
|
-
import type { TypedRouteList } from './__routes'
|
|
220
|
-
`;
|
|
221
|
-
const staticDeclarations = `
|
|
222
|
-
type TypedRouteParamsStructure = {
|
|
223
|
-
[K in TypedRouteList]: Record<string, string | number> | never;
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
type TypedLocationAsRelativeRaw<T extends TypedRouteList> = {
|
|
227
|
-
name?: T;
|
|
228
|
-
params?: TypedRouteParams[T];
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
type TypedRouteLocationRaw<T extends TypedRouteList> = RouteQueryAndHash &
|
|
232
|
-
TypedLocationAsRelativeRaw<T> &
|
|
233
|
-
RouteLocationOptions;
|
|
234
|
-
|
|
235
|
-
interface _TypedRouter {
|
|
236
|
-
/**
|
|
237
|
-
* Remove an existing route by its name.
|
|
238
|
-
*
|
|
239
|
-
* @param name - Name of the route to remove
|
|
240
|
-
*/
|
|
241
|
-
removeRoute(name: TypedRouteList): void;
|
|
242
|
-
/**
|
|
243
|
-
* Checks if a route with a given name exists
|
|
244
|
-
*
|
|
245
|
-
* @param name - Name of the route to check
|
|
246
|
-
*/
|
|
247
|
-
hasRoute(name: TypedRouteList): boolean;
|
|
248
|
-
/**
|
|
249
|
-
* Returns the {@link RouteLocation | normalized version} of a
|
|
250
|
-
* {@link RouteLocationRaw | route location}. Also includes an \`href\` property
|
|
251
|
-
* that includes any existing \`base\`. By default the \`currentLocation\` used is
|
|
252
|
-
* \`route.currentRoute\` and should only be overriden in advanced use cases.
|
|
253
|
-
*
|
|
254
|
-
* @param to - Raw route location to resolve
|
|
255
|
-
* @param currentLocation - Optional current location to resolve against
|
|
256
|
-
*/
|
|
257
|
-
resolve<T extends TypedRouteList>(
|
|
258
|
-
to: TypedRouteLocationRaw<T>,
|
|
259
|
-
currentLocation?: RouteLocationNormalizedLoaded
|
|
260
|
-
): RouteLocation & {
|
|
261
|
-
href: string;
|
|
262
|
-
};
|
|
263
|
-
/**
|
|
264
|
-
* Programmatically navigate to a new URL by pushing an entry in the history
|
|
265
|
-
* stack.
|
|
266
|
-
*
|
|
267
|
-
* @param to - Route location to navigate to
|
|
268
|
-
*/
|
|
269
|
-
push<T extends TypedRouteList>(
|
|
270
|
-
to: TypedRouteLocationRaw<T>
|
|
271
|
-
): Promise<NavigationFailure | void | undefined>;
|
|
272
|
-
/**
|
|
273
|
-
* Programmatically navigate to a new URL by replacing the current entry in
|
|
274
|
-
* the history stack.
|
|
275
|
-
*
|
|
276
|
-
* @param to - Route location to navigate to
|
|
277
|
-
*/
|
|
278
|
-
replace<T extends TypedRouteList>(
|
|
279
|
-
to: TypedRouteLocationRaw<T>
|
|
280
|
-
): Promise<NavigationFailure | void | undefined>;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export interface TypedRouter extends _TypedRouter {}
|
|
284
|
-
declare global {
|
|
285
|
-
export interface TypedRouter extends _TypedRouter {}
|
|
558
|
+
function constructRouteMap(routesConfig) {
|
|
559
|
+
try {
|
|
560
|
+
let routesObjectTemplate = "{";
|
|
561
|
+
let routesDeclTemplate = "{";
|
|
562
|
+
let routesList = [];
|
|
563
|
+
let routesParams = [];
|
|
564
|
+
const output = { routesObjectTemplate, routesDeclTemplate, routesList, routesParams };
|
|
565
|
+
startGenerator({
|
|
566
|
+
output,
|
|
567
|
+
routesConfig
|
|
568
|
+
});
|
|
569
|
+
return output;
|
|
570
|
+
} catch (e) {
|
|
571
|
+
throw new Error("Generation failed");
|
|
286
572
|
}
|
|
287
|
-
`;
|
|
288
|
-
|
|
289
|
-
function createRuntimePluginFile(routesDeclTemplate) {
|
|
290
|
-
return `
|
|
291
|
-
${signatureTemplate}
|
|
292
|
-
import { defineNuxtPlugin } from '#app';
|
|
293
|
-
|
|
294
|
-
export default defineNuxtPlugin(() => {
|
|
295
|
-
const router = useRouter();
|
|
296
|
-
const routesList = ${routesDeclTemplate};
|
|
297
|
-
|
|
298
|
-
return {
|
|
299
|
-
provide: {
|
|
300
|
-
typedRouter: router as TypedRouter,
|
|
301
|
-
routesList,
|
|
302
|
-
},
|
|
303
|
-
};
|
|
304
|
-
});
|
|
305
|
-
`;
|
|
306
|
-
}
|
|
307
|
-
function createRuntimeHookFile(routesDeclTemplate) {
|
|
308
|
-
return `
|
|
309
|
-
${signatureTemplate}
|
|
310
|
-
import { useNuxtApp } from '#app';
|
|
311
|
-
import { TypedRouter, RouteListDecl } from './typed-router';
|
|
312
|
-
|
|
313
|
-
/** Returns instances of $typedRouter and $routesList fully typed to use in your components or your Vuex/Pinia store
|
|
314
|
-
*
|
|
315
|
-
* @exemple
|
|
316
|
-
*
|
|
317
|
-
* \`\`\`ts
|
|
318
|
-
* const { router, routes } = useTypedRouter();
|
|
319
|
-
* \`\`\`
|
|
320
|
-
*/
|
|
321
|
-
export const useTypedRouter = (): {
|
|
322
|
-
/** Export of $router with type check */
|
|
323
|
-
router: TypedRouter,
|
|
324
|
-
/** Contains a typed dictionnary of all your route names (for syntax sugar) */
|
|
325
|
-
routes: RouteListDecl
|
|
326
|
-
} => {
|
|
327
|
-
const { $router } = useNuxtApp();
|
|
328
|
-
|
|
329
|
-
const routesList = ${routesDeclTemplate};
|
|
330
|
-
|
|
331
|
-
return {
|
|
332
|
-
router: $router,
|
|
333
|
-
routes: routesList,
|
|
334
|
-
} as any;
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
`;
|
|
338
573
|
}
|
|
339
|
-
function
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
${signatureTemplate}
|
|
353
|
-
|
|
354
|
-
export const ${routesObjectName} = ${routesObjectTemplate};
|
|
355
|
-
|
|
356
|
-
${createTypedRouteListExport(routesList)}
|
|
357
|
-
`;
|
|
358
|
-
}
|
|
359
|
-
function createDeclarationRoutesFile({
|
|
360
|
-
routesDeclTemplate,
|
|
361
|
-
routesList,
|
|
362
|
-
routesParams
|
|
363
|
-
}) {
|
|
364
|
-
return `
|
|
365
|
-
${signatureTemplate}
|
|
366
|
-
${staticDeclImports}
|
|
367
|
-
|
|
368
|
-
export type RouteListDecl = ${routesDeclTemplate};
|
|
369
|
-
|
|
370
|
-
${createTypedRouteParamsExport(routesParams)}
|
|
371
|
-
|
|
372
|
-
${staticDeclarations}
|
|
373
|
-
`;
|
|
374
|
-
}
|
|
375
|
-
function createTypedRouteListExport(routesList) {
|
|
376
|
-
return `export type TypedRouteList = ${routesList.map((m) => `'${m}'`).join("|\n")}`;
|
|
377
|
-
}
|
|
378
|
-
function createTypedRouteParamsExport(routesParams) {
|
|
379
|
-
return `export type TypedRouteParams = {
|
|
380
|
-
${routesParams.map(
|
|
381
|
-
({ name, params }) => `"${name}": ${params.length ? `{
|
|
382
|
-
${params.map(({ key, required, type }) => `"${key}"${required ? "" : "?"}: ${type}`).join(",\n")}
|
|
383
|
-
}` : "never"}`
|
|
384
|
-
).join(",\n")}
|
|
385
|
-
}`;
|
|
574
|
+
function startGenerator({ output, routesConfig }) {
|
|
575
|
+
routesConfig.forEach((route, index) => {
|
|
576
|
+
const rootSiblingsRoutes = routesConfig.filter((rt) => rt.chunkName !== route.chunkName);
|
|
577
|
+
walkThoughRoutes({
|
|
578
|
+
route,
|
|
579
|
+
level: 0,
|
|
580
|
+
output,
|
|
581
|
+
siblings: rootSiblingsRoutes,
|
|
582
|
+
isLast: isItemLast(routesConfig, index)
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
output.routesObjectTemplate += "}";
|
|
586
|
+
output.routesDeclTemplate += "}";
|
|
386
587
|
}
|
|
387
588
|
|
|
388
|
-
function
|
|
589
|
+
function createTypedRouter({ srcDir, plugin, nuxt }) {
|
|
389
590
|
try {
|
|
390
591
|
extendPages(async (routes) => {
|
|
391
592
|
if (routes.length) {
|
|
392
|
-
const
|
|
593
|
+
const outputData = constructRouteMap(routes);
|
|
393
594
|
if (plugin) {
|
|
394
|
-
|
|
395
|
-
nuxt.hook("build:done", async () => {
|
|
396
|
-
const pluginFolder = `${srcDir}/plugins`;
|
|
397
|
-
await saveRouteFiles(
|
|
398
|
-
pluginFolder,
|
|
399
|
-
srcDir,
|
|
400
|
-
pluginName,
|
|
401
|
-
createRuntimePluginFile(routesDeclTemplate)
|
|
402
|
-
);
|
|
403
|
-
});
|
|
595
|
+
handlePluginFileSave({ nuxt, routesDeclTemplate: outputData.routesDeclTemplate, srcDir });
|
|
404
596
|
}
|
|
405
|
-
await
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
"__useTypedRouter.ts",
|
|
410
|
-
createRuntimeHookFile(routesDeclTemplate)
|
|
411
|
-
),
|
|
412
|
-
saveRouteFiles(
|
|
413
|
-
outDir,
|
|
414
|
-
srcDir,
|
|
415
|
-
`__routes.ts`,
|
|
416
|
-
createRuntimeRoutesFile({ routesList, routesObjectTemplate, routesObjectName })
|
|
417
|
-
),
|
|
418
|
-
saveRouteFiles(
|
|
419
|
-
outDir,
|
|
420
|
-
srcDir,
|
|
421
|
-
`typed-router.d.ts`,
|
|
422
|
-
createDeclarationRoutesFile({ routesDeclTemplate, routesList, routesParams })
|
|
423
|
-
),
|
|
424
|
-
saveRouteFiles(outDir, srcDir, "index.ts", createRuntimeIndexFile())
|
|
425
|
-
]);
|
|
426
|
-
console.log(logSymbols.success, `[typed-router] Routes definitions generated`);
|
|
597
|
+
await saveGeneratedFiles({
|
|
598
|
+
srcDir,
|
|
599
|
+
outputData
|
|
600
|
+
});
|
|
427
601
|
} else {
|
|
428
602
|
console.log(
|
|
429
603
|
logSymbols.warning,
|
|
@@ -447,13 +621,18 @@ const module = defineNuxtModule({
|
|
|
447
621
|
compatibility: { nuxt: "^3.0.0-rc.1", bridge: false }
|
|
448
622
|
},
|
|
449
623
|
defaults: {
|
|
450
|
-
|
|
451
|
-
routesObjectName: "routerPagesNames"
|
|
624
|
+
plugin: false
|
|
452
625
|
},
|
|
453
626
|
setup(moduleOptions, nuxt) {
|
|
454
627
|
const srcDir = nuxt.options.srcDir;
|
|
455
|
-
const { plugin
|
|
456
|
-
nuxt.
|
|
628
|
+
const { plugin } = moduleOptions;
|
|
629
|
+
nuxt.options.alias = {
|
|
630
|
+
...nuxt.options.alias,
|
|
631
|
+
"@typed-router": fileURLToPath(
|
|
632
|
+
new URL(`${nuxt.options.rootDir}/.nuxt/typed-router`, import.meta.url)
|
|
633
|
+
)
|
|
634
|
+
};
|
|
635
|
+
nuxt.hook("pages:extend", () => createTypedRouter({ srcDir, nuxt, plugin }));
|
|
457
636
|
}
|
|
458
637
|
});
|
|
459
638
|
|
package/main.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-typed-router",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0-beta.0",
|
|
4
4
|
"description": "Provide autocompletion for pages route names generated by Nuxt router",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/module.cjs",
|
|
@@ -20,9 +20,11 @@
|
|
|
20
20
|
"dev": "nuxi dev playground",
|
|
21
21
|
"dev:build": "nuxi build playground",
|
|
22
22
|
"dev:prepare": "nuxt-module-build --stub && nuxi prepare playground",
|
|
23
|
-
"build:test": "cross-env NUXT_BUILD_TYPE=stub
|
|
24
|
-
"test": "
|
|
25
|
-
"test:watch": "
|
|
23
|
+
"build:test": "cross-env NUXT_BUILD_TYPE=stub pnpm run prepack && pnpm run dev:build",
|
|
24
|
+
"test": "pnpm run dev:prepare && pnpm run build:test && vitest run",
|
|
25
|
+
"test:watch": "pnpm run build:test && vitest",
|
|
26
|
+
"docs:dev": "cd docs && pnpm run dev",
|
|
27
|
+
"docs:buid": "(cd docs && nuxi build)"
|
|
26
28
|
},
|
|
27
29
|
"publishConfig": {
|
|
28
30
|
"access": "public"
|
|
@@ -67,11 +69,11 @@
|
|
|
67
69
|
"@types/node": "^17.0.23",
|
|
68
70
|
"@types/prettier": "^2.7.2",
|
|
69
71
|
"cross-env": "^7.0.3",
|
|
70
|
-
"eslint": "8.
|
|
71
|
-
"eslint-config-prettier": "^8.
|
|
72
|
+
"eslint": "8.31.0",
|
|
73
|
+
"eslint-config-prettier": "^8.6.0",
|
|
72
74
|
"nuxt": "3.0.0",
|
|
73
75
|
"typescript": "^4.9.4",
|
|
74
|
-
"vitest": "^0.
|
|
76
|
+
"vitest": "^0.27.0",
|
|
75
77
|
"vue-router": "^4.1.6"
|
|
76
78
|
}
|
|
77
79
|
}
|