diff --git a/.github/renovate.json5 b/.github/renovate.json5
index e87294b5b1d..9c04587027d 100644
--- a/.github/renovate.json5
+++ b/.github/renovate.json5
@@ -28,7 +28,7 @@
},
{
groupName: 'build',
- matchPackageNames: ['vite', 'terser'],
+ matchPackageNames: ['vite', '@swc/core'],
matchPackagePrefixes: ['rollup', 'esbuild', '@rollup', '@vitejs'],
},
{
diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml
index 24f012a9d71..8daee2ed5f9 100644
--- a/.github/workflows/autofix.yml
+++ b/.github/workflows/autofix.yml
@@ -31,4 +31,4 @@ jobs:
- name: Run prettier
run: pnpm run format
- - uses: autofix-ci/action@dd55f44df8f7cdb7a6bf74c78677eb8acd40cd0a
+ - uses: autofix-ci/action@2891949f3779a1cafafae1523058501de3d4e944
diff --git a/.github/workflows/size-data.yml b/.github/workflows/size-data.yml
index 767114e0d9f..a702d0fef90 100644
--- a/.github/workflows/size-data.yml
+++ b/.github/workflows/size-data.yml
@@ -41,3 +41,13 @@ jobs:
with:
name: size-data
path: temp/size
+
+ - name: Save PR number
+ if: ${{github.event_name == 'pull_request'}}
+ run: echo ${{ github.event.number }} > ./pr.txt
+
+ - uses: actions/upload-artifact@v4
+ if: ${{github.event_name == 'pull_request'}}
+ with:
+ name: pr-number
+ path: pr.txt
diff --git a/.github/workflows/size-report.yml b/.github/workflows/size-report.yml
index eb0d7d4fd83..5cec662874e 100644
--- a/.github/workflows/size-report.yml
+++ b/.github/workflows/size-report.yml
@@ -35,6 +35,19 @@ jobs:
- name: Install dependencies
run: pnpm install
+ - name: Download PR number
+ uses: dawidd6/action-download-artifact@v6
+ with:
+ name: pr-number
+ run_id: ${{ github.event.workflow_run.id }}
+ path: /tmp/pr-number
+
+ - name: Read PR Number
+ id: pr-number
+ uses: juliangruber/read-file-action@v1
+ with:
+ path: /tmp/pr-number/pr.txt
+
- name: Download Size Data
uses: dawidd6/action-download-artifact@v6
with:
@@ -55,10 +68,18 @@ jobs:
- name: Prepare report
run: pnpm tsx scripts/size-report.ts > size-report.md
+ - name: Read Size Report
+ id: size-report
+ uses: juliangruber/read-file-action@v1
+ with:
+ path: ./size-report.md
+
- name: Create Comment
- uses: thollander/actions-comment-pull-request@v2.5.0
+ uses: actions-cool/maintain-one-comment@v3
with:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- filePath: size-report.md
- pr_number: ${{ github.event.workflow_run.pull_requests[0].number }}
- comment_tag: VUE_CORE_SIZE
+ token: ${{ secrets.GITHUB_TOKEN }}
+ number: ${{ steps.pr-number.outputs.content }}
+ body: |
+ ${{ steps.size-report.outputs.content }}
+
+ body-include: ''
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 41f21c0647b..8bb29d9eedc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,36 @@
+## [3.4.32](https://github.com/vuejs/core/compare/v3.4.31...v3.4.32) (2024-07-17)
+
+
+### Bug Fixes
+
+* **build:** use consistent minify options from previous terser config ([789675f](https://github.com/vuejs/core/commit/789675f65d2b72cf979ba6a29bd323f716154a4b))
+* **compiler-sfc:** correctly resolve type annotation for declared function ([#11279](https://github.com/vuejs/core/issues/11279)) ([b287aee](https://github.com/vuejs/core/commit/b287aeec3ea85f20e4b1fc3d907c901bdc2a0176)), closes [#11266](https://github.com/vuejs/core/issues/11266)
+* **defineModel:** force local update when setter results in same emitted value ([de174e1](https://github.com/vuejs/core/commit/de174e1aa756508c7542605a448e55a373afb1ed)), closes [#10279](https://github.com/vuejs/core/issues/10279) [#10301](https://github.com/vuejs/core/issues/10301)
+* **hmr:** hmr reload should work with async component ([#11248](https://github.com/vuejs/core/issues/11248)) ([c8b9794](https://github.com/vuejs/core/commit/c8b97945759e869c997d60c3350d2451c5ff7887))
+* **hydration:** fix tracking of reactive style objects in production ([c10e40a](https://github.com/vuejs/core/commit/c10e40a217b89ab7e0f7f3515242d4246ecffbdd)), closes [#11372](https://github.com/vuejs/core/issues/11372)
+* **hydration:** handle consectuvie text nodes during hydration ([f44c3b3](https://github.com/vuejs/core/commit/f44c3b37d446d5f8e34539029dae0d806b25bb47)), closes [#7285](https://github.com/vuejs/core/issues/7285) [#7301](https://github.com/vuejs/core/issues/7301)
+* **reactivity:** ensure `unref` correctly resolves type for `ShallowRef` ([#11360](https://github.com/vuejs/core/issues/11360)) ([a509e30](https://github.com/vuejs/core/commit/a509e30f059fcdd158f39fdf34670b1019eaf2d1)), closes [#11356](https://github.com/vuejs/core/issues/11356)
+* **reactivity:** shallowReactive map "unwraps" the nested refs ([#8503](https://github.com/vuejs/core/issues/8503)) ([50ddafe](https://github.com/vuejs/core/commit/50ddafe91b9195cf94124466239f82c9794699fb)), closes [#8501](https://github.com/vuejs/core/issues/8501) [#11249](https://github.com/vuejs/core/issues/11249)
+* **runtime-core:** avoid recursive warning ([3ee7b4c](https://github.com/vuejs/core/commit/3ee7b4c7b1374c5bdc50a579b49f6bc15022b085)), closes [#8074](https://github.com/vuejs/core/issues/8074)
+* **runtime-core:** bail manually rendered compiler slot fragments in all cases ([3d34f40](https://github.com/vuejs/core/commit/3d34f406ac7497dafd2f4e62ab23579b78a0e08a)), closes [#10870](https://github.com/vuejs/core/issues/10870)
+* **runtime-core:** do not emit when defineModel ref is set with same value ([#11162](https://github.com/vuejs/core/issues/11162)) ([f1bb0ae](https://github.com/vuejs/core/commit/f1bb0aef084b5cdd4d49aecfed01ec106d9b6897)), closes [#11125](https://github.com/vuejs/core/issues/11125)
+* **runtime-core:** errors during component patch should be caught by error handlers ([ee0248a](https://github.com/vuejs/core/commit/ee0248accff589a94688e177e5e3af10c18288cb))
+* **runtime-core:** force diff slot fallback content and provided content ([d76dd9c](https://github.com/vuejs/core/commit/d76dd9c58de24b273bc55af3a8ed81ba693e9683)), closes [#7256](https://github.com/vuejs/core/issues/7256) [#9200](https://github.com/vuejs/core/issues/9200) [#9308](https://github.com/vuejs/core/issues/9308) [#7266](https://github.com/vuejs/core/issues/7266) [#9213](https://github.com/vuejs/core/issues/9213)
+* **runtime-core:** more edge case fix for manually rendered compiled slot ([685e3f3](https://github.com/vuejs/core/commit/685e3f381c024b9f4023e60fe0545dc60d90d984)), closes [#11336](https://github.com/vuejs/core/issues/11336)
+* **runtime-core:** use separate prop caches for components and mixins ([#11350](https://github.com/vuejs/core/issues/11350)) ([b0aa234](https://github.com/vuejs/core/commit/b0aa234e5e7a611c018de68bc31e0cf55518d5ce)), closes [#7998](https://github.com/vuejs/core/issues/7998)
+* **runtime-dom:** properly handle innerHTML unmount into new children ([#11159](https://github.com/vuejs/core/issues/11159)) ([3e9e32e](https://github.com/vuejs/core/commit/3e9e32ee0a6d0fbf67e9098a66ff0a1ea6647806)), closes [#9135](https://github.com/vuejs/core/issues/9135)
+* **teleport:** skip teleported nodes when locating patch anchor ([8655ced](https://github.com/vuejs/core/commit/8655ced480ea0fe453ff5fe445cecf97b91ec260)), closes [#9071](https://github.com/vuejs/core/issues/9071) [#9134](https://github.com/vuejs/core/issues/9134) [#9313](https://github.com/vuejs/core/issues/9313) [#9313](https://github.com/vuejs/core/issues/9313)
+* **v-model:** component v-model modifiers trim and number when cases don't match ([#9609](https://github.com/vuejs/core/issues/9609)) ([7fb6eb8](https://github.com/vuejs/core/commit/7fb6eb882b64bf99a99d00606e54b0e050674206)), closes [#4848](https://github.com/vuejs/core/issues/4848) [#4850](https://github.com/vuejs/core/issues/4850) [#4850](https://github.com/vuejs/core/issues/4850)
+* **v-once:** properly unmount v-once cached trees ([d343a0d](https://github.com/vuejs/core/commit/d343a0dc01663f91db42b4ddb693e6fffcb45873)), closes [#5154](https://github.com/vuejs/core/issues/5154) [#8809](https://github.com/vuejs/core/issues/8809)
+
+
+### Performance Improvements
+
+* **server-renderer:** avoid unnecessary checks in `createBuffer` ([#11364](https://github.com/vuejs/core/issues/11364)) ([fc205bf](https://github.com/vuejs/core/commit/fc205bf4decde5ce0f4a61394ffa3914b502c287))
+* **server-renderer:** optimize `unrollBuffer` by avoiding promises ([#11340](https://github.com/vuejs/core/issues/11340)) ([05779a7](https://github.com/vuejs/core/commit/05779a70bd0b567ae458a07636d229bd07c44c4e))
+
+
+
## [3.4.31](https://github.com/vuejs/core/compare/v3.4.30...v3.4.31) (2024-06-28)
diff --git a/package.json b/package.json
index bb2576a67b9..e4fa3447529 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"private": true,
- "version": "3.4.31",
- "packageManager": "pnpm@9.4.0",
+ "version": "3.4.32",
+ "packageManager": "pnpm@9.5.0",
"type": "module",
"scripts": {
"dev": "node scripts/dev.js",
@@ -59,58 +59,53 @@
"node": ">=18.12.0"
},
"devDependencies": {
- "@babel/parser": "^7.24.7",
- "@babel/types": "^7.24.7",
- "@codspeed/vitest-plugin": "^3.1.0",
+ "@babel/parser": "catalog:",
+ "@babel/types": "catalog:",
"@rollup/plugin-alias": "^5.1.0",
- "@rollup/plugin-commonjs": "^25.0.8",
+ "@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "5.0.4",
- "@swc/core": "^1.6.5",
+ "@swc/core": "^1.6.13",
"@types/hash-sum": "^1.0.2",
- "@types/minimist": "^1.2.5",
- "@types/node": "^20.14.8",
+ "@types/node": "^20.14.10",
"@types/semver": "^7.5.8",
"@vitest/coverage-istanbul": "^1.6.0",
"@vue/consolidate": "1.0.0",
"conventional-changelog-cli": "^4.1.0",
"enquirer": "^2.4.1",
- "esbuild": "^0.21.5",
+ "esbuild": "^0.23.0",
"esbuild-plugin-polyfill-node": "^0.3.0",
- "eslint": "^9.5.0",
- "eslint-plugin-import-x": "^0.5.1",
+ "eslint": "^9.6.0",
+ "eslint-plugin-import-x": "^0.5.3",
"eslint-plugin-vitest": "^0.5.4",
- "estree-walker": "^2.0.2",
- "execa": "^9.3.0",
+ "estree-walker": "catalog:",
"jsdom": "^24.1.0",
"lint-staged": "^15.2.7",
"lodash": "^4.17.21",
"magic-string": "^0.30.10",
"markdown-table": "^3.0.3",
"marked": "^12.0.2",
- "minimist": "^1.2.8",
- "npm-run-all2": "^6.2.0",
+ "npm-run-all2": "^6.2.2",
"picocolors": "^1.0.1",
"prettier": "^3.3.2",
"pretty-bytes": "^6.1.1",
"pug": "^3.0.3",
- "puppeteer": "~22.12.0",
- "rimraf": "^5.0.7",
- "rollup": "^4.18.0",
+ "puppeteer": "~22.12.1",
+ "rimraf": "^5.0.9",
+ "rollup": "^4.18.1",
"rollup-plugin-dts": "^6.1.1",
"rollup-plugin-esbuild": "^6.1.1",
"rollup-plugin-polyfill-node": "^0.13.0",
"semver": "^7.6.2",
"serve": "^14.2.3",
"simple-git-hooks": "^2.11.1",
- "terser": "^5.31.1",
"todomvc-app-css": "^2.4.3",
"tslib": "^2.6.3",
- "tsx": "^4.15.7",
+ "tsx": "^4.16.2",
"typescript": "~5.4.5",
- "typescript-eslint": "^7.13.1",
- "vite": "^5.3.1",
+ "typescript-eslint": "^7.15.0",
+ "vite": "catalog:",
"vitest": "^1.6.0"
},
"pnpm": {
diff --git a/packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap b/packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap
index 2bdb4afa4c0..db268af4f9b 100644
--- a/packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap
+++ b/packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap
@@ -19,12 +19,12 @@ export function render(_ctx, _cache) {
}"
`;
-exports[`compiler: codegen > CacheExpression w/ isVNode: true 1`] = `
+exports[`compiler: codegen > CacheExpression w/ isVOnce: true 1`] = `
"
export function render(_ctx, _cache) {
return _cache[1] || (
_setBlockTracking(-1),
- _cache[1] = foo,
+ (_cache[1] = foo).cacheIndex = 1,
_setBlockTracking(1),
_cache[1]
)
@@ -54,7 +54,7 @@ return function render(_ctx, _cache) {
[foo + bar]: bar
}, [
_createElementVNode("p", { "some-key": "foo" })
- ], 16)
+ ], 16 /* FULL_PROPS */)
}
}"
`;
@@ -98,7 +98,7 @@ exports[`compiler: codegen > forNode 1`] = `
"
return function render(_ctx, _cache) {
with (_ctx) {
- return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(), 1))
+ return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(), 1 /* TEXT */))
}
}"
`;
diff --git a/packages/compiler-core/__tests__/codegen.spec.ts b/packages/compiler-core/__tests__/codegen.spec.ts
index 9c923075634..7724d507cb2 100644
--- a/packages/compiler-core/__tests__/codegen.spec.ts
+++ b/packages/compiler-core/__tests__/codegen.spec.ts
@@ -267,7 +267,7 @@ describe('compiler: codegen', () => {
disableTracking: true,
props: undefined,
children: createCallExpression(RENDER_LIST),
- patchFlag: '1',
+ patchFlag: PatchFlags.TEXT,
dynamicProps: undefined,
directives: undefined,
loc: locStub,
@@ -303,7 +303,7 @@ describe('compiler: codegen', () => {
disableTracking: false,
props: undefined,
children: createCallExpression(RENDER_LIST),
- patchFlag: genFlagText(PatchFlags.STABLE_FRAGMENT),
+ patchFlag: PatchFlags.STABLE_FRAGMENT,
dynamicProps: undefined,
directives: undefined,
loc: locStub,
@@ -364,7 +364,7 @@ describe('compiler: codegen', () => {
),
],
// flag
- PatchFlags.FULL_PROPS + '',
+ PatchFlags.FULL_PROPS,
),
}),
)
@@ -375,7 +375,7 @@ describe('compiler: codegen', () => {
[foo + bar]: bar
}, [
_${helperNameMap[CREATE_ELEMENT_VNODE]}("p", { "some-key": "foo" })
- ], ${PatchFlags.FULL_PROPS})`)
+ ], ${genFlagText(PatchFlags.FULL_PROPS)})`)
expect(code).toMatchSnapshot()
})
@@ -437,7 +437,7 @@ describe('compiler: codegen', () => {
expect(code).toMatchSnapshot()
})
- test('CacheExpression w/ isVNode: true', () => {
+ test('CacheExpression w/ isVOnce: true', () => {
const { code } = generate(
createRoot({
cached: 1,
@@ -456,7 +456,7 @@ describe('compiler: codegen', () => {
`
_cache[1] || (
_setBlockTracking(-1),
- _cache[1] = foo,
+ (_cache[1] = foo).cacheIndex = 1,
_setBlockTracking(1),
_cache[1]
)
@@ -666,11 +666,14 @@ describe('compiler: codegen', () => {
})
test('with patchFlag and no children/props', () => {
- expect(genCode(createVNodeCall(null, `"div"`, undefined, undefined, '1')))
- .toMatchInlineSnapshot(`
- "return _createElementVNode("div", null, null, 1)
- "
- `)
+ expect(
+ genCode(
+ createVNodeCall(null, `"div"`, undefined, undefined, PatchFlags.TEXT),
+ ),
+ ).toMatchInlineSnapshot(`
+ "return _createElementVNode("div", null, null, 1 /* TEXT */)
+ "
+ `)
})
test('as block', () => {
diff --git a/packages/compiler-core/__tests__/transform.spec.ts b/packages/compiler-core/__tests__/transform.spec.ts
index a56be51bc5c..0946d364838 100644
--- a/packages/compiler-core/__tests__/transform.spec.ts
+++ b/packages/compiler-core/__tests__/transform.spec.ts
@@ -19,7 +19,6 @@ import { transformFor } from '../src/transforms/vFor'
import { transformElement } from '../src/transforms/transformElement'
import { transformSlotOutlet } from '../src/transforms/transformSlotOutlet'
import { transformText } from '../src/transforms/transformText'
-import { genFlagText } from './testUtils'
import { PatchFlags } from '@vue/shared'
describe('compiler: transform', () => {
@@ -358,7 +357,7 @@ describe('compiler: transform', () => {
{ type: NodeTypes.ELEMENT, tag: `div` },
{ type: NodeTypes.ELEMENT, tag: `div` },
] as any,
- genFlagText(PatchFlags.STABLE_FRAGMENT),
+ PatchFlags.STABLE_FRAGMENT,
),
)
})
@@ -374,10 +373,7 @@ describe('compiler: transform', () => {
{ type: NodeTypes.ELEMENT, tag: `div` },
{ type: NodeTypes.COMMENT },
] as any,
- genFlagText([
- PatchFlags.STABLE_FRAGMENT,
- PatchFlags.DEV_ROOT_FRAGMENT,
- ]),
+ PatchFlags.STABLE_FRAGMENT | PatchFlags.DEV_ROOT_FRAGMENT,
),
)
})
diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap
index 1c1203552db..3d13c4066d9 100644
--- a/packages/compiler-core/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap
+++ b/packages/compiler-core/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap
@@ -9,7 +9,7 @@ return function render(_ctx, _cache) {
return _cache[0] || (
_setBlockTracking(-1),
- _cache[0] = _createElementVNode("div", { id: foo }, null, 8 /* PROPS */, ["id"]),
+ (_cache[0] = _createElementVNode("div", { id: foo }, null, 8 /* PROPS */, ["id"])).cacheIndex = 0,
_setBlockTracking(1),
_cache[0]
)
@@ -29,7 +29,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
_cache[0] || (
_setBlockTracking(-1),
- _cache[0] = _createVNode(_component_Comp, { id: foo }, null, 8 /* PROPS */, ["id"]),
+ (_cache[0] = _createVNode(_component_Comp, { id: foo }, null, 8 /* PROPS */, ["id"])).cacheIndex = 0,
_setBlockTracking(1),
_cache[0]
)
@@ -48,7 +48,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
_cache[0] || (
_setBlockTracking(-1),
- _cache[0] = _createElementVNode("div", { id: foo }, null, 8 /* PROPS */, ["id"]),
+ (_cache[0] = _createElementVNode("div", { id: foo }, null, 8 /* PROPS */, ["id"])).cacheIndex = 0,
_setBlockTracking(1),
_cache[0]
)
@@ -67,7 +67,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
_cache[0] || (
_setBlockTracking(-1),
- _cache[0] = _renderSlot($slots, "default"),
+ (_cache[0] = _renderSlot($slots, "default")).cacheIndex = 0,
_setBlockTracking(1),
_cache[0]
)
@@ -86,7 +86,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
_cache[0] || (
_setBlockTracking(-1),
- _cache[0] = _createElementVNode("div"),
+ (_cache[0] = _createElementVNode("div")).cacheIndex = 0,
_setBlockTracking(1),
_cache[0]
)
diff --git a/packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts b/packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts
index d6c46b52eb3..d5a34243bca 100644
--- a/packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts
+++ b/packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts
@@ -21,7 +21,7 @@ import { transformIf } from '../../src/transforms/vIf'
import { transformFor } from '../../src/transforms/vFor'
import { transformBind } from '../../src/transforms/vBind'
import { transformOn } from '../../src/transforms/vOn'
-import { createObjectMatcher, genFlagText } from '../testUtils'
+import { createObjectMatcher } from '../testUtils'
import { transformText } from '../../src/transforms/transformText'
import { PatchFlags } from '@vue/shared'
@@ -180,7 +180,7 @@ describe('compiler: hoistStatic transform', () => {
id: `[foo]`,
}),
children: undefined,
- patchFlag: genFlagText(PatchFlags.PROPS),
+ patchFlag: PatchFlags.PROPS,
dynamicProps: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_hoisted_1`,
@@ -242,7 +242,7 @@ describe('compiler: hoistStatic transform', () => {
ref: `[foo]`,
}),
children: undefined,
- patchFlag: genFlagText(PatchFlags.NEED_PATCH),
+ patchFlag: PatchFlags.NEED_PATCH,
},
},
])
@@ -263,7 +263,7 @@ describe('compiler: hoistStatic transform', () => {
content: `_hoisted_1`,
},
children: undefined,
- patchFlag: genFlagText(PatchFlags.NEED_PATCH),
+ patchFlag: PatchFlags.NEED_PATCH,
directives: {
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
@@ -286,7 +286,7 @@ describe('compiler: hoistStatic transform', () => {
tag: `"div"`,
props: { content: `_hoisted_1` },
children: { type: NodeTypes.INTERPOLATION },
- patchFlag: genFlagText(PatchFlags.TEXT),
+ patchFlag: PatchFlags.TEXT,
},
},
])
@@ -365,7 +365,7 @@ describe('compiler: hoistStatic transform', () => {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: RENDER_LIST,
},
- patchFlag: genFlagText(PatchFlags.UNKEYED_FRAGMENT),
+ patchFlag: PatchFlags.UNKEYED_FRAGMENT,
})
const innerBlockCodegen = forBlockCodegen!.children.arguments[1]
expect(innerBlockCodegen.returns).toMatchObject({
@@ -496,7 +496,7 @@ describe('compiler: hoistStatic transform', () => {
constType: ConstantTypes.NOT_CONSTANT,
},
},
- patchFlag: `1 /* TEXT */`,
+ patchFlag: PatchFlags.TEXT,
},
},
],
diff --git a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts
index 10b9747d1ee..bf3510a052d 100644
--- a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts
+++ b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts
@@ -37,7 +37,7 @@ import { transformStyle } from '../../../compiler-dom/src/transforms/transformSt
import { transformOn } from '../../src/transforms/vOn'
import { transformBind } from '../../src/transforms/vBind'
import { PatchFlags } from '@vue/shared'
-import { createObjectMatcher, genFlagText } from '../testUtils'
+import { createObjectMatcher } from '../testUtils'
import { transformText } from '../../src/transforms/transformText'
import { parseWithForTransform } from './vFor.spec'
@@ -521,7 +521,7 @@ describe('compiler: element transform', () => {
// keep-alive should not compile content to slots
children: [{ type: NodeTypes.ELEMENT, tag: 'span' }],
// should get a dynamic slots flag to force updates
- patchFlag: genFlagText(PatchFlags.DYNAMIC_SLOTS),
+ patchFlag: PatchFlags.DYNAMIC_SLOTS,
})
}
@@ -588,7 +588,7 @@ describe('compiler: element transform', () => {
})
// should factor in props returned by custom directive transforms
// in patchFlag analysis
- expect(node.patchFlag).toMatch(PatchFlags.PROPS + '')
+ expect(node.patchFlag).toBe(PatchFlags.PROPS)
expect(node.dynamicProps).toMatch(`"bar"`)
})
@@ -612,7 +612,7 @@ describe('compiler: element transform', () => {
tag: `"div"`,
props: undefined,
children: undefined,
- patchFlag: genFlagText(PatchFlags.NEED_PATCH), // should generate appropriate flag
+ patchFlag: PatchFlags.NEED_PATCH, // should generate appropriate flag
directives: {
type: NodeTypes.JS_ARRAY_EXPRESSION,
elements: [
@@ -945,26 +945,26 @@ describe('compiler: element transform', () => {
expect(node.patchFlag).toBeUndefined()
const { node: node2 } = parseWithBind(`
{{ foo }}
`)
- expect(node2.patchFlag).toBe(genFlagText(PatchFlags.TEXT))
+ expect(node2.patchFlag).toBe(PatchFlags.TEXT)
// multiple nodes, merged with optimize text
const { node: node3 } = parseWithBind(`foo {{ bar }} baz
`)
- expect(node3.patchFlag).toBe(genFlagText(PatchFlags.TEXT))
+ expect(node3.patchFlag).toBe(PatchFlags.TEXT)
})
test('CLASS', () => {
const { node } = parseWithBind(``)
- expect(node.patchFlag).toBe(genFlagText(PatchFlags.CLASS))
+ expect(node.patchFlag).toBe(PatchFlags.CLASS)
})
test('STYLE', () => {
const { node } = parseWithBind(``)
- expect(node.patchFlag).toBe(genFlagText(PatchFlags.STYLE))
+ expect(node.patchFlag).toBe(PatchFlags.STYLE)
})
test('PROPS', () => {
const { node } = parseWithBind(``)
- expect(node.patchFlag).toBe(genFlagText(PatchFlags.PROPS))
+ expect(node.patchFlag).toBe(PatchFlags.PROPS)
expect(node.dynamicProps).toBe(`["foo", "baz"]`)
})
@@ -973,7 +973,7 @@ describe('compiler: element transform', () => {
``,
)
expect(node.patchFlag).toBe(
- genFlagText([PatchFlags.CLASS, PatchFlags.STYLE, PatchFlags.PROPS]),
+ PatchFlags.CLASS | PatchFlags.STYLE | PatchFlags.PROPS,
)
expect(node.dynamicProps).toBe(`["foo", "baz"]`)
})
@@ -983,40 +983,40 @@ describe('compiler: element transform', () => {
const { node } = parseWithBind(
``,
)
- expect(node.patchFlag).toBe(genFlagText(PatchFlags.PROPS))
+ expect(node.patchFlag).toBe(PatchFlags.PROPS)
expect(node.dynamicProps).toBe(`["id", "class", "style"]`)
})
test('FULL_PROPS (v-bind)', () => {
const { node } = parseWithBind(``)
- expect(node.patchFlag).toBe(genFlagText(PatchFlags.FULL_PROPS))
+ expect(node.patchFlag).toBe(PatchFlags.FULL_PROPS)
})
test('FULL_PROPS (dynamic key)', () => {
const { node } = parseWithBind(``)
- expect(node.patchFlag).toBe(genFlagText(PatchFlags.FULL_PROPS))
+ expect(node.patchFlag).toBe(PatchFlags.FULL_PROPS)
})
test('FULL_PROPS (w/ others)', () => {
const { node } = parseWithBind(
``,
)
- expect(node.patchFlag).toBe(genFlagText(PatchFlags.FULL_PROPS))
+ expect(node.patchFlag).toBe(PatchFlags.FULL_PROPS)
})
test('NEED_PATCH (static ref)', () => {
const { node } = parseWithBind(``)
- expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_PATCH))
+ expect(node.patchFlag).toBe(PatchFlags.NEED_PATCH)
})
test('NEED_PATCH (dynamic ref)', () => {
const { node } = parseWithBind(``)
- expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_PATCH))
+ expect(node.patchFlag).toBe(PatchFlags.NEED_PATCH)
})
test('NEED_PATCH (custom directives)', () => {
const { node } = parseWithBind(``)
- expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_PATCH))
+ expect(node.patchFlag).toBe(PatchFlags.NEED_PATCH)
})
test('NEED_PATCH (vnode hooks)', () => {
@@ -1025,7 +1025,7 @@ describe('compiler: element transform', () => {
cacheHandlers: true,
}).ast
const node = (root as any).children[0].codegenNode
- expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_PATCH))
+ expect(node.patchFlag).toBe(PatchFlags.NEED_PATCH)
})
test('script setup inline mode template ref (binding exists)', () => {
@@ -1120,7 +1120,7 @@ describe('compiler: element transform', () => {
},
})
// should only have props flag
- expect(node.patchFlag).toBe(genFlagText(PatchFlags.PROPS))
+ expect(node.patchFlag).toBe(PatchFlags.PROPS)
const { node: node2 } = parseWithElementTransform(
``,
@@ -1130,21 +1130,15 @@ describe('compiler: element transform', () => {
},
},
)
- expect(node2.patchFlag).toBe(
- genFlagText([PatchFlags.PROPS, PatchFlags.NEED_HYDRATION]),
- )
+ expect(node2.patchFlag).toBe(PatchFlags.PROPS | PatchFlags.NEED_HYDRATION)
})
test('NEED_HYDRATION for v-bind.prop', () => {
const { node } = parseWithBind(``)
- expect(node.patchFlag).toBe(
- genFlagText([PatchFlags.PROPS, PatchFlags.NEED_HYDRATION]),
- )
+ expect(node.patchFlag).toBe(PatchFlags.PROPS | PatchFlags.NEED_HYDRATION)
const { node: node2 } = parseWithBind(``)
- expect(node2.patchFlag).toBe(
- genFlagText([PatchFlags.PROPS, PatchFlags.NEED_HYDRATION]),
- )
+ expect(node2.patchFlag).toBe(PatchFlags.PROPS | PatchFlags.NEED_HYDRATION)
})
// #5870
@@ -1157,9 +1151,7 @@ describe('compiler: element transform', () => {
},
},
)
- expect(node.patchFlag).toBe(
- genFlagText([PatchFlags.PROPS, PatchFlags.NEED_HYDRATION]),
- )
+ expect(node.patchFlag).toBe(PatchFlags.PROPS | PatchFlags.NEED_HYDRATION)
})
test('should not have PROPS patchflag for constant v-on handlers', () => {
@@ -1173,7 +1165,7 @@ describe('compiler: element transform', () => {
},
})
// should only have hydration flag
- expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_HYDRATION))
+ expect(node.patchFlag).toBe(PatchFlags.NEED_HYDRATION)
})
})
diff --git a/packages/compiler-core/__tests__/transforms/vFor.spec.ts b/packages/compiler-core/__tests__/transforms/vFor.spec.ts
index 94f75f2a63b..d0e95fcbcb3 100644
--- a/packages/compiler-core/__tests__/transforms/vFor.spec.ts
+++ b/packages/compiler-core/__tests__/transforms/vFor.spec.ts
@@ -18,8 +18,8 @@ import {
import { ErrorCodes } from '../../src/errors'
import { type CompilerOptions, generate } from '../../src'
import { FRAGMENT, RENDER_LIST, RENDER_SLOT } from '../../src/runtimeHelpers'
-import { PatchFlagNames, PatchFlags } from '@vue/shared'
-import { createObjectMatcher, genFlagText } from '../testUtils'
+import { PatchFlags } from '@vue/shared'
+import { createObjectMatcher } from '../testUtils'
export function parseWithForTransform(
template: string,
@@ -696,10 +696,10 @@ describe('compiler: v-for', () => {
tag: FRAGMENT,
disableTracking,
patchFlag: !disableTracking
- ? genFlagText(PatchFlags.STABLE_FRAGMENT)
+ ? PatchFlags.STABLE_FRAGMENT
: keyed
- ? genFlagText(PatchFlags.KEYED_FRAGMENT)
- : genFlagText(PatchFlags.UNKEYED_FRAGMENT),
+ ? PatchFlags.KEYED_FRAGMENT
+ : PatchFlags.UNKEYED_FRAGMENT,
children: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: RENDER_LIST,
@@ -822,7 +822,7 @@ describe('compiler: v-for', () => {
constType: ConstantTypes.NOT_CONSTANT,
},
},
- patchFlag: genFlagText(PatchFlags.TEXT),
+ patchFlag: PatchFlags.TEXT,
},
})
expect(generate(root).code).toMatchSnapshot()
@@ -846,7 +846,7 @@ describe('compiler: v-for', () => {
{ type: NodeTypes.TEXT, content: `hello` },
{ type: NodeTypes.ELEMENT, tag: `span` },
],
- patchFlag: genFlagText(PatchFlags.STABLE_FRAGMENT),
+ patchFlag: PatchFlags.STABLE_FRAGMENT,
},
})
expect(generate(root).code).toMatchSnapshot()
@@ -950,7 +950,7 @@ describe('compiler: v-for', () => {
{ type: NodeTypes.TEXT, content: `hello` },
{ type: NodeTypes.ELEMENT, tag: `span` },
],
- patchFlag: genFlagText(PatchFlags.STABLE_FRAGMENT),
+ patchFlag: PatchFlags.STABLE_FRAGMENT,
},
})
expect(generate(root).code).toMatchSnapshot()
@@ -971,7 +971,7 @@ describe('compiler: v-for', () => {
}),
isBlock: true,
disableTracking: true,
- patchFlag: genFlagText(PatchFlags.UNKEYED_FRAGMENT),
+ patchFlag: PatchFlags.UNKEYED_FRAGMENT,
children: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: RENDER_LIST,
@@ -1009,7 +1009,7 @@ describe('compiler: v-for', () => {
}),
isBlock: true,
disableTracking: true,
- patchFlag: genFlagText(PatchFlags.UNKEYED_FRAGMENT),
+ patchFlag: PatchFlags.UNKEYED_FRAGMENT,
children: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: RENDER_LIST,
@@ -1048,9 +1048,7 @@ describe('compiler: v-for', () => {
const {
node: { codegenNode },
} = parseWithForTransform('test
')
- expect(codegenNode.patchFlag).toBe(
- `${PatchFlags.KEYED_FRAGMENT} /* ${PatchFlagNames[PatchFlags.KEYED_FRAGMENT]} */`,
- )
+ expect(codegenNode.patchFlag).toBe(PatchFlags.KEYED_FRAGMENT)
})
test('template v-for key w/ :key shorthand on template injected to the child', () => {
diff --git a/packages/compiler-core/__tests__/transforms/vSlot.spec.ts b/packages/compiler-core/__tests__/transforms/vSlot.spec.ts
index 3afcf0fc053..4766c2ca9d8 100644
--- a/packages/compiler-core/__tests__/transforms/vSlot.spec.ts
+++ b/packages/compiler-core/__tests__/transforms/vSlot.spec.ts
@@ -24,7 +24,7 @@ import {
trackVForSlotScopes,
} from '../../src/transforms/vSlot'
import { CREATE_SLOTS, RENDER_LIST } from '../../src/runtimeHelpers'
-import { createObjectMatcher, genFlagText } from '../testUtils'
+import { createObjectMatcher } from '../testUtils'
import { PatchFlags } from '@vue/shared'
import { transformFor } from '../../src/transforms/vFor'
import { transformIf } from '../../src/transforms/vIf'
@@ -432,7 +432,7 @@ describe('compiler: transform component slots', () => {
),
// nested slot should be forced dynamic, since scope variables
// are not tracked as dependencies of the slot.
- patchFlag: genFlagText(PatchFlags.DYNAMIC_SLOTS),
+ patchFlag: PatchFlags.DYNAMIC_SLOTS,
},
},
// test scope
@@ -474,9 +474,7 @@ describe('compiler: transform component slots', () => {
const div = ((root.children[0] as ForNode).children[0] as ElementNode)
.codegenNode as any
const comp = div.children[0]
- expect(comp.codegenNode.patchFlag).toBe(
- genFlagText(PatchFlags.DYNAMIC_SLOTS),
- )
+ expect(comp.codegenNode.patchFlag).toBe(PatchFlags.DYNAMIC_SLOTS)
})
test('should only force dynamic slots when actually using scope vars w/ prefixIdentifiers: true', () => {
@@ -494,7 +492,7 @@ describe('compiler: transform component slots', () => {
flag = (innerComp.codegenNode as VNodeCall).patchFlag
}
if (shouldForce) {
- expect(flag).toBe(genFlagText(PatchFlags.DYNAMIC_SLOTS))
+ expect(flag).toBe(PatchFlags.DYNAMIC_SLOTS)
} else {
expect(flag).toBeUndefined()
}
@@ -581,8 +579,8 @@ describe('compiler: transform component slots', () => {
},
],
})
- expect((root as any).children[0].codegenNode.patchFlag).toMatch(
- PatchFlags.DYNAMIC_SLOTS + '',
+ expect((root as any).children[0].codegenNode.patchFlag).toBe(
+ PatchFlags.DYNAMIC_SLOTS,
)
expect(generate(root).code).toMatchSnapshot()
})
@@ -630,8 +628,8 @@ describe('compiler: transform component slots', () => {
},
],
})
- expect((root as any).children[0].codegenNode.patchFlag).toMatch(
- PatchFlags.DYNAMIC_SLOTS + '',
+ expect((root as any).children[0].codegenNode.patchFlag).toBe(
+ PatchFlags.DYNAMIC_SLOTS,
)
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})
@@ -693,8 +691,8 @@ describe('compiler: transform component slots', () => {
},
],
})
- expect((root as any).children[0].codegenNode.patchFlag).toMatch(
- PatchFlags.DYNAMIC_SLOTS + '',
+ expect((root as any).children[0].codegenNode.patchFlag).toBe(
+ PatchFlags.DYNAMIC_SLOTS,
)
expect((root as any).children[0].children.length).toBe(3)
expect(generate(root).code).toMatchSnapshot()
@@ -744,8 +742,8 @@ describe('compiler: transform component slots', () => {
},
],
})
- expect((root as any).children[0].codegenNode.patchFlag).toMatch(
- PatchFlags.DYNAMIC_SLOTS + '',
+ expect((root as any).children[0].codegenNode.patchFlag).toBe(
+ PatchFlags.DYNAMIC_SLOTS,
)
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})
diff --git a/packages/compiler-core/package.json b/packages/compiler-core/package.json
index 42b8e9f5b79..f3202f80fb0 100644
--- a/packages/compiler-core/package.json
+++ b/packages/compiler-core/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue/compiler-core",
- "version": "3.4.31",
+ "version": "3.4.32",
"description": "@vue/compiler-core",
"main": "index.js",
"module": "dist/compiler-core.esm-bundler.js",
@@ -46,13 +46,13 @@
},
"homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-core#readme",
"dependencies": {
- "@babel/parser": "^7.24.7",
+ "@babel/parser": "catalog:",
"@vue/shared": "workspace:*",
"entities": "^4.5.0",
- "estree-walker": "^2.0.2",
- "source-map-js": "^1.2.0"
+ "estree-walker": "catalog:",
+ "source-map-js": "catalog:"
},
"devDependencies": {
- "@babel/types": "^7.24.7"
+ "@babel/types": "catalog:"
}
}
diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts
index 91354b1b40b..bbbb7f8a19d 100644
--- a/packages/compiler-core/src/ast.ts
+++ b/packages/compiler-core/src/ast.ts
@@ -1,4 +1,4 @@
-import { isString } from '@vue/shared'
+import { type PatchFlags, isString } from '@vue/shared'
import {
CREATE_BLOCK,
CREATE_ELEMENT_BLOCK,
@@ -331,7 +331,7 @@ export interface VNodeCall extends Node {
| ForRenderListExpression // v-for fragment call
| SimpleExpressionNode // hoisted
| undefined
- patchFlag: string | undefined
+ patchFlag: PatchFlags | undefined
dynamicProps: string | SimpleExpressionNode | undefined
directives: DirectiveArguments | undefined
isBlock: boolean
@@ -416,7 +416,7 @@ export interface CacheExpression extends Node {
type: NodeTypes.JS_CACHE_EXPRESSION
index: number
value: JSChildNode
- isVNode: boolean
+ isVOnce: boolean
}
export interface MemoExpression extends CallExpression {
@@ -561,7 +561,7 @@ export interface ForCodegenNode extends VNodeCall {
tag: typeof FRAGMENT
props: undefined
children: ForRenderListExpression
- patchFlag: string
+ patchFlag: PatchFlags
disableTracking: boolean
}
@@ -771,13 +771,13 @@ export function createConditionalExpression(
export function createCacheExpression(
index: number,
value: JSChildNode,
- isVNode: boolean = false,
+ isVOnce: boolean = false,
): CacheExpression {
return {
type: NodeTypes.JS_CACHE_EXPRESSION,
index,
value,
- isVNode,
+ isVOnce,
loc: locStub,
}
}
diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts
index 39170bac5a0..b6535c5cef9 100644
--- a/packages/compiler-core/src/codegen.ts
+++ b/packages/compiler-core/src/codegen.ts
@@ -35,7 +35,13 @@ import {
isSimpleIdentifier,
toValidAssetId,
} from './utils'
-import { isArray, isString, isSymbol } from '@vue/shared'
+import {
+ PatchFlagNames,
+ type PatchFlags,
+ isArray,
+ isString,
+ isSymbol,
+} from '@vue/shared'
import {
CREATE_COMMENT,
CREATE_ELEMENT_VNODE,
@@ -843,6 +849,28 @@ function genVNodeCall(node: VNodeCall, context: CodegenContext) {
disableTracking,
isComponent,
} = node
+
+ // add dev annotations to patch flags
+ let patchFlagString
+ if (patchFlag) {
+ if (__DEV__) {
+ if (patchFlag < 0) {
+ // special flags (negative and mutually exclusive)
+ patchFlagString = patchFlag + ` /* ${PatchFlagNames[patchFlag]} */`
+ } else {
+ // bitwise flags
+ const flagNames = Object.keys(PatchFlagNames)
+ .map(Number)
+ .filter(n => n > 0 && patchFlag & n)
+ .map(n => PatchFlagNames[n as PatchFlags])
+ .join(`, `)
+ patchFlagString = patchFlag + ` /* ${flagNames} */`
+ }
+ } else {
+ patchFlagString = String(patchFlag)
+ }
+ }
+
if (directives) {
push(helper(WITH_DIRECTIVES) + `(`)
}
@@ -857,7 +885,7 @@ function genVNodeCall(node: VNodeCall, context: CodegenContext) {
: getVNodeHelper(context.inSSR, isComponent)
push(helper(callHelper) + `(`, NewlineType.None, node)
genNodeList(
- genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
+ genNullableArgs([tag, props, children, patchFlagString, dynamicProps]),
context,
)
push(`)`)
@@ -1009,15 +1037,16 @@ function genConditionalExpression(
function genCacheExpression(node: CacheExpression, context: CodegenContext) {
const { push, helper, indent, deindent, newline } = context
push(`_cache[${node.index}] || (`)
- if (node.isVNode) {
+ if (node.isVOnce) {
indent()
push(`${helper(SET_BLOCK_TRACKING)}(-1),`)
newline()
+ push(`(`)
}
push(`_cache[${node.index}] = `)
genNode(node.value, context)
- if (node.isVNode) {
- push(`,`)
+ if (node.isVOnce) {
+ push(`).cacheIndex = ${node.index},`)
newline()
push(`${helper(SET_BLOCK_TRACKING)}(1),`)
newline()
diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts
index 69821f7f879..83ed8a92064 100644
--- a/packages/compiler-core/src/transform.ts
+++ b/packages/compiler-core/src/transform.ts
@@ -382,7 +382,7 @@ function createRootCodegen(root: RootNode, context: TransformContext) {
helper(FRAGMENT),
undefined,
root.children,
- patchFlag + (__DEV__ ? ` /* ${patchFlagText} */` : ``),
+ patchFlag,
undefined,
undefined,
true,
diff --git a/packages/compiler-core/src/transforms/hoistStatic.ts b/packages/compiler-core/src/transforms/hoistStatic.ts
index 67bdaa887bf..5942b73097b 100644
--- a/packages/compiler-core/src/transforms/hoistStatic.ts
+++ b/packages/compiler-core/src/transforms/hoistStatic.ts
@@ -70,8 +70,7 @@ function walk(
: getConstantType(child, context)
if (constantType > ConstantTypes.NOT_CONSTANT) {
if (constantType >= ConstantTypes.CAN_HOIST) {
- ;(child.codegenNode as VNodeCall).patchFlag =
- PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``)
+ ;(child.codegenNode as VNodeCall).patchFlag = PatchFlags.HOISTED
child.codegenNode = context.hoist(child.codegenNode!)
hoistedCount++
continue
@@ -81,9 +80,9 @@ function walk(
// hoisting.
const codegenNode = child.codegenNode!
if (codegenNode.type === NodeTypes.VNODE_CALL) {
- const flag = getPatchFlag(codegenNode)
+ const flag = codegenNode.patchFlag
if (
- (!flag ||
+ (flag === undefined ||
flag === PatchFlags.NEED_PATCH ||
flag === PatchFlags.TEXT) &&
getGeneratedPropsConstantType(child, context) >=
@@ -179,8 +178,7 @@ export function getConstantType(
) {
return ConstantTypes.NOT_CONSTANT
}
- const flag = getPatchFlag(codegenNode)
- if (!flag) {
+ if (codegenNode.patchFlag === undefined) {
let returnType = ConstantTypes.CAN_STRINGIFY
// Element itself has no patch flag. However we still need to check:
@@ -365,8 +363,3 @@ function getNodeProps(node: PlainElementNode) {
return codegenNode.props
}
}
-
-function getPatchFlag(node: VNodeCall): number | undefined {
- const flag = node.patchFlag
- return flag ? parseInt(flag, 10) : undefined
-}
diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts
index b1be06db703..ebaf08375b9 100644
--- a/packages/compiler-core/src/transforms/transformElement.ts
+++ b/packages/compiler-core/src/transforms/transformElement.ts
@@ -23,7 +23,6 @@ import {
createVNodeCall,
} from '../ast'
import {
- PatchFlagNames,
PatchFlags,
camelize,
capitalize,
@@ -101,8 +100,7 @@ export const transformElement: NodeTransform = (node, context) => {
let vnodeProps: VNodeCall['props']
let vnodeChildren: VNodeCall['children']
- let vnodePatchFlag: VNodeCall['patchFlag']
- let patchFlag: number = 0
+ let patchFlag: VNodeCall['patchFlag'] | 0 = 0
let vnodeDynamicProps: VNodeCall['dynamicProps']
let dynamicPropNames: string[] | undefined
let vnodeDirectives: VNodeCall['directives']
@@ -206,27 +204,8 @@ export const transformElement: NodeTransform = (node, context) => {
}
// patchFlag & dynamicPropNames
- if (patchFlag !== 0) {
- if (__DEV__) {
- if (patchFlag < 0) {
- // special flags (negative and mutually exclusive)
- vnodePatchFlag =
- patchFlag + ` /* ${PatchFlagNames[patchFlag as PatchFlags]} */`
- } else {
- // bitwise flags
- const flagNames = Object.keys(PatchFlagNames)
- .map(Number)
- .filter(n => n > 0 && patchFlag & n)
- .map(n => PatchFlagNames[n as PatchFlags])
- .join(`, `)
- vnodePatchFlag = patchFlag + ` /* ${flagNames} */`
- }
- } else {
- vnodePatchFlag = String(patchFlag)
- }
- if (dynamicPropNames && dynamicPropNames.length) {
- vnodeDynamicProps = stringifyDynamicPropNames(dynamicPropNames)
- }
+ if (dynamicPropNames && dynamicPropNames.length) {
+ vnodeDynamicProps = stringifyDynamicPropNames(dynamicPropNames)
}
node.codegenNode = createVNodeCall(
@@ -234,7 +213,7 @@ export const transformElement: NodeTransform = (node, context) => {
vnodeTag,
vnodeProps,
vnodeChildren,
- vnodePatchFlag,
+ patchFlag === 0 ? undefined : patchFlag,
vnodeDynamicProps,
vnodeDirectives,
!!shouldUseBlock,
diff --git a/packages/compiler-core/src/transforms/vFor.ts b/packages/compiler-core/src/transforms/vFor.ts
index 16c48ede067..0db961528e6 100644
--- a/packages/compiler-core/src/transforms/vFor.ts
+++ b/packages/compiler-core/src/transforms/vFor.ts
@@ -46,7 +46,7 @@ import {
} from '../runtimeHelpers'
import { processExpression } from './transformExpression'
import { validateBrowserExpression } from '../validateExpression'
-import { PatchFlagNames, PatchFlags } from '@vue/shared'
+import { PatchFlags } from '@vue/shared'
import { transformBindShorthand } from './vBind'
export const transformFor = createStructuralDirectiveTransform(
@@ -109,8 +109,7 @@ export const transformFor = createStructuralDirectiveTransform(
helper(FRAGMENT),
undefined,
renderExp,
- fragmentFlag +
- (__DEV__ ? ` /* ${PatchFlagNames[fragmentFlag]} */` : ``),
+ fragmentFlag,
undefined,
undefined,
true /* isBlock */,
@@ -169,10 +168,7 @@ export const transformFor = createStructuralDirectiveTransform(
helper(FRAGMENT),
keyProperty ? createObjectExpression([keyProperty]) : undefined,
node.children,
- PatchFlags.STABLE_FRAGMENT +
- (__DEV__
- ? ` /* ${PatchFlagNames[PatchFlags.STABLE_FRAGMENT]} */`
- : ``),
+ PatchFlags.STABLE_FRAGMENT,
undefined,
undefined,
true,
diff --git a/packages/compiler-core/src/transforms/vIf.ts b/packages/compiler-core/src/transforms/vIf.ts
index a6437f89c63..06255b25fdc 100644
--- a/packages/compiler-core/src/transforms/vIf.ts
+++ b/packages/compiler-core/src/transforms/vIf.ts
@@ -280,7 +280,7 @@ function createChildrenCodegenNode(
helper(FRAGMENT),
createObjectExpression([keyProperty]),
children,
- patchFlag + (__DEV__ ? ` /* ${patchFlagText} */` : ``),
+ patchFlag,
undefined,
undefined,
true,
diff --git a/packages/compiler-dom/__tests__/decoderHtmlBrowser.spec.ts b/packages/compiler-dom/__tests__/decoderHtmlBrowser.spec.ts
index ecdc070ad96..507d4243b88 100644
--- a/packages/compiler-dom/__tests__/decoderHtmlBrowser.spec.ts
+++ b/packages/compiler-dom/__tests__/decoderHtmlBrowser.spec.ts
@@ -40,5 +40,7 @@ describe('decodeHtmlBrowser', () => {
true,
),
).toBe('&')
+ expect(decodeHtmlBrowser('"', true)).toBe('"')
+ expect(decodeHtmlBrowser("'", true)).toBe("'")
})
})
diff --git a/packages/compiler-dom/__tests__/transforms/vHtml.spec.ts b/packages/compiler-dom/__tests__/transforms/vHtml.spec.ts
index a59a7a40aaf..bca2fdf9558 100644
--- a/packages/compiler-dom/__tests__/transforms/vHtml.spec.ts
+++ b/packages/compiler-dom/__tests__/transforms/vHtml.spec.ts
@@ -6,10 +6,7 @@ import {
} from '@vue/compiler-core'
import { transformVHtml } from '../../src/transforms/vHtml'
import { transformElement } from '../../../compiler-core/src/transforms/transformElement'
-import {
- createObjectMatcher,
- genFlagText,
-} from '../../../compiler-core/__tests__/testUtils'
+import { createObjectMatcher } from '../../../compiler-core/__tests__/testUtils'
import { PatchFlags } from '@vue/shared'
import { DOMErrorCodes } from '../../src/errors'
@@ -34,7 +31,7 @@ describe('compiler: v-html transform', () => {
innerHTML: `[test]`,
}),
children: undefined,
- patchFlag: genFlagText(PatchFlags.PROPS),
+ patchFlag: PatchFlags.PROPS,
dynamicProps: `["innerHTML"]`,
})
})
@@ -53,7 +50,7 @@ describe('compiler: v-html transform', () => {
innerHTML: `[test]`,
}),
children: undefined, // <-- children should have been removed
- patchFlag: genFlagText(PatchFlags.PROPS),
+ patchFlag: PatchFlags.PROPS,
dynamicProps: `["innerHTML"]`,
})
})
diff --git a/packages/compiler-dom/__tests__/transforms/vOn.spec.ts b/packages/compiler-dom/__tests__/transforms/vOn.spec.ts
index 2e80729119d..f53fbb69b07 100644
--- a/packages/compiler-dom/__tests__/transforms/vOn.spec.ts
+++ b/packages/compiler-dom/__tests__/transforms/vOn.spec.ts
@@ -14,7 +14,6 @@ import { transformOn } from '../../src/transforms/vOn'
import { V_ON_WITH_KEYS, V_ON_WITH_MODIFIERS } from '../../src/runtimeHelpers'
import { transformElement } from '../../../compiler-core/src/transforms/transformElement'
import { transformExpression } from '../../../compiler-core/src/transforms/transformExpression'
-import { genFlagText } from '../../../compiler-core/__tests__/testUtils'
import { PatchFlags } from '@vue/shared'
function parseWithVOn(template: string, options: CompilerOptions = {}) {
@@ -272,7 +271,7 @@ describe('compiler-dom: transform v-on', () => {
// should not treat cached handler as dynamicProp, so it should have no
// dynamicProps flags and only the hydration flag
expect((root as any).children[0].codegenNode.patchFlag).toBe(
- genFlagText(PatchFlags.NEED_HYDRATION),
+ PatchFlags.NEED_HYDRATION,
)
expect(prop).toMatchObject({
key: {
@@ -300,6 +299,6 @@ describe('compiler-dom: transform v-on', () => {
},
})
// should only have hydration flag
- expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_HYDRATION))
+ expect(node.patchFlag).toBe(PatchFlags.NEED_HYDRATION)
})
})
diff --git a/packages/compiler-dom/__tests__/transforms/vText.spec.ts b/packages/compiler-dom/__tests__/transforms/vText.spec.ts
index 1b717e83398..e96ab297298 100644
--- a/packages/compiler-dom/__tests__/transforms/vText.spec.ts
+++ b/packages/compiler-dom/__tests__/transforms/vText.spec.ts
@@ -6,10 +6,7 @@ import {
} from '@vue/compiler-core'
import { transformVText } from '../../src/transforms/vText'
import { transformElement } from '../../../compiler-core/src/transforms/transformElement'
-import {
- createObjectMatcher,
- genFlagText,
-} from '../../../compiler-core/__tests__/testUtils'
+import { createObjectMatcher } from '../../../compiler-core/__tests__/testUtils'
import { PatchFlags } from '@vue/shared'
import { DOMErrorCodes } from '../../src/errors'
@@ -36,7 +33,7 @@ describe('compiler: v-text transform', () => {
},
}),
children: undefined,
- patchFlag: genFlagText(PatchFlags.PROPS),
+ patchFlag: PatchFlags.PROPS,
dynamicProps: `["textContent"]`,
})
})
@@ -57,7 +54,7 @@ describe('compiler: v-text transform', () => {
},
}),
children: undefined, // <-- children should have been removed
- patchFlag: genFlagText(PatchFlags.PROPS),
+ patchFlag: PatchFlags.PROPS,
dynamicProps: `["textContent"]`,
})
})
diff --git a/packages/compiler-dom/package.json b/packages/compiler-dom/package.json
index b18785c7d74..131abe7b33b 100644
--- a/packages/compiler-dom/package.json
+++ b/packages/compiler-dom/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue/compiler-dom",
- "version": "3.4.31",
+ "version": "3.4.32",
"description": "@vue/compiler-dom",
"main": "index.js",
"module": "dist/compiler-dom.esm-bundler.js",
diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts
index 0b5549cc407..b8b71878e03 100644
--- a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts
+++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts
@@ -635,6 +635,26 @@ describe('resolveType', () => {
})
})
+ // #11266
+ test('correctly parse type annotation for declared function', () => {
+ const { props } = resolve(`
+ import { ExtractPropTypes } from 'vue'
+ interface UploadFile {
+ xhr?: T
+ }
+ declare function uploadProps(): {
+ fileList: {
+ type: PropType[]>
+ default: UploadFile[]
+ }
+ }
+ type UploadProps = ExtractPropTypes>
+ defineProps()`)
+ expect(props).toStrictEqual({
+ fileList: ['Array'],
+ })
+ })
+
describe('generics', () => {
test('generic with type literal', () => {
expect(
diff --git a/packages/compiler-sfc/package.json b/packages/compiler-sfc/package.json
index 22ee88edb57..0031aac7df1 100644
--- a/packages/compiler-sfc/package.json
+++ b/packages/compiler-sfc/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue/compiler-sfc",
- "version": "3.4.31",
+ "version": "3.4.32",
"description": "@vue/compiler-sfc",
"main": "dist/compiler-sfc.cjs.js",
"module": "dist/compiler-sfc.esm-browser.js",
@@ -42,26 +42,26 @@
},
"homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-sfc#readme",
"dependencies": {
- "@babel/parser": "^7.24.7",
+ "@babel/parser": "catalog:",
"@vue/compiler-core": "workspace:*",
"@vue/compiler-dom": "workspace:*",
"@vue/compiler-ssr": "workspace:*",
"@vue/shared": "workspace:*",
- "estree-walker": "^2.0.2",
- "magic-string": "^0.30.10",
- "postcss": "^8.4.38",
- "source-map-js": "^1.2.0"
+ "estree-walker": "catalog:",
+ "magic-string": "catalog:",
+ "postcss": "^8.4.39",
+ "source-map-js": "catalog:"
},
"devDependencies": {
- "@babel/types": "^7.24.7",
+ "@babel/types": "catalog:",
"@vue/consolidate": "^1.0.0",
"hash-sum": "^2.0.0",
"lru-cache": "10.1.0",
"merge-source-map": "^1.1.0",
- "minimatch": "^9.0.4",
+ "minimatch": "^9.0.5",
"postcss-modules": "^6.0.0",
"postcss-selector-parser": "^6.1.0",
"pug": "^3.0.3",
- "sass": "^1.77.6"
+ "sass": "^1.77.8"
}
}
diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts
index 5a4c84b4ab7..398d3ba9f42 100644
--- a/packages/compiler-sfc/src/script/resolveType.ts
+++ b/packages/compiler-sfc/src/script/resolveType.ts
@@ -177,6 +177,7 @@ function innerResolveTypeElements(
case 'TSInterfaceDeclaration':
return resolveInterfaceMembers(ctx, node, scope, typeParameters)
case 'TSTypeAliasDeclaration':
+ case 'TSTypeAnnotation':
case 'TSParenthesizedType':
return resolveTypeElements(
ctx,
diff --git a/packages/compiler-ssr/package.json b/packages/compiler-ssr/package.json
index a7b71e94cba..2e90c2f292a 100644
--- a/packages/compiler-ssr/package.json
+++ b/packages/compiler-ssr/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue/compiler-ssr",
- "version": "3.4.31",
+ "version": "3.4.32",
"description": "@vue/compiler-ssr",
"main": "dist/compiler-ssr.cjs.js",
"types": "dist/compiler-ssr.d.ts",
diff --git a/packages/dts-test/ref.test-d.ts b/packages/dts-test/ref.test-d.ts
index 5d4c9d95dab..1456c523239 100644
--- a/packages/dts-test/ref.test-d.ts
+++ b/packages/dts-test/ref.test-d.ts
@@ -452,3 +452,7 @@ describe('toRef <-> toValue', () => {
),
)
})
+
+// unref
+declare const text: ShallowRef | ComputedRef | MaybeRef
+expectType(unref(text))
diff --git a/packages/global.d.ts b/packages/global.d.ts
index 1bae0b929fb..79b55171384 100644
--- a/packages/global.d.ts
+++ b/packages/global.d.ts
@@ -19,15 +19,6 @@ declare var __FEATURE_PROD_DEVTOOLS__: boolean
declare var __FEATURE_SUSPENSE__: boolean
declare var __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__: boolean
-// for tests
-declare namespace jest {
- interface Matchers {
- toHaveBeenWarned(): R
- toHaveBeenWarnedLast(): R
- toHaveBeenWarnedTimes(n: number): R
- }
-}
-
declare module '*.vue' {}
declare module 'file-saver' {
diff --git a/packages/reactivity/__tests__/reactive.spec.ts b/packages/reactivity/__tests__/reactive.spec.ts
index bd4ec402bb2..1c25c49121b 100644
--- a/packages/reactivity/__tests__/reactive.spec.ts
+++ b/packages/reactivity/__tests__/reactive.spec.ts
@@ -2,6 +2,8 @@ import { isRef, ref } from '../src/ref'
import {
isProxy,
isReactive,
+ isReadonly,
+ isShallow,
markRaw,
reactive,
readonly,
@@ -359,4 +361,25 @@ describe('reactivity/reactive', () => {
const c = computed(() => {})
expect(isProxy(c)).toBe(false)
})
+
+ test('The results of the shallow and readonly assignments are the same (Map)', () => {
+ const map = reactive(new Map())
+ map.set('foo', shallowReactive({ a: 2 }))
+ expect(isShallow(map.get('foo'))).toBe(true)
+
+ map.set('bar', readonly({ b: 2 }))
+ expect(isReadonly(map.get('bar'))).toBe(true)
+ })
+
+ test('The results of the shallow and readonly assignments are the same (Set)', () => {
+ const set = reactive(new Set())
+ set.add(shallowReactive({ a: 2 }))
+ set.add(readonly({ b: 2 }))
+ let count = 0
+ for (const i of set) {
+ if (count === 0) expect(isShallow(i)).toBe(true)
+ else expect(isReadonly(i)).toBe(true)
+ count++
+ }
+ })
})
diff --git a/packages/reactivity/__tests__/shallowReactive.spec.ts b/packages/reactivity/__tests__/shallowReactive.spec.ts
index e9b64d39b36..cb5ef8e6d97 100644
--- a/packages/reactivity/__tests__/shallowReactive.spec.ts
+++ b/packages/reactivity/__tests__/shallowReactive.spec.ts
@@ -123,6 +123,29 @@ describe('shallowReactive', () => {
shallowSet.forEach(x => expect(isReactive(x)).toBe(false))
})
+ test('Setting a reactive object on a shallowReactive map', () => {
+ const msg = ref('ads')
+ const bar = reactive({ msg })
+ const foo = shallowReactive(new Map([['foo1', bar]]))
+ foo.set('foo2', bar)
+
+ expect(isReactive(foo.get('foo2'))).toBe(true)
+ expect(isReactive(foo.get('foo1'))).toBe(true)
+ })
+
+ test('Setting a reactive object on a shallowReactive set', () => {
+ const msg = ref(1)
+ const bar = reactive({ msg })
+ const foo = reactive({ msg })
+
+ const deps = shallowReactive(new Set([bar]))
+ deps.add(foo)
+
+ deps.forEach(dep => {
+ expect(isReactive(dep)).toBe(true)
+ })
+ })
+
// #1210
test('onTrack on called on objectSpread', () => {
const onTrackFn = vi.fn()
diff --git a/packages/reactivity/package.json b/packages/reactivity/package.json
index 8f989994260..bbea042b9cb 100644
--- a/packages/reactivity/package.json
+++ b/packages/reactivity/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue/reactivity",
- "version": "3.4.31",
+ "version": "3.4.32",
"description": "@vue/reactivity",
"main": "index.js",
"module": "dist/reactivity.esm-bundler.js",
diff --git a/packages/reactivity/src/collectionHandlers.ts b/packages/reactivity/src/collectionHandlers.ts
index 7c4b36fbe94..c39e3ed48cd 100644
--- a/packages/reactivity/src/collectionHandlers.ts
+++ b/packages/reactivity/src/collectionHandlers.ts
@@ -1,4 +1,10 @@
-import { toRaw, toReactive, toReadonly } from './reactive'
+import {
+ isReadonly,
+ isShallow,
+ toRaw,
+ toReactive,
+ toReadonly,
+} from './reactive'
import {
ITERATE_KEY,
MAP_KEY_ITERATE_KEY,
@@ -72,8 +78,10 @@ function size(target: IterableCollections, isReadonly = false) {
return Reflect.get(target, 'size', target)
}
-function add(this: SetTypes, value: unknown) {
- value = toRaw(value)
+function add(this: SetTypes, value: unknown, _isShallow = false) {
+ if (!_isShallow && !isShallow(value) && !isReadonly(value)) {
+ value = toRaw(value)
+ }
const target = toRaw(this)
const proto = getProto(target)
const hadKey = proto.has.call(target, value)
@@ -84,8 +92,10 @@ function add(this: SetTypes, value: unknown) {
return this
}
-function set(this: MapTypes, key: unknown, value: unknown) {
- value = toRaw(value)
+function set(this: MapTypes, key: unknown, value: unknown, _isShallow = false) {
+ if (!_isShallow && !isShallow(value) && !isReadonly(value)) {
+ value = toRaw(value)
+ }
const target = toRaw(this)
const { has, get } = getProto(target)
@@ -263,8 +273,12 @@ function createInstrumentations() {
return size(this as unknown as IterableCollections)
},
has,
- add,
- set,
+ add(this: SetTypes, value: unknown) {
+ return add.call(this, value, true)
+ },
+ set(this: MapTypes, key: unknown, value: unknown) {
+ return set.call(this, key, value, true)
+ },
delete: deleteEntry,
clear,
forEach: createForEach(false, true),
diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts
index e47b8aa5582..3e9b05062f3 100644
--- a/packages/reactivity/src/ref.ts
+++ b/packages/reactivity/src/ref.ts
@@ -235,7 +235,7 @@ export type MaybeRefOrGetter = MaybeRef | (() => T)
* @param ref - Ref or plain value to be converted into the plain value.
* @see {@link https://vuejs.org/api/reactivity-utilities.html#unref}
*/
-export function unref(ref: MaybeRef | ComputedRef): T {
+export function unref(ref: MaybeRef | ComputedRef | ShallowRef): T {
return isRef(ref) ? ref.value : ref
}
@@ -255,7 +255,9 @@ export function unref(ref: MaybeRef | ComputedRef): T {
* @param source - A getter, an existing ref, or a non-function value.
* @see {@link https://vuejs.org/api/reactivity-utilities.html#tovalue}
*/
-export function toValue(source: MaybeRefOrGetter | ComputedRef): T {
+export function toValue(
+ source: MaybeRefOrGetter | ComputedRef | ShallowRef,
+): T {
return isFunction(source) ? source() : unref(source)
}
diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts
index febfd462d06..82630c46377 100644
--- a/packages/runtime-core/__tests__/apiWatch.spec.ts
+++ b/packages/runtime-core/__tests__/apiWatch.spec.ts
@@ -5,6 +5,7 @@ import {
defineComponent,
getCurrentInstance,
nextTick,
+ onErrorCaptured,
reactive,
ref,
watch,
@@ -1576,4 +1577,60 @@ describe('api: watch', () => {
expect(spy).toHaveBeenCalledTimes(1)
expect(foo.value.a).toBe(2)
})
+
+ test('watch immediate error in effect scope should be catched by onErrorCaptured', async () => {
+ const warn = vi.spyOn(console, 'warn')
+ warn.mockImplementation(() => {})
+ const ERROR_IN_SCOPE = 'ERROR_IN_SCOPE'
+ const ERROR_OUT_SCOPE = 'ERROR_OUT_SCOPE'
+
+ const errors = ref([])
+ const Comp = {
+ setup() {
+ const trigger = ref(0)
+
+ effectScope(true).run(() => {
+ watch(
+ trigger,
+ () => {
+ throw new Error(ERROR_IN_SCOPE)
+ },
+ { immediate: true },
+ )
+ })
+
+ watchEffect(() => {
+ throw new Error(ERROR_OUT_SCOPE)
+ })
+
+ return () => ''
+ },
+ }
+
+ const root = nodeOps.createElement('div')
+ render(
+ h(
+ {
+ setup(_, { slots }) {
+ onErrorCaptured(e => {
+ errors.value.push(e.message)
+ return false
+ })
+
+ return () => h('div', slots.default && slots.default())
+ },
+ },
+ null,
+ () => [h(Comp)],
+ ),
+ root,
+ )
+ await nextTick()
+ // only watchEffect as ran so far
+ expect(errors.value).toHaveLength(2)
+ expect(errors.value[0]).toBe(ERROR_IN_SCOPE)
+ expect(errors.value[1]).toBe(ERROR_OUT_SCOPE)
+
+ warn.mockRestore()
+ })
})
diff --git a/packages/runtime-core/__tests__/componentEmits.spec.ts b/packages/runtime-core/__tests__/componentEmits.spec.ts
index 86307275bf8..e2e9044a1b4 100644
--- a/packages/runtime-core/__tests__/componentEmits.spec.ts
+++ b/packages/runtime-core/__tests__/componentEmits.spec.ts
@@ -356,6 +356,83 @@ describe('component: emit', () => {
expect(fn2).toHaveBeenCalledWith('two')
})
+ test('.trim modifier should work with v-model on component for kebab-cased props and camelCased emit', () => {
+ const Foo = defineComponent({
+ render() {},
+ created() {
+ this.$emit('update:firstName', ' one ')
+ },
+ })
+
+ const fn1 = vi.fn()
+
+ const Comp = () =>
+ h(Foo, {
+ 'first-name': null,
+ 'first-nameModifiers': { trim: true },
+ 'onUpdate:first-name': fn1,
+ })
+
+ render(h(Comp), nodeOps.createElement('div'))
+
+ expect(fn1).toHaveBeenCalledTimes(1)
+ expect(fn1).toHaveBeenCalledWith('one')
+ })
+
+ test('.trim modifier should work with v-model on component for camelCased props and kebab-cased emit', () => {
+ const Foo = defineComponent({
+ render() {},
+ created() {
+ this.$emit('update:model-value', ' one ')
+ this.$emit('update:first-name', ' two ')
+ },
+ })
+
+ const fn1 = vi.fn()
+ const fn2 = vi.fn()
+
+ const Comp = () =>
+ h(Foo, {
+ modelValue: null,
+ modelModifiers: { trim: true },
+ 'onUpdate:modelValue': fn1,
+
+ firstName: null,
+ firstNameModifiers: { trim: true },
+ 'onUpdate:firstName': fn2,
+ })
+
+ render(h(Comp), nodeOps.createElement('div'))
+
+ expect(fn1).toHaveBeenCalledTimes(1)
+ expect(fn1).toHaveBeenCalledWith('one')
+ expect(fn2).toHaveBeenCalledTimes(1)
+ expect(fn2).toHaveBeenCalledWith('two')
+ })
+
+ test('.trim modifier should work with v-model on component for mixed cased props and emit', () => {
+ const Foo = defineComponent({
+ render() {},
+ created() {
+ this.$emit('update:base-URL', ' one ')
+ },
+ })
+
+ const fn1 = vi.fn()
+
+ const Comp = () =>
+ h(Foo, {
+ 'base-URL': null,
+ 'base-URLModifiers': { trim: true },
+ 'onUpdate:base-URL': fn1,
+ })
+
+ render(h(Comp), nodeOps.createElement('div'))
+
+ expect(fn1).toHaveBeenCalledTimes(1)
+ expect(fn1).toHaveBeenCalledWith('one')
+ })
+
test('.trim and .number modifiers should work with v-model on component', () => {
const Foo = defineComponent({
render() {},
diff --git a/packages/runtime-core/__tests__/componentProps.spec.ts b/packages/runtime-core/__tests__/componentProps.spec.ts
index 8244580005b..8c9c38b3c1f 100644
--- a/packages/runtime-core/__tests__/componentProps.spec.ts
+++ b/packages/runtime-core/__tests__/componentProps.spec.ts
@@ -538,6 +538,96 @@ describe('component props', () => {
expect(renderProxy.$props).toMatchObject(props)
})
+ test('merging props from global mixins and extends', () => {
+ let renderProxy: any
+ let extendedRenderProxy: any
+
+ const defaultProp = ' from global'
+ const props = {
+ globalProp: {
+ type: String,
+ default: defaultProp,
+ },
+ }
+ const globalMixin = {
+ props,
+ }
+ const Comp = {
+ render(this: any) {
+ renderProxy = this
+ return h('div', ['Comp', this.globalProp])
+ },
+ }
+ const ExtendedComp = {
+ extends: Comp,
+ render(this: any) {
+ extendedRenderProxy = this
+ return h('div', ['ExtendedComp', this.globalProp])
+ },
+ }
+
+ const app = createApp(
+ {
+ render: () => [h(ExtendedComp), h(Comp)],
+ },
+ {},
+ )
+ app.mixin(globalMixin)
+
+ const root = nodeOps.createElement('div')
+ app.mount(root)
+
+ expect(serializeInner(root)).toMatch(
+ `ExtendedComp from global
Comp from global
`,
+ )
+ expect(renderProxy.$props).toMatchObject({ globalProp: defaultProp })
+ expect(extendedRenderProxy.$props).toMatchObject({
+ globalProp: defaultProp,
+ })
+ })
+
+ test('merging props for a component that is also used as a mixin', () => {
+ const CompA = {
+ render(this: any) {
+ return this.foo
+ },
+ }
+
+ const mixin = {
+ props: {
+ foo: {
+ default: 'from mixin',
+ },
+ },
+ }
+
+ const CompB = {
+ mixins: [mixin, CompA],
+ render(this: any) {
+ return this.foo
+ },
+ }
+
+ const app = createApp({
+ render() {
+ return [h(CompA), ', ', h(CompB)]
+ },
+ })
+
+ app.mixin({
+ props: {
+ foo: {
+ default: 'from global mixin',
+ },
+ },
+ })
+
+ const root = nodeOps.createElement('div')
+ app.mount(root)
+
+ expect(serializeInner(root)).toMatch(`from global mixin, from mixin`)
+ })
+
test('props type support BigInt', () => {
const Comp = {
props: {
diff --git a/packages/runtime-core/__tests__/componentSlots.spec.ts b/packages/runtime-core/__tests__/componentSlots.spec.ts
index 09b37932147..6042ccbd734 100644
--- a/packages/runtime-core/__tests__/componentSlots.spec.ts
+++ b/packages/runtime-core/__tests__/componentSlots.spec.ts
@@ -7,7 +7,7 @@ import {
ref,
render,
} from '@vue/runtime-test'
-import { normalizeVNode } from '../src/vnode'
+import { createBlock, normalizeVNode } from '../src/vnode'
import { createSlots } from '../src/helpers/createSlots'
describe('component: slots', () => {
@@ -25,8 +25,21 @@ describe('component: slots', () => {
}
test('initSlots: instance.slots should be set correctly', () => {
+ let instance: any
+ const Comp = {
+ render() {
+ instance = getCurrentInstance()
+ return h('div')
+ },
+ }
+ const slots = { foo: () => {}, _: 1 }
+ render(createBlock(Comp, null, slots), nodeOps.createElement('div'))
+ expect(instance.slots).toMatchObject(slots)
+ })
+
+ test('initSlots: instance.slots should remove compiler marker if parent is using manual render function', () => {
const { slots } = renderWithSlots({ _: 1 })
- expect(slots).toMatchObject({ _: 1 })
+ expect(slots).toMatchObject({})
})
test('initSlots: should normalize object slots (when value is null, string, array)', () => {
diff --git a/packages/runtime-core/__tests__/components/Teleport.spec.ts b/packages/runtime-core/__tests__/components/Teleport.spec.ts
index 9c85cd8beb6..aca9432b6e1 100644
--- a/packages/runtime-core/__tests__/components/Teleport.spec.ts
+++ b/packages/runtime-core/__tests__/components/Teleport.spec.ts
@@ -16,7 +16,7 @@ import {
serializeInner,
withDirectives,
} from '@vue/runtime-test'
-import { Fragment, createVNode } from '../../src/vnode'
+import { Fragment, createCommentVNode, createVNode } from '../../src/vnode'
import { compile, render as domRender } from 'vue'
describe('renderer: teleport', () => {
@@ -553,4 +553,71 @@ describe('renderer: teleport', () => {
`"teleported
"`,
)
})
+
+ //#9071
+ test('toggle sibling node inside target node', async () => {
+ const root = document.createElement('div')
+ const show = ref(false)
+ const App = defineComponent({
+ setup() {
+ return () => {
+ return show.value
+ ? h(Teleport, { to: root }, [h('div', 'teleported')])
+ : h('div', 'foo')
+ }
+ },
+ })
+
+ domRender(h(App), root)
+ expect(root.innerHTML).toMatchInlineSnapshot('"foo
"')
+
+ show.value = true
+ await nextTick()
+
+ expect(root.innerHTML).toMatchInlineSnapshot(
+ '"teleported
"',
+ )
+
+ show.value = false
+ await nextTick()
+
+ expect(root.innerHTML).toMatchInlineSnapshot('"foo
"')
+ })
+
+ test('unmount previous sibling node inside target node', async () => {
+ const root = document.createElement('div')
+ const parentShow = ref(false)
+ const childShow = ref(true)
+
+ const Comp = {
+ setup() {
+ return () => h(Teleport, { to: root }, [h('div', 'foo')])
+ },
+ }
+
+ const App = defineComponent({
+ setup() {
+ return () => {
+ return parentShow.value
+ ? h(Fragment, { key: 0 }, [
+ childShow.value ? h(Comp) : createCommentVNode('v-if'),
+ ])
+ : createCommentVNode('v-if')
+ }
+ },
+ })
+
+ domRender(h(App), root)
+ expect(root.innerHTML).toMatchInlineSnapshot('""')
+
+ parentShow.value = true
+ await nextTick()
+ expect(root.innerHTML).toMatchInlineSnapshot(
+ '"foo
"',
+ )
+
+ parentShow.value = false
+ await nextTick()
+ expect(root.innerHTML).toMatchInlineSnapshot('""')
+ })
})
diff --git a/packages/runtime-core/__tests__/errorHandling.spec.ts b/packages/runtime-core/__tests__/errorHandling.spec.ts
index 085127677ba..4cf4ffe3126 100644
--- a/packages/runtime-core/__tests__/errorHandling.spec.ts
+++ b/packages/runtime-core/__tests__/errorHandling.spec.ts
@@ -1,4 +1,6 @@
import {
+ type VNode,
+ computed,
createApp,
defineComponent,
h,
@@ -11,6 +13,7 @@ import {
watch,
watchEffect,
} from '@vue/runtime-test'
+import { ErrorCodes, ErrorTypeStrings } from '../src/errorHandling'
describe('error handling', () => {
test('propagation', () => {
@@ -609,5 +612,63 @@ describe('error handling', () => {
expect(handler).toHaveBeenCalledTimes(1)
})
+ test('errors in scheduler job with owner instance should be caught', async () => {
+ let vnode: VNode
+ const x = ref(0)
+ const app = createApp({
+ render() {
+ return (vnode = vnode || h('div', x.value))
+ },
+ })
+
+ app.config.errorHandler = vi.fn()
+ app.mount(nodeOps.createElement('div'))
+
+ const error = new Error('error')
+ Object.defineProperty(vnode!, 'el', {
+ get() {
+ throw error
+ },
+ })
+
+ x.value++
+ await nextTick()
+ expect(app.config.errorHandler).toHaveBeenCalledWith(
+ error,
+ {},
+ ErrorTypeStrings[ErrorCodes.COMPONENT_UPDATE],
+ )
+ })
+
+ // #11286
+ test('handle error in computed', async () => {
+ const err = new Error()
+ const handler = vi.fn()
+
+ const count = ref(1)
+ const x = computed(() => {
+ if (count.value === 2) throw err
+ return count.value + 1
+ })
+
+ const app = createApp({
+ setup() {
+ return () => x.value
+ },
+ })
+
+ app.config.errorHandler = handler
+ app.mount(nodeOps.createElement('div'))
+
+ count.value = 2
+
+ await nextTick()
+ expect(handler).toHaveBeenCalledWith(
+ err,
+ {},
+ ErrorTypeStrings[ErrorCodes.COMPONENT_UPDATE],
+ )
+ })
+
// native event handler handling should be tested in respective renderers
})
diff --git a/packages/runtime-core/__tests__/helpers/renderSlot.spec.ts b/packages/runtime-core/__tests__/helpers/renderSlot.spec.ts
index b6183ac3c36..c4ae077ba34 100644
--- a/packages/runtime-core/__tests__/helpers/renderSlot.spec.ts
+++ b/packages/runtime-core/__tests__/helpers/renderSlot.spec.ts
@@ -26,13 +26,17 @@ describe('renderSlot', () => {
const vnode = renderSlot(
{ default: () => [(child = h('child'))] },
'default',
+ { key: 'foo' },
)
expect(vnode.children).toEqual([child])
+ expect(vnode.key).toBe('foo')
})
it('should render slot fallback', () => {
- const vnode = renderSlot({}, 'default', {}, () => ['fallback'])
+ const vnode = renderSlot({}, 'default', { key: 'foo' }, () => ['fallback'])
expect(vnode.children).toEqual(['fallback'])
+ // should attach fallback key postfix
+ expect(vnode.key).toBe('foo_fb')
})
it('should warn render ssr slot', () => {
diff --git a/packages/runtime-core/__tests__/helpers/useModel.spec.ts b/packages/runtime-core/__tests__/helpers/useModel.spec.ts
index c02af337b87..097e52f9167 100644
--- a/packages/runtime-core/__tests__/helpers/useModel.spec.ts
+++ b/packages/runtime-core/__tests__/helpers/useModel.spec.ts
@@ -1,6 +1,7 @@
import {
Fragment,
type Ref,
+ type TestElement,
createApp,
createBlock,
createElementBlock,
@@ -526,4 +527,129 @@ describe('useModel', () => {
await nextTick()
expect(msg.value).toBe('UGHH')
})
+
+ // #10279
+ test('force local update when setter formats value to the same value', async () => {
+ let childMsg: Ref
+ let childModifiers: Record
+
+ const compRender = vi.fn()
+ const parentRender = vi.fn()
+
+ const Comp = defineComponent({
+ props: ['msg', 'msgModifiers'],
+ emits: ['update:msg'],
+ setup(props) {
+ ;[childMsg, childModifiers] = useModel(props, 'msg', {
+ set(val) {
+ if (childModifiers.number) {
+ return val.replace(/\D+/g, '')
+ }
+ },
+ })
+ return () => {
+ compRender()
+ return h('input', {
+ // simulate how v-model works
+ onVnodeBeforeMount(vnode) {
+ ;(vnode.el as TestElement).props.value = childMsg.value
+ },
+ onVnodeBeforeUpdate(vnode) {
+ ;(vnode.el as TestElement).props.value = childMsg.value
+ },
+ onInput(value: any) {
+ childMsg.value = value
+ },
+ })
+ }
+ },
+ })
+
+ const msg = ref(1)
+ const Parent = defineComponent({
+ setup() {
+ return () => {
+ parentRender()
+ return h(Comp, {
+ msg: msg.value,
+ msgModifiers: { number: true },
+ 'onUpdate:msg': val => {
+ msg.value = val
+ },
+ })
+ }
+ },
+ })
+
+ const root = nodeOps.createElement('div')
+ render(h(Parent), root)
+
+ expect(parentRender).toHaveBeenCalledTimes(1)
+ expect(compRender).toHaveBeenCalledTimes(1)
+ expect(serializeInner(root)).toBe('')
+
+ const input = root.children[0] as TestElement
+
+ // simulate v-model update
+ input.props.onInput((input.props.value = '2'))
+ await nextTick()
+ expect(msg.value).toBe(2)
+ expect(parentRender).toHaveBeenCalledTimes(2)
+ expect(compRender).toHaveBeenCalledTimes(2)
+ expect(serializeInner(root)).toBe('')
+
+ input.props.onInput((input.props.value = '2a'))
+ await nextTick()
+ expect(msg.value).toBe(2)
+ expect(parentRender).toHaveBeenCalledTimes(2)
+ // should force local update
+ expect(compRender).toHaveBeenCalledTimes(3)
+ expect(serializeInner(root)).toBe('')
+
+ input.props.onInput((input.props.value = '2a'))
+ await nextTick()
+ expect(parentRender).toHaveBeenCalledTimes(2)
+ // should not force local update if set to the same value
+ expect(compRender).toHaveBeenCalledTimes(3)
+ })
+
+ test('set no change value', async () => {
+ let changeChildMsg: (() => void) | null = null
+
+ const compRender = vi.fn()
+ const Comp = defineComponent({
+ props: ['msg'],
+ emits: ['update:msg'],
+ setup(props) {
+ const childMsg = useModel(props, 'msg')
+ changeChildMsg = () => {
+ childMsg.value = childMsg.value
+ }
+ return () => {
+ return childMsg.value
+ }
+ },
+ })
+
+ const msg = ref('HI')
+ const Parent = defineComponent({
+ setup() {
+ return () =>
+ h(Comp, {
+ msg: msg.value,
+ 'onUpdate:msg': val => {
+ msg.value = val
+ compRender()
+ },
+ })
+ },
+ })
+
+ const root = nodeOps.createElement('div')
+ render(h(Parent), root)
+
+ expect(compRender).toBeCalledTimes(0)
+ changeChildMsg!()
+ expect(compRender).toBeCalledTimes(0)
+ })
})
diff --git a/packages/runtime-core/__tests__/hmr.spec.ts b/packages/runtime-core/__tests__/hmr.spec.ts
index 39aece16a5a..ba9c7c0780b 100644
--- a/packages/runtime-core/__tests__/hmr.spec.ts
+++ b/packages/runtime-core/__tests__/hmr.spec.ts
@@ -29,6 +29,8 @@ function compileToFunction(template: string) {
return render
}
+const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n))
+
describe('hot module replacement', () => {
test('inject global runtime', () => {
expect(createRecord).toBeDefined()
@@ -436,18 +438,23 @@ describe('hot module replacement', () => {
const Parent: ComponentOptions = {
setup() {
- const com = ref()
- const changeRef = (value: any) => {
- com.value = value
- }
+ const com1 = ref()
+ const changeRef1 = (value: any) => (com1.value = value)
+
+ const com2 = ref()
+ const changeRef2 = (value: any) => (com2.value = value)
- return () => [h(Child, { ref: changeRef }), com.value?.count]
+ return () => [
+ h(Child, { ref: changeRef1 }),
+ h(Child, { ref: changeRef2 }),
+ com1.value?.count,
+ ]
},
}
render(h(Parent), root)
await nextTick()
- expect(serializeInner(root)).toBe(`0
0`)
+ expect(serializeInner(root)).toBe(`0
0
0`)
reload(childId, {
__hmrId: childId,
@@ -458,9 +465,9 @@ describe('hot module replacement', () => {
render: compileToFunction(`{{ count }}
`),
})
await nextTick()
- expect(serializeInner(root)).toBe(`1
1`)
- expect(unmountSpy).toHaveBeenCalledTimes(1)
- expect(mountSpy).toHaveBeenCalledTimes(1)
+ expect(serializeInner(root)).toBe(`1
1
1`)
+ expect(unmountSpy).toHaveBeenCalledTimes(2)
+ expect(mountSpy).toHaveBeenCalledTimes(2)
})
// #1156 - static nodes should retain DOM element reference across updates
@@ -805,4 +812,43 @@ describe('hot module replacement', () => {
`2
`,
)
})
+
+ // #11248
+ test('reload async component with multiple instances', async () => {
+ const root = nodeOps.createElement('div')
+ const childId = 'test-child-id'
+ const Child: ComponentOptions = {
+ __hmrId: childId,
+ data() {
+ return { count: 0 }
+ },
+ render: compileToFunction(`{{ count }}
`),
+ }
+ const Comp = runtimeTest.defineAsyncComponent(() => Promise.resolve(Child))
+ const appId = 'test-app-id'
+ const App: ComponentOptions = {
+ __hmrId: appId,
+ render: () => [h(Comp), h(Comp)],
+ }
+ createRecord(appId, App)
+
+ render(h(App), root)
+
+ await timeout()
+
+ expect(serializeInner(root)).toBe(`0
0
`)
+
+ // change count to 1
+ reload(childId, {
+ __hmrId: childId,
+ data() {
+ return { count: 1 }
+ },
+ render: compileToFunction(`{{ count }}
`),
+ })
+
+ await timeout()
+
+ expect(serializeInner(root)).toBe(`1
1
`)
+ })
})
diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts
index 53974bc9384..ea1d626f7c4 100644
--- a/packages/runtime-core/__tests__/hydration.spec.ts
+++ b/packages/runtime-core/__tests__/hydration.spec.ts
@@ -22,6 +22,7 @@ import {
nextTick,
onMounted,
openBlock,
+ reactive,
ref,
renderSlot,
useCssVars,
@@ -31,7 +32,7 @@ import {
withDirectives,
} from '@vue/runtime-dom'
import { type SSRContext, renderToString } from '@vue/server-renderer'
-import { PatchFlags } from '@vue/shared'
+import { PatchFlags, normalizeStyle } from '@vue/shared'
import { vShowOriginalDisplay } from '../../runtime-dom/src/directives/vShow'
import { expect } from 'vitest'
@@ -148,6 +149,15 @@ describe('SSR hydration', () => {
expect(container.innerHTML).toBe(`bar
`)
})
+ // #7285
+ test('element with multiple continuous text vnodes', async () => {
+ // should no mismatch warning
+ const { container } = mountWithHydration('fooo
', () =>
+ h('div', ['fo', createTextVNode('o'), 'o']),
+ )
+ expect(container.textContent).toBe('fooo')
+ })
+
test('element with elements children', async () => {
const msg = ref('foo')
const fn = vi.fn()
@@ -239,6 +249,17 @@ describe('SSR hydration', () => {
)
})
+ // #7285
+ test('Fragment (multiple continuous text vnodes)', async () => {
+ // should no mismatch warning
+ const { container } = mountWithHydration('fooo', () => [
+ 'fo',
+ createTextVNode('o'),
+ 'o',
+ ])
+ expect(container.textContent).toBe('fooo')
+ })
+
test('Teleport', async () => {
const msg = ref('foo')
const fn = vi.fn()
@@ -1176,6 +1197,38 @@ describe('SSR hydration', () => {
expect(text.nodeType).toBe(3)
})
+ // #11372
+ test('object style value tracking in prod', async () => {
+ __DEV__ = false
+ try {
+ const style = reactive({ color: 'red' })
+ const Comp = {
+ render(this: any) {
+ return (
+ openBlock(),
+ createElementBlock(
+ 'div',
+ {
+ style: normalizeStyle(style),
+ },
+ null,
+ 4 /* STYLE */,
+ )
+ )
+ },
+ }
+ const { container } = mountWithHydration(
+ ``,
+ () => h(Comp),
+ )
+ style.color = 'green'
+ await nextTick()
+ expect(container.innerHTML).toBe(``)
+ } finally {
+ __DEV__ = true
+ }
+ })
+
test('app.unmount()', async () => {
const container = document.createElement('DIV')
container.innerHTML = ''
@@ -1317,76 +1370,83 @@ describe('SSR hydration', () => {
// #10607
test('update component stable slot (prod + optimized mode)', async () => {
__DEV__ = false
- const container = document.createElement('div')
- container.innerHTML = ``
- const Comp = {
- render(this: any) {
- return (
- openBlock(),
- createElementBlock('div', null, [renderSlot(this.$slots, 'default')])
- )
- },
- }
- const show = ref(false)
- const clicked = ref(false)
-
- const Wrapper = {
- setup() {
- const items = ref([])
- onMounted(() => {
- items.value = [1]
- })
- return () => {
+ try {
+ const container = document.createElement('div')
+ container.innerHTML = ``
+ const Comp = {
+ render(this: any) {
return (
openBlock(),
- createBlock(Comp, null, {
- default: withCtx(() => [
- createElementVNode('div', null, [
+ createElementBlock('div', null, [
+ renderSlot(this.$slots, 'default'),
+ ])
+ )
+ },
+ }
+ const show = ref(false)
+ const clicked = ref(false)
+
+ const Wrapper = {
+ setup() {
+ const items = ref([])
+ onMounted(() => {
+ items.value = [1]
+ })
+ return () => {
+ return (
+ openBlock(),
+ createBlock(Comp, null, {
+ default: withCtx(() => [
createElementVNode('div', null, [
- clicked.value
- ? (openBlock(),
- createElementBlock('div', { key: 0 }, 'foo'))
- : createCommentVNode('v-if', true),
+ createElementVNode('div', null, [
+ clicked.value
+ ? (openBlock(),
+ createElementBlock('div', { key: 0 }, 'foo'))
+ : createCommentVNode('v-if', true),
+ ]),
]),
+ createElementVNode(
+ 'div',
+ null,
+ items.value.length,
+ 1 /* TEXT */,
+ ),
]),
- createElementVNode(
- 'div',
- null,
- items.value.length,
- 1 /* TEXT */,
- ),
- ]),
- _: 1 /* STABLE */,
- })
- )
- }
- },
- }
- createSSRApp({
- components: { Wrapper },
- data() {
- return { show }
- },
- template: ``,
- }).mount(container)
+ _: 1 /* STABLE */,
+ })
+ )
+ }
+ },
+ }
+ createSSRApp({
+ components: { Wrapper },
+ data() {
+ return { show }
+ },
+ template: ``,
+ }).mount(container)
- await nextTick()
- expect(container.innerHTML).toBe(
- ``,
- )
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ ``,
+ )
- show.value = true
- await nextTick()
- expect(async () => {
- clicked.value = true
+ show.value = true
await nextTick()
- }).not.toThrow("Cannot read properties of null (reading 'insertBefore')")
+ expect(async () => {
+ clicked.value = true
+ await nextTick()
+ }).not.toThrow("Cannot read properties of null (reading 'insertBefore')")
- await nextTick()
- expect(container.innerHTML).toBe(
- ``,
- )
- __DEV__ = true
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ ``,
+ )
+ } catch (e) {
+ throw e
+ } finally {
+ __DEV__ = true
+ }
})
describe('mismatch handling', () => {
diff --git a/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts b/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts
index 556ab75209b..e0b49a5e7eb 100644
--- a/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts
+++ b/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts
@@ -8,6 +8,8 @@ import {
createApp,
createBlock,
createCommentVNode,
+ createElementBlock,
+ createElementVNode,
createTextVNode,
createVNode,
defineComponent,
@@ -23,6 +25,7 @@ import {
renderList,
renderSlot,
serialize,
+ setBlockTracking,
withCtx,
} from '@vue/runtime-test'
import { PatchFlags, SlotFlags } from '@vue/shared'
@@ -434,7 +437,7 @@ describe('renderer: optimized mode', () => {
const App = {
setup() {
return () => {
- return createVNode(Comp, null, {
+ return createBlock(Comp, null, {
default: withCtx(() => [
createVNode('p', null, foo.value, PatchFlags.TEXT),
]),
@@ -560,6 +563,7 @@ describe('renderer: optimized mode', () => {
const state = ref(0)
const CompA = {
+ name: 'A',
setup(props: any, { slots }: SetupContext) {
return () => {
return (
@@ -571,6 +575,7 @@ describe('renderer: optimized mode', () => {
}
const Wrapper = {
+ name: 'Wrapper',
setup(props: any, { slots }: SetupContext) {
// use the manually written render function to rendering the optimized slots,
// which should make subsequent updates exit the optimized mode correctly
@@ -581,6 +586,7 @@ describe('renderer: optimized mode', () => {
}
const app = createApp({
+ name: 'App',
setup() {
return () => {
return (
@@ -959,4 +965,271 @@ describe('renderer: optimized mode', () => {
// should successfully unmount without error
expect(inner(root)).toBe(``)
})
+
+ // #10870
+ test('should bail manually rendered compiler slots for both mount and update', async () => {
+ // only reproducible in prod
+ __DEV__ = false
+ function Outer(_: any, { slots }: any) {
+ return slots.default()
+ }
+ const Mid = {
+ render(ctx: any) {
+ return (
+ openBlock(),
+ createElementBlock('div', null, [renderSlot(ctx.$slots, 'default')])
+ )
+ },
+ }
+ const state1 = ref(true)
+ const state2 = ref(true)
+ const App = {
+ render() {
+ return (
+ openBlock(),
+ createBlock(Outer, null, {
+ default: withCtx(() => [
+ createVNode(
+ Mid,
+ { foo: state2.value },
+ {
+ default: withCtx(() => [
+ createElementVNode('div', null, [
+ createElementVNode('div', null, [
+ state2.value
+ ? (openBlock(),
+ createElementBlock(
+ 'div',
+ {
+ key: 0,
+ id: 'if',
+ foo: state1.value,
+ },
+ null,
+ 8 /* PROPS */,
+ ['foo'],
+ ))
+ : createCommentVNode('v-if', true),
+ ]),
+ ]),
+ ]),
+ _: 1 /* STABLE */,
+ },
+ 8 /* PROPS */,
+ ['foo'],
+ ),
+ ]),
+ _: 1 /* STABLE */,
+ })
+ )
+ },
+ }
+
+ const app = createApp(App)
+ app.config.errorHandler = vi.fn()
+
+ try {
+ app.mount(root)
+
+ state1.value = false
+ await nextTick()
+
+ state2.value = false
+ await nextTick()
+ } finally {
+ __DEV__ = true
+ expect(app.config.errorHandler).not.toHaveBeenCalled()
+ }
+ })
+
+ // #11336
+ test('should bail manually rendered compiler slots for both mount and update (2)', async () => {
+ // only reproducible in prod
+ __DEV__ = false
+ const n = ref(0)
+ function Outer(_: any, { slots }: any) {
+ n.value // track
+ return slots.default()
+ }
+ const Mid = {
+ render(ctx: any) {
+ return (
+ openBlock(),
+ createElementBlock('div', null, [renderSlot(ctx.$slots, 'default')])
+ )
+ },
+ }
+ const show = ref(false)
+ const App = {
+ render() {
+ return (
+ openBlock(),
+ createBlock(Outer, null, {
+ default: withCtx(() => [
+ createVNode(Mid, null, {
+ default: withCtx(() => [
+ createElementVNode('div', null, [
+ show.value
+ ? (openBlock(),
+ createElementBlock('div', { key: 0 }, '1'))
+ : createCommentVNode('v-if', true),
+ createElementVNode('div', null, '2'),
+ createElementVNode('div', null, '3'),
+ ]),
+ createElementVNode('div', null, '4'),
+ ]),
+ _: 1 /* STABLE */,
+ }),
+ ]),
+ _: 1 /* STABLE */,
+ })
+ )
+ },
+ }
+
+ const app = createApp(App)
+ app.config.errorHandler = vi.fn()
+
+ try {
+ app.mount(root)
+
+ // force Outer update, which will assign new slots to Mid
+ // we want to make sure the compiled slot flag doesn't accidentally
+ // get assigned again
+ n.value++
+ await nextTick()
+
+ show.value = true
+ await nextTick()
+ } finally {
+ __DEV__ = true
+ expect(app.config.errorHandler).not.toHaveBeenCalled()
+ }
+ })
+
+ test('diff slot and slot fallback node', async () => {
+ const Comp = {
+ props: ['show'],
+ setup(props: any, { slots }: SetupContext) {
+ return () => {
+ return (
+ openBlock(),
+ createElementBlock('div', null, [
+ renderSlot(slots, 'default', { hide: !props.show }, () => [
+ (openBlock(),
+ (block = createElementBlock(
+ Fragment,
+ { key: 0 },
+ [createTextVNode('foo')],
+ PatchFlags.STABLE_FRAGMENT,
+ ))),
+ ]),
+ ])
+ )
+ }
+ },
+ }
+
+ const show = ref(true)
+ const app = createApp({
+ render() {
+ return (
+ openBlock(),
+ createBlock(
+ Comp,
+ { show: show.value },
+ {
+ default: withCtx(({ hide }: { hide: boolean }) => [
+ !hide
+ ? (openBlock(),
+ createElementBlock(
+ Fragment,
+ { key: 0 },
+ [
+ createCommentVNode('comment'),
+ createElementVNode(
+ 'div',
+ null,
+ 'bar',
+ PatchFlags.HOISTED,
+ ),
+ ],
+ PatchFlags.STABLE_FRAGMENT,
+ ))
+ : createCommentVNode('v-if', true),
+ ]),
+ _: SlotFlags.STABLE,
+ },
+ PatchFlags.PROPS,
+ ['show'],
+ )
+ )
+ },
+ })
+
+ app.mount(root)
+ expect(inner(root)).toBe('')
+ expect(block).toBe(null)
+
+ show.value = false
+ await nextTick()
+ expect(inner(root)).toBe('foo
')
+
+ show.value = true
+ await nextTick()
+ expect(inner(root)).toBe('')
+ })
+
+ test('should not take unmount children fast path if children contain cached nodes', async () => {
+ const show = ref(true)
+ const spyUnmounted = vi.fn()
+
+ const Child = {
+ setup() {
+ onUnmounted(spyUnmounted)
+ return () => createVNode('div', null, 'Child')
+ },
+ }
+
+ const app = createApp({
+ render(_: any, cache: any) {
+ return show.value
+ ? (openBlock(),
+ createBlock('div', null, [
+ createVNode('div', null, [
+ cache[0] ||
+ (setBlockTracking(-1),
+ ((cache[0] = createVNode('div', null, [
+ createVNode(Child),
+ ])).cacheIndex = 0),
+ setBlockTracking(1),
+ cache[0]),
+ ]),
+ ]))
+ : createCommentVNode('v-if', true)
+ },
+ })
+
+ app.mount(root)
+ expect(inner(root)).toBe(
+ '',
+ )
+
+ show.value = false
+ await nextTick()
+ expect(inner(root)).toBe('')
+ expect(spyUnmounted).toHaveBeenCalledTimes(1)
+
+ show.value = true
+ await nextTick()
+ expect(inner(root)).toBe(
+ '',
+ )
+
+ // should unmount again, this verifies previous cache was properly cleared
+ show.value = false
+ await nextTick()
+ expect(inner(root)).toBe('')
+ expect(spyUnmounted).toHaveBeenCalledTimes(2)
+ })
})
diff --git a/packages/runtime-core/__tests__/vnode.spec.ts b/packages/runtime-core/__tests__/vnode.spec.ts
index 39a7abd5f89..2e0eee1f280 100644
--- a/packages/runtime-core/__tests__/vnode.spec.ts
+++ b/packages/runtime-core/__tests__/vnode.spec.ts
@@ -2,6 +2,7 @@ import {
Comment,
Fragment,
Text,
+ type VNode,
cloneVNode,
createBlock,
createVNode,
@@ -633,7 +634,9 @@ describe('vnode', () => {
setBlockTracking(1),
vnode1,
]))
- expect(vnode.dynamicChildren).toStrictEqual([])
+ const expected: VNode['dynamicChildren'] = []
+ expected.hasOnce = true
+ expect(vnode.dynamicChildren).toStrictEqual(expected)
})
// #5657
test('error of slot function execution should not affect block tracking', () => {
diff --git a/packages/runtime-core/package.json b/packages/runtime-core/package.json
index 48ff50e8f8a..f08ce5c9b35 100644
--- a/packages/runtime-core/package.json
+++ b/packages/runtime-core/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue/runtime-core",
- "version": "3.4.31",
+ "version": "3.4.32",
"description": "@vue/runtime-core",
"main": "index.js",
"module": "dist/runtime-core.esm-bundler.js",
diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts
index b5faa856eca..df3a6376989 100644
--- a/packages/runtime-core/src/component.ts
+++ b/packages/runtime-core/src/component.ts
@@ -736,13 +736,14 @@ export let isInSSRComponentSetup = false
export function setupComponent(
instance: ComponentInternalInstance,
isSSR = false,
+ optimized = false,
) {
isSSR && setInSSRSetupState(isSSR)
const { props, children } = instance.vnode
const isStateful = isStatefulComponent(instance)
initProps(instance, props, isStateful, isSSR)
- initSlots(instance, children)
+ initSlots(instance, children, optimized)
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
diff --git a/packages/runtime-core/src/componentEmits.ts b/packages/runtime-core/src/componentEmits.ts
index 4551235bc5a..b6589b92227 100644
--- a/packages/runtime-core/src/componentEmits.ts
+++ b/packages/runtime-core/src/componentEmits.ts
@@ -28,6 +28,7 @@ import {
compatModelEmit,
compatModelEventPrefix,
} from './compat/componentVModel'
+import { getModelModifiers } from './helpers/useModel'
export type ObjectEmitsOptions = Record<
string,
@@ -125,16 +126,12 @@ export function emit(
const isModelListener = event.startsWith('update:')
// for v-model update:xxx events, apply modifiers on args
- const modelArg = isModelListener && event.slice(7)
- if (modelArg && modelArg in props) {
- const modifiersKey = `${
- modelArg === 'modelValue' ? 'model' : modelArg
- }Modifiers`
- const { number, trim } = props[modifiersKey] || EMPTY_OBJ
- if (trim) {
+ const modifiers = isModelListener && getModelModifiers(props, event.slice(7))
+ if (modifiers) {
+ if (modifiers.trim) {
args = rawArgs.map(a => (isString(a) ? a.trim() : a))
}
- if (number) {
+ if (modifiers.number) {
args = rawArgs.map(looseToNumber)
}
}
diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts
index 5a4292b6f36..9d7b7f0e4a5 100644
--- a/packages/runtime-core/src/componentProps.ts
+++ b/packages/runtime-core/src/componentProps.ts
@@ -496,12 +496,15 @@ function resolvePropValue(
return value
}
+const mixinPropsCache = new WeakMap()
+
export function normalizePropsOptions(
comp: ConcreteComponent,
appContext: AppContext,
asMixin = false,
): NormalizedPropsOptions {
- const cache = appContext.propsCache
+ const cache =
+ __FEATURE_OPTIONS_API__ && asMixin ? mixinPropsCache : appContext.propsCache
const cached = cache.get(comp)
if (cached) {
return cached
diff --git a/packages/runtime-core/src/componentSlots.ts b/packages/runtime-core/src/componentSlots.ts
index 2bc3466c459..438c56efb47 100644
--- a/packages/runtime-core/src/componentSlots.ts
+++ b/packages/runtime-core/src/componentSlots.ts
@@ -12,7 +12,6 @@ import {
ShapeFlags,
SlotFlags,
def,
- extend,
isArray,
isFunction,
} from '@vue/shared'
@@ -161,17 +160,36 @@ const normalizeVNodeSlots = (
instance.slots.default = () => normalized
}
+const assignSlots = (
+ slots: InternalSlots,
+ children: Slots,
+ optimized: boolean,
+) => {
+ for (const key in children) {
+ // #2893
+ // when rendering the optimized slots by manually written render function,
+ // do not copy the `slots._` compiler flag so that `renderSlot` creates
+ // slot Fragment with BAIL patchFlag to force full updates
+ if (optimized || key !== '_') {
+ slots[key] = children[key]
+ }
+ }
+}
+
export const initSlots = (
instance: ComponentInternalInstance,
children: VNodeNormalizedChildren,
+ optimized: boolean,
) => {
const slots = (instance.slots = createInternalObject())
if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
const type = (children as RawSlots)._
if (type) {
- extend(slots, children as InternalSlots)
+ assignSlots(slots, children as Slots, optimized)
// make compiler marker non-enumerable
- def(slots, '_', type, true)
+ if (optimized) {
+ def(slots, '_', type, true)
+ }
} else {
normalizeObjectSlots(children as RawSlots, slots, instance)
}
@@ -195,7 +213,7 @@ export const updateSlots = (
if (__DEV__ && isHmrUpdating) {
// Parent was HMR updated so slot content may have changed.
// force update slots and mark instance for hmr as well
- extend(slots, children as Slots)
+ assignSlots(slots, children as Slots, optimized)
trigger(instance, TriggerOpTypes.SET, '$slots')
} else if (optimized && type === SlotFlags.STABLE) {
// compiled AND stable.
@@ -204,14 +222,7 @@ export const updateSlots = (
} else {
// compiled but dynamic (v-if/v-for on slots) - update slots, but skip
// normalization.
- extend(slots, children as Slots)
- // #2893
- // when rendering the optimized slots by manually written render function,
- // we need to delete the `slots._` flag if necessary to make subsequent updates reliable,
- // i.e. let the `renderSlot` create the bailed Fragment
- if (!optimized && type === SlotFlags.STABLE) {
- delete slots._
- }
+ assignSlots(slots, children as Slots, optimized)
}
} else {
needDeletionCheck = !(children as RawSlots).$stable
diff --git a/packages/runtime-core/src/components/Teleport.ts b/packages/runtime-core/src/components/Teleport.ts
index a10ae84d428..65437300cff 100644
--- a/packages/runtime-core/src/components/Teleport.ts
+++ b/packages/runtime-core/src/components/Teleport.ts
@@ -21,6 +21,8 @@ export interface TeleportProps {
disabled?: boolean
}
+export const TeleportEndKey = Symbol('_vte')
+
export const isTeleport = (type: any): boolean => type.__isTeleport
const isTeleportDisabled = (props: VNode['props']): boolean =>
@@ -105,11 +107,16 @@ export const TeleportImpl = {
const mainAnchor = (n2.anchor = __DEV__
? createComment('teleport end')
: createText(''))
- insert(placeholder, container, anchor)
- insert(mainAnchor, container, anchor)
const target = (n2.target = resolveTarget(n2.props, querySelector))
+ const targetStart = (n2.targetStart = createText(''))
const targetAnchor = (n2.targetAnchor = createText(''))
+ insert(placeholder, container, anchor)
+ insert(mainAnchor, container, anchor)
+ // attach a special property so we can skip teleported content in
+ // renderer's nextSibling search
+ targetStart[TeleportEndKey] = targetAnchor
if (target) {
+ insert(targetStart, target)
insert(targetAnchor, target)
// #2652 we could be teleporting from a non-SVG tree into an SVG tree
if (namespace === 'svg' || isTargetSVG(target)) {
@@ -146,6 +153,7 @@ export const TeleportImpl = {
} else {
// update content
n2.el = n1.el
+ n2.targetStart = n1.targetStart
const mainAnchor = (n2.anchor = n1.anchor)!
const target = (n2.target = n1.target)!
const targetAnchor = (n2.targetAnchor = n1.targetAnchor)!
@@ -253,9 +261,18 @@ export const TeleportImpl = {
{ um: unmount, o: { remove: hostRemove } }: RendererInternals,
doRemove: boolean,
) {
- const { shapeFlag, children, anchor, targetAnchor, target, props } = vnode
+ const {
+ shapeFlag,
+ children,
+ anchor,
+ targetStart,
+ targetAnchor,
+ target,
+ props,
+ } = vnode
if (target) {
+ hostRemove(targetStart!)
hostRemove(targetAnchor!)
}
diff --git a/packages/runtime-core/src/customFormatter.ts b/packages/runtime-core/src/customFormatter.ts
index abd3a329922..9cda478345c 100644
--- a/packages/runtime-core/src/customFormatter.ts
+++ b/packages/runtime-core/src/customFormatter.ts
@@ -24,6 +24,7 @@ export function initCustomFormatter() {
// custom formatter for Chrome
// https://www.mattzeunert.com/2016/02/19/custom-chrome-devtools-object-formatters.html
const formatter = {
+ __vue_custom_formatter: true,
header(obj: unknown) {
// TODO also format ComponentPublicInstance & ctx.slots/attrs in setup
if (!isObject(obj)) {
diff --git a/packages/runtime-core/src/errorHandling.ts b/packages/runtime-core/src/errorHandling.ts
index 41c92cbd34a..d243db5bffd 100644
--- a/packages/runtime-core/src/errorHandling.ts
+++ b/packages/runtime-core/src/errorHandling.ts
@@ -23,6 +23,7 @@ export enum ErrorCodes {
FUNCTION_REF,
ASYNC_COMPONENT_LOADER,
SCHEDULER,
+ COMPONENT_UPDATE,
}
export const ErrorTypeStrings: Record = {
@@ -54,16 +55,15 @@ export const ErrorTypeStrings: Record = {
[ErrorCodes.APP_WARN_HANDLER]: 'app warnHandler',
[ErrorCodes.FUNCTION_REF]: 'ref function',
[ErrorCodes.ASYNC_COMPONENT_LOADER]: 'async component loader',
- [ErrorCodes.SCHEDULER]:
- 'scheduler flush. This is likely a Vue internals bug. ' +
- 'Please open an issue at https://github.com/vuejs/core .',
+ [ErrorCodes.SCHEDULER]: 'scheduler flush',
+ [ErrorCodes.COMPONENT_UPDATE]: 'component update',
}
export type ErrorTypes = LifecycleHooks | ErrorCodes
export function callWithErrorHandling(
fn: Function,
- instance: ComponentInternalInstance | null,
+ instance: ComponentInternalInstance | null | undefined,
type: ErrorTypes,
args?: unknown[],
) {
@@ -105,7 +105,7 @@ export function callWithAsyncErrorHandling(
export function handleError(
err: unknown,
- instance: ComponentInternalInstance | null,
+ instance: ComponentInternalInstance | null | undefined,
type: ErrorTypes,
throwInDev = true,
) {
diff --git a/packages/runtime-core/src/helpers/renderSlot.ts b/packages/runtime-core/src/helpers/renderSlot.ts
index 8a7608219d9..f0b13904f08 100644
--- a/packages/runtime-core/src/helpers/renderSlot.ts
+++ b/packages/runtime-core/src/helpers/renderSlot.ts
@@ -65,11 +65,13 @@ export function renderSlot(
Fragment,
{
key:
- props.key ||
- // slot content array of a dynamic conditional slot may have a branch
- // key attached in the `createSlots` helper, respect that
- (validSlotContent && (validSlotContent as any).key) ||
- `_${name}`,
+ (props.key ||
+ // slot content array of a dynamic conditional slot may have a branch
+ // key attached in the `createSlots` helper, respect that
+ (validSlotContent && (validSlotContent as any).key) ||
+ `_${name}`) +
+ // #7256 force differentiate fallback content from actual content
+ (!validSlotContent && fallback ? '_fb' : ''),
},
validSlotContent || (fallback ? fallback() : []),
validSlotContent && (slots as RawSlots)._ === SlotFlags.STABLE
diff --git a/packages/runtime-core/src/helpers/useModel.ts b/packages/runtime-core/src/helpers/useModel.ts
index f6fbca554a7..493264ea73a 100644
--- a/packages/runtime-core/src/helpers/useModel.ts
+++ b/packages/runtime-core/src/helpers/useModel.ts
@@ -29,9 +29,13 @@ export function useModel(
const camelizedName = camelize(name)
const hyphenatedName = hyphenate(name)
+ const modifiers = getModelModifiers(props, name)
const res = customRef((track, trigger) => {
let localValue: any
+ let prevSetValue: any
+ let prevEmittedValue: any
+
watchSyncEffect(() => {
const propValue = props[name]
if (hasChanged(localValue, propValue)) {
@@ -39,12 +43,17 @@ export function useModel(
trigger()
}
})
+
return {
get() {
track()
return options.get ? options.get(localValue) : localValue
},
+
set(value) {
+ if (!hasChanged(value, localValue)) {
+ return
+ }
const rawProps = i.vnode!.props
if (
!(
@@ -56,27 +65,38 @@ export function useModel(
(`onUpdate:${name}` in rawProps ||
`onUpdate:${camelizedName}` in rawProps ||
`onUpdate:${hyphenatedName}` in rawProps)
- ) &&
- hasChanged(value, localValue)
+ )
) {
+ // no v-model, local update
localValue = value
trigger()
}
- i.emit(`update:${name}`, options.set ? options.set(value) : value)
+ const emittedValue = options.set ? options.set(value) : value
+ i.emit(`update:${name}`, emittedValue)
+ // #10279: if the local value is converted via a setter but the value
+ // emitted to parent was the same, the parent will not trigger any
+ // updates and there will be no prop sync. However the local input state
+ // may be out of sync, so we need to force an update here.
+ if (
+ value !== emittedValue &&
+ value !== prevSetValue &&
+ emittedValue === prevEmittedValue
+ ) {
+ trigger()
+ }
+ prevSetValue = value
+ prevEmittedValue = emittedValue
},
}
})
- const modifierKey =
- name === 'modelValue' ? 'modelModifiers' : `${name}Modifiers`
-
// @ts-expect-error
res[Symbol.iterator] = () => {
let i = 0
return {
next() {
if (i < 2) {
- return { value: i++ ? props[modifierKey] || {} : res, done: false }
+ return { value: i++ ? modifiers || EMPTY_OBJ : res, done: false }
} else {
return { done: true }
}
@@ -86,3 +106,14 @@ export function useModel(
return res
}
+
+export const getModelModifiers = (
+ props: Record,
+ modelName: string,
+): Record | undefined => {
+ return modelName === 'modelValue' || modelName === 'model-value'
+ ? props.modelModifiers
+ : props[`${modelName}Modifiers`] ||
+ props[`${camelize(modelName)}Modifiers`] ||
+ props[`${hyphenate(modelName)}Modifiers`]
+}
diff --git a/packages/runtime-core/src/helpers/withMemo.ts b/packages/runtime-core/src/helpers/withMemo.ts
index 72ef814eefa..708726e89c9 100644
--- a/packages/runtime-core/src/helpers/withMemo.ts
+++ b/packages/runtime-core/src/helpers/withMemo.ts
@@ -15,7 +15,7 @@ export function withMemo(
// shallow clone
ret.memo = memo.slice()
- ret.memoIndex = index
+ ret.cacheIndex = index
return (cache[index] = ret)
}
diff --git a/packages/runtime-core/src/hmr.ts b/packages/runtime-core/src/hmr.ts
index 8196eb89195..5a4a95705b0 100644
--- a/packages/runtime-core/src/hmr.ts
+++ b/packages/runtime-core/src/hmr.ts
@@ -14,7 +14,10 @@ type HMRComponent = ComponentOptions | ClassComponent
export let isHmrUpdating = false
-export const hmrDirtyComponents = new Set()
+export const hmrDirtyComponents = new Map<
+ ConcreteComponent,
+ Set
+>()
export interface HMRRuntime {
createRecord: typeof createRecord
@@ -110,18 +113,21 @@ function reload(id: string, newComp: HMRComponent) {
// create a snapshot which avoids the set being mutated during updates
const instances = [...record.instances]
- for (const instance of instances) {
+ for (let i = 0; i < instances.length; i++) {
+ const instance = instances[i]
const oldComp = normalizeClassComponent(instance.type as HMRComponent)
- if (!hmrDirtyComponents.has(oldComp)) {
+ let dirtyInstances = hmrDirtyComponents.get(oldComp)
+ if (!dirtyInstances) {
// 1. Update existing comp definition to match new one
if (oldComp !== record.initialDef) {
updateComponentDef(oldComp, newComp)
}
// 2. mark definition dirty. This forces the renderer to replace the
// component on patch.
- hmrDirtyComponents.add(oldComp)
+ hmrDirtyComponents.set(oldComp, (dirtyInstances = new Set()))
}
+ dirtyInstances.add(instance)
// 3. invalidate options resolution cache
instance.appContext.propsCache.delete(instance.type as any)
@@ -131,9 +137,9 @@ function reload(id: string, newComp: HMRComponent) {
// 4. actually update
if (instance.ceReload) {
// custom element
- hmrDirtyComponents.add(oldComp)
+ dirtyInstances.add(instance)
instance.ceReload((newComp as any).styles)
- hmrDirtyComponents.delete(oldComp)
+ dirtyInstances.delete(instance)
} else if (instance.parent) {
// 4. Force the parent instance to re-render. This will cause all updated
// components to be unmounted and re-mounted. Queue the update so that we
@@ -141,8 +147,8 @@ function reload(id: string, newComp: HMRComponent) {
instance.parent.effect.dirty = true
queueJob(() => {
instance.parent!.update()
- // #6930 avoid infinite recursion
- hmrDirtyComponents.delete(oldComp)
+ // #6930, #11248 avoid infinite recursion
+ dirtyInstances.delete(instance)
})
} else if (instance.appContext.reload) {
// root instance mounted via createApp() has a reload method
@@ -159,11 +165,7 @@ function reload(id: string, newComp: HMRComponent) {
// 5. make sure to cleanup dirty hmr components after update
queuePostFlushCb(() => {
- for (const instance of instances) {
- hmrDirtyComponents.delete(
- normalizeClassComponent(instance.type as HMRComponent),
- )
- }
+ hmrDirtyComponents.clear()
})
}
diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts
index 8eca0705dfd..c371542607d 100644
--- a/packages/runtime-core/src/hydration.ts
+++ b/packages/runtime-core/src/hydration.ts
@@ -39,6 +39,7 @@ import {
} from './components/Suspense'
import type { TeleportImpl, TeleportVNode } from './components/Teleport'
import { isAsyncWrapper } from './apiAsyncComponent'
+import { isReactive } from '@vue/reactivity'
export type RootHydrateFunction = (
vnode: VNode,
@@ -464,15 +465,7 @@ export function createHydrationFunctions(
// force hydrate v-bind with .prop modifiers
key[0] === '.'
) {
- patchProp(
- el,
- key,
- null,
- props[key],
- undefined,
- undefined,
- parentComponent,
- )
+ patchProp(el, key, null, props[key], undefined, parentComponent)
}
}
} else if (props.onClick) {
@@ -484,9 +477,13 @@ export function createHydrationFunctions(
null,
props.onClick,
undefined,
- undefined,
parentComponent,
)
+ } else if (patchFlag & PatchFlags.STYLE && isReactive(props.style)) {
+ // #11372: object style values are iterated during patch instead of
+ // render/normalization phase, but style patch is skipped during
+ // hydration, so we need to force iterate the object to track deps
+ for (const key in props.style) props.style[key]
}
}
@@ -531,7 +528,27 @@ export function createHydrationFunctions(
const vnode = optimized
? children[i]
: (children[i] = normalizeVNode(children[i]))
+ const isText = vnode.type === Text
if (node) {
+ if (isText && !optimized) {
+ // #7285 possible consecutive text vnodes from manual render fns or
+ // JSX-compiled fns, but on the client the browser parses only 1 text
+ // node.
+ // look ahead for next possible text vnode
+ let next = children[i + 1]
+ if (next && (next = normalizeVNode(next)).type === Text) {
+ // create an extra TextNode on the client for the next vnode to
+ // adopt
+ insert(
+ createText(
+ (node as Text).data.slice((vnode.children as string).length),
+ ),
+ container,
+ nextSibling(node),
+ )
+ ;(node as Text).data = vnode.children as string
+ }
+ }
node = hydrateNode(
node,
vnode,
@@ -540,7 +557,7 @@ export function createHydrationFunctions(
slotScopeIds,
optimized,
)
- } else if (vnode.type === Text && !vnode.children) {
+ } else if (isText && !vnode.children) {
// #7215 create a TextNode for empty text node
// because server rendered HTML won't contain a text node
insert((vnode.el = createText('')), container)
diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts
index d5c5b6d8dfb..db674f987d8 100644
--- a/packages/runtime-core/src/renderer.ts
+++ b/packages/runtime-core/src/renderer.ts
@@ -58,7 +58,11 @@ import {
type SuspenseImpl,
queueEffectWithSuspense,
} from './components/Suspense'
-import type { TeleportImpl, TeleportVNode } from './components/Teleport'
+import {
+ TeleportEndKey,
+ type TeleportImpl,
+ type TeleportVNode,
+} from './components/Teleport'
import { type KeepAliveContext, isKeepAlive } from './components/KeepAlive'
import { isHmrUpdating, registerHMR, unregisterHMR } from './hmr'
import { type RootHydrateFunction, createHydrationFunctions } from './hydration'
@@ -103,10 +107,7 @@ export interface RendererOptions<
prevValue: any,
nextValue: any,
namespace?: ElementNamespace,
- prevChildren?: VNode[],
parentComponent?: ComponentInternalInstance | null,
- parentSuspense?: SuspenseBoundary | null,
- unmountChildren?: UnmountChildrenFn,
): void
insert(el: HostNode, parent: HostElement, anchor?: HostNode | null): void
remove(el: HostNode): void
@@ -140,7 +141,7 @@ export interface RendererOptions<
// functions provided via options, so the internal constraint is really just
// a generic object.
export interface RendererNode {
- [key: string]: any
+ [key: string | symbol]: any
}
export interface RendererElement extends RendererNode {}
@@ -666,17 +667,7 @@ function baseCreateRenderer(
if (props) {
for (const key in props) {
if (key !== 'value' && !isReservedProp(key)) {
- hostPatchProp(
- el,
- key,
- null,
- props[key],
- namespace,
- vnode.children as VNode[],
- parentComponent,
- parentSuspense,
- unmountChildren,
- )
+ hostPatchProp(el, key, null, props[key], namespace, parentComponent)
}
}
/**
@@ -829,6 +820,15 @@ function baseCreateRenderer(
dynamicChildren = null
}
+ // #9135 innerHTML / textContent unset needs to happen before possible
+ // new children mount
+ if (
+ (oldProps.innerHTML && newProps.innerHTML == null) ||
+ (oldProps.textContent && newProps.textContent == null)
+ ) {
+ hostSetElementText(el, '')
+ }
+
if (dynamicChildren) {
patchBlockChildren(
n1.dynamicChildren!,
@@ -865,15 +865,7 @@ function baseCreateRenderer(
// (i.e. at the exact same position in the source template)
if (patchFlag & PatchFlags.FULL_PROPS) {
// element props contain dynamic keys, full diff needed
- patchProps(
- el,
- n2,
- oldProps,
- newProps,
- parentComponent,
- parentSuspense,
- namespace,
- )
+ patchProps(el, oldProps, newProps, parentComponent, namespace)
} else {
// class
// this flag is matched when the element has dynamic class bindings.
@@ -904,17 +896,7 @@ function baseCreateRenderer(
const next = newProps[key]
// #1471 force patch value
if (next !== prev || key === 'value') {
- hostPatchProp(
- el,
- key,
- prev,
- next,
- namespace,
- n1.children as VNode[],
- parentComponent,
- parentSuspense,
- unmountChildren,
- )
+ hostPatchProp(el, key, prev, next, namespace, parentComponent)
}
}
}
@@ -929,15 +911,7 @@ function baseCreateRenderer(
}
} else if (!optimized && dynamicChildren == null) {
// unoptimized, full diff
- patchProps(
- el,
- n2,
- oldProps,
- newProps,
- parentComponent,
- parentSuspense,
- namespace,
- )
+ patchProps(el, oldProps, newProps, parentComponent, namespace)
}
if ((vnodeHook = newProps.onVnodeUpdated) || dirs) {
@@ -994,11 +968,9 @@ function baseCreateRenderer(
const patchProps = (
el: RendererElement,
- vnode: VNode,
oldProps: Data,
newProps: Data,
parentComponent: ComponentInternalInstance | null,
- parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
) => {
if (oldProps !== newProps) {
@@ -1011,10 +983,7 @@ function baseCreateRenderer(
oldProps[key],
null,
namespace,
- vnode.children as VNode[],
parentComponent,
- parentSuspense,
- unmountChildren,
)
}
}
@@ -1026,17 +995,7 @@ function baseCreateRenderer(
const prev = oldProps[key]
// defer patching value
if (next !== prev && key !== 'value') {
- hostPatchProp(
- el,
- key,
- prev,
- next,
- namespace,
- vnode.children as VNode[],
- parentComponent,
- parentSuspense,
- unmountChildren,
- )
+ hostPatchProp(el, key, prev, next, namespace, parentComponent)
}
}
if ('value' in newProps) {
@@ -1229,7 +1188,7 @@ function baseCreateRenderer(
if (__DEV__) {
startMeasure(instance, `init`)
}
- setupComponent(instance)
+ setupComponent(instance, false, optimized)
if (__DEV__) {
endMeasure(instance, `init`)
}
@@ -1587,6 +1546,7 @@ function baseCreateRenderer(
effect.run()
}
})
+ update.i = instance
update.id = instance.uid
// allowRecurse
// #1801, #2043 component render effects should allow recursive updates
@@ -1599,7 +1559,6 @@ function baseCreateRenderer(
effect.onTrigger = instance.rtg
? e => invokeArrayFns(instance.rtg!, e)
: void 0
- update.ownerInstance = instance
}
update()
@@ -2109,7 +2068,7 @@ function baseCreateRenderer(
shapeFlag,
patchFlag,
dirs,
- memoIndex,
+ cacheIndex,
} = vnode
if (patchFlag === PatchFlags.BAIL) {
@@ -2122,8 +2081,8 @@ function baseCreateRenderer(
}
// #6593 should clean memo cache when unmount
- if (memoIndex != null) {
- parentComponent!.renderCache[memoIndex] = undefined
+ if (cacheIndex != null) {
+ parentComponent!.renderCache[cacheIndex] = undefined
}
if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
@@ -2164,6 +2123,12 @@ function baseCreateRenderer(
)
} else if (
dynamicChildren &&
+ // #5154
+ // when v-once is used inside a block, setBlockTracking(-1) marks the
+ // parent block with hasOnce: true
+ // so that it doesn't take the fast path during unmount - otherwise
+ // components nested in v-once are never unmounted.
+ !dynamicChildren.hasOnce &&
// #1153: fast path should not be taken for non-stable (v-for) fragments
(type !== Fragment ||
(patchFlag > 0 && patchFlag & PatchFlags.STABLE_FRAGMENT))
@@ -2362,7 +2327,12 @@ function baseCreateRenderer(
if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) {
return vnode.suspense!.next()
}
- return hostNextSibling((vnode.anchor || vnode.el)!)
+ const el = hostNextSibling((vnode.anchor || vnode.el)!)
+ // #9071, #9313
+ // teleported content can mess up nextSibling searches during patch so
+ // we need to skip them during nextSibling search
+ const teleportEnd = el && el[TeleportEndKey]
+ return teleportEnd ? hostNextSibling(teleportEnd) : el
}
let isFlushing = false
diff --git a/packages/runtime-core/src/scheduler.ts b/packages/runtime-core/src/scheduler.ts
index 4ae1c6d46e7..c1f9d4c9141 100644
--- a/packages/runtime-core/src/scheduler.ts
+++ b/packages/runtime-core/src/scheduler.ts
@@ -26,9 +26,8 @@ export interface SchedulerJob extends Function {
/**
* Attached by renderer.ts when setting up a component's render effect
* Used to obtain component information when reporting max recursive updates.
- * dev only.
*/
- ownerInstance?: ComponentInternalInstance
+ i?: ComponentInternalInstance
}
export type SchedulerJobs = SchedulerJob | SchedulerJob[]
@@ -240,7 +239,11 @@ function flushJobs(seen?: CountMap) {
if (__DEV__ && check(job)) {
continue
}
- callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
+ callWithErrorHandling(
+ job,
+ job.i,
+ job.i ? ErrorCodes.COMPONENT_UPDATE : ErrorCodes.SCHEDULER,
+ )
}
}
} finally {
@@ -265,7 +268,7 @@ function checkRecursiveUpdates(seen: CountMap, fn: SchedulerJob) {
} else {
const count = seen.get(fn)!
if (count > RECURSION_LIMIT) {
- const instance = fn.ownerInstance
+ const instance = fn.i
const componentName = instance && getComponentName(instance.type)
handleError(
`Maximum recursive updates exceeded${
diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts
index 7abd45c7fa5..75024b73c77 100644
--- a/packages/runtime-core/src/vnode.ts
+++ b/packages/runtime-core/src/vnode.ts
@@ -92,10 +92,22 @@ export type VNodeRef =
) => void)
export type VNodeNormalizedRefAtom = {
+ /**
+ * component instance
+ */
i: ComponentInternalInstance
+ /**
+ * Actual ref
+ */
r: VNodeRef
- k?: string // setup ref key
- f?: boolean // refInFor marker
+ /**
+ * setup ref key
+ */
+ k?: string
+ /**
+ * refInFor marker
+ */
+ f?: boolean
}
export type VNodeNormalizedRef =
@@ -186,6 +198,7 @@ export interface VNode<
el: HostNode | null
anchor: HostNode | null // fragment anchor
target: HostElement | null // teleport target
+ targetStart: HostNode | null // teleport target start anchor
targetAnchor: HostNode | null // teleport target anchor
/**
* number of elements contained in a static vnode
@@ -214,7 +227,7 @@ export interface VNode<
/**
* @internal
*/
- dynamicChildren: VNode[] | null
+ dynamicChildren: (VNode[] & { hasOnce?: boolean }) | null
// application root node only
appContext: AppContext | null
@@ -231,7 +244,7 @@ export interface VNode<
/**
* @internal index for cleaning v-memo cache
*/
- memoIndex?: number
+ cacheIndex?: number
/**
* @internal __COMPAT__ only
*/
@@ -247,8 +260,8 @@ export interface VNode<
// can divide a template into nested blocks, and within each block the node
// structure would be stable. This allows us to skip most children diffing
// and only worry about the dynamic nodes (indicated by patch flags).
-export const blockStack: (VNode[] | null)[] = []
-export let currentBlock: VNode[] | null = null
+export const blockStack: VNode['dynamicChildren'][] = []
+export let currentBlock: VNode['dynamicChildren'] = null
/**
* Open a block.
@@ -299,6 +312,11 @@ export let isBlockTreeEnabled = 1
*/
export function setBlockTracking(value: number) {
isBlockTreeEnabled += value
+ if (value < 0 && currentBlock) {
+ // mark current block so it doesn't take fast path and skip possible
+ // nested components duriung unmount
+ currentBlock.hasOnce = true
+ }
}
function setupBlock(vnode: VNode) {
@@ -370,17 +388,16 @@ export function isVNode(value: any): value is VNode {
}
export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
- if (
- __DEV__ &&
- n2.shapeFlag & ShapeFlags.COMPONENT &&
- hmrDirtyComponents.has(n2.type as ConcreteComponent)
- ) {
- // #7042, ensure the vnode being unmounted during HMR
- // bitwise operations to remove keep alive flags
- n1.shapeFlag &= ~ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
- n2.shapeFlag &= ~ShapeFlags.COMPONENT_KEPT_ALIVE
- // HMR only: if the component has been hot-updated, force a reload.
- return false
+ if (__DEV__ && n2.shapeFlag & ShapeFlags.COMPONENT && n1.component) {
+ const dirtyInstances = hmrDirtyComponents.get(n2.type as ConcreteComponent)
+ if (dirtyInstances && dirtyInstances.has(n1.component)) {
+ // #7042, ensure the vnode being unmounted during HMR
+ // bitwise operations to remove keep alive flags
+ n1.shapeFlag &= ~ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
+ n2.shapeFlag &= ~ShapeFlags.COMPONENT_KEPT_ALIVE
+ // HMR only: if the component has been hot-updated, force a reload.
+ return false
+ }
}
return n1.type === n2.type && n1.key === n2.key
}
@@ -461,6 +478,7 @@ function createBaseVNode(
el: null,
anchor: null,
target: null,
+ targetStart: null,
targetAnchor: null,
staticCount: 0,
shapeFlag,
@@ -661,6 +679,7 @@ export function cloneVNode(
? (children as VNode[]).map(deepCloneVNode)
: children,
target: vnode.target,
+ targetStart: vnode.targetStart,
targetAnchor: vnode.targetAnchor,
staticCount: vnode.staticCount,
shapeFlag: vnode.shapeFlag,
diff --git a/packages/runtime-core/src/warning.ts b/packages/runtime-core/src/warning.ts
index d130638b6af..169cdd907ae 100644
--- a/packages/runtime-core/src/warning.ts
+++ b/packages/runtime-core/src/warning.ts
@@ -30,7 +30,12 @@ export function popWarningContext() {
stack.pop()
}
+let isWarning = false
+
export function warn(msg: string, ...args: any[]) {
+ if (isWarning) return
+ isWarning = true
+
// avoid props formatting or warn handler tracking deps that might be mutated
// during patch, leading to infinite recursion.
pauseTracking()
@@ -70,6 +75,7 @@ export function warn(msg: string, ...args: any[]) {
}
resetTracking()
+ isWarning = false
}
export function getComponentTrace(): ComponentTraceStack {
diff --git a/packages/runtime-dom/__tests__/patchProps.spec.ts b/packages/runtime-dom/__tests__/patchProps.spec.ts
index 61dd98513ce..7f418847f5f 100644
--- a/packages/runtime-dom/__tests__/patchProps.spec.ts
+++ b/packages/runtime-dom/__tests__/patchProps.spec.ts
@@ -1,5 +1,5 @@
import { patchProp } from '../src/patchProp'
-import { h, render } from '../src'
+import { h, nextTick, ref, render } from '../src'
describe('runtime-dom: props patching', () => {
test('basic', () => {
@@ -133,6 +133,25 @@ describe('runtime-dom: props patching', () => {
expect(fn).toHaveBeenCalled()
})
+ test('patch innerHTML porp', async () => {
+ const root = document.createElement('div')
+ const state = ref(false)
+ const Comp = {
+ render: () => {
+ if (state.value) {
+ return h('div', [h('del', null, 'baz')])
+ } else {
+ return h('div', { innerHTML: 'baz' })
+ }
+ },
+ }
+ render(h(Comp), root)
+ expect(root.innerHTML).toBe(`baz
`)
+ state.value = true
+ await nextTick()
+ expect(root.innerHTML).toBe(`baz
`)
+ })
+
test('textContent unmount prev children', () => {
const fn = vi.fn()
const comp = {
diff --git a/packages/runtime-dom/package.json b/packages/runtime-dom/package.json
index 490be0fbd28..49b301b4717 100644
--- a/packages/runtime-dom/package.json
+++ b/packages/runtime-dom/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue/runtime-dom",
- "version": "3.4.31",
+ "version": "3.4.32",
"description": "@vue/runtime-dom",
"main": "index.js",
"module": "dist/runtime-dom.esm-bundler.js",
diff --git a/packages/runtime-dom/src/modules/props.ts b/packages/runtime-dom/src/modules/props.ts
index 2eb83ea39f7..04f0d0e866d 100644
--- a/packages/runtime-dom/src/modules/props.ts
+++ b/packages/runtime-dom/src/modules/props.ts
@@ -10,19 +10,13 @@ export function patchDOMProp(
el: any,
key: string,
value: any,
- // the following args are passed only due to potential innerHTML/textContent
- // overriding existing VNodes, in which case the old tree must be properly
- // unmounted.
- prevChildren: any,
parentComponent: any,
- parentSuspense: any,
- unmountChildren: any,
) {
if (key === 'innerHTML' || key === 'textContent') {
- if (prevChildren) {
- unmountChildren(prevChildren, parentComponent, parentSuspense)
- }
- el[key] = value == null ? '' : value
+ // null value case is handled in renderer patchElement before patching
+ // children
+ if (value === null) return
+ el[key] = value
return
}
diff --git a/packages/runtime-dom/src/patchProp.ts b/packages/runtime-dom/src/patchProp.ts
index e7b733c74af..f3ef14ee83c 100644
--- a/packages/runtime-dom/src/patchProp.ts
+++ b/packages/runtime-dom/src/patchProp.ts
@@ -21,10 +21,7 @@ export const patchProp: DOMRendererOptions['patchProp'] = (
prevValue,
nextValue,
namespace,
- prevChildren,
parentComponent,
- parentSuspense,
- unmountChildren,
) => {
const isSVG = namespace === 'svg'
if (key === 'class') {
@@ -43,15 +40,7 @@ export const patchProp: DOMRendererOptions['patchProp'] = (
? ((key = key.slice(1)), false)
: shouldSetAsProp(el, key, nextValue, isSVG)
) {
- patchDOMProp(
- el,
- key,
- nextValue,
- prevChildren,
- parentComponent,
- parentSuspense,
- unmountChildren,
- )
+ patchDOMProp(el, key, nextValue, parentComponent)
// #6007 also set form state as attributes so they work with
// or libs / extensions that expect attributes
// #11163 custom elements may use value as an prop and set it as object
diff --git a/packages/server-renderer/__tests__/createBuffer.bench.ts b/packages/server-renderer/__tests__/createBuffer.bench.ts
new file mode 100644
index 00000000000..fff20f927fb
--- /dev/null
+++ b/packages/server-renderer/__tests__/createBuffer.bench.ts
@@ -0,0 +1,65 @@
+import { bench, describe } from 'vitest'
+
+import { createBuffer } from '../src/render'
+
+describe('createBuffer', () => {
+ let stringBuffer = createBuffer()
+
+ bench(
+ 'string only',
+ () => {
+ for (let i = 0; i < 10; i += 1) {
+ stringBuffer.push('hello')
+ }
+ },
+ {
+ setup() {
+ stringBuffer = createBuffer()
+ },
+ },
+ )
+
+ let stringNestedBuffer = createBuffer()
+
+ bench(
+ 'string with nested',
+ () => {
+ for (let i = 0; i < 10; i += 1) {
+ if (i % 3 === 0) {
+ stringNestedBuffer.push('hello')
+ } else {
+ const buffer = createBuffer()
+ buffer.push('hello')
+ stringNestedBuffer.push(buffer.getBuffer())
+ }
+ }
+ },
+ {
+ setup() {
+ stringNestedBuffer = createBuffer()
+ },
+ },
+ )
+
+ bench(
+ 'string with nested async',
+ () => {
+ for (let i = 0; i < 10; i += 1) {
+ if (i % 3 === 0) {
+ const buffer = createBuffer()
+ buffer.push('hello')
+ stringNestedBuffer.push(Promise.resolve(buffer.getBuffer()))
+ } else {
+ const buffer = createBuffer()
+ buffer.push('hello')
+ stringNestedBuffer.push(buffer.getBuffer())
+ }
+ }
+ },
+ {
+ setup() {
+ stringNestedBuffer = createBuffer()
+ },
+ },
+ )
+})
diff --git a/packages/server-renderer/__tests__/unrollBuffer.bench.ts b/packages/server-renderer/__tests__/unrollBuffer.bench.ts
new file mode 100644
index 00000000000..b5e03cea602
--- /dev/null
+++ b/packages/server-renderer/__tests__/unrollBuffer.bench.ts
@@ -0,0 +1,74 @@
+import { bench, describe } from 'vitest'
+
+import { type SSRBuffer, createBuffer } from '../src/render'
+import { unrollBuffer } from '../src/renderToString'
+
+function createSyncBuffer(levels: number, itemsPerLevel: number): SSRBuffer {
+ const buffer = createBuffer()
+
+ function addItems(buf: ReturnType, level: number) {
+ for (let i = 1; i <= levels * itemsPerLevel; i++) {
+ buf.push(`sync${level}.${i}`)
+ }
+ if (level < levels) {
+ const subBuffer = createBuffer()
+ addItems(subBuffer, level + 1)
+ buf.push(subBuffer.getBuffer())
+ }
+ }
+
+ addItems(buffer, 1)
+ return buffer.getBuffer()
+}
+
+function createMixedBuffer(levels: number, itemsPerLevel: number): SSRBuffer {
+ const buffer = createBuffer()
+
+ function addItems(buf: ReturnType, level: number) {
+ for (let i = 1; i <= levels * itemsPerLevel; i++) {
+ if (i % 3 === 0) {
+ // @ts-expect-error testing...
+ buf.push(Promise.resolve(`async${level}.${i}`))
+ } else {
+ buf.push(`sync${level}.${i}`)
+ }
+ }
+ if (level < levels) {
+ const subBuffer = createBuffer()
+ addItems(subBuffer, level + 1)
+ buf.push(subBuffer.getBuffer())
+ }
+ }
+
+ addItems(buffer, 1)
+ return buffer.getBuffer()
+}
+
+describe('unrollBuffer', () => {
+ let syncBuffer = createBuffer().getBuffer()
+ let mixedBuffer = createBuffer().getBuffer()
+
+ bench(
+ 'sync',
+ () => {
+ return unrollBuffer(syncBuffer) as any
+ },
+ {
+ setup() {
+ syncBuffer = createSyncBuffer(5, 3)
+ },
+ },
+ )
+
+ bench(
+ 'mixed',
+ () => {
+ return unrollBuffer(mixedBuffer) as any
+ },
+ {
+ setup() {
+ mixedBuffer = createMixedBuffer(5, 3)
+ },
+ },
+ )
+})
diff --git a/packages/server-renderer/package.json b/packages/server-renderer/package.json
index cace5e23b1b..81a3fd1d444 100644
--- a/packages/server-renderer/package.json
+++ b/packages/server-renderer/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue/server-renderer",
- "version": "3.4.31",
+ "version": "3.4.32",
"description": "@vue/server-renderer",
"main": "index.js",
"module": "dist/server-renderer.esm-bundler.js",
diff --git a/packages/server-renderer/src/render.ts b/packages/server-renderer/src/render.ts
index 7e274c3b981..ab84dd212f0 100644
--- a/packages/server-renderer/src/render.ts
+++ b/packages/server-renderer/src/render.ts
@@ -73,9 +73,9 @@ export function createBuffer() {
const isStringItem = isString(item)
if (appendable && isStringItem) {
buffer[buffer.length - 1] += item as string
- } else {
- buffer.push(item)
+ return
}
+ buffer.push(item)
appendable = isStringItem
if (isPromise(item) || (isArray(item) && item.hasAsync)) {
// promise, or child buffer with async, mark as async.
diff --git a/packages/server-renderer/src/renderToString.ts b/packages/server-renderer/src/renderToString.ts
index 0e9299ee834..b931a4d55b8 100644
--- a/packages/server-renderer/src/renderToString.ts
+++ b/packages/server-renderer/src/renderToString.ts
@@ -11,26 +11,46 @@ import { type SSRBuffer, type SSRContext, renderComponentVNode } from './render'
const { isVNode } = ssrUtils
-async function unrollBuffer(buffer: SSRBuffer): Promise {
- if (buffer.hasAsync) {
- let ret = ''
- for (let i = 0; i < buffer.length; i++) {
- let item = buffer[i]
- if (isPromise(item)) {
- item = await item
- }
- if (isString(item)) {
- ret += item
- } else {
- ret += await unrollBuffer(item)
- }
+function nestedUnrollBuffer(
+ buffer: SSRBuffer,
+ parentRet: string,
+ startIndex: number,
+): Promise | string {
+ if (!buffer.hasAsync) {
+ return parentRet + unrollBufferSync(buffer)
+ }
+
+ let ret = parentRet
+ for (let i = startIndex; i < buffer.length; i += 1) {
+ const item = buffer[i]
+ if (isString(item)) {
+ ret += item
+ continue
}
- return ret
- } else {
- // sync buffer can be more efficiently unrolled without unnecessary await
- // ticks
- return unrollBufferSync(buffer)
+
+ if (isPromise(item)) {
+ return item.then(nestedItem => {
+ buffer[i] = nestedItem
+ return nestedUnrollBuffer(buffer, ret, i)
+ })
+ }
+
+ const result = nestedUnrollBuffer(item, ret, 0)
+ if (isPromise(result)) {
+ return result.then(nestedItem => {
+ buffer[i] = nestedItem
+ return nestedUnrollBuffer(buffer, '', i)
+ })
+ }
+
+ ret = result
}
+
+ return ret
+}
+
+export function unrollBuffer(buffer: SSRBuffer): Promise | string {
+ return nestedUnrollBuffer(buffer, '', 0)
}
function unrollBufferSync(buffer: SSRBuffer): string {
diff --git a/packages/sfc-playground/package.json b/packages/sfc-playground/package.json
index 98f2a00128b..f4e50d687a1 100644
--- a/packages/sfc-playground/package.json
+++ b/packages/sfc-playground/package.json
@@ -10,10 +10,10 @@
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.5",
- "vite": "^5.3.1"
+ "vite": "catalog:"
},
"dependencies": {
- "@vue/repl": "^4.2.1",
+ "@vue/repl": "^4.3.1",
"file-saver": "^2.0.5",
"jszip": "^3.10.1",
"vue": "workspace:*"
diff --git a/packages/sfc-playground/src/App.vue b/packages/sfc-playground/src/App.vue
index 4b62519ce6e..7501b200ce8 100644
--- a/packages/sfc-playground/src/App.vue
+++ b/packages/sfc-playground/src/App.vue
@@ -129,7 +129,13 @@ onMounted(() => {
:preview-options="{
customCode: {
importCode: `import { initCustomFormatter } from 'vue'`,
- useCode: `initCustomFormatter()`,
+ useCode: `if (window.devtoolsFormatters) {
+ const index = window.devtoolsFormatters.findIndex((v) => v.__vue_custom_formatter)
+ window.devtoolsFormatters.splice(index, 1)
+ initCustomFormatter()
+ } else {
+ initCustomFormatter()
+ }`,
},
}"
/>
diff --git a/packages/sfc-playground/src/download/template/package.json b/packages/sfc-playground/src/download/template/package.json
index 540a5087cf2..9cce6418897 100644
--- a/packages/sfc-playground/src/download/template/package.json
+++ b/packages/sfc-playground/src/download/template/package.json
@@ -12,6 +12,6 @@
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.5",
- "vite": "^5.3.1"
+ "vite": "^5.3.3"
}
}
diff --git a/packages/sfc-playground/vite.config.ts b/packages/sfc-playground/vite.config.ts
index e30078f4b79..ee9bbd4abf4 100644
--- a/packages/sfc-playground/vite.config.ts
+++ b/packages/sfc-playground/vite.config.ts
@@ -2,9 +2,11 @@ import fs from 'node:fs'
import path from 'node:path'
import { type Plugin, defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
-import { execaSync } from 'execa'
+import { spawnSync } from 'node:child_process'
-const commit = execaSync('git', ['rev-parse', '--short=7', 'HEAD']).stdout
+const commit = spawnSync('git', ['rev-parse', '--short=7', 'HEAD'])
+ .stdout.toString()
+ .trim()
export default defineConfig({
plugins: [
diff --git a/packages/shared/package.json b/packages/shared/package.json
index a332a6ac4e0..c60f0873017 100644
--- a/packages/shared/package.json
+++ b/packages/shared/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue/shared",
- "version": "3.4.31",
+ "version": "3.4.32",
"description": "internal utils shared across @vue packages",
"main": "index.js",
"module": "dist/shared.esm-bundler.js",
diff --git a/packages/vue-compat/package.json b/packages/vue-compat/package.json
index 1745f1424e0..6503406872b 100644
--- a/packages/vue-compat/package.json
+++ b/packages/vue-compat/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue/compat",
- "version": "3.4.31",
+ "version": "3.4.32",
"description": "Vue 3 compatibility build for Vue 2",
"main": "index.js",
"module": "dist/vue.runtime.esm-bundler.js",
@@ -52,9 +52,9 @@
},
"homepage": "https://github.com/vuejs/core/tree/main/packages/vue-compat#readme",
"dependencies": {
- "@babel/parser": "^7.24.7",
- "estree-walker": "^2.0.2",
- "source-map-js": "^1.2.0"
+ "@babel/parser": "catalog:",
+ "estree-walker": "catalog:",
+ "source-map-js": "catalog:"
},
"peerDependencies": {
"vue": "workspace:*"
diff --git a/packages/vue/README.md b/packages/vue/README.md
index 2aca524e03d..ae9eb402a51 100644
--- a/packages/vue/README.md
+++ b/packages/vue/README.md
@@ -14,7 +14,7 @@
- Contains hard-coded prod/dev branches, and the prod build is pre-minified. Use the `*.prod.js` files for production.
- **`vue(.runtime).esm-browser(.prod).js`**:
- - For usage via native ES modules imports (in browser via `