refactor: adjust internal vnode types + more dts tests
This commit is contained in:
		
							parent
							
								
									957d3a0547
								
							
						
					
					
						commit
						dfc7c0f12a
					
				
							
								
								
									
										6
									
								
								.github/contributing.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/contributing.md
									
									
									
									
										vendored
									
									
								
							| @ -174,6 +174,12 @@ Unit tests are collocated with the code being tested in each package, inside dir | ||||
| 
 | ||||
| - Only use platform-specific runtimes if the test is asserting platform-specific behavior. | ||||
| 
 | ||||
| ### Testing Type Definition Correctness | ||||
| 
 | ||||
| This project uses [tsd](https://github.com/SamVerschueren/tsd) to test the built definition files (`*.d.ts`). | ||||
| 
 | ||||
| Type tests are located in the `test-dts` directory. To run the dts tests, run `yarn test-dts`. Note that the type test requires all relevant `*.d.ts` files to be built first (and the script does it for you). Once the `d.ts` files are built and up-to-date, the tests can be re-run by simply running `./node_modules/.bin/tsd`. | ||||
| 
 | ||||
| ## Financial Contribution | ||||
| 
 | ||||
| As a pure community-driven project without major corporate backing, we also welcome financial contributions via Patreon and OpenCollective. | ||||
|  | ||||
| @ -1,174 +0,0 @@ | ||||
| import { createComponent } from '../src/apiCreateComponent' | ||||
| import { ref } from '@vue/reactivity' | ||||
| import { PropType } from '../src/componentProps' | ||||
| import { h } from '../src/h' | ||||
| 
 | ||||
| // mock React just for TSX testing purposes
 | ||||
| const React = { | ||||
|   createElement: () => {} | ||||
| } | ||||
| 
 | ||||
| test('createComponent type inference', () => { | ||||
|   const MyComponent = createComponent({ | ||||
|     props: { | ||||
|       a: Number, | ||||
|       // required should make property non-void
 | ||||
|       b: { | ||||
|         type: String, | ||||
|         required: true | ||||
|       }, | ||||
|       // default value should infer type and make it non-void
 | ||||
|       bb: { | ||||
|         default: 'hello' | ||||
|       }, | ||||
|       // explicit type casting
 | ||||
|       cc: Array as PropType<string[]>, | ||||
|       // required + type casting
 | ||||
|       dd: { | ||||
|         type: Array as PropType<string[]>, | ||||
|         required: true | ||||
|       }, | ||||
|       // explicit type casting with constructor
 | ||||
|       ccc: Array as () => string[], | ||||
|       // required + contructor type casting
 | ||||
|       ddd: { | ||||
|         type: Array as () => string[], | ||||
|         required: true | ||||
|       } | ||||
|     }, | ||||
|     setup(props) { | ||||
|       props.a && props.a * 2 | ||||
|       props.b.slice() | ||||
|       props.bb.slice() | ||||
|       props.cc && props.cc.push('hoo') | ||||
|       props.dd.push('dd') | ||||
|       return { | ||||
|         c: ref(1), | ||||
|         d: { | ||||
|           e: ref('hi') | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     render() { | ||||
|       const props = this.$props | ||||
|       props.a && props.a * 2 | ||||
|       props.b.slice() | ||||
|       props.bb.slice() | ||||
|       props.cc && props.cc.push('hoo') | ||||
|       props.dd.push('dd') | ||||
|       this.a && this.a * 2 | ||||
|       this.b.slice() | ||||
|       this.bb.slice() | ||||
|       this.c * 2 | ||||
|       this.d.e.slice() | ||||
|       this.cc && this.cc.push('hoo') | ||||
|       this.dd.push('dd') | ||||
|       return h('div', this.bb) | ||||
|     } | ||||
|   }) | ||||
|   // test TSX props inference
 | ||||
|   ;<MyComponent | ||||
|     a={1} | ||||
|     b="foo" | ||||
|     dd={['foo']} | ||||
|     ddd={['foo']} | ||||
|     // should allow extraneous as attrs
 | ||||
|     class="bar" | ||||
|   /> | ||||
| }) | ||||
| 
 | ||||
| test('type inference w/ optional props declaration', () => { | ||||
|   const Comp = createComponent({ | ||||
|     setup(props: { msg: string }) { | ||||
|       props.msg | ||||
|       return { | ||||
|         a: 1 | ||||
|       } | ||||
|     }, | ||||
|     render() { | ||||
|       this.$props.msg | ||||
|       this.msg | ||||
|       this.a * 2 | ||||
|       return h('div', this.msg) | ||||
|     } | ||||
|   }) | ||||
|   ;<Comp msg="hello" /> | ||||
| }) | ||||
| 
 | ||||
| test('type inference w/ direct setup function', () => { | ||||
|   const Comp = createComponent((props: { msg: string }) => { | ||||
|     return () => <div>{props.msg}</div> | ||||
|   }) | ||||
|   ;<Comp msg="hello" /> | ||||
| }) | ||||
| 
 | ||||
| test('type inference w/ array props declaration', () => { | ||||
|   const Comp = createComponent({ | ||||
|     props: ['a', 'b'], | ||||
|     setup(props) { | ||||
|       props.a | ||||
|       props.b | ||||
|       return { | ||||
|         c: 1 | ||||
|       } | ||||
|     }, | ||||
|     render() { | ||||
|       this.$props.a | ||||
|       this.$props.b | ||||
|       this.a | ||||
|       this.b | ||||
|       this.c | ||||
|     } | ||||
|   }) | ||||
|   ;<Comp a={1} b={2} /> | ||||
| }) | ||||
| 
 | ||||
| test('with legacy options', () => { | ||||
|   createComponent({ | ||||
|     props: { a: Number }, | ||||
|     setup() { | ||||
|       return { | ||||
|         b: 123 | ||||
|       } | ||||
|     }, | ||||
|     data() { | ||||
|       // Limitation: we cannot expose the return result of setup() on `this`
 | ||||
|       // here in data() - somehow that would mess up the inference
 | ||||
|       return { | ||||
|         c: this.a || 123 | ||||
|       } | ||||
|     }, | ||||
|     computed: { | ||||
|       d(): number { | ||||
|         return this.b + 1 | ||||
|       } | ||||
|     }, | ||||
|     watch: { | ||||
|       a() { | ||||
|         this.b + 1 | ||||
|       } | ||||
|     }, | ||||
|     created() { | ||||
|       this.a && this.a * 2 | ||||
|       this.b * 2 | ||||
|       this.c * 2 | ||||
|       this.d * 2 | ||||
|     }, | ||||
|     methods: { | ||||
|       doSomething() { | ||||
|         this.a && this.a * 2 | ||||
|         this.b * 2 | ||||
|         this.c * 2 | ||||
|         this.d * 2 | ||||
|         return (this.a || 0) + this.b + this.c + this.d | ||||
|       } | ||||
|     }, | ||||
|     render() { | ||||
|       this.a && this.a * 2 | ||||
|       this.b * 2 | ||||
|       this.c * 2 | ||||
|       this.d * 2 | ||||
|       return h('div', (this.a || 0) + this.b + this.c + this.d) | ||||
|     } | ||||
|   }) | ||||
| }) | ||||
| @ -22,7 +22,7 @@ describe('keep-alive', () => { | ||||
|     one = { | ||||
|       name: 'one', | ||||
|       data: () => ({ msg: 'one' }), | ||||
|       render() { | ||||
|       render(this: any) { | ||||
|         return h('div', this.msg) | ||||
|       }, | ||||
|       created: jest.fn(), | ||||
| @ -34,7 +34,7 @@ describe('keep-alive', () => { | ||||
|     two = { | ||||
|       name: 'two', | ||||
|       data: () => ({ msg: 'two' }), | ||||
|       render() { | ||||
|       render(this: any) { | ||||
|         return h('div', this.msg) | ||||
|       }, | ||||
|       created: jest.fn(), | ||||
|  | ||||
| @ -83,7 +83,7 @@ export type ComponentOptionsWithoutProps< | ||||
|   M extends MethodOptions = {} | ||||
| > = ComponentOptionsBase<Props, RawBindings, D, C, M> & { | ||||
|   props?: undefined | ||||
| } & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M>> | ||||
| } & ThisType<ComponentPublicInstance<{}, RawBindings, D, C, M, Props>> | ||||
| 
 | ||||
| export type ComponentOptionsWithArrayProps< | ||||
|   PropNames extends string = string, | ||||
| @ -459,7 +459,7 @@ function createWatcher( | ||||
|   ctx: ComponentPublicInstance, | ||||
|   key: string | ||||
| ) { | ||||
|   const getter = () => ctx[key] | ||||
|   const getter = () => (ctx as Data)[key] | ||||
|   if (isString(raw)) { | ||||
|     const handler = renderContext[raw] | ||||
|     if (isFunction(handler)) { | ||||
|  | ||||
| @ -19,7 +19,8 @@ import { recordEffect } from './apiReactivity' | ||||
| import { | ||||
|   currentInstance, | ||||
|   ComponentInternalInstance, | ||||
|   currentSuspense | ||||
|   currentSuspense, | ||||
|   Data | ||||
| } from './component' | ||||
| import { | ||||
|   ErrorCodes, | ||||
| @ -219,7 +220,7 @@ export function instanceWatch( | ||||
|   cb: Function, | ||||
|   options?: WatchOptions | ||||
| ): StopHandle { | ||||
|   const ctx = this.renderProxy! | ||||
|   const ctx = this.renderProxy as Data | ||||
|   const getter = isString(source) ? () => ctx[source] : source.bind(ctx) | ||||
|   const stop = watch(getter, cb.bind(ctx), options) | ||||
|   onBeforeUnmount(stop, this) | ||||
|  | ||||
| @ -25,7 +25,7 @@ import { | ||||
|   makeMap, | ||||
|   isPromise | ||||
| } from '@vue/shared' | ||||
| import { SuspenseBoundary } from './rendererSuspense' | ||||
| import { SuspenseBoundary } from './components/Suspense' | ||||
| import { | ||||
|   CompilerError, | ||||
|   CompilerOptions, | ||||
|  | ||||
| @ -26,7 +26,6 @@ export type ComponentPublicInstance< | ||||
|   M extends MethodOptions = {}, | ||||
|   PublicProps = P | ||||
| > = { | ||||
|   [key: string]: any | ||||
|   $data: D | ||||
|   $props: PublicProps | ||||
|   $attrs: Data | ||||
|  | ||||
| @ -13,7 +13,7 @@ import { onBeforeUnmount, injectHook, onUnmounted } from '../apiLifecycle' | ||||
| import { isString, isArray } from '@vue/shared' | ||||
| import { watch } from '../apiWatch' | ||||
| import { ShapeFlags } from '../shapeFlags' | ||||
| import { SuspenseBoundary } from '../rendererSuspense' | ||||
| import { SuspenseBoundary } from './Suspense' | ||||
| import { | ||||
|   RendererInternals, | ||||
|   queuePostRenderEffect, | ||||
| @ -39,7 +39,7 @@ export interface KeepAliveSink { | ||||
|   deactivate: (vnode: VNode) => void | ||||
| } | ||||
| 
 | ||||
| export const KeepAlive = { | ||||
| const KeepAliveImpl = { | ||||
|   name: `KeepAlive`, | ||||
| 
 | ||||
|   // Marker for special handling inside the renderer. We are not using a ===
 | ||||
| @ -201,13 +201,20 @@ export const KeepAlive = { | ||||
| } | ||||
| 
 | ||||
| if (__DEV__) { | ||||
|   ;(KeepAlive as any).props = { | ||||
|   ;(KeepAliveImpl as any).props = { | ||||
|     include: [String, RegExp, Array], | ||||
|     exclude: [String, RegExp, Array], | ||||
|     max: [String, Number] | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // export the public type for h/tsx inference
 | ||||
| export const KeepAlive = (KeepAliveImpl as any) as { | ||||
|   new (): { | ||||
|     $props: KeepAliveProps | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function getName(comp: Component): string | void { | ||||
|   return (comp as FunctionalComponent).displayName || comp.name | ||||
| } | ||||
| @ -268,7 +275,7 @@ function registerKeepAliveHook( | ||||
|   if (target) { | ||||
|     let current = target.parent | ||||
|     while (current && current.parent) { | ||||
|       if (current.parent.type === KeepAlive) { | ||||
|       if (current.parent.type === KeepAliveImpl) { | ||||
|         injectToKeepAliveRoot(wrappedHook, type, target, current) | ||||
|       } | ||||
|       current = current.parent | ||||
|  | ||||
| @ -1,15 +1,27 @@ | ||||
| import { VNode, normalizeVNode, VNodeChild } from './vnode' | ||||
| import { ShapeFlags } from './shapeFlags' | ||||
| import { VNode, normalizeVNode, VNodeChild } from '../vnode' | ||||
| import { ShapeFlags } from '../shapeFlags' | ||||
| import { isFunction, isArray } from '@vue/shared' | ||||
| import { ComponentInternalInstance, handleSetupResult } from './component' | ||||
| import { Slots } from './componentSlots' | ||||
| import { RendererInternals } from './renderer' | ||||
| import { queuePostFlushCb, queueJob } from './scheduler' | ||||
| import { updateHOCHostEl } from './componentRenderUtils' | ||||
| import { handleError, ErrorCodes } from './errorHandling' | ||||
| import { pushWarningContext, popWarningContext } from './warning' | ||||
| import { ComponentInternalInstance, handleSetupResult } from '../component' | ||||
| import { Slots } from '../componentSlots' | ||||
| import { RendererInternals } from '../renderer' | ||||
| import { queuePostFlushCb, queueJob } from '../scheduler' | ||||
| import { updateHOCHostEl } from '../componentRenderUtils' | ||||
| import { handleError, ErrorCodes } from '../errorHandling' | ||||
| import { pushWarningContext, popWarningContext } from '../warning' | ||||
| 
 | ||||
| export const Suspense = { | ||||
| export interface SuspenseProps { | ||||
|   onResolve?: () => void | ||||
|   onRecede?: () => void | ||||
| } | ||||
| 
 | ||||
| // Suspense exposes a component-like API, and is treated like a component
 | ||||
| // in the compiler, but internally it's a special built-in type that hooks
 | ||||
| // directly into the renderer.
 | ||||
| export const SuspenseImpl = { | ||||
|   // In order to make Suspense tree-shakable, we need to avoid importing it
 | ||||
|   // directly in the renderer. The renderer checks for the __isSuspense flag
 | ||||
|   // on a vnode's type and calls the `process` method, passing in renderer
 | ||||
|   // internals.
 | ||||
|   __isSuspense: true, | ||||
|   process( | ||||
|     n1: VNode | null, | ||||
| @ -49,6 +61,14 @@ export const Suspense = { | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // Force-casted public typing for h and TSX props inference
 | ||||
| export const Suspense = ((__FEATURE_SUSPENSE__ | ||||
|   ? SuspenseImpl | ||||
|   : null) as any) as { | ||||
|   __isSuspense: true | ||||
|   new (): { $props: SuspenseProps } | ||||
| } | ||||
| 
 | ||||
| function mountSuspense( | ||||
|   n2: VNode, | ||||
|   container: object, | ||||
| @ -5,9 +5,9 @@ import { | ||||
|   VNodeChildren, | ||||
|   Fragment, | ||||
|   Portal, | ||||
|   isVNode, | ||||
|   Suspense | ||||
|   isVNode | ||||
| } from './vnode' | ||||
| import { Suspense, SuspenseProps } from './components/Suspense' | ||||
| import { isObject, isArray } from '@vue/shared' | ||||
| import { RawSlots } from './componentSlots' | ||||
| import { FunctionalComponent } from './component' | ||||
| @ -67,6 +67,9 @@ type RawChildren = | ||||
| 
 | ||||
| // fake constructor type returned from `createComponent`
 | ||||
| interface Constructor<P = any> { | ||||
|   __isFragment?: never | ||||
|   __isPortal?: never | ||||
|   __isSuspense?: never | ||||
|   new (): { $props: P } | ||||
| } | ||||
| 
 | ||||
| @ -100,12 +103,7 @@ export function h( | ||||
| export function h(type: typeof Suspense, children?: RawChildren): VNode | ||||
| export function h( | ||||
|   type: typeof Suspense, | ||||
|   props?: | ||||
|     | (RawProps & { | ||||
|         onResolve?: () => void | ||||
|         onRecede?: () => void | ||||
|       }) | ||||
|     | null, | ||||
|   props?: (RawProps & SuspenseProps) | null, | ||||
|   children?: RawChildren | RawSlots | ||||
| ): VNode | ||||
| 
 | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| // Public API ------------------------------------------------------------------
 | ||||
| 
 | ||||
| export const version = __VERSION__ | ||||
| export * from './apiReactivity' | ||||
| export * from './apiWatch' | ||||
| export * from './apiLifecycle' | ||||
| @ -23,9 +24,10 @@ export { | ||||
|   createBlock | ||||
| } from './vnode' | ||||
| // VNode type symbols
 | ||||
| export { Text, Comment, Fragment, Portal, Suspense } from './vnode' | ||||
| export { Text, Comment, Fragment, Portal } from './vnode' | ||||
| // Internal Components
 | ||||
| export { KeepAlive } from './components/KeepAlive' | ||||
| export { Suspense, SuspenseProps } from './components/Suspense' | ||||
| export { KeepAlive, KeepAliveProps } from './components/KeepAlive' | ||||
| // VNode flags
 | ||||
| export { PublicShapeFlags as ShapeFlags } from './shapeFlags' | ||||
| import { PublicPatchFlags } from '@vue/shared' | ||||
| @ -111,6 +113,4 @@ export { | ||||
|   FunctionDirective, | ||||
|   DirectiveArguments | ||||
| } from './directives' | ||||
| export { SuspenseBoundary } from './rendererSuspense' | ||||
| 
 | ||||
| export const version = __VERSION__ | ||||
| export { SuspenseBoundary } from './components/Suspense' | ||||
|  | ||||
| @ -47,9 +47,9 @@ import { ComponentPublicInstance } from './componentProxy' | ||||
| import { App, createAppAPI } from './apiApp' | ||||
| import { | ||||
|   SuspenseBoundary, | ||||
|   Suspense, | ||||
|   queueEffectWithSuspense | ||||
| } from './rendererSuspense' | ||||
|   queueEffectWithSuspense, | ||||
|   SuspenseImpl | ||||
| } from './components/Suspense' | ||||
| import { ErrorCodes, callWithErrorHandling } from './errorHandling' | ||||
| import { KeepAliveSink } from './components/KeepAlive' | ||||
| 
 | ||||
| @ -265,7 +265,7 @@ export function createRenderer< | ||||
|             optimized | ||||
|           ) | ||||
|         } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { | ||||
|           ;(type as typeof Suspense).process( | ||||
|           ;(type as typeof SuspenseImpl).process( | ||||
|             n1, | ||||
|             n2, | ||||
|             container, | ||||
|  | ||||
| @ -16,31 +16,25 @@ import { RawSlots } from './componentSlots' | ||||
| import { ShapeFlags } from './shapeFlags' | ||||
| import { isReactive, Ref } from '@vue/reactivity' | ||||
| import { AppContext } from './apiApp' | ||||
| import { SuspenseBoundary } from './rendererSuspense' | ||||
| import { SuspenseBoundary } from './components/Suspense' | ||||
| import { DirectiveBinding } from './directives' | ||||
| import { Suspense as SuspenseImpl } from './rendererSuspense' | ||||
| import { SuspenseImpl } from './components/Suspense' | ||||
| 
 | ||||
| export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as any) as { | ||||
|   // type differentiator for h()
 | ||||
|   __isFragment: true | ||||
|   new (): { | ||||
|     $props: VNodeProps | ||||
|   } | ||||
| } | ||||
| export const Portal = (Symbol(__DEV__ ? 'Portal' : undefined) as any) as { | ||||
|   // type differentiator for h()
 | ||||
|   __isPortal: true | ||||
|   new (): { | ||||
|     $props: VNodeProps & { target: string | object } | ||||
|   } | ||||
| } | ||||
| export const Text = Symbol(__DEV__ ? 'Text' : undefined) | ||||
| export const Comment = Symbol(__DEV__ ? 'Comment' : undefined) | ||||
| 
 | ||||
| // Export Suspense with casting to avoid circular type dependency between
 | ||||
| // `suspense.ts` and `createRenderer.ts` in exported types.
 | ||||
| // A circular type dependency causes tsc to generate d.ts with dynmaic import()
 | ||||
| // calls using realtive paths, which works for separate d.ts files, but will
 | ||||
| // fail after d.ts rollup with API Extractor.
 | ||||
| const Suspense = ((__FEATURE_SUSPENSE__ ? SuspenseImpl : null) as any) as { | ||||
|   __isSuspense: true | ||||
| } | ||||
| export { Suspense } | ||||
| 
 | ||||
| export type VNodeTypes = | ||||
|   | string | ||||
|   | Component | ||||
| @ -48,12 +42,12 @@ export type VNodeTypes = | ||||
|   | typeof Portal | ||||
|   | typeof Text | ||||
|   | typeof Comment | ||||
|   | typeof Suspense | ||||
|   | typeof SuspenseImpl | ||||
| 
 | ||||
| export interface VNodeProps { | ||||
|   [key: string]: any | ||||
|   key?: string | number | ||||
|   ref?: string | Ref | ((ref: object) => void) | ||||
|   ref?: string | Ref | ((ref: object | null) => void) | ||||
| } | ||||
| 
 | ||||
| type VNodeChildAtom<HostNode, HostElement> = | ||||
|  | ||||
| @ -116,7 +116,7 @@ const classify = (str: string): string => | ||||
|   str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, '') | ||||
| 
 | ||||
| function formatComponentName(vnode: ComponentVNode, file?: string): string { | ||||
|   const Component = vnode.type | ||||
|   const Component = vnode.type as Component | ||||
|   let name = isFunction(Component) | ||||
|     ? Component.displayName || Component.name | ||||
|     : Component.name | ||||
|  | ||||
							
								
								
									
										36
									
								
								packages/runtime-dom/jsx.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										36
									
								
								packages/runtime-dom/jsx.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -1,3 +1,5 @@ | ||||
| import { Ref, ComponentPublicInstance } from '@vue/runtime-core' | ||||
| 
 | ||||
| // This code is based on https://github.com/wonderful-panda/vue-tsx-support
 | ||||
| // published under the MIT license.
 | ||||
| // Copyright by @wonderful-panda
 | ||||
| @ -740,7 +742,12 @@ type EventHandlers<E> = { | ||||
|   [K in StringKeyOf<E>]?: E[K] extends Function ? E[K] : (payload: E[K]) => void | ||||
| } | ||||
| 
 | ||||
| type ElementAttrs<T> = T & EventHandlers<Events> | ||||
| type ReservedProps = { | ||||
|   key?: string | number | ||||
|   ref?: string | Ref | ((ref: Element | ComponentPublicInstance | null) => void) | ||||
| } | ||||
| 
 | ||||
| type ElementAttrs<T> = T & EventHandlers<Events> & ReservedProps | ||||
| 
 | ||||
| type NativeElements = { | ||||
|   [K in StringKeyOf<IntrinsicElementAttributes>]: ElementAttrs< | ||||
| @ -748,16 +755,21 @@ type NativeElements = { | ||||
|   > | ||||
| } | ||||
| 
 | ||||
| declare namespace JSX { | ||||
|   interface Element {} | ||||
|   interface ElementClass { | ||||
|     $props: {} | ||||
|   } | ||||
|   interface ElementAttributesProperty { | ||||
|     $props: {} | ||||
|   } | ||||
|   interface IntrinsicElements extends NativeElements { | ||||
|     // allow arbitrary elements
 | ||||
|     [name: string]: any | ||||
| declare global { | ||||
|   namespace JSX { | ||||
|     interface Element {} | ||||
|     interface ElementClass { | ||||
|       $props: {} | ||||
|     } | ||||
|     interface ElementAttributesProperty { | ||||
|       $props: {} | ||||
|     } | ||||
|     interface IntrinsicElements extends NativeElements { | ||||
|       // allow arbitrary elements
 | ||||
|       [name: string]: any | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // suppress ts:2669
 | ||||
| export {} | ||||
|  | ||||
| @ -97,7 +97,7 @@ function createConfig(output, plugins = []) { | ||||
|         declaration: shouldEmitDeclarations, | ||||
|         declarationMap: shouldEmitDeclarations | ||||
|       }, | ||||
|       exclude: ['**/__tests__'] | ||||
|       exclude: ['**/__tests__', 'test-dts'] | ||||
|     } | ||||
|   }) | ||||
|   // we only need to check TS and generate declarations once for each build.
 | ||||
|  | ||||
							
								
								
									
										224
									
								
								test-dts/createComponent.test-d.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										224
									
								
								test-dts/createComponent.test-d.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,224 @@ | ||||
| import { describe } from './util' | ||||
| import { expectError, expectType } from 'tsd' | ||||
| import { createComponent, PropType, ref } from './index' | ||||
| 
 | ||||
| describe('with object props', () => { | ||||
|   interface ExpectedProps { | ||||
|     a?: number | undefined | ||||
|     b: string | ||||
|     bb: string | ||||
|     cc?: string[] | undefined | ||||
|     dd: string[] | ||||
|     ccc?: string[] | undefined | ||||
|     ddd: string[] | ||||
|   } | ||||
| 
 | ||||
|   const MyComponent = createComponent({ | ||||
|     props: { | ||||
|       a: Number, | ||||
|       // required should make property non-void
 | ||||
|       b: { | ||||
|         type: String, | ||||
|         required: true | ||||
|       }, | ||||
|       // default value should infer type and make it non-void
 | ||||
|       bb: { | ||||
|         default: 'hello' | ||||
|       }, | ||||
|       // explicit type casting
 | ||||
|       cc: Array as PropType<string[]>, | ||||
|       // required + type casting
 | ||||
|       dd: { | ||||
|         type: Array as PropType<string[]>, | ||||
|         required: true | ||||
|       }, | ||||
|       // explicit type casting with constructor
 | ||||
|       ccc: Array as () => string[], | ||||
|       // required + contructor type casting
 | ||||
|       ddd: { | ||||
|         type: Array as () => string[], | ||||
|         required: true | ||||
|       } | ||||
|     }, | ||||
|     setup(props) { | ||||
|       // type assertion. See https://github.com/SamVerschueren/tsd
 | ||||
|       expectType<ExpectedProps['a']>(props.a) | ||||
|       expectType<ExpectedProps['b']>(props.b) | ||||
|       expectType<ExpectedProps['bb']>(props.bb) | ||||
|       expectType<ExpectedProps['cc']>(props.cc) | ||||
|       expectType<ExpectedProps['dd']>(props.dd) | ||||
|       expectType<ExpectedProps['ccc']>(props.ccc) | ||||
|       expectType<ExpectedProps['ddd']>(props.ddd) | ||||
| 
 | ||||
|       // setup context
 | ||||
|       return { | ||||
|         c: ref(1), | ||||
|         d: { | ||||
|           e: ref('hi') | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     render() { | ||||
|       const props = this.$props | ||||
|       expectType<ExpectedProps['a']>(props.a) | ||||
|       expectType<ExpectedProps['b']>(props.b) | ||||
|       expectType<ExpectedProps['bb']>(props.bb) | ||||
|       expectType<ExpectedProps['cc']>(props.cc) | ||||
|       expectType<ExpectedProps['dd']>(props.dd) | ||||
|       expectType<ExpectedProps['ccc']>(props.ccc) | ||||
|       expectType<ExpectedProps['ddd']>(props.ddd) | ||||
| 
 | ||||
|       // should also expose declared props on `this`
 | ||||
|       expectType<ExpectedProps['a']>(this.a) | ||||
|       expectType<ExpectedProps['b']>(this.b) | ||||
|       expectType<ExpectedProps['bb']>(this.bb) | ||||
|       expectType<ExpectedProps['cc']>(this.cc) | ||||
|       expectType<ExpectedProps['dd']>(this.dd) | ||||
|       expectType<ExpectedProps['ccc']>(this.ccc) | ||||
|       expectType<ExpectedProps['ddd']>(this.ddd) | ||||
| 
 | ||||
|       // assert setup context unwrapping
 | ||||
|       expectType<number>(this.c) | ||||
|       expectType<string>(this.d.e) | ||||
| 
 | ||||
|       return null | ||||
|     } | ||||
|   }) | ||||
| 
 | ||||
|   // Test TSX
 | ||||
|   expectType<JSX.Element>( | ||||
|     <MyComponent | ||||
|       a={1} | ||||
|       b="b" | ||||
|       bb="bb" | ||||
|       cc={['cc']} | ||||
|       dd={['dd']} | ||||
|       ccc={['ccc']} | ||||
|       ddd={['ddd']} | ||||
|       // should allow extraneous as attrs
 | ||||
|       class="bar" | ||||
|       // should allow key
 | ||||
|       key={'foo'} | ||||
|       // should allow ref
 | ||||
|       ref={'foo'} | ||||
|     /> | ||||
|   ) | ||||
| 
 | ||||
|   // missing required props
 | ||||
|   expectError(<MyComponent />) | ||||
| 
 | ||||
|   // wrong prop types
 | ||||
|   expectError( | ||||
|     <MyComponent a={'wrong type'} b="foo" dd={['foo']} ddd={['foo']} /> | ||||
|   ) | ||||
|   expectError(<MyComponent b="foo" dd={[123]} ddd={['foo']} />) | ||||
| }) | ||||
| 
 | ||||
| describe('type inference w/ optional props declaration', () => { | ||||
|   const MyComponent = createComponent({ | ||||
|     setup(_props: { msg: string }) { | ||||
|       return { | ||||
|         a: 1 | ||||
|       } | ||||
|     }, | ||||
|     render() { | ||||
|       expectType<string>(this.$props.msg) | ||||
|       expectError(this.msg) | ||||
|       expectType<number>(this.a) | ||||
|       return null | ||||
|     } | ||||
|   }) | ||||
| 
 | ||||
|   expectType<JSX.Element>(<MyComponent msg="foo" />) | ||||
|   expectError(<MyComponent />) | ||||
|   expectError(<MyComponent msg={1} />) | ||||
| }) | ||||
| 
 | ||||
| describe('type inference w/ direct setup function', () => { | ||||
|   const MyComponent = createComponent((_props: { msg: string }) => {}) | ||||
|   expectType<JSX.Element>(<MyComponent msg="foo" />) | ||||
|   expectError(<MyComponent />) | ||||
|   expectError(<MyComponent msg={1} />) | ||||
| }) | ||||
| 
 | ||||
| describe('type inference w/ array props declaration', () => { | ||||
|   createComponent({ | ||||
|     props: ['a', 'b'], | ||||
|     setup(props) { | ||||
|       props.a | ||||
|       props.b | ||||
|       return { | ||||
|         c: 1 | ||||
|       } | ||||
|     }, | ||||
|     render() { | ||||
|       expectType<{ a?: any; b?: any }>(this.$props) | ||||
|       expectType<any>(this.a) | ||||
|       expectType<any>(this.b) | ||||
|       expectType<number>(this.c) | ||||
|     } | ||||
|   }) | ||||
| }) | ||||
| 
 | ||||
| describe('type inference w/ options API', () => { | ||||
|   createComponent({ | ||||
|     props: { a: Number }, | ||||
|     setup() { | ||||
|       return { | ||||
|         b: 123 | ||||
|       } | ||||
|     }, | ||||
|     data() { | ||||
|       // Limitation: we cannot expose the return result of setup() on `this`
 | ||||
|       // here in data() - somehow that would mess up the inference
 | ||||
|       expectType<number | undefined>(this.a) | ||||
|       return { | ||||
|         c: this.a || 123 | ||||
|       } | ||||
|     }, | ||||
|     computed: { | ||||
|       d(): number { | ||||
|         expectType<number>(this.b) | ||||
|         return this.b + 1 | ||||
|       } | ||||
|     }, | ||||
|     watch: { | ||||
|       a() { | ||||
|         expectType<number>(this.b) | ||||
|         this.b + 1 | ||||
|       } | ||||
|     }, | ||||
|     created() { | ||||
|       // props
 | ||||
|       expectType<number | undefined>(this.a) | ||||
|       // returned from setup()
 | ||||
|       expectType<number>(this.b) | ||||
|       // returned from data()
 | ||||
|       expectType<number>(this.c) | ||||
|       // computed
 | ||||
|       expectType<number>(this.d) | ||||
|     }, | ||||
|     methods: { | ||||
|       doSomething() { | ||||
|         // props
 | ||||
|         expectType<number | undefined>(this.a) | ||||
|         // returned from setup()
 | ||||
|         expectType<number>(this.b) | ||||
|         // returned from data()
 | ||||
|         expectType<number>(this.c) | ||||
|         // computed
 | ||||
|         expectType<number>(this.d) | ||||
|       } | ||||
|     }, | ||||
|     render() { | ||||
|       // props
 | ||||
|       expectType<number | undefined>(this.a) | ||||
|       // returned from setup()
 | ||||
|       expectType<number>(this.b) | ||||
|       // returned from data()
 | ||||
|       expectType<number>(this.c) | ||||
|       // computed
 | ||||
|       expectType<number>(this.d) | ||||
|     } | ||||
|   }) | ||||
| }) | ||||
| @ -1,85 +1,117 @@ | ||||
| // This file tests a number of cases that *should* fail using tsd:
 | ||||
| // https://github.com/SamVerschueren/tsd
 | ||||
| // It will probably show up red in VSCode, and it's intended. We cannot use
 | ||||
| // directives like @ts-ignore or @ts-nocheck since that would suppress the
 | ||||
| // errors that should be caught.
 | ||||
| 
 | ||||
| import { describe } from './util' | ||||
| import { expectError } from 'tsd' | ||||
| import { h, createComponent, ref, Fragment, Portal, Suspense } from './index' | ||||
| 
 | ||||
| // h inference w/ element
 | ||||
| // key
 | ||||
| h('div', { key: 1 }) | ||||
| h('div', { key: 'foo' }) | ||||
| expectError(h('div', { key: [] })) | ||||
| expectError(h('div', { key: {} })) | ||||
| // ref
 | ||||
| h('div', { ref: 'foo' }) | ||||
| h('div', { ref: ref(null) }) | ||||
| h('div', { ref: el => {} }) | ||||
| expectError(h('div', { ref: [] })) | ||||
| expectError(h('div', { ref: {} })) | ||||
| expectError(h('div', { ref: 123 })) | ||||
| 
 | ||||
| // h inference w/ Fragment
 | ||||
| // only accepts array children
 | ||||
| h(Fragment, ['hello']) | ||||
| h(Fragment, { key: 123 }, ['hello']) | ||||
| expectError(h(Fragment, 'foo')) | ||||
| expectError(h(Fragment, { key: 123 }, 'bar')) | ||||
| 
 | ||||
| // h inference w/ Portal
 | ||||
| h(Portal, { target: '#foo' }, 'hello') | ||||
| expectError(h(Portal)) | ||||
| expectError(h(Portal, {})) | ||||
| expectError(h(Portal, { target: '#foo' })) | ||||
| 
 | ||||
| // h inference w/ Suspense
 | ||||
| h(Suspense, { onRecede: () => {}, onResolve: () => {} }, 'hello') | ||||
| h(Suspense, 'foo') | ||||
| h(Suspense, () => 'foo') | ||||
| h(Suspense, null, { | ||||
|   default: () => 'foo' | ||||
| describe('h inference w/ element', () => { | ||||
|   // key
 | ||||
|   h('div', { key: 1 }) | ||||
|   h('div', { key: 'foo' }) | ||||
|   expectError(h('div', { key: [] })) | ||||
|   expectError(h('div', { key: {} })) | ||||
|   // ref
 | ||||
|   h('div', { ref: 'foo' }) | ||||
|   h('div', { ref: ref(null) }) | ||||
|   h('div', { ref: el => {} }) | ||||
|   expectError(h('div', { ref: [] })) | ||||
|   expectError(h('div', { ref: {} })) | ||||
|   expectError(h('div', { ref: 123 })) | ||||
| }) | ||||
| expectError(h(Suspense, { onResolve: 1 })) | ||||
| 
 | ||||
| // h inference w/ functional component
 | ||||
| const Func = (_props: { foo: string; bar?: number }) => '' | ||||
| h(Func, { foo: 'hello' }) | ||||
| h(Func, { foo: 'hello', bar: 123 }) | ||||
| expectError(h(Func, { foo: 123 })) | ||||
| expectError(h(Func, {})) | ||||
| expectError(h(Func, { bar: 123 })) | ||||
| describe('h inference w/ Fragment', () => { | ||||
|   // only accepts array children
 | ||||
|   h(Fragment, ['hello']) | ||||
|   h(Fragment, { key: 123 }, ['hello']) | ||||
|   expectError(h(Fragment, 'foo')) | ||||
|   expectError(h(Fragment, { key: 123 }, 'bar')) | ||||
| }) | ||||
| 
 | ||||
| // h inference w/ plain object component
 | ||||
| const Foo = { | ||||
|   props: { | ||||
|     foo: String | ||||
|   } | ||||
| } | ||||
| describe('h inference w/ Portal', () => { | ||||
|   h(Portal, { target: '#foo' }, 'hello') | ||||
|   expectError(h(Portal)) | ||||
|   expectError(h(Portal, {})) | ||||
|   expectError(h(Portal, { target: '#foo' })) | ||||
| }) | ||||
| 
 | ||||
| h(Foo, { foo: 'ok' }) | ||||
| h(Foo, { foo: 'ok', class: 'extra' }) | ||||
| // should fail on wrong type
 | ||||
| expectError(h(Foo, { foo: 1 })) | ||||
| describe('h inference w/ Suspense', () => { | ||||
|   h(Suspense, { onRecede: () => {}, onResolve: () => {} }, 'hello') | ||||
|   h(Suspense, 'foo') | ||||
|   h(Suspense, () => 'foo') | ||||
|   h(Suspense, null, { | ||||
|     default: () => 'foo' | ||||
|   }) | ||||
|   expectError(h(Suspense, { onResolve: 1 })) | ||||
| }) | ||||
| 
 | ||||
| // h inference w/ createComponent
 | ||||
| const Bar = createComponent({ | ||||
|   props: { | ||||
|     foo: String, | ||||
|     bar: { | ||||
|       type: Number, | ||||
|       required: true | ||||
| describe('h inference w/ functional component', () => { | ||||
|   const Func = (_props: { foo: string; bar?: number }) => '' | ||||
|   h(Func, { foo: 'hello' }) | ||||
|   h(Func, { foo: 'hello', bar: 123 }) | ||||
|   expectError(h(Func, { foo: 123 })) | ||||
|   expectError(h(Func, {})) | ||||
|   expectError(h(Func, { bar: 123 })) | ||||
| }) | ||||
| 
 | ||||
| describe('h inference w/ plain object component', () => { | ||||
|   const Foo = { | ||||
|     props: { | ||||
|       foo: String | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   h(Foo, { foo: 'ok' }) | ||||
|   h(Foo, { foo: 'ok', class: 'extra' }) | ||||
|   // should fail on wrong type
 | ||||
|   expectError(h(Foo, { foo: 1 })) | ||||
| }) | ||||
| 
 | ||||
| h(Bar, { bar: 1 }) | ||||
| h(Bar, { bar: 1, foo: 'ok' }) | ||||
| // should allow extraneous props (attrs fallthrough)
 | ||||
| h(Bar, { bar: 1, foo: 'ok', class: 'extra' }) | ||||
| // should fail on missing required prop
 | ||||
| expectError(h(Bar, {})) | ||||
| expectError(h(Bar, { foo: 'ok' })) | ||||
| // should fail on wrong type
 | ||||
| expectError(h(Bar, { bar: 1, foo: 1 })) | ||||
| describe('h inference w/ createComponent', () => { | ||||
|   const Foo = createComponent({ | ||||
|     props: { | ||||
|       foo: String, | ||||
|       bar: { | ||||
|         type: Number, | ||||
|         required: true | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
| 
 | ||||
|   h(Foo, { bar: 1 }) | ||||
|   h(Foo, { bar: 1, foo: 'ok' }) | ||||
|   // should allow extraneous props (attrs fallthrough)
 | ||||
|   h(Foo, { bar: 1, foo: 'ok', class: 'extra' }) | ||||
|   // should fail on missing required prop
 | ||||
|   expectError(h(Foo, {})) | ||||
|   expectError(h(Foo, { foo: 'ok' })) | ||||
|   // should fail on wrong type
 | ||||
|   expectError(h(Foo, { bar: 1, foo: 1 })) | ||||
| }) | ||||
| 
 | ||||
| describe('h inference w/ createComponent + optional props', () => { | ||||
|   const Foo = createComponent({ | ||||
|     setup(_props: { foo?: string; bar: number }) {} | ||||
|   }) | ||||
| 
 | ||||
|   h(Foo, { bar: 1 }) | ||||
|   h(Foo, { bar: 1, foo: 'ok' }) | ||||
|   // should allow extraneous props (attrs fallthrough)
 | ||||
|   h(Foo, { bar: 1, foo: 'ok', class: 'extra' }) | ||||
|   // should fail on missing required prop
 | ||||
|   expectError(h(Foo, {})) | ||||
|   expectError(h(Foo, { foo: 'ok' })) | ||||
|   // should fail on wrong type
 | ||||
|   expectError(h(Foo, { bar: 1, foo: 1 })) | ||||
| }) | ||||
| 
 | ||||
| describe('h inference w/ createComponent + direct function', () => { | ||||
|   const Foo = createComponent((_props: { foo?: string; bar: number }) => {}) | ||||
| 
 | ||||
|   h(Foo, { bar: 1 }) | ||||
|   h(Foo, { bar: 1, foo: 'ok' }) | ||||
|   // should allow extraneous props (attrs fallthrough)
 | ||||
|   h(Foo, { bar: 1, foo: 'ok', class: 'extra' }) | ||||
|   // should fail on missing required prop
 | ||||
|   expectError(h(Foo, {})) | ||||
|   expectError(h(Foo, { foo: 'ok' })) | ||||
|   // should fail on wrong type
 | ||||
|   expectError(h(Foo, { bar: 1, foo: 1 })) | ||||
| }) | ||||
|  | ||||
							
								
								
									
										6
									
								
								test-dts/index.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								test-dts/index.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -1 +1,7 @@ | ||||
| // This directory contains a number of d.ts assertions using tsd:
 | ||||
| // https://github.com/SamVerschueren/tsd
 | ||||
| // The tests checks type errors and will probably show up red in VSCode, and
 | ||||
| // it's intended. We cannot use directives like @ts-ignore or @ts-nocheck since
 | ||||
| // that would suppress the errors that should be caught.
 | ||||
| 
 | ||||
| export * from '@vue/runtime-dom' | ||||
|  | ||||
							
								
								
									
										41
									
								
								test-dts/tsx.test-d.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								test-dts/tsx.test-d.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | ||||
| // TSX w/ createComponent is tested in createComponent.test-d.tsx
 | ||||
| 
 | ||||
| import { expectError, expectType } from 'tsd' | ||||
| import { KeepAlive, Suspense, Fragment, Portal } from '@vue/runtime-dom' | ||||
| 
 | ||||
| expectType<JSX.Element>(<div />) | ||||
| expectType<JSX.Element>(<div id="foo" />) | ||||
| expectType<JSX.Element>(<input value="foo" />) | ||||
| 
 | ||||
| // unknown prop
 | ||||
| expectError(<div foo="bar" />) | ||||
| 
 | ||||
| // allow key/ref on arbitrary element
 | ||||
| expectType<JSX.Element>(<div key="foo" />) | ||||
| expectType<JSX.Element>(<div ref="bar" />) | ||||
| 
 | ||||
| expectType<JSX.Element>( | ||||
|   <input | ||||
|     onInput={e => { | ||||
|       // infer correct event type
 | ||||
|       expectType<EventTarget | null>(e.target) | ||||
|     }} | ||||
|   /> | ||||
| ) | ||||
| 
 | ||||
| // built-in types
 | ||||
| expectType<JSX.Element>(<Fragment />) | ||||
| expectType<JSX.Element>(<Fragment key="1" />) | ||||
| 
 | ||||
| expectType<JSX.Element>(<Portal target="#foo" />) | ||||
| // target is required
 | ||||
| expectError(<Portal />) | ||||
| 
 | ||||
| // KeepAlive
 | ||||
| expectType<JSX.Element>(<KeepAlive include="foo" exclude={['a']} />) | ||||
| expectError(<KeepAlive include={123} />) | ||||
| 
 | ||||
| // Suspense
 | ||||
| expectType<JSX.Element>(<Suspense />) | ||||
| expectType<JSX.Element>(<Suspense onResolve={() => {}} onRecede={() => {}} />) | ||||
| expectError(<Suspense onResolve={123} />) | ||||
							
								
								
									
										4
									
								
								test-dts/util.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								test-dts/util.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| // aesthetic utility for making test-d.ts look more like actual tests
 | ||||
| // and makes it easier to navigate test cases with folding
 | ||||
| // it's a noop since test-d.ts files are not actually run.
 | ||||
| export function describe(_name: string, _fn: () => void) {} | ||||
| @ -15,7 +15,7 @@ | ||||
|     "resolveJsonModule": true, | ||||
|     "esModuleInterop": true, | ||||
|     "removeComments": false, | ||||
|     "jsx": "react", | ||||
|     "jsx": "preserve", | ||||
|     "lib": ["esnext", "dom"], | ||||
|     "types": ["jest", "node"], | ||||
|     "rootDir": ".", | ||||
| @ -35,6 +35,7 @@ | ||||
|     "packages/global.d.ts", | ||||
|     "packages/runtime-dom/jsx.d.ts", | ||||
|     "packages/*/src", | ||||
|     "packages/*/__tests__" | ||||
|     "packages/*/__tests__", | ||||
|     "test-dts" | ||||
|   ] | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user