diff --git a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineAttrs.spec.ts.snap b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineAttrs.spec.ts.snap new file mode 100644 index 00000000000..80d066e8a67 --- /dev/null +++ b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineAttrs.spec.ts.snap @@ -0,0 +1,46 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`defineAttrs() > basic usage 1`] = ` +"import { useAttrs as _useAttrs, defineComponent as _defineComponent } from 'vue' + +export default /*#__PURE__*/_defineComponent({ + setup(__props, { expose: __expose }) { + __expose(); + + const attrs = _useAttrs() + +return { attrs } +} + +})" +`; + +exports[`defineAttrs() > w/o generic params 1`] = ` +"import { useAttrs as _useAttrs } from 'vue' + +export default { + setup(__props, { expose: __expose }) { + __expose(); + + const attrs = _useAttrs() + +return { attrs } +} + +}" +`; + +exports[`defineAttrs() > w/o return value 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' + +export default /*#__PURE__*/_defineComponent({ + setup(__props, { expose: __expose }) { + __expose(); + + + +return { } +} + +})" +`; diff --git a/packages/compiler-sfc/__tests__/compileScript/defineAttrs.spec.ts b/packages/compiler-sfc/__tests__/compileScript/defineAttrs.spec.ts new file mode 100644 index 00000000000..e155e3c6bfd --- /dev/null +++ b/packages/compiler-sfc/__tests__/compileScript/defineAttrs.spec.ts @@ -0,0 +1,40 @@ +import { compileSFCScript as compile, assertCode } from '../utils' + +describe('defineAttrs()', () => { + test('basic usage', () => { + const { content } = 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/dts-test/defineComponent.test-d.tsx b/packages/dts-test/defineComponent.test-d.tsx index 7466249e10f..7a9e9737785 100644 --- a/packages/dts-test/defineComponent.test-d.tsx +++ b/packages/dts-test/defineComponent.test-d.tsx @@ -10,10 +10,13 @@ import { SetupContext, h, SlotsType, + AttrsType, Slots, - VNode + VNode, + ImgHTMLAttributes, + StyleValue } from 'vue' -import { describe, expectType, IsUnion } from './utils' +import { describe, expectType, IsUnion, test } from './utils' describe('with object props', () => { interface ExpectedProps { @@ -1188,6 +1191,270 @@ describe('async setup', () => { vm.a = 2 }) +describe('define attrs', () => { + test('define attrs w/ object props', () => { + const MyComp = defineComponent({ + props: { + foo: String + }, + attrs: Object as AttrsType<{ + bar?: number + }>, + created() { + expectType(this.$attrs.bar) + } + }) + expectType() + }) + + test('define attrs w/ array props', () => { + const MyComp = defineComponent({ + props: ['foo'], + attrs: Object as AttrsType<{ + bar?: number + }>, + created() { + expectType(this.$attrs.bar) + } + }) + expectType() + }) + + test('define attrs w/ no props', () => { + const MyComp = defineComponent({ + attrs: Object as AttrsType<{ + bar?: number + }>, + created() { + expectType(this.$attrs.bar) + } + }) + expectType() + }) + + test('define attrs w/ composition api', () => { + const MyComp = defineComponent({ + props: { + foo: { + type: String, + required: true + } + }, + attrs: Object as AttrsType<{ + bar?: number + }>, + setup(props, { attrs }) { + expectType(props.foo) + expectType(attrs.bar) + } + }) + expectType() + }) + + test('define attrs w/ functional component', () => { + const MyComp = defineComponent( + (props: { foo: string }, ctx) => { + expectType(ctx.attrs.bar) + return () => ( + // return a render function (both JSX and h() works) +
{props.foo}
+ ) + }, + { + attrs: Object as AttrsType<{ + bar?: number + }> + } + ) + expectType() + }) + + test('define attrs as low priority', () => { + const MyComp = defineComponent({ + props: { + foo: String + }, + attrs: Object as AttrsType<{ + foo?: number + }>, + created() { + // @ts-expect-error + this.$attrs.foo + + expectType(this.foo) + } + }) + expectType() + }) + + test('define required attrs', () => { + const MyComp = defineComponent({ + attrs: Object as AttrsType<{ + bar: number + }>, + created() { + expectType(this.$attrs.bar) + } + }) + expectType() + // @ts-expect-error + expectType() + }) + + test('define no attrs w/ object props', () => { + const MyComp = defineComponent({ + props: { + foo: String + }, + created() { + expectType(this.$attrs.bar) + } + }) + // @ts-expect-error + expectType() + }) + + test('define no attrs w/ functional component', () => { + const MyComp = defineComponent((props: { foo: string }, ctx) => { + expectType(ctx.attrs.bar) + return () => ( + // return a render function (both JSX and h() works) +
{props.foo}
+ ) + }) + expectType() + // @ts-expect-error + expectType() + }) + test('wrap elements', () => { + const MyImg = defineComponent({ + props: { + foo: String + }, + attrs: Object as AttrsType, + created() { + expectType(this.$attrs.class) + expectType(this.$attrs.style) + }, + render() { + return + } + }) + expectType() + }) + + test('wrap components', () => { + const Child = defineComponent({ + props: { + foo: String + }, + emits: { + baz: (val: number) => true + }, + render() { + return
{this.foo}
+ } + }) + const Comp = defineComponent({ + props: { + bar: Number + }, + attrs: Object as AttrsType, + created() { + expectType(this.$attrs.class) + expectType(this.$attrs.style) + }, + render() { + return + } + }) + expectType( + { + expectType(val) + }} + /> + ) + }) + + test('wrap components w/ functional component', () => { + const Child = defineComponent((props: { foo: string }, ctx) => { + return () =>
{props.foo}
+ }) + const Comp = defineComponent({ + props: { + bar: Number + }, + attrs: Object as AttrsType, + created() { + expectType(this.$attrs.class) + expectType(this.$attrs.style) + }, + render() { + return + } + }) + expectType( + + ) + }) + test('ignore reserved props', () => { + defineComponent({ + attrs: Object as AttrsType<{ + bar?: number + }>, + created() { + // @ts-expect-error reserved props + this.$attrs.key + // @ts-expect-error reserved props + this.$attrs.ref + } + }) + defineComponent( + (props: { foo: string }, ctx) => { + // @ts-expect-error reserved props + ctx.attrs.key + // @ts-expect-error reserved props + ctx.attrs.ref + return () =>
{props.foo}
+ }, + { + attrs: Object as AttrsType<{ + bar?: number + }> + } + ) + }) + + test('always readonly', () => { + defineComponent({ + attrs: Object as AttrsType<{ + bar?: number + }>, + created() { + // @ts-expect-error readonly + this.$attrs.class = 'test' + } + }) + defineComponent( + (props: { foo: string }, ctx) => { + // @ts-expect-error readonly + ctx.attrs.bar = 1 + return () =>
{props.foo}
+ }, + { + attrs: Object as AttrsType<{ + bar?: number + }> + } + ) + }) +}) + // #5948 describe('DefineComponent should infer correct types when assigning to Component', () => { let component: Component diff --git a/packages/dts-test/defineCustomElement.test-d.ts b/packages/dts-test/defineCustomElement.test-d.ts index 4e7cf228372..5d56a65c0c8 100644 --- a/packages/dts-test/defineCustomElement.test-d.ts +++ b/packages/dts-test/defineCustomElement.test-d.ts @@ -1,5 +1,5 @@ -import { defineCustomElement } from 'vue' -import { expectType, describe } from './utils' +import { defineCustomElement, AttrsType } from 'vue' +import { describe, expectType, test } from './utils' describe('inject', () => { // with object inject @@ -62,3 +62,96 @@ describe('inject', () => { } }) }) + +describe('define attrs', () => { + test('define attrs w/ object props', () => { + defineCustomElement({ + props: { + foo: String + }, + attrs: Object as AttrsType<{ + bar?: number + }>, + created() { + expectType(this.$attrs.bar) + } + }) + }) + + test('define attrs w/ array props', () => { + defineCustomElement({ + props: ['foo'], + attrs: Object as AttrsType<{ + bar?: number + }>, + created() { + expectType(this.$attrs.bar) + } + }) + }) + + test('define attrs w/ no props', () => { + defineCustomElement({ + attrs: Object as AttrsType<{ + bar?: number + }>, + created() { + expectType(this.$attrs.bar) + } + }) + }) + + test('define attrs as low priority', () => { + defineCustomElement({ + props: { + foo: String + }, + attrs: Object as AttrsType<{ + foo: number + }>, + created() { + // @ts-expect-error + this.$attrs.foo + expectType(this.foo) + } + }) + }) + + test('define attrs w/ no attrs', () => { + defineCustomElement({ + props: { + foo: String + }, + created() { + expectType(this.$attrs.bar) + expectType(this.$attrs.baz) + } + }) + }) + + test('default attrs like class, style', () => { + defineCustomElement({ + props: { + foo: String + }, + created() { + expectType(this.$attrs.class) + expectType(this.$attrs.style) + } + }) + }) + + test('define required attrs', () => { + defineCustomElement({ + props: { + foo: String + }, + attrs: Object as AttrsType<{ + bar: number + }>, + created() { + expectType(this.$attrs.bar) + } + }) + }) +}) diff --git a/packages/runtime-core/src/apiDefineComponent.ts b/packages/runtime-core/src/apiDefineComponent.ts index 272bb548751..66f8de4b65b 100644 --- a/packages/runtime-core/src/apiDefineComponent.ts +++ b/packages/runtime-core/src/apiDefineComponent.ts @@ -8,12 +8,15 @@ import { RenderFunction, ComponentOptionsBase, ComponentInjectOptions, - ComponentOptions + ComponentOptions, + AttrsType, + UnwrapAttrsType } from './componentOptions' import { SetupContext, AllowedComponentProps, - ComponentCustomProps + ComponentCustomProps, + HasDefinedAttrs } from './component' import { ExtractPropTypes, @@ -54,7 +57,8 @@ export type DefineComponent< PP = PublicProps, Props = ResolveProps, Defaults = ExtractDefaultPropTypes, - S extends SlotsType = {} + S extends SlotsType = {}, + Attrs extends AttrsType = Record > = ComponentPublicInstanceConstructor< CreateComponentPublicInstance< Props, @@ -69,7 +73,8 @@ export type DefineComponent< Defaults, true, {}, - S + S, + Attrs > & Props > & @@ -86,7 +91,8 @@ export type DefineComponent< Defaults, {}, string, - S + S, + Attrs > & PP @@ -101,18 +107,23 @@ export function defineComponent< Props extends Record, E extends EmitsOptions = {}, EE extends string = string, - S extends SlotsType = {} + S extends SlotsType = {}, + Attrs extends AttrsType = Record, + PropsAttrs = HasDefinedAttrs extends true + ? UnwrapAttrsType> + : {} >( setup: ( props: Props, - ctx: SetupContext + ctx: SetupContext ) => RenderFunction | Promise, options?: Pick & { props?: (keyof Props)[] emits?: E | EE[] slots?: S + attrs?: Attrs } -): (props: Props & EmitsToProps) => any +): (props: Props & EmitsToProps & PropsAttrs) => any export function defineComponent< Props extends Record, E extends EmitsOptions = {}, @@ -144,6 +155,7 @@ export function defineComponent< E extends EmitsOptions = {}, EE extends string = string, S extends SlotsType = {}, + Attrs extends AttrsType = Record, I extends ComponentInjectOptions = {}, II extends string = string >( @@ -159,7 +171,8 @@ export function defineComponent< EE, I, II, - S + S, + Attrs > ): DefineComponent< Props, @@ -174,7 +187,8 @@ export function defineComponent< PublicProps, ResolveProps, ExtractDefaultPropTypes, - S + S, + Attrs > // overload 3: object format with array props declaration @@ -191,6 +205,7 @@ export function defineComponent< E extends EmitsOptions = {}, EE extends string = string, S extends SlotsType = {}, + Attrs extends AttrsType = Record, I extends ComponentInjectOptions = {}, II extends string = string, Props = Readonly<{ [key in PropNames]?: any }> @@ -207,7 +222,8 @@ export function defineComponent< EE, I, II, - S + S, + Attrs > ): DefineComponent< Props, @@ -222,7 +238,8 @@ export function defineComponent< PublicProps, ResolveProps, ExtractDefaultPropTypes, - S + S, + Attrs > // overload 4: object format with object props declaration @@ -241,7 +258,8 @@ export function defineComponent< EE extends string = string, S extends SlotsType = {}, I extends ComponentInjectOptions = {}, - II extends string = string + II extends string = string, + Attrs extends AttrsType = Record >( options: ComponentOptionsWithObjectProps< PropsOptions, @@ -255,7 +273,8 @@ export function defineComponent< EE, I, II, - S + S, + Attrs > ): DefineComponent< PropsOptions, @@ -270,7 +289,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..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 `