mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-25 15:15:19 +00:00
refactor(nuxt): refactor island response + improve rendering (#25190)
This commit is contained in:
parent
b3fb75c00e
commit
986786a4a9
@ -27,6 +27,6 @@ export default defineComponent({
|
|||||||
console.log(e)
|
console.log(e)
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => createVNode(component || 'span', { ...props.context.props, 'nuxt-ssr-component-uid': '' })
|
return () => createVNode(component || 'span', { ...props.context.props, 'data-island-uid': '' })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { Component } from 'vue'
|
import type { Component } from 'vue'
|
||||||
import { Fragment, Teleport, computed, createStaticVNode, createVNode, defineComponent, getCurrentInstance, h, nextTick, onMounted, ref, toRaw, watch } from 'vue'
|
import { Fragment, Teleport, computed, createStaticVNode, createVNode, defineComponent, getCurrentInstance, h, nextTick, onMounted, ref, toRaw, watch, withMemo } from 'vue'
|
||||||
import { debounce } from 'perfect-debounce'
|
import { debounce } from 'perfect-debounce'
|
||||||
import { hash } from 'ohash'
|
import { hash } from 'ohash'
|
||||||
import { appendResponseHeader } from 'h3'
|
import { appendResponseHeader } from 'h3'
|
||||||
@ -13,29 +13,29 @@ import { join } from 'pathe'
|
|||||||
import type { NuxtIslandResponse } from '../../core/runtime/nitro/renderer'
|
import type { NuxtIslandResponse } from '../../core/runtime/nitro/renderer'
|
||||||
import { useNuxtApp, useRuntimeConfig } from '../nuxt'
|
import { useNuxtApp, useRuntimeConfig } from '../nuxt'
|
||||||
import { prerenderRoutes, useRequestEvent } from '../composables/ssr'
|
import { prerenderRoutes, useRequestEvent } from '../composables/ssr'
|
||||||
import { getFragmentHTML, getSlotProps } from './utils'
|
import { getFragmentHTML } from './utils'
|
||||||
|
|
||||||
// @ts-expect-error virtual file
|
// @ts-expect-error virtual file
|
||||||
import { remoteComponentIslands, selectiveClient } from '#build/nuxt.config.mjs'
|
import { remoteComponentIslands, selectiveClient } from '#build/nuxt.config.mjs'
|
||||||
|
|
||||||
const pKey = '_islandPromises'
|
const pKey = '_islandPromises'
|
||||||
const SSR_UID_RE = /nuxt-ssr-component-uid="([^"]*)"/
|
const SSR_UID_RE = /data-island-uid="([^"]*)"/
|
||||||
const UID_ATTR = /nuxt-ssr-component-uid(="([^"]*)")?/
|
const DATA_ISLAND_UID_RE = /data-island-uid/g
|
||||||
const SLOTNAME_RE = /nuxt-ssr-slot-name="([^"]*)"/g
|
const SLOTNAME_RE = /data-island-slot="([^"]*)"/g
|
||||||
const SLOT_FALLBACK_RE = /<div nuxt-slot-fallback-start="([^"]*)"[^>]*><\/div>(((?!<div nuxt-slot-fallback-end[^>]*>)[\s\S])*)<div nuxt-slot-fallback-end[^>]*><\/div>/g
|
const SLOT_FALLBACK_RE = / data-island-slot="([^"]*)"[^>]*>/g
|
||||||
|
|
||||||
let id = 0
|
let id = 1
|
||||||
const getId = import.meta.client ? () => (id++).toString() : randomUUID
|
const getId = import.meta.client ? () => (id++).toString() : randomUUID
|
||||||
|
|
||||||
const components = import.meta.client ? new Map<string, Component>() : undefined
|
const components = import.meta.client ? new Map<string, Component>() : undefined
|
||||||
|
|
||||||
async function loadComponents (source = '/', paths: Record<string, string>) {
|
async function loadComponents (source = '/', paths: NuxtIslandResponse['components']) {
|
||||||
const promises = []
|
const promises = []
|
||||||
|
|
||||||
for (const component in paths) {
|
for (const component in paths) {
|
||||||
if (!(components!.has(component))) {
|
if (!(components!.has(component))) {
|
||||||
promises.push((async () => {
|
promises.push((async () => {
|
||||||
const chunkSource = join(source, paths[component])
|
const chunkSource = join(source, paths[component].chunk)
|
||||||
const c = await import(/* @vite-ignore */ chunkSource).then(m => m.default || m)
|
const c = await import(/* @vite-ignore */ chunkSource).then(m => m.default || m)
|
||||||
components!.set(component, c)
|
components!.set(component, c)
|
||||||
})())
|
})())
|
||||||
@ -44,14 +44,6 @@ async function loadComponents (source = '/', paths: Record<string, string>) {
|
|||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
}
|
}
|
||||||
|
|
||||||
function emptyPayload () {
|
|
||||||
return {
|
|
||||||
chunks: {},
|
|
||||||
props: {},
|
|
||||||
teleports: {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'NuxtIsland',
|
name: 'NuxtIsland',
|
||||||
props: {
|
props: {
|
||||||
@ -78,6 +70,8 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async setup (props, { slots, expose }) {
|
async setup (props, { slots, expose }) {
|
||||||
|
let canTeleport = import.meta.server
|
||||||
|
const teleportKey = ref(0)
|
||||||
const key = ref(0)
|
const key = ref(0)
|
||||||
const canLoadClientComponent = computed(() => selectiveClient && (props.dangerouslyLoadClientComponents || !props.source))
|
const canLoadClientComponent = computed(() => selectiveClient && (props.dangerouslyLoadClientComponents || !props.source))
|
||||||
const error = ref<unknown>(null)
|
const error = ref<unknown>(null)
|
||||||
@ -91,7 +85,7 @@ export default defineComponent({
|
|||||||
// TODO: remove use of `$fetch.raw` when nitro 503 issues on windows dev server are resolved
|
// TODO: remove use of `$fetch.raw` when nitro 503 issues on windows dev server are resolved
|
||||||
const eventFetch = import.meta.server ? event.fetch : import.meta.dev ? $fetch.raw : globalThis.fetch
|
const eventFetch = import.meta.server ? event.fetch : import.meta.dev ? $fetch.raw : globalThis.fetch
|
||||||
const mounted = ref(false)
|
const mounted = ref(false)
|
||||||
onMounted(() => { mounted.value = true })
|
onMounted(() => { mounted.value = true; teleportKey.value++ })
|
||||||
|
|
||||||
function setPayload (key: string, result: NuxtIslandResponse) {
|
function setPayload (key: string, result: NuxtIslandResponse) {
|
||||||
nuxtApp.payload.data[key] = {
|
nuxtApp.payload.data[key] = {
|
||||||
@ -101,60 +95,51 @@ export default defineComponent({
|
|||||||
? {}
|
? {}
|
||||||
: { params: { ...props.context, props: props.props ? JSON.stringify(props.props) : undefined } },
|
: { params: { ...props.context, props: props.props ? JSON.stringify(props.props) : undefined } },
|
||||||
result: {
|
result: {
|
||||||
chunks: result.chunks,
|
|
||||||
props: result.props,
|
props: result.props,
|
||||||
teleports: result.teleports
|
slots: result.slots,
|
||||||
|
components: result.components
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
...result
|
...result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// needs to be non-reactive because we don't want to trigger re-renders
|
|
||||||
// at hydration, we only retrieve props/chunks/teleports from payload. See the reviver at nuxt\src\app\plugins\revive-payload.client.ts
|
|
||||||
// If not hydrating, fetchComponent() will set it
|
|
||||||
const rawPayload = nuxtApp.isHydrating ? toRaw(nuxtApp.payload.data)?.[`${props.name}_${hashId.value}`] ?? emptyPayload() : emptyPayload()
|
|
||||||
|
|
||||||
const nonReactivePayload: Pick<NuxtIslandResponse, 'chunks'| 'props' | 'teleports'> = {
|
const payloadSlots: NonNullable<NuxtIslandResponse['slots']> = {}
|
||||||
chunks: rawPayload.chunks,
|
const payloadComponents: NonNullable<NuxtIslandResponse['components']> = {}
|
||||||
props: rawPayload.props,
|
|
||||||
teleports: rawPayload.teleports
|
if (nuxtApp.isHydrating) {
|
||||||
|
Object.assign(payloadSlots, toRaw(nuxtApp.payload.data[`${props.name}_${hashId.value}`])?.slots ?? {})
|
||||||
|
Object.assign(payloadComponents, toRaw(nuxtApp.payload.data[`${props.name}_${hashId.value}`])?.components ?? {})
|
||||||
}
|
}
|
||||||
|
|
||||||
const ssrHTML = ref<string>('')
|
const ssrHTML = ref<string>('')
|
||||||
|
|
||||||
if (import.meta.client) {
|
if (import.meta.client && nuxtApp.isHydrating) {
|
||||||
ssrHTML.value = getFragmentHTML(instance.vnode?.el ?? null, true)?.join('') || ''
|
ssrHTML.value = getFragmentHTML(instance.vnode?.el ?? null, true)?.join('') || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const slotProps = computed(() => getSlotProps(ssrHTML.value))
|
|
||||||
const uid = ref<string>(ssrHTML.value.match(SSR_UID_RE)?.[1] ?? getId())
|
const uid = ref<string>(ssrHTML.value.match(SSR_UID_RE)?.[1] ?? getId())
|
||||||
const availableSlots = computed(() => [...ssrHTML.value.matchAll(SLOTNAME_RE)].map(m => m[1]))
|
const availableSlots = computed(() => [...ssrHTML.value.matchAll(SLOTNAME_RE)].map(m => m[1]))
|
||||||
|
|
||||||
const html = computed(() => {
|
const html = computed(() => {
|
||||||
const currentSlots = Object.keys(slots)
|
const currentSlots = Object.keys(slots)
|
||||||
let html = ssrHTML.value
|
let html = ssrHTML.value
|
||||||
|
|
||||||
if (import.meta.client && !canLoadClientComponent.value) {
|
if (import.meta.client && !canLoadClientComponent.value) {
|
||||||
for (const [key, value] of Object.entries(nonReactivePayload.teleports || {})) {
|
for (const [key, value] of Object.entries(payloadComponents || {})) {
|
||||||
html = html.replace(new RegExp(`<div [^>]*nuxt-ssr-client="${key}"[^>]*>`), (full) => {
|
html = html.replace(new RegExp(` data-island-uid="${uid.value}" data-island-client="${key}"[^>]*>`), (full) => {
|
||||||
return full + value
|
return full + value.html
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return html.replace(SLOT_FALLBACK_RE, (full, slotName, content) => {
|
return html.replaceAll(SLOT_FALLBACK_RE, (full, slotName) => {
|
||||||
// remove fallback to insert slots
|
if (!currentSlots.includes(slotName)) {
|
||||||
if (currentSlots.includes(slotName)) {
|
return full + payloadSlots[slotName]?.fallback ?? ''
|
||||||
return ''
|
|
||||||
}
|
}
|
||||||
return content
|
return full
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
function setUid () {
|
|
||||||
uid.value = ssrHTML.value.match(SSR_UID_RE)?.[1] ?? getId() as string
|
|
||||||
}
|
|
||||||
|
|
||||||
const cHead = ref<Record<'link' | 'style', Array<Record<string, string>>>>({ link: [], style: [] })
|
const cHead = ref<Record<'link' | 'style', Array<Record<string, string>>>>({ link: [], style: [] })
|
||||||
useHead(cHead)
|
useHead(cHead)
|
||||||
|
|
||||||
@ -198,26 +183,25 @@ export default defineComponent({
|
|||||||
const res: NuxtIslandResponse = await nuxtApp[pKey][uid.value]
|
const res: NuxtIslandResponse = await nuxtApp[pKey][uid.value]
|
||||||
cHead.value.link = res.head.link
|
cHead.value.link = res.head.link
|
||||||
cHead.value.style = res.head.style
|
cHead.value.style = res.head.style
|
||||||
ssrHTML.value = res.html.replace(UID_ATTR, () => {
|
ssrHTML.value = res.html.replaceAll(DATA_ISLAND_UID_RE, `data-island-uid="${uid.value}"`)
|
||||||
return `nuxt-ssr-component-uid="${getId()}"`
|
|
||||||
})
|
|
||||||
key.value++
|
key.value++
|
||||||
error.value = null
|
error.value = null
|
||||||
|
Object.assign(payloadSlots, res.slots || {})
|
||||||
|
Object.assign(payloadComponents, res.components || {})
|
||||||
|
|
||||||
if (selectiveClient && import.meta.client) {
|
if (selectiveClient && import.meta.client) {
|
||||||
if (canLoadClientComponent.value && res.chunks) {
|
if (canLoadClientComponent.value && res.components) {
|
||||||
await loadComponents(props.source, res.chunks)
|
await loadComponents(props.source, res.components)
|
||||||
}
|
}
|
||||||
nonReactivePayload.props = res.props
|
|
||||||
}
|
}
|
||||||
nonReactivePayload.teleports = res.teleports
|
|
||||||
nonReactivePayload.chunks = res.chunks
|
|
||||||
|
|
||||||
if (import.meta.client) {
|
if (import.meta.client) {
|
||||||
// must await next tick for Teleport to work correctly with static node re-rendering
|
// must await next tick for Teleport to work correctly with static node re-rendering
|
||||||
await nextTick()
|
nextTick(() => {
|
||||||
|
canTeleport = true
|
||||||
|
teleportKey.value++
|
||||||
|
})
|
||||||
}
|
}
|
||||||
setUid()
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e
|
error.value = e
|
||||||
}
|
}
|
||||||
@ -241,46 +225,61 @@ export default defineComponent({
|
|||||||
fetchComponent()
|
fetchComponent()
|
||||||
} else if (import.meta.server || !nuxtApp.isHydrating || !nuxtApp.payload.serverRendered) {
|
} else if (import.meta.server || !nuxtApp.isHydrating || !nuxtApp.payload.serverRendered) {
|
||||||
await fetchComponent()
|
await fetchComponent()
|
||||||
} else if (selectiveClient && canLoadClientComponent.value && nonReactivePayload.chunks) {
|
} else if (selectiveClient && canLoadClientComponent.value) {
|
||||||
await loadComponents(props.source, nonReactivePayload.chunks)
|
await loadComponents(props.source, payloadComponents)
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return (_ctx: any, _cache: any) => {
|
||||||
if (!html.value || error.value) {
|
if (!html.value || error.value) {
|
||||||
return [slots.fallback?.({ error: error.value }) ?? createVNode('div')]
|
return [slots.fallback?.({ error: error.value }) ?? createVNode('div')]
|
||||||
}
|
}
|
||||||
const nodes = [createVNode(Fragment, {
|
return [
|
||||||
key: key.value
|
withMemo([key.value], () => {
|
||||||
}, [h(createStaticVNode(html.value || '<div></div>', 1))])]
|
return createVNode(Fragment, { key: key.value }, [h(createStaticVNode(html.value || '<div></div>', 1))])
|
||||||
|
}, _cache, 0),
|
||||||
|
|
||||||
if (uid.value && (mounted.value || nuxtApp.isHydrating || import.meta.server) && html.value) {
|
// should away be triggered ONE tick after re-rendering the static node
|
||||||
for (const slot in slots) {
|
withMemo([teleportKey.value], () => {
|
||||||
if (availableSlots.value.includes(slot)) {
|
const teleports = []
|
||||||
nodes.push(createVNode(Teleport, { to: import.meta.client ? `[nuxt-ssr-component-uid='${uid.value}'] [nuxt-ssr-slot-name='${slot}']` : `uid=${uid.value};slot=${slot}` }, {
|
// this is used to force trigger Teleport when vue makes the diff between old and new node
|
||||||
default: () => (slotProps.value[slot] ?? [undefined]).map((data: any) => slots[slot]?.(data))
|
const isKeyOdd = teleportKey.value === 0 || !!(teleportKey.value && !(teleportKey.value % 2))
|
||||||
}))
|
|
||||||
}
|
if (uid.value && html.value && (import.meta.server || props.lazy ? canTeleport : mounted.value || nuxtApp.isHydrating)) {
|
||||||
}
|
for (const slot in slots) {
|
||||||
if (import.meta.server) {
|
if (availableSlots.value.includes(slot)) {
|
||||||
for (const [id, html] of Object.entries(nonReactivePayload.teleports ?? {})) {
|
teleports.push(createVNode(Teleport,
|
||||||
nodes.push(createVNode(Teleport, { to: `uid=${uid.value};client=${id}` }, {
|
// use different selectors for even and odd teleportKey to force trigger the teleport
|
||||||
default: () => [createStaticVNode(html, 1)]
|
{ to: import.meta.client ? `${isKeyOdd ? 'div' : ''}[data-island-uid="${uid.value}"][data-island-slot="${slot}"]` : `uid=${uid.value};slot=${slot}` },
|
||||||
}))
|
{ default: () => (payloadSlots[slot].props?.length ? payloadSlots[slot].props : [{}]).map((data: any) => slots[slot]?.(data)) })
|
||||||
}
|
)
|
||||||
}
|
|
||||||
if (selectiveClient && import.meta.client && canLoadClientComponent.value) {
|
|
||||||
for (const [id, props] of Object.entries(nonReactivePayload.props ?? {})) {
|
|
||||||
const component = components!.get(id.split('-')[0])!
|
|
||||||
const vnode = createVNode(Teleport, { to: `[nuxt-ssr-component-uid='${uid.value}'] [nuxt-ssr-client="${id}"]` }, {
|
|
||||||
default: () => {
|
|
||||||
return [h(component, props)]
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
nodes.push(vnode)
|
if (import.meta.server) {
|
||||||
|
for (const [id, info] of Object.entries(payloadComponents ?? {})) {
|
||||||
|
const { html } = info
|
||||||
|
teleports.push(createVNode(Teleport, { to: `uid=${uid.value};client=${id}` }, {
|
||||||
|
default: () => [createStaticVNode(html, 1)]
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (selectiveClient && import.meta.client && canLoadClientComponent.value) {
|
||||||
|
for (const [id, info] of Object.entries(payloadComponents ?? {})) {
|
||||||
|
const { props } = info
|
||||||
|
const component = components!.get(id)!
|
||||||
|
// use different selectors for even and odd teleportKey to force trigger the teleport
|
||||||
|
const vnode = createVNode(Teleport, { to: `${isKeyOdd ? 'div' : ''}[data-island-uid='${uid.value}'][data-island-client="${id}"]` }, {
|
||||||
|
default: () => {
|
||||||
|
return [h(component, props)]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
teleports.push(vnode)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
return h(Fragment, teleports)
|
||||||
return nodes
|
}, _cache, 1)
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { Component, } from 'vue'
|
import type { Component } from 'vue'
|
||||||
import { Teleport, defineComponent, h } from 'vue'
|
import { Teleport, defineComponent, h } from 'vue'
|
||||||
import { useNuxtApp } from '../nuxt'
|
import { useNuxtApp } from '../nuxt'
|
||||||
// @ts-expect-error virtual file
|
// @ts-expect-error virtual file
|
||||||
@ -14,7 +14,7 @@ type ExtendedComponent = Component & {
|
|||||||
* this teleport the component in SSR only if it needs to be hydrated on client
|
* this teleport the component in SSR only if it needs to be hydrated on client
|
||||||
*/
|
*/
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'NuxtTeleportSsrClient',
|
name: 'NuxtTeleportIslandClient',
|
||||||
props: {
|
props: {
|
||||||
to: {
|
to: {
|
||||||
type: String,
|
type: String,
|
||||||
@ -34,28 +34,26 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
setup (props, { slots }) {
|
setup (props, { slots }) {
|
||||||
if (!props.nuxtClient) { return () => slots.default!() }
|
const nuxtApp = useNuxtApp()
|
||||||
|
|
||||||
const app = useNuxtApp()
|
if (!nuxtApp.ssrContext?.islandContext || !props.nuxtClient) { return () => slots.default!() }
|
||||||
const islandContext = app.ssrContext!.islandContext!
|
|
||||||
|
const islandContext = nuxtApp.ssrContext!.islandContext!
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
const slot = slots.default!()[0]
|
const slot = slots.default!()[0]
|
||||||
const slotType = (slot.type as ExtendedComponent)
|
const slotType = (slot.type as ExtendedComponent)
|
||||||
const name = (slotType.__name || slotType.name) as string
|
const name = (slotType.__name || slotType.name) as string
|
||||||
|
|
||||||
if (import.meta.dev) {
|
islandContext.components[props.to] = {
|
||||||
const path = '_nuxt/' + paths[name]
|
chunk: import.meta.dev ? '_nuxt/' + paths[name] : paths[name],
|
||||||
islandContext.chunks[name] = path
|
props: slot.props || {}
|
||||||
} else {
|
|
||||||
islandContext.chunks[name] = paths[name]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
islandContext.propsData[props.to] = slot.props || {}
|
|
||||||
|
|
||||||
return [h('div', {
|
return [h('div', {
|
||||||
style: 'display: contents;',
|
style: 'display: contents;',
|
||||||
'nuxt-ssr-client': props.to
|
'data-island-uid': '',
|
||||||
|
'data-island-client': props.to
|
||||||
}, []), h(Teleport, { to: props.to }, slot)]
|
}, []), h(Teleport, { to: props.to }, slot)]
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
import { Teleport, defineComponent, h } from 'vue'
|
||||||
|
import { useNuxtApp } from '../nuxt'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* component only used within islands for slot teleport
|
||||||
|
*/
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'NuxtTeleportIslandSlot',
|
||||||
|
props: {
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* must be an array to handle v-for
|
||||||
|
*/
|
||||||
|
props: {
|
||||||
|
type: Object as () => Array<any>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup (props, { slots }) {
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
const islandContext = nuxtApp.ssrContext?.islandContext
|
||||||
|
|
||||||
|
if(!islandContext) {
|
||||||
|
return () => slots.default?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
islandContext.slots[props.name] = {
|
||||||
|
props: (props.props || []) as unknown[]
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const vnodes = [h('div', {
|
||||||
|
style: 'display: contents;',
|
||||||
|
'data-island-uid': '',
|
||||||
|
'data-island-slot': props.name,
|
||||||
|
})]
|
||||||
|
|
||||||
|
if (slots.fallback) {
|
||||||
|
vnodes.push(h(Teleport, { to: `island-fallback=${props.name}`}, slots.fallback()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return vnodes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
@ -2,7 +2,6 @@ import { h } from 'vue'
|
|||||||
import type { Component, RendererNode } from 'vue'
|
import type { Component, RendererNode } from 'vue'
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
import { isString, isPromise, isArray, isObject } from '@vue/shared'
|
import { isString, isPromise, isArray, isObject } from '@vue/shared'
|
||||||
import destr from 'destr'
|
|
||||||
import type { RouteLocationNormalized } from '#vue-router'
|
import type { RouteLocationNormalized } from '#vue-router'
|
||||||
// @ts-expect-error virtual file
|
// @ts-expect-error virtual file
|
||||||
import { START_LOCATION } from '#build/pages'
|
import { START_LOCATION } from '#build/pages'
|
||||||
@ -143,7 +142,7 @@ export function getFragmentHTML (element: RendererNode | null, withoutSlots = fa
|
|||||||
}
|
}
|
||||||
if (withoutSlots) {
|
if (withoutSlots) {
|
||||||
const clone = element.cloneNode(true)
|
const clone = element.cloneNode(true)
|
||||||
clone.querySelectorAll('[nuxt-ssr-slot-name]').forEach((n: Element) => { n.innerHTML = '' })
|
clone.querySelectorAll('[data-island-slot]').forEach((n: Element) => { n.innerHTML = '' })
|
||||||
return [clone.outerHTML]
|
return [clone.outerHTML]
|
||||||
}
|
}
|
||||||
return [element.outerHTML]
|
return [element.outerHTML]
|
||||||
@ -158,7 +157,7 @@ function getFragmentChildren (element: RendererNode | null, blocks: string[] = [
|
|||||||
} else if (!isStartFragment(element)) {
|
} else if (!isStartFragment(element)) {
|
||||||
const clone = element.cloneNode(true) as Element
|
const clone = element.cloneNode(true) as Element
|
||||||
if (withoutSlots) {
|
if (withoutSlots) {
|
||||||
clone.querySelectorAll('[nuxt-ssr-slot-name]').forEach((n) => { n.innerHTML = '' })
|
clone.querySelectorAll('[data-island-slot]').forEach((n) => { n.innerHTML = '' })
|
||||||
}
|
}
|
||||||
blocks.push(clone.outerHTML)
|
blocks.push(clone.outerHTML)
|
||||||
}
|
}
|
||||||
@ -175,15 +174,3 @@ function isStartFragment (element: RendererNode) {
|
|||||||
function isEndFragment (element: RendererNode) {
|
function isEndFragment (element: RendererNode) {
|
||||||
return element.nodeName === '#comment' && element.nodeValue === ']'
|
return element.nodeName === '#comment' && element.nodeValue === ']'
|
||||||
}
|
}
|
||||||
const SLOT_PROPS_RE = /<div[^>]*nuxt-ssr-slot-name="([^"]*)" nuxt-ssr-slot-data="([^"]*)"[^/|>]*>/g
|
|
||||||
|
|
||||||
export function getSlotProps (html: string) {
|
|
||||||
const slotsDivs = html.matchAll(SLOT_PROPS_RE)
|
|
||||||
const data: Record<string, any> = {}
|
|
||||||
for (const slot of slotsDivs) {
|
|
||||||
const [_, slotName, json] = slot
|
|
||||||
const slotData = destr(decodeHtmlEntities(json))
|
|
||||||
data[slotName] = slotData
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
@ -11,17 +11,17 @@ import { resolvePath } from '@nuxt/kit'
|
|||||||
import { isVue } from '../core/utils'
|
import { isVue } from '../core/utils'
|
||||||
|
|
||||||
interface ServerOnlyComponentTransformPluginOptions {
|
interface ServerOnlyComponentTransformPluginOptions {
|
||||||
getComponents: () => Component[]
|
getComponents: () => Component[]
|
||||||
/**
|
/**
|
||||||
* passed down to `NuxtTeleportSsrClient`
|
* passed down to `NuxtTeleportIslandClient`
|
||||||
* should be done only in dev mode as we use build:manifest result in production
|
* should be done only in dev mode as we use build:manifest result in production
|
||||||
*/
|
*/
|
||||||
rootDir?: string
|
rootDir?: string
|
||||||
isDev?: boolean
|
isDev?: boolean
|
||||||
/**
|
/**
|
||||||
* allow using `nuxt-client` attribute on components
|
* allow using `nuxt-client` attribute on components
|
||||||
*/
|
*/
|
||||||
selectiveClient?: boolean
|
selectiveClient?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ComponentChunkOptions {
|
interface ComponentChunkOptions {
|
||||||
@ -33,7 +33,11 @@ const SCRIPT_RE = /<script[^>]*>/g
|
|||||||
const HAS_SLOT_OR_CLIENT_RE = /(<slot[^>]*>)|(nuxt-client)/
|
const HAS_SLOT_OR_CLIENT_RE = /(<slot[^>]*>)|(nuxt-client)/
|
||||||
const TEMPLATE_RE = /<template>([\s\S]*)<\/template>/
|
const TEMPLATE_RE = /<template>([\s\S]*)<\/template>/
|
||||||
const NUXTCLIENT_ATTR_RE = /\snuxt-client(="[^"]*")?/g
|
const NUXTCLIENT_ATTR_RE = /\snuxt-client(="[^"]*")?/g
|
||||||
const IMPORT_CODE = '\nimport { vforToArray as __vforToArray } from \'#app/components/utils\'' + '\nimport NuxtTeleportSsrClient from \'#app/components/nuxt-teleport-ssr-client\''
|
const IMPORT_CODE = '\nimport { vforToArray as __vforToArray } from \'#app/components/utils\'' + '\nimport NuxtTeleportIslandClient from \'#app/components/nuxt-teleport-island-client\'' + '\nimport NuxtTeleportSsrSlot from \'#app/components/nuxt-teleport-island-slot\''
|
||||||
|
|
||||||
|
function wrapWithVForDiv (code: string, vfor: string): string {
|
||||||
|
return `<div v-for="${vfor}" style="display: contents;">${code}</div>`
|
||||||
|
}
|
||||||
|
|
||||||
export const islandsTransform = createUnplugin((options: ServerOnlyComponentTransformPluginOptions, meta) => {
|
export const islandsTransform = createUnplugin((options: ServerOnlyComponentTransformPluginOptions, meta) => {
|
||||||
const isVite = meta.framework === 'vite'
|
const isVite = meta.framework === 'vite'
|
||||||
@ -66,49 +70,38 @@ export const islandsTransform = createUnplugin((options: ServerOnlyComponentTran
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let hasNuxtClient = false
|
let hasNuxtClient = false
|
||||||
|
|
||||||
const ast = parse(template[0])
|
const ast = parse(template[0])
|
||||||
await walk(ast, (node) => {
|
await walk(ast, (node) => {
|
||||||
if (node.type === ELEMENT_NODE) {
|
if (node.type === ELEMENT_NODE) {
|
||||||
if (node.name === 'slot') {
|
if (node.name === 'slot') {
|
||||||
const { attributes, children, loc, isSelfClosingTag } = node
|
const { attributes, children, loc } = node
|
||||||
|
|
||||||
|
// pass slot fallback to NuxtTeleportSsrSlot fallback
|
||||||
|
if (children.length) {
|
||||||
|
const attrString = Object.entries(attributes).map(([name, value]) => name ? `${name}="${value}" ` : value).join(' ')
|
||||||
|
const slice = code.slice(startingIndex + loc[0].end, startingIndex + loc[1].start).replaceAll(/:?key="[^"]"/g, '')
|
||||||
|
s.overwrite(startingIndex + loc[0].start, startingIndex + loc[1].end, `<slot ${attrString} /><template #fallback>${attributes["v-for"] ? wrapWithVForDiv(slice, attributes['v-for']) : slice}</template>`)
|
||||||
|
}
|
||||||
|
|
||||||
const slotName = attributes.name ?? 'default'
|
const slotName = attributes.name ?? 'default'
|
||||||
let vfor: [string, string] | undefined
|
let vfor: [string, string] | undefined
|
||||||
if (attributes['v-for']) {
|
if (attributes['v-for']) {
|
||||||
vfor = attributes['v-for'].split(' in ').map((v: string) => v.trim()) as [string, string]
|
vfor = attributes['v-for'].split(' in ').map((v: string) => v.trim()) as [string, string]
|
||||||
delete attributes['v-for']
|
|
||||||
}
|
}
|
||||||
|
delete attributes['v-for']
|
||||||
|
|
||||||
if (attributes.name) { delete attributes.name }
|
if (attributes.name) { delete attributes.name }
|
||||||
if (attributes['v-bind']) {
|
if (attributes['v-bind']) {
|
||||||
attributes._bind = attributes['v-bind']
|
attributes._bind = attributes['v-bind']
|
||||||
delete attributes['v-bind']
|
delete attributes['v-bind']
|
||||||
}
|
}
|
||||||
const bindings = getBindings(attributes, vfor)
|
const bindings = getPropsToString(attributes, vfor)
|
||||||
|
|
||||||
if (isSelfClosingTag) {
|
// add the wrapper
|
||||||
s.overwrite(startingIndex + loc[0].start, startingIndex + loc[0].end, `<div style="display: contents;" nuxt-ssr-slot-name="${slotName}" ${bindings}/>`)
|
s.appendLeft(startingIndex + loc[0].start, `<NuxtTeleportSsrSlot name="${slotName}" :props="${bindings}">`)
|
||||||
} else {
|
s.appendRight(startingIndex + loc[1].end, '</NuxtTeleportSsrSlot>')
|
||||||
s.overwrite(startingIndex + loc[0].start, startingIndex + loc[0].end, `<div style="display: contents;" nuxt-ssr-slot-name="${slotName}" ${bindings}>`)
|
|
||||||
s.overwrite(startingIndex + loc[1].start, startingIndex + loc[1].end, '</div>')
|
|
||||||
|
|
||||||
if (children.length > 1) {
|
|
||||||
// need to wrap instead of applying v-for on each child
|
|
||||||
const wrapperTag = `<div ${vfor ? `v-for="${vfor[0]} in ${vfor[1]}"` : ''} style="display: contents;">`
|
|
||||||
s.appendRight(startingIndex + loc[0].end, `<div nuxt-slot-fallback-start="${slotName}"/>${wrapperTag}`)
|
|
||||||
s.appendLeft(startingIndex + loc[1].start, '</div><div nuxt-slot-fallback-end/>')
|
|
||||||
} else if (children.length === 1) {
|
|
||||||
if (vfor && children[0].type === ELEMENT_NODE) {
|
|
||||||
const { loc, name, attributes, isSelfClosingTag } = children[0]
|
|
||||||
const attrs = Object.entries(attributes).map(([attr, val]) => `${attr}="${val}"`).join(' ')
|
|
||||||
s.overwrite(startingIndex + loc[0].start, startingIndex + loc[0].end, `<${name} v-for="${vfor[0]} in ${vfor[1]}" ${attrs} ${isSelfClosingTag ? '/' : ''}>`)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.appendRight(startingIndex + loc[0].end, `<div nuxt-slot-fallback-start="${slotName}"/>`)
|
|
||||||
s.appendLeft(startingIndex + loc[1].start, '<div nuxt-slot-fallback-end/>')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (options.selectiveClient && ('nuxt-client' in node.attributes || ':nuxt-client' in node.attributes)) {
|
} else if (options.selectiveClient && ('nuxt-client' in node.attributes || ':nuxt-client' in node.attributes)) {
|
||||||
hasNuxtClient = true
|
hasNuxtClient = true
|
||||||
const attributeValue = node.attributes[':nuxt-client'] || node.attributes['nuxt-client'] || 'true'
|
const attributeValue = node.attributes[':nuxt-client'] || node.attributes['nuxt-client'] || 'true'
|
||||||
@ -117,7 +110,7 @@ export const islandsTransform = createUnplugin((options: ServerOnlyComponentTran
|
|||||||
const htmlCode = code.slice(startingIndex + node.loc[0].start, startingIndex + node.loc[1].end)
|
const htmlCode = code.slice(startingIndex + node.loc[0].start, startingIndex + node.loc[1].end)
|
||||||
const uid = hash(id + node.loc[0].start + node.loc[0].end)
|
const uid = hash(id + node.loc[0].start + node.loc[0].end)
|
||||||
|
|
||||||
s.overwrite(startingIndex + node.loc[0].start, startingIndex + node.loc[1].end, `<NuxtTeleportSsrClient to="${node.name}-${uid}" ${rootDir && isDev ? `root-dir="${rootDir}"` : ''} :nuxt-client="${attributeValue}">${htmlCode.replaceAll(NUXTCLIENT_ATTR_RE, '')}</NuxtTeleportSsrClient>`)
|
s.overwrite(startingIndex + node.loc[0].start, startingIndex + node.loc[1].end, `<NuxtTeleportIslandClient to="${node.name}-${uid}" ${rootDir && isDev ? `root-dir="${rootDir}"` : ''} :nuxt-client="${attributeValue}">${htmlCode.replaceAll(NUXTCLIENT_ATTR_RE, '')}</NuxtTeleportIslandClient>`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -142,14 +135,14 @@ function isBinding (attr: string): boolean {
|
|||||||
return attr.startsWith(':')
|
return attr.startsWith(':')
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBindings (bindings: Record<string, string>, vfor?: [string, string]): string {
|
function getPropsToString (bindings: Record<string, string>, vfor?: [string, string]): string {
|
||||||
if (Object.keys(bindings).length === 0) { return '' }
|
if (Object.keys(bindings).length === 0) { return 'undefined' }
|
||||||
const content = Object.entries(bindings).filter(b => b[0] && b[0] !== '_bind').map(([name, value]) => isBinding(name) ? `${name.slice(1)}: ${value}` : `${name}: \`${value}\``).join(',')
|
const content = Object.entries(bindings).filter(b => b[0] && b[0] !== '_bind').map(([name, value]) => isBinding(name) ? `${name.slice(1)}: ${value}` : `${name}: \`${value}\``).join(',')
|
||||||
const data = bindings._bind ? `mergeProps(${bindings._bind}, { ${content} })` : `{ ${content} }`
|
const data = bindings._bind ? `mergeProps(${bindings._bind}, { ${content} })` : `{ ${content} }`
|
||||||
if (!vfor) {
|
if (!vfor) {
|
||||||
return `:nuxt-ssr-slot-data="JSON.stringify([${data}])"`
|
return `[${data}]`
|
||||||
} else {
|
} else {
|
||||||
return `:nuxt-ssr-slot-data="JSON.stringify(__vforToArray(${vfor[1]}).map(${vfor[0]} => (${data})))"`
|
return `__vforToArray(${vfor[1]}).map(${vfor[0]} => (${data}))`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,7 +151,7 @@ export const componentsChunkPlugin = createUnplugin((options: ComponentChunkOpti
|
|||||||
return {
|
return {
|
||||||
name: 'componentsChunkPlugin',
|
name: 'componentsChunkPlugin',
|
||||||
vite: {
|
vite: {
|
||||||
async config (config) {
|
async config (config) {
|
||||||
const components = options.getComponents()
|
const components = options.getComponents()
|
||||||
config.build = config.build || {}
|
config.build = config.build || {}
|
||||||
config.build.rollupOptions = config.build.rollupOptions || {}
|
config.build.rollupOptions = config.build.rollupOptions || {}
|
||||||
|
@ -58,10 +58,18 @@ export interface NuxtIslandContext {
|
|||||||
name: string
|
name: string
|
||||||
props?: Record<string, any>
|
props?: Record<string, any>
|
||||||
url?: string
|
url?: string
|
||||||
// chunks to load components
|
slots: Record<string, Omit<NuxtIslandSlotResponse, 'html' | 'fallback'>>
|
||||||
chunks: Record<string, string>
|
components: Record<string, Omit<NuxtIslandClientResponse, 'html'>>
|
||||||
// props to be sent back
|
}
|
||||||
propsData: Record<string, any>
|
|
||||||
|
export interface NuxtIslandSlotResponse {
|
||||||
|
props: Array<unknown>
|
||||||
|
fallback: string
|
||||||
|
}
|
||||||
|
export interface NuxtIslandClientResponse {
|
||||||
|
html: string
|
||||||
|
props: unknown
|
||||||
|
chunk: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NuxtIslandResponse {
|
export interface NuxtIslandResponse {
|
||||||
@ -72,9 +80,9 @@ export interface NuxtIslandResponse {
|
|||||||
link: (Record<string, string>)[]
|
link: (Record<string, string>)[]
|
||||||
style: ({ innerHTML: string, key: string })[]
|
style: ({ innerHTML: string, key: string })[]
|
||||||
}
|
}
|
||||||
chunks?: Record<string, string>
|
|
||||||
props?: Record<string, Record<string, any>>
|
props?: Record<string, Record<string, any>>
|
||||||
teleports?: Record<string, string>
|
components?: Record<string, NuxtIslandClientResponse>
|
||||||
|
slots?: Record<string, NuxtIslandSlotResponse>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NuxtRenderResponse {
|
export interface NuxtRenderResponse {
|
||||||
@ -195,10 +203,8 @@ async function getIslandContext (event: H3Event): Promise<NuxtIslandContext> {
|
|||||||
id: hashId,
|
id: hashId,
|
||||||
name: componentName,
|
name: componentName,
|
||||||
props: destr(context.props) || {},
|
props: destr(context.props) || {},
|
||||||
uid: destr(context.uid) || undefined,
|
slots: {},
|
||||||
chunks: {},
|
components: {},
|
||||||
propsData: {},
|
|
||||||
teleports: {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
@ -421,7 +427,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
|||||||
head: normalizeChunks([headTags, ssrContext.styles]),
|
head: normalizeChunks([headTags, ssrContext.styles]),
|
||||||
bodyAttrs: bodyAttrs ? [bodyAttrs] : [],
|
bodyAttrs: bodyAttrs ? [bodyAttrs] : [],
|
||||||
bodyPrepend: normalizeChunks([bodyTagsOpen, ssrContext.teleports?.body]),
|
bodyPrepend: normalizeChunks([bodyTagsOpen, ssrContext.teleports?.body]),
|
||||||
body: [process.env.NUXT_COMPONENT_ISLANDS ? replaceClientTeleport(ssrContext, replaceServerOnlyComponentsSlots(ssrContext, _rendered.html)) : _rendered.html],
|
body: [process.env.NUXT_COMPONENT_ISLANDS ? replaceIslandTeleports(ssrContext, _rendered.html) : _rendered.html],
|
||||||
bodyAppend: [bodyTags]
|
bodyAppend: [bodyTags]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -446,9 +452,8 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
|||||||
head: islandHead,
|
head: islandHead,
|
||||||
html: getServerComponentHTML(htmlContext.body),
|
html: getServerComponentHTML(htmlContext.body),
|
||||||
state: ssrContext.payload.state,
|
state: ssrContext.payload.state,
|
||||||
chunks: islandContext.chunks,
|
components: getClientIslandResponse(ssrContext),
|
||||||
props: islandContext.propsData,
|
slots: getSlotIslandResponse(ssrContext)
|
||||||
teleports: ssrContext.teleports || {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await nitroApp.hooks.callHook('render:island', islandResponse, { event, islandContext })
|
await nitroApp.hooks.callHook('render:island', islandResponse, { event, islandContext })
|
||||||
@ -593,37 +598,56 @@ function getServerComponentHTML (body: string[]): string {
|
|||||||
return match ? match[1] : body[0]
|
return match ? match[1] : body[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
const SSR_TELEPORT_MARKER = /^uid=([^;]*);slot=(.*)$/
|
const SSR_SLOT_TELEPORT_MARKER = /^uid=([^;]*);slot=(.*)$/
|
||||||
const SSR_CLIENT_TELEPORT_MARKER = /^uid=([^;]*);client=(.*)$/
|
const SSR_CLIENT_TELEPORT_MARKER = /^uid=([^;]*);client=(.*)$/
|
||||||
function replaceServerOnlyComponentsSlots (ssrContext: NuxtSSRContext, html: string): string {
|
|
||||||
const { teleports, islandContext } = ssrContext
|
function getSlotIslandResponse (ssrContext: NuxtSSRContext): NuxtIslandResponse['slots'] {
|
||||||
if (islandContext || !teleports) { return html }
|
if (!ssrContext.islandContext) { return {} }
|
||||||
for (const key in teleports) {
|
const response: NuxtIslandResponse['slots'] = {}
|
||||||
const match = key.match(SSR_TELEPORT_MARKER)
|
for (const slot in ssrContext.islandContext.slots) {
|
||||||
if (!match) { continue }
|
response[slot] = {
|
||||||
const [, uid, slot] = match
|
...ssrContext.islandContext.slots[slot],
|
||||||
if (!uid || !slot) { continue }
|
fallback: ssrContext.teleports?.[`island-fallback=${slot}`] || ''
|
||||||
html = html.replace(new RegExp(`<div [^>]*nuxt-ssr-component-uid="${uid}"[^>]*>((?!nuxt-ssr-slot-name="${slot}"|nuxt-ssr-component-uid)[\\s\\S])*<div [^>]*nuxt-ssr-slot-name="${slot}"[^>]*>`), (full) => {
|
}
|
||||||
return full + teleports[key]
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return html
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO merge with replaceServerOnlyComponentsSlots once slots are refactored
|
function getClientIslandResponse (ssrContext: NuxtSSRContext): NuxtIslandResponse['components'] {
|
||||||
function replaceClientTeleport (ssrContext: NuxtSSRContext, html: string) {
|
if (!ssrContext.islandContext) { return {} }
|
||||||
|
const response: NuxtIslandResponse['components'] = {}
|
||||||
|
for (const clientUid in ssrContext.islandContext.components) {
|
||||||
|
const html = ssrContext.teleports?.[clientUid] || ''
|
||||||
|
response[clientUid] = {
|
||||||
|
...ssrContext.islandContext.components[clientUid],
|
||||||
|
html,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceIslandTeleports (ssrContext: NuxtSSRContext, html: string) {
|
||||||
const { teleports, islandContext } = ssrContext
|
const { teleports, islandContext } = ssrContext
|
||||||
|
|
||||||
if (islandContext || !teleports) { return html }
|
if (islandContext || !teleports) { return html }
|
||||||
for (const key in teleports) {
|
for (const key in teleports) {
|
||||||
const match = key.match(SSR_CLIENT_TELEPORT_MARKER)
|
const matchClientComp = key.match(SSR_CLIENT_TELEPORT_MARKER)
|
||||||
if (!match) { continue }
|
if (matchClientComp) {
|
||||||
const [, uid, clientId] = match
|
const [, uid, clientId] = matchClientComp
|
||||||
if (!uid || !clientId) { continue }
|
if (!uid || !clientId) { continue }
|
||||||
html = html.replace(new RegExp(`<div [^>]*nuxt-ssr-component-uid="${uid}"[^>]*>((?!nuxt-ssr-client="${clientId}"|nuxt-ssr-component-uid)[\\s\\S])*<div [^>]*nuxt-ssr-client="${clientId}"[^>]*>`), (full) => {
|
html = html.replace(new RegExp(` data-island-client="${clientId}"[^>]*>`), (full) => {
|
||||||
|
return full + teleports[key]
|
||||||
return full + teleports[key]
|
})
|
||||||
})
|
continue
|
||||||
|
}
|
||||||
|
const matchSlot = key.match(SSR_SLOT_TELEPORT_MARKER)
|
||||||
|
if (matchSlot) {
|
||||||
|
const [, uid, slot] = matchSlot
|
||||||
|
if (!uid || !slot) { continue }
|
||||||
|
html = html.replace(new RegExp(` data-island-uid="${uid}" data-island-slot="${slot}"[^>]*>`), (full) => {
|
||||||
|
return full + teleports[key]
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return html
|
return html
|
||||||
}
|
}
|
||||||
|
@ -63,15 +63,19 @@ describe('islandTransform - server and island components', () => {
|
|||||||
expect(normalizeLineEndings(result)).toMatchInlineSnapshot(`
|
expect(normalizeLineEndings(result)).toMatchInlineSnapshot(`
|
||||||
"<template>
|
"<template>
|
||||||
<div>
|
<div>
|
||||||
<div style="display: contents;" nuxt-ssr-slot-name="default" />
|
<NuxtTeleportSsrSlot name="default" :props="undefined"><slot /></NuxtTeleportSsrSlot>
|
||||||
|
|
||||||
<div style="display: contents;" nuxt-ssr-slot-name="named" :nuxt-ssr-slot-data="JSON.stringify([{ some-data: someData }])"/>
|
<NuxtTeleportSsrSlot name="named" :props="[{ some-data: someData }]"><slot name="named" :some-data="someData" /></NuxtTeleportSsrSlot>
|
||||||
<div style="display: contents;" nuxt-ssr-slot-name="other" :nuxt-ssr-slot-data="JSON.stringify([{ some-data: someData }])"/>
|
<NuxtTeleportSsrSlot name="other" :props="[{ some-data: someData }]"><slot
|
||||||
|
name="other"
|
||||||
|
:some-data="someData"
|
||||||
|
/></NuxtTeleportSsrSlot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { vforToArray as __vforToArray } from '#app/components/utils'
|
import { vforToArray as __vforToArray } from '#app/components/utils'
|
||||||
import NuxtTeleportSsrClient from '#app/components/nuxt-teleport-ssr-client'
|
import NuxtTeleportIslandClient from '#app/components/nuxt-teleport-island-client'
|
||||||
|
import NuxtTeleportSsrSlot from '#app/components/nuxt-teleport-island-slot'
|
||||||
const someData = 'some data'
|
const someData = 'some data'
|
||||||
|
|
||||||
</script>"
|
</script>"
|
||||||
@ -95,14 +99,15 @@ describe('islandTransform - server and island components', () => {
|
|||||||
expect(normalizeLineEndings(result)).toMatchInlineSnapshot(`
|
expect(normalizeLineEndings(result)).toMatchInlineSnapshot(`
|
||||||
"<template>
|
"<template>
|
||||||
<div>
|
<div>
|
||||||
<div style="display: contents;" nuxt-ssr-slot-name="default" :nuxt-ssr-slot-data="JSON.stringify([{ some-data: someData }])"><div nuxt-slot-fallback-start="default"/><div style="display: contents;">
|
<NuxtTeleportSsrSlot name="default" :props="[{ some-data: someData }]"><slot :some-data="someData" /><template #fallback>
|
||||||
<div>fallback</div>
|
<div>fallback</div>
|
||||||
</div><div nuxt-slot-fallback-end/></div>
|
</template></NuxtTeleportSsrSlot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { vforToArray as __vforToArray } from '#app/components/utils'
|
import { vforToArray as __vforToArray } from '#app/components/utils'
|
||||||
import NuxtTeleportSsrClient from '#app/components/nuxt-teleport-ssr-client'
|
import NuxtTeleportIslandClient from '#app/components/nuxt-teleport-island-client'
|
||||||
|
import NuxtTeleportSsrSlot from '#app/components/nuxt-teleport-island-slot'
|
||||||
const someData = 'some data'
|
const someData = 'some data'
|
||||||
|
|
||||||
</script>"
|
</script>"
|
||||||
@ -153,10 +158,10 @@ describe('islandTransform - server and island components', () => {
|
|||||||
<p>message: {{ message }}</p>
|
<p>message: {{ message }}</p>
|
||||||
<p>Below is the slot I want to be hydrated on the client</p>
|
<p>Below is the slot I want to be hydrated on the client</p>
|
||||||
<div>
|
<div>
|
||||||
<div style="display: contents;" nuxt-ssr-slot-name="default" ><div nuxt-slot-fallback-start="default"/>
|
<NuxtTeleportSsrSlot name="default" :props="undefined"><slot /><template #fallback>
|
||||||
This is the default content of the slot, I should not see this after
|
This is the default content of the slot, I should not see this after
|
||||||
the client loading has completed.
|
the client loading has completed.
|
||||||
<div nuxt-slot-fallback-end/></div>
|
</template></NuxtTeleportSsrSlot>
|
||||||
</div>
|
</div>
|
||||||
<p>Above is the slot I want to be hydrated on the client</p>
|
<p>Above is the slot I want to be hydrated on the client</p>
|
||||||
</template>
|
</template>
|
||||||
@ -166,7 +171,8 @@ describe('islandTransform - server and island components', () => {
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { vforToArray as __vforToArray } from '#app/components/utils'
|
import { vforToArray as __vforToArray } from '#app/components/utils'
|
||||||
import NuxtTeleportSsrClient from '#app/components/nuxt-teleport-ssr-client'
|
import NuxtTeleportIslandClient from '#app/components/nuxt-teleport-island-client'
|
||||||
|
import NuxtTeleportSsrSlot from '#app/components/nuxt-teleport-island-slot'
|
||||||
export interface Props {
|
export interface Props {
|
||||||
count?: number;
|
count?: number;
|
||||||
}
|
}
|
||||||
@ -184,9 +190,9 @@ describe('islandTransform - server and island components', () => {
|
|||||||
it('test transform with vite in dev', async () => {
|
it('test transform with vite in dev', async () => {
|
||||||
const result = await viteTransform(`<template>
|
const result = await viteTransform(`<template>
|
||||||
<div>
|
<div>
|
||||||
<!-- should not be wrapped by NuxtTeleportSsrClient -->
|
<!-- should not be wrapped by NuxtTeleportIslandClient -->
|
||||||
<HelloWorld />
|
<HelloWorld />
|
||||||
<!-- should be wrapped by NuxtTeleportSsrClient with a rootDir attr -->
|
<!-- should be wrapped by NuxtTeleportIslandClient with a rootDir attr -->
|
||||||
<HelloWorld nuxt-client />
|
<HelloWorld nuxt-client />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -199,16 +205,17 @@ describe('islandTransform - server and island components', () => {
|
|||||||
expect(normalizeLineEndings(result)).toMatchInlineSnapshot(`
|
expect(normalizeLineEndings(result)).toMatchInlineSnapshot(`
|
||||||
"<template>
|
"<template>
|
||||||
<div>
|
<div>
|
||||||
<!-- should not be wrapped by NuxtTeleportSsrClient -->
|
<!-- should not be wrapped by NuxtTeleportIslandClient -->
|
||||||
<HelloWorld />
|
<HelloWorld />
|
||||||
<!-- should be wrapped by NuxtTeleportSsrClient with a rootDir attr -->
|
<!-- should be wrapped by NuxtTeleportIslandClient with a rootDir attr -->
|
||||||
<NuxtTeleportSsrClient to="HelloWorld-PIVollAJCe" root-dir="/root" :nuxt-client="true"><HelloWorld /></NuxtTeleportSsrClient>
|
<NuxtTeleportIslandClient to="HelloWorld-u9V1gHwUZN" root-dir="/root" :nuxt-client="true"><HelloWorld /></NuxtTeleportIslandClient>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { vforToArray as __vforToArray } from '#app/components/utils'
|
import { vforToArray as __vforToArray } from '#app/components/utils'
|
||||||
import NuxtTeleportSsrClient from '#app/components/nuxt-teleport-ssr-client'
|
import NuxtTeleportIslandClient from '#app/components/nuxt-teleport-island-client'
|
||||||
|
import NuxtTeleportSsrSlot from '#app/components/nuxt-teleport-island-slot'
|
||||||
import HelloWorld from './HelloWorld.vue'
|
import HelloWorld from './HelloWorld.vue'
|
||||||
</script>
|
</script>
|
||||||
"
|
"
|
||||||
@ -234,13 +241,14 @@ describe('islandTransform - server and island components', () => {
|
|||||||
"<template>
|
"<template>
|
||||||
<div>
|
<div>
|
||||||
<HelloWorld />
|
<HelloWorld />
|
||||||
<NuxtTeleportSsrClient to="HelloWorld-CyH3UXLuYA" :nuxt-client="true"><HelloWorld /></NuxtTeleportSsrClient>
|
<NuxtTeleportIslandClient to="HelloWorld-CyH3UXLuYA" :nuxt-client="true"><HelloWorld /></NuxtTeleportIslandClient>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { vforToArray as __vforToArray } from '#app/components/utils'
|
import { vforToArray as __vforToArray } from '#app/components/utils'
|
||||||
import NuxtTeleportSsrClient from '#app/components/nuxt-teleport-ssr-client'
|
import NuxtTeleportIslandClient from '#app/components/nuxt-teleport-island-client'
|
||||||
|
import NuxtTeleportSsrSlot from '#app/components/nuxt-teleport-island-slot'
|
||||||
import HelloWorld from './HelloWorld.vue'
|
import HelloWorld from './HelloWorld.vue'
|
||||||
</script>
|
</script>
|
||||||
"
|
"
|
||||||
@ -269,13 +277,14 @@ describe('islandTransform - server and island components', () => {
|
|||||||
"<template>
|
"<template>
|
||||||
<div>
|
<div>
|
||||||
<HelloWorld />
|
<HelloWorld />
|
||||||
<NuxtTeleportSsrClient to="HelloWorld-eo0XycWCUV" :nuxt-client="nuxtClient"><HelloWorld :nuxt-client="nuxtClient" /></NuxtTeleportSsrClient>
|
<NuxtTeleportIslandClient to="HelloWorld-eo0XycWCUV" :nuxt-client="nuxtClient"><HelloWorld :nuxt-client="nuxtClient" /></NuxtTeleportIslandClient>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { vforToArray as __vforToArray } from '#app/components/utils'
|
import { vforToArray as __vforToArray } from '#app/components/utils'
|
||||||
import NuxtTeleportSsrClient from '#app/components/nuxt-teleport-ssr-client'
|
import NuxtTeleportIslandClient from '#app/components/nuxt-teleport-island-client'
|
||||||
|
import NuxtTeleportSsrSlot from '#app/components/nuxt-teleport-island-slot'
|
||||||
import HelloWorld from './HelloWorld.vue'
|
import HelloWorld from './HelloWorld.vue'
|
||||||
|
|
||||||
const nuxtClient = false
|
const nuxtClient = false
|
||||||
@ -309,7 +318,8 @@ describe('islandTransform - server and island components', () => {
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { vforToArray as __vforToArray } from '#app/components/utils'
|
import { vforToArray as __vforToArray } from '#app/components/utils'
|
||||||
import NuxtTeleportSsrClient from '#app/components/nuxt-teleport-ssr-client'
|
import NuxtTeleportIslandClient from '#app/components/nuxt-teleport-island-client'
|
||||||
|
import NuxtTeleportSsrSlot from '#app/components/nuxt-teleport-island-slot'
|
||||||
import HelloWorld from './HelloWorld.vue'
|
import HelloWorld from './HelloWorld.vue'
|
||||||
|
|
||||||
const nuxtClient = false
|
const nuxtClient = false
|
||||||
@ -331,16 +341,17 @@ describe('islandTransform - server and island components', () => {
|
|||||||
expect(result).toMatchInlineSnapshot(`
|
expect(result).toMatchInlineSnapshot(`
|
||||||
"<script setup>
|
"<script setup>
|
||||||
import { vforToArray as __vforToArray } from '#app/components/utils'
|
import { vforToArray as __vforToArray } from '#app/components/utils'
|
||||||
import NuxtTeleportSsrClient from '#app/components/nuxt-teleport-ssr-client'</script><template>
|
import NuxtTeleportIslandClient from '#app/components/nuxt-teleport-island-client'
|
||||||
|
import NuxtTeleportSsrSlot from '#app/components/nuxt-teleport-island-slot'</script><template>
|
||||||
<div>
|
<div>
|
||||||
<HelloWorld />
|
<HelloWorld />
|
||||||
<NuxtTeleportSsrClient to="HelloWorld-CyH3UXLuYA" :nuxt-client="true"><HelloWorld /></NuxtTeleportSsrClient>
|
<NuxtTeleportIslandClient to="HelloWorld-CyH3UXLuYA" :nuxt-client="true"><HelloWorld /></NuxtTeleportIslandClient>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
"
|
"
|
||||||
`)
|
`)
|
||||||
expect(result).toContain(`import NuxtTeleportSsrClient from '#app/components/nuxt-teleport-ssr-client'`)
|
expect(result).toContain(`import NuxtTeleportIslandClient from '#app/components/nuxt-teleport-island-client'`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -349,10 +360,10 @@ describe('islandTransform - server and island components', () => {
|
|||||||
const spyOnWarn = vi.spyOn(console, 'warn')
|
const spyOnWarn = vi.spyOn(console, 'warn')
|
||||||
const result = await webpackTransform(`<template>
|
const result = await webpackTransform(`<template>
|
||||||
<div>
|
<div>
|
||||||
<!-- should not be wrapped by NuxtTeleportSsrClient -->
|
<!-- should not be wrapped by NuxtTeleportIslandClient -->
|
||||||
<HelloWorld />
|
<HelloWorld />
|
||||||
|
|
||||||
<!-- should be not wrapped by NuxtTeleportSsrClient for now -->
|
<!-- should be not wrapped by NuxtTeleportIslandClient for now -->
|
||||||
<HelloWorld nuxt-client />
|
<HelloWorld nuxt-client />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -366,17 +377,18 @@ describe('islandTransform - server and island components', () => {
|
|||||||
expect(normalizeLineEndings(result)).toMatchInlineSnapshot(`
|
expect(normalizeLineEndings(result)).toMatchInlineSnapshot(`
|
||||||
"<template>
|
"<template>
|
||||||
<div>
|
<div>
|
||||||
<!-- should not be wrapped by NuxtTeleportSsrClient -->
|
<!-- should not be wrapped by NuxtTeleportIslandClient -->
|
||||||
<HelloWorld />
|
<HelloWorld />
|
||||||
|
|
||||||
<!-- should be not wrapped by NuxtTeleportSsrClient for now -->
|
<!-- should be not wrapped by NuxtTeleportIslandClient for now -->
|
||||||
<HelloWorld nuxt-client />
|
<HelloWorld nuxt-client />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { vforToArray as __vforToArray } from '#app/components/utils'
|
import { vforToArray as __vforToArray } from '#app/components/utils'
|
||||||
import NuxtTeleportSsrClient from '#app/components/nuxt-teleport-ssr-client'
|
import NuxtTeleportIslandClient from '#app/components/nuxt-teleport-island-client'
|
||||||
|
import NuxtTeleportSsrSlot from '#app/components/nuxt-teleport-island-slot'
|
||||||
import HelloWorld from './HelloWorld.vue'
|
import HelloWorld from './HelloWorld.vue'
|
||||||
|
|
||||||
const someData = 'some data'
|
const someData = 'some data'
|
||||||
|
@ -113,7 +113,7 @@ describe('pages', () => {
|
|||||||
// should apply attributes to client-only components
|
// should apply attributes to client-only components
|
||||||
expect(html).toContain('<div style="color:red;" class="client-only"></div>')
|
expect(html).toContain('<div style="color:red;" class="client-only"></div>')
|
||||||
// should render server-only components
|
// should render server-only components
|
||||||
expect(html.replace(/ nuxt-ssr-component-uid="[^"]*"/, '')).toContain('<div class="server-only" style="background-color:gray;"> server-only component </div>')
|
expect(html.replace(/ data-island-uid="[^"]*"/, '')).toContain('<div class="server-only" style="background-color:gray;"> server-only component </div>')
|
||||||
// should register global components automatically
|
// should register global components automatically
|
||||||
expect(html).toContain('global component registered automatically')
|
expect(html).toContain('global component registered automatically')
|
||||||
expect(html).toContain('global component via suffix')
|
expect(html).toContain('global component via suffix')
|
||||||
@ -1558,12 +1558,12 @@ describe('server components/islands', () => {
|
|||||||
await page.waitForLoadState('networkidle')
|
await page.waitForLoadState('networkidle')
|
||||||
await page.getByText('Go to page without lazy server component').click()
|
await page.getByText('Go to page without lazy server component').click()
|
||||||
|
|
||||||
const text = (await page.innerText('pre')).replace(/nuxt-ssr-client="([^"]*)"/g, (_, content) => `nuxt-ssr-client="${content.split('-')[0]}"`)
|
const text = (await page.innerText('pre')).replaceAll(/ data-island-uid="([^"]*)"/g, '').replace(/data-island-client="([^"]*)"/g, (_, content) => `data-island-client="${content.split('-')[0]}"`)
|
||||||
|
|
||||||
if (isWebpack) {
|
if (isWebpack) {
|
||||||
expect(text).toMatchInlineSnapshot('" End page <pre></pre><section id="fallback"><div nuxt-ssr-component-uid="4"> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div style="display:contents;" nuxt-ssr-slot-name="default"></div></div></section><section id="no-fallback"><div nuxt-ssr-component-uid="5"> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div style="display:contents;" nuxt-ssr-slot-name="default"></div></div></section><div nuxt-ssr-component-uid="3"> ServerWithClient.server.vue : <p>count: 0</p> This component should not be preloaded <div><!--[--><div>a</div><div>b</div><div>c</div><!--]--></div> This is not interactive <div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div class="interactive-component-wrapper" style="border:solid 1px red;"> The component bellow is not a slot but declared as interactive <div class="sugar-counter" nuxt-client=""> Sugar Counter 12 x 1 = 12 <button> Inc </button></div></div></div>"')
|
expect(text).toMatchInlineSnapshot('" End page <pre></pre><section id="fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"></div><!--]--></div></section><section id="no-fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"></div><!--]--></div></section><div> ServerWithClient.server.vue : <p>count: 0</p> This component should not be preloaded <div><!--[--><div>a</div><div>b</div><div>c</div><!--]--></div> This is not interactive <div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div class="interactive-component-wrapper" style="border:solid 1px red;"> The component bellow is not a slot but declared as interactive <div class="sugar-counter" nuxt-client=""> Sugar Counter 12 x 1 = 12 <button> Inc </button></div></div></div>"')
|
||||||
} else {
|
} else {
|
||||||
expect(text).toMatchInlineSnapshot('" End page <pre></pre><section id="fallback"><div nuxt-ssr-component-uid="4"> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div style="display:contents;" nuxt-ssr-slot-name="default"></div></div></section><section id="no-fallback"><div nuxt-ssr-component-uid="5"> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div style="display:contents;" nuxt-ssr-slot-name="default"></div></div></section><div nuxt-ssr-component-uid="3"> ServerWithClient.server.vue : <p>count: 0</p> This component should not be preloaded <div><!--[--><div>a</div><div>b</div><div>c</div><!--]--></div> This is not interactive <div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div class="interactive-component-wrapper" style="border:solid 1px red;"> The component bellow is not a slot but declared as interactive <!--[--><div style="display: contents;" nuxt-ssr-client="Counter"></div><!--teleport start--><!--teleport end--><!--]--></div></div>"')
|
expect(text).toMatchInlineSnapshot(`" End page <pre></pre><section id="fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"></div><!--]--></div></section><section id="no-fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"></div><!--]--></div></section><div> ServerWithClient.server.vue : <p>count: 0</p> This component should not be preloaded <div><!--[--><div>a</div><div>b</div><div>c</div><!--]--></div> This is not interactive <div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div class="interactive-component-wrapper" style="border:solid 1px red;"> The component bellow is not a slot but declared as interactive <!--[--><div style="display: contents;" data-island-client="Counter"></div><!--teleport start--><!--teleport end--><!--]--></div></div>"`)
|
||||||
}
|
}
|
||||||
expect(text).toContain('async component that was very long')
|
expect(text).toContain('async component that was very long')
|
||||||
|
|
||||||
@ -1775,22 +1775,22 @@ describe('component islands', () => {
|
|||||||
it('renders components with route', async () => {
|
it('renders components with route', async () => {
|
||||||
const result: NuxtIslandResponse = await $fetch('/__nuxt_island/RouteComponent.json?url=/foo')
|
const result: NuxtIslandResponse = await $fetch('/__nuxt_island/RouteComponent.json?url=/foo')
|
||||||
|
|
||||||
|
result.html = result.html.replace(/ data-island-uid="[^"]*"/g, '')
|
||||||
if (isDev()) {
|
if (isDev()) {
|
||||||
result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates') && (l.href.startsWith('_nuxt/components/islands/') && l.href.includes('_nuxt/components/islands/RouteComponent')))
|
result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates') && (l.href.startsWith('_nuxt/components/islands/') && l.href.includes('_nuxt/components/islands/RouteComponent')))
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(result).toMatchInlineSnapshot(`
|
expect(result).toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
"chunks": {},
|
"components": {},
|
||||||
"head": {
|
"head": {
|
||||||
"link": [],
|
"link": [],
|
||||||
"style": [],
|
"style": [],
|
||||||
},
|
},
|
||||||
"html": "<pre nuxt-ssr-component-uid> Route: /foo
|
"html": "<pre data-island-uid> Route: /foo
|
||||||
</pre>",
|
</pre>",
|
||||||
"props": {},
|
"slots": {},
|
||||||
"state": {},
|
"state": {},
|
||||||
"teleports": {},
|
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
@ -1804,19 +1804,57 @@ describe('component islands', () => {
|
|||||||
if (isDev()) {
|
if (isDev()) {
|
||||||
result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates') && (l.href.startsWith('_nuxt/components/islands/') && l.href.includes('_nuxt/components/islands/LongAsyncComponent')))
|
result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates') && (l.href.startsWith('_nuxt/components/islands/') && l.href.includes('_nuxt/components/islands/LongAsyncComponent')))
|
||||||
}
|
}
|
||||||
|
result.html = result.html.replaceAll(/ (data-island-uid|data-island-client)="([^"]*)"/g, '')
|
||||||
expect(result).toMatchInlineSnapshot(`
|
expect(result).toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
"chunks": {},
|
"components": {},
|
||||||
"head": {
|
"head": {
|
||||||
"link": [],
|
"link": [],
|
||||||
"style": [],
|
"style": [],
|
||||||
},
|
},
|
||||||
"html": "<div nuxt-ssr-component-uid><div> count is above 2 </div><div style="display:contents;" nuxt-ssr-slot-name="default"></div> that was very long ... <div id="long-async-component-count">3</div> <div style="display:contents;" nuxt-ssr-slot-name="test" nuxt-ssr-slot-data="[{"count":3}]"></div><p>hello world !!!</p><div style="display:contents;" nuxt-ssr-slot-name="hello" nuxt-ssr-slot-data="[{"t":0},{"t":1},{"t":2}]"><div nuxt-slot-fallback-start="hello"></div><!--[--><div style="display:contents;"><div> fallback slot -- index: 0</div></div><div style="display:contents;"><div> fallback slot -- index: 1</div></div><div style="display:contents;"><div> fallback slot -- index: 2</div></div><!--]--><div nuxt-slot-fallback-end></div></div><div style="display:contents;" nuxt-ssr-slot-name="fallback" nuxt-ssr-slot-data="[{"t":"fall"},{"t":"back"}]"><div nuxt-slot-fallback-start="fallback"></div><!--[--><div style="display:contents;"><div>fall slot -- index: 0</div><div class="fallback-slot-content"> wonderful fallback </div></div><div style="display:contents;"><div>back slot -- index: 1</div><div class="fallback-slot-content"> wonderful fallback </div></div><!--]--><div nuxt-slot-fallback-end></div></div></div>",
|
"html": "<div data-island-uid><div> count is above 2 </div><!--[--><div style="display: contents;" data-island-uid data-island-slot="default"></div><!--]--> that was very long ... <div id="long-async-component-count">3</div> <!--[--><div style="display: contents;" data-island-uid data-island-slot="test"></div><!--]--><p>hello world !!!</p><!--[--><div style="display: contents;" data-island-uid data-island-slot="hello"></div><!--teleport start--><!--teleport end--><!--]--><!--[--><div style="display: contents;" data-island-uid data-island-slot="fallback"></div><!--teleport start--><!--teleport end--><!--]--></div>",
|
||||||
"props": {},
|
"slots": {
|
||||||
"state": {},
|
"default": {
|
||||||
"teleports": {},
|
"fallback": "",
|
||||||
}
|
"props": [],
|
||||||
`)
|
},
|
||||||
|
"fallback": {
|
||||||
|
"fallback": "<!--[--><div style="display:contents;"><div>fall slot -- index: 0</div><div class="fallback-slot-content"> wonderful fallback </div></div><div style="display:contents;"><div>back slot -- index: 1</div><div class="fallback-slot-content"> wonderful fallback </div></div><!--]--><!--teleport anchor-->",
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"t": "fall",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": "back",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"hello": {
|
||||||
|
"fallback": "<!--[--><div style="display:contents;"><div> fallback slot -- index: 0</div></div><div style="display:contents;"><div> fallback slot -- index: 1</div></div><div style="display:contents;"><div> fallback slot -- index: 2</div></div><!--]--><!--teleport anchor-->",
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"t": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"fallback": "",
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"count": 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"state": {},
|
||||||
|
}
|
||||||
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('render .server async component', async () => {
|
it('render .server async component', async () => {
|
||||||
@ -1829,23 +1867,23 @@ describe('component islands', () => {
|
|||||||
result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates') && (l.href.startsWith('_nuxt/components/islands/') && l.href.includes('_nuxt/components/islands/AsyncServerComponent')))
|
result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates') && (l.href.startsWith('_nuxt/components/islands/') && l.href.includes('_nuxt/components/islands/AsyncServerComponent')))
|
||||||
}
|
}
|
||||||
result.props = {}
|
result.props = {}
|
||||||
result.teleports = {}
|
result.components = {}
|
||||||
result.chunks = {}
|
result.slots = {}
|
||||||
result.html = result.html.replace(/ nuxt-ssr-client="([^"]*)"/g, (_, content) => `'nuxt-ssr-client="${content.split('-')[0]}"`)
|
result.html = result.html.replaceAll(/ (data-island-uid|data-island-client)="([^"]*)"/g, '')
|
||||||
|
|
||||||
expect(result).toMatchInlineSnapshot(`
|
expect(result).toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
"chunks": {},
|
"components": {},
|
||||||
"head": {
|
"head": {
|
||||||
"link": [],
|
"link": [],
|
||||||
"style": [],
|
"style": [],
|
||||||
},
|
},
|
||||||
"html": "<div nuxt-ssr-component-uid> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">2</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div style="display:contents;" nuxt-ssr-slot-name="default"></div></div>",
|
"html": "<div data-island-uid> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">2</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-uid data-island-slot="default"></div><!--]--></div>",
|
||||||
"props": {},
|
"props": {},
|
||||||
"state": {},
|
"slots": {},
|
||||||
"teleports": {},
|
"state": {},
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!isWebpack) {
|
if (!isWebpack) {
|
||||||
@ -1854,40 +1892,33 @@ describe('component islands', () => {
|
|||||||
if (isDev()) {
|
if (isDev()) {
|
||||||
result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates') && (l.href.startsWith('_nuxt/components/islands/') && l.href.includes('_nuxt/components/islands/AsyncServerComponent')))
|
result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates') && (l.href.startsWith('_nuxt/components/islands/') && l.href.includes('_nuxt/components/islands/AsyncServerComponent')))
|
||||||
}
|
}
|
||||||
const { props, teleports, chunks } = result
|
const { components } = result
|
||||||
result.props = {}
|
result.components = {}
|
||||||
result.teleports = {}
|
result.slots = {}
|
||||||
result.chunks = {}
|
result.html = result.html.replace(/ data-island-client="([^"]*)"/g, (_, content) => ` data-island-client="${content.split('-')[0]}"`)
|
||||||
result.html = result.html.replace(/ nuxt-ssr-client="([^"]*)"/g, (_, content) => `'nuxt-ssr-client="${content.split('-')[0]}"`)
|
|
||||||
|
|
||||||
const propsEntries = Object.entries(props || {})
|
const teleportsEntries = Object.entries(components || {})
|
||||||
const teleportsEntries = Object.entries(teleports || {})
|
|
||||||
const chunksEntries = Object.entries(chunks || {})
|
|
||||||
|
|
||||||
expect(result).toMatchInlineSnapshot(`
|
expect(result).toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
"chunks": {},
|
"components": {},
|
||||||
"head": {
|
"head": {
|
||||||
"link": [],
|
"link": [],
|
||||||
"style": [],
|
"style": [],
|
||||||
},
|
},
|
||||||
"html": "<div nuxt-ssr-component-uid> ServerWithClient.server.vue : <p>count: 0</p> This component should not be preloaded <div><!--[--><div>a</div><div>b</div><div>c</div><!--]--></div> This is not interactive <div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div class="interactive-component-wrapper" style="border:solid 1px red;"> The component bellow is not a slot but declared as interactive <!--[--><div style="display: contents;"'nuxt-ssr-client="Counter"></div><!--teleport start--><!--teleport end--><!--]--></div></div>",
|
"html": "<div data-island-uid> ServerWithClient.server.vue : <p>count: 0</p> This component should not be preloaded <div><!--[--><div>a</div><div>b</div><div>c</div><!--]--></div> This is not interactive <div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div class="interactive-component-wrapper" style="border:solid 1px red;"> The component bellow is not a slot but declared as interactive <!--[--><div style="display: contents;" data-island-uid data-island-client="Counter"></div><!--teleport start--><!--teleport end--><!--]--></div></div>",
|
||||||
"props": {},
|
"slots": {},
|
||||||
"state": {},
|
"state": {},
|
||||||
"teleports": {},
|
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
expect(propsEntries).toHaveLength(1)
|
|
||||||
expect(teleportsEntries).toHaveLength(1)
|
expect(teleportsEntries).toHaveLength(1)
|
||||||
expect(propsEntries[0][0].startsWith('Counter-')).toBeTruthy()
|
|
||||||
expect(teleportsEntries[0][0].startsWith('Counter-')).toBeTruthy()
|
expect(teleportsEntries[0][0].startsWith('Counter-')).toBeTruthy()
|
||||||
expect(chunksEntries[0][0]).toBe('Counter')
|
expect(teleportsEntries[0][1].props).toMatchInlineSnapshot(`
|
||||||
expect(propsEntries[0][1]).toMatchInlineSnapshot(`
|
|
||||||
{
|
{
|
||||||
"multiplier": 1,
|
"multiplier": 1,
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
expect(teleportsEntries[0][1]).toMatchInlineSnapshot('"<div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--teleport anchor-->"')
|
expect(teleportsEntries[0][1].html).toMatchInlineSnapshot('"<div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--teleport anchor-->"')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1900,7 +1931,7 @@ describe('component islands', () => {
|
|||||||
obj: { foo: 42, bar: false, me: 'hi' }
|
obj: { foo: 42, bar: false, me: 'hi' }
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
result.html = result.html.replace(/ nuxt-ssr-component-uid="([^"]*)"/g, '')
|
result.html = result.html.replace(/ data-island-uid="([^"]*)"/g, '')
|
||||||
|
|
||||||
if (isDev()) {
|
if (isDev()) {
|
||||||
result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates'))
|
result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates'))
|
||||||
@ -1939,8 +1970,8 @@ describe('component islands', () => {
|
|||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(result.html.replace(/data-v-\w+|"|<!--.*-->/g, '')).toMatchInlineSnapshot(`
|
expect(result.html.replace(/data-v-\w+|"|<!--.*-->/g, '').replace(/data-island-uid="[^"]"/g, '')).toMatchInlineSnapshot(`
|
||||||
"<div nuxt-ssr-component-uid > Was router enabled: true <br > Props: <pre >{
|
"<div data-island-uid > Was router enabled: true <br > Props: <pre >{
|
||||||
number: 3487,
|
number: 3487,
|
||||||
str: something,
|
str: something,
|
||||||
obj: {
|
obj: {
|
||||||
|
Loading…
Reference in New Issue
Block a user