This commit is contained in:
julien huang 2023-08-10 21:01:09 +02:00
parent c5f94be5d1
commit c5d930b2aa
7 changed files with 169 additions and 41 deletions

View File

@ -0,0 +1,54 @@
import { Teleport, defineComponent, h } from 'vue'
import { useNuxtApp } from '#app'
import { relative } from 'path'
/**
* component only used with componentsIsland
* this teleport the component in SSR only if
*/
export default defineComponent({
name: 'TeleportIfClient',
props: {
to: String,
nuxtClient: {
type: Boolean,
default: false
},
/**
* ONLY used in dev mode since we use build:manifest result in production
* do not pass any value in production
*/
rootDir: {
type: String,
default: null
}
},
setup (props, { slots }) {
const app = useNuxtApp()
const islandContext = app.ssrContext!.islandContext
const slot = slots.default!()[0]
console.log(slot)
if (process.dev) {
console.log(app)
const path = '__nuxt/' + relative(props.rootDir, slot.type.__file)
islandContext.chunks[slot.type.__name] = path
}
islandContext.propsData[props.to] = slot.props || {}
// todo set prop in payload
return () => {
if (props.nuxtClient) {
return [h('div', {
style: 'display: contents;',
'nuxt-ssr-client': props.to
}, [slot]), h(Teleport, { to: props.to }, slot)]
}
return slot
}
}
})

View File

@ -9,6 +9,7 @@ import type { FetchResponse } from 'ofetch'
// eslint-disable-next-line import/no-restricted-paths
import type { NuxtIslandResponse } from '../../core/runtime/nitro/renderer'
import { getFragmentHTML, getSlotProps } from './utils'
import { useNuxtApp, useRuntimeConfig } from '#app/nuxt'
import { useRequestEvent } from '#app/composables/ssr'
@ -21,6 +22,23 @@ const SLOT_FALLBACK_RE = /<div nuxt-slot-fallback-start="([^"]*)"[^>]*><\/div>((
let id = 0
const getId = process.client ? () => (id++).toString() : randomUUID
const components = process.client ? new Map<string, unknown>() : undefined
async function loadComponents (paths: Record<string, string>) {
const promises = []
debugger
for (const component in paths) {
if (!(components!.has(component))) {
promises.push((async () => {
const c = await import('http://localhost:3000/__nuxt/components/SugarCounter.vue')
debugger
components!.set(component, c.default ?? c)
})())
}
}
await Promise.all(promises)
}
export default defineComponent({
name: 'NuxtIsland',
@ -71,7 +89,9 @@ export default defineComponent({
head: {
link: [],
style: []
}
},
chunks: {},
props: {}
})
}
ssrHTML.value = renderedHTML ?? '<div></div>'
@ -90,6 +110,9 @@ export default defineComponent({
return content
})
})
// no need for reactivity
let interactiveComponentsList = {}
function setUid () {
uid.value = ssrHTML.value.match(SSR_UID_RE)?.[1] ?? getId() as string
}
@ -142,6 +165,11 @@ export default defineComponent({
await nextTick()
}
setUid()
if (process.client) {
await loadComponents(res.chunks)
interactiveComponentsList = res.props
}
}
if (import.meta.hot) {
@ -163,6 +191,7 @@ export default defineComponent({
const nodes = [createVNode(Fragment, {
key: key.value
}, [h(createStaticVNode(html.value, 1))])]
if (uid.value && (mounted.value || nuxtApp.isHydrating || process.server)) {
for (const slot in slots) {
if (availableSlots.value.includes(slot)) {
@ -171,6 +200,15 @@ export default defineComponent({
}))
}
}
if (process.client && html.value.includes('nuxt-ssr-client') && mounted.value) {
for (const id in interactiveComponentsList) {
const vnode = createVNode(Teleport, { to: `[nuxt-ssr-component-uid='${uid.value}'] [nuxt-ssr-client="${id}"]` }, {
default: () => [h(components!.get(id.split('-')[0]), interactiveComponentsList[id])]
})
nodes.push(vnode)
}
}
}
return nodes
}

View File

@ -4,10 +4,16 @@ 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 { isVue } from '../core/utils'
interface ServerOnlyComponentTransformPluginOptions {
getComponents: () => Component[]
/**
* passed down to `TeleportIfClient`
* should be done only in dev mode as we use build:manifest result in production
*/
rootDir?: string
}
const SCRIPT_RE = /<script[^>]*>/g
@ -36,12 +42,13 @@ export const islandsTransform = createUnplugin((options: ServerOnlyComponentTran
const s = new MagicString(code)
s.replace(SCRIPT_RE, (full) => {
return full + '\nimport { vforToArray as __vforToArray } from \'#app/components/utils\''
return full + '\nimport { vforToArray as __vforToArray } from \'#app/components/utils\'' + '\nimport TeleportIfClient from \'#app/components/TeleportIfClient\''
})
const ast = parse(template[0])
await walk(ast, (node) => {
if (node.type === ELEMENT_NODE && node.name === 'slot') {
if (node.type === ELEMENT_NODE) {
if (node.name === 'slot') {
const { attributes, children, loc, isSelfClosingTag } = node
const slotName = attributes.name ?? 'default'
let vfor: [string, string] | undefined
@ -78,6 +85,13 @@ export const islandsTransform = createUnplugin((options: ServerOnlyComponentTran
s.appendLeft(startingIndex + loc[1].start, '<div nuxt-slot-fallback-end/>')
}
}
} else if ('nuxt-client' in node.attributes) {
// handle granular interactivity
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)
s.overwrite(node.loc[0].start, node.loc[1].end, `<TeleportIfClient to="${node.name}-${uid}" ${options.rootDir ? `root-dir="${options.rootDir}"` : ''} :nuxt-client="${node.attributes['nuxt-client'] || 'true'}">${htmlCode}</TeleportIfClient>`)
}
}
})

View File

@ -226,7 +226,8 @@ export default defineNuxtModule<ComponentsOptions>({
if (isServer && nuxt.options.experimental.componentIslands) {
config.plugins.push(islandsTransform.vite({
getComponents
getComponents,
rootDir: nuxt.options.rootDir
}))
}
if (!isServer && nuxt.options.experimental.componentIslands) {

View File

@ -269,6 +269,10 @@ async function initNuxt (nuxt: Nuxt) {
priority: 10, // built-in that we do not expect the user to override
filePath: resolve(nuxt.options.appDir, 'components/nuxt-island')
})
nuxt.hook('build:manifest', (manifest) => {
debugger
})
}
// Add experimental cross-origin prefetch support using Speculation Rules API

View File

@ -40,6 +40,10 @@ export interface NuxtIslandContext {
name: string
props?: Record<string, any>
url?: string
// chunks to load components
chunks: Record<string, string>
// props to be sent back
propsData: Record<string, any>
}
export interface NuxtIslandResponse {
@ -50,6 +54,8 @@ export interface NuxtIslandResponse {
link: (Record<string, string>)[]
style: ({ innerHTML: string, key: string })[]
}
chunks: Record<string, string>
props: Record<string, Record<string, any>>
}
export interface NuxtRenderResponse {
@ -164,7 +170,9 @@ async function getIslandContext (event: H3Event): Promise<NuxtIslandContext> {
id: hashId,
name: componentName,
props: destr(context.props) || {},
uid: destr(context.uid) || undefined
uid: destr(context.uid) || undefined,
chunks: {},
propsData: {}
}
return ctx
@ -365,7 +373,9 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
id: islandContext.id,
head,
html: getServerComponentHTML(htmlContext.body),
state: ssrContext.payload.state
state: ssrContext.payload.state,
chunks: islandContext.chunks,
props: islandContext.propsData
}
await nitroApp.hooks.callHook('render:island', islandResponse, { event, islandContext })

View File

@ -5,6 +5,13 @@
<div id="async-server-component-count">
{{ count }}
</div>
<div style="border: solid 1px red;">
The component bellow is not a slot but declared as interactive
<SugarCounter nuxt-client :multiplier="1" />
</div>
<slot />
</div>
</template>