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', () => {
|
test('defineEmits w/ type', () => {
|
||||||
const { content } = compile(`
|
const { content } = compile(`
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -942,7 +987,6 @@ const emit = defineEmits(['a', 'b'])
|
|||||||
test('defineProps/Emit() w/ both type and non-type args', () => {
|
test('defineProps/Emit() w/ both type and non-type args', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
compile(`<script setup lang="ts">
|
compile(`<script setup lang="ts">
|
||||||
import { defineProps } from 'vue'
|
|
||||||
defineProps<{}>({})
|
defineProps<{}>({})
|
||||||
</script>`)
|
</script>`)
|
||||||
}).toThrow(`cannot accept both type and non-type arguments`)
|
}).toThrow(`cannot accept both type and non-type arguments`)
|
||||||
|
@ -36,8 +36,11 @@ import { rewriteDefault } from './rewriteDefault'
|
|||||||
|
|
||||||
const DEFINE_PROPS = 'defineProps'
|
const DEFINE_PROPS = 'defineProps'
|
||||||
const DEFINE_EMIT = 'defineEmit'
|
const DEFINE_EMIT = 'defineEmit'
|
||||||
const DEFINE_EMITS = 'defineEmits'
|
|
||||||
const DEFINE_EXPOSE = 'defineExpose'
|
const DEFINE_EXPOSE = 'defineExpose'
|
||||||
|
const WITH_DEFAULTS = 'withDefaults'
|
||||||
|
|
||||||
|
// deprecated
|
||||||
|
const DEFINE_EMITS = 'defineEmits'
|
||||||
|
|
||||||
export interface SFCScriptCompileOptions {
|
export interface SFCScriptCompileOptions {
|
||||||
/**
|
/**
|
||||||
@ -191,6 +194,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 propsTypeDecl: TSTypeLiteral | undefined
|
let propsTypeDecl: TSTypeLiteral | undefined
|
||||||
let propsIdentifier: string | undefined
|
let propsIdentifier: string | undefined
|
||||||
let emitRuntimeDecl: Node | undefined
|
let emitRuntimeDecl: Node | undefined
|
||||||
@ -262,68 +266,95 @@ export function compileScript(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function processDefineProps(node: Node): boolean {
|
function processDefineProps(node: Node): boolean {
|
||||||
if (isCallOf(node, DEFINE_PROPS)) {
|
if (!isCallOf(node, DEFINE_PROPS)) {
|
||||||
if (hasDefinePropsCall) {
|
return false
|
||||||
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
|
|
||||||
}
|
}
|
||||||
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 {
|
function processDefineEmits(node: Node): boolean {
|
||||||
if (isCallOf(node, DEFINE_EMIT) || isCallOf(node, DEFINE_EMITS)) {
|
if (!isCallOf(node, c => c === DEFINE_EMIT || c === DEFINE_EMITS)) {
|
||||||
if (hasDefineEmitCall) {
|
return false
|
||||||
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
|
|
||||||
}
|
}
|
||||||
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 {
|
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
|
// 1. process normal <script> first if it exists
|
||||||
let scriptAst
|
let scriptAst
|
||||||
if (script) {
|
if (script) {
|
||||||
@ -675,7 +763,8 @@ export function compileScript(
|
|||||||
// process `defineProps` and `defineEmit(s)` calls
|
// process `defineProps` and `defineEmit(s)` calls
|
||||||
if (
|
if (
|
||||||
processDefineProps(node.expression) ||
|
processDefineProps(node.expression) ||
|
||||||
processDefineEmits(node.expression)
|
processDefineEmits(node.expression) ||
|
||||||
|
processWithDefaults(node.expression)
|
||||||
) {
|
) {
|
||||||
s.remove(node.start! + startOffset, node.end! + startOffset)
|
s.remove(node.start! + startOffset, node.end! + startOffset)
|
||||||
} else if (processDefineExpose(node.expression)) {
|
} else if (processDefineExpose(node.expression)) {
|
||||||
@ -692,7 +781,8 @@ export function compileScript(
|
|||||||
if (node.type === 'VariableDeclaration' && !node.declare) {
|
if (node.type === 'VariableDeclaration' && !node.declare) {
|
||||||
for (const decl of node.declarations) {
|
for (const decl of node.declarations) {
|
||||||
if (decl.init) {
|
if (decl.init) {
|
||||||
const isDefineProps = processDefineProps(decl.init)
|
const isDefineProps =
|
||||||
|
processDefineProps(decl.init) || processWithDefaults(decl.init)
|
||||||
if (isDefineProps) {
|
if (isDefineProps) {
|
||||||
propsIdentifier = scriptSetup.content.slice(
|
propsIdentifier = scriptSetup.content.slice(
|
||||||
decl.id.start!,
|
decl.id.start!,
|
||||||
@ -812,6 +902,7 @@ export function compileScript(
|
|||||||
// 5. check useOptions args to make sure it doesn't reference setup scope
|
// 5. check useOptions args to make sure it doesn't reference setup scope
|
||||||
// variables
|
// variables
|
||||||
checkInvalidScopeReference(propsRuntimeDecl, DEFINE_PROPS)
|
checkInvalidScopeReference(propsRuntimeDecl, DEFINE_PROPS)
|
||||||
|
checkInvalidScopeReference(propsRuntimeDefaults, DEFINE_PROPS)
|
||||||
checkInvalidScopeReference(emitRuntimeDecl, DEFINE_PROPS)
|
checkInvalidScopeReference(emitRuntimeDecl, DEFINE_PROPS)
|
||||||
|
|
||||||
// 6. remove non-script content
|
// 6. remove non-script content
|
||||||
@ -1080,9 +1171,14 @@ function walkDeclaration(
|
|||||||
for (const { id, init } of node.declarations) {
|
for (const { id, init } of node.declarations) {
|
||||||
const isDefineCall = !!(
|
const isDefineCall = !!(
|
||||||
isConst &&
|
isConst &&
|
||||||
(isCallOf(init, DEFINE_PROPS) ||
|
isCallOf(
|
||||||
isCallOf(init, DEFINE_EMIT) ||
|
init,
|
||||||
isCallOf(init, DEFINE_EMITS))
|
c =>
|
||||||
|
c === DEFINE_PROPS ||
|
||||||
|
c === DEFINE_EMIT ||
|
||||||
|
c === DEFINE_EMITS ||
|
||||||
|
c === WITH_DEFAULTS
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if (id.type === 'Identifier') {
|
if (id.type === 'Identifier') {
|
||||||
let bindingType
|
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[]) {
|
function toRuntimeTypeString(types: string[]) {
|
||||||
return types.some(t => t === 'null')
|
return types.some(t => t === 'null')
|
||||||
? `null`
|
? `null`
|
||||||
@ -1567,13 +1640,15 @@ function isFunction(node: Node): node is FunctionNode {
|
|||||||
|
|
||||||
function isCallOf(
|
function isCallOf(
|
||||||
node: Node | null | undefined,
|
node: Node | null | undefined,
|
||||||
name: string
|
test: string | ((id: string) => boolean)
|
||||||
): node is CallExpression {
|
): node is CallExpression {
|
||||||
return !!(
|
return !!(
|
||||||
node &&
|
node &&
|
||||||
node.type === 'CallExpression' &&
|
node.type === 'CallExpression' &&
|
||||||
node.callee.type === 'Identifier' &&
|
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 {
|
import {
|
||||||
defineEmits,
|
defineEmits,
|
||||||
defineProps,
|
defineProps,
|
||||||
|
defineExpose,
|
||||||
|
withDefaults,
|
||||||
useAttrs,
|
useAttrs,
|
||||||
useSlots
|
useSlots,
|
||||||
|
mergeDefaults
|
||||||
} from '../src/apiSetupHelpers'
|
} from '../src/apiSetupHelpers'
|
||||||
|
|
||||||
describe('SFC <script setup> helpers', () => {
|
describe('SFC <script setup> helpers', () => {
|
||||||
@ -19,6 +22,12 @@ describe('SFC <script setup> helpers', () => {
|
|||||||
|
|
||||||
defineEmits()
|
defineEmits()
|
||||||
expect(`defineEmits() is a compiler-hint`).toHaveBeenWarned()
|
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)', () => {
|
test('useSlots / useAttrs (no args)', () => {
|
||||||
@ -58,4 +67,26 @@ describe('SFC <script setup> helpers', () => {
|
|||||||
expect(slots).toBe(ctx!.slots)
|
expect(slots).toBe(ctx!.slots)
|
||||||
expect(attrs).toBe(ctx!.attrs)
|
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
|
createSetupContext
|
||||||
} from './component'
|
} from './component'
|
||||||
import { EmitFn, EmitsOptions } from './componentEmits'
|
import { EmitFn, EmitsOptions } from './componentEmits'
|
||||||
import { ComponentObjectPropsOptions, ExtractPropTypes } from './componentProps'
|
import {
|
||||||
|
ComponentObjectPropsOptions,
|
||||||
|
PropOptions,
|
||||||
|
ExtractPropTypes
|
||||||
|
} from './componentProps'
|
||||||
import { warn } from './warning'
|
import { warn } from './warning'
|
||||||
|
|
||||||
type InferDefaults<T> = {
|
// dev only
|
||||||
[K in keyof T]?: NonNullable<T[K]> extends object
|
const warnRuntimeUsage = (method: string) =>
|
||||||
? () => NonNullable<T[K]>
|
warn(
|
||||||
: NonNullable<T[K]>
|
`${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>`.
|
* Vue `<script setup>` compiler macro for declaring component props. The
|
||||||
* This is stripped away in the compiled code and should never be actually
|
* expected argument is the same as the component `props` option.
|
||||||
* called at runtime.
|
*
|
||||||
|
* 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<
|
export function defineProps<
|
||||||
TypeProps = undefined,
|
PP extends ComponentObjectPropsOptions = ComponentObjectPropsOptions
|
||||||
PropNames extends string = string,
|
>(props: PP): Readonly<ExtractPropTypes<PP>>
|
||||||
InferredProps = { [key in PropNames]?: any }
|
// overload 3: typed-based declaration
|
||||||
>(
|
export function defineProps<TypeProps>(): Readonly<TypeProps>
|
||||||
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>
|
|
||||||
// implementation
|
// implementation
|
||||||
export function defineProps() {
|
export function defineProps() {
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
warn(
|
warnRuntimeUsage(`defineProps`)
|
||||||
`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.`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return null as any
|
return null as any
|
||||||
}
|
}
|
||||||
|
|
||||||
export function defineEmits<
|
/**
|
||||||
TypeEmit = undefined,
|
* Vue `<script setup>` compiler macro for declaring a component's emitted
|
||||||
E extends EmitsOptions = EmitsOptions,
|
* events. The expected argument is the same as the component `emits` option.
|
||||||
EE extends string = string,
|
*
|
||||||
InferredEmit = EmitFn<E>
|
* Example runtime declaration:
|
||||||
>(emitOptions?: E | EE[]): TypeEmit extends undefined ? InferredEmit : TypeEmit
|
* ```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
|
// implementation
|
||||||
export function defineEmits() {
|
export function defineEmits() {
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
warn(
|
warnRuntimeUsage(`defineEmits`)
|
||||||
`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.`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return null as any
|
return null as any
|
||||||
}
|
}
|
||||||
@ -70,16 +111,70 @@ export function defineEmits() {
|
|||||||
*/
|
*/
|
||||||
export const defineEmit = 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>) {
|
export function defineExpose(exposed?: Record<string, any>) {
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
warn(
|
warnRuntimeUsage(`defineExpose`)
|
||||||
`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.`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.
|
* @deprecated use `useSlots` and `useAttrs` instead.
|
||||||
*/
|
*/
|
||||||
@ -93,6 +188,14 @@ export function useContext(): SetupContext {
|
|||||||
return getContext()
|
return getContext()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useSlots(): SetupContext['slots'] {
|
||||||
|
return getContext().slots
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAttrs(): SetupContext['attrs'] {
|
||||||
|
return getContext().attrs
|
||||||
|
}
|
||||||
|
|
||||||
function getContext(): SetupContext {
|
function getContext(): SetupContext {
|
||||||
const i = getCurrentInstance()!
|
const i = getCurrentInstance()!
|
||||||
if (__DEV__ && !i) {
|
if (__DEV__ && !i) {
|
||||||
@ -101,10 +204,25 @@ function getContext(): SetupContext {
|
|||||||
return i.setupContext || (i.setupContext = createSetupContext(i))
|
return i.setupContext || (i.setupContext = createSetupContext(i))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSlots(): SetupContext['slots'] {
|
/**
|
||||||
return getContext().slots
|
* Runtime helper for merging default declarations. Imported by compiled code
|
||||||
}
|
* only.
|
||||||
|
* @internal
|
||||||
export function useAttrs(): SetupContext['attrs'] {
|
*/
|
||||||
return getContext().attrs
|
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
|
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
|
type?: PropType<T> | true | null
|
||||||
required?: boolean
|
required?: boolean
|
||||||
default?: D | DefaultFactory<D> | null | undefined | object
|
default?: D | DefaultFactory<D> | null | undefined | object
|
||||||
|
@ -44,9 +44,17 @@ export { provide, inject } from './apiInject'
|
|||||||
export { nextTick } from './scheduler'
|
export { nextTick } from './scheduler'
|
||||||
export { defineComponent } from './apiDefineComponent'
|
export { defineComponent } from './apiDefineComponent'
|
||||||
export { defineAsyncComponent } from './apiAsyncComponent'
|
export { defineAsyncComponent } from './apiAsyncComponent'
|
||||||
|
|
||||||
|
// <script setup> API ----------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
defineProps,
|
defineProps,
|
||||||
defineEmits,
|
defineEmits,
|
||||||
|
defineExpose,
|
||||||
|
withDefaults,
|
||||||
|
// internal
|
||||||
|
mergeDefaults,
|
||||||
|
// deprecated
|
||||||
defineEmit,
|
defineEmit,
|
||||||
useContext
|
useContext
|
||||||
} from './apiSetupHelpers'
|
} from './apiSetupHelpers'
|
||||||
@ -140,7 +148,6 @@ export {
|
|||||||
DeepReadonly
|
DeepReadonly
|
||||||
} from '@vue/reactivity'
|
} from '@vue/reactivity'
|
||||||
export {
|
export {
|
||||||
// types
|
|
||||||
WatchEffect,
|
WatchEffect,
|
||||||
WatchOptions,
|
WatchOptions,
|
||||||
WatchOptionsBase,
|
WatchOptionsBase,
|
||||||
|
@ -214,7 +214,12 @@ async function doCompileScript(
|
|||||||
|
|
||||||
return [code, compiledScript.bindings]
|
return [code, compiledScript.bindings]
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
store.errors = [e]
|
store.errors = [
|
||||||
|
e.stack
|
||||||
|
.split('\n')
|
||||||
|
.slice(0, 12)
|
||||||
|
.join('\n')
|
||||||
|
]
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { withDefaults } from '../packages/runtime-core/src/apiSetupHelpers'
|
||||||
import {
|
import {
|
||||||
expectType,
|
expectType,
|
||||||
defineProps,
|
defineProps,
|
||||||
@ -19,30 +20,29 @@ describe('defineProps w/ type declaration', () => {
|
|||||||
props.bar
|
props.bar
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('defineProps w/ type declaration + defaults', () => {
|
describe('defineProps w/ type declaration + withDefaults', () => {
|
||||||
defineProps<{
|
const res = withDefaults(
|
||||||
number?: number
|
defineProps<{
|
||||||
arr?: string[]
|
number?: number
|
||||||
arr2?: string[]
|
arr?: string[]
|
||||||
obj?: { x: number }
|
obj?: { x: number }
|
||||||
obj2?: { x: number }
|
fn?: (e: string) => void
|
||||||
obj3?: { x: number }
|
x?: string
|
||||||
}>(
|
}>(),
|
||||||
{},
|
|
||||||
{
|
{
|
||||||
number: 1,
|
number: 123,
|
||||||
|
arr: () => [],
|
||||||
arr: () => [''],
|
|
||||||
// @ts-expect-error not using factory
|
|
||||||
arr2: [''],
|
|
||||||
|
|
||||||
obj: () => ({ x: 123 }),
|
obj: () => ({ x: 123 }),
|
||||||
// @ts-expect-error not using factory
|
fn: () => {}
|
||||||
obj2: { x: 123 },
|
|
||||||
// @ts-expect-error factory return type does not match
|
|
||||||
obj3: () => ({ x: 'foo' })
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
res.number + 1
|
||||||
|
res.arr.push('hi')
|
||||||
|
res.obj.x
|
||||||
|
res.fn('hi')
|
||||||
|
// @ts-expect-error
|
||||||
|
res.x.slice()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('defineProps w/ runtime declaration', () => {
|
describe('defineProps w/ runtime declaration', () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user