fix(compiler-sfc): fix function default value handling w/ props destructure

This commit is contained in:
Evan You 2023-03-29 22:21:27 +08:00
parent 1a04fba10b
commit e10a89e608
6 changed files with 127 additions and 25 deletions

View File

@ -78,13 +78,34 @@ return (_ctx, _cache) => {
}" }"
`; `;
exports[`sfc props transform > default values w/ runtime declaration 1`] = ` exports[`sfc props transform > default values w/ array runtime declaration 1`] = `
"import { mergeDefaults as _mergeDefaults } from 'vue' "import { mergeDefaults as _mergeDefaults } from 'vue'
export default { export default {
props: _mergeDefaults(['foo', 'bar'], { props: _mergeDefaults(['foo', 'bar', 'baz'], {
foo: 1, foo: 1,
bar: () => ({}) bar: () => ({}),
func: () => {}, __skip_func: true
}),
setup(__props) {
return () => {}
}
}"
`;
exports[`sfc props transform > default values w/ object runtime declaration 1`] = `
"import { mergeDefaults as _mergeDefaults } from 'vue'
export default {
props: _mergeDefaults({ foo: Number, bar: Object, func: Function, ext: null }, {
foo: 1,
bar: () => ({}),
func: () => {}, __skip_func: true,
ext: x, __skip_ext: true
}), }),
setup(__props) { setup(__props) {
@ -102,7 +123,8 @@ exports[`sfc props transform > default values w/ type declaration 1`] = `
export default /*#__PURE__*/_defineComponent({ export default /*#__PURE__*/_defineComponent({
props: { props: {
foo: { type: Number, required: false, default: 1 }, foo: { type: Number, required: false, default: 1 },
bar: { type: Object, required: false, default: () => ({}) } bar: { type: Object, required: false, default: () => ({}) },
func: { type: Function, required: false, default: () => {} }
}, },
setup(__props: any) { setup(__props: any) {
@ -124,7 +146,7 @@ export default /*#__PURE__*/_defineComponent({
baz: null, baz: null,
boola: { type: Boolean }, boola: { type: Boolean },
boolb: { type: [Boolean, Number] }, boolb: { type: [Boolean, Number] },
func: { type: Function, default: () => (() => {}) } func: { type: Function, default: () => {} }
}, },
setup(__props: any) { setup(__props: any) {

View File

@ -69,17 +69,40 @@ describe('sfc props transform', () => {
}) })
}) })
test('default values w/ runtime declaration', () => { test('default values w/ array runtime declaration', () => {
const { content } = compile(` const { content } = compile(`
<script setup> <script setup>
const { foo = 1, bar = {} } = defineProps(['foo', 'bar']) const { foo = 1, bar = {}, func = () => {} } = defineProps(['foo', 'bar', 'baz'])
</script> </script>
`) `)
// literals can be used as-is, non-literals are always returned from a // literals can be used as-is, non-literals are always returned from a
// function // function
expect(content).toMatch(`props: _mergeDefaults(['foo', 'bar'], { // functions need to be marked with a skip marker
expect(content).toMatch(`props: _mergeDefaults(['foo', 'bar', 'baz'], {
foo: 1, foo: 1,
bar: () => ({}) bar: () => ({}),
func: () => {}, __skip_func: true
})`)
assertCode(content)
})
test('default values w/ object runtime declaration', () => {
const { content } = compile(`
<script setup>
const { foo = 1, bar = {}, func = () => {}, ext = x } = defineProps({ foo: Number, bar: Object, func: Function, ext: null })
</script>
`)
// literals can be used as-is, non-literals are always returned from a
// function
// functions need to be marked with a skip marker since we cannot always
// safely infer whether runtime type is Function (e.g. if the runtime decl
// is imported, or spreads another object)
expect(content)
.toMatch(`props: _mergeDefaults({ foo: Number, bar: Object, func: Function, ext: null }, {
foo: 1,
bar: () => ({}),
func: () => {}, __skip_func: true,
ext: x, __skip_ext: true
})`) })`)
assertCode(content) assertCode(content)
}) })
@ -87,14 +110,15 @@ describe('sfc props transform', () => {
test('default values w/ type declaration', () => { test('default values w/ type declaration', () => {
const { content } = compile(` const { content } = compile(`
<script setup lang="ts"> <script setup lang="ts">
const { foo = 1, bar = {} } = defineProps<{ foo?: number, bar?: object }>() const { foo = 1, bar = {}, func = () => {} } = defineProps<{ foo?: number, bar?: object, func?: () => any }>()
</script> </script>
`) `)
// literals can be used as-is, non-literals are always returned from a // literals can be used as-is, non-literals are always returned from a
// function // function
expect(content).toMatch(`props: { expect(content).toMatch(`props: {
foo: { type: Number, required: false, default: 1 }, foo: { type: Number, required: false, default: 1 },
bar: { type: Object, required: false, default: () => ({}) } bar: { type: Object, required: false, default: () => ({}) },
func: { type: Function, required: false, default: () => {} }
}`) }`)
assertCode(content) assertCode(content)
}) })
@ -116,7 +140,7 @@ describe('sfc props transform', () => {
baz: null, baz: null,
boola: { type: Boolean }, boola: { type: Boolean },
boolb: { type: [Boolean, Number] }, boolb: { type: [Boolean, Number] },
func: { type: Function, default: () => (() => {}) } func: { type: Function, default: () => {} }
}`) }`)
assertCode(content) assertCode(content)
}) })

View File

@ -862,9 +862,11 @@ export function compileScript(
${keys ${keys
.map(key => { .map(key => {
let defaultString: string | undefined let defaultString: string | undefined
const destructured = genDestructuredDefaultValue(key) const destructured = genDestructuredDefaultValue(key, props[key].type)
if (destructured) { if (destructured) {
defaultString = `default: ${destructured}` defaultString = `default: ${destructured.valueString}${
destructured.needSkipFactory ? `, skipFactory: true` : ``
}`
} else if (hasStaticDefaults) { } else if (hasStaticDefaults) {
const prop = propsRuntimeDefaults!.properties.find(node => { const prop = propsRuntimeDefaults!.properties.find(node => {
if (node.type === 'SpreadElement') return false if (node.type === 'SpreadElement') return false
@ -925,15 +927,38 @@ export function compileScript(
return `\n props: ${propsDecls},` return `\n props: ${propsDecls},`
} }
function genDestructuredDefaultValue(key: string): string | undefined { function genDestructuredDefaultValue(
key: string,
inferredType?: string[]
):
| {
valueString: string
needSkipFactory: boolean
}
| undefined {
const destructured = propsDestructuredBindings[key] const destructured = propsDestructuredBindings[key]
if (destructured && destructured.default) { const defaultVal = destructured && destructured.default
if (defaultVal) {
const value = scriptSetup!.content.slice( const value = scriptSetup!.content.slice(
destructured.default.start!, defaultVal.start!,
destructured.default.end! defaultVal.end!
) )
const isLiteral = isLiteralNode(destructured.default) const unwrapped = unwrapTSNode(defaultVal)
return isLiteral ? value : `() => (${value})` // If the default value is a function or is an identifier referencing
// external value, skip factory wrap. This is needed when using
// destructure w/ runtime declaration since we cannot safely infer
// whether tje expected runtime prop type is `Function`.
const needSkipFactory =
!inferredType &&
(isFunctionType(unwrapped) || unwrapped.type === 'Identifier')
const needFactoryWrap =
!needSkipFactory &&
!isLiteralNode(unwrapped) &&
!inferredType?.includes('Function')
return {
valueString: needFactoryWrap ? `() => (${value})` : value,
needSkipFactory
}
} }
} }
@ -1693,7 +1718,12 @@ export function compileScript(
const defaults: string[] = [] const defaults: string[] = []
for (const key in propsDestructuredBindings) { for (const key in propsDestructuredBindings) {
const d = genDestructuredDefaultValue(key) const d = genDestructuredDefaultValue(key)
if (d) defaults.push(`${key}: ${d}`) if (d)
defaults.push(
`${key}: ${d.valueString}${
d.needSkipFactory ? `, __skip_${key}: true` : ``
}`
)
} }
if (defaults.length) { if (defaults.length) {
declCode = `${helper( declCode = `${helper(

View File

@ -114,6 +114,17 @@ describe('SFC <script setup> helpers', () => {
}) })
}) })
test('merging with skipFactory', () => {
const fn = () => {}
const merged = mergeDefaults(['foo', 'bar', 'baz'], {
foo: fn,
__skip_foo: true
})
expect(merged).toMatchObject({
foo: { default: fn, skipFactory: true }
})
})
test('should warn missing', () => { test('should warn missing', () => {
mergeDefaults({}, { foo: 1 }) mergeDefaults({}, { foo: 1 })
expect( expect(

View File

@ -259,18 +259,22 @@ export function mergeDefaults(
) )
: raw : raw
for (const key in defaults) { for (const key in defaults) {
const opt = props[key] if (key.startsWith('__skip')) continue
let opt = props[key]
if (opt) { if (opt) {
if (isArray(opt) || isFunction(opt)) { if (isArray(opt) || isFunction(opt)) {
props[key] = { type: opt, default: defaults[key] } opt = props[key] = { type: opt, default: defaults[key] }
} else { } else {
opt.default = defaults[key] opt.default = defaults[key]
} }
} else if (opt === null) { } else if (opt === null) {
props[key] = { default: defaults[key] } opt = props[key] = { default: defaults[key] }
} else if (__DEV__) { } else if (__DEV__) {
warn(`props default key "${key}" has no corresponding declaration.`) warn(`props default key "${key}" has no corresponding declaration.`)
} }
if (opt && defaults[`__skip_${key}`]) {
opt.skipFactory = true
}
} }
return props return props
} }

View File

@ -58,7 +58,14 @@ export interface PropOptions<T = any, D = T> {
required?: boolean required?: boolean
default?: D | DefaultFactory<D> | null | undefined | object default?: D | DefaultFactory<D> | null | undefined | object
validator?(value: unknown): boolean validator?(value: unknown): boolean
/**
* @internal
*/
skipCheck?: boolean skipCheck?: boolean
/**
* @internal
*/
skipFactory?: boolean
} }
export type PropType<T> = PropConstructor<T> | PropConstructor<T>[] export type PropType<T> = PropConstructor<T> | PropConstructor<T>[]
@ -425,7 +432,11 @@ function resolvePropValue(
// default values // default values
if (hasDefault && value === undefined) { if (hasDefault && value === undefined) {
const defaultValue = opt.default const defaultValue = opt.default
if (opt.type !== Function && isFunction(defaultValue)) { if (
opt.type !== Function &&
!opt.skipFactory &&
isFunction(defaultValue)
) {
const { propsDefaults } = instance const { propsDefaults } = instance
if (key in propsDefaults) { if (key in propsDefaults) {
value = propsDefaults[key] value = propsDefaults[key]