From 24bedf9f40f6149684940e132f57226d537680f8 Mon Sep 17 00:00:00 2001 From: rudy-xhd Date: Sat, 31 Dec 2022 23:01:17 +0800 Subject: [PATCH 01/20] feat(types): infer attrs in `defineComponent` --- packages/dts-test/defineComponent.test-d.tsx | 123 +++++++++++++++++- .../runtime-core/src/apiDefineComponent.ts | 54 +++++--- packages/runtime-core/src/apiSetupHelpers.ts | 7 +- packages/runtime-core/src/component.ts | 9 +- packages/runtime-core/src/componentOptions.ts | 42 +++++- .../src/componentPublicInstance.ts | 25 ++-- packages/runtime-core/src/index.ts | 3 +- 7 files changed, 217 insertions(+), 46 deletions(-) diff --git a/packages/dts-test/defineComponent.test-d.tsx b/packages/dts-test/defineComponent.test-d.tsx index 7466249e10f..073020af3fc 100644 --- a/packages/dts-test/defineComponent.test-d.tsx +++ b/packages/dts-test/defineComponent.test-d.tsx @@ -10,6 +10,7 @@ import { SetupContext, h, SlotsType, + AttrsType, Slots, VNode } from 'vue' @@ -1043,7 +1044,7 @@ describe('inject', () => { expectType(this.foo) expectType(this.bar) // @ts-expect-error - this.foobar = 1 + expectError((this.foobar = 1)) } }) @@ -1055,7 +1056,7 @@ describe('inject', () => { expectType(this.foo) expectType(this.bar) // @ts-expect-error - this.foobar = 1 + expectError((this.foobar = 1)) } }) @@ -1075,7 +1076,7 @@ describe('inject', () => { expectType(this.foo) expectType(this.bar) // @ts-expect-error - this.foobar = 1 + expectError((this.foobar = 1)) } }) @@ -1084,9 +1085,9 @@ describe('inject', () => { props: ['a', 'b'], created() { // @ts-expect-error - this.foo = 1 + expectError((this.foo = 1)) // @ts-expect-error - this.bar = 1 + expectError((this.bar = 1)) } }) }) @@ -1188,6 +1189,118 @@ describe('async setup', () => { vm.a = 2 }) +describe('define attrs', () => { + test('define attrs w/ object props', () => { + type CompAttrs = { + bar: number + baz?: string + } + const MyComp = defineComponent( + { + props: { + foo: String + }, + attrs: Object as AttrsType, + created() { + expectType(this.$attrs.bar) + expectType(this.$attrs.baz) + } + } + ) + expectType() + }) + + test('define attrs w/ array props', () => { + type CompAttrs = { + bar: number + baz?: string + } + const MyComp = defineComponent( + { + props: ['foo'], + attrs: Object as AttrsType, + created() { + expectType(this.$attrs.bar) + expectType(this.$attrs.baz) + } + } + ) + expectType() + }) + + test('define attrs w/ no props', () => { + type CompAttrs = { + bar: number + baz?: string + } + const MyComp = defineComponent( + { + attrs: Object as AttrsType, + created() { + expectType(this.$attrs.bar) + expectType(this.$attrs.baz) + } + } + ) + expectType() + }) + + test('define attrs w/ function component', () => { + type CompAttrs = { + bar: number + baz?: string + } + const MyComp = defineComponent( + (props: { foo: string }, ctx) => { + expectType(ctx.attrs.bar) + expectType(ctx.attrs.bar) + expectType(ctx.attrs.baz) + return () => ( + // return a render function (both JSX and h() works) +
+ {props.foo} +
+ ) + }, { + attrs: Object as AttrsType + } + ) + expectType() + }) + + test('define attrs as low priority', () => { + type CompAttrs = { + foo: number + } + const MyComp = defineComponent( + { + props: { + foo: String + }, + attrs: Object as AttrsType, + created() { + // @ts-expect-error + console.log(this.$attrs.foo) + } + } + ) + expectType() + }) + + test('define attrs w/ default attrs such as class, style', () => { + const MyComp = defineComponent({ + props: { + foo: String + }, + created() { + expectType(this.$attrs.class) + expectType(this.$attrs.style) + } + }) + expectType() + }) +}) + // #5948 describe('DefineComponent should infer correct types when assigning to Component', () => { let component: Component diff --git a/packages/runtime-core/src/apiDefineComponent.ts b/packages/runtime-core/src/apiDefineComponent.ts index 272bb548751..26be3fe89be 100644 --- a/packages/runtime-core/src/apiDefineComponent.ts +++ b/packages/runtime-core/src/apiDefineComponent.ts @@ -8,7 +8,9 @@ import { RenderFunction, ComponentOptionsBase, ComponentInjectOptions, - ComponentOptions + ComponentOptions, + AttrsType, + UnwrapAttrsType } from './componentOptions' import { SetupContext, @@ -54,7 +56,8 @@ export type DefineComponent< PP = PublicProps, Props = ResolveProps, Defaults = ExtractDefaultPropTypes, - S extends SlotsType = {} + S extends SlotsType = {}, + Attrs extends AttrsType = {}, > = ComponentPublicInstanceConstructor< CreateComponentPublicInstance< Props, @@ -69,7 +72,8 @@ export type DefineComponent< Defaults, true, {}, - S + S, + Attrs > & Props > & @@ -86,7 +90,8 @@ export type DefineComponent< Defaults, {}, string, - S + S, + Attrs > & PP @@ -101,18 +106,20 @@ export function defineComponent< Props extends Record, E extends EmitsOptions = {}, EE extends string = string, - S extends SlotsType = {} + S extends SlotsType = {}, + Attrs extends AttrsType = {} >( setup: ( props: Props, - ctx: SetupContext + ctx: SetupContext ) => RenderFunction | Promise, options?: Pick & { props?: (keyof Props)[] emits?: E | EE[] - slots?: S + slots?: S, + attrs?: Attrs } -): (props: Props & EmitsToProps) => any +): (props: Props & EmitsToProps & UnwrapAttrsType) => any export function defineComponent< Props extends Record, E extends EmitsOptions = {}, @@ -144,10 +151,11 @@ export function defineComponent< E extends EmitsOptions = {}, EE extends string = string, S extends SlotsType = {}, + Attrs extends AttrsType = {}, I extends ComponentInjectOptions = {}, - II extends string = string + II extends string = string, >( - options: ComponentOptionsWithoutProps< + comp: ComponentOptionsWithoutProps< Props, RawBindings, D, @@ -159,7 +167,8 @@ export function defineComponent< EE, I, II, - S + S, + Attrs > ): DefineComponent< Props, @@ -174,7 +183,8 @@ export function defineComponent< PublicProps, ResolveProps, ExtractDefaultPropTypes, - S + S, + Attrs > // overload 3: object format with array props declaration @@ -191,11 +201,12 @@ export function defineComponent< E extends EmitsOptions = {}, EE extends string = string, S extends SlotsType = {}, + Attrs extends AttrsType = {}, I extends ComponentInjectOptions = {}, II extends string = string, Props = Readonly<{ [key in PropNames]?: any }> >( - options: ComponentOptionsWithArrayProps< + comp: ComponentOptionsWithArrayProps< PropNames, RawBindings, D, @@ -207,7 +218,8 @@ export function defineComponent< EE, I, II, - S + S, + Attrs > ): DefineComponent< Props, @@ -222,7 +234,8 @@ export function defineComponent< PublicProps, ResolveProps, ExtractDefaultPropTypes, - S + S, + Attrs > // overload 4: object format with object props declaration @@ -241,9 +254,10 @@ export function defineComponent< EE extends string = string, S extends SlotsType = {}, I extends ComponentInjectOptions = {}, - II extends string = string + II extends string = string, + Attrs extends AttrsType = {} >( - options: ComponentOptionsWithObjectProps< + comp: ComponentOptionsWithObjectProps< PropsOptions, RawBindings, D, @@ -255,7 +269,8 @@ export function defineComponent< EE, I, II, - S + S, + Attrs > ): DefineComponent< PropsOptions, @@ -270,7 +285,8 @@ export function defineComponent< PublicProps, ResolveProps, ExtractDefaultPropTypes, - S + S, + Attrs > // implementation, close to no-op diff --git a/packages/runtime-core/src/apiSetupHelpers.ts b/packages/runtime-core/src/apiSetupHelpers.ts index 93200667081..4256a588a7c 100644 --- a/packages/runtime-core/src/apiSetupHelpers.ts +++ b/packages/runtime-core/src/apiSetupHelpers.ts @@ -11,7 +11,8 @@ import { setCurrentInstance, SetupContext, createSetupContext, - unsetCurrentInstance + unsetCurrentInstance, + Data } from './component' import { EmitFn, EmitsOptions, ObjectEmitsOptions } from './componentEmits' import { @@ -349,8 +350,8 @@ export function useSlots(): SetupContext['slots'] { return getContext().slots } -export function useAttrs(): SetupContext['attrs'] { - return getContext().attrs +export function useAttrs(): SetupContext['attrs'] { + return getContext().attrs as T } export function useModel, K extends keyof T>( diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 57a53a39b76..3096236167f 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -40,10 +40,12 @@ import { AppContext, createAppContext, AppConfig } from './apiCreateApp' import { Directive, validateDirectiveName } from './directives' import { applyOptions, + AttrsType, ComponentOptions, ComputedOptions, MethodOptions, - resolveMergedOptions + resolveMergedOptions, + UnwrapAttrsType } from './componentOptions' import { EmitsOptions, @@ -184,10 +186,11 @@ type LifecycleHook = TFn[] | null // use `E extends any` to force evaluating type to fix #2362 export type SetupContext< E = EmitsOptions, - S extends SlotsType = {} + S extends SlotsType = {}, + Attrs extends AttrsType = {} > = E extends any ? { - attrs: Data + attrs: UnwrapAttrsType, slots: UnwrapSlotsType emit: EmitFn expose: (exposed?: Record) => void diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts index de4d304448a..c250b857d00 100644 --- a/packages/runtime-core/src/componentOptions.ts +++ b/packages/runtime-core/src/componentOptions.ts @@ -111,7 +111,8 @@ export interface ComponentOptionsBase< Defaults = {}, I extends ComponentInjectOptions = {}, II extends string = string, - S extends SlotsType = {} + S extends SlotsType = {}, + Attrs extends AttrsType = {} > extends LegacyOptions, ComponentInternalOptions, ComponentCustomOptions { @@ -141,6 +142,7 @@ export interface ComponentOptionsBase< inheritAttrs?: boolean emits?: (E | EE[]) & ThisType slots?: S + attrs?: Attrs // TODO infer public instance type based on exposed keys expose?: string[] serverPrefetch?(): void | Promise @@ -224,6 +226,7 @@ export type ComponentOptionsWithoutProps< I extends ComponentInjectOptions = {}, II extends string = string, S extends SlotsType = {}, + Attrs extends AttrsType = {}, PE = Props & EmitsToProps > = ComponentOptionsBase< PE, @@ -238,7 +241,8 @@ export type ComponentOptionsWithoutProps< {}, I, II, - S + S, + Attrs > & { props?: undefined } & ThisType< @@ -255,7 +259,8 @@ export type ComponentOptionsWithoutProps< {}, false, I, - S + S, + Attrs > > @@ -272,6 +277,7 @@ export type ComponentOptionsWithArrayProps< I extends ComponentInjectOptions = {}, II extends string = string, S extends SlotsType = {}, + Attrs extends AttrsType = {}, Props = Prettify>> > = ComponentOptionsBase< Props, @@ -286,7 +292,8 @@ export type ComponentOptionsWithArrayProps< {}, I, II, - S + S, + Attrs > & { props: PropNames[] } & ThisType< @@ -303,7 +310,8 @@ export type ComponentOptionsWithArrayProps< {}, false, I, - S + S, + Attrs > > @@ -320,6 +328,7 @@ export type ComponentOptionsWithObjectProps< I extends ComponentInjectOptions = {}, II extends string = string, S extends SlotsType = {}, + Attrs extends AttrsType = {}, Props = Prettify & EmitsToProps>>, Defaults = ExtractDefaultPropTypes > = ComponentOptionsBase< @@ -335,7 +344,8 @@ export type ComponentOptionsWithObjectProps< Defaults, I, II, - S + S, + Attrs > & { props: PropsOptions & ThisType } & ThisType< @@ -352,7 +362,8 @@ export type ComponentOptionsWithObjectProps< Defaults, false, I, - S + S, + Attrs > > @@ -406,6 +417,23 @@ export type ComponentOptionsMixin = ComponentOptionsBase< any > +declare const AttrSymbol: unique symbol +export type AttrsType = Record> = { + [AttrSymbol]?: T +} +export type UnwrapAttrsType< + S extends AttrsType, + T = NonNullable +> = [keyof S] extends [never] + ? Data + : Readonly< + Prettify<{ + [K in keyof T]: NonNullable extends (...args: any[]) => any + ? T[K] + : T[K] + }> + > + export type ComputedOptions = Record< string, ComputedGetter | WritableComputedOptions diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts index 7b552c8f92a..2643e27fb4b 100644 --- a/packages/runtime-core/src/componentPublicInstance.ts +++ b/packages/runtime-core/src/componentPublicInstance.ts @@ -1,4 +1,5 @@ import { + AllowedComponentProps, ComponentInternalInstance, Data, getExposeProxy, @@ -37,7 +38,9 @@ import { shouldCacheAccess, MergedComponentOptionsOverride, InjectToObject, - ComponentInjectOptions + ComponentInjectOptions, + AttrsType, + UnwrapAttrsType } from './componentOptions' import { EmitsOptions, EmitFn } from './componentEmits' import { SlotsType, UnwrapSlotsType } from './componentSlots' @@ -149,6 +152,7 @@ export type CreateComponentPublicInstance< MakeDefaultsOptional extends boolean = false, I extends ComponentInjectOptions = {}, S extends SlotsType = {}, + Attrs extends AttrsType = {}, PublicMixin = IntersectionMixin & IntersectionMixin, PublicP = UnwrapMixinsType & EnsureNonVoid

, PublicB = UnwrapMixinsType & EnsureNonVoid, @@ -182,10 +186,12 @@ export type CreateComponentPublicInstance< Defaults, {}, string, - S + S, + Attrs >, I, - S + S, + Attrs > // public properties exposed on the proxy, which is used as the render context @@ -202,14 +208,17 @@ export type ComponentPublicInstance< MakeDefaultsOptional extends boolean = false, Options = ComponentOptionsBase, I extends ComponentInjectOptions = {}, - S extends SlotsType = {} + S extends SlotsType = {}, + Attrs extends AttrsType = {} > = { $: ComponentInternalInstance $data: D - $props: MakeDefaultsOptional extends true - ? Partial & Omit & PublicProps, keyof Defaults> - : Prettify

& PublicProps - $attrs: Data + $props: Prettify< + MakeDefaultsOptional extends true + ? Partial & Omit

& Omit, keyof (P & PublicProps)> + : P & PublicProps & Omit, keyof (P & PublicProps)> + > + $attrs: Omit, keyof (P & PublicProps)> & AllowedComponentProps $refs: Data $slots: UnwrapSlotsType $root: ComponentPublicInstance | null diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 85bd92e75b0..9fae025d70e 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -246,7 +246,8 @@ export type { MethodOptions, ComputedOptions, RuntimeCompilerOptions, - ComponentInjectOptions + ComponentInjectOptions, + AttrsType } from './componentOptions' export type { EmitsOptions, ObjectEmitsOptions } from './componentEmits' export type { From e874c4f1a60d23cb6b21a66571a1efdd7c161c64 Mon Sep 17 00:00:00 2001 From: rudy-xhd Date: Sun, 1 Jan 2023 12:50:12 +0800 Subject: [PATCH 02/20] feat(compiler-sfc): `useAttrs` support generic type --- .../__snapshots__/compileScript.spec.ts.snap | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap b/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap index 949c9946d9f..2465105a8cb 100644 --- a/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap +++ b/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap @@ -1237,6 +1237,39 @@ return { get aa() { return aa }, set aa(v) { aa = v }, bb, cc, dd, get a() { ret }" `; +exports[`SFC compile + `) + assertCode(content) + expect(content).toMatch(`const attrs = _useAttrs()`) + expect(content).not.toMatch('defineAttrs') + }) + + test('w/o return value', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).not.toMatch('defineAttrs') + expect(content).not.toMatch(`_useAttrs`) + }) + + test('w/o generic params', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`const attrs = _useAttrs()`) + expect(content).not.toMatch('defineAttrs') + }) +}) diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index 2a33f69936d..f5c902f3df9 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -53,6 +53,7 @@ import { import { analyzeScriptBindings } from './script/analyzeScriptBindings' import { isImportUsed } from './script/importUsageCheck' import { processAwait } from './script/topLevelAwait' +import { processDefineAttrs } from './script/defineAttrs' export interface SFCScriptCompileOptions { /** @@ -512,7 +513,8 @@ export function compileScript( processDefineProps(ctx, expr) || processDefineEmits(ctx, expr) || processDefineOptions(ctx, expr) || - processDefineSlots(ctx, expr) + processDefineSlots(ctx, expr) || + processDefineAttrs(ctx, expr) ) { ctx.s.remove(node.start! + startOffset, node.end! + startOffset) } else if (processDefineExpose(ctx, expr)) { @@ -550,7 +552,8 @@ export function compileScript( !isDefineProps && processDefineEmits(ctx, init, decl.id) !isDefineEmits && (processDefineSlots(ctx, init, decl.id) || - processDefineModel(ctx, init, decl.id)) + processDefineModel(ctx, init, decl.id) || + processDefineAttrs(ctx, init, decl.id)) if ( isDefineProps && diff --git a/packages/compiler-sfc/src/script/context.ts b/packages/compiler-sfc/src/script/context.ts index 692eab3ab9e..0d1607ab46d 100644 --- a/packages/compiler-sfc/src/script/context.ts +++ b/packages/compiler-sfc/src/script/context.ts @@ -36,6 +36,7 @@ export class ScriptCompileContext { hasDefineOptionsCall = false hasDefineSlotsCall = false hasDefineModelCall = false + hasDefineAttrsCall = false // defineProps propsCall: CallExpression | undefined diff --git a/packages/compiler-sfc/src/script/defineAttrs.ts b/packages/compiler-sfc/src/script/defineAttrs.ts new file mode 100644 index 00000000000..d2eda8d9962 --- /dev/null +++ b/packages/compiler-sfc/src/script/defineAttrs.ts @@ -0,0 +1,33 @@ +import { LVal, Node } from '@babel/types' +import { isCallOf } from './utils' +import { ScriptCompileContext } from './context' + +export const DEFINE_ATTRS = 'defineAttrs' + +export function processDefineAttrs( + ctx: ScriptCompileContext, + node: Node, + declId?: LVal +): boolean { + if (!isCallOf(node, DEFINE_ATTRS)) { + return false + } + if (ctx.hasDefineAttrsCall) { + ctx.error(`duplicate ${DEFINE_ATTRS}() call`, node) + } + ctx.hasDefineAttrsCall = true + + if (node.arguments.length > 0) { + ctx.error(`${DEFINE_ATTRS}() cannot accept arguments`, node) + } + + if (declId) { + ctx.s.overwrite( + ctx.startOffset! + node.start!, + ctx.startOffset! + node.end!, + `${ctx.helper('useAttrs')}()` + ) + } + + return true +} diff --git a/packages/runtime-core/src/apiSetupHelpers.ts b/packages/runtime-core/src/apiSetupHelpers.ts index 93200667081..b673d20e7f8 100644 --- a/packages/runtime-core/src/apiSetupHelpers.ts +++ b/packages/runtime-core/src/apiSetupHelpers.ts @@ -18,7 +18,8 @@ import { ComponentOptionsMixin, ComponentOptionsWithoutProps, ComputedOptions, - MethodOptions + MethodOptions, + StrictUnwrapAttrsType } from './componentOptions' import { ComponentPropsOptions, @@ -215,6 +216,15 @@ export function defineSlots< return null as any } +export function defineAttrs< + Attrs extends Record = Record +>(): StrictUnwrapAttrsType { + if (__DEV__) { + warnRuntimeUsage(`defineAttrs`) + } + return null as any +} + /** * (**Experimental**) Vue `