feat(sfc): withDefaults helper
This commit is contained in:
parent
3ffc7be864
commit
4c5844a9ca
@ -881,3 +881,50 @@ return { }
|
||||
|
||||
})"
|
||||
`;
|
||||
|
||||
exports[`SFC compile <script setup> with TypeScript withDefaults (dynamic) 1`] = `
|
||||
"import { mergeDefaults as _mergeDefaults, defineComponent as _defineComponent } from 'vue'
|
||||
import { defaults } from './foo'
|
||||
|
||||
export default _defineComponent({
|
||||
props: _mergeDefaults({
|
||||
foo: { type: String, required: false },
|
||||
bar: { type: Number, required: false }
|
||||
}, { ...defaults }) as unknown as undefined,
|
||||
setup(__props: {
|
||||
foo?: string
|
||||
bar?: number
|
||||
}, { expose }) {
|
||||
expose()
|
||||
|
||||
const props = __props
|
||||
|
||||
|
||||
return { props, defaults }
|
||||
}
|
||||
|
||||
})"
|
||||
`;
|
||||
|
||||
exports[`SFC compile <script setup> with TypeScript withDefaults (static) 1`] = `
|
||||
"import { defineComponent as _defineComponent } from 'vue'
|
||||
|
||||
export default _defineComponent({
|
||||
props: {
|
||||
foo: { type: String, required: false, default: 'hi' },
|
||||
bar: { type: Number, required: false }
|
||||
} as unknown as undefined,
|
||||
setup(__props: {
|
||||
foo?: string
|
||||
bar?: number
|
||||
}, { expose }) {
|
||||
expose()
|
||||
|
||||
const props = __props
|
||||
|
||||
|
||||
return { props }
|
||||
}
|
||||
|
||||
})"
|
||||
`;
|
||||
|
@ -592,6 +592,51 @@ const emit = defineEmits(['a', 'b'])
|
||||
})
|
||||
})
|
||||
|
||||
test('withDefaults (static)', () => {
|
||||
const { content, bindings } = compile(`
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(defineProps<{
|
||||
foo?: string
|
||||
bar?: number
|
||||
}>(), {
|
||||
foo: 'hi'
|
||||
})
|
||||
</script>
|
||||
`)
|
||||
assertCode(content)
|
||||
expect(content).toMatch(
|
||||
`foo: { type: String, required: false, default: 'hi' }`
|
||||
)
|
||||
expect(content).toMatch(`bar: { type: Number, required: false }`)
|
||||
expect(content).toMatch(`const props = __props`)
|
||||
expect(bindings).toStrictEqual({
|
||||
foo: BindingTypes.PROPS,
|
||||
bar: BindingTypes.PROPS,
|
||||
props: BindingTypes.SETUP_CONST
|
||||
})
|
||||
})
|
||||
|
||||
test('withDefaults (dynamic)', () => {
|
||||
const { content } = compile(`
|
||||
<script setup lang="ts">
|
||||
import { defaults } from './foo'
|
||||
const props = withDefaults(defineProps<{
|
||||
foo?: string
|
||||
bar?: number
|
||||
}>(), { ...defaults })
|
||||
</script>
|
||||
`)
|
||||
assertCode(content)
|
||||
expect(content).toMatch(`import { mergeDefaults as _mergeDefaults`)
|
||||
expect(content).toMatch(
|
||||
`
|
||||
_mergeDefaults({
|
||||
foo: { type: String, required: false },
|
||||
bar: { type: Number, required: false }
|
||||
}, { ...defaults })`.trim()
|
||||
)
|
||||
})
|
||||
|
||||
test('defineEmits w/ type', () => {
|
||||
const { content } = compile(`
|
||||
<script setup lang="ts">
|
||||
@ -942,7 +987,6 @@ const emit = defineEmits(['a', 'b'])
|
||||
test('defineProps/Emit() w/ both type and non-type args', () => {
|
||||
expect(() => {
|
||||
compile(`<script setup lang="ts">
|
||||
import { defineProps } from 'vue'
|
||||
defineProps<{}>({})
|
||||
</script>`)
|
||||
}).toThrow(`cannot accept both type and non-type arguments`)
|
||||
|
@ -36,8 +36,11 @@ import { rewriteDefault } from './rewriteDefault'
|
||||
|
||||
const DEFINE_PROPS = 'defineProps'
|
||||
const DEFINE_EMIT = 'defineEmit'
|
||||
const DEFINE_EMITS = 'defineEmits'
|
||||
const DEFINE_EXPOSE = 'defineExpose'
|
||||
const WITH_DEFAULTS = 'withDefaults'
|
||||
|
||||
// deprecated
|
||||
const DEFINE_EMITS = 'defineEmits'
|
||||
|
||||
export interface SFCScriptCompileOptions {
|
||||
/**
|
||||
@ -191,6 +194,7 @@ export function compileScript(
|
||||
let hasDefineEmitCall = false
|
||||
let hasDefineExposeCall = false
|
||||
let propsRuntimeDecl: Node | undefined
|
||||
let propsRuntimeDefaults: Node | undefined
|
||||
let propsTypeDecl: TSTypeLiteral | undefined
|
||||
let propsIdentifier: string | undefined
|
||||
let emitRuntimeDecl: Node | undefined
|
||||
@ -262,68 +266,95 @@ export function compileScript(
|
||||
}
|
||||
|
||||
function processDefineProps(node: Node): boolean {
|
||||
if (isCallOf(node, DEFINE_PROPS)) {
|
||||
if (hasDefinePropsCall) {
|
||||
error(`duplicate ${DEFINE_PROPS}() call`, node)
|
||||
}
|
||||
hasDefinePropsCall = true
|
||||
propsRuntimeDecl = node.arguments[0]
|
||||
// context call has type parameters - infer runtime types from it
|
||||
if (node.typeParameters) {
|
||||
if (propsRuntimeDecl) {
|
||||
error(
|
||||
`${DEFINE_PROPS}() cannot accept both type and non-type arguments ` +
|
||||
`at the same time. Use one or the other.`,
|
||||
node
|
||||
)
|
||||
}
|
||||
const typeArg = node.typeParameters.params[0]
|
||||
if (typeArg.type === 'TSTypeLiteral') {
|
||||
propsTypeDecl = typeArg
|
||||
} else {
|
||||
error(
|
||||
`type argument passed to ${DEFINE_PROPS}() must be a literal type.`,
|
||||
typeArg
|
||||
)
|
||||
}
|
||||
}
|
||||
return true
|
||||
if (!isCallOf(node, DEFINE_PROPS)) {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
|
||||
if (hasDefinePropsCall) {
|
||||
error(`duplicate ${DEFINE_PROPS}() call`, node)
|
||||
}
|
||||
hasDefinePropsCall = true
|
||||
|
||||
propsRuntimeDecl = node.arguments[0]
|
||||
|
||||
// call has type parameters - infer runtime types from it
|
||||
if (node.typeParameters) {
|
||||
if (propsRuntimeDecl) {
|
||||
error(
|
||||
`${DEFINE_PROPS}() cannot accept both type and non-type arguments ` +
|
||||
`at the same time. Use one or the other.`,
|
||||
node
|
||||
)
|
||||
}
|
||||
|
||||
const typeArg = node.typeParameters.params[0]
|
||||
if (typeArg.type === 'TSTypeLiteral') {
|
||||
propsTypeDecl = typeArg
|
||||
} else {
|
||||
error(
|
||||
`type argument passed to ${DEFINE_PROPS}() must be a literal type.`,
|
||||
typeArg
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function processWithDefaults(node: Node): boolean {
|
||||
if (!isCallOf(node, WITH_DEFAULTS)) {
|
||||
return false
|
||||
}
|
||||
if (processDefineProps(node.arguments[0])) {
|
||||
if (propsRuntimeDecl) {
|
||||
error(
|
||||
`${WITH_DEFAULTS} can only be used with type-based ` +
|
||||
`${DEFINE_PROPS} declaration.`,
|
||||
node
|
||||
)
|
||||
}
|
||||
propsRuntimeDefaults = node.arguments[1]
|
||||
} else {
|
||||
error(
|
||||
`${WITH_DEFAULTS}' first argument must be a ${DEFINE_PROPS} call.`,
|
||||
node.arguments[0] || node
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function processDefineEmits(node: Node): boolean {
|
||||
if (isCallOf(node, DEFINE_EMIT) || isCallOf(node, DEFINE_EMITS)) {
|
||||
if (hasDefineEmitCall) {
|
||||
error(`duplicate ${DEFINE_EMITS}() call`, node)
|
||||
}
|
||||
hasDefineEmitCall = true
|
||||
emitRuntimeDecl = node.arguments[0]
|
||||
if (node.typeParameters) {
|
||||
if (emitRuntimeDecl) {
|
||||
error(
|
||||
`${DEFINE_EMIT}() cannot accept both type and non-type arguments ` +
|
||||
`at the same time. Use one or the other.`,
|
||||
node
|
||||
)
|
||||
}
|
||||
const typeArg = node.typeParameters.params[0]
|
||||
if (
|
||||
typeArg.type === 'TSFunctionType' ||
|
||||
typeArg.type === 'TSTypeLiteral'
|
||||
) {
|
||||
emitTypeDecl = typeArg
|
||||
} else {
|
||||
error(
|
||||
`type argument passed to ${DEFINE_EMITS}() must be a function type ` +
|
||||
`or a literal type with call signatures.`,
|
||||
typeArg
|
||||
)
|
||||
}
|
||||
}
|
||||
return true
|
||||
if (!isCallOf(node, c => c === DEFINE_EMIT || c === DEFINE_EMITS)) {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
if (hasDefineEmitCall) {
|
||||
error(`duplicate ${DEFINE_EMITS}() call`, node)
|
||||
}
|
||||
hasDefineEmitCall = true
|
||||
emitRuntimeDecl = node.arguments[0]
|
||||
if (node.typeParameters) {
|
||||
if (emitRuntimeDecl) {
|
||||
error(
|
||||
`${DEFINE_EMIT}() cannot accept both type and non-type arguments ` +
|
||||
`at the same time. Use one or the other.`,
|
||||
node
|
||||
)
|
||||
}
|
||||
const typeArg = node.typeParameters.params[0]
|
||||
if (
|
||||
typeArg.type === 'TSFunctionType' ||
|
||||
typeArg.type === 'TSTypeLiteral'
|
||||
) {
|
||||
emitTypeDecl = typeArg
|
||||
} else {
|
||||
error(
|
||||
`type argument passed to ${DEFINE_EMITS}() must be a function type ` +
|
||||
`or a literal type with call signatures.`,
|
||||
typeArg
|
||||
)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function processDefineExpose(node: Node): boolean {
|
||||
@ -480,6 +511,63 @@ 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 =
|
||||
propsRuntimeDefaults &&
|
||||
propsRuntimeDefaults.type === 'ObjectExpression' &&
|
||||
propsRuntimeDefaults.properties.every(
|
||||
node => node.type === 'ObjectProperty' && !node.computed
|
||||
)
|
||||
|
||||
let propsDecls = `{
|
||||
${keys
|
||||
.map(key => {
|
||||
let defaultString: string | undefined
|
||||
if (hasStaticDefaults) {
|
||||
const prop = (propsRuntimeDefaults as ObjectExpression).properties.find(
|
||||
(node: any) => node.key.name === key
|
||||
) as ObjectProperty
|
||||
if (prop) {
|
||||
// prop has corresponding static default value
|
||||
defaultString = `default: ${source.slice(
|
||||
prop.value.start! + startOffset,
|
||||
prop.value.end! + startOffset
|
||||
)}`
|
||||
}
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
const { type, required } = props[key]
|
||||
return `${key}: { type: ${toRuntimeTypeString(
|
||||
type
|
||||
)}, required: ${required}${
|
||||
defaultString ? `, ${defaultString}` : ``
|
||||
} }`
|
||||
} else {
|
||||
// production: checks are useless
|
||||
return `${key}: ${defaultString ? `{ ${defaultString} }` : 'null'}`
|
||||
}
|
||||
})
|
||||
.join(',\n ')}\n }`
|
||||
|
||||
if (propsRuntimeDefaults && !hasStaticDefaults) {
|
||||
propsDecls = `${helper('mergeDefaults')}(${propsDecls}, ${source.slice(
|
||||
propsRuntimeDefaults.start! + startOffset,
|
||||
propsRuntimeDefaults.end! + startOffset
|
||||
)})`
|
||||
}
|
||||
|
||||
return `\n props: ${propsDecls} as unknown as undefined,`
|
||||
}
|
||||
|
||||
// 1. process normal <script> first if it exists
|
||||
let scriptAst
|
||||
if (script) {
|
||||
@ -675,7 +763,8 @@ export function compileScript(
|
||||
// process `defineProps` and `defineEmit(s)` calls
|
||||
if (
|
||||
processDefineProps(node.expression) ||
|
||||
processDefineEmits(node.expression)
|
||||
processDefineEmits(node.expression) ||
|
||||
processWithDefaults(node.expression)
|
||||
) {
|
||||
s.remove(node.start! + startOffset, node.end! + startOffset)
|
||||
} else if (processDefineExpose(node.expression)) {
|
||||
@ -692,7 +781,8 @@ export function compileScript(
|
||||
if (node.type === 'VariableDeclaration' && !node.declare) {
|
||||
for (const decl of node.declarations) {
|
||||
if (decl.init) {
|
||||
const isDefineProps = processDefineProps(decl.init)
|
||||
const isDefineProps =
|
||||
processDefineProps(decl.init) || processWithDefaults(decl.init)
|
||||
if (isDefineProps) {
|
||||
propsIdentifier = scriptSetup.content.slice(
|
||||
decl.id.start!,
|
||||
@ -812,6 +902,7 @@ export function compileScript(
|
||||
// 5. check useOptions args to make sure it doesn't reference setup scope
|
||||
// variables
|
||||
checkInvalidScopeReference(propsRuntimeDecl, DEFINE_PROPS)
|
||||
checkInvalidScopeReference(propsRuntimeDefaults, DEFINE_PROPS)
|
||||
checkInvalidScopeReference(emitRuntimeDecl, DEFINE_PROPS)
|
||||
|
||||
// 6. remove non-script content
|
||||
@ -1080,9 +1171,14 @@ function walkDeclaration(
|
||||
for (const { id, init } of node.declarations) {
|
||||
const isDefineCall = !!(
|
||||
isConst &&
|
||||
(isCallOf(init, DEFINE_PROPS) ||
|
||||
isCallOf(init, DEFINE_EMIT) ||
|
||||
isCallOf(init, DEFINE_EMITS))
|
||||
isCallOf(
|
||||
init,
|
||||
c =>
|
||||
c === DEFINE_PROPS ||
|
||||
c === DEFINE_EMIT ||
|
||||
c === DEFINE_EMITS ||
|
||||
c === WITH_DEFAULTS
|
||||
)
|
||||
)
|
||||
if (id.type === 'Identifier') {
|
||||
let bindingType
|
||||
@ -1318,29 +1414,6 @@ function inferRuntimeType(
|
||||
}
|
||||
}
|
||||
|
||||
function genRuntimeProps(props: Record<string, PropTypeData>) {
|
||||
const keys = Object.keys(props)
|
||||
if (!keys.length) {
|
||||
return ``
|
||||
}
|
||||
|
||||
if (!__DEV__) {
|
||||
// production: generate array version only
|
||||
return `\n props: [\n ${keys
|
||||
.map(k => JSON.stringify(k))
|
||||
.join(',\n ')}\n ] as unknown as undefined,`
|
||||
}
|
||||
|
||||
return `\n props: {\n ${keys
|
||||
.map(key => {
|
||||
const { type, required } = props[key]
|
||||
return `${key}: { type: ${toRuntimeTypeString(
|
||||
type
|
||||
)}, required: ${required} }`
|
||||
})
|
||||
.join(',\n ')}\n } as unknown as undefined,`
|
||||
}
|
||||
|
||||
function toRuntimeTypeString(types: string[]) {
|
||||
return types.some(t => t === 'null')
|
||||
? `null`
|
||||
@ -1567,13 +1640,15 @@ function isFunction(node: Node): node is FunctionNode {
|
||||
|
||||
function isCallOf(
|
||||
node: Node | null | undefined,
|
||||
name: string
|
||||
test: string | ((id: string) => boolean)
|
||||
): node is CallExpression {
|
||||
return !!(
|
||||
node &&
|
||||
node.type === 'CallExpression' &&
|
||||
node.callee.type === 'Identifier' &&
|
||||
node.callee.name === name
|
||||
(typeof test === 'string'
|
||||
? node.callee.name === test
|
||||
: test(node.callee.name))
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -8,8 +8,11 @@ import {
|
||||
import {
|
||||
defineEmits,
|
||||
defineProps,
|
||||
defineExpose,
|
||||
withDefaults,
|
||||
useAttrs,
|
||||
useSlots
|
||||
useSlots,
|
||||
mergeDefaults
|
||||
} from '../src/apiSetupHelpers'
|
||||
|
||||
describe('SFC <script setup> helpers', () => {
|
||||
@ -19,6 +22,12 @@ describe('SFC <script setup> helpers', () => {
|
||||
|
||||
defineEmits()
|
||||
expect(`defineEmits() is a compiler-hint`).toHaveBeenWarned()
|
||||
|
||||
defineExpose()
|
||||
expect(`defineExpose() is a compiler-hint`).toHaveBeenWarned()
|
||||
|
||||
withDefaults({}, {})
|
||||
expect(`withDefaults() is a compiler-hint`).toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('useSlots / useAttrs (no args)', () => {
|
||||
@ -58,4 +67,26 @@ describe('SFC <script setup> helpers', () => {
|
||||
expect(slots).toBe(ctx!.slots)
|
||||
expect(attrs).toBe(ctx!.attrs)
|
||||
})
|
||||
|
||||
test('mergeDefaults', () => {
|
||||
const merged = mergeDefaults(
|
||||
{
|
||||
foo: null,
|
||||
bar: { type: String, required: false }
|
||||
},
|
||||
{
|
||||
foo: 1,
|
||||
bar: 'baz'
|
||||
}
|
||||
)
|
||||
expect(merged).toMatchObject({
|
||||
foo: { default: 1 },
|
||||
bar: { type: String, required: false, default: 'baz' }
|
||||
})
|
||||
|
||||
mergeDefaults({}, { foo: 1 })
|
||||
expect(
|
||||
`props default key "foo" has no corresponding declaration`
|
||||
).toHaveBeenWarned()
|
||||
})
|
||||
})
|
||||
|
@ -4,63 +4,104 @@ import {
|
||||
createSetupContext
|
||||
} from './component'
|
||||
import { EmitFn, EmitsOptions } from './componentEmits'
|
||||
import { ComponentObjectPropsOptions, ExtractPropTypes } from './componentProps'
|
||||
import {
|
||||
ComponentObjectPropsOptions,
|
||||
PropOptions,
|
||||
ExtractPropTypes
|
||||
} from './componentProps'
|
||||
import { warn } from './warning'
|
||||
|
||||
type InferDefaults<T> = {
|
||||
[K in keyof T]?: NonNullable<T[K]> extends object
|
||||
? () => NonNullable<T[K]>
|
||||
: NonNullable<T[K]>
|
||||
}
|
||||
// dev only
|
||||
const warnRuntimeUsage = (method: string) =>
|
||||
warn(
|
||||
`${method}() is a compiler-hint helper that is only usable inside ` +
|
||||
`<script setup> of a single file component. Its arguments should be ` +
|
||||
`compiled away and passing it at runtime has no effect.`
|
||||
)
|
||||
|
||||
/**
|
||||
* Compile-time-only helper used for declaring props inside `<script setup>`.
|
||||
* This is stripped away in the compiled code and should never be actually
|
||||
* called at runtime.
|
||||
* Vue `<script setup>` compiler macro for declaring component props. The
|
||||
* expected argument is the same as the component `props` option.
|
||||
*
|
||||
* Example runtime declaration:
|
||||
* ```js
|
||||
* // using Array syntax
|
||||
* const props = defineProps(['foo', 'bar'])
|
||||
* // using Object syntax
|
||||
* const props = defineProps({
|
||||
* foo: String,
|
||||
* bar: {
|
||||
* type: Number,
|
||||
* required: true
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* Equivalent type-based decalration:
|
||||
* ```ts
|
||||
* // will be compiled into equivalent runtime declarations
|
||||
* const props = defineProps<{
|
||||
* foo?: string
|
||||
* bar: number
|
||||
* }>()
|
||||
* ```
|
||||
*
|
||||
* This is only usable inside `<script setup>`, is compiled away in the
|
||||
* output and should **not** be actually called at runtime.
|
||||
*/
|
||||
// overload 1: string props
|
||||
// overload 1: runtime props w/ array
|
||||
export function defineProps<PropNames extends string = string>(
|
||||
props: PropNames[]
|
||||
): Readonly<{ [key in PropNames]?: any }>
|
||||
// overload 2: runtime props w/ object
|
||||
export function defineProps<
|
||||
TypeProps = undefined,
|
||||
PropNames extends string = string,
|
||||
InferredProps = { [key in PropNames]?: any }
|
||||
>(
|
||||
props?: PropNames[]
|
||||
): Readonly<TypeProps extends undefined ? InferredProps : TypeProps>
|
||||
// overload 2: object props
|
||||
export function defineProps<
|
||||
TypeProps = undefined,
|
||||
PP extends ComponentObjectPropsOptions = ComponentObjectPropsOptions,
|
||||
InferredProps = ExtractPropTypes<PP>
|
||||
>(
|
||||
props?: PP,
|
||||
defaults?: InferDefaults<TypeProps>
|
||||
): Readonly<TypeProps extends undefined ? InferredProps : TypeProps>
|
||||
PP extends ComponentObjectPropsOptions = ComponentObjectPropsOptions
|
||||
>(props: PP): Readonly<ExtractPropTypes<PP>>
|
||||
// overload 3: typed-based declaration
|
||||
export function defineProps<TypeProps>(): Readonly<TypeProps>
|
||||
// implementation
|
||||
export function defineProps() {
|
||||
if (__DEV__) {
|
||||
warn(
|
||||
`defineProps() is a compiler-hint helper that is only usable inside ` +
|
||||
`<script setup> of a single file component. Its arguments should be ` +
|
||||
`compiled away and passing it at runtime has no effect.`
|
||||
)
|
||||
warnRuntimeUsage(`defineProps`)
|
||||
}
|
||||
return null as any
|
||||
}
|
||||
|
||||
export function defineEmits<
|
||||
TypeEmit = undefined,
|
||||
E extends EmitsOptions = EmitsOptions,
|
||||
EE extends string = string,
|
||||
InferredEmit = EmitFn<E>
|
||||
>(emitOptions?: E | EE[]): TypeEmit extends undefined ? InferredEmit : TypeEmit
|
||||
/**
|
||||
* Vue `<script setup>` compiler macro for declaring a component's emitted
|
||||
* events. The expected argument is the same as the component `emits` option.
|
||||
*
|
||||
* Example runtime declaration:
|
||||
* ```js
|
||||
* const emit = defineEmits(['change', 'update'])
|
||||
* ```
|
||||
*
|
||||
* Example type-based decalration:
|
||||
* ```ts
|
||||
* const emit = defineEmits<{
|
||||
* (event: 'change'): void
|
||||
* (event: 'update', id: number): void
|
||||
* }>()
|
||||
*
|
||||
* emit('change')
|
||||
* emit('update', 1)
|
||||
* ```
|
||||
*
|
||||
* This is only usable inside `<script setup>`, is compiled away in the
|
||||
* output and should **not** be actually called at runtime.
|
||||
*/
|
||||
// overload 1: runtime emits w/ array
|
||||
export function defineEmits<EE extends string = string>(
|
||||
emitOptions: EE[]
|
||||
): EmitFn<EE[]>
|
||||
export function defineEmits<E extends EmitsOptions = EmitsOptions>(
|
||||
emitOptions: E
|
||||
): EmitFn<E>
|
||||
export function defineEmits<TypeEmit>(): TypeEmit
|
||||
// implementation
|
||||
export function defineEmits() {
|
||||
if (__DEV__) {
|
||||
warn(
|
||||
`defineEmits() is a compiler-hint helper that is only usable inside ` +
|
||||
`<script setup> of a single file component. Its arguments should be ` +
|
||||
`compiled away and passing it at runtime has no effect.`
|
||||
)
|
||||
warnRuntimeUsage(`defineEmits`)
|
||||
}
|
||||
return null as any
|
||||
}
|
||||
@ -70,16 +111,70 @@ export function defineEmits() {
|
||||
*/
|
||||
export const defineEmit = defineEmits
|
||||
|
||||
/**
|
||||
* Vue `<script setup>` compiler macro for declaring a component's exposed
|
||||
* instance properties when it is accessed by a parent component via template
|
||||
* refs.
|
||||
*
|
||||
* `<script setup>` components are closed by default - i.e. varaibles inside
|
||||
* the `<script setup>` scope is not exposed to parent unless explicitly exposed
|
||||
* via `defineExpose`.
|
||||
*
|
||||
* This is only usable inside `<script setup>`, is compiled away in the
|
||||
* output and should **not** be actually called at runtime.
|
||||
*/
|
||||
export function defineExpose(exposed?: Record<string, any>) {
|
||||
if (__DEV__) {
|
||||
warn(
|
||||
`defineExpose() is a compiler-hint helper that is only usable inside ` +
|
||||
`<script setup> of a single file component. Its usage should be ` +
|
||||
`compiled away and calling it at runtime has no effect.`
|
||||
)
|
||||
warnRuntimeUsage(`defineExpose`)
|
||||
}
|
||||
}
|
||||
|
||||
type NotUndefined<T> = T extends undefined ? never : T
|
||||
|
||||
type InferDefaults<T> = {
|
||||
[K in keyof T]?: NotUndefined<T[K]> extends (
|
||||
| number
|
||||
| string
|
||||
| boolean
|
||||
| symbol
|
||||
| Function)
|
||||
? NotUndefined<T[K]>
|
||||
: (props: T) => NotUndefined<T[K]>
|
||||
}
|
||||
|
||||
type PropsWithDefaults<Base, Defaults> = Base &
|
||||
{
|
||||
[K in keyof Defaults]: K extends keyof Base ? NotUndefined<Base[K]> : never
|
||||
}
|
||||
|
||||
/**
|
||||
* Vue `<script setup>` compiler macro for providing props default values when
|
||||
* using type-based `defineProps` decalration.
|
||||
*
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* withDefaults(defineProps<{
|
||||
* size?: number
|
||||
* labels?: string[]
|
||||
* }>(), {
|
||||
* size: 3,
|
||||
* labels: () => ['default label']
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* This is only usable inside `<script setup>`, is compiled away in the output
|
||||
* and should **not** be actually called at runtime.
|
||||
*/
|
||||
export function withDefaults<Props, Defaults extends InferDefaults<Props>>(
|
||||
props: Props,
|
||||
defaults: Defaults
|
||||
): PropsWithDefaults<Props, Defaults> {
|
||||
if (__DEV__) {
|
||||
warnRuntimeUsage(`withDefaults`)
|
||||
}
|
||||
return null as any
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use `useSlots` and `useAttrs` instead.
|
||||
*/
|
||||
@ -93,6 +188,14 @@ export function useContext(): SetupContext {
|
||||
return getContext()
|
||||
}
|
||||
|
||||
export function useSlots(): SetupContext['slots'] {
|
||||
return getContext().slots
|
||||
}
|
||||
|
||||
export function useAttrs(): SetupContext['attrs'] {
|
||||
return getContext().attrs
|
||||
}
|
||||
|
||||
function getContext(): SetupContext {
|
||||
const i = getCurrentInstance()!
|
||||
if (__DEV__ && !i) {
|
||||
@ -101,10 +204,25 @@ function getContext(): SetupContext {
|
||||
return i.setupContext || (i.setupContext = createSetupContext(i))
|
||||
}
|
||||
|
||||
export function useSlots(): SetupContext['slots'] {
|
||||
return getContext().slots
|
||||
}
|
||||
|
||||
export function useAttrs(): SetupContext['attrs'] {
|
||||
return getContext().attrs
|
||||
/**
|
||||
* Runtime helper for merging default declarations. Imported by compiled code
|
||||
* only.
|
||||
* @internal
|
||||
*/
|
||||
export function mergeDefaults(
|
||||
// the base props is compiler-generated and guaranteed to be in this shape.
|
||||
props: Record<string, PropOptions | null>,
|
||||
defaults: Record<string, any>
|
||||
) {
|
||||
for (const key in defaults) {
|
||||
const val = props[key]
|
||||
if (val) {
|
||||
val.default = defaults[key]
|
||||
} else if (val === null) {
|
||||
props[key] = { default: defaults[key] }
|
||||
} else if (__DEV__) {
|
||||
warn(`props default key "${key}" has no corresponding declaration.`)
|
||||
}
|
||||
}
|
||||
return props
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ export type Prop<T, D = T> = PropOptions<T, D> | PropType<T>
|
||||
|
||||
type DefaultFactory<T> = (props: Data) => T | null | undefined
|
||||
|
||||
interface PropOptions<T = any, D = T> {
|
||||
export interface PropOptions<T = any, D = T> {
|
||||
type?: PropType<T> | true | null
|
||||
required?: boolean
|
||||
default?: D | DefaultFactory<D> | null | undefined | object
|
||||
|
@ -44,9 +44,17 @@ export { provide, inject } from './apiInject'
|
||||
export { nextTick } from './scheduler'
|
||||
export { defineComponent } from './apiDefineComponent'
|
||||
export { defineAsyncComponent } from './apiAsyncComponent'
|
||||
|
||||
// <script setup> API ----------------------------------------------------------
|
||||
|
||||
export {
|
||||
defineProps,
|
||||
defineEmits,
|
||||
defineExpose,
|
||||
withDefaults,
|
||||
// internal
|
||||
mergeDefaults,
|
||||
// deprecated
|
||||
defineEmit,
|
||||
useContext
|
||||
} from './apiSetupHelpers'
|
||||
@ -140,7 +148,6 @@ export {
|
||||
DeepReadonly
|
||||
} from '@vue/reactivity'
|
||||
export {
|
||||
// types
|
||||
WatchEffect,
|
||||
WatchOptions,
|
||||
WatchOptionsBase,
|
||||
|
@ -214,7 +214,12 @@ async function doCompileScript(
|
||||
|
||||
return [code, compiledScript.bindings]
|
||||
} catch (e) {
|
||||
store.errors = [e]
|
||||
store.errors = [
|
||||
e.stack
|
||||
.split('\n')
|
||||
.slice(0, 12)
|
||||
.join('\n')
|
||||
]
|
||||
return
|
||||
}
|
||||
} else {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { withDefaults } from '../packages/runtime-core/src/apiSetupHelpers'
|
||||
import {
|
||||
expectType,
|
||||
defineProps,
|
||||
@ -19,30 +20,29 @@ describe('defineProps w/ type declaration', () => {
|
||||
props.bar
|
||||
})
|
||||
|
||||
describe('defineProps w/ type declaration + defaults', () => {
|
||||
defineProps<{
|
||||
number?: number
|
||||
arr?: string[]
|
||||
arr2?: string[]
|
||||
obj?: { x: number }
|
||||
obj2?: { x: number }
|
||||
obj3?: { x: number }
|
||||
}>(
|
||||
{},
|
||||
describe('defineProps w/ type declaration + withDefaults', () => {
|
||||
const res = withDefaults(
|
||||
defineProps<{
|
||||
number?: number
|
||||
arr?: string[]
|
||||
obj?: { x: number }
|
||||
fn?: (e: string) => void
|
||||
x?: string
|
||||
}>(),
|
||||
{
|
||||
number: 1,
|
||||
|
||||
arr: () => [''],
|
||||
// @ts-expect-error not using factory
|
||||
arr2: [''],
|
||||
|
||||
number: 123,
|
||||
arr: () => [],
|
||||
obj: () => ({ x: 123 }),
|
||||
// @ts-expect-error not using factory
|
||||
obj2: { x: 123 },
|
||||
// @ts-expect-error factory return type does not match
|
||||
obj3: () => ({ x: 'foo' })
|
||||
fn: () => {}
|
||||
}
|
||||
)
|
||||
|
||||
res.number + 1
|
||||
res.arr.push('hi')
|
||||
res.obj.x
|
||||
res.fn('hi')
|
||||
// @ts-expect-error
|
||||
res.x.slice()
|
||||
})
|
||||
|
||||
describe('defineProps w/ runtime declaration', () => {
|
||||
|
Loading…
Reference in New Issue
Block a user