diff --git a/packages/nuxt/src/app/components/nuxt-island.client.ts b/packages/nuxt/src/app/components/nuxt-island.client.ts index c1e2441813..1e7ae3440e 100644 --- a/packages/nuxt/src/app/components/nuxt-island.client.ts +++ b/packages/nuxt/src/app/components/nuxt-island.client.ts @@ -2,9 +2,7 @@ import type { Component } from 'vue' import { Fragment, Teleport, computed, createStaticVNode, createVNode, defineComponent, getCurrentInstance, h, nextTick, onMounted, ref, toRaw, watch } from 'vue' import { debounce } from 'perfect-debounce' import { hash } from 'ohash' -import { appendResponseHeader } from 'h3' import { useHead } from '@unhead/vue' -import { randomUUID } from 'uncrypto' import { joinURL, withQuery } from 'ufo' import type { FetchResponse } from 'ofetch' import { join } from 'pathe' @@ -12,16 +10,11 @@ import { join } from 'pathe' // eslint-disable-next-line import/no-restricted-paths import type { NuxtIslandResponse } from '../../core/runtime/nitro/renderer' import { useNuxtApp, useRuntimeConfig } from '../nuxt' -import { useRequestEvent } from '../composables/ssr' -import { getFragmentHTML, getSlotProps } from './utils' +import { SLOTNAME_RE, SSR_UID_RE, UID_ATTR, getFragmentHTML, getSlotProps, nuxtIslandProps, pKey } from './utils' // @ts-expect-error virtual file import { remoteComponentIslands, selectiveClient } from '#build/nuxt.config.mjs' -const pKey = '_islandPromises' -const SSR_UID_RE = /nuxt-ssr-component-uid="([^"]*)"/ -const UID_ATTR = /nuxt-ssr-component-uid(="([^"]*)")?/ -const SLOTNAME_RE = /nuxt-ssr-slot-name="([^"]*)"/g const SLOT_FALLBACK_RE = /
]*><\/div>(((?!
]*>)[\s\S])*)
]*><\/div>/g let id = 0 @@ -55,29 +48,10 @@ function emptyPayload () { export default defineComponent({ name: 'NuxtIsland', props: { - name: { - type: String, - required: true - }, - lazy: Boolean, - props: { - type: Object, - default: () => undefined - }, - context: { - type: Object, - default: () => ({}) - }, - source: { - type: String, - default: () => undefined - }, - dangerouslyLoadClientComponents: { - type: Boolean, - default: false - } + ...nuxtIslandProps }, async setup (props, { slots, expose }) { + // used to force re-render the static content const key = ref(0) const canLoadClientComponent = computed(() => selectiveClient && (props.dangerouslyLoadClientComponents || !props.source)) const error = ref(null) @@ -120,6 +94,7 @@ export default defineComponent({ const ssrHTML = ref(getFragmentHTML(instance.vnode?.el ?? null, true)?.join('') || '') const slotProps = computed(() => getSlotProps(ssrHTML.value)) + // during hydration we directly retrieve the uid from the payload const uid = ref(ssrHTML.value.match(SSR_UID_RE)?.[1] ?? getId()) const availableSlots = computed(() => [...ssrHTML.value.matchAll(SLOTNAME_RE)].map(m => m[1])) @@ -128,6 +103,7 @@ export default defineComponent({ let html = ssrHTML.value if (!canLoadClientComponent.value) { + // replace all client components with their static content for (const [key, value] of Object.entries(nonReactivePayload.teleports || {})) { html = html.replace(new RegExp(`
]*nuxt-ssr-client="${key}"[^>]*>`), (full) => { return full + value @@ -184,6 +160,7 @@ export default defineComponent({ ssrHTML.value = res.html.replace(UID_ATTR, () => { return `nuxt-ssr-component-uid="${getId()}"` }) + // force re-render the static content key.value++ error.value = null @@ -196,7 +173,8 @@ export default defineComponent({ nonReactivePayload.teleports = res.teleports nonReactivePayload.chunks = res.chunks - // must await next tick for Teleport to work correctly with static node re-rendering + // must await next tick for Teleport to work correctly so vue can teleport the content to the new static node + // teleport update is based on uid await nextTick() setUid() @@ -230,6 +208,7 @@ export default defineComponent({ return [slots.fallback?.({ error: error.value }) ?? createVNode('div')] } const nodes = [createVNode(Fragment, { + // static nodes in build need to be keyed to force it to re-render key: key.value }, [h(createStaticVNode(html.value || '
', 1))])] diff --git a/packages/nuxt/src/app/components/nuxt-island.server.ts b/packages/nuxt/src/app/components/nuxt-island.server.ts index e83ef38d99..ab836852bb 100644 --- a/packages/nuxt/src/app/components/nuxt-island.server.ts +++ b/packages/nuxt/src/app/components/nuxt-island.server.ts @@ -9,40 +9,15 @@ import { joinURL, withQuery } from 'ufo' import type { NuxtIslandResponse } from '../../core/runtime/nitro/renderer' import { useNuxtApp, useRuntimeConfig } from '../nuxt' import { prerenderRoutes, useRequestEvent } from '../composables/ssr' -import { getSlotProps } from './utils' +import { SLOTNAME_RE, SSR_UID_RE, UID_ATTR, getSlotProps, nuxtIslandProps, pKey } from './utils' // @ts-expect-error virtual file import { remoteComponentIslands, selectiveClient } from '#build/nuxt.config.mjs' -const pKey = '_islandPromises' -const SSR_UID_RE = /nuxt-ssr-component-uid="([^"]*)"/ -const UID_ATTR = /nuxt-ssr-component-uid(="([^"]*)")?/ -const SLOTNAME_RE = /nuxt-ssr-slot-name="([^"]*)"/g - export default defineComponent({ name: 'NuxtIsland', props: { - name: { - type: String, - required: true - }, - lazy: Boolean, - props: { - type: Object, - default: () => undefined - }, - context: { - type: Object, - default: () => ({}) - }, - source: { - type: String, - default: () => undefined - }, - dangerouslyLoadClientComponents: { - type: Boolean, - default: false - } + ...nuxtIslandProps }, async setup(props, { slots }) { const error = ref(null) @@ -52,7 +27,6 @@ export default defineComponent({ const hashId = computed(() => hash([props.name, filteredProps.value, props.context, props.source])) const event = useRequestEvent() - function setPayload(key: string, result: NuxtIslandResponse) { nuxtApp.payload.data[key] = { __nuxt_island: { @@ -69,11 +43,7 @@ export default defineComponent({ ...result } } - const nonReactivePayload: Pick = { - chunks: {}, - props: {}, - teleports: {} - } + const teleports: NuxtIslandResponse['teleports'] = {} const ssrHTML = ref('') @@ -116,37 +86,33 @@ export default defineComponent({ setPayload(key, result) return result } - - async function fetchComponent(force = false) { + + try { nuxtApp[pKey] = nuxtApp[pKey] || {} if (!nuxtApp[pKey][uid.value]) { nuxtApp[pKey][uid.value] = _fetchComponent().finally(() => { delete nuxtApp[pKey]![uid.value] }) } - try { - const res: NuxtIslandResponse = await nuxtApp[pKey][uid.value] - cHead.value.link = res.head.link - cHead.value.style = res.head.style - ssrHTML.value = res.html.replace(UID_ATTR, () => { - return `nuxt-ssr-component-uid="${randomUUID()}"` - }) - nonReactivePayload.teleports = res.teleports - nonReactivePayload.chunks = res.chunks + const res: NuxtIslandResponse = await nuxtApp[pKey][uid.value] + cHead.value.link = res.head.link + cHead.value.style = res.head.style + ssrHTML.value = res.html.replace(UID_ATTR, () => { + return `nuxt-ssr-component-uid="${randomUUID()}"` + }) + Object.assign(teleports, res.teleports) - setUid() - } catch (e) { - error.value = e - } + setUid() + } catch(e) { + error.value = e } - await fetchComponent() - + return () => { if (!ssrHTML.value || error.value) { return [slots.fallback?.({ error: error.value }) ?? createVNode('div')] } - const nodes = [createVNode(Fragment, {}, [h(createStaticVNode(ssrHTML.value || '
', 1))])] + const nodes = [createVNode(Fragment, null, [h(createStaticVNode(ssrHTML.value || '
', 1))])] // render slots and teleports if (uid.value && ssrHTML.value) { @@ -157,7 +123,7 @@ export default defineComponent({ })) } } - for (const [id, html] of Object.entries(nonReactivePayload.teleports ?? {})) { + for (const [id, html] of Object.entries(teleports ?? {})) { nodes.push(createVNode(Teleport, { to: `uid=${uid.value};client=${id}` }, { default: () => [createStaticVNode(html, 1)] })) diff --git a/packages/nuxt/src/app/components/utils.ts b/packages/nuxt/src/app/components/utils.ts index dbe8d23744..997d89005d 100644 --- a/packages/nuxt/src/app/components/utils.ts +++ b/packages/nuxt/src/app/components/utils.ts @@ -185,3 +185,32 @@ export function getSlotProps (html: string) { } return data } + +export const pKey = '_islandPromises' +export const SSR_UID_RE = /nuxt-ssr-component-uid="([^"]*)"/ +export const UID_ATTR = /nuxt-ssr-component-uid(="([^"]*)")?/ +export const SLOTNAME_RE = /nuxt-ssr-slot-name="([^"]*)"/g + +export const nuxtIslandProps = { + name: { + type: String, + required: true + }, + lazy: Boolean, + props: { + type: Object, + default: () => undefined + }, + context: { + type: Object, + default: () => ({}) + }, + source: { + type: String, + default: () => undefined + }, + dangerouslyLoadClientComponents: { + type: Boolean, + default: false + } +} \ No newline at end of file