import { Fragment, Teleport, computed, createStaticVNode, createVNode, defineComponent, getCurrentInstance, h, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, toRaw, watch, withMemo } from "vue"; import { debounce } from "perfect-debounce"; import { hash } from "ohash"; import { appendResponseHeader } from "h3"; import { randomUUID } from "uncrypto"; import { joinURL, withQuery } from "ufo"; import { useNuxtApp, useRuntimeConfig } from "../nuxt.js"; import { prerenderRoutes, useRequestEvent } from "../composables/ssr.js"; import { injectHead } from "../composables/head.js"; import { getFragmentHTML, isEndFragment, isStartFragment } from "./utils.js"; import { appBaseURL, remoteComponentIslands, selectiveClient } from "#build/nuxt.config.mjs"; const pKey = "_islandPromises"; const SSR_UID_RE = /data-island-uid="([^"]*)"/; const DATA_ISLAND_UID_RE = /data-island-uid(="")?(?!="[^"])/g; const SLOTNAME_RE = /data-island-slot="([^"]*)"/g; const SLOT_FALLBACK_RE = / data-island-slot="([^"]*)"[^>]*>/g; const ISLAND_SCOPE_ID_RE = /^<[^> ]*/; let id = 1; const getId = import.meta.client ? () => (id++).toString() : randomUUID; const components = import.meta.client ? /* @__PURE__ */ new Map() : void 0; async function loadComponents(source = appBaseURL, paths) { if (!paths) { return; } const promises = []; for (const [component, item] of Object.entries(paths)) { if (!components.has(component)) { promises.push((async () => { const chunkSource = joinURL(source, item.chunk); const c = await import( /* @vite-ignore */ chunkSource ).then((m) => m.default || m); components.set(component, c); })()); } } await Promise.all(promises); } export default defineComponent({ name: "NuxtIsland", inheritAttrs: false, props: { name: { type: String, required: true }, lazy: Boolean, props: { type: Object, default: () => void 0 }, context: { type: Object, default: () => ({}) }, scopeId: { type: String, default: () => void 0 }, source: { type: String, default: () => void 0 }, dangerouslyLoadClientComponents: { type: Boolean, default: false } }, emits: ["error"], async setup(props, { slots, expose, emit }) { let canTeleport = import.meta.server; const teleportKey = shallowRef(0); const key = shallowRef(0); const canLoadClientComponent = computed(() => selectiveClient && (props.dangerouslyLoadClientComponents || !props.source)); const error = ref(null); const config = useRuntimeConfig(); const nuxtApp = useNuxtApp(); const filteredProps = computed(() => props.props ? Object.fromEntries(Object.entries(props.props).filter(([key2]) => !key2.startsWith("data-v-"))) : {}); const hashId = computed(() => hash([props.name, filteredProps.value, props.context, props.source]).replace(/[-_]/g, "")); const instance = getCurrentInstance(); const event = useRequestEvent(); let activeHead; const eventFetch = import.meta.server ? event.fetch : import.meta.dev ? $fetch.raw : globalThis.fetch; const mounted = shallowRef(false); onMounted(() => { mounted.value = true; teleportKey.value++; }); onBeforeUnmount(() => { if (activeHead) { activeHead.dispose(); } }); function setPayload(key2, result) { const toRevive = {}; if (result.props) { toRevive.props = result.props; } if (result.slots) { toRevive.slots = result.slots; } if (result.components) { toRevive.components = result.components; } if (result.head) { toRevive.head = result.head; } nuxtApp.payload.data[key2] = { __nuxt_island: { key: key2, ...import.meta.server && import.meta.prerender ? {} : { params: { ...props.context, props: props.props ? JSON.stringify(props.props) : void 0 } }, result: toRevive }, ...result }; } const payloads = {}; if (instance.vnode.el) { const slots2 = toRaw(nuxtApp.payload.data[`${props.name}_${hashId.value}`])?.slots; if (slots2) { payloads.slots = slots2; } if (selectiveClient) { const components2 = toRaw(nuxtApp.payload.data[`${props.name}_${hashId.value}`])?.components; if (components2) { payloads.components = components2; } } } const ssrHTML = ref(""); if (import.meta.client && instance.vnode?.el) { if (import.meta.dev) { let currentEl = instance.vnode.el; let startEl = null; let isFirstElement = true; while (currentEl) { if (isEndFragment(currentEl)) { if (startEl !== currentEl.previousSibling) { console.warn(`[\`Server components(and islands)\`] "${props.name}" must have a single root element. (HTML comments are considered elements as well.)`); } break; } else if (!isStartFragment(currentEl) && isFirstElement) { isFirstElement = false; if (currentEl.nodeType === 1) { startEl = currentEl; } } currentEl = currentEl.nextSibling; } } ssrHTML.value = getFragmentHTML(instance.vnode.el, true)?.join("") || ""; const key2 = `${props.name}_${hashId.value}`; nuxtApp.payload.data[key2] ||= {}; nuxtApp.payload.data[key2].html = ssrHTML.value.replaceAll(new RegExp(`data-island-uid="${ssrHTML.value.match(SSR_UID_RE)?.[1] || ""}"`, "g"), `data-island-uid=""`); } const uid = ref(ssrHTML.value.match(SSR_UID_RE)?.[1] || getId()); const currentSlots = new Set(Object.keys(slots)); const availableSlots = computed(() => new Set([...ssrHTML.value.matchAll(SLOTNAME_RE)].map((m) => m[1]))); const html = computed(() => { let html2 = ssrHTML.value; if (props.scopeId) { html2 = html2.replace(ISLAND_SCOPE_ID_RE, (full) => full + " " + props.scopeId); } if (import.meta.client && !canLoadClientComponent.value) { for (const [key2, value] of Object.entries(payloads.components || {})) { html2 = html2.replace(new RegExp(` data-island-uid="${uid.value}" data-island-component="${key2}"[^>]*>`), (full) => { return full + value.html; }); } } if (payloads.slots) { return html2.replaceAll(SLOT_FALLBACK_RE, (full, slotName) => { if (!currentSlots.has(slotName)) { return full + (payloads.slots?.[slotName]?.fallback || ""); } return full; }); } return html2; }); const head = injectHead(); async function _fetchComponent(force = false) { const key2 = `${props.name}_${hashId.value}`; if (!force && nuxtApp.payload.data[key2]?.html) { return nuxtApp.payload.data[key2]; } const url = remoteComponentIslands && props.source ? new URL(`/__nuxt_island/${key2}.json`, props.source).href : `/__nuxt_island/${key2}.json`; if (import.meta.server && import.meta.prerender) { nuxtApp.runWithContext(() => prerenderRoutes(url)); } const r = await eventFetch(withQuery(import.meta.dev && import.meta.client || props.source ? url : joinURL(config.app.baseURL ?? "", url), { ...props.context, props: props.props ? JSON.stringify(props.props) : void 0 })); try { const result = import.meta.server || !import.meta.dev ? await r.json() : r._data; if (import.meta.server && import.meta.prerender) { const hints = r.headers.get("x-nitro-prerender"); if (hints) { appendResponseHeader(event, "x-nitro-prerender", hints); } } setPayload(key2, result); return result; } catch (e) { if (r.status !== 200) { throw new Error(e.toString(), { cause: r }); } throw e; } } async function fetchComponent(force = false) { nuxtApp[pKey] ||= {}; nuxtApp[pKey][uid.value] ||= _fetchComponent(force).finally(() => { delete nuxtApp[pKey][uid.value]; }); try { const res = await nuxtApp[pKey][uid.value]; ssrHTML.value = res.html.replaceAll(DATA_ISLAND_UID_RE, `data-island-uid="${uid.value}"`); key.value++; error.value = null; payloads.slots = res.slots || {}; payloads.components = res.components || {}; if (selectiveClient && import.meta.client) { if (canLoadClientComponent.value && res.components) { await loadComponents(props.source, res.components); } } if (res?.head) { if (activeHead) { activeHead.patch(res.head); } else { activeHead = head.push(res.head); } } if (import.meta.client) { nextTick(() => { canTeleport = true; teleportKey.value++; }); } } catch (e) { error.value = e; emit("error", e); } } expose({ refresh: () => fetchComponent(true) }); if (import.meta.hot) { import.meta.hot.on(`nuxt-server-component:${props.name}`, () => { fetchComponent(true); }); } if (import.meta.client) { watch(props, debounce(() => fetchComponent(), 100), { deep: true }); } if (import.meta.client && !instance.vnode.el && props.lazy) { fetchComponent(); } else if (import.meta.server || !instance.vnode.el || !nuxtApp.payload.serverRendered) { await fetchComponent(); } else if (selectiveClient && canLoadClientComponent.value) { await loadComponents(props.source, payloads.components); } return (_ctx, _cache) => { if (!html.value || error.value) { return [slots.fallback?.({ error: error.value }) ?? createVNode("div")]; } return [ withMemo([key.value], () => { return createVNode(Fragment, { key: key.value }, [h(createStaticVNode(html.value || "
", 1))]); }, _cache, 0), // should away be triggered ONE tick after re-rendering the static node withMemo([teleportKey.value], () => { const teleports = []; const isKeyOdd = teleportKey.value === 0 || !!(teleportKey.value && !(teleportKey.value % 2)); if (uid.value && html.value && (import.meta.server || props.lazy ? canTeleport : mounted.value || instance.vnode?.el)) { for (const slot in slots) { if (availableSlots.value.has(slot)) { teleports.push( createVNode( Teleport, // use different selectors for even and odd teleportKey to force trigger the teleport { to: import.meta.client ? `${isKeyOdd ? "div" : ""}[data-island-uid="${uid.value}"][data-island-slot="${slot}"]` : `uid=${uid.value};slot=${slot}` }, { default: () => (payloads.slots?.[slot]?.props?.length ? payloads.slots[slot].props : [{}]).map((data) => slots[slot]?.(data)) } ) ); } } if (selectiveClient) { if (import.meta.server) { if (payloads.components) { for (const [id2, info] of Object.entries(payloads.components)) { const { html: html2, slots: slots2 } = info; let replaced = html2.replaceAll("data-island-uid", `data-island-uid="${uid.value}"`); for (const slot in slots2) { replaced = replaced.replaceAll(`data-island-slot="${slot}">`, (full) => full + slots2[slot]); } teleports.push(createVNode(Teleport, { to: `uid=${uid.value};client=${id2}` }, { default: () => [createStaticVNode(replaced, 1)] })); } } } else if (canLoadClientComponent.value && payloads.components) { for (const [id2, info] of Object.entries(payloads.components)) { const { props: props2, slots: slots2 } = info; const component = components.get(id2); const vnode = createVNode(Teleport, { to: `${isKeyOdd ? "div" : ""}[data-island-uid='${uid.value}'][data-island-component="${id2}"]` }, { default: () => { return [h(component, props2, Object.fromEntries(Object.entries(slots2 || {}).map(([k, v]) => [ k, () => createStaticVNode(`
${v}
`, 1) ])))]; } }); teleports.push(vnode); } } } } return h(Fragment, teleports); }, _cache, 1) ]; }; } });