diff --git a/packages/compiler-core/__tests__/hydration.spec.ts b/packages/compiler-core/__tests__/hydration.spec.ts
index 6d311bfe2..fc020a457 100644
--- a/packages/compiler-core/__tests__/hydration.spec.ts
+++ b/packages/compiler-core/__tests__/hydration.spec.ts
@@ -98,7 +98,7 @@ describe('SSR hydration', () => {
const msg = ref('foo')
const fn = jest.fn()
const { vnode, container } = mountWithHydration(
- '
foo
',
+ 'foo
',
() =>
h('div', [
[h('span', msg.value), [h('span', { class: msg.value, onClick: fn })]]
@@ -136,7 +136,9 @@ describe('SSR hydration', () => {
msg.value = 'bar'
await nextTick()
- expect(vnode.el.innerHTML).toBe(`bar`)
+ expect(vnode.el.innerHTML).toBe(
+ `bar`
+ )
})
test('portal', async () => {
diff --git a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts
index 89e4b1d8d..9872b22d7 100644
--- a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts
+++ b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts
@@ -219,11 +219,11 @@ describe('ssr: components', () => {
foo: ({ list }, _push, _parent, _scopeId) => {
if (_push) {
if (_ctx.ok) {
- _push(\`\`)
+ _push(\`
\`)
_ssrRenderList(list, (i) => {
_push(\`\`)
})
- _push(\`
\`)
+ _push(\`
\`)
} else {
_push(\`\`)
}
@@ -242,11 +242,11 @@ describe('ssr: components', () => {
bar: ({ ok }, _push, _parent, _scopeId) => {
if (_push) {
if (ok) {
- _push(\`\`)
+ _push(\`
\`)
_ssrRenderList(_ctx.list, (i) => {
_push(\`\`)
})
- _push(\`
\`)
+ _push(\`
\`)
} else {
_push(\`\`)
}
@@ -281,7 +281,7 @@ describe('ssr: components', () => {
.toMatchInlineSnapshot(`
"
return function ssrRender(_ctx, _push, _parent) {
- _push(\`\`)
+ _push(\`\`)
}"
`)
diff --git a/packages/compiler-ssr/__tests__/ssrVFor.spec.ts b/packages/compiler-ssr/__tests__/ssrVFor.spec.ts
index eb301ab9b..62697ffa2 100644
--- a/packages/compiler-ssr/__tests__/ssrVFor.spec.ts
+++ b/packages/compiler-ssr/__tests__/ssrVFor.spec.ts
@@ -6,9 +6,11 @@ describe('ssr: v-for', () => {
"const { ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
+ _push(\`\`)
_ssrRenderList(_ctx.list, (i) => {
_push(\`\`)
})
+ _push(\`\`)
}"
`)
})
@@ -19,9 +21,11 @@ describe('ssr: v-for', () => {
"const { ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
+ _push(\`\`)
_ssrRenderList(_ctx.list, (i) => {
_push(\`foobar
\`)
})
+ _push(\`\`)
}"
`)
})
@@ -37,8 +41,9 @@ describe('ssr: v-for', () => {
"const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
+ _push(\`\`)
_ssrRenderList(_ctx.list, (row, i) => {
- _push(\`\`)
+ _push(\`
\`)
_ssrRenderList(row, (j) => {
_push(\`
\${
_ssrInterpolate(i)
@@ -46,8 +51,9 @@ describe('ssr: v-for', () => {
_ssrInterpolate(j)
}
\`)
})
- _push(\`
\`)
+ _push(\`
\`)
})
+ _push(\`\`)
}"
`)
})
@@ -58,9 +64,11 @@ describe('ssr: v-for', () => {
"const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
+ _push(\`\`)
_ssrRenderList(_ctx.list, (i) => {
- _push(\`\${_ssrInterpolate(i)}\`)
+ _push(\`\${_ssrInterpolate(i)}\`)
})
+ _push(\`\`)
}"
`)
})
@@ -73,9 +81,11 @@ describe('ssr: v-for', () => {
"const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
+ _push(\`\`)
_ssrRenderList(_ctx.list, (i) => {
_push(\`\${_ssrInterpolate(i)}\`)
})
+ _push(\`\`)
}"
`)
})
@@ -89,13 +99,15 @@ describe('ssr: v-for', () => {
"const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
+ _push(\`\`)
_ssrRenderList(_ctx.list, (i) => {
- _push(\`\${
+ _push(\`\${
_ssrInterpolate(i)
}\${
_ssrInterpolate(i + 1)
- }\`)
+ }\`)
})
+ _push(\`\`)
}"
`)
})
@@ -111,9 +123,11 @@ describe('ssr: v-for', () => {
"const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
+ _push(\`\`)
_ssrRenderList(_ctx.list, ({ foo }, index) => {
_push(\`\${_ssrInterpolate(foo + _ctx.bar + index)}
\`)
})
+ _push(\`\`)
}"
`)
})
diff --git a/packages/compiler-ssr/__tests__/ssrVIf.spec.ts b/packages/compiler-ssr/__tests__/ssrVIf.spec.ts
index 8ea086797..0e887c121 100644
--- a/packages/compiler-ssr/__tests__/ssrVIf.spec.ts
+++ b/packages/compiler-ssr/__tests__/ssrVIf.spec.ts
@@ -80,7 +80,7 @@ describe('ssr: v-if', () => {
"
return function ssrRender(_ctx, _push, _parent) {
if (_ctx.foo) {
- _push(\`hello\`)
+ _push(\`hello\`)
} else {
_push(\`\`)
}
@@ -110,7 +110,7 @@ describe('ssr: v-if', () => {
"
return function ssrRender(_ctx, _push, _parent) {
if (_ctx.foo) {
- _push(\`hi
ho
\`)
+ _push(\`hi
ho
\`)
} else {
_push(\`\`)
}
@@ -126,9 +126,11 @@ describe('ssr: v-if', () => {
return function ssrRender(_ctx, _push, _parent) {
if (_ctx.foo) {
+ _push(\`\`)
_ssrRenderList(_ctx.list, (i) => {
_push(\`\`)
})
+ _push(\`\`)
} else {
_push(\`\`)
}
@@ -145,7 +147,7 @@ describe('ssr: v-if', () => {
"
return function ssrRender(_ctx, _push, _parent) {
if (_ctx.foo) {
- _push(\`hi
ho
\`)
+ _push(\`hi
ho
\`)
} else {
_push(\`\`)
}
diff --git a/packages/compiler-ssr/src/ssrCodegenTransform.ts b/packages/compiler-ssr/src/ssrCodegenTransform.ts
index b9ef0c166..c82c82808 100644
--- a/packages/compiler-ssr/src/ssrCodegenTransform.ts
+++ b/packages/compiler-ssr/src/ssrCodegenTransform.ts
@@ -10,7 +10,8 @@ import {
createBlockStatement,
CompilerOptions,
IfStatement,
- CallExpression
+ CallExpression,
+ isText
} from '@vue/compiler-dom'
import { isString, escapeHtml } from '@vue/shared'
import { SSR_INTERPOLATE, ssrHelpers } from './runtimeHelpers'
@@ -28,7 +29,9 @@ import { ssrProcessElement } from './transforms/ssrTransformElement'
export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) {
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)
// Finalize helpers.
@@ -104,8 +107,12 @@ function createChildContext(
export function processChildren(
children: TemplateChildNode[],
- context: SSRTransformContext
+ context: SSRTransformContext,
+ asFragment = false
) {
+ if (asFragment) {
+ context.pushStringPart(``)
+ }
for (let i = 0; i < children.length; i++) {
const child = children[i]
if (child.type === NodeTypes.ELEMENT) {
@@ -128,14 +135,18 @@ export function processChildren(
ssrProcessFor(child, context)
}
}
+ if (asFragment) {
+ context.pushStringPart(``)
+ }
}
export function processChildrenAsStatement(
children: TemplateChildNode[],
parentContext: SSRTransformContext,
+ asFragment = false,
withSlotScopeId = parentContext.withSlotScopeId
): BlockStatement {
const childContext = createChildContext(parentContext, withSlotScopeId)
- processChildren(children, childContext)
+ processChildren(children, childContext, asFragment)
return createBlockStatement(childContext.body)
}
diff --git a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
index 1acf45ee5..ff4ea8fa9 100644
--- a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
+++ b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
@@ -30,7 +30,8 @@ import {
traverseNode,
ExpressionNode,
TemplateNode,
- SUSPENSE
+ SUSPENSE,
+ TRANSITION_GROUP
} from '@vue/compiler-dom'
import { SSR_RENDER_COMPONENT } from '../runtimeHelpers'
import {
@@ -151,7 +152,7 @@ export function ssrProcessComponent(
return ssrProcessSuspense(node, context)
} else {
// real fall-through (e.g. KeepAlive): just render its children.
- processChildren(node.children, context)
+ processChildren(node.children, context, component === TRANSITION_GROUP)
}
} else {
// finish up slot function expressions from the 1st pass.
@@ -167,6 +168,7 @@ export function ssrProcessComponent(
processChildrenAsStatement(
children,
context,
+ false,
true /* withSlotScopeId */
),
vnodeBranch
diff --git a/packages/compiler-ssr/src/transforms/ssrVFor.ts b/packages/compiler-ssr/src/transforms/ssrVFor.ts
index 1921b8f00..a4a78a8db 100644
--- a/packages/compiler-ssr/src/transforms/ssrVFor.ts
+++ b/packages/compiler-ssr/src/transforms/ssrVFor.ts
@@ -4,7 +4,8 @@ import {
processFor,
createCallExpression,
createFunctionExpression,
- createForLoopParams
+ createForLoopParams,
+ NodeTypes
} from '@vue/compiler-dom'
import {
SSRTransformContext,
@@ -21,14 +22,23 @@ export const ssrTransformFor = createStructuralDirectiveTransform(
// This is called during the 2nd transform pass to construct the SSR-sepcific
// codegen nodes.
export function ssrProcessFor(node: ForNode, context: SSRTransformContext) {
+ const needFragmentWrapper =
+ node.children.length !== 1 || node.children[0].type !== NodeTypes.ELEMENT
const renderLoop = createFunctionExpression(
createForLoopParams(node.parseResult)
)
- renderLoop.body = processChildrenAsStatement(node.children, context)
+ renderLoop.body = processChildrenAsStatement(
+ node.children,
+ context,
+ needFragmentWrapper
+ )
+ // v-for always renders a fragment
+ context.pushStringPart(``)
context.pushStatement(
createCallExpression(context.helper(SSR_RENDER_LIST), [
node.source,
renderLoop
])
)
+ context.pushStringPart(``)
}
diff --git a/packages/compiler-ssr/src/transforms/ssrVIf.ts b/packages/compiler-ssr/src/transforms/ssrVIf.ts
index d1c71e1d5..aad7ad14d 100644
--- a/packages/compiler-ssr/src/transforms/ssrVIf.ts
+++ b/packages/compiler-ssr/src/transforms/ssrVIf.ts
@@ -4,7 +4,10 @@ import {
IfNode,
createIfStatement,
createBlockStatement,
- createCallExpression
+ createCallExpression,
+ IfBranchNode,
+ BlockStatement,
+ NodeTypes
} from '@vue/compiler-dom'
import {
SSRTransformContext,
@@ -23,17 +26,14 @@ export function ssrProcessIf(node: IfNode, context: SSRTransformContext) {
const [rootBranch] = node.branches
const ifStatement = createIfStatement(
rootBranch.condition!,
- processChildrenAsStatement(rootBranch.children, context)
+ processIfBranch(rootBranch, context)
)
context.pushStatement(ifStatement)
let currentIf = ifStatement
for (let i = 1; i < node.branches.length; i++) {
const branch = node.branches[i]
- const branchBlockStatement = processChildrenAsStatement(
- branch.children,
- context
- )
+ const branchBlockStatement = processIfBranch(branch, context)
if (branch.condition) {
// else-if
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)
+}
diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts
index bf28cf552..ccba387d5 100644
--- a/packages/runtime-core/src/component.ts
+++ b/packages/runtime-core/src/component.ts
@@ -144,7 +144,6 @@ export interface ComponentInternalInstance {
// suspense related
asyncDep: Promise | null
- asyncResult: unknown
asyncResolved: boolean
// storage for any extra properties
@@ -215,7 +214,6 @@ export function createComponentInstance(
// async dependency management
asyncDep: null,
- asyncResult: null,
asyncResolved: false,
// user namespace for storing whatever the user assigns to `this`
@@ -367,7 +365,7 @@ function setupStatefulComponent(
if (isPromise(setupResult)) {
if (isSSR) {
// return the promise so server-renderer can wait on it
- return setupResult.then(resolvedResult => {
+ return setupResult.then((resolvedResult: unknown) => {
handleSetupResult(instance, resolvedResult, parentSuspense, isSSR)
})
} else if (__FEATURE_SUSPENSE__) {
diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts
index 34066daef..b67ededa3 100644
--- a/packages/runtime-core/src/components/Suspense.ts
+++ b/packages/runtime-core/src/components/Suspense.ts
@@ -5,8 +5,8 @@ import { Slots } from '../componentSlots'
import { RendererInternals, MoveType, SetupRenderEffectFn } from '../renderer'
import { queuePostFlushCb, queueJob } from '../scheduler'
import { updateHOCHostEl } from '../componentRenderUtils'
-import { handleError, ErrorCodes } from '../errorHandling'
import { pushWarningContext, popWarningContext } from '../warning'
+import { handleError, ErrorCodes } from '../errorHandling'
export interface SuspenseProps {
onResolve?: () => void
@@ -59,7 +59,8 @@ export const SuspenseImpl = {
rendererInternals
)
}
- }
+ },
+ hydrate: hydrateSuspense
}
// Force-casted public typing for h and TSX props inference
@@ -97,14 +98,10 @@ function mountSuspense(
rendererInternals
))
- const { content, fallback } = normalizeSuspenseChildren(n2)
- suspense.subTree = content
- suspense.fallbackTree = fallback
-
// start mounting the content subtree in an off-dom container
patch(
null,
- content,
+ suspense.subTree,
hiddenContainer,
null,
parentComponent,
@@ -117,7 +114,7 @@ function mountSuspense(
// mount the fallback tree
patch(
null,
- fallback,
+ suspense.fallbackTree,
container,
anchor,
parentComponent,
@@ -125,7 +122,7 @@ function mountSuspense(
isSVG,
optimized
)
- n2.el = fallback.el
+ n2.el = suspense.fallbackTree.el
} else {
// Suspense has no async deps. Just resolve.
suspense.resolve()
@@ -209,6 +206,7 @@ export interface SuspenseBoundary<
subTree: HostVNode
fallbackTree: HostVNode
deps: number
+ isHydrating: boolean
isResolved: boolean
isUnmounted: boolean
effects: Function[]
@@ -235,7 +233,8 @@ function createSuspenseBoundary(
anchor: HostNode | null,
isSVG: boolean,
optimized: boolean,
- rendererInternals: RendererInternals
+ rendererInternals: RendererInternals,
+ isHydrating = false
): SuspenseBoundary {
const {
p: patch,
@@ -245,6 +244,12 @@ function createSuspenseBoundary(
o: { parentNode }
} = rendererInternals
+ const getCurrentTree = () =>
+ suspense.isResolved || suspense.isHydrating
+ ? suspense.subTree
+ : suspense.fallbackTree
+
+ const { content, fallback } = normalizeSuspenseChildren(vnode)
const suspense: SuspenseBoundary = {
vnode,
parent,
@@ -255,8 +260,9 @@ function createSuspenseBoundary(
hiddenContainer,
anchor,
deps: 0,
- subTree: (null as unknown) as VNode, // will be set immediately after creation
- fallbackTree: (null as unknown) as VNode, // will be set immediately after creation
+ subTree: content,
+ fallbackTree: fallback,
+ isHydrating,
isResolved: false,
isUnmounted: false,
effects: [],
@@ -283,17 +289,22 @@ function createSuspenseBoundary(
container
} = suspense
- // this is initial anchor on mount
- let { anchor } = suspense
- // unmount fallback tree
- if (fallbackTree.el) {
- // 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)
+ if (suspense.isHydrating) {
+ suspense.isHydrating = false
+ } else {
+ // this is initial anchor on mount
+ let { anchor } = suspense
+ // unmount fallback tree
+ if (fallbackTree.el) {
+ // 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!)
// suspense as the root node of a component...
if (parentComponent && parentComponent.subTree === vnode) {
@@ -367,19 +378,12 @@ function createSuspenseBoundary(
},
move(container, anchor, type) {
- move(
- suspense.isResolved ? suspense.subTree : suspense.fallbackTree,
- container,
- anchor,
- type
- )
+ move(getCurrentTree(), container, anchor, type)
suspense.container = container
},
next() {
- return next(
- suspense.isResolved ? suspense.subTree : suspense.fallbackTree
- )
+ return next(getCurrentTree())
},
registerDep(instance, setupRenderEffect) {
@@ -392,6 +396,7 @@ function createSuspenseBoundary(
})
}
+ const hydratedEl = instance.vnode.el
suspense.deps++
instance
.asyncDep!.catch(err => {
@@ -411,14 +416,23 @@ function createSuspenseBoundary(
pushWarningContext(vnode)
}
handleSetupResult(instance, asyncSetupResult, suspense, false)
- // unset placeholder, otherwise this will be treated as a hydration mount
- vnode.el = null
+ if (hydratedEl) {
+ // vnode may have been replaced if an update happened before the
+ // async dep is reoslved.
+ vnode.el = hydratedEl
+ }
setupRenderEffect(
instance,
vnode,
- // component may have been moved before resolve
- parentNode(instance.subTree.el)!,
- next(instance.subTree),
+ // component may have been moved before resolve.
+ // if this is not a hydration, instance.subTree will be the comment
+ // 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,
isSVG
)
@@ -449,6 +463,53 @@ function createSuspenseBoundary(
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(
vnode: VNode
): {
diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts
index 93716ebb8..3aaa29281 100644
--- a/packages/runtime-core/src/hydration.ts
+++ b/packages/runtime-core/src/hydration.ts
@@ -1,5 +1,5 @@
import { VNode, normalizeVNode, Text, Comment, Static, Fragment } from './vnode'
-import { queuePostFlushCb, flushPostFlushCbs } from './scheduler'
+import { flushPostFlushCbs } from './scheduler'
import { ComponentInternalInstance } from './component'
import { invokeDirectiveHook } from './directives'
import { warn } from './warning'
@@ -11,6 +11,11 @@ import {
isString
} from '@vue/shared'
import { RendererInternals } from './renderer'
+import {
+ SuspenseImpl,
+ SuspenseBoundary,
+ queueEffectWithSuspense
+} from './components/Suspense'
export type RootHydrateFunction = (
vnode: VNode,
@@ -25,16 +30,27 @@ const enum DOMNodeTypes {
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
// But we have to place it in core due to tight coupling with core - splitting
// it out creates a ton of unnecessary complexity.
// Hydration also depends on some renderer internal logic which needs to be
// passed in via arguments.
-export function createHydrationFunctions({
- mt: mountComponent,
- p: patch,
- o: { patchProp, createText }
-}: RendererInternals) {
+export function createHydrationFunctions(
+ rendererInternals: RendererInternals
+) {
+ const {
+ mt: mountComponent,
+ p: patch,
+ n: next,
+ o: { patchProp, nextSibling, parentNode }
+ } = rendererInternals
+
const hydrate: RootHydrateFunction = (vnode, container) => {
if (__DEV__ && !container.hasChildNodes()) {
warn(
@@ -45,7 +61,7 @@ export function createHydrationFunctions({
return
}
hasMismatch = false
- hydrateNode(container.firstChild!, vnode)
+ hydrateNode(container.firstChild!, vnode, null, null)
flushPostFlushCbs()
if (hasMismatch && !__TEST__) {
// this error should show up in production
@@ -56,7 +72,8 @@ export function createHydrationFunctions({
const hydrateNode = (
node: Node,
vnode: VNode,
- parentComponent: ComponentInternalInstance | null = null,
+ parentComponent: ComponentInternalInstance | null,
+ parentSuspense: SuspenseBoundary | null,
optimized = false
): Node | null => {
const { type, shapeFlag } = vnode
@@ -67,7 +84,7 @@ export function createHydrationFunctions({
switch (type) {
case Text:
if (domType !== DOMNodeTypes.TEXT) {
- return handleMismtach(node, vnode, parentComponent)
+ return handleMismtach(node, vnode, parentComponent, parentSuspense)
}
if ((node as Text).data !== vnode.children) {
hasMismatch = true
@@ -79,48 +96,83 @@ export function createHydrationFunctions({
)
;(node as Text).data = vnode.children as string
}
- return node.nextSibling
+ return nextSibling(node)
case Comment:
if (domType !== DOMNodeTypes.COMMENT) {
- return handleMismtach(node, vnode, parentComponent)
+ return handleMismtach(node, vnode, parentComponent, parentSuspense)
}
- return node.nextSibling
+ return nextSibling(node)
case Static:
if (domType !== DOMNodeTypes.ELEMENT) {
- return handleMismtach(node, vnode, parentComponent)
+ return handleMismtach(node, vnode, parentComponent, parentSuspense)
}
- return node.nextSibling
+ return nextSibling(node)
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:
if (shapeFlag & ShapeFlags.ELEMENT) {
if (
domType !== DOMNodeTypes.ELEMENT ||
vnode.type !== (node as Element).tagName.toLowerCase()
) {
- return handleMismtach(node, vnode, parentComponent)
+ return handleMismtach(node, vnode, parentComponent, parentSuspense)
}
return hydrateElement(
node as Element,
vnode,
parentComponent,
+ parentSuspense,
optimized
)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
// when setting up the render effect, if the initial vnode already
// has .el set, the component will perform hydration instead of mount
// 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
- 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) {
if (domType !== DOMNodeTypes.COMMENT) {
- return handleMismtach(node, vnode, parentComponent)
+ return handleMismtach(node, vnode, parentComponent, parentSuspense)
}
- hydratePortal(vnode, parentComponent, optimized)
- return node.nextSibling
+ hydratePortal(vnode, parentComponent, parentSuspense, optimized)
+ return nextSibling(node)
} 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__) {
warn('Invalid HostVNode type:', type, `(${typeof type})`)
}
@@ -132,6 +184,7 @@ export function createHydrationFunctions({
el: Element,
vnode: VNode,
parentComponent: ComponentInternalInstance | null,
+ parentSuspense: SuspenseBoundary | null,
optimized: boolean
) => {
optimized = optimized || vnode.dynamicChildren !== null
@@ -161,9 +214,9 @@ export function createHydrationFunctions({
invokeDirectiveHook(onVnodeBeforeMount, parentComponent, vnode)
}
if (onVnodeMounted != null) {
- queuePostFlushCb(() => {
+ queueEffectWithSuspense(() => {
invokeDirectiveHook(onVnodeMounted, parentComponent, vnode)
- })
+ }, parentSuspense)
}
}
// children
@@ -177,6 +230,7 @@ export function createHydrationFunctions({
vnode,
el,
parentComponent,
+ parentSuspense,
optimized
)
let hasWarned = false
@@ -215,6 +269,7 @@ export function createHydrationFunctions({
vnode: VNode,
container: Element,
parentComponent: ComponentInternalInstance | null,
+ parentSuspense: SuspenseBoundary | null,
optimized: boolean
): Node | null => {
optimized = optimized || vnode.dynamicChildren !== null
@@ -226,7 +281,13 @@ export function createHydrationFunctions({
? children[i]
: (children[i] = normalizeVNode(children[i]))
if (node) {
- node = hydrateNode(node, vnode, parentComponent, optimized)
+ node = hydrateNode(
+ node,
+ vnode,
+ parentComponent,
+ parentSuspense,
+ optimized
+ )
} else {
hasMismatch = true
if (__DEV__ && !hasWarned) {
@@ -237,34 +298,43 @@ export function createHydrationFunctions({
hasWarned = true
}
// 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
}
const hydrateFragment = (
- node: Node,
+ node: Comment,
vnode: VNode,
parentComponent: ComponentInternalInstance | null,
+ parentSuspense: SuspenseBoundary | null,
optimized: boolean
) => {
- const parent = node.parentNode as Element
- parent.insertBefore((vnode.el = createText('')), node)
- const next = hydrateChildren(
- node,
- vnode,
- parent,
- parentComponent,
- optimized
+ return nextSibling(
+ (vnode.anchor = hydrateChildren(
+ nextSibling(node)!,
+ vnode,
+ parentNode(node)!,
+ parentComponent,
+ parentSuspense,
+ optimized
+ )!)
)
- parent.insertBefore((vnode.anchor = createText('')), next)
- return next
}
const hydratePortal = (
vnode: VNode,
parentComponent: ComponentInternalInstance | null,
+ parentSuspense: SuspenseBoundary | null,
optimized: boolean
) => {
const targetSelector = vnode.props && vnode.props.target
@@ -277,6 +347,7 @@ export function createHydrationFunctions({
vnode,
target,
parentComponent,
+ parentSuspense,
optimized
)
} else if (__DEV__) {
@@ -290,7 +361,8 @@ export function createHydrationFunctions({
const handleMismtach = (
node: Node,
vnode: VNode,
- parentComponent: ComponentInternalInstance | null
+ parentComponent: ComponentInternalInstance | null,
+ parentSuspense: SuspenseBoundary | null
) => {
hasMismatch = true
__DEV__ &&
@@ -298,16 +370,43 @@ export function createHydrationFunctions({
`Hydration node mismatch:\n- Client vnode:`,
vnode.type,
`\n- Server rendered DOM:`,
- node
+ node,
+ node.nodeType === DOMNodeTypes.TEXT ? `(text)` : ``
)
vnode.el = null
- const next = node.nextSibling
- const container = node.parentNode as Element
+ const next = nextSibling(node)
+ const container = parentNode(node)!
container.removeChild(node)
- // TODO Suspense and SVG
- patch(null, vnode, container, next, parentComponent)
+ // TODO Suspense
+ patch(
+ null,
+ vnode,
+ container,
+ next,
+ parentComponent,
+ parentSuspense,
+ isSVGContainer(container)
+ )
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
}
diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts
index 741beafec..f3d5fcbea 100644
--- a/packages/runtime-core/src/renderer.ts
+++ b/packages/runtime-core/src/renderer.ts
@@ -202,7 +202,7 @@ type UnmountChildrenFn = (
export type MountComponentFn = (
initialVNode: VNode,
- container: HostElement | null, // only null during hydration
+ container: HostElement,
anchor: HostNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
@@ -219,7 +219,7 @@ type ProcessTextOrCommentFn = (
export type SetupRenderEffectFn = (
instance: ComponentInternalInstance,
initialVNode: VNode,
- container: HostElement | null, // only null during hydration
+ container: HostElement,
anchor: HostNode | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean
@@ -991,7 +991,7 @@ function baseCreateRenderer<
const mountComponent: MountComponentFn = (
initialVNode,
- container, // only null during hydration
+ container,
anchor,
parentComponent,
parentSuspense,
@@ -1031,9 +1031,10 @@ function baseCreateRenderer<
parentSuspense.registerDep(instance, setupRenderEffect)
// Give it a placeholder if this is not hydration
- const placeholder = (instance.subTree = createVNode(Comment))
- processCommentNode(null, placeholder, container!, anchor)
- initialVNode.el = placeholder.el
+ if (!initialVNode.el) {
+ const placeholder = (instance.subTree = createVNode(Comment))
+ processCommentNode(null, placeholder, container!, anchor)
+ }
return
}
@@ -1069,12 +1070,17 @@ function baseCreateRenderer<
}
if (initialVNode.el && hydrateNode) {
// 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 {
patch(
null,
subTree,
- container!, // container is only null during hydration
+ container,
anchor,
instance,
parentSuspense,
diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts
index 272731e8a..775ca7cc9 100644
--- a/packages/runtime-core/src/vnode.ts
+++ b/packages/runtime-core/src/vnode.ts
@@ -212,7 +212,8 @@ export function createVNode(
): VNode {
if (!type) {
if (__DEV__) {
- warn(`Invalid vnode type when creating vnode: ${type}.`)
+ debugger
+ warn(`fsef Invalid vnode type when creating vnode: ${type}.`)
}
type = Comment
}
diff --git a/packages/server-renderer/__tests__/renderToString.spec.ts b/packages/server-renderer/__tests__/renderToString.spec.ts
index 07d0e2f4f..710dbc9f4 100644
--- a/packages/server-renderer/__tests__/renderToString.spec.ts
+++ b/packages/server-renderer/__tests__/renderToString.spec.ts
@@ -257,7 +257,7 @@ describe('ssr: renderToString', () => {
)
).toBe(
`parent
` +
- `from slot` +
+ `from slot` +
`
`
)
@@ -272,7 +272,9 @@ describe('ssr: renderToString', () => {
}
})
)
- ).toBe(``)
+ ).toBe(
+ ``
+ )
})
test('nested components with vnode slots', async () => {
@@ -316,7 +318,7 @@ describe('ssr: renderToString', () => {
)
).toBe(
`parent
` +
- `from slot` +
+ `from slot` +
`
`
)
})
@@ -328,13 +330,13 @@ describe('ssr: renderToString', () => {
}
const app = createApp({
+ components: { Child },
template: `parent{{ msg }}
`
})
- app.component('Child', Child)
expect(await renderToString(app)).toBe(
`parent
` +
- `from slot` +
+ `from slot` +
`
`
)
})
@@ -360,6 +362,7 @@ describe('ssr: renderToString', () => {
expect(await renderToString(app)).toBe(
`parent
` +
+ // no comment anchors because slot is used directly as element children
`from slot` +
`
`
)
@@ -456,7 +459,9 @@ describe('ssr: renderToString', () => {
createCommentVNode('qux')
])
)
- ).toBe(`foobarbaz
`)
+ ).toBe(
+ `foobarbaz
`
+ )
})
test('void elements', async () => {
diff --git a/packages/server-renderer/__tests__/ssrSuspense.spec.ts b/packages/server-renderer/__tests__/ssrSuspense.spec.ts
index 38e016981..02a50fa16 100644
--- a/packages/server-renderer/__tests__/ssrSuspense.spec.ts
+++ b/packages/server-renderer/__tests__/ssrSuspense.spec.ts
@@ -33,7 +33,7 @@ describe('SSR Suspense', () => {
}
})
- expect(await renderToString(app)).toBe(`async
`)
+ expect(await renderToString(app)).toBe(`async
`)
})
test('with async component', async () => {
@@ -49,7 +49,7 @@ describe('SSR Suspense', () => {
}
})
- expect(await renderToString(app)).toBe(`async
`)
+ expect(await renderToString(app)).toBe(`async
`)
})
test('fallback', async () => {
@@ -68,7 +68,9 @@ describe('SSR Suspense', () => {
}
})
- expect(await renderToString(app)).toBe(`fallback
`)
+ expect(await renderToString(app)).toBe(
+ `fallback
`
+ )
expect('Uncaught error in async setup').toHaveBeenWarned()
})
})
diff --git a/packages/server-renderer/src/helpers/ssrRenderSlot.ts b/packages/server-renderer/src/helpers/ssrRenderSlot.ts
index 1aae61f2b..bfdf137f0 100644
--- a/packages/server-renderer/src/helpers/ssrRenderSlot.ts
+++ b/packages/server-renderer/src/helpers/ssrRenderSlot.ts
@@ -18,6 +18,8 @@ export function ssrRenderSlot(
push: PushFn,
parentComponent: ComponentInternalInstance
) {
+ // template-compiled slots are always rendered as fragments
+ push(``)
const slotFn = slots[slotName]
if (slotFn) {
if (slotFn.length > 1) {
@@ -31,4 +33,5 @@ export function ssrRenderSlot(
} else if (fallbackRenderFn) {
fallbackRenderFn()
}
+ push(``)
}
diff --git a/packages/server-renderer/src/helpers/ssrRenderSuspense.ts b/packages/server-renderer/src/helpers/ssrRenderSuspense.ts
index efb4bcd9c..3f1f66d3e 100644
--- a/packages/server-renderer/src/helpers/ssrRenderSuspense.ts
+++ b/packages/server-renderer/src/helpers/ssrRenderSuspense.ts
@@ -1,19 +1,30 @@
import { PushFn, ResolvedSSRBuffer, createBuffer } from '../renderToString'
-import { NOOP } from '@vue/shared'
type ContentRenderFn = (push: PushFn) => void
export async function ssrRenderSuspense({
- default: renderContent = NOOP,
- fallback: renderFallback = NOOP
+ default: renderContent,
+ fallback: renderFallback
}: Record): Promise {
try {
- const { push, getBuffer } = createBuffer()
- renderContent(push)
- return await getBuffer()
+ if (renderContent) {
+ const { push, getBuffer } = createBuffer()
+ push(``)
+ renderContent(push)
+ push(``)
+ return await getBuffer()
+ } else {
+ return []
+ }
} catch {
- const { push, getBuffer } = createBuffer()
- renderFallback(push)
- return getBuffer()
+ if (renderFallback) {
+ const { push, getBuffer } = createBuffer()
+ push(``)
+ renderFallback(push)
+ push(``)
+ return getBuffer()
+ } else {
+ return []
+ }
}
}
diff --git a/packages/server-renderer/src/renderToString.ts b/packages/server-renderer/src/renderToString.ts
index ee9dd8143..524c19c5e 100644
--- a/packages/server-renderer/src/renderToString.ts
+++ b/packages/server-renderer/src/renderToString.ts
@@ -256,7 +256,9 @@ function renderVNode(
push(children ? `` : ``)
break
case Fragment:
+ push(``) // open
renderVNodeChildren(push, children as VNodeArrayChildren, parentComponent)
+ push(``) // close
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
diff --git a/rollup.config.js b/rollup.config.js
index c90f4e03c..8aa1d1980 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -106,7 +106,12 @@ function createConfig(format, output, plugins = []) {
format === 'esm-bundler-runtime' ? `src/runtime.ts` : `src/index.ts`
const external =
- isGlobalBuild || isRawESMBuild ? [] : Object.keys(pkg.dependencies || {})
+ isGlobalBuild || isRawESMBuild
+ ? []
+ : [
+ ...Object.keys(pkg.dependencies || {}),
+ ...Object.keys(pkg.peerDependencies || {})
+ ]
const nodePlugins = packageOptions.enableNonBrowserBranches
? [