Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(compiler-sfc): setup props type should not be optional when setup default value #4466

Merged
merged 4 commits into from Sep 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -1037,11 +1037,13 @@ import { defaults } from './foo'
export default /*#__PURE__*/_defineComponent({
props: _mergeDefaults({
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,
setup(__props: {
foo?: string
bar?: number
baz: boolean
}, { expose }) {
expose()

Expand All @@ -1060,12 +1062,11 @@ exports[`SFC compile <script setup> with TypeScript withDefaults (static) 1`] =
export default /*#__PURE__*/_defineComponent({
props: {
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,
setup(__props: {
foo?: string
bar?: number
}, { expose }) {
setup(__props: { foo: string, bar?: number, baz: boolean, qux(): number }, { expose }) {
expose()

const props = __props
Expand Down
18 changes: 16 additions & 2 deletions packages/compiler-sfc/__tests__/compileScript.spec.ts
Expand Up @@ -800,8 +800,11 @@ const emit = defineEmits(['a', 'b'])
const props = withDefaults(defineProps<{
foo?: string
bar?: number
baz: boolean
qux?(): number
}>(), {
foo: 'hi'
foo: 'hi',
qux() { return 1 }
})
</script>
`)
Expand All @@ -810,10 +813,19 @@ const emit = defineEmits(['a', 'b'])
`foo: { type: String, required: false, default: 'hi' }`
)
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(bindings).toStrictEqual({
foo: BindingTypes.PROPS,
bar: BindingTypes.PROPS,
baz: BindingTypes.PROPS,
qux: BindingTypes.PROPS,
props: BindingTypes.SETUP_CONST
})
})
Expand All @@ -825,6 +837,7 @@ const emit = defineEmits(['a', 'b'])
const props = withDefaults(defineProps<{
foo?: string
bar?: number
baz: boolean
}>(), { ...defaults })
</script>
`)
Expand All @@ -834,7 +847,8 @@ const emit = defineEmits(['a', 'b'])
`
_mergeDefaults({
foo: { type: String, required: false },
bar: { type: Number, required: false }
bar: { type: Number, required: false },
baz: { type: Boolean, required: true }
}, { ...defaults })`.trim()
)
})
Expand Down
112 changes: 85 additions & 27 deletions packages/compiler-sfc/src/compileScript.ts
Expand Up @@ -38,7 +38,8 @@ import {
RestElement,
TSInterfaceBody,
AwaitExpression,
Program
Program,
ObjectMethod
} from '@babel/types'
import { walk } from 'estree-walker'
import { RawSourceMap } from 'source-map'
Expand Down Expand Up @@ -242,7 +243,7 @@ export function compileScript(
let hasDefineEmitCall = false
let hasDefineExposeCall = false
let propsRuntimeDecl: Node | undefined
let propsRuntimeDefaults: Node | undefined
let propsRuntimeDefaults: ObjectExpression | undefined
let propsTypeDecl: TSTypeLiteral | TSInterfaceBody | undefined
let propsTypeDeclRaw: Node | undefined
let propsIdentifier: string | undefined
Expand Down Expand Up @@ -384,7 +385,16 @@ export function compileScript(
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 {
error(
`${WITH_DEFAULTS}' first argument must be a ${DEFINE_PROPS} call.`,
Expand Down Expand Up @@ -513,38 +523,51 @@ export function compileScript(
)
}

function genRuntimeProps(props: Record<string, PropTypeData>) {
const keys = Object.keys(props)
if (!keys.length) {
return ``
}

// 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.
const hasStaticDefaults =
/**
* 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 =>
(node.type === 'ObjectProperty' && !node.computed) ||
node.type === 'ObjectMethod'
)
)
}

function genRuntimeProps(props: Record<string, PropTypeData>) {
const keys = Object.keys(props)
if (!keys.length) {
return ``
}
const hasStaticDefaults = checkStaticDefaults()
const scriptSetupSource = scriptSetup!.content
let propsDecls = `{
${keys
.map(key => {
let defaultString: string | undefined
if (hasStaticDefaults) {
const prop = (
propsRuntimeDefaults as ObjectExpression
).properties.find(
const prop = propsRuntimeDefaults!.properties.find(
(node: any) => node.key.name === key
) as ObjectProperty
) as ObjectProperty | ObjectMethod
if (prop) {
// prop has corresponding static default value
defaultString = `default: ${source.slice(
prop.value.start! + startOffset,
prop.value.end! + startOffset
)}`
if (prop.type === 'ObjectProperty') {
// prop has corresponding static default value
defaultString = `default: ${scriptSetupSource.slice(
prop.value.start!,
prop.value.end!
)}`
} else {
defaultString = `default() ${scriptSetupSource.slice(
prop.body.start!,
prop.body.end!
)}`
}
}
}

Expand Down Expand Up @@ -572,6 +595,44 @@ export function compileScript(
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
let scriptAst
if (script) {
Expand Down Expand Up @@ -990,10 +1051,7 @@ export function compileScript(
// 9. finalize setup() argument signature
let args = `__props`
if (propsTypeDecl) {
args += `: ${scriptSetup.content.slice(
propsTypeDecl.start!,
propsTypeDecl.end!
)}`
args += genSetupPropsType(propsTypeDecl)
}
// inject user assignment of props
// we use a default __props so that template expressions referencing props
Expand Down