]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
refactor(compiler-sfc): destructure built-in properties ($emit,$attrs,$slots) in...
authorRizumu Ayaka <rizumu@ayaka.moe>
Wed, 20 Aug 2025 09:50:53 +0000 (17:50 +0800)
committerGitHub <noreply@github.com>
Wed, 20 Aug 2025 09:50:53 +0000 (17:50 +0800)
Co-authored-by: daiwei <daiwei521@126.com>
packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap
packages/compiler-sfc/__tests__/compileScript.spec.ts
packages/compiler-sfc/src/compileScript.ts
packages/compiler-sfc/src/parse.ts
packages/compiler-sfc/src/script/importUsageCheck.ts

index 2acac64b0fbce9c2e195936e31285e0a90dcd864..0c6f78a19a1e06fc2e160e706a53348c5ef6d56f 100644 (file)
@@ -823,6 +823,450 @@ return (_ctx, _cache) => {
 }"
 `;
 
+exports[`SFC compile <script setup> > inlineTemplate mode > destructure setup context for built-in properties > should alias __emit to $emit when defineEmits is used 1`] = `
+"import { openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
+
+
+export default {
+  emits: ['click'],
+  setup(__props, { emit: __emit }) {
+  const $emit = __emit
+
+            const emit = __emit
+          
+return (_ctx, _cache) => {
+  return (_openBlock(), _createElementBlock("div", {
+    onClick: _cache[0] || (_cache[0] = $event => ($emit('click')))
+  }))
+}
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> > inlineTemplate mode > destructure setup context for built-in properties > should alias __props to $props when $props is used 1`] = `
+"import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
+
+
+export default {
+  setup(__props) {
+  const $props = __props
+/* ... */
+return (_ctx, _cache) => {
+  return (_openBlock(), _createElementBlock("div", null, _toDisplayString($props), 1 /* TEXT */))
+}
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> > inlineTemplate mode > destructure setup context for built-in properties > should extract all built-in properties when they are used 1`] = `
+"import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
+
+
+export default {
+  setup(__props, { emit: $emit, attrs: $attrs, slots: $slots }) {
+  const $props = __props
+/* ... */
+return (_ctx, _cache) => {
+  return (_openBlock(), _createElementBlock("div", null, _toDisplayString($props) + _toDisplayString($slots) + _toDisplayString($emit) + _toDisplayString($attrs), 1 /* TEXT */))
+}
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> > inlineTemplate mode > destructure setup context for built-in properties > should extract attrs when $attrs is used 1`] = `
+"import { normalizeProps as _normalizeProps, guardReactiveProps as _guardReactiveProps, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
+
+
+export default {
+  setup(__props, { attrs: $attrs }) {
+/* ... */
+return (_ctx, _cache) => {
+  return (_openBlock(), _createElementBlock("div", _normalizeProps(_guardReactiveProps($attrs)), null, 16 /* FULL_PROPS */))
+}
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> > inlineTemplate mode > destructure setup context for built-in properties > should extract emit when $emit is used 1`] = `
+"import { openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
+
+
+export default {
+  setup(__props, { emit: $emit }) {
+/* ... */
+return (_ctx, _cache) => {
+  return (_openBlock(), _createElementBlock("div", {
+    onClick: _cache[0] || (_cache[0] = $event => ($emit('click')))
+  }))
+}
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> > inlineTemplate mode > destructure setup context for built-in properties > should extract slots when $slots is used 1`] = `
+"import { resolveComponent as _resolveComponent, openBlock as _openBlock, createBlock as _createBlock } from "vue"
+
+
+export default {
+  setup(__props, { slots: $slots }) {
+/* ... */
+return (_ctx, _cache) => {
+  const _component_Comp = _resolveComponent("Comp")
+
+  return (_openBlock(), _createBlock(_component_Comp, {
+    foo: $slots.foo
+  }, null, 8 /* PROPS */, ["foo"]))
+}
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> > inlineTemplate mode > destructure setup context for built-in properties > should not extract built-in properties when neither is used 1`] = `
+"import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
+
+
+export default {
+  setup(__props) {
+/* ... */
+return (_ctx, _cache) => {
+  return (_openBlock(), _createElementBlock("div", null, _toDisplayString(_ctx.msg), 1 /* TEXT */))
+}
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> > inlineTemplate mode > destructure setup context for built-in properties > user-defined properties override > should handle mixed defineEmits and user-defined $emit 1`] = `
+"import { unref as _unref, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
+
+
+export default {
+  emits: ['click'],
+  setup(__props, { emit: __emit }) {
+
+              const emit = __emit
+              let $emit
+            
+return (_ctx, _cache) => {
+  return (_openBlock(), _createElementBlock("div", {
+    onClick: _cache[0] || (_cache[0] = $event => (_unref($emit)('click')))
+  }, "click"))
+}
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> > inlineTemplate mode > destructure setup context for built-in properties > user-defined properties override > should not extract $attrs when user defines it 1`] = `
+"import { unref as _unref, normalizeProps as _normalizeProps, guardReactiveProps as _guardReactiveProps, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
+
+
+export default {
+  setup(__props) {
+let $attrs
+return (_ctx, _cache) => {
+  return (_openBlock(), _createElementBlock("div", _normalizeProps(_guardReactiveProps(_unref($attrs))), null, 16 /* FULL_PROPS */))
+}
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> > inlineTemplate mode > destructure setup context for built-in properties > user-defined properties override > should not extract $emit when user defines it 1`] = `
+"import { unref as _unref, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
+
+
+export default {
+  setup(__props) {
+let $emit
+return (_ctx, _cache) => {
+  return (_openBlock(), _createElementBlock("div", {
+    onClick: _cache[0] || (_cache[0] = $event => (_unref($emit)('click')))
+  }, "click"))
+}
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> > inlineTemplate mode > destructure setup context for built-in properties > user-defined properties override > should not extract $slots when user defines it 1`] = `
+"import { unref as _unref, resolveComponent as _resolveComponent, openBlock as _openBlock, createBlock as _createBlock } from "vue"
+
+
+export default {
+  setup(__props) {
+let $slots
+return (_ctx, _cache) => {
+  const _component_Comp = _resolveComponent("Comp")
+
+  return (_openBlock(), _createBlock(_component_Comp, {
+    foo: _unref($slots).foo
+  }, null, 8 /* PROPS */, ["foo"]))
+}
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> > inlineTemplate mode > destructure setup context for built-in properties > user-defined properties override > should not generate $props alias when user defines it 1`] = `
+"import { unref as _unref, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
+
+
+export default {
+  setup(__props) {
+let $props
+return (_ctx, _cache) => {
+  return (_openBlock(), _createElementBlock("div", null, _toDisplayString(_unref($props).msg), 1 /* TEXT */))
+}
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> > inlineTemplate mode > destructure setup context for built-in properties > user-defined properties override > should only extract non-user-defined properties 1`] = `
+"import { unref as _unref, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
+
+
+export default {
+  setup(__props, { emit: $emit, slots: $slots }) {
+  const $props = __props
+let $attrs
+return (_ctx, _cache) => {
+  return (_openBlock(), _createElementBlock("div", null, _toDisplayString(_unref($attrs)) + _toDisplayString($slots) + _toDisplayString($emit) + _toDisplayString($props), 1 /* TEXT */))
+}
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> > inlineTemplate mode > destructuring setup context for built-in properties > should alias __emit to $emit when defineEmits is used 1`] = `
+"import { openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
+
+
+export default {
+  emits: ['click'],
+  setup(__props, { emit: __emit }) {
+  const $emit = __emit
+
+            const emit = __emit
+          
+return (_ctx, _cache) => {
+  return (_openBlock(), _createElementBlock("div", {
+    onClick: _cache[0] || (_cache[0] = $event => ($emit('click')))
+  }))
+}
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> > inlineTemplate mode > destructuring setup context for built-in properties > should alias __props to $props when $props is used 1`] = `
+"import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
+
+
+export default {
+  setup(__props) {
+  const $props = __props
+/* ... */
+return (_ctx, _cache) => {
+  return (_openBlock(), _createElementBlock("div", null, _toDisplayString($props), 1 /* TEXT */))
+}
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> > inlineTemplate mode > destructuring setup context for built-in properties > should extract all built-in properties when they are used 1`] = `
+"import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
+
+
+export default {
+  setup(__props, { emit: $emit, attrs: $attrs, slots: $slots }) {
+  const $props = __props
+/* ... */
+return (_ctx, _cache) => {
+  return (_openBlock(), _createElementBlock("div", null, _toDisplayString($props) + _toDisplayString($slots) + _toDisplayString($emit) + _toDisplayString($attrs), 1 /* TEXT */))
+}
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> > inlineTemplate mode > destructuring setup context for built-in properties > should extract attrs when $attrs is used 1`] = `
+"import { normalizeProps as _normalizeProps, guardReactiveProps as _guardReactiveProps, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
+
+
+export default {
+  setup(__props, { attrs: $attrs }) {
+/* ... */
+return (_ctx, _cache) => {
+  return (_openBlock(), _createElementBlock("div", _normalizeProps(_guardReactiveProps($attrs)), null, 16 /* FULL_PROPS */))
+}
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> > inlineTemplate mode > destructuring setup context for built-in properties > should extract emit when $emit is used 1`] = `
+"import { openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
+
+
+export default {
+  setup(__props, { emit: $emit }) {
+/* ... */
+return (_ctx, _cache) => {
+  return (_openBlock(), _createElementBlock("div", {
+    onClick: _cache[0] || (_cache[0] = $event => ($emit('click')))
+  }))
+}
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> > inlineTemplate mode > destructuring setup context for built-in properties > should extract slots when $slots is used 1`] = `
+"import { resolveComponent as _resolveComponent, openBlock as _openBlock, createBlock as _createBlock } from "vue"
+
+
+export default {
+  setup(__props, { slots: $slots }) {
+/* ... */
+return (_ctx, _cache) => {
+  const _component_Comp = _resolveComponent("Comp")
+
+  return (_openBlock(), _createBlock(_component_Comp, {
+    foo: $slots.foo
+  }, null, 8 /* PROPS */, ["foo"]))
+}
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> > inlineTemplate mode > destructuring setup context for built-in properties > should not extract built-in properties when neither is used 1`] = `
+"import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
+
+
+export default {
+  setup(__props) {
+/* ... */
+return (_ctx, _cache) => {
+  return (_openBlock(), _createElementBlock("div", null, _toDisplayString(_ctx.msg), 1 /* TEXT */))
+}
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> > inlineTemplate mode > destructuring setup context for built-in properties > user-defined properties override > should handle mixed defineEmits and user-defined $emit 1`] = `
+"import { unref as _unref, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
+
+
+export default {
+  emits: ['click'],
+  setup(__props, { emit: __emit }) {
+
+              const emit = __emit
+              let $emit
+            
+return (_ctx, _cache) => {
+  return (_openBlock(), _createElementBlock("div", {
+    onClick: _cache[0] || (_cache[0] = $event => (_unref($emit)('click')))
+  }, "click"))
+}
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> > inlineTemplate mode > destructuring setup context for built-in properties > user-defined properties override > should not extract $attrs when user defines it 1`] = `
+"import { unref as _unref, normalizeProps as _normalizeProps, guardReactiveProps as _guardReactiveProps, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
+
+
+export default {
+  setup(__props) {
+let $attrs
+return (_ctx, _cache) => {
+  return (_openBlock(), _createElementBlock("div", _normalizeProps(_guardReactiveProps(_unref($attrs))), null, 16 /* FULL_PROPS */))
+}
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> > inlineTemplate mode > destructuring setup context for built-in properties > user-defined properties override > should not extract $emit when user defines it 1`] = `
+"import { unref as _unref, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
+
+
+export default {
+  setup(__props) {
+let $emit
+return (_ctx, _cache) => {
+  return (_openBlock(), _createElementBlock("div", {
+    onClick: _cache[0] || (_cache[0] = $event => (_unref($emit)('click')))
+  }, "click"))
+}
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> > inlineTemplate mode > destructuring setup context for built-in properties > user-defined properties override > should not extract $slots when user defines it 1`] = `
+"import { unref as _unref, resolveComponent as _resolveComponent, openBlock as _openBlock, createBlock as _createBlock } from "vue"
+
+
+export default {
+  setup(__props) {
+let $slots
+return (_ctx, _cache) => {
+  const _component_Comp = _resolveComponent("Comp")
+
+  return (_openBlock(), _createBlock(_component_Comp, {
+    foo: _unref($slots).foo
+  }, null, 8 /* PROPS */, ["foo"]))
+}
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> > inlineTemplate mode > destructuring setup context for built-in properties > user-defined properties override > should not generate $props alias when user defines it 1`] = `
+"import { unref as _unref, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
+
+
+export default {
+  setup(__props) {
+let $props
+return (_ctx, _cache) => {
+  return (_openBlock(), _createElementBlock("div", null, _toDisplayString(_unref($props).msg), 1 /* TEXT */))
+}
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> > inlineTemplate mode > destructuring setup context for built-in properties > user-defined properties override > should only extract non-user-defined properties 1`] = `
+"import { unref as _unref, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
+
+
+export default {
+  setup(__props, { emit: $emit, slots: $slots }) {
+  const $props = __props
+let $attrs
+return (_ctx, _cache) => {
+  return (_openBlock(), _createElementBlock("div", null, _toDisplayString(_unref($attrs)) + _toDisplayString($slots) + _toDisplayString($emit) + _toDisplayString($props), 1 /* TEXT */))
+}
+}
+
+}"
+`;
+
 exports[`SFC compile <script setup> > inlineTemplate mode > referencing scope components and directives 1`] = `
 "import { unref as _unref, createElementVNode as _createElementVNode, withDirectives as _withDirectives, createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
 
index 134beff9668bc63a8a3b9f40839de56a049738e2..6392e538a78d67842f586fac7dcf40ddc919fa9c 100644 (file)
@@ -717,6 +717,151 @@ describe('SFC compile <script setup>', () => {
         consumer.originalPositionFor(getPositionInCode(content, 'Error')),
       ).toMatchObject(getPositionInCode(source, `Error`))
     })
+
+    describe('destructure setup context for built-in properties', () => {
+      const theCompile = (template: string, setup = '/* ... */') =>
+        compile(
+          `<script setup>${setup}</script>\n<template>${template}</template>`,
+          { inlineTemplate: true },
+        )
+
+      test('should extract attrs when $attrs is used', () => {
+        let { content } = theCompile('<div v-bind="$attrs"></div>')
+        expect(content).toMatch('setup(__props, { attrs: $attrs })')
+        expect(content).not.toMatch('slots: $slots')
+        expect(content).not.toMatch('emit: $emit')
+        expect(content).not.toMatch('const $props = __props')
+        assertCode(content)
+      })
+
+      test('should extract slots when $slots is used', () => {
+        let { content } = theCompile('<Comp :foo="$slots.foo"></Comp>')
+        expect(content).toMatch('setup(__props, { slots: $slots })')
+        assertCode(content)
+      })
+
+      test('should alias __props to $props when $props is used', () => {
+        let { content } = theCompile('<div>{{ $props }}</div>')
+        expect(content).toMatch('setup(__props)')
+        expect(content).toMatch('const $props = __props')
+        assertCode(content)
+      })
+
+      test('should extract emit when $emit is used', () => {
+        let { content } = theCompile(`<div @click="$emit('click')"></div>`)
+        expect(content).toMatch('setup(__props, { emit: $emit })')
+        expect(content).not.toMatch('const $emit = __emit')
+        assertCode(content)
+      })
+
+      test('should alias __emit to $emit when defineEmits is used', () => {
+        let { content } = compile(
+          `
+          <script setup>
+            const emit = defineEmits(['click'])
+          </script>
+          <template>
+            <div @click="$emit('click')"></div>
+          </template>
+        `,
+          { inlineTemplate: true },
+        )
+        expect(content).toMatch('setup(__props, { emit: __emit })')
+        expect(content).toMatch('const $emit = __emit')
+        expect(content).toMatch('const emit = __emit')
+        assertCode(content)
+      })
+
+      test('should extract all built-in properties when they are used', () => {
+        let { content } = theCompile(
+          '<div>{{ $props }}{{ $slots }}{{ $emit }}{{ $attrs }}</div>',
+        )
+        expect(content).toMatch(
+          'setup(__props, { emit: $emit, attrs: $attrs, slots: $slots })',
+        )
+        expect(content).toMatch('const $props = __props')
+        assertCode(content)
+      })
+
+      test('should not extract built-in properties when neither is used', () => {
+        let { content } = theCompile('<div>{{ msg }}</div>')
+        expect(content).toMatch('setup(__props)')
+        expect(content).not.toMatch('attrs: $attrs')
+        expect(content).not.toMatch('slots: $slots')
+        expect(content).not.toMatch('emit: $emit')
+        expect(content).not.toMatch('props: $props')
+        assertCode(content)
+      })
+
+      describe('user-defined properties override', () => {
+        test('should not extract $attrs when user defines it', () => {
+          let { content } = theCompile(
+            '<div v-bind="$attrs"></div>',
+            'let $attrs',
+          )
+          expect(content).toMatch('setup(__props)')
+          expect(content).not.toMatch('attrs: $attrs')
+          assertCode(content)
+        })
+
+        test('should not extract $slots when user defines it', () => {
+          let { content } = theCompile(
+            '<Comp :foo="$slots.foo"></Comp>',
+            'let $slots',
+          )
+          expect(content).toMatch('setup(__props)')
+          expect(content).not.toMatch('slots: $slots')
+          assertCode(content)
+        })
+
+        test('should not extract $emit when user defines it', () => {
+          let { content } = theCompile(
+            `<div @click="$emit('click')">click</div>`,
+            'let $emit',
+          )
+          expect(content).toMatch('setup(__props)')
+          expect(content).not.toMatch('emit: $emit')
+          assertCode(content)
+        })
+
+        test('should not generate $props alias when user defines it', () => {
+          let { content } = theCompile(
+            '<div>{{ $props.msg }}</div>',
+            'let $props',
+          )
+          expect(content).toMatch('setup(__props)')
+          expect(content).not.toMatch('const $props = __props')
+          assertCode(content)
+        })
+
+        test('should only extract non-user-defined properties', () => {
+          let { content } = theCompile(
+            '<div>{{ $attrs }}{{ $slots }}{{ $emit }}{{ $props }}</div>',
+            'let $attrs',
+          )
+          expect(content).toMatch(
+            'setup(__props, { emit: $emit, slots: $slots })',
+          )
+          expect(content).not.toMatch('attrs: $attrs')
+          expect(content).toMatch('const $props = __props')
+          assertCode(content)
+        })
+
+        test('should handle mixed defineEmits and user-defined $emit', () => {
+          let { content } = theCompile(
+            `<div @click="$emit('click')">click</div>`,
+            `
+              const emit = defineEmits(['click'])
+              let $emit
+            `,
+          )
+          expect(content).toMatch('setup(__props, { emit: __emit })')
+          expect(content).toMatch('const emit = __emit')
+          expect(content).not.toMatch('const $emit = __emit')
+          assertCode(content)
+        })
+      })
+    })
   })
 
   describe('with TypeScript', () => {
index 54ca260bdd6f48d58686d3af24bf939dc9346372..c2fcc46c073a3bec3a1caead77e82c8aa07b5637 100644 (file)
@@ -59,7 +59,7 @@ import { DEFINE_SLOTS, processDefineSlots } from './script/defineSlots'
 import { DEFINE_MODEL, processDefineModel } from './script/defineModel'
 import { getImportedName, isCallOf, isLiteralNode } from './script/utils'
 import { analyzeScriptBindings } from './script/analyzeScriptBindings'
-import { isImportUsed } from './script/importUsageCheck'
+import { isUsedInTemplate } from './script/importUsageCheck'
 import { processAwait } from './script/topLevelAwait'
 
 export interface SFCScriptCompileOptions {
@@ -181,6 +181,7 @@ export function compileScript(
   const scriptSetupLang = scriptSetup && scriptSetup.lang
   const vapor = sfc.vapor || options.vapor
   const ssr = options.templateOptions?.ssr
+  const setupPreambleLines = [] as string[]
 
   if (!scriptSetup) {
     if (!script) {
@@ -246,7 +247,7 @@ export function compileScript(
   ) {
     // template usage check is only needed in non-inline mode, so we can skip
     // the work if inlineTemplate is true.
-    let isUsedInTemplate = needTemplateUsageCheck
+    let isImportUsed = needTemplateUsageCheck
     if (
       needTemplateUsageCheck &&
       ctx.isTS &&
@@ -254,7 +255,7 @@ export function compileScript(
       !sfc.template.src &&
       !sfc.template.lang
     ) {
-      isUsedInTemplate = isImportUsed(local, sfc)
+      isImportUsed = isUsedInTemplate(local, sfc)
     }
 
     ctx.userImports[local] = {
@@ -263,7 +264,7 @@ export function compileScript(
       local,
       source,
       isFromSetup,
-      isUsedInTemplate,
+      isUsedInTemplate: isImportUsed,
     }
   }
 
@@ -284,8 +285,42 @@ export function compileScript(
     })
   }
 
+  function buildDestructureElements() {
+    if (!sfc.template || !sfc.template.ast) return
+
+    const builtins = {
+      $props: {
+        bindingType: BindingTypes.SETUP_REACTIVE_CONST,
+        setup: () => setupPreambleLines.push(`const $props = __props`),
+      },
+      $emit: {
+        bindingType: BindingTypes.SETUP_CONST,
+        setup: () =>
+          ctx.emitDecl
+            ? setupPreambleLines.push(`const $emit = __emit`)
+            : destructureElements.push('emit: $emit'),
+      },
+      $attrs: {
+        bindingType: BindingTypes.SETUP_REACTIVE_CONST,
+        setup: () => destructureElements.push('attrs: $attrs'),
+      },
+      $slots: {
+        bindingType: BindingTypes.SETUP_REACTIVE_CONST,
+        setup: () => destructureElements.push('slots: $slots'),
+      },
+    }
+
+    for (const [name, config] of Object.entries(builtins)) {
+      if (isUsedInTemplate(name, sfc) && !ctx.bindingMetadata[name]) {
+        config.setup()
+        ctx.bindingMetadata[name] = config.bindingType
+      }
+    }
+  }
+
   const scriptAst = ctx.scriptAst
   const scriptSetupAst = ctx.scriptSetupAst!
+  const inlineMode = options.inlineTemplate
 
   // 1.1 walk import declarations of <script>
   if (scriptAst) {
@@ -302,7 +337,7 @@ export function compileScript(
               (specifier.type === 'ImportSpecifier' &&
                 specifier.importKind === 'type'),
             false,
-            !options.inlineTemplate,
+            !inlineMode,
           )
         }
       }
@@ -370,7 +405,7 @@ export function compileScript(
               (specifier.type === 'ImportSpecifier' &&
                 specifier.importKind === 'type'),
             true,
-            !options.inlineTemplate,
+            !inlineMode,
           )
         }
       }
@@ -811,12 +846,16 @@ export function compileScript(
   }
 
   const destructureElements =
-    ctx.hasDefineExposeCall || !options.inlineTemplate
-      ? [`expose: __expose`]
-      : []
+    ctx.hasDefineExposeCall || !inlineMode ? [`expose: __expose`] : []
   if (ctx.emitDecl) {
     destructureElements.push(`emit: __emit`)
   }
+
+  // destructure built-in properties (e.g. $emit, $attrs, $slots)
+  if (inlineMode) {
+    buildDestructureElements()
+  }
+
   if (destructureElements.length) {
     args += `, { ${destructureElements.join(', ')} }`
   }
@@ -824,10 +863,7 @@ export function compileScript(
   let templateMap
   // 9. generate return statement
   let returned
-  if (
-    !options.inlineTemplate ||
-    (!sfc.template && ctx.hasDefaultExportRender)
-  ) {
+  if (!inlineMode || (!sfc.template && ctx.hasDefaultExportRender)) {
     // non-inline mode, or has manual render in normal <script>
     // return bindings from script and script setup
     const allBindings: Record<string, any> = {
@@ -927,7 +963,7 @@ export function compileScript(
     }
   }
 
-  if (!options.inlineTemplate && !__TEST__) {
+  if (!inlineMode && !__TEST__) {
     // in non-inline mode, the `__isScriptSetup: true` flag is used by
     // componentPublicInstance proxy to allow properties that start with $ or _
     ctx.s.appendRight(
@@ -976,8 +1012,12 @@ export function compileScript(
 
   // <script setup> components are closed by default. If the user did not
   // explicitly call `defineExpose`, call expose() with no args.
-  const exposeCall =
-    ctx.hasDefineExposeCall || options.inlineTemplate ? `` : `  __expose();\n`
+  if (!ctx.hasDefineExposeCall && !inlineMode)
+    setupPreambleLines.push(`__expose();`)
+
+  const setupPreamble = setupPreambleLines.length
+    ? `  ${setupPreambleLines.join('\n  ')}\n`
+    : ''
   // wrap setup code with function.
   if (ctx.isTS) {
     // for TS, make sure the exported type is still valid type with
@@ -994,7 +1034,7 @@ export function compileScript(
         vapor && !ssr ? `defineVaporComponent` : `defineComponent`,
       )}({${def}${runtimeOptions}\n  ${
         hasAwait ? `async ` : ``
-      }setup(${args}) {\n${exposeCall}`,
+      }setup(${args}) {\n${setupPreamble}`,
     )
     ctx.s.appendRight(endOffset, `})`)
   } else {
@@ -1010,14 +1050,14 @@ export function compileScript(
         `\n${genDefaultAs} /*@__PURE__*/Object.assign(${
           defaultExport ? `${normalScriptDefaultVar}, ` : ''
         }${definedOptions ? `${definedOptions}, ` : ''}{${runtimeOptions}\n  ` +
-          `${hasAwait ? `async ` : ``}setup(${args}) {\n${exposeCall}`,
+          `${hasAwait ? `async ` : ``}setup(${args}) {\n${setupPreamble}`,
       )
       ctx.s.appendRight(endOffset, `})`)
     } else {
       ctx.s.prependLeft(
         startOffset,
         `\n${genDefaultAs} {${runtimeOptions}\n  ` +
-          `${hasAwait ? `async ` : ``}setup(${args}) {\n${exposeCall}`,
+          `${hasAwait ? `async ` : ``}setup(${args}) {\n${setupPreamble}`,
       )
       ctx.s.appendRight(endOffset, `}`)
     }
index 01d0794e0d8d1dd54c2b8ba92cbc012c0d41dbc2..9172cfc67ff31dea77991826a94baa8246053ce1 100644 (file)
@@ -16,7 +16,7 @@ import type { TemplateCompiler } from './compileTemplate'
 import { parseCssVars } from './style/cssVars'
 import { createCache } from './cache'
 import type { ImportBinding } from './compileScript'
-import { isImportUsed } from './script/importUsageCheck'
+import { isUsedInTemplate } from './script/importUsageCheck'
 import type { LRUCache } from 'lru-cache'
 import { genCacheKey } from '@vue/shared'
 
@@ -449,7 +449,7 @@ export function hmrShouldReload(
   for (const key in prevImports) {
     // if an import was previous unused, but now is used, we need to force
     // reload so that the script now includes that import.
-    if (!prevImports[key].isUsedInTemplate && isImportUsed(key, next)) {
+    if (!prevImports[key].isUsedInTemplate && isUsedInTemplate(key, next)) {
       return true
     }
   }
index 22ef37cf37f5ab98ab4081a4a893f74cf6fc599f..6fc16db446b8981205bd5e80f0ccd8f2f9f7f0ac 100644 (file)
@@ -11,12 +11,16 @@ import { createCache } from '../cache'
 import { camelize, capitalize, isBuiltInDirective } from '@vue/shared'
 
 /**
- * Check if an import is used in the SFC's template. This is used to determine
- * the properties that should be included in the object returned from setup()
- * when not using inline mode.
+ * Check if an identifier is used in the SFC's template.
+ * - 1.used to determine the properties that should be included in the object returned from setup()
+ *   when not using inline mode.
+ * - 2.check whether the built-in properties such as $attrs, $slots, $emit are used in the template
  */
-export function isImportUsed(local: string, sfc: SFCDescriptor): boolean {
-  return resolveTemplateUsedIdentifiers(sfc).has(local)
+export function isUsedInTemplate(
+  identifier: string,
+  sfc: SFCDescriptor,
+): boolean {
+  return resolveTemplateUsedIdentifiers(sfc).has(identifier)
 }
 
 const templateUsageCheckCache = createCache<Set<string>>()