From f32d14825b245b6e3b37f5df630ddd995f295881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=85=A7=E5=9D=A4?= Date: Mon, 15 Sep 2025 17:49:19 +0800 Subject: [PATCH 01/11] fix(TransitionGroup): filter out transition-specific props to avoid invalid HTML attributes - Add props filtering logic to exclude transition-specific and TransitionGroup-specific props - Prevents invalid HTML attributes like 'name' from being applied to DOM elements - Fixes #13037 where TransitionGroup with tag='ul' was generating invalid HTML The filtering ensures only valid HTML attributes are passed to the rendered element, resolving W3C validation errors when using TransitionGroup with specific tags. --- .../runtime-dom/src/components/TransitionGroup.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/runtime-dom/src/components/TransitionGroup.ts b/packages/runtime-dom/src/components/TransitionGroup.ts index 33a1533c725..94d66e90d09 100644 --- a/packages/runtime-dom/src/components/TransitionGroup.ts +++ b/packages/runtime-dom/src/components/TransitionGroup.ts @@ -130,6 +130,19 @@ const TransitionGroupImpl: ComponentOptions = /*@__PURE__*/ decorate({ tag = 'span' } + // Filter out transition-specific props and TransitionGroup-specific props + // to avoid invalid HTML attributes + const filteredProps: Record = {} + for (const key in rawProps) { + if ( + !(key in TransitionPropsValidators) && + key !== 'tag' && + key !== 'moveClass' + ) { + filteredProps[key] = (rawProps as any)[key] + } + } + prevChildren = [] if (children) { for (let i = 0; i < children.length; i++) { @@ -167,7 +180,7 @@ const TransitionGroupImpl: ComponentOptions = /*@__PURE__*/ decorate({ } } - return createVNode(tag, null, children) + return createVNode(tag, filteredProps, children) } }, }) From 2a131487e6dcc4b1b81288c730939ee903df89df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=85=A7=E5=9D=A4?= Date: Tue, 16 Sep 2025 09:19:31 +0800 Subject: [PATCH 02/11] =?UTF-8?q?fix(TransitionGroup):=20=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=20hasOwn=20=E5=87=BD=E6=95=B0=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=B1=9E=E6=80=A7=E8=BF=87=E6=BB=A4=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 TransitionGroup 中引入 hasOwn 函数,替代原有的属性检查方式 - 确保仅有效的 HTML 属性被传递到渲染的元素中,进一步避免无效的 HTML 属性问题 --- packages/runtime-dom/src/components/TransitionGroup.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/runtime-dom/src/components/TransitionGroup.ts b/packages/runtime-dom/src/components/TransitionGroup.ts index 94d66e90d09..8e326b25b50 100644 --- a/packages/runtime-dom/src/components/TransitionGroup.ts +++ b/packages/runtime-dom/src/components/TransitionGroup.ts @@ -27,7 +27,7 @@ import { useTransitionState, warn, } from '@vue/runtime-core' -import { extend } from '@vue/shared' +import { extend, hasOwn } from '@vue/shared' const positionMap = new WeakMap() const newPositionMap = new WeakMap() @@ -135,7 +135,7 @@ const TransitionGroupImpl: ComponentOptions = /*@__PURE__*/ decorate({ const filteredProps: Record = {} for (const key in rawProps) { if ( - !(key in TransitionPropsValidators) && + !hasOwn(TransitionPropsValidators, key) && key !== 'tag' && key !== 'moveClass' ) { @@ -179,8 +179,7 @@ const TransitionGroupImpl: ComponentOptions = /*@__PURE__*/ decorate({ warn(` children must be keyed.`) } } - - return createVNode(tag, filteredProps, children) + return createVNode(tag, tag === Fragment ? null : filteredProps, children) } }, }) From d65747a82ffc8041a668a269dc06db503d9291ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=85=A7=E5=9D=A4?= Date: Thu, 18 Sep 2025 08:37:36 +0800 Subject: [PATCH 03/11] fix(TransitionGroup): filter private props to avoid invalid HTML attributes - Add filtering for `name` prop in SSR transform logic - Prevents `name` from being rendered as DOM attribute (e.g. `
    `) - Fixes #13037 (invalid HTML from TransitionGroup with custom `tag`) Ensures only valid HTML attributes are passed to the rendered element, resolving W3C validation issues. --- .../src/transforms/ssrTransformTransitionGroup.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts b/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts index 27ddebec103..930e2b48daf 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts @@ -32,7 +32,17 @@ export function ssrTransformTransitionGroup( return (): void => { const tag = findProp(node, 'tag') if (tag) { - const otherProps = node.props.filter(p => p !== tag) + // 在处理 TransitionGroup 的属性时,过滤掉 name/tag 等私有 props + const otherProps = node.props.filter(p => { + // 排除 tag(已单独处理)和 name(私有 props,不该透传) + if ( + p === tag || + (p.type === NodeTypes.ATTRIBUTE && p.name === 'name') + ) { + return false + } + return true + }) const { props, directives } = buildProps( node, context, From ca7407e2bdb52f9aaddef55c1097ddb4d7e31ad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=85=A7=E5=9D=A4?= Date: Thu, 18 Sep 2025 20:01:42 +0800 Subject: [PATCH 04/11] fix(compiler-ssr): filter out all transition-specific props in TransitionGroup SSR transform This change improves the SSR transform for TransitionGroup by properly filtering out all transition-specific props that should not be passed through to the rendered element. The implementation: 1. Re-creates the transition props validators structure to mirror runtime logic 2. Filters out both static and dynamic transition-specific props 3. Handles both camelCase and kebab-case prop names 4. Excludes TransitionGroup-specific props like moveClass/move-class 5. Adds comprehensive test coverage for prop filtering This ensures that only relevant props are passed to the rendered element in SSR, matching the behavior of client-side rendering. --- .../__tests__/ssrTransitionGroup.spec.ts | 70 +++++++++++++++ .../transforms/ssrTransformTransitionGroup.ts | 90 +++++++++++++++++-- 2 files changed, 154 insertions(+), 6 deletions(-) diff --git a/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts b/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts index 82122e621c7..cf7806e7126 100644 --- a/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts @@ -147,4 +147,74 @@ describe('transition-group', () => { }" `) }) + + test('filters out transition-specific props', () => { + expect( + compile( + ` + `, + ).code, + ).toMatchInlineSnapshot(` + "const { mergeProps: _mergeProps } = require("vue") + const { ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`
\`) + }" + `) + }) + + test('filters out moveClass prop', () => { + expect( + compile( + ` + `, + ).code, + ).toMatchInlineSnapshot(` + "const { mergeProps: _mergeProps } = require("vue") + const { ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`\`) + }" + `) + }) + + test('filters out dynamic transition props', () => { + expect( + compile( + ` + `, + ).code, + ).toMatchInlineSnapshot(` + "const { mergeProps: _mergeProps } = require("vue") + const { ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`\`) + }" + `) + }) + + test('filters out transition event handlers', () => { + expect( + compile( + ` + `, + ).code, + ).toMatchInlineSnapshot(` + "const { mergeProps: _mergeProps } = require("vue") + const { ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`\`) + }" + `) + }) }) diff --git a/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts b/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts index 930e2b48daf..44f368aa642 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts @@ -9,6 +9,7 @@ import { createCallExpression, findProp, } from '@vue/compiler-dom' +import { hasOwn } from '@vue/shared' import { SSR_RENDER_ATTRS } from '../runtimeHelpers' import { type SSRTransformContext, @@ -16,6 +17,55 @@ import { } from '../ssrCodegenTransform' import { buildSSRProps } from './ssrTransformElement' +// Import transition props validators from the runtime +const TransitionPropsValidators = (() => { + // Re-create the TransitionPropsValidators structure that's used at runtime + // This mirrors the logic from @vue/runtime-dom/src/components/Transition.ts + const BaseTransitionPropsValidators = { + mode: String, + appear: Boolean, + persisted: Boolean, + onBeforeEnter: [Function, Array], + onEnter: [Function, Array], + onAfterEnter: [Function, Array], + onEnterCancelled: [Function, Array], + onBeforeLeave: [Function, Array], + onLeave: [Function, Array], + onAfterLeave: [Function, Array], + onLeaveCancelled: [Function, Array], + onBeforeAppear: [Function, Array], + onAppear: [Function, Array], + onAfterAppear: [Function, Array], + onAppearCancelled: [Function, Array], + } + + const DOMTransitionPropsValidators = { + name: String, + type: String, + css: { type: Boolean, default: true }, + duration: [String, Number, Object], + enterFromClass: String, + enterActiveClass: String, + enterToClass: String, + appearFromClass: String, + appearActiveClass: String, + appearToClass: String, + leaveFromClass: String, + leaveActiveClass: String, + leaveToClass: String, + } + + return { + ...BaseTransitionPropsValidators, + ...DOMTransitionPropsValidators, + } +})() + +// Helper function to convert kebab-case to camelCase +function kebabToCamel(str: string): string { + return str.replace(/-([a-z])/g, (_, char) => char.toUpperCase()) +} + const wipMap = new WeakMap() interface WIPEntry { @@ -32,15 +82,43 @@ export function ssrTransformTransitionGroup( return (): void => { const tag = findProp(node, 'tag') if (tag) { - // 在处理 TransitionGroup 的属性时,过滤掉 name/tag 等私有 props + // 在处理 TransitionGroup 的属性时,过滤掉所有 transition 相关的私有 props const otherProps = node.props.filter(p => { - // 排除 tag(已单独处理)和 name(私有 props,不该透传) - if ( - p === tag || - (p.type === NodeTypes.ATTRIBUTE && p.name === 'name') - ) { + // 排除 tag(已单独处理) + if (p === tag) { return false } + + // 排除所有 transition 相关的属性和 TransitionGroup 特有的属性 + // 这里的逻辑镜像了运行时 TransitionGroup 的属性过滤逻辑 + if (p.type === NodeTypes.ATTRIBUTE) { + // 静态属性:检查属性名(支持 kebab-case 转 camelCase) + const propName = p.name + const camelCaseName = kebabToCamel(propName) + return ( + !hasOwn(TransitionPropsValidators, propName) && + !hasOwn(TransitionPropsValidators, camelCaseName) && + propName !== 'moveClass' && + propName !== 'move-class' + ) + } else if (p.type === NodeTypes.DIRECTIVE && p.name === 'bind') { + // 动态属性:检查绑定的属性名 + if ( + p.arg && + p.arg.type === NodeTypes.SIMPLE_EXPRESSION && + p.arg.isStatic + ) { + const argName = p.arg.content + const camelCaseArgName = kebabToCamel(argName) + return ( + !hasOwn(TransitionPropsValidators, argName) && + !hasOwn(TransitionPropsValidators, camelCaseArgName) && + argName !== 'moveClass' && + argName !== 'move-class' + ) + } + } + return true }) const { props, directives } = buildProps( From 81615a2177e54650ba4184054a21c9192957097e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=85=A7=E5=9D=A4?= Date: Thu, 18 Sep 2025 20:37:48 +0800 Subject: [PATCH 05/11] refactor: update comments to English (previously Chinese) --- .../__tests__/ssrTransitionGroup.spec.ts | 16 +++++++++ .../transforms/ssrTransformTransitionGroup.ts | 36 +++++++++---------- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts b/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts index cf7806e7126..ccb29fee3c3 100644 --- a/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts @@ -217,4 +217,20 @@ describe('transition-group', () => { }" `) }) + + test('filters out all transition props including empty values', () => { + expect( + compile( + ` + `, + ).code, + ).toMatchInlineSnapshot(` + "const { mergeProps: _mergeProps } = require("vue") + const { ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`\`) + }" + `) + }) }) diff --git a/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts b/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts index 44f368aa642..dbe947f2057 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts @@ -82,27 +82,27 @@ export function ssrTransformTransitionGroup( return (): void => { const tag = findProp(node, 'tag') if (tag) { - // 在处理 TransitionGroup 的属性时,过滤掉所有 transition 相关的私有 props + // Filter out all transition-related private props when processing TransitionGroup attributes const otherProps = node.props.filter(p => { - // 排除 tag(已单独处理) + // Exclude tag (already handled separately) if (p === tag) { return false } - // 排除所有 transition 相关的属性和 TransitionGroup 特有的属性 - // 这里的逻辑镜像了运行时 TransitionGroup 的属性过滤逻辑 + // Exclude all transition-related attributes and TransitionGroup-specific attributes + // This logic mirrors the runtime TransitionGroup attribute filtering logic if (p.type === NodeTypes.ATTRIBUTE) { - // 静态属性:检查属性名(支持 kebab-case 转 camelCase) + // Static attributes: check attribute name (supports kebab-case to camelCase conversion) const propName = p.name const camelCaseName = kebabToCamel(propName) - return ( - !hasOwn(TransitionPropsValidators, propName) && - !hasOwn(TransitionPropsValidators, camelCaseName) && - propName !== 'moveClass' && - propName !== 'move-class' - ) + const shouldFilter = + hasOwn(TransitionPropsValidators, propName) || + hasOwn(TransitionPropsValidators, camelCaseName) || + propName === 'moveClass' || + propName === 'move-class' + return !shouldFilter } else if (p.type === NodeTypes.DIRECTIVE && p.name === 'bind') { - // 动态属性:检查绑定的属性名 + // Dynamic attributes: check bound attribute name if ( p.arg && p.arg.type === NodeTypes.SIMPLE_EXPRESSION && @@ -110,12 +110,12 @@ export function ssrTransformTransitionGroup( ) { const argName = p.arg.content const camelCaseArgName = kebabToCamel(argName) - return ( - !hasOwn(TransitionPropsValidators, argName) && - !hasOwn(TransitionPropsValidators, camelCaseArgName) && - argName !== 'moveClass' && - argName !== 'move-class' - ) + const shouldFilter = + hasOwn(TransitionPropsValidators, argName) || + hasOwn(TransitionPropsValidators, camelCaseArgName) || + argName === 'moveClass' || + argName === 'move-class' + return !shouldFilter } } From a324b4ed8cbc33487248636883dda528205ade5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=85=A7=E5=9D=A4?= Date: Thu, 18 Sep 2025 21:22:06 +0800 Subject: [PATCH 06/11] refactor(TransitionGroup): improve props handling and filtering - Replace TransitionPropsValidators with direct props definition to exclude 'mode' - Combine BaseTransition props with DOM-specific transition props explicitly - Remove manual props filtering logic by properly defining allowed props - Clean up unnecessary hasOwn import since it's no longer used - Simplify vnode creation by removing filteredProps --- .../__tests__/ssrTransitionGroup.spec.ts | 21 +++++++ .../src/components/TransitionGroup.ts | 55 +++++++++++-------- 2 files changed, 53 insertions(+), 23 deletions(-) diff --git a/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts b/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts index ccb29fee3c3..a785becc5cf 100644 --- a/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts @@ -148,6 +148,27 @@ describe('transition-group', () => { `) }) + test('transition props should NOT fallthrough (runtime should handle this)', () => { + // This test verifies that if runtime fallthrough is working correctly, + // SSR should still filter out transition props for clean HTML + expect( + compile( + ` + `, + ).code, + ).toMatchInlineSnapshot(` + "const { mergeProps: _mergeProps } = require("vue") + const { ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`\`) + }" + `) + }) + test('filters out transition-specific props', () => { expect( compile( diff --git a/packages/runtime-dom/src/components/TransitionGroup.ts b/packages/runtime-dom/src/components/TransitionGroup.ts index 8e326b25b50..59295f95eee 100644 --- a/packages/runtime-dom/src/components/TransitionGroup.ts +++ b/packages/runtime-dom/src/components/TransitionGroup.ts @@ -1,7 +1,6 @@ import { type ElementWithTransition, type TransitionProps, - TransitionPropsValidators, addTransitionClass, forceReflow, getTransitionInfo, @@ -9,6 +8,7 @@ import { resolveTransitionProps, vtcKey, } from './Transition' +import { BaseTransitionPropsValidators } from '@vue/runtime-core' import { type ComponentOptions, DeprecationTypes, @@ -27,7 +27,7 @@ import { useTransitionState, warn, } from '@vue/runtime-core' -import { extend, hasOwn } from '@vue/shared' +import { extend } from '@vue/shared' const positionMap = new WeakMap() const newPositionMap = new WeakMap() @@ -44,9 +44,7 @@ export type TransitionGroupProps = Omit & { * so that it can be annotated as pure */ const decorate = (t: typeof TransitionGroupImpl) => { - // TransitionGroup does not support "mode" so we need to remove it from the - // props declarations, but direct delete operation is considered a side effect - delete t.props.mode + // TransitionGroup does not support "mode", already excluded from props definition if (__COMPAT__) { t.__isBuiltIn = true } @@ -56,10 +54,34 @@ const decorate = (t: typeof TransitionGroupImpl) => { const TransitionGroupImpl: ComponentOptions = /*@__PURE__*/ decorate({ name: 'TransitionGroup', - props: /*@__PURE__*/ extend({}, TransitionPropsValidators, { - tag: String, - moveClass: String, - }), + props: /*@__PURE__*/ (() => { + // Create TransitionGroup props by combining BaseTransition props (excluding mode) + // with DOM-specific transition props and TransitionGroup-specific props + const DOMTransitionPropsValidators = { + name: String, + type: String, + css: { type: Boolean, default: true }, + duration: [String, Number, Object], + enterFromClass: String, + enterActiveClass: String, + enterToClass: String, + appearFromClass: String, + appearActiveClass: String, + appearToClass: String, + leaveFromClass: String, + leaveActiveClass: String, + leaveToClass: String, + } + + // Combine all props except 'mode' (which TransitionGroup doesn't support) + const baseProps = extend({}, BaseTransitionPropsValidators) + delete baseProps.mode + + return extend({}, baseProps, DOMTransitionPropsValidators, { + tag: String, + moveClass: String, + }) + })(), setup(props: TransitionGroupProps, { slots }: SetupContext) { const instance = getCurrentInstance()! @@ -130,19 +152,6 @@ const TransitionGroupImpl: ComponentOptions = /*@__PURE__*/ decorate({ tag = 'span' } - // Filter out transition-specific props and TransitionGroup-specific props - // to avoid invalid HTML attributes - const filteredProps: Record = {} - for (const key in rawProps) { - if ( - !hasOwn(TransitionPropsValidators, key) && - key !== 'tag' && - key !== 'moveClass' - ) { - filteredProps[key] = (rawProps as any)[key] - } - } - prevChildren = [] if (children) { for (let i = 0; i < children.length; i++) { @@ -179,7 +188,7 @@ const TransitionGroupImpl: ComponentOptions = /*@__PURE__*/ decorate({ warn(` children must be keyed.`) } } - return createVNode(tag, tag === Fragment ? null : filteredProps, children) + return createVNode(tag, null, children) } }, }) From a64f7f3f8142a2b2519ac276c488c8f110b7d1f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=85=A7=E5=9D=A4?= Date: Fri, 19 Sep 2025 15:51:00 +0800 Subject: [PATCH 07/11] refactor(TransitionGroup): from main branchprocess for TransitionGroup --- .../src/components/TransitionGroup.ts | 39 +++++-------------- 1 file changed, 9 insertions(+), 30 deletions(-) diff --git a/packages/runtime-dom/src/components/TransitionGroup.ts b/packages/runtime-dom/src/components/TransitionGroup.ts index 59295f95eee..33a1533c725 100644 --- a/packages/runtime-dom/src/components/TransitionGroup.ts +++ b/packages/runtime-dom/src/components/TransitionGroup.ts @@ -1,6 +1,7 @@ import { type ElementWithTransition, type TransitionProps, + TransitionPropsValidators, addTransitionClass, forceReflow, getTransitionInfo, @@ -8,7 +9,6 @@ import { resolveTransitionProps, vtcKey, } from './Transition' -import { BaseTransitionPropsValidators } from '@vue/runtime-core' import { type ComponentOptions, DeprecationTypes, @@ -44,7 +44,9 @@ export type TransitionGroupProps = Omit & { * so that it can be annotated as pure */ const decorate = (t: typeof TransitionGroupImpl) => { - // TransitionGroup does not support "mode", already excluded from props definition + // TransitionGroup does not support "mode" so we need to remove it from the + // props declarations, but direct delete operation is considered a side effect + delete t.props.mode if (__COMPAT__) { t.__isBuiltIn = true } @@ -54,34 +56,10 @@ const decorate = (t: typeof TransitionGroupImpl) => { const TransitionGroupImpl: ComponentOptions = /*@__PURE__*/ decorate({ name: 'TransitionGroup', - props: /*@__PURE__*/ (() => { - // Create TransitionGroup props by combining BaseTransition props (excluding mode) - // with DOM-specific transition props and TransitionGroup-specific props - const DOMTransitionPropsValidators = { - name: String, - type: String, - css: { type: Boolean, default: true }, - duration: [String, Number, Object], - enterFromClass: String, - enterActiveClass: String, - enterToClass: String, - appearFromClass: String, - appearActiveClass: String, - appearToClass: String, - leaveFromClass: String, - leaveActiveClass: String, - leaveToClass: String, - } - - // Combine all props except 'mode' (which TransitionGroup doesn't support) - const baseProps = extend({}, BaseTransitionPropsValidators) - delete baseProps.mode - - return extend({}, baseProps, DOMTransitionPropsValidators, { - tag: String, - moveClass: String, - }) - })(), + props: /*@__PURE__*/ extend({}, TransitionPropsValidators, { + tag: String, + moveClass: String, + }), setup(props: TransitionGroupProps, { slots }: SetupContext) { const instance = getCurrentInstance()! @@ -188,6 +166,7 @@ const TransitionGroupImpl: ComponentOptions = /*@__PURE__*/ decorate({ warn(` children must be keyed.`) } } + return createVNode(tag, null, children) } }, From 9f665ff1bda6967b7f07ecf0261853b80020eb1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=85=A7=E5=9D=A4?= Date: Tue, 23 Sep 2025 16:16:26 +0800 Subject: [PATCH 08/11] =?UTF-8?q?fix(compiler-ssr):=20filter=20v-bind=20ob?= =?UTF-8?q?ject=20props=20in=20TransitionGroup=20SSR=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=E2=94=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/transforms/ssrTransformTransitionGroup.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts b/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts index dbe947f2057..98900abf02e 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts @@ -116,6 +116,10 @@ export function ssrTransformTransitionGroup( argName === 'moveClass' || argName === 'move-class' return !shouldFilter + } else if (!p.arg) { + // v-bind without argument (e.g., v-bind="props") - filter out entirely + // since it may contain transition-specific props that should not be rendered + return false } } From bf98abeb4045db4873510d24577437de01650224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=85=A7=E5=9D=A4?= Date: Tue, 23 Sep 2025 17:11:00 +0800 Subject: [PATCH 09/11] fix(compiler-ssr): runtime filter for v-bind object in TransitionGroup SSR Replace compile-time removal of v-bind objects with runtime filtering to preserve valid attributes (id, data-*, etc.) while filtering transition-specific props. - Add SSR_FILTER_TRANSITION_PROPS runtime helper - Detect user-written v-bind objects (exclude compiler-generated _attrs) - Apply runtime filtering only when object v-bind is present - Preserve existing behavior for single prop bindings - Add comprehensive SSR tests for mixed object/prop scenarios --- .../__tests__/ssrTransitionGroup.spec.ts | 51 +++++++++++++++++++ packages/compiler-ssr/src/runtimeHelpers.ts | 4 ++ .../transforms/ssrTransformTransitionGroup.ts | 32 +++++++++--- .../src/helpers/ssrRenderAttrs.ts | 30 +++++++++++ packages/server-renderer/src/internal.ts | 1 + 5 files changed, 112 insertions(+), 6 deletions(-) diff --git a/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts b/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts index a785becc5cf..df8cb3992bc 100644 --- a/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts @@ -254,4 +254,55 @@ describe('transition-group', () => { }" `) }) + + test('object v-bind with mixed valid and transition props', () => { + expect( + compile( + ` + `, + ).code, + ).toMatchInlineSnapshot(` + "const { mergeProps: _mergeProps } = require("vue") + const { ssrRenderAttrs: _ssrRenderAttrs, ssrFilterTransitionProps: _ssrFilterTransitionProps } = require("vue/server-renderer") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`\`) + }" + `) + }) + + test('object v-bind filters runtime computed transition props', () => { + expect( + compile( + ` + `, + ).code, + ).toMatchInlineSnapshot(` + "const { mergeProps: _mergeProps } = require("vue") + const { ssrRenderAttrs: _ssrRenderAttrs, ssrFilterTransitionProps: _ssrFilterTransitionProps } = require("vue/server-renderer") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`\`) + }" + `) + }) + + test('mixed single prop bindings and object v-bind', () => { + expect( + compile( + ` + `, + ).code, + ).toMatchInlineSnapshot(` + "const { mergeProps: _mergeProps } = require("vue") + const { ssrRenderAttrs: _ssrRenderAttrs, ssrFilterTransitionProps: _ssrFilterTransitionProps } = require("vue/server-renderer") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`\`) + }" + `) + }) }) diff --git a/packages/compiler-ssr/src/runtimeHelpers.ts b/packages/compiler-ssr/src/runtimeHelpers.ts index 0e2c8c67bed..0311e5f0802 100644 --- a/packages/compiler-ssr/src/runtimeHelpers.ts +++ b/packages/compiler-ssr/src/runtimeHelpers.ts @@ -27,6 +27,9 @@ export const SSR_RENDER_TELEPORT: unique symbol = Symbol(`ssrRenderTeleport`) export const SSR_RENDER_SUSPENSE: unique symbol = Symbol(`ssrRenderSuspense`) export const SSR_GET_DIRECTIVE_PROPS: unique symbol = Symbol(`ssrGetDirectiveProps`) +export const SSR_FILTER_TRANSITION_PROPS: unique symbol = Symbol( + `ssrFilterTransitionProps`, +) export const ssrHelpers: Record = { [SSR_INTERPOLATE]: `ssrInterpolate`, @@ -48,6 +51,7 @@ export const ssrHelpers: Record = { [SSR_RENDER_TELEPORT]: `ssrRenderTeleport`, [SSR_RENDER_SUSPENSE]: `ssrRenderSuspense`, [SSR_GET_DIRECTIVE_PROPS]: `ssrGetDirectiveProps`, + [SSR_FILTER_TRANSITION_PROPS]: `ssrFilterTransitionProps`, } // Note: these are helpers imported from @vue/server-renderer diff --git a/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts b/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts index 98900abf02e..b24d9561f5a 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts @@ -10,7 +10,10 @@ import { findProp, } from '@vue/compiler-dom' import { hasOwn } from '@vue/shared' -import { SSR_RENDER_ATTRS } from '../runtimeHelpers' +import { + SSR_FILTER_TRANSITION_PROPS, + SSR_RENDER_ATTRS, +} from '../runtimeHelpers' import { type SSRTransformContext, processChildren, @@ -72,6 +75,7 @@ interface WIPEntry { tag: AttributeNode | DirectiveNode propsExp: string | JSChildNode | null scopeId: string | null + sawObjectVBind: boolean } // phase 1: build props @@ -82,6 +86,9 @@ export function ssrTransformTransitionGroup( return (): void => { const tag = findProp(node, 'tag') if (tag) { + // Track whether we saw object v-bind (v-bind without argument) + let sawObjectVBind = false + // Filter out all transition-related private props when processing TransitionGroup attributes const otherProps = node.props.filter(p => { // Exclude tag (already handled separately) @@ -116,10 +123,16 @@ export function ssrTransformTransitionGroup( argName === 'moveClass' || argName === 'move-class' return !shouldFilter - } else if (!p.arg) { - // v-bind without argument (e.g., v-bind="props") - filter out entirely - // since it may contain transition-specific props that should not be rendered - return false + } else if ( + !p.arg && + p.exp && + p.exp.type === NodeTypes.SIMPLE_EXPRESSION && + p.exp.content !== '_attrs' + ) { + // Object v-bind (v-bind="props") - only count user-written bindings + // Exclude compiler-generated _attrs binding + sawObjectVBind = true + return true // Keep the object v-bind directive } } @@ -135,14 +148,21 @@ export function ssrTransformTransitionGroup( ) let propsExp = null if (props || directives.length) { + const ssrPropsExp = buildSSRProps(props, directives, context) propsExp = createCallExpression(context.helper(SSR_RENDER_ATTRS), [ - buildSSRProps(props, directives, context), + sawObjectVBind + ? createCallExpression( + context.helper(SSR_FILTER_TRANSITION_PROPS), + [ssrPropsExp], + ) + : ssrPropsExp, ]) } wipMap.set(node, { tag, propsExp, scopeId: context.scopeId || null, + sawObjectVBind, }) } } diff --git a/packages/server-renderer/src/helpers/ssrRenderAttrs.ts b/packages/server-renderer/src/helpers/ssrRenderAttrs.ts index b082da03fe8..e3a726702bc 100644 --- a/packages/server-renderer/src/helpers/ssrRenderAttrs.ts +++ b/packages/server-renderer/src/helpers/ssrRenderAttrs.ts @@ -115,3 +115,33 @@ function ssrResetCssVars(raw: unknown) { } return raw } + +// TransitionGroup transition props that should be filtered in SSR +const transitionPropsToFilter = /*@__PURE__*/ makeMap( + `mode,appear,persisted,onBeforeEnter,onEnter,onAfterEnter,onEnterCancelled,` + + `onBeforeLeave,onLeave,onAfterLeave,onLeaveCancelled,onBeforeAppear,` + + `onAppear,onAfterAppear,onAppearCancelled,name,type,css,duration,` + + `enterFromClass,enterActiveClass,enterToClass,appearFromClass,` + + `appearActiveClass,appearToClass,leaveFromClass,leaveActiveClass,` + + `leaveToClass,moveClass,move-class`, +) + +function kebabToCamel(str: string): string { + return str.replace(/-([a-z])/g, (_, char) => char.toUpperCase()) +} + +export function ssrFilterTransitionProps( + props: Record, +): Record { + const filtered: Record = {} + for (const key in props) { + // Filter out transition-specific props (both camelCase and kebab-case) + if ( + !transitionPropsToFilter(key) && + !transitionPropsToFilter(kebabToCamel(key)) + ) { + filtered[key] = props[key] + } + } + return filtered +} diff --git a/packages/server-renderer/src/internal.ts b/packages/server-renderer/src/internal.ts index 3a2054066c3..48b2e557473 100644 --- a/packages/server-renderer/src/internal.ts +++ b/packages/server-renderer/src/internal.ts @@ -9,6 +9,7 @@ export { ssrRenderAttrs, ssrRenderAttr, ssrRenderDynamicAttr, + ssrFilterTransitionProps, } from './helpers/ssrRenderAttrs' export { ssrInterpolate } from './helpers/ssrInterpolate' export { ssrRenderList } from './helpers/ssrRenderList' From 55902031dbce97a980775e757897bb1c5e951300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=85=A7=E5=9D=A4?= Date: Tue, 23 Sep 2025 17:42:17 +0800 Subject: [PATCH 10/11] fix(compiler-ssr): runtime filter transition props in TransitionGroup SSR Add isTransition parameter to ssrRenderAttrs to filter transition-specific props at runtime, preserving valid attributes (id, data-*, etc.) while removing transition props (name, moveClass, appear, etc.). - Add isTransition parameter to ssrRenderAttrs function - Update TransitionGroup SSR transform to pass isTransition=true flag - Handle both cases: with component props and _attrs-only - Filter transition props in runtime for both camelCase and kebab-case - Add comprehensive tests for transition prop filtering - Update all existing TransitionGroup SSR test snapshots --- .../__tests__/ssrTransitionGroup.spec.ts | 36 ++++++------- packages/compiler-ssr/src/runtimeHelpers.ts | 4 -- .../transforms/ssrTransformTransitionGroup.ts | 44 +++++++--------- .../__tests__/ssrRenderAttrs.spec.ts | 51 +++++++++++++++++++ .../src/helpers/ssrRenderAttrs.ts | 21 ++------ packages/server-renderer/src/internal.ts | 1 - 6 files changed, 90 insertions(+), 67 deletions(-) diff --git a/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts b/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts index df8cb3992bc..190739ceff5 100644 --- a/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts @@ -29,7 +29,7 @@ describe('transition-group', () => { "const { ssrRenderAttrs: _ssrRenderAttrs, ssrRenderList: _ssrRenderList } = require("vue/server-renderer") return function ssrRender(_ctx, _push, _parent, _attrs) { - _push(\`\`) + _push(\`\`) _ssrRenderList(_ctx.list, (i) => { _push(\`
\`) }) @@ -48,7 +48,7 @@ describe('transition-group', () => { "const { ssrRenderAttrs: _ssrRenderAttrs, ssrRenderList: _ssrRenderList } = require("vue/server-renderer") return function ssrRender(_ctx, _push, _parent, _attrs) { - _push(\`\`) + _push(\`\`) _ssrRenderList(_ctx.list, (i) => { _push(\`
\`) }) @@ -70,7 +70,7 @@ describe('transition-group', () => { "const { ssrRenderAttrs: _ssrRenderAttrs, ssrRenderList: _ssrRenderList } = require("vue/server-renderer") return function ssrRender(_ctx, _push, _parent, _attrs) { - _push(\`\`) + _push(\`\`) _ssrRenderList(_ctx.list, (i) => { _push(\`
\`) }) @@ -91,7 +91,7 @@ describe('transition-group', () => { _push(\`<\${ _ctx.someTag }\${ - _ssrRenderAttrs(_attrs) + _ssrRenderAttrs(_attrs, _ctx.someTag, true) }>\`) _ssrRenderList(_ctx.list, (i) => { _push(\`
\`) @@ -143,7 +143,7 @@ describe('transition-group', () => { _push(\`\`) + }, _attrs), "ul", true)}>\`) }" `) }) @@ -164,7 +164,7 @@ describe('transition-group', () => { _push(\`\`) + }, _attrs), "ul", true)}>\`) }" `) }) @@ -183,7 +183,7 @@ describe('transition-group', () => { _push(\`\`) + }, _attrs), "ul", true)}>\`) }" `) }) @@ -199,7 +199,7 @@ describe('transition-group', () => { const { ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer") return function ssrRender(_ctx, _push, _parent, _attrs) { - _push(\`\`) + _push(\`\`) }" `) }) @@ -218,7 +218,7 @@ describe('transition-group', () => { _push(\`\`) + }, _attrs), "ul", true)}>\`) }" `) }) @@ -234,7 +234,7 @@ describe('transition-group', () => { const { ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer") return function ssrRender(_ctx, _push, _parent, _attrs) { - _push(\`\`) + _push(\`\`) }" `) }) @@ -250,7 +250,7 @@ describe('transition-group', () => { const { ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer") return function ssrRender(_ctx, _push, _parent, _attrs) { - _push(\`\`) + _push(\`\`) }" `) }) @@ -263,10 +263,10 @@ describe('transition-group', () => { ).code, ).toMatchInlineSnapshot(` "const { mergeProps: _mergeProps } = require("vue") - const { ssrRenderAttrs: _ssrRenderAttrs, ssrFilterTransitionProps: _ssrFilterTransitionProps } = require("vue/server-renderer") + const { ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer") return function ssrRender(_ctx, _push, _parent, _attrs) { - _push(\`\`) + _push(\`\`) }" `) }) @@ -279,10 +279,10 @@ describe('transition-group', () => { ).code, ).toMatchInlineSnapshot(` "const { mergeProps: _mergeProps } = require("vue") - const { ssrRenderAttrs: _ssrRenderAttrs, ssrFilterTransitionProps: _ssrFilterTransitionProps } = require("vue/server-renderer") + const { ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer") return function ssrRender(_ctx, _push, _parent, _attrs) { - _push(\`\`) + _push(\`\`) }" `) }) @@ -295,13 +295,13 @@ describe('transition-group', () => { ).code, ).toMatchInlineSnapshot(` "const { mergeProps: _mergeProps } = require("vue") - const { ssrRenderAttrs: _ssrRenderAttrs, ssrFilterTransitionProps: _ssrFilterTransitionProps } = require("vue/server-renderer") + const { ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer") return function ssrRender(_ctx, _push, _parent, _attrs) { - _push(\`\`) + }, _attrs), "ul", true)}>\`) }" `) }) diff --git a/packages/compiler-ssr/src/runtimeHelpers.ts b/packages/compiler-ssr/src/runtimeHelpers.ts index 0311e5f0802..0e2c8c67bed 100644 --- a/packages/compiler-ssr/src/runtimeHelpers.ts +++ b/packages/compiler-ssr/src/runtimeHelpers.ts @@ -27,9 +27,6 @@ export const SSR_RENDER_TELEPORT: unique symbol = Symbol(`ssrRenderTeleport`) export const SSR_RENDER_SUSPENSE: unique symbol = Symbol(`ssrRenderSuspense`) export const SSR_GET_DIRECTIVE_PROPS: unique symbol = Symbol(`ssrGetDirectiveProps`) -export const SSR_FILTER_TRANSITION_PROPS: unique symbol = Symbol( - `ssrFilterTransitionProps`, -) export const ssrHelpers: Record = { [SSR_INTERPOLATE]: `ssrInterpolate`, @@ -51,7 +48,6 @@ export const ssrHelpers: Record = { [SSR_RENDER_TELEPORT]: `ssrRenderTeleport`, [SSR_RENDER_SUSPENSE]: `ssrRenderSuspense`, [SSR_GET_DIRECTIVE_PROPS]: `ssrGetDirectiveProps`, - [SSR_FILTER_TRANSITION_PROPS]: `ssrFilterTransitionProps`, } // Note: these are helpers imported from @vue/server-renderer diff --git a/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts b/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts index b24d9561f5a..8a9381c697b 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts @@ -10,10 +10,7 @@ import { findProp, } from '@vue/compiler-dom' import { hasOwn } from '@vue/shared' -import { - SSR_FILTER_TRANSITION_PROPS, - SSR_RENDER_ATTRS, -} from '../runtimeHelpers' +import { SSR_RENDER_ATTRS } from '../runtimeHelpers' import { type SSRTransformContext, processChildren, @@ -75,7 +72,6 @@ interface WIPEntry { tag: AttributeNode | DirectiveNode propsExp: string | JSChildNode | null scopeId: string | null - sawObjectVBind: boolean } // phase 1: build props @@ -86,9 +82,6 @@ export function ssrTransformTransitionGroup( return (): void => { const tag = findProp(node, 'tag') if (tag) { - // Track whether we saw object v-bind (v-bind without argument) - let sawObjectVBind = false - // Filter out all transition-related private props when processing TransitionGroup attributes const otherProps = node.props.filter(p => { // Exclude tag (already handled separately) @@ -123,16 +116,6 @@ export function ssrTransformTransitionGroup( argName === 'moveClass' || argName === 'move-class' return !shouldFilter - } else if ( - !p.arg && - p.exp && - p.exp.type === NodeTypes.SIMPLE_EXPRESSION && - p.exp.content !== '_attrs' - ) { - // Object v-bind (v-bind="props") - only count user-written bindings - // Exclude compiler-generated _attrs binding - sawObjectVBind = true - return true // Keep the object v-bind directive } } @@ -148,21 +131,18 @@ export function ssrTransformTransitionGroup( ) let propsExp = null if (props || directives.length) { - const ssrPropsExp = buildSSRProps(props, directives, context) propsExp = createCallExpression(context.helper(SSR_RENDER_ATTRS), [ - sawObjectVBind - ? createCallExpression( - context.helper(SSR_FILTER_TRANSITION_PROPS), - [ssrPropsExp], - ) - : ssrPropsExp, + buildSSRProps(props, directives, context), + tag.type === NodeTypes.ATTRIBUTE + ? `"${tag.value!.content}"` + : tag.exp!, + `true`, // isTransition flag ]) } wipMap.set(node, { tag, propsExp, scopeId: context.scopeId || null, - sawObjectVBind, }) } } @@ -182,6 +162,11 @@ export function ssrProcessTransitionGroup( context.pushStringPart(tag.exp!) if (propsExp) { context.pushStringPart(propsExp) + } else { + // No component props, but we still need to handle _attrs with transition filtering + context.pushStringPart(`\${_ssrRenderAttrs(_attrs, `) + context.pushStringPart(tag.exp!) + context.pushStringPart(`, true)}`) } if (scopeId) { context.pushStringPart(` ${scopeId}`) @@ -215,6 +200,11 @@ export function ssrProcessTransitionGroup( context.pushStringPart(`<${tag.value!.content}`) if (propsExp) { context.pushStringPart(propsExp) + } else { + // No component props, but we still need to handle _attrs with transition filtering + context.pushStringPart( + `\${_ssrRenderAttrs(_attrs, "${tag.value!.content}", true)}`, + ) } if (scopeId) { context.pushStringPart(` ${scopeId}`) @@ -224,7 +214,7 @@ export function ssrProcessTransitionGroup( context.pushStringPart(``) } } else { - // fragment + // fragment - no tag, just render children processChildren(node, context, true, true, true) } } diff --git a/packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts b/packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts index 984387bb864..830bc496426 100644 --- a/packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts +++ b/packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts @@ -7,6 +7,57 @@ import { import { escapeHtml } from '@vue/shared' describe('ssr: renderAttrs', () => { + test('filters transition props when isTransition is true', () => { + expect( + ssrRenderAttrs( + { + id: 'test', + class: 'container', + name: 'fade', + moveClass: 'move', + 'data-value': 42, + appear: true, + duration: 300, + 'enter-from-class': 'enter', + }, + 'ul', + true, + ), + ).toBe(' id="test" class="container" data-value="42"') + }) + + test('keeps all props when isTransition is false', () => { + expect( + ssrRenderAttrs( + { + id: 'test', + class: 'container', + name: 'fade', + moveClass: 'move', + 'data-value': 42, + }, + 'ul', + false, + ), + ).toBe( + ' id="test" class="container" name="fade" moveclass="move" data-value="42"', + ) + }) + + test('keeps all props when isTransition is undefined', () => { + expect( + ssrRenderAttrs({ + id: 'test', + class: 'container', + name: 'fade', + moveClass: 'move', + 'data-value': 42, + }), + ).toBe( + ' id="test" class="container" name="fade" moveclass="move" data-value="42"', + ) + }) + test('ignore reserved props', () => { expect( ssrRenderAttrs({ diff --git a/packages/server-renderer/src/helpers/ssrRenderAttrs.ts b/packages/server-renderer/src/helpers/ssrRenderAttrs.ts index e3a726702bc..693a37dc89c 100644 --- a/packages/server-renderer/src/helpers/ssrRenderAttrs.ts +++ b/packages/server-renderer/src/helpers/ssrRenderAttrs.ts @@ -27,13 +27,16 @@ const shouldIgnoreProp = /*@__PURE__*/ makeMap( export function ssrRenderAttrs( props: Record, tag?: string, + isTransition?: boolean, ): string { let ret = '' for (const key in props) { if ( shouldIgnoreProp(key) || isOn(key) || - (tag === 'textarea' && key === 'value') + (tag === 'textarea' && key === 'value') || + (isTransition && transitionPropsToFilter(key)) || + (isTransition && transitionPropsToFilter(kebabToCamel(key))) ) { continue } @@ -129,19 +132,3 @@ const transitionPropsToFilter = /*@__PURE__*/ makeMap( function kebabToCamel(str: string): string { return str.replace(/-([a-z])/g, (_, char) => char.toUpperCase()) } - -export function ssrFilterTransitionProps( - props: Record, -): Record { - const filtered: Record = {} - for (const key in props) { - // Filter out transition-specific props (both camelCase and kebab-case) - if ( - !transitionPropsToFilter(key) && - !transitionPropsToFilter(kebabToCamel(key)) - ) { - filtered[key] = props[key] - } - } - return filtered -} diff --git a/packages/server-renderer/src/internal.ts b/packages/server-renderer/src/internal.ts index 48b2e557473..3a2054066c3 100644 --- a/packages/server-renderer/src/internal.ts +++ b/packages/server-renderer/src/internal.ts @@ -9,7 +9,6 @@ export { ssrRenderAttrs, ssrRenderAttr, ssrRenderDynamicAttr, - ssrFilterTransitionProps, } from './helpers/ssrRenderAttrs' export { ssrInterpolate } from './helpers/ssrInterpolate' export { ssrRenderList } from './helpers/ssrRenderList' From 2a477e8e538571255bf1e049febf3da305032c84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=85=A7=E5=9D=A4?= Date: Tue, 23 Sep 2025 17:54:48 +0800 Subject: [PATCH 11/11] fix(compiler-ssr): runtime filter transition props in TransitionGroup SSR Add isTransition parameter to ssrRenderAttrs to filter transition-specific props at runtime, preserving valid attributes (id, data-*, etc.) while removing transition props (name, moveClass, appear, etc.). - Add isTransition parameter to ssrRenderAttrs function - Update TransitionGroup SSR transform to pass isTransition=true flag - Handle both cases: with component props and _attrs-only - Filter transition props in runtime for both camelCase and kebab-case - Add comprehensive tests for transition prop filtering - Update all existing TransitionGroup SSR test snapshots --- packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts b/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts index 190739ceff5..b23d6020491 100644 --- a/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts @@ -223,7 +223,9 @@ describe('transition-group', () => { `) }) - test('filters out transition event handlers', () => { + test('event handlers are omitted in SSR (not transition-specific)', () => { + // This test verifies that event handlers are filtered out during SSR compilation, + // not because of transition filtering but because SSR skips event listeners entirely expect( compile( `