feat(nuxt): add warning in dev mode if layouts/pages do not have a single root node (#5469)

This commit is contained in:
Daniel Roe 2022-08-23 11:25:48 +01:00 committed by GitHub
parent 2802638ea8
commit cfb7e59171
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 112 additions and 12 deletions

View File

@ -1,4 +1,4 @@
import { defineComponent, isRef, Ref, Transition } from 'vue'
import { defineComponent, isRef, nextTick, onMounted, Ref, Transition, VNode } from 'vue'
import { _wrapIf } from './utils'
import { useRoute } from '#app'
// @ts-ignore
@ -16,6 +16,18 @@ export default defineComponent({
setup (props, context) {
const route = useRoute()
let vnode: VNode
let _layout: string | false
if (process.dev && process.client) {
onMounted(() => {
nextTick(() => {
if (_layout && ['#comment', '#text'].includes(vnode?.el?.nodeName)) {
console.warn(`[nuxt] \`${_layout}\` layout does not have a single root node and will cause errors when navigating between routes.`)
}
})
})
}
return () => {
const layout = (isRef(props.name) ? props.name.value : props.name) ?? route.meta.layout as string ?? 'default'
@ -24,10 +36,20 @@ export default defineComponent({
console.warn(`Invalid layout \`${layout}\` selected.`)
}
const transitionProps = route.meta.layoutTransition ?? defaultLayoutTransition
// We avoid rendering layout transition if there is no layout to render
return _wrapIf(Transition, hasLayout && (route.meta.layoutTransition ?? defaultLayoutTransition),
_wrapIf(layouts[layout], hasLayout, context.slots)
).default()
return _wrapIf(Transition, hasLayout && transitionProps, {
default: () => {
if (process.dev && process.client && transitionProps) {
_layout = layout
vnode = _wrapIf(layouts[layout], hasLayout, context.slots).default()
return vnode
}
return _wrapIf(layouts[layout], hasLayout, context.slots).default()
}
}).default()
}
}
})

View File

@ -1,5 +1,7 @@
import { computed, DefineComponent, defineComponent, h, inject, provide, reactive, Suspense, Transition } from 'vue'
import { RouteLocation, RouteLocationNormalized, RouteLocationNormalizedLoaded, RouterView } from 'vue-router'
import { computed, defineComponent, h, inject, provide, reactive, onMounted, nextTick, Suspense, Transition } from 'vue'
import type { DefineComponent, VNode } from 'vue'
import { RouteLocationNormalized, RouteLocationNormalizedLoaded, RouterView } from 'vue-router'
import type { RouteLocation } from 'vue-router'
import { generateRouteKey, RouterViewSlotProps, wrapInKeepAlive } from './utils'
import { useNuxtApp } from '#app'
@ -34,15 +36,16 @@ export default defineComponent({
if (!routeProps.Component) { return }
const key = generateRouteKey(props.pageKey, routeProps)
const transitionProps = routeProps.route.meta.pageTransition ?? defaultPageTransition
return _wrapIf(Transition, routeProps.route.meta.pageTransition ?? defaultPageTransition,
return _wrapIf(Transition, transitionProps,
wrapInKeepAlive(routeProps.route.meta.keepalive, isNested && nuxtApp.isHydrating
// Include route children in parent suspense
? h(Component, { key, routeProps, pageKey: key } as {})
? h(Component, { key, routeProps, pageKey: key, hasTransition: !!transitionProps } as {})
: h(Suspense, {
onPending: () => nuxtApp.callHook('page:start', routeProps.Component),
onResolve: () => nuxtApp.callHook('page:finish', routeProps.Component)
}, { default: () => h(Component, { key, routeProps, pageKey: key } as {}) })
}, { default: () => h(Component, { key, routeProps, pageKey: key, hasTransition: !!transitionProps } as {}) })
)).default()
}
})
@ -60,7 +63,7 @@ const defaultPageTransition = { name: 'page', mode: 'out-in' }
const Component = defineComponent({
// TODO: Type props
// eslint-disable-next-line vue/require-prop-types
props: ['routeProps', 'pageKey'],
props: ['routeProps', 'pageKey', 'hasTransition'],
setup (props) {
// Prevent reactivity when the page will be rerendered in a different suspense fork
const previousKey = props.pageKey
@ -73,6 +76,26 @@ const Component = defineComponent({
}
provide('_route', reactive(route))
return () => h(props.routeProps.Component)
let vnode: VNode
if (process.dev && process.client && props.hasTransition) {
onMounted(() => {
nextTick(() => {
if (['#comment', '#text'].includes(vnode?.el?.nodeName)) {
const filename = (vnode?.type as any).__file
console.warn(`[nuxt] \`${filename}\` does not have a single root node and will cause errors when navigating between routes.`)
}
})
})
}
return () => {
if (process.dev && process.client) {
vnode = h(props.routeProps.Component)
return vnode
}
return h(props.routeProps.Component)
}
}
})

View File

@ -1,8 +1,9 @@
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { joinURL } from 'ufo'
// import { isWindows } from 'std-env'
import { setup, fetch, $fetch, startServer } from '@nuxt/test-utils'
import { expectNoClientErrors } from './utils'
import { expectNoClientErrors, renderPage } from './utils'
await setup({
rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url)),
@ -359,6 +360,28 @@ describe('automatically keyed composables', () => {
})
})
if (process.env.NUXT_TEST_DEV) {
describe('detecting invalid root nodes', () => {
it('should detect invalid root nodes in pages', async () => {
for (const path of ['1', '2', '3', '4']) {
const { consoleLogs } = await renderPage(joinURL('/invalid-root', path))
const consoleLogsWarns = consoleLogs.filter(i => i.type === 'warning').map(w => w.text).join('\n')
expect(consoleLogsWarns).toContain('does not have a single root node and will cause errors when navigating between routes')
}
})
it('should not complain if there is no transition', async () => {
for (const path of ['fine']) {
const { consoleLogs } = await renderPage(joinURL('/invalid-root', path))
const consoleLogsWarns = consoleLogs.filter(i => i.type === 'warning')
expect(consoleLogsWarns.length).toEqual(0)
}
})
})
}
describe('dynamic paths', () => {
if (process.env.NUXT_TEST_DEV) {
// TODO:

View File

@ -0,0 +1,4 @@
<template>
<div />
<slot />
</template>

View File

@ -0,0 +1,4 @@
<template>
<!-- comment -->
<div>Test</div>
</template>

View File

@ -0,0 +1,3 @@
<template>
Just some text
</template>

View File

@ -0,0 +1,4 @@
<template>
<div>Multiple</div>
<div>elements</div>
</template>

View File

@ -0,0 +1,7 @@
<template>
<div>Fine</div>
</template>
<script setup>
definePageMeta({ layout: 'invalid-root' })
</script>

View File

@ -0,0 +1,10 @@
<template>
<div>Multiple</div>
<div>elements</div>
</template>
<script setup>
definePageMeta({
pageTransition: false
})
</script>