Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,18 @@
Before you declare a bug fixed or behavior supported, check the relevant public contract: docs, types, and tests. Distinguish runtime behavior from behavior users can rely on. Report gaps or conflicts before you choose a fix.

In codegen specs (e.g. `generateRouteResolver.spec.ts`), assert generated code with inline snapshots or `toContain`. Do not evaluate generated code with `new Function`. Test runtime behavior in the runtime matcher specs.

## Skills

### `skills/migrate-v5-experimental-router`

Keep this skill in sync with file-based routing, the experimental router, param parsers, and scroll restoration. Reference paths below are relative to this skill.

#### Behavior and features

- Update the matching reference when you change `experimental_createRouter`, the resolver, matchers, param parsers, `definePage()` resolver codegen, `ScrollRestoration`, or `onRouteRendered` behavior.
- When you implement a missing feature (for example `addRoute()`, `props`, or `definePage()` path overrides), remove it from `references/limitations.md` and document the new API.

#### Diagnostics

Add new diagnostics used by these features to `references/diagnostics.md`.
65 changes: 65 additions & 0 deletions skills/migrate-v5-experimental-router/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
name: migrate-v5-experimental-router
disable-model-invocation: true
description: Step-by-step help to migrate your Vue Router app to file-based routing, the Vue Router 5 experimental router, param parsers, or scroll restoration.
---

# Migrate to the Vue Router 5 experimental router

The experimental router replaces the `routes` array and the path-ranking matcher with a **resolver**: a fixed, ordered list of route records. Each record has matcher objects for its path, query, and hash. File-based routing generates this resolver at build time. Param parsers convert URL strings to typed values (and back) inside the matchers.

> [!WARNING]
> The experimental router is **not production-ready**. APIs can change in a minor release. Tell the user this before changing the app. For a production app, confirm that the user has authorized experimental APIs; ask only if their request leaves this unclear.

## Migration phases

Select the phases needed for the user’s request. For a full migration, use the order below. Verify each phase before you continue. Commit only when the user asks.

| Phase | What changes | Router | Reference |
| ----- | --------------------------------------------------------------- | ---------------------- | ---------------------------------------------------------------------- |
| 0 | Audit the app. Find blockers. | any | [references/audit.md](references/audit.md) |
| 1 | `routes` array to `src/pages/` files | stable | [references/file-based-routing.md](references/file-based-routing.md) |
| 2 | `createRouter` to `experimental_createRouter` + `auto-resolver` | experimental | [references/experimental-router.md](references/experimental-router.md) |
| 3 | Manual coercion and `:id(\\d+)` to param parsers | experimental | [references/param-parsers.md](references/param-parsers.md) |
| 4 | `scrollBehavior` to `ScrollRestoration` | stable or experimental | [references/scroll-restoration.md](references/scroll-restoration.md) |

Phase 4 does not depend on phases 1 to 3. `ScrollRestoration` and `onRouteRendered()` also work with the stable `createRouter()`. You can do it first or alone.

If the app cannot use file-based routing, skip phase 1 and write the resolver by hand. See [references/fixed-resolver.md](references/fixed-resolver.md).

## Before you start: check the blockers

Before phase 2, read [references/limitations.md](references/limitations.md). Check dynamic routing, `beforeEnter`, route-record `props`, path overrides, and custom query parsing. The reference gives the current status and workarounds.

If the audit finds a blocker that has no workaround, stop and report it to the user. Do not invent an API.

## Rules

1. **Check the public contract.** Before you say a feature works, check the docs (`packages/docs/experimental/`, `packages/docs/file-based-routing/`), the types (`vue-router/experimental` exports), and the tests. Runtime behavior that has no docs or tests can change.
2. **Import from the correct entry.** Router factory, matchers, parsers, scroll restoration, data loaders: `vue-router/experimental`. Generated resolver: `vue-router/auto-resolver`. Generated routes (stable router only): `vue-router/auto-routes`. `RouterLink`, `RouterView`, `useRoute`, `useRouter`, history factories: `vue-router`.
3. **Install plugins before the router.** `app.use(DataLoaderPlugin, { router })` and `app.use(ScrollRestoration, { router, ... })` come before `app.use(router)`.
4. **Register `RouterLink` and `RouterView` yourself.** The experimental router `install()` does not register them.
5. **Use array query values.** `router.push({ query: { q: ['vue'] } })`. Non-array values warn (`D0002`).
6. **Use `null` to remove an optional param.** `undefined` or `''` warns (`R0122`).
7. **Do not put variables in `definePage()`.** It is extracted at build time (`B0002`). Imports are allowed.

## Verification

After each selected phase, use the project’s existing scripts and test tools:

1. Start the dev server. For file-based routing, check that the plugin writes the route map `.d.ts`.
2. Run the type check (`vue-tsc --noEmit` or the project script).
3. Run the relevant unit and e2e tests, if available.
4. Test affected routes and representative dynamic, 404, and redirect cases. Use back/forward. For scroll changes, check capture and restoration.
5. Read the browser console and build output. Resolve migration-related warnings and errors; explain informational diagnostics. Codes are in [references/diagnostics.md](references/diagnostics.md).

## Reference index

- [audit.md](references/audit.md): find what the app uses, with search commands.
- [file-based-routing.md](references/file-based-routing.md): phase 1, file names, `definePage()`, HMR.
- [experimental-router.md](references/experimental-router.md): phase 2, behavior changes.
- [param-parsers.md](references/param-parsers.md): phase 3, path, query, hash params.
- [fixed-resolver.md](references/fixed-resolver.md): hand-written resolver without file-based routing.
- [scroll-restoration.md](references/scroll-restoration.md): phase 4, `scrollBehavior` patterns.
- [limitations.md](references/limitations.md): not implemented features and workarounds.
- [diagnostics.md](references/diagnostics.md): diagnostic codes you can see during the migration.
86 changes: 86 additions & 0 deletions skills/migrate-v5-experimental-router/references/audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Phase 0: Audit the app

Find what the app uses before you change code. Write the results as a short list for the user. Mark each item as **OK**, **needs change**, or **blocker**.

## 1. Versions and setup

- `vue-router` must be `>=5`. Check `package.json` and the lockfile.
- `unplugin-vue-router` must be removed. Its imports move to `vue-router/vite`, `vue-router/unplugin`, `vue-router/auto-routes`, `vue-router/experimental`. See `packages/docs/guide/migration/v4-to-v5.md`.
- Find the bundler: Vite, webpack, Rollup, esbuild, Rspack. HMR for routes only works with Vite.
- Find the package manager from the lockfile.

## 2. Search commands

Run these from the app root. Exclude `node_modules` and build output.

```sh
# router creation and options
rg -n "createRouter\(|experimental_createRouter\(" src
rg -n "scrollBehavior|parseQuery|stringifyQuery|linkActiveClass|strict:|sensitive:" src

# dynamic routing: BLOCKER for phase 2
rg -n "\.addRoute\(|\.removeRoute\(|\.clearRoutes\(|\.hasRoute\(|\.getRoutes\(" src
rg -n "setupLayouts|routes\.push\(|routes\.unshift\(" src

# record features that change
rg -n "beforeEnter" src
rg -n "props:\s*(true|\{|\()" src
rg -n "alias:" src
rg -n "redirect:" src
rg -n "children:" src

# param patterns: candidates for param parsers
rg -n "path:\s*['\"\`][^'\"\`]*:\w+\(" src # custom regexp like :id(\\d+)
rg -n "Number\(route\.params|parseInt\(route\.params|route\.params\.\w+ as" src
rg -n "route\.query\.\w+" src

# navigation that changes meaning
rg -n "push\(\{\s*(params|query|hash):" src # relative object locations
rg -n "push\(\{\s*name:" src # named locations
rg -n "query:\s*\{" src

# in-component guards and meta
rg -n "onBeforeRouteLeave|onBeforeRouteUpdate|beforeRouteEnter|beforeRouteLeave|beforeRouteUpdate" src
rg -n "\.meta\.\w+" src
```

## 3. Classify the findings

| Finding | Status | Where to fix |
| ---------------------------------------------- | ------------------------------- | ------------------------------------------------------------------------------ |
| `addRoute` / `removeRoute` / `clearRoutes` | blocker for phase 2 | [limitations.md](limitations.md#dynamic-routing) |
| `setupLayouts(routes)` or `routes.push(...)` | blocker for phase 2 | [limitations.md](limitations.md#dynamic-routing) |
| `beforeEnter` | needs change in phase 2 | [experimental-router.md](experimental-router.md#beforeenter) |
| `props` on records | needs change in phase 2 | [experimental-router.md](experimental-router.md#props) |
| `parseQuery` / `stringifyQuery` | needs change in phase 2 | [experimental-router.md](experimental-router.md#query-values-are-arrays) |
| `route.query.x` read as a string | needs change in phase 2 | [experimental-router.md](experimental-router.md#query-values-are-arrays) |
| `push({ params })` without `name` | needs change in phase 2 | [experimental-router.md](experimental-router.md#relative-and-named-navigation) |
| `push({ name })` that relies on current params | needs change in phase 2 | [experimental-router.md](experimental-router.md#relative-and-named-navigation) |
| relative `redirect` on a record | needs change in phase 2 | [experimental-router.md](experimental-router.md#redirects) |
| `:id(\\d+)` or `Number(route.params.id)` | optional, phase 3 | [param-parsers.md](param-parsers.md) |
| `scrollBehavior` | optional, phase 4 | [scroll-restoration.md](scroll-restoration.md) |
| `children`, `alias`, named views, `meta` | OK, file conventions cover them | [file-based-routing.md](file-based-routing.md) |
| in-component guards | OK, they still run | |

## 4. Make a route map

Summarize the affected routes and exceptions. Include paths, names, component files, params, meta, redirects, aliases, and guards where they affect the migration. Use this map to name files in phase 1 and choose verification cases. Make a full table only when the migration needs a record-by-record map.

Example:

| Path | Name | Component | Notes |
| ------------------ | ----------- | -------------------- | -------------------------- |
| `/` | `home` | `views/Home.vue` | |
| `/users` | `users` | `views/Users.vue` | layout, has `<RouterView>` |
| `/users/:id(\\d+)` | `user` | `views/User.vue` | `props: true` |
| `/admin` | | `layouts/Admin.vue` | `meta.requiresAuth` |
| `/:pathMatch(.*)*` | `not-found` | `views/NotFound.vue` | |

## 5. Set the migration scope

Use the phases requested by the user. Ask which phases to do only when the request is unclear. Common scopes:

- "File-based routing only": do phase 1, keep the stable router.
- "Typed params": phases 1 to 3.
- "Only fix scroll": phase 4 alone, with the stable router.
- "No file-based routing, but the new matcher": phase 2 with a hand-written resolver ([fixed-resolver.md](fixed-resolver.md)).
49 changes: 49 additions & 0 deletions skills/migrate-v5-experimental-router/references/diagnostics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Diagnostics during the migration

Vue Router reports problems with stable codes. Runtime codes appear in the browser console (dev only). Build codes (`B`) appear in the dev server or build output. Source: `packages/router/src/diagnostics.ts` and `packages/router/src/unplugin/diagnostics.ts`. Read the `why` and `fix` text in those files for the current wording.

## Runtime

| Code | Cause | Fix |
| ------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------ |
| `D0001` | A record uses `beforeEnter` (experimental router) | Move the check to `meta` + `router.beforeEach()` |
| `D0002` | A query value is not an array (experimental router) | `{ key: [value] }`. Use `undefined` to remove the key. |
| `R0008` | Invalid redirect. In the experimental router: a relative record redirect | Return a name or an absolute path |
| `R0122` | An optional path param is removed with `undefined` or `''` | Use `null` |
| `R0040` | `el: '#x'` has no element with id `x`, but `querySelector` matches | Use an id, or a selector without `#` |
| `R0041` | Invalid selector in `el` | `` `#${CSS.escape(id)}` `` |
| `R0042` | No element for the `el` selector | Check the selector, or guard against missing elements |
| `R0043` | Two active `useScrollRestoration()` calls share a key with different functions | Give each call its own `key` |
| `R0044` | `useScrollRestoration()` without the plugin | `app.use(ScrollRestoration, {...})` before `app.use(router)` |
| `R1007` | `DataLoaderPlugin` installed twice | Install it once |
| `R1008` | Data loaders are experimental | Information only |
| `R1009` | A loader returns `new NavigationResult()` | Call `reroute(to)` |

Plain warnings (no code) from the experimental router:

- `Cannot resolve relative location "..." without a "name" or a current location. This will crash in production.`: pass `router.currentRoute.value` to `router.resolve()`.
- `No match found for location with path "..."`: add a catch-all page or fix the link.
- `Record "x" not found` (thrown): the route name does not exist. Use the generated name.
- `A "hash" should always start with the character "#"`: add `#`.
- `Query param "x" ... uses raw param parser ... format: 'value'`: set `format: 'array'`.
- A console error for a missing custom parser: create `src/params/<name>.ts`.

## Build (file-based routing)

| Code | Cause |
| ------------------ | ----------------------------------------------------- |
| `B0001` / `B0003` | Syntax error in `definePage()` |
| `B0002` | `definePage()` uses a `<script setup>` binding |
| `B0004` | `name` is not a string literal or `false` |
| `B0005` | `path` is not a string literal |
| `B0006` | Unsupported `default` value for a query param |
| `B0007` / `B0008` | `alias` is not a string literal or an array of them |
| `B0012` to `B0015` | Invalid `<route>` block content or language |
| `B0017` | A param without a name in a file name (`[]`) |
| `B0018` | A parser is re-exported, raw detection fails |
| `B0019` | Unknown parser name in a route |
| `B0020` | Duplicate `definePage()` calls |
| `B0021` | A parser is set in the file name and in `params.path` |
| `B0022` | More than one hash param in a route |
| `B0023` | Invalid custom `re` for a path param |
| `B0024` | A custom `re` for a repeatable param can match `/` |
Loading
Loading