diff --git a/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap b/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap index 649b92d17f0..2813a0be788 100644 --- a/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap +++ b/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap @@ -634,6 +634,19 @@ return { } }" `; +exports[`SFC compile <script setup> > defineOptions() > basic usage 1`] = ` +"export default /*#__PURE__*/Object.assign({ name: 'FooApp' }, { + setup(__props, { expose }) { + expose(); + + + +return { } +} + +})" +`; + exports[`SFC compile <script setup> > defineProps w/ external definition 1`] = ` "import { propsModel } from './props' diff --git a/packages/compiler-sfc/__tests__/compileScript.spec.ts b/packages/compiler-sfc/__tests__/compileScript.spec.ts index 3ea7632f68b..94d622037e1 100644 --- a/packages/compiler-sfc/__tests__/compileScript.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript.spec.ts @@ -172,6 +172,68 @@ const myEmit = defineEmits(['foo', 'bar']) expect(content).toMatch(`emits: ['a'],`) }) + describe('defineOptions()', () => { + test('basic usage', () => { + const { content } = compile(` +<script setup> +defineOptions({ name: 'FooApp' }) +</script> + `) + assertCode(content) + // should remove defineOptions import and call + expect(content).not.toMatch('defineOptions') + // should include context options in default export + expect(content).toMatch( + `export default /*#__PURE__*/Object.assign({ name: 'FooApp' }, ` + ) + }) + + it('should emit an error with two defineProps', () => { + expect(() => + compile(` + <script setup> + defineOptions({ name: 'FooApp' }) + defineOptions({ name: 'BarApp' }) + </script> + `) + ).toThrowError('[@vue/compiler-sfc] duplicate defineOptions() call') + }) + + it('should emit an error with props or emits property', () => { + expect(() => + compile(` + <script setup> + defineOptions({ props: { foo: String } }) + </script> + `) + ).toThrowError( + '[@vue/compiler-sfc] defineOptions() cannot be used to declare props. Use defineProps() instead.' + ) + + expect(() => + compile(` + <script setup> + defineOptions({ emits: ['update'] }) + </script> + `) + ).toThrowError( + '[@vue/compiler-sfc] defineOptions() cannot be used to declare emits. Use defineEmits() instead.' + ) + }) + + it('should emit an error with type generic', () => { + expect(() => + compile(` + <script setup lang="ts"> + defineOptions<{ name: 'FooApp' }>() + </script> + `) + ).toThrowError( + '[@vue/compiler-sfc] defineOptions() cannot accept type arguments' + ) + }) + }) + test('defineExpose()', () => { const { content } = compile(` <script setup> @@ -1136,7 +1198,7 @@ const emit = defineEmits(['a', 'b']) `) assertCode(content) }) - + // #7111 test('withDefaults (static) w/ production mode', () => { const { content } = compile( @@ -1277,7 +1339,6 @@ const emit = defineEmits(['a', 'b']) expect(content).toMatch(`emits: ["foo", "bar"]`) }) - test('defineEmits w/ type from normal script', () => { const { content } = compile(` <script lang="ts"> diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index 310a9e374ec..e05b09a17f8 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -62,6 +62,7 @@ const DEFINE_PROPS = 'defineProps' const DEFINE_EMITS = 'defineEmits' const DEFINE_EXPOSE = 'defineExpose' const WITH_DEFAULTS = 'withDefaults' +const DEFINE_OPTIONS = 'defineOptions' // constants const DEFAULT_VAR = `__default__` @@ -270,6 +271,7 @@ export function compileScript( let hasDefineExposeCall = false let hasDefaultExportName = false let hasDefaultExportRender = false + let hasDefineOptionsCall = false let propsRuntimeDecl: Node | undefined let propsRuntimeDefaults: ObjectExpression | undefined let propsDestructureDecl: Node | undefined @@ -281,6 +283,7 @@ export function compileScript( let emitsTypeDecl: EmitsDeclType | undefined let emitsTypeDeclRaw: Node | undefined let emitIdentifier: string | undefined + let optionsRuntimeDecl: Node | undefined let hasAwait = false let hasInlinedSsrRenderFn = false // props/emits declared via types @@ -647,6 +650,50 @@ export function compileScript( }) } + function processDefineOptions(node: Node): boolean { + if (!isCallOf(node, DEFINE_OPTIONS)) { + return false + } + if (hasDefineOptionsCall) { + error(`duplicate ${DEFINE_OPTIONS}() call`, node) + } + if (node.typeParameters) { + error(`${DEFINE_OPTIONS}() cannot accept type arguments`, node) + } + + hasDefineOptionsCall = true + optionsRuntimeDecl = node.arguments[0] + + let propsOption = undefined + let emitsOption = undefined + if (optionsRuntimeDecl.type === 'ObjectExpression') { + for (const prop of optionsRuntimeDecl.properties) { + if ( + (prop.type === 'ObjectProperty' || prop.type === 'ObjectMethod') && + prop.key.type === 'Identifier' + ) { + if (prop.key.name === 'props') propsOption = prop + if (prop.key.name === 'emits') emitsOption = prop + } + } + } + + if (propsOption) { + error( + `${DEFINE_OPTIONS}() cannot be used to declare props. Use ${DEFINE_PROPS}() instead.`, + propsOption + ) + } + if (emitsOption) { + error( + `${DEFINE_OPTIONS}() cannot be used to declare emits. Use ${DEFINE_EMITS}() instead.`, + emitsOption + ) + } + + return true + } + function resolveQualifiedType( node: Node, qualifier: (node: Node) => boolean @@ -1175,6 +1222,7 @@ export function compileScript( if ( processDefineProps(node.expression) || processDefineEmits(node.expression) || + processDefineOptions(node.expression) || processWithDefaults(node.expression) ) { s.remove(node.start! + startOffset, node.end! + startOffset) @@ -1195,6 +1243,13 @@ export function compileScript( for (let i = 0; i < total; i++) { const decl = node.declarations[i] if (decl.init) { + if (processDefineOptions(decl.init)) { + error( + `${DEFINE_OPTIONS}() has no returning value, it cannot be assigned.`, + node + ) + } + // defineProps / defineEmits const isDefineProps = processDefineProps(decl.init, decl.id, node.kind) || @@ -1339,6 +1394,7 @@ export function compileScript( checkInvalidScopeReference(propsRuntimeDefaults, DEFINE_PROPS) checkInvalidScopeReference(propsDestructureDecl, DEFINE_PROPS) checkInvalidScopeReference(emitsRuntimeDecl, DEFINE_EMITS) + checkInvalidScopeReference(optionsRuntimeDecl, DEFINE_OPTIONS) // 6. remove non-script content if (script) { @@ -1626,6 +1682,13 @@ export function compileScript( runtimeOptions += genRuntimeEmits(typeDeclaredEmits) } + let definedOptions = '' + if (optionsRuntimeDecl) { + definedOptions = scriptSetup.content + .slice(optionsRuntimeDecl.start!, optionsRuntimeDecl.end!) + .trim() + } + // <script setup> components are closed by default. If the user did not // explicitly call `defineExpose`, call expose() with no args. const exposeCall = @@ -1637,7 +1700,9 @@ export function compileScript( // we have to use object spread for types to be merged properly // user's TS setting should compile it down to proper targets // export default defineComponent({ ...__default__, ... }) - const def = defaultExport ? `\n ...${DEFAULT_VAR},` : `` + const def = + (defaultExport ? `\n ...${DEFAULT_VAR},` : ``) + + (definedOptions ? `\n ...${definedOptions},` : '') s.prependLeft( startOffset, `\nexport default /*#__PURE__*/${helper( @@ -1648,12 +1713,14 @@ export function compileScript( ) s.appendRight(endOffset, `})`) } else { - if (defaultExport) { + if (defaultExport || definedOptions) { // without TS, can't rely on rest spread, so we use Object.assign // export default Object.assign(__default__, { ... }) s.prependLeft( startOffset, - `\nexport default /*#__PURE__*/Object.assign(${DEFAULT_VAR}, {${runtimeOptions}\n ` + + `\nexport default /*#__PURE__*/Object.assign(${ + defaultExport ? `${DEFAULT_VAR}, ` : '' + }${definedOptions ? `${definedOptions}, ` : ''}{${runtimeOptions}\n ` + `${hasAwait ? `async ` : ``}setup(${args}) {\n${exposeCall}` ) s.appendRight(endOffset, `})`) diff --git a/packages/runtime-core/src/apiSetupHelpers.ts b/packages/runtime-core/src/apiSetupHelpers.ts index 71ac1191a31..981e8d60a6a 100644 --- a/packages/runtime-core/src/apiSetupHelpers.ts +++ b/packages/runtime-core/src/apiSetupHelpers.ts @@ -7,6 +7,12 @@ import { unsetCurrentInstance } from './component' import { EmitFn, EmitsOptions } from './componentEmits' +import { + ComponentOptionsMixin, + ComponentOptionsWithoutProps, + ComputedOptions, + MethodOptions +} from './componentOptions' import { ComponentPropsOptions, ComponentObjectPropsOptions, @@ -143,6 +149,33 @@ export function defineExpose< } } +export function defineOptions< + RawBindings = {}, + D = {}, + C extends ComputedOptions = {}, + M extends MethodOptions = {}, + Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, + Extends extends ComponentOptionsMixin = ComponentOptionsMixin, + E extends EmitsOptions = EmitsOptions, + EE extends string = string +>( + options?: ComponentOptionsWithoutProps< + {}, + RawBindings, + D, + C, + M, + Mixin, + Extends, + E, + EE + > & { emits?: undefined } +): void { + if (__DEV__) { + warnRuntimeUsage(`defineOptions`) + } +} + type NotUndefined<T> = T extends undefined ? never : T type InferDefaults<T> = { diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 622f5ecc57e..06f9a2affd4 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -68,6 +68,7 @@ export { defineProps, defineEmits, defineExpose, + defineOptions, withDefaults, // internal mergeDefaults, diff --git a/packages/runtime-core/types/scriptSetupHelpers.d.ts b/packages/runtime-core/types/scriptSetupHelpers.d.ts index 4d168212c54..ba4ca79fc59 100644 --- a/packages/runtime-core/types/scriptSetupHelpers.d.ts +++ b/packages/runtime-core/types/scriptSetupHelpers.d.ts @@ -3,11 +3,13 @@ type _defineProps = typeof defineProps type _defineEmits = typeof defineEmits type _defineExpose = typeof defineExpose +type _defineOptions = typeof defineOptions type _withDefaults = typeof withDefaults declare global { const defineProps: _defineProps const defineEmits: _defineEmits const defineExpose: _defineExpose + const defineOptions: _defineOptions const withDefaults: _withDefaults }