wip: defineOptions -> defineProps + defineEmit + useContext

This commit is contained in:
Evan You 2020-11-24 15:12:59 -05:00
parent ae2caad740
commit 47d73c23e1
13 changed files with 593 additions and 523 deletions

View File

@ -33,26 +33,39 @@ return { x }
export const n = 1" export const n = 1"
`; `;
exports[`SFC compile <script setup> defineOptions() 1`] = ` exports[`SFC compile <script setup> defineEmit() 1`] = `
"export default { "export default {
expose: [], expose: [],
props: { emits: ['foo', 'bar'],
foo: String setup(__props, { emit: myEmit }) {
},
emit: ['a', 'b'],
setup(__props, { props, emit }) {
const bar = 1 return { myEmit }
return { props, emit, bar }
} }
}" }"
`; `;
exports[`SFC compile <script setup> errors should allow defineOptions() referencing imported binding 1`] = ` exports[`SFC compile <script setup> defineProps() 1`] = `
"export default {
expose: [],
props: {
foo: String
},
setup(__props) {
const props = __props
const bar = 1
return { props, bar }
}
}"
`;
exports[`SFC compile <script setup> errors should allow defineProps/Emit() referencing imported binding 1`] = `
"import { bar } from './bar' "import { bar } from './bar'
export default { export default {
@ -62,17 +75,21 @@ export default {
default: () => bar default: () => bar
} }
}, },
emits: {
foo: () => bar > 1
},
setup(__props) { setup(__props) {
return { bar } return { bar }
} }
}" }"
`; `;
exports[`SFC compile <script setup> errors should allow defineOptions() referencing scope var 1`] = ` exports[`SFC compile <script setup> errors should allow defineProps/Emit() referencing scope var 1`] = `
"export default { "export default {
expose: [], expose: [],
props: { props: {
@ -80,11 +97,15 @@ exports[`SFC compile <script setup> errors should allow defineOptions() referenc
default: bar => bar + 1 default: bar => bar + 1
} }
}, },
emits: {
foo: bar => bar > 1
},
setup(__props) { setup(__props) {
const bar = 1 const bar = 1
return { bar } return { bar }
} }
@ -594,37 +615,18 @@ return { a, b, c, d, x }
}" }"
`; `;
exports[`SFC compile <script setup> with TypeScript defineOptions w/ runtime options 1`] = ` exports[`SFC compile <script setup> with TypeScript defineEmit w/ type (union) 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
export default _defineComponent({
expose: [],
props: { foo: String },
emits: ['a', 'b'],
setup(__props, { props, emit }) {
return { props, emit }
}
})"
`;
exports[`SFC compile <script setup> with TypeScript defineOptions w/ type / extract emits (union) 1`] = `
"import { Slots as _Slots, defineComponent as _defineComponent } from 'vue'
export default _defineComponent({ export default _defineComponent({
expose: [], expose: [],
emits: [\\"foo\\", \\"bar\\", \\"baz\\"] as unknown as undefined, emits: [\\"foo\\", \\"bar\\", \\"baz\\"] as unknown as undefined,
setup(__props, { emit }: { setup(__props, { emit }: {
props: {}, emit: (((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void)),
emit: ((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void), slots: any,
slots: Slots, attrs: any
attrs: Record<string, any> }) {
}) {
@ -634,19 +636,18 @@ return { emit }
})" })"
`; `;
exports[`SFC compile <script setup> with TypeScript defineOptions w/ type / extract emits 1`] = ` exports[`SFC compile <script setup> with TypeScript defineEmit w/ type 1`] = `
"import { Slots as _Slots, defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
export default _defineComponent({ export default _defineComponent({
expose: [], expose: [],
emits: [\\"foo\\", \\"bar\\"] as unknown as undefined, emits: [\\"foo\\", \\"bar\\"] as unknown as undefined,
setup(__props, { emit }: { setup(__props, { emit }: {
props: {}, emit: ((e: 'foo' | 'bar') => void),
emit: (e: 'foo' | 'bar') => void, slots: any,
slots: Slots, attrs: any
attrs: Record<string, any> }) {
}) {
@ -656,7 +657,7 @@ return { emit }
})" })"
`; `;
exports[`SFC compile <script setup> with TypeScript defineOptions w/ type / extract props 1`] = ` exports[`SFC compile <script setup> with TypeScript defineProps w/ type 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
interface Test {} interface Test {}
@ -689,7 +690,30 @@ export default _defineComponent({
literalUnionMixed: { type: [String, Number, Boolean], required: true }, literalUnionMixed: { type: [String, Number, Boolean], required: true },
intersection: { type: Object, required: true } intersection: { type: Object, required: true }
} as unknown as undefined, } as unknown as undefined,
setup(__props) { setup(__props: {
string: string
number: number
boolean: boolean
object: object
objectLiteral: { a: number }
fn: (n: number) => void
functionRef: Function
objectRef: Object
array: string[]
arrayRef: Array<any>
tuple: [number, number]
set: Set<string>
literal: 'foo'
optional?: any
recordRef: Record<string, null>
interface: Test
alias: Alias
union: string | number
literalUnion: 'foo' | 'bar'
literalUnionMixed: 'foo' | 1 | boolean
intersection: Test & {}
}) {
@ -699,6 +723,26 @@ return { }
})" })"
`; `;
exports[`SFC compile <script setup> with TypeScript defineProps/Emit w/ runtime options 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
export default _defineComponent({
expose: [],
props: { foo: String },
emits: ['a', 'b'],
setup(__props, { emit }) {
const props = __props
return { props, emit }
}
})"
`;
exports[`SFC compile <script setup> with TypeScript hoist type declarations 1`] = ` exports[`SFC compile <script setup> with TypeScript hoist type declarations 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
export interface Foo {} export interface Foo {}

View File

@ -16,17 +16,13 @@ describe('SFC compile <script setup>', () => {
expect(content).toMatch('return { a, b, c, d, x }') expect(content).toMatch('return { a, b, c, d, x }')
}) })
test('defineOptions()', () => { test('defineProps()', () => {
const { content, bindings } = compile(` const { content, bindings } = compile(`
<script setup> <script setup>
import { defineOptions } from 'vue' import { defineProps } from 'vue'
const { props, emit } = defineOptions({ const props = defineProps({
props: {
foo: String foo: String
},
emit: ['a', 'b']
}) })
const bar = 1 const bar = 1
</script> </script>
`) `)
@ -36,21 +32,42 @@ const bar = 1
expect(bindings).toStrictEqual({ expect(bindings).toStrictEqual({
foo: BindingTypes.PROPS, foo: BindingTypes.PROPS,
bar: BindingTypes.SETUP_CONST, bar: BindingTypes.SETUP_CONST,
props: BindingTypes.SETUP_CONST, props: BindingTypes.SETUP_CONST
emit: BindingTypes.SETUP_CONST
}) })
// should remove defineOptions import and call // should remove defineOptions import and call
expect(content).not.toMatch('defineOptions') expect(content).not.toMatch('defineProps')
// should generate correct setup signature // should generate correct setup signature
expect(content).toMatch(`setup(__props, { props, emit }) {`) expect(content).toMatch(`setup(__props) {`)
// should assign user identifier to it
expect(content).toMatch(`const props = __props`)
// should include context options in default export // should include context options in default export
expect(content).toMatch(`export default { expect(content).toMatch(`export default {
expose: [], expose: [],
props: { props: {
foo: String foo: String
}, },`)
emit: ['a', 'b'],`) })
test('defineEmit()', () => {
const { content, bindings } = compile(`
<script setup>
import { defineEmit } from 'vue'
const myEmit = defineEmit(['foo', 'bar'])
</script>
`)
assertCode(content)
expect(bindings).toStrictEqual({
myEmit: BindingTypes.SETUP_CONST
})
// should remove defineOptions import and call
expect(content).not.toMatch('defineEmit')
// should generate correct setup signature
expect(content).toMatch(`setup(__props, { emit: myEmit }) {`)
// should include context options in default export
expect(content).toMatch(`export default {
expose: [],
emits: ['foo', 'bar'],`)
}) })
describe('<script> and <script setup> co-usage', () => { describe('<script> and <script setup> co-usage', () => {
@ -174,7 +191,7 @@ const bar = 1
// function, const, component import // function, const, component import
const { content } = compile( const { content } = compile(
`<script setup> `<script setup>
import { ref, defineOptions } from 'vue' import { ref } from 'vue'
import Foo from './Foo.vue' import Foo from './Foo.vue'
import other from './util' import other from './util'
const count = ref(0) const count = ref(0)
@ -360,14 +377,12 @@ const bar = 1
assertCode(content) assertCode(content)
}) })
test('defineOptions w/ runtime options', () => { test('defineProps/Emit w/ runtime options', () => {
const { content } = compile(` const { content } = compile(`
<script setup lang="ts"> <script setup lang="ts">
import { defineOptions } from 'vue' import { defineProps, defineEmit } from 'vue'
const { props, emit } = defineOptions({ const props = defineProps({ foo: String })
props: { foo: String }, const emit = defineEmit(['a', 'b'])
emits: ['a', 'b']
})
</script> </script>
`) `)
assertCode(content) assertCode(content)
@ -375,19 +390,18 @@ const { props, emit } = defineOptions({
expose: [], expose: [],
props: { foo: String }, props: { foo: String },
emits: ['a', 'b'], emits: ['a', 'b'],
setup(__props, { props, emit }) {`) setup(__props, { emit }) {`)
}) })
test('defineOptions w/ type / extract props', () => { test('defineProps w/ type', () => {
const { content, bindings } = compile(` const { content, bindings } = compile(`
<script setup lang="ts"> <script setup lang="ts">
import { defineOptions } from 'vue' import { defineProps } from 'vue'
interface Test {} interface Test {}
type Alias = number[] type Alias = number[]
defineOptions<{ defineProps<{
props: {
string: string string: string
number: number number: number
boolean: boolean boolean: boolean
@ -410,7 +424,6 @@ const { props, emit } = defineOptions({
literalUnion: 'foo' | 'bar' literalUnion: 'foo' | 'bar'
literalUnionMixed: 'foo' | 1 | boolean literalUnionMixed: 'foo' | 1 | boolean
intersection: Test & {} intersection: Test & {}
}
}>() }>()
</script>`) </script>`)
assertCode(content) assertCode(content)
@ -466,33 +479,28 @@ const { props, emit } = defineOptions({
}) })
}) })
test('defineOptions w/ type / extract emits', () => { test('defineEmit w/ type', () => {
const { content } = compile(` const { content } = compile(`
<script setup lang="ts"> <script setup lang="ts">
import { defineOptions } from 'vue' import { defineEmit } from 'vue'
const { emit } = defineOptions<{ const emit = defineEmit<(e: 'foo' | 'bar') => void>()
emit: (e: 'foo' | 'bar') => void
}>()
</script> </script>
`) `)
assertCode(content) assertCode(content)
expect(content).toMatch(`props: {},\n emit: (e: 'foo' | 'bar') => void,`) expect(content).toMatch(`emit: ((e: 'foo' | 'bar') => void),`)
expect(content).toMatch(`emits: ["foo", "bar"] as unknown as undefined`) expect(content).toMatch(`emits: ["foo", "bar"] as unknown as undefined`)
}) })
test('defineOptions w/ type / extract emits (union)', () => { test('defineEmit w/ type (union)', () => {
const type = `((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void)`
const { content } = compile(` const { content } = compile(`
<script setup lang="ts"> <script setup lang="ts">
import { defineOptions } from 'vue' import { defineEmit } from 'vue'
const { emit } = defineOptions<{ const emit = defineEmit<${type}>()
emit: ((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void)
}>()
</script> </script>
`) `)
assertCode(content) assertCode(content)
expect(content).toMatch( expect(content).toMatch(`emit: (${type}),`)
`props: {},\n emit: ((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void),`
)
expect(content).toMatch( expect(content).toMatch(
`emits: ["foo", "bar", "baz"] as unknown as undefined` `emits: ["foo", "bar", "baz"] as unknown as undefined`
) )
@ -774,70 +782,95 @@ const { props, emit } = defineOptions({
).toThrow(`ref: statements can only contain assignment expressions`) ).toThrow(`ref: statements can only contain assignment expressions`)
}) })
test('defineOptions() 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 { defineOptions } from 'vue' import { defineProps } from 'vue'
defineOptions<{}>({}) defineProps<{}>({})
</script>`)
}).toThrow(`cannot accept both type and non-type arguments`)
expect(() => {
compile(`<script setup lang="ts">
import { defineEmit } from 'vue'
defineEmit<{}>({})
</script>`) </script>`)
}).toThrow(`cannot accept both type and non-type arguments`) }).toThrow(`cannot accept both type and non-type arguments`)
}) })
test('defineOptions() referencing local var', () => { test('defineProps/Emit() referencing local var', () => {
expect(() => expect(() =>
compile(`<script setup> compile(`<script setup>
import { defineOptions } from 'vue' import { defineProps } from 'vue'
const bar = 1 const bar = 1
defineOptions({ defineProps({
props: {
foo: { foo: {
default: () => bar default: () => bar
} }
}
}) })
</script>`) </script>`)
).toThrow(`cannot reference locally declared variables`) ).toThrow(`cannot reference locally declared variables`)
})
test('defineOptions() referencing ref declarations', () => {
expect(() => expect(() =>
compile(`<script setup> compile(`<script setup>
import { defineOptions } from 'vue' import { defineEmit } from 'vue'
const bar = 'hello'
defineEmit([bar])
</script>`)
).toThrow(`cannot reference locally declared variables`)
})
test('defineProps/Emit() referencing ref declarations', () => {
expect(() =>
compile(`<script setup>
import { defineProps } from 'vue'
ref: bar = 1 ref: bar = 1
defineOptions({ defineProps({
props: { bar } bar
})
</script>`)
).toThrow(`cannot reference locally declared variables`)
expect(() =>
compile(`<script setup>
import { defineEmit } from 'vue'
ref: bar = 1
defineEmit({
bar
}) })
</script>`) </script>`)
).toThrow(`cannot reference locally declared variables`) ).toThrow(`cannot reference locally declared variables`)
}) })
test('should allow defineOptions() referencing scope var', () => { test('should allow defineProps/Emit() referencing scope var', () => {
assertCode( assertCode(
compile(`<script setup> compile(`<script setup>
import { defineOptions } from 'vue' import { defineProps, defineEmit } from 'vue'
const bar = 1 const bar = 1
defineOptions({ defineProps({
props: {
foo: { foo: {
default: bar => bar + 1 default: bar => bar + 1
} }
} })
defineEmit({
foo: bar => bar > 1
}) })
</script>`).content </script>`).content
) )
}) })
test('should allow defineOptions() referencing imported binding', () => { test('should allow defineProps/Emit() referencing imported binding', () => {
assertCode( assertCode(
compile(`<script setup> compile(`<script setup>
import { defineOptions } from 'vue' import { defineProps, defineEmit } from 'vue'
import { bar } from './bar' import { bar } from './bar'
defineOptions({ defineProps({
props: {
foo: { foo: {
default: () => bar default: () => bar
} }
} })
defineEmit({
foo: () => bar > 1
}) })
</script>`).content </script>`).content
) )
@ -1063,11 +1096,9 @@ describe('SFC analyze <script> bindings', () => {
it('works for script setup', () => { it('works for script setup', () => {
const { bindings } = compile(` const { bindings } = compile(`
<script setup> <script setup>
import { defineOptions, ref as r } from 'vue' import { defineProps, ref as r } from 'vue'
defineOptions({ defineProps({
props: { foo: String
foo: String,
}
}) })
const a = r(1) const a = r(1)

View File

@ -20,13 +20,11 @@ describe('CSS vars injection', () => {
test('w/ <script setup> binding analysis', () => { test('w/ <script setup> binding analysis', () => {
const { content } = compileSFCScript( const { content } = compileSFCScript(
`<script setup> `<script setup>
import { defineOptions, ref } from 'vue' import { defineProps, ref } from 'vue'
const color = 'red' const color = 'red'
const size = ref('10px') const size = ref('10px')
defineOptions({ defineProps({
props: {
foo: String foo: String
}
}) })
</script>\n` + </script>\n` +
`<style> `<style>

View File

@ -29,7 +29,8 @@ import { CSS_VARS_HELPER, genCssVarsCode, injectCssVarsCalls } from './cssVars'
import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate' import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate'
import { warnExperimental, warnOnce } from './warn' import { warnExperimental, warnOnce } from './warn'
const DEFINE_OPTIONS = 'defineOptions' const DEFINE_PROPS = 'defineProps'
const DEFINE_EMIT = 'defineEmit'
export interface SFCScriptCompileOptions { export interface SFCScriptCompileOptions {
/** /**
@ -162,17 +163,16 @@ export function compileScript(
const refIdentifiers: Set<Identifier> = new Set() const refIdentifiers: Set<Identifier> = new Set()
const enableRefSugar = options.refSugar !== false const enableRefSugar = options.refSugar !== false
let defaultExport: Node | undefined let defaultExport: Node | undefined
let hasOptionsCall = false let hasDefinePropsCall = false
let optionsExp: string | undefined let hasDefineEmitCall = false
let optionsArg: ObjectExpression | undefined let propsRuntimeDecl: Node | undefined
let optionsType: TSTypeLiteral | undefined let propsTypeDecl: TSTypeLiteral | undefined
let propsIdentifier: string | undefined
let emitRuntimeDecl: Node | undefined
let emitTypeDecl: TSFunctionType | TSUnionType | undefined
let emitIdentifier: string | undefined
let hasAwait = false let hasAwait = false
let hasInlinedSsrRenderFn = false let hasInlinedSsrRenderFn = false
// context types to generate
let propsType = `{}`
let emitType = `(e: string, ...args: any[]) => void`
let slotsType = `Slots`
let attrsType = `Record<string, any>`
// props/emits declared via types // props/emits declared via types
const typeDeclaredProps: Record<string, PropTypeData> = {} const typeDeclaredProps: Record<string, PropTypeData> = {}
const typeDeclaredEmits: Set<string> = new Set() const typeDeclaredEmits: Set<string> = new Set()
@ -236,38 +236,28 @@ export function compileScript(
} }
} }
function processDefineOptions(node: Node): boolean { function processDefineProps(node: Node): boolean {
if (isCallOf(node, DEFINE_OPTIONS)) { if (isCallOf(node, DEFINE_PROPS)) {
if (hasOptionsCall) { if (hasDefinePropsCall) {
error(`duplicate ${DEFINE_OPTIONS}() call`, node) error(`duplicate ${DEFINE_PROPS}() call`, node)
}
hasOptionsCall = true
const optsArg = node.arguments[0]
if (optsArg) {
if (optsArg.type === 'ObjectExpression') {
optionsArg = optsArg
} else {
error(
`${DEFINE_OPTIONS}() argument must be an object literal.`,
optsArg
)
}
} }
hasDefinePropsCall = true
propsRuntimeDecl = node.arguments[0]
// context call has type parameters - infer runtime types from it // context call has type parameters - infer runtime types from it
if (node.typeParameters) { if (node.typeParameters) {
if (optionsArg) { if (propsRuntimeDecl) {
error( error(
`${DEFINE_OPTIONS}() cannot accept both type and non-type arguments ` + `${DEFINE_PROPS}() cannot accept both type and non-type arguments ` +
`at the same time. Use one or the other.`, `at the same time. Use one or the other.`,
node node
) )
} }
const typeArg = node.typeParameters.params[0] const typeArg = node.typeParameters.params[0]
if (typeArg.type === 'TSTypeLiteral') { if (typeArg.type === 'TSTypeLiteral') {
optionsType = typeArg propsTypeDecl = typeArg
} else { } else {
error( error(
`type argument passed to ${DEFINE_OPTIONS}() must be a literal type.`, `type argument passed to ${DEFINE_PROPS}() must be a literal type.`,
typeArg typeArg
) )
} }
@ -277,6 +267,56 @@ export function compileScript(
return false return false
} }
function processDefineEmit(node: Node): boolean {
if (isCallOf(node, DEFINE_EMIT)) {
if (hasDefineEmitCall) {
error(`duplicate ${DEFINE_EMIT}() 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 === 'TSUnionType'
) {
emitTypeDecl = typeArg
} else {
error(
`type argument passed to ${DEFINE_EMIT}() must be a function type ` +
`or a union of function types.`,
typeArg
)
}
}
return true
}
return false
}
function checkInvalidScopeReference(node: Node | undefined, method: string) {
if (!node) return
walkIdentifiers(node, id => {
if (setupBindings[id.name]) {
error(
`\`${method}()\` in <script setup> cannot reference locally ` +
`declared variables because it will be hoisted outside of the ` +
`setup() function. If your component options requires initialization ` +
`in the module scope, use a separate normal <script> to export ` +
`the options instead.`,
id
)
}
})
}
function processRefExpression(exp: Expression, statement: LabeledStatement) { function processRefExpression(exp: Expression, statement: LabeledStatement) {
if (exp.type === 'AssignmentExpression') { if (exp.type === 'AssignmentExpression') {
const { left, right } = exp const { left, right } = exp
@ -562,7 +602,10 @@ export function compileScript(
specifier.imported.name specifier.imported.name
const source = node.source.value const source = node.source.value
const existing = userImports[local] const existing = userImports[local]
if (source === 'vue' && imported === DEFINE_OPTIONS) { if (
source === 'vue' &&
(imported === DEFINE_PROPS || imported === DEFINE_EMIT)
) {
removeSpecifier(specifier) removeSpecifier(specifier)
} else if (existing) { } else if (existing) {
if (existing.source === source && existing.imported === imported) { if (existing.source === source && existing.imported === imported) {
@ -585,17 +628,32 @@ export function compileScript(
} }
} }
// process `defineProps` and `defineEmit` calls
if ( if (
node.type === 'ExpressionStatement' && node.type === 'ExpressionStatement' &&
processDefineOptions(node.expression) (processDefineProps(node.expression) ||
processDefineEmit(node.expression))
) { ) {
s.remove(node.start! + startOffset, node.end! + startOffset) s.remove(node.start! + startOffset, node.end! + startOffset)
} }
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 && processDefineOptions(decl.init)) { if (decl.init) {
optionsExp = scriptSetup.content.slice(decl.id.start!, decl.id.end!) const isDefineProps = processDefineProps(decl.init)
if (isDefineProps) {
propsIdentifier = scriptSetup.content.slice(
decl.id.start!,
decl.id.end!
)
}
const isDefineEmit = processDefineEmit(decl.init)
if (isDefineEmit) {
emitIdentifier = scriptSetup.content.slice(
decl.id.start!,
decl.id.end!
)
}
if (isDefineProps || isDefineEmit)
if (node.declarations.length === 1) { if (node.declarations.length === 1) {
s.remove(node.start! + startOffset, node.end! + startOffset) s.remove(node.start! + startOffset, node.end! + startOffset)
} else { } else {
@ -691,61 +749,17 @@ export function compileScript(
} }
// 4. extract runtime props/emits code from setup context type // 4. extract runtime props/emits code from setup context type
if (optionsType) { if (propsTypeDecl) {
for (const m of optionsType.members) { extractRuntimeProps(propsTypeDecl, typeDeclaredProps, declaredTypes)
if (m.type === 'TSPropertySignature' && m.key.type === 'Identifier') {
const typeNode = m.typeAnnotation!.typeAnnotation
const typeString = scriptSetup.content.slice(
typeNode.start!,
typeNode.end!
)
const key = m.key.name
if (key === 'props') {
propsType = typeString
if (typeNode.type === 'TSTypeLiteral') {
extractRuntimeProps(typeNode, typeDeclaredProps, declaredTypes)
} else {
// TODO be able to trace references
error(`props type must be an object literal type`, typeNode)
}
} else if (key === 'emit') {
emitType = typeString
if (
typeNode.type === 'TSFunctionType' ||
typeNode.type === 'TSUnionType'
) {
extractRuntimeEmits(typeNode, typeDeclaredEmits)
} else {
// TODO be able to trace references
error(`emit type must be a function type`, typeNode)
}
} else if (key === 'attrs') {
attrsType = typeString
} else if (key === 'slots') {
slotsType = typeString
} else {
error(`invalid setup context property: "${key}"`, m.key)
}
}
} }
if (emitTypeDecl) {
extractRuntimeEmits(emitTypeDecl, typeDeclaredEmits)
} }
// 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
if (optionsArg) { checkInvalidScopeReference(propsRuntimeDecl, DEFINE_PROPS)
walkIdentifiers(optionsArg, id => { checkInvalidScopeReference(emitRuntimeDecl, DEFINE_PROPS)
if (setupBindings[id.name]) {
error(
`\`${DEFINE_OPTIONS}()\` in <script setup> cannot reference locally ` +
`declared variables because it will be hoisted outside of the ` +
`setup() function. If your component options requires initialization ` +
`in the module scope, use a separate normal <script> to export ` +
`the options instead.`,
id
)
}
})
}
// 6. remove non-script content // 6. remove non-script content
if (script) { if (script) {
@ -766,31 +780,17 @@ export function compileScript(
s.remove(endOffset, source.length) s.remove(endOffset, source.length)
} }
// 7. finalize setup argument signature. // 7. analyze binding metadata
let args = optionsExp ? `__props, ${optionsExp}` : `__props`
if (optionsExp && optionsType) {
if (slotsType === 'Slots') {
helperImports.add('Slots')
}
args += `: {
props: ${propsType},
emit: ${emitType},
slots: ${slotsType},
attrs: ${attrsType}
}`
}
// 8. analyze binding metadata
if (scriptAst) { if (scriptAst) {
Object.assign(bindingMetadata, analyzeScriptBindings(scriptAst)) Object.assign(bindingMetadata, analyzeScriptBindings(scriptAst))
} }
if (optionsType) { if (propsRuntimeDecl) {
for (const key in typeDeclaredProps) { for (const key of getObjectOrArrayExpressionKeys(propsRuntimeDecl)) {
bindingMetadata[key] = BindingTypes.PROPS bindingMetadata[key] = BindingTypes.PROPS
} }
} }
if (optionsArg) { for (const key in typeDeclaredProps) {
Object.assign(bindingMetadata, analyzeBindingsFromOptions(optionsArg)) bindingMetadata[key] = BindingTypes.PROPS
} }
for (const [key, { isType, source }] of Object.entries(userImports)) { for (const [key, { isType, source }] of Object.entries(userImports)) {
if (isType) continue if (isType) continue
@ -803,7 +803,7 @@ export function compileScript(
bindingMetadata[key] = setupBindings[key] bindingMetadata[key] = setupBindings[key]
} }
// 9. inject `useCssVars` calls // 8. inject `useCssVars` calls
if (cssVars.length) { if (cssVars.length) {
helperImports.add(CSS_VARS_HELPER) helperImports.add(CSS_VARS_HELPER)
helperImports.add('unref') helperImports.add('unref')
@ -818,6 +818,35 @@ export function compileScript(
) )
} }
// 9. finalize setup() argument signature
let args = `__props`
if (propsTypeDecl) {
args += `: ${scriptSetup.content.slice(
propsTypeDecl.start!,
propsTypeDecl.end!
)}`
}
// inject user assignment of props
// we use a default __props so that template expressions referencing props
// can use it directly
if (propsIdentifier) {
s.prependRight(startOffset, `\nconst ${propsIdentifier} = __props`)
}
if (emitIdentifier) {
args +=
emitIdentifier === `emit` ? `, { emit }` : `, { emit: ${emitIdentifier} }`
if (emitTypeDecl) {
args += `: {
emit: (${scriptSetup.content.slice(
emitTypeDecl.start!,
emitTypeDecl.end!
)}),
slots: any,
attrs: any
}`
}
}
// 10. generate return statement // 10. generate return statement
let returned let returned
if (options.inlineTemplate) { if (options.inlineTemplate) {
@ -896,13 +925,19 @@ export function compileScript(
if (hasInlinedSsrRenderFn) { if (hasInlinedSsrRenderFn) {
runtimeOptions += `\n __ssrInlineRender: true,` runtimeOptions += `\n __ssrInlineRender: true,`
} }
if (optionsArg) { if (propsRuntimeDecl) {
runtimeOptions += `\n ${scriptSetup.content runtimeOptions += `\n props: ${scriptSetup.content
.slice(optionsArg.start! + 1, optionsArg.end! - 1) .slice(propsRuntimeDecl.start!, propsRuntimeDecl.end!)
.trim()},` .trim()},`
} else if (optionsType) { } else if (propsTypeDecl) {
runtimeOptions += runtimeOptions += genRuntimeProps(typeDeclaredProps)
genRuntimeProps(typeDeclaredProps) + genRuntimeEmits(typeDeclaredEmits) }
if (emitRuntimeDecl) {
runtimeOptions += `\n emits: ${scriptSetup.content
.slice(emitRuntimeDecl.start!, emitRuntimeDecl.end!)
.trim()},`
} else if (emitTypeDecl) {
runtimeOptions += genRuntimeEmits(typeDeclaredEmits)
} }
if (isTS) { if (isTS) {
// for TS, make sure the exported type is still valid type with // for TS, make sure the exported type is still valid type with
@ -975,13 +1010,16 @@ function walkDeclaration(
const isConst = node.kind === 'const' const isConst = node.kind === 'const'
// export const foo = ... // export const foo = ...
for (const { id, init } of node.declarations) { for (const { id, init } of node.declarations) {
const isUseOptionsCall = !!(isConst && isCallOf(init, DEFINE_OPTIONS)) const isDefineCall = !!(
isConst &&
(isCallOf(init, DEFINE_PROPS) || isCallOf(init, DEFINE_EMIT))
)
if (id.type === 'Identifier') { if (id.type === 'Identifier') {
let bindingType let bindingType
if ( if (
// if a declaration is a const literal, we can mark it so that // if a declaration is a const literal, we can mark it so that
// the generated render fn code doesn't need to unref() it // the generated render fn code doesn't need to unref() it
isUseOptionsCall || isDefineCall ||
(isConst && (isConst &&
canNeverBeRef(init!, userImportAlias['reactive'] || 'reactive')) canNeverBeRef(init!, userImportAlias['reactive'] || 'reactive'))
) { ) {
@ -997,9 +1035,9 @@ function walkDeclaration(
} }
bindings[id.name] = bindingType bindings[id.name] = bindingType
} else if (id.type === 'ObjectPattern') { } else if (id.type === 'ObjectPattern') {
walkObjectPattern(id, bindings, isConst, isUseOptionsCall) walkObjectPattern(id, bindings, isConst, isDefineCall)
} else if (id.type === 'ArrayPattern') { } else if (id.type === 'ArrayPattern') {
walkArrayPattern(id, bindings, isConst, isUseOptionsCall) walkArrayPattern(id, bindings, isConst, isDefineCall)
} }
} }
} else if ( } else if (
@ -1016,7 +1054,7 @@ function walkObjectPattern(
node: ObjectPattern, node: ObjectPattern,
bindings: Record<string, BindingTypes>, bindings: Record<string, BindingTypes>,
isConst: boolean, isConst: boolean,
isUseOptionsCall = false isDefineCall = false
) { ) {
for (const p of node.properties) { for (const p of node.properties) {
if (p.type === 'ObjectProperty') { if (p.type === 'ObjectProperty') {
@ -1024,13 +1062,13 @@ function walkObjectPattern(
if (p.key.type === 'Identifier') { if (p.key.type === 'Identifier') {
if (p.key === p.value) { if (p.key === p.value) {
// const { x } = ... // const { x } = ...
bindings[p.key.name] = isUseOptionsCall bindings[p.key.name] = isDefineCall
? BindingTypes.SETUP_CONST ? BindingTypes.SETUP_CONST
: isConst : isConst
? BindingTypes.SETUP_MAYBE_REF ? BindingTypes.SETUP_MAYBE_REF
: BindingTypes.SETUP_LET : BindingTypes.SETUP_LET
} else { } else {
walkPattern(p.value, bindings, isConst, isUseOptionsCall) walkPattern(p.value, bindings, isConst, isDefineCall)
} }
} }
} else { } else {
@ -1047,10 +1085,10 @@ function walkArrayPattern(
node: ArrayPattern, node: ArrayPattern,
bindings: Record<string, BindingTypes>, bindings: Record<string, BindingTypes>,
isConst: boolean, isConst: boolean,
isUseOptionsCall = false isDefineCall = false
) { ) {
for (const e of node.elements) { for (const e of node.elements) {
e && walkPattern(e, bindings, isConst, isUseOptionsCall) e && walkPattern(e, bindings, isConst, isDefineCall)
} }
} }
@ -1058,10 +1096,10 @@ function walkPattern(
node: Node, node: Node,
bindings: Record<string, BindingTypes>, bindings: Record<string, BindingTypes>,
isConst: boolean, isConst: boolean,
isUseOptionsCall = false isDefineCall = false
) { ) {
if (node.type === 'Identifier') { if (node.type === 'Identifier') {
bindings[node.name] = isUseOptionsCall bindings[node.name] = isDefineCall
? BindingTypes.SETUP_CONST ? BindingTypes.SETUP_CONST
: isConst : isConst
? BindingTypes.SETUP_MAYBE_REF ? BindingTypes.SETUP_MAYBE_REF
@ -1077,7 +1115,7 @@ function walkPattern(
walkArrayPattern(node, bindings, isConst) walkArrayPattern(node, bindings, isConst)
} else if (node.type === 'AssignmentPattern') { } else if (node.type === 'AssignmentPattern') {
if (node.left.type === 'Identifier') { if (node.left.type === 'Identifier') {
bindings[node.left.name] = isUseOptionsCall bindings[node.left.name] = isDefineCall
? BindingTypes.SETUP_CONST ? BindingTypes.SETUP_CONST
: isConst : isConst
? BindingTypes.SETUP_MAYBE_REF ? BindingTypes.SETUP_MAYBE_REF
@ -1418,43 +1456,6 @@ function isFunction(node: Node): node is FunctionNode {
return /Function(?:Expression|Declaration)$|Method$/.test(node.type) return /Function(?:Expression|Declaration)$|Method$/.test(node.type)
} }
function getObjectExpressionKeys(node: ObjectExpression): string[] {
const keys = []
for (const prop of node.properties) {
if (
(prop.type === 'ObjectProperty' || prop.type === 'ObjectMethod') &&
!prop.computed
) {
if (prop.key.type === 'Identifier') {
keys.push(prop.key.name)
} else if (prop.key.type === 'StringLiteral') {
keys.push(prop.key.value)
}
}
}
return keys
}
function getArrayExpressionKeys(node: ArrayExpression): string[] {
const keys = []
for (const element of node.elements) {
if (element && element.type === 'StringLiteral') {
keys.push(element.value)
}
}
return keys
}
function getObjectOrArrayExpressionKeys(property: ObjectProperty): string[] {
if (property.value.type === 'ArrayExpression') {
return getArrayExpressionKeys(property.value)
}
if (property.value.type === 'ObjectExpression') {
return getObjectExpressionKeys(property.value)
}
return []
}
function isCallOf(node: Node | null, name: string): node is CallExpression { function isCallOf(node: Node | null, name: string): node is CallExpression {
return !!( return !!(
node && node &&
@ -1542,7 +1543,7 @@ function analyzeBindingsFromOptions(node: ObjectExpression): BindingMetadata {
if (property.key.name === 'props') { if (property.key.name === 'props') {
// props: ['foo'] // props: ['foo']
// props: { foo: ... } // props: { foo: ... }
for (const key of getObjectOrArrayExpressionKeys(property)) { for (const key of getObjectOrArrayExpressionKeys(property.value)) {
bindings[key] = BindingTypes.PROPS bindings[key] = BindingTypes.PROPS
} }
} }
@ -1551,7 +1552,7 @@ function analyzeBindingsFromOptions(node: ObjectExpression): BindingMetadata {
else if (property.key.name === 'inject') { else if (property.key.name === 'inject') {
// inject: ['foo'] // inject: ['foo']
// inject: { foo: {} } // inject: { foo: {} }
for (const key of getObjectOrArrayExpressionKeys(property)) { for (const key of getObjectOrArrayExpressionKeys(property.value)) {
bindings[key] = BindingTypes.OPTIONS bindings[key] = BindingTypes.OPTIONS
} }
} }
@ -1599,3 +1600,40 @@ function analyzeBindingsFromOptions(node: ObjectExpression): BindingMetadata {
return bindings return bindings
} }
function getObjectExpressionKeys(node: ObjectExpression): string[] {
const keys = []
for (const prop of node.properties) {
if (
(prop.type === 'ObjectProperty' || prop.type === 'ObjectMethod') &&
!prop.computed
) {
if (prop.key.type === 'Identifier') {
keys.push(prop.key.name)
} else if (prop.key.type === 'StringLiteral') {
keys.push(prop.key.value)
}
}
}
return keys
}
function getArrayExpressionKeys(node: ArrayExpression): string[] {
const keys = []
for (const element of node.elements) {
if (element && element.type === 'StringLiteral') {
keys.push(element.value)
}
}
return keys
}
function getObjectOrArrayExpressionKeys(value: Node): string[] {
if (value.type === 'ArrayExpression') {
return getArrayExpressionKeys(value)
}
if (value.type === 'ObjectExpression') {
return getObjectExpressionKeys(value)
}
return []
}

View File

@ -1,91 +0,0 @@
import { EmitFn, EmitsOptions } from './componentEmits'
import { ComponentObjectPropsOptions, ExtractPropTypes } from './componentProps'
import { Slots } from './componentSlots'
import { Directive } from './directives'
import { warn } from './warning'
interface DefaultContext {
props: {}
attrs: Record<string, unknown>
emit: (...args: any[]) => void
slots: Slots
}
interface InferredContext<P, E> {
props: Readonly<P>
attrs: Record<string, unknown>
emit: EmitFn<E>
slots: Slots
}
type InferContext<T extends Partial<DefaultContext>, P, E> = {
[K in keyof DefaultContext]: T[K] extends {} ? T[K] : InferredContext<P, E>[K]
}
/**
* This is a subset of full options that are still useful in the context of
* <script setup>. Technically, other options can be used too, but are
* discouraged - if using TypeScript, we nudge users away from doing so by
* disallowing them in types.
*/
interface Options<E extends EmitsOptions, EE extends string> {
emits?: E | EE[]
name?: string
inhertiAttrs?: boolean
directives?: Record<string, Directive>
}
/**
* Compile-time-only helper used for declaring options and retrieving props
* and the setup context inside `<script setup>`.
* This is stripped away in the compiled code and should never be actually
* called at runtime.
*/
// overload 1: no props
export function defineOptions<
T extends Partial<DefaultContext> = {},
E extends EmitsOptions = EmitsOptions,
EE extends string = string
>(
options?: Options<E, EE> & {
props?: undefined
}
): InferContext<T, {}, E>
// overload 2: object props
export function defineOptions<
T extends Partial<DefaultContext> = {},
E extends EmitsOptions = EmitsOptions,
EE extends string = string,
PP extends string = string,
P = Readonly<{ [key in PP]?: any }>
>(
options?: Options<E, EE> & {
props?: PP[]
}
): InferContext<T, P, E>
// overload 3: object props
export function defineOptions<
T extends Partial<DefaultContext> = {},
E extends EmitsOptions = EmitsOptions,
EE extends string = string,
PP extends ComponentObjectPropsOptions = ComponentObjectPropsOptions,
P = ExtractPropTypes<PP>
>(
options?: Options<E, EE> & {
props?: PP
}
): InferContext<T, P, E>
// implementation
export function defineOptions() {
if (__DEV__) {
warn(
`defineContext() is a compiler-hint helper that is only usable inside ` +
`<script setup> of a single file component. It will be compiled away ` +
`and should not be used in final distributed code.`
)
}
return 0 as any
}

View File

@ -0,0 +1,60 @@
import { shallowReadonly } from '@vue/reactivity'
import { getCurrentInstance, SetupContext } from './component'
import { EmitFn, EmitsOptions } from './componentEmits'
import { ComponentObjectPropsOptions, ExtractPropTypes } from './componentProps'
import { warn } from './warning'
/**
* 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.
*/
// overload 1: string props
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): Readonly<TypeProps extends undefined ? InferredProps : TypeProps>
// implementation
export function defineProps(props?: any) {
if (__DEV__ && props) {
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.`
)
}
return __DEV__
? shallowReadonly(getCurrentInstance()!.props)
: getCurrentInstance()!.props
}
export function defineEmit<
TypeEmit = undefined,
E extends EmitsOptions = EmitsOptions,
EE extends string = string,
InferredEmit = EmitFn<E>
>(emitOptions?: E | EE[]): TypeEmit extends undefined ? InferredEmit : TypeEmit
// implementation
export function defineEmit(emitOptions?: any) {
if (__DEV__ && emitOptions) {
warn(
`defineEmit() 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 getCurrentInstance()!.emit
}
export function useContext(): SetupContext {
return getCurrentInstance()!.setupContext!
}

View File

@ -105,7 +105,7 @@ export interface ComponentInternalOptions {
export interface FunctionalComponent<P = {}, E extends EmitsOptions = {}> export interface FunctionalComponent<P = {}, E extends EmitsOptions = {}>
extends ComponentInternalOptions { extends ComponentInternalOptions {
// use of any here is intentional so it can be a valid JSX Element constructor // use of any here is intentional so it can be a valid JSX Element constructor
(props: P, ctx: Omit<SetupContext<E, P>, 'expose'>): any (props: P, ctx: Omit<SetupContext<E>, 'expose'>): any
props?: ComponentPropsOptions<P> props?: ComponentPropsOptions<P>
emits?: E | (keyof E)[] emits?: E | (keyof E)[]
inheritAttrs?: boolean inheritAttrs?: boolean
@ -167,8 +167,7 @@ export const enum LifecycleHooks {
ERROR_CAPTURED = 'ec' ERROR_CAPTURED = 'ec'
} }
export interface SetupContext<E = EmitsOptions, P = Data> { export interface SetupContext<E = EmitsOptions> {
props: P
attrs: Data attrs: Data
slots: Slots slots: Slots
emit: EmitFn<E> emit: EmitFn<E>
@ -775,7 +774,6 @@ function createSetupContext(instance: ComponentInternalInstance): SetupContext {
}) })
} else { } else {
return { return {
props: instance.props,
attrs: instance.attrs, attrs: instance.attrs,
slots: instance.slots, slots: instance.slots,
emit: instance.emit, emit: instance.emit,

View File

@ -98,7 +98,7 @@ export interface ComponentOptionsBase<
setup?: ( setup?: (
this: void, this: void,
props: Props, props: Props,
ctx: SetupContext<E, Props> ctx: SetupContext<E>
) => Promise<RawBindings> | RawBindings | RenderFunction | void ) => Promise<RawBindings> | RawBindings | RenderFunction | void
name?: string name?: string
template?: string | object // can be a direct DOM node template?: string | object // can be a direct DOM node

View File

@ -95,7 +95,6 @@ export function renderComponentRoot(
props, props,
__DEV__ __DEV__
? { ? {
props,
get attrs() { get attrs() {
markAttrsAccessed() markAttrsAccessed()
return attrs return attrs
@ -103,7 +102,7 @@ export function renderComponentRoot(
slots, slots,
emit emit
} }
: { props, attrs, slots, emit } : { attrs, slots, emit }
) )
: render(props, null as any /* we know it doesn't need it */) : render(props, null as any /* we know it doesn't need it */)
) )

View File

@ -43,7 +43,7 @@ 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'
export { defineOptions } from './apiDefineOptions' export { defineProps, defineEmit, useContext } from './apiSetupHelpers'
// Advanced API ---------------------------------------------------------------- // Advanced API ----------------------------------------------------------------

View File

@ -1,96 +0,0 @@
import { expectType, defineOptions, Slots, describe } from './index'
describe('no args', () => {
const { props, attrs, emit, slots } = defineOptions()
expectType<{}>(props)
expectType<Record<string, unknown>>(attrs)
expectType<(...args: any[]) => void>(emit)
expectType<Slots>(slots)
// @ts-expect-error
props.foo
// should be able to emit anything
emit('foo')
emit('bar')
})
describe('with type arg', () => {
const { props, attrs, emit, slots } = defineOptions<{
props: {
foo: string
}
emit: (e: 'change') => void
}>()
// explicitly declared type should be refined
expectType<string>(props.foo)
// @ts-expect-error
props.bar
emit('change')
// @ts-expect-error
emit()
// @ts-expect-error
emit('bar')
// non explicitly declared type should fallback to default type
expectType<Record<string, unknown>>(attrs)
expectType<Slots>(slots)
})
// with runtime arg
describe('with runtime arg (array syntax)', () => {
const { props, emit } = defineOptions({
props: ['foo', 'bar'],
emits: ['foo', 'bar']
})
expectType<{
foo?: any
bar?: any
}>(props)
// @ts-expect-error
props.baz
emit('foo')
emit('bar', 123)
// @ts-expect-error
emit('baz')
})
describe('with runtime arg (object syntax)', () => {
const { props, emit } = defineOptions({
props: {
foo: String,
bar: {
type: Number,
default: 1
},
baz: {
type: Array,
required: true
}
},
emits: {
foo: () => {},
bar: null
}
})
expectType<{
foo?: string
bar: number
baz: unknown[]
}>(props)
props.foo && props.foo + 'bar'
props.bar + 1
// @ts-expect-error should be readonly
props.bar++
props.baz.push(1)
emit('foo')
emit('bar')
// @ts-expect-error
emit('baz')
})

View File

@ -0,0 +1,89 @@
import {
expectType,
defineProps,
defineEmit,
useContext,
Slots,
describe
} from './index'
describe('defineProps w/ type declaration', () => {
// type declaration
const props = defineProps<{
foo: string
}>()
// explicitly declared type should be refined
expectType<string>(props.foo)
// @ts-expect-error
props.bar
})
describe('defineProps w/ runtime declaration', () => {
// runtime declaration
const props = defineProps({
foo: String,
bar: {
type: Number,
default: 1
},
baz: {
type: Array,
required: true
}
})
expectType<{
foo?: string
bar: number
baz: unknown[]
}>(props)
props.foo && props.foo + 'bar'
props.bar + 1
// @ts-expect-error should be readonly
props.bar++
props.baz.push(1)
const props2 = defineProps(['foo', 'bar'])
props2.foo + props2.bar
// @ts-expect-error
props2.baz
})
describe('defineEmit w/ type declaration', () => {
const emit = defineEmit<(e: 'change') => void>()
emit('change')
// @ts-expect-error
emit()
// @ts-expect-error
emit('bar')
})
describe('defineEmit w/ runtime declaration', () => {
const emit = defineEmit({
foo: () => {},
bar: null
})
emit('foo')
emit('bar', 123)
// @ts-expect-error
emit('baz')
const emit2 = defineEmit(['foo', 'bar'])
emit2('foo')
emit2('bar', 123)
// @ts-expect-error
emit2('baz')
})
describe('useContext', () => {
const { attrs, emit, slots } = useContext()
expectType<Record<string, unknown>>(attrs)
expectType<(...args: any[]) => void>(emit)
expectType<Slots>(slots)
// @ts-expect-error
props.foo
// should be able to emit anything
emit('foo')
emit('bar')
})