From fc0df0d9afd5c961a50cd1e4c6d647eb90819572 Mon Sep 17 00:00:00 2001 From: Alex Snezhko Date: Sat, 4 Oct 2025 21:30:02 -0700 Subject: [PATCH 1/2] fix(compiler-ssr): expand v-model `option` selected inclusion logic --- .../compiler-ssr/__tests__/ssrVModel.spec.ts | 111 +++++++++++++++ .../compiler-ssr/src/transforms/ssrVModel.ts | 129 ++++++++++++++++-- 2 files changed, 228 insertions(+), 12 deletions(-) diff --git a/packages/compiler-ssr/__tests__/ssrVModel.spec.ts b/packages/compiler-ssr/__tests__/ssrVModel.spec.ts index 8a439dbf4b5..11c5cbf0fc4 100644 --- a/packages/compiler-ssr/__tests__/ssrVModel.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrVModel.spec.ts @@ -292,6 +292,117 @@ describe('ssr: v-model', () => { _push(\`\`) }" `) + + expect( + compileWithWrapper( + ``, + ).code, + ).toMatchInlineSnapshot(` + "const { ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`\`) + }" + `) + + expect( + compileWithWrapper( + ``, + ).code, + ).toMatchInlineSnapshot(` + "const { ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`\`) + }" + `) + + expect( + compileWithWrapper( + ``, + ).code, + ).toMatchInlineSnapshot(` + "const { ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`\`) + }" + `) + + expect( + compileWithWrapper( + ``, + ).code, + ).toMatchInlineSnapshot(` + "const { ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`\`) + }" + `) + + expect( + compileWithWrapper(``) + .code, + ).toMatchInlineSnapshot(` + "const { ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`\`) + }" + `) }) test('', () => { diff --git a/packages/compiler-ssr/src/transforms/ssrVModel.ts b/packages/compiler-ssr/src/transforms/ssrVModel.ts index cbe5b2b42a3..707bf8442d8 100644 --- a/packages/compiler-ssr/src/transforms/ssrVModel.ts +++ b/packages/compiler-ssr/src/transforms/ssrVModel.ts @@ -6,12 +6,16 @@ import { NodeTypes, type PlainElementNode, type TemplateChildNode, + type TemplateLiteral, + type TextNode, createCallExpression, createConditionalExpression, createDOMCompilerError, createInterpolation, createObjectProperty, createSimpleExpression, + createTemplateLiteral, + findDir, findProp, hasDynamicKeyVBind, transformModel, @@ -54,21 +58,26 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => { function processOption(plainNode: PlainElementNode) { if (plainNode.tag === 'option') { if (plainNode.props.findIndex(p => p.name === 'selected') === -1) { - const value = findValueBinding(plainNode) + const value = findOptionValue(plainNode) plainNode.ssrCodegenNode!.elements.push( createConditionalExpression( createCallExpression(context.helper(SSR_INCLUDE_BOOLEAN_ATTR), [ - createConditionalExpression( - createCallExpression(`Array.isArray`, [model]), - createCallExpression(context.helper(SSR_LOOSE_CONTAIN), [ - model, - value, - ]), - createCallExpression(context.helper(SSR_LOOSE_EQUAL), [ - model, - value, - ]), - ), + value.maybeArray + ? createConditionalExpression( + createCallExpression(`Array.isArray`, [model]), + createCallExpression(context.helper(SSR_LOOSE_CONTAIN), [ + model, + value.node, + ]), + createCallExpression(context.helper(SSR_LOOSE_EQUAL), [ + model, + value.node, + ]), + ) + : createCallExpression(context.helper(SSR_LOOSE_EQUAL), [ + model, + value.node, + ]), ]), createSimpleExpression(' selected', true), createSimpleExpression('', true), @@ -190,6 +199,64 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => { } } +interface OptionValue { + node: ExpressionNode | TemplateLiteral + maybeArray: boolean +} + +function findOptionValue(node: PlainElementNode): OptionValue { + const valueBinding = findProp(node, 'value') + if (valueBinding) { + return { + node: + valueBinding.type === NodeTypes.DIRECTIVE + ? valueBinding.exp! + : createSimpleExpression(valueBinding.value!.content, true), + maybeArray: true, + } + } + + const textDir = findDir(node, 'text') + if (textDir) { + return { node: textDir.exp!, maybeArray: false } + } + + if ( + node.children.every( + x => + x.type === NodeTypes.TEXT || + x.type === NodeTypes.COMMENT || + x.type === NodeTypes.INTERPOLATION, + ) + ) { + const relevantNodes = collapseTextBetweenComments(node.children).filter( + x => x.type !== NodeTypes.COMMENT, + ) + if (relevantNodes.length) { + const textContentValue = createTemplateLiteral( + relevantNodes.map((x, i) => { + if (x.type === NodeTypes.TEXT) { + let content = x.content + if (i === 0) { + content = content.trimStart() + } else if (i === relevantNodes.length - 1) { + content = content.trimEnd() + } + return createSimpleExpression(content, true) + } else { + return x.content + } + }), + ) + if (textContentValue) { + return { node: textContentValue, maybeArray: false } + } + } + } + + return { node: createSimpleExpression(``, true), maybeArray: false } +} + function findValueBinding(node: PlainElementNode): ExpressionNode { const valueBinding = findProp(node, 'value') return valueBinding @@ -198,3 +265,41 @@ function findValueBinding(node: PlainElementNode): ExpressionNode { : createSimpleExpression(valueBinding.value!.content, true) : createSimpleExpression(`null`, false) } + +function collapseTextBetweenComments( + children: T[], +) { + const result: (T | TextNode)[] = [] + let prevTextNode: TextNode | undefined + for (let i = 0; i < children.length; i++) { + const child = children[i] + if (child.type === NodeTypes.TEXT) { + if (prevTextNode) { + const prevContent = prevTextNode.content + let thisContent = child.content + if (prevContent.endsWith(' ') && thisContent.startsWith(' ')) { + thisContent = thisContent.slice(1) + } + const combined: TextNode = { + ...prevTextNode, + content: prevContent + thisContent, + } + prevTextNode = combined + } else { + prevTextNode = child + } + } else if (child.type === NodeTypes.COMMENT) { + continue + } else { + if (prevTextNode) { + result.push(prevTextNode) + prevTextNode = undefined + } + result.push(child) + } + } + if (prevTextNode) { + result.push(prevTextNode) + } + return result +} From 15d6f922df89216b4a0455559329d8721fcff644 Mon Sep 17 00:00:00 2001 From: Alex Snezhko Date: Sat, 4 Oct 2025 21:56:15 -0700 Subject: [PATCH 2/2] fix: single text element trailing whitespace --- .../compiler-ssr/__tests__/ssrVModel.spec.ts | 16 ++++++++++++++++ .../compiler-ssr/src/transforms/ssrVModel.ts | 3 ++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/compiler-ssr/__tests__/ssrVModel.spec.ts b/packages/compiler-ssr/__tests__/ssrVModel.spec.ts index 11c5cbf0fc4..8afdda39b3d 100644 --- a/packages/compiler-ssr/__tests__/ssrVModel.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrVModel.spec.ts @@ -309,6 +309,22 @@ describe('ssr: v-model', () => { }" `) + expect( + compileWithWrapper( + ``, + ).code, + ).toMatchInlineSnapshot(` + "const { ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`\`) + }" + `) + expect( compileWithWrapper( ``, diff --git a/packages/compiler-ssr/src/transforms/ssrVModel.ts b/packages/compiler-ssr/src/transforms/ssrVModel.ts index 707bf8442d8..5213c199500 100644 --- a/packages/compiler-ssr/src/transforms/ssrVModel.ts +++ b/packages/compiler-ssr/src/transforms/ssrVModel.ts @@ -239,7 +239,8 @@ function findOptionValue(node: PlainElementNode): OptionValue { let content = x.content if (i === 0) { content = content.trimStart() - } else if (i === relevantNodes.length - 1) { + } + if (i === relevantNodes.length - 1) { content = content.trimEnd() } return createSimpleExpression(content, true)