feat(nuxt): add experimental `typedPages` option (#20367)

This commit is contained in:
Eduardo San Martin Morote 2023-05-09 19:08:07 +02:00 committed by GitHub
parent 80d7899f49
commit 5781cf1569
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 339 additions and 35 deletions

View File

@ -43,6 +43,10 @@
{
"pattern": "@nuxt/test-utils",
"group": "external"
},
{
"pattern": "#vue-router",
"group": "external"
}
]
}

View File

@ -94,6 +94,7 @@
"unenv": "^1.4.1",
"unimport": "^3.0.6",
"unplugin": "^1.3.1",
"unplugin-vue-router": "^0.6.4",
"untyped": "^1.3.2",
"vue": "^3.2.47",
"vue-bundle-renderer": "^1.0.3",

View File

@ -1,6 +1,6 @@
import type { ComputedRef, DefineComponent, PropType } from 'vue'
import { computed, defineComponent, h, onBeforeUnmount, onMounted, ref, resolveComponent } from 'vue'
import type { RouteLocation, RouteLocationRaw } from 'vue-router'
import type { RouteLocation, RouteLocationRaw } from '#vue-router'
import { hasProtocol, parseQuery, parseURL, withTrailingSlash, withoutTrailingSlash } from 'ufo'
import { preloadRouteComponents } from '../composables/preload'
@ -24,8 +24,8 @@ export type NuxtLinkOptions = {
export type NuxtLinkProps = {
// Routing
to?: string | RouteLocationRaw
href?: string | RouteLocationRaw
to?: RouteLocationRaw
href?: RouteLocationRaw
external?: boolean
replace?: boolean
custom?: boolean
@ -81,12 +81,12 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
props: {
// Routing
to: {
type: [String, Object] as PropType<string | RouteLocationRaw>,
type: [String, Object] as PropType<RouteLocationRaw>,
default: undefined,
required: false
},
href: {
type: [String, Object] as PropType<string | RouteLocationRaw>,
type: [String, Object] as PropType<RouteLocationRaw>,
default: undefined,
required: false
},

View File

@ -1,5 +1,5 @@
import type { Component } from 'vue'
import type { RouteLocationRaw, Router } from 'vue-router'
import type { RouteLocationRaw, Router } from '#vue-router'
import { useNuxtApp } from '../nuxt'
import { useRouter } from './router'

View File

@ -1,6 +1,6 @@
import { getCurrentInstance, inject, onUnmounted } from 'vue'
import type { Ref } from 'vue'
import type { NavigationFailure, NavigationGuard, RouteLocationNormalized, RouteLocationNormalizedLoaded, RouteLocationPathRaw, RouteLocationRaw, Router } from 'vue-router'
import type { NavigationFailure, NavigationGuard, RouteLocationNormalized, RouteLocationPathRaw, RouteLocationRaw, Router, useRoute as _useRoute, useRouter as _useRouter } from '#vue-router'
import { sanitizeStatusCode } from 'h3'
import { hasProtocol, joinURL, parseURL } from 'ufo'
@ -11,11 +11,11 @@ import { useState } from './state'
import type { PageMeta } from '#app'
export const useRouter = () => {
export const useRouter: typeof _useRouter = () => {
return useNuxtApp()?.$router as Router
}
export const useRoute = (): RouteLocationNormalizedLoaded => {
export const useRoute: typeof _useRoute = () => {
if (process.dev && isProcessingMiddleware()) {
console.warn('[nuxt] Calling `useRoute` within middleware may lead to misleading results. Instead, use the (to, from) arguments passed to the middleware to access the new and old routes.')
}

View File

@ -1,7 +1,7 @@
/* eslint-disable no-use-before-define */
import { getCurrentInstance, reactive } from 'vue'
import type { App, Ref, VNode, onErrorCaptured } from 'vue'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import type { RouteLocationNormalizedLoaded } from '#vue-router'
import type { HookCallback, Hookable } from 'hookable'
import { createHooks } from 'hookable'
import { getContext } from 'unctx'

View File

@ -1,10 +1,14 @@
import { existsSync, readdirSync } from 'node:fs'
import { mkdir, readFile } from 'node:fs/promises'
import { addComponent, addPlugin, addTemplate, addVitePlugin, addWebpackPlugin, defineNuxtModule, findPath, updateTemplates } from '@nuxt/kit'
import { join, relative, resolve } from 'pathe'
import { dirname, join, relative, resolve } from 'pathe'
import { genImport, genObjectFromRawEntries, genString } from 'knitwork'
import escapeRE from 'escape-string-regexp'
import { joinURL } from 'ufo'
import type { NuxtApp, NuxtPage } from 'nuxt/schema'
import { createRoutesContext } from 'unplugin-vue-router'
import { resolveOptions } from 'unplugin-vue-router/options'
import type { EditableTreeNode, Options as TypedRouterOptions } from 'unplugin-vue-router'
import { distDir } from '../dirs'
import { normalizeRoutes, resolvePagesRoutes } from './utils'
@ -15,7 +19,9 @@ export default defineNuxtModule({
meta: {
name: 'pages'
},
setup (_options, nuxt) {
async setup (_options, nuxt) {
const useExperimentalTypedPages = nuxt.options.experimental.typedPages
const pagesDirs = nuxt.options._layers.map(
layer => resolve(layer.config.srcDir, layer.config.dir?.pages || 'pages')
)
@ -67,18 +73,82 @@ export default defineNuxtModule({
return
}
if (useExperimentalTypedPages) {
const declarationFile = './types/typed-router.d.ts'
const options: TypedRouterOptions = {
routesFolder: [],
dts: resolve(nuxt.options.buildDir, declarationFile),
logs: nuxt.options.debug,
async beforeWriteFiles (rootPage) {
rootPage.children.forEach(child => child.delete())
const pages = await resolvePagesRoutes()
await nuxt.callHook('pages:extend', pages)
function addPage (parent: EditableTreeNode, page: NuxtPage) {
// @ts-expect-error TODO: either fix types upstream or figure out another
// way to add a route without a file, which must be possible
const route = parent.insert(page.path, page.file)
if (page.meta) {
route.addToMeta(page.meta)
}
if (page.alias) {
route.addAlias(page.alias)
}
if (page.name) {
route.name = page.name
}
// TODO: implement redirect support
// if (page.redirect) {}
if (page.children) {
page.children.forEach(child => addPage(route, child))
}
}
for (const page of pages) {
addPage(rootPage, page)
}
}
}
nuxt.hook('prepare:types', ({ references }) => {
// This file will be generated by unplugin-vue-router
references.push({ path: declarationFile })
})
const context = createRoutesContext(resolveOptions(options))
const dtsFile = resolve(nuxt.options.buildDir, declarationFile)
await mkdir(dirname(dtsFile), { recursive: true })
await context.scanPages(false)
if (nuxt.options._prepare) {
// TODO: could we generate this from context instead?
const dts = await readFile(dtsFile, 'utf-8')
addTemplate({
filename: 'types/typed-router.d.ts',
getContents: () => dts
})
}
// Regenerate types/typed-router.d.ts when adding or removing pages
nuxt.hook('builder:generateApp', async (options) => {
if (!options?.filter || options.filter({ filename: 'routes.mjs' } as any)) {
await context.scanPages()
}
})
}
const runtimeDir = resolve(distDir, 'pages/runtime')
// Add $router types
nuxt.hook('prepare:types', ({ references }) => {
references.push({ types: 'vue-router' })
references.push({ types: useExperimentalTypedPages ? 'vue-router/auto' : 'vue-router' })
})
// Add vue-router route guard imports
nuxt.hook('imports:sources', (sources) => {
const routerImports = sources.find(s => s.from === '#app' && s.imports.includes('onBeforeRouteLeave'))
if (routerImports) {
routerImports.from = 'vue-router'
routerImports.from = '#vue-router'
}
})
@ -144,7 +214,7 @@ export default defineNuxtModule({
nuxt.hook('imports:extend', (imports) => {
imports.push(
{ name: 'definePageMeta', as: 'definePageMeta', from: resolve(runtimeDir, 'composables') },
{ name: 'useLink', as: 'useLink', from: 'vue-router' }
{ name: 'useLink', as: 'useLink', from: '#vue-router' }
)
})
@ -177,6 +247,7 @@ export default defineNuxtModule({
await nuxt.callHook('pages:extend', pages)
const sourceFiles = getSources(pages)
for (const key in manifest) {
if (manifest[key].isEntry) {
manifest[key].dynamicImports =
@ -185,6 +256,17 @@ export default defineNuxtModule({
}
})
// adds support for #vue-router alias
addTemplate({
filename: 'vue-router.mjs',
// TODO: use `vue-router/auto` when we have support for page metadata
getContents: () => 'export * from \'vue-router\';'
})
addTemplate({
filename: 'vue-router.d.ts',
getContents: () => `export * from '${useExperimentalTypedPages ? 'vue-router/auto' : 'vue-router'}'`
})
// Add routes template
addTemplate({
filename: 'routes.mjs',
@ -278,10 +360,13 @@ export default defineNuxtModule({
filePath: resolve(distDir, 'pages/runtime/page')
})
nuxt.options.alias['#vue-router'] = join(nuxt.options.buildDir, 'vue-router')
// Add declarations for middleware keys
nuxt.hook('prepare:types', ({ references }) => {
references.push({ path: resolve(nuxt.options.buildDir, 'types/middleware.d.ts') })
references.push({ path: resolve(nuxt.options.buildDir, 'types/layouts.d.ts') })
references.push({ path: resolve(nuxt.options.buildDir, 'vue-router.d.ts') })
})
}
})

View File

@ -1,6 +1,6 @@
import type { KeepAliveProps, TransitionProps, UnwrapRef } from 'vue'
import { getCurrentInstance } from 'vue'
import type { RouteLocationNormalized, RouteLocationNormalizedLoaded, RouteRecordRedirectOption } from 'vue-router'
import type { RouteLocationNormalized, RouteLocationNormalizedLoaded, RouteRecordRedirectOption } from '#vue-router'
import { useRoute } from 'vue-router'
import type { NuxtError } from '#app'

View File

@ -1,8 +1,8 @@
import { Suspense, Transition, computed, defineComponent, h, nextTick, onMounted, provide, reactive } from 'vue'
import type { KeepAliveProps, TransitionProps, VNode } from 'vue'
import { RouterView } from 'vue-router'
import { RouterView } from '#vue-router'
import { defu } from 'defu'
import type { RouteLocation, RouteLocationNormalized, RouteLocationNormalizedLoaded } from 'vue-router'
import type { RouteLocation, RouteLocationNormalized, RouteLocationNormalizedLoaded } from '#vue-router'
import type { RouterViewSlotProps } from './utils'
import { generateRouteKey, wrapInKeepAlive } from './utils'

View File

@ -1,12 +1,12 @@
import { computed, isReadonly, reactive, shallowRef } from 'vue'
import type { Ref } from 'vue'
import type { RouteLocation, Router } from 'vue-router'
import type { RouteLocation, Router } from '#vue-router'
import {
createMemoryHistory,
createRouter,
createWebHashHistory,
createWebHistory
} from 'vue-router'
} from '#vue-router'
import { createError } from 'h3'
import { withoutBase } from 'ufo'
@ -44,7 +44,7 @@ function createCurrentLocation (
return path + search + hash
}
export default defineNuxtPlugin({
const plugin: Plugin<{ router: Router }> = defineNuxtPlugin({
name: 'nuxt:router',
enforce: 'pre',
async setup (nuxtApp) {
@ -201,4 +201,6 @@ export default defineNuxtPlugin({
return { provide: { router } }
}
}) as Plugin<{ router: Router }>
})
export default plugin

View File

@ -1,4 +1,4 @@
import type { RouteLocationNormalized, RouterScrollBehavior } from 'vue-router'
import type { RouteLocationNormalized, RouterScrollBehavior } from '#vue-router'
import { nextTick } from 'vue'
import type { RouterConfig } from 'nuxt/schema'
import { useNuxtApp } from '#app/nuxt'

View File

@ -1,5 +1,5 @@
import { KeepAlive, h } from 'vue'
import type { RouteLocationMatched, RouteLocationNormalizedLoaded, RouterView } from 'vue-router'
import type { RouteLocationMatched, RouteLocationNormalizedLoaded, RouterView } from '#vue-router'
type InstanceOf<T> = T extends new (...args: any[]) => infer R ? R : never
type RouterViewSlot = Exclude<InstanceOf<typeof RouterView>['$slots']['default'], undefined>

View File

@ -156,6 +156,9 @@ export default defineUntypedSchema({
/** Resolve `~`, `~~`, `@` and `@@` aliases located within layers with respect to their layer source and root directories. */
localLayerAliases: true,
/** Enable the new experimental typed router using [unplugin-vue-router](https://github.com/posva/unplugin-vue-router). */
typedPages: false,
/**
* Set an alternative watcher that will be used as the watching service for Nuxt.
*

View File

@ -447,6 +447,9 @@ importers:
unplugin:
specifier: ^1.3.1
version: 1.3.1
unplugin-vue-router:
specifier: ^0.6.4
version: 0.6.4(rollup@3.21.5)(vue-router@4.1.6)(vue@3.2.47)
untyped:
specifier: ^1.3.2
version: 1.3.2
@ -1068,6 +1071,11 @@ packages:
resolution: {integrity: sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==}
engines: {node: '>=6.9.0'}
/@babel/helper-string-parser@7.21.5:
resolution: {integrity: sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==}
engines: {node: '>=6.9.0'}
dev: false
/@babel/helper-validator-identifier@7.19.1:
resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==}
engines: {node: '>=6.9.0'}
@ -1278,6 +1286,15 @@ packages:
'@babel/helper-validator-identifier': 7.19.1
to-fast-properties: 2.0.0
/@babel/types@7.21.5:
resolution: {integrity: sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/helper-string-parser': 7.21.5
'@babel/helper-validator-identifier': 7.19.1
to-fast-properties: 2.0.0
dev: false
/@cloudflare/kv-asset-handler@0.3.0:
resolution: {integrity: sha512-9CB/MKf/wdvbfkUdfrj+OkEwZ5b7rws0eogJ4293h+7b6KX5toPwym+VQKmILafNB9YiehqY0DlNrDcDhdWHSQ==}
dependencies:
@ -2505,6 +2522,25 @@ packages:
'@volar/vue-language-core': 1.6.4
typescript: 5.0.4
/@vue-macros/common@1.3.1(rollup@3.21.5)(vue@3.2.47):
resolution: {integrity: sha512-Lc5aP/8HNJD1XrnvpeNuWcCf82bZdR3auN/chA1b/1rKZgSnmQkH9f33tKO9qLwXSy+u4hpCi8Rw+oUuF1KCeg==}
engines: {node: '>=14.19.0'}
peerDependencies:
vue: ^2.7.0 || ^3.2.25
peerDependenciesMeta:
vue:
optional: true
dependencies:
'@babel/types': 7.21.5
'@rollup/pluginutils': 5.0.2(rollup@3.21.5)
'@vue/compiler-sfc': 3.3.0-beta.5
local-pkg: 0.4.3
magic-string-ast: 0.1.2
vue: 3.2.47
transitivePeerDependencies:
- rollup
dev: false
/@vue/babel-helper-vue-transform-on@1.0.2:
resolution: {integrity: sha512-hz4R8tS5jMn8lDq6iD+yWL6XNB699pGIVLk7WSJnn1dbpjaazsjZQkieJoRX6gW5zpYSCFqQ7jUquPNY65tQYA==}
@ -2540,6 +2576,15 @@ packages:
estree-walker: 2.0.2
source-map-js: 1.0.2
/@vue/compiler-core@3.3.0-beta.5:
resolution: {integrity: sha512-rwKXIMPDKBzKypcZ7Zc+i4e7ItnhlMEu9QZveek2yLxzMG0QimvZnVKB7eD21cQ9MCwEYW4bb8zjisPMJNNaqQ==}
dependencies:
'@babel/parser': 7.21.4
'@vue/shared': 3.3.0-beta.5
estree-walker: 2.0.2
source-map-js: 1.0.2
dev: false
/@vue/compiler-dom@3.2.47:
resolution: {integrity: sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ==}
dependencies:
@ -2552,6 +2597,13 @@ packages:
'@vue/compiler-core': 3.3.0-beta.3
'@vue/shared': 3.3.0-beta.3
/@vue/compiler-dom@3.3.0-beta.5:
resolution: {integrity: sha512-OsYuAzl8zHRym5TfDhCLrcTSBt71BFJXnTC9uWO+SfgqadadWZxv1piPebjtwJcODkks5OAGfdhxzKdNzzddXw==}
dependencies:
'@vue/compiler-core': 3.3.0-beta.5
'@vue/shared': 3.3.0-beta.5
dev: false
/@vue/compiler-sfc@3.2.47:
resolution: {integrity: sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ==}
dependencies:
@ -2580,6 +2632,21 @@ packages:
postcss: 8.4.23
source-map-js: 1.0.2
/@vue/compiler-sfc@3.3.0-beta.5:
resolution: {integrity: sha512-CbiY2dkzU5IG652ygLUSufLGvXPKI12TQp1PeHs9acjgFjhvSJCSKmAOaCWnXgFsAgpbipPHgAMfWJ8B0h6Sjw==}
dependencies:
'@babel/parser': 7.21.4
'@vue/compiler-core': 3.3.0-beta.5
'@vue/compiler-dom': 3.3.0-beta.5
'@vue/compiler-ssr': 3.3.0-beta.5
'@vue/reactivity-transform': 3.3.0-beta.5
'@vue/shared': 3.3.0-beta.5
estree-walker: 2.0.2
magic-string: 0.30.0
postcss: 8.4.23
source-map-js: 1.0.2
dev: false
/@vue/compiler-ssr@3.2.47:
resolution: {integrity: sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw==}
dependencies:
@ -2592,6 +2659,13 @@ packages:
'@vue/compiler-dom': 3.3.0-beta.3
'@vue/shared': 3.3.0-beta.3
/@vue/compiler-ssr@3.3.0-beta.5:
resolution: {integrity: sha512-16njciFrQ8ejVdH5tsaPbJwpkpBB2z2VWCxfC69UOylCgPxiW01syE9S/mozRvv5Ken9Sr9bd2MjebG/SEpPNg==}
dependencies:
'@vue/compiler-dom': 3.3.0-beta.5
'@vue/shared': 3.3.0-beta.5
dev: false
/@vue/devtools-api@6.5.0:
resolution: {integrity: sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==}
@ -2613,6 +2687,16 @@ packages:
estree-walker: 2.0.2
magic-string: 0.30.0
/@vue/reactivity-transform@3.3.0-beta.5:
resolution: {integrity: sha512-v/PCri6+vL8WwwgowHlIopwiqonCj9wV60ZYDdX8AMDxk9Q01h2cWh61JF4XOX7qDs7NdJ7zSVyKSz4B98OXBw==}
dependencies:
'@babel/parser': 7.21.4
'@vue/compiler-core': 3.3.0-beta.5
'@vue/shared': 3.3.0-beta.5
estree-walker: 2.0.2
magic-string: 0.30.0
dev: false
/@vue/reactivity@3.2.47:
resolution: {integrity: sha512-7khqQ/75oyyg+N/e+iwV6lpy1f5wq759NdlS1fpAhFXa8VeAIKGgk2E/C4VF59lx5b+Ezs5fpp/5WsRYXQiKxQ==}
dependencies:
@ -2651,6 +2735,10 @@ packages:
/@vue/shared@3.3.0-beta.3:
resolution: {integrity: sha512-st1SnB/Bkbb9TsieeI4TRX9TqHYIR5wvIma3ZtEben55EYSWa1q5u2BhTNgABSdH+rv3Xwfrvpwh5PmCw6Y53g==}
/@vue/shared@3.3.0-beta.5:
resolution: {integrity: sha512-ImwhHfOzuQrfA05Kx4s7J9g7QJt0sZqSlPvPdd6xj5tTEnPNNJYZOHaIP973mtuEuv4Zfh9v+CLiER6E6gtSqg==}
dev: false
/@webassemblyjs/ast@1.11.5:
resolution: {integrity: sha512-LHY/GSAZZRpsNQH+/oHqhRQ5FT7eoULcBqgfyTB5nQHogFnK3/7QoN7dLnwSE/JkUAF0SrRuclT7ODqMFtWxxQ==}
dependencies:
@ -2997,6 +3085,14 @@ packages:
resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
dev: true
/ast-walker-scope@0.4.1:
resolution: {integrity: sha512-Ro3nmapMxi/remlJdzFH0tiA7A59KDbxVoLlKWaLDrPELiftb9b8w+CCyWRM+sXZH5KHRAgv8feedW6mihvCHA==}
engines: {node: '>=14.19.0'}
dependencies:
'@babel/parser': 7.21.4
'@babel/types': 7.21.5
dev: false
/async-sema@3.1.1:
resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==}
@ -6077,6 +6173,13 @@ packages:
resolution: {integrity: sha512-65/Jky17UwSb0BuB9V+MyDpsOtXKmYwzhyl+cOa9XUiI4uV2Ouy/2voFP3+al0BjZbJgMBD8FojMpAf+Z+qn4A==}
engines: {node: 14 || >=16.14}
/magic-string-ast@0.1.2:
resolution: {integrity: sha512-P53AZrzq7hclCU6HWj88xNZHmP15DKjMmK/vBytO1qnpYP3ul4IEZlyCE0aU3JRnmgWmZPmoTKj4Bls7v0pMyA==}
engines: {node: '>=14.19.0'}
dependencies:
magic-string: 0.30.0
dev: false
/magic-string@0.30.0:
resolution: {integrity: sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==}
engines: {node: '>=12'}
@ -8352,6 +8455,33 @@ packages:
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
engines: {node: '>= 10.0.0'}
/unplugin-vue-router@0.6.4(rollup@3.21.5)(vue-router@4.1.6)(vue@3.2.47):
resolution: {integrity: sha512-9THVhhtbVFxbsIibjK59oPwMI1UCxRWRPX7azSkTUABsxovlOXJys5SJx0kd/0oKIqNJuYgkRfAgPuO77SqCOg==}
peerDependencies:
vue-router: ^4.1.0
peerDependenciesMeta:
vue-router:
optional: true
dependencies:
'@babel/types': 7.21.5
'@rollup/pluginutils': 5.0.2(rollup@3.21.5)
'@vue-macros/common': 1.3.1(rollup@3.21.5)(vue@3.2.47)
ast-walker-scope: 0.4.1
chokidar: 3.5.3
fast-glob: 3.2.12
json5: 2.2.3
local-pkg: 0.4.3
mlly: 1.2.0
pathe: 1.1.0
scule: 1.0.0
unplugin: 1.3.1
vue-router: 4.1.6(vue@3.2.47)
yaml: 2.2.2
transitivePeerDependencies:
- rollup
- vue
dev: false
/unplugin@1.3.1:
resolution: {integrity: sha512-h4uUTIvFBQRxUKS2Wjys6ivoeofGhxzTe2sRWlooyjHXVttcVfV/JiavNd3d4+jty0SVV0dxGw9AkY9MwiaCEw==}
dependencies:
@ -9044,6 +9174,11 @@ packages:
engines: {node: '>= 14'}
dev: true
/yaml@2.2.2:
resolution: {integrity: sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==}
engines: {node: '>= 14'}
dev: false
/yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}

View File

@ -198,6 +198,7 @@ export default defineNuxtConfig({
}
},
experimental: {
typedPages: true,
polyfillVueUseHead: true,
renderJsonPayloads: process.env.TEST_PAYLOAD !== 'js',
respectNoSSRHeader: true,

View File

@ -1,7 +1,7 @@
<template>
<div>
<h1>[...slug].vue</h1>
<div>catchall at {{ $route.params.slug[0] }}</div>
<div>catchall at {{ route.params.slug?.[0] }}</div>
<div>Middleware ran: {{ !!($route.meta.override as any)?.includes('extended middleware') }}</div>
</div>
</template>
@ -11,7 +11,8 @@ definePageMeta({
middleware: ['override'],
validate: to => to.path !== '/forbidden'
})
if (useRoute().path.includes('navigate-some-path')) {
const route = useRoute('slug')
if (route.path.includes('navigate-some-path')) {
throw createError('navigate-some-path setup running')
}
</script>

View File

@ -1,7 +1,11 @@
<script setup lang="ts">
const route = useRoute('nested-foo-bar')
</script>
<template>
<div>
<div>nested/[foo]/[bar].vue</div>
<div>foo: {{ $route.params.foo }}</div>
<div>bar: {{ $route.params.bar }}</div>
<div>foo: {{ route.params.foo }}</div>
<div>bar: {{ route.params.bar }}</div>
</div>
</template>

View File

@ -1,6 +1,10 @@
<script setup lang="ts">
const route = useRoute('nested-foo')
</script>
<template>
<div>
<div>nested/[foo]/index.vue</div>
<div>foo: {{ $route.params.foo }}</div>
<div>foo: {{ route.params.foo }}</div>
</div>
</template>

View File

@ -1,7 +1,11 @@
<script setup lang="ts">
const route = useRoute('nested-foo-user-group')
</script>
<template>
<div>
<div>nested/[foo]/user-[group].vue</div>
<div>foo: {{ $route.params.foo }}</div>
<div>group: {{ $route.params.group }}</div>
<div>foo: {{ route.params.foo }}</div>
<div>group: {{ route.params.group }}</div>
</div>
</template>

View File

@ -14,7 +14,7 @@ const links = [
{ name: 'nuxt-link-trailing-slash' },
{ query: { 'with-state': 'true' }, state: { foo: 'bar' } },
{ query: { 'without-state': 'true' } }
]
] as const
const route = useRoute()
const windowState = computed(() => {

View File

@ -35,7 +35,7 @@
</template>
<script setup lang="ts">
const route = useRoute()
const route = useRoute('random-id')
const pageKey = 'rand_' + route.params.id

View File

@ -1,13 +1,13 @@
import { describe, expectTypeOf, it } from 'vitest'
import type { Ref } from 'vue'
import type { FetchError } from 'ofetch'
import type { NavigationFailure, RouteLocationNormalizedLoaded, RouteLocationRaw, Router, useRouter as vueUseRouter } from 'vue-router'
import type { NavigationFailure, RouteLocationNormalizedLoaded, RouteLocationRaw, Router, useRouter as vueUseRouter } from '#vue-router'
import type { AppConfig, RuntimeValue } from 'nuxt/schema'
import { defineNuxtConfig } from 'nuxt/config'
import { callWithNuxt, isVue3 } from '#app'
import type { NavigateToOptions } from '#app/composables/router'
import { NuxtPage } from '#components'
import { NuxtLink, NuxtPage } from '#components'
import { useRouter } from '#imports'
interface TestResponse { message: string }
@ -107,6 +107,63 @@ describe('middleware', () => {
})
})
describe('typed router integration', () => {
it('allows typing useRouter', () => {
const router = useRouter()
// @ts-expect-error this named route does not exist
router.push({ name: 'some-thing' })
// this one does
router.push({ name: 'fixed-keyed-child-parent' })
// @ts-expect-error this is an invalid param
router.push({ name: 'random-id', params: { bob: 23 } })
router.push({ name: 'random-id', params: { id: 4 } })
})
it('allows typing useRoute', () => {
const route = useRoute('random-id')
// @ts-expect-error this param does not exist
const _invalid = route.params.something
// this param does
const _valid = route.params.id
})
it('allows typing navigateTo', () => {
// @ts-expect-error this named route does not exist
navigateTo({ name: 'some-thing' })
// this one does
navigateTo({ name: 'fixed-keyed-child-parent' })
// @ts-expect-error this is an invalid param
navigateTo({ name: 'random-id', params: { bob: 23 } })
navigateTo({ name: 'random-id', params: { id: 4 } })
})
it('allows typing middleware', () => {
defineNuxtRouteMiddleware((to) => {
expectTypeOf(to.name).not.toBeAny()
// @ts-expect-error this route does not exist
expectTypeOf(to.name === 'bob').toMatchTypeOf<boolean>()
expectTypeOf(to.name === 'assets').toMatchTypeOf<boolean>()
})
})
it('respects pages:extend augmentation', () => {
// added via pages:extend
expectTypeOf(useRoute().name === 'internal-async-parent').toMatchTypeOf<boolean>()
// @ts-expect-error this route does not exist
expectTypeOf(useRoute().name === 'invalid').toMatchTypeOf<boolean>()
})
it('allows typing NuxtLink', () => {
// @ts-expect-error this named route does not exist
h(NuxtLink, { to: { name: 'some-thing' } })
// this one does
h(NuxtLink, { to: { name: 'fixed-keyed-child-parent' } })
// @ts-expect-error this is an invalid param
h(NuxtLink, { to: { name: 'random-id', params: { bob: 23 } } })
h(NuxtLink, { to: { name: 'random-id', params: { id: 4 } } })
})
})
describe('layouts', () => {
it('recognizes named layouts', () => {
definePageMeta({ layout: 'custom' })

View File

@ -31,6 +31,9 @@
],
"#internal/nitro/utils": [
"./node_modules/nitropack/dist/runtime/utils"
],
"#vue-router": [
"./node_modules/vue-router"
]
}
},