feat(ssr/suspense): suspense hydration

In order to support hydration of async components, server-rendered
fragments must be explicitly marked with comment nodes.
This commit is contained in:
Evan You 2020-03-12 22:19:41 -04:00
parent b3d7d64931
commit a3cc970030
19 changed files with 385 additions and 139 deletions

View File

@ -98,7 +98,7 @@ describe('SSR hydration', () => {
const msg = ref('foo') const msg = ref('foo')
const fn = jest.fn() const fn = jest.fn()
const { vnode, container } = mountWithHydration( const { vnode, container } = mountWithHydration(
'<div><span>foo</span><span class="foo"></span></div>', '<div><!----><span>foo</span><!----><span class="foo"></span><!----><!----></div>',
() => () =>
h('div', [ h('div', [
[h('span', msg.value), [h('span', { class: msg.value, onClick: fn })]] [h('span', msg.value), [h('span', { class: msg.value, onClick: fn })]]
@ -136,7 +136,9 @@ describe('SSR hydration', () => {
msg.value = 'bar' msg.value = 'bar'
await nextTick() await nextTick()
expect(vnode.el.innerHTML).toBe(`<span>bar</span><span class="bar"></span>`) expect(vnode.el.innerHTML).toBe(
`<!----><span>bar</span><!----><span class="bar"></span><!----><!---->`
)
}) })
test('portal', async () => { test('portal', async () => {

View File

@ -219,11 +219,11 @@ describe('ssr: components', () => {
foo: ({ list }, _push, _parent, _scopeId) => { foo: ({ list }, _push, _parent, _scopeId) => {
if (_push) { if (_push) {
if (_ctx.ok) { if (_ctx.ok) {
_push(\`<div\${_scopeId}>\`) _push(\`<div\${_scopeId}><!--1-->\`)
_ssrRenderList(list, (i) => { _ssrRenderList(list, (i) => {
_push(\`<span\${_scopeId}></span>\`) _push(\`<span\${_scopeId}></span>\`)
}) })
_push(\`</div>\`) _push(\`<!--0--></div>\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -242,11 +242,11 @@ describe('ssr: components', () => {
bar: ({ ok }, _push, _parent, _scopeId) => { bar: ({ ok }, _push, _parent, _scopeId) => {
if (_push) { if (_push) {
if (ok) { if (ok) {
_push(\`<div\${_scopeId}>\`) _push(\`<div\${_scopeId}><!--1-->\`)
_ssrRenderList(_ctx.list, (i) => { _ssrRenderList(_ctx.list, (i) => {
_push(\`<span\${_scopeId}></span>\`) _push(\`<span\${_scopeId}></span>\`)
}) })
_push(\`</div>\`) _push(\`<!--0--></div>\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -281,7 +281,7 @@ describe('ssr: components', () => {
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
" "
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
_push(\`<div></div>\`) _push(\`<!--1--><div></div><!--0-->\`)
}" }"
`) `)

View File

@ -6,9 +6,11 @@ describe('ssr: v-for', () => {
"const { ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\") "const { ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
_push(\`<!--1-->\`)
_ssrRenderList(_ctx.list, (i) => { _ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`) _push(\`<div></div>\`)
}) })
_push(\`<!--0-->\`)
}" }"
`) `)
}) })
@ -19,9 +21,11 @@ describe('ssr: v-for', () => {
"const { ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\") "const { ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
_push(\`<!--1-->\`)
_ssrRenderList(_ctx.list, (i) => { _ssrRenderList(_ctx.list, (i) => {
_push(\`<div>foo<span>bar</span></div>\`) _push(\`<div>foo<span>bar</span></div>\`)
}) })
_push(\`<!--0-->\`)
}" }"
`) `)
}) })
@ -37,8 +41,9 @@ describe('ssr: v-for', () => {
"const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\") "const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
_push(\`<!--1-->\`)
_ssrRenderList(_ctx.list, (row, i) => { _ssrRenderList(_ctx.list, (row, i) => {
_push(\`<div>\`) _push(\`<div><!--1-->\`)
_ssrRenderList(row, (j) => { _ssrRenderList(row, (j) => {
_push(\`<div>\${ _push(\`<div>\${
_ssrInterpolate(i) _ssrInterpolate(i)
@ -46,8 +51,9 @@ describe('ssr: v-for', () => {
_ssrInterpolate(j) _ssrInterpolate(j)
}</div>\`) }</div>\`)
}) })
_push(\`</div>\`) _push(\`<!--0--></div>\`)
}) })
_push(\`<!--0-->\`)
}" }"
`) `)
}) })
@ -58,9 +64,11 @@ describe('ssr: v-for', () => {
"const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\") "const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
_push(\`<!--1-->\`)
_ssrRenderList(_ctx.list, (i) => { _ssrRenderList(_ctx.list, (i) => {
_push(\`\${_ssrInterpolate(i)}\`) _push(\`<!--1-->\${_ssrInterpolate(i)}<!--0-->\`)
}) })
_push(\`<!--0-->\`)
}" }"
`) `)
}) })
@ -73,9 +81,11 @@ describe('ssr: v-for', () => {
"const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\") "const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
_push(\`<!--1-->\`)
_ssrRenderList(_ctx.list, (i) => { _ssrRenderList(_ctx.list, (i) => {
_push(\`<span>\${_ssrInterpolate(i)}</span>\`) _push(\`<span>\${_ssrInterpolate(i)}</span>\`)
}) })
_push(\`<!--0-->\`)
}" }"
`) `)
}) })
@ -89,13 +99,15 @@ describe('ssr: v-for', () => {
"const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\") "const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
_push(\`<!--1-->\`)
_ssrRenderList(_ctx.list, (i) => { _ssrRenderList(_ctx.list, (i) => {
_push(\`<span>\${ _push(\`<!--1--><span>\${
_ssrInterpolate(i) _ssrInterpolate(i)
}</span><span>\${ }</span><span>\${
_ssrInterpolate(i + 1) _ssrInterpolate(i + 1)
}</span>\`) }</span><!--0-->\`)
}) })
_push(\`<!--0-->\`)
}" }"
`) `)
}) })
@ -111,9 +123,11 @@ describe('ssr: v-for', () => {
"const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\") "const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
_push(\`<!--1-->\`)
_ssrRenderList(_ctx.list, ({ foo }, index) => { _ssrRenderList(_ctx.list, ({ foo }, index) => {
_push(\`<div>\${_ssrInterpolate(foo + _ctx.bar + index)}</div>\`) _push(\`<div>\${_ssrInterpolate(foo + _ctx.bar + index)}</div>\`)
}) })
_push(\`<!--0-->\`)
}" }"
`) `)
}) })

View File

@ -80,7 +80,7 @@ describe('ssr: v-if', () => {
" "
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`hello\`) _push(\`<!--1-->hello<!--0-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -110,7 +110,7 @@ describe('ssr: v-if', () => {
" "
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<div>hi</div><div>ho</div>\`) _push(\`<!--1--><div>hi</div><div>ho</div><!--0-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -126,9 +126,11 @@ describe('ssr: v-if', () => {
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<!--1-->\`)
_ssrRenderList(_ctx.list, (i) => { _ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`) _push(\`<div></div>\`)
}) })
_push(\`<!--0-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -145,7 +147,7 @@ describe('ssr: v-if', () => {
" "
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<div>hi</div><div>ho</div>\`) _push(\`<!--1--><div>hi</div><div>ho</div><!--0-->\`)
} else { } else {
_push(\`<div></div>\`) _push(\`<div></div>\`)
} }

View File

@ -10,7 +10,8 @@ import {
createBlockStatement, createBlockStatement,
CompilerOptions, CompilerOptions,
IfStatement, IfStatement,
CallExpression CallExpression,
isText
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { isString, escapeHtml } from '@vue/shared' import { isString, escapeHtml } from '@vue/shared'
import { SSR_INTERPOLATE, ssrHelpers } from './runtimeHelpers' import { SSR_INTERPOLATE, ssrHelpers } from './runtimeHelpers'
@ -28,7 +29,9 @@ import { ssrProcessElement } from './transforms/ssrTransformElement'
export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) { export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) {
const context = createSSRTransformContext(ast, options) const context = createSSRTransformContext(ast, options)
processChildren(ast.children, context) const isFragment =
ast.children.length > 1 && ast.children.some(c => !isText(c))
processChildren(ast.children, context, isFragment)
ast.codegenNode = createBlockStatement(context.body) ast.codegenNode = createBlockStatement(context.body)
// Finalize helpers. // Finalize helpers.
@ -104,8 +107,12 @@ function createChildContext(
export function processChildren( export function processChildren(
children: TemplateChildNode[], children: TemplateChildNode[],
context: SSRTransformContext context: SSRTransformContext,
asFragment = false
) { ) {
if (asFragment) {
context.pushStringPart(`<!--1-->`)
}
for (let i = 0; i < children.length; i++) { for (let i = 0; i < children.length; i++) {
const child = children[i] const child = children[i]
if (child.type === NodeTypes.ELEMENT) { if (child.type === NodeTypes.ELEMENT) {
@ -128,14 +135,18 @@ export function processChildren(
ssrProcessFor(child, context) ssrProcessFor(child, context)
} }
} }
if (asFragment) {
context.pushStringPart(`<!--0-->`)
}
} }
export function processChildrenAsStatement( export function processChildrenAsStatement(
children: TemplateChildNode[], children: TemplateChildNode[],
parentContext: SSRTransformContext, parentContext: SSRTransformContext,
asFragment = false,
withSlotScopeId = parentContext.withSlotScopeId withSlotScopeId = parentContext.withSlotScopeId
): BlockStatement { ): BlockStatement {
const childContext = createChildContext(parentContext, withSlotScopeId) const childContext = createChildContext(parentContext, withSlotScopeId)
processChildren(children, childContext) processChildren(children, childContext, asFragment)
return createBlockStatement(childContext.body) return createBlockStatement(childContext.body)
} }

View File

@ -30,7 +30,8 @@ import {
traverseNode, traverseNode,
ExpressionNode, ExpressionNode,
TemplateNode, TemplateNode,
SUSPENSE SUSPENSE,
TRANSITION_GROUP
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { SSR_RENDER_COMPONENT } from '../runtimeHelpers' import { SSR_RENDER_COMPONENT } from '../runtimeHelpers'
import { import {
@ -151,7 +152,7 @@ export function ssrProcessComponent(
return ssrProcessSuspense(node, context) return ssrProcessSuspense(node, context)
} else { } else {
// real fall-through (e.g. KeepAlive): just render its children. // real fall-through (e.g. KeepAlive): just render its children.
processChildren(node.children, context) processChildren(node.children, context, component === TRANSITION_GROUP)
} }
} else { } else {
// finish up slot function expressions from the 1st pass. // finish up slot function expressions from the 1st pass.
@ -167,6 +168,7 @@ export function ssrProcessComponent(
processChildrenAsStatement( processChildrenAsStatement(
children, children,
context, context,
false,
true /* withSlotScopeId */ true /* withSlotScopeId */
), ),
vnodeBranch vnodeBranch

View File

@ -4,7 +4,8 @@ import {
processFor, processFor,
createCallExpression, createCallExpression,
createFunctionExpression, createFunctionExpression,
createForLoopParams createForLoopParams,
NodeTypes
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { import {
SSRTransformContext, SSRTransformContext,
@ -21,14 +22,23 @@ export const ssrTransformFor = createStructuralDirectiveTransform(
// This is called during the 2nd transform pass to construct the SSR-sepcific // This is called during the 2nd transform pass to construct the SSR-sepcific
// codegen nodes. // codegen nodes.
export function ssrProcessFor(node: ForNode, context: SSRTransformContext) { export function ssrProcessFor(node: ForNode, context: SSRTransformContext) {
const needFragmentWrapper =
node.children.length !== 1 || node.children[0].type !== NodeTypes.ELEMENT
const renderLoop = createFunctionExpression( const renderLoop = createFunctionExpression(
createForLoopParams(node.parseResult) createForLoopParams(node.parseResult)
) )
renderLoop.body = processChildrenAsStatement(node.children, context) renderLoop.body = processChildrenAsStatement(
node.children,
context,
needFragmentWrapper
)
// v-for always renders a fragment
context.pushStringPart(`<!--1-->`)
context.pushStatement( context.pushStatement(
createCallExpression(context.helper(SSR_RENDER_LIST), [ createCallExpression(context.helper(SSR_RENDER_LIST), [
node.source, node.source,
renderLoop renderLoop
]) ])
) )
context.pushStringPart(`<!--0-->`)
} }

View File

@ -4,7 +4,10 @@ import {
IfNode, IfNode,
createIfStatement, createIfStatement,
createBlockStatement, createBlockStatement,
createCallExpression createCallExpression,
IfBranchNode,
BlockStatement,
NodeTypes
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { import {
SSRTransformContext, SSRTransformContext,
@ -23,17 +26,14 @@ export function ssrProcessIf(node: IfNode, context: SSRTransformContext) {
const [rootBranch] = node.branches const [rootBranch] = node.branches
const ifStatement = createIfStatement( const ifStatement = createIfStatement(
rootBranch.condition!, rootBranch.condition!,
processChildrenAsStatement(rootBranch.children, context) processIfBranch(rootBranch, context)
) )
context.pushStatement(ifStatement) context.pushStatement(ifStatement)
let currentIf = ifStatement let currentIf = ifStatement
for (let i = 1; i < node.branches.length; i++) { for (let i = 1; i < node.branches.length; i++) {
const branch = node.branches[i] const branch = node.branches[i]
const branchBlockStatement = processChildrenAsStatement( const branchBlockStatement = processIfBranch(branch, context)
branch.children,
context
)
if (branch.condition) { if (branch.condition) {
// else-if // else-if
currentIf = currentIf.alternate = createIfStatement( currentIf = currentIf.alternate = createIfStatement(
@ -52,3 +52,15 @@ export function ssrProcessIf(node: IfNode, context: SSRTransformContext) {
]) ])
} }
} }
function processIfBranch(
branch: IfBranchNode,
context: SSRTransformContext
): BlockStatement {
const { children } = branch
const needFragmentWrapper =
(children.length !== 1 || children[0].type !== NodeTypes.ELEMENT) &&
// optimize away nested fragments when the only child is a ForNode
!(children.length === 1 && children[0].type === NodeTypes.FOR)
return processChildrenAsStatement(children, context, needFragmentWrapper)
}

View File

@ -144,7 +144,6 @@ export interface ComponentInternalInstance {
// suspense related // suspense related
asyncDep: Promise<any> | null asyncDep: Promise<any> | null
asyncResult: unknown
asyncResolved: boolean asyncResolved: boolean
// storage for any extra properties // storage for any extra properties
@ -215,7 +214,6 @@ export function createComponentInstance(
// async dependency management // async dependency management
asyncDep: null, asyncDep: null,
asyncResult: null,
asyncResolved: false, asyncResolved: false,
// user namespace for storing whatever the user assigns to `this` // user namespace for storing whatever the user assigns to `this`
@ -367,7 +365,7 @@ function setupStatefulComponent(
if (isPromise(setupResult)) { if (isPromise(setupResult)) {
if (isSSR) { if (isSSR) {
// return the promise so server-renderer can wait on it // return the promise so server-renderer can wait on it
return setupResult.then(resolvedResult => { return setupResult.then((resolvedResult: unknown) => {
handleSetupResult(instance, resolvedResult, parentSuspense, isSSR) handleSetupResult(instance, resolvedResult, parentSuspense, isSSR)
}) })
} else if (__FEATURE_SUSPENSE__) { } else if (__FEATURE_SUSPENSE__) {

View File

@ -5,8 +5,8 @@ import { Slots } from '../componentSlots'
import { RendererInternals, MoveType, SetupRenderEffectFn } from '../renderer' import { RendererInternals, MoveType, SetupRenderEffectFn } from '../renderer'
import { queuePostFlushCb, queueJob } from '../scheduler' import { queuePostFlushCb, queueJob } from '../scheduler'
import { updateHOCHostEl } from '../componentRenderUtils' import { updateHOCHostEl } from '../componentRenderUtils'
import { handleError, ErrorCodes } from '../errorHandling'
import { pushWarningContext, popWarningContext } from '../warning' import { pushWarningContext, popWarningContext } from '../warning'
import { handleError, ErrorCodes } from '../errorHandling'
export interface SuspenseProps { export interface SuspenseProps {
onResolve?: () => void onResolve?: () => void
@ -59,7 +59,8 @@ export const SuspenseImpl = {
rendererInternals rendererInternals
) )
} }
} },
hydrate: hydrateSuspense
} }
// Force-casted public typing for h and TSX props inference // Force-casted public typing for h and TSX props inference
@ -97,14 +98,10 @@ function mountSuspense(
rendererInternals rendererInternals
)) ))
const { content, fallback } = normalizeSuspenseChildren(n2)
suspense.subTree = content
suspense.fallbackTree = fallback
// start mounting the content subtree in an off-dom container // start mounting the content subtree in an off-dom container
patch( patch(
null, null,
content, suspense.subTree,
hiddenContainer, hiddenContainer,
null, null,
parentComponent, parentComponent,
@ -117,7 +114,7 @@ function mountSuspense(
// mount the fallback tree // mount the fallback tree
patch( patch(
null, null,
fallback, suspense.fallbackTree,
container, container,
anchor, anchor,
parentComponent, parentComponent,
@ -125,7 +122,7 @@ function mountSuspense(
isSVG, isSVG,
optimized optimized
) )
n2.el = fallback.el n2.el = suspense.fallbackTree.el
} else { } else {
// Suspense has no async deps. Just resolve. // Suspense has no async deps. Just resolve.
suspense.resolve() suspense.resolve()
@ -209,6 +206,7 @@ export interface SuspenseBoundary<
subTree: HostVNode subTree: HostVNode
fallbackTree: HostVNode fallbackTree: HostVNode
deps: number deps: number
isHydrating: boolean
isResolved: boolean isResolved: boolean
isUnmounted: boolean isUnmounted: boolean
effects: Function[] effects: Function[]
@ -235,7 +233,8 @@ function createSuspenseBoundary<HostNode, HostElement>(
anchor: HostNode | null, anchor: HostNode | null,
isSVG: boolean, isSVG: boolean,
optimized: boolean, optimized: boolean,
rendererInternals: RendererInternals<HostNode, HostElement> rendererInternals: RendererInternals<HostNode, HostElement>,
isHydrating = false
): SuspenseBoundary<HostNode, HostElement> { ): SuspenseBoundary<HostNode, HostElement> {
const { const {
p: patch, p: patch,
@ -245,6 +244,12 @@ function createSuspenseBoundary<HostNode, HostElement>(
o: { parentNode } o: { parentNode }
} = rendererInternals } = rendererInternals
const getCurrentTree = () =>
suspense.isResolved || suspense.isHydrating
? suspense.subTree
: suspense.fallbackTree
const { content, fallback } = normalizeSuspenseChildren(vnode)
const suspense: SuspenseBoundary<HostNode, HostElement> = { const suspense: SuspenseBoundary<HostNode, HostElement> = {
vnode, vnode,
parent, parent,
@ -255,8 +260,9 @@ function createSuspenseBoundary<HostNode, HostElement>(
hiddenContainer, hiddenContainer,
anchor, anchor,
deps: 0, deps: 0,
subTree: (null as unknown) as VNode, // will be set immediately after creation subTree: content,
fallbackTree: (null as unknown) as VNode, // will be set immediately after creation fallbackTree: fallback,
isHydrating,
isResolved: false, isResolved: false,
isUnmounted: false, isUnmounted: false,
effects: [], effects: [],
@ -283,17 +289,22 @@ function createSuspenseBoundary<HostNode, HostElement>(
container container
} = suspense } = suspense
// this is initial anchor on mount if (suspense.isHydrating) {
let { anchor } = suspense suspense.isHydrating = false
// unmount fallback tree } else {
if (fallbackTree.el) { // this is initial anchor on mount
// if the fallback tree was mounted, it may have been moved let { anchor } = suspense
// as part of a parent suspense. get the latest anchor for insertion // unmount fallback tree
anchor = next(fallbackTree) if (fallbackTree.el) {
unmount(fallbackTree, parentComponent, suspense, true) // if the fallback tree was mounted, it may have been moved
// as part of a parent suspense. get the latest anchor for insertion
anchor = next(fallbackTree)
unmount(fallbackTree, parentComponent, suspense, true)
}
// move content from off-dom container to actual container
move(subTree, container, anchor, MoveType.ENTER)
} }
// move content from off-dom container to actual container
move(subTree, container, anchor, MoveType.ENTER)
const el = (vnode.el = subTree.el!) const el = (vnode.el = subTree.el!)
// suspense as the root node of a component... // suspense as the root node of a component...
if (parentComponent && parentComponent.subTree === vnode) { if (parentComponent && parentComponent.subTree === vnode) {
@ -367,19 +378,12 @@ function createSuspenseBoundary<HostNode, HostElement>(
}, },
move(container, anchor, type) { move(container, anchor, type) {
move( move(getCurrentTree(), container, anchor, type)
suspense.isResolved ? suspense.subTree : suspense.fallbackTree,
container,
anchor,
type
)
suspense.container = container suspense.container = container
}, },
next() { next() {
return next( return next(getCurrentTree())
suspense.isResolved ? suspense.subTree : suspense.fallbackTree
)
}, },
registerDep(instance, setupRenderEffect) { registerDep(instance, setupRenderEffect) {
@ -392,6 +396,7 @@ function createSuspenseBoundary<HostNode, HostElement>(
}) })
} }
const hydratedEl = instance.vnode.el
suspense.deps++ suspense.deps++
instance instance
.asyncDep!.catch(err => { .asyncDep!.catch(err => {
@ -411,14 +416,23 @@ function createSuspenseBoundary<HostNode, HostElement>(
pushWarningContext(vnode) pushWarningContext(vnode)
} }
handleSetupResult(instance, asyncSetupResult, suspense, false) handleSetupResult(instance, asyncSetupResult, suspense, false)
// unset placeholder, otherwise this will be treated as a hydration mount if (hydratedEl) {
vnode.el = null // vnode may have been replaced if an update happened before the
// async dep is reoslved.
vnode.el = hydratedEl
}
setupRenderEffect( setupRenderEffect(
instance, instance,
vnode, vnode,
// component may have been moved before resolve // component may have been moved before resolve.
parentNode(instance.subTree.el)!, // if this is not a hydration, instance.subTree will be the comment
next(instance.subTree), // placeholder.
hydratedEl
? parentNode(hydratedEl)!
: parentNode(instance.subTree.el)!,
// anchor will not be used if this is hydration, so only need to
// consider the comment placeholder case.
hydratedEl ? null : next(instance.subTree),
suspense, suspense,
isSVG isSVG
) )
@ -449,6 +463,53 @@ function createSuspenseBoundary<HostNode, HostElement>(
return suspense return suspense
} }
function hydrateSuspense(
node: Node,
vnode: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean,
rendererInternals: RendererInternals,
hydrateNode: (
node: Node,
vnode: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
optimized: boolean
) => Node | null
): Node | null {
const suspense = (vnode.suspense = createSuspenseBoundary(
vnode,
parentSuspense,
parentComponent,
node.parentNode,
document.createElement('div'),
null,
isSVG,
optimized,
rendererInternals,
true /* hydrating */
))
// there are two possible scenarios for server-rendered suspense:
// - success: ssr content should be fully resolved
// - failure: ssr content should be the fallback branch.
// however, on the client we don't really know if it has failed or not
// attempt to hydrate the DOM assuming it has succeeded, but we still
// need to construct a suspense boundary first
const result = hydrateNode(
node,
suspense.subTree,
parentComponent,
suspense,
optimized
)
if (suspense.deps === 0) {
suspense.resolve()
}
return result
}
export function normalizeSuspenseChildren( export function normalizeSuspenseChildren(
vnode: VNode vnode: VNode
): { ): {

View File

@ -1,5 +1,5 @@
import { VNode, normalizeVNode, Text, Comment, Static, Fragment } from './vnode' import { VNode, normalizeVNode, Text, Comment, Static, Fragment } from './vnode'
import { queuePostFlushCb, flushPostFlushCbs } from './scheduler' import { flushPostFlushCbs } from './scheduler'
import { ComponentInternalInstance } from './component' import { ComponentInternalInstance } from './component'
import { invokeDirectiveHook } from './directives' import { invokeDirectiveHook } from './directives'
import { warn } from './warning' import { warn } from './warning'
@ -11,6 +11,11 @@ import {
isString isString
} from '@vue/shared' } from '@vue/shared'
import { RendererInternals } from './renderer' import { RendererInternals } from './renderer'
import {
SuspenseImpl,
SuspenseBoundary,
queueEffectWithSuspense
} from './components/Suspense'
export type RootHydrateFunction = ( export type RootHydrateFunction = (
vnode: VNode<Node, Element>, vnode: VNode<Node, Element>,
@ -25,16 +30,27 @@ const enum DOMNodeTypes {
let hasMismatch = false let hasMismatch = false
const isSVGContainer = (container: Element) =>
/svg/.test(container.namespaceURI!) && container.tagName !== 'foreignObject'
const isComment = (node: Node): node is Comment =>
node.nodeType === DOMNodeTypes.COMMENT
// Note: hydration is DOM-specific // Note: hydration is DOM-specific
// But we have to place it in core due to tight coupling with core - splitting // But we have to place it in core due to tight coupling with core - splitting
// it out creates a ton of unnecessary complexity. // it out creates a ton of unnecessary complexity.
// Hydration also depends on some renderer internal logic which needs to be // Hydration also depends on some renderer internal logic which needs to be
// passed in via arguments. // passed in via arguments.
export function createHydrationFunctions({ export function createHydrationFunctions(
mt: mountComponent, rendererInternals: RendererInternals<Node, Element>
p: patch, ) {
o: { patchProp, createText } const {
}: RendererInternals<Node, Element>) { mt: mountComponent,
p: patch,
n: next,
o: { patchProp, nextSibling, parentNode }
} = rendererInternals
const hydrate: RootHydrateFunction = (vnode, container) => { const hydrate: RootHydrateFunction = (vnode, container) => {
if (__DEV__ && !container.hasChildNodes()) { if (__DEV__ && !container.hasChildNodes()) {
warn( warn(
@ -45,7 +61,7 @@ export function createHydrationFunctions({
return return
} }
hasMismatch = false hasMismatch = false
hydrateNode(container.firstChild!, vnode) hydrateNode(container.firstChild!, vnode, null, null)
flushPostFlushCbs() flushPostFlushCbs()
if (hasMismatch && !__TEST__) { if (hasMismatch && !__TEST__) {
// this error should show up in production // this error should show up in production
@ -56,7 +72,8 @@ export function createHydrationFunctions({
const hydrateNode = ( const hydrateNode = (
node: Node, node: Node,
vnode: VNode, vnode: VNode,
parentComponent: ComponentInternalInstance | null = null, parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
optimized = false optimized = false
): Node | null => { ): Node | null => {
const { type, shapeFlag } = vnode const { type, shapeFlag } = vnode
@ -67,7 +84,7 @@ export function createHydrationFunctions({
switch (type) { switch (type) {
case Text: case Text:
if (domType !== DOMNodeTypes.TEXT) { if (domType !== DOMNodeTypes.TEXT) {
return handleMismtach(node, vnode, parentComponent) return handleMismtach(node, vnode, parentComponent, parentSuspense)
} }
if ((node as Text).data !== vnode.children) { if ((node as Text).data !== vnode.children) {
hasMismatch = true hasMismatch = true
@ -79,48 +96,83 @@ export function createHydrationFunctions({
) )
;(node as Text).data = vnode.children as string ;(node as Text).data = vnode.children as string
} }
return node.nextSibling return nextSibling(node)
case Comment: case Comment:
if (domType !== DOMNodeTypes.COMMENT) { if (domType !== DOMNodeTypes.COMMENT) {
return handleMismtach(node, vnode, parentComponent) return handleMismtach(node, vnode, parentComponent, parentSuspense)
} }
return node.nextSibling return nextSibling(node)
case Static: case Static:
if (domType !== DOMNodeTypes.ELEMENT) { if (domType !== DOMNodeTypes.ELEMENT) {
return handleMismtach(node, vnode, parentComponent) return handleMismtach(node, vnode, parentComponent, parentSuspense)
} }
return node.nextSibling return nextSibling(node)
case Fragment: case Fragment:
return hydrateFragment(node, vnode, parentComponent, optimized) if (domType !== DOMNodeTypes.COMMENT) {
return handleMismtach(node, vnode, parentComponent, parentSuspense)
}
return hydrateFragment(
node as Comment,
vnode,
parentComponent,
parentSuspense,
optimized
)
default: default:
if (shapeFlag & ShapeFlags.ELEMENT) { if (shapeFlag & ShapeFlags.ELEMENT) {
if ( if (
domType !== DOMNodeTypes.ELEMENT || domType !== DOMNodeTypes.ELEMENT ||
vnode.type !== (node as Element).tagName.toLowerCase() vnode.type !== (node as Element).tagName.toLowerCase()
) { ) {
return handleMismtach(node, vnode, parentComponent) return handleMismtach(node, vnode, parentComponent, parentSuspense)
} }
return hydrateElement( return hydrateElement(
node as Element, node as Element,
vnode, vnode,
parentComponent, parentComponent,
parentSuspense,
optimized optimized
) )
} else if (shapeFlag & ShapeFlags.COMPONENT) { } else if (shapeFlag & ShapeFlags.COMPONENT) {
// when setting up the render effect, if the initial vnode already // when setting up the render effect, if the initial vnode already
// has .el set, the component will perform hydration instead of mount // has .el set, the component will perform hydration instead of mount
// on its sub-tree. // on its sub-tree.
mountComponent(vnode, null, null, parentComponent, null, false) const container = parentNode(node)!
mountComponent(
vnode,
container,
null,
parentComponent,
parentSuspense,
isSVGContainer(container)
)
const subTree = vnode.component!.subTree const subTree = vnode.component!.subTree
return (subTree.anchor || subTree.el).nextSibling if (subTree) {
return next(subTree)
} else {
// no subTree means this is an async component
// try to locate the ending node
return isComment(node) && node.data === '1'
? locateClosingAsyncAnchor(node)
: nextSibling(node)
}
} else if (shapeFlag & ShapeFlags.PORTAL) { } else if (shapeFlag & ShapeFlags.PORTAL) {
if (domType !== DOMNodeTypes.COMMENT) { if (domType !== DOMNodeTypes.COMMENT) {
return handleMismtach(node, vnode, parentComponent) return handleMismtach(node, vnode, parentComponent, parentSuspense)
} }
hydratePortal(vnode, parentComponent, optimized) hydratePortal(vnode, parentComponent, parentSuspense, optimized)
return node.nextSibling return nextSibling(node)
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
// TODO Suspense return (vnode.type as typeof SuspenseImpl).hydrate(
node,
vnode,
parentComponent,
parentSuspense,
isSVGContainer(parentNode(node)!),
optimized,
rendererInternals,
hydrateNode
)
} else if (__DEV__) { } else if (__DEV__) {
warn('Invalid HostVNode type:', type, `(${typeof type})`) warn('Invalid HostVNode type:', type, `(${typeof type})`)
} }
@ -132,6 +184,7 @@ export function createHydrationFunctions({
el: Element, el: Element,
vnode: VNode, vnode: VNode,
parentComponent: ComponentInternalInstance | null, parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
optimized: boolean optimized: boolean
) => { ) => {
optimized = optimized || vnode.dynamicChildren !== null optimized = optimized || vnode.dynamicChildren !== null
@ -161,9 +214,9 @@ export function createHydrationFunctions({
invokeDirectiveHook(onVnodeBeforeMount, parentComponent, vnode) invokeDirectiveHook(onVnodeBeforeMount, parentComponent, vnode)
} }
if (onVnodeMounted != null) { if (onVnodeMounted != null) {
queuePostFlushCb(() => { queueEffectWithSuspense(() => {
invokeDirectiveHook(onVnodeMounted, parentComponent, vnode) invokeDirectiveHook(onVnodeMounted, parentComponent, vnode)
}) }, parentSuspense)
} }
} }
// children // children
@ -177,6 +230,7 @@ export function createHydrationFunctions({
vnode, vnode,
el, el,
parentComponent, parentComponent,
parentSuspense,
optimized optimized
) )
let hasWarned = false let hasWarned = false
@ -215,6 +269,7 @@ export function createHydrationFunctions({
vnode: VNode, vnode: VNode,
container: Element, container: Element,
parentComponent: ComponentInternalInstance | null, parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
optimized: boolean optimized: boolean
): Node | null => { ): Node | null => {
optimized = optimized || vnode.dynamicChildren !== null optimized = optimized || vnode.dynamicChildren !== null
@ -226,7 +281,13 @@ export function createHydrationFunctions({
? children[i] ? children[i]
: (children[i] = normalizeVNode(children[i])) : (children[i] = normalizeVNode(children[i]))
if (node) { if (node) {
node = hydrateNode(node, vnode, parentComponent, optimized) node = hydrateNode(
node,
vnode,
parentComponent,
parentSuspense,
optimized
)
} else { } else {
hasMismatch = true hasMismatch = true
if (__DEV__ && !hasWarned) { if (__DEV__ && !hasWarned) {
@ -237,34 +298,43 @@ export function createHydrationFunctions({
hasWarned = true hasWarned = true
} }
// the SSRed DOM didn't contain enough nodes. Mount the missing ones. // the SSRed DOM didn't contain enough nodes. Mount the missing ones.
patch(null, vnode, container) patch(
null,
vnode,
container,
null,
parentComponent,
parentSuspense,
isSVGContainer(container)
)
} }
} }
return node return node
} }
const hydrateFragment = ( const hydrateFragment = (
node: Node, node: Comment,
vnode: VNode, vnode: VNode,
parentComponent: ComponentInternalInstance | null, parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
optimized: boolean optimized: boolean
) => { ) => {
const parent = node.parentNode as Element return nextSibling(
parent.insertBefore((vnode.el = createText('')), node) (vnode.anchor = hydrateChildren(
const next = hydrateChildren( nextSibling(node)!,
node, vnode,
vnode, parentNode(node)!,
parent, parentComponent,
parentComponent, parentSuspense,
optimized optimized
)!)
) )
parent.insertBefore((vnode.anchor = createText('')), next)
return next
} }
const hydratePortal = ( const hydratePortal = (
vnode: VNode, vnode: VNode,
parentComponent: ComponentInternalInstance | null, parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
optimized: boolean optimized: boolean
) => { ) => {
const targetSelector = vnode.props && vnode.props.target const targetSelector = vnode.props && vnode.props.target
@ -277,6 +347,7 @@ export function createHydrationFunctions({
vnode, vnode,
target, target,
parentComponent, parentComponent,
parentSuspense,
optimized optimized
) )
} else if (__DEV__) { } else if (__DEV__) {
@ -290,7 +361,8 @@ export function createHydrationFunctions({
const handleMismtach = ( const handleMismtach = (
node: Node, node: Node,
vnode: VNode, vnode: VNode,
parentComponent: ComponentInternalInstance | null parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null
) => { ) => {
hasMismatch = true hasMismatch = true
__DEV__ && __DEV__ &&
@ -298,16 +370,43 @@ export function createHydrationFunctions({
`Hydration node mismatch:\n- Client vnode:`, `Hydration node mismatch:\n- Client vnode:`,
vnode.type, vnode.type,
`\n- Server rendered DOM:`, `\n- Server rendered DOM:`,
node node,
node.nodeType === DOMNodeTypes.TEXT ? `(text)` : ``
) )
vnode.el = null vnode.el = null
const next = node.nextSibling const next = nextSibling(node)
const container = node.parentNode as Element const container = parentNode(node)!
container.removeChild(node) container.removeChild(node)
// TODO Suspense and SVG // TODO Suspense
patch(null, vnode, container, next, parentComponent) patch(
null,
vnode,
container,
next,
parentComponent,
parentSuspense,
isSVGContainer(container)
)
return next return next
} }
const locateClosingAsyncAnchor = (node: Node | null): Node | null => {
let match = 0
while (node) {
node = nextSibling(node)
if (node && isComment(node)) {
if (node.data === '1') match++
if (node.data === '0') {
if (match === 0) {
return nextSibling(node)
} else {
match--
}
}
}
}
return node
}
return [hydrate, hydrateNode] as const return [hydrate, hydrateNode] as const
} }

View File

@ -202,7 +202,7 @@ type UnmountChildrenFn<HostNode, HostElement> = (
export type MountComponentFn<HostNode, HostElement> = ( export type MountComponentFn<HostNode, HostElement> = (
initialVNode: VNode<HostNode, HostElement>, initialVNode: VNode<HostNode, HostElement>,
container: HostElement | null, // only null during hydration container: HostElement,
anchor: HostNode | null, anchor: HostNode | null,
parentComponent: ComponentInternalInstance | null, parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary<HostNode, HostElement> | null, parentSuspense: SuspenseBoundary<HostNode, HostElement> | null,
@ -219,7 +219,7 @@ type ProcessTextOrCommentFn<HostNode, HostElement> = (
export type SetupRenderEffectFn<HostNode, HostElement> = ( export type SetupRenderEffectFn<HostNode, HostElement> = (
instance: ComponentInternalInstance, instance: ComponentInternalInstance,
initialVNode: VNode<HostNode, HostElement>, initialVNode: VNode<HostNode, HostElement>,
container: HostElement | null, // only null during hydration container: HostElement,
anchor: HostNode | null, anchor: HostNode | null,
parentSuspense: SuspenseBoundary<HostNode, HostElement> | null, parentSuspense: SuspenseBoundary<HostNode, HostElement> | null,
isSVG: boolean isSVG: boolean
@ -991,7 +991,7 @@ function baseCreateRenderer<
const mountComponent: MountComponentFn<HostNode, HostElement> = ( const mountComponent: MountComponentFn<HostNode, HostElement> = (
initialVNode, initialVNode,
container, // only null during hydration container,
anchor, anchor,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
@ -1031,9 +1031,10 @@ function baseCreateRenderer<
parentSuspense.registerDep(instance, setupRenderEffect) parentSuspense.registerDep(instance, setupRenderEffect)
// Give it a placeholder if this is not hydration // Give it a placeholder if this is not hydration
const placeholder = (instance.subTree = createVNode(Comment)) if (!initialVNode.el) {
processCommentNode(null, placeholder, container!, anchor) const placeholder = (instance.subTree = createVNode(Comment))
initialVNode.el = placeholder.el processCommentNode(null, placeholder, container!, anchor)
}
return return
} }
@ -1069,12 +1070,17 @@ function baseCreateRenderer<
} }
if (initialVNode.el && hydrateNode) { if (initialVNode.el && hydrateNode) {
// vnode has adopted host node - perform hydration instead of mount. // vnode has adopted host node - perform hydration instead of mount.
hydrateNode(initialVNode.el as Node, subTree, instance) hydrateNode(
initialVNode.el as Node,
subTree,
instance,
parentSuspense
)
} else { } else {
patch( patch(
null, null,
subTree, subTree,
container!, // container is only null during hydration container,
anchor, anchor,
instance, instance,
parentSuspense, parentSuspense,

View File

@ -212,7 +212,8 @@ export function createVNode(
): VNode { ): VNode {
if (!type) { if (!type) {
if (__DEV__) { if (__DEV__) {
warn(`Invalid vnode type when creating vnode: ${type}.`) debugger
warn(`fsef Invalid vnode type when creating vnode: ${type}.`)
} }
type = Comment type = Comment
} }

View File

@ -257,7 +257,7 @@ describe('ssr: renderToString', () => {
) )
).toBe( ).toBe(
`<div>parent<div class="child">` + `<div>parent<div class="child">` +
`<span>from slot</span>` + `<!--1--><span>from slot</span><!--0-->` +
`</div></div>` `</div></div>`
) )
@ -272,7 +272,9 @@ describe('ssr: renderToString', () => {
} }
}) })
) )
).toBe(`<div>parent<div class="child">fallback</div></div>`) ).toBe(
`<div>parent<div class="child"><!--1-->fallback<!--0--></div></div>`
)
}) })
test('nested components with vnode slots', async () => { test('nested components with vnode slots', async () => {
@ -316,7 +318,7 @@ describe('ssr: renderToString', () => {
) )
).toBe( ).toBe(
`<div>parent<div class="child">` + `<div>parent<div class="child">` +
`<span>from slot</span>` + `<!--1--><span>from slot</span><!--0-->` +
`</div></div>` `</div></div>`
) )
}) })
@ -328,13 +330,13 @@ describe('ssr: renderToString', () => {
} }
const app = createApp({ const app = createApp({
components: { Child },
template: `<div>parent<Child v-slot="{ msg }"><span>{{ msg }}</span></Child></div>` template: `<div>parent<Child v-slot="{ msg }"><span>{{ msg }}</span></Child></div>`
}) })
app.component('Child', Child)
expect(await renderToString(app)).toBe( expect(await renderToString(app)).toBe(
`<div>parent<div class="child">` + `<div>parent<div class="child">` +
`<span>from slot</span>` + `<!--1--><span>from slot</span><!--0-->` +
`</div></div>` `</div></div>`
) )
}) })
@ -360,6 +362,7 @@ describe('ssr: renderToString', () => {
expect(await renderToString(app)).toBe( expect(await renderToString(app)).toBe(
`<div>parent<div class="child">` + `<div>parent<div class="child">` +
// no comment anchors because slot is used directly as element children
`<span>from slot</span>` + `<span>from slot</span>` +
`</div></div>` `</div></div>`
) )
@ -456,7 +459,9 @@ describe('ssr: renderToString', () => {
createCommentVNode('qux') createCommentVNode('qux')
]) ])
) )
).toBe(`<div>foo<span>bar</span><span>baz</span><!--qux--></div>`) ).toBe(
`<div>foo<span>bar</span><!--1--><span>baz</span><!--0--><!--qux--></div>`
)
}) })
test('void elements', async () => { test('void elements', async () => {

View File

@ -33,7 +33,7 @@ describe('SSR Suspense', () => {
} }
}) })
expect(await renderToString(app)).toBe(`<div>async</div>`) expect(await renderToString(app)).toBe(`<!--1--><div>async</div><!--0-->`)
}) })
test('with async component', async () => { test('with async component', async () => {
@ -49,7 +49,7 @@ describe('SSR Suspense', () => {
} }
}) })
expect(await renderToString(app)).toBe(`<div>async</div>`) expect(await renderToString(app)).toBe(`<!--1--><div>async</div><!--0-->`)
}) })
test('fallback', async () => { test('fallback', async () => {
@ -68,7 +68,9 @@ describe('SSR Suspense', () => {
} }
}) })
expect(await renderToString(app)).toBe(`<div>fallback</div>`) expect(await renderToString(app)).toBe(
`<!--1--><div>fallback</div><!--0-->`
)
expect('Uncaught error in async setup').toHaveBeenWarned() expect('Uncaught error in async setup').toHaveBeenWarned()
}) })
}) })

View File

@ -18,6 +18,8 @@ export function ssrRenderSlot(
push: PushFn, push: PushFn,
parentComponent: ComponentInternalInstance parentComponent: ComponentInternalInstance
) { ) {
// template-compiled slots are always rendered as fragments
push(`<!--1-->`)
const slotFn = slots[slotName] const slotFn = slots[slotName]
if (slotFn) { if (slotFn) {
if (slotFn.length > 1) { if (slotFn.length > 1) {
@ -31,4 +33,5 @@ export function ssrRenderSlot(
} else if (fallbackRenderFn) { } else if (fallbackRenderFn) {
fallbackRenderFn() fallbackRenderFn()
} }
push(`<!--0-->`)
} }

View File

@ -1,19 +1,30 @@
import { PushFn, ResolvedSSRBuffer, createBuffer } from '../renderToString' import { PushFn, ResolvedSSRBuffer, createBuffer } from '../renderToString'
import { NOOP } from '@vue/shared'
type ContentRenderFn = (push: PushFn) => void type ContentRenderFn = (push: PushFn) => void
export async function ssrRenderSuspense({ export async function ssrRenderSuspense({
default: renderContent = NOOP, default: renderContent,
fallback: renderFallback = NOOP fallback: renderFallback
}: Record<string, ContentRenderFn | undefined>): Promise<ResolvedSSRBuffer> { }: Record<string, ContentRenderFn | undefined>): Promise<ResolvedSSRBuffer> {
try { try {
const { push, getBuffer } = createBuffer() if (renderContent) {
renderContent(push) const { push, getBuffer } = createBuffer()
return await getBuffer() push(`<!--1-->`)
renderContent(push)
push(`<!--0-->`)
return await getBuffer()
} else {
return []
}
} catch { } catch {
const { push, getBuffer } = createBuffer() if (renderFallback) {
renderFallback(push) const { push, getBuffer } = createBuffer()
return getBuffer() push(`<!--1-->`)
renderFallback(push)
push(`<!--0-->`)
return getBuffer()
} else {
return []
}
} }
} }

View File

@ -256,7 +256,9 @@ function renderVNode(
push(children ? `<!--${children}-->` : `<!---->`) push(children ? `<!--${children}-->` : `<!---->`)
break break
case Fragment: case Fragment:
push(`<!--1-->`) // open
renderVNodeChildren(push, children as VNodeArrayChildren, parentComponent) renderVNodeChildren(push, children as VNodeArrayChildren, parentComponent)
push(`<!--0-->`) // close
break break
default: default:
if (shapeFlag & ShapeFlags.ELEMENT) { if (shapeFlag & ShapeFlags.ELEMENT) {

View File

@ -106,7 +106,12 @@ function createConfig(format, output, plugins = []) {
format === 'esm-bundler-runtime' ? `src/runtime.ts` : `src/index.ts` format === 'esm-bundler-runtime' ? `src/runtime.ts` : `src/index.ts`
const external = const external =
isGlobalBuild || isRawESMBuild ? [] : Object.keys(pkg.dependencies || {}) isGlobalBuild || isRawESMBuild
? []
: [
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {})
]
const nodePlugins = packageOptions.enableNonBrowserBranches const nodePlugins = packageOptions.enableNonBrowserBranches
? [ ? [