Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: experimental typed routes #3142

Merged
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a37559c
feat: generated route types
BobbieGoede Sep 27, 2024
65becf5
chore: use experimental `typedPages` in playground
BobbieGoede Sep 27, 2024
2bb26c9
fix: type generation throwing error during `prepare` step
BobbieGoede Sep 27, 2024
1214bd2
feat: improve `defineI18nRoute` parameter type
BobbieGoede Sep 27, 2024
63e553f
fix: linting errors
BobbieGoede Sep 27, 2024
d05df07
fix: linting errors
BobbieGoede Sep 27, 2024
d836d51
fix: linting errors
BobbieGoede Sep 27, 2024
88fe1e3
fix: linting errors
BobbieGoede Sep 27, 2024
2f7b3d2
fix: rename conflicting router type
BobbieGoede Sep 28, 2024
10353ac
refactor: split experiment route type feature
BobbieGoede Sep 28, 2024
87101f7
chore: reference to original and dependency router source types
BobbieGoede Sep 28, 2024
d937c52
fix: remove unnecessary logic
BobbieGoede Sep 29, 2024
50030ea
docs: describe `experimental.typedPages` option
BobbieGoede Sep 29, 2024
113a490
fix: add missing i18n suffix to router augmentation
BobbieGoede Sep 29, 2024
5226fc0
refactor: remove internal global type augmentation
BobbieGoede Sep 29, 2024
16ac01a
refactor: rename typed router function
BobbieGoede Sep 29, 2024
e3ec9fa
fix: remove duplicate generated type
BobbieGoede Sep 29, 2024
40a6543
fix: auto enable when nuxt `typedPages` is enabled
BobbieGoede Sep 30, 2024
d661a2d
test: add some very basic tests
BobbieGoede Sep 30, 2024
91539df
chore: add `unplugin-vue-router` dependency
BobbieGoede Sep 30, 2024
4a407ac
docs: update default description
BobbieGoede Sep 30, 2024
cc6de7c
chore: cleanup
BobbieGoede Sep 30, 2024
385db97
fix: `NuxtLinkLocale` type completion
BobbieGoede Sep 30, 2024
56c6f51
fix: prevent completion for paths in i18n route utilities
BobbieGoede Sep 30, 2024
312efe2
fix: allow passing `path` but provide no completion
BobbieGoede Sep 30, 2024
352ce15
fix: linting errors
BobbieGoede Sep 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { defineBuildConfig } from 'unbuild'

export default defineBuildConfig({
externals: ['node:fs', 'node:url', 'webpack', '@babel/parser']
externals: ['node:fs', 'node:url', 'webpack', '@babel/parser', 'unplugin-vue-router', 'unplugin-vue-router/options']
})
8 changes: 4 additions & 4 deletions docs/content/docs/5.v9/4.api/3.compiler-macros.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ defineI18nRoute({
defineI18nRoute(route: I18nRoute | false) => void

interface I18nRoute {
paths?: Record<string, string>
locales?: string[]
paths?: Record<Locale, `/${string}`>
locales?: Locale[]
}
```

Expand All @@ -42,12 +42,12 @@ An object accepting the following i18n route settings:

- **`paths`**

- **Type**: `Record<Locale, string>`
- **Type**: `Record<Locale, `/${string}`>`

Customize page component routes per locale. You can specify static and dynamic paths for vue-router.

- **`locales`**

- **Type**: `string[]`
- **Type**: `Locale[]`

Some locales to which the page component should be localized.
3 changes: 2 additions & 1 deletion playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ export default defineNuxtConfig({
experimental: {
localeDetector: './localeDetector.ts',
switchLocalePathLinkSSR: true,
autoImportTranslationFunctions: true
autoImportTranslationFunctions: true,
typedPages: true
},
compilation: {
// jit: false,
Expand Down
3 changes: 2 additions & 1 deletion playground/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ definePageMeta({
<p>{{ $t('bar.buz', { name: 'buz' }) }}</p>
<h2>Pages</h2>
<nav>
<NuxtLink :to="localePath('/')">Home</NuxtLink> | <NuxtLink :to="localePath({ name: 'about' })">About</NuxtLink> |
<NuxtLink :to="localePath('index')">Home</NuxtLink> |
<NuxtLink :to="localePath({ name: 'about' })">About</NuxtLink> |
<NuxtLink :to="localePath({ name: 'blog' })">Blog</NuxtLink> |
<NuxtLink :to="localePath({ name: 'server' })">Server</NuxtLink> |
<NuxtLink :to="localePath({ name: 'category-id', params: { id: 'foo' } })">Category</NuxtLink> |
Expand Down
3 changes: 2 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ export const DEFAULT_OPTIONS = {
experimental: {
localeDetector: '',
switchLocalePathLinkSSR: false,
autoImportTranslationFunctions: false
autoImportTranslationFunctions: false,
typedPages: false
},
bundle: {
compositionOnly: true,
Expand Down
69 changes: 69 additions & 0 deletions src/gen.ts
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wow! This typing is too amazing!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should reference the original source types πŸ˜… I had to redefine/reimplement a bunch of internal types of vue-router and uvr, so the credits for these go to @posva

Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,75 @@ declare module '#app' {
}
}

declare module 'vue-router' {
import type { RouteNamedMapI18n } from 'vue-router/auto-routes'

export interface TypesConfig {
RouteNamedMapI18n: RouteNamedMapI18n
}

export type RouteMapI18n =
TypesConfig extends Record<'RouteNamedMapI18n', infer RouteNamedMap> ? RouteNamedMap : RouteMapGeneric

export type RouteLocationRawI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n
? RouteLocationAsStringI18n | RouteLocationAsRelativeGeneric | RouteLocationAsPathGeneric
:
| _LiteralUnion<RouteLocationAsStringTypedList<RouteMapI18n>[Name], string>
| RouteLocationAsRelativeTypedList<RouteMapI18n>[Name]

export type RouteLocationResolved<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n
? RouteLocationResolvedGeneric
: RouteLocationResolvedTypedList<RouteMapI18n>[Name]

export interface RouteLocationNormalizedLoadedTypedI18n<
RouteMapI18n extends RouteMapGeneric = RouteMapGeneric,
Name extends keyof RouteMapI18n = keyof RouteMapI18n
> extends RouteLocationNormalizedLoadedGeneric {
name: Extract<Name, string | symbol>
params: RouteMapI18n[Name]['params']
}
export type RouteLocationNormalizedLoadedTypedListI18n<RouteMapOriginal extends RouteMapGeneric = RouteMapGeneric> = {
[N in keyof RouteMapOriginal]: RouteLocationNormalizedLoadedTypedI18n<RouteMapOriginal, N>
}
export type RouteLocationNormalizedLoadedI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n
? RouteLocationNormalizedLoadedGeneric
: RouteLocationNormalizedLoadedTypedListI18n<RouteMapI18n>[Name]

type _LiteralUnion<LiteralType, BaseType extends string = string> = LiteralType | (BaseType & Record<never, never>)

export type RouteLocationAsStringI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n
? string
: _LiteralUnion<RouteLocationAsStringTypedList<RouteMapI18n>[Name], string>

export type RouteLocationAsStringI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n
? string
: _LiteralUnion<RouteLocationAsStringTypedList<RouteMapI18n>[Name], string>

export type RouteLocationAsRelativeI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n
? RouteLocationAsRelativeGeneric
: RouteLocationAsRelativeTypedList<RouteMapI18n>[Name]

export type RouteLocationAsPathI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n ? RouteLocationAsPathGeneric : RouteLocationAsPathTypedList<RouteMapI18n>[Name]

/**
* Helper to generate a type safe version of the {@link RouteLocationAsRelative} type.
*/
export interface RouteLocationAsRelativeTyped<
RouteMapI18n extends RouteMapGeneric = RouteMapGeneric,
Name extends keyof RouteMapI18n = keyof RouteMapI18n
> extends RouteLocationAsRelativeGeneric {
name?: Extract<Name, string | symbol>
params?: RouteMapI18n[Name]['paramsRaw']
}
}

${(options.experimental?.autoImportTranslationFunctions && globalTranslationTypes) || ''}

export {}`
Expand Down
72 changes: 72 additions & 0 deletions src/internal-global-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,78 @@ declare module '#app' {
}
}

// This needs to be generated, or else `RouteMapI18n` will fall back to `RouteMapGeneric`
// declare module 'vue-router' {
// import type { RouteNamedMapI18n } from 'vue-router/auto-routes'

// export interface TypesConfig {
// RouteNamedMapI18n: RouteNamedMapI18n
// }
// }

declare module 'vue-router' {
export type RouteMapI18n =
TypesConfig extends Record<'RouteNamedMapI18n', infer RouteNamedMap> ? RouteNamedMap : RouteMapGeneric

export type RouteLocationRawI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n
? RouteLocationAsStringI18n | RouteLocationAsRelativeGeneric | RouteLocationAsPathGeneric
:
| _LiteralUnion<RouteLocationAsStringTypedList<RouteMapI18n>[Name], string>
| RouteLocationAsRelativeTypedList<RouteMapI18n>[Name]

export type RouteLocationResolvedI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n
? RouteLocationResolvedGeneric
: RouteLocationResolvedTypedList<RouteMapI18n>[Name]

export interface RouteLocationNormalizedLoadedTypedI18n<
RouteMapI18n extends RouteMapGeneric = RouteMapGeneric,
Name extends keyof RouteMapI18n = keyof RouteMapI18n
> extends RouteLocationNormalizedLoadedGeneric {
name: Extract<Name, string | symbol>
params: RouteMapI18n[Name]['params']
}
export type RouteLocationNormalizedLoadedTypedListI18n<RouteMapOriginal extends RouteMapGeneric = RouteMapGeneric> = {
[N in keyof RouteMapOriginal]: RouteLocationNormalizedLoadedTypedI18n<RouteMapOriginal, N>
}
export type RouteLocationNormalizedLoadedI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n
? RouteLocationNormalizedLoadedGeneric
: RouteLocationNormalizedLoadedTypedListI18n<RouteMapI18n>[Name]

type _LiteralUnion<LiteralType, BaseType extends string = string> = LiteralType | (BaseType & Record<never, never>)

export type RouteLocationAsStringI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n
? string
: _LiteralUnion<RouteLocationAsStringTypedList<RouteMapI18n>[Name], string>

export type RouteLocationAsStringI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n
? string
: _LiteralUnion<RouteLocationAsStringTypedList<RouteMapI18n>[Name], string>

export type RouteLocationAsRelativeI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n
? RouteLocationAsRelativeGeneric
: RouteLocationAsRelativeTypedList<RouteMapI18n>[Name]

export type RouteLocationAsPathI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n ? RouteLocationAsPathGeneric : RouteLocationAsPathTypedList<RouteMapI18n>[Name]

/**
* Helper to generate a type safe version of the {@link RouteLocationAsRelative} type.
*/
export interface RouteLocationAsRelativeTyped<
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this the same type exported by the router?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm most of the types should be the same except suffixed with I18n, which this type seems to be missing. This might conflict with existing router types 🫣 going to look into it.

Reasoning behind redefining the same types but suffixed is to use the RouteMapI18n type, perhaps there's a better way to go about this that I'm unaware of.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn’t extending the same route map from Vue router works? Or do you need to have two separate route maps?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're generating routes for each locale (depending on the routing strategy) and custom localized route the generated types are localized, this PR generates RouteMapI18n (which ironically contains the routes without localization) to be used for the i18n routing utility functions.

Some examples to illustrate the problem we're trying to solve:

Named route completion
image

Path completion (not desirable when using custom routes)
image

I guess this is specific to i18n, I doubt there are other use cases where generating a separate map is necessary πŸ€”

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would need to try it out to see the needed final types. I was hoping the types were flexible enough to at least keep things typer for the router methods. I18n methods could use extra types for localized route names (that seems to be the main difference)

RouteMapI18n extends RouteMapGeneric = RouteMapGeneric,
Name extends keyof RouteMapI18n = keyof RouteMapI18n
> extends RouteLocationAsRelativeGeneric {
name?: Extract<Name, string | symbol>
params?: RouteMapI18n[Name]['paramsRaw']
}
}

declare global {
var $t: Composer['t']
var $rt: Composer['rt']
Expand Down
2 changes: 1 addition & 1 deletion src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export default defineNuxtModule<NuxtI18nOptions>({
/**
* setup nuxt/pages
*/
setupPages(ctx, nuxt)
await setupPages(ctx, nuxt)

/**
* ignore `/` during prerender when using prefixed routing
Expand Down
Loading