export const n = 1"
`;
-exports[`SFC compile <script setup> defineOptions() 1`] = `
+exports[`SFC compile <script setup> defineEmit() 1`] = `
"export default {
expose: [],
- props: {
- foo: String
- },
- emit: ['a', 'b'],
- setup(__props, { props, emit }) {
+ emits: ['foo', 'bar'],
+ setup(__props, { emit: myEmit }) {
+
+return { myEmit }
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> defineProps() 1`] = `
+"export default {
+ expose: [],
+ props: {
+ foo: String
+},
+ setup(__props) {
+
+const props = __props
const bar = 1
-return { props, emit, bar }
+return { props, bar }
}
}"
`;
-exports[`SFC compile <script setup> errors should allow defineOptions() referencing imported binding 1`] = `
+exports[`SFC compile <script setup> errors should allow defineProps/Emit() referencing imported binding 1`] = `
"import { bar } from './bar'
-
+
export default {
expose: [],
props: {
- foo: {
- default: () => bar
- }
- },
+ foo: {
+ default: () => bar
+ }
+ },
+ emits: {
+ foo: () => bar > 1
+ },
setup(__props) {
-
+
+
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 {
expose: [],
props: {
- foo: {
- default: bar => bar + 1
- }
- },
+ foo: {
+ default: bar => bar + 1
+ }
+ },
+ emits: {
+ foo: bar => bar > 1
+ },
setup(__props) {
const bar = 1
+
return { bar }
}
}"
`;
-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'
-
-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({
expose: [],
emits: [\\"foo\\", \\"bar\\", \\"baz\\"] as unknown as undefined,
setup(__props, { emit }: {
- props: {},
- emit: ((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void),
- slots: Slots,
- attrs: Record<string, any>
-}) {
+ emit: (((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void)),
+ slots: any,
+ attrs: any
+ }) {
})"
`;
-exports[`SFC compile <script setup> with TypeScript defineOptions w/ type / extract emits 1`] = `
-"import { Slots as _Slots, defineComponent as _defineComponent } from 'vue'
+exports[`SFC compile <script setup> with TypeScript defineEmit w/ type 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
export default _defineComponent({
expose: [],
emits: [\\"foo\\", \\"bar\\"] as unknown as undefined,
setup(__props, { emit }: {
- props: {},
- emit: (e: 'foo' | 'bar') => void,
- slots: Slots,
- attrs: Record<string, any>
-}) {
+ emit: ((e: 'foo' | 'bar') => void),
+ slots: any,
+ attrs: any
+ }) {
})"
`;
-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'
interface Test {}
literalUnionMixed: { type: [String, Number, Boolean], required: true },
intersection: { type: Object, required: true }
} 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 & {}
+ }) {
})"
`;
+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`] = `
"import { defineComponent as _defineComponent } from 'vue'
export interface Foo {}
export default {
expose: [],
props: {
- foo: String
- },
+ foo: String
+ },
setup(__props) {
_useCssVars(_ctx => ({
expect(content).toMatch('return { a, b, c, d, x }')
})
- test('defineOptions()', () => {
+ test('defineProps()', () => {
const { content, bindings } = compile(`
<script setup>
-import { defineOptions } from 'vue'
-const { props, emit } = defineOptions({
- props: {
- foo: String
- },
- emit: ['a', 'b']
+import { defineProps } from 'vue'
+const props = defineProps({
+ foo: String
})
-
const bar = 1
</script>
`)
expect(bindings).toStrictEqual({
foo: BindingTypes.PROPS,
bar: BindingTypes.SETUP_CONST,
- props: BindingTypes.SETUP_CONST,
- emit: BindingTypes.SETUP_CONST
+ props: BindingTypes.SETUP_CONST
})
// should remove defineOptions import and call
- expect(content).not.toMatch('defineOptions')
+ expect(content).not.toMatch('defineProps')
// 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
expect(content).toMatch(`export default {
expose: [],
props: {
- foo: String
- },
- emit: ['a', 'b'],`)
+ foo: String
+},`)
+ })
+
+ 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', () => {
// function, const, component import
const { content } = compile(
`<script setup>
- import { ref, defineOptions } from 'vue'
+ import { ref } from 'vue'
import Foo from './Foo.vue'
import other from './util'
const count = ref(0)
assertCode(content)
})
- test('defineOptions w/ runtime options', () => {
+ test('defineProps/Emit w/ runtime options', () => {
const { content } = compile(`
<script setup lang="ts">
-import { defineOptions } from 'vue'
-const { props, emit } = defineOptions({
- props: { foo: String },
- emits: ['a', 'b']
-})
+import { defineProps, defineEmit } from 'vue'
+const props = defineProps({ foo: String })
+const emit = defineEmit(['a', 'b'])
</script>
`)
assertCode(content)
expose: [],
props: { foo: String },
emits: ['a', 'b'],
- setup(__props, { props, emit }) {`)
+ setup(__props, { emit }) {`)
})
- test('defineOptions w/ type / extract props', () => {
+ test('defineProps w/ type', () => {
const { content, bindings } = compile(`
<script setup lang="ts">
- import { defineOptions } from 'vue'
+ import { defineProps } from 'vue'
interface Test {}
type Alias = number[]
- defineOptions<{
- 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 & {}
- }
+ defineProps<{
+ 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 & {}
}>()
</script>`)
assertCode(content)
})
})
- test('defineOptions w/ type / extract emits', () => {
+ test('defineEmit w/ type', () => {
const { content } = compile(`
<script setup lang="ts">
- import { defineOptions } from 'vue'
- const { emit } = defineOptions<{
- emit: (e: 'foo' | 'bar') => void
- }>()
+ import { defineEmit } from 'vue'
+ const emit = defineEmit<(e: 'foo' | 'bar') => void>()
</script>
`)
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`)
})
- 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(`
<script setup lang="ts">
- import { defineOptions } from 'vue'
- const { emit } = defineOptions<{
- emit: ((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void)
- }>()
+ import { defineEmit } from 'vue'
+ const emit = defineEmit<${type}>()
</script>
`)
assertCode(content)
- expect(content).toMatch(
- `props: {},\n emit: ((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void),`
- )
+ expect(content).toMatch(`emit: (${type}),`)
expect(content).toMatch(
`emits: ["foo", "bar", "baz"] as unknown as undefined`
)
).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(() => {
+ compile(`<script setup lang="ts">
+ import { defineProps } from 'vue'
+ defineProps<{}>({})
+ </script>`)
+ }).toThrow(`cannot accept both type and non-type arguments`)
+
expect(() => {
compile(`<script setup lang="ts">
- import { defineOptions } from 'vue'
- defineOptions<{}>({})
+ import { defineEmit } from 'vue'
+ defineEmit<{}>({})
</script>`)
}).toThrow(`cannot accept both type and non-type arguments`)
})
- test('defineOptions() referencing local var', () => {
+ test('defineProps/Emit() referencing local var', () => {
expect(() =>
compile(`<script setup>
- import { defineOptions } from 'vue'
+ import { defineProps } from 'vue'
const bar = 1
- defineOptions({
- props: {
- foo: {
- default: () => bar
- }
+ defineProps({
+ foo: {
+ default: () => bar
}
})
</script>`)
).toThrow(`cannot reference locally declared variables`)
+
+ expect(() =>
+ compile(`<script setup>
+ import { defineEmit } from 'vue'
+ const bar = 'hello'
+ defineEmit([bar])
+ </script>`)
+ ).toThrow(`cannot reference locally declared variables`)
})
- test('defineOptions() referencing ref declarations', () => {
+ test('defineProps/Emit() referencing ref declarations', () => {
+ expect(() =>
+ compile(`<script setup>
+ import { defineProps } from 'vue'
+ ref: bar = 1
+ defineProps({
+ bar
+ })
+ </script>`)
+ ).toThrow(`cannot reference locally declared variables`)
+
expect(() =>
compile(`<script setup>
- import { defineOptions } from 'vue'
+ import { defineEmit } from 'vue'
ref: bar = 1
- defineOptions({
- props: { bar }
+ defineEmit({
+ bar
})
</script>`)
).toThrow(`cannot reference locally declared variables`)
})
- test('should allow defineOptions() referencing scope var', () => {
+ test('should allow defineProps/Emit() referencing scope var', () => {
assertCode(
compile(`<script setup>
- import { defineOptions } from 'vue'
+ import { defineProps, defineEmit } from 'vue'
const bar = 1
- defineOptions({
- props: {
- foo: {
- default: bar => bar + 1
- }
+ defineProps({
+ foo: {
+ default: bar => bar + 1
}
})
+ defineEmit({
+ foo: bar => bar > 1
+ })
</script>`).content
)
})
- test('should allow defineOptions() referencing imported binding', () => {
+ test('should allow defineProps/Emit() referencing imported binding', () => {
assertCode(
compile(`<script setup>
- import { defineOptions } from 'vue'
- import { bar } from './bar'
- defineOptions({
- props: {
- foo: {
- default: () => bar
- }
- }
- })
+ import { defineProps, defineEmit } from 'vue'
+ import { bar } from './bar'
+ defineProps({
+ foo: {
+ default: () => bar
+ }
+ })
+ defineEmit({
+ foo: () => bar > 1
+ })
</script>`).content
)
})
it('works for script setup', () => {
const { bindings } = compile(`
<script setup>
- import { defineOptions, ref as r } from 'vue'
- defineOptions({
- props: {
- foo: String,
- }
+ import { defineProps, ref as r } from 'vue'
+ defineProps({
+ foo: String
})
const a = r(1)
test('w/ <script setup> binding analysis', () => {
const { content } = compileSFCScript(
`<script setup>
- import { defineOptions, ref } from 'vue'
+ import { defineProps, ref } from 'vue'
const color = 'red'
const size = ref('10px')
- defineOptions({
- props: {
- foo: String
- }
+ defineProps({
+ foo: String
})
</script>\n` +
`<style>
import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate'
import { warnExperimental, warnOnce } from './warn'
-const DEFINE_OPTIONS = 'defineOptions'
+const DEFINE_PROPS = 'defineProps'
+const DEFINE_EMIT = 'defineEmit'
export interface SFCScriptCompileOptions {
/**
const refIdentifiers: Set<Identifier> = new Set()
const enableRefSugar = options.refSugar !== false
let defaultExport: Node | undefined
- let hasOptionsCall = false
- let optionsExp: string | undefined
- let optionsArg: ObjectExpression | undefined
- let optionsType: TSTypeLiteral | undefined
+ let hasDefinePropsCall = false
+ let hasDefineEmitCall = false
+ let propsRuntimeDecl: Node | 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 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
const typeDeclaredProps: Record<string, PropTypeData> = {}
const typeDeclaredEmits: Set<string> = new Set()
}
}
- function processDefineOptions(node: Node): boolean {
- if (isCallOf(node, DEFINE_OPTIONS)) {
- if (hasOptionsCall) {
- error(`duplicate ${DEFINE_OPTIONS}() call`, node)
+ function processDefineProps(node: Node): boolean {
+ if (isCallOf(node, DEFINE_PROPS)) {
+ if (hasDefinePropsCall) {
+ error(`duplicate ${DEFINE_PROPS}() call`, node)
}
- hasOptionsCall = true
- const optsArg = node.arguments[0]
- if (optsArg) {
- if (optsArg.type === 'ObjectExpression') {
- optionsArg = optsArg
+ 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(
- `${DEFINE_OPTIONS}() argument must be an object literal.`,
- optsArg
+ `type argument passed to ${DEFINE_PROPS}() must be a literal type.`,
+ typeArg
)
}
}
- // context call has type parameters - infer runtime types from it
+ return true
+ }
+ 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 (optionsArg) {
+ if (emitRuntimeDecl) {
error(
- `${DEFINE_OPTIONS}() cannot accept both type and non-type arguments ` +
+ `${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 === 'TSTypeLiteral') {
- optionsType = typeArg
+ if (
+ typeArg.type === 'TSFunctionType' ||
+ typeArg.type === 'TSUnionType'
+ ) {
+ emitTypeDecl = typeArg
} else {
error(
- `type argument passed to ${DEFINE_OPTIONS}() must be a literal type.`,
+ `type argument passed to ${DEFINE_EMIT}() must be a function type ` +
+ `or a union of function types.`,
typeArg
)
}
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) {
if (exp.type === 'AssignmentExpression') {
const { left, right } = exp
specifier.imported.name
const source = node.source.value
const existing = userImports[local]
- if (source === 'vue' && imported === DEFINE_OPTIONS) {
+ if (
+ source === 'vue' &&
+ (imported === DEFINE_PROPS || imported === DEFINE_EMIT)
+ ) {
removeSpecifier(specifier)
} else if (existing) {
if (existing.source === source && existing.imported === imported) {
}
}
+ // process `defineProps` and `defineEmit` calls
if (
node.type === 'ExpressionStatement' &&
- processDefineOptions(node.expression)
+ (processDefineProps(node.expression) ||
+ processDefineEmit(node.expression))
) {
s.remove(node.start! + startOffset, node.end! + startOffset)
}
-
if (node.type === 'VariableDeclaration' && !node.declare) {
for (const decl of node.declarations) {
- if (decl.init && processDefineOptions(decl.init)) {
- optionsExp = scriptSetup.content.slice(decl.id.start!, decl.id.end!)
- if (node.declarations.length === 1) {
- s.remove(node.start! + startOffset, node.end! + startOffset)
- } else {
- s.remove(decl.start! + startOffset, decl.end! + startOffset)
+ if (decl.init) {
+ 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) {
+ s.remove(node.start! + startOffset, node.end! + startOffset)
+ } else {
+ s.remove(decl.start! + startOffset, decl.end! + startOffset)
+ }
}
}
}
}
// 4. extract runtime props/emits code from setup context type
- if (optionsType) {
- for (const m of optionsType.members) {
- 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 (propsTypeDecl) {
+ extractRuntimeProps(propsTypeDecl, typeDeclaredProps, declaredTypes)
+ }
+ if (emitTypeDecl) {
+ extractRuntimeEmits(emitTypeDecl, typeDeclaredEmits)
}
// 5. check useOptions args to make sure it doesn't reference setup scope
// variables
- if (optionsArg) {
- walkIdentifiers(optionsArg, id => {
- 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
- )
- }
- })
- }
+ checkInvalidScopeReference(propsRuntimeDecl, DEFINE_PROPS)
+ checkInvalidScopeReference(emitRuntimeDecl, DEFINE_PROPS)
// 6. remove non-script content
if (script) {
s.remove(endOffset, source.length)
}
- // 7. finalize setup argument signature.
- 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
+ // 7. analyze binding metadata
if (scriptAst) {
Object.assign(bindingMetadata, analyzeScriptBindings(scriptAst))
}
- if (optionsType) {
- for (const key in typeDeclaredProps) {
+ if (propsRuntimeDecl) {
+ for (const key of getObjectOrArrayExpressionKeys(propsRuntimeDecl)) {
bindingMetadata[key] = BindingTypes.PROPS
}
}
- if (optionsArg) {
- Object.assign(bindingMetadata, analyzeBindingsFromOptions(optionsArg))
+ for (const key in typeDeclaredProps) {
+ bindingMetadata[key] = BindingTypes.PROPS
}
for (const [key, { isType, source }] of Object.entries(userImports)) {
if (isType) continue
bindingMetadata[key] = setupBindings[key]
}
- // 9. inject `useCssVars` calls
+ // 8. inject `useCssVars` calls
if (cssVars.length) {
helperImports.add(CSS_VARS_HELPER)
helperImports.add('unref')
)
}
+ // 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
let returned
if (options.inlineTemplate) {
if (hasInlinedSsrRenderFn) {
runtimeOptions += `\n __ssrInlineRender: true,`
}
- if (optionsArg) {
- runtimeOptions += `\n ${scriptSetup.content
- .slice(optionsArg.start! + 1, optionsArg.end! - 1)
+ if (propsRuntimeDecl) {
+ runtimeOptions += `\n props: ${scriptSetup.content
+ .slice(propsRuntimeDecl.start!, propsRuntimeDecl.end!)
+ .trim()},`
+ } else if (propsTypeDecl) {
+ runtimeOptions += genRuntimeProps(typeDeclaredProps)
+ }
+ if (emitRuntimeDecl) {
+ runtimeOptions += `\n emits: ${scriptSetup.content
+ .slice(emitRuntimeDecl.start!, emitRuntimeDecl.end!)
.trim()},`
- } else if (optionsType) {
- runtimeOptions +=
- genRuntimeProps(typeDeclaredProps) + genRuntimeEmits(typeDeclaredEmits)
+ } else if (emitTypeDecl) {
+ runtimeOptions += genRuntimeEmits(typeDeclaredEmits)
}
if (isTS) {
// for TS, make sure the exported type is still valid type with
const isConst = node.kind === 'const'
// export const foo = ...
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') {
let bindingType
if (
// if a declaration is a const literal, we can mark it so that
// the generated render fn code doesn't need to unref() it
- isUseOptionsCall ||
+ isDefineCall ||
(isConst &&
canNeverBeRef(init!, userImportAlias['reactive'] || 'reactive'))
) {
}
bindings[id.name] = bindingType
} else if (id.type === 'ObjectPattern') {
- walkObjectPattern(id, bindings, isConst, isUseOptionsCall)
+ walkObjectPattern(id, bindings, isConst, isDefineCall)
} else if (id.type === 'ArrayPattern') {
- walkArrayPattern(id, bindings, isConst, isUseOptionsCall)
+ walkArrayPattern(id, bindings, isConst, isDefineCall)
}
}
} else if (
node: ObjectPattern,
bindings: Record<string, BindingTypes>,
isConst: boolean,
- isUseOptionsCall = false
+ isDefineCall = false
) {
for (const p of node.properties) {
if (p.type === 'ObjectProperty') {
if (p.key.type === 'Identifier') {
if (p.key === p.value) {
// const { x } = ...
- bindings[p.key.name] = isUseOptionsCall
+ bindings[p.key.name] = isDefineCall
? BindingTypes.SETUP_CONST
: isConst
? BindingTypes.SETUP_MAYBE_REF
: BindingTypes.SETUP_LET
} else {
- walkPattern(p.value, bindings, isConst, isUseOptionsCall)
+ walkPattern(p.value, bindings, isConst, isDefineCall)
}
}
} else {
node: ArrayPattern,
bindings: Record<string, BindingTypes>,
isConst: boolean,
- isUseOptionsCall = false
+ isDefineCall = false
) {
for (const e of node.elements) {
- e && walkPattern(e, bindings, isConst, isUseOptionsCall)
+ e && walkPattern(e, bindings, isConst, isDefineCall)
}
}
node: Node,
bindings: Record<string, BindingTypes>,
isConst: boolean,
- isUseOptionsCall = false
+ isDefineCall = false
) {
if (node.type === 'Identifier') {
- bindings[node.name] = isUseOptionsCall
+ bindings[node.name] = isDefineCall
? BindingTypes.SETUP_CONST
: isConst
? BindingTypes.SETUP_MAYBE_REF
walkArrayPattern(node, bindings, isConst)
} else if (node.type === 'AssignmentPattern') {
if (node.left.type === 'Identifier') {
- bindings[node.left.name] = isUseOptionsCall
+ bindings[node.left.name] = isDefineCall
? BindingTypes.SETUP_CONST
: isConst
? BindingTypes.SETUP_MAYBE_REF
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 {
return !!(
node &&
if (property.key.name === 'props') {
// props: ['foo']
// props: { foo: ... }
- for (const key of getObjectOrArrayExpressionKeys(property)) {
+ for (const key of getObjectOrArrayExpressionKeys(property.value)) {
bindings[key] = BindingTypes.PROPS
}
}
else if (property.key.name === 'inject') {
// inject: ['foo']
// inject: { foo: {} }
- for (const key of getObjectOrArrayExpressionKeys(property)) {
+ for (const key of getObjectOrArrayExpressionKeys(property.value)) {
bindings[key] = BindingTypes.OPTIONS
}
}
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 []
+}
+++ /dev/null
-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
-}
--- /dev/null
+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!
+}
export interface FunctionalComponent<P = {}, E extends EmitsOptions = {}>
extends ComponentInternalOptions {
// 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>
emits?: E | (keyof E)[]
inheritAttrs?: boolean
ERROR_CAPTURED = 'ec'
}
-export interface SetupContext<E = EmitsOptions, P = Data> {
- props: P
+export interface SetupContext<E = EmitsOptions> {
attrs: Data
slots: Slots
emit: EmitFn<E>
})
} else {
return {
- props: instance.props,
attrs: instance.attrs,
slots: instance.slots,
emit: instance.emit,
setup?: (
this: void,
props: Props,
- ctx: SetupContext<E, Props>
+ ctx: SetupContext<E>
) => Promise<RawBindings> | RawBindings | RenderFunction | void
name?: string
template?: string | object // can be a direct DOM node
props,
__DEV__
? {
- props,
get attrs() {
markAttrsAccessed()
return attrs
slots,
emit
}
- : { props, attrs, slots, emit }
+ : { attrs, slots, emit }
)
: render(props, null as any /* we know it doesn't need it */)
)
export { nextTick } from './scheduler'
export { defineComponent } from './apiDefineComponent'
export { defineAsyncComponent } from './apiAsyncComponent'
-export { defineOptions } from './apiDefineOptions'
+export { defineProps, defineEmit, useContext } from './apiSetupHelpers'
// Advanced API ----------------------------------------------------------------
+++ /dev/null
-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')
-})
--- /dev/null
+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')
+})