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"
`;
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 }) {
const bar = 1
return { props, emit, bar }
return { myEmit }
}
}"
`;
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'
export default {
@ -62,17 +75,21 @@ export default {
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: {
@ -80,11 +97,15 @@ exports[`SFC compile <script setup> errors should allow defineOptions() referenc
default: bar => bar + 1
}
},
emits: {
foo: bar => bar > 1
},
setup(__props) {
const bar = 1
return { bar }
}
@ -594,36 +615,17 @@ 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'
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
}) {
@ -634,18 +636,17 @@ return { emit }
})"
`;
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
}) {
@ -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'
interface Test {}
@ -689,7 +690,30 @@ export default _defineComponent({
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 & {}
}) {
@ -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`] = `
"import { defineComponent as _defineComponent } from 'vue'
export interface Foo {}

View File

@ -16,17 +16,13 @@ describe('SFC compile <script setup>', () => {
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: {
import { defineProps } from 'vue'
const props = defineProps({
foo: String
},
emit: ['a', 'b']
})
const bar = 1
</script>
`)
@ -36,21 +32,42 @@ const bar = 1
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'],`)
},`)
})
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', () => {
@ -174,7 +191,7 @@ const bar = 1
// 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)
@ -360,14 +377,12 @@ const bar = 1
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)
@ -375,19 +390,18 @@ const { props, emit } = defineOptions({
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: {
defineProps<{
string: string
number: number
boolean: boolean
@ -410,7 +424,6 @@ const { props, emit } = defineOptions({
literalUnion: 'foo' | 'bar'
literalUnionMixed: 'foo' | 1 | boolean
intersection: Test & {}
}
}>()
</script>`)
assertCode(content)
@ -466,33 +479,28 @@ const { props, emit } = defineOptions({
})
})
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`
)
@ -774,70 +782,95 @@ const { props, emit } = defineOptions({
).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 { defineOptions } from 'vue'
defineOptions<{}>({})
import { defineProps } from 'vue'
defineProps<{}>({})
</script>`)
}).toThrow(`cannot accept both type and non-type arguments`)
expect(() => {
compile(`<script setup lang="ts">
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: {
defineProps({
foo: {
default: () => bar
}
}
})
</script>`)
).toThrow(`cannot reference locally declared variables`)
})
test('defineOptions() referencing ref declarations', () => {
expect(() =>
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
defineOptions({
props: { bar }
defineProps({
bar
})
</script>`)
).toThrow(`cannot reference locally declared variables`)
expect(() =>
compile(`<script setup>
import { defineEmit } from 'vue'
ref: bar = 1
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: {
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 { defineProps, defineEmit } from 'vue'
import { bar } from './bar'
defineOptions({
props: {
defineProps({
foo: {
default: () => bar
}
}
})
defineEmit({
foo: () => bar > 1
})
</script>`).content
)
@ -1063,11 +1096,9 @@ describe('SFC analyze <script> bindings', () => {
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)

View File

@ -20,13 +20,11 @@ describe('CSS vars injection', () => {
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: {
defineProps({
foo: String
}
})
</script>\n` +
`<style>

View File

@ -29,7 +29,8 @@ import { CSS_VARS_HELPER, genCssVarsCode, injectCssVarsCalls } from './cssVars'
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 {
/**
@ -162,17 +163,16 @@ export function compileScript(
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()
@ -236,38 +236,28 @@ export function compileScript(
}
}
function processDefineOptions(node: Node): boolean {
if (isCallOf(node, DEFINE_OPTIONS)) {
if (hasOptionsCall) {
error(`duplicate ${DEFINE_OPTIONS}() 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
)
}
function processDefineProps(node: Node): boolean {
if (isCallOf(node, DEFINE_PROPS)) {
if (hasDefinePropsCall) {
error(`duplicate ${DEFINE_PROPS}() call`, node)
}
hasDefinePropsCall = true
propsRuntimeDecl = node.arguments[0]
// context call has type parameters - infer runtime types from it
if (node.typeParameters) {
if (optionsArg) {
if (propsRuntimeDecl) {
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.`,
node
)
}
const typeArg = node.typeParameters.params[0]
if (typeArg.type === 'TSTypeLiteral') {
optionsType = typeArg
propsTypeDecl = typeArg
} else {
error(
`type argument passed to ${DEFINE_OPTIONS}() must be a literal type.`,
`type argument passed to ${DEFINE_PROPS}() must be a literal type.`,
typeArg
)
}
@ -277,6 +267,56 @@ export function compileScript(
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) {
if (exp.type === 'AssignmentExpression') {
const { left, right } = exp
@ -562,7 +602,10 @@ export function compileScript(
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) {
@ -585,17 +628,32 @@ export function compileScript(
}
}
// 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 (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 {
@ -691,61 +749,17 @@ export function compileScript(
}
// 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) {
@ -766,31 +780,17 @@ export function compileScript(
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
@ -803,7 +803,7 @@ export function compileScript(
bindingMetadata[key] = setupBindings[key]
}
// 9. inject `useCssVars` calls
// 8. inject `useCssVars` calls
if (cssVars.length) {
helperImports.add(CSS_VARS_HELPER)
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
let returned
if (options.inlineTemplate) {
@ -896,13 +925,19 @@ export function compileScript(
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 (optionsType) {
runtimeOptions +=
genRuntimeProps(typeDeclaredProps) + genRuntimeEmits(typeDeclaredEmits)
} else if (propsTypeDecl) {
runtimeOptions += genRuntimeProps(typeDeclaredProps)
}
if (emitRuntimeDecl) {
runtimeOptions += `\n emits: ${scriptSetup.content
.slice(emitRuntimeDecl.start!, emitRuntimeDecl.end!)
.trim()},`
} else if (emitTypeDecl) {
runtimeOptions += genRuntimeEmits(typeDeclaredEmits)
}
if (isTS) {
// for TS, make sure the exported type is still valid type with
@ -975,13 +1010,16 @@ function walkDeclaration(
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'))
) {
@ -997,9 +1035,9 @@ function walkDeclaration(
}
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 (
@ -1016,7 +1054,7 @@ function walkObjectPattern(
node: ObjectPattern,
bindings: Record<string, BindingTypes>,
isConst: boolean,
isUseOptionsCall = false
isDefineCall = false
) {
for (const p of node.properties) {
if (p.type === 'ObjectProperty') {
@ -1024,13 +1062,13 @@ function walkObjectPattern(
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 {
@ -1047,10 +1085,10 @@ function walkArrayPattern(
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)
}
}
@ -1058,10 +1096,10 @@ function walkPattern(
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
@ -1077,7 +1115,7 @@ function walkPattern(
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
@ -1418,43 +1456,6 @@ function isFunction(node: Node): node is FunctionNode {
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 &&
@ -1542,7 +1543,7 @@ function analyzeBindingsFromOptions(node: ObjectExpression): BindingMetadata {
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
}
}
@ -1551,7 +1552,7 @@ function analyzeBindingsFromOptions(node: ObjectExpression): BindingMetadata {
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
}
}
@ -1599,3 +1600,40 @@ function analyzeBindingsFromOptions(node: ObjectExpression): BindingMetadata {
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 = {}>
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
@ -167,8 +167,7 @@ export const enum LifecycleHooks {
ERROR_CAPTURED = 'ec'
}
export interface SetupContext<E = EmitsOptions, P = Data> {
props: P
export interface SetupContext<E = EmitsOptions> {
attrs: Data
slots: Slots
emit: EmitFn<E>
@ -775,7 +774,6 @@ function createSetupContext(instance: ComponentInternalInstance): SetupContext {
})
} else {
return {
props: instance.props,
attrs: instance.attrs,
slots: instance.slots,
emit: instance.emit,

View File

@ -98,7 +98,7 @@ export interface ComponentOptionsBase<
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

View File

@ -95,7 +95,6 @@ export function renderComponentRoot(
props,
__DEV__
? {
props,
get attrs() {
markAttrsAccessed()
return attrs
@ -103,7 +102,7 @@ export function renderComponentRoot(
slots,
emit
}
: { props, attrs, slots, emit }
: { attrs, slots, emit }
)
: 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 { defineComponent } from './apiDefineComponent'
export { defineAsyncComponent } from './apiAsyncComponent'
export { defineOptions } from './apiDefineOptions'
export { defineProps, defineEmit, useContext } from './apiSetupHelpers'
// 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')
})