nuxt-typed-router 2.3.4 → 3.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 CHANGED
@@ -24,9 +24,6 @@
24
24
  - Out of the box `i18n` support
25
25
  - Supports routes extended by config and modules
26
26
 
27
- > ⚠️ Since `v2.1.x`, `useTypedRouter` and `useTypedRoute` are no longer exported.
28
- The package can now override types from `useRouter`, `useRoute` and `navigateTo`
29
-
30
27
  <br/>
31
28
 
32
29
  <br/>
package/dist/module.d.ts CHANGED
@@ -1,6 +1,14 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
2
 
3
3
  interface ModuleOptions {
4
+ /**
5
+ * ⛔️ Experimental, disable it if you encounter problems
6
+ *
7
+ * Enables path autocomplete and path validity for programmatic validation
8
+ *
9
+ * @default true
10
+ */
11
+ experimentalPathCheck?: boolean;
4
12
  /**
5
13
  * Set to false if you don't want a plugin generated
6
14
  * @default false
package/dist/module.json CHANGED
@@ -5,5 +5,5 @@
5
5
  "nuxt": "^3.0.0",
6
6
  "bridge": false
7
7
  },
8
- "version": "2.3.4"
8
+ "version": "3.0.0-beta.0"
9
9
  }
package/dist/module.mjs CHANGED
@@ -2,6 +2,7 @@ import { addPluginTemplate, extendPages, defineNuxtModule, createResolver } from
2
2
  import chalk from 'chalk';
3
3
  import logSymbols from 'log-symbols';
4
4
  import { defu } from 'defu';
5
+ import { customAlphabet, nanoid as nanoid$1 } from 'nanoid';
5
6
  import prettier from 'prettier';
6
7
  import fs from 'fs';
7
8
  import { fileURLToPath } from 'url';
@@ -13,6 +14,7 @@ class ModuleOptionsStore {
13
14
  constructor() {
14
15
  this.plugin = false;
15
16
  this.strict = false;
17
+ this.experimentalPathCheck = true;
16
18
  this.autoImport = false;
17
19
  this.rootDir = "";
18
20
  this.i18n = false;
@@ -31,6 +33,8 @@ class ModuleOptionsStore {
31
33
  this.i18n = options.i18n;
32
34
  if (options.i18nLocales != null)
33
35
  this.i18nLocales = options.i18nLocales;
36
+ if (options.experimentalPathCheck != null)
37
+ this.experimentalPathCheck = options.experimentalPathCheck;
34
38
  }
35
39
  getResolvedStrictOptions() {
36
40
  let resolved;
@@ -158,24 +162,111 @@ function createRoutesParamsRecordResolvedExport(routesParams) {
158
162
  }`;
159
163
  }
160
164
 
165
+ const nanoid = customAlphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 10);
166
+ function createRoutePathSchema(routePaths) {
167
+ return `export type RoutePathSchema =
168
+ ${routePaths.filter((f) => !!f.path).map((route) => `"${route.path}"`).join("|")}
169
+ `;
170
+ }
171
+ function createValidatePathTypes(pathElements) {
172
+ let pathConditions = pathElements.map(createTypeValidatePathCondition);
173
+ const conditionsList = pathConditions.map((m) => m.condition);
174
+ return `
175
+ ${pathConditions.length ? conditionsList.join("\n\n") : ""}
176
+
177
+ export type ValidatePath<T extends string> = T extends string
178
+ ? T extends '/'
179
+ ? T
180
+ : ${pathConditions.length ? pathConditions.map((t) => `${t.typeName}<T> extends true ? T`).join(": ") : "never"}
181
+ : string extends T
182
+ ? T
183
+ : \`Error: \${${pathConditions.map((t) => `${t.typeName}<T>`).join("|")}}\` : 'Type should be a string';
184
+
185
+
186
+ export type RouteNameFromPath<T extends string> = T extends string
187
+ ? T extends '/'
188
+ ? "index"
189
+ : ${pathConditions.length ? pathConditions.map((t) => `${t.typeName}<T> extends true ? "${t.routeName}"`).join(": ") : "never"}
190
+ : never : never;
191
+
192
+ `;
193
+ }
194
+ function createTypeValidatePathCondition(elements) {
195
+ const typeName = `Validate${nanoid(7)}`;
196
+ const params = /* @__PURE__ */ new Map();
197
+ const routeName = elements.flat()[0].routeName;
198
+ const hasOnlyNames = elements.flat().every((elem) => elem.type === "name");
199
+ const condition = `type ${typeName}<T> = T extends \`/${elements.map((elementArray, index) => {
200
+ return elementArray.map((elem) => {
201
+ const isLast = index === elements.flat().length - 1;
202
+ if (elem.type === "name" && isLast && !hasOnlyNames) {
203
+ const id = nanoid(6);
204
+ params.set(elem.id, id);
205
+ return `${elem.content}\${infer ${id}}`;
206
+ } else if (elem.type === "name") {
207
+ return elem.content;
208
+ } else if (elem.type === "param" || elem.type === "optionalParam") {
209
+ const id = nanoid(6);
210
+ params.set(elem.id, id);
211
+ return `\${infer ${id}}`;
212
+ } else if (elem.type === "catchAll") {
213
+ return `\${string}`;
214
+ }
215
+ }).join("");
216
+ }).join("/")}\`
217
+ ? ${hasOnlyNames ? `true :` : elements.flat().map((elem, index) => {
218
+ let output = "";
219
+ const isLast = index === elements.flat().length - 1;
220
+ const isName = elem.type === "name";
221
+ const isOptional = elem.type === "optionalParam";
222
+ const isParam = elem.type === "param";
223
+ elem.type === "catchAll";
224
+ if (isName && isLast) {
225
+ output = `ValidEndOfPath<${params.get(elem.id)}> extends false ? "End of path '${elem.fullPath}' is invalid" : true :`;
226
+ } else if (isParam && isLast) {
227
+ output = `ValidParam<${params.get(elem.id)}> extends false ? "Parameter {${elem.content}} of path '${elem.fullPath}' is invalid" : true :`;
228
+ } else if (isParam) {
229
+ output = `ValidStringPath<${params.get(elem.id)}> extends false ? "Parameter {${elem.content}} of path '${elem.fullPath}' is required" : `;
230
+ } else if (isOptional && isLast) {
231
+ output = `ValidParam<${params.get(elem.id)}, false> extends false ? "Parameter {${elem.content}} of path '${elem.fullPath}' is invalid" : true :`;
232
+ } else if (isLast) {
233
+ output += "true :";
234
+ }
235
+ return output;
236
+ }).join("")} false ;`;
237
+ return {
238
+ typeName,
239
+ condition,
240
+ routeName
241
+ };
242
+ }
243
+
161
244
  function createRoutesTypesFile({
162
245
  routesList,
163
246
  routesObjectTemplate,
164
247
  routesDeclTemplate,
165
- routesParams
248
+ routesParams,
249
+ routesPaths
166
250
  }) {
251
+ const filteredRoutesList = routesList.filter(
252
+ (routeName, index) => routesList.indexOf(routeName) === index
253
+ );
254
+ const filteredRoutesParams = routesParams.filter(
255
+ (route, index) => routesParams.findIndex((r) => route.name === r.name) === index
256
+ );
167
257
  return (
168
258
  /* typescript */
169
259
  `
170
- ${createRoutesNamesListExport(routesList)}
260
+ ${createRoutesNamesListExport(filteredRoutesList)}
261
+ export type WithoutBracket<T extends string> = T extends \`:\${string}\` ? never : T;
171
262
 
172
- ${createRoutesParamsRecordExport(routesParams)}
263
+ ${createRoutesParamsRecordExport(filteredRoutesParams)}
173
264
 
174
- ${createRoutesParamsRecordResolvedExport(routesParams)}
265
+ ${createRoutesParamsRecordResolvedExport(filteredRoutesParams)}
175
266
 
176
- ${createRoutesNamedLocationsExport(routesParams)}
267
+ ${createRoutesNamedLocationsExport(filteredRoutesParams)}
177
268
 
178
- ${createRoutesNamedLocationsResolvedExport(routesParams)}
269
+ ${createRoutesNamedLocationsResolvedExport(filteredRoutesParams)}
179
270
 
180
271
  export type RoutesNamesListRecord = ${routesDeclTemplate};
181
272
 
@@ -193,15 +284,10 @@ function returnIfTrue(condition, template, otherwise) {
193
284
  }
194
285
  return otherwise ?? "";
195
286
  }
196
- function returnIfFalse(condition, template, otherwise) {
197
- if (!condition) {
198
- return template;
199
- }
200
- return otherwise ?? "";
201
- }
202
287
 
203
288
  function createTypedRouterFile() {
204
289
  const strictOptions = moduleOptionStore.getResolvedStrictOptions();
290
+ const { i18n, experimentalPathCheck } = moduleOptionStore;
205
291
  return (
206
292
  /* typescript */
207
293
  `
@@ -220,8 +306,12 @@ function createTypedRouterFile() {
220
306
  RoutesNamedLocationsResolved,
221
307
  RoutesNamesList,
222
308
  RoutesParamsRecord,
223
- RoutesParamsRecordResolved
309
+ RoutesParamsRecordResolved,
224
310
  } from './__routes';
311
+ ${returnIfTrue(
312
+ experimentalPathCheck,
313
+ `import type {TypedPathParameter, RouteNameFromPath} from './__paths';`
314
+ )}
225
315
  import type { HasOneRequiredParameter } from './__types_utils';
226
316
 
227
317
 
@@ -231,28 +321,28 @@ function createTypedRouterFile() {
231
321
  * RouteLocationRaw with discrimanated name and params properties
232
322
  * {@link RouteLocationRaw}
233
323
  * */
234
- export type TypedRouteLocationRaw =
324
+ export type TypedRouteLocationRaw<T extends string = string> =
235
325
  | (Omit<Exclude<RouteLocationRaw, string>, 'name' | 'params'> & RoutesNamedLocations)
236
- ${returnIfFalse(strictOptions.router.strictToArgument, "| string")}
326
+ | Omit<RouteLocationPathRaw, 'path'>
237
327
  ${returnIfTrue(
238
- strictOptions.router.strictRouteLocation,
239
- `| Omit<RouteLocationPathRaw, 'path'>`,
240
- "| RouteLocationPathRaw"
328
+ experimentalPathCheck && !strictOptions.router.strictRouteLocation,
329
+ `& {path?: TypedPathParameter<T>}`
241
330
  )}
331
+ ${returnIfTrue(!experimentalPathCheck && !strictOptions.router.strictToArgument, ` | string`)}
242
332
  ;
243
333
 
244
334
 
245
335
  /**
246
336
  * Alternative version of {@link TypedRouteLocationRaw} but with a name generic
247
337
  */
248
- export type TypedRouteLocationRawFromName<T extends RoutesNamesList> =
338
+ export type TypedRouteLocationRawFromName<T extends RoutesNamesList, P extends string = string> =
249
339
  | (Omit<Exclude<RouteLocationRaw, string>, 'name' | 'params'> & TypedLocationAsRelativeRaw<T>)
250
- ${returnIfFalse(strictOptions.router.strictToArgument, "| string")}
340
+ | Omit<RouteLocationPathRaw, 'path'>
251
341
  ${returnIfTrue(
252
- strictOptions.router.strictRouteLocation,
253
- `| Omit<RouteLocationPathRaw, 'path'>`,
254
- "| RouteLocationPathRaw"
342
+ experimentalPathCheck && !strictOptions.router.strictRouteLocation,
343
+ `& {path?: TypedPathParameter<P>}`
255
344
  )}
345
+ ${returnIfTrue(!experimentalPathCheck && !strictOptions.router.strictToArgument, ` | string`)}
256
346
 
257
347
  /**
258
348
  * Generic providing inference and dynamic inclusion of \`params\` property
@@ -294,24 +384,36 @@ function createTypedRouterFile() {
294
384
  * @param to - Raw route location to resolve
295
385
  * @param currentLocation - Optional current location to resolve against
296
386
  */
297
- resolve<T extends RoutesNamesList>(
298
- to: TypedRouteLocationRawFromName<T>,
299
- currentLocation?: TypedRoute
387
+ resolve<T extends RoutesNamesList, P extends string>(
388
+ to: TypedRouteLocationRawFromName<T, P>,
389
+ currentLocation?: TypedRouteLocationRaw
300
390
  ): TypedRouteLocationFromName<T>;
391
+ ${returnIfTrue(
392
+ experimentalPathCheck && !strictOptions.router.strictToArgument,
393
+ `resolve<T extends string>(to: TypedPathParameter<T>, currentLocation?: TypedRouteLocationRaw): TypedRouteLocationFromName<RouteNameFromPath<T>>;`
394
+ )}
301
395
  /**
302
396
  * Programmatically navigate to a new URL by pushing an entry in the history
303
397
  * stack.
304
398
  *
305
399
  * @param to - Route location to navigate to
306
400
  */
307
- push(to: TypedRouteLocationRaw): Promise<NavigationFailure | void | undefined>;
401
+ push<T extends RoutesNamesList, P extends string>(to: TypedRouteLocationRawFromName<T, P>): Promise<NavigationFailure | void | undefined>;
402
+ ${returnIfTrue(
403
+ experimentalPathCheck && !strictOptions.router.strictToArgument,
404
+ `push<T extends string>(to: TypedPathParameter<T>): Promise<NavigationFailure | void | undefined>;`
405
+ )}
308
406
  /**
309
407
  * Programmatically navigate to a new URL by replacing the current entry in
310
408
  * the history stack.
311
409
  *
312
410
  * @param to - Route location to navigate to
313
411
  */
314
- replace(to: TypedRouteLocationRaw): Promise<NavigationFailure | void | undefined>;
412
+ replace<T extends RoutesNamesList, P extends string>(to: TypedRouteLocationRawFromName<T, P>): Promise<NavigationFailure | void | undefined>;
413
+ ${returnIfTrue(
414
+ experimentalPathCheck && !strictOptions.router.strictToArgument,
415
+ `replace<T extends string>(to: TypedPathParameter<T>): Promise<NavigationFailure | void | undefined>;`
416
+ )}
315
417
  }
316
418
 
317
419
 
@@ -354,21 +456,25 @@ function createTypedRouterFile() {
354
456
  }
355
457
 
356
458
  function createTypedRouterDefinitionFile() {
357
- const { plugin, autoImport, i18n } = moduleOptionStore;
358
459
  const strictOptions = moduleOptionStore.getResolvedStrictOptions();
460
+ const { plugin, autoImport, i18n, experimentalPathCheck } = moduleOptionStore;
359
461
  return (
360
462
  /* typescript */
361
463
  `
362
464
 
363
- import type { NuxtLinkProps } from '#app';
364
- import type { DefineComponent } from 'vue';
465
+ import type { NuxtLinkProps, PageMeta } from '#app';
466
+ import NuxtLink from 'nuxt/dist/app/components/nuxt-link';
365
467
  import type { RouteLocationRaw, RouteLocationPathRaw } from 'vue-router';
366
- import type { RoutesNamesList, RoutesNamedLocations, RoutesNamesListRecord } from './__routes';
367
- import type {TypedRouter, TypedRoute} from './__router';
468
+ import type { RoutesNamedLocations, RoutesNamesListRecord, RoutesNamesList } from './__routes';
469
+ import type {TypedRouter, TypedRoute, TypedRouteLocationRawFromName, TypedLocationAsRelativeRaw} from './__router';
368
470
  import { useRoute as _useRoute } from './__useTypedRoute';
369
471
  import { useRouter as _useRouter } from './__useTypedRouter';
370
472
  import { navigateTo as _navigateTo } from './__navigateTo';
371
473
  import { useLocalePath as _useLocalePath, useLocaleRoute as _useLocaleRoute} from './__i18n-router';
474
+ import {definePageMeta as _definePageMeta} from './__definePageMeta';
475
+
476
+ ${returnIfTrue(experimentalPathCheck, `import type {TypedPathParameter} from './__paths';`)}
477
+
372
478
 
373
479
  declare global {
374
480
 
@@ -379,6 +485,8 @@ function createTypedRouterDefinitionFile() {
379
485
  const useRoute: typeof _useRoute;
380
486
  const useRouter: typeof _useRouter;
381
487
  const navigateTo: typeof _navigateTo;
488
+ const definePageMeta: typeof _definePageMeta;
489
+
382
490
  ${returnIfTrue(
383
491
  i18n,
384
492
  /* typescript */
@@ -391,33 +499,33 @@ function createTypedRouterDefinitionFile() {
391
499
  )}
392
500
  }
393
501
 
394
- type TypedNuxtLinkProps = Omit<NuxtLinkProps, 'to'> & {
502
+ type TypedNuxtLinkProps<T extends string> = Omit<NuxtLinkProps, 'to'> &
503
+ {
395
504
  to:
396
- Omit<Exclude<RouteLocationRaw, string>, 'name' | 'params'> & RoutesNamedLocations
397
- ${returnIfFalse(strictOptions.NuxtLink.strictToArgument, "| string")}
398
- ${returnIfTrue(
399
- strictOptions.NuxtLink.strictRouteLocation,
400
- `| Omit<RouteLocationPathRaw, 'path'>`,
401
- "| RouteLocationPathRaw"
505
+ | Omit<Exclude<RouteLocationRaw, string>, 'name' | 'params'> & RoutesNamedLocations
506
+ | Omit<RouteLocationPathRaw, 'path'>
507
+ ${returnIfTrue(
508
+ experimentalPathCheck && !strictOptions.NuxtLink.strictRouteLocation,
509
+ `& {path?: TypedPathParameter<T>}`
402
510
  )}
403
- };
511
+ ${returnIfTrue(
512
+ !experimentalPathCheck && !strictOptions.NuxtLink.strictToArgument,
513
+ ` | string`
514
+ )}
515
+ ${returnIfTrue(
516
+ experimentalPathCheck && !strictOptions.NuxtLink.strictToArgument,
517
+ ` | TypedPathParameter<T>`
518
+ )}
519
+ }
404
520
 
405
- export type TypedNuxtLink = DefineComponent<
406
- TypedNuxtLinkProps,
407
- {},
408
- {},
409
- import('vue').ComputedOptions,
410
- import('vue').MethodOptions,
411
- import('vue').ComponentOptionsMixin,
412
- import('vue').ComponentOptionsMixin,
413
- {},
414
- string,
415
- import('vue').VNodeProps &
416
- import('vue').AllowedComponentProps &
417
- import('vue').ComponentCustomProps,
418
- Readonly<TypedNuxtLinkProps>,
419
- {}
420
- >;
521
+
522
+
523
+ export type TypedNuxtLink = new <P extends string>(props: TypedNuxtLinkProps<P>) => Omit<
524
+ typeof NuxtLink,
525
+ '$props'
526
+ > & {
527
+ $props: TypedNuxtLinkProps<P>;
528
+ };
421
529
 
422
530
  declare module '@vue/runtime-core' {
423
531
  export interface GlobalComponents {
@@ -447,7 +555,7 @@ function createTypedRouterDefinitionFile() {
447
555
  }
448
556
 
449
557
  function createIndexFile() {
450
- const { i18n } = moduleOptionStore;
558
+ const { i18n, experimentalPathCheck } = moduleOptionStore;
451
559
  return (
452
560
  /* typescript */
453
561
  `
@@ -469,11 +577,34 @@ function createIndexFile() {
469
577
  RoutesNamesList,
470
578
  RoutesNamesListRecord,
471
579
  RoutesParamsRecord,
580
+ RoutePath,
581
+ RoutePathByName
472
582
  } from './__routes';
473
583
  export { useRoute } from './__useTypedRoute';
474
584
  export { useRouter } from './__useTypedRouter';
475
585
  export { navigateTo } from './__navigateTo';
476
- ${returnIfTrue(i18n, `export {useLocalePath, useLocaleRoute} from './__i18n-router.ts';`)}
586
+ export { definePageMeta } from './__definePageMeta';
587
+
588
+ ${returnIfTrue(
589
+ experimentalPathCheck,
590
+ `export type { ValidatePath, RoutePathSchema, TypedPathParameter, RouteNameFromPath } from './__paths';`
591
+ )}
592
+ ${returnIfTrue(i18n, `export {useLocalePath, useLocaleRoute} from './__i18n-router';`)}
593
+
594
+ export const helpers = {
595
+ route(
596
+ to: TypedRouteLocationRawFromName<T, P>,
597
+ ): [T] extends [never] ? string : Required<
598
+ (Omit<Exclude<RouteLocationRaw, string>, 'name' | 'params'> & TypedLocationAsRelativeRaw<T>)
599
+ > {
600
+ return to;
601
+ },
602
+ path(
603
+ to: TypedPathParameter<T>,
604
+ ) : [T] extends [never] ? string : Required<TypedRouteLocationRawFromName<RouteNameFromPath<T>, T>> {
605
+ return to;
606
+ }
607
+ }
477
608
  `
478
609
  );
479
610
  }
@@ -502,7 +633,7 @@ function createPluginFile() {
502
633
  );
503
634
  }
504
635
 
505
- function createUseTypedRouteFile(routesDeclTemplate) {
636
+ function createUseTypedRouteFile() {
506
637
  return (
507
638
  /* typescript */
508
639
  `
@@ -542,7 +673,7 @@ function createUseTypedRouteFile(routesDeclTemplate) {
542
673
  );
543
674
  }
544
675
 
545
- function createUseTypedRouterFile(routesDeclTemplate) {
676
+ function createUseTypedRouterFile() {
546
677
  return (
547
678
  /* typescript */
548
679
  `
@@ -570,14 +701,20 @@ function createUseTypedRouterFile(routesDeclTemplate) {
570
701
  }
571
702
 
572
703
  function createNavigateToFile() {
704
+ const { router } = moduleOptionStore.getResolvedStrictOptions();
705
+ const { experimentalPathCheck } = moduleOptionStore;
573
706
  return (
574
707
  /* typescript */
575
708
  `
576
709
  import { navigateTo as defaultNavigateTo } from '#app';
577
710
  import type { NavigateToOptions } from 'nuxt/dist/app/composables/router';
578
711
  import type { NavigationFailure } from 'vue-router';
579
- import type { TypedRouteLocationRawFromName, TypedRouteFromName } from './__router';
712
+ import type { TypedRouteLocationRawFromName, TypedRouteFromName, TypedRoute } from './__router';
580
713
  import type { RoutesNamesList } from './__routes';
714
+ ${returnIfTrue(
715
+ experimentalPathCheck,
716
+ `import type {TypedPathParameter, RouteNameFromPath} from './__paths';`
717
+ )}
581
718
 
582
719
  /**
583
720
  * Typed clone of \`navigateTo\`
@@ -588,10 +725,22 @@ function createNavigateToFile() {
588
725
  * const resolved = navigateTo({name: 'foo', params: {foo: 'bar'}});
589
726
  * \`\`\`
590
727
  */
591
- export const navigateTo: <T extends RoutesNamesList>(
592
- to: TypedRouteLocationRawFromName<T>,
728
+
729
+
730
+ interface NavigateToFunction {
731
+ <T extends RoutesNamesList, P extends string>(
732
+ to: TypedRouteLocationRawFromName<T, P>,
593
733
  options?: NavigateToOptions
594
- ) => Promise<void | NavigationFailure | TypedRouteFromName<T>> = defaultNavigateTo as any;
734
+ ) : Promise<void | NavigationFailure | TypedRouteFromName<T>>
735
+ ${returnIfTrue(
736
+ experimentalPathCheck && !router.strictToArgument,
737
+ `<T extends string>(
738
+ to: TypedPathParameter<T>,
739
+ options?: NavigateToOptions
740
+ ) : Promise<void | NavigationFailure | TypedRouteFromName<RouteNameFromPath<T>>>`
741
+ )}
742
+ }
743
+ export const navigateTo: NavigateToFunction = defaultNavigateTo as any;
595
744
 
596
745
  `
597
746
  );
@@ -622,27 +771,49 @@ function createTypeUtilsRuntimeFile() {
622
771
  }
623
772
 
624
773
  function createi18nRouterFile() {
625
- const { i18nLocales } = moduleOptionStore;
774
+ const { router } = moduleOptionStore.getResolvedStrictOptions();
775
+ const { i18nLocales, experimentalPathCheck } = moduleOptionStore;
626
776
  return (
627
777
  /* typescript */
628
778
  `
629
-
779
+ import type { RouteLocationRaw } from 'vue-router';
630
780
  import { useLocalePath as _useLocalePath, useLocaleRoute as _useLocaleRoute} from 'vue-i18n-routing';
631
- import type {TypedRouteLocationRawFromName, TypedRouteFromName, TypedLocationAsRelativeRaw} from './__router';
781
+ import type {TypedRouteLocationRawFromName, TypedLocationAsRelativeRaw, TypedRouteFromName} from './__router';
632
782
  import type {RoutesNamesList} from './__routes';
783
+ ${returnIfTrue(
784
+ experimentalPathCheck,
785
+ `import type {TypedPathParameter, RouteNameFromPath} from './__paths';`
786
+ )}
633
787
 
634
788
  export type I18nLocales = ${i18nLocales.length ? i18nLocales.map((loc) => `"${loc}"`).join("|") : "string"};
635
789
 
636
- export type TypedToLocalePath = <T extends RoutesNamesList = never>(
637
- to: TypedRouteLocationRawFromName<T>,
638
- locale?: I18nLocales | undefined
639
- ) => [T] extends [never] ? string : Required<TypedLocationAsRelativeRaw<T>>;
790
+ export interface TypedToLocalePath {
791
+ <T extends RoutesNamesList, P extends string>(
792
+ to: TypedRouteLocationRawFromName<T, P>,
793
+ locale?: I18nLocales | undefined
794
+ ) : [T] extends [never] ? string : Required<
795
+ (Omit<Exclude<RouteLocationRaw, string>, 'name' | 'params'> & TypedLocationAsRelativeRaw<T>)
796
+ >
797
+ ${returnIfTrue(
798
+ experimentalPathCheck && !router.strictToArgument,
799
+ `<T extends string>(
800
+ to: TypedPathParameter<T>,
801
+ locale?: I18nLocales | undefined
802
+ ) : [T] extends [never] ? string : Required<TypedRouteLocationRawFromName<RouteNameFromPath<T>, T>>;`
803
+ )}
804
+ }
640
805
 
641
806
  export function useLocalePath(options?: Pick<NonNullable<Parameters<typeof _useLocalePath>[0]>, 'i18n'>): TypedToLocalePath {
642
807
  return _useLocalePath(options) as any;
643
808
  }
644
809
 
645
- export type TypedLocaleRoute = <T extends RoutesNamesList>(to: TypedRouteLocationRawFromName<T>, locale?: I18nLocales | undefined) => TypedRouteFromName<T>;
810
+ export interface TypedLocaleRoute {
811
+ <T extends RoutesNamesList, P extends string>(to: TypedRouteLocationRawFromName<T, P>, locale?: I18nLocales | undefined) : TypedRouteFromName<T>
812
+ ${returnIfTrue(
813
+ experimentalPathCheck && !router.strictToArgument,
814
+ ` <T extends string>(to: TypedPathParameter<T>, locale?: I18nLocales | undefined) : TypedRouteFromName<RouteNameFromPath<T>>;`
815
+ )}
816
+ }
646
817
 
647
818
 
648
819
  export function useLocaleRoute(options?: Pick<NonNullable<Parameters<typeof _useLocaleRoute>[0]>, 'i18n'>): TypedLocaleRoute {
@@ -653,6 +824,283 @@ function createi18nRouterFile() {
653
824
  );
654
825
  }
655
826
 
827
+ const routeParamExtractRegxp = /(:(\w+)(\(.+\)[*+]?)?(\?)?)+/g;
828
+ function extractParamsFromPathDecl(path) {
829
+ let params = [];
830
+ let matches;
831
+ do {
832
+ matches = routeParamExtractRegxp.exec(path);
833
+ if (matches) {
834
+ const [_, mtch, key, catchAll, optional] = matches;
835
+ if (mtch) {
836
+ const _param = {
837
+ name: key,
838
+ optional: !!optional,
839
+ catchAll: !!catchAll
840
+ };
841
+ params.push(_param);
842
+ }
843
+ }
844
+ } while (matches);
845
+ return params;
846
+ }
847
+ function replaceParamsFromPathDecl(path) {
848
+ const replacedPath = path.replace(routeParamExtractRegxp, (_, mtch, key, catchAll, optional) => {
849
+ return catchAll ? `\${((string | ''))}` : `\${(string ${optional ? "| ''" : ""})}`;
850
+ });
851
+ return replacedPath;
852
+ }
853
+
854
+ function extractRouteParamsFromPath(path, isIndexFileForRouting, previousParams) {
855
+ const params = extractParamsFromPathDecl(path);
856
+ let allMergedParams = params.map(
857
+ ({ name, optional, catchAll }) => ({
858
+ key: name,
859
+ required: !optional,
860
+ notRequiredOnPage: optional,
861
+ catchAll
862
+ })
863
+ );
864
+ if (previousParams?.length) {
865
+ allMergedParams = previousParams.map((m) => ({ ...m, required: false })).concat(allMergedParams);
866
+ }
867
+ if (!params.length && isIndexFileForRouting) {
868
+ const lastItem = allMergedParams[allMergedParams.length - 1];
869
+ if (lastItem) {
870
+ lastItem.required = true;
871
+ }
872
+ }
873
+ return allMergedParams;
874
+ }
875
+
876
+ const ExtractRegex = /(^(\/)?([^:/]+)?(:(\w+)(\(.+\)[*+]?)?(\?)?)*([^:/]+)?)+/g;
877
+ function destructurePath(path, fullPath, routeName) {
878
+ let allPathElements = [];
879
+ let _path = `${path}`;
880
+ do {
881
+ const { pathElements, strippedPath } = extractPathElements(_path, fullPath, routeName);
882
+ allPathElements = allPathElements.concat(pathElements);
883
+ _path = _path.replace(strippedPath, "");
884
+ } while (_path.length);
885
+ return allPathElements;
886
+ }
887
+ function extractPathElements(partOfPath, fullPath, routeName) {
888
+ let pathElements = [];
889
+ let strippedPath = "";
890
+ let matches;
891
+ matches = ExtractRegex.exec(partOfPath);
892
+ if (matches) {
893
+ const [_, mtch, slash, path1, paramDef, key, catchAll, optional, path2] = matches;
894
+ if (mtch) {
895
+ strippedPath = mtch;
896
+ if (path1) {
897
+ pathElements.push({ type: "name", content: path1, fullPath, id: nanoid$1(6), routeName });
898
+ }
899
+ if (key) {
900
+ pathElements.push({
901
+ type: catchAll ? "catchAll" : optional ? "optionalParam" : "param",
902
+ content: key,
903
+ fullPath,
904
+ id: nanoid$1(6),
905
+ routeName
906
+ });
907
+ }
908
+ if (path2) {
909
+ pathElements.push({ type: "name", content: path2, fullPath, id: nanoid$1(6), routeName });
910
+ }
911
+ }
912
+ }
913
+ return { pathElements, strippedPath };
914
+ }
915
+
916
+ function createPathsFiles({ routesPaths }) {
917
+ const filteredRoutesPaths = routesPaths.filter(
918
+ (route, index) => routesPaths.findIndex((r) => route.name === r.name) === index && !routesPaths.find((r) => `${route.path}/` === r.path)
919
+ ).sort((a, b) => {
920
+ const pathCountA = a.path.split("/");
921
+ const pathCountB = b.path.split("/");
922
+ pathCountA.splice(0, 1);
923
+ pathCountB.splice(0, 1);
924
+ const maxIndex = Math.max(pathCountA.length, pathCountB.length) - 1;
925
+ let order = 0;
926
+ let index = 0;
927
+ let alphabetOrder;
928
+ let hasElement;
929
+ let hasParam;
930
+ let indexOfParam;
931
+ do {
932
+ alphabetOrder = pathCountA[index]?.localeCompare(pathCountB[index]);
933
+ hasElement = (pathCountA[index] != null ? 1 : 0) - (pathCountB[index] != null ? 1 : 0);
934
+ hasParam = (pathCountA[index]?.includes(":") ? 1 : 0) - (pathCountB[index]?.includes(":") ? 1 : 0);
935
+ indexOfParam = pathCountB[index]?.indexOf(":") - pathCountA[index]?.indexOf(":");
936
+ if (alphabetOrder !== 0 && index === 0) {
937
+ order = alphabetOrder;
938
+ break;
939
+ } else {
940
+ if (hasElement !== 0) {
941
+ order = hasElement;
942
+ break;
943
+ } else if (hasParam !== 0) {
944
+ order = hasParam;
945
+ break;
946
+ } else if (hasParam === 0 && indexOfParam !== 0) {
947
+ order = indexOfParam;
948
+ break;
949
+ } else if (alphabetOrder !== 0) {
950
+ order = alphabetOrder;
951
+ break;
952
+ }
953
+ }
954
+ index = index + 1;
955
+ } while (index < maxIndex);
956
+ return order;
957
+ });
958
+ const pathElements = filteredRoutesPaths.filter((f) => f.path && f.path !== "/").map((route) => {
959
+ return route.path.split("/").filter((f) => f.length).map((m) => destructurePath(m, route.path, route.name));
960
+ });
961
+ const validatePathTypes = createValidatePathTypes(pathElements);
962
+ return (
963
+ /* typescript */
964
+ `
965
+
966
+ ${createRoutePathSchema(filteredRoutesPaths)};
967
+
968
+ type ValidStringPath<T> = T extends \`\${string} \${string}\` ? false : T extends '' ? false : true;
969
+
970
+ type ValidParam<T, R extends boolean = true> = T extends \`\${infer A}/\${infer B}\`
971
+ ? A extends \`\${string} \${string}\`
972
+ ? false
973
+ : A extends \`?\${string}\`
974
+ ? false
975
+ : A extends \`\${string} \${string}\`
976
+ ? false
977
+ : A extends ''
978
+ ? B extends ''
979
+ ? true
980
+ : false
981
+ : B extends \`?\${string}\`
982
+ ? false
983
+ : B extends \`#\${string}\`
984
+ ? true
985
+ : B extends ''
986
+ ? true
987
+ : false
988
+ : R extends true
989
+ ? T extends ''
990
+ ? false
991
+ : ValidParam<T, false>
992
+ : T extends \`?\${string}\`
993
+ ? false
994
+ : T extends \`\${string} \${string}\`
995
+ ? false
996
+ : true;
997
+
998
+ type ValidEndOfPath<T> = T extends \`/\`
999
+ ? true
1000
+ : T extends ''
1001
+ ? true
1002
+ : T extends \`\${string} \${string}\`
1003
+ ? false
1004
+ : T extends \`?\${string}\`
1005
+ ? true
1006
+ : T extends \`#\${string}\`
1007
+ ? true
1008
+ : false;
1009
+
1010
+ ${validatePathTypes}
1011
+
1012
+
1013
+ export type TypedPathParameter<T extends string> = ValidatePath<T> | RoutePathSchema
1014
+
1015
+ `
1016
+ );
1017
+ }
1018
+
1019
+ function createDefinePageMetaFile() {
1020
+ const strictOptions = moduleOptionStore.getResolvedStrictOptions();
1021
+ const { experimentalPathCheck } = moduleOptionStore;
1022
+ return (
1023
+ /* typescript */
1024
+ `
1025
+
1026
+ import { definePageMeta as defaultDefinePageMeta } from '#imports';
1027
+ import type {PageMeta, NuxtError} from '#app'
1028
+ import type {TypedRouteFromName, TypedRoute, TypedRouteLocationRawFromName} from './__router';
1029
+ import type {RoutesNamesList, RoutesNamedLocations} from './__routes';
1030
+ ${returnIfTrue(experimentalPathCheck, `import type {TypedPathParameter} from './__paths';`)}
1031
+
1032
+ type FilteredPageMeta = {
1033
+ [T in keyof PageMeta as [unknown] extends [PageMeta[T]] ? never : T]: PageMeta[T];
1034
+ }
1035
+
1036
+ export type TypedPageMeta<T extends RoutesNamesList, P extends string, U extends RoutesNamesList> = Omit<FilteredPageMeta, 'redirect' | 'validate' | 'key'> & {
1037
+ /**
1038
+ * Validate whether a given route can validly be rendered with this page.
1039
+ *
1040
+ * Return true if it is valid, or false if not. If another match can't be found,
1041
+ * this will mean a 404. You can also directly return an object with
1042
+ * statusCode/statusMessage to respond immediately with an error (other matches
1043
+ * will not be checked).
1044
+ */
1045
+ validate?: (route: [T] extends [never] ? TypedRoute : TypedRouteFromName<T>) => boolean | Promise<boolean> | Partial<NuxtError> | Promise<Partial<NuxtError>>;
1046
+ /**
1047
+ * Where to redirect if the route is directly matched. The redirection happens
1048
+ * before any navigation guard and triggers a new navigation with the new
1049
+ * target location.
1050
+ */
1051
+ redirect?:
1052
+ | TypedRouteLocationRawFromName<U, P>
1053
+ | ((to: [T] extends [never] ? TypedRoute : TypedRouteFromName<T>)
1054
+ => TypedRouteLocationRawFromName<any, P> ${returnIfTrue(
1055
+ experimentalPathCheck && !strictOptions.router.strictToArgument,
1056
+ ` | TypedPathParameter<P>`
1057
+ )})
1058
+ ${returnIfTrue(
1059
+ experimentalPathCheck && !strictOptions.router.strictToArgument,
1060
+ ` | TypedPathParameter<P>`
1061
+ )}
1062
+ key?: false | string | ((route: [T] extends [never] ? TypedRoute : TypedRouteFromName<T>) => string);
1063
+ }
1064
+
1065
+
1066
+ /**
1067
+ * Typed clone of \`definePageMeta\`
1068
+ *
1069
+ * \u26A0\uFE0F Types for the redirect function may be buggy or not display autocomplete
1070
+ *
1071
+ * @exemple
1072
+ *
1073
+ * \`\`\`ts
1074
+ * definePageMeta('current-location-name', {
1075
+ * validate(route) {
1076
+ * });
1077
+ * // or
1078
+ * definePageMeta({
1079
+ * validate(route) {
1080
+ * });
1081
+ * \`\`\`
1082
+ */
1083
+ export function definePageMeta<
1084
+ P extends string,
1085
+ U extends RoutesNamesList
1086
+ >(meta: TypedPageMeta<never, P, U>): void;
1087
+ export function definePageMeta<
1088
+ T extends RoutesNamesList,
1089
+ P extends string,
1090
+ U extends RoutesNamesList
1091
+ >(routeName: T, meta: TypedPageMeta<T, P, U>): void;
1092
+ export function definePageMeta(metaOrName: any, meta?: any): void {
1093
+ if (typeof metaOrName === 'string') {
1094
+ return defaultDefinePageMeta(meta as any);
1095
+ } else {
1096
+ return defaultDefinePageMeta(metaOrName as any);
1097
+ }
1098
+ }
1099
+
1100
+ `
1101
+ );
1102
+ }
1103
+
656
1104
  async function handleAddPlugin() {
657
1105
  const pluginName = "__typed-router.plugin.ts";
658
1106
  addPluginTemplate({
@@ -735,9 +1183,8 @@ const watermarkTemplate = `
735
1183
  `;
736
1184
 
737
1185
  let previousGeneratedRoutes = "";
738
- async function saveGeneratedFiles({
739
- outputData: { routesDeclTemplate, routesList, routesObjectTemplate, routesParams }
740
- }) {
1186
+ let firstRun = false;
1187
+ async function saveGeneratedFiles({ outputData }) {
741
1188
  const { i18n } = moduleOptionStore;
742
1189
  const filesMap = [
743
1190
  {
@@ -748,19 +1195,22 @@ async function saveGeneratedFiles({
748
1195
  fileName: "__useTypedRoute.ts",
749
1196
  content: createUseTypedRouteFile()
750
1197
  },
1198
+ {
1199
+ fileName: "__paths.d.ts",
1200
+ content: createPathsFiles(outputData)
1201
+ },
751
1202
  {
752
1203
  fileName: `__routes.ts`,
753
- content: createRoutesTypesFile({
754
- routesList,
755
- routesObjectTemplate,
756
- routesDeclTemplate,
757
- routesParams
758
- })
1204
+ content: createRoutesTypesFile(outputData)
759
1205
  },
760
1206
  {
761
1207
  fileName: "__navigateTo.ts",
762
1208
  content: createNavigateToFile()
763
1209
  },
1210
+ {
1211
+ fileName: "__definePageMeta.ts",
1212
+ content: createDefinePageMetaFile()
1213
+ },
764
1214
  {
765
1215
  fileName: `__router.d.ts`,
766
1216
  content: createTypedRouterFile()
@@ -794,57 +1244,21 @@ async function saveGeneratedFiles({
794
1244
  return processPathAndWriteFile({ content: waterMakeredContent, fileName });
795
1245
  })
796
1246
  );
797
- if (previousGeneratedRoutes !== routesList.join(",")) {
798
- previousGeneratedRoutes = routesList.join(",");
1247
+ if (previousGeneratedRoutes !== outputData.routesList.join(",")) {
1248
+ previousGeneratedRoutes = outputData.routesList.join(",");
799
1249
  console.log(logSymbols.success, `Router autocompletions generated \u{1F6A6}`);
800
- }
801
- }
802
-
803
- const routeParamExtractRegxp = /(:(\w+)(\(.+\)[*+]?)?(\?)?)+/g;
804
- function extractRouteParamsFromPath(path, isIndexFileForRouting, previousParams) {
805
- let params = [];
806
- let matches;
807
- do {
808
- matches = routeParamExtractRegxp.exec(path);
809
- if (matches) {
810
- const [_, mtch, key, catchAll, optional] = matches;
811
- if (mtch) {
812
- params.push({ name: key, optional: !!optional, catchAll: !!catchAll });
813
- }
814
- }
815
- } while (matches);
816
- let allMergedParams = params.map(
817
- ({ name, optional, catchAll }) => ({
818
- key: name,
819
- required: !optional,
820
- notRequiredOnPage: optional,
821
- catchAll
822
- })
823
- );
824
- if (previousParams?.length) {
825
- allMergedParams = previousParams.map((m) => ({ ...m, required: false })).concat(allMergedParams);
826
- }
827
- if (!params.length && isIndexFileForRouting) {
828
- const lastItem = allMergedParams[allMergedParams.length - 1];
829
- if (lastItem) {
830
- lastItem.required = true;
1250
+ if (!firstRun) {
1251
+ firstRun = true;
1252
+ console.log(
1253
+ logSymbols.warning,
1254
+ chalk.yellow(
1255
+ `Route path autocomplete is still experimental. You can disable it with the "nuxtTypedRouter.experimentalPathCheck: false" option`
1256
+ )
1257
+ );
831
1258
  }
832
1259
  }
833
- return allMergedParams;
834
1260
  }
835
1261
 
836
- function extractMatchingSiblings(mainRoute, siblingRoutes) {
837
- return siblingRoutes?.filter((s) => {
838
- const chunkName = extractChunkMain(mainRoute.file);
839
- if (chunkName && s.name) {
840
- const siblingChunkName = extractChunkMain(s.file);
841
- if (!siblingChunkName)
842
- return false;
843
- return chunkName === siblingChunkName;
844
- }
845
- return false;
846
- });
847
- }
848
1262
  function extractUnMatchingSiblings(mainRoute, siblingRoutes) {
849
1263
  return siblingRoutes?.filter((s) => {
850
1264
  const chunkName = extractChunkMain(mainRoute.file);
@@ -862,11 +1276,14 @@ function extractChunkMain(chunkName) {
862
1276
  return chunkArray?.join("/");
863
1277
  }
864
1278
 
865
- function createKeyedName(route) {
1279
+ function createKeyedName(route, parent) {
866
1280
  const splittedPaths = route.path.split("/");
867
1281
  const parentPath = splittedPaths[splittedPaths.length - 1];
868
- const nameKey = camelCase(parentPath || "index");
869
- return nameKey;
1282
+ if (parent) {
1283
+ return camelCase(parentPath || "index");
1284
+ } else {
1285
+ return camelCase(route.path.split("/").join("-") || "index");
1286
+ }
870
1287
  }
871
1288
  function createNameKeyFromFullName(route, level, parentName) {
872
1289
  let splitted = [];
@@ -878,68 +1295,65 @@ function createNameKeyFromFullName(route, level, parentName) {
878
1295
  const keyName = route.path === "" ? "index" : camelCase(splitted.join("-")) || "index";
879
1296
  return keyName;
880
1297
  }
1298
+ function hasi18nSibling(source, route) {
1299
+ return source.some((rt) => {
1300
+ return route.name?.match(new RegExp(`^(${rt.name})___[a-zA-Z]+`, "g")) || rt.path !== "/" && route.path?.match(new RegExp(`/?[a-zA-Z]+${rt.path}`, "g"));
1301
+ });
1302
+ }
881
1303
  function walkThoughRoutes({
882
1304
  route,
883
1305
  level,
884
1306
  siblings,
885
- parentName,
1307
+ parent,
886
1308
  previousParams,
887
1309
  output,
888
1310
  isLast
889
1311
  }) {
890
- const matchingSiblings = extractMatchingSiblings(route, siblings);
891
- const haveMatchingSiblings = !!matchingSiblings?.length && route.path !== "/";
892
1312
  const chunkArray = route.file?.split("/") ?? [];
893
- const lastChunkArray = chunkArray[chunkArray?.length - 1].split(".vue")[0];
894
- const isRootSibling = lastChunkArray === "index";
895
- if (route.children?.length && !haveMatchingSiblings || !route.children?.length && haveMatchingSiblings && isRootSibling) {
896
- let childrenChunks = haveMatchingSiblings ? matchingSiblings : route.children;
897
- let nameKey = createKeyedName(route);
898
- const hasSamePathI18nSibling = siblings?.some((sibling) => {
899
- const _name = createKeyedName(sibling);
900
- return _name === nameKey;
901
- });
902
- if (hasSamePathI18nSibling) {
903
- nameKey = camelCase(route.path.split("/").join("-"));
904
- }
905
- output.routesObjectTemplate += `${nameKey}:{`;
906
- output.routesDeclTemplate += `"${nameKey}":{`;
907
- const allRouteParams = extractRouteParamsFromPath(route.path, false, previousParams);
908
- childrenChunks?.map(
909
- (routeConfig, index) => walkThoughRoutes({
910
- route: routeConfig,
911
- level: level + 1,
912
- siblings: extractUnMatchingSiblings(route, siblings),
913
- parentName: nameKey,
914
- previousParams: allRouteParams,
915
- output,
916
- isLast: isItemLast(childrenChunks, index)
917
- })
918
- );
919
- output.routesObjectTemplate += "},";
920
- output.routesDeclTemplate += `}${isLast ? "" : ","}`;
921
- } else if (route.name) {
922
- let keyName = createNameKeyFromFullName(route, level, parentName);
923
- const hasSameNameI18nSibling = siblings?.some((sibling) => {
924
- const _name = createNameKeyFromFullName(sibling, level, parentName);
925
- return _name === keyName;
926
- });
927
- if (hasSameNameI18nSibling) {
928
- keyName = camelCase(route.name);
929
- }
930
- output.routesObjectTemplate += `'${keyName}': '${route.name}' as const,`;
931
- output.routesDeclTemplate += `"${keyName}": "${route.name}"${isLast ? "" : ","}`;
932
- output.routesList.push(route.name);
933
- const isIndexFileForRouting = route.path === "";
934
- const allRouteParams = extractRouteParamsFromPath(
935
- route.path,
936
- isIndexFileForRouting,
937
- previousParams
938
- );
939
- output.routesParams.push({
1313
+ chunkArray[chunkArray?.length - 1].split(".vue")[0];
1314
+ if (!hasi18nSibling(output.routesPaths, route)) {
1315
+ const newPath = `${parent?.path ?? ""}${route.path.startsWith("/") ? route.path : `/${route.path}`}`;
1316
+ output.routesPaths.push({
940
1317
  name: route.name,
941
- params: allRouteParams
1318
+ typePath: replaceParamsFromPathDecl(newPath),
1319
+ path: newPath
942
1320
  });
1321
+ if (route.children?.length) {
1322
+ let childrenChunks = route.children;
1323
+ let nameKey = createKeyedName(route, parent);
1324
+ const allRouteParams = extractRouteParamsFromPath(route.path, false, previousParams);
1325
+ const newRoute = { ...route, name: nameKey, path: newPath };
1326
+ output.routesObjectTemplate += `${nameKey}:{`;
1327
+ output.routesDeclTemplate += `"${nameKey}":{`;
1328
+ childrenChunks?.map(
1329
+ (routeConfig, index) => walkThoughRoutes({
1330
+ route: routeConfig,
1331
+ level: level + 1,
1332
+ siblings: extractUnMatchingSiblings(route, siblings),
1333
+ parent: newRoute,
1334
+ previousParams: allRouteParams,
1335
+ output,
1336
+ isLast: isItemLast(childrenChunks, index)
1337
+ })
1338
+ );
1339
+ output.routesObjectTemplate += "},";
1340
+ output.routesDeclTemplate += `}${isLast ? "" : ","}`;
1341
+ } else if (route.name) {
1342
+ let keyName = createNameKeyFromFullName(route, level, parent?.name);
1343
+ output.routesObjectTemplate += `'${keyName}': '${route.name}' as const,`;
1344
+ output.routesDeclTemplate += `"${keyName}": "${route.name}"${isLast ? "" : ","}`;
1345
+ output.routesList.push(route.name);
1346
+ const isIndexFileForRouting = route.path === "";
1347
+ const allRouteParams = extractRouteParamsFromPath(
1348
+ route.path,
1349
+ isIndexFileForRouting,
1350
+ previousParams
1351
+ );
1352
+ output.routesParams.push({
1353
+ name: route.name,
1354
+ params: allRouteParams
1355
+ });
1356
+ }
943
1357
  }
944
1358
  }
945
1359
 
@@ -949,14 +1363,23 @@ function constructRouteMap(routesConfig) {
949
1363
  let routesDeclTemplate = "{";
950
1364
  let routesList = [];
951
1365
  let routesParams = [];
952
- const output = { routesObjectTemplate, routesDeclTemplate, routesList, routesParams };
1366
+ let routesPaths = [];
1367
+ let routesPathsTree = "{";
1368
+ const output = {
1369
+ routesObjectTemplate,
1370
+ routesDeclTemplate,
1371
+ routesList,
1372
+ routesParams,
1373
+ routesPaths,
1374
+ routesPathsTree
1375
+ };
953
1376
  startGenerator({
954
1377
  output,
955
1378
  routesConfig
956
1379
  });
957
1380
  return output;
958
1381
  } catch (e) {
959
- throw new Error("Generation failed");
1382
+ throw new Error("Generation failed", e);
960
1383
  }
961
1384
  }
962
1385
  function startGenerator({ output, routesConfig }) {
@@ -1034,7 +1457,8 @@ const module = defineNuxtModule({
1034
1457
  },
1035
1458
  defaults: {
1036
1459
  plugin: false,
1037
- strict: false
1460
+ strict: false,
1461
+ experimentalPathCheck: true
1038
1462
  },
1039
1463
  setup(moduleOptions, nuxt) {
1040
1464
  const { resolve } = createResolver(import.meta.url);
@@ -1066,8 +1490,28 @@ const module = defineNuxtModule({
1066
1490
  "@typed-router": resolve(`${rootDir}/.nuxt/typed-router`)
1067
1491
  };
1068
1492
  nuxt.options.typescript.tsConfig = {
1069
- include: ["./typed-router/typed-router.d.ts"]
1493
+ include: ["./typed-router/typed-router.d.ts"],
1494
+ ...moduleOptions.experimentalPathCheck && {
1495
+ // Enable Volar components generics https://github.com/vuejs/rfcs/discussions/436
1496
+ vueCompilerOptions: {
1497
+ jsxTemplates: true,
1498
+ experimentalRfc436: true
1499
+ }
1500
+ }
1070
1501
  };
1502
+ if (nuxt.options.dev) {
1503
+ nuxt.hook("devtools:customTabs", (tabs) => {
1504
+ tabs.push({
1505
+ name: "nuxt-typed-router",
1506
+ title: "Nuxt Typed Router",
1507
+ icon: "https://github.com/victorgarciaesgi/nuxt-typed-router/blob/master/.github/images/logo.png?raw=true",
1508
+ view: {
1509
+ type: "iframe",
1510
+ src: "https://nuxt-typed-router.vercel.app/"
1511
+ }
1512
+ });
1513
+ });
1514
+ }
1071
1515
  createTypedRouter({ nuxt });
1072
1516
  }
1073
1517
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-typed-router",
3
- "version": "2.3.4",
4
- "description": "Provide autocompletion for pages route names generated by Nuxt router",
3
+ "version": "3.0.0-beta.0",
4
+ "description": "Provide autocompletion for paths, routes names and params in Nuxt apps",
5
5
  "type": "module",
6
6
  "main": "./dist/module.cjs",
7
7
  "types": "./dist/types.d.ts",
@@ -22,13 +22,15 @@
22
22
  "dev:prepare": "nuxt-module-build --stub && nuxi prepare playground && pnpm run test:prepare-fixtures",
23
23
  "build:test": "cross-env NUXT_BUILD_TYPE=stub pnpm run prepack && pnpm run dev:build",
24
24
  "test:prepare-fixtures": "nuxi prepare test/fixtures/simple && nuxi prepare test/fixtures/withOptions && nuxi prepare test/fixtures/complex",
25
- "test:fixtures": " vitest run --dir test",
26
- "test:types": "pnpm run typecheck && pnpm run test:vue && vitest typecheck --run --dir test",
25
+ "test:fixtures": "vitest run --dir test",
26
+ "test:types": "pnpm run typecheck && pnpm run test:vue",
27
27
  "test:vue": "vue-tsc -p test/fixtures/simple/tsconfig.json --noEmit && vue-tsc -p test/fixtures/complex/tsconfig.json --noEmit && vue-tsc -p test/fixtures/withOptions/tsconfig.json --noEmit",
28
28
  "test": "pnpm run dev:prepare && pnpm run test:types && pnpm run test:fixtures",
29
+ "lint": "eslint --ext .ts --ext .vue .",
29
30
  "docs:dev": "cd docs && pnpm run dev",
30
31
  "docs:build": "npm run dev:prepare && cd docs && nuxi generate",
31
- "typecheck": "tsc --noEmit"
32
+ "typecheck": "tsc --noEmit",
33
+ "release": "pnpm test && standard-version && git push --follow-tags && npm publish"
32
34
  },
33
35
  "publishConfig": {
34
36
  "access": "public"
@@ -57,37 +59,41 @@
57
59
  "url": "https://github.com/victorgarciaesgi/nuxt-typed-router/issues"
58
60
  },
59
61
  "dependencies": {
60
- "@nuxt/kit": "3.1.1",
62
+ "@nuxt/kit": "^3.2.0",
61
63
  "chalk": "^5.2.0",
62
64
  "defu": "^6.1.2",
63
65
  "lodash-es": "^4.17.21",
64
66
  "log-symbols": "^5.1.0",
65
- "mkdirp": "^1.0.4",
67
+ "mkdirp": "^2.1.3",
68
+ "nanoid": "^4.0.1",
66
69
  "pathe": "1.1.0",
67
- "prettier": "2.8.3"
70
+ "prettier": "2.8.4"
68
71
  },
69
72
  "devDependencies": {
73
+ "@nuxt/devtools": "^0.1.0",
70
74
  "@nuxt/module-builder": "^0.2.1",
71
- "@nuxt/test-utils": "^3.1.1",
72
- "@nuxt/types": "^2.15.8",
75
+ "@nuxt/test-utils": "^3.2.0",
76
+ "@nuxt/types": "^2.16.0",
73
77
  "@nuxtjs/eslint-config-typescript": "^12.0.0",
74
- "@nuxtjs/web-vitals": "^0.2.2",
75
78
  "@nuxtjs/i18n": "8.0.0-beta.9",
79
+ "@nuxtjs/web-vitals": "^0.2.2",
76
80
  "@types/lodash-es": "^4.17.6",
77
81
  "@types/mkdirp": "^1.0.2",
78
- "@types/node": "^17.0.23",
82
+ "@types/node": "^18.13.0",
79
83
  "@types/prettier": "^2.7.2",
80
- "@typescript-eslint/eslint-plugin": "^5.50.0",
81
- "@typescript-eslint/parser": "^5.50.0",
82
- "@vue/test-utils": "^2.2.8",
84
+ "@typescript-eslint/eslint-plugin": "^5.51.0",
85
+ "@typescript-eslint/parser": "^5.51.0",
86
+ "@vue/test-utils": "^2.2.10",
83
87
  "cross-env": "^7.0.3",
84
88
  "eslint": "8.33.0",
85
89
  "eslint-config-prettier": "^8.6.0",
86
90
  "eslint-plugin-vue": "^9.9.0",
87
- "nuxt": "3.1.1",
91
+ "nuxt": "3.2.0",
88
92
  "playwright": "1.30.0",
93
+ "standard-version": "^9.5.0",
94
+ "tsd": "^0.25.0",
89
95
  "typescript": "^4.9.5",
90
- "vitest": "^0.28.3",
96
+ "vitest": "^0.28.2",
91
97
  "vue-eslint-parser": "^9.1.0",
92
98
  "vue-router": "^4.1.6",
93
99
  "vue-tsc": "^1.0.24"