fix(nuxt): render a div when client-only hydrates w/o element (#23899)

This commit is contained in:
Julien Huang 2023-10-25 02:34:22 +02:00 committed by GitHub
parent 8c77ce81b9
commit a037512562
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 16 additions and 6 deletions

View File

@ -40,7 +40,7 @@ export function createClientOnly<T extends ComponentOptions> (component: T) {
? createElementVNode(res.type, res.props, res.children, res.patchFlag, res.dynamicProps, res.shapeFlag) ? createElementVNode(res.type, res.props, res.children, res.patchFlag, res.dynamicProps, res.shapeFlag)
: h(res) : h(res)
} else { } else {
const fragment = getFragmentHTML(ctx._.vnode.el ?? null) const fragment = getFragmentHTML(ctx._.vnode.el ?? null) ?? ['<div></div>']
return process.client ? createStaticVNode(fragment.join(''), fragment.length) : h('div', ctx.$attrs ?? ctx._.attrs) return process.client ? createStaticVNode(fragment.join(''), fragment.length) : h('div', ctx.$attrs ?? ctx._.attrs)
} }
} }
@ -79,7 +79,7 @@ export function createClientOnly<T extends ComponentOptions> (component: T) {
? createElementVNode(res.type, res.props, res.children, res.patchFlag, res.dynamicProps, res.shapeFlag) ? createElementVNode(res.type, res.props, res.children, res.patchFlag, res.dynamicProps, res.shapeFlag)
: h(res) : h(res)
} else { } else {
const fragment = getFragmentHTML(instance?.vnode.el ?? null) const fragment = getFragmentHTML(instance?.vnode.el ?? null) ?? ['<div></div>']
return process.client ? createStaticVNode(fragment.join(''), fragment.length) : h('div', ctx.attrs) return process.client ? createStaticVNode(fragment.join(''), fragment.length) : h('div', ctx.attrs)
} }
} }

View File

@ -73,10 +73,10 @@ export default defineComponent({
const ssrHTML = ref<string>('') const ssrHTML = ref<string>('')
if (import.meta.client) { if (import.meta.client) {
const renderedHTML = getFragmentHTML(instance.vnode?.el ?? null).join('') const renderedHTML = getFragmentHTML(instance.vnode?.el ?? null)?.join('') ?? ''
if (renderedHTML && nuxtApp.isHydrating) { if (renderedHTML && nuxtApp.isHydrating) {
setPayload(`${props.name}_${hashId.value}`, { setPayload(`${props.name}_${hashId.value}`, {
html: getFragmentHTML(instance.vnode?.el ?? null, true).join(''), html: getFragmentHTML(instance.vnode?.el ?? null, true)?.join('') ?? '',
state: {}, state: {},
head: { head: {
link: [], link: [],

View File

@ -104,7 +104,7 @@ export function vforToArray (source: any): any[] {
* @param withoutSlots purge all slots from the HTML string retrieved * @param withoutSlots purge all slots from the HTML string retrieved
* @returns {string[]} An array of string which represent the content of each element. Use `.join('')` to retrieve a component vnode.el HTML * @returns {string[]} An array of string which represent the content of each element. Use `.join('')` to retrieve a component vnode.el HTML
*/ */
export function getFragmentHTML (element: RendererNode | null, withoutSlots = false): string[] { export function getFragmentHTML (element: RendererNode | null, withoutSlots = false): string[] | null {
if (element) { if (element) {
if (element.nodeName === '#comment' && element.nodeValue === '[') { if (element.nodeName === '#comment' && element.nodeValue === '[') {
return getFragmentChildren(element, [], withoutSlots) return getFragmentChildren(element, [], withoutSlots)
@ -116,7 +116,7 @@ export function getFragmentHTML (element: RendererNode | null, withoutSlots = fa
} }
return [element.outerHTML] return [element.outerHTML]
} }
return [] return null
} }
function getFragmentChildren (element: RendererNode | null, blocks: string[] = [], withoutSlots = false) { function getFragmentChildren (element: RendererNode | null, blocks: string[] = [], withoutSlots = false) {

View File

@ -362,6 +362,13 @@ describe('pages', () => {
expect(pageErrors).toEqual([]) expect(pageErrors).toEqual([])
await page.close() await page.close()
// don't expect any errors or warning on client-side navigation
const { page: page2, consoleLogs: consoleLogs2 } = await renderPage('/')
await page2.locator('#to-client-only-components').click()
// force wait for a few ticks
await page2.waitForTimeout(50)
expect(consoleLogs2.some(log => log.type === 'error' || log.type === 'warning')).toBeFalsy()
await page2.close()
}) })
it('/wrapper-expose/layout', async () => { it('/wrapper-expose/layout', async () => {

View File

@ -32,6 +32,9 @@
<NuxtLink to="/chunk-error" :prefetch="false"> <NuxtLink to="/chunk-error" :prefetch="false">
Chunk error Chunk error
</NuxtLink> </NuxtLink>
<NuxtLink id="to-client-only-components" to="/client-only-components">
createClientOnly()
</NuxtLink>
<NuxtLink id="middleware-abort-non-fatal" to="/middleware-abort-non-fatal" :prefetch="false"> <NuxtLink id="middleware-abort-non-fatal" to="/middleware-abort-non-fatal" :prefetch="false">
Middleware abort navigation Middleware abort navigation
</NuxtLink> </NuxtLink>