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 `