diff --git a/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap b/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap index 2acac64b0fb..0c6f78a19a1 100644 --- a/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap +++ b/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap @@ -823,6 +823,450 @@ return (_ctx, _cache) => { }" `; +exports[`SFC compile \n`, + { inlineTemplate: true }, + ) + + test('should extract attrs when $attrs is used', () => { + let { content } = theCompile('
') + 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('') + expect(content).toMatch('setup(__props, { slots: $slots })') + assertCode(content) + }) + + test('should alias __props to $props when $props is used', () => { + let { content } = theCompile('
{{ $props }}
') + expect(content).toMatch('setup(__props)') + expect(content).toMatch('const $props = __props') + assertCode(content) + }) + + test('should extract emit when $emit is used', () => { + let { content } = theCompile(`
`) + 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( + ` + + + `, + { 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( + '
{{ $props }}{{ $slots }}{{ $emit }}{{ $attrs }}
', + ) + 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('
{{ msg }}
') + 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( + '
', + '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( + '', + '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( + `
click
`, + '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( + '
{{ $props.msg }}
', + '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( + '
{{ $attrs }}{{ $slots }}{{ $emit }}{{ $props }}
', + '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( + `
click
`, + ` + 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', () => { diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index 54ca260bdd6..c2fcc46c073 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -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