mirror of
https://github.com/nuxt/nuxt.git
synced 2025-02-23 17:10:07 +00:00
WIP
This commit is contained in:
parent
c5f94be5d1
commit
c5d930b2aa
54
packages/nuxt/src/app/components/TeleportIfClient.ts
Normal file
54
packages/nuxt/src/app/components/TeleportIfClient.ts
Normal 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
|
||||
}
|
||||
}
|
||||
})
|
@ -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
|
||||
}
|
||||
|
@ -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>`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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 })
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user