diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index f67b55727..50a86fc65 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -92,7 +92,7 @@ export { // For getting a hold of the internal instance in setup() - useful for advanced // plugins -export { getCurrentInstance } from './component' +export { getCurrentInstance, getExposeProxy } from './component' // For raw render function users export { h } from './h' diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index d1e5723f2..e73f036c0 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -1,6 +1,6 @@ -import { EffectScope, type Ref, ref } from '@vue/reactivity' +import { EffectScope, isRef, type Ref, ref } from '@vue/reactivity' -import { EMPTY_OBJ } from '@vue/shared' +import { EMPTY_OBJ, isArray } from '@vue/shared' import type { Block } from './render' import type { DirectiveBinding } from './directive' import { @@ -11,7 +11,7 @@ import { import type { Data } from '@vue/shared' import { VaporLifecycleHooks } from './enums' - +import { warn } from '@vue/runtime-core' export type Component = FunctionalComponent | ObjectComponent export type SetupFn = (props: any, ctx: any) => Block | Data @@ -41,6 +41,10 @@ export interface ComponentInternalInstance { props: Data setupState: Data + // exposed properties via expose() + exposed: Record | null + exposeProxy: Record | null + /** directives */ dirs: Map @@ -122,6 +126,54 @@ export const unsetCurrentInstance = () => { currentInstance = null } +// TODO: Maybe it can be reused +// TODO: https://github.com/vuejs/core/blob/04d2c05054c26b02fbc1d84839b0ed5cd36455b6/packages/runtime-core/src/component.ts#L186C1-L186C1 +export type SetupContext = { + expose: (exposed?: Record) => void +} + +// TODO: Maybe it can be reused +// TODO: https://github.com/vuejs/core/blob/04d2c05054c26b02fbc1d84839b0ed5cd36455b6/packages/runtime-core/src/component.ts#L983 +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) + return Object.freeze({ + expose, + }) + } else { + return { + expose, + } + } +} + let uid = 0 export const createComponentInstance = ( component: ObjectComponent | FunctionalComponent, @@ -146,6 +198,10 @@ export const createComponentInstance = ( props: EMPTY_OBJ, setupState: EMPTY_OBJ, + // exposed properties via expose() + exposed: null, + exposeProxy: null, + dirs: new Map(), // lifecycle diff --git a/packages/runtime-vapor/src/directive.ts b/packages/runtime-vapor/src/directive.ts index f59ce6a14..bdd0b4724 100644 --- a/packages/runtime-vapor/src/directive.ts +++ b/packages/runtime-vapor/src/directive.ts @@ -1,7 +1,7 @@ import { isFunction } from '@vue/shared' import { type ComponentInternalInstance, currentInstance } from './component' import { watchEffect } from './apiWatch' - +import { getExposeProxy } from '@vue/runtime-core' export type DirectiveModifiers = Record export interface DirectiveBinding { @@ -69,6 +69,10 @@ export function withDirectives( } const instance = currentInstance + // TODO: Wait for the component instance type to be completed before deleting `any` + const instanceProxy = + (getExposeProxy(currentInstance as any) as ComponentInternalInstance) || + currentInstance.proxy if (!instance.dirs.has(node)) instance.dirs.set(node, []) const bindings = instance.dirs.get(node)! @@ -84,7 +88,7 @@ export function withDirectives( const binding: DirectiveBinding = { dir, - instance, + instance: instanceProxy, source, value: null, // set later oldValue: null, diff --git a/packages/runtime-vapor/src/render.ts b/packages/runtime-vapor/src/render.ts index bce3a2be7..8dd9ac364 100644 --- a/packages/runtime-vapor/src/render.ts +++ b/packages/runtime-vapor/src/render.ts @@ -4,6 +4,7 @@ import { type Component, type ComponentInternalInstance, createComponentInstance, + createSetupContext, setCurrentInstance, unsetCurrentInstance, } from './component' @@ -47,7 +48,7 @@ export function mountComponent( setCurrentInstance(instance) const block = instance.scope.run(() => { const { component, props } = instance - const ctx = { expose: () => {} } + const ctx = createSetupContext(instance) const setupFn = typeof component === 'function' ? component : component.setup