>;
30 | key?: false | string | ((route: TypedRoute) => string);
31 | /** Allow types augmented by other modules */
32 | [key: string]: any;
33 | }
34 |
35 |
36 | /**
37 | * Typed clone of \`definePageMeta\`
38 | *
39 | * ⚠️ Types for the redirect function may be buggy or not display autocomplete
40 | * Use \`helpers.route\` or \`helpers.path\` to provide autocomplete.
41 | *
42 | * \`\`\`ts
43 | * import {helpers} from '@typed-router';
44 | * definePageMeta({
45 | * redirect(route) {
46 | * return helpers.path('/foo')
47 | * }
48 | * });
49 | * \`\`\`
50 | * @exemple
51 | *
52 | * \`\`\`ts
53 | * definePageMeta({
54 | * validate(route) {
55 | * });
56 | * \`\`\`
57 | */
58 | export function definePageMeta(
59 | meta: TypedPageMeta & { redirect: TypedRouteLocationRawFromName }
60 | ): void;
61 | ${returnIfTrue(
62 | pathCheck && !strictOptions.router.strictToArgument,
63 | `export function definePageMeta
(
64 | meta: TypedPageMeta & { redirect: TypedPathParameter
}
65 | ): void;`
66 | )}
67 | export function definePageMeta
(
68 | meta: TypedPageMeta & {
69 | redirect?: (to: TypedRoute) => TypedRouteLocationRaw
${returnIfTrue(
70 | pathCheck && !strictOptions.router.strictToArgument,
71 | ` | TypedPathParameter
`
72 | )};
73 | }
74 | ): void;
75 | export function definePageMeta
(
76 | meta: TypedPageMeta & {
77 | redirect?: () => TypedRouteLocationRaw
${returnIfTrue(
78 | pathCheck && !strictOptions.router.strictToArgument,
79 | ` | TypedPathParameter
`
80 | )};
81 | }
82 | ): void;
83 | export function definePageMeta(meta?: TypedPageMeta): void {
84 | return defaultDefinePageMeta(meta);
85 | }
86 |
87 | `;
88 | }
89 |
--------------------------------------------------------------------------------
/src/core/output/generators/files/__helpers.file.ts:
--------------------------------------------------------------------------------
1 | import { returnIfFalse, returnIfTrue } from '../../../../utils';
2 | import { moduleOptionStore } from '../../../config';
3 |
4 | export function createHelpersFile() {
5 | const { pathCheck } = moduleOptionStore;
6 |
7 | return /* typescript */ `
8 |
9 |
10 |
11 | import type { RouteLocationRaw } from 'vue-router';
12 | import type { TypedRouteLocationRawFromName, TypedLocationAsRelativeRaw } from './__router';
13 | import type { RoutesNamesList } from './__routes';
14 | ${returnIfTrue(
15 | pathCheck,
16 | `import type {TypedPathParameter, RouteNameFromPath} from './__paths';`
17 | )}
18 |
19 | export const helpers = {
20 | route(
21 | to: TypedRouteLocationRawFromName
22 | ): [T] extends [never]
23 | ? string
24 | : Required<
25 | Omit, 'name' | 'params' | 'path'> & TypedLocationAsRelativeRaw
26 | > {
27 | return to as any;
28 | },
29 | path(
30 | to: TypedPathParameter
31 | ): [T] extends [never]
32 | ? string
33 | : Required, T>> {
34 | return to as any;
35 | },
36 | };
37 |
38 |
39 | `;
40 | }
41 |
--------------------------------------------------------------------------------
/src/core/output/generators/files/__i18n-router.file.ts:
--------------------------------------------------------------------------------
1 | import { returnIfTrue } from '../../../../utils';
2 | import { moduleOptionStore } from '../../../config';
3 |
4 | export function createi18nRouterFile() {
5 | const { router, NuxtLink } = moduleOptionStore.getResolvedStrictOptions();
6 | const { i18nOptions, pathCheck, i18nLocales } = moduleOptionStore;
7 |
8 | const LocalePathType =
9 | i18nOptions?.strategy === 'no_prefix' ? 'TypedPathParameter' : 'TypedLocalePathParameter';
10 |
11 | return /* typescript */ `
12 | import type { RouteLocationRaw } from 'vue-router';
13 | import { useLocalePath as _useLocalePath, useLocaleRoute as _useLocaleRoute} from '#imports';
14 | import type {TypedRouteLocationRawFromName, TypedLocationAsRelativeRaw, TypedRouteFromName} from './__router';
15 | import type {RoutesNamesList} from './__routes';
16 | ${returnIfTrue(
17 | pathCheck,
18 | `import type {TypedLocalePathParameter, TypedPathParameter, RouteNameFromLocalePath} from './__paths';`
19 | )}
20 |
21 | export type I18nLocales = ${
22 | i18nLocales?.length ? i18nLocales.map((loc) => `"${loc}"`).join('|') : 'string'
23 | };
24 |
25 |
26 | export type NuxtLocaleRoute =
27 | | TypedRouteLocationRawFromName
28 | ${returnIfTrue(!pathCheck && !NuxtLink.strictToArgument, ` | string`)}
29 | ${returnIfTrue(pathCheck && NuxtLink.strictToArgument, ` | (E extends true ? string : never)`)}
30 | ${returnIfTrue(
31 | pathCheck && !NuxtLink.strictToArgument,
32 | ` | (E extends true ? string : ${LocalePathType})`
33 | )}
34 |
35 | export interface TypedToLocalePath {
36 | (
37 | to: TypedRouteLocationRawFromName,
38 | locale?: I18nLocales | undefined
39 | ) : [T] extends [never] ? string : Required<
40 | (Omit, 'name' | 'params' | 'path'> & TypedLocationAsRelativeRaw)
41 | >
42 | ${returnIfTrue(
43 | pathCheck && !router.strictToArgument,
44 | `(
45 | to: ${LocalePathType},
46 | locale?: I18nLocales | undefined
47 | ) : [T] extends [never] ? string : Required, T>>;`
48 | )}
49 | }
50 |
51 | export function useLocalePath(): TypedToLocalePath {
52 | return _useLocalePath() as any;
53 | }
54 |
55 | export interface TypedLocaleRoute {
56 | (to: TypedRouteLocationRawFromName, locale?: I18nLocales | undefined) : TypedRouteFromName
57 | ${returnIfTrue(
58 | pathCheck && !router.strictToArgument,
59 | ` (to: ${LocalePathType}, locale?: I18nLocales | undefined) : TypedRouteFromName>;`
60 | )}
61 | }
62 |
63 |
64 | export function useLocaleRoute(): TypedLocaleRoute {
65 | return _useLocaleRoute() as any;
66 | }
67 |
68 | `;
69 | }
70 |
--------------------------------------------------------------------------------
/src/core/output/generators/files/__navigateTo.file.ts:
--------------------------------------------------------------------------------
1 | import { returnIfTrue } from '../../../../utils';
2 | import { moduleOptionStore } from '../../../config';
3 |
4 | export function createNavigateToFile() {
5 | const { router } = moduleOptionStore.getResolvedStrictOptions();
6 | const { pathCheck } = moduleOptionStore;
7 | return /* typescript */ `
8 | import { navigateTo as defaultNavigateTo } from '#imports';
9 | import type { NavigateToOptions } from 'nuxt/dist/app/composables/router';
10 | import type { NavigationFailure } from 'vue-router';
11 | import type { TypedRouteLocationRawFromName, TypedRouteFromName, TypedRoute } from './__router';
12 | import type { RoutesNamesList } from './__routes';
13 | ${returnIfTrue(
14 | pathCheck,
15 | `import type {TypedPathParameter, RouteNameFromPath} from './__paths';`
16 | )}
17 |
18 | type TypedNavigateToOptions = Omit & {
19 | external?: E
20 | }
21 |
22 | /**
23 | * Typed clone of \`navigateTo\`
24 | *
25 | * @exemple
26 | *
27 | * \`\`\`ts
28 | * const resolved = navigateTo({name: 'foo', params: {foo: 'bar'}});
29 | * \`\`\`
30 | */
31 |
32 |
33 | interface NavigateToFunction {
34 | (
35 | to: TypedRouteLocationRawFromName,
36 | options?: TypedNavigateToOptions
37 | ) : Promise>
38 | ${returnIfTrue(
39 | pathCheck && !router.strictToArgument,
40 | `(
41 | to: (E extends true ? string : TypedPathParameter),
42 | options?: TypedNavigateToOptions
43 | ) : Promise>>`
44 | )}
45 | }
46 |
47 | export const navigateTo: NavigateToFunction = defaultNavigateTo as any;
48 |
49 | `;
50 | }
51 |
--------------------------------------------------------------------------------
/src/core/output/generators/files/__paths.file.ts:
--------------------------------------------------------------------------------
1 | import { returnIfTrue } from '../../../../../src/utils';
2 | import type { GeneratorOutput } from '../../../../types';
3 | import { moduleOptionStore } from '../../../config';
4 | import { destructurePath } from '../../../parser/params';
5 | import {
6 | createLocaleRoutePathSchema,
7 | createRoutePathSchema,
8 | createValidatePathTypes,
9 | } from '../blocks';
10 |
11 | export function createPathsFiles({ routesPaths, routesList }: GeneratorOutput) {
12 | const { i18n, i18nOptions } = moduleOptionStore;
13 | const hasPrefixStrategy = i18n && i18nOptions?.strategy !== 'no_prefix';
14 |
15 | const filteredRoutesPaths = routesPaths
16 | .filter((route) => !routesPaths.find((r) => `${route.path}/` === r.path))
17 | .map((route) => ({
18 | ...route,
19 | path: route.path.replace(/\(\)/g, ''),
20 | }))
21 | .sort((a, b) => {
22 | const pathCountA = a.path.split('/');
23 | const pathCountB = b.path.split('/');
24 | pathCountA.splice(0, 1);
25 | pathCountB.splice(0, 1);
26 | const maxIndex = Math.max(pathCountA.length, pathCountB.length) - 1;
27 |
28 | let order = 0;
29 | let index = 0;
30 | let reason: string = '';
31 |
32 | let alphabetOrder: number;
33 | let hasElement: number;
34 | let hasParam: number;
35 | let indexOfParam: number;
36 | do {
37 | alphabetOrder = pathCountA[index]?.localeCompare(pathCountB[index] ?? '') ?? 0;
38 | hasElement = (pathCountA[index] != null ? 1 : 0) - (pathCountB[index] != null ? 1 : 0);
39 | hasParam =
40 | (pathCountA[index]?.includes(':') ? 1 : 0) - (pathCountB[index]?.includes(':') ? 1 : 0);
41 | indexOfParam =
42 | (pathCountB[index]?.indexOf(':') ?? 0) - (pathCountA[index]?.indexOf(':') ?? 0);
43 |
44 | if (alphabetOrder !== 0 && index === 0) {
45 | order = alphabetOrder;
46 | reason = 'Alphabet-0';
47 | break;
48 | } else {
49 | if (hasElement !== 0) {
50 | order = hasElement;
51 | reason = 'No element';
52 | break;
53 | } else if (hasParam !== 0) {
54 | order = hasParam;
55 | reason = 'No param';
56 | break;
57 | } else if (hasParam === 0 && indexOfParam !== 0) {
58 | order = indexOfParam;
59 | reason = 'Param index';
60 | break;
61 | } else if (alphabetOrder !== 0) {
62 | order = alphabetOrder;
63 | reason = 'Alphabet';
64 | break;
65 | }
66 | }
67 | index = index + 1;
68 | } while (index < maxIndex);
69 | // console.log(a.path, b.path, order, reason);
70 | return order;
71 | });
72 |
73 | const pathElements = filteredRoutesPaths
74 | .filter((f) => f.path && f.path !== '/')
75 | .map((route) => {
76 | return route.path
77 | .split('/')
78 | .filter((f) => f.length)
79 | .map((m) => destructurePath(m, route));
80 | })
81 | .filter((f) => f.length);
82 |
83 | const validatePathTypes = createValidatePathTypes(pathElements, routesList);
84 | const validateLocalePathTypes = createValidatePathTypes(pathElements, routesList, true);
85 |
86 | return /* typescript */ `
87 |
88 | ${createRoutePathSchema(filteredRoutesPaths)};
89 |
90 | ${returnIfTrue(hasPrefixStrategy, createLocaleRoutePathSchema(filteredRoutesPaths))}
91 |
92 | type ValidStringPath = T extends \`\${string} \${string}\` ? false : T extends '' ? false : true;
93 |
94 | type ValidParam = T extends \`\${infer A}/\${infer B}\`
95 | ? A extends \`\${string} \${string}\`
96 | ? false
97 | : A extends \`?\${string}\`
98 | ? false
99 | : A extends \`\${string} \${string}\`
100 | ? false
101 | : A extends ''
102 | ? B extends ''
103 | ? true
104 | : false
105 | : B extends \`?\${string}\`
106 | ? false
107 | : B extends \`#\${string}\`
108 | ? true
109 | : B extends ''
110 | ? true
111 | : false
112 | : R extends true
113 | ? T extends ''
114 | ? false
115 | : ValidParam
116 | : T extends \`?\${string}\`
117 | ? false
118 | : T extends \`\${string} \${string}\`
119 | ? false
120 | : true;
121 |
122 | type ValidEndOfPath = T extends \`/\`
123 | ? true
124 | : T extends ''
125 | ? true
126 | : T extends \`\${string} \${string}\`
127 | ? false
128 | : T extends \`?\${string}\`
129 | ? true
130 | : T extends \`#\${string}\`
131 | ? true
132 | : false;
133 |
134 | ${validatePathTypes}
135 | ${returnIfTrue(hasPrefixStrategy, validateLocalePathTypes)}
136 |
137 |
138 | export type TypedPathParameter = ValidatePath | RoutePathSchema;
139 | ${returnIfTrue(
140 | hasPrefixStrategy,
141 | `export type TypedLocalePathParameter = ValidateLocalePath | LocaleRoutePathSchema;`
142 | )}
143 |
144 | `;
145 | }
146 |
--------------------------------------------------------------------------------
/src/core/output/generators/files/__routes.file.ts:
--------------------------------------------------------------------------------
1 | import type { GeneratorOutput } from '../../../../types';
2 | import {
3 | createRoutesNamedLocationsExport,
4 | createRoutesNamedLocationsResolvedExport,
5 | createRoutesNamesListExport,
6 | createRoutesParamsRecordExport,
7 | createRoutesParamsRecordResolvedExport,
8 | } from '../blocks';
9 |
10 | export function createRoutesTypesFile({
11 | routesList,
12 | routesObjectTemplate,
13 | routesDeclTemplate,
14 | routesParams,
15 | routesPaths,
16 | }: GeneratorOutput): string {
17 | return /* typescript */ `
18 | ${createRoutesNamesListExport(routesList)}
19 |
20 | ${createRoutesParamsRecordExport(routesParams)}
21 |
22 | ${createRoutesParamsRecordResolvedExport(routesParams)}
23 |
24 | ${createRoutesNamedLocationsExport(routesParams)}
25 |
26 | ${createRoutesNamedLocationsResolvedExport(routesParams)}
27 |
28 | export type RoutesNamesListRecord = ${routesDeclTemplate};
29 |
30 | export const routesNames = ${routesObjectTemplate};
31 | `;
32 | }
33 |
--------------------------------------------------------------------------------
/src/core/output/generators/files/__typed-router.d.file.ts:
--------------------------------------------------------------------------------
1 | import { returnIfTrue } from '../../../../utils';
2 | import { moduleOptionStore } from '../../../config';
3 |
4 | export function createTypedRouterDefinitionFile(): string {
5 | const strictOptions = moduleOptionStore.getResolvedStrictOptions();
6 | const { plugin, autoImport, i18n, pathCheck } = moduleOptionStore;
7 |
8 | return /* typescript */ `
9 |
10 | import type { NuxtLinkProps, PageMeta, NuxtApp } from 'nuxt/app';
11 | import NuxtLink from 'nuxt/dist/app/components/nuxt-link';
12 | import type { RouteLocationRaw, RouteLocationPathRaw, RouteLocation, RouterLinkProps, UseLinkReturn } from 'vue-router';
13 | import type { RoutesNamedLocations, RoutesNamesListRecord, RoutesNamesList } from './__routes';
14 | import type {TypedRouter, TypedRoute, TypedRouteLocationRawFromName, TypedLocationAsRelativeRaw, NuxtRoute} from './__router';
15 | import { useRoute as _useRoute } from './__useTypedRoute';
16 | import { useRouter as _useRouter } from './__useTypedRouter';
17 | import { useLink as _useLink } from './__useTypedLink';
18 | import { navigateTo as _navigateTo } from './__navigateTo';
19 | import type { DefineSetupFnComponent, SlotsType, UnwrapRef, VNode } from 'vue';
20 |
21 | ${returnIfTrue(
22 | i18n,
23 | `import { useLocalePath as _useLocalePath, useLocaleRoute as _useLocaleRoute} from './__i18n-router';
24 | import type {TypedNuxtLinkLocale} from './__NuxtLinkLocale'`
25 | )}
26 |
27 | import {definePageMeta as _definePageMeta} from './__definePageMeta';
28 |
29 | ${returnIfTrue(pathCheck, `import type {TypedPathParameter} from './__paths';`)}
30 |
31 |
32 | declare global {
33 |
34 | ${returnIfTrue(
35 | autoImport,
36 | /* typescript */ `
37 | const useRoute: typeof _useRoute;
38 | const useRouter: typeof _useRouter;
39 | const useLink: typeof _useLink;
40 | const navigateTo: typeof _navigateTo;
41 | const definePageMeta: typeof _definePageMeta;
42 |
43 | ${returnIfTrue(
44 | i18n,
45 | /* typescript */ `
46 | const useLocalePath: typeof _useLocalePath;
47 | const useLocaleRoute: typeof _useLocaleRoute;
48 | `
49 | )}
50 | `
51 | )}
52 | }
53 |
54 | type TypedNuxtLinkProps<
55 | T extends RoutesNamesList,
56 | P extends string,
57 | E extends boolean = false,
58 | CustomProp extends boolean = false> = Omit, 'to' | 'external'> &
59 | {
60 | to: NuxtRoute,
61 | external?: E
62 | }
63 |
64 | type NuxtLinkDefaultSlotProps = CustomProp extends true ? {
65 | href: string;
66 | navigate: (e?: MouseEvent) => Promise;
67 | prefetch: (nuxtApp?: NuxtApp) => Promise;
68 | route: (RouteLocation & {
69 | href: string;
70 | }) | undefined;
71 | rel: string | null;
72 | target: '_blank' | '_parent' | '_self' | '_top' | (string & {}) | null;
73 | isExternal: boolean;
74 | isActive: false;
75 | isExactActive: false;
76 | } : UnwrapRef;
77 |
78 | type NuxtLinkSlots = {
79 | default?: (props: NuxtLinkDefaultSlotProps) => VNode[];
80 | };
81 |
82 |
83 |
84 | export type TypedNuxtLink = (new (props: TypedNuxtLinkProps) => InstanceType, [], SlotsType>>>) & Record
85 |
86 | declare module 'vue' {
87 | interface GlobalComponents {
88 | NuxtLink: TypedNuxtLink;
89 | ${returnIfTrue(i18n, ` NuxtLinkLocale: TypedNuxtLinkLocale;`)}
90 | }
91 | }
92 |
93 | ${returnIfTrue(
94 | plugin,
95 | /* typescript */ `
96 | interface CustomPluginProperties {
97 | $typedRouter: TypedRouter,
98 | $typedRoute: TypedRoute,
99 | $routesNames: RoutesNamesListRecord
100 | }
101 | declare module '#app' {
102 | interface NuxtApp extends CustomPluginProperties {}
103 | }
104 | declare module 'vue' {
105 | interface ComponentCustomProperties extends CustomPluginProperties {}
106 | }
107 | `
108 | )}
109 | `;
110 | }
111 |
--------------------------------------------------------------------------------
/src/core/output/generators/files/__types-utils.d.file.ts:
--------------------------------------------------------------------------------
1 | export function createTypeUtilsRuntimeFile() {
2 | return /* typescript */ `
3 |
4 | import { RoutesNamesList, RoutesParamsRecord } from './__routes';
5 |
6 | // - Type utils
7 | export type ExtractRequiredParameters> = Pick<
8 | T,
9 | { [K in keyof T]: undefined extends T[K] ? never : K }[keyof T]
10 | >;
11 |
12 | export type HasOneRequiredParameter = [RoutesParamsRecord[T]] extends [
13 | never
14 | ]
15 | ? false
16 | : [keyof ExtractRequiredParameters] extends [undefined]
17 | ? false
18 | : true;
19 | `;
20 | }
21 |
--------------------------------------------------------------------------------
/src/core/output/generators/files/__useTypedLink.file.ts:
--------------------------------------------------------------------------------
1 | import { returnIfTrue } from '../../../../utils';
2 | import { moduleOptionStore } from '../../../config';
3 |
4 | export function createUseTypedLinkFile(): string {
5 | const strictOptions = moduleOptionStore.getResolvedStrictOptions();
6 | const { pathCheck } = moduleOptionStore;
7 |
8 | return /* typescript */ `
9 |
10 | import { useLink as defaultLink } from '#imports';
11 | import type {MaybeRef, Ref} from 'vue';
12 | import type { NavigateToOptions } from 'nuxt/dist/app/composables/router';
13 | import type { NavigationFailure } from 'vue-router';
14 | import type { TypedRouteLocationRawFromName, TypedRouteFromName, TypedRoute } from './__router';
15 | import type { RoutesNamesList } from './__routes';
16 | ${returnIfTrue(
17 | pathCheck,
18 | `import type {TypedPathParameter, RouteNameFromPath} from './__paths';`
19 | )}
20 |
21 |
22 | type LinkedRoute = {
23 | route: ComputedRef & {
24 | href: string;
25 | }>;
26 | href: ComputedRef;
27 | isActive: ComputedRef;
28 | isExactActive: ComputedRef;
29 | navigate: (e?: MouseEvent) => Promise;
30 | };
31 |
32 |
33 | interface UseLinkFunction {
34 | (props: {
35 | to: TypedRouteLocationRawFromName;
36 | replace?: MaybeRef;
37 | }): LinkedRoute;
38 | (props: {
39 | to: Ref>;
40 | replace?: MaybeRef;
41 | }): LinkedRoute;
42 |
43 | ${returnIfTrue(
44 | pathCheck && !strictOptions.router.strictToArgument,
45 | `(
46 | props: {
47 | to: TypedPathParameter
,
48 | replace?: MaybeRef
49 | }
50 | ) : LinkedRoute>
51 | (props: {
52 | to: Ref>;
53 | replace?: MaybeRef;
54 | }): LinkedRoute>;
55 |
56 | `
57 | )}
58 | }
59 |
60 | /**
61 | * Typed clone of \`useLink\`
62 | *
63 | * @exemple
64 | *
65 | * \`\`\`ts
66 | * const router = useLink(props);
67 | * \`\`\`
68 | */
69 | export const useLink: UseLinkFunction = defaultLink as any;
70 |
71 | `;
72 | }
73 |
--------------------------------------------------------------------------------
/src/core/output/generators/files/__useTypedRoute.file.ts:
--------------------------------------------------------------------------------
1 | export function createUseTypedRouteFile(): string {
2 | return /* typescript */ `
3 | import { useRoute as defaultRoute } from '#imports';
4 | import type { RoutesNamesList } from './__routes';
5 | import type {TypedRoute, TypedRouteFromName} from './__router'
6 |
7 | /**
8 | * Typed clone of \`useRoute\`
9 | *
10 | * @exemple
11 | *
12 | * \`\`\`ts
13 | * const route = useRoute();
14 | * \`\`\`
15 | *
16 | * \`\`\`ts
17 | * const route = useRoute('my-route-with-param-id');
18 | * route.params.id // autocompletes!
19 | * \`\`\`
20 | *
21 | * \`\`\`ts
22 | * const route = useRoute();
23 | * if (route.name === 'my-route-with-param-id') {
24 | * route.params.id // autocompletes!
25 | * }
26 | * \`\`\`
27 | */
28 | export function useRoute(
29 | name?: T
30 | ): [T] extends [never] ? TypedRoute : TypedRouteFromName {
31 | const route = defaultRoute();
32 |
33 | return route as any;
34 | }
35 | `;
36 | }
37 |
--------------------------------------------------------------------------------
/src/core/output/generators/files/__useTypedRouter.file.ts:
--------------------------------------------------------------------------------
1 | export function createUseTypedRouterFile(): string {
2 | return /* typescript */ `
3 |
4 | import { useRouter as defaultRouter } from '#imports';
5 | import type { TypedRouter } from './__router';
6 |
7 | /**
8 | * Typed clone of \`useRouter\`
9 | *
10 | * @exemple
11 | *
12 | * \`\`\`ts
13 | * const router = useRouter();
14 | * \`\`\`
15 | */
16 | export function useRouter(): TypedRouter {
17 | const router = defaultRouter();
18 |
19 | return router;
20 | };
21 |
22 | `;
23 | }
24 |
--------------------------------------------------------------------------------
/src/core/output/generators/files/index.file.ts:
--------------------------------------------------------------------------------
1 | import { returnIfTrue } from '../../../../utils';
2 | import { moduleOptionStore } from '../../../config';
3 |
4 | export function createIndexFile(): string {
5 | const { i18n, i18nOptions, pathCheck } = moduleOptionStore;
6 | const hasPrefixStrategy = i18n && i18nOptions?.strategy !== 'no_prefix';
7 |
8 | return /* typescript */ `
9 |
10 | export type {
11 | TypedLocationAsRelativeRaw,
12 | TypedResolvedMatcherLocation,
13 | TypedRoute,
14 | TypedRouteFromName,
15 | TypedRouteLocation,
16 | TypedRouteLocationFromName,
17 | TypedRouteLocationRaw,
18 | TypedRouteLocationRawFromName,
19 | TypedRouter,
20 | NuxtRoute
21 | } from './__router';
22 | export { routesNames } from './__routes';
23 | export type {
24 | RoutesNamedLocations,
25 | RoutesNamedLocationsResolved,
26 | RoutesNamesList,
27 | RoutesNamesListRecord,
28 | RoutesParamsRecord,
29 | } from './__routes';
30 | export { useRoute } from './__useTypedRoute';
31 | export { useRouter } from './__useTypedRouter';
32 | export { useLink } from './__useTypedLink';
33 | export { navigateTo } from './__navigateTo';
34 | export { definePageMeta } from './__definePageMeta';
35 | export { helpers } from './__helpers';
36 |
37 | ${returnIfTrue(
38 | pathCheck,
39 | `export type { ValidatePath, RoutePathSchema, TypedPathParameter, RouteNameFromPath, ${returnIfTrue(
40 | hasPrefixStrategy,
41 | `TypedLocalePathParameter`
42 | )} } from './__paths';`
43 | )}
44 | ${returnIfTrue(
45 | i18n,
46 | `export {useLocalePath, useLocaleRoute} from './__i18n-router';
47 | export type {TypedToLocalePath, TypedLocaleRoute, I18nLocales} from './__i18n-router';`
48 | )}
49 |
50 |
51 | `;
52 | }
53 |
--------------------------------------------------------------------------------
/src/core/output/generators/files/index.ts:
--------------------------------------------------------------------------------
1 | export * from './__routes.file';
2 | export * from './__router.d.file';
3 | export * from './__typed-router.d.file';
4 | export * from './index.file';
5 | export * from './plugin.file';
6 | export * from './__useTypedRoute.file';
7 | export * from './__useTypedRouter.file';
8 | export * from './__useTypedLink.file';
9 | export * from './__navigateTo.file';
10 | export * from './__types-utils.d.file';
11 | export * from './__i18n-router.file';
12 | export * from './__paths.file';
13 | export * from './__definePageMeta.file';
14 | export * from './__helpers.file';
15 | export * from './__NuxtLinkLocale.file';
16 |
--------------------------------------------------------------------------------
/src/core/output/generators/files/plugin.file.ts:
--------------------------------------------------------------------------------
1 | export function createPluginFile(): string {
2 | return /* typescript */ `
3 |
4 | import { defineNuxtPlugin, useRouter, useRoute } from '#imports';
5 | import {TypedRouter, TypedRoute, routesNames} from '@typed-router';
6 |
7 | export default defineNuxtPlugin(() => {
8 | const router = useRouter();
9 | const route = useRoute();
10 |
11 | return {
12 | provide: {
13 | typedRouter: router as TypedRouter,
14 | typedRoute: route as TypedRoute,
15 | routesNames,
16 | },
17 | };
18 | });
19 | `;
20 | }
21 |
--------------------------------------------------------------------------------
/src/core/output/generators/index.ts:
--------------------------------------------------------------------------------
1 | export * from './files';
2 | export * from './blocks';
3 |
--------------------------------------------------------------------------------
/src/core/output/index.ts:
--------------------------------------------------------------------------------
1 | export * from './fileSave';
2 |
--------------------------------------------------------------------------------
/src/core/output/static/index.ts:
--------------------------------------------------------------------------------
1 | export * from './watermark.template';
2 |
--------------------------------------------------------------------------------
/src/core/output/static/watermark.template.ts:
--------------------------------------------------------------------------------
1 | export const watermarkTemplate = `
2 | // @ts-nocheck
3 | // eslint-disable
4 | // ---------------------------------------------------
5 | // 🚗🚦 Generated by nuxt-typed-router. Do not modify !
6 | // ---------------------------------------------------
7 |
8 |
9 |
10 | `;
11 |
--------------------------------------------------------------------------------
/src/core/parser/base.ts:
--------------------------------------------------------------------------------
1 | import type { NuxtPage } from '@nuxt/schema';
2 | import type { GeneratorOutput, RouteParamsDecl, RoutePathsDecl } from '../../types';
3 | import { isItemLast } from '../../utils';
4 | import { walkThoughRoutes } from './walkRoutes';
5 |
6 | export function constructRouteMap(routesConfig: NuxtPage[]): GeneratorOutput {
7 | try {
8 | let routesObjectTemplate = '{';
9 | let routesDeclTemplate = '{';
10 | let routesList: string[] = [];
11 | let routesParams: RouteParamsDecl[] = [];
12 | let routesPaths: RoutePathsDecl[] = [];
13 |
14 | const output = {
15 | routesObjectTemplate,
16 | routesDeclTemplate,
17 | routesList,
18 | routesParams,
19 | routesPaths,
20 | };
21 |
22 | startGenerator({
23 | output,
24 | routesConfig,
25 | });
26 |
27 | return output;
28 | } catch (e) {
29 | throw new Error(`Generation failed: ${e}`);
30 | }
31 | }
32 |
33 | type StartGeneratorParams = {
34 | output: GeneratorOutput;
35 | routesConfig: NuxtPage[];
36 | };
37 |
38 | export function startGenerator({ output, routesConfig }: StartGeneratorParams): void {
39 | routesConfig.forEach((route, index) => {
40 | const rootSiblingsRoutes = routesConfig.filter((rt) => rt.path !== route.path);
41 | walkThoughRoutes({
42 | route,
43 | level: 0,
44 | output,
45 | siblings: rootSiblingsRoutes,
46 | isLast: isItemLast(routesConfig, index),
47 | isLocale: false,
48 | });
49 | });
50 | output.routesObjectTemplate += '}';
51 | output.routesDeclTemplate += '}';
52 | }
53 |
--------------------------------------------------------------------------------
/src/core/parser/extractChunks.ts:
--------------------------------------------------------------------------------
1 | import type { NuxtPage } from '@nuxt/schema';
2 |
3 | export function extractUnMatchingSiblings(
4 | mainRoute: NuxtPage,
5 | siblingRoutes?: NuxtPage[]
6 | ): NuxtPage[] | undefined {
7 | return siblingRoutes?.filter((s) => {
8 | return s.name !== mainRoute.name;
9 | });
10 | }
11 |
--------------------------------------------------------------------------------
/src/core/parser/i18n.modifiers.ts:
--------------------------------------------------------------------------------
1 | import type { NuxtPage } from '@nuxt/schema';
2 | import { moduleOptionStore } from '../config';
3 | import type { RoutePathsDecl } from '../../types';
4 | import logSymbols from 'log-symbols';
5 |
6 | const specialCharacterRegxp = /([^a-zA-Z0-9_])/gm;
7 |
8 | /** Will check if the is a route generated by @nuxtjs/i18n */
9 | export function is18Sibling(source: RoutePathsDecl[], route: NuxtPage) {
10 | const { i18n, i18nOptions, i18nLocales } = moduleOptionStore;
11 | if (i18n && i18nOptions && i18nOptions?.strategy !== 'no_prefix') {
12 | const i18LocalesRecognizer = i18nLocales
13 | ?.map((m) => m.replace(specialCharacterRegxp, '\\$&'))
14 | .join('|');
15 |
16 | return !!route.path?.match(new RegExp(`^/?(${i18LocalesRecognizer})(/.*)?$`, 'g'));
17 | }
18 | return false;
19 | }
20 |
21 | export function modifyRoutePrefixDefaultIfI18n(route: NuxtPage) {
22 | const { i18n, i18nOptions, i18nLocales } = moduleOptionStore;
23 | if (i18n && i18nOptions && route.name) {
24 | const separator = i18nOptions?.routesNameSeparator ?? '___';
25 | const i18LocalesRecognizer = i18nLocales
26 | ?.map((m) => m.replace(specialCharacterRegxp, '\\$&'))
27 | .join('|');
28 | if (i18nOptions?.strategy === 'prefix_and_default') {
29 | const routeDefaultRegXp = new RegExp(
30 | `([a-zA-Z0-9-]+)${separator}(${i18LocalesRecognizer})${separator}default`,
31 | 'g'
32 | );
33 | const match = routeDefaultRegXp.exec(route.name);
34 | if (match) {
35 | const [_, routeName] = match;
36 | route.name = routeName;
37 | return {
38 | ...route,
39 | name: routeName,
40 | };
41 | }
42 | } else if (i18nOptions?.strategy === 'prefix_except_default') {
43 | let defaultLocale = i18nLocales.find((f) => f === i18nOptions.defaultLocale)
44 | ? i18nOptions.defaultLocale?.replace(specialCharacterRegxp, '\\$&')
45 | : undefined;
46 |
47 | const routeDefaultNameRegXp = new RegExp(`^([a-zA-Z0-9-]+)${separator}${defaultLocale}`, 'g');
48 | const match = routeDefaultNameRegXp.exec(route.name);
49 | if (match) {
50 | const [_, routeName] = match;
51 | return {
52 | ...route,
53 | name: routeName,
54 | };
55 | }
56 | }
57 | }
58 | return route;
59 | }
60 |
--------------------------------------------------------------------------------
/src/core/parser/index.ts:
--------------------------------------------------------------------------------
1 | export * from './base';
2 |
--------------------------------------------------------------------------------
/src/core/parser/params/destructurePath.ts:
--------------------------------------------------------------------------------
1 | import { nanoid } from 'nanoid/non-secure';
2 | import type { RoutePathsDecl } from '../../../../src/types';
3 |
4 | const ExtractRegex = /(^(\/)?([^:/]+)?(:(\w+)(\((.*)\)[*+]?)?(\?)?)*([^:/]+)?)+/g;
5 | export type DestructuredPath = {
6 | type: 'name' | 'param' | 'optionalParam' | 'catchAll';
7 | content: string;
8 | fullPath?: string;
9 | id: string;
10 | routeName: string;
11 | isLocale: boolean;
12 | };
13 |
14 | export function destructurePath(path: string, route: RoutePathsDecl): DestructuredPath[] {
15 | let allPathElements: DestructuredPath[] = [];
16 | let _path = `${path}`;
17 | do {
18 | const { pathElements, strippedPath } = extractPathElements(_path, route);
19 | allPathElements = allPathElements.concat(pathElements);
20 | _path = _path.replace(strippedPath, '');
21 | } while (_path.length);
22 |
23 | return allPathElements;
24 | }
25 |
26 | function extractPathElements(partOfPath: string, route: RoutePathsDecl) {
27 | let pathElements: DestructuredPath[] = [];
28 | let strippedPath = '';
29 | let matches: RegExpExecArray | null;
30 | matches = ExtractRegex.exec(partOfPath);
31 | if (matches) {
32 | const [_, mtch, slash, path1, paramDef, key, catchAll, parentheseContent, optional, path2] =
33 | matches;
34 | if (mtch) {
35 | strippedPath = mtch;
36 |
37 | const sharedProperties = {
38 | fullPath: route.path,
39 | routeName: route.name!,
40 | isLocale: route.isLocale,
41 | };
42 | if (path1) {
43 | pathElements.push({
44 | type: 'name',
45 | content: path1,
46 | id: nanoid(6),
47 | ...sharedProperties,
48 | });
49 | }
50 | if (key) {
51 | pathElements.push({
52 | type: catchAll && parentheseContent ? 'catchAll' : optional ? 'optionalParam' : 'param',
53 | content: key,
54 | id: nanoid(6),
55 | ...sharedProperties,
56 | });
57 | }
58 | if (path2) {
59 | pathElements.push({
60 | type: 'name',
61 | content: path2,
62 | id: nanoid(6),
63 | ...sharedProperties,
64 | });
65 | }
66 | }
67 | }
68 |
69 | return { pathElements, strippedPath };
70 | }
71 |
--------------------------------------------------------------------------------
/src/core/parser/params/extractParams.ts:
--------------------------------------------------------------------------------
1 | import type { ParamDecl } from '../../../types';
2 | import { extractParamsFromPathDecl } from './replaceParams';
3 |
4 | export function extractRouteParamsFromPath(
5 | path: string,
6 | isIndexFileForRouting: boolean,
7 | previousParams?: ParamDecl[]
8 | ): ParamDecl[] {
9 | const params = extractParamsFromPathDecl(path);
10 |
11 | let allMergedParams = params.map(
12 | ({ name, optional, catchAll }): ParamDecl => ({
13 | key: name,
14 | required: !optional,
15 | notRequiredOnPage: optional,
16 | catchAll,
17 | })
18 | );
19 | if (previousParams?.length) {
20 | allMergedParams = previousParams
21 | .map((m) => ({ ...m, required: false }))
22 | .concat(allMergedParams);
23 | }
24 | if (!params.length && isIndexFileForRouting) {
25 | const lastItem = allMergedParams[allMergedParams.length - 1];
26 | if (lastItem) {
27 | lastItem.required = true;
28 | }
29 | }
30 | return allMergedParams;
31 | }
32 |
--------------------------------------------------------------------------------
/src/core/parser/params/index.ts:
--------------------------------------------------------------------------------
1 | export * from './extractParams';
2 | export * from './replaceParams';
3 | export * from './destructurePath';
4 |
--------------------------------------------------------------------------------
/src/core/parser/params/replaceParams.ts:
--------------------------------------------------------------------------------
1 | const routeParamExtractRegxp = /(:(\w+)(\(\.[^(]\)[*+]?)?(\?)?)+/g;
2 |
3 | type ExtractedParam = { name: string; optional: boolean; catchAll: boolean };
4 |
5 | export function extractParamsFromPathDecl(path: string): ExtractedParam[] {
6 | let params: ExtractedParam[] = [];
7 | let matches: RegExpExecArray | null;
8 | do {
9 | matches = routeParamExtractRegxp.exec(path);
10 | if (matches) {
11 | const [_, mtch, key, catchAll, optional] = matches;
12 | if (mtch && key) {
13 | const _param = {
14 | name: key,
15 | optional: !!optional,
16 | catchAll: !!catchAll,
17 | } satisfies ExtractedParam;
18 | params.push(_param);
19 | }
20 | }
21 | } while (matches);
22 |
23 | return params;
24 | }
25 |
--------------------------------------------------------------------------------
/src/core/parser/removeNuxtDefs.ts:
--------------------------------------------------------------------------------
1 | import { readFile } from 'fs/promises';
2 | import { existsSync } from 'fs';
3 | import { createResolver } from '@nuxt/kit';
4 | import { processPathAndWriteFile } from '../fs';
5 |
6 | type RemoveNuxtDefinitionsOptions = {
7 | buildDir: string;
8 | autoImport: boolean;
9 | };
10 |
11 | export async function removeNuxtDefinitions({
12 | buildDir,
13 | autoImport,
14 | }: RemoveNuxtDefinitionsOptions): Promise {
15 | const { resolve } = createResolver(import.meta.url);
16 |
17 | // Remove NuxtLink from .nuxt/components.d.ts
18 | const componentFilePath = resolve(buildDir, 'components.d.ts');
19 | if (existsSync(componentFilePath)) {
20 | const componentDefinitions = await readFile(componentFilePath, {
21 | encoding: 'utf8',
22 | });
23 | const replacedNuxtLink = componentDefinitions.replace(
24 | /'NuxtLink': typeof import\(".*"\)\['default'\]|'NuxtLinkLocale': typeof import\(".*"\)\['default'\]/gm,
25 | ''
26 | );
27 |
28 | processPathAndWriteFile({
29 | content: replacedNuxtLink,
30 | fileName: 'components.d.ts',
31 | outDir: '.nuxt',
32 | });
33 | }
34 |
35 | // Remove global imports from .nuxt/types/imports.d.ts
36 |
37 | if (autoImport) {
38 | const importsFilePath = resolve(buildDir, 'types/imports.d.ts');
39 | if (existsSync(importsFilePath)) {
40 | let globalDefinitions = await readFile(importsFilePath, {
41 | encoding: 'utf8',
42 | });
43 |
44 | const importsToRemove = [
45 | 'useRouter',
46 | 'useRoute',
47 | 'useLocalePath',
48 | 'useLocaleRoute',
49 | 'definePageMeta',
50 | 'navigateTo',
51 | ].map((m) => new RegExp(`const ${m}: typeof import\\('.*'\\)\\['${m}'\\]`, 'gm'));
52 |
53 | importsToRemove.forEach((imp) => {
54 | globalDefinitions = globalDefinitions.replace(imp, '');
55 | });
56 |
57 | processPathAndWriteFile({
58 | content: globalDefinitions,
59 | fileName: 'types/imports.d.ts',
60 | outDir: '.nuxt',
61 | });
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/core/parser/walkRoutes.ts:
--------------------------------------------------------------------------------
1 | import type { NuxtPage } from '@nuxt/schema';
2 | import { camelCase } from 'lodash-es';
3 | import type { GeneratorOutput, ParamDecl } from '../../types';
4 | import { isItemLast } from '../../utils';
5 | import { moduleOptionStore } from '../config';
6 | import { extractUnMatchingSiblings } from './extractChunks';
7 | import { is18Sibling, modifyRoutePrefixDefaultIfI18n } from './i18n.modifiers';
8 | import { extractRouteParamsFromPath } from './params';
9 |
10 | type WalkThoughRoutesParams = {
11 | route: NuxtPage;
12 | level: number;
13 | siblings?: NuxtPage[];
14 | parent?: NuxtPage;
15 | previousParams?: ParamDecl[];
16 | output: GeneratorOutput;
17 | isLast: boolean;
18 | isLocale: boolean;
19 | };
20 |
21 | function createKeyedName(route: NuxtPage, parent?: NuxtPage): string {
22 | const splittedPaths = route.path.split('/');
23 | const parentPath = splittedPaths[splittedPaths.length - 1];
24 | if (parent) {
25 | return camelCase(parentPath || 'index');
26 | } else {
27 | return camelCase(route.path.split('/').join('-')) || 'index';
28 | }
29 | }
30 |
31 | function createNameKeyFromFullName(route: NuxtPage, level: number, parentName?: string): string {
32 | let splitted: string[] = [];
33 | splitted = route.name?.split('-') ?? [];
34 | splitted = splitted.slice(level, splitted.length);
35 | if (splitted[0] === parentName) {
36 | splitted.splice(0, 1);
37 | }
38 |
39 | const keyName = route.path === '' ? 'index' : camelCase(splitted.join('-')) || 'index';
40 |
41 | return keyName;
42 | }
43 |
44 | /** Mutates the output object with generated routes */
45 | export function walkThoughRoutes({
46 | route: _route,
47 | level,
48 | siblings,
49 | parent,
50 | previousParams,
51 | output,
52 | isLast,
53 | isLocale,
54 | }: WalkThoughRoutesParams) {
55 | const route = modifyRoutePrefixDefaultIfI18n(_route);
56 | const isLocaleRoute = isLocale || is18Sibling(output.routesPaths, route);
57 |
58 | if (route.file && moduleOptionStore.resolvedIgnoredRoutes.includes(route.file)) {
59 | return;
60 | }
61 |
62 | const newPath = `${parent?.path ?? ''}${
63 | route.path.startsWith('/') || parent?.path === '/' ? route.path : `/${route.path}`
64 | }`;
65 |
66 | if (parent?.path !== '/' || newPath !== parent?.path) {
67 | output.routesPaths.push({
68 | name: route.name,
69 | path: newPath,
70 | isLocale: isLocaleRoute,
71 | });
72 | }
73 |
74 | // Filter routes added by i18n module
75 | if (route.children?.length) {
76 | // - Route with children
77 |
78 | let childrenChunks = route.children;
79 | let nameKey = createKeyedName(route, parent);
80 | const allRouteParams = extractRouteParamsFromPath(route.path, false, previousParams);
81 |
82 | const newRoute = { ...route, name: nameKey, path: newPath } satisfies NuxtPage;
83 |
84 | if (!isLocaleRoute) {
85 | // Output
86 | output.routesObjectTemplate += `${nameKey}:{`;
87 | output.routesDeclTemplate += `"${nameKey}":{`;
88 | }
89 |
90 | // Recursive walk though children
91 | childrenChunks?.map((routeConfig, index) =>
92 | walkThoughRoutes({
93 | route: routeConfig,
94 | level: level + 1,
95 | siblings: extractUnMatchingSiblings(route, siblings),
96 | parent: newRoute,
97 | previousParams: allRouteParams,
98 | output,
99 | isLast: isItemLast(childrenChunks, index),
100 | isLocale: isLocaleRoute,
101 | })
102 | );
103 | if (!isLocaleRoute) {
104 | output.routesObjectTemplate += '},';
105 | output.routesDeclTemplate += `}${isLast ? '' : ','}`;
106 | }
107 |
108 | // Output
109 | } else if (route.name && !isLocaleRoute) {
110 | // - Single route
111 |
112 | let keyName = createNameKeyFromFullName(route, level, parent?.name);
113 |
114 | output.routesObjectTemplate += `'${keyName}': '${route.name}' as const,`;
115 | output.routesDeclTemplate += `"${keyName}": "${route.name}"${isLast ? '' : ','}`;
116 | output.routesList.push(route.name);
117 |
118 | // Params
119 | const isIndexFileForRouting = route.path === '';
120 | const allRouteParams = extractRouteParamsFromPath(
121 | route.path,
122 | isIndexFileForRouting,
123 | previousParams
124 | );
125 | output.routesParams.push({
126 | name: route.name,
127 | params: allRouteParams,
128 | });
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/src/module.ts:
--------------------------------------------------------------------------------
1 | import { defineNuxtModule, createResolver, addTemplate } from '@nuxt/kit';
2 | import type { Nuxt } from '@nuxt/schema';
3 | import type { NuxtI18nOptions } from '@nuxtjs/i18n';
4 | import { createTypedRouter } from './core';
5 | import { moduleOptionStore } from './core/config';
6 | import type { ModuleOptions } from './types';
7 | import { removeNuxtDefinitions } from './core/parser/removeNuxtDefs';
8 | export type { ModuleOptions } from './types';
9 |
10 | export default defineNuxtModule({
11 | meta: {
12 | name: 'nuxt-typed-router',
13 | configKey: 'nuxtTypedRouter',
14 | compatibility: { nuxt: '>=3.0.0', bridge: false },
15 | },
16 | defaults: {
17 | plugin: false,
18 | strict: false,
19 | pathCheck: true,
20 | disablePrettier: false,
21 | removeNuxtDefs: true,
22 | ignoreRoutes: [],
23 | },
24 | setup(moduleOptions, nuxt: Nuxt) {
25 | const { resolve } = createResolver(import.meta.url);
26 |
27 | const rootDir = nuxt.options.rootDir;
28 | let i18nOptions: NuxtI18nOptions | null = null;
29 |
30 | const hasi18nModuleRegistered = nuxt.options.modules.some((mod) => {
31 | if (Array.isArray(mod)) {
32 | const [moduleName, options] = mod;
33 | const isRegistered = moduleName === '@nuxtjs/i18n';
34 | if (isRegistered) {
35 | i18nOptions = options;
36 | }
37 | return isRegistered;
38 | } else {
39 | const isRegistered = mod === '@nuxtjs/i18n';
40 | if (isRegistered) {
41 | i18nOptions = (nuxt.options as any).i18n;
42 | }
43 | return isRegistered;
44 | }
45 | });
46 |
47 | const isDocumentDriven =
48 | !!nuxt.options.modules.find((mod) => {
49 | if (Array.isArray(mod)) {
50 | return mod[0] === '@nuxt/content';
51 | } else {
52 | return mod === '@nuxt/content';
53 | }
54 | }) &&
55 | 'content' in nuxt.options &&
56 | 'documentDriven' in (nuxt.options.content as any);
57 |
58 | moduleOptionStore.updateOptions({
59 | ...moduleOptions,
60 | i18n: hasi18nModuleRegistered,
61 | i18nOptions,
62 | isDocumentDriven,
63 | });
64 |
65 | nuxt.options.alias = {
66 | ...nuxt.options.alias,
67 | '@typed-router': resolve(`${rootDir}/.nuxt/typed-router`),
68 | };
69 |
70 | // Force register of type declaration
71 | nuxt.hook('prepare:types', (options) => {
72 | options.tsConfig.include?.unshift('./typed-router/typed-router.d.ts');
73 | if (moduleOptions.removeNuxtDefs) {
74 | removeNuxtDefinitions({
75 | autoImport: nuxt.options.imports.autoImport ?? true,
76 | buildDir: nuxt.options.buildDir,
77 | });
78 | }
79 | });
80 |
81 | nuxt.hook('build:done', () => {
82 | if (moduleOptions.removeNuxtDefs) {
83 | removeNuxtDefinitions({
84 | autoImport: nuxt.options.imports.autoImport ?? true,
85 | buildDir: nuxt.options.buildDir,
86 | });
87 | }
88 | });
89 |
90 | if (nuxt.options.dev) {
91 | nuxt.hook('devtools:customTabs' as any, (tabs: any[]) => {
92 | tabs.push({
93 | name: 'nuxt-typed-router',
94 | title: 'Nuxt Typed Router',
95 | icon: 'https://github.com/victorgarciaesgi/nuxt-typed-router/blob/master/.github/images/logo.png?raw=true',
96 | view: {
97 | type: 'iframe',
98 | src: 'https://nuxt-typed-router.vercel.app/',
99 | },
100 | });
101 | });
102 | }
103 |
104 | createTypedRouter({ nuxt });
105 | },
106 | });
107 |
--------------------------------------------------------------------------------
/src/types/config.types.ts:
--------------------------------------------------------------------------------
1 | export interface ModuleOptions {
2 | /**
3 | *
4 | * Enables path autocomplete and path validity for programmatic validation
5 | *
6 | * @default true
7 | */
8 | pathCheck?: boolean;
9 | /**
10 | * Set to false if you don't want a plugin generated
11 | * @default false
12 | */
13 | plugin?: boolean;
14 | /**
15 | * Customise Route location arguments strictness for `NuxtLink` or `router`
16 | * All strict options are disabled by default.
17 | * You can tweak options to add strict router navigation options.
18 | *
19 | * By passing `true` you can enable all of them
20 | *
21 | * @default false
22 | */
23 | strict?: boolean | StrictOptions;
24 | /**
25 | * Remove Nuxt definitions to avoid conflicts
26 | * @default true
27 | */
28 | removeNuxtDefs?: boolean;
29 | /**
30 | * ⚠️ Experimental
31 | *
32 | * Exclude certain routes from being included into the generated types
33 | * Ex: 404 routes or catchAll routes
34 | */
35 | ignoreRoutes?: string[];
36 | /**
37 | * Disable prettier formatter
38 | * @default false
39 | */
40 | disablePrettier?: boolean;
41 | }
42 |
43 | export interface StrictOptions {
44 | NuxtLink?: StrictParamsOptions;
45 | router?: StrictParamsOptions;
46 | }
47 |
48 | export interface StrictParamsOptions {
49 | /**
50 | * Prevent passing string path to the RouteLocation argument.
51 | *
52 | * Ex:
53 | * ```vue
54 | *
55 | * // Error ❌
56 | *
57 | * ```
58 | * Or
59 | * ```ts
60 | * router.push('/login'); // Error ❌
61 | * navigateTo('/login'); // Error ❌
62 | * ```
63 | *
64 | * @default false
65 | */
66 | strictToArgument?: boolean;
67 | /**
68 | * Prevent passing a `params` property in the RouteLocation argument.
69 | *
70 | * Ex:
71 | * ```vue
72 | *
73 | * // Error ❌
74 | *
75 | * ```
76 | * Or
77 | * ```ts
78 | * router.push({path: "/login"}); // Error ❌
79 | * navigateTo({path: "/login"}); // Error ❌
80 | * ```
81 | *
82 | * @default false
83 | */
84 | strictRouteLocation?: boolean;
85 | }
86 |
--------------------------------------------------------------------------------
/src/types/generator.types.ts:
--------------------------------------------------------------------------------
1 | export interface ParamDecl {
2 | key: string;
3 | required: boolean;
4 | notRequiredOnPage: boolean;
5 | catchAll: boolean;
6 | }
7 |
8 | export interface RouteParamsDecl {
9 | name: string;
10 | params: ParamDecl[];
11 | }
12 |
13 | export interface RoutePathsDecl {
14 | path: string;
15 | name?: string;
16 | isLocale: boolean;
17 | }
18 |
19 | export interface GeneratorOutput {
20 | /** String template of the exported route object of `__routes.ts` file (contains `as const`) */
21 | routesObjectTemplate: string;
22 | /** String template of the injected $routeList in Nuxt plugin */
23 | routesDeclTemplate: string;
24 | /** String array of the all the routes for the Union type */
25 | routesList: string[];
26 | /** Array of RouteParams mapping with routeList */
27 | routesParams: RouteParamsDecl[];
28 | routesPaths: RoutePathsDecl[];
29 | }
30 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './config.types';
2 | export * from './generator.types';
3 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './misc.utils';
2 |
--------------------------------------------------------------------------------
/src/utils/misc.utils.ts:
--------------------------------------------------------------------------------
1 | export function isItemLast(array: any[] | undefined, index: number): boolean {
2 | return array ? index === array.length - 1 : false;
3 | }
4 |
5 | export function returnIfTrue(condition: boolean | undefined, template: string, otherwise?: string) {
6 | if (condition) {
7 | return template;
8 | }
9 | return otherwise ?? '';
10 | }
11 |
12 | export function returnIfFalse(
13 | condition: boolean | undefined,
14 | template: string,
15 | otherwise?: string
16 | ) {
17 | if (!condition) {
18 | return template;
19 | }
20 | return otherwise ?? '';
21 | }
22 |
--------------------------------------------------------------------------------
/test/README.md:
--------------------------------------------------------------------------------
1 | # Tests
2 |
3 | Tests are divided in multiple ways
4 |
5 | - e2e
6 | - fixtures unit test for types with `vue-tsc`
7 | - Unit test with `vitest typecheck`
8 |
9 |
10 | Files to update for testing:
11 |
12 | - e2e/base.spec.ts
13 | - fixtures/simple/components/*.vue
14 | - fixtures/simple/tests/*.ts
15 | - fixtures/complex/components/*.vue
16 | - fixtures/complex/tests/*.ts
17 |
18 |
19 | Cannot use pnpm workspaces because of a Nuxt bug with workspaces
20 |
21 | `simple` fixture repo is for a vanilla config project.
22 |
23 | `complex` fixture is for a heavily modified config project (like plugin, srcDir modified etc..)
--------------------------------------------------------------------------------
/test/e2e/complex/complex.spec.ts:
--------------------------------------------------------------------------------
1 | import { fileURLToPath } from 'node:url';
2 | import { describe, it, expect } from 'vitest';
3 | import { setup, $fetch, createPage } from '@nuxt/test-utils/e2e';
4 | import { expectNoClientErrors } from '../utils';
5 | import { timeout } from '$$/utils';
6 |
7 | const TIME = 2000;
8 |
9 | describe('Complex config behaviour', async () => {
10 | await setup({
11 | rootDir: fileURLToPath(new URL('../../fixtures/complex', import.meta.url)),
12 | setupTimeout: 120000,
13 | dev: true,
14 | });
15 |
16 | it('should display the root page without error', async () => {
17 | const html = await $fetch('/');
18 |
19 | expect(html).toContain('Navigate button');
20 | expect(html).toContain('Navigate link');
21 | expect(html).toContain('NavigateTo button');
22 | expect(html).toContain('Navigate plugin');
23 |
24 | await expectNoClientErrors('/');
25 | });
26 |
27 | // // Commented for now because of a Nuxt bug still happening to me
28 |
29 | // it('should navigate correctly with useRouter', async () => {
30 | // const page = await createPage('/');
31 | // await page.click('#useRouter');
32 | // const html = await page.innerHTML('body');
33 | // await timeout(TIME);
34 |
35 | // await expectNoClientErrors('/');
36 | // });
37 |
38 | // it('should navigate correctly with nuxtLink', async () => {
39 | // const page = await createPage('/');
40 | // await page.click('#nuxtLink');
41 |
42 | // await timeout(TIME);
43 | // const html = await page.innerHTML('body');
44 |
45 | // await expectNoClientErrors('/');
46 | // });
47 |
48 | // it('should navigate correctly with navigateTo', async () => {
49 | // const page = await createPage('/');
50 | // await page.click('#navigateTo');
51 | // const html = await page.innerHTML('body');
52 | // await timeout(TIME);
53 |
54 | // await expectNoClientErrors('/');
55 | // });
56 | });
57 |
--------------------------------------------------------------------------------
/test/e2e/simple/simple.spec.ts:
--------------------------------------------------------------------------------
1 | import { fileURLToPath } from 'node:url';
2 | import { describe, it, expect, assertType, expectTypeOf } from 'vitest';
3 | import { setup, $fetch, createPage } from '@nuxt/test-utils/e2e';
4 | import { expectNoClientErrors } from '../utils';
5 | import { timeout } from '$$/utils';
6 |
7 | const TIME = 2000;
8 |
9 | describe('Simple config behaviour', async () => {
10 | await setup({
11 | rootDir: fileURLToPath(new URL('../../fixtures/simple', import.meta.url)),
12 | setupTimeout: 120000,
13 | dev: true,
14 | });
15 |
16 | it('should display the root page without error', async () => {
17 | const html = await $fetch('/');
18 |
19 | expect(html).toContain('Navigate button');
20 | expect(html).toContain('Navigate link');
21 | expect(html).toContain('NavigateTo button');
22 |
23 | await expectNoClientErrors('/');
24 | });
25 |
26 | it('should navigate correctly with useRouter', async () => {
27 | const page = await createPage('/');
28 | await page.click('#useRouter');
29 | const html = await page.innerHTML('body');
30 | await timeout(TIME);
31 |
32 | await expectNoClientErrors('/');
33 | });
34 |
35 | it('should navigate correctly with nuxtLink', async () => {
36 | const page = await createPage('/');
37 | await page.click('#nuxtLink');
38 | await timeout(TIME);
39 | const html = await page.innerHTML('body');
40 |
41 | await expectNoClientErrors('/');
42 | });
43 |
44 | it('should navigate correctly with navigateTo', async () => {
45 | const page = await createPage('/');
46 | await page.click('#navigateTo');
47 | const html = await page.innerHTML('body');
48 | await timeout(TIME);
49 |
50 | await expectNoClientErrors('/');
51 | });
52 | });
53 |
--------------------------------------------------------------------------------
/test/e2e/utils.ts:
--------------------------------------------------------------------------------
1 | import { getBrowser, url, useTestContext } from '@nuxt/test-utils';
2 |
3 | // Taken from nuxt/framework repo
4 | export async function renderPage(path = '/') {
5 | const ctx = useTestContext();
6 | if (!ctx.options.browser) {
7 | throw new Error('`renderPage` require `options.browser` to be set');
8 | }
9 |
10 | const browser = await getBrowser();
11 | const page = await browser.newPage({});
12 | const pageErrors: Error[] = [];
13 | const consoleLogs: { type: string; text: string }[] = [];
14 |
15 | page.on('console', (message: any) => {
16 | consoleLogs.push({
17 | type: message.type(),
18 | text: message.text(),
19 | });
20 | });
21 | page.on('pageerror', (err: any) => {
22 | pageErrors.push(err);
23 | });
24 |
25 | if (path) {
26 | await page.goto(url(path), { waitUntil: 'networkidle' });
27 | }
28 |
29 | return {
30 | page,
31 | pageErrors,
32 | consoleLogs,
33 | };
34 | }
35 |
36 | // Taken from nuxt/framework repo
37 | export async function expectNoClientErrors(path: string) {
38 | const ctx = useTestContext();
39 | if (!ctx.options.browser) {
40 | return;
41 | }
42 |
43 | const { pageErrors, consoleLogs } = (await renderPage(path))!;
44 |
45 | const consoleLogErrors = consoleLogs.filter((i) => i.type === 'error');
46 | const consoleLogWarnings = consoleLogs.filter((i) => i.type === 'warning');
47 |
48 | expect(pageErrors).toEqual([]);
49 | expect(consoleLogErrors).toEqual([]);
50 | expect(consoleLogWarnings).toEqual([]);
51 | }
52 |
--------------------------------------------------------------------------------
/test/fixtures/complex/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log*
3 | .nuxt
4 | .nitro
5 | .cache
6 | .output
7 | .env
8 | dist
9 |
10 | src/plugins
--------------------------------------------------------------------------------
/test/fixtures/complex/README.md:
--------------------------------------------------------------------------------
1 | # Nuxt 3 Minimal Starter
2 |
3 | Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
4 |
5 | ## Setup
6 |
7 | Make sure to install the dependencies:
8 |
9 | ```bash
10 | # yarn
11 | yarn install
12 |
13 | # npm
14 | npm install
15 |
16 | # pnpm
17 | pnpm install
18 | ```
19 |
20 | ## Development Server
21 |
22 | Start the development server on http://localhost:3000
23 |
24 | ```bash
25 | npm run dev
26 | ```
27 |
28 | ## Production
29 |
30 | Build the application for production:
31 |
32 | ```bash
33 | npm run build
34 | ```
35 |
36 | Locally preview production build:
37 |
38 | ```bash
39 | npm run preview
40 | ```
41 |
42 | Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
43 |
--------------------------------------------------------------------------------
/test/fixtures/complex/app.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/test/fixtures/complex/app/components/testModule.vue:
--------------------------------------------------------------------------------
1 | test
2 |
--------------------------------------------------------------------------------
/test/fixtures/complex/app/modules/testAddRoute.ts:
--------------------------------------------------------------------------------
1 | import { createResolver, defineNuxtModule, extendPages } from '@nuxt/kit';
2 |
3 | export default defineNuxtModule({
4 | setup() {
5 | const { resolve } = createResolver(import.meta.url);
6 | extendPages((routes) => {
7 | routes.push({
8 | file: resolve('../components/testModule.vue'),
9 | path: '/testModule/:foo',
10 | name: 'test-module',
11 | });
12 | });
13 | },
14 | });
15 |
--------------------------------------------------------------------------------
/test/fixtures/complex/app/pages/[...404].vue:
--------------------------------------------------------------------------------
1 |
2 | Foo
3 |
4 |
--------------------------------------------------------------------------------
/test/fixtures/complex/app/pages/admin/[id].vue:
--------------------------------------------------------------------------------
1 | H1
2 |
--------------------------------------------------------------------------------
/test/fixtures/complex/app/pages/admin/[id]/action-[slug].vue:
--------------------------------------------------------------------------------
1 | H1
2 |
--------------------------------------------------------------------------------
/test/fixtures/complex/app/pages/admin/[id]/index.vue:
--------------------------------------------------------------------------------
1 | H1
2 |
--------------------------------------------------------------------------------
/test/fixtures/complex/app/pages/admin/[id]/profile.vue:
--------------------------------------------------------------------------------
1 | H1
2 |
--------------------------------------------------------------------------------
/test/fixtures/complex/app/pages/admin/[id]/settings.vue:
--------------------------------------------------------------------------------
1 | H1
2 |
--------------------------------------------------------------------------------
/test/fixtures/complex/app/pages/admin/panel/[[blou]].vue:
--------------------------------------------------------------------------------
1 | H1
2 |
--------------------------------------------------------------------------------
/test/fixtures/complex/app/pages/baguette.vue:
--------------------------------------------------------------------------------
1 | H1
2 |
--------------------------------------------------------------------------------
/test/fixtures/complex/app/pages/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Navigate link
9 |
10 |
11 |
12 |
13 |
14 |
15 |
38 |
--------------------------------------------------------------------------------
/test/fixtures/complex/app/pages/user/[foo]-[[bar]].vue:
--------------------------------------------------------------------------------
1 | test
2 |
--------------------------------------------------------------------------------
/test/fixtures/complex/app/pages/user/[id].vue:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/fixtures/complex/app/pages/user/[id]/[slug].vue:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/fixtures/complex/app/pages/user/[id]/[slug]/articles.vue:
--------------------------------------------------------------------------------
1 | H1
2 |
--------------------------------------------------------------------------------
/test/fixtures/complex/app/pages/user/[id]/[slug]/index.vue:
--------------------------------------------------------------------------------
1 |
2 | Navigate back
3 |
4 |
--------------------------------------------------------------------------------
/test/fixtures/complex/app/pages/user/[id]/index.vue:
--------------------------------------------------------------------------------
1 | Youhou
2 |
--------------------------------------------------------------------------------
/test/fixtures/complex/app/pages/user/[id]/posts.vue:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/fixtures/complex/app/pages/user/[one]-foo-[two].vue:
--------------------------------------------------------------------------------
1 | test
2 |
--------------------------------------------------------------------------------
/test/fixtures/complex/app/pages/user/catch/[...slug].vue:
--------------------------------------------------------------------------------
1 | test
2 |
--------------------------------------------------------------------------------
/test/fixtures/complex/app/pages/user/index.vue:
--------------------------------------------------------------------------------
1 | H1
2 |
--------------------------------------------------------------------------------
/test/fixtures/complex/app/pages/user/test-[[optional]].vue:
--------------------------------------------------------------------------------
1 | test
2 |
--------------------------------------------------------------------------------
/test/fixtures/complex/nuxt.config.ts:
--------------------------------------------------------------------------------
1 | import TestModuleRoute from './app/modules/testAddRoute';
2 |
3 | export default defineNuxtConfig({
4 | modules: ['nuxt-typed-router', TestModuleRoute, '@nuxtjs/i18n'],
5 | future: {
6 | compatibilityVersion: 4,
7 | },
8 | nuxtTypedRouter: {
9 | plugin: true,
10 | ignoreRoutes: ['[...404].vue'],
11 | },
12 | i18n: {
13 | locales: ['en', 'fr'],
14 | defaultLocale: 'en',
15 | },
16 | imports: {
17 | autoImport: false,
18 | },
19 | vite: {
20 | resolve: {
21 | dedupe: ['vue-router'],
22 | },
23 | },
24 | compatibilityDate: '2025-01-07',
25 | });
26 |
--------------------------------------------------------------------------------
/test/fixtures/complex/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "complex",
4 | "version": "1.0.0",
5 | "scripts": {
6 | "build": "nuxt build",
7 | "dev": "nuxt dev",
8 | "generate": "nuxt generate",
9 | "preview": "nuxt preview"
10 | },
11 | "devDependencies": {
12 | "@intlify/core-base": "~11.1.5",
13 | "@intlify/message-compiler": "~11.1.5",
14 | "@intlify/shared": "~11.1.5",
15 | "@intlify/vue-i18n-bridge": "1.1.0",
16 | "@intlify/vue-router-bridge": "1.1.0",
17 | "@nuxtjs/i18n": "9.5.5",
18 | "nuxt": "3.17.5",
19 | "nuxt-typed-router": "workspace:*",
20 | "vue": "3.5.16",
21 | "vue-i18n": "~11.1.5"
22 | }
23 | }
--------------------------------------------------------------------------------
/test/fixtures/complex/tests/i18n/useLocaleRoute.spec-d.ts:
--------------------------------------------------------------------------------
1 | import { assertType, test } from 'vitest';
2 | import type { RouteLocationMatched } from 'vue-router';
3 | import type { TypedRouteFromName } from '@typed-router';
4 | import { useLocaleRoute } from '@typed-router';
5 |
6 | // @ts-expect-error Ensure global imports are disabled
7 | declare const globalDecl: (typeof globalThis)['useLocaleRoute'];
8 |
9 | const localeRoute = useLocaleRoute();
10 |
11 | // ! ------ Should Error ❌
12 |
13 | // @ts-expect-error
14 | assertType(localeRoute({ name: 'index' }, 'DE'));
15 | // * index.vue
16 | // @ts-expect-error
17 | assertType(localeRoute({ name: 'index', params: { id: 1 } }, 'es'));
18 | // @ts-expect-error
19 | assertType(localeRoute({ name: 'index', params: { id: 1 } }));
20 | // @ts-expect-error
21 | assertType(localeRoute({ name: 'blabla-baguette' }));
22 |
23 | // * --- [id].vue
24 | // @ts-expect-error
25 | assertType(localeRoute({ name: 'user-id' }));
26 | // @ts-expect-error
27 | assertType(localeRoute({ name: 'user-id', params: { foo: 'bar' } }));
28 |
29 | // * --- [foo]-[[bar]].vue
30 | // @ts-expect-error
31 | assertType(localeRoute({ name: 'user-foo-bar' }));
32 | // @ts-expect-error
33 | assertType(localeRoute({ name: 'user-foo-bar', params: { bar: 1 } }));
34 |
35 | // * --- [...slug].vue
36 | // @ts-expect-error
37 | assertType(localeRoute({ name: 'user-slug' }));
38 | // @ts-expect-error
39 | assertType(localeRoute({ name: 'user-slug', params: { slug: 1 } }));
40 |
41 | // * --- [one]-foo-[two].vue
42 | // @ts-expect-error
43 | assertType(localeRoute({ name: 'user-one-foo-two' }));
44 | // @ts-expect-error
45 | assertType(localeRoute({ name: 'user-one-foo-two', params: { one: 1 } }));
46 |
47 | // * --- [id]/[slug].vue
48 | // @ts-expect-error
49 | assertType(localeRoute({ name: 'user-id-slug' }));
50 | // @ts-expect-error
51 | assertType(localeRoute({ name: 'user-id-slug', params: { id: 1 } }));
52 |
53 | // * --- Routes added by modules
54 | // @ts-expect-error
55 | assertType(localeRoute({ name: 'test-module' }));
56 |
57 | // * --- Path navigation
58 | // @ts-expect-error
59 | assertType(localeRoute('/fooooo'));
60 | // @ts-expect-error
61 | assertType(localeRoute({ path: '/foooo' }));
62 |
63 | // * Basic types
64 |
65 | test('Basic', () => {
66 | const resolved = localeRoute({ name: 'user-foo-bar', params: { foo: 1 } }, 'fr');
67 |
68 | assertType>(resolved);
69 | assertType<'user-foo-bar'>(resolved.name);
70 | assertType<{
71 | foo: string;
72 | bar?: string | undefined;
73 | }>(resolved.params);
74 | assertType(resolved.fullPath);
75 | assertType(resolved.hash);
76 | assertType(resolved.path);
77 | assertType(resolved.matched);
78 | });
79 |
80 | // * index.vue
81 |
82 | test('index', () => {
83 | const resolved = localeRoute({ name: 'index' }, 'fr');
84 |
85 | assertType>(resolved);
86 | assertType<'index'>(resolved.name);
87 | // @ts-expect-error
88 | assertType(resolved.params);
89 | });
90 |
91 | // * --- [id].vue
92 |
93 | test('[id]', () => {
94 | const resolved = localeRoute({ name: 'user-id', params: { id: 1 } }, 'fr');
95 |
96 | assertType>(resolved);
97 | assertType<'user-id'>(resolved.name);
98 | assertType<{ id: string }>(resolved.params);
99 | });
100 |
101 | // * --- [foo]-[[bar]].vue
102 | test('[foo]-[[bar]]', () => {
103 | const resolved = localeRoute({ name: 'user-foo-bar', params: { foo: 1, bar: 1 } }, 'fr');
104 |
105 | assertType>(resolved);
106 | assertType<'user-foo-bar'>(resolved.name);
107 | assertType<{
108 | foo: string;
109 | bar?: string | undefined;
110 | }>(resolved.params);
111 | });
112 |
113 | // * --- [...slug].vue
114 | test('[...slug]', () => {
115 | const resolved = localeRoute({ name: 'user-catch-slug', params: { slug: [1, 2] } }, 'fr');
116 |
117 | assertType>(resolved);
118 | assertType<'user-catch-slug'>(resolved.name);
119 | assertType<{
120 | slug: string[];
121 | }>(resolved.params);
122 | });
123 |
124 | // * --- [one]-foo-[two].vue
125 | test('[one]-foo-[two]', () => {
126 | const resolved = localeRoute({ name: 'user-one-foo-two', params: { one: 1, two: 2 } }, 'fr');
127 |
128 | assertType>(resolved);
129 | assertType<'user-one-foo-two'>(resolved.name);
130 | assertType<{
131 | one: string;
132 | two: string;
133 | }>(resolved.params);
134 | });
135 |
136 | // * --- [id]/[slug].vue
137 |
138 | test('[id]/[slug]', () => {
139 | const resolved = localeRoute({ name: 'user-id-slug', params: { slug: 1, id: '1' } }, 'fr');
140 |
141 | assertType>(resolved);
142 | assertType<'user-id-slug'>(resolved.name);
143 | assertType<{
144 | id: string;
145 | slug: string;
146 | }>(resolved.params);
147 | });
148 |
149 | // * --- Routes added by modules
150 |
151 | test('Routes added by modules', () => {
152 | const resolved = localeRoute({ name: 'test-module', params: { foo: 1 } }, 'fr');
153 |
154 | assertType>(resolved);
155 | assertType<'test-module'>(resolved.name);
156 | assertType<{
157 | foo: string;
158 | }>(resolved.params);
159 | });
160 |
--------------------------------------------------------------------------------
/test/fixtures/complex/tests/misc/definePageMeta.spec-d.ts:
--------------------------------------------------------------------------------
1 | import { assertType, expectTypeOf } from 'vitest';
2 | import { definePageMeta } from '@typed-router';
3 |
4 | // Given
5 |
6 | // - Usage of useRouter with useRouter
7 |
8 | // ! ------ Should Error ❌
9 |
10 | // * index.vue
11 | definePageMeta({ redirect: { name: 'index' } });
12 | // @ts-expect-error
13 | definePageMeta({ redirect: { name: 'index', params: { id: 1 } } });
14 | // @ts-expect-error
15 | definePageMeta({ redirect: { name: 'blabla-baguette' } });
16 |
17 | // * --- [id].vue
18 | // @ts-expect-error
19 | definePageMeta({ redirect: { name: 'user-id' } });
20 | // @ts-expect-error
21 | definePageMeta({ redirect: { name: 'user-id', params: { foo: 'bar' } } });
22 |
23 | // * --- [foo]-[[bar]].vue
24 | // @ts-expect-error
25 | definePageMeta({ redirect: { name: 'user-foo-bar' } });
26 | // @ts-expect-error
27 | definePageMeta({ redirect: { name: 'user-foo-bar', params: { bar: 1 } } });
28 |
29 | // * --- [...slug].vue
30 | // @ts-expect-error
31 | definePageMeta({ redirect: { name: 'user-slug' } });
32 | // @ts-expect-error
33 | definePageMeta({ redirect: { name: 'user-slug', params: { slug: 1 } } });
34 |
35 | // * --- [one]-foo-[two].vue
36 | // @ts-expect-error
37 | definePageMeta({ redirect: { name: 'user-one-foo-two' } });
38 | // @ts-expect-error
39 | definePageMeta({ redirect: { name: 'user-one-foo-two', params: { one: 1 } } });
40 |
41 | // * --- [id]/[slug].vue
42 | // @ts-expect-error
43 | definePageMeta({ redirect: { name: 'user-id-slug' } });
44 | // @ts-expect-error
45 | definePageMeta({ redirect: { name: 'user-id-slug', params: { id: 1 } } });
46 |
47 | // * --- Routes added by modules
48 | // @ts-expect-error
49 | definePageMeta({ redirect: { name: 'test-module' } });
50 |
51 | // * --- Path navigation
52 | // @ts-expect-error
53 | definePageMeta({ redirect: '/fooooooooooo' });
54 | // @ts-expect-error
55 | definePageMeta({ redirect: { path: '/foo' } });
56 |
57 | // $ ----- Should be valid ✅
58 |
59 | definePageMeta({ redirect: { name: 'index' } });
60 | definePageMeta({ redirect: { name: 'user-id', params: { id: 1 }, hash: 'baz' } });
61 | definePageMeta({ redirect: { name: 'user-foo-bar', params: { foo: 'bar' }, force: true } });
62 | definePageMeta({ redirect: { name: 'user-foo-bar', params: { foo: 'bar', bar: 'baz' } } });
63 | definePageMeta({ redirect: { name: 'user-catch-slug', params: { slug: ['foo'] } } });
64 | definePageMeta({ redirect: { name: 'user-catch-slug', params: { slug: [1, 2, 3] } } });
65 | definePageMeta({ redirect: { name: 'user-one-foo-two', params: { one: 1, two: '2' } } });
66 | definePageMeta({
67 | redirect: { name: 'user-id-slug', params: { slug: '2' }, query: { foo: 'bar' } },
68 | });
69 |
70 | // --- Path navigation
71 |
72 | // ! ------ Should Error ❌
73 |
74 | // @ts-expect-error
75 | assertType(definePageMeta({ redirect: '' }));
76 | // @ts-expect-error
77 | assertType(definePageMeta({ redirect: '/admin ' }));
78 | // @ts-expect-error
79 | assertType(definePageMeta({ redirect: '/admin/ /' }));
80 | // @ts-expect-error
81 | assertType(definePageMeta({ redirect: `/ / // / / eefzr` }));
82 | // @ts-expect-error
83 | assertType(definePageMeta({ redirect: '/elzhlzehflzhef' }));
84 | // @ts-expect-error
85 | assertType(definePageMeta({ redirect: '/admin/foo/bar' }));
86 | // @ts-expect-error
87 | assertType(definePageMeta({ redirect: '/admin/foo/bar/baz' }));
88 | // @ts-expect-error
89 | assertType(definePageMeta({ redirect: `/admin/${id}/action-bar/taz?query` }));
90 | // @ts-expect-error
91 | assertType(definePageMeta({ redirect: '/admin/panel/3O9393/bar' }));
92 | // @ts-expect-error
93 | assertType(definePageMeta({ redirect: '/admin/foo/ profile/ezfje' }));
94 | // @ts-expect-error
95 | assertType(definePageMeta({ redirect: '/admin/3U93U/settings/baz' }));
96 | // @ts-expect-error
97 | assertType(definePageMeta({ redirect: '/admin/panel/?fjzk' }));
98 |
99 | // $ ----- Should be valid ✅
100 |
101 | const id = '38789803';
102 | assertType(definePageMeta({ redirect: '/' }));
103 | assertType(definePageMeta({ redirect: '/baguette' }));
104 | assertType(definePageMeta({ redirect: '/admin/foo' }));
105 | assertType(definePageMeta({ redirect: '/admin/foo/' }));
106 | assertType(definePageMeta({ redirect: `/admin/${id}/action-bar#hash` }));
107 | assertType(definePageMeta({ redirect: `/admin/${id}/action-bar?query=bar` }));
108 | assertType(definePageMeta({ redirect: '/admin/foo/profile/' }));
109 | assertType(definePageMeta({ redirect: `/admin/${id}/settings` }));
110 | assertType(definePageMeta({ redirect: '/admin/panel/' }));
111 | assertType(definePageMeta({ redirect: '/admin/panel/938783/' }));
112 | assertType(definePageMeta({ redirect: '/user/38873-' }));
113 | assertType(definePageMeta({ redirect: '/user/38673/bar/#hash' }));
114 | assertType(definePageMeta({ redirect: '/user/ç9737/foo/articles?baz=foo' }));
115 | assertType(definePageMeta({ redirect: '/user/catch/1/2' }));
116 | assertType(definePageMeta({ redirect: '/user/test-' }));
117 | assertType(definePageMeta({ redirect: '/user' }));
118 |
--------------------------------------------------------------------------------
/test/fixtures/complex/tests/router/$typedRouter.spec-d.ts:
--------------------------------------------------------------------------------
1 | import { useNuxtApp } from '#imports';
2 | import { assertType } from 'vitest';
3 | import type { TypedRouter } from '@typed-router';
4 |
5 | // Given
6 | const { $typedRouter } = useNuxtApp();
7 |
8 | assertType($typedRouter);
9 |
10 | // - Usage of localePath with useRouter
11 |
12 | // ! ------ Should Error ❌
13 |
14 | // * index.vue
15 | // @ts-expect-error
16 | $typedRouter.push({ name: 'index', params: { id: 1 } });
17 | // @ts-expect-error
18 | $typedRouter.push({ name: 'index', params: { id: 1 } });
19 | // @ts-expect-error
20 | $typedRouter.push({ name: 'blabla-baguette' });
21 |
22 | // * --- [id].vue
23 | // @ts-expect-error
24 | $typedRouter.push({ name: 'user-id' });
25 | // @ts-expect-error
26 | $typedRouter.push({ name: 'user-id', params: { foo: 'bar' } });
27 |
28 | // * --- [foo]-[[bar]].vue
29 | // @ts-expect-error
30 | $typedRouter.push({ name: 'user-foo-bar' });
31 | // @ts-expect-error
32 | $typedRouter.push({ name: 'user-foo-bar', params: { bar: 1 } });
33 |
34 | // * --- [...slug].vue
35 | // @ts-expect-error
36 | $typedRouter.push({ name: 'user-slug' });
37 | // @ts-expect-error
38 | $typedRouter.push({ name: 'user-slug', params: { slug: 1 } });
39 |
40 | // * --- [one]-foo-[two].vue
41 | // @ts-expect-error
42 | $typedRouter.push({ name: 'user-one-foo-two' });
43 | // @ts-expect-error
44 | $typedRouter.push({ name: 'user-one-foo-two', params: { one: 1 } });
45 |
46 | // * --- [id]/[slug].vue
47 | // @ts-expect-error
48 | $typedRouter.push({ name: 'user-id-slug' });
49 | // @ts-expect-error
50 | $typedRouter.push({ name: 'user-id-slug', params: { id: 1 } });
51 |
52 | // * --- Routes added by modules
53 | // @ts-expect-error
54 | $typedRouter.push({ name: 'test-module' });
55 |
56 | // * --- Path navigation
57 | // @ts-expect-error
58 | $typedRouter.push('/admin/:id/foo');
59 |
60 | // $ ----- Should be valid ✅
61 |
62 | $typedRouter.push({ name: 'index' });
63 | $typedRouter.push({ name: 'user-id', params: { id: 1 }, hash: 'baz' });
64 | $typedRouter.push({ name: 'user-foo-bar', params: { foo: 'bar' }, force: true });
65 | $typedRouter.push({ name: 'user-foo-bar', params: { foo: 'bar', bar: 'baz' } });
66 | $typedRouter.push({ name: 'user-catch-slug', params: { slug: ['foo'] } });
67 | $typedRouter.push({ name: 'user-catch-slug', params: { slug: [1, 2, 3] } });
68 | $typedRouter.push({ name: 'user-one-foo-two', params: { one: 1, two: '2' } });
69 | $typedRouter.push({ name: 'user-id-slug', params: { slug: '2' }, query: { foo: 'bar' } });
70 | $typedRouter.push({ name: 'test-module', params: { foo: 1 }, query: { foo: 'bar' } });
71 |
72 | $typedRouter.replace({ name: 'index' });
73 | $typedRouter.replace({ name: 'user-id', params: { id: 1 }, hash: 'baz' });
74 |
75 | // * Resolved routes
76 |
77 | const resolved1 = $typedRouter.resolve({ name: 'index' });
78 | assertType<'index'>(resolved1.name);
79 | // @ts-expect-error
80 | assertType<'index'>(resolved1.params);
81 |
82 | const resolved2 = $typedRouter.resolve({ name: 'user-id', params: { id: 1 }, hash: 'baz' });
83 | assertType<'user-id'>(resolved2.name);
84 | assertType<{ id: string }>(resolved2.params);
85 |
--------------------------------------------------------------------------------
/test/fixtures/complex/tests/router/NuxtLink.spec-d.ts:
--------------------------------------------------------------------------------
1 | import { assertType, vi } from 'vitest';
2 | import type { GlobalComponents } from 'vue';
3 |
4 | const NuxtLink: GlobalComponents['NuxtLink'] = vi.fn() as any;
5 |
6 | // ! ------ Should Error ❌
7 |
8 | // * index.vue
9 | // @ts-expect-error
10 | assertType(new NuxtLink({ to: { name: 'index', params: { id: 1 } } }));
11 | // @ts-expect-error
12 | assertType(new NuxtLink({ to: { name: 'index', params: { id: 1 } } }));
13 | // @ts-expect-error
14 | assertType(new NuxtLink({ to: { name: 'blabla-baguette' } }));
15 |
16 | // * --- [id].vue
17 | // @ts-expect-error
18 | assertType(new NuxtLink({ to: { name: 'user-id' } }));
19 | // @ts-expect-error
20 | assertType(new NuxtLink({ to: { name: 'user-id', params: { foo: 'bar' } } }));
21 |
22 | // * --- [foo]-[[bar]].vue
23 | // @ts-expect-error
24 | assertType(new NuxtLink({ to: { name: 'user-foo-bar' } }));
25 | // @ts-expect-error
26 | assertType(new NuxtLink({ to: { name: 'user-foo-bar', params: { bar: 1 } } }));
27 |
28 | // * --- [...slug].vue
29 | // @ts-expect-error
30 | assertType(new NuxtLink({ to: { name: 'user-slug' } }));
31 | // @ts-expect-error
32 | assertType(new NuxtLink({ to: { name: 'user-slug', params: { slug: 1 } } }));
33 |
34 | // * --- [one]-foo-[two].vue
35 | // @ts-expect-error
36 | assertType(new NuxtLink({ to: { name: 'user-one-foo-two' } }));
37 | // @ts-expect-error
38 | assertType(new NuxtLink({ to: { name: 'user-one-foo-two', params: { one: 1 } } }));
39 |
40 | // * --- [id]/[slug].vue
41 | // @ts-expect-error
42 | assertType(new NuxtLink({ to: { name: 'user-id-slug' } }));
43 | // @ts-expect-error
44 | assertType(new NuxtLink({ to: { name: 'user-id-slug', params: { id: 1 } } }));
45 |
46 | // * --- Routes added by modules
47 | // @ts-expect-error
48 | assertType(new NuxtLink({ to: { name: 'test-module' } }));
49 |
50 | // --- Path navigation
51 |
52 | // ! ------ Should Error ❌
53 |
54 | // @ts-expect-error
55 | assertType(new NuxtLink({ to: '' }));
56 | // @ts-expect-error
57 | assertType(new NuxtLink({ to: '/admin ' }));
58 | // @ts-expect-error
59 | assertType(new NuxtLink({ to: '/admin/ /' }));
60 | // @ts-expect-error
61 | assertType(new NuxtLink({ to: `/ / // / / eefzr` }));
62 | // @ts-expect-error
63 | assertType(new NuxtLink({ to: '/elzhlzehflzhef' }));
64 | // @ts-expect-error
65 | assertType(new NuxtLink({ to: '/admin/foo/bar' }));
66 | // @ts-expect-error
67 | assertType(new NuxtLink({ to: '/admin/foo/bar/baz' }));
68 | // @ts-expect-error
69 | assertType(new NuxtLink({ to: `/admin/${id}/action-bar/taz?query` }));
70 | // @ts-expect-error
71 | assertType(new NuxtLink({ to: '/admin/panel/3O9393/bar' }));
72 | // @ts-expect-error
73 | assertType(new NuxtLink({ to: '/admin/foo/ profile/ezfje' }));
74 | // @ts-expect-error
75 | assertType(new NuxtLink({ to: '/admin/3U93U/settings/baz' }));
76 | // @ts-expect-error
77 | assertType(new NuxtLink({ to: '/admin/panel/?fjzk' }));
78 |
79 | // $ ----- Should be valid ✅
80 |
81 | const id = '38789803';
82 | assertType(new NuxtLink({ to: '/' }));
83 | assertType(new NuxtLink({ to: '/baguette' }));
84 | assertType(new NuxtLink({ to: '/admin/foo' }));
85 | assertType(new NuxtLink({ to: '/admin/foo/' }));
86 | assertType(new NuxtLink({ to: `/admin/${id}/action-bar#hash` }));
87 | assertType(new NuxtLink({ to: `/admin/${id}/action-bar?query=bar` }));
88 | assertType(new NuxtLink({ to: '/admin/foo/profile/' }));
89 | assertType(new NuxtLink({ to: `/admin/${id}/settings` }));
90 | assertType(new NuxtLink({ to: '/admin/panel/' }));
91 | assertType(new NuxtLink({ to: '/admin/panel/938783/' }));
92 | assertType(new NuxtLink({ to: '/user/38873-' }));
93 | assertType(new NuxtLink({ to: '/user/38673/bar/#hash' }));
94 | assertType(new NuxtLink({ to: '/user/ç9737/foo/articles?baz=foo' }));
95 | assertType(new NuxtLink({ to: '/user/catch/1/2' }));
96 | assertType(new NuxtLink({ to: '/user/test-' }));
97 | assertType(new NuxtLink({ to: '/user' }));
98 |
99 | // $ ----- Should be valid ✅
100 |
101 | assertType(new NuxtLink({ to: { name: 'index' } }));
102 | assertType(new NuxtLink({ to: { name: 'user-id', params: { id: 1 }, hash: 'baz' } }));
103 | assertType(new NuxtLink({ to: { name: 'user-foo-bar', params: { foo: 'bar' }, force: true } }));
104 | assertType(
105 | new NuxtLink({
106 | to: { name: 'user-foo-bar', params: { foo: 'bar', bar: 'baz' } },
107 | })
108 | );
109 | assertType(new NuxtLink({ to: { name: 'user-catch-slug', params: { slug: ['foo'] } } }));
110 | assertType(new NuxtLink({ to: { name: 'user-catch-slug', params: { slug: [1, 2, 3] } } }));
111 | assertType(new NuxtLink({ to: { name: 'user-one-foo-two', params: { one: 1, two: '2' } } }));
112 | assertType(
113 | new NuxtLink({
114 | to: { name: 'user-id-slug', params: { slug: '2' }, query: { foo: 'bar' } },
115 | })
116 | );
117 |
118 | assertType(
119 | new NuxtLink({
120 | to: { name: 'test-module', params: { foo: 1 }, query: { foo: 'bar' } },
121 | })
122 | );
123 |
124 | // - With External prop
125 |
126 | // $ ----- Should be valid ✅
127 |
128 | assertType(new NuxtLink({ to: '/admin/:id/', external: false }));
129 | assertType(new NuxtLink({ to: 'http://google.com', external: true }));
130 |
--------------------------------------------------------------------------------
/test/fixtures/complex/tests/router/useRouter.spec-d.ts:
--------------------------------------------------------------------------------
1 | import { assertType } from 'vitest';
2 | import type { TypedRouter, } from '@typed-router';
3 | import { useRouter } from '@typed-router';
4 |
5 | // @ts-expect-error Ensure global imports are disabled
6 | declare const globalDecl: (typeof globalThis)['useRouter'];
7 |
8 | // Given
9 | const router = useRouter();
10 |
11 | assertType(router);
12 |
13 | // - Usage of useRouter with useRouter
14 |
15 | // ! ------ Should Error ❌
16 |
17 | // * index.vue
18 | // @ts-expect-error
19 | router.push({ name: 'index', params: { id: 1 } });
20 | // @ts-expect-error
21 | router.push({ name: 'index', params: { id: 1 } });
22 | // @ts-expect-error
23 | router.push({ name: 'blabla-baguette' });
24 |
25 | // * --- [id].vue
26 | // @ts-expect-error
27 | router.push({ name: 'user-id' });
28 | // @ts-expect-error
29 | router.push({ name: 'user-id', params: { foo: 'bar' } });
30 |
31 | // * --- [foo]-[[bar]].vue
32 | // @ts-expect-error
33 | router.push({ name: 'user-foo-bar' });
34 | // @ts-expect-error
35 | router.push({ name: 'user-foo-bar', params: { bar: 1 } });
36 |
37 | // * --- [...slug].vue
38 | // @ts-expect-error
39 | router.push({ name: 'user-slug' });
40 | // @ts-expect-error
41 | router.push({ name: 'user-slug', params: { slug: 1 } });
42 |
43 | // * --- [one]-foo-[two].vue
44 | // @ts-expect-error
45 | router.push({ name: 'user-one-foo-two' });
46 | // @ts-expect-error
47 | router.push({ name: 'user-one-foo-two', params: { one: 1 } });
48 |
49 | // * --- [id]/[slug].vue
50 | // @ts-expect-error
51 | router.push({ name: 'user-id-slug' });
52 | // @ts-expect-error
53 | router.push({ name: 'user-id-slug', params: { id: 1 } });
54 |
55 | // * --- Routes added by modules
56 | // @ts-expect-error
57 | router.push({ name: 'test-module' });
58 |
59 | // $ ----- Should be valid ✅
60 |
61 | router.push({ name: 'index' });
62 | router.push({ name: 'user-id', params: { id: 1 }, hash: 'baz' });
63 | router.push({ name: 'user-foo-bar', params: { foo: 'bar' }, force: true });
64 | router.push({ name: 'user-foo-bar', params: { foo: 'bar', bar: 'baz' } });
65 | router.push({ name: 'user-catch-slug', params: { slug: ['foo'] } });
66 | router.push({ name: 'user-catch-slug', params: { slug: [1, 2, 3] } });
67 | router.push({ name: 'user-one-foo-two', params: { one: 1, two: '2' } });
68 | router.push({ name: 'user-id-slug', params: { slug: '2' }, query: { foo: 'bar' } });
69 | router.push({ name: 'test-module', params: { foo: 1 }, query: { foo: 'bar' } });
70 |
71 | router.replace({ name: 'index' });
72 | router.replace({ name: 'user-id', params: { id: 1 }, hash: 'baz' });
73 |
74 | // --- Path navigation
75 |
76 | // ! ------ Should Error ❌
77 |
78 | // @ts-expect-error
79 | assertType(router.push(''));
80 | // @ts-expect-error
81 | assertType(router.push('/admin '));
82 | // @ts-expect-error
83 | assertType(router.push('/admin/ /'));
84 | // @ts-expect-error
85 | assertType(router.push(`/ / // / / eefzr`));
86 | // @ts-expect-error
87 | assertType(router.push('/elzhlzehflzhef'));
88 | // @ts-expect-error
89 | assertType(router.push('/admin/foo/bar'));
90 | // @ts-expect-error
91 | assertType(router.push('/admin/foo/bar/baz'));
92 | // @ts-expect-error
93 | assertType(router.push(`/admin/${id}/action-bar/taz?query`));
94 | // @ts-expect-error
95 | assertType(router.push('/admin/panel/3O9393/bar'));
96 | // @ts-expect-error
97 | assertType(router.push('/admin/foo/ profile/ezfje'));
98 | // @ts-expect-error
99 | assertType(router.push('/admin/3U93U/settings/baz'));
100 | // @ts-expect-error
101 | assertType(router.push('/admin/panel/?fjzk'));
102 |
103 | // $ ----- Should be valid ✅
104 |
105 | const id = '38789803';
106 | assertType(router.push('/'));
107 | assertType(router.push('/baguette'));
108 | assertType(router.push('/admin/foo'));
109 | assertType(router.push('/admin/foo/'));
110 | assertType(router.push(`/admin/${id}/action-bar#hash`));
111 | assertType(router.push(`/admin/${id}/action-bar?query=bar`));
112 | assertType(router.push('/admin/foo/profile/'));
113 | assertType(router.push(`/admin/${id}/settings`));
114 | assertType(router.push('/admin/panel/'));
115 | assertType(router.push('/admin/panel/938783/'));
116 | assertType(router.push('/user/38873-'));
117 | assertType(router.push('/user/38673/bar/#hash'));
118 | assertType(router.push('/user/ç9737/foo/articles?baz=foo'));
119 | assertType(router.push('/user/catch/1/2'));
120 | assertType(router.push('/user/test-'));
121 | assertType(router.push('/user'));
122 |
123 | // * Resolved routes
124 |
125 | const resolved1 = router.resolve({ name: 'index' });
126 | assertType<'index'>(resolved1.name);
127 | // @ts-expect-error
128 | assertType<'index'>(resolved1.params);
129 |
130 | const resolved2 = router.resolve({ name: 'user-id', params: { id: 1 }, hash: 'baz' });
131 | assertType<'user-id'>(resolved2.name);
132 | assertType<{ id: string }>(resolved2.params);
133 |
--------------------------------------------------------------------------------
/test/fixtures/complex/tests/routes/useRoute.spec-d.ts:
--------------------------------------------------------------------------------
1 | import { assertType, test } from 'vitest';
2 | import type { RouteLocationMatched } from 'vue-router';
3 | import type { TypedRouteFromName } from '@typed-router';
4 | import {useRoute} from '@typed-router'
5 |
6 | // @ts-expect-error Ensure global imports are disabled
7 | declare const globalDecl: (typeof globalThis)['useRoute'];
8 |
9 | // Given
10 | const route = useRoute();
11 |
12 | test('Basic', () => {
13 | const namedRoute = useRoute('user-foo-bar');
14 |
15 | assertType>(namedRoute);
16 | assertType<'user-foo-bar'>(namedRoute.name);
17 | assertType<{
18 | foo: string;
19 | bar?: string | undefined;
20 | }>(namedRoute.params);
21 | assertType(namedRoute.fullPath);
22 | assertType(namedRoute.hash);
23 | assertType(namedRoute.path);
24 | assertType(namedRoute.matched);
25 | });
26 |
27 | // * index.vue
28 |
29 | if (route.name === 'index') {
30 | assertType<'index'>(route.name);
31 | assertType(route.params);
32 | }
33 |
34 | // * --- [id].vue
35 | if (route.name === 'user-id') {
36 | assertType>(route);
37 | assertType<'user-id'>(route.name);
38 | assertType<{ id: string }>(route.params);
39 | }
40 |
41 | // * --- [foo]-[[bar]].vue
42 | if (route.name === 'user-foo-bar') {
43 | assertType>(route);
44 | assertType<'user-foo-bar'>(route.name);
45 | assertType<{
46 | foo: string;
47 | bar?: string | undefined;
48 | }>(route.params);
49 | }
50 |
51 | // * --- [...slug].vue
52 | if (route.name === 'user-catch-slug') {
53 | assertType>(route);
54 | assertType<'user-catch-slug'>(route.name);
55 | assertType<{
56 | slug: string[];
57 | }>(route.params);
58 | }
59 |
60 | // * --- [one]-foo-[two].vue
61 | if (route.name === 'user-one-foo-two') {
62 | assertType>(route);
63 | assertType<'user-one-foo-two'>(route.name);
64 | assertType<{
65 | one: string;
66 | two: string;
67 | }>(route.params);
68 | }
69 |
70 | // * --- [id]/[slug].vue
71 | if (route.name === 'user-id-slug') {
72 | assertType>(route);
73 | assertType<'user-id-slug'>(route.name);
74 | assertType<{
75 | id: string;
76 | slug: string;
77 | }>(route.params);
78 | }
79 |
80 | // * --- Routes added by modules
81 | if (route.name === 'test-module') {
82 | assertType>(route);
83 | assertType<'test-module'>(route.name);
84 | assertType<{
85 | foo: string;
86 | }>(route.params);
87 | }
88 |
--------------------------------------------------------------------------------
/test/fixtures/complex/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // https://nuxt.com/docs/guide/concepts/typescript
3 | "extends": "./.nuxt/tsconfig.json"
4 | }
5 |
--------------------------------------------------------------------------------
/test/fixtures/simple/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log*
3 | .nuxt
4 | .nitro
5 | .cache
6 | .output
7 | .env
8 | dist
9 |
--------------------------------------------------------------------------------
/test/fixtures/simple/README.md:
--------------------------------------------------------------------------------
1 | # Nuxt 3 Minimal Starter
2 |
3 | Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
4 |
5 | ## Setup
6 |
7 | Make sure to install the dependencies:
8 |
9 | ```bash
10 | # yarn
11 | yarn install
12 |
13 | # npm
14 | npm install
15 |
16 | # pnpm
17 | pnpm install
18 | ```
19 |
20 | ## Development Server
21 |
22 | Start the development server on http://localhost:3000
23 |
24 | ```bash
25 | npm run dev
26 | ```
27 |
28 | ## Production
29 |
30 | Build the application for production:
31 |
32 | ```bash
33 | npm run build
34 | ```
35 |
36 | Locally preview production build:
37 |
38 | ```bash
39 | npm run preview
40 | ```
41 |
42 | Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
43 |
--------------------------------------------------------------------------------
/test/fixtures/simple/app.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/test/fixtures/simple/app/pages/admin.vue:
--------------------------------------------------------------------------------
1 | H1
2 |
--------------------------------------------------------------------------------
/test/fixtures/simple/app/pages/admin/[id].vue:
--------------------------------------------------------------------------------
1 | H1
2 |
--------------------------------------------------------------------------------
/test/fixtures/simple/app/pages/admin/[id]/action-[slug].vue:
--------------------------------------------------------------------------------
1 | H1
2 |
--------------------------------------------------------------------------------
/test/fixtures/simple/app/pages/admin/[id]/index.vue:
--------------------------------------------------------------------------------
1 | H1
2 |
--------------------------------------------------------------------------------
/test/fixtures/simple/app/pages/admin/[id]/profile.vue:
--------------------------------------------------------------------------------
1 | H1
2 |
--------------------------------------------------------------------------------
/test/fixtures/simple/app/pages/admin/[id]/settings.vue:
--------------------------------------------------------------------------------
1 | H1
2 |
--------------------------------------------------------------------------------
/test/fixtures/simple/app/pages/admin/panel/[[blou]].vue:
--------------------------------------------------------------------------------
1 | H1
2 |
--------------------------------------------------------------------------------
/test/fixtures/simple/app/pages/baguette.vue:
--------------------------------------------------------------------------------
1 | H1
2 |
--------------------------------------------------------------------------------
/test/fixtures/simple/app/pages/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Navigate link
7 |
8 |
9 |
10 |
11 |
25 |
--------------------------------------------------------------------------------
/test/fixtures/simple/app/pages/user/[foo]-[[bar]].vue:
--------------------------------------------------------------------------------
1 | test
2 |
--------------------------------------------------------------------------------
/test/fixtures/simple/app/pages/user/[id].vue:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/fixtures/simple/app/pages/user/[id]/[slug].vue:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/fixtures/simple/app/pages/user/[id]/[slug]/articles.vue:
--------------------------------------------------------------------------------
1 | H1
2 |
--------------------------------------------------------------------------------
/test/fixtures/simple/app/pages/user/[id]/[slug]/index.vue:
--------------------------------------------------------------------------------
1 |
2 | Navigate back
3 |
4 |
--------------------------------------------------------------------------------
/test/fixtures/simple/app/pages/user/[id]/index.vue:
--------------------------------------------------------------------------------
1 | Youhou
2 |
--------------------------------------------------------------------------------
/test/fixtures/simple/app/pages/user/[id]/posts.vue:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/fixtures/simple/app/pages/user/[one]-foo-[two].vue:
--------------------------------------------------------------------------------
1 | test
2 |
--------------------------------------------------------------------------------
/test/fixtures/simple/app/pages/user/catch/[...slug].vue:
--------------------------------------------------------------------------------
1 | test
2 |
--------------------------------------------------------------------------------
/test/fixtures/simple/app/pages/user/index.vue:
--------------------------------------------------------------------------------
1 | H1
2 |
--------------------------------------------------------------------------------
/test/fixtures/simple/app/pages/user/test-[[optional]].vue:
--------------------------------------------------------------------------------
1 | test
2 |
--------------------------------------------------------------------------------
/test/fixtures/simple/nuxt.config.ts:
--------------------------------------------------------------------------------
1 | export default defineNuxtConfig({
2 | modules: ['nuxt-typed-router'],
3 | future: {
4 | compatibilityVersion: 4,
5 | },
6 | vite: {
7 | resolve: {
8 | dedupe: ['vue-router'],
9 | },
10 | },
11 | compatibilityDate: '2025-01-07',
12 | });
13 |
--------------------------------------------------------------------------------
/test/fixtures/simple/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "simple",
4 | "version": "1.0.0",
5 | "scripts": {
6 | "build": "nuxt build",
7 | "dev": "nuxt dev",
8 | "generate": "nuxt generate",
9 | "preview": "nuxt preview"
10 | },
11 | "devDependencies": {
12 | "nuxt": "3.17.5",
13 | "nuxt-typed-router": "workspace:*",
14 | "vue": "3.5.16"
15 | }
16 | }
--------------------------------------------------------------------------------
/test/fixtures/simple/tests/misc/definePageMeta.spec-d.ts:
--------------------------------------------------------------------------------
1 | import { assertType } from 'vitest';
2 | import { definePageMeta } from '@typed-router';
3 |
4 | // Given
5 |
6 | // - Usage of useRouter with useRouter
7 |
8 | // ! ------ Should Error ❌
9 |
10 | // * index.vue
11 | definePageMeta({ redirect: { name: 'index' } });
12 | // @ts-expect-error
13 | definePageMeta({ redirect: { name: 'index', params: { id: 1 } } });
14 | // @ts-expect-error
15 | definePageMeta({ redirect: { name: 'blabla-baguette' } });
16 |
17 | // * --- [id].vue
18 | // @ts-expect-error
19 | definePageMeta({ redirect: { name: 'user-id' } });
20 | // @ts-expect-error
21 | definePageMeta({ redirect: { name: 'user-id', params: { foo: 'bar' } } });
22 |
23 | // * --- [foo]-[[bar]].vue
24 | // @ts-expect-error
25 | definePageMeta({ redirect: { name: 'user-foo-bar' } });
26 | // @ts-expect-error
27 | definePageMeta({ redirect: { name: 'user-foo-bar', params: { bar: 1 } } });
28 |
29 | // * --- [...slug].vue
30 | // @ts-expect-error
31 | definePageMeta({ redirect: { name: 'user-slug' } });
32 | // @ts-expect-error
33 | definePageMeta({ redirect: { name: 'user-slug', params: { slug: 1 } } });
34 |
35 | // * --- [one]-foo-[two].vue
36 | // @ts-expect-error
37 | definePageMeta({ redirect: { name: 'user-one-foo-two' } });
38 | // @ts-expect-error
39 | definePageMeta({ redirect: { name: 'user-one-foo-two', params: { one: 1 } } });
40 |
41 | // * --- [id]/[slug].vue
42 | // @ts-expect-error
43 | definePageMeta({ redirect: { name: 'user-id-slug' } });
44 | // @ts-expect-error
45 | definePageMeta({ redirect: { name: 'user-id-slug', params: { id: 1 } } });
46 |
47 | // * --- Routes added by modules
48 | // @ts-expect-error
49 | definePageMeta({ redirect: { name: 'test-module' } });
50 |
51 | // * --- Path navigation
52 | // @ts-expect-error
53 | definePageMeta({ redirect: '/fooooooooooo' });
54 |
55 | // @ts-expect-error
56 | definePageMeta({ redirect: { path: '/foo' } });
57 |
58 | // $ ----- Should be valid ✅
59 |
60 | definePageMeta({ redirect: { name: 'index' } });
61 | definePageMeta({ redirect: { name: 'user-id', params: { id: 1 }, hash: 'baz' } });
62 | definePageMeta({ redirect: { name: 'user-foo-bar', params: { foo: 'bar' }, force: true } });
63 | definePageMeta({ redirect: { name: 'user-foo-bar', params: { foo: 'bar', bar: 'baz' } } });
64 | definePageMeta({ redirect: { name: 'user-catch-slug', params: { slug: ['foo'] } } });
65 | definePageMeta({ redirect: { name: 'user-catch-slug', params: { slug: [1, 2, 3] } } });
66 | definePageMeta({ redirect: { name: 'user-one-foo-two', params: { one: 1, two: '2' } } });
67 | definePageMeta({
68 | redirect: { name: 'user-id-slug', params: { slug: '2' }, query: { foo: 'bar' } },
69 | });
70 |
71 | // --- Path navigation
72 |
73 | // ! ------ Should Error ❌
74 |
75 | // @ts-expect-error
76 | assertType(definePageMeta({ redirect: '' }));
77 | // @ts-expect-error
78 | assertType(definePageMeta({ redirect: '/admin ' }));
79 | // @ts-expect-error
80 | assertType(definePageMeta({ redirect: '/admin/ /' }));
81 | // @ts-expect-error
82 | assertType(definePageMeta({ redirect: `/ / // / / eefzr` }));
83 | // @ts-expect-error
84 | assertType(definePageMeta({ redirect: '/elzhlzehflzhef' }));
85 | // @ts-expect-error
86 | assertType(definePageMeta({ redirect: '/admin/foo/bar' }));
87 | // @ts-expect-error
88 | assertType(definePageMeta({ redirect: '/admin/foo/bar/baz' }));
89 | // @ts-expect-error
90 | assertType(definePageMeta({ redirect: `/admin/${id}/action-bar/taz?query` }));
91 | // @ts-expect-error
92 | assertType(definePageMeta({ redirect: '/admin/panel/3O9393/bar' }));
93 | // @ts-expect-error
94 | assertType(definePageMeta({ redirect: '/admin/foo/ profile/ezfje' }));
95 | // @ts-expect-error
96 | assertType(definePageMeta({ redirect: '/admin/3U93U/settings/baz' }));
97 | // @ts-expect-error
98 | assertType(definePageMeta({ redirect: '/admin/panel/?fjzk' }));
99 |
100 | // $ ----- Should be valid ✅
101 |
102 | const id = '38789803';
103 | assertType(definePageMeta({ redirect: '/' }));
104 | assertType(definePageMeta({ redirect: '/baguette' }));
105 | assertType(definePageMeta({ redirect: '/admin/foo' }));
106 | assertType(definePageMeta({ redirect: '/admin/foo/' }));
107 | assertType(definePageMeta({ redirect: `/admin/${id}/action-bar#hash` }));
108 | assertType(definePageMeta({ redirect: `/admin/${id}/action-bar?query=bar` }));
109 | assertType(definePageMeta({ redirect: '/admin/foo/profile/' }));
110 | assertType(definePageMeta({ redirect: `/admin/${id}/settings` }));
111 | assertType(definePageMeta({ redirect: '/admin/panel/' }));
112 | assertType(definePageMeta({ redirect: '/admin/panel/938783/' }));
113 | assertType(definePageMeta({ redirect: '/user/38873-' }));
114 | assertType(definePageMeta({ redirect: '/user/38673/bar/#hash' }));
115 | assertType(definePageMeta({ redirect: '/user/ç9737/foo/articles?baz=foo' }));
116 | assertType(definePageMeta({ redirect: '/user/catch/1/2' }));
117 | assertType(definePageMeta({ redirect: '/user/test-' }));
118 | assertType(definePageMeta({ redirect: '/user' }));
119 |
--------------------------------------------------------------------------------
/test/fixtures/simple/tests/router/NuxtLink.spec-d.ts:
--------------------------------------------------------------------------------
1 | import { assertType, vi } from 'vitest';
2 | import type { TypedNuxtLink } from '../../.nuxt/typed-router/typed-router';
3 |
4 | const NuxtLink: TypedNuxtLink = vi.fn() as any;
5 |
6 | // ! ------ Should Error ❌
7 |
8 | // * index.vue
9 | // @ts-expect-error
10 | assertType(new NuxtLink({ to: { name: 'index', params: { id: 1 } } }));
11 | // @ts-expect-error
12 | assertType(new NuxtLink({ to: { name: 'index', params: { id: 1 } } }));
13 | // @ts-expect-error
14 | assertType(new NuxtLink({ to: { name: 'blabla-baguette' } }));
15 |
16 | // * --- [id].vue
17 | // @ts-expect-error
18 | assertType(new NuxtLink({ to: { name: 'user-id' } }));
19 | // @ts-expect-error
20 | assertType(new NuxtLink({ to: { name: 'user-id', params: { foo: 'bar' } } }));
21 |
22 | // * --- [foo]-[[bar]].vue
23 | // @ts-expect-error
24 | assertType(new NuxtLink({ to: { name: 'user-foo-bar' } }));
25 | // @ts-expect-error
26 | assertType(new NuxtLink({ to: { name: 'user-foo-bar', params: { bar: 1 } } }));
27 |
28 | // * --- [...slug].vue
29 | // @ts-expect-error
30 | assertType(new NuxtLink({ to: { name: 'user-catch-slug' } }));
31 | // @ts-expect-error
32 | assertType(new NuxtLink({ to: { name: 'user-catch-slug', params: { slug: 1 } } }));
33 |
34 | // * --- [one]-foo-[two].vue
35 | // @ts-expect-error
36 | assertType(new NuxtLink({ to: { name: 'user-one-foo-two' } }));
37 | // @ts-expect-error
38 | assertType(new NuxtLink({ to: { name: 'user-one-foo-two', params: { one: 1 } } }));
39 |
40 | // * --- [id]/[slug].vue
41 | // @ts-expect-error
42 | assertType(new NuxtLink({ to: { name: 'user-id-slug' } }));
43 | // @ts-expect-error
44 | assertType(new NuxtLink({ to: { name: 'user-id-slug', params: { id: 1 } } }));
45 |
46 | // * --- Routes added by modules
47 | // @ts-expect-error
48 | assertType(new NuxtLink({ to: { name: 'test-module' } }));
49 |
50 | // $ ----- Should be valid ✅
51 |
52 | assertType(new NuxtLink({ to: { name: 'index' } }));
53 | assertType(new NuxtLink({ to: { name: 'user-id', params: { id: 1 }, hash: 'baz' } }));
54 | assertType(new NuxtLink({ to: { name: 'user-foo-bar', params: { foo: 'bar' }, force: true } }));
55 | assertType(
56 | new NuxtLink({
57 | to: { name: 'user-foo-bar', params: { foo: 'bar', bar: 'baz' } },
58 | })
59 | );
60 | assertType(new NuxtLink({ to: { name: 'user-catch-slug', params: { slug: ['foo'] } } }));
61 | assertType(new NuxtLink({ to: { name: 'user-catch-slug', params: { slug: [1, 2, 3] } } }));
62 | assertType(new NuxtLink({ to: { name: 'user-one-foo-two', params: { one: 1, two: '2' } } }));
63 | assertType(
64 | new NuxtLink({
65 | to: { name: 'user-id-slug', params: { slug: '2' }, query: { foo: 'bar' } },
66 | })
67 | );
68 |
69 | // --- Path navigation
70 |
71 | // ! ------ Should Error ❌
72 |
73 | // @ts-expect-error
74 | assertType(new NuxtLink({ to: '' }));
75 | // @ts-expect-error
76 | assertType(new NuxtLink({ to: '/admin ' }));
77 | // @ts-expect-error
78 | assertType(new NuxtLink({ to: '/admin/ /' }));
79 | // @ts-expect-error
80 | assertType(new NuxtLink({ to: `/ / // / / eefzr` }));
81 | // @ts-expect-error
82 | assertType(new NuxtLink({ to: '/elzhlzehflzhef' }));
83 | // @ts-expect-error
84 | assertType(new NuxtLink({ to: '/admin/foo/bar' }));
85 | // @ts-expect-error
86 | assertType(new NuxtLink({ to: '/admin/foo/bar/baz' }));
87 | // @ts-expect-error
88 | assertType(new NuxtLink({ to: `/admin/${id}/action-bar/taz?query` }));
89 | // @ts-expect-error
90 | assertType(new NuxtLink({ to: '/admin/panel/3O9393/bar' }));
91 | // @ts-expect-error
92 | assertType(new NuxtLink({ to: '/admin/foo/ profile/ezfje' }));
93 | // @ts-expect-error
94 | assertType(new NuxtLink({ to: '/admin/3U93U/settings/baz' }));
95 | // @ts-expect-error
96 | assertType(new NuxtLink({ to: '/admin/panel/?fjzk' }));
97 |
98 | // $ ----- Should be valid ✅
99 |
100 | const id = '38789803';
101 | assertType(new NuxtLink({ to: '/' }));
102 | assertType(new NuxtLink({ to: '/baguette' }));
103 | assertType(new NuxtLink({ to: '/admin/foo' }));
104 | assertType(new NuxtLink({ to: '/admin/foo/' }));
105 | assertType(new NuxtLink({ to: `/admin/${id}/action-bar#hash` }));
106 | assertType(new NuxtLink({ to: `/admin/${id}/action-bar?query=bar` }));
107 | assertType(new NuxtLink({ to: '/admin/foo/profile/' }));
108 | assertType(new NuxtLink({ to: `/admin/${id}/settings` }));
109 | assertType(new NuxtLink({ to: '/admin/panel/' }));
110 | assertType(new NuxtLink({ to: '/admin/panel/938783/' }));
111 | assertType(new NuxtLink({ to: '/user/38873-' }));
112 | assertType(new NuxtLink({ to: '/user/38673/bar/#hash' }));
113 | assertType(new NuxtLink({ to: '/user/ç9737/foo/articles?baz=foo' }));
114 | assertType(new NuxtLink({ to: '/user/catch/1/2' }));
115 | assertType(new NuxtLink({ to: '/user/test-' }));
116 | assertType(new NuxtLink({ to: '/user' }));
117 |
118 | // - With External prop
119 |
120 | // $ ----- Should be valid ✅
121 |
122 | assertType(new NuxtLink({ to: '/admin/:id/', external: false }));
123 | assertType(new NuxtLink({ to: 'http://google.com', external: true }));
124 |
--------------------------------------------------------------------------------
/test/fixtures/simple/tests/router/useRouter.spec-d.ts:
--------------------------------------------------------------------------------
1 | import { assertType, test } from 'vitest';
2 | import type { TypedRouter } from '@typed-router';
3 |
4 | // Given
5 | const router = useRouter();
6 |
7 | assertType(router);
8 |
9 | // - Usage of useRouter with useRouter
10 |
11 | // ! ------ Should Error ❌
12 |
13 | // * index.vue
14 | // @ts-expect-error
15 | router.push({ name: 'index', params: { id: 1 } });
16 | // @ts-expect-error
17 | router.push({ name: 'index', params: { id: 1 } });
18 | // @ts-expect-error
19 | router.push({ name: 'blabla-baguette' });
20 |
21 | // * --- [id].vue
22 | // @ts-expect-error
23 | router.push({ name: 'user-id' });
24 | // @ts-expect-error
25 | router.push({ name: 'user-id', params: { foo: 'bar' } });
26 |
27 | // * --- [foo]-[[bar]].vue
28 | // @ts-expect-error
29 | router.push({ name: 'user-foo-bar' });
30 | // @ts-expect-error
31 | router.push({ name: 'user-foo-bar', params: { bar: 1 } });
32 |
33 | // * --- [...slug].vue
34 | // @ts-expect-error
35 | router.push({ name: 'user-slug' });
36 | // @ts-expect-error
37 | router.push({ name: 'user-slug', params: { slug: 1 } });
38 |
39 | // * --- [one]-foo-[two].vue
40 | // @ts-expect-error
41 | router.push({ name: 'user-one-foo-two' });
42 | // @ts-expect-error
43 | router.push({ name: 'user-one-foo-two', params: { one: 1 } });
44 |
45 | // * --- [id]/[slug].vue
46 | // @ts-expect-error
47 | router.push({ name: 'user-id-slug' });
48 | // @ts-expect-error
49 | router.push({ name: 'user-id-slug', params: { id: 1 } });
50 |
51 | // * --- Routes added by modules
52 | // @ts-expect-error
53 | router.push({ name: 'test-module' });
54 |
55 | // * --- Path navigation
56 | // @ts-expect-error
57 | router.push('/fooooooooooo');
58 | // @ts-expect-error
59 | router.push({ path: '/foo' });
60 |
61 | // $ ----- Should be valid ✅
62 |
63 | router.push({ name: 'index' });
64 | router.push({ name: 'user-id', params: { id: 1 }, hash: 'baz' });
65 | router.push({ name: 'user-foo-bar', params: { foo: 'bar' }, force: true });
66 | router.push({ name: 'user-foo-bar', params: { foo: 'bar', bar: 'baz' } });
67 | router.push({ name: 'user-catch-slug', params: { slug: ['foo'] } });
68 | router.push({ name: 'user-catch-slug', params: { slug: [1, 2, 3] } });
69 | router.push({ name: 'user-one-foo-two', params: { one: 1, two: '2' } });
70 | router.push({ name: 'user-id-slug', params: { slug: '2' }, query: { foo: 'bar' } });
71 |
72 | router.replace({ name: 'index' });
73 | router.replace({ name: 'user-id', params: { id: 1 }, hash: 'baz' });
74 | router.replace('/admin');
75 |
76 | // --- Path navigation
77 |
78 | // ! ------ Should Error ❌
79 |
80 | // @ts-expect-error
81 | assertType(router.push(''));
82 | // @ts-expect-error
83 | assertType(router.push('/admin '));
84 | // @ts-expect-error
85 | assertType(router.push('/admin/ /'));
86 | // @ts-expect-error
87 | assertType(router.push(`/ / // / / eefzr`));
88 | // @ts-expect-error
89 | assertType(router.push('/elzhlzehflzhef'));
90 | // @ts-expect-error
91 | assertType(router.push('/admin/foo/bar'));
92 | // @ts-expect-error
93 | assertType(router.push('/admin/foo/bar/baz'));
94 | // @ts-expect-error
95 | assertType(router.push(`/admin/${id}/action-bar/taz?query`));
96 | // @ts-expect-error
97 | assertType(router.push('/admin/panel/3O9393/bar'));
98 | // @ts-expect-error
99 | assertType(router.push('/admin/foo/ profile/ezfje'));
100 | // @ts-expect-error
101 | assertType(router.push('/admin/3U93U/settings/baz'));
102 | // @ts-expect-error
103 | assertType(router.push('/admin/panel/?fjzk'));
104 |
105 | // $ ----- Should be valid ✅
106 |
107 | const id = '38789803';
108 | assertType(router.push('/'));
109 | assertType(router.push('/baguette'));
110 | assertType(router.push('/admin/foo'));
111 | assertType(router.push('/admin/foo/'));
112 | assertType(router.push(`/admin/${id}/action-bar#hash`));
113 | assertType(router.push(`/admin/${id}/action-bar?query=bar`));
114 | assertType(router.push('/admin/foo/profile/'));
115 | assertType(router.push(`/admin/${id}/settings`));
116 | assertType(router.push('/admin/panel/'));
117 | assertType(router.push('/admin/panel/938783/'));
118 | assertType(router.push('/user/38873-'));
119 | assertType(router.push('/user/38673/bar/#hash'));
120 | assertType(router.push('/user/ç9737/foo/articles?baz=foo'));
121 | assertType(router.push('/user/catch/1/2'));
122 | assertType(router.push('/user/test-'));
123 | assertType(router.push('/user'));
124 |
125 | // * Resolved routes
126 |
127 | test('', () => {
128 | const resolved = router.resolve({ name: 'index' });
129 | assertType<'index'>(resolved.name);
130 | // @ts-expect-error
131 | assertType<'index'>(resolved.params);
132 | });
133 |
134 | test('', () => {
135 | const resolved = router.resolve({ name: 'user-id', params: { id: 1 }, hash: 'baz' });
136 | assertType<'user-id'>(resolved.name);
137 | assertType<{ id: string }>(resolved.params);
138 |
139 | // @ts-expect-error
140 | assertType<'user-eojzpejfze'>(resolved.name);
141 | });
142 |
143 | test('', () => {
144 | const resolved = router.resolve('/admin/foo/');
145 | assertType<'admin-id'>(resolved.name);
146 | assertType<{ id: string }>(resolved.params);
147 |
148 | // @ts-expect-error
149 | assertType<'jzeifjlfej'>(resolved.name);
150 | // @ts-expect-error
151 | assertType<{ foo: string }>(resolved.params);
152 | });
153 |
--------------------------------------------------------------------------------
/test/fixtures/simple/tests/routes/useRoute.spec-d.ts:
--------------------------------------------------------------------------------
1 | import { assertType, test } from 'vitest';
2 | import type { RouteLocationMatched } from 'vue-router';
3 | import type { TypedRouteFromName } from '@typed-router';
4 |
5 | // Given
6 | const route = useRoute();
7 |
8 | test('Basic', () => {
9 | const namedRoute = useRoute('user-foo-bar');
10 |
11 | assertType>(namedRoute);
12 | assertType<'user-foo-bar'>(namedRoute.name);
13 | assertType<{
14 | foo: string;
15 | bar?: string | undefined;
16 | }>(namedRoute.params);
17 | assertType(namedRoute.fullPath);
18 | assertType(namedRoute.hash);
19 | assertType(namedRoute.path);
20 | assertType(namedRoute.matched);
21 | });
22 |
23 | // * index.vue
24 |
25 | if (route.name === 'index') {
26 | assertType<'index'>(route.name);
27 | assertType(route.params);
28 | }
29 |
30 | // * --- [id].vue
31 | if (route.name === 'user-id') {
32 | assertType>(route);
33 | assertType<'user-id'>(route.name);
34 | assertType<{ id: string }>(route.params);
35 | }
36 |
37 | // * --- [foo]-[[bar]].vue
38 | if (route.name === 'user-foo-bar') {
39 | assertType>(route);
40 | assertType<'user-foo-bar'>(route.name);
41 | assertType<{
42 | foo: string;
43 | bar?: string | undefined;
44 | }>(route.params);
45 | }
46 |
47 | // * --- [...slug].vue
48 | if (route.name === 'user-catch-slug') {
49 | assertType>(route);
50 | assertType<'user-catch-slug'>(route.name);
51 | assertType<{
52 | slug: string[];
53 | }>(route.params);
54 | }
55 |
56 | // * --- [one]-foo-[two].vue
57 | if (route.name === 'user-one-foo-two') {
58 | assertType>(route);
59 | assertType<'user-one-foo-two'>(route.name);
60 | assertType<{
61 | one: string;
62 | two: string;
63 | }>(route.params);
64 | }
65 |
66 | // * --- [id]/[slug].vue
67 | if (route.name === 'user-id-slug') {
68 | assertType>(route);
69 | assertType<'user-id-slug'>(route.name);
70 | assertType<{
71 | id: string;
72 | slug: string;
73 | }>(route.params);
74 | }
75 |
--------------------------------------------------------------------------------
/test/fixtures/simple/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // https://nuxt.com/docs/guide/concepts/typescript
3 | "extends": "./.nuxt/tsconfig.json",
4 | "compilerOptions": {
5 | "verbatimModuleSyntax": false
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/test/fixtures/withOptions/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log*
3 | .nuxt
4 | .nitro
5 | .cache
6 | .output
7 | .env
8 | dist
9 |
--------------------------------------------------------------------------------
/test/fixtures/withOptions/README.md:
--------------------------------------------------------------------------------
1 | # Nuxt 3 Minimal Starter
2 |
3 | Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
4 |
5 | ## Setup
6 |
7 | Make sure to install the dependencies:
8 |
9 | ```bash
10 | # yarn
11 | yarn install
12 |
13 | # npm
14 | npm install
15 |
16 | # pnpm
17 | pnpm install
18 | ```
19 |
20 | ## Development Server
21 |
22 | Start the development server on http://localhost:3000
23 |
24 | ```bash
25 | npm run dev
26 | ```
27 |
28 | ## Production
29 |
30 | Build the application for production:
31 |
32 | ```bash
33 | npm run build
34 | ```
35 |
36 | Locally preview production build:
37 |
38 | ```bash
39 | npm run preview
40 | ```
41 |
42 | Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
43 |
--------------------------------------------------------------------------------
/test/fixtures/withOptions/app.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/test/fixtures/withOptions/nuxt.config.ts:
--------------------------------------------------------------------------------
1 | import NuxtTypedRouter from '../../..';
2 |
3 | export default defineNuxtConfig({
4 | modules: [NuxtTypedRouter],
5 | vite: {
6 | resolve: {
7 | dedupe: ['vue-router'],
8 | },
9 | },
10 | });
11 |
--------------------------------------------------------------------------------
/test/fixtures/withOptions/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "with-options",
4 | "version": "1.0.0",
5 | "scripts": {
6 | "build": "nuxt build",
7 | "dev": "nuxt dev",
8 | "generate": "nuxt generate",
9 | "preview": "nuxt preview"
10 | },
11 | "devDependencies": {
12 | "nuxt": "3.17.5",
13 | "nuxt-typed-router": "workspace:*"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/test/fixtures/withOptions/pages/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
--------------------------------------------------------------------------------
/test/fixtures/withOptions/pages/user/[...slug].vue:
--------------------------------------------------------------------------------
1 | test
2 |
--------------------------------------------------------------------------------
/test/fixtures/withOptions/pages/user/[foo]-[[bar]].vue:
--------------------------------------------------------------------------------
1 | test
2 |
--------------------------------------------------------------------------------
/test/fixtures/withOptions/pages/user/[id].vue:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/fixtures/withOptions/pages/user/[id]/[slug].vue:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/fixtures/withOptions/pages/user/[id]/[slug]/articles.vue:
--------------------------------------------------------------------------------
1 | H1
2 |
--------------------------------------------------------------------------------
/test/fixtures/withOptions/pages/user/[id]/[slug]/index.vue:
--------------------------------------------------------------------------------
1 |
2 | Navigate back
3 |
4 |
--------------------------------------------------------------------------------
/test/fixtures/withOptions/pages/user/[id]/index.vue:
--------------------------------------------------------------------------------
1 | Youhou
2 |
--------------------------------------------------------------------------------
/test/fixtures/withOptions/pages/user/[id]/posts.vue:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/fixtures/withOptions/pages/user/[one]-foo-[two].vue:
--------------------------------------------------------------------------------
1 | test
2 |
--------------------------------------------------------------------------------
/test/fixtures/withOptions/pages/user/index.vue:
--------------------------------------------------------------------------------
1 | H1
2 |
--------------------------------------------------------------------------------
/test/fixtures/withOptions/pages/user/test-[[optional]].vue:
--------------------------------------------------------------------------------
1 | test
2 |
--------------------------------------------------------------------------------
/test/fixtures/withOptions/tests/e2e/withPartialStrict.spec.ts:
--------------------------------------------------------------------------------
1 | test('empty')
2 |
3 | // import { fileURLToPath } from 'node:url';
4 | // import { setup } from '@nuxt/test-utils';
5 | // import { assertType } from 'vitest';
6 | // import { useRouter } from '../../.nuxt/typed-router';
7 | // import type { TypedNuxtLink } from '../../.nuxt/typed-router/typed-router';
8 |
9 | // test.skip('The strict option should behave correctly with partial strict options', async () => {
10 | // await setup({
11 | // rootDir: fileURLToPath(new URL('../../fixtures/withOptions', import.meta.url)),
12 | // setupTimeout: 120000,
13 | // nuxtConfig: {
14 | // nuxtTypedRouter: {
15 | // strict: {
16 | // NuxtLink: {
17 | // strictRouteLocation: true,
18 | // },
19 | // router: {
20 | // strictToArgument: true,
21 | // },
22 | // },
23 | // },
24 | // },
25 | // } as any);
26 |
27 | // // const diagnostic = await runTypesDiagnostics(__dirname, __filename);
28 |
29 | // // expect(diagnostic.length).toBe(0);
30 |
31 | // const NuxtLink: TypedNuxtLink = vi.fn() as any;
32 |
33 | // const router = { push: vi.fn() } as unknown as ReturnType;
34 |
35 | // assertType(router.push('/user'));
36 | // // @ts-expect-error
37 | // assertType(router.push({ path: '/login' }));
38 |
39 | // // @ts-expect-error
40 | // assertType(new NuxtLink('/user'));
41 | // assertType(new NuxtLink({ to: { path: '/user' } }));
42 | // });
43 |
--------------------------------------------------------------------------------
/test/fixtures/withOptions/tests/e2e/withStrict.spec.ts:
--------------------------------------------------------------------------------
1 | test('empty')
2 | // import { setup } from '@nuxt/test-utils';
3 | // import { fileURLToPath } from 'node:url';
4 | // import { assertType } from 'vitest';
5 | // import { useRouter } from '../../.nuxt/typed-router';
6 | // import type { TypedNuxtLink } from '../../.nuxt/typed-router/typed-router';
7 |
8 | // test.skip('The strict option should behave correctly with strict: true', async () => {
9 | // await setup({
10 | // rootDir: fileURLToPath(new URL('../../fixtures/withOptions', import.meta.url)),
11 | // setupTimeout: 120000,
12 | // nuxtConfig: {
13 | // nuxtTypedRouter: {
14 | // strict: true,
15 | // },
16 | // },
17 | // } as any);
18 |
19 | // // const diagnostic = await runTypesDiagnostics(__dirname, __filename);
20 |
21 | // // expect(diagnostic.length).toBe(0);
22 |
23 | // const NuxtLink: TypedNuxtLink = vi.fn() as any;
24 |
25 | // const router = { push: vi.fn() } as unknown as ReturnType;
26 |
27 | // // @ts-expect-error
28 | // assertType(router.push('/foo'));
29 | // // @ts-expect-error
30 | // assertType(router.push({ path: '/login' }));
31 |
32 | // // @ts-expect-error
33 | // assertType(new NuxtLink('/login'));
34 | // // @ts-expect-error
35 | // assertType(new NuxtLink({ path: '/goooo' }));
36 | // });
37 |
38 |
--------------------------------------------------------------------------------
/test/fixtures/withOptions/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.nuxt/tsconfig.json",
3 | "compilerOptions": {
4 | "types": ["vitest/globals"]
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/test/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "preserve",
4 | "target": "ESNext",
5 | "module": "ESNext",
6 | "moduleResolution": "Node",
7 | "skipLibCheck": true,
8 | "strict": true,
9 | "allowJs": true,
10 | "noEmit": true,
11 | "resolveJsonModule": true,
12 | "allowSyntheticDefaultImports": true,
13 | "types": ["node", "vitest/globals"],
14 | "paths": {
15 | "$$/*": ["./*"]
16 | }
17 | },
18 | "include": ["./**/*.ts", "./fixtures/**/.nuxt/**/*.ts", "./**/*.tsx"]
19 | }
20 |
--------------------------------------------------------------------------------
/test/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './tsd.utils';
2 | export * from './typecheck';
3 |
4 | export function timeout(count: number) {
5 | return new Promise((resolve) => setTimeout(resolve, count));
6 | }
7 |
--------------------------------------------------------------------------------
/test/utils/tsd.utils.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import tsd from 'tsd';
3 |
4 | export async function runTypesDiagnostics(dirName: string, fileName: string) {
5 | const diagnostic = await tsd({
6 | cwd: dirName,
7 | testFiles: [path.basename(fileName)],
8 | typingsFile: `./${path.basename(fileName)}`,
9 | });
10 |
11 | if (diagnostic.length) {
12 | console.error(
13 | diagnostic.map(
14 | (m) => `Error in file ${m.fileName}:${m.line}:${m.column}:
15 | ${m.message}`
16 | )
17 | );
18 | }
19 |
20 | return diagnostic;
21 | }
22 |
--------------------------------------------------------------------------------
/test/utils/typecheck.ts:
--------------------------------------------------------------------------------
1 | // Check required param
2 | export function required(arg: string) {}
3 |
4 | // Check optional param
5 | export function optional(arg: undefined extends T ? T : never) {}
6 |
7 | // Check array params
8 | export function array(arg: string[]) {}
9 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./playground/.nuxt/tsconfig.json",
3 | "compilerOptions": {
4 | "module": "ESNext"
5 | },
6 | "include": ["./src/**/*.ts"]
7 | }
8 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import { defineConfig } from 'vitest/config';
3 |
4 | export default defineConfig({
5 | test: {
6 | globals: true,
7 | testTimeout: 10000,
8 | threads: false,
9 | },
10 | resolve: {
11 | alias: {
12 | $$: path.resolve(__dirname, './test'),
13 | },
14 | },
15 | });
16 |
--------------------------------------------------------------------------------