Преглед изворни кода

Merge branch 'main' into edison/fix/asyncComponentPlaceholder

edison пре 5 месеци
родитељ
комит
c68204a940
46 измењених фајлова са 917 додато и 320 уклоњено
  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. 1 1
      .vscode/settings.json
  8. 6 2
      eslint.config.js
  9. 14 14
      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. 6 10
      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. 1 1
      packages/reactivity/src/effect.ts
  24. 5 0
      packages/runtime-core/__tests__/hydration.spec.ts
  25. 4 1
      packages/runtime-core/src/apiAsyncComponent.ts
  26. 8 1
      packages/runtime-core/src/apiSetupHelpers.ts
  27. 8 0
      packages/runtime-core/src/component.ts
  28. 2 2
      packages/runtime-core/src/componentOptions.ts
  29. 14 3
      packages/runtime-core/src/componentPublicInstance.ts
  30. 8 4
      packages/runtime-core/src/hydration.ts
  31. 21 9
      packages/runtime-core/src/renderer.ts
  32. 222 0
      packages/runtime-dom/__tests__/customElement.spec.ts
  33. 51 0
      packages/runtime-dom/__tests__/directives/vModel.spec.ts
  34. 34 0
      packages/runtime-dom/__tests__/patchAttrs.spec.ts
  35. 35 7
      packages/runtime-dom/src/apiCustomElement.ts
  36. 15 7
      packages/runtime-dom/src/components/TransitionGroup.ts
  37. 9 10
      packages/runtime-dom/src/directives/vModel.ts
  38. 1 1
      packages/runtime-dom/src/jsx.ts
  39. 7 0
      packages/runtime-dom/src/patchProp.ts
  40. 35 0
      packages/server-renderer/__tests__/ssrDirectives.spec.ts
  41. 0 1
      packages/vue-compat/README.md
  42. 56 0
      packages/vue/__tests__/e2e/TransitionGroup.spec.ts
  43. 1 1
      packages/vue/jsx-runtime/index.d.ts
  44. 1 1
      packages/vue/jsx.d.ts
  45. 237 207
      pnpm-lock.yaml
  46. 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

@@ -26,7 +26,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'
@@ -88,10 +88,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'

+ 1 - 1
.vscode/settings.json

@@ -4,7 +4,7 @@
 
   "cSpell.enabledLanguageIds": ["markdown", "plaintext", "text", "yml"],
 
-  // Use prettier to format typescript, javascript and JSON files
+  // Use prettier to format TypeScript, JavaScript and JSON files
   "[typescript]": {
     "editor.defaultFormatter": "esbenp.prettier-vscode"
   },

+ 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',

+ 14 - 14
package.json

@@ -1,7 +1,7 @@
 {
   "private": true,
   "version": "3.5.22",
-  "packageManager": "pnpm@10.17.1",
+  "packageManager": "pnpm@10.20.0",
   "type": "module",
   "scripts": {
     "dev": "node scripts/dev.js",
@@ -65,43 +65,43 @@
     "@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/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",
-    "rollup": "^4.52.2",
+    "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

@@ -11,7 +11,7 @@
     "enableNonBrowserBranches": true
   },
   "dependencies": {
-    "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,
@@ -347,15 +348,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(
@@ -372,7 +376,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

@@ -58,10 +58,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"
   }
 }

+ 6 - 10
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`,
@@ -853,7 +850,7 @@ export function registerTS(_loadTS: () => typeof TS): void {
       ) {
         throw new Error(
           'Failed to load TypeScript, which is required for resolving imported types. ' +
-            'Please make sure "typescript" is installed as a project dependency.',
+            'Please make sure "TypeScript" is installed as a project dependency.',
         )
       } else {
         throw new Error(
@@ -951,7 +948,7 @@ function importSourceToScope(
         if (!ts) {
           return ctx.error(
             `Failed to resolve import source ${JSON.stringify(source)}. ` +
-              `typescript is required as a peer dep for vue in order ` +
+              `TypeScript is required as a peer dep for vue in order ` +
               `to support resolving types from module imports.`,
             node,
             scope,
@@ -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,

+ 1 - 1
packages/reactivity/src/effect.ts

@@ -311,7 +311,7 @@ function prepareDeps(sub: Subscriber) {
 }
 
 function cleanupDeps(sub: Subscriber) {
-  // Cleanup unsued deps
+  // Cleanup unused deps
   let head
   let tail = sub.depsTail
   let link = tail

+ 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

@@ -241,7 +241,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

@@ -1270,6 +1270,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.
    */

+ 2 - 2
packages/runtime-core/src/componentOptions.ts

@@ -444,8 +444,8 @@ interface LegacyOptions<
    * #3468
    *
    * type-only, used to assist Mixin's type inference,
-   * typescript will try to simplify the inferred `Mixin` type,
-   * with the `__differentiator`, typescript won't be able to combine different mixins,
+   * TypeScript will try to simplify the inferred `Mixin` type,
+   * with the `__differentiator`, TypeScript won't be able to combine different mixins,
    * because the `__differentiator` will be different
    */
   __differentiator?: keyof D | keyof C | keyof M

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

@@ -448,7 +448,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 (
@@ -545,7 +549,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)) {
@@ -582,7 +590,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) ||

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

@@ -460,18 +460,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

@@ -621,15 +621,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',

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

@@ -5,6 +5,7 @@ import {
   nextTick,
   ref,
   render,
+  vModelCheckbox,
   vModelDynamic,
   withDirectives,
 } from '@vue/runtime-dom'
@@ -344,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)
@@ -1445,4 +1449,51 @@ describe('vModel', () => {
 
     expect(inputNum1.value).toBe('1')
   })
+
+  it(`should support mutating an array or set value for a checkbox`, async () => {
+    const component = defineComponent({
+      data() {
+        return { value: [] }
+      },
+      render() {
+        return [
+          withDirectives(
+            h('input', {
+              type: 'checkbox',
+              class: 'foo',
+              value: 'foo',
+              'onUpdate:modelValue': setValue.bind(this),
+            }),
+            [[vModelCheckbox, this.value]],
+          ),
+        ]
+      },
+    })
+    render(h(component), root)
+
+    const foo = root.querySelector('.foo')
+    const data = root._vnode.component.data
+
+    expect(foo.checked).toEqual(false)
+
+    data.value.push('foo')
+    await nextTick()
+    expect(foo.checked).toEqual(true)
+
+    data.value[0] = 'bar'
+    await nextTick()
+    expect(foo.checked).toEqual(false)
+
+    data.value = new Set()
+    await nextTick()
+    expect(foo.checked).toEqual(false)
+
+    data.value.add('foo')
+    await nextTick()
+    expect(foo.checked).toEqual(true)
+
+    data.value.delete('foo')
+    await nextTick()
+    expect(foo.checked).toEqual(false)
+  })
 })

+ 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
    */

+ 15 - 7
packages/runtime-dom/src/components/TransitionGroup.ts

@@ -29,8 +29,13 @@ import {
 } from '@vue/runtime-core'
 import { extend } from '@vue/shared'
 
-const positionMap = new WeakMap<VNode, DOMRect>()
-const newPositionMap = new WeakMap<VNode, DOMRect>()
+interface Position {
+  top: number
+  left: number
+}
+
+const positionMap = new WeakMap<VNode, Position>()
+const newPositionMap = new WeakMap<VNode, Position>()
 const moveCbKey = Symbol('_moveCb')
 const enterCbKey = Symbol('_enterCb')
 
@@ -145,10 +150,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,
+            })
           }
         }
       }
@@ -189,7 +194,10 @@ function callPendingCbs(c: VNode) {
 }
 
 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 {

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

@@ -45,6 +45,12 @@ type ModelDirective<T, Modifiers extends string = string> = ObjectDirective<
   Modifiers
 >
 
+function castValue(value: string, trim?: boolean, number?: boolean | null) {
+  if (trim) value = value.trim()
+  if (number) value = looseToNumber(value)
+  return value
+}
+
 // We are exporting the v-model runtime directly as vnode hooks so that it can
 // be tree-shaken in case v-model is never used.
 export const vModelText: ModelDirective<
@@ -57,18 +63,11 @@ export const vModelText: ModelDirective<
       number || (vnode.props && vnode.props.type === 'number')
     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 (castToNumber) {
-        domValue = looseToNumber(domValue)
-      }
-      el[assignKey](domValue)
+      el[assignKey](castValue(el.value, trim, castToNumber))
     })
-    if (trim) {
+    if (trim || castToNumber) {
       addEventListener(el, 'change', () => {
-        el.value = el.value.trim()
+        el.value = castValue(el.value, trim, castToNumber)
       })
     }
     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

+ 7 - 0
packages/runtime-dom/src/patchProp.ts

@@ -111,6 +111,13 @@ function shouldSetAsProp(
     return false
   }
 
+  // #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' && el.tagName === 'IFRAME') {
+    return false
+  }
+
   // #1787, #2840 form property on form elements is readonly and must be set as
   // attribute.
   if (key === 'form') {

+ 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(

+ 0 - 1
packages/vue-compat/README.md

@@ -320,7 +320,6 @@ Features that start with `COMPILER_` are compiler-specific: if you are using the
 | FILTERS                      | ✔   | Filters removed (this option affects only runtime filter APIs)        | [link](https://v3-migration.vuejs.org/breaking-changes/filters.html)                                                      |
 | COMPILER_IS_ON_ELEMENT       | ✔   | `is` usage is now restricted to `<component>` only                    | [link](https://v3-migration.vuejs.org/breaking-changes/custom-elements-interop.html)                                      |
 | COMPILER_V_BIND_SYNC         | ✔   | `v-bind.sync` replaced by `v-model` with arguments                    | [link](https://v3-migration.vuejs.org/breaking-changes/v-model.html)                                                      |
-| COMPILER_V_BIND_PROP         | ✔   | `v-bind.prop` modifier removed                                        |                                                                                                                           |
 | COMPILER_V_BIND_OBJECT_ORDER | ✔   | `v-bind="object"` is now order sensitive                              | [link](https://v3-migration.vuejs.org/breaking-changes/v-bind.html)                                                       |
 | COMPILER_V_ON_NATIVE         | ✔   | `v-on.native` modifier removed                                        | [link](https://v3-migration.vuejs.org/breaking-changes/v-on-native-modifier-removed.html)                                 |
 | COMPILER_V_FOR_REF           | ✔   | `ref` in `v-for` (compiler support)                                   |                                                                                                                           |

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

@@ -646,6 +646,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'

Разлика између датотеке није приказан због своје велике величине
+ 237 - 207
pnpm-lock.yaml


+ 3 - 3
pnpm-workspace.yaml

@@ -3,10 +3,10 @@ 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
-  'magic-string': ^0.30.19
+  'magic-string': ^0.30.21
   'source-map-js': ^1.2.1
   'vite': ^5.4.15
   '@vitejs/plugin-vue': ^6.0.1

Неке датотеке нису приказане због велике количине промена