diff --git a/packages/runtime-vapor/__tests__/componentExpose.spec.ts b/packages/runtime-vapor/__tests__/componentExpose.spec.ts new file mode 100644 index 000000000..7e046f9bc --- /dev/null +++ b/packages/runtime-vapor/__tests__/componentExpose.spec.ts @@ -0,0 +1,49 @@ +import { describe, expect } from 'vitest' +import { makeRender } from './_utils' +import { type Ref, ref } from '@vue/reactivity' + +const define = makeRender() + +describe('component expose', () => { + test('should work', async () => { + const expxosedObj = { foo: 1 } + const { render } = define({ + setup(_, { expose }) { + expose(expxosedObj) + }, + }) + const { instance } = render() + expect(instance.exposed).toEqual(expxosedObj) + }) + + test('should warn when called multiple times', async () => { + const { render } = define({ + setup(_, { expose }) { + expose() + expose() + }, + }) + render() + expect( + 'expose() should be called only once per setup().', + ).toHaveBeenWarned() + }) + + test('should warn when passed non-object', async () => { + const exposedRef = ref([1, 2, 3]) + const { render } = define({ + setup(_, { expose }) { + expose(exposedRef.value) + }, + }) + render() + expect( + 'expose() should be passed a plain object, received array.', + ).toHaveBeenWarned() + exposedRef.value = ref(1) + render() + expect( + 'expose() should be passed a plain object, received ref.', + ).toHaveBeenWarned() + }) +}) diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index e33233e10..43e89aaa5 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -1,5 +1,5 @@ -import { EffectScope } from '@vue/reactivity' -import { EMPTY_OBJ, NOOP, isFunction } from '@vue/shared' +import { EffectScope, isRef } from '@vue/reactivity' +import { EMPTY_OBJ, isArray, isFunction } from '@vue/shared' import type { Block } from './apiRender' import type { DirectiveBinding } from './directives' import { @@ -45,6 +45,30 @@ export type SetupContext = E extends any export function createSetupContext( instance: ComponentInternalInstance, ): SetupContext { + const expose: SetupContext['expose'] = exposed => { + if (__DEV__) { + if (instance.exposed) { + warn(`expose() should be called only once per setup().`) + } + if (exposed != null) { + let exposedType: string = typeof exposed + if (exposedType === 'object') { + if (isArray(exposed)) { + exposedType = 'array' + } else if (isRef(exposed)) { + exposedType = 'ref' + } + } + if (exposedType !== 'object') { + warn( + `expose() should be passed a plain object, received ${exposedType}.`, + ) + } + } + } + instance.exposed = exposed || {} + } + if (__DEV__) { // We use getters in dev in case libs like test-utils overwrite instance // properties (overwrites should not be done in prod) @@ -58,7 +82,7 @@ export function createSetupContext( get emit() { return (event: string, ...args: any[]) => instance.emit(event, ...args) }, - expose: NOOP, + expose, }) } else { return { @@ -67,7 +91,7 @@ export function createSetupContext( }, emit: instance.emit, slots: instance.slots, - expose: NOOP, + expose, } } } @@ -114,9 +138,12 @@ export interface ComponentInternalInstance { attrs: Data slots: InternalSlots refs: Data + // exposed properties via expose() + exposed?: Record attrsProxy?: Data slotsProxy?: Slots + exposeProxy?: Record // lifecycle isMounted: boolean