From 7546248d79954bf16da87d41072d5df7881375f0 Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 17 Dec 2025 16:46:49 +0800 Subject: [PATCH 1/3] fix(compiler-sfc): demote const reactive bindings used in v-model --- .../__tests__/transforms/vModel.spec.ts | 17 ++++ packages/compiler-core/src/errors.ts | 2 + .../compiler-core/src/transforms/vModel.ts | 9 ++ packages/compiler-dom/src/errors.ts | 2 +- .../__snapshots__/compileScript.spec.ts.snap | 38 +++++++++ .../__tests__/compileScript.spec.ts | 81 ++++++++++++++++++ packages/compiler-sfc/src/compileScript.ts | 82 +++++++++++++++++++ 7 files changed, 230 insertions(+), 1 deletion(-) diff --git a/packages/compiler-core/__tests__/transforms/vModel.spec.ts b/packages/compiler-core/__tests__/transforms/vModel.spec.ts index 82dd4909fd6..1f27bad5c22 100644 --- a/packages/compiler-core/__tests__/transforms/vModel.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vModel.spec.ts @@ -582,5 +582,22 @@ describe('compiler: transform v-model', () => { }), ) }) + + test('used on const binding', () => { + const onError = vi.fn() + parseWithVModel('
', { + onError, + bindingMetadata: { + c: BindingTypes.LITERAL_CONST, + }, + }) + + expect(onError).toHaveBeenCalledTimes(1) + expect(onError).toHaveBeenCalledWith( + expect.objectContaining({ + code: ErrorCodes.X_V_MODEL_ON_CONST, + }), + ) + }) }) }) diff --git a/packages/compiler-core/src/errors.ts b/packages/compiler-core/src/errors.ts index 58e113ab19e..ea8e8f27048 100644 --- a/packages/compiler-core/src/errors.ts +++ b/packages/compiler-core/src/errors.ts @@ -88,6 +88,7 @@ export enum ErrorCodes { X_V_MODEL_MALFORMED_EXPRESSION, X_V_MODEL_ON_SCOPE_VARIABLE, X_V_MODEL_ON_PROPS, + X_V_MODEL_ON_CONST, X_INVALID_EXPRESSION, X_KEEP_ALIVE_INVALID_CHILDREN, @@ -176,6 +177,7 @@ export const errorMessages: Record = { [ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION]: `v-model value must be a valid JavaScript member expression.`, [ErrorCodes.X_V_MODEL_ON_SCOPE_VARIABLE]: `v-model cannot be used on v-for or v-slot scope variables because they are not writable.`, [ErrorCodes.X_V_MODEL_ON_PROPS]: `v-model cannot be used on a prop, because local prop bindings are not writable.\nUse a v-bind binding combined with a v-on listener that emits update:x event instead.`, + [ErrorCodes.X_V_MODEL_ON_CONST]: `v-model cannot be used on a const binding because it is not writable.`, [ErrorCodes.X_INVALID_EXPRESSION]: `Error parsing JavaScript expression: `, [ErrorCodes.X_KEEP_ALIVE_INVALID_CHILDREN]: ` expects exactly one child component.`, [ErrorCodes.X_VNODE_HOOKS]: `@vnode-* hooks in templates are no longer supported. Use the vue: prefix instead. For example, @vnode-mounted should be changed to @vue:mounted. @vnode-* hooks support has been removed in 3.4.`, diff --git a/packages/compiler-core/src/transforms/vModel.ts b/packages/compiler-core/src/transforms/vModel.ts index 598c1ea4387..ff722db9b54 100644 --- a/packages/compiler-core/src/transforms/vModel.ts +++ b/packages/compiler-core/src/transforms/vModel.ts @@ -48,6 +48,15 @@ export const transformModel: DirectiveTransform = (dir, node, context) => { return createTransformProps() } + // const bindings are not writable. + if ( + bindingType === BindingTypes.LITERAL_CONST || + bindingType === BindingTypes.SETUP_CONST + ) { + context.onError(createCompilerError(ErrorCodes.X_V_MODEL_ON_CONST, exp.loc)) + return createTransformProps() + } + const maybeRef = !__BROWSER__ && context.inline && diff --git a/packages/compiler-dom/src/errors.ts b/packages/compiler-dom/src/errors.ts index faf6fb56441..f7a5a44f913 100644 --- a/packages/compiler-dom/src/errors.ts +++ b/packages/compiler-dom/src/errors.ts @@ -21,7 +21,7 @@ export function createDOMCompilerError( } export enum DOMErrorCodes { - X_V_HTML_NO_EXPRESSION = 53 /* ErrorCodes.__EXTEND_POINT__ */, + X_V_HTML_NO_EXPRESSION = 54 /* ErrorCodes.__EXTEND_POINT__ */, X_V_HTML_WITH_CHILDREN, X_V_TEXT_NO_EXPRESSION, X_V_TEXT_WITH_CHILDREN, diff --git a/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap b/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap index 2acac64b0fb..795f0ae8840 100644 --- a/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap +++ b/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap @@ -639,6 +639,44 @@ return { foo, bar, baz, y, z } }" `; +exports[`SFC compile + + + `) + + expect(content).toMatch( + `let name = reactive({ first: 'john', last: 'doe' })`, + ) + expect(bindings!.name).toBe(BindingTypes.SETUP_LET) + expect(warnOnceMock).toHaveBeenCalledTimes(1) + expect(warnOnceMock).toHaveBeenCalledWith( + expect.stringContaining( + '`v-model` cannot update a `const` reactive binding', + ), + ) + assertCode(content) + }) + + test('demote const reactive binding to let when used in v-model (inlineTemplate)', () => { + warnOnceMock.mockClear() + const { content, bindings } = compile( + ` + + + + `, + { inlineTemplate: true }, + ) + + expect(content).toMatch( + `let name = reactive({ first: 'john', last: 'doe' })`, + ) + expect(bindings!.name).toBe(BindingTypes.SETUP_LET) + expect(warnOnceMock).toHaveBeenCalledTimes(1) + expect(warnOnceMock).toHaveBeenCalledWith( + expect.stringContaining( + '`v-model` cannot update a `const` reactive binding', + ), + ) + assertCode(content) + }) + + test('v-model should error on literal const bindings', () => { + expect(() => + compile( + ` + + + `, + { inlineTemplate: true }, + ), + ).toThrow('v-model cannot be used on a const binding') + }) + describe('