From b9469831a99616785f487a92b5f7c85ac601f41d Mon Sep 17 00:00:00 2001 From: julien huang Date: Fri, 28 Oct 2022 20:55:01 +0200 Subject: [PATCH 01/47] feat(nuxt): add ClientIfFail component --- .../src/app/components/client-if-fail.mjs | 39 +++++++++++++++++++ packages/nuxt/src/core/nuxt.ts | 6 +++ 2 files changed, 45 insertions(+) create mode 100644 packages/nuxt/src/app/components/client-if-fail.mjs diff --git a/packages/nuxt/src/app/components/client-if-fail.mjs b/packages/nuxt/src/app/components/client-if-fail.mjs new file mode 100644 index 00000000000..711c6d8eb88 --- /dev/null +++ b/packages/nuxt/src/app/components/client-if-fail.mjs @@ -0,0 +1,39 @@ +import { defineComponent, createElementBlock, onErrorCaptured } from 'vue' + +export default defineComponent({ + props: { + uid: { + type: String, + required: true + } + }, + setup (props, ctx) { + const slot = ctx.slots.default() + if (process.server) { + const error = ref(false) + + onErrorCaptured((_, instance) => { + error.value = true + + useState(`error_component_${props.uid}`, () => true) + // modify ssr render to force render a simple div + instance._.ssrRender = (_ctx, _push, _parent, _attrs) => { + _push('
') + } + return false + }) + return () => slot + } + const mounted = ref(false) + const ssrFailed = useState(`error_component_${props.uid}`) + + if (ssrFailed.value) { + onMounted(() => { mounted.value = true }) + } + return () => ssrFailed.value + ? mounted.value + ? slot + : slot.map(() => createElementBlock('div')) + : slot + } +}) diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts index a0145231736..5a1a6bbb56b 100644 --- a/packages/nuxt/src/core/nuxt.ts +++ b/packages/nuxt/src/core/nuxt.ts @@ -168,6 +168,12 @@ async function initNuxt (nuxt: Nuxt) { filePath: resolve(nuxt.options.appDir, 'components/nuxt-loading-indicator') }) + // Add + addComponent({ + name: 'ClientIfFail', + filePath: resolve(nuxt.options.appDir, 'components/client-if-fail') + }) + // Deprecate hooks nuxt.hooks.deprecateHooks({ 'autoImports:sources': { From df05c9908c881f80bc76cabd525dce74b56e2a5a Mon Sep 17 00:00:00 2001 From: julien huang Date: Sat, 15 Oct 2022 10:20:26 +0200 Subject: [PATCH 02/47] test(nuxt): add basic test for clientIfFail --- test/basic.test.ts | 10 +++++++++ .../basic/components/BreakInSetup.vue | 10 +++++++++ test/fixtures/basic/pages/client-if-fail.vue | 22 +++++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 test/fixtures/basic/components/BreakInSetup.vue create mode 100644 test/fixtures/basic/pages/client-if-fail.vue diff --git a/test/basic.test.ts b/test/basic.test.ts index 7d95f6712c1..f5dcdbcd4e0 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -236,6 +236,16 @@ describe('pages', () => { expect(html).not.toContain('client only script') await expectNoClientErrors('/client-only-explicit-import') }) + + it('client-if-fail', async () => { + const html = await $fetch('/client-if-fail') + // ensure failed components are not rendered server-side + expect(html).not.toContain('This breaks in server-side setup.') + // ensure not failed component should be rendered + expect(html).toContain('Sugar Counter') + + await expectNoClientErrors('/client-if-fail') + }) }) describe('head tags', () => { diff --git a/test/fixtures/basic/components/BreakInSetup.vue b/test/fixtures/basic/components/BreakInSetup.vue new file mode 100644 index 00000000000..8eef1408d05 --- /dev/null +++ b/test/fixtures/basic/components/BreakInSetup.vue @@ -0,0 +1,10 @@ + + + diff --git a/test/fixtures/basic/pages/client-if-fail.vue b/test/fixtures/basic/pages/client-if-fail.vue new file mode 100644 index 00000000000..3f959b832f4 --- /dev/null +++ b/test/fixtures/basic/pages/client-if-fail.vue @@ -0,0 +1,22 @@ + + + From 498bfa2347702ae66f231f5fc94a88059a5add58 Mon Sep 17 00:00:00 2001 From: julien huang Date: Sat, 15 Oct 2022 10:44:28 +0200 Subject: [PATCH 03/47] fix(nuxt): fix client-if-fail reactivity --- packages/nuxt/src/app/components/client-if-fail.mjs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/nuxt/src/app/components/client-if-fail.mjs b/packages/nuxt/src/app/components/client-if-fail.mjs index 711c6d8eb88..54ebe8a53d8 100644 --- a/packages/nuxt/src/app/components/client-if-fail.mjs +++ b/packages/nuxt/src/app/components/client-if-fail.mjs @@ -8,7 +8,6 @@ export default defineComponent({ } }, setup (props, ctx) { - const slot = ctx.slots.default() if (process.server) { const error = ref(false) @@ -22,7 +21,7 @@ export default defineComponent({ } return false }) - return () => slot + return () => ctx.slots.default() } const mounted = ref(false) const ssrFailed = useState(`error_component_${props.uid}`) @@ -32,8 +31,8 @@ export default defineComponent({ } return () => ssrFailed.value ? mounted.value - ? slot - : slot.map(() => createElementBlock('div')) - : slot + ? ctx.slots.default() + : ctx.slots.default().map(() => createElementBlock('div')) + : ctx.slots.default() } }) From 461a0d9a276c0f56ffd60fb7cc291a32ef6b8576 Mon Sep 17 00:00:00 2001 From: julien huang Date: Sat, 15 Oct 2022 10:51:47 +0200 Subject: [PATCH 04/47] test(basic): fix component name --- test/fixtures/basic/pages/client-if-fail.vue | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/fixtures/basic/pages/client-if-fail.vue b/test/fixtures/basic/pages/client-if-fail.vue index 3f959b832f4..fc93e29c63f 100644 --- a/test/fixtures/basic/pages/client-if-fail.vue +++ b/test/fixtures/basic/pages/client-if-fail.vue @@ -3,16 +3,16 @@ Hello World
- + - - - - - - + + + + + + - +
From 2e4ec63b051d7300b07bc23038dace83ae4ccc51 Mon Sep 17 00:00:00 2001 From: julien huang Date: Sat, 15 Oct 2022 13:59:28 +0200 Subject: [PATCH 05/47] test(basic): fix tests --- test/basic.test.ts | 8 +++++++- test/fixtures/basic/pages/client-if-fail.vue | 5 ++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/test/basic.test.ts b/test/basic.test.ts index f5dcdbcd4e0..ed7f971c204 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -242,9 +242,15 @@ describe('pages', () => { // ensure failed components are not rendered server-side expect(html).not.toContain('This breaks in server-side setup.') // ensure not failed component should be rendered - expect(html).toContain('Sugar Counter') + expect(html).toContain('Sugar Counter 0 x 2 = 0') await expectNoClientErrors('/client-if-fail') + + const page = await createPage('/client-if-fail') + await page.waitForLoadState('networkidle') + // ensure components reactivity + await page.locator('#increment-count').click() + expect(await page.locator('#sugar-counter').innerHTML()).toContain('Sugar Counter 1 x 2 = 2') }) }) diff --git a/test/fixtures/basic/pages/client-if-fail.vue b/test/fixtures/basic/pages/client-if-fail.vue index fc93e29c63f..00770998750 100644 --- a/test/fixtures/basic/pages/client-if-fail.vue +++ b/test/fixtures/basic/pages/client-if-fail.vue @@ -11,9 +11,12 @@
- + + From 243791b315d7c1c9f1e4c48548839e0267ee0f99 Mon Sep 17 00:00:00 2001 From: julien huang Date: Sat, 15 Oct 2022 14:07:03 +0200 Subject: [PATCH 06/47] fix(nuxt): allow empty slot for client-if-fail --- packages/nuxt/src/app/components/client-if-fail.mjs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/nuxt/src/app/components/client-if-fail.mjs b/packages/nuxt/src/app/components/client-if-fail.mjs index 54ebe8a53d8..c68b0782405 100644 --- a/packages/nuxt/src/app/components/client-if-fail.mjs +++ b/packages/nuxt/src/app/components/client-if-fail.mjs @@ -21,7 +21,7 @@ export default defineComponent({ } return false }) - return () => ctx.slots.default() + return () => ctx.slots.default?.() } const mounted = ref(false) const ssrFailed = useState(`error_component_${props.uid}`) @@ -31,8 +31,8 @@ export default defineComponent({ } return () => ssrFailed.value ? mounted.value - ? ctx.slots.default() - : ctx.slots.default().map(() => createElementBlock('div')) - : ctx.slots.default() + ? ctx.slots.default?.() + : ctx.slots.default?.().map(() => createElementBlock('div')) + : ctx.slots.default?.() } }) From 7ca9d4c1d47522237ff0b6d26b023c3878b8b8d4 Mon Sep 17 00:00:00 2001 From: julien huang Date: Sat, 15 Oct 2022 16:43:46 +0200 Subject: [PATCH 07/47] fix(nuxt): pass missing attributes to client-if-fail ssr divs --- .../nuxt/src/app/components/client-if-fail.mjs | 15 ++++++++------- test/basic.test.ts | 2 ++ test/fixtures/basic/pages/client-if-fail.vue | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/nuxt/src/app/components/client-if-fail.mjs b/packages/nuxt/src/app/components/client-if-fail.mjs index c68b0782405..47c393a53e7 100644 --- a/packages/nuxt/src/app/components/client-if-fail.mjs +++ b/packages/nuxt/src/app/components/client-if-fail.mjs @@ -1,4 +1,5 @@ import { defineComponent, createElementBlock, onErrorCaptured } from 'vue' +import { ssrRenderAttrs } from 'vue/server-renderer' export default defineComponent({ props: { @@ -7,7 +8,7 @@ export default defineComponent({ required: true } }, - setup (props, ctx) { + setup (props, { slots }) { if (process.server) { const error = ref(false) @@ -17,11 +18,11 @@ export default defineComponent({ useState(`error_component_${props.uid}`, () => true) // modify ssr render to force render a simple div instance._.ssrRender = (_ctx, _push, _parent, _attrs) => { - _push('
') + _push(``) } return false }) - return () => ctx.slots.default?.() + return () => slots.default?.() } const mounted = ref(false) const ssrFailed = useState(`error_component_${props.uid}`) @@ -29,10 +30,10 @@ export default defineComponent({ if (ssrFailed.value) { onMounted(() => { mounted.value = true }) } - return () => ssrFailed.value + return ctx => ssrFailed.value ? mounted.value - ? ctx.slots.default?.() - : ctx.slots.default?.().map(() => createElementBlock('div')) - : ctx.slots.default?.() + ? slots.default?.() + : slots.default?.().map(() => createElementBlock('div', ctx.$attrs)) + : slots.default?.() } }) diff --git a/test/basic.test.ts b/test/basic.test.ts index ed7f971c204..1b3775ba2f1 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -243,6 +243,8 @@ describe('pages', () => { expect(html).not.toContain('This breaks in server-side setup.') // ensure not failed component should be rendered expect(html).toContain('Sugar Counter 0 x 2 = 0') + // ensure failed component render a div with attributes + expect(html).toContain('
') await expectNoClientErrors('/client-if-fail') diff --git a/test/fixtures/basic/pages/client-if-fail.vue b/test/fixtures/basic/pages/client-if-fail.vue index 00770998750..ac7338d0176 100644 --- a/test/fixtures/basic/pages/client-if-fail.vue +++ b/test/fixtures/basic/pages/client-if-fail.vue @@ -6,7 +6,7 @@ - + From a5fbfc7c30c8d67bde88fa6f8f19155650e1f428 Mon Sep 17 00:00:00 2001 From: julien huang Date: Sat, 15 Oct 2022 16:58:10 +0200 Subject: [PATCH 08/47] docs(components): add ClientIfFail docs --- docs/content/2.guide/2.directory-structure/1.components.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/content/2.guide/2.directory-structure/1.components.md b/docs/content/2.guide/2.directory-structure/1.components.md index 6149e8b7d2f..f9ec24e26b4 100644 --- a/docs/content/2.guide/2.directory-structure/1.components.md +++ b/docs/content/2.guide/2.directory-structure/1.components.md @@ -243,6 +243,10 @@ The content will not be included in production builds and tree-shaken. ``` +## Component + +Nuxt provides the `` component to render its child components if theses fails to render in SSR. + ## Library Authors Making Vue component libraries with automatic tree-shaking and component registration is super easy ✨ From 092da307a6607cb9e535064f2ea084ff6ca52a10 Mon Sep 17 00:00:00 2001 From: julien huang Date: Sat, 15 Oct 2022 17:11:10 +0200 Subject: [PATCH 09/47] feat(nuxt): add ssr-error event to ClientIfFail --- packages/nuxt/src/app/components/client-if-fail.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/nuxt/src/app/components/client-if-fail.mjs b/packages/nuxt/src/app/components/client-if-fail.mjs index 47c393a53e7..9ca8b2362d6 100644 --- a/packages/nuxt/src/app/components/client-if-fail.mjs +++ b/packages/nuxt/src/app/components/client-if-fail.mjs @@ -8,7 +8,8 @@ export default defineComponent({ required: true } }, - setup (props, { slots }) { + emits: ['ssr-error'], + setup (props, { slots, emit }) { if (process.server) { const error = ref(false) @@ -20,6 +21,7 @@ export default defineComponent({ instance._.ssrRender = (_ctx, _push, _parent, _attrs) => { _push(``) } + emit('ssr-error', instance) return false }) return () => slots.default?.() From fa472629fbcc2ff3e7fcba811e73608d9f5214c8 Mon Sep 17 00:00:00 2001 From: julien huang Date: Sat, 15 Oct 2022 20:06:05 +0200 Subject: [PATCH 10/47] feat(nuxt): auto uid for ClientIfFail --- .../src/app/components/client-if-fail.mjs | 7 +- .../src/components/client-if-fail-auto-id.ts | 76 +++++++++++++++++++ packages/nuxt/src/components/module.ts | 9 +++ 3 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 packages/nuxt/src/components/client-if-fail-auto-id.ts diff --git a/packages/nuxt/src/app/components/client-if-fail.mjs b/packages/nuxt/src/app/components/client-if-fail.mjs index 9ca8b2362d6..0a8feda008b 100644 --- a/packages/nuxt/src/app/components/client-if-fail.mjs +++ b/packages/nuxt/src/app/components/client-if-fail.mjs @@ -4,8 +4,7 @@ import { ssrRenderAttrs } from 'vue/server-renderer' export default defineComponent({ props: { uid: { - type: String, - required: true + type: String } }, emits: ['ssr-error'], @@ -16,7 +15,7 @@ export default defineComponent({ onErrorCaptured((_, instance) => { error.value = true - useState(`error_component_${props.uid}`, () => true) + useState(`${props.uid}`, () => true) // modify ssr render to force render a simple div instance._.ssrRender = (_ctx, _push, _parent, _attrs) => { _push(``) @@ -27,7 +26,7 @@ export default defineComponent({ return () => slots.default?.() } const mounted = ref(false) - const ssrFailed = useState(`error_component_${props.uid}`) + const ssrFailed = useState(`${props.uid}`) if (ssrFailed.value) { onMounted(() => { mounted.value = true }) diff --git a/packages/nuxt/src/components/client-if-fail-auto-id.ts b/packages/nuxt/src/components/client-if-fail-auto-id.ts new file mode 100644 index 00000000000..9a40d1bd5f6 --- /dev/null +++ b/packages/nuxt/src/components/client-if-fail-auto-id.ts @@ -0,0 +1,76 @@ +import { pathToFileURL } from 'node:url' +import { createUnplugin } from 'unplugin' +import { parseQuery, parseURL } from 'ufo' +import { ComponentsOptions } from '@nuxt/schema' +import MagicString from 'magic-string' +import { isAbsolute, relative } from 'pathe' +import { hash } from 'ohash' + +interface LoaderOptions { + sourcemap?: boolean + transform?: ComponentsOptions['transform'], + rootDir: string +} + +function isVueTemplate (id: string) { + // Bare `.vue` file (in Vite) + if (id.endsWith('.vue')) { + return true + } + + const { search } = parseURL(decodeURIComponent(pathToFileURL(id).href)) + if (!search) { + return false + } + + const query = parseQuery(search) + + // Macro + if (query.macro) { + return true + } + + // Non-Vue or Styles + if (!('vue' in query) || query.type === 'style') { + return false + } + + // Query `?vue&type=template` (in Webpack or external template) + return true +} + +export const clientIfFailPlugin = createUnplugin((options: LoaderOptions) => { + const exclude = options.transform?.exclude || [] + const include = options.transform?.include || [] + + return { + name: 'nuxt:client-if-fail-auto-id', + enforce: 'pre', + transformInclude (id) { + if (exclude.some(pattern => id.match(pattern))) { + return false + } + if (include.some(pattern => id.match(pattern))) { + return true + } + return isVueTemplate(id) + }, + transform (code, id) { + const s = new MagicString(code) + const relativeID = isAbsolute(id) ? relative(options.rootDir, id) : id + + s.replace(/<([cC]lient-?[iI]f-?[fF]ail)(.*)>/g, (full, name, attrs, offset) => { + return `<${name}${attrs} uid="${'$' + hash(`${relativeID}-${offset}`)}">` + }) + + if (s.hasChanged()) { + return { + code: s.toString(), + map: options.sourcemap + ? s.generateMap({ source: id, includeContent: true }) + : undefined + } + } + } + } +}) diff --git a/packages/nuxt/src/components/module.ts b/packages/nuxt/src/components/module.ts index 6f4b9419d73..54dab6e812d 100644 --- a/packages/nuxt/src/components/module.ts +++ b/packages/nuxt/src/components/module.ts @@ -3,6 +3,7 @@ import { relative, resolve } from 'pathe' import { defineNuxtModule, resolveAlias, addTemplate, addPluginTemplate, updateTemplates } from '@nuxt/kit' import type { Component, ComponentsDir, ComponentsOptions } from '@nuxt/schema' import { distDir } from '../dirs' +import { clientIfFailPlugin } from './client-if-fail-auto-id' import { componentsPluginTemplate, componentsTemplate, componentsTypeTemplate } from './templates' import { scanComponents } from './scan' import { loaderPlugin } from './loader' @@ -193,6 +194,10 @@ export default defineNuxtModule({ getComponents, mode })) + config.plugins.push(clientIfFailPlugin.vite({ + sourcemap: nuxt.options.sourcemap[mode], + rootDir: nuxt.options.rootDir + })) if (nuxt.options.experimental.treeshakeClientOnly && isServer) { config.plugins.push(TreeShakeTemplatePlugin.vite({ sourcemap: nuxt.options.sourcemap[mode], @@ -209,6 +214,10 @@ export default defineNuxtModule({ getComponents, mode })) + config.plugins.push(clientIfFailPlugin.webpack({ + sourcemap: nuxt.options.sourcemap[mode], + rootDir: nuxt.options.rootDir + })) if (nuxt.options.experimental.treeshakeClientOnly && mode === 'server') { config.plugins.push(TreeShakeTemplatePlugin.webpack({ sourcemap: nuxt.options.sourcemap[mode], From 5541a775f8b7ad4e29fa28159788a39b6b61be27 Mon Sep 17 00:00:00 2001 From: julien huang Date: Sat, 15 Oct 2022 20:08:51 +0200 Subject: [PATCH 11/47] test(nuxt): remove uid attrs from --- test/fixtures/basic/pages/client-if-fail.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/fixtures/basic/pages/client-if-fail.vue b/test/fixtures/basic/pages/client-if-fail.vue index ac7338d0176..4b3090830e7 100644 --- a/test/fixtures/basic/pages/client-if-fail.vue +++ b/test/fixtures/basic/pages/client-if-fail.vue @@ -2,14 +2,14 @@
Hello World
- + - + - + From 529bdac301f5f019014fb433b9032fa81bb21aef Mon Sep 17 00:00:00 2001 From: julien huang Date: Sat, 15 Oct 2022 20:26:12 +0200 Subject: [PATCH 12/47] docs(nuxt): update doc + add ClientIfFail api --- .../2.directory-structure/1.components.md | 15 +++++++++++++- .../3.api/2.components/8.client-if-fail.md | 20 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 docs/content/3.api/2.components/8.client-if-fail.md diff --git a/docs/content/2.guide/2.directory-structure/1.components.md b/docs/content/2.guide/2.directory-structure/1.components.md index f9ec24e26b4..0bda3861dbb 100644 --- a/docs/content/2.guide/2.directory-structure/1.components.md +++ b/docs/content/2.guide/2.directory-structure/1.components.md @@ -245,7 +245,20 @@ The content will not be included in production builds and tree-shaken. ## Component -Nuxt provides the `` component to render its child components if theses fails to render in SSR. +Nuxt provides the `` component to render its children on client side if some of them triggers an error in SSR. + +```html{}[pages/example.vue] + +``` ## Library Authors diff --git a/docs/content/3.api/2.components/8.client-if-fail.md b/docs/content/3.api/2.components/8.client-if-fail.md new file mode 100644 index 00000000000..112825d9a6e --- /dev/null +++ b/docs/content/3.api/2.components/8.client-if-fail.md @@ -0,0 +1,20 @@ +--- +title: "" +description: "Nuxt provides `` component to render its children on client side if some of them triggers an error in SSR" +--- + +# `` + +Nuxt provides `` component to render its children on client side if some of them triggers an error in SSR. + +## Events + +- **`@ssr-error`**: Event emitted when a children triggers an error in SSR. + + ```vue + + ``` From 60f582d0c58d1d689d342af541cdb96e41af72d8 Mon Sep 17 00:00:00 2001 From: julien huang Date: Sun, 16 Oct 2022 00:15:48 +0200 Subject: [PATCH 13/47] refactor(nuxt): remove duplicated code --- .../src/components/client-if-fail-auto-id.ts | 30 +------------------ packages/nuxt/src/components/helpers.ts | 29 ++++++++++++++++++ packages/nuxt/src/components/loader.ts | 30 +------------------ 3 files changed, 31 insertions(+), 58 deletions(-) create mode 100644 packages/nuxt/src/components/helpers.ts diff --git a/packages/nuxt/src/components/client-if-fail-auto-id.ts b/packages/nuxt/src/components/client-if-fail-auto-id.ts index 9a40d1bd5f6..0eaafd324b0 100644 --- a/packages/nuxt/src/components/client-if-fail-auto-id.ts +++ b/packages/nuxt/src/components/client-if-fail-auto-id.ts @@ -1,10 +1,9 @@ -import { pathToFileURL } from 'node:url' import { createUnplugin } from 'unplugin' -import { parseQuery, parseURL } from 'ufo' import { ComponentsOptions } from '@nuxt/schema' import MagicString from 'magic-string' import { isAbsolute, relative } from 'pathe' import { hash } from 'ohash' +import { isVueTemplate } from './helpers' interface LoaderOptions { sourcemap?: boolean @@ -12,33 +11,6 @@ interface LoaderOptions { rootDir: string } -function isVueTemplate (id: string) { - // Bare `.vue` file (in Vite) - if (id.endsWith('.vue')) { - return true - } - - const { search } = parseURL(decodeURIComponent(pathToFileURL(id).href)) - if (!search) { - return false - } - - const query = parseQuery(search) - - // Macro - if (query.macro) { - return true - } - - // Non-Vue or Styles - if (!('vue' in query) || query.type === 'style') { - return false - } - - // Query `?vue&type=template` (in Webpack or external template) - return true -} - export const clientIfFailPlugin = createUnplugin((options: LoaderOptions) => { const exclude = options.transform?.exclude || [] const include = options.transform?.include || [] diff --git a/packages/nuxt/src/components/helpers.ts b/packages/nuxt/src/components/helpers.ts new file mode 100644 index 00000000000..ebd4ccf5fe9 --- /dev/null +++ b/packages/nuxt/src/components/helpers.ts @@ -0,0 +1,29 @@ +import { pathToFileURL } from 'node:url' +import { parseQuery, parseURL } from 'ufo' + +export function isVueTemplate (id: string) { + // Bare `.vue` file (in Vite) + if (id.endsWith('.vue')) { + return true + } + + const { search } = parseURL(decodeURIComponent(pathToFileURL(id).href)) + if (!search) { + return false + } + + const query = parseQuery(search) + + // Macro + if (query.macro) { + return true + } + + // Non-Vue or Styles + if (!('vue' in query) || query.type === 'style') { + return false + } + + // Query `?vue&type=template` (in Webpack or external template) + return true +} diff --git a/packages/nuxt/src/components/loader.ts b/packages/nuxt/src/components/loader.ts index 452fc7025c0..9d6e2c545e0 100644 --- a/packages/nuxt/src/components/loader.ts +++ b/packages/nuxt/src/components/loader.ts @@ -1,10 +1,9 @@ -import { pathToFileURL } from 'node:url' import { createUnplugin } from 'unplugin' -import { parseQuery, parseURL } from 'ufo' import { Component, ComponentsOptions } from '@nuxt/schema' import { genDynamicImport, genImport } from 'knitwork' import MagicString from 'magic-string' import { pascalCase } from 'scule' +import { isVueTemplate } from './helpers' interface LoaderOptions { getComponents (): Component[] @@ -13,33 +12,6 @@ interface LoaderOptions { transform?: ComponentsOptions['transform'] } -function isVueTemplate (id: string) { - // Bare `.vue` file (in Vite) - if (id.endsWith('.vue')) { - return true - } - - const { search } = parseURL(decodeURIComponent(pathToFileURL(id).href)) - if (!search) { - return false - } - - const query = parseQuery(search) - - // Macro - if (query.macro) { - return true - } - - // Non-Vue or Styles - if (!('vue' in query) || query.type === 'style') { - return false - } - - // Query `?vue&type=template` (in Webpack or external template) - return true -} - export const loaderPlugin = createUnplugin((options: LoaderOptions) => { const exclude = options.transform?.exclude || [] const include = options.transform?.include || [] From a8ede69165996b89c1f7d7b99dc24fc378ced608 Mon Sep 17 00:00:00 2001 From: julien huang Date: Sun, 16 Oct 2022 20:07:57 +0200 Subject: [PATCH 14/47] refactor(nuxt): rename `` to `` --- .../2.directory-structure/1.components.md | 8 ++++---- .../3.api/2.components/8.client-fallback.md | 20 +++++++++++++++++++ .../3.api/2.components/8.client-if-fail.md | 20 ------------------- ...client-if-fail.mjs => client-fallback.mjs} | 0 ...-auto-id.ts => client-fallback-auto-id.ts} | 4 ++-- packages/nuxt/src/components/module.ts | 6 +++--- packages/nuxt/src/core/nuxt.ts | 6 +++--- test/basic.test.ts | 8 ++++---- ...client-if-fail.vue => client-fallback.vue} | 12 +++++------ 9 files changed, 42 insertions(+), 42 deletions(-) create mode 100644 docs/content/3.api/2.components/8.client-fallback.md delete mode 100644 docs/content/3.api/2.components/8.client-if-fail.md rename packages/nuxt/src/app/components/{client-if-fail.mjs => client-fallback.mjs} (100%) rename packages/nuxt/src/components/{client-if-fail-auto-id.ts => client-fallback-auto-id.ts} (90%) rename test/fixtures/basic/pages/{client-if-fail.vue => client-fallback.vue} (74%) diff --git a/docs/content/2.guide/2.directory-structure/1.components.md b/docs/content/2.guide/2.directory-structure/1.components.md index 0bda3861dbb..69735171022 100644 --- a/docs/content/2.guide/2.directory-structure/1.components.md +++ b/docs/content/2.guide/2.directory-structure/1.components.md @@ -243,19 +243,19 @@ The content will not be included in production builds and tree-shaken. ``` -## Component +## Component -Nuxt provides the `` component to render its children on client side if some of them triggers an error in SSR. +Nuxt provides the `` component to render its children on client side if some of them triggers an error in SSR. ```html{}[pages/example.vue] ``` diff --git a/docs/content/3.api/2.components/8.client-fallback.md b/docs/content/3.api/2.components/8.client-fallback.md new file mode 100644 index 00000000000..fd2a1640f4c --- /dev/null +++ b/docs/content/3.api/2.components/8.client-fallback.md @@ -0,0 +1,20 @@ +--- +title: "" +description: "Nuxt provides `` component to render its children on client side if some of them triggers an error in SSR" +--- + +# `` + +Nuxt provides `` component to render its children on client side if some of them triggers an error in SSR. + +## Events + +- **`@ssr-error`**: Event emitted when a children triggers an error in SSR. + + ```vue + + ``` diff --git a/docs/content/3.api/2.components/8.client-if-fail.md b/docs/content/3.api/2.components/8.client-if-fail.md deleted file mode 100644 index 112825d9a6e..00000000000 --- a/docs/content/3.api/2.components/8.client-if-fail.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: "" -description: "Nuxt provides `` component to render its children on client side if some of them triggers an error in SSR" ---- - -# `` - -Nuxt provides `` component to render its children on client side if some of them triggers an error in SSR. - -## Events - -- **`@ssr-error`**: Event emitted when a children triggers an error in SSR. - - ```vue - - ``` diff --git a/packages/nuxt/src/app/components/client-if-fail.mjs b/packages/nuxt/src/app/components/client-fallback.mjs similarity index 100% rename from packages/nuxt/src/app/components/client-if-fail.mjs rename to packages/nuxt/src/app/components/client-fallback.mjs diff --git a/packages/nuxt/src/components/client-if-fail-auto-id.ts b/packages/nuxt/src/components/client-fallback-auto-id.ts similarity index 90% rename from packages/nuxt/src/components/client-if-fail-auto-id.ts rename to packages/nuxt/src/components/client-fallback-auto-id.ts index 0eaafd324b0..e9fbee7bbf6 100644 --- a/packages/nuxt/src/components/client-if-fail-auto-id.ts +++ b/packages/nuxt/src/components/client-fallback-auto-id.ts @@ -11,12 +11,12 @@ interface LoaderOptions { rootDir: string } -export const clientIfFailPlugin = createUnplugin((options: LoaderOptions) => { +export const clientFallbackAutoIdPlugin = createUnplugin((options: LoaderOptions) => { const exclude = options.transform?.exclude || [] const include = options.transform?.include || [] return { - name: 'nuxt:client-if-fail-auto-id', + name: 'nuxt:client-fallback-auto-id', enforce: 'pre', transformInclude (id) { if (exclude.some(pattern => id.match(pattern))) { diff --git a/packages/nuxt/src/components/module.ts b/packages/nuxt/src/components/module.ts index 54dab6e812d..a624db8b74f 100644 --- a/packages/nuxt/src/components/module.ts +++ b/packages/nuxt/src/components/module.ts @@ -3,7 +3,7 @@ import { relative, resolve } from 'pathe' import { defineNuxtModule, resolveAlias, addTemplate, addPluginTemplate, updateTemplates } from '@nuxt/kit' import type { Component, ComponentsDir, ComponentsOptions } from '@nuxt/schema' import { distDir } from '../dirs' -import { clientIfFailPlugin } from './client-if-fail-auto-id' +import { clientFallbackAutoIdPlugin } from './client-fallback-auto-id' import { componentsPluginTemplate, componentsTemplate, componentsTypeTemplate } from './templates' import { scanComponents } from './scan' import { loaderPlugin } from './loader' @@ -194,7 +194,7 @@ export default defineNuxtModule({ getComponents, mode })) - config.plugins.push(clientIfFailPlugin.vite({ + config.plugins.push(clientFallbackAutoIdPlugin.vite({ sourcemap: nuxt.options.sourcemap[mode], rootDir: nuxt.options.rootDir })) @@ -214,7 +214,7 @@ export default defineNuxtModule({ getComponents, mode })) - config.plugins.push(clientIfFailPlugin.webpack({ + config.plugins.push(clientFallbackAutoIdPlugin.webpack({ sourcemap: nuxt.options.sourcemap[mode], rootDir: nuxt.options.rootDir })) diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts index 5a1a6bbb56b..17c9e54ec40 100644 --- a/packages/nuxt/src/core/nuxt.ts +++ b/packages/nuxt/src/core/nuxt.ts @@ -168,10 +168,10 @@ async function initNuxt (nuxt: Nuxt) { filePath: resolve(nuxt.options.appDir, 'components/nuxt-loading-indicator') }) - // Add + // Add addComponent({ - name: 'ClientIfFail', - filePath: resolve(nuxt.options.appDir, 'components/client-if-fail') + name: 'ClientFallback', + filePath: resolve(nuxt.options.appDir, 'components/client-fallback') }) // Deprecate hooks diff --git a/test/basic.test.ts b/test/basic.test.ts index 1b3775ba2f1..6dba893600a 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -237,8 +237,8 @@ describe('pages', () => { await expectNoClientErrors('/client-only-explicit-import') }) - it('client-if-fail', async () => { - const html = await $fetch('/client-if-fail') + it('client-fallback', async () => { + const html = await $fetch('/client-fallback') // ensure failed components are not rendered server-side expect(html).not.toContain('This breaks in server-side setup.') // ensure not failed component should be rendered @@ -246,9 +246,9 @@ describe('pages', () => { // ensure failed component render a div with attributes expect(html).toContain('
') - await expectNoClientErrors('/client-if-fail') + await expectNoClientErrors('/client-fallback') - const page = await createPage('/client-if-fail') + const page = await createPage('/client-fallback') await page.waitForLoadState('networkidle') // ensure components reactivity await page.locator('#increment-count').click() diff --git a/test/fixtures/basic/pages/client-if-fail.vue b/test/fixtures/basic/pages/client-fallback.vue similarity index 74% rename from test/fixtures/basic/pages/client-if-fail.vue rename to test/fixtures/basic/pages/client-fallback.vue index 4b3090830e7..a0925a91413 100644 --- a/test/fixtures/basic/pages/client-if-fail.vue +++ b/test/fixtures/basic/pages/client-fallback.vue @@ -2,17 +2,17 @@
Hello World
- + - - + + - - + + - +
`) + _push(`<${props.fallbackTag}${ssrRenderAttrs(_attrs)}>`) } emit('ssr-error', instance) return false @@ -34,7 +38,7 @@ export default defineComponent({ return ctx => ssrFailed.value ? mounted.value ? slots.default?.() - : slots.default?.().map(() => createElementBlock('div', ctx.$attrs)) + : slots.default?.().map(() => createElementBlock(props.fallbackTag, ctx.$attrs)) : slots.default?.() } }) From c442b497c5ee4705dd25301d58cb4ddaae814935 Mon Sep 17 00:00:00 2001 From: julien huang Date: Wed, 19 Oct 2022 20:45:26 +0200 Subject: [PATCH 18/47] fix(nuxt): fix client-fallback-auto-id plugin --- packages/nuxt/src/components/client-fallback-auto-id.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nuxt/src/components/client-fallback-auto-id.ts b/packages/nuxt/src/components/client-fallback-auto-id.ts index 9fbe514ee18..23f12b66166 100644 --- a/packages/nuxt/src/components/client-fallback-auto-id.ts +++ b/packages/nuxt/src/components/client-fallback-auto-id.ts @@ -36,13 +36,13 @@ export const clientFallbackAutoIdPlugin = createUnplugin((options: LoaderOptions let hasClientFallback = false let count = 0 const uidkey = 'clientFallbackUid$' - + const isSFCRender = code.includes('function _sfc_render(') || code.includes('function _sfc_ssrRender(') s.replace(/(_createVNode|_ssrRenderComponent)\((.*[cC]lient-?[fF]allback),(.*),/g, (full, renderFunction, name, props) => { hasClientFallback = true // slice to remove object curly braces {} const oldProps = props.trim() !== 'null' ? props.trim().slice(1, -1) : '' // generate string to include the uidkey into the component props - const newProps = `{ uid: $setup.${uidkey} + '${count}'${oldProps ? `, ${oldProps}` : ''} }` + const newProps = `{ uid: ${isSFCRender ? '$setup.' : ''}${uidkey} + '${count}'${oldProps ? `, ${oldProps}` : ''} }` count++ return `${renderFunction}(${name}, ${newProps} ,` }) From cd143d77eadd90d014d8c3d6e94d030d1b9a8136 Mon Sep 17 00:00:00 2001 From: julien huang Date: Wed, 19 Oct 2022 22:27:47 +0200 Subject: [PATCH 19/47] fix(nuxt): workaround for webpack to avoid transform multiple times --- packages/nuxt/src/components/client-fallback-auto-id.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/nuxt/src/components/client-fallback-auto-id.ts b/packages/nuxt/src/components/client-fallback-auto-id.ts index 23f12b66166..ae28f81aec8 100644 --- a/packages/nuxt/src/components/client-fallback-auto-id.ts +++ b/packages/nuxt/src/components/client-fallback-auto-id.ts @@ -31,12 +31,17 @@ export const clientFallbackAutoIdPlugin = createUnplugin((options: LoaderOptions transform (code, id) { const s = new MagicString(code) const relativeID = isAbsolute(id) ? relative(options.rootDir, id) : id - const imports = new Set() + + const uidkey = 'clientFallbackUid$' + + // webpack workaround -- don't transform if already transformed + if (code.includes(uidkey)) { return } + let hasClientFallback = false let count = 0 - const uidkey = 'clientFallbackUid$' const isSFCRender = code.includes('function _sfc_render(') || code.includes('function _sfc_ssrRender(') + s.replace(/(_createVNode|_ssrRenderComponent)\((.*[cC]lient-?[fF]allback),(.*),/g, (full, renderFunction, name, props) => { hasClientFallback = true // slice to remove object curly braces {} From 09dca528b484fbd08eaee8d14548bfb33d321fdd Mon Sep 17 00:00:00 2001 From: julien huang Date: Fri, 21 Oct 2022 00:43:47 +0200 Subject: [PATCH 20/47] fix(nuxt): fix feature to dev/prod/webpack --- .../2.directory-structure/1.components.md | 7 +- .../3.api/2.components/8.client-fallback.md | 13 +++ .../src/app/components/client-fallback.mjs | 88 +++++++++++++++---- .../src/components/client-fallback-auto-id.ts | 8 +- test/basic.test.ts | 14 +-- test/fixtures/basic/pages/client-fallback.vue | 16 +++- 6 files changed, 116 insertions(+), 30 deletions(-) diff --git a/docs/content/2.guide/2.directory-structure/1.components.md b/docs/content/2.guide/2.directory-structure/1.components.md index 69735171022..07fd381e523 100644 --- a/docs/content/2.guide/2.directory-structure/1.components.md +++ b/docs/content/2.guide/2.directory-structure/1.components.md @@ -245,15 +245,16 @@ The content will not be included in production builds and tree-shaken. ## Component -Nuxt provides the `` component to render its children on client side if some of them triggers an error in SSR. +Nuxt provides the `` component to render its slot in client-side if it fails to render in ssr. +You can specify a `fallbackTag` to `` to make it render a specific tag if it fails to render in ssr. ```html{}[pages/example.vue] ``` + +## Props + +- **#fallbackTag**: Specify a fallback tag to be rendered if the slot fails to render. + + ```vue + + ``` \ No newline at end of file diff --git a/packages/nuxt/src/app/components/client-fallback.mjs b/packages/nuxt/src/app/components/client-fallback.mjs index b472469aa0a..493333f1abe 100644 --- a/packages/nuxt/src/app/components/client-fallback.mjs +++ b/packages/nuxt/src/app/components/client-fallback.mjs @@ -1,7 +1,38 @@ -import { defineComponent, createElementBlock, onErrorCaptured } from 'vue' -import { ssrRenderAttrs } from 'vue/server-renderer' +import { defineComponent, createElementBlock, getCurrentInstance, onErrorCaptured, createVNode } from 'vue' +import { isPromise, isArray, isString } from '@vue/shared' +import { ssrRenderVNode, ssrRenderAttrs } from 'vue/server-renderer' + +/** + * create buffer retrieved from https://github.com/vuejs/core/blob/9617dd4b2abc07a5dc40de6e5b759e851b4d0da1/packages/server-renderer/src/render.ts#L57 + */ +function createBuffer () { + let appendable = false + const buffer = [] + return { + getBuffer () { + // Return static buffer and await on items during unroll stage + return buffer + }, + push (item) { + const isStringItem = isString(item) + if (appendable && isStringItem) { + buffer[buffer.length - 1] += item + } else { + buffer.push(item) + } + appendable = isStringItem + if (isPromise(item) || (isArray(item) && item.hasAsync)) { + // promise, or child buffer with async, mark as async. + // this allows skipping unnecessary await ticks during unroll stage + buffer.hasAsync = true + } + } + } +} export default defineComponent({ + name: 'ClientFallback', + inheritAttrs: false, props: { uid: { type: String @@ -12,33 +43,56 @@ export default defineComponent({ } }, emits: ['ssr-error'], - setup (props, { slots, emit }) { + setup (props, ctx) { if (process.server) { - const error = ref(false) - - onErrorCaptured((_, instance) => { - error.value = true + const vm = getCurrentInstance() + const ssrFailed = ref(false) + onErrorCaptured(() => { useState(`${props.uid}`, () => true) - // modify ssr render to force render a simple div - instance._.ssrRender = (_ctx, _push, _parent, _attrs) => { - _push(`<${props.fallbackTag}${ssrRenderAttrs(_attrs)}>`) - } - emit('ssr-error', instance) + ssrFailed.value = true + ctx.emit('ssr-error') return false }) - return () => slots.default?.() + + try { + const defaultSlot = ctx.slots.default?.() + const ssrVNodes = createBuffer() + + for (let i = 0; i < defaultSlot.length; i++) { + ssrRenderVNode(ssrVNodes.push, defaultSlot[i], vm) + } + + return { ssrFailed, ssrVNodes } + } catch (error) { + // catch in dev + useState(`${props.uid}`, () => true) + ctx.emit('ssr-error') + return { ssrFailed: true, ssrVNodes: [] } + } } + const mounted = ref(false) const ssrFailed = useState(`${props.uid}`) if (ssrFailed.value) { onMounted(() => { mounted.value = true }) } - return ctx => ssrFailed.value + + return () => ssrFailed.value ? mounted.value - ? slots.default?.() - : slots.default?.().map(() => createElementBlock(props.fallbackTag, ctx.$attrs)) - : slots.default?.() + ? ctx.slots.default?.() + : createElementBlock(props.fallbackTag, ctx.attrs) + : ctx.slots.default?.() + }, + ssrRender (ctx, push) { + if (ctx.ssrFailed) { + push(`<${ctx.fallbackTag}${ssrRenderAttrs(ctx.$attrs)}>`) + } else { + // push Fragment markup + push('') + push(ctx.ssrVNodes.getBuffer()) + push('') + } } }) diff --git a/packages/nuxt/src/components/client-fallback-auto-id.ts b/packages/nuxt/src/components/client-fallback-auto-id.ts index ae28f81aec8..b3b40ea4325 100644 --- a/packages/nuxt/src/components/client-fallback-auto-id.ts +++ b/packages/nuxt/src/components/client-fallback-auto-id.ts @@ -40,12 +40,12 @@ export const clientFallbackAutoIdPlugin = createUnplugin((options: LoaderOptions let hasClientFallback = false let count = 0 - const isSFCRender = code.includes('function _sfc_render(') || code.includes('function _sfc_ssrRender(') + const isSFCRender = code.includes('function _sfc_render(') || code.includes('function _sfc_ssrRender(') || code.includes('function ssrRender(') - s.replace(/(_createVNode|_ssrRenderComponent)\((.*[cC]lient-?[fF]allback),(.*),/g, (full, renderFunction, name, props) => { + s.replace(/(_createVNode|_ssrRenderComponent)\((.*[cC]lient-?[fF]allback),\s*?(?:{(.*?)}|(null))\s*?,/gs, (full, renderFunction, name, props) => { hasClientFallback = true - // slice to remove object curly braces {} - const oldProps = props.trim() !== 'null' ? props.trim().slice(1, -1) : '' + + const oldProps = props.trim() !== 'null' ? props : '' // generate string to include the uidkey into the component props const newProps = `{ uid: ${isSFCRender ? '$setup.' : ''}${uidkey} + '${count}'${oldProps ? `, ${oldProps}` : ''} }` count++ diff --git a/test/basic.test.ts b/test/basic.test.ts index 6dba893600a..8c36cfb58e7 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -241,16 +241,20 @@ describe('pages', () => { const html = await $fetch('/client-fallback') // ensure failed components are not rendered server-side expect(html).not.toContain('This breaks in server-side setup.') - // ensure not failed component should be rendered - expect(html).toContain('Sugar Counter 0 x 2 = 0') - // ensure failed component render a div with attributes - expect(html).toContain('
') + // ensure not failed component not be rendered + expect(html).not.toContain('Sugar Counter 0 x 2 = 0') + // ensure ClientFallback is being rendered with its fallback tag and attributes + expect(html).toContain('') + + // ensure not failed component are correctly rendered + expect(html).not.toContain('

') + expect(html).toContain('hi') await expectNoClientErrors('/client-fallback') const page = await createPage('/client-fallback') await page.waitForLoadState('networkidle') - // ensure components reactivity + // ensure components reactivity once mounted await page.locator('#increment-count').click() expect(await page.locator('#sugar-counter').innerHTML()).toContain('Sugar Counter 1 x 2 = 2') }) diff --git a/test/fixtures/basic/pages/client-fallback.vue b/test/fixtures/basic/pages/client-fallback.vue index a0925a91413..5d0e220f196 100644 --- a/test/fixtures/basic/pages/client-fallback.vue +++ b/test/fixtures/basic/pages/client-fallback.vue @@ -2,17 +2,31 @@
Hello World
- + + + + + + +
+ +
+ +
+ + + +