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
|
// eslint-disable-next-line import/no-restricted-paths
|
||||||
import type { NuxtIslandResponse } from '../../core/runtime/nitro/renderer'
|
import type { NuxtIslandResponse } from '../../core/runtime/nitro/renderer'
|
||||||
|
|
||||||
import { getFragmentHTML, getSlotProps } from './utils'
|
import { getFragmentHTML, getSlotProps } from './utils'
|
||||||
import { useNuxtApp, useRuntimeConfig } from '#app/nuxt'
|
import { useNuxtApp, useRuntimeConfig } from '#app/nuxt'
|
||||||
import { useRequestEvent } from '#app/composables/ssr'
|
import { useRequestEvent } from '#app/composables/ssr'
|
||||||
@ -21,6 +22,23 @@ const SLOT_FALLBACK_RE = /<div nuxt-slot-fallback-start="([^"]*)"[^>]*><\/div>((
|
|||||||
|
|
||||||
let id = 0
|
let id = 0
|
||||||
const getId = process.client ? () => (id++).toString() : randomUUID
|
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({
|
export default defineComponent({
|
||||||
name: 'NuxtIsland',
|
name: 'NuxtIsland',
|
||||||
@ -71,7 +89,9 @@ export default defineComponent({
|
|||||||
head: {
|
head: {
|
||||||
link: [],
|
link: [],
|
||||||
style: []
|
style: []
|
||||||
}
|
},
|
||||||
|
chunks: {},
|
||||||
|
props: {}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
ssrHTML.value = renderedHTML ?? '<div></div>'
|
ssrHTML.value = renderedHTML ?? '<div></div>'
|
||||||
@ -90,6 +110,9 @@ export default defineComponent({
|
|||||||
return content
|
return content
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// no need for reactivity
|
||||||
|
let interactiveComponentsList = {}
|
||||||
function setUid () {
|
function setUid () {
|
||||||
uid.value = ssrHTML.value.match(SSR_UID_RE)?.[1] ?? getId() as string
|
uid.value = ssrHTML.value.match(SSR_UID_RE)?.[1] ?? getId() as string
|
||||||
}
|
}
|
||||||
@ -142,6 +165,11 @@ export default defineComponent({
|
|||||||
await nextTick()
|
await nextTick()
|
||||||
}
|
}
|
||||||
setUid()
|
setUid()
|
||||||
|
|
||||||
|
if (process.client) {
|
||||||
|
await loadComponents(res.chunks)
|
||||||
|
interactiveComponentsList = res.props
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (import.meta.hot) {
|
if (import.meta.hot) {
|
||||||
@ -163,6 +191,7 @@ export default defineComponent({
|
|||||||
const nodes = [createVNode(Fragment, {
|
const nodes = [createVNode(Fragment, {
|
||||||
key: key.value
|
key: key.value
|
||||||
}, [h(createStaticVNode(html.value, 1))])]
|
}, [h(createStaticVNode(html.value, 1))])]
|
||||||
|
|
||||||
if (uid.value && (mounted.value || nuxtApp.isHydrating || process.server)) {
|
if (uid.value && (mounted.value || nuxtApp.isHydrating || process.server)) {
|
||||||
for (const slot in slots) {
|
for (const slot in slots) {
|
||||||
if (availableSlots.value.includes(slot)) {
|
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
|
return nodes
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,16 @@ import { parseURL } from 'ufo'
|
|||||||
import { createUnplugin } from 'unplugin'
|
import { createUnplugin } from 'unplugin'
|
||||||
import MagicString from 'magic-string'
|
import MagicString from 'magic-string'
|
||||||
import { ELEMENT_NODE, parse, walk } from 'ultrahtml'
|
import { ELEMENT_NODE, parse, walk } from 'ultrahtml'
|
||||||
|
import { hash } from 'ohash'
|
||||||
import { isVue } from '../core/utils'
|
import { isVue } from '../core/utils'
|
||||||
|
|
||||||
interface ServerOnlyComponentTransformPluginOptions {
|
interface ServerOnlyComponentTransformPluginOptions {
|
||||||
getComponents: () => Component[]
|
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
|
const SCRIPT_RE = /<script[^>]*>/g
|
||||||
@ -36,12 +42,13 @@ export const islandsTransform = createUnplugin((options: ServerOnlyComponentTran
|
|||||||
const s = new MagicString(code)
|
const s = new MagicString(code)
|
||||||
|
|
||||||
s.replace(SCRIPT_RE, (full) => {
|
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])
|
const ast = parse(template[0])
|
||||||
await walk(ast, (node) => {
|
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 { attributes, children, loc, isSelfClosingTag } = node
|
||||||
const slotName = attributes.name ?? 'default'
|
const slotName = attributes.name ?? 'default'
|
||||||
let vfor: [string, string] | undefined
|
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/>')
|
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) {
|
if (isServer && nuxt.options.experimental.componentIslands) {
|
||||||
config.plugins.push(islandsTransform.vite({
|
config.plugins.push(islandsTransform.vite({
|
||||||
getComponents
|
getComponents,
|
||||||
|
rootDir: nuxt.options.rootDir
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
if (!isServer && nuxt.options.experimental.componentIslands) {
|
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
|
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')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
nuxt.hook('build:manifest', (manifest) => {
|
||||||
|
debugger
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add experimental cross-origin prefetch support using Speculation Rules API
|
// Add experimental cross-origin prefetch support using Speculation Rules API
|
||||||
|
@ -40,6 +40,10 @@ export interface NuxtIslandContext {
|
|||||||
name: string
|
name: string
|
||||||
props?: Record<string, any>
|
props?: Record<string, any>
|
||||||
url?: string
|
url?: string
|
||||||
|
// chunks to load components
|
||||||
|
chunks: Record<string, string>
|
||||||
|
// props to be sent back
|
||||||
|
propsData: Record<string, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NuxtIslandResponse {
|
export interface NuxtIslandResponse {
|
||||||
@ -50,6 +54,8 @@ export interface NuxtIslandResponse {
|
|||||||
link: (Record<string, string>)[]
|
link: (Record<string, string>)[]
|
||||||
style: ({ innerHTML: string, key: string })[]
|
style: ({ innerHTML: string, key: string })[]
|
||||||
}
|
}
|
||||||
|
chunks: Record<string, string>
|
||||||
|
props: Record<string, Record<string, any>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NuxtRenderResponse {
|
export interface NuxtRenderResponse {
|
||||||
@ -164,7 +170,9 @@ async function getIslandContext (event: H3Event): Promise<NuxtIslandContext> {
|
|||||||
id: hashId,
|
id: hashId,
|
||||||
name: componentName,
|
name: componentName,
|
||||||
props: destr(context.props) || {},
|
props: destr(context.props) || {},
|
||||||
uid: destr(context.uid) || undefined
|
uid: destr(context.uid) || undefined,
|
||||||
|
chunks: {},
|
||||||
|
propsData: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
@ -365,7 +373,9 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
|||||||
id: islandContext.id,
|
id: islandContext.id,
|
||||||
head,
|
head,
|
||||||
html: getServerComponentHTML(htmlContext.body),
|
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 })
|
await nitroApp.hooks.callHook('render:island', islandResponse, { event, islandContext })
|
||||||
|
@ -5,6 +5,13 @@
|
|||||||
<div id="async-server-component-count">
|
<div id="async-server-component-count">
|
||||||
{{ count }}
|
{{ count }}
|
||||||
</div>
|
</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 />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
Loading…
Reference in New Issue
Block a user