Skip to content

Commit bcf5841

Browse files
authored
feat(compiler-sfc): add defineOptions macro (#5738)
1 parent f77bd36 commit bcf5841

File tree

6 files changed

+182
-5
lines changed

6 files changed

+182
-5
lines changed

packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap

+13
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,19 @@ return { }
634634
}"
635635
`;
636636

637+
exports[`SFC compile <script setup> > defineOptions() > basic usage 1`] = `
638+
"export default /*#__PURE__*/Object.assign({ name: 'FooApp' }, {
639+
setup(__props, { expose }) {
640+
expose();
641+
642+
643+
644+
return { }
645+
}
646+
647+
})"
648+
`;
649+
637650
exports[`SFC compile <script setup> > defineProps w/ external definition 1`] = `
638651
"import { propsModel } from './props'
639652

packages/compiler-sfc/__tests__/compileScript.spec.ts

+63-2
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,68 @@ const myEmit = defineEmits(['foo', 'bar'])
172172
expect(content).toMatch(`emits: ['a'],`)
173173
})
174174

175+
describe('defineOptions()', () => {
176+
test('basic usage', () => {
177+
const { content } = compile(`
178+
<script setup>
179+
defineOptions({ name: 'FooApp' })
180+
</script>
181+
`)
182+
assertCode(content)
183+
// should remove defineOptions import and call
184+
expect(content).not.toMatch('defineOptions')
185+
// should include context options in default export
186+
expect(content).toMatch(
187+
`export default /*#__PURE__*/Object.assign({ name: 'FooApp' }, `
188+
)
189+
})
190+
191+
it('should emit an error with two defineProps', () => {
192+
expect(() =>
193+
compile(`
194+
<script setup>
195+
defineOptions({ name: 'FooApp' })
196+
defineOptions({ name: 'BarApp' })
197+
</script>
198+
`)
199+
).toThrowError('[@vue/compiler-sfc] duplicate defineOptions() call')
200+
})
201+
202+
it('should emit an error with props or emits property', () => {
203+
expect(() =>
204+
compile(`
205+
<script setup>
206+
defineOptions({ props: { foo: String } })
207+
</script>
208+
`)
209+
).toThrowError(
210+
'[@vue/compiler-sfc] defineOptions() cannot be used to declare props. Use defineProps() instead.'
211+
)
212+
213+
expect(() =>
214+
compile(`
215+
<script setup>
216+
defineOptions({ emits: ['update'] })
217+
</script>
218+
`)
219+
).toThrowError(
220+
'[@vue/compiler-sfc] defineOptions() cannot be used to declare emits. Use defineEmits() instead.'
221+
)
222+
})
223+
224+
it('should emit an error with type generic', () => {
225+
expect(() =>
226+
compile(`
227+
<script setup lang="ts">
228+
defineOptions<{ name: 'FooApp' }>()
229+
</script>
230+
`)
231+
).toThrowError(
232+
'[@vue/compiler-sfc] defineOptions() cannot accept type arguments'
233+
)
234+
})
235+
})
236+
175237
test('defineExpose()', () => {
176238
const { content } = compile(`
177239
<script setup>
@@ -1136,7 +1198,7 @@ const emit = defineEmits(['a', 'b'])
11361198
`)
11371199
assertCode(content)
11381200
})
1139-
1201+
11401202
// #7111
11411203
test('withDefaults (static) w/ production mode', () => {
11421204
const { content } = compile(
@@ -1277,7 +1339,6 @@ const emit = defineEmits(['a', 'b'])
12771339
expect(content).toMatch(`emits: ["foo", "bar"]`)
12781340
})
12791341

1280-
12811342
test('defineEmits w/ type from normal script', () => {
12821343
const { content } = compile(`
12831344
<script lang="ts">

packages/compiler-sfc/src/compileScript.ts

+70-3
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ const DEFINE_PROPS = 'defineProps'
6262
const DEFINE_EMITS = 'defineEmits'
6363
const DEFINE_EXPOSE = 'defineExpose'
6464
const WITH_DEFAULTS = 'withDefaults'
65+
const DEFINE_OPTIONS = 'defineOptions'
6566

6667
// constants
6768
const DEFAULT_VAR = `__default__`
@@ -270,6 +271,7 @@ export function compileScript(
270271
let hasDefineExposeCall = false
271272
let hasDefaultExportName = false
272273
let hasDefaultExportRender = false
274+
let hasDefineOptionsCall = false
273275
let propsRuntimeDecl: Node | undefined
274276
let propsRuntimeDefaults: ObjectExpression | undefined
275277
let propsDestructureDecl: Node | undefined
@@ -281,6 +283,7 @@ export function compileScript(
281283
let emitsTypeDecl: EmitsDeclType | undefined
282284
let emitsTypeDeclRaw: Node | undefined
283285
let emitIdentifier: string | undefined
286+
let optionsRuntimeDecl: Node | undefined
284287
let hasAwait = false
285288
let hasInlinedSsrRenderFn = false
286289
// props/emits declared via types
@@ -647,6 +650,50 @@ export function compileScript(
647650
})
648651
}
649652

653+
function processDefineOptions(node: Node): boolean {
654+
if (!isCallOf(node, DEFINE_OPTIONS)) {
655+
return false
656+
}
657+
if (hasDefineOptionsCall) {
658+
error(`duplicate ${DEFINE_OPTIONS}() call`, node)
659+
}
660+
if (node.typeParameters) {
661+
error(`${DEFINE_OPTIONS}() cannot accept type arguments`, node)
662+
}
663+
664+
hasDefineOptionsCall = true
665+
optionsRuntimeDecl = node.arguments[0]
666+
667+
let propsOption = undefined
668+
let emitsOption = undefined
669+
if (optionsRuntimeDecl.type === 'ObjectExpression') {
670+
for (const prop of optionsRuntimeDecl.properties) {
671+
if (
672+
(prop.type === 'ObjectProperty' || prop.type === 'ObjectMethod') &&
673+
prop.key.type === 'Identifier'
674+
) {
675+
if (prop.key.name === 'props') propsOption = prop
676+
if (prop.key.name === 'emits') emitsOption = prop
677+
}
678+
}
679+
}
680+
681+
if (propsOption) {
682+
error(
683+
`${DEFINE_OPTIONS}() cannot be used to declare props. Use ${DEFINE_PROPS}() instead.`,
684+
propsOption
685+
)
686+
}
687+
if (emitsOption) {
688+
error(
689+
`${DEFINE_OPTIONS}() cannot be used to declare emits. Use ${DEFINE_EMITS}() instead.`,
690+
emitsOption
691+
)
692+
}
693+
694+
return true
695+
}
696+
650697
function resolveQualifiedType(
651698
node: Node,
652699
qualifier: (node: Node) => boolean
@@ -1175,6 +1222,7 @@ export function compileScript(
11751222
if (
11761223
processDefineProps(node.expression) ||
11771224
processDefineEmits(node.expression) ||
1225+
processDefineOptions(node.expression) ||
11781226
processWithDefaults(node.expression)
11791227
) {
11801228
s.remove(node.start! + startOffset, node.end! + startOffset)
@@ -1195,6 +1243,13 @@ export function compileScript(
11951243
for (let i = 0; i < total; i++) {
11961244
const decl = node.declarations[i]
11971245
if (decl.init) {
1246+
if (processDefineOptions(decl.init)) {
1247+
error(
1248+
`${DEFINE_OPTIONS}() has no returning value, it cannot be assigned.`,
1249+
node
1250+
)
1251+
}
1252+
11981253
// defineProps / defineEmits
11991254
const isDefineProps =
12001255
processDefineProps(decl.init, decl.id, node.kind) ||
@@ -1339,6 +1394,7 @@ export function compileScript(
13391394
checkInvalidScopeReference(propsRuntimeDefaults, DEFINE_PROPS)
13401395
checkInvalidScopeReference(propsDestructureDecl, DEFINE_PROPS)
13411396
checkInvalidScopeReference(emitsRuntimeDecl, DEFINE_EMITS)
1397+
checkInvalidScopeReference(optionsRuntimeDecl, DEFINE_OPTIONS)
13421398

13431399
// 6. remove non-script content
13441400
if (script) {
@@ -1626,6 +1682,13 @@ export function compileScript(
16261682
runtimeOptions += genRuntimeEmits(typeDeclaredEmits)
16271683
}
16281684

1685+
let definedOptions = ''
1686+
if (optionsRuntimeDecl) {
1687+
definedOptions = scriptSetup.content
1688+
.slice(optionsRuntimeDecl.start!, optionsRuntimeDecl.end!)
1689+
.trim()
1690+
}
1691+
16291692
// <script setup> components are closed by default. If the user did not
16301693
// explicitly call `defineExpose`, call expose() with no args.
16311694
const exposeCall =
@@ -1637,7 +1700,9 @@ export function compileScript(
16371700
// we have to use object spread for types to be merged properly
16381701
// user's TS setting should compile it down to proper targets
16391702
// export default defineComponent({ ...__default__, ... })
1640-
const def = defaultExport ? `\n ...${DEFAULT_VAR},` : ``
1703+
const def =
1704+
(defaultExport ? `\n ...${DEFAULT_VAR},` : ``) +
1705+
(definedOptions ? `\n ...${definedOptions},` : '')
16411706
s.prependLeft(
16421707
startOffset,
16431708
`\nexport default /*#__PURE__*/${helper(
@@ -1648,12 +1713,14 @@ export function compileScript(
16481713
)
16491714
s.appendRight(endOffset, `})`)
16501715
} else {
1651-
if (defaultExport) {
1716+
if (defaultExport || definedOptions) {
16521717
// without TS, can't rely on rest spread, so we use Object.assign
16531718
// export default Object.assign(__default__, { ... })
16541719
s.prependLeft(
16551720
startOffset,
1656-
`\nexport default /*#__PURE__*/Object.assign(${DEFAULT_VAR}, {${runtimeOptions}\n ` +
1721+
`\nexport default /*#__PURE__*/Object.assign(${
1722+
defaultExport ? `${DEFAULT_VAR}, ` : ''
1723+
}${definedOptions ? `${definedOptions}, ` : ''}{${runtimeOptions}\n ` +
16571724
`${hasAwait ? `async ` : ``}setup(${args}) {\n${exposeCall}`
16581725
)
16591726
s.appendRight(endOffset, `})`)

packages/runtime-core/src/apiSetupHelpers.ts

+33
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ import {
77
unsetCurrentInstance
88
} from './component'
99
import { EmitFn, EmitsOptions } from './componentEmits'
10+
import {
11+
ComponentOptionsMixin,
12+
ComponentOptionsWithoutProps,
13+
ComputedOptions,
14+
MethodOptions
15+
} from './componentOptions'
1016
import {
1117
ComponentPropsOptions,
1218
ComponentObjectPropsOptions,
@@ -143,6 +149,33 @@ export function defineExpose<
143149
}
144150
}
145151

152+
export function defineOptions<
153+
RawBindings = {},
154+
D = {},
155+
C extends ComputedOptions = {},
156+
M extends MethodOptions = {},
157+
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
158+
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
159+
E extends EmitsOptions = EmitsOptions,
160+
EE extends string = string
161+
>(
162+
options?: ComponentOptionsWithoutProps<
163+
{},
164+
RawBindings,
165+
D,
166+
C,
167+
M,
168+
Mixin,
169+
Extends,
170+
E,
171+
EE
172+
> & { emits?: undefined }
173+
): void {
174+
if (__DEV__) {
175+
warnRuntimeUsage(`defineOptions`)
176+
}
177+
}
178+
146179
type NotUndefined<T> = T extends undefined ? never : T
147180

148181
type InferDefaults<T> = {

packages/runtime-core/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export {
6868
defineProps,
6969
defineEmits,
7070
defineExpose,
71+
defineOptions,
7172
withDefaults,
7273
// internal
7374
mergeDefaults,

packages/runtime-core/types/scriptSetupHelpers.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
type _defineProps = typeof defineProps
44
type _defineEmits = typeof defineEmits
55
type _defineExpose = typeof defineExpose
6+
type _defineOptions = typeof defineOptions
67
type _withDefaults = typeof withDefaults
78

89
declare global {
910
const defineProps: _defineProps
1011
const defineEmits: _defineEmits
1112
const defineExpose: _defineExpose
13+
const defineOptions: _defineOptions
1214
const withDefaults: _withDefaults
1315
}

0 commit comments

Comments
 (0)