Skip to content

Commit e17e603

Browse files
authored
feat(compiler-core): resolve slot prop bindings as components (#13573)
close #8553
1 parent 2668703 commit e17e603

6 files changed

Lines changed: 241 additions & 11 deletions

File tree

packages/compiler-core/__tests__/transforms/transformElement.spec.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,121 @@ describe('compiler: element transform', () => {
121121
expect(node.tag).toBe(`Example`)
122122
})
123123

124+
test('resolve component from scoped slot bindings and shadows setup bindings', () => {
125+
const { code } = baseCompile(
126+
`<Example v-slot="{ Foo }"><Foo /></Example>`,
127+
{
128+
prefixIdentifiers: true,
129+
bindingMetadata: {
130+
Example: BindingTypes.SETUP_CONST,
131+
Foo: BindingTypes.SETUP_MAYBE_REF,
132+
},
133+
},
134+
)
135+
136+
expect(code).toContain(`_createVNode(Foo)`)
137+
expect(code).not.toContain(`_component_Foo`)
138+
expect(code).not.toContain(`_resolveComponent("Foo")`)
139+
expect(code).not.toContain(`$setup["Foo"]`)
140+
})
141+
142+
test('resolve kebab-cased component from scoped slot bindings', () => {
143+
const { code } = baseCompile(
144+
`<Example v-slot="{ fooBar }"><foo-bar /></Example>`,
145+
{
146+
prefixIdentifiers: true,
147+
isNativeTag: tag => tag !== 'foo-bar',
148+
bindingMetadata: {
149+
Example: BindingTypes.SETUP_CONST,
150+
},
151+
},
152+
)
153+
154+
expect(code).toContain(`_createVNode(fooBar)`)
155+
expect(code).not.toContain(`_component_foo_bar`)
156+
expect(code).not.toContain(`_resolveComponent("foo-bar")`)
157+
})
158+
159+
test('does not resolve component from inactive scoped slot bindings', () => {
160+
const { code } = baseCompile(
161+
`<Example v-slot="{ Foo }"><Foo /></Example><Foo />`,
162+
{
163+
prefixIdentifiers: true,
164+
bindingMetadata: {
165+
Example: BindingTypes.SETUP_CONST,
166+
},
167+
},
168+
)
169+
170+
expect(code).toContain(`_createVNode(Foo)`)
171+
expect(code).toContain(`const _component_Foo = _resolveComponent("Foo")`)
172+
expect(code).toContain(`_createVNode(_component_Foo)`)
173+
})
174+
175+
test('does not resolve component from v-for bindings', () => {
176+
const { code } = baseCompile(
177+
`<template v-for="Foo in list"><Foo :value="Foo" /></template>`,
178+
{
179+
prefixIdentifiers: true,
180+
},
181+
)
182+
183+
expect(code).toContain(`const _component_Foo = _resolveComponent("Foo")`)
184+
expect(code).toContain(`_createBlock(_component_Foo`)
185+
expect(code).toContain(`value: Foo`)
186+
expect(code).not.toContain(`_createVNode(Foo`)
187+
expect(code).not.toContain(`_createBlock(Foo`)
188+
})
189+
190+
test('does not resolve component from scoped slot bindings shadowed by v-for', () => {
191+
const { code } = baseCompile(
192+
`<Example v-slot="{ Foo }"><template v-for="Foo in list"><Foo /></template></Example>`,
193+
{
194+
prefixIdentifiers: true,
195+
bindingMetadata: {
196+
Example: BindingTypes.SETUP_CONST,
197+
},
198+
},
199+
)
200+
201+
expect(code).toContain(`const _component_Foo = _resolveComponent("Foo")`)
202+
expect(code).toContain(`_createBlock(_component_Foo)`)
203+
expect(code).not.toContain(`_createVNode(Foo)`)
204+
expect(code).not.toContain(`_createBlock(Foo)`)
205+
})
206+
207+
test('resolve component from scoped slot bindings shadowing v-for', () => {
208+
const { code } = baseCompile(
209+
`<div v-for="Foo in list"><Example v-slot="{ Foo }"><Foo /></Example></div>`,
210+
{
211+
prefixIdentifiers: true,
212+
bindingMetadata: {
213+
Example: BindingTypes.SETUP_CONST,
214+
},
215+
},
216+
)
217+
218+
expect(code).toContain(`_createVNode(Foo)`)
219+
expect(code).not.toContain(`_component_Foo`)
220+
expect(code).not.toContain(`_resolveComponent("Foo")`)
221+
})
222+
223+
test('resolve component from template scoped slot bindings', () => {
224+
const { code } = baseCompile(
225+
`<Example><template #default="{ Foo }"><Foo /></template></Example>`,
226+
{
227+
prefixIdentifiers: true,
228+
bindingMetadata: {
229+
Example: BindingTypes.SETUP_CONST,
230+
},
231+
},
232+
)
233+
234+
expect(code).toContain(`_createVNode(Foo)`)
235+
expect(code).not.toContain(`_component_Foo`)
236+
expect(code).not.toContain(`_resolveComponent("Foo")`)
237+
})
238+
124239
test('resolve namespaced component from setup bindings', () => {
125240
const { root, node } = parseWithElementTransform(`<Foo.Example/>`, {
126241
bindingMetadata: {
@@ -175,6 +290,23 @@ describe('compiler: element transform', () => {
175290
expect(node.tag).toBe('_unref($props["Foo"]).Example')
176291
})
177292

293+
test('resolve namespaced component from scoped slot bindings', () => {
294+
const { code } = baseCompile(
295+
`<Example v-slot="slotProps"><slot-props.Foo /></Example>`,
296+
{
297+
prefixIdentifiers: true,
298+
isNativeTag: tag => tag !== 'slot-props.Foo',
299+
bindingMetadata: {
300+
Example: BindingTypes.SETUP_CONST,
301+
},
302+
},
303+
)
304+
305+
expect(code).toContain(`_createVNode(slotProps.Foo)`)
306+
expect(code).not.toContain(`_component_slot_props`)
307+
expect(code).not.toContain(`_resolveComponent("slot-props.Foo")`)
308+
})
309+
178310
test('do not resolve component from non-script-setup bindings', () => {
179311
const bindingMetadata = {
180312
Example: BindingTypes.SETUP_MAYBE_REF,

packages/compiler-core/src/transform.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ export interface ImportItem {
8181
path: string
8282
}
8383

84+
type IdentifierScopeType = 'local' | 'slot'
85+
8486
export interface TransformContext
8587
extends
8688
Required<Omit<TransformOptions, keyof CompilerCompatOptions>>,
@@ -95,6 +97,7 @@ export interface TransformContext
9597
temps: number
9698
cached: (CacheExpression | null)[]
9799
identifiers: { [name: string]: number | undefined }
100+
identifierScopes: { [name: string]: IdentifierScopeType[] | undefined }
98101
scopes: {
99102
vFor: number
100103
vSlot: number
@@ -114,8 +117,9 @@ export interface TransformContext
114117
replaceNode(node: TemplateChildNode): void
115118
removeNode(node?: TemplateChildNode): void
116119
onNodeRemoved(): void
117-
addIdentifiers(exp: ExpressionNode | string): void
120+
addIdentifiers(exp: ExpressionNode | string, type?: IdentifierScopeType): void
118121
removeIdentifiers(exp: ExpressionNode | string): void
122+
isSlotScopeIdentifier(name: string): boolean
119123
hoist(exp: string | JSChildNode | ArrayExpression): SimpleExpressionNode
120124
cache(exp: JSChildNode, isVNode?: boolean, inVOnce?: boolean): CacheExpression
121125
constantCache: WeakMap<TemplateChildNode, ConstantTypes>
@@ -193,6 +197,7 @@ export function createTransformContext(
193197
constantCache: new WeakMap(),
194198
temps: 0,
195199
identifiers: Object.create(null),
200+
identifierScopes: Object.create(null),
196201
scopes: {
197202
vFor: 0,
198203
vSlot: 0,
@@ -267,15 +272,15 @@ export function createTransformContext(
267272
context.parent!.children.splice(removalIndex, 1)
268273
},
269274
onNodeRemoved: NOOP,
270-
addIdentifiers(exp) {
275+
addIdentifiers(exp, type = 'local') {
271276
// identifier tracking only happens in non-browser builds.
272277
if (!__BROWSER__) {
273278
if (isString(exp)) {
274-
addId(exp)
279+
addId(exp, type)
275280
} else if (exp.identifiers) {
276-
exp.identifiers.forEach(addId)
281+
exp.identifiers.forEach(id => addId(id, type))
277282
} else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
278-
addId(exp.content)
283+
addId(exp.content, type)
279284
}
280285
}
281286
},
@@ -290,6 +295,10 @@ export function createTransformContext(
290295
}
291296
}
292297
},
298+
isSlotScopeIdentifier(name) {
299+
const scopes = context.identifierScopes[name]
300+
return scopes ? scopes[scopes.length - 1] === 'slot' : false
301+
},
293302
hoist(exp) {
294303
if (isString(exp)) exp = createSimpleExpression(exp)
295304
context.hoists.push(exp)
@@ -318,16 +327,21 @@ export function createTransformContext(
318327
context.filters = new Set()
319328
}
320329

321-
function addId(id: string) {
322-
const { identifiers } = context
330+
function addId(id: string, type: IdentifierScopeType) {
331+
const { identifiers, identifierScopes } = context
323332
if (identifiers[id] === undefined) {
324333
identifiers[id] = 0
325334
}
326335
identifiers[id]!++
336+
;(identifierScopes[id] || (identifierScopes[id] = [])).push(type)
327337
}
328338

329339
function removeId(id: string) {
330340
context.identifiers[id]!--
341+
const scopes = context.identifierScopes[id]
342+
if (scopes) {
343+
scopes.pop()
344+
}
331345
}
332346

333347
return context

packages/compiler-core/src/transforms/transformElement.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,21 @@ export function resolveComponentType(
282282
return builtIn
283283
}
284284

285-
// 3. user component (from setup bindings)
285+
// 3. component from slot props
286+
// this is skipped in browser build since browser builds do not perform
287+
// identifier tracking.
288+
if (!__BROWSER__) {
289+
const fromScope = resolveSlotScopeReference(tag, context)
290+
if (fromScope) return fromScope
291+
292+
const dotIndex = tag.indexOf('.')
293+
if (dotIndex > 0) {
294+
const ns = resolveSlotScopeReference(tag.slice(0, dotIndex), context)
295+
if (ns) return ns + tag.slice(dotIndex)
296+
}
297+
}
298+
299+
// 4. user component (from setup bindings)
286300
// this is skipped in browser build since browser builds do not perform
287301
// binding analysis.
288302
if (!__BROWSER__) {
@@ -299,7 +313,7 @@ export function resolveComponentType(
299313
}
300314
}
301315

302-
// 4. Self referencing component (inferred from filename)
316+
// 5. Self referencing component (inferred from filename)
303317
if (
304318
!__BROWSER__ &&
305319
context.selfName &&
@@ -313,12 +327,29 @@ export function resolveComponentType(
313327
return toValidAssetId(tag, `component`)
314328
}
315329

316-
// 5. user component (resolve)
330+
// 6. user component (resolve)
317331
context.helper(RESOLVE_COMPONENT)
318332
context.components.add(tag)
319333
return toValidAssetId(tag, `component`)
320334
}
321335

336+
function resolveSlotScopeReference(name: string, context: TransformContext) {
337+
const camelName = camelize(name)
338+
const PascalName = capitalize(camelName)
339+
const isInSlotScope = (reference: string) =>
340+
context.isSlotScopeIdentifier(reference)
341+
342+
if (isInSlotScope(name)) {
343+
return name
344+
}
345+
if (isInSlotScope(camelName)) {
346+
return camelName
347+
}
348+
if (isInSlotScope(PascalName)) {
349+
return PascalName
350+
}
351+
}
352+
322353
function resolveSetupReference(name: string, context: TransformContext) {
323354
const bindings = context.bindingMetadata
324355
if (!bindings || bindings.__isScriptSetup === false) {

packages/compiler-core/src/transforms/vSlot.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export const trackSlotScopes: NodeTransform = (node, context) => {
5757
if (vSlot) {
5858
const slotProps = vSlot.exp
5959
if (!__BROWSER__ && context.prefixIdentifiers) {
60-
slotProps && context.addIdentifiers(slotProps)
60+
slotProps && context.addIdentifiers(slotProps, 'slot')
6161
}
6262
context.scopes.vSlot++
6363
return () => {

packages/compiler-ssr/__tests__/ssrComponent.spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,54 @@ describe('ssr: components', () => {
104104
`)
105105
})
106106

107+
test('slot prop component', () => {
108+
const { code } = compile(`<foo v-slot="{ Foo }"><Foo /></foo>`)
109+
110+
expect(code).toContain(
111+
`_ssrRenderComponent(Foo, null, null, _parent, _scopeId)`,
112+
)
113+
expect(code).toContain(`_createVNode(Foo)`)
114+
expect(code).not.toContain(`_component_Foo`)
115+
expect(code).not.toContain(`_resolveComponent("Foo")`)
116+
})
117+
118+
test('namespaced slot prop component', () => {
119+
const { code } = compile(
120+
`<foo v-slot="slotProps"><slot-props.Foo /></foo>`,
121+
)
122+
123+
expect(code).toContain(
124+
`_ssrRenderComponent(slotProps.Foo, null, null, _parent, _scopeId)`,
125+
)
126+
expect(code).toContain(`_createVNode(slotProps.Foo)`)
127+
expect(code).not.toContain(`_component_slot_props`)
128+
expect(code).not.toContain(`_resolveComponent("slot-props.Foo")`)
129+
})
130+
131+
test('does not resolve v-for binding as slot prop component', () => {
132+
const { code } = compile(
133+
`<template v-for="Foo in list"><Foo :value="Foo" /></template>`,
134+
)
135+
136+
expect(code).toContain(`const _component_Foo = _resolveComponent("Foo")`)
137+
expect(code).toContain(`_ssrRenderComponent(_component_Foo`)
138+
expect(code).toContain(`value: Foo`)
139+
expect(code).not.toContain(`_ssrRenderComponent(Foo,`)
140+
})
141+
142+
test('does not resolve slot prop component when shadowed by v-for', () => {
143+
const { code } = compile(
144+
`<foo v-slot="{ Foo }"><template v-for="Foo in list"><Foo /></template></foo>`,
145+
)
146+
147+
expect(code).toContain(`const _component_Foo = _resolveComponent("Foo")`)
148+
expect(code).toContain(`_ssrRenderComponent(_component_Foo`)
149+
expect(code).toContain(`_createBlock(_component_Foo)`)
150+
expect(code).not.toContain(`_ssrRenderComponent(Foo, null`)
151+
expect(code).not.toContain(`_createVNode(Foo)`)
152+
expect(code).not.toContain(`_createBlock(Foo)`)
153+
})
154+
107155
test('empty attribute should not produce syntax error', () => {
108156
// previously this would produce syntax error `default: _withCtx((, _push, ...)`
109157
expect(compile(`<foo v-slot="">foo</foo>`).code).not.toMatch(`(,`)

packages/compiler-ssr/src/transforms/ssrTransformComponent.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,11 @@ function subTransform(
353353
// inherit parent scope analysis state
354354
childContext.scopes = { ...parentContext.scopes }
355355
childContext.identifiers = { ...parentContext.identifiers }
356+
childContext.identifierScopes = Object.create(null)
357+
for (const name in parentContext.identifierScopes) {
358+
childContext.identifierScopes[name] =
359+
parentContext.identifierScopes[name]!.slice()
360+
}
356361
childContext.imports = parentContext.imports
357362
// traverse
358363
traverseNode(childRoot, childContext)

0 commit comments

Comments
 (0)