mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-23 22:25:12 +00:00
wip
This commit is contained in:
parent
03058b5132
commit
3b51e4a148
@ -75,7 +75,8 @@ export default defineComponent({
|
||||
},
|
||||
emits: ['error'],
|
||||
async setup (props, { slots, expose, emit }) {
|
||||
let canTeleport = import.meta.server
|
||||
console.log(slots.default?.toString())
|
||||
let canTeleport = import.meta.server
|
||||
const teleportKey = ref(0)
|
||||
const key = ref(0)
|
||||
const canLoadClientComponent = computed(() => selectiveClient && (props.dangerouslyLoadClientComponents || !props.source))
|
||||
|
@ -34,7 +34,6 @@ export default defineComponent({
|
||||
|
||||
// if there's already a teleport parent, we don't need to teleport or to render the wrapped component client side
|
||||
if (!nuxtApp.ssrContext?.islandContext || !props.nuxtClient || inject(NuxtTeleportIslandSymbol, false)) { return () => slots.default?.() }
|
||||
|
||||
provide(NuxtTeleportIslandSymbol, props.to)
|
||||
const islandContext = nuxtApp.ssrContext!.islandContext!
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import type { VNode } from 'vue'
|
||||
import { Teleport, createVNode, defineComponent, h, inject } from 'vue'
|
||||
import consola from 'consola'
|
||||
import { useNuxtApp } from '../nuxt'
|
||||
import { NuxtTeleportIslandSymbol } from './nuxt-teleport-island-component'
|
||||
|
||||
@ -20,10 +21,12 @@ export default defineComponent({
|
||||
*/
|
||||
props: {
|
||||
type: Object as () => Array<any>,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
setup (props, { slots }) {
|
||||
const nuxtApp = useNuxtApp()
|
||||
const nuxtApp = useNuxtApp()
|
||||
console.log(slots.default?.toString())
|
||||
const islandContext = nuxtApp.ssrContext?.islandContext
|
||||
if (!islandContext) {
|
||||
return () => slots.default?.()[0]
|
||||
|
@ -1,8 +1,11 @@
|
||||
import { h } from 'vue'
|
||||
import type { Component, RendererNode } from 'vue'
|
||||
import { createVNode, h } from 'vue'
|
||||
import type { Component, DefineComponent, RendererNode, VNode, renderSlot } from 'vue'
|
||||
// eslint-disable-next-line
|
||||
import { isString, isPromise, isArray, isObject } from '@vue/shared'
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
import NuxtTeleportIslandSlot from './nuxt-teleport-island-slot'
|
||||
import NuxtTeleportClient from './nuxt-teleport-island-component'
|
||||
import { useNuxtApp } from '#app'
|
||||
// @ts-expect-error virtual file
|
||||
import { START_LOCATION } from '#build/pages'
|
||||
|
||||
@ -155,3 +158,74 @@ function isStartFragment (element: RendererNode) {
|
||||
function isEndFragment (element: RendererNode) {
|
||||
return element.nodeName === '#comment' && element.nodeValue === ']'
|
||||
}
|
||||
|
||||
/**
|
||||
* convert a component to wrap all slots with a teleport
|
||||
*/
|
||||
export function withIslandTeleport (component: DefineComponent) {
|
||||
if (import.meta.client) { return component }
|
||||
return {
|
||||
...component,
|
||||
render: renderForIsland(component.render),
|
||||
setup: setupForIsland(component.setup),
|
||||
} as DefineComponent
|
||||
}
|
||||
|
||||
/**
|
||||
* export
|
||||
*/
|
||||
|
||||
function renderForIsland (render: any) {
|
||||
if (!render) { return undefined }
|
||||
|
||||
return (ctx, _cache, $props, $setup, $data, $options) => {
|
||||
|
||||
const _ctx = {
|
||||
...ctx,
|
||||
$slots: {
|
||||
...ctx.$slots,
|
||||
...Object.keys(ctx.$slots).reduce((acc, key) => {
|
||||
acc[key] = (...args: any) => {
|
||||
return h(NuxtTeleportIslandSlot, {
|
||||
name: key,
|
||||
props: [args],
|
||||
}, { default: () => ctx.$slots[key]?.(...args) })
|
||||
}
|
||||
return acc
|
||||
}, {}),
|
||||
},
|
||||
}
|
||||
for (const key in ctx.$slots) {
|
||||
ctx.$slots[key] = (...args: any) => {
|
||||
return h(NuxtTeleportIslandSlot, {
|
||||
name: key,
|
||||
props: [args],
|
||||
}, { default: () => ctx.$slots[key]?.(...args) })
|
||||
}
|
||||
}
|
||||
console.log('renderForIsland', _ctx, ctx, $setup)
|
||||
return render(_ctx, _cache, $props, $setup, $data, $options)
|
||||
}
|
||||
}
|
||||
|
||||
function setupForIsland (setup: (props, ctx) => any) {
|
||||
if(!setup) { return}
|
||||
return (props, ctx) => {
|
||||
// const slots = Object.keys(ctx.slots).reduce((acc, key) => {
|
||||
|
||||
// return Object.assign(acc, {[key]: (...args: any) => {
|
||||
// return createVNode(NuxtTeleportIslandSlot, {
|
||||
// name: key,
|
||||
// }, { default: () => ctx.slots[key]?.(...args) })
|
||||
// }})
|
||||
// }, {})
|
||||
for(const key in ctx.slots) {
|
||||
ctx.slots[key] = (...args: any) => {
|
||||
return createVNode(NuxtTeleportIslandSlot, {
|
||||
name: key,
|
||||
}, { default: () => ctx.slots[key]?.(...args) })
|
||||
}
|
||||
}
|
||||
return setup?.(props, ctx)
|
||||
}
|
||||
}
|
||||
|
@ -71,31 +71,7 @@ export const islandsTransform = createUnplugin((options: ServerOnlyComponentTran
|
||||
const ast = parse(template[0])
|
||||
await walk(ast, (node) => {
|
||||
if (node.type === ELEMENT_NODE) {
|
||||
if (node.name === 'slot') {
|
||||
const { attributes, children, loc } = node
|
||||
|
||||
const slotName = attributes.name ?? 'default'
|
||||
|
||||
if (attributes.name) { delete attributes.name }
|
||||
if (attributes['v-bind']) {
|
||||
attributes._bind = extractAttributes(attributes, ['v-bind'])['v-bind']
|
||||
}
|
||||
const teleportAttributes = extractAttributes(attributes, ['v-if', 'v-else-if', 'v-else'])
|
||||
const bindings = getPropsToString(attributes)
|
||||
// add the wrapper
|
||||
s.appendLeft(startingIndex + loc[0].start, `<NuxtTeleportSsrSlot${attributeToString(teleportAttributes)} name="${slotName}" :props="${bindings}">`)
|
||||
|
||||
if (children.length) {
|
||||
// pass slot fallback to NuxtTeleportSsrSlot fallback
|
||||
const attrString = attributeToString(attributes)
|
||||
const slice = code.slice(startingIndex + loc[0].end, startingIndex + loc[1].start).replaceAll(/:?key="[^"]"/g, '')
|
||||
s.overwrite(startingIndex + loc[0].start, startingIndex + loc[1].end, `<slot${attrString.replaceAll(EXTRACTED_ATTRS_RE, '')}/><template #fallback>${attributes['v-for'] ? wrapWithVForDiv(slice, attributes['v-for']) : slice}</template>`)
|
||||
} else {
|
||||
s.overwrite(startingIndex + loc[0].start, startingIndex + loc[0].end, code.slice(startingIndex + loc[0].start, startingIndex + loc[0].end).replaceAll(EXTRACTED_ATTRS_RE, ''))
|
||||
}
|
||||
|
||||
s.appendRight(startingIndex + loc[1].end, '</NuxtTeleportSsrSlot>')
|
||||
} else if (options.selectiveClient && ('nuxt-client' in node.attributes || ':nuxt-client' in node.attributes)) {
|
||||
if (options.selectiveClient && ('nuxt-client' in node.attributes || ':nuxt-client' in node.attributes)) {
|
||||
hasNuxtClient = true
|
||||
const { loc, attributes } = node
|
||||
const attributeValue = attributes[':nuxt-client'] || attributes['nuxt-client'] || 'true'
|
||||
|
@ -41,6 +41,7 @@ export const loaderPlugin = createUnplugin((options: LoaderOptions) => {
|
||||
const imports = new Set<string>()
|
||||
const map = new Map<Component, string>()
|
||||
const s = new MagicString(code)
|
||||
imports.add(genImport(resolve(distDir, 'app/components/utils'), [{ name: 'withIslandTeleport' }]))
|
||||
|
||||
// replace `_resolveComponent("...")` to direct import
|
||||
s.replace(/(?<=[ (])_?resolveComponent\(\s*["'](lazy-|Lazy(?=[A-Z]))?([^'"]*)["'][^)]*\)/g, (full: string, lazy: string, name: string) => {
|
||||
@ -59,6 +60,8 @@ export const loaderPlugin = createUnplugin((options: LoaderOptions) => {
|
||||
if (isServerOnly) {
|
||||
imports.add(genImport(serverComponentRuntime, [{ name: 'createServerComponent' }]))
|
||||
imports.add(`const ${identifier} = createServerComponent(${JSON.stringify(component.pascalName)})`)
|
||||
imports.add(`const ${identifier}_converted = withIslandTeleport(${identifier})`)
|
||||
identifier += '_converted'
|
||||
if (!options.experimentalComponentIslands) {
|
||||
logger.warn(`Standalone server components (\`${name}\`) are not yet supported without enabling \`experimental.componentIslands\`.`)
|
||||
}
|
||||
@ -70,13 +73,14 @@ export const loaderPlugin = createUnplugin((options: LoaderOptions) => {
|
||||
imports.add(genImport('#app/components/client-only', [{ name: 'createClientOnly' }]))
|
||||
identifier += '_client'
|
||||
}
|
||||
|
||||
if (lazy) {
|
||||
imports.add(genImport('vue', [{ name: 'defineAsyncComponent', as: '__defineAsyncComponent' }]))
|
||||
identifier += '_lazy'
|
||||
imports.add(`const ${identifier} = __defineAsyncComponent(${genDynamicImport(component.filePath, { interopDefault: false })}.then(c => c.${component.export ?? 'default'} || c)${isClientOnly ? '.then(c => createClientOnly(c))' : ''})`)
|
||||
imports.add(`const ${identifier} = __defineAsyncComponent(${genDynamicImport(component.filePath, { interopDefault: false })}.then(c => withIslandTeleport(c.${component.export ?? 'default'} || c))${isClientOnly ? '.then(c => createClientOnly(c))' : ''})`)
|
||||
} else {
|
||||
imports.add(genImport(component.filePath, [{ name: component._raw ? 'default' : component.export, as: identifier }]))
|
||||
imports.add(`const ${identifier}_converted = withIslandTeleport(${identifier})`)
|
||||
identifier += '_converted'
|
||||
|
||||
if (isClientOnly) {
|
||||
imports.add(`const ${identifier}_wrapped = createClientOnly(${identifier})`)
|
||||
|
@ -238,9 +238,9 @@ export default defineNuxtModule<ComponentsOptions>({
|
||||
|
||||
if (nuxt.options.experimental.componentIslands) {
|
||||
const selectiveClient = typeof nuxt.options.experimental.componentIslands === 'object' && nuxt.options.experimental.componentIslands.selectiveClient
|
||||
writeFileSync(join(nuxt.options.buildDir, 'components-chunk.mjs'), 'export const paths = {}')
|
||||
|
||||
if (isClient && selectiveClient) {
|
||||
writeFileSync(join(nuxt.options.buildDir, 'components-chunk.mjs'), 'export const paths = {}')
|
||||
if (!nuxt.options.dev) {
|
||||
config.plugins.push(componentsChunkPlugin.vite({
|
||||
getComponents,
|
||||
|
@ -2,6 +2,7 @@ import { defineComponent, getCurrentInstance, h, ref } from 'vue'
|
||||
import NuxtIsland from '#app/components/nuxt-island'
|
||||
import { useRoute } from '#app/composables/router'
|
||||
import { isPrerendered } from '#app/composables/payload'
|
||||
import { withIslandTeleport } from '#app/components/utils'
|
||||
|
||||
/* @__NO_SIDE_EFFECTS__ */
|
||||
export const createServerComponent = (name: string) => {
|
||||
@ -11,15 +12,15 @@ export const createServerComponent = (name: string) => {
|
||||
props: { lazy: Boolean },
|
||||
emits: ['error'],
|
||||
setup (props, { attrs, slots, expose, emit }) {
|
||||
const vm = getCurrentInstance()
|
||||
const vm = getCurrentInstance()
|
||||
const islandRef = ref<null | typeof NuxtIsland>(null)
|
||||
|
||||
expose({
|
||||
refresh: () => islandRef.value?.refresh(),
|
||||
})
|
||||
|
||||
console.log(slots.default?.toString())
|
||||
return () => {
|
||||
return h(NuxtIsland, {
|
||||
return h( withIslandTeleport(NuxtIsland), {
|
||||
name,
|
||||
lazy: props.lazy,
|
||||
props: attrs,
|
||||
|
@ -33,6 +33,7 @@ import type { NuxtPayload, NuxtSSRContext } from '#app'
|
||||
import { appHead, appId, appRootAttrs, appRootTag, appTeleportAttrs, appTeleportTag, componentIslands, multiApp } from '#internal/nuxt.config.mjs'
|
||||
// @ts-expect-error virtual file
|
||||
import { buildAssetsURL, publicAssetsURL } from '#internal/nuxt/paths'
|
||||
import consola from 'consola'
|
||||
|
||||
// @ts-expect-error private property consumed by vite-generated url helpers
|
||||
globalThis.__buildAssetsURL = buildAssetsURL
|
||||
@ -473,6 +474,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
||||
// TODO: remove for v4
|
||||
islandHead.link = islandHead.link || []
|
||||
islandHead.style = islandHead.style || []
|
||||
consola.log('teleports', ssrContext.teleports)
|
||||
|
||||
const islandResponse: NuxtIslandResponse = {
|
||||
id: islandContext.id,
|
||||
@ -655,7 +657,7 @@ function getServerComponentHTML (body: string): string {
|
||||
|
||||
const SSR_SLOT_TELEPORT_MARKER = /^uid=([^;]*);slot=(.*)$/
|
||||
const SSR_CLIENT_TELEPORT_MARKER = /^uid=([^;]*);client=(.*)$/
|
||||
const SSR_CLIENT_SLOT_MARKER = /^island-slot=[^;]*;(.*)$/
|
||||
const SSR_CLIENT_SLOT_MARKER = /^island-slot=[^;]*$/
|
||||
|
||||
function getSlotIslandResponse (ssrContext: NuxtSSRContext): NuxtIslandResponse['slots'] {
|
||||
if (!ssrContext.islandContext || !Object.keys(ssrContext.islandContext.slots).length) { return undefined }
|
||||
@ -668,12 +670,13 @@ function getSlotIslandResponse (ssrContext: NuxtSSRContext): NuxtIslandResponse[
|
||||
}
|
||||
return response
|
||||
}
|
||||
const logger = consola
|
||||
|
||||
logger.wrapConsole()
|
||||
function getClientIslandResponse (ssrContext: NuxtSSRContext): NuxtIslandResponse['components'] {
|
||||
if (!ssrContext.islandContext || !Object.keys(ssrContext.islandContext.components).length) { return undefined }
|
||||
const response: NuxtIslandResponse['components'] = {}
|
||||
|
||||
for (const clientUid in ssrContext.islandContext.components) {
|
||||
for (const clientUid in ssrContext.islandContext.components) {
|
||||
// remove teleport anchor to avoid hydration issues
|
||||
const html = ssrContext.teleports?.[clientUid].replaceAll('<!--teleport start anchor-->', '') || ''
|
||||
response[clientUid] = {
|
||||
@ -688,10 +691,9 @@ function getClientIslandResponse (ssrContext: NuxtSSRContext): NuxtIslandRespons
|
||||
function getComponentSlotTeleport (teleports: Record<string, string>) {
|
||||
const entries = Object.entries(teleports)
|
||||
const slots: Record<string, string> = {}
|
||||
|
||||
for (const [key, value] of entries) {
|
||||
const match = key.match(SSR_CLIENT_SLOT_MARKER)
|
||||
if (match) {
|
||||
for (const [key, value] of entries) {
|
||||
const match = key.match(SSR_CLIENT_SLOT_MARKER)
|
||||
if (match) {
|
||||
const [, slot] = match
|
||||
if (!slot) { continue }
|
||||
slots[slot] = value
|
||||
|
1
test/fixtures/basic/nuxt.config.ts
vendored
1
test/fixtures/basic/nuxt.config.ts
vendored
@ -238,6 +238,7 @@ export default defineNuxtConfig({
|
||||
},
|
||||
},
|
||||
features: {
|
||||
devLogs: false,
|
||||
inlineStyles: id => !!id && !id.includes('assets.vue'),
|
||||
},
|
||||
experimental: {
|
||||
|
93
test/fixtures/basic/pages/islands.vue
vendored
93
test/fixtures/basic/pages/islands.vue
vendored
@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
const islandProps = ref({
|
||||
bool: true,
|
||||
number: 100,
|
||||
@ -15,102 +16,14 @@ const count = ref(0)
|
||||
<template>
|
||||
<div>
|
||||
Pure island component:
|
||||
<div class="box">
|
||||
<NuxtIsland
|
||||
name="PureComponent"
|
||||
:props="islandProps"
|
||||
/>
|
||||
<div id="wrapped-client-only">
|
||||
<ClientOnly>
|
||||
<NuxtIsland
|
||||
name="PureComponent"
|
||||
:props="islandProps"
|
||||
/>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
id="increase-pure-component"
|
||||
@click="islandProps.number++"
|
||||
>
|
||||
Increase
|
||||
</button>
|
||||
<hr>
|
||||
Route island component:
|
||||
<div
|
||||
v-if="routeIslandVisible"
|
||||
class="box"
|
||||
>
|
||||
<NuxtIsland
|
||||
name="RouteComponent"
|
||||
:context="{ url: '/test' }"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
v-else
|
||||
id="show-route"
|
||||
@click="routeIslandVisible = true"
|
||||
>
|
||||
Show
|
||||
</button>
|
||||
|
||||
<p>async .server component</p>
|
||||
|
||||
<AsyncServerComponent :count="count">
|
||||
<div id="slot-in-server">
|
||||
Slot with in .server component
|
||||
</div>
|
||||
</AsyncServerComponent>
|
||||
<div>
|
||||
Async component (1000ms):
|
||||
<div>
|
||||
<NuxtIsland
|
||||
name="LongAsyncComponent"
|
||||
:props="{ count }"
|
||||
>
|
||||
<div>Interactive testing slot</div>
|
||||
<div id="first-sugar-counter">
|
||||
<Counter :multiplier="testCount" />
|
||||
</div>
|
||||
<template #test="scoped">
|
||||
<div id="test-slot">
|
||||
Slot with name test - scoped data {{ scoped }}
|
||||
</div>
|
||||
</template>
|
||||
<template #hello="scoped">
|
||||
<div id="test-slot">
|
||||
Slot with name hello - scoped data {{ scoped }}
|
||||
</div>
|
||||
</template>
|
||||
</NuxtIsland>
|
||||
<button
|
||||
id="update-server-components"
|
||||
@click="count++"
|
||||
>
|
||||
add +1 to count
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p>Island with props mounted client side</p>
|
||||
<button
|
||||
id="show-island"
|
||||
@click="showIslandSlot = true"
|
||||
>
|
||||
Show Interactive island
|
||||
</button>
|
||||
<div id="island-mounted-client-side">
|
||||
<NuxtIsland
|
||||
v-if="showIslandSlot"
|
||||
name="LongAsyncComponent"
|
||||
:props="{ count }"
|
||||
>
|
||||
<div>Interactive testing slot post SSR</div>
|
||||
<Counter :multiplier="testCount" />
|
||||
</NuxtIsland>
|
||||
</div>
|
||||
</div>
|
||||
<server-with-client />
|
||||
<ServerWithNestedClient />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user