From 3b51e4a1485ddfb8b1614d870cb9280671282553 Mon Sep 17 00:00:00 2001 From: Julien Huang Date: Wed, 11 Sep 2024 21:20:35 +0200 Subject: [PATCH] wip --- .../nuxt/src/app/components/nuxt-island.ts | 3 +- .../nuxt-teleport-island-component.ts | 1 - .../components/nuxt-teleport-island-slot.ts | 5 +- packages/nuxt/src/app/components/utils.ts | 78 ++++++++++++++- .../nuxt/src/components/islandsTransform.ts | 26 +---- packages/nuxt/src/components/loader.ts | 8 +- packages/nuxt/src/components/module.ts | 2 +- .../components/runtime/server-component.ts | 7 +- .../nuxt/src/core/runtime/nitro/renderer.ts | 18 ++-- test/fixtures/basic/nuxt.config.ts | 1 + test/fixtures/basic/pages/islands.vue | 95 +------------------ 11 files changed, 109 insertions(+), 135 deletions(-) diff --git a/packages/nuxt/src/app/components/nuxt-island.ts b/packages/nuxt/src/app/components/nuxt-island.ts index 937f9d3136..07ec6f9541 100644 --- a/packages/nuxt/src/app/components/nuxt-island.ts +++ b/packages/nuxt/src/app/components/nuxt-island.ts @@ -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)) diff --git a/packages/nuxt/src/app/components/nuxt-teleport-island-component.ts b/packages/nuxt/src/app/components/nuxt-teleport-island-component.ts index 875204ccf4..e7fb8f2a72 100644 --- a/packages/nuxt/src/app/components/nuxt-teleport-island-component.ts +++ b/packages/nuxt/src/app/components/nuxt-teleport-island-component.ts @@ -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! diff --git a/packages/nuxt/src/app/components/nuxt-teleport-island-slot.ts b/packages/nuxt/src/app/components/nuxt-teleport-island-slot.ts index 7db8042735..ae516f1166 100644 --- a/packages/nuxt/src/app/components/nuxt-teleport-island-slot.ts +++ b/packages/nuxt/src/app/components/nuxt-teleport-island-slot.ts @@ -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, + 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] diff --git a/packages/nuxt/src/app/components/utils.ts b/packages/nuxt/src/app/components/utils.ts index bb16708588..939ab157a7 100644 --- a/packages/nuxt/src/app/components/utils.ts +++ b/packages/nuxt/src/app/components/utils.ts @@ -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) + } +} diff --git a/packages/nuxt/src/components/islandsTransform.ts b/packages/nuxt/src/components/islandsTransform.ts index d583e3425d..f131a2befe 100644 --- a/packages/nuxt/src/components/islandsTransform.ts +++ b/packages/nuxt/src/components/islandsTransform.ts @@ -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, ``) - - 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, ``) - } 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, '') - } 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' diff --git a/packages/nuxt/src/components/loader.ts b/packages/nuxt/src/components/loader.ts index b8e5d38b62..0c2861170b 100644 --- a/packages/nuxt/src/components/loader.ts +++ b/packages/nuxt/src/components/loader.ts @@ -41,6 +41,7 @@ export const loaderPlugin = createUnplugin((options: LoaderOptions) => { const imports = new Set() const map = new Map() 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})`) diff --git a/packages/nuxt/src/components/module.ts b/packages/nuxt/src/components/module.ts index 9fa26f4ed4..73ee631e3a 100644 --- a/packages/nuxt/src/components/module.ts +++ b/packages/nuxt/src/components/module.ts @@ -238,9 +238,9 @@ export default defineNuxtModule({ 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, diff --git a/packages/nuxt/src/components/runtime/server-component.ts b/packages/nuxt/src/components/runtime/server-component.ts index c5ecee9b2e..0d01b0456a 100644 --- a/packages/nuxt/src/components/runtime/server-component.ts +++ b/packages/nuxt/src/components/runtime/server-component.ts @@ -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) expose({ refresh: () => islandRef.value?.refresh(), }) - + console.log(slots.default?.toString()) return () => { - return h(NuxtIsland, { + return h( withIslandTeleport(NuxtIsland), { name, lazy: props.lazy, props: attrs, diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts index 000e2facc4..0459b9880c 100644 --- a/packages/nuxt/src/core/runtime/nitro/renderer.ts +++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts @@ -33,7 +33,8 @@ 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 // @ts-expect-error private property consumed by vite-generated url helpers @@ -473,6 +474,7 @@ export default defineRenderHandler(async (event): Promise', '') || '' response[clientUid] = { @@ -688,10 +691,9 @@ function getClientIslandResponse (ssrContext: NuxtSSRContext): NuxtIslandRespons function getComponentSlotTeleport (teleports: Record) { const entries = Object.entries(teleports) const slots: Record = {} - - 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 diff --git a/test/fixtures/basic/nuxt.config.ts b/test/fixtures/basic/nuxt.config.ts index 767f3008b2..2c3cc08d92 100644 --- a/test/fixtures/basic/nuxt.config.ts +++ b/test/fixtures/basic/nuxt.config.ts @@ -238,6 +238,7 @@ export default defineNuxtConfig({ }, }, features: { + devLogs: false, inlineStyles: id => !!id && !id.includes('assets.vue'), }, experimental: { diff --git a/test/fixtures/basic/pages/islands.vue b/test/fixtures/basic/pages/islands.vue index 148675861b..9aa8958a8a 100644 --- a/test/fixtures/basic/pages/islands.vue +++ b/test/fixtures/basic/pages/islands.vue @@ -1,4 +1,5 @@