diff --git a/packages/runtime-core/__tests__/componentPublicInstance.spec.ts b/packages/runtime-core/__tests__/componentPublicInstance.spec.ts index 47dcb03c3ab..d3a4cef93f7 100644 --- a/packages/runtime-core/__tests__/componentPublicInstance.spec.ts +++ b/packages/runtime-core/__tests__/componentPublicInstance.spec.ts @@ -4,7 +4,8 @@ import { getCurrentInstance, nodeOps, createApp, - shallowReadonly + shallowReadonly, + defineComponent } from '@vue/runtime-test' import { ComponentInternalInstance, ComponentOptions } from '../src/component' @@ -458,4 +459,24 @@ describe('component: proxy', () => { )} was accessed during render ` + `but is not defined on instance.` ).toHaveBeenWarned() }) + + test('should prevent mutating script setup bindings', () => { + const Comp = defineComponent({ + render() {}, + setup() { + return { + __isScriptSetup: true, + foo: 1 + } + }, + mounted() { + expect('foo' in this).toBe(false) + try { + this.foo = 123 + } catch (e) {} + } + }) + render(h(Comp), nodeOps.createElement('div')) + expect(`Cannot mutate <script setup> binding "foo"`).toHaveBeenWarned() + }) }) diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts index 8f24b8dace4..695df45898a 100644 --- a/packages/runtime-core/src/componentPublicInstance.ts +++ b/packages/runtime-core/src/componentPublicInstance.ts @@ -270,6 +270,9 @@ export interface ComponentRenderContext { export const isReservedPrefix = (key: string) => key === '_' || key === '$' +const hasSetupBinding = (state: Data, key: string) => + state !== EMPTY_OBJ && !state.__isScriptSetup && hasOwn(state, key) + export const PublicInstanceProxyHandlers: ProxyHandler<any> = { get({ _: instance }: ComponentRenderContext, key: string) { const { ctx, setupState, data, props, accessCache, type, appContext } = @@ -280,19 +283,6 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = { return true } - // prioritize <script setup> bindings during dev. - // this allows even properties that start with _ or $ to be used - so that - // it aligns with the production behavior where the render fn is inlined and - // indeed has access to all declared variables. - if ( - __DEV__ && - setupState !== EMPTY_OBJ && - setupState.__isScriptSetup && - hasOwn(setupState, key) - ) { - return setupState[key] - } - // data / props / ctx // This getter gets called for every property access on the render context // during render and is a major hotspot. The most expensive part of this @@ -314,7 +304,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = { return props![key] // default: just fallthrough } - } else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) { + } else if (hasSetupBinding(setupState, key)) { accessCache![key] = AccessTypes.SETUP return setupState[key] } else if (data !== EMPTY_OBJ && hasOwn(data, key)) { @@ -403,26 +393,28 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = { value: any ): boolean { const { data, setupState, ctx } = instance - if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) { + if (hasSetupBinding(setupState, key)) { setupState[key] = value return true + } else if ( + __DEV__ && + setupState.__isScriptSetup && + hasOwn(setupState, key) + ) { + warn(`Cannot mutate <script setup> binding "${key}" from Options API.`) + return false } else if (data !== EMPTY_OBJ && hasOwn(data, key)) { data[key] = value return true } else if (hasOwn(instance.props, key)) { - __DEV__ && - warn( - `Attempting to mutate prop "${key}". Props are readonly.`, - instance - ) + __DEV__ && warn(`Attempting to mutate prop "${key}". Props are readonly.`) return false } if (key[0] === '$' && key.slice(1) in instance) { __DEV__ && warn( `Attempting to mutate public property "${key}". ` + - `Properties starting with $ are reserved and readonly.`, - instance + `Properties starting with $ are reserved and readonly.` ) return false } else { @@ -449,7 +441,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = { return ( !!accessCache![key] || (data !== EMPTY_OBJ && hasOwn(data, key)) || - (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) || + hasSetupBinding(setupState, key) || ((normalizedProps = propsOptions[0]) && hasOwn(normalizedProps, key)) || hasOwn(ctx, key) || hasOwn(publicPropertiesMap, key) ||