Просмотр исходного кода

chore: Merge branch 'main' into minor

daiwei 7 месяцев назад
Родитель
Сommit
735f891771
44 измененных файлов с 859 добавлено и 306 удалено
  1. 2 2
      .github/workflows/autofix.yml
  2. 1 1
      .github/workflows/ci.yml
  3. 1 1
      .github/workflows/release.yml
  4. 3 3
      .github/workflows/size-data.yml
  5. 2 2
      .github/workflows/size-report.yml
  6. 8 8
      .github/workflows/test.yml
  7. 21 0
      changelogs/CHANGELOG-3.5.md
  8. 6 2
      eslint.config.js
  9. 12 12
      package.json
  10. 1 1
      packages-private/sfc-playground/src/download/template/package.json
  11. 1 1
      packages-private/template-explorer/package.json
  12. 10 0
      packages/compiler-core/__tests__/transforms/__snapshots__/transformExpressions.spec.ts.snap
  13. 8 0
      packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts
  14. 21 0
      packages/compiler-core/__tests__/transforms/vBind.spec.ts
  15. 1 1
      packages/compiler-core/src/parser.ts
  16. 8 4
      packages/compiler-core/src/transforms/transformExpression.ts
  17. 7 2
      packages/compiler-core/src/transforms/transformVBindShorthand.ts
  18. 5 1
      packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts
  19. 2 2
      packages/compiler-sfc/package.json
  20. 4 8
      packages/compiler-sfc/src/script/resolveType.ts
  21. 18 0
      packages/compiler-sfc/src/script/utils.ts
  22. 13 5
      packages/compiler-ssr/src/transforms/ssrTransformElement.ts
  23. 5 0
      packages/runtime-core/__tests__/hydration.spec.ts
  24. 4 1
      packages/runtime-core/src/apiAsyncComponent.ts
  25. 8 1
      packages/runtime-core/src/apiSetupHelpers.ts
  26. 8 0
      packages/runtime-core/src/component.ts
  27. 14 3
      packages/runtime-core/src/componentPublicInstance.ts
  28. 12 0
      packages/runtime-core/src/components/Suspense.ts
  29. 8 4
      packages/runtime-core/src/hydration.ts
  30. 21 9
      packages/runtime-core/src/renderer.ts
  31. 222 0
      packages/runtime-dom/__tests__/customElement.spec.ts
  32. 3 0
      packages/runtime-dom/__tests__/directives/vModel.spec.ts
  33. 34 0
      packages/runtime-dom/__tests__/patchAttrs.spec.ts
  34. 35 7
      packages/runtime-dom/src/apiCustomElement.ts
  35. 18 10
      packages/runtime-dom/src/components/TransitionGroup.ts
  36. 11 10
      packages/runtime-dom/src/directives/vModel.ts
  37. 1 1
      packages/runtime-dom/src/jsx.ts
  38. 35 0
      packages/server-renderer/__tests__/ssrDirectives.spec.ts
  39. 7 0
      packages/shared/src/domAttrConfig.ts
  40. 56 0
      packages/vue/__tests__/e2e/TransitionGroup.spec.ts
  41. 1 1
      packages/vue/jsx-runtime/index.d.ts
  42. 1 1
      packages/vue/jsx.d.ts
  43. 197 199
      pnpm-lock.yaml
  44. 3 3
      pnpm-workspace.yaml

+ 2 - 2
.github/workflows/autofix.yml

@@ -14,10 +14,10 @@ jobs:
       - uses: actions/checkout@v5
 
       - name: Install pnpm
-        uses: pnpm/action-setup@v4.1.0
+        uses: pnpm/action-setup@v4.2.0
 
       - name: Install Node.js
-        uses: actions/setup-node@v5
+        uses: actions/setup-node@v6
         with:
           node-version-file: '.node-version'
           registry-url: 'https://registry.npmjs.org'

+ 1 - 1
.github/workflows/ci.yml

@@ -27,7 +27,7 @@ jobs:
         uses: pnpm/action-setup@v4
 
       - name: Install Node.js
-        uses: actions/setup-node@v5
+        uses: actions/setup-node@v6
         with:
           node-version-file: '.node-version'
           registry-url: 'https://registry.npmjs.org'

+ 1 - 1
.github/workflows/release.yml

@@ -27,7 +27,7 @@ jobs:
         uses: pnpm/action-setup@v4
 
       - name: Install Node.js
-        uses: actions/setup-node@v5
+        uses: actions/setup-node@v6
         with:
           node-version-file: '.node-version'
           registry-url: 'https://registry.npmjs.org'

+ 3 - 3
.github/workflows/size-data.yml

@@ -25,10 +25,10 @@ jobs:
       - uses: actions/checkout@v5
 
       - name: Install pnpm
-        uses: pnpm/action-setup@v4.1.0
+        uses: pnpm/action-setup@v4.2.0
 
       - name: Install Node.js
-        uses: actions/setup-node@v5
+        uses: actions/setup-node@v6
         with:
           node-version-file: '.node-version'
           cache: pnpm
@@ -45,7 +45,7 @@ jobs:
           echo ${{ github.base_ref }} > ./temp/size/base.txt
 
       - name: Upload Size Data
-        uses: actions/upload-artifact@v4
+        uses: actions/upload-artifact@v5
         with:
           name: size-data
           path: temp/size

+ 2 - 2
.github/workflows/size-report.yml

@@ -25,10 +25,10 @@ jobs:
       - uses: actions/checkout@v5
 
       - name: Install pnpm
-        uses: pnpm/action-setup@v4.1.0
+        uses: pnpm/action-setup@v4.2.0
 
       - name: Install Node.js
-        uses: actions/setup-node@v5
+        uses: actions/setup-node@v6
         with:
           node-version-file: '.node-version'
           cache: pnpm

+ 8 - 8
.github/workflows/test.yml

@@ -14,10 +14,10 @@ jobs:
       - uses: actions/checkout@v5
 
       - name: Install pnpm
-        uses: pnpm/action-setup@v4.1.0
+        uses: pnpm/action-setup@v4.2.0
 
       - name: Install Node.js
-        uses: actions/setup-node@v5
+        uses: actions/setup-node@v6
         with:
           node-version-file: '.node-version'
           cache: 'pnpm'
@@ -35,10 +35,10 @@ jobs:
       - uses: actions/checkout@v5
 
       - name: Install pnpm
-        uses: pnpm/action-setup@v4.1.0
+        uses: pnpm/action-setup@v4.2.0
 
       - name: Install Node.js
-        uses: actions/setup-node@v5
+        uses: actions/setup-node@v6
         with:
           node-version-file: '.node-version'
           cache: 'pnpm'
@@ -63,10 +63,10 @@ jobs:
           key: chromium-${{ hashFiles('pnpm-lock.yaml') }}
 
       - name: Install pnpm
-        uses: pnpm/action-setup@v4.1.0
+        uses: pnpm/action-setup@v4.2.0
 
       - name: Install Node.js
-        uses: actions/setup-node@v5
+        uses: actions/setup-node@v6
         with:
           node-version-file: '.node-version'
           cache: 'pnpm'
@@ -114,10 +114,10 @@ jobs:
       - uses: actions/checkout@v5
 
       - name: Install pnpm
-        uses: pnpm/action-setup@v4.1.0
+        uses: pnpm/action-setup@v4.2.0
 
       - name: Install Node.js
-        uses: actions/setup-node@v5
+        uses: actions/setup-node@v6
         with:
           node-version-file: '.node-version'
           cache: 'pnpm'

+ 21 - 0
changelogs/CHANGELOG-3.5.md

@@ -1,3 +1,24 @@
+## [3.5.23](https://github.com/vuejs/core/compare/v3.5.22...v3.5.23) (2025-11-06)
+
+
+### Bug Fixes
+
+* **compiler-core:** correctly handle ts type assertions in expressions ([#13397](https://github.com/vuejs/core/issues/13397)) ([e6544ac](https://github.com/vuejs/core/commit/e6544ac292b5b473274f87cdb83ebeac3e7e61a4)), closes [#13395](https://github.com/vuejs/core/issues/13395)
+* **compiler-core:** fix v-bind shorthand handling for in-DOM templates ([#13933](https://github.com/vuejs/core/issues/13933)) ([b3cca26](https://github.com/vuejs/core/commit/b3cca2611c656b85f0c4e737b9ec248d2627dded)), closes [#13930](https://github.com/vuejs/core/issues/13930)
+* **compiler-sfc:** resolve numeric literals and template literals without expressions as static property key ([#13998](https://github.com/vuejs/core/issues/13998)) ([75d44c7](https://github.com/vuejs/core/commit/75d44c718981f91843e197265cc68e82fe2532dd))
+* **compiler-ssr:** textarea with v-text directive SSR ([#13975](https://github.com/vuejs/core/issues/13975)) ([006a0c1](https://github.com/vuejs/core/commit/006a0c1011a224bcbf21195c6df76812c3a7e757))
+* **compiler:** using guard instead of non-nullish assertion ([#13982](https://github.com/vuejs/core/issues/13982)) ([dcc6f36](https://github.com/vuejs/core/commit/dcc6f362577ed86ccad31c2623c6cf75137dd27a))
+* **custom-element:** batch custom element prop patching ([#13478](https://github.com/vuejs/core/issues/13478)) ([c13e674](https://github.com/vuejs/core/commit/c13e674fb9f92ab9339d28a862d18de460faf56e)), closes [#12619](https://github.com/vuejs/core/issues/12619)
+* **custom-element:** optimize slot retrieval to avoid duplicates ([#13961](https://github.com/vuejs/core/issues/13961)) ([84ca349](https://github.com/vuejs/core/commit/84ca349fef73f6f55fc98299fcfa5c1eeef721db)), closes [#13955](https://github.com/vuejs/core/issues/13955)
+* **hydration:** avoid mismatch during hydrate text with newlines in interpolation ([#9232](https://github.com/vuejs/core/issues/9232)) ([6cbdf78](https://github.com/vuejs/core/commit/6cbdf7823b0c961190bee5b7c117b7f2bbeb832f)), closes [#9229](https://github.com/vuejs/core/issues/9229)
+* **runtime-core:** pass props and children to loadingComponent ([#13997](https://github.com/vuejs/core/issues/13997)) ([40c4b2a](https://github.com/vuejs/core/commit/40c4b2a876ce606973521dfc3024e26bfc10953a))
+* **runtime-dom:** ensure iframe sandbox is handled as an attribute to prevent unintended behavior ([#13950](https://github.com/vuejs/core/issues/13950)) ([5689884](https://github.com/vuejs/core/commit/5689884c8e32cda6a802ac36b4d23218f67b38ed)), closes [#13946](https://github.com/vuejs/core/issues/13946)
+* **suspense:** clear placeholder and fallback el after resolve to enable GC ([#13928](https://github.com/vuejs/core/issues/13928)) ([f411c66](https://github.com/vuejs/core/commit/f411c6604c12c531883aa0d30b81a7f69092f8a6))
+* **transition-group:** use offsetLeft and offsetTop instead of getBoundingClientRect  to avoid transform scale affect animation ([#6108](https://github.com/vuejs/core/issues/6108)) ([dc4dd59](https://github.com/vuejs/core/commit/dc4dd594fbecce6ed7f44ffa69dc8b5d022287b6)), closes [#6105](https://github.com/vuejs/core/issues/6105)
+* **v-model:** handle number modifier on change ([#13959](https://github.com/vuejs/core/issues/13959)) ([8fbe48f](https://github.com/vuejs/core/commit/8fbe48fe396d830999afd07f9413d899157d5f5e)), closes [#13958](https://github.com/vuejs/core/issues/13958)
+
+
+
 ## [3.5.22](https://github.com/vuejs/core/compare/v3.5.21...v3.5.22) (2025-09-25)
 
 

+ 6 - 2
eslint.config.js

@@ -1,5 +1,6 @@
 import importX from 'eslint-plugin-import-x'
 import tseslint from 'typescript-eslint'
+import { defineConfig } from 'eslint/config'
 import vitest from '@vitest/eslint-plugin'
 import { builtinModules } from 'node:module'
 
@@ -12,7 +13,7 @@ const banConstEnum = {
     'Please use non-const enums. This project automatically inlines enums.',
 }
 
-export default tseslint.config(
+export default defineConfig(
   {
     files: ['**/*.js', '**/*.ts', '**/*.tsx'],
     extends: [tseslint.configs.base],
@@ -60,7 +61,10 @@ export default tseslint.config(
       ],
       // This rule enforces the preference for using '@ts-expect-error' comments in TypeScript
       // code to indicate intentional type errors, improving code clarity and maintainability.
-      '@typescript-eslint/prefer-ts-expect-error': 'error',
+      '@typescript-eslint/ban-ts-comment': [
+        'error',
+        { minimumDescriptionLength: 0 },
+      ],
       // Enforce the use of 'import type' for importing types
       '@typescript-eslint/consistent-type-imports': [
         'error',

+ 12 - 12
package.json

@@ -65,44 +65,44 @@
     "@babel/parser": "catalog:",
     "@babel/types": "catalog:",
     "@rollup/plugin-alias": "^5.1.1",
-    "@rollup/plugin-commonjs": "^28.0.6",
+    "@rollup/plugin-commonjs": "^28.0.9",
     "@rollup/plugin-json": "^6.1.0",
-    "@rollup/plugin-node-resolve": "^16.0.1",
+    "@rollup/plugin-node-resolve": "^16.0.3",
     "@rollup/plugin-replace": "5.0.4",
-    "@swc/core": "^1.13.5",
+    "@swc/core": "^1.14.0",
     "@types/hash-sum": "^1.0.2",
-    "@types/node": "^22.18.6",
+    "@types/node": "^22.19.0",
     "@types/semver": "^7.7.1",
     "@types/serve-handler": "^6.1.4",
     "@vitest/ui": "^3.0.2",
     "@vitest/coverage-v8": "^3.2.4",
-    "@vitest/eslint-plugin": "^1.3.12",
+    "@vitest/eslint-plugin": "^1.4.0",
     "@vue/consolidate": "1.0.0",
     "conventional-changelog-cli": "^5.0.0",
     "enquirer": "^2.4.1",
-    "esbuild": "^0.25.10",
+    "esbuild": "^0.25.12",
     "esbuild-plugin-polyfill-node": "^0.3.0",
     "eslint": "^9.27.0",
     "eslint-plugin-import-x": "^4.13.1",
     "estree-walker": "catalog:",
-    "jsdom": "^27.0.0",
+    "jsdom": "^27.1.0",
     "lint-staged": "^16.0.0",
     "lodash": "^4.17.21",
-    "magic-string": "^0.30.19",
+    "magic-string": "^0.30.21",
     "markdown-table": "^3.0.4",
     "marked": "13.0.3",
     "npm-run-all2": "^8.0.4",
     "picocolors": "^1.1.1",
     "prettier": "^3.5.3",
-    "pretty-bytes": "^6.1.1",
+    "pretty-bytes": "^7.1.0",
     "pug": "^3.0.3",
-    "puppeteer": "~24.22.2",
-    "rimraf": "^6.0.1",
+    "puppeteer": "~24.28.0",
+    "rimraf": "^6.1.0",
     "rollup": "^4.52.5",
     "rollup-plugin-dts": "^6.2.3",
     "rollup-plugin-esbuild": "^6.2.1",
     "rollup-plugin-polyfill-node": "^0.13.0",
-    "semver": "^7.7.2",
+    "semver": "^7.7.3",
     "serve": "^14.2.5",
     "serve-handler": "^6.1.6",
     "simple-git-hooks": "^2.13.0",

+ 1 - 1
packages-private/sfc-playground/src/download/template/package.json

@@ -12,6 +12,6 @@
   },
   "devDependencies": {
     "@vitejs/plugin-vue": "^6.0.1",
-    "vite": "^7.1.7"
+    "vite": "^7.1.12"
   }
 }

+ 1 - 1
packages-private/template-explorer/package.json

@@ -12,7 +12,7 @@
   },
   "dependencies": {
     "@vue/compiler-vapor": "workspace:^",
-    "monaco-editor": "^0.53.0",
+    "monaco-editor": "^0.54.0",
     "source-map-js": "^1.2.1"
   }
 }

+ 10 - 0
packages/compiler-core/__tests__/transforms/__snapshots__/transformExpressions.spec.ts.snap

@@ -14,6 +14,16 @@ return function render(_ctx, _cache, $props, $setup, $data, $options) {
 }"
 `;
 
+exports[`compiler: expression transform > expression with type 1`] = `
+"const { openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
+
+return function render(_ctx, _cache) {
+  return (_openBlock(), _createElementBlock("div", {
+    onClick: _ctx.handleClick
+  }, null, 8 /* PROPS */, ["onClick"]))
+}"
+`;
+
 exports[`compiler: expression transform > should allow leak of var declarations in for loop 1`] = `
 "const { openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
 

+ 8 - 0
packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts

@@ -754,4 +754,12 @@ describe('compiler: expression transform', () => {
       expect(code).toMatch(`_ctx.bar`)
     })
   })
+
+  test('expression with type', () => {
+    const { code } = compile(
+      `<div @click="(<number>handleClick as any)"></div>`,
+    )
+    expect(code).toMatch(`onClick: _ctx.handleClick`)
+    expect(code).toMatchSnapshot()
+  })
 })

+ 21 - 0
packages/compiler-core/__tests__/transforms/vBind.spec.ts

@@ -112,6 +112,27 @@ describe('compiler: transform v-bind', () => {
     })
   })
 
+  test('no expression (shorthand) in-DOM templates', () => {
+    try {
+      __BROWSER__ = true
+      // :id in in-DOM templates will be parsed into :id="" by browser
+      const node = parseWithVBind(`<div :id="" />`)
+      const props = (node.codegenNode as VNodeCall).props as ObjectExpression
+      expect(props.properties[0]).toMatchObject({
+        key: {
+          content: `id`,
+          isStatic: true,
+        },
+        value: {
+          content: `id`,
+          isStatic: false,
+        },
+      })
+    } finally {
+      __BROWSER__ = false
+    }
+  })
+
   test('dynamic arg', () => {
     const node = parseWithVBind(`<div v-bind:[id]="id"/>`)
     const props = (node.codegenNode as VNodeCall).props as CallExpression

+ 1 - 1
packages/compiler-core/src/parser.ts

@@ -1054,7 +1054,7 @@ export function baseParse(input: string, options?: ParserOptions): RootNode {
         `[@vue/compiler-core] decodeEntities option is passed but will be ` +
           `ignored in non-browser builds.`,
       )
-    } else if (__BROWSER__ && !currentOptions.decodeEntities) {
+    } else if (__BROWSER__ && !__TEST__ && !currentOptions.decodeEntities) {
       throw new Error(
         `[@vue/compiler-core] decodeEntities option is required in browser builds.`,
       )

+ 8 - 4
packages/compiler-core/src/transforms/transformExpression.ts

@@ -18,6 +18,7 @@ import {
   createSimpleExpression,
 } from '../ast'
 import {
+  TS_NODE_TYPES,
   isInDestructureAssignment,
   isInNewExpression,
   isStaticProperty,
@@ -348,15 +349,18 @@ export function processExpression(
   // an ExpressionNode has the `.children` property, it will be used instead of
   // `.content`.
   const children: CompoundExpressionNode['children'] = []
+  const isTSNode = TS_NODE_TYPES.includes(ast.type)
   ids.sort((a, b) => a.start - b.start)
   ids.forEach((id, i) => {
     // range is offset by -1 due to the wrapping parens when parsed
     const start = id.start - 1
     const end = id.end - 1
     const last = ids[i - 1]
-    const leadingText = rawExp.slice(last ? last.end - 1 : 0, start)
-    if (leadingText.length || id.prefix) {
-      children.push(leadingText + (id.prefix || ``))
+    if (!(isTSNode && i === 0)) {
+      const leadingText = rawExp.slice(last ? last.end - 1 : 0, start)
+      if (leadingText.length || id.prefix) {
+        children.push(leadingText + (id.prefix || ``))
+      }
     }
     const source = rawExp.slice(start, end)
     children.push(
@@ -373,7 +377,7 @@ export function processExpression(
           : ConstantTypes.NOT_CONSTANT,
       ),
     )
-    if (i === ids.length - 1 && end < rawExp.length) {
+    if (i === ids.length - 1 && end < rawExp.length && !isTSNode) {
       children.push(rawExp.slice(end))
     }
   })

+ 7 - 2
packages/compiler-core/src/transforms/transformVBindShorthand.ts

@@ -15,9 +15,14 @@ export const transformVBindShorthand: NodeTransform = (node, context) => {
       if (
         prop.type === NodeTypes.DIRECTIVE &&
         prop.name === 'bind' &&
-        !prop.exp
+        (!prop.exp ||
+          // #13930 :foo in in-DOM templates will be parsed into :foo="" by browser
+          (__BROWSER__ &&
+            prop.exp.type === NodeTypes.SIMPLE_EXPRESSION &&
+            !prop.exp.content.trim())) &&
+        prop.arg
       ) {
-        const arg = prop.arg!
+        const arg = prop.arg
         if (arg.type !== NodeTypes.SIMPLE_EXPRESSION || !arg.isStatic) {
           // only simple expression is allowed for same-name shorthand
           context.onError(

+ 5 - 1
packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts

@@ -20,6 +20,8 @@ describe('resolveType', () => {
       foo: number // property
       bar(): void // method
       'baz': string // string literal key
+      [\`qux\`]: boolean // template literal key
+      123: symbol // numeric literal key
       (e: 'foo'): void // call signature
       (e: 'bar'): void
     }>()`)
@@ -27,6 +29,8 @@ describe('resolveType', () => {
       foo: ['Number'],
       bar: ['Function'],
       baz: ['String'],
+      qux: ['Boolean'],
+      123: ['Symbol'],
     })
     expect(calls?.length).toBe(2)
   })
@@ -195,7 +199,7 @@ describe('resolveType', () => {
     type T = 'foo' | 'bar'
     type S = 'x' | 'y'
     defineProps<{
-      [\`_\${T}_\${S}_\`]: string
+      [K in \`_\${T}_\${S}_\`]: string
     }>()
     `).props,
     ).toStrictEqual({

+ 2 - 2
packages/compiler-sfc/package.json

@@ -59,10 +59,10 @@
     "hash-sum": "^2.0.0",
     "lru-cache": "10.1.0",
     "merge-source-map": "^1.1.0",
-    "minimatch": "~10.0.3",
+    "minimatch": "~10.1.1",
     "postcss-modules": "^6.0.1",
     "postcss-selector-parser": "^7.1.0",
     "pug": "^3.0.3",
-    "sass": "^1.93.2"
+    "sass": "^1.93.3"
   }
 }

+ 4 - 8
packages/compiler-sfc/src/script/resolveType.ts

@@ -29,6 +29,7 @@ import {
   createGetCanonicalFileName,
   getId,
   getImportedName,
+  getStringLiteralKey,
   joinPaths,
   normalizePath,
 } from './utils'
@@ -336,13 +337,9 @@ function typeElementsToMap(
         Object.assign(scope.types, typeParameters)
       }
       ;(e as MaybeWithScope)._ownerScope = scope
-      const name = getId(e.key)
-      if (name && !e.computed) {
+      const name = getStringLiteralKey(e)
+      if (name !== null) {
         res.props[name] = e as ResolvedElements['props'][string]
-      } else if (e.key.type === 'TemplateLiteral') {
-        for (const key of resolveTemplateKeys(ctx, e.key, scope)) {
-          res.props[key] = e as ResolvedElements['props'][string]
-        }
       } else {
         ctx.error(
           `Unsupported computed key in type referenced by a macro`,
@@ -2029,8 +2026,7 @@ function findStaticPropertyType(node: TSTypeLiteral, key: string) {
   const prop = node.members.find(
     m =>
       m.type === 'TSPropertySignature' &&
-      !m.computed &&
-      getId(m.key) === key &&
+      getStringLiteralKey(m) === key &&
       m.typeAnnotation,
   )
   return prop && prop.typeAnnotation!.typeAnnotation

+ 18 - 0
packages/compiler-sfc/src/script/utils.ts

@@ -7,6 +7,8 @@ import type {
   ImportSpecifier,
   Node,
   StringLiteral,
+  TSMethodSignature,
+  TSPropertySignature,
 } from '@babel/types'
 import path from 'path'
 
@@ -79,6 +81,22 @@ export function getId(node: Expression) {
       : null
 }
 
+export function getStringLiteralKey(
+  node: TSPropertySignature | TSMethodSignature,
+): string | null {
+  return node.computed
+    ? node.key.type === 'TemplateLiteral' && !node.key.expressions.length
+      ? node.key.quasis.map(q => q.value.cooked).join('')
+      : null
+    : node.key.type === 'Identifier'
+      ? node.key.name
+      : node.key.type === 'StringLiteral'
+        ? node.key.value
+        : node.key.type === 'NumericLiteral'
+          ? String(node.key.value)
+          : null
+}
+
 const identity = (str: string) => str
 const fileNameLowerCaseRegExp = /[^\u0130\u0131\u00DFa-z0-9\\/:\-_\. ]+/g
 const toLowerCase = (str: string) => str.toLowerCase()

+ 13 - 5
packages/compiler-ssr/src/transforms/ssrTransformElement.ts

@@ -122,8 +122,13 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
             | InterpolationNode
             | undefined
           // If interpolation, this is dynamic <textarea> content, potentially
-          // injected by v-model and takes higher priority than v-bind value
-          if (!existingText || existingText.type !== NodeTypes.INTERPOLATION) {
+          // injected by v-model and takes higher priority than v-bind value.
+          // Additionally, directives with content overrides (v-text/v-html)
+          // have higher priority than the merged props.
+          if (
+            !hasContentOverrideDirective(node) &&
+            (!existingText || existingText.type !== NodeTypes.INTERPOLATION)
+          ) {
             // <textarea> with dynamic v-bind. We don't know if the final props
             // will contain .value, so we will have to do something special:
             // assign the merged props to a temp variable, and check whether
@@ -176,9 +181,8 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
             ]
           }
         } else if (directives.length && !node.children.length) {
-          // v-text directive has higher priority than the merged props
-          const vText = findDir(node, 'text')
-          if (!vText) {
+          // v-text/v-html have higher priority than the merged props
+          if (!hasContentOverrideDirective(node)) {
             const tempId = `_temp${context.temps++}`
             propsExp.arguments = [
               createAssignmentExpression(
@@ -449,6 +453,10 @@ function findVModel(node: PlainElementNode): DirectiveNode | undefined {
   ) as DirectiveNode | undefined
 }
 
+function hasContentOverrideDirective(node: PlainElementNode): boolean {
+  return !!findDir(node, 'text') || !!findDir(node, 'html')
+}
+
 export function ssrProcessElement(
   node: PlainElementNode,
   context: SSRTransformContext,

+ 5 - 0
packages/runtime-core/__tests__/hydration.spec.ts

@@ -82,6 +82,11 @@ describe('SSR hydration', () => {
     expect(`Hydration children mismatch in <div>`).not.toHaveBeenWarned()
   })
 
+  test('text w/ newlines', async () => {
+    mountWithHydration('<div>1\n2\n3</div>', () => h('div', '1\r\n2\r3'))
+    expect(`Hydration text mismatch`).not.toHaveBeenWarned()
+  })
+
   test('comment', () => {
     const { vnode, container } = mountWithHydration('<!---->', () => null)
     expect(vnode.el).toBe(container.firstChild)

+ 4 - 1
packages/runtime-core/src/apiAsyncComponent.ts

@@ -156,7 +156,10 @@ export function defineAsyncComponent<
             error: error.value,
           })
         } else if (loadingComponent && !delayed.value) {
-          return createVNode(loadingComponent)
+          return createInnerComp(
+            loadingComponent as ConcreteComponent,
+            instance,
+          )
         }
       }
     },

+ 8 - 1
packages/runtime-core/src/apiSetupHelpers.ts

@@ -319,7 +319,14 @@ type InferDefaults<T> = {
   [K in keyof T]?: InferDefault<T, T[K]>
 }
 
-type NativeType = null | number | string | boolean | symbol | Function
+type NativeType =
+  | null
+  | undefined
+  | number
+  | string
+  | boolean
+  | symbol
+  | Function
 
 type InferDefault<P, T> =
   | ((props: P) => T & {})

+ 8 - 0
packages/runtime-core/src/component.ts

@@ -1333,6 +1333,14 @@ export interface ComponentCustomElementInterface {
     shouldReflect?: boolean,
     shouldUpdate?: boolean,
   ): void
+  /**
+   * @internal
+   */
+  _beginPatch(): void
+  /**
+   * @internal
+   */
+  _endPatch(): void
   /**
    * @internal attached by the nested Teleport when shadowRoot is false.
    */

+ 14 - 3
packages/runtime-core/src/componentPublicInstance.ts

@@ -450,7 +450,11 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
       } else if (hasSetupBinding(setupState, key)) {
         accessCache![key] = AccessTypes.SETUP
         return setupState[key]
-      } else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
+      } else if (
+        __FEATURE_OPTIONS_API__ &&
+        data !== EMPTY_OBJ &&
+        hasOwn(data, key)
+      ) {
         accessCache![key] = AccessTypes.DATA
         return data[key]
       } else if (
@@ -547,7 +551,11 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
     ) {
       warn(`Cannot mutate <script setup> binding "${key}" from Options API.`)
       return false
-    } else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
+    } else if (
+      __FEATURE_OPTIONS_API__ &&
+      data !== EMPTY_OBJ &&
+      hasOwn(data, key)
+    ) {
       data[key] = value
       return true
     } else if (hasOwn(instance.props, key)) {
@@ -584,7 +592,10 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
     let normalizedProps, cssModules
     return !!(
       accessCache![key] ||
-      (data !== EMPTY_OBJ && key[0] !== '$' && hasOwn(data, key)) ||
+      (__FEATURE_OPTIONS_API__ &&
+        data !== EMPTY_OBJ &&
+        key[0] !== '$' &&
+        hasOwn(data, key)) ||
       hasSetupBinding(setupState, key) ||
       ((normalizedProps = propsOptions[0]) && hasOwn(normalizedProps, key)) ||
       hasOwn(ctx, key) ||

+ 12 - 0
packages/runtime-core/src/components/Suspense.ts

@@ -530,6 +530,7 @@ function createSuspenseBoundary(
         effects,
         parentComponent,
         container,
+        isInFallback,
       } = suspense
 
       // if there's a transition happening we need to wait it to finish.
@@ -552,6 +553,10 @@ function createSuspenseBoundary(
                 parentComponent,
               )
               queuePostFlushCb(effects)
+              // clear el reference from fallback vnode to allow GC after transition
+              if (isInFallback && vnode.ssFallback) {
+                vnode.ssFallback.el = null
+              }
             }
           }
         }
@@ -571,6 +576,11 @@ function createSuspenseBoundary(
             anchor = next(activeBranch)
           }
           unmount(activeBranch, parentComponent, suspense, true)
+          // clear el reference from fallback vnode to allow GC
+          // only clear immediately if there's no delayed transition
+          if (!delayEnter && isInFallback && vnode.ssFallback) {
+            vnode.ssFallback.el = null
+          }
         }
         if (!delayEnter) {
           // move content from off-dom container to actual container
@@ -735,6 +745,8 @@ function createSuspenseBoundary(
             optimized,
           )
           if (placeholder) {
+            // clean up placeholder reference
+            vnode.placeholder = null
             remove(placeholder)
           }
           updateHOCHostEl(instance, vnode.el)

+ 8 - 4
packages/runtime-core/src/hydration.ts

@@ -486,18 +486,22 @@ export function createHydrationFunctions(
         ) {
           clientText = clientText.slice(1)
         }
-        if (el.textContent !== clientText) {
+        const { textContent } = el
+        if (
+          textContent !== clientText &&
+          // innerHTML normalize \r\n or \r into a single \n in the DOM
+          textContent !== clientText.replace(/\r\n|\r/g, '\n')
+        ) {
           if (!isMismatchAllowed(el, MismatchTypes.TEXT)) {
             ;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
               warn(
                 `Hydration text content mismatch on`,
                 el,
-                `\n  - rendered on server: ${el.textContent}` +
-                  `\n  - expected on client: ${vnode.children as string}`,
+                `\n  - rendered on server: ${textContent}` +
+                  `\n  - expected on client: ${clientText}`,
               )
             logMismatchError()
           }
-
           el.textContent = vnode.children as string
         }
       }

+ 21 - 9
packages/runtime-core/src/renderer.ts

@@ -650,15 +650,27 @@ function baseCreateRenderer(
         optimized,
       )
     } else {
-      patchElement(
-        n1,
-        n2,
-        parentComponent,
-        parentSuspense,
-        namespace,
-        slotScopeIds,
-        optimized,
-      )
+      const customElement = !!(n1.el && (n1.el as VueElement)._isVueCE)
+        ? (n1.el as VueElement)
+        : null
+      try {
+        if (customElement) {
+          customElement._beginPatch()
+        }
+        patchElement(
+          n1,
+          n2,
+          parentComponent,
+          parentSuspense,
+          namespace,
+          slotScopeIds,
+          optimized,
+        )
+      } finally {
+        if (customElement) {
+          customElement._endPatch()
+        }
+      }
     }
   }
 

+ 222 - 0
packages/runtime-dom/__tests__/customElement.spec.ts

@@ -499,6 +499,190 @@ describe('defineCustomElement', () => {
         '<div><span>1 is number</span><span>true is boolean</span></div>',
       )
     })
+
+    test('should patch all props together', async () => {
+      let prop1Calls = 0
+      let prop2Calls = 0
+      const E = defineCustomElement({
+        props: {
+          prop1: {
+            type: String,
+            default: 'default1',
+          },
+          prop2: {
+            type: String,
+            default: 'default2',
+          },
+        },
+        data() {
+          return {
+            data1: 'defaultData1',
+            data2: 'defaultData2',
+          }
+        },
+        watch: {
+          prop1(_) {
+            prop1Calls++
+            this.data2 = this.prop2
+          },
+          prop2(_) {
+            prop2Calls++
+            this.data1 = this.prop1
+          },
+        },
+        render() {
+          return h('div', [
+            h('h1', this.prop1),
+            h('h1', this.prop2),
+            h('h2', this.data1),
+            h('h2', this.data2),
+          ])
+        },
+      })
+      customElements.define('my-watch-element', E)
+
+      render(h('my-watch-element'), container)
+      const e = container.childNodes[0] as VueElement
+      expect(e).toBeInstanceOf(E)
+      expect(e._instance).toBeTruthy()
+      expect(e.shadowRoot!.innerHTML).toBe(
+        `<div><h1>default1</h1><h1>default2</h1><h2>defaultData1</h2><h2>defaultData2</h2></div>`,
+      )
+      expect(prop1Calls).toBe(0)
+      expect(prop2Calls).toBe(0)
+
+      // patch props
+      render(
+        h('my-watch-element', { prop1: 'newValue1', prop2: 'newValue2' }),
+        container,
+      )
+      await nextTick()
+      expect(e.shadowRoot!.innerHTML).toBe(
+        `<div><h1>newValue1</h1><h1>newValue2</h1><h2>newValue1</h2><h2>newValue2</h2></div>`,
+      )
+      expect(prop1Calls).toBe(1)
+      expect(prop2Calls).toBe(1)
+
+      // same prop values
+      render(
+        h('my-watch-element', { prop1: 'newValue1', prop2: 'newValue2' }),
+        container,
+      )
+      await nextTick()
+      expect(e.shadowRoot!.innerHTML).toBe(
+        `<div><h1>newValue1</h1><h1>newValue2</h1><h2>newValue1</h2><h2>newValue2</h2></div>`,
+      )
+      expect(prop1Calls).toBe(1)
+      expect(prop2Calls).toBe(1)
+
+      // update only prop1
+      render(
+        h('my-watch-element', { prop1: 'newValue3', prop2: 'newValue2' }),
+        container,
+      )
+      await nextTick()
+      expect(e.shadowRoot!.innerHTML).toBe(
+        `<div><h1>newValue3</h1><h1>newValue2</h1><h2>newValue1</h2><h2>newValue2</h2></div>`,
+      )
+      expect(prop1Calls).toBe(2)
+      expect(prop2Calls).toBe(1)
+    })
+
+    test('should patch all props together (async)', async () => {
+      let prop1Calls = 0
+      let prop2Calls = 0
+      const E = defineCustomElement(
+        defineAsyncComponent(() =>
+          Promise.resolve(
+            defineComponent({
+              props: {
+                prop1: {
+                  type: String,
+                  default: 'default1',
+                },
+                prop2: {
+                  type: String,
+                  default: 'default2',
+                },
+              },
+              data() {
+                return {
+                  data1: 'defaultData1',
+                  data2: 'defaultData2',
+                }
+              },
+              watch: {
+                prop1(_) {
+                  prop1Calls++
+                  this.data2 = this.prop2
+                },
+                prop2(_) {
+                  prop2Calls++
+                  this.data1 = this.prop1
+                },
+              },
+              render() {
+                return h('div', [
+                  h('h1', this.prop1),
+                  h('h1', this.prop2),
+                  h('h2', this.data1),
+                  h('h2', this.data2),
+                ])
+              },
+            }),
+          ),
+        ),
+      )
+      customElements.define('my-async-watch-element', E)
+
+      render(h('my-async-watch-element'), container)
+
+      await new Promise(r => setTimeout(r))
+      const e = container.childNodes[0] as VueElement
+      expect(e).toBeInstanceOf(E)
+      expect(e._instance).toBeTruthy()
+      expect(e.shadowRoot!.innerHTML).toBe(
+        `<div><h1>default1</h1><h1>default2</h1><h2>defaultData1</h2><h2>defaultData2</h2></div>`,
+      )
+      expect(prop1Calls).toBe(0)
+      expect(prop2Calls).toBe(0)
+
+      // patch props
+      render(
+        h('my-async-watch-element', { prop1: 'newValue1', prop2: 'newValue2' }),
+        container,
+      )
+      await nextTick()
+      expect(e.shadowRoot!.innerHTML).toBe(
+        `<div><h1>newValue1</h1><h1>newValue2</h1><h2>newValue1</h2><h2>newValue2</h2></div>`,
+      )
+      expect(prop1Calls).toBe(1)
+      expect(prop2Calls).toBe(1)
+
+      // same prop values
+      render(
+        h('my-async-watch-element', { prop1: 'newValue1', prop2: 'newValue2' }),
+        container,
+      )
+      await nextTick()
+      expect(e.shadowRoot!.innerHTML).toBe(
+        `<div><h1>newValue1</h1><h1>newValue2</h1><h2>newValue1</h2><h2>newValue2</h2></div>`,
+      )
+      expect(prop1Calls).toBe(1)
+      expect(prop2Calls).toBe(1)
+
+      // update only prop1
+      render(
+        h('my-async-watch-element', { prop1: 'newValue3', prop2: 'newValue2' }),
+        container,
+      )
+      await nextTick()
+      expect(e.shadowRoot!.innerHTML).toBe(
+        `<div><h1>newValue3</h1><h1>newValue2</h1><h2>newValue1</h2><h2>newValue2</h2></div>`,
+      )
+      expect(prop1Calls).toBe(2)
+      expect(prop2Calls).toBe(1)
+    })
   })
 
   describe('attrs', () => {
@@ -1430,6 +1614,44 @@ describe('defineCustomElement', () => {
       app.unmount()
     })
 
+    test('teleport target is ancestor of custom element host', async () => {
+      const Child = defineCustomElement(
+        {
+          render() {
+            return [
+              h(Teleport, { to: '#t1' }, [renderSlot(this.$slots, 'header')]),
+            ]
+          },
+        },
+        { shadowRoot: false },
+      )
+      customElements.define('my-el-teleport-child-target', Child)
+
+      const App = {
+        render() {
+          return h('div', { id: 't1' }, [
+            h('my-el-teleport-child-target', null, {
+              default: () => [h('div', { slot: 'header' }, 'header')],
+            }),
+          ])
+        },
+      }
+      const app = createApp(App)
+      app.mount(container)
+
+      const target1 = document.getElementById('t1')!
+      expect(target1.outerHTML).toBe(
+        `<div id="t1">` +
+          `<my-el-teleport-child-target data-v-app="">` +
+          `<!--teleport start--><!--teleport end-->` +
+          `</my-el-teleport-child-target>` +
+          `<div slot="header">header</div>` +
+          `</div>`,
+      )
+
+      app.unmount()
+    })
+
     test('toggle nested custom element with shadowRoot: false', async () => {
       customElements.define(
         'my-el-child-shadow-false',

+ 3 - 0
packages/runtime-dom/__tests__/directives/vModel.spec.ts

@@ -345,6 +345,9 @@ describe('vModel', () => {
     triggerEvent('input', number)
     await nextTick()
     expect(data.number).toEqual(1.2)
+    triggerEvent('change', number)
+    await nextTick()
+    expect(number.value).toEqual('1.2')
 
     trim.value = '    hello, world    '
     triggerEvent('input', trim)

+ 34 - 0
packages/runtime-dom/__tests__/patchAttrs.spec.ts

@@ -88,4 +88,38 @@ describe('runtime-dom: attrs patching', () => {
     expect(el2.dataset.test).toBe(undefined)
     expect(testvalue).toBe(obj)
   })
+
+  // #13946
+  test('sandbox should be handled as attribute even if property exists', () => {
+    const iframe = document.createElement('iframe') as any
+    let propSetCount = 0
+    // simulate sandbox property in jsdom environment
+    Object.defineProperty(iframe, 'sandbox', {
+      configurable: true,
+      enumerable: true,
+      get() {
+        return this._sandbox
+      },
+      set(v) {
+        propSetCount++
+        this._sandbox = v
+      },
+    })
+
+    patchProp(iframe, 'sandbox', null, 'allow-scripts')
+    expect(iframe.getAttribute('sandbox')).toBe('allow-scripts')
+    expect(propSetCount).toBe(0)
+
+    patchProp(iframe, 'sandbox', 'allow-scripts', null)
+    expect(iframe.hasAttribute('sandbox')).toBe(false)
+    expect(iframe.getAttribute('sandbox')).toBe(null)
+    expect(propSetCount).toBe(0)
+
+    patchProp(iframe, 'sandbox', null, '')
+    expect(iframe.getAttribute('sandbox')).toBe('')
+    expect(iframe.hasAttribute('sandbox')).toBe(true)
+    expect(propSetCount).toBe(0)
+
+    delete iframe.sandbox
+  })
 })

+ 35 - 7
packages/runtime-dom/src/apiCustomElement.ts

@@ -229,6 +229,8 @@ export class VueElement
 
   private _connected = false
   private _resolved = false
+  private _patching = false
+  private _dirty = false
   private _numberProps: Record<string, true> | null = null
   private _styleChildren = new WeakSet()
   private _pendingResolve: Promise<void> | undefined
@@ -468,11 +470,11 @@ export class VueElement
     // defining getter/setters on prototype
     for (const key of declaredPropKeys.map(camelize)) {
       Object.defineProperty(this, key, {
-        get() {
+        get(this: VueElement) {
           return this._getProp(key)
         },
-        set(val) {
-          this._setProp(key, val, true, true)
+        set(this: VueElement, val) {
+          this._setProp(key, val, true, !this._patching)
         },
       })
     }
@@ -506,6 +508,7 @@ export class VueElement
     shouldUpdate = false,
   ): void {
     if (val !== this._props[key]) {
+      this._dirty = true
       if (val === REMOVAL) {
         delete this._props[key]
       } else {
@@ -685,11 +688,18 @@ export class VueElement
     if (this._teleportTargets) {
       roots.push(...this._teleportTargets)
     }
-    return roots.reduce<HTMLSlotElement[]>((res, i) => {
-      res.push(...Array.from(i.querySelectorAll('slot')))
-      return res
-    }, [])
+
+    const slots = new Set<HTMLSlotElement>()
+    for (const root of roots) {
+      const found = root.querySelectorAll<HTMLSlotElement>('slot')
+      for (let i = 0; i < found.length; i++) {
+        slots.add(found[i])
+      }
+    }
+
+    return Array.from(slots)
   }
+
   /**
    * @internal
    */
@@ -697,6 +707,24 @@ export class VueElement
     this._applyStyles(comp.styles, comp)
   }
 
+  /**
+   * @internal
+   */
+  _beginPatch(): void {
+    this._patching = true
+    this._dirty = false
+  }
+
+  /**
+   * @internal
+   */
+  _endPatch(): void {
+    this._patching = false
+    if (this._dirty && this._instance) {
+      this._update()
+    }
+  }
+
   /**
    * @internal
    */

+ 18 - 10
packages/runtime-dom/src/components/TransitionGroup.ts

@@ -30,9 +30,14 @@ import {
 } from '@vue/runtime-core'
 import { extend } from '@vue/shared'
 
-const positionMap = new WeakMap<VNode, DOMRect>()
-const newPositionMap = new WeakMap<VNode, DOMRect>()
-export const moveCbKey: symbol = Symbol('_moveCb')
+interface Position {
+  top: number
+  left: number
+}
+
+const positionMap = new WeakMap<VNode, Position>()
+const newPositionMap = new WeakMap<VNode, Position>()
+export const moveCbKey: unique symbol = Symbol('_moveCb')
 const enterCbKey = Symbol('_enterCb')
 
 export type TransitionGroupProps = Omit<TransitionProps, 'mode'> & {
@@ -133,10 +138,10 @@ const TransitionGroupImpl: ComponentOptions = /*@__PURE__*/ decorate({
                 instance,
               ),
             )
-            positionMap.set(
-              child,
-              (child.el as Element).getBoundingClientRect(),
-            )
+            positionMap.set(child, {
+              left: (child.el as HTMLElement).offsetLeft,
+              top: (child.el as HTMLElement).offsetTop,
+            })
           }
         }
       }
@@ -176,7 +181,10 @@ export function callPendingCbs(el: any): void {
 }
 
 function recordPosition(c: VNode) {
-  newPositionMap.set(c, (c.el as Element).getBoundingClientRect())
+  newPositionMap.set(c, {
+    left: (c.el as HTMLElement).offsetLeft,
+    top: (c.el as HTMLElement).offsetTop,
+  })
 }
 
 function applyTranslation(c: VNode): VNode | undefined {
@@ -193,8 +201,8 @@ function applyTranslation(c: VNode): VNode | undefined {
 
 // shared between vdom and vapor
 export function baseApplyTranslation(
-  oldPos: DOMRect,
-  newPos: DOMRect,
+  oldPos: Position,
+  newPos: Position,
   el: ElementWithTransition,
 ): boolean {
   const dx = oldPos.left - newPos.left

+ 11 - 10
packages/runtime-dom/src/directives/vModel.ts

@@ -74,6 +74,12 @@ export const vModelText: ModelDirective<
   },
 }
 
+function castValue(value: string, trim?: boolean, number?: boolean | null) {
+  if (trim) value = value.trim()
+  if (number) value = looseToNumber(value)
+  return value
+}
+
 /**
  * @internal
  */
@@ -86,18 +92,13 @@ export const vModelTextInit = (
 ): void => {
   addEventListener(el, lazy ? 'change' : 'input', e => {
     if ((e.target as any).composing) return
-    let domValue: string | number = el.value
-    if (trim) {
-      domValue = domValue.trim()
-    }
-    if (number || el.type === 'number') {
-      domValue = looseToNumber(domValue)
-    }
-    ;(set || (el as any)[assignKey])(domValue)
+    ;(set || (el as any)[assignKey])(
+      castValue(el.value, trim, number || el.type === 'number'),
+    )
   })
-  if (trim) {
+  if (trim || number) {
     addEventListener(el, 'change', () => {
-      el.value = el.value.trim()
+      el.value = castValue(el.value, trim, number || el.type === 'number')
     })
   }
   if (!lazy) {

+ 1 - 1
packages/runtime-dom/src/jsx.ts

@@ -1440,7 +1440,7 @@ type EventHandlers<E> = {
 
 import type { VNodeRef } from '@vue/runtime-core'
 
-export type ReservedProps = {
+export interface ReservedProps {
   key?: PropertyKey | undefined
   ref?: VNodeRef | undefined
   ref_for?: boolean | undefined

+ 35 - 0
packages/server-renderer/__tests__/ssrDirectives.spec.ts

@@ -263,6 +263,41 @@ describe('ssr: directives', () => {
     })
   })
 
+  describe('template with v-text / v-html', () => {
+    test('element with v-html', async () => {
+      expect(
+        await renderToString(
+          createApp({
+            data: () => ({ foo: 'hello' }),
+            template: `<span v-html="foo"/>`,
+          }),
+        ),
+      ).toBe(`<span>hello</span>`)
+    })
+
+    test('textarea with v-text', async () => {
+      expect(
+        await renderToString(
+          createApp({
+            data: () => ({ foo: 'hello' }),
+            template: `<textarea v-text="foo"/>`,
+          }),
+        ),
+      ).toBe(`<textarea>hello</textarea>`)
+    })
+
+    test('textarea with v-html', async () => {
+      expect(
+        await renderToString(
+          createApp({
+            data: () => ({ foo: 'hello' }),
+            template: `<textarea v-html="foo"/>`,
+          }),
+        ),
+      ).toBe(`<textarea>hello</textarea>`)
+    })
+  })
+
   describe('vnode v-show', () => {
     test('basic', async () => {
       expect(

+ 7 - 0
packages/shared/src/domAttrConfig.ts

@@ -199,5 +199,12 @@ export function shouldSetAsAttr(tagName: string, key: string): boolean {
     return true
   }
 
+  // #13946 iframe.sandbox should always be set as attribute since setting
+  // the property to null results in 'null' string, and setting to empty string
+  // enables the most restrictive sandbox mode instead of no sandboxing.
+  if (key === 'sandbox' && tagName === 'IFRAME') {
+    return true
+  }
+
   return false
 }

+ 56 - 0
packages/vue/__tests__/e2e/TransitionGroup.spec.ts

@@ -648,6 +648,62 @@ describe('e2e: TransitionGroup', () => {
     E2E_TIMEOUT,
   )
 
+  // #6105
+  test(
+    'with scale',
+    async () => {
+      await page().evaluate(() => {
+        const { createApp, ref, onMounted } = (window as any).Vue
+        createApp({
+          template: `
+            <div id="container">
+              <div class="scale" style="transform: scale(2) translateX(50%) translateY(50%)">
+                <transition-group tag="ul">
+                  <li v-for="item in items" :key="item">{{item}}</li>
+                </transition-group>
+                <button id="toggleBtn" @click="click">button</button>
+              </div>
+            </div>
+          `,
+          setup: () => {
+            const items = ref(['a', 'b', 'c'])
+            const click = () => {
+              items.value.reverse()
+            }
+
+            onMounted(() => {
+              const styleNode = document.createElement('style')
+              styleNode.innerHTML = `.v-move {
+                transition: transform 0.5s ease;
+              }`
+              document.body.appendChild(styleNode)
+            })
+
+            return { items, click }
+          },
+        }).mount('#app')
+      })
+
+      const original_top = await page().$eval('ul li:nth-child(1)', node => {
+        return node.getBoundingClientRect().top
+      })
+      const new_top = await page().evaluate(() => {
+        const el = document.querySelector('ul li:nth-child(1)')
+        const p = new Promise(resolve => {
+          el!.addEventListener('transitionstart', () => {
+            const new_top = el!.getBoundingClientRect().top
+            resolve(new_top)
+          })
+        })
+        ;(document.querySelector('#toggleBtn') as any)!.click()
+        return p
+      })
+
+      expect(original_top).toBeLessThan(new_top as number)
+    },
+    E2E_TIMEOUT,
+  )
+
   test(
     'not leaking after children unmounted',
     async () => {

+ 1 - 1
packages/vue/jsx-runtime/index.d.ts

@@ -1,4 +1,4 @@
-/* eslint-disable @typescript-eslint/prefer-ts-expect-error */
+/* eslint-disable @typescript-eslint/ban-ts-comment */
 import type { NativeElements, ReservedProps, VNode } from '@vue/runtime-dom'
 
 /**

+ 1 - 1
packages/vue/jsx.d.ts

@@ -1,4 +1,4 @@
-/* eslint-disable @typescript-eslint/prefer-ts-expect-error */
+/* eslint-disable @typescript-eslint/ban-ts-comment */
 // global JSX namespace registration
 // somehow we have to copy=pase the jsx-runtime types here to make TypeScript happy
 import type { NativeElements, ReservedProps, VNode } from '@vue/runtime-dom'

Разница между файлами не показана из-за своего большого размера
+ 197 - 199
pnpm-lock.yaml


+ 3 - 3
pnpm-workspace.yaml

@@ -3,12 +3,12 @@ packages:
   - 'packages-private/*'
 
 catalog:
-  '@babel/parser': ^7.28.4
-  '@babel/types': ^7.28.4
+  '@babel/parser': ^7.28.5
+  '@babel/types': ^7.28.5
   'estree-walker': ^2.0.2
   'vite': ^6.1.0
   '@vitejs/plugin-vue': ^6.0.1
-  'magic-string': ^0.30.19
+  'magic-string': ^0.30.21
   'source-map-js': ^1.2.1
 
 onlyBuiltDependencies:

Некоторые файлы не были показаны из-за большого количества измененных файлов