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: add generic location types #2264

Merged
merged 28 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f92282b
feat: wip typed routes
posva Jun 3, 2024
51edf69
refactor: migrate guard types
posva Jun 10, 2024
40c87f6
chore: tests
posva Jun 10, 2024
3d0bde3
ci: upgrade tests
posva Jun 10, 2024
bc58364
ci: avoid double run
posva Jun 10, 2024
63f6a67
ci: remove patch check coverage
posva Jun 10, 2024
c8173b0
refactor: remove old RouteLocation
posva Jun 10, 2024
274d245
refactor: remove old RouteRecordName
posva Jun 10, 2024
4e0d730
refactor: remove old types
posva Jun 11, 2024
839e45b
refactor: fix todos
posva Jun 11, 2024
57601c0
docs: line
posva Jun 11, 2024
51f0182
docs: fix links
posva Jun 11, 2024
e03edfd
build: update release script
posva Jun 11, 2024
215107f
release: vue-router@4.4.0-alpha.0
posva Jun 11, 2024
6a90774
feat: type useLink
posva Jun 12, 2024
c990b0e
docs: typed routes update
posva Jun 12, 2024
36acc3d
chore: expose useLink return type
posva Jun 12, 2024
8f16b16
docs: fix
posva Jun 12, 2024
27e2970
release: vue-router@4.4.0-alpha.1
posva Jun 12, 2024
a7a8452
fix: allow arbitrary strings in RouteLocationRaw
posva Jun 12, 2024
d1e3e95
release: vue-router@4.4.0-alpha.2
posva Jun 12, 2024
958a1ad
docs: note about generic route record name
posva Jun 13, 2024
3675b05
refactor: remove RouteRecordNameGeneric
posva Jun 13, 2024
2510170
refactor: use RouteRecordNameGeneric
posva Jun 13, 2024
c8a6f34
test: migrate to vitest
posva Jun 17, 2024
f7f78f0
test: migrate type tests from uvr
posva Jun 17, 2024
abe223d
feat: add a clearRoutes method
posva Jun 19, 2024
edff284
release: vue-router@4.4.0-alpha.3
posva Jun 19, 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
19 changes: 10 additions & 9 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ name: test

on:
push:
branches:
- main
paths-ignore:
- 'packages/docs/**'
- 'packages/playground/**'
pull_request:
branches:
- main
paths-ignore:
- 'packages/docs/**'
- 'packages/playground/**'
Expand All @@ -15,14 +19,12 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
version: 8.5.0
- uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'pnpm'
node-version: 'lts/*'
cache: pnpm
- name: 'BrowserStack Env Setup'
uses: 'browserstack/github-actions/setup-env@master'
# forks do not have access to secrets so just skip this
Expand All @@ -34,10 +36,9 @@ jobs:
- run: pnpm install
- run: pnpm run lint
- run: pnpm run -r test:types
- run: pnpm run -r test:unit
- run: pnpm run -r build
- run: pnpm run -r build:dts
- run: pnpm run -r test:dts
- run: pnpm run -r test:unit

# e2e tests that that run locally
- run: pnpm run -r test:e2e:ci
Expand Down
3 changes: 3 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
coverage:
status:
patch: off
4 changes: 2 additions & 2 deletions packages/docs/guide/advanced/navigation-guards.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ Global before guards are called in creation order, whenever a navigation is trig

Every guard function receives two arguments:

- **`to`**: the target route location [in a normalized format](../../api/interfaces/RouteLocationNormalized.md) being navigated to.
- **`from`**: the current route location [in a normalized format](../../api/interfaces/RouteLocationNormalized.md) being navigated away from.
- **`to`**: the target route location [in a normalized format](../../api/#RouteLocationNormalized) being navigated to.
- **`from`**: the current route location [in a normalized format](../../api/#RouteLocationNormalized) being navigated away from.

And can optionally return any of the following values:

Expand Down
68 changes: 62 additions & 6 deletions packages/docs/guide/advanced/typed-routes.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,66 @@
# Typed Routes (v4.1.0+)
# Typed Routes <Badge type="tip" text="v4.4.0+" />

::: danger ‼️ Experimental feature

Starting from v4.1.0, we are introducing a new feature called Typed Routes. This **experimental** feature is enabled through a Vite/webpack/Rollup plugin.
::: danger
‼️ Experimental feature
:::

![RouterLink to autocomplete](https://user-images.githubusercontent.com/664177/176442066-c4e7fa31-4f06-4690-a49f-ed0fd880dfca.png)

[Check the v4.1 release notes](https://github.com/vuejs/router/releases/tag/v4.1.0) for more information about this feature.
[Check out the plugin](https://github.com/posva/unplugin-vue-router) GitHub repository for installation instructions and documentation.
It's possible to configure the router to have a _map_ of typed routes. While this can be done manually, it is recommended to use the [unplugin-vue-router](https://github.com/posva/unplugin-vue-router) plugin to generate the routes and the types automatically.

## Manual Configuration

Here is an example of how to manually configure typed routes:

```ts
// import the `RouteRecordInfo` type from vue-router to type your routes
import type { RouteRecordInfo } from 'vue-router'

// Define an interface of routes
export interface RouteNamedMap {
// each key is a name
home: RouteRecordInfo<
// here we have the same name
'home',
// this is the path, it will appear in autocompletion
'/',
// these are the raw params. In this case, there are no params allowed
Record<never, never>,
// these are the normalized params
Record<never, never>
>
// repeat for each route..
// Note you can name them whatever you want
'named-param': RouteRecordInfo<
'named-param',
'/:name',
{ name: string | number }, // raw value
{ name: string } // normalized value
>
'article-details': RouteRecordInfo<
'article-details',
'/articles/:id+',
{ id: Array<number | string> },
{ id: string[] }
>
'not-found': RouteRecordInfo<
'not-found',
'/:path(.*)',
{ path: string },
{ path: string }
>
}

// Last, you will need to augment the Vue Router types with this map of routes
declare module 'vue-router' {
interface TypesConfig {
RouteNamedMap: RouteNamedMap
}
}
```

::: tip

This is indeed tedious and error-prone. That's why it's recommended to use [unplugin-vue-router](https://github.com/posva/unplugin-vue-router) to generate the routes and the types automatically.

:::
14 changes: 7 additions & 7 deletions packages/docs/guide/essentials/active-links.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,22 @@ The RouterLink component adds two CSS classes to active links, `router-link-acti

## When are links active?

A RouterLink is considered to be ***active*** if:
A RouterLink is considered to be **_active_** if:

1. It matches the same route record (i.e. configured route) as the current location.
2. It has the same values for the `params` as the current location.

If you're using [nested routes](./nested-routes), any links to ancestor routes will also be considered active if the relevant `params` match.

Other route properties, such as the [`query`](../../api/interfaces/RouteLocationNormalized#query), are not taken into account.
Other route properties, such as the [`query`](../../api/interfaces/RouteLocationBase.html#query), are not taken into account.

The path doesn't necessarily need to be a perfect match. For example, using an [`alias`](./redirect-and-alias#Alias) would still be considered a match, so long as it resolves to the same route record and `params`.

If a route has a [`redirect`](./redirect-and-alias#Redirect), it won't be followed when checking whether a link is active.

## Exact active links

An ***exact*** match does not include ancestor routes.
An **_exact_** match does not include ancestor routes.

Let's imagine we have the following routes:

Expand All @@ -34,9 +34,9 @@ const routes = [
{
path: 'role/:roleId',
component: Role,
}
]
}
},
],
},
]
```

Expand All @@ -51,7 +51,7 @@ Then consider these two links:
</RouterLink>
```

If the current location path is `/user/erina/role/admin` then these would both be considered _active_, so the class `router-link-active` would be applied to both links. But only the second link would be considered _exact_, so only that second link would have the class `router-link-exact-active`.
If the current location path is `/user/erina/role/admin` then these would both be considered _active_, so the class `router-link-active` would be applied to both links. But only the second link would be considered _exact_, so only that second link would have the class `router-link-exact-active`.

## Configuring the classes

Expand Down
17 changes: 10 additions & 7 deletions packages/docs/guide/essentials/dynamic-matching.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ A _param_ is denoted by a colon `:`. When a route is matched, the value of its _

You can have multiple _params_ in the same route, and they will map to corresponding fields on `route.params`. Examples:

| pattern | matched path | route.params |
| ------------------------------ | ------------------------ | -------------------------------------- |
| /users/:username | /users/eduardo | `{ username: 'eduardo' }` |
| pattern | matched path | route.params |
| ------------------------------ | ------------------------ | ---------------------------------------- |
| /users/:username | /users/eduardo | `{ username: 'eduardo' }` |
| /users/:username/posts/:postId | /users/eduardo/posts/123 | `{ username: 'eduardo', postId: '123' }` |

In addition to `route.params`, the `route` object also exposes other useful information such as `route.query` (if there is a query in the URL), `route.hash`, etc. You can check out the full details in the [API Reference](../../api/interfaces/RouteLocationNormalized.md).
In addition to `route.params`, the `route` object also exposes other useful information such as `route.query` (if there is a query in the URL), `route.hash`, etc. You can check out the full details in the [API Reference](../../api/#RouteLocationNormalized).

A working demo of this example can be found [here](https://codesandbox.io/s/route-params-vue-router-examples-mlb14?from-embed&initialpath=%2Fusers%2Feduardo%2Fposts%2F1).

Expand Down Expand Up @@ -69,9 +69,12 @@ import { useRoute } from 'vue-router'

const route = useRoute()

watch(() => route.params.id, (newId, oldId) => {
// react to route changes...
})
watch(
() => route.params.id,
(newId, oldId) => {
// react to route changes...
}
)
</script>
```

Expand Down
2 changes: 1 addition & 1 deletion packages/docs/guide/migration/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ Note this will work if `path` was `/parent/` as the relative location `home` to

Decoded values in `params`, `query`, and `hash` are now consistent no matter where the navigation is initiated (older browsers will still produce unencoded `path` and `fullPath`). The initial navigation should yield the same results as in-app navigations.

Given any [normalized route location](/api/interfaces/RouteLocationNormalized.md):
Given any [normalized route location](/api/#RouteLocationNormalized):

- Values in `path`, `fullPath` are not decoded anymore. They will appear as provided by the browser (most browsers provide them encoded). e.g. directly writing on the address bar `https://example.com/hello world` will yield the encoded version: `https://example.com/hello%20world` and both `path` and `fullPath` will be `/hello%20world`.
- `hash` is now decoded, that way it can be copied over: `router.push({ hash: $route.hash })` and be used directly in [scrollBehavior](/api/interfaces/RouterOptions.md#scrollBehavior)'s `el` option.
Expand Down
2 changes: 1 addition & 1 deletion packages/docs/typedoc-markdown.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const __dirname = path.dirname(new URL(import.meta.url).pathname)
const DEFAULT_OPTIONS = {
// disableOutputCheck: true,
cleanOutputDir: true,
excludeInternal: true,
excludeInternal: false,
readme: 'none',
out: path.resolve(__dirname, './api'),
entryDocument: 'index.md',
Expand Down
4 changes: 2 additions & 2 deletions packages/docs/zh/guide/migration/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ createRouter({
})
```

**原因**: Vue支持的所有浏览器都支持 [HTML5 History API](https://developer.mozilla.org/zh-CN/docs/Web/API/History_API),因此我们不再需要使用 `location.hash`,而可以直接使用 `history.pushState()`。
**原因**: Vue 支持的所有浏览器都支持 [HTML5 History API](https://developer.mozilla.org/zh-CN/docs/Web/API/History_API),因此我们不再需要使用 `location.hash`,而可以直接使用 `history.pushState()`。

### 删除了 `*`(星标或通配符)路由

Expand Down Expand Up @@ -436,7 +436,7 @@ const routes = [

<!-- TODO: translate chinese API entries -->

给定任何[规范化的路由地址](/zh/api/interfaces/RouteLocationNormalized.md):
给定任何[规范化的路由地址](/zh/api/#RouteLocationNormalized):

- `path`, `fullPath`中的值不再被解码了。例如,直接在地址栏上写 "<https://example.com/hello> world",将得到编码后的版本:"https://example.com/hello%20world",而 "path "和 "fullPath "都是"/hello%20world"。
- `hash` 现在被解码了,这样就可以复制过来。`router.push({ hash: $route.hash })` 可以直接用于 [scrollBehavior](/zh/api/interfaces/RouterOptions.md#Properties-scrollBehavior) 的 `el` 配置中。
Expand Down
64 changes: 25 additions & 39 deletions packages/playground/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -183,51 +183,37 @@
</div>
</template>

<script lang="ts">
import { defineComponent, inject, computed, ref } from 'vue'
<script lang="ts" setup>
import { inject, computed, ref } from 'vue'
import { scrollWaiter } from './scrollWaiter'
import { useLink, useRoute } from 'vue-router'
import { useLink, useRoute, RouterLink } from 'vue-router'
import AppLink from './AppLink.vue'

export default defineComponent({
name: 'App',
components: { AppLink },
setup() {
const route = useRoute()
const state = inject('state')
const viewName = ref('default')
const route = useRoute()
const state = inject('state')
const viewName = ref('default')

useLink({ to: '/' })
useLink({ to: '/documents/hello' })
useLink({ to: '/children' })
useLink({ to: '/' })
useLink({ to: '/documents/hello' })
useLink({ to: '/children' })

const currentLocation = computed(() => {
const { matched, ...rest } = route
return rest
})
const currentLocation = computed(() => {
const { matched, ...rest } = route
return rest
})

function flushWaiter() {
scrollWaiter.flush()
}
function setupWaiter() {
scrollWaiter.add()
}
function flushWaiter() {
scrollWaiter.flush()
}
function setupWaiter() {
scrollWaiter.add()
}

const nextUserLink = computed(
() => '/users/' + String((Number(route.params.id) || 0) + 1)
)
const nextUserLink = computed(
() => '/users/' + String((Number(route.params.id) || 0) + 1)
)

return {
currentLocation,
nextUserLink,
state,
flushWaiter,
setupWaiter,
viewName,
toggleViewName() {
viewName.value = viewName.value === 'default' ? 'other' : 'default'
},
}
},
})
function toggleViewName() {
viewName.value = viewName.value === 'default' ? 'other' : 'default'
}
</script>
43 changes: 43 additions & 0 deletions packages/playground/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { ComponentPublicInstance } from 'vue'
import { router, routerHistory } from './router'
import { globalState } from './store'
import App from './App.vue'
import { useRoute, type ParamValue, type RouteRecordInfo } from 'vue-router'

declare global {
interface Window {
Expand All @@ -29,3 +30,45 @@ app.provide('state', globalState)
app.use(router)

window.vm = app.mount('#app')

export interface RouteNamedMap {
home: RouteRecordInfo<'home', '/', Record<never, never>, Record<never, never>>
'/[name]': RouteRecordInfo<
'/[name]',
'/:name',
{ name: ParamValue<true> },
{ name: ParamValue<false> }
>
'/[...path]': RouteRecordInfo<
'/[...path]',
'/:path(.*)',
{ path: ParamValue<true> },
{ path: ParamValue<false> }
>
}

declare module 'vue-router' {
interface TypesConfig {
RouteNamedMap: RouteNamedMap
}
}

function _ok() {
const r = useRoute()

if (r.name === '/[name]') {
r.params.name.toUpperCase()
// @ts-expect-error: Not existing route
} else if (r.name === 'nope') {
console.log('nope')
}

router.push({
name: '/[name]',
params: { name: 'hey' },
})

router
.resolve({ name: '/[name]', params: { name: 2 } })
.params.name.toUpperCase()
}
Loading
Loading