From fd5a0f8a637da4bccb19b2f54dc10b029aac11c4 Mon Sep 17 00:00:00 2001 From: julien huang Date: Thu, 22 Sep 2022 19:56:33 +0200 Subject: [PATCH 01/10] fix(nuxt): remove Fragment and use h() to render component render --- packages/nuxt/src/app/components/client-only.mjs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/nuxt/src/app/components/client-only.mjs b/packages/nuxt/src/app/components/client-only.mjs index 3e0cf41bdd6..fe8722cfb7d 100644 --- a/packages/nuxt/src/app/components/client-only.mjs +++ b/packages/nuxt/src/app/components/client-only.mjs @@ -1,4 +1,4 @@ -import { ref, onMounted, defineComponent, createElementBlock, h, Fragment } from 'vue' +import { ref, onMounted, defineComponent, createElementBlock, h } from 'vue' export default defineComponent({ name: 'ClientOnly', @@ -31,7 +31,7 @@ export function createClientOnly (component) { // override the component render (non script setup component) clone.render = (ctx, ...args) => { return ctx.mounted$ - ? h(Fragment, ctx.$attrs ?? ctx._.attrs, component.render(ctx, ...args)) + ? h(component.render(ctx, ...args)) : h('div', ctx.$attrs ?? ctx._.attrs) } } else if (clone.template) { @@ -52,8 +52,7 @@ export function createClientOnly (component) { ? { ...setupState, mounted$ } : (...args) => { return mounted$.value - // use Fragment to avoid oldChildren is null issue - ? h(Fragment, ctx.attrs, setupState(...args)) + ? h(setupState(...args)) : h('div', ctx.attrs) } }) From 56812d0d8b892bb28640449a2a8e76cafc784539 Mon Sep 17 00:00:00 2001 From: julien huang Date: Thu, 22 Sep 2022 23:33:45 +0200 Subject: [PATCH 02/10] fix(nuxt): use createElementBlock if children is null or string --- packages/nuxt/src/app/components/client-only.mjs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/nuxt/src/app/components/client-only.mjs b/packages/nuxt/src/app/components/client-only.mjs index fe8722cfb7d..14b44024e0c 100644 --- a/packages/nuxt/src/app/components/client-only.mjs +++ b/packages/nuxt/src/app/components/client-only.mjs @@ -51,9 +51,14 @@ export function createClientOnly (component) { return typeof setupState !== 'function' ? { ...setupState, mounted$ } : (...args) => { - return mounted$.value - ? h(setupState(...args)) - : h('div', ctx.attrs) + if (mounted$.value) { + const res = setupState(...args) + return (res.children === null || typeof res.children === 'string') + ? createElementBlock(res.type, res.props, res.children, res.patchFlag, res.dynamicProps, res.shapeFlag) + : h(res) + } else { + return h('div', ctx.attrs) + } } }) } From 4e4757a896ff2b1eb69041b6aaf5abe793eab98c Mon Sep 17 00:00:00 2001 From: julien huang Date: Fri, 23 Sep 2022 16:17:26 +0200 Subject: [PATCH 03/10] test(basic): reinforce .client components test --- test/basic.test.ts | 45 +++++++++++++++++++ .../basic/components/client/MultiRootNode.vue | 10 +++++ .../components/client/MultiRootNodeScript.vue | 15 +++++++ .../basic/components/client/NoState.vue | 3 ++ .../basic/components/client/Script.client.vue | 39 ++++++++++++++++ .../components/client/SetupScript.client.vue | 14 ++++++ .../client/StringChildStateful.client.vue | 14 ++++++ .../StringChildStatefulScript.client.vue | 18 ++++++++ .../basic/pages/client-only-components.vue | 28 +++++++++--- 9 files changed, 180 insertions(+), 6 deletions(-) create mode 100644 test/fixtures/basic/components/client/MultiRootNode.vue create mode 100644 test/fixtures/basic/components/client/MultiRootNodeScript.vue create mode 100644 test/fixtures/basic/components/client/NoState.vue create mode 100644 test/fixtures/basic/components/client/Script.client.vue create mode 100644 test/fixtures/basic/components/client/SetupScript.client.vue create mode 100644 test/fixtures/basic/components/client/StringChildStateful.client.vue create mode 100644 test/fixtures/basic/components/client/StringChildStatefulScript.client.vue diff --git a/test/basic.test.ts b/test/basic.test.ts index 2fe9c019dba..f1bd5a71189 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -144,6 +144,7 @@ describe('pages', () => { }) it('/client-only-components', async () => { + // ensure components are not rendered server-side const html = await $fetch('/client-only-components') expect(html).toContain('
') expect(html).toContain('
') @@ -151,6 +152,50 @@ describe('pages', () => { expect(html).not.toContain('Should not be server rendered') await expectNoClientErrors('/client-only-components') + + const page = await createPage('/client-only-components') + + await page.waitForLoadState('networkidle') + + const hiddenSelectors = [ + '.string-stateful-should-be-hidden', + '.client-script-should-be-hidden', + '.string-stateful-script-should-be-hidden' + ] + const visibleSelectors = [ + '.string-stateful', + '.string-stateful-script', + '.client-only-script', + '.client-only-script-setup', + '.no-state' + ] + // ensure directive are correctly applied + await Promise.all(hiddenSelectors.map(selector => page.locator(selector).isHidden())) + .then(results => results.forEach(isHidden => expect(isHidden).toBeTruthy())) + // ensure hidden components are still rendered + await Promise.all(hiddenSelectors.map(selector => page.locator(selector).innerHTML())) + .then(results => results.forEach(innerHTML => expect(innerHTML).not.toBe(''))) + + // ensure single root node components are rendered once on client (should not be empty) + await Promise.all(visibleSelectors.map(selector => page.locator(selector).innerHTML())) + .then(results => results.forEach(innerHTML => expect(innerHTML).not.toBe(''))) + + // ensure multi-root-node is correctly rendered + expect(await page.locator('.multi-root-node-count').innerHTML()).toBe('0') + expect(await page.locator('.multi-root-node-button').innerHTML()).toBe('add 1 to count') + expect(await page.locator('.multi-root-node-script-count').innerHTML()).toBe('0') + expect(await page.locator('.multi-root-node-script-button').innerHTML()).toBe('add 1 to count') + + // ensure components reactivity + await page.locator('.multi-root-node-button').click() + await page.locator('.multi-root-node-script-button').click() + await page.locator('.client-only-script button').click() + await page.locator('.client-only-script-setup button').click() + + expect(await page.locator('.multi-root-node-count').innerHTML()).toBe('1') + expect(await page.locator('.multi-root-node-script-count').innerHTML()).toBe('1') + expect(await page.locator('.client-only-script-setup button').innerHTML()).toBe('1') + expect(await page.locator('.client-only-script button').innerHTML()).toBe('1') }) }) diff --git a/test/fixtures/basic/components/client/MultiRootNode.vue b/test/fixtures/basic/components/client/MultiRootNode.vue new file mode 100644 index 00000000000..f27a0f92caf --- /dev/null +++ b/test/fixtures/basic/components/client/MultiRootNode.vue @@ -0,0 +1,10 @@ + + + diff --git a/test/fixtures/basic/components/client/MultiRootNodeScript.vue b/test/fixtures/basic/components/client/MultiRootNodeScript.vue new file mode 100644 index 00000000000..cbc614b48e5 --- /dev/null +++ b/test/fixtures/basic/components/client/MultiRootNodeScript.vue @@ -0,0 +1,15 @@ + + + diff --git a/test/fixtures/basic/components/client/NoState.vue b/test/fixtures/basic/components/client/NoState.vue new file mode 100644 index 00000000000..92f3b3e3bec --- /dev/null +++ b/test/fixtures/basic/components/client/NoState.vue @@ -0,0 +1,3 @@ + diff --git a/test/fixtures/basic/components/client/Script.client.vue b/test/fixtures/basic/components/client/Script.client.vue new file mode 100644 index 00000000000..b21f8b5c8bf --- /dev/null +++ b/test/fixtures/basic/components/client/Script.client.vue @@ -0,0 +1,39 @@ + + + + + + + diff --git a/test/fixtures/basic/components/client/SetupScript.client.vue b/test/fixtures/basic/components/client/SetupScript.client.vue new file mode 100644 index 00000000000..edb9da630f3 --- /dev/null +++ b/test/fixtures/basic/components/client/SetupScript.client.vue @@ -0,0 +1,14 @@ + + + diff --git a/test/fixtures/basic/components/client/StringChildStateful.client.vue b/test/fixtures/basic/components/client/StringChildStateful.client.vue new file mode 100644 index 00000000000..48626719b08 --- /dev/null +++ b/test/fixtures/basic/components/client/StringChildStateful.client.vue @@ -0,0 +1,14 @@ + + + diff --git a/test/fixtures/basic/components/client/StringChildStatefulScript.client.vue b/test/fixtures/basic/components/client/StringChildStatefulScript.client.vue new file mode 100644 index 00000000000..cf083c8cdd2 --- /dev/null +++ b/test/fixtures/basic/components/client/StringChildStatefulScript.client.vue @@ -0,0 +1,18 @@ + + + diff --git a/test/fixtures/basic/pages/client-only-components.vue b/test/fixtures/basic/pages/client-only-components.vue index f020ea1cfc1..0397d39638c 100644 --- a/test/fixtures/basic/pages/client-only-components.vue +++ b/test/fixtures/basic/pages/client-only-components.vue @@ -1,18 +1,34 @@ + + From d1be61ec5e3c6299796eef110cc28114e7db0c1d Mon Sep 17 00:00:00 2001 From: julien huang Date: Fri, 23 Sep 2022 16:18:54 +0200 Subject: [PATCH 04/10] fix(nuxt): use createElementBlock for string child components --- packages/nuxt/src/app/components/client-only.mjs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/nuxt/src/app/components/client-only.mjs b/packages/nuxt/src/app/components/client-only.mjs index 14b44024e0c..a40183ff37b 100644 --- a/packages/nuxt/src/app/components/client-only.mjs +++ b/packages/nuxt/src/app/components/client-only.mjs @@ -30,9 +30,14 @@ export function createClientOnly (component) { if (clone.render) { // override the component render (non script setup component) clone.render = (ctx, ...args) => { - return ctx.mounted$ - ? h(component.render(ctx, ...args)) - : h('div', ctx.$attrs ?? ctx._.attrs) + if (ctx.mounted$) { + const res = component.render(ctx, ...args) + return (res.children === null || typeof res.children === 'string') + ? createElementBlock(res.type, res.props, res.children, res.patchFlag, res.dynamicProps, res.shapeFlag) + : h(res) + } else { + return h('div', ctx.attrs) + } } } else if (clone.template) { // handle runtime-compiler template From 569f3d485f6bca223062f171ecdbe8b6aeba1214 Mon Sep 17 00:00:00 2001 From: julien huang Date: Fri, 23 Sep 2022 16:33:01 +0200 Subject: [PATCH 05/10] fix(nuxt): fix attrs --- packages/nuxt/src/app/components/client-only.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxt/src/app/components/client-only.mjs b/packages/nuxt/src/app/components/client-only.mjs index a40183ff37b..e787f7f3492 100644 --- a/packages/nuxt/src/app/components/client-only.mjs +++ b/packages/nuxt/src/app/components/client-only.mjs @@ -36,7 +36,7 @@ export function createClientOnly (component) { ? createElementBlock(res.type, res.props, res.children, res.patchFlag, res.dynamicProps, res.shapeFlag) : h(res) } else { - return h('div', ctx.attrs) + return h('div', ctx.$attrs ?? ctx._.attrs) } } } else if (clone.template) { From 7beb94faa5329b0482df1dbe7eb3a408f58772da Mon Sep 17 00:00:00 2001 From: julien huang Date: Fri, 23 Sep 2022 16:43:37 +0200 Subject: [PATCH 06/10] test: lint + fix dev test + component ref test --- test/basic.test.ts | 7 ++++ .../basic/components/client/MultiRootNode.vue | 12 ++++--- .../components/client/MultiRootNodeScript.vue | 20 ++++++----- .../basic/components/client/Script.client.vue | 30 +++++++++------- .../components/client/SetupScript.client.vue | 12 ++++--- .../client/StringChildStateful.client.vue | 8 ++--- .../StringChildStatefulScript.client.vue | 16 ++++----- .../basic/pages/client-only-components.vue | 36 ++++++++++++++++--- 8 files changed, 96 insertions(+), 45 deletions(-) diff --git a/test/basic.test.ts b/test/basic.test.ts index f1bd5a71189..28df136ddcc 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -196,6 +196,13 @@ describe('pages', () => { expect(await page.locator('.multi-root-node-script-count').innerHTML()).toBe('1') expect(await page.locator('.client-only-script-setup button').innerHTML()).toBe('1') expect(await page.locator('.client-only-script button').innerHTML()).toBe('1') + + // ensure component ref is working and reactive + await page.locator('button.test-ref').click() + expect(await page.locator('.client-only-script-setup button').innerHTML()).toBe('2') + expect(await page.locator('.client-only-script button').innerHTML()).toBe('2') + expect(await page.locator('.string-stateful-script').innerHTML()).toBe('1') + expect(await page.locator('.string-stateful').innerHTML()).toBe('1') }) }) diff --git a/test/fixtures/basic/components/client/MultiRootNode.vue b/test/fixtures/basic/components/client/MultiRootNode.vue index f27a0f92caf..247ad18db1a 100644 --- a/test/fixtures/basic/components/client/MultiRootNode.vue +++ b/test/fixtures/basic/components/client/MultiRootNode.vue @@ -1,10 +1,14 @@ diff --git a/test/fixtures/basic/components/client/MultiRootNodeScript.vue b/test/fixtures/basic/components/client/MultiRootNodeScript.vue index cbc614b48e5..67ed503ca3d 100644 --- a/test/fixtures/basic/components/client/MultiRootNodeScript.vue +++ b/test/fixtures/basic/components/client/MultiRootNodeScript.vue @@ -1,15 +1,19 @@ diff --git a/test/fixtures/basic/components/client/Script.client.vue b/test/fixtures/basic/components/client/Script.client.vue index b21f8b5c8bf..86767ad9378 100644 --- a/test/fixtures/basic/components/client/Script.client.vue +++ b/test/fixtures/basic/components/client/Script.client.vue @@ -1,27 +1,33 @@ diff --git a/test/fixtures/basic/components/client/SetupScript.client.vue b/test/fixtures/basic/components/client/SetupScript.client.vue index edb9da630f3..becd6eb01e6 100644 --- a/test/fixtures/basic/components/client/SetupScript.client.vue +++ b/test/fixtures/basic/components/client/SetupScript.client.vue @@ -1,13 +1,17 @@