diff --git a/packages-private/dts-test/vapor/defineVaporCustomElement.test-d.ts b/packages-private/dts-test/vapor/defineVaporCustomElement.test-d.ts new file mode 100644 index 00000000000..72ff2b89a1b --- /dev/null +++ b/packages-private/dts-test/vapor/defineVaporCustomElement.test-d.ts @@ -0,0 +1,248 @@ +import { + type VaporElementConstructor, + defineVaporComponent, + defineVaporCustomElement, +} from 'vue' +import { describe, expectType, test } from '../utils' + +describe('defineVaporCustomElement using defineVaporComponent return type', () => { + test('with object emits', () => { + const Comp1Vapor = defineVaporComponent({ + props: { + a: String, + }, + emits: { + click: () => true, + }, + }) + const Comp = defineVaporCustomElement(Comp1Vapor) + expectType(Comp) + + const instance = new Comp() + expectType(instance.a) + instance.a = '' + }) + + test('with array emits', () => { + const Comp1Vapor = defineVaporComponent({ + props: { + a: Number, + }, + emits: ['click'], + }) + const Comp = defineVaporCustomElement(Comp1Vapor) + expectType(Comp) + + const instance = new Comp() + expectType(instance.a) + instance.a = 42 + }) + + test('with required props', () => { + const Comp1Vapor = defineVaporComponent({ + props: { + a: { type: Number, required: true }, + }, + }) + const Comp = defineVaporCustomElement(Comp1Vapor) + expectType(Comp) + + const instance = new Comp() + expectType(instance.a) + instance.a = 42 + }) + + test('with default props', () => { + const Comp1Vapor = defineVaporComponent({ + props: { + a: { + type: Number, + default: 1, + validator: () => true, + }, + }, + emits: ['click'], + }) + const Comp = defineVaporCustomElement(Comp1Vapor) + expectType(Comp) + + const instance = new Comp() + expectType(instance.a) + instance.a = 42 + }) + + test('with extra options', () => { + const Comp1Vapor = defineVaporComponent({ + props: { + a: { + type: Number, + default: 1, + validator: () => true, + }, + }, + emits: ['click'], + }) + const Comp = defineVaporCustomElement(Comp1Vapor, { + shadowRoot: false, + styles: [`div { color: red; }`], + nonce: 'xxx', + shadowRootOptions: { + delegatesFocus: false, + }, + configureApp: app => { + app.provide('a', 1) + }, + }) + expectType(Comp) + + const instance = new Comp() + expectType(instance.a) + instance.a = 42 + }) +}) + +describe('defineVaporCustomElement with direct setup function', () => { + test('basic setup function', () => { + const Comp = defineVaporCustomElement((props: { msg: string }) => { + expectType(props.msg) + return [] + }) + expectType>(Comp) + + const instance = new Comp() + expectType(instance.msg) + }) + + test('setup function with emits', () => { + const Comp = defineVaporCustomElement( + (props: { msg: string }, ctx) => { + ctx.emit('foo') + return [] + }, + { + emits: ['foo'], + }, + ) + expectType>(Comp) + + const instance = new Comp() + expectType(instance.msg) + }) + + test('setup function with extra options', () => { + const Comp = defineVaporCustomElement( + (props: { msg: string }, ctx) => { + ctx.emit('foo') + return [] + }, + { + name: 'Foo', + emits: ['foo'], + inheritAttrs: false, + shadowRoot: false, + styles: [`div { color: red; }`], + nonce: 'xxx', + shadowRootOptions: { + delegatesFocus: false, + }, + configureApp: app => { + app.provide('a', 1) + }, + }, + ) + expectType>(Comp) + + const instance = new Comp() + expectType(instance.msg) + }) +}) + +describe('defineVaporCustomElement with options object', () => { + test('with object props', () => { + const Comp = defineVaporCustomElement({ + props: { + foo: String, + bar: { + type: Number, + required: true, + }, + }, + setup(props) { + expectType(props.foo) + expectType(props.bar) + }, + }) + expectType(Comp) + + const instance = new Comp() + expectType(instance.foo) + expectType(instance.bar) + }) + + test('with array props', () => { + const Comp = defineVaporCustomElement({ + props: ['foo', 'bar'], + setup(props) { + expectType(props.foo) + expectType(props.bar) + }, + }) + expectType(Comp) + + const instance = new Comp() + expectType(instance.foo) + expectType(instance.bar) + }) + + test('with emits', () => { + const Comp = defineVaporCustomElement({ + props: { + value: String, + }, + emits: { + change: (val: string) => true, + }, + setup(props, { emit }) { + emit('change', 'test') + // @ts-expect-error + emit('change', 123) + // @ts-expect-error + emit('unknown') + }, + }) + expectType(Comp) + + const instance = new Comp() + expectType(instance.value) + }) + + test('with extra options', () => { + const Comp = defineVaporCustomElement( + { + props: { + value: String, + }, + emits: { + change: (val: string) => true, + }, + setup(props, { emit }) { + emit('change', 'test') + // @ts-expect-error + emit('change', 123) + // @ts-expect-error + emit('unknown') + }, + }, + { + shadowRoot: false, + configureApp: app => { + app.provide('a', 1) + }, + }, + ) + expectType(Comp) + + const instance = new Comp() + expectType(instance.value) + }) +}) diff --git a/packages/runtime-vapor/__tests__/customElement.spec.ts b/packages/runtime-vapor/__tests__/customElement.spec.ts index 116b517340d..6042a9616d4 100644 --- a/packages/runtime-vapor/__tests__/customElement.spec.ts +++ b/packages/runtime-vapor/__tests__/customElement.spec.ts @@ -1,5 +1,5 @@ import type { MockedFunction } from 'vitest' -import type { VaporElement } from '../src/apiDefineVaporCustomElement' +import type { VaporElement } from '../src/apiDefineCustomElement' import { type HMRRuntime, type Ref, @@ -230,8 +230,7 @@ describe('defineVaporCustomElement', () => { }) test('props via properties', async () => { - // TODO remove this after type inference done - const e = new E() as any + const e = new E() e.foo = 'one' e.bar = { x: 'two' } container.appendChild(e) @@ -266,8 +265,7 @@ describe('defineVaporCustomElement', () => { }) test('props via attributes and properties changed together', async () => { - // TODO remove this after type inference done - const e = new E() as any + const e = new E() e.foo = 'foo1' e.bar = { x: 'bar1' } container.appendChild(e) @@ -617,7 +615,7 @@ describe('defineVaporCustomElement', () => { return template('', true)() }, }, - { shadowRootOptions: { delegatesFocus: true } } as any, + { shadowRootOptions: { delegatesFocus: true } }, ) customElements.define('my-el-with-delegate-focus', E) @@ -689,7 +687,7 @@ describe('defineVaporCustomElement', () => { }) test('emit from within async component wrapper', async () => { - const p = new Promise(res => res(CompDef as any)) + const p = new Promise(res => res(CompDef)) const E = defineVaporCustomElement(defineVaporAsyncComponent(() => p)) customElements.define('my-async-el-emits', E) container.innerHTML = `` @@ -715,7 +713,7 @@ describe('defineVaporCustomElement', () => { test('emit in an async component wrapper with properties bound', async () => { const E = defineVaporCustomElement( defineVaporAsyncComponent( - () => new Promise(res => res(CompDef as any)), + () => new Promise(res => res(CompDef)), ), ) customElements.define('my-async-el-props-emits', E) @@ -938,7 +936,7 @@ describe('defineVaporCustomElement', () => { app.provide('shared', 'shared') app.provide('outer', 'outer') }, - } as any, + }, ) const Inner = defineVaporCustomElement( @@ -963,7 +961,7 @@ describe('defineVaporCustomElement', () => { app.provide('outer', 'override-outer') app.provide('inner', 'inner') }, - } as any, + }, ) const InnerChild = defineVaporCustomElement({ @@ -1107,7 +1105,7 @@ describe('defineVaporCustomElement', () => { return [createComponent(Baz)] }, }, - { shadowRoot: false } as any, + { shadowRoot: false }, ) customElements.define('my-foo-with-shadowroot-false', Foo) @@ -1125,7 +1123,7 @@ describe('defineVaporCustomElement', () => { return template('
hello
', true)() }, }, - { nonce: 'xxx' } as any, + { nonce: 'xxx' }, ) customElements.define('my-el-with-nonce', Foo) container.innerHTML = `` @@ -1141,16 +1139,18 @@ describe('defineVaporCustomElement', () => { const E = defineVaporCustomElement( defineVaporAsyncComponent(() => { loaderSpy() - return Promise.resolve({ - props: ['msg'], - styles: [`div { color: red }`], - setup(props: any) { - const n0 = template('
', true)() as any - const x0 = txt(n0) as any - renderEffect(() => setText(x0, props.msg)) - return n0 - }, - }) + return Promise.resolve( + defineVaporComponent({ + props: ['msg'], + styles: [`div { color: red }`], + setup(props: any) { + const n0 = template('
', true)() as any + const x0 = txt(n0) as any + renderEffect(() => setText(x0, props.msg)) + return n0 + }, + } as any), + ) }), ) customElements.define('my-el-async', E) @@ -1194,16 +1194,18 @@ describe('defineVaporCustomElement', () => { test('set DOM property before resolve', async () => { const E = defineVaporCustomElement( defineVaporAsyncComponent(() => { - return Promise.resolve({ - props: ['msg'], - setup(props: any) { - expect(typeof props.msg).toBe('string') - const n0 = template('
', true)() as any - const x0 = txt(n0) as any - renderEffect(() => setText(x0, props.msg)) - return n0 - }, - }) + return Promise.resolve( + defineVaporComponent({ + props: ['msg'], + setup(props: any) { + expect(typeof props.msg).toBe('string') + const n0 = template('
', true)() as any + const x0 = txt(n0) as any + renderEffect(() => setText(x0, props.msg)) + return n0 + }, + }), + ) }), ) customElements.define('my-el-async-2', E) @@ -1236,16 +1238,18 @@ describe('defineVaporCustomElement', () => { test('Number prop casting before resolve', async () => { const E = defineVaporCustomElement( defineVaporAsyncComponent(() => { - return Promise.resolve({ - props: { n: Number }, - setup(props: any) { - expect(props.n).toBe(20) - const n0 = template('
', true)() as any - const x0 = txt(n0) as any - renderEffect(() => setText(x0, `${props.n},${typeof props.n}`)) - return n0 - }, - }) + return Promise.resolve( + defineVaporComponent({ + props: { n: Number }, + setup(props: any) { + expect(props.n).toBe(20) + const n0 = template('
', true)() as any + const x0 = txt(n0) as any + renderEffect(() => setText(x0, `${props.n},${typeof props.n}`)) + return n0 + }, + }), + ) }), ) customElements.define('my-el-async-3', E) @@ -1260,22 +1264,24 @@ describe('defineVaporCustomElement', () => { test('with slots', async () => { const E = defineVaporCustomElement( defineVaporAsyncComponent(() => { - return Promise.resolve({ - setup() { - const t0 = template('
fallback
') - const t1 = template('
') - const n3 = t1() as any - setInsertionState(n3, null) - createSlot('default', null, () => { - const n2 = t0() - return n2 - }) - const n5 = t1() as any - setInsertionState(n5, null) - createSlot('named', null) - return [n3, n5] - }, - }) + return Promise.resolve( + defineVaporComponent({ + setup() { + const t0 = template('
fallback
') + const t1 = template('
') + const n3 = t1() as any + setInsertionState(n3, null) + createSlot('default', null, () => { + const n2 = t0() + return n2 + }) + const n5 = t1() as any + setInsertionState(n5, null) + createSlot('named', null) + return [n3, n5] + }, + }), + ) }), ) customElements.define('my-el-async-slots', E) @@ -1343,7 +1349,7 @@ describe('defineVaporCustomElement', () => { return [n0, n1, n2] }, }, - { shadowRoot: false } as any, + { shadowRoot: false }, ) customElements.define('my-el-shadowroot-false-slots', ES) @@ -1384,7 +1390,7 @@ describe('defineVaporCustomElement', () => { return createSlot('default') }, }, - { shadowRoot: false } as any, + { shadowRoot: false }, ) customElements.define('my-child', Child) @@ -1398,7 +1404,7 @@ describe('defineVaporCustomElement', () => { return createSlot('default') }, }, - { shadowRoot: false } as any, + { shadowRoot: false }, ) customElements.define('my-parent', Parent) @@ -1443,7 +1449,7 @@ describe('defineVaporCustomElement', () => { ) }, }, - { shadowRoot: false } as any, + { shadowRoot: false }, ) customElements.define('my-el-teleport-child', Child) const Parent = defineVaporCustomElement( @@ -1452,7 +1458,7 @@ describe('defineVaporCustomElement', () => { return createSlot('default') }, }, - { shadowRoot: false } as any, + { shadowRoot: false }, ) customElements.define('my-el-teleport-parent', Parent) @@ -1498,7 +1504,7 @@ describe('defineVaporCustomElement', () => { ] }, }, - { shadowRoot: false } as any, + { shadowRoot: false }, ) customElements.define('my-el-two-teleport-child', Child) @@ -1549,7 +1555,7 @@ describe('defineVaporCustomElement', () => { ] }, }, - { shadowRoot: false } as any, + { shadowRoot: false }, ) customElements.define('my-el-two-teleport-child-0', Child) @@ -1585,7 +1591,7 @@ describe('defineVaporCustomElement', () => { return n0 }, }, - { shadowRoot: false } as any, + { shadowRoot: false }, ), ) const ChildWrapper = { @@ -1615,7 +1621,7 @@ describe('defineVaporCustomElement', () => { ) }, }, - { shadowRoot: false } as any, + { shadowRoot: false }, ), ) const ParentWrapper = { @@ -1808,15 +1814,17 @@ describe('defineVaporCustomElement', () => { let fooVal: string | undefined = '' const E = defineVaporCustomElement( defineVaporAsyncComponent(() => { - return Promise.resolve({ - setup() { - provide('foo', 'foo') - const n0 = template('
')() as any - setInsertionState(n0, null) - createSlot('default', null) - return n0 - }, - }) + return Promise.resolve( + defineVaporComponent({ + setup() { + provide('foo', 'foo') + const n0 = template('
')() as any + setInsertionState(n0, null) + createSlot('default', null) + return n0 + }, + }), + ) }), ) @@ -1842,15 +1850,17 @@ describe('defineVaporCustomElement', () => { let barVal: string | undefined = '' const E = defineVaporCustomElement( defineVaporAsyncComponent(() => { - return Promise.resolve({ - setup() { - provide('foo', 'foo') - const n0 = template('
')() as any - setInsertionState(n0, null) - createSlot('default', null) - return n0 - }, - }) + return Promise.resolve( + defineVaporComponent({ + setup() { + provide('foo', 'foo') + const n0 = template('
')() as any + setInsertionState(n0, null) + createSlot('default', null) + return n0 + }, + }), + ) }), ) @@ -1903,7 +1913,7 @@ describe('defineVaporCustomElement', () => { configureApp(app: any) { app.provide('msg', 'app-injected') }, - } as any, + }, ) customElements.define('my-element-with-app', E) @@ -1914,21 +1924,23 @@ describe('defineVaporCustomElement', () => { test('work with async component', async () => { const AsyncComp = defineVaporAsyncComponent(() => { - return Promise.resolve({ - setup() { - const msg = inject('msg') - const n0 = template('
', true)() as any - const x0 = txt(n0) as any - renderEffect(() => setText(x0, msg as string)) - return n0 - }, - } as any) + return Promise.resolve( + defineVaporComponent({ + setup() { + const msg = inject('msg') + const n0 = template('
', true)() as any + const x0 = txt(n0) as any + renderEffect(() => setText(x0, msg as string)) + return n0 + }, + }), + ) }) const E = defineVaporCustomElement(AsyncComp, { configureApp(app: any) { app.provide('msg', 'app-injected') }, - } as any) + }) customElements.define('my-async-element-with-app', E) container.innerHTML = `` @@ -1954,7 +1966,7 @@ describe('defineVaporCustomElement', () => { configureApp(app: any) { app.provide('msg', 'app-injected') }, - } as any) + }) customElements.define('my-element-with-app-hmr', E) container.innerHTML = `` @@ -2010,16 +2022,18 @@ describe('defineVaporCustomElement', () => { test('Props can be casted when mounting custom elements in component rendering functions', async () => { const E = defineVaporCustomElement( defineVaporAsyncComponent(() => - Promise.resolve({ - props: ['fooValue'], - setup(props: any) { - expect(props.fooValue).toBe('fooValue') - const n0 = template('
', true)() as any - const x0 = txt(n0) as any - renderEffect(() => setText(x0, props.fooValue)) - return n0 - }, - }), + Promise.resolve( + defineVaporComponent({ + props: ['fooValue'], + setup(props: any) { + expect(props.fooValue).toBe('fooValue') + const n0 = template('
', true)() as any + const x0 = txt(n0) as any + renderEffect(() => setText(x0, props.fooValue)) + return n0 + }, + }), + ), ), ) customElements.define('my-el-async-4', E) @@ -2103,7 +2117,7 @@ describe('defineVaporCustomElement', () => { name: 'Foo', } - defineVaporCustomElement(Foo, { shadowRoot: false } as any) + defineVaporCustomElement(Foo, { shadowRoot: false }) expect(Foo).toEqual({ __vapor: true, diff --git a/packages/runtime-vapor/src/apiDefineVaporCustomElement.ts b/packages/runtime-vapor/src/apiDefineCustomElement.ts similarity index 61% rename from packages/runtime-vapor/src/apiDefineVaporCustomElement.ts rename to packages/runtime-vapor/src/apiDefineCustomElement.ts index f896b49eb5b..f5f2cd8648a 100644 --- a/packages/runtime-vapor/src/apiDefineVaporCustomElement.ts +++ b/packages/runtime-vapor/src/apiDefineCustomElement.ts @@ -7,8 +7,13 @@ import { isFragment, } from '.' import { + type ComponentObjectPropsOptions, type CreateAppFunction, type CustomElementOptions, + type EmitFn, + type EmitsOptions, + type EmitsToProps, + type ExtractPropTypes, VueElementBase, warn, } from '@vue/runtime-dom' @@ -19,17 +24,124 @@ import type { } from './component' import type { Block } from './block' import { withHydration } from './dom/hydration' +import type { + DefineVaporComponent, + DefineVaporSetupFnComponent, + VaporRenderResult, +} from './apiDefineComponent' +import type { StaticSlots } from './componentSlots' export type VaporElementConstructor

= { new (initialProps?: Record): VaporElement & P } -// TODO type inference +// overload 1: direct setup function +export function defineVaporCustomElement( + setup: ( + props: Props, + ctx: { + attrs: Record + slots: StaticSlots + emit: EmitFn + expose: (exposed: Record) => void + }, + ) => RawBindings | VaporRenderResult, + options?: Pick & + CustomElementOptions & { + props?: (keyof Props)[] + }, +): VaporElementConstructor +export function defineVaporCustomElement( + setup: ( + props: Props, + ctx: { + attrs: Record + slots: StaticSlots + emit: EmitFn + expose: (exposed: Record) => void + }, + ) => RawBindings | VaporRenderResult, + options?: Pick & + CustomElementOptions & { + props?: ComponentObjectPropsOptions + }, +): VaporElementConstructor + +// overload 2: defineVaporCustomElement with options object, infer props from options +export function defineVaporCustomElement< + // props + RuntimePropsOptions extends + ComponentObjectPropsOptions = ComponentObjectPropsOptions, + RuntimePropsKeys extends string = string, + // emits + RuntimeEmitsOptions extends EmitsOptions = {}, + RuntimeEmitsKeys extends string = string, + Slots extends StaticSlots = StaticSlots, + // resolved types + InferredProps = string extends RuntimePropsKeys + ? ComponentObjectPropsOptions extends RuntimePropsOptions + ? {} + : ExtractPropTypes + : { [key in RuntimePropsKeys]?: any }, + ResolvedProps = InferredProps & EmitsToProps, +>( + options: CustomElementOptions & { + props?: (RuntimePropsOptions & ThisType) | RuntimePropsKeys[] + emits?: RuntimeEmitsOptions | RuntimeEmitsKeys[] + slots?: Slots + setup?: ( + props: Readonly, + ctx: { + attrs: Record + slots: Slots + emit: EmitFn + expose: (exposed: Record) => void + }, + ) => any + } & ThisType, + extraOptions?: CustomElementOptions, +): VaporElementConstructor + +// overload 3: defining a custom element from the returned value of +// `defineVaporComponent` +export function defineVaporCustomElement< + T extends + | DefineVaporComponent + | DefineVaporSetupFnComponent, +>( + options: T, + extraOptions?: CustomElementOptions, +): VaporElementConstructor< + T extends DefineVaporComponent< + infer RuntimePropsOptions, + any, + any, + any, + any, + any, + any, + any, + any, + any + > + ? ComponentObjectPropsOptions extends RuntimePropsOptions + ? {} + : ExtractPropTypes + : T extends DefineVaporSetupFnComponent< + infer P extends Record, + any, + any, + any, + any + > + ? P + : unknown +> /*@__NO_SIDE_EFFECTS__*/ export function defineVaporCustomElement( options: any, - extraOptions?: Omit, + extraOptions?: Omit & CustomElementOptions, /** * @internal */ @@ -52,6 +164,7 @@ export const defineVaporSSRCustomElement = (( options: any, extraOptions?: Omit, ) => { + // @ts-expect-error return defineVaporCustomElement(options, extraOptions, createVaporSSRApp) }) as typeof defineVaporCustomElement diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index c66e5f90d1c..c92396b61b1 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -116,7 +116,7 @@ import type { VaporRenderResult, } from './apiDefineComponent' import { DynamicFragment, isFragment } from './fragment' -import type { VaporElement } from './apiDefineVaporCustomElement' +import type { VaporElement } from './apiDefineCustomElement' import { parentSuspense, setParentSuspense } from './components/Suspense' export { currentInstance } from '@vue/runtime-dom' diff --git a/packages/runtime-vapor/src/dom/prop.ts b/packages/runtime-vapor/src/dom/prop.ts index 946fecee813..a11911097cd 100644 --- a/packages/runtime-vapor/src/dom/prop.ts +++ b/packages/runtime-vapor/src/dom/prop.ts @@ -43,7 +43,7 @@ import { } from '../component' import { isHydrating, logMismatchError } from './hydration' import { type Block, normalizeBlock } from '../block' -import type { VaporElement } from '../apiDefineVaporCustomElement' +import type { VaporElement } from '../apiDefineCustomElement' type TargetElement = Element & { $root?: true diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index e6bd31a7fc1..d553e9dc776 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -14,7 +14,9 @@ export { VaporKeepAliveImpl as VaporKeepAlive } from './components/KeepAlive' export { defineVaporCustomElement, defineVaporSSRCustomElement, -} from './apiDefineVaporCustomElement' + VaporElement, + type VaporElementConstructor, +} from './apiDefineCustomElement' // compiler-use only export { insert, prepend, remove, type Block } from './block'