fix(compiler-sfc): fix function default value handling w/ props destructure
This commit is contained in:
parent
1a04fba10b
commit
e10a89e608
|
@ -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) {
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]
|
||||||
|
|
Loading…
Reference in New Issue