Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sfc): add defineEmits and deprecate defineEmit #3725

Merged
merged 1 commit into from Jun 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -54,7 +54,7 @@ return { a }
}"
`;

exports[`SFC compile <script setup> defineEmit() 1`] = `
exports[`SFC compile <script setup> defineEmits() 1`] = `
"export default {
expose: [],
emits: ['foo', 'bar'],
Expand Down Expand Up @@ -711,7 +711,7 @@ return { a, b, c, d, x }
}"
`;

exports[`SFC compile <script setup> with TypeScript defineEmit w/ type (type literal w/ call signatures) 1`] = `
exports[`SFC compile <script setup> with TypeScript defineEmits w/ type (type literal w/ call signatures) 1`] = `
"import { defineComponent as _defineComponent } from 'vue'


Expand All @@ -732,7 +732,7 @@ return { emit }
})"
`;

exports[`SFC compile <script setup> with TypeScript defineEmit w/ type 1`] = `
exports[`SFC compile <script setup> with TypeScript defineEmits w/ type 1`] = `
"import { defineComponent as _defineComponent } from 'vue'


Expand Down
71 changes: 46 additions & 25 deletions packages/compiler-sfc/__tests__/compileScript.spec.ts
Expand Up @@ -49,7 +49,7 @@ const bar = 1
},`)
})

test('defineEmit()', () => {
test('defineEmit() (deprecated)', () => {
const { content, bindings } = compile(`
<script setup>
import { defineEmit } from 'vue'
Expand All @@ -61,7 +61,28 @@ const myEmit = defineEmit(['foo', 'bar'])
myEmit: BindingTypes.SETUP_CONST
})
// should remove defineOptions import and call
expect(content).not.toMatch('defineEmit')
expect(content).not.toMatch(/defineEmits?/)
// 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'],`)
})

test('defineEmits()', () => {
const { content, bindings } = compile(`
<script setup>
import { defineEmits } from 'vue'
const myEmit = defineEmits(['foo', 'bar'])
</script>
`)
assertCode(content)
expect(bindings).toStrictEqual({
myEmit: BindingTypes.SETUP_CONST
})
// should remove defineOptions import and call
expect(content).not.toMatch('defineEmits')
// should generate correct setup signature
expect(content).toMatch(`setup(__props, { emit: myEmit }) {`)
// should include context options in default export
Expand Down Expand Up @@ -145,9 +166,9 @@ const myEmit = defineEmit(['foo', 'bar'])
test('should allow defineProps/Emit at the start of imports', () => {
assertCode(
compile(`<script setup>
import { defineProps, defineEmit, ref } from 'vue'
import { defineProps, defineEmits, ref } from 'vue'
defineProps(['foo'])
defineEmit(['bar'])
defineEmits(['bar'])
const r = ref(0)
</script>`).content
)
Expand Down Expand Up @@ -445,9 +466,9 @@ const myEmit = defineEmit(['foo', 'bar'])
test('defineProps/Emit w/ runtime options', () => {
const { content } = compile(`
<script setup lang="ts">
import { defineProps, defineEmit } from 'vue'
import { defineProps, defineEmits } from 'vue'
const props = defineProps({ foo: String })
const emit = defineEmit(['a', 'b'])
const emit = defineEmits(['a', 'b'])
</script>
`)
assertCode(content)
Expand Down Expand Up @@ -544,36 +565,36 @@ const emit = defineEmit(['a', 'b'])
})
})

test('defineEmit w/ type', () => {
test('defineEmits w/ type', () => {
const { content } = compile(`
<script setup lang="ts">
import { defineEmit } from 'vue'
const emit = defineEmit<(e: 'foo' | 'bar') => void>()
import { defineEmits } from 'vue'
const emit = defineEmits<(e: 'foo' | 'bar') => void>()
</script>
`)
assertCode(content)
expect(content).toMatch(`emit: ((e: 'foo' | 'bar') => void),`)
expect(content).toMatch(`emits: ["foo", "bar"] as unknown as undefined`)
})

test('defineEmit w/ type (union)', () => {
test('defineEmits w/ type (union)', () => {
const type = `((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void)`
expect(() =>
compile(`
<script setup lang="ts">
import { defineEmit } from 'vue'
const emit = defineEmit<${type}>()
import { defineEmits } from 'vue'
const emit = defineEmits<${type}>()
</script>
`)
).toThrow()
})

test('defineEmit w/ type (type literal w/ call signatures)', () => {
test('defineEmits w/ type (type literal w/ call signatures)', () => {
const type = `{(e: 'foo' | 'bar'): void; (e: 'baz', id: number): void;}`
const { content } = compile(`
<script setup lang="ts">
import { defineEmit } from 'vue'
const emit = defineEmit<${type}>()
import { defineEmits } from 'vue'
const emit = defineEmits<${type}>()
</script>
`)
assertCode(content)
Expand Down Expand Up @@ -897,8 +918,8 @@ const emit = defineEmit(['a', 'b'])

expect(() => {
compile(`<script setup lang="ts">
import { defineEmit } from 'vue'
defineEmit<{}>({})
import { defineEmits } from 'vue'
defineEmits<{}>({})
</script>`)
}).toThrow(`cannot accept both type and non-type arguments`)
})
Expand All @@ -918,9 +939,9 @@ const emit = defineEmit(['a', 'b'])

expect(() =>
compile(`<script setup>
import { defineEmit } from 'vue'
import { defineEmits } from 'vue'
const bar = 'hello'
defineEmit([bar])
defineEmits([bar])
</script>`)
).toThrow(`cannot reference locally declared variables`)
})
Expand All @@ -938,9 +959,9 @@ const emit = defineEmit(['a', 'b'])

expect(() =>
compile(`<script setup>
import { defineEmit } from 'vue'
import { defineEmits } from 'vue'
ref: bar = 1
defineEmit({
defineEmits({
bar
})
</script>`)
Expand All @@ -950,14 +971,14 @@ const emit = defineEmit(['a', 'b'])
test('should allow defineProps/Emit() referencing scope var', () => {
assertCode(
compile(`<script setup>
import { defineProps, defineEmit } from 'vue'
import { defineProps, defineEmits } from 'vue'
const bar = 1
defineProps({
foo: {
default: bar => bar + 1
}
})
defineEmit({
defineEmits({
foo: bar => bar > 1
})
</script>`).content
Expand All @@ -967,14 +988,14 @@ const emit = defineEmit(['a', 'b'])
test('should allow defineProps/Emit() referencing imported binding', () => {
assertCode(
compile(`<script setup>
import { defineProps, defineEmit } from 'vue'
import { defineProps, defineEmits } from 'vue'
import { bar } from './bar'
defineProps({
foo: {
default: () => bar
}
})
defineEmit({
defineEmits({
foo: () => bar > 1
})
</script>`).content
Expand Down
27 changes: 16 additions & 11 deletions packages/compiler-sfc/src/compileScript.ts
Expand Up @@ -36,6 +36,7 @@ import { rewriteDefault } from './rewriteDefault'

const DEFINE_PROPS = 'defineProps'
const DEFINE_EMIT = 'defineEmit'
const DEFINE_EMITS = 'defineEmits'

export interface SFCScriptCompileOptions {
/**
Expand Down Expand Up @@ -282,10 +283,10 @@ export function compileScript(
return false
}

function processDefineEmit(node: Node): boolean {
if (isCallOf(node, DEFINE_EMIT)) {
function processDefineEmits(node: Node): boolean {
if (isCallOf(node, DEFINE_EMIT) || isCallOf(node, DEFINE_EMITS)) {
if (hasDefineEmitCall) {
error(`duplicate ${DEFINE_EMIT}() call`, node)
error(`duplicate ${DEFINE_EMITS}() call`, node)
}
hasDefineEmitCall = true
emitRuntimeDecl = node.arguments[0]
Expand All @@ -305,7 +306,7 @@ export function compileScript(
emitTypeDecl = typeArg
} else {
error(
`type argument passed to ${DEFINE_EMIT}() must be a function type ` +
`type argument passed to ${DEFINE_EMITS}() must be a function type ` +
`or a literal type with call signatures.`,
typeArg
)
Expand Down Expand Up @@ -623,7 +624,9 @@ export function compileScript(
const existing = userImports[local]
if (
source === 'vue' &&
(imported === DEFINE_PROPS || imported === DEFINE_EMIT)
(imported === DEFINE_PROPS ||
imported === DEFINE_EMIT ||
imported === DEFINE_EMITS)
) {
removeSpecifier(i)
} else if (existing) {
Expand All @@ -647,11 +650,11 @@ export function compileScript(
}
}

// process `defineProps` and `defineEmit` calls
// process `defineProps` and `defineEmit(s)` calls
if (
node.type === 'ExpressionStatement' &&
(processDefineProps(node.expression) ||
processDefineEmit(node.expression))
processDefineEmits(node.expression))
) {
s.remove(node.start! + startOffset, node.end! + startOffset)
}
Expand All @@ -665,14 +668,14 @@ export function compileScript(
decl.id.end!
)
}
const isDefineEmit = processDefineEmit(decl.init)
if (isDefineEmit) {
const isDefineEmits = processDefineEmits(decl.init)
if (isDefineEmits) {
emitIdentifier = scriptSetup.content.slice(
decl.id.start!,
decl.id.end!
)
}
if (isDefineProps || isDefineEmit)
if (isDefineProps || isDefineEmits)
if (node.declarations.length === 1) {
s.remove(node.start! + startOffset, node.end! + startOffset)
} else {
Expand Down Expand Up @@ -1036,7 +1039,9 @@ function walkDeclaration(
for (const { id, init } of node.declarations) {
const isDefineCall = !!(
isConst &&
(isCallOf(init, DEFINE_PROPS) || isCallOf(init, DEFINE_EMIT))
(isCallOf(init, DEFINE_PROPS) ||
isCallOf(init, DEFINE_EMIT) ||
isCallOf(init, DEFINE_EMITS))
)
if (id.type === 'Identifier') {
let bindingType
Expand Down
6 changes: 3 additions & 3 deletions packages/runtime-core/__tests__/apiSetupHelpers.spec.ts
Expand Up @@ -5,15 +5,15 @@ import {
render,
SetupContext
} from '@vue/runtime-test'
import { defineEmit, defineProps, useContext } from '../src/apiSetupHelpers'
import { defineEmits, defineProps, useContext } from '../src/apiSetupHelpers'

describe('SFC <script setup> helpers', () => {
test('should warn runtime usage', () => {
defineProps()
expect(`defineProps() is a compiler-hint`).toHaveBeenWarned()

defineEmit()
expect(`defineEmit() is a compiler-hint`).toHaveBeenWarned()
defineEmits()
expect(`defineEmits() is a compiler-hint`).toHaveBeenWarned()
})

test('useContext (no args)', () => {
Expand Down
11 changes: 8 additions & 3 deletions packages/runtime-core/src/apiSetupHelpers.ts
Expand Up @@ -38,24 +38,29 @@ export function defineProps() {
return null as any
}

export function defineEmit<
export function defineEmits<
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() {
export function defineEmits() {
if (__DEV__) {
warn(
`defineEmit() is a compiler-hint helper that is only usable inside ` +
`defineEmits() is a compiler-hint helper that is only usable inside ` +
`<script setup> of a single file component. Its arguments should be ` +
`compiled away and passing it at runtime has no effect.`
)
}
return null as any
}

/**
* @deprecated use `defineEmits` instead.
*/
export const defineEmit = defineEmits

export function useContext(): SetupContext {
const i = getCurrentInstance()!
if (__DEV__ && !i) {
Expand Down
7 changes: 6 additions & 1 deletion packages/runtime-core/src/index.ts
Expand Up @@ -43,7 +43,12 @@ export { provide, inject } from './apiInject'
export { nextTick } from './scheduler'
export { defineComponent } from './apiDefineComponent'
export { defineAsyncComponent } from './apiAsyncComponent'
export { defineProps, defineEmit, useContext } from './apiSetupHelpers'
export {
defineProps,
defineEmits,
defineEmit,
useContext
} from './apiSetupHelpers'

// Advanced API ----------------------------------------------------------------

Expand Down
4 changes: 2 additions & 2 deletions packages/sfc-playground/src/codemirror/CodeMirror.vue
Expand Up @@ -3,7 +3,7 @@
</template>

<script setup lang="ts">
import { ref, onMounted, defineProps, defineEmit, watchEffect } from 'vue'
import { ref, onMounted, defineProps, defineEmits, watchEffect } from 'vue'
import { debounce } from '../utils'
import CodeMirror from './codemirror'

Expand All @@ -24,7 +24,7 @@ const props = defineProps({
}
})

const emit = defineEmit<(e: 'change', value: string) => void>()
const emit = defineEmits<(e: 'change', value: string) => void>()

onMounted(() => {
const addonOptions = {
Expand Down