fix(compiler-ssr): proper scope analysis for ssr vnode slot fallback (#7184)

close #7095
This commit is contained in:
edison 2023-10-25 01:01:29 +08:00 committed by GitHub
parent 7374e93f02
commit e09c26bc9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 58 additions and 33 deletions

View File

@ -100,11 +100,12 @@ export const trackVForSlotScopes: NodeTransform = (node, context) => {
export type SlotFnBuilder = (
slotProps: ExpressionNode | undefined,
vForExp: ExpressionNode | undefined,
slotChildren: TemplateChildNode[],
loc: SourceLocation
) => FunctionExpression
const buildClientSlotFn: SlotFnBuilder = (props, children, loc) =>
const buildClientSlotFn: SlotFnBuilder = (props, _vForExp, children, loc) =>
createFunctionExpression(
props,
children,
@ -149,7 +150,7 @@ export function buildSlots(
slotsProperties.push(
createObjectProperty(
arg || createSimpleExpression('default', true),
buildSlotFn(exp, children, loc)
buildSlotFn(exp, undefined, children, loc)
)
)
}
@ -201,11 +202,17 @@ export function buildSlots(
hasDynamicSlots = true
}
const slotFunction = buildSlotFn(slotProps, slotChildren, slotLoc)
const vFor = findDir(slotElement, 'for')
const slotFunction = buildSlotFn(
slotProps,
vFor?.exp,
slotChildren,
slotLoc
)
// check if this slot is conditional (v-if/v-for)
let vIf: DirectiveNode | undefined
let vElse: DirectiveNode | undefined
let vFor: DirectiveNode | undefined
if ((vIf = findDir(slotElement, 'if'))) {
hasDynamicSlots = true
dynamicSlots.push(
@ -257,7 +264,7 @@ export function buildSlots(
createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, vElse.loc)
)
}
} else if ((vFor = findDir(slotElement, 'for'))) {
} else if (vFor) {
hasDynamicSlots = true
const parseResult =
vFor.parseResult ||
@ -306,7 +313,7 @@ export function buildSlots(
props: ExpressionNode | undefined,
children: TemplateChildNode[]
) => {
const fn = buildSlotFn(props, children, loc)
const fn = buildSlotFn(props, undefined, children, loc)
if (__COMPAT__ && context.compatConfig) {
fn.isNonScopedSlot = true
}

View File

@ -181,11 +181,14 @@ describe('ssr: components', () => {
})
test('v-for slot', () => {
expect(
compile(`<foo>
<template v-for="key in names" v-slot:[key]="{ msg }">{{ msg + key + bar }}</template>
</foo>`).code
).toMatchInlineSnapshot(`
const { code } = compile(`<foo>
<template v-for="(key, index) in names" v-slot:[key]="{ msg }">{{ msg + key + index + bar }}</template>
</foo>`)
expect(code).not.toMatch(`_ctx.msg`)
expect(code).not.toMatch(`_ctx.key`)
expect(code).not.toMatch(`_ctx.index`)
expect(code).toMatch(`_ctx.bar`)
expect(code).toMatchInlineSnapshot(`
"const { resolveComponent: _resolveComponent, withCtx: _withCtx, toDisplayString: _toDisplayString, createTextVNode: _createTextVNode, renderList: _renderList, createSlots: _createSlots } = require(\\"vue\\")
const { ssrRenderComponent: _ssrRenderComponent, ssrInterpolate: _ssrInterpolate } = require(\\"vue/server-renderer\\")
@ -193,15 +196,15 @@ describe('ssr: components', () => {
const _component_foo = _resolveComponent(\\"foo\\")
_push(_ssrRenderComponent(_component_foo, _attrs, _createSlots({ _: 2 /* DYNAMIC */ }, [
_renderList(_ctx.names, (key) => {
_renderList(_ctx.names, (key, index) => {
return {
name: key,
fn: _withCtx(({ msg }, _push, _parent, _scopeId) => {
if (_push) {
_push(\`\${_ssrInterpolate(msg + key + _ctx.bar)}\`)
_push(\`\${_ssrInterpolate(msg + key + index + _ctx.bar)}\`)
} else {
return [
_createTextVNode(_toDisplayString(msg + _ctx.key + _ctx.bar), 1 /* TEXT */)
_createTextVNode(_toDisplayString(msg + key + index + _ctx.bar), 1 /* TEXT */)
]
}
})

View File

@ -125,8 +125,10 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
// fallback in case the child is render-fn based). Store them in an array
// for later use.
if (clonedNode.children.length) {
buildSlots(clonedNode, context, (props, children) => {
vnodeBranches.push(createVNodeSlotBranch(props, children, context))
buildSlots(clonedNode, context, (props, vFor, children) => {
vnodeBranches.push(
createVNodeSlotBranch(props, vFor, children, context)
)
return createFunctionExpression(undefined)
})
}
@ -150,7 +152,7 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
const wipEntries: WIPSlotEntry[] = []
wipMap.set(node, wipEntries)
const buildSSRSlotFn: SlotFnBuilder = (props, children, loc) => {
const buildSSRSlotFn: SlotFnBuilder = (props, _vForExp, children, loc) => {
const param0 = (props && stringifyExpression(props)) || `_`
const fn = createFunctionExpression(
[param0, `_push`, `_parent`, `_scopeId`],
@ -277,6 +279,7 @@ const vnodeDirectiveTransforms = {
function createVNodeSlotBranch(
props: ExpressionNode | undefined,
vForExp: ExpressionNode | undefined,
children: TemplateChildNode[],
parentContext: TransformContext
): ReturnStatement {
@ -303,8 +306,8 @@ function createVNodeSlotBranch(
tag: 'template',
tagType: ElementTypes.TEMPLATE,
isSelfClosing: false,
// important: provide v-slot="props" on the wrapper for proper
// scope analysis
// important: provide v-slot="props" and v-for="exp" on the wrapper for
// proper scope analysis
props: [
{
type: NodeTypes.DIRECTIVE,
@ -313,6 +316,14 @@ function createVNodeSlotBranch(
arg: undefined,
modifiers: [],
loc: locStub
},
{
type: NodeTypes.DIRECTIVE,
name: 'for',
exp: vForExp,
arg: undefined,
modifiers: [],
loc: locStub
}
],
children,

View File

@ -36,20 +36,24 @@ export function ssrTransformSuspense(
wipSlots: []
}
wipMap.set(node, wipEntry)
wipEntry.slotsExp = buildSlots(node, context, (_props, children, loc) => {
const fn = createFunctionExpression(
[],
undefined, // no return, assign body later
true, // newline
false, // suspense slots are not treated as normal slots
loc
)
wipEntry.wipSlots.push({
fn,
children
})
return fn
}).slots
wipEntry.slotsExp = buildSlots(
node,
context,
(_props, _vForExp, children, loc) => {
const fn = createFunctionExpression(
[],
undefined, // no return, assign body later
true, // newline
false, // suspense slots are not treated as normal slots
loc
)
wipEntry.wipSlots.push({
fn,
children
})
return fn
}
).slots
}
}
}