mirror of
https://github.com/nuxt/nuxt.git
synced 2025-02-19 15:10:58 +00:00
feat(nuxt3)!: enable using <NuxtLayout>
without pages integration (#3610)
This commit is contained in:
parent
5c69ba64a4
commit
7bf338da8c
@ -8,20 +8,25 @@ head.title: Layouts directory
|
|||||||
|
|
||||||
Nuxt provides a customizable layouts framework you can use throughout your application, ideal for extracting common UI or code patterns into reusable layout components.
|
Nuxt provides a customizable layouts framework you can use throughout your application, ideal for extracting common UI or code patterns into reusable layout components.
|
||||||
|
|
||||||
Page layouts are placed in the `layouts/` directory and will be automatically loaded via asynchronous import when used. If you create a `layouts/default.vue` this will be used for all pages in your app. Other layouts are used by setting a `layout` property as part of your component's options.
|
Layouts are placed in the `layouts/` directory and will be automatically loaded via asynchronous import when used. Layouts are used by setting a `layout` property as part of your page metadata (if you are using the `~/pages` integration), or by using the `<NuxtLayout>` component.
|
||||||
|
|
||||||
If you only have a single layout in your application, you can alternatively use [app.vue](/docs/directory-structure/app).
|
If you only have a single layout in your application, it is recommended to use [app.vue](/docs/directory-structure/app) instead.
|
||||||
|
|
||||||
## Example: a custom layout
|
::alert{type=warning}
|
||||||
|
Unlike other components, your layouts must have a single root element to allow Nuxt to apply transitions between layout changes.
|
||||||
|
::
|
||||||
|
|
||||||
|
## Example: Enabling layouts with `app.vue`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
-| layouts/
|
-| layouts/
|
||||||
---| custom.vue
|
---| custom.vue
|
||||||
|
-| app.vue
|
||||||
```
|
```
|
||||||
|
|
||||||
In your layout files, you'll need to use `<slot />` to define where the page content of your layout will be loaded. For example:
|
In your layout files, you'll need to use `<slot />` to define where the content of your layout will be loaded. For example:
|
||||||
|
|
||||||
```vue
|
```vue{}[layouts/custom.vue]
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
Some shared layout content:
|
Some shared layout content:
|
||||||
@ -30,9 +35,28 @@ In your layout files, you'll need to use `<slot />` to define where the page con
|
|||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
|
|
||||||
Given the example above, you can use a custom layout like this:
|
Here's how you might use that layout in `app.vue`:
|
||||||
|
|
||||||
```vue
|
```vue{}[app.vue]
|
||||||
|
<template>
|
||||||
|
<NuxtLayout name="custom">
|
||||||
|
Hello world!
|
||||||
|
</NuxtLayout>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: setting the layout with `~/pages`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
-| layouts/
|
||||||
|
---| custom.vue
|
||||||
|
-| pages/
|
||||||
|
---| index.vue
|
||||||
|
```
|
||||||
|
|
||||||
|
You can set your layout within your page components like this:
|
||||||
|
|
||||||
|
```vue{}[pages/index.vue]
|
||||||
<script>
|
<script>
|
||||||
// This will also work in `<script setup>`
|
// This will also work in `<script setup>`
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
@ -45,9 +69,9 @@ definePageMeta({
|
|||||||
Learn more about [defining page meta](/docs/directory-structure/pages#page-metadata).
|
Learn more about [defining page meta](/docs/directory-structure/pages#page-metadata).
|
||||||
::
|
::
|
||||||
|
|
||||||
## Example: using with slots
|
## Example: manual control with `~/pages`
|
||||||
|
|
||||||
You can also take full control (for example, with slots) by using the `<NuxtLayout>` component (which is globally available throughout your application) by setting `layout: false`.
|
Even if you are using the `~/pages` integration, you can take full control by using the `<NuxtLayout>` component (which is globally available throughout your application), by setting `layout: false`.
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
<template>
|
<template>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { defineComponent, isRef, Ref, Transition } from 'vue'
|
import { defineComponent, isRef, Ref, Transition } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { _wrapIf } from './utils'
|
||||||
import { wrapIf } from './utils'
|
import { useRoute } from '#app'
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import layouts from '#build/layouts'
|
import layouts from '#build/layouts'
|
||||||
|
|
||||||
@ -25,8 +25,8 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We avoid rendering layout transition if there is no layout to render
|
// We avoid rendering layout transition if there is no layout to render
|
||||||
return wrapIf(Transition, hasLayout && (route.meta.layoutTransition ?? defaultLayoutTransition),
|
return _wrapIf(Transition, hasLayout && (route.meta.layoutTransition ?? defaultLayoutTransition),
|
||||||
wrapIf(layouts[layout], hasLayout, context.slots)
|
_wrapIf(layouts[layout], hasLayout, context.slots)
|
||||||
).default()
|
).default()
|
||||||
}
|
}
|
||||||
}
|
}
|
17
packages/nuxt3/src/app/components/utils.ts
Normal file
17
packages/nuxt3/src/app/components/utils.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { h } from 'vue'
|
||||||
|
import type { Component } from 'vue'
|
||||||
|
|
||||||
|
const Fragment = {
|
||||||
|
setup (_props, { slots }) {
|
||||||
|
return () => slots.default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal utility
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
export const _wrapIf = (component: Component, props: any, slots: any) => {
|
||||||
|
return { default: () => props ? h(component, props === true ? {} : props, slots) : h(Fragment, {}, slots) }
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import { promises as fsp } from 'fs'
|
import { promises as fsp } from 'fs'
|
||||||
import { dirname, resolve } from 'pathe'
|
import { dirname, resolve, basename, extname } from 'pathe'
|
||||||
import defu from 'defu'
|
import defu from 'defu'
|
||||||
|
import { kebabCase } from 'scule'
|
||||||
import type { Nuxt, NuxtApp, NuxtPlugin } from '@nuxt/schema'
|
import type { Nuxt, NuxtApp, NuxtPlugin } from '@nuxt/schema'
|
||||||
import { findPath, resolveFiles, normalizePlugin, normalizeTemplate, compileTemplate, templateUtils, tryResolveModule } from '@nuxt/kit'
|
import { findPath, resolveFiles, normalizePlugin, normalizeTemplate, compileTemplate, templateUtils, tryResolveModule } from '@nuxt/kit'
|
||||||
|
|
||||||
@ -70,6 +71,16 @@ export async function resolveApp (nuxt: Nuxt, app: NuxtApp) {
|
|||||||
app.errorComponent = (await findPath(['~/error'])) || resolve(nuxt.options.appDir, 'components/nuxt-error-page.vue')
|
app.errorComponent = (await findPath(['~/error'])) || resolve(nuxt.options.appDir, 'components/nuxt-error-page.vue')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve layouts
|
||||||
|
app.layouts = {}
|
||||||
|
for (const config of [nuxt.options, ...nuxt.options._extends.map(layer => layer.config)]) {
|
||||||
|
const layoutFiles = await resolveFiles(config.srcDir, `${config.dir.layouts}/*{${config.extensions.join(',')}}`)
|
||||||
|
for (const file of layoutFiles) {
|
||||||
|
const name = getNameFromPath(file)
|
||||||
|
app.layouts[name] = app.layouts[name] || { name, file }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve plugins
|
// Resolve plugins
|
||||||
app.plugins = []
|
app.plugins = []
|
||||||
for (const config of [...nuxt.options._extends.map(layer => layer.config), nuxt.options]) {
|
for (const config of [...nuxt.options._extends.map(layer => layer.config), nuxt.options]) {
|
||||||
@ -85,3 +96,7 @@ export async function resolveApp (nuxt: Nuxt, app: NuxtApp) {
|
|||||||
// Extend app
|
// Extend app
|
||||||
await nuxt.callHook('app:resolve', app)
|
await nuxt.callHook('app:resolve', app)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getNameFromPath (path: string) {
|
||||||
|
return kebabCase(basename(path).replace(extname(path), '')).replace(/["']/g, '')
|
||||||
|
}
|
||||||
|
@ -79,6 +79,11 @@ async function initNuxt (nuxt: Nuxt) {
|
|||||||
filePath: tryResolveModule('@nuxt/ui-templates/templates/welcome.vue')
|
filePath: tryResolveModule('@nuxt/ui-templates/templates/welcome.vue')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
addComponent({
|
||||||
|
name: 'NuxtLayout',
|
||||||
|
filePath: resolve(nuxt.options.appDir, 'components/layout')
|
||||||
|
})
|
||||||
|
|
||||||
// Add <ClientOnly>
|
// Add <ClientOnly>
|
||||||
addComponent({
|
addComponent({
|
||||||
name: 'ClientOnly',
|
name: 'ClientOnly',
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { templateUtils } from '@nuxt/kit'
|
import { templateUtils } from '@nuxt/kit'
|
||||||
import type { Nuxt, NuxtApp } from '@nuxt/schema'
|
import type { Nuxt, NuxtApp, NuxtTemplate } from '@nuxt/schema'
|
||||||
import { genArrayFromRaw, genDynamicImport, genExport, genImport, genString } from 'knitwork'
|
import { genArrayFromRaw, genDynamicImport, genExport, genImport, genObjectFromRawEntries, genString } from 'knitwork'
|
||||||
|
|
||||||
import { isAbsolute, join, relative } from 'pathe'
|
import { isAbsolute, join, relative } from 'pathe'
|
||||||
import { resolveSchema, generateTypes } from 'untyped'
|
import { resolveSchema, generateTypes } from 'untyped'
|
||||||
@ -154,3 +154,17 @@ export const schemaTemplate = {
|
|||||||
].join('\n')
|
].join('\n')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add layouts template
|
||||||
|
export const layoutTemplate: NuxtTemplate = {
|
||||||
|
filename: 'layouts.mjs',
|
||||||
|
getContents ({ app }) {
|
||||||
|
const layoutsObject = genObjectFromRawEntries(Object.values(app.layouts).map(({ name, file }) => {
|
||||||
|
return [name, `defineAsyncComponent({ suspensible: false, loader: ${genDynamicImport(file)} })`]
|
||||||
|
}))
|
||||||
|
return [
|
||||||
|
'import { defineAsyncComponent } from \'vue\'',
|
||||||
|
`export default ${layoutsObject}`
|
||||||
|
].join('\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -4,7 +4,7 @@ import { resolve } from 'pathe'
|
|||||||
import { genDynamicImport, genString, genArrayFromRaw, genImport, genObjectFromRawEntries } from 'knitwork'
|
import { genDynamicImport, genString, genArrayFromRaw, genImport, genObjectFromRawEntries } from 'knitwork'
|
||||||
import escapeRE from 'escape-string-regexp'
|
import escapeRE from 'escape-string-regexp'
|
||||||
import { distDir } from '../dirs'
|
import { distDir } from '../dirs'
|
||||||
import { resolveLayouts, resolvePagesRoutes, normalizeRoutes, resolveMiddleware, getImportName } from './utils'
|
import { resolvePagesRoutes, normalizeRoutes, resolveMiddleware, getImportName } from './utils'
|
||||||
import { TransformMacroPlugin, TransformMacroPluginOptions } from './macros'
|
import { TransformMacroPlugin, TransformMacroPluginOptions } from './macros'
|
||||||
|
|
||||||
export default defineNuxtModule({
|
export default defineNuxtModule({
|
||||||
@ -111,12 +111,11 @@ export default defineNuxtModule({
|
|||||||
|
|
||||||
addTemplate({
|
addTemplate({
|
||||||
filename: 'types/layouts.d.ts',
|
filename: 'types/layouts.d.ts',
|
||||||
getContents: async () => {
|
getContents: ({ app }) => {
|
||||||
const composablesFile = resolve(runtimeDir, 'composables')
|
const composablesFile = resolve(runtimeDir, 'composables')
|
||||||
const layouts = await resolveLayouts(nuxt)
|
|
||||||
return [
|
return [
|
||||||
'import { ComputedRef, Ref } from \'vue\'',
|
'import { ComputedRef, Ref } from \'vue\'',
|
||||||
`export type LayoutKey = ${layouts.map(layout => genString(layout.name)).join(' | ') || 'string'}`,
|
`export type LayoutKey = ${Object.keys(app.layouts).map(name => genString(name)).join(' | ') || 'string'}`,
|
||||||
`declare module ${genString(composablesFile)} {`,
|
`declare module ${genString(composablesFile)} {`,
|
||||||
' interface PageMeta {',
|
' interface PageMeta {',
|
||||||
' layout?: false | LayoutKey | Ref<LayoutKey> | ComputedRef<LayoutKey>',
|
' layout?: false | LayoutKey | Ref<LayoutKey> | ComputedRef<LayoutKey>',
|
||||||
@ -126,22 +125,7 @@ export default defineNuxtModule({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add layouts template
|
// Add declarations for middleware keys
|
||||||
addTemplate({
|
|
||||||
filename: 'layouts.mjs',
|
|
||||||
async getContents () {
|
|
||||||
const layouts = await resolveLayouts(nuxt)
|
|
||||||
const layoutsObject = genObjectFromRawEntries(layouts.map(({ name, file }) => {
|
|
||||||
return [name, `defineAsyncComponent({ suspensible: false, loader: ${genDynamicImport(file)} })`]
|
|
||||||
}))
|
|
||||||
return [
|
|
||||||
'import { defineAsyncComponent } from \'vue\'',
|
|
||||||
`export default ${layoutsObject}`
|
|
||||||
].join('\n')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add declarations for middleware and layout keys
|
|
||||||
nuxt.hook('prepare:types', ({ references }) => {
|
nuxt.hook('prepare:types', ({ references }) => {
|
||||||
references.push({ path: resolve(nuxt.options.buildDir, 'types/middleware.d.ts') })
|
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, 'types/layouts.d.ts') })
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { defineComponent, h, Suspense, Transition } from 'vue'
|
import { defineComponent, h, Suspense, Transition } from 'vue'
|
||||||
import { RouteLocationNormalizedLoaded, RouterView } from 'vue-router'
|
import { RouteLocationNormalizedLoaded, RouterView } from 'vue-router'
|
||||||
|
|
||||||
import { generateRouteKey, RouterViewSlotProps, wrapIf, wrapInKeepAlive } from './utils'
|
import { generateRouteKey, RouterViewSlotProps, wrapInKeepAlive } from './utils'
|
||||||
import { useNuxtApp } from '#app'
|
import { useNuxtApp } from '#app'
|
||||||
|
import { _wrapIf } from '#app/components/utils'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'NuxtPage',
|
name: 'NuxtPage',
|
||||||
@ -18,7 +19,7 @@ export default defineComponent({
|
|||||||
return () => {
|
return () => {
|
||||||
return h(RouterView, {}, {
|
return h(RouterView, {}, {
|
||||||
default: (routeProps: RouterViewSlotProps) => routeProps.Component &&
|
default: (routeProps: RouterViewSlotProps) => routeProps.Component &&
|
||||||
wrapIf(Transition, routeProps.route.meta.pageTransition ?? defaultPageTransition,
|
_wrapIf(Transition, routeProps.route.meta.pageTransition ?? defaultPageTransition,
|
||||||
wrapInKeepAlive(routeProps.route.meta.keepalive, h(Suspense, {
|
wrapInKeepAlive(routeProps.route.meta.keepalive, h(Suspense, {
|
||||||
onPending: () => nuxtApp.callHook('page:start', routeProps.Component),
|
onPending: () => nuxtApp.callHook('page:start', routeProps.Component),
|
||||||
onResolve: () => nuxtApp.callHook('page:finish', routeProps.Component)
|
onResolve: () => nuxtApp.callHook('page:finish', routeProps.Component)
|
||||||
|
@ -8,7 +8,6 @@ import {
|
|||||||
} from 'vue-router'
|
} from 'vue-router'
|
||||||
import { createError } from 'h3'
|
import { createError } from 'h3'
|
||||||
import NuxtPage from './page'
|
import NuxtPage from './page'
|
||||||
import NuxtLayout from './layout'
|
|
||||||
import { callWithNuxt, defineNuxtPlugin, useRuntimeConfig, NuxtApp, throwError, clearError } from '#app'
|
import { callWithNuxt, defineNuxtPlugin, useRuntimeConfig, NuxtApp, throwError, clearError } from '#app'
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import routes from '#build/routes'
|
import routes from '#build/routes'
|
||||||
@ -18,7 +17,6 @@ import { globalMiddleware, namedMiddleware } from '#build/middleware'
|
|||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
NuxtPage: typeof NuxtPage
|
NuxtPage: typeof NuxtPage
|
||||||
NuxtLayout: typeof NuxtLayout
|
|
||||||
NuxtLink: typeof RouterLink
|
NuxtLink: typeof RouterLink
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
NuxtNestedPage: typeof NuxtPage
|
NuxtNestedPage: typeof NuxtPage
|
||||||
@ -29,7 +27,6 @@ declare module 'vue' {
|
|||||||
|
|
||||||
export default defineNuxtPlugin((nuxtApp) => {
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
nuxtApp.vueApp.component('NuxtPage', NuxtPage)
|
nuxtApp.vueApp.component('NuxtPage', NuxtPage)
|
||||||
nuxtApp.vueApp.component('NuxtLayout', NuxtLayout)
|
|
||||||
nuxtApp.vueApp.component('NuxtLink', RouterLink)
|
nuxtApp.vueApp.component('NuxtLink', RouterLink)
|
||||||
// TODO: remove before release - present for backwards compatibility & intentionally undocumented
|
// TODO: remove before release - present for backwards compatibility & intentionally undocumented
|
||||||
nuxtApp.vueApp.component('NuxtNestedPage', NuxtPage)
|
nuxtApp.vueApp.component('NuxtNestedPage', NuxtPage)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, KeepAlive, h } from 'vue'
|
import { KeepAlive, h } from 'vue'
|
||||||
import { RouterView, RouteLocationMatched, RouteLocationNormalizedLoaded } from 'vue-router'
|
import { RouterView, RouteLocationMatched, RouteLocationNormalizedLoaded } from 'vue-router'
|
||||||
|
|
||||||
type InstanceOf<T> = T extends new (...args: any[]) => infer R ? R : never
|
type InstanceOf<T> = T extends new (...args: any[]) => infer R ? R : never
|
||||||
@ -17,16 +17,6 @@ export const generateRouteKey = (override: string | ((route: RouteLocationNormal
|
|||||||
return typeof source === 'function' ? source(routeProps.route) : source
|
return typeof source === 'function' ? source(routeProps.route) : source
|
||||||
}
|
}
|
||||||
|
|
||||||
const Fragment = {
|
|
||||||
setup (_props, { slots }) {
|
|
||||||
return () => slots.default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const wrapIf = (component: Component, props: any, slots: any) => {
|
|
||||||
return { default: () => props ? h(component, props === true ? {} : props, slots) : h(Fragment, {}, slots) }
|
|
||||||
}
|
|
||||||
|
|
||||||
export const wrapInKeepAlive = (props: any, children: any) => {
|
export const wrapInKeepAlive = (props: any, children: any) => {
|
||||||
return { default: () => process.client && props ? h(KeepAlive, props === true ? {} : props, children) : children }
|
return { default: () => process.client && props ? h(KeepAlive, props === true ? {} : props, children) : children }
|
||||||
}
|
}
|
||||||
|
@ -217,15 +217,6 @@ function prepareRoutes (routes: NuxtPage[], parent?: NuxtPage) {
|
|||||||
return routes
|
return routes
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveLayouts (nuxt: Nuxt) {
|
|
||||||
const layoutDir = resolve(nuxt.options.srcDir, nuxt.options.dir.layouts)
|
|
||||||
const files = await resolveFiles(layoutDir, `*{${nuxt.options.extensions.join(',')}}`)
|
|
||||||
|
|
||||||
const layouts = files.map(file => ({ name: getNameFromPath(file), file }))
|
|
||||||
await nuxt.callHook('pages:layouts:extend', layouts)
|
|
||||||
return layouts
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> = new Set()): { imports: Set<string>, routes: string } {
|
export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> = new Set()): { imports: Set<string>, routes: string } {
|
||||||
return {
|
return {
|
||||||
imports: metaImports,
|
imports: metaImports,
|
||||||
|
@ -70,7 +70,6 @@ export interface NuxtHooks {
|
|||||||
'builder:generateApp': () => HookResult
|
'builder:generateApp': () => HookResult
|
||||||
'pages:extend': (pages: NuxtPage[]) => HookResult
|
'pages:extend': (pages: NuxtPage[]) => HookResult
|
||||||
'pages:middleware:extend': (middleware: NuxtMiddleware[]) => HookResult
|
'pages:middleware:extend': (middleware: NuxtMiddleware[]) => HookResult
|
||||||
'pages:layouts:extend': (layouts: NuxtLayout[]) => HookResult
|
|
||||||
|
|
||||||
// Auto imports
|
// Auto imports
|
||||||
'autoImports:sources': (presets: ImportPresetWithDeperection[]) => HookResult
|
'autoImports:sources': (presets: ImportPresetWithDeperection[]) => HookResult
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { Hookable } from 'hookable'
|
import type { Hookable } from 'hookable'
|
||||||
import type { Ignore } from 'ignore'
|
import type { Ignore } from 'ignore'
|
||||||
import type { NuxtHooks } from './hooks'
|
import type { NuxtHooks, NuxtLayout } from './hooks'
|
||||||
import type { NuxtOptions } from './config'
|
import type { NuxtOptions } from './config'
|
||||||
|
|
||||||
export interface Nuxt {
|
export interface Nuxt {
|
||||||
@ -57,6 +57,7 @@ export interface NuxtApp {
|
|||||||
dir: string
|
dir: string
|
||||||
extensions: string[]
|
extensions: string[]
|
||||||
plugins: NuxtPlugin[]
|
plugins: NuxtPlugin[]
|
||||||
|
layouts: Record<string, NuxtLayout>
|
||||||
templates: NuxtTemplate[]
|
templates: NuxtTemplate[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user