diff --git a/packages/runtime-core/__tests__/apiOptions.spec.ts b/packages/runtime-core/__tests__/apiOptions.spec.ts
index 836373c7..70952e17 100644
--- a/packages/runtime-core/__tests__/apiOptions.spec.ts
+++ b/packages/runtime-core/__tests__/apiOptions.spec.ts
@@ -8,7 +8,8 @@ import {
nextTick,
renderToString,
ref,
- defineComponent
+ defineComponent,
+ createApp
} from '@vue/runtime-test'
import { mockWarn } from '@vue/shared'
@@ -562,6 +563,28 @@ describe('api: options', () => {
expect(serializeInner(root)).toBe(`
1,1,3
`)
})
+ test('optionMergeStrategies', () => {
+ let merged: string
+ const App = defineComponent({
+ render() {},
+ mixins: [{ foo: 'mixin' }],
+ extends: { foo: 'extends' },
+ foo: 'local',
+ mounted() {
+ merged = this.$options.foo
+ }
+ })
+
+ const app = createApp(App)
+ app.mixin({
+ foo: 'global'
+ })
+ app.config.optionMergeStrategies.foo = (a, b) => (a ? `${a},` : ``) + b
+
+ app.mount(nodeOps.createElement('div'))
+ expect(merged!).toBe('global,extends,mixin,local')
+ })
+
describe('warnings', () => {
mockWarn()
diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts
index 3c032503..7c63f0b0 100644
--- a/packages/runtime-core/src/apiCreateApp.ts
+++ b/packages/runtime-core/src/apiCreateApp.ts
@@ -36,11 +36,21 @@ export interface App {
_context: AppContext
}
+export type OptionMergeFunction = (
+ to: unknown,
+ from: unknown,
+ instance: any,
+ key: string
+) => any
+
export interface AppConfig {
+ // @private
+ readonly isNativeTag?: (tag: string) => boolean
+
devtools: boolean
performance: boolean
- readonly isNativeTag?: (tag: string) => boolean
- isCustomElement?: (tag: string) => boolean
+ optionMergeStrategies: Record
+ isCustomElement: (tag: string) => boolean
errorHandler?: (
err: unknown,
instance: ComponentPublicInstance | null,
@@ -73,9 +83,10 @@ export type Plugin =
export function createAppContext(): AppContext {
return {
config: {
+ isNativeTag: NO,
devtools: true,
performance: false,
- isNativeTag: NO,
+ optionMergeStrategies: {},
isCustomElement: NO,
errorHandler: undefined,
warnHandler: undefined
diff --git a/packages/runtime-core/src/apiOptions.ts b/packages/runtime-core/src/apiOptions.ts
index aea55810..a33e62fb 100644
--- a/packages/runtime-core/src/apiOptions.ts
+++ b/packages/runtime-core/src/apiOptions.ts
@@ -14,7 +14,8 @@ import {
isObject,
isArray,
EMPTY_OBJ,
- NOOP
+ NOOP,
+ hasOwn
} from '@vue/shared'
import { computed } from './apiComputed'
import { watch, WatchOptions, WatchCallback } from './apiWatch'
@@ -75,11 +76,16 @@ export interface ComponentOptionsBase<
directives?: Record
inheritAttrs?: boolean
+ // Internal ------------------------------------------------------------------
+
+ // marker for AsyncComponentWrapper
+ __asyncLoader?: () => Promise
+ // cache for merged $options
+ __merged?: ComponentOptions
+
// type-only differentiator to separate OptionWithoutProps from a constructor
// type returned by defineComponent() or FunctionalComponent
call?: never
- // marker for AsyncComponentWrapper
- __asyncLoader?: () => Promise
// type-only differentiators for built-in Vnode types
__isFragment?: never
__isPortal?: never
@@ -161,7 +167,8 @@ export interface LegacyOptions<
C extends ComputedOptions,
M extends MethodOptions
> {
- el?: any
+ // allow any custom options
+ [key: string]: any
// state
// Limitation: we cannot expose RawBindings on the `this` context for data
@@ -501,3 +508,31 @@ function createWatcher(
warn(`Invalid watch option: "${key}"`)
}
}
+
+export function resolveMergedOptions(
+ instance: ComponentInternalInstance
+): ComponentOptions {
+ const raw = instance.type as ComponentOptions
+ const { __merged, mixins, extends: extendsOptions } = raw
+ if (__merged) return __merged
+ const globalMixins = instance.appContext.mixins
+ if (!globalMixins && !mixins && !extendsOptions) return raw
+ const options = {}
+ globalMixins && globalMixins.forEach(m => mergeOptions(options, m, instance))
+ extendsOptions && mergeOptions(options, extendsOptions, instance)
+ mixins && mixins.forEach(m => mergeOptions(options, m, instance))
+ mergeOptions(options, raw, instance)
+ return (raw.__merged = options)
+}
+
+function mergeOptions(to: any, from: any, instance: ComponentInternalInstance) {
+ const strats = instance.appContext.config.optionMergeStrategies
+ for (const key in from) {
+ const strat = strats && strats[key]
+ if (strat) {
+ to[key] = strat(to[key], from[key], instance.proxy, key)
+ } else if (!hasOwn(to, key)) {
+ to[key] = from[key]
+ }
+ }
+}
diff --git a/packages/runtime-core/src/componentProxy.ts b/packages/runtime-core/src/componentProxy.ts
index 78ba4339..e40fb801 100644
--- a/packages/runtime-core/src/componentProxy.ts
+++ b/packages/runtime-core/src/componentProxy.ts
@@ -6,7 +6,8 @@ import {
ExtractComputedReturns,
ComponentOptionsBase,
ComputedOptions,
- MethodOptions
+ MethodOptions,
+ resolveMergedOptions
} from './apiOptions'
import { ReactiveEffect, UnwrapRef } from '@vue/reactivity'
import { warn } from './warning'
@@ -61,7 +62,7 @@ const publicPropertiesMap: Record<
$parent: i => i.parent,
$root: i => i.root,
$emit: i => i.emit,
- $options: i => i.type,
+ $options: i => (__FEATURE_OPTIONS__ ? resolveMergedOptions(i) : i.type),
$forceUpdate: i => () => queueJob(i.update),
$nextTick: () => nextTick,
$watch: __FEATURE_OPTIONS__ ? i => instanceWatch.bind(i) : NOOP
diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts
index d6be3c56..f5fbb9af 100644
--- a/packages/runtime-core/src/index.ts
+++ b/packages/runtime-core/src/index.ts
@@ -163,7 +163,8 @@ export {
AppConfig,
AppContext,
Plugin,
- CreateAppFunction
+ CreateAppFunction,
+ OptionMergeFunction
} from './apiCreateApp'
export {
VNode,