mirror of
https://github.com/nuxt/nuxt.git
synced 2025-01-18 17:35:57 +00:00
fix(nuxt): use useId
for island client component teleport id (#30151)
This commit is contained in:
parent
374967ba10
commit
231b7d17c3
@ -1,5 +1,5 @@
|
||||
import type { Component, InjectionKey } from 'vue'
|
||||
import { Teleport, defineComponent, h, inject, provide } from 'vue'
|
||||
import { Teleport, defineComponent, h, inject, provide, useId } from 'vue'
|
||||
import { useNuxtApp } from '../nuxt'
|
||||
// @ts-expect-error virtual file
|
||||
import { paths } from '#build/components-chunk'
|
||||
@ -20,10 +20,6 @@ export default defineComponent({
|
||||
name: 'NuxtTeleportIslandComponent',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
to: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
nuxtClient: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@ -31,11 +27,12 @@ export default defineComponent({
|
||||
},
|
||||
setup (props, { slots }) {
|
||||
const nuxtApp = useNuxtApp()
|
||||
const to = useId()
|
||||
|
||||
// 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)
|
||||
provide(NuxtTeleportIslandSymbol, to)
|
||||
const islandContext = nuxtApp.ssrContext!.islandContext!
|
||||
|
||||
return () => {
|
||||
@ -43,7 +40,7 @@ export default defineComponent({
|
||||
const slotType = slot.type as ExtendedComponent
|
||||
const name = (slotType.__name || slotType.name) as string
|
||||
|
||||
islandContext.components[props.to] = {
|
||||
islandContext.components[to] = {
|
||||
chunk: import.meta.dev ? nuxtApp.$config.app.buildAssetsDir + paths[name] : paths[name],
|
||||
props: slot.props || {},
|
||||
}
|
||||
@ -51,8 +48,8 @@ export default defineComponent({
|
||||
return [h('div', {
|
||||
'style': 'display: contents;',
|
||||
'data-island-uid': '',
|
||||
'data-island-component': props.to,
|
||||
}, []), h(Teleport, { to: props.to }, slot)]
|
||||
'data-island-component': to,
|
||||
}, []), h(Teleport, { to }, slot)]
|
||||
}
|
||||
},
|
||||
})
|
||||
|
@ -6,7 +6,6 @@ import { parseURL } from 'ufo'
|
||||
import { createUnplugin } from 'unplugin'
|
||||
import MagicString from 'magic-string'
|
||||
import { ELEMENT_NODE, parse, walk } from 'ultrahtml'
|
||||
import { hash } from 'ohash'
|
||||
import { resolvePath } from '@nuxt/kit'
|
||||
import defu from 'defu'
|
||||
import { isVue } from '../../core/utils'
|
||||
@ -113,8 +112,6 @@ export const IslandsTransformPlugin = (options: ServerOnlyComponentTransformPlug
|
||||
|
||||
const { loc, attributes } = node
|
||||
const attributeValue = attributes[':nuxt-client'] || attributes['nuxt-client'] || 'true'
|
||||
|
||||
const uid = hash(id + node.loc[0].start + node.loc[0].end)
|
||||
const wrapperAttributes = extractAttributes(attributes, ['v-if', 'v-else-if', 'v-else'])
|
||||
|
||||
let startTag = code.slice(startingIndex + loc[0].start, startingIndex + loc[0].end).replace(NUXTCLIENT_ATTR_RE, '')
|
||||
@ -122,7 +119,7 @@ export const IslandsTransformPlugin = (options: ServerOnlyComponentTransformPlug
|
||||
startTag = startTag.replaceAll(EXTRACTED_ATTRS_RE, '')
|
||||
}
|
||||
|
||||
s.appendLeft(startingIndex + loc[0].start, `<NuxtTeleportIslandComponent${attributeToString(wrapperAttributes)} to="${node.name}-${uid}" :nuxt-client="${attributeValue}">`)
|
||||
s.appendLeft(startingIndex + loc[0].start, `<NuxtTeleportIslandComponent${attributeToString(wrapperAttributes)} :nuxt-client="${attributeValue}">`)
|
||||
s.overwrite(startingIndex + loc[0].start, startingIndex + loc[0].end, startTag)
|
||||
s.appendRight(startingIndex + loc[1].end, '</NuxtTeleportIslandComponent>')
|
||||
})
|
||||
|
@ -271,7 +271,7 @@ withDefaults(defineProps<{ things?: any[]; somethingElse?: string }>(), {
|
||||
"<template>
|
||||
<div>
|
||||
<HelloWorld />
|
||||
<NuxtTeleportIslandComponent to="HelloWorld-CyH3UXLuYA" :nuxt-client="true"><HelloWorld /></NuxtTeleportIslandComponent>
|
||||
<NuxtTeleportIslandComponent :nuxt-client="true"><HelloWorld /></NuxtTeleportIslandComponent>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -305,7 +305,7 @@ withDefaults(defineProps<{ things?: any[]; somethingElse?: string }>(), {
|
||||
"<template>
|
||||
<div>
|
||||
<HelloWorld />
|
||||
<NuxtTeleportIslandComponent to="HelloWorld-eo0XycWCUV" :nuxt-client="nuxtClient"><HelloWorld /></NuxtTeleportIslandComponent>
|
||||
<NuxtTeleportIslandComponent :nuxt-client="nuxtClient"><HelloWorld /></NuxtTeleportIslandComponent>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -376,7 +376,7 @@ withDefaults(defineProps<{ things?: any[]; somethingElse?: string }>(), {
|
||||
import NuxtTeleportSsrSlot from '#app/components/nuxt-teleport-island-slot'</script><template>
|
||||
<div>
|
||||
<HelloWorld />
|
||||
<NuxtTeleportIslandComponent to="HelloWorld-CyH3UXLuYA" :nuxt-client="true"><HelloWorld /></NuxtTeleportIslandComponent>
|
||||
<NuxtTeleportIslandComponent :nuxt-client="true"><HelloWorld /></NuxtTeleportIslandComponent>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -402,9 +402,9 @@ withDefaults(defineProps<{ things?: any[]; somethingElse?: string }>(), {
|
||||
import NuxtTeleportIslandComponent from '#app/components/nuxt-teleport-island-component'
|
||||
import NuxtTeleportSsrSlot from '#app/components/nuxt-teleport-island-slot'</script><template>
|
||||
<div>
|
||||
<NuxtTeleportIslandComponent v-if="false" to="HelloWorld-D9uaHyzL7X" :nuxt-client="true"><HelloWorld /></NuxtTeleportIslandComponent>
|
||||
<NuxtTeleportIslandComponent v-else-if="true" to="HelloWorld-o4RZMtArnE" :nuxt-client="true"><HelloWorld /></NuxtTeleportIslandComponent>
|
||||
<NuxtTeleportIslandComponent v-else to="HelloWorld-m1IbXHdd8O" :nuxt-client="true"><HelloWorld /></NuxtTeleportIslandComponent>
|
||||
<NuxtTeleportIslandComponent v-if="false" :nuxt-client="true"><HelloWorld /></NuxtTeleportIslandComponent>
|
||||
<NuxtTeleportIslandComponent v-else-if="true" :nuxt-client="true"><HelloWorld /></NuxtTeleportIslandComponent>
|
||||
<NuxtTeleportIslandComponent v-else :nuxt-client="true"><HelloWorld /></NuxtTeleportIslandComponent>
|
||||
</div>
|
||||
</template>
|
||||
"
|
||||
|
@ -1960,12 +1960,12 @@ describe('server components/islands', () => {
|
||||
await page.waitForLoadState('networkidle')
|
||||
await page.getByText('Go to page without lazy server component').click()
|
||||
|
||||
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, 'data-island-component')
|
||||
|
||||
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"><!--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 below 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 {
|
||||
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 below 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 below is not a slot but declared as interactive <!--[--><div style="display: contents;" data-island-component></div><!--teleport start--><!--teleport end--><!--]--></div></div>"')
|
||||
}
|
||||
expect(text).toContain('async component that was very long')
|
||||
|
||||
@ -2316,7 +2316,7 @@ describe('component islands', () => {
|
||||
const { components } = result
|
||||
result.components = {}
|
||||
result.slots = {}
|
||||
result.html = result.html.replace(/ data-island-component="([^"]*)"/g, (_, content) => ` data-island-component="${content.split('-')[0]}"`)
|
||||
result.html = result.html.replace(/data-island-component="([^"]*)"/g, 'data-island-component')
|
||||
|
||||
const teleportsEntries = Object.entries(components || {})
|
||||
|
||||
@ -2327,12 +2327,11 @@ describe('component islands', () => {
|
||||
"link": [],
|
||||
"style": [],
|
||||
},
|
||||
"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 below is not a slot but declared as interactive <!--[--><div style="display: contents;" data-island-uid data-island-component="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 below is not a slot but declared as interactive <!--[--><div style="display: contents;" data-island-uid data-island-component></div><!--teleport start--><!--teleport end--><!--]--></div></div>",
|
||||
"slots": {},
|
||||
}
|
||||
`)
|
||||
expect(teleportsEntries).toHaveLength(1)
|
||||
expect(teleportsEntries[0]![0].startsWith('Counter-')).toBeTruthy()
|
||||
expect(teleportsEntries[0]![1].props).toMatchInlineSnapshot(`
|
||||
{
|
||||
"multiplier": 1,
|
||||
|
Loading…
Reference in New Issue
Block a user