test(vapor): api expose (partial)

This commit is contained in:
Evan You 2024-12-10 17:00:35 +08:00
parent baf68a0fe4
commit 12ef12105b
No known key found for this signature in database
GPG Key ID: 00E9AB7A6704CE0A
7 changed files with 121 additions and 117 deletions

View File

@ -380,16 +380,13 @@ export interface GenericComponentInstance {
// exposed properties via expose() // exposed properties via expose()
exposed: Record<string, any> | null exposed: Record<string, any> | null
exposeProxy: Record<string, any> | null
/** /**
* setup related * setup related
* @internal * @internal
*/ */
setupState?: Data setupState?: Data
/**
* @internal
*/
setupContext?: any
/** /**
* devtools access to additional info * devtools access to additional info
* @internal * @internal
@ -603,6 +600,10 @@ export interface ComponentInternalInstance extends GenericComponentInstance {
* @internal * @internal
*/ */
setupState: Data setupState: Data
/**
* @internal
*/
setupContext?: SetupContext | null
// main proxy that serves as the public instance (`this`) // main proxy that serves as the public instance (`this`)
proxy: ComponentPublicInstance | null proxy: ComponentPublicInstance | null
@ -1131,30 +1132,6 @@ function getSlotsProxy(instance: ComponentInternalInstance): Slots {
export function createSetupContext( export function createSetupContext(
instance: ComponentInternalInstance, instance: ComponentInternalInstance,
): SetupContext { ): 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__) { if (__DEV__) {
// We use getters in dev in case libs like test-utils overwrite instance // We use getters in dev in case libs like test-utils overwrite instance
// properties (overwrites should not be done in prod) // properties (overwrites should not be done in prod)
@ -1173,46 +1150,69 @@ export function createSetupContext(
get emit() { get emit() {
return (event: string, ...args: any[]) => instance.emit(event, ...args) return (event: string, ...args: any[]) => instance.emit(event, ...args)
}, },
expose, expose: exposed => expose(instance, exposed as any),
}) })
} else { } else {
return { return {
attrs: new Proxy(instance.attrs, attrsProxyHandlers), attrs: new Proxy(instance.attrs, attrsProxyHandlers),
slots: instance.slots, slots: instance.slots,
emit: instance.emit, emit: instance.emit,
expose, expose: exposed => expose(instance, exposed as any),
} }
} }
} }
export function getComponentPublicInstance( /**
* @internal
*/
export function expose(
instance: GenericComponentInstance, instance: GenericComponentInstance,
exposed: Record<string, any>,
): void {
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 || {}
}
export function getComponentPublicInstance(
instance: ComponentInternalInstance,
): ComponentPublicInstance | ComponentInternalInstance['exposed'] | null { ): ComponentPublicInstance | ComponentInternalInstance['exposed'] | null {
if (instance.exposed) { if (instance.exposed) {
if ('exposeProxy' in instance) { return (
return ( instance.exposeProxy ||
instance.exposeProxy || (instance.exposeProxy = new Proxy(proxyRefs(markRaw(instance.exposed)), {
(instance.exposeProxy = new Proxy( get(target, key: string) {
proxyRefs(markRaw(instance.exposed)), if (key in target) {
{ return target[key]
get(target, key: string) { } else if (key in publicPropertiesMap) {
if (key in target) { return publicPropertiesMap[key](
return target[key] instance as ComponentInternalInstance,
} else if (key in publicPropertiesMap) { )
return publicPropertiesMap[key]( }
instance as ComponentInternalInstance, },
) has(target, key: string) {
} return key in target || key in publicPropertiesMap
}, },
has(target, key: string) { }))
return key in target || key in publicPropertiesMap )
},
},
))
)
} else {
return instance.exposed
}
} else { } else {
return instance.proxy return instance.proxy
} }

View File

@ -499,6 +499,7 @@ export {
type ComponentInternalOptions, type ComponentInternalOptions,
type GenericComponentInstance, type GenericComponentInstance,
type LifecycleHook, type LifecycleHook,
expose,
nextUid, nextUid,
validateComponentName, validateComponentName,
} from './component' } from './component'

View File

@ -6,7 +6,7 @@ import type { RawProps } from '../src/componentProps'
export interface RenderContext { export interface RenderContext {
component: VaporComponent component: VaporComponent
host: HTMLElement host: HTMLElement
instance: VaporComponentInstance | undefined instance: Record<string, any> | undefined
app: App app: App
create: (props?: RawProps) => RenderContext create: (props?: RawProps) => RenderContext
mount: (container?: string | ParentNode) => RenderContext mount: (container?: string | ParentNode) => RenderContext

View File

@ -6,25 +6,22 @@ import { currentInstance } from '@vue/runtime-dom'
import { defineVaporComponent } from '../src/apiDefineComponent' import { defineVaporComponent } from '../src/apiDefineComponent'
const define = makeRender() const define = makeRender()
describe.todo('api: expose', () => {
test('via setup context', () => { describe('api: expose', () => {
test.todo('via setup context + template ref', () => {
const Child = defineVaporComponent({ const Child = defineVaporComponent({
setup(_, { expose }) { setup(_, { expose }) {
expose({ expose({
foo: 1, foo: 1,
bar: ref(2), bar: ref(2),
}) })
return { return []
bar: ref(3),
baz: ref(4),
}
}, },
}) })
const childRef = ref() const childRef = ref()
define({ define({
render: () => { render: () => {
const n0 = createComponent(Child) const n0 = createComponent(Child)
setRef(n0, childRef)
return n0 return n0
}, },
}).render() }).render()
@ -35,7 +32,7 @@ describe.todo('api: expose', () => {
expect(childRef.value.baz).toBeUndefined() expect(childRef.value.baz).toBeUndefined()
}) })
test('via setup context (expose empty)', () => { test.todo('via setup context + template ref (expose empty)', () => {
let childInstance: VaporComponentInstance | null = null let childInstance: VaporComponentInstance | null = null
const Child = defineVaporComponent({ const Child = defineVaporComponent({
setup(_) { setup(_) {
@ -62,13 +59,11 @@ describe.todo('api: expose', () => {
expose({ expose({
foo: 1, foo: 1,
}) })
return { return []
bar: 2,
}
}, },
}).render() }).render()
expect(instance!.exposed!.foo).toBe(1) expect(instance!.foo).toBe(1)
expect(instance!.exposed!.bar).toBe(undefined) expect(instance!.bar).toBe(undefined)
}) })
test('warning for ref', () => { test('warning for ref', () => {

View File

@ -1,13 +1,7 @@
import type { SetupContext } from '../src/component' import { createComponent, defineVaporComponent, template } from '../src'
import { import { ref, useAttrs, useSlots } from '@vue/runtime-dom'
createComponent,
defineComponent,
ref,
template,
useAttrs,
useSlots,
} from '../src'
import { makeRender } from './_utils' import { makeRender } from './_utils'
import type { VaporComponentInstance } from '../src/component'
const define = makeRender<any>() const define = makeRender<any>()
@ -15,15 +9,17 @@ describe.todo('SFC <script setup> helpers', () => {
test.todo('should warn runtime usage', () => {}) test.todo('should warn runtime usage', () => {})
test('useSlots / useAttrs (no args)', () => { test('useSlots / useAttrs (no args)', () => {
let slots: SetupContext['slots'] | undefined let slots: VaporComponentInstance['slots'] | undefined
let attrs: SetupContext['attrs'] | undefined let attrs: VaporComponentInstance['attrs'] | undefined
const Comp = { const Comp = defineVaporComponent({
setup() { setup() {
// @ts-expect-error
slots = useSlots() slots = useSlots()
attrs = useAttrs() attrs = useAttrs()
return []
}, },
} })
const count = ref(0) const count = ref(0)
const passedAttrs = { id: () => count.value } const passedAttrs = { id: () => count.value }
const passedSlots = { const passedSlots = {
@ -45,14 +41,16 @@ describe.todo('SFC <script setup> helpers', () => {
}) })
test('useSlots / useAttrs (with args)', () => { test('useSlots / useAttrs (with args)', () => {
let slots: SetupContext['slots'] | undefined let slots: VaporComponentInstance['slots'] | undefined
let attrs: SetupContext['attrs'] | undefined let attrs: VaporComponentInstance['attrs'] | undefined
let ctx: SetupContext | undefined let ctx: VaporComponentInstance | undefined
const Comp = defineComponent({ const Comp = defineVaporComponent({
setup(_, _ctx) { setup(_, _ctx) {
// @ts-expect-error
slots = useSlots() slots = useSlots()
attrs = useAttrs() attrs = useAttrs()
ctx = _ctx ctx = _ctx as VaporComponentInstance
return []
}, },
}) })
const { render } = define({ render: () => createComponent(Comp) }) const { render } = define({ render: () => createComponent(Comp) })

View File

@ -2,6 +2,7 @@ import {
type VaporComponent, type VaporComponent,
type VaporComponentInstance, type VaporComponentInstance,
createComponent, createComponent,
getExposed,
mountComponent, mountComponent,
unmountComponent, unmountComponent,
} from './component' } from './component'
@ -41,7 +42,7 @@ export const createVaporApp: CreateAppFunction<ParentNode, VaporComponent> = (
comp, comp,
props, props,
) => { ) => {
if (!_createApp) _createApp = createAppAPI(mountApp, unmountApp, i => i) if (!_createApp) _createApp = createAppAPI(mountApp, unmountApp, getExposed)
const app = _createApp(comp, props) const app = _createApp(comp, props)
if (__DEV__) { if (__DEV__) {

View File

@ -14,6 +14,7 @@ import {
callWithErrorHandling, callWithErrorHandling,
currentInstance, currentInstance,
endMeasure, endMeasure,
expose,
nextUid, nextUid,
popWarningContext, popWarningContext,
pushWarningContext, pushWarningContext,
@ -24,7 +25,12 @@ import {
warn, warn,
} from '@vue/runtime-dom' } from '@vue/runtime-dom'
import { type Block, insert, isBlock, remove } from './block' import { type Block, insert, isBlock, remove } from './block'
import { pauseTracking, proxyRefs, resetTracking } from '@vue/reactivity' import {
markRaw,
pauseTracking,
proxyRefs,
resetTracking,
} from '@vue/reactivity'
import { EMPTY_OBJ, invokeArrayFns, isFunction, isString } from '@vue/shared' import { EMPTY_OBJ, invokeArrayFns, isFunction, isString } from '@vue/shared'
import { import {
type DynamicPropsSource, type DynamicPropsSource,
@ -55,7 +61,7 @@ export type VaporComponent = FunctionalVaporComponent | ObjectVaporComponent
export type VaporSetupFn = ( export type VaporSetupFn = (
props: any, props: any,
ctx: SetupContext, ctx: Pick<VaporComponentInstance, 'slots' | 'attrs' | 'emit' | 'expose'>,
) => Block | Record<string, any> | undefined ) => Block | Record<string, any> | undefined
export type FunctionalVaporComponent = VaporSetupFn & export type FunctionalVaporComponent = VaporSetupFn &
@ -156,12 +162,10 @@ export function createComponent(
pauseTracking() pauseTracking()
const setupFn = isFunction(component) ? component : component.setup const setupFn = isFunction(component) ? component : component.setup
const setupContext = (instance.setupContext =
setupFn && setupFn.length > 1 ? new SetupContext(instance) : null)
const setupResult = setupFn const setupResult = setupFn
? callWithErrorHandling(setupFn, instance, ErrorCodes.SETUP_FUNCTION, [ ? callWithErrorHandling(setupFn, instance, ErrorCodes.SETUP_FUNCTION, [
instance.props, instance.props,
setupContext, instance,
]) || EMPTY_OBJ ]) || EMPTY_OBJ
: EMPTY_OBJ : EMPTY_OBJ
@ -252,17 +256,22 @@ export class VaporComponentInstance implements GenericComponentInstance {
block: Block block: Block
scope: EffectScope scope: EffectScope
props: Record<string, any>
attrs: Record<string, any>
slots: StaticSlots
exposed: Record<string, any> | null
rawProps: RawProps rawProps: RawProps
rawSlots: RawSlots rawSlots: RawSlots
props: Record<string, any>
attrs: Record<string, any>
propsDefaults: Record<string, any> | null
slots: StaticSlots
emit: EmitFn emit: EmitFn
emitted: Record<string, boolean> | null emitted: Record<string, boolean> | null
propsDefaults: Record<string, any> | null
expose: (exposed: Record<string, any>) => void
exposed: Record<string, any> | null
exposeProxy: Record<string, any> | null
// for useTemplateRef() // for useTemplateRef()
refs: Record<string, any> refs: Record<string, any>
@ -296,8 +305,6 @@ export class VaporComponentInstance implements GenericComponentInstance {
ec?: LifecycleHook // LifecycleHooks.ERROR_CAPTURED ec?: LifecycleHook // LifecycleHooks.ERROR_CAPTURED
sp?: LifecycleHook<() => Promise<unknown>> // LifecycleHooks.SERVER_PREFETCH sp?: LifecycleHook<() => Promise<unknown>> // LifecycleHooks.SERVER_PREFETCH
setupContext?: SetupContext | null
// dev only // dev only
setupState?: Record<string, any> setupState?: Record<string, any>
devtoolsRawSetupState?: any devtoolsRawSetupState?: any
@ -336,8 +343,15 @@ export class VaporComponentInstance implements GenericComponentInstance {
this.scope = new EffectScope(true) this.scope = new EffectScope(true)
this.emit = emit.bind(null, this) this.emit = emit.bind(null, this)
this.expose = expose.bind(null, this)
this.refs = EMPTY_OBJ this.refs = EMPTY_OBJ
this.emitted = this.exposed = this.propsDefaults = this.suspense = null this.emitted =
this.exposed =
this.exposeProxy =
this.propsDefaults =
this.suspense =
null
this.isMounted = this.isMounted =
this.isUnmounted = this.isUnmounted =
this.isUpdating = this.isUpdating =
@ -383,22 +397,6 @@ export function isVaporComponent(
return value instanceof VaporComponentInstance return value instanceof VaporComponentInstance
} }
export class SetupContext {
attrs: Record<string, any>
emit: EmitFn
slots: Readonly<StaticSlots>
expose: (exposed?: Record<string, any>) => void
constructor(instance: VaporComponentInstance) {
this.attrs = instance.attrs
this.emit = instance.emit
this.slots = instance.slots
this.expose = (exposed = {}) => {
instance.exposed = exposed
}
}
}
/** /**
* Used when a component cannot be resolved at compile time * Used when a component cannot be resolved at compile time
* and needs rely on runtime resolution - where it might fallback to a plain * and needs rely on runtime resolution - where it might fallback to a plain
@ -489,3 +487,14 @@ export function unmountComponent(
remove(instance.block, parent) remove(instance.block, parent)
} }
} }
export function getExposed(
instance: GenericComponentInstance,
): Record<string, any> | undefined {
if (instance.exposed) {
return (
instance.exposeProxy ||
(instance.exposeProxy = proxyRefs(markRaw(instance.exposed)))
)
}
}