]*nuxt-ssr-client="${key}"[^>]*>`), (full) => {
return full + value
@@ -165,24 +158,14 @@ export default defineComponent({
const url = remoteComponentIslands && props.source ? new URL(`/__nuxt_island/${key}.json`, props.source).href : `/__nuxt_island/${key}.json`
- if (import.meta.server && import.meta.prerender) {
- // Hint to Nitro to prerender the island component
- nuxtApp.runWithContext(() => prerenderRoutes(url))
- }
// TODO: Validate response
// $fetch handles the app.baseURL in dev
- const r = await eventFetch(withQuery(((import.meta.dev && import.meta.client) || props.source) ? url : joinURL(config.app.baseURL ?? '', url), {
+ const r = await eventFetch(withQuery((import.meta.dev || props.source) ? url : joinURL(config.app.baseURL ?? '', url), {
...props.context,
props: props.props ? JSON.stringify(props.props) : undefined
}))
- const result = import.meta.server || !import.meta.dev ? await r.json() : (r as FetchResponse
)._data
- // TODO: support passing on more headers
- if (import.meta.server && import.meta.prerender) {
- const hints = r.headers.get('x-nitro-prerender')
- if (hints) {
- appendResponseHeader(event, 'x-nitro-prerender', hints)
- }
- }
+ const result = import.meta.dev ? (r as FetchResponse)._data : await r.json()
+
setPayload(key, result)
return result
}
@@ -204,7 +187,7 @@ export default defineComponent({
key.value++
error.value = null
- if (selectiveClient && import.meta.client) {
+ if (selectiveClient) {
if (canLoadClientComponent.value && res.chunks) {
await loadComponents(props.source, res.chunks)
}
@@ -213,10 +196,9 @@ export default defineComponent({
nonReactivePayload.teleports = res.teleports
nonReactivePayload.chunks = res.chunks
- if (import.meta.client) {
- // must await next tick for Teleport to work correctly with static node re-rendering
- await nextTick()
- }
+ // must await next tick for Teleport to work correctly with static node re-rendering
+ await nextTick()
+
setUid()
} catch (e) {
error.value = e
@@ -233,13 +215,11 @@ export default defineComponent({
})
}
- if (import.meta.client) {
- watch(props, debounce(() => fetchComponent(), 100))
- }
-
- if (import.meta.client && !nuxtApp.isHydrating && props.lazy) {
+ watch(props, debounce(() => fetchComponent(), 100))
+
+ if (!nuxtApp.isHydrating && props.lazy) {
fetchComponent()
- } else if (import.meta.server || !nuxtApp.isHydrating || !nuxtApp.payload.serverRendered) {
+ } else if (!nuxtApp.isHydrating || !nuxtApp.payload.serverRendered) {
await fetchComponent()
} else if (selectiveClient && canLoadClientComponent.value && nonReactivePayload.chunks) {
await loadComponents(props.source, nonReactivePayload.chunks)
@@ -253,22 +233,15 @@ export default defineComponent({
key: key.value
}, [h(createStaticVNode(html.value || '', 1))])]
- if (uid.value && (mounted.value || nuxtApp.isHydrating || import.meta.server) && html.value) {
+ if (uid.value && (mounted.value || nuxtApp.isHydrating) && html.value) {
for (const slot in slots) {
if (availableSlots.value.includes(slot)) {
- nodes.push(createVNode(Teleport, { to: import.meta.client ? `[nuxt-ssr-component-uid='${uid.value}'] [nuxt-ssr-slot-name='${slot}']` : `uid=${uid.value};slot=${slot}` }, {
+ nodes.push(createVNode(Teleport, { to: `[nuxt-ssr-component-uid='${uid.value}'] [nuxt-ssr-slot-name='${slot}']` }, {
default: () => (slotProps.value[slot] ?? [undefined]).map((data: any) => slots[slot]?.(data))
}))
}
}
- if (import.meta.server) {
- for (const [id, html] of Object.entries(nonReactivePayload.teleports ?? {})) {
- nodes.push(createVNode(Teleport, { to: `uid=${uid.value};client=${id}` }, {
- default: () => [createStaticVNode(html, 1)]
- }))
- }
- }
- if (selectiveClient && import.meta.client && canLoadClientComponent.value) {
+ if (selectiveClient && 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}"]` }, {
diff --git a/packages/nuxt/src/app/components/nuxt-island.server.ts b/packages/nuxt/src/app/components/nuxt-island.server.ts
new file mode 100644
index 0000000000..e83ef38d99
--- /dev/null
+++ b/packages/nuxt/src/app/components/nuxt-island.server.ts
@@ -0,0 +1,169 @@
+import { Fragment, Teleport, computed, createStaticVNode, createVNode, defineComponent, h, ref, } from 'vue'
+import { hash } from 'ohash'
+import { appendResponseHeader } from 'h3'
+import { useHead } from '@unhead/vue'
+import { randomUUID } from 'uncrypto'
+import { joinURL, withQuery } from 'ufo'
+
+// eslint-disable-next-line import/no-restricted-paths
+import type { NuxtIslandResponse } from '../../core/runtime/nitro/renderer'
+import { useNuxtApp, useRuntimeConfig } from '../nuxt'
+import { prerenderRoutes, useRequestEvent } from '../composables/ssr'
+import { getSlotProps } from './utils'
+
+// @ts-expect-error virtual file
+import { remoteComponentIslands, selectiveClient } from '#build/nuxt.config.mjs'
+
+const pKey = '_islandPromises'
+const SSR_UID_RE = /nuxt-ssr-component-uid="([^"]*)"/
+const UID_ATTR = /nuxt-ssr-component-uid(="([^"]*)")?/
+const SLOTNAME_RE = /nuxt-ssr-slot-name="([^"]*)"/g
+
+export default defineComponent({
+ name: 'NuxtIsland',
+ props: {
+ name: {
+ type: String,
+ required: true
+ },
+ lazy: Boolean,
+ props: {
+ type: Object,
+ default: () => undefined
+ },
+ context: {
+ type: Object,
+ default: () => ({})
+ },
+ source: {
+ type: String,
+ default: () => undefined
+ },
+ dangerouslyLoadClientComponents: {
+ type: Boolean,
+ default: false
+ }
+ },
+ async setup(props, { slots }) {
+ const error = ref(null)
+ const config = useRuntimeConfig()
+ const nuxtApp = useNuxtApp()
+ const filteredProps = computed(() => props.props ? Object.fromEntries(Object.entries(props.props).filter(([key]) => !key.startsWith('data-v-'))) : {})
+ const hashId = computed(() => hash([props.name, filteredProps.value, props.context, props.source]))
+ const event = useRequestEvent()
+
+
+ function setPayload(key: string, result: NuxtIslandResponse) {
+ nuxtApp.payload.data[key] = {
+ __nuxt_island: {
+ key,
+ ...(import.meta.prerender)
+ ? {}
+ : { params: { ...props.context, props: props.props ? JSON.stringify(props.props) : undefined } },
+ result: {
+ chunks: result.chunks,
+ props: result.props,
+ teleports: result.teleports
+ }
+ },
+ ...result
+ }
+ }
+ const nonReactivePayload: Pick = {
+ chunks: {},
+ props: {},
+ teleports: {}
+ }
+
+ const ssrHTML = ref('')
+
+ const slotProps = computed(() => getSlotProps(ssrHTML.value))
+ const uid = ref(ssrHTML.value.match(SSR_UID_RE)?.[1] ?? randomUUID())
+ const availableSlots = computed(() => [...ssrHTML.value.matchAll(SLOTNAME_RE)].map(m => m[1]))
+
+ function setUid() {
+ uid.value = ssrHTML.value.match(SSR_UID_RE)?.[1] ?? randomUUID() as string
+ }
+
+ const cHead = ref>>>({ link: [], style: [] })
+ useHead(cHead)
+
+ async function _fetchComponent() {
+ const key = `${props.name}_${hashId.value}`
+
+ if (nuxtApp.payload.data[key]?.html) { return nuxtApp.payload.data[key] }
+
+ const url = remoteComponentIslands && props.source ? new URL(`/__nuxt_island/${key}.json`, props.source).href : `/__nuxt_island/${key}.json`
+
+ if (import.meta.prerender) {
+ // Hint to Nitro to prerender the island component
+ nuxtApp.runWithContext(() => prerenderRoutes(url))
+ }
+
+ // TODO: Validate response
+ const r = await event.fetch(withQuery((props.source) ? url : joinURL(config.app.baseURL ?? '', url), {
+ ...props.context,
+ props: props.props ? JSON.stringify(props.props) : undefined
+ }))
+ const result = await r.json()
+ // TODO: support passing on more headers
+ if (import.meta.prerender) {
+ const hints = r.headers.get('x-nitro-prerender')
+ if (hints) {
+ appendResponseHeader(event, 'x-nitro-prerender', hints)
+ }
+ }
+ setPayload(key, result)
+ return result
+ }
+
+ async function fetchComponent(force = false) {
+ nuxtApp[pKey] = nuxtApp[pKey] || {}
+ if (!nuxtApp[pKey][uid.value]) {
+ nuxtApp[pKey][uid.value] = _fetchComponent().finally(() => {
+ delete nuxtApp[pKey]![uid.value]
+ })
+ }
+ try {
+ const res: NuxtIslandResponse = await nuxtApp[pKey][uid.value]
+ cHead.value.link = res.head.link
+ cHead.value.style = res.head.style
+ ssrHTML.value = res.html.replace(UID_ATTR, () => {
+ return `nuxt-ssr-component-uid="${randomUUID()}"`
+ })
+ nonReactivePayload.teleports = res.teleports
+ nonReactivePayload.chunks = res.chunks
+
+ setUid()
+ } catch (e) {
+ error.value = e
+ }
+ }
+
+ await fetchComponent()
+
+ return () => {
+ if (!ssrHTML.value || error.value) {
+ return [slots.fallback?.({ error: error.value }) ?? createVNode('div')]
+ }
+ const nodes = [createVNode(Fragment, {}, [h(createStaticVNode(ssrHTML.value || '', 1))])]
+
+ // render slots and teleports
+ if (uid.value && ssrHTML.value) {
+ for (const slot in slots) {
+ if (availableSlots.value.includes(slot)) {
+ nodes.push(createVNode(Teleport, { to: `uid=${uid.value};slot=${slot}` }, {
+ default: () => (slotProps.value[slot] ?? [undefined]).map((data: any) => slots[slot]?.(data))
+ }))
+ }
+ }
+ for (const [id, html] of Object.entries(nonReactivePayload.teleports ?? {})) {
+ nodes.push(createVNode(Teleport, { to: `uid=${uid.value};client=${id}` }, {
+ default: () => [createStaticVNode(html, 1)]
+ }))
+ }
+ }
+ return nodes
+ }
+ }
+})
diff --git a/packages/nuxt/src/components/loader.ts b/packages/nuxt/src/components/loader.ts
index 2c86a709cb..9fa6e9403f 100644
--- a/packages/nuxt/src/components/loader.ts
+++ b/packages/nuxt/src/components/loader.ts
@@ -57,8 +57,9 @@ export const loaderPlugin = createUnplugin((options: LoaderOptions) => {
const isServerOnly = !component._raw && component.mode === 'server' &&
!components.some(c => c.pascalName === component.pascalName && c.mode === 'client')
if (isServerOnly) {
+ imports.add(genImport(`#app/components/nuxt-island.${options.mode}`, 'NuxtIsland'))
imports.add(genImport(serverComponentRuntime, [{ name: 'createServerComponent' }]))
- imports.add(`const ${identifier} = createServerComponent(${JSON.stringify(name)})`)
+ imports.add(`const ${identifier} = createServerComponent(${JSON.stringify(name)}, NuxtIsland)`)
if (!options.experimentalComponentIslands) {
logger.warn(`Standalone server components (\`${name}\`) are not yet supported without enabling \`experimental.componentIslands\`.`)
}
diff --git a/packages/nuxt/src/components/module.ts b/packages/nuxt/src/components/module.ts
index bcc6fb259b..bf73c9c0c0 100644
--- a/packages/nuxt/src/components/module.ts
+++ b/packages/nuxt/src/components/module.ts
@@ -130,11 +130,11 @@ export default defineNuxtModule({
const unpluginServer = createTransformPlugin(nuxt, getComponents, 'server')
const unpluginClient = createTransformPlugin(nuxt, getComponents, 'client')
- addVitePlugin(() => unpluginServer.vite(), { server: true, client: false })
- addVitePlugin(() => unpluginClient.vite(), { server: false, client: true })
+ addVitePlugin(() => unpluginServer.vite({ bundle: 'server'}), { server: true, client: false })
+ addVitePlugin(() => unpluginClient.vite({ bundle: 'client'}), { server: false, client: true })
- addWebpackPlugin(() => unpluginServer.webpack(), { server: true, client: false })
- addWebpackPlugin(() => unpluginClient.webpack(), { server: false, client: true })
+ addWebpackPlugin(() => unpluginServer.webpack({ bundle: 'server'}), { server: true, client: false })
+ addWebpackPlugin(() => unpluginClient.webpack({ bundle: 'client'}), { server: false, client: true })
// Do not prefetch global components chunks
nuxt.hook('build:manifest', (manifest) => {
diff --git a/packages/nuxt/src/components/runtime/server-component.ts b/packages/nuxt/src/components/runtime/server-component.ts
index 3a2463b09d..0df8c7c032 100644
--- a/packages/nuxt/src/components/runtime/server-component.ts
+++ b/packages/nuxt/src/components/runtime/server-component.ts
@@ -1,8 +1,12 @@
-import { defineComponent, h, ref } from 'vue'
-import NuxtIsland from '#app/components/nuxt-island'
+import { DefineComponent, defineComponent, h, ref } from 'vue'
+/**
+ * Since NuxtIsland is split into a server and client file
+ * we need to pass it as an argument to createServerComponent
+ * It is normally injected by transform plugins
+ */
/*@__NO_SIDE_EFFECTS__*/
-export const createServerComponent = (name: string) => {
+export const createServerComponent = (name: string, NuxtIsland: DefineComponent) => {
return defineComponent({
name,
inheritAttrs: false,
diff --git a/packages/nuxt/src/components/transform.ts b/packages/nuxt/src/components/transform.ts
index 39258224e3..1b5ba95592 100644
--- a/packages/nuxt/src/components/transform.ts
+++ b/packages/nuxt/src/components/transform.ts
@@ -47,7 +47,7 @@ export function createTransformPlugin (nuxt: Nuxt, getComponents: getComponentsT
})
}
- return createUnplugin(() => ({
+ return createUnplugin(({bundle} : {bundle: 'server'|'client'}) => ({
name: 'nuxt:components:imports',
transformInclude (id) {
id = normalize(id)
@@ -88,10 +88,16 @@ export function createTransformPlugin (nuxt: Nuxt, getComponents: getComponentsT
}
} else if (mode === 'server' || mode === 'server,async') {
const name = query.nuxt_component_name
+ console.log( [
+ `import NuxtIsland from "#app/components/nuxt-island.${bundle}"`,
+ `import { createServerComponent } from ${JSON.stringify(serverComponentRuntime)}`,
+ `export default createServerComponent(${JSON.stringify(name)}, NuxtIsland)`
+ ].join('\n'))
return {
code: [
+ `import NuxtIsland from "#app/components/nuxt-island.${bundle}"`,
`import { createServerComponent } from ${JSON.stringify(serverComponentRuntime)}`,
- `export default createServerComponent(${JSON.stringify(name)})`
+ `export default createServerComponent(${JSON.stringify(name)}, NuxtIsland)`
].join('\n'),
map: null
}
diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts
index da541b99d8..0ac1eaadf7 100644
--- a/packages/nuxt/src/core/nuxt.ts
+++ b/packages/nuxt/src/core/nuxt.ts
@@ -294,7 +294,16 @@ async function initNuxt (nuxt: Nuxt) {
addComponent({
name: 'NuxtIsland',
priority: 10, // built-in that we do not expect the user to override
- filePath: resolve(nuxt.options.appDir, 'components/nuxt-island')
+ filePath: resolve(nuxt.options.appDir, 'components/nuxt-island.server'),
+ mode: 'server',
+ _raw: true
+ })
+ addComponent({
+ name: 'NuxtIsland',
+ priority: 10, // built-in that we do not expect the user to override
+ filePath: resolve(nuxt.options.appDir, 'components/nuxt-island.client'),
+ mode: 'client',
+ _raw: true
})
if (!nuxt.options.ssr) {