mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-22 05:35:13 +00:00
feat(nuxt): allow using nuxt-client
in all components (#25479)
This commit is contained in:
parent
a80bdd1e59
commit
6d93014c52
@ -318,7 +318,7 @@ You can partially hydrate a component by setting a `nuxt-client` attribute on th
|
|||||||
```
|
```
|
||||||
|
|
||||||
::alert{type=info}
|
::alert{type=info}
|
||||||
This only works within a server component. Slots for client components are not available yet.
|
This only works within a server component. Slots for client components are working only with `experimental.componentIsland.selectiveClient` set to `'deep'` and since they are rendered server-side, they are not interactive once client-side.
|
||||||
::
|
::
|
||||||
|
|
||||||
#### Server Component Context
|
#### Server Component Context
|
||||||
|
@ -260,20 +260,24 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
if (import.meta.server) {
|
if (import.meta.server) {
|
||||||
for (const [id, info] of Object.entries(payloads.components ?? {})) {
|
for (const [id, info] of Object.entries(payloads.components ?? {})) {
|
||||||
const { html } = info
|
const { html, slots } = info
|
||||||
|
let replaced = html.replaceAll('data-island-uid', `data-island-uid="${uid.value}"`)
|
||||||
|
for (const slot in slots) {
|
||||||
|
replaced = replaced.replaceAll(`data-island-slot="${slot}">`, (full) => full + slots[slot])
|
||||||
|
}
|
||||||
teleports.push(createVNode(Teleport, { to: `uid=${uid.value};client=${id}` }, {
|
teleports.push(createVNode(Teleport, { to: `uid=${uid.value};client=${id}` }, {
|
||||||
default: () => [createStaticVNode(html, 1)]
|
default: () => [createStaticVNode(replaced, 1)]
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
} else if (selectiveClient && import.meta.client && canLoadClientComponent.value) {
|
||||||
if (selectiveClient && import.meta.client && canLoadClientComponent.value) {
|
|
||||||
for (const [id, info] of Object.entries(payloads.components ?? {})) {
|
for (const [id, info] of Object.entries(payloads.components ?? {})) {
|
||||||
const { props } = info
|
const { props, slots } = info
|
||||||
const component = components!.get(id)!
|
const component = components!.get(id)!
|
||||||
// use different selectors for even and odd teleportKey to force trigger the teleport
|
// 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-component="${id}"]` }, {
|
const vnode = createVNode(Teleport, { to: `${isKeyOdd ? 'div' : ''}[data-island-uid='${uid.value}'][data-island-component="${id}"]` }, {
|
||||||
default: () => {
|
default: () => {
|
||||||
return [h(component, props)]
|
return [h(component, props, Object.fromEntries(Object.entries(slots || {}).map(([k, v]) => ([k, () => createStaticVNode(`<div style="display: contents" data-island-uid data-island-slot="${k}">${v}</div>`, 1)
|
||||||
|
]))))]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
teleports.push(vnode)
|
teleports.push(vnode)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { Component } from 'vue'
|
import type { Component, InjectionKey } from 'vue'
|
||||||
import { Teleport, defineComponent, h } from 'vue'
|
import { Teleport, defineComponent, h, inject, provide } from 'vue'
|
||||||
import { useNuxtApp } from '../nuxt'
|
import { useNuxtApp } from '../nuxt'
|
||||||
// @ts-expect-error virtual file
|
// @ts-expect-error virtual file
|
||||||
import { paths } from '#build/components-chunk'
|
import { paths } from '#build/components-chunk'
|
||||||
@ -9,6 +9,8 @@ type ExtendedComponent = Component & {
|
|||||||
__name: string
|
__name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const NuxtTeleportIslandSymbol = Symbol('NuxtTeleportIslandComponent') as InjectionKey<false | string>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* component only used with componentsIsland
|
* component only used with componentsIsland
|
||||||
* 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
|
||||||
@ -37,8 +39,10 @@ export default defineComponent({
|
|||||||
setup (props, { slots }) {
|
setup (props, { slots }) {
|
||||||
const nuxtApp = useNuxtApp()
|
const nuxtApp = useNuxtApp()
|
||||||
|
|
||||||
if (!nuxtApp.ssrContext?.islandContext || !props.nuxtClient) { return () => slots.default!() }
|
// if there's already a teleport parent, we don't need to teleport or to render the wrapped component client side
|
||||||
|
if (!nuxtApp.ssrContext?.islandContext || !props.nuxtClient || inject(NuxtTeleportIslandSymbol, false)) { return () => slots.default?.() }
|
||||||
|
|
||||||
|
provide(NuxtTeleportIslandSymbol, props.to)
|
||||||
const islandContext = nuxtApp.ssrContext!.islandContext!
|
const islandContext = nuxtApp.ssrContext!.islandContext!
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { Teleport, defineComponent, h } from 'vue'
|
import type { VNode } from 'vue'
|
||||||
|
import { Teleport, createVNode, defineComponent, h, inject } from 'vue'
|
||||||
import { useNuxtApp } from '../nuxt'
|
import { useNuxtApp } from '../nuxt'
|
||||||
|
import { NuxtTeleportIslandSymbol } from './nuxt-teleport-island-component'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* component only used within islands for slot teleport
|
* component only used within islands for slot teleport
|
||||||
*/
|
*/
|
||||||
@ -9,37 +11,50 @@ export default defineComponent({
|
|||||||
name: 'NuxtTeleportIslandSlot',
|
name: 'NuxtTeleportIslandSlot',
|
||||||
props: {
|
props: {
|
||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* must be an array to handle v-for
|
* must be an array to handle v-for
|
||||||
*/
|
*/
|
||||||
props: {
|
props: {
|
||||||
type: Object as () => Array<any>
|
type: Object as () => Array<any>
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setup (props, { slots }) {
|
setup (props, { slots }) {
|
||||||
const nuxtApp = useNuxtApp()
|
const nuxtApp = useNuxtApp()
|
||||||
const islandContext = nuxtApp.ssrContext?.islandContext
|
const islandContext = nuxtApp.ssrContext?.islandContext
|
||||||
|
if (!islandContext) {
|
||||||
if(!islandContext) {
|
return () => slots.default?.()[0]
|
||||||
return () => slots.default?.()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const componentName = inject(NuxtTeleportIslandSymbol, false)
|
||||||
islandContext.slots[props.name] = {
|
islandContext.slots[props.name] = {
|
||||||
props: (props.props || []) as unknown[]
|
props: (props.props || []) as unknown[]
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
const vnodes = [h('div', {
|
const vnodes: VNode[] = []
|
||||||
style: 'display: contents;',
|
|
||||||
'data-island-uid': '',
|
if (nuxtApp.ssrContext?.islandContext && slots.default) {
|
||||||
'data-island-slot': props.name,
|
vnodes.push(h('div', {
|
||||||
})]
|
style: 'display: contents;',
|
||||||
|
'data-island-uid': '',
|
||||||
|
'data-island-slot': props.name,
|
||||||
|
}, {
|
||||||
|
// Teleport in slot to not be hydrated client-side with the staticVNode
|
||||||
|
default: () => [createVNode(Teleport, { to: `island-slot=${componentName};${props.name}` }, slots.default?.())]
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
vnodes.push(h('div', {
|
||||||
|
style: 'display: contents;',
|
||||||
|
'data-island-uid': '',
|
||||||
|
'data-island-slot': props.name,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
if (slots.fallback) {
|
if (slots.fallback) {
|
||||||
vnodes.push(h(Teleport, { to: `island-fallback=${props.name}`}, slots.fallback()))
|
vnodes.push(h(Teleport, { to: `island-fallback=${props.name}` }, slots.fallback()))
|
||||||
}
|
}
|
||||||
|
|
||||||
return vnodes
|
return vnodes
|
||||||
|
@ -21,7 +21,7 @@ interface ServerOnlyComponentTransformPluginOptions {
|
|||||||
/**
|
/**
|
||||||
* allow using `nuxt-client` attribute on components
|
* allow using `nuxt-client` attribute on components
|
||||||
*/
|
*/
|
||||||
selectiveClient?: boolean
|
selectiveClient?: boolean | 'deep'
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ComponentChunkOptions {
|
interface ComponentChunkOptions {
|
||||||
@ -47,6 +47,7 @@ export const islandsTransform = createUnplugin((options: ServerOnlyComponentTran
|
|||||||
enforce: 'pre',
|
enforce: 'pre',
|
||||||
transformInclude (id) {
|
transformInclude (id) {
|
||||||
if (!isVue(id)) { return false }
|
if (!isVue(id)) { return false }
|
||||||
|
if (options.selectiveClient === 'deep') { return true }
|
||||||
const components = options.getComponents()
|
const components = options.getComponents()
|
||||||
|
|
||||||
const islands = components.filter(component =>
|
const islands = components.filter(component =>
|
||||||
|
@ -70,6 +70,7 @@ export interface NuxtIslandClientResponse {
|
|||||||
html: string
|
html: string
|
||||||
props: unknown
|
props: unknown
|
||||||
chunk: string
|
chunk: string
|
||||||
|
slots?: Record<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NuxtIslandResponse {
|
export interface NuxtIslandResponse {
|
||||||
@ -629,6 +630,7 @@ function getServerComponentHTML (body: string[]): string {
|
|||||||
|
|
||||||
const SSR_SLOT_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=(.*)$/
|
||||||
|
const SSR_CLIENT_SLOT_MARKER = /^island-slot=(?:[^;]*);(.*)$/
|
||||||
|
|
||||||
function getSlotIslandResponse (ssrContext: NuxtSSRContext): NuxtIslandResponse['slots'] {
|
function getSlotIslandResponse (ssrContext: NuxtSSRContext): NuxtIslandResponse['slots'] {
|
||||||
if (!ssrContext.islandContext) { return {} }
|
if (!ssrContext.islandContext) { return {} }
|
||||||
@ -636,7 +638,7 @@ function getSlotIslandResponse (ssrContext: NuxtSSRContext): NuxtIslandResponse[
|
|||||||
for (const slot in ssrContext.islandContext.slots) {
|
for (const slot in ssrContext.islandContext.slots) {
|
||||||
response[slot] = {
|
response[slot] = {
|
||||||
...ssrContext.islandContext.slots[slot],
|
...ssrContext.islandContext.slots[slot],
|
||||||
fallback: ssrContext.teleports?.[`island-fallback=${slot}`]
|
fallback: ssrContext.teleports?.[`island-fallback=${slot}`],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return response
|
return response
|
||||||
@ -645,16 +647,33 @@ function getSlotIslandResponse (ssrContext: NuxtSSRContext): NuxtIslandResponse[
|
|||||||
function getClientIslandResponse (ssrContext: NuxtSSRContext): NuxtIslandResponse['components'] {
|
function getClientIslandResponse (ssrContext: NuxtSSRContext): NuxtIslandResponse['components'] {
|
||||||
if (!ssrContext.islandContext) { return {} }
|
if (!ssrContext.islandContext) { return {} }
|
||||||
const response: NuxtIslandResponse['components'] = {}
|
const response: NuxtIslandResponse['components'] = {}
|
||||||
|
|
||||||
for (const clientUid in ssrContext.islandContext.components) {
|
for (const clientUid in ssrContext.islandContext.components) {
|
||||||
const html = ssrContext.teleports?.[clientUid] || ''
|
const html = ssrContext.teleports?.[clientUid] || ''
|
||||||
response[clientUid] = {
|
response[clientUid] = {
|
||||||
...ssrContext.islandContext.components[clientUid],
|
...ssrContext.islandContext.components[clientUid],
|
||||||
html,
|
html,
|
||||||
|
slots: getComponentSlotTeleport(ssrContext.teleports ?? {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getComponentSlotTeleport (teleports: Record<string, string>) {
|
||||||
|
const entries = Object.entries(teleports)
|
||||||
|
const slots: Record<string, string> = {}
|
||||||
|
|
||||||
|
for (const [key, value] of entries) {
|
||||||
|
const match = key.match(SSR_CLIENT_SLOT_MARKER)
|
||||||
|
if (match) {
|
||||||
|
const [, slot] = match
|
||||||
|
if (!slot) { continue }
|
||||||
|
slots[slot] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return slots
|
||||||
|
}
|
||||||
|
|
||||||
function replaceIslandTeleports (ssrContext: NuxtSSRContext, html: string) {
|
function replaceIslandTeleports (ssrContext: NuxtSSRContext, html: string) {
|
||||||
const { teleports, islandContext } = ssrContext
|
const { teleports, islandContext } = ssrContext
|
||||||
|
|
||||||
|
@ -386,7 +386,7 @@ export const nuxtConfigTemplate: NuxtTemplate = {
|
|||||||
`export const cookieStore = ${!!ctx.nuxt.options.experimental.cookieStore}`,
|
`export const cookieStore = ${!!ctx.nuxt.options.experimental.cookieStore}`,
|
||||||
`export const appManifest = ${!!ctx.nuxt.options.experimental.appManifest}`,
|
`export const appManifest = ${!!ctx.nuxt.options.experimental.appManifest}`,
|
||||||
`export const remoteComponentIslands = ${typeof ctx.nuxt.options.experimental.componentIslands === 'object' && ctx.nuxt.options.experimental.componentIslands.remoteIsland}`,
|
`export const remoteComponentIslands = ${typeof ctx.nuxt.options.experimental.componentIslands === 'object' && ctx.nuxt.options.experimental.componentIslands.remoteIsland}`,
|
||||||
`export const selectiveClient = ${typeof ctx.nuxt.options.experimental.componentIslands === 'object' && ctx.nuxt.options.experimental.componentIslands.selectiveClient}`,
|
`export const selectiveClient = ${typeof ctx.nuxt.options.experimental.componentIslands === 'object' && Boolean(ctx.nuxt.options.experimental.componentIslands.selectiveClient)}`,
|
||||||
`export const devPagesDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.dir.pages) : 'null'}`,
|
`export const devPagesDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.dir.pages) : 'null'}`,
|
||||||
`export const devRootDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.rootDir) : 'null'}`,
|
`export const devRootDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.rootDir) : 'null'}`,
|
||||||
`export const nuxtLinkDefaults = ${JSON.stringify(ctx.nuxt.options.experimental.defaults.nuxtLink)}`,
|
`export const nuxtLinkDefaults = ${JSON.stringify(ctx.nuxt.options.experimental.defaults.nuxtLink)}`,
|
||||||
|
@ -1610,6 +1610,12 @@ describe('server components/islands', () => {
|
|||||||
// test islands wrapped with client-only
|
// test islands wrapped with client-only
|
||||||
expect(await page.locator('#wrapped-client-only').innerHTML()).toContain('Was router enabled')
|
expect(await page.locator('#wrapped-client-only').innerHTML()).toContain('Was router enabled')
|
||||||
|
|
||||||
|
if (!isWebpack) {
|
||||||
|
// test nested client components
|
||||||
|
await page.locator('.server-with-nested-client button').click()
|
||||||
|
expect(await page.locator('.server-with-nested-client .sugar-counter').innerHTML()).toContain('Sugar Counter 13 x 1 = 13')
|
||||||
|
}
|
||||||
|
|
||||||
if (!isWebpack) {
|
if (!isWebpack) {
|
||||||
// test client component interactivity
|
// test client component interactivity
|
||||||
expect(await page.locator('.interactive-component-wrapper').innerHTML()).toContain('Sugar Counter 12')
|
expect(await page.locator('.interactive-component-wrapper').innerHTML()).toContain('Sugar Counter 12')
|
||||||
@ -1667,9 +1673,9 @@ describe('server components/islands', () => {
|
|||||||
const text = (await page.innerText('pre')).replaceAll(/ data-island-uid="([^"]*)"/g, '').replace(/data-island-component="([^"]*)"/g, (_, content) => `data-island-component="${content.split('-')[0]}"`)
|
const text = (await page.innerText('pre')).replaceAll(/ data-island-uid="([^"]*)"/g, '').replace(/data-island-component="([^"]*)"/g, (_, content) => `data-island-component="${content.split('-')[0]}"`)
|
||||||
|
|
||||||
if (isWebpack) {
|
if (isWebpack) {
|
||||||
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>"')
|
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"><!--teleport start--><!--teleport end--></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"><!--teleport start--><!--teleport end--></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> 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-component="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"><!--teleport start--><!--teleport end--></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"><!--teleport start--><!--teleport end--></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-component="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')
|
||||||
|
|
||||||
@ -1928,7 +1934,7 @@ describe('component islands', () => {
|
|||||||
"link": [],
|
"link": [],
|
||||||
"style": [],
|
"style": [],
|
||||||
},
|
},
|
||||||
"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>",
|
"html": "<div data-island-uid><div> count is above 2 </div><!--[--><div style="display: contents;" data-island-uid data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--> that was very long ... <div id="long-async-component-count">3</div> <!--[--><div style="display: contents;" data-island-uid data-island-slot="test"><!--teleport start--><!--teleport end--></div><!--]--><p>hello world !!!</p><!--[--><div style="display: contents;" data-island-uid data-island-slot="hello"><!--teleport start--><!--teleport end--></div><!--teleport start--><!--teleport end--><!--]--><!--[--><div style="display: contents;" data-island-uid data-island-slot="fallback"><!--teleport start--><!--teleport end--></div><!--teleport start--><!--teleport end--><!--]--></div>",
|
||||||
"slots": {
|
"slots": {
|
||||||
"default": {
|
"default": {
|
||||||
"props": [],
|
"props": [],
|
||||||
@ -1992,7 +1998,7 @@ describe('component islands', () => {
|
|||||||
"link": [],
|
"link": [],
|
||||||
"style": [],
|
"style": [],
|
||||||
},
|
},
|
||||||
"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>",
|
"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"><!--teleport start--><!--teleport end--></div><!--]--></div>",
|
||||||
"props": {},
|
"props": {},
|
||||||
"slots": {},
|
"slots": {},
|
||||||
"state": {},
|
"state": {},
|
||||||
|
9
test/fixtures/basic/components/CounterWithNuxtClient.vue
vendored
Normal file
9
test/fixtures/basic/components/CounterWithNuxtClient.vue
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
this is a normal component within a server component
|
||||||
|
<Counter
|
||||||
|
nuxt-client
|
||||||
|
:multiplier="1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
5
test/fixtures/basic/components/ServerWithNestedClient.server.vue
vendored
Normal file
5
test/fixtures/basic/components/ServerWithNestedClient.server.vue
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div class="server-with-nested-client">
|
||||||
|
<CounterWithNuxtClient />
|
||||||
|
</div>
|
||||||
|
</template>
|
2
test/fixtures/basic/nuxt.config.ts
vendored
2
test/fixtures/basic/nuxt.config.ts
vendored
@ -211,7 +211,7 @@ export default defineNuxtConfig({
|
|||||||
restoreState: true,
|
restoreState: true,
|
||||||
clientNodeCompat: true,
|
clientNodeCompat: true,
|
||||||
componentIslands: {
|
componentIslands: {
|
||||||
selectiveClient: true
|
selectiveClient: 'deep'
|
||||||
},
|
},
|
||||||
treeshakeClientOnly: true,
|
treeshakeClientOnly: true,
|
||||||
asyncContext: process.env.TEST_CONTEXT === 'async',
|
asyncContext: process.env.TEST_CONTEXT === 'async',
|
||||||
|
1
test/fixtures/basic/pages/islands.vue
vendored
1
test/fixtures/basic/pages/islands.vue
vendored
@ -110,6 +110,7 @@ const count = ref(0)
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ServerWithClient />
|
<ServerWithClient />
|
||||||
|
<ServerWithNestedClient />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ vi.mock('vue', async (original) => {
|
|||||||
const consoleError = vi.spyOn(console, 'error')
|
const consoleError = vi.spyOn(console, 'error')
|
||||||
const consoleWarn = vi.spyOn(console, 'warn')
|
const consoleWarn = vi.spyOn(console, 'warn')
|
||||||
|
|
||||||
function expectNoConsoleIssue() {
|
function expectNoConsoleIssue () {
|
||||||
expect(consoleError).not.toHaveBeenCalled()
|
expect(consoleError).not.toHaveBeenCalled()
|
||||||
expect(consoleWarn).not.toHaveBeenCalled()
|
expect(consoleWarn).not.toHaveBeenCalled()
|
||||||
}
|
}
|
||||||
@ -93,7 +93,7 @@ describe('runtime server component', () => {
|
|||||||
link: [],
|
link: [],
|
||||||
style: []
|
style: []
|
||||||
},
|
},
|
||||||
json() {
|
json () {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -123,7 +123,7 @@ describe('client components', () => {
|
|||||||
vi.doMock(mockPath, () => ({
|
vi.doMock(mockPath, () => ({
|
||||||
default: {
|
default: {
|
||||||
name: 'ClientComponent',
|
name: 'ClientComponent',
|
||||||
setup() {
|
setup () {
|
||||||
return () => h('div', 'client component')
|
return () => h('div', 'client component')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -145,7 +145,7 @@ describe('client components', () => {
|
|||||||
chunk: mockPath
|
chunk: mockPath
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
json() {
|
json () {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -184,14 +184,14 @@ describe('client components', () => {
|
|||||||
style: []
|
style: []
|
||||||
},
|
},
|
||||||
components: {},
|
components: {},
|
||||||
json() {
|
json () {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
await wrapper.vm.$.exposed!.refresh()
|
await wrapper.vm.$.exposed!.refresh()
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(wrapper.html()).toMatchInlineSnapshot( `
|
expect(wrapper.html()).toMatchInlineSnapshot(`
|
||||||
"<div data-island-uid="3">hello<div>
|
"<div data-island-uid="3">hello<div>
|
||||||
<div>fallback</div>
|
<div>fallback</div>
|
||||||
</div>
|
</div>
|
||||||
@ -202,10 +202,10 @@ describe('client components', () => {
|
|||||||
expectNoConsoleIssue()
|
expectNoConsoleIssue()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
it('should not replace nested client components data-island-uid', async () => {
|
it('should not replace nested client components data-island-uid', async () => {
|
||||||
const componentId = 'Client-12345'
|
const componentId = 'Client-12345'
|
||||||
|
|
||||||
const stubFetch = vi.fn(() => {
|
const stubFetch = vi.fn(() => {
|
||||||
return {
|
return {
|
||||||
id: '1234',
|
id: '1234',
|
||||||
@ -215,7 +215,7 @@ describe('client components', () => {
|
|||||||
link: [],
|
link: [],
|
||||||
style: []
|
style: []
|
||||||
},
|
},
|
||||||
json() {
|
json () {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -238,4 +238,66 @@ describe('client components', () => {
|
|||||||
vi.mocked(fetch).mockReset()
|
vi.mocked(fetch).mockReset()
|
||||||
expectNoConsoleIssue()
|
expectNoConsoleIssue()
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
it('pass a slot to a client components within islands', async () => {
|
||||||
|
const mockPath = '/nuxt-client-with-slot.js'
|
||||||
|
const componentId = 'ClientWithSlot-12345'
|
||||||
|
|
||||||
|
vi.doMock(mockPath, () => ({
|
||||||
|
default: {
|
||||||
|
name: 'ClientWithSlot',
|
||||||
|
setup (_, { slots }) {
|
||||||
|
return () => h('div', { class: "client-component" }, slots.default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const stubFetch = vi.fn(() => {
|
||||||
|
return {
|
||||||
|
id: '123',
|
||||||
|
html: `<div data-island-uid>hello<div data-island-uid data-island-component="${componentId}"></div></div>`,
|
||||||
|
state: {},
|
||||||
|
head: {
|
||||||
|
link: [],
|
||||||
|
style: []
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
[componentId]: {
|
||||||
|
html: '<div>fallback</div>',
|
||||||
|
props: {},
|
||||||
|
chunk: mockPath,
|
||||||
|
slots: {
|
||||||
|
default: '<div>slot in client component</div>'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
json () {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.stubGlobal('fetch', stubFetch)
|
||||||
|
const wrapper = await mountSuspended(NuxtIsland, {
|
||||||
|
props: {
|
||||||
|
name: 'NuxtClientWithSlot',
|
||||||
|
},
|
||||||
|
attachTo: 'body'
|
||||||
|
})
|
||||||
|
expect(fetch).toHaveBeenCalledOnce()
|
||||||
|
expect(wrapper.html()).toMatchInlineSnapshot(`
|
||||||
|
"<div data-island-uid="5">hello<div data-island-uid="5" data-island-component="ClientWithSlot-12345">
|
||||||
|
<div class="client-component">
|
||||||
|
<div style="display: contents" data-island-uid="" data-island-slot="default">
|
||||||
|
<div>slot in client component</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--teleport start-->
|
||||||
|
<!--teleport end-->"
|
||||||
|
`)
|
||||||
|
|
||||||
|
expectNoConsoleIssue()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user