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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,6 +14,7 @@ import {
callWithErrorHandling,
currentInstance,
endMeasure,
expose,
nextUid,
popWarningContext,
pushWarningContext,
@ -24,7 +25,12 @@ import {
warn,
} from '@vue/runtime-dom'
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 {
type DynamicPropsSource,
@ -55,7 +61,7 @@ export type VaporComponent = FunctionalVaporComponent | ObjectVaporComponent
export type VaporSetupFn = (
props: any,
ctx: SetupContext,
ctx: Pick<VaporComponentInstance, 'slots' | 'attrs' | 'emit' | 'expose'>,
) => Block | Record<string, any> | undefined
export type FunctionalVaporComponent = VaporSetupFn &
@ -156,12 +162,10 @@ export function createComponent(
pauseTracking()
const setupFn = isFunction(component) ? component : component.setup
const setupContext = (instance.setupContext =
setupFn && setupFn.length > 1 ? new SetupContext(instance) : null)
const setupResult = setupFn
? callWithErrorHandling(setupFn, instance, ErrorCodes.SETUP_FUNCTION, [
instance.props,
setupContext,
instance,
]) || EMPTY_OBJ
: EMPTY_OBJ
@ -252,17 +256,22 @@ export class VaporComponentInstance implements GenericComponentInstance {
block: Block
scope: EffectScope
props: Record<string, any>
attrs: Record<string, any>
slots: StaticSlots
exposed: Record<string, any> | null
rawProps: RawProps
rawSlots: RawSlots
props: Record<string, any>
attrs: Record<string, any>
propsDefaults: Record<string, any> | null
slots: StaticSlots
emit: EmitFn
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()
refs: Record<string, any>
@ -296,8 +305,6 @@ export class VaporComponentInstance implements GenericComponentInstance {
ec?: LifecycleHook // LifecycleHooks.ERROR_CAPTURED
sp?: LifecycleHook<() => Promise<unknown>> // LifecycleHooks.SERVER_PREFETCH
setupContext?: SetupContext | null
// dev only
setupState?: Record<string, any>
devtoolsRawSetupState?: any
@ -336,8 +343,15 @@ export class VaporComponentInstance implements GenericComponentInstance {
this.scope = new EffectScope(true)
this.emit = emit.bind(null, this)
this.expose = expose.bind(null, this)
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.isUnmounted =
this.isUpdating =
@ -383,22 +397,6 @@ export function isVaporComponent(
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
* and needs rely on runtime resolution - where it might fallback to a plain
@ -489,3 +487,14 @@ export function unmountComponent(
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)))
)
}
}