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..c4686328dd8
--- /dev/null
+++ b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineAttrs.spec.ts.snap
@@ -0,0 +1,48 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`defineAttrs() > basic usage 1`] = `
+"import { useAttrs as _useAttrs, defineComponent as _defineComponent } from 'vue'
+
+const __sfc__ = /*#__PURE__*/_defineComponent({
+ setup(__props, { expose: __expose }) {
+ __expose();
+
+ const attrs = _useAttrs()
+
+return { attrs }
+}
+
+})
+export default __sfc__"
+`;
+
+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'
+
+const __sfc__ = /*#__PURE__*/_defineComponent({
+ setup(__props, { expose: __expose }) {
+ __expose();
+
+
+
+return { }
+}
+
+})
+export default __sfc__"
+`;
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..3f8262ed4f0 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 {
/**
@@ -164,6 +165,7 @@ export function compileScript(
const scopeId = options.id ? options.id.replace(/^data-v-/, '') : ''
const scriptLang = script && script.lang
const scriptSetupLang = scriptSetup && scriptSetup.lang
+ const exportName = options.genDefaultAs || '__sfc__'
// TODO remove in 3.4
const enableReactivityTransform = !!options.reactivityTransform
@@ -512,7 +514,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 +553,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 &&
@@ -948,9 +952,10 @@ export function compileScript(
}
// 11. finalize default export
- const genDefaultAs = options.genDefaultAs
- ? `const ${options.genDefaultAs} =`
- : `export default`
+ let genDefaultAs =
+ options.genDefaultAs || ctx.attrsTypeDecl
+ ? `const ${exportName} =`
+ : `export default`
let runtimeOptions = ``
if (!ctx.hasDefaultExportName && filename && filename !== DEFAULT_FILENAME) {
@@ -1020,6 +1025,9 @@ export function compileScript(
ctx.s.appendRight(endOffset, `}`)
}
}
+ if (ctx.attrsTypeDecl) {
+ ctx.s.appendRight(endOffset, `\nexport default ${exportName}\n`)
+ }
// 12. finalize Vue helper imports
if (ctx.helperImports.size > 0) {
diff --git a/packages/compiler-sfc/src/script/context.ts b/packages/compiler-sfc/src/script/context.ts
index 5fe09d28a42..5b143e9b2b7 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
@@ -58,6 +59,9 @@ export class ScriptCompileContext {
// defineOptions
optionsRuntimeDecl: Node | undefined
+ // defineAttrs
+ attrsTypeDecl: Node | undefined
+
// codegen
bindingMetadata: BindingMetadata = {}
helperImports: Set = new Set()
diff --git a/packages/compiler-sfc/src/script/defineAttrs.ts b/packages/compiler-sfc/src/script/defineAttrs.ts
new file mode 100644
index 00000000000..44581096f8c
--- /dev/null
+++ b/packages/compiler-sfc/src/script/defineAttrs.ts
@@ -0,0 +1,36 @@
+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)
+ }
+ const type =
+ (node.typeParameters && node.typeParameters.params[0]) || undefined
+ ctx.attrsTypeDecl = type
+
+ 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 `