fix(nuxt): add parent scopeId to server components (#27497)

This commit is contained in:
Maik Kowol 2024-06-11 00:20:27 +02:00 committed by GitHub
parent cd95d99704
commit 9655ce6f62
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 29 additions and 6 deletions

View File

@ -1,4 +1,4 @@
import type { Component } from 'vue' import type { Component, PropType } from 'vue'
import { Fragment, Teleport, computed, createStaticVNode, createVNode, defineComponent, getCurrentInstance, h, nextTick, onMounted, ref, toRaw, watch, withMemo } 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'
@ -59,6 +59,10 @@ export default defineComponent({
type: Object, type: Object,
default: () => ({}), default: () => ({}),
}, },
scopeId: {
type: String as PropType<string | undefined | null>,
default: () => undefined,
},
source: { source: {
type: String, type: String,
default: () => undefined, default: () => undefined,
@ -131,6 +135,10 @@ export default defineComponent({
const currentSlots = Object.keys(slots) const currentSlots = Object.keys(slots)
let html = ssrHTML.value let html = ssrHTML.value
if (props.scopeId) {
html = html.replace(/^<[^> ]*/, full => full + ' ' + props.scopeId)
}
if (import.meta.client && !canLoadClientComponent.value) { if (import.meta.client && !canLoadClientComponent.value) {
for (const [key, value] of Object.entries(payloads.components || {})) { for (const [key, value] of Object.entries(payloads.components || {})) {
html = html.replace(new RegExp(` data-island-uid="${uid.value}" data-island-component="${key}"[^>]*>`), (full) => { html = html.replace(new RegExp(` data-island-uid="${uid.value}" data-island-component="${key}"[^>]*>`), (full) => {

View File

@ -1,4 +1,4 @@
import { defineComponent, h, ref } from 'vue' import { defineComponent, getCurrentInstance, h, ref } from 'vue'
import NuxtIsland from '#app/components/nuxt-island' import NuxtIsland from '#app/components/nuxt-island'
import { useRoute } from '#app/composables/router' import { useRoute } from '#app/composables/router'
import { isPrerendered } from '#app/composables/payload' import { isPrerendered } from '#app/composables/payload'
@ -11,6 +11,7 @@ export const createServerComponent = (name: string) => {
props: { lazy: Boolean }, props: { lazy: Boolean },
emits: ['error'], emits: ['error'],
setup (props, { attrs, slots, expose, emit }) { setup (props, { attrs, slots, expose, emit }) {
const vm = getCurrentInstance()
const islandRef = ref<null | typeof NuxtIsland>(null) const islandRef = ref<null | typeof NuxtIsland>(null)
expose({ expose({
@ -22,6 +23,7 @@ export const createServerComponent = (name: string) => {
name, name,
lazy: props.lazy, lazy: props.lazy,
props: attrs, props: attrs,
scopeId: vm?.vnode.scopeId,
ref: islandRef, ref: islandRef,
onError: (err) => { onError: (err) => {
emit('error', err) emit('error', err)

View File

@ -1,6 +1,6 @@
import { beforeEach } from 'node:test' import { beforeEach } from 'node:test'
import { describe, expect, it, vi } from 'vitest' import { describe, expect, it, vi } from 'vitest'
import { h, nextTick } from 'vue' import { defineComponent, h, nextTick, popScopeId, pushScopeId } from 'vue'
import { mountSuspended } from '@nuxt/test-utils/runtime' import { mountSuspended } from '@nuxt/test-utils/runtime'
import { createServerComponent } from '../../packages/nuxt/src/components/runtime/server-component' import { createServerComponent } from '../../packages/nuxt/src/components/runtime/server-component'
import { createSimpleRemoteIslandProvider } from '../fixtures/remote-provider' import { createSimpleRemoteIslandProvider } from '../fixtures/remote-provider'
@ -133,6 +133,19 @@ describe('runtime server component', () => {
expect(wrapper.emitted('error')).toHaveLength(1) expect(wrapper.emitted('error')).toHaveLength(1)
vi.mocked(fetch).mockReset() vi.mocked(fetch).mockReset()
}) })
it('expect NuxtIsland to have parent scopeId', async () => {
const wrapper = await mountSuspended(defineComponent({
render () {
pushScopeId('data-v-654e2b21')
const vnode = h(createServerComponent('dummyName'))
popScopeId()
return vnode
},
}))
expect(wrapper.find('*').attributes()).toHaveProperty('data-v-654e2b21')
})
}) })
describe('client components', () => { describe('client components', () => {
@ -186,7 +199,7 @@ describe('client components', () => {
expect(fetch).toHaveBeenCalledOnce() expect(fetch).toHaveBeenCalledOnce()
expect(wrapper.html()).toMatchInlineSnapshot(` expect(wrapper.html()).toMatchInlineSnapshot(`
"<div data-island-uid="4">hello<div data-island-uid="4" data-island-component="Client-12345"> "<div data-island-uid="5">hello<div data-island-uid="5" data-island-component="Client-12345">
<div>client component</div> <div>client component</div>
</div> </div>
</div> </div>
@ -212,7 +225,7 @@ describe('client components', () => {
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="4">hello<div> "<div data-island-uid="5">hello<div>
<div>fallback</div> <div>fallback</div>
</div> </div>
</div>" </div>"
@ -305,7 +318,7 @@ describe('client components', () => {
}) })
expect(fetch).toHaveBeenCalledOnce() expect(fetch).toHaveBeenCalledOnce()
expect(wrapper.html()).toMatchInlineSnapshot(` expect(wrapper.html()).toMatchInlineSnapshot(`
"<div data-island-uid="6">hello<div data-island-uid="6" data-island-component="ClientWithSlot-12345"> "<div data-island-uid="7">hello<div data-island-uid="7" data-island-component="ClientWithSlot-12345">
<div class="client-component"> <div class="client-component">
<div style="display: contents" data-island-uid="" data-island-slot="default"> <div style="display: contents" data-island-uid="" data-island-slot="default">
<div>slot in client component</div> <div>slot in client component</div>