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', () => { `
1

3

1

3

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( - `
1
`, - ) + await nextTick() + expect(container.innerHTML).toBe( + `
1
`, + ) - 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( - `
foo
1
`, - ) - __DEV__ = true + await nextTick() + expect(container.innerHTML).toBe( + `
foo
1
`, + ) + } 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('
bar
') + expect(block).toBe(null) + + show.value = false + await nextTick() + expect(inner(root)).toBe('
foo
') + + show.value = true + await nextTick() + expect(inner(root)).toBe('
bar
') + }) + + 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( + '
Child
', + ) + + show.value = false + await nextTick() + expect(inner(root)).toBe('') + expect(spyUnmounted).toHaveBeenCalledTimes(1) + + show.value = true + await nextTick() + expect(inner(root)).toBe( + '
Child
', + ) + + // 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 `