fix(compiler-sfc): generate matching prop types when withDefaults is used (#4466)
fix #4455
This commit is contained in:
parent
305883a12f
commit
85807967dc
@ -1037,11 +1037,13 @@ import { defaults } from './foo'
|
|||||||
export default /*#__PURE__*/_defineComponent({
|
export default /*#__PURE__*/_defineComponent({
|
||||||
props: _mergeDefaults({
|
props: _mergeDefaults({
|
||||||
foo: { type: String, required: false },
|
foo: { type: String, required: false },
|
||||||
bar: { type: Number, required: false }
|
bar: { type: Number, required: false },
|
||||||
|
baz: { type: Boolean, required: true }
|
||||||
}, { ...defaults }) as unknown as undefined,
|
}, { ...defaults }) as unknown as undefined,
|
||||||
setup(__props: {
|
setup(__props: {
|
||||||
foo?: string
|
foo?: string
|
||||||
bar?: number
|
bar?: number
|
||||||
|
baz: boolean
|
||||||
}, { expose }) {
|
}, { expose }) {
|
||||||
expose()
|
expose()
|
||||||
|
|
||||||
@ -1060,12 +1062,11 @@ exports[`SFC compile <script setup> with TypeScript withDefaults (static) 1`] =
|
|||||||
export default /*#__PURE__*/_defineComponent({
|
export default /*#__PURE__*/_defineComponent({
|
||||||
props: {
|
props: {
|
||||||
foo: { type: String, required: false, default: 'hi' },
|
foo: { type: String, required: false, default: 'hi' },
|
||||||
bar: { type: Number, required: false }
|
bar: { type: Number, required: false },
|
||||||
|
baz: { type: Boolean, required: true },
|
||||||
|
qux: { type: Function, required: false, default() { return 1 } }
|
||||||
} as unknown as undefined,
|
} as unknown as undefined,
|
||||||
setup(__props: {
|
setup(__props: { foo: string, bar?: number, baz: boolean, qux(): number }, { expose }) {
|
||||||
foo?: string
|
|
||||||
bar?: number
|
|
||||||
}, { expose }) {
|
|
||||||
expose()
|
expose()
|
||||||
|
|
||||||
const props = __props
|
const props = __props
|
||||||
|
@ -800,8 +800,11 @@ const emit = defineEmits(['a', 'b'])
|
|||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
foo?: string
|
foo?: string
|
||||||
bar?: number
|
bar?: number
|
||||||
|
baz: boolean
|
||||||
|
qux?(): number
|
||||||
}>(), {
|
}>(), {
|
||||||
foo: 'hi'
|
foo: 'hi',
|
||||||
|
qux() { return 1 }
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
`)
|
`)
|
||||||
@ -810,10 +813,19 @@ const emit = defineEmits(['a', 'b'])
|
|||||||
`foo: { type: String, required: false, default: 'hi' }`
|
`foo: { type: String, required: false, default: 'hi' }`
|
||||||
)
|
)
|
||||||
expect(content).toMatch(`bar: { type: Number, required: false }`)
|
expect(content).toMatch(`bar: { type: Number, required: false }`)
|
||||||
|
expect(content).toMatch(`baz: { type: Boolean, required: true }`)
|
||||||
|
expect(content).toMatch(
|
||||||
|
`qux: { type: Function, required: false, default() { return 1 } }`
|
||||||
|
)
|
||||||
|
expect(content).toMatch(
|
||||||
|
`{ foo: string, bar?: number, baz: boolean, qux(): number }`
|
||||||
|
)
|
||||||
expect(content).toMatch(`const props = __props`)
|
expect(content).toMatch(`const props = __props`)
|
||||||
expect(bindings).toStrictEqual({
|
expect(bindings).toStrictEqual({
|
||||||
foo: BindingTypes.PROPS,
|
foo: BindingTypes.PROPS,
|
||||||
bar: BindingTypes.PROPS,
|
bar: BindingTypes.PROPS,
|
||||||
|
baz: BindingTypes.PROPS,
|
||||||
|
qux: BindingTypes.PROPS,
|
||||||
props: BindingTypes.SETUP_CONST
|
props: BindingTypes.SETUP_CONST
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -825,6 +837,7 @@ const emit = defineEmits(['a', 'b'])
|
|||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
foo?: string
|
foo?: string
|
||||||
bar?: number
|
bar?: number
|
||||||
|
baz: boolean
|
||||||
}>(), { ...defaults })
|
}>(), { ...defaults })
|
||||||
</script>
|
</script>
|
||||||
`)
|
`)
|
||||||
@ -834,7 +847,8 @@ const emit = defineEmits(['a', 'b'])
|
|||||||
`
|
`
|
||||||
_mergeDefaults({
|
_mergeDefaults({
|
||||||
foo: { type: String, required: false },
|
foo: { type: String, required: false },
|
||||||
bar: { type: Number, required: false }
|
bar: { type: Number, required: false },
|
||||||
|
baz: { type: Boolean, required: true }
|
||||||
}, { ...defaults })`.trim()
|
}, { ...defaults })`.trim()
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -38,7 +38,8 @@ import {
|
|||||||
RestElement,
|
RestElement,
|
||||||
TSInterfaceBody,
|
TSInterfaceBody,
|
||||||
AwaitExpression,
|
AwaitExpression,
|
||||||
Program
|
Program,
|
||||||
|
ObjectMethod
|
||||||
} from '@babel/types'
|
} from '@babel/types'
|
||||||
import { walk } from 'estree-walker'
|
import { walk } from 'estree-walker'
|
||||||
import { RawSourceMap } from 'source-map'
|
import { RawSourceMap } from 'source-map'
|
||||||
@ -242,7 +243,7 @@ export function compileScript(
|
|||||||
let hasDefineEmitCall = false
|
let hasDefineEmitCall = false
|
||||||
let hasDefineExposeCall = false
|
let hasDefineExposeCall = false
|
||||||
let propsRuntimeDecl: Node | undefined
|
let propsRuntimeDecl: Node | undefined
|
||||||
let propsRuntimeDefaults: Node | undefined
|
let propsRuntimeDefaults: ObjectExpression | undefined
|
||||||
let propsTypeDecl: TSTypeLiteral | TSInterfaceBody | undefined
|
let propsTypeDecl: TSTypeLiteral | TSInterfaceBody | undefined
|
||||||
let propsTypeDeclRaw: Node | undefined
|
let propsTypeDeclRaw: Node | undefined
|
||||||
let propsIdentifier: string | undefined
|
let propsIdentifier: string | undefined
|
||||||
@ -384,7 +385,16 @@ export function compileScript(
|
|||||||
node
|
node
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
propsRuntimeDefaults = node.arguments[1]
|
propsRuntimeDefaults = node.arguments[1] as ObjectExpression
|
||||||
|
if (
|
||||||
|
!propsRuntimeDefaults ||
|
||||||
|
propsRuntimeDefaults.type !== 'ObjectExpression'
|
||||||
|
) {
|
||||||
|
error(
|
||||||
|
`The 2nd argument of ${WITH_DEFAULTS} must be an object literal.`,
|
||||||
|
propsRuntimeDefaults || node
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
error(
|
error(
|
||||||
`${WITH_DEFAULTS}' first argument must be a ${DEFINE_PROPS} call.`,
|
`${WITH_DEFAULTS}' first argument must be a ${DEFINE_PROPS} call.`,
|
||||||
@ -513,38 +523,51 @@ export function compileScript(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check defaults. If the default object is an object literal with only
|
||||||
|
* static properties, we can directly generate more optimzied default
|
||||||
|
* decalrations. Otherwise we will have to fallback to runtime merging.
|
||||||
|
*/
|
||||||
|
function checkStaticDefaults() {
|
||||||
|
return (
|
||||||
|
propsRuntimeDefaults &&
|
||||||
|
propsRuntimeDefaults.type === 'ObjectExpression' &&
|
||||||
|
propsRuntimeDefaults.properties.every(
|
||||||
|
node =>
|
||||||
|
(node.type === 'ObjectProperty' && !node.computed) ||
|
||||||
|
node.type === 'ObjectMethod'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function genRuntimeProps(props: Record<string, PropTypeData>) {
|
function genRuntimeProps(props: Record<string, PropTypeData>) {
|
||||||
const keys = Object.keys(props)
|
const keys = Object.keys(props)
|
||||||
if (!keys.length) {
|
if (!keys.length) {
|
||||||
return ``
|
return ``
|
||||||
}
|
}
|
||||||
|
const hasStaticDefaults = checkStaticDefaults()
|
||||||
// check defaults. If the default object is an object literal with only
|
const scriptSetupSource = scriptSetup!.content
|
||||||
// static properties, we can directly generate more optimzied default
|
|
||||||
// decalrations. Otherwise we will have to fallback to runtime merging.
|
|
||||||
const hasStaticDefaults =
|
|
||||||
propsRuntimeDefaults &&
|
|
||||||
propsRuntimeDefaults.type === 'ObjectExpression' &&
|
|
||||||
propsRuntimeDefaults.properties.every(
|
|
||||||
node => node.type === 'ObjectProperty' && !node.computed
|
|
||||||
)
|
|
||||||
|
|
||||||
let propsDecls = `{
|
let propsDecls = `{
|
||||||
${keys
|
${keys
|
||||||
.map(key => {
|
.map(key => {
|
||||||
let defaultString: string | undefined
|
let defaultString: string | undefined
|
||||||
if (hasStaticDefaults) {
|
if (hasStaticDefaults) {
|
||||||
const prop = (
|
const prop = propsRuntimeDefaults!.properties.find(
|
||||||
propsRuntimeDefaults as ObjectExpression
|
|
||||||
).properties.find(
|
|
||||||
(node: any) => node.key.name === key
|
(node: any) => node.key.name === key
|
||||||
) as ObjectProperty
|
) as ObjectProperty | ObjectMethod
|
||||||
if (prop) {
|
if (prop) {
|
||||||
// prop has corresponding static default value
|
if (prop.type === 'ObjectProperty') {
|
||||||
defaultString = `default: ${source.slice(
|
// prop has corresponding static default value
|
||||||
prop.value.start! + startOffset,
|
defaultString = `default: ${scriptSetupSource.slice(
|
||||||
prop.value.end! + startOffset
|
prop.value.start!,
|
||||||
)}`
|
prop.value.end!
|
||||||
|
)}`
|
||||||
|
} else {
|
||||||
|
defaultString = `default() ${scriptSetupSource.slice(
|
||||||
|
prop.body.start!,
|
||||||
|
prop.body.end!
|
||||||
|
)}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -572,6 +595,44 @@ export function compileScript(
|
|||||||
return `\n props: ${propsDecls} as unknown as undefined,`
|
return `\n props: ${propsDecls} as unknown as undefined,`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function genSetupPropsType(node: TSTypeLiteral | TSInterfaceBody) {
|
||||||
|
const scriptSetupSource = scriptSetup!.content
|
||||||
|
if (checkStaticDefaults()) {
|
||||||
|
// if withDefaults() is used, we need to remove the optional flags
|
||||||
|
// on props that have default values
|
||||||
|
let res = `: { `
|
||||||
|
const members = node.type === 'TSTypeLiteral' ? node.members : node.body
|
||||||
|
for (const m of members) {
|
||||||
|
if (
|
||||||
|
(m.type === 'TSPropertySignature' ||
|
||||||
|
m.type === 'TSMethodSignature') &&
|
||||||
|
m.typeAnnotation &&
|
||||||
|
m.key.type === 'Identifier'
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
propsRuntimeDefaults!.properties.some(
|
||||||
|
(p: any) => p.key.name === (m.key as Identifier).name
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
res +=
|
||||||
|
m.key.name +
|
||||||
|
(m.type === 'TSMethodSignature' ? '()' : '') +
|
||||||
|
scriptSetupSource.slice(
|
||||||
|
m.typeAnnotation.start!,
|
||||||
|
m.typeAnnotation.end!
|
||||||
|
) +
|
||||||
|
', '
|
||||||
|
} else {
|
||||||
|
res += scriptSetupSource.slice(m.start!, m.end!) + `, `
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (res.length ? res.slice(0, -2) : res) + ` }`
|
||||||
|
} else {
|
||||||
|
return `: ${scriptSetupSource.slice(node.start!, node.end!)}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 1. process normal <script> first if it exists
|
// 1. process normal <script> first if it exists
|
||||||
let scriptAst
|
let scriptAst
|
||||||
if (script) {
|
if (script) {
|
||||||
@ -990,10 +1051,7 @@ export function compileScript(
|
|||||||
// 9. finalize setup() argument signature
|
// 9. finalize setup() argument signature
|
||||||
let args = `__props`
|
let args = `__props`
|
||||||
if (propsTypeDecl) {
|
if (propsTypeDecl) {
|
||||||
args += `: ${scriptSetup.content.slice(
|
args += genSetupPropsType(propsTypeDecl)
|
||||||
propsTypeDecl.start!,
|
|
||||||
propsTypeDecl.end!
|
|
||||||
)}`
|
|
||||||
}
|
}
|
||||||
// inject user assignment of props
|
// inject user assignment of props
|
||||||
// we use a default __props so that template expressions referencing props
|
// we use a default __props so that template expressions referencing props
|
||||||
|
Loading…
Reference in New Issue
Block a user