From 29923890f95ad77a82d8becadb347b00967c0c65 Mon Sep 17 00:00:00 2001 From: Julien Huang Date: Tue, 31 Dec 2024 00:24:58 +0100 Subject: [PATCH] fix(nuxt): remove div in client-only pages --- .../components/runtime/client-component.ts | 95 +++++++++++++------ .../pages/client-only-page/index.client.vue | 6 -- .../basic/pages/client-only-page/normal.vue | 5 - 3 files changed, 68 insertions(+), 38 deletions(-) diff --git a/packages/nuxt/src/components/runtime/client-component.ts b/packages/nuxt/src/components/runtime/client-component.ts index fd401f8836..62266d64ca 100644 --- a/packages/nuxt/src/components/runtime/client-component.ts +++ b/packages/nuxt/src/components/runtime/client-component.ts @@ -1,32 +1,73 @@ -import { defineAsyncComponent, defineComponent, h } from 'vue' -import type { AsyncComponentLoader } from 'vue' -import ClientOnly from '#app/components/client-only' -import { useNuxtApp } from '#app/nuxt' +import { isPromise } from 'node:util/types' +import { type AsyncComponentLoader, type ComponentOptions, h, onMounted, ref } from 'vue' +import { useNuxtApp } from '#app' +import ServerPlaceholder from '#app/components/server-placeholder' /* @__NO_SIDE_EFFECTS__ */ export const createClientPage = (loader: AsyncComponentLoader) => { - const page = defineAsyncComponent(import.meta.dev - ? () => loader().then((m) => { - // mark component as client-only for `definePageMeta` - (m.default || m).__clientOnlyPage = true - return m.default || m - }) - : loader) - - return defineComponent({ - inheritAttrs: false, - setup (_, { attrs }) { - const nuxtApp = useNuxtApp() - if (import.meta.server || nuxtApp.isHydrating) { - // wrapped with div to avoid Transition issues - // @see https://github.com/nuxt/nuxt/pull/25037#issuecomment-1877423894 - return () => h('div', [ - h(ClientOnly, undefined, { - default: () => h(page, attrs), - }), - ]) - } - return () => h(page, attrs) - }, + // Vue-router: Write "() => import('./MyPage.vue')" instead of "defineAsyncComponent(() => import('./MyPage.vue'))". + return loader().then((m) => { + const c = m.default || m + if (import.meta.dev) { + // mark component as client-only for `definePageMeta` + c.__clientOnlyPage = true + } + return pageToClientOnly(c) }) } + +const cache = new WeakMap() + +function pageToClientOnly (component: T) { + if (import.meta.server) { + return ServerPlaceholder + } + + if (cache.has(component)) { + return cache.get(component) + } + + const clone = { ...component } + + if (clone.render) { + // override the component render (non script setup component) or dev mode + clone.render = (ctx: any, cache: any, $props: any, $setup: any, $data: any, $options: any) => ($setup.mounted$ ?? ctx.mounted$) + ? h(component.render?.bind(ctx)(ctx, cache, $props, $setup, $data, $options)) + : h('div') + } else if (clone.template) { + // handle runtime-compiler template + clone.template = ` + + + ` + } + + clone.setup = (props, ctx) => { + const nuxtApp = useNuxtApp() + const mounted$ = ref(nuxtApp.isHydrating === false) + onMounted(() => { + mounted$.value = true + }) + const setupState = component.setup?.(props, ctx) || {} + if (isPromise(setupState)) { + return Promise.resolve(setupState).then((setupState: any) => { + if (typeof setupState !== 'function') { + setupState = setupState || {} + setupState.mounted$ = mounted$ + return setupState + } + return (...args: any[]) => (mounted$.value || !nuxtApp.isHydrating) ? h(setupState(...args)) : h('div') + }) + } else { + return typeof setupState === 'function' + ? (...args: any[]) => (mounted$.value || !nuxtApp.isHydrating) + ? h(setupState(...args)) + : h('div') + : Object.assign(setupState, { mounted$ }) + } + } + + cache.set(component, clone) + + return clone +} diff --git a/test/fixtures/basic/pages/client-only-page/index.client.vue b/test/fixtures/basic/pages/client-only-page/index.client.vue index 0f06e4d49c..f4fa62a325 100644 --- a/test/fixtures/basic/pages/client-only-page/index.client.vue +++ b/test/fixtures/basic/pages/client-only-page/index.client.vue @@ -1,10 +1,4 @@