Skip to content

Commit e10a89e

Browse files
committed
fix(compiler-sfc): fix function default value handling w/ props destructure
1 parent 1a04fba commit e10a89e

File tree

6 files changed

+127
-25
lines changed

6 files changed

+127
-25
lines changed

packages/compiler-sfc/__tests__/__snapshots__/compileScriptPropsTransform.spec.ts.snap

+27-5
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,34 @@ return (_ctx, _cache) => {
7878
}"
7979
`;
8080

81-
exports[`sfc props transform > default values w/ runtime declaration 1`] = `
81+
exports[`sfc props transform > default values w/ array runtime declaration 1`] = `
8282
"import { mergeDefaults as _mergeDefaults } from 'vue'
8383
8484
export default {
85-
props: _mergeDefaults(['foo', 'bar'], {
85+
props: _mergeDefaults(['foo', 'bar', 'baz'], {
8686
foo: 1,
87-
bar: () => ({})
87+
bar: () => ({}),
88+
func: () => {}, __skip_func: true
89+
}),
90+
setup(__props) {
91+
92+
93+
94+
return () => {}
95+
}
96+
97+
}"
98+
`;
99+
100+
exports[`sfc props transform > default values w/ object runtime declaration 1`] = `
101+
"import { mergeDefaults as _mergeDefaults } from 'vue'
102+
103+
export default {
104+
props: _mergeDefaults({ foo: Number, bar: Object, func: Function, ext: null }, {
105+
foo: 1,
106+
bar: () => ({}),
107+
func: () => {}, __skip_func: true,
108+
ext: x, __skip_ext: true
88109
}),
89110
setup(__props) {
90111
@@ -102,7 +123,8 @@ exports[`sfc props transform > default values w/ type declaration 1`] = `
102123
export default /*#__PURE__*/_defineComponent({
103124
props: {
104125
foo: { type: Number, required: false, default: 1 },
105-
bar: { type: Object, required: false, default: () => ({}) }
126+
bar: { type: Object, required: false, default: () => ({}) },
127+
func: { type: Function, required: false, default: () => {} }
106128
},
107129
setup(__props: any) {
108130
@@ -124,7 +146,7 @@ export default /*#__PURE__*/_defineComponent({
124146
baz: null,
125147
boola: { type: Boolean },
126148
boolb: { type: [Boolean, Number] },
127-
func: { type: Function, default: () => (() => {}) }
149+
func: { type: Function, default: () => {} }
128150
},
129151
setup(__props: any) {
130152

packages/compiler-sfc/__tests__/compileScriptPropsTransform.spec.ts

+31-7
Original file line numberDiff line numberDiff line change
@@ -69,32 +69,56 @@ describe('sfc props transform', () => {
6969
})
7070
})
7171

72-
test('default values w/ runtime declaration', () => {
72+
test('default values w/ array runtime declaration', () => {
7373
const { content } = compile(`
7474
<script setup>
75-
const { foo = 1, bar = {} } = defineProps(['foo', 'bar'])
75+
const { foo = 1, bar = {}, func = () => {} } = defineProps(['foo', 'bar', 'baz'])
7676
</script>
7777
`)
7878
// literals can be used as-is, non-literals are always returned from a
7979
// function
80-
expect(content).toMatch(`props: _mergeDefaults(['foo', 'bar'], {
80+
// functions need to be marked with a skip marker
81+
expect(content).toMatch(`props: _mergeDefaults(['foo', 'bar', 'baz'], {
8182
foo: 1,
82-
bar: () => ({})
83+
bar: () => ({}),
84+
func: () => {}, __skip_func: true
85+
})`)
86+
assertCode(content)
87+
})
88+
89+
test('default values w/ object runtime declaration', () => {
90+
const { content } = compile(`
91+
<script setup>
92+
const { foo = 1, bar = {}, func = () => {}, ext = x } = defineProps({ foo: Number, bar: Object, func: Function, ext: null })
93+
</script>
94+
`)
95+
// literals can be used as-is, non-literals are always returned from a
96+
// function
97+
// functions need to be marked with a skip marker since we cannot always
98+
// safely infer whether runtime type is Function (e.g. if the runtime decl
99+
// is imported, or spreads another object)
100+
expect(content)
101+
.toMatch(`props: _mergeDefaults({ foo: Number, bar: Object, func: Function, ext: null }, {
102+
foo: 1,
103+
bar: () => ({}),
104+
func: () => {}, __skip_func: true,
105+
ext: x, __skip_ext: true
83106
})`)
84107
assertCode(content)
85108
})
86109

87110
test('default values w/ type declaration', () => {
88111
const { content } = compile(`
89112
<script setup lang="ts">
90-
const { foo = 1, bar = {} } = defineProps<{ foo?: number, bar?: object }>()
113+
const { foo = 1, bar = {}, func = () => {} } = defineProps<{ foo?: number, bar?: object, func?: () => any }>()
91114
</script>
92115
`)
93116
// literals can be used as-is, non-literals are always returned from a
94117
// function
95118
expect(content).toMatch(`props: {
96119
foo: { type: Number, required: false, default: 1 },
97-
bar: { type: Object, required: false, default: () => ({}) }
120+
bar: { type: Object, required: false, default: () => ({}) },
121+
func: { type: Function, required: false, default: () => {} }
98122
}`)
99123
assertCode(content)
100124
})
@@ -116,7 +140,7 @@ describe('sfc props transform', () => {
116140
baz: null,
117141
boola: { type: Boolean },
118142
boolb: { type: [Boolean, Number] },
119-
func: { type: Function, default: () => (() => {}) }
143+
func: { type: Function, default: () => {} }
120144
}`)
121145
assertCode(content)
122146
})

packages/compiler-sfc/src/compileScript.ts

+39-9
Original file line numberDiff line numberDiff line change
@@ -862,9 +862,11 @@ export function compileScript(
862862
${keys
863863
.map(key => {
864864
let defaultString: string | undefined
865-
const destructured = genDestructuredDefaultValue(key)
865+
const destructured = genDestructuredDefaultValue(key, props[key].type)
866866
if (destructured) {
867-
defaultString = `default: ${destructured}`
867+
defaultString = `default: ${destructured.valueString}${
868+
destructured.needSkipFactory ? `, skipFactory: true` : ``
869+
}`
868870
} else if (hasStaticDefaults) {
869871
const prop = propsRuntimeDefaults!.properties.find(node => {
870872
if (node.type === 'SpreadElement') return false
@@ -925,15 +927,38 @@ export function compileScript(
925927
return `\n props: ${propsDecls},`
926928
}
927929

928-
function genDestructuredDefaultValue(key: string): string | undefined {
930+
function genDestructuredDefaultValue(
931+
key: string,
932+
inferredType?: string[]
933+
):
934+
| {
935+
valueString: string
936+
needSkipFactory: boolean
937+
}
938+
| undefined {
929939
const destructured = propsDestructuredBindings[key]
930-
if (destructured && destructured.default) {
940+
const defaultVal = destructured && destructured.default
941+
if (defaultVal) {
931942
const value = scriptSetup!.content.slice(
932-
destructured.default.start!,
933-
destructured.default.end!
943+
defaultVal.start!,
944+
defaultVal.end!
934945
)
935-
const isLiteral = isLiteralNode(destructured.default)
936-
return isLiteral ? value : `() => (${value})`
946+
const unwrapped = unwrapTSNode(defaultVal)
947+
// If the default value is a function or is an identifier referencing
948+
// external value, skip factory wrap. This is needed when using
949+
// destructure w/ runtime declaration since we cannot safely infer
950+
// whether tje expected runtime prop type is `Function`.
951+
const needSkipFactory =
952+
!inferredType &&
953+
(isFunctionType(unwrapped) || unwrapped.type === 'Identifier')
954+
const needFactoryWrap =
955+
!needSkipFactory &&
956+
!isLiteralNode(unwrapped) &&
957+
!inferredType?.includes('Function')
958+
return {
959+
valueString: needFactoryWrap ? `() => (${value})` : value,
960+
needSkipFactory
961+
}
937962
}
938963
}
939964

@@ -1693,7 +1718,12 @@ export function compileScript(
16931718
const defaults: string[] = []
16941719
for (const key in propsDestructuredBindings) {
16951720
const d = genDestructuredDefaultValue(key)
1696-
if (d) defaults.push(`${key}: ${d}`)
1721+
if (d)
1722+
defaults.push(
1723+
`${key}: ${d.valueString}${
1724+
d.needSkipFactory ? `, __skip_${key}: true` : ``
1725+
}`
1726+
)
16971727
}
16981728
if (defaults.length) {
16991729
declCode = `${helper(

packages/runtime-core/__tests__/apiSetupHelpers.spec.ts

+11
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,17 @@ describe('SFC <script setup> helpers', () => {
114114
})
115115
})
116116

117+
test('merging with skipFactory', () => {
118+
const fn = () => {}
119+
const merged = mergeDefaults(['foo', 'bar', 'baz'], {
120+
foo: fn,
121+
__skip_foo: true
122+
})
123+
expect(merged).toMatchObject({
124+
foo: { default: fn, skipFactory: true }
125+
})
126+
})
127+
117128
test('should warn missing', () => {
118129
mergeDefaults({}, { foo: 1 })
119130
expect(

packages/runtime-core/src/apiSetupHelpers.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -259,18 +259,22 @@ export function mergeDefaults(
259259
)
260260
: raw
261261
for (const key in defaults) {
262-
const opt = props[key]
262+
if (key.startsWith('__skip')) continue
263+
let opt = props[key]
263264
if (opt) {
264265
if (isArray(opt) || isFunction(opt)) {
265-
props[key] = { type: opt, default: defaults[key] }
266+
opt = props[key] = { type: opt, default: defaults[key] }
266267
} else {
267268
opt.default = defaults[key]
268269
}
269270
} else if (opt === null) {
270-
props[key] = { default: defaults[key] }
271+
opt = props[key] = { default: defaults[key] }
271272
} else if (__DEV__) {
272273
warn(`props default key "${key}" has no corresponding declaration.`)
273274
}
275+
if (opt && defaults[`__skip_${key}`]) {
276+
opt.skipFactory = true
277+
}
274278
}
275279
return props
276280
}

packages/runtime-core/src/componentProps.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,14 @@ export interface PropOptions<T = any, D = T> {
5858
required?: boolean
5959
default?: D | DefaultFactory<D> | null | undefined | object
6060
validator?(value: unknown): boolean
61+
/**
62+
* @internal
63+
*/
6164
skipCheck?: boolean
65+
/**
66+
* @internal
67+
*/
68+
skipFactory?: boolean
6269
}
6370

6471
export type PropType<T> = PropConstructor<T> | PropConstructor<T>[]
@@ -425,7 +432,11 @@ function resolvePropValue(
425432
// default values
426433
if (hasDefault && value === undefined) {
427434
const defaultValue = opt.default
428-
if (opt.type !== Function && isFunction(defaultValue)) {
435+
if (
436+
opt.type !== Function &&
437+
!opt.skipFactory &&
438+
isFunction(defaultValue)
439+
) {
429440
const { propsDefaults } = instance
430441
if (key in propsDefaults) {
431442
value = propsDefaults[key]

0 commit comments

Comments
 (0)