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 cfcc607c72d..fe51563dfd2 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 5fe09d28a42..030370ebbad 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 `