diff --git a/docs/3.api/3.utils/define-page-meta.md b/docs/3.api/3.utils/define-page-meta.md index e8427faf84..49aa8bc849 100644 --- a/docs/3.api/3.utils/define-page-meta.md +++ b/docs/3.api/3.utils/define-page-meta.md @@ -30,6 +30,7 @@ interface PageMeta { redirect?: RouteRecordRedirectOption name?: string path?: string + props?: RouteRecordRaw['props'] alias?: string | string[] pageTransition?: boolean | TransitionProps layoutTransition?: boolean | TransitionProps @@ -63,6 +64,12 @@ interface PageMeta { You may define a [custom regular expression](#using-a-custom-regular-expression) if you have a more complex pattern than can be expressed with the file name. + **`props`** + + - **Type**: [`RouteRecordRaw['props']`](https://router.vuejs.org/guide/essentials/passing-props) + + Allows accessing the route `params` as props passed to the page component. + **`alias`** - **Type**: `string | string[]` diff --git a/packages/nuxt/src/pages/runtime/composables.ts b/packages/nuxt/src/pages/runtime/composables.ts index eb60aada99..b752a101d4 100644 --- a/packages/nuxt/src/pages/runtime/composables.ts +++ b/packages/nuxt/src/pages/runtime/composables.ts @@ -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, RouteRecordRaw, RouteRecordRedirectOption } from 'vue-router' import { useRoute } from 'vue-router' import type { NitroRouteConfig } from 'nitro/types' import { useNuxtApp } from '#app/nuxt' @@ -37,6 +37,11 @@ export interface PageMeta { name?: string /** You may define a path matcher, if you have a more complex pattern than can be expressed with the file name. */ path?: string + /** + * Allows accessing the route `params` as props passed to the page component. + * @see https://router.vuejs.org/guide/essentials/passing-props + */ + props?: RouteRecordRaw['props'] /** Set to `false` to avoid scrolling to top on page navigations */ scrollToTop?: boolean | ((to: RouteLocationNormalizedLoaded, from: RouteLocationNormalizedLoaded) => boolean) } diff --git a/packages/nuxt/src/pages/utils.ts b/packages/nuxt/src/pages/utils.ts index 25ef9ead8a..23b8fe1892 100644 --- a/packages/nuxt/src/pages/utils.ts +++ b/packages/nuxt/src/pages/utils.ts @@ -190,7 +190,7 @@ export function extractScriptContent (html: string) { } const PAGE_META_RE = /definePageMeta\([\s\S]*?\)/ -const extractionKeys = ['name', 'path', 'alias', 'redirect'] as const +const extractionKeys = ['name', 'path', 'props', 'alias', 'redirect'] as const const DYNAMIC_META_KEY = '__nuxt_dynamic_meta_key' as const const pageContentsCache: Record = {} @@ -272,7 +272,7 @@ export async function getRouteMeta (contents: string, absolutePath: string): Pro continue } - if (property.value.type !== 'Literal' || typeof property.value.value !== 'string') { + if (property.value.type !== 'Literal' || (typeof property.value.value !== 'string' && typeof property.value.value !== 'boolean')) { console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not a string literal or array of string literals (reading \`${absolutePath}\`).`) dynamicProperties.add(key) continue @@ -539,6 +539,7 @@ export function normalizeRoutes (routes: NuxtPage[], metaImports: Set = const metaRoute: NormalizedRoute = { name: `${metaImportName}?.name ?? ${route.name}`, path: `${metaImportName}?.path ?? ${route.path}`, + props: `${metaImportName}?.props ?? false`, meta: `${metaImportName} || {}`, alias: `${metaImportName}?.alias || []`, redirect: `${metaImportName}?.redirect`, @@ -582,7 +583,7 @@ async function createClientPage(loader) { } // set to extracted value or delete if none extracted - for (const key of ['meta', 'alias', 'redirect'] satisfies NormalizedRouteKeys) { + for (const key of ['meta', 'alias', 'redirect', 'props'] satisfies NormalizedRouteKeys) { if (markedDynamic.has(key)) { continue } if (route[key] == null) { diff --git a/packages/nuxt/test/__snapshots__/pages-override-meta-disabled.test.ts.snap b/packages/nuxt/test/__snapshots__/pages-override-meta-disabled.test.ts.snap index 8bc2211aff..7dd113b1af 100644 --- a/packages/nuxt/test/__snapshots__/pages-override-meta-disabled.test.ts.snap +++ b/packages/nuxt/test/__snapshots__/pages-override-meta-disabled.test.ts.snap @@ -6,6 +6,7 @@ "meta": "{ ...(mockMeta || {}), ...{"someMetaData":true} }", "name": "mockMeta?.name ?? "pushed-route"", "path": "mockMeta?.path ?? "/"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, ], @@ -24,6 +25,18 @@ "meta": "{ ...(mockMeta || {}), ...{"test":1} }", "name": "mockMeta?.name ?? "page-with-meta"", "path": "mockMeta?.path ?? "/page-with-meta"", + "props": "mockMeta?.props ?? false", + "redirect": "mockMeta?.redirect", + }, + ], + "route.meta props generate by file": [ + { + "alias": "mockMeta?.alias || []", + "component": "() => import("pages/page-with-props.vue")", + "meta": "mockMeta || {}", + "name": "mockMeta?.name ?? "page-with-props"", + "path": "mockMeta?.path ?? "/page-with-props"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, ], @@ -34,6 +47,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "test:name"", "path": "mockMeta?.path ?? "/test\\:name"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, ], @@ -50,6 +64,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "param-index"", "path": "mockMeta?.path ?? """, + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, { @@ -58,6 +73,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "param-index-sibling"", "path": "mockMeta?.path ?? "sibling"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, ], @@ -65,6 +81,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? undefined", "path": "mockMeta?.path ?? """, + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, { @@ -73,6 +90,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "param-sibling"", "path": "mockMeta?.path ?? "sibling"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, ], @@ -80,6 +98,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? undefined", "path": "mockMeta?.path ?? "/param"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, { @@ -91,6 +110,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "wrapper-expose-other"", "path": "mockMeta?.path ?? """, + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, { @@ -99,6 +119,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "wrapper-expose-other-sibling"", "path": "mockMeta?.path ?? "sibling"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, ], @@ -106,6 +127,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? undefined", "path": "mockMeta?.path ?? "/wrapper-expose/other"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, ], @@ -116,6 +138,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "home"", "path": "mockMeta?.path ?? "/"", + "props": "mockMeta?.props ?? false", "redirect": ""/"", }, ], @@ -126,6 +149,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "slug"", "path": "mockMeta?.path ?? "/:slug(.*)*"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, { @@ -134,6 +158,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "index"", "path": "mockMeta?.path ?? "/"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, ], @@ -144,6 +169,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "index"", "path": "mockMeta?.path ?? "/"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, { @@ -152,6 +178,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "slug"", "path": "mockMeta?.path ?? "/:slug()"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, { @@ -163,6 +190,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "foo"", "path": "mockMeta?.path ?? """, + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, ], @@ -170,6 +198,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? undefined", "path": "mockMeta?.path ?? "/:foo?"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, { @@ -178,6 +207,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "optional-opt"", "path": "mockMeta?.path ?? "/optional/:opt?"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, { @@ -186,6 +216,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "optional-prefix-opt"", "path": "mockMeta?.path ?? "/optional/prefix-:opt?"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, { @@ -194,6 +225,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "optional-opt-postfix"", "path": "mockMeta?.path ?? "/optional/:opt?-postfix"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, { @@ -202,6 +234,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "optional-prefix-opt-postfix"", "path": "mockMeta?.path ?? "/optional/prefix-:opt?-postfix"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, { @@ -210,6 +243,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "bar"", "path": "mockMeta?.path ?? "/:bar()"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, { @@ -218,6 +252,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "nonopt-slug"", "path": "mockMeta?.path ?? "/nonopt/:slug()"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, { @@ -226,6 +261,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "opt-slug"", "path": "mockMeta?.path ?? "/opt/:slug?"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, { @@ -234,6 +270,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "sub-route-slug"", "path": "mockMeta?.path ?? "/:sub?/route-:slug()"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, ], @@ -244,6 +281,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "stories"", "path": "mockMeta?.path ?? "/:stories(.*)*"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, { @@ -252,6 +290,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "stories-id"", "path": "mockMeta?.path ?? "/stories/:id()"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, ], @@ -262,6 +301,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "stories-id"", "path": "mockMeta?.path ?? "/stories/:id()"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, { @@ -270,6 +310,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "stories"", "path": "mockMeta?.path ?? "/:stories(.*)*"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, ], @@ -280,6 +321,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "kebab-case"", "path": "mockMeta?.path ?? "/kebab-case"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, ], @@ -290,6 +332,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "snake_case"", "path": "mockMeta?.path ?? "/snake_case"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, ], @@ -300,6 +343,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "index"", "path": "mockMeta?.path ?? "/"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, { @@ -308,6 +352,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "parent"", "path": "mockMeta?.path ?? "/parent"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, { @@ -316,6 +361,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "parent-child"", "path": "mockMeta?.path ?? "/parent/child"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, ], @@ -329,6 +375,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "parent-child"", "path": "mockMeta?.path ?? "child"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, ], @@ -336,6 +383,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "parent"", "path": "mockMeta?.path ?? "/parent"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, ], @@ -346,6 +394,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "index"", "path": "mockMeta?.path ?? "/"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, { @@ -357,6 +406,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "about"", "path": "mockMeta?.path ?? """, + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, ], @@ -364,6 +414,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? undefined", "path": "mockMeta?.path ?? "/about"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, ], @@ -377,6 +428,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "index-index-all"", "path": "mockMeta?.path ?? "all"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, ], @@ -384,6 +436,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "index"", "path": "mockMeta?.path ?? "/"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, ], @@ -394,6 +447,7 @@ "meta": "{ ...(mockMeta || {}), ...{"test":1} }", "name": "mockMeta?.name ?? "page-with-meta"", "path": "mockMeta?.path ?? "/page-with-meta"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, ], @@ -404,6 +458,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "parent-child"", "path": "mockMeta?.path ?? "/parent/:child()"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, { @@ -412,6 +467,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "parent-child"", "path": "mockMeta?.path ?? "/parent-:child()"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, ], @@ -422,6 +478,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "foo"", "path": "mockMeta?.path ?? "/:foo?"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, { @@ -430,6 +487,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "foo"", "path": "mockMeta?.path ?? "/:foo()"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, ], @@ -440,6 +498,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "a1_1a"", "path": "mockMeta?.path ?? "/:a1_1a()"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, { @@ -448,6 +507,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "b2.2b"", "path": "mockMeta?.path ?? "/:b2.2b()"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, { @@ -456,6 +516,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "b2_2b"", "path": "mockMeta?.path ?? "/:b2()_:2b()"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, { @@ -464,6 +525,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "c33c"", "path": "mockMeta?.path ?? "/:c33c?"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, { @@ -472,6 +534,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "d44d"", "path": "mockMeta?.path ?? "/:d44d?"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, ], @@ -482,6 +545,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "home"", "path": "mockMeta?.path ?? "/"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, ], @@ -492,6 +556,7 @@ "meta": "mockMeta || {}", "name": "mockMeta?.name ?? "index"", "path": "mockMeta?.path ?? "/"", + "props": "mockMeta?.props ?? false", "redirect": "mockMeta?.redirect", }, ], diff --git a/packages/nuxt/test/__snapshots__/pages-override-meta-enabled.test.ts.snap b/packages/nuxt/test/__snapshots__/pages-override-meta-enabled.test.ts.snap index 26a4cc97a1..d02977f9b3 100644 --- a/packages/nuxt/test/__snapshots__/pages-override-meta-enabled.test.ts.snap +++ b/packages/nuxt/test/__snapshots__/pages-override-meta-enabled.test.ts.snap @@ -24,6 +24,13 @@ "path": ""/page-with-meta"", }, ], + "route.meta props generate by file": [ + { + "component": "() => import("pages/page-with-props.vue")", + "name": ""page-with-props"", + "path": ""/page-with-props"", + }, + ], "should allow pages with `:` in their path": [ { "component": "() => import("pages/test:name.vue")", diff --git a/packages/nuxt/test/page-metadata.test.ts b/packages/nuxt/test/page-metadata.test.ts index ed24e11e7c..6206e07c08 100644 --- a/packages/nuxt/test/page-metadata.test.ts +++ b/packages/nuxt/test/page-metadata.test.ts @@ -211,6 +211,7 @@ describe('normalizeRoutes', () => { { name: indexN6pT4Un8hYMeta?.name ?? undefined, path: indexN6pT4Un8hYMeta?.path ?? "/", + props: indexN6pT4Un8hYMeta?.props ?? false, meta: { ...(indexN6pT4Un8hYMeta || {}), ...{"layout":"test","foo":"bar"} }, alias: indexN6pT4Un8hYMeta?.alias || [], redirect: indexN6pT4Un8hYMeta?.redirect, diff --git a/packages/nuxt/test/pages.test.ts b/packages/nuxt/test/pages.test.ts index a7fe4b2751..3ff396e5cb 100644 --- a/packages/nuxt/test/pages.test.ts +++ b/packages/nuxt/test/pages.test.ts @@ -601,6 +601,30 @@ describe('pages:generateRoutesFromFiles', () => { }, ], }, + { + description: 'route.meta props generate by file', + files: [ + { + path: `${pagesDir}/page-with-props.vue`, + template: ` + + `, + }, + ], + output: [ + { + name: 'page-with-props', + path: '/page-with-props', + file: `${pagesDir}/page-with-props.vue`, + children: [], + props: true, + }, + ], + }, { description: 'should handle route groups', files: [ diff --git a/packages/schema/src/types/hooks.ts b/packages/schema/src/types/hooks.ts index aa9287a82f..3b88d3ec82 100644 --- a/packages/schema/src/types/hooks.ts +++ b/packages/schema/src/types/hooks.ts @@ -8,7 +8,7 @@ import type { Import, InlinePreset, Unimport } from 'unimport' import type { Compiler, Configuration, Stats } from 'webpack' import type { Nitro, NitroConfig } from 'nitro/types' import type { Schema, SchemaDefinition } from 'untyped' -import type { RouteLocationRaw } from 'vue-router' +import type { RouteLocationRaw, RouteRecordRaw } from 'vue-router' import type { VueCompilerOptions } from '@vue/language-core' import type { NuxtCompatibility, NuxtCompatibilityIssues, ViteConfig } from '..' import type { Component, ComponentsOptions } from './components' @@ -28,6 +28,7 @@ export type VueTSConfig = 0 extends 1 & VueCompilerOptions ? TSConfig : TSConfig export type NuxtPage = { name?: string path: string + props?: RouteRecordRaw['props'] file?: string meta?: Record alias?: string[] | string