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

View File

@ -181,11 +181,14 @@ describe('ssr: components', () => {
}) })
test('v-for slot', () => { test('v-for slot', () => {
expect( const { code } = compile(`<foo>
compile(`<foo> <template v-for="(key, index) in names" v-slot:[key]="{ msg }">{{ msg + key + index + bar }}</template>
<template v-for="key in names" v-slot:[key]="{ msg }">{{ msg + key + bar }}</template> </foo>`)
</foo>`).code expect(code).not.toMatch(`_ctx.msg`)
).toMatchInlineSnapshot(` 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 { resolveComponent: _resolveComponent, withCtx: _withCtx, toDisplayString: _toDisplayString, createTextVNode: _createTextVNode, renderList: _renderList, createSlots: _createSlots } = require(\\"vue\\")
const { ssrRenderComponent: _ssrRenderComponent, ssrInterpolate: _ssrInterpolate } = require(\\"vue/server-renderer\\") const { ssrRenderComponent: _ssrRenderComponent, ssrInterpolate: _ssrInterpolate } = require(\\"vue/server-renderer\\")
@ -193,15 +196,15 @@ describe('ssr: components', () => {
const _component_foo = _resolveComponent(\\"foo\\") const _component_foo = _resolveComponent(\\"foo\\")
_push(_ssrRenderComponent(_component_foo, _attrs, _createSlots({ _: 2 /* DYNAMIC */ }, [ _push(_ssrRenderComponent(_component_foo, _attrs, _createSlots({ _: 2 /* DYNAMIC */ }, [
_renderList(_ctx.names, (key) => { _renderList(_ctx.names, (key, index) => {
return { return {
name: key, name: key,
fn: _withCtx(({ msg }, _push, _parent, _scopeId) => { fn: _withCtx(({ msg }, _push, _parent, _scopeId) => {
if (_push) { if (_push) {
_push(\`\${_ssrInterpolate(msg + key + _ctx.bar)}\`) _push(\`\${_ssrInterpolate(msg + key + index + _ctx.bar)}\`)
} else { } else {
return [ 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 // fallback in case the child is render-fn based). Store them in an array
// for later use. // for later use.
if (clonedNode.children.length) { if (clonedNode.children.length) {
buildSlots(clonedNode, context, (props, children) => { buildSlots(clonedNode, context, (props, vFor, children) => {
vnodeBranches.push(createVNodeSlotBranch(props, children, context)) vnodeBranches.push(
createVNodeSlotBranch(props, vFor, children, context)
)
return createFunctionExpression(undefined) return createFunctionExpression(undefined)
}) })
} }
@ -150,7 +152,7 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
const wipEntries: WIPSlotEntry[] = [] const wipEntries: WIPSlotEntry[] = []
wipMap.set(node, wipEntries) wipMap.set(node, wipEntries)
const buildSSRSlotFn: SlotFnBuilder = (props, children, loc) => { const buildSSRSlotFn: SlotFnBuilder = (props, _vForExp, children, loc) => {
const param0 = (props && stringifyExpression(props)) || `_` const param0 = (props && stringifyExpression(props)) || `_`
const fn = createFunctionExpression( const fn = createFunctionExpression(
[param0, `_push`, `_parent`, `_scopeId`], [param0, `_push`, `_parent`, `_scopeId`],
@ -277,6 +279,7 @@ const vnodeDirectiveTransforms = {
function createVNodeSlotBranch( function createVNodeSlotBranch(
props: ExpressionNode | undefined, props: ExpressionNode | undefined,
vForExp: ExpressionNode | undefined,
children: TemplateChildNode[], children: TemplateChildNode[],
parentContext: TransformContext parentContext: TransformContext
): ReturnStatement { ): ReturnStatement {
@ -303,8 +306,8 @@ function createVNodeSlotBranch(
tag: 'template', tag: 'template',
tagType: ElementTypes.TEMPLATE, tagType: ElementTypes.TEMPLATE,
isSelfClosing: false, isSelfClosing: false,
// important: provide v-slot="props" on the wrapper for proper // important: provide v-slot="props" and v-for="exp" on the wrapper for
// scope analysis // proper scope analysis
props: [ props: [
{ {
type: NodeTypes.DIRECTIVE, type: NodeTypes.DIRECTIVE,
@ -313,6 +316,14 @@ function createVNodeSlotBranch(
arg: undefined, arg: undefined,
modifiers: [], modifiers: [],
loc: locStub loc: locStub
},
{
type: NodeTypes.DIRECTIVE,
name: 'for',
exp: vForExp,
arg: undefined,
modifiers: [],
loc: locStub
} }
], ],
children, children,

View File

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