2023-05-11 08:37:32 +00:00
|
|
|
import type { RendererNode, Slots } from 'vue'
|
2023-04-20 21:41:20 +00:00
|
|
|
import { computed, createStaticVNode, defineComponent, getCurrentInstance, h, ref, watch } from 'vue'
|
2022-11-24 12:24:14 +00:00
|
|
|
import { debounce } from 'perfect-debounce'
|
|
|
|
import { hash } from 'ohash'
|
2023-05-01 22:55:24 +00:00
|
|
|
import { appendResponseHeader } from 'h3'
|
2023-03-10 08:01:21 +00:00
|
|
|
import { useHead } from '@unhead/vue'
|
|
|
|
|
2022-11-24 12:24:14 +00:00
|
|
|
// eslint-disable-next-line import/no-restricted-paths
|
|
|
|
import type { NuxtIslandResponse } from '../../core/runtime/nitro/renderer'
|
2023-02-09 06:26:41 +00:00
|
|
|
import { useNuxtApp } from '#app/nuxt'
|
|
|
|
import { useRequestEvent } from '#app/composables/ssr'
|
2022-11-24 12:24:14 +00:00
|
|
|
|
|
|
|
const pKey = '_islandPromises'
|
|
|
|
|
|
|
|
export default defineComponent({
|
|
|
|
name: 'NuxtIsland',
|
|
|
|
props: {
|
|
|
|
name: {
|
|
|
|
type: String,
|
|
|
|
required: true
|
|
|
|
},
|
|
|
|
props: {
|
|
|
|
type: Object,
|
|
|
|
default: () => undefined
|
|
|
|
},
|
|
|
|
context: {
|
|
|
|
type: Object,
|
|
|
|
default: () => ({})
|
|
|
|
}
|
|
|
|
},
|
|
|
|
async setup (props) {
|
|
|
|
const nuxtApp = useNuxtApp()
|
|
|
|
const hashId = computed(() => hash([props.name, props.props, props.context]))
|
2023-03-20 21:47:06 +00:00
|
|
|
const instance = getCurrentInstance()!
|
2023-01-20 12:10:58 +00:00
|
|
|
const event = useRequestEvent()
|
|
|
|
|
2023-04-20 21:41:20 +00:00
|
|
|
const html = ref<string>(process.client ? getFragmentHTML(instance?.vnode?.el).join('') ?? '<div></div>' : '<div></div>')
|
2023-02-27 19:02:11 +00:00
|
|
|
const cHead = ref<Record<'link' | 'style', Array<Record<string, string>>>>({ link: [], style: [] })
|
2022-11-24 12:24:14 +00:00
|
|
|
useHead(cHead)
|
|
|
|
|
|
|
|
function _fetchComponent () {
|
2023-01-20 12:10:58 +00:00
|
|
|
const url = `/__nuxt_island/${props.name}:${hashId.value}`
|
|
|
|
if (process.server && process.env.prerender) {
|
|
|
|
// Hint to Nitro to prerender the island component
|
2023-05-01 22:55:24 +00:00
|
|
|
appendResponseHeader(event, 'x-nitro-prerender', url)
|
2023-01-20 12:10:58 +00:00
|
|
|
}
|
2022-11-24 12:24:14 +00:00
|
|
|
// TODO: Validate response
|
2023-01-20 12:10:58 +00:00
|
|
|
return $fetch<NuxtIslandResponse>(url, {
|
2022-11-24 12:24:14 +00:00
|
|
|
params: {
|
|
|
|
...props.context,
|
|
|
|
props: props.props ? JSON.stringify(props.props) : undefined
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2023-04-20 21:41:20 +00:00
|
|
|
const key = ref(0)
|
2022-11-24 12:24:14 +00:00
|
|
|
async function fetchComponent () {
|
|
|
|
nuxtApp[pKey] = nuxtApp[pKey] || {}
|
|
|
|
if (!nuxtApp[pKey][hashId.value]) {
|
|
|
|
nuxtApp[pKey][hashId.value] = _fetchComponent().finally(() => {
|
2023-03-14 10:09:50 +00:00
|
|
|
delete nuxtApp[pKey]![hashId.value]
|
2022-11-24 12:24:14 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
const res: NuxtIslandResponse = await nuxtApp[pKey][hashId.value]
|
|
|
|
cHead.value.link = res.head.link
|
|
|
|
cHead.value.style = res.head.style
|
|
|
|
html.value = res.html
|
2023-04-20 21:41:20 +00:00
|
|
|
key.value++
|
2022-11-24 12:24:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (process.client) {
|
|
|
|
watch(props, debounce(fetchComponent, 100))
|
|
|
|
}
|
|
|
|
|
2023-01-14 01:13:48 +00:00
|
|
|
if (process.server || !nuxtApp.isHydrating) {
|
|
|
|
await fetchComponent()
|
|
|
|
}
|
2023-05-11 08:37:32 +00:00
|
|
|
return () => h((_, { slots }) => (slots as Slots).default?.(), { key: key.value }, {
|
2023-04-20 21:41:20 +00:00
|
|
|
default: () => [createStaticVNode(html.value, 1)]
|
|
|
|
})
|
2022-11-24 12:24:14 +00:00
|
|
|
}
|
|
|
|
})
|
2023-04-20 21:41:20 +00:00
|
|
|
|
|
|
|
// TODO refactor with https://github.com/nuxt/nuxt/pull/19231
|
|
|
|
function getFragmentHTML (element: RendererNode | null) {
|
|
|
|
if (element) {
|
|
|
|
if (element.nodeName === '#comment' && element.nodeValue === '[') {
|
|
|
|
return getFragmentChildren(element)
|
|
|
|
}
|
|
|
|
return [element.outerHTML]
|
|
|
|
}
|
|
|
|
return []
|
|
|
|
}
|
|
|
|
|
|
|
|
function getFragmentChildren (element: RendererNode | null, blocks: string[] = []) {
|
|
|
|
if (element && element.nodeName) {
|
|
|
|
if (isEndFragment(element)) {
|
|
|
|
return blocks
|
|
|
|
} else if (!isStartFragment(element)) {
|
|
|
|
blocks.push(element.outerHTML)
|
|
|
|
}
|
|
|
|
|
|
|
|
getFragmentChildren(element.nextSibling, blocks)
|
|
|
|
}
|
|
|
|
return blocks
|
|
|
|
}
|
|
|
|
|
|
|
|
function isStartFragment (element: RendererNode) {
|
|
|
|
return element.nodeName === '#comment' && element.nodeValue === '['
|
|
|
|
}
|
|
|
|
|
|
|
|
function isEndFragment (element: RendererNode) {
|
|
|
|
return element.nodeName === '#comment' && element.nodeValue === ']'
|
|
|
|
}
|