Skip to content

Commit 8580796

Browse files
authored
fix(compiler-sfc): generate matching prop types when withDefaults is used (#4466)
fix #4455
1 parent 305883a commit 8580796

File tree

3 files changed

+108
-35
lines changed

3 files changed

+108
-35
lines changed

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

+7-6
Original file line numberDiff line numberDiff line change
@@ -1037,11 +1037,13 @@ import { defaults } from './foo'
10371037
export default /*#__PURE__*/_defineComponent({
10381038
props: _mergeDefaults({
10391039
foo: { type: String, required: false },
1040-
bar: { type: Number, required: false }
1040+
bar: { type: Number, required: false },
1041+
baz: { type: Boolean, required: true }
10411042
}, { ...defaults }) as unknown as undefined,
10421043
setup(__props: {
10431044
foo?: string
10441045
bar?: number
1046+
baz: boolean
10451047
}, { expose }) {
10461048
expose()
10471049
@@ -1060,12 +1062,11 @@ exports[`SFC compile <script setup> with TypeScript withDefaults (static) 1`] =
10601062
export default /*#__PURE__*/_defineComponent({
10611063
props: {
10621064
foo: { type: String, required: false, default: 'hi' },
1063-
bar: { type: Number, required: false }
1065+
bar: { type: Number, required: false },
1066+
baz: { type: Boolean, required: true },
1067+
qux: { type: Function, required: false, default() { return 1 } }
10641068
} as unknown as undefined,
1065-
setup(__props: {
1066-
foo?: string
1067-
bar?: number
1068-
}, { expose }) {
1069+
setup(__props: { foo: string, bar?: number, baz: boolean, qux(): number }, { expose }) {
10691070
expose()
10701071
10711072
const props = __props

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

+16-2
Original file line numberDiff line numberDiff line change
@@ -800,8 +800,11 @@ const emit = defineEmits(['a', 'b'])
800800
const props = withDefaults(defineProps<{
801801
foo?: string
802802
bar?: number
803+
baz: boolean
804+
qux?(): number
803805
}>(), {
804-
foo: 'hi'
806+
foo: 'hi',
807+
qux() { return 1 }
805808
})
806809
</script>
807810
`)
@@ -810,10 +813,19 @@ const emit = defineEmits(['a', 'b'])
810813
`foo: { type: String, required: false, default: 'hi' }`
811814
)
812815
expect(content).toMatch(`bar: { type: Number, required: false }`)
816+
expect(content).toMatch(`baz: { type: Boolean, required: true }`)
817+
expect(content).toMatch(
818+
`qux: { type: Function, required: false, default() { return 1 } }`
819+
)
820+
expect(content).toMatch(
821+
`{ foo: string, bar?: number, baz: boolean, qux(): number }`
822+
)
813823
expect(content).toMatch(`const props = __props`)
814824
expect(bindings).toStrictEqual({
815825
foo: BindingTypes.PROPS,
816826
bar: BindingTypes.PROPS,
827+
baz: BindingTypes.PROPS,
828+
qux: BindingTypes.PROPS,
817829
props: BindingTypes.SETUP_CONST
818830
})
819831
})
@@ -825,6 +837,7 @@ const emit = defineEmits(['a', 'b'])
825837
const props = withDefaults(defineProps<{
826838
foo?: string
827839
bar?: number
840+
baz: boolean
828841
}>(), { ...defaults })
829842
</script>
830843
`)
@@ -834,7 +847,8 @@ const emit = defineEmits(['a', 'b'])
834847
`
835848
_mergeDefaults({
836849
foo: { type: String, required: false },
837-
bar: { type: Number, required: false }
850+
bar: { type: Number, required: false },
851+
baz: { type: Boolean, required: true }
838852
}, { ...defaults })`.trim()
839853
)
840854
})

packages/compiler-sfc/src/compileScript.ts

+85-27
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ import {
3838
RestElement,
3939
TSInterfaceBody,
4040
AwaitExpression,
41-
Program
41+
Program,
42+
ObjectMethod
4243
} from '@babel/types'
4344
import { walk } from 'estree-walker'
4445
import { RawSourceMap } from 'source-map'
@@ -242,7 +243,7 @@ export function compileScript(
242243
let hasDefineEmitCall = false
243244
let hasDefineExposeCall = false
244245
let propsRuntimeDecl: Node | undefined
245-
let propsRuntimeDefaults: Node | undefined
246+
let propsRuntimeDefaults: ObjectExpression | undefined
246247
let propsTypeDecl: TSTypeLiteral | TSInterfaceBody | undefined
247248
let propsTypeDeclRaw: Node | undefined
248249
let propsIdentifier: string | undefined
@@ -384,7 +385,16 @@ export function compileScript(
384385
node
385386
)
386387
}
387-
propsRuntimeDefaults = node.arguments[1]
388+
propsRuntimeDefaults = node.arguments[1] as ObjectExpression
389+
if (
390+
!propsRuntimeDefaults ||
391+
propsRuntimeDefaults.type !== 'ObjectExpression'
392+
) {
393+
error(
394+
`The 2nd argument of ${WITH_DEFAULTS} must be an object literal.`,
395+
propsRuntimeDefaults || node
396+
)
397+
}
388398
} else {
389399
error(
390400
`${WITH_DEFAULTS}' first argument must be a ${DEFINE_PROPS} call.`,
@@ -513,38 +523,51 @@ export function compileScript(
513523
)
514524
}
515525

516-
function genRuntimeProps(props: Record<string, PropTypeData>) {
517-
const keys = Object.keys(props)
518-
if (!keys.length) {
519-
return ``
520-
}
521-
522-
// check defaults. If the default object is an object literal with only
523-
// static properties, we can directly generate more optimzied default
524-
// decalrations. Otherwise we will have to fallback to runtime merging.
525-
const hasStaticDefaults =
526+
/**
527+
* check defaults. If the default object is an object literal with only
528+
* static properties, we can directly generate more optimzied default
529+
* decalrations. Otherwise we will have to fallback to runtime merging.
530+
*/
531+
function checkStaticDefaults() {
532+
return (
526533
propsRuntimeDefaults &&
527534
propsRuntimeDefaults.type === 'ObjectExpression' &&
528535
propsRuntimeDefaults.properties.every(
529-
node => node.type === 'ObjectProperty' && !node.computed
536+
node =>
537+
(node.type === 'ObjectProperty' && !node.computed) ||
538+
node.type === 'ObjectMethod'
530539
)
540+
)
541+
}
531542

543+
function genRuntimeProps(props: Record<string, PropTypeData>) {
544+
const keys = Object.keys(props)
545+
if (!keys.length) {
546+
return ``
547+
}
548+
const hasStaticDefaults = checkStaticDefaults()
549+
const scriptSetupSource = scriptSetup!.content
532550
let propsDecls = `{
533551
${keys
534552
.map(key => {
535553
let defaultString: string | undefined
536554
if (hasStaticDefaults) {
537-
const prop = (
538-
propsRuntimeDefaults as ObjectExpression
539-
).properties.find(
555+
const prop = propsRuntimeDefaults!.properties.find(
540556
(node: any) => node.key.name === key
541-
) as ObjectProperty
557+
) as ObjectProperty | ObjectMethod
542558
if (prop) {
543-
// prop has corresponding static default value
544-
defaultString = `default: ${source.slice(
545-
prop.value.start! + startOffset,
546-
prop.value.end! + startOffset
547-
)}`
559+
if (prop.type === 'ObjectProperty') {
560+
// prop has corresponding static default value
561+
defaultString = `default: ${scriptSetupSource.slice(
562+
prop.value.start!,
563+
prop.value.end!
564+
)}`
565+
} else {
566+
defaultString = `default() ${scriptSetupSource.slice(
567+
prop.body.start!,
568+
prop.body.end!
569+
)}`
570+
}
548571
}
549572
}
550573
@@ -572,6 +595,44 @@ export function compileScript(
572595
return `\n props: ${propsDecls} as unknown as undefined,`
573596
}
574597

598+
function genSetupPropsType(node: TSTypeLiteral | TSInterfaceBody) {
599+
const scriptSetupSource = scriptSetup!.content
600+
if (checkStaticDefaults()) {
601+
// if withDefaults() is used, we need to remove the optional flags
602+
// on props that have default values
603+
let res = `: { `
604+
const members = node.type === 'TSTypeLiteral' ? node.members : node.body
605+
for (const m of members) {
606+
if (
607+
(m.type === 'TSPropertySignature' ||
608+
m.type === 'TSMethodSignature') &&
609+
m.typeAnnotation &&
610+
m.key.type === 'Identifier'
611+
) {
612+
if (
613+
propsRuntimeDefaults!.properties.some(
614+
(p: any) => p.key.name === (m.key as Identifier).name
615+
)
616+
) {
617+
res +=
618+
m.key.name +
619+
(m.type === 'TSMethodSignature' ? '()' : '') +
620+
scriptSetupSource.slice(
621+
m.typeAnnotation.start!,
622+
m.typeAnnotation.end!
623+
) +
624+
', '
625+
} else {
626+
res += scriptSetupSource.slice(m.start!, m.end!) + `, `
627+
}
628+
}
629+
}
630+
return (res.length ? res.slice(0, -2) : res) + ` }`
631+
} else {
632+
return `: ${scriptSetupSource.slice(node.start!, node.end!)}`
633+
}
634+
}
635+
575636
// 1. process normal <script> first if it exists
576637
let scriptAst
577638
if (script) {
@@ -990,10 +1051,7 @@ export function compileScript(
9901051
// 9. finalize setup() argument signature
9911052
let args = `__props`
9921053
if (propsTypeDecl) {
993-
args += `: ${scriptSetup.content.slice(
994-
propsTypeDecl.start!,
995-
propsTypeDecl.end!
996-
)}`
1054+
args += genSetupPropsType(propsTypeDecl)
9971055
}
9981056
// inject user assignment of props
9991057
// we use a default __props so that template expressions referencing props

0 commit comments

Comments
 (0)