mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-25 23:22:02 +00:00
feat(nuxt): add <NuxtClientFallback>
component (#8216)
This commit is contained in:
parent
c4222b1f6e
commit
1729d2e42f
@ -342,6 +342,28 @@ The content will not be included in production builds and tree-shaken.
|
|||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## <NuxtClientFallback> Component
|
||||||
|
|
||||||
|
::StabilityEdge
|
||||||
|
|
||||||
|
Nuxt provides the `<NuxtClientFallback>` component to render its content on the client if any of its children trigger an error in SSR.
|
||||||
|
You can specify a `fallbackTag` to make it render a specific tag if it fails to render on the server.
|
||||||
|
|
||||||
|
```html{}[pages/example.vue]
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Sidebar />
|
||||||
|
<!-- this component will be rendered on client-side -->
|
||||||
|
<NuxtClientFallback fallback-tag="span">
|
||||||
|
<Comments />
|
||||||
|
<BrokeInSSR />
|
||||||
|
</NuxtClientFallback>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
## Library Authors
|
## Library Authors
|
||||||
|
|
||||||
Making Vue component libraries with automatic tree-shaking and component registration is super easy ✨
|
Making Vue component libraries with automatic tree-shaking and component registration is super easy ✨
|
||||||
|
60
docs/3.api/2.components/1.nuxt-client-fallback.md
Normal file
60
docs/3.api/2.components/1.nuxt-client-fallback.md
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
title: "<NuxtClientFallback>"
|
||||||
|
description: "Nuxt provides `<NuxtClientFallback>` component to render its content on the client if any of its children trigger an error in SSR"
|
||||||
|
---
|
||||||
|
|
||||||
|
# `<NuxtClientFallback>`
|
||||||
|
|
||||||
|
Nuxt provides a `<NuxtClientFallback>` component to render its content on the client if any of its children trigger an error in SSR.
|
||||||
|
|
||||||
|
::alert{type=warning}
|
||||||
|
This component is experimental and in order to use it you must enable the `experimental.clientFallback` option in your `nuxt.config`.
|
||||||
|
::
|
||||||
|
|
||||||
|
:StabilityEdge{title=NuxtClientFallback}
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
- **`@ssr-error`**: Event emitted when a child triggers an error in SSR. Note that this will only be triggered on the server.
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<NuxtClientFallback @ssr-error="logSomeError">
|
||||||
|
<!-- ... -->
|
||||||
|
</NuxtClientFallback>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
- **placeholderTag** | **fallbackTag**: Specify a fallback tag to be rendered if the slot fails to render.
|
||||||
|
- **type**: `string`
|
||||||
|
- **default**: `div`
|
||||||
|
- **placeholder** | **fallback**: Specify fallback content to be rendered if the slot fails to render.
|
||||||
|
- **type**: `string`
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<!-- render <span>Hello world</span> server-side if the default slot fails to render -->
|
||||||
|
<NuxtClientFallback fallback-tag="span" fallback="Hello world">
|
||||||
|
<BrokeInSsr />
|
||||||
|
</NuxtClientFallback>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Slots
|
||||||
|
|
||||||
|
- **#fallback**: specify content to be displayed server-side if the slot fails to render.
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<NuxtClientFallback>
|
||||||
|
<!-- ... -->
|
||||||
|
<template #fallback>
|
||||||
|
<!-- this will be rendered on server side if the default slot fails to render in ssr -->
|
||||||
|
<p>Hello world</p>
|
||||||
|
</template>
|
||||||
|
</NuxtClientFallback>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
46
packages/nuxt/src/app/components/client-fallback.client.mjs
Normal file
46
packages/nuxt/src/app/components/client-fallback.client.mjs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { defineComponent, createElementBlock } from 'vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'NuxtClientFallback',
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: {
|
||||||
|
uid: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
fallbackTag: {
|
||||||
|
type: String,
|
||||||
|
default: () => 'div'
|
||||||
|
},
|
||||||
|
fallback: {
|
||||||
|
type: String,
|
||||||
|
default: () => ''
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
placeholderTag: {
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['ssr-error'],
|
||||||
|
setup (props, ctx) {
|
||||||
|
const mounted = ref(false)
|
||||||
|
const ssrFailed = useState(`${props.uid}`)
|
||||||
|
|
||||||
|
if (ssrFailed.value) {
|
||||||
|
onMounted(() => { mounted.value = true })
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (mounted.value) { return ctx.slots.default?.() }
|
||||||
|
if (ssrFailed.value) {
|
||||||
|
const slot = ctx.slots.placeholder || ctx.slots.fallback
|
||||||
|
if (slot) { return slot() }
|
||||||
|
const fallbackStr = props.placeholder || props.fallback
|
||||||
|
const fallbackTag = props.placeholderTag || props.fallbackTag
|
||||||
|
return createElementBlock(fallbackTag, null, fallbackStr)
|
||||||
|
}
|
||||||
|
return ctx.slots.default?.()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
74
packages/nuxt/src/app/components/client-fallback.server.mjs
Normal file
74
packages/nuxt/src/app/components/client-fallback.server.mjs
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { defineComponent, getCurrentInstance, onErrorCaptured } from 'vue'
|
||||||
|
import { ssrRenderVNode, ssrRenderAttrs, ssrRenderSlot } from 'vue/server-renderer'
|
||||||
|
import { createBuffer } from './utils'
|
||||||
|
|
||||||
|
const NuxtClientFallbackServer = defineComponent({
|
||||||
|
name: 'NuxtClientFallback',
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: {
|
||||||
|
uid: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
fallbackTag: {
|
||||||
|
type: String,
|
||||||
|
default: () => 'div'
|
||||||
|
},
|
||||||
|
fallback: {
|
||||||
|
type: String,
|
||||||
|
default: () => ''
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
placeholderTag: {
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['ssr-error'],
|
||||||
|
setup (props, ctx) {
|
||||||
|
const vm = getCurrentInstance()
|
||||||
|
const ssrFailed = ref(false)
|
||||||
|
|
||||||
|
onErrorCaptured(() => {
|
||||||
|
useState(`${props.uid}`, () => true)
|
||||||
|
ssrFailed.value = true
|
||||||
|
ctx.emit('ssr-error')
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
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 {
|
||||||
|
// catch in dev
|
||||||
|
useState(`${props.uid}`, () => true)
|
||||||
|
ctx.emit('ssr-error')
|
||||||
|
return { ssrFailed: true, ssrVNodes: [] }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ssrRender (ctx, push, parent) {
|
||||||
|
if (ctx.ssrFailed) {
|
||||||
|
const { fallback, placeholder } = ctx.$slots
|
||||||
|
if (fallback || placeholder) {
|
||||||
|
ssrRenderSlot(ctx.$slots, fallback ? 'fallback' : 'placeholder', {}, null, push, parent)
|
||||||
|
} else {
|
||||||
|
const content = ctx.placeholder || ctx.fallback
|
||||||
|
const tag = ctx.placeholderTag || ctx.fallbackTag
|
||||||
|
push(`<${tag}${ssrRenderAttrs(ctx.$attrs)}>${content}</${tag}>`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// push Fragment markup
|
||||||
|
push('<!--[-->')
|
||||||
|
push(ctx.ssrVNodes.getBuffer())
|
||||||
|
push('<!--]-->')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default NuxtClientFallbackServer
|
@ -1,5 +1,7 @@
|
|||||||
import { defineComponent, h } from 'vue'
|
import { defineComponent, h } from 'vue'
|
||||||
import type { Component } from 'vue'
|
import type { Component } from 'vue'
|
||||||
|
// eslint-disable-next-line
|
||||||
|
import { isString, isPromise, isArray } from '@vue/shared'
|
||||||
|
|
||||||
const Fragment = defineComponent({
|
const Fragment = defineComponent({
|
||||||
name: 'FragmentWrapper',
|
name: 'FragmentWrapper',
|
||||||
@ -16,3 +18,35 @@ const Fragment = defineComponent({
|
|||||||
export const _wrapIf = (component: Component, props: any, slots: any) => {
|
export const _wrapIf = (component: Component, props: any, slots: any) => {
|
||||||
return { default: () => props ? h(component, props === true ? {} : props, slots) : h(Fragment, {}, slots) }
|
return { default: () => props ? h(component, props === true ? {} : props, slots) : h(Fragment, {}, slots) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-use-before-define
|
||||||
|
export type SSRBuffer = SSRBufferItem[] & { hasAsync?: boolean }
|
||||||
|
export type SSRBufferItem = string | SSRBuffer | Promise<SSRBuffer>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create buffer retrieved from @vue/server-renderer
|
||||||
|
*
|
||||||
|
* @see https://github.com/vuejs/core/blob/9617dd4b2abc07a5dc40de6e5b759e851b4d0da1/packages/server-renderer/src/render.ts#L57
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
export function createBuffer () {
|
||||||
|
let appendable = false
|
||||||
|
const buffer: SSRBuffer = []
|
||||||
|
return {
|
||||||
|
getBuffer (): SSRBuffer {
|
||||||
|
return buffer
|
||||||
|
},
|
||||||
|
push (item: SSRBufferItem) {
|
||||||
|
const isStringItem = isString(item)
|
||||||
|
if (appendable && isStringItem) {
|
||||||
|
buffer[buffer.length - 1] += item as string
|
||||||
|
} else {
|
||||||
|
buffer.push(item)
|
||||||
|
}
|
||||||
|
appendable = isStringItem
|
||||||
|
if (isPromise(item) || (isArray(item) && item.hasAsync)) {
|
||||||
|
buffer.hasAsync = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
53
packages/nuxt/src/components/client-fallback-auto-id.ts
Normal file
53
packages/nuxt/src/components/client-fallback-auto-id.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { createUnplugin } from 'unplugin'
|
||||||
|
import type { 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
|
||||||
|
transform?: ComponentsOptions['transform'],
|
||||||
|
rootDir: string
|
||||||
|
}
|
||||||
|
const CLIENT_FALLBACK_RE = /<(NuxtClientFallback|nuxt-client-fallback)( [^>]*)?>/
|
||||||
|
const CLIENT_FALLBACK_GLOBAL_RE = /<(NuxtClientFallback|nuxt-client-fallback)( [^>]*)?>/g
|
||||||
|
export const clientFallbackAutoIdPlugin = createUnplugin((options: LoaderOptions) => {
|
||||||
|
const exclude = options.transform?.exclude || []
|
||||||
|
const include = options.transform?.include || []
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'nuxt:client-fallback-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) {
|
||||||
|
if (!CLIENT_FALLBACK_RE.test(code)) { return }
|
||||||
|
|
||||||
|
const s = new MagicString(code)
|
||||||
|
const relativeID = isAbsolute(id) ? relative(options.rootDir, id) : id
|
||||||
|
let count = 0
|
||||||
|
|
||||||
|
s.replace(CLIENT_FALLBACK_GLOBAL_RE, (full, name, attrs) => {
|
||||||
|
count++
|
||||||
|
if (/ :?uid=/g.test(attrs)) { return full }
|
||||||
|
return `<${name} :uid="'${hash(relativeID)}' + JSON.stringify($props) + '${count}'" ${attrs ?? ''}>`
|
||||||
|
})
|
||||||
|
|
||||||
|
if (s.hasChanged()) {
|
||||||
|
return {
|
||||||
|
code: s.toString(),
|
||||||
|
map: options.sourcemap
|
||||||
|
? s.generateMap({ source: id, includeContent: true })
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
29
packages/nuxt/src/components/helpers.ts
Normal file
29
packages/nuxt/src/components/helpers.ts
Normal file
@ -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
|
||||||
|
}
|
@ -1,11 +1,10 @@
|
|||||||
import { pathToFileURL } from 'node:url'
|
|
||||||
import { createUnplugin } from 'unplugin'
|
import { createUnplugin } from 'unplugin'
|
||||||
import { parseQuery, parseURL } from 'ufo'
|
|
||||||
import { genDynamicImport, genImport } from 'knitwork'
|
import { genDynamicImport, genImport } from 'knitwork'
|
||||||
import MagicString from 'magic-string'
|
import MagicString from 'magic-string'
|
||||||
import { pascalCase } from 'scule'
|
import { pascalCase } from 'scule'
|
||||||
import { resolve } from 'pathe'
|
import { resolve } from 'pathe'
|
||||||
import { distDir } from '../dirs'
|
import { distDir } from '../dirs'
|
||||||
|
import { isVueTemplate } from './helpers'
|
||||||
import type { Component, ComponentsOptions } from 'nuxt/schema'
|
import type { Component, ComponentsOptions } from 'nuxt/schema'
|
||||||
|
|
||||||
interface LoaderOptions {
|
interface LoaderOptions {
|
||||||
@ -16,33 +15,6 @@ interface LoaderOptions {
|
|||||||
experimentalComponentIslands?: boolean
|
experimentalComponentIslands?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
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) => {
|
export const loaderPlugin = createUnplugin((options: LoaderOptions) => {
|
||||||
const exclude = options.transform?.exclude || []
|
const exclude = options.transform?.exclude || []
|
||||||
const include = options.transform?.include || []
|
const include = options.transform?.include || []
|
||||||
@ -86,7 +58,7 @@ export const loaderPlugin = createUnplugin((options: LoaderOptions) => {
|
|||||||
return identifier
|
return identifier
|
||||||
}
|
}
|
||||||
|
|
||||||
const isClientOnly = component.mode === 'client'
|
const isClientOnly = component.mode === 'client' && component.pascalName !== 'NuxtClientFallback'
|
||||||
if (isClientOnly) {
|
if (isClientOnly) {
|
||||||
imports.add(genImport('#app/components/client-only', [{ name: 'createClientOnly' }]))
|
imports.add(genImport('#app/components/client-only', [{ name: 'createClientOnly' }]))
|
||||||
identifier += '_client'
|
identifier += '_client'
|
||||||
|
@ -2,6 +2,7 @@ import { statSync } from 'node:fs'
|
|||||||
import { relative, resolve } from 'pathe'
|
import { relative, resolve } from 'pathe'
|
||||||
import { defineNuxtModule, resolveAlias, addTemplate, addPluginTemplate, updateTemplates } from '@nuxt/kit'
|
import { defineNuxtModule, resolveAlias, addTemplate, addPluginTemplate, updateTemplates } from '@nuxt/kit'
|
||||||
import { distDir } from '../dirs'
|
import { distDir } from '../dirs'
|
||||||
|
import { clientFallbackAutoIdPlugin } from './client-fallback-auto-id'
|
||||||
import { componentsPluginTemplate, componentsTemplate, componentsIslandsTemplate, componentsTypeTemplate } from './templates'
|
import { componentsPluginTemplate, componentsTemplate, componentsIslandsTemplate, componentsTypeTemplate } from './templates'
|
||||||
import { scanComponents } from './scan'
|
import { scanComponents } from './scan'
|
||||||
import { loaderPlugin } from './loader'
|
import { loaderPlugin } from './loader'
|
||||||
@ -198,6 +199,10 @@ export default defineNuxtModule<ComponentsOptions>({
|
|||||||
getComponents
|
getComponents
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
config.plugins.push(clientFallbackAutoIdPlugin.vite({
|
||||||
|
sourcemap: nuxt.options.sourcemap[mode],
|
||||||
|
rootDir: nuxt.options.rootDir
|
||||||
|
}))
|
||||||
config.plugins.push(loaderPlugin.vite({
|
config.plugins.push(loaderPlugin.vite({
|
||||||
sourcemap: nuxt.options.sourcemap[mode],
|
sourcemap: nuxt.options.sourcemap[mode],
|
||||||
getComponents,
|
getComponents,
|
||||||
@ -216,6 +221,10 @@ export default defineNuxtModule<ComponentsOptions>({
|
|||||||
getComponents
|
getComponents
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
config.plugins.push(clientFallbackAutoIdPlugin.webpack({
|
||||||
|
sourcemap: nuxt.options.sourcemap[mode],
|
||||||
|
rootDir: nuxt.options.rootDir
|
||||||
|
}))
|
||||||
config.plugins.push(loaderPlugin.webpack({
|
config.plugins.push(loaderPlugin.webpack({
|
||||||
sourcemap: nuxt.options.sourcemap[mode],
|
sourcemap: nuxt.options.sourcemap[mode],
|
||||||
getComponents,
|
getComponents,
|
||||||
|
@ -220,6 +220,23 @@ async function initNuxt (nuxt: Nuxt) {
|
|||||||
filePath: resolve(nuxt.options.appDir, 'components/nuxt-loading-indicator')
|
filePath: resolve(nuxt.options.appDir, 'components/nuxt-loading-indicator')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Add <NuxtClientFallback>
|
||||||
|
if (nuxt.options.experimental.clientFallback) {
|
||||||
|
addComponent({
|
||||||
|
name: 'NuxtClientFallback',
|
||||||
|
priority: 10, // built-in that we do not expect the user to override
|
||||||
|
filePath: resolve(nuxt.options.appDir, 'components/client-fallback.client'),
|
||||||
|
mode: 'client'
|
||||||
|
})
|
||||||
|
|
||||||
|
addComponent({
|
||||||
|
name: 'NuxtClientFallback',
|
||||||
|
priority: 10, // built-in that we do not expect the user to override
|
||||||
|
filePath: resolve(nuxt.options.appDir, 'components/client-fallback.server'),
|
||||||
|
mode: 'server'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Add <NuxtIsland>
|
// Add <NuxtIsland>
|
||||||
if (nuxt.options.experimental.componentIslands) {
|
if (nuxt.options.experimental.componentIslands) {
|
||||||
addComponent({
|
addComponent({
|
||||||
|
@ -127,6 +127,12 @@ export default defineUntypedSchema({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to enable the experimental `<NuxtClientFallback>` component for rendering content on the client
|
||||||
|
* if there's an error in SSR.
|
||||||
|
*/
|
||||||
|
clientFallback: false,
|
||||||
|
|
||||||
/** Enable cross-origin prefetch using the Speculation Rules API. */
|
/** Enable cross-origin prefetch using the Speculation Rules API. */
|
||||||
crossOriginPrefetch: false,
|
crossOriginPrefetch: false,
|
||||||
|
|
||||||
|
@ -280,6 +280,37 @@ describe('pages', () => {
|
|||||||
expect(html).not.toContain('client only script')
|
expect(html).not.toContain('client only script')
|
||||||
await expectNoClientErrors('/client-only-explicit-import')
|
await expectNoClientErrors('/client-only-explicit-import')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('client-fallback', async () => {
|
||||||
|
const classes = [
|
||||||
|
'clientfallback-non-stateful-setup',
|
||||||
|
'clientfallback-non-stateful',
|
||||||
|
'clientfallback-stateful-setup',
|
||||||
|
'clientfallback-stateful'
|
||||||
|
]
|
||||||
|
const html = await $fetch('/client-fallback')
|
||||||
|
// ensure failed components are not rendered server-side
|
||||||
|
expect(html).not.toContain('This breaks in server-side setup.')
|
||||||
|
classes.forEach(c => expect(html).not.toContain(c))
|
||||||
|
// ensure not failed component not be rendered
|
||||||
|
expect(html).not.toContain('Sugar Counter 12 x 0 = 0')
|
||||||
|
// ensure NuxtClientFallback is being rendered with its fallback tag and attributes
|
||||||
|
expect(html).toContain('<span class="break-in-ssr">this failed to render</span>')
|
||||||
|
// ensure Fallback slot is being rendered server side
|
||||||
|
expect(html).toContain('Hello world !')
|
||||||
|
|
||||||
|
// ensure not failed component are correctly rendered
|
||||||
|
expect(html).not.toContain('<p></p>')
|
||||||
|
expect(html).toContain('hi')
|
||||||
|
|
||||||
|
await expectNoClientErrors('/client-fallback')
|
||||||
|
|
||||||
|
const page = await createPage('/client-fallback')
|
||||||
|
await page.waitForLoadState('networkidle')
|
||||||
|
// ensure components reactivity once mounted
|
||||||
|
await page.locator('#increment-count').click()
|
||||||
|
expect(await page.locator('#sugar-counter').innerHTML()).toContain('Sugar Counter 12 x 1 = 12')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('nuxt links', () => {
|
describe('nuxt links', () => {
|
||||||
|
@ -40,7 +40,7 @@ describe.skipIf(isWindows)('minimal nuxt application', () => {
|
|||||||
|
|
||||||
it('default server bundle size', async () => {
|
it('default server bundle size', async () => {
|
||||||
stats.server = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
stats.server = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
||||||
expect(stats.server.totalBytes).toBeLessThan(94400)
|
expect(stats.server.totalBytes).toBeLessThan(94450)
|
||||||
|
|
||||||
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
||||||
expect(modules.totalBytes).toBeLessThan(2713000)
|
expect(modules.totalBytes).toBeLessThan(2713000)
|
||||||
|
10
test/fixtures/basic/components/BreakInSetup.vue
vendored
Normal file
10
test/fixtures/basic/components/BreakInSetup.vue
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<script setup>
|
||||||
|
// break server-side
|
||||||
|
const data = window.__NUXT__
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
This breaks in server-side setup. {{ data.serverRendered }}
|
||||||
|
</div>
|
||||||
|
</template>
|
14
test/fixtures/basic/components/clientFallback/NonStateful.vue
vendored
Normal file
14
test/fixtures/basic/components/clientFallback/NonStateful.vue
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
non stateful
|
||||||
|
<NuxtClientFallback>
|
||||||
|
<BreakInSetup class="clientfallback-non-stateful" />
|
||||||
|
</NuxtClientFallback>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default defineNuxtComponent({
|
||||||
|
name: 'ClientFallbackStateful'
|
||||||
|
})
|
||||||
|
</script>
|
8
test/fixtures/basic/components/clientFallback/NonStatefulSetup.vue
vendored
Normal file
8
test/fixtures/basic/components/clientFallback/NonStatefulSetup.vue
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
non stateful setup
|
||||||
|
<NuxtClientFallback>
|
||||||
|
<BreakInSetup class="clientfallback-non-stateful-setup" />
|
||||||
|
</NuxtClientFallback>
|
||||||
|
</div>
|
||||||
|
</template>
|
20
test/fixtures/basic/components/clientFallback/Stateful.vue
vendored
Normal file
20
test/fixtures/basic/components/clientFallback/Stateful.vue
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
stateful test {{ state }}
|
||||||
|
<NuxtClientFallback>
|
||||||
|
<BreakInSetup class="clientfallback-stateful" />
|
||||||
|
</NuxtClientFallback>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default defineNuxtComponent({
|
||||||
|
name: 'ClientFallbackStateful',
|
||||||
|
setup () {
|
||||||
|
const state = ref(0)
|
||||||
|
return {
|
||||||
|
state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
13
test/fixtures/basic/components/clientFallback/StatefulSetup.vue
vendored
Normal file
13
test/fixtures/basic/components/clientFallback/StatefulSetup.vue
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
stateful setup {{ state }}
|
||||||
|
<NuxtClientFallback>
|
||||||
|
<BreakInSetup class="clientfallback-stateful-setup" />
|
||||||
|
</NuxtClientFallback>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const state = ref(1)
|
||||||
|
</script>
|
1
test/fixtures/basic/nuxt.config.ts
vendored
1
test/fixtures/basic/nuxt.config.ts
vendored
@ -173,6 +173,7 @@ export default defineNuxtConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
|
clientFallback: true,
|
||||||
restoreState: true,
|
restoreState: true,
|
||||||
inlineSSRStyles: id => !!id && !id.includes('assets.vue'),
|
inlineSSRStyles: id => !!id && !id.includes('assets.vue'),
|
||||||
componentIslands: true,
|
componentIslands: true,
|
||||||
|
50
test/fixtures/basic/pages/client-fallback.vue
vendored
Normal file
50
test/fixtures/basic/pages/client-fallback.vue
vendored
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
Hello World
|
||||||
|
<div id="locator-for-playwright">
|
||||||
|
<!-- single child -->
|
||||||
|
<NuxtClientFallback fallback-tag="span" class="break-in-ssr" fallback="this failed to render">
|
||||||
|
<BreakInSetup />
|
||||||
|
</NuxtClientFallback>
|
||||||
|
<!-- multi child -->
|
||||||
|
<NuxtClientFallback>
|
||||||
|
<BreakInSetup class="broke-in-ssr" />
|
||||||
|
<BreakInSetup />
|
||||||
|
</NuxtClientFallback>
|
||||||
|
<!-- don't render if one child fails in ssr -->
|
||||||
|
<NuxtClientFallback>
|
||||||
|
<BreakInSetup />
|
||||||
|
<SugarCounter id="sugar-counter" :multiplier="multiplier" />
|
||||||
|
</NuxtClientFallback>
|
||||||
|
<!-- nested children fails -->
|
||||||
|
<NuxtClientFallback>
|
||||||
|
<div>
|
||||||
|
<BreakInSetup />
|
||||||
|
</div>
|
||||||
|
<SugarCounter :multiplier="multiplier" />
|
||||||
|
</NuxtClientFallback>
|
||||||
|
<!-- should be rendered -->
|
||||||
|
<NuxtClientFallback fallback-tag="p">
|
||||||
|
<FunctionalComponent />
|
||||||
|
</NuxtClientFallback>
|
||||||
|
<!-- fallback -->
|
||||||
|
<NuxtClientFallback>
|
||||||
|
<BreakInSetup />
|
||||||
|
<template #fallback>
|
||||||
|
<div>Hello world !</div>
|
||||||
|
</template>
|
||||||
|
</NuxtClientFallback>
|
||||||
|
<ClientFallbackStateful />
|
||||||
|
<ClientFallbackStatefulSetup />
|
||||||
|
<ClientFallbackNonStatefulSetup />
|
||||||
|
<ClientFallbackNonStateful />
|
||||||
|
</div>
|
||||||
|
<button id="increment-count" @click="multiplier++">
|
||||||
|
increment count
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const multiplier = ref(0)
|
||||||
|
</script>
|
Loading…
Reference in New Issue
Block a user